9bd0d8bc8127e19965148c03e15ac60195501c63
266 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 9bd0d8bc81 |
wcc: #5 F1 promote type_chase_named + transitive-peel acceptance align-cs-up
Promote type_chase_named from cmd/w6c/cgen.c (static) to cmd/wcc/type.c (exported via ww.h) and re-route every checker single-NAMED-peel through it: check.c's ~28 inline ternaries + 3 ad-hoc loops, type.c's assignability/untyped/borrow/opaque peels. type_eq's nominal identity (check.c:114) and the resolve machinery guards stay untouched. The re-route IS the acceptance align-up — cstage loud-rejected alias shapes wwstage accepts AND runs Hare-right (F0 census, harec dealiases at every consumer): - #54 binop alias-vs-base: unify_arith gains the harec type_promote arm (ref/harec/src/check.c:1083-1105) — one-sided alias + dealias-equal promotes to the ALIAS side; alias-vs-alias stays rejected. - alias-cond family: if/for/&&/||/! chase-then-bool (harec check.c:2141/2515/3229/3572). assert stays loud (F0 2a symmetric). - #70 field access through 2-level alias chains (ken c3_chain3). - assignability through the full chain (harec types.c:989-996 dealias-both): return/init/assign legs, F0 8b idx/slice walls. - alias-of-ptr deref (harec types.c:19-22 type_dereference). The widening reaches cgen arms whose own single peels then misbehaved — both classes are closed IN THIS COMMIT so no intermediate state ships a loud->silent flip (bisect no-silent invariant): - index family: the 8b acceptance hit ptr-load base + esz=1 (SEGV / prefix-luck) — idx_eff + the N_INDEX read / index-write / &base[i] / N_SLICE (expr + call-arg) / N_FORRANGE / aggarg_srcaddr-index / castsrcprim-dot / match-field base classifies chase. - kind classifiers (ken #61-root-verify v3 find): a 2-level f64 alias param reached cg_isfloat's single peel and classified INT — silent wrong-register-class. cg_isfloat / type_isf32 / fld_isfloat / type_isstr / type_isslice chase. ken's v3 row is pinned with credit. Bootstrap asm is byte-identical before/after (w6c on every main.combined.ww cmp-equal vs a pristine |
|||
| 738d7f481c |
wcc/check: #62 typedecl layout is decl-order-INDEPENDENT — demand-resolve forward refs + loud cycle guard (#69)
check_file resolved typedecl bodies in file order with an eager under->size copy, so any body referencing a typedecl declared LATER read its size-0 placeholder and baked it in: alias size 0, tagged- union maxsz 0 (the F0 m5_match $48-frame under-allocated box), struct field offsets collapsed, array element stride 0 — a whole cstage-only family (7 size()-probe rows, all cs-fail/ww-pass pre-fix). wwstage's demand-driven tinfofornode was order-independent on every row, so this aligns cstage UP to the measured runtime-correct side (the #263-era ruling; rule 10's align-down governs acceptance surface, not layout correctness). Oracle: ken /tmp/ken_62_oracle.md — union size is 8B tag + roundup8(max CHASED member size), a fixed point over the module, never a function of decl order. resolve_typename now resolves a referenced-but-unresolved typedecl on demand via resolve_typedecl (cycle-guarded by Type.resolving); the pass-1.5 loop funnels through the same helper. No consumer can see an unresolved placeholder by construction. CYCLE GUARD — #69 ABSORBED into this rider (rob's rider condition): true typedecl cycles now LOUD-reject on BOTH stages — "circular type dependency" — mirroring harec's in_progress check (ref/harec/src/ check.c:4767 "Circular dependency for '%s'"). Pre-guard: cs silently sized cycles 0; wwstage HUNG on an alias cycle (`type a = b; type b = a` — ken's hang probe /tmp/ken62/c1_cycle.ww, killed at the 20s timeout) and stack-overflowed on a struct value cycle. The check sits at the VALUE-position size consumers only (alias root, struct field, array elem, tuple member, union member), so the legal pointer self-ref (`type node = struct { next: *node }`, the io.stream shape) stays accepted, byte-id. wwstage gets the twin tinfo.resolving flag (lib/ww/typ.ww) + circularnamed in check.ww; its arm loud-STOPS (os.exit) rather than accumulating — wwstage's AST-level alias walkers (resolvealias, aliaslookup chains) follow TNAME->TNAME by name, blind to the tinfo table, and spin on a cyclic alias graph even after the table edge is cut to tyerr (measured); cstage accumulates, its single-peel ternaries cannot loop. TWO-LAYER SPLIT — this is ONE bug number (#62) deliberately split across THREE commits (this rider + F1 + F2), per ken's sizes-correct ≠ payload-correct proof: in NORMAL decl order both stages size the box correctly (16/24, frames $64) yet both still run exit 2 — the box STORE is word0-only, a chase-blind copy-WIDTH lookup in cgen, NOT the type table. EXPECTED-FAIL after this commit: m5b_match1/m5_match stay exit-2 both stages (now byte-id BOTH orders; pre-fix the fwd order was $48-frame divergent). The Layer-2 sites and destinations: - F1 (cstage): cg_widen_tagged_store single NAMED peel, cmd/w6c/cgen.c ~2464 — the type_chase_named census family. - F2 (wwstage): rhsstructpayload bare name-keyed structlookup, no alias chase, selfhost/cmd/wcc/cgenutil.ww:3062 (structlookupchain :1691 already exists). Banked runtime payload-readback rows for F1/F2: /tmp/impl62r_layer2_rows.md. Test 944_alias_decl_order_size_run: every size class pinned in BOTH decl orders (sizes, named union, struct field offsets, array elem, 2-level chain — norm + fwd twins, prefix-luck-breaking last-word readbacks), 3 cycle BUILDERR rows + the legal ptr-self-ref row, (void|base) no-regress control; dual-stage + per-row byte-id (arrelem rows byte-id exempt: pre-existing #60 index-over-alias divergence, order-independent, cited at the rows). lib/ww/typ.ww is an embedded source: both main.combined.ww regen'd + committed (freshness gate). |
|||
| d14a23b85e |
cgen: #57 in-cap tuple cursor fill keys on the DECLARED element type — tagged elems from concrete rvalues widen, both stages
The N_TUPLE literal's stamped type is CONSTRUCTED from its elements
(check.c N_TUPLE keeps untyped/concrete element types; assignability
is consumer-side), so the in-cap cursor fill — count
(tuple_lit_gpwords/tuplitgpwords) + push (tuple_lit_push_elem/
tuplitpushelem) — never saw the DECLARED tuple type. A declared-TAGGED
element whose expr is a concrete rvalue (`return (5: size, 9)` into
(un16, size)) counted ONE word and skipped the widen entirely: 2 words
sent against the receiver's declared 3-word walk, every later element
read garbage. Both stages, byte-identical, gate-blind (ken /tmp/ken57
p8/p9: t.1 read entry-junk). The let-literal twin
(`let t: (un16, size) = (5: size, 9)`) and the tagged-SECOND-elem
shift broke identically (probes q1/q2). The over-cap (sret) arm
already walks declared params (#240/#22b) — only the in-cap path was
declared-blind.
Fix threads the declared tuple type into the ONE shared helper pair
and its two loop sites:
- tuple_lit_gpwords/tuplitpushelem take the declared elem type;
declared-TAGGED + concrete rvalue widens into the shared tagged
scratch (cg_tagscr_slot/tagscradd + cg_widen_tagged_store/
cgwidentaggedstore, the cgreturn tagged-@retscr shape) and pushes
the box words; declared-TAGGED gates the SSE row off (a (void|f64)
box rides INTEGER eightbytes). Tagged->tagged subset (eslot
mismatch) louds — the #23/#40 widening-remap family.
- cg_tuple_lit_to_cursor/cgtuplelittocursor grow a decl param;
cgreturn's in-cap N_TUPLE loops thread cg_ret_type/c.fnret.list
(the same pp/pt walk its over-cap arm does); the N_LET in-cap
tuple arm passes the declared type for an N_TUPLE rhs; the bare
cgexpr route passes NULL/nil (emission unchanged).
Ident-elem sources keep the existing slot-load push byte-identically
(t57_ident_no_regress); the CALL-elem tripwire stays loud (#41,
t57_loud_call_elem). RESIDUAL FILED, not folded (rule 11): the
N_MASSIGN destructure-reassign literal rhs routes through the bare
cgexpr path (decl=NULL) and stays silent-wrong — probe q5_massign,
task #64, cited at the massign arm both stages. The annotated
multi-let spelling (`let (a, b): (un, size) = lit`) does not parse
(both stages), so N_MLET has no declared-literal route.
941 rows t57_*: return (named + inline union), let-literal, tagged
second elem, float payload, bare-untyped payload (rides the #33
chooser through the new wire), ident anchor, loud CALL tripwire;
ken's adversarial shapes (tagged-MID elem, two tagged rvalue elems
incl. void, plain-f64 SSE coexisting with a declared-tagged box), the
in-cap/over-cap boundary loud (k57d), and the NEW #57 tag-remap loud
pinned. Pre-fix at
|
|||
| 80e7096f25 |
cgen: #50 append/insert tagged-dst VALUE boxes PRE-grow, both stages
The #34 value-store dispatch's tagged arm ran the widen choke-point (cg_widen_tagged_store / cgwidentaggedstore — which cgexprs the value internally) AFTER cg_append_grow, so a value expression reading the destination (xs.len) saw post-grow state. Hare evaluates arguments before the call's effects; the scalar/str/slice arms already ordered value-first. insert() inherited the bug through its desugar-to-append ( |
|||
| 24e02b259c |
wcc/check: inferred-let tuple literal carries its slot-layout size — 0-size local smashed saved BP/RIP (#44)
The N_TUPLE expr arm built its TY_TUPLE with size 0 (only the
annotated N_TTUPLE resolve_type route computed the layout), and
type_default passes TY_TUPLE through, so an inferred
`let t = (4: size, 2: size)` planted a 0-size local at offset 0 —
the element stores landed on the saved BP/RIP and main segfaulted
on RET (cstage; the arg shape instead fell to the global-symbol
path and link-failed). wwstage (exprtype N_TUPLE -> tinfofornode)
was runtime-correct throughout — cstage aligns UP to it; all
fixed shapes are now byte-id. Slot rule mirrors the N_TTUPLE twin
and cgen tuple_eslot, with untyped elements sized at their
type_default (element types stay untyped for the consumer-side
assignability contract).
7 table rows in 941 pin the class (cast/bare/mixed/float elems,
destructure-from-local, call-arg, nested); each fails at master
|
|||
| e8977a413d |
cgen: #38a chained-dot TAGGED leaf — cursor read + widen store, value-chain + ptr-chain, both stages
A tagged leaf at the end of a dot chain (depth >= 2) fell to the
one-word scalar tail in FOUR walkers — the value-chain read spine,
the value-chain assign walker, the chained-*struct read branch, and
the chained-*struct assign branch — in BOTH stages, byte-identical,
gate-blind:
READ: one MOVQ pulled the TAG word only; is-tests passed by
tag-luck while as/match/let consumers read stale DX/CX/R8
as payload (ken x5c: o.r.min as size added entry-zero DX —
want 28 got 20; ken b8 same through a mid-chain *rep).
ASSIGN: one sized store landed the rhs ON the tag slot, payload
kept its old bytes (ken x5d: `o.r.min = 8: size` made the
following is-test false).
The single-dot arms were already correct — each fix is that arm
verbatim at the chained site:
read: the tagged cursor (AX=tag, DX=val0, R8=val2 before
CX=val1 — the base may live in CX/AX; >32B box leaves its
ADDRESS in AX, the #37 mem-read convention). wwstage rides
the existing cgloadtaggedfield helper; the *struct-base
branches stage AX into BX first (cursor targets AX).
assign: full slot rewrite via the shared widener
(cg_widen_tagged_store / cgwidentaggedstore, base BX/BP),
plain `=` only — compound on tagged stays loud (#133).
The nested-LITERAL half of the old #38 filing dissolved: the
canonical fill's tagged-field widen delegation (#23,
|
|||
| 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 ( |
|||
| 4c46d3afde |
cgen: #49 aggregate-ASSIGN word0-only family — one mem-to-mem funnel (cg_aggcopy), both stages
Whole-aggregate reassignment `b = a` fell to the N_ASSIGN scalar tail
and copied ONE MOVQ — word 0 of any struct/array/tuple — in BOTH
stages, byte-identical, gate-blind (ken f49_min asm proof; latent
because lib style is let-init, whose #265/#268 copy is full-width).
Same class at three more positions: struct-lit FIELD init from an
ident source (`outer{.., r = r}`, the #38 non-tagged half), the deref
place `*p = s` (#31-A), and the module-let global `g = a` / `g = pt{..}`.
Fix: extract the C1.25 assign-resolver word-copy tail verbatim into
cg_aggcopy/aggcopy — the ONE place-resolved (SI)->(BX) aggregate copy
— and wire it at the N_ASSIGN ident-aggregate arm (local + global),
the deref-place divert into the existing resolver aggregate arm, and
the structlit-fill aggregate-field arm, all fed by aggarg_srcaddr
(the closed #265/#268 dispatch). The new arms key on the FULL alias
chase (type_chase_named / chased stamped tinfo, the #22 precedent) in
BOTH stages — the region's single-peel `lu`/`fu` would miss
`type b = a; type a = struct` on cstage while the wwstage twin fired
(ken R1, gA3b: master cs ran the word0 corruption, exit 2; now 0).
Non-addressable aggregate rhs (tuple-lit, unhandled call shapes) dies
LOUD (rule 7) instead of silently truncating: #31-E `*p = (3,4)` and
#31-G's deref flavor `*p = mk()` are now loud both stages (the INDEX
flavor `a[i] = mk()` stays in the legacy INDEX arm — receive
machinery, not this funnel; still filed under #31). #31-B rides: the
cstage-only <=24B gate before cg_structlit_fill_bp is lifted (the
wwstage twin never gated — a >24B literal reassign was
cs-zero/ww-filled, rule-10 break). Global structlit reassign rides
the existing DST_GLOBAL fill machinery.
Unsplit (rule 11): the assign arm, fill arm and deref divert all
route through the one new funnel (cg_aggcopy + aggarg_srcaddr) in
both stages; splitting by site or by stage would ship a transient
cs!=ww (gate-red) or a funnel with no consumer.
941 t2_reject_chain_arg: the row's tuple-LITERAL field fill now louds
at the #49 fill arm before reaching the pinned ARG-site reject; the
fill switches to an ident source (newly working via the fill arm) so
the original arg-site pin still fires.
test/wcc/812_agg_assign_width.c: 17 runtime-readback rows (the only
oracle for a gate-blind class) + per-row asm byte-id; every row fails
at
|
|||
| 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. |
|||
| 413aafa599 |
w6c+w6c_ww: tagged-union struct-lit payload fills via the canonical fill (#23)
The widen choke-point's struct-payload arm carried its own inline N_STRUCTLIT field loop -- a parallel fill that drifted from cg_structlit_fill/cgstructlitfill: no tagged-field widen arm, so a (void|T)-typed field's raw scalar landed in the field's TAG word (silent truncation past the first tagged field, both stages, byte-id, gate-blind; prober-9 PG5). Delete both loops and delegate to the canonical fill at the payload base: one fill path, one widen path, mutually recursive. Inherits the nested-struct/call/arrlit field arms and closes a latent fsz==2 cs!=ww (old ww loop's fieldstoreop MOVW vs cstage MOVQ). Test 938: 15-row table-driven runtime readback (incl. ellipsis autofill, offset-0 tagged field, (void|str) payload, 3-level widen-fill recursion torture), all 13 bug rows silent-fail at master 6699158; 2 rows skip the byte-id check loudly (pre-existing match-on-tagged-FIELD readback cs!=ww, master-confirmed, separate family). |
|||
| 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. |
|||
| 06b0fea98b |
w6c+w6c_ww: struct-lit store into indexed/deref/field place fills via resolver (#20)
A struct-LITERAL rhs aimed at an N_INDEX element (a[i] = pt{...},
(*ts)[i].caps[k] = capture{...}), an N_UN deref place (*p = pt{...}),
or an indexed-base FIELD place (a[i].f = pt{...}, reviewer-20 sibling)
fell to a scalar store tail in BOTH stages: cgexpr on a struct
literal emits nothing (AX=0) and one MOVQ zeroed the place's first
word — every field silently dropped, a str-leading element's
content.ptr nulled (downstream SEGFAULT). Byte-identically wrong, so
every byte-id gate was blind; runtime pins added.
Fix: divert struct-lit-rhs INDEX/UN-STAR/DOT-over-INDEX places past
the legacy arms and widen the F6 assign-resolver gate
(N_DOT -> N_DOT|N_INDEX|N_UN); the existing C1.25 aggregate arm
materialises the literal into a fresh per-use @placescr slot and
word-copies to the cgplaceaddr-resolved address. No new path;
@placescr alloc site stays single per stage. Rider (task #32): an
array-LITERAL rhs at assignment — unwired for EVERY place kind, same
silent zero-word tail — now dies loud at one choke-point until the
fill lands; build-fail rows pin it.
Gates regex fold-5a (run_thread groupstart capture store,
regex.ha:643-651). Residual adjacent gaps (deref ident-rhs truncation,
>24B ident reassign cs!=ww, struct compound acceptance, value-global
DATAW, tuple-lit deref truncation, CALL-rhs RAX-only store) probed
pre-existing and filed as tasks #31 A-G / #32.
|
|||
| 0ce98cc5dc |
wcc+w6c_ww: append() spread place-chain sources via cgplaceaddr (#35)
#35 (fold-5 blocker, PG6): the append() spread arm dispatched on SOURCE node kind — ident-local only; both fold-5 dup shapes loud-rejected on BOTH stages: deref-spine `append(dup, (*tsp)[0].caps...)` (add_thread regex.ha:569/572) and indexed `append(res, ts[0].caps...)` (search ha:820). Route every non-ident-local source through cgplaceaddr (the established place choke-point: C1 stores, C1.25 aggregate fields, FA1 append targets, #49 element sources): the source header ADDRESS resolves ONCE, pre-grow, into a fresh per-site @appsprscr spill, and every loop iteration re-reads .ptr/.len THROUGH the spilled header after the grow. This adapts the #49 split ruling to the spread's full-range copy: the chain's rvalues (deref-root pointer expr, index exprs) evaluate exactly once PRE-grow — an index reading the dst's len sees the pre-append value — while the source data base and len re-derive live each iteration, so a source header aliasing the dst header re-roots post-realloc. A header reached through a buffer the grow reallocs reads the STALE copy — bit-identical under the non-reclaiming rt/malloc, the same documented #49 stale-base hole (pinned by the spread_selfalias_chain row). Ident-local sources keep the legacy BP-disp emission byte-identical. New loudness with the same fix: the spread source's stamped type must chase to TY_SLICE/TY_STR — a [N]T array ident source previously read its first 16 DATA bytes as a {ptr,len} header, silently. Array wiring plus the remaining rvalue sources (CALL, slicing exprs) stay loud, filed task #27. Global-ident sources now resolve on cstage but are blocked by a pre-existing wwstage checker reject ("let: not assignable", task #29) — no dual-stage row until that closes. 806_append_place: reject_spread_src GRADUATES to a runtime row (spread_place_deref, the old reject source verbatim + readback); new rows spread_dup_copy (PG6 verbatim: both fold-5 shapes, 56B capture elements, copy-semantics mutation pin, want 139), spread_place_kinds (str 24B headers / narrow i32 / empty deref source), spread_growth_place (40-elem spread crossing cap doublings), spread_selfalias_chain (source header inside the dst's grown buffer), and the two new loud-tail rejects (CALL rvalue, [N]T array) pinning the "#35:" diagnostic on both stages. 87 fixtures green (was 70), per-row cs/ww asm byte-cmp included. Unblocks regex fold-5a: add_thread regex.ha:569/572 + search ha:820 spreads go from loud-bound to real spread. |
|||
| 1bcf2726cf |
wcc+w6c+w6c_ww: delete() range form delete(xs[lo:hi]) (fold-5a P2)
Hare's delete also takes a slicing place (harec check.c:1981-2027 EXPR_SLICE; Hare spells it delete(xs[i..j])): remove [lo, hi) — shift [hi..len) down count = hi-lo strides, len -= count, cap unchanged; lo defaults 0, hi defaults len, so delete(xs[:]) clears the slice with storage retained. Checker accepts N_SLICE next to N_INDEX (object must chase to a slice, harec :2024); the old range-unimplemented reject and its #35 cite drop. Lowering (both stages, converged byte-identical by construction) is the single-element arm's same-slice whole-stride word-copy loop with a DYNAMIC src offset (count*esz via a src register) instead of the constant one-stride. Base shapes: local slice ident, deref-of-local, plus NEW indexed local-slice base xs[g][lo:hi] — the fold-5a consumer shape (regex.ha:333 delete(jump_idxs[group_level][..]); outer stride off the type table). Bounds stay implicit, inheriting the documented single-element posture (no index checks anywhere in cgen). Operands evaluate left-to-right, exactly once, before the shift (harec order); only the header ADDRESS is taken before operand eval, so a bound expression's writes through the slice land before the copy. test/809: 64 fixtures — full/explicit/re-clear/head/mid/tail/empty a:a/end-boundary len:len/explicit 0:0 on a never-appended (nil-ptr) slice, single-vs-range equivalence, cap preservation, esz 1/2/4/8/16/24 copy tails against the dynamic src, operand order-of-eval (lo/hi CALLs fire once each, in order) + aliasing-visibility pins, the EXACT [][]size regex consumer shape, deref base, 2 reject rows w/ diagnostic text; every accept row cs==ww asm byte-id. test/804: reject_range row retired (form now accepted), reject_nonindex text follows the widened message. |
|||
| 74767c70cc |
wcc/check+wcc_ww/check: reject overlong array literal — frame-smash class (#71)
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. |
|||
| 33ec0fb1ac |
w6c+w6c_ww: (*p)[i] deref base materializes the array ADDRESS (#61 C)
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. |
|||
| eea3e197c2 |
w6c+w6c_ww: *[N]T indexing strides by element, not whole array (#61 A+B)
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. |
|||
| 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
|
|||
| bb8a44a564 |
w6c+w6c_ww: cast-wrapped tuple literal widens its whole payload into a tagged slot (#66)
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
|
|||
| 8578ad0533 |
wcc+w6c+w6c_ww: global tuple lets — DATA emit + element reads + len(g.N) (C-t3, #48)
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). |
|||
| 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). |
|||
| fdfc2ce318 |
wcc+w6c+w6c_ww: tuple slot layout SSoT — checker size = cgen slot stride (C-t0)
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. |
|||
| 9861f73bbb |
wcc+w6c+w6c_ww: insert() builtin — single-element slice insertion (part of #35)
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. |
|||
| c34a48a81f |
wcc+w6c_ww: append() struct-element sources via split place-resolve (#49)
#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
|
|||
| 796d41bb9f |
wcc+w6c_ww: len() over place-resolved operands (F2/FA2)
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 |
|||
| 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
|
|||
| 403625e433 |
wcc+w6c_ww: >48B tagged args from place-resolved sources (#38b cont.)
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. |
|||
| 76994a8279 |
wcc+w6c_ww: typed dot-read resolver — kill both silent N_DOT fallbacks (F4)
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. |
|||
| b630a7cf20 |
wcc+w6c_ww: append through pointer-to-slice place via cgplaceaddr (FA1)
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. |
|||
| 48df04a8ca |
wcc+w6c_ww: loud-gate try-propagation over multi-success unions (F8/F9 interim)
? 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. |
|||
| cfc2985c61 |
wcc+w6c_ww: aggregate-field stores via cgplaceaddr (C1.25)
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). |
|||
| 32063d0da0 |
wcc+w6c_ww: >48B tagged by-value args — MEMORY-class two-phase push (#38b)
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. |
|||
| e3e6b5a820 |
wcc+w6c_ww: cgplaceaddr resolver — deref-base assign stores (F6)
(*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. |
|||
| 37febab9d5 |
wcc+w6c+w6c_ww: delete() builtin — single-element slice removal (part of #35)
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. |
|||
| 9732061a7e |
w6c+w6c_ww: free() compiles to a no-op (ww has no free) (fix #27)
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.
|
|||
| c2308a11c7 |
w6c+w6c_ww: size-keyed @tagscr — one tagged scratch per slot size (fix #44)
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. |
|||
| 147a8a26b9 | w6c: unstale the cg_sret_retsize header doc for the #38 tagged arm (#38 review) | |||
| 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. |
|||
| 5f15eb3d09 |
w6c+w6c_ww: tagged widen-store struct-literal slice field keeps the 3-word header
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).
|
|||
| 36be9f469d |
w6c+w6c_ww: loud-stop non-ident/non-local append spread source (#34 review)
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. |
|||
| faade48513 |
w6c+w6c_ww: append() stores the full element width per element kind (fix #34)
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. |
|||
| 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. |
|||
| c490ed3ec1 |
w6c+w6c_ww: store full slice header for struct-literal slice fields (fix #24)
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.
|
|||
| 8dda8ea76c |
w6c+w6c_ww: global-base arm for indexed struct-element field read (fix #21)
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.
|
|||
| d40224755a |
w6c+w6c_ww: emit module-level slice-literal static-init (header+backing+reloc) (fix #10 part a)
`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.
|
|||
| 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. |
|||
| 7ca32432b1 |
w6c+wcc/check: infer [_]T array length from initializer element count (fix #7)
`[_]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. |
|||
| 84ed2ab15a |
w6c+cgen: read .cap of an indexed str/slice array element (fix #13, #20 read-sibling)
`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. |