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` | 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/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/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/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(); 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;