Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,26 @@ public BytecodeCompiler(String sourceName, int sourceLine) {
*/
public BytecodeCompiler(String sourceName, int sourceLine, ErrorMessageUtil errorUtil,
Map<String, Integer> parentRegistry) {
this(sourceName, sourceLine, errorUtil, parentRegistry, null);
}

/**
* Constructor for eval STRING with parent scope variable registry and declaration kinds.
* Initializes symbolTable with variables from parent scope, preserving 'our' vs 'my' decls
* so that captured 'our' (package) variables are still resolved via GlobalVariable at
* runtime instead of being bound to the scalar captured at eval-entry time (which would
* make `local $OurVar` in the caller invisible to the eval body).
*
* @param sourceName Source name for error messages
* @param sourceLine Source line for error messages
* @param errorUtil Error message utility
* @param parentRegistry Variable registry from parent scope (for eval STRING)
* @param parentDecls Optional map of varName -> decl kind ("my"/"our"/"state"). Any name
* not present defaults to "my".
*/
public BytecodeCompiler(String sourceName, int sourceLine, ErrorMessageUtil errorUtil,
Map<String, Integer> parentRegistry,
Map<String, String> parentDecls) {
this.sourceName = sourceName;
this.sourceLine = sourceLine;
this.errorUtil = errorUtil;
Expand All @@ -153,12 +173,17 @@ public BytecodeCompiler(String sourceName, int sourceLine, ErrorMessageUtil erro
this.isEvalString = true;

if (parentRegistry != null) {
// Add parent scope variables to symbolTable (for eval STRING variable capture)
// Add parent scope variables to symbolTable (for eval STRING variable capture).
// Preserve the outer declaration kind: 'our' stays 'our' so the variable is
// resolved through GlobalVariable (visible to caller-side local), while 'my'/'state'
// use the captured register slot.
for (Map.Entry<String, Integer> entry : parentRegistry.entrySet()) {
String varName = entry.getKey();
int regIndex = entry.getValue();
if (regIndex >= 3) {
symbolTable.addVariableWithIndex(varName, regIndex, "my");
String decl = parentDecls != null ? parentDecls.get(varName) : null;
if (decl == null || decl.isEmpty()) decl = "my";
symbolTable.addVariableWithIndex(varName, regIndex, decl);
}
}

Expand Down Expand Up @@ -1385,9 +1410,11 @@ void handleArrayElementAccess(BinaryOperatorNode node, OperatorNode leftOp) {
emitReg(arrayReg);
emit(nameIdx);
emit(currentSubroutineBeginId);
} else if (hasVariable(arrayVarName)) {
} else if (hasVariable(arrayVarName) && !isOurVariable(arrayVarName)) {
arrayReg = getVariableRegister(arrayVarName);
} else {
// 'our' arrays (or undeclared globals) must be fetched from the global
// registry on every access so callers' `local @OurArr` is visible.
arrayReg = allocateRegister();
String globalArrayName = NameNormalizer.normalizeVariableName(
varName,
Expand Down Expand Up @@ -1615,9 +1642,11 @@ void handleHashElementAccess(BinaryOperatorNode node, OperatorNode leftOp) {
emitReg(hashReg);
emit(nameIdx);
emit(currentSubroutineBeginId);
} else if (hasVariable(hashVarName)) {
} else if (hasVariable(hashVarName) && !isOurVariable(hashVarName)) {
hashReg = getVariableRegister(hashVarName);
} else {
// 'our' hashes (or undeclared globals) must be fetched from the global
// registry on every access so callers' `local %OurHash` is visible.
hashReg = allocateRegister();
String globalHashName = NameNormalizer.normalizeVariableName(
varName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,11 @@ public static RuntimeList evalStringList(String perlCode,

// Step 4: Compile AST to interpreter bytecode with adjusted variable registry.
// The compile-time package is already propagated via ctx.symbolTable.
// NOTE: We do NOT propagate 'our' decls from the seeded symbol table here,
// because nested evals (this code path) inject captured outer `my` variables
// as `our` in a synthetic seed package purely for parser purposes. Those
// captured-lexical variables must still live in registers at runtime, so
// we keep the default "my" decl in the BytecodeCompiler.
BytecodeCompiler compiler = new BytecodeCompiler(
evalFileName,
sourceLine,
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/org/perlonjava/core/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public final class Configuration {
* Automatically populated by Gradle/Maven during build.
* DO NOT EDIT MANUALLY - this value is replaced at build time.
*/
public static final String gitCommitId = "6d48b1ad0";
public static final String gitCommitId = "c1e3e0acb";

/**
* Git commit date of the build (ISO format: YYYY-MM-DD).
Expand All @@ -48,7 +48,7 @@ public final class Configuration {
* Parsed by App::perlbrew and other tools via: perl -V | grep "Compiled at"
* DO NOT EDIT MANUALLY - this value is replaced at build time.
*/
public static final String buildTimestamp = "Apr 21 2026 21:54:08";
public static final String buildTimestamp = "Apr 21 2026 22:36:17";

// Prevent instantiation
private Configuration() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1366,6 +1366,23 @@ public static RuntimeList evalStringWithInterpreter(
}
}

// Build a parallel map of declaration kinds so the BytecodeCompiler can distinguish
// 'our' (package) variables from true lexicals. 'our' variables must resolve via
// GlobalVariable.getGlobalVariable() on each access so that `local $OurVar` in the
// caller is visible inside the eval. Without this hint, captured 'our' vars are
// treated as lexical 'my' vars bound to the scalar captured at eval-entry time.
Map<String, String> adjustedDecls = new HashMap<>();
if (ctx.capturedEnv != null) {
for (int i = 3; i < ctx.capturedEnv.length; i++) {
String varName = ctx.capturedEnv[i];
if (varName == null) continue;
SymbolTable.SymbolEntry entry = capturedSymbolTable.getSymbolEntry(varName);
if (entry != null && !entry.decl().isEmpty()) {
adjustedDecls.put(varName, entry.decl());
}
}
}

// Compile to InterpretedCode with variable registry.
//
// setCompilePackage() is safe here (unlike EvalStringHandler) because:
Expand All @@ -1381,7 +1398,8 @@ public static RuntimeList evalStringWithInterpreter(
evalCompilerOptions.fileName,
1,
evalCtx.errorUtil,
adjustedRegistry);
adjustedRegistry,
adjustedDecls);
compiler.setCompilePackage(capturedSymbolTable.getCurrentPackage());
interpretedCode = compiler.compile(ast, evalCtx);
evalTrace("evalStringWithInterpreter compiled tag=" + evalTag +
Expand Down
Loading