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(); + } } /** 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.