Pre-#48 wwstage matchscrutt's N_INDEX arm required ibase.kind ==
N_IDENT; an index over any other base (match (h.xs[i]) = N_INDEX over
N_DOT, the regex fold-2a re.insts[i] shape) returned nil, so cgmatch
dispatched with scrutt=nil — every case arm's variant index clamped to
0 (CMPQ $0) and @match_spill fell to the 16B default. SILENT cs≠ww
runtime-wrong (cstage N_MATCH reads the checker-stamped s->type for
every scrutinee shape, cmd/w6c/cgen.c:7510). The non-ident-base arm now
returns the scrutinee node itself behind an istaggedtype gate — the
stamped-carrier pattern of the #67 N_DOT arm and the #45 cgtypetest
fix — so any base shape resolves the element's tagged tinfo for both
variant indices and spill sizing.
Same-class load half, one commit per the #133-expanded precedent:
cgindex's generic-fallback tagged-element load was the only arm missing
the slot>24 R8 word (both ident arms and cstage cgen.c:9106-9117 have
it), so a >24B-slot element via a non-ident base under-read the cursor
and the now-correctly-sized spill stored stale R8.
928_match_nonident_idx_run pins the repro shape (field-base slice
index, all variants both polarities), the regex shape (56B-slot
inst-like union, payload reads within the 32B cursor per #43), and
ident/array/slice ident-base controls — per row cs==ww byte-id +
runtime via both drivers. w6c/wwdump combined.ww regen'd via canonical
make; selfhost corpus hand-cmp'd cs==ww both stages.
Pre-existing siblings surfaced while probing, NOT folded (rule 11),
reported for filing: (a) cgindex element classification skips N_CALL
bases entirely (mk()[0] — wrong esz + not tagged-classified, cs≠ww,
runtime-wrong, also non-match contexts); (b) `as` on a non-ident
carrier still clamps the variant to 0 (cgtagvariantidx's N_TTAGGED node
gate rejects the stamped carrier; byte-identical to master, the #200
spill fix covered only slot sizing).
Pre-#45 wwstage `is` resolved only N_IDENT scrutinees; xs[i] / p.field
/ call() fell through with scrutoff=0 + scrutt=nil and emitted
MOVQ (BP),AX; CMPQ $0,AX — tag read off the saved-BP word, variant
clamped to 0 (SILENT cs≠ww; cstage cgexprs the scrutinee and compares
the real tag in AX). The non-ident arm now cgexprs the scrutinee (tag
lands in AX) and compares directly. NOT the `as` twin's @asrt_spill
(#200): cmp against cstage shows N_TYPETEST never spills — `as`
re-reads payload words after the check, `is` consumes only the tag,
and a spill would break rule-10 byte-id. Variant index resolves from
the STAMPED scrutinee type via flatvariantidx/flatslicevariantidx
(matchscrutt's node walk can't carry N_DOT through cgtypetest's
N_TTAGGED gate). Ident path untouched (control row + hand-cmp vs
pre-#45 w6c_ww). wwstage-only source change; cs==ww byte-id pinned
per row in 927_is_nonident_run.
A fn mixing two tagged slot sizes smaller-first (regex compile(): 56B
append-element widen then 64B sret return) hit the #15/#26c rule-7
grow-fatal — the single shared per-fn @tagscr is first-use-sized and
its pinned offset can't grow. Key the scratch by slot size instead:
@tagscr<sz>, one first-use-allocated slot per distinct size, all three
sites (widen-store via_outer, widen-push, N_INDEX tagged-element
assign) funnelled through cg_tagscr_slot / tagscradd in both stages.
Single-size fns emit byte-identical asm to pre-fix (control row pinned
+ hand-cmp'd vs master w6c). 736's tagscr_size_grow_fatal fixture
pinned the now-unreachable fatal; converted to a byte-id succ row.
Runtime rows live in 926_tagscr_sizes_run.
A tagged-union RETURN rides a fixed AX(tag)+DX/CX/R8 cursor (TUPLE_GPCAP
eightbytes = 32B slot); wider slots were silently truncated at the
return crossing — payload word 4+ built in the callee frame and died
there, byte-identical on both stages (gate-blind). Blocks regex fold-2a
((regex | error | nomem) = 64B slot).
Classifier: cg_sret_retsize / sretretsize gain a TY_TAGGED arm
(<= TUPLE_GPCAP*8 stays register-ABI — the (str|nomem)/(s3|bool) 32B
boundary class is pinned unchanged byte-for-byte vs master). Callee:
cgreturn writes the slot through *(@sretarg) via the existing widener
non-BP base (bare return stores the void tag); exact-type 'return f();'
rides the #9 sret-forward. Receive: let/assign/discard reuse the
generic #23/#10 sret protocol; the match scrutinee passes its spill
slot as the sret dest (tagged-specific, no tuple precedent).
This could NOT land as a gate-first interim loud-stop (the planned
#38a): lib/errors/errors.ww errno() already returns a 40B
(errors.error) slot in-tree — the cgenstmt.ww-documented #222 latent —
so a bare gate breaks the build. errno graduates to sret here instead;
errnotest pins it at runtime (its cstage run; the wwstage run was
already failing at master via an unrelated pre-existing indirect-call
arg-classification divergence, reported separately) and test/926's
errno-shaped row reads the previously-dropped tail word on both stages.
The unwired cursor consumers of an sret-class call result loud-stop
(rule 7) rather than read a cursor the callee no longer fills:
widening forward/receive ((A|B)->(A|B|C) mem-to-mem tag-remap, filed
#40), ?/!/is/as operands, argument position, and the >48B tagged-arg
class both stages previously mishandled silently. One-class-one-commit
per the #133 carve-out: post-flip those consumers would read AX (now
the dest pointer) as the tag — a gates-trailing commit would leave a
silently-wrong bisect point, so the flip and its gates are not
separable.
test/926: 15 rows — 56B regex-shaped round-trips (literal/local/
assign/match-scrutinee/forward/str-variant/multi-call), 40B repro +
bare-return-void, the errno-shaped tail-read graduation row, 32B
boundary rows pinned register-ABI by asm sentinel, and 3 loud-stop
rows pinned as build failures on both stages.
Both stages lowered the append element store as one sized mov from AX —
correct only for scalars <= 8B. A str/slice element kept only .ptr
(byte-id-blind), a tagged element got its raw payload written into the
tag slot (the #12 pathology, no boxing), a struct element kept only its
first qword. wwstage additionally fed rt_ensure membsz from bare
elemsizeof, whose 8-sentinel under-allocated and mis-strided named
tagged/struct elements (the #8 family; cs!=ww on the SI imm + stride).
Fix, keyed on the DECLARED slice local's element type (cstage
su->sub->size as before; wwstage elemsizeofc off the stamped tnode —
never the value node, the #25/#31 esz=0 trap), applied to both the
single-value and spread bodies (2 arms x 2 stages):
- scalar 1/2/4/8: untouched (u8 asm byte-identical to pre-fix).
- str/slice: AX/BX/CX pushed across rt_ensure, dst in DX (BX holds the
element .len after the pops — the #24 register discipline), 3-word
store.
- tagged: grow first, dst -> BX, box via the #12 widen choke-point
(cg_widen_tagged_store / cgwidentaggedstore via_outer).
- struct: grow first; literal -> dst spilled to per-fn @appendscr
(cached on cstage to mirror wwstage's @-prefix localadd dedup) +
structlit fill DST_PTR_LOCAL; local ident -> word-copy; any other
source shape is a rule-7 loud-stop, never a silent scalar
fall-through. struct-from-call deferred.
- spread: the source element is already a fully-formed T (tag
included), so the wide arm grows first and whole-width word-copies
&items[i] -> dst, recomputing both addresses from the slice headers
after the possibly-reallocating rt_ensure.
The elemsizeofc swap also corrects the named-scalar-alias membsz
(wwstage fed SI=$8 where cstage fed $4); no in-tree consumer appended
to such a slice, so nothing was riding the wrong 8 (lib/selfhost append
sites are all u8).
Test 800_append_wide_elem: 13 rows (runtime readback per kind, 2-append
realloc survival, spread str+tagged, @appendscr dedup, enum-alias esz,
loud-stop build-fail) + per-row cs==ww byte-id, which subsumes the
frame canary.
cg_structlit_fill / cgstructlitfill had a TY_STR arm that stored all
three header words (ptr@+0, len@+8, cap@+16) but no TY_SLICE arm, so a
slice field in a struct literal `cl{ items = b, n = .. }` fell through to
the generic scalar tail and stored only the ptr word — the field's .len
and .cap read 0. str fields (the same 24B {ptr,len,cap} shape) worked;
slice fields silently dropped two words.
Both stages emitted IDENTICAL wrong asm, so the 990-997 byte-id gate was
green on both-wrong; runtime readback is the only correctness net. Same
is_str/is_slice discrimination gap as #10 part-b, here in the
struct-literal field-init path.
A slice is the same 24B header shape as str, so widen the str arm's
guard to TY_STR || TY_SLICE (cstage) / isstrtype || isslicetype
(wwstage) and let a slice ride the already-correct 3-word store. The
TAGGED arm stays ordered before it, so a nullable/tagged slice
(TY_TAGGED) still routes to the widener, not the 3-word store.
Test 689 (table-driven, runtime readback + dual-stage asm byte-id):
slice .len/.cap/.ptr, a scalar field beside/before the slice, a slice at
a non-zero field offset, two slice fields, and a str field beside a
slice (str-arm regression pin). 33/33 ok.
Port of ref/hare/regex/regex.ha fold 1 (the data model). Lands the
full type model — error, the inst_* variants + 10-variant inst union
(the nominally-distinct same-underlying size/void aliases included),
result/capture, charset + items, the regex struct — plus finish().
Test 989_regex_run pins variant discrimination, payload extraction,
struct shapes, and finish() on cstage; w6c == w6c_ww byte-identical.
Two fold-1 constructs are held back behind filed compiler/fidelity
gaps, documented at their sites (regex tasks A–D):
- charclass_map (regex.ha:74-87): const [](str, *fn(rune) bool)
table — blocked on the array-literal->slice element-coercion
checker gap (type.c:402-404 #258 borrow uses exact type_eq,
no element decay). It needs `import ascii;`, so both land with
the consuming fold (compile) once the gap is fixed.
- finish() free()s; ww is a no-free runtime (rt/alloc.s:30), so the
faithful body drops the frees, as the port drops every Hare
free(). Kept as a no-op for API parity.
DEFERRED to later folds: compile()/exec/find/replace.
The `arr[i].field` N_DOT read branch in both stages was gated on a LOCAL
base lookup (cstage `localfind != 0`, wwstage `localfindnode != nil`). A
module-GLOBAL base (`let g: [2]pt = [...]`) missed it:
- cstage fell to a generic index-load that drops f->offset — it read
element[i] at offset 0, so `g[i].b` returned a's value (g[0].b -> 1,
g[1].b -> 3 instead of 2, 4).
- wwstage fell to the module-qualified SB fallback — garbage, no main.g
load at all.
Silent, byte-id-divergent. This is the READ twin of #11 (the global
`g[i] = v` write fix) and the #15 sibling. Local `[N]struct` bases read
correctly (tests 680/681 cover only those), which is why it was never
caught.
Fix (both stages, converged byte-identical): resolve the global the same
way the N_INDEX arm does — cstage `let_islet || def_isarraydef`, wwstage
`letvartnode || defvartnode` — and dispatch the base load by shape: array
-> LEAQ name(SB) (the symbol IS the storage), slice/ptr -> MOVQ name(SB)
(the symbol's first word IS the .ptr). The field then loads at f->offset
exactly as the local arm does. esz (element stride) and f->offset both
come from the type table (rule 13). Mirrors #11's write-side global-base
resolution. combined.ww embeds (w6c + wwdump) regenerate.
688_global_arr_elem_field: global `[2]pt` reads of .a/.b on both elements
(the .b reads are the bug), a non-8-aligned `[2]rec {tag:u8,x:i32,y:i64}`
to stress f->offset + a u8 sub-word leaf, and a slice-base read
(`let g: []rec = arr;`) that exercises the MOVQ-deref .ptr arm. Runtime
(cstage build+run) + cstage==wwstage byte-id per row. The slice row is
byte-id ONLY: its read asm is correct and identical on both stages, but a
slice-of-struct module global does not data-emit a symbol yet (a separate,
pre-existing data-emission gap, sibling of #10/#20), so it cannot link/run.
`let g: []T = [v0, v1, …];` at module scope had no cgen arm: emit_lets /
emitletdataw handled str-lit and array-lit but not slice-lit, so NO
`DATAW main.g` was emitted and BOTH stages failed to link ("undefined
reference to main.g"). byte-id-blind — only the link step exposed it.
emit_slice_data / emitslicedata (parallel to the #18 str-array reloc
helper, generalized to a 24B header + array-backed data):
1. writable backing DATAW "<mangled g>.d" holding the k element bytes,
routed through the emit_array_lit_bytes / emitarraylitbytes choke-
point via a synthesized [k]T (int/float element kinds reduce exactly
as a [N]T global's do);
2. 24B header { ptr-placeholder, LE len, LE cap } (len = cap = k), word
sizes from the type table (ty_uintptr/ty_size, primtypesize) per
rule-13;
3. DATAR g+0 -> backing patches the ptr word.
The backing label's second '.' can't collide with a user global (source
identifiers carry no '.').
New emit_lets / slice arm gated on N_ARRLIT + slice-typed; rides on #18,
which keeps the module-level initializer as N_ARRLIT in both stages.
Aliased-slice spelling (`type S = []T; let g: S = [...]`): cstage
let_isslice already resolves the alias via type_unwrap, but wwstage
letvarisslice keyed only on the syntactic N_TSLICE node — unlike its
siblings letvarisstr/letvarisstruct/letvarisfloat, which all walk the
N_TNAME alias chain. So an aliased-slice global misrouted to the str arm
and never reached emitslicedata, link-failing on wwstage while cstage
emitted correctly (a cs≠ww divergence this fix would otherwise introduce).
letvarisslice now walks the alias chain exactly as letvarisstr does
(align wwstage UP to runtime-correct cstage, the #211 pattern); an alias
of a slice IS a slice. emitslicedata gains the nil/non-slice guard cstage
emit_slice_data already had (rule-10 symmetry; unreachable behind the
gate, guards the su.sub deref).
rule-7 loud-stops, symmetric both stages: read-only `def` slice-literal
(DATAR holder must be DATAW, w6a asm.c:362), `...` repeat (a slice
literal has no target length), and slice-of-{str,slice,tagged} elements
(per-element relocs / #17) — never silent no-emit.
Deferred (filed): struct-element module-level slice-literal surfaces a
separate checker cs!=ww ("let: not assignable" on wwstage, wrong runtime
on cstage) — out of #10's data-emission scope.
Test 687 (table-driven): []u8/[]i64/[]i32 element read-back + len + cap +
1-element edge + aliased-slice-type, dual-stage runtime + asm byte-id,
plus 3 build-fail rows for the loud-stops. selfhost combined.ww
regenerated.
Post-#1, size(str) == size(slice) == 24. emitletdataw's str arm
(~cgen.ww:2074) and slice arm (~cgen.ww:2139) were sequential `if`s
gated on SIZE alone, so a bare 24-byte global matched BOTH and BOTH
fired the no-rhs zero fallback — two `DATAW main.g` rows. cstage
discriminates on type kind (let_isstr/let_isslice, cgen.c:1026/1036)
and emits one; the link+run is correct either way, so the divergence
was byte-id-visible only.
Gate the two arms on the declared type kind via the new letdeclkind
helper (d.lhs.type_, TY_NAMED-peeled — the resolvewalk-stamped
type-expression node), mutually exclusive: a 24B global now hits one
arm. Falls to the str arm when unstamped, where the zero-init bytes are
identical, so byte-id holds for that case too.
cstage already correct — no change. New test 686 (5 runtime rows + 5
byte-id rows) pins single-emit + cs==ww.
A [N]tagged-union array-literal element fell through the is_agg
multi-word-copy path (STRUCT/ARRAY/TUPLE/str/slice only) to the scalar
1-word store: the raw value landed in word 0 (the tag slot) with no tag
written and no payload boxed, so a later match found no variant. Both
stages under-copied identically, so the copy-depth bug was byte-id-blind
— a stride-only fix would still store 1 word and pass the gate green on
both-wrong.
Route each tagged element through cg_widen_tagged_store / the N_LET "BP"
tagged-store wrapper — the same choke-point let-init, vararg gather and
struct-field stores already use — so boxing, tag-remap and zero-pad-to-
slot come for free. esz now comes from the stamped slot size (rule-13);
the wwstage narrow override only covered widths 1/2/4, leaving a 16/24B
tagged element on the wrong 8-byte sentinel stride. rule-7 loud-stops
the unwired `[N]tagged=[x...]` repeat-fill (the widen call consumes the
node and trashes AX).
test/wcc/685: table-driven runtime readback (106/42/13) + a build-fail
row for the repeat-fill loud-stop, both stages.
`[_]T = [...]` (canonical Hare array-length inference) silently
miscompiled to a zero-length array: the parser already left the array
type's length child nil as the infer sentinel — distinct from an
explicit [N] — but neither checker stamped the real count, so `len(x)`
returned 0 with no diagnostic (rule-7 silent miscompile). Module-level
was worse on wwstage, where `x.len` on ANY global array (even an
explicit [N]) fell to the SB fallback and mis-emitted `MOVQ len(SB), AX`
(linker: undefined reference to len).
The length lives in the stamped TYPE and cgen already keys stride /
length / data-emission off it, so stamping the inferred count at the one
checker inference point closes it permanently (rob's #7 ruling):
- check.c clet + module-level N_LET pass-2: count the initializer's
elements and patch the array type's length (the Sym too, so a later
x.len reads the inferred alen). No-init / non-array init can't infer
-> loud error, never a silent zero-length array.
- check.ww inferarraylen: the wwstage twin — stamp a synthesized
N_INTLIT length child before resolvewalk caches the array tinfo;
same loud-error rule. Idempotent for the module-level double-call.
- cgenexpr.ww cgdot: the missing wwstage arm for a top-level [N]T
global's .len / .ptr (cstage cgen.c:8011 already had it).
- cgenutil.ww letslotsize: drop the now-redundant [_] slot-size
intercept — a workaround for this very bug; the stamped length flows
through the general slotsize path (rule 7).
Both stages converge byte-identical; new table-driven test 684 covers
[_]int/[_]str/[_]u8 local + module-level, len + element read-back,
dual-stage runtime + asm byte-id, plus three negative no-infer rows.
A `let t: [N][]u8 = [a, b]` / `[N]str` literal init lowered each
element's {ptr,len,cap} header into AX/BX/CX (cgexpr) but stored only
some words: a slice element fell through to the scalar 1-word MOVQ
(dropping .len AND .cap), a str element stored 2 words (dropping .cap,
latent). Each element is 24B (post-#1) and must be copied whole.
wwstage was worse — a slice element matched no esz branch, so esz
stayed the 8 sentinel: the per-element stride collapsed (element i+1
overwrote element i's tail), the -96-vs-80 cs!=ww frame divergence.
This is the str/slice arm of the #270 aggregate-element-store family.
struct/array/tuple already copy correctly via the #270-1c is_agg
multi-word path; str/slice were the documented follow-up (cgen.c:9037,
cgenstmt.ww deferral). They can't join is_agg (that path word-copies
from a source slot and rejects non-ident/structlit elements, whereas
str/slice elements are commonly exprs cgexpr lowers into registers) —
the correct mechanism is the existing register header store, extended.
Fix (BOTH stages, converged byte-identical): cstage adds
is_slice_el = type_isslice(esub) and stores 3 words (incl CX->base+16,
the cap) for `is_str_el || is_slice_el`, in the main loop and the
repeat-fill. wwstage adds isslicel (esubti.kind == TY_SLICE -> esz =
esubti.size, fixing the stride) and the matching 3-word store. Closes
[N][]u8 (the bug) and the latent [N]str cap-drop in one branch.
The latent str cap-drop is now stored, but the indexed-element `.cap`
READ (`t[i].cap`) stays broken — a distinct cgindex/dot-selector bug,
cs!=ww divergent, filed as task #13. The new test validates the stored
cap via a whole-element copy (`let q = t[i]; q.cap`), which reads
through the correct ident-load path. [N]tagged literal init is the
remaining sibling (is_agg excludes TY_TAGGED), task #12.
Test 683_arr_strslice_elem: table-driven, dual-stage runtime + asm
byte-id; slice/str .len, 3-element stride-24, cap-via-copy, .ptr deref,
plus a [N]struct regression pin proving the is_agg path is untouched.
Indexing a GLOBAL `str` or GLOBAL slice (`s[i]` / `g[i]` where s/g are
module-level lets) read a wide {ptr,len,cap} header with an 8-byte stride
and a full-word MOVQ load instead of the .ptr + element-width load. So
`s[1]` over a global str read 8 bytes at ptr+8 rather than the single byte
at ptr+1 (cstage emits MOVZBQ). LOCAL str/slice index was already clean.
Root: wwstage cgindex (selfhost/cmd/wcc/cgenexpr.ww) dispatched the element
size + base-materialisation off the base tnode KIND, enumerating only
N_TARRAY (global `[N]T`) and N_TPTR (global `*T`). A global str (tnode
N_TNAME "str") and a global slice (N_TSLICE) matched NEITHER arm, so esz
stayed at the default 8 and the base fell through to the wide-header
fallback. cstage `case N_INDEX:` (cmd/w6c/cgen.c) dispatches esz off the
RESOLVED base type (`idx_eff(lhs->type)->sub->size`), uniform across
local/global/str/slice/ptr.
Fix aligns cgindex's global-resolution arm UP to cstage's uniform type-
driven dispatch — the same template the sister fn cgslice already uses:
resolve esz via elemsizeofc(c, tn) with no kind gate, then drive the base
load by tn.kind == N_TARRAY ? LEAQ : MOVQ name(SB). A global str/slice now
resolves esz=1 off the type table (elemsizeofc, just fixed in #8 to read
stamped tinfo) and routes through the EXISTING isglobalptr emission
(MOVQ name(SB),BX; ADDQ; MOVZBQ (BX),AX) — byte-identical to cstage. The
element-kind flags (elemisstr/elemisslice) for a global `[]str`/`[][]u8`
element are still set by the downstream block, so those route to cgslicehdr
unchanged.
Close-by-construction: cgindex's one global-ident resolution arm is the
single site computing a global element base for the read-index path (the
&arr[i] address-of in cgun and the arr[i]=v store in cgassign are separate
node paths, out of scope). Any indexable global base now resolves esz off
the type table, exactly like cstage and like cgslice.
combined.ww embeds regenerate (w6c + wwdump). New 803_globalidx_run pins
runtime (cstage build+run) + cs==ww byte-id across global str index
(positions 0/1/2 + sum), global slice index (TEXT-only byte-id — a bare
`let g: []u8;` decl emits a divergent zero-header DATAW orthogonal to the
index read, the #7/#18 static-init family), and local str/slice/array
index regression pins. A stride-8 regression re-fails the 5 global rows.
wwstage sized a named-enum array element (`[N]tk`, tk = enum i32) as a
raw 8-byte slot instead of its i32 backing (4), via two sibling code
paths that both derived the element width structurally and missed the
enum's underlying size:
- elemsizeofc (cgenutil.ww) was the odd-one-out among the elem*c
helpers: elemissignedc/elemisfloatc already read the checker-stamped
tinfo (t.type_.sub), but elemsizeofc went elemsizeof->primsize->
slotsize, and primsize("tk")=0 fell through to 8. This drove the
cgindex READ: `a[i]` strode by 8 (MOVQ) where cstage strode by 4
(MOVSXD), reading the wrong/out-of-bounds element for i>=1.
- the array-literal init STORE (cgenstmt.ww) computed its own esz the
same way (primsize=0 -> stayed at the 8 sentinel, enum is not an
aggregate), so a local `[N]enum` literal stored at stride 8 into a
stride-4 frame slot, overrunning it and smashing the saved BP /
return addr -> wwstage-built binary SEGFAULTED.
Both align UP to cstage, which reads the stamped element size uniformly
(N_INDEX idx_eff(bt)->sub->size; N_LET array-init lu->sub->size,
cgen.c:6387). The read fix brings all four elem*c helpers onto the same
tinfo SSoT; the store fix takes the stamped element size for a narrow
scalar. Closing both close-by-construction at the size source.
No in-tree [N]enum / aliased-narrow element existed before kwtab, so
this was byte-id-gate-blind until now. test/wcc/682_arr_enum_elem.c
pins it table-driven: global+local reads, local init-store, signed
sign-extend, and a frame-smash row, each run through both stages with
exit-code and cstage==wwstage asm-byte-id checks.
`len(xs[i])` over a [N]str/[]str (and []T slice) element returned the
element's .ptr, not its length, on BOTH stages (shared gap, not rule-10):
the len() builtin had no N_INDEX arm, so it fell to the bare-cgexpr
fallback, where the N_INDEX str/slice load (cgslicehdr) leaves AX=.ptr,
BX=.len, CX=.cap — and len() returned AX (the ptr) as the length.
Add an N_INDEX arm gated on a (TY_SLICE||TY_STR) element in both stages:
cgexpr the element, then MOVQ BX,AX to shuffle the len word into the
result reg — the same shape as the #14 .len pseudo-field fix. Byte-id
neutral (no bootstrap source uses len(indexed-element)); regenerated
w6c + wwdump combined.ww. New 802_lenidx_run pins runtime + cs==ww.
A module-level `let xs: [N]str = ["a","b",...];` static init emitted no
.data: a str element carries a ptr->rodata relocation, not just bytes, so
it fell through the byte-only array-emit path and left the table symbol
undefined (w6l: undefined reference). Shared gap on both stages, not
rule-10.
emit_strarray_data / emitstrarraydata apply the scalar-str-global pattern
per element at offset idx*esz: a DATAW row of {0-ptr placeholder, LE len,
cap} plus a per-element DATAR sym+idx*esz,_S_n reloc. let_pre_intern /
letpreintern pre-intern each element strlit so the _S_ rodata rows precede
the DATAR references. Stride routes through etype->size (rule 13). Scoped
to the DATAW (`let`) directive: A_DATAR requires a DATAW holder, so
`def [N]str` and str-in-aggregate stay a filed follow-up.
919_strarray_static_run pins runtime (len-sum, element .ptr deref, var
index, empty slot, repeat suffix) + cs==ww byte-id. w6c + wwdump
combined.ww regenerated.
A string literal is TY_UNTYPED_STR, not TY_STR, so `"abc".len` missed
the typed slice/str pseudo-field gate in cgen.c's N_DOT and fell to the
final base-eval fallback, which left AX=.ptr — `.len` returned the
pointer instead of the length. wwstage's cgdot catch-all already did the
BX->AX shuffle, so the two stages diverged (rule-10). Align cstage UP:
the N_DOT fallback emits MOVQ BX,AX for `.len`. `.ptr` is unchanged
(already returned AX); `.cap` deliberately not added (wwstage catch-all
is ptr/len only — mirror exactly).
byte-id was blind here: no bootstrap source uses literal `.len` (lengths
are hardcoded around literals), so the gate never exercised it. New test
801 pins both dimensions (cstage run + cs==ww byte-id) over
len/empty/multibyte/ptr-deref/arg-passthrough rows.
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).
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 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.
Port of ref/hare/crypto/sha256/sha256.ha — block-processed [64]u8
chunks, u32 modular arithmetic, hash::hash + io.writer surface. The
state embeds hash.hash (inline vtable at offset 0); the vtable + sum/
reset slots are wired post-construction (base64/memio convention).
u32 WRAPPING + vtable dispatch CONFIRMED CLEAN: all NIST vectors verify
byte-identical — empty, "abc", the 56-byte block-boundary case, and the
one-million-'a' multi-block stream (1000-byte chunks across many blocks,
stressing write()'s partial-block carry). cgen truncates u32 add/shift/
rotate to 32 bits correctly; no masking workaround needed.
Semantics-preserving spelling divergences (slice-copy as byte loops,
close()/digest loops) are noted at-site per CLAUDE.md rule 5/13.
ONE BEHAVIORAL DIVERGENCE, blocked on a cgen bug (flagged for ken/drew):
Hare's sum() snapshots the state (`let copy = *h`) so it is re-entrant.
That deref-copy of an array-containing struct miscompiles in ww cgen
(copied array fields come back zeroed). So sum() runs on the live state
and is SINGLE-SHOT until the cgen fix lands; every current caller does
one terminal sum(), so the digests are unaffected. Minimal repro:
type t = struct { h: [4]u32 };
let c: t = *(&s); // c.h reads back wrong
A sibling bug (array return-by-value zeroes the result) was also found
and is avoided in the test's buffer-based helper. Both filed for ken.
The hash/crypto modules are dead-imported (no selfhost combined.ww
regen). 9xx test numbers are full, so the run-test shares the 989
prefix with siphash (distinct `short` name; 949_* multi-file precedent).
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).
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.
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.
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.
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).
w6a/w6l/ww transitively import strings (-> bytes, encoding.utf8) yet
their _ww targets listed only os/rt/time, so `make` left their canonical
binaries stale when a transitive lib source changed. w6c_ww/wwdump_ww
already list the full closure; mirror it. Surfaced by 995_self_rebuild
diverging on a bytes.ww edit (canonical not rebuilt, live rebuild was).
Test-speed "immediate wins" from task #19 (build/test-infra only, no
compiler/cgen change — byte-id-neutral; all 237 pass, 950/990-997 +
combined_ww_fresh unchanged).
#1 Parallel build + ccache. MAKEFLAGS += -j$(NPROC) by default: the
C-compile DAG and the five wwstage builds write disjoint outputs (each
.o distinct; each wwstage tool's side files land at its own
selfhost/cmd/<tool>/main.* stem), so -j is order-independent.
test/run's Phase-2 byte-id gates are a single serial recipe that -j
does not reach. CC is wrapped with ccache when present (content-
addressed, byte-identical to plain cc); falls back to bare $(CC).
#2 Kill 990's duplicate ww1->ww2 compile. probe_ww1_to_ww2 recompiled
main.combined.ww (~70s) to assert the ww2 binary is executable — but
probe_bootstrap_fixed_point already compiles ww1->ww2, assembles,
links, and *runs* ww2 to produce ww3, so executability is proven and
the byte-id assertions (ww2.s==ww3.s, ww2==ww3) are untouched. Drop
the redundant probe. make test ~8:30 -> 7:39.
Add `make smoke [FIXTURE=x.ww]`: inner-loop cross-stage byte-id check
(cstage w6c vs wwstage w6c_ww .s diff) on a small self-contained
fixture, seconds. Catches cs!=ww emission divergence per fold; NOT a
substitute for the full 990-997 gate before landing a cgen/ABI fold.
Fold A made the CALLEE emit an over-capacity tuple return (> 4 GP or > 2
SSE eightbytes) via sret, but every receive site stayed loud-stopped, so
such a fn was not yet usefully callable. Fold B wires the call/receive end
by aligning every receive gate UP to the shared cg_sret_retsize() /
callsretsize() > 0 predicate (never a kind), per Rob's (B) ruling:
- single-var-let `let t = f();` cstage gate generalised from
TY_STRUCT&&>24 to cg_sret_retsize(lt)>0; the let's slot IS the
sret dest, the callee writes the whole tuple there, t.0/t.1 read
by offset. wwstage already keyed callsretsize (verified).
- N_ASSIGN-ident `t = f();` same generalisation; global arm
kept TY_STRUCT-only (a tuple-global has no sret-to-symbol path in
either stage). wwstage grows a tuple-local arm (rettupleof gates
it apart from the >24B-struct recv, which keeps its own path).
- destructure `let (a,b) = f();` and `a,b = f();` — the genuinely
new wiring: the callee sret's into the @sretscr discard slot, then
a copy-out loop moves each element to its binding at the SAME
packed offset the SEND wrote (foff += element size), each at its
natural width (#169); a `_` binding skips its store but advances
foff. Both stages, byte-identical.
- return-forward `return f();` cstage forward gate generalised
to the predicate, reusing cg_sret_forward verbatim. wwstage
already keyed sretretsize (verified).
The escape boundary stays loud: arg-pass `g(f())` fatals identically in
both stages (tuple arg exceeds return-cursor ABI capacity).
Test 799 is the runtime net Fold A deferred (byte-id is blind to a
SEND/RECEIVE layout mismatch): the bytes.cut-shaped ([]u8,[]u8) round-trip
over destructure / single-var-let / reassign / return-forward, each both
RUN under cstage and asserted cs==ww byte-identical. Tests 945 (row F)
and 956 (f64x3) flip from asserting the old over-cap loud-stop to
asserting the now-working sret round-trip. combined.ww amalgams (w6c +
wwdump embed the wcc cgen) regenerated. Unblocks #4 bytes.cut/rcut.
A tuple return whose SysV register-return footprint exceeds the caps
(> 4 integer eightbytes or > 2 SSE eightbytes) previously LOUD-STOPPED
at the N_RETURN SEND. Fold A makes the CALLEE emit such a return through
the existing >24B-struct sret skeleton:
- classifier (cg_sret_retsize / sretretsize) grows a TY_TUPLE arm:
walk the element footprint over the SAME caps the SEND uses, and
return the tuple's natural total size (type table) when over-cap,
else 0. The gp/sse caps are factored to a single shared SSoT
(TUPLE_GPCAP / TUPLE_SSECAP — cgen.c macros in cstage, cgen.ww defs
in wwstage) consumed by the classifier AND every emit/receive site
(the SEND, the destructure guards, the cgcall arg guard) — so
classify and emit can't disagree in either stage.
- the SEND replaces the loud-stop with a write-through: cgexpr each
element, store it through *(@sretarg) at its packed layout offset
(the t.0/t.1 positional layout), each at its natural width so a
narrow tail stores MOVL/MOVB not an over-MOVQ (#169); the dest base
reloads into DX each step since a wide element clobbers AX/BX/CX.
Then the existing struct-sret epilogue (MOVQ @sretarg->AX; ret).
- the prologue already wires @sretarg when the classifier is nonzero.
The CALL/receive side is deliberately untouched: the N_MLET/N_MASSIGN
destructure loud-stops stay, so an over-cap tuple return is not yet
usefully callable. The end-to-end round-trip arrives with Fold B (#10-B).
Symmetric cstage (cmd/w6c/cgen.c) + wwstage (cgen.ww / cgenstmt.ww /
cgenutil.ww); combined.ww amalgams regenerated. Test 798 asserts the
callee now COMPILES (no loud-stop) and w6c vs w6c_ww .s byte-identical
across all-wide, str, narrow-tail, and float-over-cap shapes; no runtime
row (uncallable until Fold B). All 236 pass incl. 990-997 byte-id.
len(str-or-slice-global) was wrong in both stages, differently. cstage's
len() arm did a BP-relative slot load; localfind returns 0 for a global,
so it emitted `MOVQ 8(BP),AX` — a bogus stack slot. wwstage's arm only
handled locals; a global fell through to cgexpr, which loads the whole
header and leaves AX=.ptr, not .len.
Both stages now emit the global .len load — LEAQ name(SB),CX; MOVQ
8(CX),AX (.len field; header is ptr@0/len@8/cap@16). The LEAQ symbol
routes through the post-#1 value mangle (cstage mahint c->cur_mod,
wwstage emitsymnamehint c.curmod), not a raw name, so a private
same-module same-leaf str global can't re-open the #1 collision.
The local-str case is unchanged (control). Slice-global rows wait on
#233 (cstage rejects `let g: []u8 = [...]` init); the str global proves
the path. Byte-id-blind, so a committed runtime + cs==ww test (797) is
the net.
The cross-module dotted value-global read (`aa.v`) and addr-of (`&aa.v`)
still mangled their symbol via the non-preferring leaf lookup (cstage
masym / wwstage emitsymname), so they emitted `LEAQ main.v(SB)` — the
WRONG module's same-leaf global — returning 99 instead of 7. #1 fixed the
DATA def-site and the bare-ident load; these four dotted LOAD/addr sites
were the residual.
Thread the dotted module name (the `m` in `m.x`) — n->lhs->str /
opnd->lhs->str / lhs.str / basenm — into the existing value mangle
(cstage mahint, wwstage emitsymnamehint), the same polarity the TY_FN
branch beside each site already uses via mafn/emitfnname. The addr-of
spine-walk for a bare-root `&global.field` is a different shape and is
left untouched.
Byte-id-blind (the bootstrap has no colliding leaves), so a committed
runtime + cs==ww test (796) is the net.
Port ref/hare/ascii/string.ha strlower/strupper as the allocating entry
points: byte-wise ASCII case fold, equivalent to Hare's rune fold since
case-folding only touches bytes <0x80 and every UTF-8 multibyte byte is
>=0x80 (passes through unchanged, length-preserving). nomem arises only
from the allocation's `?`.
strlower_buf/strupper_buf are deferred: ww has no nomem-value form or
capacity-bounded static-append to express Hare's too-small-buffer path
(#230); restore the two-tier delegation when those land.
Divergence (rule 7): the empty-input fast path returns a nil/0 str
because ww's alloc([], 0) routes through nomem, whereas Hare allocs a
zero-length buffer and zero-loops; documented at the bypass site.
Test vectors mirror Hare's @test (ABC/abc/[[[/こ/empty/aB1z). Adds
lib/ascii/asciitest.ww + test/wcc/904_ascii_run.c (registered in the
Makefile TESTS list and a build rule). Regenerates the ascii-embedding
selfhost combined.ww amalgams (#110 freshness); the wwdump amalgam also
reorders the ascii block after strings to satisfy the new import edge.
A bare cross-module value-global load mis-qualified its symbol: cgen
mangled it with curmod via a non-preferring leaf lookup, so an exported
`let v` in module aa emitted both its DATA storage AND its bare-load as
main.v, colliding with main's private v. aa.getv() returned 99, not 7.
Functions were already correct (they thread a cur_mod hint via mafn /
emitfnname); value-globals did not. Both stages emitted IDENTICAL wrong
asm, so the byte-id gate was blind to it; combined.ww (frontend) is clean
-- the bug is purely in cgen. This is the cgen residual of #55 (#1 cgen
value-global module-qualifier).
Fix, symmetric in cmd/w6c/cgen.c + selfhost/cmd/wcc/{cgen,cgenexpr}.ww:
reference-site mangle uses the resolved module (curmod-prefer for bare
idents); definition/DATA-site mangle uses the decl's own module
(d->module / d.nmod) -- threaded per-site the way fns already do, via
mahint / emitsymnamehint. The fn-mangle path is left byte-for-byte
untouched.
Deviation from the signed-off spec (ratified by rob-pike after this
finding): the spec prescribed reusing the fn lookup (mod_mangle_fn /
modlookupforfn), but its first-match fallback mis-fires for value-
globals -- mod_collect export-skips exported non-fn decls (cgen.c:1059)
to keep their bare-name data ABI, so an exported leaf is absent from the
module map and the fallback grabs another module's same-leaf private
global. The value path therefore uses a distinct exact-(name,module)-or-
bare lookup (mod_lookup_value / modlookupvalue): mangle only on an exact
match, else stay bare. Byte-id-neutral on all existing single-owner code;
exported globals stay bare (ABI preserved), private stay module-qualified.
Honest boundary (rule 7): if two modules BOTH export the same value leaf,
both stay bare and the linker sees a duplicate symbol -- a correct, loud,
link-time ABI clash (like C), NOT a silent miscompile; left to the
linker, not papered over with a cgen heuristic.
Test: test/wcc/795_xmod_valglobal_run.c -- runtime (the exported global
read returns its own value, not the colliding private one) + cs==ww
byte-id, across i32-let / def-const / f64-let. Sibling to the checker
test 794_xmod_ident_prefer, which deliberately omitted byte-id because
this cgen bug diverged the asm independently.
exprtype's N_IDENT branch resolved a bare value-ident through the
flat-scope scopelookup, which bucket-walks and returns whichever
same-leaf symbol heads the bucket (the last-registered one). Under a
foreign curmod that binds a same-named symbol from the wrong module
and drags in its declaration's type: resolving `read` to io.read while
checking os pulled io.read's (size|eof|error) return node, whose bare
`error` then bound strconv.error instead of io.error. The mistyped
union variant made the tagged-tag remap's flatvariantidxt return -1
(correctly: the union held the wrong type), collapsing the tag to 0 —
the #226 fmt cs/ww asm divergence.
Resolve through scopelookupprefer(c.cur, c.curmod, e.str), preferring
the current module, mirroring cstage cmd/wcc/check.c:66
scope_lookup_prefer. Sibling bare-leaf sites already migrated: #56
(N_CALL callee), #53 (bare TNAME).
#226 is thereby an instance of #55, not a nominal-identity gap:
io.error is already a sound sym-cached singleton. The remaining
bare-leaf sites (N_DOT-callee leaf, varianterr, scruttype) and the
cgen-side cgident analogue are tracked separately. fmt's 777/780/781
stay STAGE_CS pending a separate spread-union residual.
test/wcc/794: cross-module bare-leaf value-ident, reject->accept
polarity (w6c_ww must accept the cstage-emitted combined); no byte-id
assertion as the minimal value-ident also trips the open cgen-side
cgident bug.
rhstaggedabicall keyed the tagged-vs-scalar call-source decision off the
callee result type looked up by leaf NAME (fnretlookupmod), with the
receiver variable used as the "module". A value-receiver fn-ptr field
call s.f(...) whose leaf collides with a same-named global fn then
mis-bound the global's register shape, so the source was misclassified
as scalar and widened wrong: silent cs/ww asm divergence and wrong
runtime. Read the checker-stamped N_CALL result type (src.type_)
instead, mirroring the N_DOT sister branch. cstage already reads u->ret
off the typed callee (cmd/wcc/check.c:1490) and harec selects by
interned type id, not name (ref/harec/src/types.c:714).
Graduates test/wcc/782 to STAGE_WW + byte_id.
cg_widen_tagged_store (cmd/w6c/cgen.c) and the wwstage twin cgwidentaggedstorebp (selfhost/cmd/wcc/cgenutil.ww) wrote only the tag (slot+0) and value (slot+8) in their scalar and float arms, leaving the high pad words (slot+16..sz) as stack garbage on the BP/let/assign/return-scratch path, which never pre-zeroes. A passthrough return or u8-reinterpret of a narrow scalar/float widened into a >16B union (fmt's field = (...formattable | *mods) is 32B via the str variant) then read that garbage. Both stages were wrong identically, so the byte-id gates stayed green while the runtime truncated; fmt's spread-union scalar widen is the first real consumer. Both arms now tail-zero slot+16..sz (gated size>16), mirroring the tagged-subset/struct tail-zeros and keeping the stages byte-identical (rule 10). Adds runtime test 793; regenerates w6c/wwdump combined.ww. fmt byte-id graduation still awaits the other residual, #226 (io.read nominal-remap).
The wwstage checker walked a match's raw AST variant list and never expanded a ...inner spread variant, so it rejected fmt's match over field = (...formattable | *mods) ('not a variant of scrutinee'). cstage's resolve_type flattens the spread at type-build. Mirror that in the AST exhaustiveness walk (casevariantin + a recursive checkvariantcovered): when a variant resolves to N_TTAGGED via a spread, recurse into its members. Additive + spread-gated -- typeeqast / casevariantpairmatch (#13) / casecovers untouched, so non-spread matches and 990-997 byte-id are unaffected. Also size a spread N_TTAGGED off each flattened member (mirror cstage check.c), dropping the inner union tag word (field 40B to 32B). Closes the #209 CHECKER reject; full fmt-byte-id still awaits cgen cluster #226 (io.read nominal-remap) + #227 (spread-widen ABI), so fmt tests stay cstage-only with retargeted comments. Adds test 792; regenerates w6c/wwdump combined.ww.
Graduates the fmt fprint family (fprint/fprintf/fprintln/fprintfln + internal putbytes/writeone/format*) from io.stream to io.handle, so a file (fd) prints directly through io.write's file-arm (commit-1). Removes the fdsink placeholder -- the fake-stream-vtable-over-os.write shim that stood in for the missing handle. The 8 stdio wrappers route over os.STD{OUT,ERR}_FILENO (new i32 filenos in lib/os; os is the import floor, so it can't hold an io.file-typed handle like Hare's os::stdout_file -- consumers cast i32 to io.file). Migrates the fd-shim sentinel tests 777/780/781 to fprint-over-handle as their headers designed, cstage-only per the pre-existing #209 (fmt is wwstage-uncompilable). Regenerates the 6 os-embedding combined.ww.
Ports ref/hare/io handle.ha: a handle is (file | *stream); ww stream is already *vtable (#94 collapse) so the payload is (file | stream). file=i32 (Hare int is 32-bit, ww int is 8B word -- width-faithful, USER-ruled). read/write/close/seek/tell match on the handle: file-arm to os.read/write/close/lseek (os plays Hare sys role), stream-arm to the unchanged st_* vtable bodies; seeker is the 4th vtable slot (copier deferred). On a file-arm syscall error the stub returns errors.unsupported with a #199b marker -- faithful errno to io.error needs io.error to spread ...errors.error (#204/#199b-blocked); only the error value is lossy until then, the type stays faithful. Regenerates w6c/wwdump combined.ww.