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, JsCallArguments argumentsList)
{
Completion result;
JsArguments? argumentsInstance = null;
if (Function.Body is not FunctionBody)
{
// https://tc39.es/ecma262/#sec-runtime-semantics-evaluateconcisebody
_bodyExpression ??= JintExpression.Build((Expression) Function.Body);
if (Function.Async)
{
// local copies to prevent capturing closure created on top of method
var function = functionObject;
var jsValues = argumentsList;
var promiseCapability = PromiseConstructor.NewPromiseCapability(context.Engine, context.Engine.Realm.Intrinsics.Promise);
AsyncFunctionStart(context, promiseCapability, context =>
{
context.Engine.FunctionDeclarationInstantiation(function, jsValues);
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)
{
// local copies to prevent capturing closure created on top of method
var function = functionObject;
var arguments = argumentsList;
var promiseCapability = PromiseConstructor.NewPromiseCapability(context.Engine, context.Engine.Realm.Intrinsics.Promise);
_bodyStatementList ??= new JintStatementList(Function);
AsyncFunctionStart(context, promiseCapability, context =>
{
context.Engine.FunctionDeclarationInstantiation(function, arguments);
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, e.Error);
return;
}
if (result.Type == CompletionType.Normal)
{
promiseCapability.Resolve.Call(JsValue.Undefined, JsValue.Undefined);
}
else if (result.Type == CompletionType.Return)
{
promiseCapability.Resolve.Call(JsValue.Undefined, result.Value);
}
else
{
promiseCapability.Reject.Call(JsValue.Undefined, 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,
JsCallArguments 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 DeclarationCache? LexicalDeclarations;
public HashSet? ParameterBindings;
public List? VarsToInitialize;
public bool NeedsEvalContext;
internal readonly record struct VariableValuePair(Key Name, JsValue? InitialValue);
}
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;
state.ArgumentsObjectNeeded = true;
var thisMode = strict ? FunctionThisMode.Strict : FunctionThisMode.Global;
if (function.Type == NodeType.ArrowFunctionExpression)
{
thisMode = FunctionThisMode.Lexical;
}
if (thisMode == FunctionThisMode.Lexical || hasArguments)
{
state.ArgumentsObjectNeeded = false;
}
else if (!state.HasParameterExpressions)
{
if (state.FunctionNames.Contains(KnownKeys.Arguments) || lexicalNames?.Contains(KnownKeys.Arguments.Name) == true)
{
state.ArgumentsObjectNeeded = false;
}
}
if (state.ArgumentsObjectNeeded)
{
// just one extra check...
state.ArgumentsObjectNeeded = ArgumentsUsageAstVisitor.HasArgumentsReference(function);
}
state.NeedsEvalContext = !strict;
if (state.NeedsEvalContext)
{
// yet another extra check
state.NeedsEvalContext = EvalContextAstVisitor.HasEvalOrDebugger(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, InitialValue: null));
}
}
}
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)
{
state.LexicalDeclarations = DeclarationCacheBuilder.Build(hoistingScope._lexicalDeclarations);
}
return state;
}
private static void GetBoundNames(
Node parameter,
List target,
bool checkDuplicates,
ref bool hasRestParameter,
ref bool hasParameterExpressions,
ref bool hasDuplicates,
ref bool hasArguments)
{
Start:
if (parameter.Type == NodeType.Identifier)
{
var key = (Key) ((Identifier) parameter).Name;
target.Add(key);
hasDuplicates |= checkDuplicates && target.Contains(key);
hasArguments |= key == KnownKeys.Arguments;
return;
}
while (true)
{
if (parameter.Type == NodeType.RestElement)
{
hasRestParameter = true;
parameter = ((RestElement) parameter).Argument;
continue;
}
if (parameter.Type == NodeType.ArrayPattern)
{
foreach (var element in ((ArrayPattern) parameter).Elements.AsSpan())
{
if (element is null)
{
continue;
}
if (element.Type == NodeType.RestElement)
{
hasRestParameter = true;
parameter = ((RestElement) element).Argument;
goto Start;
}
GetBoundNames(
element,
target,
checkDuplicates,
ref hasRestParameter,
ref hasParameterExpressions,
ref hasDuplicates,
ref hasArguments);
}
}
else if (parameter.Type == NodeType.ObjectPattern)
{
foreach (var property in ((ObjectPattern) parameter).Properties.AsSpan())
{
if (property.Type == NodeType.RestElement)
{
hasRestParameter = true;
parameter = ((RestElement) property).Argument;
goto Start;
}
GetBoundNames(
((AssignmentProperty) property).Value,
target,
checkDuplicates,
ref hasRestParameter,
ref hasParameterExpressions,
ref hasDuplicates,
ref hasArguments);
}
}
else if (parameter.Type == NodeType.AssignmentPattern)
{
var assignmentPattern = (AssignmentPattern) parameter;
hasParameterExpressions |= ExpressionAstVisitor.HasExpression(assignmentPattern.ChildNodes);
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);
foreach (var parameter in function.Params.AsSpan())
{
var type = parameter.Type;
if (type == NodeType.Identifier)
{
var key = (Key) ((Identifier) parameter).Name;
state.HasDuplicates |= parameterNames.Contains(key);
hasArguments |= key == KnownKeys.Arguments;
parameterNames.Add(key);
}
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;
}
foreach (var parameter in function.Params.AsSpan())
{
if (HasArgumentsReference(parameter))
{
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;
}
}
private static class EvalContextAstVisitor
{
public static bool HasEvalOrDebugger(IFunction function)
{
if (HasEvalOrDebugger(function.Body))
{
return true;
}
return false;
}
private static bool HasEvalOrDebugger(Node node)
{
foreach (var childNode in node.ChildNodes)
{
var childType = childNode.Type;
if (childType == NodeType.DebuggerStatement)
{
return true;
}
if (childType == NodeType.CallExpression)
{
if (((CallExpression) childNode).Callee is Identifier identifier && identifier.Name.Equals("eval", StringComparison.Ordinal))
{
return true;
}
}
else if (childType != NodeType.FunctionDeclaration && !childNode.ChildNodes.IsEmpty())
{
if (HasEvalOrDebugger(childNode))
{
return true;
}
}
}
return false;
}
}
private static class ExpressionAstVisitor
{
internal static bool HasExpression(ChildNodes nodes)
{
foreach (var childNode in nodes)
{
switch (childNode.Type)
{
case NodeType.ArrowFunctionExpression:
case NodeType.FunctionExpression:
case NodeType.CallExpression:
case NodeType.AssignmentExpression:
return true;
case NodeType.Identifier:
case NodeType.Literal:
continue;
default:
if (!childNode.ChildNodes.IsEmpty())
{
if (HasExpression(childNode.ChildNodes))
{
return true;
}
}
break;
}
}
return false;
}
}
}