The wwstage nodeisstr/nodeisslice recognizers were name-keyed for
every non-local shape: an indirect fn-pointer callee ((*f)() — the
errnotest #59.5 divergence, streq receiving a shifted register file)
and module-global let/const idents (path.sepstr — union-widen pushes
zero-filled len/cap) both fell to false while cstage keys off the
checker stamp unconditionally. Both recognizers now fall back to the
stamped n.type_; push and pop sites share them, so the drain stays
balanced by construction. Graduates the #59.5 errnotest pin.
x.slicefield[:] / x.strfield[2:] emitted MOVQ $0 for the omitted hi
bound on BOTH stages (N_IDENT-gated dispatch; symmetric, so byte
identity never caught it) in all four sites: cgexpr N_SLICE + pushargs
(cstage), cgslice + pushargsrev (wwstage). The new arm re-evaluates
the pure field read for its {ptr,len,cap} header and takes .len,
covering local, viaptr, dot-chain, (*p), arr[i], and global inners.
Call inners still loud-reject upstream. Sibling of the #252/#257
array-field arms.
let x: []T = g.buf on a module-global struct dispatched only str
fields to the 3-word header load; a []T field fell to the scalar
tail (ptr word only), so len/cap read stale registers and the shape
was byteid-divergent against cstage's TY_STR||TY_SLICE arm (#263).
The N_TFN arm stamped only size and return, so every fn tinfo carried
a nil param chain and typeeq judged any two same-return fn types
equal: tagged-union dedup collapsed (*fn(A) T | *fn(B) T) to a bare
8-byte pointer and match read the pointer word as a tag, falling
through every arm for a real second-variant value. Build the tparam
chain like cstage's N_TFN resolve (bare ... sets the FFI variadic
flag; a Hare T... param wraps to []T with a per-param variadic bit
that typeeq now compares, mirroring cstage type_eq). Graduates the
four r76_typeeq_fn pins; the DATABYTEID_DIVERGED ledger is empty.
cstage types the binding ty_u8; the wwstage single-binder arm peeled
the element only for slice/array iterables, so a str scrutinee fell to
the N_FORRANGE fallback decl and exprtype stamped the binding str.
The stamp-keyed call-arg marshal then pushed the 3-word str ABI for a
1-word scalar, and an uncast compare on the binding was falsely
rejected. Route str through the same synthetic-N_LET binder with a u8
element. Graduates r940_str_forrange_arg.
cgwidentaggedstorebp's struct-ident arms (local and module-global)
counted copy bytes from structinfo.totsize, the round-8 slot width, so
a struct whose natural size is not a multiple of 8 took the MOVQ word
loop and copied the source slot's dirty pad bytes over the zero-fill
the arm itself just established; the width-true tail emitter beneath
was dead code. cstage keys the same copy on su->size. Route both arms
through copysrcnatsize (the stamped-tinfo natural size, #71).
Graduates r71_tagged_return_scratch_mixed.
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.
The plain `*p = v` arm's storeop map handled 1- and 4-byte pointees
but let a 2-byte one fall to MOVQ, an 8-byte over-store that clobbers
six adjacent bytes (masked in the pinned fixture by the lone frame
slot; loud with `&a[0]` of a [4]i16). cstage fldstoreop maps 2 to
MOVW. Graduates r660_i16_out_param_negative.
Every probe's assertion is owned by a current gate: the compile and
link probes by make all and the bootstrap fixed point; build/run and
cs/ww byte identity by the fixture corpus, test-data-byteid, and
989_lib_byteid; wwstage driver and toolchain parity by 993/995;
checker-diagnostic parity by the corpus' both-stage //ww:error rows.
The wwdump -t/-a dump-parity probes gated the frontend port's
convergence, which the compiler-output identity gates now own end to
end; carrier ran green at retirement.
What was still uniquely alive migrates: smoke.ww becomes corpus
fixture selfhost_smoke (upgraded from a cstage-only build to both
frontends, byte-identical, exit 42 on both toolchains; corpus pins
move to 1,225/763/2,450 with the new identity hash in the same
commit), and sym_link.ww's scope/sym behavior rows become in-language
lib/ww/syntax/symtest.ww under LIBRARY_TESTS. uses.ww (parser-stub-era
-a fixture) and the already-orphaned tagged_ptr_ret.ww/trypromote.ww
retire with the probe corpus. Bootstrap native gates drop to six;
frontend numeric-sync comments now cite the rule-6 mirror instead of
the retired diff probe.
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).
Scratch acquisition is one bare mkdir that refuses a pre-existing
.sepwork; the path reaches cleanup only after acquisition succeeds.
ww build and ww test -o keep the tree for the caller; ww run and
no-output ww test build under an owned /tmp/ww_{run,test}_<pid> dir
and remove it on every result, escalating rc on cleanup failure.
The content-keyed package cache is deleted from both stages; -S stops
after per-package .s emission; --sep is gone and unknown flags are
rejected; directory ww test delegates to the wwtest coordinator; the
wwstage driver routes every subprocess through os.exec.
The wwstage Makefile recipes move off the persistent <tool>_ww.d
build-in-place shape to a per-invocation mktemp workspace: the old
recipes relied on the retired pre-wipe semantics and any rebuild
would trip the new refusal on their leftover main.sepwork.
The three 989_pkgcache_* carriers lose their subject with the cache
and go with it; the rest of the wave's carrier adaptation follows in
its own commits.
A module-global base has no frame slot; treating its local-lookup miss
as offset zero wrote .ptr/.len/.cap at the caller return address. LEAQ
the symbol like the struct-field global arms do. Both stages.
The a[i] = src copy loop enumerated ident/field/deref sources; an
N_INDEX rhs fell to the scalar tail and truncated the element. Route
it through the generic place-address funnel. Both stages.
The direct-field assignment arms enumerate CALL, STRUCTLIT, and local
IDENT producers; an addressable N_DOT/N_INDEX/deref rhs fell through to
the scalar tail, so a 16-byte struct field copied only its first word.
Resolve both places through the existing address funnels and use the
tail-aware aggregate copier. Both stages.
A str `def` has no name(SB) header; the streq push arms read a frame
slot that does not exist. Load the literal through cgexpr (AX=ptr,
BX=len) instead. Both stages.
wwstage exprtype's N_CALL fn-value arm peeled ALL pointer levels before the
TY_FN gate, so a deref-less `pf(21)` where pf:**fn type-checked and lowered --
then segfaulted at runtime (a silent miscompile). cstage peels exactly one
level and loud-rejects (the C6a discipline, check.c:1947).
Align wwstage DOWN: peel one level (loop -> if); a remaining non-TY_FN callee
hits a loud "calling non-function" reject mirroring cstage's message. Both
stages now reject the exotic deref-less `**fn`/`***fn` shape; the legitimate
`(*pf)(21)` and one-level deref-less `f(21)` (f:*fn) still compile + run.
Multi-level autoderef is a separate deferred FEATURE, not a miscompile to lower
(rule 7/10 -- align the richer stage down to the leaner, no value ships).
ww-only change (cstage is the correct oracle); a reject emits no asm, so the
byte-id baselines and LANGBYTEID floor are unchanged.
Pins: cfail test/wcc/data/fnptr_pp_derefless_reject (both stages reject,
reddens-on-revert -- the silent miscompile resurfaces if the fix is reverted) +
test/lang/fnptr_derefless_call_test (positive guard (*pf)(21)==42 and one-level
f(21), value-asserted + byte-id, so the fix does not over-reject the legitimate
one-level autoderef).
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 field LAYOUT (offset + field type) by bare-leaf
name (structlookupchain / structlookup / letvarstructinfo) at nine field
READ / addr-of / let-copy receiver arms whose base is a *struct pointer, a
value struct, or a module global. Under a cross-module same-leaf collision
(two modules each exporting a `pair`, 16B vs 24B) the bare-leaf lookup
first-matches the wrong-size struct -> the field is read / addressed / copied
at the wrong offset with the wrong width. cstage type-keys off the stamped
node.type_ (type_chase_named(base->type)->fields) and is correct; this aligns
wwstage UP to it (ww-only change).
Route the nine arms off the stamped receiver tinfo: R1/R2 *struct reads via a
new shared choke-point cgptrfieldloadtf (the tinfo twin of cgptrfieldload);
A1/A2/A3 addr-of and W3 scalar global-store via tichase(recv.type_)->fields;
C1/W4a copy/size via structabisizetn(tichase(.type_)). Mirrors #21 (5ae6e34);
the *struct arms peel the pointee with tichase(.type_).sub, the faithful twin
of cstage type_chase_named(bu->sub).
The global value-struct arms (R2/R3/A3/W3/W4a) are converted-for-construction:
a global struct's type is always explicitly qualified, so the bare leaf already
resolved correctly and they cannot be reddened -- byte-id (cs.s==ww.s) is their
net, not a value pin. Labelled so the absent reddening pin is explained, not
silent.
Commit 1 of a 2-commit arc (RULING R2 / Opt-2, .ai/ken-31-spec.md): closes the
field-LAYOUT read/copy/addr surface. Commit 2 converts the W1/W2/W5/W4b store
loops to a tinfo-native fill (cgstructlitfilltn + sretretsizetn) and closes the
in-loop nested sub-arms by construction. Part of the #224 name-keyed-cgen
cluster retirement.
Pin: test/wcc/797 value-asserts R1 ptr-read / C1 let-copy / A1/A2 addr, each
reddening under independent per-arm revert (a ratchet proves nothing for this
silent-capable class).
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).
A stack array literal returned into a tagged-union slice success variant (fn mk() ([]i32|e) = { return [10,20,30]; }) slipped past reject_arrlit_borrow — it bailed when the dst was not TY_SLICE, but a union dst is TY_TAGGED — so cgen built an all-zeros slice header: a silent both-stage miscompile (and the .ptr would dangle anyway, no outliving backing). Extend the reject to chase a TY_TAGGED dst to its slice success variant, then apply the existing reject; this is the #25/#31 treatment seen through the union, and matches Hare (rule-9). Reached by all acceptance sites (return/assign/call-arg) so the class closes by construction. Both stages converge on an identical accept/reject decision (an array literal assignable to a union is assignable to a slice or array variant; neither stage can accept it). Full support — promoting the literal to an outliving backing — is the separate #33 arc. Compile-error fixture + a positive over-reach guard (a real slice into the union still compiles). No asm emitted by a reject, so byte-id is unchanged (no floor ratchet).
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.
The in-cap aggregate-receive materialise emitted a single narrow tail MOV that fell to MOVB for a 3/5/6/7-byte sub-8 tail, storing one byte while the scratch->dest copy read the full tail from uninitialised scratch — silently dropping members at the C2c whole-element arm (arr[i]=mk()) and loud-stopping at the #11 field arm. The scratch slot is ceil-8 padded (local_alloc/localadd round to 8) and the copy reads only tsz bytes, so flipping the tail default MOVB->MOVQ stores the full register harmlessly into the slot's own pad (in-bounds for in-cap <=24B); 1/2/4-byte tails stay byte-identical. Both stages symmetric. Removes the now-redundant #11 sub-8-tail loud-stop (keeps the float #165 and over-cap #234 loud-stops). The same narrow-tail materialise recurs at 6 other cstage sites (task #14).
Retires the obsolete idx_dot_aggret_subtail_loud //ww:error fixture (both stages now compile the case) and converts it to a positive cstage run-test; the struct-field shape is byte-id-divergent only via the pre-existing #9 frame-size bug, so the value pin uses array-field shapes. Value-asserting, reddens under each stage's independent revert.
The N_TRYUNW/N_TRYPROP success shuffle materialized {ptr,len,cap} into {AX,BX,CX} only when the success variant was a str; a slice success got only MOVQ DX,AX (ptr), leaving every slice-unwrap consumer (call-arg push, let-receive store, ident-source) reading junk .len/.cap — silent on BOTH stages (byte-id blind, cstage not the oracle). Widen the success gate to type_isstr||type_isslice (cstage) / typeisstr||typeisslice (wwstage) at all four shuffle sites; str and slice share the identical 24B header shuffle. Stays str||slice-specific — a struct success variant uses a different {AX,DX,CX} ABI (task #12). Value-asserting pin (len!=cap, poison-decoy) reddens under each stage's independent revert.
Follows #6 Mech A (Fix-R); order forced (C1 first or the slice call-arg push reddens byte-id).
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.
The arr[i].f=src legacy assign block enumerated scalar field-type arms then fell to a 1-word scalar default, so a non-call aggregate source (ident/dot/index) cgexpr'd only its first word into AX and stored one eightbyte — silent on BOTH stages (byte-id blind). The non-indexed bases (local/deref/chained/global) reach the general assign resolver's canonical aggargsrcaddr+aggcopy; the indexed arm short-circuited before it. Route the indexed base through the block's own proven &arr[i] spine into the same aggargsrcaddr+aggcopy emitters (DRY — no third copy), dual-site symmetric. Unlike #11's in-cap arm, the source is a memory address so aggcopy is a pure memcpy: float bits and the sub-8 tail transport verbatim, no loud-stop needed. Did not fall through to the general resolver because its cgplaceaddr N_INDEX arm rejects a *[N]S (TY_PTR) base (latent resolver gap, filed separately).
Contained to the indexed base + non-call aggregate-field rhs; value-asserting pins redden under each stage's independent revert.
The arr[i].f=mk() assign arm had no aggregate-field sub-arm, so a by-value aggregate field receive fell to the scalar default (one MOVQ, dropping DX/CX) — silent on BOTH stages (byte-id blind). Add a dual-site symmetric in-cap N_CALL arm mirroring C2c (c83a340): scratch-first materialise AX/DX/CX, then word-copy to (fi.foff+k*8) within &arr[i], sizing from the natural field size fi.fsz (not slotsize). Rule-7 LOUD-STOP for the three cases the in-cap GP path cannot transport: over-cap sret (#11c/#234), a float-bearing field whose eightbyte classifies SSE (#11/#165), and a 3/5/6/7-byte sub-8 tail the single narrow tail MOV cannot express (the general cascade tail is the shared C2c/#11 follow-up, task #10). Value-asserting pins (poison-seeded, redden under each stage's independent revert) plus cfail pins for the three loud-stops.
Contained to the indexed base + in-cap call rhs; arr[i].f=src (#11b) and over-cap (#11c) are separate.
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.
`arr[i] = mk()` where mk returns an in-cap (<=24B) struct/tuple/array
left the result in the #4 cgreturn registers (AX/DX/CX), but the
N_ASSIGN-into-N_INDEX path had no arm for an N_CALL rhs, so it fell to
the scalar store tail: only member 0 was written and the index scale
clobbered CX. Both stages emitted byte-identical wrong code (the
documented-but-silent #31-G gap), so the byte-id gate was blind to it.
Add an in-cap N_CALL-rhs arm: materialise the return into a frame scratch
first (keeping the CALL at the frame's natural 16B alignment), resolve
&arr[i], then word-copy the full eightbyte count + sub-8 tail -- mirroring
the #4 receive shape and the #270-1b copy. The eightbyte count derives
from the element size in the type table. Over-cap returns (#234),
non-call rhs (#270-1b) and tuple literals (#121) are unaffected; the
sibling field/deref shapes stay loud (#24).
Surfaced by the codegen miscompile hunt (finding C2c). Pinned by
test/lang/idx_aggret_recv_test.ww (10 value-asserting rows: 2/3-eightbyte
structs+tuples, array elem, sub-8 tail, const/runtime index, all four
base shapes; reddens on revert).
An untyped float literal defaults to f64, so in an f32 context it was
materialized as f64 then bit-truncated by a raw MOVSS (low-32 reinterpret)
rather than narrowed -- e.g. `let x: f32 = 2.0f32; x * 3.0` multiplied by
0.0f. Twelve byte-id-gate-blind both-wrong miscompiles, all this one cause
(compare, binop, call-arg, struct-field, array-elem against an untyped
literal).
Broaden coerce_floatlit to stamp the untyped fconst type_=f32 across the
f32-context sites (assign rhs, call-arg, struct-field, array-elem) and to
descend the implicit-cast shapes (peel unary +/-/cast, recurse binop
operands AND the binop node, recurse arrlit elems), mirroring harec's
lower_implicit_cast. The existing CVTSD2SS gate then fires; cgen is
unchanged. f64 contexts are untouched -- the stamp is gated on TY_F32.
Surfaced by the float codegen sub-hunt (= the deferred #120). Pinned by
test/lang/f32_untyped_narrow_test.ww (22 value-asserting rows incl. f64
controls; reddens on revert).
`let s: str = *h` (a str/slice loaded by value through *str / *[]T)
fell through the N_UN deref arm to the scalar load, emitting a single
MOVQ that read only the 8B .ptr and left .len/.cap from stale registers,
so len(*p) returned garbage. Both stages emitted byte-identical wrong
code, so the self-compile byte-id gate was blind to it. Add a str/slice
arm that loads the full {ptr,len,cap} via cgslicehdr when the chased
pointee is TY_STR/TY_SLICE.
Surfaced by the codegen miscompile hunt (finding C1b). Pinned by
test/lang/deref_hdr_test.ww, which interposes a different-sized decoy
header so the test reddens when the arm is reverted.
wwstage rejected a module-level `const/let []T = [arrlit]` global with "let: not assignable"; cstage accepts (textbook Hare, ref/hare/path/stack.ha:30). The arrlit->slice admission in checkletassign was gated local-only; lift it to module scope too, aligning wwstage UP to cstage's arrlit_init_fits (check.c:3406-3409, slice arm 519-520). cstage unchanged.
Two guards the un-gating requires: the n.rhs.lhs=arr stash stays local-only (a module decl keeps its raw N_ARRLIT for DATA emit, so stashing would leave an untyped count node for the pass-3 asserttyped walker); and tuple-element slice globals are excluded at module scope, because the synthesis delegates element checks to isassignable which lacks a strict tuple arm (#38) -- a [](str,*fn) table would over-accept a sig-mismatched &fn that cstage's strict type_assignable rejects (#124) -- so they stay on the existing typeeqast path.
Closes two divergences 944_alias_emit_b7 pinned: Group A (cstage-runs/ww-rejects) migrates to test/lang/slice_global_arg_test.ww (promoted from _runonly, now cs==ww byte-id); Group B converges to a shared emit_slice_data reject with the identical diagnostic.
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).
Reading gp.f through a module-global pointer miscompiled in BOTH stages,
differently: cstage classified gp as a local at boff 0 and derefed BP
(MOVQ (BP),BX), wwstage collapsed gp.f to an undefined global symbol f
(MOVQ f(SB)). Both now load the pointer value from the global's data slot
before the field offset, converging on MOVQ gp(SB),BX; MOVQ off(BX),AX.
cstage mirrors the #6 store decline; wwstage gains a global-ptr arm and
shares a cgptrfieldload helper with the local arm.
Fused, not split: the two stages must emit byte-identical asm, so a
one-stage commit would fail the byte-id gate. Sibling byte-divergences
filed: #16 (chained-spine gp.x.y), #17 (>32B tagged word-order).
Test: table-driven 689_globptr_field_read_run (24 rows, runtime + byte-id).
The selfhost checker's resolvewalk had no loop-nesting guard and no
N_BREAK/N_CONTINUE arm, so `break`/`continue` outside any loop fell through
the generic child recursion and was silently accepted -- while cstage
(cmd/wcc/check.c) correctly rejects them. A cs!=ww checker divergence
(rule 10); cstage is correct (break/continue outside a loop is an error in
Hare/C/Go), so align wwstage DOWN, not cstage up.
Mirror cstage's mechanism exactly (check.c:598/2494/2529/2611): a `loops`
counter incremented around for and for-range bodies -- the for-`else` and
the init/cond/post walked OUTSIDE the count, since a break there targets an
enclosing loop -- rejecting break/continue when loops==0 with a
byte-identical `file:line:col: error: <kw> outside loop` diagnostic.
match/switch are not loop targets, matching cstage.
The divergence survived because 300_check.c only exercised the in-process C
checker, never w6c_ww; the fix adds 4 rows to the both-stage
989_catA_f2_reject carrier (break/continue outside loop, the for-else
els-outside-count edge, and an in-loop control). make clean && make test:
all 402 passed, byte-id self-compile gates 990-996 green.
wwi_emit took the .wwi `package` leaf from the first primary decl's module tag;
a fully empty primary module body (zero decls) had none, so the leaf stayed the
literal default "main" and the importer rejected it ("package main does not
match import path <leaf>"). The module identity is only available at parse time
(curmod is overwritten by imported //ww:module sections before emit), so stamp
the primary path onto the N_FILE node (TK_MODULE and TK_MODRESET rp!=NULL sites,
only-if-empty so a bare-reset `package main` root stays "main") and, when the
decl-scan finds no leaf, fall back to that stamped path. Symmetric cstage+
selfhost; both detect scan-miss via the same found-flag so the emitted .wwi
stays byte-identical.
Regression: test/wcc/989_wwileaf_run.c, table-driven over {empty body,
comment-only, nested a.b.c} decl-less shapes, non-vacuity proven.
A torn producer write (e.g. disk-full mid-copy) could leave a 0-byte P.wwi or
P.o in out/.pkgcache under a self-consistent key; cache_lookup checked only
existence, so every later build HIT and served the empty artifact forever
(silent serve-wrong). Reject size==0 on both sides, symmetric across stages:
store refuses to commit a 0-byte temp before the key write, lookup treats a
0-byte cached artifact as a MISS so existing poison self-heals on re-derive.
A valid .wwi/.o is never 0 bytes, so the guard cannot misfire.
Regression: test/wcc/989_pkgcache_poison_run.c, table-driven over
{poison P.wwi | P.o | both}, non-vacuity proven by guard-neuter.
cgassign had dedicated N_DOT-store arms for a local-ptr base, a global
value-struct, and chained bases, but none for a global-pointer scalar
field. That case fell through to the generic cgplaceaddr/dotchainaddr
route, which folds the field offset (ADDQ $foff,BX) then stores to (BX).
cstage emits a single displacement store (MOVQ AX,foff(BX)) via its
via_ptr global scalar arm, so the two stages diverged on asm shape
(rule 10). Both forms are runtime-correct here -- BX is a fresh throwaway
in the generic route -- so this was a byte-id divergence, not a
miscompile.
Add the missing displacement-store arm, predicate-mirroring cstage's
via_ptr global scalar arm exactly: plain assignment only, scalar field
only; non-scalar field types stay on the generic path (their global-ptr
deref is a separate deferred item). glob_ptr_field_test.ww gains an
off-8 row as the regression pin -- offset-0 cannot catch it because
ADDQ $0 is suppressed.
Surfaced by the fold-2 Fam-5 migration.
A bodied fn with a bare C-style `...` was silently accepted by cstage
and SEGFAULTED wwstage (resolvefnbody walked a typeless `...` param).
Gate it: bare C-`...` is allowed only on bodiless decls (extern /
@symbol prototypes), the real FFI path; Hare-style `T...` is unaffected.
ww restricts C-`...` to bodiless decls pending vastart/vaarg/vaend
builtins (#16); harec permits bodied C-variadic fns (check.c:3656) -- a
documented divergence, reopened when #16 lands.
Test 852 runs both stages; its reject rows require the gate's diagnostic
(not merely a nonzero exit), so a crash can't pass them vacuously.
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.
Mirrors cstage check.c:1491-1495 (type_isint via the syntax.typeisint tinfo chaser, which chases TY_NAMED.under/TY_ENUM.sub — not the AST-keyed isinttypeast that would falsely reject an alias-int index). Record-and-continue, before the base-bail. Reject path emits no asm so cstage==wwstage byte-id holds (453 green). Pre-existing index double-emit deferred (#6).
The wwstage checker silently accepted enums with a non-integer storage type, duplicate members, or a non-constant member value; cstage already rejects all three (cmd/wcc/check.c:1000-1042). Add validateenummembers, a pure read-only diagnostic dispatched once per enum decl from resolvewalk's N_TENUM arm (check.ww:791, beside stampenumvals -- not the per-query recompute arms), mirroring the catB-7/14 validatestructfields pattern. Storage gate uses typeisint on the resolved tinfo (the exact type_isint mirror: chases TY_NAMED.under and TY_ENUM.sub, so an int-alias storage is accepted; raw-AST isinttypeast would not). Duplicate members: O(n^2) name walk. Unfoldable values reuse enumvalfold with until=member (forward-only). Emits via cerr + c.errs, no mutation, so valid-program codegen is unchanged and cstage==wwstage byte-id holds.
wwstage's value-fold message is intentionally generic where cstage's eval_enum_value gives per-reason text (enumvalfold returns a bool, not a reason); both reject. Documented at the site, filed as follow-up task #10.
Test: new table-driven both-stage reject test 850_enum_reject (non-int storage / duplicate member / forward-ref rows + a distinct-member control whose backward-ref value builds and runs). Full make test: 452 green incl. 990-997 byte-id.