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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ A Perl compiler and runtime for the JVM that:
- Compiles Perl scripts to Java bytecode
- Integrates with Java libraries (JDBC databases, Maven dependencies)
- Supports most Perl 5.42 features
- Includes 150+ core Perl modules (DBI, HTTP::Tiny, JSON, YAML, Text::CSV, Time::Piece)
- Includes 150+ core Perl modules (DBI, HTTP::Tiny, JSON, YAML, Text::CSV)

## Quick Start

Expand Down
2 changes: 1 addition & 1 deletion docs/about/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Release history of PerlOnJava. See [Roadmap](roadmap.md) for future plans.
- Perl debugger with `-d`
- Non-local control flow: `last`/`next`/`redo`/`goto LABEL`
- Tail call with trampoline for `goto &NAME` and `goto __SUB__`
- Add modules: `Time::Piece`, `TOML`.
- Add modules: `TOML`.
- Bugfix: operator override in Time::Hires now works.
- Bugfix: internal temp variables are now pre-initialized.
- Optimization: faster list assignment.
Expand Down
2 changes: 0 additions & 2 deletions docs/reference/feature-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -706,8 +706,6 @@ The `:encoding()` layer supports all encodings provided by Java's `Charset.forNa
- ✅ **Tie::Scalar** module.
- ✅ **Time::HiRes** module.
- ✅ **Time::Local** module.
- ✅ **Time::Piece** module.
- ✅ **Time::Seconds** module.
- ✅ **UNIVERSAL**: `isa`, `can`, `DOES`, `VERSION` are implemented. `isa` operator is implemented.
- ✅ **URI::Escape** module.
- ✅ **Socket** module: socket constants and functions (`pack_sockaddr_in`, `unpack_sockaddr_in`, `sockaddr_in`, `inet_aton`, `inet_ntoa`, `gethostbyname`).
Expand Down
6 changes: 2 additions & 4 deletions src/main/java/org/perlonjava/app/cli/ArgumentParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -318,10 +318,8 @@ private static int processClusteredSwitches(String[] args, CompilerOptions parse
return index;

case 'w':
// enable many useful warnings via $^W (old-style global warnings)
// Note: This intentionally does NOT add "use warnings" - $^W=1 at initialization
// is sufficient and avoids line number offset issues
parsedArgs.warnFlag = true;
// enable many useful warnings
parsedArgs.moduleUseStatements.add(new ModuleUseStatement(switchChar, "warnings", null, false));
break;
case 'W':
// enable all warnings
Expand Down
1 change: 0 additions & 1 deletion src/main/java/org/perlonjava/app/cli/CompilerOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ public class CompilerOptions implements Cloneable {
public boolean allowUnsafeOperations = false; // For -U
public boolean runUnderDebugger = false; // For -d
public boolean taintWarnings = false; // For -t
public boolean warnFlag = false; // For -w (sets $^W = 1)
public String debugFlags = ""; // For -D
// Unicode/encoding flags for -C switches
public boolean unicodeStdin = false; // -CS or -CI
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ public static RuntimeList executePerlCode(CompilerOptions compilerOptions,
boolean isTopLevelScript,
int callerContext) throws Exception {

// Save the current scope so we can restore it after execution.
// This is critical because require/do should not leak their scope to the caller.
ScopedSymbolTable savedCurrentScope = SpecialBlockParser.getCurrentScope();

// Store the isMainProgram flag in CompilerOptions for use during code generation
compilerOptions.isMainProgram = isTopLevelScript;

Expand Down Expand Up @@ -184,11 +188,19 @@ public static RuntimeList executePerlCode(CompilerOptions compilerOptions,
ctx.symbolTable = ctx.symbolTable.snapShot();
SpecialBlockParser.setCurrentScope(ctx.symbolTable);

// Compile to executable (compiler or interpreter based on flag)
RuntimeCode runtimeCode = compileToExecutable(ast, ctx);
try {
// Compile to executable (compiler or interpreter based on flag)
RuntimeCode runtimeCode = compileToExecutable(ast, ctx);

// Execute (unified path for both backends)
return executeCode(runtimeCode, ctx, isTopLevelScript, callerContext);
// Execute (unified path for both backends)
return executeCode(runtimeCode, ctx, isTopLevelScript, callerContext);
} finally {
// Restore the caller's scope so require/do doesn't leak its scope to the caller.
// But do NOT restore for top-level scripts - we want the main script's pragmas to persist.
if (savedCurrentScope != null && !isTopLevelScript) {
SpecialBlockParser.setCurrentScope(savedCurrentScope);
}
}
}

/**
Expand All @@ -203,6 +215,9 @@ public static RuntimeList executePerlAST(Node ast,
List<LexerToken> tokens,
CompilerOptions compilerOptions) throws Exception {

// Save the current scope so we can restore it after execution.
ScopedSymbolTable savedCurrentScope = SpecialBlockParser.getCurrentScope();

ScopedSymbolTable globalSymbolTable = new ScopedSymbolTable();
globalSymbolTable.enterScope();
globalSymbolTable.addVariable("this", "", null);
Expand Down Expand Up @@ -236,11 +251,18 @@ public static RuntimeList executePerlAST(Node ast,
ctx.symbolTable = ctx.symbolTable.snapShot();
SpecialBlockParser.setCurrentScope(ctx.symbolTable);

// Compile to executable (compiler or interpreter based on flag)
RuntimeCode runtimeCode = compileToExecutable(ast, ctx);
try {
// Compile to executable (compiler or interpreter based on flag)
RuntimeCode runtimeCode = compileToExecutable(ast, ctx);

// executePerlAST is always called from special blocks which use VOID context
return executeCode(runtimeCode, ctx, false, RuntimeContextType.VOID);
// executePerlAST is always called from special blocks which use VOID context
return executeCode(runtimeCode, ctx, false, RuntimeContextType.VOID);
} finally {
// Restore the caller's scope
if (savedCurrentScope != null) {
SpecialBlockParser.setCurrentScope(savedCurrentScope);
}
}
}

/**
Expand Down
24 changes: 5 additions & 19 deletions src/main/java/org/perlonjava/backend/jvm/EmitForeach.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
import org.perlonjava.frontend.analysis.RegexUsageDetector;
import org.perlonjava.frontend.astnode.*;
import org.perlonjava.runtime.perlmodule.Warnings;
import org.perlonjava.runtime.runtimetypes.GlobalContext;
import org.perlonjava.runtime.runtimetypes.GlobalVariable;
import org.perlonjava.runtime.runtimetypes.RuntimeContextType;

public class EmitForeach {
Expand Down Expand Up @@ -134,18 +132,10 @@ public static void emitFor1(EmitterVisitor emitterVisitor, For1Node node) {
if (variableNode instanceof OperatorNode opNode &&
(opNode.operator.equals("my") || opNode.operator.equals("our"))) {
isDeclaredInFor = true;
// Shadow warning is emitted during parsing in OperatorParser.addVariableToScope()
// For loop variables, we need to temporarily disable it to avoid spurious warnings
// Check both lexical 'shadow' category and $^W
boolean isWarningEnabled = Warnings.warningManager.isWarningEnabled("shadow");
boolean isWarnVarEnabled = GlobalVariable.getGlobalVariable(GlobalContext.encodeSpecialVar("W")).getBoolean();
boolean isWarningEnabled = Warnings.warningManager.isWarningEnabled("redefine");
if (isWarningEnabled) {
// turn off lexical "shadow" warning for loop variables
Warnings.warningManager.setWarningState("shadow", false);
}
if (isWarnVarEnabled) {
// temporarily turn off $^W for loop variables
GlobalVariable.getGlobalVariable(GlobalContext.encodeSpecialVar("W")).set(0);
// turn off "masks earlier declaration" warning
Warnings.warningManager.setWarningState("redefine", false);
}
// emit the variable declarations
variableNode.accept(emitterVisitor.with(RuntimeContextType.VOID));
Expand All @@ -170,12 +160,8 @@ public static void emitFor1(EmitterVisitor emitterVisitor, For1Node node) {
}

if (isWarningEnabled) {
// restore lexical warnings
Warnings.warningManager.setWarningState("shadow", true);
}
if (isWarnVarEnabled) {
// restore $^W
GlobalVariable.getGlobalVariable(GlobalContext.encodeSpecialVar("W")).set(1);
// restore warnings
Warnings.warningManager.setWarningState("redefine", true);
}

// Reset global variable check after rewriting
Expand Down
46 changes: 3 additions & 43 deletions src/main/java/org/perlonjava/backend/jvm/EmitOperator.java
Original file line number Diff line number Diff line change
Expand Up @@ -262,26 +262,8 @@ static void handleIndexBuiltin(EmitterVisitor emitterVisitor, OperatorNode node)
static void handleAtan2(EmitterVisitor emitterVisitor, OperatorNode node) {
EmitterVisitor scalarVisitor = emitterVisitor.with(RuntimeContextType.SCALAR);
if (node.operand instanceof ListNode operand) {
// Spill the first operand before evaluating the second so non-local control flow
// propagation can't jump to returnLabel with an extra value on the JVM operand stack.
MethodVisitor mv = emitterVisitor.ctx.mv;
operand.elements.get(0).accept(scalarVisitor);

int leftSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot();
boolean pooled = leftSlot >= 0;
if (!pooled) {
leftSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable();
}
mv.visitVarInsn(Opcodes.ASTORE, leftSlot);

operand.elements.get(1).accept(scalarVisitor);

mv.visitVarInsn(Opcodes.ALOAD, leftSlot);
mv.visitInsn(Opcodes.SWAP);

if (pooled) {
emitterVisitor.ctx.javaClassInfo.releaseSpillSlot();
}
emitOperator(node, emitterVisitor);
}
}
Expand Down Expand Up @@ -559,31 +541,9 @@ static void handleGlobBuiltin(EmitterVisitor emitterVisitor, OperatorNode node)

// Handles the 'range' operator, which creates a range of values.
static void handleRangeOperator(EmitterVisitor emitterVisitor, BinaryOperatorNode node) {
// Spill the left operand before evaluating the right side so non-local control flow
// propagation can't jump to returnLabel with an extra value on the JVM operand stack.
if (ENABLE_SPILL_BINARY_LHS) {
MethodVisitor mv = emitterVisitor.ctx.mv;
node.left.accept(emitterVisitor.with(RuntimeContextType.SCALAR));

int leftSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot();
boolean pooled = leftSlot >= 0;
if (!pooled) {
leftSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable();
}
mv.visitVarInsn(Opcodes.ASTORE, leftSlot);

node.right.accept(emitterVisitor.with(RuntimeContextType.SCALAR));

mv.visitVarInsn(Opcodes.ALOAD, leftSlot);
mv.visitInsn(Opcodes.SWAP);

if (pooled) {
emitterVisitor.ctx.javaClassInfo.releaseSpillSlot();
}
} else {
node.left.accept(emitterVisitor.with(RuntimeContextType.SCALAR));
node.right.accept(emitterVisitor.with(RuntimeContextType.SCALAR));
}
// Accept both left and right operands in SCALAR context.
node.left.accept(emitterVisitor.with(RuntimeContextType.SCALAR));
node.right.accept(emitterVisitor.with(RuntimeContextType.SCALAR));
emitOperator(node, emitterVisitor);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,18 +103,9 @@ private static void handleDeleteExistsInner(OperatorNode node, EmitterVisitor em
// Check if this is a compound expression like $hash->{key}[index]
if (binop.left instanceof BinaryOperatorNode leftBinop && leftBinop.operator.equals("->")) {
// Handle compound hash->array dereference for exists/delete
// Spill the left operand before evaluating the index so non-local control flow
// propagation can't jump to returnLabel with an extra value on the JVM operand stack.
MethodVisitor mv = emitterVisitor.ctx.mv;
// First evaluate the hash dereference to get the array
leftBinop.accept(emitterVisitor.with(RuntimeContextType.SCALAR));

int leftSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot();
boolean pooled = leftSlot >= 0;
if (!pooled) {
leftSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable();
}
mv.visitVarInsn(Opcodes.ASTORE, leftSlot);

// Now emit the index
if (binop.right instanceof ArrayLiteralNode arrayLiteral &&
arrayLiteral.elements.size() == 1) {
Expand All @@ -125,13 +116,6 @@ private static void handleDeleteExistsInner(OperatorNode node, EmitterVisitor em
emitterVisitor.ctx.errorUtil);
}

mv.visitVarInsn(Opcodes.ALOAD, leftSlot);
mv.visitInsn(Opcodes.SWAP);

if (pooled) {
emitterVisitor.ctx.javaClassInfo.releaseSpillSlot();
}

// Call the appropriate method
if (operator.equals("exists")) {
emitterVisitor.ctx.mv.visitMethodInsn(
Expand Down
11 changes: 9 additions & 2 deletions src/main/java/org/perlonjava/backend/jvm/EmitVariable.java
Original file line number Diff line number Diff line change
Expand Up @@ -1092,8 +1092,15 @@ static void handleMyOperator(EmitterVisitor emitterVisitor, OperatorNode node) {
String name = ((IdentifierNode) identifierNode).name;
String var = sigil + name;
emitterVisitor.ctx.logDebug("MY " + operator + " " + sigil + name);
// Note: shadow warning is emitted during parsing in OperatorParser.addVariableToScope()
// We don't emit it again here to avoid duplicates
if (emitterVisitor.ctx.symbolTable.getVariableIndexInCurrentScope(var) != -1) {
if (Warnings.warningManager.isWarningEnabled("redefine")) {
System.err.println(
emitterVisitor.ctx.errorUtil.errorMessage(node.getIndex(),
"Warning: \"" + operator + "\" variable "
+ var
+ " masks earlier declaration in same ctx.symbolTable"));
}
}
int varIndex = emitterVisitor.ctx.symbolTable.addVariable(var, operator, sigilNode);
// TODO optimization - SETVAR+MY can be combined

Expand Down
16 changes: 5 additions & 11 deletions src/main/java/org/perlonjava/frontend/parser/OperatorParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import org.perlonjava.frontend.lexer.LexerToken;
import org.perlonjava.runtime.operators.WarnDie;
import org.perlonjava.runtime.perlmodule.Strict;
import org.perlonjava.runtime.runtimetypes.GlobalContext;
import org.perlonjava.runtime.runtimetypes.GlobalVariable;
import org.perlonjava.runtime.runtimetypes.NameNormalizer;
import org.perlonjava.runtime.runtimetypes.PerlCompilerException;
Expand Down Expand Up @@ -233,16 +232,11 @@ private static void addVariableToScope(EmitterContext ctx, String operator, Oper
String name = ((IdentifierNode) identifierNode).name;
String var = sigil + name;
if (ctx.symbolTable.getVariableIndexInCurrentScope(var) != -1) {
// Check if shadow warnings are enabled via 'use warnings "shadow"' or via $^W (-w flag)
boolean shadowEnabled = ctx.symbolTable.isWarningCategoryEnabled("shadow")
|| GlobalVariable.getGlobalVariable(GlobalContext.encodeSpecialVar("W")).getBoolean();
if (shadowEnabled) {
// "our" uses "redeclared", "my"/"state" use "masks earlier declaration in same scope"
String message = operator.equals("our")
? "\"" + operator + "\" variable " + var + " redeclared"
: "\"" + operator + "\" variable " + var + " masks earlier declaration in same scope";
System.err.print(ctx.errorUtil.warningMessage(node.getIndex(), message));
}
System.err.println(
ctx.errorUtil.errorMessage(node.getIndex(),
"Warning: \"" + operator + "\" variable "
+ var
+ " masks earlier declaration in same ctx.symbolTable"));
}
int varIndex = ctx.symbolTable.addVariable(var, operator, node);
// Note: the isDeclaredReference flag is stored in node.annotations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import static org.perlonjava.frontend.parser.NumberParser.parseNumber;
import static org.perlonjava.frontend.parser.ParserNodeUtils.atUnderscoreArgs;
import static org.perlonjava.frontend.parser.ParserNodeUtils.scalarUnderscore;
import static org.perlonjava.frontend.parser.SpecialBlockParser.getCurrentScope;
import static org.perlonjava.frontend.parser.SpecialBlockParser.runSpecialBlock;
import static org.perlonjava.frontend.parser.SpecialBlockParser.setCurrentScope;
import static org.perlonjava.frontend.parser.StringParser.parseVstring;
Expand Down Expand Up @@ -533,6 +534,12 @@ public static Node parseUseDeclaration(Parser parser, LexerToken token) {
useWarnings(new RuntimeArray(
new RuntimeScalar("warnings"),
new RuntimeScalar("all")), RuntimeContextType.VOID);
// Copy warning flags to ALL levels of the parser's symbol table
// This matches what's done after import() for 'use warnings'
java.util.BitSet currentWarnings = getCurrentScope().warningFlagsStack.peek();
for (int i = 0; i < parser.ctx.symbolTable.warningFlagsStack.size(); i++) {
parser.ctx.symbolTable.warningFlagsStack.set(i, (java.util.BitSet) currentWarnings.clone());
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ public static RuntimeList sort(RuntimeList runtimeList, RuntimeScalar perlCompar

// Retrieve the comparison result and return it as an integer
return result.getFirst().getInt();
} catch (PerlExitException e) {
// exit() should propagate immediately - don't wrap it
throw e;
} catch (Exception e) {
// Wrap any exceptions thrown by the comparator in a RuntimeException
throw new RuntimeException(e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,18 +209,6 @@ public String errorMessage(int index, String message) {
return message + " at " + loc.fileName() + " line " + loc.lineNumber() + ", near " + errorMessageQuote(nearString) + "\n";
}

/**
* Constructs a warning message without "near" context, matching Perl's warning format.
*
* @param index the index of the token where the warning occurred
* @param message the warning message
* @return the formatted warning message
*/
public String warningMessage(int index, String message) {
SourceLocation loc = getSourceLocationAccurate(index);
return message + " at " + loc.fileName() + " line " + loc.lineNumber() + ".\n";
}

private String buildNearString(int index) {
int end = Math.min(tokens.size() - 1, index + 5);
StringBuilder sb = new StringBuilder();
Expand Down
Loading
Loading