perr() in parse.ww wrote the "w6a: " prefix with a hand-counted length
of 4, but the string is 5 bytes — the trailing space was dropped, so
every assembler diagnostic printed as "w6a:<file>" with no separating
space. Replace the prefix length (and the ": " / "\n" literal writes in
the same function) with the string's own .len via the local-binding
idiom, fixing the off-by-one and closing the hand-count class here. Uses
str-variable .len (correct on both stages), not "literal".len (cstage
miscompile, #14), so this is byte-identical cs==ww.
Verified: w6a_ww on a bad input now writes "w6a: <file>: <msg>\n" with
the space restored; w6c and w6c_ww emit byte-identical asm for the
regenerated main.combined.ww.
The 5 literal os.write diagnostics in obj.ww ("cannot read object",
"missing .text", "missing .symtab", and two "duplicate symbol") passed
hand-counted byte lengths that were each short by one, dropping the
trailing '\n' so every diagnostic printed without its newline. Replace
each magic length with the string's own .len via the local-binding
idiom (let m: str = "..."; os.write(2, m.ptr, m.len: u64);) — the
established wcc/err.ww + w6c/w6l/main.ww pattern — which fixes the
off-by-one and closes the hand-count class by construction. Uses
str-variable .len (correct on both stages), not "literal".len (cstage
miscompile, #14), so this is byte-identical cs==ww.
Also fold two trivially-safe nested-if collapses in the same file:
the archive-member skip guard (three sequential `if (first != ...)`
with no else → one &&-chain) and the text/data exclusivity guard
(`if (intext) { if (indt) ...`→ `if (intext && indt)`).
Verified: w6l_ww on a missing object now writes the full
"w6l: cannot read object\n"; w6c and w6c_ww emit byte-identical asm
for the regenerated main.combined.ww.
The argv-error diagnostics in w6c/main.ww (7 sites) and w6l/main.ww
(13 literal sites) passed hand-counted byte lengths to os.write that
were systematically short by one — every length dropped the final
byte (usually '\n'; "w6l: cannot find -l" dropped the 'l'), so the
diagnostics printed truncated. Replace each magic length with the
string's own .len, which both fixes the off-by-one and closes the
hand-count class by construction.
Uses the local-binding idiom (let m: str = "..."; os.write(2, m.ptr,
m.len: u64);) — the established wcc/err.ww pattern — rather than
"literal".len directly: string-literal .len is miscompiled on cstage
(returns the pointer, not the length; cstage != wwstage), filed as
#14. str-variable .len is correct on both stages, so this is byte-
identical cs==ww and independent of #14. The w6l runtime cstr write
(os.write(2, nm, cstrlen(nm))) is unchanged.
strings.bytesub two endpoint guards, wcc cgdot/cgassign 4-deep
allptr/N_IDENT/localfindnode pyramids, and w6l isarchive's 8 sequential
magic-byte rejects. The isarchive len<8 read-guard stays a separate
statement before the || chain so the byte reads remain bounded. Not
byte-id-neutral (short-circuit emits tighter branches / renumbered
labels) but functionally identical; cs==ww stage-parity holds.
Regenerated all embedding combined.ww.
Fold the 13-arm Jcc else-if pyramid in encode() to a single switch (op)
with the terminal else (isjcc=false) as the empty-label default case.
Pure op->cc value mapping, so a switch expresses it exactly.
Not byte-id-neutral (if-chain -> switch dispatch changes the asm). The
ladder<->switch byte-emission equivalence is pinned by 991_w6a_ww, whose
corpus (wwdump/w6l/w6a main.s) emits all 13 Jcc conditions, and was
independently reproduced at landing (all 13 mnemonics assemble
byte-identical C-w6a vs w6a_ww).
Regenerates the w6a combined.ww amalgamation (Jcc region only).
Fold the 69-arm `if (k == nkind.N_X) return "..."` ladder in nkname to a
single `switch (k)` with the terminal `return "?"` as the fall-past
default. The other ast.ww ladders stay: pr()'s kind dispatch is
side-effecting (emits output, recurses) and uses ||-grouped multi-kind
predicates, not a pure value->value mapping a switch can express.
Not byte-id-neutral (if-chain -> switch dispatch changes the asm), so
the ladder->switch equivalence is pinned by a new table-driven test:
lib/ww/asttest.ww drives nkname over every nkind plus the out-of-band
"?" fallback, wired as 905_nkname_run (same `ww run` @test shape as
904_tok_run). The 990_selfhost wwdump diff only covers kinds that
appear in its corpus.
Regenerates the w6c + wwdump combined.ww amalgamations (nkname region
only).
Fold the ~88-arm `if (k == tkind.TK_X) return "..."` ladder in tokname
to a single `switch (k)` with the terminal `return "<?>"` as the
fall-past default. kwlookup stays an if-ladder: it dispatches on
streqn() string compares over distinct literals, which a value-switch
can't express.
Not byte-id-neutral (if-chain -> switch dispatch changes the asm), so
the ladder->switch equivalence is pinned by a new table-driven test:
lib/ww/lex/toktest.ww drives tokname over every tkind plus the
out-of-band "<?>" fallback, and kwlookup over every keyword plus
non-keywords, wired as 904_tok_run (same `ww run` @test shape as
904_ascii_run). The 990_selfhost wwdump diff only covers kinds that
appear in its corpus.
Regenerates the w6c + wwdump combined.ww amalgamations (tokname region
only).
Drop 112 redundant `let x: T = rhs` annotations where the rhs already
infers T (newnode→*node, p.curfile/curtext→str, p.curline/curcol→i32,
accepttok/== →bool, parse*→*node). stmt.ww (75) + parse.ww (37).
Regenerate the two embedders' combined.ww (w6c, wwdump). Byte-id-neutral:
cstage-w6c asm of each combined.ww is identical pre/post.
byte-id-neutral: every magic-decimal sub is value-equivalent to its
ASCII char-literal. 0x7F (ELF byte-0), ELFCLASS64=2, NUL sentinels,
and hex/itoa radix arithmetic kept decimal (no char identity).
Regenerated w6a/w6l main.combined.ww (the only two embedders).
Pure byte-id-neutral substitution of ASCII magic decimals with char
literals across lex.ww (131) + tok.ww (19); regen w6c + wwdump
combined.ww (lex is embedded in those two only).
escape() out-values for \a (7) and \b (8) are now '\a'/'\b' — both
the C bootstrap lexer (cmd/wcc/lex.c:160-161) and ww's own escape()
map them, so the literals are value-equivalent and stage-symmetric.
Digit-value arithmetic (parseint/parsef64/hexchar: c - '0' over u8)
is left decimal: a rune/i32 char literal would promote the u8 operand
and is not byte-id-neutral. Multi-byte type-suffix comments (i8../f64)
are kept — they label more than a single char.
Replace magic ASCII decimals with char literals in ascii/fnmatch/shlex
predicates (e.g. `c < 48` → `c < '0'`). Byte-id-neutral: ascii params are
rune, so rune<rune emission is unchanged; fnmatch/shlex compare u8 against
value-preserving (<=126) rune constants. Range bounds (0/31/127), the ±32
case offset, the 128 high-bit mask, and fnmatch 0u8 sentinels stay decimal.
Regenerate the three combined.ww that embed ascii (w6c, wwdump, smoke).
Add functional rows pinning predicates reachable only via fnmatch ctype
classes / shlex split: [[:space:]]/[[:print:]]/[[:graph:]] + the '\t' arm
of [[:blank:]] (fnmatchtest), '\t'/'\n' split separators + issafe's
special-char set (shlextest) — so a wrong substitution would be caught.
An empty `[]` carries no element type; ww gets it only from a let
annotation (the #45 retype). Both stages used to silently default the
element to u8, and in value-form positions (return / call-arg) the
lowering miscompiled — malloc(8) ignoring n, a 16B *u8|nomem where a 24B
slice was expected (#5). Now every empty alloc that isn't a
let-annotated binding fails to infer with a loud error, aligning ww DOWN
to harec (ref/harec/src/check.c:1801-1802).
Mechanism: clet / checkletassign flags the single alloc call node that a
`let x: []T =` rescues (save/restore around the init walk); the alloc
branch errors on any empty alloc that isn't that node. The #45 wide-T
retype path is kept. wwstage needs an extra not-yet-stamped guard because
resolvewalk re-types value nodes context-free after checkletassign.
Tests: negative cstage-driver 729 (table-driven: bare-let, return,
call-arg, assignment) + positive @test in attest_pass.ww exercising the
u8 and the wide-i32 (#45) paths at runtime. Both stages reject
symmetrically; byte-id verified on []u8 and []i32.
The wwstage checker rejected a match-bound binder used in a `yield` arm
of a match-AS-EXPRESSION (`let v = match (x) { case let p: *T => yield
*p; ... }`) with asserttyped:un/bin/index; cstage compiled it.
resolvewalk stamps the yield operand's type_ during the in-scope N_MCASE
arm walk. exprtype's N_MATCH arm then derived the match's type by
re-running exprtype on the same operand to recover a type NODE — but the
arm binder's scope is already popped, so the re-derive returned nil and
the N_UN/N_BIN/N_INDEX restamp arms overwrote the good in-scope stamp
with nil. cstage never re-runs: match_yield_type reads the operand's
cached ->type (cmd/wcc/check.c:121).
Root fix (align wwstage UP): matchyieldtype now returns a *tinfo and, at
the post-walk call, READS the operand's cached node.type_ instead of
re-running exprtype — so no operand shape can be clobbered by
construction (deref/bin/index all vanish, no per-arm guards). The
exprtype N_MATCH consumer stamps e.type_ from that tinfo directly (no
tinfofornode round-trip). The pre-walk call (checkletassign L302 /
checkretassign L303 run before the in-scope arm walk, so the operand is
nil there) keeps the nil-safe re-derive — benign and load-bearing: it
types the void-arm literal so let/return-assign has a usable node. The
re-derived node (or btype for the bare-binder idiom) is carried back via
an out-param for the assignability check and for the N_MLET/N_MASSIGN
tuple-destructure consumers (`let (a,b) = match { case let t => yield t
}`, test 945). cstage is single-pass so its else is dead; eliminating
the pre-walk call is #279.
Supersedes the narrow N_UN non-clobber guard (removed — its match
consumer is gone). @test check_match_ptr_deref extended to pin the whole
operand class (deref / bin / slice-index / deref-then-field), dual-stage
(910 + 997) with correct runtime + cs==ww byte-id. The *[N]T ptr-to-
array index variant is blocked separately by #278. Both compiler-
imported combined.ww regenerated. smoke + test-unit (242) + 994 w6c_ww
byte-id (18 corpus incl. selfhost combined.ww) green.
Review fixes for the #272 fold (reviewer272b gate; rob+ken ruling). Bundled
because the wwstage catch-all message carries the citation and the combined.ww
regen covers both .ww edits.
- wwstage cgreturn close-by-construction catch-all keyed on the SYNTACTIC
return-type node (N_TARRAY / N_TNAME+structlookup), so a named-alias
aggregate return type (type a=[N]T / type a=struct) bypassed both the
handling arms AND the loud-stop, falling to the scalar default = silent
segfault/truncation; cstage (type_chase_named at all 4 N_RETURN sites)
stayed correct. Re-key the catch-all on the RESOLVED tinfo (chase
TY_NAMED -> TY_ARRAY/TY_STRUCT) so wwstage LOUD-STOPS (rule 7) instead of
miscompiling. cstage stays correct; the full wwstage tinfo-kind dispatch
(align UP, byte-id) is #277. Established wwstage-stricter divergence
(cf #264), no bootstrap consumer (990-997 green).
- #276 citations at-site (both stages): the cstage >24B array-literal return
loud-stop and the <=24B STRUCT global-receive residual now cite #276. The
wwstage >24B array-literal routes through the tinfo-keyed catch-all
(#272/#276/#277). Correction: ALL <=24B struct globals truncate
symmetrically (byte-id-clean), not only float-bearing -- #276 broadened.
- Cosmetic: fix a double-encoded U+2264 (mojibake) in the cgen.c commit-2
comment.
combined.ww regenerated (#110).
The caller-half of the global case: `g = mk()` into a GLOBAL array
stored only the first word — a ≤24B reg-return landed `MOVQ AX, g(SB)`
(8 of 24 bytes); a >24B sret-return hit the #220 sret-to-symbol gate
which was TY_STRUCT-only and fell through to the same truncation.
≤24B: the local aggregate-receive arm was `off != 0`-only, so a global
array fell to the scalar IDENT store. Add a global ARRAY arm — LEAQ
name(SB), DI then store the full+tail words from AX/DX/CX (an array is
never float-class, so AX/DX/CX is always the transport; no `g+8(SB)`
operand form exists). Mirrors the str/slice global arm.
>24B: add TY_ARRAY to the #220 sret-to-symbol gate (cg_sret_dest_sym /
sretdestnode) — the callee writes the whole array through RDI.
A ≤24B STRUCT global receive can be float-class (X0/X1, not AX/DX/CX),
so it is left at its pre-existing symmetric behaviour — no consumer.
949_aggret_source_run gains global_recv (c → 15) and global_recv_sret
(>24B → 22), both with per-row byte-id.
The N_RETURN aggregate arms gated the return source on N_IDENT ||
N_STRUCTLIT; every other aggregate rvalue (array literal, o.field N_DOT,
a[i] N_INDEX, *p deref) fell through to the scalar-AX default = a silent
8-byte truncation. Both stages emitted byte-IDENTICAL wrong asm, so the
byte-id gate could not catch it (#263 class) — the fix converges on the
runtime oracle.
Mirror the arg-side closure #271 landed: both arms (≤24B @retscr and
>24B sret) now funnel N_ARRLIT through the literal element fill and
N_DOT/N_INDEX/deref through aggarg_srcaddr + the #265/#268 whole-
aggregate copy. Type-agnostic, so struct AND array returns are closed.
A close-by-construction loud-stop (rule 7) guards any future unhandled
aggregate source from reaching the scalar default.
Closes the callee-half of (b)/(c) and the addressable siblings. The
g = mk() global-receive caller-half is commit-2.
949_aggret_source_run pins the class: array-literal / N_DOT / N_INDEX /
deref / named-ident control / >24B-sret-deref / struct-field / struct-
deref, each summing all members (full readback) with per-row byte-id.
Passing an aggregate BY VALUE as a call argument worked ONLY for a ≤16B
struct from an IDENT source; every non-ident source — CALL mk(), N_DOT
o.f, N_INDEX a[i], DEREF *p — and every array / >24B-struct (even as an
ident) fell to the scalar default: one PUSHQ for a multi-word aggregate,
stack-imbalancing against the type-based multi-word drain. cs!=ww, both
garbage (f(mk()) cs4/ww236, f(o.f) cs8/ww108, f(a[i]) cs4/ww28, f(*p)
cs4/ww140; arrays + 32B sret struct same).
The arg-pass twin of the #265/#268 let-init copy. A new aggregate-arg
push arm materialises the source into the arg convention: the source
ADDRESS in SI (ident LEAQ / deref operand / dotchainaddr #253 /
&base[i] spine #252-270) then its ceil(sz/8) words pushed high→low; a
CALL receives first — ≤24B in AX/DX/CX pushed straight, >24B sret'd
into a per-fn @aggargscr then pushed from there. The pop-forward drain
gained a matching array / >16B-struct arm and the callee prologue an
is_bigagg receive (ceil(sz/8) GP eightbytes), so caller and callee
agree on the multi-word layout. The ≤16B-struct-IDENT fast path is
untouched (byte-id preserved).
The new-arm exclusion is TYPE-keyed (the stamped tinfo, mirroring
cstage node_isstructarg over args[i]->type), not the name-keyed
structparamsize — a name-keyed gate re-opened the #211/#13 cross-module
same-leaf collision (784 symmetric: an 8B `sa.s` struct whose
name-resolution collides with `sb.s = *vtable` would miss the struct
fast path and wrongly enter the new arm, diverging from cstage's
1-word push). A float-bearing ≤16B struct from a non-ident source
loud-stops in both stages (the #165 SSE eightbyte transport the GP
push/drain can't model; out of scope). A const array/struct `def`
global as an aggregate arg is aligned DOWN to the leaner wwstage
(both loud-stop) per rule-10.
#110: cgen is compiler-imported by w6c + wwdump — main.combined.ww
regen'd for both.
949 rows: arg_{struct16,arr16,struct32}_{call,dot,idx,deref,ident},
full member readback (struct 16B reg-class + 32B sret-class + array
[4]u32, each non-ident source + ident control); byteid=1 throughout
(master both-broken-and-divergent → converge on the correct full
push, #263). All 111 dotbaseaddr + 3/3 784 pass; test-unit 241 green;
sizelint + smoke OK; the full w6c compiler source (214705 asm lines)
self-compiles cs==ww byte-id.
`let x: [2]inner = [inner{..}, inner{..}]` left the array unpopulated:
the N_ARRLIT per-element store handled scalar/str/float ONLY, so a
struct/array/tuple element hit the multi-word-store gap and stored just
the first 8 bytes (cs0/ww0). Both stages symmetric-broken; converge on
the populated result (#263).
Fix: an aggregate element of an array literal fills each element slot
from its source — cg_structlit_fill_bp for an N_STRUCTLIT element,
word-copy for an N_IDENT element (reusing COMMIT 2's per-element copy
shape). esz is the element's natural size (cstage esub->size). cgen.c
N_ARRLIT arm + cgenstmt.ww cglet. An aggregate `...` repeat and other
element shapes hard-stop loud (rule-7).
949 rows: arrlit_structlit, arrlit_structident (8B struct, byteid=1,
full readback). All 96 pass; test-unit 241 green; smoke OK.
The array-of-struct element store/copy family — one primitive (&(array
element) for an AGGREGATE element, used as address, never deref/truncate)
across three consumers. Both stages were symmetric-broken; converge on
the runtime-correct full-address/full-copy (#263).
(1a) `a[i].m[j] = v` (a:[N]struct) segfaulted: the `arr[i].field` arm
computed &a[i] then DEREF'd it (loaded the struct's first 8 bytes as a
value) for an `[N]T`-typed field → garbage base. Now an array-typed
field of an array element leaves the field ADDRESS (the #135 read-side,
applied to the array-element base). cgen.c arm + cgenexpr.ww cgdot
N_INDEX-lhs branch.
(1b) `a[i] = aggregateval` truncated the copy to an 8B MOVQ. New
aggregate (struct/array/tuple >8B) element-store branch word-copies the
element from the rhs source address (ident / N_DOT field / `*p` deref) —
the WRITE-twin of the #268 let-init loop. cgen.c N_INDEX store +
cgenexpr.ww cgassign.
(3a) `let c = x.arr[i]` (N_DOT base) / `let c = a[i][j]` (nested) dropped
the copy: the #268 let-init N_INDEX source-addr arm was N_IDENT-base-
gated. Now computes &base[idx] via cg_dotbase_addr (N_DOT field) or the
&abase[bidx] spine (nested N_IDENT-array base). cgen.c N_LET +
cgenstmt.ww cglet.
949 rows: elemfield_store, elem_struct_store, elem_arr_store,
letcopy_{dot,nest}_prim, letcopy_subarr (byteid=1); letcopy_{dot,nest}_
struct (byteid=0 — run-correct, byte-id blocked by the orthogonal
value-nested-struct frame divergence #254). All 94 pass; test-unit 241
green.
elemsizeofc drilled a 2D `[N][M]T` base's OUTER-index stride down to the
scalar T (the documented elemsizeof FOOTGUN: it bottoms out at the inner
prim size, 4 for [M]u32). The `direct != 8` short-circuit then returned
that scalar size, so wwstage emitted esz=$4 where cstage emits $12 (the
sub-array size, idx_eff(bt)->sub->size = sub.size*elen, type.c:121). The
runtime stayed self-consistent (write+read the same wrong stride) so it
masked until a CROSS-CELL access — a[0][j] and a[1][j] alias.
Fix: detect a nested-array element ([M]T inside [N][M]T) before the
short-circuit and return the element-array tinfo's natural .size, the
sub-array stride. wwstage-only; aligns up to cstage. w6c unchanged.
949 rows: nest2d_u32/u8/i32 (cross-cell write+readback, byte-id).
The fold-1b unified arm (bb2f4e1) added an N_IDENT addressable-rhs source
setup, but the two stages gated the GLOBAL case differently: cstage used
let_islet || def_isarraydef, wwstage used isletvar || deflookup (ANY def).
On a struct-typed `def` used as an aggregate-copy rhs (`let c: T = G`)
wwstage copied the whole value (correct) while cstage truncated to the 8B
scalar tail — a cs!=ww divergence (rule-10). A struct-LET global already
copies on both, so the def gap was also an internal cstage inconsistency.
Struct defs are first-class laid-out aggregates (DATA storage + field
load, #129 A.2/A.3), so converge on the correct full copy on both: add
def_isstructdef to cstage's predicate and replace wwstage's broad
deflookup with the def_is{array,struct}def pairing already held identical
in defisaddressable. 949 +2 rows (array-def + struct-def global, full
readback, byteid=1).
#265 fold-1 landed the deref-rhs aggregate copy as one slot→slot memcpy
loop fed from a source address in SI. fold-1b adds the remaining
addressable-rhs source-address setups, all routed into that SAME loop:
- array IDENT `let c: [N]T = s` — LEAQ the source slot into SI.
Pre-fix both stages truncated to the 8B scalar tail.
- N_DOT field `let c: A = o.i` — cg_dotchain_addr / dotchainaddr
(#253) lands &(o.i) in SI. Pre-fix truncated to 8B.
- N_INDEX element `let c: A = a[i]` — the &base[i] spine (#252:
scaled index + LEAQ base) lands the element address in SI. Pre-fix
scalar-loaded the element address as a value → segfault.
Size (the #254 non-slot-padded ABI extent) comes from the declared let
type for every shape (lu->size / structabisize|tinfo.size), independent
of the rhs; only the per-rhs address setup differs. The deref arm
becomes one branch of the unified arm. Struct-IDENT keeps its own #32
slot-copy arm above (unchanged). With those, the whole addressable-rhs
let-init-copy family is closed by construction: struct-ident / array-
ident / deref / N_DOT / N_INDEX all full-copy, both stages byte-identical
(rule-10).
949 gains 9 full-readback rows (every member written distinct + summed,
so a partial copy fails): array-ident 16B/32B + 12B(MOVL)/11B(MOVW+MOVB)
tails; N_DOT struct-field 16B + array-field 32B + 11B-tail struct field;
N_INDEX struct element 16B/32B. The N_INDEX source array is populated
through a `*inner` to `&a[i]` (the #135/#252 store path) because the
array-of-struct element direct store (`a[i].m[j]=v` / `a[i]=s` / struct-
array literal) segfaults on a SEPARATE pre-existing bug, reported
alongside this fold. w6c+wwdump combined.ww regen (#110). 70/70 949,
test-unit 241, sizelint, smoke green.
A `let c: T = *p` (T a struct or array, >8B) copied no full aggregate:
cstage dropped the init entirely (c read garbage); wwstage emitted only
the scalar `MOVQ AX,off(BP)` tail (first 8 bytes). Both wrong, differently
— converge BOTH stages on a size-driven slot-to-slot memcpy: cgexpr the
deref operand to the source address in AX, MOVQ AX,SI, then a MOVQ run
plus a sized MOVL/MOVW/MOVB tail over the #254 non-slot-padded ABI extent
(lu->size / structabisize for a struct, tinfo.size for an array). Mirror
arms in cgen.c N_LET and cgenstmt.ww cglet, byte-identical (rule-10).
Unblocks sha256's faithful `let copy = *h`. The by-value aggregate RETURN
ABI (array/struct return truncates to AX) is fold-2 (#267, deferred).
949 gains 6 full-readback rows (every member written distinct + summed,
so a truncated copy fails): struct{[4]u32} 16B, struct{[8]u32} 32B via
both *(&s) and *p (sha256 shape), bare [4]u32, and non-8-mult tails
([3]u32 12B → MOVL, [11]u8 11B → MOVW+MOVB). w6c+wwdump combined.ww regen
(#110). 61/61 949, test-unit 240, sizelint, smoke green.
The #259 store fix unmasked a pre-existing latent cs!=ww in the tagged-
element READ via an N_DOT base (`x.o[i]`) / chained N_INDEX base
(`m[i][j]`): wwstage materialized the element as a SCALAR one-word load +
zeroed tag where cstage copies the full tagged slot — silently dropping
the tag/payload-high word (wrong variant). Three sites all keyed off the
same N_IDENT-only gate; cstage classifies TY_TAGGED for ANY base off the
checker-stamped element type. Align wwstage UP:
- cgindex (cgenexpr.ww): the N_DOT/N_INDEX-base arm now sets
elem_tagged/elem_slot_sz from n.type_ (the stamped element tinfo),
mirroring cstage cgen.c:8101 — the full-slot copy arms then fire.
- rhstaggedabicall (cgenutil.ww): the N_INDEX branch reads
typeistagged(src.type_) for any base instead of an N_IDENT-only
structural lookup, mirroring cstage's src->type keying — fixes the
let-init / call-arg widen-source spill.
- forwardtagged (cgenstmt.ww): the return-path passthrough gate now
accepts N_INDEX/N_DOT tagged rhs (which cgexpr materializes into the
tagged ABI), not just N_CALL — fixes `return x.o[i]`.
read + call-arg + return + chained 2D all close by construction (one
materialization path). cstage unchanged (pure wwstage-align-up). 949
gains 9 #261 rows (i32 + explicit-void variant per shape proves the tag
survives) and flips the two #259 read-back rows to byteid=1.
wwstage conflated SLOT-size (round-to-8, for frame) with ABI-size (true)
for a nested value-struct. A nested value-struct field is sized via
fieldsize() (TY_STRUCT -> ti.slotsize = 8), poisoning structabisize and
registerstruct si.totsize to 8 for a struct whose true ABI size is 4.
Two emission sites then over-sized, both SILENT cs!=ww divergences:
D1 (local, cgenstmt.ww cglet): zsz = structabisize = 8 hit the
`zsz == 8` zero arm (#213) -> a stray `MOVQ $0, off(BP)` cstage
never emits (ABI 4 is sub-8 -> left uninit per the shared no-rhs
zero-init policy).
D2 (global, cgen.ww emitletdataw): the struct zero arm wrote
letemitsize/si.totsize = 8 DATAW bytes; cstage cg_let_emit_size
returns u->size = 4.
Fix sources the zero-init extent from the type table's tinfo.size
(peeling TY_NAMED) at both sites — the same value cstage reads
(cgen.c:8397 / :978). fieldsize / registerstruct / frame slot-padding
stay UNTOUCHED: moving the fix into the size helpers would shift
nested-struct field offsets and re-diverge other byte-id. Pure
wwstage-align-down; cstage cmd/w6c/cgen.c unchanged.
Test 949_valstruct_subsize_run: D1 local + D2 global over ABI sizes
1/2/4 (the whole sub-8 / non-8-multiple class), each cstage-run +
cs==ww .s byte-id; plus a >8 (16B) local+global NEGATIVE control
proving the fix didn't disable legitimate multi-word zero-init.
Regen w6c + wwdump main.combined.ww (cgen is compiler-imported, #110).
Hare admits an array with a defined length wherever its element slice is
expected (assign / return / call-arg / init) as a borrow; ww rejected it
everywhere (the #108(c) exclusion), so base64 worked around the gap with
explicit a[0:n] slices.
type_assignable / isassignable now admit array->slice on an exact element
match (mirror ref/harec/src/types.c:1080-1097, the SLICE-dst arm). The four
acceptance sites route through one shared helper (desugar_arrayslice /
desugararrayslice) that rewrites the array expr to the explicit full slice
arr[0:len(arr)] — an N_SLICE over the array base. cgen is untouched: the
existing slice lowering (#252/#257/#135 made array bases, incl struct-field
arrays, correct) materialises the borrow header {.ptr=&arr[0], .len=N,
.cap=N}, byte-identically in both stages.
wwstage runs no general call-arg / N_ASSIGN typecheck, so checkassign +
desugarcallargs are added solely to route those two contexts through the
shared desugar (rule-10). desugarcallargs additionally loud-rejects an
element-MISMATCH array into a []T param, scoped to that shape so wwstage's
broader call-arg leniency is untouched.
953_arraytoslice_run covers the four contexts + a borrow-alias proof + the
i32/u8 element axis (dual-stage run + cs==ww byte-id), plus mismatch-reject
rows asserting both stages refuse [4]i32 -> []u8. Regen'd w6c + wwdump
combined.ww (#110).
#259: the tagged-union array-field indexed STORE arm computed &arr[i]
from a non-ident base (`x.o[1]=v` where o:[N](T|void)) with a plain
cgexpr(base) — the N_DOT array field auto-derefs (loads the field's
first 8 bytes AS a pointer) -> garbage dest -> SEGFAULT. Route the base
through the array-gated helper cg_dotbase_addr/dotbaseaddr (dst BX keeps
the scaled index live in AX; viaptr + chained handled by the shared
helper), mirroring #257. Symmetric both stages. This was the last
unrouted cgexpr(base) cell in the array-field-base-address family
(#135/#252/#253/#255/#257) — proof-grep of both stages now shows ZERO
unrouted base cells in the slice/decay/addr/index/store builders, so the
family is closed by construction. (The chained-ptr-field scalar/str/
float store sites at cgenexpr.ww:6489+ / cgen.c:4379+ correctly cgexpr
the pointer spine and are the #133 family, not array-field-address.)
#256: align wwstage dotchainaddr's N_IDENT non-local arm to carry
cstage cg_dotchain_addr's `let_islet || def_isstructdef` guard (here
isletvar || deflookup) instead of emitting LEAQ name(SB) unconditionally.
Unreachable on valid input (a struct-typed chain root is always local /
let-global / struct def) so zero divergent asm — never-silent ethos only.
Tests (949): store-only byte-id rows (tagged_store_own/_ptr) gate the
#259 store base-address emission cs==ww; store+readback rows
(tagged_store_*_rd) are run-only (cstage) proving the store wrote the
right slot (66/77) and no longer segfaults. byte-id on the readback rows
is blocked by an ORTHOGONAL newly-surfaced divergence in the N_DOT-base
tagged-element READ materialization (sibling of #255: wwstage loads one
word + zeroes the tag where cstage copies the full 16-byte slot) — the
store base is already byte-id; only the read-back diverges. Reported
separately for triage.
combined.ww regen'd (w6c + wwdump embed cgen).
An inline slice of a struct `[N]T`-field passed DIRECTLY as a call
argument (`rd(x.o[lo:hi])`) materialized the slice .ptr from the field
VALUE, not its ADDRESS: the pushargs/pushargsrev N_SLICE inline builder's
non-ident else-arm did plain cgexpr(base), so the N_DOT field auto-derefs
(MOVL field,AX used as .ptr) -> callee derefs garbage -> SEGFAULT. The
let-init / assign-rhs / return / hoist-to-local contexts already routed
through the cgslice #252 choke-point; only this call-arg builder kept a
private duplicate. cs==ww both segfaulted identically pre-fix (gate-blind).
Fix (symmetric both stages):
- route the else-arm through cg_dotbase_addr / dotbaseaddr (the cgslice
#252 choke-point: array-field-gated, so `[]T`/str/`*T` fields fall
through to cgexpr; chained inner `o.p.m` handled via its #253 arm);
- extend the N_IDENT-only esz gate to N_DOT bases, taking the element
width from the checker-stamped base->type (rule-13 type table), so
non-u8 call-arg slices scale stride.
Before: `MOVL -8(BP),AX; PUSHQ AX` (field value as .ptr). After:
`LEAQ -8(BP),AX; PUSHQ AX` (field address). cs==ww byte-identical.
Helper note: used dotbaseaddr (not dotchainaddr as first scoped) — it is
the established cgslice choke-point and is array-field-gated, so a slice/
str-typed field base keeps the correct cgexpr header-ptr load; bare
dotchainaddr lacks that gate and would mis-emit the field address for
those. dotbaseaddr already handles the chained `o.p.m` inner via #253.
Tests: test/wcc/949 gains 6 call-arg rows (u8, i32-esz-stride, via-*struct,
chained, + hoist-to-local and bare-local-array controls), each run-
correctness AND cs==ww byte-id.
PROOF-GREP residual: the tagged-union-element indexed-STORE arm
(cgen.c:~4972 / cgenexpr.ww:~5024) is the same N_DOT-base auto-deref shape,
still unrouted in BOTH stages (symmetric, segfaults) — a distinct
consumption axis filed separately; NOT fixed here.
The cgindex N_DOT-base arm set esz from the checker-stamped element
tinfo but skipped signedness, so loadopsz saw signed_elem=false and
emitted MOVL/MOVZ* (zero-extend) where cstage's fldloadop reads
signedness from the element type and emits MOVSXD/MOVSWQ/MOVSBQ. A
negative i8/i16/i32 read of `x.o[k]` (struct `[N]T` field) round-tripped
with the wrong upper bits — silent cs!=ww, byte-id-blind since bootstrap
never indexes signed-narrow struct array-fields.
Mirror the sibling N_INDEX-base arm: signed_elem = typeissigned(dt).
loadopsz already keys on (signed,sz), so this closes all three narrow
widths at once. Pure wwstage-up; cstage unchanged.
949 gains nload_i32/i16/i8 negative-read rows (run + cs==ww byte-id).
combined.ww regen'd for w6c + wwdump (the cgen embedders).
cg_dotbase_addr / dotbaseaddr rejected a non-ident inner, so a chained
base (`o.p.m[i]` / `o.i.m[i]` / `o.a.b.m[i]`) fell to cgexpr(base) which
auto-derefs the array field's first 8 bytes AS a pointer -> garbage base
-> segfault (base64 fillobuf `s.enc.encmap[...]` blocker). Extend the one
helper per stage to accept a chained inner: a new cg_dotchain_addr /
dotchainaddr recovers the container base via the dot-chain spine (recurse
to &x, deref when x is a *struct, sum field offsets), keeping the same
no-AX/no-stack spill contract. dotbaseaddr then takes the pointer VALUE of
inner when viaptr, else its ADDRESS, and adds the field offset. One fix
closes every op (index r/w, addr-of, slice, compound) since all route
through the helper. Symmetric cs==ww byte-id.
test/949: +22 rows. Chained-PTR (rd/wr/addr/slice x2/compound), deeper
(value+ptr leaf links, triple-pointer exercising the internal deref),
non-u8 esz stride (i32 addr+slice), and single-level controls — all
byte-id. The chained VALUE-container arm (`o.i.m`) is run-only (byteid=0):
it needs a value nested-struct instance, which trips THREE orthogonal
pre-existing cs!=ww emission divergences (bare-let zero-init policy,
global DATAW byte count, i32 element-load opcode in the index fallback)
unrelated to #253. Run correctness proves the segfault is gone for that
cell; byte-id there awaits the separate wwstage value-nested-struct fix.
Taking &x.o[i] (address-of) or slicing x.o[lo:hi] / x.o[lo:] of a
struct's [N]T-typed FIELD computed the field's VALUE as the base
address (MOVL off(BP),AX) instead of its ADDRESS (LEAQ off(BP),AX) ->
garbage pointer -> segfault. The index read/write path was fixed in
#135; this is the unwired addr-of + slice sibling — both base-address
paths fell to the generic cgexpr(base) auto-deref.
Wire the #135 cg_dotbase_addr / dotbaseaddr helper into the addr-of
N_INDEX complex-base arm and the N_SLICE base arm, symmetric on both
stages (guarded if(!dotbase) cgexpr(base)). Extend the slice element
stride (esz) and default-hi length to an N_DOT array-field base too,
read from the field's element tinfo / array length via the type table
(rule-13) — so non-u8 element slices scale correctly and s.obuf[lo:]
gets the array's element count.
cstage already derived default-hi via base->type (alen); only wwstage
needed the N_DOT default-hi arm. cs==ww byte-identical on every shape.
test/949_dotbase_addr_slice_run: 7 dual-stage rows (addr-of local +
*struct param, explicit + default-hi u8 slice, non-u8 [4]i32 stride,
bare-local control), run + cs==ww byte-id. Regen w6c/wwdump combined.ww.
`let a:[4]u8=[65,66,67,68]`, `def D:[4]u8=['A',..]`, and `enc{m=[65,..]}`
rejected with "init [4]i32 not assignable to declared [4]u8": an array
literal's element type came from the elements via type_default (int-lit
-> i32, rune-lit -> rune) with no declared-element-type propagation. The
scalar path already narrows (`let c:u8='A'`); only array aggregation at
the let/def/struct-field sites #130 (test 920) left unwired did not.
Fix = the int/rune analogue of coerce_floatlit, realised as the EXISTING
#130 accept-if-fits range-check — NOT a node-type restamp. cgen drives
the array element WIDTH from the declared type at every site (cgen.c
local-let lu->sub, emit_array_data d->type), so a restamp would be dead
code (the array literal keeps its [N]i32/[N]rune node type; the cs==ww
byte-id gate confirms the bytes emit u8-wide regardless). Per element:
foldable int/rune literal -> defcastfits range-check vs declared T
(in-range accept, out-of-range REJECT loud, rule-7); non-foldable ->
type_assignable / isassignable.
cstage (check.c): wire arrlit_init_fits into clet (local let),
struct-field-init, and def-init — the three sites the #130 module-let
path already covered.
wwstage (check.ww): factor checkletassign's inline #130 block into
checkarrlitfits and call it from the let path, the def path, and a
TARGETED array-field walk in the N_STRUCTLIT arm. This also closes a
pre-existing rule-7 wwstage over-accept: the def path ran NO init
assignability check and the N_STRUCTLIT head-stamp parks field
assignability (#23), so out-of-range / str array elements silently
over-accepted (a truncating miscompile) at those two sites. The
struct-field walk is the array-field accept-if-fits ONLY — it reuses the
stable N_TSTRUCT field-list walk (astoffset precedent), isolated from
the broader parked #23 field-assignability walk.
Regenerated w6c + wwdump combined.ww (embed check.ww). New test 951
covers let/def/struct-field x int/rune accept (run + cs==ww byte-id) and
out-of-range/str reject (both stages). test-unit 237 + smoke green.
A struct literal initialising an array-typed field as a local
(`e{ encmap = [..] }`) silently dropped the initializer: cg_structlit_fill
(cstage) / cgstructlitfill (wwstage) had no TY_ARRAY field arm, so the
array field fell to the generic scalar tail — cgexpr the N_ARRLIT (→ AX≈0)
then store one sized word — losing every element. cstage returned 0;
wwstage emitted byte-identical wrong code. (The GLOBAL literal-init path
is unaffected: it goes through emit_struct_lit_bytes, already correct via
#129 A.3.)
Both stages now element-wise store the N_ARRLIT at base+field_off+i*esz,
reusing the proven N_LET array-init shape (cgen.c:8467 / cgenstmt.ww:1393)
for int and float elements plus its `...` repeat fill; esz routes through
the type table (rule 13). str/slice/struct/tagged ELEMENT arrays are the
N_LET path's documented multi-word gap (cgen.c:8462) — converted from the
silent drop to a LOUD rule-7 error in both stages, not left silent.
Symmetric both stages (rule 10), byte-identical .s.
The `...` repeat in a struct-literal array field is checker-unreachable
today (the field type-check rejects `[v...]` length inference — a
separate checker gap); the arm mirrors N_LET's repeat for symmetry.
Test 949_structlit_arrfield_run: +local literal-init reads (idx 0 / last
element), cstage run + cs==ww byte-id.
Reading an array-typed field of a module-global struct value (`G.arr[i]`)
silently miscompiled: the N_INDEX fallback's cg_dotbase_addr (cstage) /
dotbaseaddr (wwstage) helper — the #135 sibling that computes &(s.field)
for a `[N]T` field — had no module-global-struct base arm. cstage emitted
`LEAQ (BP)` (localfind returns 0 for a global, so it read the stack frame
→ 0); wwstage's localfindnode returned nil and the fallback keyed on the
FIELD name, so it returned false and the caller's cgexpr(N_DOT) loaded the
field VALUE as a pointer → SEGFAULT. The .data was already correct
(emit_struct_lit_bytes #129 A.3); only the READ base address was wrong.
Both stages now emit `LEAQ name(SB) (+ ADDQ field_off)` for a global
value-struct base, mirroring the scalar global-field read (cgen.c:7532);
const globals resolve via def_isstructdef. Symmetric both stages (rule
10), byte-identical .s. Unblocks base64's `const std_encoding.encmap[i]`
reads (#22).
Test 949_structlit_arrfield_run: global `let`/`def` struct array-field
read, cstage run + cs==ww byte-id.
The old buffer surface (encodedsize/decodedsize + encode(dst,src) i32 +
decode(dst,src) (i32|invalid)) does not exist in Hare — it predates the
#94 io vtable and mis-cited hex.ha:175 while implementing a different
signature. Replace it with Hare's real surface
(ref/hare/encoding/hex/hex.ha):
- newencoder(out: io.handle) (:28) — write-only encoder stream.
- encode(out: io.handle, in) (size | io.error) (:91).
- encodestr(in) str (:68).
- decodestr(s) ([]u8 | errors.invalid) (:175).
Divergences (documented at-site):
- The streaming DECODER (newdecoder/decode_reader, :120,:129) is
DEFERRED to #247, blocked on #199b: Hare's decode_reader returns
errors::invalid, which fits Hare's io::error (spreads
...errors::error). ww's io.error (lib/io/types.ww:55-62) does not
carry errors.invalid, and io.read's (size|eof|error) can't propagate
it, so a hex decoder *stream* can't faithfully report invalid hex
through io.read yet. decodestr ships as a direct transform meanwhile.
- nomem dropped from encodestr/decodestr returns (ww memio.dynamic has
no failure path — same memio.string rule-9 carve-out, memio.ww:208).
- The local hex.invalid type is deleted in favor of errors.invalid
(that was the original divergence).
- encode uses a single io.write rather than Hare's io::writeall (ww has
none — fmt.fprint:498-501: callers drive write-all over raw io.write;
encode_writer is whole-slice so a single write is equivalent).
- dump (:212) deferred: ww has no default-arg support and fmt's
formattable lacks u64 (#209), so the address column can't be ported
faithfully yet.
hex is now import-bearing, so it moves off the 900_stdlib standalone-
compile list (like fmt/os/strings/bufio/bytes/errors before it); coverage
stays at 979_hex_run.c. The stale "mirrors lib/encoding/hex.encode"
comments in lib/encoding/utf8/utf8.ww are updated, which regenerates the
6 selfhost combined.ww (5 cmd + test/smoke) (comment-only, byte-id-neutral).
tinfocachelookup walked a flat prepend-only association list on every
cache miss -> O(N) scan x O(N) calls = O(N2) (91% of all wwstage
instructions on a 5k-line input; w6c_ww ~265x slower than its C twin).
Replace the single list head with a node-ptr hash index, mirroring
sym.ww scope.buckets (rule-12): NBUCKETS_TINFO=8192 power-of-two
buckets, ptr hashed via (key>>4)&(N-1) (>>4 drops the always-zero
aligned low bits so buckets don't cluster), cnext now chains within a
bucket. First-match-in-bucket preserves the old most-recent-bind-wins
order -> identical *tinfo per node -> byte-identical asm.
cstage (cmd/wcc C) has no such cache, so this is wwstage-internal:
no emitted-asm change, no cstage-symmetry obligation. Verified
byte-identical output (baseline vs new binary, same 32k-line input)
and 52.6s -> 0.54s (~97x). combined.ww regenerated for w6c + wwdump
(only tools embedding typ.ww). test-unit (235) + smoke green.
Graduates the integer FORMAT side to verbatim Hare ports, completing the
round-trip whose parse half landed in fold-1, and adds the machine-word
entry points.
- u64tos: ref/hare/strconv/utos.ha:10-42. Replaces the pre-graduation
basedigit() helper with Hare's rune LUT (lut_upper/lut_lower), single
static buffer + bytes.reverse, and strings.frombytes for the
`*(&s: *str)` reinterpret (rule-9 carve-out; ww's lib/types has no
`string` struct). basedigit deleted (now dead).
- i64tos: ref/hare/strconv/itos.ha:10-32. Now `if (i >= 0) u64tos(i)`
else negate-and-prefix via `u64tos((-i): u64)`. This fixes the
i64tos-on-I64_MIN bug (cgen.ww #144): the old `n = -n; for (n > 0)`
left n at the I64_MIN bit pattern (still negative), emitting just
"-". The `(-i): u64` two's-complement reinterpret yields the true
magnitude 9223372036854775808.
- itos/utos/ztos/uptrtos: int/uint/size/uintptr 8B machine-word
wrappers (itos.ha:52, utos.ha:62/67/72), parallel to fold-1's
stoi/stou/stoz. The existing iN/uN width wrappers are unchanged.
Divergences documented at-site: no static assert; LUT-select + base
normalize via the existing basenum() (ww has no if-expression); explicit
copy loop for Hare's slice-assign.
Probes (drew PROBE-BEFORE-COMMIT, all green on BOTH stages):
- i64tos(I64_MIN) == "-9223372036854775808": cstage `ww run` exit 0 +
wwstage-compiled binary exit 0; cs==ww .s byte-identical on the real
combined (30190 lines).
- static `[0...]` fill + rune LUT static-init emit byte-identically
cross-stage (isolated smoke probe + the combined byte-id).
- frombytes (not a types::string mirror) per rule 9.
Tests: extend inttest.ww with test_u64tos[_bases] / test_i64tos[_bases]
(verbatim utos.ha:74-103 / itos.ha:54-87, flat assert sequences;
feedback_test_match_hare_source) + test_word_wrappers. I64_MIN inputs
spelled -I64_MAX-1 (proj #245: wwstage mis-lexes the 2^63 literal).
combined.ww regen: strconv is compiler-imported via fmt, so w6c +
wwdump main.combined.ww + smoke.combined.ww are regenerated.
cstage cbinop routes every comparison through unify_arith
(cmd/wcc/check.c:952), which loud-rejects an error-typed operand
paired with a differing type (e.g. strconv.invalid != i32). wwstage
binoptype returned bool for comparisons without any unify step, so it
silently accepted a program cstage rejects -- a rule-10 break (align
the leaner-but-leniner wwstage DOWN to cstage).
Scope the rejection to an error operand (varianterr) mismatched with
the other (typeeqast) so the broad differing-types diagnostic -- whose
typeeqast-vs-cstage-type_eq asymmetry risk could reject valid bootstrap
code -- stays out of wwstage. Covers the whole comparison family
(EQ/NEQ/LT/LE/GT/GE), all of which cstage routes through unify_arith.
Found by impl-strconv3 writing the strconv test. Gate-blind: the
bootstrap never compares an error type to an int, so byte-id stayed
green while the stages disagreed on what's a valid program.
test/wcc/949_errtype_compare.c: both drivers reject invalid !=/==/< i32
(K_BUILDERR); same-error-type and plain-int compares still accept on
both stages + cs==ww byte-id (K_RUN). 12/12.
Add the int/uint/size entry points (ref/hare/strconv/stoi.ha:53,
stou.ha:107,113). Hare clamps to types::INT_MIN/MAX, UINT_MAX, SIZE_MAX
via stoiminmax/stoumax; ww's int/uint/size are 8B machine words
(INT/UINT/SIZE limits == I64/U64 per lib/types/types.ww:30-37), so the
clamp is a no-op — the full i64/u64 range parses with no spurious
overflow. Documented at-site (the bound consts are package-private, so
inlining them would just re-encode I64/U64_MAX).
Tests: extend inttest.ww with test_stoi_stou_stoz — value path, sign,
overflow pass-through, and the no-clamp fidelity (I64_MAX/U64_MAX parse
without overflow) plus hex/bin bases through the shared parseint core.
combined.ww regen: w6c + wwdump main.combined.ww.
Port ref/hare/strconv/stou.ha:8-65 (rune_to_integer + parseint) and the
stoi64/stou64 fidelity rewrite (stoi.ha:9-17, stou.ha:70-76) over the old
digval loop. parseint is the shared sign + per-digit + multiply-overflow
core returning ((bool, u64) | invalid | overflow); stoi64/stou64 destructure
its `(sign, u)` tuple-in-union result — the shape unblocked by #242/#241.
Wins over the prior ad-hoc parse: leading '+' accepted, '-' on stou64 is
overflow (not silently dropped), wraparound overflow detection (n < old),
and the invalid payload carries the offending byte index per Hare.
Tests: lib/strconv/test/inttest.ww (run via test/wcc/922_strconv_int_run.c),
inline per-case checks mirroring Hare's assert sequences stoi.ha:56-86 /
stou.ha:116-138 (Hare's strconv int tests are flat sequences, not row
tables; feedback_test_match_hare_source). Covers valid dec/hex/oct/bin,
+/- sign, invalid+index, overflow, and U64_MAX / I64_MAX / I64_MIN
boundaries. The I64_MIN expectation is spelled -I64_MAX-1 (Hare's own
two's-complement identity) to isolate the test from #245 (wwstage mis-lexes
the literal 9223372036854775808 -> 0); the parse INPUT is unaffected and
yields the correct value on both stages.
combined.ww regen: strconv is compiler-imported (via fmt), so w6c +
wwdump main.combined.ww are regenerated.
cgexpr could not produce a tuple VALUE, so a destructure / let bind of an
RVALUE tuple read garbage past the first element (cstage) or left an untyped
binder aborting wwstage's asserttyped gate — a DANGEROUS gate-blind cs!=ww,
and the strconv-int blocker (Hare's stoi64/stou64 require
`let (sign, u) = parseint(s, base)?`). Three feeders, all routed at the same
SysV register-return cursor the cgmlet/cgmassign consumers already read:
- an N_TUPLE literal fell to the `cgexpr_int(0)` / `MOVQ $0, AX` default;
- a tuple-typed IDENT loaded only word0 into AX (`yield t`, `return t`,
`let q = t`), leaving DX/CX stale;
- the `?`/`!` unwrap of a tuple-in-union payload lifted only word0->AX,
stranding word1 in CX (the scalar/str success ABI).
Fix (both stages, byte-identical per rule 10):
- cgexpr packs an N_TUPLE literal into the cursor (cg_tuple_lit_to_cursor /
cgtuplelittocursor — a byte-identical reuse of cgreturn's in-register
N_TUPLE arm) and a tuple IDENT from its slot at the register-ABI stride
(cg_tuple_slot_to_cursor / cgtupleslottocursor);
- the ?/! unwrap shifts a tuple success payload down one integer reg past
the tag (cg_tagged_tuple_payload_shift / cgtaggedtuplepayloadshift),
loud-stopping a float/slice/str payload element (the SysV per-eightbyte
tagged-tuple-payload classification is #243);
- wwstage's checker recovers the popped match-arm binder type for a
`yield <binder>` operand (matchyieldtype's scope-free fallback to the
arm's declared type), so the destructured binders stamp — cstage reads
the operand's already-stamped ->type, wwstage caches only a tinfo.
Over-cap rvalue-tuple materialisation (no slot to sret a bare expression
value into) loud-stops both stages — the #10 follow-up.
NOT closed (distinct root, deferred to #238/task #6): single-var
`let q = (true, 9u64)` then `q.N` — the N_LET tuple-init sz==16||32 gate
drops a narrow-first mixed tuple, and the N_DOT tuple-field PACKED-offset
reader disagrees with tuple_store's 8B stride. Not the rvalue-into-cursor
fix and not a strconv blocker (strconv destructures); documented at the test
header.
Test 945_rvalue_tuple_destructure_run: literal destructure, match-yield
destructure, and the ?-call strconv shape, each run + cs==ww byte-id on both
drivers (9 checks). Embedded w6c/wwdump combined.ww regenerated.
A mixed-scalar tuple WRAPPED IN A TAGGED UNION (the (neg, n) shape Hare's
strconv parseint returns, ((bool,u64)|invalid|overflow)) miscompiled three
ways, all gate-blind (no bootstrap tuple-in-union):
(a) cstage CONSTRUCTION: a tuple variant fell through the N_RETURN scalar
shuffle, which ZEROED tag + payload — the operands were never packed.
Route the tuple variant through the scratch-slot widen path; add a
TY_TUPLE arm to cg_widen_tagged_store that packs each element into the
union payload at the register-ABI 8B stride + sets the variant tag.
(b) wwstage CHECKER: `let (a,b)=t` over a plain tuple ident (the match-
bound union payload) left the un-annotated binders UNTYPED, so the bin
node reading them was untyped -> asserttyped abort. The element-type
distribution only fired for an N_CALL rhs. Consume the rhs tuple type
for ANY rhs (mirror cstage check.c:2017).
(c) BOTH stages DESTRUCTURE: the register-cursor receive assumes the rhs
left every element in AX/DX/CX (a call's tuple-return ABI). For a tuple
IDENT cgexpr loads only word0->AX, so the 2nd binder read a STALE DX.
Copy each element from the ident's slot at the 8B stride.
Construction is correct at ANY variant position (the resolved tag, not a
default 0); wwstage resolves it via the typeeq core (flatvariantidxt), not
taggedvariantindext whose str/slice shape-fallback would mask a mismatch.
Two rule-7 loud-stops cover shapes this slotted packing can't yet handle,
on BOTH stages, so neither silently miscompiles:
- a tuple with a SysV-eightbyte-sharing narrow pair (e.g. (i32,i32,u64)),
caught by the 8+payload > slot-size guard (the eightbyte tuple
classification is #243);
- a tuple built from a BARE LITERAL element (`true`/`false`, suffix-less
`7`). cstage's cg_tag_for_variant can't type the literal (#241), returns
-1, and loud-stops. wwstage types `true` as bool and `7` as untyped_int,
so flatvariantidxt WOULD resolve the variant — a program cstage rejects
but wwstage accepts is the cs!=ww divergence rule 10 forbids. wwstage
mirrors cstage's CONDITION (a bare-literal element), not its -1
mechanism, with an explicit guard that aligns the richer side DOWN. Lift
BOTH guards together when #241 lands cstage literal typing -> symmetric
accept.
Test 940_tuple_in_union: 4 K_RUN rows (variant 0, void arm, tuple at
variant 1 two ways) x cstage-run + wwstage-run + cs==ww byte-id, plus 2
K_BUILDERR rows (eightbyte-share, bare-literal) asserting a loud stop with
the #242 diagnostic on BOTH drivers = 16 ok.
An over-cap tuple mixing a scalar with slices/str (e.g. (int,[]u8,str),
56B) laid out differently in the two stages — gate-blind, since no
bootstrap path returns such a tuple. Two silent cs!=ww bugs, one per
ABI side:
- callee SEND (cstage cgen.c N_RETURN over-cap-tuple arm): foff
advanced by the LITERAL expression's type size. A bare int literal
element is stamped TY_UNTYPED_INT (size 0), so `e->type->size`
added 0 for a leading scalar — the next element clobbered it at
offset 0 and every trailing element packed 8 bytes low. wwstage
already sized from the return-type tuple (c.fnret.list), so the
callee frames diverged. Fix: size foff from cg_ret_type's tuple
params (rule-13 type table), aligning cstage to wwstage and to the
t.N reader's f->offset.
- caller RECEIVE (wwstage cgenstmt.ww cglet N_TTUPLE arm): the
in-cap register tuple-receive branch had no capacity gate, so a
56B over-cap tuple was received via AX/DX/CX/R8 (+ R8 fill)
instead of from the sret dest the callee wrote. cstage gates the
twin branch on `sz == 16 || sz == 32` and falls over-cap tuples
through to the sret receive. Fix: add the same size gate to
wwstage, aligning it to cstage.
Both stages now emit byte-identical asm and the value round-trips.
Regen w6c + wwdump combined.ww (cgenstmt embeds in both).
New 940_mixed_scalar_tuple_sret_run: leading/trailing/middle scalar
shapes, annotated + inferred let, each self-asserting every element
(scalar direct, slice/str via len) — both drivers exit 0 + cs==ww
byte-id (12/12).
len() special-cased only a plain N_IDENT slice operand (load .len at
BP+off+8) and an array operand (fold $alen); every other shape fell back
to a bare cgexpr(operand), which for a slice leaves AX=.ptr. A tuple-
element read (t.N) loads only AX=.ptr, so len(t.N) on a slice/str tuple
element returned the slice's .ptr word AS its length — a silent
miscompile, gate-blind because the bootstrap never does len() on a
slice-typed tuple element (sibling of the #234/#237 tuple-sret cluster).
Both stages: detect a slice/str tuple-element len() operand and load the
element's .len word directly at BP + element_off + 8, mirroring the
N_IDENT slice arm and the tuple-field-offset walk (element_off sums
preceding element sizes through the type table). Byte-identical asm
(rule 10). The separate tuple-element-read full-header gap is #238; a
leading-scalar mixed-tuple has its own pre-existing sret-layout cs/ww
divergence, filed apart from #235.
Test 903_tuple_elem_slice_len_run: 4 slice/str-only tuple rows (two/
three slices, str+slice, slice+str; distinct lengths), build+run both
drivers + cs==ww byte-id. 12/12 ok.
The STORE-twin of the Fold-B over-cap-tuple sret RECEIVE (a937d67). Fold B
wired single-var-let / destructure / reassign / return-forward to receive a
> 4-eightbyte (sret) tuple-returning call, but a FIELD or INDEXED-lvalue
dest stayed unwired: the store dropped the callee's sret body (a truncated
MOVQ through a stale RDI) — a silent miscompile, gate-blind because the
bootstrap never field-stores a wide tuple.
Per Rob's ruling A (one class, one commit): convert the silent miscompile
into either a CORRECT store or a LOUD stop, never a fall-through.
- cstage cmd/w6c/cgen.c: the struct-field N_DOT store and the N_INDEX
lvalue store each gain an arm keyed on cg_sret_retsize(dest) > 0 &&
rhs == N_CALL. A LOCAL dest (BP-relative, not via_ptr / global) sets
cg_sret_dest_off so the callee's hidden RDI writes the WHOLE tuple
straight into the slot — field: boff + foff; indexed: boff + cidx*esz
(a CONSTANT index into a local value array, the only indexed form whose
dest is a static BP offset). Every other dest fatals "#234-tail".
- wwstage selfhost/cmd/wcc/cgenexpr.ww: symmetric (rule 10). The direct
struct-local field branch sets c.sretdestoff = lc.off + fi.foff; the
via_ptr branch, the global branch, and the N_INDEX arm hard-stop loud
with the same #234-tail diagnostic. The field branches key on
sretretsize(fi.tnode) > 0 (fi.tnode is a real type-AST node). The
N_INDEX arm keys its ENTRY on callsretsize(c, n.rhs) > 0 — the
callee-return-type SSoT (cgenutil.ww) the receive sites use — NOT on
sretretsize(elemtn): elemtn is only a type node for an N_IDENT base, a
VALUE node for an N_DOT base (`s.arr[i]`) / chained (`a[i][k]`), which
fell to sretretsize=0 and let those forms drop SILENTLY through to the
truncating store. The callee return type equals the dest-element type
(checker-guaranteed), so the verdict is byte-identical to cstage's
cg_sret_retsize, and the base-shape split then loud-stops every
non-local-array form, base-kind-independent.
Deferred (#234-tail): a via_ptr field (`p.f`), a global field (`g.f`), an
N_DOT-base index (`s.arr[i]`), a chained index (`a[i][k]`), and a runtime /
slice / pointer index all need a runtime RDI-pointer dest, which
cg_sret_dest_off (BP-relative only) can't express — they hard-error loud
(rule 7), never a truncating store.
Depends on #237 (committed first): the wwstage struct-field slot for a
tuple field is only correctly sized with that fix, so the struct-field arm
is byte-id-symmetric here.
Test 940: indexed-on-local and local-struct-field rows RUN on both stages
(exit 0) AND assert cs==ww byte-id; readback via a raw pointer
(`(&dest):*int; p[i]`) since a tuple-element read `dest.N` is a separate gap
(#238). Builderr rows assert the via_ptr / global / runtime-index /
N_DOT-base / chained-index forms loud-stop with #234-tail on BOTH drivers
(the N_DOT-base + chained rows are the regression witnesses for the wwstage
silent-store gap closed by the callsretsize re-key). The bootstrap exercises
no such store, so the w6c/wwdump combined amalgams regen with no asm change
(byte-id-neutral bootstrap; the new hard-error never fires self-compiling).
The wwstage checker `fieldslotsize` (check.ww) summed each struct field's
SLOT width to stamp the enclosing struct's tinfo.slotsize, but had no
TY_TUPLE arm — a tuple-typed field fell through to the 8B default. So
`struct { f: ([]u8,[]u8) }` stamped slotsize=8 while size=48 (the natural
element sum, correct). A `let s: S` slot is allocated off ti.slotsize
(cgenutil.ww slotsize), so wwstage reserved an 8-byte frame slot for a
48-byte struct: a SILENT stack-corrupting miscompile.
cstage has no size/slotsize split — it sizes the field at f->type->size=48
throughout — so the stages diverged on the emitted frame ($16 wwstage vs
$64 cstage), invisible to a cstage-only check and caught only by cs==ww
byte-id (rule 10).
Add the TY_TUPLE arm (return the tuple's own slotsize, the per-element slot
sum already stamped at the N_TTUPLE arm with slices at 24 each). This
aligns the checker's field-slotsize with cgenutil.ww fieldsize, which
already returns the tuple's natural size (48). The stale comment claiming
"TY_TUPLE inside a struct currently defaults to 8 in cgenutil" is removed —
fieldsize stopped defaulting to 8 at the 2026-05-23 review.
Test 930 pins cs==ww .s byte-id for a struct with a tuple field (with and
without a leading scalar field, foff 0 and !=0); pure frame-size gate, no
runtime — the divergence is fully visible in the emitted assembly. No
selfhost source has a tuple-typed struct field, so the w6c/wwdump combined
amalgams regen with no asm change (byte-id-neutral bootstrap).