e8977a413d6fa43776b4ea193d3faae19aec1bfb
97 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 322667b820 |
cgen: #35/#46 Family C — tagged transport from deref/cast/unwrap sources goes mem-based, both stages
A tagged value reaching a transport consumer from a DEREF, CAST, or UNWRAP source materialized as ONE scalar word — the N_UN(STAR) arm's localloadop pulled word0 (the tag) and every cursor consumer then transported garbage payload (cs pushed stale DX, ww stored 0/garbage; divergent asm AND silent-wrong on both stages — ken f35 asm proof + ken37v D3a/D3b). Consumers × sources × sizes all wrong: arg push, let-init, assign, return, match scrutinee, as, widen — from *p at 16/24/32B, from identity/widening tagged casts, and from `?`/`!` whose success variant is itself tagged (nested box: payload words dropped). `is` and ww-match-16B passed only by stale-register luck. Fix extends the #37 mem-based machinery ( |
|||
| 26d375410e |
w6c+w6c_ww: >32B tagged INDEX/DOT reads go mem-based — box address in AX (#37)
A tagged box wider than the AX/DX/CX/R8 cursor (size > TUPLE_GPCAP*8)
read via N_INDEX or N_DOT now leaves its ADDRESS in AX — joining the
sret-call mem-based class the #38b gates already speak — and every
cursor consumer branches on one shared predicate (cg_tagged_memread /
taggedmemread) before reading AX as the tag. <=32B keeps the cursor
byte-for-byte (32B-at-cap asm proven identical to base on both the
t.N and INDEX routes).
Emitters: N_INDEX ident+fallback arms, N_DOT tuple-element (flips the
#22b loud bound), N_DOT struct-field + ptr-chained-field (were silent
clamps at 32B); wwstage twins gate cgloadtaggedfield at the helper
choke-point. Consumers: match spill x2 and the widen-store subset +
nested arms (the let/assign/return-widen/arg-widen/vararg choke-point)
copy the box from memory, then share the existing zero-pad + tag-remap
tail; `is` loads the tag through the address; `as` spills mem-based.
Rule-7 loud bounds replace silent cursor garbage for the non-mem-based
>32B kinds, `?`/`!`, and the 33-48B in-reg tagged arg push (mem-based
push stays the #35 family); the exact-type >32B return passthrough
from INDEX/DOT flips from its #38b loud into the widener route. The
pre-existing >48B memarg stack blit (cgplaceaddr) never used the
cursor and is pinned unchanged.
Reviewer-37 amendment: the non-mem-based >32B loud was ONE-SIDED on
two wwstage routes — cgwidentaggedstorebp had no fall-through guard
at all (`let w = *p` on a 56B box: cstage loud, wwstage silent word0
truncation), and cgmatch's guard keyed on matchscrutt-resolved
spillsz, which defaults under cap for kinds matchscrutt can't resolve
(N_UN deref et al), so `match (*p)` slipped it the same way. Both now
loud off the stamped src/scrut type_ (the kind-blind key cstage
already uses), restoring the rule-10 symmetry the body claims.
Emitters and consumers ship as ONE commit: they share the memread
contract, and splitting would open a transient window where a wired
emitter hands an address to an unwired consumer (silent garbage) —
the #61-precedent route-sharing fuse. The CX-global-tuple-base LEAQ
arm is TRIPWIRE wiring: a >32B tagged global-tuple element is
unreachable today (module-level tuple inits are int/str-literal-only;
tagged elements loud at the DATA emit), and the LEAQ keeps the same
base_reg generality as the cursor walk it replaces (ken note, task
record).
This was the last 5b compiler gate: `match insts[pc]` on the regex
inst union (inst_lit|inst_repeat, 56B) was silent-wrong gate-blind
byte-id on both stages (payload words 3+ dropped past the R8 clamp).
test 941 grows 165->200 checks: the #22b BUILDERR pin flips to a
runtime row, plus the 56B driver match, str+nested-tagged payload,
let/is/assign, indexed return, widening (identity and reversed-order
remap), 56B memarg, ken's X1 composition row, 32B-at-cap INDEX
boundary, the ptr-chained p.f match (BX-arm) and (*p)[i] fallback-arm
rows, and the two deref loud-symmetry BUILDERR pins. At base
|
|||
| a72e8158c7 |
w6c+w6c_ww: over-cap (sret) tuple returns carry tagged elements — MEMORY-class box copy (#22b)
The over-cap SEND gains a per-element tagged arm: a declared-TAGGED element copies its whole box mem-to-mem through the sret pointer (@sretarg reloaded into DX, eslot/8 AX-bounced word pairs) from its ident-local slot — cgexpr can't source it (tagged ident load is word0-only; the AX/DX/CX/R8 box cursor would collide with the DX dest-base reload). Non-ident/widening sources stay loud (call-source is task #41 per rob's loud+filed ruling, widening #23/#40). The receive legs were already eslot-generic from #22a except the wwstage MASSIGN sret copy-out, whose wide=(str||slice) key dropped a tagged element to the scalar 8B branch — re-keyed to the accessor's eslot>8. The over-cap DECISION now rides cg_sret_retsize/sretretsize on the DECLARED return type — the same SSoT the prologue @sretarg wiring and the caller use. The old expr-shape count disagreed with the classifier for declared-tagged elements with unwidened-payload exprs: the emit took the register path against an sret-reading caller — built clean, ran garbage, BOTH stages, byte-id, gate-blind (941 b22_reject_widen_ skew). A rule-7 net behind the branch catches any future inverse skew. The same classify/emit class on the LET side: wwstage's cgexpr cursor materialiser counts a void literal elem as 0 words, so a void-bearing over-cap literal init slipped its loud and fell past every store arm to NOTHING (cs louded via the ken-R1 net; ww ran silent-wrong) — wwstage gains the same net. foff advances ride tuple_eslot/tupeslotn (THE accessor): ken's K4 proved the old wide?esz:8 void-elem 8-advance was a LIVE silent miscompile at base (declared-void-elem over-cap return, send wrote +8 where every receive walks the 0-slot). The unwiring also made >32B boxes transportable, exposing the t.N tagged read's unbounded k<eslot/8 cursor walk: cstage OOB-indexed tuple_rseq[4] (invalid asm), wwstage tupreg-clamped k>=3 to R8 (silent payload-word-3 drop). Bounded loud both stages; the mem-based box read is #37's fold (transport itself is size-generic and correct — 941 b22_big_box_mlet). One commit by the #61 route-sharing precedent: unwiring the send is the only thing that makes the ww massign arm, both skew nets, and the >32B read reachable — the one split order that avoids a transient silent-wrong build (send last) lands those arms DEAD, unexercisable by any test until the unwiring commit, so a defect in them would bisect to the wrong commit; fused, every arm lands with the 941 rows that pin it, preserving the #22a exit invariant (zero silent tagged-tuple paths: correct-via-accessor or LOUD with a task cite; proof-greps in-tree — tuple_ebytes/tupebytes 0 hits, all remaining tuple_rseq/tupreg walks cap-validated upstream). Driver: regex fold-5b parse_repetition ((void|size),(void|size),size) = 5 GP eightbytes > TUPLE_GPCAP — pinned end-to-end (construct, send, let/MLET/MASSIGN receive, t.N is/as, destructure) plus head/mid/tail, str+tagged and SSE-over-cap mixes, `_` skip, and the reject pins: 941 165/165 (15 new rows), test-unit 281/281. ken runtime-oracle PASS x2 (independent hand-math layout audit, K1-K5 probes); reviewer-22b amendments (test-only): ken-K5 32B-exact box-at-cap row, both-tagged 2-elem row, chained-sret row, #37-family cite pinned in the big-box experr. Residuals filed: #39 return-forward divergence, #41 call-source tripwire, #37 mem-based box read. |
|||
| 66991585d6 |
wcc+w6c+w6c_ww: tagged tuple elements take their full slot — tuple_eslot accessor (#22a)
slot = roundup8(size(elem)) — 8B is a FLOOR, not a ceiling (user- ratified 2026-06-04; the #237 fieldslotsize precedent; (str,str)=48B predates this — tagged was the one truncated >8B kind). Pre-fix the checkers truncated a tagged element to one 8B slot and every cgen transport walk strode wide=(STR||SLICE)-else-8: cstage read the NEIGHBOR slot, wwstage read ZEROS — both-wrong-differently, so the byte-id gates were blind (prober-9 PG1, /tmp/p9). One stride accessor per stage — tuple_eslot (cgen.c) / tupeslot (cgenstmt.ww) — now feeds every tuple walk; the per-site predicates are deleted as absorbed. Sizer: check.c N_TTUPLE + check.ww tupleelemslot gain the TY_TAGGED arm (astsize already rides the type table since commit 0). Routes flipped to the accessor, both stages symmetric: cursor producers (lit/slot-to-cursor; tagged ident elements load their box from the slot — cgexpr's tagged ident load is word0-only), let-receive (tuple_store/tupstore generalized to eslot/8 words), N_RETURN send, by-value param receive, arg restage/drain (tagged stays loud per C-t2), destructure (MLET/MASSIGN, ident + sret + in-cap), t.N read + len(t.N) + global-g.N offset walks (t.N gains the tagged box load: AX=tag, DX/CX/R8=payload — the is/as spill cursor), sret classifier, DATA emit. wwstage cgtagvariantidx gains the #67 stamped-carrier arm (flatvariantidxt on .type_) — its AST-only key silently clamped 't.0 is size' to tag 0; fused here because the tuple-element read this commit wires is its only exercisable consumer. Exit invariant: zero silent tagged-tuple paths — in-cap shapes (<=4 GP eightbytes) are correct end-to-end; everything else is LOUD: over-cap sret return (#22b, task #28), call-arg (C-t2 #32), non-local literal element sources (#22b/#23), tuple-in-union payload (#242/#22b), global static-init, element write (pre-existing). Closure proof-grep at HEAD: 'tuple_ebytes|tupebytes' -> 0 hits; 'wide.*=.*(TY_SLICE|TY_STR)' tuple-walk survivors are all behind tagged loud-guards (cgen.c:2535/2568/12013 widen-store + over-cap send; cgenutil.ww:3527 twin). Latent cross-checks closed by the accessor: wwstage MLET-sret strode esz (4 for i32) vs cstage 8; wwstage param-receive strode slotsize (composite slotsize) vs cstage 8; both now the accessor's floor-8. Tagged inits in pins use the CAST form (5: size) — the bare untyped-int widen-store mis-tag is pre-existing at master and filed (task #33). 941 gains 13 rows: t22_* size/align folds (+ void-elem 0-slot pin), the full runtime round-trip (read, is/as, both element orders, void variant, destructure, literal-let), two-tagged-elem in-cap and float+tagged in BOTH orders (ken k1/k2 regression-pinned as rows), t.N-as-call-arg, 5 loud pins (arg, over-cap return, call-source element, global init, element write), and the sret_narrow_mix_* triple below. Runtime rows exit-checked under both drivers + byte-id. 129/129; unit tests green; sizelint clean. ken R1 (re-validation fix): an OVER-CAP tuple init whose rhs is not a CALL fell past every cstage N_LET store arm to NOTHING — silent uninitialized-frame reads — while wwstage loud-rejected the same shape. Pre-existing for (str,str) literals; the #22a tagged slots routed >16B-box tagged shapes into it (k5b/k5c/k5d, base-correct because base truncated them in-cap). cstage now routes the rhs through cgexpr (the cursor materialisers carry the exact wwstage loud texts) with a trailing fatal as the net; 941 pins both the tagged and the (str,str) spellings of the hole. reviewer-22 (review fixes, folded): the MLET-receive slot flip above landed ALONE on the wwstage over-cap sret family — the ww RETURN-send and MASSIGN-receive still strode packed esz (4 for u32), so `let (a,b,c) = f()` over (str,u32,str) read c at slot offset 32 while ww's send wrote it packed at 28: a ww runtime REGRESSION vs base (base was packed-consistent on both sides — ran right, byte-diff). Both walks now stride the slot (send mirrors cstage's `wide ? esz : 8`; MASSIGN strides tupeslotn) — closing, in the same stroke, the pre-existing base skews: ww `let t = f()` over-cap narrow-mix read (reader slot-laid vs send packed, runtime-wrong at base) and the cs≠ww asm on all three routings. Second find: t.N tagged element as a CALL ARG — cstage rides its generic node_istaggedarg cursor push, but wwstage's kind-gated aistagged missed N_DOT and mis-routed the box into the widening branch (taggedvariantindex -1 clamped to 0: callee read variant 0, silent, ww-only). cgenutil.ww gains the N_DOT arm (aistagged + pushargsrev), the #67 stamped-carrier twin of the N_INDEX (#12) arm. The N_DOT arm was a LIVE wwstage stdlib miscompile: hextest's import graph carried two t.N tagged-arg sites (base .s diff = exactly the clamped-tag PUSHQ pair) — the 989 lib-byteid ratchet caught the convergence and #59.4 graduates to M_ID (runtime-correct per 979_hex_run). Probes: /tmp/r22. Residual filed: tagged arg from deref/cast/unwrap sources is a word0-only read on BOTH stages (pre-existing, base-confirmed) — generalizing wwstage's kind gate to cstage's type-keyed check rides that task. |
|||
| 0055ac2cd3 |
w6c+w6c_ww: for-range over a non-ident slice base — bound from len, base ptr spilled (#70)
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
|
|||
| 6426fac6f2 |
w6c+w6c_ww: tuple by-value ARG send — every cursor-filling producer rides #163 (C-t2, #32)
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. |
|||
| 12af54f9f8 |
w6c+w6c_ww: tuple-let receive keyed on type classify, not producer shape (C-t1, #33)
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). |
|||
| d642017643 |
wcc+w6c_ww: aggregate let/range element copies via cgplaceaddr (F5)
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
|
|||
| 4f3967835e |
w6c+w6c_ww: tagged sret for slot>32B returns (fix #38)
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. |
|||
| bf1037d8c4 |
wcc/check+w6c+w6c_ww: materialize array-literal slice-borrow base into per-fn scratch (fix #25 + #31)
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. |
|||
| 63142770de |
w6c+w6c_ww: box [N]tagged array-literal elements via the tagged-store path (fix #12)
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. |
|||
| b3d4d2df32 |
w6c+cgen: full 24B header store for str/slice array-literal elements (fix #20, #270 str/slice arm)
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.
|
|||
| d39691a3d7 |
w6c_ww/cgen: size [N]enum element from tinfo not slotsize (fix #8)
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.
|
|||
| 418dd21f34 |
w6c+wwstage: wwstage alias-aggregate-return loud-stop + #276 citations (#272 review)
Review fixes for the #272 fold (reviewer272b gate; rob+ken ruling). Bundled because the wwstage catch-all message carries the citation and the combined.ww regen covers both .ww edits. - wwstage cgreturn close-by-construction catch-all keyed on the SYNTACTIC return-type node (N_TARRAY / N_TNAME+structlookup), so a named-alias aggregate return type (type a=[N]T / type a=struct) bypassed both the handling arms AND the loud-stop, falling to the scalar default = silent segfault/truncation; cstage (type_chase_named at all 4 N_RETURN sites) stayed correct. Re-key the catch-all on the RESOLVED tinfo (chase TY_NAMED -> TY_ARRAY/TY_STRUCT) so wwstage LOUD-STOPS (rule 7) instead of miscompiling. cstage stays correct; the full wwstage tinfo-kind dispatch (align UP, byte-id) is #277. Established wwstage-stricter divergence (cf #264), no bootstrap consumer (990-997 green). - #276 citations at-site (both stages): the cstage >24B array-literal return loud-stop and the <=24B STRUCT global-receive residual now cite #276. The wwstage >24B array-literal routes through the tinfo-keyed catch-all (#272/#276/#277). Correction: ALL <=24B struct globals truncate symmetrically (byte-id-clean), not only float-bearing -- #276 broadened. - Cosmetic: fix a double-encoded U+2264 (mojibake) in the cgen.c commit-2 comment. combined.ww regenerated (#110). |
|||
| 0d39129741 |
w6c+wwstage: aggregate return from any addressable source (#272 commit-1)
The N_RETURN aggregate arms gated the return source on N_IDENT || N_STRUCTLIT; every other aggregate rvalue (array literal, o.field N_DOT, a[i] N_INDEX, *p deref) fell through to the scalar-AX default = a silent 8-byte truncation. Both stages emitted byte-IDENTICAL wrong asm, so the byte-id gate could not catch it (#263 class) — the fix converges on the runtime oracle. Mirror the arg-side closure #271 landed: both arms (≤24B @retscr and >24B sret) now funnel N_ARRLIT through the literal element fill and N_DOT/N_INDEX/deref through aggarg_srcaddr + the #265/#268 whole- aggregate copy. Type-agnostic, so struct AND array returns are closed. A close-by-construction loud-stop (rule 7) guards any future unhandled aggregate source from reaching the scalar default. Closes the callee-half of (b)/(c) and the addressable siblings. The g = mk() global-receive caller-half is commit-2. 949_aggret_source_run pins the class: array-literal / N_DOT / N_INDEX / deref / named-ident control / >24B-sret-deref / struct-field / struct- deref, each summing all members (full readback) with per-row byte-id. |
|||
| 3c37b98164 |
w6c+wwstage: [N]struct literal element store (#270-1c)
`let x: [2]inner = [inner{..}, inner{..}]` left the array unpopulated:
the N_ARRLIT per-element store handled scalar/str/float ONLY, so a
struct/array/tuple element hit the multi-word-store gap and stored just
the first 8 bytes (cs0/ww0). Both stages symmetric-broken; converge on
the populated result (#263).
Fix: an aggregate element of an array literal fills each element slot
from its source — cg_structlit_fill_bp for an N_STRUCTLIT element,
word-copy for an N_IDENT element (reusing COMMIT 2's per-element copy
shape). esz is the element's natural size (cstage esub->size). cgen.c
N_ARRLIT arm + cgenstmt.ww cglet. An aggregate `...` repeat and other
element shapes hard-stop loud (rule-7).
949 rows: arrlit_structlit, arrlit_structident (8B struct, byteid=1,
full readback). All 96 pass; test-unit 241 green; smoke OK.
|
|||
| 6f18f42a4a |
w6c+wwstage: &aggregate-array-element addressing + store/copy (#270-1)
The array-of-struct element store/copy family — one primitive (&(array element) for an AGGREGATE element, used as address, never deref/truncate) across three consumers. Both stages were symmetric-broken; converge on the runtime-correct full-address/full-copy (#263). (1a) `a[i].m[j] = v` (a:[N]struct) segfaulted: the `arr[i].field` arm computed &a[i] then DEREF'd it (loaded the struct's first 8 bytes as a value) for an `[N]T`-typed field → garbage base. Now an array-typed field of an array element leaves the field ADDRESS (the #135 read-side, applied to the array-element base). cgen.c arm + cgenexpr.ww cgdot N_INDEX-lhs branch. (1b) `a[i] = aggregateval` truncated the copy to an 8B MOVQ. New aggregate (struct/array/tuple >8B) element-store branch word-copies the element from the rhs source address (ident / N_DOT field / `*p` deref) — the WRITE-twin of the #268 let-init loop. cgen.c N_INDEX store + cgenexpr.ww cgassign. (3a) `let c = x.arr[i]` (N_DOT base) / `let c = a[i][j]` (nested) dropped the copy: the #268 let-init N_INDEX source-addr arm was N_IDENT-base- gated. Now computes &base[idx] via cg_dotbase_addr (N_DOT field) or the &abase[bidx] spine (nested N_IDENT-array base). cgen.c N_LET + cgenstmt.ww cglet. 949 rows: elemfield_store, elem_struct_store, elem_arr_store, letcopy_{dot,nest}_prim, letcopy_subarr (byteid=1); letcopy_{dot,nest}_ struct (byteid=0 — run-correct, byte-id blocked by the orthogonal value-nested-struct frame divergence #254). All 94 pass; test-unit 241 green. |
|||
| ebbc3f98c2 |
w6c+wwstage: array return-by-value via the struct-return ABI (#267 fold-2)
Wire TY_ARRAY into the existing struct-return gates so arrays ride the same reg-class (<=24B in AX:DX:CX) / sret-class (>24B) path the struct return ABI already emits byte-identically. No new ABI machinery. Both stages, uniform gate-widen: - cg_sret_retsize / sretretsize: +TY_ARRAY (natural size sub.size*len, the type table) -> auto-enables sret send/recv + the >24B sret N_IDENT word-copy + return-forward, all keyed on the shared sret SSoT. - cgreturn <=24B reg-send: +TY_ARRAY (N_IDENT scratch word-copy -> AX/DX/CX). reg-class return-forward rides the default cgexpr passthrough. - let-init / assign <=24B recv: +TY_ARRAY (AX/DX/CX sized stores). struct_float_class stays struct-only: pure-int element arrays only; no pure-float-array-return consumer exists today. 949 +11 rows: reg-class 8/16/24B + sret-class 32B, [N]u32 and [N]u8, at let-init/assign/return-forward, full-member readback, + a struct- return regression control. All cstage-run + cs==ww byte-id. |
|||
| 35b517ca3e |
w6c+wwstage: aggregate let-init copy from a struct-DEF global (#268 reviewer)
The fold-1b unified arm (
|
|||
| bb2f4e1dfe |
w6c+wwstage: aggregate let-init copy for ident-array/N_DOT/N_INDEX rhs (#268 fold-1b) — close addressable-rhs copy family
#265 fold-1 landed the deref-rhs aggregate copy as one slot→slot memcpy loop fed from a source address in SI. fold-1b adds the remaining addressable-rhs source-address setups, all routed into that SAME loop: - array IDENT `let c: [N]T = s` — LEAQ the source slot into SI. Pre-fix both stages truncated to the 8B scalar tail. - N_DOT field `let c: A = o.i` — cg_dotchain_addr / dotchainaddr (#253) lands &(o.i) in SI. Pre-fix truncated to 8B. - N_INDEX element `let c: A = a[i]` — the &base[i] spine (#252: scaled index + LEAQ base) lands the element address in SI. Pre-fix scalar-loaded the element address as a value → segfault. Size (the #254 non-slot-padded ABI extent) comes from the declared let type for every shape (lu->size / structabisize|tinfo.size), independent of the rhs; only the per-rhs address setup differs. The deref arm becomes one branch of the unified arm. Struct-IDENT keeps its own #32 slot-copy arm above (unchanged). With those, the whole addressable-rhs let-init-copy family is closed by construction: struct-ident / array- ident / deref / N_DOT / N_INDEX all full-copy, both stages byte-identical (rule-10). 949 gains 9 full-readback rows (every member written distinct + summed, so a partial copy fails): array-ident 16B/32B + 12B(MOVL)/11B(MOVW+MOVB) tails; N_DOT struct-field 16B + array-field 32B + 11B-tail struct field; N_INDEX struct element 16B/32B. The N_INDEX source array is populated through a `*inner` to `&a[i]` (the #135/#252 store path) because the array-of-struct element direct store (`a[i].m[j]=v` / `a[i]=s` / struct- array literal) segfaults on a SEPARATE pre-existing bug, reported alongside this fold. w6c+wwdump combined.ww regen (#110). 70/70 949, test-unit 241, sizelint, smoke green. |
|||
| 4d3f8467a8 |
w6c+wwstage: full-size aggregate copy for deref-rhs let-init (#265 fold-1)
A `let c: T = *p` (T a struct or array, >8B) copied no full aggregate: cstage dropped the init entirely (c read garbage); wwstage emitted only the scalar `MOVQ AX,off(BP)` tail (first 8 bytes). Both wrong, differently — converge BOTH stages on a size-driven slot-to-slot memcpy: cgexpr the deref operand to the source address in AX, MOVQ AX,SI, then a MOVQ run plus a sized MOVL/MOVW/MOVB tail over the #254 non-slot-padded ABI extent (lu->size / structabisize for a struct, tinfo.size for an array). Mirror arms in cgen.c N_LET and cgenstmt.ww cglet, byte-identical (rule-10). Unblocks sha256's faithful `let copy = *h`. The by-value aggregate RETURN ABI (array/struct return truncates to AX) is fold-2 (#267, deferred). 949 gains 6 full-readback rows (every member written distinct + summed, so a truncated copy fails): struct{[4]u32} 16B, struct{[8]u32} 32B via both *(&s) and *p (sha256 shape), bare [4]u32, and non-8-mult tails ([3]u32 12B → MOVL, [11]u8 11B → MOVW+MOVB). w6c+wwdump combined.ww regen (#110). 61/61 949, test-unit 240, sizelint, smoke green. |
|||
| 0afc272f47 |
wwstage: copy full tagged-element slot for N_DOT/N_INDEX-base index read (#261)
The #259 store fix unmasked a pre-existing latent cs!=ww in the tagged- element READ via an N_DOT base (`x.o[i]`) / chained N_INDEX base (`m[i][j]`): wwstage materialized the element as a SCALAR one-word load + zeroed tag where cstage copies the full tagged slot — silently dropping the tag/payload-high word (wrong variant). Three sites all keyed off the same N_IDENT-only gate; cstage classifies TY_TAGGED for ANY base off the checker-stamped element type. Align wwstage UP: - cgindex (cgenexpr.ww): the N_DOT/N_INDEX-base arm now sets elem_tagged/elem_slot_sz from n.type_ (the stamped element tinfo), mirroring cstage cgen.c:8101 — the full-slot copy arms then fire. - rhstaggedabicall (cgenutil.ww): the N_INDEX branch reads typeistagged(src.type_) for any base instead of an N_IDENT-only structural lookup, mirroring cstage's src->type keying — fixes the let-init / call-arg widen-source spill. - forwardtagged (cgenstmt.ww): the return-path passthrough gate now accepts N_INDEX/N_DOT tagged rhs (which cgexpr materializes into the tagged ABI), not just N_CALL — fixes `return x.o[i]`. read + call-arg + return + chained 2D all close by construction (one materialization path). cstage unchanged (pure wwstage-align-up). 949 gains 9 #261 rows (i32 + explicit-void variant per shape proves the tag survives) and flips the two #259 read-back rows to byteid=1. |
|||
| be23d7227a |
w6c+wwstage: source sub-8 value-struct ABI-size from tinfo.size at zero-init+DATAW (#254)
wwstage conflated SLOT-size (round-to-8, for frame) with ABI-size (true)
for a nested value-struct. A nested value-struct field is sized via
fieldsize() (TY_STRUCT -> ti.slotsize = 8), poisoning structabisize and
registerstruct si.totsize to 8 for a struct whose true ABI size is 4.
Two emission sites then over-sized, both SILENT cs!=ww divergences:
D1 (local, cgenstmt.ww cglet): zsz = structabisize = 8 hit the
`zsz == 8` zero arm (#213) -> a stray `MOVQ $0, off(BP)` cstage
never emits (ABI 4 is sub-8 -> left uninit per the shared no-rhs
zero-init policy).
D2 (global, cgen.ww emitletdataw): the struct zero arm wrote
letemitsize/si.totsize = 8 DATAW bytes; cstage cg_let_emit_size
returns u->size = 4.
Fix sources the zero-init extent from the type table's tinfo.size
(peeling TY_NAMED) at both sites — the same value cstage reads
(cgen.c:8397 / :978). fieldsize / registerstruct / frame slot-padding
stay UNTOUCHED: moving the fix into the size helpers would shift
nested-struct field offsets and re-diverge other byte-id. Pure
wwstage-align-down; cstage cmd/w6c/cgen.c unchanged.
Test 949_valstruct_subsize_run: D1 local + D2 global over ABI sizes
1/2/4 (the whole sub-8 / non-8-multiple class), each cstage-run +
cs==ww .s byte-id; plus a >8 (16B) local+global NEGATIVE control
proving the fix didn't disable legitimate multi-word zero-init.
Regen w6c + wwdump main.combined.ww (cgen is compiler-imported, #110).
|
|||
| 5d023c0ef0 |
w6c+wwstage: cgexpr materializes tuple rvalues + unwrap-shift for tuple-payload destructure (#241)
cgexpr could not produce a tuple VALUE, so a destructure / let bind of an
RVALUE tuple read garbage past the first element (cstage) or left an untyped
binder aborting wwstage's asserttyped gate — a DANGEROUS gate-blind cs!=ww,
and the strconv-int blocker (Hare's stoi64/stou64 require
`let (sign, u) = parseint(s, base)?`). Three feeders, all routed at the same
SysV register-return cursor the cgmlet/cgmassign consumers already read:
- an N_TUPLE literal fell to the `cgexpr_int(0)` / `MOVQ $0, AX` default;
- a tuple-typed IDENT loaded only word0 into AX (`yield t`, `return t`,
`let q = t`), leaving DX/CX stale;
- the `?`/`!` unwrap of a tuple-in-union payload lifted only word0->AX,
stranding word1 in CX (the scalar/str success ABI).
Fix (both stages, byte-identical per rule 10):
- cgexpr packs an N_TUPLE literal into the cursor (cg_tuple_lit_to_cursor /
cgtuplelittocursor — a byte-identical reuse of cgreturn's in-register
N_TUPLE arm) and a tuple IDENT from its slot at the register-ABI stride
(cg_tuple_slot_to_cursor / cgtupleslottocursor);
- the ?/! unwrap shifts a tuple success payload down one integer reg past
the tag (cg_tagged_tuple_payload_shift / cgtaggedtuplepayloadshift),
loud-stopping a float/slice/str payload element (the SysV per-eightbyte
tagged-tuple-payload classification is #243);
- wwstage's checker recovers the popped match-arm binder type for a
`yield <binder>` operand (matchyieldtype's scope-free fallback to the
arm's declared type), so the destructured binders stamp — cstage reads
the operand's already-stamped ->type, wwstage caches only a tinfo.
Over-cap rvalue-tuple materialisation (no slot to sret a bare expression
value into) loud-stops both stages — the #10 follow-up.
NOT closed (distinct root, deferred to #238/task #6): single-var
`let q = (true, 9u64)` then `q.N` — the N_LET tuple-init sz==16||32 gate
drops a narrow-first mixed tuple, and the N_DOT tuple-field PACKED-offset
reader disagrees with tuple_store's 8B stride. Not the rvalue-into-cursor
fix and not a strconv blocker (strconv destructures); documented at the test
header.
Test 945_rvalue_tuple_destructure_run: literal destructure, match-yield
destructure, and the ?-call strconv shape, each run + cs==ww byte-id on both
drivers (9 checks). Embedded w6c/wwdump combined.ww regenerated.
|
|||
| 6fc85f9aaf |
w6c+wwstage: construct + bind tuple-in-union payload (#242)
A mixed-scalar tuple WRAPPED IN A TAGGED UNION (the (neg, n) shape Hare's
strconv parseint returns, ((bool,u64)|invalid|overflow)) miscompiled three
ways, all gate-blind (no bootstrap tuple-in-union):
(a) cstage CONSTRUCTION: a tuple variant fell through the N_RETURN scalar
shuffle, which ZEROED tag + payload — the operands were never packed.
Route the tuple variant through the scratch-slot widen path; add a
TY_TUPLE arm to cg_widen_tagged_store that packs each element into the
union payload at the register-ABI 8B stride + sets the variant tag.
(b) wwstage CHECKER: `let (a,b)=t` over a plain tuple ident (the match-
bound union payload) left the un-annotated binders UNTYPED, so the bin
node reading them was untyped -> asserttyped abort. The element-type
distribution only fired for an N_CALL rhs. Consume the rhs tuple type
for ANY rhs (mirror cstage check.c:2017).
(c) BOTH stages DESTRUCTURE: the register-cursor receive assumes the rhs
left every element in AX/DX/CX (a call's tuple-return ABI). For a tuple
IDENT cgexpr loads only word0->AX, so the 2nd binder read a STALE DX.
Copy each element from the ident's slot at the 8B stride.
Construction is correct at ANY variant position (the resolved tag, not a
default 0); wwstage resolves it via the typeeq core (flatvariantidxt), not
taggedvariantindext whose str/slice shape-fallback would mask a mismatch.
Two rule-7 loud-stops cover shapes this slotted packing can't yet handle,
on BOTH stages, so neither silently miscompiles:
- a tuple with a SysV-eightbyte-sharing narrow pair (e.g. (i32,i32,u64)),
caught by the 8+payload > slot-size guard (the eightbyte tuple
classification is #243);
- a tuple built from a BARE LITERAL element (`true`/`false`, suffix-less
`7`). cstage's cg_tag_for_variant can't type the literal (#241), returns
-1, and loud-stops. wwstage types `true` as bool and `7` as untyped_int,
so flatvariantidxt WOULD resolve the variant — a program cstage rejects
but wwstage accepts is the cs!=ww divergence rule 10 forbids. wwstage
mirrors cstage's CONDITION (a bare-literal element), not its -1
mechanism, with an explicit guard that aligns the richer side DOWN. Lift
BOTH guards together when #241 lands cstage literal typing -> symmetric
accept.
Test 940_tuple_in_union: 4 K_RUN rows (variant 0, void arm, tuple at
variant 1 two ways) x cstage-run + wwstage-run + cs==ww byte-id, plus 2
K_BUILDERR rows (eightbyte-share, bare-literal) asserting a loud stop with
the #242 diagnostic on BOTH drivers = 16 ok.
|
|||
| b79f005489 |
w6c+wwstage: agree on mixed-scalar tuple sret layout (#240)
An over-cap tuple mixing a scalar with slices/str (e.g. (int,[]u8,str),
56B) laid out differently in the two stages — gate-blind, since no
bootstrap path returns such a tuple. Two silent cs!=ww bugs, one per
ABI side:
- callee SEND (cstage cgen.c N_RETURN over-cap-tuple arm): foff
advanced by the LITERAL expression's type size. A bare int literal
element is stamped TY_UNTYPED_INT (size 0), so `e->type->size`
added 0 for a leading scalar — the next element clobbered it at
offset 0 and every trailing element packed 8 bytes low. wwstage
already sized from the return-type tuple (c.fnret.list), so the
callee frames diverged. Fix: size foff from cg_ret_type's tuple
params (rule-13 type table), aligning cstage to wwstage and to the
t.N reader's f->offset.
- caller RECEIVE (wwstage cgenstmt.ww cglet N_TTUPLE arm): the
in-cap register tuple-receive branch had no capacity gate, so a
56B over-cap tuple was received via AX/DX/CX/R8 (+ R8 fill)
instead of from the sret dest the callee wrote. cstage gates the
twin branch on `sz == 16 || sz == 32` and falls over-cap tuples
through to the sret receive. Fix: add the same size gate to
wwstage, aligning it to cstage.
Both stages now emit byte-identical asm and the value round-trips.
Regen w6c + wwdump combined.ww (cgenstmt embeds in both).
New 940_mixed_scalar_tuple_sret_run: leading/trailing/middle scalar
shapes, annotated + inferred let, each self-asserting every element
(scalar direct, slice/str via len) — both drivers exit 0 + cs==ww
byte-id (12/12).
|
|||
| a937d67377 |
w6c+wwstage: receive over-cap tuple sret returns at the call site (#10 Fold B)
Fold A made the CALLEE emit an over-capacity tuple return (> 4 GP or > 2
SSE eightbytes) via sret, but every receive site stayed loud-stopped, so
such a fn was not yet usefully callable. Fold B wires the call/receive end
by aligning every receive gate UP to the shared cg_sret_retsize() /
callsretsize() > 0 predicate (never a kind), per Rob's (B) ruling:
- single-var-let `let t = f();` cstage gate generalised from
TY_STRUCT&&>24 to cg_sret_retsize(lt)>0; the let's slot IS the
sret dest, the callee writes the whole tuple there, t.0/t.1 read
by offset. wwstage already keyed callsretsize (verified).
- N_ASSIGN-ident `t = f();` same generalisation; global arm
kept TY_STRUCT-only (a tuple-global has no sret-to-symbol path in
either stage). wwstage grows a tuple-local arm (rettupleof gates
it apart from the >24B-struct recv, which keeps its own path).
- destructure `let (a,b) = f();` and `a,b = f();` — the genuinely
new wiring: the callee sret's into the @sretscr discard slot, then
a copy-out loop moves each element to its binding at the SAME
packed offset the SEND wrote (foff += element size), each at its
natural width (#169); a `_` binding skips its store but advances
foff. Both stages, byte-identical.
- return-forward `return f();` cstage forward gate generalised
to the predicate, reusing cg_sret_forward verbatim. wwstage
already keyed sretretsize (verified).
The escape boundary stays loud: arg-pass `g(f())` fatals identically in
both stages (tuple arg exceeds return-cursor ABI capacity).
Test 799 is the runtime net Fold A deferred (byte-id is blind to a
SEND/RECEIVE layout mismatch): the bytes.cut-shaped ([]u8,[]u8) round-trip
over destructure / single-var-let / reassign / return-forward, each both
RUN under cstage and asserted cs==ww byte-identical. Tests 945 (row F)
and 956 (f64x3) flip from asserting the old over-cap loud-stop to
asserting the now-working sret round-trip. combined.ww amalgams (w6c +
wwdump embed the wcc cgen) regenerated. Unblocks #4 bytes.cut/rcut.
|
|||
| 19e6b68d03 |
w6c+wwstage: emit over-cap tuple return via sret callee-side (#10 Fold A)
A tuple return whose SysV register-return footprint exceeds the caps
(> 4 integer eightbytes or > 2 SSE eightbytes) previously LOUD-STOPPED
at the N_RETURN SEND. Fold A makes the CALLEE emit such a return through
the existing >24B-struct sret skeleton:
- classifier (cg_sret_retsize / sretretsize) grows a TY_TUPLE arm:
walk the element footprint over the SAME caps the SEND uses, and
return the tuple's natural total size (type table) when over-cap,
else 0. The gp/sse caps are factored to a single shared SSoT
(TUPLE_GPCAP / TUPLE_SSECAP — cgen.c macros in cstage, cgen.ww defs
in wwstage) consumed by the classifier AND every emit/receive site
(the SEND, the destructure guards, the cgcall arg guard) — so
classify and emit can't disagree in either stage.
- the SEND replaces the loud-stop with a write-through: cgexpr each
element, store it through *(@sretarg) at its packed layout offset
(the t.0/t.1 positional layout), each at its natural width so a
narrow tail stores MOVL/MOVB not an over-MOVQ (#169); the dest base
reloads into DX each step since a wide element clobbers AX/BX/CX.
Then the existing struct-sret epilogue (MOVQ @sretarg->AX; ret).
- the prologue already wires @sretarg when the classifier is nonzero.
The CALL/receive side is deliberately untouched: the N_MLET/N_MASSIGN
destructure loud-stops stay, so an over-cap tuple return is not yet
usefully callable. The end-to-end round-trip arrives with Fold B (#10-B).
Symmetric cstage (cmd/w6c/cgen.c) + wwstage (cgen.ww / cgenstmt.ww /
cgenutil.ww); combined.ww amalgams regenerated. Test 798 asserts the
callee now COMPILES (no loud-stop) and w6c vs w6c_ww .s byte-identical
across all-wide, str, narrow-tail, and float-over-cap shapes; no runtime
row (uncallable until Fold B). All 236 pass incl. 990-997 byte-id.
|
|||
| c4e29df4e9 |
wwstage: zero-init 8B composite locals in bare-let to match cstage (#213)
wwstage's cglet no-rhs path zero-inited only 8B primitives (MOVQ) and >8B composites (XORQ run), so an 8B *composite* local (single-field struct/tagged, e.g. struct{src:*vtable}) declared bare (let b: box;) was left uninitialized -- reading an unassigned field returned stack garbage (a silent read-before-init), and it diverged from cstage which zero-inits any 8B local (cs!=ww byte-id, surfaced by #5's bufio box{src:io.stream}). Add the missing arm: a non-array composite of size 8 emits MOVQ $0, matching cstage's no-rhs sz==8 zeroing. cstage unchanged (already correct -- align wwstage UP). Scope is 8B-only: cstage does not zero-init sub-8 composites either (sub-8 falls through to nothing on both stages, already cs==ww), so zeroing sub-8 on wwstage would create a new divergence; the sub-8 read-before-init garbage is a separate shared-both-stages latent (#20). Adds test/wcc/790 (8B byte-id row + read-before-init correctness lock reading 0 on both stages). rule-10 align-up; closes the #213 8B-composite slice; unblocks post-eFinal #5.
|
|||
| 34c437fd63 |
wwstage: register error-structs in collectstructs so large-union struct-variant returns widen (#9)
collectstructs registered a struct only when the typedecl body is N_TSTRUCT, so an error-struct (type X = !struct{...}, whose body is N_TBANG{N_TSTRUCT}) never entered wwstage's c.structs table. The name-keyed structlookup then missed at the return-widen sites, and wwstage dropped the struct construction when returning a struct variant of a large (>4-eightbyte) union -- wrong runtime value and cs!=ww. cstage has no struct name-table (pure tinfo) and was correct. Peel the N_TBANG body in collectstructs so error-structs register; both existing cstage-mirrored widen arms then fire. Provably byte-id-inert: no committed source defines a !struct today. Adds test/wcc/785 (struct-variant return + named-void control, both-stage byte-id + runtime). The >4-eightbyte 5th-word truncation on return remains, symmetric (cs==ww) and unread by the tag/early-word path; #222's sret hidden-pointer cutover is the committed fix (table-retirement tracked as the wwstage->tinfo SSoT arc). Aligns wwstage up to cstage (rule-10).
|
|||
| fb53b4798b |
wcc: TYPE-based forward-tagged predicate in wwstage cgreturn (#201)
cgreturn's forwardtagged detection was keyed on the CALLEE NAME (N_IDENT/N_DOT only via fnretlookupmod), so any other callee shape fell through to the variant-tag synthesis path — clobbering the just-returned AX/DX/CX/R8 tagged-ABI words. The deref-call case `(*r)(...)` (impl-e1-resume STOP, 994 w6c_ww byte-id red) was the proximate trigger. Replace with a TYPE-BASED predicate over the checker-stamped tinfos (rhs.type_ vs c.fnret.type_), mirroring cstage cgen.c:8007 passthrough. Peel TY_NAMED on both sides then identity-check the underlying TY_TAGGED — sufficient for the NAMED case because tinfocache memoizes per typedecl (#191 lineage). Variant-pointer fallback walks the params chain when identity fails so anonymous unions like the cross-module (i32 | void) shared between strings.byteindex and bytes.index still forward correctly; full recursive tinfo structural-eq is gated by #178 (typeeqast's TY_TAGGED arm conservatively returns false today). Probe 770_return_tagged_forward covers 6 rows — IDENT forward, widen non-matching, deref-call (the bug), scalar (sanity), nested call, cross-module forward — each gated on cstage runtime + wwstage runtime + cs.s == ww.s byte-identity. |
|||
| 66a91c8969 |
wcc: converge let-IDENT memcpy, IDENT-assign recv, nested struct call-recv onto structabisize
Three more wwstage cgen sites still used unrounded structnaturalsize where cstage rounds via lu->size — pre-existing gate-blind cs!=ww latents the #169b reviewer surfaced: cgenstmt N_LET struct-IDENT memcpy (let p2: T = p1; twin cgen.c:7869), cgenexpr N_ASSIGN N_IDENT-lhs register RECV (s = mk(); twin cgen.c:4700-4737), and cgenutil's nested struct N_CALL recv inside cgstructlitfill (twin cgen.c:2121). Converge all three onto structabisize, completing the same-class closure started by #169 and continued by #169b. Also corrected the inline comment at cgenutil.ww:3273-3286 that wrongly claimed fl->type->size was natural (check.c:760 sets ABI). sretretsize at cgenutil.ww:1301 is gate-equivalent natural and is left alone. Probe 698 +3 rows (one per converged site) with cs==ww .s byte-cmp and a pre-fix-rebuild discriminator. 990-997 byte-id hold. |
|||
| 39f9267bc9 |
wcc: converge DOT-recv, structlit-fill, and bare-let zero-init onto structabisize
Three wwstage cgen sites still used the unrounded structnaturalsize where cstage rounds via lu->size (check.c:760), pre-existing gate-blind cs!=ww latents flagged in #169's reviewer notes: cgenexpr DOT register-RECV for obj.f = mk() (~5175/5393/5628/6164); cgstructlitfill's TK_ELLIPSIS zero-fill branch (cgenutil); and cglet bare 'let z: T;' zero-init of a maxalign<8 struct (cgenstmt). Each produced MOVQ-vs-MOVL or wider-write divergence vs cstage on the trailing word of a sub-eightbyte tail. Converge all three onto the maxalign-rounded structabisize the #169 work established at the register-ABI sites (cite cstage cgen.c:7720 RECV twin + cgen.c:2085 cg_structlit_fill). cgstructlitfill's signature drops the external totsize parameter in favor of one internal source; the field-walk path is untouched, only the ELLIPSIS zero-fill uses the ABI size. cglet's slot allocation stays on the frame size; only the zero-fill extent uses ABI. Gate-blind (the bootstrap exercises none of these shapes); covered by 5 new rows in probe 698 with cs==ww .s byte-cmp and a pre-fix-rebuild proving the exact MOVQ-vs-MOVL discrimination. 990-997 byte-id hold. |
|||
| d92c199d25 |
wcc: retire exprfloatkind, read the checker stamp directly
exprfloatkind was wwstage cgen's structural float-classifier — a workaround for the checker stamp being untrustworthy. With the previous commit arming the asserttyped bail, every checked value-node is now stamped (or cited-exempt), so its job collapses to a 2-liner reading n.type_ — the same path cstage cgen has always taken. Retire it: inline the stamp-read at its eight sites (cgcast, cgun, cgbin lhs+rhs, cgcall pop, pushargsrev, cgwidentaggedstorebp, cgreturn x2 collapsed), delete the wrapper, and delete the two residual sibling-evidence loud-aborts (cgbin float-arith, cgwidentaggedstorebp float-arm) — their operands are real source value-exprs the armed bail now stamps, so the guards can never fire. One synth-post-checker value-node remained outside the bail's reach: the variadic-slice descriptor pushed in pushargsrev/cgcall (cgenexpr.ww). Stamp it at synthesis with the variadic param's []T slice tinfo so the inlined reads see a stamped node, no nil special-case. Byte-id-neutral by design (slice tinfo and nil both read non-float); 990-997 confirm. Closes the bail-rearm arc — wwstage now reads the same float-class SSoT cstage does, the gate-blind float-classification family is closed, and the build+test corpus is asserttyped-clean by construction. |
|||
| c882bcf27c |
wcc: struct-return float fields via SSE return regs (#171a)
The RETURN twin of #165: a qualifying float-struct was returned GP-only (struct{f64,f64} in AX/DX instead of X0/X1) — value-correct via GP transport but not SysV register-class conformant. Route each float eightbyte through the SSE return cursor (X0/X1) and each integer eightbyte through GP (AX/DX) via independent cursors, at the struct-return SEND and RECV, both stages, reusing struct_float_class verbatim. Closes the temporary tuple-SSE/struct-GP divergence opened across #164/#165. A qualifying struct has >=1 lone f64 so maxalign is 8 and the ABI slot is an 8-multiple — no sub-8 tail — so #169's sized tail is unreachable here and the integer eightbyte uses a full MOVQ (cstage agrees, proven by the f64i32 cs==ww byte-id). f32 / multi-float-per-eightbyte stays GP (deferred #171b); >16B stays sret. Gate-blind and value-correct, so the discriminator is the SEND/RECV register class (MOVSD X0/X1 vs MOVQ AX/DX) — covered by probe 946_structret_run. |
|||
| d4e500f61c |
wcc: struct ABI size maxalign-rounded via structabisize (#169)
wwstage struct-return RECV and RETURN used unrounded / round-to-8 sizes where
cstage uses the maxalign-rounded lu->size / rt->size, so a struct with maxalign
8 and a sub-8 tail (e.g. struct{i64,i32}) — or a maxalign<8 struct on the
return path — unpacked with a different trailing-word width (MOVL vs MOVQ)
between stages. Value-correct either way, but a cs!=ww asm divergence.
Add a dedicated structabisize = round(natural, maxalign) used only at the two
register-ABI sites. structnaturalsize stays unrounded: cstage's >24B sret and
memory-move path (cgen.c:8150, Task #33) genuinely uses the unrounded natural
size, so the two are different sizes — rounding the shared metric breaks 995.
maxalign derives from each field's tinfo.align (mirrors cstage check.c:708),
not an fsz ladder (a ladder over-rounds composite [N]u8 fields).
Gate-blind (no bootstrap struct hits the maxalign-8+tail shape) — the
discriminator is the cs==ww .s byte-cmp; covered by probe 698.
|
|||
| 153c7b3b46 |
wcc: multi-float tuple return via SSE cursor (#164, #107)
A multi-float tuple return mis-routed: SEND pushed a stale AX leaving the float stranded in X0, while RECV (#105) read every float from X0 — so a (f64,f64) return collided both floats. Add an SSE cursor [X0,X1] parallel to the GP cursor [AX,DX,CX,R8], placing each element by its SysV class + within-class index (ref/qbe/amd64/sysv.c retr), symmetric send/recv across both stages, via a generic tuple_store/tupstore+tupsse helper that #171 will reuse for struct-return convergence. (f64,f64,f64) = 3 SSE eightbytes exceeds the 2-register cap and now fails loud (rule 7) rather than colliding. Unifying the 16B and 32B whole-tuple-single-var branches onto the dual cursor was required for f64+str coexistence; it also fixes a latent str-first single-var bug (the old 32B branch read .ptr from DX while the send placed it in AX). No str-first or 32B tuple exists in-tree, so integer paths stay byte-identical (990-997 green). |
|||
| 4a91bdc8db |
wcc: tagged float-variant return packs float bits via X0-spill (#157)
The N_RETURN tagged-pack scalar-variant arm did MOVQ AX,DX, but a float variant's value is in X0 not AX -> packed stale int (broke stof64/stof32 return (f64|invalid|overflow)). Fix: float variant bridges X0->DX via a stack slot (SUBQ $8,SP; MOVQ $0,(SP); MOVSS|MOVSD X0,(SP); MOVQ (SP),DX; ADDQ $8,SP), gated type_isfloat/exprfloatkind. No MOVQ-xmm->gp form exists, hence the spill (715-class, cgreturn-register-pack twin of 715's store-to-slot). Zero-slot-first -> deterministic f32 high-4. AX-independent -> also resolves the multi-variant cs!=ww. Bootstrap-NEUTRAL (compiler has no float-tagged-return). Test 707 +3 rows (f64/f32/multi, slot+8 bit-exact; f32 no-f32-arg to isolate #143). Make test 184/184 incl 990-997 byte-id. |
|||
| d960971c6e |
wcc: continue runs post-step in 3-clause for and range form (#138)
`for (init; cond; post) { ... continue; ... }` and `for (let i .. xs)
{ ... continue; ... }` now emit a `post` (3-clause) or `rpost` (range)
label between the body and the JMP back to the cond-test. `continue`
jumps to that label, runs the post-step, then re-tests the loop
condition — mirrors C/Go/Hare semantics. Pre-fix both stages emitted
`JMP loop_top` for continue, SKIPPING the post-step → the value that
triggered continue never advanced → silent infinite loop on the first
matching iteration. Found by impl-strconv-fold2 during the fold-3
decimal.ha port: `leftshift_newdigits`'s `for (... i+=1) { ... else
if (d.digits[i]==p5[i]) continue; ... }` would infinite-loop at the
first equal digit.
BOTH stages were identically buggy → 990-997 cs==ww byte-id held →
gate-blind. Bootstrap audit (`grep -rE 'for \(let .*\.\.' lib/
selfhost/`) confirmed zero existing callers with continue in either
the 3-clause or range form; bootstrap-NEUTRAL.
Sites: cmd/w6c/cgen.c N_FOR + N_FORRANGE; selfhost/cmd/wcc/
cgenstmt.ww cgfor + cgforrange. 1-clause `for (cond)` byte-id
preserved (cont_target stays = loop_top when n.rhs == nil). Rule-11
carve-out: 3-clause and range share the lowered structure; fixing
one without the other would leave the same silent miscompile in
N_FORRANGE — one-class closure on the continue-skips-post bug, same
precedent as #133-expanded.
911_continue_run: 4 rows. for3_skip_one (lead's repro, was infinite
loop, now 4), for3_skip_two (nested continues, 30), range_skip
(Hare-range continue, was infinite loop, now 120), for1_continue_
byteid (1-clause regression assertion — bootstrap shape unchanged).
Pre-existing parser-side divergences (cstage silently drops post in
the never-used 2-clause `for (cond; post)`; wwstage doesn't support
infinite `for {}`) deferred to #139 — not in decimal.ha, no shared
class with the cgen continue-skips-post.
|
|||
| 0a2747ce5b |
wcc: collapse exprfloatkind structural body onto checker stamp (#121)
wwstage exprfloatkind now reads n.type_ via classifytinfo (typeisf32→1, typeisfloat→2, else 0) — wwstage classifies float-ness from the cstage- equal stamp; delete the 9-arm structural body + the transient bridge. Permanent residual sibling-evidence guards: cgbin float-arith (primary, fires on unstamped float operand), tupstore + cgwidentaggedstore float arms (pins, unreachable today, contract assertions against future regression). cgcast excluded (int→float CVTSI2SD legitimately nil-typed). Closes the gate-blind cs≠ww float family root (STATUS:14, named (b)-phase goal). Builds on |
|||
| 7d7ed964b0 |
wcc: f32 array-element store from X0 (#122)
Float array-element stores (array-literal init, [v...] repeat-fill, and arr[i]=v) now route from X0 via MOVSS/MOVSD in both stages; the AX path stored the raw double low-bits, garbage for f32 (f64 worked by accident). A clobbering call-index (a[geti()]=v) loses the X0 value — deferred to #125. |
|||
| ec19d0ad20 |
cgen: tuple receive spills f64 word from XMM, not integer reg (both stages, #105)
A (f64,i64)/(i64,f64) tuple returns its f64 word in X0 (the SSE return reg) and its integer word in an integer reg (tuple_rseq AX/DX). All three tuple-from-call receive forms — single-var (cglet), destructure (N_MLET), reassign (N_MASSIGN) — share the #83 tuple_rseq cursor and all spilled the f64 word via MOVQ from the integer cursor; that reg holds garbage (the float is in X0), and #103-FACE-Z's field read (MOVSD slot,X0) then reads it. A single-return callee masked it (a float-literal return leaves the f64 bits in AX, and X0 stays live); a branched callee with a non- literal f64 word has an inner CALL clobber AX, exposing the corruption. Make every receive spill class-aware: an f64/f32 word spills MOVSD/MOVSS from X0 (the single SSE return reg, which survives the reg->mem stores regardless of the word's position), an integer word spills MOVQ from its tuple_rseq reg as before. cstage applies this at all three inline sites (cglet, N_MLET, N_MASSIGN); wwstage at the cglet branch and in the shared tupstore helper (covering cgmlet and cgmassign). The integer/str/slice path is byte-identical to before, so bootstrap codegen is unperturbed. Multi-float tuples collide on X0 at the RETURN (#107), out of scope here. |
|||
| 6a586cf792 |
cgen: wwstage cglet spills both eightbytes of a 16B tuple-from-call receive (#102)
wwstage-only. cglet had no 16B whole-tuple-from-call receive branch, so `let t = call()` whose callee returns a 2-eightbyte (16B) tuple fell through to the generic single-word store (MOVQ AX, off(BP)) and never spilled word1 (the DX eightbyte) — silent loss of t.1. Align to cstage cgen.c:6652, which spills both AX->off+0 and DX->off+8. Not a tupstore cursor off-by-one and not f64-specific: the destructure form `let (a,b) = call()` (cgmlet + tupstore cursor) was already byte-id; only the whole-tuple N_LET receive dropped word1, for any element mix incl. all-integer (i64,i64). An f64 element surfaced it first. The f64 element rides its eightbyte in AX/DX at receive and is re-read from X0/XMM at field-read (already byte-id), so no SSE cursor is needed. |
|||
| 5a0427ef32 |
cgen: N-ary tuple destructure positional store + loud-stop (both stages, #83)
Replace the str-only XOR (e0_is_str ^ e1_is_str) at the tuple send
(N_RETURN) and receive (N_MLET/N_MASSIGN) sites with a positional
per-element register cursor, mirroring harec create_unpack_bindings
(ref/harec/src/check.c:1354-1416). Each element rides consecutive
eightbytes over [AX,DX,CX,R8]; a slice/str rides its 3-word
{ptr,len,cap} header (ref/hare/rt/ensure.ha:4-8), a scalar rides 1.
Send and receive walk the SAME type-table widths so element->register
agrees. This routes []u8 elements through the 3-word path (the XOR was
slice-blind, dropping len+cap to the scalar fallback) and closes the
pre-existing (scalar,slice) cs!=ww divergence by construction. cstage
and wwstage emit byte-identical asm.
Both receive sites derive each element's width from the rhs tuple's
element types (n->rhs->type->params / the callee return type) -- the
SAME producer view the send site walks -- NOT the binding type: a `_`
lvalue is an N_IDENT with empty str the checker never type-stamps, so a
binding-typed width mis-sized a wide `_` and desynced the cursor for the
next element (cstage read DX, wwstage R8). harec `_` skips the store but
CONSUMES its tuple offset; the cursor advance honours that.
Loud-stop (rule 7): the register file holds 4 eightbytes; a tuple whose
elements sum to >4 (([]u8,[]u8)/(str,str)=6) cannot be register-returned,
so the send site aborts at compile time citing the return-ABI capacity
(#10) rather than silently miscompiling. The receive loop guards the
same predicate (defense-in-depth). Routed through each stage's EXISTING
pinned-fatal idiom: cstage fatal() (cmd/wcc/err.c), wwstage the inline
os.write(2,...)+os.exit(1) at cgen.ww:604 -- no new diagnostics path.
N_MASSIGN (`a,b=f()`, bare comma, pre-declared) is a retained
ww-EXTENSION beyond Hare's binding-only tuple-unpack (Go/rob-pike
multi-assign, rule-9 carve-out); the loop covers it identically to
N_MLET.
Test 945_tuple_nary_destructure_run: (i64,[]u8)+(i64,str) store+read
len/cap for both N_MLET and N_MASSIGN, a single-str control, a wide-
first blank `_,a=f()` row (the cursor-desync discriminator), and a
([]u8,[]u8) row asserting the loud BUILDERR carries the cited
diagnostic; dual ww/ww_ww drivers.
|
|||
| 3d7c707bd2 |
cgen: ww str for-range loop-var narrows to MOVZBQ -- Phase 2 step-3 Fold 1 (align-up)
Ranging a str (for (let b .. = s)) and reading the loop var back emitted MOVZBQ on cstage (correct u8 zero-extend) but MOVQ on wwstage (the missed case, #14). Align wwstage UP. ww cgforrange derived the element-type node only for slice/array; for a str scrutinee it left elemt=nil, so the loop var registered with no type and localloadop short-circuited to MOVQ. Fix: for a str scrutinee, synthesize a u8 element node (type_ = str.sub = u8, from F1) as elemt, so localadd hands the loop var a u8 tnode and the GENERIC narrow-load fires (MOVZBQ) -- consuming str.sub as F1 intended, mirroring how []u8 supplies its element node. NOT an if-str special-case. cstage already correct, untouched (ww-only). str's own type stays nominal. GATE is the ASM SHAPE byte-id (cstage==wwstage at the loop-var read), NOT a runtime probe: the divergence is runtime-benign (MOVQ and MOVZBQ read the same zero-extended byte) so a runtime test passes both ways and cannot distinguish -- it was a byte-id-INVISIBLE divergence (990-997 green despite cstage!=ww, since no bootstrap input exercises a narrow-read str loop var). Verified fail-pre (the cstage-MOVZBQ vs wwstage-MOVQ 1-line diff) / pass-post (.s byte-identical). []u8/slice/array for-range emission unchanged. test/wcc/940 carries the fixture (runtime corpus coverage, both drivers). main.combined.ww regenerated via the canonical make path. |
|||
| 80527f3868 |
cgen: str a,s=call() N_MASSIGN tuple-elem store -> 3-word -- Phase 2 G3 (both stages)
Reassign-destructuring a (scalar,str) tuple (a, s = call(), N_MASSIGN) stored only the str's ptr (DX->slot+0), dropping len/cap -- the last STORE-cluster gap. Reachable (valid ww; checker accepts str tuple elements) but unexercised in bootstrap (all N_MASSIGN sites returned <=8B tuples). Mirror the N_MLET destructure-store oracle (cgen.c:7475): on the one-str XOR, route the str's 3 words DX/CX/R8 -> slot+0/+8/+16; the slot pre-exists (localfind, not localadd). wwstage has no checker, so it derives str-ness from the callee return-type tuple via fnretlookupmod (structurally identical to cgmlet). Both XOR positions (str at l0 and l1). Kind-gated, never size==24. cstage==wwstage byte-identical. Scope = one-str only, matching N_MLET exactly; str+str-both is unhandled by N_MLET too and is filed as a shared gap (task #22), with WHY-comments at both destructure sites. N_MLET emission unchanged (its edit is comment-only, verified byte-identical). test/wcc/939: table-driven write-then-read-cap over both XOR positions (a,s=mk() and s,a=mk2()); cap!=len via mutation (not a sub-slice, #20); pre-poisoned via a non-G3 let-init; full triple+scalar asserted; fail-before/pass-after on both drivers. Completes the str-cap STORE cluster -- the read/write round-trip is now whole. main.combined.ww regenerated via the canonical make path. |
|||
| 1140a590bf |
wcc: str -> 24B {ptr,len,cap}, 3-reg ABI -- parity with []u8 (both stages)
A ww `str` becomes a 24-byte {ptr,len,cap} value, identical in layout to
[]u8 -- the enabling prerequisite for the Phase 2 `str == []u8` collapse.
Both stages, atomically:
- ty_str 16->24B; str value flows 3-reg AX/BX/CX (was 2-reg); str literals
emit cap (=len).
- str in a tagged union grows to a 32B slot, using the AX/DX/CX/R8 4th-word
path already used by 32B slice-variant unions -- str-variant is now
structurally identical.
- tuple (scalar,str) return: 4-reg AX/DX/CX/R8 + 32B receive, extending the
existing type-keyed return (no sret).
- str == []u8 for index and .ptr/.len/.cap, kind-gated where size-based
dispatch collided at 24B; cstage and wwstage mirror exactly.
- table-driven runtime coverage: test/wcc/928_str_abi_run.c.
Cannot be split (rule 10/11): a 24B str and a 16B str cannot coexist across
the two compiler stages without breaking byte-identity, so the size change
and every dependent ABI/codegen site land in one atomic commit, both stages.
Known follow-ups (zero corpus impact, tracked): str-literal global .cap
static-init; >16B struct by-value (pre-existing); tagged-union
match-scrutinee stage divergence (pre-existing).
|
|||
| 901ddf20b9 |
selfhost/cmd/wcc/cgenutil: tagged-store machinery reads tinfo.params (#68)
cgwidentaggedstore/storebp/cgwidentagremap took a type *node and re-derived the tagged shape via resolvetagged + N_TTAGGED.list walks. Migrate them onto the stamped tinfo (the store-side parallel of #66's match-side flip): dst is now the tagged *tinfo (peel TY_NAMED->du, gate TY_TAGGED), variant lookup + tag-remap read tinfo.params by typeeq, mirroring cstage cg_widen_tagged_store / cg_widen_tag_remap / cg_tag_for_variant (cmd/w6c/cgen.c:1273/1177/503). The N_CAST widening test flips from surface-name streq to `castu==dt || (castu tagged && typeeq(castt,dst))` (cgen.c:1295). Node-form flatvariantidx/taggedvariantindex become thin shims over new tinfo cores (flatvariantidxt/taggedvariantindext) so node-side callers (cgreturn cgenstmt:269, pushargs cgenutil:192) are untouched. The 9 cgwidentaggedstore callers pass node.type_ (each already istaggedtype/slotsize-gated). resolvetagged is retained for its 7 match-side callers. Incidentally retires two latent ww-vs-cstage divergences, both byte-id-neutral on the corpus: the old N_CAST test set castisdst for ANY N_TTAGGED regardless of type equality (cstage guards on type_eq), and the old remap walked the UNflattened src.list (cstage walks the flattened params). Unblocks cgassign's indexvaluetnode drop (#69/#61d). make test 134/134, byte-id 990-997 hold. |
|||
| 3036ba766d |
selfhost/cmd/wcc: type-key tagged variant match (#66, Phase-N step 3)
The user-ruled B-full semantic change: flip tagged-union variant matching from surface-NAME to TYPE-identity (typeeq over tinfo.params), mirroring cstage cg_variant_match (cmd/w6c/cgen.c:451). A cross-module `a.T` != `b.T` and `type linerr=!str` != str are now distinguished by the per-decl TY_NAMED pointer (Phase-N #64). ww has no type_assignable, so the untyped/loose arm keeps the str/slice shape fallback (rule-10 align-down). The 5 helpers (flatvariantidx, flatslicevariantidx, taggedvariantindex, cgtagvariantidx, cgmatch dispatch) flip; nomem propagation (NAMED-name scan, no source value) and the f64 widen arm (float-kind classification, no pattern node) are not arm-by-value discrimination and stay name/kind-keyed. The flip requires value nodes to carry nominal identity. exprtype's N_STRUCTLIT arm stamped the flattened body, so `overflow{}` (overflow=!void) got TY_VOID and missed its variant -- fixed to stamp the per-decl NAMED (mktname(lhs.str) -> tinfofornode reuses the #64 NAMED build/cache, same ptr the union variant resolved to), mirroring the N_CAST/N_IDENT arms + cstage. Returns the body node unchanged (only e.type_ rides NAMED); struct-lit layout is unaffected -- cgstructlitfill is structlookup(name)-keyed, never reads NAMED.fields. The fix now hits all `T{}` stamps, kept byte-id by the #63/#65 structural-walker peels. 931_variant_typekey_run: table-driven, both stages, /tmp-isolated. Two rows widen an alias-FIRST variant from a call (no surface name): `(linerr|str)` str-via-call -> idx 1, `(ec|i32)` i32-via-call -> idx 1. Empirically discriminating: FAILS pre-flip (wwstage falls to the leading-shape variant, exit 10; cstage exit 0) and PASSES post-flip -- locking in the capability byte-id can't reach (the corpus has no name-key/type-key-disagreeing co-variant, which is why name-keying survived). make test 134/134 (byte-id 990-997 green; 995 self-rebuild green). |
|||
| b8e5a921f8 |
selfhost/cmd/wcc: collapse type-kind predicates onto n.type_ (A.6.3b, #46)
The node-keyed kind helpers (typeis8byteprimitive, isstrtype/raw,
isslicetype/raw, istaggedtype/raw, isfloattype, isf32type/raw,
isf64typeraw, isnullabletype) each re-walked TNAME aliases via
aliaslookup and peeled TBANG by hand — duplicating cstage's single-
peel kind predicates at the AST level. After A.6.2 every type-AST
kind these read is tinfo-stamped at check.ww L426-436, and
tinfofornode collapses N_TBANG (check.ww:1145-1152) and the TY_NAMED
chain, so each predicate folds to one tinfo read.
Six new tinfo helpers in lib/ww/typ.ww mirror their cstage SSoT
verbatim:
typeisstr — cstage cgen.c:159 `type_isstr` (TY_STR / TY_UNTYPED_STR)
typeisslice — cstage cgen.c:174 `type_isslice`
typeistagged — cstage cgen.c:516 `type_istagged`
typeisf32 — cstage cgen.c:188 `type_isf32`
typeisnullable — cstage cgen.c:396 `type_isnullable` (reads tinfo.nullable
stamped at check.ww:1309-1318)
typeis8byteprim — cstage cgen.c N_LET sz==8 ladder (slot-pad set)
Rule 9 carve-out per the A.6.3a precedent: each helper has a named
cstage counterpart; the wwstage shape mirrors it directly. The five
dead AST-walking variants (isstrtyperaw, isslicetyperaw,
istaggedtyperaw, isf32typeraw, isf64typeraw) are deleted; the five
remaining callsites (cgenstmt cglet / cgmlet str-routing, cgenexpr
cgdot tuple-field) graduate to the alias-aware isstrtype(c, t).
nullableptrtag stays AST-keyed for now — tinfofornode doesn't
populate TY_TAGGED.params (check.ww:1287-1337 sets size / align /
nullable but not the variant chain), so the tinfo equivalent of
cstage cgen.c:405 `nullable_ptr_tag` can't read params today. WHY
comment at the site cites #50 / A.6.3f as the graduation point,
alongside the variant-index work and the tparam-population glue.
Byte-identity (994/995) is the behavior gate; full `make test` green
at 133/133 confirms.
|