diff --git a/src/OneScript.Core/Contexts/BslParameterInfo.cs b/src/OneScript.Core/Contexts/BslParameterInfo.cs index 893af58cb..e9f2c17b6 100644 --- a/src/OneScript.Core/Contexts/BslParameterInfo.cs +++ b/src/OneScript.Core/Contexts/BslParameterInfo.cs @@ -23,6 +23,8 @@ internal BslParameterInfo() AttrsImpl = ParameterAttributes.In; ClassImpl = typeof(BslValue); } + + public static BslParameterInfo Create() => new BslParameterInfo(); #region Attributes Infrastructure @@ -99,6 +101,22 @@ internal void SetDefaultValue(BslPrimitiveValue val) AttrsImpl |= ParameterAttributes.HasDefault | ParameterAttributes.Optional; } + public void SetDefaultValueIndex(int index) + { + ConstantValueIndex = index; + AttrsImpl |= ParameterAttributes.HasDefault | ParameterAttributes.Optional; + } + + public void SetByValue(bool byVal) + { + ExplicitByVal = byVal; + } + + public void SetPosition(int position) + { + PositionImpl = position; + } + internal void SetOwner(MemberInfo parent) { MemberImpl = parent; diff --git a/src/OneScript.Core/Contexts/BslScriptFieldInfo.cs b/src/OneScript.Core/Contexts/BslScriptFieldInfo.cs index 9cc7ca115..4868fe6d3 100644 --- a/src/OneScript.Core/Contexts/BslScriptFieldInfo.cs +++ b/src/OneScript.Core/Contexts/BslScriptFieldInfo.cs @@ -31,6 +31,8 @@ internal BslScriptFieldInfo(string name) _name = name; } + public static BslScriptFieldInfo Create(string name = null) => new BslScriptFieldInfo(name); + public override Type DeclaringType => _declaringType; public override string Name => _name; public override Type ReflectedType => _declaringType; diff --git a/src/OneScript.Core/Contexts/Internal/IBuildableMember.cs b/src/OneScript.Core/Contexts/Internal/IBuildableMember.cs index 49341fbe9..762377a7b 100644 --- a/src/OneScript.Core/Contexts/Internal/IBuildableMember.cs +++ b/src/OneScript.Core/Contexts/Internal/IBuildableMember.cs @@ -10,7 +10,7 @@ This Source Code Form is subject to the terms of the namespace OneScript.Contexts.Internal { - internal interface IBuildableMember + public interface IBuildableMember { void SetDeclaringType(Type type); void SetName(string name); @@ -21,12 +21,12 @@ internal interface IBuildableMember void SetDispatchIndex(int index); } - internal interface IBuildableMethod : IBuildableMember + public interface IBuildableMethod : IBuildableMember { void SetParameters(IEnumerable parameters); } - internal interface IBuildableProperty : IBuildableMember + public interface IBuildableProperty : IBuildableMember { void CanRead(bool canRead); void CanWrite(bool canWrite); diff --git a/src/OneScript.Language/Sources/SourceCode.cs b/src/OneScript.Language/Sources/SourceCode.cs index 48c10007f..a936bbb47 100644 --- a/src/OneScript.Language/Sources/SourceCode.cs +++ b/src/OneScript.Language/Sources/SourceCode.cs @@ -18,11 +18,12 @@ public class SourceCode : ISourceCodeIndexer private string _code = null; - internal SourceCode(string sourceName, ICodeSource source, string ownerPackageId = null) + internal SourceCode(string sourceName, ICodeSource source, string ownerPackageId = null, bool isCompiled = false) { _source = source; Name = sourceName; OwnerPackageId = ownerPackageId; + IsCompiled = isCompiled; } public SourceCodeIterator CreateIterator() @@ -41,6 +42,11 @@ public SourceCodeIterator CreateIterator() /// Идентификатор пакета-владельца. null если модуль не принадлежит библиотеке. /// public string OwnerPackageId { get; } + + /// + /// Признак того, что модуль загружен из скомпилированного кода (.osc, .oslib) + /// + public bool IsCompiled { get; } public string GetSourceCode() { diff --git a/src/OneScript.Language/Sources/SourceCodeBuilder.cs b/src/OneScript.Language/Sources/SourceCodeBuilder.cs index fa307c2d1..dee929b55 100644 --- a/src/OneScript.Language/Sources/SourceCodeBuilder.cs +++ b/src/OneScript.Language/Sources/SourceCodeBuilder.cs @@ -16,6 +16,7 @@ public class SourceCodeBuilder private ICodeSource _source; private string _moduleName; private string _ownerPackageId; + private bool _isCompiled; private SourceCodeBuilder() { @@ -41,6 +42,15 @@ public SourceCodeBuilder WithOwnerPackage(string packageId) _ownerPackageId = packageId; return this; } + + /// + /// Помечает исходный код как загруженный из скомпилированного файла. + /// + public SourceCodeBuilder AsCompiled() + { + _isCompiled = true; + return this; + } public SourceCode Build() { @@ -50,7 +60,7 @@ public SourceCode Build() if (_moduleName == default) _moduleName = _source.Location; - return new SourceCode(_moduleName, _source, _ownerPackageId); + return new SourceCode(_moduleName, _source, _ownerPackageId, _isCompiled); } public static SourceCodeBuilder Create() => new SourceCodeBuilder(); diff --git a/src/ScriptEngine.HostedScript/FileSystemDependencyResolver.cs b/src/ScriptEngine.HostedScript/FileSystemDependencyResolver.cs index abf504ca8..61131ff64 100644 --- a/src/ScriptEngine.HostedScript/FileSystemDependencyResolver.cs +++ b/src/ScriptEngine.HostedScript/FileSystemDependencyResolver.cs @@ -15,12 +15,15 @@ This Source Code Form is subject to the terms of the using OneScript.Execution; using OneScript.Localization; using OneScript.Sources; +using ScriptEngine.Compiler.Packaged; namespace ScriptEngine.HostedScript { public class FileSystemDependencyResolver : IDependencyResolver { public const string PREDEFINED_LOADER_FILE = "package-loader.os"; + public const string COMPILED_LIBRARY_EXTENSION = ".oslib"; + private readonly List _libs = new List(); private LibraryLoader _defaultLoader; private object _defaultLoaderLocker = new object(); @@ -94,17 +97,31 @@ public PackageInfo Resolve(SourceCode module, string libraryName, IBslProcess pr private PackageInfo LoadByName(string libraryName, IBslProcess process) { + // Сначала ищем скомпилированную библиотеку (.oslib) foreach (var path in SearchDirectories) { - if(!Directory.Exists(path)) + if (!Directory.Exists(path)) continue; + var oslibPath = Path.Combine(path, libraryName + COMPILED_LIBRARY_EXTENSION); + if (File.Exists(oslibPath)) + { + return LoadCompiledLibrary(oslibPath, process); + } + var libraryPath = Path.Combine(path, libraryName); - var loadAttempt = LoadByPath(libraryPath, process); + var loadAttempt = LoadByPath(libraryPath, process); if (loadAttempt != null) return loadAttempt; } + // Проверяем в корневой папке + var rootOslibPath = Path.Combine(LibraryRoot, libraryName + COMPILED_LIBRARY_EXTENSION); + if (File.Exists(rootOslibPath)) + { + return LoadCompiledLibrary(rootOslibPath, process); + } + var rootPath = Path.Combine(LibraryRoot, libraryName); return LoadByPath(rootPath, process); } @@ -130,6 +147,13 @@ private PackageInfo LoadByRelativePath(SourceCode module, string libraryPath, IB realPath = libraryPath; } + // Сначала проверяем скомпилированную библиотеку (.oslib) + var oslibPath = realPath + COMPILED_LIBRARY_EXTENSION; + if (File.Exists(oslibPath)) + { + return LoadCompiledLibrary(oslibPath, process); + } + return LoadByPath(realPath, process); } @@ -277,6 +301,60 @@ private static string GetLibraryId(string libraryPath) { return Path.GetFullPath(libraryPath); } + + private PackageInfo LoadCompiledLibrary(string oslibPath, IBslProcess process) + { + var id = GetLibraryId(oslibPath); + var existedLib = _libs.FirstOrDefault(x => x.id == id); + if (existedLib != null) + { + if (existedLib.state == ProcessingState.Discovered) + { + string libStack = ListToStringStack(_libs, id); + throw new DependencyResolveException( + new BilingualString( + $"Ошибка загрузки библиотеки {id}. Обнаружены циклические зависимости.\n", + $"Error loading library {id}. Circular dependencies found.\n") + libStack); + } + + return existedLib.loadingResult; + } + + var newLib = new Library { id = id, state = ProcessingState.Discovered }; + int newLibIndex = _libs.Count; + + try + { + _libs.Add(newLib); + + var loader = new Compiler.Packaged.LibraryLoader(Engine); + var loadedLib = loader.LoadFromFile(oslibPath, process); + + // Сначала загружаем зависимости + foreach (var dep in loadedLib.Dependencies) + { + var depPackage = LoadByName(dep, process); + if (depPackage == null) + { + throw new DependencyResolveException( + new BilingualString( + $"Не найдена зависимость '{dep}' для библиотеки '{loadedLib.Name}'", + $"Dependency '{dep}' not found for library '{loadedLib.Name}'")); + } + } + + var package = new PackageInfo(oslibPath, loadedLib.Name); + newLib.state = ProcessingState.Processed; + newLib.loadingResult = package; + + return package; + } + catch (Exception) + { + _libs.RemoveAt(newLibIndex); + throw; + } + } private static bool PathHasInvalidChars(string path) { diff --git a/src/ScriptEngine.HostedScript/LibraryLoader.cs b/src/ScriptEngine.HostedScript/LibraryLoader.cs index adc76a06b..15587691d 100644 --- a/src/ScriptEngine.HostedScript/LibraryLoader.cs +++ b/src/ScriptEngine.HostedScript/LibraryLoader.cs @@ -287,5 +287,39 @@ public static void TraceLoadLibrary(string message) SystemLogger.Write("LRE: " + message); } } + + /// + /// Загружает библиотеку и возвращает информацию о ней с скомпилированными модулями. + /// Используется для сборки .oslib файлов. + /// + public ExternalLibraryInfo LoadLibraryWithInfo(string libraryPath, IBslProcess process) + { + var package = new PackageInfo(libraryPath, Path.GetFileName(libraryPath)); + var library = new ExternalLibraryInfo(package); + _librariesInProgress.Push(new LibraryLoadingContext(library)); + try + { + bool success; + if (!_customized) + { + success = DefaultProcessing(libraryPath, process); + } + else + { + success = CustomizedProcessing(libraryPath, process); + } + + if (!success) + return null; + + CompileDelayedModules(library, process); + + return library; + } + finally + { + _librariesInProgress.Pop(); + } + } } } diff --git a/src/ScriptEngine/Compiler/Packaged/AnnotationDto.cs b/src/ScriptEngine/Compiler/Packaged/AnnotationDto.cs new file mode 100644 index 000000000..ea49a0d3b --- /dev/null +++ b/src/ScriptEngine/Compiler/Packaged/AnnotationDto.cs @@ -0,0 +1,38 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System.Collections.Generic; +using MessagePack; + +namespace ScriptEngine.Compiler.Packaged +{ + /// + /// DTO для сериализации аннотации + /// + [MessagePackObject] + public class AnnotationDto + { + [Key(0)] + public string Name { get; set; } + + [Key(1)] + public List Parameters { get; set; } + } + + /// + /// DTO для сериализации параметра аннотации + /// + [MessagePackObject] + public class AnnotationParameterDto + { + [Key(0)] + public string Name { get; set; } + + [Key(1)] + public string Value { get; set; } + } +} diff --git a/src/ScriptEngine/Compiler/Packaged/BundleBuilder.cs b/src/ScriptEngine/Compiler/Packaged/BundleBuilder.cs new file mode 100644 index 000000000..864b07911 --- /dev/null +++ b/src/ScriptEngine/Compiler/Packaged/BundleBuilder.cs @@ -0,0 +1,199 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using MessagePack; +using OneScript.Compilation; +using OneScript.Contexts; +using OneScript.Execution; +using OneScript.Sources; +using ScriptEngine.Libraries; +using ScriptEngine.Machine; +using ScriptEngine.Machine.Contexts; + +namespace ScriptEngine.Compiler.Packaged +{ + /// + /// Построитель бандла — собирает все зависимости в один пакет + /// + public class BundleBuilder + { + private readonly ScriptingEngine _engine; + private readonly ICompilerFrontend _compiler; + private readonly CompiledModulePackager _packager; + private readonly HashSet _collectedSources; + private int _loadOrder; + + public BundleBuilder(ScriptingEngine engine, ICompilerFrontend compiler) + { + _engine = engine; + _compiler = compiler; + _packager = new CompiledModulePackager(); + _collectedSources = new HashSet(StringComparer.OrdinalIgnoreCase); + _loadOrder = 0; + } + + /// + /// Собирает бандл из главного скрипта + /// + public CompiledPackageDto Build(SourceCode entrySource, IBslProcess process) + { + var package = new CompiledPackageDto + { + Type = PackageType.Bundle, + Name = Path.GetFileNameWithoutExtension(entrySource.Location ?? "bundle") + }; + + // Компилируем главный модуль — это запустит загрузку всех зависимостей + var entryModule = _compiler.Compile(entrySource, process); + + if (!(entryModule is StackRuntimeModule stackModule)) + { + throw new InvalidOperationException( + "Only stack runtime modules can be bundled. Native modules are not supported."); + } + + // Собираем все загруженные модули библиотек из окружения + // и строим маппинг контекстов на символьные имена + var contextSymbols = new Dictionary(); + CollectLibraryModules(package, contextSymbols); + + // Передаём маппинг в packager для правильной сериализации bindings + _packager.SetContextSymbols(contextSymbols); + + // Добавляем главный модуль последним (он зависит от библиотек) + var entryDto = new PackagedScriptDto + { + Type = ScriptType.Entry, + Symbol = null, + OwnerLibrary = null, + LoadOrder = _loadOrder++, + Module = _packager.ConvertToDto(stackModule) + }; + package.Scripts.Add(entryDto); + + return package; + } + + /// + /// Сохраняет бандл в поток + /// + public void Save(Stream stream, CompiledPackageDto package) + { + MessagePackSerializer.Serialize(stream, package); + } + + /// + /// Сохраняет бандл в массив байт + /// + public byte[] Save(CompiledPackageDto package) + { + return MessagePackSerializer.Serialize(package); + } + + private void CollectLibraryModules(CompiledPackageDto package, Dictionary contextSymbols) + { + var env = _engine.Environment; + + // Проходим по всем присоединённым контекстам + foreach (var context in env.AttachedContexts) + { + // Ищем UserScriptContextInstance — это загруженные модули библиотек + if (context is UserScriptContextInstance userScript) + { + TryCollectUserScript(userScript, package, null, contextSymbols); + } + + // PropertyBag содержит глобальные свойства, включая модули + if (context is PropertyBag propertyBag) + { + CollectFromPropertyBag(propertyBag, package, contextSymbols); + } + } + } + + private void CollectFromPropertyBag(PropertyBag propertyBag, CompiledPackageDto package, Dictionary contextSymbols) + { + for (int i = 0; i < propertyBag.Count; i++) + { + var value = propertyBag.GetPropValue(i); + if (value is UserScriptContextInstance userScript) + { + var symbol = propertyBag.GetPropName(i); + TryCollectUserScript(userScript, package, symbol, contextSymbols); + } + } + } + + private void TryCollectUserScript( + UserScriptContextInstance userScript, + CompiledPackageDto package, + string knownSymbol, + Dictionary contextSymbols) + { + var module = userScript.Module; + if (!(module is StackRuntimeModule stackModule)) + return; + + var sourceLocation = stackModule.Source?.Location; + + // Пропускаем если уже собрали (по пути к исходнику) + if (!string.IsNullOrEmpty(sourceLocation)) + { + if (_collectedSources.Contains(sourceLocation)) + return; + _collectedSources.Add(sourceLocation); + } + + // Определяем символьное имя + var symbol = knownSymbol ?? FindModuleSymbol(userScript); + + // Добавляем в маппинг контекстов + if (!string.IsNullOrEmpty(symbol)) + { + contextSymbols[userScript] = symbol; + } + + var scriptDto = new PackagedScriptDto + { + Type = ScriptType.Module, + Symbol = symbol, + OwnerLibrary = null, // TODO: определить библиотеку-владельца + LoadOrder = _loadOrder++, + Module = _packager.ConvertToDto(stackModule) + }; + + package.Scripts.Add(scriptDto); + } + + private string FindModuleSymbol(UserScriptContextInstance userScript) + { + var env = _engine.Environment; + + // Ищем в присоединённых контекстах + foreach (var context in env.AttachedContexts) + { + if (context is PropertyBag propertyBag) + { + for (int i = 0; i < propertyBag.Count; i++) + { + var value = propertyBag.GetPropValue(i); + if (ReferenceEquals(value, userScript)) + { + return propertyBag.GetPropName(i); + } + } + } + } + + return null; + } + } +} diff --git a/src/ScriptEngine/Compiler/Packaged/BundleLoader.cs b/src/ScriptEngine/Compiler/Packaged/BundleLoader.cs new file mode 100644 index 000000000..16cc8b0fc --- /dev/null +++ b/src/ScriptEngine/Compiler/Packaged/BundleLoader.cs @@ -0,0 +1,206 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using MessagePack; +using OneScript.Contexts; +using OneScript.Execution; +using ScriptEngine.Machine; +using ScriptEngine.Machine.Contexts; + +namespace ScriptEngine.Compiler.Packaged +{ + /// + /// Загрузчик бандлов — восстанавливает все модули из пакета + /// + public class BundleLoader + { + private readonly ScriptingEngine _engine; + private readonly CompiledModulePackager _packager; + private readonly Dictionary _loadedModules; + private string _bundlePath; + + public BundleLoader(ScriptingEngine engine) + { + _engine = engine; + _packager = new CompiledModulePackager(); + _loadedModules = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + /// + /// Загружает бандл из потока + /// + public LoadedBundle Load(Stream stream) + { + var package = MessagePackSerializer.Deserialize(stream); + return LoadPackage(package); + } + + /// + /// Загружает бандл из массива байт + /// + public LoadedBundle Load(byte[] data) + { + var package = MessagePackSerializer.Deserialize(data); + return LoadPackage(package); + } + + /// + /// Загружает бандл из файла + /// + public LoadedBundle LoadFromFile(string path) + { + _bundlePath = Path.GetFullPath(path); + using (var stream = File.OpenRead(path)) + { + return Load(stream); + } + } + + private LoadedBundle LoadPackage(CompiledPackageDto package) + { + ValidatePackage(package); + + var result = new LoadedBundle + { + Name = package.Name, + Type = package.Type + }; + + // Загружаем скрипты в порядке LoadOrder + var orderedScripts = package.Scripts + .OrderBy(s => s.LoadOrder) + .ToList(); + + // Сначала загружаем все модули библиотек (без entry) + foreach (var scriptDto in orderedScripts.Where(s => s.Type != ScriptType.Entry)) + { + LoadLibraryScript(scriptDto, result); + } + + // Теперь загружаем entry module (он может ссылаться на модули библиотек) + var entryScript = orderedScripts.FirstOrDefault(s => s.Type == ScriptType.Entry); + if (entryScript != null) + { + LoadEntryScript(entryScript, result); + } + + return result; + } + + private void LoadLibraryScript(PackagedScriptDto scriptDto, LoadedBundle result) + { + // Для модулей библиотек используем расширенный lookup, включающий уже загруженные модули + var env = _engine.Environment; + // Для бандла все модули ссылаются на .osc файл + var module = _packager.ConvertFromDto(scriptDto.Module, env, _bundlePath); + + switch (scriptDto.Type) + { + case ScriptType.Module: + var instance = RegisterModule(scriptDto.Symbol, module); + if (instance != null) + { + result.Modules[scriptDto.Symbol] = module; + // Запоминаем для последующего использования в lookup + _loadedModules[scriptDto.Symbol] = instance; + } + break; + + case ScriptType.Class: + RegisterClass(scriptDto.Symbol, module); + result.Classes[scriptDto.Symbol] = module; + break; + } + } + + private void LoadEntryScript(PackagedScriptDto scriptDto, LoadedBundle result) + { + // Entry module загружается с учётом всех уже загруженных модулей + var env = _engine.Environment; + // Для бандла entry module тоже ссылается на .osc файл + var module = _packager.ConvertFromDto(scriptDto.Module, env, _bundlePath); + result.EntryModule = module; + } + + private void ValidatePackage(CompiledPackageDto package) + { + if (package.MagicHeader != CompiledPackageDto.Magic) + { + throw new InvalidOperationException("Invalid compiled package format"); + } + + if (package.Version > CompiledPackageDto.FormatVersion) + { + throw new InvalidOperationException($"Unsupported package version: {package.Version}"); + } + + if (package.Type == PackageType.Library && package.Dependencies.Count > 0) + { + // Для библиотек с зависимостями нужно сначала загрузить зависимости + // TODO: Реализовать для .oslib + throw new NotSupportedException( + "Libraries with external dependencies are not yet supported. Use bundle format."); + } + } + + private UserScriptContextInstance RegisterModule(string symbol, StackRuntimeModule module) + { + if (string.IsNullOrEmpty(symbol)) + return null; + + var process = _engine.NewProcess(); + var instance = _engine.CreateUninitializedSDO(module); + + // Регистрируем как глобальное свойство + _engine.Environment.InjectGlobalProperty(instance, symbol, symbol, true); + + // Инициализируем модуль + _engine.InitializeSDO(instance, process); + + return instance; + } + + private void RegisterClass(string symbol, StackRuntimeModule module) + { + if (string.IsNullOrEmpty(symbol)) + return; + + // Регистрируем класс через фабрику + _engine.AttachedScriptsFactory?.RegisterTypeModule(symbol, module); + } + } + + /// + /// Результат загрузки бандла + /// + public class LoadedBundle + { + public string Name { get; set; } + public PackageType Type { get; set; } + + /// + /// Главный модуль (точка входа) + /// + public StackRuntimeModule EntryModule { get; set; } + + /// + /// Загруженные модули библиотек + /// + public Dictionary Modules { get; } = + new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Загруженные классы библиотек + /// + public Dictionary Classes { get; } = + new Dictionary(StringComparer.OrdinalIgnoreCase); + } +} diff --git a/src/ScriptEngine/Compiler/Packaged/CommandDto.cs b/src/ScriptEngine/Compiler/Packaged/CommandDto.cs new file mode 100644 index 000000000..9e7c6bf15 --- /dev/null +++ b/src/ScriptEngine/Compiler/Packaged/CommandDto.cs @@ -0,0 +1,24 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using MessagePack; + +namespace ScriptEngine.Compiler.Packaged +{ + /// + /// DTO для сериализации команды байт-кода + /// + [MessagePackObject] + public class CommandDto + { + [Key(0)] + public int Code { get; set; } + + [Key(1)] + public int Argument { get; set; } + } +} diff --git a/src/ScriptEngine/Compiler/Packaged/CompiledModuleDto.cs b/src/ScriptEngine/Compiler/Packaged/CompiledModuleDto.cs new file mode 100644 index 000000000..f6204aa15 --- /dev/null +++ b/src/ScriptEngine/Compiler/Packaged/CompiledModuleDto.cs @@ -0,0 +1,58 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System.Collections.Generic; +using MessagePack; + +namespace ScriptEngine.Compiler.Packaged +{ + /// + /// DTO для сериализации скомпилированного модуля + /// + [MessagePackObject] + public class CompiledModuleDto + { + public const int FormatVersion = 1; + public const string Magic = "OSC1"; + + [Key(0)] + public string MagicHeader { get; set; } = Magic; + + [Key(1)] + public int Version { get; set; } = FormatVersion; + + [Key(2)] + public List Constants { get; set; } = new List(); + + [Key(3)] + public List Identifiers { get; set; } = new List(); + + [Key(4)] + public List Code { get; set; } = new List(); + + [Key(5)] + public List Methods { get; set; } = new List(); + + [Key(6)] + public List Fields { get; set; } = new List(); + + [Key(7)] + public List VariableRefs { get; set; } = new List(); + + [Key(8)] + public List MethodRefs { get; set; } = new List(); + + [Key(9)] + public List ModuleAttributes { get; set; } = new List(); + + [Key(10)] + public int EntryMethodIndex { get; set; } = -1; + + [Key(11)] + public string SourceFileName { get; set; } + } +} diff --git a/src/ScriptEngine/Compiler/Packaged/CompiledModulePackager.cs b/src/ScriptEngine/Compiler/Packaged/CompiledModulePackager.cs new file mode 100644 index 000000000..5e280a979 --- /dev/null +++ b/src/ScriptEngine/Compiler/Packaged/CompiledModulePackager.cs @@ -0,0 +1,605 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using MessagePack; +using OneScript.Compilation.Binding; +using OneScript.Contexts; +using OneScript.Contexts.Internal; +using OneScript.Language.Sources; +using OneScript.Sources; +using OneScript.Types; +using OneScript.Values; +using ScriptEngine.Machine; +using ScriptEngine.Machine.Contexts; + +namespace ScriptEngine.Compiler.Packaged +{ + /// + /// Сериализация и десериализация скомпилированных модулей + /// + public class CompiledModulePackager + { + private Dictionary _contextSymbols; + + /// + /// Устанавливает маппинг контекстов на их символьные имена. + /// Используется при сборке бандла. + /// + public void SetContextSymbols(Dictionary symbols) + { + _contextSymbols = symbols; + } + + /// + /// Сериализует модуль в поток + /// + public void Save(Stream stream, StackRuntimeModule module) + { + var dto = ConvertToDto(module); + MessagePackSerializer.Serialize(stream, dto); + } + + /// + /// Сериализует модуль в массив байт + /// + public byte[] Save(StackRuntimeModule module) + { + var dto = ConvertToDto(module); + return MessagePackSerializer.Serialize(dto); + } + + /// + /// Десериализует модуль из потока + /// + public StackRuntimeModule Load(Stream stream, IRuntimeEnvironment environment) + { + var dto = MessagePackSerializer.Deserialize(stream); + return ConvertFromDto(dto, environment); + } + + /// + /// Десериализует модуль из массива байт + /// + public StackRuntimeModule Load(byte[] data, IRuntimeEnvironment environment) + { + var dto = MessagePackSerializer.Deserialize(data); + return ConvertFromDto(dto, environment); + } + + /// + /// Конвертирует модуль в DTO (используется также BundleBuilder) + /// + public CompiledModuleDto ConvertToDto(StackRuntimeModule module) + { + var dto = new CompiledModuleDto + { + EntryMethodIndex = module.EntryMethodIndex, + SourceFileName = module.Source?.Location + }; + + // Constants + foreach (var constant in module.Constants) + { + dto.Constants.Add(ConvertConstant(constant)); + } + + // Identifiers + dto.Identifiers.AddRange(module.Identifiers); + + // Code + foreach (var cmd in module.Code) + { + dto.Code.Add(new CommandDto { Code = (int)cmd.Code, Argument = cmd.Argument }); + } + + // Methods + foreach (var method in module.Methods.Cast()) + { + dto.Methods.Add(ConvertMethod(method)); + } + + // Fields + foreach (var field in module.Fields.Cast()) + { + dto.Fields.Add(ConvertField(field)); + } + + // Variable refs + foreach (var binding in module.VariableRefs) + { + dto.VariableRefs.Add(ConvertBinding(binding)); + } + + // Method refs + foreach (var binding in module.MethodRefs) + { + dto.MethodRefs.Add(ConvertBinding(binding)); + } + + // Module attributes + foreach (var attr in module.ModuleAttributes) + { + dto.ModuleAttributes.Add(ConvertAnnotationAttribute(attr)); + } + + return dto; + } + + private ConstantDto ConvertConstant(BslPrimitiveValue value) + { + var dto = new ConstantDto(); + + switch (value) + { + case BslUndefinedValue _: + dto.Type = ConstantType.Undefined; + break; + case BslNullValue _: + dto.Type = ConstantType.Null; + break; + case BslStringValue str: + dto.Type = ConstantType.String; + dto.StringValue = (string)str; + break; + case BslNumericValue num: + dto.Type = ConstantType.Number; + dto.NumberValue = (decimal)num; + break; + case BslBooleanValue b: + dto.Type = ConstantType.Boolean; + dto.BoolValue = (bool)b; + break; + case BslDateValue date: + dto.Type = ConstantType.Date; + dto.DateTicks = ((DateTime)date).Ticks; + break; + default: + throw new InvalidOperationException($"Unknown constant type: {value?.GetType().Name}"); + } + + return dto; + } + + private MethodDto ConvertMethod(MachineMethodInfo method) + { + var runtime = method.GetRuntimeMethod(); + var sig = runtime.Signature; + + var dto = new MethodDto + { + Name = sig.Name, + Alias = sig.Alias, + IsFunction = sig.IsFunction, + IsExport = sig.IsExport, + IsAsync = sig.IsAsync, + IsDeprecated = sig.IsDeprecated, + ThrowOnUseDeprecated = sig.ThrowOnUseDeprecated, + EntryPoint = runtime.EntryPoint, + LocalVariables = runtime.LocalVariables?.ToList() ?? new List() + }; + + if (sig.Params != null) + { + foreach (var param in sig.Params) + { + dto.Parameters.Add(ConvertParameter(param)); + } + } + + if (sig.Annotations != null) + { + foreach (var anno in sig.Annotations) + { + dto.Annotations.Add(ConvertAnnotation(anno)); + } + } + + return dto; + } + + private ParameterDto ConvertParameter(ParameterDefinition param) + { + var dto = new ParameterDto + { + Name = param.Name, + IsByValue = param.IsByValue, + HasDefaultValue = param.HasDefaultValue, + DefaultValueIndex = param.DefaultValueIndex + }; + + if (param.Annotations != null) + { + dto.Annotations = param.Annotations.Select(ConvertAnnotation).ToList(); + } + + return dto; + } + + private AnnotationDto ConvertAnnotation(AnnotationDefinition anno) + { + var dto = new AnnotationDto { Name = anno.Name }; + + if (anno.Parameters != null) + { + dto.Parameters = anno.Parameters.Select(p => new AnnotationParameterDto + { + Name = p.Name, + Value = p.RuntimeValue?.ToString() + }).ToList(); + } + + return dto; + } + + private AnnotationDto ConvertAnnotationAttribute(BslAnnotationAttribute attr) + { + var dto = new AnnotationDto { Name = attr.Name }; + + if (attr.Parameters != null && attr.Parameters.Any()) + { + dto.Parameters = attr.Parameters.Select(p => new AnnotationParameterDto + { + Name = p.Name, + Value = p.Value?.ToString() + }).ToList(); + } + + return dto; + } + + private FieldDto ConvertField(BslScriptFieldInfo field) + { + var dto = new FieldDto + { + Name = field.Name, + IsExport = field.IsPublic, + DispatchId = field.DispatchId + }; + + var annotations = field.GetAnnotations(); + if (annotations != null && annotations.Any()) + { + dto.Annotations = annotations.Select(ConvertAnnotationAttribute).ToList(); + } + + return dto; + } + + private SymbolBindingDto ConvertBinding(ModuleSymbolBinding binding) + { + var dto = new SymbolBindingDto + { + Kind = binding.Kind, + MemberNumber = binding.MemberNumber, + ScopeIndex = binding.ScopeIndex + }; + + if (binding.Kind == ScopeBindingKind.Static && binding.Target != null) + { + // Сохраняем имя контекста для восстановления при загрузке + dto.ContextName = GetContextIdentifier(binding.Target); + + // Для PropertyBag сохраняем также имя свойства + if (binding.Target is PropertyBag propertyBag) + { + dto.MemberName = propertyBag.GetPropName(binding.MemberNumber); + } + } + + return dto; + } + + private string GetContextIdentifier(IAttachableContext context) + { + // Для пользовательских модулей используем специальный префикс + символьное имя + if (context is UserScriptContextInstance) + { + // Пытаемся найти символьное имя модуля + if (_contextSymbols != null && _contextSymbols.TryGetValue(context, out var symbol)) + { + return "$UserModule:" + symbol; + } + } + + // Для системных контекстов используем полное имя типа + return context.GetType().FullName; + } + + /// + /// Конвертирует DTO обратно в модуль (используется также BundleLoader) + /// + public StackRuntimeModule ConvertFromDto(CompiledModuleDto dto, IRuntimeEnvironment environment) + { + return ConvertFromDto(dto, environment, null); + } + + /// + /// Конвертирует DTO обратно в модуль с возможностью переопределить путь к источнику. + /// Используется для бандлов, где все модули должны ссылаться на .osc файл. + /// + /// DTO модуля + /// Окружение выполнения + /// Путь к скомпилированному файлу (для бандлов — путь к .osc) + public StackRuntimeModule ConvertFromDto(CompiledModuleDto dto, IRuntimeEnvironment environment, string overrideSourcePath) + { + if (dto.MagicHeader != CompiledModuleDto.Magic) + { + throw new InvalidOperationException("Invalid compiled module format"); + } + + if (dto.Version > CompiledModuleDto.FormatVersion) + { + throw new InvalidOperationException($"Unsupported module version: {dto.Version}"); + } + + var module = new StackRuntimeModule(typeof(Machine.Contexts.UserScriptContextInstance)) + { + EntryMethodIndex = dto.EntryMethodIndex + }; + + // Создаём SourceCode для модуля + // Для бандлов используем путь к .osc файлу (overrideSourcePath) + // Для библиотек используем оригинальный путь (dto.SourceFileName) + var sourcePath = overrideSourcePath ?? dto.SourceFileName; + if (!string.IsNullOrEmpty(sourcePath)) + { + module.Source = SourceCodeBuilder.Create() + .FromSource(new CompiledCodeSource(sourcePath)) + .WithName(dto.SourceFileName ?? sourcePath) + .AsCompiled() + .Build(); + } + + // Constants + foreach (var constDto in dto.Constants) + { + module.Constants.Add(ConvertConstantFromDto(constDto)); + } + + // Identifiers + module.Identifiers.AddRange(dto.Identifiers); + + // Code + foreach (var cmdDto in dto.Code) + { + module.Code.Add(new Command + { + Code = (OperationCode)cmdDto.Code, + Argument = cmdDto.Argument + }); + } + + // Build context lookup for symbol resolution + var contextLookup = BuildContextLookup(environment); + + // Variable refs + foreach (var bindingDto in dto.VariableRefs) + { + module.VariableRefs.Add(ConvertBindingFromDto(bindingDto, contextLookup)); + } + + // Method refs + foreach (var bindingDto in dto.MethodRefs) + { + module.MethodRefs.Add(ConvertBindingFromDto(bindingDto, contextLookup)); + } + + // Fields + foreach (var fieldDto in dto.Fields) + { + module.Fields.Add(ConvertFieldFromDto(fieldDto, module.ClassType)); + } + + // Methods + for (int i = 0; i < dto.Methods.Count; i++) + { + module.Methods.Add(ConvertMethodFromDto(dto.Methods[i], module.ClassType, i)); + } + + // Module attributes + foreach (var attrDto in dto.ModuleAttributes) + { + module.ModuleAttributes.Add(ConvertAnnotationAttributeFromDto(attrDto)); + } + + return module; + } + + private BslPrimitiveValue ConvertConstantFromDto(ConstantDto dto) + { + return dto.Type switch + { + ConstantType.Undefined => BslUndefinedValue.Instance, + ConstantType.Null => BslNullValue.Instance, + ConstantType.String => BslStringValue.Create(dto.StringValue ?? string.Empty), + ConstantType.Number => BslNumericValue.Create(dto.NumberValue ?? 0), + ConstantType.Boolean => dto.BoolValue == true ? BslBooleanValue.True : BslBooleanValue.False, + ConstantType.Date => BslDateValue.Create(new DateTime(dto.DateTicks ?? 0)), + _ => throw new InvalidOperationException($"Unknown constant type: {dto.Type}") + }; + } + + private Dictionary BuildContextLookup(IRuntimeEnvironment environment) + { + var lookup = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var context in environment.AttachedContexts) + { + // Для системных контекстов — по имени типа + var typeKey = context.GetType().FullName; + if (!string.IsNullOrEmpty(typeKey) && !lookup.ContainsKey(typeKey)) + { + lookup[typeKey] = context; + } + + // Для PropertyBag — добавляем пользовательские модули по символьным именам + if (context is PropertyBag propertyBag) + { + for (int i = 0; i < propertyBag.Count; i++) + { + var value = propertyBag.GetPropValue(i); + if (value is IAttachableContext attachable) + { + var symbol = propertyBag.GetPropName(i); + var userModuleKey = "$UserModule:" + symbol; + if (!lookup.ContainsKey(userModuleKey)) + { + lookup[userModuleKey] = attachable; + } + } + } + } + } + + return lookup; + } + + private ModuleSymbolBinding ConvertBindingFromDto(SymbolBindingDto dto, Dictionary contextLookup) + { + var binding = new ModuleSymbolBinding + { + Kind = dto.Kind, + MemberNumber = dto.MemberNumber, + ScopeIndex = dto.ScopeIndex + }; + + if (dto.Kind == ScopeBindingKind.Static && !string.IsNullOrEmpty(dto.ContextName)) + { + if (contextLookup.TryGetValue(dto.ContextName, out var context)) + { + binding.Target = context; + + // Для PropertyBag восстанавливаем MemberNumber по имени + if (context is PropertyBag propertyBag && !string.IsNullOrEmpty(dto.MemberName)) + { + binding.MemberNumber = propertyBag.GetPropertyNumber(dto.MemberName); + } + } + else + { + throw new InvalidOperationException($"Cannot resolve context: {dto.ContextName}"); + } + } + + return binding; + } + + private BslScriptFieldInfo ConvertFieldFromDto(FieldDto dto, Type ownerType) + { + var builder = BslScriptFieldInfo.Create(dto.Name); + var buildable = (IBuildableMember)builder; + + buildable.SetDispatchIndex(dto.DispatchId); + buildable.SetExportFlag(dto.IsExport); + buildable.SetDeclaringType(ownerType); + + if (dto.Annotations != null) + { + buildable.SetAnnotations(dto.Annotations.Select(ConvertAnnotationAttributeFromDto)); + } + + return builder; + } + + private BslScriptMethodInfo ConvertMethodFromDto(MethodDto dto, Type ownerType, int index) + { + var builder = MachineMethodInfo.Create(); + var buildable = (IBuildableMember)builder; + var buildableMethod = (IBuildableMethod)builder; + + buildable.SetName(dto.Name); + buildable.SetAlias(dto.Alias); + buildable.SetExportFlag(dto.IsExport); + buildable.SetDeclaringType(ownerType); + + // DispatchId = METHOD_COUNT + index + // Для UserScriptContextInstance METHOD_COUNT = 1 + buildable.SetDispatchIndex(1 + index); + + if (dto.IsFunction) + { + buildable.SetDataType(typeof(OneScript.Values.BslValue)); + } + + // Parameters + var parameters = dto.Parameters.Select((p, idx) => ConvertParameterFromDto(p, idx, builder)).ToList(); + buildableMethod.SetParameters(parameters); + + // Annotations + if (dto.Annotations != null) + { + buildable.SetAnnotations(dto.Annotations.Select(ConvertAnnotationAttributeFromDto)); + } + + // Runtime parameters + builder.SetRuntimeParameters(dto.EntryPoint, dto.LocalVariables.ToArray()); + + return builder; + } + + private BslParameterInfo ConvertParameterFromDto(ParameterDto dto, int position, BslScriptMethodInfo method) + { + var builder = BslParameterInfo.Create(); + var buildable = (IBuildableMember)builder; + + buildable.SetName(dto.Name); + builder.SetPosition(position); + builder.SetByValue(dto.IsByValue); + + if (dto.HasDefaultValue) + { + builder.SetDefaultValueIndex(dto.DefaultValueIndex); + } + + if (dto.Annotations != null) + { + buildable.SetAnnotations(dto.Annotations.Select(ConvertAnnotationAttributeFromDto)); + } + + return builder; + } + + private BslAnnotationAttribute ConvertAnnotationAttributeFromDto(AnnotationDto dto) + { + if (dto.Parameters != null && dto.Parameters.Count > 0) + { + var parameters = dto.Parameters.Select(p => new BslAnnotationParameter( + p.Name, + p.Value != null ? BslStringValue.Create(p.Value) : null + )); + return new BslAnnotationAttribute(dto.Name, parameters); + } + + return new BslAnnotationAttribute(dto.Name); + } + } + + /// + /// Фиктивный источник кода для скомпилированных модулей + /// + public class CompiledCodeSource : ICodeSource + { + private readonly string _location; + + public CompiledCodeSource(string location) + { + _location = location; + } + + public string Location => _location; + + public string GetSourceCode() + { + // Исходный код недоступен для скомпилированных модулей + return string.Empty; + } + } +} diff --git a/src/ScriptEngine/Compiler/Packaged/CompiledPackageDto.cs b/src/ScriptEngine/Compiler/Packaged/CompiledPackageDto.cs new file mode 100644 index 000000000..3b1c2ff1e --- /dev/null +++ b/src/ScriptEngine/Compiler/Packaged/CompiledPackageDto.cs @@ -0,0 +1,125 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System.Collections.Generic; +using MessagePack; + +namespace ScriptEngine.Compiler.Packaged +{ + /// + /// Тип скомпилированного пакета + /// + public enum PackageType : byte + { + /// + /// Бандл — самодостаточный пакет со всеми зависимостями (.osc) + /// + Bundle = 0, + + /// + /// Библиотека — переиспользуемый пакет (.oslib) + /// + Library = 1 + } + + /// + /// Тип скрипта в пакете + /// + public enum ScriptType : byte + { + /// + /// Точка входа (главный модуль) + /// + Entry = 0, + + /// + /// Глобальный модуль библиотеки + /// + Module = 1, + + /// + /// Класс библиотеки + /// + Class = 2 + } + + /// + /// DTO для скомпилированного пакета (бандл или библиотека) + /// + [MessagePackObject] + public class CompiledPackageDto + { + public const int FormatVersion = 1; + public const string Magic = "OSCP"; // OneScript Compiled Package + + [Key(0)] + public string MagicHeader { get; set; } = Magic; + + [Key(1)] + public int Version { get; set; } = FormatVersion; + + [Key(2)] + public PackageType Type { get; set; } + + /// + /// Имя пакета (для библиотек — имя библиотеки) + /// + [Key(3)] + public string Name { get; set; } + + /// + /// Список зависимостей (имена библиотек). + /// Для бандла — пустой (всё включено). + /// Для .oslib — список внешних зависимостей. + /// + [Key(4)] + public List Dependencies { get; set; } = new List(); + + /// + /// Скрипты пакета (модули, классы, точка входа) + /// + [Key(5)] + public List Scripts { get; set; } = new List(); + } + + /// + /// DTO для скрипта внутри пакета + /// + [MessagePackObject] + public class PackagedScriptDto + { + /// + /// Тип скрипта + /// + [Key(0)] + public ScriptType Type { get; set; } + + /// + /// Символьное имя (для модулей и классов) + /// + [Key(1)] + public string Symbol { get; set; } + + /// + /// Имя библиотеки-владельца (для модулей/классов из библиотек) + /// + [Key(2)] + public string OwnerLibrary { get; set; } + + /// + /// Порядок загрузки (для правильной инициализации) + /// + [Key(3)] + public int LoadOrder { get; set; } + + /// + /// Скомпилированный модуль + /// + [Key(4)] + public CompiledModuleDto Module { get; set; } + } +} diff --git a/src/ScriptEngine/Compiler/Packaged/ConstantDto.cs b/src/ScriptEngine/Compiler/Packaged/ConstantDto.cs new file mode 100644 index 000000000..581f4a425 --- /dev/null +++ b/src/ScriptEngine/Compiler/Packaged/ConstantDto.cs @@ -0,0 +1,43 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using MessagePack; + +namespace ScriptEngine.Compiler.Packaged +{ + /// + /// DTO для сериализации константы + /// + [MessagePackObject] + public class ConstantDto + { + [Key(0)] + public ConstantType Type { get; set; } + + [Key(1)] + public string StringValue { get; set; } + + [Key(2)] + public decimal? NumberValue { get; set; } + + [Key(3)] + public long? DateTicks { get; set; } + + [Key(4)] + public bool? BoolValue { get; set; } + } + + public enum ConstantType : byte + { + Undefined = 0, + Null = 1, + String = 2, + Number = 3, + Boolean = 4, + Date = 5 + } +} diff --git a/src/ScriptEngine/Compiler/Packaged/FieldDto.cs b/src/ScriptEngine/Compiler/Packaged/FieldDto.cs new file mode 100644 index 000000000..d41ebf810 --- /dev/null +++ b/src/ScriptEngine/Compiler/Packaged/FieldDto.cs @@ -0,0 +1,31 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System.Collections.Generic; +using MessagePack; + +namespace ScriptEngine.Compiler.Packaged +{ + /// + /// DTO для сериализации поля модуля (переменной) + /// + [MessagePackObject] + public class FieldDto + { + [Key(0)] + public string Name { get; set; } + + [Key(1)] + public bool IsExport { get; set; } + + [Key(2)] + public int DispatchId { get; set; } + + [Key(3)] + public List Annotations { get; set; } + } +} diff --git a/src/ScriptEngine/Compiler/Packaged/LibraryBuilder.cs b/src/ScriptEngine/Compiler/Packaged/LibraryBuilder.cs new file mode 100644 index 000000000..615f5885d --- /dev/null +++ b/src/ScriptEngine/Compiler/Packaged/LibraryBuilder.cs @@ -0,0 +1,411 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using MessagePack; +using OneScript.Commons; +using OneScript.Compilation; +using OneScript.Execution; +using OneScript.Language.SyntaxAnalysis.AstNodes; +using OneScript.Sources; +using ScriptEngine.Libraries; +using ScriptEngine.Machine; + +namespace ScriptEngine.Compiler.Packaged +{ + /// + /// Построитель скомпилированной библиотеки (.oslib) + /// + public class LibraryBuilder + { + private readonly ScriptingEngine _engine; + private readonly ICompilerFrontend _compiler; + private readonly CompiledModulePackager _packager; + private readonly HashSet _dependencies; + private int _loadOrder; + + public LibraryBuilder(ScriptingEngine engine, ICompilerFrontend compiler) + { + _engine = engine; + _compiler = compiler; + _packager = new CompiledModulePackager(); + _dependencies = new HashSet(StringComparer.OrdinalIgnoreCase); + _loadOrder = 0; + } + + // Стандартные папки с модулями + private static readonly string[] ModuleFolders = { "Modules", "Модули", "src" }; + + // Стандартные папки с классами + private static readonly string[] ClassFolders = { "Classes", "Классы" }; + + // Папки, которые следует игнорировать + private static readonly HashSet IgnoredFolders = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "tests", "test", "тесты", "examples", "примеры", "doc", "docs", + "bin", "obj", ".git", ".svn", "node_modules", "addins" + }; + + /// + /// Собирает библиотеку из уже загруженной ExternalLibraryInfo + /// + public CompiledPackageDto BuildFromLoaded(ExternalLibraryInfo libraryInfo) + { + var libraryName = libraryInfo.Package.ShortName; + + var package = new CompiledPackageDto + { + Type = PackageType.Library, + Name = libraryName + }; + + // Добавляем модули + foreach (var moduleInfo in libraryInfo.Modules) + { + if (moduleInfo.Module is StackRuntimeModule stackModule) + { + // Собираем зависимости + if (!string.IsNullOrEmpty(moduleInfo.FilePath) && File.Exists(moduleInfo.FilePath)) + { + var source = _engine.Loader.FromFile(moduleInfo.FilePath); + CollectDependencies(source); + } + + var scriptDto = new PackagedScriptDto + { + Type = ScriptType.Module, + Symbol = moduleInfo.Symbol, + OwnerLibrary = libraryName, + LoadOrder = _loadOrder++, + Module = _packager.ConvertToDto(stackModule) + }; + + package.Scripts.Add(scriptDto); + } + } + + // Добавляем классы + foreach (var classInfo in libraryInfo.Classes) + { + if (classInfo.Module is StackRuntimeModule stackModule) + { + // Собираем зависимости + if (!string.IsNullOrEmpty(classInfo.FilePath) && File.Exists(classInfo.FilePath)) + { + var source = _engine.Loader.FromFile(classInfo.FilePath); + CollectDependencies(source); + } + + var scriptDto = new PackagedScriptDto + { + Type = ScriptType.Class, + Symbol = classInfo.Symbol, + OwnerLibrary = libraryName, + LoadOrder = _loadOrder++, + Module = _packager.ConvertToDto(stackModule) + }; + + package.Scripts.Add(scriptDto); + } + } + + if (package.Scripts.Count == 0) + { + throw new InvalidOperationException($"No valid script modules found in library: {libraryName}"); + } + + // Добавляем собранные зависимости + package.Dependencies.AddRange(_dependencies); + + return package; + } + + /// + /// Собирает библиотеку из папки + /// + public CompiledPackageDto Build(string libraryPath, IBslProcess process) + { + var libraryName = Path.GetFileName(libraryPath); + + var package = new CompiledPackageDto + { + Type = PackageType.Library, + Name = libraryName + }; + + // Собираем модули из корня и стандартных папок + CollectModules(libraryPath, libraryName, package, process); + + // Собираем классы из стандартных папок + CollectClasses(libraryPath, libraryName, package, process); + + if (package.Scripts.Count == 0) + { + throw new InvalidOperationException($"No valid script files found in library: {libraryPath}"); + } + + // Добавляем собранные зависимости + package.Dependencies.AddRange(_dependencies); + + return package; + } + + private void CollectModules(string libraryPath, string libraryName, CompiledPackageDto package, IBslProcess process) + { + // Сначала ищем в корне библиотеки + CompileModulesInFolder(libraryPath, libraryName, package, process); + + // Затем в стандартных папках модулей + foreach (var folderName in ModuleFolders) + { + var modulesPath = Path.Combine(libraryPath, folderName); + if (Directory.Exists(modulesPath)) + { + CompileModulesRecursive(modulesPath, libraryName, package, process); + } + } + + // Проверяем подпапки core, tools и т.д. (для библиотек типа oint) + foreach (var subDir in Directory.EnumerateDirectories(libraryPath)) + { + var dirName = Path.GetFileName(subDir); + if (IgnoredFolders.Contains(dirName) || ClassFolders.Any(c => c.Equals(dirName, StringComparison.OrdinalIgnoreCase))) + continue; + + // Проверяем есть ли внутри папка Modules + foreach (var folderName in ModuleFolders) + { + var nestedModulesPath = Path.Combine(subDir, folderName); + if (Directory.Exists(nestedModulesPath)) + { + CompileModulesRecursive(nestedModulesPath, libraryName, package, process); + } + } + } + } + + private void CollectClasses(string libraryPath, string libraryName, CompiledPackageDto package, IBslProcess process) + { + // Ищем классы в стандартных папках + foreach (var folderName in ClassFolders) + { + var classesPath = Path.Combine(libraryPath, folderName); + if (Directory.Exists(classesPath)) + { + CompileClassesRecursive(classesPath, libraryName, package, process); + } + } + + // Проверяем подпапки (для библиотек типа oint) + foreach (var subDir in Directory.EnumerateDirectories(libraryPath)) + { + var dirName = Path.GetFileName(subDir); + if (IgnoredFolders.Contains(dirName)) + continue; + + foreach (var folderName in ClassFolders) + { + var nestedClassesPath = Path.Combine(subDir, folderName); + if (Directory.Exists(nestedClassesPath)) + { + CompileClassesRecursive(nestedClassesPath, libraryName, package, process); + } + } + } + } + + private void CompileModulesInFolder(string folderPath, string libraryName, CompiledPackageDto package, IBslProcess process) + { + var scriptFiles = Directory.EnumerateFiles(folderPath, "*.os") + .Select(x => new { Name = Path.GetFileNameWithoutExtension(x), Path = x }) + .Where(x => Utils.IsValidIdentifier(x.Name)) + .ToList(); + + foreach (var scriptFile in scriptFiles) + { + CompileModule(scriptFile.Path, scriptFile.Name, libraryName, package, process); + } + } + + private void CompileModulesRecursive(string folderPath, string libraryName, CompiledPackageDto package, IBslProcess process) + { + CompileModulesInFolder(folderPath, libraryName, package, process); + + // Рекурсивно обрабатываем подпапки + foreach (var subDir in Directory.EnumerateDirectories(folderPath)) + { + var dirName = Path.GetFileName(subDir); + if (IgnoredFolders.Contains(dirName)) + continue; + + // Пропускаем папки классов внутри папок модулей + if (ClassFolders.Any(c => c.Equals(dirName, StringComparison.OrdinalIgnoreCase))) + { + CompileClassesRecursive(subDir, libraryName, package, process); + } + else + { + CompileModulesRecursive(subDir, libraryName, package, process); + } + } + } + + private void CompileModule(string filePath, string moduleName, string libraryName, CompiledPackageDto package, IBslProcess process) + { + // Проверяем, не добавлен ли уже модуль с таким именем + if (package.Scripts.Any(s => s.Type == ScriptType.Module && + s.Symbol.Equals(moduleName, StringComparison.OrdinalIgnoreCase))) + { + return; // Модуль уже добавлен + } + + var source = _engine.Loader.FromFile(filePath); + CollectDependencies(source); + + var module = _compiler.Compile(source, process); + + if (!(module is StackRuntimeModule stackModule)) + { + throw new InvalidOperationException( + $"Only stack runtime modules can be compiled to library. " + + $"File '{filePath}' uses native runtime."); + } + + var scriptDto = new PackagedScriptDto + { + Type = ScriptType.Module, + Symbol = moduleName, + OwnerLibrary = libraryName, + LoadOrder = _loadOrder++, + Module = _packager.ConvertToDto(stackModule) + }; + + package.Scripts.Add(scriptDto); + } + + private void CompileClassesRecursive(string classesPath, string libraryName, CompiledPackageDto package, IBslProcess process) + { + var classFiles = Directory.EnumerateFiles(classesPath, "*.os") + .Select(x => new { Name = Path.GetFileNameWithoutExtension(x), Path = x }) + .Where(x => Utils.IsValidIdentifier(x.Name)) + .ToList(); + + foreach (var classFile in classFiles) + { + CompileClass(classFile.Path, classFile.Name, libraryName, package, process); + } + + // Рекурсивно обрабатываем подпапки + foreach (var subDir in Directory.EnumerateDirectories(classesPath)) + { + var dirName = Path.GetFileName(subDir); + if (IgnoredFolders.Contains(dirName)) + continue; + + CompileClassesRecursive(subDir, libraryName, package, process); + } + } + + private void CompileClass(string filePath, string className, string libraryName, CompiledPackageDto package, IBslProcess process) + { + // Проверяем, не добавлен ли уже класс с таким именем + if (package.Scripts.Any(s => s.Type == ScriptType.Class && + s.Symbol.Equals(className, StringComparison.OrdinalIgnoreCase))) + { + return; // Класс уже добавлен + } + + var source = _engine.Loader.FromFile(filePath); + CollectDependencies(source); + + var module = _compiler.Compile(source, process); + + if (!(module is StackRuntimeModule stackModule)) + { + throw new InvalidOperationException( + $"Only stack runtime modules can be compiled to library. " + + $"File '{filePath}' uses native runtime."); + } + + var scriptDto = new PackagedScriptDto + { + Type = ScriptType.Class, + Symbol = className, + OwnerLibrary = libraryName, + LoadOrder = _loadOrder++, + Module = _packager.ConvertToDto(stackModule) + }; + + package.Scripts.Add(scriptDto); + } + + private void CollectDependencies(SourceCode source) + { + // Парсим исходник для поиска директив #Использовать + // Зависимости добавляются в _dependencies + // + // Примечание: это упрощённая реализация. + // В идеале нужно использовать лексер/парсер для точного извлечения. + + var code = source.GetSourceCode(); + var lines = code.Split('\n'); + + foreach (var line in lines) + { + var trimmed = line.Trim(); + if (trimmed.StartsWith("#Использовать", StringComparison.OrdinalIgnoreCase) || + trimmed.StartsWith("#Use", StringComparison.OrdinalIgnoreCase)) + { + var depName = ExtractDependencyName(trimmed); + if (!string.IsNullOrEmpty(depName) && !depName.StartsWith("\"") && !depName.StartsWith(".")) + { + // Это ссылка на внешнюю библиотеку по имени + _dependencies.Add(depName); + } + } + } + } + + private string ExtractDependencyName(string directive) + { + // #Использовать ИмяБиблиотеки + // #Использовать "путь/к/библиотеке" + var parts = directive.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length >= 2) + { + var name = parts[1].Trim(); + // Убираем комментарии + var commentIdx = name.IndexOf("//"); + if (commentIdx >= 0) + { + name = name.Substring(0, commentIdx).Trim(); + } + return name; + } + return null; + } + + /// + /// Сохраняет библиотеку в поток + /// + public void Save(Stream stream, CompiledPackageDto package) + { + MessagePackSerializer.Serialize(stream, package); + } + + /// + /// Сохраняет библиотеку в массив байт + /// + public byte[] Save(CompiledPackageDto package) + { + return MessagePackSerializer.Serialize(package); + } + } +} diff --git a/src/ScriptEngine/Compiler/Packaged/LibraryLoader.cs b/src/ScriptEngine/Compiler/Packaged/LibraryLoader.cs new file mode 100644 index 000000000..1c21621ce --- /dev/null +++ b/src/ScriptEngine/Compiler/Packaged/LibraryLoader.cs @@ -0,0 +1,166 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using MessagePack; +using OneScript.Compilation; +using OneScript.Contexts; +using OneScript.Execution; +using ScriptEngine.Libraries; +using ScriptEngine.Machine; +using ScriptEngine.Machine.Contexts; + +namespace ScriptEngine.Compiler.Packaged +{ + /// + /// Загрузчик скомпилированных библиотек (.oslib) + /// + public class LibraryLoader + { + private readonly ScriptingEngine _engine; + private readonly CompiledModulePackager _packager; + + public LibraryLoader(ScriptingEngine engine) + { + _engine = engine; + _packager = new CompiledModulePackager(); + } + + /// + /// Загружает библиотеку из потока + /// + public LoadedLibrary Load(Stream stream, IBslProcess process) + { + var package = MessagePackSerializer.Deserialize(stream); + return LoadPackage(package, process); + } + + /// + /// Загружает библиотеку из файла + /// + public LoadedLibrary LoadFromFile(string path, IBslProcess process) + { + using (var stream = File.OpenRead(path)) + { + return Load(stream, process); + } + } + + private LoadedLibrary LoadPackage(CompiledPackageDto package, IBslProcess process) + { + ValidatePackage(package); + + var result = new LoadedLibrary + { + Name = package.Name, + Dependencies = package.Dependencies.ToList() + }; + + var env = _engine.Environment; + + // Загружаем скрипты в порядке LoadOrder + var orderedScripts = package.Scripts + .OrderBy(s => s.LoadOrder) + .ToList(); + + // Сначала загружаем модули + foreach (var scriptDto in orderedScripts.Where(s => s.Type == ScriptType.Module)) + { + var module = _packager.ConvertFromDto(scriptDto.Module, env); + var instance = RegisterModule(scriptDto.Symbol, module, process); + if (instance != null) + { + result.Modules[scriptDto.Symbol] = module; + } + } + + // Затем классы + foreach (var scriptDto in orderedScripts.Where(s => s.Type == ScriptType.Class)) + { + var module = _packager.ConvertFromDto(scriptDto.Module, env); + RegisterClass(scriptDto.Symbol, module); + result.Classes[scriptDto.Symbol] = module; + } + + return result; + } + + private void ValidatePackage(CompiledPackageDto package) + { + if (package.MagicHeader != CompiledPackageDto.Magic) + { + throw new InvalidOperationException("Invalid compiled library format"); + } + + if (package.Version > CompiledPackageDto.FormatVersion) + { + throw new InvalidOperationException($"Unsupported library version: {package.Version}"); + } + + if (package.Type != PackageType.Library) + { + throw new InvalidOperationException($"Expected library package, got: {package.Type}"); + } + } + + private UserScriptContextInstance RegisterModule(string symbol, StackRuntimeModule module, IBslProcess process) + { + if (string.IsNullOrEmpty(symbol)) + return null; + + var instance = _engine.CreateUninitializedSDO(module); + + // Регистрируем как глобальное свойство + _engine.Environment.InjectGlobalProperty(instance, symbol, symbol, true); + + // Инициализируем модуль + _engine.InitializeSDO(instance, process); + + return instance; + } + + private void RegisterClass(string symbol, StackRuntimeModule module) + { + if (string.IsNullOrEmpty(symbol)) + return; + + // Регистрируем класс через фабрику + var factory = _engine.AttachedScriptsFactory; + if (factory == null) + { + throw new InvalidOperationException($"AttachedScriptsFactory is not initialized. Cannot register class '{symbol}'"); + } + + factory.RegisterTypeModule(symbol, module); + } + } + + /// + /// Результат загрузки библиотеки + /// + public class LoadedLibrary + { + public string Name { get; set; } + + public List Dependencies { get; set; } = new List(); + + /// + /// Загруженные модули библиотеки + /// + public Dictionary Modules { get; } = + new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Загруженные классы библиотеки + /// + public Dictionary Classes { get; } = + new Dictionary(StringComparer.OrdinalIgnoreCase); + } +} diff --git a/src/ScriptEngine/Compiler/Packaged/MethodDto.cs b/src/ScriptEngine/Compiler/Packaged/MethodDto.cs new file mode 100644 index 000000000..4a92c13be --- /dev/null +++ b/src/ScriptEngine/Compiler/Packaged/MethodDto.cs @@ -0,0 +1,74 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System.Collections.Generic; +using MessagePack; + +namespace ScriptEngine.Compiler.Packaged +{ + /// + /// DTO для сериализации метода + /// + [MessagePackObject] + public class MethodDto + { + [Key(0)] + public string Name { get; set; } + + [Key(1)] + public string Alias { get; set; } + + [Key(2)] + public bool IsFunction { get; set; } + + [Key(3)] + public bool IsExport { get; set; } + + [Key(4)] + public bool IsAsync { get; set; } + + [Key(5)] + public bool IsDeprecated { get; set; } + + [Key(6)] + public bool ThrowOnUseDeprecated { get; set; } + + [Key(7)] + public int EntryPoint { get; set; } + + [Key(8)] + public List LocalVariables { get; set; } = new List(); + + [Key(9)] + public List Parameters { get; set; } = new List(); + + [Key(10)] + public List Annotations { get; set; } = new List(); + } + + /// + /// DTO для сериализации параметра метода + /// + [MessagePackObject] + public class ParameterDto + { + [Key(0)] + public string Name { get; set; } + + [Key(1)] + public bool IsByValue { get; set; } + + [Key(2)] + public bool HasDefaultValue { get; set; } + + [Key(3)] + public int DefaultValueIndex { get; set; } + + [Key(4)] + public List Annotations { get; set; } + } +} diff --git a/src/ScriptEngine/Compiler/Packaged/SymbolBindingDto.cs b/src/ScriptEngine/Compiler/Packaged/SymbolBindingDto.cs new file mode 100644 index 000000000..d31ac512c --- /dev/null +++ b/src/ScriptEngine/Compiler/Packaged/SymbolBindingDto.cs @@ -0,0 +1,51 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using MessagePack; +using OneScript.Compilation.Binding; + +namespace ScriptEngine.Compiler.Packaged +{ + /// + /// DTO для сериализации привязки символа (переменной или метода) + /// + [MessagePackObject] + public class SymbolBindingDto + { + /// + /// Тип привязки: Static, ThisScope, FrameScope + /// + [Key(0)] + public ScopeBindingKind Kind { get; set; } + + /// + /// Номер члена в целевом контексте + /// + [Key(1)] + public int MemberNumber { get; set; } + + /// + /// Индекс области видимости (для FrameScope) + /// + [Key(2)] + public int ScopeIndex { get; set; } + + /// + /// Имя целевого контекста (для Static). + /// Используется для восстановления ссылки при загрузке. + /// + [Key(3)] + public string ContextName { get; set; } + + /// + /// Имя свойства/метода в контексте (для PropertyBag). + /// Используется для восстановления MemberNumber при загрузке. + /// + [Key(4)] + public string MemberName { get; set; } + } +} diff --git a/src/ScriptEngine/Machine/Contexts/ScriptInformationContext.cs b/src/ScriptEngine/Machine/Contexts/ScriptInformationContext.cs index 9351ad5da..bc0d6308b 100644 --- a/src/ScriptEngine/Machine/Contexts/ScriptInformationContext.cs +++ b/src/ScriptEngine/Machine/Contexts/ScriptInformationContext.cs @@ -17,10 +17,12 @@ namespace ScriptEngine.Machine.Contexts public class ScriptInformationContext : AutoContext { private readonly string _origin; + private readonly bool _isCompiled; public ScriptInformationContext(SourceCode codeSrc) { _origin = codeSrc.Location; + _isCompiled = codeSrc.IsCompiled; } /// @@ -50,6 +52,19 @@ public string Path } } } + + /// + /// Признак того, что сценарий загружен из скомпилированного кода (.osc, .oslib). + /// Позволяет в прикладном коде определить способ вычисления путей к ресурсам. + /// + [ContextProperty("ЭтоСкомпилированныйКод", "IsCompiledCode")] + public bool IsCompiledCode + { + get + { + return _isCompiled; + } + } } } diff --git a/src/ScriptEngine/Machine/MachineMethodInfo.cs b/src/ScriptEngine/Machine/MachineMethodInfo.cs index 6a4dd85cf..acbfe923b 100644 --- a/src/ScriptEngine/Machine/MachineMethodInfo.cs +++ b/src/ScriptEngine/Machine/MachineMethodInfo.cs @@ -10,11 +10,17 @@ This Source Code Form is subject to the terms of the namespace ScriptEngine.Machine { - internal class MachineMethodInfo : BslScriptMethodInfo + public class MachineMethodInfo : BslScriptMethodInfo { private MachineMethod _method; + + internal MachineMethodInfo() + { + } + + public static MachineMethodInfo Create() => new MachineMethodInfo(); - internal void SetRuntimeParameters(int entryPoint, string[] locals) + public void SetRuntimeParameters(int entryPoint, string[] locals) { _method = new MachineMethod { diff --git a/src/ScriptEngine/ScriptEngine.csproj b/src/ScriptEngine/ScriptEngine.csproj index f81d68951..914523371 100644 --- a/src/ScriptEngine/ScriptEngine.csproj +++ b/src/ScriptEngine/ScriptEngine.csproj @@ -46,6 +46,10 @@ + + + + diff --git a/src/oscript/BehaviorSelector.cs b/src/oscript/BehaviorSelector.cs index 264cfb64f..703cfd4d1 100644 --- a/src/oscript/BehaviorSelector.cs +++ b/src/oscript/BehaviorSelector.cs @@ -6,12 +6,15 @@ This Source Code Form is subject to the terms of the ----------------------------------------------------------*/ using System; using System.Collections.Generic; +using System.IO; using System.Text; namespace oscript { static class BehaviorSelector { + private const string CompiledExtension = ".osc"; + public static AppBehavior Select(string[] cmdLineArgs) { var helper = new CmdLineHelper(cmdLineArgs); @@ -23,6 +26,13 @@ public static AppBehavior Select(string[] cmdLineArgs) if (!arg.StartsWith("-")) { var path = arg; + + // Автоматически определяем тип файла по расширению + if (Path.GetExtension(path).Equals(CompiledExtension, StringComparison.OrdinalIgnoreCase)) + { + return new ExecuteCompiledBehavior(path, helper.Tail()); + } + return new ExecuteScriptBehavior(path, helper.Tail()); } @@ -40,6 +50,8 @@ private static AppBehavior SelectParametrized(CmdLineHelper helper) initializers.Add("-measure", MeasureBehavior.Create); initializers.Add("-compile", ShowCompiledBehavior.Create); + initializers.Add("-build", SaveCompiledBehavior.Create); + initializers.Add("-buildlib", BuildLibraryBehavior.Create); initializers.Add("-check", CheckSyntaxBehavior.Create); initializers.Add("-cgi", h => new CgiBehavior()); initializers.Add("-version", h => new ShowVersionBehavior()); diff --git a/src/oscript/BuildLibraryBehavior.cs b/src/oscript/BuildLibraryBehavior.cs new file mode 100644 index 000000000..bab73d71f --- /dev/null +++ b/src/oscript/BuildLibraryBehavior.cs @@ -0,0 +1,117 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System; +using System.IO; +using OneScript.Execution; +using OneScript.Language; +using ScriptEngine.Compiler.Packaged; +using ScriptEngine.HostedScript; +using ScriptEngine.Libraries; +using ScriptEngine.Machine; + +namespace oscript +{ + /// + /// Поведение для компиляции библиотеки в .oslib файл + /// + internal class BuildLibraryBehavior : AppBehavior + { + private readonly string _libraryPath; + private readonly string _outputPath; + + public BuildLibraryBehavior(string libraryPath, string outputPath) + { + _libraryPath = libraryPath; + _outputPath = outputPath; + } + + public override int Execute() + { + if (!Directory.Exists(_libraryPath)) + { + Output.WriteLine($"Library directory not found: '{_libraryPath}'"); + return 2; + } + + try + { + var builder = ConsoleHostBuilder.Create(_libraryPath); + var hostedScript = ConsoleHostBuilder.Build(builder); + hostedScript.Initialize(); + + var process = hostedScript.Engine.NewProcess(); + + // Загружаем библиотеку через LibraryLoader и получаем ExternalLibraryInfo + var libraryLoader = ScriptEngine.HostedScript.LibraryLoader.Create(hostedScript.Engine, process); + var libraryInfo = libraryLoader.LoadLibraryWithInfo(_libraryPath, process); + + if (libraryInfo == null) + { + Output.WriteLine($"Failed to load library: '{_libraryPath}'"); + return 1; + } + + // Собираем пакет из загруженных модулей + var packageBuilder = new LibraryBuilder(hostedScript.Engine, hostedScript.GetCompilerService()); + var package = packageBuilder.BuildFromLoaded(libraryInfo); + + var outputPath = string.IsNullOrEmpty(_outputPath) + ? _libraryPath + ".oslib" + : _outputPath; + + using (var stream = File.Create(outputPath)) + { + packageBuilder.Save(stream, package); + } + + var moduleCount = package.Scripts.Count; + Output.WriteLine($"Library compiled to: {outputPath} ({moduleCount} module(s))"); + + if (package.Dependencies.Count > 0) + { + Output.WriteLine($"Dependencies: {string.Join(", ", package.Dependencies)}"); + } + + return 0; + } + catch (ScriptException e) + { + Output.WriteLine(e.Message); + return 1; + } + catch (Exception e) + { + Output.WriteLine($"Error: {e.Message}"); + return 1; + } + } + + public static AppBehavior Create(CmdLineHelper helper) + { + var libraryPath = helper.Next(); + if (libraryPath == null) + { + Output.WriteLine("Library path is required"); + return null; + } + + // Убираем trailing slash если есть + libraryPath = libraryPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + + // Опциональный путь для выходного файла + string outputPath = null; + var next = helper.Next(); + if (next != null && !next.StartsWith("-")) + { + outputPath = next; + } + + return new BuildLibraryBehavior(libraryPath, outputPath); + } + } +} diff --git a/src/oscript/ExecuteCompiledBehavior.cs b/src/oscript/ExecuteCompiledBehavior.cs new file mode 100644 index 000000000..db927b5f0 --- /dev/null +++ b/src/oscript/ExecuteCompiledBehavior.cs @@ -0,0 +1,115 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System; +using System.IO; +using OneScript.Sources; +using OneScript.StandardLibrary; +using ScriptEngine; +using ScriptEngine.Compiler.Packaged; +using ScriptEngine.HostedScript; +using ScriptEngine.Machine; + +namespace oscript +{ + /// + /// Поведение для выполнения скомпилированного бандла (.osc файла) + /// + internal class ExecuteCompiledBehavior : AppBehavior, IHostApplication, ISystemLogWriter + { + private readonly string _path; + private readonly string[] _scriptArgs; + + public ExecuteCompiledBehavior(string path, string[] args) + { + _path = path; + _scriptArgs = args; + } + + public override int Execute() + { + if (!File.Exists(_path)) + { + Echo($"Compiled module file is not found '{_path}'"); + return 2; + } + + SystemLogger.SetWriter(this); + + try + { + var builder = ConsoleHostBuilder.Create(_path); + var hostedScript = ConsoleHostBuilder.Build(builder); + hostedScript.Initialize(); + + // Создаём source для контекста + var source = SourceCodeBuilder.Create() + .FromSource(new CompiledCodeSource(_path)) + .WithName(Path.GetFileName(_path)) + .AsCompiled() + .Build(); + + hostedScript.SetGlobalEnvironment(this, source); + + // Загружаем бандл + var bundleLoader = new BundleLoader(hostedScript.Engine); + var bundle = bundleLoader.LoadFromFile(_path); + + if (bundle.EntryModule == null) + { + Echo("Bundle does not contain entry point"); + return 1; + } + + // Создаём и запускаем процесс + var bslProcess = hostedScript.Engine.NewProcess(); + hostedScript.Engine.NewObject(bundle.EntryModule, bslProcess); + + hostedScript.Dispose(); + return 0; + } + catch (ScriptInterruptionException e) + { + return e.ExitCode; + } + catch (Exception e) + { + ShowExceptionInfo(e); + return 1; + } + } + + #region IHostApplication Members + + public void Echo(string text, MessageStatusEnum status = MessageStatusEnum.Ordinary) + { + ConsoleHostImpl.Echo(text, status); + } + + public void ShowExceptionInfo(Exception exc) + { + ConsoleHostImpl.ShowExceptionInfo(exc); + } + + public bool InputString(out string result, string prompt, int maxLen, bool multiline) + { + return ConsoleHostImpl.InputString(out result, prompt, maxLen, multiline); + } + + public string[] GetCommandLineArguments() + { + return _scriptArgs; + } + + #endregion + + public void Write(string text) + { + Console.Error.WriteLine(text); + } + } +} diff --git a/src/oscript/SaveCompiledBehavior.cs b/src/oscript/SaveCompiledBehavior.cs new file mode 100644 index 000000000..91faa3442 --- /dev/null +++ b/src/oscript/SaveCompiledBehavior.cs @@ -0,0 +1,110 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System; +using System.IO; +using OneScript.Language; +using ScriptEngine.Compiler; +using ScriptEngine.Compiler.Packaged; +using ScriptEngine.Machine; + +namespace oscript +{ + /// + /// Поведение для сохранения скомпилированного модуля/бандла в файл + /// + internal class SaveCompiledBehavior : AppBehavior + { + private readonly string _sourcePath; + private readonly string _outputPath; + + public SaveCompiledBehavior(string sourcePath, string outputPath) + { + _sourcePath = sourcePath; + _outputPath = outputPath; + } + + public override int Execute() + { + if (!File.Exists(_sourcePath)) + { + Output.WriteLine($"Script file is not found '{_sourcePath}'"); + return 2; + } + + try + { + var builder = ConsoleHostBuilder.Create(_sourcePath); + var hostedScript = ConsoleHostBuilder.Build(builder); + hostedScript.Initialize(); + + var source = hostedScript.Loader.FromFile(_sourcePath); + var compiler = hostedScript.GetCompilerService(); + hostedScript.SetGlobalEnvironment(new DoNothingHost(), source); + + var process = hostedScript.Engine.NewProcess(); + + // Используем BundleBuilder для сборки всех зависимостей + var bundleBuilder = new BundleBuilder(hostedScript.Engine, compiler); + var package = bundleBuilder.Build(source, process); + + var outputPath = string.IsNullOrEmpty(_outputPath) + ? Path.ChangeExtension(_sourcePath, ".osc") + : _outputPath; + + using (var stream = File.Create(outputPath)) + { + bundleBuilder.Save(stream, package); + } + + var moduleCount = package.Scripts.Count; + var libModules = moduleCount - 1; // минус entry point + + if (libModules > 0) + { + Output.WriteLine($"Bundle saved to: {outputPath} ({libModules} library module(s) included)"); + } + else + { + Output.WriteLine($"Compiled module saved to: {outputPath}"); + } + + return 0; + } + catch (ScriptException e) + { + Output.WriteLine(e.Message); + return 1; + } + catch (Exception e) + { + Output.WriteLine($"Error: {e.Message}"); + return 1; + } + } + + public static AppBehavior Create(CmdLineHelper helper) + { + var sourcePath = helper.Next(); + if (sourcePath == null) + { + Output.WriteLine("Source file path is required"); + return null; + } + + // Опциональный путь для выходного файла + string outputPath = null; + var next = helper.Next(); + if (next != null && !next.StartsWith("-")) + { + outputPath = next; + } + + return new SaveCompiledBehavior(sourcePath, outputPath); + } + } +} diff --git a/src/oscript/ShowUsageBehavior.cs b/src/oscript/ShowUsageBehavior.cs index ee1c26a57..b29fb5dae 100644 --- a/src/oscript/ShowUsageBehavior.cs +++ b/src/oscript/ShowUsageBehavior.cs @@ -24,6 +24,10 @@ public override int Execute() Output.WriteLine("Modes:"); Output.WriteLine($" {"-measure",modeWidth} Measures script execution time."); Output.WriteLine($" {"-compile",modeWidth} Shows compiled module without execution."); + Output.WriteLine($" {"-build",modeWidth} Compiles script and saves to .osc file."); + Output.WriteLine($" {"",modeWidth} Usage: -build [output.osc]"); + Output.WriteLine($" {"-buildlib",modeWidth} Compiles library folder to .oslib file."); + Output.WriteLine($" {"",modeWidth} Usage: -buildlib [output.oslib]"); Output.WriteLine($" {"-check",modeWidth} Provides syntax check."); Output.WriteLine($" {"",modeWidth} Options:"); Output.WriteLine($" {"",modeWidth} {"-cgi",subOptionWidth} Syntax check in CGI-mode."); @@ -45,6 +49,11 @@ public override int Execute() Output.WriteLine("CGI Mode:"); Output.WriteLine(" oscript.exe -cgi [script_arguments...]"); Output.WriteLine(" Runs as CGI application under HTTP-server."); + Output.WriteLine(); + + Output.WriteLine("Compiled Modules:"); + Output.WriteLine(" oscript.exe script.osc [arguments...]"); + Output.WriteLine(" Runs pre-compiled module (.osc file) directly."); return 0; }