From 1a7643888dcc5dd730cd4793fe1bf06ff65034dd Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Thu, 26 Feb 2026 14:27:10 +0100 Subject: [PATCH 01/10] Interpreter: track PC for caller() and honor #line in eval --- dev/custom_bytecode/SKILL.md | 34 --------- .../backend/bytecode/BytecodeInterpreter.java | 4 + .../backend/bytecode/InterpreterState.java | 21 ++++++ .../runtimetypes/ErrorMessageUtil.java | 75 +++++++++++++++++++ .../runtimetypes/ExceptionFormatter.java | 25 ++++++- 5 files changed, 122 insertions(+), 37 deletions(-) diff --git a/dev/custom_bytecode/SKILL.md b/dev/custom_bytecode/SKILL.md index 5dbc95944..8b470b147 100644 --- a/dev/custom_bytecode/SKILL.md +++ b/dev/custom_bytecode/SKILL.md @@ -105,42 +105,8 @@ Tool prints list of operators needing emit cases. Add between markers: // GENERATED_OPERATORS_END ``` -### Critical: LASTOP Management - -Tool reads `LASTOP` from Opcodes.java to determine starting opcode: - -```java -// In Opcodes.java -public static final short REDO = 220; - -// Last manually-assigned opcode (for tool reference) -private static final short LASTOP = 220; // ← UPDATE WHEN ADDING MANUAL OPCODES -``` - -**When adding manual opcodes:** -1. Add constant BEFORE generated section -2. Update `LASTOP = ` -3. Run tool - it starts at LASTOP + 1 - ### Gotchas -**1. Don't Edit Generated Sections** -- Between `// GENERATED_*_START` and `// GENERATED_*_END` -- Tool overwrites on regeneration -- Your changes will be lost! - -**2. LASTOP Drift** -```java -// WRONG: Forgot to update LASTOP -public static final short MY_NEW_OP = 221; -private static final short LASTOP = 220; // ← Still 220! - -// Tool starts at 221, collides with MY_NEW_OP! - -// RIGHT: Always update LASTOP -public static final short MY_NEW_OP = 221; -private static final short LASTOP = 221; // ← Updated! -``` **3. Import Path Conversion** - Tool auto-converts: `org/perlonjava/operators/...` → `org.perlonjava.operators....` diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index ef589e88b..cd123fc68 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -72,6 +72,10 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c try { // Main dispatch loop - JVM JIT optimizes switch to tableswitch (O(1) jump) while (pc < bytecode.length) { + // Update current PC for caller()/stack trace reporting. + // This allows ExceptionFormatter to map pc->tokenIndex->line using code.errorUtil, + // which also honors #line directives inside eval strings. + InterpreterState.setCurrentPc(pc); int opcode = bytecode[pc++]; switch (opcode) { diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpreterState.java b/src/main/java/org/perlonjava/backend/bytecode/InterpreterState.java index 72c6656d1..0103e306c 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpreterState.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpreterState.java @@ -18,6 +18,9 @@ public class InterpreterState { private static final ThreadLocal> frameStack = ThreadLocal.withInitial(ArrayDeque::new); + private static final ThreadLocal> pcStack = + ThreadLocal.withInitial(ArrayDeque::new); + /** * Thread-local RuntimeScalar holding the runtime current package name. * @@ -60,6 +63,7 @@ public InterpreterFrame(InterpretedCode code, String packageName, String subrout */ public static void push(InterpretedCode code, String packageName, String subroutineName) { frameStack.get().push(new InterpreterFrame(code, packageName, subroutineName)); + pcStack.get().push(0); } /** @@ -71,6 +75,19 @@ public static void pop() { if (!stack.isEmpty()) { stack.pop(); } + + Deque pcs = pcStack.get(); + if (!pcs.isEmpty()) { + pcs.pop(); + } + } + + public static void setCurrentPc(int pc) { + Deque pcs = pcStack.get(); + if (!pcs.isEmpty()) { + pcs.pop(); + pcs.push(pc); + } } /** @@ -93,4 +110,8 @@ public static InterpreterFrame current() { public static List getStack() { return new ArrayList<>(frameStack.get()); } + + public static List getPcStack() { + return new ArrayList<>(pcStack.get()); + } } \ No newline at end of file diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java index 485383d15..7025fe0b5 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java @@ -18,6 +18,9 @@ public class ErrorMessageUtil { private int tokenIndex; private int lastLineNumber; + public record SourceLocation(String fileName, int lineNumber) { + } + /** * Constructs an ErrorMessageUtil with the specified file name and list of tokens. * @@ -252,5 +255,77 @@ public int getLineNumberAccurate(int index) { } return lineNumber; } + + public SourceLocation getSourceLocationAccurate(int index) { + String currentFileName = fileName; + int lineNumber = 1; + + boolean atBeginningOfLine = true; + int i = 0; + while (i <= index && i < tokens.size()) { + LexerToken tok = tokens.get(i); + if (tok.type == LexerTokenType.EOF) { + break; + } + + if (tok.type == LexerTokenType.NEWLINE) { + lineNumber++; + atBeginningOfLine = true; + i++; + continue; + } + + if (atBeginningOfLine && tok.type == LexerTokenType.OPERATOR && tok.text.equals("#")) { + int j = i + 1; + while (j < tokens.size() && tokens.get(j).type == LexerTokenType.WHITESPACE) { + j++; + } + if (j < tokens.size() && tokens.get(j).type == LexerTokenType.IDENTIFIER && tokens.get(j).text.equals("line")) { + j++; + while (j < tokens.size() && tokens.get(j).type == LexerTokenType.WHITESPACE) { + j++; + } + if (j < tokens.size() && tokens.get(j).type == LexerTokenType.NUMBER) { + int directiveLine = -1; + try { + directiveLine = Integer.parseInt(tokens.get(j).text); + } catch (NumberFormatException e) { + directiveLine = -1; + } + j++; + while (j < tokens.size() && tokens.get(j).type == LexerTokenType.WHITESPACE) { + j++; + } + if (j < tokens.size() && tokens.get(j).type == LexerTokenType.OPERATOR && tokens.get(j).text.equals("\"")) { + j++; + StringBuilder filenameBuilder = new StringBuilder(); + while (j < tokens.size() && !(tokens.get(j).type == LexerTokenType.OPERATOR && tokens.get(j).text.equals("\""))) { + filenameBuilder.append(tokens.get(j).text); + j++; + } + if (j < tokens.size() && tokens.get(j).type == LexerTokenType.OPERATOR && tokens.get(j).text.equals("\"")) { + String directiveFile = filenameBuilder.toString(); + if (!directiveFile.isEmpty()) { + currentFileName = directiveFile; + } + } + } + + if (directiveLine >= 1) { + // The directive applies to the following line. + lineNumber = directiveLine - 1; + } + } + } + } + + if (tok.type != LexerTokenType.WHITESPACE) { + atBeginningOfLine = false; + } + i++; + } + + return new SourceLocation(currentFileName, lineNumber); + } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ExceptionFormatter.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ExceptionFormatter.java index d699a6170..a57c306eb 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ExceptionFormatter.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ExceptionFormatter.java @@ -55,6 +55,7 @@ private static ArrayList> formatThrowable(Throwable t) { // Each BytecodeInterpreter.execute() JVM frame corresponds to one Perl call // level; consuming them in order gives the correct nested call stack. var interpreterFrames = InterpreterState.getStack(); + var interpreterPcs = InterpreterState.getPcStack(); int interpreterFrameIndex = 0; for (var element : t.getStackTrace()) { @@ -86,6 +87,7 @@ private static ArrayList> formatThrowable(Throwable t) { String pkg = (interpreterFrameIndex == 0) ? InterpreterState.currentPackage.get().toString() : frame.packageName; + int currentInterpreterFrameIndex = interpreterFrameIndex; interpreterFrameIndex++; String subName = frame.subroutineName; @@ -95,11 +97,28 @@ private static ArrayList> formatThrowable(Throwable t) { var entry = new ArrayList(); entry.add(pkg); - entry.add(frame.code.sourceName); - entry.add(String.valueOf(frame.code.sourceLine)); + String filename = frame.code.sourceName; + String line = String.valueOf(frame.code.sourceLine); + if (currentInterpreterFrameIndex < interpreterPcs.size()) { + Integer tokenIndex = null; + int pc = interpreterPcs.get(currentInterpreterFrameIndex); + if (frame.code.pcToTokenIndex != null && !frame.code.pcToTokenIndex.isEmpty()) { + var entryPc = frame.code.pcToTokenIndex.floorEntry(pc); + if (entryPc != null) { + tokenIndex = entryPc.getValue(); + } + } + if (tokenIndex != null && frame.code.errorUtil != null) { + ErrorMessageUtil.SourceLocation loc = frame.code.errorUtil.getSourceLocationAccurate(tokenIndex); + filename = loc.fileName(); + line = String.valueOf(loc.lineNumber()); + } + } + entry.add(filename); + entry.add(line); entry.add(subName); stackTrace.add(entry); - lastFileName = frame.code.sourceName != null ? frame.code.sourceName : ""; + lastFileName = filename != null ? filename : ""; } } } else if (element.getClassName().contains("org.perlonjava.anon") || From 996d4eaed9c269e44aa61dfc949cefbacdf07613 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Thu, 26 Feb 2026 15:17:46 +0100 Subject: [PATCH 02/10] Fix eval interpreter context propagation and add eval tracing --- .../backend/bytecode/CompileOperator.java | 4 + .../backend/bytecode/EvalStringHandler.java | 78 ++++++++++++------- .../backend/bytecode/InterpreterState.java | 2 +- .../backend/bytecode/SlowOpcodeHandler.java | 66 +++++++++++++--- .../org/perlonjava/backend/jvm/EmitEval.java | 18 ++++- .../backend/jvm/EmitLogicalOperator.java | 10 +++ .../frontend/analysis/EmitterVisitor.java | 5 +- .../runtime/runtimetypes/RuntimeCode.java | 28 +++++++ src/main/perl/lib/B.pm | 30 +++++++ 9 files changed, 195 insertions(+), 46 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index 971e9dc1f..7daa3ae7a 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -840,6 +840,10 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode bytecodeCompiler.emitWithToken(Opcodes.EVAL_STRING, node.getIndex()); bytecodeCompiler.emitReg(rd); bytecodeCompiler.emitReg(stringReg); + // Encode the eval operator's own call context (VOID/SCALAR/LIST) so + // wantarray() inside the eval body and the eval return value follow + // the correct context even when the surrounding sub is VOID. + bytecodeCompiler.emit(bytecodeCompiler.currentCallContext); 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 5980ef615..6259c44b0 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java @@ -28,6 +28,15 @@ */ public class EvalStringHandler { + private static final boolean EVAL_TRACE = + System.getenv("JPERL_EVAL_TRACE") != null; + + private static void evalTrace(String msg) { + if (EVAL_TRACE) { + System.err.println("[eval-trace] " + msg); + } + } + /** * Evaluate a Perl string dynamically. * @@ -52,7 +61,18 @@ public static RuntimeScalar evalString(String perlCode, String sourceName, int sourceLine, int callContext) { + return evalStringList(perlCode, currentCode, registers, sourceName, sourceLine, callContext).scalar(); + } + + public static RuntimeList evalStringList(String perlCode, + InterpretedCode currentCode, + RuntimeBase[] registers, + String sourceName, + int sourceLine, + int callContext) { try { + evalTrace("EvalStringHandler enter ctx=" + callContext + " srcName=" + sourceName + + " srcLine=" + sourceLine + " codeLen=" + (perlCode != null ? perlCode.length() : -1)); // Step 1: Clear $@ at start of eval GlobalVariable.getGlobalVariable("main::@").set(""); @@ -85,17 +105,19 @@ public static RuntimeScalar evalString(String perlCode, String compilePackage = InterpreterState.currentPackage.get().toString(); symbolTable.setCurrentPackage(compilePackage, false); + evalTrace("EvalStringHandler compilePackage=" + compilePackage + " fileName=" + opts.fileName); + ErrorMessageUtil errorUtil = new ErrorMessageUtil(sourceName, tokens); EmitterContext ctx = new EmitterContext( - new JavaClassInfo(), - symbolTable, - null, // mv - null, // cw - RuntimeContextType.SCALAR, - false, // isBoxed - errorUtil, - opts, - null // unitcheckBlocks + new JavaClassInfo(), + symbolTable, + null, // mv + null, // cw + callContext, + false, // isBoxed + errorUtil, + opts, + null // unitcheckBlocks ); Parser parser = new Parser(ctx, tokens); @@ -110,7 +132,7 @@ public static RuntimeScalar evalString(String perlCode, // Sort parent variables by register index for consistent ordering List> sortedVars = new ArrayList<>( - currentCode.variableRegistry.entrySet() + currentCode.variableRegistry.entrySet() ); sortedVars.sort(Map.Entry.comparingByValue()); @@ -149,8 +171,8 @@ public static RuntimeScalar evalString(String perlCode, continue; } } else if (!(value instanceof RuntimeArray || - value instanceof RuntimeHash || - value instanceof RuntimeCode)) { + value instanceof RuntimeHash || + value instanceof RuntimeCode)) { // Skip this register - it contains an internal object continue; } @@ -165,23 +187,22 @@ 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(). + // The compile-time package is already propagated via ctx.symbolTable. BytecodeCompiler compiler = new BytecodeCompiler( - sourceName + " (eval)", - sourceLine, - errorUtil, - adjustedRegistry // Pass adjusted registry for variable capture + sourceName + " (eval)", + sourceLine, + errorUtil, + adjustedRegistry // Pass adjusted registry for variable capture ); InterpretedCode evalCode = compiler.compile(ast, ctx); // Pass ctx for context propagation + + evalTrace("EvalStringHandler compiled bytecodeLen=" + (evalCode != null ? evalCode.bytecode.length : -1) + + " src=" + (evalCode != null ? evalCode.sourceName : "null")); if (RuntimeCode.DISASSEMBLE) { System.out.println(evalCode.disassemble()); } // Step 4.5: Store source lines in debugger symbol table if $^P flags are set - // This implements Perl's eval source retention feature for debugging - // Generate eval filename and store lines in @{"_<(eval N)"} int debugFlags = GlobalVariable.getGlobalVariable(GlobalContext.encodeSpecialVar("P")).getInt(); if (debugFlags != 0) { String evalFilename = RuntimeCode.getNextEvalFilename(); @@ -197,18 +218,17 @@ public static RuntimeScalar evalString(String perlCode, } // Step 6: Execute the compiled code - // Use the true outer call context so wantarray() inside the eval body - // returns the correct value (undef for void, false for scalar, true for list). RuntimeArray args = new RuntimeArray(); // Empty @_ RuntimeList result = evalCode.apply(args, callContext); - - // Step 7: Return scalar result - return result.scalar(); - + evalTrace("EvalStringHandler exec ok ctx=" + callContext + + " resultScalar=" + (result != null ? result.scalar().toString() : "null") + + " resultBool=" + (result != null && result.scalar() != null ? result.scalar().getBoolean() : false) + + " $@=" + GlobalVariable.getGlobalVariable("main::@").toString()); + return result; } catch (Exception e) { - // Step 8: Handle errors - set $@ and return undef + evalTrace("EvalStringHandler exec exception ctx=" + callContext + " ex=" + e.getClass().getSimpleName() + " msg=" + e.getMessage()); WarnDie.catchEval(e); - return RuntimeScalarCache.scalarUndef; + return new RuntimeList(new RuntimeScalar()); } } diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpreterState.java b/src/main/java/org/perlonjava/backend/bytecode/InterpreterState.java index 0103e306c..60e102066 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpreterState.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpreterState.java @@ -114,4 +114,4 @@ public static List getStack() { public static List getPcStack() { return new ArrayList<>(pcStack.get()); } -} \ No newline at end of file +} diff --git a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java index e0584e432..e2a553861 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java @@ -56,6 +56,15 @@ */ public class SlowOpcodeHandler { + private static final boolean EVAL_TRACE = + System.getenv("JPERL_EVAL_TRACE") != null; + + private static void evalTrace(String msg) { + if (EVAL_TRACE) { + System.err.println("[eval-trace] " + msg); + } + } + // ================================================================= // SLICE AND DEREFERENCE OPERATIONS // ================================================================= @@ -261,6 +270,13 @@ public static int executeEvalString( int rd = bytecode[pc++]; int stringReg = bytecode[pc++]; + int evalCallContext = RuntimeContextType.SCALAR; + // Newer bytecode encodes the eval operator's own call context (VOID/SCALAR/LIST) + // so eval semantics are correct even when the surrounding statement is compiled + // in VOID context. + if (pc < bytecode.length) { + evalCallContext = bytecode[pc++]; + } // Get the code string - handle both RuntimeScalar and RuntimeList (from string interpolation) RuntimeBase codeValue = registers[stringReg]; @@ -272,23 +288,49 @@ public static int executeEvalString( codeScalar = codeValue.scalar(); } String perlCode = codeScalar.toString(); + evalTrace("EVAL_STRING opcode enter rd=r" + rd + " strReg=r" + stringReg + + " ctx=" + evalCallContext + " outerWantarray=" + ((RuntimeScalar) registers[2]).getInt() + + " src=" + (code != null ? code.sourceName : "null")); // Read outer wantarray from register 2 (set by BytecodeInterpreter from the call site context). // This is the true calling context (VOID/SCALAR/LIST) that wantarray() inside the // eval body must reflect — exactly as evalStringWithInterpreter receives callContext. - int callContext = ((RuntimeScalar) registers[2]).getInt(); - - // Call EvalStringHandler to parse, compile, and execute - RuntimeScalar result = EvalStringHandler.evalString( - perlCode, - code, // Current InterpretedCode for context - registers, // Current registers for variable access - code.sourceName, - code.sourceLine, - callContext // True outer context for wantarray inside eval body - ); + int callContext = evalCallContext; + if (registers[2] instanceof RuntimeScalar rs) { + // For backward compatibility with older bytecode, or if evalCallContext + // is not set correctly, fall back to the outer wantarray register. + if (callContext == 0 && rs.value != null) { + callContext = rs.getInt(); + } + } - registers[rd] = result; + if (callContext == RuntimeContextType.LIST) { + // Return list context result + RuntimeList result = EvalStringHandler.evalStringList( + perlCode, + code, // Current InterpretedCode for context + registers, // Current registers for variable access + code.sourceName, + code.sourceLine, + callContext + ); + registers[rd] = result; + evalTrace("EVAL_STRING opcode exit LIST stored=" + (registers[rd] != null ? registers[rd].getClass().getSimpleName() : "null") + + " scalar=" + result.scalar().toString()); + } else { + // Scalar/void context: return scalar result + RuntimeScalar result = EvalStringHandler.evalString( + perlCode, + code, // Current InterpretedCode for context + registers, // Current registers for variable access + code.sourceName, + code.sourceLine, + callContext + ); + registers[rd] = result; + evalTrace("EVAL_STRING opcode exit SCALAR/VOID stored=" + (registers[rd] != null ? registers[rd].getClass().getSimpleName() : "null") + + " val=" + result.toString() + " bool=" + result.getBoolean()); + } return pc; } diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitEval.java b/src/main/java/org/perlonjava/backend/jvm/EmitEval.java index 860c79f47..5c68353b4 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitEval.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitEval.java @@ -50,6 +50,15 @@ * */ public class EmitEval { + + private static final boolean EVAL_TRACE = + System.getenv("JPERL_EVAL_TRACE") != null; + + private static void evalTrace(String msg) { + if (EVAL_TRACE) { + System.err.println("[eval-trace] " + msg); + } + } /** * Handles the emission of bytecode for the Perl 'eval' operator. * @@ -89,6 +98,9 @@ static void handleEvalOperator(EmitterVisitor emitterVisitor, OperatorNode node) EmitterContext ctx = emitterVisitor.ctx; MethodVisitor mv = ctx.mv; + evalTrace("EmitEval.handleEvalOperator op=" + node.operator + " ctx=" + emitterVisitor.ctx.contextType + + " file=" + emitterVisitor.ctx.compilerOptions.fileName + " token=" + node.tokenIndex); + // Log current symbol table state for debugging emitterVisitor.ctx.logDebug("(eval) ctx.symbolTable.getAllVisibleVariables"); @@ -569,12 +581,14 @@ private static void emitEvalInterpreterPath(EmitterVisitor emitterVisitor, Strin // For eval, use the context determined by how the eval result is used // This matches the compiler path which uses a compile-time constant if (emitterVisitor.ctx.contextType == RuntimeContextType.RUNTIME) { - // If context is RUNTIME, load it from wantarray variable - mv.visitVarInsn(Opcodes.ILOAD, emitterVisitor.ctx.symbolTable.getVariableIndex("wantarray")); + // If context is RUNTIME, load it from apply(@_, callContext) method argument. + // Slot 0=this, 1=@_, 2=callContext. + mv.visitVarInsn(Opcodes.ILOAD, 2); } else { // Otherwise use the compile-time constant (LIST/SCALAR/VOID) mv.visitLdcInsn(emitterVisitor.ctx.contextType); } + evalTrace("EmitEval.emitEvalInterpreterPath tag=" + evalTag + " pushCtx=" + emitterVisitor.ctx.contextType); // Stack: [RuntimeScalar(String), String, Object[], RuntimeArray(@_), int] // Call evalStringWithInterpreter which returns RuntimeList directly diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitLogicalOperator.java b/src/main/java/org/perlonjava/backend/jvm/EmitLogicalOperator.java index de2599f57..b8de49a11 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitLogicalOperator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitLogicalOperator.java @@ -20,6 +20,15 @@ */ public class EmitLogicalOperator { + private static final boolean EVAL_TRACE = + System.getenv("JPERL_EVAL_TRACE") != null; + + private static void evalTrace(String msg) { + if (EVAL_TRACE) { + System.err.println("[eval-trace] " + msg); + } + } + /** * Emits bytecode for the flip-flop operator, which is used in range-like conditions. * @@ -277,6 +286,7 @@ private static void emitLogicalOperatorSimple(EmitterVisitor emitterVisitor, Bin Label endLabel = new Label(); if (emitterVisitor.ctx.contextType == RuntimeContextType.VOID) { + evalTrace("EmitLogicalOperatorSimple VOID op=" + node.operator + " emit LHS in SCALAR; RHS in SCALAR"); node.left.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/runtimetypes/RuntimeBase", getBoolean, "()Z", false); mv.visitJumpInsn(compareOpcode, endLabel); diff --git a/src/main/java/org/perlonjava/frontend/analysis/EmitterVisitor.java b/src/main/java/org/perlonjava/frontend/analysis/EmitterVisitor.java index 9510c0e3d..9ee9e17df 100644 --- a/src/main/java/org/perlonjava/frontend/analysis/EmitterVisitor.java +++ b/src/main/java/org/perlonjava/frontend/analysis/EmitterVisitor.java @@ -65,8 +65,9 @@ public EmitterVisitor with(int contextType) { public void pushCallContext() { // push call context to stack if (ctx.contextType == RuntimeContextType.RUNTIME) { - // Retrieve wantarray value from JVM local vars - ctx.mv.visitVarInsn(Opcodes.ILOAD, ctx.symbolTable.getVariableIndex("wantarray")); + // Retrieve callContext from apply(@_, callContext) method argument. + // Slot 0=this, 1=@_, 2=callContext. + ctx.mv.visitVarInsn(Opcodes.ILOAD, 2); } else { ctx.mv.visitLdcInsn(ctx.contextType); } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index bc1a18868..55a2e050a 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -63,6 +63,15 @@ public class RuntimeCode extends RuntimeBase implements RuntimeScalarReference { public static final boolean EVAL_VERBOSE = System.getenv("JPERL_EVAL_VERBOSE") != null; + public static final boolean EVAL_TRACE = + System.getenv("JPERL_EVAL_TRACE") != null; + + private static void evalTrace(String msg) { + if (EVAL_TRACE) { + System.err.println("[eval-trace] " + msg); + } + } + /** * Flag to enable disassembly of eval STRING bytecode. * When set, prints the interpreter bytecode for each eval STRING compilation. @@ -711,6 +720,9 @@ public static RuntimeList evalStringWithInterpreter( RuntimeArray args, int callContext) throws Throwable { + evalTrace("evalStringWithInterpreter enter tag=" + evalTag + " ctx=" + callContext + + " codeType=" + code.type + " codeLen=" + (code.toString() != null ? code.toString().length() : -1)); + // Retrieve the eval context that was saved at program compile-time EmitterContext ctx = RuntimeCode.evalContext.get(evalTag); @@ -734,6 +746,8 @@ public static RuntimeList evalStringWithInterpreter( try { String evalString = code.toString(); + evalTrace("evalStringWithInterpreter parse start tag=" + evalTag + " ctx=" + callContext + + " fileName=" + ctx.compilerOptions.fileName); // Handle Unicode source detection (same logic as evalStringHelper) boolean hasUnicode = false; if (!ctx.isEvalbytes && code.type != RuntimeScalarType.BYTE_STRING) { @@ -857,6 +871,9 @@ public static RuntimeList evalStringWithInterpreter( adjustedRegistry); compiler.setCompilePackage(capturedSymbolTable.getCurrentPackage()); interpretedCode = compiler.compile(ast, evalCtx); + evalTrace("evalStringWithInterpreter compiled tag=" + evalTag + + " bytecodeLen=" + (interpretedCode != null ? interpretedCode.bytecode.length : -1) + + " src=" + (interpretedCode != null ? interpretedCode.sourceName : "null")); if (DISASSEMBLE) { System.out.println(interpretedCode.disassemble()); } @@ -940,6 +957,11 @@ public static RuntimeList evalStringWithInterpreter( try { result = interpretedCode.apply(args, callContext); + evalTrace("evalStringWithInterpreter exec ok tag=" + evalTag + " ctx=" + callContext + + " resultClass=" + (result != null ? result.getClass().getSimpleName() : "null") + + " resultScalar=" + (result != null ? result.scalar().toString() : "null") + + " resultBool=" + (result != null && result.scalar() != null ? result.scalar().getBoolean() : false)); + // Clear $@ on successful execution RuntimeScalar err = GlobalVariable.getGlobalVariable("main::@"); err.set(""); @@ -947,6 +969,8 @@ public static RuntimeList evalStringWithInterpreter( return result; } catch (PerlDieException e) { + evalTrace("evalStringWithInterpreter exec die tag=" + evalTag + " ctx=" + callContext + + " payload=" + (e.getPayload() != null ? e.getPayload().getFirst().toString() : "null")); // Runtime error - set $@ and return undef/empty list RuntimeScalar err = GlobalVariable.getGlobalVariable("main::@"); RuntimeBase payload = e.getPayload(); @@ -976,6 +1000,8 @@ public static RuntimeList evalStringWithInterpreter( } } catch (Throwable e) { + evalTrace("evalStringWithInterpreter exec throwable tag=" + evalTag + " ctx=" + callContext + + " ex=" + e.getClass().getSimpleName() + " msg=" + e.getMessage()); // Other runtime errors - set $@ and return undef/empty list RuntimeScalar err = GlobalVariable.getGlobalVariable("main::@"); String message = e.getMessage(); @@ -993,6 +1019,8 @@ public static RuntimeList evalStringWithInterpreter( } } finally { + evalTrace("evalStringWithInterpreter exit tag=" + evalTag + " ctx=" + callContext + + " $@=" + GlobalVariable.getGlobalVariable("main::@").toString()); // Restore dynamic variables (local) to their state before eval DynamicVariableManager.popToLocalLevel(dynamicVarLevel); diff --git a/src/main/perl/lib/B.pm b/src/main/perl/lib/B.pm index 62af71c8f..41a40621a 100644 --- a/src/main/perl/lib/B.pm +++ b/src/main/perl/lib/B.pm @@ -11,6 +11,11 @@ our $VERSION = '1.00_perlonjava'; # Flag indicating this is a stub implementation with limited introspection our $INCOMPLETE = 1; +# SV flags (very partial) +use constant { + SVf_IOK => 0x0001, +}; + # CV flags use constant { CVf_ANON => 0x0004, @@ -25,6 +30,24 @@ package B::SV { my ($class, $ref) = @_; return bless { ref => $ref }, $class; } + + sub FLAGS { + my $self = shift; + my $r = $self->{ref}; + + # For the debugger source arrays (@{"_<..."}), perl stores lines as PVIV with IOK. + # This stub implementation marks any defined, non-empty scalar as having IOK. + if (ref($r) eq 'SCALAR') { + my $v = $$r; + return (defined($v) && length($v)) ? B::SVf_IOK() : 0; + } + + return 0; + } +} + +package B::PVIV { + our @ISA = ('B::SV'); } package B::CV { @@ -129,12 +152,19 @@ sub svref_2object { return B::CV->new($ref); } + if ($type eq 'SCALAR') { + return B::PVIV->new($ref); + } + return B::SV->new($ref); } # Export CVf_ANON as a function sub CVf_ANON() { return 0x0004; } +# Export SVf_IOK as a function +sub SVf_IOK() { return 0x0001; } + # Special SV names our @specialsv_name = ('Nullsv', '&PL_sv_undef', '&PL_sv_yes', '&PL_sv_no'); From e3732e84868f0e231b6902bc9c9ec4c76c2752fc Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Thu, 26 Feb 2026 15:25:23 +0100 Subject: [PATCH 03/10] Fix logical or/|| in RUNTIME context for eval interpreter --- .../org/perlonjava/backend/jvm/EmitLogicalOperator.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitLogicalOperator.java b/src/main/java/org/perlonjava/backend/jvm/EmitLogicalOperator.java index b8de49a11..2b2a659b0 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitLogicalOperator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitLogicalOperator.java @@ -321,10 +321,10 @@ private static void emitLogicalOperatorSimple(EmitterVisitor emitterVisitor, Bin rewritten = true; } - // For RUNTIME context, preserve it; otherwise use SCALAR for boolean evaluation - int operandContext = emitterVisitor.ctx.contextType == RuntimeContextType.RUNTIME - ? RuntimeContextType.RUNTIME - : RuntimeContextType.SCALAR; + // Logical operators always evaluate their operands in scalar context for truthiness. + // Even when the enclosing context is RUNTIME, evaluating operands in RUNTIME can + // propagate VOID into constructs like `eval STRING`, breaking `eval "1" or die`. + int operandContext = RuntimeContextType.SCALAR; resultRef = emitterVisitor.ctx.javaClassInfo.acquireSpillRefOrAllocate(emitterVisitor.ctx.symbolTable); From 50c7149050a43b02564626cf9115d394114a0594 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Thu, 26 Feb 2026 15:31:18 +0100 Subject: [PATCH 04/10] Bytecode: lexical scalar assignment uses SET_SCALAR to preserve lvalue --- .../org/perlonjava/backend/bytecode/CompileAssignment.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java index 27f5a3608..27d36d228 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java @@ -723,8 +723,10 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.emitReg(targetReg); bytecodeCompiler.emitReg(valueReg); } else { - // Regular lexical - use MOVE - bytecodeCompiler.emit(Opcodes.MOVE); + // Regular lexical - use SET_SCALAR to avoid aliasing cached read-only scalars. + // This keeps the lexical's RuntimeScalar object mutable, which is required for + // lvalue uses like: ($x = expr) =~ s/// + bytecodeCompiler.emit(Opcodes.SET_SCALAR); bytecodeCompiler.emitReg(targetReg); bytecodeCompiler.emitReg(valueReg); } From bfdabeefae9cd7857cd4afbd5c1570470e8d0ed6 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Thu, 26 Feb 2026 15:59:44 +0100 Subject: [PATCH 05/10] Fix eval interpreter $SIG{__DIE__} and exception handling via WarnDie.catchEval Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../org/perlonjava/runtime/runtimetypes/RuntimeCode.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index 55a2e050a..21ed1ed17 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -1002,13 +1002,7 @@ public static RuntimeList evalStringWithInterpreter( } catch (Throwable e) { evalTrace("evalStringWithInterpreter exec throwable tag=" + evalTag + " ctx=" + callContext + " ex=" + e.getClass().getSimpleName() + " msg=" + e.getMessage()); - // Other runtime errors - set $@ and return undef/empty list - RuntimeScalar err = GlobalVariable.getGlobalVariable("main::@"); - String message = e.getMessage(); - if (message == null || message.isEmpty()) { - message = e.getClass().getSimpleName(); - } - err.set(message); + WarnDie.catchEval(e); // Return undef/empty list if (callContext == RuntimeContextType.LIST) { From cd3611cb90461947c00e55d4dab2d1cc1f5cc98e Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Thu, 26 Feb 2026 16:10:56 +0100 Subject: [PATCH 06/10] Parser: make yadayada (...) expression contexts a syntax error --- .../perlonjava/frontend/parser/StatementResolver.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/org/perlonjava/frontend/parser/StatementResolver.java b/src/main/java/org/perlonjava/frontend/parser/StatementResolver.java index 3f7e9559f..c1656a31d 100644 --- a/src/main/java/org/perlonjava/frontend/parser/StatementResolver.java +++ b/src/main/java/org/perlonjava/frontend/parser/StatementResolver.java @@ -548,6 +548,17 @@ public static Node parseStatement(Parser parser, String label) { Node result = switch (token.text) { case "..." -> { TokenUtils.consume(parser); + // The yadayada operator `...` is only valid as a standalone statement. + // In expression contexts (e.g. "... + 0", "... if 1", "my $a = ..."), perl5 + // reports a syntax error at compile time. + LexerToken next = TokenUtils.peek(parser); + boolean isStatementTerminator = + next.type == LexerTokenType.EOF || + next.text.equals(";") || + next.text.equals("}"); + if (!isStatementTerminator) { + throw new PerlCompilerException(parser.tokenIndex, "syntax error", parser.ctx.errorUtil); + } yield dieWarnNode(parser, "die", new ListNode(List.of( new StringNode("Unimplemented", parser.tokenIndex)), parser.tokenIndex), parser.tokenIndex); } From 798fca7c850034eb8b827084e410ee65dd195c6f Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Thu, 26 Feb 2026 17:29:01 +0100 Subject: [PATCH 07/10] Bytecode: fix lexical scalar assignment to avoid aliasing and in-place mutation bugs The previous SET_SCALAR-only approach modified the existing RuntimeScalar object in-place, which corrupted 'local' variable restoration when the register was shared. The original MOVE approach aliased constants from the pool, corrupting them on later mutation. Fix: emit LOAD_UNDEF to allocate a fresh RuntimeScalar, then SET_SCALAR to copy the value into it. This preserves lvalue semantics while avoiding both aliasing and in-place mutation issues. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/bytecode/CompileAssignment.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java index 27d36d228..b4214d360 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java @@ -723,9 +723,15 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.emitReg(targetReg); bytecodeCompiler.emitReg(valueReg); } else { - // Regular lexical - use SET_SCALAR to avoid aliasing cached read-only scalars. - // This keeps the lexical's RuntimeScalar object mutable, which is required for - // lvalue uses like: ($x = expr) =~ s/// + // Regular lexical - create a fresh RuntimeScalar, then copy the value into it. + // LOAD_UNDEF allocates a new mutable RuntimeScalar in the target register; + // SET_SCALAR copies the source value into it. + // This avoids two bugs: + // - MOVE aliases constants from the pool, corrupting them on later mutation + // - SET_SCALAR alone modifies the existing object in-place, which breaks + // 'local' variable restoration when the register was shared + bytecodeCompiler.emit(Opcodes.LOAD_UNDEF); + bytecodeCompiler.emitReg(targetReg); bytecodeCompiler.emit(Opcodes.SET_SCALAR); bytecodeCompiler.emitReg(targetReg); bytecodeCompiler.emitReg(valueReg); From d4a24f66fe495f2edd406315effa01e54e8df879 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Thu, 26 Feb 2026 17:45:04 +0100 Subject: [PATCH 08/10] Fix logical operator context propagation for wantarray The LHS of ||/&&// must be SCALAR (for truthiness), but the RHS must inherit the enclosing context (including RUNTIME) since its value becomes the result. The previous fix forced SCALAR for both operands, breaking wantarray propagation through logical operators at sub exit. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/jvm/EmitLogicalOperator.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitLogicalOperator.java b/src/main/java/org/perlonjava/backend/jvm/EmitLogicalOperator.java index 2b2a659b0..55f8b79b5 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitLogicalOperator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitLogicalOperator.java @@ -321,15 +321,18 @@ private static void emitLogicalOperatorSimple(EmitterVisitor emitterVisitor, Bin rewritten = true; } - // Logical operators always evaluate their operands in scalar context for truthiness. - // Even when the enclosing context is RUNTIME, evaluating operands in RUNTIME can - // propagate VOID into constructs like `eval STRING`, breaking `eval "1" or die`. - int operandContext = RuntimeContextType.SCALAR; + // LHS is always evaluated in SCALAR context for the boolean/truthiness test. + // RHS inherits the enclosing context (including RUNTIME) since its value + // becomes the result of the logical operator and must honour wantarray. + int lhsContext = RuntimeContextType.SCALAR; + int rhsContext = emitterVisitor.ctx.contextType == RuntimeContextType.RUNTIME + ? RuntimeContextType.RUNTIME + : RuntimeContextType.SCALAR; resultRef = emitterVisitor.ctx.javaClassInfo.acquireSpillRefOrAllocate(emitterVisitor.ctx.symbolTable); // Evaluate LHS and store it. - node.left.accept(emitterVisitor.with(operandContext)); + node.left.accept(emitterVisitor.with(lhsContext)); emitterVisitor.ctx.javaClassInfo.storeSpillRef(mv, resultRef); // Boolean test on the stored LHS. @@ -338,7 +341,7 @@ private static void emitLogicalOperatorSimple(EmitterVisitor emitterVisitor, Bin mv.visitJumpInsn(compareOpcode, endLabel); // LHS didn't short-circuit: evaluate RHS, overwrite result. - node.right.accept(emitterVisitor.with(operandContext)); + node.right.accept(emitterVisitor.with(rhsContext)); emitterVisitor.ctx.javaClassInfo.storeSpillRef(mv, resultRef); // Return whichever side won the short-circuit. From f5690e2ce36af538c5c6be624241aa11a20cded5 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Thu, 26 Feb 2026 17:54:38 +0100 Subject: [PATCH 09/10] Fix kvhslice scalar context in bytecode compiler Two fixes for key/value hash slice (%h{...}) in scalar context: 1. Remove top-level ARRAY_SIZE before RETURN in BytecodeCompiler.compile(). ARRAY_SIZE returns element count for RuntimeList, but eval callers already call RuntimeList.scalar() which correctly returns the last element. The double conversion produced wrong results for kvhslice. 2. Add LIST_TO_SCALAR conversion in handleHashKeyValueSlice() for scalar context, matching the existing pattern in handleArrayKeyValueSlice(). Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/bytecode/BytecodeCompiler.java | 21 ++++++++----------- 1 file changed, 9 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 1fb01f516..d5a3b98c6 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -494,17 +494,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; @@ -1379,7 +1368,15 @@ void handleHashKeyValueSlice(BinaryOperatorNode node, OperatorNode leftOp) { emitReg(rd); emitReg(hashReg); emitReg(keysListReg); - lastResultReg = rd; + if (currentCallContext == RuntimeContextType.SCALAR) { + int scalarReg = allocateRegister(); + emit(Opcodes.LIST_TO_SCALAR); + emitReg(scalarReg); + emitReg(rd); + lastResultReg = scalarReg; + } else { + lastResultReg = rd; + } } /** From ff4fdda8358f937382601341a4ae612ac91d8dc8 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Thu, 26 Feb 2026 18:06:32 +0100 Subject: [PATCH 10/10] Bytecode compiler: support local *glob = value assignment Add handling for local typeglob assignment (e.g. local *::name = ref) in the bytecode compiler assignment handler. Emits LOCAL_GLOB to save/localize the glob, then STORE_GLOB to assign the value. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/bytecode/CompileAssignment.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java index b4214d360..056e6ee0a 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java @@ -500,6 +500,27 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.lastResultReg = hashReg; return; + } else if (sigilOp.operator.equals("*") && sigilOp.operand instanceof IdentifierNode) { + // Handle local *glob = value + node.right.accept(bytecodeCompiler); + int valueReg = bytecodeCompiler.lastResultReg; + + String globalName = NameNormalizer.normalizeVariableName( + ((IdentifierNode) sigilOp.operand).name, + bytecodeCompiler.getCurrentPackage()); + int nameIdx = bytecodeCompiler.addToStringPool(globalName); + + int globReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emitWithToken(Opcodes.LOCAL_GLOB, node.getIndex()); + bytecodeCompiler.emitReg(globReg); + bytecodeCompiler.emit(nameIdx); + + bytecodeCompiler.emit(Opcodes.STORE_GLOB); + bytecodeCompiler.emitReg(globReg); + bytecodeCompiler.emitReg(valueReg); + + bytecodeCompiler.lastResultReg = globReg; + return; } } else if (localOperand instanceof ListNode) { // Handle local($x) = value or local($x, $y) = (v1, v2)