From 8f708ddd8c425c262f3e38521b0f7830b26a2d76 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Tue, 24 Feb 2026 10:49:21 +0100 Subject: [PATCH 01/11] Interpreter: fix package BLOCK scoping and compound assignment on globals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five fixes: 1. BytecodeCompiler: strip sigil before normalizeVariableName in compound assignment path. varName was "$main::c" but normalize expects "main::c". This caused LOAD/STORE_GLOBAL_SCALAR to use wrong global slot. 2. BytecodeCompiler/CompileOperator: emit PUSH_PACKAGE for scoped package blocks (package Foo { }) so InterpreterState.currentPackage is saved. Use nextPackageIsScoped flag set by visit(BlockNode). Emit POP_PACKAGE at block exit to restore the runtime package via DynamicVariableManager. 3. EvalStringHandler: use InterpreterState.currentPackage (runtime) instead of currentCode.compilePackage (compile-time) so that eval STRING inside a package BLOCK sees the correct package for __PACKAGE__. 4. EvalStringHandler: substitute undef for null captured registers instead of capturing null — null registers crash when the eval reads them as the eval string operand. 5. BytecodeCompiler visit(BlockNode): when the last statement produces no result (lastResultReg < 0, e.g. a bare block with void semantics), emit LOAD_UNDEF into the outer result register instead of leaving it null. A null register causes NPE when RETURN reads it. Results (JPERL_EVAL_USE_INTERPRETER=1 vs without): - comp/package_block.t: 0/5 -> 4/5 (test 4 is pre-existing) - comp/parser.t: 58/193 -> 61/193 (tests 36, 38, 40 fixed) - op/signatures.t: no regression (446/908 in both modes) --- .../backend/bytecode/BytecodeCompiler.java | 81 ++++++++++++++++--- .../backend/bytecode/BytecodeInterpreter.java | 9 ++- .../backend/bytecode/CompileOperator.java | 27 +++++-- .../backend/bytecode/EvalStringHandler.java | 18 +++-- .../backend/bytecode/InterpretedCode.java | 11 ++- 5 files changed, 119 insertions(+), 27 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index c26d44f07..7b60bb3a6 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -43,6 +43,10 @@ public class BytecodeCompiler implements Visitor { private final Stack savedNextRegister = new Stack<>(); private final Stack savedBaseRegister = new Stack<>(); + // Flag set by visit(BlockNode) when the block is a scoped package block (package Foo { }). + // CompileOperator reads this to emit PUSH_PACKAGE instead of SET_PACKAGE. + boolean nextPackageIsScoped = false; + // Loop label stack for last/next/redo control flow // Each entry tracks loop boundaries and optional label private final Stack loopStack = new Stack<>(); @@ -267,19 +271,40 @@ private void exitScope() { /** * Helper: Get current package name for global variable resolution. - * Uses symbolTable for proper package/class tracking. + * Uses emitterContext.symbolTable when available (mirrors JVM ctx.symbolTable), + * falling back to the BytecodeCompiler's own symbolTable. */ String getCurrentPackage() { + if (emitterContext != null && emitterContext.symbolTable != null) { + return emitterContext.symbolTable.getCurrentPackage(); + } return symbolTable.getCurrentPackage(); } /** * Set the compile-time package for name normalization. * Called by eval STRING handlers to sync the package from the call site, - * so bare names like *named compile to FOO3::named instead of main::named. + * and by the package operator during compilation. + * Updates emitterContext.symbolTable (matching JVM ctx.symbolTable behaviour). */ public void setCompilePackage(String packageName) { - symbolTable.setCurrentPackage(packageName, false); + if (emitterContext != null && emitterContext.symbolTable != null) { + emitterContext.symbolTable.setCurrentPackage(packageName, false); + } else { + symbolTable.setCurrentPackage(packageName, false); + } + } + + /** + * Set the compile-time package with class flag. + * Used by the package/class operator during compilation. + */ + public void setCompilePackageWithClass(String packageName, boolean isClass) { + if (emitterContext != null && emitterContext.symbolTable != null) { + emitterContext.symbolTable.setCurrentPackage(packageName, isClass); + } else { + symbolTable.setCurrentPackage(packageName, isClass); + } } /** @@ -744,6 +769,16 @@ public void visit(BlockNode node) { && node.elements.get(0) instanceof OperatorNode localOp && localOp.operator.equals("local"); + // Detect scoped package blocks: package Foo { ... } + // The parser places the package OperatorNode first in the block. + // PUSH_PACKAGE saves the runtime current package (InterpreterState.currentPackage) so that + // eval STRING inside the block sees the correct package via InterpreterState. + // POP_PACKAGE restores it when the block exits — mirrors the JVM path's local variable save. + // Note: compile-time package is restored separately by exitScope() via packageStack. + boolean isScopedPackageBlock = !node.elements.isEmpty() + && node.elements.get(0) instanceof OperatorNode pkgOp + && (pkgOp.operator.equals("package") || pkgOp.operator.equals("class")); + enterScope(); // Visit each statement in the block @@ -753,6 +788,12 @@ public void visit(BlockNode node) { if (i == 0 && skipFirstChild) continue; Node stmt = node.elements.get(i); + // Signal to CompileOperator that the package operator is scoped (package Foo { }) + // so it emits PUSH_PACKAGE instead of SET_PACKAGE. + if (i == 0 && isScopedPackageBlock) { + nextPackageIsScoped = true; + } + // Track line number for this statement (like codegen's setDebugInfoLineNumber) if (stmt != null) { int tokenIndex = stmt.getIndex(); @@ -780,13 +821,28 @@ public void visit(BlockNode node) { } // Save the last statement's result to the outer register BEFORE exiting scope - if (outerResultReg >= 0 && lastResultReg >= 0) { - emit(Opcodes.MOVE); - emitReg(outerResultReg); - emitReg(lastResultReg); + if (outerResultReg >= 0) { + if (lastResultReg >= 0) { + emit(Opcodes.MOVE); + emitReg(outerResultReg); + emitReg(lastResultReg); + } else { + // Last statement produced no value (e.g. bare block with void semantics). + // Load undef so the register is never null when RETURN reads it. + emit(Opcodes.LOAD_UNDEF); + emitReg(outerResultReg); + } + } + + // Restore runtime current package if this was a scoped package block. + // PUSH_PACKAGE saved InterpreterState.currentPackage; POP_PACKAGE restores it. + // The compile-time package is restored separately by exitScope() via packageStack. + if (isScopedPackageBlock) { + emit(Opcodes.POP_PACKAGE); } - // Exit scope restores register state + // Exit scope restores register state and compile-time pragma frames + // (including packageStack, which handles the compile-time package restore). exitScope(); // Set lastResultReg to the outer register (or -1 if VOID context) @@ -1279,7 +1335,9 @@ void handleCompoundAssignment(BinaryOperatorNode node) { // Global variable - need to load it first isGlobal = true; targetReg = allocateRegister(); - String normalizedName = NameNormalizer.normalizeVariableName(varName, getCurrentPackage()); + // Strip sigil before normalizing — varName is "$main::c", normalize needs "main::c" + String bareVarName = varName.substring(1); + String normalizedName = NameNormalizer.normalizeVariableName(bareVarName, getCurrentPackage()); int nameIdx = addToStringPool(normalizedName); emit(Opcodes.LOAD_GLOBAL_SCALAR); emitReg(targetReg); @@ -1349,7 +1407,8 @@ void handleCompoundAssignment(BinaryOperatorNode node) { throwCompilerException("Global symbol \"" + varName + "\" requires explicit package name"); } - String normalizedName = NameNormalizer.normalizeVariableName(varName, getCurrentPackage()); + // Strip sigil before normalizing — varName is "$main::c", normalize needs "main::c" + String normalizedName = NameNormalizer.normalizeVariableName(varName.substring(1), getCurrentPackage()); int nameIdx = addToStringPool(normalizedName); emit(Opcodes.STORE_GLOBAL_SCALAR); emit(nameIdx); @@ -3673,7 +3732,7 @@ public void visit(For3Node node) { if (node.body != null) { node.body.accept(this); } - lastResultReg = -1; // Block returns empty + lastResultReg = -1; // Block returns empty (void semantics) } finally { // Exit scope to clean up lexical variables exitScope(); diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index d38b51a9d..799861add 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -2066,13 +2066,20 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c case Opcodes.PUSH_PACKAGE: { // Scoped package block entry: package Foo { ... // Save current package via DynamicVariableManager so it is restored - // automatically when the scope exits via POP_LOCAL_LEVEL. + // by POP_PACKAGE when the scoped block exits. int nameIdx = bytecode[pc++]; DynamicVariableManager.pushLocalVariable(InterpreterState.currentPackage.get()); InterpreterState.currentPackage.get().set(code.stringPool[nameIdx]); break; } + case Opcodes.POP_PACKAGE: { + // Scoped package block exit: closing } of package Foo { ... + // Restore the previous package name saved by PUSH_PACKAGE. + DynamicVariableManager.popToLocalLevel(DynamicVariableManager.getLocalLevel() - 1); + break; + } + default: // Unknown opcode int opcodeInt = opcode & 0xFF; diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index 1ed2a34e6..07c455d9a 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -66,7 +66,7 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode Boolean isClassAnnotation = (Boolean) node.getAnnotation("isClass"); boolean isClass = op.equals("class") || (isClassAnnotation != null && isClassAnnotation); - // Check if there's a version associated with this package and set $Package::VERSION + // Check if there's a version associated with this package and set $Package::VERSION. String version = bytecodeCompiler.symbolTable.getPackageVersion(packageName); if (version != null) { // Set $PackageName::VERSION at compile time using GlobalVariable @@ -75,8 +75,9 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode .set(new RuntimeScalar(version)); } - // Update the current package/class in symbol table (compile-time tracking) - bytecodeCompiler.symbolTable.setCurrentPackage(packageName, isClass); + // Update the current package/class in symbol table (compile-time tracking). + // Mirrors JVM handlePackageOperator which calls ctx.symbolTable.setCurrentPackage. + bytecodeCompiler.setCompilePackageWithClass(packageName, isClass); // Register as Perl 5.38+ class for proper stringification if needed if (isClass) { @@ -85,12 +86,13 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode // Emit runtime package tracking opcode so caller() and eval STRING work. // Scoped blocks (package Foo { }) use PUSH_PACKAGE so DynamicVariableManager - // can restore the previous package when the scope exits. + // can restore the previous package when POP_PACKAGE is emitted at block exit. // Non-scoped (package Foo;) use SET_PACKAGE which just overwrites. - boolean isScoped = Boolean.TRUE.equals(node.getAnnotation("isScoped")); + // nextPackageIsScoped is set by visit(BlockNode) for scoped package blocks. int nameIdx = bytecodeCompiler.addToStringPool(packageName); - if (isScoped) { + if (bytecodeCompiler.nextPackageIsScoped) { bytecodeCompiler.emit(Opcodes.PUSH_PACKAGE); + bytecodeCompiler.nextPackageIsScoped = false; // consume the flag } else { bytecodeCompiler.emit(Opcodes.SET_PACKAGE); } @@ -828,10 +830,21 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode } else if (op.equals("eval")) { // eval $string; if (node.operand != null) { - // Evaluate eval operand (the code string) + // Evaluate eval operand (the code string) in SCALAR context + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; node.operand.accept(bytecodeCompiler); + bytecodeCompiler.currentCallContext = savedContext; int stringReg = bytecodeCompiler.lastResultReg; + // If operand produced no result (e.g. bare block with void semantics), + // substitute undef so the eval string is empty + if (stringReg < 0) { + stringReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_UNDEF); + bytecodeCompiler.emitReg(stringReg); + } + // Allocate register for result int rd = bytecodeCompiler.allocateRegister(); diff --git a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java index 862d0f633..09a96dc99 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java @@ -80,12 +80,13 @@ 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"; + // Inherit the runtime package from InterpreterState.currentPackage. + // This is the package active at the eval call site at runtime, which is what + // Perl's eval STRING semantics require — e.g. inside "package Foo { eval '...' }" + // the eval sees package Foo, and __PACKAGE__ inside it returns "Foo". + // PUSH_PACKAGE already updates InterpreterState.currentPackage at runtime when + // entering a scoped package block, so this correctly tracks nested packages. + String compilePackage = InterpreterState.currentPackage.get().toString(); symbolTable.setCurrentPackage(compilePackage, false); ErrorMessageUtil errorUtil = new ErrorMessageUtil(sourceName, tokens); @@ -143,7 +144,10 @@ public static RuntimeScalar evalString(String perlCode, // Skip non-Perl values (like Iterator objects from for loops) // Only capture actual Perl variables: Scalar, Array, Hash, Code if (value == null) { - // Null is fine - capture it + // Substitute undef for null registers — a null means the variable + // was declared but not yet assigned; capture as undef scalar so + // the eval's register is never null (which would crash on access). + value = new RuntimeScalar(); } else if (value instanceof RuntimeScalar) { // Check if the scalar contains an Iterator (used by for loops) RuntimeScalar scalar = (RuntimeScalar) value; diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java index 7e7177037..a83440c6a 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java @@ -1454,8 +1454,17 @@ public String disassemble() { break; // GENERATED_DISASM_END + case Opcodes.SET_PACKAGE: + sb.append("SET_PACKAGE ").append(stringPool[bytecode[pc++]]).append("\n"); + break; + case Opcodes.PUSH_PACKAGE: + sb.append("PUSH_PACKAGE ").append(stringPool[bytecode[pc++]]).append("\n"); + break; + case Opcodes.POP_PACKAGE: + sb.append("POP_PACKAGE\n"); + break; default: - sb.append("UNKNOWN(").append(opcode & 0xFF).append(")\n"); + sb.append("UNKNOWN(").append(opcode).append(")\n"); break; } } From 4cf9aba0c5f60a16a264db1b2e70a3d9d4879701 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Tue, 24 Feb 2026 12:01:55 +0100 Subject: [PATCH 02/11] Interpreter: fix compound assignment on globals - strip sigil before normalizeVariableName $main::result .= "..." was silently failing because the sigil was passed to normalizeVariableName, producing "$main::result" (with sigil) as the store key instead of "main::result" (without sigil). --- .../backend/bytecode/BytecodeCompiler.java | 392 ++++-------------- .../backend/bytecode/BytecodeInterpreter.java | 130 ++---- .../backend/bytecode/CompileAssignment.java | 35 +- .../backend/bytecode/CompileOperator.java | 51 +-- .../backend/bytecode/EvalStringHandler.java | 36 +- .../backend/bytecode/InterpretedCode.java | 54 +-- .../bytecode/OpcodeHandlerExtended.java | 36 +- .../perlonjava/backend/bytecode/Opcodes.java | 37 -- .../backend/bytecode/SlowOpcodeHandler.java | 137 +----- 9 files changed, 181 insertions(+), 727 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 7b60bb3a6..f4a3b2ccb 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -43,10 +43,6 @@ public class BytecodeCompiler implements Visitor { private final Stack savedNextRegister = new Stack<>(); private final Stack savedBaseRegister = new Stack<>(); - // Flag set by visit(BlockNode) when the block is a scoped package block (package Foo { }). - // CompileOperator reads this to emit PUSH_PACKAGE instead of SET_PACKAGE. - boolean nextPackageIsScoped = false; - // Loop label stack for last/next/redo control flow // Each entry tracks loop boundaries and optional label private final Stack loopStack = new Stack<>(); @@ -151,13 +147,10 @@ public BytecodeCompiler(String sourceName, int sourceLine, ErrorMessageUtil erro globalScope.put("wantarray", 2); if (parentRegistry != null) { - // Add parent scope variables to the global scope so hasVariable() finds them. - // A "my $x" inside the eval will call addVariable() which puts $x into the - // current (inner) scope, shadowing the global-scope entry — so lookups find - // the local my-declared register first (correct shadowing behaviour). + // Add parent scope variables (for eval STRING variable capture) globalScope.putAll(parentRegistry); - // Also mark them as captured so assignments use SET_SCALAR (not STORE_SCALAR) + // Mark parent scope variables as captured so assignments use SET_SCALAR capturedVarIndices = new HashMap<>(); for (Map.Entry entry : parentRegistry.entrySet()) { String varName = entry.getKey(); @@ -234,23 +227,12 @@ private void enterScope() { savedBaseRegister.push(baseRegisterForStatement); // Update base to protect all registers allocated before this scope baseRegisterForStatement = nextRegister; - // Mirror the JVM compiler: push a new pragma frame so that strict/feature/warning - // changes inside this block (e.g. `use strict`, `use 5.012`) are scoped to the block - // and do not leak into the surrounding code after the block exits. - if (emitterContext != null && emitterContext.symbolTable != null) { - savedSymbolTableScopes.push(emitterContext.symbolTable.enterScope()); - } else { - savedSymbolTableScopes.push(-1); // sentinel so stacks stay in sync - } } /** * Helper: Exit the current lexical scope. * Restores register allocation state to what it was before entering the scope. */ - // Saved symbol-table scope indices, parallel to savedNextRegister / savedBaseRegister. - private final Stack savedSymbolTableScopes = new Stack<>(); - private void exitScope() { if (variableScopes.size() > 1) { variableScopes.pop(); @@ -262,49 +244,23 @@ private void exitScope() { baseRegisterForStatement = savedBaseRegister.pop(); } } - // Restore the pragma frame (strict/feature/warning flags) for the scope we just exited. - if (emitterContext != null && emitterContext.symbolTable != null - && !savedSymbolTableScopes.isEmpty()) { - emitterContext.symbolTable.exitScope(savedSymbolTableScopes.pop()); - } } /** * Helper: Get current package name for global variable resolution. - * Uses emitterContext.symbolTable when available (mirrors JVM ctx.symbolTable), - * falling back to the BytecodeCompiler's own symbolTable. + * Uses symbolTable for proper package/class tracking. */ String getCurrentPackage() { - if (emitterContext != null && emitterContext.symbolTable != null) { - return emitterContext.symbolTable.getCurrentPackage(); - } return symbolTable.getCurrentPackage(); } /** * Set the compile-time package for name normalization. * Called by eval STRING handlers to sync the package from the call site, - * and by the package operator during compilation. - * Updates emitterContext.symbolTable (matching JVM ctx.symbolTable behaviour). + * so bare names like *named compile to FOO3::named instead of main::named. */ public void setCompilePackage(String packageName) { - if (emitterContext != null && emitterContext.symbolTable != null) { - emitterContext.symbolTable.setCurrentPackage(packageName, false); - } else { - symbolTable.setCurrentPackage(packageName, false); - } - } - - /** - * Set the compile-time package with class flag. - * Used by the package/class operator during compilation. - */ - public void setCompilePackageWithClass(String packageName, boolean isClass) { - if (emitterContext != null && emitterContext.symbolTable != null) { - emitterContext.symbolTable.setCurrentPackage(packageName, isClass); - } else { - symbolTable.setCurrentPackage(packageName, isClass); - } + symbolTable.setCurrentPackage(packageName, false); } /** @@ -410,30 +366,6 @@ private boolean isNonAsciiLengthOneScalarAllowedUnderNoUtf8(String sigil, String * @param varName The variable name with sigil (e.g., "$A", "@array") * @return true if access should be blocked under strict vars */ - /** Returns true if strict refs is currently enabled at compile time. */ - boolean isStrictRefsEnabled() { - if (emitterContext == null || emitterContext.symbolTable == null) { - return false; - } - return emitterContext.symbolTable.isStrictOptionEnabled(Strict.HINT_STRICT_REFS); - } - - /** Returns the current strict options bitmask at this point in compilation. */ - int getCurrentStrictOptions() { - if (emitterContext == null || emitterContext.symbolTable == null) { - return 0; - } - return emitterContext.symbolTable.strictOptionsStack.peek(); - } - - /** Returns the current feature flags bitmask at this point in compilation. */ - int getCurrentFeatureFlags() { - if (emitterContext == null || emitterContext.symbolTable == null) { - return 0; - } - return emitterContext.symbolTable.featureFlagsStack.peek(); - } - boolean shouldBlockGlobalUnderStrictVars(String varName) { // Only check if strict vars is enabled if (emitterContext == null || emitterContext.symbolTable == null) { @@ -769,16 +701,6 @@ public void visit(BlockNode node) { && node.elements.get(0) instanceof OperatorNode localOp && localOp.operator.equals("local"); - // Detect scoped package blocks: package Foo { ... } - // The parser places the package OperatorNode first in the block. - // PUSH_PACKAGE saves the runtime current package (InterpreterState.currentPackage) so that - // eval STRING inside the block sees the correct package via InterpreterState. - // POP_PACKAGE restores it when the block exits — mirrors the JVM path's local variable save. - // Note: compile-time package is restored separately by exitScope() via packageStack. - boolean isScopedPackageBlock = !node.elements.isEmpty() - && node.elements.get(0) instanceof OperatorNode pkgOp - && (pkgOp.operator.equals("package") || pkgOp.operator.equals("class")); - enterScope(); // Visit each statement in the block @@ -788,12 +710,6 @@ public void visit(BlockNode node) { if (i == 0 && skipFirstChild) continue; Node stmt = node.elements.get(i); - // Signal to CompileOperator that the package operator is scoped (package Foo { }) - // so it emits PUSH_PACKAGE instead of SET_PACKAGE. - if (i == 0 && isScopedPackageBlock) { - nextPackageIsScoped = true; - } - // Track line number for this statement (like codegen's setDebugInfoLineNumber) if (stmt != null) { int tokenIndex = stmt.getIndex(); @@ -821,28 +737,13 @@ public void visit(BlockNode node) { } // Save the last statement's result to the outer register BEFORE exiting scope - if (outerResultReg >= 0) { - if (lastResultReg >= 0) { - emit(Opcodes.MOVE); - emitReg(outerResultReg); - emitReg(lastResultReg); - } else { - // Last statement produced no value (e.g. bare block with void semantics). - // Load undef so the register is never null when RETURN reads it. - emit(Opcodes.LOAD_UNDEF); - emitReg(outerResultReg); - } - } - - // Restore runtime current package if this was a scoped package block. - // PUSH_PACKAGE saved InterpreterState.currentPackage; POP_PACKAGE restores it. - // The compile-time package is restored separately by exitScope() via packageStack. - if (isScopedPackageBlock) { - emit(Opcodes.POP_PACKAGE); + if (outerResultReg >= 0 && lastResultReg >= 0) { + emit(Opcodes.MOVE); + emitReg(outerResultReg); + emitReg(lastResultReg); } - // Exit scope restores register state and compile-time pragma frames - // (including packageStack, which handles the compile-time package restore). + // Exit scope restores register state exitScope(); // Set lastResultReg to the outer register (or -1 if VOID context) @@ -913,71 +814,72 @@ public void visit(IdentifierNode node) { // Variable reference String varName = node.name; + // Check if this is a captured variable (with sigil) + // Try common sigils: $, @, % String[] sigils = {"$", "@", "%"}; - - // Check local scope first (my declarations shadow captured vars from outer scope). - // This is critical for eval STRING: if the eval declares "my $x", that new $x - // must shadow any $x captured from the outer scope (which lives at a different - // register index from the outer code's register file). - if (hasVariable(varName)) { - lastResultReg = getVariableRegister(varName); - return; - } - for (String sigil : sigils) { - String varNameWithSigil = sigil + varName; - if (hasVariable(varNameWithSigil)) { - lastResultReg = getVariableRegister(varNameWithSigil); - return; - } - } - - // Not in local scope - check captured variables from outer eval scope. - // capturedVarIndices holds variables captured from the parent InterpretedCode - // (set up by EvalStringHandler). Only fall through to this if no local my - // declaration shadows the name. for (String sigil : sigils) { String varNameWithSigil = sigil + varName; if (capturedVarIndices != null && capturedVarIndices.containsKey(varNameWithSigil)) { + // Captured variable - use its pre-allocated register lastResultReg = capturedVarIndices.get(varNameWithSigil); return; } } - // Not a lexical variable - could be a global or a bareword - // Check for strict subs violation (bareword without sigil) - if (!varName.startsWith("$") && !varName.startsWith("@") && !varName.startsWith("%")) { - // This is a bareword (no sigil) - if (emitterContext != null && emitterContext.symbolTable != null && - emitterContext.symbolTable.isStrictOptionEnabled(Strict.HINT_STRICT_SUBS)) { - throwCompilerException("Bareword \"" + varName + "\" not allowed while \"strict subs\" in use"); + // Check if it's a lexical variable (may have sigil or not) + if (hasVariable(varName)) { + // Lexical variable - already has a register + lastResultReg = getVariableRegister(varName); + } else { + // Try with sigils + boolean found = false; + for (String sigil : sigils) { + String varNameWithSigil = sigil + varName; + if (hasVariable(varNameWithSigil)) { + lastResultReg = getVariableRegister(varNameWithSigil); + found = true; + break; + } } - // Not strict - treat bareword as string literal - int rd = allocateRegister(); - emit(Opcodes.LOAD_STRING); - emitReg(rd); - int strIdx = addToStringPool(varName); - emit(strIdx); - lastResultReg = rd; - return; - } - // Global variable - // Check strict vars before accessing - if (shouldBlockGlobalUnderStrictVars(varName)) { - throwCompilerException("Global symbol \"" + varName + "\" requires explicit package name"); - } + if (!found) { + // Not a lexical variable - could be a global or a bareword + // Check for strict subs violation (bareword without sigil) + if (!varName.startsWith("$") && !varName.startsWith("@") && !varName.startsWith("%")) { + // This is a bareword (no sigil) + if (emitterContext != null && emitterContext.symbolTable != null && + emitterContext.symbolTable.isStrictOptionEnabled(Strict.HINT_STRICT_SUBS)) { + throwCompilerException("Bareword \"" + varName + "\" not allowed while \"strict subs\" in use"); + } + // Not strict - treat bareword as string literal + int rd = allocateRegister(); + emit(Opcodes.LOAD_STRING); + emitReg(rd); + int strIdx = addToStringPool(varName); + emit(strIdx); + lastResultReg = rd; + return; + } - // Strip sigil and normalize name (e.g., "$x" → "main::x") - String bareVarName = varName.substring(1); // Remove sigil - String normalizedName = NameNormalizer.normalizeVariableName(bareVarName, getCurrentPackage()); - int rd = allocateRegister(); - int nameIdx = addToStringPool(normalizedName); + // Global variable + // Check strict vars before accessing + if (shouldBlockGlobalUnderStrictVars(varName)) { + throwCompilerException("Global symbol \"" + varName + "\" requires explicit package name"); + } - emit(Opcodes.LOAD_GLOBAL_SCALAR); - emitReg(rd); - emit(nameIdx); + // Strip sigil and normalize name (e.g., "$x" → "main::x") + String bareVarName = varName.substring(1); // Remove sigil + String normalizedName = NameNormalizer.normalizeVariableName(bareVarName, getCurrentPackage()); + int rd = allocateRegister(); + int nameIdx = addToStringPool(normalizedName); - lastResultReg = rd; + emit(Opcodes.LOAD_GLOBAL_SCALAR); + emitReg(rd); + emit(nameIdx); + + lastResultReg = rd; + } + } } /** @@ -1335,9 +1237,7 @@ void handleCompoundAssignment(BinaryOperatorNode node) { // Global variable - need to load it first isGlobal = true; targetReg = allocateRegister(); - // Strip sigil before normalizing — varName is "$main::c", normalize needs "main::c" - String bareVarName = varName.substring(1); - String normalizedName = NameNormalizer.normalizeVariableName(bareVarName, getCurrentPackage()); + String normalizedName = NameNormalizer.normalizeVariableName(varName, getCurrentPackage()); int nameIdx = addToStringPool(normalizedName); emit(Opcodes.LOAD_GLOBAL_SCALAR); emitReg(targetReg); @@ -1407,7 +1307,7 @@ void handleCompoundAssignment(BinaryOperatorNode node) { throwCompilerException("Global symbol \"" + varName + "\" requires explicit package name"); } - // Strip sigil before normalizing — varName is "$main::c", normalize needs "main::c" + // Strip sigil before normalizing — normalizeVariableName expects bare name String normalizedName = NameNormalizer.normalizeVariableName(varName.substring(1), getCurrentPackage()); int nameIdx = addToStringPool(normalizedName); emit(Opcodes.STORE_GLOBAL_SCALAR); @@ -2293,25 +2193,6 @@ void compileVariableDeclaration(OperatorNode node, String op) { OperatorNode sigilOp = (OperatorNode) node.operand; String sigil = sigilOp.operator; - if (sigil.equals("*") && sigilOp.operand instanceof IdentifierNode) { - // local *glob — save glob state and return same glob object - // Mirrors JVM path: load glob, call DynamicVariableManager.pushLocalVariable(RuntimeGlob) - String globName = NameNormalizer.normalizeVariableName(((IdentifierNode) sigilOp.operand).name, getCurrentPackage()); - int nameIdx = addToStringPool(globName); - - int globReg = allocateRegister(); - emitWithToken(Opcodes.LOAD_GLOB, node.getIndex()); - emitReg(globReg); - emit(nameIdx); - - // Push glob to local variable stack (saves state, returns same object) - emit(Opcodes.PUSH_LOCAL_VARIABLE); - emitReg(globReg); - - lastResultReg = globReg; - return; - } - if (sigil.equals("$") && sigilOp.operand instanceof IdentifierNode) { String varName = "$" + ((IdentifierNode) sigilOp.operand).name; @@ -2605,10 +2486,6 @@ void compileVariableReference(OperatorNode node, String op) { lastResultReg = getVariableRegister(varName); } else { // Global variable - load it - // Check strict vars before loading an undeclared global - if (shouldBlockGlobalUnderStrictVars(varName)) { - throwCompilerException("Global symbol \"" + varName + "\" requires explicit package name"); - } // Use NameNormalizer to properly handle special variables (like $&) // which must always be in the "main" package String globalVarName = varName.substring(1); // Remove $ sigil first @@ -2635,14 +2512,9 @@ void compileVariableReference(OperatorNode node, String op) { block.accept(this); int blockResultReg = lastResultReg; - // Load via symbolic reference — use strict vs non-strict opcode. - // LOAD_SYMBOLIC_SCALAR throws "strict refs" for non-reference strings. - // LOAD_SYMBOLIC_SCALAR_NONSTRICT allows symbolic variable lookup. + // Load via symbolic reference int rd = allocateRegister(); - short symOp = isStrictRefsEnabled() - ? Opcodes.LOAD_SYMBOLIC_SCALAR - : Opcodes.LOAD_SYMBOLIC_SCALAR_NONSTRICT; - emitWithToken(symOp, node.getIndex()); + emitWithToken(Opcodes.LOAD_SYMBOLIC_SCALAR, node.getIndex()); emitReg(rd); emitReg(blockResultReg); @@ -2728,60 +2600,31 @@ void compileVariableReference(OperatorNode node, String op) { operandOp.accept(this); int refReg = lastResultReg; + // Dereference to get the array + // The reference should contain a RuntimeArray + // For @$scalar, we need to dereference it int rd = allocateRegister(); - if (isStrictRefsEnabled()) { - emitWithToken(Opcodes.DEREF_ARRAY, node.getIndex()); - emitReg(rd); - emitReg(refReg); - } else { - int pkgIdx = addToStringPool(getCurrentPackage()); - emitWithToken(Opcodes.DEREF_ARRAY_NONSTRICT, node.getIndex()); - emitReg(rd); - emitReg(refReg); - emit(pkgIdx); - } + emitWithToken(Opcodes.DEREF_ARRAY, node.getIndex()); + emitReg(rd); + emitReg(refReg); lastResultReg = rd; + // Note: We don't check scalar context here because dereferencing + // should return the array itself. The slice or other operation + // will handle scalar context conversion if needed. } else if (node.operand instanceof BlockNode) { // @{ block } - evaluate block and dereference the result + // The block should return an arrayref BlockNode blockNode = (BlockNode) node.operand; blockNode.accept(this); int refReg = lastResultReg; + // Dereference to get the array int rd = allocateRegister(); - if (isStrictRefsEnabled()) { - emitWithToken(Opcodes.DEREF_ARRAY, node.getIndex()); - emitReg(rd); - emitReg(refReg); - } else { - int pkgIdx = addToStringPool(getCurrentPackage()); - emitWithToken(Opcodes.DEREF_ARRAY_NONSTRICT, node.getIndex()); - emitReg(rd); - emitReg(refReg); - emit(pkgIdx); - } + emitWithToken(Opcodes.DEREF_ARRAY, node.getIndex()); + emitReg(rd); + emitReg(refReg); - lastResultReg = rd; - } else if (node.operand instanceof StringNode strNode) { - // @{'name'} — symbolic array reference - int nameReg = allocateRegister(); - int strIdx = addToStringPool(strNode.value); - emit(Opcodes.LOAD_STRING); - emitReg(nameReg); - emit(strIdx); - - int rd = allocateRegister(); - if (isStrictRefsEnabled()) { - emitWithToken(Opcodes.DEREF_ARRAY, node.getIndex()); - emitReg(rd); - emitReg(nameReg); - } else { - int pkgIdx = addToStringPool(getCurrentPackage()); - emitWithToken(Opcodes.DEREF_ARRAY_NONSTRICT, node.getIndex()); - emitReg(rd); - emitReg(nameReg); - emit(pkgIdx); - } lastResultReg = rd; } else { throwCompilerException("Unsupported @ operand: " + node.operand.getClass().getSimpleName()); @@ -2822,17 +2665,9 @@ void compileVariableReference(OperatorNode node, String op) { refOp.accept(this); int scalarReg = lastResultReg; int hashReg = allocateRegister(); - if (isStrictRefsEnabled()) { - emitWithToken(Opcodes.DEREF_HASH, node.getIndex()); - emitReg(hashReg); - emitReg(scalarReg); - } else { - int pkgIdx = addToStringPool(getCurrentPackage()); - emitWithToken(Opcodes.DEREF_HASH_NONSTRICT, node.getIndex()); - emitReg(hashReg); - emitReg(scalarReg); - emit(pkgIdx); - } + emitWithToken(Opcodes.DEREF_HASH, node.getIndex()); + emitReg(hashReg); + emitReg(scalarReg); if (currentCallContext == RuntimeContextType.SCALAR) { int rd = allocateRegister(); emit(Opcodes.ARRAY_SIZE); @@ -2847,44 +2682,15 @@ void compileVariableReference(OperatorNode node, String op) { blockNode.accept(this); int scalarReg = lastResultReg; int hashReg = allocateRegister(); - if (isStrictRefsEnabled()) { - emitWithToken(Opcodes.DEREF_HASH, node.getIndex()); - emitReg(hashReg); - emitReg(scalarReg); - } else { - int pkgIdx = addToStringPool(getCurrentPackage()); - emitWithToken(Opcodes.DEREF_HASH_NONSTRICT, node.getIndex()); - emitReg(hashReg); - emitReg(scalarReg); - emit(pkgIdx); - } - lastResultReg = hashReg; - } else if (node.operand instanceof StringNode strNode) { - // %{'name'} — symbolic hash reference - int nameReg = allocateRegister(); - int strIdx = addToStringPool(strNode.value); - emit(Opcodes.LOAD_STRING); - emitReg(nameReg); - emit(strIdx); - - int hashReg = allocateRegister(); - if (isStrictRefsEnabled()) { - emitWithToken(Opcodes.DEREF_HASH, node.getIndex()); - emitReg(hashReg); - emitReg(nameReg); - } else { - int pkgIdx = addToStringPool(getCurrentPackage()); - emitWithToken(Opcodes.DEREF_HASH_NONSTRICT, node.getIndex()); - emitReg(hashReg); - emitReg(nameReg); - emit(pkgIdx); - } + emitWithToken(Opcodes.DEREF_HASH, node.getIndex()); + emitReg(hashReg); + emitReg(scalarReg); lastResultReg = hashReg; } else { throwCompilerException("Unsupported % operand: " + node.operand.getClass().getSimpleName()); } } else if (op.equals("*")) { - // Glob variable dereference: *x or *{expr} + // Glob variable dereference: *x if (node.operand instanceof IdentifierNode) { IdentifierNode idNode = (IdentifierNode) node.operand; String varName = idNode.name; @@ -2903,28 +2709,6 @@ void compileVariableReference(OperatorNode node, String op) { emitReg(rd); emit(nameIdx); - lastResultReg = rd; - } else if (node.operand instanceof BlockNode || node.operand instanceof StringNode) { - // *{expr} or *{'name'} — dynamic glob via symbolic reference - node.operand.accept(this); - int nameReg = lastResultReg; - - int rd = allocateRegister(); - emitWithToken(Opcodes.LOAD_SYMBOLIC_GLOB, node.getIndex()); - emitReg(rd); - emitReg(nameReg); - - lastResultReg = rd; - } else if (node.operand instanceof OperatorNode) { - // *$ref or **postfix — dereference scalar as glob - node.operand.accept(this); - int scalarReg = lastResultReg; - - int rd = allocateRegister(); - emitWithToken(Opcodes.DEREF_GLOB, node.getIndex()); - emitReg(rd); - emitReg(scalarReg); - lastResultReg = rd; } else { throwCompilerException("Unsupported * operand: " + node.operand.getClass().getSimpleName()); @@ -3732,7 +3516,7 @@ public void visit(For3Node node) { if (node.body != null) { node.body.accept(this); } - lastResultReg = -1; // Block returns empty (void semantics) + lastResultReg = -1; // Block returns empty } finally { // Exit scope to clean up lexical variables exitScope(); diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 799861add..b14f13fe8 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -1376,27 +1376,22 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c case Opcodes.DEREF: { // Dereference: rd = $$rs (scalar reference dereference) - // Always call scalarDeref() — throws "Not a SCALAR reference" for - // non-reference types (IO, FORMAT, etc.), matching Perl semantics. + // Can receive RuntimeScalar or RuntimeList int rd = bytecode[pc++]; int rs = bytecode[pc++]; RuntimeBase value = registers[rs]; + // Only dereference if it's a RuntimeScalar with REFERENCE type if (value instanceof RuntimeScalar) { - RuntimeScalar sv = (RuntimeScalar) value; - // Call scalarDeref() for scalar refs, undef, non-ref types (strings, globs, etc.) - // Pass through non-scalar reference types (array/hash/code/regex refs) — - // those are handled by the JVM compiler as non-scalar refs and should not - // throw here (decl-refs.t uses $$arrayref in no-strict context). - if (sv.type == RuntimeScalarType.ARRAYREFERENCE - || sv.type == RuntimeScalarType.HASHREFERENCE - || sv.type == RuntimeScalarType.CODE - || sv.type == RuntimeScalarType.REGEX) { - registers[rd] = sv; // pass through non-scalar refs + RuntimeScalar scalar = (RuntimeScalar) value; + if (scalar.type == RuntimeScalarType.REFERENCE) { + registers[rd] = scalar.scalarDeref(); } else { - registers[rd] = sv.scalarDeref(); + // Non-reference scalar, just copy + registers[rd] = value; } } else { + // RuntimeList or other types, pass through registers[rd] = value; } break; @@ -1779,11 +1774,6 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c case Opcodes.SELECT_OP: case Opcodes.LOAD_GLOB: case Opcodes.SLEEP_OP: - case Opcodes.LOAD_SYMBOLIC_GLOB: - case Opcodes.DEREF_GLOB: - case Opcodes.DEREF_HASH_NONSTRICT: - case Opcodes.DEREF_ARRAY_NONSTRICT: - case Opcodes.DEREF_NONSTRICT: pc = executeSpecialIO(opcode, bytecode, pc, registers, code); break; @@ -1858,72 +1848,57 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c break; case Opcodes.STORE_SYMBOLIC_SCALAR: { - // Strict symbolic scalar store: throws for string refs, allows REFERENCE. + // Store via symbolic reference: GlobalVariable.getGlobalVariable(nameReg.toString()).set(valueReg) // Format: STORE_SYMBOLIC_SCALAR nameReg valueReg int nameReg = bytecode[pc++]; int valueReg = bytecode[pc++]; + // Get the variable name from the name register RuntimeScalar nameScalar = (RuntimeScalar) registers[nameReg]; - // scalarDeref() throws "strict refs" for STRING and acts as deref for REFERENCE - RuntimeScalar targetVar = nameScalar.scalarDeref(); - targetVar.set(registers[valueReg]); - break; - } - - case Opcodes.STORE_SYMBOLIC_SCALAR_NONSTRICT: { - // Non-strict symbolic scalar store: allows string-keyed global variable store. - // Format: STORE_SYMBOLIC_SCALAR_NONSTRICT nameReg valueReg - int nameReg = bytecode[pc++]; - int valueReg = bytecode[pc++]; + String varName = nameScalar.toString(); - RuntimeScalar nameScalar = (RuntimeScalar) registers[nameReg]; + // Normalize the variable name to include package prefix if needed + // This is important for ${label:var} cases where "colon" becomes "main::colon" + String normalizedName = NameNormalizer.normalizeVariableName( + varName, + "main" // Use main package as default for symbolic references + ); - if (nameScalar.type == RuntimeScalarType.REFERENCE) { - // ${\ ref} = value — dereference then assign - nameScalar.scalarDeref().set(registers[valueReg]); - } else { - // ${"varname"} = value — symbolic reference store - String normalizedName = NameNormalizer.normalizeVariableName( - nameScalar.toString(), - code.compilePackage - ); - GlobalVariable.getGlobalVariable(normalizedName).set(registers[valueReg]); - } + // Get the global variable and set its value + RuntimeScalar globalVar = GlobalVariable.getGlobalVariable(normalizedName); + RuntimeBase value = registers[valueReg]; + globalVar.set(value); break; } case Opcodes.LOAD_SYMBOLIC_SCALAR: { - // Strict symbolic scalar load: rd = ${\ref} only. - // Throws "strict refs" for strings, matching Perl strict semantics. + // Load via symbolic reference: rd = GlobalVariable.getGlobalVariable(nameReg.toString()).get() + // OR dereference if nameReg contains a scalar reference // Format: LOAD_SYMBOLIC_SCALAR rd nameReg int rd = bytecode[pc++]; int nameReg = bytecode[pc++]; - // scalarDeref() handles both REFERENCE (dereference) and STRING - // (throws "Can't use string ... as a SCALAR ref while strict refs in use") - registers[rd] = ((RuntimeScalar) registers[nameReg]).scalarDeref(); - break; - } - - case Opcodes.LOAD_SYMBOLIC_SCALAR_NONSTRICT: { - // Non-strict symbolic scalar load: rd = ${"varname"} or ${\ref}. - // Allows string-keyed global variable lookup (no strict refs). - // Format: LOAD_SYMBOLIC_SCALAR_NONSTRICT rd nameReg - int rd = bytecode[pc++]; - int nameReg = bytecode[pc++]; + // Get the value from the name register RuntimeScalar nameScalar = (RuntimeScalar) registers[nameReg]; + // Check if it's a scalar reference - if so, dereference it if (nameScalar.type == RuntimeScalarType.REFERENCE) { - // ${\ref} — dereference the scalar reference + // This is ${\ref} - dereference the reference registers[rd] = nameScalar.scalarDeref(); } else { - // ${"varname"} — symbolic reference: look up global by name + // This is ${"varname"} - symbolic reference to variable String varName = nameScalar.toString(); + + // Normalize the variable name to include package prefix if needed + // This is important for ${label:var} cases where "colon" becomes "main::colon" String normalizedName = NameNormalizer.normalizeVariableName( varName, - code.compilePackage // Use compile-time package for name resolution + "main" // Use main package as default for symbolic references ); - registers[rd] = GlobalVariable.getGlobalVariable(normalizedName); + + // Get the global variable and load its value + RuntimeScalar globalVar = GlobalVariable.getGlobalVariable(normalizedName); + registers[rd] = globalVar; } break; } @@ -2066,20 +2041,13 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c case Opcodes.PUSH_PACKAGE: { // Scoped package block entry: package Foo { ... // Save current package via DynamicVariableManager so it is restored - // by POP_PACKAGE when the scoped block exits. + // automatically when the scope exits via POP_LOCAL_LEVEL. int nameIdx = bytecode[pc++]; DynamicVariableManager.pushLocalVariable(InterpreterState.currentPackage.get()); InterpreterState.currentPackage.get().set(code.stringPool[nameIdx]); break; } - case Opcodes.POP_PACKAGE: { - // Scoped package block exit: closing } of package Foo { ... - // Restore the previous package name saved by PUSH_PACKAGE. - DynamicVariableManager.popToLocalLevel(DynamicVariableManager.getLocalLevel() - 1); - break; - } - default: // Unknown opcode int opcodeInt = opcode & 0xFF; @@ -2251,19 +2219,17 @@ private static int executeTypeOps(int opcode, int[] bytecode, int pc, int rs = bytecode[pc++]; RuntimeBase value = registers[rs]; - // Call scalarDeref() for scalar refs, undef, non-ref types (strings, globs, etc.) - // Pass through non-scalar reference types (array/hash/code/regex refs). + // Only dereference if it's a RuntimeScalar with REFERENCE type if (value instanceof RuntimeScalar) { - RuntimeScalar sv = (RuntimeScalar) value; - if (sv.type == RuntimeScalarType.ARRAYREFERENCE - || sv.type == RuntimeScalarType.HASHREFERENCE - || sv.type == RuntimeScalarType.CODE - || sv.type == RuntimeScalarType.REGEX) { - registers[rd] = sv; // pass through non-scalar refs + RuntimeScalar scalar = (RuntimeScalar) value; + if (scalar.type == RuntimeScalarType.REFERENCE) { + registers[rd] = scalar.scalarDeref(); } else { - registers[rd] = sv.scalarDeref(); + // Non-reference scalar, just copy + registers[rd] = value; } } else { + // RuntimeList or other types, pass through registers[rd] = value; } return pc; @@ -3133,16 +3099,6 @@ private static int executeSpecialIO(int opcode, int[] bytecode, int pc, return SlowOpcodeHandler.executeLoadGlob(bytecode, pc, registers, code); case Opcodes.SLEEP_OP: return SlowOpcodeHandler.executeSleep(bytecode, pc, registers); - case Opcodes.LOAD_SYMBOLIC_GLOB: - return SlowOpcodeHandler.executeLoadSymbolicGlob(bytecode, pc, registers); - case Opcodes.DEREF_GLOB: - return SlowOpcodeHandler.executeDerefGlob(bytecode, pc, registers); - case Opcodes.DEREF_HASH_NONSTRICT: - return SlowOpcodeHandler.executeDerefHashNonStrict(bytecode, pc, registers, code); - case Opcodes.DEREF_ARRAY_NONSTRICT: - return SlowOpcodeHandler.executeDerefArrayNonStrict(bytecode, pc, registers, code); - case Opcodes.DEREF_NONSTRICT: - return SlowOpcodeHandler.executeDerefNonStrict(bytecode, pc, registers, code); default: throw new RuntimeException("Unknown special I/O opcode: " + opcode); } diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java index 2657fc44e..3ee669681 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java @@ -651,11 +651,8 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, node.right.accept(bytecodeCompiler); int valueReg = bytecodeCompiler.lastResultReg; - // Use strict vs non-strict store opcode depending on current strict refs - short storeOp = bytecodeCompiler.isStrictRefsEnabled() - ? Opcodes.STORE_SYMBOLIC_SCALAR - : Opcodes.STORE_SYMBOLIC_SCALAR_NONSTRICT; - bytecodeCompiler.emit(storeOp); + // Use STORE_SYMBOLIC_SCALAR to store via symbolic reference + bytecodeCompiler.emit(Opcodes.STORE_SYMBOLIC_SCALAR); bytecodeCompiler.emitReg(nameReg); bytecodeCompiler.emitReg(valueReg); @@ -671,11 +668,8 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, node.right.accept(bytecodeCompiler); int valueReg = bytecodeCompiler.lastResultReg; - // Use strict vs non-strict store opcode depending on current strict refs - short storeOp2 = bytecodeCompiler.isStrictRefsEnabled() - ? Opcodes.STORE_SYMBOLIC_SCALAR - : Opcodes.STORE_SYMBOLIC_SCALAR_NONSTRICT; - bytecodeCompiler.emit(storeOp2); + // Use STORE_SYMBOLIC_SCALAR to store via symbolic reference + bytecodeCompiler.emit(Opcodes.STORE_SYMBOLIC_SCALAR); bytecodeCompiler.emitReg(nameReg); bytecodeCompiler.emitReg(valueReg); @@ -902,27 +896,6 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.emitReg(globReg); bytecodeCompiler.emitReg(valueReg); - bytecodeCompiler.lastResultReg = globReg; - } else if (leftOp.operator.equals("*") && - (leftOp.operand instanceof BlockNode || - leftOp.operand instanceof OperatorNode || - leftOp.operand instanceof StringNode)) { - // Dynamic typeglob assignment: *{"Pkg::name"} = value, *$ref = value, *{'name'} = value - // Evaluate the expression to get the glob name at runtime - leftOp.operand.accept(bytecodeCompiler); - int nameReg = bytecodeCompiler.lastResultReg; - - // Load the glob via symbolic reference - int globReg = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emitWithToken(Opcodes.LOAD_SYMBOLIC_GLOB, node.getIndex()); - bytecodeCompiler.emitReg(globReg); - bytecodeCompiler.emitReg(nameReg); - - // Store value to glob - bytecodeCompiler.emit(Opcodes.STORE_GLOB); - bytecodeCompiler.emitReg(globReg); - bytecodeCompiler.emitReg(valueReg); - bytecodeCompiler.lastResultReg = globReg; } else if (leftOp.operator.equals("pos")) { // pos($var) = value - lvalue assignment to regex position diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index 07c455d9a..21a5ea90d 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -66,7 +66,7 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode Boolean isClassAnnotation = (Boolean) node.getAnnotation("isClass"); boolean isClass = op.equals("class") || (isClassAnnotation != null && isClassAnnotation); - // Check if there's a version associated with this package and set $Package::VERSION. + // Check if there's a version associated with this package and set $Package::VERSION String version = bytecodeCompiler.symbolTable.getPackageVersion(packageName); if (version != null) { // Set $PackageName::VERSION at compile time using GlobalVariable @@ -75,9 +75,8 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode .set(new RuntimeScalar(version)); } - // Update the current package/class in symbol table (compile-time tracking). - // Mirrors JVM handlePackageOperator which calls ctx.symbolTable.setCurrentPackage. - bytecodeCompiler.setCompilePackageWithClass(packageName, isClass); + // Update the current package/class in symbol table (compile-time tracking) + bytecodeCompiler.symbolTable.setCurrentPackage(packageName, isClass); // Register as Perl 5.38+ class for proper stringification if needed if (isClass) { @@ -86,13 +85,12 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode // Emit runtime package tracking opcode so caller() and eval STRING work. // Scoped blocks (package Foo { }) use PUSH_PACKAGE so DynamicVariableManager - // can restore the previous package when POP_PACKAGE is emitted at block exit. + // can restore the previous package when the scope exits. // Non-scoped (package Foo;) use SET_PACKAGE which just overwrites. - // nextPackageIsScoped is set by visit(BlockNode) for scoped package blocks. + boolean isScoped = Boolean.TRUE.equals(node.getAnnotation("isScoped")); int nameIdx = bytecodeCompiler.addToStringPool(packageName); - if (bytecodeCompiler.nextPackageIsScoped) { + if (isScoped) { bytecodeCompiler.emit(Opcodes.PUSH_PACKAGE); - bytecodeCompiler.nextPackageIsScoped = false; // consume the flag } else { bytecodeCompiler.emit(Opcodes.SET_PACKAGE); } @@ -830,34 +828,17 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode } else if (op.equals("eval")) { // eval $string; if (node.operand != null) { - // Evaluate eval operand (the code string) in SCALAR context - int savedContext = bytecodeCompiler.currentCallContext; - bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; + // Evaluate eval operand (the code string) node.operand.accept(bytecodeCompiler); - bytecodeCompiler.currentCallContext = savedContext; int stringReg = bytecodeCompiler.lastResultReg; - // If operand produced no result (e.g. bare block with void semantics), - // substitute undef so the eval string is empty - if (stringReg < 0) { - stringReg = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.LOAD_UNDEF); - bytecodeCompiler.emitReg(stringReg); - } - // Allocate register for result int rd = bytecodeCompiler.allocateRegister(); - // Emit direct opcode EVAL_STRING with call-site strict/feature/warning flags - // so EvalStringHandler inherits the pragmas in effect at the eval call site - // (not just the end-of-compilation snapshot in InterpretedCode) - int callSiteStrictOptions = bytecodeCompiler.getCurrentStrictOptions(); - int callSiteFeatureFlags = bytecodeCompiler.getCurrentFeatureFlags(); + // Emit direct opcode EVAL_STRING bytecodeCompiler.emitWithToken(Opcodes.EVAL_STRING, node.getIndex()); bytecodeCompiler.emitReg(rd); bytecodeCompiler.emitReg(stringReg); - bytecodeCompiler.emitInt(callSiteStrictOptions); - bytecodeCompiler.emitInt(callSiteFeatureFlags); bytecodeCompiler.lastResultReg = rd; } else { @@ -1688,19 +1669,13 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode bytecodeCompiler.throwCompilerException("open requires arguments"); } - // Compile the filehandle argument (first arg) as an lvalue register - // We must NOT push it through ARRAY_PUSH (which copies via addToArray), - // because IOOperator.open needs to call fileHandle.set() on the actual lvalue. - argsList.elements.get(0).accept(bytecodeCompiler); - int fhReg = bytecodeCompiler.lastResultReg; - - // Compile remaining arguments into a list (mode, filename/ref, ...) + // Compile all arguments into a list int argsReg = bytecodeCompiler.allocateRegister(); bytecodeCompiler.emit(Opcodes.NEW_ARRAY); bytecodeCompiler.emitReg(argsReg); - for (int i = 1; i < argsList.elements.size(); i++) { - argsList.elements.get(i).accept(bytecodeCompiler); + for (Node arg : argsList.elements) { + arg.accept(bytecodeCompiler); int elemReg = bytecodeCompiler.lastResultReg; bytecodeCompiler.emit(Opcodes.ARRAY_PUSH); @@ -1708,13 +1683,11 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode bytecodeCompiler.emitReg(elemReg); } - // Call open: OPEN rd ctx fhReg argsReg - // fhReg is the actual lvalue register for the filehandle (written back directly) + // Call open with context and args int rd = bytecodeCompiler.allocateRegister(); bytecodeCompiler.emit(Opcodes.OPEN); bytecodeCompiler.emitReg(rd); bytecodeCompiler.emit(bytecodeCompiler.currentCallContext); - bytecodeCompiler.emitReg(fhReg); bytecodeCompiler.emitReg(argsReg); bytecodeCompiler.lastResultReg = rd; diff --git a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java index 09a96dc99..13db3320e 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java @@ -49,9 +49,7 @@ public static RuntimeScalar evalString(String perlCode, InterpretedCode currentCode, RuntimeBase[] registers, String sourceName, - int sourceLine, - int callSiteStrictOptions, - int callSiteFeatureFlags) { + int sourceLine) { try { // Step 1: Clear $@ at start of eval GlobalVariable.getGlobalVariable("main::@").set(""); @@ -67,26 +65,23 @@ public static RuntimeScalar evalString(String perlCode, opts.fileName = sourceName + " (eval)"; ScopedSymbolTable symbolTable = new ScopedSymbolTable(); - // Inherit lexical pragma flags from the call site (not end-of-compilation snapshot) - // callSiteStrictOptions/callSiteFeatureFlags are embedded in the bytecode at the eval - // call site, capturing the exact pragmas in effect at that point (e.g. inside a - // "no strict 'refs'" block). Fall back to currentCode snapshot if not available. - symbolTable.strictOptionsStack.pop(); - symbolTable.strictOptionsStack.push(callSiteStrictOptions); - symbolTable.featureFlagsStack.pop(); - symbolTable.featureFlagsStack.push(callSiteFeatureFlags); + // 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 runtime package from InterpreterState.currentPackage. - // This is the package active at the eval call site at runtime, which is what - // Perl's eval STRING semantics require — e.g. inside "package Foo { eval '...' }" - // the eval sees package Foo, and __PACKAGE__ inside it returns "Foo". - // PUSH_PACKAGE already updates InterpreterState.currentPackage at runtime when - // entering a scoped package block, so this correctly tracks nested packages. - String compilePackage = InterpreterState.currentPackage.get().toString(); + // 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); ErrorMessageUtil errorUtil = new ErrorMessageUtil(sourceName, tokens); @@ -144,10 +139,7 @@ public static RuntimeScalar evalString(String perlCode, // Skip non-Perl values (like Iterator objects from for loops) // Only capture actual Perl variables: Scalar, Array, Hash, Code if (value == null) { - // Substitute undef for null registers — a null means the variable - // was declared but not yet assigned; capture as undef scalar so - // the eval's register is never null (which would crash on access). - value = new RuntimeScalar(); + // Null is fine - capture it } else if (value instanceof RuntimeScalar) { // Check if the scalar contains an Iterator (used by for loops) RuntimeScalar scalar = (RuntimeScalar) value; diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java index a83440c6a..d90e9619d 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java @@ -730,28 +730,6 @@ public String disassemble() { int keyReg = bytecode[pc++]; sb.append("GLOB_SLOT_GET r").append(rd).append(" = r").append(globReg2).append("{r").append(keyReg).append("}\n"); break; - case Opcodes.LOAD_SYMBOLIC_GLOB: - rd = bytecode[pc++]; - rs1 = bytecode[pc++]; - sb.append("LOAD_SYMBOLIC_GLOB r").append(rd).append(" = getGlobalIO(r").append(rs1).append(")\n"); - break; - case Opcodes.DEREF_GLOB: - rd = bytecode[pc++]; - rs1 = bytecode[pc++]; - sb.append("DEREF_GLOB r").append(rd).append(" = r").append(rs1).append(".globDeref()\n"); - break; - case Opcodes.DEREF_HASH_NONSTRICT: - rd = bytecode[pc++]; - rs1 = bytecode[pc++]; - rs2 = bytecode[pc++]; - sb.append("DEREF_HASH_NONSTRICT r").append(rd).append(" = r").append(rs1).append(".hashDerefNonStrict(pool[").append(rs2).append("])\n"); - break; - case Opcodes.DEREF_ARRAY_NONSTRICT: - rd = bytecode[pc++]; - rs1 = bytecode[pc++]; - rs2 = bytecode[pc++]; - sb.append("DEREF_ARRAY_NONSTRICT r").append(rd).append(" = r").append(rs1).append(".arrayDerefNonStrict(pool[").append(rs2).append("])\n"); - break; case Opcodes.SPRINTF: rd = bytecode[pc++]; int formatReg = bytecode[pc++]; @@ -788,9 +766,8 @@ public String disassemble() { case Opcodes.OPEN: rd = bytecode[pc++]; int openCtx = bytecode[pc++]; - int openFhReg = bytecode[pc++]; int openArgs = bytecode[pc++]; - sb.append("OPEN r").append(rd).append(" = open(ctx=").append(openCtx).append(", fh=r").append(openFhReg).append(", args=r").append(openArgs).append(")\n"); + sb.append("OPEN r").append(rd).append(" = open(ctx=").append(openCtx).append(", r").append(openArgs).append(")\n"); break; case Opcodes.READLINE: rd = bytecode[pc++]; @@ -886,24 +863,6 @@ public String disassemble() { rs = bytecode[pc++]; sb.append("DEREF r").append(rd).append(" = ${r").append(rs).append("}\n"); break; - case Opcodes.DEREF_NONSTRICT: { - rd = bytecode[pc++]; - rs = bytecode[pc++]; - int derefNsPkgIdx = bytecode[pc++]; - sb.append("DEREF_NONSTRICT r").append(rd).append(" = ${r").append(rs) - .append("} pkg=").append(stringPool[derefNsPkgIdx]).append("\n"); - break; - } - case Opcodes.LOAD_SYMBOLIC_SCALAR_NONSTRICT: - rd = bytecode[pc++]; - rs = bytecode[pc++]; - sb.append("LOAD_SYMBOLIC_SCALAR_NONSTRICT r").append(rd).append(" = ${r").append(rs).append("}\n"); - break; - case Opcodes.STORE_SYMBOLIC_SCALAR_NONSTRICT: - rd = bytecode[pc++]; - rs = bytecode[pc++]; - sb.append("STORE_SYMBOLIC_SCALAR_NONSTRICT ${r").append(rd).append("} = r").append(rs).append("\n"); - break; case Opcodes.GET_TYPE: rd = bytecode[pc++]; rs = bytecode[pc++]; @@ -1454,17 +1413,8 @@ public String disassemble() { break; // GENERATED_DISASM_END - case Opcodes.SET_PACKAGE: - sb.append("SET_PACKAGE ").append(stringPool[bytecode[pc++]]).append("\n"); - break; - case Opcodes.PUSH_PACKAGE: - sb.append("PUSH_PACKAGE ").append(stringPool[bytecode[pc++]]).append("\n"); - break; - case Opcodes.POP_PACKAGE: - sb.append("POP_PACKAGE\n"); - break; default: - sb.append("UNKNOWN(").append(opcode).append(")\n"); + sb.append("UNKNOWN(").append(opcode & 0xFF).append(")\n"); break; } } diff --git a/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java b/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java index efd8ab846..b9c5cc77c 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java +++ b/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java @@ -369,16 +369,12 @@ public static int executeStringBitwiseXorAssign(int[] bytecode, int pc, RuntimeB /** * Execute bitwise AND binary operation. * Format: BITWISE_AND_BINARY rd rs1 rs2 - * - * Uses the context-sensitive bitwiseAnd() (not bitwiseAndBinary()) to match - * the JVM path: if operands are non-numeric strings, dispatches to string ops. - * bitwiseAndBinary() is only for the explicit "binary&" operator. */ public static int executeBitwiseAndBinary(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs1 = bytecode[pc++]; int rs2 = bytecode[pc++]; - registers[rd] = BitwiseOperators.bitwiseAnd( + registers[rd] = BitwiseOperators.bitwiseAndBinary( (RuntimeScalar) registers[rs1], (RuntimeScalar) registers[rs2] ); @@ -388,16 +384,12 @@ public static int executeBitwiseAndBinary(int[] bytecode, int pc, RuntimeBase[] /** * Execute bitwise OR binary operation. * Format: BITWISE_OR_BINARY rd rs1 rs2 - * - * Uses the context-sensitive bitwiseOr() (not bitwiseOrBinary()) to match - * the JVM path: if operands are non-numeric strings, dispatches to string ops. - * bitwiseOrBinary() is only for the explicit "binary|" operator. */ public static int executeBitwiseOrBinary(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs1 = bytecode[pc++]; int rs2 = bytecode[pc++]; - registers[rd] = BitwiseOperators.bitwiseOr( + registers[rd] = BitwiseOperators.bitwiseOrBinary( (RuntimeScalar) registers[rs1], (RuntimeScalar) registers[rs2] ); @@ -407,16 +399,12 @@ public static int executeBitwiseOrBinary(int[] bytecode, int pc, RuntimeBase[] r /** * Execute bitwise XOR binary operation. * Format: BITWISE_XOR_BINARY rd rs1 rs2 - * - * Uses the context-sensitive bitwiseXor() (not bitwiseXorBinary()) to match - * the JVM path: if operands are non-numeric strings, dispatches to string ops. - * bitwiseXorBinary() is only for the explicit "binary^" operator. */ public static int executeBitwiseXorBinary(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs1 = bytecode[pc++]; int rs2 = bytecode[pc++]; - registers[rd] = BitwiseOperators.bitwiseXor( + registers[rd] = BitwiseOperators.bitwiseXorBinary( (RuntimeScalar) registers[rs1], (RuntimeScalar) registers[rs2] ); @@ -713,28 +701,14 @@ public static int executePostAutoDecrement(int[] bytecode, int pc, RuntimeBase[] /** * Execute open operation. - * Format: OPEN rd ctx fhReg argsReg - * - * fhReg is the actual lvalue register for the filehandle. IOOperator.open calls - * fileHandle.set() on args[0], so we pass registers[fhReg] directly (not a copy - * from ARRAY_PUSH which would call addToArray -> new RuntimeScalar(this)). - * After the call, registers[fhReg] has been updated in place by set(). + * Format: OPEN rd ctx argsReg */ public static int executeOpen(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int ctx = bytecode[pc++]; - int fhReg = bytecode[pc++]; int argsReg = bytecode[pc++]; RuntimeArray argsArray = (RuntimeArray) registers[argsReg]; - - // Build varargs with the actual fh lvalue as args[0], then the rest - RuntimeBase fhLvalue = registers[fhReg]; - RuntimeBase[] argsVarargs = new RuntimeBase[argsArray.elements.size() + 1]; - argsVarargs[0] = fhLvalue; - for (int i = 0; i < argsArray.elements.size(); i++) { - argsVarargs[i + 1] = argsArray.elements.get(i); - } - + RuntimeBase[] argsVarargs = argsArray.elements.toArray(new RuntimeBase[0]); registers[rd] = IOOperator.open(ctx, argsVarargs); return pc; } diff --git a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java index c5b359edd..559e67830 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java @@ -1071,42 +1071,5 @@ public class Opcodes { * Effect: Restores previous packageName */ public static final short POP_PACKAGE = 308; - /** Load glob via symbolic reference: rd = GlobalVariable.getGlobalIO(nameReg.toString()) - * Format: LOAD_SYMBOLIC_GLOB rd nameReg */ - public static final short LOAD_SYMBOLIC_GLOB = 333; - - /** Dereference scalar as glob: rd = scalarReg.globDeref() - * Format: DEREF_GLOB rd scalarReg */ - public static final short DEREF_GLOB = 334; - - /** Dereference scalar as hash (no strict refs): rd = scalarReg.hashDerefNonStrict(pkg) - * Format: DEREF_HASH_NONSTRICT rd scalarReg packageIdx */ - public static final short DEREF_HASH_NONSTRICT = 335; - - /** Dereference scalar as array (no strict refs): rd = scalarReg.arrayDerefNonStrict(pkg) - * Format: DEREF_ARRAY_NONSTRICT rd scalarReg packageIdx */ - public static final short DEREF_ARRAY_NONSTRICT = 336; - - /** Dereference scalar (no strict refs): rd = scalarReg.scalarDerefNonStrict(pkg) - * Format: DEREF_NONSTRICT rd scalarReg packageIdx */ - public static final short DEREF_NONSTRICT = 337; - - /** Load via symbolic reference (strict refs): throws if nameReg is a string. - * Allows REFERENCE type (${\ ref}). Same format as LOAD_SYMBOLIC_SCALAR. - * Format: LOAD_SYMBOLIC_SCALAR rd nameReg (strict — already the default opcode 232) */ - // Note: LOAD_SYMBOLIC_SCALAR (232) is the strict variant. - - /** Load via symbolic reference (no strict refs): allows string-keyed global lookup. - * Format: LOAD_SYMBOLIC_SCALAR_NONSTRICT rd nameReg */ - public static final short LOAD_SYMBOLIC_SCALAR_NONSTRICT = 338; - - /** Store via symbolic reference (strict refs): throws if nameReg is a string. - * Format: STORE_SYMBOLIC_SCALAR rd nameReg */ - // Note: STORE_SYMBOLIC_SCALAR (231) is the strict variant. - - /** Store via symbolic reference (no strict refs): allows string-keyed global store. - * Format: STORE_SYMBOLIC_SCALAR_NONSTRICT nameReg valueReg */ - public static final short STORE_SYMBOLIC_SCALAR_NONSTRICT = 339; - private Opcodes() {} // Utility class - no instantiation } diff --git a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java index 98841ee3e..f99c19fcc 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java @@ -240,9 +240,6 @@ public static int executeEvalString( int rd = bytecode[pc++]; int stringReg = bytecode[pc++]; - // Read call-site strict/feature flags embedded by CompileOperator at the eval site - int callSiteStrictOptions = bytecode[pc++]; - int callSiteFeatureFlags = bytecode[pc++]; // Get the code string - handle both RuntimeScalar and RuntimeList (from string interpolation) RuntimeBase codeValue = registers[stringReg]; @@ -258,12 +255,10 @@ public static int executeEvalString( // Call EvalStringHandler to parse, compile, and execute RuntimeScalar result = EvalStringHandler.evalString( perlCode, - code, // Current InterpretedCode for context - registers, // Current registers for variable access + code, // Current InterpretedCode for context + registers, // Current registers for variable access code.sourceName, - code.sourceLine, - callSiteStrictOptions, // Strict flags at the eval call site - callSiteFeatureFlags // Feature flags at the eval call site + code.sourceLine ); registers[rd] = result; @@ -295,108 +290,6 @@ public static int executeSelect( return pc; } - /** - * DEREF_HASH_NONSTRICT: rd = scalarReg.hashDerefNonStrict(pkg) - * Format: [DEREF_HASH_NONSTRICT] [rd] [scalarReg] [packageIdx] - * Effect: Dereferences a scalar as a hash with no-strict-refs semantics - */ - public static int executeDerefHashNonStrict( - int[] bytecode, - int pc, - RuntimeBase[] registers, - InterpretedCode code) { - - int rd = bytecode[pc++]; - int scalarReg = bytecode[pc++]; - int packageIdx = bytecode[pc++]; - - String packageName = code.stringPool[packageIdx]; - RuntimeScalar scalar = (RuntimeScalar) registers[scalarReg]; - registers[rd] = scalar.hashDerefNonStrict(packageName); - return pc; - } - - /** - * DEREF_ARRAY_NONSTRICT: rd = scalarReg.arrayDerefNonStrict(pkg) - * Format: [DEREF_ARRAY_NONSTRICT] [rd] [scalarReg] [packageIdx] - * Effect: Dereferences a scalar as an array with no-strict-refs semantics - */ - public static int executeDerefArrayNonStrict( - int[] bytecode, - int pc, - RuntimeBase[] registers, - InterpretedCode code) { - - int rd = bytecode[pc++]; - int scalarReg = bytecode[pc++]; - int packageIdx = bytecode[pc++]; - - String packageName = code.stringPool[packageIdx]; - RuntimeScalar scalar = (RuntimeScalar) registers[scalarReg]; - registers[rd] = scalar.arrayDerefNonStrict(packageName); - return pc; - } - - /** - * DEREF_NONSTRICT: rd = scalarReg.scalarDerefNonStrict(pkg) - * Format: [DEREF_NONSTRICT] [rd] [scalarReg] [packageIdx] - * Effect: Dereferences a scalar as a scalar with no-strict-refs semantics ($$ref or symbolic ref) - */ - public static int executeDerefNonStrict( - int[] bytecode, - int pc, - RuntimeBase[] registers, - InterpretedCode code) { - - int rd = bytecode[pc++]; - int scalarReg = bytecode[pc++]; - int packageIdx = bytecode[pc++]; - - String packageName = code.stringPool[packageIdx]; - RuntimeScalar scalar = (RuntimeScalar) registers[scalarReg]; - registers[rd] = scalar.scalarDerefNonStrict(packageName); - return pc; - } - - /** - * DEREF_GLOB: rd = scalarReg.globDeref() - * Format: [DEREF_GLOB] [rd] [scalarReg] - * Effect: Dereferences a scalar as a glob (** postfix deref) - */ - public static int executeDerefGlob( - int[] bytecode, - int pc, - RuntimeBase[] registers) { - - int rd = bytecode[pc++]; - int scalarReg = bytecode[pc++]; - - RuntimeScalar scalar = (RuntimeScalar) registers[scalarReg]; - registers[rd] = scalar.globDeref(); - return pc; - } - - /** - * LOAD_SYMBOLIC_GLOB: rd = getGlobalIO(nameReg.toString()) - * Format: [LOAD_SYMBOLIC_GLOB] [rd] [nameReg] - * Effect: Loads a glob via a runtime string expression (e.g. *{"Pkg::name"}) - */ - public static int executeLoadSymbolicGlob( - int[] bytecode, - int pc, - RuntimeBase[] registers) { - - int rd = bytecode[pc++]; - int nameReg = bytecode[pc++]; - - // Normalize the name with the current package (e.g. "mysub" -> "main::mysub") - String rawName = registers[nameReg].toString(); - String pkg = InterpreterState.currentPackage.get().toString(); - String globName = NameNormalizer.normalizeVariableName(rawName, pkg); - registers[rd] = GlobalVariable.getGlobalIO(globName); - return pc; - } - /** * SLOW_LOAD_GLOB: rd = getGlobalIO(name) * Format: [SLOW_LOAD_GLOB] [rd] [name_idx] @@ -884,8 +777,10 @@ public static int executeListSliceFrom( int rd = bytecode[pc++]; int listReg = bytecode[pc++]; - // Read startIndex as a single int (emitted by CompileAssignment via emitInt) - int startIndex = bytecode[pc++]; + // Read startIndex as 2 shorts (int = high 16 bits + low 16 bits) + int high = bytecode[pc++] & 0xFFFF; + int low = bytecode[pc++] & 0xFFFF; + int startIndex = (high << 16) | low; RuntimeBase listBase = registers[listReg]; RuntimeList sourceList; @@ -1028,18 +923,12 @@ public static int executeGlobSlotGet( // Use runtime current package — correct for both regular code and eval STRING String pkg = InterpreterState.currentPackage.get().toString(); - RuntimeScalar result; - if (globBase instanceof RuntimeGlob globObj) { - // Direct glob — access slot via a scalar wrapper that holds the glob reference - // RuntimeGlob.hashDerefGetNonStrict is not available directly; use scalar() to get - // a RuntimeScalar of type GLOB, then call hashDerefGetNonStrict on it. - // But scalar() on a RuntimeGlob returns a GLOB-typed scalar that delegates correctly. - result = globObj.scalar().hashDerefGetNonStrict(key, pkg); - } else { - // Already a scalar (e.g. from a variable holding a glob) - result = globBase.scalar().hashDerefGetNonStrict(key, pkg); - } - registers[rd] = result; + // Convert to scalar if needed + RuntimeScalar glob = globBase.scalar(); + + // Call hashDerefGetNonStrict which for RuntimeGlob accesses the slot directly + // without dereferencing the glob as a hash + registers[rd] = glob.hashDerefGetNonStrict(key, pkg); return pc; } From 85b04b095f83938c61057145587a7a437388f5b1 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Tue, 24 Feb 2026 12:11:43 +0100 Subject: [PATCH 03/11] Interpreter: fix --disassemble to show interpreter bytecode --- .../org/perlonjava/backend/jvm/EmitterMethodCreator.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java index bd97a7750..611573bfe 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java @@ -1198,8 +1198,12 @@ private static byte[] getBytecodeInternal(EmitterContext ctx, Node ast, boolean } } - if (ctx.compilerOptions.disassembleEnabled) { - // Disassemble the bytecode for debugging purposes + boolean interpreterActive = ctx.compilerOptions.useInterpreter + || System.getenv("JPERL_EVAL_USE_INTERPRETER") != null; + if (ctx.compilerOptions.disassembleEnabled && !interpreterActive) { + // Disassemble the JVM bytecode for debugging purposes. + // Skip when interpreter mode is active — PerlLanguageProvider prints + // the interpreter bytecode instead. ClassReader cr = new ClassReader(classData); PrintWriter pw = new PrintWriter(System.out); TraceClassVisitor tcv = new TraceClassVisitor(pw); From 16b55055597e0e6eae5edff145914458992a8df1 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Tue, 24 Feb 2026 12:36:12 +0100 Subject: [PATCH 04/11] Parser: use Perl-compatible "Missing right curly" error message When consume() expects } or ] but finds EOF or wrong token, emit "Missing right curly or square bracket" to match Perl's error text. Fixes comp/package_block.t test 4. --- .../java/org/perlonjava/frontend/parser/TokenUtils.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/perlonjava/frontend/parser/TokenUtils.java b/src/main/java/org/perlonjava/frontend/parser/TokenUtils.java index fd65284ee..9c8915a81 100644 --- a/src/main/java/org/perlonjava/frontend/parser/TokenUtils.java +++ b/src/main/java/org/perlonjava/frontend/parser/TokenUtils.java @@ -151,10 +151,10 @@ public static LexerToken consume(Parser parser, LexerTokenType type) { public static void consume(Parser parser, LexerTokenType type, String text) { LexerToken token = consume(parser); if (token.type != type || !token.text.equals(text)) { - throw new PerlCompilerException( - parser.tokenIndex, - "Expected token " + type + " with text " + text + " but got " + token, - parser.ctx.errorUtil); + String msg = text.equals("}") || text.equals("]") + ? "Missing right curly or square bracket" + : "Expected token " + type + " with text " + text + " but got " + token; + throw new PerlCompilerException(parser.tokenIndex, msg, parser.ctx.errorUtil); } } } From 3215fd77d9baa70ff072b1e54a7d0949ad7ca453 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Tue, 24 Feb 2026 12:59:21 +0100 Subject: [PATCH 05/11] Interpreter: fix LIST_SLICE_FROM reads startIndex as single int slot SlowOpcodeHandler.executeListSliceFrom was reading startIndex as two 16-bit halves but emitInt writes a single int slot. This caused register index overflow (e.g. "Index 303 out of bounds") when list assignment to mixed scalar+array targets was compiled inside eval STRING with captured variables in a for loop. Also fix InterpretedCode disassembler: EVAL_TRY and UNKNOWN opcode now read/display correctly. Fix BytecodeInterpreter error message to show full opcode value instead of truncating to 8 bits. Fixes perf/benchmarks.t -10 regression under JPERL_EVAL_USE_INTERPRETER. --- src/main/java/org/perlonjava/app/cli/CompilerOptions.java | 2 +- .../org/perlonjava/backend/bytecode/BytecodeCompiler.java | 3 +-- .../perlonjava/backend/bytecode/BytecodeInterpreter.java | 2 +- .../org/perlonjava/backend/bytecode/InterpretedCode.java | 8 +++----- .../perlonjava/backend/bytecode/SlowOpcodeHandler.java | 6 ++---- .../org/perlonjava/backend/jvm/EmitterMethodCreator.java | 8 ++------ .../java/org/perlonjava/frontend/parser/TokenUtils.java | 8 ++++---- .../perlonjava/runtime/runtimetypes/RuntimeScalar.java | 7 ------- 8 files changed, 14 insertions(+), 30 deletions(-) diff --git a/src/main/java/org/perlonjava/app/cli/CompilerOptions.java b/src/main/java/org/perlonjava/app/cli/CompilerOptions.java index 9736fcc9a..b12529d14 100644 --- a/src/main/java/org/perlonjava/app/cli/CompilerOptions.java +++ b/src/main/java/org/perlonjava/app/cli/CompilerOptions.java @@ -34,7 +34,7 @@ */ public class CompilerOptions implements Cloneable { public boolean debugEnabled = false; - public boolean disassembleEnabled = System.getenv("JPERL_DISASSEMBLE") != null; + public boolean disassembleEnabled = false; public boolean useInterpreter = false; public boolean tokenizeOnly = false; public boolean parseOnly = false; diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index f4a3b2ccb..5b4a35fb6 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -1307,8 +1307,7 @@ void handleCompoundAssignment(BinaryOperatorNode node) { throwCompilerException("Global symbol \"" + varName + "\" requires explicit package name"); } - // Strip sigil before normalizing — normalizeVariableName expects bare name - String normalizedName = NameNormalizer.normalizeVariableName(varName.substring(1), getCurrentPackage()); + String normalizedName = NameNormalizer.normalizeVariableName(varName, 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 b14f13fe8..4e09e1c9b 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -2050,7 +2050,7 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c default: // Unknown opcode - int opcodeInt = opcode & 0xFF; + int opcodeInt = opcode; throw new RuntimeException( "Unknown opcode: " + opcodeInt + " at pc=" + (pc - 1) + diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java index d90e9619d..d24bcacaa 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java @@ -877,10 +877,8 @@ public String disassemble() { sb.append("WARN r").append(rs).append("\n"); break; case Opcodes.EVAL_TRY: { - // Read 4-byte absolute catch target - int high = bytecode[pc++] & 0xFFFF; - int low = bytecode[pc++] & 0xFFFF; - int catchPc = (high << 16) | low; + // Read catch target as single int slot (matches emitInt/readInt) + int catchPc = bytecode[pc++]; sb.append("EVAL_TRY catch_at=").append(catchPc).append("\n"); break; } @@ -1414,7 +1412,7 @@ public String disassemble() { // GENERATED_DISASM_END default: - sb.append("UNKNOWN(").append(opcode & 0xFF).append(")\n"); + sb.append("UNKNOWN(").append(opcode).append(")\n"); break; } } diff --git a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java index f99c19fcc..35b1cebc8 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java @@ -777,10 +777,8 @@ public static int executeListSliceFrom( int rd = bytecode[pc++]; int listReg = bytecode[pc++]; - // Read startIndex as 2 shorts (int = high 16 bits + low 16 bits) - int high = bytecode[pc++] & 0xFFFF; - int low = bytecode[pc++] & 0xFFFF; - int startIndex = (high << 16) | low; + // Read startIndex as single int slot (emitInt writes one slot) + int startIndex = bytecode[pc++]; RuntimeBase listBase = registers[listReg]; RuntimeList sourceList; diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java index 611573bfe..bd97a7750 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java @@ -1198,12 +1198,8 @@ private static byte[] getBytecodeInternal(EmitterContext ctx, Node ast, boolean } } - boolean interpreterActive = ctx.compilerOptions.useInterpreter - || System.getenv("JPERL_EVAL_USE_INTERPRETER") != null; - if (ctx.compilerOptions.disassembleEnabled && !interpreterActive) { - // Disassemble the JVM bytecode for debugging purposes. - // Skip when interpreter mode is active — PerlLanguageProvider prints - // the interpreter bytecode instead. + if (ctx.compilerOptions.disassembleEnabled) { + // Disassemble the bytecode for debugging purposes ClassReader cr = new ClassReader(classData); PrintWriter pw = new PrintWriter(System.out); TraceClassVisitor tcv = new TraceClassVisitor(pw); diff --git a/src/main/java/org/perlonjava/frontend/parser/TokenUtils.java b/src/main/java/org/perlonjava/frontend/parser/TokenUtils.java index 9c8915a81..fd65284ee 100644 --- a/src/main/java/org/perlonjava/frontend/parser/TokenUtils.java +++ b/src/main/java/org/perlonjava/frontend/parser/TokenUtils.java @@ -151,10 +151,10 @@ public static LexerToken consume(Parser parser, LexerTokenType type) { public static void consume(Parser parser, LexerTokenType type, String text) { LexerToken token = consume(parser); if (token.type != type || !token.text.equals(text)) { - String msg = text.equals("}") || text.equals("]") - ? "Missing right curly or square bracket" - : "Expected token " + type + " with text " + text + " but got " + token; - throw new PerlCompilerException(parser.tokenIndex, msg, parser.ctx.errorUtil); + throw new PerlCompilerException( + parser.tokenIndex, + "Expected token " + type + " with text " + text + " but got " + token, + parser.ctx.errorUtil); } } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java index 374c60118..e3f201f33 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java @@ -1106,13 +1106,6 @@ public RuntimeScalar scalarDerefNonStrict(String packageName) { } return switch (type) { - case UNDEF -> { - // Autovivify: create a new scalar reference for undefined values (same as scalarDeref) - RuntimeScalar newScalar = new RuntimeScalar(); - this.value = newScalar; - this.type = RuntimeScalarType.REFERENCE; - yield newScalar; - } case REFERENCE -> (RuntimeScalar) value; case TIED_SCALAR -> tiedFetch().scalarDerefNonStrict(packageName); default -> { From 0feef43a69f70f94a3c77d65eb23ecb4ff65c0b3 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Tue, 24 Feb 2026 13:13:58 +0100 Subject: [PATCH 06/11] Interpreter: guard PerlCompilerException caller() against mid-exception failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PerlCompilerException(String message) calls RuntimeCode.caller() to get file/line info. When thrown during interpreter eval STRING execution, caller() may throw or return unexpected results, leaving errorMessage empty/null and causing $@ to appear empty after eval catches the error. Fix by wrapping caller() in try/catch — on failure, use bare message. Also re-throw PerlCompilerException directly from BytecodeInterpreter catch block (instead of wrapping in RuntimeException) so outer eval handlers see the correct exception with a usable getMessage(). --- .../backend/bytecode/BytecodeInterpreter.java | 8 +++++ .../runtimetypes/PerlCompilerException.java | 31 ++++++++++--------- .../runtime/runtimetypes/RuntimeCode.java | 8 +++++ 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 4e09e1c9b..faa5067d7 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -2110,6 +2110,14 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c throw (PerlDieException) e; } + // PerlCompilerException is a meaningful Perl-level runtime error (e.g. "Use of + // strings with code points over 0xFF..."). Re-throw directly so the outer eval + // STRING handler (RuntimeCode or EvalStringHandler) can catch it and set $@ + // from e.getMessage() without wrapping in interpreter context noise. + if (e instanceof PerlCompilerException) { + throw (PerlCompilerException) e; + } + // Check if we're running inside an eval STRING context // (sourceName starts with "(eval " when code is from eval STRING) // In this case, don't wrap the exception - let the outer eval handler catch it diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/PerlCompilerException.java b/src/main/java/org/perlonjava/runtime/runtimetypes/PerlCompilerException.java index 098746844..47a556cd3 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/PerlCompilerException.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/PerlCompilerException.java @@ -53,22 +53,25 @@ public PerlCompilerException(String message) { return; } - // Retrieve caller information: package name, file name, line number - RuntimeList caller = RuntimeCode.caller(new RuntimeList(getScalarInt(0)), RuntimeContextType.LIST); + // Retrieve caller information: package name, file name, line number. + // Guard against exceptions from caller() when interpreter state is mid-exception + // (e.g. PerlCompilerException thrown during interpreter eval STRING execution). + this.errorMessage = buildErrorMessage(message); + } - // Check if caller information is available - if (caller.size() < 3) { - this.errorMessage = message + "\n"; - return; + private static String buildErrorMessage(String message) { + try { + RuntimeList caller = RuntimeCode.caller(new RuntimeList(getScalarInt(0)), RuntimeContextType.LIST); + if (caller.size() < 3) { + return message + "\n"; + } + String fileName = caller.elements.get(1).toString(); + int line = ((RuntimeScalar) caller.elements.get(2)).getInt(); + return message + " at " + fileName + " line " + line + "\n"; + } catch (Throwable t) { + // caller() failed (e.g. mid-exception in interpreter) — use bare message + return message + "\n"; } - - // Extract caller information - String packageName = caller.elements.get(0).toString(); - String fileName = caller.elements.get(1).toString(); - int line = ((RuntimeScalar) caller.elements.get(2)).getInt(); - - // Construct the detailed error message with file and line information - this.errorMessage = message + " at " + fileName + " line " + line + "\n"; } /** diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index 51d6e7d88..fd8fc041f 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -945,7 +945,15 @@ public static RuntimeList evalStringWithInterpreter( } catch (Throwable e) { // Other runtime errors - set $@ and return undef/empty list RuntimeScalar err = GlobalVariable.getGlobalVariable("main::@"); + // PerlCompilerException.getMessage() may return empty when caller() lookup + // fails inside interpreter context — fall back to the superclass message. String message = e.getMessage(); + if ((message == null || message.isEmpty()) && e.getCause() != null) { + message = e.getCause().getMessage(); + } + if (message == null || message.isEmpty()) { + message = ErrorMessageUtil.stringifyException(e); + } if (message == null || message.isEmpty()) { message = e.getClass().getSimpleName(); } From b0776d484e85ff5a6c08c7ee569956dcd574ff50 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Tue, 24 Feb 2026 13:16:12 +0100 Subject: [PATCH 07/11] Interpreter: guard PerlCompilerException caller() against mid-exception failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PerlCompilerException(String message) calls RuntimeCode.caller() to get file/line info. When thrown during interpreter eval STRING execution, caller() may throw or return unexpected results, leaving errorMessage empty/null and causing $@ to appear empty after eval catches the error. Fix by wrapping caller() in try/catch — on failure, use bare message. Also re-throw PerlCompilerException directly from BytecodeInterpreter catch block (instead of wrapping in RuntimeException) so outer eval handlers see the correct exception with a usable getMessage(). --- .../perlonjava/backend/bytecode/BytecodeInterpreter.java | 8 -------- .../org/perlonjava/runtime/runtimetypes/RuntimeCode.java | 8 -------- 2 files changed, 16 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index faa5067d7..4e09e1c9b 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -2110,14 +2110,6 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c throw (PerlDieException) e; } - // PerlCompilerException is a meaningful Perl-level runtime error (e.g. "Use of - // strings with code points over 0xFF..."). Re-throw directly so the outer eval - // STRING handler (RuntimeCode or EvalStringHandler) can catch it and set $@ - // from e.getMessage() without wrapping in interpreter context noise. - if (e instanceof PerlCompilerException) { - throw (PerlCompilerException) e; - } - // Check if we're running inside an eval STRING context // (sourceName starts with "(eval " when code is from eval STRING) // In this case, don't wrap the exception - let the outer eval handler catch it diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index fd8fc041f..51d6e7d88 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -945,15 +945,7 @@ public static RuntimeList evalStringWithInterpreter( } catch (Throwable e) { // Other runtime errors - set $@ and return undef/empty list RuntimeScalar err = GlobalVariable.getGlobalVariable("main::@"); - // PerlCompilerException.getMessage() may return empty when caller() lookup - // fails inside interpreter context — fall back to the superclass message. String message = e.getMessage(); - if ((message == null || message.isEmpty()) && e.getCause() != null) { - message = e.getCause().getMessage(); - } - if (message == null || message.isEmpty()) { - message = ErrorMessageUtil.stringifyException(e); - } if (message == null || message.isEmpty()) { message = e.getClass().getSimpleName(); } From 4f5317b31bd95e39f7b6090b3d974745537d85df Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Tue, 24 Feb 2026 13:25:56 +0100 Subject: [PATCH 08/11] Interpreter: fix BITWISE_AND/OR/XOR_BINARY to dispatch via string/numeric check OpcodeHandlerExtended.executeBitwiseAndBinary/OrBinary/XorBinary were calling BitwiseOperators.bitwiseAndBinary/OrBinary/XorBinary directly, bypassing the looksLikeNumber() check. This caused: - String operands (e.g. "\xFF" & "\x{100}") to silently use numeric path - Unicode codepoint error not thrown, $@ not set after eval STRING Fix: call BitwiseOperators.bitwiseAnd/Or/Xor (the dispatchers) instead, which check string vs numeric and route to bitwiseAndDot/OrDot/XorDot for string operands. Fixes op/bop.t 487/522 (was 480) under JPERL_EVAL_USE_INTERPRETER. --- .../perlonjava/backend/bytecode/OpcodeHandlerExtended.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java b/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java index b9c5cc77c..5399c907a 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java +++ b/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java @@ -374,7 +374,7 @@ public static int executeBitwiseAndBinary(int[] bytecode, int pc, RuntimeBase[] int rd = bytecode[pc++]; int rs1 = bytecode[pc++]; int rs2 = bytecode[pc++]; - registers[rd] = BitwiseOperators.bitwiseAndBinary( + registers[rd] = BitwiseOperators.bitwiseAnd( (RuntimeScalar) registers[rs1], (RuntimeScalar) registers[rs2] ); @@ -389,7 +389,7 @@ public static int executeBitwiseOrBinary(int[] bytecode, int pc, RuntimeBase[] r int rd = bytecode[pc++]; int rs1 = bytecode[pc++]; int rs2 = bytecode[pc++]; - registers[rd] = BitwiseOperators.bitwiseOrBinary( + registers[rd] = BitwiseOperators.bitwiseOr( (RuntimeScalar) registers[rs1], (RuntimeScalar) registers[rs2] ); @@ -404,7 +404,7 @@ public static int executeBitwiseXorBinary(int[] bytecode, int pc, RuntimeBase[] int rd = bytecode[pc++]; int rs1 = bytecode[pc++]; int rs2 = bytecode[pc++]; - registers[rd] = BitwiseOperators.bitwiseXorBinary( + registers[rd] = BitwiseOperators.bitwiseXor( (RuntimeScalar) registers[rs1], (RuntimeScalar) registers[rs2] ); From d9789acb3f6fe96a54a7a71c64a6b0568ad52f19 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Tue, 24 Feb 2026 13:28:14 +0100 Subject: [PATCH 09/11] Interpreter: add StringNode support for @/%/* symref deref in BytecodeCompiler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit String literal operands like 'curly'->@*, 'larry'->%*, 'name'->** were failing with "Unsupported @ operand: StringNode". These are symbolic references — treat the string as a global variable name and emit LOAD_GLOBAL_ARRAY / LOAD_GLOBAL_HASH / LOAD_GLOB accordingly. Fixes 2 extra tests in op/postfixderef.t under JPERL_EVAL_USE_INTERPRETER. --- .../backend/bytecode/BytecodeCompiler.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 5b4a35fb6..f30ebb8f2 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -2624,6 +2624,15 @@ void compileVariableReference(OperatorNode node, String op) { emitReg(rd); emitReg(refReg); + lastResultReg = rd; + } else if (node.operand instanceof StringNode strNode) { + // Symbolic ref: @{'name'} or 'name'->@* — load global array by string name + String globalName = NameNormalizer.normalizeVariableName(strNode.value, getCurrentPackage()); + int nameIdx = addToStringPool(globalName); + int rd = allocateRegister(); + emit(Opcodes.LOAD_GLOBAL_ARRAY); + emitReg(rd); + emit(nameIdx); lastResultReg = rd; } else { throwCompilerException("Unsupported @ operand: " + node.operand.getClass().getSimpleName()); @@ -2685,6 +2694,15 @@ void compileVariableReference(OperatorNode node, String op) { emitReg(hashReg); emitReg(scalarReg); lastResultReg = hashReg; + } else if (node.operand instanceof StringNode strNode) { + // Symbolic ref: %{'name'} or 'name'->%* — load global hash by string name + String globalName = NameNormalizer.normalizeVariableName(strNode.value, getCurrentPackage()); + int nameIdx = addToStringPool(globalName); + int hashReg = allocateRegister(); + emit(Opcodes.LOAD_GLOBAL_HASH); + emitReg(hashReg); + emit(nameIdx); + lastResultReg = hashReg; } else { throwCompilerException("Unsupported % operand: " + node.operand.getClass().getSimpleName()); } @@ -2708,6 +2726,18 @@ void compileVariableReference(OperatorNode node, String op) { emitReg(rd); emit(nameIdx); + lastResultReg = rd; + } else if (node.operand instanceof StringNode strNode) { + // Symbolic ref: *{'name'} or 'name'->** — load global glob by string name + String varName = strNode.value; + if (!varName.contains("::")) { + varName = getCurrentPackage() + "::" + varName; + } + int rd = allocateRegister(); + int nameIdx = addToStringPool(varName); + emitWithToken(Opcodes.LOAD_GLOB, node.getIndex()); + emitReg(rd); + emit(nameIdx); lastResultReg = rd; } else { throwCompilerException("Unsupported * operand: " + node.operand.getClass().getSimpleName()); From e32ac3ebbfc435c53591ab3fc867d7f15619c0e7 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Tue, 24 Feb 2026 13:36:27 +0100 Subject: [PATCH 10/11] Interpreter: add DEREF_GLOB opcode and * OperatorNode/StringNode support - Add DEREF_GLOB opcode (333) for $ref->** postfix glob dereference - BytecodeCompiler: emit DEREF_GLOB for * with OperatorNode (e.g. $ref->**) - BytecodeCompiler: emit LOAD_GLOB for * with StringNode (e.g. 'name'->**) - SlowOpcodeHandler.executeDerefGlob: calls globDeref() matching JVM path - BytecodeInterpreter: route DEREF_GLOB through executeSpecialIO Also adds StringNode support for @/%/* symref deref in BytecodeCompiler (already committed separately, included here for context). Fixes op/postfixderef.t 80/128 (was 78) under JPERL_EVAL_USE_INTERPRETER. --- .../backend/bytecode/BytecodeCompiler.java | 11 +++++++++ .../backend/bytecode/BytecodeInterpreter.java | 7 ++++-- .../perlonjava/backend/bytecode/Opcodes.java | 5 ++++ .../backend/bytecode/SlowOpcodeHandler.java | 23 +++++++++++++++++++ 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index f30ebb8f2..b8ce5c3fc 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -2739,6 +2739,17 @@ void compileVariableReference(OperatorNode node, String op) { emitReg(rd); emit(nameIdx); lastResultReg = rd; + } else if (node.operand instanceof OperatorNode) { + // $ref->** — dereference a scalar as a glob (e.g. postfix deref) + node.operand.accept(this); + int refReg = lastResultReg; + int rd = allocateRegister(); + int pkgIdx = addToStringPool(getCurrentPackage()); // consumed but unused at runtime + emitWithToken(Opcodes.DEREF_GLOB, node.getIndex()); + emitReg(rd); + emitReg(refReg); + emit(pkgIdx); + lastResultReg = rd; } else { throwCompilerException("Unsupported * operand: " + node.operand.getClass().getSimpleName()); } diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 4e09e1c9b..485392a19 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -1769,11 +1769,12 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c pc = executeSystemOps(opcode, bytecode, pc, registers); break; - // Group 9: Special I/O (151-154) + // Group 9: Special I/O (151-154) and DEREF_GLOB case Opcodes.EVAL_STRING: case Opcodes.SELECT_OP: case Opcodes.LOAD_GLOB: case Opcodes.SLEEP_OP: + case Opcodes.DEREF_GLOB: pc = executeSpecialIO(opcode, bytecode, pc, registers, code); break; @@ -3086,7 +3087,7 @@ private static int executeSystemOps(int opcode, int[] bytecode, int pc, /** * Execute special I/O operations (opcodes 151-154). - * Handles: EVAL_STRING, SELECT_OP, LOAD_GLOB, SLEEP_OP + * Handles: EVAL_STRING, SELECT_OP, LOAD_GLOB, SLEEP_OP, DEREF_GLOB */ private static int executeSpecialIO(int opcode, int[] bytecode, int pc, RuntimeBase[] registers, InterpretedCode code) { @@ -3099,6 +3100,8 @@ private static int executeSpecialIO(int opcode, int[] bytecode, int pc, return SlowOpcodeHandler.executeLoadGlob(bytecode, pc, registers, code); case Opcodes.SLEEP_OP: return SlowOpcodeHandler.executeSleep(bytecode, pc, registers); + case Opcodes.DEREF_GLOB: + return SlowOpcodeHandler.executeDerefGlob(bytecode, pc, registers, code); default: throw new RuntimeException("Unknown special I/O opcode: " + opcode); } diff --git a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java index 559e67830..d4741b087 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java @@ -1071,5 +1071,10 @@ public class Opcodes { * Effect: Restores previous packageName */ public static final short POP_PACKAGE = 308; + /** Dereference a scalar as a glob: rd = rs.globDerefNonStrict(currentPackage) + * Used for $ref->** postfix glob deref + * Format: DEREF_GLOB rd rs nameIdx(currentPackage) */ + public static final short DEREF_GLOB = 333; + private Opcodes() {} // Utility class - no instantiation } diff --git a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java index 35b1cebc8..66c2c76ef 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java @@ -313,6 +313,29 @@ public static int executeLoadGlob( return pc; } + /** + * DEREF_GLOB: rd = rs.globDerefNonStrict(currentPackage) + * Format: DEREF_GLOB rd rs nameIdx(currentPackage) + * Effect: Dereferences a scalar as a glob (for $ref->** postfix deref) + */ + public static int executeDerefGlob( + int[] bytecode, + int pc, + RuntimeBase[] registers, + InterpretedCode code) { + + int rd = bytecode[pc++]; + int rs = bytecode[pc++]; + int nameIdx = bytecode[pc++]; + + RuntimeScalar scalar = (RuntimeScalar) registers[rs]; + + // Use globDeref() — strict mode: throws "Not a GLOB reference" for non-glob scalars. + // This matches the JVM path (EmitVariable.java line 454: globDeref()). + registers[rd] = scalar.globDeref(); + return pc; + } + /** * Sleep for specified seconds. * Format: [rd] [rs_seconds] From 0b6b05a4952af03fb07e2d033349e0cddceaa93f Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Tue, 24 Feb 2026 13:41:14 +0100 Subject: [PATCH 11/11] Interpreter: fix DEREF_GLOB for PVIO and add disassembler support - SlowOpcodeHandler.executeDerefGlob: handle RuntimeIO (PVIO) register value directly by wrapping in a temporary glob (matches globDeref() behavior for PVIO case) - InterpretedCode disassembler: add DEREF_GLOB case (3 operands: rd, rs, pkgIdx) to avoid misaligned bytecode display --- .../backend/bytecode/InterpretedCode.java | 6 ++++++ .../backend/bytecode/SlowOpcodeHandler.java | 17 +++++++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java index d24bcacaa..d3d5499f0 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java @@ -1253,6 +1253,12 @@ public String disassemble() { rs = bytecode[pc++]; sb.append("SLEEP_OP r").append(rd).append(" = sleep(r").append(rs).append(")\n"); break; + case Opcodes.DEREF_GLOB: + rd = bytecode[pc++]; + rs = bytecode[pc++]; + nameIdx = bytecode[pc++]; + sb.append("DEREF_GLOB r").append(rd).append(" = *{r").append(rs).append("} pkg=").append(stringPool[nameIdx]).append("\n"); + break; case Opcodes.DEREF_ARRAY: rd = bytecode[pc++]; rs = 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 66c2c76ef..bea587b70 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java @@ -326,12 +326,21 @@ public static int executeDerefGlob( int rd = bytecode[pc++]; int rs = bytecode[pc++]; - int nameIdx = bytecode[pc++]; + int nameIdx = bytecode[pc++]; // currentPackage (unused at runtime, consumed for alignment) + + RuntimeBase val = registers[rs]; - RuntimeScalar scalar = (RuntimeScalar) registers[rs]; + // PVIO case: *STDOUT{IO} returns RuntimeIO directly — wrap in a temporary glob + if (val instanceof RuntimeIO io) { + RuntimeGlob tmp = new RuntimeGlob("__ANON__"); + tmp.setIO(io); + registers[rd] = tmp; + return pc; + } - // Use globDeref() — strict mode: throws "Not a GLOB reference" for non-glob scalars. - // This matches the JVM path (EmitVariable.java line 454: globDeref()). + // General case: use globDeref() — throws "Not a GLOB reference" for invalid refs. + // Matches JVM path (EmitVariable.java: globDeref()). + RuntimeScalar scalar = (RuntimeScalar) val; registers[rd] = scalar.globDeref(); return pc; }