diff --git a/dev/design/test_pass_rate_improvement_plan.md b/dev/design/test_pass_rate_improvement_plan.md index 00e5cc812..71973a1e9 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,192 @@ Remaining op/state.t failures (15 + 14 blocked): ### Next Steps -1. Continue Phase 1 quick wins (items 1.1-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/test-pass-rate-quick-wins` + +| 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 + +#### 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()`. + +#### 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 | +| 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; 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"; 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) + +**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. + +#### 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 `${< ''` 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/app/cli/ArgumentParser.java b/src/main/java/org/perlonjava/app/cli/ArgumentParser.java index ba94ddd38..b80cd942e 100644 --- a/src/main/java/org/perlonjava/app/cli/ArgumentParser.java +++ b/src/main/java/org/perlonjava/app/cli/ArgumentParser.java @@ -370,17 +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 - index = handleInputRecordSeparator(args, parsedArgs, index, j, arg); + 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 - index = handleLineEndingProcessing(args, parsedArgs, index, j, arg); + 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); @@ -448,7 +498,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 +1033,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/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 1b7c55680..7f22497c2 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 @@ -4179,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; @@ -5400,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 @@ -5524,15 +5551,35 @@ Map 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 4f7023766..fb6f7b156 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) { @@ -1076,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); } @@ -1592,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); } @@ -2542,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 7e43c3b24..6ae78e040 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); @@ -1436,10 +1463,32 @@ 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}); } + 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/backend/bytecode/Disassemble.java b/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java index f35d12c38..5451178a4 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++]; @@ -1835,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++]; @@ -1860,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 c25af477b..f94bc41ff 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java @@ -2054,6 +2054,44 @@ 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; + + // 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/backend/jvm/EmitControlFlow.java b/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java index 42b201648..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); } } @@ -495,7 +498,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 7f337cb9f..13b46fbf6 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 = "9cd24576f"; + public static final String gitCommitId = "80afde768"; /** * 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/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/OperatorParser.java b/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java index bf382d8ec..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 @@ -859,8 +861,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); } 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) { 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 ) diff --git a/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java b/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java index d9a1c9b27..7792fe5b0 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,9 @@ 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; + 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 @@ -747,8 +751,52 @@ 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); + // Java-registered methods (via registerMethod) have isStatic=true and methodHandle set + isBuiltinSub = existingCode.isStatic && existingCode.methodHandle != null; + } } - + + // Emit "Prototype mismatch" and "Subroutine redefined" warnings + // 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); + 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); 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/ListOperators.java b/src/main/java/org/perlonjava/runtime/operators/ListOperators.java index 2cf89ad70..f83c32b04 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,14 +122,37 @@ 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); + // 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); 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/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/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"); 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/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); 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/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(); } 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); 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; 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 }