The wwstage `arr[i].field` read arm was syntactic where cstage is
type-table-driven: element typing keyed on tnode KINDs (N_TSLICE/
N_TARRAY/N_TPTR) with an N_TNAME element resolved by structlookup
NAME — any base typed via an alias (`type result = []capture`,
p11b/F10) missed every gate and died at the interim C2 loud guard
(pre-C2: fell silently to the SB fallback). Re-key the arm onto the
checker-stamped tinfo (lhs.type_ element / idxbase.type_ base, NAMED
peeled), mirroring cstage cgen.c case N_DOT's N_INDEX-lhs arm 1:1 —
the #209/#211 name-keyed->tinfo-SSoT cluster. Emission sequence is
unchanged; the C2 "C3/task #8" guard retires with the arm wired
(task #37's ptr-chain guard is untouched). Global classification
mirrors cstage let_islet || def_isarraydef via isletvar /
defvartnode-N_TARRAY (the cgplaceaddr C2 pattern).
F7/FA5/FA3-ww/FA6 (non-ident idxbase shapes, task #17's ww halves)
were already closed by C2's read-resolver recursion; p7_composed,
pA5, pA9 run exit-0 byte-id and are pinned as rows here. Task #29's
asserttyped bail (strings.frombytes over a slice-expr in the
composed context) no longer reproduces at HEAD — dissolved during
the C1.25->C2 arc; p7_composed builds clean on w6c_ww, runs 0,
byte-id.
Behind the retired guard three alias-blind NON-cgdot sites surface
(`let l: wlist = []` checker reject / alias-array global emits no
DATA / alias-array arrlit-init under-copies 8B per element, cs!=ww
runtime): filed as task #38, same name-keyed class, separate sites.
test/805: +5 rows — alias-slice field-kind matrix, alias-array +
viaptr-element bases, p11b-essence let-bound alias reads (the
task-#14 `?` factored out), the composed p7 match/compound hot
shape, and the pA9 free()-operand acceptance row (FA6).
Task #8; the last cgen gate before regex tranche B.
A typed depth-2+ field chain behind an index/deref spine
(threads[0].cap.end, (*p)[i].f.g) aborted the cgdot chain walker
(N_IDENT roots only) and fell into the module-qualified-leaf fallback
— a silent `MOVQ <leaf>(SB)` global read of a colliding symbol
(p6min10 exit 66) or a link error. Single-dot reads behind a deref-
index ((*ts)[i].pc, (*p)[i].slicefield) fell into the bottom catch-all
instead, which is offset- and header-blind: offset-0 scalars read
correctly by COINCIDENCE, nonzero offsets and slice headers were
silently wrong in BOTH stages (FA3, pA5). `&threads[0].cap` dropped
the address-of silently and SEGFAULTed at the deref (task #6,
reviewer-A route).
cgplaceaddr (C1) grows an N_IDENT root (local / let / DATA-backed
def) and the N_INDEX base gate relaxes to recursion, so indexed-ident
and deref-rooted spines resolve; enumerated arms still dispatch first,
keeping every pre-C1 shape's asm. case N_DOT routes any TYPED read no
arm matched through the resolver (scalar fldloadop, float X0, str/
slice 3-word header, [N]T address); the module-leaf fallback is gated
to UNTYPED chains, the catch-all to untyped-str pseudo-fields, and
the TK_AMP tail is resolver-or-loud. Leaf kinds without a register
convention (tagged, aggregate) and unaddressable shapes die LOUD
(rule 7). wwstage mirrors symmetrically; two of its arm gaps must not
take the resolver (its sequence differs from cstage's arms — cs!=ww):
ident-indexed alias reads loud-cite C3 (task #8) and non-local-rooted
ptr-chains loud-cite task #37. A third verdict divergence is comment-
documented at the wwstage aggregate gate: cstage's let-init consumes
`let c = (*ts)[i].cap` BEFORE its N_DOT tail (emitting NO copy — the
F5 bug), so that shape cs-builds/ww-louds until the F5 let-copy lands
(task #7); absent from the gate corpus.
806 identroot_dot graduates from BUILD_FAIL: the C2 ident root makes
append(h.xs, v) through *holder resolve via C1.5's place consumer
(run-verified, byte-id). p6min9/p6min10 read-halves are fixed but the
probes stay blocked on the #36 literal under-copy this commit
unmasked (struct-ident field rhs copies 8B; repro filed with the
task).
test/805: +7 rows (typed depth-2 behind ident-index incl the 777
global-collision pin, deref-index, width/float/[N]u8 matrix, FA3
slice-field + .cap-behind-spine, &-route with compound-through-
pointer, C1's reject_tail graduated to stores, neutrality pins) and
+4 reject rows pinning the new loud texts; the C1.25 raw-byte
readbacks graduate to typed depth-2 reads.
Re-key the append() lowering from BP-displacement assumptions onto a
resolver-provided header PLACE (task #15, the add_thread hard-blocker;
cgplaceaddr's third consumer after C1/C1.25). One mirrored choke-point,
two failure modes: cstage 0-defaulted sn_off for any non-ident target,
so 0(BP)/8(BP) became the "slice header" and rt_ensure corrupted the
CALLER frame (SIGSEGV); wwstage cgappend silently emitted nothing
(gate-blind cs!=ww).
cg_append_grow/cg_append_slot (mirror cgappendgrow/cgappendslot) factor
the 5 grow + 5 slot header-access sites. Ident-local targets keep the
legacy BP-disp emission byte-identical (probed across all 9 existing
source shapes, before/after .s). Non-ident targets resolve once through
cgplaceaddr and spill the header address to an @apphdrscr slot:
rt_ensure may realloc .ptr but never moves the header, so the slot
stays valid; every access reloads from it. The slot is allocated fresh
per append SITE, not cached per fn: a nested append-through-pointer
inside a value expression (match-yield arm) spills its own resolve, and
a shared slot would hand the outer grow/slot reloads the inner target's
header — silent cross-slice corruption (pinned by the reentrant_value
row). Indirect mode keys esz/element-kind/load-op off the
checker-stamped target tinfo (no declared tnode behind `*p`; the
#209/#211 discipline). Unwired target places die LOUD "#15: append()
target place unsupported (rule-7)" on BOTH stages — the
silent-corruption class is closed by construction.
The FA4/#35 boundary is unchanged: non-ident spread SOURCES stay loud
(pinned by a reject row). Surfaced pre-existing checker divergence
filed as task #34 (wwstage rejects global slice-lit let).
test/wcc/806: 14 runtime rows (element kinds x target shapes, spread,
narrow-signed spread load, cap-crossing realloc loop with branched
callee + caller-frame sentinels, deref-spine target, nested-append
reentrancy, direct-arm neutrality pin) + 2 exact-text reject rows,
both drivers + per-row cs==ww asm byte-id.
? and ! assume ONE success member end-to-end: the checker collapses
the result to the first non-error variant (check.c tagged_success_type
/ check.ww exprtype) and cgen emits a single tag compare, so any other
success member is silently mistaken for an error — ? propagates it to
the caller (p11h: []capture read back as nomem, exit 21), ! aborts on
it. Until the honest subset-union result typing lands (task #14, harec
check.c:2759-2835), both stages loud-reject |success| > 1 at the
checker choke-points (one per stage), identical diagnostic, both ops
per rob's one-class ruling (#133 precedent). (T|err1|err2) — one
success, many errors — stays legal (925 canary + new accept rows).
F9 rides along (task #12): wwstage scruttype only resolves IDENT/DOT,
so the direct forms f()? is T / match(f()?) / f()! is T slipped its
lenient-miss contract and were silently ACCEPTED where cstage rejects
(cs!=ww, gate-blind). checkisas/checkmatchexhaust now resolve the
try-result via exprtype, keyed on the RESOLVED success type — a named
tagged success ((ab|nomem)? is i32) keeps being accepted, matching
cstage's verdict empirically.
test/wcc/806: 11 rows x dual driver + byte-id accepts (26 fixtures);
reject rows pin exact per-stage diagnostic text; p11h + q_card2_unw
graduated to rejects; call-arg-position reject + void-success accept
pin position-independence and the dominant lib/ (void|err)? shape.
Tasks #5 + #12; #14 lifts both gates together.
Wire struct/array/tuple field STORE through the C1 assign-resolver
(task #23): structlit rhs materialises into a FRESH-per-use @placescr
slot (the @slicescr discipline — a cached slot is the #31 multi-live
trap) then word-copies to the resolved address; addressable rhs
(ident/global/dot/deref) sources via aggarg_srcaddr with the dest
spilled around the dispatch (#270-1b order). Kept loud: compound on
aggregate, sret call rhs (#234-tail), <=24B call rhs (task #24),
unaddressable literal rhs, ww-only anonymous-struct structinfo miss.
test/wcc/805: +6 rows (40B structlit incl ... autofill, ident+deref
source, nested literal, [3]u8 MOVW/MOVB and [3]u32 MOVL tails,
two same-size stores in one fn pinning fresh-per-use) +3 exact-text
reject rows. Tuple-field row blocked by the pre-existing tuple
param/let-init word-2 drops (tasks #32/#33, documented in-row).
p7b_capstore_only graduates byte-id and runs; p7_composed builds and
runs on cstage, wwstage stays behind the pre-existing #29 asserttyped
bail (verified identical at master with w6c_ww).
Task #19 (the #38b residual surfaced by FC2 evidence): a tagged arg
whose slot exceeds the 6-reg convention (>48B) is MEMORY-class per
ref/qbe/amd64/sysv.c:80-85 (inmem) / :411-426 (stack blit). Caller
stages the whole slot below every register-class word (two-phase
push, rightmost-first, leftmost mem arg at 16(BP)); callee registers
the param in place at positive BP offsets with zero prologue bytes;
the merged slot count feeds the existing caller-cleanup ADDQ.
Argument-side mirror of the #38 tagged-sret fix, same classify
machinery (tagged_memarg_size / taggedmemargsize beside their
register-class siblings).
Pre-fix, the exact-typed arg loud-stopped on both stages, but
WIDENING a concrete variant into a >48B param slipped the old guard
silently — cstage pushed one scalar word while wwstage emitted an
uncapped greedy stitch (wrong on both AND cs≠ww, gate-blind). Widen
sources now route through the @tagscr scratch for mem slots.
Loud boundaries kept (rule 7), each with its own diagnostic:
sret-class tagged CALL result as mem-arg source (#40-family
follow-up), global tagged let (task #25, broken at any size
pre-existing), >48B variadic element, and mem-arg + register-
overflow mixing (caller check + callee prologue mirror).
Single commit: caller staging, callee receive, and both stages are
one inseparable ABI class — landing any half alone breaks byte-id
or runtime correctness (the #38 flip precedent); test/929 (15
table-driven rows: 56B/64B slots, widen-slip pin, source shapes,
mixed orders both ways, two-mem call, 200k-call loop, 48B-boundary
absence pin byte-id'd vs master, 5 reject rows pinning the exact
per-guard diagnostic on both stages) rides with it.
(*ts)[i].field = v / OP= v (the regex run_thread hot shape, task #4)
compiled to NOTHING in both stages, byte-identically: the N_DOT lhs
roots at N_UN(STAR), so the arr[i].field arm (idxbase must be IDENT)
and the chained-ptr-field arm (base must be *struct) both miss and
the N_ASSIGN dispatch fell off the switch silently, rhs unevaluated.
cgplaceaddr (one per stage) is ADDRESS COMPUTATION ONLY — N_UN(STAR)
root, N_INDEX hop over a slice/array place (.ptr hop for slice),
N_DOT struct-field hop with one deref for a *struct base. Call-sites
keep their own emission: scalar fldstoreop store, str/slice 3-word
header store staged through DX, 10-op compound template with the
chained-ptr-field register roles. Ident-rooted spines stay with the
enumerated arms — verified asm-neutral over the 84 fold2b probe
sources against fresh master-HEAD binaries (7 diffs = the F6 family
now emitting stores; 2 verdict flips = aggregate-field stores, now
loud).
Silent dispatch tails go LOUD for N_DOT lvalues the resolver can't
address and for unresolved-identifier targets (cstage float-ident arm
aligned to wwstage's resolve-first order). Aggregate-field stores
loud-reject pending the follow-up resolver commit (task #23, ≤24B
N_CALL rhs split to #24). The non-DOT tail stays silent deliberately:
going loud there would asymmetrically surface the pre-existing
str-base element-store divergence — task #22, cited at both sites.
test/805: 17 rows x 2 drivers + 12 cs==ww byte-id fixtures — widths
(incl narrow-compound fldloadop sign/zero-extension), all 10 compound
ops (DIVQ/IDIVQ/SHLQ/SARQ/SHRQ), str + slice 3-word stores, *[N]T
base, runtime call index, ident-base neutrality pins, and 5 reject
rows asserting exact diagnostic text.
Hare's delete(xs[i]) (ref/harec/src/check.c:1981-2027): checker accepts
an N_INDEX over a slice-typed base, stamps void; loud-rejects the range
form delete(xs[i..j]) (stays filed on #35 — regex fold-2b's consumers
are all single-element), non-index operands, array bases, wrong arity.
Lowering (both stages, converged byte-identical by construction):
ascending word-copy loop shifts [i+1..len) down one esz stride, then
hdr.len -= 1; cap unchanged. The move is a same-type whole-stride byte
copy — src and dst are elements of the SAME slice, so no boxing exists
for any element kind; one loop serves scalar/narrow/str/struct/tagged.
esz off the STAMPED base type (#34/#48 discipline). Base shapes: local
slice ident (LEAQ) and deref-of-local ptr-to-slice (MOVQ — the fold-2b
delete_thread shape); others rule-7 loud-stop.
test/804: 38 fixtures — first/middle/last/to-empty, esz 1/4/8/24/56
(MOVB/MOVL tails + 7-qword tagged), cap-unchanged, (*threads)[i], 4
checker reject rows; every accept row cs==ww asm byte-id.
The free(x) builtin lowered to CALL ffi_resolve("free") in cstage and
fell through to a generic CALL free in wwstage (which had no free arm
at all) -- an undefined reference at w6l unless an @symbol decl
happened to be in scope. ww has no free by design (rt/alloc.s:30 --
the bump allocator cannot reclaim a mid-chunk pointer; process exit
does), so both stages now evaluate the operand for side effects
(Hare's free(expr) evaluates expr) and emit nothing else, letting
Hare code that calls free() port verbatim (regex fold-2b calls it at
4+ sites). The 2-arg os.free(p, n) public API is untouched: the
builtin gate requires exactly one bare-ident-callee arg.
930_free_noop_run pins per row: w6c/w6c_ww byte-id, no free symbol
in the .s, deref-after-free validity, and the operand side effect
running once per free() via a global counter.
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).
The header still cited cstage's retired cg_tagscr single cache and
listed the scratch by its pre-#44 name; the grow-fatal mechanism doc
stays (it still guards @retscr et al.) with a pointer to how #44
sidesteps it for the tagged scratch.
The #45 non-ident arm tag-compared the word in AX against the variant
index; for the nullable (*T | void) fold that word IS the pointer —
`h.m is *t` on a non-null pointer answered FALSE (silent cs≠ww,
cstage correct: CMPQ $0 + JE/JNE polarity per cgen.c N_TYPETEST).
The ident path had the same missing nullable arm since before #45
(pre-existing at master, unexercised in the bootstrap corpus). One
nullable branch at the shared compare choke-point closes both halves:
want stays RAW (cstage tests tag == ptr_tag unclamped, a no-match -1
takes the void polarity). Rows nullable_dot_field + nullable_ident
pin both polarities and both states in 927; whole-corpus control
(5 selfhost combined.ww, master-vs-branch w6c + w6c_ww) byte-id.
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.
The cg_widen_tagged_store / cgwidentaggedstorebp struct-payload arm
gated the 3-word {ptr,len,cap} field store on TY_STR only; a slice-
typed field inside a union-payload struct literal fell to the scalar
tail and silently dropped .len/.cap (the #24 gap's widener twin, both
stages symmetric so byte-id gates were blind). Surfaced by #38's
regex-shaped consumer: wide{xs: []u8, ys: []u8, n} widened into
(wide | error | nomem).
A spread whose source was not a local ident fell PAST the spread arm:
cstage continued into the single-value stores with the N_SPREAD node
(garbage store), wwstage silently SKIPPED the value entirely — a silent,
cs!=ww-divergent miscompile (append(ys, f()...): cs exit 0 / ww exit 144,
want 3), reachable for every element kind and predating #34 for scalars.
Both stages now rule-7 loud-stop the shape (deferred, task #37).
Also pins the widener's already-tagged single-value source path
(tagged_ident_src row, i64 + bool members) and adds the
spread_call_loudstop BUILD_FAIL row — test 800 is now 15 rows / 43
fixtures.
shlex.appendstr, getopt.appendoption, bytes.appendslice and
strings.appendstr existed only because the append builtin stored the
first 8 bytes of the element; each carried its own @symbol("rt_ensure")
bind and a grow-then-store-through-*T body, with comments promising to
"collapse in one go when the append builtin is fixed". The previous
commit fixed the builtin; this removes all four helpers and their
rt_ensure binds and spells every call site as plain append().
Bonus correctness: getopt's appendoption passed a hardcoded membsz of
24, stale since the str 24B redesign made option {rune, str} 32B — the
manual growth under-allocated past 6 options while &opts.ptr[i] strode
32 (latent OOB). The builtin derives membsz from the type table
(probe: MOVQ $32, SI), closing that drift by construction.
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.
A one-step `let xs: []T = [e0,e1,..]` had two faults. #31 (silent, cs!=ww):
the #258 array→slice borrow wrapped the un-addressable N_ARRLIT directly as
the N_SLICE base and cgen never spilled it to a stack slot, so .ptr dangled
(`let xs:[]i32=[10,20,30]; xs[1]` returned the un-stored header 1; []u8/[]str
segfaulted). #25 (over-strict): a slice target fell through to the exact-
element type_eq borrow gate, rejecting bare-int-width ([]u8=[1,2,3]) and str
elements the array-init path coerces.
Fix (re-stamp + per-borrow scratch; both stages byte-identical asm):
- Checker re-stamps the slice arrlit as [count]T, reusing the array-init
per-element coercion + range-check (#25): in-range accepts, out-of-range
loud-rejects. cstage arrlit_init_fits gains a TY_SLICE arm; wwstage
checkletassign mirrors it and stashes the synthesized [count]T tnode on
arrlit.lhs (free for N_ARRLIT) so cgen can size the backing NODE-wise
(elemsizeofc) and count from the tnode's .rhs intlit — the arrlit's own
value tinfo carries the literal's untyped element (unsized), so node-first
sizing is required (a cstage/wwstage representation divergence; cstage's
Type IS sized and reads base->type).
- cgen materialises the N_ARRLIT borrow base into a FRESH per-borrow
@slicescr stack slot (distinct slot per borrow: a borrow's backing must
outlive the lowering, so it can't share a cached @aggargscr/@tagscr-style
slot — two live borrows would alias one backing; localalloc/local_alloc
is always-fresh), filled by REUSING the array-init element fill extracted
from the N_LET path (cstage cg_arrlit_fill_bp, wwstage cgarrlitfillbp —
same store sequence the byte-id-green `let a:[N]T=[..]` uses, the
frame-order + store-op guarantee), then LEAQ'd as the base.
Supported ONLY at a `let` init. In call-arg / return / assign position
there is no addressable backing, so both stages LOUD-REJECT ("bind it to a
`let` first") — aligning cstage DOWN to wwstage (which already refused the
untyped arrlit element) per rule-10; this closes#31's silent call-arg
segfault as a compile error. Full non-let support is deferred (#33).
Escape (rule-8 WHY): a `let xs:[]T=[..]; return xs;` returns a slice into a
freed frame slot = dangling, IDENTICAL to the pre-existing named-array
borrow and Hare-consistent (no escape analysis / GC / heap promotion).
Test 953_arrlit_slice_run: 8 accept rows (cstage runtime readback +
cs==ww byte-id, frame-size canary incl.) covering the #31 i32 pin, bare-int→u8
coercion, str readback, the multi-live soundness pin (xs[0]+ys[0]=5, not 8 —
proves fresh-per-borrow), and a mutate-through-borrow proof; 4 reject rows
(out-of-range element + the three non-let contexts, loud in both stages).
Tuple-element slices stay blocked by the pre-existing #30 array-init FATAL.
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.
wwstage over-rejected an inline `let g: []pt = [pt{..}, pt{..}]`. The
N_ARRLIT exprtype arm inferred its element type from the first element,
an N_STRUCTLIT, whose exprtype arm deliberately returns the struct BODY
(N_TSTRUCT) per #66. The array->slice isassignable arm then typeeqast's
the declared element (N_TNAME "pt") against that body and bails on the
TNAME-vs-TSTRUCT kind mismatch -> confident-false -> reject. The reject
is a KIND mismatch, not a nominal-compare weakness: typeeqast is already
streq-keyed for N_TNAME.
Narrow fix: when the first arrlit element is a named struct literal,
capture the NAMED type (mktname) so su.lhs matches the declared N_TNAME
shape, mirroring cstage's element inference. typeeqast and the
N_STRUCTLIT #66 body-return are untouched; non-named elements keep the
existing first-element shape. e.type_ via tinfofornode still resolves
[N]pt for cgen, so cstage/wwstage stay byte-identical.
Test 687 gains struct_pt (sum+len+cap == 14), struct_3f (mixed-width
u8/i64/i32 field offsets == 23), and struct_arrvar_local (the
array-VARIABLE form still accepts+runs; local scope since the
module-scope variable form is the deferred #22 link gap).
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.
The #258 borrow desugar lowers `let s: []T = arr` to a runtime
`arr[0:len]` (N_SLICE over the array base) so the slice header is built
at run time. That is only meaningful for a LOCAL let — a fn body
executes the borrow. A module-level let is static data with no runtime
to run the borrow; its rhs must stay the raw N_ARRLIT so cgen can
materialize it as DATA.
cstage splits this by checker: clet (the desugar site, check.c:1993)
runs only from cstmt (local statements); module-level lets are checked
in check_file pass-2 (check.c:2549) which never desugars. wwstage runs
ONE checkletassign for both — function-body lets via resolvewalk's
post-order walk (a block scope is pushed, c.cur != c.top) and top-level
lets via checkfile pass-2 (no scope pushed, c.cur == c.top). Mirror
cstage's split by gating the desugar call on `c.cur != c.top` (the same
module-scope test as check.ww:184). The #130 module-level assignability
check in checkletassign is untouched.
Without this, a module-level `let g: []u8 = [1u8,2u8,3u8]` reached cgen
as N_SLICE in wwstage but N_ARRLIT in cstage — the cs!=ww shape that
blocked #10 part-a's wwstage data-emission. Locals stay byte-identical
(named-array->slice still works, exit 8 both stages); 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.
The #13 .cap read-fix gated the wwstage CX→AX shuffle on the base's
type being TY_SLICE/TY_STR, on the assumption that a bare string literal
types as untyped_str and so misses the gate (matching cstage, whose
cap-shuffle lives only in the typed pseudo-field branch). That assumption
is false on wwstage: its checker stamps N_STRLIT as `str` (check.ww:2322),
not untyped_str as cstage does (check.c:1079). So `"abc".cap` passed the
TY_STR gate and emitted a stray `MOVQ CX, AX` on wwstage only — while
cstage's untyped catch-all never shuffles it — a rule-10 byte-id break.
A string literal's cgexpr loads only AX=ptr/BX=len (cgen.ww N_STRLIT),
never a CX cap, so the shuffle was garbage on top of divergent. Exclude
N_STRLIT from the gate: `"abc".cap` now returns AX unshuffled on both
stages, byte-identical. The typed `t[i].cap` path (lhs N_INDEX) is
unaffected.
The underlying N_STRLIT type divergence (cstage untyped_str vs wwstage
str) is a separate latent checker issue, filed for follow-up; this commit
keeps the cgen byte-identical regardless.
683: new BYTEID_ONLY row str_lit_cap_symmetry pins the edge (asm-byte-id
asserted; runtime value is a link-time address). 39/39 ok; test-unit
251/251; smoke cs==ww; sizelint clean. combined.ww (w6c + wwdump) regen'd.
`t[i].cap` (t a `[N][]u8` / `[N]str`) miscompiled in BOTH stages,
divergently — the read-side sibling of #20's store fix. cgexpr on the
indexed element leaves the full {ptr,len,cap} header (AX/BX/CX via
cgslicehdr), but the `.cap` field-selector never shuffled CX→AX:
cstage's typed pseudo-field else-branch handled only .ptr/.len, so
`.cap` fell through returning AX=.ptr; wwstage's cgdot non-ident
catch-all likewise handled only .ptr/.len, emitting no read (stale AX).
`t[i].len` already worked (BX→AX shuffle) — only `.cap` was missing.
Fix mirrors the .len shuffle: add the .cap CX→AX arm in both stages.
The shuffle fires ONLY for a typed slice/str base (TY_SLICE/TY_STR
after NAMED-chase); an untyped str literal (`"abc".cap`) leaves only
AX=ptr/BX=len and must return AX unshuffled — keeping the wwstage
catch-all byte-identical with cstage, whose cap-shuffle lives in the
typed branch, not the untyped catch-all.
Validated direct `t[i].cap` (slice + str, elements 0/1) against the
whole-element-copy oracle (`let q=t[i]; q.cap`, made correct by #20),
plus .len-after-index regression pins, in test 683; dual-stage runtime
+ byte-id (36/36 ok). combined.ww regenerated.
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.
Three sibling arms of the #10 global-str/slice INDEX miscompile (23670d7,
the READ path) shared the identical N_TARRAY/N_TPTR tnode-KIND whitelist in
their global-ident resolution arm and were still LIVE and silently cs!=ww:
- cgun `&s[1]` / `&g[1]` (cgenexpr.ww N_INDEX addr-of) — a global str
(tnode N_TNAME) / slice (N_TSLICE) matched neither arm, so esz stayed at
the default 8 and the base fell to the complex-base fallback: a wide
{ptr,len,cap} header + 8-byte stride instead of MOVQ name(SB) (.ptr) +
ADDQ.
- cgassign `g[1] = v` store AND `g[1] OP= v` compound (two arms) — same
whitelist; a global slice store emitted a full-word MOVQ at an 8-byte
stride: an 8-BYTE OUT-OF-BOUNDS WRITE past a 1-byte element (memory
corruption) instead of MOVB at .ptr+1.
cstage (cmd/w6c/cgen.c) is the runtime-correct reference and was already
uniform across all three: esz off idx_eff(base->type)->sub->size and the
base load gated by is_arr (TY_ARRAY -> LEAQ name(SB), every other -> MOVQ
name(SB), since a str/slice's .ptr IS the symbol's first word). Align the
wwstage UP to that, mirroring the just-landed cgindex template (#10): resolve
esz via elemsizeofc with no kind gate, dispatch the base by N_TARRAY ? LEAQ :
MOVQ name(SB). The store/compound arms also resolve elemtn exactly like their
local branch (element node for ARRAY/SLICE/PTR; nil for str so tnodestoreop
picks MOVB) so a global []str store routes to the 3-word header store and the
compound arm's str/slice hard-error still fires.
Close-by-construction: the global element base/stride is now computed off the
resolved type at every wwstage index site — read (cgindex, #10), addr-of
(cgun), store + compound (cgassign) — with no remaining tnode-kind whitelist.
cgslice/cgbaselen already resolved via elemsizeofc.
803_globalidx_run extends from 9 to 18 rows: global str/slice addr-of (read
back through the pointer), global slice store AND compound store `g[i] OP= v`
(the distinct third fixed arm, with adjacent-element addends as the OOB-write
guard on both), a WIDTH>1 signed variant of each (esz=4 stride/store-width pin),
and local addr-of/store regression pins. Runtime (cstage build+run) + cs==ww
byte-id per row. combined.ww embeds (w6c + wwdump) regenerate.
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.
Replace kwlookup's 30-arm streqn if-ladder with two parallel module-level
tables — kwnames: [30]str + kwkinds: [30]tkind — scanned linearly via
strings.compare, and delete the hand-rolled streqn. Rides #18 (module-
level [N]str static-init + relocations) for the kwnames data and #8
([N]enum element sizing) for the kwkinds[i] read, which is itself the
construct that surfaced the #8 elemsizeofc/array-init-store miscompile.
Two parallel arrays rather than a [N]kwent array-of-struct: a str inside
an aggregate element is the filed #18 follow-up. Mirrors the C twin
cmd/wcc/tok.c kwlookup (N=30, linear, no hash). Source re-applied on top
of the #8 cgen fix and regenerated fresh: the wwstage-compiled toktest
now runs correctly (exit 0, no segfault) where pre-#8 it smashed the
frame on the local [N]tkind init store.
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.
The open-coded len-guard + byte-by-byte equality loop in localfind is
exactly strings.compare(ln, name) == 0 (==0 demands equal length AND
equal bytes; no prefix false-match). Matches cstage cgen.c:1669
strcmp(l->name, name) == 0. The @-fallback streq + localfindnode streq
calls are shared-helper calls, left for the wholesale swap (#17).
Regenerated w6c + wwdump combined.ww (the only amalgamations embedding
cgen.ww's localfind).
57 piecewise stderr diagnostics in the checker spelled out
os.write(2, m.ptr, NNu64) with hand-counted byte literals. Replace the
38 hand-counted literal sites and 19 var sites with a tool-local
cerr(m: str) helper that takes .len off the str, eliminating the
off-by-one hazard. All 38 prior hand-counts were already correct, so
stderr is byte-identical. cerr lives in check.ww (not err.ww, which
ports err.c for the dead C-driven path); regen w6c + wwdump
combined.ww.
23-arm top-level if (k == nkind.N_X) dispatch ladder becomes one
switch (k) with an empty-label default case for the AX=0 fallback.
N_RUNELIT stays a separate arm (no float check, unlike cstage's
INTLIT grouping). Not byte-id-neutral (if-chain -> switch); 990-997
cstage==wwstage byte-id is the functional-equivalence gate. 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.
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 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.