From f23c2308fc303fe3b21a6e382291d8193721ce8f Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Tue, 24 Feb 2026 20:12:47 +0100 Subject: [PATCH 1/7] Fix interpreter eval: global compound assign and scoped package blocks --- .../backend/bytecode/BytecodeCompiler.java | 39 ++++++++++++++++++- .../backend/bytecode/BytecodeInterpreter.java | 10 +++++ .../backend/bytecode/EvalStringHandler.java | 17 +++++--- .../frontend/parser/StatementParser.java | 4 ++ .../runtime/runtimetypes/RuntimeCode.java | 9 ++++- 5 files changed, 70 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 3533fd10f..a84f95c24 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -697,6 +697,26 @@ public void visit(BlockNode node) { outerResultReg = allocateRegister(); } + // Detect scoped package/class blocks: parseOptionalPackageBlock inserts the + // package/class OperatorNode as the first element and marks it with isScoped=true. + // The package node itself emits PUSH_PACKAGE; we emit POP_PACKAGE here so normal + // fallthrough restores the previous package. + boolean isScopedPackageBlock = !node.elements.isEmpty() + && node.elements.getFirst() instanceof OperatorNode pkgOp + && (pkgOp.operator.equals("package") || pkgOp.operator.equals("class")) + && Boolean.TRUE.equals(pkgOp.getAnnotation("isScoped")); + + // Scoped package/class blocks change the compiler's symbolTable current package when + // compiling the leading package/class OperatorNode. Restore it after the block so + // subsequent name resolution (notably eval STRING compilation) uses the correct + // call-site package. + String savedCompilePackage = null; + boolean savedCompilePackageIsClass = false; + if (isScopedPackageBlock) { + savedCompilePackage = symbolTable.getCurrentPackage(); + savedCompilePackageIsClass = symbolTable.currentPackageIsClass(); + } + // Detect the BlockNode([local $_, For1Node(needsArrayOfAlias)]) pattern produced // by the parser for implicit-$_ foreach loops. For1Node emits LOCAL_SCALAR_SAVE_LEVEL // itself, so the 'local $_' child must be skipped here to avoid double-emission. @@ -749,6 +769,14 @@ public void visit(BlockNode node) { emitReg(lastResultReg); } + // Restore previous package for scoped package/class blocks. + if (isScopedPackageBlock) { + emit(Opcodes.POP_PACKAGE); + + // Restore compile-time package tracking. + symbolTable.setCurrentPackage(savedCompilePackage, savedCompilePackageIsClass); + } + // Exit scope restores register state exitScope(); @@ -1243,7 +1271,12 @@ void handleCompoundAssignment(BinaryOperatorNode node) { // Global variable - need to load it first isGlobal = true; targetReg = allocateRegister(); - String normalizedName = NameNormalizer.normalizeVariableName(varName, getCurrentPackage()); + // NameNormalizer expects a variable name WITHOUT the sigil. + // Using "$main::x" here would create a different global key than + // regular assignments (which normalize "main::x"), so compound + // assigns would update the wrong global. + String bareVarName = varName.substring(1); + String normalizedName = NameNormalizer.normalizeVariableName(bareVarName, getCurrentPackage()); int nameIdx = addToStringPool(normalizedName); emit(Opcodes.LOAD_GLOBAL_SCALAR); emitReg(targetReg); @@ -1313,7 +1346,9 @@ void handleCompoundAssignment(BinaryOperatorNode node) { throwCompilerException("Global symbol \"" + varName + "\" requires explicit package name"); } - String normalizedName = NameNormalizer.normalizeVariableName(varName, getCurrentPackage()); + // NameNormalizer expects a variable name WITHOUT the sigil. + String bareVarName = varName.substring(1); + String normalizedName = NameNormalizer.normalizeVariableName(bareVarName, getCurrentPackage()); int nameIdx = addToStringPool(normalizedName); emit(Opcodes.STORE_GLOBAL_SCALAR); emit(nameIdx); diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 21bc64746..2ccef2f83 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -2052,6 +2052,16 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c break; } + case Opcodes.POP_PACKAGE: { + // Scoped package block exit: closing } of package Foo { ... } + // Restore the previous package by popping exactly one saved dynamic state. + int level = DynamicVariableManager.getLocalLevel(); + if (level > 0) { + DynamicVariableManager.popToLocalLevel(level - 1); + } + break; + } + default: // Unknown opcode int opcodeInt = opcode; diff --git a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java index 13db3320e..3cf16cce4 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java @@ -76,12 +76,17 @@ public static RuntimeScalar evalString(String perlCode, symbolTable.warningFlagsStack.push((java.util.BitSet) currentCode.warningFlags.clone()); } - // Inherit the compile-time package from the calling code, matching what - // evalStringHelper (JVM path) does via capturedSymbolTable.snapShot(). - // Using the compile-time package (not InterpreterState.currentPackage which is - // the runtime package) ensures bare names like *named resolve to FOO3::named - // when the eval call site is inside "package FOO3". - String compilePackage = (currentCode != null) ? currentCode.compilePackage : "main"; + // eval STRING compiles in the current *runtime* package. + // This can change dynamically via scoped package blocks (package Foo { ... }). + // Using the runtime package here ensures nested eval("__PACKAGE__") matches + // the JVM compiler behavior. + String compilePackage; + RuntimeScalar runtimePkg = InterpreterState.currentPackage.get(); + if (runtimePkg != null && runtimePkg.getDefinedBoolean()) { + compilePackage = runtimePkg.toString(); + } else { + compilePackage = (currentCode != null) ? currentCode.compilePackage : "main"; + } symbolTable.setCurrentPackage(compilePackage, false); ErrorMessageUtil errorUtil = new ErrorMessageUtil(sourceName, tokens); diff --git a/src/main/java/org/perlonjava/frontend/parser/StatementParser.java b/src/main/java/org/perlonjava/frontend/parser/StatementParser.java index d9032a951..b443f6bb4 100644 --- a/src/main/java/org/perlonjava/frontend/parser/StatementParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/StatementParser.java @@ -879,6 +879,10 @@ public static BlockNode parseOptionalPackageBlock(Parser parser, IdentifierNode parser.isInClassBlock = wasInClassBlock; } + // Mark the package/class declaration as scoped so backends can restore + // the previous package when the block exits. + packageNode.setAnnotation("isScoped", true); + // Insert packageNode as first statement in block block.elements.addFirst(packageNode); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index 51d6e7d88..032b9d969 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -842,7 +842,14 @@ public static RuntimeList evalStringWithInterpreter( 1, evalCtx.errorUtil, adjustedRegistry); - compiler.setCompilePackage(capturedSymbolTable.getCurrentPackage()); + // eval STRING runs in the *current runtime package*, which can change + // dynamically via package Foo { } blocks. Use the runtime package here so + // nested eval("__PACKAGE__") matches the JVM compiler behavior. + String runtimePackage = RuntimeCode.getCurrentPackage(); + if (runtimePackage.endsWith("::")) { + runtimePackage = runtimePackage.substring(0, runtimePackage.length() - 2); + } + compiler.setCompilePackage(runtimePackage); interpretedCode = compiler.compile(ast, evalCtx); if (DISASSEMBLE) { System.out.println(interpretedCode.disassemble()); From 331452326d10c8daa3ada7755f79fdcf007a08f4 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Tue, 24 Feb 2026 21:29:48 +0100 Subject: [PATCH 2/7] Align eval string context handling --- .../backend/bytecode/BytecodeCompiler.java | 16 ++++++++++---- .../backend/bytecode/BytecodeInterpreter.java | 8 +++++++ .../backend/bytecode/CompileAssignment.java | 15 +++++++++++++ .../backend/bytecode/CompileOperator.java | 8 +++++-- .../backend/bytecode/EvalStringHandler.java | 22 +++++++++---------- .../backend/bytecode/InterpretedCode.java | 5 ++++- .../backend/bytecode/SlowOpcodeHandler.java | 10 ++++++--- 7 files changed, 63 insertions(+), 21 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index a84f95c24..581fb1cf5 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -3937,9 +3937,13 @@ public void visit(ListNode node) { // List elements should be evaluated in LIST context if (node.elements.size() == 1) { int savedContext = currentCallContext; - currentCallContext = RuntimeContextType.LIST; try { - node.elements.get(0).accept(this); + Node element = node.elements.get(0); + String argContext = (String) element.getAnnotation("context"); + currentCallContext = (argContext != null && argContext.equals("SCALAR")) + ? RuntimeContextType.SCALAR + : RuntimeContextType.LIST; + element.accept(this); int elemReg = lastResultReg; int listReg = allocateRegister(); @@ -3958,11 +3962,15 @@ public void visit(ListNode node) { // Evaluate each element into a register // List elements should be evaluated in LIST context int savedContext = currentCallContext; - currentCallContext = RuntimeContextType.LIST; try { int[] elementRegs = new int[node.elements.size()]; for (int i = 0; i < node.elements.size(); i++) { - node.elements.get(i).accept(this); + Node element = node.elements.get(i); + String argContext = (String) element.getAnnotation("context"); + currentCallContext = (argContext != null && argContext.equals("SCALAR")) + ? RuntimeContextType.SCALAR + : RuntimeContextType.LIST; + element.accept(this); elementRegs[i] = lastResultReg; } diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 2ccef2f83..c4e033e21 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -794,6 +794,10 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c int rd = bytecode[pc++]; int operandReg = bytecode[pc++]; RuntimeBase operand = registers[operandReg]; + if (operand == null) { + registers[rd] = RuntimeScalarCache.scalarUndef; + break; + } if (operand instanceof RuntimeList) { // For RuntimeList in list assignment context, return the count registers[rd] = new RuntimeScalar(((RuntimeList) operand).size()); @@ -2396,6 +2400,10 @@ private static int executeCollections(int opcode, int[] bytecode, int pc, int rd = bytecode[pc++]; int operandReg = bytecode[pc++]; RuntimeBase operand = registers[operandReg]; + if (operand == null) { + registers[rd] = RuntimeScalarCache.scalarUndef; + return pc; + } if (operand instanceof RuntimeList) { registers[rd] = new RuntimeScalar(((RuntimeList) operand).size()); } else { diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java index 372da7562..c3c833519 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java @@ -1,7 +1,9 @@ package org.perlonjava.backend.bytecode; +import org.perlonjava.frontend.analysis.LValueVisitor; import org.perlonjava.frontend.astnode.*; import org.perlonjava.runtime.runtimetypes.NameNormalizer; +import org.perlonjava.runtime.runtimetypes.PerlCompilerException; import org.perlonjava.runtime.runtimetypes.RuntimeContextType; import java.util.ArrayList; @@ -14,6 +16,19 @@ public class CompileAssignment { * Handles all forms of assignment including my/our/local, scalars, arrays, hashes, and slices. */ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, BinaryOperatorNode node) { + // Ensure compile-time lvalue checks match JVM behavior. + // In particular, this detects invalid lvalues like: + // ($a ? $x : ($y)) = 5 + // which must throw "Assignment to both a list and a scalar". + try { + LValueVisitor.getContext(node.left); + } catch (PerlCompilerException e) { + throw e; + } catch (RuntimeException e) { + // LValueVisitor may throw other runtime exceptions; preserve message as a compile error. + throw new PerlCompilerException(node.getIndex(), e.getMessage(), bytecodeCompiler.errorUtil, e); + } + // Determine the calling context for the RHS based on LHS type int rhsContext = RuntimeContextType.LIST; // Default diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index c419ee711..53d90f2c4 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -835,10 +835,11 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode // Allocate register for result int rd = bytecodeCompiler.allocateRegister(); - // Emit direct opcode EVAL_STRING + // Emit direct opcode EVAL_STRING with calling context bytecodeCompiler.emitWithToken(Opcodes.EVAL_STRING, node.getIndex()); bytecodeCompiler.emitReg(rd); bytecodeCompiler.emitReg(stringReg); + bytecodeCompiler.emit(bytecodeCompiler.currentCallContext); bytecodeCompiler.lastResultReg = rd; } else { @@ -889,8 +890,11 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode bytecodeCompiler.lastResultReg = undefReg; } else if (op.equals("unaryMinus")) { // Unary minus: -$x - // Compile operand + // Compile operand in scalar context + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; node.operand.accept(bytecodeCompiler); + bytecodeCompiler.currentCallContext = savedContext; int operandReg = bytecodeCompiler.lastResultReg; // Allocate result register diff --git a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java index 3cf16cce4..6b459f2b3 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java @@ -45,11 +45,12 @@ public class EvalStringHandler { * @param sourceLine Source line for error messages * @return RuntimeScalar result of evaluation (undef on error) */ - public static RuntimeScalar evalString(String perlCode, - InterpretedCode currentCode, - RuntimeBase[] registers, - String sourceName, - int sourceLine) { + public static RuntimeList evalString(String perlCode, + InterpretedCode currentCode, + RuntimeBase[] registers, + String sourceName, + int sourceLine, + int callContext) { try { // Step 1: Clear $@ at start of eval GlobalVariable.getGlobalVariable("main::@").set(""); @@ -95,7 +96,7 @@ public static RuntimeScalar evalString(String perlCode, symbolTable, null, // mv null, // cw - RuntimeContextType.SCALAR, + callContext, false, // isBoxed errorUtil, opts, @@ -202,15 +203,14 @@ public static RuntimeScalar evalString(String perlCode, // Step 6: Execute the compiled code RuntimeArray args = new RuntimeArray(); // Empty @_ - RuntimeList result = evalCode.apply(args, RuntimeContextType.SCALAR); - - // Step 7: Return scalar result - return result.scalar(); + return evalCode.apply(args, callContext); } catch (Exception e) { // Step 8: Handle errors - set $@ and return undef WarnDie.catchEval(e); - return RuntimeScalarCache.scalarUndef; + return (callContext == RuntimeContextType.LIST) + ? new RuntimeList() + : new RuntimeList(RuntimeScalarCache.scalarUndef); } } diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java index d3d5499f0..3f2a457ad 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java @@ -1236,7 +1236,10 @@ public String disassemble() { case Opcodes.EVAL_STRING: rd = bytecode[pc++]; rs = bytecode[pc++]; - sb.append("EVAL_STRING r").append(rd).append(" = eval(r").append(rs).append(")\n"); + int evalCtx = bytecode[pc++]; + sb.append("EVAL_STRING r").append(rd) + .append(" = eval(r").append(rs) + .append(", ctx=").append(evalCtx).append(")\n"); break; case Opcodes.SELECT_OP: rd = bytecode[pc++]; diff --git a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java index 09eb9aac0..f64ad802a 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java @@ -240,6 +240,7 @@ public static int executeEvalString( int rd = bytecode[pc++]; int stringReg = bytecode[pc++]; + int callContext = bytecode[pc++]; // Get the code string - handle both RuntimeScalar and RuntimeList (from string interpolation) RuntimeBase codeValue = registers[stringReg]; @@ -253,15 +254,18 @@ public static int executeEvalString( String perlCode = codeScalar.toString(); // Call EvalStringHandler to parse, compile, and execute - RuntimeScalar result = EvalStringHandler.evalString( + RuntimeList result = EvalStringHandler.evalString( perlCode, code, // Current InterpretedCode for context registers, // Current registers for variable access code.sourceName, - code.sourceLine + code.sourceLine, + callContext ); - registers[rd] = result; + registers[rd] = (callContext == RuntimeContextType.SCALAR) + ? result.scalar() + : result; return pc; } From d592f8821decf5be1a59fa85f735fc4fc0319b61 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Tue, 24 Feb 2026 22:11:15 +0100 Subject: [PATCH 3/7] Fix BytecodeCompiler: remove unconditional scalar conversion for eval STRING results --- .../perlonjava/backend/bytecode/BytecodeCompiler.java | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 581fb1cf5..06cb2455d 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -490,17 +490,6 @@ public InterpretedCode compile(Node node, EmitterContext ctx) { // Visit the node to generate bytecode node.accept(this); - // Convert result to scalar context if needed (for eval STRING) - if (currentCallContext == RuntimeContextType.SCALAR && lastResultReg >= 0) { - RuntimeBase lastResult = null; // Can't access at compile time - // Use ARRAY_SIZE to convert arrays/lists to scalar count - int scalarReg = allocateRegister(); - emit(Opcodes.ARRAY_SIZE); - emitReg(scalarReg); - emitReg(lastResultReg); - lastResultReg = scalarReg; - } - // Emit RETURN with last result register // If no result was produced, return undef instead of register 0 ("this") int returnReg; From b2b50f0e8f6ea83548ef64f783268b0da2087d2a Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Tue, 24 Feb 2026 22:28:42 +0100 Subject: [PATCH 4/7] EvalStringHandler: use RuntimeCode.getCurrentPackage() matching JVM compiler --- .../backend/bytecode/CompileOperator.java | 3 ++- .../backend/bytecode/EvalStringHandler.java | 18 +++++++----------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index 53d90f2c4..63d9fd272 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -835,7 +835,8 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode // Allocate register for result int rd = bytecodeCompiler.allocateRegister(); - // Emit direct opcode EVAL_STRING with calling context + // Emit EVAL_STRING with calling context. + // Package is determined at runtime via RuntimeCode.getCurrentPackage(). bytecodeCompiler.emitWithToken(Opcodes.EVAL_STRING, node.getIndex()); bytecodeCompiler.emitReg(rd); bytecodeCompiler.emitReg(stringReg); diff --git a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java index 6b459f2b3..16a121469 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java @@ -77,18 +77,14 @@ public static RuntimeList evalString(String perlCode, symbolTable.warningFlagsStack.push((java.util.BitSet) currentCode.warningFlags.clone()); } - // eval STRING compiles in the current *runtime* package. - // This can change dynamically via scoped package blocks (package Foo { ... }). - // Using the runtime package here ensures nested eval("__PACKAGE__") matches - // the JVM compiler behavior. - String compilePackage; - RuntimeScalar runtimePkg = InterpreterState.currentPackage.get(); - if (runtimePkg != null && runtimePkg.getDefinedBoolean()) { - compilePackage = runtimePkg.toString(); - } else { - compilePackage = (currentCode != null) ? currentCode.compilePackage : "main"; + // Use the current runtime package, exactly as RuntimeCode.evalStringWithInterpreter does. + // RuntimeCode.getCurrentPackage() uses caller() to get the runtime package, + // which correctly reflects dynamic package changes (package Foo { } blocks). + String runtimePackage = RuntimeCode.getCurrentPackage(); + if (runtimePackage.endsWith("::")) { + runtimePackage = runtimePackage.substring(0, runtimePackage.length() - 2); } - symbolTable.setCurrentPackage(compilePackage, false); + symbolTable.setCurrentPackage(runtimePackage, false); ErrorMessageUtil errorUtil = new ErrorMessageUtil(sourceName, tokens); EmitterContext ctx = new EmitterContext( From bbe08b1f851dc851daf28a147d827d1a91a7425a Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Wed, 25 Feb 2026 09:37:09 +0100 Subject: [PATCH 5/7] BytecodeCompiler: fix eval STRING return value when END/special blocks are present When JPERL_EVAL_USE_INTERPRETER=1, eval STRING was returning undef instead of the last expression's value in two cases: 1. eval '1; END { }' returned undef because OperatorNode("undef") placeholder for the END block overwrote lastResultReg after the real last expression. 2. INIT { eval 'END { }; 1' or die } returned undef because the eval body was compiled in VOID context, discarding the result register entirely. Fixes: - CompileOperator: OperatorNode("undef") with no operand in VOID context is a no-op, not LOAD_UNDEF (END/BEGIN/INIT/CHECK/UNITCHECK placeholders) - BytecodeCompiler.visit(BlockNode): pre-scan to find last non-placeholder statement; freeze lastResultReg after visiting it so trailing placeholders cannot clobber it - BytecodeCompiler.compile(): eval STRING body uses at least SCALAR context so the result register is always allocated (never VOID) --- .../backend/bytecode/BytecodeCompiler.java | 58 +++++++++++++++++-- .../backend/bytecode/CompileOperator.java | 18 ++++-- 2 files changed, 67 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 06cb2455d..f193b77ae 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -472,9 +472,14 @@ public InterpretedCode compile(Node node, EmitterContext ctx) { // Detect closure variables if context is provided if (ctx != null) { detectClosureVariables(node, ctx); - // Use the calling context from EmitterContext for top-level expressions - // This is crucial for eval STRING to propagate context correctly - currentCallContext = ctx.contextType; + // Use the calling context from EmitterContext for top-level expressions. + // Exception: eval STRING body always produces the value of its last expression, + // even when the caller uses it in void context. Compiling the body in VOID + // context would discard the result (e.g. `INIT { eval '1' or die }` would + // fail because eval returns undef). Use SCALAR as the minimum context. + currentCallContext = (ctx.contextType == RuntimeContextType.VOID) + ? RuntimeContextType.SCALAR + : ctx.contextType; } // If we have captured variables, allocate registers for them @@ -720,6 +725,25 @@ public void visit(BlockNode node) { // Visit each statement in the block int numStatements = node.elements.size(); + + // Pre-scan to find the last value-producing statement index. + // Special blocks (END/BEGIN/INIT/CHECK/UNITCHECK) parse to OperatorNode("undef") + // and produce no return value. They must not be treated as the last statement + // for return-value purposes (e.g. `eval '1; END { }'` must return 1, not undef). + int lastValueProducingIndex = numStatements - 1; + for (int i = numStatements - 1; i >= 0; i--) { + Node stmt = node.elements.get(i); + // OperatorNode("undef") with no operand is how the parser represents special + // blocks that have already been executed (BEGIN/END/INIT/CHECK/UNITCHECK). + boolean isSpecialBlockPlaceholder = stmt instanceof OperatorNode op + && "undef".equals(op.operator) + && op.operand == null; + if (!isSpecialBlockPlaceholder) { + lastValueProducingIndex = i; + break; + } + } + for (int i = 0; i < numStatements; i++) { // Skip the 'local $_' child when For1Node handles it via LOCAL_SCALAR_SAVE_LEVEL if (i == 0 && skipFirstChild) continue; @@ -736,8 +760,8 @@ public void visit(BlockNode node) { int savedContext = currentCallContext; // If this is not an assignment or other value-using construct, use VOID context - // EXCEPT for the last statement in a block, which should use the block's context - boolean isLastStatement = (i == numStatements - 1); + // EXCEPT for the last value-producing statement in a block, which uses the block's context + boolean isLastStatement = (i == lastValueProducingIndex); if (!isLastStatement && !(stmt instanceof BinaryOperatorNode && ((BinaryOperatorNode) stmt).operator.equals("="))) { currentCallContext = RuntimeContextType.VOID; } @@ -746,6 +770,30 @@ public void visit(BlockNode node) { currentCallContext = savedContext; + // After the last value-producing statement, preserve its lastResultReg. + // Subsequent void-context statements (e.g. END/BEGIN placeholders) must + // not overwrite it, even if they set lastResultReg = -1. + if (isLastStatement) { + // Freeze the result here; nothing after this should change it + int frozenResult = lastResultReg; + // Recycle temporaries, then restore + recycleTemporaryRegisters(); + lastResultReg = frozenResult; + // Continue visiting remaining statements (e.g. END placeholders) as void + for (int j = i + 1; j < numStatements; j++) { + Node trailing = node.elements.get(j); + if (trailing == null) continue; + int savedCtx2 = currentCallContext; + currentCallContext = RuntimeContextType.VOID; + trailing.accept(this); + currentCallContext = savedCtx2; + recycleTemporaryRegisters(); + } + // Restore frozen result and break out of outer loop + lastResultReg = frozenResult; + break; + } + // Recycle temporary registers after each statement // enterScope() protects registers allocated before entering a scope recycleTemporaryRegisters(); diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index 63d9fd272..8048073be 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -885,10 +885,20 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode // undef operator - returns undefined value // Can be used standalone: undef // Or with an operand to undef a variable: undef $x (not implemented yet) - int undefReg = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.LOAD_UNDEF); - bytecodeCompiler.emitReg(undefReg); - bytecodeCompiler.lastResultReg = undefReg; + // + // Special case: in VOID context with no operand, this is a no-op placeholder + // emitted by the parser for END/BEGIN/INIT/CHECK/UNITCHECK blocks that have + // already been executed. Emitting LOAD_UNDEF here would overwrite the previous + // statement's result (e.g. `eval '1; END { }'` must return 1, not undef). + if (node.operand == null + && bytecodeCompiler.currentCallContext == RuntimeContextType.VOID) { + bytecodeCompiler.lastResultReg = -1; + } else { + int undefReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_UNDEF); + bytecodeCompiler.emitReg(undefReg); + bytecodeCompiler.lastResultReg = undefReg; + } } else if (op.equals("unaryMinus")) { // Unary minus: -$x // Compile operand in scalar context From fcd12ed16cf72d6b0c14b7dd31ba6dd5505cf021 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Wed, 25 Feb 2026 10:07:23 +0100 Subject: [PATCH 6/7] BytecodeCompiler: fix eval STRING package resolution via compile-time scope snapshot When JPERL_EVAL_USE_INTERPRETER=1, bare names inside eval STRING (e.g. *named{CODE}) were resolving to main:: instead of the correct package at the eval call site (e.g. FOO3::named inside package FOO3 { }). Root cause: BytecodeCompiler.symbolTable defaulted to 'main' and was not being updated with the compile-time package/pragma state. Fix mirrors the JVM compiler's approach exactly: - CompileOperator: at EVAL_STRING emit time, snapshot the scope manager (BytecodeCompiler.symbolTable.snapShot()) and store it as an EmitterContext in RuntimeCode.evalContext under a unique evalTag, exactly as EmitEval/evalTag does for JVM-compiled eval STRING. The evalTag is baked into the EVAL_STRING opcode (4th field). - SlowOpcodeHandler: read evalTag from opcode, pass to EvalStringHandler. - EvalStringHandler: retrieve saved EmitterContext from evalContext, use its symbolTable snapshot directly as the compile-time scope. - BytecodeCompiler.compile(): sync package+pragmas from ctx.symbolTable so BytecodeCompiler resolves bare names in the eval body correctly. - RuntimeCode.evalStringWithInterpreter: use capturedSymbolTable (compile-time scope from EmitEval) instead of RuntimeCode.getCurrentPackage() which uses caller() and cannot see scoped package blocks. Results: op/stash.t, comp/package_block.t, comp/parser.t now all show identical pass counts between JVM and interpreter modes. --- .../backend/bytecode/BytecodeCompiler.java | 18 +++++++- .../backend/bytecode/CompileOperator.java | 29 ++++++++++++- .../backend/bytecode/EvalStringHandler.java | 42 +++++++------------ .../backend/bytecode/InterpretedCode.java | 4 +- .../backend/bytecode/SlowOpcodeHandler.java | 5 ++- .../runtime/runtimetypes/RuntimeCode.java | 11 ++--- 6 files changed, 68 insertions(+), 41 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index f193b77ae..3469f845d 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -37,7 +37,8 @@ public class BytecodeCompiler implements Visitor { // Symbol table for package/class tracking // Tracks current package, class flag, and package versions like the compiler does - final ScopedSymbolTable symbolTable = new ScopedSymbolTable(); + // Initialized to a fresh table; overwritten in compile() from ctx.symbolTable when available. + ScopedSymbolTable symbolTable = new ScopedSymbolTable(); // Stack to save/restore register state when entering/exiting scopes private final Stack savedNextRegister = new Stack<>(); @@ -480,6 +481,21 @@ public InterpretedCode compile(Node node, EmitterContext ctx) { currentCallContext = (ctx.contextType == RuntimeContextType.VOID) ? RuntimeContextType.SCALAR : ctx.contextType; + + // Sync package, pragmas, warnings, and features from ctx.symbolTable. + // ctx.symbolTable is the compile-time scope snapshot at the eval call site — + // it has the correct package (e.g. FOO3) and pragma state. + if (ctx.symbolTable != null) { + symbolTable.setCurrentPackage( + ctx.symbolTable.getCurrentPackage(), + ctx.symbolTable.currentPackageIsClass()); + symbolTable.strictOptionsStack.pop(); + symbolTable.strictOptionsStack.push(ctx.symbolTable.strictOptionsStack.peek()); + symbolTable.featureFlagsStack.pop(); + symbolTable.featureFlagsStack.push(ctx.symbolTable.featureFlagsStack.peek()); + symbolTable.warningFlagsStack.pop(); + symbolTable.warningFlagsStack.push((java.util.BitSet) ctx.symbolTable.warningFlagsStack.peek().clone()); + } } // If we have captured variables, allocate registers for them diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index 8048073be..5cb24fcc6 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -1,16 +1,26 @@ package org.perlonjava.backend.bytecode; import org.perlonjava.frontend.astnode.*; +import org.perlonjava.backend.jvm.EmitterContext; +import org.perlonjava.backend.jvm.JavaClassInfo; import org.perlonjava.runtime.runtimetypes.ClassRegistry; import org.perlonjava.runtime.runtimetypes.GlobalVariable; import org.perlonjava.runtime.runtimetypes.RuntimeScalar; import org.perlonjava.runtime.runtimetypes.NameNormalizer; import org.perlonjava.runtime.runtimetypes.RuntimeContextType; +import org.perlonjava.runtime.runtimetypes.RuntimeCode; +import org.perlonjava.app.cli.CompilerOptions; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + + public class CompileOperator { + /** Counter for generating unique eval tags, matching JVM compiler's evalTag scheme. */ + private static final AtomicInteger EVAL_TAG_COUNTER = new AtomicInteger(0); + public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode node) { // Track token index for error reporting bytecodeCompiler.currentTokenIndex = node.getIndex(); @@ -835,12 +845,27 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode // Allocate register for result int rd = bytecodeCompiler.allocateRegister(); - // Emit EVAL_STRING with calling context. - // Package is determined at runtime via RuntimeCode.getCurrentPackage(). + // Store a compile-time EmitterContext snapshot in RuntimeCode.evalContext, + // exactly as the JVM compiler does via EmitEval/evalTag. + // The evalTag is unique per eval site and baked into the string pool. + String evalTag = "interp_eval_" + EVAL_TAG_COUNTER.incrementAndGet(); + EmitterContext snapCtx = new EmitterContext( + new JavaClassInfo(), + bytecodeCompiler.symbolTable.snapShot(), // compile-time scope snapshot + null, null, + bytecodeCompiler.currentCallContext, + false, + null, + new CompilerOptions(), + null + ); + RuntimeCode.evalContext.put(evalTag, snapCtx); + int tagIdx = bytecodeCompiler.addToStringPool(evalTag); bytecodeCompiler.emitWithToken(Opcodes.EVAL_STRING, node.getIndex()); bytecodeCompiler.emitReg(rd); bytecodeCompiler.emitReg(stringReg); bytecodeCompiler.emit(bytecodeCompiler.currentCallContext); + bytecodeCompiler.emit(tagIdx); bytecodeCompiler.lastResultReg = rd; } else { diff --git a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java index 16a121469..169cc5305 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java @@ -50,7 +50,8 @@ public static RuntimeList evalString(String perlCode, RuntimeBase[] registers, String sourceName, int sourceLine, - int callContext) { + int callContext, + String evalTag) { try { // Step 1: Clear $@ at start of eval GlobalVariable.getGlobalVariable("main::@").set(""); @@ -59,32 +60,18 @@ public static RuntimeList evalString(String perlCode, Lexer lexer = new Lexer(perlCode); List tokens = lexer.tokenize(); - // Create minimal EmitterContext for parsing - // IMPORTANT: Inherit strict/feature/warning flags from parent scope - // This matches Perl's eval STRING semantics where eval inherits lexical pragmas + // Retrieve compile-time EmitterContext from RuntimeCode.evalContext — + // exactly as evalStringWithInterpreter does. The context was stored by + // CompileOperator when the EVAL_STRING opcode was emitted, capturing the + // exact scope manager state (package, pragmas, warnings) at that call site. + EmitterContext savedCtx = (evalTag != null) ? RuntimeCode.evalContext.get(evalTag) : null; + ScopedSymbolTable callSiteScope = (savedCtx != null) ? savedCtx.symbolTable : null; + CompilerOptions opts = new CompilerOptions(); opts.fileName = sourceName + " (eval)"; - ScopedSymbolTable symbolTable = new ScopedSymbolTable(); - - // Inherit lexical pragma flags from parent if available - if (currentCode != null) { - // Replace default values with parent's flags - symbolTable.strictOptionsStack.pop(); - symbolTable.strictOptionsStack.push(currentCode.strictOptions); - symbolTable.featureFlagsStack.pop(); - symbolTable.featureFlagsStack.push(currentCode.featureFlags); - symbolTable.warningFlagsStack.pop(); - symbolTable.warningFlagsStack.push((java.util.BitSet) currentCode.warningFlags.clone()); - } - - // Use the current runtime package, exactly as RuntimeCode.evalStringWithInterpreter does. - // RuntimeCode.getCurrentPackage() uses caller() to get the runtime package, - // which correctly reflects dynamic package changes (package Foo { } blocks). - String runtimePackage = RuntimeCode.getCurrentPackage(); - if (runtimePackage.endsWith("::")) { - runtimePackage = runtimePackage.substring(0, runtimePackage.length() - 2); - } - symbolTable.setCurrentPackage(runtimePackage, false); + ScopedSymbolTable symbolTable = (callSiteScope != null) + ? callSiteScope.snapShot() + : new ScopedSymbolTable(); ErrorMessageUtil errorUtil = new ErrorMessageUtil(sourceName, tokens); EmitterContext ctx = new EmitterContext( @@ -166,15 +153,14 @@ public static RuntimeList evalString(String perlCode, } // Step 4: Compile AST to interpreter bytecode with adjusted variable registry. - // The compile-time package is already propagated via ctx.symbolTable (set above - // from currentCode.compilePackage), so BytecodeCompiler will use it for name - // resolution (e.g. *named -> FOO3::named) without needing setCompilePackage(). BytecodeCompiler compiler = new BytecodeCompiler( sourceName + " (eval)", sourceLine, errorUtil, adjustedRegistry // Pass adjusted registry for variable capture ); + // BytecodeCompiler.compile() will snapshot ctx.symbolTable (which already + // has the correct package set above) — no need to call setCompilePackage(). InterpretedCode evalCode = compiler.compile(ast, ctx); // Pass ctx for context propagation if (RuntimeCode.DISASSEMBLE) { System.out.println(evalCode.disassemble()); diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java index 3f2a457ad..5d1287daf 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java @@ -1237,9 +1237,11 @@ public String disassemble() { rd = bytecode[pc++]; rs = bytecode[pc++]; int evalCtx = bytecode[pc++]; + int evalTagIdx = bytecode[pc++]; sb.append("EVAL_STRING r").append(rd) .append(" = eval(r").append(rs) - .append(", ctx=").append(evalCtx).append(")\n"); + .append(", ctx=").append(evalCtx) + .append(", tag=").append(stringPool[evalTagIdx]).append(")\n"); break; case Opcodes.SELECT_OP: rd = bytecode[pc++]; diff --git a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java index f64ad802a..1e40adc87 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java @@ -241,6 +241,8 @@ public static int executeEvalString( int rd = bytecode[pc++]; int stringReg = bytecode[pc++]; int callContext = bytecode[pc++]; + // Compile-time evalTag baked by CompileOperator — retrieve scope from RuntimeCode.evalContext + String evalTag = code.stringPool[bytecode[pc++]]; // Get the code string - handle both RuntimeScalar and RuntimeList (from string interpolation) RuntimeBase codeValue = registers[stringReg]; @@ -260,7 +262,8 @@ public static int executeEvalString( registers, // Current registers for variable access code.sourceName, code.sourceLine, - callContext + callContext, + evalTag // Compile-time evalTag — EvalStringHandler retrieves scope via RuntimeCode.evalContext ); registers[rd] = (callContext == RuntimeContextType.SCALAR) diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index 032b9d969..d5dda446c 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -842,14 +842,9 @@ public static RuntimeList evalStringWithInterpreter( 1, evalCtx.errorUtil, adjustedRegistry); - // eval STRING runs in the *current runtime package*, which can change - // dynamically via package Foo { } blocks. Use the runtime package here so - // nested eval("__PACKAGE__") matches the JVM compiler behavior. - String runtimePackage = RuntimeCode.getCurrentPackage(); - if (runtimePackage.endsWith("::")) { - runtimePackage = runtimePackage.substring(0, runtimePackage.length() - 2); - } - compiler.setCompilePackage(runtimePackage); + // BytecodeCompiler.compile() snapshots evalCtx.symbolTable (= capturedSymbolTable + // snapshot with correct compile-time package, pragmas, and flags) — no need + // to call setCompilePackage() separately. interpretedCode = compiler.compile(ast, evalCtx); if (DISASSEMBLE) { System.out.println(interpretedCode.disassemble()); From 16096b95cf96cbc30585960bb020247ba54f92ee Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Wed, 25 Feb 2026 10:07:50 +0100 Subject: [PATCH 7/7] Fix eval STRING package resolution via compile-time scope snapshot --- .../backend/bytecode/CompileAssignment.java | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java index c3c833519..74ea953bc 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java @@ -278,7 +278,41 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, OperatorNode sigilOp = (OperatorNode) element; String sigil = sigilOp.operator; - if (sigilOp.operand instanceof IdentifierNode) { + // Handle backslash-declared ref: my (\$f, $g) = (...) + // \$f means: declare $f as a lexical scalar, assign i-th RHS + // element to it via SET_SCALAR so any ref taken earlier stays valid. + if (sigil.equals("\\") && + sigilOp.operand instanceof OperatorNode varNode && + varNode.operator.equals("$") && + varNode.operand instanceof IdentifierNode idNode) { + String varName = "$" + idNode.name; + int varReg; + if (bytecodeCompiler.hasVariable(varName)) { + varReg = bytecodeCompiler.getVariableRegister(varName); + } else { + varReg = bytecodeCompiler.addVariable(varName, "my"); + bytecodeCompiler.emit(Opcodes.LOAD_UNDEF); + bytecodeCompiler.emitReg(varReg); + } + + int indexReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_INT); + bytecodeCompiler.emitReg(indexReg); + bytecodeCompiler.emitInt(i); + + int elemReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.ARRAY_GET); + bytecodeCompiler.emitReg(elemReg); + bytecodeCompiler.emitReg(rhsListReg); + bytecodeCompiler.emitReg(indexReg); + + // SET_SCALAR mutates varReg in place so any ref captured + // from LOAD_UNDEF above (via CREATE_REF in the declaration + // visitor) keeps pointing at the correct object. + bytecodeCompiler.emit(Opcodes.SET_SCALAR); + bytecodeCompiler.emitReg(varReg); + bytecodeCompiler.emitReg(elemReg); + } else if (sigilOp.operand instanceof IdentifierNode) { String varName = sigil + ((IdentifierNode) sigilOp.operand).name; int varReg;