From e2c83211abf5305c9b44d7dfa396d8620ee757fb Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 13 Mar 2026 14:38:20 +0100 Subject: [PATCH 1/2] Fix list assignment to hash/array element lvalues in interpreter Simplified list assignment in bytecode compiler to follow the JVM backend approach: compile the LHS ListNode directly to produce a RuntimeList of lvalues, then use SET_FROM_LIST. Now list assignments like ($hash{key}) = @arr work correctly. Generated with Devin (https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/bytecode/CompileAssignment.java | 101 +++--------------- 1 file changed, 12 insertions(+), 89 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java index afaeb4f22..32e8fae78 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java @@ -1522,6 +1522,8 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, } else if (node.left instanceof ListNode listNode) { // List assignment: ($a, $b) = ... or () = ... + // Follow the JVM backend approach: compile LHS as a list of lvalues, + // then call setFromList() on it. // In scalar context, returns the number of elements on RHS // In list context, returns the RHS list LValueVisitor.getContext(node.left); @@ -1536,80 +1538,6 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.emitReg(rhsListReg); bytecodeCompiler.emitReg(rhsReg); - // Resolve all LHS variables and collect their registers - List varRegs = new ArrayList<>(); - for (Node lhsElement : listNode.elements) { - if (lhsElement instanceof OperatorNode lhsOp && lhsOp.operand instanceof IdentifierNode idNode) { - String sigil = lhsOp.operator; - String varName = sigil + idNode.name; - - if (sigil.equals("$")) { - if (bytecodeCompiler.hasVariable(varName)) { - int targetReg = bytecodeCompiler.getVariableRegister(varName); - if (!((bytecodeCompiler.capturedVarIndices != null && bytecodeCompiler.capturedVarIndices.containsKey(varName)) - || bytecodeCompiler.closureCapturedVarNames.contains(varName))) { - bytecodeCompiler.emit(Opcodes.LOAD_UNDEF); - bytecodeCompiler.emitReg(targetReg); - } - varRegs.add(targetReg); - } else { - if (bytecodeCompiler.shouldBlockGlobalUnderStrictVars(varName)) { - bytecodeCompiler.throwCompilerException("Global symbol \"" + varName + "\" requires explicit package name"); - } - String normalizedName = NameNormalizer.normalizeVariableName(idNode.name, bytecodeCompiler.getCurrentPackage()); - int nameIdx = bytecodeCompiler.addToStringPool(normalizedName); - int globalReg = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_SCALAR); - bytecodeCompiler.emitReg(globalReg); - bytecodeCompiler.emit(nameIdx); - varRegs.add(globalReg); - } - } else if (sigil.equals("@")) { - int arrayReg; - if (bytecodeCompiler.currentSubroutineBeginId != 0 && bytecodeCompiler.currentSubroutineClosureVars != null - && bytecodeCompiler.currentSubroutineClosureVars.contains(varName)) { - arrayReg = bytecodeCompiler.allocateRegister(); - int nameIdx = bytecodeCompiler.addToStringPool(varName); - bytecodeCompiler.emitWithToken(Opcodes.RETRIEVE_BEGIN_ARRAY, node.getIndex()); - bytecodeCompiler.emitReg(arrayReg); - bytecodeCompiler.emit(nameIdx); - bytecodeCompiler.emit(bytecodeCompiler.currentSubroutineBeginId); - } else if (bytecodeCompiler.hasVariable(varName)) { - arrayReg = bytecodeCompiler.getVariableRegister(varName); - } else { - arrayReg = bytecodeCompiler.allocateRegister(); - String globalName = NameNormalizer.normalizeVariableName(idNode.name, bytecodeCompiler.getCurrentPackage()); - int nameIdx = bytecodeCompiler.addToStringPool(globalName); - bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_ARRAY); - bytecodeCompiler.emitReg(arrayReg); - bytecodeCompiler.emit(nameIdx); - } - varRegs.add(arrayReg); - } else if (sigil.equals("%")) { - int hashReg; - if (bytecodeCompiler.currentSubroutineBeginId != 0 && bytecodeCompiler.currentSubroutineClosureVars != null - && bytecodeCompiler.currentSubroutineClosureVars.contains(varName)) { - hashReg = bytecodeCompiler.allocateRegister(); - int nameIdx = bytecodeCompiler.addToStringPool(varName); - bytecodeCompiler.emitWithToken(Opcodes.RETRIEVE_BEGIN_HASH, node.getIndex()); - bytecodeCompiler.emitReg(hashReg); - bytecodeCompiler.emit(nameIdx); - bytecodeCompiler.emit(bytecodeCompiler.currentSubroutineBeginId); - } else if (bytecodeCompiler.hasVariable(varName)) { - hashReg = bytecodeCompiler.getVariableRegister(varName); - } else { - hashReg = bytecodeCompiler.allocateRegister(); - String globalName = NameNormalizer.normalizeVariableName(idNode.name, bytecodeCompiler.getCurrentPackage()); - int nameIdx = bytecodeCompiler.addToStringPool(globalName); - bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_HASH); - bytecodeCompiler.emitReg(hashReg); - bytecodeCompiler.emit(nameIdx); - } - varRegs.add(hashReg); - } - } - } - int countReg = -1; if (outerContext == RuntimeContextType.SCALAR) { countReg = bytecodeCompiler.allocateRegister(); @@ -1618,22 +1546,17 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.emitReg(rhsListReg); } - // Build LHS list and assign via SET_FROM_LIST - if (!varRegs.isEmpty()) { - int lhsListReg = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.CREATE_LIST); - bytecodeCompiler.emitReg(lhsListReg); - bytecodeCompiler.emit(varRegs.size()); - for (int reg : varRegs) { - bytecodeCompiler.emitReg(reg); - } + // Compile LHS ListNode in LIST context - this produces a RuntimeList of lvalues + // This follows the JVM backend approach (EmitVariable.java line 837) + bytecodeCompiler.compileNode(listNode, -1, RuntimeContextType.LIST); + int lhsListReg = bytecodeCompiler.lastResultReg; - int resultReg = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.SET_FROM_LIST); - bytecodeCompiler.emitReg(resultReg); - bytecodeCompiler.emitReg(lhsListReg); - bytecodeCompiler.emitReg(rhsListReg); - } + // Call SET_FROM_LIST to assign RHS values to LHS lvalues + int resultReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.SET_FROM_LIST); + bytecodeCompiler.emitReg(resultReg); + bytecodeCompiler.emitReg(lhsListReg); + bytecodeCompiler.emitReg(rhsListReg); if (countReg >= 0) { bytecodeCompiler.lastResultReg = countReg; From 65607cb5d7dd253cbbf9e8fc7b4653c2e13e8d44 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 13 Mar 2026 15:01:39 +0100 Subject: [PATCH 2/2] Fix special filehandle (STDOUT/STDERR/STDIN) resolution in interpreter The interpreter was incorrectly qualifying special filehandles like STDOUT, STDERR, and STDIN with the current package instead of always using main::. For example, `\*STDOUT` in package Foo was resolving to `Foo::STDOUT` instead of `main::STDOUT`, causing the RuntimeIO to be null and print operations to silently fail. Fixed by using NameNormalizer.normalizeVariableName() which properly handles special variables that must always be in the main package: - BytecodeCompiler: glob (*) variable reference - CompileAssignment: local list assignment This fixes Test::More/Test2 output being silent in interpreter mode. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/bytecode/BytecodeCompiler.java | 15 +++++---------- .../backend/bytecode/CompileAssignment.java | 4 ++-- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 442ed885e..4341f4ab3 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -3731,12 +3731,9 @@ void compileVariableReference(OperatorNode node, String op) { } else if (op.equals("*")) { // Glob variable dereference: *x if (node.operand instanceof IdentifierNode idNode) { - String varName = idNode.name; - - // Add package prefix if not present - if (!varName.contains("::")) { - varName = getCurrentPackage() + "::" + varName; - } + // Use NameNormalizer to properly handle special handles (STDOUT, STDERR, STDIN, etc.) + // which must always be in the "main" package, regardless of current package. + String varName = NameNormalizer.normalizeVariableName(idNode.name, getCurrentPackage()); // Allocate register for glob int rd = allocateOutputRegister(); @@ -3750,10 +3747,8 @@ void compileVariableReference(OperatorNode node, String op) { lastResultReg = rd; } else if (node.operand instanceof StringNode strNode) { // Symbolic ref: *{'name'} or 'name'->** — load global glob by string name - String varName = strNode.value; - if (!varName.contains("::")) { - varName = getCurrentPackage() + "::" + varName; - } + // Use NameNormalizer to properly handle special handles (STDOUT, STDERR, etc.) + String varName = NameNormalizer.normalizeVariableName(strNode.value, getCurrentPackage()); int rd = allocateOutputRegister(); int nameIdx = addToStringPool(varName); emitWithToken(Opcodes.LOAD_GLOB, node.getIndex()); diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java index 32e8fae78..f02dec02d 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java @@ -183,7 +183,7 @@ private static boolean handleLocalListAssignment(BytecodeCompiler bc, BinaryOper } bc.compileNode(node.right, -1, rhsContext); int valueReg = bc.lastResultReg; - String globalVarName = bc.getCurrentPackage() + "::" + idNode.name; + String globalVarName = NameNormalizer.normalizeVariableName(idNode.name, bc.getCurrentPackage()); int nameIdx = bc.addToStringPool(globalVarName); int localReg = bc.allocateRegister(); bc.emitWithToken(Opcodes.LOCAL_SCALAR, node.getIndex()); @@ -207,7 +207,7 @@ private static boolean handleLocalListAssignment(BytecodeCompiler bc, BinaryOper bc.throwCompilerException("Can't localize lexical variable " + varName); return true; } - String globalVarName = bc.getCurrentPackage() + "::" + idNode.name; + String globalVarName = NameNormalizer.normalizeVariableName(idNode.name, bc.getCurrentPackage()); int nameIdx = bc.addToStringPool(globalVarName); int localReg = bc.allocateRegister(); bc.emitWithToken(Opcodes.LOCAL_SCALAR, node.getIndex());