From 909cd194e180695bd463994461b7ae418e8600ed Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Thu, 26 Feb 2026 22:18:45 +0100 Subject: [PATCH] Fix bytecode compiler my-extraction in short-circuit ops and AUTOLOAD with forward declarations Two fixes: 1. Bytecode compiler (interpreter backend): extract my declarations before short-circuit jumps in &&/||// operators. Previously, 'my $x = expr if COND' left the register as Java null when the condition was false, causing NPEs. Now uses FindDeclarationVisitor to emit the declaration unconditionally before the conditional jump, matching the JVM emitter behavior. 2. Method resolution: forward declarations (sub foo;) no longer prevent AUTOLOAD from being called. Previously, findMethodInHierarchy would find the forward declaration, see it was undefined, and skip to the next class in the hierarchy (bypassing AUTOLOAD). Now it falls through to the AUTOLOAD check for the same class. This fixes ExifTool VerboseInfo and other Writer.pl methods loaded via AUTOLOAD. ExifTool.t: 24/35 tests pass (up from 12/35, test 17 no longer crashes). Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../bytecode/CompileBinaryOperator.java | 218 ++++++++++-------- .../runtime/mro/InheritanceResolver.java | 22 +- 2 files changed, 128 insertions(+), 112 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java index 98010eb1e..576357ec7 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java @@ -1,9 +1,44 @@ package org.perlonjava.backend.bytecode; +import org.perlonjava.frontend.analysis.FindDeclarationVisitor; import org.perlonjava.frontend.astnode.*; import org.perlonjava.runtime.runtimetypes.RuntimeContextType; public class CompileBinaryOperator { + + private static class DeclRewrite { + final OperatorNode declaration; + final String savedOperator; + final Node savedOperand; + + DeclRewrite(OperatorNode declaration, String savedOperator, Node savedOperand) { + this.declaration = declaration; + this.savedOperator = savedOperator; + this.savedOperand = savedOperand; + } + + void restore() { + declaration.operator = savedOperator; + declaration.operand = savedOperand; + } + } + + private static DeclRewrite extractMyDeclaration(BytecodeCompiler bc, Node rightNode) { + OperatorNode decl = FindDeclarationVisitor.findOperator(rightNode, "my"); + if (decl != null && decl.operand instanceof OperatorNode innerOp) { + String savedOp = decl.operator; + Node savedOperand = decl.operand; + int savedCtx = bc.currentCallContext; + bc.currentCallContext = RuntimeContextType.VOID; + decl.accept(bc); + bc.currentCallContext = savedCtx; + decl.operator = innerOp.operator; + decl.operand = innerOp.operand; + return new DeclRewrite(decl, savedOp, savedOperand); + } + return null; + } + static void visitBinaryOperator(BytecodeCompiler bytecodeCompiler, BinaryOperatorNode node) { // Track token index for error reporting bytecodeCompiler.currentTokenIndex = node.getIndex(); @@ -430,134 +465,115 @@ else if (node.right instanceof BinaryOperatorNode) { // Handle short-circuit operators specially - don't compile right operand yet! if (node.operator.equals("&&") || node.operator.equals("and")) { - // Logical AND with short-circuit evaluation - // Only evaluate right side if left side is true - - // Compile left operand in scalar context (need boolean value) - int savedContext = bytecodeCompiler.currentCallContext; - bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; - node.left.accept(bytecodeCompiler); - int rs1 = bytecodeCompiler.lastResultReg; - bytecodeCompiler.currentCallContext = savedContext; - - // Allocate result register and move left value to it - int rd = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.MOVE); - bytecodeCompiler.emitReg(rd); - bytecodeCompiler.emitReg(rs1); + DeclRewrite rewrite = extractMyDeclaration(bytecodeCompiler, node.right); + try { + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; + node.left.accept(bytecodeCompiler); + int rs1 = bytecodeCompiler.lastResultReg; + bytecodeCompiler.currentCallContext = savedContext; - // Mark position for forward jump - int skipRightPos = bytecodeCompiler.bytecode.size(); + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); - // Emit conditional jump: if (!rd) skip right evaluation - bytecodeCompiler.emit(Opcodes.GOTO_IF_FALSE); - bytecodeCompiler.emitReg(rd); - bytecodeCompiler.emitInt(0); // Placeholder for offset (will be patched) + int skipRightPos = bytecodeCompiler.bytecode.size(); + bytecodeCompiler.emit(Opcodes.GOTO_IF_FALSE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitInt(0); - // NOW compile right operand (only executed if left was true) - node.right.accept(bytecodeCompiler); - int rs2 = bytecodeCompiler.lastResultReg; + node.right.accept(bytecodeCompiler); + int rs2 = bytecodeCompiler.lastResultReg; - // Move right result to rd (overwriting left value) - bytecodeCompiler.emit(Opcodes.MOVE); - bytecodeCompiler.emitReg(rd); - bytecodeCompiler.emitReg(rs2); + bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs2); - // Patch the forward jump offset - int skipRightTarget = bytecodeCompiler.bytecode.size(); - bytecodeCompiler.patchIntOffset(skipRightPos + 2, skipRightTarget); + int skipRightTarget = bytecodeCompiler.bytecode.size(); + bytecodeCompiler.patchIntOffset(skipRightPos + 2, skipRightTarget); - bytecodeCompiler.lastResultReg = rd; + bytecodeCompiler.lastResultReg = rd; + } finally { + if (rewrite != null) rewrite.restore(); + } return; } if (node.operator.equals("||") || node.operator.equals("or")) { - // Logical OR with short-circuit evaluation - // Only evaluate right side if left side is false - - // Compile left operand in scalar context (need boolean value) - int savedContext = bytecodeCompiler.currentCallContext; - bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; - node.left.accept(bytecodeCompiler); - int rs1 = bytecodeCompiler.lastResultReg; - bytecodeCompiler.currentCallContext = savedContext; - - // Allocate result register and move left value to it - int rd = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.MOVE); - bytecodeCompiler.emitReg(rd); - bytecodeCompiler.emitReg(rs1); + DeclRewrite rewrite = extractMyDeclaration(bytecodeCompiler, node.right); + try { + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; + node.left.accept(bytecodeCompiler); + int rs1 = bytecodeCompiler.lastResultReg; + bytecodeCompiler.currentCallContext = savedContext; - // Mark position for forward jump - int skipRightPos = bytecodeCompiler.bytecode.size(); + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); - // Emit conditional jump: if (rd) skip right evaluation - bytecodeCompiler.emit(Opcodes.GOTO_IF_TRUE); - bytecodeCompiler.emitReg(rd); - bytecodeCompiler.emitInt(0); // Placeholder for offset (will be patched) + int skipRightPos = bytecodeCompiler.bytecode.size(); + bytecodeCompiler.emit(Opcodes.GOTO_IF_TRUE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitInt(0); - // NOW compile right operand (only executed if left was false) - node.right.accept(bytecodeCompiler); - int rs2 = bytecodeCompiler.lastResultReg; + node.right.accept(bytecodeCompiler); + int rs2 = bytecodeCompiler.lastResultReg; - // Move right result to rd (overwriting left value) - bytecodeCompiler.emit(Opcodes.MOVE); - bytecodeCompiler.emitReg(rd); - bytecodeCompiler.emitReg(rs2); + bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs2); - // Patch the forward jump offset - int skipRightTarget = bytecodeCompiler.bytecode.size(); - bytecodeCompiler.patchIntOffset(skipRightPos + 2, skipRightTarget); + int skipRightTarget = bytecodeCompiler.bytecode.size(); + bytecodeCompiler.patchIntOffset(skipRightPos + 2, skipRightTarget); - bytecodeCompiler.lastResultReg = rd; + bytecodeCompiler.lastResultReg = rd; + } finally { + if (rewrite != null) rewrite.restore(); + } return; } if (node.operator.equals("//")) { - // Defined-OR with short-circuit evaluation - // Only evaluate right side if left side is undefined - - // Compile left operand in scalar context (need to test definedness) - int savedContext = bytecodeCompiler.currentCallContext; - bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; - node.left.accept(bytecodeCompiler); - int rs1 = bytecodeCompiler.lastResultReg; - bytecodeCompiler.currentCallContext = savedContext; - - // Allocate result register and move left value to it - int rd = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.MOVE); - bytecodeCompiler.emitReg(rd); - bytecodeCompiler.emitReg(rs1); + DeclRewrite rewrite = extractMyDeclaration(bytecodeCompiler, node.right); + try { + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; + node.left.accept(bytecodeCompiler); + int rs1 = bytecodeCompiler.lastResultReg; + bytecodeCompiler.currentCallContext = savedContext; - // Check if left is defined - int definedReg = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.DEFINED); - bytecodeCompiler.emitReg(definedReg); - bytecodeCompiler.emitReg(rd); + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); - // Mark position for forward jump - int skipRightPos = bytecodeCompiler.bytecode.size(); + int definedReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.DEFINED); + bytecodeCompiler.emitReg(definedReg); + bytecodeCompiler.emitReg(rd); - // Emit conditional jump: if (defined) skip right evaluation - bytecodeCompiler.emit(Opcodes.GOTO_IF_TRUE); - bytecodeCompiler.emitReg(definedReg); - bytecodeCompiler.emitInt(0); // Placeholder for offset (will be patched) + int skipRightPos = bytecodeCompiler.bytecode.size(); + bytecodeCompiler.emit(Opcodes.GOTO_IF_TRUE); + bytecodeCompiler.emitReg(definedReg); + bytecodeCompiler.emitInt(0); - // NOW compile right operand (only executed if left was undefined) - node.right.accept(bytecodeCompiler); - int rs2 = bytecodeCompiler.lastResultReg; + node.right.accept(bytecodeCompiler); + int rs2 = bytecodeCompiler.lastResultReg; - // Move right result to rd (overwriting left value) - bytecodeCompiler.emit(Opcodes.MOVE); - bytecodeCompiler.emitReg(rd); - bytecodeCompiler.emitReg(rs2); + bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs2); - // Patch the forward jump offset - int skipRightTarget = bytecodeCompiler.bytecode.size(); - bytecodeCompiler.patchIntOffset(skipRightPos + 2, skipRightTarget); + int skipRightTarget = bytecodeCompiler.bytecode.size(); + bytecodeCompiler.patchIntOffset(skipRightPos + 2, skipRightTarget); - bytecodeCompiler.lastResultReg = rd; + bytecodeCompiler.lastResultReg = rd; + } finally { + if (rewrite != null) rewrite.restore(); + } return; } diff --git a/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java b/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java index dae5aa00b..eccbb790a 100644 --- a/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java +++ b/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java @@ -318,18 +318,18 @@ public static RuntimeScalar findMethodInHierarchy(String methodName, String perl if (GlobalVariable.existsGlobalCodeRef(normalizedClassMethodName)) { RuntimeScalar codeRef = GlobalVariable.getGlobalCodeRef(normalizedClassMethodName); // Perl method lookup should ignore undefined CODE slots (e.g. after `undef *pkg::method`). - if (!codeRef.getDefinedBoolean()) { - continue; - } - // Cache the found method - cacheMethod(cacheKey, codeRef); - - if (TRACE_METHOD_RESOLUTION) { - System.err.println(" FOUND method!"); - System.err.flush(); + // But don't skip to next class — fall through to AUTOLOAD check for this class. + if (codeRef.getDefinedBoolean()) { + // Cache the found method + cacheMethod(cacheKey, codeRef); + + if (TRACE_METHOD_RESOLUTION) { + System.err.println(" FOUND method!"); + System.err.flush(); + } + + return codeRef; } - - return codeRef; } // Method not found in current class, check AUTOLOAD