An array literal with more elements than the declared [N] passed the
per-element accept-if-fits checks in both stages and cgen then stored
every element at its natural offset, writing past the slot: local
frames smashed silently (the repeat form [1,2,3...] into [2]int wrote
at the saved BP), module DATA corrupted neighbours. All four
declaration contexts (local let, module let, def, struct-field
literal) funnel through one choke point per stage — arrlit_init_fits
(check.c) / checkarrlitfits (check.ww) — which now pre-counts the
literal (skipping the ... marker) and rejects count > N naming both
counts.
cstage clet's blanket has_arr_repeat bypass is narrowed to non-array
declared targets: repeat literals into arrays now run the same
overlong + #130 range checks wwstage's checkletassign always ran
(the bypass let [2]u8 = [999...] dodge the range check cstage-only).
checkarrlitfits also recurses into NESTED array-literal elements
(declared elem node N_TARRAY): cstage catches the nested shape
through its typed-literal assignability net, which wwstage's untyped
elements have no analog of — [2][2]int = [[1,2,3],[4,5]] at module
scope silently emitted corrupted DATA (1,2,4,5) and the struct-field
twin likewise. Recursion through the one choke point closes any
depth; a named-alias element type still bypasses — task #16.
alen==0/nil-length stays exempt ([0]/[_] sentinel conflation and
un-inferred [_] in def/struct-field — task #11); a non-INTLIT length
child (def-named [N]) is exempt in wwstage — task #13; under-long
literals keep their current accept (Hare rejects — task #10);
wwstage's overlong accept at assign/call-arg/return position (cstage
already rejects) is task #12; exact-fit bare-int nested cs-reject/
ww-accept divergence is pre-existing — task #17.
The flat checker scope makes ANY decl named assert/abort anywhere in
the combined unit disable the builtin unit-wide (the #45 shadow shape:
scope_lookup_prefer's cross-module fallback finds it). lib carried
three colliding @symbol("rt_abort") shims (os, time, strconv/stof)
plus the os.assert wrapper, so a bare assert(cond) in ANY program
importing os mis-bound os.assert and failed arity — a hard blocker for
regex fold-5 (regex.ha:660/670 bring builtin-assert mass). Ruled
respell-now per the recurrence test (#45 -> #58).
Delete the shims and the os.assert wrapper; every bare abort(msg)
caller (regex, strings, utf8, hash, getopt, encoding/*, time, stof)
now lands on the builtin, and the ~40 os.assert(c, m) sites respell to
the builtin assert(c, m) — restoring the exact Hare spelling the lib
ports diverged from (e.g. ref/hare/bytes/tokenize.ha:23). os.assert
had no Hare counterpart (Hare's assert is a language builtin); rule-9
wrapper removed. temp/dirs/bufio already use the non-colliding rtabort
spelling and keep it.
Now-dead 'import os;' lines kept (pre-existing precedent:
lib/strconv/strconv.ww carries one); a tree-wide dead-import sweep is
a separate concern. regex.ww's if+abort workarounds citing #58 stay
for the fold-5 owner to fold back into assert.
combined.ww regenerated for all five selfhost tools + the smoke
fixture via make.
wwstage had no EXPR_ASSERT-family intercept: the checker left bare
assert/abort calls untyped (asserttyped gate 4 skipped them by design)
and cgcall fell through to the regular call path, emitting
CALL assert(SB) for a symbol that exists nowhere — link-fail. cstage
was already correct (tag ty_err at check.c:1536-1572, lower inline via
rt_abort at cgen.c:6618-6663).
Mirror the same tag-then-lower pair: exprtype N_CALL stamps the call
void and the callee TY_ERR behind the scopelookupprefer no-shadow gate
(the isassertfam predicate), with the cstage arg diagnostics (cond must
be bool, msg must be str, arity caps); cgcall keys on the TY_ERR tag
and emits the identical CMPQ/JNE/rt_abort sequence. A user-shadowed
assert/abort (same-module or cross-module, the #45 shape, task #14)
stays untagged on the regular call path — byte-id for all existing lib
code preserved.
The cond check does NOT alias-peel: cstage compares ty_bool by
identity (check.c:1560), so `type myb = bool` is rejected there;
wwstage aligns down per rule 10 (a resolvealias here was accepting it
— cs/ww accept-reject divergence). Widening both stages together
belongs to the alias-peel choke-point arc (task #5, #47/#68).
The resolvewalk N_IDENT resolution counter learns the builtin shape:
an unshadowed abort/assert ident binds no sym BY DESIGN, so wwdump
-r's zero-unresolved gate (990 probe 4) counts it resolved instead of
failing builtin-using units.
test 957: 13 rows — pass/fail/msg/bare abort (run exit + rt_abort
stderr content; no-msg rows pin EMPTY stderr = the (NULL,0) shape),
assert in an imported module, same-module + cross-module shadow
controls, 5 checker rejects pinned on diagnostic CONTENT (shared
substring; cstage prefixes pos, wwstage cerr is bare) incl. the
alias-of-bool cond row pinning the rule-10 down-alignment; each
positive row pins cstage run exit + cs==ww byte-id. On pre-fix master
11/13 rows trip (survivors = the two shadow controls).
Residual (separate root, deferred diagnostic class): zero-arg assert()
is not intercepted by either stage; cstage rejects via the generic
undefined-ident path, wwstage's undefined-callee diagnostic is the
class deferred behind wiring checkfile into w6c_ww.
Both stages SEGV'd identically (byte-id-blind): cgun's TK_STAR emitted
a scalar MOVQ (AX),AX for an array pointee, so the index consumed
a[0]'s VALUE as its base — a wild deref. An array value IS its address
everywhere in this cgen (#270-1a), so the ARRAY pointee now takes the
same skip as the #185 *fn deref in both stages: `*p` leaves AX = p's
value, and every consumer that materializes a complex index base via
cgexpr(base) — N_INDEX read fallback, cgassign store/compound, TK_AMP,
N_SLICE — gets the array address from the one deref choke-point.
wwstage additionally joins the N_UN-TK_STAR base to the stamped-tinfo
esz arms (cgindex / cgassign store + compound / TK_AMP &(*p)[i]) where
cstage reads base->type uniformly: without it, esz fell to the 8B
default and a narrow element would mis-stride the moment the base
started materializing (cs!=ww only reachable post-choke-point-fix,
which is why it rides this commit).
949_ptrarr_index_run grows the deref_* rows: read (8B/4B/param-base),
write (8B / 1B+neighbor-guards), compound — runtime + byte-id, the only
nets that can see a both-stages-identical miscompile.
Indexing through a pointer-to-array auto-derefs, so esz and the element
classification must come from the pointee array's ELEMENT (cstage
idx_eff semantics, cgen.c:1163). Two halves of one root class:
A (wwstage-only, cs!=ww, cstage runtime-correct): elemsizeofc's #270-2
nested-array block treated an N_TPTR pointee-array like a [N][M]T outer
index and returned the whole-array size — every p[i] read/write/
compound scaled by N*size(T), and the same wrong element belief reached
the store-width chooser (var-idx write emitted an N*8B aggregate copy
sourced at the 8B rhs slot: caller-frame smash, the siphash round()
corruption). Fixed via two wwstage choke-points mirroring idx_eff:
idxeffti (tinfo: NAMED peel + TY_PTR->TY_ARRAY drill; feeds elemsizeofc
and elemissignedc/elemisfloatc/elemisf32c) and idxelemtn (node: element
tnode with the same drill; feeds every cgindex/cgassign/nodeisstr/
match-scrutinee elemtn resolution).
B (BOTH stages identically wrong, byte-id-BLIND): the TK_AMP &base[i]
arm read bu->sub->size without the ptr peel (&p[3]-&a[0] = 96, not 24).
cstage now routes esz through idx_eff.
A and B are FUSED by the pre-existing routing topology, not by choice
(rule 11): wwstage's TK_AMP arm already reads its esz via elemsizeofc
(selfhost/cmd/wcc/cgenexpr.ww:4095, the #11 addr-of twin of the #10
cgindex fix), so fixing A's choke-point flips wwstage's half of B in
the same stroke. A standalone A leaves &p[i] transiently cs!=ww;
B-first is the mirror transient; carving the TK_AMP caller out of the
fixed choke-point to preserve the wrong stride for one commit would be
a deliberate known-wrong intermediate (rule-7, vetoed by rob). One
choke-point, two enrolled routes — un-fusable without a red
intermediate.
Close-by-construction proof-grep (both stages): every remaining raw
sub->size index-stride read is TY_ARRAY-gated, a slice-only builtin
(delete/insert), a checker-stamped element tinfo (indexresult already
decays *[N]T, check.ww:2277-2284), or a non-index context (tuple
slots, let-init elements). Two true residuals filed with site+symptom
instead of silently absorbed: N_SLICE through *[N]T does not decay
(LOUD type error, Hare divergence; team task #18) and non-ident
cast-expression index bases keep wwstage's 8B-default esz (pre-existing
#74-style cluster; team task #19). cstage's N_INDEX read-side
str/slice header gates also move from u->sub to esub (identical for
every non-ptr-to-array base; honest for *[N]str — pre-fix BOTH stages
were runtime-wrong there, differently).
949_ptrarr_index_run pins the class at runtime + byte-id: {1,2,4,8}B
elems, const+var idx, param/local/cast bases, read/write/compound,
neighbor guards, &p[i] pointer-difference, siphash-round mix shape.
989_lib_byteid: siphash_test graduates #59.7 DIVERGE -> ID (ratchet
tripped loud pre-update; no other #59.x pin flipped in the same run).
(*p)[i] (sub-bug C) follows separately.
The N_FORRANGE header's non-ident arm stored cgexpr's AX into the
single bound temp — but a slice-valued cgexpr leaves AX=ptr, BX=len,
CX=cap, so the loop compared i against the DATA POINTER; and the
per-iteration element address had no non-ident base arm at all, so
the bound reload doubled as the base. One slot, two roles, holding
the wrong word. An empty slice coincidentally exited (ptr==0), which
is how regex.finish's `for (let charset .. re.charsets)` — planted
verbatim in fold 1 — stayed latent until fold 4 produced the first
non-empty charsets and SEGV'd. Byte-id both stages (the 989 M_ID
entry held on both-wrong-identical); first-consumer surfacing, the
kwtab/#8 pattern.
Fix mirrors the correct local-base arm: bound = BX (len), base ptr
spilled to a dedicated .rgb slot and reloaded per iteration. Covers
field-chain, indexed-element (the task-#57 shape) and call-result
bases. Two shapes whose cgexpr does NOT deliver the header convention
stay LOUD instead of silently wrong (rule 7): deref bases (*p — the
#11 deref-spine family) and non-ident ARRAY bases.
test/937: field (value+ptr roots), 24B-str-header field (the finish
shape), indexed, call, empty-header, eval-once (header captured at
loop entry, not re-read per iteration) rows + the two reject pins,
per-row cs==ww byte-id; verified failing 14/22 at the #66 parent
bb8a44a.
The #242 tuple arm of the widen choke-point (cg_widen_tagged_store /
cgwidentaggedstorebp) gated on a BARE N_TUPLE source. The cast-to-
CONCRETE-VARIANT wrapper ((a, b): range_alias) — the only spelling
real code uses (ref/hare/regex/regex.ha:213) — is not a widen-cast
(its destination is the variant, not the union), so the peel left it
intact and it fell to the SCALAR arm: cursor word 0 stored, payload
slot 1+ silently zero-filled. Both stages, byte-id, gate-blind.
Fix at the choke-point: peel N_CAST(lhs=N_TUPLE) where the NAMED-
peeled cast type is TY_TUPLE and iterate the inner element list; the
variant tag keeps resolving from the CAST's type (exact named match),
so the #241 untyped-element loud-stop stays scoped to the bare form
on both stages.
Closure by construction needed two more arms (reviewer proof-grep):
cg_widen_tagged_push's direct-push fast path classified a tuple-typed
ARG source as scalar — pushed word 0 only AND coerced an unresolved
tag to 0 — so f(((a,b): rng)) bypassed the fixed arm entirely (and
the bare typed (a,b) arg dropped slot 1 the same way). Tuple-typed
sources now route through the scratch store. The remaining non-
literal tuple sources (ident / call result / match binding) have no
word-copy arm in the store and fell to its scalar arm — loud-stop
(rule 7) until #72 wires them. Every tagged-payload materialisation
now funnels through cg_widen_tagged_store, which handles or rejects
every tuple shape: let/assign/return/append (cgen.c:7511) directly,
arg push via the scratch route.
test/936: cast-tuple matrix (let / append local+index-place+deref-
place+ptr-field-place / ident+float+str elements / 3-member layout-neutrality /
direct-arg) + bare-form no-regress (return + arg) + bare-literal and
tuple-ident reject pins, per-row cs==ww byte-id; verified failing
24/40 at parent 8578ad0.
Unblocks regex fold-4 (charset_range_item construction).
Global tuple lets were WHOLLY unwired, silently: let_emit_size returned
0 so emit_lets SKIPPED the definition (no DATA, no diagnostic), then
cstage's t.N read and #235 len arm read BP-frame garbage (localfind→0)
while wwstage — with the tuple never in collectlets — mis-emitted the
field index as a symbol (`MOVQ 0(SB), AX`). ken's #48 was the len()
facet of this.
Now: let_emit_size/letemitsize admit TY_TUPLE (slot-sum size, rides
C-t0); emit_tuple_data/emittupledata lay the slot-format DATAW row —
a scalar element one 8B LE word, a str element its 24B header slot
with a DATAR ptr patch at the element's slot offset (the #18 [N]str
per-element pattern; strlits pre-interned in element order) — and any
element that doesn't reduce to an int/str literal dies LOUD instead
of skipped. The t.N read and len arms gain the global base (LEAQ
sym(SB) into CX, the struct-field-global pattern; wwstage's C5 len
loud-stop graduates to the working path). A GLOBAL tuple as a
first-class VALUE (`let q = g;`) loud-stops on both stages — pre-fix
it byte-identically loaded word0 only and read a stale cursor for
words 1+ (element reads are the supported surface).
941 grows the t3 rows: global element reads (str+i64 and packed
u32,u32 incl. len(g.0)) + rejects (float-element init, whole-value
use, pre-existing element-write anchor). 7/82 checks fail at the
C-t2 parent (cs silent-garbage runtime, ww C5 build-fail, both
rejects vacuous-or-absent).
node_tuplearg was N_CALL-scoped and its comment claimed non-call forms
"loud-stop" — they did NOT: a tuple ident/literal/unwrap arg fell to
the scalar single-PUSHQ default, skewing every later arg register so
the callee read garbage word 2 (byte-id both stages, the gate-blind
both-wrong class; packed shapes SIGSEGV'd pre-C-t0). The receive side
(cgfn #163 walk) was already correct.
cgexpr already fills the return-ABI cursor for every supported
producer (#241: ident via slot-to-cursor, literal via lit-to-cursor,
unwrap via payload shift; call via the return ABI) — the send now
admits exactly those into the existing @tupargscr restage + per-class
drain (node_tuplearg widened; wwstage gains nodetuplearg, mirroring it
over the local tnode / inferletcalltype; rettupleof stays N_CALL-scoped
for the destructure receives). Any OTHER tuple-typed source shape
loud-stops at the push site — the false comment's claim, now true
(rule 7). Literal tuple elements are stamped expr types, so the
restage/drain wide test goes type_isstr/type_isslice (TY_UNTYPED_STR-
aware) with the ty_str->size header stride; the wwstage twin walks a
literal's VALUE exprs the way cgtuplelittocursor classifies them.
Ken review demands folded in: (1) a NESTED composite element
(tuple/struct/array/tagged inside the tuple) occupies more than the
one GP word the restage walk counts — the checker accepted it and it
ran WRONG (inner words skewed, wwstage SIGSEGV); both stages' restage
walks now loud-stop the element kind (wiring is the filed follow-up,
task #65). (2) the variadic interaction probed: a tuple arg ahead of
a variadic tail rides the restage correctly (positive row);
variadic-of-tuples stays bounded-loud via the tuple-in-slice read
surface.
941 grows the t2 matrix: packed/16B params with branched callees,
mixed arg orders both ways, literal arg, (f64,i64) param, unwrap arg,
ken's >6-GP-pressure stress (4 leading scalars + tuple + a 7th
stack-class word), variadic-after-tuple, plus rule-7 reject rows
(chain-source arg, nested-element arg, variadic-of-tuples, over-cap
ident arg) and the fold-4 charset substrate pin ([](u32,u32) append
stays LOUD). At the C-t1 parent 18/73 checks fail: every runtime arg
row except the (f64,i64) anchor on BOTH stages (byte-identically — the
gate-blind both-wrong class) and the chain/nested args silently
accepted.
wwstage cglet's tuple receive was producer-SHAPE-keyed: the mixed
str/scalar arm required s0_is_str != s1_is_str (syntactic) and the
rt16 arm required an N_CALL rhs (rettupleof), so a scalar-scalar tuple
LITERAL `let t: (u32,u32) = (3,4)` matched neither and fell to the
generic single-word store — word 1 silently dropped (#209/#211-class
syntactic-vs-type keying). cstage's twin arm was sz==16/32 magic-size
keyed, so 24B 3-scalar tuples dropped words 2+ on BOTH sources.
Both stages now key the same way: declared-type TY_TUPLE + in-cap
register classify (cg_sret_retsize / sretretsize == 0, the shared
SSoT), alias-peeled; the two wwstage shape arms collapse into one
type-keyed arm walking the declared element list (the #240 lesson —
never the producer's). Over-cap falls through to the sret receive
exactly as before; unannotated `let t = f()` rides inferletcalltype.
941 grows the t1 rows: lit packed/16B/3-scalar + call 3-scalar fail at
the C-t0 parent (10/39 checks — wwstage lit halves AND both-stage
24B halves), mixed-lit + unannotated-call anchor the untouched paths.
Filed while probing: cstage silently accepts an over-cap tuple-LITERAL
let where wwstage loud-stops (pre-existing at master, task #64).
The checker computed TY_TUPLE size as the packed element-size sum
((u32,u32) = 8B) while every cgen cursor-transport site strode 8B
slots (16B). 16B tuples were blind to the split (slot == packed);
packed tuples hit it everywhere: cstage let-receive keyed on sz 16/32
missed sz 8 and dropped word 1, the cgfn param receive spilled
8B/element into a packed-sized local (saved-BP clobber, SIGSEGV), and
mixed (u32,f64)/(u32,str) shapes missed the receive arms entirely.
Slot layout is now the SSoT (user-ratified): the flip lives in the two
checkers' N_TTUPLE size computation only (check.c, check.ww
tupleelemslot + stamp); cgen's packed-keyed walks (t.N read, #235 len
arm, over-cap sret send/receive pair) align onto the slot stride, and
the wwstage t.N read gains the natural-width load (tnodeloadop) to
byte-id with cstage's fldloadop. ttupleelem.offset re-stamped
slot-cumulative (no consumers yet). The #242/#243 eightbyte-share
loud-stop dissolves by construction (no two narrows ever share an
eightbyte) — 940's eightbyte_share row graduates to a runtime
round-trip. Hare-layout divergence documented at both checker sites;
re-alignment is task #60. #32 send skew and #33 wwstage literal-let
receive are separate commits on this base.
941_tuple_slot_layout_run pins the matrix: 4 packed rows fail at the
parent (8/21 checks), 3 neutral anchors prove 16B/32B emission
untouched.
Hare's insert(xs[idx], v) (ref/harec/src/check.c:745
check_expr_append_insert — append/insert share the checker arm,
"insert" at :786): checker accepts an INDEX place over a slice plus
one value, stamps void; idx == len is a legal end-insert (the
ref/hare os/exec/platform_cmd.ha:86 idiom). Loud-rejects with exact
texts: spread form insert(xs[i], vs...) (filed, #35 — also covers
harec's with-length form via the arity check), range place (not
Hare; harec only parses ACCESS_INDEX, :784), non-index operands,
array bases, wrong arity. delete()-parity throughout.
Lowering (both stages, converged byte-identical by construction) is
a DESUGAR: append(xs, v) — reusing append's grow (rt_ensure) and the
entire #34 value-store dispatch (scalar / str-slice header / tagged
widen / struct fill) verbatim, one boxing choke-point — lands v at
slot len-1; then a rotate-right of [idx, len) moves it home through
a fresh per-site esz frame scratch (@insscr). The rotate is delete's
shift loop in reverse (descending j, the safe memmove-up direction)
and is a same-slice whole-stride raw byte move — no boxing exists
for any element kind. idx evaluates BEFORE the grow (Hare's
left-to-right operand order — pinned by the pregrow_len_idx row,
insert(xs[len(xs)-1], v): pre-grow [7,13,11] vs post-grow [7,11,13];
an idx==len(xs) end-insert cannot discriminate, the rotate
degenerates either way). Base shapes: local slice ident (LEAQ) and
deref-of-local ptr-to-slice (MOVQ); others rule-7 loud-stop, like
delete.
test/807: 57 fixtures — front/middle/end + idx==len via len(xs) +
the pre-grow eval-order pin, esz 1/2/4/8/16/24/56 (MOVB/MOVW/MOVL
tails, struct body, str header, 7-qword tagged from a typed local
[the regex fold-3 ha:347 newinst shape] and from a cast rvalue
[ha:419/441]), empty-slice grow, (*p)[i] deref base, front-insert
loop, 6 checker reject rows with diagnostic-text checks; every
accept row cs==ww asm byte-id.
io.error grows errors.invalid (ref/hare/io/types.ha:11 spreads
...errors::error, which includes it; Hare's memio seek returns it on
out-of-bounds, stream.ha:134-136) — appended last so existing member
tags stay put; no exhaustive io.error matches exist in lib.
seekfn mirrors ref/hare/memio/stream.ha:122-140 over the flat header,
shared by fixed/dynamic/dynamicfrom (Hare wires the same seek into
both vtables). The io.off-vs-i64 arithmetic runs on an i64 copy:
cstage binop typing is nominal on aliases, wwstage accepts (filed,
ww-core #54).
791's stream_seek_unsupported row re-pins st_seek's void-arm on a
hand-built seekerless vtable: its old premise (memio wires no seeker)
is retired by this commit; memio seek success is pinned by memiotest.
w6c/wwdump main.combined.ww regen'd: they embed lib/io + lib/memio
(the freshness gates are blind to this — embedded-source discipline);
w6a/w6l/ww don't embed io, verified untouched.
#49 (#35's single-element sibling, tranche-C pre-check PC2): the
struct-element append arm dispatched on SOURCE node kind — N_STRUCTLIT
(literal fill) and N_IDENT (local word-copy) only; every
place-resolvable chain died on the rule-7 fatal in BOTH stages,
including search()'s result-build line
`append(res, threads[best_idx].root_capture)` (regex.ha:819).
Wire those shapes with a SPLIT resolve around the grow (the #49
ruling): the chain's rvalues — deref-root pointer expr, index expr —
evaluate exactly once PRE-grow into @appendsroot/@appendsoff (an index
reading the slice header sees the pre-append len, Hare's argument
order), then only the BASE re-derives POST-grow from the live storage
and the stashed offsets land back on top, so a self-append source
re-roots in the post-realloc buffer. harec resolves an aggregate
source address wholly PRE-grow (gen.c: gen_load returns the address
for STORAGE_STRUCT, gen_store copies after rt.ensure) — a
use-after-free under a reclaiming allocator; per #263 we align to the
runtime-correct side, not the reference. A pointer ALIASING the grown
buffer keeps Hare's own stale-base hole (sound today only because
rt/malloc.ww never reclaims). Supported shapes are bounded: root
(local/global ident | deref) + at most one index + trailing direct
fields; all else stays on the #34 fatal, including CALL rvalues (the
#42-style bound, new reject row pins the text in both stages). The
N_STRUCTLIT/N_IDENT fast-paths keep their emission byte-identical.
806_append_place grows eight rows: indexed-field 56B capture (the
ha:819 shape, header readback), computed-index whole element,
deref-spine param pair, deref source, self-append ×33 crossing three
cap-doubling reallocs, the split-order semantics pin (a CALLED index
helper reading len must run once and see the PRE-grow len — the
pre-split emission failed exactly there), an element-kind ×
place-source matrix row (scalar/narrow/str/slice/tagged route via the
pre-existing arms — regression net), and the CALL-source reject. The
six fix rows verified FAILING against a pristine 796d41b build on
both drivers (loud #34 fatal, identically in cstage and w6c_ww —
there was no silent path at master); 70/70 fixtures green here
including per-row cs/ww asm byte-cmp.
Unblocks regex fold-2b tranche C (search) — PC2 was the lone
pre-check failure; PC1/PC3/PC4 passed at base.
C5 (tasks #10 + #41): the len() builtin's operand handling was an arm
enumeration that leaked FOUR siblings over time (#235 tuple-elem →
#19 indexed-elem → F2 len(xs[i].field) → FA2/FB1 len(*p)) — every
unhandled slice/str operand shape fell to a bare cgexpr fallback that
returned the slice DATA POINTER as the length. Silent ptr-garbage,
byte-id both stages, gate-blind. Probing at d642017 surfaced the full
family: len(*p) (param 48 / local 64), len(xs[i].field) (147),
len((*p)[i].field) (10), len(s.field) (75), len(p.field) (87) — plus
the same garbage for non-place operands len("abc") (40),
len(xs[1:3]) (48), len(mk()) (0). Review widened it twice more:
len(**pp) (chained deref, garbage 208 at e481cb8) and the EMPTY-slice
deref (len 0 reported as .ptr — masked by exit-code truncation, hence
the branchy test row).
The enumeration is closed by construction (ken's verdict): enumerated
fast-paths keep their pre-fix asm byte-identically (ident local/global,
#235 tuple element — not resolver-addressable, cgplaceaddr has no
TY_TUPLE hop — #19 indexed element, TY_ARRAY const fold), then ONE
uniform header-place route via cgplaceaddr resolves every other
slice/str place and reads the .len word at place+8 (the same offset
math as the ident arm). Non-place operands (string literal, slicing
expr, call result) die LOUD per rule 7 — previously the same silent
ptr-garbage; Hare instead const-folds len of literals, that parity is
filed as #46. The ident arm's off==0 non-let residue (MOVQ 8(BP)
garbage) now also routes resolver-or-loud. cstage's dispatch peel is
aligned to wwstage's existing TY_NAMED loop-chase (single-peel +
loud tail would have surfaced as cs-rejects/ww-accepts on 2-level
aliases).
Asm-neutrality: all five embedded main.combined.ww corpora compile
byte-identically under pristine-parent w6c vs fixed w6c; per-shape
pins (ident local/global, tuple, index, array) NEUTRAL + cs==ww.
802_lenidx_run grows 14 rows: the nine garbage shapes (incl. computed
index through a deref spine, param-vs-local *p, chained **pp, empty
slice), two neutrality controls (global and tuple fast-paths have
dedicated runs: 797, 903), three reject rows pinning the exact rule-7
text in BOTH stages; all fix rows verified FAILING against a pristine
build of the parent e481cb8 (12/19 fail there, 19/19 green here).
Consumers unblocked: regex fold-2b tranche C ha:795/798
len(threads[i].captures); lib/regex add_thread's (*threads).len
dodge (regex.ww:238, WHY comment cites #41) reverts to len(*threads)
with the tranche-C port, not here.
F5 (task #7): the N_LET aggregate-copy arm's source-addr enumeration
(cgen.c #265/#268) had TY_ARRAY-ident/N_DOT/N_INDEX bases but no
TY_SLICE base and no deref-spine shapes, so `let e: th = xs[0]` fell
out with havesrc=0 — cstage emitted NOTHING (slot uninitialised),
wwstage fell to its scalar default (8B truncation): gate-blind cs≠ww
(p6min13). Every remaining ADDRESSABLE rhs now resolves through
cgplaceaddr (the C1 resolver; enumerated arms dispatch first, their
asm untouched), and the arm closes by construction with a loud tail —
nothing below it can initialise a >8B struct/array slot, so any
unhandled rhs shape dies loud instead of silently. A pre-tail #38b
guard keeps the established `?`/`!`-on-sret loud-stop marker in
wwstage (mirror of cstage's pre-arm fatal; pre-fix that shape reached
the cgtryunw/cgtryprop gates which the tail now pre-empts in let
position).
Reviewer-C2 inheritance: `let c: capture = (*ts)[i].cap` (aggregate
leaf behind a deref spine) — wwstage's documented cgdot aggregate-leaf
loud is retired for let position (cglet routes the copy through the
resolver before cgexpr sees the leaf; the loud stays as the guard for
non-let expr positions), and cstage's silent no-copy on the same shape
is fixed by the same resolver fallback.
By-value RANGE payloads ride the same class: N_FORRANGE's single-bind
load truncated every aggregate element to one fldloadop word. Both
stages now word-copy the full element extent (MOVQ run + sized
MOVL/MOVW/MOVB tail, the #270-1b idiom) for esz > 8. wwstage esz is
re-keyed elemsizeof→elemsizeofc (the 8-sentinel hid struct elements
from the copy gate — the #8 named-narrow precedent), with a
stamped-slc.type_ fallback + element-tnode synthesis for non-ident
scrutinees (tinfo SSoT, #209/#211). The wwstage checker now binds the
ELEMENT type on single-bind ranges via a synthetic N_LET binder node
(mirror of cstage check.c N_FORRANGE scope_define(..., elem, ...));
pre-fix the binding's decl was the N_FORRANGE node itself, so any
field read off a by-value binding asserttyped-bailed. The checker
half folds in under rule 11 because the split is unsound in either
order: cgen-first is untestable (every field read off the binding
still bails), checker-first converts that loud bail into the 8B
SILENT truncation — only the pair closes the class.
FC0 graduates: regex.finish's by-value range over 24B charset elems
(non-ident scrutinee re.charsets) was the lib/regex byte-cmp's ONLY
hunk since fold-1 — cstage 8-of-24-byte copy + IMULQ $24 vs wwstage
1-byte MOVZBQ, runtime-masked by the no-op loop body. The byte-cmp is
now ZERO hunks (regex_test.combined.ww, w6c vs w6c_ww).
#36 disposition: NOT folded. p6min9/p6min10's remaining failure is the
struct-ident field rhs inside a struct LITERAL (cg_structlit_fill
under-copy) — a different choke-point from the let-copy source-addr
machinery; they still exit 4 here and stay blocked on #36 (read half
landed in C2).
Residual filed as task #43: an UNANNOTATED aggregate let
(`let e = xs[0]`) still skips the wwstage arm (aggn/letslotsize are
annotation-keyed; cstage keys the stamped n->type and now full-copies)
— cs≠ww on that shape remains, #38-family. A landmine comment in
test 805 marks the gap.
test 805: +6 rows — let-from-slice-elem 16B (p6min13 verbatim) /
24B/40B/12B(MOVQ+MOVL tail) matrix / deref-spine leaf / by-value range
([]struct both-fields sum, []capture 40B, []str 24B header) / range
edges (empty slice, by-VALUE binder-mutation pin, 12B elem MOVL tail)
/ reject row pinning the loud-tail text on both stages. All six fail
at the pristine parent 403625e (re-verified post-rebase; 121 prior
fixtures stay green there).
Task #40 (FB3): the #38b MEMORY-class staging only accepted ident /
aggarg_srcaddr-enumerated sources, loud-stopping on a slice-element
arg — the regex run_thread loop-condition shape
is_consuming_inst(re.insts[threads[i].pc]) (regex.ha:602) and B4's
(*p)[i] spelling. The staging now falls back to cgplaceaddr (the F6
resolver) for the slot's source address; the existing word-push
machinery is unchanged. Placed AFTER aggarg_srcaddr so every
pre-#40 shape keeps its asm byte-exact (verified vs a master w6c).
Boundaries kept loud (rule 7): exact-type rvalue (no place to
resolve), sret-class call source (#40-family follow-up), global
tagged let (task #25), variadic element, register-overflow mixing.
Diagnostic re-worded to match: slice-element is wired, rvalue and
unresolvable-place sources are not.
test/929 grows 6 run rows (element source with literal+computed
index, deref-spine, nested re.insts[threads[i].pc] spelling,
element mixed with register args both orders, fn-call index with
two mem args per call — the inner CALL runs inside the mem
pre-pass with the other slot already staged — and global-slice
element, the storage-backed twin of the loud fail_global_src
boundary — all build-fail loud at master) and 1 reject row pinning
the new boundary text on both stages.
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.