From bbcbdfd43ff4ec52eed99368c63e6d7361e76fa0 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Fri, 6 Feb 2026 22:27:55 +0100 Subject: [PATCH 1/2] Fix BEGIN blocks to access outer lexical variables in eval STRING In perl5, BEGIN blocks inside eval STRING can access outer lexical variables with their runtime values. For example: my @imports = qw(md5 md5_hex); eval q{ use Digest::MD5 @imports }; # BEGIN sees @imports Previously, BEGIN blocks would see empty variables because they execute during parsing, before the eval class is instantiated with runtime values. Solution: 1. Modified evalStringHelper() to accept runtime values array 2. Store runtime values in ThreadLocal during eval STRING parsing 3. In SpecialBlockParser, alias special globals to runtime objects 4. Global variable keys don't include sigil (use "pkg::arr" not "pkg::@arr") 5. Added backwards-compatible evalStringHelper(code, evalTag) overload This fixes Test::More use_ok() pattern which relies on this behavior. Note: control_flow.t test 11 is currently failing with "Can't find label" error. This needs further investigation as it may be a separate issue. Co-Authored-By: Claude Opus 4.6 --- .../java/org/perlonjava/codegen/EmitEval.java | 34 +++- .../perlonjava/parser/SpecialBlockParser.java | 44 +++- .../org/perlonjava/runtime/RuntimeCode.java | 188 +++++++++++++++++- 3 files changed, 246 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/perlonjava/codegen/EmitEval.java b/src/main/java/org/perlonjava/codegen/EmitEval.java index b380f92a2..7890aea9e 100644 --- a/src/main/java/org/perlonjava/codegen/EmitEval.java +++ b/src/main/java/org/perlonjava/codegen/EmitEval.java @@ -179,24 +179,38 @@ static void handleEvalOperator(EmitterVisitor emitterVisitor, OperatorNode node) mv.visitLdcInsn(evalTag); // Stack: [RuntimeScalar(String), String] + // Calculate how many variables need to be passed + // We skip 'this', '@_', and 'wantarray' which are handled separately + int skipVariables = EmitterMethodCreator.skipVariables; + + // Build array of runtime values for captured variables + // These are passed to evalStringHelper so BEGIN blocks can access outer lexical variables + mv.visitIntInsn(Opcodes.BIPUSH, newEnv.length - skipVariables); + mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object"); + // Stack: [RuntimeScalar(String), String, Object[]] + + // Fill the runtime values array with actual variable values from local variables + for (Integer index : newSymbolTable.getAllVisibleVariables().keySet()) { + if (index >= skipVariables) { + String varName = newEnv[index]; + mv.visitInsn(Opcodes.DUP); + mv.visitIntInsn(Opcodes.BIPUSH, index - skipVariables); + mv.visitVarInsn(Opcodes.ALOAD, emitterVisitor.ctx.symbolTable.getVariableIndex(varName)); + mv.visitInsn(Opcodes.AASTORE); + } + } + // Stack: [RuntimeScalar(String), String, Object[]] + // Call evalStringHelper to compile the eval string at runtime - // This method: - // 1. Retrieves the EmitterContext using evalTag - // 2. Parses and compiles the eval string - // 3. Returns the generated Class object - // 4. Caches the result for repeated evals of the same string + // Now passes runtime values so BEGIN blocks can access outer lexical variables mv.visitMethodInsn( Opcodes.INVOKESTATIC, "org/perlonjava/runtime/RuntimeCode", "evalStringHelper", - "(Lorg/perlonjava/runtime/RuntimeScalar;Ljava/lang/String;)Ljava/lang/Class;", + "(Lorg/perlonjava/runtime/RuntimeScalar;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/Class;", false); // Stack: [Class] - // Calculate how many variables need to be passed to the constructor - // We skip 'this', '@_', and 'wantarray' which are handled separately - int skipVariables = EmitterMethodCreator.skipVariables; - // Create array of parameter types for the constructor // Each captured variable becomes a constructor parameter (including null gaps) mv.visitIntInsn(Opcodes.BIPUSH, newEnv.length - skipVariables); diff --git a/src/main/java/org/perlonjava/parser/SpecialBlockParser.java b/src/main/java/org/perlonjava/parser/SpecialBlockParser.java index cfc5320f0..f2890a9e9 100644 --- a/src/main/java/org/perlonjava/parser/SpecialBlockParser.java +++ b/src/main/java/org/perlonjava/parser/SpecialBlockParser.java @@ -132,13 +132,15 @@ static RuntimeList runSpecialBlock(Parser parser, String blockPhase, Node block) if (entry.name().startsWith("&")) { continue; } - + + String packageName; if (entry.decl().equals("our")) { // "our" variable lives in a Perl package + packageName = entry.perlPackage(); // Emit: package PKG nodes.add( new OperatorNode("package", - new IdentifierNode(entry.perlPackage(), tokenIndex), tokenIndex)); + new IdentifierNode(packageName, tokenIndex), tokenIndex)); } else { // "my" or "state" variable live in a special BEGIN package // Retrieve the variable id from the AST; create a new id if needed @@ -146,12 +148,48 @@ static RuntimeList runSpecialBlock(Parser parser, String blockPhase, Node block) if (ast.id == 0) { ast.id = EmitterMethodCreator.classCounter++; } + packageName = PersistentVariable.beginPackage(ast.id); // Emit: package BEGIN_PKG nodes.add( new OperatorNode("package", - new IdentifierNode(PersistentVariable.beginPackage(ast.id), tokenIndex), tokenIndex)); + new IdentifierNode(packageName, tokenIndex), tokenIndex)); } + // CLEAN FIX: For eval STRING, make special globals aliases to closed variables + // This allows BEGIN blocks to access outer lexical variables with their runtime values. + // + // In perl5: my @arr = qw(a b); eval q{ BEGIN { say @arr } }; # prints: a b + // The special global BEGIN_PKG::@arr is an ALIAS to the closed @arr variable. + // + // Implementation: Set the global variable to reference the same runtime object. + if (!entry.decl().equals("our")) { + RuntimeCode.EvalRuntimeContext evalCtx = RuntimeCode.getEvalRuntimeContext(); + if (evalCtx != null) { + Object runtimeValue = evalCtx.getRuntimeValue(entry.name()); + if (runtimeValue != null) { + // Create alias: set special global to reference the runtime object + // IMPORTANT: Global variable keys do NOT include the sigil + // entry.name() is "@arr" but the key should be "packageName::arr" + String varNameWithoutSigil = entry.name().substring(1); // Remove the sigil + String fullName = packageName + "::" + varNameWithoutSigil; + + // Put in the appropriate global map based on variable type + if (runtimeValue instanceof RuntimeArray) { + GlobalVariable.globalArrays.put(fullName, (RuntimeArray) runtimeValue); + parser.ctx.logDebug("BEGIN block: Aliased array " + fullName); + } else if (runtimeValue instanceof RuntimeHash) { + GlobalVariable.globalHashes.put(fullName, (RuntimeHash) runtimeValue); + parser.ctx.logDebug("BEGIN block: Aliased hash " + fullName); + } else if (runtimeValue instanceof RuntimeScalar) { + GlobalVariable.globalVariables.put(fullName, (RuntimeScalar) runtimeValue); + parser.ctx.logDebug("BEGIN block: Aliased scalar " + fullName); + } + } + } + } + // Emit: our $var + // When we've aliased the variable above, the "our" declaration will fetch the + // existing global (our alias) instead of creating a new empty one. nodes.add( new OperatorNode( "our", diff --git a/src/main/java/org/perlonjava/runtime/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/RuntimeCode.java index f8d511188..a96162910 100644 --- a/src/main/java/org/perlonjava/runtime/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/RuntimeCode.java @@ -14,6 +14,7 @@ import org.perlonjava.operators.ModuleOperators; import org.perlonjava.scriptengine.PerlLanguageProvider; import org.perlonjava.symbols.ScopedSymbolTable; +import org.perlonjava.symbols.SymbolTable; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; @@ -40,6 +41,82 @@ public class RuntimeCode extends RuntimeBase implements RuntimeScalarReference { // Lookup object for performing method handle operations public static final MethodHandles.Lookup lookup = MethodHandles.lookup(); + + /** + * ThreadLocal storage for runtime values of captured variables during eval STRING compilation. + * + * PROBLEM: In perl5, BEGIN blocks inside eval STRING can access outer lexical variables' runtime values: + * my @imports = qw(a b); + * eval q{ BEGIN { say @imports } }; # perl5 prints: a b + * + * In PerlOnJava, BEGIN blocks execute during parsing (before the eval class is instantiated), + * so they couldn't access runtime values - they would see empty variables. + * + * SOLUTION: When evalStringHelper() is called, the runtime values are stored in this ThreadLocal. + * During parsing, when SpecialBlockParser sets up BEGIN blocks, it can access these runtime values + * and use them to initialize the special globals that lexical variables become in BEGIN blocks. + * + * This ThreadLocal stores: + * - Key: The evalTag identifying this eval compilation + * - Value: EvalRuntimeContext containing: + * - runtimeValues: Object[] of captured variable values + * - capturedEnv: String[] of captured variable names (matching array indices) + * + * Thread-safety: Each thread's eval compilation uses its own ThreadLocal storage, so parallel + * eval compilations don't interfere with each other. + */ + private static final ThreadLocal evalRuntimeContext = new ThreadLocal<>(); + + /** + * Container for runtime context during eval STRING compilation. + * Holds both the runtime values and variable names so SpecialBlockParser can + * match variables to their values. + */ + public static class EvalRuntimeContext { + public final Object[] runtimeValues; + public final String[] capturedEnv; + public final String evalTag; + + public EvalRuntimeContext(Object[] runtimeValues, String[] capturedEnv, String evalTag) { + this.runtimeValues = runtimeValues; + this.capturedEnv = capturedEnv; + this.evalTag = evalTag; + } + + /** + * Get the runtime value for a variable by name. + * + * IMPORTANT: The capturedEnv array includes all variables (including 'this', '@_', 'wantarray'), + * but runtimeValues array skips the first skipVariables (currently 3). + * So if @imports is at capturedEnv[5], its value is at runtimeValues[5-3=2]. + * + * @param varName The variable name (e.g., "@imports", "$scalar") + * @return The runtime value, or null if not found + */ + public Object getRuntimeValue(String varName) { + int skipVariables = 3; // 'this', '@_', 'wantarray' + for (int i = skipVariables; i < capturedEnv.length; i++) { + if (varName.equals(capturedEnv[i])) { + int runtimeIndex = i - skipVariables; + if (runtimeIndex >= 0 && runtimeIndex < runtimeValues.length) { + return runtimeValues[runtimeIndex]; + } + } + } + return null; + } + } + + /** + * Get the current eval runtime context for accessing variable runtime values during parsing. + * This is called by SpecialBlockParser when setting up BEGIN blocks. + * + * @return The current eval runtime context, or null if not in eval STRING compilation + */ + public static EvalRuntimeContext getEvalRuntimeContext() { + return evalRuntimeContext.get(); + } + // Cache for memoization of evalStringHelper results private static final int CLASS_CACHE_SIZE = 100; private static final Map> evalCache = new LinkedHashMap>(CLASS_CACHE_SIZE, 0.75f, true) { @@ -122,26 +199,71 @@ public static void copy(RuntimeCode code, RuntimeCode codeFrom) { code.codeObject = codeFrom.codeObject; } + /** + * Backwards-compatible overload for code compiled before runtimeValues parameter was added. + * This allows pre-compiled Perl modules to continue working with the new signature. + * + * @param code the RuntimeScalar containing the eval string + * @param evalTag the tag used to retrieve the eval context + * @return the compiled Class representing the anonymous subroutine + * @throws Exception if an error occurs during compilation + */ + public static Class evalStringHelper(RuntimeScalar code, String evalTag) throws Exception { + return evalStringHelper(code, evalTag, new Object[0]); + } + /** * Compiles the text of an eval string into a Class that represents an anonymous subroutine. * After the Class is returned to the caller, an instance of the Class will be populated * with closure variables, and then makeCodeObject() will be called to transform the Class * instance into a Perl CODE object. * - * @param code the RuntimeScalar containing the eval string - * @param evalTag the tag used to retrieve the eval context + * IMPORTANT CHANGE: This method now accepts runtime values of captured variables. + * + * WHY THIS IS NEEDED: + * In perl5, BEGIN blocks inside eval STRING can access outer lexical variables' runtime values. + * For example: + * my @imports = qw(md5 md5_hex); + * eval q{ use Digest::MD5 @imports }; # BEGIN block sees @imports = (md5 md5_hex) + * + * Previously in PerlOnJava, BEGIN blocks would see empty variables because they execute + * during parsing, before the eval class is instantiated with runtime values. + * + * NOW: We pass runtime values to this method and store them in ThreadLocal storage. + * SpecialBlockParser can then access these values when setting up BEGIN blocks, + * allowing lexical variables to be initialized with their runtime values. + * + * @param code the RuntimeScalar containing the eval string + * @param evalTag the tag used to retrieve the eval context + * @param runtimeValues the runtime values of captured variables (Object[] matching capturedEnv order) * @return the compiled Class representing the anonymous subroutine * @throws Exception if an error occurs during compilation */ - public static Class evalStringHelper(RuntimeScalar code, String evalTag) throws Exception { + public static Class evalStringHelper(RuntimeScalar code, String evalTag, Object[] runtimeValues) throws Exception { // Retrieve the eval context that was saved at program compile-time EmitterContext ctx = RuntimeCode.evalContext.get(evalTag); - // Check if the eval string contains non-ASCII characters - // If so, treat it as Unicode source to preserve Unicode characters during parsing - // EXCEPT for evalbytes, which must treat everything as bytes - String evalString = code.toString(); + // Store runtime values in ThreadLocal so SpecialBlockParser can access them during parsing. + // This enables BEGIN blocks to see outer lexical variables' runtime values. + // + // CRITICAL: The runtimeValues array matches capturedEnv order (both skip first 3 variables). + // SpecialBlockParser will use getRuntimeValue() to look up values by variable name. + // + // Example: If @imports is at capturedEnv[5], its runtime value is at runtimeValues[5-3=2] + // (because both arrays skip 'this', '@_', and 'wantarray') + EvalRuntimeContext runtimeCtx = new EvalRuntimeContext( + runtimeValues, + ctx.capturedEnv, // Variable names in same order as runtimeValues + evalTag + ); + evalRuntimeContext.set(runtimeCtx); + + try { + // Check if the eval string contains non-ASCII characters + // If so, treat it as Unicode source to preserve Unicode characters during parsing + // EXCEPT for evalbytes, which must treat everything as bytes + String evalString = code.toString(); boolean hasUnicode = false; if (!ctx.isEvalbytes && code.type != RuntimeScalarType.BYTE_STRING) { for (int i = 0; i < evalString.length(); i++) { @@ -220,6 +342,51 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag) thro // the eval code is parsed with the correct feature/strict/warning context ScopedSymbolTable parseSymbolTable = capturedSymbolTable.snapShot(); + // CRITICAL: Pre-create aliases for captured variables BEFORE parsing + // This allows BEGIN blocks in the eval string to access outer lexical variables. + // + // When the eval string is parsed, variable references in BEGIN blocks will be + // resolved to these special package globals that we're aliasing now. + // + // Example: my @arr = qw(a b); eval q{ BEGIN { say @arr } }; + // We create: globalArrays["BEGIN_PKG_x::@arr"] = (the runtime @arr object) + // 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(); + for (SymbolTable.SymbolEntry entry : capturedVars.values()) { + if (!entry.name().equals("@_") && !entry.decl().isEmpty() && !entry.name().startsWith("&")) { + if (!entry.decl().equals("our")) { + // "my" or "state" variables get special BEGIN package globals + Object runtimeValue = runtimeCtx.getRuntimeValue(entry.name()); + if (runtimeValue != null) { + // Get or create the special package ID + // IMPORTANT: We need to set the ID NOW (before parsing) so that when + // runSpecialBlock is called during parsing, it uses the SAME ID + OperatorNode ast = entry.ast(); + if (ast != null) { + if (ast.id == 0) { + ast.id = EmitterMethodCreator.classCounter++; + } + String packageName = PersistentVariable.beginPackage(ast.id); + // IMPORTANT: Global variable keys do NOT include the sigil + // entry.name() is "@arr" but the key should be "packageName::arr" + String varNameWithoutSigil = entry.name().substring(1); // Remove the sigil + String fullName = packageName + "::" + varNameWithoutSigil; + + // Alias the global to the runtime value + if (runtimeValue instanceof RuntimeArray) { + GlobalVariable.globalArrays.put(fullName, (RuntimeArray) runtimeValue); + } else if (runtimeValue instanceof RuntimeHash) { + GlobalVariable.globalHashes.put(fullName, (RuntimeHash) runtimeValue); + } else if (runtimeValue instanceof RuntimeScalar) { + GlobalVariable.globalVariables.put(fullName, (RuntimeScalar) runtimeValue); + } + } + } + } + } + } + EmitterContext evalCtx = new EmitterContext( new JavaClassInfo(), // internal java class name parseSymbolTable, // symbolTable @@ -302,6 +469,13 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag) thro } return generatedClass; + } finally { + // Clean up ThreadLocal to prevent memory leaks + // IMPORTANT: Always clean up ThreadLocal in finally block to ensure it's removed + // even if compilation fails. Failure to do so could cause memory leaks in + // long-running applications with thread pools. + evalRuntimeContext.remove(); + } } /** From f7a73de772c20262c0c7be90026ab79a1cbd0c17 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Sat, 7 Feb 2026 14:41:14 +0100 Subject: [PATCH 2/2] Fix pos() reset to clear zero-length /g guard --- .../perlonjava/runtime/RuntimePosLvalue.java | 56 ++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/perlonjava/runtime/RuntimePosLvalue.java b/src/main/java/org/perlonjava/runtime/RuntimePosLvalue.java index 2a67bc3a0..f13a306ef 100644 --- a/src/main/java/org/perlonjava/runtime/RuntimePosLvalue.java +++ b/src/main/java/org/perlonjava/runtime/RuntimePosLvalue.java @@ -49,7 +49,7 @@ public static RuntimeScalar pos(RuntimeScalar perlVariable) { if (cachedEntry == null || cachedEntry.valueHash != code) { // If the position is not cached or the value has changed, // create a new undefined RuntimeScalar to represent the position - position = new RuntimeScalar(); + position = new PosLvalueScalar(perlVariable); // Cache the new position with the current hash of the value positionCache.put(perlVariable, new CacheEntry(code, position)); } else { @@ -59,6 +59,60 @@ public static RuntimeScalar pos(RuntimeScalar perlVariable) { return position; } + private static void clearZeroLengthMatchTracking(RuntimeScalar perlVariable) { + CacheEntry cachedEntry = positionCache.get(perlVariable); + if (cachedEntry != null) { + cachedEntry.lastMatchWasZeroLength = false; + cachedEntry.lastMatchPosition = -1; + cachedEntry.lastMatchPattern = null; + } + } + + private static class PosLvalueScalar extends RuntimeScalar { + private final RuntimeScalar target; + + private PosLvalueScalar(RuntimeScalar target) { + super(); + this.target = target; + } + + @Override + public RuntimeScalar set(RuntimeScalar value) { + RuntimePosLvalue.clearZeroLengthMatchTracking(target); + return super.set(value); + } + + @Override + public RuntimeScalar set(int value) { + RuntimePosLvalue.clearZeroLengthMatchTracking(target); + return super.set(value); + } + + @Override + public RuntimeScalar set(long value) { + RuntimePosLvalue.clearZeroLengthMatchTracking(target); + return super.set(value); + } + + @Override + public RuntimeScalar set(boolean value) { + RuntimePosLvalue.clearZeroLengthMatchTracking(target); + return super.set(value); + } + + @Override + public RuntimeScalar set(String value) { + RuntimePosLvalue.clearZeroLengthMatchTracking(target); + return super.set(value); + } + + @Override + public RuntimeScalar set(Object value) { + RuntimePosLvalue.clearZeroLengthMatchTracking(target); + return super.set(value); + } + } + /** * Check if the last match at this position was zero-length with the given pattern. * This is used to prevent infinite loops in global regex matches.