pushargsrev's N_SLICE arm resolved esz and the base address from the
stamped tinfo of an N_DOT [N]T-field base but let the omitted hi bound
fall to $0, so the #258 array-to-slice borrow desugar handed callees a
zero-length slice: base64 clear()'s bytes.zero(e.ibuf) was a runtime
no-op under the wwstage frontend. Take the element count from the
field's array tinfo like the cgslice #252 arm and cstage's bu->alen.
Graduates r989_libprecond_decodedsize_aligned and the 989_lib_byteid
#59.3 base64 pin.
peel-ok/sizelint-ok/primsize-ok annotations lose their tools; sites
keep the WHY in plain words. Citations of retired carriers move to
their fixture or @test successors (949_errtype_compare -> r949_*,
900_stdlib -> library owners).
The W1/W2/W5/W4b store / structlit-fill arms still resolved struct field
LAYOUT by bare-leaf name (structlookupchain / structlookup / letvarstructinfo)
inside the per-field store loop -- the write-side twin of commit-1's read fix.
Under a cross-module same-leaf collision (two modules each exporting a `pair`,
16B vs 24B) the field is stored at the wrong offset/width; an interior 2-byte
field store silently corrupted neighbours. cstage walks the resolved Type*
(never a name) and is correct; this aligns wwstage UP (ww-only change).
Convert each store loop to a tinfo-native fill. Two new helpers in cgenutil.ww:
sretretsizetn (a verbatim port of cstage cg_sret_retsize -- 4 kind-branches,
no TY_BANG, dropping the #129 module-juggling that was pure name-keying
compensation, redundant once the lookup walks resolved tinfo) and
cgstructlitfilltn (the tinfo-native twin of cgstructlitfill, recursing on
tichase(tf.type_).fields with NO name lookup at any nesting level). Supporting
twins storeopsz / tfieldlookup / cgdotfieldhardstoptn. Because the fill never
bottoms out on a name, the W4b "decisive wall" (a structlit that needed a
(sname,smod)-keyed structinfo the stamped leaf-only tinfo couldn't supply)
dissolves by construction, with no module-aware tinfo->structinfo bridge.
This CLOSES the nine in-loop nested sub-arms (N_CALL / N_STRUCTLIT / N_IDENT
field receive) by construction -- the tf walk has no name lookup. The
non-reddenable global arms (W5/W4b) are converted-for-construction: a qualified
global decl resolves its bare leaf correctly today, so byte-id (cs.s==ww.s) is
their net, not a value pin. `!T` is kind-preserving in both stages
(resolve_type / tinfofornode), so a `!struct` sret sizes as the inner struct,
never TAGGED -- the #129 drop changes no size.
Completes the 2-commit #31 arc (RULING R2 / Opt-2, .ai/ken-31-spec.md): the
#224 name-keyed receiver-layout cluster is now closed by construction. The
residual nested-field surface (the shared cgstructlitfill kept for its non-#31
callers + the cgenstmt/cgenutil let-receive/fill family) is tracked as #32.
Pins: test/wcc/797 value-asserts ptr-WRITE (W1) and val-WRITE (W2), each
reddening under independent per-arm revert; nestfill_box drives
cgstructlitfilltn's nested recursion (bootstrap-uncovered -- the corpus has no
struct-literal field store -- so the value pin is its only net).
wwstage cgen resolved struct LAYOUT by bare-leaf name (structlookup /
structparamsize) at three caller-side sites — the by-value arg push
(cgenutil), the let-receive copy width (cgenstmt), and the field-read
offset (cgenexpr). Under a cross-module same-leaf collision (two modules
each exporting a `pair`, 16B vs 24B) the name lookup first-matches the
WRONG type, so the push dropped the 2nd eightbyte, the receive over-copied,
and the field read the wrong offset. cstage type-keys off the stamped
tinfo and is correct; this aligns wwstage UP to it (ww-only change).
Route all three sites through the stamped node.type_ via a new
structabisizetn(*tinfo) accessor (push + receive) and tichase(type_).fields
(field-read, structlookupchain removed). One commit (rule-11 carve-out):
the collision drives all three at once and no per-site fixture isolates, so
it cannot bisect-split. A scoped slice of the #209/#211 name-keyed-cgen
cluster retirement; the cgdot *struct-ptr/global and let-copy siblings stay
name-keyed and are filed (#31).
New table-driven test 793_xmod_struct_argpush_collide_run (4 scenarios:
push/recv/field over 16B and 12B tails) reddens under a revert of the three
cgen files. Full make test green (336 passed); make sizelint clean.
Completes the #14 close-by-construction begun by the helper extraction (e7fefa3): every <=24B aggregate register-store now routes through cg_agg_reg_store/cgaggregstore and handles all tail sizes. B/C/D/G fix a silent both-stage drop of a 3/5/6/7-byte tail (byte-id-blind: both stages dropped identically, so the gate could not see it). A/F enable a previously loud-rejected shape (a cgen backend gap, not a type rejection; harec accepts and lowers it). G (global g=f() array) routes symmetrically, dest_padded=false. The #11 arr[i].f scratch loops fold into the helper (dest_padded=true, byte-id zero-change), completing the grep-audit. Pins value-assert each eightbyte (the class is byte-id-blind) and redden under each stage's independent revert; site H's 3/5/6/7 let-receive stays a loud fatal (#22).
cg_structlit_fill/cgstructlitfill dispatched scalar field stores as {1->MOVB, 4->MOVL, else->MOVQ} with no fsz==2 case, so a 2-byte field was stored with an 8-byte MOVQ. Interior over-stores were harmlessly overwritten by the next field, but the LAST field at the frame edge corrupted the saved base pointer: an (S|e) union success variant places the struct payload after the 8B tag, landing the last field at -4(BP), so MOVQ AX,-4(BP) wrote into saved [BP] and POPQ BP restored garbage — a silent both-stage caller-frame clobber. Route the scalar store through the existing fldstoreop/fieldstoreop helper ({1->MOVB,2->MOVW,4->MOVL,else->MOVQ}), both stages; the #13 graduation comments already pre-documented this resolution. Pure width fix, no loud-stop (scalar widths are always {1,2,4,8} and narrowing is always correct). Value-asserting pin: an i64 sentinel live across the union-maker call (detects the clobber directly) + all members, with a non-union control.
A struct/array success variant in an (S|e)! / r? unwrap dropped eightbytes on BOTH stages (byte-id blind). Two layers: (L1) the unwrap success shuffle (cgtrytaggedshift) matched no arm for a struct/array success and fell to a bare MOVQ DX,AX, materializing only w0 — widen the existing nested-TAGGED shift's gate to admit TY_STRUCT/TY_ARRAY (the in-cap union packs the payload as raw GP words past the tag, so that shift is exact); (L2) the aggregate store arms gated on rhs.kind==N_CALL and stored one word for an unwrap rhs — relax to also admit N_TRYUNW/N_TRYPROP at the three silent store shapes (arr[i]=, single-dot field, indexed-field), reusing the materialise scratch path (now #10-correct). Rule-7 LOUD-STOP for a float-bearing success variant (an SSE eightbyte cannot ride the GP {AX,DX,CX} shift, #165). The four already-loud unwrap consumers (let-receive #7, call-arg #271, assign-existing #49, resolver-field #24) stay loud; global/chained single-dot field (#16) and the sub-8-tail-through-unwrap union-maker frame clobber (#15) are separate follow-ups. Value-asserting pin, reddens under each stage's independent revert.
nodeisslice/nodeisstr lacked an N_TRYUNW/N_TRYPROP arm, so a str/slice produced by an unwrap (f()!, r!, r?) and passed as a call arg fell to the 1-word scalar push, dropping .len/.cap; cstage's type-keyed node_isslice/node_isstr already pushed 3 words. Add the type-keyed arm reading the checker-stamped success-variant n.type_, mirroring #9's N_UN/TK_STAR arm. Fixes the str case (wwstage align-up to cstage); the slice success shuffle that both stages still get wrong is fixed in the Mech B follow-up.
nodeisslice/nodeisstr lacked an N_UN(TK_STAR) arm, so a deref-source slice/str call arg (f(*h), h:*[]T) fell to the scalar single-PUSHQ default — marshalling only .ptr and dropping .len/.cap. Add the type-keyed arm (read checker-stamped n.type_, mirror cstage node_isslice/node_isstr and the sibling N_DOT/N_INDEX arms). cgen already loads the full 24B header (C1b c67f362); this fixes only the call-arg push/pop count. wwstage-only align-up; cstage was always correct.
`alloc(Outer{ x = Inner{q=10} })` dropped the nested struct-literal field:
the alloc path had its own inline fill loop with only scalar/float/str
arms, so a field whose value is itself an N_STRUCTLIT fell to the scalar
tail and stored MOVQ $0 (cgexpr leaves a whole aggregate in no register)
over the inner slot. Both stages emitted the identical wrong fill, so the
byte-id gate was blind to it.
Route alloc's fill through the existing shared structlit-fill helper (the
one the BP-relative/global/local structlit sites already use -- it handles
nested-struct recursion, N_ARRLIT, str/slice and tagged) via a new 4th
destination mode DST_PTR_SP that reloads the heap base from (SP). This
deletes alloc's divergent inline loop, the lone site lacking the recursion.
As a side effect it also fixes a latent slice-field drop in the driver's
own alloc(sepgraph{...}) (pkg.len/.cap were dropped; the consumer reads
neither -- g.n is the count SSoT). Nested-array fields are closed in-class;
a nested tuple-LITERAL field now errors loudly and symmetrically (the #49
non-addressable gap, previously dropped silently at alloc only).
Surfaced by the codegen miscompile hunt (finding C7c). Pinned by
test/lang/alloc_nested_field_test.ww (nested struct depth 1+2, nested
array, adjacent multi-nested, sibling-no-clobber; reddens on revert).
Routing preservation proven: the whole test/lang corpus is byte-identical
HEAD vs fixed except the new pin; self-compile byte-id (990-996) green.
A >32B tagged-union field (slice payload) read through a direct *struct
pointer byte-diverged: wwstage's cgloadtaggedfield always loaded R8@+24
before CX@+16, but cstage's direct-*struct-ptr arm (cgen.c ~11926) loads in
offset order CX@+16 then R8@+24. Both ran correct -- a pre-existing rule-10
asm divergence, for a local *struct ptr as well as a global one.
Thread a cxlast flag through cgloadtaggedfield: the direct-ptr site
(cgptrfieldload, the shared local+global chokepoint) passes cxlast=false to
match cstage's offset order; the other 5 callers keep cxlast=true (byte
unchanged). A global flip was rejected -- it would clobber the CX-base
callers (CX@+16 first destroys the base before the R8@+24 read), and the
chained-BX caller must stay R8-first to mirror cstage's chained twin
(cgen.c ~12021); the order is a genuine per-arm property of cstage, not
derivable from the base register.
Test: +2 rows (tagged_slice_field via global *struct ptr, _local via local
*struct ptr), runtime + byte-id; both proven to fail byte-id with only the
compiler files reverted.
wwstage's chained-N_DOT resolver (dotchainresolve) didn't resolve a global
*struct root (only local *T and global value-struct), so gp.sf.len / gp.x.y
bailed to an inner-dot load + shuffle, byte-diverging from cstage's offset-fold.
Both stages already ran correct after #15 (475c003) -- a pure rule-10 asm
divergence. cstage is untouched (the oracle); wwstage aligns up.
Resolve a global N_TPTR root, and extract emitchainbase for the viacx base-load
(byte-identical across the 5 read + 2 store sites it replaces). The chained
STORE caller declines the global-ptr root (yok=false) so it falls to cstage's
address-spine mirror -- matching the #6/#15 decline-to-resolver discipline;
local *T chained stores still fold.
Test: +2 chained rows (gp.sf.len, gp.x.q), runtime + byte-id; proven to fail
byte-id with only the compiler files reverted, pass with the fix.
Sibling follow-ups filed: #17 (>32B tagged word-order), #18 (chained read into
an i64 sink MOVSXD check).
Mirror cstage's C-variadic call handling in the ww self-host: parse a
bare `...` param (decl.ww), skip param-keyed desugar for it to avoid a
nil-deref (check.ww), and emit AL = XMM-reg count plus CVTSS2SD
promotion of f32 args in the variadic tail (cgenutil.ww, cgenexpr.ww).
Closes the cat-A wwstage silent miscompile (AL=0, unpromoted f32 tail).
Parse/check/cgen are one atomic align-up (parse alone miscompiles, so
not bisect-splittable). 989_ffivariadic now runs dual-stage (cstage ww
+ wwstage ww_ww), 12/12; w6c==w6c_ww byte-identical. Byte-id alone is
blind here (the bootstrap calls no float-bearing C variadic), so the
ww_ww runtime rows are the real net.
After the frontend consolidated into one syntax package (#74), wcc still referenced syntax symbols unqualified — residue of the old flat combined namespace, where bare refs resolved by accident. Under separate compilation Hare and Go both require the package qualifier, so those bare refs would not sep-resolve.
Qualify every wcc reference to a syntax type, function, or enum member as syntax.X across the seven syntax-importing files. Resolution-only: the resolved symbol and emitted code are unchanged, so the two combined.ww regenerate textually but all five _ww binaries hold byte-for-byte. The struct-literal sites resolve via #76. This makes w6c fully separate-compilable.
The ww compiler frontend was split across packages lex (lex+tok), ww
(ast+sym+typ), and parse — mirroring Hare's ref/hare/hare/{ast,lex,parse}.
That split's only payoff is third-party reuse, which ww has zero of: the
frontend is consumed by exactly one client, the wcc backend. The split's
cost is a wide cross-package export surface — every fn over a sibling
package's type must export it, and under separate compilation that
re-triggers check_exported_type, plus a phantom `import tok;` (tok lives
in package lex). Consolidate into ONE package lib/ww/syntax/, modelled on
Go's cmd/compile/internal/syntax. The 9 files move in (package syntax);
the intra-frontend mutual references become same-package; wcc and the
tool mains import syntax. No cstage C change (the C frontend mangles from
the source package clause). Internal data shapes (AST kinds, token model,
lexer/parser state) still mirror ref/hare/hare per rule 6/12 — only the
module decomposition collapses; the stdlib is untouched.
USER-approved (#74); spec .ai/rob-frontend-reorg.md (drew2 fidelity-
confirmed). Rule-6 carve-out documented in CLAUDE.md. Dissolves the tok
phantom import; collapses the intra-frontend export sprawl. Byte-id
rebaseline (lex.X/parse.X/ww.X -> syntax.X); cs==ww held. The residual
syntax->wcc export surface (10 types) + the unqualified-ref question are
separate follow-ups (#72/#75).
Switch symbol mangling from the import leaf clause to the full dotted import path for directory packages; single-file imports keep package-clause mangling (isdir-gate: imported<=>directory-import). The root build unit's fn main stays bare, every other top-level decl mangles, closing #31's duplicate-main hazard by construction (#32). Both stages, byte-identical.
Single commit, not split: the bare rename (f244af3) is red on its own because it unmasks cross-module resolution gaps that do not reproduce pre-M1, so the fixes are intrinsic to making the rename correct. Included: wwstage fnret/fnparamslookupmod map import alias->path (#199b cross-module union-variant scrutinee resolved the wrong fn's union); cstage use_path prefers the referencing module's import for an ambiguous leaf alias (sha256 crypto.math vs strconv math). Tests table-driven: 989_m1mangle_run/_sym, 989_m1union_run (gate-visible per-arm exit codes + cs==ww byte-id).
wwstage's slotsize() shared its TY_STRUCT arm with TUPLE/ARRAY and
returned ti.slotsize — the SUM of the slot-padded field widths. For a
struct LOCAL that over-reserves the frame slot whenever a field is a
sub-8 nested composite: a nested inner{x:u8,y:u8} (size 2, slotsize 8)
pads its in-struct footprint, and the local inherits that pad. cstage
has no slotsize SSoT — it reserves the local at f->type->size, the
checker's NATURAL r.size (cmd/w6c/cgen.c). So on outer{a:u8,
p:inner{x:u8,y:u8}, z:i64} wwstage emitted frame $32 / struct-base
-24(BP) while cstage emitted $16 / -16(BP): a uniform -8 BP shift on
every field access. Both stages exit 0 (each self-consistent), so it is
runtime-invisible — but it is a cs!=ww .s divergence (rule 10) and a
latent byte-id gate-landmine the day such a struct enters the corpus.
Same dual-SSoT leak as #44 (field-OFFSET) / #55, one notion over:
struct-local-slot-SIZE.
Fix: split the TY_STRUCT arm out and return round8(ti.size). The TUPLE
arm (8B/elem slot, user ruling #60) and the ARRAY arm (element stride,
#48 [N]Alias 24B) keep ti.slotsize — those are deliberate, ruled
divergences and are untouched. The struct-local slot consumers
(cgendecl.ww letslotsize via cglet, cgenstmt.ww) all flow through this
arm; si.totsize (registerstruct → structabisize / global-emit) is a
separate consumer and is not this path.
CLASS-N corpus-neutral: every corpus struct local is 8-aligned, so
round8(ti.size) == slotsize for all of them and the w6c_ww/wwdump_ww
emission does not move (994 byte-id on 18 corpus inputs + 995 5-tool
self-rebuild both green post-fix). 989_structlocal_frame is the
FRAME-ABSOLUTE proof (w6c vs w6c_ww .s byte-diff; nested3 + tail_u32 +
flat control) — the .s twin of the runtime 989_nestfield_run, which
deliberately does not gate the frame and points here for it.
wwstage carried TWO struct-layout sources. registerstruct (cgenutil.ww)
recomputed each field's `fi.foff` via fieldsize — slot-padded, round-8 —
for the WRITE (construction / field store) path, while the READ path
(cgplaceaddr / dotbaseaddr) used the checker's natural `tfield.offset`.
They diverged iff a struct had a nested sub-8 composite field
(slotsize != size) plus a successor: ww wrote the successor at the
slot-padded offset and read it at the natural offset, mis-addressing its
own field. cstage has no structinfo and reads tfield directly, self-
consistently natural (cmd/w6c/cgen.c).
Fix: make `fi.foff` a VIEW of the checker's already-built natural layout.
Lock-step walk tstruct.list (AST N_TFIELD) and ti.fields (tfield) — both
head-first declared order, both skip non-TFIELD identically — and copy
foff = tf.offset, fsz = tf.type_.size. si.totsize keeps the slot-padded
stack-slot number (ti.slotsize, already 8-rounded at check.ww:2259).
fieldsize is no longer called here (its `*p OP=` scalar-width caller is
untouched). LOUD nil-guards on tstruct.type_ / tichase / a tfield walk
desync — all unreachable post-check, never silent. fi.tnode stays the
AST node (its node-keyed readers need it); repointing the ~60 fi.foff
readers to tfield is the out-of-scope (ii-b) follow-up.
This unifies ww's second source onto the value cstage already emits, so
cs==ww is preserved, not newly created (wwstage-cgen only; no cstage
edit). The shape is corpus-absent — ww uses both sources on its own
structs, so a divergent struct would have broken the bootstrap — hence
gate-blind; 989_nestfield_run is the proof (nested inner{x:u8,y:u8} in
outer{a:u8,p:inner[,z:i64]}, every field read back == written, dual-stage
cs==ww). It also makes 681 ragged_tail_12B genuinely correct: the
predecessor #71 already shrank the whole-struct copy to the source's
natural length, so packing mark at natural offset 12 no longer clobbers.
Passing buf[1:] of a [MAX]u8 as a call arg dropped the default-hi
length (the arg-push N_SLICE arms' fallback covered only named-alias
bases). Same type-table resolve at pushargsrev local+global. This
closes the def-dim dimension family by construction: every N_INTLIT-
keyed dim consumer (cgslice, cgdot, letemitsize, arg-push) now
carries the tichase().alen fallback — grep-proven, no consumer
remains. Fourth member surfaced by the family grep.
Widening a runtime f64 into a tagged slot pushed a stale AX as the
payload while the value sat in X0 — both stages shared the push bug
(float literals dodged it because TK_FLOAT loads AX too); the
divergent pop sides then produced different garbage. Spill the
payload from X0 (MOVSD) with the variant tag. Review item #49.
Both stages move in one commit: one emission contract; splitting
would leave the byte-id gates red between the halves.
The arg-drain loop checked node_isfloat before popping a widened
arg's GP words, so a float arg adjacent to a widened (tagged) arg
read the wrong stack slot: the f64 took the widened payload (#30), or
the float arm ate the tag word into X0 and the payload landed in DI
as the tag (#48). One missing branch, two manifestations — mirror
cstage's widen-first pop (cgen.c:9650-9665). Review items #30+#48
(fold reviewer-verified one-mechanism against the cstage twin).
Widening a global tagged union into a wider tagged slot copied
nothing of the box; treat the global ident as a tagged source and
copy the full box from g(SB) through the nested arm. Both-wrong pair:
cstage spills frame garbage as the box (filed task #44, residual
non-deterministic so the rows assert divergence only). The non-nested
SUBSET-widen shape remains open as task #49 (site comment at the
fall-through). Review item #51.
Widening a global struct into a tagged slot copied word0 only; copy
the full payload from g(SB). Both-wrong pair: cstage zero-fills the
payload (filed task #43); rows pin ww-runtime-correct with the
documented cstage residual. Review item #50.
Slicing a module-global str with default hi emitted nothing for the
bound; load the len word from g(SB)+8 (mirror the cstage twin).
Review item #47; dual-stage rows red-proven.
wwstage computed tagged sizes/tags off the raw variant list — size()
folded wrong constants (size((*u8|void)) 16 vs 8, (i32|never) 16 vs 4)
and duplicate variants got divergent tag numbering vs cstage, while
ww's own cgen layout folded nullable but its size() didn't. Make
tinfofornode's N_TTAGGED arm the normalization SSoT mirroring cstage
resolve_type (check.c:801-882): never-drop, duplicate dedup via
structural typeeq, single-variant collapse, nullable fold on the
normalized pair; astsize/astalign delegate, and voidvariantindex reads
the normalized ti.params (cgen.c:900-911) so construct/match/void tag
readers agree. Corpus-neutral (zero-move on all combineds);
989_tagnorm_run pins the folds dual-stage, red-proven. Review items
#1/#3; residual #45 filed (AST-keyed nullable gate at global emit).
A bare module-global unsigned operand got signed IDIV/SAR/Jcc — the
predicate's ident arm only consulted the local table, so globals fell
through to signed (review finding #25). Read the stamp for the global
arm; corpus emission is unmoved (no bootstrap code div/shift/cmps a
bare unsigned global). 989_gunsigned_run pins cs==ww (red 3/8
pre-fix).
A slice/str ELEMENT of an indexed expression passed as a call-arg
pushed one word instead of the 24B/16B header — the predicates had no
N_INDEX arm, so element-typed args fell to the scalar path (review
findings #45/#46). Read the element-type stamp; dual-stage rows in
989_idxarg_run pin cs==ww (red 2/8 on pre-fix binaries).
Collapse the localfindnode tnode read to the stamped n.type_ — a
zero-delta mechanic validation for the F7 predicate type-keying arc:
self-compile byte-id holds on all four bootstrap combineds, proving
n.type_ matches the local-table truth on every corpus local before
any behavior-changing conversion lands. The localfindnode gate stays
so a global-not-def ident still routes false.
A widening sub-union cast (return/let `v: inner` into a union whose member
is the nested `inner`) skipped the nested widen arm and emitted tag=0 (a
2nd-variant value returned the wrong payload). cstage loud-stops #35;
wwstage now matches: cgwidentaggedstorebp louds on the subset cast, and
cgreturn routes genuine-widening tagged returns (rhs TY_TAGGED, ru1!=fu1)
through the widener choke-point so the same loud fires. Same-type returns
stay on the proven passthrough. Faithful inner-tag->outer-index remap
deferred. test/wcc/835 S1 row.
An empty zero-length array diverged cs!=ww in asm (both ran correct=7):
a [0]int global emitted a spurious DATAW main.X(SB),"", and a [0]int local
reserved a $16 frame slot. cstage emits neither. wwstage-only, byte-id-only.
The global DATAW emit is now gated on sz > 0 (skips the empty array).
The local frame: localreserve dropped its sub-8 floor (if asz<8 asz=8) to
mirror cstage's localslot formula (frame+sz+7)&~7 -- but that floor was
MASKING slotsize(TY_VOID)=0 (a void local), which cstage defaults to 8B;
removing the floor alone collided the zero-size void slot with a spilled
param (a real miscompile -- 1132 self-compile hunks). So letslotsize now
returns 8 for a void local, while empty-struct / [0]-array stay genuine 0.
The frame formula is byte-id-neutral for every sz>=1 local (round8 already
>= 8); only true zero-size cases change.
cstage unchanged (w6c md5 unchanged). byte-id 990-997 8/8 (the full
self-compile is what caught the void-local class); test/wcc/820 un-carves
the #9 empty-[0] byte-id exclusion + adds void-local/local-[0] rows.
A let-global slice or str passed by value as an argument was silently
field-dropped by wwstage: pushargsrev's global branch had only the #150-A
struct arm, and wwstage's nodeisslice/nodeisstr are local-keyed (false for
a global), so a global slice/str ident fell to the scalar single-PUSHQ,
pushing one of the three header words {ptr,len,cap} -> the callee read
garbage for .len/.cap. cstage was already correct.
wwstage-only, caller-side only (slice/str params already received
correctly). A global slice/str arm in pushargsrev, type-keyed on
tichase(arg.type_).kind, with two arms byte-matching cstage's two distinct
sequences -- str via cgslicehdr (CX-base, cgen.c:1866), slice per-word
(BX-base, cgen.c:9124). Gate is isletvar-only (a def has no name(SB)
holder; cstage const-folds it -- def-str/slice-by-value is the residual
task #21). cstage unchanged (w6c md5 unchanged); byte-id 990-997 8/8,
no lib pin flips. test/wcc/829 table-driven, byte-id per type.
Passing a module-global struct by value -- let g: pt = pt{...}; take(g)
-- was silently miscompiled, mirror-opposite on the two stages. cstage's
by-value struct-arg arm hit localfind(g)->0 and read 2 words from the
frame (MOVQ (BP)), never main.g(SB) -> returned garbage. wwstage used the
correct main.g(SB) base but fell through to the scalar single-PUSHQ
default, pushing one word for a 2-word struct -> dropped a field.
Both stages now take the off==0 global branch: LEAQ main.NAME(SB) and copy
all struct-size/8 eightbytes (reusing the GAP-A.ptr/#231 global-base
predicate), converging to one byte-identical sequence. The local path
(off!=0) is unchanged; >16B aggregates (#271) already resolved globals.
Commit A of the cluster; the cstage-only inferred-global-type Sym-repoint
(every let g = ... module-global yields <nil> downstream) is Commit B
(#18). Slice/str global-by-value args have the same wwstage field-drop --
filed (#10 G-valglobal-arg; struct closed here). byte-id 990-997 8/8.
test/wcc/822 table-driven, byte-id per stage.
A tuple LITERAL with a declared-tagged element reached the cursor-fill
helper (cg_tuple_lit_to_cursor) through the generic cgexpr(N_TUPLE) arm
with no declared type, so the element was stored stamped-keyed at its
constructed scalar width rather than widened into the declared tagged box.
Both consumers ran silent and wrong on both stages (#263 gate-blind:
cs==ww byte-identical, both wrong — runtime is the only net).
#64 massign: N_MASSIGN derives a declared tuple type from the lvalue
binding types and threads it into cg_tuple_lit_to_cursor + the receive
loop (mirror of the #57 N_LET wire); a `_` target falls back to the rhs
literal element type for cursor stride.
#68 call-arg: the send is made param-aware (fill over the PARAM tuple) and
the restage guard graduates a declared-tagged element to a real widen
(reusing cg_widen_tagged_store); nested tuple/struct/array elements and
tagged elements with no param decl stay rule-7 loud. The matching
pop/drain is made param-aware too so push count == pop count: a
param-aware send pushes the box's N words, so the drain must pop N or the
SysV arg sequence skews. This is a push/pop balance requirement of the
send change, not a separate latent under-drain (the standalone trailing-
arg drain is already correct at HEAD).
Closed by construction: the only remaining cg_tuple_lit_to_cursor caller
passing NULL/nil is the generic cgexpr(N_TUPLE) arm, provably non-widening
(constructed type == governing type). The four widening consumers — LET,
RETURN, MASSIGN, call-arg — are all decl-wired. Whole-tuple single-ident
reassign from a tuple literal is rule-7 loud (task #49), not a silent
widening consumer, so the residual NULL arm stays non-widening.
Pin: 945_tuple_lit_declblind_run — massign / call-arg / `_`-control /
call-arg-drain / nested-tuple-ERR rows, each base-fail at abd97e6 and
post-pass with cs==ww byte-id.
io.empty (discard+EOF stream, ref/hare/io/empty.ha:4-17) — needed by getopt's
two-pass printusage width measurement. Diverges from Hare's `const empty: *stream`:
a `let _empty_vt` + `fn empty()` that wires the fn-ptr slots per call, because
const-init of a vtable struct with fn-ptr fields is blocked (#118, ruled accept).
Co-discovered while making empty() byte-identical across stages: three
wwstage-only cgen fixes (cstage was already correct; wwstage aligned down):
- #129 sretretsize: consult the same-module pointer-alias before structlookup's
any-module struct fallback (io.stream = *vtable was mis-sized as memio's 56B
struct -> spurious sret save).
- #129 callsretsize: swap curmod to the callee's module before sret-size
classification (cross-module callee context).
- #130 cgassign global-struct tagged-union field store: add the missing arm
(was a 1-word store) mirroring cstage cgen.c:4893-4912.
The three are inseparable from io.empty here — splitting them out leaves a
divergent-asm intermediate (993/995 red), so they ride one commit per the
one-class gate-repair carve-out (#133-expanded precedent). Regenerates the
embedded combined.ww; 989_lib_byteid pins bufio + fmt graduated to M_ID.
(cgenexpr.ww fix-3 inline comment cites the #129 cluster; narrow to #130 on
next touch to avoid a regen for a comment.)
cg_widen_tagged_store only handled a tuple LITERAL (N_TUPLE / cast-of-
N_TUPLE) widened into a tagged box; any addressable non-literal tuple
source -- IDENT var, INDEX tbl[i], DEREF *p -- hit the `else fatal`
("tuple-typed source shape unwired"). Both stages loud-identical
(honest, no silent miscompile). This blocked indexing a const tuple
table into a union (regex charclass_map[i] -> charset union).
Add an addressable-tuple-source arm, both stages (cgen.c +
cgenutil.ww twin). It resolves the source address via the cgplaceaddr
place-spine (covering ident/index/deref -- one mechanism, so the trio
is family-closed) and block-copies the tuple's type-table ->size bytes
into the box payload (after the 8B tag), then stamps the variant tag.
No re-slotting: a tuple's in-memory layout uses the same eslot strides
(str=24B header, *fn=8B, ...) as the box payload the literal loop
fills, so source-layout == dest-layout. The existing narrow-pack and
tag-unresolved guards stay as the honest boundary; CALL/sret tuple
sources (different receive, #68-kin) stay loud.
align-BOTH: both stages were loud (no runtime reference), and byte-id
is structurally blind to an identical-wrong emission -- so correctness
is proven by a RUNTIME read-back pin (944_nonlit_tuple_widen_run, per
shape: match-extract + assert str header + call the fn-ptr elem with
distinct fns so a stale pointer is caught). 936's old reject row
graduates to a run row. Both stages byte-identical (990-997 green).
A tuple containing a tagged-union element, used as a union member
(e.g. ((void|size),(void|size),size) | error), loud-stopped in the
cgen return-store: the tuple-in-union store walk had scalar/float/
str/slice element arms but no TY_TAGGED-element arm. A PLAIN
tuple-in-union already worked -- the blocker was the tagged element.
Add the recursive two-level widen arm at both stages
(cg_widen_tagged_store / cgwidentaggedstorebp): for each tagged
element, re-enter the tagged-box store (inner tag@slot+0,
payload@slot+8) at the element's tuple-payload offset, then stamp the
outer tuple tag. Slot strides come from the type table
(roundup8(eu->size)) -- the checker already sizes the shape correctly
(tuple->size measured 40, union box 48; check.c:715-720). The
recursion descends a finite type tree (a tagged element is never a
tuple literal, so it can't re-enter the tuple arm); unsupported
deeper nesting still louds via the existing size/tag guards.
Both stages get the same arm -> byte-id (990-997 green; additive,
bootstrap-neutral). cstage runs the full b1c shape
(construct+return+match-extract) as the runtime reference; wwstage's
store rides on byte-id until gap-B. gap-B (wwstage checker
match-acceptance of the tuple-with-tagged case pattern) is a separate
commit -- wwstage still louds the match honestly at the checker.
Pin 944_tuple_tagged_union_run.
wwstage pushargsrev treated a narrower tagged-union argument widened into
a wider tagged param slot as a concrete variant: taggedvariantindex<0
clamped the tag to 0 and pushed word0 only (deref/index/dot silent-wrong;
ident ran correct only by prefix-union tag-index luck). cstage is correct
(cg_widen_tagged_push routes src_is_tagged unconditionally); align ww UP.
Three arms in cgenutil.ww, all mirroring cstage cgen.c:
- slot-gate the ident aistagged short-circuit so a slot-differ tagged
ident falls to the widen path instead of the raw 2-word push;
- route a tagged source in the widensz>0 arm through @tagscr +
cgwidentaggedstore + push high->low (cgen.c cg_widen_tagged_push);
- cgwidentaggedstore cursor arm (<=32B INDEX/DOT) spills by source
width, zero-pads, and tag-remaps (cgen.c 2698-2714) — was dst-slot
spill of stale high regs with no pad and no remap.
Same-slot tagged->tagged is byte-id-neutral by construction (empty pad +
identity remap). cstage untouched; 4 legs x {aligned, misaligned-tag}
converge ww->cs byte-identical. Pin 944_tagged_widen_arg_run.
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.
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
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.
A nominally-unrelated, structurally-equal NAMED source into a NAMED
variant (kb95_unrel: ta/tb same-layout structs, src ta -> (void|tb))
was LIVE both-wrong-identical byte-id silent: both checkers accept,
both cgens tagged 0. After c1's chain arm finds no shared chain
node, match the variant whose CHASED type type_eq's/typeeq's the
source's chased bottom — chased type EQUALITY only, no
type_is_assignable scalar import, no int widening (ken's binding
scalar warning). Same NAMED-source branch, both stages
(cg_tag_for_variant / flatvariantidxt), forced fuse.
Correctness reference, cite 1 — harec tagged_select_subtype P2+P3
(ref/harec/src/types.c:702-739), verbatim:
if (t->id == subtype->id) {
return t;
}
if (type_is_assignable(ctx, t, subtype)) {
selected = t;
++nassign;
}
...
if (nassign == 1) {
return selected;
}
return NULL;
with type_is_assignable's non-tagged path dealiasing both sides and
accepting composites only via interned pointer equality
(types.c:988-1002), verbatim:
if (type_dealias(ctx, to)->storage != STORAGE_TAGGED) {
to = type_dealias(ctx, to);
from = type_dealias(ctx, from);
}
...
if (to == from && to->storage != STORAGE_VOID) {
return true;
}
Cite 2 — type_hash interns bare composites STRUCTURALLY (banked as
types.c:72-81; verified in the vendored copy at types.c:444 +
struct/union arm :514-525), verbatim:
case STORAGE_UNION:
hash = fnv1a_size(hash, type->struct_union.packed);
for (const struct struct_field *field = type->struct_union.fields;
field; field = field->next) {
if (field->name) {
hash = fnv1a_s(hash, field->name);
}
hash = fnv1a_u32(hash, type_hash(field->type));
hash = fnv1a_size(hash, field->offset);
}
— no decl ident in the hash, so harec's two decls dealias to ONE
interned node and `to == from` holds: acceptance is DEFINITIONAL
under interning, not an arm whose text could be misread. Our store
does not intern; chased type equality is the non-interned rendering
of the same rule.
Honest divergence (the >=2-structural-match hard-error STAYS): under
harec's interning two structurally-identical variants are ONE type —
a union cannot contain it twice — so the ambiguity case is
unrepresentable there; our hard-error (twin texts, shared tail
"source structurally matches >=2 variants — ambiguous without
nominal layout (#95)") is the correct nominal-lossy-model rendering,
not a harec deviation.
Pin table: unrel_struct row added (kb95_unrel graduates ok/1-ok/1 ->
0/0, byte-id held) — suite now 48/48. All c1 rows unmoved.
Invariants: 163-row dissolution matrix at tip — same 3 family
graduations as c1, ZERO new movers; five mains cs-vs-ww byte-id OK;
make all 0; sizelint 0; peellint 0 (no new peel sites — the
structural leg reads only chased ends); all 944 suites + 808 green.
w6c_ww/wwdump_ww main.combined.ww regen'd.
A NAMED struct source that was not pointer-identical to a NAMED
variant fell through every pass of cg_tag_for_variant (cmd/w6c/
cgen.c) / flatvariantidxt (selfhost/cmd/wcc/cgenutil.ww) and the
widen stored tag 0 — both stages, byte-identical, gate-blind: wrong
tag on VALID code at any alias depth, in both chain directions
(.ai/ken-95-oracle.md §2: kb5_v2s1i, kb95_2lvl_i, kb95_deep_src,
kb95_deep_var all both-wrong-identical at base).
New pass 1b, identical both stages (the same route — forced fuse):
after pass-1 exact (unchanged, FIRST — the (str|linerr) protection,
harec's P1 short-circuit), a NAMED source matches the variant whose
NAMED chain shares a pointer-identical node with the source's chain
(an alias IS-A its base through the chain). Two linear NAMED chains
intersect iff they share their chased bottom node (ken §1), so the
walk is implemented as pointer identity of the chased ends through
type_chase_named/tichase — the blessed chase choke-point. NO raw
.under/->under hops were added, so the anticipated `peel-ok: nominal
chain walk (#95)` annotations are unnecessary and the peellint
whitelist is UNCHANGED (continues the B6/B7 fold-peels-into-chase
arc; peellint green).
Variants are counted UNGATED (bare prims are type-table singletons,
so a bare variant node can BE the source's chased bottom): the >=2
guard stays equivalent to harec's nassign>=2 -> NULL
(ref/harec/src/types.c:734-738, tagged_select_subtype P2/P3). >=2
chain hits hard-error with twin texts (prefix convention, shared
tail "source alias chain reaches >=2 variants — ambiguous without
nominal layout (#95)") — drew's ambiguity proviso extended to the
chained set; was a SILENT member-0 tag. Pass-2 bare-source fallback
unchanged. Chased type EQUALITY only — no type_is_assignable scalar
import, no int widening (ken's binding scalar warning).
Pin table (new suite test/wcc/944_variant_chain_b95_run.c, 45
checks, Makefile-wired):
GRADUATIONS exit 1->0 both stages: chain_1lvl_i (kb5_v2s1i
HEADLINE, byte-id held), chain_2lvl_i, chain_deep_src,
chain_deep_var (byte-id held), chain_call_bound81 (kb5_v2s1),
chain_call2_bound81 (kb4_v2_struct2, #95's original) — the two
CALL-src rows waive byte-id, pre-existing #81 zero-fill asm noise
(NO at base too).
NEW LOUD: chain_amb_loud (kb95_amb) — silent tag 0 -> hard-error
both stages.
MUST-NOT-MOVE held: chain_amb_srcA/B (pass-1 precedence),
nom_str/nom_err (#218 nominal regression pin), exact_ctl
(kb5_v2sE2), bare_ctl/bare_2lvl/bare_ambig/bare_ambig2 (pass-2
controls), callret_bound277 (kb5_v2sE #277 cells unchanged,
dual-cell pin).
Invariants: ken's 163-row dissolution matrix rerun — exactly 3
movers, all family graduations (v2s1i/v2s1/v2_struct2 1->0), zero
non-family movers, detectors unmoved. Five mains cs-vs-ww byte-id
OK (ww/w6c/w6a/w6l/wwdump). make all 0; sizelint 0; peellint 0; all
944 suites + 808 green. w6c_ww/wwdump_ww main.combined.ww regen'd
(cgenutil.ww embeds).
The last four raw `->under` reads outside the whitelist were the
static-DATA emitters' ELEMENT-type single peels (the outer type already
chased): emit_array_lit_bytes:14356, emit_strarray_data:14574,
emit_slice_data:14788, let_pre_intern:15088 -> type_chase_named.
:15088 is the :14574 row's label-order leg and must flip in the same
commit or _S_ labels intern in emit order, not decl order (the in-tree
comment at the site); the strarr row's byte-id is the coupling proof.
Behavior moves (ken B7 first-position oracle + impl pre-state, all
pre-observed at 05f7af7):
- [N]alias-struct + [N]alias-str globals graduate cs link-ERR
("undefined reference") -> 0/0 BYTE-ID (cs emits ww's DATAW).
- zero-consumer latent silence closed: a never-referenced
2-level-elem-alias global silently lacked DATA (no reference, no
link error); now emits, pinned by the byte-id cell.
- []alias-str diagnostic routing: the alias escaped the 3-way
slice-of-{str,slice,tagged} fatal onto the downstream "not a
foldable constant" text — now the intended 3-way text (== control).
- []alias-tagged DESIGNED NARROWING: the alias dodged the 3-way fatal
ENTIRELY — cs silently accepted + RAN WRONG for reachable consumer
shapes (review-verified at base: a len+payload-read probe exits 1;
the len-only row was luck-correct). Now loud with the 3-way text;
widen what the gate SEES, never what it ACCEPTS (B6-c2 precedent).
- kb7_slc/slc0 scalar legs byte-NEUTRAL (the synthesized-array
choke-point already handled them); full kb corpus sweep: movers are
exactly the two graduation shapes, nothing else.
tools/peellint (sizelint clone, dep of test/test-unit): character-scan
strips comments and string/char literals, then matches the under-token
accessor-spelling-wide — `->under`/`.under` in C (deref-dot is the
same peel), `.under` in ww, optional whitespace after the operator,
and the line-split continuation (operator at EOL, `under` next line).
Scope cmd/wcc + cmd/w6c + selfhost/cmd/wcc + lib/ww (lib/ww/typ.ww
ruled IN — it is type.c's ww mirror, the accessor layer itself);
`peel-ok`/`peellint-ok` annotations exempt a 10-line window. Green at
this tip = zero unwhitelisted raw peels survive; the gate lands in the
commit that deletes the last raw read (the-funnel-completing-commit-
carries-the-gate; sizelint precedent). Whitelist, 27 entries:
cmd/wcc/type.c :78 :141 construction, :162 chase body,
:180 :193 :214 recursive chase
cmd/wcc/check.c :102 :2572 resolve-state probes, :2586 construction
cmd/w6c/cgen.c :731 probe-cleared scan peel (B5-c1),
:813/:814 :834/:835 peel-ok #218 variant-match
lib/ww/typ.ww :316 construction, :374 :385 :410 :437 :447 :463
:475 :488 :514 recursive chase
selfhost/cmd/wcc/cgenutil.ww :1302 chase body (tichase),
:2759 probe-cleared peel
selfhost/cmd/wcc/check.ww :1815 construction (peellint-ok)
Negative validation wired into 944_peellint_gate (B4 precedent):
re-introduced raw peel (C and ww spellings) REDS the lint; corrupted
annotation (peel-okk-…, token-bounded matcher) REDS the lint; the
check.ww:3683 "io.underread" prose, a code read of a longer field, and
comment-quoted tokens are pinned green regression rows; real tree must
lint clean. 944_alias_emit_b7_run pins all four emit paths
table-driven (14 rows / 36 checks) incl. ken's ww observation cells
(ww checker rejects slice-literal globals, "let: not assignable" —
unmoved; plain []str louds at ww's own emitslicedata 3-way, pinned by
the shared needle).
REVIEW AMENDMENT (reviewer-B7, fix-what-you-find): the frozen tip's
regex matcher passed five compiling evasion spellings green — `t ->
under` spacing, `t->`/EOL + `under` next-line (both stages; ww parses
`t.`/EOL too), C deref-dot `(*t).under`, ww `t. under`, and a string
literal containing a block-comment opener that blinded the regex
comment-strip for the rest of the file. The matcher is now a
character scan (comments + string/char literals stripped before
matching) with the widened token rule above; all six spellings are
pinned RED rows in 944_peellint_gate (checks 10 -> 16). The 10-line
annotation window stays as designed (a peel within an annotation's
window is exempt by construction — the window IS the exemption
mechanism). Lint + test bytes only; zero compiler-source bytes moved
in review.
What this does NOT close, said out loud (f2-ruling): a consumer that
never spells `under` at all — a switch on t->kind that simply never
peels — has no token for the lint to see. The accessor+lint closes the
WRONG-PEEL class (single-peel where chase was needed) by construction;
the NO-PEEL class is closed only at sites where classification routes
through the internalized chasing helpers, and contained elsewhere by
the acceptance-commit-carries-tripwires doctrine, which stays standing
for every future acceptance widening. The gate does not make alias
bugs impossible; it makes the four-times-burned shape unwritable.
Rule-11 note: forced fuse — the four conversions ARE the last raw-read
deletions; peellint cannot be green one commit earlier (consumer-graph
-forces-the-fuse precedent, #61).
Invariants: cs asm byte-NEUTRAL on the whole bootstrap corpus (five
mains + smoke, base-input pre==post); five mains cs==ww byte-id at
tip; _ww binary quartet bit-identical to the W2 baseline (ww changes
are comment-only annotation bytes — codegen-inert, proven by the md5
hold); w6c_ww+wwdump main.combined.ww regen'd via make, idempotent;
989 lib ratchet zero flips (31 byte-id / 9 pinned-divergent / 3
pinned-wwreject across 43 units); sizelint 0; peellint 0;
make test-unit "all 294 tests passed" (292 + the two new suites).
The ww twin of B5-c1's :3195 cg_structlit_fill chase — closes the
R4-blessed one-train acceptance divergence (cs-loud / ww-SILENT-WRONG
on alias-element array fills, ken FLAG-3 / kb5_fill2). The #249 fill
arm's loud-gate element classification was alias-blind on two of four
keys: a raw N_TSLICE kind test and a structlookup leaf-name probe —
`type el = el0;` matched neither, bypassed the gate, and fell to the
scalar tail (one word per element, silent exit 1, gate-blind #263
class). Now: issliceel via isslicetype (stamped tinfo, the str/tagged
keys' existing route) and isstructel via tichase + TY_STRUCT kind —
the tnodeisagg discipline the neighboring #49 arm already uses, twin
of cstage type_chase_named at cgen.c:3179. NO new helper.
Loudness-alignment only, zero acceptance movement elsewhere: probe
matrix holds direct str/slice/struct/tagged elem LOUD (twin texts),
int/alias-int/f64 elem fills build + run 0/0 byte-id, 1- and 2-level
alias struct/slice/tagged elems now LOUD both stages.
ww-only: cs side untouched (zero C bytes; w6c up-to-date across the
edit). B5 detector rows hold post-B5 expected state (targ/tmem/wpush/
null/succ/f32p/def93 all 0/0 byte-id). 989 ratchet ZERO flips
(31 ID / 9 pinned-divergent / 3 pinned-wwreject). combined.ww regen'd
for both embedded targets (w6c_ww, wwdump).
944_alias_cgen_b5_run: fill2 graduates the dual-cell K_CSERR_WWRUN
pin to the K_BUILDERR LOUD-HOLD pair (fill0-class, shared experr
tail); fill0 control holds. Check count holds at 84/84 — the kind
flip is cell-count-neutral (2 driver cells either side, no byte-id
cell). All 944-family suites green; sizelint 0; test-unit all 291
passed. Closes#100; the R4 divergence window closes with it.
F2a batch-4 c2. Site: cgenutil.ww flatvariantidxt pass-2 (was :2895
at 74195ac, :2903 at 4adf914 post-batch-3) + cs twin cg_tag_for_variant
(cmd/w6c/cgen.c:920-933).
The structural fallback matched a bare source against a NAMED variant
by peeling exactly ONE level (pu.under compare) — a 2-level-alias
variant (type a=*X; type b=a) missed every pass and the widen
defaulted to tag 0, SILENT (the legacy-#17-comment class; that
comment's "task #17" label is retired here — current task #17 is the
unrelated arrlit item).
cs-twin probe DECIDED THE FUSE (spec obligation): v2_alias2 (bare
*i64 into 2-level ptr-alias variant) ran exit 1/1 BOTH-WRONG-IDENTICAL
byte-id pre-fix — cs has the identical single peel, so both stages fix
in this commit (NOT ww-only align-up). NOTE: the cs BINARY is NOT
frozen this train — this commit legitimately moves cstage codegen;
movers must stay bounded to the c2 family (verdict-sweep obligation).
Fix: typeeq/type_eq against tichase/type_chase_named of the variant;
the TY_NAMED gate keeps bare variants in pass-1's exact domain; drew's
>=2-candidate hard-error now guards the CHASED match set (v2_ambig
pins it HOLDING; v2_ambig2 pins the RATIFIED acceptance NARROWING —
2-level twins flip build-accepted-silent-mis-tag -> hard-error BOTH
stages, FLAG-P1/kb4_v2_ambig2). Not nominal-sensitive beyond the
documented proviso — the chase only deepens the structural compare;
nominal choice among >=2 candidates still hard-errors (#209/#211 hold
condition does not trip).
ROUTE-TRACE (rob's §3 ruling) — outcome (b): post-c2 the c4_bool2
shape (2-level bool alias variant, concrete-bool source on ww) runs
0/0 fully BYTE-ID — ww's concrete bool (the #90 stamp divergence,
still open) now reaches the variant through the chased structural
fallback and converges with cs's untyped-funnel route on the same
tag. The graduation therefore belongs to THIS commit: 944
untyped_bool2lvl_bound90 flips K_RUN_CS -> K_RUN here; c5 (#90 stamp
flip) pins its own acceptance rows.
Pin rows (944, all OBSERVED at the c1 base):
v2_ctrl bare *T into 1-level NAMED-*T (io vtable shape)
pre 0/0 byte-id -> post HOLDS (the #15 consumer);
v2_alias2 bare *T into 2-level alias variant
pre cs1/ww1 byte-id (both-wrong) -> post 0/0 byte-id;
v2_struct2 bare anonymous-let struct into 2-level alias variant
pre cs1/ww1 -> post 0/0 runtime; byte-id SKIPPED via new
K_RUN_NOID row kind: asm diverges on 3 PRE-EXISTING cglet
zero-fill lines (cs XORQ+2 stores, ww none — #81 class,
runtime-correct both, orthogonal to the tag; observed
identical pre/post). Flip to K_RUN when that closes. The
adjacent NAMED-source shape is task #95 (ken b4-oracle),
OUT of this set;
v2_ambig bare source matching >=2 NAMED variants (1-level twins)
-> hard-error HOLDS both stages (K_BUILDERR, diag pinned);
v2_ambig2 2-LEVEL twin variants — the RATIFIED narrowing pin:
pre build-ACCEPTED both stages (chase-less fallback
matched neither twin; silent mis-tag, byte-identical)
-> post HARD-ERROR both stages (K_BUILDERR, diag pinned).
Corpus: five-mains NEUTRAL vs the c1 build on identical inputs (both
stages — the 2-level variant shape is zero-in-corpus, as the old
comment predicted); cs==ww byte-id holds; 944 181/181.
combined.ww regens ride along.
The N_STRUCTLIT arm still did bare structlookup on the literal's type
name — an alias-named struct LITERAL widened into a union (`type ali =
base; let v: (void|ali) = ali{...}`) missed the registered structinfo
and fell to the scalar widen arm: word0-only/dropped payload, cs 0 /
ww EXIT 1, byte-id NO (reviewer-B2 find, pre-existing at 486f7f8's
parent). Exactly the class batch-2 c2 closed for the N_IDENT-local arm
of the SAME function; same funnel fix — route through structlookupchain
(cgenutil.ww:1758) and return si.sname so every consumer's re-lookup
hits the REGISTERED name.
One pre-authorized rider: structlookupchain's entry gate accepted only
N_TNAME, but a struct literal's type ref parses as N_IDENT (expression
position, lib/ww/parse/expr.ww builds s.lhs as N_IDENT — OBSERVED).
Widened to `N_TNAME || N_IDENT`; the entry-kind predicate is spelled
twice (the early-return and the chain loop's first iteration — the
loop reassigns cur to aliaslookup results, which are always N_TNAME,
so later iterations are untouched). Consumer census, all 10 call
sites: cgenutil:3187(+this arm; post-commit position), cgenexpr:2927/3006/8775/9011,
cgenstmt:3033 guard on N_TNAME explicitly before calling;
cgenexpr:5756 (etnode = type-spec .lhs), cgenstmt:1978 (arrtn.lhs or
the synthesized #79 N_TNAME), cgenstmt:2695 (cglet n.lhs declared
type spec) pass parsetype products only — parsetype builds zero
N_IDENT nodes (lib/ww/parse/parse.ww). No existing caller can pass
N_IDENT: the widening is strictly accepting-more; existing callers
unmoved. NO second inline chase in the arm (close-by-construction).
Base spellings byte-id: structlookup hits at the chain entry and
si.sname == the literal's own name — same string out, same asm.
Pin: 944_alias_accept_run union_slit_{base_ctl,alias,alias2,order} —
base control holds 0/0; 1/2-level alias + permuted decl order graduate
cs0/ww1-byte-id-NO -> 0/0 byte-id. #63 PROBED post-c3 per the
enrollment rule: does NOT green at this site (8B alias struct-lit
let-init still ww silent-zero-fill exit 1; 16B still loud "aggregate
init from unhandled rhs shape") — its miss is the cgenstmt let-init
dispatch, not rhsstructpayload; documented on task #63, left red, no
row enrolled.
Light gates: test-unit 290 green; sizelint 0; 989 ratchet zero flips;
five-mains NEUTRAL vs master-74195ac scratch on identical inputs +
cs==ww on all five. combined.ww regens ride along (#110).