using System.Runtime.CompilerServices; using Jint.Native; using Jint.Native.Function; using Jint.Native.Generator; using Jint.Native.Promise; using Jint.Runtime.Environments; using Jint.Runtime.Interpreter.Expressions; namespace Jint.Runtime.Interpreter; /// /// Works as memento for function execution. Optimization to cache things that don't change. /// internal sealed class JintFunctionDefinition { private JintExpression? _bodyExpression; private JintStatementList? _bodyStatementList; public readonly string? Name; public readonly IFunction Function; public JintFunctionDefinition(IFunction function) { Function = function; Name = !string.IsNullOrEmpty(function.Id?.Name) ? function.Id!.Name : null; } public bool Strict => Function.IsStrict(); public FunctionThisMode ThisMode => Function.IsStrict() ? FunctionThisMode.Strict : FunctionThisMode.Global; /// /// https://tc39.es/ecma262/#sec-ordinarycallevaluatebody /// [MethodImpl(MethodImplOptions.AggressiveInlining | (MethodImplOptions) 512)] internal Completion EvaluateBody(EvaluationContext context, Function functionObject, JsValue[] argumentsList) { Completion result; JsArguments? argumentsInstance = null; if (Function.Expression) { // https://tc39.es/ecma262/#sec-runtime-semantics-evaluateconcisebody _bodyExpression ??= JintExpression.Build((Expression) Function.Body); if (Function.Async) { var promiseCapability = PromiseConstructor.NewPromiseCapability(context.Engine, context.Engine.Realm.Intrinsics.Promise); AsyncFunctionStart(context, promiseCapability, context => { context.Engine.FunctionDeclarationInstantiation(functionObject, argumentsList); context.RunBeforeExecuteStatementChecks(Function.Body); var jsValue = _bodyExpression.GetValue(context).Clone(); return new Completion(CompletionType.Return, jsValue, _bodyExpression._expression); }); result = new Completion(CompletionType.Return, promiseCapability.PromiseInstance, Function.Body); } else { argumentsInstance = context.Engine.FunctionDeclarationInstantiation(functionObject, argumentsList); context.RunBeforeExecuteStatementChecks(Function.Body); var jsValue = _bodyExpression.GetValue(context).Clone(); result = new Completion(CompletionType.Return, jsValue, Function.Body); } } else if (Function.Generator) { result = EvaluateGeneratorBody(context, functionObject, argumentsList); } else { if (Function.Async) { var promiseCapability = PromiseConstructor.NewPromiseCapability(context.Engine, context.Engine.Realm.Intrinsics.Promise); _bodyStatementList ??= new JintStatementList(Function); AsyncFunctionStart(context, promiseCapability, context => { context.Engine.FunctionDeclarationInstantiation(functionObject, argumentsList); return _bodyStatementList.Execute(context); }); result = new Completion(CompletionType.Return, promiseCapability.PromiseInstance, Function.Body); } else { // https://tc39.es/ecma262/#sec-runtime-semantics-evaluatefunctionbody argumentsInstance = context.Engine.FunctionDeclarationInstantiation(functionObject, argumentsList); _bodyStatementList ??= new JintStatementList(Function); result = _bodyStatementList.Execute(context); } } argumentsInstance?.FunctionWasCalled(); return result; } /// /// https://tc39.es/ecma262/#sec-async-functions-abstract-operations-async-function-start /// private static void AsyncFunctionStart(EvaluationContext context, PromiseCapability promiseCapability, Func asyncFunctionBody) { var runningContext = context.Engine.ExecutionContext; var asyncContext = runningContext; AsyncBlockStart(context, promiseCapability, asyncFunctionBody, asyncContext); } /// /// https://tc39.es/ecma262/#sec-asyncblockstart /// private static void AsyncBlockStart( EvaluationContext context, PromiseCapability promiseCapability, Func asyncBody, in ExecutionContext asyncContext) { var runningContext = context.Engine.ExecutionContext; // Set the code evaluation state of asyncContext such that when evaluation is resumed for that execution contxt the following steps will be performed: Completion result; try { result = asyncBody(context); } catch (JavaScriptException e) { promiseCapability.Reject.Call(JsValue.Undefined, new[] { e.Error }); return; } if (result.Type == CompletionType.Normal) { promiseCapability.Resolve.Call(JsValue.Undefined, new[] { JsValue.Undefined }); } else if (result.Type == CompletionType.Return) { promiseCapability.Resolve.Call(JsValue.Undefined, new[] { result.Value }); } else { promiseCapability.Reject.Call(JsValue.Undefined, new[] { result.Value }); } /* 4. Push asyncContext onto the execution context stack; asyncContext is now the running execution context. 5. Resume the suspended evaluation of asyncContext. Let result be the value returned by the resumed computation. 6. Assert: When we return here, asyncContext has already been removed from the execution context stack and runningContext is the currently running execution context. 7. Assert: result is a normal completion with a value of unused. The possible sources of this value are Await or, if the async function doesn't await anything, step 3.g above. 8. Return unused. */ } /// /// https://tc39.es/ecma262/#sec-runtime-semantics-evaluategeneratorbody /// private Completion EvaluateGeneratorBody( EvaluationContext context, Function functionObject, JsValue[] argumentsList) { var engine = context.Engine; engine.FunctionDeclarationInstantiation(functionObject, argumentsList); var G = engine.Realm.Intrinsics.Function.OrdinaryCreateFromConstructor( functionObject, static intrinsics => intrinsics.GeneratorFunction.PrototypeObject.PrototypeObject, static (Engine engine , Realm _, object? _) => new GeneratorInstance(engine)); _bodyStatementList ??= new JintStatementList(Function); _bodyStatementList.Reset(); G.GeneratorStart(_bodyStatementList); return new Completion(CompletionType.Return, G, Function.Body); } internal State Initialize() { var node = (Node) Function; var state = (State) (node.UserData ??= BuildState(Function)); return state; } internal sealed class State { public bool HasRestParameter; public int Length; public Key[] ParameterNames = null!; public bool HasDuplicates; public bool IsSimpleParameterList; public bool HasParameterExpressions; public bool ArgumentsObjectNeeded; public List? VarNames; public LinkedList? FunctionsToInitialize; public readonly HashSet FunctionNames = new(); public LexicalVariableDeclaration[] LexicalDeclarations = Array.Empty(); public HashSet? ParameterBindings; public List? VarsToInitialize; internal struct VariableValuePair { public Key Name; public JsValue? InitialValue; } internal struct LexicalVariableDeclaration { public bool IsConstantDeclaration; public List BoundNames; } } internal static State BuildState(IFunction function) { var state = new State(); ProcessParameters(function, state, out var hasArguments); var strict = function.IsStrict(); var hoistingScope = HoistingScope.GetFunctionLevelDeclarations(strict, function); var functionDeclarations = hoistingScope._functionDeclarations; var lexicalNames = hoistingScope._lexicalNames; state.VarNames = hoistingScope._varNames; LinkedList? functionsToInitialize = null; if (functionDeclarations != null) { functionsToInitialize = new LinkedList(); for (var i = functionDeclarations.Count - 1; i >= 0; i--) { var d = functionDeclarations[i]; var fn = d.Id!.Name; if (state.FunctionNames.Add(fn)) { functionsToInitialize.AddFirst(d); } } } state.FunctionsToInitialize = functionsToInitialize; const string ParameterNameArguments = "arguments"; state.ArgumentsObjectNeeded = true; var thisMode = strict ? FunctionThisMode.Strict : FunctionThisMode.Global; if (function.Type == NodeType.ArrowFunctionExpression) { thisMode = FunctionThisMode.Lexical; } if (thisMode == FunctionThisMode.Lexical) { state.ArgumentsObjectNeeded = false; } else if (hasArguments) { state.ArgumentsObjectNeeded = false; } else if (!state.HasParameterExpressions) { if (state.FunctionNames.Contains(ParameterNameArguments) || lexicalNames?.Contains(ParameterNameArguments) == true) { state.ArgumentsObjectNeeded = false; } } if (state.ArgumentsObjectNeeded) { // just one extra check... state.ArgumentsObjectNeeded = ArgumentsUsageAstVisitor.HasArgumentsReference(function); } var parameterBindings = new HashSet(state.ParameterNames); if (state.ArgumentsObjectNeeded) { parameterBindings.Add(KnownKeys.Arguments); } state.ParameterBindings = parameterBindings; var varsToInitialize = new List(); if (!state.HasParameterExpressions) { var instantiatedVarNames = state.VarNames != null ? new HashSet(state.ParameterBindings) : new HashSet(); for (var i = 0; i < state.VarNames?.Count; i++) { var n = state.VarNames[i]; if (instantiatedVarNames.Add(n)) { varsToInitialize.Add(new State.VariableValuePair { Name = n }); } } } else { var instantiatedVarNames = state.VarNames != null ? new HashSet(state.ParameterBindings) : null; for (var i = 0; i < state.VarNames?.Count; i++) { var n = state.VarNames[i]; if (instantiatedVarNames!.Add(n)) { JsValue? initialValue = null; if (!state.ParameterBindings.Contains(n) || state.FunctionNames.Contains(n)) { initialValue = JsValue.Undefined; } varsToInitialize.Add(new State.VariableValuePair { Name = n, InitialValue = initialValue }); } } } state.VarsToInitialize = varsToInitialize; if (hoistingScope._lexicalDeclarations != null) { var _lexicalDeclarations = hoistingScope._lexicalDeclarations; var lexicalDeclarationsCount = _lexicalDeclarations.Count; var declarations = new State.LexicalVariableDeclaration[lexicalDeclarationsCount]; for (var i = 0; i < lexicalDeclarationsCount; i++) { var d = _lexicalDeclarations[i]; var boundNames = new List(); d.GetBoundNames(boundNames); declarations[i] = new State.LexicalVariableDeclaration { IsConstantDeclaration = d.IsConstantDeclaration(), BoundNames = boundNames }; } state.LexicalDeclarations = declarations; } return state; } private static void GetBoundNames( Node? parameter, List target, bool checkDuplicates, ref bool _hasRestParameter, ref bool _hasParameterExpressions, ref bool _hasDuplicates, ref bool hasArguments) { if (parameter is Identifier identifier) { _hasDuplicates |= checkDuplicates && target.Contains(identifier.Name); target.Add(identifier.Name); hasArguments |= string.Equals(identifier.Name, "arguments", StringComparison.Ordinal); return; } while (true) { if (parameter is RestElement restElement) { _hasRestParameter = true; _hasParameterExpressions = true; parameter = restElement.Argument; continue; } if (parameter is ArrayPattern arrayPattern) { _hasParameterExpressions = true; ref readonly var arrayPatternElements = ref arrayPattern.Elements; for (var i = 0; i < arrayPatternElements.Count; i++) { var expression = arrayPatternElements[i]; GetBoundNames( expression, target, checkDuplicates, ref _hasRestParameter, ref _hasParameterExpressions, ref _hasDuplicates, ref hasArguments); } } else if (parameter is ObjectPattern objectPattern) { _hasParameterExpressions = true; ref readonly var objectPatternProperties = ref objectPattern.Properties; for (var i = 0; i < objectPatternProperties.Count; i++) { var property = objectPatternProperties[i]; if (property is AssignmentProperty p) { GetBoundNames( p.Value, target, checkDuplicates, ref _hasRestParameter, ref _hasParameterExpressions, ref _hasDuplicates, ref hasArguments); } else { _hasRestParameter = true; _hasParameterExpressions = true; parameter = ((RestElement) property).Argument; continue; } } } else if (parameter is AssignmentPattern assignmentPattern) { _hasParameterExpressions = true; parameter = assignmentPattern.Left; continue; } break; } } private static void ProcessParameters( IFunction function, State state, out bool hasArguments) { hasArguments = false; state.IsSimpleParameterList = true; var countParameters = true; ref readonly var functionDeclarationParams = ref function.Params; var count = functionDeclarationParams.Count; var parameterNames = new List(count); for (var i = 0; i < count; i++) { var parameter = functionDeclarationParams[i]; var type = parameter.Type; if (type == NodeType.Identifier) { var id = (Identifier) parameter; state.HasDuplicates |= parameterNames.Contains(id.Name); hasArguments = string.Equals(id.Name, "arguments", StringComparison.Ordinal); parameterNames.Add(id.Name); } else if (type != NodeType.Literal) { countParameters &= type != NodeType.AssignmentPattern; state.IsSimpleParameterList = false; GetBoundNames( parameter, parameterNames, checkDuplicates: true, ref state.HasRestParameter, ref state.HasParameterExpressions, ref state.HasDuplicates, ref hasArguments); } if (countParameters && type is NodeType.Identifier or NodeType.ObjectPattern or NodeType.ArrayPattern) { state.Length++; } } state.ParameterNames = parameterNames.ToArray(); } private static class ArgumentsUsageAstVisitor { public static bool HasArgumentsReference(IFunction function) { if (HasArgumentsReference(function.Body)) { return true; } ref readonly var parameters = ref function.Params; for (var i = 0; i < parameters.Count; ++i) { if (HasArgumentsReference(parameters[i])) { return true; } } return false; } private static bool HasArgumentsReference(Node node) { foreach (var childNode in node.ChildNodes) { var childType = childNode.Type; if (childType == NodeType.Identifier) { if (string.Equals(((Identifier) childNode).Name, "arguments", StringComparison.Ordinal)) { return true; } } else if (childType != NodeType.FunctionDeclaration && !childNode.ChildNodes.IsEmpty()) { if (HasArgumentsReference(childNode)) { return true; } } } return false; } } }