Commit Graph

230 Commits

Author SHA1 Message Date
754944a755 wcc/cgen: #121 indexed tuple-element read + literal-store round-trip (both-stage)
Reading or storing a tuple element of an indexed array element was
broken across the board (the fold-6 read-path). One fused commit,
both stages, four faces of indexed tuple-element access:

 - FIELD read `tbl[i].N`: was loud ("unsupported field-read shape" --
   the field-read dispatch keyed on an N_IDENT base; an INDEX base fell
   to a fatal). Now resolves &tbl[i] via the place-spine and reads the
   field at addr+foff through the existing per-kind arms (str-triple /
   scalar / fn-ptr).
 - WHOLE read `let e = tbl[i]`: was a silent word0-only truncation
   (plain-tuple kin of #37/#58, which covered only tagged). Now a full
   cursor fill from &tbl[i].
 - STORE `a[i] = (3,4)` (N_TUPLE-literal rhs): was a silent word0-only
   store -- the write face of the read. The aggregate-store-into-index
   site handled ident/dot/deref tuple rhs but not the literal; now it
   materializes the literal and word-copies. Narrow: N_IDENT base only
   (N_DOT/chained stay deferred, #270).
 - for-range over a const-slice-of-tuple: was a divergent SEGV; now a
   symmetric loud-stop on both stages (filed #122).

The store and read were a round-trip that passed test 809 only by luck
(broken store XOR broken read canceled). Fixing the read alone exposed
the silent store; rule-7 obliges fixing both, so 809 is now genuinely
correct, not luck-correct. Both faces are byte-id-blind (#263) -- the
net is a runtime round-trip pin with distinct-per-word values and a
real call clobbering the cursor registers between store and read, so a
word0-only store or read is caught. Both stages byte-identical
(990-997 green). Pin 947_tuple_index_read_run.
2026-06-06 22:23:43 +09:00
351abb0ab3 wcc/cgen: #58 indexed tagged-field read+assign cursor arm (both-stage)
Reading or writing a tagged field of an indexed array element
(xs[i].field) was broken on BOTH stages, byte-identically and
silently (#263 gate-blind): the arr[i].field branches had arms for
array/str/slice/float but no TY_TAGGED arm, so the tagged field fell
to the single-word scalar path. READ loaded only the tag word (stale
payload -> `xs[i].min as T` read garbage); ASSIGN stored the raw
unboxed scalar into the tag slot, corrupting the box.

Insert a TY_TAGGED cursor arm before each scalar fallback, both
sites both stages (cgen.c read + assign; cgenexpr.ww cgdot N_INDEX-lhs
read + cgassign indexed-field). READ mirrors cg_tagged_memread
(payload -> DX/CX/R8, tag -> AX last). ASSIGN synthesizes the tag for
the concrete variant (taggedvariantindext) and stores tag+payload via
the str/slice 3-word store spine -- not the source-remap widener
(concrete rhs has no source tag to remap).

>32B / multi-word / float payloads are loud-stopped at all four arms
(emission not yet wired; see #114). That shape is reachable today via
a narrow-variant ctor, so it louds rather than silently miscompiling.
Both stages get the same arm -> byte-id preserved (990-997 green; the
runtime is the net for this #263 class). Pin 944_idx_tagged_field_run
(read/assign runtime rows + >32B expect-loud rows).
2026-06-06 16:21:45 +09:00
5d596206c6 wcc/cgen: #94 def-array indexed &-base leg (both-stage)
`&D[i]` over a module-level DEF array SEGV'd on BOTH stages: the
TK_AMP N_INDEX N_IDENT base classify checked only the local and let
legs, so a def-array base fell to a wrong else — cstage zero-based
the addend (XORQ BX,BX -> wild pointer, cgen.c) while wwstage
value-loaded the symbol (MOVQ name(SB) = D[0], not its address,
cgenexpr.ww complex-base fallback). Divergent asm, both wild.

Add one def-array leg per stage, mirroring the working let leg:
- cs: `def_isarraydef(base) -> LEAQ name(SB),BX` alongside let_islet.
- ww: the `defvartnode` fallback the read-side cgindex already takes
  (cgenexpr.ww:1762) -> N_TARRAY classifies isglobalarr -> LEAQ
  name(SB).
The def DATA symbol already exists (plain &D + D[i]-read work), so
once the base is the address the existing i*esz scale + ADDQ
round-trips. cs and ww now emit BYTE-IDENTICAL LEAQ-SB asm — the
both-broken -> both-correct convergence is the point (#263 class).

Rows (944_def_amp_idx_run, all 0/0 byte-id): amp_int [3]int,
amp_u32 [3]u32 esz=4 (narrow scale), amp_arg &D[2] as a func-arg;
controls ctrl_plain (&D), ctrl_read (D[i]), ctrl_2d (&M[1][1]) keep
working. *p spelled `let v: T = *p` — `*p: T` parses as `*(p: T)`.

OUT (filed #112): &D[..] slicing a def-array is a distinct parse
reject needing a Hare-fidelity ruling — not this leg.
2026-06-06 13:27:24 +09:00
26ba1ad1b5 wcc_ww/cgen+peellint: #109 close #101 primsize-alias family by construction
Route the 16 routable bare-primsize GUARD sites (is-primitive /
struct-vs-prim dispatch) through the #101 aliasprimsize SSoT helper.
Byte-NEUTRAL by construction: an alias-narrow name is already
neutralized downstream by the same arm, so routing emits no new asm
(the empty-flip-set ken oracled).
  Shape-A exclude-prim-early (3): cgenutil sretretsize / structparamsize
    / structfloatclass — `primsize>0 return` then structlookup→nil
    returns the same value; route returns it early, same.
  Shape-B prim-guard-then-structlookup (13): cgenutil 4604/4650 +
    cgenexpr 4136/10244 + the 9-site CALL/assign cluster — primsize==0
    →structlookup→nil→fall to normal; route skips the block→same normal.

Install the peellint bare-primsize FINALE (B7 lint-fuse contract):
tools/peellint now rejects any bare primsize() in the ww stage outside
the annotated whitelist.  Evasion-hardened per the B7 lesson — a
character scan (comments + string/char literals stripped first) and a
LEFT+RIGHT word-bounded match of the bare `primsize` TOKEN (not just
`primsize(`), so the aliasprimsize() wrapper is never a hit and every
compiling spelling reds: the call primsize(nm), the paren-wrap
(primsize)(nm), the function-value bind `let p = primsize`, and any
line-split.  ww-only (the C stage dealiases via type_chase_named, no
primsize symbol).  Two independent exemption windows (peel-ok vs
primsize-ok) so neither rule blinds the other.  Runs as a make-test dep.

Whitelist the 6 designed exemptions with primsize-ok WHY-annotations:
  machinery — aliasprimsize body (SSoT chase) | typenodeprimresolved +
    exprprimresolved (#11/#33 prim-resolver chasers) | cgcast leaf-loop +
    cgenexpr #11 deref-store (own ps==0 fallback; route would regress
    #11) | the primsize oracle/definition itself (nothing below to chase).
  structural — elemsizeof x2 + paramfieldsize (chase lives in the -c
    twin elemsizeofc; threading c is the dormant #110).

Empty-flip-set proof: zero C bytes; cstage binaries bit-identical;
bootstrap byte-id 990-997 + 950 all green (w6c == w6c_ww on the full
selfhost, self-rebuild identical); combined.ww (w6c + wwdump) regen
idempotent; sizelint 0; peellint 0 (raw-peel AND bare-primsize over the
whole tree = the close-by-construction proof, zero unwhitelisted
survivors).  Tests: 944_peellint_gate +14 rows (bare / space-before-paren
/ name-at-EOL split / string-blind opener / paren-wrap / fn-value-bind
RED; aliasprimsize wrapper + primsize-ok annotated GREEN; corrupt
annotation RED; independent peel/primsize windows; C-file out-of-scope).

Closes the #101 primsize-alias family by construction.  #109.
2026-06-06 13:06:37 +09:00
4459a49d3a wcc/cgen: #87 plain tagged-union module-global DATA + match SB-resolution (both-stage)
A PLAIN (non-alias) module-level tagged-union global SEGV'd on BOTH
stages: no static DATA was emitted (let_emit_size/letemitsize returned 0
for TY_TAGGED) so the global was never registered, and the match
scrutinee resolved it as a frame-local at offset 0 — reading saved BP as
the tag. Two sub-sites, one route (neither half ships alone — DATA
without SB-resolution still SEGVs; SB-resolution without DATA reads
nothing):

(a) DATA-emitter — a non-nullable TY_TAGGED arm emits the box that
    byte-MIRRORS a runtime LOCAL of the same type: tag word at +0 (the
    const-selected variant index via cg_tag_for_variant / taggedvariant-
    index), payload at +8, zero-padded to the union box size. int and
    str/slice literal variants are wired (str carries a DATAR ptr patch
    at +8); any other variant payload loud-stops (rule 7). emit_tagged_
    data + emittaggeddata are the per-stage twins; let_pre_intern/
    letpreintern gain the matching str-variant intern. Nullable stays 0
    so the (*T|void) one-word fold keeps the 8B scalar arm.

(b) match-scrutinee global resolution — the PLAIN-tagged twin of #78:
    a global tagged ident scrutinee LEAQs name(SB) and copies the box
    into an @match_spill slot the dispatch indexes off BP.

DATA target (mirror of the local box, verified byte-for-byte): for
(i32|str)=42 the 32B box is tag0 | 42@8 | zero-pad; for ="x" it is
tag1 | ptr0@8(DATAR _S_n) | len@16 | cap@24. cs and ww emit byte-
identical asm.

Pins (rob §3, dual-stage 910 cstage + 997 wwstage, attest_pass.ww): the
tagged global-vs-local byte-identity pin (match over the GLOBAL gives the
same arm/value as over a LOCAL — was SEGV both stages) and the str-
variant tag-1 pin, plus the #86 tuple global-vs-local lock-pin guarding
the already-correct emitter path.

929 fail_global_src graduates: a >48B tagged GLOBAL by-value arg now
resolves through the cgplaceaddr MEMORY-class arm (LEAQ g(SB) + blit)
instead of the #38b loud-stop, and runs correctly (uninit zero box ->
first variant); the row becomes a positive run pin. The struct-variant
>48B init still loud-stops via the data emitter.

The first-class-VALUE copy of a tagged ident (`let q = g`) stays a
pre-existing silent #49/#46 sibling (local and global identically),
filed separately — out of this fold's two sub-sites.
2026-06-06 12:05:56 +09:00
45f5415209 wcc_ww/cgen: #101 narrow-alias fill-stride via aliasprimsize SSoT
A struct-literal array fill sized a narrow-alias element off a bare
primsize(name): `type my32 = u32` gave primsize("my32")=0, so the
element width defaulted to 8 and a [3]my32 strode MOVQ -24/-16/-8 —
field n collided with arr[2] (kw1_101 run exit 1). cstage chases
my32->u32->4 (MOVL stride-4) at the twin sites and is runtime-correct;
this is a ww-only align-up, cs untouched.

Fix: a new aliasprimsize(c, nm) SSoT helper — primsize(nm), else an
aliaslookup-chase N_TNAME loop then primsize — and route the SIZE-use
primsize() family through it. The 7 c-bearing bare-no-chase size-use
sites are routed: cgen:993 (letemitsize), cgenstmt:1896 (cgarrlitfillbp),
cgenutil:1579 (elemsizeofc fallback)/1657+1665 (nodeprimwidth)/4791
(cgstructlitfill = the kw1_101 site), cgenexpr:6902 (cgcall vararg esz).
This is the rule-13 close-by-construction shape (one accessor for
"resolved primitive size"), not a per-site patch.

kw1_101 is the SOLE asm mover (byte-id NO->YES, run 1->0, MOVL
stride-4); every other routed site is latent/byte-neutral. Bootstrap:
all 5 combined units stay w6c==w6c_ww byte-identical. sizelint 0,
peellint 0, test-unit 296/296.

Scope fence (rob route-7-decline-6 ruling): three DESIGNED-exemption
sites carry inline primsize-ok annotations — elemsizeof :1475/:1499 and
paramfieldsize :3541 are structural (no-`c`, non-chasing) BY DESIGN;
their alias-chasing twin elemsizeofc is the routed :1579 leg. These are
the #109 peellint-whitelist seeds. Three further declines are already
correct chasing paths, not bare-no-chase bug shapes (typenodeprimresolved
:2026 / exprprimresolved :2063 are the chase machinery itself; cgassign
:7631 already chases via typenodeprimresolved, #11). The ~17 GUARD sites
(is-primitive dispatch) + the peellint finale are the committed #109
follow-on. Threading `c` into the structural sizers is dormant #110.

#101
2026-06-06 10:04:37 +09:00
fc50a27f3e cgen: #95 c3 reviewer-fold — is/as gate exact-only, no widening leak
c1/c2 widened flatvariantidxt (selfhost) with the chain + structural
tag-synthesis arms and a >=2 ambiguity os.exit, scoped to the cgen
WIDEN consumer. But flatvariantidxt is a choke-point: the wwstage is/as
ACCEPTANCE gate (check.ww:4677, the #198 spread fallback) reuses it, so
the widening leaked into checker acceptance — vs base 329481c:
  * `let v:(void|ali)=…; v is base` (ali=base): cstage rejects, wwstage
    ACCEPTED+built — new cs!=ww acceptance divergence (rule-10 break);
  * `(void|tb)`, `v is ta` (unrelated same-layout): same leak via the c2
    structural arm;
  * `(ali|ali2)`, `v is base`: wwstage DIED with the cgen-internal fatal
    "flatvariantidxt: source alias chain reaches >=2 variants" DURING
    CHECK — a cgen diag surfacing in the checker (layering).
cstage is unaffected: its is/as gate (check.c:2036) is independent of
cg_tag_for_variant (cgen-phase only), so the fuse was already broken at
this site — the cgen-helper change moved wwstage's CHECKER but not
cstage's. This contradicts the #95 fold scope ("cgen-tag fold, no
acceptance change except the ambiguity hard-error [at the widen site]").

Fix (rob-ruled): the is/as gate needs only nominal variant membership =
pass 1. Add an explicit `exactonly` mode to flatvariantidxt — the
checker caller passes true (returns after the exact loop: no chain/
structural arms, no os.exit), every cgen caller passes false (full
tag-synthesis, unchanged). Two consumers, two modes — the honest
representation, not a wrapper. cstage's cg_tag_for_variant has no twin
checker caller, so it stays full-only and is UNTOUCHED by c3 (rule-10
satisfied: the param changes no asm — cgen always passes false; the
checker now MATCHES cstage's reject). casevariantin still backs the
#198 spread fallback.

Pins (test/wcc/944_variant_chain_b95_run.c, +4 rows -> 56 checks):
  isas_chain_reject / isas_unrel_reject — BOTH stages reject the leaked
  is/as shapes (shared experr substring "not a variant"); the c1 chain +
  c2 structural arms no longer widen acceptance.
  isas_amb_reject_notcrash — the (ali|ali2)/`is base` shape rejects
  CLEANLY (the cgen fatal text would be absent -> red), NOT a crash.
  twin_prim_alias_amb — rob's obligated mixed prim/alias TWIN:
  (int | ai) ai=int, source aj=int — both share the int bottom under
  all-variants counting, so the cgen WIDEN (full mode) hard-errors
  ("source alias chain reaches >=2 variants"), pinned LOUD both stages.

The deferred question (should is/as EVER accept cgen's richer chain/
structural shapes? = a checker-strictness feature, both stages together)
is filed as task #107, explicitly NOT folded here.

Invariants: c1/c2 cgen behavior unchanged (all cgen callers pass false =
full mode); suite byte-id rows + the dissolution corpus hold. make all
0; sizelint 0; peellint 0 (the mode param adds no peel sites); combined.ww
regen idempotent; test-unit "all 295 tests passed". c3 touches ZERO
cstage bytes — cmd/w6c/cgen.c carries only the c1/c2 additions, and
cmd/wcc/check.c is unchanged from base 329481c.
2026-06-06 08:07:03 +09:00
05f7af76d1 wcc_ww/cgen: W2 #102 indexed-elem field store + &-field gates chase the alias chain
The two ww-side gates from ken's B6-c3 STOP re-attribution
(.ai/ken-b6-oracle.md addendum): WWSTAGE was the alias-blind side at
exactly two cgenexpr.ww sites, both keyed on a bare name-keyed
structlookup that only holds struct-decl names, so an alias-NAMED
spelling missed and fell to the generic-but-runtime-correct route —
byte-divergent from the canonical dedicated shape cs pins post-B6-c3:

  (1) `arr[i].f = v` indexed-elem struct-field STORE arm (the write
      twin of the task-#8 READ arm, which already chases via tichase);
  (2) `&p.f` ptr-field fallback in the TK_AMP N_DOT single-dot leg.

REVIEW AMENDMENT (reviewer-W2, rob's close-by-construction
obligation): the proof-sweep of the two arms found ONE same-class
survivor — (3) the value-struct `&x.f` leg, one leg below (2) in the
same single-DOT arm, same alias-blind bare structlookup. Probe
rW2_ampv reproduced the identical bit-proven mechanism (ww_alias !=
ww_plain; cs_alias == ww_plain) at both base and the frozen tip; the
other in-arm routes are clean (letvar* global helpers chase, str/len
pseudo-field alias legs are filed-loud #96, chained depth>=2 resolves
via tinfo). Same chase applied; arm survivor count is now zero by
the same grep+probe construction.

All three now resolve through structlookupchain (#22) — the name
layer's own alias-chain accessor (the cstage transitive-peel mirror),
already consumed by the sibling `p.f = v` assign arm and cgdot's
pointer-to-struct read. Plain rows short-circuit at its structlookup
head, so non-alias emission is byte-identical by construction; alias
rows land on the same structinfo as their plain twins, so the
dedicated emission converges bit-exactly. esz stays sound:
elemsizeofc reads the chased stamped tinfo (#8 leg).

Convergence proven with ken's pre-staged W2 instrument: ww_alias ==
ww_plain bit-IDENTICAL for kb6_idxf and kb6_ampf (cmp exit 0), which
also lands cs==ww byte-id on both rows; plain controls byte-id held
and their ww asm unchanged base->tip. Full ken-corpus matrix vs the
B6-tip baseline: movers are exactly {kb6_idxf, kb6_ampf} NO->YES;
detectors unmoved (kw1_101 cs0/ww1, fill2 both-loud, tuparg_c
cs0/ww1, fsarg2/fsarg0/try pinned texts verbatim, B5/B6 graduations
all byte-id). Zero behavioral change on any accepted program, zero
acceptance change in either direction.

ww-only: zero C-source bytes; cs binaries (w6c/ww/w6a/w6l) md5
bit-identical base->tip; w6c_ww + wwdump main.combined.ww regen'd via
make, idempotent. 989 lib ratchet: zero flips (31 byte-identical /
9 pinned-divergent #59 / 3 pinned-wwreject #59 across 43 units).

944_alias_cgen_b6_run: idxf_2lvl + ampf_2lvl graduate K_RUN_NOID ->
K_RUN (the two restored byte-id cells) — B6-c3's mutation teeth
restored (reviewer-B6 N-a closes); + amendment rows ampv_2lvl /
ampv_plain_ctl pin the third gate (checks 61 -> 69). All 9 944-family
suites green; sizelint 0; make test-unit "all 292 tests passed".
2026-06-06 05:11:29 +09:00
d5cb1bd69e wcc_ww/cgen: #82 cgun &base[i] classify off the chased stamped base type
The TK_AMP N_INDEX arm keyed arrayness off the SYNTACTIC tnode (local
leg isarr at the baselocal read; global leg isglobalarr/isglobalptr at
the letvartnode read) — an alias-typed base (tnode N_TNAME) missed the
N_TARRAY gate, so the base materialized as MOVQ (element-0 VALUE)
instead of LEAQ (storage address): wild pointer, SEGV/corruption on
the deref. SILENT class (metric-1). The global leg graduated from
latent to live when g-fold #77/#78 landed alias-global DATA emit.

Fix re-keys both legs off tichase(base.type_) gated on TY_NAMED — the
landed cgindex #60 idiom (cgenexpr.ww:1800-1820). cstage already
classifies off the chased type (type_chase_named, cmd/w6c/cgen.c:
4172-4188) and is the runtime-correct reference: align ww UP. esz does
NOT move — elemsizeofc chases internally since batch-2 (PREMISE-2
probe-confirmed via amp_narrow: stride right, base wrong pre-fix).
Non-alias rows byte-id-neutral by construction (TY_NAMED gate).

Pin: 944_alias_amp_idx_run, 8 rows through the taken pointer (plain
local/global+str controls hold 0/0; 1/2-level alias local + global,
fwd-ref decl order, narrow [4]u32 graduate cs0/wwSEGV-byte-id-NO ->
0/0 byte-id). Probed OUT, filed not fixed (spec §1 NOTE-2): &D[i]
def-array base breaks at a DIFFERENT site both stages (cs XORQ BX,BX
zero-base cgen.c:4209-4212, ww complex-base fallback; both SEGV 139).

Light gates: test-unit 289 green; sizelint 0; 989 ratchet zero flips
(31 ID / 9 DIVERGE / 3 WWREJECT pins hold); five-mains NEUTRAL vs
master-74195ac scratch build on identical inputs + cs==ww on all
five. combined.ww regens ride along (#110).
2026-06-05 23:06:46 +09:00
ef93b1637e wcc_ww/cgen: collapse cgenexpr.ww's plain TY_NAMED chase loops into tichase — byte-id neutral
Mechanical sweep, F2a batch 1 commit 2 (alias arc #5, rob F2 ruling:
one chased accessor is the only spelled way to dealias; this
enumeration seeds the future peellint whitelist). Every loop matching
the exact plain shape

    for (X != nil && X.kind == tykind.TY_NAMED) { X = X.under; };

(modulo variable name and one-line vs three-line layout, body exactly
the peel, guard exactly nil+NAMED) becomes `X = tichase(X);`. Census of
cgenexpr.ww at commit 1: 71 `.under` lines — 49 one-line peels + 21
multi-line-spelled peels, all eyes-classified as the plain shape, all
70 converted (count-asserted).

Survivor enumeration (raw `.under` occurrences left in cgenexpr.ww):
- cgenexpr.ww:2886 — comment text only ("follows the resolved
  NAMED.under pointer"), no code read. Classification: not a read.
Zero code-level raw `.under` reads remain in this file.

Byte-id evidence: pre-sweep w6c_ww (commit-1 build) vs post-sweep
w6c_ww compared over a 31-input corpus — all selfhost/cmd/*/
main.combined.ww (the compiler's own full source) plus the #60/#79
probe set — identical exit codes and byte-identical .s for every
input. make test-unit green (287). combined.ww regens ride along
(cgenexpr.ww is an embedded source).
2026-06-05 20:14:49 +09:00
da81a4c86e wcc_ww/cgen: #60+#79 alias-NAMED array/slice ELEMENT paths read the chased tinfo — tichase lands, SEGV families graduate byte-id
One class: alias-blind base+esz at the array/slice ELEMENT paths —
index read/write, slice-expr, for-range, and literal-init store. The
wwstage cgen derived element size and base addressing from the
type-AST tnode; an alias-typed base (`type arr = [4]int; let a: arr`)
shows only the N_TNAME leaf, so esz fell to a sentinel (1 on the read
side, 8 on the init-store side) and the base classified as a POINTER
(MOVQ of array words, no IMULQ): m8b_idx1/range1 SEGV 139, m8b_slice1
silent-wrong past little-endian prefix-luck (m8c_slice1big exit 2),
m7c global [2]row read SEGV via the alias-blind element-is-array
classify, and (#79, ken F2a1 oracle) `type A=[4]u32; let a:A=[...]`
stored MOVQ stride-8 over a stride-4 slot — elements 2/3 landed at
0(BP)/+8(BP), a saved-BP/RIP smash masked whenever esz==8. cstage
reads everything off the chased stamped type (type_chase_named/
idx_eff, correct post-F1), so every fixed shape graduates
ww-SEGV/silent-wrong -> 0/0 byte-id.

New tichase() in cgenutil.ww: nil-passthrough transitive TY_NAMED
peel, exact twin of cmd/wcc/type.c:160-162. Routed sites, all gated on
the stamped type being TY_NAMED (non-alias paths byte-identical):

- cgindex (cgenexpr.ww): elem facts (esz/signed/float/f32) off
  tichase(n.type_); etn falls back to n for the tagged/str/slice
  classify; LEAQ-vs-MOVQ base off the chased kind; elem-is-array
  supplemented by tinfoisarray(n.type_) for alias ELEMENTS (m7c).
- cgassign N_INDEX store + compound arms (cgenexpr.ww): esz +
  elemtn=lhs (the stamped-element idiom of the N_DOT/N_INDEX arms);
  chased-kind base classify at all four LEAQ/MOVQ sites.
- cgslice + cgbasecap (cgenexpr.ww): esz, base classify, default-hi
  (TY_ARRAY -> $alen / TY_SLICE|TY_STR -> +8 len), cap word at +16;
  global-str cap keeps the #73 carve-out.
- cgforrange (cgenstmt.ww, cross-file leg: the range pin cannot green
  without it): esz, element-node synthesis off .sub (FC0 precedent),
  isarr/isslicestr classify, alen off the chased tinfo.
- cgarrlitfillbp (cgenstmt.ww, #79): an alias [count]T arrtn is the
  N_TNAME leaf (elemn nil) — synthesise the element node off the
  chased sub so the existing prim/agg/slice/tagged/narrow dispatch
  works unchanged; `...` repeat bound off the chased alen (cstage
  cg_arrlit_fill_bp receives the pre-chased bu and reads bu->alen).
  #8-PAIR COVERAGE: this is the STORE half of #8's two size-sources.
  The elemsizeofc READ half chases the ELEMENT internally (idxeffti +
  esub peel, the #8 fix) but NOT an alias-typed INDEXABLE node — that
  leg is covered at its #60-family call sites by the gates above
  (cgindex/cgslice/store/compound/cgforrange/pusharg). Remaining
  alias-blind elemsizeofc callers are enumerated as residuals below.
- bare-let classify (cgenstmt.ww, #79 rider): `let a: arrk;` with an
  alias-to-array type took the composite zero-fill cstage doesn't
  emit (cstage keys the no-init shape on the chased lu->kind: arrays
  keep the per-index-write contract; an 8B alias-array still falls to
  the single MOVQ $0 arm). Required for the loopfill_1024 pin's
  byte-id; closes the array kind of the uninit-alias divergence.
- pusharg N_SLICE (cgenutil.ww, pulled in by the same pin rule: the
  944 slice_of_alias_arg row is a distinct lowering from cgslice):
  esz, base classify, default-hi.

Tests: new 944_alias_idx_family_run (19 rows: idx/slice/range/init
controls + 1-level + 2-level + decl-order permutations + index store
+ compound (+=, *=) + #79 [4]u32 literal-init + alias `[v...]` repeat
+ uninit [1024] loop-fill + slice1big (1000 elems, values >255,
LAST-element readback, default-hi, .cap, range count) + re-slice of an
alias slice + range over an alias slice + m7c global 2D + GLOBAL
alias-slice indexed read + slice-as-call-arg; dual-stage run +
per-row byte-id; LAST elements asserted throughout). The six
944_alias_accept_run rows citing "#60 (F2 batch 1)" flip K_RUN_CS ->
K_RUN (incl. slicefield_wholeread_2lvl: its 738d7f4-era receive-spine
divergence no longer reproduces at the F1-merged base, verified
byte-id + 0/0). 989_lib_byteid checked: no DIVERGE entry graduates
(the test fails loudly on graduation; lib has no alias-base consumers
— the shape SEGVed before this fix).

NOT pinned (g-fold territory, #77/#78): direct alias-typed global
ARRAY rows. Expected state probe-verified UNCHANGED by this diff:
`let g: arr = [...]` -> ww link-ERR (no DATA emitted), cs 1-level
runs 0, cs 2-level runs WRONG (silent). The alias-GLOBAL base legs
added here (isglobalarr reclassify, global default-hi/cap) are
cs-aligned but runtime-unreachable until the DATA emit lands.

Residuals filed with the team: alias-blind elemsizeofc callers not in
the #60 pin family — cgun &a[i] addr-of (cgenexpr.ww:4638 region,
task #82), append() on an alias-typed slice local (:5287),
`alloc([], n)` into an alias-slice let (cgenstmt.ww:2159),
arr[i].field= float store (:8536); tagged-element READ under an
alias base keeps the ident-arm nullable semantics; checker
asserttyped on `untyped_lit * rangevar` over an alias slice
(pre-existing, check.ww is batch 4, task #80); uninit alias-to-STRUCT
zero-fill unchanged (correct: cstage fills composites);
range-destructure over alias-to-tuple-slice.

selfhost/cmd/{w6c,wwdump}/main.combined.ww regenerated (cgen*.ww are
embedded sources).
2026-06-05 20:14:49 +09:00
d14a23b85e cgen: #57 in-cap tuple cursor fill keys on the DECLARED element type — tagged elems from concrete rvalues widen, both stages
The N_TUPLE literal's stamped type is CONSTRUCTED from its elements
(check.c N_TUPLE keeps untyped/concrete element types; assignability
is consumer-side), so the in-cap cursor fill — count
(tuple_lit_gpwords/tuplitgpwords) + push (tuple_lit_push_elem/
tuplitpushelem) — never saw the DECLARED tuple type. A declared-TAGGED
element whose expr is a concrete rvalue (`return (5: size, 9)` into
(un16, size)) counted ONE word and skipped the widen entirely: 2 words
sent against the receiver's declared 3-word walk, every later element
read garbage. Both stages, byte-identical, gate-blind (ken /tmp/ken57
p8/p9: t.1 read entry-junk). The let-literal twin
(`let t: (un16, size) = (5: size, 9)`) and the tagged-SECOND-elem
shift broke identically (probes q1/q2). The over-cap (sret) arm
already walks declared params (#240/#22b) — only the in-cap path was
declared-blind.

Fix threads the declared tuple type into the ONE shared helper pair
and its two loop sites:
  - tuple_lit_gpwords/tuplitpushelem take the declared elem type;
    declared-TAGGED + concrete rvalue widens into the shared tagged
    scratch (cg_tagscr_slot/tagscradd + cg_widen_tagged_store/
    cgwidentaggedstore, the cgreturn tagged-@retscr shape) and pushes
    the box words; declared-TAGGED gates the SSE row off (a (void|f64)
    box rides INTEGER eightbytes). Tagged->tagged subset (eslot
    mismatch) louds — the #23/#40 widening-remap family.
  - cg_tuple_lit_to_cursor/cgtuplelittocursor grow a decl param;
    cgreturn's in-cap N_TUPLE loops thread cg_ret_type/c.fnret.list
    (the same pp/pt walk its over-cap arm does); the N_LET in-cap
    tuple arm passes the declared type for an N_TUPLE rhs; the bare
    cgexpr route passes NULL/nil (emission unchanged).

Ident-elem sources keep the existing slot-load push byte-identically
(t57_ident_no_regress); the CALL-elem tripwire stays loud (#41,
t57_loud_call_elem). RESIDUAL FILED, not folded (rule 11): the
N_MASSIGN destructure-reassign literal rhs routes through the bare
cgexpr path (decl=NULL) and stays silent-wrong — probe q5_massign,
task #64, cited at the massign arm both stages. The annotated
multi-let spelling (`let (a, b): (un, size) = lit`) does not parse
(both stages), so N_MLET has no declared-literal route.

941 rows t57_*: return (named + inline union), let-literal, tagged
second elem, float payload, bare-untyped payload (rides the #33
chooser through the new wire), ident anchor, loud CALL tripwire;
ken's adversarial shapes (tagged-MID elem, two tagged rvalue elems
incl. void, plain-f64 SSE coexisting with a declared-tagged box), the
in-cap/over-cap boundary loud (k57d), and the NEW #57 tag-remap loud
pinned. Pre-fix at e8977a4: p8/p9 rows exit 1, q1_let exit 1,
q2_mixed exit 2.

Task #57.
2026-06-05 09:42:50 +09:00
80e7096f25 cgen: #50 append/insert tagged-dst VALUE boxes PRE-grow, both stages
The #34 value-store dispatch's tagged arm ran the widen choke-point
(cg_widen_tagged_store / cgwidentaggedstore — which cgexprs the value
internally) AFTER cg_append_grow, so a value expression reading the
destination (xs.len) saw post-grow state. Hare evaluates arguments
before the call's effects; the scalar/str/slice arms already ordered
value-first. insert() inherited the bug through its desugar-to-append
(9861f73) — one boxing choke-point, so this is the whole fix. Box the
value into a fresh per-site @apptagscr frame scratch pre-grow (zero,
BP-rooted widen store), then raw-copy the finished box into the new
slot post-grow.

Consumer evidence: regex fold-5b's {,0} rows (reviewer-5b's mutant;
ken's corrected-root matrix /tmp/ken_silent, f50v4_tagged exit 15 both
stages, byte-id, gate-blind). lib/regex.ww:643-647's pre-bound
split_target workaround comment GRADUATES with this commit; the
comment update itself rides the next regex touch per the standing
rule.

test/807: 57 -> 84 fixtures. New rows: tagged_pregrow_val (f50v4
exact), scalar_pregrow_val (no-regress control), tagged_selfref_val,
tagged_str_payload, tagged_regex_minrep (the {,0} shape standalone),
tagged_append_pregrow_val (direct append, the fix site),
tagged_realloc_selfref_loop (ken k50a: old-base read across actual
rt_ensure base moves), tagged_seq_positions (ken k50b: sequenced
inserts at 0/mid, each len-reading) and tagged_void_variant (ken k50c:
tag-only box through the fresh scratch). Mutation at e8977a4 fails
exactly the five eval-order-discriminating tagged rows, both stages.

The STRUCT-LITERAL value arm keeps its post-grow field-expr fill —
same eval-order class, different arm; filed (ww-core #59), cited
at-site, not folded.
2026-06-05 09:31:23 +09:00
e8977a413d cgen: #38a chained-dot TAGGED leaf — cursor read + widen store, value-chain + ptr-chain, both stages
A tagged leaf at the end of a dot chain (depth >= 2) fell to the
one-word scalar tail in FOUR walkers — the value-chain read spine,
the value-chain assign walker, the chained-*struct read branch, and
the chained-*struct assign branch — in BOTH stages, byte-identical,
gate-blind:

  READ:   one MOVQ pulled the TAG word only; is-tests passed by
          tag-luck while as/match/let consumers read stale DX/CX/R8
          as payload (ken x5c: o.r.min as size added entry-zero DX —
          want 28 got 20; ken b8 same through a mid-chain *rep).
  ASSIGN: one sized store landed the rhs ON the tag slot, payload
          kept its old bytes (ken x5d: `o.r.min = 8: size` made the
          following is-test false).

The single-dot arms were already correct — each fix is that arm
verbatim at the chained site:

  read:   the tagged cursor (AX=tag, DX=val0, R8=val2 before
          CX=val1 — the base may live in CX/AX; >32B box leaves its
          ADDRESS in AX, the #37 mem-read convention). wwstage rides
          the existing cgloadtaggedfield helper; the *struct-base
          branches stage AX into BX first (cursor targets AX).
  assign: full slot rewrite via the shared widener
          (cg_widen_tagged_store / cgwidentaggedstore, base BX/BP),
          plain `=` only — compound on tagged stays loud (#133).

The nested-LITERAL half of the old #38 filing dissolved: the
canonical fill's tagged-field widen delegation (#23, 413aafa) already
covers any depth — x5c's literal init emits a correct tag at HEAD;
only the chained READ/ASSIGN halves were live (probe-proven, asm at
/tmp/pB).

Probes converge byte-id and runtime-correct: x5c/d/e (28), depth-2
(b3/b9), (void|str) 32B box + match readback (b5), *outer root (b7),
mid-chain *rep (b8), non-tagged control (f38a). 938 rows chained_*
pin all six shapes. Residuals filed, not folded (rule 11): the
INDEX-spine sibling xs[i].min payload read (task #58); tagged-field
GLOBALS stay loud at DATA emit (#129 A.2), so the walkers'
global arms are mirror-only today.

Task #38.
2026-06-05 08:39:40 +09:00
322667b820 cgen: #35/#46 Family C — tagged transport from deref/cast/unwrap sources goes mem-based, both stages
A tagged value reaching a transport consumer from a DEREF, CAST, or
UNWRAP source materialized as ONE scalar word — the N_UN(STAR) arm's
localloadop pulled word0 (the tag) and every cursor consumer then
transported garbage payload (cs pushed stale DX, ww stored 0/garbage;
divergent asm AND silent-wrong on both stages — ken f35 asm proof +
ken37v D3a/D3b). Consumers × sources × sizes all wrong: arg push,
let-init, assign, return, match scrutinee, as, widen — from *p at
16/24/32B, from identity/widening tagged casts, and from `?`/`!`
whose success variant is itself tagged (nested box: payload words
dropped). `is` and ww-match-16B passed only by stale-register luck.

Fix extends the #37 mem-based machinery (26d3754) below the 32B cap
instead of re-deriving:

  - cg_tagged_memread/taggedmemread: an N_UN(STAR) deref of a
    non-nullable tagged box is mem-based at ANY size — the pointer
    value IS the box address. The N_UN emitter skips the scalar load
    (joins the TY_FN/TY_ARRAY value-is-address skip); the existing
    size-generic memread arms in widen-store/match/as pick the source
    up unchanged, `is` loads the tag through the address.
  - arg push: the memread fatal becomes the mem-based push (words
    high→low from (AX)) — closes #35's word0-only push and wires the
    33-48B INDEX/DOT loud as a side effect. Pop side drains via
    pushargsrev's returned word count, unchanged.
  - cg_tagged_castpeel/taggedcastpeel: tagged→tagged casts are
    transport-transparent; peel at the call-arg widen decision and the
    widen-store entry so the ident/deref arms see the carrier and the
    remap keys on the operand type. The identity-only subset
    (cg_tagged_idcastpeel) applies at is/as/match, which key variant
    indices on the scrutinee's own type; surviving non-identity casts
    die LOUD there and at the widen cursor arms (rule 7) instead of
    word0 garbage. The peel also wires 929's >48B memarg same-type
    cast row (place resolves post-peel; loud pin flips to a run row).
  - tryprop/tryunw: IDENT sources load the cursor from their slot,
    memread sources from the box address (≤32B); >32B non-call and
    global tagged idents die loud (rule 7). A TAGGED success variant
    shifts the nested box past the outer tag (twin of the #241 tuple
    shift) — closes the unwrap-source half (ken unw16).
  - wwstage alignment UP to the cstage type-keyed routes:
    rhstaggedabicall admits N_UN deref + N_TRYPROP/N_TRYUNW (stamped
    type), matchscrutt carries the N_UN stamped type (spill size +
    variant indices; was nil → tag-0 clamp + mis-sized spill),
    cgreturn routes memread sources through the widener (the
    fall-through wrapped the un-deref'd POINTER as payload), and
    pushargsrev's aistagged gate admits the deref kind.

Emitters and consumers ship as ONE commit: they share the memread
contract, and splitting opens a transient window where a wired
emitter hands an address to an unwired consumer — the #61/#37
route-sharing fuse. The test flips ride along because they pin the
flip itself: 941's two #37 deref loud-symmetry pins become run rows
(the loud is now wired), 929's fail_rvalue_cast becomes
memarg_idcast_peeled.

No-drift bar held: ≤32B IDENT/INDEX/DOT sources emit byte-identical
asm vs master 4c46d3a (probe corpus nd1: ident let/match/arg,
struct-field, indexed element, call, nullable, ident-widen — both
stages IDENTICAL); is-on-deref is incidentally byte-identical too
(the tag load moved from the emitter to the consumer).

Tests: 941 grows 252→272 checks — ken's exact f35/D3a/D3b shapes,
each consumer × source × size cell (16/24/32/56B, str + struct
payloads), neighbor-guard row, identity-cast arg, widening-cast let
(payload checked — the old cs pass was is-only luck), success-first
unwrap-to-tagged + ident/deref unwrap, the 56B slice-deref let+match
flips (payload-pinned), and 2 rule-7 loud pins (global tagged `?`,
cast-to-third-union). Reviewer-C rows commit ken's remaining
adversarial shapes (gC1 deref-wrapped cast arg, gC2 void-variant
deref, gC3 slice-element-pointer deref, gC6 56B memarg-leg deref arg
— that one a regression pin, already place-resolved at base), a 40B
deref ARG (the 33-48B mem-push leg, silent word0 at base), and a
multi-arg pop-balance row (tagged-deref arg mid-list, called twice —
the original #35 1-push-2-pops symptom). At base 4c46d3a the impl
rows fail 53/254 (silent-wrong exits, cs≠ww asm, missing louds); the
reviewer rows kill at base too (flip rows LOUD, others wrong-exit)
except the gC6 pin. At HEAD 272/272 + 929 22 rows + test-unit 284.

Residuals (filed separately, pre-existing): #216 success-tag
divergence — error-FIRST unions emit CMPQ $1 (cs s_tag) vs CMPQ $0
(ww first-param) and ww's first-param success-type read misses the
tagged shift; match-expr tagged yield is cstage-checker-rejected
while ww runs it (the #34/#43 acceptance family); `as` binding a
STRUCT payload at let-init stays loud ("aggregate init from unhandled
rhs shape"); wwstage tagged-source arg-widen-into-WIDER-slot (the
pushargsrev #21-comment out-of-scope boundary, task #55) — the deref
leg of that family is now cstage-correct via the widen-store memread
arm but stays wwstage-silent-wrong (joins the pre-existing INDEX leg;
ident leg runs by prefix-luck under-push); truncating scalar cast as
a box payload drops the conversion (task #56, both stages, untouched
by the peel — scalar→scalar casts are never peeled).
2026-06-05 07:39:24 +09:00
4c46d3afde cgen: #49 aggregate-ASSIGN word0-only family — one mem-to-mem funnel (cg_aggcopy), both stages
Whole-aggregate reassignment `b = a` fell to the N_ASSIGN scalar tail
and copied ONE MOVQ — word 0 of any struct/array/tuple — in BOTH
stages, byte-identical, gate-blind (ken f49_min asm proof; latent
because lib style is let-init, whose #265/#268 copy is full-width).
Same class at three more positions: struct-lit FIELD init from an
ident source (`outer{.., r = r}`, the #38 non-tagged half), the deref
place `*p = s` (#31-A), and the module-let global `g = a` / `g = pt{..}`.

Fix: extract the C1.25 assign-resolver word-copy tail verbatim into
cg_aggcopy/aggcopy — the ONE place-resolved (SI)->(BX) aggregate copy
— and wire it at the N_ASSIGN ident-aggregate arm (local + global),
the deref-place divert into the existing resolver aggregate arm, and
the structlit-fill aggregate-field arm, all fed by aggarg_srcaddr
(the closed #265/#268 dispatch). The new arms key on the FULL alias
chase (type_chase_named / chased stamped tinfo, the #22 precedent) in
BOTH stages — the region's single-peel `lu`/`fu` would miss
`type b = a; type a = struct` on cstage while the wwstage twin fired
(ken R1, gA3b: master cs ran the word0 corruption, exit 2; now 0).
Non-addressable aggregate rhs (tuple-lit, unhandled call shapes) dies
LOUD (rule 7) instead of silently truncating: #31-E `*p = (3,4)` and
#31-G's deref flavor `*p = mk()` are now loud both stages (the INDEX
flavor `a[i] = mk()` stays in the legacy INDEX arm — receive
machinery, not this funnel; still filed under #31). #31-B rides: the
cstage-only <=24B gate before cg_structlit_fill_bp is lifted (the
wwstage twin never gated — a >24B literal reassign was
cs-zero/ww-filled, rule-10 break). Global structlit reassign rides
the existing DST_GLOBAL fill machinery.

Unsplit (rule 11): the assign arm, fill arm and deref divert all
route through the one new funnel (cg_aggcopy + aggarg_srcaddr) in
both stages; splitting by site or by stage would ship a transient
cs!=ww (gate-red) or a funnel with no consumer.

941 t2_reject_chain_arg: the row's tuple-LITERAL field fill now louds
at the #49 fill arm before reaching the pinned ARG-site reject; the
fill switches to an ident source (newly working via the fill arm) so
the original arg-site pin still fires.

test/wcc/812_agg_assign_width.c: 17 runtime-readback rows (the only
oracle for a gate-blind class) + per-row asm byte-id; every row fails
at 7545bf7 (ken matrix f49_min/f49b/f49c/fA_16b/f38b + gA3b/gA6 +
impl-A probes; reviewer-A re-probed 5 rows + the gA2 12B shape at a
master git-archive scratch). Alias rows use FIELD-WISE init: the
struct-LIT spelling louds earlier at the pre-existing task-#7
aggregate-let bound on wwstage (the #5 alias-arc's hole, not this
funnel's). Reviewer-A amendment (test-only, K5 self-certify): add
the ken-gA2 odd-size row (12B {u32,u32,u32}, maxalign 4 — pins the
MOVL tail; master both stages exit 3) and gA4's neighbor guards on
the deref row, completing ken's validated matrix in the committed
suite.
2026-06-05 06:18:45 +09:00
26d375410e w6c+w6c_ww: >32B tagged INDEX/DOT reads go mem-based — box address in AX (#37)
A tagged box wider than the AX/DX/CX/R8 cursor (size > TUPLE_GPCAP*8)
read via N_INDEX or N_DOT now leaves its ADDRESS in AX — joining the
sret-call mem-based class the #38b gates already speak — and every
cursor consumer branches on one shared predicate (cg_tagged_memread /
taggedmemread) before reading AX as the tag. <=32B keeps the cursor
byte-for-byte (32B-at-cap asm proven identical to base on both the
t.N and INDEX routes).

Emitters: N_INDEX ident+fallback arms, N_DOT tuple-element (flips the
#22b loud bound), N_DOT struct-field + ptr-chained-field (were silent
clamps at 32B); wwstage twins gate cgloadtaggedfield at the helper
choke-point. Consumers: match spill x2 and the widen-store subset +
nested arms (the let/assign/return-widen/arg-widen/vararg choke-point)
copy the box from memory, then share the existing zero-pad + tag-remap
tail; `is` loads the tag through the address; `as` spills mem-based.
Rule-7 loud bounds replace silent cursor garbage for the non-mem-based
>32B kinds, `?`/`!`, and the 33-48B in-reg tagged arg push (mem-based
push stays the #35 family); the exact-type >32B return passthrough
from INDEX/DOT flips from its #38b loud into the widener route. The
pre-existing >48B memarg stack blit (cgplaceaddr) never used the
cursor and is pinned unchanged.

Reviewer-37 amendment: the non-mem-based >32B loud was ONE-SIDED on
two wwstage routes — cgwidentaggedstorebp had no fall-through guard
at all (`let w = *p` on a 56B box: cstage loud, wwstage silent word0
truncation), and cgmatch's guard keyed on matchscrutt-resolved
spillsz, which defaults under cap for kinds matchscrutt can't resolve
(N_UN deref et al), so `match (*p)` slipped it the same way. Both now
loud off the stamped src/scrut type_ (the kind-blind key cstage
already uses), restoring the rule-10 symmetry the body claims.

Emitters and consumers ship as ONE commit: they share the memread
contract, and splitting would open a transient window where a wired
emitter hands an address to an unwired consumer (silent garbage) —
the #61-precedent route-sharing fuse. The CX-global-tuple-base LEAQ
arm is TRIPWIRE wiring: a >32B tagged global-tuple element is
unreachable today (module-level tuple inits are int/str-literal-only;
tagged elements loud at the DATA emit), and the LEAQ keeps the same
base_reg generality as the cursor walk it replaces (ken note, task
record).

This was the last 5b compiler gate: `match insts[pc]` on the regex
inst union (inst_lit|inst_repeat, 56B) was silent-wrong gate-blind
byte-id on both stages (payload words 3+ dropped past the R8 clamp).
test 941 grows 165->200 checks: the #22b BUILDERR pin flips to a
runtime row, plus the 56B driver match, str+nested-tagged payload,
let/is/assign, indexed return, widening (identity and reversed-order
remap), 56B memarg, ken's X1 composition row, 32B-at-cap INDEX
boundary, the ptr-chained p.f match (BX-arm) and (*p)[i] fallback-arm
rows, and the two deref loud-symmetry BUILDERR pins. At base f272068
the 11 bug rows fail (2 BUILDERR flips + 7 silent-wrong + 2 missing-
loud pins, both drivers); the anchors pass. Oracle: ken PASS at
bf21964b pre-amendment; re-bind on the amended tree pending (source
bytes changed: cgenutil.ww/cgenexpr.ww louds + combined.ww regen).
2026-06-05 04:06:19 +09:00
a72e8158c7 w6c+w6c_ww: over-cap (sret) tuple returns carry tagged elements — MEMORY-class box copy (#22b)
The over-cap SEND gains a per-element tagged arm: a declared-TAGGED
element copies its whole box mem-to-mem through the sret pointer
(@sretarg reloaded into DX, eslot/8 AX-bounced word pairs) from its
ident-local slot — cgexpr can't source it (tagged ident load is
word0-only; the AX/DX/CX/R8 box cursor would collide with the DX
dest-base reload). Non-ident/widening sources stay loud (call-source
is task #41 per rob's loud+filed ruling, widening #23/#40). The
receive legs were already eslot-generic from #22a except the wwstage
MASSIGN sret copy-out, whose wide=(str||slice) key dropped a tagged
element to the scalar 8B branch — re-keyed to the accessor's eslot>8.

The over-cap DECISION now rides cg_sret_retsize/sretretsize on the
DECLARED return type — the same SSoT the prologue @sretarg wiring and
the caller use. The old expr-shape count disagreed with the classifier
for declared-tagged elements with unwidened-payload exprs: the emit
took the register path against an sret-reading caller — built clean,
ran garbage, BOTH stages, byte-id, gate-blind (941 b22_reject_widen_
skew). A rule-7 net behind the branch catches any future inverse skew.
The same classify/emit class on the LET side: wwstage's cgexpr cursor
materialiser counts a void literal elem as 0 words, so a void-bearing
over-cap literal init slipped its loud and fell past every store arm
to NOTHING (cs louded via the ken-R1 net; ww ran silent-wrong) —
wwstage gains the same net. foff advances ride tuple_eslot/tupeslotn
(THE accessor): ken's K4 proved the old wide?esz:8 void-elem 8-advance
was a LIVE silent miscompile at base (declared-void-elem over-cap
return, send wrote +8 where every receive walks the 0-slot).

The unwiring also made >32B boxes transportable, exposing the t.N
tagged read's unbounded k<eslot/8 cursor walk: cstage OOB-indexed
tuple_rseq[4] (invalid asm), wwstage tupreg-clamped k>=3 to R8 (silent
payload-word-3 drop). Bounded loud both stages; the mem-based box read
is #37's fold (transport itself is size-generic and correct — 941
b22_big_box_mlet). One commit by the #61 route-sharing precedent:
unwiring the send is the only thing that makes the ww massign arm,
both skew nets, and the >32B read reachable — the one split order
that avoids a transient silent-wrong build (send last) lands those
arms DEAD, unexercisable by any test until the unwiring commit, so a
defect in them would bisect to the wrong commit; fused, every arm
lands with the 941 rows that pin it, preserving the #22a exit
invariant (zero silent tagged-tuple paths: correct-via-accessor or
LOUD with a task cite; proof-greps in-tree — tuple_ebytes/tupebytes 0
hits, all remaining tuple_rseq/tupreg walks cap-validated upstream).

Driver: regex fold-5b parse_repetition ((void|size),(void|size),size)
= 5 GP eightbytes > TUPLE_GPCAP — pinned end-to-end (construct, send,
let/MLET/MASSIGN receive, t.N is/as, destructure) plus head/mid/tail,
str+tagged and SSE-over-cap mixes, `_` skip, and the reject pins: 941
165/165 (15 new rows), test-unit 281/281. ken runtime-oracle PASS x2
(independent hand-math layout audit, K1-K5 probes); reviewer-22b
amendments (test-only): ken-K5 32B-exact box-at-cap row, both-tagged
2-elem row, chained-sret row, #37-family cite pinned in the big-box
experr. Residuals filed: #39 return-forward divergence, #41
call-source tripwire, #37 mem-based box read.
2026-06-05 02:47:14 +09:00
66991585d6 wcc+w6c+w6c_ww: tagged tuple elements take their full slot — tuple_eslot accessor (#22a)
slot = roundup8(size(elem)) — 8B is a FLOOR, not a ceiling (user-
ratified 2026-06-04; the #237 fieldslotsize precedent; (str,str)=48B
predates this — tagged was the one truncated >8B kind). Pre-fix the
checkers truncated a tagged element to one 8B slot and every cgen
transport walk strode wide=(STR||SLICE)-else-8: cstage read the
NEIGHBOR slot, wwstage read ZEROS — both-wrong-differently, so the
byte-id gates were blind (prober-9 PG1, /tmp/p9).

One stride accessor per stage — tuple_eslot (cgen.c) / tupeslot
(cgenstmt.ww) — now feeds every tuple walk; the per-site predicates
are deleted as absorbed. Sizer: check.c N_TTUPLE + check.ww
tupleelemslot gain the TY_TAGGED arm (astsize already rides the type
table since commit 0). Routes flipped to the accessor, both stages
symmetric: cursor producers (lit/slot-to-cursor; tagged ident
elements load their box from the slot — cgexpr's tagged ident load
is word0-only), let-receive (tuple_store/tupstore generalized to
eslot/8 words), N_RETURN send, by-value param receive, arg
restage/drain (tagged stays loud per C-t2), destructure
(MLET/MASSIGN, ident + sret + in-cap), t.N read + len(t.N) +
global-g.N offset walks (t.N gains the tagged box load: AX=tag,
DX/CX/R8=payload — the is/as spill cursor), sret classifier, DATA
emit. wwstage cgtagvariantidx gains the #67 stamped-carrier arm
(flatvariantidxt on .type_) — its AST-only key silently clamped
't.0 is size' to tag 0; fused here because the tuple-element read
this commit wires is its only exercisable consumer.

Exit invariant: zero silent tagged-tuple paths — in-cap shapes
(<=4 GP eightbytes) are correct end-to-end; everything else is LOUD:
over-cap sret return (#22b, task #28), call-arg (C-t2 #32),
non-local literal element sources (#22b/#23), tuple-in-union payload
(#242/#22b), global static-init, element write (pre-existing).
Closure proof-grep at HEAD: 'tuple_ebytes|tupebytes' -> 0 hits;
'wide.*=.*(TY_SLICE|TY_STR)' tuple-walk survivors are all behind
tagged loud-guards (cgen.c:2535/2568/12013 widen-store + over-cap
send; cgenutil.ww:3527 twin).

Latent cross-checks closed by the accessor: wwstage MLET-sret strode
esz (4 for i32) vs cstage 8; wwstage param-receive strode slotsize
(composite slotsize) vs cstage 8; both now the accessor's floor-8.
Tagged inits in pins use the CAST form (5: size) — the bare
untyped-int widen-store mis-tag is pre-existing at master and filed
(task #33).

941 gains 13 rows: t22_* size/align folds (+ void-elem 0-slot pin),
the full runtime round-trip (read, is/as, both element orders, void
variant, destructure, literal-let), two-tagged-elem in-cap and
float+tagged in BOTH orders (ken k1/k2 regression-pinned as rows),
t.N-as-call-arg, 5 loud pins (arg, over-cap return, call-source
element, global init, element write), and the sret_narrow_mix_*
triple below. Runtime rows exit-checked under both drivers +
byte-id. 129/129; unit tests green; sizelint clean.

ken R1 (re-validation fix): an OVER-CAP tuple init whose rhs is not a
CALL fell past every cstage N_LET store arm to NOTHING — silent
uninitialized-frame reads — while wwstage loud-rejected the same
shape. Pre-existing for (str,str) literals; the #22a tagged slots
routed >16B-box tagged shapes into it (k5b/k5c/k5d, base-correct
because base truncated them in-cap). cstage now routes the rhs
through cgexpr (the cursor materialisers carry the exact wwstage
loud texts) with a trailing fatal as the net; 941 pins both the
tagged and the (str,str) spellings of the hole.

reviewer-22 (review fixes, folded): the MLET-receive slot flip above
landed ALONE on the wwstage over-cap sret family — the ww RETURN-send
and MASSIGN-receive still strode packed esz (4 for u32), so
`let (a,b,c) = f()` over (str,u32,str) read c at slot offset 32 while
ww's send wrote it packed at 28: a ww runtime REGRESSION vs base
(base was packed-consistent on both sides — ran right, byte-diff).
Both walks now stride the slot (send mirrors cstage's
`wide ? esz : 8`; MASSIGN strides tupeslotn) — closing, in the same
stroke, the pre-existing base skews: ww `let t = f()` over-cap
narrow-mix read (reader slot-laid vs send packed, runtime-wrong at
base) and the cs≠ww asm on all three routings. Second find: t.N
tagged element as a CALL ARG — cstage rides its generic
node_istaggedarg cursor push, but wwstage's kind-gated aistagged
missed N_DOT and mis-routed the box into the widening branch
(taggedvariantindex -1 clamped to 0: callee read variant 0, silent,
ww-only). cgenutil.ww gains the N_DOT arm (aistagged + pushargsrev),
the #67 stamped-carrier twin of the N_INDEX (#12) arm. The N_DOT arm
was a LIVE wwstage stdlib miscompile: hextest's import graph carried
two t.N tagged-arg sites (base .s diff = exactly the clamped-tag
PUSHQ pair) — the 989 lib-byteid ratchet caught the convergence and
#59.4 graduates to M_ID (runtime-correct per 979_hex_run). Probes:
/tmp/r22. Residual filed: tagged arg from deref/cast/unwrap sources
is a word0-only read on BOTH stages (pre-existing, base-confirmed) —
generalizing wwstage's kind gate to cstage's type-keyed check rides
that task.
2026-06-05 01:37:35 +09:00
06b0fea98b w6c+w6c_ww: struct-lit store into indexed/deref/field place fills via resolver (#20)
A struct-LITERAL rhs aimed at an N_INDEX element (a[i] = pt{...},
(*ts)[i].caps[k] = capture{...}), an N_UN deref place (*p = pt{...}),
or an indexed-base FIELD place (a[i].f = pt{...}, reviewer-20 sibling)
fell to a scalar store tail in BOTH stages: cgexpr on a struct
literal emits nothing (AX=0) and one MOVQ zeroed the place's first
word — every field silently dropped, a str-leading element's
content.ptr nulled (downstream SEGFAULT). Byte-identically wrong, so
every byte-id gate was blind; runtime pins added.

Fix: divert struct-lit-rhs INDEX/UN-STAR/DOT-over-INDEX places past
the legacy arms and widen the F6 assign-resolver gate
(N_DOT -> N_DOT|N_INDEX|N_UN); the existing C1.25 aggregate arm
materialises the literal into a fresh per-use @placescr slot and
word-copies to the cgplaceaddr-resolved address. No new path;
@placescr alloc site stays single per stage. Rider (task #32): an
array-LITERAL rhs at assignment — unwired for EVERY place kind, same
silent zero-word tail — now dies loud at one choke-point until the
fill lands; build-fail rows pin it.

Gates regex fold-5a (run_thread groupstart capture store,
regex.ha:643-651). Residual adjacent gaps (deref ident-rhs truncation,
>24B ident reassign cs!=ww, struct compound acceptance, value-global
DATAW, tuple-lit deref truncation, CALL-rhs RAX-only store) probed
pre-existing and filed as tasks #31 A-G / #32.
2026-06-05 00:19:47 +09:00
0ce98cc5dc wcc+w6c_ww: append() spread place-chain sources via cgplaceaddr (#35)
#35 (fold-5 blocker, PG6): the append() spread arm dispatched on
SOURCE node kind — ident-local only; both fold-5 dup shapes
loud-rejected on BOTH stages: deref-spine `append(dup,
(*tsp)[0].caps...)` (add_thread regex.ha:569/572) and indexed
`append(res, ts[0].caps...)` (search ha:820).

Route every non-ident-local source through cgplaceaddr (the
established place choke-point: C1 stores, C1.25 aggregate fields, FA1
append targets, #49 element sources): the source header ADDRESS
resolves ONCE, pre-grow, into a fresh per-site @appsprscr spill, and
every loop iteration re-reads .ptr/.len THROUGH the spilled header
after the grow. This adapts the #49 split ruling to the spread's
full-range copy: the chain's rvalues (deref-root pointer expr, index
exprs) evaluate exactly once PRE-grow — an index reading the dst's
len sees the pre-append value — while the source data base and len
re-derive live each iteration, so a source header aliasing the dst
header re-roots post-realloc. A header reached through a buffer the
grow reallocs reads the STALE copy — bit-identical under the
non-reclaiming rt/malloc, the same documented #49 stale-base hole
(pinned by the spread_selfalias_chain row). Ident-local sources keep
the legacy BP-disp emission byte-identical.

New loudness with the same fix: the spread source's stamped type must
chase to TY_SLICE/TY_STR — a [N]T array ident source previously read
its first 16 DATA bytes as a {ptr,len} header, silently. Array wiring
plus the remaining rvalue sources (CALL, slicing exprs) stay loud,
filed task #27. Global-ident sources now resolve on cstage but are
blocked by a pre-existing wwstage checker reject ("let: not
assignable", task #29) — no dual-stage row until that closes.

806_append_place: reject_spread_src GRADUATES to a runtime row
(spread_place_deref, the old reject source verbatim + readback); new
rows spread_dup_copy (PG6 verbatim: both fold-5 shapes, 56B capture
elements, copy-semantics mutation pin, want 139), spread_place_kinds
(str 24B headers / narrow i32 / empty deref source),
spread_growth_place (40-elem spread crossing cap doublings),
spread_selfalias_chain (source header inside the dst's grown buffer),
and the two new loud-tail rejects (CALL rvalue, [N]T array) pinning
the "#35:" diagnostic on both stages. 87 fixtures green (was 70),
per-row cs/ww asm byte-cmp included.

Unblocks regex fold-5a: add_thread regex.ha:569/572 + search ha:820
spreads go from loud-bound to real spread.
2026-06-05 00:00:00 +09:00
1bcf2726cf wcc+w6c+w6c_ww: delete() range form delete(xs[lo:hi]) (fold-5a P2)
Hare's delete also takes a slicing place (harec check.c:1981-2027
EXPR_SLICE; Hare spells it delete(xs[i..j])): remove [lo, hi) — shift
[hi..len) down count = hi-lo strides, len -= count, cap unchanged; lo
defaults 0, hi defaults len, so delete(xs[:]) clears the slice with
storage retained. Checker accepts N_SLICE next to N_INDEX (object must
chase to a slice, harec :2024); the old range-unimplemented reject and
its #35 cite drop.

Lowering (both stages, converged byte-identical by construction) is the
single-element arm's same-slice whole-stride word-copy loop with a
DYNAMIC src offset (count*esz via a src register) instead of the
constant one-stride. Base shapes: local slice ident, deref-of-local,
plus NEW indexed local-slice base xs[g][lo:hi] — the fold-5a consumer
shape (regex.ha:333 delete(jump_idxs[group_level][..]); outer stride
off the type table). Bounds stay implicit, inheriting the documented
single-element posture (no index checks anywhere in cgen). Operands
evaluate left-to-right, exactly once, before the shift (harec order);
only the header ADDRESS is taken before operand eval, so a bound
expression's writes through the slice land before the copy.

test/809: 64 fixtures — full/explicit/re-clear/head/mid/tail/empty
a:a/end-boundary len:len/explicit 0:0 on a never-appended (nil-ptr)
slice, single-vs-range equivalence, cap preservation, esz 1/2/4/8/16/24
copy tails against the dynamic src, operand order-of-eval (lo/hi CALLs
fire once each, in order) + aliasing-visibility pins, the EXACT
[][]size regex consumer shape, deref base, 2 reject rows w/ diagnostic
text; every accept row cs==ww asm byte-id. test/804: reject_range row
retired (form now accepted), reject_nonindex text follows the widened
message.
2026-06-04 23:37:08 +09:00
e091dfbdbe wcc-ww: assert/abort builtins — checker tag + cgen rt_abort lowering (#58)
wwstage had no EXPR_ASSERT-family intercept: the checker left bare
assert/abort calls untyped (asserttyped gate 4 skipped them by design)
and cgcall fell through to the regular call path, emitting
CALL assert(SB) for a symbol that exists nowhere — link-fail. cstage
was already correct (tag ty_err at check.c:1536-1572, lower inline via
rt_abort at cgen.c:6618-6663).

Mirror the same tag-then-lower pair: exprtype N_CALL stamps the call
void and the callee TY_ERR behind the scopelookupprefer no-shadow gate
(the isassertfam predicate), with the cstage arg diagnostics (cond must
be bool, msg must be str, arity caps); cgcall keys on the TY_ERR tag
and emits the identical CMPQ/JNE/rt_abort sequence. A user-shadowed
assert/abort (same-module or cross-module, the #45 shape, task #14)
stays untagged on the regular call path — byte-id for all existing lib
code preserved.

The cond check does NOT alias-peel: cstage compares ty_bool by
identity (check.c:1560), so `type myb = bool` is rejected there;
wwstage aligns down per rule 10 (a resolvealias here was accepting it
— cs/ww accept-reject divergence). Widening both stages together
belongs to the alias-peel choke-point arc (task #5, #47/#68).

The resolvewalk N_IDENT resolution counter learns the builtin shape:
an unshadowed abort/assert ident binds no sym BY DESIGN, so wwdump
-r's zero-unresolved gate (990 probe 4) counts it resolved instead of
failing builtin-using units.

test 957: 13 rows — pass/fail/msg/bare abort (run exit + rt_abort
stderr content; no-msg rows pin EMPTY stderr = the (NULL,0) shape),
assert in an imported module, same-module + cross-module shadow
controls, 5 checker rejects pinned on diagnostic CONTENT (shared
substring; cstage prefixes pos, wwstage cerr is bare) incl. the
alias-of-bool cond row pinning the rule-10 down-alignment; each
positive row pins cstage run exit + cs==ww byte-id. On pre-fix master
11/13 rows trip (survivors = the two shadow controls).

Residual (separate root, deferred diagnostic class): zero-arg assert()
is not intercepted by either stage; cstage rejects via the generic
undefined-ident path, wwstage's undefined-callee diagnostic is the
class deferred behind wiring checkfile into w6c_ww.
2026-06-04 22:42:47 +09:00
33ec0fb1ac w6c+w6c_ww: (*p)[i] deref base materializes the array ADDRESS (#61 C)
Both stages SEGV'd identically (byte-id-blind): cgun's TK_STAR emitted
a scalar MOVQ (AX),AX for an array pointee, so the index consumed
a[0]'s VALUE as its base — a wild deref. An array value IS its address
everywhere in this cgen (#270-1a), so the ARRAY pointee now takes the
same skip as the #185 *fn deref in both stages: `*p` leaves AX = p's
value, and every consumer that materializes a complex index base via
cgexpr(base) — N_INDEX read fallback, cgassign store/compound, TK_AMP,
N_SLICE — gets the array address from the one deref choke-point.

wwstage additionally joins the N_UN-TK_STAR base to the stamped-tinfo
esz arms (cgindex / cgassign store + compound / TK_AMP &(*p)[i]) where
cstage reads base->type uniformly: without it, esz fell to the 8B
default and a narrow element would mis-stride the moment the base
started materializing (cs!=ww only reachable post-choke-point-fix,
which is why it rides this commit).

949_ptrarr_index_run grows the deref_* rows: read (8B/4B/param-base),
write (8B / 1B+neighbor-guards), compound — runtime + byte-id, the only
nets that can see a both-stages-identical miscompile.
2026-06-04 22:34:09 +09:00
eea3e197c2 w6c+w6c_ww: *[N]T indexing strides by element, not whole array (#61 A+B)
Indexing through a pointer-to-array auto-derefs, so esz and the element
classification must come from the pointee array's ELEMENT (cstage
idx_eff semantics, cgen.c:1163). Two halves of one root class:

A (wwstage-only, cs!=ww, cstage runtime-correct): elemsizeofc's #270-2
nested-array block treated an N_TPTR pointee-array like a [N][M]T outer
index and returned the whole-array size — every p[i] read/write/
compound scaled by N*size(T), and the same wrong element belief reached
the store-width chooser (var-idx write emitted an N*8B aggregate copy
sourced at the 8B rhs slot: caller-frame smash, the siphash round()
corruption). Fixed via two wwstage choke-points mirroring idx_eff:
idxeffti (tinfo: NAMED peel + TY_PTR->TY_ARRAY drill; feeds elemsizeofc
and elemissignedc/elemisfloatc/elemisf32c) and idxelemtn (node: element
tnode with the same drill; feeds every cgindex/cgassign/nodeisstr/
match-scrutinee elemtn resolution).

B (BOTH stages identically wrong, byte-id-BLIND): the TK_AMP &base[i]
arm read bu->sub->size without the ptr peel (&p[3]-&a[0] = 96, not 24).
cstage now routes esz through idx_eff.

A and B are FUSED by the pre-existing routing topology, not by choice
(rule 11): wwstage's TK_AMP arm already reads its esz via elemsizeofc
(selfhost/cmd/wcc/cgenexpr.ww:4095, the #11 addr-of twin of the #10
cgindex fix), so fixing A's choke-point flips wwstage's half of B in
the same stroke. A standalone A leaves &p[i] transiently cs!=ww;
B-first is the mirror transient; carving the TK_AMP caller out of the
fixed choke-point to preserve the wrong stride for one commit would be
a deliberate known-wrong intermediate (rule-7, vetoed by rob). One
choke-point, two enrolled routes — un-fusable without a red
intermediate.

Close-by-construction proof-grep (both stages): every remaining raw
sub->size index-stride read is TY_ARRAY-gated, a slice-only builtin
(delete/insert), a checker-stamped element tinfo (indexresult already
decays *[N]T, check.ww:2277-2284), or a non-index context (tuple
slots, let-init elements). Two true residuals filed with site+symptom
instead of silently absorbed: N_SLICE through *[N]T does not decay
(LOUD type error, Hare divergence; team task #18) and non-ident
cast-expression index bases keep wwstage's 8B-default esz (pre-existing
#74-style cluster; team task #19). cstage's N_INDEX read-side
str/slice header gates also move from u->sub to esub (identical for
every non-ptr-to-array base; honest for *[N]str — pre-fix BOTH stages
were runtime-wrong there, differently).

949_ptrarr_index_run pins the class at runtime + byte-id: {1,2,4,8}B
elems, const+var idx, param/local/cast bases, read/write/compound,
neighbor guards, &p[i] pointer-difference, siphash-round mix shape.
989_lib_byteid: siphash_test graduates #59.7 DIVERGE -> ID (ratchet
tripped loud pre-update; no other #59.x pin flipped in the same run).
(*p)[i] (sub-bug C) follows separately.
2026-06-04 22:34:09 +09:00
8578ad0533 wcc+w6c+w6c_ww: global tuple lets — DATA emit + element reads + len(g.N) (C-t3, #48)
Global tuple lets were WHOLLY unwired, silently: let_emit_size returned
0 so emit_lets SKIPPED the definition (no DATA, no diagnostic), then
cstage's t.N read and #235 len arm read BP-frame garbage (localfind→0)
while wwstage — with the tuple never in collectlets — mis-emitted the
field index as a symbol (`MOVQ 0(SB), AX`). ken's #48 was the len()
facet of this.

Now: let_emit_size/letemitsize admit TY_TUPLE (slot-sum size, rides
C-t0); emit_tuple_data/emittupledata lay the slot-format DATAW row —
a scalar element one 8B LE word, a str element its 24B header slot
with a DATAR ptr patch at the element's slot offset (the #18 [N]str
per-element pattern; strlits pre-interned in element order) — and any
element that doesn't reduce to an int/str literal dies LOUD instead
of skipped. The t.N read and len arms gain the global base (LEAQ
sym(SB) into CX, the struct-field-global pattern; wwstage's C5 len
loud-stop graduates to the working path). A GLOBAL tuple as a
first-class VALUE (`let q = g;`) loud-stops on both stages — pre-fix
it byte-identically loaded word0 only and read a stale cursor for
words 1+ (element reads are the supported surface).

941 grows the t3 rows: global element reads (str+i64 and packed
u32,u32 incl. len(g.0)) + rejects (float-element init, whole-value
use, pre-existing element-write anchor). 7/82 checks fail at the
C-t2 parent (cs silent-garbage runtime, ww C5 build-fail, both
rejects vacuous-or-absent).
2026-06-04 19:37:26 +09:00
6426fac6f2 w6c+w6c_ww: tuple by-value ARG send — every cursor-filling producer rides #163 (C-t2, #32)
node_tuplearg was N_CALL-scoped and its comment claimed non-call forms
"loud-stop" — they did NOT: a tuple ident/literal/unwrap arg fell to
the scalar single-PUSHQ default, skewing every later arg register so
the callee read garbage word 2 (byte-id both stages, the gate-blind
both-wrong class; packed shapes SIGSEGV'd pre-C-t0). The receive side
(cgfn #163 walk) was already correct.

cgexpr already fills the return-ABI cursor for every supported
producer (#241: ident via slot-to-cursor, literal via lit-to-cursor,
unwrap via payload shift; call via the return ABI) — the send now
admits exactly those into the existing @tupargscr restage + per-class
drain (node_tuplearg widened; wwstage gains nodetuplearg, mirroring it
over the local tnode / inferletcalltype; rettupleof stays N_CALL-scoped
for the destructure receives). Any OTHER tuple-typed source shape
loud-stops at the push site — the false comment's claim, now true
(rule 7). Literal tuple elements are stamped expr types, so the
restage/drain wide test goes type_isstr/type_isslice (TY_UNTYPED_STR-
aware) with the ty_str->size header stride; the wwstage twin walks a
literal's VALUE exprs the way cgtuplelittocursor classifies them.

Ken review demands folded in: (1) a NESTED composite element
(tuple/struct/array/tagged inside the tuple) occupies more than the
one GP word the restage walk counts — the checker accepted it and it
ran WRONG (inner words skewed, wwstage SIGSEGV); both stages' restage
walks now loud-stop the element kind (wiring is the filed follow-up,
task #65). (2) the variadic interaction probed: a tuple arg ahead of
a variadic tail rides the restage correctly (positive row);
variadic-of-tuples stays bounded-loud via the tuple-in-slice read
surface.

941 grows the t2 matrix: packed/16B params with branched callees,
mixed arg orders both ways, literal arg, (f64,i64) param, unwrap arg,
ken's >6-GP-pressure stress (4 leading scalars + tuple + a 7th
stack-class word), variadic-after-tuple, plus rule-7 reject rows
(chain-source arg, nested-element arg, variadic-of-tuples, over-cap
ident arg) and the fold-4 charset substrate pin ([](u32,u32) append
stays LOUD). At the C-t1 parent 18/73 checks fail: every runtime arg
row except the (f64,i64) anchor on BOTH stages (byte-identically — the
gate-blind both-wrong class) and the chain/nested args silently
accepted.
2026-06-04 19:35:50 +09:00
fdfc2ce318 wcc+w6c+w6c_ww: tuple slot layout SSoT — checker size = cgen slot stride (C-t0)
The checker computed TY_TUPLE size as the packed element-size sum
((u32,u32) = 8B) while every cgen cursor-transport site strode 8B
slots (16B). 16B tuples were blind to the split (slot == packed);
packed tuples hit it everywhere: cstage let-receive keyed on sz 16/32
missed sz 8 and dropped word 1, the cgfn param receive spilled
8B/element into a packed-sized local (saved-BP clobber, SIGSEGV), and
mixed (u32,f64)/(u32,str) shapes missed the receive arms entirely.

Slot layout is now the SSoT (user-ratified): the flip lives in the two
checkers' N_TTUPLE size computation only (check.c, check.ww
tupleelemslot + stamp); cgen's packed-keyed walks (t.N read, #235 len
arm, over-cap sret send/receive pair) align onto the slot stride, and
the wwstage t.N read gains the natural-width load (tnodeloadop) to
byte-id with cstage's fldloadop. ttupleelem.offset re-stamped
slot-cumulative (no consumers yet). The #242/#243 eightbyte-share
loud-stop dissolves by construction (no two narrows ever share an
eightbyte) — 940's eightbyte_share row graduates to a runtime
round-trip. Hare-layout divergence documented at both checker sites;
re-alignment is task #60. #32 send skew and #33 wwstage literal-let
receive are separate commits on this base.

941_tuple_slot_layout_run pins the matrix: 4 packed rows fail at the
parent (8/21 checks), 3 neutral anchors prove 16B/32B emission
untouched.
2026-06-04 19:06:54 +09:00
9861f73bbb wcc+w6c+w6c_ww: insert() builtin — single-element slice insertion (part of #35)
Hare's insert(xs[idx], v) (ref/harec/src/check.c:745
check_expr_append_insert — append/insert share the checker arm,
"insert" at :786): checker accepts an INDEX place over a slice plus
one value, stamps void; idx == len is a legal end-insert (the
ref/hare os/exec/platform_cmd.ha:86 idiom). Loud-rejects with exact
texts: spread form insert(xs[i], vs...) (filed, #35 — also covers
harec's with-length form via the arity check), range place (not
Hare; harec only parses ACCESS_INDEX, :784), non-index operands,
array bases, wrong arity. delete()-parity throughout.

Lowering (both stages, converged byte-identical by construction) is
a DESUGAR: append(xs, v) — reusing append's grow (rt_ensure) and the
entire #34 value-store dispatch (scalar / str-slice header / tagged
widen / struct fill) verbatim, one boxing choke-point — lands v at
slot len-1; then a rotate-right of [idx, len) moves it home through
a fresh per-site esz frame scratch (@insscr). The rotate is delete's
shift loop in reverse (descending j, the safe memmove-up direction)
and is a same-slice whole-stride raw byte move — no boxing exists
for any element kind. idx evaluates BEFORE the grow (Hare's
left-to-right operand order — pinned by the pregrow_len_idx row,
insert(xs[len(xs)-1], v): pre-grow [7,13,11] vs post-grow [7,11,13];
an idx==len(xs) end-insert cannot discriminate, the rotate
degenerates either way). Base shapes: local slice ident (LEAQ) and
deref-of-local ptr-to-slice (MOVQ); others rule-7 loud-stop, like
delete.

test/807: 57 fixtures — front/middle/end + idx==len via len(xs) +
the pre-grow eval-order pin, esz 1/2/4/8/16/24/56 (MOVB/MOVW/MOVL
tails, struct body, str header, 7-qword tagged from a typed local
[the regex fold-3 ha:347 newinst shape] and from a cast rvalue
[ha:419/441]), empty-slice grow, (*p)[i] deref base, front-insert
loop, 6 checker reject rows with diagnostic-text checks; every
accept row cs==ww asm byte-id.
2026-06-04 17:24:57 +09:00
c34a48a81f wcc+w6c_ww: append() struct-element sources via split place-resolve (#49)
#49 (#35's single-element sibling, tranche-C pre-check PC2): the
struct-element append arm dispatched on SOURCE node kind — N_STRUCTLIT
(literal fill) and N_IDENT (local word-copy) only; every
place-resolvable chain died on the rule-7 fatal in BOTH stages,
including search()'s result-build line
`append(res, threads[best_idx].root_capture)` (regex.ha:819).

Wire those shapes with a SPLIT resolve around the grow (the #49
ruling): the chain's rvalues — deref-root pointer expr, index expr —
evaluate exactly once PRE-grow into @appendsroot/@appendsoff (an index
reading the slice header sees the pre-append len, Hare's argument
order), then only the BASE re-derives POST-grow from the live storage
and the stashed offsets land back on top, so a self-append source
re-roots in the post-realloc buffer. harec resolves an aggregate
source address wholly PRE-grow (gen.c: gen_load returns the address
for STORAGE_STRUCT, gen_store copies after rt.ensure) — a
use-after-free under a reclaiming allocator; per #263 we align to the
runtime-correct side, not the reference. A pointer ALIASING the grown
buffer keeps Hare's own stale-base hole (sound today only because
rt/malloc.ww never reclaims). Supported shapes are bounded: root
(local/global ident | deref) + at most one index + trailing direct
fields; all else stays on the #34 fatal, including CALL rvalues (the
#42-style bound, new reject row pins the text in both stages). The
N_STRUCTLIT/N_IDENT fast-paths keep their emission byte-identical.

806_append_place grows eight rows: indexed-field 56B capture (the
ha:819 shape, header readback), computed-index whole element,
deref-spine param pair, deref source, self-append ×33 crossing three
cap-doubling reallocs, the split-order semantics pin (a CALLED index
helper reading len must run once and see the PRE-grow len — the
pre-split emission failed exactly there), an element-kind ×
place-source matrix row (scalar/narrow/str/slice/tagged route via the
pre-existing arms — regression net), and the CALL-source reject. The
six fix rows verified FAILING against a pristine 796d41b build on
both drivers (loud #34 fatal, identically in cstage and w6c_ww —
there was no silent path at master); 70/70 fixtures green here
including per-row cs/ww asm byte-cmp.

Unblocks regex fold-2b tranche C (search) — PC2 was the lone
pre-check failure; PC1/PC3/PC4 passed at base.
2026-06-04 14:37:24 +09:00
796d41bb9f wcc+w6c_ww: len() over place-resolved operands (F2/FA2)
C5 (tasks #10 + #41): the len() builtin's operand handling was an arm
enumeration that leaked FOUR siblings over time (#235 tuple-elem →
#19 indexed-elem → F2 len(xs[i].field) → FA2/FB1 len(*p)) — every
unhandled slice/str operand shape fell to a bare cgexpr fallback that
returned the slice DATA POINTER as the length. Silent ptr-garbage,
byte-id both stages, gate-blind. Probing at d642017 surfaced the full
family: len(*p) (param 48 / local 64), len(xs[i].field) (147),
len((*p)[i].field) (10), len(s.field) (75), len(p.field) (87) — plus
the same garbage for non-place operands len("abc") (40),
len(xs[1:3]) (48), len(mk()) (0). Review widened it twice more:
len(**pp) (chained deref, garbage 208 at e481cb8) and the EMPTY-slice
deref (len 0 reported as .ptr — masked by exit-code truncation, hence
the branchy test row).

The enumeration is closed by construction (ken's verdict): enumerated
fast-paths keep their pre-fix asm byte-identically (ident local/global,
#235 tuple element — not resolver-addressable, cgplaceaddr has no
TY_TUPLE hop — #19 indexed element, TY_ARRAY const fold), then ONE
uniform header-place route via cgplaceaddr resolves every other
slice/str place and reads the .len word at place+8 (the same offset
math as the ident arm). Non-place operands (string literal, slicing
expr, call result) die LOUD per rule 7 — previously the same silent
ptr-garbage; Hare instead const-folds len of literals, that parity is
filed as #46. The ident arm's off==0 non-let residue (MOVQ 8(BP)
garbage) now also routes resolver-or-loud. cstage's dispatch peel is
aligned to wwstage's existing TY_NAMED loop-chase (single-peel +
loud tail would have surfaced as cs-rejects/ww-accepts on 2-level
aliases).

Asm-neutrality: all five embedded main.combined.ww corpora compile
byte-identically under pristine-parent w6c vs fixed w6c; per-shape
pins (ident local/global, tuple, index, array) NEUTRAL + cs==ww.
802_lenidx_run grows 14 rows: the nine garbage shapes (incl. computed
index through a deref spine, param-vs-local *p, chained **pp, empty
slice), two neutrality controls (global and tuple fast-paths have
dedicated runs: 797, 903), three reject rows pinning the exact rule-7
text in BOTH stages; all fix rows verified FAILING against a pristine
build of the parent e481cb8 (12/19 fail there, 19/19 green here).

Consumers unblocked: regex fold-2b tranche C ha:795/798
len(threads[i].captures); lib/regex add_thread's (*threads).len
dodge (regex.ww:238, WHY comment cites #41) reverts to len(*threads)
with the tranche-C port, not here.
2026-06-04 13:37:19 +09:00
d642017643 wcc+w6c_ww: aggregate let/range element copies via cgplaceaddr (F5)
F5 (task #7): the N_LET aggregate-copy arm's source-addr enumeration
(cgen.c #265/#268) had TY_ARRAY-ident/N_DOT/N_INDEX bases but no
TY_SLICE base and no deref-spine shapes, so `let e: th = xs[0]` fell
out with havesrc=0 — cstage emitted NOTHING (slot uninitialised),
wwstage fell to its scalar default (8B truncation): gate-blind cs≠ww
(p6min13). Every remaining ADDRESSABLE rhs now resolves through
cgplaceaddr (the C1 resolver; enumerated arms dispatch first, their
asm untouched), and the arm closes by construction with a loud tail —
nothing below it can initialise a >8B struct/array slot, so any
unhandled rhs shape dies loud instead of silently. A pre-tail #38b
guard keeps the established `?`/`!`-on-sret loud-stop marker in
wwstage (mirror of cstage's pre-arm fatal; pre-fix that shape reached
the cgtryunw/cgtryprop gates which the tail now pre-empts in let
position).

Reviewer-C2 inheritance: `let c: capture = (*ts)[i].cap` (aggregate
leaf behind a deref spine) — wwstage's documented cgdot aggregate-leaf
loud is retired for let position (cglet routes the copy through the
resolver before cgexpr sees the leaf; the loud stays as the guard for
non-let expr positions), and cstage's silent no-copy on the same shape
is fixed by the same resolver fallback.

By-value RANGE payloads ride the same class: N_FORRANGE's single-bind
load truncated every aggregate element to one fldloadop word. Both
stages now word-copy the full element extent (MOVQ run + sized
MOVL/MOVW/MOVB tail, the #270-1b idiom) for esz > 8. wwstage esz is
re-keyed elemsizeof→elemsizeofc (the 8-sentinel hid struct elements
from the copy gate — the #8 named-narrow precedent), with a
stamped-slc.type_ fallback + element-tnode synthesis for non-ident
scrutinees (tinfo SSoT, #209/#211). The wwstage checker now binds the
ELEMENT type on single-bind ranges via a synthetic N_LET binder node
(mirror of cstage check.c N_FORRANGE scope_define(..., elem, ...));
pre-fix the binding's decl was the N_FORRANGE node itself, so any
field read off a by-value binding asserttyped-bailed. The checker
half folds in under rule 11 because the split is unsound in either
order: cgen-first is untestable (every field read off the binding
still bails), checker-first converts that loud bail into the 8B
SILENT truncation — only the pair closes the class.

FC0 graduates: regex.finish's by-value range over 24B charset elems
(non-ident scrutinee re.charsets) was the lib/regex byte-cmp's ONLY
hunk since fold-1 — cstage 8-of-24-byte copy + IMULQ $24 vs wwstage
1-byte MOVZBQ, runtime-masked by the no-op loop body. The byte-cmp is
now ZERO hunks (regex_test.combined.ww, w6c vs w6c_ww).

#36 disposition: NOT folded. p6min9/p6min10's remaining failure is the
struct-ident field rhs inside a struct LITERAL (cg_structlit_fill
under-copy) — a different choke-point from the let-copy source-addr
machinery; they still exit 4 here and stay blocked on #36 (read half
landed in C2).

Residual filed as task #43: an UNANNOTATED aggregate let
(`let e = xs[0]`) still skips the wwstage arm (aggn/letslotsize are
annotation-keyed; cstage keys the stamped n->type and now full-copies)
— cs≠ww on that shape remains, #38-family. A landmine comment in
test 805 marks the gap.

test 805: +6 rows — let-from-slice-elem 16B (p6min13 verbatim) /
24B/40B/12B(MOVQ+MOVL tail) matrix / deref-spine leaf / by-value range
([]struct both-fields sum, []capture 40B, []str 24B header) / range
edges (empty slice, by-VALUE binder-mutation pin, 12B elem MOVL tail)
/ reject row pinning the loud-tail text on both stages. All six fail
at the pristine parent 403625e (re-verified post-rebase; 121 prior
fixtures stay green there).
2026-06-04 13:00:03 +09:00
074e68f05d w6c_ww: tinfo re-key of the cgdot N_INDEX-base arm (F7/F10)
The wwstage `arr[i].field` read arm was syntactic where cstage is
type-table-driven: element typing keyed on tnode KINDs (N_TSLICE/
N_TARRAY/N_TPTR) with an N_TNAME element resolved by structlookup
NAME — any base typed via an alias (`type result = []capture`,
p11b/F10) missed every gate and died at the interim C2 loud guard
(pre-C2: fell silently to the SB fallback). Re-key the arm onto the
checker-stamped tinfo (lhs.type_ element / idxbase.type_ base, NAMED
peeled), mirroring cstage cgen.c case N_DOT's N_INDEX-lhs arm 1:1 —
the #209/#211 name-keyed->tinfo-SSoT cluster. Emission sequence is
unchanged; the C2 "C3/task #8" guard retires with the arm wired
(task #37's ptr-chain guard is untouched). Global classification
mirrors cstage let_islet || def_isarraydef via isletvar /
defvartnode-N_TARRAY (the cgplaceaddr C2 pattern).

F7/FA5/FA3-ww/FA6 (non-ident idxbase shapes, task #17's ww halves)
were already closed by C2's read-resolver recursion; p7_composed,
pA5, pA9 run exit-0 byte-id and are pinned as rows here. Task #29's
asserttyped bail (strings.frombytes over a slice-expr in the
composed context) no longer reproduces at HEAD — dissolved during
the C1.25->C2 arc; p7_composed builds clean on w6c_ww, runs 0,
byte-id.

Behind the retired guard three alias-blind NON-cgdot sites surface
(`let l: wlist = []` checker reject / alias-array global emits no
DATA / alias-array arrlit-init under-copies 8B per element, cs!=ww
runtime): filed as task #38, same name-keyed class, separate sites.

test/805: +5 rows — alias-slice field-kind matrix, alias-array +
viaptr-element bases, p11b-essence let-bound alias reads (the
task-#14 `?` factored out), the composed p7 match/compound hot
shape, and the pA9 free()-operand acceptance row (FA6).

Task #8; the last cgen gate before regex tranche B.
2026-06-04 11:34:16 +09:00
76994a8279 wcc+w6c_ww: typed dot-read resolver — kill both silent N_DOT fallbacks (F4)
A typed depth-2+ field chain behind an index/deref spine
(threads[0].cap.end, (*p)[i].f.g) aborted the cgdot chain walker
(N_IDENT roots only) and fell into the module-qualified-leaf fallback
— a silent `MOVQ <leaf>(SB)` global read of a colliding symbol
(p6min10 exit 66) or a link error. Single-dot reads behind a deref-
index ((*ts)[i].pc, (*p)[i].slicefield) fell into the bottom catch-all
instead, which is offset- and header-blind: offset-0 scalars read
correctly by COINCIDENCE, nonzero offsets and slice headers were
silently wrong in BOTH stages (FA3, pA5). `&threads[0].cap` dropped
the address-of silently and SEGFAULTed at the deref (task #6,
reviewer-A route).

cgplaceaddr (C1) grows an N_IDENT root (local / let / DATA-backed
def) and the N_INDEX base gate relaxes to recursion, so indexed-ident
and deref-rooted spines resolve; enumerated arms still dispatch first,
keeping every pre-C1 shape's asm. case N_DOT routes any TYPED read no
arm matched through the resolver (scalar fldloadop, float X0, str/
slice 3-word header, [N]T address); the module-leaf fallback is gated
to UNTYPED chains, the catch-all to untyped-str pseudo-fields, and
the TK_AMP tail is resolver-or-loud. Leaf kinds without a register
convention (tagged, aggregate) and unaddressable shapes die LOUD
(rule 7). wwstage mirrors symmetrically; two of its arm gaps must not
take the resolver (its sequence differs from cstage's arms — cs!=ww):
ident-indexed alias reads loud-cite C3 (task #8) and non-local-rooted
ptr-chains loud-cite task #37. A third verdict divergence is comment-
documented at the wwstage aggregate gate: cstage's let-init consumes
`let c = (*ts)[i].cap` BEFORE its N_DOT tail (emitting NO copy — the
F5 bug), so that shape cs-builds/ww-louds until the F5 let-copy lands
(task #7); absent from the gate corpus.

806 identroot_dot graduates from BUILD_FAIL: the C2 ident root makes
append(h.xs, v) through *holder resolve via C1.5's place consumer
(run-verified, byte-id). p6min9/p6min10 read-halves are fixed but the
probes stay blocked on the #36 literal under-copy this commit
unmasked (struct-ident field rhs copies 8B; repro filed with the
task).

test/805: +7 rows (typed depth-2 behind ident-index incl the 777
global-collision pin, deref-index, width/float/[N]u8 matrix, FA3
slice-field + .cap-behind-spine, &-route with compound-through-
pointer, C1's reject_tail graduated to stores, neutrality pins) and
+4 reject rows pinning the new loud texts; the C1.25 raw-byte
readbacks graduate to typed depth-2 reads.
2026-06-04 11:07:13 +09:00
b630a7cf20 wcc+w6c_ww: append through pointer-to-slice place via cgplaceaddr (FA1)
Re-key the append() lowering from BP-displacement assumptions onto a
resolver-provided header PLACE (task #15, the add_thread hard-blocker;
cgplaceaddr's third consumer after C1/C1.25). One mirrored choke-point,
two failure modes: cstage 0-defaulted sn_off for any non-ident target,
so 0(BP)/8(BP) became the "slice header" and rt_ensure corrupted the
CALLER frame (SIGSEGV); wwstage cgappend silently emitted nothing
(gate-blind cs!=ww).

cg_append_grow/cg_append_slot (mirror cgappendgrow/cgappendslot) factor
the 5 grow + 5 slot header-access sites. Ident-local targets keep the
legacy BP-disp emission byte-identical (probed across all 9 existing
source shapes, before/after .s). Non-ident targets resolve once through
cgplaceaddr and spill the header address to an @apphdrscr slot:
rt_ensure may realloc .ptr but never moves the header, so the slot
stays valid; every access reloads from it. The slot is allocated fresh
per append SITE, not cached per fn: a nested append-through-pointer
inside a value expression (match-yield arm) spills its own resolve, and
a shared slot would hand the outer grow/slot reloads the inner target's
header — silent cross-slice corruption (pinned by the reentrant_value
row). Indirect mode keys esz/element-kind/load-op off the
checker-stamped target tinfo (no declared tnode behind `*p`; the
#209/#211 discipline). Unwired target places die LOUD "#15: append()
target place unsupported (rule-7)" on BOTH stages — the
silent-corruption class is closed by construction.

The FA4/#35 boundary is unchanged: non-ident spread SOURCES stay loud
(pinned by a reject row). Surfaced pre-existing checker divergence
filed as task #34 (wwstage rejects global slice-lit let).

test/wcc/806: 14 runtime rows (element kinds x target shapes, spread,
narrow-signed spread load, cap-crossing realloc loop with branched
callee + caller-frame sentinels, deref-spine target, nested-append
reentrancy, direct-arm neutrality pin) + 2 exact-text reject rows,
both drivers + per-row cs==ww asm byte-id.
2026-06-04 10:44:18 +09:00
cfc2985c61 wcc+w6c_ww: aggregate-field stores via cgplaceaddr (C1.25)
Wire struct/array/tuple field STORE through the C1 assign-resolver
(task #23): structlit rhs materialises into a FRESH-per-use @placescr
slot (the @slicescr discipline — a cached slot is the #31 multi-live
trap) then word-copies to the resolved address; addressable rhs
(ident/global/dot/deref) sources via aggarg_srcaddr with the dest
spilled around the dispatch (#270-1b order). Kept loud: compound on
aggregate, sret call rhs (#234-tail), <=24B call rhs (task #24),
unaddressable literal rhs, ww-only anonymous-struct structinfo miss.

test/wcc/805: +6 rows (40B structlit incl ... autofill, ident+deref
source, nested literal, [3]u8 MOVW/MOVB and [3]u32 MOVL tails,
two same-size stores in one fn pinning fresh-per-use) +3 exact-text
reject rows. Tuple-field row blocked by the pre-existing tuple
param/let-init word-2 drops (tasks #32/#33, documented in-row).
p7b_capstore_only graduates byte-id and runs; p7_composed builds and
runs on cstage, wwstage stays behind the pre-existing #29 asserttyped
bail (verified identical at master with w6c_ww).
2026-06-04 10:00:16 +09:00
32063d0da0 wcc+w6c_ww: >48B tagged by-value args — MEMORY-class two-phase push (#38b)
Task #19 (the #38b residual surfaced by FC2 evidence): a tagged arg
whose slot exceeds the 6-reg convention (>48B) is MEMORY-class per
ref/qbe/amd64/sysv.c:80-85 (inmem) / :411-426 (stack blit). Caller
stages the whole slot below every register-class word (two-phase
push, rightmost-first, leftmost mem arg at 16(BP)); callee registers
the param in place at positive BP offsets with zero prologue bytes;
the merged slot count feeds the existing caller-cleanup ADDQ.
Argument-side mirror of the #38 tagged-sret fix, same classify
machinery (tagged_memarg_size / taggedmemargsize beside their
register-class siblings).

Pre-fix, the exact-typed arg loud-stopped on both stages, but
WIDENING a concrete variant into a >48B param slipped the old guard
silently — cstage pushed one scalar word while wwstage emitted an
uncapped greedy stitch (wrong on both AND cs≠ww, gate-blind). Widen
sources now route through the @tagscr scratch for mem slots.

Loud boundaries kept (rule 7), each with its own diagnostic:
sret-class tagged CALL result as mem-arg source (#40-family
follow-up), global tagged let (task #25, broken at any size
pre-existing), >48B variadic element, and mem-arg + register-
overflow mixing (caller check + callee prologue mirror).

Single commit: caller staging, callee receive, and both stages are
one inseparable ABI class — landing any half alone breaks byte-id
or runtime correctness (the #38 flip precedent); test/929 (15
table-driven rows: 56B/64B slots, widen-slip pin, source shapes,
mixed orders both ways, two-mem call, 200k-call loop, 48B-boundary
absence pin byte-id'd vs master, 5 reject rows pinning the exact
per-guard diagnostic on both stages) rides with it.
2026-06-04 09:36:15 +09:00
e3e6b5a820 wcc+w6c_ww: cgplaceaddr resolver — deref-base assign stores (F6)
(*ts)[i].field = v / OP= v (the regex run_thread hot shape, task #4)
compiled to NOTHING in both stages, byte-identically: the N_DOT lhs
roots at N_UN(STAR), so the arr[i].field arm (idxbase must be IDENT)
and the chained-ptr-field arm (base must be *struct) both miss and
the N_ASSIGN dispatch fell off the switch silently, rhs unevaluated.

cgplaceaddr (one per stage) is ADDRESS COMPUTATION ONLY — N_UN(STAR)
root, N_INDEX hop over a slice/array place (.ptr hop for slice),
N_DOT struct-field hop with one deref for a *struct base. Call-sites
keep their own emission: scalar fldstoreop store, str/slice 3-word
header store staged through DX, 10-op compound template with the
chained-ptr-field register roles. Ident-rooted spines stay with the
enumerated arms — verified asm-neutral over the 84 fold2b probe
sources against fresh master-HEAD binaries (7 diffs = the F6 family
now emitting stores; 2 verdict flips = aggregate-field stores, now
loud).

Silent dispatch tails go LOUD for N_DOT lvalues the resolver can't
address and for unresolved-identifier targets (cstage float-ident arm
aligned to wwstage's resolve-first order). Aggregate-field stores
loud-reject pending the follow-up resolver commit (task #23, ≤24B
N_CALL rhs split to #24). The non-DOT tail stays silent deliberately:
going loud there would asymmetrically surface the pre-existing
str-base element-store divergence — task #22, cited at both sites.

test/805: 17 rows x 2 drivers + 12 cs==ww byte-id fixtures — widths
(incl narrow-compound fldloadop sign/zero-extension), all 10 compound
ops (DIVQ/IDIVQ/SHLQ/SARQ/SHRQ), str + slice 3-word stores, *[N]T
base, runtime call index, ident-base neutrality pins, and 5 reject
rows asserting exact diagnostic text.
2026-06-04 09:13:16 +09:00
37febab9d5 wcc+w6c+w6c_ww: delete() builtin — single-element slice removal (part of #35)
Hare's delete(xs[i]) (ref/harec/src/check.c:1981-2027): checker accepts
an N_INDEX over a slice-typed base, stamps void; loud-rejects the range
form delete(xs[i..j]) (stays filed on #35 — regex fold-2b's consumers
are all single-element), non-index operands, array bases, wrong arity.

Lowering (both stages, converged byte-identical by construction):
ascending word-copy loop shifts [i+1..len) down one esz stride, then
hdr.len -= 1; cap unchanged. The move is a same-type whole-stride byte
copy — src and dst are elements of the SAME slice, so no boxing exists
for any element kind; one loop serves scalar/narrow/str/struct/tagged.
esz off the STAMPED base type (#34/#48 discipline). Base shapes: local
slice ident (LEAQ) and deref-of-local ptr-to-slice (MOVQ — the fold-2b
delete_thread shape); others rule-7 loud-stop.

test/804: 38 fixtures — first/middle/last/to-empty, esz 1/4/8/24/56
(MOVB/MOVL tails + 7-qword tagged), cap-unchanged, (*threads)[i], 4
checker reject rows; every accept row cs==ww asm byte-id.
2026-06-04 06:41:12 +09:00
9732061a7e w6c+w6c_ww: free() compiles to a no-op (ww has no free) (fix #27)
The free(x) builtin lowered to CALL ffi_resolve("free") in cstage and
fell through to a generic CALL free in wwstage (which had no free arm
at all) -- an undefined reference at w6l unless an @symbol decl
happened to be in scope. ww has no free by design (rt/alloc.s:30 --
the bump allocator cannot reclaim a mid-chunk pointer; process exit
does), so both stages now evaluate the operand for side effects
(Hare's free(expr) evaluates expr) and emit nothing else, letting
Hare code that calls free() port verbatim (regex fold-2b calls it at
4+ sites). The 2-arg os.free(p, n) public API is untouched: the
builtin gate requires exactly one bare-ident-callee arg.

930_free_noop_run pins per row: w6c/w6c_ww byte-id, no free symbol
in the .s, deref-after-free validity, and the operand side effect
running once per free() via a global counter.
2026-06-04 06:07:18 +09:00
d099c29b86 w6c_ww: matchscrutt resolves non-ident index bases via stamped type (fix #48)
Pre-#48 wwstage matchscrutt's N_INDEX arm required ibase.kind ==
N_IDENT; an index over any other base (match (h.xs[i]) = N_INDEX over
N_DOT, the regex fold-2a re.insts[i] shape) returned nil, so cgmatch
dispatched with scrutt=nil — every case arm's variant index clamped to
0 (CMPQ $0) and @match_spill fell to the 16B default. SILENT cs≠ww
runtime-wrong (cstage N_MATCH reads the checker-stamped s->type for
every scrutinee shape, cmd/w6c/cgen.c:7510). The non-ident-base arm now
returns the scrutinee node itself behind an istaggedtype gate — the
stamped-carrier pattern of the #67 N_DOT arm and the #45 cgtypetest
fix — so any base shape resolves the element's tagged tinfo for both
variant indices and spill sizing.

Same-class load half, one commit per the #133-expanded precedent:
cgindex's generic-fallback tagged-element load was the only arm missing
the slot>24 R8 word (both ident arms and cstage cgen.c:9106-9117 have
it), so a >24B-slot element via a non-ident base under-read the cursor
and the now-correctly-sized spill stored stale R8.

928_match_nonident_idx_run pins the repro shape (field-base slice
index, all variants both polarities), the regex shape (56B-slot
inst-like union, payload reads within the 32B cursor per #43), and
ident/array/slice ident-base controls — per row cs==ww byte-id +
runtime via both drivers. w6c/wwdump combined.ww regen'd via canonical
make; selfhost corpus hand-cmp'd cs==ww both stages.

Pre-existing siblings surfaced while probing, NOT folded (rule 11),
reported for filing: (a) cgindex element classification skips N_CALL
bases entirely (mk()[0] — wrong esz + not tagged-classified, cs≠ww,
runtime-wrong, also non-match contexts); (b) `as` on a non-ident
carrier still clamps the variant to 0 (cgtagvariantidx's N_TTAGGED node
gate rejects the stamped carrier; byte-identical to master, the #200
spill fix covered only slot sizing).
2026-06-04 05:21:46 +09:00
b3d6bc4420 w6c_ww: cgtypetest nullable is discriminates pointer-vs-null (#45 review)
The #45 non-ident arm tag-compared the word in AX against the variant
index; for the nullable (*T | void) fold that word IS the pointer —
`h.m is *t` on a non-null pointer answered FALSE (silent cs≠ww,
cstage correct: CMPQ $0 + JE/JNE polarity per cgen.c N_TYPETEST).
The ident path had the same missing nullable arm since before #45
(pre-existing at master, unexercised in the bootstrap corpus). One
nullable branch at the shared compare choke-point closes both halves:
want stays RAW (cstage tests tag == ptr_tag unclamped, a no-match -1
takes the void polarity). Rows nullable_dot_field + nullable_ident
pin both polarities and both states in 927; whole-corpus control
(5 selfhost combined.ww, master-vs-branch w6c + w6c_ww) byte-id.
2026-06-04 04:54:43 +09:00
b2e4388792 w6c_ww: cgtypetest resolves non-ident scrutinees, no-spill tag compare (fix #45)
Pre-#45 wwstage `is` resolved only N_IDENT scrutinees; xs[i] / p.field
/ call() fell through with scrutoff=0 + scrutt=nil and emitted
MOVQ (BP),AX; CMPQ $0,AX — tag read off the saved-BP word, variant
clamped to 0 (SILENT cs≠ww; cstage cgexprs the scrutinee and compares
the real tag in AX). The non-ident arm now cgexprs the scrutinee (tag
lands in AX) and compares directly. NOT the `as` twin's @asrt_spill
(#200): cmp against cstage shows N_TYPETEST never spills — `as`
re-reads payload words after the check, `is` consumes only the tag,
and a spill would break rule-10 byte-id. Variant index resolves from
the STAMPED scrutinee type via flatvariantidx/flatslicevariantidx
(matchscrutt's node walk can't carry N_DOT through cgtypetest's
N_TTAGGED gate). Ident path untouched (control row + hand-cmp vs
pre-#45 w6c_ww). wwstage-only source change; cs==ww byte-id pinned
per row in 927_is_nonident_run.
2026-06-04 04:40:54 +09:00
c2308a11c7 w6c+w6c_ww: size-keyed @tagscr — one tagged scratch per slot size (fix #44)
A fn mixing two tagged slot sizes smaller-first (regex compile(): 56B
append-element widen then 64B sret return) hit the #15/#26c rule-7
grow-fatal — the single shared per-fn @tagscr is first-use-sized and
its pinned offset can't grow. Key the scratch by slot size instead:
@tagscr<sz>, one first-use-allocated slot per distinct size, all three
sites (widen-store via_outer, widen-push, N_INDEX tagged-element
assign) funnelled through cg_tagscr_slot / tagscradd in both stages.
Single-size fns emit byte-identical asm to pre-fix (control row pinned
+ hand-cmp'd vs master w6c). 736's tagscr_size_grow_fatal fixture
pinned the now-unreachable fatal; converted to a byte-id succ row.
Runtime rows live in 926_tagscr_sizes_run.
2026-06-04 04:34:54 +09:00
4f3967835e w6c+w6c_ww: tagged sret for slot>32B returns (fix #38)
A tagged-union RETURN rides a fixed AX(tag)+DX/CX/R8 cursor (TUPLE_GPCAP
eightbytes = 32B slot); wider slots were silently truncated at the
return crossing — payload word 4+ built in the callee frame and died
there, byte-identical on both stages (gate-blind). Blocks regex fold-2a
((regex | error | nomem) = 64B slot).

Classifier: cg_sret_retsize / sretretsize gain a TY_TAGGED arm
(<= TUPLE_GPCAP*8 stays register-ABI — the (str|nomem)/(s3|bool) 32B
boundary class is pinned unchanged byte-for-byte vs master). Callee:
cgreturn writes the slot through *(@sretarg) via the existing widener
non-BP base (bare return stores the void tag); exact-type 'return f();'
rides the #9 sret-forward. Receive: let/assign/discard reuse the
generic #23/#10 sret protocol; the match scrutinee passes its spill
slot as the sret dest (tagged-specific, no tuple precedent).

This could NOT land as a gate-first interim loud-stop (the planned
#38a): lib/errors/errors.ww errno() already returns a 40B
(errors.error) slot in-tree — the cgenstmt.ww-documented #222 latent —
so a bare gate breaks the build. errno graduates to sret here instead;
errnotest pins it at runtime (its cstage run; the wwstage run was
already failing at master via an unrelated pre-existing indirect-call
arg-classification divergence, reported separately) and test/926's
errno-shaped row reads the previously-dropped tail word on both stages.

The unwired cursor consumers of an sret-class call result loud-stop
(rule 7) rather than read a cursor the callee no longer fills:
widening forward/receive ((A|B)->(A|B|C) mem-to-mem tag-remap, filed
#40), ?/!/is/as operands, argument position, and the >48B tagged-arg
class both stages previously mishandled silently. One-class-one-commit
per the #133 carve-out: post-flip those consumers would read AX (now
the dest pointer) as the tag — a gates-trailing commit would leave a
silently-wrong bisect point, so the flip and its gates are not
separable.

test/926: 15 rows — 56B regex-shaped round-trips (literal/local/
assign/match-scrutinee/forward/str-variant/multi-call), 40B repro +
bare-return-void, the errno-shaped tail-read graduation row, 32B
boundary rows pinned register-ABI by asm sentinel, and 3 loud-stop
rows pinned as build failures on both stages.
2026-06-04 03:47:44 +09:00
36be9f469d w6c+w6c_ww: loud-stop non-ident/non-local append spread source (#34 review)
A spread whose source was not a local ident fell PAST the spread arm:
cstage continued into the single-value stores with the N_SPREAD node
(garbage store), wwstage silently SKIPPED the value entirely — a silent,
cs!=ww-divergent miscompile (append(ys, f()...): cs exit 0 / ww exit 144,
want 3), reachable for every element kind and predating #34 for scalars.
Both stages now rule-7 loud-stop the shape (deferred, task #37).

Also pins the widener's already-tagged single-value source path
(tagged_ident_src row, i64 + bool members) and adds the
spread_call_loudstop BUILD_FAIL row — test 800 is now 15 rows / 43
fixtures.
2026-06-04 02:27:06 +09:00
faade48513 w6c+w6c_ww: append() stores the full element width per element kind (fix #34)
Both stages lowered the append element store as one sized mov from AX —
correct only for scalars <= 8B. A str/slice element kept only .ptr
(byte-id-blind), a tagged element got its raw payload written into the
tag slot (the #12 pathology, no boxing), a struct element kept only its
first qword. wwstage additionally fed rt_ensure membsz from bare
elemsizeof, whose 8-sentinel under-allocated and mis-strided named
tagged/struct elements (the #8 family; cs!=ww on the SI imm + stride).

Fix, keyed on the DECLARED slice local's element type (cstage
su->sub->size as before; wwstage elemsizeofc off the stamped tnode —
never the value node, the #25/#31 esz=0 trap), applied to both the
single-value and spread bodies (2 arms x 2 stages):

- scalar 1/2/4/8: untouched (u8 asm byte-identical to pre-fix).
- str/slice: AX/BX/CX pushed across rt_ensure, dst in DX (BX holds the
  element .len after the pops — the #24 register discipline), 3-word
  store.
- tagged: grow first, dst -> BX, box via the #12 widen choke-point
  (cg_widen_tagged_store / cgwidentaggedstore via_outer).
- struct: grow first; literal -> dst spilled to per-fn @appendscr
  (cached on cstage to mirror wwstage's @-prefix localadd dedup) +
  structlit fill DST_PTR_LOCAL; local ident -> word-copy; any other
  source shape is a rule-7 loud-stop, never a silent scalar
  fall-through. struct-from-call deferred.
- spread: the source element is already a fully-formed T (tag
  included), so the wide arm grows first and whole-width word-copies
  &items[i] -> dst, recomputing both addresses from the slice headers
  after the possibly-reallocating rt_ensure.

The elemsizeofc swap also corrects the named-scalar-alias membsz
(wwstage fed SI=$8 where cstage fed $4); no in-tree consumer appended
to such a slice, so nothing was riding the wrong 8 (lib/selfhost append
sites are all u8).

Test 800_append_wide_elem: 13 rows (runtime readback per kind, 2-append
realloc survival, spread str+tagged, @appendscr dedup, enum-alias esz,
loud-stop build-fail) + per-row cs==ww byte-id, which subsumes the
frame canary.
2026-06-04 02:14:14 +09:00
bf1037d8c4 wcc/check+w6c+w6c_ww: materialize array-literal slice-borrow base into per-fn scratch (fix #25 + #31)
A one-step `let xs: []T = [e0,e1,..]` had two faults. #31 (silent, cs!=ww):
the #258 array→slice borrow wrapped the un-addressable N_ARRLIT directly as
the N_SLICE base and cgen never spilled it to a stack slot, so .ptr dangled
(`let xs:[]i32=[10,20,30]; xs[1]` returned the un-stored header 1; []u8/[]str
segfaulted). #25 (over-strict): a slice target fell through to the exact-
element type_eq borrow gate, rejecting bare-int-width ([]u8=[1,2,3]) and str
elements the array-init path coerces.

Fix (re-stamp + per-borrow scratch; both stages byte-identical asm):
 - Checker re-stamps the slice arrlit as [count]T, reusing the array-init
   per-element coercion + range-check (#25): in-range accepts, out-of-range
   loud-rejects. cstage arrlit_init_fits gains a TY_SLICE arm; wwstage
   checkletassign mirrors it and stashes the synthesized [count]T tnode on
   arrlit.lhs (free for N_ARRLIT) so cgen can size the backing NODE-wise
   (elemsizeofc) and count from the tnode's .rhs intlit — the arrlit's own
   value tinfo carries the literal's untyped element (unsized), so node-first
   sizing is required (a cstage/wwstage representation divergence; cstage's
   Type IS sized and reads base->type).
 - cgen materialises the N_ARRLIT borrow base into a FRESH per-borrow
   @slicescr stack slot (distinct slot per borrow: a borrow's backing must
   outlive the lowering, so it can't share a cached @aggargscr/@tagscr-style
   slot — two live borrows would alias one backing; localalloc/local_alloc
   is always-fresh), filled by REUSING the array-init element fill extracted
   from the N_LET path (cstage cg_arrlit_fill_bp, wwstage cgarrlitfillbp —
   same store sequence the byte-id-green `let a:[N]T=[..]` uses, the
   frame-order + store-op guarantee), then LEAQ'd as the base.

Supported ONLY at a `let` init. In call-arg / return / assign position
there is no addressable backing, so both stages LOUD-REJECT ("bind it to a
`let` first") — aligning cstage DOWN to wwstage (which already refused the
untyped arrlit element) per rule-10; this closes #31's silent call-arg
segfault as a compile error. Full non-let support is deferred (#33).

Escape (rule-8 WHY): a `let xs:[]T=[..]; return xs;` returns a slice into a
freed frame slot = dangling, IDENTICAL to the pre-existing named-array
borrow and Hare-consistent (no escape analysis / GC / heap promotion).

Test 953_arrlit_slice_run: 8 accept rows (cstage runtime readback +
cs==ww byte-id, frame-size canary incl.) covering the #31 i32 pin, bare-int→u8
coercion, str readback, the multi-live soundness pin (xs[0]+ys[0]=5, not 8 —
proves fresh-per-borrow), and a mutate-through-borrow proof; 4 reject rows
(out-of-range element + the three non-let contexts, loud in both stages).
Tuple-element slices stay blocked by the pre-existing #30 array-init FATAL.
2026-06-04 01:44:39 +09:00
8dda8ea76c w6c+w6c_ww: global-base arm for indexed struct-element field read (fix #21)
The `arr[i].field` N_DOT read branch in both stages was gated on a LOCAL
base lookup (cstage `localfind != 0`, wwstage `localfindnode != nil`). A
module-GLOBAL base (`let g: [2]pt = [...]`) missed it:

  - cstage fell to a generic index-load that drops f->offset — it read
    element[i] at offset 0, so `g[i].b` returned a's value (g[0].b -> 1,
    g[1].b -> 3 instead of 2, 4).
  - wwstage fell to the module-qualified SB fallback — garbage, no main.g
    load at all.

Silent, byte-id-divergent. This is the READ twin of #11 (the global
`g[i] = v` write fix) and the #15 sibling. Local `[N]struct` bases read
correctly (tests 680/681 cover only those), which is why it was never
caught.

Fix (both stages, converged byte-identical): resolve the global the same
way the N_INDEX arm does — cstage `let_islet || def_isarraydef`, wwstage
`letvartnode || defvartnode` — and dispatch the base load by shape: array
-> LEAQ name(SB) (the symbol IS the storage), slice/ptr -> MOVQ name(SB)
(the symbol's first word IS the .ptr). The field then loads at f->offset
exactly as the local arm does. esz (element stride) and f->offset both
come from the type table (rule 13). Mirrors #11's write-side global-base
resolution. combined.ww embeds (w6c + wwdump) regenerate.

688_global_arr_elem_field: global `[2]pt` reads of .a/.b on both elements
(the .b reads are the bug), a non-8-aligned `[2]rec {tag:u8,x:i32,y:i64}`
to stress f->offset + a u8 sub-word leaf, and a slice-base read
(`let g: []rec = arr;`) that exercises the MOVQ-deref .ptr arm. Runtime
(cstage build+run) + cstage==wwstage byte-id per row. The slice row is
byte-id ONLY: its read asm is correct and identical on both stages, but a
slice-of-struct module global does not data-emit a symbol yet (a separate,
pre-existing data-emission gap, sibling of #10/#20), so it cannot link/run.
2026-06-03 22:38:02 +09:00
7ca32432b1 w6c+wcc/check: infer [_]T array length from initializer element count (fix #7)
`[_]T = [...]` (canonical Hare array-length inference) silently
miscompiled to a zero-length array: the parser already left the array
type's length child nil as the infer sentinel — distinct from an
explicit [N] — but neither checker stamped the real count, so `len(x)`
returned 0 with no diagnostic (rule-7 silent miscompile). Module-level
was worse on wwstage, where `x.len` on ANY global array (even an
explicit [N]) fell to the SB fallback and mis-emitted `MOVQ len(SB), AX`
(linker: undefined reference to len).

The length lives in the stamped TYPE and cgen already keys stride /
length / data-emission off it, so stamping the inferred count at the one
checker inference point closes it permanently (rob's #7 ruling):

  - check.c clet + module-level N_LET pass-2: count the initializer's
    elements and patch the array type's length (the Sym too, so a later
    x.len reads the inferred alen). No-init / non-array init can't infer
    -> loud error, never a silent zero-length array.
  - check.ww inferarraylen: the wwstage twin — stamp a synthesized
    N_INTLIT length child before resolvewalk caches the array tinfo;
    same loud-error rule. Idempotent for the module-level double-call.
  - cgenexpr.ww cgdot: the missing wwstage arm for a top-level [N]T
    global's .len / .ptr (cstage cgen.c:8011 already had it).
  - cgenutil.ww letslotsize: drop the now-redundant [_] slot-size
    intercept — a workaround for this very bug; the stamped length flows
    through the general slotsize path (rule 7).

Both stages converge byte-identical; new table-driven test 684 covers
[_]int/[_]str/[_]u8 local + module-level, len + element read-back,
dual-stage runtime + asm byte-id, plus three negative no-infer rows.
2026-06-03 18:44:39 +09:00