Engine.Ast.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. using System.Runtime.InteropServices;
  2. using System.Text.RegularExpressions;
  3. using Esprima;
  4. using Esprima.Ast;
  5. using Jint.Native;
  6. using Jint.Runtime;
  7. using Jint.Runtime.Interpreter;
  8. using Jint.Runtime.Interpreter.Expressions;
  9. using Jint.Runtime.Interpreter.Statements;
  10. using Environment = Jint.Runtime.Environments.Environment;
  11. namespace Jint;
  12. public partial class Engine
  13. {
  14. /// <summary>
  15. /// Prepares a script for the engine that includes static analysis data to speed up execution during run-time.
  16. /// </summary>
  17. /// <remarks>
  18. /// Returned instance is reusable and thread-safe. You should prepare scripts only once and then reuse them.
  19. /// </remarks>
  20. public static Script PrepareScript(string script, string? source = null, bool strict = false)
  21. {
  22. var astAnalyzer = new AstAnalyzer(new ScriptPreparationOptions());
  23. var options = ParserOptions.Default with
  24. {
  25. AllowReturnOutsideFunction = true, OnNodeCreated = astAnalyzer.NodeVisitor
  26. };
  27. return new JavaScriptParser(options).ParseScript(script, source, strict);
  28. }
  29. /// <summary>
  30. /// Prepares a module for the engine that includes static analysis data to speed up execution during run-time.
  31. /// </summary>
  32. /// <remarks>
  33. /// Returned instance is reusable and thread-safe. You should prepare modules only once and then reuse them.
  34. /// </remarks>
  35. public static Module PrepareModule(string script, string? source = null)
  36. {
  37. var astAnalyzer = new AstAnalyzer(new ScriptPreparationOptions());
  38. var options = ParserOptions.Default with
  39. {
  40. OnNodeCreated = astAnalyzer.NodeVisitor
  41. };
  42. return new JavaScriptParser(options).ParseModule(script, source);
  43. }
  44. [StructLayout(LayoutKind.Auto)]
  45. private readonly record struct ScriptPreparationOptions(bool CompileRegex, bool FoldConstants)
  46. {
  47. public ScriptPreparationOptions() : this(CompileRegex: true, FoldConstants: true)
  48. {
  49. }
  50. }
  51. private sealed class AstAnalyzer
  52. {
  53. private readonly ScriptPreparationOptions _options;
  54. private readonly Dictionary<string, Environment.BindingName> _bindingNames = new(StringComparer.Ordinal);
  55. private readonly Dictionary<string, Regex> _regexes = new(StringComparer.Ordinal);
  56. public AstAnalyzer(ScriptPreparationOptions options)
  57. {
  58. _options = options;
  59. }
  60. public void NodeVisitor(Node node)
  61. {
  62. switch (node.Type)
  63. {
  64. case Nodes.Identifier:
  65. var identifier = (Identifier) node;
  66. var name = identifier.Name;
  67. if (!_bindingNames.TryGetValue(name, out var bindingName))
  68. {
  69. _bindingNames[name] = bindingName = new Environment.BindingName(JsString.CachedCreate(name));
  70. }
  71. node.AssociatedData = new JintIdentifierExpression(identifier, bindingName);
  72. break;
  73. case Nodes.Literal:
  74. var literal = (Literal) node;
  75. var constantValue = JintLiteralExpression.ConvertToJsValue(literal);
  76. node.AssociatedData = constantValue is not null ? new JintConstantExpression(literal, constantValue) : null;
  77. if (node.AssociatedData is null && literal.TokenType == TokenType.RegularExpression && _options.CompileRegex)
  78. {
  79. var regExpLiteral = (RegExpLiteral) literal;
  80. var regExpParseResult = regExpLiteral.ParseResult;
  81. // only compile if there's no negative lookahead, it works incorrectly under NET 7 and NET 8
  82. // https://github.com/dotnet/runtime/issues/97455
  83. if (regExpParseResult.Success && !regExpLiteral.Raw.Contains("(?!"))
  84. {
  85. if (!_regexes.TryGetValue(regExpLiteral.Raw, out var regex))
  86. {
  87. regex = regExpParseResult.Regex!;
  88. if ((regex.Options & RegexOptions.Compiled) == RegexOptions.None)
  89. {
  90. regex = new Regex(regex.ToString(), regex.Options | RegexOptions.Compiled, regex.MatchTimeout);
  91. }
  92. _regexes[regExpLiteral.Raw] = regex;
  93. }
  94. regExpLiteral.AssociatedData = regex;
  95. }
  96. }
  97. break;
  98. case Nodes.MemberExpression:
  99. node.AssociatedData = JintMemberExpression.InitializeDeterminedProperty((MemberExpression) node, cache: true);
  100. break;
  101. case Nodes.ArrowFunctionExpression:
  102. case Nodes.FunctionDeclaration:
  103. case Nodes.FunctionExpression:
  104. var function = (IFunction) node;
  105. node.AssociatedData = JintFunctionDefinition.BuildState(function);
  106. break;
  107. case Nodes.Program:
  108. node.AssociatedData = new CachedHoistingScope((Program) node);
  109. break;
  110. case Nodes.UnaryExpression:
  111. node.AssociatedData = JintUnaryExpression.BuildConstantExpression((UnaryExpression) node);
  112. break;
  113. case Nodes.BinaryExpression:
  114. var binaryExpression = (BinaryExpression) node;
  115. if (_options.FoldConstants
  116. && binaryExpression.Operator != BinaryOperator.InstanceOf
  117. && binaryExpression.Operator != BinaryOperator.In
  118. && binaryExpression is { Left: Literal leftLiteral, Right: Literal rightLiteral })
  119. {
  120. var left = JintLiteralExpression.ConvertToJsValue(leftLiteral);
  121. var right = JintLiteralExpression.ConvertToJsValue(rightLiteral);
  122. if (left is not null && right is not null)
  123. {
  124. // we have fixed result
  125. try
  126. {
  127. var result = JintBinaryExpression.Build(binaryExpression);
  128. var context = new EvaluationContext();
  129. node.AssociatedData = new JintConstantExpression(binaryExpression, (JsValue) result.EvaluateWithoutNodeTracking(context));
  130. }
  131. catch
  132. {
  133. // probably caused an error and error reporting doesn't work without engine
  134. }
  135. }
  136. }
  137. break;
  138. case Nodes.ReturnStatement:
  139. var returnStatement = (ReturnStatement) node;
  140. if (returnStatement.Argument is Literal returnedLiteral)
  141. {
  142. var returnValue = JintLiteralExpression.ConvertToJsValue(returnedLiteral);
  143. if (returnValue is not null)
  144. {
  145. node.AssociatedData = new ConstantStatement(returnStatement, CompletionType.Return, returnValue);
  146. }
  147. }
  148. break;
  149. }
  150. }
  151. }
  152. }
  153. internal sealed class CachedHoistingScope
  154. {
  155. public CachedHoistingScope(Program program)
  156. {
  157. Scope = HoistingScope.GetProgramLevelDeclarations(program);
  158. VarNames = new List<Key>();
  159. GatherVarNames(Scope, VarNames);
  160. LexNames = new List<CachedLexicalName>();
  161. GatherLexNames(Scope, LexNames);
  162. }
  163. internal static void GatherVarNames(HoistingScope scope, List<Key> boundNames)
  164. {
  165. var varDeclarations = scope._variablesDeclarations;
  166. if (varDeclarations != null)
  167. {
  168. for (var i = 0; i < varDeclarations.Count; i++)
  169. {
  170. var d = varDeclarations[i];
  171. d.GetBoundNames(boundNames);
  172. }
  173. }
  174. }
  175. internal static void GatherLexNames(HoistingScope scope, List<CachedLexicalName> boundNames)
  176. {
  177. var lexDeclarations = scope._lexicalDeclarations;
  178. if (lexDeclarations != null)
  179. {
  180. var temp = new List<Key>();
  181. for (var i = 0; i < lexDeclarations.Count; i++)
  182. {
  183. var d = lexDeclarations[i];
  184. temp.Clear();
  185. d.GetBoundNames(temp);
  186. for (var j = 0; j < temp.Count; j++)
  187. {
  188. boundNames.Add(new CachedLexicalName(temp[j], d.IsConstantDeclaration()));
  189. }
  190. }
  191. }
  192. }
  193. [StructLayout(LayoutKind.Auto)]
  194. internal readonly record struct CachedLexicalName(Key Name, bool Constant);
  195. public HoistingScope Scope { get; }
  196. public List<Key> VarNames { get; }
  197. public List<CachedLexicalName> LexNames { get; }
  198. }
  199. internal static class AstPreparationExtensions
  200. {
  201. internal static HoistingScope GetHoistingScope(this Program program)
  202. {
  203. return program.AssociatedData is CachedHoistingScope cached ? cached.Scope : HoistingScope.GetProgramLevelDeclarations(program);
  204. }
  205. internal static List<Key> GetVarNames(this Program program, HoistingScope hoistingScope)
  206. {
  207. List<Key> boundNames;
  208. if (program.AssociatedData is CachedHoistingScope cached)
  209. {
  210. boundNames = cached.VarNames;
  211. }
  212. else
  213. {
  214. boundNames = new List<Key>();
  215. CachedHoistingScope.GatherVarNames(hoistingScope, boundNames);
  216. }
  217. return boundNames;
  218. }
  219. internal static List<CachedHoistingScope.CachedLexicalName> GetLexNames(this Program program, HoistingScope hoistingScope)
  220. {
  221. List<CachedHoistingScope.CachedLexicalName> boundNames;
  222. if (program.AssociatedData is CachedHoistingScope cached)
  223. {
  224. boundNames = cached.LexNames;
  225. }
  226. else
  227. {
  228. boundNames = new List<CachedHoistingScope.CachedLexicalName>();
  229. CachedHoistingScope.GatherLexNames(hoistingScope, boundNames);
  230. }
  231. return boundNames;
  232. }
  233. }