Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
6796372
feat: implement non-quick-fix test improvements
fglock Apr 1, 2026
7f5128e
fix: support local @{expr} and local %{expr} in interpreter
fglock Apr 1, 2026
51f82ea
feat: fix Perl version checking, no VERSION, and error message formats
fglock Apr 1, 2026
26eab3e
fix: flatten array arguments in printf for correct format processing
fglock Apr 1, 2026
4182733
fix: improve stat/lstat and file test operators
fglock Apr 1, 2026
4a31eea
fix: substr OOB lvalue dies on assignment, lstat *_ croak after stat
fglock Apr 1, 2026
fbd20f1
fix: symbolic deref for non-string types, local $#array, readdir null…
fglock Apr 1, 2026
4662080
Fix 'x' and 'isa' parsed as infix operators in label/bareword context
fglock Apr 1, 2026
4b88885
Fix 'for our $var' and complex lvalue for loops (e.g., for ${*$f})
fglock Apr 1, 2026
b1a3d3f
Fix gmtime/localtime crash on out-of-range and NaN values
fglock Apr 1, 2026
875e7fb
Fix if/unless return value when no branch is taken
fglock Apr 1, 2026
91c55ba
Fix ucfirst to handle one-to-many Unicode titlecase mappings
fglock Apr 1, 2026
217e036
Improve ucfirst: handle combining characters (U+0345) with code-point…
fglock Apr 1, 2026
f47be0b
Update test failure analysis with 2026-04-01 investigation results
fglock Apr 1, 2026
7d1d907
Fix do-file to set EISDIR when @INC path is a directory
fglock Apr 1, 2026
b3cab93
fix: while loop returns false condition value instead of undef
fglock Apr 1, 2026
a9cc4fa
fix: detect my() in false conditional error (Perl 5.30+, RT #133543)
fglock Apr 1, 2026
cadab04
fix: push/unshift error messages, Internals::SvREADONLY for arrays, p…
fglock Apr 1, 2026
957bbe9
fix: select with 4 args compiled in LIST context instead of scalar
fglock Apr 1, 2026
e2e6636
fix: ++ on vstring flattens to STRING type (matches Perl behavior)
fglock Apr 1, 2026
9e1144c
fix: oct/hex overflow detection for values exceeding 64-bit unsigned …
fglock Apr 1, 2026
06c7f2c
fix: non-fatal (?{...}) code blocks, \(LIST interpreter fix, regex re…
fglock Apr 1, 2026
6e2915c
fix: \(LIST interpreter fix and regex recompilation bug
fglock Apr 1, 2026
896bfd3
fix: add JPERL_UNIMPLEMENTED=warn for tests with (?{...}) code blocks
fglock Apr 1, 2026
e58ff4b
fix: resolve opcode collisions after rebase and VERSION error message…
fglock Apr 1, 2026
d8d4d61
fix: closure.t and for.t regressions, update plan with regression ana…
fglock Apr 1, 2026
1736106
fix: regex deref, glob inc/dec read-only, sparse array reverse
fglock Apr 1, 2026
91a2492
fix: die qr{x} now appends location info like string messages
fglock Apr 1, 2026
2c9e011
fix: isa parse, glob copy inc/dec, decl-refs regression, concat regre…
fglock Apr 1, 2026
16a116f
docs: update test-failures plan with session fixes
fglock Apr 1, 2026
f1605b9
fix: remove taint_support key from Config.pm to restore taint test sc…
fglock Apr 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,24 @@ All reported regressions have been investigated. The issues fall into two catego

### How to Check Regressions

When a unit test fails on a feature branch, always verify whether it also fails on master before trying to fix it:

```bash
# 1. Save your work
git diff > /tmp/my-changes.patch

# 2. Switch to master and do a clean build
git checkout master
make clean ; make

# 3. If the test passes on master, it's a regression you introduced — fix it
# 4. If the test also fails on master, it's pre-existing — don't waste time on it

# 5. Switch back to your branch
git checkout feature/your-branch
git apply /tmp/my-changes.patch
```

```bash
# Run specific test
cd perl5_t/t && ../../jperl <test>.t
Expand Down
378 changes: 235 additions & 143 deletions dev/prompts/test-failures-not-quick-fix.md

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion dev/tools/perl_test_runner.pl
Original file line number Diff line number Diff line change
Expand Up @@ -242,14 +242,21 @@ sub run_single_test {
local $ENV{JPERL_UNIMPLEMENTED} = $test_file =~ m{
re/pat_rt_report.t
| re/pat.t
| re/pat_advanced.t
| re/regex_sets.t
| re/regexp_unicode_prop.t
| re/reg_eval_scope.t
| re/subst.t
| re/substT.t
| re/subst_wamp.t
| op/pack.t
| op/index.t
| op/split.t
| op/pos.t
| re/reg_pmod.t
| op/sprintf.t
| base/lex.t }x
| base/lex.t
| comp/parser.t }x
? "warn" : "";
local $ENV{JPERL_OPTS} = $test_file =~ m{
re/pat.t
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3424,6 +3424,21 @@ void compileVariableDeclaration(OperatorNode node, String op) {
lastResultReg = rd;
return;
}

// Handle: local $#array (without assignment)
if (sigil.equals("$#")) {
int arrayReg = CompileAssignment.resolveArrayForDollarHash(this, sigilOp);
// Save the array state so it's restored on scope exit
emit(Opcodes.PUSH_LOCAL_VARIABLE);
emitReg(arrayReg);
// Return the current array size as the result
int resultReg = allocateOutputRegister();
emit(Opcodes.ARRAY_SIZE);
emitReg(resultReg);
emitReg(arrayReg);
lastResultReg = resultReg;
return;
}
} else if (node.operand instanceof ListNode listNode) {
// local ($x, $y) - list of localized global variables
List<Integer> varRegs = new ArrayList<>();
Expand Down Expand Up @@ -3660,6 +3675,24 @@ void compileVariableDeclaration(OperatorNode node, String op) {
lastResultReg = rd;
return;
}
// local @{expr} / local %{expr} - localize a dynamic array/hash by name
// Implemented by localizing the typeglob (covers the array/hash slot)
if (node.operand instanceof OperatorNode sigilOp4
&& (sigilOp4.operator.equals("@") || sigilOp4.operator.equals("%"))
&& sigilOp4.operand instanceof BlockNode blockNode2) {
if (blockNode2.elements.size() == 1) {
compileNode(blockNode2.elements.getFirst(), -1, RuntimeContextType.SCALAR);
} else {
compileNode(blockNode2, -1, RuntimeContextType.SCALAR);
}
int nameReg = lastResultReg;
int rd = allocateOutputRegister();
emit(Opcodes.LOCAL_GLOB_DYNAMIC);
emitReg(rd);
emitReg(nameReg);
lastResultReg = rd;
return;
}
// General fallback for any lvalue expression (matches JVM backend behavior)
// Handles: local $hash{key}, local $array[index], local $obj->method->{key}, etc.
if (node.operand instanceof BinaryOperatorNode binOp) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,14 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c
pc = InlineOpcodeHandler.executeArrayDelete(bytecode, pc, registers);
}

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

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

case Opcodes.HASH_KEYS -> {
pc = InlineOpcodeHandler.executeHashKeys(bytecode, pc, registers);
}
Expand Down Expand Up @@ -1918,6 +1926,14 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c
pc = SlowOpcodeHandler.executeArraySliceDelete(bytecode, pc, registers);
}

case Opcodes.HASH_SLICE_DELETE_LOCAL -> {
pc = SlowOpcodeHandler.executeHashSliceDeleteLocal(bytecode, pc, registers);
}

case Opcodes.ARRAY_SLICE_DELETE_LOCAL -> {
pc = SlowOpcodeHandler.executeArraySliceDeleteLocal(bytecode, pc, registers);
}

case Opcodes.HASH_KV_SLICE_DELETE -> {
pc = SlowOpcodeHandler.executeHashKVSliceDelete(bytecode, pc, registers);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,22 @@ private static boolean handleLocalAssignment(BytecodeCompiler bc, BinaryOperator
&& innerSigilOp.operand instanceof IdentifierNode idNode) {
return handleLocalOurAssignment(bc, node, innerSigilOp, idNode, rhsContext);
}
// Handle: local $#array = value
if (sigil.equals("$#")) {
int arrayReg = resolveArrayForDollarHash(bc, sigilOp);
// Save the array state so it's restored on scope exit
bc.emit(Opcodes.PUSH_LOCAL_VARIABLE);
bc.emitReg(arrayReg);
// Compile the RHS value
bc.compileNode(node.right, -1, rhsContext);
int valueReg = bc.lastResultReg;
// Set $#array to the new value
bc.emit(Opcodes.SET_ARRAY_LAST_INDEX);
bc.emitReg(arrayReg);
bc.emitReg(valueReg);
bc.lastResultReg = valueReg;
return true;
}
}
if (localOperand instanceof ListNode listNode) {
return handleLocalListAssignment(bc, node, listNode, rhsContext);
Expand Down
178 changes: 178 additions & 0 deletions src/main/java/org/perlonjava/backend/bytecode/CompileExistsDelete.java
Original file line number Diff line number Diff line change
Expand Up @@ -506,4 +506,182 @@ private static int compileArrayIndex(BytecodeCompiler bc, BinaryOperatorNode arr
bc.compileNode(indexNode.elements.get(0), -1, RuntimeContextType.SCALAR);
return bc.lastResultReg;
}

/**
* Handles `delete local` in the bytecode interpreter.
* Mirrors visitDelete but uses HASH_DELETE_LOCAL / ARRAY_DELETE_LOCAL opcodes.
*/
public static void visitDeleteLocal(BytecodeCompiler bc, OperatorNode node) {
if (node.operand == null || !(node.operand instanceof ListNode list) || list.elements.isEmpty()) {
bc.throwCompilerException("delete local requires an argument");
return;
}
Node arg = list.elements.get(0);
if (arg instanceof BinaryOperatorNode binOp) {
switch (binOp.operator) {
case "{" -> visitDeleteLocalHash(bc, node, binOp);
case "[" -> visitDeleteLocalArray(bc, node, binOp);
case "->" -> visitDeleteLocalArrow(bc, node, binOp);
default -> bc.throwCompilerException("delete local requires hash or array element");
}
} else {
bc.throwCompilerException("delete local requires hash or array element");
}
}

private static void visitDeleteLocalHash(BytecodeCompiler bc, OperatorNode node, BinaryOperatorNode hashAccess) {
if (hashAccess.left instanceof OperatorNode leftOp && leftOp.operator.equals("@")) {
visitDeleteLocalHashSlice(bc, node, hashAccess, leftOp);
return;
}
int hashReg = resolveHashFromBinaryOp(bc, hashAccess, node.getIndex());
int keyReg = compileHashKey(bc, hashAccess.right);
int rd = bc.allocateOutputRegister();
bc.emit(Opcodes.HASH_DELETE_LOCAL);
bc.emitReg(rd);
bc.emitReg(hashReg);
bc.emitReg(keyReg);
bc.lastResultReg = rd;
}

private static void visitDeleteLocalHashSlice(BytecodeCompiler bc, OperatorNode node, BinaryOperatorNode hashAccess, OperatorNode leftOp) {
int hashReg;
if (leftOp.operand instanceof IdentifierNode id) {
String hashVarName = "%" + id.name;
if (bc.hasVariable(hashVarName)) {
hashReg = bc.getVariableRegister(hashVarName);
} else {
hashReg = bc.allocateRegister();
String globalHashName = NameNormalizer.normalizeVariableName(id.name, bc.getCurrentPackage());
int nameIdx = bc.addToStringPool(globalHashName);
bc.emit(Opcodes.LOAD_GLOBAL_HASH);
bc.emitReg(hashReg);
bc.emit(nameIdx);
}
} else {
bc.throwCompilerException("Hash slice delete local requires identifier");
return;
}
if (!(hashAccess.right instanceof HashLiteralNode keysNode)) {
bc.throwCompilerException("Hash slice delete local requires HashLiteralNode");
return;
}
List<Integer> keyRegs = new ArrayList<>();
for (Node keyElement : keysNode.elements) {
if (keyElement instanceof IdentifierNode keyId) {
int keyReg = bc.allocateRegister();
int keyIdx = bc.addToStringPool(keyId.name);
bc.emit(Opcodes.LOAD_STRING);
bc.emitReg(keyReg);
bc.emit(keyIdx);
keyRegs.add(keyReg);
} else {
bc.compileNode(keyElement, -1, RuntimeContextType.SCALAR);
keyRegs.add(bc.lastResultReg);
}
}
int keysListReg = bc.allocateRegister();
bc.emit(Opcodes.CREATE_LIST);
bc.emitReg(keysListReg);
bc.emit(keyRegs.size());
for (int keyReg : keyRegs) {
bc.emitReg(keyReg);
}
int rd = bc.allocateOutputRegister();
bc.emit(Opcodes.HASH_SLICE_DELETE_LOCAL);
bc.emitReg(rd);
bc.emitReg(hashReg);
bc.emitReg(keysListReg);
bc.lastResultReg = rd;
}

private static void visitDeleteLocalArray(BytecodeCompiler bc, OperatorNode node, BinaryOperatorNode arrayAccess) {
if (arrayAccess.left instanceof OperatorNode leftOp && leftOp.operator.equals("@")) {
visitDeleteLocalArraySlice(bc, node, arrayAccess, leftOp);
return;
}
int arrayReg = compileArrayForExistsDelete(bc, arrayAccess, node.getIndex());
int indexReg = compileArrayIndex(bc, arrayAccess);
int rd = bc.allocateOutputRegister();
bc.emit(Opcodes.ARRAY_DELETE_LOCAL);
bc.emitReg(rd);
bc.emitReg(arrayReg);
bc.emitReg(indexReg);
bc.lastResultReg = rd;
}

private static void visitDeleteLocalArraySlice(BytecodeCompiler bc, OperatorNode node, BinaryOperatorNode arrayAccess, OperatorNode leftOp) {
int arrayReg;
if (leftOp.operand instanceof IdentifierNode id) {
String arrayVarName = "@" + id.name;
if (bc.hasVariable(arrayVarName)) {
arrayReg = bc.getVariableRegister(arrayVarName);
} else {
arrayReg = bc.allocateRegister();
String globalArrayName = NameNormalizer.normalizeVariableName(id.name, bc.getCurrentPackage());
int nameIdx = bc.addToStringPool(globalArrayName);
bc.emit(Opcodes.LOAD_GLOBAL_ARRAY);
bc.emitReg(arrayReg);
bc.emit(nameIdx);
}
} else {
bc.throwCompilerException("Array slice delete local requires identifier");
return;
}
if (!(arrayAccess.right instanceof ArrayLiteralNode indicesNode)) {
bc.throwCompilerException("Array slice delete local requires ArrayLiteralNode");
return;
}
List<Integer> indexRegs = new ArrayList<>();
for (Node indexElement : indicesNode.elements) {
bc.compileNode(indexElement, -1, RuntimeContextType.SCALAR);
indexRegs.add(bc.lastResultReg);
}
int indicesListReg = bc.allocateRegister();
bc.emit(Opcodes.CREATE_LIST);
bc.emitReg(indicesListReg);
bc.emit(indexRegs.size());
for (int indexReg : indexRegs) {
bc.emitReg(indexReg);
}
int rd = bc.allocateOutputRegister();
bc.emit(Opcodes.ARRAY_SLICE_DELETE_LOCAL);
bc.emitReg(rd);
bc.emitReg(arrayReg);
bc.emitReg(indicesListReg);
bc.lastResultReg = rd;
}

private static void visitDeleteLocalArrow(BytecodeCompiler bc, OperatorNode node, BinaryOperatorNode arrowAccess) {
if (arrowAccess.right instanceof HashLiteralNode) {
bc.compileNode(arrowAccess.left, -1, RuntimeContextType.SCALAR);
int refReg = bc.lastResultReg;
int hashReg = derefHash(bc, refReg, node.getIndex());
int keyReg = compileHashKey(bc, arrowAccess.right);
int rd = bc.allocateOutputRegister();
bc.emit(Opcodes.HASH_DELETE_LOCAL);
bc.emitReg(rd);
bc.emitReg(hashReg);
bc.emitReg(keyReg);
bc.lastResultReg = rd;
} else if (arrowAccess.right instanceof ArrayLiteralNode indexNode) {
bc.compileNode(arrowAccess.left, -1, RuntimeContextType.SCALAR);
int refReg = bc.lastResultReg;
int arrayReg = derefArray(bc, refReg, node.getIndex());
if (indexNode.elements.isEmpty()) {
bc.throwCompilerException("Array index required for delete local");
return;
}
bc.compileNode(indexNode.elements.get(0), -1, RuntimeContextType.SCALAR);
int indexReg = bc.lastResultReg;
int rd = bc.allocateOutputRegister();
bc.emit(Opcodes.ARRAY_DELETE_LOCAL);
bc.emitReg(rd);
bc.emitReg(arrayReg);
bc.emitReg(indexReg);
bc.lastResultReg = rd;
} else {
bc.throwCompilerException("delete local requires hash or array element");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,7 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode
case "sprintf" -> visitSprintf(bytecodeCompiler, node);
case "exists" -> CompileExistsDelete.visitExists(bytecodeCompiler, node);
case "delete" -> CompileExistsDelete.visitDelete(bytecodeCompiler, node);
case "delete_local" -> CompileExistsDelete.visitDeleteLocal(bytecodeCompiler, node);
case "die", "warn" -> visitDieWarn(bytecodeCompiler, node, op);

// Pop/shift
Expand Down Expand Up @@ -1076,7 +1077,7 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode
int rd = bytecodeCompiler.allocateOutputRegister();
boolean hasArgs = node.operand instanceof ListNode ln && !ln.elements.isEmpty();
if (hasArgs) {
node.operand.accept(bytecodeCompiler);
bytecodeCompiler.compileNode(node.operand, -1, RuntimeContextType.LIST);
int listReg = bytecodeCompiler.lastResultReg;
bytecodeCompiler.emitWithToken(Opcodes.SELECT, node.getIndex());
bytecodeCompiler.emitReg(rd);
Expand Down
28 changes: 28 additions & 0 deletions src/main/java/org/perlonjava/backend/bytecode/Disassemble.java
Original file line number Diff line number Diff line change
Expand Up @@ -852,6 +852,34 @@ public static String disassemble(InterpretedCode interpretedCode) {
int idxDeleteReg = interpretedCode.bytecode[pc++];
sb.append("ARRAY_DELETE r").append(rd).append(" = delete r").append(arrDeleteReg).append("[r").append(idxDeleteReg).append("]\n");
break;
case Opcodes.HASH_DELETE_LOCAL:
rd = interpretedCode.bytecode[pc++];
int hashDLReg = interpretedCode.bytecode[pc++];
int keyDLReg = interpretedCode.bytecode[pc++];
sb.append("HASH_DELETE_LOCAL r").append(rd).append(" = delete local r").append(hashDLReg).append("{r").append(keyDLReg).append("}\n");
break;
case Opcodes.ARRAY_DELETE_LOCAL:
rd = interpretedCode.bytecode[pc++];
int arrDLReg = interpretedCode.bytecode[pc++];
int idxDLReg = interpretedCode.bytecode[pc++];
sb.append("ARRAY_DELETE_LOCAL r").append(rd).append(" = delete local r").append(arrDLReg).append("[r").append(idxDLReg).append("]\n");
break;
case Opcodes.HASH_SLICE_DELETE_LOCAL: {
rd = interpretedCode.bytecode[pc++];
int hsdlHashReg = interpretedCode.bytecode[pc++];
int hsdlKeysReg = interpretedCode.bytecode[pc++];
sb.append("HASH_SLICE_DELETE_LOCAL r").append(rd).append(" = delete local r").append(hsdlHashReg)
.append("{r").append(hsdlKeysReg).append("}\n");
break;
}
case Opcodes.ARRAY_SLICE_DELETE_LOCAL: {
rd = interpretedCode.bytecode[pc++];
int asdlArrayReg = interpretedCode.bytecode[pc++];
int asdlIndicesReg = interpretedCode.bytecode[pc++];
sb.append("ARRAY_SLICE_DELETE_LOCAL r").append(rd).append(" = delete local r").append(asdlArrayReg)
.append("[r").append(asdlIndicesReg).append("]\n");
break;
}
case Opcodes.HASH_KEYS:
rd = interpretedCode.bytecode[pc++];
int hashKeysReg = interpretedCode.bytecode[pc++];
Expand Down
Loading
Loading