Bladeren bron

Prevent users from passing arbitrary parser options to Jint (#1831)

* Reduce parsing options for end users (replace ParserOptions with Script/ModuleParseOptions in the public API)
* Enforce that pre-parsed scripts can be passed to Jint only if they were created using Engine.PrepareScript/PrepareModule
* Avoid copying beefy ExecutionContext struct where possible
* Propagate ParserOptions via ExecutionContext
* Introduce alias for Esprima.Ast.Module
adams85 1 jaar geleden
bovenliggende
commit
e1e2d6d580
45 gewijzigde bestanden met toevoegingen van 754 en 229 verwijderingen
  1. 1 0
      Directory.Build.props
  2. 1 1
      Directory.Packages.props
  3. 1 1
      Jint.Benchmark/DromaeoBenchmark.cs
  4. 2 3
      Jint.Benchmark/EngineComparisonBenchmark.cs
  5. 4 5
      Jint.Benchmark/EngineConstructionBenchmark.cs
  6. 1 1
      Jint.Benchmark/ObjectAccessBenchmark.cs
  7. 1 1
      Jint.Benchmark/ShadowRealmBenchmark.cs
  8. 1 1
      Jint.Benchmark/SingleScriptBenchmark.cs
  9. 3 3
      Jint.Repl/Program.cs
  10. 2 6
      Jint.Tests.CommonScripts/ConcurrencyTest.cs
  11. 12 8
      Jint.Tests.PublicInterface/ModuleLoaderTests.cs
  12. 1 2
      Jint.Tests.PublicInterface/ShadowRealmTests.cs
  13. 1 1
      Jint.Tests.Test262/State.cs
  14. 10 4
      Jint.Tests.Test262/Test262Test.cs
  15. 6 6
      Jint.Tests/Runtime/EngineTests.ScriptPreparation.cs
  16. 7 7
      Jint.Tests/Runtime/EngineTests.cs
  17. 8 8
      Jint.Tests/Runtime/ErrorTests.cs
  18. 215 0
      Jint.Tests/Runtime/ParserOptionsPropagationTests.cs
  19. 23 32
      Jint/Engine.Ast.cs
  20. 18 0
      Jint/Engine.Defaults.cs
  21. 1 1
      Jint/Engine.Modules.cs
  22. 58 43
      Jint/Engine.cs
  23. 4 5
      Jint/HoistingScope.cs
  24. 6 4
      Jint/Native/Function/ClassDefinition.cs
  25. 6 4
      Jint/Native/Function/EvalFunction.cs
  26. 4 3
      Jint/Native/Function/Function.cs
  27. 5 4
      Jint/Native/Function/FunctionInstance.Dynamic.cs
  28. 2 2
      Jint/Native/Function/ScriptFunction.cs
  29. 1 1
      Jint/Native/Generator/GeneratorInstance.cs
  30. 0 5
      Jint/Native/Json/JsonParser.cs
  31. 2 1
      Jint/Native/RegExp/RegExpConstructor.cs
  32. 33 19
      Jint/Native/ShadowRealm/ShadowRealm.cs
  33. 8 1
      Jint/Native/ShadowRealm/ShadowRealmPrototype.cs
  34. 116 0
      Jint/ParsingOptions.cs
  35. 51 0
      Jint/PreparationOptions.cs
  36. 20 0
      Jint/Prepared.cs
  37. 13 8
      Jint/Runtime/Debugger/DebugHandler.cs
  38. 4 1
      Jint/Runtime/Environments/ExecutionContext.cs
  39. 12 0
      Jint/Runtime/ExceptionHelper.cs
  40. 26 6
      Jint/Runtime/ExecutionContextStack.cs
  41. 1 1
      Jint/Runtime/Modules/BuilderModule.cs
  42. 33 15
      Jint/Runtime/Modules/ModuleBuilder.cs
  43. 15 8
      Jint/Runtime/Modules/ModuleFactory.cs
  44. 9 5
      Jint/Runtime/Modules/SourceTextModule.cs
  45. 6 2
      Jint/Runtime/Modules/SyntheticModule.cs

+ 1 - 0
Directory.Build.props

@@ -32,6 +32,7 @@
     <Using Include="Esprima.Utils" />
 
     <Using Include="Esprima.Ast.BindingPattern" Alias="DestructuringPattern" />
+    <Using Include="Esprima.Ast.Module" Alias="AstModule" />
     <Using Include="Esprima.Ast.Nodes" Alias="NodeType" />
     <Using Include="Esprima.JavaScriptParser" Alias="Parser" />
     <Using Include="Esprima.Location" Alias="SourceLocation" />

+ 1 - 1
Directory.Packages.props

@@ -6,7 +6,7 @@
   <ItemGroup>
     <PackageVersion Include="BenchmarkDotNet" Version="0.13.12" />
     <PackageVersion Include="BenchmarkDotNet.TestAdapter" Version="0.13.12" />
-    <PackageVersion Include="Esprima" Version="3.0.4" />
+    <PackageVersion Include="Esprima" Version="3.0.5" />
     <PackageVersion Include="FluentAssertions" Version="6.12.0" />
     <PackageVersion Include="Flurl.Http.Signed" Version="3.2.4" />
     <PackageVersion Include="Jurassic" Version="3.2.7" />

+ 1 - 1
Jint.Benchmark/DromaeoBenchmark.cs

@@ -15,7 +15,7 @@ public class DromaeoBenchmark
         {"dromaeo-string-base64", null}
     };
 
-    private readonly Dictionary<string, Script> _prepared = new();
+    private readonly Dictionary<string, Prepared<Script>> _prepared = new();
 
     private Engine engine;
 

+ 2 - 3
Jint.Benchmark/EngineComparisonBenchmark.cs

@@ -12,7 +12,7 @@ namespace Jint.Benchmark;
 [BenchmarkCategory("EngineComparison")]
 public class EngineComparisonBenchmark
 {
-    private static readonly Dictionary<string, Script> _parsedScripts = new();
+    private static readonly Dictionary<string, Prepared<Script>> _parsedScripts = new();
 
     private static readonly Dictionary<string, string> _files = new()
     {
@@ -38,7 +38,6 @@ public class EngineComparisonBenchmark
     [GlobalSetup]
     public void Setup()
     {
-        var parser = new Parser();
         foreach (var fileName in _files.Keys.ToList())
         {
             var script = File.ReadAllText($"Scripts/{fileName}.js");
@@ -47,7 +46,7 @@ public class EngineComparisonBenchmark
                 script = _dromaeoHelpers + Environment.NewLine + Environment.NewLine + script;
             }
             _files[fileName] = script;
-            _parsedScripts[fileName] = parser.ParseScript(script, strict: true);
+            _parsedScripts[fileName] = Engine.PrepareScript(script, strict: true);
         }
     }
 

+ 4 - 5
Jint.Benchmark/EngineConstructionBenchmark.cs

@@ -6,15 +6,14 @@ namespace Jint.Benchmark;
 [MemoryDiagnoser]
 public class EngineConstructionBenchmark
 {
-    private Script _program;
-    private Script _simple;
+    private Prepared<Script> _program;
+    private Prepared<Script> _simple;
 
     [GlobalSetup]
     public void GlobalSetup()
     {
-        var parser = new Parser();
-        _program = parser.ParseScript("([].length + ''.length)");
-        _simple = parser.ParseScript("1");
+        _program = Engine.PrepareScript("([].length + ''.length)");
+        _simple = Engine.PrepareScript("1");
         new Engine().Evaluate(_program);
     }
 

+ 1 - 1
Jint.Benchmark/ObjectAccessBenchmark.cs

@@ -5,7 +5,7 @@ namespace Jint.Benchmark;
 [MemoryDiagnoser]
 public class ObjectAccessBenchmark
 {
-    private readonly Script _script;
+    private readonly Prepared<Script> _script;
 
     public ObjectAccessBenchmark()
     {

+ 1 - 1
Jint.Benchmark/ShadowRealmBenchmark.cs

@@ -11,7 +11,7 @@ public class ShadowRealmBenchmark
 ";
 
     private Engine engine;
-    private Script parsedScript;
+    private Prepared<Script> parsedScript;
 
     [GlobalSetup]
     public void Setup()

+ 1 - 1
Jint.Benchmark/SingleScriptBenchmark.cs

@@ -6,7 +6,7 @@ namespace Jint.Benchmark;
 public abstract class SingleScriptBenchmark
 {
     private string _script;
-    private Script _parsedScript;
+    private Prepared<Script> _parsedScript;
 
     protected abstract string FileName { get; }
 

+ 3 - 3
Jint.Repl/Program.cs

@@ -40,10 +40,10 @@ Console.WriteLine("Type 'exit' to leave, " +
 Console.WriteLine();
 
 var defaultColor = Console.ForegroundColor;
-var parserOptions = new ParserOptions
+var parsingOptions = new ScriptParsingOptions
 {
     Tolerant = true,
-    RegExpParseMode = RegExpParseMode.AdaptToInterpreted
+    CompileRegex = false,
 };
 
 var serializer = new JsonSerializer(engine);
@@ -60,7 +60,7 @@ while (true)
 
     try
     {
-        var result = engine.Evaluate(input, parserOptions);
+        var result = engine.Evaluate(input, parsingOptions);
         JsValue str = result;
         if (!result.IsPrimitive() && result is not IJsPrimitive)
         {

+ 2 - 6
Jint.Tests.CommonScripts/ConcurrencyTest.cs

@@ -4,14 +4,10 @@ namespace Jint.Tests.CommonScripts;
 public class ConcurrencyTest
 {
     [Test]
-    [TestCase(true)]
-    [TestCase(false)]
-    public void ConcurrentEnginesCanUseSameAst(bool prepared)
+    public void ConcurrentEnginesCanUseSameAst()
     {
         var scriptContents = SunSpiderTests.GetEmbeddedFile("babel-standalone.js");
-        var script = prepared
-            ? Engine.PrepareScript(scriptContents)
-            : new Parser().ParseScript(scriptContents);
+        var script = Engine.PrepareScript(scriptContents);
 
         Parallel.ForEach(Enumerable.Range(0, 3), x =>
         {

+ 12 - 8
Jint.Tests.PublicInterface/ModuleLoaderTests.cs

@@ -51,7 +51,7 @@ public class ModuleLoaderTests
     public void CustomModuleLoaderWithCachingSupport()
     {
         // Different engines use the same module loader.
-        // The module loader caches the parsed Esprima.Ast.Module
+        // The module loader caches the parsed Module
         // which allows to re-use these for different engine runs.
         var store = new ModuleStore(new Dictionary<string, string>()
         {
@@ -177,7 +177,7 @@ public class ModuleLoaderTests
     /// <summary>
     /// <para>
     /// A simple <see cref="IModuleLoader"/> implementation which will
-    /// re-use prepared <see cref="Esprima.Ast.Module"/> or <see cref="JsValue"/> modules to
+    /// re-use prepared <see cref="AstModule"/> or <see cref="JsValue"/> modules to
     /// produce <see cref="Jint.Runtime.Modules.Module"/>.
     /// </para>
     /// <para>
@@ -231,20 +231,24 @@ public class ModuleLoaderTests
 
         private sealed class ParsedModule
         {
-            private readonly Esprima.Ast.Module? _textModule;
+            private readonly Prepared<AstModule>? _textModule;
             private readonly (JsValue Json, string Location)? _jsonModule;
 
-            private ParsedModule(Esprima.Ast.Module? textModule, (JsValue Json, string Location)? jsonModule)
+            private ParsedModule(in Prepared<AstModule> textModule)
             {
                 _textModule = textModule;
-                _jsonModule = jsonModule;
+            }
+
+            private ParsedModule(JsValue json, string location)
+            {
+                _jsonModule = (json, location);
             }
 
             public static ParsedModule TextModule(string script, string location)
-                => new(Engine.PrepareModule(script, location), null);
+                => new(Engine.PrepareModule(script, location));
 
             public static ParsedModule JsonModule(string json, string location)
-                => new(null, (ParseJson(json), location));
+                => new(ParseJson(json), location);
 
             private static JsValue ParseJson(string json)
             {
@@ -258,7 +262,7 @@ public class ModuleLoaderTests
                 if (_jsonModule is not null)
                     return ModuleFactory.BuildJsonModule(engine, _jsonModule.Value.Json, _jsonModule.Value.Location);
                 if (_textModule is not null)
-                    return ModuleFactory.BuildSourceTextModule(engine, _textModule);
+                    return ModuleFactory.BuildSourceTextModule(engine, _textModule.Value);
                 throw new InvalidOperationException("Unexpected state - no module type available");
             }
         }

+ 1 - 2
Jint.Tests.PublicInterface/ShadowRealmTests.cs

@@ -83,8 +83,7 @@ public class ShadowRealmTests
         var shadowRealm2 = engine.Intrinsics.ShadowRealm.Construct();
         shadowRealm2.SetValue("message", "realm 2");
 
-        var parser = new Parser();
-        var script = parser.ParseScript("(function hello() {return \"hello \" + message})();");
+        var script = Engine.PrepareScript("(function hello() {return \"hello \" + message})();");
 
         // Act & Assert
         Assert.Equal("hello engine", engine.Evaluate(script));

+ 1 - 1
Jint.Tests.Test262/State.cs

@@ -8,5 +8,5 @@ public static partial class State
     /// <summary>
     /// Pre-compiled scripts for faster execution.
     /// </summary>
-    public static readonly Dictionary<string, Script> Sources = new(StringComparer.OrdinalIgnoreCase);
+    public static readonly Dictionary<string, Prepared<Script>> Sources = new(StringComparer.OrdinalIgnoreCase);
 }

+ 10 - 4
Jint.Tests.Test262/Test262Test.cs

@@ -37,9 +37,10 @@ public abstract partial class Test262Test
                     throw new Exception("only script parsing supported");
                 }
 
-                var options = new ParserOptions { RegExpParseMode = RegExpParseMode.AdaptToInterpreted, Tolerant = false };
-                var parser = new Parser(options);
-                var script = parser.ParseScript(args.At(0).AsString());
+                var script = Engine.PrepareScript(args.At(0).AsString(), options: new ScriptPreparationOptions
+                {
+                    ParsingOptions = ScriptParsingOptions.Default with { CompileRegex = false, Tolerant = false },
+                });
 
                 return engine.Evaluate(script);
             }), true, true, true));
@@ -93,7 +94,12 @@ public abstract partial class Test262Test
         }
         else
         {
-            engine.Execute(new Parser().ParseScript(file.Program, source: file.FileName));
+            var script = Engine.PrepareScript(file.Program, source: file.FileName, options: new ScriptPreparationOptions
+            {
+                ParsingOptions = ScriptParsingOptions.Default with { CompileRegex = false, Tolerant = false },
+            });
+
+            engine.Execute(script);
         }
     }
 

+ 6 - 6
Jint.Tests/Runtime/EngineTests.ScriptPreparation.cs

@@ -12,16 +12,16 @@ public partial class EngineTests
     public void ScriptPreparationAcceptsReturnOutsideOfFunctions()
     {
         var preparedScript = Engine.PrepareScript("return 1;");
-        Assert.IsType<ReturnStatement>(preparedScript.Body[0]);
+        Assert.IsType<ReturnStatement>(preparedScript.Program.Body[0]);
     }
 
     [Fact]
     public void CanPreCompileRegex()
     {
         var script = Engine.PrepareScript("var x = /[cgt]/ig; var y = /[cgt]/ig; 'g'.match(x).length;");
-        var declaration = Assert.IsType<VariableDeclaration>(script.Body[0]);
+        var declaration = Assert.IsType<VariableDeclaration>(script.Program.Body[0]);
         var init = Assert.IsType<RegExpLiteral>(declaration.Declarations[0].Init);
-        var regex = Assert.IsType<Regex>(init.AssociatedData);
+        var regex = Assert.IsType<Regex>(init.Value);
         Assert.Equal("[cgt]", regex.ToString());
         Assert.Equal(RegexOptions.Compiled, regex.Options & RegexOptions.Compiled);
 
@@ -32,7 +32,7 @@ public partial class EngineTests
     public void ScriptPreparationFoldsConstants()
     {
         var preparedScript = Engine.PrepareScript("return 1 + 2;");
-        var returnStatement = Assert.IsType<ReturnStatement>(preparedScript.Body[0]);
+        var returnStatement = Assert.IsType<ReturnStatement>(preparedScript.Program.Body[0]);
         var constant = Assert.IsType<JintConstantExpression>(returnStatement.Argument?.AssociatedData);
         Assert.Equal(3, constant.GetValue(null!));
 
@@ -43,7 +43,7 @@ public partial class EngineTests
     public void ScriptPreparationOptimizesNegatingUnaryExpression()
     {
         var preparedScript = Engine.PrepareScript("-1");
-        var expression = Assert.IsType<ExpressionStatement>(preparedScript.Body[0]);
+        var expression = Assert.IsType<ExpressionStatement>(preparedScript.Program.Body[0]);
         var unaryExpression = Assert.IsType<UnaryExpression>(expression.Expression);
         var constant = Assert.IsType<JintConstantExpression>(unaryExpression.AssociatedData);
 
@@ -55,7 +55,7 @@ public partial class EngineTests
     public void ScriptPreparationOptimizesConstantReturn()
     {
         var preparedScript = Engine.PrepareScript("return false;");
-        var statement = Assert.IsType<ReturnStatement>(preparedScript.Body[0]);
+        var statement = Assert.IsType<ReturnStatement>(preparedScript.Program.Body[0]);
         var returnStatement = Assert.IsType<ConstantStatement>(statement.AssociatedData);
 
         var builtStatement = JintStatement.Build(statement);

+ 7 - 7
Jint.Tests/Runtime/EngineTests.cs

@@ -1296,8 +1296,8 @@ var prep = function (fn) { fn(); };
         public void ShouldExecuteKnockoutWithoutErrorWhetherTolerantOrIntolerant()
         {
             var content = GetEmbeddedFile("knockout-3.4.0.js");
-            _engine.Execute(content, new ParserOptions { Tolerant = true });
-            _engine.Execute(content, new ParserOptions { Tolerant = false });
+            _engine.Execute(content, new ScriptParsingOptions { Tolerant = true });
+            _engine.Execute(content, new ScriptParsingOptions { Tolerant = false });
         }
 
         [Fact]
@@ -1314,7 +1314,7 @@ var prep = function (fn) { fn(); };
         {
             var code = "if({ __proto__: [], __proto__:[] } instanceof Array) {}";
 
-            Exception ex = Assert.Throws<ParseErrorException>(() => _engine.Execute(code, new ParserOptions { Tolerant = false }));
+            Exception ex = Assert.Throws<ParseErrorException>(() => _engine.Execute(code, new ScriptParsingOptions { Tolerant = false }));
             Assert.Contains("Duplicate __proto__ fields are not allowed in object literals", ex.Message);
 
             ex = Assert.Throws<JavaScriptException>(() => _engine.Execute($"eval('{code}')"));
@@ -2944,7 +2944,7 @@ x.test = {
         public void ExecuteWithParserOptionsShouldTriggerBeforeEvaluateEvent()
         {
             TestBeforeEvaluateEvent(
-                (engine, code) => engine.Execute(code, ParserOptions.Default),
+                (engine, code) => engine.Execute(code, ScriptParsingOptions.Default),
                 expectedSource: "<anonymous>"
             );
         }
@@ -2953,7 +2953,7 @@ x.test = {
         public void ExecuteWithSourceAndParserOptionsShouldTriggerBeforeEvaluateEvent()
         {
             TestBeforeEvaluateEvent(
-                (engine, code) => engine.Execute(code, "mysource", ParserOptions.Default),
+                (engine, code) => engine.Execute(code, "mysource", ScriptParsingOptions.Default),
                 expectedSource: "mysource"
             );
         }
@@ -2980,7 +2980,7 @@ x.test = {
         public void EvaluateWithParserOptionsShouldTriggerBeforeEvaluateEvent()
         {
             TestBeforeEvaluateEvent(
-                (engine, code) => engine.Evaluate(code, ParserOptions.Default),
+                (engine, code) => engine.Evaluate(code, ScriptParsingOptions.Default),
                 expectedSource: "<anonymous>"
             );
         }
@@ -2989,7 +2989,7 @@ x.test = {
         public void EvaluateWithSourceAndParserOptionsShouldTriggerBeforeEvaluateEvent()
         {
             TestBeforeEvaluateEvent(
-                (engine, code) => engine.Evaluate(code, "mysource", ParserOptions.Default),
+                (engine, code) => engine.Evaluate(code, "mysource", ScriptParsingOptions.Default),
                 expectedSource: "mysource"
             );
         }

+ 8 - 8
Jint.Tests/Runtime/ErrorTests.cs

@@ -294,12 +294,12 @@ var x = b(7);";
     return item;
 })(getItem);";
 
-            var parserOptions = new ParserOptions
+            var parsingOptions = new ScriptParsingOptions
             {
-                RegExpParseMode = RegExpParseMode.AdaptToInterpreted,
+                CompileRegex = false,
                 Tolerant = true
             };
-            var ex = Assert.Throws<JavaScriptException>(() => engine.Execute(script, "get-item.js", parserOptions));
+            var ex = Assert.Throws<JavaScriptException>(() => engine.Execute(script, "get-item.js", parsingOptions));
 
             const string expected = @"Error: Cannot read property '5' of null
    at getItem (items, itemIndex) get-item.js:2:22
@@ -383,11 +383,11 @@ try {
         [Fact]
         public void CallStackWorksWithRecursiveCalls()
         {
-            static ParserOptions CreateParserOptions()
+            static ScriptParsingOptions CreateParsingOptions()
             {
-                return new ParserOptions
+                return new ScriptParsingOptions
                 {
-                    RegExpParseMode = RegExpParseMode.AdaptToInterpreted,
+                    CompileRegex = false,
                     Tolerant = true
                 };
             }
@@ -406,13 +406,13 @@ executeFile(""second-file.js"");",
 nuм -= 3;",
                         _ => throw new FileNotFoundException($"File '{path}' not exist.", path)
                     };
-                    engine.Execute(content, path, CreateParserOptions());
+                    engine.Execute(content, path, CreateParsingOptions());
                 }));
                 engine.Execute(
                     @"var num = 5;
 executeFile(""first-file.js"");",
                     "main-file.js",
-                    CreateParserOptions()
+                    CreateParsingOptions()
                 );
             });
 

+ 215 - 0
Jint.Tests/Runtime/ParserOptionsPropagationTests.cs

@@ -0,0 +1,215 @@
+#nullable enable
+
+using Jint.Native;
+using Jint.Runtime;
+using Jint.Runtime.Modules;
+
+namespace Jint.Tests.Runtime;
+
+public class ParserOptionsPropagationTests
+{
+    public enum SourceKind
+    {
+        Script,
+        ModuleViaBuilder,
+        ModuleViaFactory,
+    }
+
+    private sealed class ModuleScript : IModuleLoader
+    {
+        public const string MainSpecifier = "main";
+        private readonly bool _prepare;
+        private readonly string _code;
+        private readonly ModuleParsingOptions _parsingOptions;
+
+        public ModuleScript(bool prepare, string code, ModuleParsingOptions parsingOptions)
+        {
+            _prepare = prepare;
+            _code = code;
+            _parsingOptions = parsingOptions;
+        }
+
+        ResolvedSpecifier IModuleLoader.Resolve(string? referencingModuleLocation, ModuleRequest moduleRequest)
+        {
+            if (moduleRequest.Specifier == MainSpecifier)
+                return new ResolvedSpecifier(moduleRequest, MainSpecifier, Uri: null, SpecifierType.Bare);
+
+            throw new ArgumentException(null, nameof(moduleRequest));
+        }
+
+        Jint.Runtime.Modules.Module IModuleLoader.LoadModule(Engine engine, ResolvedSpecifier resolved)
+        {
+            if (resolved.ModuleRequest.Specifier == MainSpecifier)
+            {
+                return _prepare
+                    ? ModuleFactory.BuildSourceTextModule(engine, Engine.PrepareModule(_code, MainSpecifier, options: new ModulePreparationOptions { ParsingOptions = _parsingOptions }))
+                    : ModuleFactory.BuildSourceTextModule(engine, resolved, _code, _parsingOptions);
+            }
+
+            throw new ArgumentException(null, nameof(resolved));
+        }
+    }
+
+    [Theory]
+    // NOTE: Can't test eval as Esprima can't parse eval code in non-tolerant mode.
+    //[InlineData(SourceKind.Script, @"'' + eval('!!({g\\u0065t y() {} })')", false, false, null)]
+    //[InlineData(SourceKind.Script, @"'' + eval('!!({g\\u0065t y() {} })')", true, false, null)]
+    //[InlineData(SourceKind.Script, @"({g\u0065t x() {} }) + eval('!!({g\\u0065t y() {} })')", false, true, "[object Object]true")]
+    //[InlineData(SourceKind.Script, @"({g\u0065t x() {} }) + eval('!!({g\\u0065t y() {} })')", true, true, "[object Object]true")]
+    //[InlineData(SourceKind.ModuleViaBuilder, @"export default '' + eval('!!({g\\u0065t y() {} })')", false, false, null)]
+    //[InlineData(SourceKind.ModuleViaBuilder, @"export default '' + eval('!!({g\\u0065t y() {} })')", true, false, null)]
+    //[InlineData(SourceKind.ModuleViaBuilder, @"export default ({g\u0065t x() {} }) + eval('!!({g\\u0065t y() {} })')", false, true, "[object Object]true")]
+    //[InlineData(SourceKind.ModuleViaBuilder, @"export default ({g\u0065t x() {} }) + eval('!!({g\\u0065t y() {} })')", true, true, "[object Object]true")]
+    //[InlineData(SourceKind.ModuleViaFactory, @"export default '' + eval('!!({g\\u0065t y() {} })')", false, false, null)]
+    //[InlineData(SourceKind.ModuleViaFactory, @"export default '' + eval('!!({g\\u0065t y() {} })')", true, false, null)]
+    //[InlineData(SourceKind.ModuleViaFactory, @"export default ({g\u0065t x() {} }) + eval('!!({g\\u0065t y() {} })')", false, true, "[object Object]true")]
+    //[InlineData(SourceKind.ModuleViaFactory, @"export default ({g\u0065t x() {} }) + eval('!!({g\\u0065t y() {} })')", true, true, "[object Object]true")]
+
+    [InlineData(SourceKind.Script, @"'' + new Function('return !!({g\\u0065t y() {} })')()", false, false, null)]
+    [InlineData(SourceKind.Script, @"'' + new Function('return !!({g\\u0065t y() {} })')()", true, false, null)]
+    [InlineData(SourceKind.Script, @"({g\u0065t x() {} }) + new Function('return !!({g\\u0065t y() {} })')()", false, true, "[object Object]true")]
+    [InlineData(SourceKind.Script, @"({g\u0065t x() {} }) + new Function('return !!({g\\u0065t y() {} })')()", true, true, "[object Object]true")]
+    [InlineData(SourceKind.ModuleViaBuilder, @"export default '' + new Function('return !!({g\\u0065t y() {} })')()", false, false, null)]
+    [InlineData(SourceKind.ModuleViaBuilder, @"export default '' + new Function('return !!({g\\u0065t y() {} })')()", true, false, null)]
+    [InlineData(SourceKind.ModuleViaBuilder, @"export default ({g\u0065t x() {} }) + new Function('return !!({g\\u0065t y() {} })')()", false, true, "[object Object]true")]
+    [InlineData(SourceKind.ModuleViaBuilder, @"export default ({g\u0065t x() {} }) + new Function('return !!({g\\u0065t y() {} })')()", true, true, "[object Object]true")]
+    [InlineData(SourceKind.ModuleViaFactory, @"export default '' + new Function('return !!({g\\u0065t y() {} })')()", false, false, null)]
+    [InlineData(SourceKind.ModuleViaFactory, @"export default '' + new Function('return !!({g\\u0065t y() {} })')()", true, false, null)]
+    [InlineData(SourceKind.ModuleViaFactory, @"export default ({g\u0065t x() {} }) + new Function('return !!({g\\u0065t y() {} })')()", false, true, "[object Object]true")]
+    [InlineData(SourceKind.ModuleViaFactory, @"export default ({g\u0065t x() {} }) + new Function('return !!({g\\u0065t y() {} })')()", true, true, "[object Object]true")]
+
+    [InlineData(SourceKind.Script, @"'' + new ShadowRealm().evaluate('!!({g\\u0065t y() {} })')", false, false, null)]
+    [InlineData(SourceKind.Script, @"'' + new ShadowRealm().evaluate('!!({g\\u0065t y() {} })')", true, false, null)]
+    [InlineData(SourceKind.Script, @"({g\u0065t x() {} }) + new ShadowRealm().evaluate('!!({g\\u0065t y() {} })')", false, true, "[object Object]true")]
+    [InlineData(SourceKind.Script, @"({g\u0065t x() {} }) + new ShadowRealm().evaluate('!!({g\\u0065t y() {} })')", true, true, "[object Object]true")]
+    [InlineData(SourceKind.ModuleViaBuilder, @"export default '' + new ShadowRealm().evaluate('!!({g\\u0065t y() {} })')", false, false, null)]
+    [InlineData(SourceKind.ModuleViaBuilder, @"export default '' + new ShadowRealm().evaluate('!!({g\\u0065t y() {} })')", true, false, null)]
+    [InlineData(SourceKind.ModuleViaBuilder, @"export default ({g\u0065t x() {} }) + new ShadowRealm().evaluate('!!({g\\u0065t y() {} })')", false, true, "[object Object]true")]
+    [InlineData(SourceKind.ModuleViaBuilder, @"export default ({g\u0065t x() {} }) + new ShadowRealm().evaluate('!!({g\\u0065t y() {} })')", true, true, "[object Object]true")]
+    [InlineData(SourceKind.ModuleViaFactory, @"export default '' + new ShadowRealm().evaluate('!!({g\\u0065t y() {} })')", false, false, null)]
+    [InlineData(SourceKind.ModuleViaFactory, @"export default '' + new ShadowRealm().evaluate('!!({g\\u0065t y() {} })')", true, false, null)]
+    [InlineData(SourceKind.ModuleViaFactory, @"export default ({g\u0065t x() {} }) + new ShadowRealm().evaluate('!!({g\\u0065t y() {} })')", false, true, "[object Object]true")]
+    [InlineData(SourceKind.ModuleViaFactory, @"export default ({g\u0065t x() {} }) + new ShadowRealm().evaluate('!!({g\\u0065t y() {} })')", true, true, "[object Object]true")]
+    public void DynamicCodeShouldBeParsedWithCallerParserOptions(SourceKind sourceKind, string code, bool prepare, bool tolerant, string? expectedReturnValue)
+    {
+        Engine engine;
+        Func<JsValue> evalAction;
+
+        if (sourceKind == SourceKind.Script)
+        {
+            var parsingOptions = ScriptParsingOptions.Default with { Tolerant = tolerant };
+            engine = new Engine();
+
+            if (prepare)
+            {
+                var preparedScript = Engine.PrepareScript(code, options: new ScriptPreparationOptions { ParsingOptions = parsingOptions });
+                evalAction = () => engine.Evaluate(preparedScript);
+            }
+            else
+            {
+                evalAction = () => engine.Evaluate(code, parsingOptions);
+            }
+        }
+        else
+        {
+            var parsingOptions = ModuleParsingOptions.Default with { Tolerant = tolerant };
+            if (sourceKind == SourceKind.ModuleViaBuilder)
+            {
+                engine = new Engine();
+
+                if (prepare)
+                {
+                    engine.Modules.Add("main", o =>
+                    {
+                        var preparedModule = Engine.PrepareModule(code, options: new ModulePreparationOptions { ParsingOptions = parsingOptions });
+                        o.AddModule(preparedModule);
+                    });
+                }
+                else
+                {
+                    engine.Modules.Add("main", o => o.AddSource(code).WithOptions(_ => parsingOptions));
+                }
+            }
+            else if (sourceKind == SourceKind.ModuleViaFactory)
+            {
+                var moduleScript = new ModuleScript(prepare, code, parsingOptions);
+                engine = new Engine(o => o.EnableModules(moduleScript));
+            }
+            else
+            {
+                throw new InvalidOperationException();
+            }
+
+            evalAction = () =>
+            {
+                var ns = engine.Modules.Import("main");
+                return ns.Get("default");
+            };
+        }
+
+        if (expectedReturnValue is not null)
+        {
+            Assert.Equal(expectedReturnValue, evalAction());
+        }
+        else
+        {
+            var ex = Assert.ThrowsAny<Exception>(evalAction);
+            Assert.True(ex is JavaScriptException or ParseErrorException);
+        }
+    }
+
+    [Theory]
+    [InlineData(false, false, null)]
+    [InlineData(false, true, "true")]
+    [InlineData(true, false, null)]
+    [InlineData(true, true, "true")]
+    public void TransitivelyImportedModuleShouldBeParsedWithOwnParserOptions(bool mainTolerant, bool otherTolerant, string? expectedReturnValue)
+    {
+        var engine = new Engine();
+        engine.Modules.Add("main", o => o.AddSource("import { x } from 'other'; export default x").WithOptions(_ => ModuleParsingOptions.Default with { Tolerant = mainTolerant }));
+        engine.Modules.Add("other", o => o.AddSource("export const x = '' + new Function('return !!({g\\\\u0065t y() {} })')()").WithOptions(_ => ModuleParsingOptions.Default with { Tolerant = otherTolerant }));
+
+        var evalAction = () =>
+        {
+            var ns = engine.Modules.Import("main");
+            return ns.Get("default");
+        };
+
+        if (expectedReturnValue is not null)
+        {
+            Assert.Equal(expectedReturnValue, evalAction());
+        }
+        else
+        {
+            var ex = Assert.Throws<JavaScriptException>(evalAction);
+            Assert.True(ex.Error.InstanceofOperator(engine.Realm.Intrinsics.SyntaxError));
+        }
+    }
+
+    [Fact]
+    public void RealmsShouldBeIsolatedWithRegardToParserOptions()
+    {
+        var engine = new Engine();
+
+        var parsingOptions = ScriptParsingOptions.Default with { AllowReturnOutsideFunction = true, Tolerant = true };
+        Assert.Equal("true", engine.Evaluate("return '' + new Function('return !!({g\\\\u0065t y() {} })')()", parsingOptions));
+
+        var shadowRealm1 = engine.Intrinsics.ShadowRealm.Construct();
+        var shadowRealm2 = engine.Intrinsics.ShadowRealm.Construct();
+
+        var ex = Assert.Throws<JavaScriptException>(() => shadowRealm1.Evaluate("'' + new Function('return !!({g\\\\u0065t y() {} })')()", parsingOptions with { Tolerant = false }));
+        Assert.True(ex.Error.InstanceofOperator(engine.Intrinsics.TypeError));
+
+        Assert.Equal("true", engine.Evaluate("'' + new Function('return !!({g\\\\u0065t y() {} })')()", parsingOptions));
+
+        ex = Assert.Throws<JavaScriptException>(() => shadowRealm2.Evaluate("'' + new Function('return !!({g\\\\u0065t y() {} })')()", parsingOptions with { Tolerant = false }));
+        Assert.True(ex.Error.InstanceofOperator(engine.Intrinsics.TypeError));
+
+        Assert.Equal("true", shadowRealm1.Evaluate("'' + new Function('return !!({g\\\\u0065t y() {} })')()", parsingOptions));
+
+        ex = Assert.Throws<JavaScriptException>(() => engine.Evaluate("'' + new Function('return !!({g\\\\u0065t y() {} })')()", parsingOptions with { Tolerant = false }));
+        Assert.True(ex.Error.InstanceofOperator(engine.Intrinsics.SyntaxError));
+
+        Assert.Equal("true", shadowRealm2.Evaluate("'' + new Function('return !!({g\\\\u0065t y() {} })')()", parsingOptions));
+    }
+}

+ 23 - 32
Jint/Engine.Ast.cs

@@ -17,15 +17,13 @@ public partial class Engine
     /// <remarks>
     /// Returned instance is reusable and thread-safe. You should prepare scripts only once and then reuse them.
     /// </remarks>
-    public static Script PrepareScript(string script, string? source = null, bool strict = false)
+    public static Prepared<Script> PrepareScript(string code, string? source = null, bool strict = false, ScriptPreparationOptions? options = null)
     {
-        var astAnalyzer = new AstAnalyzer(new ScriptPreparationOptions());
-        var options = ParserOptions.Default with
-        {
-            AllowReturnOutsideFunction = true, OnNodeCreated = astAnalyzer.NodeVisitor
-        };
-
-        return new Parser(options).ParseScript(script, source, strict);
+        options ??= ScriptPreparationOptions.Default;
+        var astAnalyzer = new AstAnalyzer(options);
+        var parserOptions = options.GetParserOptions();
+        var preparedScript = new Parser(parserOptions with { OnNodeCreated = astAnalyzer.NodeVisitor }).ParseScript(code, source, strict);
+        return new Prepared<Script>(preparedScript, parserOptions);
     }
 
     /// <summary>
@@ -34,34 +32,26 @@ public partial class Engine
     /// <remarks>
     /// Returned instance is reusable and thread-safe. You should prepare modules only once and then reuse them.
     /// </remarks>
-    public static Module PrepareModule(string script, string? source = null)
+    public static Prepared<Module> PrepareModule(string code, string? source = null, ModulePreparationOptions? options = null)
     {
-        var astAnalyzer = new AstAnalyzer(new ScriptPreparationOptions());
-        var options = ParserOptions.Default with
-        {
-            OnNodeCreated = astAnalyzer.NodeVisitor
-        };
-
-        return new Parser(options).ParseModule(script, source);
-    }
-
-    [StructLayout(LayoutKind.Auto)]
-    private readonly record struct ScriptPreparationOptions(bool CompileRegex, bool FoldConstants)
-    {
-        public ScriptPreparationOptions() : this(CompileRegex: true, FoldConstants: true)
-        {
-        }
+        options ??= ModulePreparationOptions.Default;
+        var astAnalyzer = new AstAnalyzer(options);
+        var parserOptions = options.GetParserOptions();
+        var preparedModule = new Parser(parserOptions with { OnNodeCreated = astAnalyzer.NodeVisitor }).ParseModule(code, source);
+        return new Prepared<Module>(preparedModule, parserOptions);
     }
 
     private sealed class AstAnalyzer
     {
-        private readonly ScriptPreparationOptions _options;
+        private static readonly bool _canCompileNegativeLookaroundAssertions = typeof(Regex).Assembly.GetName().Version?.Major is not (null or 7 or 8);
+
+        private readonly IPreparationOptions<IParsingOptions> _preparationOptions;
         private readonly Dictionary<string, Environment.BindingName> _bindingNames = new(StringComparer.Ordinal);
         private readonly Dictionary<string, Regex> _regexes = new(StringComparer.Ordinal);
 
-        public AstAnalyzer(ScriptPreparationOptions options)
+        public AstAnalyzer(IPreparationOptions<IParsingOptions> preparationOptions)
         {
-            _options = options;
+            _preparationOptions = preparationOptions;
         }
 
         public void NodeVisitor(Node node)
@@ -86,21 +76,22 @@ public partial class Engine
                     var constantValue = JintLiteralExpression.ConvertToJsValue(literal);
                     node.AssociatedData = constantValue is not null ? new JintConstantExpression(literal, constantValue) : null;
 
-                    if (node.AssociatedData is null && literal.TokenType == TokenKind.RegularExpression && _options.CompileRegex)
+                    if (node.AssociatedData is null && literal.TokenType == TokenKind.RegularExpression
+                        && !_canCompileNegativeLookaroundAssertions && _preparationOptions.ParsingOptions.CompileRegex != false)
                     {
                         var regExpLiteral = (RegExpLiteral) literal;
                         var regExpParseResult = regExpLiteral.ParseResult;
 
                         // only compile if there's no negative lookahead, it works incorrectly under NET 7 and NET 8
                         // https://github.com/dotnet/runtime/issues/97455
-                        if (regExpParseResult.Success && !regExpLiteral.Raw.Contains("(?!"))
+                        if (regExpParseResult.Success && regExpLiteral.Raw.Contains("(?!"))
                         {
                             if (!_regexes.TryGetValue(regExpLiteral.Raw, out var regex))
                             {
                                 regex = regExpParseResult.Regex!;
-                                if ((regex.Options & RegexOptions.Compiled) == RegexOptions.None)
+                                if ((regex.Options & RegexOptions.Compiled) != RegexOptions.None)
                                 {
-                                    regex = new Regex(regex.ToString(), regex.Options | RegexOptions.Compiled, regex.MatchTimeout);
+                                    regex = new Regex(regex.ToString(), regex.Options & ~RegexOptions.Compiled, regex.MatchTimeout);
                                 }
 
                                 _regexes[regExpLiteral.Raw] = regex;
@@ -133,7 +124,7 @@ public partial class Engine
 
                 case NodeType.BinaryExpression:
                     var binaryExpression = (BinaryExpression) node;
-                    if (_options.FoldConstants
+                    if (_preparationOptions.FoldConstants
                         && binaryExpression.Operator != BinaryOperator.InstanceOf
                         && binaryExpression.Operator != BinaryOperator.In
                         && binaryExpression is { Left: Literal leftLiteral, Right: Literal rightLiteral })

+ 18 - 0
Jint/Engine.Defaults.cs

@@ -0,0 +1,18 @@
+namespace Jint;
+
+public partial class Engine
+{
+    internal const bool FoldConstantsOnPrepareByDefault = true;
+
+    internal static readonly ParserOptions BaseParserOptions = ParserOptions.Default with
+    {
+        Tolerant = true,
+
+        // This should be kept sync with https://github.com/sebastienros/esprima-dotnet/blob/v3.0.4/test/Esprima.Tests/JavaScriptParserTests.cs#L14-L18
+#if DEBUG
+        MaxAssignmentDepth = 200,
+#else
+        MaxAssignmentDepth = 500,
+#endif
+    };
+}

+ 1 - 1
Jint/Engine.Modules.cs

@@ -64,7 +64,7 @@ public partial class Engine
         private BuilderModule LoadFromBuilder(string specifier, ModuleBuilder moduleBuilder, ResolvedSpecifier moduleResolution)
         {
             var parsedModule = moduleBuilder.Parse();
-            var module = new BuilderModule(_engine, _engine.Realm, parsedModule, location: parsedModule.Location.Source, async: false);
+            var module = new BuilderModule(_engine, _engine.Realm, parsedModule, location: parsedModule.Program!.Location.Source, async: false);
             _modules[moduleResolution.Key] = module;
             moduleBuilder.BindExportedValues(module);
             _builders.Remove(specifier);

+ 58 - 43
Jint/Engine.cs

@@ -144,12 +144,7 @@ namespace Jint
             CallStack = new JintCallStack(Options.Constraints.MaxRecursionDepth >= 0);
             _stackGuard = new StackGuard(this);
 
-            _defaultParserOptions = ParserOptions.Default with
-            {
-                AllowReturnOutsideFunction = true,
-                RegexTimeout = Options.Constraints.RegexTimeout
-            };
-
+            _defaultParserOptions = ScriptParsingOptions.Default.GetParserOptions(Options);
             _defaultParser = new Parser(_defaultParserOptions);
         }
 
@@ -196,8 +191,30 @@ namespace Jint
 
         public DebugHandler Debugger => _debugger ??= new DebugHandler(this, Options.Debugger.InitialStepMode);
 
+        // TODO: needed
+        internal ParserOptions GetActiveParserOptions()
+        {
+            return _executionContexts?.GetActiveParserOptions() ?? _defaultParserOptions;
+        }
 
-        internal ExecutionContext EnterExecutionContext(
+        internal Parser GetParserFor(ScriptParsingOptions parsingOptions, out ParserOptions parserOptions)
+        {
+            if (ReferenceEquals(parsingOptions, ScriptParsingOptions.Default))
+            {
+                parserOptions = _defaultParserOptions;
+                return _defaultParser;
+            }
+
+            parserOptions = parsingOptions.GetParserOptions(Options);
+            return new Parser(parserOptions);
+        }
+
+        internal Parser GetParserFor(ParserOptions parserOptions)
+        {
+            return ReferenceEquals(parserOptions, _defaultParserOptions) ? _defaultParser : new Parser(parserOptions);
+        }
+
+        internal void EnterExecutionContext(
             Environment lexicalEnvironment,
             Environment variableEnvironment,
             Realm realm,
@@ -212,13 +229,11 @@ namespace Jint
                 null);
 
             _executionContexts.Push(context);
-            return context;
         }
 
-        internal ExecutionContext EnterExecutionContext(in ExecutionContext context)
+        internal void EnterExecutionContext(in ExecutionContext context)
         {
             _executionContexts.Push(context);
-            return context;
         }
 
         /// <summary>
@@ -327,74 +342,73 @@ namespace Jint
         /// <summary>
         /// Evaluates code and returns last return value.
         /// </summary>
-        public JsValue Evaluate(string code)
-            => Evaluate(code, "<anonymous>", _defaultParserOptions);
-
-        /// <summary>
-        /// Evaluates code and returns last return value.
-        /// </summary>
-        public JsValue Evaluate(string code, string source)
-            => Evaluate(code, source, _defaultParserOptions);
+        public JsValue Evaluate(string code, string? source = null)
+        {
+            var script = _defaultParser.ParseScript(code, source ?? "<anonymous>", _isStrict);
+            return Evaluate(new Prepared<Script>(script, _defaultParserOptions));
+        }
 
         /// <summary>
         /// Evaluates code and returns last return value.
         /// </summary>
-        public JsValue Evaluate(string code, ParserOptions parserOptions)
-            => Evaluate(code, "<anonymous>", parserOptions);
+        public JsValue Evaluate(string code, ScriptParsingOptions parsingOptions)
+            => Evaluate(code, "<anonymous>", parsingOptions);
 
         /// <summary>
         /// Evaluates code and returns last return value.
         /// </summary>
-        public JsValue Evaluate(string code, string source, ParserOptions parserOptions)
+        public JsValue Evaluate(string code, string source, ScriptParsingOptions parsingOptions)
         {
-            var parser = ReferenceEquals(_defaultParserOptions, parserOptions)
-                ? _defaultParser
-                : new Parser(parserOptions);
-
+            var parser = GetParserFor(parsingOptions, out var parserOptions);
             var script = parser.ParseScript(code, source, _isStrict);
-
-            return Evaluate(script);
+            return Evaluate(new Prepared<Script>(script, parserOptions));
         }
 
         /// <summary>
         /// Evaluates code and returns last return value.
         /// </summary>
-        public JsValue Evaluate(Script script)
-            => Execute(script)._completionValue;
+        public JsValue Evaluate(in Prepared<Script> preparedScript)
+            => Execute(preparedScript)._completionValue;
 
         /// <summary>
         /// Executes code into engine and returns the engine instance (useful for chaining).
         /// </summary>
         public Engine Execute(string code, string? source = null)
-            => Execute(code, source ?? "<anonymous>", _defaultParserOptions);
+        {
+            var script = _defaultParser.ParseScript(code, source ?? "<anonymous>", _isStrict);
+            return Execute(new Prepared<Script>(script, _defaultParserOptions));
+        }
 
         /// <summary>
         /// Executes code into engine and returns the engine instance (useful for chaining).
         /// </summary>
-        public Engine Execute(string code, ParserOptions parserOptions)
-            => Execute(code, "<anonymous>", parserOptions);
+        public Engine Execute(string code, ScriptParsingOptions parsingOptions)
+            => Execute(code, "<anonymous>", parsingOptions);
 
         /// <summary>
         /// Executes code into engine and returns the engine instance (useful for chaining).
         /// </summary>
-        public Engine Execute(string code, string source, ParserOptions parserOptions)
+        public Engine Execute(string code, string source, ScriptParsingOptions parsingOptions)
         {
-            var parser = ReferenceEquals(_defaultParserOptions, parserOptions)
-                ? _defaultParser
-                : new Parser(parserOptions);
-
+            var parser = GetParserFor(parsingOptions, out var parserOptions);
             var script = parser.ParseScript(code, source, _isStrict);
-
-            return Execute(script);
+            return Execute(new Prepared<Script>(script, parserOptions));
         }
 
         /// <summary>
         /// Executes code into engine and returns the engine instance (useful for chaining).
         /// </summary>
-        public Engine Execute(Script script)
+        public Engine Execute(in Prepared<Script> preparedScript)
         {
+            if (!preparedScript.IsValid)
+            {
+                ExceptionHelper.ThrowInvalidPreparedScriptArgumentException(nameof(preparedScript));
+            }
+
+            var script = preparedScript.Program;
+            var parserOptions = preparedScript.ParserOptions;
             var strict = _isStrict || script.Strict;
-            ExecuteWithConstraints(strict, () => ScriptEvaluation(new ScriptRecord(Realm, script, script.Location.Source)));
+            ExecuteWithConstraints(strict, () => ScriptEvaluation(new ScriptRecord(Realm, script, script.Location.Source), parserOptions));
 
             return this;
         }
@@ -402,7 +416,7 @@ namespace Jint
         /// <summary>
         /// https://tc39.es/ecma262/#sec-runtime-semantics-scriptevaluation
         /// </summary>
-        private Engine ScriptEvaluation(ScriptRecord scriptRecord)
+        private Engine ScriptEvaluation(ScriptRecord scriptRecord, ParserOptions parserOptions)
         {
             Debugger.OnBeforeEvaluate(scriptRecord.EcmaScriptCode);
 
@@ -413,7 +427,8 @@ namespace Jint
                 lexicalEnvironment: globalEnv,
                 variableEnvironment: globalEnv,
                 privateEnvironment: null,
-                Realm);
+                Realm,
+                parserOptions: parserOptions);
 
             EnterExecutionContext(scriptContext);
             try

+ 4 - 5
Jint/HoistingScope.cs

@@ -1,5 +1,4 @@
 using Jint.Runtime.Modules;
-using Module = Esprima.Ast.Module;
 
 namespace Jint
 {
@@ -57,7 +56,7 @@ namespace Jint
         }
 
         public static HoistingScope GetModuleLevelDeclarations(
-            Module module,
+            AstModule module,
             bool collectVarNames = false,
             bool collectLexicalNames = false)
         {
@@ -122,7 +121,7 @@ namespace Jint
         }
 
         public static void GetImportsAndExports(
-            Module module,
+            AstModule module,
             out HashSet<ModuleRequest> requestedModules,
             out List<ImportEntry>? importEntries,
             out List<ExportEntry> localExportEntries,
@@ -243,7 +242,7 @@ namespace Jint
                             }
                         }
 
-                        if (parent is null or Module && variableDeclaration.Kind != VariableDeclarationKind.Var)
+                        if (parent is null or AstModule && variableDeclaration.Kind != VariableDeclarationKind.Var)
                         {
                             _lexicalDeclarations ??= new List<Declaration>();
                             _lexicalDeclarations.Add(variableDeclaration);
@@ -270,7 +269,7 @@ namespace Jint
                             _functions.Add((FunctionDeclaration)childNode);
                         }
                     }
-                    else if (childType == NodeType.ClassDeclaration && parent is null or Module)
+                    else if (childType == NodeType.ClassDeclaration && parent is null or AstModule)
                     {
                         _lexicalDeclarations ??= new List<Declaration>();
                         _lexicalDeclarations.Add((Declaration) childNode);

+ 6 - 4
Jint/Native/Function/ClassDefinition.cs

@@ -21,17 +21,19 @@ internal sealed class ClassDefinition
 
     static ClassDefinition()
     {
+        var parser = new Parser();
+
         // generate missing constructor AST only once
-        static MethodDefinition CreateConstructorMethodDefinition(string source)
+        static MethodDefinition CreateConstructorMethodDefinition(Parser parser, string source)
         {
-            var script = new Parser().ParseScript(source);
+            var script = parser.ParseScript(source);
             var classDeclaration = (ClassDeclaration) script.Body[0];
             return (MethodDefinition) classDeclaration.Body.Body[0];
         }
 
-        _superConstructor = CreateConstructorMethodDefinition("class temp { constructor(...args) { super(...args); } }");
+        _superConstructor = CreateConstructorMethodDefinition(parser, "class temp extends X { constructor(...args) { super(...args); } }");
         _defaultSuperCall = (CallExpression) ((ExpressionStatement) _superConstructor.Value.Body.Body[0]).Expression;
-        _emptyConstructor = CreateConstructorMethodDefinition("class temp { constructor() {} }");
+        _emptyConstructor = CreateConstructorMethodDefinition(parser, "class temp { constructor() {} }");
     }
 
     public ClassDefinition(

+ 6 - 4
Jint/Native/Function/EvalFunction.cs

@@ -10,9 +10,6 @@ public sealed class EvalFunction : Function
 {
     private static readonly JsString _functionName = new("eval");
 
-    private static readonly ParserOptions _parserOptions = ParserOptions.Default with { Tolerant = true };
-    private readonly Parser _parser = new(_parserOptions);
-
     internal EvalFunction(
         Engine engine,
         Realm realm,
@@ -75,9 +72,14 @@ public sealed class EvalFunction : Function
         }
 
         Script? script = null;
+        var parserOptions = _engine.GetActiveParserOptions();
+        var adjustedParserOptions = !parserOptions.Tolerant || parserOptions.AllowReturnOutsideFunction
+            ? parserOptions with { Tolerant = true, AllowReturnOutsideFunction = false }
+            : parserOptions;
+        var parser = _engine.GetParserFor(adjustedParserOptions);
         try
         {
-            script = _parser.ParseScript(x.ToString(), strict: strictCaller);
+            script = parser.ParseScript(x.ToString(), strict: strictCaller);
         }
         catch (ParseErrorException e)
         {

+ 4 - 3
Jint/Native/Function/Function.cs

@@ -284,7 +284,7 @@ namespace Jint.Native.Function
         /// <summary>
         /// https://tc39.es/ecma262/#sec-ordinarycallbindthis
         /// </summary>
-        internal void OrdinaryCallBindThis(ExecutionContext calleeContext, JsValue thisArgument)
+        internal void OrdinaryCallBindThis(in ExecutionContext calleeContext, JsValue thisArgument)
         {
             if (_thisMode == FunctionThisMode.Lexical)
             {
@@ -318,7 +318,7 @@ namespace Jint.Native.Function
         /// <summary>
         /// https://tc39.es/ecma262/#sec-prepareforordinarycall
         /// </summary>
-        internal ExecutionContext PrepareForOrdinaryCall(JsValue newTarget)
+        internal ref readonly ExecutionContext PrepareForOrdinaryCall(JsValue newTarget)
         {
             var callerContext = _engine.ExecutionContext;
 
@@ -339,7 +339,8 @@ namespace Jint.Native.Function
             // NOTE: Any exception objects produced after this point are associated with calleeRealm.
             // Return calleeContext.
 
-            return _engine.EnterExecutionContext(calleeContext);
+            _engine.EnterExecutionContext(calleeContext);
+            return ref _engine.ExecutionContext;
         }
 
         internal void MakeConstructor(bool writableProperty = true, ObjectInstance? prototype = null)

+ 5 - 4
Jint/Native/Function/FunctionInstance.Dynamic.cs

@@ -142,11 +142,12 @@ public partial class Function
                 }
             }
 
-            Parser parser = new(new ParserOptions
+            var parserOptions = _engine.GetActiveParserOptions();
+            if (!parserOptions.AllowReturnOutsideFunction)
             {
-                Tolerant = false,
-                RegexTimeout = _engine.Options.Constraints.RegexTimeout
-            });
+                parserOptions = parserOptions with { AllowReturnOutsideFunction = true };
+            }
+            Parser parser = new(parserOptions);
             function = (IFunction) parser.ParseScript(functionExpression, source: null, _engine._isStrict).Body[0];
         }
         catch (ParseErrorException ex)

+ 2 - 2
Jint/Native/Function/ScriptFunction.cs

@@ -63,7 +63,7 @@ namespace Jint.Native.Function
             {
                 try
                 {
-                    var calleeContext = PrepareForOrdinaryCall(Undefined);
+                    ref readonly var calleeContext = ref PrepareForOrdinaryCall(Undefined);
 
                     if (_isClassConstructor)
                     {
@@ -142,7 +142,7 @@ namespace Jint.Native.Function
                     static (Engine engine, Realm _, object? _) => new JsObject(engine));
             }
 
-            var calleeContext = PrepareForOrdinaryCall(newTarget);
+            ref readonly var calleeContext = ref PrepareForOrdinaryCall(newTarget);
             var constructorEnv = (FunctionEnvironment) calleeContext.LexicalEnvironment;
 
             var strict = _thisMode == FunctionThisMode.Strict;

+ 1 - 1
Jint/Native/Generator/GeneratorInstance.cs

@@ -101,7 +101,7 @@ internal sealed class GeneratorInstance : ObjectInstance
         return ResumeExecution(genContext, new EvaluationContext(_engine));
     }
 
-    private ObjectInstance ResumeExecution(ExecutionContext genContext, EvaluationContext context)
+    private ObjectInstance ResumeExecution(in ExecutionContext genContext, EvaluationContext context)
     {
         _generatorState = GeneratorState.Executing;
         _engine.EnterExecutionContext(genContext);

+ 0 - 5
Jint/Native/Json/JsonParser.cs

@@ -688,11 +688,6 @@ namespace Jint.Native.Json
         }
 
         public JsValue Parse(string code)
-        {
-            return Parse(code, null);
-        }
-
-        public JsValue Parse(string code, ParserOptions? options)
         {
             _source = code;
             _index = 0;

+ 2 - 1
Jint/Native/RegExp/RegExpConstructor.cs

@@ -101,9 +101,10 @@ namespace Jint.Native.RegExp
 
             var f = flags.IsUndefined() ? "" : TypeConverter.ToString(flags);
 
+            var parserOptions = _engine.GetActiveParserOptions();
             try
             {
-                var regExpParseResult = Scanner.AdaptRegExp(p, f, compiled: false, _engine.Options.Constraints.RegexTimeout);
+                var regExpParseResult = Scanner.AdaptRegExp(p, f, compiled: false, parserOptions.RegexTimeout);
 
                 if (!regExpParseResult.Success)
                 {

+ 33 - 19
Jint/Native/ShadowRealm/ShadowRealm.cs

@@ -19,31 +19,32 @@ namespace Jint.Native.ShadowRealm;
 public sealed class ShadowRealm : ObjectInstance
 #pragma warning restore MA0049
 {
-    private readonly Parser _parser;
     internal readonly Realm _shadowRealm;
     private readonly ExecutionContext _executionContext;
 
-    internal ShadowRealm(Engine engine, ExecutionContext executionContext, Realm shadowRealm) : base(engine)
+    internal ShadowRealm(Engine engine, in ExecutionContext executionContext, Realm shadowRealm) : base(engine)
     {
-        _parser = new(new ParserOptions
-        {
-            Tolerant = false,
-            RegexTimeout = engine.Options.Constraints.RegexTimeout
-        });
         _executionContext = executionContext;
         _shadowRealm = shadowRealm;
     }
 
-    public JsValue Evaluate(string sourceText)
+    public JsValue Evaluate(string sourceText, ScriptParsingOptions? parsingOptions = null)
     {
         var callerRealm = _engine.Realm;
-        return PerformShadowRealmEval(sourceText, callerRealm);
+        var parserOptions = parsingOptions?.GetParserOptions() ?? _engine.GetActiveParserOptions();
+        var parser = _engine.GetParserFor(parserOptions);
+        return PerformShadowRealmEval(sourceText, parserOptions, parser, callerRealm);
     }
 
-    public JsValue Evaluate(Script script)
+    public JsValue Evaluate(in Prepared<Script> preparedScript)
     {
+        if (!preparedScript.IsValid)
+        {
+            ExceptionHelper.ThrowInvalidPreparedScriptArgumentException(nameof(preparedScript));
+        }
+
         var callerRealm = _engine.Realm;
-        return PerformShadowRealmEval(script, callerRealm);
+        return PerformShadowRealmEval(preparedScript, callerRealm);
     }
 
     public JsValue ImportValue(string specifier, string exportName)
@@ -100,7 +101,7 @@ public sealed class ShadowRealm : ObjectInstance
     /// <summary>
     /// https://tc39.es/proposal-shadowrealm/#sec-performshadowrealmeval
     /// </summary>
-    internal JsValue PerformShadowRealmEval(string sourceText, Realm callerRealm)
+    internal JsValue PerformShadowRealmEval(string sourceText, ParserOptions parserOptions, Parser parser, Realm callerRealm)
     {
         var evalRealm = _shadowRealm;
 
@@ -109,7 +110,7 @@ public sealed class ShadowRealm : ObjectInstance
         Script script;
         try
         {
-            script = _parser.ParseScript(sourceText, source: null, _engine._isStrict);
+            script = parser.ParseScript(sourceText, source: null, _engine._isStrict);
         }
         catch (ParseErrorException e)
         {
@@ -125,22 +126,23 @@ public sealed class ShadowRealm : ObjectInstance
             return default;
         }
 
-        return PerformShadowRealmEvalInternal(script, callerRealm);
+        return PerformShadowRealmEvalInternal(new Prepared<Script>(script, parserOptions), callerRealm);
     }
 
-    internal JsValue PerformShadowRealmEval(Script script, Realm callerRealm)
+    internal JsValue PerformShadowRealmEval(in Prepared<Script> preparedScript, Realm callerRealm)
     {
         var evalRealm = _shadowRealm;
 
         _engine._host.EnsureCanCompileStrings(callerRealm, evalRealm);
 
-        return PerformShadowRealmEvalInternal(script, callerRealm);
+        return PerformShadowRealmEvalInternal(preparedScript, callerRealm);
     }
 
-    internal JsValue PerformShadowRealmEvalInternal(Script script, Realm callerRealm)
+    internal JsValue PerformShadowRealmEvalInternal(in Prepared<Script> preparedScript, Realm callerRealm)
     {
         var evalRealm = _shadowRealm;
 
+        var script = preparedScript.Program!;
         ref readonly var body = ref script.Body;
         if (body.Count == 0)
         {
@@ -162,7 +164,7 @@ public sealed class ShadowRealm : ObjectInstance
 
         // If runningContext is not already suspended, suspend runningContext.
 
-        var evalContext = new ExecutionContext(null, lexEnv, varEnv, null, evalRealm, null);
+        var evalContext = new ExecutionContext(null, lexEnv, varEnv, null, evalRealm, null, parserOptions: preparedScript.ParserOptions);
         _engine.EnterExecutionContext(evalContext);
 
         Completion result;
@@ -172,7 +174,19 @@ public sealed class ShadowRealm : ObjectInstance
 
             using (new StrictModeScope(strictEval, force: true))
             {
-                result = new JintScript(script).Execute(new EvaluationContext(_engine));
+                // _activeEvaluationContext must be set or e.g. a nested eval could lead to NullReferenceException...
+
+                // TODO: is this correct or should we join the current EvaluationContext if any?
+                var originalEvaluationContext = _engine._activeEvaluationContext;
+                _engine._activeEvaluationContext = new EvaluationContext(_engine);
+                try
+                {
+                    result = new JintScript(script).Execute(_engine._activeEvaluationContext);
+                }
+                finally
+                {
+                    _engine._activeEvaluationContext = originalEvaluationContext;
+                }
             }
 
             if (result.Type == CompletionType.Throw)

+ 8 - 1
Jint/Native/ShadowRealm/ShadowRealmPrototype.cs

@@ -53,7 +53,14 @@ internal sealed class ShadowRealmPrototype : Prototype
             ExceptionHelper.ThrowTypeError(_realm, "Invalid source text " + sourceText);
         }
 
-        return shadowRealm.PerformShadowRealmEval(sourceText.AsString(), _realm);
+        var parserOptions = _engine.GetActiveParserOptions();
+        // Just like in the case of eval, we don't allow top level returns.
+        var adjustedParserOptions = parserOptions.AllowReturnOutsideFunction
+            ? parserOptions with { AllowReturnOutsideFunction = false }
+            : parserOptions;
+        var parser = _engine.GetParserFor(adjustedParserOptions);
+
+        return shadowRealm.PerformShadowRealmEval(sourceText.AsString(), parserOptions, parser, _realm);
     }
 
     /// <summary>

+ 116 - 0
Jint/ParsingOptions.cs

@@ -0,0 +1,116 @@
+using System.Text.RegularExpressions;
+
+namespace Jint
+{
+    public interface IParsingOptions
+    {
+        /// <summary>
+        /// Gets or sets whether to create compiled <see cref="Regex"/> instances when adapting regular expressions.
+        /// Defaults to <see langword="null"/>, which means that in the case of non-prepared scripts and modules
+        /// regular expressions will be interpreted, otherwise they will be compiled.
+        /// </summary>
+        bool? CompileRegex { get; init; }
+
+        /// <summary>
+        /// Gets or sets the default timeout for created <see cref="Regex"/> instances.
+        /// Defaults to <see langword="null"/>, which means that in the case of non-prepared scripts and modules
+        /// the <see cref="Options.ConstraintOptions.RegexTimeout"/> setting should apply,
+        /// otherwise the default of the <see cref="ParserOptions.RegexTimeout"/> setting (10 seconds).
+        /// </summary>
+        /// <remarks>
+        /// Please note that <see cref="Options.ConstraintOptions.RegexTimeout"/> setting will be ignored
+        /// if this option is set to a value other than <see langword="null"/>.
+        /// </remarks>
+        TimeSpan? RegexTimeout { get; init; }
+
+        /// <summary>
+        /// Gets or sets whether to parse the source code in tolerant mode.
+        /// Defaults to <see langword="true"/>.
+        /// </summary>
+        /// <remarks>
+        /// Please note that there is a plan to change the default to <see langword="false"/> in the next major version.
+        /// </remarks>
+        bool Tolerant { get; init; }
+    }
+
+    public sealed record class ScriptParsingOptions : IParsingOptions
+    {
+        private static readonly ParserOptions _defaultParserOptions = Engine.BaseParserOptions with
+        {
+            AllowReturnOutsideFunction = true,
+            RegExpParseMode = RegExpParseMode.AdaptToInterpreted,
+        };
+
+        public static readonly ScriptParsingOptions Default = new();
+
+        /// <summary>
+        /// Gets or sets whether to allow return statements at the top level.
+        /// Defaults to <see langword="true"/>.
+        /// </summary>
+        /// <remarks>
+        /// Please note that there is a plan to change the default to <see langword="false"/> in the next major version.
+        /// </remarks>
+        public bool AllowReturnOutsideFunction { get; init; } = _defaultParserOptions.AllowReturnOutsideFunction;
+
+        /// <inheritdoc/>
+        public bool? CompileRegex { get; init; }
+
+        /// <inheritdoc/>
+        public TimeSpan? RegexTimeout { get; init; }
+
+        /// <inheritdoc/>
+        public bool Tolerant { get; init; } = _defaultParserOptions.Tolerant;
+
+        internal ParserOptions ApplyTo(ParserOptions parserOptions, RegExpParseMode defaultRegExpParseMode, TimeSpan defaultRegexTimeout) => parserOptions with
+        {
+            AllowReturnOutsideFunction = AllowReturnOutsideFunction,
+            RegExpParseMode = CompileRegex is null
+                ? defaultRegExpParseMode
+                : (CompileRegex.Value ? RegExpParseMode.AdaptToCompiled : RegExpParseMode.AdaptToInterpreted),
+            RegexTimeout = RegexTimeout ?? defaultRegexTimeout,
+            Tolerant = Tolerant,
+        };
+
+        internal ParserOptions GetParserOptions() => ReferenceEquals(this, Default)
+            ? _defaultParserOptions
+            : ApplyTo(_defaultParserOptions, _defaultParserOptions.RegExpParseMode, _defaultParserOptions.RegexTimeout);
+
+        internal ParserOptions GetParserOptions(Options engineOptions)
+            => ApplyTo(_defaultParserOptions, _defaultParserOptions.RegExpParseMode, engineOptions.Constraints.RegexTimeout);
+    }
+
+    public sealed record class ModuleParsingOptions : IParsingOptions
+    {
+        private static readonly ParserOptions _defaultParserOptions = Engine.BaseParserOptions with
+        {
+            RegExpParseMode = RegExpParseMode.AdaptToInterpreted,
+        };
+
+        public static readonly ModuleParsingOptions Default = new();
+
+        /// <inheritdoc/>
+        public bool? CompileRegex { get; init; }
+
+        /// <inheritdoc/>
+        public TimeSpan? RegexTimeout { get; init; }
+
+        /// <inheritdoc/>
+        public bool Tolerant { get; init; } = _defaultParserOptions.Tolerant;
+
+        internal ParserOptions ApplyTo(ParserOptions baseOptions, RegExpParseMode defaultRegExpParseMode, TimeSpan defaultRegexTimeout) => baseOptions with
+        {
+            RegExpParseMode = CompileRegex is null
+                ? defaultRegExpParseMode
+                : (CompileRegex.Value ? RegExpParseMode.AdaptToCompiled : RegExpParseMode.AdaptToInterpreted),
+            RegexTimeout = RegexTimeout ?? defaultRegexTimeout,
+            Tolerant = Tolerant,
+        };
+
+        internal ParserOptions GetParserOptions() => ReferenceEquals(this, Default)
+            ? _defaultParserOptions
+            : ApplyTo(_defaultParserOptions, _defaultParserOptions.RegExpParseMode, _defaultParserOptions.RegexTimeout);
+
+        internal ParserOptions GetParserOptions(Options engineOptions)
+            => ApplyTo(_defaultParserOptions, _defaultParserOptions.RegExpParseMode, engineOptions.Constraints.RegexTimeout);
+    }
+}

+ 51 - 0
Jint/PreparationOptions.cs

@@ -0,0 +1,51 @@
+namespace Jint;
+
+public interface IPreparationOptions<out TParsingOptions>
+    where TParsingOptions : IParsingOptions
+{
+    TParsingOptions ParsingOptions { get; }
+
+    /// <summary>
+    /// Gets or sets whether to fold constant expressions during the preparation phase.
+    /// Defaults to <see langword="true"/>.
+    /// </summary>
+    bool FoldConstants { get; init; }
+}
+
+public sealed record class ScriptPreparationOptions : IPreparationOptions<ScriptParsingOptions>
+{
+    private static readonly ParserOptions _defaultParserOptions = ScriptParsingOptions.Default.GetParserOptions() with
+    {
+        RegExpParseMode = RegExpParseMode.AdaptToCompiled,
+    };
+
+    public static readonly ScriptPreparationOptions Default = new();
+
+    public ScriptParsingOptions ParsingOptions { get; init; } = ScriptParsingOptions.Default;
+
+    /// <inheritdoc/>
+    public bool FoldConstants { get; init; } = Engine.FoldConstantsOnPrepareByDefault;
+
+    internal ParserOptions GetParserOptions() => ReferenceEquals(this, Default)
+        ? _defaultParserOptions
+        : ParsingOptions.ApplyTo(_defaultParserOptions, _defaultParserOptions.RegExpParseMode, _defaultParserOptions.RegexTimeout);
+}
+
+public sealed record class ModulePreparationOptions : IPreparationOptions<ModuleParsingOptions>
+{
+    private static readonly ParserOptions _defaultParserOptions = ModuleParsingOptions.Default.GetParserOptions() with
+    {
+        RegExpParseMode = RegExpParseMode.AdaptToCompiled
+    };
+
+    public static readonly ModulePreparationOptions Default = new();
+
+    public ModuleParsingOptions ParsingOptions { get; init; } = ModuleParsingOptions.Default;
+
+    /// <inheritdoc/>
+    public bool FoldConstants { get; init; } = Engine.FoldConstantsOnPrepareByDefault;
+
+    internal ParserOptions GetParserOptions() => ReferenceEquals(this, Default)
+        ? _defaultParserOptions
+        : ParsingOptions.ApplyTo(_defaultParserOptions, _defaultParserOptions.RegExpParseMode, _defaultParserOptions.RegexTimeout);
+}

+ 20 - 0
Jint/Prepared.cs

@@ -0,0 +1,20 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace Jint;
+
+public readonly struct Prepared<TProgram> where TProgram : Program
+{
+    internal Prepared(TProgram program, ParserOptions parserOptions)
+    {
+        Program = program;
+        ParserOptions = parserOptions;
+    }
+
+    public TProgram? Program { get; }
+
+    public ParserOptions? ParserOptions { get; }
+
+    [MemberNotNullWhen(true, nameof(Program))]
+    [MemberNotNullWhen(true, nameof(ParserOptions))]
+    public bool IsValid => Program is not null;
+}

+ 13 - 8
Jint/Runtime/Debugger/DebugHandler.cs

@@ -80,8 +80,13 @@ namespace Jint.Runtime.Debugger
         /// Internally, this is used for evaluating breakpoint conditions, but may also be used for e.g. watch lists
         /// in a debugger.
         /// </remarks>
-        public JsValue Evaluate(Script script)
+        public JsValue Evaluate(in Prepared<Script> preparedScript)
         {
+            if (!preparedScript.IsValid)
+            {
+                ExceptionHelper.ThrowInvalidPreparedScriptArgumentException(nameof(preparedScript));
+            }
+
             var context = _engine._activeEvaluationContext;
             if (context == null)
             {
@@ -89,7 +94,7 @@ namespace Jint.Runtime.Debugger
             }
             var callStackSize = _engine.CallStack.Count;
 
-            var list = new JintStatementList(null, script.Body);
+            var list = new JintStatementList(null, preparedScript.Program.Body);
             Completion result;
             try
             {
@@ -121,15 +126,15 @@ namespace Jint.Runtime.Debugger
             return result.GetValueOrDefault();
         }
 
-        /// <inheritdoc cref="Evaluate(Script)" />
-        public JsValue Evaluate(string source, ParserOptions? options = null)
+        /// <inheritdoc cref="Evaluate(in Prepared{Script})" />
+        public JsValue Evaluate(string sourceText, ScriptParsingOptions? parsingOptions = null)
         {
-            options ??= new ParserOptions();
-            var parser = new Parser(options);
+            var parserOptions = parsingOptions?.GetParserOptions() ?? _engine.GetActiveParserOptions();
+            var parser = _engine.GetParserFor(parserOptions);
             try
             {
-                var script = parser.ParseScript(source, "evaluation");
-                return Evaluate(script);
+                var script = parser.ParseScript(sourceText, "evaluation");
+                return Evaluate(new Prepared<Script>(script, parserOptions));
             }
             catch (ParseErrorException ex)
             {

+ 4 - 1
Jint/Runtime/Environments/ExecutionContext.cs

@@ -13,7 +13,8 @@ namespace Jint.Runtime.Environments
             PrivateEnvironment? privateEnvironment,
             Realm realm,
             GeneratorInstance? generator = null,
-            Function? function = null)
+            Function? function = null,
+            ParserOptions? parserOptions = null)
         {
             ScriptOrModule = scriptOrModule;
             LexicalEnvironment = lexicalEnvironment;
@@ -22,6 +23,7 @@ namespace Jint.Runtime.Environments
             Realm = realm;
             Function = function;
             Generator = generator;
+            ParserOptions = parserOptions;
         }
 
         public readonly IScriptOrModule? ScriptOrModule;
@@ -31,6 +33,7 @@ namespace Jint.Runtime.Environments
         public readonly Realm Realm;
         public readonly Function? Function;
         public readonly GeneratorInstance? Generator;
+        public readonly ParserOptions? ParserOptions;
 
         public bool Suspended => Generator?._generatorState == GeneratorState.SuspendedYield;
 

+ 12 - 0
Jint/Runtime/ExceptionHelper.cs

@@ -214,5 +214,17 @@ namespace Jint.Runtime
         {
             throw new ModuleResolutionException(message, specifier, parent, filePath);
         }
+
+        [DoesNotReturn]
+        public static void ThrowInvalidPreparedScriptArgumentException(string paramName)
+        {
+            throw new ArgumentException($"Instances of {typeof(Prepared<Script>)} returned by {nameof(Engine.PrepareScript)} are allowed only.", paramName);
+        }
+
+        [DoesNotReturn]
+        public static void ThrowInvalidPreparedModuleArgumentException(string paramName)
+        {
+            throw new ArgumentException($"Instances of {typeof(Prepared<AstModule>)} returned by {nameof(Engine.PrepareModule)} are allowed only.", paramName);
+        }
     }
 }

+ 26 - 6
Jint/Runtime/ExecutionContextStack.cs

@@ -19,29 +19,33 @@ namespace Jint.Runtime
         {
             var array = _stack._array;
             var size = _stack._size;
-            array[size - 1] = array[size - 1].UpdateLexicalEnvironment(newEnv);
+            ref var executionContext = ref array[size - 1];
+            executionContext = executionContext.UpdateLexicalEnvironment(newEnv);
         }
 
         public void ReplaceTopVariableEnvironment(Environment newEnv)
         {
             var array = _stack._array;
             var size = _stack._size;
-            array[size - 1] = array[size - 1].UpdateVariableEnvironment(newEnv);
+            ref var executionContext = ref array[size - 1];
+            executionContext = executionContext.UpdateVariableEnvironment(newEnv);
         }
 
         public void ReplaceTopPrivateEnvironment(PrivateEnvironment? newEnv)
         {
             var array = _stack._array;
             var size = _stack._size;
-            array[size - 1] = array[size - 1].UpdatePrivateEnvironment(newEnv);
+            ref var executionContext = ref array[size - 1];
+            executionContext = executionContext.UpdatePrivateEnvironment(newEnv);
         }
 
         public ref readonly ExecutionContext ReplaceTopGenerator(GeneratorInstance newEnv)
         {
             var array = _stack._array;
             var size = _stack._size;
-            array[size - 1] = array[size - 1].UpdateGenerator(newEnv);
-            return ref array[size - 1];
+            ref var executionContext = ref array[size - 1];
+            executionContext = executionContext.UpdateGenerator(newEnv);
+            return ref executionContext;
         }
 
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -62,7 +66,7 @@ namespace Jint.Runtime
             var size = _stack._size;
             for (var i = size - 1; i > -1; --i)
             {
-                var context = array[i];
+                ref readonly var context = ref array[i];
                 if (context.ScriptOrModule is not null)
                 {
                     return context.ScriptOrModule;
@@ -71,5 +75,21 @@ namespace Jint.Runtime
 
             return null;
         }
+
+        public ParserOptions? GetActiveParserOptions()
+        {
+            var array = _stack._array;
+            var size = _stack._size;
+            for (var i = size - 1; i > -1; --i)
+            {
+                ref readonly var context = ref array[i];
+                if (context.ParserOptions is not null)
+                {
+                    return context.ParserOptions;
+                }
+            }
+
+            return null;
+        }
     }
 }

+ 1 - 1
Jint/Runtime/Modules/BuilderModule.cs

@@ -9,7 +9,7 @@ internal sealed class BuilderModule : SourceTextModule
 {
     private List<KeyValuePair<string, JsValue>> _exportBuilderDeclarations = new();
 
-    internal BuilderModule(Engine engine, Realm realm, Esprima.Ast.Module source, string? location, bool async)
+    internal BuilderModule(Engine engine, Realm realm, in Prepared<AstModule> source, string? location, bool async)
         : base(engine, realm, source, location, async)
     {
     }

+ 33 - 15
Jint/Runtime/Modules/ModuleBuilder.cs

@@ -8,19 +8,32 @@ public sealed class ModuleBuilder
 {
     private readonly Engine _engine;
     private readonly string _specifier;
-    private global::Esprima.Ast.Module? _module;
+    private Prepared<AstModule>? _module;
     private readonly List<string> _sourceRaw = new();
     private readonly Dictionary<string, JsValue> _exports = new(StringComparer.Ordinal);
-    private readonly ParserOptions _options;
+    private readonly ParserOptions _defaultParserOptions;
+    private readonly Parser _defaultParser;
+    private ModuleParsingOptions _parsingOptions;
 
     internal ModuleBuilder(Engine engine, string specifier)
     {
         _engine = engine;
         _specifier = specifier;
-        _options = new ParserOptions
+        _parsingOptions = ModuleParsingOptions.Default;
+        _defaultParserOptions = _parsingOptions.GetParserOptions(engine.Options);
+        _defaultParser = new Parser(_defaultParserOptions);
+    }
+
+    private Parser GetParserFor(ModuleParsingOptions parsingOptions, out ParserOptions parserOptions)
+    {
+        if (ReferenceEquals(parsingOptions, ModuleParsingOptions.Default))
         {
-            RegexTimeout = engine.Options.Constraints.RegexTimeout
-        };
+            parserOptions = _defaultParserOptions;
+            return _defaultParser;
+        }
+
+        parserOptions = parsingOptions.GetParserOptions(_engine.Options);
+        return new Parser(parserOptions);
     }
 
     public ModuleBuilder AddSource(string code)
@@ -33,7 +46,7 @@ public sealed class ModuleBuilder
         return this;
     }
 
-    public ModuleBuilder AddModule(global::Esprima.Ast.Module module)
+    public ModuleBuilder AddModule(in Prepared<AstModule> preparedModule)
     {
         if (_sourceRaw.Count > 0)
         {
@@ -44,7 +57,7 @@ public sealed class ModuleBuilder
         {
             throw new InvalidOperationException("pre-compiled module already exists.");
         }
-        _module = module;
+        _module = preparedModule;
         return this;
     }
 
@@ -116,31 +129,36 @@ public sealed class ModuleBuilder
         return this;
     }
 
-    public ModuleBuilder WithOptions(Action<ParserOptions> configure)
+    public ModuleBuilder WithOptions(Func<ModuleParsingOptions, ModuleParsingOptions> configure)
     {
-        configure(_options);
+        _parsingOptions = configure(_parsingOptions);
         return this;
     }
 
-    internal global::Esprima.Ast.Module Parse()
+    internal Prepared<AstModule> Parse()
     {
-        if (_module != null) return _module;
+        if (_module != null) return _module.Value;
+
+        ParserOptions parserOptions;
         if (_sourceRaw.Count <= 0)
         {
-            return new global::Esprima.Ast.Module(NodeList.Create(Array.Empty<Statement>()));
+            parserOptions = ReferenceEquals(_parsingOptions, ModuleParsingOptions.Default)
+                ? _defaultParserOptions
+                : _parsingOptions.GetParserOptions(_engine.Options);
+            return new Prepared<AstModule>(new AstModule(NodeList.Create(Array.Empty<Statement>())), parserOptions);
         }
 
-        var parser = new Parser(_options);
+        var parser = GetParserFor(_parsingOptions, out parserOptions);
         try
         {
             var source = _sourceRaw.Count == 1 ? _sourceRaw[0] : string.Join(Environment.NewLine, _sourceRaw);
-            return parser.ParseModule(source, _specifier);
+            return new Prepared<AstModule>(parser.ParseModule(source, _specifier), parserOptions);
         }
         catch (ParseErrorException ex)
         {
             var location = SourceLocation.From(Position.From(ex.LineNumber, ex.Column), Position.From(ex.LineNumber, ex.Column), _specifier);
             ExceptionHelper.ThrowSyntaxError(_engine.Realm, $"Error while loading module: error in module '{_specifier}': {ex.Error}", location);
-            return null!;
+            return default;
         }
     }
 

+ 15 - 8
Jint/Runtime/Modules/ModuleFactory.cs

@@ -19,13 +19,15 @@ public static class ModuleFactory
     /// </remarks>
     /// <exception cref="ParseErrorException">Is thrown if the provided <paramref name="code"/> can not be parsed.</exception>
     /// <exception cref="JavaScriptException">Is thrown if an error occured when parsing <paramref name="code"/>.</exception>
-    public static Module BuildSourceTextModule(Engine engine, ResolvedSpecifier resolved, string code)
+    public static Module BuildSourceTextModule(Engine engine, ResolvedSpecifier resolved, string code, ModuleParsingOptions? parsingOptions = null)
     {
         var source = resolved.Uri?.LocalPath ?? resolved.Key;
-        Esprima.Ast.Module module;
+        AstModule module;
+        var parserOptions = (parsingOptions ?? ModuleParsingOptions.Default).GetParserOptions();
+        var parser = new Parser(parserOptions);
         try
         {
-            module = new Parser().ParseModule(code, source);
+            module = parser.ParseModule(code, source);
         }
         catch (ParseErrorException ex)
         {
@@ -38,20 +40,25 @@ public static class ModuleFactory
             module = null;
         }
 
-        return BuildSourceTextModule(engine, module);
+        return BuildSourceTextModule(engine, new Prepared<AstModule>(module, parserOptions));
     }
 
     /// <summary>
     /// Creates a <see cref="Module"/> for the usage within the given <paramref name="engine"/>
-    /// from the parsed <paramref name="parsedModule"/>.
+    /// from the parsed <paramref name="preparedModule"/>.
     /// </summary>
     /// <remarks>
     /// The returned modules location (see <see cref="Module.Location"/>) will be set
-    /// to <see cref="Location.Source"/> of the <paramref name="parsedModule"/>.
+    /// to <see cref="SourceLocation.Source"/> of the <paramref name="preparedModule"/>.
     /// </remarks>
-    public static Module BuildSourceTextModule(Engine engine, Esprima.Ast.Module parsedModule)
+    public static Module BuildSourceTextModule(Engine engine, Prepared<AstModule> preparedModule)
     {
-        return new SourceTextModule(engine, engine.Realm, parsedModule, parsedModule.Location.Source, async: false);
+        if (!preparedModule.IsValid)
+        {
+            ExceptionHelper.ThrowInvalidPreparedModuleArgumentException(nameof(preparedModule));
+        }
+
+        return new SourceTextModule(engine, engine.Realm, preparedModule, preparedModule.Program!.Location.Source, async: false);
     }
 
     /// <summary>

+ 9 - 5
Jint/Runtime/Modules/SourceTextModule.cs

@@ -1,4 +1,5 @@
-using Jint.Native.Object;
+using System.Diagnostics;
+using Jint.Native.Object;
 using Jint.Native.Promise;
 using Jint.Runtime.Environments;
 using Jint.Runtime.Interpreter;
@@ -20,7 +21,8 @@ internal sealed record ExportEntry(string? ExportName, ModuleRequest? ModuleRequ
 /// </summary>
 internal class SourceTextModule : CyclicModule
 {
-    internal readonly Esprima.Ast.Module _source;
+    internal readonly AstModule _source;
+    private readonly ParserOptions _parserOptions;
     private ExecutionContext _context;
     private ObjectInstance? _importMeta;
     private readonly List<ImportEntry>? _importEntries;
@@ -28,10 +30,12 @@ internal class SourceTextModule : CyclicModule
     private readonly List<ExportEntry> _indirectExportEntries;
     private readonly List<ExportEntry> _starExportEntries;
 
-    internal SourceTextModule(Engine engine, Realm realm, Esprima.Ast.Module source, string? location, bool async)
+    internal SourceTextModule(Engine engine, Realm realm, in Prepared<AstModule> source, string? location, bool async)
         : base(engine, realm, location, async)
     {
-        _source = source;
+        Debug.Assert(source.IsValid);
+        _source = source.Program!;
+        _parserOptions = source.ParserOptions!;
 
         // https://tc39.es/ecma262/#sec-parsemodule
 
@@ -323,7 +327,7 @@ internal class SourceTextModule : CyclicModule
     /// </summary>
     internal override Completion ExecuteModule(PromiseCapability? capability = null)
     {
-        var moduleContext = new ExecutionContext(this, _environment, _environment, privateEnvironment: null, _realm);
+        var moduleContext = new ExecutionContext(this, _environment, _environment, privateEnvironment: null, _realm, parserOptions: _parserOptions);
         if (!_hasTLA)
         {
             using (new StrictModeScope(strict: true, force: true))

+ 6 - 2
Jint/Runtime/Modules/SyntheticModule.cs

@@ -7,15 +7,18 @@ namespace Jint.Runtime.Modules;
 internal sealed class SyntheticModule : Module
 {
     private readonly JsValue _obj;
+    private readonly ParserOptions? _parserOptions;
     private readonly List<string> _exportNames = ["default"];
 
-    internal SyntheticModule(Engine engine, Realm realm, JsValue obj, string? location)
+    internal SyntheticModule(Engine engine, Realm realm, JsValue obj, string? location, ParserOptions? parserOptions = null)
         : base(engine, realm, location)
     {
         _obj = obj;
 
         var env = JintEnvironment.NewModuleEnvironment(_engine, realm.GlobalEnv);
         _environment = env;
+        _parserOptions = parserOptions;
+
     }
 
     public override List<string> GetExportedNames(List<CyclicModule>? exportStarSet = null) => _exportNames;
@@ -43,7 +46,8 @@ internal sealed class SyntheticModule : Module
             variableEnvironment: _environment,
             lexicalEnvironment: _environment,
             privateEnvironment: null,
-            generator: null);
+            generator: null,
+            parserOptions: _parserOptions);
 
         // 7.Suspend the currently running execution context.
         _engine.EnterExecutionContext(moduleContext);