diff --git a/rhino/src/main/java/org/mozilla/javascript/ArrowFunction.java b/rhino/src/main/java/org/mozilla/javascript/ArrowFunction.java index 399d1daeed..3f7d3f3aed 100644 --- a/rhino/src/main/java/org/mozilla/javascript/ArrowFunction.java +++ b/rhino/src/main/java/org/mozilla/javascript/ArrowFunction.java @@ -36,8 +36,7 @@ public ArrowFunction( @Override public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { - Scriptable callThis = boundThis != null ? boundThis : ScriptRuntime.getTopCallScope(cx); - return targetFunction.call(cx, scope, callThis, args); + return targetFunction.call(cx, scope, getCallThis(cx), args); } @Override @@ -74,6 +73,14 @@ String decompile(int indent, int flags) { return super.decompile(indent, flags); } + Scriptable getCallThis(Context cx) { + return boundThis != null ? boundThis : ScriptRuntime.getTopCallScope(cx); + } + + Callable getTargetFunction() { + return targetFunction; + } + static boolean equalObjectGraphs(ArrowFunction f1, ArrowFunction f2, EqualObjectGraphs eq) { return eq.equalGraphs(f1.boundThis, f2.boundThis) && eq.equalGraphs(f1.targetFunction, f2.targetFunction); diff --git a/rhino/src/main/java/org/mozilla/javascript/BoundFunction.java b/rhino/src/main/java/org/mozilla/javascript/BoundFunction.java index ffc0702cc2..cb13a26e24 100644 --- a/rhino/src/main/java/org/mozilla/javascript/BoundFunction.java +++ b/rhino/src/main/java/org/mozilla/javascript/BoundFunction.java @@ -52,14 +52,7 @@ public BoundFunction( @Override public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] extraArgs) { - Scriptable callThis = boundThis; - if (callThis == null && ScriptRuntime.hasTopCall(cx)) { - callThis = ScriptRuntime.getTopCallScope(cx); - } - if (callThis == null) { - callThis = getTopLevelScope(scope); - } - return targetFunction.call(cx, scope, callThis, concat(boundArgs, extraArgs)); + return targetFunction.call(cx, scope, getCallThis(cx, scope), concat(boundArgs, extraArgs)); } @Override @@ -98,6 +91,25 @@ private static Object[] concat(Object[] first, Object[] second) { return args; } + Callable getTargetFunction() { + return targetFunction; + } + + Object[] getBoundArgs() { + return boundArgs; + } + + Scriptable getCallThis(Context cx, Scriptable scope) { + Scriptable callThis = boundThis; + if (callThis == null && ScriptRuntime.hasTopCall(cx)) { + callThis = ScriptRuntime.getTopCallScope(cx); + } + if (callThis == null) { + callThis = getTopLevelScope(scope); + } + return callThis; + } + static boolean equalObjectGraphs(BoundFunction f1, BoundFunction f2, EqualObjectGraphs eq) { return eq.equalGraphs(f1.boundThis, f2.boundThis) && eq.equalGraphs(f1.targetFunction, f2.targetFunction) diff --git a/rhino/src/main/java/org/mozilla/javascript/Interpreter.java b/rhino/src/main/java/org/mozilla/javascript/Interpreter.java index ef8c947f28..48a1844b8a 100644 --- a/rhino/src/main/java/org/mozilla/javascript/Interpreter.java +++ b/rhino/src/main/java/org/mozilla/javascript/Interpreter.java @@ -220,6 +220,19 @@ void initializeArgs( } } + // While maximum stack sizes are normally statically calculated by the compiler, in some + // situations we can dynamically need a larger stack, specifically when we're peeling bound + // functions, Function.apply, and no-such-method handlers for invocation. + Object[] ensureStackLength(int length) { + if (length > stack.length) { + stack = Arrays.copyOf(stack, length); + sDbl = Arrays.copyOf(sDbl, length); + // TODO: adjust idata idata.itsMaxFrameArray & idata.itsMaxStack so they start with + // larger stacks next time? Not clear this is always a good idea. + } + return stack; + } + CallFrame cloneFrozen() { if (!frozen) Kit.codeBug(); @@ -1761,6 +1774,136 @@ private static Object interpretLoop(Context cx, CallFrame frame, Object throwabl calleeScope = ScriptableObject.getTopLevelScope(frame.scope); } + // Iteratively reduce known function types: arrows, lambdas, + // bound functions, call/apply, and no-such-method-handler in + // order to make a best-effort to keep them in this interpreter + // loop so continuations keep working. The loop initializer and + // condition are formulated so that they short-circuit the loop + // if the function is already an interpreted function, which + // should be the majority of cases. + for (boolean notInt = !(fun instanceof InterpretedFunction); + notInt; ) { + if (fun instanceof ArrowFunction) { + ArrowFunction afun = (ArrowFunction) fun; + fun = afun.getTargetFunction(); + funThisObj = afun.getCallThis(cx); + } else if (fun instanceof LambdaFunction) { + fun = ((LambdaFunction) fun).getTarget(); + } else if (fun instanceof BoundFunction) { + BoundFunction bfun = (BoundFunction) fun; + fun = bfun.getTargetFunction(); + funThisObj = bfun.getCallThis(cx, calleeScope); + Object[] boundArgs = bfun.getBoundArgs(); + int blen = boundArgs.length; + if (blen > 0) { + stack = + frame.ensureStackLength( + blen + stackTop + 2 + indexReg); + sDbl = frame.sDbl; + System.arraycopy( + stack, + stackTop + 2, + stack, + stackTop + 2 + blen, + indexReg); + System.arraycopy( + sDbl, + stackTop + 2, + sDbl, + stackTop + 2 + blen, + indexReg); + System.arraycopy( + boundArgs, 0, stack, stackTop + 2, blen); + indexReg += blen; + } + } else if (fun instanceof IdFunctionObject) { + IdFunctionObject ifun = (IdFunctionObject) fun; + // Bug 405654 -- make the best effort to keep + // Function.apply and Function.call within this + // interpreter loop invocation + if (BaseFunction.isApplyOrCall(ifun)) { + // funThisObj becomes fun + fun = ScriptRuntime.getCallable(funThisObj); + // first arg becomes thisObj + funThisObj = + getApplyThis( + cx, + stack, + sDbl, + stackTop + 2, + indexReg, + fun, + frame); + if (BaseFunction.isApply(ifun)) { + // Apply: second argument after new "this" + // should be array-like + // and we'll spread its elements on the stack + Object[] callArgs = + indexReg < 2 + ? ScriptRuntime.emptyArgs + : ScriptRuntime + .getApplyArguments( + cx, + stack[ + stackTop + + 3]); + int alen = callArgs.length; + stack = + frame.ensureStackLength( + alen + stackTop + 2); + sDbl = frame.sDbl; + System.arraycopy( + callArgs, 0, stack, stackTop + 2, alen); + indexReg = alen; + } else { + // Call: shift args left, starting from 2nd + if (indexReg > 0) { + if (indexReg > 1) { + System.arraycopy( + stack, + stackTop + 3, + stack, + stackTop + 2, + indexReg - 1); + System.arraycopy( + sDbl, + stackTop + 3, + sDbl, + stackTop + 2, + indexReg - 1); + } + indexReg--; + } + } + } else { + // Some other IdFunctionObject we don't know how to + // reduce. + break; + } + } else if (fun instanceof NoSuchMethodShim) { + NoSuchMethodShim nsmfun = (NoSuchMethodShim) fun; + // Bug 447697 -- make best effort to keep + // __noSuchMethod__ within this interpreter loop + // invocation. + stack = frame.ensureStackLength(stackTop + 4); + sDbl = frame.sDbl; + Object[] elements = + getArgsArray( + stack, sDbl, stackTop + 2, indexReg); + fun = nsmfun.noSuchMethodMethod; + stack[stackTop + 2] = nsmfun.methodName; + stack[stackTop + 3] = + cx.newArray(calleeScope, elements); + indexReg = 2; + } else if (fun == null) { + throw ScriptRuntime.notFunctionError(null, null); + } else { + // Current function is something that we can't reduce + // further. + break; + } + } + if (fun instanceof InterpretedFunction) { InterpretedFunction ifun = (InterpretedFunction) fun; if (frame.fnOrScript.securityDomain @@ -1839,64 +1982,6 @@ private static Object interpretLoop(Context cx, CallFrame frame, Object throwabl cx, frame.parentFrame, false); continue Loop; } - // Bug 405654 -- make best effort to keep Function.apply and - // Function.call within this interpreter loop invocation - if (BaseFunction.isApplyOrCall(ifun)) { - Callable applyCallable = - ScriptRuntime.getCallable(funThisObj); - if (applyCallable instanceof InterpretedFunction) { - InterpretedFunction iApplyCallable = - (InterpretedFunction) applyCallable; - if (frame.fnOrScript.securityDomain - == iApplyCallable.securityDomain) { - frame = - initFrameForApplyOrCall( - cx, - frame, - indexReg, - stack, - sDbl, - stackTop, - op, - calleeScope, - ifun, - iApplyCallable); - continue StateLoop; - } - } - } - } - - // Bug 447697 -- make best effort to keep __noSuchMethod__ - // within this - // interpreter loop invocation - if (fun instanceof NoSuchMethodShim) { - // get the shim and the actual method - NoSuchMethodShim noSuchMethodShim = (NoSuchMethodShim) fun; - Callable noSuchMethodMethod = - noSuchMethodShim.noSuchMethodMethod; - // if the method is in fact an InterpretedFunction - if (noSuchMethodMethod instanceof InterpretedFunction) { - InterpretedFunction ifun = - (InterpretedFunction) noSuchMethodMethod; - if (frame.fnOrScript.securityDomain - == ifun.securityDomain) { - frame = - initFrameForNoSuchMethod( - cx, - frame, - indexReg, - stack, - sDbl, - stackTop, - op, - funThisObj, - calleeScope, - noSuchMethodShim, - ifun); - continue StateLoop; - } - } } cx.lastInterpreterFrame = frame; @@ -3062,54 +3147,6 @@ private static int doRefNsName( return stackTop; } - /** Call __noSuchMethod__. */ - private static CallFrame initFrameForNoSuchMethod( - Context cx, - CallFrame frame, - int indexReg, - Object[] stack, - double[] sDbl, - int stackTop, - int op, - Scriptable funThisObj, - Scriptable calleeScope, - NoSuchMethodShim noSuchMethodShim, - InterpretedFunction ifun) { - // create an args array from the stack - Object[] argsArray = null; - // exactly like getArgsArray except that the first argument - // is the method name from the shim - int shift = stackTop + 2; - Object[] elements = new Object[indexReg]; - for (int i = 0; i < indexReg; ++i, ++shift) { - Object val = stack[shift]; - if (val == DOUBLE_MARK) { - val = ScriptRuntime.wrapNumber(sDbl[shift]); - } - elements[i] = val; - } - argsArray = new Object[2]; - argsArray[0] = noSuchMethodShim.methodName; - argsArray[1] = cx.newArray(calleeScope, elements); - - // exactly the same as if it's a regular InterpretedFunction - CallFrame callParentFrame = frame; - if (op == Icode_TAIL_CALL) { - callParentFrame = frame.parentFrame; - exitFrame(cx, frame, null); - } - // init the frame with the underlying method with the - // adjusted args array and shim's function - CallFrame calleeFrame = - initFrame( - cx, calleeScope, funThisObj, argsArray, null, 0, 2, ifun, callParentFrame); - if (op != Icode_TAIL_CALL) { - frame.savedStackTop = stackTop; - frame.savedCallOp = op; - } - return calleeFrame; - } - private static boolean doEquals(Object[] stack, double[] sDbl, int stackTop) { Object rhs = stack[stackTop + 1]; Object lhs = stack[stackTop]; @@ -3294,74 +3331,42 @@ private static Object thawGenerator( return Scriptable.NOT_FOUND; } - private static CallFrame initFrameForApplyOrCall( + private static Scriptable getApplyThis( Context cx, - CallFrame frame, - int indexReg, Object[] stack, double[] sDbl, - int stackTop, - int op, - Scriptable calleeScope, - IdFunctionObject ifun, - InterpretedFunction iApplyCallable) { - Scriptable applyThis; - if (indexReg != 0) { - Object obj = stack[stackTop + 2]; - if (obj == DOUBLE_MARK) obj = ScriptRuntime.wrapNumber(sDbl[stackTop + 2]); - applyThis = ScriptRuntime.toObjectOrNull(cx, obj, frame.scope); - } else { - applyThis = null; - } - if (applyThis == null) { - // This covers the case of args[0] == (null|undefined) as well. - applyThis = ScriptRuntime.getTopCallScope(cx); - } - if (op == Icode_TAIL_CALL) { - exitFrame(cx, frame, null); - frame = frame.parentFrame; - } else { - frame.savedStackTop = stackTop; - frame.savedCallOp = op; - } - final CallFrame calleeFrame; - if (BaseFunction.isApply(ifun)) { - Object[] callArgs = - indexReg < 2 - ? ScriptRuntime.emptyArgs - : ScriptRuntime.getApplyArguments(cx, stack[stackTop + 3]); - calleeFrame = - initFrame( - cx, - calleeScope, - applyThis, - callArgs, - null, - 0, - callArgs.length, - iApplyCallable, - frame); + int thisIdx, + int indexReg, + Callable target, + CallFrame frame) { + // This is a workaround for what is most likely a bug. We compute applyThis differently for + // interpreted and non-interpreted functions. It feels like exactly one of these two + // strategies should be correct, but choosing one or the other for all functions will break + // different sets of test262 tests. + if (target instanceof InterpretedFunction) { + Scriptable applyThis; + if (indexReg != 0) { + Object obj = stack[thisIdx]; + if (obj == DOUBLE_MARK) obj = ScriptRuntime.wrapNumber(sDbl[thisIdx]); + applyThis = ScriptRuntime.toObjectOrNull(cx, obj, frame.scope); + } else { + applyThis = null; + } + if (applyThis == null) { + // This covers the case of args[0] == (null|undefined) as well. + applyThis = ScriptRuntime.getTopCallScope(cx); + } + return applyThis; } else { - // Shift args left - for (int i = 1; i < indexReg; ++i) { - stack[stackTop + 1 + i] = stack[stackTop + 2 + i]; - sDbl[stackTop + 1 + i] = sDbl[stackTop + 2 + i]; - } - int argCount = indexReg < 2 ? 0 : indexReg - 1; - calleeFrame = - initFrame( - cx, - calleeScope, - applyThis, - stack, - sDbl, - stackTop + 2, - argCount, - iApplyCallable, - frame); + Object obj; + if (indexReg != 0) { + obj = stack[thisIdx]; + if (obj == DOUBLE_MARK) obj = ScriptRuntime.wrapNumber(sDbl[thisIdx]); + } else { + obj = null; + } + return ScriptRuntime.getApplyOrCallThis(cx, frame.scope, obj, indexReg); } - - return calleeFrame; } private static CallFrame initFrame( diff --git a/rhino/src/main/java/org/mozilla/javascript/LambdaFunction.java b/rhino/src/main/java/org/mozilla/javascript/LambdaFunction.java index a736c797a5..bad8f44d78 100644 --- a/rhino/src/main/java/org/mozilla/javascript/LambdaFunction.java +++ b/rhino/src/main/java/org/mozilla/javascript/LambdaFunction.java @@ -70,4 +70,8 @@ public int getArity() { public String getFunctionName() { return name; } + + Callable getTarget() { + return target; + } } diff --git a/rhino/src/main/java/org/mozilla/javascript/ScriptRuntime.java b/rhino/src/main/java/org/mozilla/javascript/ScriptRuntime.java index 93fca1f27e..c16ba509c4 100644 --- a/rhino/src/main/java/org/mozilla/javascript/ScriptRuntime.java +++ b/rhino/src/main/java/org/mozilla/javascript/ScriptRuntime.java @@ -2806,22 +2806,7 @@ public static Object applyOrCall( int L = args.length; Callable function = getCallable(thisObj); - Scriptable callThis = null; - if (L != 0) { - if (cx.hasFeature(Context.FEATURE_OLD_UNDEF_NULL_THIS)) { - callThis = toObjectOrNull(cx, args[0], scope); - } else { - callThis = - args[0] == Undefined.instance - ? Undefined.SCRIPTABLE_UNDEFINED - : toObjectOrNull(cx, args[0], scope); - } - } - if (callThis == null && cx.hasFeature(Context.FEATURE_OLD_UNDEF_NULL_THIS)) { - callThis = - getTopCallScope( - cx); // This covers the case of args[0] == (null|undefined) as well. - } + Scriptable callThis = getApplyOrCallThis(cx, scope, L == 0 ? null : args[0], L); Object[] callArgs; if (isApply) { @@ -2840,6 +2825,25 @@ public static Object applyOrCall( return function.call(cx, scope, callThis, callArgs); } + static Scriptable getApplyOrCallThis(Context cx, Scriptable scope, Object arg0, int l) { + Scriptable callThis; + if (l != 0) { + callThis = + arg0 == Undefined.instance + && !cx.hasFeature(Context.FEATURE_OLD_UNDEF_NULL_THIS) + ? Undefined.SCRIPTABLE_UNDEFINED + : toObjectOrNull(cx, arg0, scope); + } else { + callThis = null; + } + if (callThis == null && cx.hasFeature(Context.FEATURE_OLD_UNDEF_NULL_THIS)) { + callThis = + getTopCallScope( + cx); // This covers the case of args[0] == (null|undefined) as well. + } + return callThis; + } + /** @return true if the passed in Scriptable looks like an array */ private static boolean isArrayLike(Scriptable obj) { return obj != null diff --git a/tests/src/test/java/org/mozilla/javascript/tests/InterpreterFunctionPeelingTest.java b/tests/src/test/java/org/mozilla/javascript/tests/InterpreterFunctionPeelingTest.java new file mode 100644 index 0000000000..7b6c224aae --- /dev/null +++ b/tests/src/test/java/org/mozilla/javascript/tests/InterpreterFunctionPeelingTest.java @@ -0,0 +1,77 @@ +package org.mozilla.javascript.tests; + +import org.junit.Assert; +import org.junit.Test; +import org.mozilla.javascript.Context; +import org.mozilla.javascript.ContinuationPending; +import org.mozilla.javascript.Script; +import org.mozilla.javascript.Scriptable; + +// Tests that continuations work across arrow function, bound function, and apply/call invocations. +public class InterpreterFunctionPeelingTest { + public static final Runnable CAPTURER = + () -> { + try (var cx = Context.enter()) { + throw cx.captureContinuation(); + } + }; + + public static void executeScript(String script) { + try (var cx = Context.enter()) { + cx.setOptimizationLevel(-1); + Script s = cx.compileString(script, "unknown source", 0, null); + Scriptable scope = cx.initStandardObjects(); + scope.put("c", scope, Context.javaToJS(CAPTURER, scope)); + Assert.assertThrows( + ContinuationPending.class, + () -> { + cx.executeScriptWithContinuations(s, scope); + }); + } + } + + @Test + public void testBind() { + executeScript("function capture(){c.run()};capture.bind(this)()"); + } + + @Test + public void testBindCall() { + executeScript("function capture(){c.run()};capture.bind(this).call()"); + } + + @Test + public void testBindApply() { + executeScript("function capture(){c.run()};capture.bind(this).apply()"); + } + + @Test + public void testArrow() { + executeScript("capture=()=>{c.run()};capture()"); + } + + @Test + public void testArrowCall() { + executeScript("capture=()=>{c.run()};capture.call()"); + } + + @Test + public void testArrowApply() { + executeScript("capture=()=>{c.run()};capture.apply()"); + } + + @Test + public void testArrowBindCall() { + executeScript("capture=()=>{c.run()};capture.bind(this).call()"); + } + + @Test + public void testArrowBindApply() { + executeScript("capture=()=>{c.run()};capture.bind(this).apply()"); + } + + @Test + public void testArrowImmediate() { + executeScript("(()=>{c.run()})()"); + } +}