Skip to content

Commit 26f35b0

Browse files
authored
Merge pull request #176 from fglock/fix-test-more-imports
Fix pos() reset after study() to clear /g zero-length guard
2 parents fab2412 + f7a73de commit 26f35b0

File tree

4 files changed

+301
-21
lines changed

4 files changed

+301
-21
lines changed

src/main/java/org/perlonjava/codegen/EmitEval.java

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -179,24 +179,38 @@ static void handleEvalOperator(EmitterVisitor emitterVisitor, OperatorNode node)
179179
mv.visitLdcInsn(evalTag);
180180
// Stack: [RuntimeScalar(String), String]
181181

182+
// Calculate how many variables need to be passed
183+
// We skip 'this', '@_', and 'wantarray' which are handled separately
184+
int skipVariables = EmitterMethodCreator.skipVariables;
185+
186+
// Build array of runtime values for captured variables
187+
// These are passed to evalStringHelper so BEGIN blocks can access outer lexical variables
188+
mv.visitIntInsn(Opcodes.BIPUSH, newEnv.length - skipVariables);
189+
mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object");
190+
// Stack: [RuntimeScalar(String), String, Object[]]
191+
192+
// Fill the runtime values array with actual variable values from local variables
193+
for (Integer index : newSymbolTable.getAllVisibleVariables().keySet()) {
194+
if (index >= skipVariables) {
195+
String varName = newEnv[index];
196+
mv.visitInsn(Opcodes.DUP);
197+
mv.visitIntInsn(Opcodes.BIPUSH, index - skipVariables);
198+
mv.visitVarInsn(Opcodes.ALOAD, emitterVisitor.ctx.symbolTable.getVariableIndex(varName));
199+
mv.visitInsn(Opcodes.AASTORE);
200+
}
201+
}
202+
// Stack: [RuntimeScalar(String), String, Object[]]
203+
182204
// Call evalStringHelper to compile the eval string at runtime
183-
// This method:
184-
// 1. Retrieves the EmitterContext using evalTag
185-
// 2. Parses and compiles the eval string
186-
// 3. Returns the generated Class object
187-
// 4. Caches the result for repeated evals of the same string
205+
// Now passes runtime values so BEGIN blocks can access outer lexical variables
188206
mv.visitMethodInsn(
189207
Opcodes.INVOKESTATIC,
190208
"org/perlonjava/runtime/RuntimeCode",
191209
"evalStringHelper",
192-
"(Lorg/perlonjava/runtime/RuntimeScalar;Ljava/lang/String;)Ljava/lang/Class;",
210+
"(Lorg/perlonjava/runtime/RuntimeScalar;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/Class;",
193211
false);
194212
// Stack: [Class]
195213

196-
// Calculate how many variables need to be passed to the constructor
197-
// We skip 'this', '@_', and 'wantarray' which are handled separately
198-
int skipVariables = EmitterMethodCreator.skipVariables;
199-
200214
// Create array of parameter types for the constructor
201215
// Each captured variable becomes a constructor parameter (including null gaps)
202216
mv.visitIntInsn(Opcodes.BIPUSH, newEnv.length - skipVariables);

src/main/java/org/perlonjava/parser/SpecialBlockParser.java

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,26 +132,64 @@ static RuntimeList runSpecialBlock(Parser parser, String blockPhase, Node block)
132132
if (entry.name().startsWith("&")) {
133133
continue;
134134
}
135-
135+
136+
String packageName;
136137
if (entry.decl().equals("our")) {
137138
// "our" variable lives in a Perl package
139+
packageName = entry.perlPackage();
138140
// Emit: package PKG
139141
nodes.add(
140142
new OperatorNode("package",
141-
new IdentifierNode(entry.perlPackage(), tokenIndex), tokenIndex));
143+
new IdentifierNode(packageName, tokenIndex), tokenIndex));
142144
} else {
143145
// "my" or "state" variable live in a special BEGIN package
144146
// Retrieve the variable id from the AST; create a new id if needed
145147
OperatorNode ast = entry.ast();
146148
if (ast.id == 0) {
147149
ast.id = EmitterMethodCreator.classCounter++;
148150
}
151+
packageName = PersistentVariable.beginPackage(ast.id);
149152
// Emit: package BEGIN_PKG
150153
nodes.add(
151154
new OperatorNode("package",
152-
new IdentifierNode(PersistentVariable.beginPackage(ast.id), tokenIndex), tokenIndex));
155+
new IdentifierNode(packageName, tokenIndex), tokenIndex));
153156
}
157+
// CLEAN FIX: For eval STRING, make special globals aliases to closed variables
158+
// This allows BEGIN blocks to access outer lexical variables with their runtime values.
159+
//
160+
// In perl5: my @arr = qw(a b); eval q{ BEGIN { say @arr } }; # prints: a b
161+
// The special global BEGIN_PKG::@arr is an ALIAS to the closed @arr variable.
162+
//
163+
// Implementation: Set the global variable to reference the same runtime object.
164+
if (!entry.decl().equals("our")) {
165+
RuntimeCode.EvalRuntimeContext evalCtx = RuntimeCode.getEvalRuntimeContext();
166+
if (evalCtx != null) {
167+
Object runtimeValue = evalCtx.getRuntimeValue(entry.name());
168+
if (runtimeValue != null) {
169+
// Create alias: set special global to reference the runtime object
170+
// IMPORTANT: Global variable keys do NOT include the sigil
171+
// entry.name() is "@arr" but the key should be "packageName::arr"
172+
String varNameWithoutSigil = entry.name().substring(1); // Remove the sigil
173+
String fullName = packageName + "::" + varNameWithoutSigil;
174+
175+
// Put in the appropriate global map based on variable type
176+
if (runtimeValue instanceof RuntimeArray) {
177+
GlobalVariable.globalArrays.put(fullName, (RuntimeArray) runtimeValue);
178+
parser.ctx.logDebug("BEGIN block: Aliased array " + fullName);
179+
} else if (runtimeValue instanceof RuntimeHash) {
180+
GlobalVariable.globalHashes.put(fullName, (RuntimeHash) runtimeValue);
181+
parser.ctx.logDebug("BEGIN block: Aliased hash " + fullName);
182+
} else if (runtimeValue instanceof RuntimeScalar) {
183+
GlobalVariable.globalVariables.put(fullName, (RuntimeScalar) runtimeValue);
184+
parser.ctx.logDebug("BEGIN block: Aliased scalar " + fullName);
185+
}
186+
}
187+
}
188+
}
189+
154190
// Emit: our $var
191+
// When we've aliased the variable above, the "our" declaration will fetch the
192+
// existing global (our alias) instead of creating a new empty one.
155193
nodes.add(
156194
new OperatorNode(
157195
"our",

src/main/java/org/perlonjava/runtime/RuntimeCode.java

Lines changed: 181 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.perlonjava.operators.ModuleOperators;
1515
import org.perlonjava.scriptengine.PerlLanguageProvider;
1616
import org.perlonjava.symbols.ScopedSymbolTable;
17+
import org.perlonjava.symbols.SymbolTable;
1718

1819
import java.lang.invoke.MethodHandle;
1920
import 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

Comments
 (0)