From a1114df316daa8c5b81c489dcf96faf717558c63 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Sat, 5 Mar 2022 10:54:06 +0200 Subject: [PATCH 01/31] Module handling fixes and improvements --- Jint.Tests.Test262/Language/ModuleTestHost.cs | 51 ----- Jint.Tests.Test262/Language/ModuleTests.cs | 49 +---- Jint.Tests.Test262/Test262Test.cs | 12 +- Jint.Tests/Runtime/ModuleTests.cs | 19 ++ Jint/Engine.Modules.cs | 32 ++- Jint/Engine.cs | 29 +-- Jint/EsprimaExtensions.cs | 10 +- Jint/HoistingScope.cs | 2 +- Jint/ModuleBuilder.cs | 2 +- Jint/Native/Function/ClassDefinition.cs | 4 +- Jint/Options.cs | 5 + .../Environments/ModuleEnvironmentRecord.cs | 13 +- Jint/Runtime/Host.cs | 42 ++-- .../Expressions/JintClassExpression.cs | 2 +- .../Interpreter/Expressions/JintExpression.cs | 1 + .../Expressions/JintImportExpression.cs | 50 +++++ .../JintClassDeclarationStatement.cs | 2 +- .../JintExportDefaultDeclaration.cs | 81 +++++++- .../Statements/JintExportNamedDeclaration.cs | 25 +-- .../Statements/JintImportDeclaration.cs | 12 +- Jint/Runtime/Modules/DefaultModuleLoader.cs | 5 + Jint/Runtime/Modules/JsModule.cs | 187 +++++++++--------- Jint/Runtime/Modules/ModuleNamespace.cs | 65 +++++- 23 files changed, 426 insertions(+), 274 deletions(-) delete mode 100644 Jint.Tests.Test262/Language/ModuleTestHost.cs create mode 100644 Jint/Runtime/Interpreter/Expressions/JintImportExpression.cs diff --git a/Jint.Tests.Test262/Language/ModuleTestHost.cs b/Jint.Tests.Test262/Language/ModuleTestHost.cs deleted file mode 100644 index 89b5604eae..0000000000 --- a/Jint.Tests.Test262/Language/ModuleTestHost.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using System.IO; -using Jint.Native; -using Jint.Native.Object; -using Jint.Runtime; -using Jint.Runtime.Interop; - -namespace Jint.Tests.Test262.Language -{ - // Hacky way to get objects from assert.js and sta.js into the module context - internal sealed class ModuleTestHost : Host - { - private readonly static Dictionary _staticValues = new(); - - static ModuleTestHost() - { - var assemblyPath = new Uri(typeof(ModuleTestHost).GetTypeInfo().Assembly.Location).LocalPath; - var assemblyDirectory = new FileInfo(assemblyPath).Directory; - - var basePath = assemblyDirectory.Parent.Parent.Parent.FullName; - - var engine = new Engine(); - var assertSource = File.ReadAllText(Path.Combine(basePath, "harness", "assert.js")); - var staSource = File.ReadAllText(Path.Combine(basePath, "harness", "sta.js")); - - engine.Execute(assertSource); - engine.Execute(staSource); - - _staticValues["assert"] = engine.GetValue("assert"); - _staticValues["Test262Error"] = engine.GetValue("Test262Error"); - _staticValues["$ERROR"] = engine.GetValue("$ERROR"); - _staticValues["$DONOTEVALUATE"] = engine.GetValue("$DONOTEVALUATE"); - - _staticValues["print"] = new ClrFunctionInstance(engine, "print", (thisObj, args) => TypeConverter.ToString(args.At(0))); - } - - protected override ObjectInstance CreateGlobalObject(Realm realm) - { - var globalObj = base.CreateGlobalObject(realm); - - foreach (var key in _staticValues.Keys) - { - globalObj.FastAddProperty(key, _staticValues[key], true, true, true); - } - - return globalObj; - } - } -} diff --git a/Jint.Tests.Test262/Language/ModuleTests.cs b/Jint.Tests.Test262/Language/ModuleTests.cs index b28be07f76..122fdb312f 100644 --- a/Jint.Tests.Test262/Language/ModuleTests.cs +++ b/Jint.Tests.Test262/Language/ModuleTests.cs @@ -1,10 +1,4 @@ -using Jint.Runtime; -using Jint.Runtime.Modules; -using System; -using System.IO; -using System.Reflection; using Xunit; -using Xunit.Sdk; namespace Jint.Tests.Test262.Language; @@ -15,7 +9,7 @@ public class ModuleTests : Test262Test [MemberData(nameof(SourceFiles), "language\\module-code", true, Skip = "Skipped")] protected void ModuleCode(SourceFile sourceFile) { - RunModuleTest(sourceFile); + RunTestInternal(sourceFile); } [Theory(DisplayName = "language\\export")] @@ -23,7 +17,7 @@ protected void ModuleCode(SourceFile sourceFile) [MemberData(nameof(SourceFiles), "language\\export", true, Skip = "Skipped")] protected void Export(SourceFile sourceFile) { - RunModuleTest(sourceFile); + RunTestInternal(sourceFile); } [Theory(DisplayName = "language\\import")] @@ -31,43 +25,6 @@ protected void Export(SourceFile sourceFile) [MemberData(nameof(SourceFiles), "language\\import", true, Skip = "Skipped")] protected void Import(SourceFile sourceFile) { - RunModuleTest(sourceFile); - } - - private static void RunModuleTest(SourceFile sourceFile) - { - if (sourceFile.Skip) - { - return; - } - - var code = sourceFile.Code; - - var options = new Options(); - options.Host.Factory = _ => new ModuleTestHost(); - options.EnableModules(Path.Combine(BasePath, "test")); - - var engine = new Engine(options); - - var negative = code.IndexOf("negative:", StringComparison.OrdinalIgnoreCase) != -1; - string lastError = null; - - try - { - engine.LoadModule(sourceFile.FullPath); - } - catch (JavaScriptException ex) - { - lastError = ex.ToString(); - } - catch (Exception ex) - { - lastError = ex.ToString(); - } - - if (!negative && !string.IsNullOrWhiteSpace(lastError)) - { - throw new XunitException(lastError); - } + RunTestInternal(sourceFile); } } \ No newline at end of file diff --git a/Jint.Tests.Test262/Test262Test.cs b/Jint.Tests.Test262/Test262Test.cs index 018f054f34..3b90df4501 100644 --- a/Jint.Tests.Test262/Test262Test.cs +++ b/Jint.Tests.Test262/Test262Test.cs @@ -101,6 +101,7 @@ protected void RunTestCode(string fileName, string code, bool strict) var engine = new Engine(cfg => cfg .LocalTimeZone(_pacificTimeZone) .Strict(strict) + .EnableModules(Path.Combine(BasePath, "test", Path.GetDirectoryName(fileName)!)) ); engine.Execute(Sources["sta.js"]); @@ -151,6 +152,8 @@ protected void RunTestCode(string fileName, string code, bool strict) engine.Execute(Sources[file.Trim()]); } } + + var module = Regex.IsMatch(code, @"flags:\s*?\[.*?module.*?]"); if (code.IndexOf("propertyHelper.js", StringComparison.OrdinalIgnoreCase) != -1) { @@ -162,7 +165,14 @@ protected void RunTestCode(string fileName, string code, bool strict) bool negative = code.IndexOf("negative:", StringComparison.Ordinal) > -1; try { - engine.Execute(new JavaScriptParser(code, new ParserOptions(fileName)).ParseScript()); + if (module) + { + engine.Execute(new JavaScriptParser(code, new ParserOptions(fileName)).ParseModule()); + } + else + { + engine.Execute(new JavaScriptParser(code, new ParserOptions(fileName)).ParseScript()); + } } catch (JavaScriptException j) { diff --git a/Jint.Tests/Runtime/ModuleTests.cs b/Jint.Tests/Runtime/ModuleTests.cs index c1c1833185..dc5583ef1e 100644 --- a/Jint.Tests/Runtime/ModuleTests.cs +++ b/Jint.Tests/Runtime/ModuleTests.cs @@ -1,8 +1,10 @@ #if(NET6_0_OR_GREATER) using System.IO; using System.Reflection; +using Jint.Runtime.Modules; #endif using System; +using Esprima; using Jint.Native; using Jint.Runtime; using Xunit; @@ -234,6 +236,23 @@ public void CanImportFromFile() Assert.Equal("John Doe", result); } + + [Fact] + public void CanReferenceModuleImportFromFileInInlineScript() + { + var engine = new Engine(options => options.EnableModules(GetBasePath())); + const string script = @" + import { formatName } from './modules/format-name.js' + formatName('John', 'Doe'); + "; + + var ex = Assert.Throws(() => engine.Evaluate(script)); + Assert.Equal("Cannot use import/export statements outside a module", ex.Message); + + var value = engine.Evaluate(script, SourceType.Module); + Assert.IsType(value); + } + private static string GetBasePath() { diff --git a/Jint/Engine.Modules.cs b/Jint/Engine.Modules.cs index bd646684bd..f332532576 100644 --- a/Jint/Engine.Modules.cs +++ b/Jint/Engine.Modules.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using Esprima; +using Esprima.Ast; using Jint.Native; using Jint.Native.Object; using Jint.Native.Promise; @@ -41,7 +42,7 @@ internal JsModule LoadModule(string? referencingModuleLocation, string specifier if (_builders.TryGetValue(specifier, out var moduleBuilder)) { var parsedModule = moduleBuilder.Parse(); - module = new JsModule(this, _host.CreateRealm(), parsedModule, null, false); + module = new JsModule(this, Realm, parsedModule, null, false); // Early link is required because we need to bind values before returning module.Link(); moduleBuilder.BindExportedValues(module); @@ -50,7 +51,7 @@ internal JsModule LoadModule(string? referencingModuleLocation, string specifier else { var parsedModule = ModuleLoader.LoadModule(this, moduleResolution); - module = new JsModule(this, _host.CreateRealm(), parsedModule, moduleResolution.Uri?.LocalPath, false); + module = new JsModule(this, Realm, parsedModule, moduleResolution.Uri?.LocalPath, false); } _modules[moduleResolution.Key] = module; @@ -79,13 +80,30 @@ public void AddModule(string specifier, ModuleBuilder moduleBuilder) public ObjectInstance ImportModule(string specifier) { - var moduleResolution = ModuleLoader.Resolve(null, specifier); + var moduleResolution = ModuleLoader.Resolve(referencingModuleLocation: null, specifier); if (!_modules.TryGetValue(moduleResolution.Key, out var module)) { module = LoadModule(null, specifier); } + return Execute(specifier, module); + } + + public Engine Execute(Module module) + { + Evaluate(module); + return this; + } + + public JsValue Evaluate(Module module) + { + var jsModule = new JsModule(this, Realm, module, location: null, async: false); + return Execute(specifier: null, jsModule); + } + + private ObjectInstance Execute(string? specifier, JsModule module) + { if (module.Status == ModuleStatus.Unlinked) { module.Link(); @@ -108,11 +126,7 @@ public ObjectInstance ImportModule(string specifier) } } - if (evaluationResult == null) - { - ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module evaluation did not return a promise"); - } - else if (evaluationResult is not PromiseInstance promise) + if (evaluationResult is not PromiseInstance promise) { ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module evaluation did not return a promise: {evaluationResult.Type}"); } @@ -129,7 +143,7 @@ public ObjectInstance ImportModule(string specifier) if (module.Status == ModuleStatus.Evaluated) { // TODO what about callstack and thrown exceptions? - RunAvailableContinuations(_eventLoop); + RunAvailableContinuations(); return JsModule.GetModuleNamespace(module); } diff --git a/Jint/Engine.cs b/Jint/Engine.cs index e742837607..08b0b4a874 100644 --- a/Jint/Engine.cs +++ b/Jint/Engine.cs @@ -239,20 +239,24 @@ public void ResetCallStack() CallStack.Clear(); } - public JsValue Evaluate(string source) - => Execute(source, DefaultParserOptions)._completionValue; + public JsValue Evaluate(string source, SourceType sourceType = SourceType.Script) + => Evaluate(source, DefaultParserOptions, sourceType); - public JsValue Evaluate(string source, ParserOptions parserOptions) - => Execute(source, parserOptions)._completionValue; + public JsValue Evaluate(string source, ParserOptions parserOptions, SourceType sourceType = SourceType.Script) + => sourceType == SourceType.Script + ? Evaluate(new JavaScriptParser(source, parserOptions).ParseScript()) + : Evaluate(new JavaScriptParser(source, parserOptions).ParseModule()); public JsValue Evaluate(Script script) => Execute(script)._completionValue; - public Engine Execute(string source) - => Execute(source, DefaultParserOptions); + public Engine Execute(string source, SourceType sourceType = SourceType.Script) + => Execute(source, DefaultParserOptions, sourceType); - public Engine Execute(string source, ParserOptions parserOptions) - => Execute(new JavaScriptParser(source, parserOptions).ParseScript()); + public Engine Execute(string source, ParserOptions parserOptions, SourceType sourceType = SourceType.Script) + => sourceType == SourceType.Script + ? Execute(new JavaScriptParser(source, parserOptions).ParseScript()) + : Execute(new JavaScriptParser(source, parserOptions).ParseModule()); public Engine Execute(Script script) { @@ -284,7 +288,7 @@ Engine DoInvoke() } // TODO what about callstack and thrown exceptions? - RunAvailableContinuations(_eventLoop); + RunAvailableContinuations(); _completionValue = result.GetValueOrDefault(); @@ -320,7 +324,7 @@ public ManualPromise RegisterPromise() Action SettleWith(FunctionInstance settle) => value => { settle.Call(JsValue.Undefined, new[] {value}); - RunAvailableContinuations(_eventLoop); + RunAvailableContinuations(); }; return new ManualPromise(promise, SettleWith(resolve), SettleWith(reject)); @@ -331,10 +335,9 @@ internal void AddToEventLoop(Action continuation) _eventLoop.Events.Enqueue(continuation); } - - private static void RunAvailableContinuations(EventLoop loop) + internal void RunAvailableContinuations() { - var queue = loop.Events; + var queue = _eventLoop.Events; while (true) { diff --git a/Jint/EsprimaExtensions.cs b/Jint/EsprimaExtensions.cs index 31b12bebca..f95db78b7d 100644 --- a/Jint/EsprimaExtensions.cs +++ b/Jint/EsprimaExtensions.cs @@ -320,7 +320,7 @@ internal static void GetExportEntries(this ExportDeclaration export, List GetExportNames(StatementListItem declaration) break; case VariableDeclaration variableDeclaration: - var declarators = variableDeclaration.Declarations; - foreach (var declarator in declarators) + ref readonly var declarators = ref variableDeclaration.Declarations; + for (var i = 0; i < declarators.Count; i++) { + var declarator = declarators[i]; var varName = declarator.Id.As()?.Name; if (varName is not null) { diff --git a/Jint/HoistingScope.cs b/Jint/HoistingScope.cs index 78e9953466..7819545450 100644 --- a/Jint/HoistingScope.cs +++ b/Jint/HoistingScope.cs @@ -30,7 +30,7 @@ private HoistingScope( } public static HoistingScope GetProgramLevelDeclarations( - Script script, + Program script, bool collectVarNames = false, bool collectLexicalNames = false) { diff --git a/Jint/ModuleBuilder.cs b/Jint/ModuleBuilder.cs index 00f6ac3d52..2616e40035 100644 --- a/Jint/ModuleBuilder.cs +++ b/Jint/ModuleBuilder.cs @@ -16,7 +16,7 @@ public sealed class ModuleBuilder private readonly List _sourceRaw = new(); private readonly Dictionary _exports = new(); - public ModuleBuilder(Engine engine) + internal ModuleBuilder(Engine engine) { _engine = engine; } diff --git a/Jint/Native/Function/ClassDefinition.cs b/Jint/Native/Function/ClassDefinition.cs index fefec0c377..23e2b627ef 100644 --- a/Jint/Native/Function/ClassDefinition.cs +++ b/Jint/Native/Function/ClassDefinition.cs @@ -35,11 +35,11 @@ static MethodDefinition CreateConstructorMethodDefinition(string source) } public ClassDefinition( - Identifier? className, + string? className, Expression? superClass, ClassBody body) { - _className = className?.Name; + _className = className; _superClass = superClass; _body = body; } diff --git a/Jint/Options.cs b/Jint/Options.cs index a34ec76950..8e422f16c9 100644 --- a/Jint/Options.cs +++ b/Jint/Options.cs @@ -74,6 +74,11 @@ public class Options /// public IReferenceResolver ReferenceResolver { get; set; } = DefaultReferenceResolver.Instance; + /// + /// Whether to return details of unhandled exceptions. Poses a security risk. Defaults to false. + /// + public bool IncludeExceptionDetails { get; set; } + /// /// Called by the instance that loads this /// once it is loaded. diff --git a/Jint/Runtime/Environments/ModuleEnvironmentRecord.cs b/Jint/Runtime/Environments/ModuleEnvironmentRecord.cs index df2bf7019f..c4357f1a74 100644 --- a/Jint/Runtime/Environments/ModuleEnvironmentRecord.cs +++ b/Jint/Runtime/Environments/ModuleEnvironmentRecord.cs @@ -16,18 +16,26 @@ internal ModuleEnvironmentRecord(Engine engine) : base(engine, false) { } + /// + /// https://tc39.es/ecma262/#sec-module-environment-records-getthisbinding + /// public override JsValue GetThisBinding() { return Undefined; } + /// + /// https://tc39.es/ecma262/#sec-createimportbinding + /// public void CreateImportBinding(string importName, JsModule module, string name) { _hasBindings = true; _importBindings[importName] = new IndirectBinding(module, name); } - // https://tc39.es/ecma262/#sec-module-environment-records-getbindingvalue-n-s + /// + /// https://tc39.es/ecma262/#sec-module-environment-records-getbindingvalue-n-s + /// public override JsValue GetBindingValue(string name, bool strict) { if (_importBindings.TryGetValue(name, out var indirectBinding)) @@ -50,6 +58,9 @@ internal override bool TryGetBinding(in BindingName name, bool strict, out Bindi return base.TryGetBinding(name, strict, out binding, out value); } + /// + /// https://tc39.es/ecma262/#sec-module-environment-records-hasthisbinding + /// public override bool HasThisBinding() => true; private readonly record struct IndirectBinding(JsModule Module, string BindingName); diff --git a/Jint/Runtime/Host.cs b/Jint/Runtime/Host.cs index 05a9ba4272..194c145ce3 100644 --- a/Jint/Runtime/Host.cs +++ b/Jint/Runtime/Host.cs @@ -1,3 +1,5 @@ +#nullable enable + using Jint.Native; using Jint.Native.Global; using Jint.Native.Object; @@ -11,7 +13,20 @@ namespace Jint.Runtime { public class Host { - protected Engine Engine { get; private set; } + private Engine? _engine; + + protected Engine Engine + { + get + { + if (_engine is null) + { + ExceptionHelper.ThrowInvalidOperationException("Initialize has not been called"); + } + return _engine!; + } + private set => _engine = value; + } /// /// Initializes the host. @@ -93,8 +108,6 @@ protected virtual void CreateIntrinsics(Realm realmRec) /// /// https://tc39.es/ecma262/#sec-hostensurecancompilestrings /// - /// - /// public virtual void EnsureCanCompileStrings(Realm callerRealm, Realm evalRealm) { } @@ -102,46 +115,35 @@ public virtual void EnsureCanCompileStrings(Realm callerRealm, Realm evalRealm) /// /// https://tc39.es/ecma262/#sec-hostresolveimportedmodule /// - /// - /// - /// - protected internal virtual JsModule ResolveImportedModule(JsModule referencingModule, string specifier) + protected internal virtual JsModule ResolveImportedModule(JsModule? referencingModule, string specifier) { - return Engine.LoadModule(referencingModule.Location, specifier); + return Engine.LoadModule(referencingModule?.Location, specifier); } /// /// https://tc39.es/ecma262/#sec-hostimportmoduledynamically /// - /// - /// - /// - internal virtual void ImportModuleDynamically(JsModule referencingModule, string specifier, PromiseCapability promiseCapability) + internal virtual void ImportModuleDynamically(JsModule? referencingModule, string specifier, PromiseCapability promiseCapability) { var promise = Engine.RegisterPromise(); try { - Engine.LoadModule(referencingModule.Location, specifier); + Engine.LoadModule(referencingModule?.Location, specifier); promise.Resolve(JsValue.Undefined); - } catch (JavaScriptException ex) { promise.Reject(ex.Error); } - FinishDynamicImport(referencingModule, specifier, promiseCapability, (PromiseInstance)promise.Promise); + FinishDynamicImport(referencingModule, specifier, promiseCapability, (PromiseInstance) promise.Promise); } /// /// https://tc39.es/ecma262/#sec-finishdynamicimport /// - /// - /// - /// - /// - internal virtual void FinishDynamicImport(JsModule referencingModule, string specifier, PromiseCapability promiseCapability, PromiseInstance innerPromise) + internal virtual void FinishDynamicImport(JsModule? referencingModule, string specifier, PromiseCapability promiseCapability, PromiseInstance innerPromise) { var onFulfilled = new ClrFunctionInstance(Engine, "", (thisObj, args) => { diff --git a/Jint/Runtime/Interpreter/Expressions/JintClassExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintClassExpression.cs index 863c762fae..f17cce1c54 100644 --- a/Jint/Runtime/Interpreter/Expressions/JintClassExpression.cs +++ b/Jint/Runtime/Interpreter/Expressions/JintClassExpression.cs @@ -9,7 +9,7 @@ internal sealed class JintClassExpression : JintExpression public JintClassExpression(ClassExpression expression) : base(expression) { - _classDefinition = new ClassDefinition(expression.Id, expression.SuperClass, expression.Body); + _classDefinition = new ClassDefinition(expression.Id?.Name, expression.SuperClass, expression.Body); } protected override ExpressionResult EvaluateInternal(EvaluationContext context) diff --git a/Jint/Runtime/Interpreter/Expressions/JintExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintExpression.cs index a48bf85be0..f7d9476c98 100644 --- a/Jint/Runtime/Interpreter/Expressions/JintExpression.cs +++ b/Jint/Runtime/Interpreter/Expressions/JintExpression.cs @@ -193,6 +193,7 @@ protected internal static JintExpression Build(Engine engine, Expression express Nodes.TemplateLiteral => new JintTemplateLiteralExpression((TemplateLiteral) expression), Nodes.TaggedTemplateExpression => new JintTaggedTemplateExpression((TaggedTemplateExpression) expression), Nodes.ClassExpression => new JintClassExpression((ClassExpression) expression), + Nodes.Import => new JintImportExpression((Import) expression), Nodes.Super => new JintSuperExpression((Super) expression), Nodes.MetaProperty => new JintMetaPropertyExpression((MetaProperty) expression), Nodes.ChainExpression => ((ChainExpression) expression).Expression.Type == Nodes.CallExpression diff --git a/Jint/Runtime/Interpreter/Expressions/JintImportExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintImportExpression.cs new file mode 100644 index 0000000000..7e5e76c871 --- /dev/null +++ b/Jint/Runtime/Interpreter/Expressions/JintImportExpression.cs @@ -0,0 +1,50 @@ +#nullable enable + +using Esprima.Ast; +using Jint.Native.Promise; +using Jint.Runtime.Modules; + +namespace Jint.Runtime.Interpreter.Expressions; + +internal sealed class JintImportExpression : JintExpression +{ + private JintExpression _importExpression; + + public JintImportExpression(Import expression) : base(expression) + { + _initialized = false; + _importExpression = null!; + } + + protected override void Initialize(EvaluationContext context) + { + var expression = ((Import) _expression).Source; + _importExpression = Build(context.Engine, expression!); + } + + /// + /// https://tc39.es/ecma262/#sec-import-calls + /// + protected override ExpressionResult EvaluateInternal(EvaluationContext context) + { + var module = context.Engine.GetActiveScriptOrModule().AsModule(context.Engine, context.LastSyntaxNode.Location); + + var argRef = _importExpression.Evaluate(context); + context.Engine.RunAvailableContinuations(); + var value = context.Engine.GetValue(argRef.Value); + var specifier = value.UnwrapIfPromise(); + + if (specifier is ModuleNamespace) + { + // already resolved + return NormalCompletion(value); + } + + var promiseCapability = PromiseConstructor.NewPromiseCapability(context.Engine, context.Engine.Realm.Intrinsics.Promise); + var specifierString = TypeConverter.ToString(specifier); + + // 6.IfAbruptRejectPromise(specifierString, promiseCapability); + context.Engine._host.ImportModuleDynamically(module, specifierString, promiseCapability); + return NormalCompletion(promiseCapability.PromiseInstance); + } +} \ No newline at end of file diff --git a/Jint/Runtime/Interpreter/Statements/JintClassDeclarationStatement.cs b/Jint/Runtime/Interpreter/Statements/JintClassDeclarationStatement.cs index 16b76393fe..0929c40555 100644 --- a/Jint/Runtime/Interpreter/Statements/JintClassDeclarationStatement.cs +++ b/Jint/Runtime/Interpreter/Statements/JintClassDeclarationStatement.cs @@ -11,7 +11,7 @@ internal sealed class JintClassDeclarationStatement : JintStatement { - private JintExpression? _init; + private ClassDefinition? _classDefinition; + private JintFunctionDeclarationStatement? _functionDeclaration; + private JintExpression? _assignmentExpression; + private JintExpression? _simpleExpression; public JintExportDefaultDeclaration(ExportDefaultDeclaration statement) : base(statement) { @@ -15,17 +22,75 @@ public JintExportDefaultDeclaration(ExportDefaultDeclaration statement) : base(s protected override void Initialize(EvaluationContext context) { - _init = JintExpression.Build(context.Engine, (Expression)_statement.Declaration); + if (_statement.Declaration is ClassDeclaration classDeclaration) + { + _classDefinition = new ClassDefinition(className: classDeclaration.Id?.Name, classDeclaration.SuperClass, classDeclaration.Body); + } + else if (_statement.Declaration is FunctionDeclaration functionDeclaration) + { + _functionDeclaration = new JintFunctionDeclarationStatement(functionDeclaration); + } + else if (_statement.Declaration is AssignmentExpression assignmentExpression) + { + _assignmentExpression = JintAssignmentExpression.Build(context.Engine, assignmentExpression); + } + else + { + _simpleExpression = JintExpression.Build(context.Engine, (Expression) _statement.Declaration); + } } - // https://tc39.es/ecma262/#sec-exports-runtime-semantics-evaluation + /// + /// https://tc39.es/ecma262/#sec-exports-runtime-semantics-evaluation + /// protected override Completion ExecuteInternal(EvaluationContext context) { - var module = context.Engine.GetActiveScriptOrModule().AsModule(context.Engine, context.LastSyntaxNode.Location); - - var completion = _init?.GetValue(context) ?? Completion.Empty(); - module._environment.CreateImmutableBindingAndInitialize("*default*", true, completion.Value); + var env = context.Engine.ExecutionContext.LexicalEnvironment; + JsValue value; + if (_classDefinition is not null) + { + value = _classDefinition.BuildConstructor(context, env); + var classBinding = _classDefinition._className; + if (classBinding != null) + { + env.CreateMutableBinding(classBinding); + env.InitializeBinding(classBinding, value); + } + } + else if (_functionDeclaration is not null) + { + value = _functionDeclaration.Execute(context).GetValueOrDefault(); + } + else if (_assignmentExpression is not null) + { + value = _assignmentExpression.GetValue(context).GetValueOrDefault(); + } + else + { + value = _simpleExpression!.GetValue(context).GetValueOrDefault(); + } + if (value is ObjectInstance oi && !oi.HasOwnProperty("name")) + { + oi.SetFunctionName("default"); + } + + env.InitializeBinding("*default*", value); return Completion.Empty(); } -} + + /// + /// https://tc39.es/ecma262/#sec-initializeboundname + /// + private void InitializeBoundName(string name, JsValue value, EnvironmentRecord? environment) + { + if (environment is not null) + { + environment.InitializeBinding(name, value); + } + else + { + ExceptionHelper.ThrowNotImplementedException(); + } + } +} \ No newline at end of file diff --git a/Jint/Runtime/Interpreter/Statements/JintExportNamedDeclaration.cs b/Jint/Runtime/Interpreter/Statements/JintExportNamedDeclaration.cs index 89cc09f445..0b0257b925 100644 --- a/Jint/Runtime/Interpreter/Statements/JintExportNamedDeclaration.cs +++ b/Jint/Runtime/Interpreter/Statements/JintExportNamedDeclaration.cs @@ -2,6 +2,7 @@ using Esprima.Ast; using Jint.Native; +using Jint.Runtime.Environments; using Jint.Runtime.Interpreter.Expressions; namespace Jint.Runtime.Interpreter.Statements; @@ -12,10 +13,7 @@ internal sealed class JintExportNamedDeclaration : JintStatement protected override Completion ExecuteInternal(EvaluationContext context) { - var module = context.Engine.GetActiveScriptOrModule().AsModule(context.Engine, context.LastSyntaxNode.Location); + var env = (ModuleEnvironmentRecord) context.Engine.ExecutionContext.LexicalEnvironment; if (_specifiers != null) { + var module = context.Engine.GetActiveScriptOrModule().AsModule(context.Engine, context.LastSyntaxNode.Location); foreach (var specifier in _specifiers) { - if (specifier.Local is not JintIdentifierExpression local || specifier.Exported is not JintIdentifierExpression exported) - { - ExceptionHelper.ThrowSyntaxError(context.Engine.Realm, "", context.LastSyntaxNode.Location); - return default; - } - - var localKey = local._expressionName.Key.Name; - var exportedKey = exported._expressionName.Key.Name; + var localKey = specifier.Local; + var exportedKey = specifier.Exported; if (localKey != exportedKey) { - module._environment.CreateImportBinding(exportedKey, module, localKey); + env.CreateImportBinding(exportedKey, module, localKey); } } } diff --git a/Jint/Runtime/Interpreter/Statements/JintImportDeclaration.cs b/Jint/Runtime/Interpreter/Statements/JintImportDeclaration.cs index 2d55f66a75..d1bbbcb5a2 100644 --- a/Jint/Runtime/Interpreter/Statements/JintImportDeclaration.cs +++ b/Jint/Runtime/Interpreter/Statements/JintImportDeclaration.cs @@ -1,7 +1,6 @@ #nullable enable using Esprima.Ast; -using Jint.Native.Promise; namespace Jint.Runtime.Interpreter.Statements; @@ -17,13 +16,8 @@ protected override void Initialize(EvaluationContext context) protected override Completion ExecuteInternal(EvaluationContext context) { - var module = context.Engine.GetActiveScriptOrModule().AsModule(context.Engine, context.LastSyntaxNode.Location); - var specifier = _statement.Source.StringValue; - var promiseCapability = PromiseConstructor.NewPromiseCapability(context.Engine, context.Engine.Realm.Intrinsics.Promise); - var specifierString = TypeConverter.ToString(specifier); - - // TODO: This comment was in @lahma's code: 6.IfAbruptRejectPromise(specifierString, promiseCapability); - context.Engine._host.ImportModuleDynamically(module, specifierString, promiseCapability); - return NormalCompletion(promiseCapability.PromiseInstance); + // just to ensure module context or valid + context.Engine.GetActiveScriptOrModule().AsModule(context.Engine, context.LastSyntaxNode.Location); + return Completion.Empty(); } } diff --git a/Jint/Runtime/Modules/DefaultModuleLoader.cs b/Jint/Runtime/Modules/DefaultModuleLoader.cs index 558a313a82..e7535b293c 100644 --- a/Jint/Runtime/Modules/DefaultModuleLoader.cs +++ b/Jint/Runtime/Modules/DefaultModuleLoader.cs @@ -145,6 +145,11 @@ public Module LoadModule(Engine engine, ResolvedSpecifier resolved) ExceptionHelper.ThrowSyntaxError(engine.Realm, $"Error while loading module: error in module '{resolved.Uri.LocalPath}': {ex.Error}"); module = null; } + catch (Exception) + { + ExceptionHelper.ThrowJavaScriptException(engine, $"Could not load module {resolved.Uri?.LocalPath}", Completion.Empty()); + module = null; + } return module; } diff --git a/Jint/Runtime/Modules/JsModule.cs b/Jint/Runtime/Modules/JsModule.cs index cb40c57043..d2d46b5321 100644 --- a/Jint/Runtime/Modules/JsModule.cs +++ b/Jint/Runtime/Modules/JsModule.cs @@ -1,7 +1,6 @@ using System; using Esprima.Ast; using System.Collections.Generic; -using System.Linq; using Esprima; using Jint.Native; using Jint.Native.Object; @@ -90,7 +89,6 @@ internal JsModule(Engine engine, Realm realm, Module source, string location, bo out _starExportEntries); //ToDo async modules - } public string Location { get; } @@ -102,7 +100,7 @@ internal JsModule(Engine engine, Realm realm, Module source, string location, bo public static ObjectInstance GetModuleNamespace(JsModule module) { var ns = module._namespace; - if(ns is null) + if (ns is null) { var exportedNames = module.GetExportedNames(); var unambiguousNames = new List(); @@ -110,7 +108,7 @@ public static ObjectInstance GetModuleNamespace(JsModule module) { var name = exportedNames[i]; var resolution = module.ResolveExport(name); - if(resolution is not null) + if (resolution is not null) { unambiguousNames.Add(name); } @@ -143,11 +141,11 @@ internal void BindExportedValue(string name, JsValue value) /// public List GetExportedNames(List exportStarSet = null) { - exportStarSet ??= new(); + exportStarSet ??= new List(); if (exportStarSet.Contains(this)) { //Reached the starting point of an export * circularity - return new(); + return new List(); } exportStarSet.Add(this); @@ -164,7 +162,7 @@ public List GetExportedNames(List exportStarSet = null) exportedNames.Add(e.ImportName ?? e.ExportName); } - for(var i = 0; i < _starExportEntries.Count; i++) + for (var i = 0; i < _starExportEntries.Count; i++) { var e = _starExportEntries[i]; var requestedModule = _engine._host.ResolveImportedModule(this, e.ModuleRequest); @@ -188,36 +186,35 @@ public List GetExportedNames(List exportStarSet = null) /// internal ResolvedBinding ResolveExport(string exportName, List resolveSet = null) { - resolveSet ??= new(); + resolveSet ??= new List(); - for(var i = 0; i < resolveSet.Count; i++) + for (var i = 0; i < resolveSet.Count; i++) { var r = resolveSet[i]; - if(this == r.Module && exportName == r.ExportName) + if (ReferenceEquals(this, r.Module) && exportName == r.ExportName) { - //circular import request + // circular import request return null; } } - resolveSet.Add(new(this, exportName)); - for(var i = 0; i < _localExportEntries.Count; i++) + resolveSet.Add(new ExportResolveSetItem(this, exportName)); + for (var i = 0; i < _localExportEntries.Count; i++) { var e = _localExportEntries[i]; - if (exportName == (e.ImportName ?? e.ExportName)) { return new ResolvedBinding(this, e.LocalName ?? e.ExportName); } } - for(var i = 0; i < _indirectExportEntries.Count; i++) + for (var i = 0; i < _indirectExportEntries.Count; i++) { - var e = _localExportEntries[i]; - if (exportName.Equals(e.ImportName ?? e.ExportName)) + var e = _indirectExportEntries[i]; + if (exportName == (e.ImportName ?? e.ExportName)) { var importedModule = _engine._host.ResolveImportedModule(this, e.ModuleRequest); - if(e.ImportName == "*") + if (e.ImportName == "*") { return new ResolvedBinding(importedModule, "*namespace*"); } @@ -235,25 +232,25 @@ internal ResolvedBinding ResolveExport(string exportName, List 0) { ExceptionHelper.ThrowInvalidOperationException("Error while linking module: One or more modules were not linked"); } @@ -330,19 +328,20 @@ public JsValue Evaluate() var stack = new Stack(); var capability = PromiseConstructor.NewPromiseCapability(_engine, _realm.Intrinsics.Promise); - int asyncEvalOrder = 0; + var asyncEvalOrder = 0; module._topLevelCapability = capability; var result = Evaluate(module, stack, 0, ref asyncEvalOrder); - if(result.Type != CompletionType.Normal) + if (result.Type != CompletionType.Normal) { - foreach(var m in stack) + foreach (var m in stack) { m.Status = ModuleStatus.Evaluated; m._evalError = result; } - capability.Reject.Call(Undefined, new [] { result.Value }); + + capability.Reject.Call(Undefined, new[] { result.Value }); } else { @@ -358,7 +357,7 @@ public JsValue Evaluate() if (!module._asyncEvaluation) { - if(module.Status != ModuleStatus.Evaluated) + if (module.Status != ModuleStatus.Evaluated) { ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); } @@ -366,14 +365,13 @@ public JsValue Evaluate() capability.Resolve.Call(Undefined, Array.Empty()); } - if (stack.Any()) + if (stack.Count > 0) { ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); } } return capability.PromiseInstance; - } /// @@ -381,16 +379,16 @@ public JsValue Evaluate() /// private int Link(JsModule module, Stack stack, int index) { - if(module.Status is - ModuleStatus.Linking or - ModuleStatus.Linked or - ModuleStatus.EvaluatingAsync or - ModuleStatus.Evaluating) + if (module.Status is + ModuleStatus.Linking or + ModuleStatus.Linked or + ModuleStatus.EvaluatingAsync or + ModuleStatus.Evaluating) { return index; } - if(module.Status != ModuleStatus.Unlinked) + if (module.Status != ModuleStatus.Unlinked) { ExceptionHelper.ThrowInvalidOperationException("Error while linking module: Module in an invalid state"); } @@ -409,7 +407,9 @@ ModuleStatus.EvaluatingAsync or //TODO: Should we link only when a module is requested? https://tc39.es/ecma262/#sec-example-cyclic-module-record-graphs Should we support retry? if (requiredModule.Status == ModuleStatus.Unlinked) + { requiredModule.Link(); + } if (requiredModule.Status != ModuleStatus.Linking && requiredModule.Status != ModuleStatus.Linked && @@ -418,20 +418,20 @@ ModuleStatus.EvaluatingAsync or ExceptionHelper.ThrowInvalidOperationException($"Error while linking module: Required module is in an invalid state: {requiredModule.Status}"); } - if(requiredModule.Status == ModuleStatus.Linking && !stack.Contains(requiredModule)) + if (requiredModule.Status == ModuleStatus.Linking && !stack.Contains(requiredModule)) { ExceptionHelper.ThrowInvalidOperationException($"Error while linking module: Required module is in an invalid state: {requiredModule.Status}"); } if (requiredModule.Status == ModuleStatus.Linking) { - module._dfsAncestorIndex = System.Math.Min(module._dfsAncestorIndex, requiredModule._dfsAncestorIndex); + module._dfsAncestorIndex = Math.Min(module._dfsAncestorIndex, requiredModule._dfsAncestorIndex); } } module.InitializeEnvironment(); - if (stack.Count(m => m == module) != 1) + if (StackReferenceCount(stack, module) != 1) { ExceptionHelper.ThrowInvalidOperationException("Error while linking module: Recursive dependency detected"); } @@ -455,7 +455,6 @@ ModuleStatus.EvaluatingAsync or } return index; - } /// @@ -463,9 +462,9 @@ ModuleStatus.EvaluatingAsync or /// private Completion Evaluate(JsModule module, Stack stack, int index, ref int asyncEvalOrder) { - if(module.Status == ModuleStatus.EvaluatingAsync || module.Status == ModuleStatus.Evaluated) + if (module.Status == ModuleStatus.EvaluatingAsync || module.Status == ModuleStatus.Evaluated) { - if(module._evalError is null) + if (module._evalError is null) { return new Completion(CompletionType.Normal, index, null, default); } @@ -473,7 +472,7 @@ private Completion Evaluate(JsModule module, Stack stack, int index, r return module._evalError.Value; } - if(module.Status == ModuleStatus.Evaluating) + if (module.Status == ModuleStatus.Evaluating) { return new Completion(CompletionType.Normal, index, null, default); } @@ -497,7 +496,7 @@ private Completion Evaluate(JsModule module, Stack stack, int index, r { var requiredModule = _engine._host.ResolveImportedModule(module, moduleSpecifier); var result = Evaluate(module, stack, index, ref asyncEvalOrder); - if(result.Type != CompletionType.Normal) + if (result.Type != CompletionType.Normal) { return result; } @@ -538,14 +537,14 @@ private Completion Evaluate(JsModule module, Stack stack, int index, r ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module is in an invalid state: {requiredModule.Status}"); } - if(requiredModule.Status == ModuleStatus.Evaluating) + if (requiredModule.Status == ModuleStatus.Evaluating) { - module._dfsAncestorIndex = System.Math.Min(module._dfsAncestorIndex, requiredModule._dfsAncestorIndex); + module._dfsAncestorIndex = Math.Min(module._dfsAncestorIndex, requiredModule._dfsAncestorIndex); } else { requiredModule = requiredModule._cycleRoot; - if(requiredModule.Status != ModuleStatus.EvaluatingAsync && requiredModule.Status != ModuleStatus.Evaluated) + if (requiredModule.Status != ModuleStatus.EvaluatingAsync && requiredModule.Status != ModuleStatus.Evaluated) { ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); } @@ -560,7 +559,7 @@ private Completion Evaluate(JsModule module, Stack stack, int index, r Completion completion; - if(module._pendingAsyncDependencies > 0 || module._hasTLA) + if (module._pendingAsyncDependencies > 0 || module._hasTLA) { if (module._asyncEvaluation) { @@ -583,7 +582,7 @@ private Completion Evaluate(JsModule module, Stack stack, int index, r completion = module.Execute(); } - if(stack.Count(x => x == module) != 1) + if (StackReferenceCount(stack, module) != 1) { ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); } @@ -593,9 +592,9 @@ private Completion Evaluate(JsModule module, Stack stack, int index, r ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); } - if(module._dfsIndex == module._dfsAncestorIndex) + if (module._dfsIndex == module._dfsAncestorIndex) { - bool done = false; + var done = false; while (!done) { var requiredModule = stack.Pop(); @@ -616,12 +615,26 @@ private Completion Evaluate(JsModule module, Stack stack, int index, r return completion; } + private static int StackReferenceCount(Stack stack, JsModule module) + { + var count = 0; + foreach (var item in stack) + { + if (ReferenceEquals(item, module)) + { + count++; + } + } + + return count; + } + /// /// https://tc39.es/ecma262/#sec-source-text-module-record-initialize-environment /// private void InitializeEnvironment() { - for(var i = 0; i < _indirectExportEntries.Count; i++) + for (var i = 0; i < _indirectExportEntries.Count; i++) { var e = _indirectExportEntries[i]; var resolution = ResolveExport(e.ExportName); @@ -652,7 +665,7 @@ private void InitializeEnvironment() var resolution = importedModule.ResolveExport(ie.ImportName); if (resolution is null || resolution == ResolvedBinding.Ambiguous) { - ExceptionHelper.ThrowSyntaxError(_realm, "Ambigous import statement for identifier " + ie.ImportName); + ExceptionHelper.ThrowSyntaxError(_realm, "Ambiguous import statement for identifier " + ie.ImportName); } if (resolution.BindingName == "*namespace*") @@ -677,23 +690,22 @@ private void InitializeEnvironment() var hoistingScope = HoistingScope.GetModuleLevelDeclarations(_source); var varDeclarations = hoistingScope._variablesDeclarations; - var declaredVarNames = new List(); - if(varDeclarations != null) + var declaredVarNames = new HashSet(); + if (varDeclarations != null) { var boundNames = new List(); - for(var i = 0; i < varDeclarations.Count; i++) + for (var i = 0; i < varDeclarations.Count; i++) { var d = varDeclarations[i]; boundNames.Clear(); d.GetBoundNames(boundNames); - for(var j = 0; j < boundNames.Count; j++) + for (var j = 0; j < boundNames.Count; j++) { var dn = boundNames[j]; - if (!declaredVarNames.Contains(dn)) + if (declaredVarNames.Add(dn)) { env.CreateMutableBinding(dn, false); env.InitializeBinding(dn, Undefined); - declaredVarNames.Add(dn); } } } @@ -701,10 +713,10 @@ private void InitializeEnvironment() var lexDeclarations = hoistingScope._lexicalDeclarations; - if(lexDeclarations != null) + if (lexDeclarations != null) { var boundNames = new List(); - for(var i = 0; i < lexDeclarations.Count; i++) + for (var i = 0; i < lexDeclarations.Count; i++) { var d = lexDeclarations[i]; boundNames.Clear(); @@ -726,14 +738,14 @@ private void InitializeEnvironment() var functionDeclarations = hoistingScope._functionDeclarations; - if(functionDeclarations != null) + if (functionDeclarations != null) { - for(var i = 0; i < functionDeclarations.Count; i++) + for (var i = 0; i < functionDeclarations.Count; i++) { var d = functionDeclarations[i]; - var fn = d.Id.Name; + var fn = d.Id?.Name ?? "*default*"; var fd = new JintFunctionDefinition(_engine, d); - env.CreateImmutableBinding(fn, true); + env.CreateMutableBinding(fn, true); var fo = realm.Intrinsics.Function.InstantiateFunctionObject(fd, env); env.InitializeBinding(fn, fo); } @@ -750,7 +762,7 @@ private Completion Execute(PromiseCapability capability = null) var moduleContext = new ExecutionContext(this, _environment, _environment, null, _realm); if (!_hasTLA) { - using (new StrictModeScope(strict: true)) + using (new StrictModeScope(true, force: true)) { _engine.EnterExecutionContext(moduleContext); var statementList = new JintStatementList(null, _source.Body); @@ -771,7 +783,7 @@ private Completion Execute(PromiseCapability capability = null) /// private Completion ExecuteAsync() { - if((Status != ModuleStatus.Evaluating && Status != ModuleStatus.EvaluatingAsync) || !_hasTLA) + if (Status != ModuleStatus.Evaluating && Status != ModuleStatus.EvaluatingAsync || !_hasTLA) { ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); } @@ -781,10 +793,9 @@ private Completion ExecuteAsync() var onFullfilled = new ClrFunctionInstance(_engine, "fulfilled", AsyncModuleExecutionFulfilled, 1, PropertyFlag.Configurable); var onRejected = new ClrFunctionInstance(_engine, "rejected", AsyncModuleExecutionRejected, 1, PropertyFlag.Configurable); - PromiseOperations.PerformPromiseThen(_engine, (PromiseInstance)capability.PromiseInstance, onFullfilled, onRejected, null); + PromiseOperations.PerformPromiseThen(_engine, (PromiseInstance) capability.PromiseInstance, onFullfilled, onRejected, null); return Execute(capability); - } /// @@ -792,19 +803,19 @@ private Completion ExecuteAsync() /// private void GatherAvailableAncestors(List execList) { - foreach(var m in _asyncParentModules) + foreach (var m in _asyncParentModules) { - if(!execList.Contains(m) && m._cycleRoot._evalError is null) + if (!execList.Contains(m) && m._cycleRoot._evalError is null) { - if(m.Status != ModuleStatus.EvaluatingAsync || - m._evalError is not null || - !m._asyncEvaluation || - m._pendingAsyncDependencies <= 0) + if (m.Status != ModuleStatus.EvaluatingAsync || + m._evalError is not null || + !m._asyncEvaluation || + m._pendingAsyncDependencies <= 0) { ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); } - if(--m._pendingAsyncDependencies == 0) + if (--m._pendingAsyncDependencies == 0) { execList.Add(m); if (!m._hasTLA) @@ -821,10 +832,10 @@ m._evalError is not null || /// private JsValue AsyncModuleExecutionFulfilled(JsValue thisObj, JsValue[] arguments) { - var module = (JsModule)arguments.At(0); + var module = (JsModule) arguments.At(0); if (module.Status == ModuleStatus.Evaluated) { - if(module._evalError is not null) + if (module._evalError is not null) { ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); } @@ -841,7 +852,7 @@ private JsValue AsyncModuleExecutionFulfilled(JsValue thisObj, JsValue[] argumen if (module._topLevelCapability is not null) { - if(module._cycleRoot is null) + if (module._cycleRoot is null) { ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); } @@ -853,7 +864,7 @@ private JsValue AsyncModuleExecutionFulfilled(JsValue thisObj, JsValue[] argumen module.GatherAvailableAncestors(execList); execList.Sort((x, y) => x._asyncEvalOrder - y._asyncEvalOrder); - for(var i = 0; i < execList.Count; i++) + for (var i = 0; i < execList.Count; i++) { var m = execList[i]; if (m.Status == ModuleStatus.Evaluated && m._evalError is null) @@ -867,14 +878,14 @@ private JsValue AsyncModuleExecutionFulfilled(JsValue thisObj, JsValue[] argumen else { var result = m.Execute(); - if(result.Type != CompletionType.Normal) + if (result.Type != CompletionType.Normal) { AsyncModuleExecutionRejected(Undefined, new[] { m, result.Value }); } else { m.Status = ModuleStatus.Evaluated; - if(m._topLevelCapability is not null) + if (m._topLevelCapability is not null) { if (m._cycleRoot is null) { @@ -895,12 +906,12 @@ private JsValue AsyncModuleExecutionFulfilled(JsValue thisObj, JsValue[] argumen /// private JsValue AsyncModuleExecutionRejected(JsValue thisObj, JsValue[] arguments) { - JsModule module = (JsModule)arguments.At(0); - JsValue error = arguments.At(1); + var module = (JsModule) arguments.At(0); + var error = arguments.At(1); if (module.Status == ModuleStatus.Evaluated) { - if(module._evalError is null) + if (module._evalError is null) { ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); } @@ -932,7 +943,7 @@ private JsValue AsyncModuleExecutionRejected(JsValue thisObj, JsValue[] argument ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); } - module._topLevelCapability.Reject.Call(Undefined, new [] { error }); + module._topLevelCapability.Reject.Call(Undefined, new[] { error }); } @@ -949,4 +960,4 @@ public override string ToString() { return $"{Type}: {Location}"; } -} +} \ No newline at end of file diff --git a/Jint/Runtime/Modules/ModuleNamespace.cs b/Jint/Runtime/Modules/ModuleNamespace.cs index a836d7d1b4..a8eb37a4e7 100644 --- a/Jint/Runtime/Modules/ModuleNamespace.cs +++ b/Jint/Runtime/Modules/ModuleNamespace.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; +using Jint.Collections; using Jint.Native; using Jint.Native.Object; +using Jint.Native.Symbol; using Jint.Runtime.Descriptors; namespace Jint.Runtime.Modules; @@ -20,20 +22,47 @@ public ModuleNamespace(Engine engine, JsModule module, List exports) : b _exports = new HashSet(exports); } + protected override void Initialize() + { + var symbols = new SymbolDictionary(1) + { + [GlobalSymbolRegistry.ToStringTag] = new("Module", false, false, false) + }; + SetSymbols(symbols); + } + + /// + /// https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-getprototypeof + /// protected internal override ObjectInstance GetPrototypeOf() => null; + /// + /// https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-setprototypeof-v + /// public override bool SetPrototypeOf(JsValue value) => SetImmutablePrototype(value); + /// + /// https://tc39.es/ecma262/#sec-set-immutable-prototype + /// private bool SetImmutablePrototype(JsValue value) { var current = GetPrototypeOf(); return SameValue(value, current ?? Null); } + /// + /// https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-isextensible + /// public override bool Extensible => false; + /// + /// https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-preventextensions + /// public override bool PreventExtensions() => true; + /// + /// https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-getownproperty-p + /// public override PropertyDescriptor GetOwnProperty(JsValue property) { if (property.IsSymbol()) @@ -52,6 +81,9 @@ public override PropertyDescriptor GetOwnProperty(JsValue property) return new PropertyDescriptor(value, true, true, false); } + /// + /// https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-defineownproperty-p-desc + /// public override bool DefineOwnProperty(JsValue property, PropertyDescriptor desc) { if (property.IsSymbol()) @@ -66,7 +98,22 @@ public override bool DefineOwnProperty(JsValue property, PropertyDescriptor desc return false; } - if (desc.Configurable || desc.Enumerable || desc.IsAccessorDescriptor() || !desc.Writable) + if (desc.Configurable) + { + return false; + } + + if (desc.EnumerableSet && !desc.Enumerable) + { + return false; + } + + if (desc.IsAccessorDescriptor()) + { + return false; + } + + if (desc.WritableSet && !desc.Writable) { return false; } @@ -79,6 +126,9 @@ public override bool DefineOwnProperty(JsValue property, PropertyDescriptor desc return true; } + /// + /// https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-hasproperty-p + /// public override bool HasProperty(JsValue property) { if (property.IsSymbol()) @@ -90,7 +140,9 @@ public override bool HasProperty(JsValue property) return _exports.Contains(p); } - // https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-get-p-receiver + /// + /// https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-get-p-receiver + /// public override JsValue Get(JsValue property, JsValue receiver) { if (property.IsSymbol()) @@ -123,11 +175,17 @@ public override JsValue Get(JsValue property, JsValue receiver) return targetEnv.GetBindingValue(binding.BindingName, true); } + /// + /// https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-set-p-v-receiver + /// public override bool Set(JsValue property, JsValue value, JsValue receiver) { return false; } + /// + /// https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-delete-p + /// public override bool Delete(JsValue property) { if (property.IsSymbol()) @@ -139,6 +197,9 @@ public override bool Delete(JsValue property) return !_exports.Contains(p); } + /// + /// https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-ownpropertykeys + /// public override List GetOwnPropertyKeys(Types types = Types.String | Types.Symbol) { var keys = base.GetOwnPropertyKeys(types); From dcc405df7ec9727a319d79a92ce772cee4a86cf3 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Sat, 5 Mar 2022 15:09:45 +0200 Subject: [PATCH 02/31] fix GetOwnPropertyKeys --- Jint/Native/Array/ArrayPrototype.cs | 2 +- Jint/Runtime/Modules/ModuleNamespace.cs | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Jint/Native/Array/ArrayPrototype.cs b/Jint/Native/Array/ArrayPrototype.cs index 105ac5108f..0b5d565b69 100644 --- a/Jint/Native/Array/ArrayPrototype.cs +++ b/Jint/Native/Array/ArrayPrototype.cs @@ -1458,7 +1458,7 @@ public JsValue Pop(JsValue thisObject, JsValue[] arguments) return element; } - private sealed class ArrayComparer : IComparer + internal sealed class ArrayComparer : IComparer { /// /// Default instance without any compare function. diff --git a/Jint/Runtime/Modules/ModuleNamespace.cs b/Jint/Runtime/Modules/ModuleNamespace.cs index a8eb37a4e7..a4f0890628 100644 --- a/Jint/Runtime/Modules/ModuleNamespace.cs +++ b/Jint/Runtime/Modules/ModuleNamespace.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using Jint.Collections; using Jint.Native; +using Jint.Native.Array; using Jint.Native.Object; using Jint.Native.Symbol; using Jint.Runtime.Descriptors; @@ -18,7 +19,6 @@ internal sealed class ModuleNamespace : ObjectInstance public ModuleNamespace(Engine engine, JsModule module, List exports) : base(engine) { _module = module; - exports.Sort(); _exports = new HashSet(exports); } @@ -202,15 +202,19 @@ public override bool Delete(JsValue property) /// public override List GetOwnPropertyKeys(Types types = Types.String | Types.Symbol) { - var keys = base.GetOwnPropertyKeys(types); + var result = new List(); if ((types & Types.String) != 0) { + result.Capacity = _exports.Count; foreach (var export in _exports) { - keys.Add(export); + result.Add(export); } + result.Sort(ArrayPrototype.ArrayComparer.Default); } + + result.AddRange(base.GetOwnPropertyKeys(types)); - return keys; + return result; } } \ No newline at end of file From fac21f50e757f1a9537f28cca47e2e2520a15ec7 Mon Sep 17 00:00:00 2001 From: Christian Rondeau Date: Sat, 5 Mar 2022 17:03:17 -0500 Subject: [PATCH 03/31] Identify module in parsing error messages --- Jint/Engine.Modules.cs | 25 ++++++++++++++++++++----- Jint/ModuleBuilder.cs | 12 ++++++++++-- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/Jint/Engine.Modules.cs b/Jint/Engine.Modules.cs index f332532576..7901a47384 100644 --- a/Jint/Engine.Modules.cs +++ b/Jint/Engine.Modules.cs @@ -41,7 +41,22 @@ internal JsModule LoadModule(string? referencingModuleLocation, string specifier if (_builders.TryGetValue(specifier, out var moduleBuilder)) { - var parsedModule = moduleBuilder.Parse(); + Module parsedModule; + try + { + parsedModule = moduleBuilder.Parse(); + } + catch (ParserException ex) + { + ExceptionHelper.ThrowSyntaxError(Realm, $"Error while loading module: error in module '{moduleResolution.Specifier}': {ex.Error}"); + return null!; + } + catch (Exception) + { + ExceptionHelper.ThrowJavaScriptException(this, $"Could not load module {moduleResolution.Specifier}", Completion.Empty()); + return null!; + } + module = new JsModule(this, Realm, parsedModule, null, false); // Early link is required because we need to bind values before returning module.Link(); @@ -59,16 +74,16 @@ internal JsModule LoadModule(string? referencingModuleLocation, string specifier return module; } - public void AddModule(string specifier, string source) + public void AddModule(string specifier, string code) { - var moduleBuilder = new ModuleBuilder(this); - moduleBuilder.AddSource(source); + var moduleBuilder = new ModuleBuilder(this, specifier); + moduleBuilder.AddSource(code); AddModule(specifier, moduleBuilder); } public void AddModule(string specifier, Action buildModule) { - var moduleBuilder = new ModuleBuilder(this); + var moduleBuilder = new ModuleBuilder(this,specifier); buildModule(moduleBuilder); AddModule(specifier, moduleBuilder); } diff --git a/Jint/ModuleBuilder.cs b/Jint/ModuleBuilder.cs index 2616e40035..602df9aa65 100644 --- a/Jint/ModuleBuilder.cs +++ b/Jint/ModuleBuilder.cs @@ -15,10 +15,12 @@ public sealed class ModuleBuilder private readonly Engine _engine; private readonly List _sourceRaw = new(); private readonly Dictionary _exports = new(); + private readonly ParserOptions _options; - internal ModuleBuilder(Engine engine) + internal ModuleBuilder(Engine engine, string source) { _engine = engine; + _options = new ParserOptions(source); } public ModuleBuilder AddSource(string code) @@ -69,11 +71,17 @@ public ModuleBuilder ExportFunction(string name, Func fn) return this; } + public ModuleBuilder WithOptions(Action configure) + { + configure(_options); + return this; + } + internal Module Parse() { if (_sourceRaw.Count > 0) { - return new JavaScriptParser(_sourceRaw.Count == 1 ? _sourceRaw[0] : string.Join(Environment.NewLine, _sourceRaw)).ParseModule(); + return new JavaScriptParser(_sourceRaw.Count == 1 ? _sourceRaw[0] : string.Join(Environment.NewLine, _sourceRaw), _options).ParseModule(); } else { From 2e3f6ae4d3329ad826eb4248fa5e0a1d1a44b66d Mon Sep 17 00:00:00 2001 From: Christian Rondeau Date: Sat, 5 Mar 2022 20:28:49 -0500 Subject: [PATCH 04/31] Remove Execute/Evaluate a module from script --- Jint.Tests.Test262/Test262Test.cs | 4 +++- Jint.Tests/Runtime/ModuleTests.cs | 17 ----------------- Jint/Engine.Modules.cs | 17 ----------------- Jint/Engine.cs | 20 ++++++++------------ 4 files changed, 11 insertions(+), 47 deletions(-) diff --git a/Jint.Tests.Test262/Test262Test.cs b/Jint.Tests.Test262/Test262Test.cs index 3b90df4501..dcd4c0bb39 100644 --- a/Jint.Tests.Test262/Test262Test.cs +++ b/Jint.Tests.Test262/Test262Test.cs @@ -167,7 +167,9 @@ protected void RunTestCode(string fileName, string code, bool strict) { if (module) { - engine.Execute(new JavaScriptParser(code, new ParserOptions(fileName)).ParseModule()); + var moduleName = Path.GetFileNameWithoutExtension(fileName); + engine.AddModule(moduleName, builder => builder.AddSource(code).WithOptions(opts => opts.ErrorHandler.Source = fileName)); + engine.ImportModule(moduleName); } else { diff --git a/Jint.Tests/Runtime/ModuleTests.cs b/Jint.Tests/Runtime/ModuleTests.cs index dc5583ef1e..bf5dc01452 100644 --- a/Jint.Tests/Runtime/ModuleTests.cs +++ b/Jint.Tests/Runtime/ModuleTests.cs @@ -237,23 +237,6 @@ public void CanImportFromFile() Assert.Equal("John Doe", result); } - [Fact] - public void CanReferenceModuleImportFromFileInInlineScript() - { - var engine = new Engine(options => options.EnableModules(GetBasePath())); - const string script = @" - import { formatName } from './modules/format-name.js' - formatName('John', 'Doe'); - "; - - var ex = Assert.Throws(() => engine.Evaluate(script)); - Assert.Equal("Cannot use import/export statements outside a module", ex.Message); - - var value = engine.Evaluate(script, SourceType.Module); - Assert.IsType(value); - } - - private static string GetBasePath() { var assemblyPath = new Uri(typeof(ModuleTests).GetTypeInfo().Assembly.Location).LocalPath; diff --git a/Jint/Engine.Modules.cs b/Jint/Engine.Modules.cs index 7901a47384..bb88611f71 100644 --- a/Jint/Engine.Modules.cs +++ b/Jint/Engine.Modules.cs @@ -102,23 +102,6 @@ public ObjectInstance ImportModule(string specifier) module = LoadModule(null, specifier); } - return Execute(specifier, module); - } - - public Engine Execute(Module module) - { - Evaluate(module); - return this; - } - - public JsValue Evaluate(Module module) - { - var jsModule = new JsModule(this, Realm, module, location: null, async: false); - return Execute(specifier: null, jsModule); - } - - private ObjectInstance Execute(string? specifier, JsModule module) - { if (module.Status == ModuleStatus.Unlinked) { module.Link(); diff --git a/Jint/Engine.cs b/Jint/Engine.cs index 08b0b4a874..fdb47e6fa2 100644 --- a/Jint/Engine.cs +++ b/Jint/Engine.cs @@ -239,24 +239,20 @@ public void ResetCallStack() CallStack.Clear(); } - public JsValue Evaluate(string source, SourceType sourceType = SourceType.Script) - => Evaluate(source, DefaultParserOptions, sourceType); + public JsValue Evaluate(string source) + => Evaluate(source, DefaultParserOptions); - public JsValue Evaluate(string source, ParserOptions parserOptions, SourceType sourceType = SourceType.Script) - => sourceType == SourceType.Script - ? Evaluate(new JavaScriptParser(source, parserOptions).ParseScript()) - : Evaluate(new JavaScriptParser(source, parserOptions).ParseModule()); + public JsValue Evaluate(string source, ParserOptions parserOptions) + => Evaluate(new JavaScriptParser(source, parserOptions).ParseScript()); public JsValue Evaluate(Script script) => Execute(script)._completionValue; - public Engine Execute(string source, SourceType sourceType = SourceType.Script) - => Execute(source, DefaultParserOptions, sourceType); + public Engine Execute(string source) + => Execute(source, DefaultParserOptions); - public Engine Execute(string source, ParserOptions parserOptions, SourceType sourceType = SourceType.Script) - => sourceType == SourceType.Script - ? Execute(new JavaScriptParser(source, parserOptions).ParseScript()) - : Execute(new JavaScriptParser(source, parserOptions).ParseModule()); + public Engine Execute(string source, ParserOptions parserOptions) + => Execute(new JavaScriptParser(source, parserOptions).ParseScript()); public Engine Execute(Script script) { From dc2f1c9adac54fb117d6b1b94bf700e7ee931557 Mon Sep 17 00:00:00 2001 From: Christian Rondeau Date: Sat, 5 Mar 2022 21:13:38 -0500 Subject: [PATCH 05/31] Enrich error messages to identify source module --- Jint.Tests/Runtime/ModuleTests.cs | 40 +++++++++++++++++-- Jint/Engine.Modules.cs | 64 +++++++++++++++++++------------ Jint/ModuleBuilder.cs | 25 +++++++++--- 3 files changed, 96 insertions(+), 33 deletions(-) diff --git a/Jint.Tests/Runtime/ModuleTests.cs b/Jint.Tests/Runtime/ModuleTests.cs index bf5dc01452..e1dcc7ba20 100644 --- a/Jint.Tests/Runtime/ModuleTests.cs +++ b/Jint.Tests/Runtime/ModuleTests.cs @@ -99,7 +99,28 @@ public void ShouldImportAll() } [Fact] - public void ShouldPropagateThrowStatementOnCSharpImport() + public void ShouldPropagateParseError() + { + _engine.AddModule("imported", @"export const invalid;"); + _engine.AddModule("my-module", @"import { invalid } from 'imported';"); + + var exc = Assert.Throws(() => _engine.ImportModule("my-module")); + Assert.Equal("Error while loading module: error in module 'imported': Line 1: Missing initializer in const declaration", exc.Message); + } + + [Fact] + public void ShouldPropagateLinkError() + { + _engine.AddModule("imported", @"import { x } from 'my-module'; export const value = x;"); + _engine.AddModule("my-module", @"import { value } from 'imported'; export const x = 0;"); + + var exc = Assert.Throws(() => _engine.ImportModule("my-module")); + Assert.Equal("Ambiguous import statement for identifier value", exc.Message); + Assert.Equal("my-module", exc.Location.Source); + } + + [Fact] + public void ShouldPropagateExecuteError() { _engine.AddModule("my-module", @"throw new Error('imported successfully');"); @@ -204,7 +225,7 @@ public void ShouldAllowChaining() } [Fact(Skip = "TODO re-enable in module fix branch")] - public void ShouldAllowLoadingMoreThanOnce() + public void ShouldImportOnlyOnce() { var called = 0; _engine.AddModule("imported-module", builder => builder.ExportFunction("count", args => called++)); @@ -212,7 +233,20 @@ public void ShouldAllowLoadingMoreThanOnce() _engine.ImportModule("my-module"); _engine.ImportModule("my-module"); - Assert.Equal(called, 1); + Assert.Equal(1, called); + } + + [Fact] + public void ShouldAllowSelfImport() + { + _engine.AddModule("my-globals", @"export const globals = { counter: 0 };"); + _engine.AddModule("my-module", @" +import { globals } from 'my-globals'; +export const count = ++globals.counter; +"); + var ns= _engine.ImportModule("my-module"); + + Assert.Equal(1, ns.Get("count").AsInteger()); } #if(NET6_0_OR_GREATER) diff --git a/Jint/Engine.Modules.cs b/Jint/Engine.Modules.cs index bb88611f71..3d7aa88414 100644 --- a/Jint/Engine.Modules.cs +++ b/Jint/Engine.Modules.cs @@ -41,36 +41,43 @@ internal JsModule LoadModule(string? referencingModuleLocation, string specifier if (_builders.TryGetValue(specifier, out var moduleBuilder)) { - Module parsedModule; - try - { - parsedModule = moduleBuilder.Parse(); - } - catch (ParserException ex) - { - ExceptionHelper.ThrowSyntaxError(Realm, $"Error while loading module: error in module '{moduleResolution.Specifier}': {ex.Error}"); - return null!; - } - catch (Exception) - { - ExceptionHelper.ThrowJavaScriptException(this, $"Could not load module {moduleResolution.Specifier}", Completion.Empty()); - return null!; - } - - module = new JsModule(this, Realm, parsedModule, null, false); - // Early link is required because we need to bind values before returning - module.Link(); - moduleBuilder.BindExportedValues(module); - _builders.Remove(specifier); + module = LoadFromBuilder(specifier, moduleBuilder, moduleResolution); } else { - var parsedModule = ModuleLoader.LoadModule(this, moduleResolution); - module = new JsModule(this, Realm, parsedModule, moduleResolution.Uri?.LocalPath, false); + module = LoaderFromModuleLoader(moduleResolution); } + return module; + } + + private JsModule LoadFromBuilder(string specifier, ModuleBuilder moduleBuilder, ResolvedSpecifier moduleResolution) + { + var parsedModule = moduleBuilder.Parse(); + var module = new JsModule(this, Realm, parsedModule, null, false); _modules[moduleResolution.Key] = module; + // Early link is required because we need to bind values before returning + try + { + module.Link(); + } + catch (JavaScriptException ex) + { + if (ex.Location.Source == null) + ex.SetLocation(new Location(new Position(), new Position(), specifier)); + throw; + } + moduleBuilder.BindExportedValues(module); + _builders.Remove(specifier); + return module; + } + + private JsModule LoaderFromModuleLoader(ResolvedSpecifier moduleResolution) + { + var parsedModule = ModuleLoader.LoadModule(this, moduleResolution); + var module = new JsModule(this, Realm, parsedModule, moduleResolution.Uri?.LocalPath, false); + _modules[moduleResolution.Key] = module; return module; } @@ -104,7 +111,16 @@ public ObjectInstance ImportModule(string specifier) if (module.Status == ModuleStatus.Unlinked) { - module.Link(); + try + { + module.Link(); + } + catch (JavaScriptException ex) + { + if (ex.Location.Source == null) + ex.SetLocation(new Location(new Position(), new Position(), specifier)); + throw; + } } if (module.Status == ModuleStatus.Linked) diff --git a/Jint/ModuleBuilder.cs b/Jint/ModuleBuilder.cs index 602df9aa65..06a853561c 100644 --- a/Jint/ModuleBuilder.cs +++ b/Jint/ModuleBuilder.cs @@ -5,6 +5,7 @@ using Esprima; using Esprima.Ast; using Jint.Native; +using Jint.Runtime; using Jint.Runtime.Interop; using Jint.Runtime.Modules; @@ -13,14 +14,16 @@ namespace Jint; public sealed class ModuleBuilder { private readonly Engine _engine; + private readonly string _specifier; private readonly List _sourceRaw = new(); private readonly Dictionary _exports = new(); private readonly ParserOptions _options; - internal ModuleBuilder(Engine engine, string source) + internal ModuleBuilder(Engine engine, string specifier) { _engine = engine; - _options = new ParserOptions(source); + _specifier = specifier; + _options = new ParserOptions(specifier); } public ModuleBuilder AddSource(string code) @@ -79,13 +82,23 @@ public ModuleBuilder WithOptions(Action configure) internal Module Parse() { - if (_sourceRaw.Count > 0) + if (_sourceRaw.Count <= 0) return new Module(NodeList.Create(Array.Empty())); + + var javaScriptParser = new JavaScriptParser(_sourceRaw.Count == 1 ? _sourceRaw[0] : string.Join(Environment.NewLine, _sourceRaw), _options); + + try + { + return javaScriptParser.ParseModule(); + } + catch (ParserException ex) { - return new JavaScriptParser(_sourceRaw.Count == 1 ? _sourceRaw[0] : string.Join(Environment.NewLine, _sourceRaw), _options).ParseModule(); + ExceptionHelper.ThrowSyntaxError(_engine.Realm, $"Error while loading module: error in module '{_specifier}': {ex.Error}"); + return null!; } - else + catch (Exception) { - return new Module(NodeList.Create(Array.Empty())); + ExceptionHelper.ThrowJavaScriptException(_engine, $"Could not load module {_specifier}", Completion.Empty()); + return null!; } } From f3aab6249475a29219bc9820a755bda90a60fad8 Mon Sep 17 00:00:00 2001 From: Christian Rondeau Date: Sat, 5 Mar 2022 22:33:29 -0500 Subject: [PATCH 06/31] Correctly inner link, add failing test for cyclic --- Jint.Tests/Runtime/ModuleTests.cs | 19 ++++++++++++++++--- Jint/Engine.Modules.cs | 14 -------------- Jint/Options.cs | 3 +-- Jint/Runtime/Modules/JsModule.cs | 22 +++++++++++++++++----- 4 files changed, 34 insertions(+), 24 deletions(-) diff --git a/Jint.Tests/Runtime/ModuleTests.cs b/Jint.Tests/Runtime/ModuleTests.cs index e1dcc7ba20..3471a81e0f 100644 --- a/Jint.Tests/Runtime/ModuleTests.cs +++ b/Jint.Tests/Runtime/ModuleTests.cs @@ -111,11 +111,11 @@ public void ShouldPropagateParseError() [Fact] public void ShouldPropagateLinkError() { - _engine.AddModule("imported", @"import { x } from 'my-module'; export const value = x;"); - _engine.AddModule("my-module", @"import { value } from 'imported'; export const x = 0;"); + _engine.AddModule("imported", @"export invalid;"); + _engine.AddModule("my-module", @"import { value } from 'imported';"); var exc = Assert.Throws(() => _engine.ImportModule("my-module")); - Assert.Equal("Ambiguous import statement for identifier value", exc.Message); + Assert.Equal("Error while loading module: error in module 'imported': Line 1: Unexpected identifier", exc.Message); Assert.Equal("my-module", exc.Location.Source); } @@ -249,6 +249,19 @@ public void ShouldAllowSelfImport() Assert.Equal(1, ns.Get("count").AsInteger()); } + [Fact] + public void ShouldAllowCyclicImport() + { + _engine.AddModule("module2", @"import { x1 } from 'module1'; export const x2 = 2;"); + _engine.AddModule("module1", @"import { x2 } from 'module2'; export const x1 = 1;"); + + var ns1 = _engine.ImportModule("module1"); + var ns2 = _engine.ImportModule("module2"); + + Assert.Equal(1, ns1.Get("x1").AsInteger()); + Assert.Equal(2, ns2.Get("x2").AsInteger()); + } + #if(NET6_0_OR_GREATER) [Fact(Skip = "TODO re-enable in module fix branch")] diff --git a/Jint/Engine.Modules.cs b/Jint/Engine.Modules.cs index 3d7aa88414..839f074385 100644 --- a/Jint/Engine.Modules.cs +++ b/Jint/Engine.Modules.cs @@ -28,8 +28,6 @@ public partial class Engine return _executionContexts?.GetActiveScriptOrModule(); } - internal JsModule LoadModule(string specifier) => LoadModule(null, specifier); - internal JsModule LoadModule(string? referencingModuleLocation, string specifier) { var moduleResolution = ModuleLoader.Resolve(referencingModuleLocation, specifier); @@ -56,18 +54,6 @@ private JsModule LoadFromBuilder(string specifier, ModuleBuilder moduleBuilder, var parsedModule = moduleBuilder.Parse(); var module = new JsModule(this, Realm, parsedModule, null, false); _modules[moduleResolution.Key] = module; - // Early link is required because we need to bind values before returning - try - { - module.Link(); - } - catch (JavaScriptException ex) - { - if (ex.Location.Source == null) - ex.SetLocation(new Location(new Position(), new Position(), specifier)); - throw; - } - moduleBuilder.BindExportedValues(module); _builders.Remove(specifier); return module; diff --git a/Jint/Options.cs b/Jint/Options.cs index 8e422f16c9..356c7ed204 100644 --- a/Jint/Options.cs +++ b/Jint/Options.cs @@ -117,8 +117,7 @@ internal void Apply(Engine engine) (thisObj, arguments) => { var specifier = TypeConverter.ToString(arguments.At(0)); - var module = engine.LoadModule(specifier); - return JsModule.GetModuleNamespace(module); + return engine.ImportModule(specifier); }), PropertyFlag.AllForbidden)); } diff --git a/Jint/Runtime/Modules/JsModule.cs b/Jint/Runtime/Modules/JsModule.cs index d2d46b5321..95780b2142 100644 --- a/Jint/Runtime/Modules/JsModule.cs +++ b/Jint/Runtime/Modules/JsModule.cs @@ -68,6 +68,7 @@ public sealed class JsModule : JsValue, IScriptOrModule private readonly List _localExportEntries; private readonly List _indirectExportEntries; private readonly List _starExportEntries; + private readonly List> _exportBuilderDeclarations = new(); internal JsValue _evalResult; internal JsModule(Engine engine, Realm realm, Module source, string location, bool async) : base(InternalTypes.Module) @@ -132,8 +133,8 @@ private static ObjectInstance CreateModuleNamespace(JsModule module, List(name, value)); } /// @@ -275,7 +276,7 @@ public void Link() try { - Link(this, stack, 0); + InnerModuleLinking(this, stack, 0); } catch { @@ -377,7 +378,7 @@ public JsValue Evaluate() /// /// https://tc39.es/ecma262/#sec-InnerModuleLinking /// - private int Link(JsModule module, Stack stack, int index) + private int InnerModuleLinking(JsModule module, Stack stack, int index) { if (module.Status is ModuleStatus.Linking or @@ -408,7 +409,7 @@ ModuleStatus.EvaluatingAsync or //TODO: Should we link only when a module is requested? https://tc39.es/ecma262/#sec-example-cyclic-module-record-graphs Should we support retry? if (requiredModule.Status == ModuleStatus.Unlinked) { - requiredModule.Link(); + index = requiredModule.InnerModuleLinking(requiredModule, stack, index); } if (requiredModule.Status != ModuleStatus.Linking && @@ -751,6 +752,17 @@ private void InitializeEnvironment() } } + if (_exportBuilderDeclarations != null) + { + for (var i = 0; i < _exportBuilderDeclarations.Count; i++) + { + var d = _exportBuilderDeclarations[i]; + _environment.CreateImmutableBindingAndInitialize(d.Key, true, d.Value); + _localExportEntries.Add(new ExportEntry(d.Key, null, null, null)); + } + _exportBuilderDeclarations.Clear(); + } + _engine.LeaveExecutionContext(); } From e5d2968e527be068878313c1f048e256c14e9761 Mon Sep 17 00:00:00 2001 From: Christian Rondeau Date: Sun, 6 Mar 2022 00:18:44 -0500 Subject: [PATCH 07/31] Fix Test262Test not providing root module path --- Jint.Tests.Test262/SingleTest.cs | 4 ++-- Jint.Tests.Test262/Test262Test.cs | 27 ++++++++++++++------------- Jint.Tests/Runtime/ModuleTests.cs | 4 +++- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/Jint.Tests.Test262/SingleTest.cs b/Jint.Tests.Test262/SingleTest.cs index bf1f38d0f9..f18faecc36 100644 --- a/Jint.Tests.Test262/SingleTest.cs +++ b/Jint.Tests.Test262/SingleTest.cs @@ -34,12 +34,12 @@ public void TestSingle() if (code.IndexOf("onlyStrict", StringComparison.Ordinal) < 0) { - RunTestCode(sourceFile.Source, code, strict: false); + RunTestCode(sourceFile.Source, code, strict: false, null); } if (code.IndexOf("noStrict", StringComparison.Ordinal) < 0) { - RunTestCode(sourceFile.Source, code, strict: true); + RunTestCode(sourceFile.Source, code, strict: true, null); } } } diff --git a/Jint.Tests.Test262/Test262Test.cs b/Jint.Tests.Test262/Test262Test.cs index dcd4c0bb39..1f54725838 100644 --- a/Jint.Tests.Test262/Test262Test.cs +++ b/Jint.Tests.Test262/Test262Test.cs @@ -96,13 +96,17 @@ static Test262Test() } } - protected void RunTestCode(string fileName, string code, bool strict) + protected void RunTestCode(string fileName, string code, bool strict, string fullPath) { - var engine = new Engine(cfg => cfg - .LocalTimeZone(_pacificTimeZone) - .Strict(strict) - .EnableModules(Path.Combine(BasePath, "test", Path.GetDirectoryName(fileName)!)) - ); + var module = Regex.IsMatch(code, @"flags:\s*?\[.*?module.*?]"); + + var engine = new Engine(cfg => + { + cfg.LocalTimeZone(_pacificTimeZone); + cfg.Strict(strict); + if (module) + cfg.EnableModules(Path.Combine(BasePath, "test", Path.GetDirectoryName(fullPath)!)); + }); engine.Execute(Sources["sta.js"]); engine.Execute(Sources["assert.js"]); @@ -152,8 +156,6 @@ protected void RunTestCode(string fileName, string code, bool strict) engine.Execute(Sources[file.Trim()]); } } - - var module = Regex.IsMatch(code, @"flags:\s*?\[.*?module.*?]"); if (code.IndexOf("propertyHelper.js", StringComparison.OrdinalIgnoreCase) != -1) { @@ -167,9 +169,8 @@ protected void RunTestCode(string fileName, string code, bool strict) { if (module) { - var moduleName = Path.GetFileNameWithoutExtension(fileName); - engine.AddModule(moduleName, builder => builder.AddSource(code).WithOptions(opts => opts.ErrorHandler.Source = fileName)); - engine.ImportModule(moduleName); + engine.AddModule(fullPath, builder => builder.AddSource(code)); + engine.ImportModule(fullPath); } else { @@ -200,13 +201,13 @@ protected void RunTestInternal(SourceFile sourceFile) if (sourceFile.Code.IndexOf("onlyStrict", StringComparison.Ordinal) < 0) { - RunTestCode(sourceFile.Source, sourceFile.Code, strict: false); + RunTestCode(sourceFile.Source, sourceFile.Code, strict: false, fullPath: sourceFile.FullPath); } if (!_strictSkips.Contains(sourceFile.Source) && sourceFile.Code.IndexOf("noStrict", StringComparison.Ordinal) < 0) { - RunTestCode(sourceFile.Source, sourceFile.Code, strict: true); + RunTestCode(sourceFile.Source, sourceFile.Code, strict: true, fullPath: sourceFile.FullPath); } } diff --git a/Jint.Tests/Runtime/ModuleTests.cs b/Jint.Tests/Runtime/ModuleTests.cs index 3471a81e0f..6d27d8d49e 100644 --- a/Jint.Tests/Runtime/ModuleTests.cs +++ b/Jint.Tests/Runtime/ModuleTests.cs @@ -242,7 +242,9 @@ public void ShouldAllowSelfImport() _engine.AddModule("my-globals", @"export const globals = { counter: 0 };"); _engine.AddModule("my-module", @" import { globals } from 'my-globals'; -export const count = ++globals.counter; +import {} from 'my-module'; +globals.counter++; +export const count = globals.counter; "); var ns= _engine.ImportModule("my-module"); From 941286f5c925dc85da7ba7c780264796207263e5 Mon Sep 17 00:00:00 2001 From: Christian Rondeau Date: Sun, 6 Mar 2022 15:52:14 -0500 Subject: [PATCH 08/31] Fix indirect renamed exports not added to module requests --- Jint/EsprimaExtensions.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Jint/EsprimaExtensions.cs b/Jint/EsprimaExtensions.cs index f95db78b7d..6c0d9d4e54 100644 --- a/Jint/EsprimaExtensions.cs +++ b/Jint/EsprimaExtensions.cs @@ -325,10 +325,6 @@ internal static void GetExportEntries(this ExportDeclaration export, List Date: Sun, 6 Mar 2022 15:52:41 -0500 Subject: [PATCH 09/31] Fix indirect renamed exports resolution --- Jint/Runtime/Modules/JsModule.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jint/Runtime/Modules/JsModule.cs b/Jint/Runtime/Modules/JsModule.cs index 95780b2142..6124bdb1e9 100644 --- a/Jint/Runtime/Modules/JsModule.cs +++ b/Jint/Runtime/Modules/JsModule.cs @@ -638,7 +638,7 @@ private void InitializeEnvironment() for (var i = 0; i < _indirectExportEntries.Count; i++) { var e = _indirectExportEntries[i]; - var resolution = ResolveExport(e.ExportName); + var resolution = ResolveExport(e.ImportName ?? e.ExportName); if (resolution is null || resolution == ResolvedBinding.Ambiguous) { ExceptionHelper.ThrowSyntaxError(_realm, "Ambiguous import statement for identifier: " + e.ExportName); From a655f0c85353b68c9b07bad5889be578baf6a31b Mon Sep 17 00:00:00 2001 From: Christian Rondeau Date: Sun, 6 Mar 2022 23:12:28 -0500 Subject: [PATCH 10/31] Prevent writing to namespace object --- Jint/EsprimaExtensions.cs | 1 - .../Expressions/JintAssignmentExpression.cs | 14 ++++++++++++++ .../Expressions/JintUnaryExpression.cs | 18 ++++++++++++++---- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/Jint/EsprimaExtensions.cs b/Jint/EsprimaExtensions.cs index 6c0d9d4e54..d199abc90e 100644 --- a/Jint/EsprimaExtensions.cs +++ b/Jint/EsprimaExtensions.cs @@ -324,7 +324,6 @@ internal static void GetExportEntries(this ExportDeclaration export, List(); - var property = r.GetReferencedName(); + var property = referencedName; engine._referencePool.Return(r); return bindings.DeleteBinding(property.ToString()) ? JsBoolean.True : JsBoolean.False; From e8863ec7d828c59f11a0ef5a80db38e959f16f0d Mon Sep 17 00:00:00 2001 From: Christian Rondeau Date: Sun, 6 Mar 2022 23:46:33 -0500 Subject: [PATCH 11/31] Always run module tests in strict mode --- Jint.Tests.Test262/Test262Test.cs | 2 +- .../Runtime/Interpreter/Expressions/JintAssignmentExpression.cs | 2 +- Jint/Runtime/Interpreter/Expressions/JintUnaryExpression.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Jint.Tests.Test262/Test262Test.cs b/Jint.Tests.Test262/Test262Test.cs index 1f54725838..7a5283bd46 100644 --- a/Jint.Tests.Test262/Test262Test.cs +++ b/Jint.Tests.Test262/Test262Test.cs @@ -199,7 +199,7 @@ protected void RunTestInternal(SourceFile sourceFile) return; } - if (sourceFile.Code.IndexOf("onlyStrict", StringComparison.Ordinal) < 0) + if (sourceFile.Code.IndexOf("onlyStrict", StringComparison.Ordinal) < 0 && sourceFile.Code.IndexOf("module", StringComparison.Ordinal) < 0) { RunTestCode(sourceFile.Source, sourceFile.Code, strict: false, fullPath: sourceFile.FullPath); } diff --git a/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs index baec86d36c..305325f465 100644 --- a/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs +++ b/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs @@ -377,7 +377,7 @@ private ExpressionResult SetValue(EvaluationContext context) var rval = _right.GetValue(context).GetValueOrDefault(); - if (/* IS STRICT MODE && */ lref.IsPropertyReference()) + if (StrictModeScope.IsStrictModeCode && lref.IsPropertyReference()) { var lrefReferenceName = lref.GetReferencedName(); var lrefBase = lref.GetBase(); diff --git a/Jint/Runtime/Interpreter/Expressions/JintUnaryExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintUnaryExpression.cs index 087079e497..3abf3ec06a 100644 --- a/Jint/Runtime/Interpreter/Expressions/JintUnaryExpression.cs +++ b/Jint/Runtime/Interpreter/Expressions/JintUnaryExpression.cs @@ -145,7 +145,7 @@ private JsValue EvaluateJsValue(EvaluationContext context) ExceptionHelper.ThrowTypeError(engine.Realm, $"Cannot delete property '{referencedName}' of {o}"); } - if (/* IS STRICT MODE && */ !r.GetBase().AsObject().GetProperty(referencedName).Configurable) + if (StrictModeScope.IsStrictModeCode && !r.GetBase().AsObject().GetProperty(referencedName).Configurable) { ExceptionHelper.ThrowTypeError(engine.Realm, $"Cannot delete property '{referencedName}' of {o}"); } From 273439c01d9f375501239deccfe28cd70cfbc204 Mon Sep 17 00:00:00 2001 From: Christian Rondeau Date: Mon, 7 Mar 2022 00:11:49 -0500 Subject: [PATCH 12/31] Fix indirect export resolve name --- Jint/Runtime/Modules/JsModule.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jint/Runtime/Modules/JsModule.cs b/Jint/Runtime/Modules/JsModule.cs index 6124bdb1e9..a052c4c915 100644 --- a/Jint/Runtime/Modules/JsModule.cs +++ b/Jint/Runtime/Modules/JsModule.cs @@ -221,7 +221,7 @@ internal ResolvedBinding ResolveExport(string exportName, List Date: Mon, 7 Mar 2022 00:25:14 -0500 Subject: [PATCH 13/31] Rename JsModule to ModuleRecord to match ecmascript specs --- Jint/Engine.Modules.cs | 14 +++---- Jint/ModuleBuilder.cs | 2 +- .../Environments/ModuleEnvironmentRecord.cs | 4 +- Jint/Runtime/Host.cs | 8 ++-- Jint/Runtime/IScriptOrModule.Extensions.cs | 4 +- Jint/Runtime/Modules/ModuleNamespace.cs | 6 +-- .../Modules/{JsModule.cs => ModuleRecord.cs} | 38 +++++++++---------- 7 files changed, 38 insertions(+), 38 deletions(-) rename Jint/Runtime/Modules/{JsModule.cs => ModuleRecord.cs} (96%) diff --git a/Jint/Engine.Modules.cs b/Jint/Engine.Modules.cs index 839f074385..9cad64381b 100644 --- a/Jint/Engine.Modules.cs +++ b/Jint/Engine.Modules.cs @@ -17,7 +17,7 @@ public partial class Engine { internal IModuleLoader ModuleLoader { get; set; } - private readonly Dictionary _modules = new(); + private readonly Dictionary _modules = new(); private readonly Dictionary _builders = new(); /// @@ -28,7 +28,7 @@ public partial class Engine return _executionContexts?.GetActiveScriptOrModule(); } - internal JsModule LoadModule(string? referencingModuleLocation, string specifier) + internal ModuleRecord LoadModule(string? referencingModuleLocation, string specifier) { var moduleResolution = ModuleLoader.Resolve(referencingModuleLocation, specifier); @@ -49,20 +49,20 @@ internal JsModule LoadModule(string? referencingModuleLocation, string specifier return module; } - private JsModule LoadFromBuilder(string specifier, ModuleBuilder moduleBuilder, ResolvedSpecifier moduleResolution) + private ModuleRecord LoadFromBuilder(string specifier, ModuleBuilder moduleBuilder, ResolvedSpecifier moduleResolution) { var parsedModule = moduleBuilder.Parse(); - var module = new JsModule(this, Realm, parsedModule, null, false); + var module = new ModuleRecord(this, Realm, parsedModule, null, false); _modules[moduleResolution.Key] = module; moduleBuilder.BindExportedValues(module); _builders.Remove(specifier); return module; } - private JsModule LoaderFromModuleLoader(ResolvedSpecifier moduleResolution) + private ModuleRecord LoaderFromModuleLoader(ResolvedSpecifier moduleResolution) { var parsedModule = ModuleLoader.LoadModule(this, moduleResolution); - var module = new JsModule(this, Realm, parsedModule, moduleResolution.Uri?.LocalPath, false); + var module = new ModuleRecord(this, Realm, parsedModule, moduleResolution.Uri?.LocalPath, false); _modules[moduleResolution.Key] = module; return module; } @@ -145,7 +145,7 @@ public ObjectInstance ImportModule(string specifier) // TODO what about callstack and thrown exceptions? RunAvailableContinuations(); - return JsModule.GetModuleNamespace(module); + return ModuleRecord.GetModuleNamespace(module); } ExceptionHelper.ThrowNotSupportedException($"Error while evaluating module: Module is in an invalid state: '{module.Status}'"); diff --git a/Jint/ModuleBuilder.cs b/Jint/ModuleBuilder.cs index 06a853561c..601e970167 100644 --- a/Jint/ModuleBuilder.cs +++ b/Jint/ModuleBuilder.cs @@ -102,7 +102,7 @@ internal Module Parse() } } - internal void BindExportedValues(JsModule module) + internal void BindExportedValues(ModuleRecord module) { foreach (var export in _exports) { diff --git a/Jint/Runtime/Environments/ModuleEnvironmentRecord.cs b/Jint/Runtime/Environments/ModuleEnvironmentRecord.cs index c4357f1a74..9ae6f78441 100644 --- a/Jint/Runtime/Environments/ModuleEnvironmentRecord.cs +++ b/Jint/Runtime/Environments/ModuleEnvironmentRecord.cs @@ -27,7 +27,7 @@ public override JsValue GetThisBinding() /// /// https://tc39.es/ecma262/#sec-createimportbinding /// - public void CreateImportBinding(string importName, JsModule module, string name) + public void CreateImportBinding(string importName, ModuleRecord module, string name) { _hasBindings = true; _importBindings[importName] = new IndirectBinding(module, name); @@ -63,5 +63,5 @@ internal override bool TryGetBinding(in BindingName name, bool strict, out Bindi /// public override bool HasThisBinding() => true; - private readonly record struct IndirectBinding(JsModule Module, string BindingName); + private readonly record struct IndirectBinding(ModuleRecord Module, string BindingName); } \ No newline at end of file diff --git a/Jint/Runtime/Host.cs b/Jint/Runtime/Host.cs index 194c145ce3..f8c51d6bc5 100644 --- a/Jint/Runtime/Host.cs +++ b/Jint/Runtime/Host.cs @@ -115,7 +115,7 @@ public virtual void EnsureCanCompileStrings(Realm callerRealm, Realm evalRealm) /// /// https://tc39.es/ecma262/#sec-hostresolveimportedmodule /// - protected internal virtual JsModule ResolveImportedModule(JsModule? referencingModule, string specifier) + protected internal virtual ModuleRecord ResolveImportedModule(ModuleRecord? referencingModule, string specifier) { return Engine.LoadModule(referencingModule?.Location, specifier); } @@ -123,7 +123,7 @@ protected internal virtual JsModule ResolveImportedModule(JsModule? referencingM /// /// https://tc39.es/ecma262/#sec-hostimportmoduledynamically /// - internal virtual void ImportModuleDynamically(JsModule? referencingModule, string specifier, PromiseCapability promiseCapability) + internal virtual void ImportModuleDynamically(ModuleRecord? referencingModule, string specifier, PromiseCapability promiseCapability) { var promise = Engine.RegisterPromise(); @@ -143,14 +143,14 @@ internal virtual void ImportModuleDynamically(JsModule? referencingModule, strin /// /// https://tc39.es/ecma262/#sec-finishdynamicimport /// - internal virtual void FinishDynamicImport(JsModule? referencingModule, string specifier, PromiseCapability promiseCapability, PromiseInstance innerPromise) + internal virtual void FinishDynamicImport(ModuleRecord? referencingModule, string specifier, PromiseCapability promiseCapability, PromiseInstance innerPromise) { var onFulfilled = new ClrFunctionInstance(Engine, "", (thisObj, args) => { var moduleRecord = ResolveImportedModule(referencingModule, specifier); try { - var ns = JsModule.GetModuleNamespace(moduleRecord); + var ns = ModuleRecord.GetModuleNamespace(moduleRecord); promiseCapability.Resolve.Call(JsValue.Undefined, new[] { ns }); } catch (JavaScriptException ex) diff --git a/Jint/Runtime/IScriptOrModule.Extensions.cs b/Jint/Runtime/IScriptOrModule.Extensions.cs index 2e65065bcd..3da6a09090 100644 --- a/Jint/Runtime/IScriptOrModule.Extensions.cs +++ b/Jint/Runtime/IScriptOrModule.Extensions.cs @@ -7,9 +7,9 @@ namespace Jint.Runtime; internal static class ScriptOrModuleExtensions { - public static JsModule AsModule(this IScriptOrModule? scriptOrModule, Engine engine, Location location) + public static ModuleRecord AsModule(this IScriptOrModule? scriptOrModule, Engine engine, Location location) { - var module = scriptOrModule as JsModule; + var module = scriptOrModule as ModuleRecord; if (module == null) { ExceptionHelper.ThrowSyntaxError(engine.Realm, "Cannot use import/export statements outside a module", location); diff --git a/Jint/Runtime/Modules/ModuleNamespace.cs b/Jint/Runtime/Modules/ModuleNamespace.cs index a4f0890628..e46c57ae4a 100644 --- a/Jint/Runtime/Modules/ModuleNamespace.cs +++ b/Jint/Runtime/Modules/ModuleNamespace.cs @@ -13,10 +13,10 @@ namespace Jint.Runtime.Modules; /// internal sealed class ModuleNamespace : ObjectInstance { - private readonly JsModule _module; + private readonly ModuleRecord _module; private readonly HashSet _exports; - public ModuleNamespace(Engine engine, JsModule module, List exports) : base(engine) + public ModuleNamespace(Engine engine, ModuleRecord module, List exports) : base(engine) { _module = module; _exports = new HashSet(exports); @@ -163,7 +163,7 @@ public override JsValue Get(JsValue property, JsValue receiver) if (binding.BindingName == "*namespace*") { - return JsModule.GetModuleNamespace(targetModule); + return ModuleRecord.GetModuleNamespace(targetModule); } var targetEnv = targetModule._environment; diff --git a/Jint/Runtime/Modules/JsModule.cs b/Jint/Runtime/Modules/ModuleRecord.cs similarity index 96% rename from Jint/Runtime/Modules/JsModule.cs rename to Jint/Runtime/Modules/ModuleRecord.cs index a052c4c915..5891d382f4 100644 --- a/Jint/Runtime/Modules/JsModule.cs +++ b/Jint/Runtime/Modules/ModuleRecord.cs @@ -14,7 +14,7 @@ namespace Jint.Runtime.Modules; #pragma warning disable CS0649 // never assigned to, waiting for new functionalities in spec -internal sealed record ResolvedBinding(JsModule Module, string BindingName) +internal sealed record ResolvedBinding(ModuleRecord Module, string BindingName) { internal static ResolvedBinding Ambiguous => new(null, "ambiguous"); } @@ -33,7 +33,7 @@ string LocalName ); internal sealed record ExportResolveSetItem( - JsModule Module, + ModuleRecord Module, string ExportName ); @@ -43,7 +43,7 @@ string ExportName /// https://tc39.es/ecma262/#sec-cyclic-module-records /// https://tc39.es/ecma262/#sec-source-text-module-records /// -public sealed class JsModule : JsValue, IScriptOrModule +public sealed class ModuleRecord : JsValue, IScriptOrModule { private readonly Engine _engine; private readonly Realm _realm; @@ -53,11 +53,11 @@ public sealed class JsModule : JsValue, IScriptOrModule private int _dfsIndex; private int _dfsAncestorIndex; private readonly HashSet _requestedModules; - private JsModule _cycleRoot; + private ModuleRecord _cycleRoot; private bool _hasTLA; private bool _asyncEvaluation; private PromiseCapability _topLevelCapability; - private List _asyncParentModules; + private List _asyncParentModules; private int _asyncEvalOrder; private int _pendingAsyncDependencies; @@ -71,7 +71,7 @@ public sealed class JsModule : JsValue, IScriptOrModule private readonly List> _exportBuilderDeclarations = new(); internal JsValue _evalResult; - internal JsModule(Engine engine, Realm realm, Module source, string location, bool async) : base(InternalTypes.Module) + internal ModuleRecord(Engine engine, Realm realm, Module source, string location, bool async) : base(InternalTypes.Module) { _engine = engine; _realm = realm; @@ -98,7 +98,7 @@ internal JsModule(Engine engine, Realm realm, Module source, string location, bo /// /// https://tc39.es/ecma262/#sec-getmodulenamespace /// - public static ObjectInstance GetModuleNamespace(JsModule module) + public static ObjectInstance GetModuleNamespace(ModuleRecord module) { var ns = module._namespace; if (ns is null) @@ -124,7 +124,7 @@ public static ObjectInstance GetModuleNamespace(JsModule module) /// /// https://tc39.es/ecma262/#sec-modulenamespacecreate /// - private static ObjectInstance CreateModuleNamespace(JsModule module, List unambiguousNames) + private static ObjectInstance CreateModuleNamespace(ModuleRecord module, List unambiguousNames) { var m = new ModuleNamespace(module._engine, module, unambiguousNames); module._namespace = m; @@ -140,9 +140,9 @@ internal void BindExportedValue(string name, JsValue value) /// /// https://tc39.es/ecma262/#sec-getexportednames /// - public List GetExportedNames(List exportStarSet = null) + public List GetExportedNames(List exportStarSet = null) { - exportStarSet ??= new List(); + exportStarSet ??= new List(); if (exportStarSet.Contains(this)) { //Reached the starting point of an export * circularity @@ -272,7 +272,7 @@ public void Link() ExceptionHelper.ThrowInvalidOperationException("Error while linking module: Module is already either linking or evaluating"); } - var stack = new Stack(); + var stack = new Stack(); try { @@ -327,7 +327,7 @@ public JsValue Evaluate() return module._topLevelCapability.PromiseInstance; } - var stack = new Stack(); + var stack = new Stack(); var capability = PromiseConstructor.NewPromiseCapability(_engine, _realm.Intrinsics.Promise); var asyncEvalOrder = 0; module._topLevelCapability = capability; @@ -378,7 +378,7 @@ public JsValue Evaluate() /// /// https://tc39.es/ecma262/#sec-InnerModuleLinking /// - private int InnerModuleLinking(JsModule module, Stack stack, int index) + private int InnerModuleLinking(ModuleRecord module, Stack stack, int index) { if (module.Status is ModuleStatus.Linking or @@ -461,7 +461,7 @@ ModuleStatus.EvaluatingAsync or /// /// https://tc39.es/ecma262/#sec-innermoduleevaluation /// - private Completion Evaluate(JsModule module, Stack stack, int index, ref int asyncEvalOrder) + private Completion Evaluate(ModuleRecord module, Stack stack, int index, ref int asyncEvalOrder) { if (module.Status == ModuleStatus.EvaluatingAsync || module.Status == ModuleStatus.Evaluated) { @@ -616,7 +616,7 @@ private Completion Evaluate(JsModule module, Stack stack, int index, r return completion; } - private static int StackReferenceCount(Stack stack, JsModule module) + private static int StackReferenceCount(Stack stack, ModuleRecord module) { var count = 0; foreach (var item in stack) @@ -813,7 +813,7 @@ private Completion ExecuteAsync() /// /// https://tc39.es/ecma262/#sec-gather-available-ancestors /// - private void GatherAvailableAncestors(List execList) + private void GatherAvailableAncestors(List execList) { foreach (var m in _asyncParentModules) { @@ -844,7 +844,7 @@ m._evalError is not null || /// private JsValue AsyncModuleExecutionFulfilled(JsValue thisObj, JsValue[] arguments) { - var module = (JsModule) arguments.At(0); + var module = (ModuleRecord) arguments.At(0); if (module.Status == ModuleStatus.Evaluated) { if (module._evalError is not null) @@ -872,7 +872,7 @@ private JsValue AsyncModuleExecutionFulfilled(JsValue thisObj, JsValue[] argumen module._topLevelCapability.Resolve.Call(Undefined, Array.Empty()); } - var execList = new List(); + var execList = new List(); module.GatherAvailableAncestors(execList); execList.Sort((x, y) => x._asyncEvalOrder - y._asyncEvalOrder); @@ -918,7 +918,7 @@ private JsValue AsyncModuleExecutionFulfilled(JsValue thisObj, JsValue[] argumen /// private JsValue AsyncModuleExecutionRejected(JsValue thisObj, JsValue[] arguments) { - var module = (JsModule) arguments.At(0); + var module = (ModuleRecord) arguments.At(0); var error = arguments.At(1); if (module.Status == ModuleStatus.Evaluated) From 637702a7a4aea47facbbe4c790e74595b0151ea1 Mon Sep 17 00:00:00 2001 From: Christian Rondeau Date: Mon, 7 Mar 2022 00:47:22 -0500 Subject: [PATCH 14/31] Extract base ModuleRecord from CyclicModuleRecord to match ecmascript specs --- Jint/Engine.Modules.cs | 14 +- Jint/ModuleBuilder.cs | 2 +- Jint/Runtime/Host.cs | 4 +- Jint/Runtime/Modules/CyclicModuleRecord.cs | 924 ++++++++++++++++++++ Jint/Runtime/Modules/ModuleRecord.cs | 931 +-------------------- 5 files changed, 948 insertions(+), 927 deletions(-) create mode 100644 Jint/Runtime/Modules/CyclicModuleRecord.cs diff --git a/Jint/Engine.Modules.cs b/Jint/Engine.Modules.cs index 9cad64381b..9c6d9dcfd5 100644 --- a/Jint/Engine.Modules.cs +++ b/Jint/Engine.Modules.cs @@ -17,7 +17,7 @@ public partial class Engine { internal IModuleLoader ModuleLoader { get; set; } - private readonly Dictionary _modules = new(); + private readonly Dictionary _modules = new(); private readonly Dictionary _builders = new(); /// @@ -28,7 +28,7 @@ public partial class Engine return _executionContexts?.GetActiveScriptOrModule(); } - internal ModuleRecord LoadModule(string? referencingModuleLocation, string specifier) + internal CyclicModuleRecord LoadModule(string? referencingModuleLocation, string specifier) { var moduleResolution = ModuleLoader.Resolve(referencingModuleLocation, specifier); @@ -49,20 +49,20 @@ internal ModuleRecord LoadModule(string? referencingModuleLocation, string speci return module; } - private ModuleRecord LoadFromBuilder(string specifier, ModuleBuilder moduleBuilder, ResolvedSpecifier moduleResolution) + private CyclicModuleRecord LoadFromBuilder(string specifier, ModuleBuilder moduleBuilder, ResolvedSpecifier moduleResolution) { var parsedModule = moduleBuilder.Parse(); - var module = new ModuleRecord(this, Realm, parsedModule, null, false); + var module = new CyclicModuleRecord(this, Realm, parsedModule, null, false); _modules[moduleResolution.Key] = module; moduleBuilder.BindExportedValues(module); _builders.Remove(specifier); return module; } - private ModuleRecord LoaderFromModuleLoader(ResolvedSpecifier moduleResolution) + private CyclicModuleRecord LoaderFromModuleLoader(ResolvedSpecifier moduleResolution) { var parsedModule = ModuleLoader.LoadModule(this, moduleResolution); - var module = new ModuleRecord(this, Realm, parsedModule, moduleResolution.Uri?.LocalPath, false); + var module = new CyclicModuleRecord(this, Realm, parsedModule, moduleResolution.Uri?.LocalPath, false); _modules[moduleResolution.Key] = module; return module; } @@ -145,7 +145,7 @@ public ObjectInstance ImportModule(string specifier) // TODO what about callstack and thrown exceptions? RunAvailableContinuations(); - return ModuleRecord.GetModuleNamespace(module); + return CyclicModuleRecord.GetModuleNamespace(module); } ExceptionHelper.ThrowNotSupportedException($"Error while evaluating module: Module is in an invalid state: '{module.Status}'"); diff --git a/Jint/ModuleBuilder.cs b/Jint/ModuleBuilder.cs index 601e970167..bd67145caf 100644 --- a/Jint/ModuleBuilder.cs +++ b/Jint/ModuleBuilder.cs @@ -102,7 +102,7 @@ internal Module Parse() } } - internal void BindExportedValues(ModuleRecord module) + internal void BindExportedValues(CyclicModuleRecord module) { foreach (var export in _exports) { diff --git a/Jint/Runtime/Host.cs b/Jint/Runtime/Host.cs index f8c51d6bc5..5d0fd9f791 100644 --- a/Jint/Runtime/Host.cs +++ b/Jint/Runtime/Host.cs @@ -115,7 +115,7 @@ public virtual void EnsureCanCompileStrings(Realm callerRealm, Realm evalRealm) /// /// https://tc39.es/ecma262/#sec-hostresolveimportedmodule /// - protected internal virtual ModuleRecord ResolveImportedModule(ModuleRecord? referencingModule, string specifier) + protected internal virtual CyclicModuleRecord ResolveImportedModule(ModuleRecord? referencingModule, string specifier) { return Engine.LoadModule(referencingModule?.Location, specifier); } @@ -150,7 +150,7 @@ internal virtual void FinishDynamicImport(ModuleRecord? referencingModule, strin var moduleRecord = ResolveImportedModule(referencingModule, specifier); try { - var ns = ModuleRecord.GetModuleNamespace(moduleRecord); + var ns = CyclicModuleRecord.GetModuleNamespace(moduleRecord); promiseCapability.Resolve.Call(JsValue.Undefined, new[] { ns }); } catch (JavaScriptException ex) diff --git a/Jint/Runtime/Modules/CyclicModuleRecord.cs b/Jint/Runtime/Modules/CyclicModuleRecord.cs new file mode 100644 index 0000000000..7422626181 --- /dev/null +++ b/Jint/Runtime/Modules/CyclicModuleRecord.cs @@ -0,0 +1,924 @@ +using System; +using Esprima.Ast; +using System.Collections.Generic; +using Esprima; +using Jint.Native; +using Jint.Native.Object; +using Jint.Native.Promise; +using Jint.Runtime.Descriptors; +using Jint.Runtime.Environments; +using Jint.Runtime.Interop; +using Jint.Runtime.Interpreter; + +namespace Jint.Runtime.Modules; + +#pragma warning disable CS0649 // never assigned to, waiting for new functionalities in spec + +internal sealed record ResolvedBinding(CyclicModuleRecord Module, string BindingName) +{ + internal static ResolvedBinding Ambiguous => new(null, "ambiguous"); +} + +internal sealed record ImportEntry( + string ModuleRequest, + string ImportName, + string LocalName +); + +internal sealed record ExportEntry( + string ExportName, + string ModuleRequest, + string ImportName, + string LocalName +); + +/// +/// https://tc39.es/ecma262/#sec-cyclic-module-records +/// https://tc39.es/ecma262/#sec-source-text-module-records +/// +public sealed class CyclicModuleRecord : ModuleRecord +{ + private Completion? _evalError; + private int _dfsIndex; + private int _dfsAncestorIndex; + private readonly HashSet _requestedModules; + private CyclicModuleRecord _cycleRoot; + private bool _hasTLA; + private bool _asyncEvaluation; + private PromiseCapability _topLevelCapability; + private List _asyncParentModules; + private int _asyncEvalOrder; + private int _pendingAsyncDependencies; + + private readonly Module _source; + private ExecutionContext _context; + private readonly ObjectInstance _importMeta; + private readonly List _importEntries; + private readonly List _localExportEntries; + private readonly List _indirectExportEntries; + private readonly List _starExportEntries; + private readonly List> _exportBuilderDeclarations = new(); + internal JsValue _evalResult; + + internal CyclicModuleRecord(Engine engine, Realm realm, Module source, string location, bool async) : base(engine, realm, location) + { + _source = source; + + _importMeta = _realm.Intrinsics.Object.Construct(1); + _importMeta.DefineOwnProperty("url", new PropertyDescriptor(location, PropertyFlag.ConfigurableEnumerableWritable)); + + HoistingScope.GetImportsAndExports( + _source, + out _requestedModules, + out _importEntries, + out _localExportEntries, + out _indirectExportEntries, + out _starExportEntries); + + //ToDo async modules + } + + internal ModuleStatus Status { get; private set; } + + internal void BindExportedValue(string name, JsValue value) + { + if(_environment != null) ExceptionHelper.ThrowInvalidOperationException("Cannot bind exported values after the environment has been initialized"); + _exportBuilderDeclarations.Add(new KeyValuePair(name, value)); + } + + /// + /// https://tc39.es/ecma262/#sec-getexportednames + /// + public override List GetExportedNames(List exportStarSet = null) + { + exportStarSet ??= new List(); + if (exportStarSet.Contains(this)) + { + //Reached the starting point of an export * circularity + return new List(); + } + + exportStarSet.Add(this); + var exportedNames = new List(); + for (var i = 0; i < _localExportEntries.Count; i++) + { + var e = _localExportEntries[i]; + exportedNames.Add(e.ImportName ?? e.ExportName); + } + + for (var i = 0; i < _indirectExportEntries.Count; i++) + { + var e = _indirectExportEntries[i]; + exportedNames.Add(e.ImportName ?? e.ExportName); + } + + for (var i = 0; i < _starExportEntries.Count; i++) + { + var e = _starExportEntries[i]; + var requestedModule = _engine._host.ResolveImportedModule(this, e.ModuleRequest); + var starNames = requestedModule.GetExportedNames(exportStarSet); + + for (var j = 0; j < starNames.Count; j++) + { + var n = starNames[j]; + if (!"default".Equals(n) && !exportedNames.Contains(n)) + { + exportedNames.Add(n); + } + } + } + + return exportedNames; + } + + /// + /// https://tc39.es/ecma262/#sec-resolveexport + /// + internal override ResolvedBinding ResolveExport(string exportName, List resolveSet = null) + { + resolveSet ??= new List(); + + for (var i = 0; i < resolveSet.Count; i++) + { + var r = resolveSet[i]; + if (ReferenceEquals(this, r.Module) && exportName == r.ExportName) + { + // circular import request + return null; + } + } + + resolveSet.Add(new ExportResolveSetItem(this, exportName)); + for (var i = 0; i < _localExportEntries.Count; i++) + { + var e = _localExportEntries[i]; + if (exportName == (e.ImportName ?? e.ExportName)) + { + return new ResolvedBinding(this, e.LocalName ?? e.ExportName); + } + } + + for (var i = 0; i < _indirectExportEntries.Count; i++) + { + var e = _indirectExportEntries[i]; + if (exportName == (e.ImportName ?? e.ExportName)) + { + var importedModule = _engine._host.ResolveImportedModule(this, e.ModuleRequest); + if (e.ImportName == "*") + { + return new ResolvedBinding(importedModule, "*namespace*"); + } + else + { + return importedModule.ResolveExport(e.ExportName, resolveSet); + } + } + } + + if ("default".Equals(exportName)) + { + return null; + } + + ResolvedBinding starResolution = null; + + for (var i = 0; i < _starExportEntries.Count; i++) + { + var e = _starExportEntries[i]; + var importedModule = _engine._host.ResolveImportedModule(this, e.ModuleRequest); + var resolution = importedModule.ResolveExport(exportName, resolveSet); + if (resolution == ResolvedBinding.Ambiguous) + { + return resolution; + } + + if (resolution is not null) + { + if (starResolution is null) + { + starResolution = resolution; + } + else + { + if (resolution.Module != starResolution.Module || resolution.BindingName != starResolution.BindingName) + { + return ResolvedBinding.Ambiguous; + } + } + } + } + + return starResolution; + } + + /// + /// https://tc39.es/ecma262/#sec-moduledeclarationlinking + /// + public override void Link() + { + if (Status == ModuleStatus.Linking || Status == ModuleStatus.Evaluating) + { + ExceptionHelper.ThrowInvalidOperationException("Error while linking module: Module is already either linking or evaluating"); + } + + var stack = new Stack(); + + try + { + InnerModuleLinking(this, stack, 0); + } + catch + { + foreach (var m in stack) + { + m.Status = ModuleStatus.Unlinked; + m._environment = null; + m._dfsIndex = -1; + m._dfsAncestorIndex = -1; + } + + Status = ModuleStatus.Unlinked; + throw; + } + + if (Status != ModuleStatus.Linked && Status != ModuleStatus.Unlinked) + { + ExceptionHelper.ThrowInvalidOperationException("Error while linking module: Module is neither linked or unlinked"); + } + + if (stack.Count > 0) + { + ExceptionHelper.ThrowInvalidOperationException("Error while linking module: One or more modules were not linked"); + } + } + + /// + /// https://tc39.es/ecma262/#sec-moduleevaluation + /// + public override JsValue Evaluate() + { + var module = this; + + if (module.Status != ModuleStatus.Linked && + module.Status != ModuleStatus.EvaluatingAsync && + module.Status != ModuleStatus.Evaluated) + { + ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); + } + + if (module.Status == ModuleStatus.EvaluatingAsync || module.Status == ModuleStatus.Evaluated) + { + module = module._cycleRoot; + } + + if (module._topLevelCapability is not null) + { + return module._topLevelCapability.PromiseInstance; + } + + var stack = new Stack(); + var capability = PromiseConstructor.NewPromiseCapability(_engine, _realm.Intrinsics.Promise); + var asyncEvalOrder = 0; + module._topLevelCapability = capability; + + var result = Evaluate(module, stack, 0, ref asyncEvalOrder); + + if (result.Type != CompletionType.Normal) + { + foreach (var m in stack) + { + m.Status = ModuleStatus.Evaluated; + m._evalError = result; + } + + capability.Reject.Call(Undefined, new[] { result.Value }); + } + else + { + if (module.Status != ModuleStatus.EvaluatingAsync && module.Status != ModuleStatus.Evaluated) + { + ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); + } + + if (module._evalError is not null) + { + ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); + } + + if (!module._asyncEvaluation) + { + if (module.Status != ModuleStatus.Evaluated) + { + ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); + } + + capability.Resolve.Call(Undefined, Array.Empty()); + } + + if (stack.Count > 0) + { + ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); + } + } + + return capability.PromiseInstance; + } + + /// + /// https://tc39.es/ecma262/#sec-InnerModuleLinking + /// + private int InnerModuleLinking(CyclicModuleRecord module, Stack stack, int index) + { + if (module.Status is + ModuleStatus.Linking or + ModuleStatus.Linked or + ModuleStatus.EvaluatingAsync or + ModuleStatus.Evaluating) + { + return index; + } + + if (module.Status != ModuleStatus.Unlinked) + { + ExceptionHelper.ThrowInvalidOperationException("Error while linking module: Module in an invalid state"); + } + + module.Status = ModuleStatus.Linking; + module._dfsIndex = index; + module._dfsAncestorIndex = index; + index++; + stack.Push(module); + + var requestedModules = module._requestedModules; + + foreach (var moduleSpecifier in requestedModules) + { + var requiredModule = _engine._host.ResolveImportedModule(module, moduleSpecifier); + + //TODO: Should we link only when a module is requested? https://tc39.es/ecma262/#sec-example-cyclic-module-record-graphs Should we support retry? + if (requiredModule.Status == ModuleStatus.Unlinked) + { + index = requiredModule.InnerModuleLinking(requiredModule, stack, index); + } + + if (requiredModule.Status != ModuleStatus.Linking && + requiredModule.Status != ModuleStatus.Linked && + requiredModule.Status != ModuleStatus.Evaluated) + { + ExceptionHelper.ThrowInvalidOperationException($"Error while linking module: Required module is in an invalid state: {requiredModule.Status}"); + } + + if (requiredModule.Status == ModuleStatus.Linking && !stack.Contains(requiredModule)) + { + ExceptionHelper.ThrowInvalidOperationException($"Error while linking module: Required module is in an invalid state: {requiredModule.Status}"); + } + + if (requiredModule.Status == ModuleStatus.Linking) + { + module._dfsAncestorIndex = Math.Min(module._dfsAncestorIndex, requiredModule._dfsAncestorIndex); + } + } + + module.InitializeEnvironment(); + + if (StackReferenceCount(stack, module) != 1) + { + ExceptionHelper.ThrowInvalidOperationException("Error while linking module: Recursive dependency detected"); + } + + if (module._dfsIndex > module._dfsAncestorIndex) + { + ExceptionHelper.ThrowInvalidOperationException("Error while linking module: Recursive dependency detected"); + } + + if (module._dfsIndex == module._dfsAncestorIndex) + { + while (true) + { + var requiredModule = stack.Pop(); + requiredModule.Status = ModuleStatus.Linked; + if (requiredModule == module) + { + break; + } + } + } + + return index; + } + + /// + /// https://tc39.es/ecma262/#sec-innermoduleevaluation + /// + private Completion Evaluate(CyclicModuleRecord module, Stack stack, int index, ref int asyncEvalOrder) + { + if (module.Status == ModuleStatus.EvaluatingAsync || module.Status == ModuleStatus.Evaluated) + { + if (module._evalError is null) + { + return new Completion(CompletionType.Normal, index, null, default); + } + + return module._evalError.Value; + } + + if (module.Status == ModuleStatus.Evaluating) + { + return new Completion(CompletionType.Normal, index, null, default); + } + + if (module.Status != ModuleStatus.Linked) + { + ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); + } + + + module.Status = ModuleStatus.Evaluating; + module._dfsIndex = index; + module._dfsAncestorIndex = index; + module._pendingAsyncDependencies = 0; + index++; + stack.Push(module); + + var requestedModules = module._requestedModules; + + foreach (var moduleSpecifier in requestedModules) + { + var requiredModule = _engine._host.ResolveImportedModule(module, moduleSpecifier); + var result = Evaluate(module, stack, index, ref asyncEvalOrder); + if (result.Type != CompletionType.Normal) + { + return result; + } + + index = TypeConverter.ToInt32(result.Value); + + // TODO: Validate this behavior: https://tc39.es/ecma262/#sec-example-cyclic-module-record-graphs + if (requiredModule.Status == ModuleStatus.Linked) + { + var evaluationResult = requiredModule.Evaluate(); + if (evaluationResult == null) + { + ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module evaluation did not return a promise"); + } + else if (evaluationResult is not PromiseInstance promise) + { + ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module evaluation did not return a promise: {evaluationResult.Type}"); + } + else if (promise.State == PromiseState.Rejected) + { + ExceptionHelper.ThrowJavaScriptException(_engine, promise.Value, new Completion(CompletionType.Throw, promise.Value, null, new Location(new Position(), new Position(), moduleSpecifier))); + } + else if (promise.State != PromiseState.Fulfilled) + { + ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module evaluation did not return a fulfilled promise: {promise.State}"); + } + } + + if (requiredModule.Status != ModuleStatus.Evaluating && + requiredModule.Status != ModuleStatus.EvaluatingAsync && + requiredModule.Status != ModuleStatus.Evaluated) + { + ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module is in an invalid state: {requiredModule.Status}"); + } + + if (requiredModule.Status == ModuleStatus.Evaluating && !stack.Contains(requiredModule)) + { + ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module is in an invalid state: {requiredModule.Status}"); + } + + if (requiredModule.Status == ModuleStatus.Evaluating) + { + module._dfsAncestorIndex = Math.Min(module._dfsAncestorIndex, requiredModule._dfsAncestorIndex); + } + else + { + requiredModule = requiredModule._cycleRoot; + if (requiredModule.Status != ModuleStatus.EvaluatingAsync && requiredModule.Status != ModuleStatus.Evaluated) + { + ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); + } + } + + if (requiredModule._asyncEvaluation) + { + module._pendingAsyncDependencies++; + requiredModule._asyncParentModules.Add(module); + } + } + + Completion completion; + + if (module._pendingAsyncDependencies > 0 || module._hasTLA) + { + if (module._asyncEvaluation) + { + ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); + } + + module._asyncEvaluation = true; + module._asyncEvalOrder = asyncEvalOrder++; + if (module._pendingAsyncDependencies == 0) + { + completion = module.ExecuteAsync(); + } + else + { + completion = module.Execute(); + } + } + else + { + completion = module.Execute(); + } + + if (StackReferenceCount(stack, module) != 1) + { + ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); + } + + if (module._dfsAncestorIndex > module._dfsIndex) + { + ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); + } + + if (module._dfsIndex == module._dfsAncestorIndex) + { + var done = false; + while (!done) + { + var requiredModule = stack.Pop(); + if (!requiredModule._asyncEvaluation) + { + requiredModule.Status = ModuleStatus.Evaluated; + } + else + { + requiredModule.Status = ModuleStatus.EvaluatingAsync; + } + + done = requiredModule == module; + requiredModule._cycleRoot = module; + } + } + + return completion; + } + + private static int StackReferenceCount(Stack stack, CyclicModuleRecord module) + { + var count = 0; + foreach (var item in stack) + { + if (ReferenceEquals(item, module)) + { + count++; + } + } + + return count; + } + + /// + /// https://tc39.es/ecma262/#sec-source-text-module-record-initialize-environment + /// + private void InitializeEnvironment() + { + for (var i = 0; i < _indirectExportEntries.Count; i++) + { + var e = _indirectExportEntries[i]; + var resolution = ResolveExport(e.ImportName ?? e.ExportName); + if (resolution is null || resolution == ResolvedBinding.Ambiguous) + { + ExceptionHelper.ThrowSyntaxError(_realm, "Ambiguous import statement for identifier: " + e.ExportName); + } + } + + var realm = _realm; + var env = JintEnvironment.NewModuleEnvironment(_engine, realm.GlobalEnv); + _environment = env; + + if (_importEntries != null) + { + for (var i = 0; i < _importEntries.Count; i++) + { + var ie = _importEntries[i]; + var importedModule = _engine._host.ResolveImportedModule(this, ie.ModuleRequest); + if (ie.ImportName == "*") + { + var ns = GetModuleNamespace(importedModule); + env.CreateImmutableBinding(ie.LocalName, true); + env.InitializeBinding(ie.LocalName, ns); + } + else + { + var resolution = importedModule.ResolveExport(ie.ImportName); + if (resolution is null || resolution == ResolvedBinding.Ambiguous) + { + ExceptionHelper.ThrowSyntaxError(_realm, "Ambiguous import statement for identifier " + ie.ImportName); + } + + if (resolution.BindingName == "*namespace*") + { + var ns = GetModuleNamespace(resolution.Module); + env.CreateImmutableBinding(ie.LocalName, true); + env.InitializeBinding(ie.LocalName, ns); + } + else + { + env.CreateImportBinding(ie.LocalName, resolution.Module, resolution.BindingName); + } + } + } + } + + var moduleContext = new ExecutionContext(this, _environment, _environment, null, realm, null); + _context = moduleContext; + + _engine.EnterExecutionContext(_context); + + var hoistingScope = HoistingScope.GetModuleLevelDeclarations(_source); + + var varDeclarations = hoistingScope._variablesDeclarations; + var declaredVarNames = new HashSet(); + if (varDeclarations != null) + { + var boundNames = new List(); + for (var i = 0; i < varDeclarations.Count; i++) + { + var d = varDeclarations[i]; + boundNames.Clear(); + d.GetBoundNames(boundNames); + for (var j = 0; j < boundNames.Count; j++) + { + var dn = boundNames[j]; + if (declaredVarNames.Add(dn)) + { + env.CreateMutableBinding(dn, false); + env.InitializeBinding(dn, Undefined); + } + } + } + } + + var lexDeclarations = hoistingScope._lexicalDeclarations; + + if (lexDeclarations != null) + { + var boundNames = new List(); + for (var i = 0; i < lexDeclarations.Count; i++) + { + var d = lexDeclarations[i]; + boundNames.Clear(); + d.GetBoundNames(boundNames); + for (var j = 0; j < boundNames.Count; j++) + { + var dn = boundNames[j]; + if (d.IsConstantDeclaration()) + { + env.CreateImmutableBinding(dn, true); + } + else + { + env.CreateMutableBinding(dn, false); + } + } + } + } + + var functionDeclarations = hoistingScope._functionDeclarations; + + if (functionDeclarations != null) + { + for (var i = 0; i < functionDeclarations.Count; i++) + { + var d = functionDeclarations[i]; + var fn = d.Id?.Name ?? "*default*"; + var fd = new JintFunctionDefinition(_engine, d); + env.CreateMutableBinding(fn, true); + var fo = realm.Intrinsics.Function.InstantiateFunctionObject(fd, env); + env.InitializeBinding(fn, fo); + } + } + + if (_exportBuilderDeclarations != null) + { + for (var i = 0; i < _exportBuilderDeclarations.Count; i++) + { + var d = _exportBuilderDeclarations[i]; + _environment.CreateImmutableBindingAndInitialize(d.Key, true, d.Value); + _localExportEntries.Add(new ExportEntry(d.Key, null, null, null)); + } + _exportBuilderDeclarations.Clear(); + } + + _engine.LeaveExecutionContext(); + } + + /// + /// https://tc39.es/ecma262/#sec-source-text-module-record-execute-module + /// + private Completion Execute(PromiseCapability capability = null) + { + var moduleContext = new ExecutionContext(this, _environment, _environment, null, _realm); + if (!_hasTLA) + { + using (new StrictModeScope(true, force: true)) + { + _engine.EnterExecutionContext(moduleContext); + var statementList = new JintStatementList(null, _source.Body); + var result = statementList.Execute(_engine._activeEvaluationContext ?? new EvaluationContext(_engine)); //Create new evaluation context when called from e.g. module tests + _engine.LeaveExecutionContext(); + return result; + } + } + else + { + //ToDo async modules + return default; + } + } + + /// + /// https://tc39.es/ecma262/#sec-execute-async-module + /// + private Completion ExecuteAsync() + { + if (Status != ModuleStatus.Evaluating && Status != ModuleStatus.EvaluatingAsync || !_hasTLA) + { + ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); + } + + var capability = PromiseConstructor.NewPromiseCapability(_engine, _realm.Intrinsics.Promise); + + var onFullfilled = new ClrFunctionInstance(_engine, "fulfilled", AsyncModuleExecutionFulfilled, 1, PropertyFlag.Configurable); + var onRejected = new ClrFunctionInstance(_engine, "rejected", AsyncModuleExecutionRejected, 1, PropertyFlag.Configurable); + + PromiseOperations.PerformPromiseThen(_engine, (PromiseInstance) capability.PromiseInstance, onFullfilled, onRejected, null); + + return Execute(capability); + } + + /// + /// https://tc39.es/ecma262/#sec-gather-available-ancestors + /// + private void GatherAvailableAncestors(List execList) + { + foreach (var m in _asyncParentModules) + { + if (!execList.Contains(m) && m._cycleRoot._evalError is null) + { + if (m.Status != ModuleStatus.EvaluatingAsync || + m._evalError is not null || + !m._asyncEvaluation || + m._pendingAsyncDependencies <= 0) + { + ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); + } + + if (--m._pendingAsyncDependencies == 0) + { + execList.Add(m); + if (!m._hasTLA) + { + m.GatherAvailableAncestors(execList); + } + } + } + } + } + + /// + /// https://tc39.es/ecma262/#sec-async-module-execution-fulfilled + /// + private JsValue AsyncModuleExecutionFulfilled(JsValue thisObj, JsValue[] arguments) + { + var module = (CyclicModuleRecord) arguments.At(0); + if (module.Status == ModuleStatus.Evaluated) + { + if (module._evalError is not null) + { + ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); + } + + return Undefined; + } + + if (module.Status != ModuleStatus.EvaluatingAsync || + !module._asyncEvaluation || + module._evalError is not null) + { + ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); + } + + if (module._topLevelCapability is not null) + { + if (module._cycleRoot is null) + { + ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); + } + + module._topLevelCapability.Resolve.Call(Undefined, Array.Empty()); + } + + var execList = new List(); + module.GatherAvailableAncestors(execList); + execList.Sort((x, y) => x._asyncEvalOrder - y._asyncEvalOrder); + + for (var i = 0; i < execList.Count; i++) + { + var m = execList[i]; + if (m.Status == ModuleStatus.Evaluated && m._evalError is null) + { + ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); + } + else if (m._hasTLA) + { + m.ExecuteAsync(); + } + else + { + var result = m.Execute(); + if (result.Type != CompletionType.Normal) + { + AsyncModuleExecutionRejected(Undefined, new[] { m, result.Value }); + } + else + { + m.Status = ModuleStatus.Evaluated; + if (m._topLevelCapability is not null) + { + if (m._cycleRoot is null) + { + ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); + } + + m._topLevelCapability.Resolve.Call(Undefined, Array.Empty()); + } + } + } + } + + return Undefined; + } + + /// + /// https://tc39.es/ecma262/#sec-async-module-execution-rejected + /// + private JsValue AsyncModuleExecutionRejected(JsValue thisObj, JsValue[] arguments) + { + var module = (CyclicModuleRecord) arguments.At(0); + var error = arguments.At(1); + + if (module.Status == ModuleStatus.Evaluated) + { + if (module._evalError is null) + { + ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); + } + + return Undefined; + } + + if (module.Status != ModuleStatus.EvaluatingAsync || + !module._asyncEvaluation || + module._evalError is not null) + { + ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); + } + + module._evalError = new Completion(CompletionType.Throw, error, null, default); + module.Status = ModuleStatus.Evaluated; + + var asyncParentModules = module._asyncParentModules; + for (var i = 0; i < asyncParentModules.Count; i++) + { + var m = asyncParentModules[i]; + AsyncModuleExecutionRejected(thisObj, new[] { m, error }); + } + + if (module._topLevelCapability is not null) + { + if (module._cycleRoot is null) + { + ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); + } + + module._topLevelCapability.Reject.Call(Undefined, new[] { error }); + } + + + return Undefined; + } + + public override object ToObject() + { + ExceptionHelper.ThrowNotSupportedException(); + return null; + } + + public override string ToString() + { + return $"{Type}: {Location}"; + } +} \ No newline at end of file diff --git a/Jint/Runtime/Modules/ModuleRecord.cs b/Jint/Runtime/Modules/ModuleRecord.cs index 5891d382f4..1b099ba6d0 100644 --- a/Jint/Runtime/Modules/ModuleRecord.cs +++ b/Jint/Runtime/Modules/ModuleRecord.cs @@ -1,104 +1,43 @@ -using System; -using Esprima.Ast; -using System.Collections.Generic; -using Esprima; +using System.Collections.Generic; using Jint.Native; using Jint.Native.Object; -using Jint.Native.Promise; -using Jint.Runtime.Descriptors; using Jint.Runtime.Environments; -using Jint.Runtime.Interop; -using Jint.Runtime.Interpreter; namespace Jint.Runtime.Modules; -#pragma warning disable CS0649 // never assigned to, waiting for new functionalities in spec - -internal sealed record ResolvedBinding(ModuleRecord Module, string BindingName) -{ - internal static ResolvedBinding Ambiguous => new(null, "ambiguous"); -} - -internal sealed record ImportEntry( - string ModuleRequest, - string ImportName, - string LocalName -); - -internal sealed record ExportEntry( - string ExportName, - string ModuleRequest, - string ImportName, - string LocalName -); - internal sealed record ExportResolveSetItem( - ModuleRecord Module, + CyclicModuleRecord Module, string ExportName ); /// -/// Represents a module record /// https://tc39.es/ecma262/#sec-abstract-module-records -/// https://tc39.es/ecma262/#sec-cyclic-module-records -/// https://tc39.es/ecma262/#sec-source-text-module-records /// -public sealed class ModuleRecord : JsValue, IScriptOrModule +public abstract class ModuleRecord : JsValue, IScriptOrModule { - private readonly Engine _engine; - private readonly Realm _realm; + protected readonly Engine _engine; + protected readonly Realm _realm; + protected ObjectInstance _namespace; internal ModuleEnvironmentRecord _environment; - private ObjectInstance _namespace; - private Completion? _evalError; - private int _dfsIndex; - private int _dfsAncestorIndex; - private readonly HashSet _requestedModules; - private ModuleRecord _cycleRoot; - private bool _hasTLA; - private bool _asyncEvaluation; - private PromiseCapability _topLevelCapability; - private List _asyncParentModules; - private int _asyncEvalOrder; - private int _pendingAsyncDependencies; - private readonly Module _source; - private ExecutionContext _context; - private readonly ObjectInstance _importMeta; - private readonly List _importEntries; - private readonly List _localExportEntries; - private readonly List _indirectExportEntries; - private readonly List _starExportEntries; - private readonly List> _exportBuilderDeclarations = new(); - internal JsValue _evalResult; + public string Location { get; } - internal ModuleRecord(Engine engine, Realm realm, Module source, string location, bool async) : base(InternalTypes.Module) + internal ModuleRecord(Engine engine, Realm realm, string location) : base(InternalTypes.Module) { _engine = engine; _realm = realm; - _source = source; Location = location; - - _importMeta = _realm.Intrinsics.Object.Construct(1); - _importMeta.DefineOwnProperty("url", new PropertyDescriptor(location, PropertyFlag.ConfigurableEnumerableWritable)); - - HoistingScope.GetImportsAndExports( - _source, - out _requestedModules, - out _importEntries, - out _localExportEntries, - out _indirectExportEntries, - out _starExportEntries); - - //ToDo async modules } - public string Location { get; } - internal ModuleStatus Status { get; private set; } + public abstract List GetExportedNames(List exportStarSet = null); + internal abstract ResolvedBinding ResolveExport(string exportName, List resolveSet = null); + public abstract void Link(); + public abstract JsValue Evaluate(); /// /// https://tc39.es/ecma262/#sec-getmodulenamespace /// - public static ObjectInstance GetModuleNamespace(ModuleRecord module) + public static ObjectInstance GetModuleNamespace(CyclicModuleRecord module) { var ns = module._namespace; if (ns is null) @@ -124,852 +63,10 @@ public static ObjectInstance GetModuleNamespace(ModuleRecord module) /// /// https://tc39.es/ecma262/#sec-modulenamespacecreate /// - private static ObjectInstance CreateModuleNamespace(ModuleRecord module, List unambiguousNames) + private static ObjectInstance CreateModuleNamespace(CyclicModuleRecord module, List unambiguousNames) { var m = new ModuleNamespace(module._engine, module, unambiguousNames); module._namespace = m; return m; } - - internal void BindExportedValue(string name, JsValue value) - { - if(_environment != null) ExceptionHelper.ThrowInvalidOperationException("Cannot bind exported values after the environment has been initialized"); - _exportBuilderDeclarations.Add(new KeyValuePair(name, value)); - } - - /// - /// https://tc39.es/ecma262/#sec-getexportednames - /// - public List GetExportedNames(List exportStarSet = null) - { - exportStarSet ??= new List(); - if (exportStarSet.Contains(this)) - { - //Reached the starting point of an export * circularity - return new List(); - } - - exportStarSet.Add(this); - var exportedNames = new List(); - for (var i = 0; i < _localExportEntries.Count; i++) - { - var e = _localExportEntries[i]; - exportedNames.Add(e.ImportName ?? e.ExportName); - } - - for (var i = 0; i < _indirectExportEntries.Count; i++) - { - var e = _indirectExportEntries[i]; - exportedNames.Add(e.ImportName ?? e.ExportName); - } - - for (var i = 0; i < _starExportEntries.Count; i++) - { - var e = _starExportEntries[i]; - var requestedModule = _engine._host.ResolveImportedModule(this, e.ModuleRequest); - var starNames = requestedModule.GetExportedNames(exportStarSet); - - for (var j = 0; j < starNames.Count; j++) - { - var n = starNames[j]; - if (!"default".Equals(n) && !exportedNames.Contains(n)) - { - exportedNames.Add(n); - } - } - } - - return exportedNames; - } - - /// - /// https://tc39.es/ecma262/#sec-resolveexport - /// - internal ResolvedBinding ResolveExport(string exportName, List resolveSet = null) - { - resolveSet ??= new List(); - - for (var i = 0; i < resolveSet.Count; i++) - { - var r = resolveSet[i]; - if (ReferenceEquals(this, r.Module) && exportName == r.ExportName) - { - // circular import request - return null; - } - } - - resolveSet.Add(new ExportResolveSetItem(this, exportName)); - for (var i = 0; i < _localExportEntries.Count; i++) - { - var e = _localExportEntries[i]; - if (exportName == (e.ImportName ?? e.ExportName)) - { - return new ResolvedBinding(this, e.LocalName ?? e.ExportName); - } - } - - for (var i = 0; i < _indirectExportEntries.Count; i++) - { - var e = _indirectExportEntries[i]; - if (exportName == (e.ImportName ?? e.ExportName)) - { - var importedModule = _engine._host.ResolveImportedModule(this, e.ModuleRequest); - if (e.ImportName == "*") - { - return new ResolvedBinding(importedModule, "*namespace*"); - } - else - { - return importedModule.ResolveExport(e.ExportName, resolveSet); - } - } - } - - if ("default".Equals(exportName)) - { - return null; - } - - ResolvedBinding starResolution = null; - - for (var i = 0; i < _starExportEntries.Count; i++) - { - var e = _starExportEntries[i]; - var importedModule = _engine._host.ResolveImportedModule(this, e.ModuleRequest); - var resolution = importedModule.ResolveExport(exportName, resolveSet); - if (resolution == ResolvedBinding.Ambiguous) - { - return resolution; - } - - if (resolution is not null) - { - if (starResolution is null) - { - starResolution = resolution; - } - else - { - if (resolution.Module != starResolution.Module || resolution.BindingName != starResolution.BindingName) - { - return ResolvedBinding.Ambiguous; - } - } - } - } - - return starResolution; - } - - /// - /// https://tc39.es/ecma262/#sec-moduledeclarationlinking - /// - public void Link() - { - if (Status == ModuleStatus.Linking || Status == ModuleStatus.Evaluating) - { - ExceptionHelper.ThrowInvalidOperationException("Error while linking module: Module is already either linking or evaluating"); - } - - var stack = new Stack(); - - try - { - InnerModuleLinking(this, stack, 0); - } - catch - { - foreach (var m in stack) - { - m.Status = ModuleStatus.Unlinked; - m._environment = null; - m._dfsIndex = -1; - m._dfsAncestorIndex = -1; - } - - Status = ModuleStatus.Unlinked; - throw; - } - - if (Status != ModuleStatus.Linked && Status != ModuleStatus.Unlinked) - { - ExceptionHelper.ThrowInvalidOperationException("Error while linking module: Module is neither linked or unlinked"); - } - - if (stack.Count > 0) - { - ExceptionHelper.ThrowInvalidOperationException("Error while linking module: One or more modules were not linked"); - } - } - - /// - /// https://tc39.es/ecma262/#sec-moduleevaluation - /// - public JsValue Evaluate() - { - var module = this; - - if (module.Status != ModuleStatus.Linked && - module.Status != ModuleStatus.EvaluatingAsync && - module.Status != ModuleStatus.Evaluated) - { - ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); - } - - if (module.Status == ModuleStatus.EvaluatingAsync || module.Status == ModuleStatus.Evaluated) - { - module = module._cycleRoot; - } - - if (module._topLevelCapability is not null) - { - return module._topLevelCapability.PromiseInstance; - } - - var stack = new Stack(); - var capability = PromiseConstructor.NewPromiseCapability(_engine, _realm.Intrinsics.Promise); - var asyncEvalOrder = 0; - module._topLevelCapability = capability; - - var result = Evaluate(module, stack, 0, ref asyncEvalOrder); - - if (result.Type != CompletionType.Normal) - { - foreach (var m in stack) - { - m.Status = ModuleStatus.Evaluated; - m._evalError = result; - } - - capability.Reject.Call(Undefined, new[] { result.Value }); - } - else - { - if (module.Status != ModuleStatus.EvaluatingAsync && module.Status != ModuleStatus.Evaluated) - { - ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); - } - - if (module._evalError is not null) - { - ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); - } - - if (!module._asyncEvaluation) - { - if (module.Status != ModuleStatus.Evaluated) - { - ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); - } - - capability.Resolve.Call(Undefined, Array.Empty()); - } - - if (stack.Count > 0) - { - ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); - } - } - - return capability.PromiseInstance; - } - - /// - /// https://tc39.es/ecma262/#sec-InnerModuleLinking - /// - private int InnerModuleLinking(ModuleRecord module, Stack stack, int index) - { - if (module.Status is - ModuleStatus.Linking or - ModuleStatus.Linked or - ModuleStatus.EvaluatingAsync or - ModuleStatus.Evaluating) - { - return index; - } - - if (module.Status != ModuleStatus.Unlinked) - { - ExceptionHelper.ThrowInvalidOperationException("Error while linking module: Module in an invalid state"); - } - - module.Status = ModuleStatus.Linking; - module._dfsIndex = index; - module._dfsAncestorIndex = index; - index++; - stack.Push(module); - - var requestedModules = module._requestedModules; - - foreach (var moduleSpecifier in requestedModules) - { - var requiredModule = _engine._host.ResolveImportedModule(module, moduleSpecifier); - - //TODO: Should we link only when a module is requested? https://tc39.es/ecma262/#sec-example-cyclic-module-record-graphs Should we support retry? - if (requiredModule.Status == ModuleStatus.Unlinked) - { - index = requiredModule.InnerModuleLinking(requiredModule, stack, index); - } - - if (requiredModule.Status != ModuleStatus.Linking && - requiredModule.Status != ModuleStatus.Linked && - requiredModule.Status != ModuleStatus.Evaluated) - { - ExceptionHelper.ThrowInvalidOperationException($"Error while linking module: Required module is in an invalid state: {requiredModule.Status}"); - } - - if (requiredModule.Status == ModuleStatus.Linking && !stack.Contains(requiredModule)) - { - ExceptionHelper.ThrowInvalidOperationException($"Error while linking module: Required module is in an invalid state: {requiredModule.Status}"); - } - - if (requiredModule.Status == ModuleStatus.Linking) - { - module._dfsAncestorIndex = Math.Min(module._dfsAncestorIndex, requiredModule._dfsAncestorIndex); - } - } - - module.InitializeEnvironment(); - - if (StackReferenceCount(stack, module) != 1) - { - ExceptionHelper.ThrowInvalidOperationException("Error while linking module: Recursive dependency detected"); - } - - if (module._dfsIndex > module._dfsAncestorIndex) - { - ExceptionHelper.ThrowInvalidOperationException("Error while linking module: Recursive dependency detected"); - } - - if (module._dfsIndex == module._dfsAncestorIndex) - { - while (true) - { - var requiredModule = stack.Pop(); - requiredModule.Status = ModuleStatus.Linked; - if (requiredModule == module) - { - break; - } - } - } - - return index; - } - - /// - /// https://tc39.es/ecma262/#sec-innermoduleevaluation - /// - private Completion Evaluate(ModuleRecord module, Stack stack, int index, ref int asyncEvalOrder) - { - if (module.Status == ModuleStatus.EvaluatingAsync || module.Status == ModuleStatus.Evaluated) - { - if (module._evalError is null) - { - return new Completion(CompletionType.Normal, index, null, default); - } - - return module._evalError.Value; - } - - if (module.Status == ModuleStatus.Evaluating) - { - return new Completion(CompletionType.Normal, index, null, default); - } - - if (module.Status != ModuleStatus.Linked) - { - ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); - } - - - module.Status = ModuleStatus.Evaluating; - module._dfsIndex = index; - module._dfsAncestorIndex = index; - module._pendingAsyncDependencies = 0; - index++; - stack.Push(module); - - var requestedModules = module._requestedModules; - - foreach (var moduleSpecifier in requestedModules) - { - var requiredModule = _engine._host.ResolveImportedModule(module, moduleSpecifier); - var result = Evaluate(module, stack, index, ref asyncEvalOrder); - if (result.Type != CompletionType.Normal) - { - return result; - } - - index = TypeConverter.ToInt32(result.Value); - - // TODO: Validate this behavior: https://tc39.es/ecma262/#sec-example-cyclic-module-record-graphs - if (requiredModule.Status == ModuleStatus.Linked) - { - var evaluationResult = requiredModule.Evaluate(); - if (evaluationResult == null) - { - ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module evaluation did not return a promise"); - } - else if (evaluationResult is not PromiseInstance promise) - { - ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module evaluation did not return a promise: {evaluationResult.Type}"); - } - else if (promise.State == PromiseState.Rejected) - { - ExceptionHelper.ThrowJavaScriptException(_engine, promise.Value, new Completion(CompletionType.Throw, promise.Value, null, new Location(new Position(), new Position(), moduleSpecifier))); - } - else if (promise.State != PromiseState.Fulfilled) - { - ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module evaluation did not return a fulfilled promise: {promise.State}"); - } - } - - if (requiredModule.Status != ModuleStatus.Evaluating && - requiredModule.Status != ModuleStatus.EvaluatingAsync && - requiredModule.Status != ModuleStatus.Evaluated) - { - ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module is in an invalid state: {requiredModule.Status}"); - } - - if (requiredModule.Status == ModuleStatus.Evaluating && !stack.Contains(requiredModule)) - { - ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module is in an invalid state: {requiredModule.Status}"); - } - - if (requiredModule.Status == ModuleStatus.Evaluating) - { - module._dfsAncestorIndex = Math.Min(module._dfsAncestorIndex, requiredModule._dfsAncestorIndex); - } - else - { - requiredModule = requiredModule._cycleRoot; - if (requiredModule.Status != ModuleStatus.EvaluatingAsync && requiredModule.Status != ModuleStatus.Evaluated) - { - ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); - } - } - - if (requiredModule._asyncEvaluation) - { - module._pendingAsyncDependencies++; - requiredModule._asyncParentModules.Add(module); - } - } - - Completion completion; - - if (module._pendingAsyncDependencies > 0 || module._hasTLA) - { - if (module._asyncEvaluation) - { - ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); - } - - module._asyncEvaluation = true; - module._asyncEvalOrder = asyncEvalOrder++; - if (module._pendingAsyncDependencies == 0) - { - completion = module.ExecuteAsync(); - } - else - { - completion = module.Execute(); - } - } - else - { - completion = module.Execute(); - } - - if (StackReferenceCount(stack, module) != 1) - { - ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); - } - - if (module._dfsAncestorIndex > module._dfsIndex) - { - ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); - } - - if (module._dfsIndex == module._dfsAncestorIndex) - { - var done = false; - while (!done) - { - var requiredModule = stack.Pop(); - if (!requiredModule._asyncEvaluation) - { - requiredModule.Status = ModuleStatus.Evaluated; - } - else - { - requiredModule.Status = ModuleStatus.EvaluatingAsync; - } - - done = requiredModule == module; - requiredModule._cycleRoot = module; - } - } - - return completion; - } - - private static int StackReferenceCount(Stack stack, ModuleRecord module) - { - var count = 0; - foreach (var item in stack) - { - if (ReferenceEquals(item, module)) - { - count++; - } - } - - return count; - } - - /// - /// https://tc39.es/ecma262/#sec-source-text-module-record-initialize-environment - /// - private void InitializeEnvironment() - { - for (var i = 0; i < _indirectExportEntries.Count; i++) - { - var e = _indirectExportEntries[i]; - var resolution = ResolveExport(e.ImportName ?? e.ExportName); - if (resolution is null || resolution == ResolvedBinding.Ambiguous) - { - ExceptionHelper.ThrowSyntaxError(_realm, "Ambiguous import statement for identifier: " + e.ExportName); - } - } - - var realm = _realm; - var env = JintEnvironment.NewModuleEnvironment(_engine, realm.GlobalEnv); - _environment = env; - - if (_importEntries != null) - { - for (var i = 0; i < _importEntries.Count; i++) - { - var ie = _importEntries[i]; - var importedModule = _engine._host.ResolveImportedModule(this, ie.ModuleRequest); - if (ie.ImportName == "*") - { - var ns = GetModuleNamespace(importedModule); - env.CreateImmutableBinding(ie.LocalName, true); - env.InitializeBinding(ie.LocalName, ns); - } - else - { - var resolution = importedModule.ResolveExport(ie.ImportName); - if (resolution is null || resolution == ResolvedBinding.Ambiguous) - { - ExceptionHelper.ThrowSyntaxError(_realm, "Ambiguous import statement for identifier " + ie.ImportName); - } - - if (resolution.BindingName == "*namespace*") - { - var ns = GetModuleNamespace(resolution.Module); - env.CreateImmutableBinding(ie.LocalName, true); - env.InitializeBinding(ie.LocalName, ns); - } - else - { - env.CreateImportBinding(ie.LocalName, resolution.Module, resolution.BindingName); - } - } - } - } - - var moduleContext = new ExecutionContext(this, _environment, _environment, null, realm, null); - _context = moduleContext; - - _engine.EnterExecutionContext(_context); - - var hoistingScope = HoistingScope.GetModuleLevelDeclarations(_source); - - var varDeclarations = hoistingScope._variablesDeclarations; - var declaredVarNames = new HashSet(); - if (varDeclarations != null) - { - var boundNames = new List(); - for (var i = 0; i < varDeclarations.Count; i++) - { - var d = varDeclarations[i]; - boundNames.Clear(); - d.GetBoundNames(boundNames); - for (var j = 0; j < boundNames.Count; j++) - { - var dn = boundNames[j]; - if (declaredVarNames.Add(dn)) - { - env.CreateMutableBinding(dn, false); - env.InitializeBinding(dn, Undefined); - } - } - } - } - - var lexDeclarations = hoistingScope._lexicalDeclarations; - - if (lexDeclarations != null) - { - var boundNames = new List(); - for (var i = 0; i < lexDeclarations.Count; i++) - { - var d = lexDeclarations[i]; - boundNames.Clear(); - d.GetBoundNames(boundNames); - for (var j = 0; j < boundNames.Count; j++) - { - var dn = boundNames[j]; - if(d.IsConstantDeclaration()) - { - env.CreateImmutableBinding(dn, true); - } - else - { - env.CreateMutableBinding(dn, false); - } - } - } - } - - var functionDeclarations = hoistingScope._functionDeclarations; - - if (functionDeclarations != null) - { - for (var i = 0; i < functionDeclarations.Count; i++) - { - var d = functionDeclarations[i]; - var fn = d.Id?.Name ?? "*default*"; - var fd = new JintFunctionDefinition(_engine, d); - env.CreateMutableBinding(fn, true); - var fo = realm.Intrinsics.Function.InstantiateFunctionObject(fd, env); - env.InitializeBinding(fn, fo); - } - } - - if (_exportBuilderDeclarations != null) - { - for (var i = 0; i < _exportBuilderDeclarations.Count; i++) - { - var d = _exportBuilderDeclarations[i]; - _environment.CreateImmutableBindingAndInitialize(d.Key, true, d.Value); - _localExportEntries.Add(new ExportEntry(d.Key, null, null, null)); - } - _exportBuilderDeclarations.Clear(); - } - - _engine.LeaveExecutionContext(); - } - - /// - /// https://tc39.es/ecma262/#sec-source-text-module-record-execute-module - /// - private Completion Execute(PromiseCapability capability = null) - { - var moduleContext = new ExecutionContext(this, _environment, _environment, null, _realm); - if (!_hasTLA) - { - using (new StrictModeScope(true, force: true)) - { - _engine.EnterExecutionContext(moduleContext); - var statementList = new JintStatementList(null, _source.Body); - var result = statementList.Execute(_engine._activeEvaluationContext ?? new EvaluationContext(_engine)); //Create new evaluation context when called from e.g. module tests - _engine.LeaveExecutionContext(); - return result; - } - } - else - { - //ToDo async modules - return default; - } - } - - /// - /// https://tc39.es/ecma262/#sec-execute-async-module - /// - private Completion ExecuteAsync() - { - if (Status != ModuleStatus.Evaluating && Status != ModuleStatus.EvaluatingAsync || !_hasTLA) - { - ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); - } - - var capability = PromiseConstructor.NewPromiseCapability(_engine, _realm.Intrinsics.Promise); - - var onFullfilled = new ClrFunctionInstance(_engine, "fulfilled", AsyncModuleExecutionFulfilled, 1, PropertyFlag.Configurable); - var onRejected = new ClrFunctionInstance(_engine, "rejected", AsyncModuleExecutionRejected, 1, PropertyFlag.Configurable); - - PromiseOperations.PerformPromiseThen(_engine, (PromiseInstance) capability.PromiseInstance, onFullfilled, onRejected, null); - - return Execute(capability); - } - - /// - /// https://tc39.es/ecma262/#sec-gather-available-ancestors - /// - private void GatherAvailableAncestors(List execList) - { - foreach (var m in _asyncParentModules) - { - if (!execList.Contains(m) && m._cycleRoot._evalError is null) - { - if (m.Status != ModuleStatus.EvaluatingAsync || - m._evalError is not null || - !m._asyncEvaluation || - m._pendingAsyncDependencies <= 0) - { - ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); - } - - if (--m._pendingAsyncDependencies == 0) - { - execList.Add(m); - if (!m._hasTLA) - { - m.GatherAvailableAncestors(execList); - } - } - } - } - } - - /// - /// https://tc39.es/ecma262/#sec-async-module-execution-fulfilled - /// - private JsValue AsyncModuleExecutionFulfilled(JsValue thisObj, JsValue[] arguments) - { - var module = (ModuleRecord) arguments.At(0); - if (module.Status == ModuleStatus.Evaluated) - { - if (module._evalError is not null) - { - ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); - } - - return Undefined; - } - - if (module.Status != ModuleStatus.EvaluatingAsync || - !module._asyncEvaluation || - module._evalError is not null) - { - ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); - } - - if (module._topLevelCapability is not null) - { - if (module._cycleRoot is null) - { - ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); - } - - module._topLevelCapability.Resolve.Call(Undefined, Array.Empty()); - } - - var execList = new List(); - module.GatherAvailableAncestors(execList); - execList.Sort((x, y) => x._asyncEvalOrder - y._asyncEvalOrder); - - for (var i = 0; i < execList.Count; i++) - { - var m = execList[i]; - if (m.Status == ModuleStatus.Evaluated && m._evalError is null) - { - ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); - } - else if (m._hasTLA) - { - m.ExecuteAsync(); - } - else - { - var result = m.Execute(); - if (result.Type != CompletionType.Normal) - { - AsyncModuleExecutionRejected(Undefined, new[] { m, result.Value }); - } - else - { - m.Status = ModuleStatus.Evaluated; - if (m._topLevelCapability is not null) - { - if (m._cycleRoot is null) - { - ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); - } - - m._topLevelCapability.Resolve.Call(Undefined, Array.Empty()); - } - } - } - } - - return Undefined; - } - - /// - /// https://tc39.es/ecma262/#sec-async-module-execution-rejected - /// - private JsValue AsyncModuleExecutionRejected(JsValue thisObj, JsValue[] arguments) - { - var module = (ModuleRecord) arguments.At(0); - var error = arguments.At(1); - - if (module.Status == ModuleStatus.Evaluated) - { - if (module._evalError is null) - { - ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); - } - - return Undefined; - } - - if (module.Status != ModuleStatus.EvaluatingAsync || - !module._asyncEvaluation || - module._evalError is not null) - { - ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); - } - - module._evalError = new Completion(CompletionType.Throw, error, null, default); - module.Status = ModuleStatus.Evaluated; - - var asyncParentModules = module._asyncParentModules; - for (var i = 0; i < asyncParentModules.Count; i++) - { - var m = asyncParentModules[i]; - AsyncModuleExecutionRejected(thisObj, new[] { m, error }); - } - - if (module._topLevelCapability is not null) - { - if (module._cycleRoot is null) - { - ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); - } - - module._topLevelCapability.Reject.Call(Undefined, new[] { error }); - } - - - return Undefined; - } - - public override object ToObject() - { - ExceptionHelper.ThrowNotSupportedException(); - return null; - } - - public override string ToString() - { - return $"{Type}: {Location}"; - } } \ No newline at end of file From 99af4a8cfb00a70c976de68bb8b6dbae74f45581 Mon Sep 17 00:00:00 2001 From: Christian Rondeau Date: Mon, 7 Mar 2022 09:14:20 -0500 Subject: [PATCH 15/31] Use abstract ModuleRecord or ScriptOrModule to better match ecmascript --- Jint/Engine.Modules.cs | 80 +++++----- Jint/Runtime/Host.cs | 12 +- Jint/Runtime/IScriptOrModule.cs | 3 +- Jint/Runtime/Modules/CyclicModuleRecord.cs | 161 +++++++++++---------- Jint/Runtime/Modules/ModuleRecord.cs | 6 +- 5 files changed, 141 insertions(+), 121 deletions(-) diff --git a/Jint/Engine.Modules.cs b/Jint/Engine.Modules.cs index 9c6d9dcfd5..969e423590 100644 --- a/Jint/Engine.Modules.cs +++ b/Jint/Engine.Modules.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using Esprima; -using Esprima.Ast; using Jint.Native; using Jint.Native.Object; using Jint.Native.Promise; @@ -17,7 +16,7 @@ public partial class Engine { internal IModuleLoader ModuleLoader { get; set; } - private readonly Dictionary _modules = new(); + private readonly Dictionary _modules = new(); private readonly Dictionary _builders = new(); /// @@ -28,7 +27,7 @@ public partial class Engine return _executionContexts?.GetActiveScriptOrModule(); } - internal CyclicModuleRecord LoadModule(string? referencingModuleLocation, string specifier) + internal ModuleRecord LoadModule(string? referencingModuleLocation, string specifier) { var moduleResolution = ModuleLoader.Resolve(referencingModuleLocation, specifier); @@ -95,11 +94,16 @@ public ObjectInstance ImportModule(string specifier) module = LoadModule(null, specifier); } - if (module.Status == ModuleStatus.Unlinked) + if (module is not CyclicModuleRecord cyclicModule) + { + module.Link(); + EvaluateModule(specifier, module); + } + else if (cyclicModule.Status == ModuleStatus.Unlinked) { try { - module.Link(); + cyclicModule.Link(); } catch (JavaScriptException ex) { @@ -107,49 +111,53 @@ public ObjectInstance ImportModule(string specifier) ex.SetLocation(new Location(new Position(), new Position(), specifier)); throw; } - } - if (module.Status == ModuleStatus.Linked) - { - var ownsContext = _activeEvaluationContext is null; - _activeEvaluationContext ??= new EvaluationContext(this); - JsValue evaluationResult; - try - { - evaluationResult = module.Evaluate(); - } - finally + if (cyclicModule.Status == ModuleStatus.Linked) { - if (ownsContext) - { - _activeEvaluationContext = null; - } + EvaluateModule(specifier, cyclicModule); } - if (evaluationResult is not PromiseInstance promise) - { - ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module evaluation did not return a promise: {evaluationResult.Type}"); - } - else if (promise.State == PromiseState.Rejected) + if (cyclicModule.Status != ModuleStatus.Evaluated) { - ExceptionHelper.ThrowJavaScriptException(this, promise.Value, new Completion(CompletionType.Throw, promise.Value, null, new Location(new Position(), new Position(), specifier))); + ExceptionHelper.ThrowNotSupportedException($"Error while evaluating module: Module is in an invalid state: '{cyclicModule.Status}'"); } - else if (promise.State != PromiseState.Fulfilled) + } + + RunAvailableContinuations(); + + return ModuleRecord.GetModuleNamespace(module); + } + + private void EvaluateModule(string specifier, ModuleRecord cyclicModule) + { + var ownsContext = _activeEvaluationContext is null; + _activeEvaluationContext ??= new EvaluationContext(this); + JsValue evaluationResult; + try + { + evaluationResult = cyclicModule.Evaluate(); + } + finally + { + if (ownsContext) { - ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module evaluation did not return a fulfilled promise: {promise.State}"); + _activeEvaluationContext = null; } } - if (module.Status == ModuleStatus.Evaluated) + if (evaluationResult is not PromiseInstance promise) { - // TODO what about callstack and thrown exceptions? - RunAvailableContinuations(); - - return CyclicModuleRecord.GetModuleNamespace(module); + ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module evaluation did not return a promise: {evaluationResult.Type}"); + } + else if (promise.State == PromiseState.Rejected) + { + ExceptionHelper.ThrowJavaScriptException(this, promise.Value, + new Completion(CompletionType.Throw, promise.Value, null, new Location(new Position(), new Position(), specifier))); + } + else if (promise.State != PromiseState.Fulfilled) + { + ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module evaluation did not return a fulfilled promise: {promise.State}"); } - - ExceptionHelper.ThrowNotSupportedException($"Error while evaluating module: Module is in an invalid state: '{module.Status}'"); - return default; } } } diff --git a/Jint/Runtime/Host.cs b/Jint/Runtime/Host.cs index 5d0fd9f791..ec15d38ae9 100644 --- a/Jint/Runtime/Host.cs +++ b/Jint/Runtime/Host.cs @@ -115,15 +115,15 @@ public virtual void EnsureCanCompileStrings(Realm callerRealm, Realm evalRealm) /// /// https://tc39.es/ecma262/#sec-hostresolveimportedmodule /// - protected internal virtual CyclicModuleRecord ResolveImportedModule(ModuleRecord? referencingModule, string specifier) + protected internal virtual ModuleRecord ResolveImportedModule(IScriptOrModule? referencingScriptOrModule, string specifier) { - return Engine.LoadModule(referencingModule?.Location, specifier); + return Engine.LoadModule(referencingScriptOrModule?.Location, specifier); } /// /// https://tc39.es/ecma262/#sec-hostimportmoduledynamically /// - internal virtual void ImportModuleDynamically(ModuleRecord? referencingModule, string specifier, PromiseCapability promiseCapability) + internal virtual void ImportModuleDynamically(IScriptOrModule? referencingModule, string specifier, PromiseCapability promiseCapability) { var promise = Engine.RegisterPromise(); @@ -143,15 +143,15 @@ internal virtual void ImportModuleDynamically(ModuleRecord? referencingModule, s /// /// https://tc39.es/ecma262/#sec-finishdynamicimport /// - internal virtual void FinishDynamicImport(ModuleRecord? referencingModule, string specifier, PromiseCapability promiseCapability, PromiseInstance innerPromise) + internal virtual void FinishDynamicImport(IScriptOrModule? referencingModule, string specifier, PromiseCapability promiseCapability, PromiseInstance innerPromise) { var onFulfilled = new ClrFunctionInstance(Engine, "", (thisObj, args) => { var moduleRecord = ResolveImportedModule(referencingModule, specifier); try { - var ns = CyclicModuleRecord.GetModuleNamespace(moduleRecord); - promiseCapability.Resolve.Call(JsValue.Undefined, new[] { ns }); + var ns = ModuleRecord.GetModuleNamespace(moduleRecord); + promiseCapability.Resolve.Call(JsValue.Undefined, new JsValue[] { ns }); } catch (JavaScriptException ex) { diff --git a/Jint/Runtime/IScriptOrModule.cs b/Jint/Runtime/IScriptOrModule.cs index 278a61c983..97b7d9c983 100644 --- a/Jint/Runtime/IScriptOrModule.cs +++ b/Jint/Runtime/IScriptOrModule.cs @@ -1,5 +1,6 @@ namespace Jint.Runtime; -internal interface IScriptOrModule +public interface IScriptOrModule { + public string Location { get; } } diff --git a/Jint/Runtime/Modules/CyclicModuleRecord.cs b/Jint/Runtime/Modules/CyclicModuleRecord.cs index 7422626181..74b5b92b82 100644 --- a/Jint/Runtime/Modules/CyclicModuleRecord.cs +++ b/Jint/Runtime/Modules/CyclicModuleRecord.cs @@ -14,7 +14,7 @@ namespace Jint.Runtime.Modules; #pragma warning disable CS0649 // never assigned to, waiting for new functionalities in spec -internal sealed record ResolvedBinding(CyclicModuleRecord Module, string BindingName) +internal sealed record ResolvedBinding(ModuleRecord Module, string BindingName) { internal static ResolvedBinding Ambiguous => new(null, "ambiguous"); } @@ -225,7 +225,7 @@ public override void Link() try { - InnerModuleLinking(this, stack, 0); + InnerModuleLinking(stack, 0); } catch { @@ -266,7 +266,7 @@ public override JsValue Evaluate() ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); } - if (module.Status == ModuleStatus.EvaluatingAsync || module.Status == ModuleStatus.Evaluated) + if (module.Status is ModuleStatus.EvaluatingAsync or ModuleStatus.Evaluated) { module = module._cycleRoot; } @@ -281,7 +281,7 @@ public override JsValue Evaluate() var asyncEvalOrder = 0; module._topLevelCapability = capability; - var result = Evaluate(module, stack, 0, ref asyncEvalOrder); + var result = module.InnerModuleEvaluation(stack, 0, ref asyncEvalOrder); if (result.Type != CompletionType.Normal) { @@ -327,9 +327,9 @@ public override JsValue Evaluate() /// /// https://tc39.es/ecma262/#sec-InnerModuleLinking /// - private int InnerModuleLinking(CyclicModuleRecord module, Stack stack, int index) + private int InnerModuleLinking(Stack stack, int index) { - if (module.Status is + if (Status is ModuleStatus.Linking or ModuleStatus.Linked or ModuleStatus.EvaluatingAsync or @@ -338,66 +338,71 @@ ModuleStatus.EvaluatingAsync or return index; } - if (module.Status != ModuleStatus.Unlinked) + if (Status != ModuleStatus.Unlinked) { ExceptionHelper.ThrowInvalidOperationException("Error while linking module: Module in an invalid state"); } - module.Status = ModuleStatus.Linking; - module._dfsIndex = index; - module._dfsAncestorIndex = index; + Status = ModuleStatus.Linking; + _dfsIndex = index; + _dfsAncestorIndex = index; index++; - stack.Push(module); + stack.Push(this); - var requestedModules = module._requestedModules; + var requestedModules = _requestedModules; foreach (var moduleSpecifier in requestedModules) { - var requiredModule = _engine._host.ResolveImportedModule(module, moduleSpecifier); + var requiredModule = _engine._host.ResolveImportedModule(this, moduleSpecifier); + + if (requiredModule is not CyclicModuleRecord requiredCyclicModule) + { + continue; + } //TODO: Should we link only when a module is requested? https://tc39.es/ecma262/#sec-example-cyclic-module-record-graphs Should we support retry? - if (requiredModule.Status == ModuleStatus.Unlinked) + if (requiredCyclicModule.Status == ModuleStatus.Unlinked) { - index = requiredModule.InnerModuleLinking(requiredModule, stack, index); + index = requiredCyclicModule.InnerModuleLinking(stack, index); } - if (requiredModule.Status != ModuleStatus.Linking && - requiredModule.Status != ModuleStatus.Linked && - requiredModule.Status != ModuleStatus.Evaluated) + if (requiredCyclicModule.Status != ModuleStatus.Linking && + requiredCyclicModule.Status != ModuleStatus.Linked && + requiredCyclicModule.Status != ModuleStatus.Evaluated) { - ExceptionHelper.ThrowInvalidOperationException($"Error while linking module: Required module is in an invalid state: {requiredModule.Status}"); + ExceptionHelper.ThrowInvalidOperationException($"Error while linking module: Required module is in an invalid state: {requiredCyclicModule.Status}"); } - if (requiredModule.Status == ModuleStatus.Linking && !stack.Contains(requiredModule)) + if (requiredCyclicModule.Status == ModuleStatus.Linking && !stack.Contains(requiredCyclicModule)) { - ExceptionHelper.ThrowInvalidOperationException($"Error while linking module: Required module is in an invalid state: {requiredModule.Status}"); + ExceptionHelper.ThrowInvalidOperationException($"Error while linking module: Required module is in an invalid state: {requiredCyclicModule.Status}"); } - if (requiredModule.Status == ModuleStatus.Linking) + if (requiredCyclicModule.Status == ModuleStatus.Linking) { - module._dfsAncestorIndex = Math.Min(module._dfsAncestorIndex, requiredModule._dfsAncestorIndex); + _dfsAncestorIndex = Math.Min(_dfsAncestorIndex, requiredCyclicModule._dfsAncestorIndex); } } - module.InitializeEnvironment(); + InitializeEnvironment(); - if (StackReferenceCount(stack, module) != 1) + if (StackReferenceCount(stack) != 1) { ExceptionHelper.ThrowInvalidOperationException("Error while linking module: Recursive dependency detected"); } - if (module._dfsIndex > module._dfsAncestorIndex) + if (_dfsIndex > _dfsAncestorIndex) { ExceptionHelper.ThrowInvalidOperationException("Error while linking module: Recursive dependency detected"); } - if (module._dfsIndex == module._dfsAncestorIndex) + if (_dfsIndex == _dfsAncestorIndex) { while (true) { var requiredModule = stack.Pop(); requiredModule.Status = ModuleStatus.Linked; - if (requiredModule == module) + if (requiredModule == this) { break; } @@ -410,42 +415,47 @@ ModuleStatus.EvaluatingAsync or /// /// https://tc39.es/ecma262/#sec-innermoduleevaluation /// - private Completion Evaluate(CyclicModuleRecord module, Stack stack, int index, ref int asyncEvalOrder) + private Completion InnerModuleEvaluation(Stack stack, int index, ref int asyncEvalOrder) { - if (module.Status == ModuleStatus.EvaluatingAsync || module.Status == ModuleStatus.Evaluated) + if (Status is ModuleStatus.EvaluatingAsync or ModuleStatus.Evaluated) { - if (module._evalError is null) + if (_evalError is null) { return new Completion(CompletionType.Normal, index, null, default); } - return module._evalError.Value; + return _evalError.Value; } - if (module.Status == ModuleStatus.Evaluating) + if (Status == ModuleStatus.Evaluating) { return new Completion(CompletionType.Normal, index, null, default); } - if (module.Status != ModuleStatus.Linked) + if (Status != ModuleStatus.Linked) { ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); } - module.Status = ModuleStatus.Evaluating; - module._dfsIndex = index; - module._dfsAncestorIndex = index; - module._pendingAsyncDependencies = 0; + Status = ModuleStatus.Evaluating; + _dfsIndex = index; + _dfsAncestorIndex = index; + _pendingAsyncDependencies = 0; index++; - stack.Push(module); - - var requestedModules = module._requestedModules; + stack.Push(this); - foreach (var moduleSpecifier in requestedModules) + foreach (var moduleSpecifier in _requestedModules) { - var requiredModule = _engine._host.ResolveImportedModule(module, moduleSpecifier); - var result = Evaluate(module, stack, index, ref asyncEvalOrder); + var requiredModule = _engine._host.ResolveImportedModule(this, moduleSpecifier); + + if (requiredModule is not CyclicModuleRecord requiredCyclicModule) + { + ExceptionHelper.ThrowNotImplementedException($"Resolving modules of type {requiredModule.GetType()} is not implemented"); + continue; + } + + var result = requiredCyclicModule.InnerModuleEvaluation(stack, index, ref asyncEvalOrder); if (result.Type != CompletionType.Normal) { return result; @@ -454,9 +464,9 @@ private Completion Evaluate(CyclicModuleRecord module, Stack index = TypeConverter.ToInt32(result.Value); // TODO: Validate this behavior: https://tc39.es/ecma262/#sec-example-cyclic-module-record-graphs - if (requiredModule.Status == ModuleStatus.Linked) + if (requiredCyclicModule.Status == ModuleStatus.Linked) { - var evaluationResult = requiredModule.Evaluate(); + var evaluationResult = requiredCyclicModule.Evaluate(); if (evaluationResult == null) { ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module evaluation did not return a promise"); @@ -467,7 +477,8 @@ private Completion Evaluate(CyclicModuleRecord module, Stack } else if (promise.State == PromiseState.Rejected) { - ExceptionHelper.ThrowJavaScriptException(_engine, promise.Value, new Completion(CompletionType.Throw, promise.Value, null, new Location(new Position(), new Position(), moduleSpecifier))); + ExceptionHelper.ThrowJavaScriptException(_engine, promise.Value, + new Completion(CompletionType.Throw, promise.Value, null, new Location(new Position(), new Position(), moduleSpecifier))); } else if (promise.State != PromiseState.Fulfilled) { @@ -475,74 +486,74 @@ private Completion Evaluate(CyclicModuleRecord module, Stack } } - if (requiredModule.Status != ModuleStatus.Evaluating && - requiredModule.Status != ModuleStatus.EvaluatingAsync && - requiredModule.Status != ModuleStatus.Evaluated) + if (requiredCyclicModule.Status != ModuleStatus.Evaluating && + requiredCyclicModule.Status != ModuleStatus.EvaluatingAsync && + requiredCyclicModule.Status != ModuleStatus.Evaluated) { - ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module is in an invalid state: {requiredModule.Status}"); + ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module is in an invalid state: {requiredCyclicModule.Status}"); } - if (requiredModule.Status == ModuleStatus.Evaluating && !stack.Contains(requiredModule)) + if (requiredCyclicModule.Status == ModuleStatus.Evaluating && !stack.Contains(requiredCyclicModule)) { - ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module is in an invalid state: {requiredModule.Status}"); + ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module is in an invalid state: {requiredCyclicModule.Status}"); } - if (requiredModule.Status == ModuleStatus.Evaluating) + if (requiredCyclicModule.Status == ModuleStatus.Evaluating) { - module._dfsAncestorIndex = Math.Min(module._dfsAncestorIndex, requiredModule._dfsAncestorIndex); + _dfsAncestorIndex = Math.Min(_dfsAncestorIndex, requiredCyclicModule._dfsAncestorIndex); } else { - requiredModule = requiredModule._cycleRoot; - if (requiredModule.Status != ModuleStatus.EvaluatingAsync && requiredModule.Status != ModuleStatus.Evaluated) + requiredCyclicModule = requiredCyclicModule._cycleRoot; + if (requiredCyclicModule.Status != ModuleStatus.EvaluatingAsync && requiredCyclicModule.Status != ModuleStatus.Evaluated) { ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); } } - if (requiredModule._asyncEvaluation) + if (requiredCyclicModule._asyncEvaluation) { - module._pendingAsyncDependencies++; - requiredModule._asyncParentModules.Add(module); + _pendingAsyncDependencies++; + requiredCyclicModule._asyncParentModules.Add(this); } } Completion completion; - if (module._pendingAsyncDependencies > 0 || module._hasTLA) + if (_pendingAsyncDependencies > 0 || _hasTLA) { - if (module._asyncEvaluation) + if (_asyncEvaluation) { ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); } - module._asyncEvaluation = true; - module._asyncEvalOrder = asyncEvalOrder++; - if (module._pendingAsyncDependencies == 0) + _asyncEvaluation = true; + _asyncEvalOrder = asyncEvalOrder++; + if (_pendingAsyncDependencies == 0) { - completion = module.ExecuteAsync(); + completion = ExecuteAsync(); } else { - completion = module.Execute(); + completion = Execute(); } } else { - completion = module.Execute(); + completion = Execute(); } - if (StackReferenceCount(stack, module) != 1) + if (StackReferenceCount(stack) != 1) { ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); } - if (module._dfsAncestorIndex > module._dfsIndex) + if (_dfsAncestorIndex > _dfsIndex) { ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); } - if (module._dfsIndex == module._dfsAncestorIndex) + if (_dfsIndex == _dfsAncestorIndex) { var done = false; while (!done) @@ -557,20 +568,20 @@ private Completion Evaluate(CyclicModuleRecord module, Stack requiredModule.Status = ModuleStatus.EvaluatingAsync; } - done = requiredModule == module; - requiredModule._cycleRoot = module; + done = requiredModule == this; + requiredModule._cycleRoot = this; } } return completion; } - private static int StackReferenceCount(Stack stack, CyclicModuleRecord module) + private int StackReferenceCount(Stack stack) { var count = 0; foreach (var item in stack) { - if (ReferenceEquals(item, module)) + if (ReferenceEquals(item, this)) { count++; } diff --git a/Jint/Runtime/Modules/ModuleRecord.cs b/Jint/Runtime/Modules/ModuleRecord.cs index 1b099ba6d0..4b56bb2e6d 100644 --- a/Jint/Runtime/Modules/ModuleRecord.cs +++ b/Jint/Runtime/Modules/ModuleRecord.cs @@ -15,9 +15,9 @@ string ExportName /// public abstract class ModuleRecord : JsValue, IScriptOrModule { + private ObjectInstance _namespace; protected readonly Engine _engine; protected readonly Realm _realm; - protected ObjectInstance _namespace; internal ModuleEnvironmentRecord _environment; public string Location { get; } @@ -37,7 +37,7 @@ internal ModuleRecord(Engine engine, Realm realm, string location) : base(Intern /// /// https://tc39.es/ecma262/#sec-getmodulenamespace /// - public static ObjectInstance GetModuleNamespace(CyclicModuleRecord module) + public static ObjectInstance GetModuleNamespace(ModuleRecord module) { var ns = module._namespace; if (ns is null) @@ -63,7 +63,7 @@ public static ObjectInstance GetModuleNamespace(CyclicModuleRecord module) /// /// https://tc39.es/ecma262/#sec-modulenamespacecreate /// - private static ObjectInstance CreateModuleNamespace(CyclicModuleRecord module, List unambiguousNames) + private static ObjectInstance CreateModuleNamespace(ModuleRecord module, List unambiguousNames) { var m = new ModuleNamespace(module._engine, module, unambiguousNames); module._namespace = m; From bdf3cce9d67f9765a85785468eef017700ab9b23 Mon Sep 17 00:00:00 2001 From: Christian Rondeau Date: Mon, 7 Mar 2022 12:06:26 -0500 Subject: [PATCH 16/31] Extract SourceTextModuleRecord from CyclicModuleRecord to better match ecmascript --- Jint/Engine.Modules.cs | 4 +- Jint/ModuleBuilder.cs | 2 +- Jint/Runtime/Modules/BuilderModuleRecord.cs | 42 ++ Jint/Runtime/Modules/CyclicModuleRecord.cs | 405 ++---------------- Jint/Runtime/Modules/ModuleRecord.cs | 11 + .../Runtime/Modules/SourceTextModuleRecord.cs | 338 +++++++++++++++ 6 files changed, 429 insertions(+), 373 deletions(-) create mode 100644 Jint/Runtime/Modules/BuilderModuleRecord.cs create mode 100644 Jint/Runtime/Modules/SourceTextModuleRecord.cs diff --git a/Jint/Engine.Modules.cs b/Jint/Engine.Modules.cs index 969e423590..7e4808bac2 100644 --- a/Jint/Engine.Modules.cs +++ b/Jint/Engine.Modules.cs @@ -51,7 +51,7 @@ internal ModuleRecord LoadModule(string? referencingModuleLocation, string speci private CyclicModuleRecord LoadFromBuilder(string specifier, ModuleBuilder moduleBuilder, ResolvedSpecifier moduleResolution) { var parsedModule = moduleBuilder.Parse(); - var module = new CyclicModuleRecord(this, Realm, parsedModule, null, false); + var module = new BuilderModuleRecord(this, Realm, parsedModule, null, false); _modules[moduleResolution.Key] = module; moduleBuilder.BindExportedValues(module); _builders.Remove(specifier); @@ -61,7 +61,7 @@ private CyclicModuleRecord LoadFromBuilder(string specifier, ModuleBuilder modul private CyclicModuleRecord LoaderFromModuleLoader(ResolvedSpecifier moduleResolution) { var parsedModule = ModuleLoader.LoadModule(this, moduleResolution); - var module = new CyclicModuleRecord(this, Realm, parsedModule, moduleResolution.Uri?.LocalPath, false); + var module = new SourceTextModuleRecord(this, Realm, parsedModule, moduleResolution.Uri?.LocalPath, false); _modules[moduleResolution.Key] = module; return module; } diff --git a/Jint/ModuleBuilder.cs b/Jint/ModuleBuilder.cs index bd67145caf..b36a9a57fe 100644 --- a/Jint/ModuleBuilder.cs +++ b/Jint/ModuleBuilder.cs @@ -102,7 +102,7 @@ internal Module Parse() } } - internal void BindExportedValues(CyclicModuleRecord module) + internal void BindExportedValues(BuilderModuleRecord module) { foreach (var export in _exports) { diff --git a/Jint/Runtime/Modules/BuilderModuleRecord.cs b/Jint/Runtime/Modules/BuilderModuleRecord.cs new file mode 100644 index 0000000000..ba242dd47b --- /dev/null +++ b/Jint/Runtime/Modules/BuilderModuleRecord.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using Esprima.Ast; +using Jint.Native; + +namespace Jint.Runtime.Modules; + +/// +/// This is a custom ModuleRecord implementation for dynamically built modules using +/// +internal class BuilderModuleRecord : SourceTextModuleRecord +{ + private List> _exportBuilderDeclarations = new(); + + internal BuilderModuleRecord(Engine engine, Realm realm, Module source, string location, bool async) + : base(engine, realm, source, location, async) + { + } + + internal void BindExportedValue(string name, JsValue value) + { + if(_environment != null) ExceptionHelper.ThrowInvalidOperationException("Cannot bind exported values after the environment has been initialized"); + if (_exportBuilderDeclarations == null) _exportBuilderDeclarations = new(); + _exportBuilderDeclarations.Add(new KeyValuePair(name, value)); + } + + protected override void InitializeEnvironment() + { + base.InitializeEnvironment(); + + if (_exportBuilderDeclarations != null) + { + for (var i = 0; i < _exportBuilderDeclarations.Count; i++) + { + var d = _exportBuilderDeclarations[i]; + _environment.CreateImmutableBindingAndInitialize(d.Key, true, d.Value); + _localExportEntries.Add(new ExportEntry(d.Key, null, null, null)); + } + _exportBuilderDeclarations.Clear(); + } + + } +} \ No newline at end of file diff --git a/Jint/Runtime/Modules/CyclicModuleRecord.cs b/Jint/Runtime/Modules/CyclicModuleRecord.cs index 74b5b92b82..39e7c72ff2 100644 --- a/Jint/Runtime/Modules/CyclicModuleRecord.cs +++ b/Jint/Runtime/Modules/CyclicModuleRecord.cs @@ -3,12 +3,9 @@ using System.Collections.Generic; using Esprima; using Jint.Native; -using Jint.Native.Object; using Jint.Native.Promise; using Jint.Runtime.Descriptors; -using Jint.Runtime.Environments; using Jint.Runtime.Interop; -using Jint.Runtime.Interpreter; namespace Jint.Runtime.Modules; @@ -19,198 +16,31 @@ internal sealed record ResolvedBinding(ModuleRecord Module, string BindingName) internal static ResolvedBinding Ambiguous => new(null, "ambiguous"); } -internal sealed record ImportEntry( - string ModuleRequest, - string ImportName, - string LocalName -); - -internal sealed record ExportEntry( - string ExportName, - string ModuleRequest, - string ImportName, - string LocalName -); - /// /// https://tc39.es/ecma262/#sec-cyclic-module-records -/// https://tc39.es/ecma262/#sec-source-text-module-records /// -public sealed class CyclicModuleRecord : ModuleRecord +public abstract class CyclicModuleRecord : ModuleRecord { private Completion? _evalError; private int _dfsIndex; private int _dfsAncestorIndex; - private readonly HashSet _requestedModules; + protected HashSet _requestedModules; private CyclicModuleRecord _cycleRoot; - private bool _hasTLA; + protected bool _hasTLA; private bool _asyncEvaluation; private PromiseCapability _topLevelCapability; private List _asyncParentModules; private int _asyncEvalOrder; private int _pendingAsyncDependencies; - private readonly Module _source; - private ExecutionContext _context; - private readonly ObjectInstance _importMeta; - private readonly List _importEntries; - private readonly List _localExportEntries; - private readonly List _indirectExportEntries; - private readonly List _starExportEntries; - private readonly List> _exportBuilderDeclarations = new(); internal JsValue _evalResult; internal CyclicModuleRecord(Engine engine, Realm realm, Module source, string location, bool async) : base(engine, realm, location) { - _source = source; - - _importMeta = _realm.Intrinsics.Object.Construct(1); - _importMeta.DefineOwnProperty("url", new PropertyDescriptor(location, PropertyFlag.ConfigurableEnumerableWritable)); - - HoistingScope.GetImportsAndExports( - _source, - out _requestedModules, - out _importEntries, - out _localExportEntries, - out _indirectExportEntries, - out _starExportEntries); - - //ToDo async modules } internal ModuleStatus Status { get; private set; } - internal void BindExportedValue(string name, JsValue value) - { - if(_environment != null) ExceptionHelper.ThrowInvalidOperationException("Cannot bind exported values after the environment has been initialized"); - _exportBuilderDeclarations.Add(new KeyValuePair(name, value)); - } - - /// - /// https://tc39.es/ecma262/#sec-getexportednames - /// - public override List GetExportedNames(List exportStarSet = null) - { - exportStarSet ??= new List(); - if (exportStarSet.Contains(this)) - { - //Reached the starting point of an export * circularity - return new List(); - } - - exportStarSet.Add(this); - var exportedNames = new List(); - for (var i = 0; i < _localExportEntries.Count; i++) - { - var e = _localExportEntries[i]; - exportedNames.Add(e.ImportName ?? e.ExportName); - } - - for (var i = 0; i < _indirectExportEntries.Count; i++) - { - var e = _indirectExportEntries[i]; - exportedNames.Add(e.ImportName ?? e.ExportName); - } - - for (var i = 0; i < _starExportEntries.Count; i++) - { - var e = _starExportEntries[i]; - var requestedModule = _engine._host.ResolveImportedModule(this, e.ModuleRequest); - var starNames = requestedModule.GetExportedNames(exportStarSet); - - for (var j = 0; j < starNames.Count; j++) - { - var n = starNames[j]; - if (!"default".Equals(n) && !exportedNames.Contains(n)) - { - exportedNames.Add(n); - } - } - } - - return exportedNames; - } - - /// - /// https://tc39.es/ecma262/#sec-resolveexport - /// - internal override ResolvedBinding ResolveExport(string exportName, List resolveSet = null) - { - resolveSet ??= new List(); - - for (var i = 0; i < resolveSet.Count; i++) - { - var r = resolveSet[i]; - if (ReferenceEquals(this, r.Module) && exportName == r.ExportName) - { - // circular import request - return null; - } - } - - resolveSet.Add(new ExportResolveSetItem(this, exportName)); - for (var i = 0; i < _localExportEntries.Count; i++) - { - var e = _localExportEntries[i]; - if (exportName == (e.ImportName ?? e.ExportName)) - { - return new ResolvedBinding(this, e.LocalName ?? e.ExportName); - } - } - - for (var i = 0; i < _indirectExportEntries.Count; i++) - { - var e = _indirectExportEntries[i]; - if (exportName == (e.ImportName ?? e.ExportName)) - { - var importedModule = _engine._host.ResolveImportedModule(this, e.ModuleRequest); - if (e.ImportName == "*") - { - return new ResolvedBinding(importedModule, "*namespace*"); - } - else - { - return importedModule.ResolveExport(e.ExportName, resolveSet); - } - } - } - - if ("default".Equals(exportName)) - { - return null; - } - - ResolvedBinding starResolution = null; - - for (var i = 0; i < _starExportEntries.Count; i++) - { - var e = _starExportEntries[i]; - var importedModule = _engine._host.ResolveImportedModule(this, e.ModuleRequest); - var resolution = importedModule.ResolveExport(exportName, resolveSet); - if (resolution == ResolvedBinding.Ambiguous) - { - return resolution; - } - - if (resolution is not null) - { - if (starResolution is null) - { - starResolution = resolution; - } - else - { - if (resolution.Module != starResolution.Module || resolution.BindingName != starResolution.BindingName) - { - return ResolvedBinding.Ambiguous; - } - } - } - } - - return starResolution; - } - /// /// https://tc39.es/ecma262/#sec-moduledeclarationlinking /// @@ -590,166 +420,6 @@ private int StackReferenceCount(Stack stack) return count; } - /// - /// https://tc39.es/ecma262/#sec-source-text-module-record-initialize-environment - /// - private void InitializeEnvironment() - { - for (var i = 0; i < _indirectExportEntries.Count; i++) - { - var e = _indirectExportEntries[i]; - var resolution = ResolveExport(e.ImportName ?? e.ExportName); - if (resolution is null || resolution == ResolvedBinding.Ambiguous) - { - ExceptionHelper.ThrowSyntaxError(_realm, "Ambiguous import statement for identifier: " + e.ExportName); - } - } - - var realm = _realm; - var env = JintEnvironment.NewModuleEnvironment(_engine, realm.GlobalEnv); - _environment = env; - - if (_importEntries != null) - { - for (var i = 0; i < _importEntries.Count; i++) - { - var ie = _importEntries[i]; - var importedModule = _engine._host.ResolveImportedModule(this, ie.ModuleRequest); - if (ie.ImportName == "*") - { - var ns = GetModuleNamespace(importedModule); - env.CreateImmutableBinding(ie.LocalName, true); - env.InitializeBinding(ie.LocalName, ns); - } - else - { - var resolution = importedModule.ResolveExport(ie.ImportName); - if (resolution is null || resolution == ResolvedBinding.Ambiguous) - { - ExceptionHelper.ThrowSyntaxError(_realm, "Ambiguous import statement for identifier " + ie.ImportName); - } - - if (resolution.BindingName == "*namespace*") - { - var ns = GetModuleNamespace(resolution.Module); - env.CreateImmutableBinding(ie.LocalName, true); - env.InitializeBinding(ie.LocalName, ns); - } - else - { - env.CreateImportBinding(ie.LocalName, resolution.Module, resolution.BindingName); - } - } - } - } - - var moduleContext = new ExecutionContext(this, _environment, _environment, null, realm, null); - _context = moduleContext; - - _engine.EnterExecutionContext(_context); - - var hoistingScope = HoistingScope.GetModuleLevelDeclarations(_source); - - var varDeclarations = hoistingScope._variablesDeclarations; - var declaredVarNames = new HashSet(); - if (varDeclarations != null) - { - var boundNames = new List(); - for (var i = 0; i < varDeclarations.Count; i++) - { - var d = varDeclarations[i]; - boundNames.Clear(); - d.GetBoundNames(boundNames); - for (var j = 0; j < boundNames.Count; j++) - { - var dn = boundNames[j]; - if (declaredVarNames.Add(dn)) - { - env.CreateMutableBinding(dn, false); - env.InitializeBinding(dn, Undefined); - } - } - } - } - - var lexDeclarations = hoistingScope._lexicalDeclarations; - - if (lexDeclarations != null) - { - var boundNames = new List(); - for (var i = 0; i < lexDeclarations.Count; i++) - { - var d = lexDeclarations[i]; - boundNames.Clear(); - d.GetBoundNames(boundNames); - for (var j = 0; j < boundNames.Count; j++) - { - var dn = boundNames[j]; - if (d.IsConstantDeclaration()) - { - env.CreateImmutableBinding(dn, true); - } - else - { - env.CreateMutableBinding(dn, false); - } - } - } - } - - var functionDeclarations = hoistingScope._functionDeclarations; - - if (functionDeclarations != null) - { - for (var i = 0; i < functionDeclarations.Count; i++) - { - var d = functionDeclarations[i]; - var fn = d.Id?.Name ?? "*default*"; - var fd = new JintFunctionDefinition(_engine, d); - env.CreateMutableBinding(fn, true); - var fo = realm.Intrinsics.Function.InstantiateFunctionObject(fd, env); - env.InitializeBinding(fn, fo); - } - } - - if (_exportBuilderDeclarations != null) - { - for (var i = 0; i < _exportBuilderDeclarations.Count; i++) - { - var d = _exportBuilderDeclarations[i]; - _environment.CreateImmutableBindingAndInitialize(d.Key, true, d.Value); - _localExportEntries.Add(new ExportEntry(d.Key, null, null, null)); - } - _exportBuilderDeclarations.Clear(); - } - - _engine.LeaveExecutionContext(); - } - - /// - /// https://tc39.es/ecma262/#sec-source-text-module-record-execute-module - /// - private Completion Execute(PromiseCapability capability = null) - { - var moduleContext = new ExecutionContext(this, _environment, _environment, null, _realm); - if (!_hasTLA) - { - using (new StrictModeScope(true, force: true)) - { - _engine.EnterExecutionContext(moduleContext); - var statementList = new JintStatementList(null, _source.Body); - var result = statementList.Execute(_engine._activeEvaluationContext ?? new EvaluationContext(_engine)); //Create new evaluation context when called from e.g. module tests - _engine.LeaveExecutionContext(); - return result; - } - } - else - { - //ToDo async modules - return default; - } - } - /// /// https://tc39.es/ecma262/#sec-execute-async-module /// @@ -770,34 +440,6 @@ private Completion ExecuteAsync() return Execute(capability); } - /// - /// https://tc39.es/ecma262/#sec-gather-available-ancestors - /// - private void GatherAvailableAncestors(List execList) - { - foreach (var m in _asyncParentModules) - { - if (!execList.Contains(m) && m._cycleRoot._evalError is null) - { - if (m.Status != ModuleStatus.EvaluatingAsync || - m._evalError is not null || - !m._asyncEvaluation || - m._pendingAsyncDependencies <= 0) - { - ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); - } - - if (--m._pendingAsyncDependencies == 0) - { - execList.Add(m); - if (!m._hasTLA) - { - m.GatherAvailableAncestors(execList); - } - } - } - } - } /// /// https://tc39.es/ecma262/#sec-async-module-execution-fulfilled @@ -878,7 +520,7 @@ private JsValue AsyncModuleExecutionFulfilled(JsValue thisObj, JsValue[] argumen /// private JsValue AsyncModuleExecutionRejected(JsValue thisObj, JsValue[] arguments) { - var module = (CyclicModuleRecord) arguments.At(0); + var module = (SourceTextModuleRecord) arguments.At(0); var error = arguments.At(1); if (module.Status == ModuleStatus.Evaluated) @@ -918,18 +560,41 @@ private JsValue AsyncModuleExecutionRejected(JsValue thisObj, JsValue[] argument module._topLevelCapability.Reject.Call(Undefined, new[] { error }); } - return Undefined; } - public override object ToObject() + /// + /// https://tc39.es/ecma262/#sec-gather-available-ancestors + /// + private void GatherAvailableAncestors(List execList) { - ExceptionHelper.ThrowNotSupportedException(); - return null; - } + foreach (var m in _asyncParentModules) + { + if (!execList.Contains(m) && m._cycleRoot._evalError is null) + { + if (m.Status != ModuleStatus.EvaluatingAsync || + m._evalError is not null || + !m._asyncEvaluation || + m._pendingAsyncDependencies <= 0) + { + ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); + } - public override string ToString() - { - return $"{Type}: {Location}"; + if (--m._pendingAsyncDependencies == 0) + { + execList.Add(m); + if (!m._hasTLA) + { + m.GatherAvailableAncestors(execList); + } + } + } + } } + + /// + /// https://tc39.es/ecma262/#table-cyclic-module-methods + /// + protected abstract void InitializeEnvironment(); + internal abstract Completion Execute(PromiseCapability capability = null); } \ No newline at end of file diff --git a/Jint/Runtime/Modules/ModuleRecord.cs b/Jint/Runtime/Modules/ModuleRecord.cs index 4b56bb2e6d..a463eb177d 100644 --- a/Jint/Runtime/Modules/ModuleRecord.cs +++ b/Jint/Runtime/Modules/ModuleRecord.cs @@ -69,4 +69,15 @@ private static ObjectInstance CreateModuleNamespace(ModuleRecord module, List +/// https://tc39.es/ecma262/#importentry-record +/// +internal sealed record ImportEntry( + string ModuleRequest, + string ImportName, + string LocalName +); + +/// +/// https://tc39.es/ecma262/#exportentry-record +/// +internal sealed record ExportEntry( + string ExportName, + string ModuleRequest, + string ImportName, + string LocalName +); + +/// +/// https://tc39.es/ecma262/#sec-source-text-module-records +/// +public class SourceTextModuleRecord : CyclicModuleRecord +{ + private readonly Module _source; + private ExecutionContext _context; + private readonly ObjectInstance _importMeta; + private readonly List _importEntries; + internal readonly List _localExportEntries; + private readonly List _indirectExportEntries; + private readonly List _starExportEntries; + + internal SourceTextModuleRecord(Engine engine, Realm realm, Module source, string location, bool async) + : base(engine, realm, source, location, async) + { + _source = source; + + // https://tc39.es/ecma262/#sec-parsemodule + _importMeta = _realm.Intrinsics.Object.Construct(1); + _importMeta.DefineOwnProperty("url", new PropertyDescriptor(location, PropertyFlag.ConfigurableEnumerableWritable)); + + HoistingScope.GetImportsAndExports( + _source, + out _requestedModules, + out _importEntries, + out _localExportEntries, + out _indirectExportEntries, + out _starExportEntries); + + //ToDo async modules + } + /// + /// https://tc39.es/ecma262/#sec-getexportednames + /// + public override List GetExportedNames(List exportStarSet = null) + { + exportStarSet ??= new List(); + if (exportStarSet.Contains(this)) + { + //Reached the starting point of an export * circularity + return new List(); + } + + exportStarSet.Add(this); + var exportedNames = new List(); + for (var i = 0; i < _localExportEntries.Count; i++) + { + var e = _localExportEntries[i]; + exportedNames.Add(e.ImportName ?? e.ExportName); + } + + for (var i = 0; i < _indirectExportEntries.Count; i++) + { + var e = _indirectExportEntries[i]; + exportedNames.Add(e.ImportName ?? e.ExportName); + } + + for (var i = 0; i < _starExportEntries.Count; i++) + { + var e = _starExportEntries[i]; + var requestedModule = _engine._host.ResolveImportedModule(this, e.ModuleRequest); + var starNames = requestedModule.GetExportedNames(exportStarSet); + + for (var j = 0; j < starNames.Count; j++) + { + var n = starNames[j]; + if (!"default".Equals(n) && !exportedNames.Contains(n)) + { + exportedNames.Add(n); + } + } + } + + return exportedNames; + } + + /// + /// https://tc39.es/ecma262/#sec-resolveexport + /// + internal override ResolvedBinding ResolveExport(string exportName, List resolveSet = null) + { + resolveSet ??= new List(); + + for (var i = 0; i < resolveSet.Count; i++) + { + var r = resolveSet[i]; + if (ReferenceEquals(this, r.Module) && exportName == r.ExportName) + { + // circular import request + return null; + } + } + + resolveSet.Add(new ExportResolveSetItem(this, exportName)); + for (var i = 0; i < _localExportEntries.Count; i++) + { + var e = _localExportEntries[i]; + if (exportName == (e.ImportName ?? e.ExportName)) + { + return new ResolvedBinding(this, e.LocalName ?? e.ExportName); + } + } + + for (var i = 0; i < _indirectExportEntries.Count; i++) + { + var e = _indirectExportEntries[i]; + if (exportName == (e.ImportName ?? e.ExportName)) + { + var importedModule = _engine._host.ResolveImportedModule(this, e.ModuleRequest); + if (e.ImportName == "*") + { + return new ResolvedBinding(importedModule, "*namespace*"); + } + else + { + return importedModule.ResolveExport(e.ExportName, resolveSet); + } + } + } + + if ("default".Equals(exportName)) + { + return null; + } + + ResolvedBinding starResolution = null; + + for (var i = 0; i < _starExportEntries.Count; i++) + { + var e = _starExportEntries[i]; + var importedModule = _engine._host.ResolveImportedModule(this, e.ModuleRequest); + var resolution = importedModule.ResolveExport(exportName, resolveSet); + if (resolution == ResolvedBinding.Ambiguous) + { + return resolution; + } + + if (resolution is not null) + { + if (starResolution is null) + { + starResolution = resolution; + } + else + { + if (resolution.Module != starResolution.Module || resolution.BindingName != starResolution.BindingName) + { + return ResolvedBinding.Ambiguous; + } + } + } + } + + return starResolution; + } + + /// + /// https://tc39.es/ecma262/#sec-source-text-module-record-initialize-environment + /// + protected override void InitializeEnvironment() + { + for (var i = 0; i < _indirectExportEntries.Count; i++) + { + var e = _indirectExportEntries[i]; + var resolution = ResolveExport(e.ImportName ?? e.ExportName); + if (resolution is null || resolution == ResolvedBinding.Ambiguous) + { + ExceptionHelper.ThrowSyntaxError(_realm, "Ambiguous import statement for identifier: " + e.ExportName); + } + } + + var realm = _realm; + var env = JintEnvironment.NewModuleEnvironment(_engine, realm.GlobalEnv); + _environment = env; + + if (_importEntries != null) + { + for (var i = 0; i < _importEntries.Count; i++) + { + var ie = _importEntries[i]; + var importedModule = _engine._host.ResolveImportedModule(this, ie.ModuleRequest); + if (ie.ImportName == "*") + { + var ns = GetModuleNamespace(importedModule); + env.CreateImmutableBinding(ie.LocalName, true); + env.InitializeBinding(ie.LocalName, ns); + } + else + { + var resolution = importedModule.ResolveExport(ie.ImportName); + if (resolution is null || resolution == ResolvedBinding.Ambiguous) + { + ExceptionHelper.ThrowSyntaxError(_realm, "Ambiguous import statement for identifier " + ie.ImportName); + } + + if (resolution.BindingName == "*namespace*") + { + var ns = GetModuleNamespace(resolution.Module); + env.CreateImmutableBinding(ie.LocalName, true); + env.InitializeBinding(ie.LocalName, ns); + } + else + { + env.CreateImportBinding(ie.LocalName, resolution.Module, resolution.BindingName); + } + } + } + } + + var moduleContext = new ExecutionContext(this, _environment, _environment, null, realm, null); + _context = moduleContext; + + _engine.EnterExecutionContext(_context); + + var hoistingScope = HoistingScope.GetModuleLevelDeclarations(_source); + + var varDeclarations = hoistingScope._variablesDeclarations; + var declaredVarNames = new HashSet(); + if (varDeclarations != null) + { + var boundNames = new List(); + for (var i = 0; i < varDeclarations.Count; i++) + { + var d = varDeclarations[i]; + boundNames.Clear(); + d.GetBoundNames(boundNames); + for (var j = 0; j < boundNames.Count; j++) + { + var dn = boundNames[j]; + if (declaredVarNames.Add(dn)) + { + env.CreateMutableBinding(dn, false); + env.InitializeBinding(dn, Undefined); + } + } + } + } + + var lexDeclarations = hoistingScope._lexicalDeclarations; + + if (lexDeclarations != null) + { + var boundNames = new List(); + for (var i = 0; i < lexDeclarations.Count; i++) + { + var d = lexDeclarations[i]; + boundNames.Clear(); + d.GetBoundNames(boundNames); + for (var j = 0; j < boundNames.Count; j++) + { + var dn = boundNames[j]; + if (d.IsConstantDeclaration()) + { + env.CreateImmutableBinding(dn, true); + } + else + { + env.CreateMutableBinding(dn, false); + } + } + } + } + + var functionDeclarations = hoistingScope._functionDeclarations; + + if (functionDeclarations != null) + { + for (var i = 0; i < functionDeclarations.Count; i++) + { + var d = functionDeclarations[i]; + var fn = d.Id?.Name ?? "*default*"; + var fd = new JintFunctionDefinition(_engine, d); + env.CreateMutableBinding(fn, true); + var fo = realm.Intrinsics.Function.InstantiateFunctionObject(fd, env); + env.InitializeBinding(fn, fo); + } + } + + _engine.LeaveExecutionContext(); + } + + /// + /// https://tc39.es/ecma262/#sec-source-text-module-record-execute-module + /// + internal override Completion Execute(PromiseCapability capability = null) + { + var moduleContext = new ExecutionContext(this, _environment, _environment, null, _realm); + if (!_hasTLA) + { + using (new StrictModeScope(true, force: true)) + { + _engine.EnterExecutionContext(moduleContext); + var statementList = new JintStatementList(null, _source.Body); + var result = statementList.Execute(_engine._activeEvaluationContext ?? new EvaluationContext(_engine)); //Create new evaluation context when called from e.g. module tests + _engine.LeaveExecutionContext(); + return result; + } + } + else + { + //ToDo async modules + return default; + } + } +} \ No newline at end of file From adc222cc1b44c91cb6b3294f84a68f67d77ee310 Mon Sep 17 00:00:00 2001 From: Christian Rondeau Date: Mon, 7 Mar 2022 13:56:23 -0500 Subject: [PATCH 17/31] Support recursive cyclic modules --- Jint.Tests/Runtime/ModuleTests.cs | 17 ++- Jint/Engine.Modules.cs | 3 +- Jint/Runtime/Modules/CyclicModuleRecord.cs | 123 ++++++++---------- Jint/Runtime/Modules/ModuleRecord.cs | 3 + .../Runtime/Modules/SourceTextModuleRecord.cs | 7 +- 5 files changed, 67 insertions(+), 86 deletions(-) diff --git a/Jint.Tests/Runtime/ModuleTests.cs b/Jint.Tests/Runtime/ModuleTests.cs index 6d27d8d49e..8f63bb397a 100644 --- a/Jint.Tests/Runtime/ModuleTests.cs +++ b/Jint.Tests/Runtime/ModuleTests.cs @@ -1,10 +1,8 @@ #if(NET6_0_OR_GREATER) using System.IO; using System.Reflection; -using Jint.Runtime.Modules; #endif using System; -using Esprima; using Jint.Native; using Jint.Runtime; using Xunit; @@ -137,7 +135,6 @@ public void ShouldPropagateThrowStatementThroughJavaScriptImport() var exc = Assert.Throws(() => _engine.ImportModule("my-module")); Assert.Equal("imported successfully", exc.Message); - Assert.Equal("imported-module", exc.Location.Source); } [Fact] @@ -254,14 +251,16 @@ public void ShouldAllowSelfImport() [Fact] public void ShouldAllowCyclicImport() { - _engine.AddModule("module2", @"import { x1 } from 'module1'; export const x2 = 2;"); - _engine.AddModule("module1", @"import { x2 } from 'module2'; export const x1 = 1;"); + // https://tc39.es/ecma262/#sec-example-cyclic-module-record-graphs - var ns1 = _engine.ImportModule("module1"); - var ns2 = _engine.ImportModule("module2"); + _engine.AddModule("B", @"import { a } from 'A'; export const b = 'b';"); + _engine.AddModule("A", @"import { b } from 'B'; export const a = 'a';"); - Assert.Equal(1, ns1.Get("x1").AsInteger()); - Assert.Equal(2, ns2.Get("x2").AsInteger()); + var nsA = _engine.ImportModule("A"); + var nsB = _engine.ImportModule("B"); + + Assert.Equal("a", nsA.Get("a").AsString()); + Assert.Equal("b", nsB.Get("b").AsString()); } #if(NET6_0_OR_GREATER) diff --git a/Jint/Engine.Modules.cs b/Jint/Engine.Modules.cs index 7e4808bac2..932a8bac81 100644 --- a/Jint/Engine.Modules.cs +++ b/Jint/Engine.Modules.cs @@ -151,8 +151,7 @@ private void EvaluateModule(string specifier, ModuleRecord cyclicModule) } else if (promise.State == PromiseState.Rejected) { - ExceptionHelper.ThrowJavaScriptException(this, promise.Value, - new Completion(CompletionType.Throw, promise.Value, null, new Location(new Position(), new Position(), specifier))); + ExceptionHelper.ThrowJavaScriptException(this, promise.Value, new Completion(CompletionType.Throw, promise.Value, null, new Location(new Position(), new Position(), specifier))); } else if (promise.State != PromiseState.Fulfilled) { diff --git a/Jint/Runtime/Modules/CyclicModuleRecord.cs b/Jint/Runtime/Modules/CyclicModuleRecord.cs index 39e7c72ff2..7f9a68f7f3 100644 --- a/Jint/Runtime/Modules/CyclicModuleRecord.cs +++ b/Jint/Runtime/Modules/CyclicModuleRecord.cs @@ -1,7 +1,6 @@ using System; using Esprima.Ast; using System.Collections.Generic; -using Esprima; using Jint.Native; using Jint.Native.Promise; using Jint.Runtime.Descriptors; @@ -46,7 +45,7 @@ internal CyclicModuleRecord(Engine engine, Realm realm, Module source, string lo /// public override void Link() { - if (Status == ModuleStatus.Linking || Status == ModuleStatus.Evaluating) + if (Status is ModuleStatus.Linking or ModuleStatus.Evaluating) { ExceptionHelper.ThrowInvalidOperationException("Error while linking module: Module is already either linking or evaluating"); } @@ -61,19 +60,27 @@ public override void Link() { foreach (var m in stack) { - m.Status = ModuleStatus.Unlinked; m._environment = null; + + if (m.Status != ModuleStatus.Linking) + { + ExceptionHelper.ThrowInvalidOperationException("Error while linking module: Module should be linking after abrupt completion"); + } + m.Status = ModuleStatus.Unlinked; m._dfsIndex = -1; m._dfsAncestorIndex = -1; } - Status = ModuleStatus.Unlinked; + if (Status != ModuleStatus.Unlinked) + { + ExceptionHelper.ThrowInvalidOperationException("Error while processing abrupt completion of module link: Module should be unlinked after cleanup"); + } throw; } - if (Status != ModuleStatus.Linked && Status != ModuleStatus.Unlinked) + if (Status is not (ModuleStatus.Linked or ModuleStatus.EvaluatingAsync or ModuleStatus.Evaluated)) { - ExceptionHelper.ThrowInvalidOperationException("Error while linking module: Module is neither linked or unlinked"); + ExceptionHelper.ThrowInvalidOperationException("Error while linking module: Module is neither linked, evaluating-async or evaluated"); } if (stack.Count > 0) @@ -157,20 +164,20 @@ public override JsValue Evaluate() /// /// https://tc39.es/ecma262/#sec-InnerModuleLinking /// - private int InnerModuleLinking(Stack stack, int index) + protected internal override int InnerModuleLinking(Stack stack, int index) { if (Status is ModuleStatus.Linking or ModuleStatus.Linked or ModuleStatus.EvaluatingAsync or - ModuleStatus.Evaluating) + ModuleStatus.Evaluated) { return index; } if (Status != ModuleStatus.Unlinked) { - ExceptionHelper.ThrowInvalidOperationException("Error while linking module: Module in an invalid state"); + ExceptionHelper.ThrowInvalidOperationException($"Error while linking module: Module in an invalid state: {Status}"); } Status = ModuleStatus.Linking; @@ -179,31 +186,27 @@ ModuleStatus.EvaluatingAsync or index++; stack.Push(this); - var requestedModules = _requestedModules; - - foreach (var moduleSpecifier in requestedModules) + foreach (var required in _requestedModules) { - var requiredModule = _engine._host.ResolveImportedModule(this, moduleSpecifier); + var requiredModule = _engine._host.ResolveImportedModule(this, required); + + index = requiredModule.InnerModuleLinking(stack, index); if (requiredModule is not CyclicModuleRecord requiredCyclicModule) { continue; } - //TODO: Should we link only when a module is requested? https://tc39.es/ecma262/#sec-example-cyclic-module-record-graphs Should we support retry? - if (requiredCyclicModule.Status == ModuleStatus.Unlinked) - { - index = requiredCyclicModule.InnerModuleLinking(stack, index); - } - - if (requiredCyclicModule.Status != ModuleStatus.Linking && - requiredCyclicModule.Status != ModuleStatus.Linked && - requiredCyclicModule.Status != ModuleStatus.Evaluated) + if (requiredCyclicModule.Status is not ( + ModuleStatus.Linking or + ModuleStatus.Linked or + ModuleStatus.EvaluatingAsync or + ModuleStatus.Evaluated)) { ExceptionHelper.ThrowInvalidOperationException($"Error while linking module: Required module is in an invalid state: {requiredCyclicModule.Status}"); } - if (requiredCyclicModule.Status == ModuleStatus.Linking && !stack.Contains(requiredCyclicModule)) + if ((requiredCyclicModule.Status == ModuleStatus.Linking) == !stack.Contains(requiredCyclicModule)) { ExceptionHelper.ThrowInvalidOperationException($"Error while linking module: Required module is in an invalid state: {requiredCyclicModule.Status}"); } @@ -221,7 +224,7 @@ ModuleStatus.EvaluatingAsync or ExceptionHelper.ThrowInvalidOperationException("Error while linking module: Recursive dependency detected"); } - if (_dfsIndex > _dfsAncestorIndex) + if (_dfsAncestorIndex > _dfsIndex) { ExceptionHelper.ThrowInvalidOperationException("Error while linking module: Recursive dependency detected"); } @@ -245,7 +248,7 @@ ModuleStatus.EvaluatingAsync or /// /// https://tc39.es/ecma262/#sec-innermoduleevaluation /// - private Completion InnerModuleEvaluation(Stack stack, int index, ref int asyncEvalOrder) + protected internal override Completion InnerModuleEvaluation(Stack stack, int index, ref int asyncEvalOrder) { if (Status is ModuleStatus.EvaluatingAsync or ModuleStatus.Evaluated) { @@ -264,10 +267,9 @@ private Completion InnerModuleEvaluation(Stack stack, int in if (Status != ModuleStatus.Linked) { - ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); + ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module is in an invalid state: {Status}"); } - Status = ModuleStatus.Evaluating; _dfsIndex = index; _dfsAncestorIndex = index; @@ -275,45 +277,21 @@ private Completion InnerModuleEvaluation(Stack stack, int in index++; stack.Push(this); - foreach (var moduleSpecifier in _requestedModules) + foreach (var required in _requestedModules) { - var requiredModule = _engine._host.ResolveImportedModule(this, moduleSpecifier); + var requiredModule = _engine._host.ResolveImportedModule(this, required); - if (requiredModule is not CyclicModuleRecord requiredCyclicModule) - { - ExceptionHelper.ThrowNotImplementedException($"Resolving modules of type {requiredModule.GetType()} is not implemented"); - continue; - } - - var result = requiredCyclicModule.InnerModuleEvaluation(stack, index, ref asyncEvalOrder); + var result = requiredModule.InnerModuleEvaluation(stack, index, ref asyncEvalOrder); if (result.Type != CompletionType.Normal) { return result; } - index = TypeConverter.ToInt32(result.Value); - // TODO: Validate this behavior: https://tc39.es/ecma262/#sec-example-cyclic-module-record-graphs - if (requiredCyclicModule.Status == ModuleStatus.Linked) + if (requiredModule is not CyclicModuleRecord requiredCyclicModule) { - var evaluationResult = requiredCyclicModule.Evaluate(); - if (evaluationResult == null) - { - ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module evaluation did not return a promise"); - } - else if (evaluationResult is not PromiseInstance promise) - { - ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module evaluation did not return a promise: {evaluationResult.Type}"); - } - else if (promise.State == PromiseState.Rejected) - { - ExceptionHelper.ThrowJavaScriptException(_engine, promise.Value, - new Completion(CompletionType.Throw, promise.Value, null, new Location(new Position(), new Position(), moduleSpecifier))); - } - else if (promise.State != PromiseState.Fulfilled) - { - ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module evaluation did not return a fulfilled promise: {promise.State}"); - } + ExceptionHelper.ThrowNotImplementedException($"Resolving modules of type {requiredModule.GetType()} is not implemented"); + continue; } if (requiredCyclicModule.Status != ModuleStatus.Evaluating && @@ -335,10 +313,15 @@ private Completion InnerModuleEvaluation(Stack stack, int in else { requiredCyclicModule = requiredCyclicModule._cycleRoot; - if (requiredCyclicModule.Status != ModuleStatus.EvaluatingAsync && requiredCyclicModule.Status != ModuleStatus.Evaluated) + if (requiredCyclicModule.Status is not (ModuleStatus.EvaluatingAsync or ModuleStatus.Evaluated)) { ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); } + + if (requiredCyclicModule._evalError != null) + { + return requiredCyclicModule._evalError.Value; + } } if (requiredCyclicModule._asyncEvaluation) @@ -354,33 +337,34 @@ private Completion InnerModuleEvaluation(Stack stack, int in { if (_asyncEvaluation) { - ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); + ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state (async evaluation is true)"); } _asyncEvaluation = true; _asyncEvalOrder = asyncEvalOrder++; if (_pendingAsyncDependencies == 0) { - completion = ExecuteAsync(); + completion = ExecuteAsyncModule(); } else { - completion = Execute(); + // This is not in the specifications, but it's unclear whether 16.2.1.5.2.1.13 "Otherwise" should mean "Else" for 12 or "In other cases".. + completion = ExecuteModule(); } } else { - completion = Execute(); + completion = ExecuteModule(); } if (StackReferenceCount(stack) != 1) { - ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); + ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state (not found exactly once in stack)"); } if (_dfsAncestorIndex > _dfsIndex) { - ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); + ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state (mismatch DFS ancestor index)"); } if (_dfsIndex == _dfsAncestorIndex) @@ -397,7 +381,6 @@ private Completion InnerModuleEvaluation(Stack stack, int in { requiredModule.Status = ModuleStatus.EvaluatingAsync; } - done = requiredModule == this; requiredModule._cycleRoot = this; } @@ -423,7 +406,7 @@ private int StackReferenceCount(Stack stack) /// /// https://tc39.es/ecma262/#sec-execute-async-module /// - private Completion ExecuteAsync() + private Completion ExecuteAsyncModule() { if (Status != ModuleStatus.Evaluating && Status != ModuleStatus.EvaluatingAsync || !_hasTLA) { @@ -437,7 +420,7 @@ private Completion ExecuteAsync() PromiseOperations.PerformPromiseThen(_engine, (PromiseInstance) capability.PromiseInstance, onFullfilled, onRejected, null); - return Execute(capability); + return ExecuteModule(capability); } @@ -487,11 +470,11 @@ private JsValue AsyncModuleExecutionFulfilled(JsValue thisObj, JsValue[] argumen } else if (m._hasTLA) { - m.ExecuteAsync(); + m.ExecuteAsyncModule(); } else { - var result = m.Execute(); + var result = m.ExecuteModule(); if (result.Type != CompletionType.Normal) { AsyncModuleExecutionRejected(Undefined, new[] { m, result.Value }); @@ -596,5 +579,5 @@ m._evalError is not null || /// https://tc39.es/ecma262/#table-cyclic-module-methods /// protected abstract void InitializeEnvironment(); - internal abstract Completion Execute(PromiseCapability capability = null); + internal abstract Completion ExecuteModule(PromiseCapability capability = null); } \ No newline at end of file diff --git a/Jint/Runtime/Modules/ModuleRecord.cs b/Jint/Runtime/Modules/ModuleRecord.cs index a463eb177d..2b10c86021 100644 --- a/Jint/Runtime/Modules/ModuleRecord.cs +++ b/Jint/Runtime/Modules/ModuleRecord.cs @@ -34,6 +34,9 @@ internal ModuleRecord(Engine engine, Realm realm, string location) : base(Intern public abstract void Link(); public abstract JsValue Evaluate(); + protected internal abstract int InnerModuleLinking(Stack stack, int index); + protected internal abstract Completion InnerModuleEvaluation(Stack stack, int index, ref int asyncEvalOrder); + /// /// https://tc39.es/ecma262/#sec-getmodulenamespace /// diff --git a/Jint/Runtime/Modules/SourceTextModuleRecord.cs b/Jint/Runtime/Modules/SourceTextModuleRecord.cs index d131e0f43d..bc0a16263c 100644 --- a/Jint/Runtime/Modules/SourceTextModuleRecord.cs +++ b/Jint/Runtime/Modules/SourceTextModuleRecord.cs @@ -1,12 +1,9 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using Esprima.Ast; -using Jint.Native; using Jint.Native.Object; using Jint.Native.Promise; using Jint.Runtime.Descriptors; using Jint.Runtime.Environments; -using Jint.Runtime.Interop; using Jint.Runtime.Interpreter; namespace Jint.Runtime.Modules; @@ -315,7 +312,7 @@ protected override void InitializeEnvironment() /// /// https://tc39.es/ecma262/#sec-source-text-module-record-execute-module /// - internal override Completion Execute(PromiseCapability capability = null) + internal override Completion ExecuteModule(PromiseCapability capability = null) { var moduleContext = new ExecutionContext(this, _environment, _environment, null, _realm); if (!_hasTLA) From 967188f5f6ac502c444a84910a74bf6e062b0fb7 Mon Sep 17 00:00:00 2001 From: Christian Rondeau Date: Mon, 7 Mar 2022 14:50:43 -0500 Subject: [PATCH 18/31] Fix inversed renamed export implementation --- Jint/EsprimaExtensions.cs | 4 ++-- Jint/Runtime/Modules/SourceTextModuleRecord.cs | 17 +++++++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Jint/EsprimaExtensions.cs b/Jint/EsprimaExtensions.cs index d199abc90e..a4a27b9e31 100644 --- a/Jint/EsprimaExtensions.cs +++ b/Jint/EsprimaExtensions.cs @@ -317,7 +317,7 @@ internal static void GetExportEntries(this ExportDeclaration export, List + + /// /// https://tc39.es/ecma262/#sec-getexportednames /// public override List GetExportedNames(List exportStarSet = null) @@ -76,13 +77,13 @@ public override List GetExportedNames(List exportSta for (var i = 0; i < _localExportEntries.Count; i++) { var e = _localExportEntries[i]; - exportedNames.Add(e.ImportName ?? e.ExportName); + exportedNames.Add(e.ExportName); } for (var i = 0; i < _indirectExportEntries.Count; i++) { var e = _indirectExportEntries[i]; - exportedNames.Add(e.ImportName ?? e.ExportName); + exportedNames.Add(e.ExportName); } for (var i = 0; i < _starExportEntries.Count; i++) @@ -125,8 +126,9 @@ internal override ResolvedBinding ResolveExport(string exportName, List Date: Mon, 7 Mar 2022 15:09:16 -0500 Subject: [PATCH 19/31] Fix incorrect use of local vs import name in export entries --- Jint.Tests/Runtime/ModuleTests.cs | 2 -- Jint/EsprimaExtensions.cs | 2 +- Jint/Runtime/Modules/BuilderModuleRecord.cs | 2 +- Jint/Runtime/Modules/SourceTextModuleRecord.cs | 4 ++-- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Jint.Tests/Runtime/ModuleTests.cs b/Jint.Tests/Runtime/ModuleTests.cs index 8f63bb397a..ce23b18600 100644 --- a/Jint.Tests/Runtime/ModuleTests.cs +++ b/Jint.Tests/Runtime/ModuleTests.cs @@ -194,7 +194,6 @@ public void ShouldAllowExportMultipleImports() Assert.Equal("1 2", ns.Get("result").AsString()); } - /* ECMAScript 2020 "export * as ns from" [Fact] public void ShouldAllowNamedStarExport() { @@ -204,7 +203,6 @@ public void ShouldAllowNamedStarExport() Assert.Equal(5, ns.Get("ns").Get("value1").AsNumber()); } - */ [Fact] public void ShouldAllowChaining() diff --git a/Jint/EsprimaExtensions.cs b/Jint/EsprimaExtensions.cs index a4a27b9e31..5863cab5fd 100644 --- a/Jint/EsprimaExtensions.cs +++ b/Jint/EsprimaExtensions.cs @@ -330,7 +330,7 @@ internal static void GetExportEntries(this ExportDeclaration export, List Date: Mon, 7 Mar 2022 19:11:08 -0500 Subject: [PATCH 20/31] Fix two star imports conflicting not considered ambiguous --- Jint/Runtime/Modules/ModuleRecord.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jint/Runtime/Modules/ModuleRecord.cs b/Jint/Runtime/Modules/ModuleRecord.cs index 2b10c86021..aa83ac74df 100644 --- a/Jint/Runtime/Modules/ModuleRecord.cs +++ b/Jint/Runtime/Modules/ModuleRecord.cs @@ -51,7 +51,7 @@ public static ObjectInstance GetModuleNamespace(ModuleRecord module) { var name = exportedNames[i]; var resolution = module.ResolveExport(name); - if (resolution is not null) + if (resolution is not null && resolution != ResolvedBinding.Ambiguous) { unambiguousNames.Add(name); } From 3414313031bc6e5b0875bf254d51ac117f920f5b Mon Sep 17 00:00:00 2001 From: Christian Rondeau Date: Tue, 8 Mar 2022 09:48:39 -0500 Subject: [PATCH 21/31] Import bindings should be initialized as immutable undefined --- Jint/Runtime/Environments/ModuleEnvironmentRecord.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Jint/Runtime/Environments/ModuleEnvironmentRecord.cs b/Jint/Runtime/Environments/ModuleEnvironmentRecord.cs index 9ae6f78441..3864ddc23a 100644 --- a/Jint/Runtime/Environments/ModuleEnvironmentRecord.cs +++ b/Jint/Runtime/Environments/ModuleEnvironmentRecord.cs @@ -31,6 +31,7 @@ public void CreateImportBinding(string importName, ModuleRecord module, string n { _hasBindings = true; _importBindings[importName] = new IndirectBinding(module, name); + CreateImmutableBindingAndInitialize(importName, true, JsValue.Undefined); } /// From 4863368f5cbbdc747781f6c7e1f7d60362b75734 Mon Sep 17 00:00:00 2001 From: Christian Rondeau Date: Tue, 8 Mar 2022 11:48:04 -0500 Subject: [PATCH 22/31] Export default function assign function name "default" --- Jint/Runtime/Modules/SourceTextModuleRecord.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Jint/Runtime/Modules/SourceTextModuleRecord.cs b/Jint/Runtime/Modules/SourceTextModuleRecord.cs index a49be169ef..358163cb08 100644 --- a/Jint/Runtime/Modules/SourceTextModuleRecord.cs +++ b/Jint/Runtime/Modules/SourceTextModuleRecord.cs @@ -307,6 +307,7 @@ protected override void InitializeEnvironment() var fd = new JintFunctionDefinition(_engine, d); env.CreateMutableBinding(fn, true); var fo = realm.Intrinsics.Function.InstantiateFunctionObject(fd, env); + if (fn == "*default*") fo.SetFunctionName("default"); env.InitializeBinding(fn, fo); } } From d99f285c8a391e2223be3c1c60bff6e80ec8aa6c Mon Sep 17 00:00:00 2001 From: Christian Rondeau Date: Tue, 8 Mar 2022 12:01:19 -0500 Subject: [PATCH 23/31] Allow coercion of primitives to objects during assignments in strict mode --- .../Interpreter/Expressions/JintAssignmentExpression.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs index 305325f465..111fa206ae 100644 --- a/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs +++ b/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs @@ -2,6 +2,7 @@ using Esprima.Ast; using Jint.Native; using Jint.Native.Function; +using Jint.Native.Object; using Jint.Runtime.Environments; using Jint.Runtime.References; @@ -385,9 +386,11 @@ private ExpressionResult SetValue(EvaluationContext context) if (lref._strict && lrefBase is EnvironmentRecord && (lrefReferenceName == "eval" || lrefReferenceName == "arguments")) ExceptionHelper.ThrowSyntaxError(engine.Realm); - var lrefObject = lrefBase.AsObject(); - if (!lrefObject.Extensible && !lrefObject.HasOwnProperty(lrefReferenceName)) - ExceptionHelper.ThrowTypeError(engine.Realm, $"Cannot add property {lrefReferenceName}, object is not extensible"); + if (lrefBase is ObjectInstance lrefObject) + { + if (!lrefObject.Extensible && !lrefObject.HasOwnProperty(lrefReferenceName)) + ExceptionHelper.ThrowTypeError(engine.Realm, $"Cannot add property {lrefReferenceName}, object is not extensible"); + } } engine.PutValue(lref, rval); From 2089eea62d381c6b640ca68763fbf4210c4a93f9 Mon Sep 17 00:00:00 2001 From: Christian Rondeau Date: Tue, 8 Mar 2022 12:16:57 -0500 Subject: [PATCH 24/31] Remove useless strict assignement property check (fixed elsewhere) --- .../Expressions/JintAssignmentExpression.cs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs index 111fa206ae..a03c4b64ac 100644 --- a/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs +++ b/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs @@ -378,21 +378,6 @@ private ExpressionResult SetValue(EvaluationContext context) var rval = _right.GetValue(context).GetValueOrDefault(); - if (StrictModeScope.IsStrictModeCode && lref.IsPropertyReference()) - { - var lrefReferenceName = lref.GetReferencedName(); - var lrefBase = lref.GetBase(); - - if (lref._strict && lrefBase is EnvironmentRecord && (lrefReferenceName == "eval" || lrefReferenceName == "arguments")) - ExceptionHelper.ThrowSyntaxError(engine.Realm); - - if (lrefBase is ObjectInstance lrefObject) - { - if (!lrefObject.Extensible && !lrefObject.HasOwnProperty(lrefReferenceName)) - ExceptionHelper.ThrowTypeError(engine.Realm, $"Cannot add property {lrefReferenceName}, object is not extensible"); - } - } - engine.PutValue(lref, rval); engine._referencePool.Return(lref); return NormalCompletion(rval); From 0d404ea912f034ffe57ac47edfe46cfd2cf1fd1e Mon Sep 17 00:00:00 2001 From: Christian Rondeau Date: Tue, 8 Mar 2022 12:17:15 -0500 Subject: [PATCH 25/31] Skip generator tests in module tests (not implemented) --- Jint.Tests.Test262/test/skipped.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Jint.Tests.Test262/test/skipped.json b/Jint.Tests.Test262/test/skipped.json index db2e61267d..9d5bd55499 100644 --- a/Jint.Tests.Test262/test/skipped.json +++ b/Jint.Tests.Test262/test/skipped.json @@ -310,6 +310,14 @@ "source": "language/statements/class/subclass/builtin-objects/GeneratorFunction/regular-subclassing.js", "reason": "generators not implemented" }, + { + "source": "language/language/module-code/instn-local-bndng-export-gen.js", + "reason": "generators not implemented" + }, + { + "source": "language/language/module-code/instn-local-bndng-gen.js", + "reason": "generators not implemented" + }, // Eval problems From 0bce4ae2c86bf04acc91ea78bcebc0741e2a21f4 Mon Sep 17 00:00:00 2001 From: Christian Rondeau Date: Tue, 8 Mar 2022 22:52:09 -0500 Subject: [PATCH 26/31] Fix named exports incorrectly bound by the declaration --- Jint/EsprimaExtensions.cs | 9 ++++- .../Statements/JintExportNamedDeclaration.cs | 35 ------------------- 2 files changed, 8 insertions(+), 36 deletions(-) diff --git a/Jint/EsprimaExtensions.cs b/Jint/EsprimaExtensions.cs index 5863cab5fd..8ee0ae523c 100644 --- a/Jint/EsprimaExtensions.cs +++ b/Jint/EsprimaExtensions.cs @@ -330,7 +330,14 @@ internal static void GetExportEntries(this ExportDeclaration export, List 0) - { - _specifiers = new ExportedSpecifier[_statement.Specifiers.Count]; - ref readonly var statementSpecifiers = ref _statement.Specifiers; - for (var i = 0; i < statementSpecifiers.Count; i++) - { - var statementSpecifier = statementSpecifiers[i]; - - _specifiers[i] = new ExportedSpecifier( - Local: statementSpecifier.Local.GetKey(context.Engine).AsString(), - Exported: statementSpecifier.Exported.GetKey(context.Engine).AsString() - ); - } - } } /// @@ -58,22 +39,6 @@ protected override void Initialize(EvaluationContext context) /// protected override Completion ExecuteInternal(EvaluationContext context) { - var env = (ModuleEnvironmentRecord) context.Engine.ExecutionContext.LexicalEnvironment; - - if (_specifiers != null) - { - var module = context.Engine.GetActiveScriptOrModule().AsModule(context.Engine, context.LastSyntaxNode.Location); - foreach (var specifier in _specifiers) - { - var localKey = specifier.Local; - var exportedKey = specifier.Exported; - if (localKey != exportedKey) - { - env.CreateImportBinding(exportedKey, module, localKey); - } - } - } - if (_declarationStatement != null) { _declarationStatement.Execute(context); From 40136039c1230e135248c9adb30ebbb9f52ae91a Mon Sep 17 00:00:00 2001 From: Christian Rondeau Date: Tue, 8 Mar 2022 22:52:23 -0500 Subject: [PATCH 27/31] Fix skipped module-code gen tests --- Jint.Tests.Test262/test/skipped.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jint.Tests.Test262/test/skipped.json b/Jint.Tests.Test262/test/skipped.json index 9d5bd55499..9cf55ee950 100644 --- a/Jint.Tests.Test262/test/skipped.json +++ b/Jint.Tests.Test262/test/skipped.json @@ -311,11 +311,11 @@ "reason": "generators not implemented" }, { - "source": "language/language/module-code/instn-local-bndng-export-gen.js", + "source": "language/module-code/instn-local-bndng-export-gen.js", "reason": "generators not implemented" }, { - "source": "language/language/module-code/instn-local-bndng-gen.js", + "source": "language/module-code/instn-local-bndng-gen.js", "reason": "generators not implemented" }, From 5520bae5f1872fcb053b82ef6463102f14631093 Mon Sep 17 00:00:00 2001 From: Christian Rondeau Date: Tue, 8 Mar 2022 23:52:27 -0500 Subject: [PATCH 28/31] Re-enable skipped module tests and add builder function overloads --- Jint.Tests/Runtime/ModuleTests.cs | 30 ++++++++++++++++++++++++++++-- Jint/ModuleBuilder.cs | 28 +++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/Jint.Tests/Runtime/ModuleTests.cs b/Jint.Tests/Runtime/ModuleTests.cs index ce23b18600..8208ceb215 100644 --- a/Jint.Tests/Runtime/ModuleTests.cs +++ b/Jint.Tests/Runtime/ModuleTests.cs @@ -177,6 +177,33 @@ public void ShouldAddModuleFromClrType() Assert.Equal("hello world", ns.Get("exported").AsString()); } + [Fact] + public void ShouldAddModuleFromClrFunction() + { + var received = new List(); + _engine.AddModule("imported-module", builder => builder + .ExportFunction("act_noargs", () => received.Add("act_noargs")) + .ExportFunction("act_args", args => received.Add($"act_args:{args[0].AsString()}")) + .ExportFunction("fn_noargs", () => + { + received.Add("fn_noargs"); + return "ret"; + }) + .ExportFunction("fn_args", args => + { + received.Add($"fn_args:{args[0].AsString()}"); + return "ret"; + }) + ); + _engine.AddModule("my-module", @" +import * as fns from 'imported-module'; +export const result = [fns.act_noargs(), fns.act_args('ok'), fns.fn_noargs(), fns.fn_args('ok')];"); + var ns = _engine.ImportModule("my-module"); + + Assert.Equal(new[] { "act_noargs", "act_args:ok", "fn_noargs", "fn_args:ok" }, received.ToArray()); + Assert.Equal(new[] { "undefined", "undefined", "ret", "ret" }, ns.Get("result").AsArray().Select(x => x.ToString()).ToArray()); + } + private class ImportedClass { public string Value { get; set; } = "hello world"; @@ -219,7 +246,7 @@ public void ShouldAllowChaining() Assert.Equal(-1, ns.Get("num").AsInteger()); } - [Fact(Skip = "TODO re-enable in module fix branch")] + [Fact] public void ShouldImportOnlyOnce() { var called = 0; @@ -263,7 +290,6 @@ public void ShouldAllowCyclicImport() #if(NET6_0_OR_GREATER) - [Fact(Skip = "TODO re-enable in module fix branch")] public void CanLoadModuleImportsFromFiles() { var engine = new Engine(options => options.EnableModules(GetBasePath())); diff --git a/Jint/ModuleBuilder.cs b/Jint/ModuleBuilder.cs index b36a9a57fe..b2db108bf8 100644 --- a/Jint/ModuleBuilder.cs +++ b/Jint/ModuleBuilder.cs @@ -70,7 +70,33 @@ public ModuleBuilder ExportType(string name, Type type) public ModuleBuilder ExportFunction(string name, Func fn) { - _exports.Add(name, new ClrFunctionInstance(_engine, name, (@this, args) => fn(args))); + _exports.Add(name, new ClrFunctionInstance(_engine, name, (_, args) => fn(args))); + return this; + } + + public ModuleBuilder ExportFunction(string name, Func fn) + { + _exports.Add(name, new ClrFunctionInstance(_engine, name, (_, _) => fn())); + return this; + } + + public ModuleBuilder ExportFunction(string name, Action fn) + { + _exports.Add(name, new ClrFunctionInstance(_engine, name, (_, args) => + { + fn(args); + return JsValue.Undefined; + })); + return this; + } + + public ModuleBuilder ExportFunction(string name, Action fn) + { + _exports.Add(name, new ClrFunctionInstance(_engine, name, (_, _) => + { + fn(); + return JsValue.Undefined; + })); return this; } From a27cc0ee039980ab09de49a9747404ee541e1bca Mon Sep 17 00:00:00 2001 From: Christian Rondeau Date: Wed, 9 Mar 2022 00:10:43 -0500 Subject: [PATCH 29/31] Add skipped test for dynamic import --- Jint.Tests/Runtime/ModuleTests.cs | 17 ++++++++++++++- .../Expressions/JintImportExpression.cs | 21 +++++-------------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/Jint.Tests/Runtime/ModuleTests.cs b/Jint.Tests/Runtime/ModuleTests.cs index 8208ceb215..e7f5a21eb3 100644 --- a/Jint.Tests/Runtime/ModuleTests.cs +++ b/Jint.Tests/Runtime/ModuleTests.cs @@ -1,8 +1,10 @@ #if(NET6_0_OR_GREATER) +using System; using System.IO; using System.Reflection; #endif -using System; +using System.Collections.Generic; +using System.Linq; using Jint.Native; using Jint.Runtime; using Xunit; @@ -96,6 +98,19 @@ public void ShouldImportAll() Assert.Equal("exported value", ns.Get("exported").AsString()); } + [Fact(Skip = "Import promise is not resolved")] + public void ShouldImportDynamically() + { + var received = false; + _engine.AddModule("imported-module", builder => builder.ExportFunction("signal", () => received = true)); + _engine.AddModule("my-module", @"import('imported-module').then(ns => { ns.signal(); });"); + + _engine.ImportModule("my-module"); + _engine.RunAvailableContinuations(); + + Assert.True(received); + } + [Fact] public void ShouldPropagateParseError() { diff --git a/Jint/Runtime/Interpreter/Expressions/JintImportExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintImportExpression.cs index 7e5e76c871..80db7fe2f0 100644 --- a/Jint/Runtime/Interpreter/Expressions/JintImportExpression.cs +++ b/Jint/Runtime/Interpreter/Expressions/JintImportExpression.cs @@ -1,8 +1,8 @@ #nullable enable using Esprima.Ast; +using Jint.Native; using Jint.Native.Promise; -using Jint.Runtime.Modules; namespace Jint.Runtime.Interpreter.Expressions; @@ -27,24 +27,13 @@ protected override void Initialize(EvaluationContext context) /// protected override ExpressionResult EvaluateInternal(EvaluationContext context) { - var module = context.Engine.GetActiveScriptOrModule().AsModule(context.Engine, context.LastSyntaxNode.Location); - + var referencingScriptOrModule = context.Engine.GetActiveScriptOrModule(); var argRef = _importExpression.Evaluate(context); - context.Engine.RunAvailableContinuations(); - var value = context.Engine.GetValue(argRef.Value); - var specifier = value.UnwrapIfPromise(); - - if (specifier is ModuleNamespace) - { - // already resolved - return NormalCompletion(value); - } - + var specifier = context.Engine.GetValue(argRef.Value); //.UnwrapIfPromise(); var promiseCapability = PromiseConstructor.NewPromiseCapability(context.Engine, context.Engine.Realm.Intrinsics.Promise); var specifierString = TypeConverter.ToString(specifier); - - // 6.IfAbruptRejectPromise(specifierString, promiseCapability); - context.Engine._host.ImportModuleDynamically(module, specifierString, promiseCapability); + context.Engine._host.ImportModuleDynamically(referencingScriptOrModule, specifierString, promiseCapability); + context.Engine.RunAvailableContinuations(); return NormalCompletion(promiseCapability.PromiseInstance); } } \ No newline at end of file From 256e54d0b177b1aa29aec61071b4fae4e85e8695 Mon Sep 17 00:00:00 2001 From: Christian Rondeau Date: Wed, 9 Mar 2022 00:57:46 -0500 Subject: [PATCH 30/31] dynamic import --- Jint.Tests/Runtime/ModuleTests.cs | 2 +- Jint/Engine.Modules.cs | 8 +++++++- Jint/Runtime/Host.cs | 3 ++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Jint.Tests/Runtime/ModuleTests.cs b/Jint.Tests/Runtime/ModuleTests.cs index e7f5a21eb3..2b3db508fa 100644 --- a/Jint.Tests/Runtime/ModuleTests.cs +++ b/Jint.Tests/Runtime/ModuleTests.cs @@ -98,7 +98,7 @@ public void ShouldImportAll() Assert.Equal("exported value", ns.Get("exported").AsString()); } - [Fact(Skip = "Import promise is not resolved")] + [Fact] public void ShouldImportDynamically() { var received = false; diff --git a/Jint/Engine.Modules.cs b/Jint/Engine.Modules.cs index 932a8bac81..203c2d58e8 100644 --- a/Jint/Engine.Modules.cs +++ b/Jint/Engine.Modules.cs @@ -87,7 +87,12 @@ public void AddModule(string specifier, ModuleBuilder moduleBuilder) public ObjectInstance ImportModule(string specifier) { - var moduleResolution = ModuleLoader.Resolve(referencingModuleLocation: null, specifier); + return ImportModule(specifier, null); + } + + internal ObjectInstance ImportModule(string specifier, string? referencingModuleLocation) + { + var moduleResolution = ModuleLoader.Resolve(referencingModuleLocation, specifier); if (!_modules.TryGetValue(moduleResolution.Key, out var module)) { @@ -145,6 +150,7 @@ private void EvaluateModule(string specifier, ModuleRecord cyclicModule) } } + // This should instead be returned and resolved in ImportModule(specifier) only so Host.ImportModuleDynamically can use this promise if (evaluationResult is not PromiseInstance promise) { ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module evaluation did not return a promise: {evaluationResult.Type}"); diff --git a/Jint/Runtime/Host.cs b/Jint/Runtime/Host.cs index ec15d38ae9..6c3c983565 100644 --- a/Jint/Runtime/Host.cs +++ b/Jint/Runtime/Host.cs @@ -129,7 +129,8 @@ internal virtual void ImportModuleDynamically(IScriptOrModule? referencingModule try { - Engine.LoadModule(referencingModule?.Location, specifier); + // This should instead return the PromiseInstance returned by ModuleRecord.Evaluate (currently done in Engine.EvaluateModule), but until we have await this will do. + Engine.ImportModule(specifier, referencingModule?.Location); promise.Resolve(JsValue.Undefined); } catch (JavaScriptException ex) From 21da8d94e6f6a6dff0c2cf3e9137c103f8fbf27f Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Wed, 9 Mar 2022 08:26:41 +0200 Subject: [PATCH 31/31] remove LINQ from HoistingScope, add missing Fact attribute, small cleanup --- Jint.Tests.Test262/Test262Test.cs | 8 +++++-- Jint.Tests/Runtime/ModuleTests.cs | 1 + Jint/Engine.Modules.cs | 2 +- Jint/EsprimaExtensions.cs | 7 ++++-- Jint/HoistingScope.cs | 23 ++++++++++++------- Jint/ModuleBuilder.cs | 5 +++- Jint/Options.cs | 5 ---- Jint/Runtime/Host.cs | 2 +- Jint/Runtime/IScriptOrModule.cs | 2 +- .../Expressions/JintAssignmentExpression.cs | 3 +-- Jint/Runtime/Modules/BuilderModuleRecord.cs | 14 +++++++---- Jint/Runtime/Modules/CyclicModuleRecord.cs | 9 ++++++-- .../Runtime/Modules/SourceTextModuleRecord.cs | 6 ++--- 13 files changed, 54 insertions(+), 33 deletions(-) diff --git a/Jint.Tests.Test262/Test262Test.cs b/Jint.Tests.Test262/Test262Test.cs index 7a5283bd46..6d4bc4986c 100644 --- a/Jint.Tests.Test262/Test262Test.cs +++ b/Jint.Tests.Test262/Test262Test.cs @@ -30,6 +30,8 @@ public abstract class Test262Test private static readonly HashSet _strictSkips = new(StringComparer.OrdinalIgnoreCase); + private static readonly Regex _moduleFlagRegex = new Regex(@"flags:\s*?\[.*?module.*?]", RegexOptions.Compiled); + static Test262Test() { //NOTE: The Date tests in test262 assume the local timezone is Pacific Standard Time @@ -98,14 +100,16 @@ static Test262Test() protected void RunTestCode(string fileName, string code, bool strict, string fullPath) { - var module = Regex.IsMatch(code, @"flags:\s*?\[.*?module.*?]"); + var module = _moduleFlagRegex.IsMatch(code); var engine = new Engine(cfg => { cfg.LocalTimeZone(_pacificTimeZone); cfg.Strict(strict); if (module) + { cfg.EnableModules(Path.Combine(BasePath, "test", Path.GetDirectoryName(fullPath)!)); + } }); engine.Execute(Sources["sta.js"]); @@ -199,7 +203,7 @@ protected void RunTestInternal(SourceFile sourceFile) return; } - if (sourceFile.Code.IndexOf("onlyStrict", StringComparison.Ordinal) < 0 && sourceFile.Code.IndexOf("module", StringComparison.Ordinal) < 0) + if (sourceFile.Code.IndexOf("onlyStrict", StringComparison.Ordinal) < 0 && !_moduleFlagRegex.IsMatch(sourceFile.Code)) { RunTestCode(sourceFile.Source, sourceFile.Code, strict: false, fullPath: sourceFile.FullPath); } diff --git a/Jint.Tests/Runtime/ModuleTests.cs b/Jint.Tests/Runtime/ModuleTests.cs index 2b3db508fa..d5c5aa9f45 100644 --- a/Jint.Tests/Runtime/ModuleTests.cs +++ b/Jint.Tests/Runtime/ModuleTests.cs @@ -305,6 +305,7 @@ public void ShouldAllowCyclicImport() #if(NET6_0_OR_GREATER) + [Fact] public void CanLoadModuleImportsFromFiles() { var engine = new Engine(options => options.EnableModules(GetBasePath())); diff --git a/Jint/Engine.Modules.cs b/Jint/Engine.Modules.cs index 203c2d58e8..d3057b0560 100644 --- a/Jint/Engine.Modules.cs +++ b/Jint/Engine.Modules.cs @@ -75,7 +75,7 @@ public void AddModule(string specifier, string code) public void AddModule(string specifier, Action buildModule) { - var moduleBuilder = new ModuleBuilder(this,specifier); + var moduleBuilder = new ModuleBuilder(this, specifier); buildModule(moduleBuilder); AddModule(specifier, moduleBuilder); } diff --git a/Jint/EsprimaExtensions.cs b/Jint/EsprimaExtensions.cs index 8ee0ae523c..a4ed7e3d80 100644 --- a/Jint/EsprimaExtensions.cs +++ b/Jint/EsprimaExtensions.cs @@ -228,8 +228,11 @@ internal static void GetBoundNames(this Node? parameter, List target) } else if (parameter is ClassDeclaration classDeclaration) { - parameter = classDeclaration.Id; - continue; + var name = classDeclaration.Id?.Name; + if (name != null) + { + target.Add(name); + } } break; diff --git a/Jint/HoistingScope.cs b/Jint/HoistingScope.cs index 7819545450..a2e3280ff7 100644 --- a/Jint/HoistingScope.cs +++ b/Jint/HoistingScope.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using Esprima.Ast; using Jint.Runtime.Modules; @@ -172,14 +171,22 @@ public static void GetImportsAndExports( } else { - var ie = importEntries.First(x => x.LocalName == ee.LocalName); - if (ie.ImportName == "*") + for (var j = 0; j < importEntries!.Count; j++) { - localExportEntries.Add(ee); - } - else - { - indirectExportEntries.Add(new(ee.ExportName, ie.ModuleRequest, ie.ImportName, null)); + var ie = importEntries[j]; + if (ie.LocalName == ee.LocalName) + { + if (ie.ImportName == "*") + { + localExportEntries.Add(ee); + } + else + { + indirectExportEntries.Add(new(ee.ExportName, ie.ModuleRequest, ie.ImportName, null)); + } + + break; + } } } } diff --git a/Jint/ModuleBuilder.cs b/Jint/ModuleBuilder.cs index b2db108bf8..8a5851604e 100644 --- a/Jint/ModuleBuilder.cs +++ b/Jint/ModuleBuilder.cs @@ -108,7 +108,10 @@ public ModuleBuilder WithOptions(Action configure) internal Module Parse() { - if (_sourceRaw.Count <= 0) return new Module(NodeList.Create(Array.Empty())); + if (_sourceRaw.Count <= 0) + { + return new Module(NodeList.Create(Array.Empty())); + } var javaScriptParser = new JavaScriptParser(_sourceRaw.Count == 1 ? _sourceRaw[0] : string.Join(Environment.NewLine, _sourceRaw), _options); diff --git a/Jint/Options.cs b/Jint/Options.cs index 356c7ed204..65de787b9e 100644 --- a/Jint/Options.cs +++ b/Jint/Options.cs @@ -74,11 +74,6 @@ public class Options /// public IReferenceResolver ReferenceResolver { get; set; } = DefaultReferenceResolver.Instance; - /// - /// Whether to return details of unhandled exceptions. Poses a security risk. Defaults to false. - /// - public bool IncludeExceptionDetails { get; set; } - /// /// Called by the instance that loads this /// once it is loaded. diff --git a/Jint/Runtime/Host.cs b/Jint/Runtime/Host.cs index 6c3c983565..3da8387723 100644 --- a/Jint/Runtime/Host.cs +++ b/Jint/Runtime/Host.cs @@ -115,7 +115,7 @@ public virtual void EnsureCanCompileStrings(Realm callerRealm, Realm evalRealm) /// /// https://tc39.es/ecma262/#sec-hostresolveimportedmodule /// - protected internal virtual ModuleRecord ResolveImportedModule(IScriptOrModule? referencingScriptOrModule, string specifier) + internal virtual ModuleRecord ResolveImportedModule(IScriptOrModule? referencingScriptOrModule, string specifier) { return Engine.LoadModule(referencingScriptOrModule?.Location, specifier); } diff --git a/Jint/Runtime/IScriptOrModule.cs b/Jint/Runtime/IScriptOrModule.cs index 97b7d9c983..0d6f06c2e0 100644 --- a/Jint/Runtime/IScriptOrModule.cs +++ b/Jint/Runtime/IScriptOrModule.cs @@ -1,6 +1,6 @@ namespace Jint.Runtime; -public interface IScriptOrModule +internal interface IScriptOrModule { public string Location { get; } } diff --git a/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs index a03c4b64ac..727315bbc6 100644 --- a/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs +++ b/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs @@ -2,7 +2,6 @@ using Esprima.Ast; using Jint.Native; using Jint.Native.Function; -using Jint.Native.Object; using Jint.Runtime.Environments; using Jint.Runtime.References; @@ -423,4 +422,4 @@ private ExpressionResult SetValue(EvaluationContext context) } } } -} \ No newline at end of file +} diff --git a/Jint/Runtime/Modules/BuilderModuleRecord.cs b/Jint/Runtime/Modules/BuilderModuleRecord.cs index b52323164c..0651107aba 100644 --- a/Jint/Runtime/Modules/BuilderModuleRecord.cs +++ b/Jint/Runtime/Modules/BuilderModuleRecord.cs @@ -7,7 +7,7 @@ namespace Jint.Runtime.Modules; /// /// This is a custom ModuleRecord implementation for dynamically built modules using /// -internal class BuilderModuleRecord : SourceTextModuleRecord +internal sealed class BuilderModuleRecord : SourceTextModuleRecord { private List> _exportBuilderDeclarations = new(); @@ -18,8 +18,12 @@ internal BuilderModuleRecord(Engine engine, Realm realm, Module source, string l internal void BindExportedValue(string name, JsValue value) { - if(_environment != null) ExceptionHelper.ThrowInvalidOperationException("Cannot bind exported values after the environment has been initialized"); - if (_exportBuilderDeclarations == null) _exportBuilderDeclarations = new(); + if (_environment != null) + { + ExceptionHelper.ThrowInvalidOperationException("Cannot bind exported values after the environment has been initialized"); + } + + _exportBuilderDeclarations ??= new(); _exportBuilderDeclarations.Add(new KeyValuePair(name, value)); } @@ -35,8 +39,8 @@ protected override void InitializeEnvironment() _environment.CreateImmutableBindingAndInitialize(d.Key, true, d.Value); _localExportEntries.Add(new ExportEntry(d.Key, null, null, d.Key)); } + _exportBuilderDeclarations.Clear(); } - } -} \ No newline at end of file +} diff --git a/Jint/Runtime/Modules/CyclicModuleRecord.cs b/Jint/Runtime/Modules/CyclicModuleRecord.cs index 7f9a68f7f3..d65ccc6cca 100644 --- a/Jint/Runtime/Modules/CyclicModuleRecord.cs +++ b/Jint/Runtime/Modules/CyclicModuleRecord.cs @@ -66,6 +66,7 @@ public override void Link() { ExceptionHelper.ThrowInvalidOperationException("Error while linking module: Module should be linking after abrupt completion"); } + m.Status = ModuleStatus.Unlinked; m._dfsIndex = -1; m._dfsAncestorIndex = -1; @@ -75,6 +76,7 @@ public override void Link() { ExceptionHelper.ThrowInvalidOperationException("Error while processing abrupt completion of module link: Module should be unlinked after cleanup"); } + throw; } @@ -286,6 +288,7 @@ protected internal override Completion InnerModuleEvaluation(Stack /// https://tc39.es/ecma262/#sec-async-module-execution-rejected /// - private JsValue AsyncModuleExecutionRejected(JsValue thisObj, JsValue[] arguments) + private static JsValue AsyncModuleExecutionRejected(JsValue thisObj, JsValue[] arguments) { var module = (SourceTextModuleRecord) arguments.At(0); var error = arguments.At(1); @@ -579,5 +583,6 @@ m._evalError is not null || /// https://tc39.es/ecma262/#table-cyclic-module-methods /// protected abstract void InitializeEnvironment(); + internal abstract Completion ExecuteModule(PromiseCapability capability = null); -} \ No newline at end of file +} diff --git a/Jint/Runtime/Modules/SourceTextModuleRecord.cs b/Jint/Runtime/Modules/SourceTextModuleRecord.cs index 358163cb08..b6d5cc9091 100644 --- a/Jint/Runtime/Modules/SourceTextModuleRecord.cs +++ b/Jint/Runtime/Modules/SourceTextModuleRecord.cs @@ -30,7 +30,7 @@ string LocalName /// /// https://tc39.es/ecma262/#sec-source-text-module-records /// -public class SourceTextModuleRecord : CyclicModuleRecord +internal class SourceTextModuleRecord : CyclicModuleRecord { private readonly Module _source; private ExecutionContext _context; @@ -334,8 +334,8 @@ internal override Completion ExecuteModule(PromiseCapability capability = null) } else { - //ToDo async modules + ExceptionHelper.ThrowNotImplementedException("async modules not implemented"); return default; } } -} \ No newline at end of file +}