Browse Source

Compile Regex instances in ScriptPreparation (#1757)

* move node prepossessing to preparation
Marko Lahma 1 year ago
parent
commit
60cbc25bd7

+ 49 - 2
Jint.Tests/Runtime/EngineTests.ScriptPreparation.cs

@@ -1,4 +1,9 @@
+using System.Text.RegularExpressions;
 using Esprima.Ast;
+using Jint.Native;
+using Jint.Runtime.Interpreter;
+using Jint.Runtime.Interpreter.Expressions;
+using Jint.Runtime.Interpreter.Statements;
 
 namespace Jint.Tests.Runtime;
 
@@ -11,11 +16,53 @@ public partial class EngineTests
         Assert.IsType<ReturnStatement>(preparedScript.Body[0]);
     }
 
-    // TODO when folding will be part of preparation
-    // [Fact]
+    [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 init = Assert.IsType<RegExpLiteral>(declaration.Declarations[0].Init);
+        var regex = Assert.IsType<Regex>(init.AssociatedData);
+        Assert.Equal("[cgt]", regex.ToString());
+        Assert.Equal(RegexOptions.Compiled, regex.Options & RegexOptions.Compiled);
+
+        Assert.Equal(1, _engine.Evaluate(script));
+    }
+
+    [Fact]
     public void ScriptPreparationFoldsConstants()
     {
         var preparedScript = Engine.PrepareScript("return 1 + 2;");
         var returnStatement = Assert.IsType<ReturnStatement>(preparedScript.Body[0]);
+        var constant = Assert.IsType<JintConstantExpression>(returnStatement.Argument?.AssociatedData);
+        Assert.Equal(3, constant.GetValue(null!));
+
+        Assert.Equal(3, _engine.Evaluate(preparedScript));
+    }
+
+    [Fact]
+    public void ScriptPreparationOptimizesNegatingUnaryExpression()
+    {
+        var preparedScript = Engine.PrepareScript("-1");
+        var expression = Assert.IsType<ExpressionStatement>(preparedScript.Body[0]);
+        var unaryExpression = Assert.IsType<UnaryExpression>(expression.Expression);
+        var constant = Assert.IsType<JintConstantExpression>(unaryExpression.AssociatedData);
+
+        Assert.Equal(-1, constant.GetValue(null!));
+        Assert.Equal(-1, _engine.Evaluate(preparedScript));
+    }
+
+    [Fact]
+    public void ScriptPreparationOptimizesConstantReturn()
+    {
+        var preparedScript = Engine.PrepareScript("return false;");
+        var statement = Assert.IsType<ReturnStatement>(preparedScript.Body[0]);
+        var returnStatement = Assert.IsType<ConstantStatement>(statement.AssociatedData);
+
+        var builtStatement = JintStatement.Build(statement);
+        Assert.Same(returnStatement, builtStatement);
+
+        var result = builtStatement.Execute(new EvaluationContext(_engine)).Value;
+        Assert.Equal(JsBoolean.False, result);
     }
 }

+ 105 - 13
Jint/Engine.Ast.cs

@@ -1,9 +1,12 @@
 using System.Runtime.InteropServices;
+using System.Text.RegularExpressions;
 using Esprima;
 using Esprima.Ast;
 using Jint.Native;
+using Jint.Runtime;
 using Jint.Runtime.Interpreter;
 using Jint.Runtime.Interpreter.Expressions;
+using Jint.Runtime.Interpreter.Statements;
 using Environment = Jint.Runtime.Environments.Environment;
 
 namespace Jint;
@@ -18,11 +21,10 @@ public partial class Engine
     /// </remarks>
     public static Script PrepareScript(string script, string? source = null, bool strict = false)
     {
-        var astAnalyzer = new AstAnalyzer();
+        var astAnalyzer = new AstAnalyzer(new ScriptPreparationOptions());
         var options = ParserOptions.Default with
         {
-            AllowReturnOutsideFunction = true,
-            OnNodeCreated = astAnalyzer.NodeVisitor
+            AllowReturnOutsideFunction = true, OnNodeCreated = astAnalyzer.NodeVisitor
         };
 
         return new JavaScriptParser(options).ParseScript(script, source, strict);
@@ -36,47 +38,137 @@ public partial class Engine
     /// </remarks>
     public static Module PrepareModule(string script, string? source = null)
     {
-        var astAnalyzer = new AstAnalyzer();
-        var options = ParserOptions.Default with { OnNodeCreated = astAnalyzer.NodeVisitor };
+        var astAnalyzer = new AstAnalyzer(new ScriptPreparationOptions());
+        var options = ParserOptions.Default with
+        {
+            OnNodeCreated = astAnalyzer.NodeVisitor
+        };
 
         return new JavaScriptParser(options).ParseModule(script, source);
     }
 
+    [StructLayout(LayoutKind.Auto)]
+    private readonly record struct ScriptPreparationOptions(bool CompileRegex, bool FoldConstants)
+    {
+        public ScriptPreparationOptions() : this(CompileRegex: true, FoldConstants: true)
+        {
+        }
+    }
+
     private sealed class AstAnalyzer
     {
+        private readonly ScriptPreparationOptions _options;
         private readonly Dictionary<string, Environment.BindingName> _bindingNames = new(StringComparer.Ordinal);
+        private readonly Dictionary<string, Regex> _regexes = new(StringComparer.Ordinal);
+
+        public AstAnalyzer(ScriptPreparationOptions options)
+        {
+            _options = options;
+        }
 
         public void NodeVisitor(Node node)
         {
             switch (node.Type)
             {
                 case Nodes.Identifier:
+                    var identifier = (Identifier) node;
+                    var name = identifier.Name;
+
+                    if (!_bindingNames.TryGetValue(name, out var bindingName))
                     {
-                        var name = ((Identifier) node).Name;
+                        _bindingNames[name] = bindingName = new Environment.BindingName(JsString.CachedCreate(name));
+                    }
+
+                    node.AssociatedData = new JintIdentifierExpression(identifier, bindingName);
+                    break;
 
-                        if (!_bindingNames.TryGetValue(name, out var bindingName))
+                case Nodes.Literal:
+                    var literal = (Literal) node;
+
+                    var constantValue = JintLiteralExpression.ConvertToJsValue(literal);
+                    node.AssociatedData = constantValue is not null ? new JintConstantExpression(literal, constantValue) : null;
+
+                    if (node.AssociatedData is null && literal.TokenType == TokenType.RegularExpression && _options.CompileRegex)
+                    {
+                        var regExpLiteral = (RegExpLiteral) literal;
+                        var regExpParseResult = regExpLiteral.ParseResult;
+                        if (regExpParseResult.Success)
                         {
-                            _bindingNames[name] = bindingName = new Environment.BindingName(JsString.CachedCreate(name));
-                        }
+                            if (!_regexes.TryGetValue(regExpLiteral.Raw, out var regex))
+                            {
+                                regex = regExpParseResult.Regex!;
+                                if ((regex.Options & RegexOptions.Compiled) == RegexOptions.None)
+                                {
+                                    regex = new Regex(regex.ToString(), regex.Options | RegexOptions.Compiled, regex.MatchTimeout);
+                                }
 
-                        node.AssociatedData = bindingName;
-                        break;
+                                _regexes[regExpLiteral.Raw] = regex;
+                            }
+
+                            regExpLiteral.AssociatedData = regex;
+                        }
                     }
-                case Nodes.Literal:
-                    node.AssociatedData = JintLiteralExpression.ConvertToJsValue((Literal) node);
+
                     break;
+
                 case Nodes.MemberExpression:
                     node.AssociatedData = JintMemberExpression.InitializeDeterminedProperty((MemberExpression) node, cache: true);
                     break;
+
                 case Nodes.ArrowFunctionExpression:
                 case Nodes.FunctionDeclaration:
                 case Nodes.FunctionExpression:
                     var function = (IFunction) node;
                     node.AssociatedData = JintFunctionDefinition.BuildState(function);
                     break;
+
                 case Nodes.Program:
                     node.AssociatedData = new CachedHoistingScope((Program) node);
                     break;
+
+                case Nodes.UnaryExpression:
+                    node.AssociatedData = JintUnaryExpression.BuildConstantExpression((UnaryExpression) node);
+                    break;
+
+                case Nodes.BinaryExpression:
+                    var binaryExpression = (BinaryExpression) node;
+                    if (_options.FoldConstants
+                        && binaryExpression.Operator != BinaryOperator.InstanceOf
+                        && binaryExpression.Operator != BinaryOperator.In
+                        && binaryExpression is { Left: Literal leftLiteral, Right: Literal rightLiteral })
+                    {
+                        var left = JintLiteralExpression.ConvertToJsValue(leftLiteral);
+                        var right = JintLiteralExpression.ConvertToJsValue(rightLiteral);
+
+                        if (left is not null && right is not null)
+                        {
+                            // we have fixed result
+                            try
+                            {
+                                var result = JintBinaryExpression.Build(binaryExpression);
+                                var context = new EvaluationContext();
+                                node.AssociatedData = new JintConstantExpression(binaryExpression, (JsValue) result.EvaluateWithoutNodeTracking(context));
+                            }
+                            catch
+                            {
+                                // probably caused an error and error reporting doesn't work without engine
+                            }
+                        }
+                    }
+
+                    break;
+
+                case Nodes.ReturnStatement:
+                    var returnStatement = (ReturnStatement) node;
+                    if (returnStatement.Argument is Literal returnedLiteral)
+                    {
+                        var returnValue = JintLiteralExpression.ConvertToJsValue(returnedLiteral);
+                        if (returnValue is not null)
+                        {
+                            node.AssociatedData = new ConstantStatement(returnStatement, CompletionType.Return, returnValue);
+                        }
+                    }
+                    break;
             }
         }
     }

+ 1 - 1
Jint/Native/RegExp/RegExpPrototype.cs

@@ -878,7 +878,7 @@ namespace Jint.Native.RegExp
             var fullUnicode = R.FullUnicode;
             var hasIndices = R.Indices;
 
-            if (!global & !sticky && !fullUnicode && !hasIndices)
+            if (!global && !sticky && !fullUnicode && !hasIndices)
             {
                 // we can the non-stateful fast path which is the common case
                 var m = matcher.Match(s, (int) lastIndex);

+ 2 - 0
Jint/Runtime/Interpreter/Expressions/JintConstantExpression.cs

@@ -15,6 +15,8 @@ internal sealed class JintConstantExpression : JintExpression
         _value = value;
     }
 
+    public JsValue Value => _value;
+
     public override JsValue GetValue(EvaluationContext context) => _value;
 
     protected override object EvaluateInternal(EvaluationContext context) => _value;

+ 5 - 0
Jint/Runtime/Interpreter/Expressions/JintExpression.cs

@@ -90,6 +90,11 @@ namespace Jint.Runtime.Interpreter.Expressions
 
         protected internal static JintExpression Build(Expression expression)
         {
+            if (expression.AssociatedData is JintExpression preparedExpression)
+            {
+                return preparedExpression;
+            }
+
             var result = expression.Type switch
             {
                 Nodes.AssignmentExpression => JintAssignmentExpression.Build((AssignmentExpression) expression),

+ 9 - 28
Jint/Runtime/Interpreter/Expressions/JintIdentifierExpression.cs

@@ -9,50 +9,31 @@ namespace Jint.Runtime.Interpreter.Expressions;
 
 internal sealed class JintIdentifierExpression : JintExpression
 {
-    private Environment.BindingName _identifier = null!;
-    private bool _initialized;
+    private readonly Environment.BindingName _identifier;
 
-    public JintIdentifierExpression(Identifier expression) : base(expression)
+    public JintIdentifierExpression(Identifier expression) : this(expression, new Environment.BindingName(expression.Name))
     {
+        _identifier = new Environment.BindingName(((Identifier) _expression).Name);
     }
 
-    public Environment.BindingName Identifier
+    public JintIdentifierExpression(Identifier identifier, Environment.BindingName bindingName) : base(identifier)
     {
-        get
-        {
-            EnsureIdentifier();
-            return _identifier;
-        }
+        _identifier = bindingName;
     }
 
-    private void Initialize()
-    {
-        EnsureIdentifier();
-    }
-
-    [MethodImpl(MethodImplOptions.AggressiveInlining)]
-    private void EnsureIdentifier()
-    {
-        _identifier ??= _expression.AssociatedData as Environment.BindingName ?? new Environment.BindingName(((Identifier) _expression).Name);
-    }
+    public Environment.BindingName Identifier => _identifier;
 
     public bool HasEvalOrArguments
     {
         get
         {
-            var name = ((Identifier) _expression).Name;
-            return name is "eval" or "arguments";
+            var key = _identifier.Key;
+            return key == KnownKeys.Eval || key == KnownKeys.Arguments;
         }
     }
 
     protected override object EvaluateInternal(EvaluationContext context)
     {
-        if (!_initialized)
-        {
-            Initialize();
-            _initialized = true;
-        }
-
         var engine = context.Engine;
         var env = engine.ExecutionContext.LexicalEnvironment;
         var strict = StrictModeScope.IsStrictModeCode;
@@ -93,7 +74,7 @@ internal sealed class JintIdentifierExpression : JintExpression
         else
         {
             var reference = engine._referencePool.Rent(JsValue.Undefined, identifier.Value, strict, thisValue: null);
-            value = engine.GetValue(reference, true);
+            value = engine.GetValue(reference, returnReferenceToPool: true);
         }
 
         // make sure arguments access freezes state

+ 25 - 30
Jint/Runtime/Interpreter/Expressions/JintLiteralExpression.cs

@@ -1,4 +1,5 @@
 using System.Numerics;
+using System.Text.RegularExpressions;
 using Esprima;
 using Esprima.Ast;
 using Jint.Native;
@@ -27,35 +28,28 @@ namespace Jint.Runtime.Interpreter.Expressions
 
         internal static JsValue? ConvertToJsValue(Literal literal)
         {
-            if (literal.TokenType == TokenType.BooleanLiteral)
+            switch (literal.TokenType)
             {
-                return literal.BooleanValue!.Value ? JsBoolean.True : JsBoolean.False;
-            }
-
-            if (literal.TokenType == TokenType.NullLiteral)
-            {
-                return JsValue.Null;
-            }
-
-            if (literal.TokenType == TokenType.NumericLiteral)
-            {
-                // unbox only once
-                var numericValue = (double) literal.Value!;
-                var intValue = (int) numericValue;
-                return numericValue == intValue
-                       && (intValue != 0 || BitConverter.DoubleToInt64Bits(numericValue) != JsNumber.NegativeZeroBits)
-                    ? JsNumber.Create(intValue)
-                    : JsNumber.Create(numericValue);
-            }
-
-            if (literal.TokenType == TokenType.StringLiteral)
-            {
-                return JsString.Create((string) literal.Value!);
-            }
-
-            if (literal.TokenType == TokenType.BigIntLiteral)
-            {
-                return JsBigInt.Create((BigInteger) literal.Value!);
+                case TokenType.BooleanLiteral:
+                    return literal.BooleanValue!.Value ? JsBoolean.True : JsBoolean.False;
+                case TokenType.NullLiteral:
+                    return JsValue.Null;
+                case TokenType.NumericLiteral:
+                    {
+                        // unbox only once
+                        var numericValue = (double) literal.Value!;
+                        var intValue = (int) numericValue;
+                        return numericValue == intValue
+                               && (intValue != 0 || BitConverter.DoubleToInt64Bits(numericValue) != JsNumber.NegativeZeroBits)
+                            ? JsNumber.Create(intValue)
+                            : JsNumber.Create(numericValue);
+                    }
+                case TokenType.StringLiteral:
+                    return JsString.Create((string) literal.Value!);
+                case TokenType.BigIntLiteral:
+                    return JsBigInt.Create((BigInteger) literal.Value!);
+                case TokenType.RegularExpression:
+                    break;
             }
 
             return null;
@@ -75,11 +69,12 @@ namespace Jint.Runtime.Interpreter.Expressions
             var expression = (Literal) _expression;
             if (expression.TokenType == TokenType.RegularExpression)
             {
-                var regExpLiteral = (RegExpLiteral) _expression;
+                var regExpLiteral = (RegExpLiteral) expression;
                 var regExpParseResult = regExpLiteral.ParseResult;
                 if (regExpParseResult.Success)
                 {
-                    return context.Engine.Realm.Intrinsics.RegExp.Construct(regExpParseResult.Regex!, regExpLiteral.Regex.Pattern, regExpLiteral.Regex.Flags, regExpParseResult);
+                    var regex = regExpLiteral.AssociatedData as Regex ?? regExpParseResult.Regex!;
+                    return context.Engine.Realm.Intrinsics.RegExp.Construct(regex, regExpLiteral.Regex.Pattern, regExpLiteral.Regex.Flags, regExpParseResult);
                 }
 
                 ExceptionHelper.ThrowSyntaxError(context.Engine.Realm, $"Unsupported regular expression. {regExpParseResult.ConversionError!.Description}");

+ 10 - 24
Jint/Runtime/Interpreter/Expressions/JintUnaryExpression.cs

@@ -26,42 +26,28 @@ namespace Jint.Runtime.Interpreter.Expressions
 
         internal static JintExpression Build(UnaryExpression expression)
         {
-            if (expression.AssociatedData is JsValue cached)
-            {
-                return new JintConstantExpression(expression, cached);
-            }
-
             if (expression.Operator == UnaryOperator.TypeOf)
             {
-                if (expression.Argument is Literal l)
-                {
-                    var value = JintLiteralExpression.ConvertToJsValue(l);
-                    if (value is not null)
-                    {
-                        // valid for caching
-                        var evaluatedValue = JintTypeOfExpression.GetTypeOfString(value);
-                        expression.AssociatedData = evaluatedValue;
-                        return new JintConstantExpression(expression, evaluatedValue);
-                    }
-                }
-
                 return new JintTypeOfExpression(expression);
             }
 
-            if (expression.Operator == UnaryOperator.Minus
-                && expression.Argument is Literal literal)
+            return BuildConstantExpression(expression) ?? new JintUnaryExpression(expression);
+        }
+
+        internal static JintExpression? BuildConstantExpression(UnaryExpression expression)
+        {
+            if (expression is { Operator: UnaryOperator.Minus, Argument: Literal literal })
             {
                 var value = JintLiteralExpression.ConvertToJsValue(literal);
                 if (value is not null)
                 {
                     // valid for caching
                     var evaluatedValue = EvaluateMinus(value);
-                    expression.AssociatedData = evaluatedValue;
                     return new JintConstantExpression(expression, evaluatedValue);
                 }
             }
 
-            return new JintUnaryExpression(expression);
+            return null;
         }
 
         private sealed class JintTypeOfExpression : JintExpression
@@ -94,7 +80,7 @@ namespace Jint.Runtime.Interpreter.Expressions
                         return JsString.UndefinedString;
                     }
 
-                    v = engine.GetValue(rf, true);
+                    v = engine.GetValue(rf, returnReferenceToPool: true);
                 }
                 else
                 {
@@ -104,7 +90,7 @@ namespace Jint.Runtime.Interpreter.Expressions
                 return GetTypeOfString(v);
             }
 
-            internal static JsString GetTypeOfString(JsValue v)
+            private static JsString GetTypeOfString(JsValue v)
             {
                 if (v.IsUndefined())
                 {
@@ -268,7 +254,7 @@ namespace Jint.Runtime.Interpreter.Expressions
             }
         }
 
-        private static JsValue EvaluateMinus(JsValue value)
+        internal static JsValue EvaluateMinus(JsValue value)
         {
             if (value.IsInteger())
             {

+ 18 - 0
Jint/Runtime/Interpreter/Statements/ConstantReturnStatement.cs

@@ -0,0 +1,18 @@
+using Esprima.Ast;
+using Jint.Native;
+
+namespace Jint.Runtime.Interpreter.Statements;
+
+internal sealed class ConstantStatement : JintStatement
+{
+    private readonly JsValue _value;
+    private CompletionType _completionType;
+
+    public ConstantStatement(Statement statement, CompletionType completionType, JsValue value) : base(statement)
+    {
+        _completionType = completionType;
+        _value = value;
+    }
+
+    protected override Completion ExecuteInternal(EvaluationContext context) => new(_completionType, _value, _statement);
+}

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

@@ -58,6 +58,11 @@ namespace Jint.Runtime.Interpreter.Statements
 
         protected internal static JintStatement Build(Statement statement)
         {
+            if (statement.AssociatedData is JintStatement preparedStatement)
+            {
+                return preparedStatement;
+            }
+
             JintStatement? result = statement.Type switch
             {
                 Nodes.BlockStatement => new JintBlockStatement((BlockStatement) statement),

+ 0 - 1
Jint/Runtime/Interpreter/Statements/JintVariableDeclaration.cs

@@ -24,7 +24,6 @@ namespace Jint.Runtime.Interpreter.Statements
 
         protected override void Initialize(EvaluationContext context)
         {
-            var engine = context.Engine;
             _declarations = new ResolvedDeclaration[_statement.Declarations.Count];
             for (var i = 0; i < _declarations.Length; i++)
             {