From 9aee6f96d0a6e952c964a94f7ef5aeb661d3df8b Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Wed, 1 Apr 2026 09:48:08 +0200 Subject: [PATCH 01/19] Fix fileno/stat/switches/regex quick wins for test pass rate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fileno(): return undef for closed/null ioHandle instead of NPE (fh.t: 0→6/8) - stat()/lstat(): null guard after resolvePath() for NUL-in-filename (filetest.t: 227→231/436) - ArgumentParser: uncomment System.exit(1) for unrecognized switches (switches.t) - RuntimeRegex: preserve $1 across failed matches per Perl 5 semantics (universal.t: 90→122/142) Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- dev/design/test_pass_rate_improvement_plan.md | 30 +++++++++++++++---- .../perlonjava/app/cli/ArgumentParser.java | 4 +-- .../org/perlonjava/core/Configuration.java | 2 +- .../runtime/operators/IOOperator.java | 2 +- .../perlonjava/runtime/operators/Stat.java | 10 +++++++ .../runtime/regex/RuntimeRegex.java | 4 +-- .../runtime/runtimetypes/RuntimeIO.java | 3 ++ 7 files changed, 42 insertions(+), 13 deletions(-) diff --git a/dev/design/test_pass_rate_improvement_plan.md b/dev/design/test_pass_rate_improvement_plan.md index 00e5cc812..a3e2104b4 100644 --- a/dev/design/test_pass_rate_improvement_plan.md +++ b/dev/design/test_pass_rate_improvement_plan.md @@ -248,14 +248,14 @@ These are small, targeted fixes that each unblock many tests: | # | Fix | Tests Gained | Effort | |---|-----|-------------|--------| -| 1.1 | Register `experimental::smartmatch` warnings category | ~1,065 (taint.t) | Tiny | -| 1.2 | Null-check in `fileno()` for unopened handles | 8 (fh.t) | Tiny | -| 1.3 | Null-guard in file test operator for NUL-in-filename | 5 (filetest.t) | Tiny | +| 1.1 | ~~Register `experimental::smartmatch` warnings~~ (already done; taint.t blocked by missing taint mode) | ~~1,065~~ 0 | N/A | +| 1.2 | Null-check in `fileno()` for unopened handles | ~~8~~ **done** (6 gained) | Tiny | +| 1.3 | Null-guard in `stat()`/`lstat()` for NUL-in-filename | ~~5~~ **done** (4 gained) | Tiny | | 1.4 | `@UNIVERSAL::ISA` traversal in MRO | 156 (ref.t) | Small | | 1.5 | Fix dynamic `goto $variable` + state var persistence | ~~101~~ **done** (72 gained) | Small | | 1.6 | Extend `vec()` to 64-bit widths | 28 (vec.t) | Small | -| 1.7 | Fix `$1` capture after successful match (state corruption) | 38 (universal.t) | Small | -| 1.8 | Fix unrecognized-switch error message (add trailing `.`) | ~56 (switches.t) | Tiny | +| 1.7 | Fix `$1` persistence across failed regex matches | ~~38~~ **done** (32 gained) | Small | +| 1.8 | Fix unrecognized-switch `System.exit` + exit code | ~~56~~ **done** (est. 33 gained) | Tiny | | 1.9 | Fix op/tie_fetch_count.t parse error | ~343 | Small | | 1.10 | Fix op/sort.t prototype argument handling | ~206 | Small | @@ -357,10 +357,28 @@ Remaining op/state.t failures (15 + 14 blocked): ### Next Steps -1. Continue Phase 1 quick wins (items 1.1-1.10) +1. Continue Phase 1 quick wins (items 1.4, 1.6, 1.9, 1.10) 2. Investigate op/state.t goto+state interaction failures in full test context 3. Run full test suite to measure overall progress +#### Quick win batch 2 (2025-03-31) + +Branch: `fix/state-attribute-validation` + +| Fix | Tests Gained | Category | +|-----|-------------|----------| +| `fileno()` null-check for unopened/closed handles | +6 (fh.t: 0/8→6/8) | 1.2 | +| `stat()`/`lstat()` null-guard for NUL-in-filename | +4 (filetest.t: 227/436→231/436) | 1.3 | +| Unrecognized switch `System.exit(1)` (was commented out) | est. +33 (switches.t) | 1.8 | +| `$1` persistence across failed regex matches | +32 (universal.t: 90/142→122/142) | 1.7 | + +Files changed: +- `IOOperator.java` — fileno() returns undef for closed/null ioHandle +- `RuntimeIO.java` — fileno() null guard on ioHandle +- `Stat.java` — null check after resolvePath() in stat()/lstat() +- `ArgumentParser.java` — uncommented System.exit(1) for unrecognized switches +- `RuntimeRegex.java` — don't clear $1 on failed match + ### Baseline Update this document as fixes land. Use the test runner to measure progress: diff --git a/src/main/java/org/perlonjava/app/cli/ArgumentParser.java b/src/main/java/org/perlonjava/app/cli/ArgumentParser.java index ba94ddd38..96f97dec2 100644 --- a/src/main/java/org/perlonjava/app/cli/ArgumentParser.java +++ b/src/main/java/org/perlonjava/app/cli/ArgumentParser.java @@ -448,7 +448,7 @@ private static int processClusteredSwitches(String[] args, CompilerOptions parse return index; default: System.err.println("Unrecognized switch: -" + switchChar + " (-h will show valid options)."); - // System.exit(0); + System.exit(1); break; } } @@ -983,7 +983,7 @@ private static int processLongSwitches(String[] args, CompilerOptions parsedArgs break; default: System.err.println("Unrecognized switch: " + arg + " (-h will show valid options)."); - System.exit(0); + System.exit(1); break; } return index; diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 7f337cb9f..18a8ad16e 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "9cd24576f"; + public static final String gitCommitId = "d246cf22e"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). diff --git a/src/main/java/org/perlonjava/runtime/operators/IOOperator.java b/src/main/java/org/perlonjava/runtime/operators/IOOperator.java index b22fd7b94..34a26d4c0 100644 --- a/src/main/java/org/perlonjava/runtime/operators/IOOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/IOOperator.java @@ -189,7 +189,7 @@ public static RuntimeScalar fileno(int ctx, RuntimeBase... args) { return TieHandle.tiedFileno(tieHandle); } - if (fh == null) { + if (fh == null || fh.ioHandle == null || fh.ioHandle instanceof ClosedIOHandle) { return RuntimeScalarCache.scalarUndef; } diff --git a/src/main/java/org/perlonjava/runtime/operators/Stat.java b/src/main/java/org/perlonjava/runtime/operators/Stat.java index 7e4f50e3a..e1e1cceed 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Stat.java +++ b/src/main/java/org/perlonjava/runtime/operators/Stat.java @@ -172,6 +172,11 @@ public static RuntimeList stat(RuntimeScalar arg) { String filename = arg.toString(); try { Path path = resolvePath(filename); + if (path == null) { + getGlobalVariable("main::!").set(2); + updateLastStat(arg, false, 2, false); + return res; + } NativeStatFields nf = nativeStat(path.toString(), true); @@ -232,6 +237,11 @@ public static RuntimeList lstat(RuntimeScalar arg) { String filename = arg.toString(); try { Path path = resolvePath(filename); + if (path == null) { + getGlobalVariable("main::!").set(2); + updateLastStat(arg, false, 2, true); + return res; + } NativeStatFields nf = nativeStat(path.toString(), false); diff --git a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java index c4bc80121..5b918b5be 100644 --- a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java +++ b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java @@ -728,9 +728,7 @@ private static RuntimeBase matchRegexDirect(RuntimeScalar quotedRegex, RuntimeSc lastMatchedString = null; lastMatchStart = -1; lastMatchEnd = -1; - if (matcher.groupCount() > 0) { - lastCaptureGroups = null; - } + // Don't clear lastCaptureGroups - Perl preserves $1 across failed matches } if (found) { diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java index dd3cf992b..b20b12ce0 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java @@ -1219,6 +1219,9 @@ public RuntimeScalar write(String data) { * @return RuntimeScalar with the file descriptor number, or undef if not available */ public RuntimeScalar fileno() { + if (ioHandle == null) { + return RuntimeScalarCache.scalarUndef; + } return ioHandle.fileno(); } From eabbf864222d6e22418d1f2edc9a46e1ae723ce3 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Wed, 1 Apr 2026 09:55:31 +0200 Subject: [PATCH 02/19] =?UTF-8?q?Fix=20vec()=20error=20messages=20and=20un?= =?UTF-8?q?signed=2032-bit=20read=20(vec.t:=2037=E2=86=9274/78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove 'Invalid vec operation: ' prefix from error messages in RuntimeVecLvalue - Fix unsigned 32-bit vec read using Integer.toUnsignedLong() - Reorder vec read to check 64-bit before 32-bit Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../org/perlonjava/core/Configuration.java | 4 ++-- .../org/perlonjava/runtime/operators/Vec.java | 22 ++++++++++--------- .../runtimetypes/RuntimeVecLvalue.java | 2 +- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 18a8ad16e..29b6c121a 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,14 +33,14 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "d246cf22e"; + public static final String gitCommitId = "9aee6f96d"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitDate = "2026-03-31"; + public static final String gitCommitDate = "2026-04-01"; // Prevent instantiation private Configuration() { diff --git a/src/main/java/org/perlonjava/runtime/operators/Vec.java b/src/main/java/org/perlonjava/runtime/operators/Vec.java index dff19da3c..4958c4395 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Vec.java +++ b/src/main/java/org/perlonjava/runtime/operators/Vec.java @@ -66,18 +66,21 @@ public static RuntimeScalar vec(RuntimeList args) throws PerlCompilerException { int bitOffset = (offset * bits) % 8; ByteBuffer buffer = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN); - int value = 0; - if (bits == 32 && byteOffset + 4 <= data.length) { - value = buffer.getInt(byteOffset); - } else if (bits == 16 && byteOffset + 2 <= data.length) { - value = buffer.getShort(byteOffset) & 0xFFFF; - } else if (bits == 8 && byteOffset < data.length) { - value = buffer.get(byteOffset) & 0xFF; - } else if (bits == 64 && byteOffset + 8 <= data.length) { + if (bits == 64 && byteOffset + 8 <= data.length) { long longValue = buffer.getLong(byteOffset); return new RuntimeVecLvalue(strScalar, offset, bits, longValue); + } else if (bits == 32 && byteOffset + 4 <= data.length) { + long unsignedValue = Integer.toUnsignedLong(buffer.getInt(byteOffset)); + return new RuntimeVecLvalue(strScalar, offset, bits, unsignedValue); + } else if (bits == 16 && byteOffset + 2 <= data.length) { + int value = buffer.getShort(byteOffset) & 0xFFFF; + return new RuntimeVecLvalue(strScalar, offset, bits, value); + } else if (bits == 8 && byteOffset < data.length) { + int value = buffer.get(byteOffset) & 0xFF; + return new RuntimeVecLvalue(strScalar, offset, bits, value); } else { + int value = 0; for (int i = 0; i < bits; i++) { int byteIndex = byteOffset + (bitOffset + i) / 8; int bitIndex = (bitOffset + i) % 8; @@ -85,9 +88,8 @@ public static RuntimeScalar vec(RuntimeList args) throws PerlCompilerException { value |= ((data[byteIndex] >> bitIndex) & 1) << i; } } + return new RuntimeVecLvalue(strScalar, offset, bits, value); } - - return new RuntimeVecLvalue(strScalar, offset, bits, value); } /** diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeVecLvalue.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeVecLvalue.java index a5077d7fb..54879e96b 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeVecLvalue.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeVecLvalue.java @@ -85,7 +85,7 @@ public RuntimeScalar set(RuntimeScalar value) { Vec.set(args, new RuntimeScalar(newValue)); } } catch (PerlCompilerException e) { - throw new RuntimeException("Invalid vec operation: " + e.getMessage()); + throw new RuntimeException(e.getMessage()); } return this; From e0745fc447eb8afdcd1fb8d20596cc76f281f96a Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Wed, 1 Apr 2026 10:00:58 +0200 Subject: [PATCH 03/19] Implement (*pla:...) (*plb:...) (*nla:...) (*nlb:...) (*atomic:...) regex aliases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Map Perl 5.28+ alpha assertion aliases to their Java regex equivalents: - (*pla:...) / (*positive_lookahead:...) -> (?=...) - (*plb:...) / (*positive_lookbehind:...) -> (?<=...) - (*nla:...) / (*negative_lookahead:...) -> (?!...) - (*nlb:...) / (*negative_lookbehind:...) -> (? (?>...) alpha_assertions.t: 2100→2188/2320 (+88 tests) Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../org/perlonjava/core/Configuration.java | 2 +- .../runtime/regex/RegexPreprocessor.java | 65 +++++++++++++------ 2 files changed, 45 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 29b6c121a..0509a9639 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "9aee6f96d"; + public static final String gitCommitId = "eabbf8642"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). diff --git a/src/main/java/org/perlonjava/runtime/regex/RegexPreprocessor.java b/src/main/java/org/perlonjava/runtime/regex/RegexPreprocessor.java index 1244badf4..7baf2e2bf 100644 --- a/src/main/java/org/perlonjava/runtime/regex/RegexPreprocessor.java +++ b/src/main/java/org/perlonjava/runtime/regex/RegexPreprocessor.java @@ -944,31 +944,54 @@ private static int handleParentheses(String s, int offset, int length, StringBui // Check for (*...) verb patterns FIRST, before checking (? if (c2 == '*') { // (*...) control verbs like (*ACCEPT), (*FAIL), (*COMMIT), etc. - // These are Perl-specific and not supported by Java regex - - // Find the end of the verb - int verbEnd = offset + 2; - while (verbEnd < length && s.codePointAt(verbEnd) != ')') { - verbEnd++; - } - if (verbEnd < length) { - verbEnd++; // Include the closing paren - } - - // Extract the verb name for error reporting - String verb = s.substring(offset, Math.min(verbEnd, length)); + // Also handles alpha assertion aliases: (*pla:...), (*plb:...), etc. + + // Find the verb name (up to ':' or ')') + int verbNameEnd = offset + 2; + while (verbNameEnd < length) { + int cp = s.codePointAt(verbNameEnd); + if (cp == ':' || cp == ')') break; + verbNameEnd++; + } + String verbName = s.substring(offset + 2, verbNameEnd); + + // Check for alpha assertion aliases (Perl 5.28+) + String replacement = switch (verbName) { + case "pla", "positive_lookahead" -> "(?="; + case "plb", "positive_lookbehind" -> "(?<="; + case "nla", "negative_lookahead" -> "(?!"; + case "nlb", "negative_lookbehind" -> "(? "(?>"; + default -> null; + }; + + if (replacement != null && verbNameEnd < length && s.codePointAt(verbNameEnd) == ':') { + // Alpha assertion with content: (*pla:...) -> (?=...) + sb.append(replacement); + offset = handleRegex(s, verbNameEnd + 1, sb, regexFlags, true); + // Fall through to common ')' handling at end of handleParentheses + } else { + // Find the end of the verb for error reporting + int verbEnd = offset + 2; + while (verbEnd < length && s.codePointAt(verbEnd) != ')') { + verbEnd++; + } + if (verbEnd < length) { + verbEnd++; // Include the closing paren + } - // Replace with empty non-capturing group as placeholder - sb.append("(?:)"); + // Extract the verb name for error reporting + String verb = s.substring(offset, Math.min(verbEnd, length)); - // Throw error that can be caught by JPERL_UNIMPLEMENTED=warn - regexError(s, offset + 2, "Regex control verb " + verb + " not implemented"); + // Replace with empty non-capturing group as placeholder + sb.append("(?:)"); - return verbEnd; // Skip past the entire verb construct - } + // Throw error that can be caught by JPERL_UNIMPLEMENTED=warn + regexError(s, offset + 2, "Regex control verb " + verb + " not implemented"); - // Handle (? - if (c2 == '?') { + return verbEnd; // Skip past the entire verb construct + } + } else if (c2 == '?') { if (offset + 2 >= length) { // Marker should be after the ? regexError(s, offset + 2, "Sequence (? incomplete"); From 2e9957a26d217f803020d3f3b904e4f4fa636935 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Wed, 1 Apr 2026 10:16:30 +0200 Subject: [PATCH 04/19] Update test improvement plan with batch 2-3 results and investigation notes - Document batch 2 fixes (fileno, stat, switches, regex $1) - Document batch 3 fixes (vec, alpha assertions) - Add investigation notes for op/method.t indirect method parsing - Add investigation notes for op/sort.t $$ prototype comparators - Update next steps and corrected assessments (taint.t, sort.t, ref.t) Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- dev/design/test_pass_rate_improvement_plan.md | 61 ++++++++++++++++++- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/dev/design/test_pass_rate_improvement_plan.md b/dev/design/test_pass_rate_improvement_plan.md index a3e2104b4..c7b89740b 100644 --- a/dev/design/test_pass_rate_improvement_plan.md +++ b/dev/design/test_pass_rate_improvement_plan.md @@ -357,13 +357,13 @@ Remaining op/state.t failures (15 + 14 blocked): ### Next Steps -1. Continue Phase 1 quick wins (items 1.4, 1.6, 1.9, 1.10) -2. Investigate op/state.t goto+state interaction failures in full test context +1. Fix op/method.t indirect method call parsing with `()` arguments (see investigation notes below) +2. Fix op/sort.t `$$`-prototyped comparators (pass args via `@_`) 3. Run full test suite to measure overall progress #### Quick win batch 2 (2025-03-31) -Branch: `fix/state-attribute-validation` +Branch: `fix/test-pass-rate-quick-wins` | Fix | Tests Gained | Category | |-----|-------------|----------| @@ -379,6 +379,61 @@ Files changed: - `ArgumentParser.java` — uncommented System.exit(1) for unrecognized switches - `RuntimeRegex.java` — don't clear $1 on failed match +#### Quick win batch 3 (2025-03-31) + +Branch: `fix/test-pass-rate-quick-wins` + +| Fix | Tests Gained | Category | +|-----|-------------|----------| +| `vec()` error message prefix removed + unsigned 32-bit read | +37 (vec.t: 37/78→74/78) | 1.6 | +| `(*pla:...)` `(*plb:...)` `(*nla:...)` `(*nlb:...)` `(*atomic:...)` regex aliases | +88 (alpha_assertions.t: 2100→2188/2320) | 2.9 | + +Files changed: +- `Vec.java` — reorder 64-bit before 32-bit, use `Integer.toUnsignedLong()` for unsigned 32-bit +- `RuntimeVecLvalue.java` — remove "Invalid vec operation: " error prefix +- `RegexPreprocessor.java` — map alpha assertion aliases to Java regex equivalents + +#### Investigation notes: op/method.t indirect method call (item 2.6) + +**Status:** Partially investigated. Package detection fixed but arg parsing still fails. + +The parse error at line 59 (`is(method Pack ("a","b","c"), ...)`) has **two layers**: + +1. **Package detection** (fixed): When `Pack` is defined via `sub Pack::method { ... }` (without + an explicit `package Pack;` statement), `packageExistsCache` has no entry for `Pack`. Added + `isPackageLoaded()` fallback in `SubroutineParser.java:208` — this correctly detects `Pack` + as a package. However, this fix alone is insufficient. + +2. **Argument parsing after indirect method + `(`** (NOT fixed): Even when `Pack` is correctly + identified as a package, `method Pack ("a","b","c")` fails because `consumeArgsWithPrototype(parser, "@")` + at `SubroutineParser.java:247` doesn't handle parenthesized argument lists in the indirect + method context. The `(` after `Pack` causes the parser to either: + - Treat `Pack(...)` as a function call (backtracking path), OR + - Enter `consumeArgsWithPrototype` which misparses the `(...)` args + + **Confirmed with system Perl:** Both `method Pack "a"` and `method Pack ("a")` are valid + indirect method call syntax in Perl 5. PerlOnJava fails on both forms when arguments follow. + + **Root cause:** The indirect method argument consumer needs special handling when the first + token is `(` — it should parse as method arguments `Pack->method("a","b","c")`, not as + `Pack("a","b","c")` being passed to `method`. + + **Scope:** Fixing this would unblock ~163 tests in op/method.t. The fix requires changes to + the argument parsing in `SubroutineParser.java` lines 240-260, specifically how + `consumeArgsWithPrototype` interacts with `(` after the package name. + +#### Investigation notes: op/sort.t (item 1.10) + +**Status:** sort.t now runs (206 planned tests emit TAP). It's NOT a 0/0 crash — the plan doc +was outdated. Actual status needs measurement. Key issues found: + +1. **`$$`-prototyped comparators** don't receive args via `@_`. In Perl 5, `sort sub_with_$$_proto @list` + passes elements as `$_[0]`/`$_[1]` instead of `$a`/`$b`. Fix needed in `ListOperators.sort()` + (lines 86-138): detect `$$` prototype and populate `comparatorArgs`. + +2. **`sort CORE::reverse LIST`** is misparsed — `CORE::reverse` is treated as a comparator name + instead of a function applied to the list. Fix needed in `ParseMapGrepSort.parseSort()`. + ### Baseline Update this document as fixes land. Use the test runner to measure progress: From 548d2ed4b3ade6742181eb884cba18051de1a22b Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Wed, 1 Apr 2026 10:17:35 +0200 Subject: [PATCH 05/19] Support $$ prototype in sort comparators (pass args via @_) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In Perl 5, sort comparators with $$ prototype receive elements as $_[0]/$_[1] instead of $a/$b. Detect prototype and populate @_ accordingly. (sort.t: 23→25/206) Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../java/org/perlonjava/core/Configuration.java | 2 +- .../runtime/operators/ListOperators.java | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 0509a9639..0c7177edc 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "eabbf8642"; + public static final String gitCommitId = "2e9957a26"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). diff --git a/src/main/java/org/perlonjava/runtime/operators/ListOperators.java b/src/main/java/org/perlonjava/runtime/operators/ListOperators.java index 2cf89ad70..e5e947d31 100644 --- a/src/main/java/org/perlonjava/runtime/operators/ListOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/ListOperators.java @@ -103,7 +103,13 @@ public static RuntimeList sort(RuntimeList runtimeList, RuntimeScalar perlCompar } final RuntimeScalar finalComparator = comparator; - RuntimeArray comparatorArgs = new RuntimeArray(); + // Check if comparator has $$ prototype (stacked comparator) + // In Perl 5, $$-prototyped sort subs receive elements via @_ instead of $a/$b + boolean isStacked = false; + if (finalComparator.value instanceof RuntimeCode runtimeCode) { + isStacked = "$$".equals(runtimeCode.prototype); + } + final boolean stackedComparator = isStacked; // Create the sort variables RuntimeScalar varA = getGlobalVariable(packageName + "::a"); @@ -116,6 +122,13 @@ public static RuntimeList sort(RuntimeList runtimeList, RuntimeScalar perlCompar varA.set(a); varB.set(b); + // For $$-prototyped comparators, pass elements via @_ + RuntimeArray comparatorArgs = new RuntimeArray(); + if (stackedComparator) { + comparatorArgs.push(a); + comparatorArgs.push(b); + } + // Apply the Perl comparator subroutine with the arguments RuntimeList result = RuntimeCode.apply(finalComparator, comparatorArgs, RuntimeContextType.SCALAR); From 825777091f129fc5fa6d04197cbeffe1c86ae810 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Wed, 1 Apr 2026 10:22:20 +0200 Subject: [PATCH 06/19] Handle TIED_SCALAR in typeglob assignment (fetch before dispatch) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When assigning a tied scalar to a glob (*foo = $tied), call tiedFetch() to get the underlying value before dispatching on type. Fixes "typeglob assignment not implemented for 9" crash. (tie_fetch_count.t: 1/343 → 64/343) Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- src/main/java/org/perlonjava/core/Configuration.java | 2 +- .../java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java | 2 ++ .../org/perlonjava/runtime/runtimetypes/RuntimeStashEntry.java | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 0c7177edc..49f326a8e 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "2e9957a26"; + public static final String gitCommitId = "548d2ed4b"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java index a14a89dc5..8e69f1fe0 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java @@ -166,6 +166,8 @@ public RuntimeScalar set(RuntimeScalar value) { markGlobAsAssigned(); switch (value.type) { + case TIED_SCALAR: + return set(value.tiedFetch()); case CODE: GlobalVariable.getGlobalCodeRef(this.globName).set(value); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeStashEntry.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeStashEntry.java index 2ef179c70..45b6de61a 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeStashEntry.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeStashEntry.java @@ -127,6 +127,8 @@ public RuntimeScalar set(RuntimeScalar value) { switch (value.type) { + case TIED_SCALAR: + return set(value.tiedFetch()); case CODE: GlobalVariable.getGlobalCodeRef(this.globName).set(value); From 0df8f88a94d3d94fd66a83be4ead2d045fc5cad7 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Wed, 1 Apr 2026 10:27:40 +0200 Subject: [PATCH 07/19] Allow argless goto (runtime error instead of compile error) Perl 5 allows goto without arguments - it throws 'goto must have label' at runtime. Previously PerlOnJava rejected this at parse time. Now the parser accepts it, the interpreter GOTO_DYNAMIC handles the empty-string label, and the JVM backend falls back to interpreter. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../backend/bytecode/BytecodeInterpreter.java | 4 ++++ .../backend/bytecode/CompileOperator.java | 13 ++++++++++++- .../org/perlonjava/backend/jvm/EmitControlFlow.java | 5 ++++- .../java/org/perlonjava/core/Configuration.java | 2 +- .../perlonjava/frontend/parser/OperatorParser.java | 4 ++-- 5 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 4f7023766..337d2b421 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -191,6 +191,10 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c return marker; } String labelName = target.toString(); + if (labelName.isEmpty()) { + // Bare `goto` without label - runtime error like Perl 5 + throw new PerlCompilerException("goto must have label"); + } if (code.gotoLabelPcs != null) { Integer targetPc = code.gotoLabelPcs.get(labelName); if (targetPc != null) { diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index 7e43c3b24..b1462b1b0 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -1436,7 +1436,18 @@ private static void visitGoto(BytecodeCompiler bc, OperatorNode node) { return; } } - if (labelStr == null) bc.throwCompilerException("goto must be given label"); + if (labelStr == null) { + // Bare `goto` without args: emit GOTO_DYNAMIC with empty string → runtime error + int rd = bc.allocateOutputRegister(); + int emptyIdx = bc.addToStringPool(""); + bc.emit(Opcodes.LOAD_STRING); + bc.emitReg(rd); + bc.emit(emptyIdx); + bc.emit(Opcodes.GOTO_DYNAMIC); + bc.emit(rd); + bc.lastResultReg = -1; + return; + } Integer targetPc = bc.gotoLabelPcs.get(labelStr); if (targetPc != null) { bc.emit(Opcodes.GOTO); bc.emitInt(targetPc); } else { bc.emit(Opcodes.GOTO); int patchPc = bc.bytecode.size(); bc.emitInt(0); bc.pendingGotos.add(new Object[]{patchPc, labelStr}); } diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java b/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java index 42b201648..35edecb14 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java @@ -495,7 +495,10 @@ static void handleGotoLabel(EmitterVisitor emitterVisitor, OperatorNode node) { // Ensure label is provided for static goto if (labelName == null) { - throw new PerlCompilerException(node.tokenIndex, "goto must be given label", ctx.errorUtil); + // Bare `goto` without arguments - emit runtime die like Perl 5 + // Fall through to interpreter which handles this properly via GOTO_DYNAMIC + throw new PerlCompilerException(node.tokenIndex, + "Dynamic goto EXPR requires interpreter fallback", ctx.errorUtil); } // For static label, check if it's local diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 49f326a8e..7622d3ec4 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "548d2ed4b"; + public static final String gitCommitId = "825777091"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). diff --git a/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java b/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java index bf382d8ec..2765ad74b 100644 --- a/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java @@ -859,8 +859,8 @@ static OperatorNode parseReturn(Parser parser, int currentIndex) { static OperatorNode parseGoto(Parser parser, int currentIndex) { Node operand; - // Handle 'goto' keyword as a unary operator with an operand - operand = ListParser.parseZeroOrMoreList(parser, 1, false, false, false, false); + // Handle 'goto' keyword - operand is optional (bare `goto` is a runtime error) + operand = ListParser.parseZeroOrMoreList(parser, 0, false, false, false, false); // Always return a goto operator - the emitter handles &sub vs LABEL distinction return new OperatorNode("goto", operand, currentIndex); } From 6065c2f9aeb961c18c9e5a60cb411f7be8ffc4a4 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Wed, 1 Apr 2026 10:30:15 +0200 Subject: [PATCH 08/19] Update test improvement plan with batch 4 progress and sort.t notes Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- dev/design/test_pass_rate_improvement_plan.md | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/dev/design/test_pass_rate_improvement_plan.md b/dev/design/test_pass_rate_improvement_plan.md index c7b89740b..42c7a362f 100644 --- a/dev/design/test_pass_rate_improvement_plan.md +++ b/dev/design/test_pass_rate_improvement_plan.md @@ -434,6 +434,41 @@ was outdated. Actual status needs measurement. Key issues found: 2. **`sort CORE::reverse LIST`** is misparsed — `CORE::reverse` is treated as a comparator name instead of a function applied to the list. Fix needed in `ParseMapGrepSort.parseSort()`. +#### Quick win batch 4 (2025-03-31) + +Branch: `fix/test-pass-rate-quick-wins` + +| Fix | Tests Gained | Category | +|-----|-------------|----------| +| Handle TIED_SCALAR in typeglob assignment (tiedFetch before dispatch) | +63 (tie_fetch_count.t: 1/343→64/343) | 1.9 | +| Allow argless `goto` (runtime error instead of compile error) | unblocks goto.t past line 525 (still blocked at 755) | 3.6 | +| Support `$$` prototype in sort comparators (pass args via `@_`) | +2 (sort.t: 23→25/206) | 1.10 | + +Files changed: +- `RuntimeGlob.java` — add `case TIED_SCALAR: return set(value.tiedFetch())` in typeglob switch +- `RuntimeStashEntry.java` — same TIED_SCALAR case in stash entry switch +- `OperatorParser.java` — change goto arg minimum from 1 to 0 +- `EmitControlFlow.java` — bare goto falls back to interpreter +- `CompileOperator.java` — bare goto emits GOTO_DYNAMIC with empty string +- `BytecodeInterpreter.java` — GOTO_DYNAMIC empty label throws "goto must have label" +- `ListOperators.java` — detect `$$` prototype on comparator, populate `@_` args + +#### Investigation notes: op/sort.t (updated) + +**Status:** 25/206 passing (was 23). Three remaining blockers: + +1. **`$$` prototype fix landed** — +2 tests from stacked comparator support. + +2. **`sort CORE::reverse LIST`** still misparsed — `CORE::reverse` treated as comparator name. + Fix needed in `ParseMapGrepSort.parseSort()`. + +3. **`sort $glob` / `sort $globref`** — crashes with "Not a CODE reference" at test 30. + `sort $sortglob 4,1,3,2` where `$sortglob = *Backwards` needs the sort implementation + to dereference globs to their CODE slot. Fix needed in `ListOperators.sort()` around + the comparator resolution logic. + +4. **Tests 4, 6** — sort of utf8/non-utf8 lists produce wrong order. Likely locale/collation issue. + ### Baseline Update this document as fixes land. Use the test runner to measure progress: From 1900936dbdb47c826b163ef50ca59e4ab0dbc6e7 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Wed, 1 Apr 2026 10:56:43 +0200 Subject: [PATCH 09/19] Implement last/next/redo EXPR dynamic labels + update plan Add CREATE_LAST_DYNAMIC/CREATE_NEXT_DYNAMIC/CREATE_REDO_DYNAMIC opcodes for runtime-evaluated loop control labels (e.g. last $label). The JVM backend falls back to interpreter for these. Unblocks loopctl.t from 0/0 to ~45/69. Update test improvement plan with investigation notes for override.t, loopctl.t, parser.t, and heredoc.t from subagent analysis. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- dev/design/test_pass_rate_improvement_plan.md | 53 ++++++++++++++++++- .../backend/bytecode/BytecodeCompiler.java | 22 +++++++- .../backend/bytecode/BytecodeInterpreter.java | 10 ++++ .../backend/bytecode/Disassemble.java | 11 ++++ .../perlonjava/backend/bytecode/Opcodes.java | 9 ++++ .../backend/jvm/EmitControlFlow.java | 5 +- .../org/perlonjava/core/Configuration.java | 2 +- 7 files changed, 107 insertions(+), 5 deletions(-) diff --git a/dev/design/test_pass_rate_improvement_plan.md b/dev/design/test_pass_rate_improvement_plan.md index 42c7a362f..a96447539 100644 --- a/dev/design/test_pass_rate_improvement_plan.md +++ b/dev/design/test_pass_rate_improvement_plan.md @@ -443,14 +443,18 @@ Branch: `fix/test-pass-rate-quick-wins` | Handle TIED_SCALAR in typeglob assignment (tiedFetch before dispatch) | +63 (tie_fetch_count.t: 1/343→64/343) | 1.9 | | Allow argless `goto` (runtime error instead of compile error) | unblocks goto.t past line 525 (still blocked at 755) | 3.6 | | Support `$$` prototype in sort comparators (pass args via `@_`) | +2 (sort.t: 23→25/206) | 1.10 | +| Implement `last/next/redo EXPR` dynamic labels | loopctl.t: 0/0→~45/69 (hangs at test 62) | New | Files changed: - `RuntimeGlob.java` — add `case TIED_SCALAR: return set(value.tiedFetch())` in typeglob switch - `RuntimeStashEntry.java` — same TIED_SCALAR case in stash entry switch - `OperatorParser.java` — change goto arg minimum from 1 to 0 -- `EmitControlFlow.java` — bare goto falls back to interpreter +- `EmitControlFlow.java` — bare goto falls back to interpreter; dynamic `last/next/redo EXPR` falls back - `CompileOperator.java` — bare goto emits GOTO_DYNAMIC with empty string -- `BytecodeInterpreter.java` — GOTO_DYNAMIC empty label throws "goto must have label" +- `BytecodeInterpreter.java` — GOTO_DYNAMIC empty label throws "goto must have label"; new CREATE_*_DYNAMIC handlers +- `BytecodeCompiler.java` — `last/next/redo EXPR` emits CREATE_*_DYNAMIC opcodes +- `Opcodes.java` — new CREATE_LAST_DYNAMIC/CREATE_NEXT_DYNAMIC/CREATE_REDO_DYNAMIC opcodes +- `Disassemble.java` — disassembly support for new opcodes - `ListOperators.java` — detect `$$` prototype on comparator, populate `@_` args #### Investigation notes: op/sort.t (updated) @@ -469,6 +473,51 @@ Files changed: 4. **Tests 4, 6** — sort of utf8/non-utf8 lists produce wrong order. Likely locale/collation issue. +#### Investigation notes: op/override.t (item 2.7) + +**Status:** 0/0 (VerifyError crash). The test installs `CORE::GLOBAL::*` overrides via BEGIN blocks. +The JVM bytecode emitter generates invalid bytecode for the complex control flow from 30+ rewritten +builtin calls. Two-part fix needed: + +1. **Immediate:** Add `VerifyError` to `needsInterpreterFallback()` in `PerlLanguageProvider.java` + (line 479) so the interpreter backend handles it instead of crashing. + +2. **Root cause:** The bytecode generation for overridden builtins in `EmitSubroutine.handleApplyOperator()` + creates complex branch structures that fail JVM verification. Needs ASM debugging to find the + exact invalid pattern. + +#### Investigation notes: op/loopctl.t (item 1.13, was 0/0) + +**Status:** ~45/69 passing (was 0/0). The compile-time crash from `last EXPR` / `next EXPR` / `redo EXPR` +is fixed with new `CREATE_*_DYNAMIC` opcodes. Remaining failures: + +- Tests 1, 5, 20, 24: `redo` in while/until loops returns 0 instead of 1 — redo re-evaluates the + condition when it shouldn't (redo should jump to loop body, not loop condition). +- Tests 10, 11, 29, 30: `next` in for(@array) returns 0 — similar control flow issue. +- Tests 18, 37: `next` on bare block returns 0. +- Tests 46-48: `redo` lexical lifetime — redo creates a new lexical scope when it shouldn't. +- Test 49: reverse with empty array slots — unrelated to loop control. +- Tests 62-69: hang (likely `last EXPR` / `next EXPR` test at lines 1088-1123 causes infinite loop). + +#### Investigation notes: comp/parser.t (item new) + +**Status:** 96/195 (blocked at ~96). Error: "Unsupported $ operand: StringNode" in the interpreter +backend's `BytecodeCompiler.java` (line 3783). The `$` sigil handler doesn't support `StringNode` +operands, which arise from `${}` (empty braces) and `${< getCapturedVarIndices() { void handleLoopControlOperator(OperatorNode node, String op) { // Extract label if present String labelStr = null; + boolean isDynamicLabel = false; + int dynamicLabelReg = -1; if (node.operand instanceof ListNode labelNode && !labelNode.elements.isEmpty()) { Node arg = labelNode.elements.getFirst(); if (arg instanceof IdentifierNode) { labelStr = ((IdentifierNode) arg).name; } else { - throwCompilerException("Not implemented: " + node, node.getIndex()); + // Dynamic label: last EXPR, next EXPR, redo EXPR + // Evaluate expression at runtime to get label string + isDynamicLabel = true; + compileNode(arg, -1, RuntimeContextType.SCALAR); + dynamicLabelReg = lastResultReg; } } + // Dynamic label always uses non-local control flow + if (isDynamicLabel) { + short createDynOp = op.equals("last") ? Opcodes.CREATE_LAST_DYNAMIC + : op.equals("next") ? Opcodes.CREATE_NEXT_DYNAMIC + : Opcodes.CREATE_REDO_DYNAMIC; + int rd = allocateOutputRegister(); + emit(createDynOp); + emitReg(rd); + emitReg(dynamicLabelReg); + emit(Opcodes.RETURN); + emitReg(rd); + return; + } + // Find the target loop LoopInfo targetLoop = null; if (labelStr == null) { diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 337d2b421..d23026ba7 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -1080,6 +1080,16 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c pc = InlineOpcodeHandler.executeCreateRedo(bytecode, pc, registers, code); } + case Opcodes.CREATE_LAST_DYNAMIC, Opcodes.CREATE_NEXT_DYNAMIC, Opcodes.CREATE_REDO_DYNAMIC -> { + int rd = bytecode[pc++]; + int labelReg = bytecode[pc++]; + String label = ((RuntimeScalar) registers[labelReg]).toString(); + ControlFlowType type = opcode == Opcodes.CREATE_LAST_DYNAMIC ? ControlFlowType.LAST + : opcode == Opcodes.CREATE_NEXT_DYNAMIC ? ControlFlowType.NEXT + : ControlFlowType.REDO; + registers[rd] = new RuntimeControlFlowList(type, label, code.sourceName, code.sourceLine); + } + case Opcodes.CREATE_GOTO -> { pc = InlineOpcodeHandler.executeCreateGoto(bytecode, pc, registers, code); } diff --git a/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java b/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java index f35d12c38..da0821377 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java @@ -1634,6 +1634,17 @@ public static String disassemble(InterpretedCode interpretedCode) { sb.append("\n"); break; } + case Opcodes.CREATE_LAST_DYNAMIC: + case Opcodes.CREATE_NEXT_DYNAMIC: + case Opcodes.CREATE_REDO_DYNAMIC: { + rd = interpretedCode.bytecode[pc++]; + int labelReg = interpretedCode.bytecode[pc++]; + String dynName = opcode == Opcodes.CREATE_LAST_DYNAMIC ? "CREATE_LAST_DYNAMIC" + : opcode == Opcodes.CREATE_NEXT_DYNAMIC ? "CREATE_NEXT_DYNAMIC" + : "CREATE_REDO_DYNAMIC"; + sb.append(dynName).append(" r").append(rd).append(" r").append(labelReg).append("\n"); + break; + } case Opcodes.CREATE_GOTO: { rd = interpretedCode.bytecode[pc++]; int cfLabelIdx = interpretedCode.bytecode[pc++]; diff --git a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java index c25af477b..c75fd220c 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java @@ -2054,6 +2054,15 @@ public class Opcodes { public static final short SETPWENT = 411; public static final short ENDPWENT = 412; + /** + * Dynamic loop control: last/next/redo with runtime-evaluated label expression. + * Format: CREATE_LAST_DYNAMIC rd labelReg + * Creates RuntimeControlFlowList with label from registers[labelReg].toString(). + */ + public static final short CREATE_LAST_DYNAMIC = 413; + public static final short CREATE_NEXT_DYNAMIC = 414; + public static final short CREATE_REDO_DYNAMIC = 415; + private Opcodes() { } // Utility class - no instantiation } diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java b/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java index 35edecb14..780a637de 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java @@ -69,7 +69,10 @@ static void handleNextOperator(EmitterContext ctx, OperatorNode node) { // Extract the label name. labelStr = ((IdentifierNode) arg).name; } else { - throw new PerlCompilerException(node.tokenIndex, "Not implemented: " + node, ctx.errorUtil); + // Dynamic label: last EXPR, next EXPR, redo EXPR + // Fall back to interpreter which supports dynamic label evaluation + throw new PerlCompilerException(node.tokenIndex, + "Dynamic loop control EXPR requires interpreter fallback", ctx.errorUtil); } } diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 7622d3ec4..db7765d67 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "825777091"; + public static final String gitCommitId = "6065c2f9a"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). From cca6cfac4170a5cbed913c8bd221d0192c39e8e6 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Wed, 1 Apr 2026 11:05:31 +0200 Subject: [PATCH 10/19] Fix StringNode dereference, VerifyError fallback, heredoc state, prototype off-by-one - BytecodeCompiler: add StringNode support for $ and & sigil handlers, fixing 'Unsupported $ operand: StringNode' crash that blocked parser.t - PerlLanguageProvider: add VerifyError to needsInterpreterFallback() so invalid bytecode falls back to interpreter instead of crashing - ListParser: save/restore heredoc state around wantFileHandle look-ahead to prevent heredoc corruption on backtrack - PrototypeArgs: fix off-by-one in group prototype (\[$@%*]) parsing that skipped the character after the closing bracket - Update test improvement plan with getlogin investigation for override.t Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- dev/design/test_pass_rate_improvement_plan.md | 13 +++++++++++++ .../scriptengine/PerlLanguageProvider.java | 6 ++++++ .../backend/bytecode/BytecodeCompiler.java | 19 +++++++++++++++++++ .../org/perlonjava/core/Configuration.java | 2 +- .../frontend/parser/ListParser.java | 9 +++++++++ .../frontend/parser/PrototypeArgs.java | 2 +- 6 files changed, 49 insertions(+), 2 deletions(-) diff --git a/dev/design/test_pass_rate_improvement_plan.md b/dev/design/test_pass_rate_improvement_plan.md index a96447539..1c523527f 100644 --- a/dev/design/test_pass_rate_improvement_plan.md +++ b/dev/design/test_pass_rate_improvement_plan.md @@ -518,6 +518,19 @@ look-ahead in `ListParser.java` (lines 141-155). When `wantFileHandle` look-ahea **Fix:** Add `saveHeredocState()` / `restoreHeredocStateIfNeeded()` around the `wantFileHandle` section, matching the pattern already used in `looksLikeEmptyList()` (lines 305-384). +#### Investigation notes: op/override.t (updated) + +**Status:** 0/0 (compile-time crash). The initial diagnosis of VerifyError was incorrect. +The actual blocker is `Unsupported operator: getlogin` at line 20. The test uses `getlogin` +as a CORE::GLOBAL override target, but `getlogin` is not implemented as an operator. + +**Fix:** Implement `getlogin` as a built-in operator. In Perl, `getlogin()` returns the +current login name from `/dev/utmp` (or equivalent). On Java, this can be implemented as +`System.getProperty("user.name")`. Needs: +1. Add `getlogin` to the operator table in the parser +2. Implement the runtime operation (return `System.getProperty("user.name")`) +3. This unblocks override.t which tests CORE::GLOBAL overrides for ~36 built-in functions + ### Baseline Update this document as fixes land. Use the test runner to measure progress: diff --git a/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java b/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java index 1dd71f797..00d307006 100644 --- a/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java +++ b/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java @@ -478,6 +478,12 @@ private static RuntimeCode compileToExecutable(Node ast, EmitterContext ctx) thr private static boolean needsInterpreterFallback(Throwable e) { for (Throwable t = e; t != null; t = t.getCause()) { + // VerifyError means the JVM rejected the generated bytecode + // (e.g., invalid stack map frames from complex control flow). + // Fall back to interpreter instead of crashing. + if (t instanceof VerifyError) { + return true; + } String msg = t.getMessage(); if (msg != null && ( msg.contains("Method too large") || diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index a6ad93dff..060ddbcf3 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -3779,6 +3779,15 @@ void compileVariableReference(OperatorNode node, String op) { emit(pkgIdx); } lastResultReg = rd; + } else if (node.operand instanceof StringNode strNode) { + // Symbolic ref: ${'name'} — load global scalar by string name + String globalName = NameNormalizer.normalizeVariableName(strNode.value, getCurrentPackage()); + int nameIdx = addToStringPool(globalName); + int rd = allocateRegister(); + emit(Opcodes.LOAD_GLOBAL_SCALAR); + emitReg(rd); + emit(nameIdx); + lastResultReg = rd; } else { throwCompilerException("Unsupported $ operand: " + node.operand.getClass().getSimpleName()); } @@ -4082,6 +4091,16 @@ void compileVariableReference(OperatorNode node, String op) { emitReg(rd); emit(constIdx); + lastResultReg = rd; + } else if (node.operand instanceof StringNode strNode) { + // Symbolic ref: &{'name'} — look up global code reference by string name + String globalName = NameNormalizer.normalizeVariableName(strNode.value, getCurrentPackage()); + RuntimeScalar codeRef = GlobalVariable.getGlobalCodeRef(globalName); + int rd = allocateOutputRegister(); + int constIdx = addToConstantPool(codeRef); + emit(Opcodes.LOAD_CONST); + emitReg(rd); + emit(constIdx); lastResultReg = rd; } else if (node.operand instanceof BlockNode || node.operand instanceof OperatorNode) { // Dynamic code reference: &{$name} or &$name diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index db7765d67..78ff4dd89 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "6065c2f9a"; + public static final String gitCommitId = "1900936db"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). diff --git a/src/main/java/org/perlonjava/frontend/parser/ListParser.java b/src/main/java/org/perlonjava/frontend/parser/ListParser.java index f005bfaa1..463d7059e 100644 --- a/src/main/java/org/perlonjava/frontend/parser/ListParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/ListParser.java @@ -139,6 +139,11 @@ static ListNode parseZeroOrMoreList(Parser parser, int minItems, boolean wantBlo } if (wantFileHandle) { + // Save heredoc state before look-ahead, since skipWhitespace in FileHandle.parseFileHandle + // may trigger parseHeredocAfterNewline which consumes heredoc content. If we backtrack + // (no filehandle found), we need to restore the heredoc state. + List savedHeredocNodes = ParseHeredoc.saveHeredocState(parser); + if (TokenUtils.peek(parser).text.equals("(")) { TokenUtils.consume(parser); hasParen = true; @@ -149,6 +154,10 @@ static ListNode parseZeroOrMoreList(Parser parser, int minItems, boolean wantBlo parser.debugHeredocState("FILEHANDLE_BEFORE_BACKTRACK"); expr.handle = null; parser.tokenIndex = currentIndex; + + // Restore heredoc state if needed + ParseHeredoc.restoreHeredocStateIfNeeded(parser, savedHeredocNodes); + parser.debugHeredocState("FILEHANDLE_AFTER_BACKTRACK"); hasParen = false; } diff --git a/src/main/java/org/perlonjava/frontend/parser/PrototypeArgs.java b/src/main/java/org/perlonjava/frontend/parser/PrototypeArgs.java index f268e2786..54f04aa86 100644 --- a/src/main/java/org/perlonjava/frontend/parser/PrototypeArgs.java +++ b/src/main/java/org/perlonjava/frontend/parser/PrototypeArgs.java @@ -784,7 +784,7 @@ private static int handleBackslashArgument(Parser parser, ListNode args, String while (prototypeIndex < prototype.length() && prototype.charAt(prototypeIndex) != ']') { prototypeIndex++; } - return prototypeIndex + 1; // For groups, skip past the closing ']' + return prototypeIndex; // Return index of ']'; caller's i++ advances past it } public static boolean consumeCommaIfPresent(Parser parser, boolean isOptional) { From b02c5256867119e1eff927c815ea62948e8fe81c Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Wed, 1 Apr 2026 11:09:20 +0200 Subject: [PATCH 11/19] Update test improvement plan with require_errors.t and override.t findings Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- dev/design/test_pass_rate_improvement_plan.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/dev/design/test_pass_rate_improvement_plan.md b/dev/design/test_pass_rate_improvement_plan.md index 1c523527f..279b9befc 100644 --- a/dev/design/test_pass_rate_improvement_plan.md +++ b/dev/design/test_pass_rate_improvement_plan.md @@ -531,6 +531,19 @@ current login name from `/dev/utmp` (or equivalent). On Java, this can be implem 2. Implement the runtime operation (return `System.getProperty("user.name")`) 3. This unblocks override.t which tests CORE::GLOBAL overrides for ~36 built-in functions +#### Investigation notes: op/require_errors.t (item 2.4) + +**Status:** 0/0 (compile-time crash). Error: `syntax error at op/require_errors.t line 258, near "qr/\\"`. + +The offending line is: `eval { no warnings 'syscalls'; require eval "qr/\0/" };` +The `\0` inside the double-quoted string produces a null byte. The parser then sees +`qr/` + NUL + `/` and gets confused, interpreting it as `qr/\\` (unterminated regex). + +**Fix options:** +1. **Patch the test** to skip the null-byte lines (258-263) — quickest unblock +2. **Fix the string parser** to handle embedded null bytes in double-quoted strings + that contain regex-like content — proper fix but harder + ### Baseline Update this document as fixes land. Use the test runner to measure progress: From d6d80c2ac8ada0138c0dcd18a5dc83036081d71b Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Wed, 1 Apr 2026 11:21:35 +0200 Subject: [PATCH 12/19] Add 27 ExtendedNativeUtils operators to bytecode interpreter Add opcodes 416-442 for getlogin, getpwnam, getpwuid, getgrnam, getgrgid, getgrent, setgrent, endgrent, gethostbyaddr, getservbyname, getservbyport, getprotobyname, getprotobynumber, endhostent, endnetent, endprotoent, endservent, gethostent, getnetbyaddr, getnetbyname, getnetent, getprotoent, getservent, sethostent, setnetent, setprotoent, setservent. These operators were already implemented in the JVM backend but missing from the bytecode interpreter, causing 'Unsupported operator' crashes when interpreter fallback was triggered. Files changed: Opcodes.java, CompileOperator.java, MiscOpcodeHandler.java, BytecodeInterpreter.java, Disassemble.java Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- dev/design/test_pass_rate_improvement_plan.md | 7 +- .../backend/bytecode/BytecodeInterpreter.java | 96 ++++++++++++++++++- .../backend/bytecode/CompileOperator.java | 27 ++++++ .../backend/bytecode/Disassemble.java | 56 ++++++++++- .../backend/bytecode/MiscOpcodeHandler.java | 27 ++++++ .../perlonjava/backend/bytecode/Opcodes.java | 29 ++++++ .../org/perlonjava/core/Configuration.java | 2 +- 7 files changed, 237 insertions(+), 7 deletions(-) diff --git a/dev/design/test_pass_rate_improvement_plan.md b/dev/design/test_pass_rate_improvement_plan.md index 279b9befc..71973a1e9 100644 --- a/dev/design/test_pass_rate_improvement_plan.md +++ b/dev/design/test_pass_rate_improvement_plan.md @@ -539,10 +539,9 @@ The offending line is: `eval { no warnings 'syscalls'; require eval "qr/\0/" };` The `\0` inside the double-quoted string produces a null byte. The parser then sees `qr/` + NUL + `/` and gets confused, interpreting it as `qr/\\` (unterminated regex). -**Fix options:** -1. **Patch the test** to skip the null-byte lines (258-263) — quickest unblock -2. **Fix the string parser** to handle embedded null bytes in double-quoted strings - that contain regex-like content — proper fix but harder +**Fix:** Fix the string parser to handle embedded null bytes in double-quoted strings +that contain regex-like content. The null byte from `\0` confuses the tokenizer/parser +when it appears inside string content adjacent to regex delimiters. ### Baseline diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index d23026ba7..fb6f7b156 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -1606,7 +1606,20 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c Opcodes.SYMLINK, Opcodes.CHROOT, Opcodes.MKDIR, Opcodes.MSGCTL, Opcodes.SHMCTL, Opcodes.SEMCTL, Opcodes.EXEC, Opcodes.FCNTL, Opcodes.IOCTL, - Opcodes.GETPWENT, Opcodes.SETPWENT, Opcodes.ENDPWENT -> { + Opcodes.GETPWENT, Opcodes.SETPWENT, Opcodes.ENDPWENT, + Opcodes.GETLOGIN, Opcodes.GETPWNAM, Opcodes.GETPWUID, + Opcodes.GETGRNAM, Opcodes.GETGRGID, Opcodes.GETGRENT, + Opcodes.SETGRENT, Opcodes.ENDGRENT, + Opcodes.GETHOSTBYADDR, Opcodes.GETSERVBYNAME, + Opcodes.GETSERVBYPORT, Opcodes.GETPROTOBYNAME, + Opcodes.GETPROTOBYNUMBER, Opcodes.ENDHOSTENT, + Opcodes.ENDNETENT, Opcodes.ENDPROTOENT, + Opcodes.ENDSERVENT, Opcodes.GETHOSTENT, + Opcodes.GETNETBYADDR, Opcodes.GETNETBYNAME, + Opcodes.GETNETENT, Opcodes.GETPROTOENT, + Opcodes.GETSERVENT, Opcodes.SETHOSTENT, + Opcodes.SETNETENT, Opcodes.SETPROTOENT, + Opcodes.SETSERVENT -> { pc = executeSystemOps(opcode, bytecode, pc, registers); } @@ -2556,6 +2569,87 @@ private static int executeSystemOps(int opcode, int[] bytecode, int pc, case Opcodes.ENDPWENT -> { return MiscOpcodeHandler.execute(Opcodes.ENDPWENT, bytecode, pc, registers); } + case Opcodes.GETLOGIN -> { + return MiscOpcodeHandler.execute(Opcodes.GETLOGIN, bytecode, pc, registers); + } + case Opcodes.GETPWNAM -> { + return MiscOpcodeHandler.execute(Opcodes.GETPWNAM, bytecode, pc, registers); + } + case Opcodes.GETPWUID -> { + return MiscOpcodeHandler.execute(Opcodes.GETPWUID, bytecode, pc, registers); + } + case Opcodes.GETGRNAM -> { + return MiscOpcodeHandler.execute(Opcodes.GETGRNAM, bytecode, pc, registers); + } + case Opcodes.GETGRGID -> { + return MiscOpcodeHandler.execute(Opcodes.GETGRGID, bytecode, pc, registers); + } + case Opcodes.GETGRENT -> { + return MiscOpcodeHandler.execute(Opcodes.GETGRENT, bytecode, pc, registers); + } + case Opcodes.SETGRENT -> { + return MiscOpcodeHandler.execute(Opcodes.SETGRENT, bytecode, pc, registers); + } + case Opcodes.ENDGRENT -> { + return MiscOpcodeHandler.execute(Opcodes.ENDGRENT, bytecode, pc, registers); + } + case Opcodes.GETHOSTBYADDR -> { + return MiscOpcodeHandler.execute(Opcodes.GETHOSTBYADDR, bytecode, pc, registers); + } + case Opcodes.GETSERVBYNAME -> { + return MiscOpcodeHandler.execute(Opcodes.GETSERVBYNAME, bytecode, pc, registers); + } + case Opcodes.GETSERVBYPORT -> { + return MiscOpcodeHandler.execute(Opcodes.GETSERVBYPORT, bytecode, pc, registers); + } + case Opcodes.GETPROTOBYNAME -> { + return MiscOpcodeHandler.execute(Opcodes.GETPROTOBYNAME, bytecode, pc, registers); + } + case Opcodes.GETPROTOBYNUMBER -> { + return MiscOpcodeHandler.execute(Opcodes.GETPROTOBYNUMBER, bytecode, pc, registers); + } + case Opcodes.ENDHOSTENT -> { + return MiscOpcodeHandler.execute(Opcodes.ENDHOSTENT, bytecode, pc, registers); + } + case Opcodes.ENDNETENT -> { + return MiscOpcodeHandler.execute(Opcodes.ENDNETENT, bytecode, pc, registers); + } + case Opcodes.ENDPROTOENT -> { + return MiscOpcodeHandler.execute(Opcodes.ENDPROTOENT, bytecode, pc, registers); + } + case Opcodes.ENDSERVENT -> { + return MiscOpcodeHandler.execute(Opcodes.ENDSERVENT, bytecode, pc, registers); + } + case Opcodes.GETHOSTENT -> { + return MiscOpcodeHandler.execute(Opcodes.GETHOSTENT, bytecode, pc, registers); + } + case Opcodes.GETNETBYADDR -> { + return MiscOpcodeHandler.execute(Opcodes.GETNETBYADDR, bytecode, pc, registers); + } + case Opcodes.GETNETBYNAME -> { + return MiscOpcodeHandler.execute(Opcodes.GETNETBYNAME, bytecode, pc, registers); + } + case Opcodes.GETNETENT -> { + return MiscOpcodeHandler.execute(Opcodes.GETNETENT, bytecode, pc, registers); + } + case Opcodes.GETPROTOENT -> { + return MiscOpcodeHandler.execute(Opcodes.GETPROTOENT, bytecode, pc, registers); + } + case Opcodes.GETSERVENT -> { + return MiscOpcodeHandler.execute(Opcodes.GETSERVENT, bytecode, pc, registers); + } + case Opcodes.SETHOSTENT -> { + return MiscOpcodeHandler.execute(Opcodes.SETHOSTENT, bytecode, pc, registers); + } + case Opcodes.SETNETENT -> { + return MiscOpcodeHandler.execute(Opcodes.SETNETENT, bytecode, pc, registers); + } + case Opcodes.SETPROTOENT -> { + return MiscOpcodeHandler.execute(Opcodes.SETPROTOENT, bytecode, pc, registers); + } + case Opcodes.SETSERVENT -> { + return MiscOpcodeHandler.execute(Opcodes.SETSERVENT, bytecode, pc, registers); + } default -> throw new RuntimeException("Unknown system opcode: " + opcode); } } diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index b1462b1b0..a44cfe6b9 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -745,6 +745,33 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode case "getpwent" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.GETPWENT); case "setpwent" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.SETPWENT); case "endpwent" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.ENDPWENT); + case "getlogin" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.GETLOGIN); + case "getpwnam" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.GETPWNAM); + case "getpwuid" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.GETPWUID); + case "getgrnam" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.GETGRNAM); + case "getgrgid" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.GETGRGID); + case "getgrent" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.GETGRENT); + case "setgrent" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.SETGRENT); + case "endgrent" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.ENDGRENT); + case "gethostbyaddr" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.GETHOSTBYADDR); + case "getservbyname" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.GETSERVBYNAME); + case "getservbyport" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.GETSERVBYPORT); + case "getprotobyname" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.GETPROTOBYNAME); + case "getprotobynumber" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.GETPROTOBYNUMBER); + case "endhostent" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.ENDHOSTENT); + case "endnetent" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.ENDNETENT); + case "endprotoent" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.ENDPROTOENT); + case "endservent" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.ENDSERVENT); + case "gethostent" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.GETHOSTENT); + case "getnetbyaddr" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.GETNETBYADDR); + case "getnetbyname" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.GETNETBYNAME); + case "getnetent" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.GETNETENT); + case "getprotoent" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.GETPROTOENT); + case "getservent" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.GETSERVENT); + case "sethostent" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.SETHOSTENT); + case "setnetent" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.SETNETENT); + case "setprotoent" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.SETPROTOENT); + case "setservent" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.SETSERVENT); case "opendir" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.OPENDIR); case "readdir" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.READDIR); case "seekdir" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.SEEKDIR); diff --git a/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java b/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java index da0821377..5451178a4 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java @@ -1846,7 +1846,34 @@ public static String disassemble(InterpretedCode interpretedCode) { case Opcodes.IOCTL: case Opcodes.GETPWENT: case Opcodes.SETPWENT: - case Opcodes.ENDPWENT: { + case Opcodes.ENDPWENT: + case Opcodes.GETLOGIN: + case Opcodes.GETPWNAM: + case Opcodes.GETPWUID: + case Opcodes.GETGRNAM: + case Opcodes.GETGRGID: + case Opcodes.GETGRENT: + case Opcodes.SETGRENT: + case Opcodes.ENDGRENT: + case Opcodes.GETHOSTBYADDR: + case Opcodes.GETSERVBYNAME: + case Opcodes.GETSERVBYPORT: + case Opcodes.GETPROTOBYNAME: + case Opcodes.GETPROTOBYNUMBER: + case Opcodes.ENDHOSTENT: + case Opcodes.ENDNETENT: + case Opcodes.ENDPROTOENT: + case Opcodes.ENDSERVENT: + case Opcodes.GETHOSTENT: + case Opcodes.GETNETBYADDR: + case Opcodes.GETNETBYNAME: + case Opcodes.GETNETENT: + case Opcodes.GETPROTOENT: + case Opcodes.GETSERVENT: + case Opcodes.SETHOSTENT: + case Opcodes.SETNETENT: + case Opcodes.SETPROTOENT: + case Opcodes.SETSERVENT: { rd = interpretedCode.bytecode[pc++]; int sysArgsReg = interpretedCode.bytecode[pc++]; int sysCtx = interpretedCode.bytecode[pc++]; @@ -1871,6 +1898,33 @@ public static String disassemble(InterpretedCode interpretedCode) { case Opcodes.GETPWENT -> "getpwent"; case Opcodes.SETPWENT -> "setpwent"; case Opcodes.ENDPWENT -> "endpwent"; + case Opcodes.GETLOGIN -> "getlogin"; + case Opcodes.GETPWNAM -> "getpwnam"; + case Opcodes.GETPWUID -> "getpwuid"; + case Opcodes.GETGRNAM -> "getgrnam"; + case Opcodes.GETGRGID -> "getgrgid"; + case Opcodes.GETGRENT -> "getgrent"; + case Opcodes.SETGRENT -> "setgrent"; + case Opcodes.ENDGRENT -> "endgrent"; + case Opcodes.GETHOSTBYADDR -> "gethostbyaddr"; + case Opcodes.GETSERVBYNAME -> "getservbyname"; + case Opcodes.GETSERVBYPORT -> "getservbyport"; + case Opcodes.GETPROTOBYNAME -> "getprotobyname"; + case Opcodes.GETPROTOBYNUMBER -> "getprotobynumber"; + case Opcodes.ENDHOSTENT -> "endhostent"; + case Opcodes.ENDNETENT -> "endnetent"; + case Opcodes.ENDPROTOENT -> "endprotoent"; + case Opcodes.ENDSERVENT -> "endservent"; + case Opcodes.GETHOSTENT -> "gethostent"; + case Opcodes.GETNETBYADDR -> "getnetbyaddr"; + case Opcodes.GETNETBYNAME -> "getnetbyname"; + case Opcodes.GETNETENT -> "getnetent"; + case Opcodes.GETPROTOENT -> "getprotoent"; + case Opcodes.GETSERVENT -> "getservent"; + case Opcodes.SETHOSTENT -> "sethostent"; + case Opcodes.SETNETENT -> "setnetent"; + case Opcodes.SETPROTOENT -> "setprotoent"; + case Opcodes.SETSERVENT -> "setservent"; default -> "sys_op_" + opcode; }; sb.append(sysName).append(" r").append(rd) diff --git a/src/main/java/org/perlonjava/backend/bytecode/MiscOpcodeHandler.java b/src/main/java/org/perlonjava/backend/bytecode/MiscOpcodeHandler.java index 839f036ba..ab4e4be15 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/MiscOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/MiscOpcodeHandler.java @@ -94,6 +94,33 @@ public static int execute(int opcode, int[] bytecode, int pc, RuntimeBase[] regi case Opcodes.GETPWENT -> ExtendedNativeUtils.getpwent(ctx, argsArray); case Opcodes.SETPWENT -> ExtendedNativeUtils.setpwent(ctx, argsArray); case Opcodes.ENDPWENT -> ExtendedNativeUtils.endpwent(ctx, argsArray); + case Opcodes.GETLOGIN -> ExtendedNativeUtils.getlogin(ctx, argsArray); + case Opcodes.GETPWNAM -> ExtendedNativeUtils.getpwnam(ctx, argsArray); + case Opcodes.GETPWUID -> ExtendedNativeUtils.getpwuid(ctx, argsArray); + case Opcodes.GETGRNAM -> ExtendedNativeUtils.getgrnam(ctx, argsArray); + case Opcodes.GETGRGID -> ExtendedNativeUtils.getgrgid(ctx, argsArray); + case Opcodes.GETGRENT -> ExtendedNativeUtils.getgrent(ctx, argsArray); + case Opcodes.SETGRENT -> ExtendedNativeUtils.setgrent(ctx, argsArray); + case Opcodes.ENDGRENT -> ExtendedNativeUtils.endgrent(ctx, argsArray); + case Opcodes.GETHOSTBYADDR -> ExtendedNativeUtils.gethostbyaddr(ctx, argsArray); + case Opcodes.GETSERVBYNAME -> ExtendedNativeUtils.getservbyname(ctx, argsArray); + case Opcodes.GETSERVBYPORT -> ExtendedNativeUtils.getservbyport(ctx, argsArray); + case Opcodes.GETPROTOBYNAME -> ExtendedNativeUtils.getprotobyname(ctx, argsArray); + case Opcodes.GETPROTOBYNUMBER -> ExtendedNativeUtils.getprotobynumber(ctx, argsArray); + case Opcodes.ENDHOSTENT -> ExtendedNativeUtils.endhostent(ctx, argsArray); + case Opcodes.ENDNETENT -> ExtendedNativeUtils.endnetent(ctx, argsArray); + case Opcodes.ENDPROTOENT -> ExtendedNativeUtils.endprotoent(ctx, argsArray); + case Opcodes.ENDSERVENT -> ExtendedNativeUtils.endservent(ctx, argsArray); + case Opcodes.GETHOSTENT -> ExtendedNativeUtils.gethostent(ctx, argsArray); + case Opcodes.GETNETBYADDR -> ExtendedNativeUtils.getnetbyaddr(ctx, argsArray); + case Opcodes.GETNETBYNAME -> ExtendedNativeUtils.getnetbyname(ctx, argsArray); + case Opcodes.GETNETENT -> ExtendedNativeUtils.getnetent(ctx, argsArray); + case Opcodes.GETPROTOENT -> ExtendedNativeUtils.getprotoent(ctx, argsArray); + case Opcodes.GETSERVENT -> ExtendedNativeUtils.getservent(ctx, argsArray); + case Opcodes.SETHOSTENT -> ExtendedNativeUtils.sethostent(ctx, argsArray); + case Opcodes.SETNETENT -> ExtendedNativeUtils.setnetent(ctx, argsArray); + case Opcodes.SETPROTOENT -> ExtendedNativeUtils.setprotoent(ctx, argsArray); + case Opcodes.SETSERVENT -> ExtendedNativeUtils.setservent(ctx, argsArray); case Opcodes.OPENDIR -> Directory.opendir(args); case Opcodes.READDIR -> Directory.readdir(args.elements.isEmpty() ? null : (RuntimeScalar) args.elements.get(0), ctx); diff --git a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java index c75fd220c..f94bc41ff 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java @@ -2063,6 +2063,35 @@ public class Opcodes { public static final short CREATE_NEXT_DYNAMIC = 414; public static final short CREATE_REDO_DYNAMIC = 415; + // ExtendedNativeUtils operators (user/group info, network lookups, enumeration) + public static final short GETLOGIN = 416; + public static final short GETPWNAM = 417; + public static final short GETPWUID = 418; + public static final short GETGRNAM = 419; + public static final short GETGRGID = 420; + public static final short GETGRENT = 421; + public static final short SETGRENT = 422; + public static final short ENDGRENT = 423; + public static final short GETHOSTBYADDR = 424; + public static final short GETSERVBYNAME = 425; + public static final short GETSERVBYPORT = 426; + public static final short GETPROTOBYNAME = 427; + public static final short GETPROTOBYNUMBER = 428; + public static final short ENDHOSTENT = 429; + public static final short ENDNETENT = 430; + public static final short ENDPROTOENT = 431; + public static final short ENDSERVENT = 432; + public static final short GETHOSTENT = 433; + public static final short GETNETBYADDR = 434; + public static final short GETNETBYNAME = 435; + public static final short GETNETENT = 436; + public static final short GETPROTOENT = 437; + public static final short GETSERVENT = 438; + public static final short SETHOSTENT = 439; + public static final short SETNETENT = 440; + public static final short SETPROTOENT = 441; + public static final short SETSERVENT = 442; + private Opcodes() { } // Utility class - no instantiation } diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 78ff4dd89..c242a7f71 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "1900936db"; + public static final String gitCommitId = "b02c52568"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). From cc782dea1b1ca19c903b820638c2b755986b1021 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Wed, 1 Apr 2026 11:26:31 +0200 Subject: [PATCH 13/19] Fix interpreter null-node crash, FormatNode no-op, VERSION method dispatch - BytecodeCompiler: add null guard in compileNode() to handle null AST elements from format declarations, preventing NullPointerException during interpreter fallback compilation - BytecodeCompiler: make FormatNode visitor a no-op in the interpreter backend since formats are already registered by the JVM compiler. Fixes write.t crash from 'Formats not yet implemented'. - StatementParser: dispatch 'use MODULE VERSION' through Perl method resolution (can + apply) instead of calling Universal.VERSION() directly. This allows custom VERSION methods to be called. Fixes leaky-magic.t: 0/0 -> 70/71. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../backend/bytecode/BytecodeCompiler.java | 10 ++++- .../org/perlonjava/core/Configuration.java | 2 +- .../frontend/parser/StatementParser.java | 37 ++++++++++++++++--- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 060ddbcf3..7f22497c2 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -4198,6 +4198,11 @@ void emitAliasWithTarget(int destReg, int srcReg) { } void compileNode(Node node, int targetReg, int callContext) { + if (node == null) { + // Skip null nodes (e.g., from format declarations that produce empty statements) + lastResultReg = -1; + return; + } int savedTarget = targetOutputReg; int savedContext = currentCallContext; targetOutputReg = targetReg; @@ -5419,7 +5424,10 @@ public void visit(CompilerFlagNode node) { @Override public void visit(FormatNode node) { - throw new UnsupportedOperationException("Formats not yet implemented"); + // Format declarations are handled at the JVM compilation stage. + // When the interpreter backend processes the AST, formats are already + // registered, so this is a no-op. + lastResultReg = -1; } @Override diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index c242a7f71..487e2bb5d 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "b02c52568"; + public static final String gitCommitId = "d6d80c2ac"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). diff --git a/src/main/java/org/perlonjava/frontend/parser/StatementParser.java b/src/main/java/org/perlonjava/frontend/parser/StatementParser.java index 46b4d0310..823e2c658 100644 --- a/src/main/java/org/perlonjava/frontend/parser/StatementParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/StatementParser.java @@ -649,12 +649,39 @@ public static Node parseUseDeclaration(Parser parser, LexerToken token) { if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug("Use statement return: " + ret); if (versionNode != null) { - // check module version + // check module version via method dispatch (Module->VERSION(version)) + // This must go through normal method resolution so that custom VERSION + // methods (e.g., sub tests::VERSION { ... }) are called. if (CompilerOptions.DEBUG_ENABLED) parser.ctx.logDebug("use version: check module version"); - RuntimeArray args = new RuntimeArray(); - RuntimeArray.push(args, new RuntimeScalar(packageName)); - RuntimeArray.push(args, versionScalar); - Universal.VERSION(args, RuntimeContextType.SCALAR); + + // Look up the VERSION method via can() + RuntimeArray canArgs = new RuntimeArray(); + RuntimeArray.push(canArgs, new RuntimeScalar(packageName)); + RuntimeArray.push(canArgs, new RuntimeScalar("VERSION")); + RuntimeList codeList = Universal.can(canArgs, RuntimeContextType.SCALAR); + + if (codeList.size() == 1) { + RuntimeScalar code = codeList.getFirst(); + if (code.getBoolean()) { + // Call the VERSION method: Module->VERSION(version) + RuntimeArray versionArgs = new RuntimeArray(); + RuntimeArray.push(versionArgs, new RuntimeScalar(packageName)); + RuntimeArray.push(versionArgs, versionScalar); + RuntimeCode.apply(code, versionArgs, RuntimeContextType.SCALAR); + } else { + // No VERSION method found, fall back to Universal.VERSION + RuntimeArray versionArgs = new RuntimeArray(); + RuntimeArray.push(versionArgs, new RuntimeScalar(packageName)); + RuntimeArray.push(versionArgs, versionScalar); + Universal.VERSION(versionArgs, RuntimeContextType.SCALAR); + } + } else { + // can() returned unexpected result, fall back to Universal.VERSION + RuntimeArray versionArgs = new RuntimeArray(); + RuntimeArray.push(versionArgs, new RuntimeScalar(packageName)); + RuntimeArray.push(versionArgs, versionScalar); + Universal.VERSION(versionArgs, RuntimeContextType.SCALAR); + } } // call Module->import( LIST ) From 315a69923bad05a8af1fe1c5da161c6a0a230e7c Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Wed, 1 Apr 2026 11:35:34 +0200 Subject: [PATCH 14/19] Add subroutine redefined/prototype mismatch compile-time warnings Emit proper warnings when subroutines are redefined at compile time: - "Prototype mismatch: sub NAME ..." - always on (default warning) - "Constant subroutine NAME redefined" - always on (default warning) - "Subroutine NAME redefined" - requires -w or use warnings 'redefine' Matches Perl 5 behavior including: - Format differences: ": none" vs " (proto)" display - Warnings dispatched through $SIG{__WARN__} - Proper $^W and lexical warnings checking Result: comp/redef.t 1/20 -> 20/20 Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../org/perlonjava/core/Configuration.java | 2 +- .../frontend/parser/SubroutineParser.java | 46 ++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 487e2bb5d..a9fc090ff 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "d6d80c2ac"; + public static final String gitCommitId = "cc782dea1"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). diff --git a/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java b/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java index d9a1c9b27..869812b34 100644 --- a/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java @@ -14,6 +14,7 @@ import org.perlonjava.frontend.semantic.SymbolTable; import org.perlonjava.runtime.debugger.DebugState; import org.perlonjava.runtime.mro.InheritanceResolver; +import org.perlonjava.runtime.perlmodule.Warnings; import org.perlonjava.runtime.runtimetypes.*; import java.lang.reflect.Constructor; @@ -740,6 +741,8 @@ public static ListNode handleNamedSubWithFilter(Parser parser, String subName, S // This matches Perl's behavior where: // my $orig = \&foo; sub foo { "new" }; $orig->() returns "old" boolean isRedefinition = false; + String oldPrototype = null; + boolean isConstantSub = false; if (codeRef.value instanceof RuntimeCode existingCode) { // Check if the existing code has actual implementation OR pending compilation // compilerSupplier != null means there's a lazy definition waiting to be compiled @@ -747,8 +750,49 @@ public static ListNode handleNamedSubWithFilter(Parser parser, String subName, S || existingCode.methodHandle != null || existingCode.codeObject != null || existingCode.compilerSupplier != null; + if (isRedefinition) { + oldPrototype = existingCode.prototype; + // A constant sub has empty prototype "()" - detect for "Constant subroutine" warning + isConstantSub = "".equals(oldPrototype); + } } - + + // Emit "Prototype mismatch" and "Subroutine redefined" warnings + if (isRedefinition && block != null) { + String location = ""; + if (parser.ctx.errorUtil != null) { + int line = parser.ctx.errorUtil.getLineNumber(parser.tokenIndex); + location = " at " + parser.ctx.compilerOptions.fileName + " line " + line + ".\n"; + } + + // Prototype mismatch is a default warning (always on unless explicitly disabled) + boolean dollarW = GlobalVariable.getGlobalVariable("main::" + Character.toString('W' - 'A' + 1)).getBoolean(); + { + // Perl format: "sub NAME: none vs (new)" or "sub NAME (old) vs none" + // When prototype is null, display as ": none"; when defined, display as " (proto)" + String oldDisplay = oldPrototype == null ? ": none" : " (" + oldPrototype + ")"; + String newDisplay = prototype == null ? "none" : "(" + prototype + ")"; + String oldForCompare = oldPrototype == null ? "none" : "(" + oldPrototype + ")"; + if (!oldForCompare.equals(newDisplay)) { + String msg = "Prototype mismatch: sub " + fullName + oldDisplay + " vs " + newDisplay + location; + org.perlonjava.runtime.operators.WarnDie.warn( + new RuntimeScalar(msg), new RuntimeScalar("")); + } + } + + // "Constant subroutine X redefined" is a default warning (always on) + // "Subroutine X redefined" requires -w or use warnings 'redefine' + if (isConstantSub) { + String msg = "Constant subroutine " + subName + " redefined" + location; + org.perlonjava.runtime.operators.WarnDie.warn( + new RuntimeScalar(msg), new RuntimeScalar("")); + } else if (dollarW || Warnings.warningManager.isWarningEnabled("redefine")) { + String msg = "Subroutine " + subName + " redefined" + location; + org.perlonjava.runtime.operators.WarnDie.warn( + new RuntimeScalar(msg), new RuntimeScalar("")); + } + } + if (codeRef.value == null || isRedefinition) { codeRef.type = RuntimeScalarType.CODE; codeRef.value = new RuntimeCode(subName, attributes); From a84fe5d2cf90a3a2046697cd2e332852b542f10f Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Wed, 1 Apr 2026 11:53:50 +0200 Subject: [PATCH 15/19] Fix select() bareword filehandle and suppress spurious redefinition warnings - OperatorParser.parseSelect(): Add GlobalVariable.getGlobalIO() autovivification before parseBarewordHandle(), matching the pattern used by readline/stat/etc. This fixes strict subs errors when using bareword filehandles with select(). - SubroutineParser: Skip prototype mismatch and redefinition warnings for Java-registered (XS-like) built-in methods being overridden by Perl stubs. Uses isStatic flag to detect built-in methods. - warnings.pm: Add (;@) prototype to register_categories stub to match Java-registered prototype. - overload.pm: Add ($) prototype to AddrRef stub to match Java-registered prototype. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- src/main/java/org/perlonjava/core/Configuration.java | 2 +- .../java/org/perlonjava/frontend/parser/OperatorParser.java | 2 ++ .../org/perlonjava/frontend/parser/SubroutineParser.java | 6 +++++- src/main/perl/lib/overload.pm | 2 +- src/main/perl/lib/warnings.pm | 2 +- 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index a9fc090ff..a0893c2e9 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "cc782dea1"; + public static final String gitCommitId = "315a69923"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). diff --git a/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java b/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java index 2765ad74b..bf8654392 100644 --- a/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java @@ -487,6 +487,8 @@ public static OperatorNode parseSelect(Parser parser, LexerToken token, int curr if (argCount == 1) { // select FILEHANDLE if (listNode1.elements.getFirst() instanceof IdentifierNode identifierNode) { + // Autovivify the filehandle IO slot so parseBarewordHandle succeeds + GlobalVariable.getGlobalIO(FileHandle.normalizeBarewordHandle(parser, identifierNode.name)); Node handle = FileHandle.parseBarewordHandle(parser, identifierNode.name); if (handle != null) { // handle is Bareword diff --git a/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java b/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java index 869812b34..7792fe5b0 100644 --- a/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java @@ -743,6 +743,7 @@ public static ListNode handleNamedSubWithFilter(Parser parser, String subName, S boolean isRedefinition = false; String oldPrototype = null; boolean isConstantSub = false; + boolean isBuiltinSub = false; // Java-registered (XS-like) methods don't trigger redefine warnings if (codeRef.value instanceof RuntimeCode existingCode) { // Check if the existing code has actual implementation OR pending compilation // compilerSupplier != null means there's a lazy definition waiting to be compiled @@ -754,11 +755,14 @@ public static ListNode handleNamedSubWithFilter(Parser parser, String subName, S oldPrototype = existingCode.prototype; // A constant sub has empty prototype "()" - detect for "Constant subroutine" warning isConstantSub = "".equals(oldPrototype); + // Java-registered methods (via registerMethod) have isStatic=true and methodHandle set + isBuiltinSub = existingCode.isStatic && existingCode.methodHandle != null; } } // Emit "Prototype mismatch" and "Subroutine redefined" warnings - if (isRedefinition && block != null) { + // Skip warnings for Java-registered (XS-like) built-in methods being overridden by Perl stubs + if (isRedefinition && block != null && !isBuiltinSub) { String location = ""; if (parser.ctx.errorUtil != null) { int line = parser.ctx.errorUtil.getLineNumber(parser.tokenIndex); diff --git a/src/main/perl/lib/overload.pm b/src/main/perl/lib/overload.pm index 03ad240a9..8070c2b4e 100644 --- a/src/main/perl/lib/overload.pm +++ b/src/main/perl/lib/overload.pm @@ -109,7 +109,7 @@ sub Method { #return $ {*{$meth}}; } -sub AddrRef { +sub AddrRef ($) { no overloading; "$_[0]"; } diff --git a/src/main/perl/lib/warnings.pm b/src/main/perl/lib/warnings.pm index c713d7902..9369cccbf 100644 --- a/src/main/perl/lib/warnings.pm +++ b/src/main/perl/lib/warnings.pm @@ -104,7 +104,7 @@ our %Offsets = ( # NoOp warnings - warnings that have been removed but kept for compatibility our %NoOp = (); -sub register_categories { +sub register_categories (;@) { # placeholder } From dc20694ab79dd84117b54bc0eee22915f8e10a13 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Wed, 1 Apr 2026 12:08:55 +0200 Subject: [PATCH 16/19] Support GLOB and GLOBREFERENCE types in subroutine dispatch Add handling for GLOB and GLOBREFERENCE (reference to glob) types in all three RuntimeCode.apply() method variants. When a glob or glob reference is used as a subroutine (e.g., sort $glob_ref LIST), extract the code slot from the glob name and dispatch to it. This fixes sort with glob/globref comparators and general glob-as-code calls. Result: op/sort.t 30/206 -> 125+/206 (test hangs on runperl tests) Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../org/perlonjava/core/Configuration.java | 2 +- .../runtime/runtimetypes/RuntimeCode.java | 54 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index a0893c2e9..621815f22 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "315a69923"; + public static final String gitCommitId = "a84fe5d2c"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index 798a34339..3950ddbbb 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -1923,6 +1923,24 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, RuntimeArray a, int } } + // Handle GLOB type - extract CODE slot from the glob + if (runtimeScalar.type == RuntimeScalarType.GLOB) { + RuntimeGlob glob = (RuntimeGlob) runtimeScalar.value; + if (glob.globName != null) { + RuntimeScalar resolved = GlobalVariable.getGlobalCodeRef(glob.globName); + return apply(resolved, a, callContext); + } + } + + // Handle REFERENCE to GLOB (e.g., \*Foo) - dereference to get the glob, then extract CODE + if ((runtimeScalar.type == RuntimeScalarType.REFERENCE || runtimeScalar.type == RuntimeScalarType.GLOBREFERENCE) + && runtimeScalar.value instanceof RuntimeGlob glob) { + if (glob.globName != null) { + RuntimeScalar resolved = GlobalVariable.getGlobalCodeRef(glob.globName); + return apply(resolved, a, callContext); + } + } + if (runtimeScalar.type == STRING || runtimeScalar.type == BYTE_STRING) { String varName = NameNormalizer.normalizeVariableName(runtimeScalar.toString(), "main"); RuntimeScalar resolved = GlobalVariable.getGlobalCodeRef(varName); @@ -2123,6 +2141,24 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, String subroutineNa } } + // Handle GLOB type - extract CODE slot from the glob + if (runtimeScalar.type == RuntimeScalarType.GLOB) { + RuntimeGlob glob = (RuntimeGlob) runtimeScalar.value; + if (glob.globName != null) { + RuntimeScalar resolved = GlobalVariable.getGlobalCodeRef(glob.globName); + return apply(resolved, subroutineName, args, callContext); + } + } + + // Handle REFERENCE to GLOB (e.g., \*Foo) - dereference to get the glob, then extract CODE + if ((runtimeScalar.type == RuntimeScalarType.REFERENCE || runtimeScalar.type == RuntimeScalarType.GLOBREFERENCE) + && runtimeScalar.value instanceof RuntimeGlob glob) { + if (glob.globName != null) { + RuntimeScalar resolved = GlobalVariable.getGlobalCodeRef(glob.globName); + return apply(resolved, subroutineName, args, callContext); + } + } + if (runtimeScalar.type == STRING || runtimeScalar.type == BYTE_STRING) { String varName = NameNormalizer.normalizeVariableName(runtimeScalar.toString(), "main"); RuntimeScalar resolved = GlobalVariable.getGlobalCodeRef(varName); @@ -2226,6 +2262,24 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, String subroutineNa throw new PerlCompilerException(gotoErrorPrefix(subroutineName) + "ndefined subroutine &" + fullSubName + " called"); } + // Handle GLOB type - extract CODE slot from the glob + if (runtimeScalar.type == RuntimeScalarType.GLOB) { + RuntimeGlob glob = (RuntimeGlob) runtimeScalar.value; + if (glob.globName != null) { + RuntimeScalar resolved = GlobalVariable.getGlobalCodeRef(glob.globName); + return apply(resolved, subroutineName, list, callContext); + } + } + + // Handle REFERENCE to GLOB (e.g., \*Foo) - dereference to get the glob, then extract CODE + if ((runtimeScalar.type == RuntimeScalarType.REFERENCE || runtimeScalar.type == RuntimeScalarType.GLOBREFERENCE) + && runtimeScalar.value instanceof RuntimeGlob glob) { + if (glob.globName != null) { + RuntimeScalar resolved = GlobalVariable.getGlobalCodeRef(glob.globName); + return apply(resolved, subroutineName, list, callContext); + } + } + if (runtimeScalar.type == STRING || runtimeScalar.type == BYTE_STRING) { String varName = NameNormalizer.normalizeVariableName(runtimeScalar.toString(), "main"); RuntimeScalar resolved = GlobalVariable.getGlobalCodeRef(varName); From c4e5002c884b966f59c93adf252fe542c55086d5 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Wed, 1 Apr 2026 12:49:43 +0200 Subject: [PATCH 17/19] Add comprehensive test failures analysis document Document all non-quick-fix test failures across 274 failing test files, organized by missing feature/subsystem with difficulty ratings and implementation notes. Covers taint tracking, format/write, regex code blocks, delete local, caller() fields, attribute system, MRO invalidation, in-place editing, and more. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- dev/prompts/test-failures-not-quick-fix.md | 660 ++++++++++++++++++ .../org/perlonjava/core/Configuration.java | 2 +- 2 files changed, 661 insertions(+), 1 deletion(-) create mode 100644 dev/prompts/test-failures-not-quick-fix.md diff --git a/dev/prompts/test-failures-not-quick-fix.md b/dev/prompts/test-failures-not-quick-fix.md new file mode 100644 index 000000000..c1be0518c --- /dev/null +++ b/dev/prompts/test-failures-not-quick-fix.md @@ -0,0 +1,660 @@ +# PerlOnJava: Non-Quick-Fix Test Failures Analysis + +**Date:** 2026-03-31 +**Branch:** `fix/test-pass-rate-quick-wins` +**Overall pass rate:** 92.7% (239,525/258,444 tests), 254 files pass, 274 files fail/incomplete + +This document catalogs every significant non-quick-fix test failure, organized by the missing +feature or subsystem required. Each entry includes what is needed to pass the tests and an +estimated difficulty level. + +--- + +## Table of Contents + +1. [Taint Tracking](#1-taint-tracking) +2. [Format/Write System](#2-formatwrite-system) +3. [Regex Code Blocks (?{...})](#3-regex-code-blocks) +4. [delete local Construct](#4-delete-local-construct) +5. [\(LIST) Reference Creation](#5-list-reference-creation) +6. [Tied Scalar Code Deref](#6-tied-scalar-code-deref) +7. [caller() Extended Fields](#7-caller-extended-fields) +8. [Attribute System](#8-attribute-system) +9. [%^H Hints Hash (Advanced)](#9-h-hints-hash-advanced) +10. [Special Blocks Lifecycle](#10-special-blocks-lifecycle) +11. [MRO @ISA Invalidation](#11-mro-isa-invalidation) +12. [In-Place Editing ($^I / -i)](#12-in-place-editing-i---i) +13. [-C Unicode Switch](#13--c-unicode-switch) +14. [stat/lstat _ Validation](#14-statlstat-_-validation) +15. [printf Array Flattening](#15-printf-array-flattening) +16. [Duplicate Named Captures](#16-duplicate-named-captures) +17. [Closures (Advanced Edge Cases)](#17-closures-advanced-edge-cases) +18. [comp/parser.t Issues](#18-comparsert-issues) +19. [64-bit Integer Ops](#19-64-bit-integer-ops) +20. [Regex Engine Gaps](#20-regex-engine-gaps) +21. [runperl/fresh_perl Infrastructure](#21-runperlfresh_perl-infrastructure) +22. [DESTROY Destructors](#22-destroy-destructors) +23. [Class Feature (Incomplete)](#23-class-feature-incomplete) +24. [Miscellaneous](#24-miscellaneous) + +--- + +## 1. Taint Tracking + +**Test:** `op/taint.t` (4/1065) +**Blocked tests:** ~1061 + +### What is needed + +Full taint tracking system: +- A taint flag on every `RuntimeScalar` value +- Propagation of taint through string/numeric operations +- Enforcement of taint checks on dangerous ops (`kill`, `exec`, `system`, backticks, `open` with pipes) +- "Insecure dependency in X while running with -T switch" error mechanism +- The `-T` command-line switch activating taint mode + +### Current state + +`RuntimeScalar.isTainted()` always returns `false`. The `$^X` variable is never marked tainted. The `Config.pm` does not set `taint_support` key, so the test does not skip. + +### Quick workaround + +Add `taint_support => ''` to `Config.pm` so the test skips entirely. + +### Difficulty: Very Hard (full implementation), Trivial (skip workaround) + +--- + +## 2. Format/Write System + +**Tests:** `op/write.t` (0/636), `comp/form_scope.t` (0/14), `uni/write.t` (0/8) +**Blocked tests:** ~658 + +### What is needed + +1. **Full expression evaluation in format argument lines** - `RuntimeFormat.evaluateExpression()` only handles simple global variable access (`$varName` -> `main::varName`). Lexical variables, expressions, method calls, ternary operators all produce ``. +2. **Format declarations inside subroutines** that close over lexical scope +3. **`*GLOB{FORMAT}` access** - extracting the FORMAT slot from a glob +4. **`~~` (fill-until-blank) repeat fields** +5. **`^` (chomp) fields** with `...` truncation +6. **Multi-expression argument lines** like `{ 'i' . 's', "time\n", $good, 'to' }` +7. **Special variables**: `$~` (format name), `$^` (header format), `$-` (lines remaining), `$=` (page length) +8. **Pagination and header support** +9. **`Tie::Scalar` module** may not be loadable (write.t uses it) + +### Key files + +- `src/main/java/org/perlonjava/runtime/RuntimeFormat.java` (evaluateExpression method) +- `src/main/java/org/perlonjava/operators/IOOperator.java` (formline/write) +- `src/main/java/org/perlonjava/parser/FormatParser.java` + +### Difficulty: Hard + +--- + +## 3. Regex Code Blocks (?{...}) + +**Tests:** `re/reg_eval_scope.t` (0/49), `re/subst.t` (184/281), `re/substT.t` (184/281), `re/subst_wamp.t` (184/281), `re/alpha_assertions.t` (2188/2320), `re/pat_advanced.t` (49/83), `comp/parser.t` (crashes at test ~97) +**Blocked tests:** ~500+ + +### What is needed + +1. **`(?{...})` code blocks** - Execute arbitrary Perl code during regex matching. Currently, `RegexPreprocessor.handleCodeBlock()` only handles simple constants. For anything else, it throws `PerlJavaUnimplementedException`. +2. **`(??{...})` recursive/dynamic regex patterns** - Explicitly throws "not implemented" +3. **`local` within `(?{})` blocks** - proper dynamic scoping with backtracking undo +4. **Lexical variable scoping inside `(?{})` blocks** +5. **`use re 'eval'`** - enabling runtime code evaluation in interpolated patterns +6. **`$^R` (last code block result)** - partially implemented but only for constant-folded blocks + +### Key files + +- `src/main/java/org/perlonjava/regex/RegexPreprocessor.java` (handleCodeBlock) +- `src/main/java/org/perlonjava/parser/StringSegmentParser.java` (line 723, parseBlock) +- `src/main/java/org/perlonjava/runtime/RuntimeRegex.java` + +### Note + +Making `(?{UNIMPLEMENTED_CODE_BLOCK})` non-fatal (replace with `(?:)` no-op) would unblock many tests that use `(?{...})` in non-critical parts. This would give ~100 more tests in parser.t alone, plus many in subst.t variants. + +### Difficulty: Very Hard (full), Medium (non-fatal workaround) + +--- + +## 4. delete local Construct + +**Test:** `op/local.t` (0/319 - crashes before any output) +**Blocked tests:** ~319 + +### What is needed + +The `delete local` syntax: +```perl +delete local $hash{key}; # Save value, delete, restore on scope exit +delete local $array[idx]; +``` + +Currently: +- The parser (`parseDelete` in `OperatorParser.java` line 549) does NOT check for a `local` keyword after `delete` +- No `DeleteLocalNode` or compilation path exists +- The test crashes at line 164 with "Not implemented: delete with dynamic patterns" + +### Implementation plan + +1. **Parser**: `parseDelete` must check for `local` keyword and produce a new AST node +2. **Compiler**: Emit save-state, delete, and scope-exit restore +3. **Runtime**: Use existing `dynamicSaveState`/`dynamicRestoreState` mechanism on hash/array elements + +### Note + +Many tests before line 161 in local.t don't use `delete local`. If the parser didn't crash, ~100+ tests might pass. + +### Difficulty: Moderate + +--- + +## 5. \(LIST) Reference Creation + +**Test:** `op/ref.t` (97/265) +**Blocked tests:** ~155 + +### What is needed + +`\(LIST)` should return a list of references to each element. E.g., `\(@array)` returns refs to each element; `\($a, $b)` returns `(\$a, \$b)`. + +### Root cause + +`RuntimeList.flattenElements()` (line 424) does not handle `PerlRange` objects. When `\(1..3)` is evaluated, the PerlRange passes through unflattened, then `createReference()` throws "Can't create reference to list". + +### Fix + +Add PerlRange handling to `flattenElements()` (~5 lines): +```java +} else if (element instanceof PerlRange range) { + for (RuntimeScalar scalar : range) { + result.elements.add(scalar); + } +} +``` + +Also need to update `InlineOpcodeHandler.executeCreateRef()` for the bytecode interpreter path. + +### Key files + +- `src/main/java/org/perlonjava/runtime/RuntimeList.java` (flattenElements, createListReference) +- `src/main/java/org/perlonjava/runtime/PerlRange.java` +- `src/main/java/org/perlonjava/codegen/EmitOperator.java` (handleCreateReference) + +### Difficulty: Easy (this is actually a quick fix, ~5 lines) + +--- + +## 6. Tied Scalar Code Deref + +**Test:** `op/tie_fetch_count.t` (64/343) +**Blocked tests:** ~279 + +### What is needed + +`RuntimeCode.apply()` does not handle `TIED_SCALAR` type. When `$tied_var` holds a CODE ref and you call `&$tied_var`, the code falls through to "Not a CODE reference" error instead of calling `tiedFetch()` first. + +### Fix + +Add `TIED_SCALAR` handling in all three `RuntimeCode.apply()` overloads: +```java +if (runtimeScalar.type == RuntimeScalarType.TIED_SCALAR) { + return apply(runtimeScalar.tiedFetch(), subroutineName, args, callContext); +} +``` + +Also fix `RuntimeScalar.codeDerefNonStrict()` and `globDeref()` for the same pattern. + +### Key files + +- `src/main/java/org/perlonjava/runtime/RuntimeCode.java` (three apply overloads) +- `src/main/java/org/perlonjava/runtime/RuntimeScalar.java` (codeDerefNonStrict, globDeref) + +### Difficulty: Easy (this is actually a quick fix, ~6 lines across 3 methods) + +--- + +## 7. caller() Extended Fields + +**Test:** `op/caller.t` (46/112) +**Blocked tests:** ~66 + +### What is needed + +The `CallerInfo` record in `CallerStack.java` only stores `(packageName, filename, line)`. Missing fields: + +| Index | Field | Status | Difficulty | +|-------|-------|--------|------------| +| [5] | wantarray | Returns `undef` always | Medium - record call context in CallerStack | +| [6] | evaltext | Returns `undef` always | Medium - capture eval string at compile time | +| [7] | is_require | Returns `undef` always | Easy - add boolean flag | +| [10] | hinthash (%^H) | Returns `undef` always | Medium-Hard - snapshot %^H at compile time | + +### Key files + +- `src/main/java/org/perlonjava/runtime/CallerStack.java` (CallerInfo record) +- `src/main/java/org/perlonjava/runtime/RuntimeCode.java` (callerWithSub, lines 1631-1779) + +### Difficulty: Medium-Hard overall + +--- + +## 8. Attribute System + +**Test:** `op/attrs.t` (44/126), `op/attrproto.t` (3/52), `op/attrhand.t` (0/4), `uni/attrs.t` (5/31) +**Blocked tests:** ~160+ + +### What is needed + +1. **`attributes.pm` module** - entirely missing. Need to create `src/main/perl/lib/attributes.pm` +2. **`MODIFY_CODE_ATTRIBUTES` / `MODIFY_SCALAR_ATTRIBUTES` / `FETCH_CODE_ATTRIBUTES` callbacks** - user-definable hooks called when attributes are applied. Parser collects attributes into `RuntimeCode.attributes` but never dispatches these callbacks. +3. **`attributes::get()`** - retrieve attributes from a reference +4. **Variable attributes on `my` declarations** - `my $x : TieLoop = $i` +5. **`-lvalue`/`-const` attribute removal** with `-` prefix + +### Key files + +- `src/main/java/org/perlonjava/parser/OperatorParser.java` (attribute parsing) +- Need to create: `src/main/perl/lib/attributes.pm` + +### Difficulty: Medium-Hard + +--- + +## 9. %^H Hints Hash (Advanced) + +**Test:** `comp/hints.t` (23/31) +**Blocked tests:** ~8 + +### What is needed + +Basic `%^H` set/get/scope works (tests 1-8 pass). Missing: +1. **%^H propagation into `eval ""`** - eval should inherit compile-time %^H snapshot +2. **Tied `%^H`** - tie support on the special variable +3. **DESTROY during `%^H` freeing** - requires DESTROY support +4. **CHECK-time %^H state** - proper lifecycle during CHECK phase + +### Difficulty: Medium-Hard + +--- + +## 10. Special Blocks Lifecycle + +**Test:** `op/blocks.t` (9/26) +**Blocked tests:** ~17 + +### What is needed + +All 26 tests use `fresh_perl_is` (subprocess). Missing: +1. **Full block ordering**: BEGIN -> UNITCHECK -> CHECK -> INIT -> END, including blocks inside `eval`, regex `(?{...})`, nested compilations +2. **`exit` from special blocks**: `BEGIN{exit 0}`, `CHECK{exit 0}` should defer, not exit immediately +3. **`die` from special blocks**: END blocks must still run +4. **Blocks inside `(?{...})`** (depends on regex code blocks) +5. **Prototype/attribute warnings on BEGIN** - "Prototype on BEGIN block ignored" + +### Key files + +- `src/main/java/org/perlonjava/runtime/SpecialBlock.java` + +### Difficulty: Medium-Hard + +--- + +## 11. MRO @ISA Invalidation + +**Tests:** `mro/isarev.t` (7/24), `mro/isarev_utf8.t` (7/24), `mro/pkg_gen.t` (3/7), `mro/pkg_gen_utf8.t` (3/7), `mro/method_caching.t` (31/36) +**Blocked tests:** ~50+ + +### What is needed + +`mro::get_isarev` builds the reverse ISA cache lazily but **never invalidates it** when: +- Stash globs are aliased (`*Tike:: = *Dog::`) +- Stashes are deleted (`delete $::{"Dog::"}`) +- Globs are undefined (`undef *glob`) +- `%Package:: = ()` list assignment + +Perl's MRO tracks these changes in real-time through magic on `@ISA` and stash entries. + +### Key files + +- `src/main/java/org/perlonjava/mro/Mro.java` (buildIsaRevCache, lines 277-323) +- `src/main/java/org/perlonjava/runtime/GlobalVariable.java` (stash operations) + +### Difficulty: Hard + +--- + +## 12. In-Place Editing ($^I / -i) + +**Tests:** `io/argv.t` (6/53), `io/nargv.t` (0/7), `run/switches.t` (67/142), `io/inplace.t` (6/8) +**Blocked tests:** ~120+ + +### What is needed + +`DiamondIO.java` has the in-place editing framework but needs: +1. **Runtime `$^I` lifecycle** - proper `$ARGV`, `$.`, ARGVOUT management as files transition +2. **`local *ARGV` support** - DiamondIO uses static state; needs per-glob state for `local` to save/restore +3. **File permission preservation** during in-place editing (chmod bits) +4. **Error handling** when backup rename fails +5. **Warning on `-i` without file arguments** +6. **`-i` switch** in command-line argument processing + +### Key files + +- `src/main/java/org/perlonjava/runtime/DiamondIO.java` +- `src/main/java/org/perlonjava/runtime/ArgumentParser.java` + +### Difficulty: Hard + +--- + +## 13. -C Unicode Switch + +**Test:** `run/switchC.t` (2/15) +**Blocked tests:** ~13 + +### What is needed + +The `-C` flags are **parsed** in `ArgumentParser.java` (lines 469-563) and **stored** in `CompilerOptions.java` (lines 77-84) but **NEVER APPLIED** anywhere in the runtime. The flags (`unicodeStdin`, `unicodeStdout`, `unicodeStderr`, `unicodeInput`, `unicodeOutput`, `unicodeArgs`) need to be applied: +- Call `binmode` with `:encoding(UTF-8)` on STDIN/STDOUT/STDERR during initialization +- Set open pragma defaults for file I/O +- Decode `@ARGV` as UTF-8 when `-CA` is set + +### Difficulty: Medium + +--- + +## 14. stat/lstat _ Validation + +**Test:** `op/stat.t` (64/111) +**Blocked tests:** ~47 + +### What is needed + +1. **`lstat _` validation** - `Stat.lstatLastHandle()` does NOT validate `lastStatWasLstat`. Should throw "The stat preceding lstat() wasn't an lstat" when the previous call was `stat` not `lstat`. `FileTestOperator.java` already has this check for `-l _` but `Stat.java` doesn't. +2. **`lstat *FOO{IO}`** - lstat on IO reference +3. **`stat *DIR{IO}`** - stat on directory handles +4. **`-T _` breaking the stat buffer** +5. **stat on filenames with `\0`** + +### Key files + +- `src/main/java/org/perlonjava/operators/Stat.java` (lstatLastHandle) +- `src/main/java/org/perlonjava/operators/FileTestOperator.java` + +### Difficulty: Easy-Medium (lstat validation is a 1-line fix; other items are moderate) + +--- + +## 15. printf Array Flattening + +**Test:** `io/print.t` (8/24) +**Blocked tests:** ~16 + +### What is needed + +When `printf @array` is called, the RuntimeArray argument is not flattened before extracting the format string. `IOOperator.printf()` calls `list.add(args[i])` which adds the array as-is; then `removeFirst()` expects a RuntimeScalar but gets a RuntimeArray. + +Additional issues: +- Null bytes in `$\` (output record separator) +- `%n` format specifier (writes char count via substr) +- `printf +()` (empty list) + +### Key files + +- `src/main/java/org/perlonjava/operators/IOOperator.java` (printf method, line 2386) + +### Difficulty: Medium + +--- + +## 16. Duplicate Named Captures + +**Test:** `re/reg_nc_tie.t` (1/37) +**Blocked tests:** ~36 + +### What is needed + +1. **Duplicate named capture groups** - `CaptureNameEncoder.java` line 15 says "Duplicate capture group names not supported". In Perl, `(?.)(?.)` is valid; `$+{a}` returns the first match. Java's `Matcher.group("a")` returns the LAST match. +2. **`Tie::Hash::NamedCapture` module** - needed for direct `FETCH()` calls and `tied %+` +3. **`%+`/`%-` first-vs-last semantics** - `HashSpecialVariable.get()` must return the correct match + +### Key files + +- `src/main/java/org/perlonjava/regex/CaptureNameEncoder.java` +- `src/main/java/org/perlonjava/regex/RegexPreprocessor.java` (handleNamedCapture) +- `src/main/java/org/perlonjava/runtime/HashSpecialVariable.java` + +### Difficulty: Hard + +--- + +## 17. Closures (Advanced Edge Cases) + +**Test:** `op/closure.t` (246/266) +**Blocked tests:** ~20 + +### What is needed + +The first 246 tests (basic closures, combinatorial tests, eval-in-closures) pass. Remaining failures: +1. **Format + closure interaction** - formats closing over lexicals (depends on format system) +2. **`PL_cv_has_eval` cloneability** - anon subs containing `eval '1'` should be cloneable (get different code refs) +3. **DESTROY in closures** - tests that closures close over variables, not entire subs +4. **`my $x if @_`** (conditional my) - stale lexical variable edge cases +5. **Source filter + closure interaction** - requires `Filter::Util::Call` module +6. **Weak reference + closure leak** - requires `builtin::weaken` +7. **Several tests use `fresh_perl_is`** (subprocess tests) + +### Difficulty: Medium-Hard (most depend on DESTROY or format system) + +--- + +## 18. comp/parser.t Issues + +**Test:** `comp/parser.t` (63/195) +**Blocked tests:** ~132 + +### What is needed + +**Crash at test ~97**: `(?{format...write})` regex code block. Non-constant `(?{...})` throws fatal `PerlJavaUnimplementedException` outside eval. + +**Workaround**: Make `(?{UNIMPLEMENTED_CODE_BLOCK})` non-fatal (replace with `(?:)` no-op in `RegexPreprocessor.java`). This alone would unlock ~90+ tests. + +**Other failures** (pre-crash): +1. `${}` accepted as valid (should be syntax error) - `Variable.java` lines 679-682 +2. `#line` directive handling - ~40 tests for `#line N "file"` directive to set `__FILE__`/`__LINE__` +3. VCS conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) not detected as errors +4. Identifier length validation in various sigil contexts +5. Sub declaration after compilation error should be ignored + +### Key files + +- `src/main/java/org/perlonjava/regex/RegexPreprocessor.java` +- `src/main/java/org/perlonjava/parser/Variable.java` +- Lexer/parser for `#line` directives + +### Difficulty: Medium (workaround), Hard (full #line support) + +--- + +## 19. 64-bit Integer Ops + +**Test:** `op/64bitint.t` (161/435) +**Blocked tests:** ~274 + +### What is needed + +Many 64-bit integer operations produce incorrect results, likely due to: +- Overflow handling differences between Perl's IV/UV types and Java's long +- Unsigned integer semantics (Java has no unsigned long) +- Hex/octal literal parsing at 64-bit boundaries +- Bit shift operations on large values + +### Difficulty: Medium-Hard + +--- + +## 20. Regex Engine Gaps + +**Tests:** `re/regexp.t` (1803/2210), `re/regexp_noamp.t` (1810/2210), `re/regexp_notrie.t` (1803/2210), `re/regexp_qr.t` (1803/2210), etc., `re/reg_mesg.t` (1676/2509), `re/charset.t` (5462/5552), `re/pat.t` (1064/1298) +**Blocked tests:** ~3000+ across all regex test files + +### What is needed + +Major regex features still missing or incomplete: +1. **`(*sr:...)` / `(*script_run:...)` / `(*asr:...)` / `(*atomic_script_run:...)`** - script run assertions. Completely missing from `RegexPreprocessor.java`. Would require custom `ScriptRunChecker` using ICU4J's `UScript.getScriptExtensions()`. +2. **Octal escape parsing** - `\345` in patterns parsed as backreference `\3` + `45` instead of octal +3. **`(?{...})` / `(??{...})`** - code blocks (see item #3 above) +4. **Error message format differences** in `re/reg_mesg.t` +5. **Various Unicode property edge cases** in `re/charset.t` + +### Difficulty: Hard to Very Hard + +--- + +## 21. runperl/fresh_perl Infrastructure + +**Tests affected:** `op/blocks.t`, `op/closure.t` (partial), `run/fresh_perl.t` (63/91), `run/switches.t`, `run/todo.t` (14/26), many others +**Blocked tests:** ~200+ across various files + +### What is needed + +Many tests use `fresh_perl_is`/`fresh_perl_like` which spawn a PerlOnJava subprocess via `runperl()`. Issues: +1. **Subprocess spawning** sometimes hangs (op/sort.t, op/loopctl.t timeout at 300s) +2. **Exit code handling** may differ +3. **STDERR capture** may be incomplete +4. **-w, -W, -X switches** in subprocess mode + +This is cross-cutting - fixing subprocess infrastructure would improve many test files. + +### Difficulty: Medium + +--- + +## 22. DESTROY Destructors + +**Tests affected:** Many (closure.t, hints.t, tie.t, bless.t, ref.t, etc.) +**Blocked tests:** ~100+ across various files + +### What is needed + +Object destructors (`DESTROY` method) are never called. This is documented as a known unimplemented feature. Impact: +- Modules using cleanup patterns (Moo's `no Moo`, DEMOLISH) +- Tests that verify DESTROY is called during scope exit +- Tests that verify DESTROY ordering +- Circular reference cleanup +- Tied variable cleanup + +### Difficulty: Very Hard (fundamental runtime change) + +--- + +## 23. Class Feature (Incomplete) + +**Tests:** `class/accessor.t` (11/25), `class/construct.t` (8/10), `class/gh22169.t` (7/8), `class/inherit.t` (15/18), `class/phasers.t` (3/4), `class/utf8.t` (3/4) +**Blocked tests:** ~30 + +### What is needed + +The Perl `class` feature (added in Perl 5.38) is partially implemented. Missing: +- Some accessor patterns +- Inheritance edge cases +- Phase block interactions within classes +- UTF-8 class name edge cases + +### Difficulty: Medium + +--- + +## 24. Miscellaneous + +### op/override.t (0/0 crash) +**Missing:** `pop(OverridenPop->foo())` - parser error "pop requires array variable". The parser doesn't allow method call results as arguments to `pop`. **Difficulty: Medium** + +### op/dor.t (29/34) +**Failing tests:** +- `getc // ...`, `readlink // ...`, `umask // ...` don't compile with `//` +- Unterminated search pattern error messages differ +**Difficulty: Easy-Medium** + +### op/yadayada.t (31/34 incomplete) +**Issue:** Tests 32-34 output `1ok` instead of `ok` (extra `1` from print statement). TAP parser sees malformed output. **Difficulty: Easy** (output buffering issue) + +### op/signatures.t (643/908) +**Missing:** Various signature edge cases, `:prototype()` attribute interactions. **Difficulty: Medium** + +### op/tr.t (277/318), op/tr_latin1.t (1/2) +**Missing:** Various transliteration edge cases, Latin-1 specific behavior. **Difficulty: Medium** + +### op/substr.t (356/400) +**Missing:** 4-arg substr as lvalue, various edge cases. **Difficulty: Medium** + +### op/gv.t (198/266) +**Missing:** Glob/stash manipulation edge cases, `*glob{THING}` access for all slot types. **Difficulty: Medium** + +### op/eval.t (152/173) +**Missing:** Various eval edge cases, error propagation. **Difficulty: Medium** + +### op/tie.t (49/95) +**Missing:** Various tie edge cases, DESTROY interactions. **Difficulty: Medium-Hard** + +### comp/use.t (46/87) +**Missing:** `use VERSION` edge cases, `use` with import lists. **Difficulty: Medium** + +--- + +## Priority Ranking by Impact + +### Tier 1: Highest impact (1000+ tests unlocked) +| Feature | Tests blocked | Difficulty | +|---------|--------------|------------| +| Taint skip workaround | 1061 | Trivial | +| Regex code blocks (non-fatal workaround) | 500+ | Medium | +| Format/write system | 658 | Hard | + +### Tier 2: High impact (100-500 tests) +| Feature | Tests blocked | Difficulty | +|---------|--------------|------------| +| delete local | 319 | Moderate | +| Tied scalar code deref | 279 | Easy | +| \(LIST) reference creation | 155 | Easy | +| comp/parser.t (?{} non-fatal) | 132 | Medium | +| In-place editing ($^I) | 120+ | Hard | +| 64-bit integer ops | 274 | Medium-Hard | + +### Tier 3: Medium impact (30-100 tests) +| Feature | Tests blocked | Difficulty | +|---------|--------------|------------| +| caller() extended fields | 66 | Medium-Hard | +| Attribute system | 160+ | Medium-Hard | +| MRO @ISA invalidation | 50+ | Hard | +| stat/lstat validation | 47 | Easy-Medium | +| Duplicate named captures | 36 | Hard | +| Class feature completion | 30 | Medium | + +### Tier 4: Lower impact but easy +| Feature | Tests blocked | Difficulty | +|---------|--------------|------------| +| -C unicode switch | 13 | Medium | +| printf array flattening | 16 | Medium | +| Closures (edge cases) | 20 | Medium-Hard | +| %^H hints (advanced) | 8 | Medium-Hard | +| Special blocks lifecycle | 17 | Medium-Hard | + +--- + +## Recommended Implementation Order (effort vs. impact) + +1. **Taint skip** (Trivial) - 1061 tests +2. **\(LIST) flattenElements fix** (Easy, ~5 lines) - 155 tests +3. **Tied scalar code deref** (Easy, ~6 lines) - 279 tests +4. **(?{...}) non-fatal workaround** (Medium) - 500+ tests +5. **stat/lstat _ validation** (Easy) - ~7 tests + unblocks others +6. **delete local** (Moderate) - 319 tests +7. **printf array flattening** (Medium) - 16 tests +8. **-C switch application** (Medium) - 13 tests +9. **caller() extended fields** (Medium-Hard) - 66 tests +10. **attributes.pm module** (Medium-Hard) - 160+ tests diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 621815f22..5e56d4de8 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "a84fe5d2c"; + public static final String gitCommitId = "dc20694ab"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). From 80afde7682ce29e5aff8f9cccb0b35e30af7170b Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Wed, 1 Apr 2026 13:57:47 +0200 Subject: [PATCH 18/19] Fix sort comparator control flow detection and -0/-l switch parsing Three fixes: 1. Sort comparator goto/last/next/redo detection: Check the return value (RuntimeControlFlowList) from apply() instead of the dead registry check. Now correctly throws pseudo block errors for control flow in sort. 2. Interpreter goto LABEL infinite loop: Use GOTO_DYNAMIC for unresolved forward gotos instead of GOTO with PC 0. Handles both forward references and non-local gotos to outer scopes correctly. 3. ArgumentParser -0/-l switch fix: Changed break to return index for switches that consume the rest of the argument string. Results: sort.t 0/0->168/206, switches.t 67->72, uni/goto.t 2/4->4/4 Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../org/perlonjava/app/cli/ArgumentParser.java | 6 ++++-- .../backend/bytecode/CompileOperator.java | 13 ++++++++++++- .../java/org/perlonjava/core/Configuration.java | 2 +- .../runtime/operators/ListOperators.java | 16 ++++++++++++++++ 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/perlonjava/app/cli/ArgumentParser.java b/src/main/java/org/perlonjava/app/cli/ArgumentParser.java index 96f97dec2..6b77178ce 100644 --- a/src/main/java/org/perlonjava/app/cli/ArgumentParser.java +++ b/src/main/java/org/perlonjava/app/cli/ArgumentParser.java @@ -372,15 +372,17 @@ private static int processClusteredSwitches(String[] args, CompilerOptions parse return index; case '0': // Handle input record separator specified with -0 + // Return immediately as the handler consumes the rest of the argument (e.g., -0777) index = handleInputRecordSeparator(args, parsedArgs, index, j, arg); - break; + return index; case 'g': parsedArgs.inputRecordSeparator = null; break; case 'l': // Handle automatic line-ending processing + // Return immediately as the handler consumes the rest of the argument (e.g., -l012) index = handleLineEndingProcessing(args, parsedArgs, index, j, arg); - break; + return index; case 'e': // Handle inline code specified with -e index = handleInlineCode(args, parsedArgs, index, j, arg); diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index a44cfe6b9..6ae78e040 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -1477,7 +1477,18 @@ private static void visitGoto(BytecodeCompiler bc, OperatorNode node) { } Integer targetPc = bc.gotoLabelPcs.get(labelStr); if (targetPc != null) { bc.emit(Opcodes.GOTO); bc.emitInt(targetPc); } - else { bc.emit(Opcodes.GOTO); int patchPc = bc.bytecode.size(); bc.emitInt(0); bc.pendingGotos.add(new Object[]{patchPc, labelStr}); } + else { + // Label not yet seen - use GOTO_DYNAMIC which checks code.gotoLabelPcs at runtime + // (populated with all labels after compilation) and creates a GOTO marker for non-local gotos. + // This handles both forward references within the same scope and non-local gotos to outer scopes. + int rd = bc.allocateOutputRegister(); + int labelIdx = bc.addToStringPool(labelStr); + bc.emit(Opcodes.LOAD_STRING); + bc.emitReg(rd); + bc.emit(labelIdx); + bc.emit(Opcodes.GOTO_DYNAMIC); + bc.emit(rd); + } bc.lastResultReg = -1; } } diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 5e56d4de8..640e8e94c 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "dc20694ab"; + public static final String gitCommitId = "c4e5002c8"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). diff --git a/src/main/java/org/perlonjava/runtime/operators/ListOperators.java b/src/main/java/org/perlonjava/runtime/operators/ListOperators.java index e5e947d31..f83c32b04 100644 --- a/src/main/java/org/perlonjava/runtime/operators/ListOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/ListOperators.java @@ -132,11 +132,27 @@ public static RuntimeList sort(RuntimeList runtimeList, RuntimeScalar perlCompar // Apply the Perl comparator subroutine with the arguments RuntimeList result = RuntimeCode.apply(finalComparator, comparatorArgs, RuntimeContextType.SCALAR); + // Check for control flow markers (goto/last/next/redo) that tried to escape the sort block. + // The marker propagates as the return value (RuntimeControlFlowList), not via the registry. + if (result.isNonLocalGoto()) { + ControlFlowType cfType = ((RuntimeControlFlowList) result).getControlFlowType(); + String keyword = switch (cfType) { + case GOTO, TAILCALL -> "goto"; + case LAST -> "last"; + case NEXT -> "next"; + case REDO -> "redo"; + }; + throw new PerlCompilerException("Can't \"" + keyword + "\" out of a pseudo block"); + } + // Retrieve the comparison result and return it as an integer return result.getFirst().getInt(); } catch (PerlExitException e) { // exit() should propagate immediately - don't wrap it throw e; + } catch (PerlCompilerException e) { + // Propagate Perl errors directly so eval {} can catch them + throw e; } catch (Exception e) { // Wrap any exceptions thrown by the comparator in a RuntimeException throw new RuntimeException(e); From f7904c5e59b8831d5c45d3d6f7027d11d197ee53 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Wed, 1 Apr 2026 14:15:11 +0200 Subject: [PATCH 19/19] Fix -0/-l switch parsing to only consume octal digits in clustered args The previous fix (return index) broke clustered switches like -le, -0e, -lpe where -l/-0 was combined with other switches. The handlers consumed the entire rest of the arg string (including non-digit chars like 'e'), preventing subsequent switches from being processed. Now -0 and -l scan only valid octal digits (or hex with 0x prefix) from the current arg inline, advance j past the consumed digits, and break to continue processing remaining switches. This correctly handles: - -le 'code': -l (no digits), then -e processes code - -0777: -0 consumes '777' as octal, no remaining switches - -l012e 'code': -l consumes '012', then -e processes code - -0e 'code': -0 (no digits), then -e processes code Restores: exec.t 27/41, dup.t 25/29, open.t 187/216, switcht.t 9/13, term.t 7/7, srand.t 6/10, script.t 3/3. switches.t stays at 72/142. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../perlonjava/app/cli/ArgumentParser.java | 68 ++++++++++++++++--- .../org/perlonjava/core/Configuration.java | 2 +- 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/perlonjava/app/cli/ArgumentParser.java b/src/main/java/org/perlonjava/app/cli/ArgumentParser.java index 6b77178ce..b80cd942e 100644 --- a/src/main/java/org/perlonjava/app/cli/ArgumentParser.java +++ b/src/main/java/org/perlonjava/app/cli/ArgumentParser.java @@ -370,19 +370,67 @@ private static int processClusteredSwitches(String[] args, CompilerOptions parse parsedArgs.autoSplit = true; // -F implicitly sets -a parsedArgs.processOnly = true; // -F implicitly sets -n return index; - case '0': - // Handle input record separator specified with -0 - // Return immediately as the handler consumes the rest of the argument (e.g., -0777) - index = handleInputRecordSeparator(args, parsedArgs, index, j, arg); - return index; + case '0': { + // Handle input record separator: -0[octal/hex] + // Only consume octal digits (0-7) or hex (0xHH) from the current arg, + // allowing subsequent switches in the same clustered arg (e.g., -0e 'code') + int scanEnd0 = j + 1; + boolean isHex0 = false; + if (scanEnd0 + 1 < arg.length() && arg.charAt(scanEnd0) == '0' && + (arg.charAt(scanEnd0 + 1) == 'x' || arg.charAt(scanEnd0 + 1) == 'X')) { + isHex0 = true; + scanEnd0 += 2; + while (scanEnd0 < arg.length() && "0123456789abcdefABCDEF".indexOf(arg.charAt(scanEnd0)) >= 0) scanEnd0++; + } else { + while (scanEnd0 < arg.length() && arg.charAt(scanEnd0) >= '0' && arg.charAt(scanEnd0) <= '7') scanEnd0++; + } + String sepValue0 = arg.substring(j + 1, scanEnd0); + if (sepValue0.isEmpty()) { + parsedArgs.inputRecordSeparator = "\0"; + } else { + try { + int sepInt0; + if (isHex0) { + sepInt0 = Integer.parseInt(sepValue0.substring(2), 16); + } else { + sepInt0 = Integer.parseInt(sepValue0, 8); + } + if (sepInt0 == 0) parsedArgs.inputRecordSeparator = "\n\n"; + else if (sepInt0 >= 0400) parsedArgs.inputRecordSeparator = null; + else parsedArgs.inputRecordSeparator = Character.toString((char) sepInt0); + } catch (NumberFormatException e) { + System.err.println("Invalid input record separator: " + sepValue0); + System.exit(1); + } + } + j = scanEnd0 - 1; // advance past consumed digits (-1 for loop j++) + break; + } case 'g': parsedArgs.inputRecordSeparator = null; break; - case 'l': - // Handle automatic line-ending processing - // Return immediately as the handler consumes the rest of the argument (e.g., -l012) - index = handleLineEndingProcessing(args, parsedArgs, index, j, arg); - return index; + case 'l': { + // Handle automatic line-ending processing: -l[octnum] + // Only consume octal digits (0-7) from the current arg, + // allowing subsequent switches in the same clustered arg (e.g., -le 'code') + parsedArgs.lineEndingProcessing = true; + int scanEndL = j + 1; + while (scanEndL < arg.length() && arg.charAt(scanEndL) >= '0' && arg.charAt(scanEndL) <= '7') scanEndL++; + String octStrL = arg.substring(j + 1, scanEndL); + if (octStrL.isEmpty()) { + parsedArgs.outputRecordSeparator = parsedArgs.inputRecordSeparator; + } else { + try { + int sepIntL = Integer.parseInt(octStrL, 8); + parsedArgs.outputRecordSeparator = Character.toString((char) sepIntL); + } catch (NumberFormatException e) { + System.err.println("Invalid output record separator: " + octStrL); + System.exit(1); + } + } + j = scanEndL - 1; // advance past consumed octal digits (-1 for loop j++) + break; + } case 'e': // Handle inline code specified with -e index = handleInlineCode(args, parsedArgs, index, j, arg); diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 640e8e94c..13b46fbf6 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "c4e5002c8"; + public static final String gitCommitId = "80afde768"; /** * Git commit date of the build (ISO format: YYYY-MM-DD).