Skip to content
142 changes: 119 additions & 23 deletions src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ public class BytecodeCompiler implements Visitor {

// Symbol table for package/class tracking
// Tracks current package, class flag, and package versions like the compiler does
final ScopedSymbolTable symbolTable = new ScopedSymbolTable();
// Initialized to a fresh table; overwritten in compile() from ctx.symbolTable when available.
ScopedSymbolTable symbolTable = new ScopedSymbolTable();

// Stack to save/restore register state when entering/exiting scopes
private final Stack<Integer> savedNextRegister = new Stack<>();
Expand Down Expand Up @@ -472,9 +473,29 @@ public InterpretedCode compile(Node node, EmitterContext ctx) {
// Detect closure variables if context is provided
if (ctx != null) {
detectClosureVariables(node, ctx);
// Use the calling context from EmitterContext for top-level expressions
// This is crucial for eval STRING to propagate context correctly
currentCallContext = ctx.contextType;
// Use the calling context from EmitterContext for top-level expressions.
// Exception: eval STRING body always produces the value of its last expression,
// even when the caller uses it in void context. Compiling the body in VOID
// context would discard the result (e.g. `INIT { eval '1' or die }` would
// fail because eval returns undef). Use SCALAR as the minimum context.
currentCallContext = (ctx.contextType == RuntimeContextType.VOID)
? RuntimeContextType.SCALAR
: ctx.contextType;

// Sync package, pragmas, warnings, and features from ctx.symbolTable.
// ctx.symbolTable is the compile-time scope snapshot at the eval call site —
// it has the correct package (e.g. FOO3) and pragma state.
if (ctx.symbolTable != null) {
symbolTable.setCurrentPackage(
ctx.symbolTable.getCurrentPackage(),
ctx.symbolTable.currentPackageIsClass());
symbolTable.strictOptionsStack.pop();
symbolTable.strictOptionsStack.push(ctx.symbolTable.strictOptionsStack.peek());
symbolTable.featureFlagsStack.pop();
symbolTable.featureFlagsStack.push(ctx.symbolTable.featureFlagsStack.peek());
symbolTable.warningFlagsStack.pop();
symbolTable.warningFlagsStack.push((java.util.BitSet) ctx.symbolTable.warningFlagsStack.peek().clone());
}
}

// If we have captured variables, allocate registers for them
Expand All @@ -490,17 +511,6 @@ public InterpretedCode compile(Node node, EmitterContext ctx) {
// Visit the node to generate bytecode
node.accept(this);

// Convert result to scalar context if needed (for eval STRING)
if (currentCallContext == RuntimeContextType.SCALAR && lastResultReg >= 0) {
RuntimeBase lastResult = null; // Can't access at compile time
// Use ARRAY_SIZE to convert arrays/lists to scalar count
int scalarReg = allocateRegister();
emit(Opcodes.ARRAY_SIZE);
emitReg(scalarReg);
emitReg(lastResultReg);
lastResultReg = scalarReg;
}

// Emit RETURN with last result register
// If no result was produced, return undef instead of register 0 ("this")
int returnReg;
Expand Down Expand Up @@ -697,6 +707,26 @@ public void visit(BlockNode node) {
outerResultReg = allocateRegister();
}

// Detect scoped package/class blocks: parseOptionalPackageBlock inserts the
// package/class OperatorNode as the first element and marks it with isScoped=true.
// The package node itself emits PUSH_PACKAGE; we emit POP_PACKAGE here so normal
// fallthrough restores the previous package.
boolean isScopedPackageBlock = !node.elements.isEmpty()
&& node.elements.getFirst() instanceof OperatorNode pkgOp
&& (pkgOp.operator.equals("package") || pkgOp.operator.equals("class"))
&& Boolean.TRUE.equals(pkgOp.getAnnotation("isScoped"));

// Scoped package/class blocks change the compiler's symbolTable current package when
// compiling the leading package/class OperatorNode. Restore it after the block so
// subsequent name resolution (notably eval STRING compilation) uses the correct
// call-site package.
String savedCompilePackage = null;
boolean savedCompilePackageIsClass = false;
if (isScopedPackageBlock) {
savedCompilePackage = symbolTable.getCurrentPackage();
savedCompilePackageIsClass = symbolTable.currentPackageIsClass();
}

// Detect the BlockNode([local $_, For1Node(needsArrayOfAlias)]) pattern produced
// by the parser for implicit-$_ foreach loops. For1Node emits LOCAL_SCALAR_SAVE_LEVEL
// itself, so the 'local $_' child must be skipped here to avoid double-emission.
Expand All @@ -711,6 +741,25 @@ public void visit(BlockNode node) {

// Visit each statement in the block
int numStatements = node.elements.size();

// Pre-scan to find the last value-producing statement index.
// Special blocks (END/BEGIN/INIT/CHECK/UNITCHECK) parse to OperatorNode("undef")
// and produce no return value. They must not be treated as the last statement
// for return-value purposes (e.g. `eval '1; END { }'` must return 1, not undef).
int lastValueProducingIndex = numStatements - 1;
for (int i = numStatements - 1; i >= 0; i--) {
Node stmt = node.elements.get(i);
// OperatorNode("undef") with no operand is how the parser represents special
// blocks that have already been executed (BEGIN/END/INIT/CHECK/UNITCHECK).
boolean isSpecialBlockPlaceholder = stmt instanceof OperatorNode op
&& "undef".equals(op.operator)
&& op.operand == null;
if (!isSpecialBlockPlaceholder) {
lastValueProducingIndex = i;
break;
}
}

for (int i = 0; i < numStatements; i++) {
// Skip the 'local $_' child when For1Node handles it via LOCAL_SCALAR_SAVE_LEVEL
if (i == 0 && skipFirstChild) continue;
Expand All @@ -727,8 +776,8 @@ public void visit(BlockNode node) {
int savedContext = currentCallContext;

// If this is not an assignment or other value-using construct, use VOID context
// EXCEPT for the last statement in a block, which should use the block's context
boolean isLastStatement = (i == numStatements - 1);
// EXCEPT for the last value-producing statement in a block, which uses the block's context
boolean isLastStatement = (i == lastValueProducingIndex);
if (!isLastStatement && !(stmt instanceof BinaryOperatorNode && ((BinaryOperatorNode) stmt).operator.equals("="))) {
currentCallContext = RuntimeContextType.VOID;
}
Expand All @@ -737,6 +786,30 @@ public void visit(BlockNode node) {

currentCallContext = savedContext;

// After the last value-producing statement, preserve its lastResultReg.
// Subsequent void-context statements (e.g. END/BEGIN placeholders) must
// not overwrite it, even if they set lastResultReg = -1.
if (isLastStatement) {
// Freeze the result here; nothing after this should change it
int frozenResult = lastResultReg;
// Recycle temporaries, then restore
recycleTemporaryRegisters();
lastResultReg = frozenResult;
// Continue visiting remaining statements (e.g. END placeholders) as void
for (int j = i + 1; j < numStatements; j++) {
Node trailing = node.elements.get(j);
if (trailing == null) continue;
int savedCtx2 = currentCallContext;
currentCallContext = RuntimeContextType.VOID;
trailing.accept(this);
currentCallContext = savedCtx2;
recycleTemporaryRegisters();
}
// Restore frozen result and break out of outer loop
lastResultReg = frozenResult;
break;
}

// Recycle temporary registers after each statement
// enterScope() protects registers allocated before entering a scope
recycleTemporaryRegisters();
Expand All @@ -749,6 +822,14 @@ public void visit(BlockNode node) {
emitReg(lastResultReg);
}

// Restore previous package for scoped package/class blocks.
if (isScopedPackageBlock) {
emit(Opcodes.POP_PACKAGE);

// Restore compile-time package tracking.
symbolTable.setCurrentPackage(savedCompilePackage, savedCompilePackageIsClass);
}

// Exit scope restores register state
exitScope();

Expand Down Expand Up @@ -1243,7 +1324,12 @@ void handleCompoundAssignment(BinaryOperatorNode node) {
// Global variable - need to load it first
isGlobal = true;
targetReg = allocateRegister();
String normalizedName = NameNormalizer.normalizeVariableName(varName, getCurrentPackage());
// NameNormalizer expects a variable name WITHOUT the sigil.
// Using "$main::x" here would create a different global key than
// regular assignments (which normalize "main::x"), so compound
// assigns would update the wrong global.
String bareVarName = varName.substring(1);
String normalizedName = NameNormalizer.normalizeVariableName(bareVarName, getCurrentPackage());
int nameIdx = addToStringPool(normalizedName);
emit(Opcodes.LOAD_GLOBAL_SCALAR);
emitReg(targetReg);
Expand Down Expand Up @@ -1313,7 +1399,9 @@ void handleCompoundAssignment(BinaryOperatorNode node) {
throwCompilerException("Global symbol \"" + varName + "\" requires explicit package name");
}

String normalizedName = NameNormalizer.normalizeVariableName(varName, getCurrentPackage());
// NameNormalizer expects a variable name WITHOUT the sigil.
String bareVarName = varName.substring(1);
String normalizedName = NameNormalizer.normalizeVariableName(bareVarName, getCurrentPackage());
int nameIdx = addToStringPool(normalizedName);
emit(Opcodes.STORE_GLOBAL_SCALAR);
emit(nameIdx);
Expand Down Expand Up @@ -3902,9 +3990,13 @@ public void visit(ListNode node) {
// List elements should be evaluated in LIST context
if (node.elements.size() == 1) {
int savedContext = currentCallContext;
currentCallContext = RuntimeContextType.LIST;
try {
node.elements.get(0).accept(this);
Node element = node.elements.get(0);
String argContext = (String) element.getAnnotation("context");
currentCallContext = (argContext != null && argContext.equals("SCALAR"))
? RuntimeContextType.SCALAR
: RuntimeContextType.LIST;
element.accept(this);
int elemReg = lastResultReg;

int listReg = allocateRegister();
Expand All @@ -3923,11 +4015,15 @@ public void visit(ListNode node) {
// Evaluate each element into a register
// List elements should be evaluated in LIST context
int savedContext = currentCallContext;
currentCallContext = RuntimeContextType.LIST;
try {
int[] elementRegs = new int[node.elements.size()];
for (int i = 0; i < node.elements.size(); i++) {
node.elements.get(i).accept(this);
Node element = node.elements.get(i);
String argContext = (String) element.getAnnotation("context");
currentCallContext = (argContext != null && argContext.equals("SCALAR"))
? RuntimeContextType.SCALAR
: RuntimeContextType.LIST;
element.accept(this);
elementRegs[i] = lastResultReg;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -794,6 +794,10 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c
int rd = bytecode[pc++];
int operandReg = bytecode[pc++];
RuntimeBase operand = registers[operandReg];
if (operand == null) {
registers[rd] = RuntimeScalarCache.scalarUndef;
break;
}
if (operand instanceof RuntimeList) {
// For RuntimeList in list assignment context, return the count
registers[rd] = new RuntimeScalar(((RuntimeList) operand).size());
Expand Down Expand Up @@ -2052,6 +2056,16 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c
break;
}

case Opcodes.POP_PACKAGE: {
// Scoped package block exit: closing } of package Foo { ... }
// Restore the previous package by popping exactly one saved dynamic state.
int level = DynamicVariableManager.getLocalLevel();
if (level > 0) {
DynamicVariableManager.popToLocalLevel(level - 1);
}
break;
}

default:
// Unknown opcode
int opcodeInt = opcode;
Expand Down Expand Up @@ -2386,6 +2400,10 @@ private static int executeCollections(int opcode, int[] bytecode, int pc,
int rd = bytecode[pc++];
int operandReg = bytecode[pc++];
RuntimeBase operand = registers[operandReg];
if (operand == null) {
registers[rd] = RuntimeScalarCache.scalarUndef;
return pc;
}
if (operand instanceof RuntimeList) {
registers[rd] = new RuntimeScalar(((RuntimeList) operand).size());
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package org.perlonjava.backend.bytecode;

import org.perlonjava.frontend.analysis.LValueVisitor;
import org.perlonjava.frontend.astnode.*;
import org.perlonjava.runtime.runtimetypes.NameNormalizer;
import org.perlonjava.runtime.runtimetypes.PerlCompilerException;
import org.perlonjava.runtime.runtimetypes.RuntimeContextType;

import java.util.ArrayList;
Expand All @@ -14,6 +16,19 @@ public class CompileAssignment {
* Handles all forms of assignment including my/our/local, scalars, arrays, hashes, and slices.
*/
public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, BinaryOperatorNode node) {
// Ensure compile-time lvalue checks match JVM behavior.
// In particular, this detects invalid lvalues like:
// ($a ? $x : ($y)) = 5
// which must throw "Assignment to both a list and a scalar".
try {
LValueVisitor.getContext(node.left);
} catch (PerlCompilerException e) {
throw e;
} catch (RuntimeException e) {
// LValueVisitor may throw other runtime exceptions; preserve message as a compile error.
throw new PerlCompilerException(node.getIndex(), e.getMessage(), bytecodeCompiler.errorUtil, e);
}

// Determine the calling context for the RHS based on LHS type
int rhsContext = RuntimeContextType.LIST; // Default

Expand Down Expand Up @@ -263,7 +278,41 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler,
OperatorNode sigilOp = (OperatorNode) element;
String sigil = sigilOp.operator;

if (sigilOp.operand instanceof IdentifierNode) {
// Handle backslash-declared ref: my (\$f, $g) = (...)
// \$f means: declare $f as a lexical scalar, assign i-th RHS
// element to it via SET_SCALAR so any ref taken earlier stays valid.
if (sigil.equals("\\") &&
sigilOp.operand instanceof OperatorNode varNode &&
varNode.operator.equals("$") &&
varNode.operand instanceof IdentifierNode idNode) {
String varName = "$" + idNode.name;
int varReg;
if (bytecodeCompiler.hasVariable(varName)) {
varReg = bytecodeCompiler.getVariableRegister(varName);
} else {
varReg = bytecodeCompiler.addVariable(varName, "my");
bytecodeCompiler.emit(Opcodes.LOAD_UNDEF);
bytecodeCompiler.emitReg(varReg);
}

int indexReg = bytecodeCompiler.allocateRegister();
bytecodeCompiler.emit(Opcodes.LOAD_INT);
bytecodeCompiler.emitReg(indexReg);
bytecodeCompiler.emitInt(i);

int elemReg = bytecodeCompiler.allocateRegister();
bytecodeCompiler.emit(Opcodes.ARRAY_GET);
bytecodeCompiler.emitReg(elemReg);
bytecodeCompiler.emitReg(rhsListReg);
bytecodeCompiler.emitReg(indexReg);

// SET_SCALAR mutates varReg in place so any ref captured
// from LOAD_UNDEF above (via CREATE_REF in the declaration
// visitor) keeps pointing at the correct object.
bytecodeCompiler.emit(Opcodes.SET_SCALAR);
bytecodeCompiler.emitReg(varReg);
bytecodeCompiler.emitReg(elemReg);
} else if (sigilOp.operand instanceof IdentifierNode) {
String varName = sigil + ((IdentifierNode) sigilOp.operand).name;

int varReg;
Expand Down
Loading