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/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index d6f446fae..5968c3769 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); } } } @@ -3854,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; @@ -3960,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/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 856e06f57..58201a963 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -21,6 +21,27 @@ 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; + } + 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. * @@ -115,10 +136,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 +199,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] = isImmutableProxy(srcVal) ? ensureMutableScalar(srcVal) : srcVal; break; } @@ -244,6 +263,16 @@ 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++]; + if (isImmutableProxy(registers[rd])) { + registers[rd] = ensureMutableScalar(registers[rd]); + } + registers[rd].undefine(); + break; + } + // ================================================================= // VARIABLE ACCESS - GLOBAL // ================================================================= @@ -315,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 @@ -422,7 +454,17 @@ 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; + if (isImmutableProxy(rdVal)) { + rdScalar = new RuntimeScalar(); + registers[rd] = rdScalar; + } else if (rdVal instanceof RuntimeScalar) { + rdScalar = (RuntimeScalar) rdVal; + } else { + rdScalar = rdVal.scalar(); + } + registers[rs].addToScalar(rdScalar); break; } @@ -662,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 @@ -916,8 +959,9 @@ 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]; - hash.put(key.toString(), val); // Convert key to String + RuntimeBase valBase = registers[valueReg]; + RuntimeScalar val = (valBase instanceof RuntimeScalar) ? (RuntimeScalar) valBase : valBase.scalar(); + hash.put(key.toString(), ensureMutableScalar(val)); break; } @@ -999,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; } @@ -1061,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; } @@ -1173,14 +1219,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] = (isImmutableProxy(incResult)) ? 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] = (isImmutableProxy(decResult)) ? ensureMutableScalar(decResult) : decResult; break; } @@ -1188,6 +1236,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 (isImmutableProxy(registers[rd])) { + registers[rd] = ensureMutableScalar(registers[rd]); + } MathOperators.addAssign( (RuntimeScalar) registers[rd], (RuntimeScalar) registers[rs] @@ -1200,6 +1251,9 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c int rd = bytecode[pc++]; int immediate = readInt(bytecode, pc); pc += 1; + if (isImmutableProxy(registers[rd])) { + registers[rd] = ensureMutableScalar(registers[rd]); + } RuntimeScalar result = MathOperators.add((RuntimeScalar) registers[rd], immediate); ((RuntimeScalar) registers[rd]).set(result); break; @@ -2311,7 +2365,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 +2381,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 @@ -2358,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 @@ -2478,6 +2542,9 @@ private static int executeArithmetic(int opcode, int[] bytecode, int pc, case Opcodes.SUBTRACT_ASSIGN: { int rd = bytecode[pc++]; int rs = bytecode[pc++]; + if (isImmutableProxy(registers[rd])) { + registers[rd] = ensureMutableScalar(registers[rd]); + } RuntimeBase val1 = registers[rd]; RuntimeBase val2 = registers[rs]; RuntimeScalar s1 = (val1 instanceof RuntimeScalar) ? (RuntimeScalar) val1 : val1.scalar(); @@ -2489,6 +2556,9 @@ private static int executeArithmetic(int opcode, int[] bytecode, int pc, case Opcodes.MULTIPLY_ASSIGN: { int rd = bytecode[pc++]; int rs = bytecode[pc++]; + if (isImmutableProxy(registers[rd])) { + registers[rd] = ensureMutableScalar(registers[rd]); + } RuntimeBase val1 = registers[rd]; RuntimeBase val2 = registers[rs]; RuntimeScalar s1 = (val1 instanceof RuntimeScalar) ? (RuntimeScalar) val1 : val1.scalar(); @@ -2500,6 +2570,9 @@ private static int executeArithmetic(int opcode, int[] bytecode, int pc, case Opcodes.DIVIDE_ASSIGN: { int rd = bytecode[pc++]; int rs = bytecode[pc++]; + if (isImmutableProxy(registers[rd])) { + registers[rd] = ensureMutableScalar(registers[rd]); + } RuntimeBase val1 = registers[rd]; RuntimeBase val2 = registers[rs]; RuntimeScalar s1 = (val1 instanceof RuntimeScalar) ? (RuntimeScalar) val1 : val1.scalar(); @@ -2511,6 +2584,9 @@ private static int executeArithmetic(int opcode, int[] bytecode, int pc, case Opcodes.MODULUS_ASSIGN: { int rd = bytecode[pc++]; int rs = bytecode[pc++]; + if (isImmutableProxy(registers[rd])) { + 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..fe5995c2c 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 (BytecodeInterpreter.isImmutableProxy(registers[rd])) { + 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 (BytecodeInterpreter.isImmutableProxy(registers[rd])) { + 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 (BytecodeInterpreter.isImmutableProxy(registers[rd])) { + 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 (BytecodeInterpreter.isImmutableProxy(registers[rd])) { + 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 (BytecodeInterpreter.isImmutableProxy(registers[rd])) { + 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 (BytecodeInterpreter.isImmutableProxy(registers[rd])) { + 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 (BytecodeInterpreter.isImmutableProxy(registers[rd])) { + 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 (BytecodeInterpreter.isImmutableProxy(registers[rd])) { + 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 (BytecodeInterpreter.isImmutableProxy(registers[rd])) { + 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 (BytecodeInterpreter.isImmutableProxy(registers[rd])) { + 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 (BytecodeInterpreter.isImmutableProxy(registers[rd])) { + 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 (BytecodeInterpreter.isImmutableProxy(registers[rd])) { + 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 (BytecodeInterpreter.isImmutableProxy(registers[rd])) { + 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 (BytecodeInterpreter.isImmutableProxy(registers[rd])) { + registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]); + } RuntimeScalar result = BitwiseOperators.bitwiseXorDot( (RuntimeScalar) registers[rd], (RuntimeScalar) registers[rs] @@ -688,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; } @@ -699,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; } @@ -709,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; } @@ -720,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; } @@ -879,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; } @@ -892,6 +915,10 @@ public static int executeSubtractAssign(int[] bytecode, int pc, RuntimeBase[] re int rs = bytecode[pc++]; RuntimeBase val1 = registers[rd]; + if (BytecodeInterpreter.isImmutableProxy(val1)) { + 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 +936,10 @@ public static int executeMultiplyAssign(int[] bytecode, int pc, RuntimeBase[] re int rs = bytecode[pc++]; RuntimeBase val1 = registers[rd]; + if (BytecodeInterpreter.isImmutableProxy(val1)) { + 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 +956,9 @@ public static int executeDivideAssign(int[] bytecode, int pc, RuntimeBase[] regi int rd = bytecode[pc++]; int rs = bytecode[pc++]; + if (BytecodeInterpreter.isImmutableProxy(registers[rd])) { + 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 +968,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 (BytecodeInterpreter.isImmutableProxy(registers[rd])) { + registers[rd] = BytecodeInterpreter.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/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/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/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/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/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(); diff --git a/src/main/java/org/perlonjava/runtime/operators/Operator.java b/src/main/java/org/perlonjava/runtime/operators/Operator.java index d6f8716ad..e0fa6a696 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()) { @@ -354,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 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(); } } 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); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArray.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArray.java index 5ed814f69..3d13b5252 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; @@ -593,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; } @@ -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/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); + } } 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/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"); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java index be64a1ae6..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 -> { @@ -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. *