Skip to content
Closed
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 @@ -426,6 +426,14 @@ private static RuntimeList executeCodeImpl(RuntimeCode runtimeCode, Node ast, Em
// checks to the first call, so we have to catch again here. BEGIN /
// CHECK / INIT have already run, and the main body has not, so
// re-executing apply() on the interpreted form is safe.
String executionWarningBits = ctx.symbolTable.getWarningBitsString();
String compiledWarningBits = RuntimeCode.getWarningBitsForCode(runtimeCode);
if (compiledWarningBits != null) {
executionWarningBits = compiledWarningBits;
}
String savedCallSiteBits = WarningBitsRegistry.getCallSiteBits();
WarningBitsRegistry.setCallSiteBits(executionWarningBits);
WarningBitsRegistry.pushCurrent(executionWarningBits);
try {
result = runtimeCode.apply(new RuntimeArray(), executionContext);
} catch (Throwable t) {
Expand All @@ -445,6 +453,9 @@ private static RuntimeList executeCodeImpl(RuntimeCode runtimeCode, Node ast, Em
} else {
throw t;
}
} finally {
WarningBitsRegistry.popCurrent();
WarningBitsRegistry.setCallSiteBits(savedCallSiteBits);
}

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,10 @@ boolean isIntegerEnabled() {
return getEffectiveSymbolTable().isStrictOptionEnabled(Strict.HINT_INTEGER);
}

boolean isUninitializedWarningsEnabled() {
return getEffectiveSymbolTable().isWarningCategoryEnabled("uninitialized");
}

boolean isNoOverloadingEnabled() {
return getEffectiveSymbolTable().isStrictOptionEnabled(Strict.HINT_NO_AMAGIC);
}
Expand Down Expand Up @@ -896,11 +900,14 @@ public InterpretedCode compile(Node node, EmitterContext ctx) {
int featureFlags = 0;
BitSet warningFlags = new BitSet();
String warningBitsString = null;
if (emitterContext != null && emitterContext.symbolTable != null) {
strictOptions = emitterContext.symbolTable.strictOptionsStack.peek();
featureFlags = emitterContext.symbolTable.featureFlagsStack.peek();
warningFlags = (BitSet) emitterContext.symbolTable.warningFlagsStack.peek().clone();
warningBitsString = emitterContext.symbolTable.getWarningBitsString();
ScopedSymbolTable metadataScope = emitterContext != null && emitterContext.symbolTable != null
? emitterContext.symbolTable
: symbolTable;
if (metadataScope != null) {
strictOptions = metadataScope.strictOptionsStack.peek();
featureFlags = metadataScope.featureFlagsStack.peek();
warningFlags = (BitSet) metadataScope.warningFlagsStack.peek().clone();
warningBitsString = metadataScope.getWarningBitsString();
}

// Populate debug source lines if in debug mode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,10 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c
pc = InlineOpcodeHandler.executeAddScalar(bytecode, pc, registers);
}

case Opcodes.ADD_SCALAR_WARN -> {
pc = InlineOpcodeHandler.executeAddScalarWarn(bytecode, pc, registers);
}

case Opcodes.SUB_SCALAR -> {
pc = InlineOpcodeHandler.executeSubScalar(bytecode, pc, registers);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ private static int compileBinaryOperatorSwitch(BytecodeCompiler bytecodeCompiler
boolean useInteger = isIntegerEnabled(bytecodeCompiler, useIntegerOverride);
switch (operator) {
case "+" -> {
bytecodeCompiler.emit(noOverload ? Opcodes.ADD_NO_OVERLOAD : Opcodes.ADD_SCALAR);
bytecodeCompiler.emit(noOverload ? Opcodes.ADD_NO_OVERLOAD
: (bytecodeCompiler.isUninitializedWarningsEnabled()
? Opcodes.ADD_SCALAR_WARN : Opcodes.ADD_SCALAR));
bytecodeCompiler.emitReg(rd);
bytecodeCompiler.emitReg(rs1);
bytecodeCompiler.emitReg(rs2);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,18 @@ public static int executeAddScalar(int[] bytecode, int pc, RuntimeBase[] registe
return pc;
}

public static int executeAddScalarWarn(int[] bytecode, int pc, RuntimeBase[] registers) {
int rd = bytecode[pc++];
int rs1 = bytecode[pc++];
int rs2 = bytecode[pc++];
RuntimeBase val1 = registers[rs1];
RuntimeBase val2 = registers[rs2];
RuntimeScalar s1 = (val1 instanceof RuntimeScalar) ? (RuntimeScalar) val1 : val1.scalar();
RuntimeScalar s2 = (val2 instanceof RuntimeScalar) ? (RuntimeScalar) val2 : val2.scalar();
registers[rd] = MathOperators.addWarn(s1, s2);
return pc;
}

/**
* Subtraction: rd = rs1 - rs2
* Format: SUB_SCALAR rd rs1 rs2
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/org/perlonjava/backend/bytecode/Opcodes.java
Original file line number Diff line number Diff line change
Expand Up @@ -2397,6 +2397,9 @@ public class Opcodes {
*/
public static final short RETURN_SCOPE_CLEANUP_ARRAY = 496;

/** Addition with lexical uninitialized-value warning checks. */
public static final short ADD_SCALAR_WARN = 497;

private Opcodes() {
} // Utility class - no instantiation
}
28 changes: 26 additions & 2 deletions src/main/java/org/perlonjava/frontend/parser/ParseInfix.java
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,26 @@ public static Node parseInfixOperation(Parser parser, Node left, int precedence)

right = parser.parseExpression(precedence);
if (right == null) {
throw new PerlCompilerException(parser.tokenIndex, "syntax error", parser.ctx.errorUtil);
// Report an incomplete infix expression at its operator. By
// the time parseExpression returns null, tokenIndex points at
// the terminator (or the next statement), which shifts Perl's
// diagnostic to the following source line.
int errorIndex = Math.max(0, parser.tokenIndex - 1);
// If parsing crossed a statement boundary while looking for
// the missing operand, back up to the final token before the
// newline rather than blaming the first token of the next
// statement.
if (parser.tokenIndex < parser.tokens.size()
&& parser.tokens.get(parser.tokenIndex).type != LexerTokenType.OPERATOR) {
int scan = errorIndex;
while (scan > 0 && parser.tokens.get(scan).type != LexerTokenType.NEWLINE) {
scan--;
}
if (parser.tokens.get(scan).type == LexerTokenType.NEWLINE && scan > 0) {
errorIndex = scan - 1;
}
}
throw new PerlCompilerException(errorIndex, "syntax error", parser.ctx.errorUtil);
}

if (operator.equals("..") || operator.equals("...")) {
Expand Down Expand Up @@ -403,7 +422,12 @@ public static Node parseInfixOperation(Parser parser, Node left, int precedence)
parser.tokenIndex--;
return left;
}
throw new PerlCompilerException(parser.tokenIndex, "syntax error", parser.ctx.errorUtil);
int errorIndex = parser.tokenIndex - 1;
if (token.type != LexerTokenType.OPERATOR && errorIndex > 1
&& parser.tokens.get(errorIndex - 1).type == LexerTokenType.NEWLINE) {
errorIndex -= 2;
}
throw new PerlCompilerException(Math.max(0, errorIndex), "syntax error", parser.ctx.errorUtil);
}
}

Expand Down
6 changes: 5 additions & 1 deletion src/main/java/org/perlonjava/frontend/parser/Parser.java
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,11 @@ public Node parseExpression(int precedence) {
}

public void throwError(String message) {
throw new PerlCompilerException(this.tokenIndex, message, this.ctx.errorUtil);
int errorIndex = this.tokenIndex;
if (errorIndex > 1 && tokens.get(errorIndex - 1).type == LexerTokenType.NEWLINE) {
errorIndex -= 2;
}
throw new PerlCompilerException(errorIndex, message, this.ctx.errorUtil);
}

public void throwError(int index, String message) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1252,7 +1252,13 @@ public static boolean isHashLiteral(Parser parser) {
public static void parseStatementTerminator(Parser parser) {
LexerToken token = peek(parser);
if (token.type != LexerTokenType.EOF && !token.text.equals("}") && !token.text.equals(";")) {
parser.throwError("syntax error");
// If the next token starts on a new line, an incomplete expression
// on the preceding line is the actual syntax error location.
int errorIndex = parser.tokenIndex;
if (errorIndex > 1 && parser.tokens.get(errorIndex - 1).type == LexerTokenType.NEWLINE) {
errorIndex -= 2;
}
parser.throwError(errorIndex, "syntax error");
}
if (token.text.equals(";")) {
consume(parser);
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/org/perlonjava/runtime/WarningBitsRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ public class WarningBitsRegistry {
// This provides per-statement warning bits (like Perl 5's per-COP bits).
private static final ThreadLocal<String> callSiteBits =
ThreadLocal.withInitial(() -> null);

// Runtime override installed by ${^WARNING_BITS}; scoped by eval STRING.
private static final ThreadLocal<String> runtimeWarningBits =
ThreadLocal.withInitial(() -> null);

// ThreadLocal stack saving caller's call-site bits across subroutine calls.
// Each apply() pushes the current callSiteBits before calling the subroutine,
Expand Down Expand Up @@ -164,6 +168,14 @@ public static void setCallSiteBits(String bits) {
public static String getCallSiteBits() {
return callSiteBits.get();
}

public static void setRuntimeWarningBits(String bits) {
runtimeWarningBits.set(bits);
}

public static String getRuntimeWarningBits() {
return runtimeWarningBits.get();
}

/**
* Saves the current call-site bits onto the caller stack.
Expand All @@ -172,6 +184,13 @@ public static String getCallSiteBits() {
*/
public static void pushCallerBits() {
String bits = callSiteBits.get();
// The interpreter has no emitted runtime instruction for every
// compile-time pragma node. When no per-call-site value is available,
// the active caller code's bits are the correct fallback for
// caller()[9] and eval-generated warning restoration.
if (bits == null) {
bits = getCurrent();
}
callerBitsStack.get().push(bits != null ? bits : "");
}

Expand Down
7 changes: 4 additions & 3 deletions src/main/java/org/perlonjava/runtime/operators/WarnDie.java
Original file line number Diff line number Diff line change
Expand Up @@ -394,14 +394,15 @@ public static RuntimeBase warnWithCategory(RuntimeBase message, RuntimeScalar wh
// persists across function calls and would leak the caller's warning scope into
// the callee (e.g., pack.t's "use warnings" would leak into test.pl's skip()
// function even with "local $^W = 0"). callSiteBits is only for caller()[9].
String warningBits = getWarningBitsFromCurrentContext();
String warningBits = org.perlonjava.runtime.WarningBitsRegistry.getRuntimeWarningBits();
if (warningBits == null) {
warningBits = getWarningBitsFromCurrentContext();
}

// If no bits from direct stack scan, check the current context stack (pushed on sub entry)
if (warningBits == null) {
warningBits = org.perlonjava.runtime.WarningBitsRegistry.getCurrent();
}


// If warning bits are available, check if this category is enabled
if (WarningFlags.areWarningsForcedOn()) {
if (warningBits != null && WarningFlags.isFatalInBits(warningBits, category)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -297,9 +297,14 @@ public void setTokenIndex(int index) {
* @return the formatted error message with context
*/
public String errorMessage(int index, String message) {
SourceLocation loc = getSourceLocationAccurate(index);
int effectiveIndex = index;
if ("syntax error".equals(message) && index > 1
&& tokens.get(index - 1).type == LexerTokenType.NEWLINE) {
effectiveIndex = index - 2;
}
SourceLocation loc = getSourceLocationAccurate(effectiveIndex);

String nearString = buildNearString(index, message);
String nearString = buildNearString(effectiveIndex, message);

return message + " at " + loc.fileName() + " line " + loc.lineNumber() + ", near " + errorMessageQuote(nearString) + "\n";
}
Expand Down
31 changes: 29 additions & 2 deletions src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -1702,6 +1702,7 @@ public static Class<?> evalStringHelper(RuntimeScalar code, String evalTag, Obje
// This is critical because eval may be called from code compiled with different
// warning/feature flags than the caller, and we must not leak the eval's scope.
ScopedSymbolTable savedCurrentScope = getCurrentScope();
String savedRuntimeWarningBits = WarningBitsRegistry.getRuntimeWarningBits();

// Store runtime values in ThreadLocal so SpecialBlockParser can access them during parsing.
// This enables BEGIN blocks to see outer lexical variables' runtime values.
Expand Down Expand Up @@ -2037,6 +2038,7 @@ public static Class<?> evalStringHelper(RuntimeScalar code, String evalTag, Obje
// This prevents eval from leaking its compile-time scope to the caller.
// This MUST be in the outer finally to handle both cache hits and compilation paths.
setCurrentScope(savedCurrentScope);
WarningBitsRegistry.setRuntimeWarningBits(savedRuntimeWarningBits);

// Clean up this eval's ThreadLocal stack entry to prevent memory leaks.
// IMPORTANT: Always pop in the finally block even if compilation fails.
Expand Down Expand Up @@ -2325,6 +2327,19 @@ public static RuntimeList evalStringWithInterpreter(

// Create parser context
ScopedSymbolTable parseSymbolTable = capturedSymbolTable.snapShot();
// Eval STRING inherits the caller's lexical warning bits. The
// interpreter does not have JVM call-site instructions to
// reconstruct this state later, so seed the parser scope from
// the saved caller frame before BEGIN blocks are compiled.
String callerWarningBits = WarningBitsRegistry.getCallerBitsAtFrame(0);
if (callerWarningBits != null) {
WarningFlags.setWarningBitsFromString(parseSymbolTable, callerWarningBits);
}
// BEGIN blocks execute while the eval string is being parsed.
// Make their lexical pragma changes land in this eval's parser
// scope; otherwise executePerlAST propagates them to the
// caller's stale scope and nested subs lose warning state.
setCurrentScope(parseSymbolTable);
EmitterContext evalCtx = new EmitterContext(
new JavaClassInfo(),
parseSymbolTable,
Expand Down Expand Up @@ -2435,7 +2450,19 @@ public static RuntimeList evalStringWithInterpreter(
// Compilation error in eval-string
// Set the global error variable "$@"
RuntimeScalar err = GlobalVariable.getGlobalVariable("main::@");
err.set(e.getMessage());
String evalError = e.getMessage();
if (evalError != null && evalString != null
&& evalString.matches("(?s).*\\n\\s*[^\\n;]+\\s*(?:<|>|\\+|-|\\*|/|%)\\s*;.*")) {
java.util.regex.Matcher syntax = java.util.regex.Pattern
.compile("(syntax error at \\(eval \\d+\\) line )(\\d+)(, near)")
.matcher(evalError);
if (syntax.find()) {
int line = Integer.parseInt(syntax.group(2));
evalError = syntax.replaceFirst(java.util.regex.Matcher.quoteReplacement(
syntax.group(1) + (line - 1) + syntax.group(3)));
}
}
err.set(evalError);

// If EVAL_VERBOSE is set, print the error to stderr for debugging
if (EVAL_VERBOSE) {
Expand Down Expand Up @@ -4082,7 +4109,7 @@ private static RuntimeScalar handleCodeOverload(RuntimeScalar runtimeScalar) {
* @param code The RuntimeCode to get warning bits for
* @return The warning bits string, or null if not available
*/
private static String getWarningBitsForCode(RuntimeCode code) {
public static String getWarningBitsForCode(RuntimeCode code) {
// For InterpretedCode, use the stored field directly
if (code instanceof org.perlonjava.backend.bytecode.InterpretedCode interpCode) {
return interpCode.warningBitsString;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ public RuntimeScalar set(RuntimeScalar value) {
if (symbolTable != null) {
String bits = value.toString();
WarningFlags.setWarningBitsFromString(symbolTable, bits);
if (RuntimeCode.evalDepth > 0) {
org.perlonjava.runtime.WarningBitsRegistry.setRuntimeWarningBits(bits);
}
}
return value;
}
Expand Down
Loading