From 545620081e6ee91eb8376008a2e264e80cd2ab54 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 27 Feb 2026 15:55:14 +0100 Subject: [PATCH 1/2] Implement dynamic scoping for regex match variables Match variables are now saved/restored per subroutine call using the existing DynamicVariableManager infrastructure, matching Perl behavior where regex match state is dynamically scoped to the enclosing block. JVM backend: Local.localSetup detects matchRegex/replaceRegex in AST and emits RuntimeRegexState.pushLocal(); localTeardown pops via DynamicVariableManager.popToLocalLevel(). Interpreter backend: BytecodeCompiler sets containsRegex flag on InterpretedCode; BytecodeInterpreter.execute() saves/restores in try/finally based on the flag. Proxy resolution: ScalarSpecialVariable.getList() now materializes the value eagerly, and RuntimeList.resolveMatchProxies() is called before local teardown at return boundaries so that return values like ($1, $2) capture the callee match state, not the restored caller state. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/bytecode/BytecodeCompiler.java | 12 +++++- .../backend/bytecode/BytecodeInterpreter.java | 18 +++++++-- .../backend/bytecode/InterpretedCode.java | 2 + .../backend/jvm/EmitterMethodCreator.java | 9 +++++ .../org/perlonjava/backend/jvm/Local.java | 38 +++++++------------ .../runtime/regex/RuntimeRegex.java | 23 +++++++++++ .../runtimetypes/DynamicVariableManager.java | 5 +++ .../runtime/runtimetypes/RuntimeCode.java | 14 ++----- .../runtime/runtimetypes/RuntimeList.java | 9 +++++ .../runtimetypes/RuntimeRegexState.java | 25 ++++++++++++ .../runtimetypes/ScalarSpecialVariable.java | 10 ++++- 11 files changed, 123 insertions(+), 42 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index a1fa58687..ba8e7eac5 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -1,5 +1,6 @@ package org.perlonjava.backend.bytecode; +import org.perlonjava.frontend.analysis.FindDeclarationVisitor; import org.perlonjava.frontend.analysis.Visitor; import org.perlonjava.backend.jvm.EmitterMethodCreator; import org.perlonjava.backend.jvm.EmitterContext; @@ -546,7 +547,7 @@ public InterpretedCode compile(Node node, EmitterContext ctx) { } // Build InterpretedCode - return new InterpretedCode( + InterpretedCode result = new InterpretedCode( toShortArray(), constants.toArray(), stringPool.toArray(new String[0]), @@ -562,6 +563,9 @@ public InterpretedCode compile(Node node, EmitterContext ctx) { warningFlags, // Warning flags for eval STRING inheritance symbolTable.getCurrentPackage() // Compile-time package for eval STRING name resolution ); + result.containsRegex = FindDeclarationVisitor.findOperator(node, "matchRegex") != null + || FindDeclarationVisitor.findOperator(node, "replaceRegex") != null; + return result; } // ========================================================================= @@ -4103,6 +4107,12 @@ public void visit(For1Node node) { variableScopes.peek().put(varName, varReg); allDeclaredVariables.put(varName, varReg); } + } else if (varOp.operator.equals("$") && varOp.operand instanceof IdentifierNode + && globalLoopVarName == null) { + String varName = "$" + ((IdentifierNode) varOp.operand).name; + if (hasVariable(varName)) { + variableScopes.peek().put(varName, varReg); + } } } diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index ca9c24196..ab4e39927 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -48,6 +48,12 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c String frameSubName = subroutineName != null ? subroutineName : (code.subName != null ? code.subName : "(eval)"); InterpreterState.push(code, framePackageName, frameSubName); + int regexLocalLevel = -1; + if (code.containsRegex) { + regexLocalLevel = DynamicVariableManager.getLocalLevel(); + RuntimeRegexState.pushLocal(); + } + // Pure register file (NOT stack-based - matches compiler for control flow correctness) RuntimeBase[] registers = new RuntimeBase[code.maxRegisters]; @@ -88,14 +94,16 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c break; case Opcodes.RETURN: { - // Return from subroutine: return rd int retReg = bytecode[pc++]; RuntimeBase retVal = registers[retReg]; - if (retVal == null) { return new RuntimeList(); } - return retVal.getList(); + RuntimeList retList = retVal.getList(); + if (code.containsRegex) { + RuntimeList.resolveMatchProxies(retList); + } + return retList; } case Opcodes.GOTO: { @@ -2245,7 +2253,9 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c String errorMessage = formatInterpreterError(code, pc, e); throw new RuntimeException(errorMessage, e); } finally { - // Always pop the interpreter state + if (regexLocalLevel >= 0) { + DynamicVariableManager.popToLocalLevel(regexLocalLevel); + } InterpreterState.pop(); } } diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java index 0185767b0..b644bc460 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java @@ -35,6 +35,8 @@ public class InterpretedCode extends RuntimeCode { public final BitSet warningFlags; // Warning flags at compile time public final String compilePackage; // Package at compile time (for eval STRING name resolution) + public boolean containsRegex; // Whether this code contains regex ops (for match var scoping) + // Debug information (optional) public final String sourceName; // Source file name (for stack traces) public final int sourceLine; // Source line number diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java index c755d7ca9..5226e0844 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java @@ -741,6 +741,15 @@ private static byte[] getBytecodeInternal(EmitterContext ctx, Node ast, boolean mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/runtimetypes/RuntimeBase", "getList", "()Lorg/perlonjava/runtime/runtimetypes/RuntimeList;", false); mv.visitVarInsn(Opcodes.ASTORE, returnListSlot); + if (localRecord.containsRegex()) { + mv.visitVarInsn(Opcodes.ALOAD, returnListSlot); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/runtimetypes/RuntimeList", + "resolveMatchProxies", + "(Lorg/perlonjava/runtime/runtimetypes/RuntimeList;)V", + false); + } + // Phase 3: Check for control flow markers // RuntimeList is on stack after getList() diff --git a/src/main/java/org/perlonjava/backend/jvm/Local.java b/src/main/java/org/perlonjava/backend/jvm/Local.java index e2fb232f5..10065f9f9 100644 --- a/src/main/java/org/perlonjava/backend/jvm/Local.java +++ b/src/main/java/org/perlonjava/backend/jvm/Local.java @@ -22,13 +22,13 @@ public class Local { * and the index of the dynamic variable stack. */ static localRecord localSetup(EmitterContext ctx, Node ast, MethodVisitor mv) { - // Check if the code contains a 'local' operator boolean containsLocalOperator = FindDeclarationVisitor.findOperator(ast, "local") != null; + boolean containsRegex = FindDeclarationVisitor.findOperator(ast, "matchRegex") != null + || FindDeclarationVisitor.findOperator(ast, "replaceRegex") != null; + boolean needsDynamicSave = containsLocalOperator || containsRegex; int dynamicIndex = -1; - if (containsLocalOperator) { - // Allocate a local variable to store the dynamic variable stack index + if (needsDynamicSave) { dynamicIndex = ctx.symbolTable.allocateLocalVariable(); - // Get the current level of the dynamic variable stack and store it mv.visitMethodInsn(Opcodes.INVOKESTATIC, "org/perlonjava/runtime/runtimetypes/DynamicVariableManager", "getLocalLevel", @@ -36,21 +36,18 @@ static localRecord localSetup(EmitterContext ctx, Node ast, MethodVisitor mv) { false); mv.visitVarInsn(Opcodes.ISTORE, dynamicIndex); } - return new localRecord(containsLocalOperator, dynamicIndex); + if (containsRegex) { + mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/runtimetypes/RuntimeRegexState", + "pushLocal", + "()V", + false); + } + return new localRecord(needsDynamicSave, containsRegex, dynamicIndex); } - /** - * Tears down the local variable setup by restoring the dynamic variable stack - * to its previous level if a 'local' operator was present. - * - * @param localRecord The record containing information about the 'local' operator - * and the dynamic variable stack index. - * @param mv The method visitor used to generate bytecode instructions. - */ static void localTeardown(localRecord localRecord, MethodVisitor mv) { - // Add `local` teardown logic - if (localRecord.containsLocalOperator()) { - // Restore the dynamic variable stack to the recorded level + if (localRecord.needsDynamicSave()) { mv.visitVarInsn(Opcodes.ILOAD, localRecord.dynamicIndex()); mv.visitMethodInsn(Opcodes.INVOKESTATIC, "org/perlonjava/runtime/runtimetypes/DynamicVariableManager", @@ -60,13 +57,6 @@ static void localTeardown(localRecord localRecord, MethodVisitor mv) { } } - /** - * A record to store information about the presence of a 'local' operator - * and the index of the dynamic variable stack. - * - * @param containsLocalOperator Indicates if a 'local' operator is present. - * @param dynamicIndex The index of the dynamic variable stack. - */ - record localRecord(boolean containsLocalOperator, int dynamicIndex) { + record localRecord(boolean needsDynamicSave, boolean containsRegex, int dynamicIndex) { } } diff --git a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java index 48cf2a194..c7f1e0de8 100644 --- a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java +++ b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java @@ -56,6 +56,29 @@ protected boolean removeEldestEntry(Map.Entry eldest) { public static String lastSuccessfulMatchString = null; // ${^LAST_SUCCESSFUL_PATTERN} public static RuntimeRegex lastSuccessfulPattern = null; + + public static Object[] saveMatchState() { + return new Object[]{ + globalMatcher, globalMatchString, + lastMatchedString, lastMatchStart, lastMatchEnd, + lastSuccessfulMatchedString, lastSuccessfulMatchStart, lastSuccessfulMatchEnd, + lastSuccessfulMatchString, lastSuccessfulPattern + }; + } + + public static void restoreMatchState(Object[] state) { + globalMatcher = (Matcher) state[0]; + globalMatchString = (String) state[1]; + lastMatchedString = (String) state[2]; + lastMatchStart = (Integer) state[3]; + lastMatchEnd = (Integer) state[4]; + lastSuccessfulMatchedString = (String) state[5]; + lastSuccessfulMatchStart = (Integer) state[6]; + lastSuccessfulMatchEnd = (Integer) state[7]; + lastSuccessfulMatchString = (String) state[8]; + lastSuccessfulPattern = (RuntimeRegex) state[9]; + } + // Indicates if \G assertion is used private final boolean useGAssertion = false; // Compiled regex pattern diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/DynamicVariableManager.java b/src/main/java/org/perlonjava/runtime/runtimetypes/DynamicVariableManager.java index b46d02d90..4df66404d 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/DynamicVariableManager.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/DynamicVariableManager.java @@ -49,6 +49,11 @@ public static RuntimeGlob pushLocalVariable(RuntimeGlob variable) { return variable; } + public static void pushLocalDynamicState(DynamicState state) { + state.dynamicSaveState(); + variableStack.push(state); + } + /** * Pops dynamic variables from the stack until the stack size matches the specified target local level. * This is useful for restoring the stack to a previous state by removing any variables added after that state. diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index d45043e7b..c1513c59f 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -17,6 +17,7 @@ import org.perlonjava.backend.bytecode.BytecodeCompiler; import org.perlonjava.backend.bytecode.InterpretedCode; import org.perlonjava.backend.bytecode.InterpreterState; +import org.perlonjava.runtime.regex.RuntimeRegex; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; @@ -1691,13 +1692,11 @@ public boolean defined() { */ public RuntimeList apply(RuntimeArray a, int callContext) { if (constantValue != null) { - // Alternative way to create constants like: `$constant::{_CAN_PCS} = \$const` return new RuntimeList(constantValue); } try { - // Wait for the compilerThread to finish if it exists if (this.compilerSupplier != null) { - this.compilerSupplier.get(); // Wait for the task to finish + this.compilerSupplier.get(); } if (isStatic) { @@ -1706,17 +1705,13 @@ public RuntimeList apply(RuntimeArray a, int callContext) { return (RuntimeList) this.methodHandle.invoke(this.codeObject, a, callContext); } } catch (NullPointerException e) { - if (this.methodHandle == null) { throw new PerlCompilerException("Subroutine exists but has null method handle (possible compilation or registration error) at "); } else if (this.codeObject == null && !isStatic) { throw new PerlCompilerException("Subroutine exists but has null code object at "); } else { - // Original NPE from somewhere else throw new PerlCompilerException("Null pointer exception in subroutine call: " + e.getMessage() + " at "); } - - //throw new PerlCompilerException("Undefined subroutine called at "); } catch (InvocationTargetException e) { Throwable targetException = e.getTargetException(); if (!(targetException instanceof RuntimeException)) { @@ -1730,14 +1725,11 @@ public RuntimeList apply(RuntimeArray a, int callContext) { public RuntimeList apply(String subroutineName, RuntimeArray a, int callContext) { if (constantValue != null) { - // Alternative way to create constants like: `$constant::{_CAN_PCS} = \$const` return new RuntimeList(constantValue); } try { - // Wait for the compilerThread to finish if it exists if (this.compilerSupplier != null) { - // System.out.println("Waiting for compiler thread to finish..."); - this.compilerSupplier.get(); // Wait for the task to finish + this.compilerSupplier.get(); } if (isStatic) { diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java index 9c020d76e..bc6383058 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java @@ -240,6 +240,15 @@ public RuntimeList getList() { return this; } + public static void resolveMatchProxies(RuntimeList list) { + for (int i = 0; i < list.elements.size(); i++) { + RuntimeBase elem = list.elements.get(i); + if (elem instanceof ScalarSpecialVariable ssv) { + list.elements.set(i, ssv.getValueAsScalar()); + } + } + } + /** * Evaluates the boolean representation of the list. * diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeRegexState.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeRegexState.java index e69de29bb..ad6806052 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeRegexState.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeRegexState.java @@ -0,0 +1,25 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.perlonjava.runtime.regex.RuntimeRegex; + +public class RuntimeRegexState implements DynamicState { + + private Object[] savedState; + + public static void pushLocal() { + DynamicVariableManager.pushLocalDynamicState(new RuntimeRegexState()); + } + + @Override + public void dynamicSaveState() { + savedState = RuntimeRegex.saveMatchState(); + } + + @Override + public void dynamicRestoreState() { + if (savedState != null) { + RuntimeRegex.restoreMatchState(savedState); + savedState = null; + } + } +} diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java index cfea89ab4..d7ff69f65 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java @@ -88,7 +88,13 @@ public RuntimeScalar set(RuntimeScalar value) { return super.set(value); } - // Add itself to a RuntimeArray. + @Override + public RuntimeList getList() { + RuntimeList list = new RuntimeList(); + this.addToList(list); + return list; + } + public void addToArray(RuntimeArray array) { array.elements.add(new RuntimeScalar(this.getValueAsScalar())); } @@ -108,7 +114,7 @@ public RuntimeScalar addToScalar(RuntimeScalar var) { * * @return The RuntimeScalar value of the special variable, or null if not available. */ - private RuntimeScalar getValueAsScalar() { + RuntimeScalar getValueAsScalar() { try { RuntimeScalar result = switch (variableId) { case CAPTURE -> { From c684e2537bdb13eab7287d540686a8caaff0e529 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 27 Feb 2026 17:08:08 +0100 Subject: [PATCH 2/2] Fix eval STRING leaking captured variable aliases into global namespace When eval STRING runs, it creates temporary aliases in the global namespace so that BEGIN blocks inside the eval can access outer lexical variables. These aliases were not being cleaned up after compilation, causing `my` variable re-declarations in loops to pick up stale values via retrieveBeginScalar instead of creating fresh objects. This fix tracks the alias keys created during eval STRING compilation and removes them in the finally block after parsing/compilation is done. Fixes ExifTool test 22 (InsertTagValues) where `my` variables inside a while loop containing `eval $expr` were not being re-initialized on each iteration. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../runtime/runtimetypes/RuntimeCode.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index c1513c59f..950cf708e 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -415,6 +415,7 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Obje // Then when "say @arr" is parsed in the BEGIN, it resolves to BEGIN_PKG_x::@arr // which is aliased to the runtime array with values (a, b). Map capturedVars = capturedSymbolTable.getAllVisibleVariables(); + List evalAliasKeys = new ArrayList<>(); for (SymbolTable.SymbolEntry entry : capturedVars.values()) { if (!entry.name().equals("@_") && !entry.decl().isEmpty() && !entry.name().startsWith("&")) { if (!entry.decl().equals("our")) { @@ -434,6 +435,7 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Obje // entry.name() is "@arr" but the key should be "packageName::arr" String varNameWithoutSigil = entry.name().substring(1); // Remove the sigil String fullName = packageName + "::" + varNameWithoutSigil; + evalAliasKeys.add(fullName); // Alias the global to the runtime value if (runtimeValue instanceof RuntimeArray) { @@ -566,6 +568,17 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Obje setCurrentScope(capturedSymbolTable); + // Clean up eval STRING aliases from global namespace. + // These aliases were created before parsing so BEGIN blocks inside the eval + // could access outer lexicals. After compilation, they are no longer needed. + // Leaving them would cause `my` re-declarations in loops to pick up stale + // values via retrieveBeginScalar instead of creating fresh objects. + for (String key : evalAliasKeys) { + GlobalVariable.globalVariables.remove(key); + GlobalVariable.globalArrays.remove(key); + GlobalVariable.globalHashes.remove(key); + } + // Store source lines in symbol table if $^P flags are set // Do this on both success and failure paths when flags require retention // Use the original evalString and actualFileName; AST may be null on failure @@ -744,6 +757,7 @@ public static RuntimeList evalStringWithInterpreter( // Save dynamic variable level to restore after eval int dynamicVarLevel = DynamicVariableManager.getLocalLevel(); + List evalAliasKeys = new ArrayList<>(); try { String evalString = code.toString(); @@ -792,6 +806,7 @@ public static RuntimeList evalStringWithInterpreter( String packageName = PersistentVariable.beginPackage(operatorAst.id); String varNameWithoutSigil = entry.name().substring(1); String fullName = packageName + "::" + varNameWithoutSigil; + evalAliasKeys.add(fullName); if (runtimeValue instanceof RuntimeArray) { GlobalVariable.globalArrays.put(fullName, (RuntimeArray) runtimeValue); @@ -1019,6 +1034,13 @@ public static RuntimeList evalStringWithInterpreter( // Restore dynamic variables (local) to their state before eval DynamicVariableManager.popToLocalLevel(dynamicVarLevel); + // Clean up eval STRING aliases from global namespace + for (String key : evalAliasKeys) { + GlobalVariable.globalVariables.remove(key); + GlobalVariable.globalArrays.remove(key); + GlobalVariable.globalHashes.remove(key); + } + // Store source lines in debugger symbol table if $^P flags are set // Do this on both success and failure paths when flags require retention // ast and tokens may be null if parsing failed early, but storeSourceLines handles that