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
9 changes: 4 additions & 5 deletions src/main/java/org/perlonjava/app/cli/ArgumentParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,6 @@ public static CompilerOptions parseArguments(String[] args) {
StringBuilder stdinContent = new StringBuilder();
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));

if (isInteractive) {
// Interactive mode - prompt the user and read until EOF (Ctrl+D)
System.err.println("Enter Perl code (press Ctrl+D when done):");
}

// Read from stdin regardless of whether it's interactive or not
String line;
while ((line = reader.readLine()) != null) {
Expand Down Expand Up @@ -318,6 +313,10 @@ private static void processNonSwitchArgument(String[] args, CompilerOptions pars
* @param index The current index in the arguments array.
*/
private static void processShebangLine(String[] args, CompilerOptions parsedArgs, String fileContent, int index) {
if (parsedArgs.discardLeadingGarbage) {
return;
}

String[] lines = fileContent.split("\n", 2);
if (lines.length == 0 || !lines[0].startsWith("#!")) {
return;
Expand Down
109 changes: 96 additions & 13 deletions src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ public class BytecodeCompiler implements Visitor {
// instead of LOAD_UNDEF + SET_SCALAR, to preserve the RuntimeScalar identity that
// closures share.
final Set<String> closureCapturedVarNames = new HashSet<>();
// Lexical scalar variables currently used as foreach loop aliases. Assignments
// through these variables must update the aliased element in place, matching
// Perl's `foreach my $x (@array)` and `foreach $x (@array)` semantics.
private final Map<String, Integer> foreachAliasLexicalCounts = new HashMap<>();
// Source information
final String sourceName;
final int sourceLine;
Expand Down Expand Up @@ -115,6 +119,8 @@ public class BytecodeCompiler implements Visitor {
// Nesting depth inside eval blocks (goto &sub from eval is prohibited)
// 0 = not in eval block, >0 = inside eval block(s)
int evalBlockDepth;
private final ArrayDeque<Integer> evalReturnTargetRegs = new ArrayDeque<>();
private final ArrayDeque<List<Integer>> evalReturnGotoPatchPositions = new ArrayDeque<>();
// Counter tracking nesting depth inside finally blocks (control flow out of finally is prohibited)
private int finallyBlockDepth;
// Tracks whether any LOCAL_* or PUSH_LOCAL_VARIABLE opcodes are emitted (for DynamicVariableManager optimization)
Expand Down Expand Up @@ -337,6 +343,26 @@ void registerVariable(String name, int reg) {
symbolTable.addVariableWithIndex(name, reg, "my");
}

boolean isForeachAliasLexical(String name) {
return foreachAliasLexicalCounts.getOrDefault(name, 0) > 0;
}

private void pushForeachAliasLexical(String name) {
foreachAliasLexicalCounts.merge(name, 1, Integer::sum);
}

private void popForeachAliasLexical(String name) {
Integer count = foreachAliasLexicalCounts.get(name);
if (count == null) {
return;
}
if (count <= 1) {
foreachAliasLexicalCounts.remove(name);
} else {
foreachAliasLexicalCounts.put(name, count - 1);
}
}

private void enterScope() {
int scopeIdx = symbolTable.enterScope();
scopeIndices.push(scopeIdx);
Expand Down Expand Up @@ -742,6 +768,23 @@ String getEvalScopeType() {
return null;
}

boolean shouldReturnFromInlineEvalBlock() {
return evalBlockDepth > 0 && !isInMapGrepBlock;
}

void emitInlineEvalReturn(int exprReg) {
if (evalReturnTargetRegs.isEmpty() || evalReturnGotoPatchPositions.isEmpty()) {
throwCompilerException("Internal error: return outside eval block has no eval target", currentTokenIndex);
return;
}
emitAliasWithTarget(evalReturnTargetRegs.peek(), exprReg);
emit(Opcodes.GOTO);
int patchPos = bytecode.size();
emitInt(0);
evalReturnGotoPatchPositions.peek().add(patchPos);
lastResultReg = -1;
}

/**
* Compile an AST node to InterpretedCode.
*
Expand Down Expand Up @@ -1052,17 +1095,11 @@ private RuntimeBase getVariableValueFromContext(String varName, EmitterContext c
// For eval STRING, runtime values are available via evalRuntimeContext ThreadLocal
RuntimeCode.EvalRuntimeContext evalCtx = RuntimeCode.getEvalRuntimeContext();
if (evalCtx != null && evalCtx.runtimeValues() != null) {
// Find variable in captured environment
String[] capturedEnv = evalCtx.capturedEnv();
Object[] runtimeValues = evalCtx.runtimeValues();

for (int i = 0; i < capturedEnv.length; i++) {
if (capturedEnv[i].equals(varName)) {
Object value = runtimeValues[i];
if (value instanceof RuntimeBase) {
return (RuntimeBase) value;
}
}
// Eval runtime values are packed without the reserved registers
// (`this`, `@_`, `wantarray`), while capturedEnv retains them.
Object value = evalCtx.getRuntimeValue(varName);
if (value instanceof RuntimeBase) {
return (RuntimeBase) value;
}
}

Expand Down Expand Up @@ -5656,16 +5693,25 @@ private void visitEvalBlock(SubroutineNode node) {

// Track eval block nesting for "goto &sub from eval" detection
evalBlockDepth++;
evalReturnTargetRegs.push(resultReg);
evalReturnGotoPatchPositions.push(new ArrayList<>());

// Compile the eval block body
compileNode(node.block, resultReg, currentCallContext);

List<Integer> returnGotoPatchPositions = evalReturnGotoPatchPositions.pop();
evalReturnTargetRegs.pop();
evalBlockDepth--;

if (lastResultReg >= 0) {
emitAliasWithTarget(resultReg, lastResultReg);
}

int evalEndPc = bytecode.size();
for (int patchPos : returnGotoPatchPositions) {
patchIntOffset(patchPos, evalEndPc);
}

// Emit EVAL_END (clears $@)
emit(Opcodes.EVAL_END);

Expand Down Expand Up @@ -5789,6 +5835,29 @@ public void visit(For1Node node) {
varReg = allocateRegister();
}

String lexicalLoopVarName = null;
boolean restoreLexicalLoopVar = false;
if (globalLoopVarName == null && node.variable instanceof OperatorNode lexicalVarOp) {
if (lexicalVarOp.operator.equals("my") && lexicalVarOp.operand instanceof OperatorNode sigilOp
&& sigilOp.operator.equals("$") && sigilOp.operand instanceof IdentifierNode idNode) {
lexicalLoopVarName = "$" + idNode.name;
} else if (lexicalVarOp.operator.equals("$") && lexicalVarOp.operand instanceof IdentifierNode idNode) {
String varName = "$" + idNode.name;
if (hasVariable(varName) && !isOurVariable(varName)) {
lexicalLoopVarName = varName;
restoreLexicalLoopVar = true;
}
}
}

int savedLexicalLoopVarReg = -1;
if (restoreLexicalLoopVar) {
savedLexicalLoopVarReg = allocateRegister();
emit(Opcodes.ALIAS);
emitReg(savedLexicalLoopVarReg);
emitReg(varReg);
}

// Step 3b: For global loop variable: emit LOCAL_SCALAR_SAVE_LEVEL.
// This atomically saves getLocalLevel() into levelReg (pre-push), then calls makeLocal.
// POP_LOCAL_LEVEL(levelReg) after the loop correctly restores $_ for any nesting depth.
Expand Down Expand Up @@ -5830,8 +5899,17 @@ public void visit(For1Node node) {
loopInfo.cleanupScopeIndex = symbolTable.currentScopeIndex() + 1;

// Step 8: Execute body
if (node.body != null) {
node.body.accept(this);
if (lexicalLoopVarName != null) {
pushForeachAliasLexical(lexicalLoopVarName);
}
try {
if (node.body != null) {
node.body.accept(this);
}
} finally {
if (lexicalLoopVarName != null) {
popForeachAliasLexical(lexicalLoopVarName);
}
}

// Step 9: Loop check (next/continue jumps here) - the superinstruction
Expand Down Expand Up @@ -5872,6 +5950,11 @@ public void visit(For1Node node) {
emit(Opcodes.POP_LOCAL_LEVEL);
emitReg(levelReg);
}
if (savedLexicalLoopVarReg >= 0) {
emit(Opcodes.ALIAS);
emitReg(varReg);
emitReg(savedLexicalLoopVarReg);
}

// Step 12: Patch all last/next/redo jumps
for (int pc : loopInfo.breakPcs) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,19 +208,18 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c

java.util.ArrayDeque<RegexState> regexStateStack = new java.util.ArrayDeque<>();

// Optimization: only save/restore DynamicVariableManager state if the code uses localization.
// This avoids overhead for simple subroutines that don't use `local`.
// Only ordinary localized variables are conditional. Regex capture variables
// ($1, $&, @-, etc.) are dynamically scoped for every subroutine call, even
// when the callee does not use `local`.
boolean usesLocalization = code.usesLocalization;
// Record DVM level so the finally block can clean up everything pushed
// by this subroutine (local variables AND regex state snapshot).
int savedLocalLevel = usesLocalization ? DynamicVariableManager.getLocalLevel() : 0;
int savedLocalLevel = DynamicVariableManager.getLocalLevel();
// Cache the currentPackage RuntimeScalar to avoid ThreadLocal lookups in hot loop
RuntimeScalar currentPackageScalar = InterpreterState.currentPackage.get();
String savedPackage = currentPackageScalar.toString();
currentPackageScalar.set(framePackageName);
if (usesLocalization) {
RegexState.save();
}
RegexState.save();
// Track whether an exception is propagating out of this frame, so the
// finally block can do scope-exit cleanup for blessed objects in my-variables.
// Without this, DESTROY doesn't fire for objects in subroutines that are
Expand Down Expand Up @@ -2253,7 +2252,7 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c
Opcodes.SYSWRITE, Opcodes.SYSOPEN, Opcodes.SOCKET, Opcodes.BIND, Opcodes.CONNECT,
Opcodes.LISTEN, Opcodes.PIPE, Opcodes.SOCKETPAIR,
Opcodes.WRITE, Opcodes.FORMLINE, Opcodes.PRINTF, Opcodes.ACCEPT,
Opcodes.SYSSEEK, Opcodes.TRUNCATE, Opcodes.READ, Opcodes.OPENDIR, Opcodes.READDIR,
Opcodes.SYSSEEK, Opcodes.TRUNCATE, Opcodes.FLOCK, Opcodes.READ, Opcodes.OPENDIR, Opcodes.READDIR,
Opcodes.SEEKDIR -> {
pc = MiscOpcodeHandler.execute(opcode, bytecode, pc, registers);
}
Expand Down Expand Up @@ -2698,9 +2697,7 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c
// Outer finally: restore interpreter state saved at method entry.
// Unwinds all `local` variables pushed during this frame, restores
// the current package, and pops the InterpreterState call stack.
if (usesLocalization) {
DynamicVariableManager.popToLocalLevel(savedLocalLevel);
}
DynamicVariableManager.popToLocalLevel(savedLocalLevel);
currentPackageScalar.set(savedPackage);
InterpreterState.pop();
// Release cached registers for reuse
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -899,10 +899,13 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler,
int targetReg = bytecodeCompiler.getVariableRegister(varName);

if ((bytecodeCompiler.capturedVarIndices != null && bytecodeCompiler.capturedVarIndices.containsKey(varName))
|| bytecodeCompiler.closureCapturedVarNames.contains(varName)) {
// Captured variable - use SET_SCALAR to preserve aliasing.
|| bytecodeCompiler.closureCapturedVarNames.contains(varName)
|| bytecodeCompiler.isForeachAliasLexical(varName)) {
// Captured variables and active foreach lexical aliases use
// SET_SCALAR to preserve the existing RuntimeScalar identity.
// LOAD_UNDEF would replace the register with a new RuntimeScalar,
// breaking the shared reference that closures depend on.
// breaking the shared reference that closures and foreach
// element aliases depend on.
bytecodeCompiler.emit(Opcodes.SET_SCALAR);
bytecodeCompiler.emitReg(targetReg);
bytecodeCompiler.emitReg(valueReg);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1027,6 +1027,11 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode
}
int exprReg = bytecodeCompiler.lastResultReg;

if (bytecodeCompiler.shouldReturnFromInlineEvalBlock()) {
bytecodeCompiler.emitInlineEvalReturn(exprReg);
break;
}

// Emit scope exit cleanup for all my-scalars, my-hashes, and my-arrays
// in the subroutine scope (scope 0). Explicit 'return' bypasses the
// normal scope exit cleanup at block end, so we must do it here.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public static int execute(int opcode, int[] bytecode, int pc, RuntimeBase[] regi
case Opcodes.ACCEPT -> IOOperator.accept(ctx, argsArray);
case Opcodes.SYSSEEK -> IOOperator.sysseek(ctx, argsArray);
case Opcodes.TRUNCATE -> IOOperator.truncate(ctx, argsArray);
case Opcodes.FLOCK -> IOOperator.flock(ctx, argsArray);
case Opcodes.READ -> IOOperator.read(ctx, argsArray);
case Opcodes.CHOWN -> ChownOperator.chown(ctx, argsArray);
case Opcodes.WAITPID -> WaitpidOperator.waitpid(ctx, argsArray);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1382,6 +1382,7 @@ static OperatorNode parseRequire(Parser parser) {
}

String fileName = NameNormalizer.moduleToFilename(moduleName);
GlobalVariable.ensurePackageStash(moduleName);
operand = ListNode.makeList(new StringNode(fileName, parser.tokenIndex));
} else {
// Check for the specific pattern: :: followed by identifier (which is invalid for require)
Expand Down
10 changes: 6 additions & 4 deletions src/main/java/org/perlonjava/runtime/io/PipeInputChannel.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import static org.perlonjava.runtime.runtimetypes.GlobalVariable.getGlobalVariable;
import static org.perlonjava.runtime.runtimetypes.RuntimeIO.handleIOException;
import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.getScalarInt;
import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.scalarFalse;
import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.scalarTrue;

/**
Expand Down Expand Up @@ -181,8 +182,9 @@ public RuntimeScalar close() {
inputStream.close();
}

if (process != null && process.isAlive()) {
// Give the process a moment to terminate naturally
if (process != null) {
// Reap the child even if it has already exited so close()
// can return false for non-zero pipe status like Perl.
try {
exitCode = process.waitFor();
} catch (InterruptedException e) {
Expand All @@ -196,7 +198,7 @@ public RuntimeScalar close() {
getGlobalVariable("main::?").set(exitCode << 8);

isEOF = true;
return scalarTrue;
return exitCode == 0 ? scalarTrue : scalarFalse;
} catch (IOException e) {
return handleIOException(e, "close pipe failed");
}
Expand Down Expand Up @@ -389,4 +391,4 @@ private void copyPerlEnvToProcessBuilder(ProcessBuilder processBuilder) {
// If we can't access %ENV, just use inherited environment (default behavior)
}
}
}
}
11 changes: 7 additions & 4 deletions src/main/java/org/perlonjava/runtime/io/PipeOutputChannel.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import static org.perlonjava.runtime.runtimetypes.GlobalVariable.getGlobalVariable;
import static org.perlonjava.runtime.runtimetypes.RuntimeIO.handleIOException;
import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.getScalarInt;
import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.scalarFalse;
import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.scalarTrue;

/**
Expand Down Expand Up @@ -286,8 +287,10 @@ public RuntimeScalar close() {
writer.close();
}

// Wait for the process to complete
if (process != null && process.isAlive()) {
// Wait for the process to complete. waitFor() returns immediately
// for an already-exited child, which still needs to be reaped so
// close() can report the correct pipe status.
if (process != null) {
try {
exitCode = process.waitFor();
} catch (InterruptedException e) {
Expand All @@ -300,7 +303,7 @@ public RuntimeScalar close() {
getGlobalVariable("main::?").set(exitCode << 8);

isClosed = true;
return scalarTrue;
return exitCode == 0 ? scalarTrue : scalarFalse;
} catch (IOException e) {
return handleIOException(e, "close pipe failed");
}
Expand Down Expand Up @@ -447,4 +450,4 @@ private void copyPerlEnvToProcessBuilder(ProcessBuilder processBuilder) {
// If we can't access %ENV, just use inherited environment (default behavior)
}
}
}
}
Loading
Loading