From 57db82e9db6a73118be0bfbca35a44fe3a1bab9e Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sat, 7 Mar 2026 17:18:25 +0100 Subject: [PATCH 1/8] Fix interpreter: pop/push/shift/unshift on @{...} expressions The interpreter compiler was not handling BlockNode operands for array deref expressions like @{$h->{PATH}}. When compiling pop/push/shift/ unshift with such expressions, the BlockNode was visited without proper context, causing lastResultReg to be -1. Changes: - CompileOperator.emitArrayOperandRegister: Accept BlockNode alongside OperatorNode, and use compileNode() with SCALAR context instead of raw accept() - BytecodeCompiler.handlePushUnshift: Use compileNode() with SCALAR context when evaluating array dereference operands Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../org/perlonjava/backend/bytecode/BytecodeCompiler.java | 2 +- .../java/org/perlonjava/backend/bytecode/CompileOperator.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index dc33f2dfa..2b9053041 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -1805,7 +1805,7 @@ void handlePushUnshift(BinaryOperatorNode node) { } else if (leftOp.operator.equals("@") && !(leftOp.operand instanceof IdentifierNode)) { // Array dereference: @$arrayref or @{expr} // Evaluate the operand expression to get the reference, then deref - leftOp.operand.accept(this); + compileNode(leftOp.operand, -1, RuntimeContextType.SCALAR); int refReg = lastResultReg; // Dereference to get the array diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index b9d9e40ea..144abcb18 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -152,8 +152,8 @@ private static int resolveArrayOperand(BytecodeCompiler bc, OperatorNode node, S bc.emit(nameIdx); return arrayReg; } - } else if (arrayOp.operand instanceof OperatorNode) { - arrayOp.operand.accept(bc); + } else if (arrayOp.operand instanceof OperatorNode || arrayOp.operand instanceof BlockNode) { + bc.compileNode(arrayOp.operand, -1, RuntimeContextType.SCALAR); int refReg = bc.lastResultReg; int arrayReg = bc.allocateRegister(); if (bc.isStrictRefsEnabled()) { From de72a9f135008cb794a9219e3d02cd057ee47f5c Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sat, 7 Mar 2026 17:54:58 +0100 Subject: [PATCH 2/8] Fix interpreter: symbolic sub calls and @{...} assignment Two interpreter parity fixes: 1. BytecodeInterpreter CALL_SUB: Resolve symbolic code references using current package for STRING/BYTE_STRING types. This fixes cases like \&$func() where $func contains a subroutine name string - the name is now resolved in the current package instead of main. 2. CompileAssignment: Handle BlockNode operands for @{...} = ... just like OperatorNode. This fixes assignment to array deref expressions like @{$h->{list}} = (1, 2, 3). Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/bytecode/BytecodeInterpreter.java | 10 ++++++++++ .../backend/bytecode/CompileAssignment.java | 14 ++++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index d0f3eb0f4..1ae834e47 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -794,6 +794,16 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c RuntimeScalar codeRef = (codeRefBase instanceof RuntimeScalar) ? (RuntimeScalar) codeRefBase : codeRefBase.scalar(); + + // Dereference symbolic code references using current package + // This matches the JVM backend's call to codeDerefNonStrict() + // Only call for STRING/BYTE_STRING types (symbolic references) + // For CODE, REFERENCE, etc. let RuntimeCode.apply() handle errors + if (codeRef.type == RuntimeScalarType.STRING || codeRef.type == RuntimeScalarType.BYTE_STRING) { + String currentPkg = InterpreterState.currentPackage.get().toString(); + codeRef = codeRef.codeDerefNonStrict(currentPkg); + } + RuntimeBase argsBase = registers[argsReg]; RuntimeArray callArgs; diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java index 01f68d4ba..166de9031 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java @@ -889,13 +889,15 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.emitReg(valueReg); bytecodeCompiler.lastResultReg = valueReg; - } else if (leftOp.operator.equals("@") && leftOp.operand instanceof OperatorNode derefOp) { - // Array dereference assignment: @$r = ... - // The operand should be a scalar variable containing an array reference + } else if (leftOp.operator.equals("@") && (leftOp.operand instanceof OperatorNode || leftOp.operand instanceof BlockNode)) { + // Array dereference assignment: @$r = ... or @{expr} = ... + // The operand should evaluate to an array reference - if (derefOp.operator.equals("$")) { - // Compile the scalar to get the array reference - bytecodeCompiler.compileNode(derefOp, -1, rhsContext); + boolean isSimpleScalarDeref = leftOp.operand instanceof OperatorNode derefOp && derefOp.operator.equals("$"); + + if (isSimpleScalarDeref || leftOp.operand instanceof BlockNode) { + // Compile the operand to get the array reference + bytecodeCompiler.compileNode(leftOp.operand, -1, RuntimeContextType.SCALAR); int scalarRefReg = bytecodeCompiler.lastResultReg; // Dereference to get the actual array From b4b10b27d99505cc8bf6e4f10a57361d8f92609f Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sat, 7 Mar 2026 18:13:29 +0100 Subject: [PATCH 3/8] Add CODE_DEREF_NONSTRICT opcode and fix *{$expr} for interpreter - Add CODE_DEREF_NONSTRICT opcode (375) for &{$name} dynamic code refs - Implement handler in SlowOpcodeHandler.executeCodeDerefNonStrict() - Handle BlockNode for * (glob) dereference operator - Fix tr/// to compile operands in SCALAR context Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/bytecode/BytecodeCompiler.java | 21 +++++++++++++++--- .../backend/bytecode/BytecodeInterpreter.java | 5 ++++- .../backend/bytecode/CompileOperator.java | 20 ++++++++++++----- .../backend/bytecode/Disassemble.java | 6 +++++ .../perlonjava/backend/bytecode/Opcodes.java | 7 ++++++ .../backend/bytecode/SlowOpcodeHandler.java | 22 +++++++++++++++++++ 6 files changed, 72 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 2b9053041..b519515f3 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -3519,8 +3519,8 @@ void compileVariableReference(OperatorNode node, String op) { emitReg(rd); emit(nameIdx); lastResultReg = rd; - } else if (node.operand instanceof OperatorNode) { - node.operand.accept(this); + } else if (node.operand instanceof OperatorNode || node.operand instanceof BlockNode) { + compileNode(node.operand, -1, RuntimeContextType.SCALAR); int refReg = lastResultReg; int rd = allocateOutputRegister(); int pkgIdx = addToStringPool(getCurrentPackage()); @@ -3537,7 +3537,7 @@ void compileVariableReference(OperatorNode node, String op) { throwCompilerException("Unsupported * operand: " + node.operand.getClass().getSimpleName()); } } else if (op.equals("&")) { - // Code reference: &subname + // Code reference: &subname or &{expr} // Gets a reference to a named subroutine if (node.operand instanceof IdentifierNode idNode) { String subName = idNode.name; @@ -3555,6 +3555,21 @@ void compileVariableReference(OperatorNode node, String op) { emitReg(rd); emit(nameIdx); + lastResultReg = rd; + } else if (node.operand instanceof BlockNode || node.operand instanceof OperatorNode) { + // Dynamic code reference: &{$name} or &$name + // Compile the expression to get the name/value, then dereference as code + compileNode(node.operand, -1, RuntimeContextType.SCALAR); + int valueReg = lastResultReg; + + // Use CODE_DEREF_NONSTRICT to look up the code reference + int rd = allocateOutputRegister(); + int pkgIdx = addToStringPool(getCurrentPackage()); + emit(Opcodes.CODE_DEREF_NONSTRICT); + emitReg(rd); + emitReg(valueReg); + emit(pkgIdx); + lastResultReg = rd; } else { throwCompilerException("Unsupported & operand: " + node.operand.getClass().getSimpleName()); diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 1ae834e47..e51501eda 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -1390,7 +1390,7 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c case Opcodes.EVAL_STRING, Opcodes.SELECT_OP, Opcodes.LOAD_GLOB, Opcodes.SLEEP_OP, Opcodes.ALARM_OP, Opcodes.DEREF_GLOB, Opcodes.DEREF_GLOB_NONSTRICT, Opcodes.LOAD_GLOB_DYNAMIC, Opcodes.DEREF_SCALAR_STRICT, - Opcodes.DEREF_SCALAR_NONSTRICT -> { + Opcodes.DEREF_SCALAR_NONSTRICT, Opcodes.CODE_DEREF_NONSTRICT -> { pc = executeSpecialIO(opcode, bytecode, pc, registers, code); } @@ -2105,6 +2105,9 @@ private static int executeSpecialIO(int opcode, int[] bytecode, int pc, case Opcodes.DEREF_SCALAR_NONSTRICT -> { return SlowOpcodeHandler.executeDerefScalarNonStrict(bytecode, pc, registers, code); } + case Opcodes.CODE_DEREF_NONSTRICT -> { + return SlowOpcodeHandler.executeCodeDerefNonStrict(bytecode, pc, registers, code); + } default -> throw new RuntimeException("Unknown special I/O opcode: " + opcode); } } diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index 144abcb18..9feaafbe1 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -1201,12 +1201,22 @@ private static void visitTransliterate(BytecodeCompiler bc, OperatorNode node) { if (!(node.operand instanceof ListNode)) bc.throwCompilerException("tr operator requires list operand"); ListNode list = (ListNode) node.operand; if (list.elements.size() < 3) bc.throwCompilerException("tr operator requires search, replace, and modifiers"); - list.elements.get(0).accept(bc); int searchReg = bc.lastResultReg; - list.elements.get(1).accept(bc); int replaceReg = bc.lastResultReg; - list.elements.get(2).accept(bc); int modifiersReg = bc.lastResultReg; + // Compile all elements in SCALAR context (matches JVM backend) + bc.compileNode(list.elements.get(0), -1, RuntimeContextType.SCALAR); int searchReg = bc.lastResultReg; + bc.compileNode(list.elements.get(1), -1, RuntimeContextType.SCALAR); int replaceReg = bc.lastResultReg; + bc.compileNode(list.elements.get(2), -1, RuntimeContextType.SCALAR); int modifiersReg = bc.lastResultReg; int targetReg; - if (list.elements.size() > 3 && list.elements.get(3) != null) { list.elements.get(3).accept(bc); targetReg = bc.lastResultReg; } - else { targetReg = bc.allocateRegister(); int nameIdx = bc.addToStringPool(NameNormalizer.normalizeVariableName("_", bc.getCurrentPackage())); bc.emit(Opcodes.LOAD_GLOBAL_SCALAR); bc.emitReg(targetReg); bc.emit(nameIdx); } + if (list.elements.size() > 3 && list.elements.get(3) != null) { + // Target like ($y = $x) must be compiled in SCALAR context to get the scalar lvalue, not a list + bc.compileNode(list.elements.get(3), -1, RuntimeContextType.SCALAR); + targetReg = bc.lastResultReg; + } else { + targetReg = bc.allocateRegister(); + int nameIdx = bc.addToStringPool(NameNormalizer.normalizeVariableName("_", bc.getCurrentPackage())); + bc.emit(Opcodes.LOAD_GLOBAL_SCALAR); + bc.emitReg(targetReg); + bc.emit(nameIdx); + } int rd = bc.allocateOutputRegister(); bc.emit(Opcodes.TR_TRANSLITERATE); bc.emitReg(rd); bc.emitReg(searchReg); bc.emitReg(replaceReg); bc.emitReg(modifiersReg); bc.emitReg(targetReg); bc.emitInt(bc.currentCallContext); bc.lastResultReg = rd; diff --git a/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java b/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java index f190853aa..59919dc89 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java @@ -1256,6 +1256,12 @@ public static String disassemble(InterpretedCode interpretedCode) { nameIdx = interpretedCode.bytecode[pc++]; sb.append("DEREF_SCALAR_NONSTRICT r").append(rd).append(" = ${r").append(rs).append("} pkg=").append(interpretedCode.stringPool[nameIdx]).append("\n"); break; + case Opcodes.CODE_DEREF_NONSTRICT: + rd = interpretedCode.bytecode[pc++]; + rs = interpretedCode.bytecode[pc++]; + nameIdx = interpretedCode.bytecode[pc++]; + sb.append("CODE_DEREF_NONSTRICT r").append(rd).append(" = &{r").append(rs).append("} pkg=").append(interpretedCode.stringPool[nameIdx]).append("\n"); + break; case Opcodes.RETRIEVE_BEGIN_SCALAR: rd = interpretedCode.bytecode[pc++]; nameIdx = interpretedCode.bytecode[pc++]; diff --git a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java index 073ef588f..0a9b9e28d 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java @@ -1822,6 +1822,13 @@ public class Opcodes { */ public static final short QUOTE_REGEX_O = 374; + /** + * Code dereference (non-strict): rd = value.codeDerefNonStrict(package) + * For &{$name} - looks up code reference from symbolic name using current package. + * Format: CODE_DEREF_NONSTRICT rd value_reg package_string_idx + */ + public static final short CODE_DEREF_NONSTRICT = 375; + private Opcodes() { } // Utility class - no instantiation } diff --git a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java index a4af9a1c7..1d4a00074 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java @@ -1175,4 +1175,26 @@ public static int executeGlobSlotGet( return pc; } + + /** + * CODE_DEREF_NONSTRICT: rd = value.codeDerefNonStrict(package) + * For &{$name} - looks up code reference from symbolic name using specified package. + * Format: [CODE_DEREF_NONSTRICT] [rd] [value_reg] [package_string_idx] + */ + public static int executeCodeDerefNonStrict(int[] bytecode, int pc, + RuntimeBase[] registers, InterpretedCode code) { + int rd = bytecode[pc++]; + int valueReg = bytecode[pc++]; + int pkgIdx = bytecode[pc++]; + + RuntimeBase valueBase = registers[valueReg]; + RuntimeScalar value = (valueBase instanceof RuntimeScalar) + ? (RuntimeScalar) valueBase + : valueBase.scalar(); + + String pkg = code.stringPool[pkgIdx]; + registers[rd] = value.codeDerefNonStrict(pkg); + + return pc; + } } From 1d2dde20b4babc5e924c7112c3561693e8da6af6 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sat, 7 Mar 2026 18:37:24 +0100 Subject: [PATCH 4/8] Fix interpreter: compile hash/array keys and indices in scalar context Several places in the bytecode compiler were using accept(this) instead of compileNode with RuntimeContextType.SCALAR for hash keys and array indices. This caused ClassCastException when constant functions or method calls were used as keys/indices, as they return RuntimeList but the opcodes expect RuntimeScalar. Fixed locations: - BytecodeCompiler: handleArrayElementAccess, handleGeneralArrayAccess, handleArrayKeyValueSlice, handleHashSlice, handleHashKeyValueSlice, handleArraySlice (added BlockNode support) - CompileAssignment: hash key compilation in 3 places, added %$ref = ... hash dereference assignment support - CompileExistsDelete: visitExistsArrow, visitDeleteArrow, visitDeleteHashSlice, compileHashKey, compileArrayIndex - CompileOperator: compileArrayIndex Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/bytecode/BytecodeCompiler.java | 46 +++++++++++++++---- .../backend/bytecode/CompileAssignment.java | 46 +++++++++++++++++-- .../backend/bytecode/CompileExistsDelete.java | 18 +++++--- .../backend/bytecode/CompileOperator.java | 3 +- 4 files changed, 91 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index b519515f3..35bc4716e 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -938,7 +938,8 @@ void handleArrayKeyValueSlice(BinaryOperatorNode node, OperatorNode leftOp) { // Compile indices into a list List pairRegs = new ArrayList<>(); for (Node indexElement : indicesNode.elements) { - indexElement.accept(this); + // Compile index in SCALAR context to ensure RuntimeScalar + compileNode(indexElement, -1, RuntimeContextType.SCALAR); int indexReg = lastResultReg; int valueReg = allocateRegister(); @@ -1118,7 +1119,8 @@ void handleArrayElementAccess(BinaryOperatorNode node, OperatorNode leftOp) { // Handle single element access: $array[0] if (indexNode.elements.size() == 1) { Node indexExpr = indexNode.elements.get(0); - indexExpr.accept(this); + // Compile index in SCALAR context to ensure RuntimeScalar + compileNode(indexExpr, -1, RuntimeContextType.SCALAR); int indexReg = lastResultReg; // Emit ARRAY_GET opcode @@ -1197,6 +1199,28 @@ void handleArraySlice(BinaryOperatorNode node, OperatorNode leftOp) { throwCompilerException("Array slice requires array or array reference"); return; } + } else if (leftOp.operand instanceof BlockNode blockNode) { + // Array dereference slice with block: @{$arrayref}[indices] + // Compile the block to get the array reference + int savedContext = currentCallContext; + currentCallContext = RuntimeContextType.SCALAR; + blockNode.accept(this); + currentCallContext = savedContext; + int refReg = lastResultReg; + + // Dereference to get the array + arrayReg = allocateRegister(); + if (isStrictRefsEnabled()) { + emitWithToken(Opcodes.DEREF_ARRAY, node.getIndex()); + emitReg(arrayReg); + emitReg(refReg); + } else { + int pkgIdx = addToStringPool(getCurrentPackage()); + emitWithToken(Opcodes.DEREF_ARRAY_NONSTRICT, node.getIndex()); + emitReg(arrayReg); + emitReg(refReg); + emit(pkgIdx); + } } else { throwCompilerException("Array slice requires identifier or reference"); return; @@ -1305,8 +1329,8 @@ void handleHashElementAccess(BinaryOperatorNode node, OperatorNode leftOp) { lastResultReg = rd; } else { - // Expression key - compile it - keyExpr.accept(this); + // Expression key - compile it in SCALAR context to ensure we get RuntimeScalar + compileNode(keyExpr, -1, RuntimeContextType.SCALAR); int keyReg = lastResultReg; // Emit HASH_GET opcode @@ -1401,8 +1425,8 @@ void handleHashSlice(BinaryOperatorNode node, OperatorNode leftOp) { emit(keyIdx); keyRegs.add(keyReg); } else { - // Expression key - keyElement.accept(this); + // Expression key - compile in SCALAR context + compileNode(keyElement, -1, RuntimeContextType.SCALAR); keyRegs.add(lastResultReg); } } @@ -1495,7 +1519,8 @@ void handleHashKeyValueSlice(BinaryOperatorNode node, OperatorNode leftOp) { emit(keyIdx); keyRegs.add(keyReg); } else { - keyElement.accept(this); + // Expression key - compile in SCALAR context + compileNode(keyElement, -1, RuntimeContextType.SCALAR); keyRegs.add(lastResultReg); } } @@ -1644,7 +1669,8 @@ void handleGeneralArrayAccess(BinaryOperatorNode node) { // Handle single element access if (indexNode.elements.size() == 1) { Node indexExpr = indexNode.elements.get(0); - indexExpr.accept(this); + // Compile index in SCALAR context to ensure RuntimeScalar + compileNode(indexExpr, -1, RuntimeContextType.SCALAR); int indexReg = lastResultReg; // The base might be either: @@ -1714,8 +1740,8 @@ void handleGeneralHashAccess(BinaryOperatorNode node) { emitReg(keyReg); emit(keyIdx); } else { - // Expression key - compile it - keyExpr.accept(this); + // Expression key - compile it in SCALAR context to ensure RuntimeScalar + compileNode(keyExpr, -1, RuntimeContextType.SCALAR); keyReg = lastResultReg; } diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java index 166de9031..8f329038d 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java @@ -938,6 +938,41 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.emitReg(arrayReg); bytecodeCompiler.emitReg(valueReg); bytecodeCompiler.lastResultReg = valueReg; + } else if (leftOp.operator.equals("%") && (leftOp.operand instanceof OperatorNode || leftOp.operand instanceof BlockNode)) { + // Hash dereference assignment: %$r = ... or %{expr} = ... + // The operand should evaluate to a hash reference + + boolean isSimpleScalarDeref = leftOp.operand instanceof OperatorNode derefOp && derefOp.operator.equals("$"); + + if (isSimpleScalarDeref || leftOp.operand instanceof BlockNode) { + // Compile the operand to get the hash reference + bytecodeCompiler.compileNode(leftOp.operand, -1, RuntimeContextType.SCALAR); + int scalarRefReg = bytecodeCompiler.lastResultReg; + + // Dereference to get the actual hash + int hashReg = bytecodeCompiler.allocateRegister(); + if (bytecodeCompiler.isStrictRefsEnabled()) { + bytecodeCompiler.emitWithToken(Opcodes.DEREF_HASH, node.getIndex()); + bytecodeCompiler.emitReg(hashReg); + bytecodeCompiler.emitReg(scalarRefReg); + } else { + int pkgIdx = bytecodeCompiler.addToStringPool(bytecodeCompiler.getCurrentPackage()); + bytecodeCompiler.emitWithToken(Opcodes.DEREF_HASH_NONSTRICT, node.getIndex()); + bytecodeCompiler.emitReg(hashReg); + bytecodeCompiler.emitReg(scalarRefReg); + bytecodeCompiler.emit(pkgIdx); + } + + // Assign the value to the dereferenced hash + bytecodeCompiler.emit(Opcodes.HASH_SET_FROM_LIST); + bytecodeCompiler.emitReg(hashReg); + bytecodeCompiler.emitReg(valueReg); + + // In list context, return the hash flattened; in other contexts return the hash + bytecodeCompiler.lastResultReg = hashReg; + } else { + bytecodeCompiler.throwCompilerException("Assignment to unsupported hash dereference"); + } } else { if (leftOp.operator.equals("chop") || leftOp.operator.equals("chomp")) { bytecodeCompiler.throwCompilerException("Can't modify " + leftOp.operator + " in scalar assignment"); @@ -1213,8 +1248,8 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.emit(keyIdx); keyRegs.add(keyReg); } else { - // Expression key - bytecodeCompiler.compileNode(keyElement, -1, rhsContext); + // Expression key - must be compiled in SCALAR context + bytecodeCompiler.compileNode(keyElement, -1, RuntimeContextType.SCALAR); keyRegs.add(bytecodeCompiler.lastResultReg); } } @@ -1332,8 +1367,8 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.emitReg(keyReg); bytecodeCompiler.emit(keyIdx); } else { - // Expression key: $hash{$var} or $hash{func()} - bytecodeCompiler.compileNode(keyElement, -1, rhsContext); + // Expression key: $hash{$var} or $hash{func()} - must be compiled in SCALAR context + bytecodeCompiler.compileNode(keyElement, -1, RuntimeContextType.SCALAR); keyReg = bytecodeCompiler.lastResultReg; } @@ -1383,7 +1418,8 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.emitReg(keyReg); bytecodeCompiler.emit(keyIdx); } else { - bytecodeCompiler.compileNode(keyElement, -1, rhsContext); + // Expression key - must be compiled in SCALAR context + bytecodeCompiler.compileNode(keyElement, -1, RuntimeContextType.SCALAR); keyReg = bytecodeCompiler.lastResultReg; } } else { diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileExistsDelete.java b/src/main/java/org/perlonjava/backend/bytecode/CompileExistsDelete.java index 9ca1fd39c..b426400e6 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileExistsDelete.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileExistsDelete.java @@ -69,7 +69,8 @@ private static void visitExistsArrow(BytecodeCompiler bc, OperatorNode node, Bin bc.throwCompilerException("Array index required for exists"); return; } - indexNode.elements.get(0).accept(bc); + // Compile index in SCALAR context + bc.compileNode(indexNode.elements.get(0), -1, RuntimeContextType.SCALAR); int indexReg = bc.lastResultReg; int rd = bc.allocateOutputRegister(); bc.emit(Opcodes.ARRAY_EXISTS); @@ -157,7 +158,8 @@ private static void visitDeleteHashSlice(BytecodeCompiler bc, OperatorNode node, bc.emit(keyIdx); keyRegs.add(keyReg); } else { - keyElement.accept(bc); + // Compile key in SCALAR context + bc.compileNode(keyElement, -1, RuntimeContextType.SCALAR); keyRegs.add(bc.lastResultReg); } } @@ -207,7 +209,8 @@ private static void visitDeleteArrow(BytecodeCompiler bc, OperatorNode node, Bin bc.throwCompilerException("Array index required for delete"); return; } - indexNode.elements.get(0).accept(bc); + // Compile index in SCALAR context + bc.compileNode(indexNode.elements.get(0), -1, RuntimeContextType.SCALAR); int indexReg = bc.lastResultReg; int rd = bc.allocateOutputRegister(); bc.emit(Opcodes.ARRAY_DELETE); @@ -301,11 +304,13 @@ private static int compileHashKey(BytecodeCompiler bc, Node keySpec) { bc.emit(keyIdx); return keyReg; } else { - keyElement.accept(bc); + // Compile in scalar context to ensure we get a RuntimeScalar + bc.compileNode(keyElement, -1, RuntimeContextType.SCALAR); return bc.lastResultReg; } } else { - keySpec.accept(bc); + // Compile in scalar context to ensure we get a RuntimeScalar + bc.compileNode(keySpec, -1, RuntimeContextType.SCALAR); return bc.lastResultReg; } } @@ -345,7 +350,8 @@ private static int compileArrayIndex(BytecodeCompiler bc, BinaryOperatorNode arr bc.throwCompilerException("Array exists/delete requires index"); return -1; } - indexNode.elements.get(0).accept(bc); + // Compile in scalar context to ensure we get a RuntimeScalar + bc.compileNode(indexNode.elements.get(0), -1, RuntimeContextType.SCALAR); return bc.lastResultReg; } } diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index 9feaafbe1..f1f89d504 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -55,7 +55,8 @@ private static int compileArrayIndex(BytecodeCompiler bc, BinaryOperatorNode arr bc.throwCompilerException("Array exists/delete requires index"); return -1; } - indexNode.elements.get(0).accept(bc); + // Compile index in SCALAR context + bc.compileNode(indexNode.elements.get(0), -1, RuntimeContextType.SCALAR); return bc.lastResultReg; } From 719ba6cc2136a37b79663557e8c98661ff3978d7 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sat, 7 Mar 2026 18:56:18 +0100 Subject: [PATCH 5/8] Fix statement modifier my declaration to always declare variable The issue was that 'my $x = EXPR if COND' was transformed to 'COND && (my $x = EXPR)' which would skip the variable declaration entirely when the condition was false. This caused null pointer exceptions in the interpreter when later code tried to access $x. The fix transforms 'my $x = EXPR if COND' to '(my $x, COND && ($x = EXPR))' using the comma operator. This ensures: - The variable is always declared in the current scope (via 'my $x') - The assignment only happens when the condition is true This matches Perl behavior where 'my $x = 1 if 0' declares $x as undef. Same fix applies to 'unless' modifier. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../frontend/parser/StatementResolver.java | 49 ++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/perlonjava/frontend/parser/StatementResolver.java b/src/main/java/org/perlonjava/frontend/parser/StatementResolver.java index 4fc823467..4ba4e991f 100644 --- a/src/main/java/org/perlonjava/frontend/parser/StatementResolver.java +++ b/src/main/java/org/perlonjava/frontend/parser/StatementResolver.java @@ -626,14 +626,16 @@ yield new For3Node(label, TokenUtils.consume(parser); Node modifierExpression = parser.parseExpression(0); parseStatementTerminator(parser); - yield new BinaryOperatorNode("&&", modifierExpression, expression, parser.tokenIndex); + // Handle "my $x = EXPR if COND" - must declare variable even when condition is false + yield handleStatementModifierWithMy(expression, modifierExpression, "&&", parser.tokenIndex); } case "unless" -> { TokenUtils.consume(parser); Node modifierExpression = parser.parseExpression(0); parseStatementTerminator(parser); - yield new BinaryOperatorNode("||", modifierExpression, expression, parser.tokenIndex); + // Handle "my $x = EXPR unless COND" - must declare variable even when condition is true + yield handleStatementModifierWithMy(expression, modifierExpression, "||", parser.tokenIndex); } case "for", "foreach" -> { @@ -874,4 +876,47 @@ public static void parseStatementTerminator(Parser parser) { consume(parser); } } + + /** + * Handle statement modifiers (if/unless) with my declarations. + * For "my $x = EXPR if COND", the variable must be declared even when condition is false. + * Uses comma operator to declare variable in current scope: (my $x, COND && ($x = EXPR)) + * This avoids creating a new scope (which BlockNode would do). + * + * @param expression The main expression (e.g., my $x = shift) + * @param modifierExpression The condition expression + * @param operator "&&" for if, "||" for unless + * @param tokenIndex Token index for error reporting + * @return Transformed AST node + */ + private static Node handleStatementModifierWithMy(Node expression, Node modifierExpression, + String operator, int tokenIndex) { + // Check if expression is an assignment with 'my' on the left side + if (expression instanceof BinaryOperatorNode assignNode && assignNode.operator.equals("=")) { + Node left = assignNode.left; + // Check if left side is a 'my' declaration + if (left instanceof OperatorNode myNode && myNode.operator.equals("my")) { + // Transform: my $x = EXPR if COND + // Into: (my $x, COND && ($x = EXPR)) + // The comma operator evaluates both in the current scope (no new scope created) + // This ensures $x is declared even when condition is false + + // Extract the variable being declared + Node variable = myNode.operand; + + // Create the assignment without 'my': $x = EXPR + Node plainAssignment = new BinaryOperatorNode("=", variable, assignNode.right, tokenIndex); + + // Create the conditional: COND && ($x = EXPR) or COND || ($x = EXPR) + Node conditional = new BinaryOperatorNode(operator, modifierExpression, plainAssignment, tokenIndex); + + // Use comma operator: (my $x, conditional) + // ListNode in statement context acts as comma operator + return new ListNode(List.of(left, conditional), tokenIndex); + } + } + + // No 'my' declaration, use simple short-circuit + return new BinaryOperatorNode(operator, modifierExpression, expression, tokenIndex); + } } From f5b4a778fabee7a74fcebc09196d130c6779cd73 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sat, 7 Mar 2026 19:09:35 +0100 Subject: [PATCH 6/8] Add BlockNode support for hash slice: @{$hashref}{keys} The hash slice compiler was missing support for BlockNode operands, which is needed for syntax like @{$$et{OPTIONS}}{qw(key1 key2)}. This was causing 'Hash slice requires hash variable or reference' compile error. Added BlockNode handling to handleHashSlice() matching the existing pattern in handleArraySlice(). Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/bytecode/BytecodeCompiler.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 35bc4716e..501dd10c0 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -1398,6 +1398,28 @@ void handleHashSlice(BinaryOperatorNode node, OperatorNode leftOp) { emitReg(scalarRefReg); emit(pkgIdx); } + } else if (leftOp.operand instanceof BlockNode blockNode) { + // Hash dereference slice with block: @{$hashref}{keys} + // Compile the block to get the hash reference + int savedContext = currentCallContext; + currentCallContext = RuntimeContextType.SCALAR; + blockNode.accept(this); + currentCallContext = savedContext; + int refReg = lastResultReg; + + // Dereference to get the hash + hashReg = allocateRegister(); + if (isStrictRefsEnabled()) { + emitWithToken(Opcodes.DEREF_HASH, node.getIndex()); + emitReg(hashReg); + emitReg(refReg); + } else { + int pkgIdx = addToStringPool(getCurrentPackage()); + emitWithToken(Opcodes.DEREF_HASH_NONSTRICT, node.getIndex()); + emitReg(hashReg); + emitReg(refReg); + emit(pkgIdx); + } } else { throwCompilerException("Hash slice requires hash variable or reference"); return; From 159a19c15e27fea78d468d9b3a8693629a2f9548 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sat, 7 Mar 2026 19:19:03 +0100 Subject: [PATCH 7/8] Add BlockNode support for $# (array last index) operator Added BlockNode handling for $#{BLOCK} syntax in both: - CompileOperator.java: for reading $#{...} - CompileAssignment.java: for assigning $#{...} -= value This fixes compile error for syntax like: $#{$$dirInfo{VarFormatData}} -= 1 if $wasVar; Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/bytecode/CompileAssignment.java | 20 +++++++++++++++++++ .../backend/bytecode/CompileOperator.java | 10 ++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java index 8f329038d..1ec615182 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java @@ -1695,6 +1695,26 @@ static int resolveArrayForDollarHash(BytecodeCompiler bytecodeCompiler, Operator bytecodeCompiler.emit(pkgIdx); } return arrayReg; + } else if (dollarHashOp.operand instanceof BlockNode blockNode) { + // $#{BLOCK} = value - evaluate block to get array reference + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; + blockNode.accept(bytecodeCompiler); + bytecodeCompiler.currentCallContext = savedContext; + int refReg = bytecodeCompiler.lastResultReg; + int arrayReg = bytecodeCompiler.allocateRegister(); + if (bytecodeCompiler.isStrictRefsEnabled()) { + bytecodeCompiler.emitWithToken(Opcodes.DEREF_ARRAY, dollarHashOp.getIndex()); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emitReg(refReg); + } else { + int pkgIdx = bytecodeCompiler.addToStringPool(bytecodeCompiler.getCurrentPackage()); + bytecodeCompiler.emitWithToken(Opcodes.DEREF_ARRAY_NONSTRICT, dollarHashOp.getIndex()); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emitReg(refReg); + bytecodeCompiler.emit(pkgIdx); + } + return arrayReg; } bytecodeCompiler.throwCompilerException("$# assignment requires array variable"); return -1; diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index f1f89d504..e541db5e8 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -1167,6 +1167,16 @@ private static void visitArrayLastIndex(BytecodeCompiler bc, OperatorNode node) int nameIdx = bc.addToStringPool(NameNormalizer.normalizeVariableName(((IdentifierNode) node.operand).name, bc.getCurrentPackage())); bc.emit(Opcodes.LOAD_GLOBAL_ARRAY); bc.emitReg(arrayReg); bc.emit(nameIdx); } + } else if (node.operand instanceof BlockNode blockNode) { + // $#{BLOCK} - evaluate block to get array reference, then get last index + int savedContext = bc.currentCallContext; + bc.currentCallContext = RuntimeContextType.SCALAR; + blockNode.accept(bc); + bc.currentCallContext = savedContext; + int refReg = bc.lastResultReg; + arrayReg = bc.allocateRegister(); + if (bc.isStrictRefsEnabled()) { bc.emitWithToken(Opcodes.DEREF_ARRAY, node.getIndex()); bc.emitReg(arrayReg); bc.emitReg(refReg); } + else { int pkgIdx = bc.addToStringPool(bc.getCurrentPackage()); bc.emitWithToken(Opcodes.DEREF_ARRAY_NONSTRICT, node.getIndex()); bc.emitReg(arrayReg); bc.emitReg(refReg); bc.emit(pkgIdx); } } else bc.throwCompilerException("$# requires array variable"); int sizeReg = bc.allocateRegister(); bc.emit(Opcodes.ARRAY_SIZE); bc.emitReg(sizeReg); bc.emitReg(arrayReg); int oneReg = bc.allocateRegister(); bc.emit(Opcodes.LOAD_INT); bc.emitReg(oneReg); bc.emitInt(1); From b23dbbde9a522741ef6295a2e48ef6e22c9218f7 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sat, 7 Mar 2026 20:19:44 +0100 Subject: [PATCH 8/8] Fix interpreter: use LIST context for hash slice keys The commit 1d2dde20 incorrectly forced SCALAR context for hash slice keys like @hash{@codes}, which prevented array keys from expanding. Revert hash slice key compilation to use LIST context (like JVM backend), while keeping SCALAR context for single element access. This fixes 4340 test regressions in pack.t. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../org/perlonjava/backend/bytecode/BytecodeCompiler.java | 8 ++++---- .../perlonjava/backend/bytecode/CompileAssignment.java | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 501dd10c0..f1502d53a 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -1447,8 +1447,8 @@ void handleHashSlice(BinaryOperatorNode node, OperatorNode leftOp) { emit(keyIdx); keyRegs.add(keyReg); } else { - // Expression key - compile in SCALAR context - compileNode(keyElement, -1, RuntimeContextType.SCALAR); + // Expression key - use default context to allow arrays to expand + keyElement.accept(this); keyRegs.add(lastResultReg); } } @@ -1541,8 +1541,8 @@ void handleHashKeyValueSlice(BinaryOperatorNode node, OperatorNode leftOp) { emit(keyIdx); keyRegs.add(keyReg); } else { - // Expression key - compile in SCALAR context - compileNode(keyElement, -1, RuntimeContextType.SCALAR); + // Expression key - use default context to allow arrays to expand + keyElement.accept(this); keyRegs.add(lastResultReg); } } diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java index 1ec615182..0d9e25c8c 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java @@ -1248,8 +1248,8 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.emit(keyIdx); keyRegs.add(keyReg); } else { - // Expression key - must be compiled in SCALAR context - bytecodeCompiler.compileNode(keyElement, -1, RuntimeContextType.SCALAR); + // Expression key - use default context to allow arrays to expand + keyElement.accept(bytecodeCompiler); keyRegs.add(bytecodeCompiler.lastResultReg); } }