The lit/match appends went through a let-temp; the direct Hare
spelling append(insts, (r: inst_lit)) compiles and runs correctly
(probed at the real 48B-payload inst shape), so the temps were an
undocumented reshape. Void variants (skip/any) keep the typed let —
a bare type name is a symbol ref in ww — now documented at-site.
The bare-slice-decl zeroing cite pointed at shlex.ww:215, which
zeroes its header EXPLICITLY and so proves nothing; the real
mechanism is cgen.c:9836's no-rhs multi-word composite zero-fill.
Ports ref/hare/regex/regex.ha:227-263 literal arms: leading unanchored
inst_skip, inst_lit / inst_any, epilogue inst_match(false). Every
deferred metacharacter arm returns a loud not-yet-ported error (the
fold boundary); state serving only deferred arms drops with them.
Hare free()/defer-if cleanup omitted (no-free runtime, #27); bare
append per #36. 4 new @test rows pin the emitted programs incl. the
empty-input and loud-boundary cases; compile()'s >24B tagged return
doubles as a #38 sret consumer. Rides #34/#38/#44/#45/#48 — all five
fold-2a blockers now closed on master.
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.
Buildfail rows previously accepted ANY non-zero compiler exit — an
unrelated checker error would masquerade as loud-stop coverage; now
the stderr must carry the #38b/#40 marker on BOTH stages. New rows:
wide_discard_stmt (generic @sretscr discard, frame canary) and four
loud-stop shapes verified by probe during review — `?` consumption,
`is` consumption, tagged-GLOBAL receive, struct-literal tagged field
from an sret-class call (the widener #40 gate).
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).
getopt's deleted appendoption helper fed rt_ensure a hardcoded
membsz=24, stale since str went 24B (option {rune,str} = 32B): the
8-slot first grow allocated 192B while writes strode 32 — OOB past 6
options. The builtin derives 32 from the type table (MOVQ $32, SI);
this row appends 8 option literals and reads element 7 back full-width
so the class stays pinned. No in-tree test parsed >3 options.
The follow-up collapse (70fa9e2) regenerated the 5 main.combined.ww but
missed the tracked smoke amalgamation, which also embeds lib/shlex,
lib/bytes and lib/strings — caught by the combined_ww_fresh gate.
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.
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.
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.
Round-2 hardening of the #16 exit-code coverage. 255 is the single-byte
WEXITSTATUS boundary; the build-fail row pins the build-step caller
contract (a failing build must still report non-zero, both drivers
agreeing) so the procrun change that returns the real code can't silently
regress the w6c/w6a/w6l `!= 0` callers.
The shared fork/exec/wait helper collapsed every non-zero child exit to
1, so `ww_ww run <prog>` lost the program's real exit status (return 42
-> exit 1). The C ww driver's do_run returns WEXITSTATUS(status) — the
exact code. procrun now returns the real exit code instead of folding to
1; signal kill still returns 1 and fork/wait failure still returns -1,
matching do_run.
procrun is shared with the build-step callers (w6c/w6a/w6l), but they
only test `!= 0` (success vs failure), so a real non-zero code is still
`!= 0` — they are unaffected. dorun's final exec then reports the true
program exit code.
Driver behavior is not under byte-id (993 pins build_one output bytes,
not wait-status handling); 993 extended with table-driven run exit-code
rows (0/7/42) that fail under the old collapse-to-1.
The reviewer flagged two gaps in the #7 coverage: no minimal
non-empty count (a 1-element [_] is the boundary the element-counter
must still get right) and no module-level array element read-back
(only .len was checked at module scope). Add local_one_len /
mod_one_len / mod_one_elem. Both still mutation-resistant: the old
collapse-to-0 reads .len as 0, not 1, so the new len rows fail it too.
42/42 dual-stage + byte-id.
`[_]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).
main.ww's 33 piecewise stderr diagnostics carried hand-counted byte
literals (os.write(2, "lit".ptr, NNu64)). Route them through a
tool-local cerr(m: str) that takes m.len, removing the hand-count
hazard. strictpkgmismatch + `ww test` FAIL keep their runtime-length
*u8 writes (no literal to count). Regenerate main.combined.ww.
Not byte-id-neutral (error-path asm), but stderr + exit codes are
byte-identical old-vs-new across the driver diagnostics: verified by
wwstage-vs-wwstage oracle (cstage ww diag wording diverges pre-fold).
The three byte-id-critical import scanners in cmd/ww/main.ww drop
their hand-rolled byte-pyramids for strings.has{prefix,suffix} over
constructed str views, keeping every boundary guard that the lifted
helpers do not subsume:
- dirfilekeep: nlen<=3 guard kept (bare ".ww" len-3 stays rejected,
which strings.hassuffix(".ww") alone would wrongly accept).
- scanuse: i+7>len guard kept (hasprefix("import") covers 6 bytes;
the separator read at src[i+6] still needs i+6 < len).
- peekpackage: s+8<=q guard kept (hasprefix("package") covers 7
bytes; the separator read at buf[s+7] needs s+7 < q).
bytecmp (the enumeratedir sort comparator) is left as-is: it is a
magnitude-returning 3-way memcmp at a read boundary, not a spell-out.
Regenerated ww/main.combined.ww. All 6 corpus .combined.ww remain
byte-identical (cstage ww == wwstage ww_ww); 990-997 byte-id + 993
self-rebuild green.
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.