From 373de53b258a1f8adedeb242859d58a0cb82f081 Mon Sep 17 00:00:00 2001 From: "duncan.macgregor" Date: Tue, 2 Jun 2026 15:35:41 +0100 Subject: [PATCH] Introduce main V2 interpreter. --- .../org/mozilla/javascript/CallFrameV2.java | 4 +- .../org/mozilla/javascript/InterpreterV2.java | 610 ++++++++++++++++++ .../interpreterv2/CompilerData.java | 10 +- .../interpreterv2/ContinuationJump.java | 58 ++ .../interpreterv2/InstructionFormatter.java | 108 ++++ .../instruction/JumpInstruction.java | 30 + 6 files changed, 813 insertions(+), 7 deletions(-) create mode 100644 rhino/src/main/java/org/mozilla/javascript/InterpreterV2.java create mode 100644 rhino/src/main/java/org/mozilla/javascript/interpreterv2/ContinuationJump.java create mode 100644 rhino/src/main/java/org/mozilla/javascript/interpreterv2/InstructionFormatter.java create mode 100644 rhino/src/main/java/org/mozilla/javascript/interpreterv2/instruction/JumpInstruction.java diff --git a/rhino/src/main/java/org/mozilla/javascript/CallFrameV2.java b/rhino/src/main/java/org/mozilla/javascript/CallFrameV2.java index 926cd487a1..f30281a484 100644 --- a/rhino/src/main/java/org/mozilla/javascript/CallFrameV2.java +++ b/rhino/src/main/java/org/mozilla/javascript/CallFrameV2.java @@ -1,5 +1,6 @@ package org.mozilla.javascript; +import static org.mozilla.javascript.InterpreterV2.initFunction; import static org.mozilla.javascript.UniqueTag.DOUBLE_MARK; import java.io.Serializable; @@ -87,8 +88,7 @@ public CallFrameV2( for (int i = 0; i < desc.getFunctionCount(); i++) { JSDescriptor fdesc = desc.getFunction(i); if (fdesc.getFunctionType() == FunctionNode.FUNCTION_STATEMENT) { - // Commented out for now to avoid pulling everything at once. - // initFunction(cx, this.scope, desc, i); + initFunction(cx, this.scope, desc, i); } } } diff --git a/rhino/src/main/java/org/mozilla/javascript/InterpreterV2.java b/rhino/src/main/java/org/mozilla/javascript/InterpreterV2.java new file mode 100644 index 0000000000..16575408c1 --- /dev/null +++ b/rhino/src/main/java/org/mozilla/javascript/InterpreterV2.java @@ -0,0 +1,610 @@ +package org.mozilla.javascript; + +import static org.mozilla.javascript.UniqueTag.DOUBLE_MARK; + +import java.math.BigInteger; +import java.util.List; +import java.util.Objects; +import org.mozilla.javascript.ast.ScriptNode; +import org.mozilla.javascript.debug.DebuggableScript; +import org.mozilla.javascript.interpreterv2.CompilerData; +import org.mozilla.javascript.interpreterv2.GeneratorState; +import org.mozilla.javascript.interpreterv2.instruction.JumpInstruction; +import org.mozilla.javascript.interpreterv2.operand.Operand; + +public class InterpreterV2 extends AInterpreter> { + private static final int EX_CATCH_STATE = 2; // Can execute JS catch + private static final int EX_FINALLY_STATE = 1; // Can execute JS finally + private static final int EX_NO_JS_STATE = 0; // Terminate JS execution + + public static final int INVOCATION_COST = 100; + // arbitrary exception cost for instruction counting + private static final int EXCEPTION_COST = 100; + + static { + RhinoException.registerInterpreterMethod(InterpreterV2.class, "interpretInner"); + } + + public InterpreterV2() {} + + public static Object interpret(Context cx, CallFrameV2 frame, Object throwable) { + var oldFrame = cx.lastInterpreterFrame; + try { + return interpretInner(cx, frame, throwable); + } finally { + cx.lastInterpreterFrame = oldFrame; + } + } + + private static Object interpretInner(Context cx, CallFrameV2 frame, Object throwable) { + cx.lastInterpreterFrame = frame; + + GeneratorState generatorState = null; + if (throwable != null) { + if (throwable instanceof GeneratorState) { + generatorState = (GeneratorState) throwable; + + // reestablish this call frame + enterFrame(cx, frame, ScriptRuntime.emptyArgs, true); + frame.generatorState = generatorState; + throwable = null; + } else if (!(throwable instanceof ContinuationJump)) { + // It should be continuation + Kit.codeBug(); + } + } + + boolean instructionCounting = cx.instructionThreshold != 0; + // Try is here just because for dev this is nicer + var instructions = frame.compilerData.instructions; + int previousLineNumber = -1; + while (frame.pc < instructions.length) { + if (frame.throwable != null) { + int exState = getExState(cx, frame.generatorState, frame.throwable); + @SuppressWarnings("unchecked") + var cjump = + frame.throwable instanceof ContinuationJump + ? (ContinuationJump>) frame.throwable + : null; + exState = handleDebugAndInstructionCount(cx, frame, exState); + if (exState == EX_NO_JS_STATE) { + cjump = null; + } + var exceptionHandlerOffset = searchAndProcessFrame(cx, frame, exState, cjump); + frame = + processThrowable( + cx, + frame.throwable, + frame, + exceptionHandlerOffset, + instructionCounting, + cjump); + frame.throwable = null; + } + + // We could make this faster via, for example, a subclass "DebuggableInterpreter" and a + // virtual function... but I'm not sure if it would be worth it. Anyway, The JVM should + // make this very cheap. + if (cx.debugger != null) { + // We have reached a new instruction - let's see if we have reached new lines as + // well, and in that case invoke the debugger to trigger breakpoints + List lines = frame.compilerData.getLineSetFromPc(frame.pc); + if (lines != null && !lines.isEmpty()) { + for (int line : lines) { + if (line != -1 && line != previousLineNumber) { + frame.debuggerFrame.onLineChange(cx, line); + previousLineNumber = line; + } + } + } + } + + try { + var pcBefore = frame.pc; + var instruction = instructions[frame.pc]; + instruction.interpret(cx, frame); + + assert instruction instanceof JumpInstruction + || frame.pc != pcBefore + || frame.throwable != null + : ("Instruction did not advance PC: " + + instruction.getClass().getName() + + " at PC: " + + frame.pc); + + if (frame.shouldYieldToParent) { + frame.shouldYieldToParent = false; + break; + } + + } catch (Throwable ex) { + if (frame.throwable != null) { + // This is serious bug and it is better to track it ASAP + ex.printStackTrace(System.err); + throw new IllegalStateException(); + } + frame.throwable = ex; + } + } + + exitFrame(cx, frame, frame.throwable); + var interpreterResult = frame.result; + var interpreterResultDbl = frame.resultDbl; + if (frame.parentFrame != null) { + frame = frame.parentFrame; + if (frame.frozen) { + frame = frame.cloneFrozen(); + } + // TODO(jimmy) + // Need to figure out when this should happen + // or if the way we do things makes this unnecessary + // setCallResult(frame, interpreterResult, interpreterResultDbl); + } + + return frame.result == DOUBLE_MARK + ? ScriptRuntime.wrapNumber(frame.resultDbl) + : frame.result; + } + + private static int handleDebugAndInstructionCount(Context cx, CallFrameV2 frame, int exState) { + boolean instructionCounting = cx.instructionThreshold != 0; + var throwable = frame.throwable; + if (instructionCounting) { + try { + addInstructionCount(cx, frame, EXCEPTION_COST); + } catch (RuntimeException ex) { + throwable = ex; + exState = EX_FINALLY_STATE; + } catch (Error ex) { + // Error from instruction counting + // => unconditionally terminate JS + frame.throwable = ex; + exState = EX_NO_JS_STATE; + } + } + if (frame.debuggerFrame != null && throwable instanceof RuntimeException) { + // Call debugger only for RuntimeException + RuntimeException rex = (RuntimeException) throwable; + try { + frame.debuggerFrame.onExceptionThrown(cx, rex); + } catch (Throwable ex) { + // Any exception from debugger + // => unconditionally terminate JS + frame.throwable = ex; + exState = EX_NO_JS_STATE; + } + } + return exState; + } + + private static int searchAndProcessFrame( + Context cx, CallFrameV2 frame, int exState, ContinuationJump cjump) { + var throwable = frame.throwable; + for (; ; ) { + if (exState != EX_NO_JS_STATE) { + boolean onlyFinally = (exState != EX_CATCH_STATE); + var exceptionHandlerOffset = getExceptionHandler(frame, onlyFinally); + if (exceptionHandlerOffset >= 0) { + // We caught an exception, restart the loop + // with exception pending the processing at the loop + // start + return exceptionHandlerOffset; + } + } + // No allowed exception handlers in this frame, unwind + // to parent and try to look there + + frame.throwable = null; + exitFrame(cx, frame, throwable); + + frame = frame.parentFrame; + if (frame == null) { + break; + } + if (cjump != null && Objects.equals(cjump.branchFrame, frame)) { + // Continuation branch point was hit, + // restart the state loop to reenter continuation + return -1; + } + } + + if (throwable instanceof RuntimeException) { + throw (RuntimeException) throwable; + } + // Must be instance of Error or code bug + throw (Error) throwable; + } + + private static int getExState(Context cx, GeneratorState generatorState, Object throwable) { + int exState; + + if (generatorState != null + && generatorState.operation == NativeGenerator.GENERATOR_CLOSE + && throwable == generatorState.value) { + exState = EX_FINALLY_STATE; + } else if (throwable instanceof JavaScriptException) { + exState = EX_CATCH_STATE; + } else if (throwable instanceof EcmaError) { + // an offical ECMA error object, + exState = EX_CATCH_STATE; + } else if (throwable instanceof EvaluatorException) { + exState = EX_CATCH_STATE; + } else if (throwable instanceof ContinuationPending) { + exState = EX_NO_JS_STATE; + } else if (throwable instanceof RuntimeException) { + exState = + cx.hasFeature(Context.FEATURE_ENHANCED_JAVA_ACCESS) + ? EX_CATCH_STATE + : EX_FINALLY_STATE; + } else if (throwable instanceof Error) { + exState = + cx.hasFeature(Context.FEATURE_ENHANCED_JAVA_ACCESS) + ? EX_CATCH_STATE + : EX_NO_JS_STATE; + } else if (throwable instanceof ContinuationJump) { + // It must be ContinuationJump + exState = EX_FINALLY_STATE; + } else { + exState = + cx.hasFeature(Context.FEATURE_ENHANCED_JAVA_ACCESS) + ? EX_CATCH_STATE + : EX_FINALLY_STATE; + } + return exState; + } + + private static CallFrameV2 processThrowable( + Context cx, + Object throwable, + CallFrameV2 frame, + int exceptionHandlerOffset, + boolean instructionCounting, + ContinuationJump> cjump) { + // Recovering from exception, exceptionHandlerOffset contains + // the index of handler + + if (exceptionHandlerOffset >= 0) { + // Normal exception handler, transfer + // control appropriately + + if (frame.frozen) { + // XXX Deal with exceptios!!! + frame = frame.cloneFrozen(); + } + + int[] table = frame.compilerData.exceptionTable; + + frame.pc = table[exceptionHandlerOffset + CompilerData.EXCEPTION_HANDLER_SLOT]; + if (instructionCounting) { + frame.pcPrevBranch = frame.pc; + } + + frame.stackTop = frame.emptyStackTop; + int scopeLocal = + frame.localShift + + table[exceptionHandlerOffset + CompilerData.EXCEPTION_SCOPE_SLOT]; + int exLocal = + frame.localShift + + table[exceptionHandlerOffset + CompilerData.EXCEPTION_LOCAL_SLOT]; + frame.scope = (VarScope) frame.stack[scopeLocal]; + frame.stack[exLocal] = throwable; + + } else { + // Continuation restoration + // Interpreter.ContinuationJump cjump = (Interpreter.ContinuationJump) throwable; + // + // Clear throwable to indicate that exceptions are OK + + if (!Objects.equals(cjump.branchFrame, frame)) Kit.codeBug(); + + // Check that we have at least one frozen frame + // in the case of detached continuation restoration: + // unwind code ensure that + if (cjump.capturedFrame == null) Kit.codeBug(); + + // Need to rewind branchFrame, capturedFrame + // and all frames in between + int rewindCount = cjump.capturedFrame.frameIndex + 1; + if (cjump.branchFrame != null) { + rewindCount -= cjump.branchFrame.frameIndex; + } + + int enterCount = 0; + CallFrameV2[] enterFrames = null; + + CallFrameV2 x = cjump.capturedFrame; + for (int i = 0; i != rewindCount; ++i) { + if (!x.frozen) Kit.codeBug(); + if (isFrameEnterExitRequired(x)) { + if (enterFrames == null) { + // Allocate enough space to store the rest + // of rewind frames in case all of them + // would require to enter + enterFrames = new CallFrameV2[rewindCount - i]; + } + enterFrames[enterCount] = x; + ++enterCount; + } + x = x.parentFrame; + } + + while (enterCount != 0) { + // execute enter: walk enterFrames in the reverse + // order since they were stored starting from + // the capturedFrame, not branchFrame + --enterCount; + x = enterFrames[enterCount]; + enterFrame(cx, x, ScriptRuntime.emptyArgs, true); + } + + // Continuation jump is almost done: capturedFrame + // points to the call to the function that captured + // continuation, so clone capturedFrame and + // emulate return that function with the suplied result + frame = cjump.capturedFrame.cloneFrozen(); + setCallResult(frame, cjump.result, cjump.resultDbl); + // restart the execution + } + return frame; + } + + private static boolean isFrameEnterExitRequired(CallFrameV2 frame) { + return frame.debuggerFrame != null + || frame.fnOrScript.getDescriptor().requiresActivationFrame(); + } + + private static void setCallResult(CallFrameV2 frame, Object callResult, double callResultDbl) { + throw new UnsupportedOperationException("Haven't implemented all the continuations stuff"); + } + + private static int getExceptionHandler(CallFrameV2 frame, boolean onlyFinally) { + int[] exceptionTable = frame.compilerData.exceptionTable; + if (exceptionTable == null) { + // No exception handlers + return -1; + } + + // OPT: use binary search + int best = -1, bestStart = 0, bestEnd = 0; + for (int i = 0; i != exceptionTable.length; i += CompilerData.EXCEPTION_SLOT_SIZE) { + int start = exceptionTable[i + CompilerData.EXCEPTION_TRY_START_SLOT]; + int end = exceptionTable[i + CompilerData.EXCEPTION_TRY_END_SLOT]; + if (!(start <= frame.pc && frame.pc < end)) { + continue; + } + if (onlyFinally && exceptionTable[i + CompilerData.EXCEPTION_TYPE_SLOT] != 1) { + continue; + } + if (best >= 0) { + // Since handlers always nest and they never have shared end + // although they can share start it is sufficient to compare + // handlers ends + if (bestEnd < end) { + continue; + } + // Check the above assumption + if (bestStart > start) Kit.codeBug(); // should be nested + if (bestEnd == end) Kit.codeBug(); // no ens sharing + } + best = i; + bestStart = start; + bestEnd = end; + } + return best; + } + + public static boolean shouldOverrideNativeCall( + Object securityDomain, Callable fun, Scriptable funThisObj) { + if (fun instanceof KnownBuiltInFunction + && BaseFunction.isApplyOrCall((KnownBuiltInFunction) fun)) { + Callable applyCallable = ScriptRuntime.getCallable(funThisObj); + if (applyCallable instanceof JSFunction) { + JSFunction iApplyCallable = (JSFunction) applyCallable; + if (iApplyCallable.getDescriptor().getCode() instanceof CompilerData + && securityDomain == iApplyCallable.getDescriptor().getSecurityDomain()) + return false; + } + } + + return true; + } + + public static void initFunction(Context cx, VarScope scope, JSDescriptor parent, int index) { + JSFunction fn = JSFunction.createFunction(cx, scope, parent, index, null); + ScriptRuntime.initFunction( + cx, scope, fn, fn.getDescriptor().getFunctionType(), parent.isEvalFunction()); + } + + public static > Object interpret( + T fun, + CompilerData data, + Context cx, + VarScope scope, + Scriptable thisObj, + Object[] args) { + if (!ScriptRuntime.hasTopCall(cx)) { + Kit.codeBug(); + } + + JSDescriptor desc = fun.getDescriptor(); + SecurityController securityController = desc.getSecurityController(); + Object securityDomain = desc.getSecurityDomain(); + if (securityController != null && cx.interpreterSecurityDomain != securityDomain) { + Object savedDomain = cx.interpreterSecurityDomain; + cx.interpreterSecurityDomain = securityDomain; + try { + return securityController.callWithDomain( + securityDomain, cx, (Callable) fun, scope, thisObj, args); + } finally { + cx.interpreterSecurityDomain = savedDomain; + } + } + + var frame = + new CallFrameV2( + cx, + scope, + thisObj, + fun.getHomeObject(), + args, + null, + 0, + args.length, + fun, + null, + cx.lastInterpreterFrame); + + boolean parentStrictness = ScriptRuntime.enterFunctionStrictness(cx, desc.isStrict()); + try { + enterFrame(cx, frame, args, false); + return InterpreterV2.interpret(cx, frame, null); + } finally { + ScriptRuntime.exitFunctionStrictness(cx, parentStrictness); + } + } + + public static Object resumeGenerator( + Context cx, VarScope scope, int operation, Object savedState, Object value) { + CallFrameV2 frame = (CallFrameV2) savedState; + CallFrameV2 activeFrame = frame.shallowCloneFrozen(cx.lastInterpreterFrame); + try { + GeneratorState generatorState = new GeneratorState(operation, value); + if (operation == NativeGenerator.GENERATOR_CLOSE) { + try { + return interpret(cx, activeFrame, generatorState); + } catch (NativeGenerator.GeneratorClosedException e) { + // Re-throw GeneratorClosedException so ES6Generator can catch and complete + throw e; + } catch (RuntimeException e) { + // Only propagate exceptions other than closingException + if (e != value) throw e; + } + return Undefined.instance; + } + Object result = interpret(cx, activeFrame, generatorState); + if (generatorState.returnedException != null) throw generatorState.returnedException; + return result; + } finally { + activeFrame.syncStateToFrame(frame); + } + } + + public static boolean doShallowEquals( + Context cx, CallFrameV2 frame, Operand left, Operand right) { + Object rhs; + double rDouble = 0.0; + if (right.isDouble(frame)) { + rDouble = right.retrieveDouble(frame); + rhs = DOUBLE_MARK; + } else { + rhs = right.retrieve(cx, frame); + } + Object lhs; + double lDouble = 0.0; + if (left.isDouble(frame)) { + lDouble = left.retrieveDouble(frame); + lhs = DOUBLE_MARK; + } else { + lhs = left.retrieve(cx, frame); + } + if (rhs == DOUBLE_MARK) { + if (lhs instanceof Number && !(lhs instanceof BigInteger)) { + lDouble = ((Number) lhs).doubleValue(); + } else if (lhs != DOUBLE_MARK) { + return false; + } + } else if (lhs == DOUBLE_MARK) { + if (rhs instanceof Number && !(rhs instanceof BigInteger)) { + rDouble = ((Number) rhs).doubleValue(); + } else { + return false; + } + } else { + return ScriptRuntime.shallowEq(lhs, rhs); + } + return (lDouble == rDouble); + } + + public static boolean doEquals(Context cx, CallFrameV2 frame, Operand left, Operand right) { + Object rhs; + double rDouble = 0.0; + if (right.isDouble(frame)) { + rDouble = right.retrieveDouble(frame); + rhs = DOUBLE_MARK; + } else { + rhs = right.retrieve(cx, frame); + } + Object lhs; + double lDouble = 0.0; + if (left.isDouble(frame)) { + lDouble = left.retrieveDouble(frame); + lhs = DOUBLE_MARK; + } else { + lhs = left.retrieve(cx, frame); + } + if (rhs == DOUBLE_MARK) { + if (lhs == DOUBLE_MARK) { + return (lDouble == rDouble); + } + return ScriptRuntime.eqNumber(rDouble, lhs); + } + if (lhs == DOUBLE_MARK) { + return ScriptRuntime.eqNumber(lDouble, rhs); + } + return ScriptRuntime.eq(lhs, rhs); + } + + @Override + public CompilationResult compileScript( + CompilerEnvirons compilerEnv, ScriptNode tree, String rawSource) { + throw new UnsupportedOperationException(); + } + + @Override + public CompilationResult compileFunction( + CompilerEnvirons compilerEnv, ScriptNode tree, String rawSource) { + throw new UnsupportedOperationException(); + } + + @Override + public Script createScriptObject( + CompilationResult compiled, Object staticSecurityDomain) { + var result = (V2CompilationResult) compiled; + return JSFunction.createScript(result.data, result.homeObject, staticSecurityDomain); + } + + @Override + public Function createFunctionObject( + Context cx, + VarScope scope, + CompilationResult compiled, + Object staticSecurityDomain) { + var result = (V2CompilationResult) compiled; + return JSFunction.createFunction( + cx, scope, result.data, result.homeObject, staticSecurityDomain); + } + + public static void addInstructionCount(Context cx, CallFrameV2 frame, int extra) { + cx.instructionCount += frame.pc - frame.pcPrevBranch + extra; + if (cx.instructionCount > cx.instructionThreshold) { + cx.observeInstructionCount(cx.instructionCount); + cx.instructionCount = 0; + } + } + + private static class V2CompilationResult> + implements CompilationResult { + private final JSDescriptor data; + private final Scriptable homeObject; + + V2CompilationResult(JSDescriptor data, Scriptable homeObject) { + this.data = data; + this.homeObject = homeObject; + } + + @Override + public DebuggableScript getDebuggableScript() { + return data; + } + } +} diff --git a/rhino/src/main/java/org/mozilla/javascript/interpreterv2/CompilerData.java b/rhino/src/main/java/org/mozilla/javascript/interpreterv2/CompilerData.java index fa82d7b67c..7c70d114bc 100644 --- a/rhino/src/main/java/org/mozilla/javascript/interpreterv2/CompilerData.java +++ b/rhino/src/main/java/org/mozilla/javascript/interpreterv2/CompilerData.java @@ -4,9 +4,11 @@ import java.util.List; import org.mozilla.javascript.ACompilerData; import org.mozilla.javascript.Context; +import org.mozilla.javascript.InterpreterV2; import org.mozilla.javascript.JSCode; import org.mozilla.javascript.JSDescriptor; import org.mozilla.javascript.ScriptOrFn; +import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.VarScope; import org.mozilla.javascript.config.RhinoConfig; import org.mozilla.javascript.interpreterv2.instruction.Instruction; @@ -144,9 +146,8 @@ public Object execute( VarScope scope, Object thisObj, Object[] args) { - return null; - // return InterpreterV2.interpret( - // executableObject, this, cx, scope, (Scriptable) thisObj, args); + return InterpreterV2.interpret( + executableObject, this, cx, scope, (Scriptable) thisObj, args); } @Override @@ -157,8 +158,7 @@ public Object resume( VarScope scope, int operation, Object value) { - return null; - // return InterpreterV2.resumeGenerator(cx, scope, operation, state, value); + return InterpreterV2.resumeGenerator(cx, scope, operation, state, value); } @Override diff --git a/rhino/src/main/java/org/mozilla/javascript/interpreterv2/ContinuationJump.java b/rhino/src/main/java/org/mozilla/javascript/interpreterv2/ContinuationJump.java new file mode 100644 index 0000000000..29d4c55bca --- /dev/null +++ b/rhino/src/main/java/org/mozilla/javascript/interpreterv2/ContinuationJump.java @@ -0,0 +1,58 @@ +package org.mozilla.javascript.interpreterv2; + +import java.io.Serializable; +import java.util.Objects; +import org.mozilla.javascript.CallFrameV2; +import org.mozilla.javascript.Kit; +import org.mozilla.javascript.NativeContinuation; + +public final class ContinuationJump implements Serializable { + private static final long serialVersionUID = 7687739156004308247L; + + public CallFrameV2 capturedFrame; + public CallFrameV2 branchFrame; + public Object result; + public double resultDbl; + + ContinuationJump(NativeContinuation c, CallFrameV2 current) { + this.capturedFrame = (CallFrameV2) c.getImplementation(); + if (this.capturedFrame == null || current == null) { + // Continuation and current execution does not share + // any frames if there is nothing to capture or + // if there is no currently executed frames + this.branchFrame = null; + } else { + // Search for branch frame where parent frame chains starting + // from captured and current meet. + CallFrameV2 chain1 = this.capturedFrame; + CallFrameV2 chain2 = current; + + // First work parents of chain1 or chain2 until the same + // frame depth. + int diff = chain1.frameIndex - chain2.frameIndex; + if (diff != 0) { + if (diff < 0) { + // swap to make sure that + // chain1.frameIndex > chain2.frameIndex and diff > 0 + chain1 = current; + chain2 = this.capturedFrame; + diff = -diff; + } + do { + chain1 = chain1.parentFrame; + } while (--diff != 0); + if (chain1.frameIndex != chain2.frameIndex) Kit.codeBug(); + } + + // Now walk parents in parallel until a shared frame is found + // or until the root is reached. + while (!Objects.equals(chain1, chain2) && chain1 != null) { + chain1 = chain1.parentFrame; + chain2 = chain2.parentFrame; + } + + this.branchFrame = chain1; + if (this.branchFrame != null && !this.branchFrame.frozen) Kit.codeBug(); + } + } +} diff --git a/rhino/src/main/java/org/mozilla/javascript/interpreterv2/InstructionFormatter.java b/rhino/src/main/java/org/mozilla/javascript/interpreterv2/InstructionFormatter.java new file mode 100644 index 0000000000..99f8dde32f --- /dev/null +++ b/rhino/src/main/java/org/mozilla/javascript/interpreterv2/InstructionFormatter.java @@ -0,0 +1,108 @@ +package org.mozilla.javascript.interpreterv2; + +import java.math.BigInteger; +import java.util.Arrays; +import org.mozilla.javascript.interpreterv2.instruction.Instruction; +import org.mozilla.javascript.interpreterv2.operand.Operand; + +/** Utility class for formatting InterpreterV2 instructions. */ +public class InstructionFormatter { + + /** + * Format a jump target as a virtual register reference. + * + * @param targetPC the target program counter + * @return formatted target reference (e.g., "%7", "%12") + */ + public static String formatTarget(int targetPC) { + return "%" + targetPC; + } + + /** + * Format a relative jump offset for display. + * + * @param offset the jump offset (positive or negative) + * @return formatted offset string (e.g., "+5", "-3") + */ + public static String formatOffset(int offset) { + return (offset >= 0 ? "+" : "") + offset; + } + + /** + * Format multiple operands as a comma-separated parameter list. + * + * @param operands variable number of key-value pairs (key1, value1, key2, value2, ...) + * @return formatted parameter list (e.g., "args=2, receiver=true") + */ + public static String formatInstruction(Instruction instruction, Object... operands) { + return formatInstruction(instruction.getClass().getSimpleName(), operands); + } + + /** + * Like {@link #formatInstruction(Instruction, Object...)} but with an explicit instruction + * name. Used by specialized variants (e.g. the arity-specialized {@code Call}) that want to + * present themselves under a common name regardless of their concrete class. + */ + public static String formatInstruction(String name, Object... operands) { + StringBuilder sb = new StringBuilder(); + sb.append(name); + + if (operands.length != 0) { + if (operands.length % 2 != 0) { + throw new IllegalArgumentException("Operands must be provided in key-value pairs"); + } + + sb.append('('); + for (int i = 0; i < operands.length; i += 2) { + if (i > 0) { + sb.append(", "); + } + + String key = operands[i].toString(); + Object value = operands[i + 1]; + + sb.append(key).append('='); + formatOperandValue(sb, value); + } + sb.append(')'); + } + + return sb.toString(); + } + + private static void formatOperandValue(StringBuilder sb, Object value) { + if (value instanceof Object[]) { + sb.append('['); + Object[] operands = (Object[]) value; + for (int i = 0; i < operands.length; i++) { + if (i > 0) { + sb.append(", "); + } + formatOperandValue(sb, operands[i]); + } + sb.append(']'); + } else if (value instanceof boolean[]) { + sb.append(Arrays.toString((boolean[]) value)); + } else if (value instanceof Operand) { + ((Operand) value).appendDebugString(sb); + } else if (value instanceof String) { + appendString(sb, (String) value); + } else if (value instanceof BigInteger) { + sb.append(value).append('n'); + } else { + sb.append(value); + } + } + + public static void appendString(StringBuilder sb, String value) { + sb.append('"').append(escape(value)).append('"'); + } + + private static String escape(String str) { + return str.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\t", "\\t") + .replace("\r", "\\r"); + } +} diff --git a/rhino/src/main/java/org/mozilla/javascript/interpreterv2/instruction/JumpInstruction.java b/rhino/src/main/java/org/mozilla/javascript/interpreterv2/instruction/JumpInstruction.java new file mode 100644 index 0000000000..74d79f101b --- /dev/null +++ b/rhino/src/main/java/org/mozilla/javascript/interpreterv2/instruction/JumpInstruction.java @@ -0,0 +1,30 @@ +package org.mozilla.javascript.interpreterv2.instruction; + +import java.util.Set; +import org.mozilla.javascript.interpreterv2.InstructionFormatter; + +public abstract class JumpInstruction extends Instruction { + private int offset; + + public void setOffset(int offset) { + this.offset = offset; + } + + public final int getOffset() { + return offset; + } + + /** + * Returns all possible jump targets from this instruction. + * + * @param fromPC the PC of this instruction + * @return set of all possible target PCs + */ + public abstract Set getTargets(int fromPC); + + @Override + public String toDebugString() { + return InstructionFormatter.formatInstruction( + this, "offset", InstructionFormatter.formatOffset(offset)); + } +}