Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve recursion performance v1 #1269

Merged
merged 5 commits into from
Aug 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
43 changes: 43 additions & 0 deletions Jint.Tests/Runtime/EngineLimitTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Text;

namespace Jint.Tests.Runtime;

public class EngineLimitTests
{
[Fact]
public void ShouldAllowReasonableCallStackDepth()
{
#if RELEASE
const int FunctionNestingCount = 350;
#else
const int FunctionNestingCount = 170;
#endif

// generate call tree
var sb = new StringBuilder();
sb.AppendLine("var x = 10;");
sb.AppendLine();
for (var i = 1; i <= FunctionNestingCount; ++i)
{
sb.Append("function func").Append(i).Append("(func").Append(i).AppendLine("Param) {");
sb.Append(" ");
if (i != FunctionNestingCount)
{
// just to create a bit more nesting add some constructs
sb.Append("return x++ > 1 ? func").Append(i + 1).Append("(func").Append(i).AppendLine("Param): undefined;");
}
else
{
// use known CLR function to add breakpoint
sb.Append("return Math.max(0, func").Append(i).AppendLine("Param);");
}

sb.AppendLine("}");
sb.AppendLine();
}

var engine = new Engine();
engine.Execute(sb.ToString());
Assert.Equal(123, engine.Evaluate("func1(123);").AsNumber());
}
}
4 changes: 4 additions & 0 deletions Jint/Engine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1371,6 +1371,8 @@ ObjectInstance Callback()
JsValue[] arguments,
JintExpression? expression)
{
// ensure logic is in sync between Call, Construct and JintCallExpression!

var recursionDepth = CallStack.Push(functionInstance, expression, ExecutionContext);

if (recursionDepth > Options.Constraints.MaxRecursionDepth)
Expand Down Expand Up @@ -1402,6 +1404,8 @@ ObjectInstance Callback()
JsValue newTarget,
JintExpression? expression)
{
// ensure logic is in sync between Call, Construct and JintCallExpression!

var recursionDepth = CallStack.Push(functionInstance, expression, ExecutionContext);

if (recursionDepth > Options.Constraints.MaxRecursionDepth)
Expand Down
197 changes: 119 additions & 78 deletions Jint/Runtime/Interpreter/Expressions/JintCallExpression.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Esprima.Ast;
using Jint.Native;
using Jint.Native.Function;
Expand Down Expand Up @@ -69,99 +71,48 @@ static bool CanSpread(Node? e)

protected override ExpressionResult EvaluateInternal(EvaluationContext context)
{
return NormalCompletion(_calleeExpression is JintSuperExpression
? SuperCall(context)
: Call(context)
);
}

private JsValue SuperCall(EvaluationContext context)
{
var engine = context.Engine;
var thisEnvironment = (FunctionEnvironmentRecord) engine.ExecutionContext.GetThisEnvironment();
var newTarget = engine.GetNewTarget(thisEnvironment);
var func = GetSuperConstructor(thisEnvironment);
if (func is null || !func.IsConstructor)
if (_calleeExpression._expression.Type == Nodes.Super)
{
ExceptionHelper.ThrowTypeError(engine.Realm, "Not a constructor");
return NormalCompletion(SuperCall(context));
}

var argList = ArgumentListEvaluation(context);
var result = ((IConstructor) func).Construct(argList, newTarget ?? JsValue.Undefined);
var thisER = (FunctionEnvironmentRecord) engine.ExecutionContext.GetThisEnvironment();
return thisER.BindThisValue(result);
}

/// <summary>
/// https://tc39.es/ecma262/#sec-getsuperconstructor
/// </summary>
private static ObjectInstance? GetSuperConstructor(FunctionEnvironmentRecord thisEnvironment)
{
var envRec = thisEnvironment;
var activeFunction = envRec._functionObject;
var superConstructor = activeFunction.GetPrototypeOf();
return superConstructor;
}
// https://tc39.es/ecma262/#sec-function-calls

/// <summary>
/// https://tc39.es/ecma262/#sec-function-calls
/// </summary>
private JsValue Call(EvaluationContext context)
{
var reference = _calleeExpression.Evaluate(context).Value;

if (ReferenceEquals(reference, Undefined.Instance))
{
return Undefined.Instance;
return NormalCompletion(Undefined.Instance);
}

var engine = context.Engine;
var func = engine.GetValue(reference, false);

if (func.IsNullOrUndefined() && _expression.IsOptional())
{
return Undefined.Instance;
return NormalCompletion(Undefined.Instance);
}

if (reference is Reference referenceRecord
var referenceRecord = reference as Reference;
if (referenceRecord != null
&& !referenceRecord.IsPropertyReference()
&& referenceRecord.GetReferencedName() == CommonProperties.Eval
&& ReferenceEquals(func, engine.Realm.Intrinsics.Eval))
{
var argList = ArgumentListEvaluation(context);
if (argList.Length == 0)
{
return Undefined.Instance;
}

var evalFunctionInstance = (EvalFunctionInstance) func;
var evalArg = argList[0];
var strictCaller = StrictModeScope.IsStrictModeCode;
var evalRealm = evalFunctionInstance._realm;
var direct = !_expression.IsOptional();
var value = evalFunctionInstance.PerformEval(evalArg, evalRealm, strictCaller, direct);
engine._referencePool.Return(referenceRecord);
return value;
return HandleEval(context, func, engine, referenceRecord);
}

var thisCall = (CallExpression) _expression;
var tailCall = IsInTailPosition(thisCall);
return EvaluateCall(context, func, reference, thisCall.Arguments, tailCall);
}

/// <summary>
/// https://tc39.es/ecma262/#sec-evaluatecall
/// </summary>
private JsValue EvaluateCall(EvaluationContext context, JsValue func, object reference, in NodeList<Expression> arguments, bool tailPosition)
{
JsValue thisValue;
var referenceRecord = reference as Reference;
var engine = context.Engine;
// https://tc39.es/ecma262/#sec-evaluatecall

JsValue thisObject;
if (referenceRecord is not null)
{
if (referenceRecord.IsPropertyReference())
{
thisValue = referenceRecord.GetThisValue();
thisObject = referenceRecord.GetThisValue();
}
else
{
Expand All @@ -171,52 +122,142 @@ private JsValue EvaluateCall(EvaluationContext context, JsValue func, object ref
if (baseValue.IsNullOrUndefined()
&& engine._referenceResolver.TryUnresolvableReference(engine, referenceRecord, out var value))
{
thisValue = value;
thisObject = value;
}
else
{
var refEnv = (EnvironmentRecord) baseValue;
thisValue = refEnv.WithBaseObject();
thisObject = refEnv.WithBaseObject();
}
}
}
else
{
thisValue = Undefined.Instance;
thisObject = Undefined.Instance;
}

var argList = ArgumentListEvaluation(context);
var arguments = ArgumentListEvaluation(context);

if (!func.IsObject() && !engine._referenceResolver.TryGetCallable(engine, reference, out func))
{
var message = referenceRecord == null
? reference + " is not a function"
: $"Property '{referenceRecord.GetReferencedName()}' of object is not a function";
ExceptionHelper.ThrowTypeError(engine.Realm, message);
ThrowMemberisNotFunction(referenceRecord, reference, engine);
}

var callable = func as ICallable;
if (callable is null)
{
var message = $"{referenceRecord?.GetReferencedName() ?? reference} is not a function";
ExceptionHelper.ThrowTypeError(engine.Realm, message);
ThrowReferenceNotFunction(referenceRecord, reference, engine);
}

if (tailPosition)
if (tailCall)
{
// TODO tail call
// PrepareForTailCall();
}

var result = engine.Call(callable, thisValue, argList, _calleeExpression);
// ensure logic is in sync between Call, Construct and JintCallExpression!

if (!_cached && argList.Length > 0)
JsValue result;
if (callable is FunctionInstance functionInstance)
{
engine._jsValueArrayPool.ReturnArray(argList);
var callStack = engine.CallStack;
var recursionDepth = callStack.Push(functionInstance, _calleeExpression, engine.ExecutionContext);

if (recursionDepth > engine.Options.Constraints.MaxRecursionDepth)
{
// automatically pops the current element as it was never reached
ExceptionHelper.ThrowRecursionDepthOverflowException(callStack);
}

try
{
result = functionInstance.Call(thisObject, arguments);
}
finally
{
// if call stack was reset due to recursive call to engine or similar, we might not have it anymore
if (callStack.Count > 0)
{
callStack.Pop();
}
}
}
else
{
result = callable.Call(thisObject, arguments);
}

if (!_cached && arguments.Length > 0)
{
engine._jsValueArrayPool.ReturnArray(arguments);
}

engine._referencePool.Return(referenceRecord);
return result;
return NormalCompletion(result);
}

[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowReferenceNotFunction(Reference? referenceRecord1, object reference, Engine engine)
{
var message = $"{referenceRecord1?.GetReferencedName() ?? reference} is not a function";
ExceptionHelper.ThrowTypeError(engine.Realm, message);
}

[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowMemberisNotFunction(Reference? referenceRecord1, object reference, Engine engine)
{
var message = referenceRecord1 == null
? reference + " is not a function"
: $"Property '{referenceRecord1.GetReferencedName()}' of object is not a function";
ExceptionHelper.ThrowTypeError(engine.Realm, message);
}

private ExpressionResult HandleEval(EvaluationContext context, JsValue func, Engine engine, Reference referenceRecord)
{
var argList = ArgumentListEvaluation(context);
if (argList.Length == 0)
{
return NormalCompletion(Undefined.Instance);
}

var evalFunctionInstance = (EvalFunctionInstance) func;
var evalArg = argList[0];
var strictCaller = StrictModeScope.IsStrictModeCode;
var evalRealm = evalFunctionInstance._realm;
var direct = !_expression.IsOptional();
var value = evalFunctionInstance.PerformEval(evalArg, evalRealm, strictCaller, direct);
engine._referencePool.Return(referenceRecord);
return NormalCompletion(value);
}

private JsValue SuperCall(EvaluationContext context)
{
var engine = context.Engine;
var thisEnvironment = (FunctionEnvironmentRecord) engine.ExecutionContext.GetThisEnvironment();
var newTarget = engine.GetNewTarget(thisEnvironment);
var func = GetSuperConstructor(thisEnvironment);
if (func is null || !func.IsConstructor)
{
ExceptionHelper.ThrowTypeError(engine.Realm, "Not a constructor");
}

var argList = ArgumentListEvaluation(context);
var result = ((IConstructor) func).Construct(argList, newTarget ?? JsValue.Undefined);
var thisER = (FunctionEnvironmentRecord) engine.ExecutionContext.GetThisEnvironment();
return thisER.BindThisValue(result);
}

/// <summary>
/// https://tc39.es/ecma262/#sec-getsuperconstructor
/// </summary>
private static ObjectInstance? GetSuperConstructor(FunctionEnvironmentRecord thisEnvironment)
{
var envRec = thisEnvironment;
var activeFunction = envRec._functionObject;
var superConstructor = activeFunction.GetPrototypeOf();
return superConstructor;
}

/// <summary>
Expand Down
2 changes: 2 additions & 0 deletions Jint/Runtime/Interpreter/JintFunctionDefinition.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Runtime.CompilerServices;
using Esprima.Ast;
using Jint.Native;
using Jint.Native.Function;
Expand Down Expand Up @@ -36,6 +37,7 @@ internal sealed class JintFunctionDefinition
/// <summary>
/// https://tc39.es/ecma262/#sec-runtime-semantics-evaluatebody
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal Completion EvaluateBody(EvaluationContext context, FunctionInstance functionObject, JsValue[] argumentsList)
{
Completion result;
Expand Down
16 changes: 15 additions & 1 deletion Jint/Runtime/Interpreter/Statements/JintBlockStatement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,17 @@ protected override void Initialize(EvaluationContext context)

internal override bool SupportsResume => true;

protected override Completion ExecuteInternal(EvaluationContext context)
/// <summary>
/// Optimized for direct access without virtual dispatch.
/// </summary>
public Completion ExecuteBlock(EvaluationContext context)
{
if (_statementList is null)
{
_statementList = new JintStatementList(_statement, _statement.Body);
_lexicalDeclarations = HoistingScope.GetLexicalDeclarations(_statement);
}

EnvironmentRecord? oldEnv = null;
var engine = context.Engine;
if (_lexicalDeclarations != null)
Expand All @@ -41,5 +50,10 @@ protected override Completion ExecuteInternal(EvaluationContext context)

return blockValue;
}

protected override Completion ExecuteInternal(EvaluationContext context)
{
return ExecuteBlock(context);
}
}
}
4 changes: 2 additions & 2 deletions Jint/Runtime/Interpreter/Statements/JintDoWhileStatement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace Jint.Runtime.Interpreter.Statements
/// </summary>
internal sealed class JintDoWhileStatement : JintStatement<DoWhileStatement>
{
private JintStatement _body = null!;
private ProbablyBlockStatement _body;
private string? _labelSetName;
private JintExpression _test = null!;

Expand All @@ -19,7 +19,7 @@ public JintDoWhileStatement(DoWhileStatement statement) : base(statement)

protected override void Initialize(EvaluationContext context)
{
_body = Build(_statement.Body);
_body = new ProbablyBlockStatement(_statement.Body);
_test = JintExpression.Build(context.Engine, _statement.Test);
_labelSetName = _statement.LabelSet?.Name;
}
Expand Down