From 3ea95fe309445d146b876ab96a79a25ccc20241d Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sat, 28 Feb 2026 23:10:46 +0100 Subject: [PATCH 01/13] Fix @{undef} without strict refs: return empty instead of throwing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In Perl 5, @{undef} without strict refs converts undef to "" and accesses @{""} (an empty package global). PerlOnJava was always throwing "Can't use an undefined value as an ARRAY reference" regardless of strict mode. - Add strictAutovivify flag to RuntimeArray - Set flag true in arrayDeref() (strict), false in arrayDerefNonStrict() - In addToArray/scalar/lastElementIndex: throw only when strict, else return empty/0/-1 - array.t: 119 → 128 tests passing Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../runtime/runtimetypes/RuntimeArray.java | 18 +++++++++++++++--- .../runtime/runtimetypes/RuntimeScalar.java | 4 +++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArray.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArray.java index 5ed814f69..b0ff190f6 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArray.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArray.java @@ -24,6 +24,7 @@ public class RuntimeArray extends RuntimeBase implements RuntimeScalarReference, private static final Stack dynamicStateStack = new Stack<>(); // Internal type of array - PLAIN_ARRAY, AUTOVIVIFY_ARRAY, TIED_ARRAY, or READONLY_ARRAY public int type; + public boolean strictAutovivify; // List to hold the elements of the array. public List elements; // For hash assignment in scalar context: %h = (1,2,3,4) should return 4, not 2 @@ -206,7 +207,10 @@ public RuntimeScalar push(RuntimeBase value) { */ public void addToArray(RuntimeArray array) { if (this.type == AUTOVIVIFY_ARRAY) { - throw new PerlCompilerException("Can't use an undefined value as an ARRAY reference"); + if (this.strictAutovivify) { + throw new PerlCompilerException("Can't use an undefined value as an ARRAY reference"); + } + return; } List targetElements = array.elements; @@ -612,8 +616,12 @@ public RuntimeScalar scalar() { } yield getScalarInt(elements.size()); } - case AUTOVIVIFY_ARRAY -> + case AUTOVIVIFY_ARRAY -> { + if (this.strictAutovivify) { throw new PerlCompilerException("Can't use an undefined value as an ARRAY reference"); + } + yield getScalarInt(0); + } case TIED_ARRAY -> TieArray.tiedFetchSize(this); case READONLY_ARRAY -> { if (scalarContextSize != null) { @@ -629,8 +637,12 @@ public RuntimeScalar scalar() { public int lastElementIndex() { return switch (type) { case PLAIN_ARRAY -> elements.size() - 1; - case AUTOVIVIFY_ARRAY -> + case AUTOVIVIFY_ARRAY -> { + if (this.strictAutovivify) { throw new PerlCompilerException("Can't use an undefined value as an ARRAY reference"); + } + yield -1; + } case TIED_ARRAY -> TieArray.tiedFetchSize(this).getInt() - 1; case READONLY_ARRAY -> elements.size() - 1; default -> throw new IllegalStateException("Unknown array type: " + type); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java index bf054c56e..c877844d3 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java @@ -942,7 +942,9 @@ public RuntimeArray arrayDeref() { if (this instanceof RuntimeScalarReadOnly) { yield new RuntimeArray(); } - yield AutovivificationArray.createAutovivifiedArray(this); + RuntimeArray arr = AutovivificationArray.createAutovivifiedArray(this); + arr.strictAutovivify = true; + yield arr; } case VSTRING -> // 5 throw new PerlCompilerException("Not an ARRAY reference"); From 6f8ece46d1c04ad87aa9ee179862a08441823b63 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sat, 28 Feb 2026 23:14:13 +0100 Subject: [PATCH 02/13] Fix negative index vivification crash in RuntimeArrayProxyEntry When assigning to a negative index on an empty array (e.g. $a[-1] = 0), the resolved key stays negative, causing IndexOutOfBoundsException. Now throws the correct Perl error message instead. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../runtimetypes/RuntimeArrayProxyEntry.java | 4 ++ .../runtimetypes/RuntimeArraySizeLvalue.java | 42 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArrayProxyEntry.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArrayProxyEntry.java index c1a383533..8969080e0 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArrayProxyEntry.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArrayProxyEntry.java @@ -40,6 +40,10 @@ void vivify() { if (parent.type == RuntimeArray.READONLY_ARRAY) { throw new PerlCompilerException("Modification of a read-only value attempted"); } + if (key < 0) { + throw new PerlCompilerException( + "Modification of non-creatable array value attempted, subscript " + key); + } lvalue = new RuntimeScalar(); if (parent.type == RuntimeArray.AUTOVIVIFY_ARRAY) { diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArraySizeLvalue.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArraySizeLvalue.java index 518431d91..a29f325f5 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArraySizeLvalue.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArraySizeLvalue.java @@ -33,6 +33,48 @@ void vivify() { public RuntimeScalar set(RuntimeScalar value) { RuntimeArray parent = lvalue.arrayDeref(); parent.setLastElementIndex(value); + this.type = RuntimeScalarType.INTEGER; + this.value = parent.lastElementIndex(); + return this; + } + + @Override + public RuntimeScalar preAutoIncrement() { + RuntimeArray parent = lvalue.arrayDeref(); + int newIndex = parent.lastElementIndex() + 1; + parent.setLastElementIndex(new RuntimeScalar(newIndex)); + this.type = RuntimeScalarType.INTEGER; + this.value = newIndex; + return this; + } + + @Override + public RuntimeScalar postAutoIncrement() { + RuntimeArray parent = lvalue.arrayDeref(); + int oldIndex = parent.lastElementIndex(); + parent.setLastElementIndex(new RuntimeScalar(oldIndex + 1)); + this.type = RuntimeScalarType.INTEGER; + this.value = oldIndex + 1; + return new RuntimeScalar(oldIndex); + } + + @Override + public RuntimeScalar preAutoDecrement() { + RuntimeArray parent = lvalue.arrayDeref(); + int newIndex = parent.lastElementIndex() - 1; + parent.setLastElementIndex(new RuntimeScalar(newIndex)); + this.type = RuntimeScalarType.INTEGER; + this.value = newIndex; return this; } + + @Override + public RuntimeScalar postAutoDecrement() { + RuntimeArray parent = lvalue.arrayDeref(); + int oldIndex = parent.lastElementIndex(); + parent.setLastElementIndex(new RuntimeScalar(oldIndex - 1)); + this.type = RuntimeScalarType.INTEGER; + this.value = oldIndex - 1; + return new RuntimeScalar(oldIndex); + } } From 163951b9d9e2af3b046b0a5a1ba4473cbce754d5 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sat, 28 Feb 2026 23:23:51 +0100 Subject: [PATCH 03/13] Fix sysread/syswrite on tied handles, null array elements in getList MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - IOOperator: delegate sysread() on tied handles to TieHandle.tiedRead() - IOOperator: delegate syswrite() on tied handles to TieHandle.tiedWrite() - RuntimeArray.getList(): handle null elements (sparse arrays) to prevent NPE/wrong error when returning arrays with holes from subroutines Fixes: tiehandle.t 16→46 tests, array.t 128→173 tests Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../runtime/operators/IOOperator.java | 20 +++++++++++++++---- .../runtime/runtimetypes/RuntimeArray.java | 2 +- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/perlonjava/runtime/operators/IOOperator.java b/src/main/java/org/perlonjava/runtime/operators/IOOperator.java index 78aeb24d1..d861975ea 100644 --- a/src/main/java/org/perlonjava/runtime/operators/IOOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/IOOperator.java @@ -616,8 +616,13 @@ public static RuntimeScalar sysread(int ctx, RuntimeBase... args) { return new RuntimeScalar(); // undef } - if (fh instanceof TieHandle) { - throw new PerlCompilerException("sysread() is not supported on tied handles"); + if (fh instanceof TieHandle tieHandle) { + RuntimeScalar target = args[1].scalar().scalarDeref(); + RuntimeScalar length = args[2].scalar(); + RuntimeList tieArgs = args.length > 3 + ? new RuntimeList(target, length, args[3].scalar()) + : new RuntimeList(target, length); + return TieHandle.tiedRead(tieHandle, tieArgs); } // Check for closed handle @@ -754,8 +759,15 @@ public static RuntimeScalar syswrite(int ctx, RuntimeBase... args) { return new RuntimeScalar(); // undef } - if (fh instanceof TieHandle) { - throw new PerlCompilerException("syswrite() is not supported on tied handles"); + if (fh instanceof TieHandle tieHandle) { + RuntimeScalar data = args[1].scalar(); + int dataLen = data.toString().length(); + RuntimeScalar lengthArg = args.length > 2 ? args[2].scalar() : new RuntimeScalar(dataLen); + if (args.length > 3) { + return TieHandle.tiedWrite(tieHandle, data, lengthArg, args[3].scalar()); + } else { + return TieHandle.tiedWrite(tieHandle, data, lengthArg, new RuntimeScalar(0)); + } } // // Check for closed handle - but based on the debug output, diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArray.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArray.java index b0ff190f6..3d13b5252 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArray.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArray.java @@ -597,7 +597,7 @@ public RuntimeList getList() { // This is important for returning local arrays from functions RuntimeList result = new RuntimeList(); for (RuntimeScalar element : this.elements) { - result.elements.add(new RuntimeScalar(element)); + result.elements.add(element == null ? new RuntimeScalar() : new RuntimeScalar(element)); } return result; } From b60b323291d3f106b8f0f86b7878a4c41f839dc2 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sat, 28 Feb 2026 23:28:10 +0100 Subject: [PATCH 04/13] Guard tied() against ClassCastException after local *glob unwind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After local *glob restores a glob, the hash/array type field may still be TIED_HASH/TIED_ARRAY while elements are restored to the original StableHashMap/ArrayList. Add instanceof checks to prevent ClassCastException. Fixes: tiehandle.t 46→49 tests Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../java/org/perlonjava/runtime/operators/TieOperators.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/perlonjava/runtime/operators/TieOperators.java b/src/main/java/org/perlonjava/runtime/operators/TieOperators.java index e833c25c9..f407bd544 100644 --- a/src/main/java/org/perlonjava/runtime/operators/TieOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/TieOperators.java @@ -192,13 +192,13 @@ public static RuntimeScalar tied(int ctx, RuntimeBase... scalars) { } case ARRAYREFERENCE -> { RuntimeArray array = variable.arrayDeref(); - if (array.type == TIED_ARRAY) { + if (array.type == TIED_ARRAY && array.elements instanceof TieArray) { return ((TieArray) array.elements).getSelf(); } } case HASHREFERENCE -> { RuntimeHash hash = variable.hashDeref(); - if (hash.type == TIED_HASH) { + if (hash.type == TIED_HASH && hash.elements instanceof TieHash) { return ((TieHash) hash.elements).getSelf(); } } From 525ddf577304c9ca49a88dd3de7b71b366977a35 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sat, 28 Feb 2026 23:49:18 +0100 Subject: [PATCH 05/13] Fix interpreter HASH_SET and SET_SCALAR opcodes for non-scalar registers Handle cases where a register contains a RuntimeList instead of RuntimeScalar by checking the type and calling .scalar() when needed, preventing ClassCastException in the bytecode interpreter. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/bytecode/BytecodeInterpreter.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 856e06f57..061f820a1 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -422,7 +422,9 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c // addToScalar calls getValueAsScalar() for ScalarSpecialVariable int rd = bytecode[pc++]; int rs = bytecode[pc++]; - registers[rs].addToScalar((RuntimeScalar) registers[rd]); + RuntimeBase rdVal = registers[rd]; + RuntimeScalar rdScalar = (rdVal instanceof RuntimeScalar) ? (RuntimeScalar) rdVal : rdVal.scalar(); + registers[rs].addToScalar(rdScalar); break; } @@ -916,7 +918,8 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c int valueReg = bytecode[pc++]; RuntimeHash hash = (RuntimeHash) registers[hashReg]; RuntimeScalar key = (RuntimeScalar) registers[keyReg]; - RuntimeScalar val = (RuntimeScalar) registers[valueReg]; + RuntimeBase valBase = registers[valueReg]; + RuntimeScalar val = (valBase instanceof RuntimeScalar) ? (RuntimeScalar) valBase : valBase.scalar(); hash.put(key.toString(), val); // Convert key to String break; } @@ -2311,7 +2314,7 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c } // Not in eval - show detailed error with bytecode context - int errorPc = Math.max(0, pc - 1); // Go back one instruction + int errorPc = Math.max(0, pc - 1); // Show bytecode context (10 bytes before errorPc) StringBuilder bcContext = new StringBuilder(); @@ -2327,7 +2330,9 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c } bcContext.append(" ]"); - String errorMessage = "ClassCastException" + bcContext + ": " + e.getMessage(); + StackTraceElement[] st = e.getStackTrace(); + String javaLine = (st.length > 0) ? " [java:" + st[0].getFileName() + ":" + st[0].getLineNumber() + "]" : ""; + String errorMessage = "ClassCastException" + bcContext + ": " + e.getMessage() + javaLine; throw new RuntimeException(formatInterpreterError(code, errorPc, new Exception(errorMessage)), e); } catch (Throwable e) { // Check if we're inside an eval block From a394e39545d9d8c3fff050330ae63a3c08d3ff39 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sun, 1 Mar 2026 00:02:51 +0100 Subject: [PATCH 06/13] Fix SET_SCALAR bytecode opcode for read-only destination registers When a register contains a RuntimeScalarReadOnly (e.g. cached undef from an empty list scalar() call), replace it with a mutable RuntimeScalar before assigning. This prevents Modification of a read-only value attempted errors in the bytecode interpreter. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/bytecode/BytecodeInterpreter.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 061f820a1..5e7f0164f 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -423,7 +423,15 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c int rd = bytecode[pc++]; int rs = bytecode[pc++]; RuntimeBase rdVal = registers[rd]; - RuntimeScalar rdScalar = (rdVal instanceof RuntimeScalar) ? (RuntimeScalar) rdVal : rdVal.scalar(); + RuntimeScalar rdScalar; + if (rdVal instanceof RuntimeScalarReadOnly) { + rdScalar = new RuntimeScalar(); + registers[rd] = rdScalar; + } else if (rdVal instanceof RuntimeScalar) { + rdScalar = (RuntimeScalar) rdVal; + } else { + rdScalar = rdVal.scalar(); + } registers[rs].addToScalar(rdScalar); break; } From 6b02f25094227cdbf944f8cedac75b04f35ceed3 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sun, 1 Mar 2026 00:04:43 +0100 Subject: [PATCH 07/13] Fix local list form to handle typeglob, array, and hash variables The bytecode compiler only handled scalar variables in local lists like local ($x, *GLOB, @arr, %hash). Typeglob, array, and hash elements were silently skipped. This fix adds proper LOCAL_GLOB, LOCAL_ARRAY, and LOCAL_HASH opcode emission for all variable types in the list form of local. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/bytecode/BytecodeCompiler.java | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index d6f446fae..9d7c3d78e 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -3002,25 +3002,34 @@ void compileVariableDeclaration(OperatorNode node, String op) { continue; } - // Regular scalar variable in list - if (sigil.equals("$") && sigilOp.operand instanceof IdentifierNode idNode) { - String varName = "$" + idNode.name; - - // Check if it's a lexical variable - if (hasVariable(varName)) { - throwCompilerException("Can't localize lexical variable " + varName); + if (sigilOp.operand instanceof IdentifierNode idNode) { + if (sigil.equals("*")) { + String globalName = NameNormalizer.normalizeVariableName(idNode.name, getCurrentPackage()); + int nameIdx = addToStringPool(globalName); + int rd = allocateRegister(); + emit(Opcodes.LOCAL_GLOB); + emitReg(rd); + emit(nameIdx); + varRegs.add(rd); + } else { + String varName = sigil + idNode.name; + if (hasVariable(varName)) { + throwCompilerException("Can't localize lexical variable " + varName); + } + String globalVarName = NameNormalizer.normalizeVariableName(idNode.name, getCurrentPackage()); + int nameIdx = addToStringPool(globalVarName); + int rd = allocateRegister(); + if (sigil.equals("$")) { + emitWithToken(Opcodes.LOCAL_SCALAR, node.getIndex()); + } else if (sigil.equals("@")) { + emitWithToken(Opcodes.LOCAL_ARRAY, node.getIndex()); + } else if (sigil.equals("%")) { + emitWithToken(Opcodes.LOCAL_HASH, node.getIndex()); + } + emitReg(rd); + emit(nameIdx); + varRegs.add(rd); } - - // Localize global variable - String globalVarName = NameNormalizer.normalizeVariableName(idNode.name, getCurrentPackage()); - int nameIdx = addToStringPool(globalVarName); - - int rd = allocateRegister(); - emitWithToken(Opcodes.LOCAL_SCALAR, node.getIndex()); - emitReg(rd); - emit(nameIdx); - - varRegs.add(rd); } } } From db87c9cc9f5cfccf42235d302c1db019fccbcb08 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sun, 1 Mar 2026 00:21:23 +0100 Subject: [PATCH 08/13] Fix $1 returning undef in eval STRING with regex captures The eval STRING compiler path called createClassWithMethod directly without marking the AST block as blockIsSubroutine. This caused EmitBlock to emit redundant block-level regex state save/restore, which clobbered $1/$&/etc before the return value was collected. Also add getList() override to ScalarSpecialVariable to eagerly materialize regex capture values, preventing stale lazy references. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../org/perlonjava/runtime/runtimetypes/RuntimeCode.java | 1 + .../runtime/runtimetypes/ScalarSpecialVariable.java | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index 77469eda3..7f785d5dc 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -490,6 +490,7 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Obje evalCtx.capturedEnv = ctx.capturedEnv; } + ast.setAnnotation("blockIsSubroutine", true); generatedClass = EmitterMethodCreator.createClassWithMethod( evalCtx, ast, diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java index be64a1ae6..c4cdd0abb 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java @@ -270,6 +270,11 @@ public void addToList(RuntimeList list) { list.add(this.getValueAsScalar()); } + @Override + public RuntimeList getList() { + return new RuntimeList(this.getValueAsScalar()); + } + /** * Saves the current state of the RuntimeScalar instance. * From e6efaef385389bd73dfbac4ba21b89a466886217 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sun, 1 Mar 2026 11:42:51 +0100 Subject: [PATCH 09/13] Fix undef $scalar and RuntimeScalarReadOnly in bytecode interpreter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs fixed: 1. `undef $scalar` was not working in the bytecode interpreter because CompileOperator compiled it identically to bare `undef` — it never modified the variable. Now emits UNDEFINE_SCALAR opcode on the operand register before loading undef into the result register. 2. RuntimeScalarReadOnly values leaked into variable registers and hash elements, causing "Modification of a read-only value attempted" errors in large functions using the interpreter backend. Root cause: MathOperators returns cached RuntimeScalarReadOnly from getScalarInt() (e.g. unaryMinus(-1)), and MOVE copied the reference directly into variable registers. Fixed by: - MOVE: unwrap RuntimeScalarReadOnly into mutable RuntimeScalar - HASH_SET: unwrap before storing in hash - INC_REG/DEC_REG: unwrap results before storing back - All compound assignment opcodes (+=, -=, *=, /=, %=, .=, etc.): ensure target register is mutable before in-place modification - Added ensureMutableScalar() helper for consistent unwrapping Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/bytecode/BytecodeInterpreter.java | 51 +++++++++-- .../backend/bytecode/CompileOperator.java | 22 +++-- .../bytecode/OpcodeHandlerExtended.java | 91 ++++++++++++------- 3 files changed, 114 insertions(+), 50 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 5e7f0164f..c0516c1b7 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -21,6 +21,16 @@ public class BytecodeInterpreter { // Debug flag for regex compilation (set at class load time) private static final boolean DEBUG_REGEX = System.getenv("DEBUG_REGEX") != null; + static RuntimeScalar ensureMutableScalar(RuntimeBase val) { + if (val instanceof RuntimeScalarReadOnly ro) { + RuntimeScalar copy = new RuntimeScalar(); + copy.type = ro.type; + copy.value = ro.value; + return copy; + } + return (RuntimeScalar) val; + } + /** * Execute interpreted bytecode. * @@ -115,10 +125,6 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c return new RuntimeList(); } RuntimeList retList = retVal.getList(); - // Materialize $1, $&, etc. into concrete scalars BEFORE returning. - // The finally block will call savedRegexState.restore(), which overwrites - // global regex state. Any lazy ScalarSpecialVariable references in the - // return list must be resolved while this sub's regex state is still active. RuntimeCode.materializeSpecialVarsInResult(retList); return retList; } @@ -182,9 +188,11 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c case Opcodes.MOVE: { // Register copy: rd = rs + // Must unwrap RuntimeScalarReadOnly to prevent read-only values in variable registers int dest = bytecode[pc++]; int src = bytecode[pc++]; - registers[dest] = registers[src]; + RuntimeBase srcVal = registers[src]; + registers[dest] = (srcVal instanceof RuntimeScalarReadOnly) ? ensureMutableScalar(srcVal) : srcVal; break; } @@ -244,6 +252,13 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c break; } + case Opcodes.UNDEFINE_SCALAR: { + // Undefine variable in-place: rd.undefine() + int rd = bytecode[pc++]; + registers[rd].undefine(); + break; + } + // ================================================================= // VARIABLE ACCESS - GLOBAL // ================================================================= @@ -928,7 +943,7 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c RuntimeScalar key = (RuntimeScalar) registers[keyReg]; RuntimeBase valBase = registers[valueReg]; RuntimeScalar val = (valBase instanceof RuntimeScalar) ? (RuntimeScalar) valBase : valBase.scalar(); - hash.put(key.toString(), val); // Convert key to String + hash.put(key.toString(), ensureMutableScalar(val)); break; } @@ -1184,14 +1199,16 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c case Opcodes.INC_REG: { // Increment register in-place: r++ int rd = bytecode[pc++]; - registers[rd] = MathOperators.add((RuntimeScalar) registers[rd], 1); + RuntimeBase incResult = MathOperators.add((RuntimeScalar) registers[rd], 1); + registers[rd] = (incResult instanceof RuntimeScalarReadOnly) ? ensureMutableScalar(incResult) : incResult; break; } case Opcodes.DEC_REG: { // Decrement register in-place: r-- int rd = bytecode[pc++]; - registers[rd] = MathOperators.subtract((RuntimeScalar) registers[rd], 1); + RuntimeBase decResult = MathOperators.subtract((RuntimeScalar) registers[rd], 1); + registers[rd] = (decResult instanceof RuntimeScalarReadOnly) ? ensureMutableScalar(decResult) : decResult; break; } @@ -1199,6 +1216,9 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c // Add and assign: rd += rs (modifies rd in place) int rd = bytecode[pc++]; int rs = bytecode[pc++]; + if (registers[rd] instanceof RuntimeScalarReadOnly) { + registers[rd] = ensureMutableScalar(registers[rd]); + } MathOperators.addAssign( (RuntimeScalar) registers[rd], (RuntimeScalar) registers[rs] @@ -1211,6 +1231,9 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c int rd = bytecode[pc++]; int immediate = readInt(bytecode, pc); pc += 1; + if (registers[rd] instanceof RuntimeScalarReadOnly) { + registers[rd] = ensureMutableScalar(registers[rd]); + } RuntimeScalar result = MathOperators.add((RuntimeScalar) registers[rd], immediate); ((RuntimeScalar) registers[rd]).set(result); break; @@ -2491,6 +2514,9 @@ private static int executeArithmetic(int opcode, int[] bytecode, int pc, case Opcodes.SUBTRACT_ASSIGN: { int rd = bytecode[pc++]; int rs = bytecode[pc++]; + if (registers[rd] instanceof RuntimeScalarReadOnly) { + registers[rd] = ensureMutableScalar(registers[rd]); + } RuntimeBase val1 = registers[rd]; RuntimeBase val2 = registers[rs]; RuntimeScalar s1 = (val1 instanceof RuntimeScalar) ? (RuntimeScalar) val1 : val1.scalar(); @@ -2502,6 +2528,9 @@ private static int executeArithmetic(int opcode, int[] bytecode, int pc, case Opcodes.MULTIPLY_ASSIGN: { int rd = bytecode[pc++]; int rs = bytecode[pc++]; + if (registers[rd] instanceof RuntimeScalarReadOnly) { + registers[rd] = ensureMutableScalar(registers[rd]); + } RuntimeBase val1 = registers[rd]; RuntimeBase val2 = registers[rs]; RuntimeScalar s1 = (val1 instanceof RuntimeScalar) ? (RuntimeScalar) val1 : val1.scalar(); @@ -2513,6 +2542,9 @@ private static int executeArithmetic(int opcode, int[] bytecode, int pc, case Opcodes.DIVIDE_ASSIGN: { int rd = bytecode[pc++]; int rs = bytecode[pc++]; + if (registers[rd] instanceof RuntimeScalarReadOnly) { + registers[rd] = ensureMutableScalar(registers[rd]); + } RuntimeBase val1 = registers[rd]; RuntimeBase val2 = registers[rs]; RuntimeScalar s1 = (val1 instanceof RuntimeScalar) ? (RuntimeScalar) val1 : val1.scalar(); @@ -2524,6 +2556,9 @@ private static int executeArithmetic(int opcode, int[] bytecode, int pc, case Opcodes.MODULUS_ASSIGN: { int rd = bytecode[pc++]; int rs = bytecode[pc++]; + if (registers[rd] instanceof RuntimeScalarReadOnly) { + registers[rd] = ensureMutableScalar(registers[rd]); + } RuntimeBase val1 = registers[rd]; RuntimeBase val2 = registers[rs]; RuntimeScalar s1 = (val1 instanceof RuntimeScalar) ? (RuntimeScalar) val1 : val1.scalar(); diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index e6b20866a..983c66272 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -895,13 +895,21 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode bytecodeCompiler.lastResultReg = rd; } else if (op.equals("undef")) { - // undef operator - returns undefined value - // Can be used standalone: undef - // Or with an operand to undef a variable: undef $x (not implemented yet) - int undefReg = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.LOAD_UNDEF); - bytecodeCompiler.emitReg(undefReg); - bytecodeCompiler.lastResultReg = undefReg; + if (node.operand != null) { + node.operand.accept(bytecodeCompiler); + int operandReg = bytecodeCompiler.lastResultReg; + bytecodeCompiler.emit(Opcodes.UNDEFINE_SCALAR); + bytecodeCompiler.emitReg(operandReg); + int undefReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_UNDEF); + bytecodeCompiler.emitReg(undefReg); + bytecodeCompiler.lastResultReg = undefReg; + } else { + int undefReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_UNDEF); + bytecodeCompiler.emitReg(undefReg); + bytecodeCompiler.lastResultReg = undefReg; + } } else if (op.equals("unaryMinus")) { // Unary minus: -$x // Compile operand in scalar context (negation always produces a scalar) diff --git a/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java b/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java index 5b543dc6f..8b862514f 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java +++ b/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java @@ -109,6 +109,9 @@ public static int executeSubstrVar(int[] bytecode, int pc, RuntimeBase[] registe public static int executeRepeatAssign(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs = bytecode[pc++]; + if (registers[rd] instanceof RuntimeScalarReadOnly) { + registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); + } RuntimeBase result = Operator.repeat( registers[rd], (RuntimeScalar) registers[rs], @@ -130,6 +133,9 @@ public static int executeRepeatAssign(int[] bytecode, int pc, RuntimeBase[] regi public static int executePowAssign(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs = bytecode[pc++]; + if (registers[rd] instanceof RuntimeScalarReadOnly) { + registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); + } RuntimeBase val1 = registers[rd]; RuntimeBase val2 = registers[rs]; RuntimeScalar s1 = (val1 instanceof RuntimeScalar) ? (RuntimeScalar) val1 : val1.scalar(); @@ -151,6 +157,9 @@ public static int executePowAssign(int[] bytecode, int pc, RuntimeBase[] registe public static int executeLeftShiftAssign(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs = bytecode[pc++]; + if (registers[rd] instanceof RuntimeScalarReadOnly) { + registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); + } RuntimeScalar s1 = (RuntimeScalar) registers[rd]; RuntimeScalar s2 = (RuntimeScalar) registers[rs]; RuntimeScalar result = BitwiseOperators.shiftLeft(s1, s2); @@ -170,6 +179,9 @@ public static int executeLeftShiftAssign(int[] bytecode, int pc, RuntimeBase[] r public static int executeRightShiftAssign(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs = bytecode[pc++]; + if (registers[rd] instanceof RuntimeScalarReadOnly) { + registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); + } RuntimeScalar s1 = (RuntimeScalar) registers[rd]; RuntimeScalar s2 = (RuntimeScalar) registers[rs]; RuntimeScalar result = BitwiseOperators.shiftRight(s1, s2); @@ -189,12 +201,13 @@ public static int executeRightShiftAssign(int[] bytecode, int pc, RuntimeBase[] public static int executeLogicalAndAssign(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs = bytecode[pc++]; + if (registers[rd] instanceof RuntimeScalarReadOnly) { + registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); + } RuntimeScalar s1 = ((RuntimeBase) registers[rd]).scalar(); if (!s1.getBoolean()) { - // Left side is false, result is left side (no assignment needed) return pc; } - // Left side is true, assign right side RuntimeScalar s2 = ((RuntimeBase) registers[rs]).scalar(); ((RuntimeScalar) registers[rd]).set(s2); return pc; @@ -212,12 +225,13 @@ public static int executeLogicalAndAssign(int[] bytecode, int pc, RuntimeBase[] public static int executeLogicalOrAssign(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs = bytecode[pc++]; + if (registers[rd] instanceof RuntimeScalarReadOnly) { + registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); + } RuntimeScalar s1 = ((RuntimeBase) registers[rd]).scalar(); if (s1.getBoolean()) { - // Left side is true, result is left side (no assignment needed) return pc; } - // Left side is false, assign right side RuntimeScalar s2 = ((RuntimeBase) registers[rs]).scalar(); ((RuntimeScalar) registers[rd]).set(s2); return pc; @@ -226,6 +240,9 @@ public static int executeLogicalOrAssign(int[] bytecode, int pc, RuntimeBase[] r public static int executeDefinedOrAssign(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs = bytecode[pc++]; + if (registers[rd] instanceof RuntimeScalarReadOnly) { + registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); + } RuntimeScalar s1 = ((RuntimeBase) registers[rd]).scalar(); if (s1.getDefinedBoolean()) { return pc; @@ -247,6 +264,9 @@ public static int executeDefinedOrAssign(int[] bytecode, int pc, RuntimeBase[] r public static int executeStringConcatAssign(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs = bytecode[pc++]; + if (registers[rd] instanceof RuntimeScalarReadOnly) { + registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); + } RuntimeScalar result = StringOperators.stringConcat( (RuntimeScalar) registers[rd], (RuntimeScalar) registers[rs] @@ -267,6 +287,9 @@ public static int executeStringConcatAssign(int[] bytecode, int pc, RuntimeBase[ public static int executeBitwiseAndAssign(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs = bytecode[pc++]; + if (registers[rd] instanceof RuntimeScalarReadOnly) { + registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); + } RuntimeScalar result = BitwiseOperators.bitwiseAnd( (RuntimeScalar) registers[rd], (RuntimeScalar) registers[rs] @@ -287,6 +310,9 @@ public static int executeBitwiseAndAssign(int[] bytecode, int pc, RuntimeBase[] public static int executeBitwiseOrAssign(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs = bytecode[pc++]; + if (registers[rd] instanceof RuntimeScalarReadOnly) { + registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); + } RuntimeScalar result = BitwiseOperators.bitwiseOrBinary( (RuntimeScalar) registers[rd], (RuntimeScalar) registers[rs] @@ -307,6 +333,9 @@ public static int executeBitwiseOrAssign(int[] bytecode, int pc, RuntimeBase[] r public static int executeBitwiseXorAssign(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs = bytecode[pc++]; + if (registers[rd] instanceof RuntimeScalarReadOnly) { + registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); + } RuntimeScalar result = BitwiseOperators.bitwiseXorBinary( (RuntimeScalar) registers[rd], (RuntimeScalar) registers[rs] @@ -315,18 +344,12 @@ public static int executeBitwiseXorAssign(int[] bytecode, int pc, RuntimeBase[] return pc; } - /** - * Execute string bitwise AND assign operation. - * Format: STRING_BITWISE_AND_ASSIGN rd rs - * - * @param bytecode The bytecode array - * @param pc Current program counter - * @param registers Register file - * @return Updated program counter - */ public static int executeStringBitwiseAndAssign(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs = bytecode[pc++]; + if (registers[rd] instanceof RuntimeScalarReadOnly) { + registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); + } RuntimeScalar result = BitwiseOperators.bitwiseAndDot( (RuntimeScalar) registers[rd], (RuntimeScalar) registers[rs] @@ -335,18 +358,12 @@ public static int executeStringBitwiseAndAssign(int[] bytecode, int pc, RuntimeB return pc; } - /** - * Execute string bitwise OR assign operation. - * Format: STRING_BITWISE_OR_ASSIGN rd rs - * - * @param bytecode The bytecode array - * @param pc Current program counter - * @param registers Register file - * @return Updated program counter - */ public static int executeStringBitwiseOrAssign(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs = bytecode[pc++]; + if (registers[rd] instanceof RuntimeScalarReadOnly) { + registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); + } RuntimeScalar result = BitwiseOperators.bitwiseOrDot( (RuntimeScalar) registers[rd], (RuntimeScalar) registers[rs] @@ -355,18 +372,12 @@ public static int executeStringBitwiseOrAssign(int[] bytecode, int pc, RuntimeBa return pc; } - /** - * Execute string bitwise XOR assign operation. - * Format: STRING_BITWISE_XOR_ASSIGN rd rs - * - * @param bytecode The bytecode array - * @param pc Current program counter - * @param registers Register file - * @return Updated program counter - */ public static int executeStringBitwiseXorAssign(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs = bytecode[pc++]; + if (registers[rd] instanceof RuntimeScalarReadOnly) { + registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); + } RuntimeScalar result = BitwiseOperators.bitwiseXorDot( (RuntimeScalar) registers[rd], (RuntimeScalar) registers[rs] @@ -892,6 +903,10 @@ public static int executeSubtractAssign(int[] bytecode, int pc, RuntimeBase[] re int rs = bytecode[pc++]; RuntimeBase val1 = registers[rd]; + if (val1 instanceof RuntimeScalarReadOnly) { + val1 = BytecodeInterpreter.ensureMutableScalar(val1); + registers[rd] = val1; + } RuntimeBase val2 = registers[rs]; RuntimeScalar s1 = (val1 instanceof RuntimeScalar) ? (RuntimeScalar) val1 : val1.scalar(); RuntimeScalar s2 = (val2 instanceof RuntimeScalar) ? (RuntimeScalar) val2 : val2.scalar(); @@ -909,6 +924,10 @@ public static int executeMultiplyAssign(int[] bytecode, int pc, RuntimeBase[] re int rs = bytecode[pc++]; RuntimeBase val1 = registers[rd]; + if (val1 instanceof RuntimeScalarReadOnly) { + val1 = BytecodeInterpreter.ensureMutableScalar(val1); + registers[rd] = val1; + } RuntimeBase val2 = registers[rs]; RuntimeScalar s1 = (val1 instanceof RuntimeScalar) ? (RuntimeScalar) val1 : val1.scalar(); RuntimeScalar s2 = (val2 instanceof RuntimeScalar) ? (RuntimeScalar) val2 : val2.scalar(); @@ -925,6 +944,9 @@ public static int executeDivideAssign(int[] bytecode, int pc, RuntimeBase[] regi int rd = bytecode[pc++]; int rs = bytecode[pc++]; + if (registers[rd] instanceof RuntimeScalarReadOnly) { + registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); + } RuntimeBase val1 = registers[rd]; RuntimeBase val2 = registers[rs]; RuntimeScalar s1 = (val1 instanceof RuntimeScalar) ? (RuntimeScalar) val1 : val1.scalar(); @@ -934,14 +956,13 @@ public static int executeDivideAssign(int[] bytecode, int pc, RuntimeBase[] regi return pc; } - /** - * Execute modulus assign operation. - * Format: MODULUS_ASSIGN rd rs - */ public static int executeModulusAssign(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs = bytecode[pc++]; + if (registers[rd] instanceof RuntimeScalarReadOnly) { + registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); + } RuntimeBase val1 = registers[rd]; RuntimeBase val2 = registers[rs]; RuntimeScalar s1 = (val1 instanceof RuntimeScalar) ? (RuntimeScalar) val1 : val1.scalar(); From d0071f455325e7074f020ccd9a085ba2c7fbbba4 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sun, 1 Mar 2026 14:33:03 +0100 Subject: [PATCH 10/13] Add regex timeout protection against catastrophic backtracking Java's regex engine can hang indefinitely on certain patterns due to exponential backtracking inside Matcher.find(). This wraps regex input with a TimeoutCharSequence that checks elapsed time every 4096 charAt() calls and aborts after 10 seconds with a warning. Applied to all three regex entry points: match (m//), replace (s///), and split. Fixes #254. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../runtime/operators/Operator.java | 9 +++- .../regex/RegexTimeoutCharSequence.java | 48 +++++++++++++++++++ .../runtime/regex/RegexTimeoutException.java | 7 +++ .../runtime/regex/RuntimeRegex.java | 16 ++++++- 4 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/perlonjava/runtime/regex/RegexTimeoutCharSequence.java create mode 100644 src/main/java/org/perlonjava/runtime/regex/RegexTimeoutException.java diff --git a/src/main/java/org/perlonjava/runtime/operators/Operator.java b/src/main/java/org/perlonjava/runtime/operators/Operator.java index d6f8716ad..3a5471ccb 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Operator.java +++ b/src/main/java/org/perlonjava/runtime/operators/Operator.java @@ -7,6 +7,8 @@ import com.sun.jna.platform.win32.WinNT; import org.perlonjava.runtime.nativ.NativeUtils; import org.perlonjava.runtime.nativ.PosixLibrary; +import org.perlonjava.runtime.regex.RegexTimeoutCharSequence; +import org.perlonjava.runtime.regex.RegexTimeoutException; import org.perlonjava.runtime.regex.RuntimeRegex; import org.perlonjava.runtime.runtimetypes.*; @@ -144,10 +146,12 @@ public static RuntimeList split(RuntimeScalar quotedRegex, RuntimeList args, int } } } else { - Matcher matcher = pattern.matcher(inputStr); + CharSequence matchInput = new RegexTimeoutCharSequence(inputStr); + Matcher matcher = pattern.matcher(matchInput); int lastEnd = 0; int splitCount = 0; + try { while (matcher.find() && (limit <= 0 || splitCount < limit - 1)) { // Add the part before the match @@ -189,6 +193,9 @@ public static RuntimeList split(RuntimeScalar quotedRegex, RuntimeList args, int lastEnd = matcher.end(); splitCount++; } + } catch (RegexTimeoutException e) { + WarnDie.warn(new RuntimeScalar(e.getMessage() + "\n"), RuntimeScalarCache.scalarEmptyString); + } // Add the remaining part of the string if (lastEnd <= inputStr.length()) { diff --git a/src/main/java/org/perlonjava/runtime/regex/RegexTimeoutCharSequence.java b/src/main/java/org/perlonjava/runtime/regex/RegexTimeoutCharSequence.java new file mode 100644 index 000000000..8bcefd5ec --- /dev/null +++ b/src/main/java/org/perlonjava/runtime/regex/RegexTimeoutCharSequence.java @@ -0,0 +1,48 @@ +package org.perlonjava.runtime.regex; + +public class RegexTimeoutCharSequence implements CharSequence { + private static final long DEFAULT_TIMEOUT_MS = 10_000; + private static final int CHECK_INTERVAL = 4096; + + private final CharSequence inner; + private final long deadlineNanos; + private int checkCount; + + public RegexTimeoutCharSequence(CharSequence inner) { + this(inner, DEFAULT_TIMEOUT_MS); + } + + public RegexTimeoutCharSequence(CharSequence inner, long timeoutMillis) { + this.inner = inner; + this.deadlineNanos = System.nanoTime() + timeoutMillis * 1_000_000L; + } + + private RegexTimeoutCharSequence(CharSequence inner, long deadlineNanos, boolean shared) { + this.inner = inner; + this.deadlineNanos = deadlineNanos; + } + + @Override + public char charAt(int index) { + if (++checkCount % CHECK_INTERVAL == 0 && System.nanoTime() > deadlineNanos) { + throw new RegexTimeoutException( + "Regex matching timed out after " + DEFAULT_TIMEOUT_MS + "ms (catastrophic backtracking detected)"); + } + return inner.charAt(index); + } + + @Override + public int length() { + return inner.length(); + } + + @Override + public CharSequence subSequence(int start, int end) { + return new RegexTimeoutCharSequence(inner.subSequence(start, end), deadlineNanos, true); + } + + @Override + public String toString() { + return inner.toString(); + } +} diff --git a/src/main/java/org/perlonjava/runtime/regex/RegexTimeoutException.java b/src/main/java/org/perlonjava/runtime/regex/RegexTimeoutException.java new file mode 100644 index 000000000..89403d283 --- /dev/null +++ b/src/main/java/org/perlonjava/runtime/regex/RegexTimeoutException.java @@ -0,0 +1,7 @@ +package org.perlonjava.runtime.regex; + +public class RegexTimeoutException extends RuntimeException { + public RegexTimeoutException(String message) { + super(message); + } +} diff --git a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java index 9c6159ca3..1e77dea2e 100644 --- a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java +++ b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java @@ -413,7 +413,8 @@ private static RuntimeBase matchRegexDirect(RuntimeScalar quotedRegex, RuntimeSc Pattern pattern = regex.pattern; String inputStr = string.toString(); - Matcher matcher = pattern.matcher(inputStr); + CharSequence matchInput = new RegexTimeoutCharSequence(inputStr); + Matcher matcher = pattern.matcher(matchInput); // hexPrinter(inputStr); @@ -453,6 +454,7 @@ private static RuntimeBase matchRegexDirect(RuntimeScalar quotedRegex, RuntimeSc // Clearing these variables would incorrectly erase the previous successful capture // state and break tests that rely on @-/@+. + try { while (matcher.find()) { // If \G is used, ensure the match starts at the expected position if (regex.useGAssertion && isPosDefined && matcher.start() != startPos) { @@ -535,6 +537,10 @@ private static RuntimeBase matchRegexDirect(RuntimeScalar quotedRegex, RuntimeSc break; } } + } catch (RegexTimeoutException e) { + WarnDie.warn(new RuntimeScalar(e.getMessage() + "\n"), RuntimeScalarCache.scalarEmptyString); + found = false; + } // Reset pos() on failed match with /g, unless /c is set if (!found && regex.regexFlags.isGlobalMatch() && !regex.regexFlags.keepCurrentPosition()) { @@ -707,7 +713,8 @@ public static RuntimeBase replaceRegex(RuntimeScalar quotedRegex, RuntimeScalar } Pattern pattern = regex.pattern; - Matcher matcher = pattern.matcher(inputStr); + CharSequence matchInput = new RegexTimeoutCharSequence(inputStr); + Matcher matcher = pattern.matcher(matchInput); // The result string after substitutions StringBuilder resultBuffer = new StringBuilder(); @@ -720,6 +727,7 @@ public static RuntimeBase replaceRegex(RuntimeScalar quotedRegex, RuntimeScalar // This preserves capture variables from previous matches when substitution doesn't match // Perform the substitution + try { while (matcher.find()) { found++; @@ -769,6 +777,10 @@ public static RuntimeBase replaceRegex(RuntimeScalar quotedRegex, RuntimeScalar break; } } + } catch (RegexTimeoutException e) { + WarnDie.warn(new RuntimeScalar(e.getMessage() + "\n"), RuntimeScalarCache.scalarEmptyString); + found = 0; + } // Append the remaining text after the last match to the result buffer matcher.appendTail(resultBuffer); From 52360a3a9900722cc13a105bae0a4ad528dc82f9 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sun, 1 Mar 2026 15:06:47 +0100 Subject: [PATCH 11/13] Fix RuntimeScalarReadOnly and ScalarSpecialVariable leaks in bytecode interpreter The bytecode interpreter read-only checks only caught RuntimeScalarReadOnly but missed ScalarSpecialVariable (e.g. $1, $&), which also extends RuntimeBaseProxy and is effectively immutable. This caused read-only errors when registers containing special variables were used in mutating operations. - Add ScalarSpecialVariable handling to ensureMutableScalar() - Introduce isImmutableProxy() helper that checks both types - Replace all instanceof RuntimeScalarReadOnly guards with isImmutableProxy() across BytecodeInterpreter, OpcodeHandlerExtended, and SlowOpcodeHandler - Make ScalarSpecialVariable.getValueAsScalar() public for cross-package access Fixes ExifTool PLUS.t test 2 crash at STRING_CONCAT_ASSIGN opcode. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- run_exiftool_tests.sh | 27 +++++++++ .../backend/bytecode/BytecodeInterpreter.java | 56 ++++++++++++++----- .../bytecode/OpcodeHandlerExtended.java | 50 ++++++++++------- .../backend/bytecode/SlowOpcodeHandler.java | 7 ++- .../runtimetypes/ScalarSpecialVariable.java | 2 +- 5 files changed, 107 insertions(+), 35 deletions(-) create mode 100755 run_exiftool_tests.sh diff --git a/run_exiftool_tests.sh b/run_exiftool_tests.sh new file mode 100755 index 000000000..506987fb5 --- /dev/null +++ b/run_exiftool_tests.sh @@ -0,0 +1,27 @@ +#!/bin/bash +cd /Users/fglock/projects/PerlOnJava2/Image-ExifTool-13.44 +PASS=0 +FAIL=0 +PASS_LIST="" +FAIL_LIST="" +for t in t/*.t; do + name=$(basename "$t" .t) + output=$(timeout 60 java -jar ../target/perlonjava-3.0.0.jar -Ilib "$t" 2>&1) + exit_code=$? + # Check for "not ok" or non-zero exit + if echo "$output" | grep -q "^not ok"; then + FAIL=$((FAIL + 1)) + FAIL_LIST="$FAIL_LIST $name" + elif [ $exit_code -ne 0 ]; then + FAIL=$((FAIL + 1)) + FAIL_LIST="$FAIL_LIST $name" + else + PASS=$((PASS + 1)) + PASS_LIST="$PASS_LIST $name" + fi + echo "$name: exit=$exit_code" +done +echo "" +echo "PASS: $PASS" +echo "FAIL: $FAIL" +echo "Failing:$FAIL_LIST" diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index c0516c1b7..58201a963 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -28,9 +28,20 @@ static RuntimeScalar ensureMutableScalar(RuntimeBase val) { copy.value = ro.value; return copy; } + if (val instanceof ScalarSpecialVariable sv) { + RuntimeScalar src = sv.getValueAsScalar(); + RuntimeScalar copy = new RuntimeScalar(); + copy.type = src.type; + copy.value = src.value; + return copy; + } return (RuntimeScalar) val; } + static boolean isImmutableProxy(RuntimeBase val) { + return val instanceof RuntimeScalarReadOnly || val instanceof ScalarSpecialVariable; + } + /** * Execute interpreted bytecode. * @@ -192,7 +203,7 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c int dest = bytecode[pc++]; int src = bytecode[pc++]; RuntimeBase srcVal = registers[src]; - registers[dest] = (srcVal instanceof RuntimeScalarReadOnly) ? ensureMutableScalar(srcVal) : srcVal; + registers[dest] = isImmutableProxy(srcVal) ? ensureMutableScalar(srcVal) : srcVal; break; } @@ -255,6 +266,9 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c case Opcodes.UNDEFINE_SCALAR: { // Undefine variable in-place: rd.undefine() int rd = bytecode[pc++]; + if (isImmutableProxy(registers[rd])) { + registers[rd] = ensureMutableScalar(registers[rd]); + } registers[rd].undefine(); break; } @@ -330,6 +344,9 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c if (iterator.hasNext()) { RuntimeScalar element = iterator.next(); + if (isImmutableProxy(element)) { + element = ensureMutableScalar(element); + } registers[rd] = element; GlobalVariable.aliasGlobalVariable(name, element); pc = bodyTarget; // ABSOLUTE jump back to body start @@ -439,7 +456,7 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c int rs = bytecode[pc++]; RuntimeBase rdVal = registers[rd]; RuntimeScalar rdScalar; - if (rdVal instanceof RuntimeScalarReadOnly) { + if (isImmutableProxy(rdVal)) { rdScalar = new RuntimeScalar(); registers[rd] = rdScalar; } else if (rdVal instanceof RuntimeScalar) { @@ -687,7 +704,8 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c if (iterator.hasNext()) { // Get next element and jump back to body - registers[rd] = iterator.next(); + RuntimeScalar elem = iterator.next(); + registers[rd] = (isImmutableProxy(elem)) ? ensureMutableScalar(elem) : elem; pc = bodyTarget; // ABSOLUTE jump back to body start } // else: fall through to exit @@ -1025,7 +1043,8 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c // Convert to scalar if called in scalar context if (context == RuntimeContextType.SCALAR) { - registers[rd] = result.scalar(); + RuntimeBase scalarResult = result.scalar(); + registers[rd] = (isImmutableProxy(scalarResult)) ? ensureMutableScalar(scalarResult) : scalarResult; } else { registers[rd] = result; } @@ -1087,7 +1106,8 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c // Convert to scalar if called in scalar context if (context == RuntimeContextType.SCALAR) { - registers[rd] = result.scalar(); + RuntimeBase scalarResult = result.scalar(); + registers[rd] = (isImmutableProxy(scalarResult)) ? ensureMutableScalar(scalarResult) : scalarResult; } else { registers[rd] = result; } @@ -1200,7 +1220,7 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c // Increment register in-place: r++ int rd = bytecode[pc++]; RuntimeBase incResult = MathOperators.add((RuntimeScalar) registers[rd], 1); - registers[rd] = (incResult instanceof RuntimeScalarReadOnly) ? ensureMutableScalar(incResult) : incResult; + registers[rd] = (isImmutableProxy(incResult)) ? ensureMutableScalar(incResult) : incResult; break; } @@ -1208,7 +1228,7 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c // Decrement register in-place: r-- int rd = bytecode[pc++]; RuntimeBase decResult = MathOperators.subtract((RuntimeScalar) registers[rd], 1); - registers[rd] = (decResult instanceof RuntimeScalarReadOnly) ? ensureMutableScalar(decResult) : decResult; + registers[rd] = (isImmutableProxy(decResult)) ? ensureMutableScalar(decResult) : decResult; break; } @@ -1216,7 +1236,7 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c // Add and assign: rd += rs (modifies rd in place) int rd = bytecode[pc++]; int rs = bytecode[pc++]; - if (registers[rd] instanceof RuntimeScalarReadOnly) { + if (isImmutableProxy(registers[rd])) { registers[rd] = ensureMutableScalar(registers[rd]); } MathOperators.addAssign( @@ -1231,7 +1251,7 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c int rd = bytecode[pc++]; int immediate = readInt(bytecode, pc); pc += 1; - if (registers[rd] instanceof RuntimeScalarReadOnly) { + if (isImmutableProxy(registers[rd])) { registers[rd] = ensureMutableScalar(registers[rd]); } RuntimeScalar result = MathOperators.add((RuntimeScalar) registers[rd], immediate); @@ -2394,7 +2414,15 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c } // Wrap other exceptions with interpreter context including bytecode context - String errorMessage = formatInterpreterError(code, pc, e); + int debugPc = Math.max(0, pc - 3); + String opcodeInfo = " [opcodes at pc-3..pc: "; + for (int di = debugPc; di <= Math.min(pc + 2, bytecode.length - 1); di++) { + if (di == pc) opcodeInfo += ">>>"; + opcodeInfo += bytecode[di] + " "; + if (di == pc) opcodeInfo += "<<< "; + } + opcodeInfo += "]"; + String errorMessage = formatInterpreterError(code, pc, e) + opcodeInfo; throw new RuntimeException(errorMessage, e); } } // end outer while @@ -2514,7 +2542,7 @@ private static int executeArithmetic(int opcode, int[] bytecode, int pc, case Opcodes.SUBTRACT_ASSIGN: { int rd = bytecode[pc++]; int rs = bytecode[pc++]; - if (registers[rd] instanceof RuntimeScalarReadOnly) { + if (isImmutableProxy(registers[rd])) { registers[rd] = ensureMutableScalar(registers[rd]); } RuntimeBase val1 = registers[rd]; @@ -2528,7 +2556,7 @@ private static int executeArithmetic(int opcode, int[] bytecode, int pc, case Opcodes.MULTIPLY_ASSIGN: { int rd = bytecode[pc++]; int rs = bytecode[pc++]; - if (registers[rd] instanceof RuntimeScalarReadOnly) { + if (isImmutableProxy(registers[rd])) { registers[rd] = ensureMutableScalar(registers[rd]); } RuntimeBase val1 = registers[rd]; @@ -2542,7 +2570,7 @@ private static int executeArithmetic(int opcode, int[] bytecode, int pc, case Opcodes.DIVIDE_ASSIGN: { int rd = bytecode[pc++]; int rs = bytecode[pc++]; - if (registers[rd] instanceof RuntimeScalarReadOnly) { + if (isImmutableProxy(registers[rd])) { registers[rd] = ensureMutableScalar(registers[rd]); } RuntimeBase val1 = registers[rd]; @@ -2556,7 +2584,7 @@ private static int executeArithmetic(int opcode, int[] bytecode, int pc, case Opcodes.MODULUS_ASSIGN: { int rd = bytecode[pc++]; int rs = bytecode[pc++]; - if (registers[rd] instanceof RuntimeScalarReadOnly) { + if (isImmutableProxy(registers[rd])) { registers[rd] = ensureMutableScalar(registers[rd]); } RuntimeBase val1 = registers[rd]; diff --git a/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java b/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java index 8b862514f..fe5995c2c 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java +++ b/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java @@ -109,7 +109,7 @@ public static int executeSubstrVar(int[] bytecode, int pc, RuntimeBase[] registe public static int executeRepeatAssign(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs = bytecode[pc++]; - if (registers[rd] instanceof RuntimeScalarReadOnly) { + if (BytecodeInterpreter.isImmutableProxy(registers[rd])) { registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); } RuntimeBase result = Operator.repeat( @@ -133,7 +133,7 @@ public static int executeRepeatAssign(int[] bytecode, int pc, RuntimeBase[] regi public static int executePowAssign(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs = bytecode[pc++]; - if (registers[rd] instanceof RuntimeScalarReadOnly) { + if (BytecodeInterpreter.isImmutableProxy(registers[rd])) { registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); } RuntimeBase val1 = registers[rd]; @@ -157,7 +157,7 @@ public static int executePowAssign(int[] bytecode, int pc, RuntimeBase[] registe public static int executeLeftShiftAssign(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs = bytecode[pc++]; - if (registers[rd] instanceof RuntimeScalarReadOnly) { + if (BytecodeInterpreter.isImmutableProxy(registers[rd])) { registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); } RuntimeScalar s1 = (RuntimeScalar) registers[rd]; @@ -179,7 +179,7 @@ public static int executeLeftShiftAssign(int[] bytecode, int pc, RuntimeBase[] r public static int executeRightShiftAssign(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs = bytecode[pc++]; - if (registers[rd] instanceof RuntimeScalarReadOnly) { + if (BytecodeInterpreter.isImmutableProxy(registers[rd])) { registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); } RuntimeScalar s1 = (RuntimeScalar) registers[rd]; @@ -201,7 +201,7 @@ public static int executeRightShiftAssign(int[] bytecode, int pc, RuntimeBase[] public static int executeLogicalAndAssign(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs = bytecode[pc++]; - if (registers[rd] instanceof RuntimeScalarReadOnly) { + if (BytecodeInterpreter.isImmutableProxy(registers[rd])) { registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); } RuntimeScalar s1 = ((RuntimeBase) registers[rd]).scalar(); @@ -225,7 +225,7 @@ public static int executeLogicalAndAssign(int[] bytecode, int pc, RuntimeBase[] public static int executeLogicalOrAssign(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs = bytecode[pc++]; - if (registers[rd] instanceof RuntimeScalarReadOnly) { + if (BytecodeInterpreter.isImmutableProxy(registers[rd])) { registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); } RuntimeScalar s1 = ((RuntimeBase) registers[rd]).scalar(); @@ -240,7 +240,7 @@ public static int executeLogicalOrAssign(int[] bytecode, int pc, RuntimeBase[] r public static int executeDefinedOrAssign(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs = bytecode[pc++]; - if (registers[rd] instanceof RuntimeScalarReadOnly) { + if (BytecodeInterpreter.isImmutableProxy(registers[rd])) { registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); } RuntimeScalar s1 = ((RuntimeBase) registers[rd]).scalar(); @@ -264,7 +264,7 @@ public static int executeDefinedOrAssign(int[] bytecode, int pc, RuntimeBase[] r public static int executeStringConcatAssign(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs = bytecode[pc++]; - if (registers[rd] instanceof RuntimeScalarReadOnly) { + if (BytecodeInterpreter.isImmutableProxy(registers[rd])) { registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); } RuntimeScalar result = StringOperators.stringConcat( @@ -287,7 +287,7 @@ public static int executeStringConcatAssign(int[] bytecode, int pc, RuntimeBase[ public static int executeBitwiseAndAssign(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs = bytecode[pc++]; - if (registers[rd] instanceof RuntimeScalarReadOnly) { + if (BytecodeInterpreter.isImmutableProxy(registers[rd])) { registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); } RuntimeScalar result = BitwiseOperators.bitwiseAnd( @@ -310,7 +310,7 @@ public static int executeBitwiseAndAssign(int[] bytecode, int pc, RuntimeBase[] public static int executeBitwiseOrAssign(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs = bytecode[pc++]; - if (registers[rd] instanceof RuntimeScalarReadOnly) { + if (BytecodeInterpreter.isImmutableProxy(registers[rd])) { registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); } RuntimeScalar result = BitwiseOperators.bitwiseOrBinary( @@ -333,7 +333,7 @@ public static int executeBitwiseOrAssign(int[] bytecode, int pc, RuntimeBase[] r public static int executeBitwiseXorAssign(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs = bytecode[pc++]; - if (registers[rd] instanceof RuntimeScalarReadOnly) { + if (BytecodeInterpreter.isImmutableProxy(registers[rd])) { registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); } RuntimeScalar result = BitwiseOperators.bitwiseXorBinary( @@ -347,7 +347,7 @@ public static int executeBitwiseXorAssign(int[] bytecode, int pc, RuntimeBase[] public static int executeStringBitwiseAndAssign(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs = bytecode[pc++]; - if (registers[rd] instanceof RuntimeScalarReadOnly) { + if (BytecodeInterpreter.isImmutableProxy(registers[rd])) { registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); } RuntimeScalar result = BitwiseOperators.bitwiseAndDot( @@ -361,7 +361,7 @@ public static int executeStringBitwiseAndAssign(int[] bytecode, int pc, RuntimeB public static int executeStringBitwiseOrAssign(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs = bytecode[pc++]; - if (registers[rd] instanceof RuntimeScalarReadOnly) { + if (BytecodeInterpreter.isImmutableProxy(registers[rd])) { registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); } RuntimeScalar result = BitwiseOperators.bitwiseOrDot( @@ -375,7 +375,7 @@ public static int executeStringBitwiseOrAssign(int[] bytecode, int pc, RuntimeBa public static int executeStringBitwiseXorAssign(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs = bytecode[pc++]; - if (registers[rd] instanceof RuntimeScalarReadOnly) { + if (BytecodeInterpreter.isImmutableProxy(registers[rd])) { registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); } RuntimeScalar result = BitwiseOperators.bitwiseXorDot( @@ -699,6 +699,9 @@ public static int executeRindex(int[] bytecode, int pc, RuntimeBase[] registers) */ public static int executePreAutoIncrement(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; + if (BytecodeInterpreter.isImmutableProxy(registers[rd])) { + registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); + } ((RuntimeScalar) registers[rd]).preAutoIncrement(); return pc; } @@ -710,6 +713,9 @@ public static int executePreAutoIncrement(int[] bytecode, int pc, RuntimeBase[] public static int executePostAutoIncrement(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs = bytecode[pc++]; + if (BytecodeInterpreter.isImmutableProxy(registers[rs])) { + registers[rs] = BytecodeInterpreter.ensureMutableScalar(registers[rs]); + } registers[rd] = ((RuntimeScalar) registers[rs]).postAutoIncrement(); return pc; } @@ -720,6 +726,9 @@ public static int executePostAutoIncrement(int[] bytecode, int pc, RuntimeBase[] */ public static int executePreAutoDecrement(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; + if (BytecodeInterpreter.isImmutableProxy(registers[rd])) { + registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); + } ((RuntimeScalar) registers[rd]).preAutoDecrement(); return pc; } @@ -731,6 +740,9 @@ public static int executePreAutoDecrement(int[] bytecode, int pc, RuntimeBase[] public static int executePostAutoDecrement(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int rs = bytecode[pc++]; + if (BytecodeInterpreter.isImmutableProxy(registers[rs])) { + registers[rs] = BytecodeInterpreter.ensureMutableScalar(registers[rs]); + } registers[rd] = ((RuntimeScalar) registers[rs]).postAutoDecrement(); return pc; } @@ -890,7 +902,7 @@ public static int executeIteratorNext(int[] bytecode, int pc, RuntimeBase[] regi (java.util.Iterator) iterScalar.value; RuntimeScalar next = iterator.next(); - registers[rd] = next; + registers[rd] = BytecodeInterpreter.isImmutableProxy(next) ? BytecodeInterpreter.ensureMutableScalar(next) : next; return pc; } @@ -903,7 +915,7 @@ public static int executeSubtractAssign(int[] bytecode, int pc, RuntimeBase[] re int rs = bytecode[pc++]; RuntimeBase val1 = registers[rd]; - if (val1 instanceof RuntimeScalarReadOnly) { + if (BytecodeInterpreter.isImmutableProxy(val1)) { val1 = BytecodeInterpreter.ensureMutableScalar(val1); registers[rd] = val1; } @@ -924,7 +936,7 @@ public static int executeMultiplyAssign(int[] bytecode, int pc, RuntimeBase[] re int rs = bytecode[pc++]; RuntimeBase val1 = registers[rd]; - if (val1 instanceof RuntimeScalarReadOnly) { + if (BytecodeInterpreter.isImmutableProxy(val1)) { val1 = BytecodeInterpreter.ensureMutableScalar(val1); registers[rd] = val1; } @@ -944,7 +956,7 @@ public static int executeDivideAssign(int[] bytecode, int pc, RuntimeBase[] regi int rd = bytecode[pc++]; int rs = bytecode[pc++]; - if (registers[rd] instanceof RuntimeScalarReadOnly) { + if (BytecodeInterpreter.isImmutableProxy(registers[rd])) { registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); } RuntimeBase val1 = registers[rd]; @@ -960,7 +972,7 @@ public static int executeModulusAssign(int[] bytecode, int pc, RuntimeBase[] reg int rd = bytecode[pc++]; int rs = bytecode[pc++]; - if (registers[rd] instanceof RuntimeScalarReadOnly) { + if (BytecodeInterpreter.isImmutableProxy(registers[rd])) { registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); } RuntimeBase val1 = registers[rd]; diff --git a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java index 8430e8082..1bfcbf3f2 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java @@ -1020,7 +1020,12 @@ public static int executeTransliterate( RuntimeScalar search = (RuntimeScalar) registers[searchReg]; RuntimeScalar replace = (RuntimeScalar) registers[replaceReg]; RuntimeScalar modifiers = (RuntimeScalar) registers[modifiersReg]; - RuntimeScalar target = (RuntimeScalar) registers[targetReg]; + RuntimeBase targetBase = registers[targetReg]; + if (BytecodeInterpreter.isImmutableProxy(targetBase)) { + targetBase = BytecodeInterpreter.ensureMutableScalar(targetBase); + registers[targetReg] = targetBase; + } + RuntimeScalar target = (RuntimeScalar) targetBase; // Compile and apply transliteration RuntimeTransliterate tr = RuntimeTransliterate.compile(search, replace, modifiers); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java index c4cdd0abb..8b96d5916 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java @@ -108,7 +108,7 @@ public RuntimeScalar addToScalar(RuntimeScalar var) { * * @return The RuntimeScalar value of the special variable, or null if not available. */ - RuntimeScalar getValueAsScalar() { + public RuntimeScalar getValueAsScalar() { try { RuntimeScalar result = switch (variableId) { case CAPTURE -> { From 786273c8818ef4ceeb40ff613b18404031c8c5c3 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sun, 1 Mar 2026 15:51:38 +0100 Subject: [PATCH 12/13] Fix sort with named comparator and bytecode package scoping - Fix parser to correctly handle `sort SubName @list` syntax by detecting bare identifier comparators before the try/catch path, constructing a code reference AST node directly instead of letting the sub be called with the list as arguments - Propagate current package to sub-compilers in BytecodeCompiler for both named and anonymous subroutines, fixing package resolution when bytecode interpreter fallback is triggered - Add string-to-coderef resolution in ListOperators.sort() for comparators passed as string subroutine names Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/bytecode/BytecodeCompiler.java | 4 ++ .../frontend/parser/ParseMapGrepSort.java | 50 ++++++++++++------- .../runtime/operators/ListOperators.java | 14 +++++- 3 files changed, 48 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 9d7c3d78e..5968c3769 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -3863,6 +3863,8 @@ private void visitNamedSubroutine(SubroutineNode node) { this.errorUtil, packedRegistry ); + subCompiler.symbolTable.setCurrentPackage(getCurrentPackage(), + symbolTable.currentPackageIsClass()); // Set the BEGIN ID in the sub-compiler so it knows to use RETRIEVE_BEGIN opcodes subCompiler.currentSubroutineBeginId = beginId; @@ -3969,6 +3971,8 @@ private void visitAnonymousSubroutine(SubroutineNode node) { this.errorUtil, parentRegistry // Pass parent variable registry for nested closure support ); + subCompiler.symbolTable.setCurrentPackage(getCurrentPackage(), + symbolTable.currentPackageIsClass()); // Step 4: Compile the subroutine body // Sub-compiler will use parentRegistry to resolve captured variables diff --git a/src/main/java/org/perlonjava/frontend/parser/ParseMapGrepSort.java b/src/main/java/org/perlonjava/frontend/parser/ParseMapGrepSort.java index 3ed5de56c..129fefa1d 100644 --- a/src/main/java/org/perlonjava/frontend/parser/ParseMapGrepSort.java +++ b/src/main/java/org/perlonjava/frontend/parser/ParseMapGrepSort.java @@ -2,6 +2,7 @@ import org.perlonjava.frontend.astnode.*; import org.perlonjava.frontend.lexer.LexerToken; +import org.perlonjava.frontend.lexer.LexerTokenType; import org.perlonjava.runtime.runtimetypes.PerlCompilerException; import java.util.List; @@ -20,29 +21,40 @@ public class ParseMapGrepSort { static BinaryOperatorNode parseSort(Parser parser, LexerToken token) { ListNode operand; int currentIndex = parser.tokenIndex; - try { - // Handle 'sort' keyword as a Binary operator with a Code and List operands - operand = ListParser.parseZeroOrMoreList(parser, 1, true, false, false, false); - } catch (PerlCompilerException e) { - // sort $sub 1,2,3 - parser.tokenIndex = currentIndex; - - boolean paren = false; - if (peek(parser).text.equals("(")) { - TokenUtils.consume(parser); - paren = true; - } - parser.parsingForLoopVariable = true; - Node var = ParsePrimary.parsePrimary(parser); - parser.parsingForLoopVariable = false; - operand = ListParser.parseZeroOrMoreList(parser, 1, false, false, false, false); + LexerToken nextToken = peek(parser); + if (nextToken.type == LexerTokenType.IDENTIFIER && !nextToken.text.equals("{") + && !ParserTables.CORE_PROTOTYPES.containsKey(nextToken.text) + && !ParsePrimary.isIsQuoteLikeOperator(nextToken.text)) { + String subName = IdentifierParser.parseSubroutineIdentifier(parser); + Node var = new OperatorNode("&", + new IdentifierNode(subName, parser.tokenIndex), parser.tokenIndex); + operand = ListParser.parseZeroOrMoreList(parser, 0, false, false, false, false); operand.handle = var; + parser.ctx.logDebug("parseSort identifier: " + operand.handle + " : " + operand); + } else { + try { + operand = ListParser.parseZeroOrMoreList(parser, 1, true, false, false, false); + } catch (PerlCompilerException e) { + parser.tokenIndex = currentIndex; - if (paren) { - TokenUtils.consume(parser, OPERATOR, ")"); + boolean paren = false; + if (peek(parser).text.equals("(")) { + TokenUtils.consume(parser); + paren = true; + } + + parser.parsingForLoopVariable = true; + Node var = ParsePrimary.parsePrimary(parser); + parser.parsingForLoopVariable = false; + operand = ListParser.parseZeroOrMoreList(parser, 1, false, false, false, false); + operand.handle = var; + + if (paren) { + TokenUtils.consume(parser, OPERATOR, ")"); + } + parser.ctx.logDebug("parseSort: " + operand.handle + " : " + operand); } - parser.ctx.logDebug("parseSort: " + operand.handle + " : " + operand); } // transform: { 123 } diff --git a/src/main/java/org/perlonjava/runtime/operators/ListOperators.java b/src/main/java/org/perlonjava/runtime/operators/ListOperators.java index 09b0e720b..154b44cd8 100644 --- a/src/main/java/org/perlonjava/runtime/operators/ListOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/ListOperators.java @@ -78,6 +78,18 @@ public static RuntimeList sort(RuntimeList runtimeList, RuntimeScalar perlCompar // Create a new list from the elements of this RuntimeArray RuntimeArray array = runtimeList.getArrayOfAlias(); + // If comparator is a string (subroutine name), resolve it to a code reference + RuntimeScalar comparator = perlComparatorClosure; + if (comparator.type == RuntimeScalarType.STRING || + comparator.type == RuntimeScalarType.BYTE_STRING) { + String subName = comparator.toString(); + if (!subName.contains("::")) { + subName = packageName + "::" + subName; + } + comparator = GlobalVariable.getGlobalCodeRef(subName); + } + final RuntimeScalar finalComparator = comparator; + RuntimeArray comparatorArgs = new RuntimeArray(); // Create the sort variables @@ -92,7 +104,7 @@ public static RuntimeList sort(RuntimeList runtimeList, RuntimeScalar perlCompar varB.set(b); // Apply the Perl comparator subroutine with the arguments - RuntimeList result = RuntimeCode.apply(perlComparatorClosure, comparatorArgs, RuntimeContextType.SCALAR); + RuntimeList result = RuntimeCode.apply(finalComparator, comparatorArgs, RuntimeContextType.SCALAR); // Retrieve the comparison result and return it as an integer return result.getFirst().getInt(); From 98b37266965240e362726cf9e4d726c5eb2577b6 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sun, 1 Mar 2026 16:09:00 +0100 Subject: [PATCH 13/13] Fix MRO cache invalidation and splice with null elements - Fix invalidateCacheForClass to also remove the exact class entry from linearizedClassesCache (was only removing subclass entries matching "className::" prefix, missing the class itself) - Fix splice to handle null array elements (from $#a++) by replacing them with undef RuntimeScalar instead of propagating Java null These fix splice.t tests 30-32 (splice @ISA cache invalidation, splice with nonexistent array elements). Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../org/perlonjava/runtime/mro/InheritanceResolver.java | 8 +++++--- .../java/org/perlonjava/runtime/operators/Operator.java | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java b/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java index 14cf9b03b..36227b6c6 100644 --- a/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java +++ b/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java @@ -124,11 +124,13 @@ private static boolean hasIsaChanged(String className) { * Invalidate cache for a specific class and its dependents. */ private static void invalidateCacheForClass(String className) { - // Remove from linearization cache + // Remove exact class and subclasses from linearization cache + linearizedClassesCache.remove(className); linearizedClassesCache.entrySet().removeIf(entry -> entry.getKey().startsWith(className + "::")); - // Remove from method cache (entries that reference this class) - methodCache.entrySet().removeIf(entry -> entry.getKey().contains(className + "::")); + // Remove from method cache (entries for this class and subclasses) + methodCache.entrySet().removeIf(entry -> + entry.getKey().startsWith(className + "::") || entry.getKey().contains("::" + className + "::")); // Could also notify dependents here if we had that information } diff --git a/src/main/java/org/perlonjava/runtime/operators/Operator.java b/src/main/java/org/perlonjava/runtime/operators/Operator.java index 3a5471ccb..e0fa6a696 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Operator.java +++ b/src/main/java/org/perlonjava/runtime/operators/Operator.java @@ -361,7 +361,8 @@ public static RuntimeList splice(RuntimeArray runtimeArray, RuntimeList list) { // Remove elements for (int i = 0; i < length && offset < runtimeArray.size(); i++) { - removedElements.elements.add(runtimeArray.elements.remove(offset)); + RuntimeBase removed = runtimeArray.elements.remove(offset); + removedElements.elements.add(removed != null ? removed : new RuntimeScalar()); } // Add new elements