diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 3533fd10f..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<>(); @@ -472,9 +473,29 @@ 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; + + // 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 @@ -490,17 +511,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; @@ -697,6 +707,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. @@ -711,6 +741,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; @@ -727,8 +776,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; } @@ -737,6 +786,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(); @@ -749,6 +822,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 +1324,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 +1399,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); @@ -3902,9 +3990,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(); @@ -3923,11 +4015,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 21bc64746..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()); @@ -2052,6 +2056,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; @@ -2386,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..74ea953bc 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 @@ -263,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; diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index c419ee711..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,10 +845,27 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode // Allocate register for result int rd = bytecodeCompiler.allocateRegister(); - // Emit direct opcode EVAL_STRING + // 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 { @@ -883,14 +910,27 @@ 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 + // 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 13db3320e..169cc5305 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java @@ -45,11 +45,13 @@ 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, + String evalTag) { try { // Step 1: Clear $@ at start of eval GlobalVariable.getGlobalVariable("main::@").set(""); @@ -58,31 +60,18 @@ public static RuntimeScalar 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()); - } - - // 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"; - symbolTable.setCurrentPackage(compilePackage, false); + ScopedSymbolTable symbolTable = (callSiteScope != null) + ? callSiteScope.snapShot() + : new ScopedSymbolTable(); ErrorMessageUtil errorUtil = new ErrorMessageUtil(sourceName, tokens); EmitterContext ctx = new EmitterContext( @@ -90,7 +79,7 @@ public static RuntimeScalar evalString(String perlCode, symbolTable, null, // mv null, // cw - RuntimeContextType.SCALAR, + callContext, false, // isBoxed errorUtil, opts, @@ -164,15 +153,14 @@ public static RuntimeScalar 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()); @@ -197,15 +185,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..5d1287daf 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java @@ -1236,7 +1236,12 @@ 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++]; + int evalTagIdx = bytecode[pc++]; + sb.append("EVAL_STRING r").append(rd) + .append(" = eval(r").append(rs) + .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 09eb9aac0..1e40adc87 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java @@ -240,6 +240,9 @@ 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]; @@ -253,15 +256,19 @@ 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, + evalTag // Compile-time evalTag — EvalStringHandler retrieves scope via RuntimeCode.evalContext ); - registers[rd] = result; + registers[rd] = (callContext == RuntimeContextType.SCALAR) + ? result.scalar() + : result; return pc; } 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..d5dda446c 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -842,7 +842,9 @@ public static RuntimeList evalStringWithInterpreter( 1, evalCtx.errorUtil, adjustedRegistry); - compiler.setCompilePackage(capturedSymbolTable.getCurrentPackage()); + // 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());