Skip to content

Commit

Permalink
Implement JSON modules (#1711)
Browse files Browse the repository at this point in the history
  • Loading branch information
lahma committed Jan 1, 2024
1 parent 78b168c commit 7678879
Show file tree
Hide file tree
Showing 20 changed files with 316 additions and 196 deletions.
1 change: 0 additions & 1 deletion Jint.Tests.Test262/Test262Harness.settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"generators",
"import-assertions",
"iterator-helpers",
"json-modules",
"regexp-duplicate-named-groups",
"regexp-lookbehind",
"regexp-unicode-property-escapes",
Expand Down
44 changes: 10 additions & 34 deletions Jint.Tests.Test262/Test262ModuleLoader.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
#nullable enable

using Esprima;
using Esprima.Ast;
using Jint.Runtime;
using Jint.Runtime.Modules;
using Zio;

namespace Jint.Tests.Test262;

internal sealed class Test262ModuleLoader : IModuleLoader
internal sealed class Test262ModuleLoader : ModuleLoader
{
private readonly IFileSystem _fileSystem;
private readonly string _basePath;
Expand All @@ -19,44 +17,22 @@ public Test262ModuleLoader(IFileSystem fileSystem, string basePath)
_basePath = "/test/" + basePath.TrimStart('\\').TrimStart('/');
}

public ResolvedSpecifier Resolve(string? referencingModuleLocation, string specifier)
public override ResolvedSpecifier Resolve(string? referencingModuleLocation, ModuleRequest moduleRequest)
{
return new ResolvedSpecifier(referencingModuleLocation ?? "", specifier ?? "", null, SpecifierType.Bare);
return new ResolvedSpecifier(moduleRequest, moduleRequest.Specifier, null, SpecifierType.Bare);
}

public Module LoadModule(Engine engine, ResolvedSpecifier resolved)
protected override string LoadModuleContents(Engine engine, ResolvedSpecifier resolved)
{
Module module;
try
lock (_fileSystem)
{
string code;
lock (_fileSystem)
var fileName = Path.Combine(_basePath, resolved.Key).Replace('\\', '/');
if (!_fileSystem.FileExists(fileName))
{
var fileName = Path.Combine(_basePath, resolved.Key).Replace('\\', '/');
using var stream = new StreamReader(_fileSystem.OpenFile(fileName, FileMode.Open, FileAccess.Read));
code = stream.ReadToEnd();
ExceptionHelper.ThrowModuleResolutionException("Module Not Found", resolved.ModuleRequest.Specifier, parent: null, fileName);
}

var parserOptions = new ParserOptions
{
RegExpParseMode = RegExpParseMode.AdaptToInterpreted,
Tolerant = true
};

module = new JavaScriptParser(parserOptions).ParseModule(code, source: resolved.Uri?.LocalPath!);
}
catch (ParserException ex)
{
ExceptionHelper.ThrowSyntaxError(engine.Realm, $"Error while loading module: error in module '{resolved.Uri?.LocalPath}': {ex.Error}");
module = null;
}
catch (Exception ex)
{
var message = $"Could not load module {resolved.Uri?.LocalPath}: {ex.Message}";
ExceptionHelper.ThrowJavaScriptException(engine, message, (Location) default);
module = null;
using var stream = new StreamReader(_fileSystem.OpenFile(fileName, FileMode.Open, FileAccess.Read));
return stream.ReadToEnd();
}

return module;
}
}
18 changes: 9 additions & 9 deletions Jint.Tests/Runtime/Modules/DefaultModuleLoaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@ namespace Jint.Tests.Runtime.Modules;
public class DefaultModuleLoaderTests
{
[Theory]
[InlineData("./other.js", @"file:///project/folder/other.js")]
[InlineData("../model/other.js", @"file:///project/model/other.js")]
[InlineData("/project/model/other.js", @"file:///project/model/other.js")]
[InlineData("file:///project/model/other.js", @"file:///project/model/other.js")]
[InlineData("./other.js", "file:///project/folder/other.js")]
[InlineData("../model/other.js", "file:///project/model/other.js")]
[InlineData("/project/model/other.js", "file:///project/model/other.js")]
[InlineData("file:///project/model/other.js", "file:///project/model/other.js")]
public void ShouldResolveRelativePaths(string specifier, string expectedUri)
{
var resolver = new DefaultModuleLoader("file:///project");

var resolved = resolver.Resolve("file:///project/folder/script.js", specifier);
var resolved = resolver.Resolve("file:///project/folder/script.js", new ModuleRequest(specifier, []));

Assert.Equal(specifier, resolved.Specifier);
Assert.Equal(specifier, resolved.ModuleRequest.Specifier);
Assert.Equal(expectedUri, resolved.Key);
Assert.Equal(expectedUri, resolved.Uri?.AbsoluteUri);
Assert.Equal(SpecifierType.RelativeOrAbsolute, resolved.Type);
Expand All @@ -30,7 +30,7 @@ public void ShouldRejectPathsOutsideOfBasePath(string specifier)
{
var resolver = new DefaultModuleLoader("file:///project");

var exc = Assert.Throws<ModuleResolutionException>(() => resolver.Resolve("file:///project/folder/script.js", specifier));
var exc = Assert.Throws<ModuleResolutionException>(() => resolver.Resolve("file:///project/folder/script.js", new ModuleRequest(specifier, [])));
Assert.StartsWith(exc.ResolverAlgorithmError, "Unauthorized Module Path");
Assert.StartsWith(exc.Specifier, specifier);
}
Expand All @@ -40,9 +40,9 @@ public void ShouldResolveBareSpecifiers()
{
var resolver = new DefaultModuleLoader("/");

var resolved = resolver.Resolve(null, "my-module");
var resolved = resolver.Resolve(null, new ModuleRequest("my-module", []));

Assert.Equal("my-module", resolved.Specifier);
Assert.Equal("my-module", resolved.ModuleRequest.Specifier);
Assert.Equal("my-module", resolved.Key);
Assert.Equal(null, resolved.Uri?.AbsoluteUri);
Assert.Equal(SpecifierType.Bare, resolved.Type);
Expand Down
29 changes: 17 additions & 12 deletions Jint/Engine.Modules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ public partial class Engine
return _executionContexts?.GetActiveScriptOrModule();
}

internal ModuleRecord LoadModule(string? referencingModuleLocation, string specifier)
internal ModuleRecord LoadModule(string? referencingModuleLocation, ModuleRequest request)
{
var moduleResolution = ModuleLoader.Resolve(referencingModuleLocation, specifier);
var specifier = request.Specifier;
var moduleResolution = ModuleLoader.Resolve(referencingModuleLocation, request);

if (_modules.TryGetValue(moduleResolution.Key, out var module))
{
Expand Down Expand Up @@ -59,10 +60,9 @@ private BuilderModuleRecord LoadFromBuilder(string specifier, ModuleBuilder modu
return module;
}

private SourceTextModuleRecord LoadFromModuleLoader(ResolvedSpecifier moduleResolution)
private ModuleRecord LoadFromModuleLoader(ResolvedSpecifier moduleResolution)
{
var parsedModule = ModuleLoader.LoadModule(this, moduleResolution);
var module = new SourceTextModuleRecord(this, Realm, parsedModule, moduleResolution.Uri?.LocalPath, false);
var module = ModuleLoader.LoadModule(this, moduleResolution);
_modules[moduleResolution.Key] = module;
return module;
}
Expand All @@ -88,30 +88,35 @@ public void AddModule(string specifier, ModuleBuilder moduleBuilder)

public ObjectInstance ImportModule(string specifier)
{
return ImportModule(specifier, null);
return ImportModule(specifier, referencingModuleLocation: null);
}

internal ObjectInstance ImportModule(string specifier, string? referencingModuleLocation)
{
var moduleResolution = ModuleLoader.Resolve(referencingModuleLocation, specifier);
return ImportModule(new ModuleRequest(specifier, []), referencingModuleLocation);
}

internal ObjectInstance ImportModule(ModuleRequest request, string? referencingModuleLocation)
{
var moduleResolution = ModuleLoader.Resolve(referencingModuleLocation, request);

if (!_modules.TryGetValue(moduleResolution.Key, out var module))
{
module = LoadModule(null, specifier);
module = LoadModule(referencingModuleLocation: null, request);
}

if (module is not CyclicModuleRecord cyclicModule)
{
LinkModule(specifier, module);
EvaluateModule(specifier, module);
LinkModule(request.Specifier, module);
EvaluateModule(request.Specifier, module);
}
else if (cyclicModule.Status == ModuleStatus.Unlinked)
{
LinkModule(specifier, cyclicModule);
LinkModule(request.Specifier, cyclicModule);

if (cyclicModule.Status == ModuleStatus.Linked)
{
ExecuteWithConstraints(true, () => EvaluateModule(specifier, cyclicModule));
ExecuteWithConstraints(true, () => EvaluateModule(request.Specifier, cyclicModule));
}

if (cyclicModule.Status != ModuleStatus.Evaluated)
Expand Down
41 changes: 31 additions & 10 deletions Jint/EsprimaExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -330,25 +330,42 @@ internal static void GetImportEntries(this ImportDeclaration import, List<Import
{
var source = import.Source.StringValue!;
var specifiers = import.Specifiers;
requestedModules.Add(new ModuleRequest(source, []));
var attributes = GetAttributes(import.Attributes);
requestedModules.Add(new ModuleRequest(source, attributes));

foreach (var specifier in specifiers)
{
switch (specifier)
{
case ImportNamespaceSpecifier namespaceSpecifier:
importEntries.Add(new ImportEntry(source, "*", namespaceSpecifier.Local.GetModuleKey()));
importEntries.Add(new ImportEntry(new ModuleRequest(source, attributes), "*", namespaceSpecifier.Local.GetModuleKey()));
break;
case ImportSpecifier importSpecifier:
importEntries.Add(new ImportEntry(source, importSpecifier.Imported.GetModuleKey(), importSpecifier.Local.GetModuleKey()));
importEntries.Add(new ImportEntry(new ModuleRequest(source, attributes), importSpecifier.Imported.GetModuleKey(), importSpecifier.Local.GetModuleKey()!));
break;
case ImportDefaultSpecifier defaultSpecifier:
importEntries.Add(new ImportEntry(source, "default", defaultSpecifier.Local.GetModuleKey()));
importEntries.Add(new ImportEntry(new ModuleRequest(source, attributes), "default", defaultSpecifier.Local.GetModuleKey()));
break;
}
}
}

private static ModuleImportAttribute[] GetAttributes(in NodeList<ImportAttribute> importAttributes)
{
if (importAttributes.Count == 0)
{
return Array.Empty<ModuleImportAttribute>();
}

var attributes = new ModuleImportAttribute[importAttributes.Count];
for (var i = 0; i < importAttributes.Count; i++)
{
var attribute = importAttributes[i];
attributes[i] = new ModuleImportAttribute(attribute.Key.ToString(), attribute.Value.StringValue!);
}
return attributes;
}

internal static void GetExportEntries(this ExportDeclaration export, List<ExportEntry> exportEntries, HashSet<ModuleRequest> requestedModules)
{
switch (export)
Expand All @@ -359,13 +376,17 @@ internal static void GetExportEntries(this ExportDeclaration export, List<Export
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(new ModuleRequest(allDeclaration.Source.StringValue!, []));
exportEntries.Add(new(allDeclaration.Exported?.GetModuleKey(), allDeclaration.Source.StringValue, "*", null));
exportEntries.Add(new(allDeclaration.Exported?.GetModuleKey(), new ModuleRequest(allDeclaration.Source.StringValue!, []), "*", null));
break;
case ExportNamedDeclaration namedDeclaration:
ref readonly var specifiers = ref namedDeclaration.Specifiers;
if (specifiers.Count == 0)
{
GetExportEntries(false, namedDeclaration.Declaration!, exportEntries, namedDeclaration.Source?.StringValue);
ModuleRequest? moduleRequest = namedDeclaration.Source != null
? new ModuleRequest(namedDeclaration.Source?.StringValue!, [])
: null;

GetExportEntries(false, namedDeclaration.Declaration!, exportEntries, moduleRequest);
}
else
{
Expand All @@ -374,7 +395,7 @@ internal static void GetExportEntries(this ExportDeclaration export, List<Export
var specifier = specifiers[i];
if (namedDeclaration.Source != null)
{
exportEntries.Add(new(specifier.Exported.GetModuleKey(), namedDeclaration.Source.StringValue, specifier.Local.GetModuleKey(), null));
exportEntries.Add(new(specifier.Exported.GetModuleKey(), new ModuleRequest(namedDeclaration.Source.StringValue!, []), specifier.Local.GetModuleKey(), null));
}
else
{
Expand All @@ -392,7 +413,7 @@ internal static void GetExportEntries(this ExportDeclaration export, List<Export
}
}

private static void GetExportEntries(bool defaultExport, StatementListItem declaration, List<ExportEntry> exportEntries, string? moduleRequest = null)
private static void GetExportEntries(bool defaultExport, StatementListItem declaration, List<ExportEntry> exportEntries, ModuleRequest? moduleRequest = null)
{
var names = GetExportNames(declaration);

Expand Down Expand Up @@ -444,9 +465,9 @@ private static List<string> GetExportNames(StatementListItem declaration)
return result;
}

private static string? GetModuleKey(this Expression expression)
private static string GetModuleKey(this Expression expression)
{
return (expression as Identifier)?.Name ?? (expression as Literal)?.StringValue;
return (expression as Identifier)?.Name ?? (expression as Literal)!.StringValue!;
}

internal readonly record struct Record(JsValue Key, ScriptFunctionInstance Closure);
Expand Down
2 changes: 1 addition & 1 deletion Jint/HoistingScope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ public static HoistingScope GetFunctionLevelDeclarations(bool strict, IFunction

importEntries = treeWalker._importEntries;
requestedModules = treeWalker._requestedModules ?? [];
var importedBoundNames = new HashSet<string>(StringComparer.Ordinal);
var importedBoundNames = new HashSet<string?>(StringComparer.Ordinal);

if (importEntries != null)
{
Expand Down
2 changes: 1 addition & 1 deletion Jint/Native/ShadowRealm/ShadowRealm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ private static void CopyNameAndLength(WrappedFunction f, ObjectInstance target,
// 4. If runningContext is not already suspended, suspend runningContext.

_engine.EnterExecutionContext(_executionContext);
_engine._host.LoadImportedModule(null, new ModuleRequest(specifierString, new List<KeyValuePair<string, JsValue>>()), innerCapability);
_engine._host.LoadImportedModule(null, new ModuleRequest(specifierString, []), innerCapability);
_engine.LeaveExecutionContext();

var onFulfilled = new StepsFunction(_engine, callerRealm, exportNameString);
Expand Down
2 changes: 1 addition & 1 deletion Jint/Runtime/Environments/ModuleEnvironmentRecord.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ internal override bool TryGetBinding(BindingName name, bool strict, out Binding
{
if (_importBindings.TryGetValue(name.Key, out var indirectBinding))
{
value = indirectBinding.Module._environment.GetBindingValue(indirectBinding.BindingName, true);
value = indirectBinding.Module._environment.GetBindingValue(indirectBinding.BindingName, strict: true);
binding = new(value, canBeDeleted: false, mutable: false, strict: true);
return true;
}
Expand Down
6 changes: 3 additions & 3 deletions Jint/Runtime/Host.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,9 @@ public virtual void EnsureCanCompileStrings(Realm callerRealm, Realm evalRealm)
/// <summary>
/// https://tc39.es/ecma262/#sec-GetImportedModule
/// </summary>
internal virtual ModuleRecord GetImportedModule(IScriptOrModule? referrer, string specifier)
internal virtual ModuleRecord GetImportedModule(IScriptOrModule? referrer, ModuleRequest request)
{
return Engine.LoadModule(referrer?.Location, specifier);
return Engine.LoadModule(referrer?.Location, request);
}

/// <summary>
Expand Down Expand Up @@ -151,7 +151,7 @@ internal virtual void FinishLoadingImportedModule(IScriptOrModule? referrer, Mod
{
var onFulfilled = new ClrFunctionInstance(Engine, "", (thisObj, args) =>
{
var moduleRecord = GetImportedModule(referrer, moduleRequest.Specifier);
var moduleRecord = GetImportedModule(referrer, moduleRequest);
try
{
var ns = ModuleRecord.GetModuleNamespace(moduleRecord);
Expand Down

0 comments on commit 7678879

Please sign in to comment.