Skip to content

perf+merge: port DBIC-safe optimizations onto 100%-passing DBI baseline (5.6 → 12 Mcells/s)#552

Closed
fglock wants to merge 29 commits intomasterfrom
perf/dbic-safe-port
Closed

perf+merge: port DBIC-safe optimizations onto 100%-passing DBI baseline (5.6 → 12 Mcells/s)#552
fglock wants to merge 29 commits intomasterfrom
perf/dbic-safe-port

Conversation

@fglock
Copy link
Copy Markdown
Owner

@fglock fglock commented Apr 23, 2026

Goal

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.

This branch was originally developed as a 161-commit work branch, then
flattened to a single commit on top of origin/master for review.
The full per-commit history is preserved on the branch
backup/perf-dbic-safe-port-pre-flatten.

Performance

~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

Correctness fixes

  • Revert bca73bd5 ("scope-exit LIFO reverse"). That commit's
    LinkedHashMap + Collections.reverse() combination actually
    produced declaration-order cleanup, breaking
    destroy_eval_die.t test 4 ("DESTROY fires in LIFO order"). Restores
    the prior HashMap-iteration behaviour, which 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. This is the
    idiom used by DBIC's DBICTest::Util::OverrideRequire.

  • 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::OverrideRequiret/53lean_startup.t.

  • Narrow master commit 7f3e0d12d's stash-alias canonicalisation:
    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.

DBI.pm (reverted to pre-merge minimal)

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.

Infrastructure / tooling

  • 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 + followup bug list.
  • 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.

Verified state at 4329ccd24

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)

How to reproduce

git fetch origin
git checkout perf/dbic-safe-port
make                           # BUILD SUCCESSFUL
./jcpan -t Moo                 # PASS
./jcpan -t Template            # PASS
./jcpan -t DBIx::Class         # ~22 min, PASS 13858/13858, 0 Dubious

Follow-up work (tracked in dev/design/perf-dbic-safe-port.md)

  • Proper DBI 1.647 migration (fix PurePerl connect/Active so DBIC
    passes unmodified under upstream DBI).
  • Fix JSON t/13_limit.t (recursion/limit).
  • Fix Crypt::OpenSSL::Bignum exponent returning 17 instead of 65537
    (Net-SSLeay/33_x509_create_cert.t).
  • Fix Text-CSV/55_combi.t subtest 26 content mismatch.

Generated with Devin

@fglock fglock changed the title perf: port DBIC-safe subset of life_bitpacked optimizations (5.6 → 13.27 Mcells/s) perf+merge: port DBIC-safe optimizations + rebase onto master (5.6 → 12.6+ Mcells/s) Apr 23, 2026
@fglock fglock changed the title perf+merge: port DBIC-safe optimizations + rebase onto master (5.6 → 12.6+ Mcells/s) perf+merge: port DBIC-safe optimizations onto 100%-passing DBI baseline (5.6 → 12 Mcells/s) Apr 24, 2026
… 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.

## Goal

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.

## Performance

~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

## Correctness fixes

- 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.

## DBI.pm (reverted to pre-merge minimal)

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.

## Infrastructure / tooling

- `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.

## Measured at this commit

| 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) |

## Reference

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>
@fglock fglock force-pushed the perf/dbic-safe-port branch from 7294556 to 4329ccd Compare April 24, 2026 15:32
fglock and others added 26 commits April 24, 2026 18:24
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>
fglock and others added 2 commits April 25, 2026 21:21
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>
fglock added a commit that referenced this pull request Apr 26, 2026
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>
fglock added a commit that referenced this pull request Apr 26, 2026
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>
fglock added a commit that referenced this pull request Apr 26, 2026
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>
fglock added a commit that referenced this pull request Apr 26, 2026
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>
fglock added a commit that referenced this pull request Apr 26, 2026
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>
fglock added a commit that referenced this pull request Apr 26, 2026
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>
@fglock
Copy link
Copy Markdown
Owner Author

fglock commented Apr 27, 2026

Superseded by #566 (feature/dbic-final-integration), which has been merged to master.

This PR's commits were consolidated into #566's branch via rebase, along with the subsequent regression fixes (Steps A-D) that closed all remaining DBIC-final regressions while maintaining 314/314 DBIC parity.

@fglock fglock closed this Apr 27, 2026
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