From 001f1aa032f3c1d13ea3011b92205e653336bfe2 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sun, 1 Mar 2026 18:03:17 +0100 Subject: [PATCH 1/3] Fix block refactoring breaking return, fix foreach loop variable aliasing Two bugs fixed: 1. ControlFlowDetectorVisitor: Mark `return` as unsafe control flow. When large blocks are refactored into `sub { ... }->(@_)` to avoid JVM "Method too large" errors, a `return` inside the block would only return from the anonymous sub, not from the enclosing function. This caused infinite loops in ExifTool's SetNewValue (Writer.pl) where `return` inside a `while` loop was silently swallowed. 2. BytecodeInterpreter & EmitForeach: Reset loop variable register after foreach loop exits. Previously the register still aliased the last array element, so subsequent writes to the variable would corrupt the source array's last element. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/bytecode/BytecodeInterpreter.java | 6 ++++-- .../perlonjava/backend/jvm/EmitForeach.java | 19 ++++++++++++++++--- .../analysis/ControlFlowDetectorVisitor.java | 6 ++++++ 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 58201a963..be137dbe2 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -350,8 +350,9 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c registers[rd] = element; GlobalVariable.aliasGlobalVariable(name, element); pc = bodyTarget; // ABSOLUTE jump back to body start + } else { + registers[rd] = new RuntimeScalar(); } - // else: fall through to exit break; } @@ -707,8 +708,9 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c RuntimeScalar elem = iterator.next(); registers[rd] = (isImmutableProxy(elem)) ? ensureMutableScalar(elem) : elem; pc = bodyTarget; // ABSOLUTE jump back to body start + } else { + registers[rd] = new RuntimeScalar(); } - // else: fall through to exit break; } diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitForeach.java b/src/main/java/org/perlonjava/backend/jvm/EmitForeach.java index 3a1f22675..bba030841 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitForeach.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitForeach.java @@ -378,6 +378,8 @@ public static void emitFor1(EmitterVisitor emitterVisitor, For1Node node) { mv.visitLabel(afterIterLabel); } + int loopVarIndex = -1; + mv.visitLabel(loopStart); // Check for pending signals (alarm, etc.) at loop entry @@ -490,9 +492,9 @@ public static void emitFor1(EmitterVisitor emitterVisitor, For1Node node) { if (varName == null) { // Unsupported variable shape; skip assignment rather than failing compilation. } else { - int varIndex = emitterVisitor.ctx.symbolTable.getVariableIndex(varName); - emitterVisitor.ctx.logDebug("FOR1 single var name:" + varName + " index:" + varIndex); - mv.visitVarInsn(Opcodes.ASTORE, varIndex); + loopVarIndex = emitterVisitor.ctx.symbolTable.getVariableIndex(varName); + emitterVisitor.ctx.logDebug("FOR1 single var name:" + varName + " index:" + loopVarIndex); + mv.visitVarInsn(Opcodes.ASTORE, loopVarIndex); } } } @@ -587,6 +589,17 @@ public static void emitFor1(EmitterVisitor emitterVisitor, For1Node node) { mv.visitLabel(loopEnd); + if (loopVarIndex >= 0) { + mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/runtimetypes/RuntimeScalar"); + mv.visitInsn(Opcodes.DUP); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, + "org/perlonjava/runtime/runtimetypes/RuntimeScalar", + "", + "()V", + false); + mv.visitVarInsn(Opcodes.ASTORE, loopVarIndex); + } + // Restore the original value for reference aliasing: for \$x (...), for \@x (...), for \%x (...) if (isReferenceAliasing && savedValueIndex != -1) { if (actualVariable instanceof OperatorNode innerOp && innerOp.operand instanceof IdentifierNode) { diff --git a/src/main/java/org/perlonjava/frontend/analysis/ControlFlowDetectorVisitor.java b/src/main/java/org/perlonjava/frontend/analysis/ControlFlowDetectorVisitor.java index f383c2aa4..1fbacdd0f 100644 --- a/src/main/java/org/perlonjava/frontend/analysis/ControlFlowDetectorVisitor.java +++ b/src/main/java/org/perlonjava/frontend/analysis/ControlFlowDetectorVisitor.java @@ -81,6 +81,12 @@ public void scan(Node root) { if (state == 0) { String oper = op.operator; + if ("return".equals(oper)) { + if (DEBUG) System.err.println("ControlFlowDetector(scan): UNSAFE return at tokenIndex=" + op.tokenIndex); + hasUnsafeControlFlow = true; + continue; + } + if ("goto".equals(oper)) { if (allowedGotoLabels != null && op.operand instanceof ListNode labelNode && !labelNode.elements.isEmpty()) { Node arg = labelNode.elements.getFirst(); From 09672de0c410fbe7e696ac3c8ae03bdb0500849b Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sun, 1 Mar 2026 18:14:18 +0100 Subject: [PATCH 2/3] Add debug-exiftool skill for diagnosing ExifTool test failures Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .cognition/skills/debug-exiftool/SKILL.md | 168 ++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 .cognition/skills/debug-exiftool/SKILL.md diff --git a/.cognition/skills/debug-exiftool/SKILL.md b/.cognition/skills/debug-exiftool/SKILL.md new file mode 100644 index 000000000..63e35671a --- /dev/null +++ b/.cognition/skills/debug-exiftool/SKILL.md @@ -0,0 +1,168 @@ +--- +name: debug-exiftool +description: Debug and fix Image::ExifTool test failures in PerlOnJava +argument-hint: "[test-name or test-file]" +triggers: + - user + - model +--- + +# Debugging Image::ExifTool Tests in PerlOnJava + +You are debugging failures in the Image::ExifTool test suite running under PerlOnJava (a Perl-to-JVM compiler/interpreter). Failures typically stem from missing Perl features or subtle behavior differences in PerlOnJava, not bugs in ExifTool itself. + +## Project Layout + +- **PerlOnJava source**: `src/main/java/org/perlonjava/` (compiler, bytecode interpreter, runtime) +- **ExifTool distribution**: `Image-ExifTool-13.44/` (unmodified upstream) +- **ExifTool tests**: `Image-ExifTool-13.44/t/*.t` +- **ExifTool test lib**: `Image-ExifTool-13.44/t/TestLib.pm` (exports `check`, `writeCheck`, `writeInfo`, `testCompare`, `binaryCompare`, `testVerbose`, `notOK`, `done`) +- **ExifTool test data**: `Image-ExifTool-13.44/t/images/` (reference images) +- **PerlOnJava unit tests**: `src/test/resources/unit/*.t` (mvn test suite) +- **Fat JAR**: `target/perlonjava-3.0.0.jar` + +## Running Tests + +### Single ExifTool test +```bash +cd Image-ExifTool-13.44 +java -jar ../target/perlonjava-3.0.0.jar -Ilib t/ExifTool.t +``` + +### Single ExifTool test with timeout (prevents infinite loops) +```bash +cd Image-ExifTool-13.44 +timeout 60 java -jar ../target/perlonjava-3.0.0.jar -Ilib t/XMP.t +``` + +### All ExifTool tests with timeout (batch) +```bash +cd Image-ExifTool-13.44 +for t in t/*.t; do + name=$(basename "$t" .t) + printf "%-20s " "$name" + output=$(timeout 60 java -jar ../target/perlonjava-3.0.0.jar -Ilib "$t" 2>&1) + ec=$? + if [ $ec -eq 124 ]; then echo "TIMEOUT" + else + pass=$(echo "$output" | grep -cE '^ok ') + fail=$(echo "$output" | grep -cE '^not ok ') + plan=$(echo "$output" | grep -oE '^1\.\.[0-9]+' | head -1) + planned=${plan#1..} + [ $fail -gt 0 ] || [ $ec -ne 0 ] && echo "FAIL (pass=$pass fail=$fail planned=${planned:-?} exit=$ec)" || echo "PASS ($pass/${planned:-?})" + fi +done +``` + +### Build the JAR (required after Java source changes) +```bash +mvn package -q -DskipTests +``` + +### Run PerlOnJava's own test suite (154 tests) +```bash +mvn test +``` + +## Test File Anatomy + +ExifTool `.t` files follow a common pattern: +```perl +BEGIN { $| = 1; print "1..N\n"; require './t/TestLib.pm'; t::TestLib->import(); } +END { print "not ok 1\n" unless $loaded; } +use Image::ExifTool; +$loaded = 1; + +# Read test +my $exifTool = Image::ExifTool->new; +my $info = $exifTool->ImageInfo('t/images/SomeFile.ext', @tags); +print 'not ' unless check($exifTool, $info, $testname, $testnum); +print "ok $testnum\n"; + +# Write test (uses writeInfo from TestLib) +writeInfo($exifTool, 'src.jpg', 'tmp/out.jpg', \@setNewValue_args); +``` + +The `check()` function compares extracted tags against reference files in `t/ExifTool_N.out` (or `t/_N.out`). The `writeInfo()` function calls SetNewValue + WriteInfo and compares the output file. + +## Debugging Workflow + +1. **Run the failing test** and capture full output (stdout + stderr). Look for: + - `not ok N` lines (which specific sub-tests fail) + - Runtime exceptions / stack traces from Java + - `Can't locate ...` (missing module) + - `Undefined subroutine` / `Can't call method` errors + +2. **Identify the failing sub-test number** and find it in the `.t` file. Map it to the ExifTool operation (read vs write, which image format, which tags). + +3. **Check the `.out` reference file** (e.g., `t/XMP_3.out`) to understand expected output. Compare with actual output by adding debug prints or using `testVerbose`. + +4. **Isolate the Perl construct** causing the failure. Write a minimal `.pl` reproducer: + ```bash + java -jar target/perlonjava-3.0.0.jar -e 'print "test\n"' + ``` + +5. **Trace into PerlOnJava source** to find the bug. Key areas: + - **Bytecode interpreter**: `src/main/java/org/perlonjava/runtime/BytecodeInterpreter.java` + - **Compiler/emitter**: `src/main/java/org/perlonjava/codegen/` + - **Runtime operators**: `src/main/java/org/perlonjava/operators/` + - **Runtime scalars/arrays/hashes**: `src/main/java/org/perlonjava/runtime/RuntimeScalar.java`, `RuntimeArray.java`, `RuntimeHash.java` + - **IO operations**: `src/main/java/org/perlonjava/runtime/RuntimeIO.java` + - **String/regex ops**: `src/main/java/org/perlonjava/operators/StringOperators.java` + - **List operators**: `src/main/java/org/perlonjava/operators/ListOperators.java` + - **Large block refactoring** (wraps big subs in anonymous sub calls): `src/main/java/org/perlonjava/codegen/LargeBlockRefactorer.java` + +6. **Fix in PerlOnJava**, rebuild (`mvn package -q -DskipTests`), re-run the ExifTool test. + +7. **Run `mvn test`** to verify no regressions in the 154 unit tests. + +## Common Failure Patterns + +### Infinite loops / TIMEOUT +- Often caused by `return` inside a block that was refactored by `LargeBlockRefactorer.tryWholeBlockRefactoring()` into `sub { ... }->(@_)`. The `return` exits the anonymous sub instead of the enclosing function. Check `ControlFlowDetectorVisitor.java` for unsafe control flow detection. +- Can also be caused by regex catastrophic backtracking — PerlOnJava has timeout protection via `d0071f45`. + +### Foreach loop variable corruption +- After a foreach loop exits, the loop variable register may still alias the last array element. Writes to that variable corrupt the source array. Fixed in `BytecodeInterpreter.java` (`FOREACH_NEXT_OR_EXIT`) and `EmitForeach.java`. + +### Write test failures ("WriteInfo errors") +- `SetNewValue` or `WriteInfo` returning errors. Often due to missing Perl features in string/binary operations (pack/unpack edge cases, encoding, tied handles). + +### Encoding / binary data issues +- ExifTool heavily uses `binmode`, `sysread`, `syswrite`, `pack`, `unpack`, `Encode::decode`/`encode`. Check that PerlOnJava handles these correctly for the specific format being tested. + +### Missing or incomplete Perl builtins +- `local *glob` unwinding, tied handles, `pos()` after regex match, `$1`/`$2` capture variables in eval, `wantarray` in specific contexts. + +### Read-only variable violations +- Operations that try to modify read-only scalars (e.g., `$_` aliased to a constant). Check `RuntimeScalarReadOnly` usage. + +## Adding Debug Instrumentation + +When you need to trace execution inside ExifTool Perl code, add temporary prints: +```perl +print STDERR "DEBUG: variable=$variable\n"; +``` + +When tracing inside PerlOnJava Java code, use: +```java +System.err.println("DEBUG: value=" + value); +``` + +**Always remove debug instrumentation before committing.** + +## Key Files Quick Reference + +| Area | File | +|------|------| +| Bytecode interpreter | `src/main/java/org/perlonjava/runtime/BytecodeInterpreter.java` | +| Foreach emission | `src/main/java/org/perlonjava/codegen/EmitForeach.java` | +| Large block refactoring | `src/main/java/org/perlonjava/codegen/LargeBlockRefactorer.java` | +| Control flow safety | `src/main/java/org/perlonjava/codegen/ControlFlowDetectorVisitor.java` | +| Runtime scalar | `src/main/java/org/perlonjava/runtime/RuntimeScalar.java` | +| IO operations | `src/main/java/org/perlonjava/runtime/RuntimeIO.java` | +| String operators | `src/main/java/org/perlonjava/operators/StringOperators.java` | +| List operators | `src/main/java/org/perlonjava/operators/ListOperators.java` | +| Pack/Unpack | `src/main/java/org/perlonjava/operators/PackOperator.java` | +| Eval handling | `src/main/java/org/perlonjava/codegen/EmitterMethodCreator.java` | +| Dynamic variables | `src/main/java/org/perlonjava/runtime/DynamicVariableManager.java` | From fb1f832a250a9b6ff693090fc5e7fb75efa405ac Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sun, 1 Mar 2026 18:52:46 +0100 Subject: [PATCH 3/3] Fix eval STRING variable capture with ScopedSymbolTable refactor Replace BytecodeCompiler ad-hoc variableScopes Stack + allDeclaredVariables HashMap with ScopedSymbolTable for proper scope-aware variable tracking. The root cause: allDeclaredVariables was a flat map that overwrote entries when the same variable name was declared in different scopes. eval $val would capture the register from the LAST declaration, not the one visible at the eval site. This broke ~30 ExifTool write tests. Key changes: - SymbolTable/ScopedSymbolTable: add addVariableWithIndex(), getVisibleVariableRegistry() - BytecodeCompiler: use symbolTable for variable tracking, add per-eval-site registry snapshots via evalSiteRegistries list - CompileOperator: snapshot visible variables at each EVAL_STRING emission - InterpretedCode: carry evalSiteRegistries for runtime lookup - SlowOpcodeHandler/EvalStringHandler: use site-specific registry with fallback to global registry for backward compatibility Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- run_exiftool_tests.sh | 47 +-- .../backend/bytecode/BytecodeCompiler.java | 270 ++++++------------ .../backend/bytecode/CompileAssignment.java | 16 +- .../backend/bytecode/CompileOperator.java | 13 +- .../backend/bytecode/EvalStringHandler.java | 38 ++- .../backend/bytecode/InterpretedCode.java | 42 ++- .../backend/bytecode/SlowOpcodeHandler.java | 41 +-- .../frontend/semantic/ScopedSymbolTable.java | 14 + .../frontend/semantic/SymbolTable.java | 7 + 9 files changed, 230 insertions(+), 258 deletions(-) diff --git a/run_exiftool_tests.sh b/run_exiftool_tests.sh index 506987fb5..214c76768 100755 --- a/run_exiftool_tests.sh +++ b/run_exiftool_tests.sh @@ -1,27 +1,34 @@ #!/bin/bash -cd /Users/fglock/projects/PerlOnJava2/Image-ExifTool-13.44 -PASS=0 -FAIL=0 -PASS_LIST="" -FAIL_LIST="" +# Run all ExifTool test files with a 60-second timeout per test +JAR=target/perlonjava-3.0.0.jar +EXIFTOOL_DIR=Image-ExifTool-13.44 +TIMEOUT=60 + +cd "$EXIFTOOL_DIR" || exit 1 + for t in t/*.t; do name=$(basename "$t" .t) - output=$(timeout 60 java -jar ../target/perlonjava-3.0.0.jar -Ilib "$t" 2>&1) + printf "%-20s " "$name" + output=$(timeout $TIMEOUT java -jar "../$JAR" -Ilib "$t" 2>&1) exit_code=$? - # Check for "not ok" or non-zero exit - if echo "$output" | grep -q "^not ok"; then - FAIL=$((FAIL + 1)) - FAIL_LIST="$FAIL_LIST $name" - elif [ $exit_code -ne 0 ]; then - FAIL=$((FAIL + 1)) - FAIL_LIST="$FAIL_LIST $name" + if [ $exit_code -eq 124 ]; then + echo "TIMEOUT" else - PASS=$((PASS + 1)) - PASS_LIST="$PASS_LIST $name" + # Count ok/not ok lines + total=$(echo "$output" | grep -cE '^(not )?ok ') + pass=$(echo "$output" | grep -cE '^ok ') + fail=$(echo "$output" | grep -cE '^not ok ') + # Check for plan + plan=$(echo "$output" | grep -oE '^1\.\.[0-9]+' | head -1) + if [ -n "$plan" ]; then + planned=${plan#1..} + else + planned="?" + fi + if [ $fail -gt 0 ] || [ $exit_code -ne 0 ]; then + echo "FAIL (pass=$pass fail=$fail planned=$planned exit=$exit_code)" + else + echo "PASS (pass=$pass/$planned)" + fi fi - echo "$name: exit=$exit_code" done -echo "" -echo "PASS: $PASS" -echo "FAIL: $FAIL" -echo "Failing:$FAIL_LIST" diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 5968c3769..e8974b8f2 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -32,14 +32,15 @@ public class BytecodeCompiler implements Visitor { private final Map stringPoolIndex = new HashMap<>(16); // O(1) lookup private final Map constantPoolIndex = new HashMap<>(16); // O(1) lookup - // Simple variable-to-register mapping for the interpreter - // Each scope is a Map mapping variable names to register indices - final Stack> variableScopes = new Stack<>(); - - // Symbol table for package/class tracking - // Tracks current package, class flag, and package versions like the compiler does + // Scoped symbol table for variable tracking, package/class tracking, and eval STRING support. + // Replaces the old variableScopes Stack + allDeclaredVariables flat map. + // Using ScopedSymbolTable gives us proper scope-aware variable lookups and + // getAllVisibleVariables()/getVisibleVariableRegistry() for per-eval-site snapshots. final ScopedSymbolTable symbolTable = new ScopedSymbolTable(); + // Scope index stack for proper ScopedSymbolTable.exitScope() calls + private final Stack scopeIndices = new Stack<>(); + // Stack to save/restore register state when entering/exiting scopes private final Stack savedNextRegister = new Stack<>(); private final Stack savedBaseRegister = new Stack<>(); @@ -95,9 +96,9 @@ private static class LoopInfo { private String[] capturedVarNames; // Parallel array of names Map capturedVarIndices; // Name → register index - // Track ALL variables ever declared (for variableRegistry) - // This is needed because inner scopes get popped before variableRegistry is built - final Map allDeclaredVariables = new HashMap<>(); + // Per-eval-site variable registries: each eval STRING emission snapshots the + // currently visible variables so at runtime the correct registers are captured. + final List> evalSiteRegistries = new ArrayList<>(); // BEGIN support for named subroutine closures private int currentSubroutineBeginId = 0; // BEGIN ID for current named subroutine (0 = not in named sub) @@ -112,12 +113,10 @@ public BytecodeCompiler(String sourceName, int sourceLine, ErrorMessageUtil erro this.sourceLine = sourceLine; this.errorUtil = errorUtil; - // Initialize with global scope containing the 3 reserved registers - Map globalScope = new HashMap<>(); - globalScope.put("this", 0); - globalScope.put("@_", 1); - globalScope.put("wantarray", 2); - variableScopes.push(globalScope); + // Initialize symbolTable with the 3 reserved registers + symbolTable.addVariableWithIndex("this", 0, "reserved"); + symbolTable.addVariableWithIndex("@_", 1, "reserved"); + symbolTable.addVariableWithIndex("wantarray", 2, "reserved"); } // Legacy constructor for backward compatibility @@ -127,7 +126,7 @@ public BytecodeCompiler(String sourceName, int sourceLine) { /** * Constructor for eval STRING with parent scope variable registry. - * Initializes variableScopes with variables from parent scope. + * Initializes symbolTable with variables from parent scope. * * @param sourceName Source name for error messages * @param sourceLine Source line for error messages @@ -140,99 +139,68 @@ public BytecodeCompiler(String sourceName, int sourceLine, ErrorMessageUtil erro this.sourceLine = sourceLine; this.errorUtil = errorUtil; - // Initialize with global scope containing the 3 reserved registers - // plus any variables from parent scope (for eval STRING) - Map globalScope = new HashMap<>(); - globalScope.put("this", 0); - globalScope.put("@_", 1); - globalScope.put("wantarray", 2); + // Initialize symbolTable with the 3 reserved registers + symbolTable.addVariableWithIndex("this", 0, "reserved"); + symbolTable.addVariableWithIndex("@_", 1, "reserved"); + symbolTable.addVariableWithIndex("wantarray", 2, "reserved"); if (parentRegistry != null) { - // Add parent scope variables (for eval STRING variable capture) - globalScope.putAll(parentRegistry); - - // Preserve parent scope variables in the compiled code's variableRegistry so that - // nested eval STRING can capture lexicals from this eval scope. - allDeclaredVariables.putAll(parentRegistry); + // Add parent scope variables to symbolTable (for eval STRING variable capture) + for (Map.Entry entry : parentRegistry.entrySet()) { + String varName = entry.getKey(); + int regIndex = entry.getValue(); + if (regIndex >= 3) { + symbolTable.addVariableWithIndex(varName, regIndex, "my"); + } + } // Mark parent scope variables as captured so assignments use SET_SCALAR capturedVarIndices = new HashMap<>(); for (Map.Entry entry : parentRegistry.entrySet()) { String varName = entry.getKey(); int regIndex = entry.getValue(); - // Skip reserved registers if (regIndex >= 3) { capturedVarIndices.put(varName, regIndex); } } // Adjust nextRegister to account for captured variables - // Find the maximum register index used by parent scope - int maxRegister = 2; // Start with reserved registers (0-2) + int maxRegister = 2; for (Integer regIndex : parentRegistry.values()) { if (regIndex > maxRegister) { maxRegister = regIndex; } } - // Next available register is one past the maximum used this.nextRegister = maxRegister + 1; this.maxRegisterEverUsed = maxRegister; this.baseRegisterForStatement = this.nextRegister; } - - variableScopes.push(globalScope); } - /** - * Helper: Check if a variable exists in any scope. - */ boolean hasVariable(String name) { - for (int i = variableScopes.size() - 1; i >= 0; i--) { - if (variableScopes.get(i).containsKey(name)) { - return true; - } - } - return false; + return symbolTable.getVariableIndex(name) != -1; } - /** - * Helper: Get the register index for a variable. - * Returns -1 if not found. - */ int getVariableRegister(String name) { - for (int i = variableScopes.size() - 1; i >= 0; i--) { - Integer reg = variableScopes.get(i).get(name); - if (reg != null) { - return reg; - } - } - return -1; + return symbolTable.getVariableIndex(name); } - /** - * Helper: Add a variable to the current scope and return its register index. - * Allocates a new register. - */ int addVariable(String name, String declType) { int reg = allocateRegister(); - variableScopes.peek().put(name, reg); - allDeclaredVariables.put(name, reg); // Track for variableRegistry + symbolTable.addVariableWithIndex(name, reg, declType); return reg; } - /** - * Helper: Enter a new lexical scope. - * Saves current register allocation state so inner scopes don't - * interfere with outer scope register usage. - */ + void registerVariable(String name, int reg) { + symbolTable.addVariableWithIndex(name, reg, "my"); + } + private void enterScope() { - variableScopes.push(new HashMap<>()); - // Save current register state + int scopeIdx = symbolTable.enterScope(); + scopeIndices.push(scopeIdx); savedNextRegister.push(nextRegister); savedBaseRegister.push(baseRegisterForStatement); - // Update base to protect all registers allocated before this scope baseRegisterForStatement = nextRegister; - // Save current pragma state so use strict/no strict inside blocks is properly scoped if (emitterContext != null && emitterContext.symbolTable != null) { ScopedSymbolTable st = emitterContext.symbolTable; st.strictOptionsStack.push(st.strictOptionsStack.peek()); @@ -241,21 +209,15 @@ private void enterScope() { } } - /** - * Helper: Exit the current lexical scope. - * Restores register allocation state to what it was before entering the scope. - */ private void exitScope() { - if (variableScopes.size() > 1) { - variableScopes.pop(); - // Restore register state + if (!scopeIndices.isEmpty()) { + symbolTable.exitScope(scopeIndices.pop()); if (!savedNextRegister.isEmpty()) { nextRegister = savedNextRegister.pop(); } if (!savedBaseRegister.isEmpty()) { baseRegisterForStatement = savedBaseRegister.pop(); } - // Restore pragma state if (emitterContext != null && emitterContext.symbolTable != null) { ScopedSymbolTable st = emitterContext.symbolTable; st.strictOptionsStack.pop(); @@ -282,15 +244,8 @@ public void setCompilePackage(String packageName) { symbolTable.setCurrentPackage(packageName, false); } - /** - * Helper: Get all variable names in all scopes (for closure detection). - */ private String[] getVariableNames() { - Set allVars = new HashSet<>(); - for (Map scope : variableScopes) { - allVars.addAll(scope.keySet()); - } - return allVars.toArray(new String[0]); + return symbolTable.getVariableNames(); } /** @@ -536,16 +491,11 @@ public InterpretedCode compile(Node node, EmitterContext ctx) { emit(Opcodes.RETURN); emitReg(returnReg); - // Build variable registry for eval STRING support - // Use allDeclaredVariables which tracks ALL variables ever declared, - // not variableScopes which loses variables when scopes are popped - Map variableRegistry = new HashMap<>(); - variableRegistry.putAll(allDeclaredVariables); - - // Also include variables from current scopes (in case of nested contexts) - for (Map scope : variableScopes) { - variableRegistry.putAll(scope); - } + // Build variable registry for eval STRING support. + // At this point all inner scopes have exited, so getVisibleVariableRegistry() + // returns only the outermost scope variables. Per-eval-site registries + // (stored in evalSiteRegistries) provide the correct scope-aware mappings. + Map variableRegistry = symbolTable.getVisibleVariableRegistry(); // Extract strict/feature/warning flags for eval STRING inheritance int strictOptions = 0; @@ -562,17 +512,18 @@ public InterpretedCode compile(Node node, EmitterContext ctx) { toShortArray(), constants.toArray(), stringPool.toArray(new String[0]), - maxRegisterEverUsed + 1, // maxRegisters = highest register used + 1 - capturedVars, // NOW POPULATED! + maxRegisterEverUsed + 1, + capturedVars, sourceName, sourceLine, - pcToTokenIndex, // Pass token index map for error reporting - variableRegistry, // Variable registry for eval STRING - errorUtil, // Pass error util for line number lookup - strictOptions, // Strict flags for eval STRING inheritance - featureFlags, // Feature flags for eval STRING inheritance - warningFlags, // Warning flags for eval STRING inheritance - symbolTable.getCurrentPackage() // Compile-time package for eval STRING name resolution + pcToTokenIndex, + variableRegistry, + errorUtil, + strictOptions, + featureFlags, + warningFlags, + symbolTable.getCurrentPackage(), + evalSiteRegistries.isEmpty() ? null : evalSiteRegistries ); } @@ -1790,25 +1741,21 @@ void compileVariableDeclaration(OperatorNode node, String op) { emitReg(reg); emit(nameIdx); emit(sigilOp.id); - // Track this as a captured/state variable - map to the register we allocated - variableScopes.peek().put(varName, reg); - allDeclaredVariables.put(varName, reg); // Track for variableRegistry + registerVariable(varName, reg); } case "@" -> { emitWithToken(Opcodes.RETRIEVE_BEGIN_ARRAY, node.getIndex()); emitReg(reg); emit(nameIdx); emit(sigilOp.id); - variableScopes.peek().put(varName, reg); - allDeclaredVariables.put(varName, reg); // Track for variableRegistry + registerVariable(varName, reg); } case "%" -> { emitWithToken(Opcodes.RETRIEVE_BEGIN_HASH, node.getIndex()); emitReg(reg); emit(nameIdx); emit(sigilOp.id); - variableScopes.peek().put(varName, reg); - allDeclaredVariables.put(varName, reg); // Track for variableRegistry + registerVariable(varName, reg); } default -> throwCompilerException("Unsupported variable type: " + sigil); } @@ -2160,24 +2107,21 @@ void compileVariableDeclaration(OperatorNode node, String op) { emitReg(reg); emit(nameIdx); emit(sigilOp.id); - variableScopes.peek().put(varName, reg); - allDeclaredVariables.put(varName, reg); // Track for variableRegistry + registerVariable(varName, reg); } case "@" -> { emitWithToken(Opcodes.RETRIEVE_BEGIN_ARRAY, node.getIndex()); emitReg(reg); emit(nameIdx); emit(sigilOp.id); - variableScopes.peek().put(varName, reg); - allDeclaredVariables.put(varName, reg); // Track for variableRegistry + registerVariable(varName, reg); } case "%" -> { emitWithToken(Opcodes.RETRIEVE_BEGIN_HASH, node.getIndex()); emitReg(reg); emit(nameIdx); emit(sigilOp.id); - variableScopes.peek().put(varName, reg); - allDeclaredVariables.put(varName, reg); // Track for variableRegistry + registerVariable(varName, reg); } default -> throwCompilerException("Unsupported variable type in list declaration: " + sigil); } @@ -3482,19 +3426,31 @@ int allocateRegister() { return reg; } + TreeMap collectVisiblePerlVariables() { + TreeMap closureVarsByReg = new TreeMap<>(); + Map visible = symbolTable.getAllVisibleVariables(); + for (Map.Entry e : visible.entrySet()) { + int reg = e.getKey(); + String varName = e.getValue().name(); + if (reg < 3 || varName == null || varName.isEmpty()) continue; + char sigil = varName.charAt(0); + if (sigil == '$' || sigil == '@' || sigil == '%') { + closureVarsByReg.put(reg, varName); + } + } + return closureVarsByReg; + } + /** * Get the highest register index currently used by variables (not temporaries). * This is used to determine the reset point for register recycling. */ private int getHighestVariableRegister() { - int maxReg = 2; // Start with reserved registers (0=this, 1=@_, 2=wantarray) - - // Check all variable scopes - for (Map scope : variableScopes) { - for (Integer reg : scope.values()) { - if (reg > maxReg) { - maxReg = reg; - } + int maxReg = 2; + Map visible = symbolTable.getAllVisibleVariables(); + for (Integer reg : visible.keySet()) { + if (reg > maxReg) { + maxReg = reg; } } @@ -3785,33 +3741,13 @@ private void visitNamedSubroutine(SubroutineNode node) { // // Therefore capture all visible Perl variables (scalars/arrays/hashes) from the // current scope, not just variables referenced directly in the sub AST. - TreeMap closureVarsByReg = new TreeMap<>(); - for (Map scope : variableScopes) { - for (Map.Entry e : scope.entrySet()) { - String varName = e.getKey(); - Integer reg = e.getValue(); - if (reg == null || reg < 3) { - continue; - } - if (varName == null || varName.isEmpty()) { - continue; - } - char sigil = varName.charAt(0); - if (sigil != '$' && sigil != '@' && sigil != '%') { - continue; - } - closureVarsByReg.put(reg, varName); - } - } + TreeMap closureVarsByReg = collectVisiblePerlVariables(); List closureVarNames = new ArrayList<>(closureVarsByReg.values()); List closureVarIndices = new ArrayList<>(closureVarsByReg.keySet()); - // If there are closure variables, we need to store them in PersistentVariable globals - // so the named sub can retrieve them using RETRIEVE_BEGIN opcodes int beginId = 0; if (!closureVarIndices.isEmpty()) { - // Assign a unique BEGIN ID for this subroutine beginId = EmitterMethodCreator.classCounter++; // Store each closure variable in PersistentVariable globals @@ -3930,24 +3866,7 @@ private void visitAnonymousSubroutine(SubroutineNode node) { // lexicals only inside strings (so they won't appear as IdentifierNodes in the AST). // Perl still expects those lexicals to be visible to eval STRING at runtime. // Capture all visible Perl variables (scalars/arrays/hashes) from the current scope. - TreeMap closureVarsByReg = new TreeMap<>(); - for (Map scope : variableScopes) { - for (Map.Entry e : scope.entrySet()) { - String varName = e.getKey(); - Integer reg = e.getValue(); - if (reg == null || reg < 3) { - continue; - } - if (varName == null || varName.isEmpty()) { - continue; - } - char sigil = varName.charAt(0); - if (sigil != '$' && sigil != '@' && sigil != '%') { - continue; - } - closureVarsByReg.put(reg, varName); - } - } + TreeMap closureVarsByReg = collectVisiblePerlVariables(); List closureVarNames = new ArrayList<>(closureVarsByReg.values()); List closureVarIndices = new ArrayList<>(closureVarsByReg.keySet()); @@ -4153,8 +4072,7 @@ public void visit(For1Node node) { OperatorNode sigilOp = (OperatorNode) varOp2.operand; if (sigilOp.operator.equals("$") && sigilOp.operand instanceof IdentifierNode) { String varName = "$" + ((IdentifierNode) sigilOp.operand).name; - variableScopes.peek().put(varName, varReg); - allDeclaredVariables.put(varName, varReg); + registerVariable(varName, varReg); } } } @@ -4697,26 +4615,6 @@ List getBytecode() { return bytecode; } - /** - * Get the variable scopes stack. - * Used by refactored compiler classes for variable declaration. - */ - Stack> getVariableScopes() { - return variableScopes; - } - - /** - * Get the all declared variables map. - * Used by refactored compiler classes for variable registry tracking. - */ - Map getAllDeclaredVariables() { - return allDeclaredVariables; - } - - /** - * Get the captured variable indices map. - * Used by refactored compiler classes for closure support. - */ Map getCapturedVarIndices() { return capturedVarIndices; } diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java index 19ba6fbed..55afd7878 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java @@ -74,9 +74,7 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.emitReg(reg); bytecodeCompiler.emitReg(valueReg); - // Track this variable - map the name to the register we already allocated - bytecodeCompiler.variableScopes.peek().put(varName, reg); - bytecodeCompiler.allDeclaredVariables.put(varName, reg); // Track for variableRegistry + bytecodeCompiler.registerVariable(varName, reg); bytecodeCompiler.lastResultReg = reg; return; } @@ -122,9 +120,7 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.emitReg(arrayReg); bytecodeCompiler.emitReg(listReg); - // Track this variable - map the name to the register we already allocated - bytecodeCompiler.variableScopes.peek().put(varName, arrayReg); - bytecodeCompiler.allDeclaredVariables.put(varName, arrayReg); // Track for variableRegistry + bytecodeCompiler.registerVariable(varName, arrayReg); // In scalar context, return the count of elements assigned // In list/void context, return the array @@ -194,9 +190,7 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.emitReg(hashReg); bytecodeCompiler.emitReg(listReg); - // Track this variable - map the name to the register we already allocated - bytecodeCompiler.variableScopes.peek().put(varName, hashReg); - bytecodeCompiler.allDeclaredVariables.put(varName, hashReg); // Track for variableRegistry + bytecodeCompiler.registerVariable(varName, hashReg); bytecodeCompiler.lastResultReg = hashReg; return; } @@ -297,9 +291,7 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, } } - // Track this variable - bytecodeCompiler.variableScopes.peek().put(varName, varReg); - bytecodeCompiler.allDeclaredVariables.put(varName, varReg); // Track for variableRegistry + bytecodeCompiler.registerVariable(varName, varReg); } else { // Regular lexical variable (not captured) // Declare the variable diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index 983c66272..bde09565d 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -839,21 +839,20 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode } else if (op.equals("eval")) { // eval $string; if (node.operand != null) { - // Evaluate eval operand (the code string) node.operand.accept(bytecodeCompiler); int stringReg = bytecodeCompiler.lastResultReg; - - // Allocate register for result int rd = bytecodeCompiler.allocateRegister(); - // Emit direct opcode EVAL_STRING + // Snapshot visible variables for this eval site + int evalSiteIndex = bytecodeCompiler.evalSiteRegistries.size(); + bytecodeCompiler.evalSiteRegistries.add( + bytecodeCompiler.symbolTable.getVisibleVariableRegistry()); + bytecodeCompiler.emitWithToken(Opcodes.EVAL_STRING, node.getIndex()); bytecodeCompiler.emitReg(rd); bytecodeCompiler.emitReg(stringReg); - // Encode the eval operator's own call context (VOID/SCALAR/LIST) so - // wantarray() inside the eval body and the eval return value follow - // the correct context even when the surrounding sub is VOID. bytecodeCompiler.emit(bytecodeCompiler.currentCallContext); + bytecodeCompiler.emit(evalSiteIndex); bytecodeCompiler.lastResultReg = rd; } else { diff --git a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java index 1c5c619f8..744cbbe3a 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java @@ -61,7 +61,17 @@ public static RuntimeScalar evalString(String perlCode, String sourceName, int sourceLine, int callContext) { - return evalStringList(perlCode, currentCode, registers, sourceName, sourceLine, callContext).scalar(); + return evalStringList(perlCode, currentCode, registers, sourceName, sourceLine, callContext, null).scalar(); + } + + public static RuntimeScalar evalString(String perlCode, + InterpretedCode currentCode, + RuntimeBase[] registers, + String sourceName, + int sourceLine, + int callContext, + Map siteRegistry) { + return evalStringList(perlCode, currentCode, registers, sourceName, sourceLine, callContext, siteRegistry).scalar(); } public static RuntimeList evalStringList(String perlCode, @@ -70,6 +80,16 @@ public static RuntimeList evalStringList(String perlCode, String sourceName, int sourceLine, int callContext) { + return evalStringList(perlCode, currentCode, registers, sourceName, sourceLine, callContext, null); + } + + public static RuntimeList evalStringList(String perlCode, + InterpretedCode currentCode, + RuntimeBase[] registers, + String sourceName, + int sourceLine, + int callContext, + Map siteRegistry) { try { evalTrace("EvalStringHandler enter ctx=" + callContext + " srcName=" + sourceName + " srcLine=" + sourceLine + " codeLen=" + (perlCode != null ? perlCode.length() : -1)); @@ -128,11 +148,14 @@ public static RuntimeList evalStringList(String perlCode, RuntimeBase[] capturedVars = new RuntimeBase[0]; Map adjustedRegistry = null; - if (currentCode != null && currentCode.variableRegistry != null && registers != null) { + // Use per-eval-site registry if available, otherwise fall back to global registry + Map registry = siteRegistry != null ? siteRegistry + : (currentCode != null ? currentCode.variableRegistry : null); + + if (registry != null && registers != null) { - // Sort parent variables by register index for consistent ordering List> sortedVars = new ArrayList<>( - currentCode.variableRegistry.entrySet() + registry.entrySet() ); sortedVars.sort(Map.Entry.comparingByValue()); @@ -184,6 +207,13 @@ public static RuntimeList evalStringList(String perlCode, } } capturedVars = capturedList.toArray(new RuntimeBase[0]); + if (EVAL_TRACE) { + evalTrace("EvalStringHandler varRegistry keys=" + registry.keySet()); + evalTrace("EvalStringHandler adjustedRegistry=" + adjustedRegistry); + for (int ci = 0; ci < capturedVars.length; ci++) { + evalTrace("EvalStringHandler captured[" + ci + "]=" + (capturedVars[ci] != null ? capturedVars[ci].getClass().getSimpleName() + ":" + capturedVars[ci] : "null")); + } + } } // Step 4: Compile AST to interpreter bytecode with adjusted variable registry. diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java index a77d0a5ac..354ffa781 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java @@ -3,6 +3,7 @@ import org.perlonjava.runtime.runtimetypes.*; import java.util.BitSet; +import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -28,6 +29,7 @@ public class InterpretedCode extends RuntimeCode { public final int maxRegisters; // Number of registers needed public final RuntimeBase[] capturedVars; // Closure support (captured from outer scope) public final Map variableRegistry; // Variable name → register index (for eval STRING) + public final List> evalSiteRegistries; // Per-eval-site variable registries // Lexical pragma state (for eval STRING to inherit) public final int strictOptions; // Strict flags at compile time @@ -67,7 +69,7 @@ public InterpretedCode(int[] bytecode, Object[] constants, String[] stringPool, int strictOptions, int featureFlags, BitSet warningFlags) { this(bytecode, constants, stringPool, maxRegisters, capturedVars, sourceName, sourceLine, pcToTokenIndex, variableRegistry, errorUtil, - strictOptions, featureFlags, warningFlags, "main"); + strictOptions, featureFlags, warningFlags, "main", null); } public InterpretedCode(int[] bytecode, Object[] constants, String[] stringPool, @@ -78,7 +80,21 @@ public InterpretedCode(int[] bytecode, Object[] constants, String[] stringPool, ErrorMessageUtil errorUtil, int strictOptions, int featureFlags, BitSet warningFlags, String compilePackage) { - super(null, new java.util.ArrayList<>()); // Call RuntimeCode constructor with null prototype, empty attributes + this(bytecode, constants, stringPool, maxRegisters, capturedVars, + sourceName, sourceLine, pcToTokenIndex, variableRegistry, errorUtil, + strictOptions, featureFlags, warningFlags, compilePackage, null); + } + + public InterpretedCode(int[] bytecode, Object[] constants, String[] stringPool, + int maxRegisters, RuntimeBase[] capturedVars, + String sourceName, int sourceLine, + TreeMap pcToTokenIndex, + Map variableRegistry, + ErrorMessageUtil errorUtil, + int strictOptions, int featureFlags, BitSet warningFlags, + String compilePackage, + List> evalSiteRegistries) { + super(null, new java.util.ArrayList<>()); this.bytecode = bytecode; this.constants = constants; this.stringPool = stringPool; @@ -88,6 +104,7 @@ public InterpretedCode(int[] bytecode, Object[] constants, String[] stringPool, this.sourceLine = sourceLine; this.pcToTokenIndex = pcToTokenIndex; this.variableRegistry = variableRegistry; + this.evalSiteRegistries = evalSiteRegistries; this.errorUtil = errorUtil; this.strictOptions = strictOptions; this.featureFlags = featureFlags; @@ -163,15 +180,17 @@ public InterpretedCode withCapturedVars(RuntimeBase[] capturedVars) { this.constants, this.stringPool, this.maxRegisters, - capturedVars, // New captured vars + capturedVars, this.sourceName, this.sourceLine, - this.pcToTokenIndex, // Preserve token index map - this.variableRegistry, // Preserve variable registry - this.errorUtil, // Preserve error util - this.strictOptions, // Preserve strict flags - this.featureFlags, // Preserve feature flags - this.warningFlags // Preserve warning flags + this.pcToTokenIndex, + this.variableRegistry, + this.errorUtil, + this.strictOptions, + this.featureFlags, + this.warningFlags, + this.compilePackage, + this.evalSiteRegistries ); } @@ -1315,7 +1334,10 @@ public String disassemble() { case Opcodes.EVAL_STRING: rd = bytecode[pc++]; rs = bytecode[pc++]; - sb.append("EVAL_STRING r").append(rd).append(" = eval(r").append(rs).append(")\n"); + int evalCtx = bytecode[pc++]; + int evalSite = bytecode[pc++]; + sb.append("EVAL_STRING r").append(rd).append(" = eval(r").append(rs) + .append(", ctx=").append(evalCtx).append(", site=").append(evalSite).append(")\n"); break; case Opcodes.SELECT_OP: rd = bytecode[pc++]; diff --git a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java index 1bfcbf3f2..f68db44ea 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java @@ -7,6 +7,8 @@ import org.perlonjava.runtime.operators.Time; import org.perlonjava.runtime.runtimetypes.*; +import java.util.Map; + /** * Handler for rarely-used operations called directly by BytecodeInterpreter. * @@ -271,61 +273,62 @@ public static int executeEvalString( int rd = bytecode[pc++]; int stringReg = bytecode[pc++]; int evalCallContext = RuntimeContextType.SCALAR; - // Newer bytecode encodes the eval operator's own call context (VOID/SCALAR/LIST) - // so eval semantics are correct even when the surrounding statement is compiled - // in VOID context. if (pc < bytecode.length) { evalCallContext = bytecode[pc++]; } + int evalSiteIndex = -1; + if (pc < bytecode.length) { + evalSiteIndex = bytecode[pc++]; + } + + // Look up per-eval-site variable registry (scope-correct mapping) + Map siteRegistry = null; + if (evalSiteIndex >= 0 && code.evalSiteRegistries != null + && evalSiteIndex < code.evalSiteRegistries.size()) { + siteRegistry = code.evalSiteRegistries.get(evalSiteIndex); + } - // Get the code string - handle both RuntimeScalar and RuntimeList (from string interpolation) RuntimeBase codeValue = registers[stringReg]; RuntimeScalar codeScalar; if (codeValue instanceof RuntimeScalar) { codeScalar = (RuntimeScalar) codeValue; } else { - // Convert list to scalar (e.g., from string interpolation) codeScalar = codeValue.scalar(); } String perlCode = codeScalar.toString(); evalTrace("EVAL_STRING opcode enter rd=r" + rd + " strReg=r" + stringReg + - " ctx=" + evalCallContext + " outerWantarray=" + ((RuntimeScalar) registers[2]).getInt() + + " ctx=" + evalCallContext + " evalSite=" + evalSiteIndex + " src=" + (code != null ? code.sourceName : "null")); - // Read outer wantarray from register 2 (set by BytecodeInterpreter from the call site context). - // This is the true calling context (VOID/SCALAR/LIST) that wantarray() inside the - // eval body must reflect — exactly as evalStringWithInterpreter receives callContext. int callContext = evalCallContext; if (registers[2] instanceof RuntimeScalar rs) { - // For backward compatibility with older bytecode, or if evalCallContext - // is not set correctly, fall back to the outer wantarray register. if (callContext == 0 && rs.value != null) { callContext = rs.getInt(); } } if (callContext == RuntimeContextType.LIST) { - // Return list context result RuntimeList result = EvalStringHandler.evalStringList( perlCode, - code, // Current InterpretedCode for context - registers, // Current registers for variable access + code, + registers, code.sourceName, code.sourceLine, - callContext + callContext, + siteRegistry ); registers[rd] = result; evalTrace("EVAL_STRING opcode exit LIST stored=" + (registers[rd] != null ? registers[rd].getClass().getSimpleName() : "null") + " scalar=" + result.scalar().toString()); } else { - // Scalar/void context: return scalar result RuntimeScalar result = EvalStringHandler.evalString( perlCode, - code, // Current InterpretedCode for context - registers, // Current registers for variable access + code, + registers, code.sourceName, code.sourceLine, - callContext + callContext, + siteRegistry ); registers[rd] = result; evalTrace("EVAL_STRING opcode exit SCALAR/VOID stored=" + (registers[rd] != null ? registers[rd].getClass().getSimpleName() : "null") + diff --git a/src/main/java/org/perlonjava/frontend/semantic/ScopedSymbolTable.java b/src/main/java/org/perlonjava/frontend/semantic/ScopedSymbolTable.java index 7a09546a8..83fe3b7bf 100644 --- a/src/main/java/org/perlonjava/frontend/semantic/ScopedSymbolTable.java +++ b/src/main/java/org/perlonjava/frontend/semantic/ScopedSymbolTable.java @@ -218,6 +218,20 @@ public int addVariable(String name, String variableDeclType, OperatorNode ast) { return symbolTableStack.peek().addVariable(name, variableDeclType, getCurrentPackage(), ast); } + public void addVariableWithIndex(String name, int index, String variableDeclType) { + clearVisibleVariablesCache(); + symbolTableStack.peek().addVariableWithIndex(name, index, variableDeclType, getCurrentPackage()); + } + + public Map getVisibleVariableRegistry() { + Map registry = new HashMap<>(); + Map visible = getAllVisibleVariables(); + for (SymbolTable.SymbolEntry entry : visible.values()) { + registry.put(entry.name(), entry.index()); + } + return registry; + } + /** * Retrieves the index of a variable, searching from the innermost to the outermost scope. * diff --git a/src/main/java/org/perlonjava/frontend/semantic/SymbolTable.java b/src/main/java/org/perlonjava/frontend/semantic/SymbolTable.java index 2809db169..9bfe654d2 100644 --- a/src/main/java/org/perlonjava/frontend/semantic/SymbolTable.java +++ b/src/main/java/org/perlonjava/frontend/semantic/SymbolTable.java @@ -42,6 +42,13 @@ public int addVariable(String name, String variableDeclType, String perlPackage, * @param name The name of the variable to look up. * @return The index of the variable, or -1 if the variable is not found. */ + public void addVariableWithIndex(String name, int specificIndex, String variableDeclType, String perlPackage) { + variableIndex.put(name, new SymbolEntry(specificIndex, name, variableDeclType, perlPackage, null)); + if (specificIndex >= index) { + index = specificIndex + 1; + } + } + public int getVariableIndex(String name) { // Return the index of the variable, or -1 if not found return variableIndex.getOrDefault(name, new SymbolEntry(-1, null, null, null, null)).index;