1414import org .perlonjava .operators .ModuleOperators ;
1515import org .perlonjava .scriptengine .PerlLanguageProvider ;
1616import org .perlonjava .symbols .ScopedSymbolTable ;
17+ import org .perlonjava .symbols .SymbolTable ;
1718
1819import java .lang .invoke .MethodHandle ;
1920import java .lang .invoke .MethodHandles ;
@@ -40,6 +41,82 @@ public class RuntimeCode extends RuntimeBase implements RuntimeScalarReference {
4041
4142 // Lookup object for performing method handle operations
4243 public static final MethodHandles .Lookup lookup = MethodHandles .lookup ();
44+
45+ /**
46+ * ThreadLocal storage for runtime values of captured variables during eval STRING compilation.
47+ *
48+ * PROBLEM: In perl5, BEGIN blocks inside eval STRING can access outer lexical variables' runtime values:
49+ * my @imports = qw(a b);
50+ * eval q{ BEGIN { say @imports } }; # perl5 prints: a b
51+ *
52+ * In PerlOnJava, BEGIN blocks execute during parsing (before the eval class is instantiated),
53+ * so they couldn't access runtime values - they would see empty variables.
54+ *
55+ * SOLUTION: When evalStringHelper() is called, the runtime values are stored in this ThreadLocal.
56+ * During parsing, when SpecialBlockParser sets up BEGIN blocks, it can access these runtime values
57+ * and use them to initialize the special globals that lexical variables become in BEGIN blocks.
58+ *
59+ * This ThreadLocal stores:
60+ * - Key: The evalTag identifying this eval compilation
61+ * - Value: EvalRuntimeContext containing:
62+ * - runtimeValues: Object[] of captured variable values
63+ * - capturedEnv: String[] of captured variable names (matching array indices)
64+ *
65+ * Thread-safety: Each thread's eval compilation uses its own ThreadLocal storage, so parallel
66+ * eval compilations don't interfere with each other.
67+ */
68+ private static final ThreadLocal <EvalRuntimeContext > evalRuntimeContext = new ThreadLocal <>();
69+
70+ /**
71+ * Container for runtime context during eval STRING compilation.
72+ * Holds both the runtime values and variable names so SpecialBlockParser can
73+ * match variables to their values.
74+ */
75+ public static class EvalRuntimeContext {
76+ public final Object [] runtimeValues ;
77+ public final String [] capturedEnv ;
78+ public final String evalTag ;
79+
80+ public EvalRuntimeContext (Object [] runtimeValues , String [] capturedEnv , String evalTag ) {
81+ this .runtimeValues = runtimeValues ;
82+ this .capturedEnv = capturedEnv ;
83+ this .evalTag = evalTag ;
84+ }
85+
86+ /**
87+ * Get the runtime value for a variable by name.
88+ *
89+ * IMPORTANT: The capturedEnv array includes all variables (including 'this', '@_', 'wantarray'),
90+ * but runtimeValues array skips the first skipVariables (currently 3).
91+ * So if @imports is at capturedEnv[5], its value is at runtimeValues[5-3=2].
92+ *
93+ * @param varName The variable name (e.g., "@imports", "$scalar")
94+ * @return The runtime value, or null if not found
95+ */
96+ public Object getRuntimeValue (String varName ) {
97+ int skipVariables = 3 ; // 'this', '@_', 'wantarray'
98+ for (int i = skipVariables ; i < capturedEnv .length ; i ++) {
99+ if (varName .equals (capturedEnv [i ])) {
100+ int runtimeIndex = i - skipVariables ;
101+ if (runtimeIndex >= 0 && runtimeIndex < runtimeValues .length ) {
102+ return runtimeValues [runtimeIndex ];
103+ }
104+ }
105+ }
106+ return null ;
107+ }
108+ }
109+
110+ /**
111+ * Get the current eval runtime context for accessing variable runtime values during parsing.
112+ * This is called by SpecialBlockParser when setting up BEGIN blocks.
113+ *
114+ * @return The current eval runtime context, or null if not in eval STRING compilation
115+ */
116+ public static EvalRuntimeContext getEvalRuntimeContext () {
117+ return evalRuntimeContext .get ();
118+ }
119+
43120 // Cache for memoization of evalStringHelper results
44121 private static final int CLASS_CACHE_SIZE = 100 ;
45122 private static final Map <String , Class <?>> evalCache = new LinkedHashMap <String , Class <?>>(CLASS_CACHE_SIZE , 0.75f , true ) {
@@ -122,26 +199,71 @@ public static void copy(RuntimeCode code, RuntimeCode codeFrom) {
122199 code .codeObject = codeFrom .codeObject ;
123200 }
124201
202+ /**
203+ * Backwards-compatible overload for code compiled before runtimeValues parameter was added.
204+ * This allows pre-compiled Perl modules to continue working with the new signature.
205+ *
206+ * @param code the RuntimeScalar containing the eval string
207+ * @param evalTag the tag used to retrieve the eval context
208+ * @return the compiled Class representing the anonymous subroutine
209+ * @throws Exception if an error occurs during compilation
210+ */
211+ public static Class <?> evalStringHelper (RuntimeScalar code , String evalTag ) throws Exception {
212+ return evalStringHelper (code , evalTag , new Object [0 ]);
213+ }
214+
125215 /**
126216 * Compiles the text of an eval string into a Class that represents an anonymous subroutine.
127217 * After the Class is returned to the caller, an instance of the Class will be populated
128218 * with closure variables, and then makeCodeObject() will be called to transform the Class
129219 * instance into a Perl CODE object.
130220 *
131- * @param code the RuntimeScalar containing the eval string
132- * @param evalTag the tag used to retrieve the eval context
221+ * IMPORTANT CHANGE: This method now accepts runtime values of captured variables.
222+ *
223+ * WHY THIS IS NEEDED:
224+ * In perl5, BEGIN blocks inside eval STRING can access outer lexical variables' runtime values.
225+ * For example:
226+ * my @imports = qw(md5 md5_hex);
227+ * eval q{ use Digest::MD5 @imports }; # BEGIN block sees @imports = (md5 md5_hex)
228+ *
229+ * Previously in PerlOnJava, BEGIN blocks would see empty variables because they execute
230+ * during parsing, before the eval class is instantiated with runtime values.
231+ *
232+ * NOW: We pass runtime values to this method and store them in ThreadLocal storage.
233+ * SpecialBlockParser can then access these values when setting up BEGIN blocks,
234+ * allowing lexical variables to be initialized with their runtime values.
235+ *
236+ * @param code the RuntimeScalar containing the eval string
237+ * @param evalTag the tag used to retrieve the eval context
238+ * @param runtimeValues the runtime values of captured variables (Object[] matching capturedEnv order)
133239 * @return the compiled Class representing the anonymous subroutine
134240 * @throws Exception if an error occurs during compilation
135241 */
136- public static Class <?> evalStringHelper (RuntimeScalar code , String evalTag ) throws Exception {
242+ public static Class <?> evalStringHelper (RuntimeScalar code , String evalTag , Object [] runtimeValues ) throws Exception {
137243
138244 // Retrieve the eval context that was saved at program compile-time
139245 EmitterContext ctx = RuntimeCode .evalContext .get (evalTag );
140246
141- // Check if the eval string contains non-ASCII characters
142- // If so, treat it as Unicode source to preserve Unicode characters during parsing
143- // EXCEPT for evalbytes, which must treat everything as bytes
144- String evalString = code .toString ();
247+ // Store runtime values in ThreadLocal so SpecialBlockParser can access them during parsing.
248+ // This enables BEGIN blocks to see outer lexical variables' runtime values.
249+ //
250+ // CRITICAL: The runtimeValues array matches capturedEnv order (both skip first 3 variables).
251+ // SpecialBlockParser will use getRuntimeValue() to look up values by variable name.
252+ //
253+ // Example: If @imports is at capturedEnv[5], its runtime value is at runtimeValues[5-3=2]
254+ // (because both arrays skip 'this', '@_', and 'wantarray')
255+ EvalRuntimeContext runtimeCtx = new EvalRuntimeContext (
256+ runtimeValues ,
257+ ctx .capturedEnv , // Variable names in same order as runtimeValues
258+ evalTag
259+ );
260+ evalRuntimeContext .set (runtimeCtx );
261+
262+ try {
263+ // Check if the eval string contains non-ASCII characters
264+ // If so, treat it as Unicode source to preserve Unicode characters during parsing
265+ // EXCEPT for evalbytes, which must treat everything as bytes
266+ String evalString = code .toString ();
145267 boolean hasUnicode = false ;
146268 if (!ctx .isEvalbytes && code .type != RuntimeScalarType .BYTE_STRING ) {
147269 for (int i = 0 ; i < evalString .length (); i ++) {
@@ -220,6 +342,51 @@ public static Class<?> evalStringHelper(RuntimeScalar code, String evalTag) thro
220342 // the eval code is parsed with the correct feature/strict/warning context
221343 ScopedSymbolTable parseSymbolTable = capturedSymbolTable .snapShot ();
222344
345+ // CRITICAL: Pre-create aliases for captured variables BEFORE parsing
346+ // This allows BEGIN blocks in the eval string to access outer lexical variables.
347+ //
348+ // When the eval string is parsed, variable references in BEGIN blocks will be
349+ // resolved to these special package globals that we're aliasing now.
350+ //
351+ // Example: my @arr = qw(a b); eval q{ BEGIN { say @arr } };
352+ // We create: globalArrays["BEGIN_PKG_x::@arr"] = (the runtime @arr object)
353+ // Then when "say @arr" is parsed in the BEGIN, it resolves to BEGIN_PKG_x::@arr
354+ // which is aliased to the runtime array with values (a, b).
355+ Map <Integer , SymbolTable .SymbolEntry > capturedVars = capturedSymbolTable .getAllVisibleVariables ();
356+ for (SymbolTable .SymbolEntry entry : capturedVars .values ()) {
357+ if (!entry .name ().equals ("@_" ) && !entry .decl ().isEmpty () && !entry .name ().startsWith ("&" )) {
358+ if (!entry .decl ().equals ("our" )) {
359+ // "my" or "state" variables get special BEGIN package globals
360+ Object runtimeValue = runtimeCtx .getRuntimeValue (entry .name ());
361+ if (runtimeValue != null ) {
362+ // Get or create the special package ID
363+ // IMPORTANT: We need to set the ID NOW (before parsing) so that when
364+ // runSpecialBlock is called during parsing, it uses the SAME ID
365+ OperatorNode ast = entry .ast ();
366+ if (ast != null ) {
367+ if (ast .id == 0 ) {
368+ ast .id = EmitterMethodCreator .classCounter ++;
369+ }
370+ String packageName = PersistentVariable .beginPackage (ast .id );
371+ // IMPORTANT: Global variable keys do NOT include the sigil
372+ // entry.name() is "@arr" but the key should be "packageName::arr"
373+ String varNameWithoutSigil = entry .name ().substring (1 ); // Remove the sigil
374+ String fullName = packageName + "::" + varNameWithoutSigil ;
375+
376+ // Alias the global to the runtime value
377+ if (runtimeValue instanceof RuntimeArray ) {
378+ GlobalVariable .globalArrays .put (fullName , (RuntimeArray ) runtimeValue );
379+ } else if (runtimeValue instanceof RuntimeHash ) {
380+ GlobalVariable .globalHashes .put (fullName , (RuntimeHash ) runtimeValue );
381+ } else if (runtimeValue instanceof RuntimeScalar ) {
382+ GlobalVariable .globalVariables .put (fullName , (RuntimeScalar ) runtimeValue );
383+ }
384+ }
385+ }
386+ }
387+ }
388+ }
389+
223390 EmitterContext evalCtx = new EmitterContext (
224391 new JavaClassInfo (), // internal java class name
225392 parseSymbolTable , // symbolTable
@@ -302,6 +469,13 @@ public static Class<?> evalStringHelper(RuntimeScalar code, String evalTag) thro
302469 }
303470
304471 return generatedClass ;
472+ } finally {
473+ // Clean up ThreadLocal to prevent memory leaks
474+ // IMPORTANT: Always clean up ThreadLocal in finally block to ensure it's removed
475+ // even if compilation fails. Failure to do so could cause memory leaks in
476+ // long-running applications with thread pools.
477+ evalRuntimeContext .remove ();
478+ }
305479 }
306480
307481 /**
0 commit comments