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..950cf708e 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; @@ -414,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")) { @@ -433,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) { @@ -565,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 @@ -743,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(); @@ -791,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); @@ -1018,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 @@ -1691,13 +1714,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 +1727,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 +1747,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 -> {