Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
353 changes: 353 additions & 0 deletions dev/design/reduce-apply-bytecode.md

Large diffs are not rendered by default.

11 changes: 5 additions & 6 deletions src/main/java/org/perlonjava/backend/jvm/Dereference.java
Original file line number Diff line number Diff line change
Expand Up @@ -965,10 +965,9 @@ static void handleArrowOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod
}
}

// Save the call context into a local slot for the TAILCALL trampoline.
int callContextSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable();
emitterVisitor.pushCallContext();
mv.visitVarInsn(Opcodes.ISTORE, callContextSlot);
// The call context is NOT stored in a local slot (see EmitSubroutine.java comment for why:
// storing an int into a pre-null-initialised slot causes VerifyError at merge points).
// pushCallContext() is a side-effect-free inline emission and can be called at each use point.

// Allocate a unique callsite ID for inline method caching
int callsiteId = nextMethodCallsiteId++;
Expand All @@ -984,7 +983,7 @@ static void handleArrowOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod
mv.visitVarInsn(Opcodes.ALOAD, methodSlot);
mv.visitVarInsn(Opcodes.ALOAD, subSlot);
mv.visitVarInsn(Opcodes.ALOAD, argsArraySlot);
mv.visitVarInsn(Opcodes.ILOAD, callContextSlot); // push saved call context
emitterVisitor.pushCallContext(); // push call context
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
"org/perlonjava/runtime/runtimetypes/RuntimeCode",
Expand Down Expand Up @@ -1066,7 +1065,7 @@ static void handleArrowOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod
mv.visitVarInsn(Opcodes.ALOAD, emitterVisitor.ctx.javaClassInfo.tailCallCodeRefSlot);
mv.visitLdcInsn("tailcall");
mv.visitVarInsn(Opcodes.ALOAD, emitterVisitor.ctx.javaClassInfo.tailCallArgsSlot);
mv.visitVarInsn(Opcodes.ILOAD, callContextSlot); // context of the original call site
emitterVisitor.pushCallContext(); // context of the original call site
mv.visitMethodInsn(Opcodes.INVOKESTATIC,
"org/perlonjava/runtime/runtimetypes/RuntimeCode",
"apply",
Expand Down
15 changes: 11 additions & 4 deletions src/main/java/org/perlonjava/backend/jvm/EmitBinaryOperator.java
Original file line number Diff line number Diff line change
Expand Up @@ -289,9 +289,7 @@ static void handleCompoundAssignment(EmitterVisitor emitterVisitor, BinaryOperat
if (pooledRight) {
emitterVisitor.ctx.javaClassInfo.releaseSpillSlot();
}
if (pooledLeft) {
emitterVisitor.ctx.javaClassInfo.releaseSpillSlot();
}
// Note: leftSlot is released AFTER the assignment so we can reload it below
// perform the operation
// Note: operands are already on the stack (left DUPped, then right)
String baseOperator = node.operator.substring(0, node.operator.length() - 1);
Expand Down Expand Up @@ -319,7 +317,16 @@ static void handleCompoundAssignment(EmitterVisitor emitterVisitor, BinaryOperat
} else {
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/runtimetypes/RuntimeScalar", "set", "(Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", false);
}

// Discard set()/setPreservingByteString() return value and reload leftObj.
// This matches how *Assign methods (addAssign, etc.) return arg1 directly —
// for TIED_SCALAR lvalues the caller will trigger a 2nd FETCH when it reads
// the result, giving the correct Perl semantics (fetch=2 for compound assigns).
mv.visitInsn(Opcodes.POP);
mv.visitVarInsn(Opcodes.ALOAD, leftSlot);
if (pooledLeft) {
emitterVisitor.ctx.javaClassInfo.releaseSpillSlot();
}

// For string concat assign (.=), invalidate pos() since string was modified
if (node.operator.equals(".=")) {
mv.visitInsn(Opcodes.DUP);
Expand Down
16 changes: 2 additions & 14 deletions src/main/java/org/perlonjava/backend/jvm/EmitOperator.java
Original file line number Diff line number Diff line change
Expand Up @@ -321,12 +321,6 @@ static void handleSubstrOperator(EmitterVisitor emitterVisitor, OperatorNode nod
EmitterVisitor scalarVisitor = emitterVisitor.with(RuntimeContextType.SCALAR);
EmitterVisitor listVisitor = emitterVisitor.with(RuntimeContextType.LIST);
if (node.operand instanceof ListNode operand) {
// Push context
emitterVisitor.pushCallContext();

int callContextSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable();
emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ISTORE, callContextSlot);

// Create array for varargs operators
MethodVisitor mv = emitterVisitor.ctx.mv;

Expand Down Expand Up @@ -370,7 +364,7 @@ static void handleSubstrOperator(EmitterVisitor emitterVisitor, OperatorNode nod
index++;
}

mv.visitVarInsn(Opcodes.ILOAD, callContextSlot);
emitterVisitor.pushCallContext();
mv.visitVarInsn(Opcodes.ALOAD, argsArraySlot);

// Check if warnings are enabled at compile time
Expand Down Expand Up @@ -404,12 +398,6 @@ static void handleOperator(EmitterVisitor emitterVisitor, OperatorNode node) {
EmitterVisitor scalarVisitor = emitterVisitor.with(RuntimeContextType.SCALAR);
EmitterVisitor listVisitor = emitterVisitor.with(RuntimeContextType.LIST);
if (node.operand instanceof ListNode operand) {
// Push context
emitterVisitor.pushCallContext();

int callContextSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable();
emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ISTORE, callContextSlot);

// Create array for varargs operators
MethodVisitor mv = emitterVisitor.ctx.mv;

Expand Down Expand Up @@ -453,7 +441,7 @@ static void handleOperator(EmitterVisitor emitterVisitor, OperatorNode node) {
index++;
}

mv.visitVarInsn(Opcodes.ILOAD, callContextSlot);
emitterVisitor.pushCallContext();
mv.visitVarInsn(Opcodes.ALOAD, argsArraySlot);

emitOperator(node, emitterVisitor);
Expand Down
119 changes: 35 additions & 84 deletions src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java
Original file line number Diff line number Diff line change
Expand Up @@ -497,14 +497,23 @@ static void handleApplyOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod
if (CompilerOptions.DEBUG_ENABLED) emitterVisitor.ctx.logDebug("handleApplyElementOperator " + node + " in context " + emitterVisitor.ctx.contextType);
MethodVisitor mv = emitterVisitor.ctx.mv;

// Capture the call context into a local slot early.
// IMPORTANT: Do not leave the context int on the JVM operand stack while evaluating
// subroutine arguments. Argument evaluation may trigger non-local control flow
// propagation (e.g. last/next/redo) which jumps out of the expression; any stray
// stack items would then break ASM frame merging.
int callContextSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable();
emitterVisitor.pushCallContext();
mv.visitVarInsn(Opcodes.ISTORE, callContextSlot);
// Note: The call context is NOT stored in a local variable slot.
// pushCallContext() emits either a compile-time constant (LDC) or loads the
// callContext method parameter (ILOAD 2). Both are side-effect-free and can be
// re-emitted at the exact moment the value is needed on the JVM operand stack,
// so there is no need to stash the int in a slot.
//
// Storing it in a slot would be WRONG: the pre-initialisation loop in
// EmitterMethodCreator initialises every temporary slot to null (ACONST_NULL /
// ASTORE) so that reference slots are never in TOP state at merge points.
// An int slot initialised that way acquires the reference type "null" from the
// pre-init path. At a merge point (e.g. blockDispatcher) that is reachable from
// both a path that executed ISTORE-callContextSlot and a path through a
// conditional branch that skipped it, the JVM verifier sees conflicting types
// (int vs null-reference) and throws VerifyError: "Bad local variable type".
// This VerifyError triggers the interpreter-fallback which re-runs the main
// script body, calling plan() a second time and causing the "tried to plan
// twice" error in DBIx::Class torture.t (perf/reduce-apply-bytecode Phase 2).

String subroutineName = "";
if (node.left instanceof OperatorNode operatorNode && operatorNode.operator.equals("&")) {
Expand Down Expand Up @@ -593,7 +602,7 @@ static void handleApplyOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod
if (node.left instanceof SubroutineNode subNode && subNode.useTryCatch) {
mv.visitVarInsn(Opcodes.ALOAD, codeRefSlot);
mv.visitVarInsn(Opcodes.ALOAD, 1); // caller's @_ (slot 1) - shared, not copied
mv.visitVarInsn(Opcodes.ILOAD, callContextSlot);
emitterVisitor.pushCallContext();
mv.visitMethodInsn(Opcodes.INVOKESTATIC,
"org/perlonjava/runtime/runtimetypes/RuntimeCode",
"apply",
Expand Down Expand Up @@ -622,7 +631,7 @@ static void handleApplyOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod
if (node.getBooleanAnnotation("shareCallerArgs")) {
mv.visitVarInsn(Opcodes.ALOAD, codeRefSlot);
mv.visitVarInsn(Opcodes.ALOAD, 1); // caller's @_ (slot 1) - shared, not copied
mv.visitVarInsn(Opcodes.ILOAD, callContextSlot);
emitterVisitor.pushCallContext();
mv.visitMethodInsn(Opcodes.INVOKESTATIC,
"org/perlonjava/runtime/runtimetypes/RuntimeCode",
"apply",
Expand Down Expand Up @@ -711,7 +720,7 @@ static void handleApplyOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod
mv.visitVarInsn(Opcodes.ALOAD, codeRefSlot);
mv.visitVarInsn(Opcodes.ALOAD, nameSlot);
mv.visitVarInsn(Opcodes.ALOAD, argsArraySlot);
mv.visitVarInsn(Opcodes.ILOAD, callContextSlot); // Push call context to stack
emitterVisitor.pushCallContext(); // Push call context to stack
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
"org/perlonjava/runtime/runtimetypes/RuntimeCode",
Expand Down Expand Up @@ -760,7 +769,8 @@ static void handleApplyOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod
emitterVisitor.ctx.javaClassInfo.storeSpillRef(mv, baseSpills[i]);
}

// Load and check if it's a control flow marker
// Fast path: check if the result is any kind of control flow marker.
// This is the common case (no control flow) — branch skips everything below.
mv.visitVarInsn(Opcodes.ALOAD, emitterVisitor.ctx.javaClassInfo.controlFlowTempSlot);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"org/perlonjava/runtime/runtimetypes/RuntimeList",
Expand All @@ -769,91 +779,32 @@ static void handleApplyOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod
false);
mv.visitJumpInsn(Opcodes.IFEQ, notControlFlow);

// Marked: check if TAILCALL (handle locally with trampoline)
Label tailcallLoop = new Label();
Label notTailcall = new Label();

// Check if type is TAILCALL
// A control-flow marker was returned. It might be TAILCALL (goto &sub),
// LAST/NEXT/REDO/GOTO, or RETURN. Resolve any TAILCALL chain first, then
// re-check whether the final result still carries a marker that the
// block-level dispatcher needs to handle.
// Keeping resolveTailCalls() inside this branch means the common case
// (no control flow) incurs zero extra overhead. (Phase 2 of
// perf/reduce-apply-bytecode.)
mv.visitVarInsn(Opcodes.ALOAD, emitterVisitor.ctx.javaClassInfo.controlFlowTempSlot);
mv.visitTypeInsn(Opcodes.CHECKCAST, "org/perlonjava/runtime/runtimetypes/RuntimeControlFlowList");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"org/perlonjava/runtime/runtimetypes/RuntimeControlFlowList",
"getControlFlowType",
"()Lorg/perlonjava/runtime/runtimetypes/ControlFlowType;",
false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"org/perlonjava/runtime/runtimetypes/ControlFlowType",
"ordinal",
"()I",
false);
mv.visitInsn(Opcodes.ICONST_4); // TAILCALL.ordinal() = 4
mv.visitJumpInsn(Opcodes.IF_ICMPNE, notTailcall);

// TAILCALL trampoline loop - handle tail calls at the call site
mv.visitLabel(tailcallLoop);

// Extract codeRef and args from the marker
mv.visitVarInsn(Opcodes.ALOAD, emitterVisitor.ctx.javaClassInfo.controlFlowTempSlot);
mv.visitTypeInsn(Opcodes.CHECKCAST, "org/perlonjava/runtime/runtimetypes/RuntimeControlFlowList");
mv.visitInsn(Opcodes.DUP);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"org/perlonjava/runtime/runtimetypes/RuntimeControlFlowList",
"getTailCallCodeRef",
"()Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;",
false);
mv.visitVarInsn(Opcodes.ASTORE, emitterVisitor.ctx.javaClassInfo.tailCallCodeRefSlot);

mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"org/perlonjava/runtime/runtimetypes/RuntimeControlFlowList",
"getTailCallArgs",
"()Lorg/perlonjava/runtime/runtimetypes/RuntimeArray;",
false);
mv.visitVarInsn(Opcodes.ASTORE, emitterVisitor.ctx.javaClassInfo.tailCallArgsSlot);

// Call target: RuntimeCode.apply(codeRef, "tailcall", args, context)
mv.visitVarInsn(Opcodes.ALOAD, emitterVisitor.ctx.javaClassInfo.tailCallCodeRefSlot);
mv.visitLdcInsn("tailcall");
mv.visitVarInsn(Opcodes.ALOAD, emitterVisitor.ctx.javaClassInfo.tailCallArgsSlot);
mv.visitVarInsn(Opcodes.ILOAD, callContextSlot); // context of the original call site
emitterVisitor.pushCallContext();
mv.visitMethodInsn(Opcodes.INVOKESTATIC,
"org/perlonjava/runtime/runtimetypes/RuntimeCode",
"apply",
"(Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;Ljava/lang/String;Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;I)Lorg/perlonjava/runtime/runtimetypes/RuntimeList;",
"resolveTailCalls",
"(Lorg/perlonjava/runtime/runtimetypes/RuntimeList;I)Lorg/perlonjava/runtime/runtimetypes/RuntimeList;",
false);

// Store result to controlFlowTempSlot
mv.visitVarInsn(Opcodes.ASTORE, emitterVisitor.ctx.javaClassInfo.controlFlowTempSlot);

// Check if result is still a control flow marker
// Re-check: a TAILCALL chain may have ended in a normal return.
mv.visitVarInsn(Opcodes.ALOAD, emitterVisitor.ctx.javaClassInfo.controlFlowTempSlot);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"org/perlonjava/runtime/runtimetypes/RuntimeList",
"isNonLocalGoto",
"()Z",
false);
mv.visitJumpInsn(Opcodes.IFEQ, notControlFlow); // Not marked, done

// Marked: check if still TAILCALL
mv.visitVarInsn(Opcodes.ALOAD, emitterVisitor.ctx.javaClassInfo.controlFlowTempSlot);
mv.visitTypeInsn(Opcodes.CHECKCAST, "org/perlonjava/runtime/runtimetypes/RuntimeControlFlowList");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"org/perlonjava/runtime/runtimetypes/RuntimeControlFlowList",
"getControlFlowType",
"()Lorg/perlonjava/runtime/runtimetypes/ControlFlowType;",
false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"org/perlonjava/runtime/runtimetypes/ControlFlowType",
"ordinal",
"()I",
false);
mv.visitInsn(Opcodes.ICONST_4); // TAILCALL.ordinal() = 4
mv.visitJumpInsn(Opcodes.IF_ICMPEQ, tailcallLoop); // Still TAILCALL, loop

// Not TAILCALL - different marker (LAST/NEXT/REDO/GOTO), dispatch it
mv.visitJumpInsn(Opcodes.GOTO, blockDispatcher);
mv.visitJumpInsn(Opcodes.IFEQ, notControlFlow);

// Not TAILCALL initially - jump to block dispatcher
mv.visitLabel(notTailcall);
// Still a marker (LAST/NEXT/REDO/GOTO/RETURN) — dispatch it.
mv.visitJumpInsn(Opcodes.GOTO, blockDispatcher);

// Not a control flow marker - load it back and continue
Expand Down
Loading
Loading