瀏覽代碼

Module loading support (#990)

Co-authored-by: lph <[email protected]>
Co-authored-by: Marko Lahma <[email protected]>
Co-authored-by: Sébastien Ros <[email protected]>
Philipp Lüthi 3 年之前
父節點
當前提交
0c223f4559

+ 51 - 0
Jint.Tests.Test262/Language/ModuleTestHost.cs

@@ -0,0 +1,51 @@
+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<string, JsValue> _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;
+        }
+    }
+}

+ 58 - 17
Jint.Tests.Test262/Language/ModuleTests.cs

@@ -1,31 +1,72 @@
+using Jint.Runtime;
+using Jint.Runtime.Modules;
+using System;
 using Xunit;
+using Xunit.Sdk;
 
-namespace Jint.Tests.Test262.Language
+namespace Jint.Tests.Test262.Language;
+
+public class ModuleTests : Test262Test
 {
-    public class ModuleTests : Test262Test
+    [Theory(DisplayName = "language\\module-code")]
+    [MemberData(nameof(SourceFiles), "language\\module-code", false)]
+    [MemberData(nameof(SourceFiles), "language\\module-code", true, Skip = "Skipped")]
+    protected void ModuleCode(SourceFile sourceFile)
+    {
+        RunModuleTest(sourceFile);
+    }
+
+    [Theory(DisplayName = "language\\export")]
+    [MemberData(nameof(SourceFiles), "language\\export", false)]
+    [MemberData(nameof(SourceFiles), "language\\export", true, Skip = "Skipped")]
+    protected void Export(SourceFile sourceFile)
+    {
+        RunModuleTest(sourceFile);
+    }
+
+    [Theory(DisplayName = "language\\import")]
+    [MemberData(nameof(SourceFiles), "language\\import", false)]
+    [MemberData(nameof(SourceFiles), "language\\import", true, Skip = "Skipped")]
+    protected void Import(SourceFile sourceFile)
+    {
+        RunModuleTest(sourceFile);
+    }
+
+    private static void RunModuleTest(SourceFile sourceFile)
     {
-        [Theory(DisplayName = "language\\module-code", Skip = "TODO")]
-        [MemberData(nameof(SourceFiles), "language\\module-code", false)]
-        [MemberData(nameof(SourceFiles), "language\\module-code", true, Skip = "Skipped")]
-        protected void ModuleCode(SourceFile sourceFile)
+        if (sourceFile.Skip)
         {
-            RunTestInternal(sourceFile);
+            return;
         }
 
-        [Theory(DisplayName = "language\\export")]
-        [MemberData(nameof(SourceFiles), "language\\export", false)]
-        [MemberData(nameof(SourceFiles), "language\\export", true, Skip = "Skipped")]
-        protected void Export(SourceFile sourceFile)
+        var code = sourceFile.Code;
+
+        var options = new Options();
+        options.Host.Factory = _ => new ModuleTestHost();
+        options.Modules.Enabled = true;
+        options.WithModuleLoader(new DefaultModuleLoader(null));
+
+        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)
         {
-            RunTestInternal(sourceFile);
+            lastError = ex.ToString();
         }
 
-        [Theory(DisplayName = "language\\import", Skip = "TODO")]
-        [MemberData(nameof(SourceFiles), "language\\import", false)]
-        [MemberData(nameof(SourceFiles), "language\\import", true, Skip = "Skipped")]
-        protected void Import(SourceFile sourceFile)
+        if (!negative && !string.IsNullOrWhiteSpace(lastError))
         {
-            RunTestInternal(sourceFile);
+            throw new XunitException(lastError);
         }
     }
 }

+ 19 - 0
Jint.Tests.Test262/Test262Test.cs

@@ -208,6 +208,13 @@ namespace Jint.Tests.Test262
 
             foreach (var file in files)
             {
+                if (file.IndexOf("_FIXTURE", StringComparison.OrdinalIgnoreCase) != -1)
+                {
+                    // Files bearing a name which includes the sequence _FIXTURE MUST NOT be interpreted
+                    // as standalone tests; they are intended to be referenced by test files.
+                    continue;
+                }
+
                 var name = file.Substring(fixturesPath.Length + 1).Replace("\\", "/");
                 bool skip = _skipReasons.TryGetValue(name, out var reason);
 
@@ -288,6 +295,18 @@ namespace Jint.Tests.Test262
                                 skip = true;
                                 reason = "resizable-arraybuffer not implemented";
                                 break;
+                            case "json-modules":
+                                skip = true;
+                                reason = "json-modules not implemented";
+                                break;
+                            case "top-level-await":
+                                skip = true;
+                                reason = "top-level-await not implemented";
+                                break;
+                            case "import-assertions":
+                                skip = true;
+                                reason = "import-assertions not implemented";
+                                break;
                         }
                     }
                 }

+ 33 - 0
Jint/Engine.Modules.cs

@@ -0,0 +1,33 @@
+using System.Collections.Generic;
+using Jint.Runtime.Modules;
+
+namespace Jint
+{
+    public partial class Engine
+    {
+        internal IModuleLoader ModuleLoader { get; set; }
+
+        private readonly Dictionary<ModuleCacheKey, JsModule> _modules = new();
+
+        public JsModule LoadModule(string specifier) => LoadModule(null, specifier);
+
+        internal JsModule LoadModule(string referencingModuleLocation, string specifier)
+        {
+            var key = new ModuleCacheKey(referencingModuleLocation ?? string.Empty, specifier);
+
+            if (_modules.TryGetValue(key, out var module))
+            {
+                return module;
+            }
+
+            var (loadedModule, location) = ModuleLoader.LoadModule(this, specifier, referencingModuleLocation);
+            module = new JsModule(this, _host.CreateRealm(), loadedModule, location.AbsoluteUri, false);
+
+            _modules[key] = module;
+
+            return module;
+        }
+
+        internal readonly record struct ModuleCacheKey(string ReferencingModuleLocation, string Specifier);
+    }
+}

+ 127 - 0
Jint/EsprimaExtensions.cs

@@ -11,6 +11,7 @@ using Jint.Runtime;
 using Jint.Runtime.Environments;
 using Jint.Runtime.Interpreter;
 using Jint.Runtime.Interpreter.Expressions;
+using Jint.Runtime.Modules;
 
 namespace Jint
 {
@@ -128,6 +129,7 @@ namespace Jint
             {
                 return TypeConverter.ToString(d);
             }
+
             return literal.Value as string ?? Convert.ToString(literal.Value, provider: null);
         }
 
@@ -204,6 +206,7 @@ namespace Jint
                     parameter = assignmentPattern.Left;
                     continue;
                 }
+
                 break;
             }
         }
@@ -256,6 +259,130 @@ namespace Jint
             return new Record(property, closure);
         }
 
+        internal static void GetImportEntries(this ImportDeclaration import, List<ImportEntry> importEntries, HashSet<string> requestedModules)
+        {
+            var source = import.Source.StringValue!;
+            var specifiers = import.Specifiers;
+            requestedModules.Add(source!);
+
+            foreach (var specifier in specifiers)
+            {
+                switch (specifier)
+                {
+                    case ImportNamespaceSpecifier namespaceSpecifier:
+                        importEntries.Add(new ImportEntry(source, "*", namespaceSpecifier.Local.GetModuleKey()));
+                        break;
+                    case ImportSpecifier importSpecifier:
+                        importEntries.Add(new ImportEntry(source, importSpecifier.Imported.GetModuleKey(), importSpecifier.Local.GetModuleKey()));
+                        break;
+                    case ImportDefaultSpecifier defaultSpecifier:
+                        importEntries.Add(new ImportEntry(source, "default", defaultSpecifier.Local.GetModuleKey()));
+                        break;
+                }
+            }
+        }
+
+        internal static void GetExportEntries(this ExportDeclaration export, List<ExportEntry> exportEntries, HashSet<string> requestedModules)
+        {
+            switch (export)
+            {
+                case ExportDefaultDeclaration defaultDeclaration:
+                    GetExportEntries(true, defaultDeclaration.Declaration, exportEntries);
+                    break;
+                case ExportAllDeclaration allDeclaration:
+                    //Note: there is a pending PR for Esprima to support exporting an imported modules content as a namespace i.e. 'export * as ns from "mod"'
+                    requestedModules.Add(allDeclaration.Source.StringValue!);
+                    exportEntries.Add(new(null, allDeclaration.Source.StringValue, "*", null));
+                    break;
+                case ExportNamedDeclaration namedDeclaration:
+                    var specifiers = namedDeclaration.Specifiers;
+                    if (specifiers.Count == 0)
+                    {
+                        GetExportEntries(false, namedDeclaration.Declaration!, exportEntries, namedDeclaration.Source?.StringValue);
+
+                        if (namedDeclaration.Source is not null)
+                        {
+                            requestedModules.Add(namedDeclaration.Source.StringValue!);
+                        }
+                    }
+                    else
+                    {
+                        foreach (var specifier in specifiers)
+                        {
+                            exportEntries.Add(new(specifier.Local.GetModuleKey(), namedDeclaration.Source?.StringValue, specifier.Exported.GetModuleKey(), null));
+                        }
+                    }
+
+                    break;
+            }
+        }
+
+        private static void GetExportEntries(bool defaultExport, StatementListItem declaration, List<ExportEntry> exportEntries, string? moduleRequest = null)
+        {
+            var names = GetExportNames(declaration);
+
+            if (names.Count == 0)
+            {
+                if (defaultExport)
+                {
+                    exportEntries.Add(new("default", null, null, "*default*"));
+                }
+            }
+            else
+            {
+                for (var i = 0; i < names.Count; i++)
+                {
+                    var name = names[i];
+                    var exportName = defaultExport ? "default" : name;
+                    exportEntries.Add(new(exportName, moduleRequest, null, name));
+                }
+            }
+        }
+
+        private static List<string> GetExportNames(StatementListItem declaration)
+        {
+            var result = new List<string>();
+
+            switch (declaration)
+            {
+                case FunctionDeclaration functionDeclaration:
+                    var funcName = functionDeclaration.Id?.Name;
+                    if (funcName is not null)
+                    {
+                        result.Add(funcName);
+                    }
+
+                    break;
+                case ClassDeclaration classDeclaration:
+                    var className = classDeclaration.Id?.Name;
+                    if (className is not null)
+                    {
+                        result.Add(className);
+                    }
+
+                    break;
+                case VariableDeclaration variableDeclaration:
+                    var declarators = variableDeclaration.Declarations;
+                    foreach (var declarator in declarators)
+                    {
+                        var varName = declarator.Id.As<Identifier>()?.Name;
+                        if (varName is not null)
+                        {
+                            result.Add(varName);
+                        }
+                    }
+
+                    break;
+            }
+
+            return result;
+        }
+
+        private static string? GetModuleKey(this Expression expression)
+        {
+            return (expression as Identifier)?.Name ?? (expression as Literal)?.StringValue;
+        }
+
         internal readonly record struct Record(JsValue Key, ScriptFunctionInstance Closure);
     }
 }

+ 141 - 13
Jint/HoistingScope.cs

@@ -1,12 +1,14 @@
 using System.Collections.Generic;
+using System.Linq;
 using Esprima.Ast;
+using Jint.Runtime.Modules;
 
 namespace Jint
 {
     internal readonly struct HoistingScope
     {
         internal readonly List<FunctionDeclaration> _functionDeclarations;
-        
+
         internal readonly List<VariableDeclaration> _variablesDeclarations;
         internal readonly List<Key> _varNames;
 
@@ -29,7 +31,7 @@ namespace Jint
 
         public static HoistingScope GetProgramLevelDeclarations(
             Script script,
-            bool collectVarNames = false, 
+            bool collectVarNames = false,
             bool collectLexicalNames = false)
         {
             var treeWalker = new ScriptWalker(StrictModeScope.IsStrictModeCode, collectVarNames, collectLexicalNames);
@@ -41,7 +43,7 @@ namespace Jint
                 treeWalker._lexicalDeclarations,
                 treeWalker._lexicalNames);
         }
-        
+
         public static HoistingScope GetFunctionLevelDeclarations(
             IFunction node,
             bool collectVarNames = false,
@@ -49,7 +51,23 @@ namespace Jint
         {
             var treeWalker = new ScriptWalker(StrictModeScope.IsStrictModeCode, collectVarNames, collectLexicalNames);
             treeWalker.Visit(node.Body, null);
-            
+
+            return new HoistingScope(
+                treeWalker._functions,
+                treeWalker._varNames,
+                treeWalker._variableDeclarations,
+                treeWalker._lexicalDeclarations,
+                treeWalker._lexicalNames);
+        }
+
+        public static HoistingScope GetModuleLevelDeclarations(
+            Module module,
+            bool collectVarNames = false,
+            bool collectLexicalNames = false)
+        {
+            //Modules area always strict
+            var treeWalker = new ScriptWalker(true, collectVarNames, collectLexicalNames);
+            treeWalker.Visit(module, null);
             return new HoistingScope(
                 treeWalker._functions,
                 treeWalker._varNames,
@@ -60,7 +78,7 @@ namespace Jint
 
         public static List<Declaration> GetLexicalDeclarations(BlockStatement statement)
         {
-            List<Declaration> lexicalDeclarations = null ;
+            List<Declaration> lexicalDeclarations = null;
             ref readonly var statementListItems = ref statement.Body;
             for (var i = 0; i < statementListItems.Count; i++)
             {
@@ -76,15 +94,15 @@ namespace Jint
                 }
 
                 lexicalDeclarations ??= new List<Declaration>();
-                lexicalDeclarations.Add((Declaration) node);
+                lexicalDeclarations.Add((Declaration)node);
             }
-            
+
             return lexicalDeclarations;
         }
 
         public static List<Declaration> GetLexicalDeclarations(SwitchCase statement)
         {
-            List<Declaration> lexicalDeclarations = null ;
+            List<Declaration> lexicalDeclarations = null;
             ref readonly var statementListItems = ref statement.Consequent;
             for (var i = 0; i < statementListItems.Count; i++)
             {
@@ -93,8 +111,8 @@ namespace Jint
                 {
                     continue;
                 }
-                
-                var rootVariable = (VariableDeclaration) node;
+
+                var rootVariable = (VariableDeclaration)node;
                 if (rootVariable.Kind == VariableDeclarationKind.Var)
                 {
                     continue;
@@ -107,6 +125,76 @@ namespace Jint
             return lexicalDeclarations;
         }
 
+        public static void GetImportsAndExports(
+            Module module,
+            out HashSet<string> requestedModules,
+            out List<ImportEntry> importEntries,
+            out List<ExportEntry> localExportEntries,
+            out List<ExportEntry> indirectExportEntries,
+            out List<ExportEntry> starExportEntries)
+        {
+            var treeWalker = new ModuleWalker();
+            treeWalker.Visit(module);
+
+            importEntries = treeWalker._importEntries;
+            requestedModules = treeWalker._requestedModules ?? new();
+            var importedBoundNames = new HashSet<string>();
+
+            if (importEntries != null)
+            {
+                for (var i = 0; i < importEntries.Count; i++)
+                {
+                    var ie = importEntries[i];
+
+                    if (ie.LocalName is not null)
+                    {
+                        importedBoundNames.Add(ie.LocalName);
+                    }
+                }
+            }
+
+            var exportEntries = treeWalker._exportEntries;
+            localExportEntries = new();
+            indirectExportEntries = new();
+            starExportEntries = new();
+
+            if (exportEntries != null)
+            {
+                for (var i = 0; i < exportEntries.Count; i++)
+                {
+                    var ee = exportEntries[i];
+
+                    if (ee.ModuleRequest is null)
+                    {
+                        if (!importedBoundNames.Contains(ee.LocalName))
+                        {
+                            localExportEntries.Add(ee);
+                        }
+                        else
+                        {
+                            var ie = importEntries.First(x => x.LocalName == ee.LocalName);
+                            if (ie.ImportName == "*")
+                            {
+                                localExportEntries.Add(ee);
+                            }
+                            else
+                            {
+                                indirectExportEntries.Add(new(ee.ExportName, ie.ModuleRequest, ie.ImportName, null));
+                            }
+                        }
+                    }
+                    else if (ee.ImportName == "*" && ee.ExportName is null)
+                    {
+                        starExportEntries.Add(ee);
+                    }
+                    else
+                    {
+                        indirectExportEntries.Add(ee);
+                    }
+                }
+            }
+        }
+
         private sealed class ScriptWalker
         {
             internal List<FunctionDeclaration> _functions;
@@ -139,7 +227,7 @@ namespace Jint
 
                     if (childNode.Type == Nodes.VariableDeclaration)
                     {
-                        var variableDeclaration = (VariableDeclaration) childNode;
+                        var variableDeclaration = (VariableDeclaration)childNode;
                         if (variableDeclaration.Kind == VariableDeclarationKind.Var)
                         {
                             _variableDeclarations ??= new List<VariableDeclaration>();
@@ -176,12 +264,12 @@ namespace Jint
                             }
                         }
                     }
-                    else if (childNode.Type == Nodes.FunctionDeclaration 
+                    else if (childNode.Type == Nodes.FunctionDeclaration
                              // in strict mode cannot include function declarations directly under block or case clauses
                              && (!_strict || parent is null || (node.Type != Nodes.BlockStatement && node.Type != Nodes.SwitchCase)))
                     {
                         _functions ??= new List<FunctionDeclaration>();
-                        _functions.Add((FunctionDeclaration) childNode);
+                        _functions.Add((FunctionDeclaration)childNode);
                     }
 
                     if (childNode.Type != Nodes.FunctionDeclaration
@@ -195,5 +283,45 @@ namespace Jint
                 }
             }
         }
+
+        private sealed class ModuleWalker
+        {
+            internal List<ImportEntry> _importEntries;
+            internal List<ExportEntry> _exportEntries;
+            internal HashSet<string> _requestedModules;
+
+            internal void Visit(Node node)
+            {
+                foreach (var childNode in node.ChildNodes)
+                {
+                    if (childNode is null)
+                    {
+                        continue;
+                    }
+
+                    if (childNode.Type == Nodes.ImportDeclaration)
+                    {
+                        _importEntries ??= new();
+                        _requestedModules ??= new();
+                        var import = childNode as ImportDeclaration;
+                        import.GetImportEntries(_importEntries, _requestedModules);
+                    }
+                    else if (childNode.Type == Nodes.ExportAllDeclaration ||
+                             childNode.Type == Nodes.ExportDefaultDeclaration ||
+                             childNode.Type == Nodes.ExportNamedDeclaration)
+                    {
+                        _exportEntries ??= new();
+                        _requestedModules ??= new();
+                        var export = (ExportDeclaration) childNode;
+                        export.GetExportEntries(_exportEntries, _requestedModules);
+                    }
+
+                    if (childNode.ChildNodes.Count > 0)
+                    {
+                        Visit(childNode);
+                    }
+                }
+            }
+        }
     }
 }

+ 10 - 4
Jint/Native/Promise/PromiseOperations.cs

@@ -143,10 +143,16 @@ namespace Jint.Native.Promise
                     break;
             }
 
-            // TODO do we actually need to track that? 
-            // it seems this is mostly for debugging purposes.
-            // E.g. to report unhandled promises to dev
-            // 12. Set promise.[[PromiseIsHandled]] to true.
+            //https://tc39.es/ecma262/#sec-performpromisethen
+            //...
+            //13. If resultCapability is undefined, then
+            //      a. Return undefined
+            //14. Else
+            //      a. Return resultCapability.[[Promise]]
+            if (resultCapability is null)
+            {
+                return JsValue.Undefined;
+            }
 
             return resultCapability.PromiseInstance;
         }

+ 28 - 0
Jint/Options.Extensions.cs

@@ -8,6 +8,7 @@ using Jint.Native;
 using Jint.Runtime;
 using Jint.Runtime.Debugger;
 using Jint.Runtime.Interop;
+using Jint.Runtime.Modules;
 
 namespace Jint
 {
@@ -241,5 +242,32 @@ namespace Jint
         {
             options.Host.Factory = factory;
         }
+
+        /// <summary>
+        /// Enables module loading in the engine via the 'require' function. By default there's no sand-boxing and
+        /// you need to trust the script loading the modules not doing bad things.
+        /// </summary>
+        public static Options EnableModules(this Options options, bool enable = true)
+        {
+            options.Modules.Enabled = enable;
+            return options;
+        }
+
+        /// <summary>
+        /// Allows to configure module loader implementation.
+        /// </summary>
+        public static Options WithModuleLoader<T>(this Options options) where T : IModuleLoader, new()
+        {
+            return WithModuleLoader(options, new T());
+        }
+
+        /// <summary>
+        /// Allows to configure module loader implementation.
+        /// </summary>
+        public static Options WithModuleLoader(this Options options, IModuleLoader moduleLoader)
+        {
+            options.Modules.ModuleLoader = moduleLoader;
+            return options;
+        }
     }
 }

+ 53 - 0
Jint/Options.cs

@@ -12,6 +12,7 @@ using Jint.Runtime;
 using Jint.Runtime.Interop;
 using Jint.Runtime.Debugger;
 using Jint.Runtime.Descriptors;
+using Jint.Runtime.Modules;
 
 namespace Jint
 {
@@ -45,6 +46,11 @@ namespace Jint
         /// </summary>
         internal HostOptions Host { get; } = new();
 
+        /// <summary>
+        /// Module options
+        /// </summary>
+        public ModuleOptions Modules { get; } = new();
+
         /// <summary>
         /// Whether the code should be always considered to be in strict mode. Can improve performance.
         /// </summary>
@@ -97,6 +103,32 @@ namespace Jint
                 AttachExtensionMethodsToPrototypes(engine);
             }
 
+            var moduleLoader = Modules.ModuleLoader;
+            if (Modules.Enabled)
+            {
+                if (ReferenceEquals(moduleLoader, FailFastModuleLoader.Instance))
+                {
+                    moduleLoader = new DefaultModuleLoader(new System.IO.FileInfo(Assembly.GetEntryAssembly().CodeBase).DirectoryName);
+                }
+
+                if (Modules.RegisterRequire)
+                {
+                    // Node js like loading of modules
+                    engine.Realm.GlobalObject.SetProperty("require", new PropertyDescriptor(new ClrFunctionInstance(
+                            engine,
+                            "require",
+                            (thisObj, arguments) =>
+                            {
+                                var specifier = TypeConverter.ToString(arguments.At(0));
+                                var module = engine.LoadModule(specifier);
+                                return JsModule.GetModuleNamespace(module);
+                            }),
+                        PropertyFlag.AllForbidden));
+                }
+            }
+
+            engine.ModuleLoader = moduleLoader;
+
             // ensure defaults
             engine.ClrTypeConverter ??= new DefaultTypeConverter(engine);
         }
@@ -323,4 +355,25 @@ namespace Jint
     {
         internal Func<Engine, Host> Factory { get; set; } = _ => new Host();
     }
+
+    /// <summary>
+    /// Module related customization, work in progress
+    /// </summary>
+    public class ModuleOptions
+    {
+        /// <summary>
+        /// Indicates if modules are enabled in the current engine context, defaults to false.
+        /// </summary>
+        public bool Enabled { get; set; }
+
+        /// <summary>
+        /// Whether to register require function to engine which will delegate to module loader, defaults to false.
+        /// </summary>
+        public bool RegisterRequire { get; set; }
+
+        /// <summary>
+        /// Module loader implementation, by default exception will be thrown if module loading is not enabled.
+        /// </summary>
+        public IModuleLoader? ModuleLoader { get; set; } = FailFastModuleLoader.Instance;
+    }
 }

+ 3 - 3
Jint/Runtime/Environments/DeclarativeEnvironmentRecord.cs

@@ -19,12 +19,12 @@ namespace Jint.Runtime.Environments
             _catchEnvironment = catchEnvironment;
         }
 
-        public sealed override bool HasBinding(string name)
+        public override bool HasBinding(string name)
         {
             return _dictionary.ContainsKey(name);
         }
 
-        internal sealed override bool TryGetBinding(
+        internal override bool TryGetBinding(
             in BindingName name,
             bool strict,
             out Binding binding,
@@ -110,7 +110,7 @@ namespace Jint.Runtime.Environments
             }
         }
 
-        public sealed override JsValue GetBindingValue(string name, bool strict)
+        public override JsValue GetBindingValue(string name, bool strict)
         {
             _dictionary.TryGetValue(name, out var binding);
             if (binding.IsInitialized())

+ 11 - 0
Jint/Runtime/Environments/JintEnvironment.cs

@@ -95,5 +95,16 @@ namespace Jint.Runtime.Environments
         {
             return new PrivateEnvironmentRecord(outerPriv);
         }
+
+        /// <summary>
+        /// https://tc39.es/ecma262/#sec-newmoduleenvironment
+        /// </summary>
+        internal static ModuleEnvironmentRecord NewModuleEnvironment(Engine engine, EnvironmentRecord outer)
+        {
+            return new ModuleEnvironmentRecord(engine)
+            {
+                _outerEnv = outer
+            };
+        }
     }
 }

+ 55 - 0
Jint/Runtime/Environments/ModuleEnvironmentRecord.cs

@@ -0,0 +1,55 @@
+using Jint.Collections;
+using Jint.Native;
+using Jint.Runtime.Modules;
+
+namespace Jint.Runtime.Environments;
+
+/// <summary>
+/// Represents a module environment record
+/// https://tc39.es/ecma262/#sec-module-environment-records
+/// </summary>
+internal sealed class ModuleEnvironmentRecord : DeclarativeEnvironmentRecord
+{
+    private readonly HybridDictionary<IndirectBinding> _importBindings = new();
+
+    internal ModuleEnvironmentRecord(Engine engine) : base(engine, false)
+    {
+    }
+
+    public override JsValue GetThisBinding()
+    {
+        return Undefined;
+    }
+
+    public void CreateImportBinding(string importName, JsModule module, string name)
+    {
+        _hasBindings = true;
+        _importBindings[importName] = new IndirectBinding(module, name);
+    }
+
+    public override JsValue GetBindingValue(string name, bool strict)
+    {
+        if (_importBindings.TryGetValue(name, out var indirectBinding))
+        {
+            return base.GetBindingValue(name, strict);
+        }
+
+        return indirectBinding.Module._environment.GetBindingValue(indirectBinding.BindingName, true);
+    }
+
+    internal override bool TryGetBinding(in BindingName name, bool strict, out Binding binding, out JsValue value)
+    {
+        if (!_importBindings.TryGetValue(name.Key, out var indirectBinding))
+        {
+            return base.TryGetBinding(name, strict, out binding, out value);
+        }
+
+        value = indirectBinding.Module._environment.GetBindingValue(indirectBinding.BindingName, true);
+        binding = new(value, false, false, true);
+        return true;
+    }
+
+    public override bool HasThisBinding() => true;
+
+    private readonly record struct IndirectBinding(JsModule Module, string BindingName);
+}

+ 74 - 0
Jint/Runtime/Host.cs

@@ -1,6 +1,11 @@
+using Jint.Native;
 using Jint.Native.Global;
 using Jint.Native.Object;
+using Jint.Native.Promise;
+using Jint.Runtime.Descriptors;
 using Jint.Runtime.Environments;
+using Jint.Runtime.Interop;
+using Jint.Runtime.Modules;
 
 namespace Jint.Runtime
 {
@@ -92,5 +97,74 @@ namespace Jint.Runtime
         public virtual void EnsureCanCompileStrings(Realm callerRealm, Realm evalRealm)
         {
         }
+
+        /// <summary>
+        /// https://tc39.es/ecma262/#sec-hostresolveimportedmodule
+        /// </summary>
+        /// <param name="referencingModule"></param>
+        /// <param name="specifier"></param>
+        /// <returns></returns>
+        protected internal virtual JsModule ResolveImportedModule(JsModule referencingModule, string specifier)
+        {
+            return Engine.LoadModule(referencingModule._location, specifier);
+        }
+
+        /// <summary>
+        /// https://tc39.es/ecma262/#sec-hostimportmoduledynamically
+        /// </summary>
+        /// <param name="referencingModule"></param>
+        /// <param name="specifier"></param>
+        /// <param name="promiseCapability"></param>
+        internal virtual void ImportModuleDynamically(JsModule referencingModule, string specifier, PromiseCapability promiseCapability)
+        {
+            var promise = Engine.RegisterPromise();
+
+            try
+            {
+                Engine.LoadModule(referencingModule._location, specifier);
+                promise.Resolve(JsValue.Undefined);
+
+            }
+            catch (JavaScriptException ex)
+            {
+                promise.Reject(ex.Error);
+            }
+
+            FinishDynamicImport(referencingModule, specifier, promiseCapability, (PromiseInstance)promise.Promise);
+        }
+
+        /// <summary>
+        /// https://tc39.es/ecma262/#sec-finishdynamicimport
+        /// </summary>
+        /// <param name="referencingModule"></param>
+        /// <param name="specifier"></param>
+        /// <param name="promiseCapability"></param>
+        /// <param name="innerPromise"></param>
+        internal virtual void FinishDynamicImport(JsModule 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);
+                    promiseCapability.Resolve.Call(ns);
+                }
+                catch (JavaScriptException ex)
+                {
+                    promiseCapability.Reject.Call(ex.Error);
+                }
+                return JsValue.Undefined;
+            }, 0, PropertyFlag.Configurable);
+
+            var onRejected = new ClrFunctionInstance(Engine, "", (thisObj, args) =>
+            {
+                var error = args.At(0);
+                promiseCapability.Reject.Call(error);
+                return JsValue.Undefined;
+            }, 0, PropertyFlag.Configurable);
+
+            PromiseOperations.PerformPromiseThen(Engine, innerPromise, onFulfilled, onRejected, null);
+        }
     }
 }

+ 5 - 0
Jint/Runtime/Interpreter/Statements/JintStatement.cs

@@ -88,7 +88,12 @@ namespace Jint.Runtime.Interpreter.Statements
                 Nodes.WhileStatement => new JintWhileStatement((WhileStatement) statement),
                 Nodes.WithStatement => new JintWithStatement((WithStatement) statement),
                 Nodes.DebuggerStatement => new JintDebuggerStatement((DebuggerStatement) statement),
+                Nodes.Program when statement is Script s => new JintScript(s),
                 Nodes.ClassDeclaration => new JintClassDeclarationStatement((ClassDeclaration) statement),
+                Nodes.ExportAllDeclaration or
+                Nodes.ExportDefaultDeclaration or
+                Nodes.ExportNamedDeclaration or
+                Nodes.ImportDeclaration => new JintEmptyStatement(new EmptyStatement()),
                 _ => null
             };
 

+ 71 - 0
Jint/Runtime/Modules/DefaultModuleLoader.cs

@@ -0,0 +1,71 @@
+#nullable enable
+
+using System;
+using System.IO;
+using Esprima;
+using Esprima.Ast;
+
+namespace Jint.Runtime.Modules;
+
+public class DefaultModuleLoader : IModuleLoader
+{
+    private readonly string _basePath;
+
+    public DefaultModuleLoader(string basePath)
+    {
+        _basePath = basePath;
+    }
+
+    public virtual ModuleLoaderResult LoadModule(Engine engine, string location, string? referencingLocation)
+    {
+        // If no referencing location is provided, ensure location is absolute
+
+        var locationUri = referencingLocation == null 
+            ? new Uri(location, UriKind.Absolute) 
+            : new Uri(new Uri(referencingLocation, UriKind.Absolute), location)
+            ;
+
+        // Ensure the resulting resource is under the base path if it is provided
+
+        if (!String.IsNullOrEmpty(_basePath) && !locationUri.AbsolutePath.StartsWith(_basePath, StringComparison.Ordinal))
+        {
+            ExceptionHelper.ThrowArgumentException("Invalid file location.");
+        }
+
+        return LoadModule(engine, locationUri);
+    }
+
+    protected virtual ModuleLoaderResult LoadModule(Engine engine, Uri location)
+    {
+        var code = LoadModuleSourceCode(location);
+
+        Module module;
+        try
+        {
+            var parserOptions = new ParserOptions(location.ToString())
+            {
+                AdaptRegexp = true,
+                Tolerant = true
+            };
+
+            module = new JavaScriptParser(code, parserOptions).ParseModule();
+        }
+        catch (ParserException ex)
+        {
+            ExceptionHelper.ThrowSyntaxError(engine.Realm, $"Error while loading module: error in module '{location}': {ex.Error}");
+            module = null;
+        }
+
+        return new ModuleLoaderResult(module, location);
+    }
+
+    protected virtual string LoadModuleSourceCode(Uri location)
+    {
+        if (!location.IsFile)
+        {
+            ExceptionHelper.ThrowArgumentException("Only file loading is supported");
+        }
+
+        return File.ReadAllText(location.AbsolutePath);
+    }
+}

+ 19 - 0
Jint/Runtime/Modules/FailFastModuleLoader.cs

@@ -0,0 +1,19 @@
+#nullable enable
+
+namespace Jint.Runtime.Modules;
+
+internal sealed class FailFastModuleLoader : IModuleLoader
+{
+    public static readonly IModuleLoader Instance = new FailFastModuleLoader();
+
+    public ModuleLoaderResult LoadModule(Engine engine, string location, string? referencingLocation)
+    {
+        ThrowDisabledException();
+        return default;
+    }
+
+    private static void ThrowDisabledException()
+    {
+        ExceptionHelper.ThrowInvalidOperationException("Module loading has been disabled, you need to enable it in engine options");
+    }
+}

+ 22 - 0
Jint/Runtime/Modules/IModuleLoader.cs

@@ -0,0 +1,22 @@
+#nullable enable
+
+using System;
+using Esprima.Ast;
+
+namespace Jint.Runtime.Modules;
+
+/// <summary>
+/// Module loading result.
+/// </summary>
+public readonly record struct ModuleLoaderResult(Module Module, Uri Location);
+
+/// <summary>
+/// Module loader interface that allows defining how module loadings requests are handled.
+/// </summary>
+public interface IModuleLoader
+{
+    /// <summary>
+    /// Loads a module from given location.
+    /// </summary>
+    public ModuleLoaderResult LoadModule(Engine engine, string location, string? referencingLocation);
+}

+ 913 - 0
Jint/Runtime/Modules/JsModule.cs

@@ -0,0 +1,913 @@
+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
+);
+
+/// <summary>
+/// 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
+/// </summary>
+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<string> _requestedModules;
+    private JsModule _cycleRoot;
+    private bool _hasTLA;
+    private bool _asyncEvaluation;
+    private PromiseCapability _topLevelCapability;
+    private List<JsModule> _asyncParentModules;
+    private int _asyncEvalOrder;
+    private int _pendingAsyncDependencies;
+
+    private readonly Module _source;
+    private ExecutionContext _context;
+    private readonly ObjectInstance _importMeta;
+    private readonly List<ImportEntry> _importEntries;
+    private readonly List<ExportEntry> _localExportEntries;
+    private readonly List<ExportEntry> _indirectExportEntries;
+    private readonly List<ExportEntry> _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; }
+
+    /// <summary>
+    /// https://tc39.es/ecma262/#sec-getmodulenamespace
+    /// </summary>
+    public static ObjectInstance GetModuleNamespace(JsModule module)
+    {
+        var ns = module._namespace;
+        if(ns is null)
+        {
+            var exportedNames = module.GetExportedNames();
+            var unambiguousNames = new List<string>();
+            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;
+    }
+
+    /// <summary>
+    /// https://tc39.es/ecma262/#sec-modulenamespacecreate
+    /// </summary>
+    private static ObjectInstance CreateModuleNamespace(JsModule module, List<string> unambiguousNames)
+    {
+        var m = new ModuleNamespace(module._engine, module, unambiguousNames);
+        module._namespace = m;
+        return m;
+    }
+
+    /// <summary>
+    /// https://tc39.es/ecma262/#sec-getexportednames
+    /// </summary>
+    public List<string> GetExportedNames(List<JsModule> 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<string>();
+        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;
+    }
+
+    /// <summary>
+    /// https://tc39.es/ecma262/#sec-resolveexport
+    /// </summary>
+    internal ResolvedBinding ResolveExport(string exportName, List<ExportResolveSetItem> 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;
+    }
+
+    /// <summary>
+    /// https://tc39.es/ecma262/#sec-moduledeclarationlinking
+    /// </summary>
+    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<JsModule>();
+
+        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");
+        }
+    }
+
+    /// <summary>
+    /// https://tc39.es/ecma262/#sec-moduleevaluation
+    /// </summary>
+    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<JsModule>();
+        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(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);
+            }
+
+            if (stack.Any())
+            {
+                ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state");
+            }
+        }
+
+        return capability.PromiseInstance;
+
+    }
+
+    /// <summary>
+    /// https://tc39.es/ecma262/#sec-InnerModuleLinking
+    /// </summary>
+    private int Link(JsModule module, Stack<JsModule> 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;
+
+    }
+
+    /// <summary>
+    /// https://tc39.es/ecma262/#sec-innermoduleevaluation
+    /// </summary>
+    private Completion Evaluate(JsModule module, Stack<JsModule> 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);
+
+    }
+
+    /// <summary>
+    /// https://tc39.es/ecma262/#sec-source-text-module-record-initialize-environment
+    /// </summary>
+    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<string>();
+        if(varDeclarations != null)
+        {
+            var boundNames = new List<string>();
+            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<string>();
+            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();
+    }
+
+    /// <summary>
+    /// https://tc39.es/ecma262/#sec-source-text-module-record-execute-module
+    /// </summary>
+    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;
+        }
+    }
+
+    /// <summary>
+    /// https://tc39.es/ecma262/#sec-execute-async-module
+    /// </summary>
+    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);
+
+    }
+
+    /// <summary>
+    /// https://tc39.es/ecma262/#sec-gather-available-ancestors
+    /// </summary>
+    private void GatherAvailableAncestors(List<JsModule> 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);
+                    }
+                }
+            }
+        }
+    }
+
+    /// <summary>
+    /// https://tc39.es/ecma262/#sec-async-module-execution-fulfilled
+    /// </summary>
+    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);
+        }
+
+        var execList = new List<JsModule>();
+        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);
+                    }
+                }
+            }
+        }
+
+        return Undefined;
+    }
+
+    /// <summary>
+    /// https://tc39.es/ecma262/#sec-async-module-execution-rejected
+    /// </summary>
+    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(error);
+        }
+
+
+        return Undefined;
+    }
+
+    public override bool Equals(JsValue other)
+    {
+        return false;
+    }
+
+    public override object ToObject()
+    {
+        ExceptionHelper.ThrowNotSupportedException();
+        return null;
+    }
+}

+ 154 - 0
Jint/Runtime/Modules/ModuleNamespace.cs

@@ -0,0 +1,154 @@
+using System.Collections.Generic;
+using Jint.Native;
+using Jint.Native.Object;
+using Jint.Runtime.Descriptors;
+
+namespace Jint.Runtime.Modules;
+
+/// <summary>
+/// https://tc39.es/ecma262/#sec-module-namespace-exotic-objects
+/// </summary>
+internal sealed class ModuleNamespace : ObjectInstance
+{
+    private readonly JsModule _module;
+    private readonly HashSet<string> _exports;
+
+    public ModuleNamespace(Engine engine, JsModule module, List<string> exports) : base(engine)
+    {
+        _module = module;
+        exports.Sort();
+        _exports = new HashSet<string>(exports);
+    }
+
+    protected internal override ObjectInstance GetPrototypeOf() => null;
+
+    public override bool SetPrototypeOf(JsValue value) => SetImmutablePrototype(value);
+
+    private bool SetImmutablePrototype(JsValue value)
+    {
+        var current = GetPrototypeOf();
+        return SameValue(value, current ?? Null);
+    }
+
+    public override bool Extensible => false;
+
+    public override JsValue PreventExtensions() => JsBoolean.True;
+
+    public override PropertyDescriptor GetOwnProperty(JsValue property)
+    {
+        if (property.IsSymbol())
+        {
+            return base.GetOwnProperty(property);
+        }
+
+        var p = TypeConverter.ToString(property);
+
+        if (!_exports.Contains(p))
+        {
+            return PropertyDescriptor.Undefined;
+        }
+
+        var value = Get(property);
+        return new PropertyDescriptor(value, true, true, false);
+    }
+
+    public override bool DefineOwnProperty(JsValue property, PropertyDescriptor desc)
+    {
+        if (property.IsSymbol())
+        {
+            return base.DefineOwnProperty(property, desc);
+        }
+
+        var current = GetOwnProperty(property);
+
+        if (current == PropertyDescriptor.Undefined)
+        {
+            return false;
+        }
+
+        if (desc.Configurable || desc.Enumerable || desc.IsAccessorDescriptor() || !desc.Writable)
+        {
+            return false;
+        }
+
+        if (desc.Value is not null)
+        {
+            return SameValue(desc.Value, current.Value);
+        }
+
+        return true;
+    }
+
+    public override bool HasProperty(JsValue property)
+    {
+        if (property.IsSymbol())
+        {
+            return base.HasProperty(property);
+        }
+
+        var p = TypeConverter.ToString(property);
+        return _exports.Contains(p);
+    }
+
+    public override JsValue Get(JsValue property, JsValue receiver)
+    {
+        if (property.IsSymbol())
+        {
+            return base.Get(property, receiver);
+        }
+
+        var p = TypeConverter.ToString(property);
+
+        if (!_exports.Contains(p))
+        {
+            return Undefined;
+        }
+
+        var m = _module;
+        var binding = m.ResolveExport(p);
+        var targetModule = binding.Module;
+
+        if (binding.BindingName == "*namespace*")
+        {
+            return JsModule.GetModuleNamespace(targetModule);
+        }
+
+        var targetEnv = targetModule._environment;
+        if (targetEnv is null)
+        {
+            ExceptionHelper.ThrowReferenceError(_engine.Realm, "environment");
+        }
+
+        return targetEnv.GetBindingValue(binding.BindingName, true);
+    }
+
+    public override bool Set(JsValue property, JsValue value, JsValue receiver)
+    {
+        return false;
+    }
+
+    public override bool Delete(JsValue property)
+    {
+        if (property.IsSymbol())
+        {
+            return base.Delete(property);
+        }
+
+        var p = TypeConverter.ToString(property);
+        return !_exports.Contains(p);
+    }
+
+    public override List<JsValue> GetOwnPropertyKeys(Types types = Types.String | Types.Symbol)
+    {
+        var keys = base.GetOwnPropertyKeys(types);
+        if ((types & Types.String) != 0)
+        {
+            foreach (var export in _exports)
+            {
+                keys.Add(export);
+            }
+        }
+
+        return keys;
+    }
+}

+ 11 - 0
Jint/Runtime/Modules/ModuleStatus.cs

@@ -0,0 +1,11 @@
+namespace Jint.Runtime.Modules;
+
+internal enum ModuleStatus
+{
+    Unlinked,
+    Linking,
+    Linked,
+    Evaluating,
+    EvaluatingAsync,
+    Evaluated
+}

+ 1 - 0
Jint/Runtime/TypeConverter.cs

@@ -54,6 +54,7 @@ namespace Jint.Runtime
         // internal usage
         ObjectEnvironmentRecord = 512,
         RequiresCloning = 1024,
+        Module = 2048,
 
         Primitive = Boolean | String | Number | Integer | BigInt | Symbol,
         InternalFlags = ObjectEnvironmentRecord | RequiresCloning

+ 1 - 1
README.md

@@ -36,7 +36,7 @@ The entire execution engine was rebuild with performance in mind, in many cases
 - ✔ Template strings
 - ✔ Lexical scoping of variables (let and const)
 - ✔ Map and Set
--  Modules and module loaders
+-  Modules and module loaders
 - ✔ Promises (Experimental, API is unstable)
 - ✔ Reflect
 - ✔ Proxies