#nullable disable using Jint.Native; using Jint.Native.Promise; using Jint.Runtime.Descriptors; using Jint.Runtime.Interop; namespace Jint.Runtime.Modules; #pragma warning disable CS0649 // never assigned to, waiting for new functionalities in spec internal sealed record ResolvedBinding(Module Module, string BindingName) { internal static ResolvedBinding Ambiguous => new(null, "ambiguous"); } /// /// https://tc39.es/ecma262/#sec-cyclic-module-records /// public abstract class CyclicModule : Module { private Completion? _evalError; private int _dfsIndex; private int _dfsAncestorIndex; internal HashSet _requestedModules; private CyclicModule _cycleRoot; protected bool _hasTLA; private bool _asyncEvaluation; private PromiseCapability _topLevelCapability; private readonly List _asyncParentModules; private int _asyncEvalOrder; private int _pendingAsyncDependencies; internal JsValue _evalResult; private SourceLocation _abnormalCompletionLocation; internal CyclicModule(Engine engine, Realm realm, string location, bool async) : base(engine, realm, location) { } internal ModuleStatus Status { get; private set; } internal ref readonly SourceLocation AbnormalCompletionLocation => ref _abnormalCompletionLocation; /// /// https://tc39.es/ecma262/#sec-moduledeclarationlinking /// public override void Link() { if (Status is ModuleStatus.Linking or ModuleStatus.Evaluating) { Throw.InvalidOperationException("Error while linking module: Module is already either linking or evaluating"); } var stack = new Stack(); try { InnerModuleLinking(stack, 0); } catch { foreach (var m in stack) { m._environment = null; if (m.Status != ModuleStatus.Linking) { Throw.InvalidOperationException("Error while linking module: Module should be linking after abrupt completion"); } m.Status = ModuleStatus.Unlinked; m._dfsIndex = -1; m._dfsAncestorIndex = -1; } if (Status != ModuleStatus.Unlinked) { Throw.InvalidOperationException("Error while processing abrupt completion of module link: Module should be unlinked after cleanup"); } throw; } if (Status is not (ModuleStatus.Linked or ModuleStatus.EvaluatingAsync or ModuleStatus.Evaluated)) { Throw.InvalidOperationException("Error while linking module: Module is neither linked, evaluating-async or evaluated"); } if (stack.Count > 0) { Throw.InvalidOperationException("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) { Throw.InvalidOperationException("Error while evaluating module: Module is in an invalid state"); } if (module.Status is ModuleStatus.EvaluatingAsync or 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 = module.InnerModuleEvaluation(stack, 0, ref asyncEvalOrder); if (result.Type != CompletionType.Normal) { foreach (var m in stack) { m.Status = ModuleStatus.Evaluated; m._evalError = result; } _abnormalCompletionLocation = result.Location; capability.Reject.Call(Undefined, result.Value); } else { if (module.Status != ModuleStatus.EvaluatingAsync && module.Status != ModuleStatus.Evaluated) { Throw.InvalidOperationException("Error while evaluating module: Module is in an invalid state"); } if (module._evalError is not null) { Throw.InvalidOperationException("Error while evaluating module: Module is in an invalid state"); } if (!module._asyncEvaluation) { if (module.Status != ModuleStatus.Evaluated) { Throw.InvalidOperationException("Error while evaluating module: Module is in an invalid state"); } capability.Resolve.Call(Undefined, Array.Empty()); } if (stack.Count > 0) { Throw.InvalidOperationException("Error while evaluating module: Module is in an invalid state"); } } return capability.PromiseInstance; } /// /// https://tc39.es/ecma262/#sec-InnerModuleLinking /// protected internal override int InnerModuleLinking(Stack stack, int index) { if (Status is ModuleStatus.Linking or ModuleStatus.Linked or ModuleStatus.EvaluatingAsync or ModuleStatus.Evaluated) { return index; } if (Status != ModuleStatus.Unlinked) { Throw.InvalidOperationException($"Error while linking module: Module in an invalid state: {Status}"); } Status = ModuleStatus.Linking; _dfsIndex = index; _dfsAncestorIndex = index; index++; stack.Push(this); foreach (var request in _requestedModules) { var requiredModule = _engine._host.GetImportedModule(this, request); index = requiredModule.InnerModuleLinking(stack, index); if (requiredModule is not CyclicModule requiredCyclicModule) { continue; } if (requiredCyclicModule.Status is not ( ModuleStatus.Linking or ModuleStatus.Linked or ModuleStatus.EvaluatingAsync or ModuleStatus.Evaluated)) { Throw.InvalidOperationException($"Error while linking module: Required module is in an invalid state: {requiredCyclicModule.Status}"); } if ((requiredCyclicModule.Status == ModuleStatus.Linking) == !stack.Contains(requiredCyclicModule)) { Throw.InvalidOperationException($"Error while linking module: Required module is in an invalid state: {requiredCyclicModule.Status}"); } if (requiredCyclicModule.Status == ModuleStatus.Linking) { _dfsAncestorIndex = Math.Min(_dfsAncestorIndex, requiredCyclicModule._dfsAncestorIndex); } } InitializeEnvironment(); if (StackReferenceCount(stack) != 1) { Throw.InvalidOperationException("Error while linking module: Recursive dependency detected"); } if (_dfsAncestorIndex > _dfsIndex) { Throw.InvalidOperationException("Error while linking module: Recursive dependency detected"); } if (_dfsIndex == _dfsAncestorIndex) { while (true) { var requiredModule = stack.Pop(); requiredModule.Status = ModuleStatus.Linked; if (requiredModule == this) { break; } } } return index; } /// /// https://tc39.es/ecma262/#sec-innermoduleevaluation /// protected internal override Completion InnerModuleEvaluation(Stack stack, int index, ref int asyncEvalOrder) { if (Status is ModuleStatus.EvaluatingAsync or ModuleStatus.Evaluated) { if (_evalError is null) { return new Completion(CompletionType.Normal, index, default); } return _evalError.Value; } if (Status == ModuleStatus.Evaluating) { return new Completion(CompletionType.Normal, index, default); } if (Status != ModuleStatus.Linked) { Throw.InvalidOperationException($"Error while evaluating module: Module is in an invalid state: {Status}"); } Status = ModuleStatus.Evaluating; _dfsIndex = index; _dfsAncestorIndex = index; _pendingAsyncDependencies = 0; index++; stack.Push(this); foreach (var required in _requestedModules) { var requiredModule = _engine._host.GetImportedModule(this, required); var result = requiredModule.InnerModuleEvaluation(stack, index, ref asyncEvalOrder); if (result.Type != CompletionType.Normal) { return result; } index = TypeConverter.ToInt32(result.Value); if (requiredModule is CyclicModule requiredCyclicModule) { if (requiredCyclicModule.Status != ModuleStatus.Evaluating && requiredCyclicModule.Status != ModuleStatus.EvaluatingAsync && requiredCyclicModule.Status != ModuleStatus.Evaluated) { Throw.InvalidOperationException($"Error while evaluating module: Module is in an invalid state: {requiredCyclicModule.Status}"); } if (requiredCyclicModule.Status == ModuleStatus.Evaluating && !stack.Contains(requiredCyclicModule)) { Throw.InvalidOperationException($"Error while evaluating module: Module is in an invalid state: {requiredCyclicModule.Status}"); } if (requiredCyclicModule.Status == ModuleStatus.Evaluating) { _dfsAncestorIndex = Math.Min(_dfsAncestorIndex, requiredCyclicModule._dfsAncestorIndex); } else { requiredCyclicModule = requiredCyclicModule._cycleRoot; if (requiredCyclicModule.Status is not (ModuleStatus.EvaluatingAsync or ModuleStatus.Evaluated)) { Throw.InvalidOperationException("Error while evaluating module: Module is in an invalid state"); } if (requiredCyclicModule._evalError != null) { return requiredCyclicModule._evalError.Value; } } if (requiredCyclicModule._asyncEvaluation) { _pendingAsyncDependencies++; requiredCyclicModule._asyncParentModules.Add(this); } } } Completion completion; if (_pendingAsyncDependencies > 0 || _hasTLA) { if (_asyncEvaluation) { Throw.InvalidOperationException("Error while evaluating module: Module is in an invalid state (async evaluation is true)"); } _asyncEvaluation = true; _asyncEvalOrder = asyncEvalOrder++; if (_pendingAsyncDependencies == 0) { completion = ExecuteAsyncModule(); } else { // 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 = ExecuteModule(); } if (StackReferenceCount(stack) != 1) { Throw.InvalidOperationException("Error while evaluating module: Module is in an invalid state (not found exactly once in stack)"); } if (_dfsAncestorIndex > _dfsIndex) { Throw.InvalidOperationException("Error while evaluating module: Module is in an invalid state (mismatch DFS ancestor index)"); } if (_dfsIndex == _dfsAncestorIndex) { var done = false; while (!done) { var requiredModule = stack.Pop(); if (!requiredModule._asyncEvaluation) { requiredModule.Status = ModuleStatus.Evaluated; } else { requiredModule.Status = ModuleStatus.EvaluatingAsync; } done = ReferenceEquals(requiredModule, this); requiredModule._cycleRoot = this; } } return completion; } private int StackReferenceCount(Stack stack) { var count = 0; foreach (var item in stack) { if (ReferenceEquals(item, this)) { count++; } } return count; } /// /// https://tc39.es/ecma262/#sec-execute-async-module /// private Completion ExecuteAsyncModule() { if (Status != ModuleStatus.Evaluating && Status != ModuleStatus.EvaluatingAsync || !_hasTLA) { Throw.InvalidOperationException("Error while evaluating module: Module is in an invalid state"); } var capability = PromiseConstructor.NewPromiseCapability(_engine, _realm.Intrinsics.Promise); var onFullfilled = new ClrFunction(_engine, "fulfilled", AsyncModuleExecutionFulfilled, 1, PropertyFlag.Configurable); var onRejected = new ClrFunction(_engine, "rejected", AsyncModuleExecutionRejected, 1, PropertyFlag.Configurable); PromiseOperations.PerformPromiseThen(_engine, (JsPromise) capability.PromiseInstance, onFullfilled, onRejected, null); return ExecuteModule(capability); } /// /// https://tc39.es/ecma262/#sec-async-module-execution-fulfilled /// private static JsValue AsyncModuleExecutionFulfilled(JsValue thisObject, JsCallArguments arguments) { var module = (CyclicModule) arguments.At(0); if (module.Status == ModuleStatus.Evaluated) { if (module._evalError is not null) { Throw.InvalidOperationException("Error while evaluating module: Module is in an invalid state"); } return Undefined; } if (module.Status != ModuleStatus.EvaluatingAsync || !module._asyncEvaluation || module._evalError is not null) { Throw.InvalidOperationException("Error while evaluating module: Module is in an invalid state"); } if (module._topLevelCapability is not null) { if (module._cycleRoot is null) { Throw.InvalidOperationException("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) { Throw.InvalidOperationException("Error while evaluating module: Module is in an invalid state"); } else if (m._hasTLA) { m.ExecuteAsyncModule(); } else { var result = m.ExecuteModule(); if (result.Type != CompletionType.Normal) { AsyncModuleExecutionRejected(Undefined, [m, result.Value]); } else { m.Status = ModuleStatus.Evaluated; if (m._topLevelCapability is not null) { if (m._cycleRoot is null) { Throw.InvalidOperationException("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 static JsValue AsyncModuleExecutionRejected(JsValue thisObject, JsCallArguments arguments) { var module = (SourceTextModule) arguments.At(0); var error = arguments.At(1); if (module.Status == ModuleStatus.Evaluated) { if (module._evalError is null) { Throw.InvalidOperationException("Error while evaluating module: Module is in an invalid state"); } return Undefined; } if (module.Status != ModuleStatus.EvaluatingAsync || !module._asyncEvaluation || module._evalError is not null) { Throw.InvalidOperationException("Error while evaluating module: Module is in an invalid state"); } module._evalError = new Completion(CompletionType.Throw, error, default); module.Status = ModuleStatus.Evaluated; var asyncParentModules = module._asyncParentModules; for (var i = 0; i < asyncParentModules.Count; i++) { var m = asyncParentModules[i]; AsyncModuleExecutionRejected(thisObject, [m, error]); } if (module._topLevelCapability is not null) { if (module._cycleRoot is null) { Throw.InvalidOperationException("Error while evaluating module: Module is in an invalid state"); } module._topLevelCapability.Reject.Call(Undefined, error); } return Undefined; } /// /// 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) { Throw.InvalidOperationException("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/#table-cyclic-module-methods /// protected abstract void InitializeEnvironment(); internal abstract Completion ExecuteModule(PromiseCapability capability = null); }