diff --git a/src/Component/SimpleGlobalContext.cs b/src/Component/SimpleGlobalContext.cs new file mode 100644 index 000000000..9a0794f48 --- /dev/null +++ b/src/Component/SimpleGlobalContext.cs @@ -0,0 +1,25 @@ +/*---------------------------------------------------------- +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 OneScript.Contexts; +using ScriptEngine.Machine.Contexts; + +namespace Component +{ + // Компонента добавляет свой глобальный контекст, как в #344 и #911 + [GlobalContext(Category = "Тестовая компонента")] + public class SimpleGlobalContext : GlobalContextBase + { + [ContextMethod("ГлобальныйМетодКомпоненты")] + public string GlobalMethod() => "метод компоненты"; + + [ContextProperty("ГлобальноеСвойствоКомпоненты")] + public string GlobalProperty => "свойство компоненты"; + + public static IAttachableContext CreateInstance() => new SimpleGlobalContext(); + } +} diff --git a/src/OneScript.Core/Compilation/Binding/ScopeBindingDescriptor.cs b/src/OneScript.Core/Compilation/Binding/ScopeBindingDescriptor.cs index c8127c84d..e6e1c2f8c 100644 --- a/src/OneScript.Core/Compilation/Binding/ScopeBindingDescriptor.cs +++ b/src/OneScript.Core/Compilation/Binding/ScopeBindingDescriptor.cs @@ -34,7 +34,7 @@ private ScopeBindingDescriptor(ScopeBindingKind kind, IAttachableContext target, public IAttachableContext Target { get; } /// - /// Индекс области видимости в списке ExecutionFrame.Scopes. Используется для ScopeBindingKind.FrameScope. + /// Номер области видимости с конца списка ExecutionFrame.Scopes (0 - самая внутренняя). Используется для ScopeBindingKind.FrameScope. /// public int ScopeIndex { get; } diff --git a/src/ScriptEngine/Compiler/ModuleDumpWriter.cs b/src/ScriptEngine/Compiler/ModuleDumpWriter.cs index 198b13e62..edc4f8178 100644 --- a/src/ScriptEngine/Compiler/ModuleDumpWriter.cs +++ b/src/ScriptEngine/Compiler/ModuleDumpWriter.cs @@ -165,7 +165,7 @@ private void WriteSymbolMap(TextWriter output, IList map) { ScopeBindingKind.Static => item.Target?.GetType().Name ?? "null", ScopeBindingKind.ThisScope => "this", - ScopeBindingKind.FrameScope => $"frame[{item.ScopeIndex}]", + ScopeBindingKind.FrameScope => $"frame[^{item.ScopeIndex + 1}]", _ => "unknown" }; output.Write(string.Format("{0,-3}:({1},{2},{3})\n", i, item.Kind, targetInfo, item.MemberNumber)); diff --git a/src/ScriptEngine/Machine/JoinedScopes.cs b/src/ScriptEngine/Machine/JoinedScopes.cs index 60fb56c5d..a523919dd 100644 --- a/src/ScriptEngine/Machine/JoinedScopes.cs +++ b/src/ScriptEngine/Machine/JoinedScopes.cs @@ -19,11 +19,53 @@ public class JoinedScopes : IReadOnlyList { private readonly IReadOnlyList _outerScopes; private readonly IAttachableContext _innerScope; + private readonly IReadOnlyList _rootScopes; + private readonly int _innerScopesCount; public JoinedScopes(IReadOnlyList outerScopes, IAttachableContext innerScope) { _outerScopes = outerScopes ?? throw new ArgumentNullException(nameof(outerScopes)); _innerScope = innerScope ?? throw new ArgumentNullException(nameof(innerScope)); + + if (_outerScopes is JoinedScopes outer) + { + _rootScopes = outer._rootScopes; + _innerScopesCount = outer._innerScopesCount + 1; + } + else + { + _rootScopes = _outerScopes; + _innerScopesCount = 1; + } + } + + /// + /// Исходный список, к которому добавлены внутренние области (глобальные контексты окружения). + /// Может расти, пока выполняется код. + /// + public IReadOnlyList RootScopes => _rootScopes; + + /// + /// Число внутренних областей, добавленных к исходному списку. + /// + public int InnerScopesCount => _innerScopesCount; + + /// + /// Внутренняя область по номеру с конца: 0 - самая внутренняя. + /// В отличие от индексатора не зависит от размера исходного списка. + /// + public IAttachableContext FromEnd(int index) + { + if (index < 0 || index >= _innerScopesCount) + throw new ArgumentOutOfRangeException(nameof(index)); + + var scopes = this; + for (; index > 0; index--) + { + scopes = (JoinedScopes)scopes._outerScopes; + } + + return scopes._innerScope; } public IEnumerator GetEnumerator() diff --git a/src/ScriptEngine/Machine/MachineInstance.cs b/src/ScriptEngine/Machine/MachineInstance.cs index 5471c9487..20b3e0518 100644 --- a/src/ScriptEngine/Machine/MachineInstance.cs +++ b/src/ScriptEngine/Machine/MachineInstance.cs @@ -2443,53 +2443,24 @@ private SymbolTable ExtractCompilerContext() { var ctx = new SymbolTable(); var scopes = _currentFrame.Scopes ?? Array.Empty(); - var scopeCount = scopes.Count; - var thisScope = _currentFrame.ThisScope; - // Добавляем все контексты из scopes (глобальные + локальные из предыдущих кадров) - for (int index = 0; index < scopeCount; index++) + // В начале scopes - глобальные контексты окружения, дальше области модуля и вложенных вычислений. + // Выражение кэшируется, а глобальных контекстов может стать больше (ПодключитьВнешнююКомпоненту), + // поэтому глобальные связываются напрямую, а остальные области - по номеру с конца списка + var joinedScopes = scopes as JoinedScopes; + var globals = joinedScopes?.RootScopes ?? scopes; + var globalsCount = globals.Count; + for (int index = 0; index < globalsCount; index++) { - var scope = scopes[index]; - - var symbolScope = new SymbolScope(); - - // Добавляем методы - for (int i = 0; i < scope.MethodsCount; i++) - { - var methodInfo = scope.GetMethod(i); - symbolScope.DefineMethod(methodInfo.ToSymbol()); - } - - // Добавляем переменные - for (int i = 0; i < scope.VariablesCount; i++) - { - var variable = scope.GetVariable(i); - - string alias = null; - if (scope is IRuntimeContextInstance runtimeContext) - { - try - { - var propInfo = runtimeContext.GetPropertyInfo(i); - alias = propInfo.Alias; - } - catch - { - // Алиас остается пустым - } - } - - if (alias != null) - { - symbolScope.DefineVariable(new AliasedVariableSymbol(variable.Name, alias)); - } - else - { - symbolScope.DefineVariable(new LocalVariableSymbol(variable.Name)); - } - } + var scope = globals[index]; + ctx.PushScope(CreateSymbolScope(scope), ScopeBindingDescriptor.Static(scope)); + } - ctx.PushScope(symbolScope, ScopeBindingDescriptor.FrameScope(index)); + var innerScopesCount = joinedScopes?.InnerScopesCount ?? 0; + for (int index = innerScopesCount - 1; index >= 0; index--) + { + // В кадре выражения самой внутренней будет его собственная локальная область + ctx.PushScope(CreateSymbolScope(joinedScopes.FromEnd(index)), ScopeBindingDescriptor.FrameScope(index + 1)); } // Локальные переменные текущего фрейма @@ -2503,6 +2474,49 @@ private SymbolTable ExtractCompilerContext() return ctx; } + private static SymbolScope CreateSymbolScope(IAttachableContext scope) + { + var symbolScope = new SymbolScope(); + + // Добавляем методы + for (int i = 0; i < scope.MethodsCount; i++) + { + var methodInfo = scope.GetMethod(i); + symbolScope.DefineMethod(methodInfo.ToSymbol()); + } + + // Добавляем переменные + for (int i = 0; i < scope.VariablesCount; i++) + { + var variable = scope.GetVariable(i); + + string alias = null; + if (scope is IRuntimeContextInstance runtimeContext) + { + try + { + var propInfo = runtimeContext.GetPropertyInfo(i); + alias = propInfo.Alias; + } + catch + { + // Алиас остается пустым + } + } + + if (alias != null) + { + symbolScope.DefineVariable(new AliasedVariableSymbol(variable.Name, alias)); + } + else + { + symbolScope.DefineVariable(new LocalVariableSymbol(variable.Name)); + } + } + + return symbolScope; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private IAttachableContext ResolveBindingTarget(ModuleSymbolBinding binding) { diff --git a/src/ScriptEngine/Machine/ModuleSymbolBinding.cs b/src/ScriptEngine/Machine/ModuleSymbolBinding.cs index cecb8e329..6dea8f20c 100644 --- a/src/ScriptEngine/Machine/ModuleSymbolBinding.cs +++ b/src/ScriptEngine/Machine/ModuleSymbolBinding.cs @@ -33,12 +33,13 @@ internal IAttachableContext ResolveTarget(ExecutionFrame frame) private IAttachableContext ResolveFrameScope(ExecutionFrame frame) { - if (frame?.Scopes == null) + if (frame?.Scopes is not JoinedScopes scopes) throw new InvalidOperationException("Frame scopes are not available"); - if (ScopeIndex < 0 || ScopeIndex >= frame.Scopes.Count) + if (ScopeIndex < 0 || ScopeIndex >= scopes.InnerScopesCount) throw new InvalidOperationException($"Invalid scope index {ScopeIndex}"); - return frame.Scopes[ScopeIndex]; + // Номер с конца: глобальных контекстов в начале списка может стать больше + return scopes.FromEnd(ScopeIndex); } public bool Equals(ModuleSymbolBinding other) diff --git a/src/Tests/OneScript.Core.Tests/EvalScopesTests.cs b/src/Tests/OneScript.Core.Tests/EvalScopesTests.cs new file mode 100644 index 000000000..ea2684f65 --- /dev/null +++ b/src/Tests/OneScript.Core.Tests/EvalScopesTests.cs @@ -0,0 +1,78 @@ +/*---------------------------------------------------------- +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 FluentAssertions; +using OneScript.Contexts; +using ScriptEngine.Hosting; +using ScriptEngine.Machine.Contexts; +using Xunit; + +namespace OneScript.Core.Tests +{ + [GlobalContext(ManualRegistration = true)] + public class EvalScopesTestGlobals : GlobalContextBase + { + [ContextProperty("СвойствоТестовогоКонтекста")] + public string Property => "свойство контекста"; + + [ContextMethod("МетодТестовогоКонтекста")] + public string Method() => "метод контекста"; + } + + [GlobalContext(ManualRegistration = true)] + public class EvalScopesTestHost : GlobalContextBase + { + private readonly IRuntimeEnvironment _environment; + + public EvalScopesTestHost(IRuntimeEnvironment environment) + { + _environment = environment; + } + + // Как ПодключитьВнешнююКомпоненту со своим глобальным контекстом + [ContextMethod("ПодключитьТестовыйКонтекст")] + public void Attach() => _environment.InjectObject(new EvalScopesTestGlobals()); + } + + public class EvalScopesTests + { + private const string Script = + "Перем МояПеременная;\n" + + "Перем Итог Экспорт;\n" + + "\n" + + "Функция МояФункция()\n" + + " Возврат \"функция модуля\";\n" + + "КонецФункции\n" + + "\n" + + "МояПеременная = \"переменная модуля\";\n" + + "Итог = \"\";\n" + + "Значение = Неопределено;\n" + + "Для Номер = 1 По 2 Цикл\n" + + " Итог = Итог + Вычислить(\"МояПеременная\") + \"; \" + Вычислить(\"МояФункция()\") + \"; \";\n" + + " Выполнить(\"Значение = МояПеременная\");\n" + + " Итог = Итог + Значение + \"; \";\n" + + " Если Номер = 1 Тогда\n" + + " ПодключитьТестовыйКонтекст();\n" + + " КонецЕсли;\n" + + "КонецЦикла;\n"; + + [Fact] + public void CachedExpressionsSeeModuleAfterGlobalContextIsAttached() + { + var engine = DefaultEngineBuilder.Create().SetDefaultOptions().Build(); + engine.Initialize(); + engine.Environment.InjectObject(new EvalScopesTestHost(engine.Environment)); + + // Вычислить и Выполнить кэшируют выражения у машины процесса, поэтому все в одном запуске + var instance = engine.AttachedScriptsFactory.LoadFromString(engine.GetCompilerService(), Script, engine.NewProcess()); + + instance.GetPropValue(instance.GetPropertyNumber("Итог")).ToString().Should().Be( + "переменная модуля; функция модуля; переменная модуля; " + + "переменная модуля; функция модуля; переменная модуля; "); + } + } +} diff --git a/tests/global-context-addin.os b/tests/global-context-addin.os new file mode 100644 index 000000000..2dc53e8bd --- /dev/null +++ b/tests/global-context-addin.os @@ -0,0 +1,91 @@ +// Компонента, которая добавляет глобальный контекст (#344, #911). Сценарии из global-context +// выполняются отдельными процессами: подключение компоненты меняет глобальный контекст всего процесса, +// а порядок библиотек важен только при первой загрузке +#Использовать "component" + +Перем юТест; + +Функция ПолучитьСписокТестов(ЮнитТестирование) Экспорт + + юТест = ЮнитТестирование; + + ВсеТесты = Новый Массив; + ВсеТесты.Добавить("ТестДолжен_ПодключитьБиблиотекуСГлобальнымКонтекстомНеПоследней"); + ВсеТесты.Добавить("ТестДолжен_ПодключитьБиблиотекуСГлобальнымКонтекстомПоследней"); + ВсеТесты.Добавить("ТестДолжен_ЗагрузитьСценарийСБиблиотекойСГлобальнымКонтекстом"); + ВсеТесты.Добавить("ТестДолжен_ВычислитьПослеПодключенияГлобальногоКонтекста"); + ВсеТесты.Добавить("ТестДолжен_ВызватьМетодыОбъектовСозданныхДоПодключенияКомпоненты"); + + Возврат ВсеТесты; + +КонецФункции + +Функция ВыполнитьСценарий(Знач ИмяСценария) + + ПутьСценария = ОбъединитьПути(ТекущийСценарий().Каталог, "global-context", ИмяСценария); + Команда = СтрШаблон("dotnet ""%1"" -encoding=utf-8 ""%2""", ОбъединитьПути(КаталогПрограммы(), "oscript.dll"), ПутьСценария); + Процесс = СоздатьПроцесс(Команда, , Истина, , КодировкаТекста.UTF8); + Процесс.Запустить(); + Процесс.ОжидатьЗавершения(); + + Вывод = СокрЛП(СтрЗаменить(Процесс.ПотокВывода.Прочитать(), Символы.ВК, "")); + Ошибки = СокрЛП(Процесс.ПотокОшибок.Прочитать()); + юТест.ПроверитьРавенство(0, Процесс.КодВозврата, Вывод + Символы.ПС + Ошибки); + + Возврат Вывод; + +КонецФункции + +Процедура ТестДолжен_ПодключитьБиблиотекуСГлобальнымКонтекстомНеПоследней() Экспорт + + Вывод = ВыполнитьСценарий("library-first.os"); + + юТест.ПроверитьРавенство( + "метод модуля + |модуль другой библиотеки + |метод компоненты + |свойство компоненты", Вывод); + +КонецПроцедуры + +Процедура ТестДолжен_ПодключитьБиблиотекуСГлобальнымКонтекстомПоследней() Экспорт + + Вывод = ВыполнитьСценарий("library-last.os"); + + юТест.ПроверитьРавенство( + "метод модуля + |модуль другой библиотеки + |метод компоненты + |свойство компоненты", Вывод); + +КонецПроцедуры + +Процедура ТестДолжен_ЗагрузитьСценарийСБиблиотекойСГлобальнымКонтекстом() Экспорт + + Вывод = ВыполнитьСценарий("load-script.os"); + + юТест.ПроверитьРавенство("модуль другой библиотеки / метод компоненты / метод модуля", Вывод); + +КонецПроцедуры + +Процедура ТестДолжен_ВычислитьПослеПодключенияГлобальногоКонтекста() Экспорт + + Вывод = ВыполнитьСценарий("attach-at-runtime.os"); + + юТест.ПроверитьРавенство( + "переменная модуля / метод модуля / переменная модуля + |переменная модуля / метод модуля / переменная модуля + |метод компоненты + |метод модуля", Вывод); + +КонецПроцедуры + +Процедура ТестДолжен_ВызватьМетодыОбъектовСозданныхДоПодключенияКомпоненты() Экспорт + + Вывод = ВыполнитьСценарий("objects-around-attach.os"); + + юТест.ПроверитьРавенство( + "поле объекта / поле объекта / метод объекта + |поле объекта / поле объекта / метод объекта", Вывод); + +КонецПроцедуры diff --git a/tests/global-context/attach-at-runtime.os b/tests/global-context/attach-at-runtime.os new file mode 100644 index 000000000..80d15380f --- /dev/null +++ b/tests/global-context/attach-at-runtime.os @@ -0,0 +1,17 @@ +// Компонента подключается во время работы модуля: до исправления второй Вычислить +// брал свойство компоненты вместо переменной модуля или падал на номере области +Перем ПеременнаяМодуля; + +Функция МетодМодуля() + Возврат "метод модуля"; +КонецФункции + +ПеременнаяМодуля = "переменная модуля"; +Для Сч = 1 По 2 Цикл + Сообщить(СтрШаблон("%1 / %2 / %3", ПеременнаяМодуля, МетодМодуля(), Вычислить("ПеременнаяМодуля"))); + Если Сч = 1 Тогда + ПодключитьВнешнююКомпоненту(ОбъединитьПути(ТекущийСценарий().Каталог, "..", "component", "Component.dll")); + КонецЕсли; +КонецЦикла; +Сообщить(Вычислить("ГлобальныйМетодКомпоненты()")); +Выполнить("Сообщить(МетодМодуля())"); diff --git a/tests/global-context/library-first.os b/tests/global-context/library-first.os new file mode 100644 index 000000000..78c0c98a3 --- /dev/null +++ b/tests/global-context/library-first.os @@ -0,0 +1,12 @@ +// #911: библиотека, которая добавляет глобальный контекст, подключена не последней +#Использовать "../component" +#Использовать "other-lib" + +Функция МетодМодуля() + Возврат "метод модуля"; +КонецФункции + +Сообщить(МетодМодуля()); +Сообщить(ДругойМодуль.Привет()); +Сообщить(Вычислить("ГлобальныйМетодКомпоненты()")); +Сообщить(Вычислить("ГлобальноеСвойствоКомпоненты")); diff --git a/tests/global-context/library-last.os b/tests/global-context/library-last.os new file mode 100644 index 000000000..e1828a506 --- /dev/null +++ b/tests/global-context/library-last.os @@ -0,0 +1,12 @@ +// #911: библиотека, которая добавляет глобальный контекст, подключена последней +#Использовать "other-lib" +#Использовать "../component" + +Функция МетодМодуля() + Возврат "метод модуля"; +КонецФункции + +Сообщить(МетодМодуля()); +Сообщить(ДругойМодуль.Привет()); +Сообщить(Вычислить("ГлобальныйМетодКомпоненты()")); +Сообщить(Вычислить("ГлобальноеСвойствоКомпоненты")); diff --git a/tests/global-context/load-script.os b/tests/global-context/load-script.os new file mode 100644 index 000000000..83caa8b7c --- /dev/null +++ b/tests/global-context/load-script.os @@ -0,0 +1,3 @@ +// #344: сценарий с такими библиотеками загружается из другого, как тест в testrunner +Сценарий = ЗагрузитьСценарий(ОбъединитьПути(ТекущийСценарий().Каталог, "loaded-script.os")); +Сообщить(Сценарий.Проверить()); diff --git a/tests/global-context/loaded-script.os b/tests/global-context/loaded-script.os new file mode 100644 index 000000000..dc9498b08 --- /dev/null +++ b/tests/global-context/loaded-script.os @@ -0,0 +1,10 @@ +#Использовать "../component" +#Использовать "other-lib" + +Функция Проверить() Экспорт + Возврат ДругойМодуль.Привет() + " / " + Вычислить("ГлобальныйМетодКомпоненты()") + " / " + МетодМодуля(); +КонецФункции + +Функция МетодМодуля() + Возврат "метод модуля"; +КонецФункции diff --git a/tests/global-context/objects-around-attach.os b/tests/global-context/objects-around-attach.os new file mode 100644 index 000000000..0a4b20d6e --- /dev/null +++ b/tests/global-context/objects-around-attach.os @@ -0,0 +1,7 @@ +// Объекты сценариев, созданные до и после подключения компоненты +Каталог = ТекущийСценарий().Каталог; +До = ЗагрузитьСценарий(ОбъединитьПути(Каталог, "script-object.os")); +ПодключитьВнешнююКомпоненту(ОбъединитьПути(Каталог, "..", "component", "Component.dll")); +После = ЗагрузитьСценарий(ОбъединитьПути(Каталог, "script-object.os")); +Сообщить(До.Получить()); +Сообщить(После.Получить()); diff --git "a/tests/global-context/other-lib/\320\234\320\276\320\264\321\203\320\273\320\270/\320\224\321\200\321\203\320\263\320\276\320\271\320\234\320\276\320\264\321\203\320\273\321\214.os" "b/tests/global-context/other-lib/\320\234\320\276\320\264\321\203\320\273\320\270/\320\224\321\200\321\203\320\263\320\276\320\271\320\234\320\276\320\264\321\203\320\273\321\214.os" new file mode 100644 index 000000000..9fa7a05c9 --- /dev/null +++ "b/tests/global-context/other-lib/\320\234\320\276\320\264\321\203\320\273\320\270/\320\224\321\200\321\203\320\263\320\276\320\271\320\234\320\276\320\264\321\203\320\273\321\214.os" @@ -0,0 +1,3 @@ +Функция Привет() Экспорт + Возврат "модуль другой библиотеки"; +КонецФункции diff --git a/tests/global-context/script-object.os b/tests/global-context/script-object.os new file mode 100644 index 000000000..d8c86bb26 --- /dev/null +++ b/tests/global-context/script-object.os @@ -0,0 +1,11 @@ +Перем Поле; + +Функция Получить() Экспорт + Возврат Поле + " / " + Вычислить("Поле") + " / " + МетодОбъекта(); +КонецФункции + +Функция МетодОбъекта() + Возврат "метод объекта"; +КонецФункции + +Поле = "поле объекта";