From de88f9d66717cf6164c200c0c1ef433b181a8136 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Mon, 3 Aug 2026 12:35:00 +0200 Subject: [PATCH 1/2] fix: preserve interpreter eval compiler state Preserve lexical warning metadata and runtime warning-bit overrides across interpreter eval STRING compilation, including warning-aware arithmetic. Correct incomplete infix-expression diagnostics so CPAN-generated quoted subroutines report the same source line as Perl. Generated with [Codex](https://openai.com/codex) Co-Authored-By: Codex <158243242+codex[bot]@users.noreply.github.com> --- .../scriptengine/PerlLanguageProvider.java | 11 +++++++ .../backend/bytecode/BytecodeCompiler.java | 17 +++++++--- .../backend/bytecode/BytecodeInterpreter.java | 4 +++ .../bytecode/CompileBinaryOperatorHelper.java | 4 ++- .../backend/bytecode/InlineOpcodeHandler.java | 12 +++++++ .../perlonjava/backend/bytecode/Opcodes.java | 3 ++ .../frontend/parser/ParseInfix.java | 28 +++++++++++++++-- .../perlonjava/frontend/parser/Parser.java | 6 +++- .../frontend/parser/StatementResolver.java | 8 ++++- .../runtime/WarningBitsRegistry.java | 19 ++++++++++++ .../perlonjava/runtime/operators/WarnDie.java | 7 +++-- .../runtimetypes/ErrorMessageUtil.java | 9 ++++-- .../runtime/runtimetypes/RuntimeCode.java | 31 +++++++++++++++++-- .../runtimetypes/ScalarSpecialVariable.java | 1 + 14 files changed, 143 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java b/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java index 9435629d5..2fe3162b8 100644 --- a/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java +++ b/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java @@ -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) { @@ -445,6 +453,9 @@ private static RuntimeList executeCodeImpl(RuntimeCode runtimeCode, Node ast, Em } else { throw t; } + } finally { + WarningBitsRegistry.popCurrent(); + WarningBitsRegistry.setCallSiteBits(savedCallSiteBits); } try { diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 9be05128f..aa474c613 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -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); } @@ -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 diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 361c3346a..f72667a81 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -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); } diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperatorHelper.java b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperatorHelper.java index 2e69f3af6..cc1a6b3d6 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperatorHelper.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperatorHelper.java @@ -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); diff --git a/src/main/java/org/perlonjava/backend/bytecode/InlineOpcodeHandler.java b/src/main/java/org/perlonjava/backend/bytecode/InlineOpcodeHandler.java index 6ac3d9bff..d4b2e3da4 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InlineOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InlineOpcodeHandler.java @@ -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 diff --git a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java index 369f25971..2b8f51a52 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java @@ -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 } diff --git a/src/main/java/org/perlonjava/frontend/parser/ParseInfix.java b/src/main/java/org/perlonjava/frontend/parser/ParseInfix.java index ef00e4783..556f78529 100644 --- a/src/main/java/org/perlonjava/frontend/parser/ParseInfix.java +++ b/src/main/java/org/perlonjava/frontend/parser/ParseInfix.java @@ -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("...")) { @@ -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); } } diff --git a/src/main/java/org/perlonjava/frontend/parser/Parser.java b/src/main/java/org/perlonjava/frontend/parser/Parser.java index 72c74f2dc..1633d037c 100644 --- a/src/main/java/org/perlonjava/frontend/parser/Parser.java +++ b/src/main/java/org/perlonjava/frontend/parser/Parser.java @@ -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) { diff --git a/src/main/java/org/perlonjava/frontend/parser/StatementResolver.java b/src/main/java/org/perlonjava/frontend/parser/StatementResolver.java index d0dc56de4..24f1da95b 100644 --- a/src/main/java/org/perlonjava/frontend/parser/StatementResolver.java +++ b/src/main/java/org/perlonjava/frontend/parser/StatementResolver.java @@ -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); diff --git a/src/main/java/org/perlonjava/runtime/WarningBitsRegistry.java b/src/main/java/org/perlonjava/runtime/WarningBitsRegistry.java index 11ca45088..e86ecba2b 100644 --- a/src/main/java/org/perlonjava/runtime/WarningBitsRegistry.java +++ b/src/main/java/org/perlonjava/runtime/WarningBitsRegistry.java @@ -39,6 +39,10 @@ public class WarningBitsRegistry { // This provides per-statement warning bits (like Perl 5's per-COP bits). private static final ThreadLocal callSiteBits = ThreadLocal.withInitial(() -> null); + + // Runtime override installed by ${^WARNING_BITS}; scoped by eval STRING. + private static final ThreadLocal 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, @@ -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. @@ -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 : ""); } diff --git a/src/main/java/org/perlonjava/runtime/operators/WarnDie.java b/src/main/java/org/perlonjava/runtime/operators/WarnDie.java index 8a6c4d408..9b1fe6f1e 100644 --- a/src/main/java/org/perlonjava/runtime/operators/WarnDie.java +++ b/src/main/java/org/perlonjava/runtime/operators/WarnDie.java @@ -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)) { diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java index 4cfa2d859..1c3938b63 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java @@ -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"; } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index 04bdfadb7..090d10500 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -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. @@ -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. @@ -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, @@ -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) { @@ -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; diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java index 1517e4f27..6aefb6ba9 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java @@ -115,6 +115,7 @@ public RuntimeScalar set(RuntimeScalar value) { if (symbolTable != null) { String bits = value.toString(); WarningFlags.setWarningBitsFromString(symbolTable, bits); + org.perlonjava.runtime.WarningBitsRegistry.setRuntimeWarningBits(bits); } return value; } From 7b83ee0c0302d2ff6a3d2778d4ea025f2014a2c9 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Mon, 3 Aug 2026 12:59:50 +0200 Subject: [PATCH 2/2] fix: scope runtime warning-bit overrides to eval Prevent ${^WARNING_BITS} assignments from leaking into later top-level compile-time warning handling; only eval STRING execution installs the runtime override, and RuntimeCode restores the prior value on exit. Generated with [Codex](https://openai.com/codex) Co-Authored-By: Codex <158243242+codex[bot]@users.noreply.github.com> --- .../runtime/runtimetypes/ScalarSpecialVariable.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java index 6aefb6ba9..04e03228b 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java @@ -115,7 +115,9 @@ public RuntimeScalar set(RuntimeScalar value) { if (symbolTable != null) { String bits = value.toString(); WarningFlags.setWarningBitsFromString(symbolTable, bits); - org.perlonjava.runtime.WarningBitsRegistry.setRuntimeWarningBits(bits); + if (RuntimeCode.evalDepth > 0) { + org.perlonjava.runtime.WarningBitsRegistry.setRuntimeWarningBits(bits); + } } return value; }