Skip to content

Commit

Permalink
Module loading support (#990)
Browse files Browse the repository at this point in the history
Co-authored-by: lph <philipp.luethi@sva-ag.ch>
Co-authored-by: Marko Lahma <marko.lahma@gmail.com>
Co-authored-by: Sébastien Ros <sebastienros@gmail.com>
  • Loading branch information
4 people committed Dec 23, 2021
1 parent 9d84326 commit 0c223f4
Show file tree
Hide file tree
Showing 22 changed files with 1,860 additions and 38 deletions.
51 changes: 51 additions & 0 deletions Jint.Tests.Test262/Language/ModuleTestHost.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
75 changes: 58 additions & 17 deletions Jint.Tests.Test262/Language/ModuleTests.cs
Original file line number Diff line number Diff line change
@@ -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 changes: 19 additions & 0 deletions Jint.Tests.Test262/Test262Test.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,13 @@ public static IEnumerable<object[]> SourceFiles(string pathPrefix, bool skipped)

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);

Expand Down Expand Up @@ -288,6 +295,18 @@ public static IEnumerable<object[]> SourceFiles(string pathPrefix, bool skipped)
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;
}
}
}
Expand Down
33 changes: 33 additions & 0 deletions Jint/Engine.Modules.cs
Original file line number Diff line number Diff line change
@@ -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 changes: 127 additions & 0 deletions Jint/EsprimaExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Jint.Runtime.Environments;
using Jint.Runtime.Interpreter;
using Jint.Runtime.Interpreter.Expressions;
using Jint.Runtime.Modules;

namespace Jint
{
Expand Down Expand Up @@ -128,6 +129,7 @@ internal static string LiteralKeyToString(Literal literal)
{
return TypeConverter.ToString(d);
}

return literal.Value as string ?? Convert.ToString(literal.Value, provider: null);
}

Expand Down Expand Up @@ -204,6 +206,7 @@ internal static void GetBoundNames(this Node? parameter, List<string> target)
parameter = assignmentPattern.Left;
continue;
}

break;
}
}
Expand Down Expand Up @@ -256,6 +259,130 @@ internal static Record DefineMethod(this ClassProperty m, ObjectInstance obj, Ob
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);
}
}

0 comments on commit 0c223f4

Please sign in to comment.