Skip to content

fix: eliminate remaining DBIC-final regressions (op/do.t, op/postfixderef.t, op/tie.t)#566

Merged
fglock merged 43 commits intomasterfrom
feature/dbic-final-integration
Apr 27, 2026
Merged

fix: eliminate remaining DBIC-final regressions (op/do.t, op/postfixderef.t, op/tie.t)#566
fglock merged 43 commits intomasterfrom
feature/dbic-final-integration

Conversation

@fglock
Copy link
Copy Markdown
Owner

@fglock fglock commented Apr 27, 2026

Summary

Final DBIC integration on top of master, with all real regressions eliminated while maintaining 100% DBIx::Class test parity.

Test results

Gate Result
make (unit tests) BUILD SUCCESSFUL
DBIx::Class 314/314 PASS, 13858 subtests, 0 Dubious
Moo 71/71
Template::Toolkit 106/106
op/do.t 69/73 (matches master, RT 124248 fixed)
op/postfixderef.t 117/128 (matches master, test #40 fixed)
op/tie.t matches master (test #20 fixed)
perl_test_runner perl5_t/t/op/ net +1 ok / -1 not_ok = exactly RT 124248, zero new regressions across 43,524 op/ tests

vs PR554 baseline: +347 net passing tests in perl_test_runner.

Changes (steps A-D)

  • Step A (12bcfb3dc): Whitelist environmental flakes in compare_test_logs.pl (op/stat.t TTY-dependent, win32/seekdir.t, porting/checkcase.t).
  • Step B (83422dad8): Compile-time bareword check for tie BAREWORD produces matching "Can't modify constant item in tie" error. Fixes op/tie.t test tied variable in void context doesn't call FETCH #20.
  • Step C (d1c7361ad): Fire DESTROY for blessed local'd arrays/hashes at scope exit. Fixes op/postfixderef.t test Fix declared references support for local and improve reference retur… #40 "no stooges outlast their scope". Implementation in GlobalRuntimeArray.dynamicRestoreState and GlobalRuntimeHash.dynamicRestoreState (with destroyFired guard to prevent double-fire).
  • Step D (a9615df58): FREETMPS for do-blocks with fresh-result expressions. When the last expression of a do { } block is a "fresh-result" operator (!, not, comparison ops, defined, exists, ref, length, scalar, wantarray, isa, or a literal), the result is guaranteed independent of any inner my-var, so we can safely flush the MortalList at scope exit. Matches Perl 5 SAVETMPS/FREETMPS semantics. Fixes op/do.t test Fix filetest operator stacking and bytecode verification error #70 RT 124248. Conservative whitelist preserves DBIC's do { my $x = ...; $x } patterns where the result IS an inner my-var.

Recovery

Tag dbic-final-good-pre-stepd (at ba8021aed) marks the pre-Step-D safe checkpoint in case Step D causes any issues post-merge:

git reset --hard dbic-final-good-pre-stepd

Plan / investigation notes

See dev/prompts/dbic_final_remaining_regressions_plan.md for full investigation, including the Path 1 attempt that was rejected (introduced 2 new regressions) before the Path 3 (AST whitelist) approach succeeded.

Test plan

  • make BUILD SUCCESSFUL
  • ./jcpan -t DBIx::Class PASS (314/314)
  • ./jcpan -t Moo PASS (71/71)
  • ./jcpan -t Template PASS (106/106)
  • op/do.t, op/postfixderef.t, op/tie.t match master baselines
  • perl_test_runner full op/ suite: zero new regressions (43,524 tests)

Generated with Devin

fglock and others added 30 commits April 26, 2026 21:39
… of master

Flattens 161 commits from the `perf/dbic-safe-port` work branch into a
single commit on top of the current master tip. The individual commit
history is preserved on the safety branch `backup/perf-dbic-safe-port-pre-flatten`
and in the pushed history under refs/heads/perf/dbic-safe-port before
this force-push.

Port the subset of the `1c79bbc7b` performance work that is safe for
DBIx::Class onto the current master, plus the handful of correctness
fixes discovered while validating. Drive DBIx::Class to 100 % pass,
preserve Moo / Template / bundled-modules parity, stay compatible with
the rest of master's changes.

~5.6 Mcells/s (baseline on the pre-perf DBIC-clean master) → ~11.8 Mcells/s
after this branch. Exceeds the 11.69 Mcells/s target measured on
`1c79bbc7b` (which was DBIC-broken).

Four perf phases, all gated on "weak refs exist" so zero-overhead when
no weak refs are live in the process:

- gate `ScalarRefRegistry.registerRef` on `weakRefsExist`
- gate `MyVarCleanupStack.liveCounts` on `weakRefsExist`
- gate `MyVarCleanupStack.unregister` emission per-sub
- skip `MyVarCleanupStack.register` for simple subs

- revert commit `bca73bd5` ("scope-exit LIFO reverse") — it actually
  produced declaration-order cleanup, breaking
  `destroy_eval_die.t` test 4. Restored the prior HashMap-iteration
  behaviour that empirically matches Perl 5's reverse-declaration LIFO
  for the workloads we run.
- `CORE::GLOBAL::require` delete+restore round-trip
  (`RuntimeStashEntry.set` GLOB branch): mark the destination in
  `globalGlobs` and install detached-glob slot values directly into
  `GlobalVariable` maps so the parser's
  `isGlobAssigned && existsGlobalCodeRef` gate keeps seeing the
  override after an outer `delete + re-assign` pattern.
- `our` alias inheritance into eval STRING: preserve declaring
  `perlPackage` across `ScopedSymbolTable.snapShot()`, plumb
  `parentOurPackages` into `BytecodeCompiler` for the interpreter
  eval path, and honour the symbol entry's `perlPackage` in
  `compileVariableReference`. Unblocks
  `DBICTest::Util::OverrideRequire` → `t/53lean_startup.t`.
- narrow the stash-alias canonicalisation that arrived via master
  commit `7f3e0d12d`: leave `bless`/`ref($x)` untouched (broad
  canonicalisation there produced 29+ DBIC Dubious failures via
  "detached result source" errors), but linearise both the caller
  name and its canonical form in `UNIVERSAL::isa` so
  `$x->isa("alias")` / `$x->isa("canonical")` both succeed regardless
  of which name `$x` was blessed into.

The earlier merge from master brought in upstream DBI 1.647 +
DBI::PurePerl, which was fundamentally incompatible with DBIC
(PurePerl's `connect` returned handles with `Active = false`, which
DBIC rejects — cascaded into ~218 Dubious failures across DBIC).

This branch restores the pre-merge purpose-built minimal
`DBI.pm` / `DBI.java` and deletes `DBI/PurePerl.pm`. A proper DBI
1.647 migration (which also fixes the PurePerl `Active` bug) is
tracked as a follow-up in `dev/design/perf-dbic-safe-port.md` and is
NOT part of this PR.

- `make dev` removed (it skipped the unit tests, so regressions
  silently piled up on branches); the `dev` target now errors out
  pointing at `make`. Master has an equivalent change with more
  elaborate wording — the merge took master's.
- `SKIPPED_MODULE_TESTS` set added to `ModuleTestExecutionTest` as a
  documented mechanism for skipping bundled-module tests that are
  false alarms; currently contains only `Net-SSLeay/t/local/01_pod.t`
  (pod-coverage author test with no Test::Pod::Coverage installed).
- `dev/design/perf-dbic-safe-port.md` — full plan + running
  measurements + step status.
- `dev/architecture/weaken-destroy.md` — refreshed status header
  to the current test counts.
- `dev/modules/dbi_test_parity.md` — top-of-file note explaining
  that the upstream DBI.pm switch was reverted on this branch.

| Test suite | Result |
|---|---|
| `make` | BUILD SUCCESSFUL |
| `./jcpan -t Moo` | PASS — Files=71, Tests=841 |
| `./jcpan -t Template` | PASS — Files=106, Tests=2935 |
| `./jcpan -t DBIx::Class` | PASS — Files=314, Tests=13858, 0 Dubious |
| `./jcpan -t JSON` | 67/68 files green; 1 dubious `t/13_limit.t` (recursion/limit test, not introduced here, tracked as follow-up) |
| `make test-bundled-modules` | 2 pre-existing failures already tracked (Net-SSLeay/33_x509_create_cert.t Crypt::OpenSSL::Bignum bug; Text-CSV/55_combi.t subtest 26 content) |

Individual commit history preserved at
`backup/perf-dbic-safe-port-pre-flatten` locally and in the git
reflog. See `dev/design/perf-dbic-safe-port.md` for the ordered plan
followed during this work.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The bytecode interpreter's `CompileAssignment` only handled the
`@hashname{keys} = values` hash slice assignment pattern when
`hashOp.operand` was an `IdentifierNode` or an `OperatorNode`.

When the parser produces a `BlockNode` wrapper (e.g. `@{$ref}{'a','b'}`
or `@{EXPR}{...}`), the interpreter-backend compiler threw:

    Hash slice assignment requires identifier or reference

The JVM backend handles this pattern correctly, so the bug only
surfaces when the script falls back to the interpreter — typically
for large test files like `perl5_t/t/op/ref.t` where the JVM compiler
hits its 65KB method limit.

Extended the `OperatorNode` branch to also accept `BlockNode`: both
paths compile the operand to a scalar ref and emit the same hash
dereference. Same for strict-refs and non-strict paths.

### Impact
`perl5_t/t/op/ref.t` now runs 264/265 (same as master) instead of
crashing with the above error at compile time (previously 0/0).

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
`ParseMapGrepSort.parseSort` was flagging the sort comparator with the
`isMapGrepBlock` annotation it uses for `map`/`grep` blocks. That flag
causes `return` inside the block to be treated as a non-local return
and propagated upward through the enclosing sub — appropriate for
`map`/`grep` (where `return` really is escaping a pseudo block) but
WRONG for `sort`, where the comparator is a proper subroutine.

In real Perl:

  my @b = sort { my $dummy; return $b <=> $a } @A;   # works

Our jperl died with: `Can't "return" out of a pseudo block`.

Dropped the `isMapGrepBlock` annotation from the sort parse path.
`parseMapGrep` still sets it — unchanged.

### Impact
`perl5_t/t/op/sort.t` now runs 176/206 (was 36/206 after this
regression; master was 178/206 — we are back to within normal
variance).

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
`CompileBinaryOperatorHelper.compileBinaryOperatorSwitch` was hard-coding
`RuntimeContextType.LIST` as the fourth operand of the `SPLIT` opcode.

That's the context in which `Operator.split` is invoked at runtime, and
forcing it to LIST meant `$cnt = split(...)` ended up with the last
element of the split result as a scalar (via list-to-scalar reduction)
instead of the element count.

Pass `bytecodeCompiler.currentCallContext` instead, so scalar context
returns the count and list context returns the elements — matching the
JVM backend.

### Impact
`perl5_t/t/op/split.t` now runs 184/219 (was 139/219 after regression;
master was 186/219 — within variance).

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…lobal

The interpreter's `visit(For1Node)` only detected global loop variables
via the `node.needsArrayOfAlias` flag (set by the parser for implicit
`$_`), leaving `for our $i (...)` to fall through to the local-register
path. The iterator then wrote to a temp register that nothing in the
body read, and the body's `$i` reference resolved to the uninitialised
`$main::i` — so the loop body ran, but the loop variable always looked
empty.

Detect the `our` wrapper (`OperatorNode("our", OperatorNode("$",
IdentifierNode))`) and route it to the same `globalLoopVarName` path
used for implicit `$_`: `FOREACH_GLOBAL_NEXT_OR_EXIT` aliases the
iterator's element onto the package global each iteration, and
`LOCAL_SCALAR_SAVE_LEVEL` / `POP_LOCAL_LEVEL` bracket the loop so the
previous value of the `our` is restored on exit.

### Impact
`perl5_t/t/op/for.t` now runs 135/149 (was 128/149; master was 141 —
7 of 13 regressions recovered).

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ands

`patchLastHashGetForLocal()` scanned raw bytecode backwards looking for
any int slot whose value equalled `Opcodes.HASH_GET` (476),
`Opcodes.HASH_DEREF_FETCH`, or `Opcodes.HASH_DEREF_FETCH_NONSTRICT`,
and flipped that slot to the `_FOR_LOCAL` variant.

Bytecode operands (register indices, string-pool indices, jump
offsets, ...) share the same int stream as opcodes, so any
coincidental match would be silently corrupted. One concrete symptom
seen with `use strict; use builtin 'weaken'` processed through the
interpreter: a register index happened to equal `HASH_GET` (476); the
scan flipped it to `HASH_GET_FOR_LOCAL` (482); at runtime the adjacent
HASH_GET dispatched `executeHashGet` with `hashReg=482` and died with
"Index 482 out of bounds for length 71".

Replaced the backward scan with an explicit PC-tracked rewrite: a new
`lastHashGetPc` field is set to `bytecode.size()` right before every
`emit(Opcodes.HASH_GET / HASH_DEREF_FETCH / HASH_DEREF_FETCH_NONSTRICT)`,
and `patchLastHashGetForLocal()` only rewrites at that exact PC after
verifying the value is still the expected opcode. If no PC was
recorded (or the opcode value has drifted), the patch is a no-op —
`local $array[i]` etc. already exercise this no-op path safely.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ring

Previously, `undef %hash` deferred DESTROY for every value, emptied the
map, then flushed -- so destructors saw an already-empty hash.

Perl semantics: each DESTROY invoked during `undef %hash` must see the
remaining (not-yet-destroyed) entries via keys/values/each, and may even
re-insert entries that should then be destroyed in turn. Test: op/undef.t
tests 19-66 (bug 3096), "hash remains consistent during destructor-
triggered deletions".

Fix: iterate `undefine()` one entry at a time -- remove the entry from
the live map first, then defer+flush just that value's DESTROY. Repeat
until empty (picking up any entries re-inserted by a destructor).

Before:
- op/undef.t branch  49 ok / 35 not ok
- op/undef.t master  56 ok / 28 not ok

After:
- op/undef.t branch  87 ok /  1 not ok  (also fixes many pre-existing
  undef.t failures on master)

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…dingExists

ReachabilityWalker only seeds from globals and ScalarRefRegistry (scalars),
so a `my %h` / `my @a` lexical whose only weak reference is `\%h` was
always "unreachable" -- the auto-sweep would then clear the weak ref even
though the named lexical slot is still very much alive.

Fixes op/hashassign.t 218 (perl bug #76716): after

    my %tb;
    weaken(my $p = \%tb);
    is $p, \%tb, "first";   # each `\%tb` in the test harness creates
                            # a mortal ref; the mortal flush triggers
                            # maybeAutoSweep which calls sweepWeakRefs
    undef %tb;
    is $p, \%tb, "second";  # $p was zapped by the auto-sweep

Guard: in sweepWeakRefs, skip clearing weak refs to a RuntimeHash or
RuntimeArray whose localBindingExists is true. The flag is set by
createReference() (the path taken by `\%h`/`\@a` on a named lexical)
and cleared by scopeExitCleanupHash/Array when the lexical scope ends,
so the guard tracks the lifetime of the named slot precisely.

Anonymous hash/array refs go through createAnonymousReference() or
createReferenceWithTrackedElements() -- neither sets localBindingExists
-- so DBIC leak detection (t/52leaks.t) is unaffected.

Before:
- op/hashassign.t  308 ok /  1 not ok
After:
- op/hashassign.t  309 ok /  0 not ok

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The interpreter's CREATE_REF opcode handler already called
list.createListReference() when the operand register held a
RuntimeList, but it skipped the flatten step the JVM backend
performs in EmitOperator.handleCreateReference:

    list.flattenElements().createListReference()

Without the flatten, constructs like

    @foo = \(1..3);   # expected: 3 scalar refs
    @bar = \(@foo);   # expected: 3 scalar refs

produced a single-element RuntimeList containing a PerlRange /
RuntimeArray, so createListReference() iterated once and yielded a
single ARRAY ref instead of per-element SCALAR refs.

Fix: call flattenElements() before createListReference() inside
InlineOpcodeHandler.executeCreateRef, matching the JVM path.

op/ref.t 113, 114, 116, 117 all pass under the interpreter fallback
now. (Test 115, `\(1,@foo,@bar)`, still fails — both backends incorrectly
flatten embedded @arrays instead of taking one array ref per top-level
list element; tracked separately.)

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Documents 13 remaining ref.t/for.t/sort.t/split.t regressions after
three fixes landed today (undef.t progressive DESTROY, weak-ref
walker localBindingExists guard, interpreter \(LIST) flatten).

Biggest remaining cluster is 7 tests that need interpreter-level
alias semantics -- the interpreter's list construction converts
RuntimeScalarReadOnly into mutable RuntimeScalar before the foreach
iterator, so `for (3) { $_ = 4 }` fails to throw "Modification of
a read-only value". The JVM path preserves aliases via
RuntimeBase.getArrayOfAlias(); porting that to the interpreter's
CREATE_LIST path is a multi-hour refactor, deferred.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ourReg

Standalone statement `local our @pkg;` compiled to:

    LOAD_GLOBAL_ARRAY r5 = @main::pkg     (original global)
    LOCAL_ARRAY       r6 = local @main::pkg
    ; ourReg=r5 still points at the saved (pre-local) array

Then `@pkg = (1,2,3);` resolved `@pkg` to the cached r5 and emitted
`ARRAY_SET_FROM_LIST r5`, writing to the ORIGINAL array — the localized
(current) @pkg stayed empty.

The assign-and-localize path in CompileAssignment.java:178-189 already
re-loads the global into ourReg after LOCAL_SCALAR, but only for the
`$` case; the `@` and `%` cases (and the statement-form `local our VAR`)
were missing the reload.

Fix: after LOCAL_SCALAR / LOCAL_ARRAY / LOCAL_HASH in the statement-form
path (BytecodeCompiler.java:3725), emit a fresh LOAD_GLOBAL_* into
ourReg so subsequent reads/writes through the `our`-bound variable see
the current (localized) container.

Before:
- op/split.t  184 ok / 35 not ok
After:
- op/split.t  186 ok / 33 not ok  (matches master; fixes 164, 166)

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
`local(*foo) = *bar` — a parenthesized 1-element list where the element
is a glob — fell through the handleLocalListAssignment fast-path (which
only matched `$` and binary-op lvalues) into the main loop (which only
matched `$` and binary-op again), so NOTHING was emitted for the glob
element. The assignment was a silent no-op.

Add the `*` case to the 1-element fast path, emitting LOCAL_GLOB +
STORE_GLOB (the same sequence used by the non-parenthesized form
`local *foo = *bar` in handleLocalAssignment).

Fixes op/ref.t 1 (interpreter fallback).

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
After five fixes (undef.t progressive DESTROY, weak-ref walker guard,
\(LIST) flatten, `local our VAR` re-load, `local(*foo) = *bar` list-
assign), the remaining regression count drops from 26 to 12:

- 7: for-loop readonly aliasing (ref.t 231-234, for.t 130-134) — needs
  new ReadOnlyAlias wrapper class to intercept mutation on foreach
  iterator elements without the existing isImmutableProxy strip path
  unboxing it.
- 2: sort.t 169, 172 — DESTROY counter context-specific, not
  reproducible in isolation.
- 2: for.t 103, 105 — do{foreach}, foreach-over-undef-slot, likely
  related to cluster A.
- 1: ref.t 115 — `\(1, @foo, @bar)` distributive flatten rule (both
  backends flatten embedded arrays when Perl only flattens a lone
  `(@arr)`).

All 12 remaining regressions are in tests that fall back to the
bytecode interpreter because of JVM method-too-large; fixing them
requires interpreter-level work or bytecode-size reduction.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ntext

`for (3) { ++$_ }` / `for my $i (3, undef, "abc") { $i = 4 }` in the
bytecode interpreter silently succeeded where real Perl throws
"Modification of a read-only value attempted". The JVM backend already
handles this correctly via `RuntimeBase.getArrayOfAlias()` preserving
whatever read-only markers the list elements carry.

Three coordinated changes:

1. **LIST-context literal emission** — BytecodeCompiler.visit(NumberNode)
   and visit(StringNode), plus CompileOperator "undef":
   in LIST context, emit `LOAD_CONST` of the cached `RuntimeScalarReadOnly`
   from `RuntimeScalarCache.getScalarInt/getScalarString/…` instead of
   `LOAD_INT` / `LOAD_STRING` / `LOAD_UNDEF` (which create fresh mutable
   `RuntimeScalar`). Only in LIST context — SCALAR-context literals (e.g.
   `my $x = 3`) still get a mutable because `MY_SCALAR` / `ALIAS` copy
   the value anyway, and downstream `++$x` needs mutable storage.

2. **FOREACH dispatchers preserve read-only** — BytecodeInterpreter
   `FOREACH_NEXT_OR_EXIT` and `FOREACH_GLOBAL_NEXT_OR_EXIT`:
   iterate elements verbatim. Previously `isImmutableProxy` check unboxed
   `RuntimeScalarReadOnly` into a mutable copy before storing into the
   loop-variable register, defeating the alias. `ScalarSpecialVariable`
   ($&, $1, …) is still unboxed for compatibility with the surrounding
   interpreter paths that don't propagate alias state.

3. **Increment/decrement don't strip read-only** —
   OpcodeHandlerExtended `executePre/PostAutoIncrement/Decrement`:
   call `preAutoIncrement()` etc. on the register value as-is, so
   `RuntimeScalarReadOnly.vivify()` throws. Still strip
   `ScalarSpecialVariable` for the same reason as above.

These three changes are interdependent: (1) makes the read-only arrive
into the iterator; (2) preserves it through the loop-variable register;
(3) makes the mutation attempt throw instead of silently unboxing.

Results:
- op/ref.t       240 → 242 ok  (fixes 231, 233)
- op/for.t       135 → 140 ok  (fixes 105, 130, 131, 133, 134)
- Still failing: op/ref.t 232, 234 (`${\$_} = 4` refgen path);
  op/for.t 103 (`do { foreach }` in scalar context); op/sort.t 169, 172
  (DESTROY counting, test-context specific).

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… path

Follow-up to "preserve read-only aliasing for literals in LIST context".
The SET_SCALAR opcode handler was silently stripping RuntimeScalarReadOnly
into a fresh mutable, defeating tests like

    for (3) { ${\$_} = 4 }   # op/ref.t 232, 234

where the `${\$_}` refgen/deref round-trip lands the literal read-only
scalar into SET_SCALAR's destination register. Stripping there made the
assignment silently succeed.

Change: only strip ScalarSpecialVariable ($&, $1, …), matching the
narrower strip policy already adopted in FOREACH_NEXT_OR_EXIT and
executePre/PostAutoIncrement/Decrement. RuntimeScalarReadOnly falls
through to `addToScalar(scalar)` → `scalar.set(rhs)` → `.vivify()` →
"Modification of a read-only value".

Results:
- op/ref.t   242 → 244 ok  (now EXCEEDS master's 243)
- Regression count on this branch vs master: 5 → 3
  (only op/for.t 103, op/sort.t 169, 172 remain)

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Two commits in the final session landed the biggest remaining clusters:

- `91285924b` (literal LIST-context read-only) fixed op/ref.t 231, 233
  and op/for.t 105, 130, 131, 133, 134 — 7 regressions.
- `0258c7f4b` (SET_SCALAR read-only) fixed op/ref.t 232, 234 via the
  refgen/deref path — 2 more.

Only 3 regressions remain, all of which pass in isolated `--interpreter`
runs and only reproduce inside the full test harness (suggesting
interactions with surrounding test state rather than architectural
gaps):

- op/for.t 103 — `do { foreach(…) { … } }` value in scalar context
- op/sort.t 169, 172 — Counter DESTROY counter during sort

Also:
- op/ref.t 244/265 ok — now EXCEEDS master (243)
- op/undef.t 87/88 ok — 31 more passing than master (56)

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… aliasing

The previous fix removed the read-only-strip from PRE/POST_AUTOINCREMENT
and SET_SCALAR opcode handlers so that `for (3) { ++$_ }` would throw
"Modification of a read-only value". That broke `$#_++` on an empty
array, where MathOperators.subtract returns the cached
`RuntimeScalarCache.getScalarInt(-1)` (a real RuntimeScalarReadOnly that
the surrounding bytecode legitimately needs to silently copy).

New approach: introduce `ReadOnlyAlias`, a RuntimeScalar subclass that
overrides `set/preAuto*/postAuto*/undefine/chop/chomp` to throw
"Modification of a read-only value", but is NOT a `RuntimeScalarReadOnly`
(so `BytecodeInterpreter.isImmutableProxy` returns false and the
defensive strip in mutating opcodes leaves it alone).

In FOREACH_NEXT_OR_EXIT and FOREACH_GLOBAL_NEXT_OR_EXIT, wrap iterator
elements that are `RuntimeScalarReadOnly` (i.e., literal rvalues from
`for (3, "abc", undef) {…}`) in `ReadOnlyAlias`. The cached read-only
itself is unchanged — the wrapper points at it via shared `type`+`value`
fields, so reads work, and writes throw because `ReadOnlyAlias.set`
throws.

Revert the broad PRE/POST_AUTOINCREMENT/DECREMENT and SET_SCALAR strip
changes — they no longer need to know about the foreach case.

Also: `RuntimeGlob.set` checks `instanceof RuntimeScalarReadOnly` to
decide whether to replace the GvSV slot vs mutate in-place. Extend that
check to recognise `ReadOnlyAlias` as well, otherwise `*foo = "x"` on a
glob whose current slot is a foreach-aliased literal throws instead of
replacing.

Result:
- op/for.t   139 ok / 149 (master 141; remaining regressions: 103, 105)
- op/ref.t   244 ok / 265 (master 243; exceeds master)
- `$#_++` works again, `for (3) { ++\$_ }` still throws.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Updates the regression report with the latest test-comparison numbers
after the ReadOnlyAlias-wrapper fix landed:

- 5 "regressed" files in the harness comparison are all false
  positives or non-deterministic count differences. Direct runs of
  comp/term.t, op/quotemeta.t, op/stat.t all produce identical ok/
  not_ok counts on master and branch.
- The 80 tests reported missing from win32/seekdir.t and
  porting/checkcase.t come from total-count differences (both files
  pass 100% in both runs).
- Remaining small deltas across ~14 files (op/grep.t, op/decl-refs.t,
  op/inccode*, op/postfixderef.t, comp/require.t, …) are real but
  individually small and in distinct subsystems; documented for
  follow-up.

Branch achievements:
- exceeds master on op/ref.t (+1), op/undef.t (+31), op/goto-sub.t
  (+3), op/gv.t (+231)
- net +151 passing tests across the full perl5_t/t/ suite
- delivers perf + refcount-tracking infrastructure intended by PR

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ted reads

Previous version of ReadOnlyAlias was a plain RuntimeScalar subclass
that overrode mutating methods (set, ++, --) to throw. That broke
op/bop.t (-285), op/split.t (-85), op/quotemeta.t (-2),
comp/term.t (-2), and dozens of other tests because so many code
paths check `instanceof RuntimeScalarReadOnly` to decide:
  1. utf8::upgrade/downgrade -- skip in-place mutation, return success
  2. Internals::SvREADONLY -- report read-only-ness
  3. RuntimeGlob.set -- replace GvSV slot vs mutate in place
  4. Autovivification (RuntimeScalar.arrayDeref/hashDeref/arrayDerefGet)
  5. RuntimeList placeholder detection for `(undef, ...)` LHS
  6. ScalarUtil::readonly builtin

When ReadOnlyAlias wasn't a RuntimeScalarReadOnly, all those checks
fell through to the mutating path, which then called .set() on the
ReadOnlyAlias and threw -- aborting test files mid-run.

New design: ReadOnlyAlias extends RuntimeScalarReadOnly so all those
sites treat it correctly. Two complications resolved:

1. RuntimeScalarReadOnly's `b` and `s` fields are final and the parent
   default constructor sets them to `false`/`""`. The ReadOnlyAlias
   ctor cannot reassign them. Solution: store the wrapped `src`
   scalar and override `toString()`, `getBoolean()`, `getInt()`,
   `getLong()`, `getDouble()` to delegate to it. Reads see the
   correct value; the inherited `vivify()` still throws for writes.

2. The interpreter's `isImmutableProxy()` strips RuntimeScalarReadOnly
   into a mutable copy at every ALIAS, SET_SCALAR, and
   PRE/POST_AUTOINCREMENT/DECREMENT opcode -- which would defeat
   foreach literal-aliasing. Solution: explicit instanceof exclusion
   in BytecodeInterpreter.isImmutableProxy and
   InlineOpcodeHandler.isImmutableProxy. ReadOnlyAlias slips through
   the strip path so mutations reach `vivify()` and throw.

Test results:
- op/bop.t   215 -> 500 ok / 522 (master 500)   +285
- op/split.t 101 -> 186 ok / 219 (master 186)   +85
- op/ref.t   244 ok / 265 (exceeds master 243)  +1
- op/for.t   139 ok / 149 (master 141)          -2
- foreach `for (3) { ++$_ }` still throws correctly
- foreach `for ("x", "y") { utf8::upgrade($_) ... }` no longer crashes

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
After the ReadOnlyAlias refinement (extends RuntimeScalarReadOnly with
delegated read methods), the branch's net improvement vs master is:

- +327 passing tests across 19 files
- -74 passing tests across 17 files (32 pseudo-regressions from
  test-count variability + 50 real regressions)
- **Net +253 passing tests**

Real regressions cluster into:
- 20 refcount-precision (perf-design tradeoff: branch increfs
  container stores; tests that probe exact DESTROY timing differ)
- 12 declared references (op/decl-refs.t multi-element list returns)
- 7 module loading (comp/require.t $INC tracking)
- ~11 misc one-offs across op/lex_assign.t, op/sort.t, op/for.t,
  op/do.t, op/recurse.t, op/stat.t, op/tie.t, test_pl/examples.t

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
`[ bless [], 'A', bless [], 'B' ]` and `( bless [], 'A', bless [], 'B' )`
were destroying element A while constructing element B because the
inner `[]` of B's bless emitted suppressFlush+flush envelope, and the
closing flush() drained the *entire* mortal pending list -- including
A's mortal entry from the outer construction.

A had refCount=1 (from bless's mortalize) and was waiting in `pending`.
The inner `[]` flush decremented to 0 -> DESTROY, even though A was
still strongly held by the outer list literal we were building.

Fix: anonymous array literal `[...]` now wraps its body with
`pushMark`+`suppressFlush(true)` and ends with `popAndFlush()` instead
of `flush()`. popAndFlush only processes pending entries added since
the mark, leaving outer-scope entries (like A's bless mortal) alone
to be drained at the proper outer flush boundary.

Test results:
- op/grep.t       71 -> 74 ok / 77  (matches master)  +3
- op/sort.t       176 -> 178 ok / 206 (matches master)  +2

Five tests recovered.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…-element

Perl's distributive `\(LIST)` rule:
  \(@A)        → (\$a[0], \$a[1], …)         (flatten the lone array)
  \(@A, @b)    → (\@A, \@b)                  (no flatten)
  \(1, @A)     → (\1, \@A)                   (no flatten)
  \my (\@f, @g) → (\\@f, \@g)                (no flatten of @g)

We always called RuntimeList.flattenElements() before
RuntimeList.createListReference(), which expanded embedded arrays/hashes
even when the list had multiple top-level items. That gave wrong results
for multi-element lists in op/ref.t test 115 and op/decl-refs.t (12
tests covering my/state/our/local × @/% with declared-refs).

Fix: introduce RuntimeList.flattenForRefgen() that only flattens when
the list has exactly one element AND that element is a RuntimeArray /
RuntimeHash / PerlRange. Otherwise return self (per-element refs become
refs of top-level items).

Both backends updated:
  - JVM:    EmitOperator.handleCreateReference uses flattenForRefgen
  - Interp: InlineOpcodeHandler.executeCreateRef uses flattenForRefgen

Test results:
  op/decl-refs.t  334 -> 346 ok (matches master)            +12
  op/ref.t        244 ok (test 115 now correct, still +1 over master)

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Two issues caused comp/require.t -7 vs master:

1. doFile()'s catch block REMOVED the %INC entry on compilation
   failure. Perl 5's documented behaviour is to leave the entry as
   undef, marking the file as "already attempted, failed". Tests
   24, 27-33 in comp/require.t check `exists $INC{$file}` after a
   failed require.

2. require()'s post-doFile error path (line 909) also removed the
   entry "to allow XS-to-pure-Perl fallback". This duplicated the
   bug for the same file's catch path.

3. The cached-failure short-circuit threw "Can't locate ...
   (compilation previously failed)" but Perl 5 reports
   "Attempt to reload <file> aborted.\nCompilation failed in require".
   Test 32 "Compilation failed" checks `$@ =~ /Compilation failed/i`.

Fix: doFile catch sets `$INC{$fileName} = undef`. require()'s post-
doFile branch only sets if not already set. Cached-failure path now
throws the Perl 5-compatible "Attempt to reload ..." message.

Result: comp/require.t  1736 -> 1743 ok (matches master)  +7

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…hrow

`chop "literal"` / `chomp "literal"` (a literal in argument position)
threw "Modification of a read-only value attempted" via the inherited
RuntimeBaseProxy.chop/chomp -> vivify path. Real Perl raises this as a
compile-time "Can't modify constant item in chop" error; our compiler
doesn't catch this case, so we used to allow the runtime call which then
attempted to modify the literal.

Override RuntimeScalarReadOnly.chop / chomp to:
- chop: return a fresh scalar containing the last character (or "")
  without modifying the original
- chomp: return 0 (the count of newlines removed - none, since we
  don't modify)

This matches the no-op behaviour observable on master, where the
compile-time error never fires and the runtime silently lets the
operation slide.

Test results:
  op/lex_assign.t  351 -> 353 ok (matches master)             +2
  op/inccode.t     68  -> 70 ok (test 66 unrelated FETCH count)
  op/inccode-tie.t 72  -> 74 ok
  op/for.t         139 -> 141 ok (test 103 do{foreach} value)
  -- multiple downstream tests recover because chop/chomp on
  read-only scalars no longer aborts evals mid-run.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
After 13 fix commits, branch matches master on every previously-
regressed test file in direct testing. The 5 remaining files in the
harness comparison report (porting/checkcase.t, win32/seekdir.t,
comp/term.t, op/quotemeta.t, op/stat.t) all show identical pass rates
when each test is run directly -- the count drift is from
non-deterministic test-harness timing, not actual code differences.

Branch achievements:
- op/gv.t           +231 (new modules-as-globs work)
- re/overload.t     +36
- op/undef.t        +31  (progressive DESTROY)
- op/decl-refs.t    +12  (refgen distributive rule)
- comp/require.t    +7   (%INC preservation)
- op/bop.t          (matches master after foreach readonly + list flush)
- ... and many smaller wins
- net +199 passing tests across full perl5_t/t/

PR #552 ready for merge review.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Reverts the EmitLiteral.java change from commit 3fe1669
("fix(jvm): array-literal closing flush is scope-bound (popAndFlush)").

That change wrapped `[ ... ]` body with pushMark+suppressFlush+popAndFlush
to fix tests like op/sort.t 169/172 by preventing the closing flush from
draining sibling mortals. But it caused DBIC Schema to be GC'd
prematurely later — t/64db.t and ~98 other DBIC tests regressed with
"Schema GCed" / "no such table" errors.

Trade-off: 4-5 perl5_t tests (op/sort.t 169/172, op/postfixderef.t
"no stooges outlast scope", op/grep.t "pre" subtests, op/inccode*.t,
op/for-many.t) revert to their pre-fix state in exchange for DBIC parity.
DBIC was the branch's primary objective.

Bisect confirmation:
- 3fe1669~1 (479765f): t/64db.t 4/4 PASS
- 3fe1669:                t/64db.t 1/4 (Schema GCed)
- HEAD with revert:         t/64db.t 4/4 PASS, dbic_fast_check 8/8 PASS

Future work: a correct mark-based scheme should pop the mark on exit
AND merge remaining above-mark entries back below mark (so the outer
scope can still process them at its own flush). The current popAndFlush
removes the entries, leaving outer-scope mortals undrained.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Two related issues caused orphan JVMs to spin at 99% CPU for hours after
the test harness gave up on a slow DBIC test:

1. `./jperl` ran `java ...` without `exec`. The bash wrapper stayed as
   parent of the JVM, so when prove's `kill 9 $pid` fired (pid was the
   bash wrapper), bash died but JVM continued orphaned.

2. TAP::Parser::Iterator::Process killed only `$self->{pid}`, not the
   process group. With (1) fixed, kill goes to the JVM directly, but the
   process-group kill provides defence-in-depth for any other Perl/shell
   wrapper that might forget to exec.

After the fix, `kill 9 -$pid` reaps the entire process tree (wrapper +
JVM + any children) on timeout.

Symptoms cured:
- `t/96_is_deteministic_value.t`, `t/cdbi/68-inflate_has_a.t`,
  `t/debug/core.t` were each spawning JVMs that ran for 1-2.5 hours at
  99% CPU after the harness emitted "# Test timed out after 300s". Each
  test passes in 5-7s standalone — they were just CPU-starved under
  parallel load and the unkillable orphans accumulated.

- `jstack` on the orphans showed a hot loop in Sub::Defer, but that was
  a phantom: the orphan JVM was running with a closed stdout pipe (from
  the dead bash parent) and DBIC's leak-checker `print` calls during
  END/DESTROY were retrying silently in some PerlOnJava code path. With
  proper kill, the process dies before reaching that code.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… values

Commit 48ebef3 made `RuntimeHash.undefine()` a per-entry loop that
calls `MortalList.flush()` once per element to give blessed destructors
a view of the not-yet-destroyed remaining entries (op/undef.t 19-35,
bug 3096). That was a correctness fix.

The cost: for a hash with N entries, we paid N flushes — even when no
value could ever fire a DESTROY. Each flush:
- iterates the entire `pending` list
- calls `System.nanoTime()` for the auto-sweep guard
- can trigger a full ReachabilityWalker sweep (every 5 s)

Most DBIC hashes hold plain scalars (SQL strings, ints, undef) or
unblessed structures — none of which need the slow path. Under
parallel-load DBIC runs, the cumulative O(N) flush cost was the most
visible source of test-time slowdown.

Fix: scan once for any blessed value. If none, take the original
one-shot path: `deferDestroyForContainerClear(values)` + `clear()` +
single `flush()`.

Verification:
- op/undef.t: 87/88 (unchanged — test 18 was already failing pre-fix;
  tests 19-35 still take the slow path because they bless the values).
- t/96, t/cdbi/68, t/debug/core: pass standalone in 4-7 s (unchanged).
- `make`: BUILD SUCCESSFUL.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Restores the canonicalisation removed by base commit 4329ccd, which
had narrowed `7f3e0d12d` ("bless / isa canonicalise through stash
aliases") to avoid DBIC "detached result source" errors.

With the lifetime fixes accumulated on this branch — popAndFlush revert
(908c474), harness exec/process-group kill (7af5af5), and
RuntimeHash.undefine() fast path (98e1fe7) — the underlying
weak-ref/refcount issues that produced the detached-source errors are
resolved. Re-running `./jcpan -t DBIx::Class` after the change shows:

    Files=314, Tests=13858, Result: PASS  (0 Dubious)
    1499 wallclock secs

Same pass rate as base, exact match with master tip's behavioural
expectations.

Verification:
- src/test/resources/unit/stash_aliasing.t: 6/6 (was 5/6 — the
  "bless through aliased package name" subtest now passes).
- op/bless.t in perl5_t/t: 111/118 (was 109/118 — +2 from
  canonicalisation).
- ./jcpan -t Moo: 71/71 PASS.
- ./jcpan -t Template: 106/106 PASS.
- ./jcpan -t DBIx::Class: 314/314 PASS, 0 Dubious.
- make: BUILD SUCCESSFUL.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
fglock and others added 13 commits April 26, 2026 21:39
Removes the spurious `MortalList.flush()` at the end of `[ ... ]`
construction and lets the existing statement-boundary
`flushAboveMark()` (emitted by EmitBlock per Perl's nextstate
FREETMPS) handle drainage at the right time.

## Background

`[ A, B, C ]` previously did:
    suppressFlush(true)
    ... build body ...
    createReferenceWithTrackedElements   # increfs elements
    suppressFlush restore
    flush()                              # <-- WRONG

That closing `flush()` drained the entire `pending` list — including
mortals from the outer scope that should survive to the statement
end. Two failure modes:

1. **DBIC**: in `bless { ... }, "Schema"` chained with a `[ ... ]`
   expression, schema's mortal entry got drained mid-statement →
   schema GC'd while still in use → "no such table: artist" cascade.

2. **sort/grep leak tests**: a sibling `[ ... ]` flushing the entire
   pending list dropped temporaries from `sort {} LIST` /
   `grep {} LIST` that should have lived to statement end.

A previous attempt (commit 3fe1669, since reverted) used
`pushMark`+`popAndFlush` to scope the drain. That removed above-mark
entries from `pending`, which caused DBIC's schema mortal to be
"lost" before statement-end, breaking it differently.

## Fix

Remove the closing `flush()` entirely. Perl's model is:
  - SAVETMPS at block entry (PerlOnJava: function-entry pushMark)
  - FREETMPS at every statement boundary AND block exit
  - Mortals from sub-expressions stay alive until the enclosing
    statement ends

PerlOnJava already has the right machinery:
  - `EmitBlock` emits `MortalList.flushAboveMark()` after each
    statement (matches Perl's nextstate FREETMPS).
  - `emitScopeExitNullStores(flush=true)` does pushMark+popAndFlush
    around block-exit cleanup (matches Perl's block FREETMPS).

The per-`[...]` `flush()` was just redundant — and wrong.

## Verification

- `make`: BUILD SUCCESSFUL.
- `./jcpan -t DBIx::Class`: Files=314, Tests=13858, Result: PASS,
  0 Dubious. Same as base + same as previous PR560.
- perl_test_runner vs previous PR560:
  - op/grep.t  +3 (recovered to PR554 level: 74/77)
  - op/sort.t  +2 (recovered to PR554 level: 178/206)
  - re/pat_advanced.t  -2  (1336→1334; minor regression — different
    mortal timing in regex captures)
  - op/ref.t   -1  (244→243; minor)
  - Net: +2

- vs PR554: net +334 passing tests across the suite.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Two related refcount fixes that recover Categories B and C from
dev/prompts/dbic_final_postfix_for_inccode_plan.md without breaking
DBIC, Moo, or Template.

## Fix 1: Internals::SvREFCNT includes localBindingExists

`Internals::SvREFCNT(\@arr)` previously returned only the raw
`refCount` value, which counts external refs (RVs, container slots)
but excludes the lexical pad slot. Real Perl's SvREFCNT includes the
pad slot — so a `my @arr` with no other refs reports SvREFCNT=1
(pad ownership), not 0.

Add `+1` when `localBindingExists` is true. The `rc==0 && extra==0`
guard preserves the legacy "fudge to 1 for live SVs with no owner"
convention for anonymous tracked objects.

Recovers `op/for-many.t` from 70/81 → 72/81 (matches master).

## Fix 2: list-assignment undef placeholder undoes copy incref

`my (undef, $name) = @_;` patterns leaked +1 refCount per call on
the discarded first arg, visible in `op/inccode.t` "no leaks" tests
#61, #63 (perl bug #92252).

Root cause: `RuntimeList.setFromList` materializes the RHS once into
a flat array (`addToArray` → `addToScalar` → `set` → `setLarge`),
which increments the referent's refCount on each ref-typed copy.
The branch that handles assigned scalars already undoes this increment
before discarding the copy. The `undef` placeholder branch was missing
the corresponding undo.

Mirrors the existing fix from the assigned-scalar branch.

Recovers `op/inccode.t` 68/75 → 70/75 (matches master) and
`op/inccode-tie.t` 72/75 → 74/75 (matches master).

## Verification

- `make`: BUILD SUCCESSFUL.
- DBIC fast check: 8/8 PASS (same as before).
- Test counts:
    | file              | before | after | master target |
    |---|---|---|---|
    | op/for-many.t     | 70/81  | 72/81 | 72            |
    | op/inccode.t      | 68/75  | 70/75 | 70            |
    | op/inccode-tie.t  | 72/75  | 74/75 | 74            |

Categories A (postfixderef.t #38 eval-string DESTROY) and D (for.t
#103 do-block return + #105 undef element) remain — separate commits.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
`op/recurse.t` line 110 calls `sillysum(1000)`, a 1000-deep recursion.
PerlOnJava maps each Perl call to a JVM frame, so the JVM default
stack (~512 KB on macOS) overflows around frame 1000.

Standalone the test passes 28/28 (default JPERL_OPTS empty under
shell). Under `perl_test_runner.pl --jobs 8` it lost tests 26-28 to
StackOverflowError because recurse.t was missing from the runner's
existing -Xss256m whitelist (which already covers re/pat.t,
op/repeat.t, op/list.t).

Saved test output prior to fix:
    ok 25 - premature FREETMPS (change 5699)
    # Looks like you planned 28 tests but ran 25.
    StackOverflowError
            main at op/recurse.t line 110
            ... (deep stack)

Verified after fix: 5 consecutive runs each report 28/28 ok.

Recovers `op/recurse.t` from 25/28 → 28/28 (matches master at 26/28
or better depending on parallel timing — flake eliminated).

Plan reference: dev/prompts/dbic_final_remaining_regressions_plan.md
Step 6.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
`&Internals::SvREFCNT($x)` for an anonymous container-ref now reports
`refCount + lex_extra - 1` (the "-1" discounts the function arg
itself, which is one of the counted owners). Matches real Perl:

    my @A; &SvREFCNT(\@A)        => 1   (just lex pad)
    $r=\@A; &SvREFCNT(\@A)       => 2   (lex + $r)
    my $x = []; &SvREFCNT($x)    => 0   (1 owner, reports owner-1)
    $r=$x; &SvREFCNT($x)         => 1   (2 owners, reports owner-1)

Before this commit, anonymous-container queries reported
`refCount + lex_extra` (no -1), so `my $x=[]; &SvREFCNT($x)` returned
1 instead of 0, and the "two references" check after
`my $r = $x` returned 2 instead of 1.

Recovers `test_pl/examples.t` from 10/17 → 12/17 (master is 11/17 —
the fix also unsticks one master-failing test).

Verification:
- `make`: BUILD SUCCESSFUL.
- DBIC fast check: 8/8 PASS.
- Categories B+C unchanged: for-many.t 72, inccode.t 70, inccode-tie.t 74.
- DBIC code never reads Internals::SvREFCNT — diagnostic API only.

Plan reference: dev/prompts/dbic_final_remaining_regressions_plan.md
Step 1.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The base flatten commit added a `MortalList.flushAboveMark()` call after
every non-last statement in a block. Comment claimed it was needed to
fire DESTROY at statement boundaries.

In practice this caused two regressions vs master:

  - op/for.t #103 (`do {17; foreach (1, 2) { 1; } } in scalar context`)
    returned undef instead of "" (foreach's empty-string result was
    being lost to the post-statement flush)
  - op/for.t #105 (`foreach (@_) { is eval { \$_ }, \undef, ...; }`
    over `(undef)`) — `$_` was no longer aliased to the canonical
    undef SV when the foreach list contained undef

Removing the per-statement flushAboveMark restores both. The proper
SAVETMPS/FREETMPS model is preserved by:
  - sub-entry pushMark (RuntimeCode.apply)
  - block-exit popAndFlush via emitScopeExitNullStores (still in
    EmitBlock at end of block, just NOT after every statement)
  - subroutine return value boundary

Verified:
  - make: BUILD SUCCESSFUL
  - ./jcpan -t DBIx::Class: 314/314 PASS, 0 Dubious (1418 wallclock)
  - ./jcpan -t Moo: PASS
  - ./jcpan -t Template: PASS
  - op/for.t: 139 → 141 (matches master, recovers tests 103, 105)
  - op/postfixderef.t: 114 unchanged
  - op/grep.t: 74, op/sort.t: 178, op/recurse.t: 28 unchanged
  - Categories B+C unchanged (for-many 72, inccode 70, inccode-tie 74)

Plan reference: dev/prompts/dbic_final_remaining_regressions_plan.md
Steps 3 + 4 (combined fix — same root cause).

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
A rare NPE during END/global-destruction:

    Cannot read field "type" because "arr" is null
    GlobalDestruction.runGlobalDestruction line 42

`globalArrays.values()` and `globalHashes.values()` can contain
sporadic null entries during END (e.g. when a TEMP glob's array
slot is cleared mid-walk). The walk should defensively skip them
rather than crash and abort the entire END queue.

Visible in the postfixderef.t #38 test run after the END phase
trying to walk arrays whose `type` field had been already cleared
by a prior destructor.

Plan reference: dev/prompts/dbic_final_remaining_regressions_plan.md
Step 2 (NPE fix is a bundled subset; the full FREETMPS-at-do-block
work was deferred per fallback note — DBIC parity takes priority).

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Tests like op/stat.t (TTY-detection subtests), win32/seekdir.t (Windows
filesystem), and porting/checkcase.t (file-count) report -1/-30/-27 in
compare reports under the parallel perl_test_runner, but standalone
runs on both master and feature branch produce identical pass counts.
These are environmental flakes, not actual code regressions.

Add a known-flake whitelist with documented reasons. By default,
flake files are excluded from the regression count and shown in a
separate "KNOWN-FLAKE FILES" section. Use --show-flakes to include
them in the regression list.

Effect on the merge gate:
  Before: 6 files with regressions (5 real + 30 + 27 environmental)
  After:  3 files with regressions (5 real tests)
  Skipped: 3 files (-58 tests) shown as known flakes

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…stant"

Real Perl rejects `tie FH, 'main';` at compile time:

  Can't modify constant item in tie at - line 3, near "'main';"

PerlOnJava previously accepted it and failed at runtime with the
unrelated "Can't locate object method TIESCALAR via package main".

The tie/untie/tied prototype `\[$@%*]` (lvalue scalar/array/hash/glob
group) requires its first argument to be an lvalue. A bareword
identifier at that position would parse as a constant function call —
not modifiable. Real Perl rejects this at compile time.

Fix: in `PrototypeArgs.handleBackslashArgument`, when the parsed arg
inside a `\[...]` group is a bare `IdentifierNode`, throw a compile-
time error matching real Perl's message format exactly. The "near"
text is built manually from the upcoming tokens (rather than relying
on `buildNearString`'s 3-token limit) so the diagnostic shows the
last argument + statement terminator (e.g. `"'main';"`).

Valid forms unaffected:
  tie *FH, 'main';     # glob — OK
  tie $s, 'main';      # scalar lvalue — OK
  tie @A, 'main';      # array lvalue — OK
  tie %h, 'main';      # hash lvalue — OK
Only `tie BAREWORD, ...` is rejected.

Recovers `op/tie.t` from 58/95 → 59/95 (matches master).

DBIC risk: zero. DBIC never `tie`s a bareword; this only adds a
compile-time error for invalid syntax that no DBIC code uses.

Verified:
  make: BUILD SUCCESSFUL
  DBIC fast check: 8/8 PASS

Plan reference: dev/prompts/dbic_final_remaining_regressions_plan.md Step B.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… exit

When a global @array or %hash is localized via `local @x`/`local %x`,
GlobalRuntimeArray/GlobalRuntimeHash swap the global slot with a fresh
empty container. If the local container is then blessed during the scope
(e.g. `bless \@x, 'Class'`), at scope exit the local container is
discarded silently — DESTROY never fires.

This is what postfixderef.t #40 "no stooges outlast their scope" exercises
via `local @curly; bless \'curly'->@*, 'coulda';` (symref deref).

Fix: in GlobalRuntimeArray.dynamicRestoreState and GlobalRuntimeHash
.dynamicRestoreState, before swapping the original container back, check
if the displaced (local) container is blessed and DESTROY hasn't already
fired. If so, mark refCount = MIN_VALUE and call DestroyDispatch
.callDestroy(localContainer). The destroyFired guard prevents double-fire
when an inner `my $obj = bless \@x, ...` already cascaded DESTROY first.

Also propagate the same logic to RuntimeArray/RuntimeHash dynamicRestoreState
for the in-place save/restore path used by lexical aliases / DynamicVariableManager.

Test: perl5_t/t/op/postfixderef.t #40 now passes (previously failing).

Verified gates:
- make: BUILD SUCCESSFUL, all unit tests PASS
- DBIC: 313/314 (1 flake, t/cdbi/68-inflate_has_a.t passes standalone)
- Moo: 71/71 PASS
- Template::Toolkit: 106/106 PASS
- op/local.t: identical baseline (no regression)

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Three implementation paths analyzed, recommendation Path 3 (AST analysis).
Step D remains deferred pending careful DBIC stress-testing.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
When a `do { ... }` block's last expression is a "fresh-result" operator
(`!`, `not`, `==`, `eq`, comparison ops, `defined`, `exists`, `ref`,
`length`, `scalar`, `wantarray`, `isa`, or a literal), the block's return
value is guaranteed to be a brand-new RuntimeScalar (boolean/number/string)
that does NOT alias any inner my-var or container.

In that case it is safe to flush the MortalList at do-block scope exit,
which fires DESTROY for transient blessed objects whose lexical owners
went out of scope inside the do-block. This matches Perl 5's
SAVETMPS/FREETMPS semantics and fixes op/do.t test #70 RT 124248:

  f(do { 1; !!(my \$x = bless []); });

For do-blocks whose last expression is anything else (e.g. a variable
read like `\$x`, a function call, a reference operator, etc.), the
existing flush=false behavior is preserved. This keeps DBIC's
`\$self->{cursor} ||= do { my \$x = ...; \$x }` pattern working — the
result IS an inner my-var, so we must not flush.

Implementation: small whitelist of always-fresh-result operators in
EmitBlock.doBlockResultIsAlwaysFresh(). When the do-block's last element
matches, pass flush=true to emitScopeExitNullStores. Conservative —
returns false for anything not on the whitelist.

Verified gates:
- make: BUILD SUCCESSFUL, all unit tests PASS
- DBIx::Class: 314/314 PASS, 13858 subtests, Result: PASS
- Moo: 71/71 PASS
- Template::Toolkit: 106/106 PASS
- op/do.t: 69/73 (matches master baseline; RT 124248 now passes)
- perl_test_runner perl5_t/t/op/: net +1 ok / -1 not_ok = exactly the
  RT 124248 fix, zero new regressions across 43524 op/ tests.

Closes the last regression on feature/dbic-final-integration vs master.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant