From d0a643bdb8d7396a1de7d61db0dd13137039eb41 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Thu, 26 Feb 2026 18:41:43 +0100 Subject: [PATCH 1/2] Fix 5 interpreter-mode regressions: negation context, #line tracking, strict scoping, non-local control flow, undefined sub errors - CompileOperator: compile unaryMinus operand in scalar context to avoid ClassCastException when negating parenthesized expressions in list context - ErrorMessageUtil: use originalFileName in getSourceLocationAccurate() so #line directives do not retroactively change filename for earlier tokens - BytecodeCompiler: push/pop strict/feature/warning flags in enterScope/exitScope so use strict inside blocks is properly scoped - BytecodeCompiler: emit CREATE_LAST/NEXT/REDO + RETURN for non-local loop control instead of throwing compile-time error (enables last/next/redo across eval boundaries) - RuntimeCode: construct subroutine name from code.packageName/subName when subroutineName is empty, fixing Not a CODE reference vs Undefined subroutine error messages Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/bytecode/BytecodeCompiler.java | 32 +++++++++++++++---- .../backend/bytecode/CompileOperator.java | 5 ++- .../runtimetypes/ErrorMessageUtil.java | 4 ++- .../runtime/runtimetypes/RuntimeCode.java | 30 +++++++++++------ 4 files changed, 54 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index d5a3b98c6..b71fde291 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -231,6 +231,13 @@ private void enterScope() { savedBaseRegister.push(baseRegisterForStatement); // Update base to protect all registers allocated before this scope baseRegisterForStatement = nextRegister; + // Save current pragma state so use strict/no strict inside blocks is properly scoped + if (emitterContext != null && emitterContext.symbolTable != null) { + ScopedSymbolTable st = emitterContext.symbolTable; + st.strictOptionsStack.push(st.strictOptionsStack.peek()); + st.featureFlagsStack.push(st.featureFlagsStack.peek()); + st.warningFlagsStack.push((java.util.BitSet) st.warningFlagsStack.peek().clone()); + } } /** @@ -247,6 +254,13 @@ private void exitScope() { if (!savedBaseRegister.isEmpty()) { baseRegisterForStatement = savedBaseRegister.pop(); } + // Restore pragma state + if (emitterContext != null && emitterContext.symbolTable != null) { + ScopedSymbolTable st = emitterContext.symbolTable; + st.strictOptionsStack.pop(); + st.featureFlagsStack.pop(); + st.warningFlagsStack.pop(); + } } } @@ -4671,12 +4685,18 @@ void handleLoopControlOperator(OperatorNode node, String op) { if (targetLoop == null) { // No matching loop found - non-local control flow - // For now, throw an error. Later we can implement RuntimeControlFlowList - if (labelStr != null) { - throwCompilerException("Can't find label \"" + labelStr + "\"", node.getIndex()); - } else { - throwCompilerException("Can't \"" + op + "\" outside a loop block", node.getIndex()); - } + // Emit CREATE_LAST/NEXT/REDO + RETURN to propagate via RuntimeControlFlowList + short createOp = op.equals("last") ? Opcodes.CREATE_LAST + : op.equals("next") ? Opcodes.CREATE_NEXT + : Opcodes.CREATE_REDO; + int rd = allocateRegister(); + emit(createOp); + emitReg(rd); + int labelIdx = labelStr != null ? addToStringPool(labelStr) : 255; + emitReg(labelIdx); + emit(Opcodes.RETURN); + emitReg(rd); + return; } // Check if this is a pseudo-loop (do-while/bare block) which doesn't support last/next/redo diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index 7daa3ae7a..49a2ceace 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -894,8 +894,11 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode bytecodeCompiler.lastResultReg = undefReg; } else if (op.equals("unaryMinus")) { // Unary minus: -$x - // Compile operand + // Compile operand in scalar context (negation always produces a scalar) + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; node.operand.accept(bytecodeCompiler); + bytecodeCompiler.currentCallContext = savedContext; int operandReg = bytecodeCompiler.lastResultReg; // Allocate result register diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java index 7025fe0b5..b049aad4e 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java @@ -14,6 +14,7 @@ */ public class ErrorMessageUtil { private final List tokens; + private final String originalFileName; private String fileName; private int tokenIndex; private int lastLineNumber; @@ -28,6 +29,7 @@ public record SourceLocation(String fileName, int lineNumber) { * @param tokens the list of tokens */ public ErrorMessageUtil(String fileName, List tokens) { + this.originalFileName = fileName; this.fileName = fileName; this.tokens = tokens; this.tokenIndex = -1; @@ -257,7 +259,7 @@ public int getLineNumberAccurate(int index) { } public SourceLocation getSourceLocationAccurate(int index) { - String currentFileName = fileName; + String currentFileName = originalFileName; int lineNumber = 1; boolean atBeginningOfLine = true; diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index 21ed1ed17..d45043e7b 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -1541,19 +1541,31 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, String subroutineNa } // Does AUTOLOAD exist? - if (!subroutineName.isEmpty()) { - // Check if AUTOLOAD exists - String autoloadString = subroutineName.substring(0, subroutineName.lastIndexOf("::") + 2) + "AUTOLOAD"; - // System.err.println("AUTOLOAD: " + fullName); + String fullSubName = subroutineName; + if (fullSubName.isEmpty() && code.packageName != null && code.subName != null) { + fullSubName = code.packageName + "::" + code.subName; + } + + if (!fullSubName.isEmpty()) { + // If this is an imported forward declaration, check AUTOLOAD in the source package FIRST + if (code.sourcePackage != null && !code.sourcePackage.isEmpty()) { + String sourceAutoloadString = code.sourcePackage + "::AUTOLOAD"; + RuntimeScalar sourceAutoload = GlobalVariable.getGlobalCodeRef(sourceAutoloadString); + if (sourceAutoload.getDefinedBoolean()) { + String sourceSubroutineName = code.sourcePackage + "::" + code.subName; + getGlobalVariable(sourceAutoloadString).set(sourceSubroutineName); + return apply(sourceAutoload, a, callContext); + } + } + + // Check if AUTOLOAD exists in the current package + String autoloadString = fullSubName.substring(0, fullSubName.lastIndexOf("::") + 2) + "AUTOLOAD"; RuntimeScalar autoload = GlobalVariable.getGlobalCodeRef(autoloadString); if (autoload.getDefinedBoolean()) { - // System.err.println("AUTOLOAD exists: " + fullName); - // Set $AUTOLOAD name - getGlobalVariable(autoloadString).set(subroutineName); - // Call AUTOLOAD + getGlobalVariable(autoloadString).set(fullSubName); return apply(autoload, a, callContext); } - throw new PerlCompilerException("Undefined subroutine &" + subroutineName + " called at "); + throw new PerlCompilerException("Undefined subroutine &" + fullSubName + " called at "); } } From dd43cc77ea02955bbe1163e3a0f77bf221b7f7df Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Thu, 26 Feb 2026 19:19:04 +0100 Subject: [PATCH 2/2] Fix local *glob restore when glob.set() aliases slots RuntimeGlob.dynamicSaveState/RestoreState now saves and restores the actual globalVariables/globalArrays/globalHashes/globalCodeRefs map entries in addition to calling dynamicSaveState on the slot objects. This fixes local *glob = \"ref" where aliasGlobalVariable replaces the map entry: on restore the original entry is put back before its state is restored, preventing the shared static dynamicStateStack from corrupting the aliased object. Fixes uni/gv.t test 169 regression in interpreter mode. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../runtime/runtimetypes/RuntimeGlob.java | 52 +++++++++++++------ 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java index d2c5853db..89d6833ca 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java @@ -3,6 +3,7 @@ import org.perlonjava.runtime.mro.InheritanceResolver; import java.util.Iterator; +import java.util.Stack; import static org.perlonjava.runtime.runtimetypes.RuntimeScalarType.*; @@ -541,29 +542,50 @@ public RuntimeGlob undefine() { return this; } - /** - * Saves the current state of the typeglob. - */ + private static final Stack globSlotStack = new Stack<>(); + + private record GlobSlotSnapshot( + String globName, + RuntimeScalar scalar, + RuntimeArray array, + RuntimeHash hash, + RuntimeScalar code) {} + @Override public void dynamicSaveState() { - GlobalVariable.getGlobalCodeRef(this.globName).dynamicSaveState(); - GlobalVariable.getGlobalArray(this.globName).dynamicSaveState(); - GlobalVariable.getGlobalHash(this.globName).dynamicSaveState(); - GlobalVariable.getGlobalVariable(this.globName).dynamicSaveState(); + RuntimeScalar savedScalar = GlobalVariable.getGlobalVariable(this.globName); + RuntimeArray savedArray = GlobalVariable.getGlobalArray(this.globName); + RuntimeHash savedHash = GlobalVariable.getGlobalHash(this.globName); + RuntimeScalar savedCode = GlobalVariable.getGlobalCodeRef(this.globName); + globSlotStack.push(new GlobSlotSnapshot(this.globName, savedScalar, savedArray, savedHash, savedCode)); + + savedCode.dynamicSaveState(); + savedArray.dynamicSaveState(); + savedHash.dynamicSaveState(); + savedScalar.dynamicSaveState(); GlobalVariable.getGlobalFormatRef(this.globName).dynamicSaveState(); this.IO.dynamicSaveState(); } - /** - * Restores the most recently saved state of the typeglob. - */ @Override public void dynamicRestoreState() { this.IO.dynamicRestoreState(); - GlobalVariable.getGlobalVariable(this.globName).dynamicRestoreState(); - GlobalVariable.getGlobalHash(this.globName).dynamicRestoreState(); - GlobalVariable.getGlobalArray(this.globName).dynamicRestoreState(); - GlobalVariable.getGlobalCodeRef(this.globName).dynamicRestoreState(); - GlobalVariable.getGlobalFormatRef(this.globName).dynamicRestoreState(); + + GlobSlotSnapshot snap = globSlotStack.pop(); + + GlobalVariable.globalVariables.put(snap.globName, snap.scalar); + snap.scalar.dynamicRestoreState(); + + GlobalVariable.globalHashes.put(snap.globName, snap.hash); + snap.hash.dynamicRestoreState(); + + GlobalVariable.globalArrays.put(snap.globName, snap.array); + snap.array.dynamicRestoreState(); + + GlobalVariable.globalCodeRefs.put(snap.globName, snap.code); + snap.code.dynamicRestoreState(); + InheritanceResolver.invalidateCache(); + + GlobalVariable.getGlobalFormatRef(snap.globName).dynamicRestoreState(); } }