using System; using Esprima.Ast; using System.Collections.Generic; using System.Linq; 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(JsModule 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( JsModule 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 JsModule : JsValue { private readonly Engine _engine; private readonly Realm _realm; internal ModuleEnvironmentRecord _environment; private ObjectInstance _namespace; private Completion? _evalError; private int _dfsIndex; private int _dfsAncestorIndex; private readonly HashSet _requestedModules; private JsModule _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; internal readonly string _location; internal JsValue _evalResult; internal JsModule(Engine engine, Realm realm, Module source, string location, bool async) : 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 } internal ModuleStatus Status { get; private set; } /// /// https://tc39.es/ecma262/#sec-getmodulenamespace /// public static ObjectInstance GetModuleNamespace(JsModule module) { var ns = module._namespace; if(ns is null) { var exportedNames = module.GetExportedNames(); var unambiguousNames = new List(); for (var i = 0; i < exportedNames.Count; i++) { var name = exportedNames[i]; var resolution = module.ResolveExport(name); if(resolution is not null) { unambiguousNames.Add(name); } } ns = CreateModuleNamespace(module, unambiguousNames); } return ns; } /// /// https://tc39.es/ecma262/#sec-modulenamespacecreate /// private static ObjectInstance CreateModuleNamespace(JsModule module, List unambiguousNames) { var m = new ModuleNamespace(module._engine, module, unambiguousNames); module._namespace = m; return m; } /// /// https://tc39.es/ecma262/#sec-getexportednames /// public List GetExportedNames(List exportStarSet = null) { exportStarSet ??= new(); if (exportStarSet.Contains(this)) { //Reached the starting point of an export * circularity return new(); } exportStarSet.Add(this); var exportedNames = new List(); for (var i = 0; i < _localExportEntries.Count; i++) { var e = _localExportEntries[i]; exportedNames.Add(e.ExportName); } for (var i = 0; i < _indirectExportEntries.Count; i++) { var e = _indirectExportEntries[i]; exportedNames.Add(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[i]; 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(); for(var i = 0; i < resolveSet.Count; i++) { var r = resolveSet[i]; if(this == r.Module && exportName == r.ExportName) { //circular import request return null; } } resolveSet.Add(new(this, exportName)); for(var i = 0; i < _localExportEntries.Count; i++) { var e = _localExportEntries[i]; if (exportName == e.ExportName) { return new ResolvedBinding(this, e.LocalName); } } for(var i = 0; i < _indirectExportEntries.Count; i++) { var e = _localExportEntries[i]; if (exportName.Equals(e.ExportName)) { var importedModule = _engine._host.ResolveImportedModule(this, e.ModuleRequest); if(e.ImportName == "*") { return new ResolvedBinding(importedModule, "*namespace*"); } else { return importedModule.ResolveExport(e.ImportName, 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 { Link(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.Any()) { 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); int 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.Any()) { ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); } } return capability.PromiseInstance; } /// /// https://tc39.es/ecma262/#sec-InnerModuleLinking /// private int Link(JsModule 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); 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"); } if(requiredModule.Status == ModuleStatus.Linking && !stack.Contains(requiredModule)) { ExceptionHelper.ThrowInvalidOperationException("Error while linking module: Required module is in an invalid state"); } if (requiredModule.Status == ModuleStatus.Linking) { module._dfsAncestorIndex = System.Math.Min(module._dfsAncestorIndex, requiredModule._dfsAncestorIndex); } } module.InitializeEnvironment(); if (stack.Count(m => m == 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(JsModule 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); 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"); } if (requiredModule.Status == ModuleStatus.Evaluating && !stack.Contains(requiredModule)) { ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state"); } if(requiredModule.Status == ModuleStatus.Evaluating) { module._dfsAncestorIndex = System.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); } } 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) { module.ExecuteAsync(); } else { module.Execute(); } } else { module.Execute(); } if(stack.Count(x => x == 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) { bool 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 new Completion(CompletionType.Normal, index, null, default); } /// /// 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.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; 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, "Ambigous 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(_environment, _environment, null, realm, null); _context = moduleContext; _engine.EnterExecutionContext(_context); var hoistingScope = HoistingScope.GetModuleLevelDeclarations(_source); var varDeclarations = hoistingScope._variablesDeclarations; var declaredVarNames = new List(); 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.Contains(dn)) { env.CreateMutableBinding(dn, false); env.InitializeBinding(dn, Undefined); declaredVarNames.Add(dn); } } } } 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.Kind == VariableDeclarationKind.Const) { 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; var fd = new JintFunctionDefinition(_engine, d); env.CreateImmutableBinding(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 /// private Completion Execute(PromiseCapability capability = null) { var moduleContext = new ExecutionContext(_environment, _environment, null, _realm); if (!_hasTLA) { using (new StrictModeScope(strict: 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 = (JsModule)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) { JsModule module = (JsModule)arguments.At(0); JsValue 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 bool Equals(JsValue other) { return false; } public override object ToObject() { ExceptionHelper.ThrowNotSupportedException(); return null; } }