You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
regression: tracingChannel().traceCallback loses every events.push (test_gap_diagchannel_3082_3084_3085_3086, unnoticed since 2026-07-30) #7521
test-files/test_gap_diagchannel_3082_3084_3085_3086.ts is a non-snapshot gap
failure on main — it is in neither test-parity/gap_snapshot.json nor test-parity/known_failures.json, so nothing records it as accepted.
The #3086 block (tracingChannel(...).traceCallback) produces an empty events array:
$ node --experimental-strip-types test-files/test_gap_diagchannel_3082_3084_3085_3086.ts # 23 lines, exit 0
$ perry test-files/test_gap_diagchannel_3082_3084_3085_3086.ts -o /tmp/dc && /tmp/dc # exit 0, one blank line where 6 should be
Everything before it matches byte-for-byte, including 3086 ret: target-ret —
so traceCallbackdid invoke target and did return its value. What is
lost is every events.push(...): the four subscriber handlers AND the events.push("fn args:…") inside target itself. events.join("\n") then
prints one empty line.
That target ran but its own events.push did not land in the array the console.log reads is the interesting part — it points at the array binding
captured by the traced function, not at the subscriber plumbing.
This is a regression, not a host quirk
I originally filed this (in #7518) as a suspected host-local flake because it
reproduced on clean main. That reasoning was wrong: reproducing on clean main
is evidence of an unnoticed regression, not of a host quirk. Measured on one
host with --profile perry-dev builds of -p perry -p perry-runtime -p perry-stdlib -p perry-runtime-static -p perry-stdlib-static,
own build at every point:
commit
date
verdict
17c0ff952
2026-07-30
PASS
c6ed8175d
2026-07-30
PASS
33acba4c1 (main)
2026-08-06
FAIL
So the break is somewhere in c6ed8175d..main. Bisecting that window is the
first step.
Why it went unnoticed
parity (which runs the gap suite) is gated to tag pushes only — see CLAUDE.md,
"Workflow Requirements" — so a gap regression lands on main with no red check
and the tag-time run is far from the commit that caused it. Same mechanism as #7518.
Repro
perry test-files/test_gap_diagchannel_3082_3084_3085_3086.ts -o /tmp/dc && /tmp/dc
diff <(node --experimental-strip-types test-files/test_gap_diagchannel_3082_3084_3085_3086.ts)<(/tmp/dc)
Node 26.5.1 (the .node-version pin).
Acceptance
test_gap_diagchannel_3082_3084_3085_3086 byte-identical to node with exit 0,
plus the bisected commit.
Confirmed still failing at v0.5.1308 on the pinned quiet mini (full 489-test gap run, node 26.5.1 oracle).
Context from that run: it was one of only three failures outside gap_snapshot.json, and the other two are also tracked (#7548 proxy-mutator hang, #7522 zlib dead-strip). The run covers 43 PRs merged today — GC layout installation, write barriers, spread semantics, JSON materialization, constructor lowering — and introduced no new failures, so this remains exactly what it was: a regression from the c6ed8175d (#6853) window, unrelated to recent work.
That window has now produced three confirmed regressions: #7518 (fixed in #7520), this one, and possibly #7548 (bisect in progress). Worth treating the window itself as suspect rather than each symptom separately — #6853 was "fix(globals): complete Node parity", and #7518's root cause was a one-line data-table addition whose damage landed three files away. A second look at what else that PR's table completion un-gated may be cheaper than three independent bisects.
Two data points that narrow this, both on current main (v0.5.1323, 2b54d58a0)
1. It is not an auto-optimize artifact. My first repro linked the perry-auto-* archive, which matters because #7475 blames that same
feature-stripped .a for two other kernel failures. Recompiled with PERRY_NO_AUTO_OPTIMIZE=1: identical failure, same six lines lost. So this
is not in the #7475 family and the ordinary runtime is affected.
2. The closure-capture shape alone is NOT sufficient to reproduce. The
issue reasons that "target ran but its own events.push did not land" points at
the captured array binding. That is the right read of the symptom, but the
binding shape on its own is fine. All three of these match node exactly:
bare block + function declaration capturing a block-scoped const array, called directly
the same, invoked through .apply(thisArg, args)
an arrow capturing the block-scoped const, stored in an object literal, called via the property
So the fault needs traceCallback itself in the picture — the capture does not
break just because the callee is reached indirectly or through a this-bound
call. That argues against a general closure-capture regression in the c6ed8175d..main window and for something specific to how trace_callback_method_closure / diag_trace_callback re-enters the target,
most likely the synthetic-arguments closure path that forwards the surrounding
args (diagnostics.rs:1653, the position-honoring form added for #3086).
That is worth trying before paying for the full bisect, since it is a
one-file read; if it does not fall out, the bisect window is unchanged.
Note both subscriber handlers (arrows) andtarget (function declaration)
lose their pushes, so whatever goes wrong affects the array itself rather than
one callee's capture.
traceCallback is a bystander. Following the "read diagnostics.rs first" hint
produced nothing, but the reduction ladder collapsed fast: the very first probe
showed the block failing with traceCallback removed entirely (just calling target directly), then with subscribe removed, then with the channel
never created. The minimal repro has no traceCallback, no subscribers, no
closures beyond one, and no array:
import{tracingChannel}from"node:diagnostics_channel";// ANY import will do{constevents: string[]=[];functiont1(a: any){events.push("fn:"+a);}t1("A");console.log(JSON.stringify(events));// node: ["fn:A"] perry: []}
Delete the import and it passes — which is exactly why the three probes on the
issue were green. The import's only role is to make the file an ES module, hence
strict, which selects the affected lowering. It is also not array-specific:
objects, numbers and strings lose identically (const acc = {n:0} reads undefined inside t1, so acc.n++ throws).
Where:crates/perry-codegen/src/stmt/mod.rs, emit_preallocate_boxes. It
allocated a box — and, decisively, a ctx.locals slot — for every id in a Stmt::PreallocateBoxes, including ids already promoted to @perry_global_<mod>__<id>. Codegen has a rule for that collision, written out
at every other read/write site (ctx.boxed_vars.contains(id) && !ctx.module_globals.contains_key(id)), and this was the one place that never
checked. ctx.locals is consulted beforectx.module_globals on the Stmt::Let reuse path and the LocalGet/LocalSet store paths, so the
declaration wrote into the box-pointer slot, the global was never stored, and
the closure — which reads the global with zero captures — saw the undefined it
was defined with. The IR says it plainly: @perry_global_m3_ts__1 is defined,
registered as a GC root, read and written by the closure, and never stored by main.
When:lower_strict_block_fn_decls was added by #7105 (76fa7b4c9,
2026-08-01) — it made a bare block at the top level of a strict module emit PreallocateBoxes for its lexical bindings, the first time a promoted
module-level id was ever handed to emit_preallocate_boxes. Both commits
recorded here as PASS (17c0ff952, c6ed8175d, 2026-07-30) predate it, so the
recorded evidence was consistent and no bisect was needed. The #6853 window is
not involved — worth un-suspecting it for #7548 too.
Scope is much larger than this test. Every ES module with a top-level { … }
block containing a function declaration that reads a sibling let/const
compiled to a silently-empty binding. Nothing threw, so it was invisible
wherever no test printed the accumulated value.
Regression coverage went in as an in-src unit module
(crates/perry-codegen/src/stmt/prealloc_module_global_tests.rs) rather than crates/*/tests/*.rs, so it runs in the per-PR cargo-test gate instead of only
nightly — this gap test is in neither gap_snapshot.json nor known_failures.json and parity is tag-gated, which is the whole reason it sat
since 2026-07-30.
Activity
proggeramlug commentedon Aug 6, 2026
Confirmed still failing at v0.5.1308 on the pinned quiet mini (full 489-test gap run, node 26.5.1 oracle).
Context from that run: it was one of only three failures outside
gap_snapshot.json, and the other two are also tracked (#7548 proxy-mutator hang, #7522 zlib dead-strip). The run covers 43 PRs merged today — GC layout installation, write barriers, spread semantics, JSON materialization, constructor lowering — and introduced no new failures, so this remains exactly what it was: a regression from thec6ed8175d(#6853) window, unrelated to recent work.That window has now produced three confirmed regressions: #7518 (fixed in #7520), this one, and possibly #7548 (bisect in progress). Worth treating the window itself as suspect rather than each symptom separately — #6853 was "fix(globals): complete Node parity", and #7518's root cause was a one-line data-table addition whose damage landed three files away. A second look at what else that PR's table completion un-gated may be cheaper than three independent bisects.
proggeramlug commentedon Aug 7, 2026
Two data points that narrow this, both on current
main(v0.5.1323,2b54d58a0)1. It is not an auto-optimize artifact. My first repro linked the
perry-auto-*archive, which matters because #7475 blames that samefeature-stripped
.afor two other kernel failures. Recompiled withPERRY_NO_AUTO_OPTIMIZE=1: identical failure, same six lines lost. So thisis not in the #7475 family and the ordinary runtime is affected.
2. The closure-capture shape alone is NOT sufficient to reproduce. The
issue reasons that "target ran but its own
events.pushdid not land" points atthe captured array binding. That is the right read of the symptom, but the
binding shape on its own is fine. All three of these match node exactly:
functiondeclaration capturing a block-scopedconstarray, called directly.apply(thisArg, args)const, stored in an object literal, called via the propertySo the fault needs
traceCallbackitself in the picture — the capture does notbreak just because the callee is reached indirectly or through a
this-boundcall. That argues against a general closure-capture regression in the
c6ed8175d..mainwindow and for something specific to howtrace_callback_method_closure/diag_trace_callbackre-enters the target,most likely the synthetic-arguments closure path that forwards the surrounding
args (
diagnostics.rs:1653, theposition-honoring form added for #3086).That is worth trying before paying for the full bisect, since it is a
one-file read; if it does not fall out, the bisect window is unchanged.
Note both subscriber handlers (arrows) and
target(function declaration)lose their pushes, so whatever goes wrong affects the array itself rather than
one callee's capture.
proggeramlug commentedon Aug 7, 2026
Root cause found — it is not
diagnostics_channel, and the window is #7105, not #6853PR #7580.
traceCallbackis a bystander. Following the "readdiagnostics.rsfirst" hintproduced nothing, but the reduction ladder collapsed fast: the very first probe
showed the block failing with
traceCallbackremoved entirely (just callingtargetdirectly), then withsubscriberemoved, then with the channelnever created. The minimal repro has no
traceCallback, no subscribers, noclosures beyond one, and no array:
Delete the
importand it passes — which is exactly why the three probes on theissue were green. The import's only role is to make the file an ES module, hence
strict, which selects the affected lowering. It is also not array-specific:
objects, numbers and strings lose identically (
const acc = {n:0}readsundefinedinsidet1, soacc.n++throws).Where:
crates/perry-codegen/src/stmt/mod.rs,emit_preallocate_boxes. Itallocated a box — and, decisively, a
ctx.localsslot — for every id in aStmt::PreallocateBoxes, including ids already promoted to@perry_global_<mod>__<id>. Codegen has a rule for that collision, written outat every other read/write site (
ctx.boxed_vars.contains(id) && !ctx.module_globals.contains_key(id)), and this was the one place that neverchecked.
ctx.localsis consulted beforectx.module_globalson theStmt::Letreuse path and theLocalGet/LocalSetstore paths, so thedeclaration wrote into the box-pointer slot, the global was never stored, and
the closure — which reads the global with zero captures — saw the
undefineditwas defined with. The IR says it plainly:
@perry_global_m3_ts__1is defined,registered as a GC root, read and written by the closure, and never stored by
main.When:
lower_strict_block_fn_declswas added by #7105 (76fa7b4c9,2026-08-01) — it made a bare block at the top level of a strict module emit
PreallocateBoxesfor its lexical bindings, the first time a promotedmodule-level id was ever handed to
emit_preallocate_boxes. Both commitsrecorded here as PASS (
17c0ff952,c6ed8175d, 2026-07-30) predate it, so therecorded evidence was consistent and no bisect was needed. The #6853 window is
not involved — worth un-suspecting it for #7548 too.
Scope is much larger than this test. Every ES module with a top-level
{ … }block containing a
functiondeclaration that reads a siblinglet/constcompiled to a silently-empty binding. Nothing threw, so it was invisible
wherever no test printed the accumulated value.
Regression coverage went in as an in-
srcunit module(
crates/perry-codegen/src/stmt/prealloc_module_global_tests.rs) rather thancrates/*/tests/*.rs, so it runs in the per-PRcargo-testgate instead of onlynightly — this gap test is in neither
gap_snapshot.jsonnorknown_failures.jsonandparityis tag-gated, which is the whole reason it satsince 2026-07-30.
fix(codegen): PreallocateBoxes must not shadow a module-level global
fix(codegen): PreallocateBoxes must not shadow a module-level global (#…