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;
public partial class Engine
{
///
/// Prepares a script for the engine that includes static analysis data to speed up execution during run-time.
///
///
/// Returned instance is reusable and thread-safe. You should prepare scripts only once and then reuse them.
///
public static Script PrepareScript(string script, string? source = null, bool strict = false)
{
var astAnalyzer = new AstAnalyzer(new ScriptPreparationOptions());
var options = ParserOptions.Default with
{
AllowReturnOutsideFunction = true, OnNodeCreated = astAnalyzer.NodeVisitor
};
return new JavaScriptParser(options).ParseScript(script, source, strict);
}
///
/// Prepares a module for the engine that includes static analysis data to speed up execution during run-time.
///
///
/// Returned instance is reusable and thread-safe. You should prepare modules only once and then reuse them.
///
public static Module PrepareModule(string script, string? source = null)
{
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 _bindingNames = new(StringComparer.Ordinal);
private readonly Dictionary _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))
{
_bindingNames[name] = bindingName = new Environment.BindingName(JsString.CachedCreate(name));
}
node.AssociatedData = new JintIdentifierExpression(identifier, bindingName);
break;
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;
// 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 (!_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);
}
_regexes[regExpLiteral.Raw] = regex;
}
regExpLiteral.AssociatedData = regex;
}
}
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;
}
}
}
}
internal sealed class CachedHoistingScope
{
public CachedHoistingScope(Program program)
{
Scope = HoistingScope.GetProgramLevelDeclarations(program);
VarNames = new List();
GatherVarNames(Scope, VarNames);
LexNames = new List();
GatherLexNames(Scope, LexNames);
}
internal static void GatherVarNames(HoistingScope scope, List boundNames)
{
var varDeclarations = scope._variablesDeclarations;
if (varDeclarations != null)
{
for (var i = 0; i < varDeclarations.Count; i++)
{
var d = varDeclarations[i];
d.GetBoundNames(boundNames);
}
}
}
internal static void GatherLexNames(HoistingScope scope, List boundNames)
{
var lexDeclarations = scope._lexicalDeclarations;
if (lexDeclarations != null)
{
var temp = new List();
for (var i = 0; i < lexDeclarations.Count; i++)
{
var d = lexDeclarations[i];
temp.Clear();
d.GetBoundNames(temp);
for (var j = 0; j < temp.Count; j++)
{
boundNames.Add(new CachedLexicalName(temp[j], d.IsConstantDeclaration()));
}
}
}
}
[StructLayout(LayoutKind.Auto)]
internal readonly record struct CachedLexicalName(Key Name, bool Constant);
public HoistingScope Scope { get; }
public List VarNames { get; }
public List LexNames { get; }
}
internal static class AstPreparationExtensions
{
internal static HoistingScope GetHoistingScope(this Program program)
{
return program.AssociatedData is CachedHoistingScope cached ? cached.Scope : HoistingScope.GetProgramLevelDeclarations(program);
}
internal static List GetVarNames(this Program program, HoistingScope hoistingScope)
{
List boundNames;
if (program.AssociatedData is CachedHoistingScope cached)
{
boundNames = cached.VarNames;
}
else
{
boundNames = new List();
CachedHoistingScope.GatherVarNames(hoistingScope, boundNames);
}
return boundNames;
}
internal static List GetLexNames(this Program program, HoistingScope hoistingScope)
{
List boundNames;
if (program.AssociatedData is CachedHoistingScope cached)
{
boundNames = cached.LexNames;
}
else
{
boundNames = new List();
CachedHoistingScope.GatherLexNames(hoistingScope, boundNames);
}
return boundNames;
}
}