f1dcd4ecae4ce0d86d92c5dc8df2d24a3ad2abf4
137 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| f1dcd4ecae |
wcc/check: #141 def-dim array as struct field — fold def in dim, shared arrayelen across 3 ww readers (both stages)
A def-dimensioned array [MAX]u8 used as a struct field was BOTH-WRONG: cstage
loud-rejected ("array length must be an integer literal"); wwstage silently
sized the dim to 0, so the next field overlapped it (frame-smash). The
reference is neither stage — it is Hare: accept + fold the def.
cstage: fold the def into the dim via eval_def_const. The fold needs def NAMES
visible when resolve_typedecl walks struct bodies, so a stub loop binds
def-name stubs (type=NULL, filled in place by the existing def loop) before
resolve_typedecl — this extends check_file's existing names-first USE+TYPEDECL
pass to DEFs; def-TYPE resolution stays in its original order, and the
kind-filtered type lookup (#225) keeps the SK_DEF stub out of type position.
wwstage: one shared arrayelen(c, rhs) (INTLIT -> uval; else evaldefconst;
else 0) routed through astsize / tinfofornode / checkarrlitfits.
Closes #13's def-dim cstage-reject half (the slice-repeat clause stays open).
Pin test/wcc/951 (5 rows incl a cross-module os.PATH_MAX dim + a ~4KB shape;
teeth = cstage loud-reject + ww frame-smash). cgen-first blocker for the
path::buffer arc (type buffer = struct{[MAX]u8, ...}).
|
|||
| d0a1e2a221 |
wcc/check: #133 const-expr scalar module-global — fold+stamp let-init like def, emit DATA (both stages)
A module-global let with a const-expr init (let s = 7*6) emitted NO DATA word: cstage LINK-FAILed (undefined main.s, loud), wwstage was SILENT (no DATA, MOVSXD on stale AX, exit 152). The DEF pass-2 arm already const-folds + stamps its rhs to N_INTLIT (the #88 eval_def_const/stamp_intlit machinery); the LET pass-2 arm omitted it. Mirror it: after the assignability check, fold the rhs and stamp N_INTLIT when the plain-literal fold missed AND the const-fold succeeded. The existing DATA-emit downstream then fires (DATAW 42 + load). Both stages, byte-identical. Closes the inferred const-expr global and the typed b-ii case (let s:i64=7*6, link-fail both stages) with one stamp. Gated on genuine int-const success (the eval return value, not the out-param): str/struct/slice/call/runtime-operand rhs short-circuit before the stamp and are left untouched — never zeroed. Non-const rhs stays on its current loud route; div-by-zero stays loud. Latent in selfhost (no const-expr module globals → 990-997 byte-id unchanged). Pin: 947 rows C1 inferred 7*6, C2 typed b-ii, C3 def-ref K*7, C4 unary-over-binop, C5 div-by-zero loud-guard; cs==ww byte-id. |
|||
| 66d69537a5 |
wcc/cgen: #124 cross-module &fn in a const — N_DOT reloc + checker accept (both-stage)
A cross-module `&module.fn` in a const emitted no static reloc (the const was never defined -> w6l undefined-reference, both stages) and wwstage's checker rejected the const fn-table. #117/#119 wired the &fn->DATAR const-data reloc for SAME-module &fn only; charclass_map (fold-6) needs cross-module (12x &ascii.isXXX). cgen: add the N_DOT arm to the &fn->symbol helper (node_fnptr_sym / nodefnptr + the two ww emit sites), emitting mafn(leaf, module-ident) -- exactly the symbol a runtime &mod.fn or a direct cross-module call already emits. The helper is the SSoT for both the scalar (#119) and tuple-row (#117) const-data paths, so one arm closes both. checker: type a cross-module `&mod.fn` as `*fn(...)` in the TK_AMP arm (the N_DOT twin of #206's N_IDENT fn-ptr synthesis, gated on a resolved SK_FN/N_FNDECL leaf), so isassignable affirmatively accepts the const table -- aligning wwstage UP to cstage's actual acceptance reason rather than by abdication. The SK_FN gate keeps a non-fn `&mod.var` from synthesizing a fn type (the one pre-existing nonfn-scalar cs!=ww slip is N_IDENT-base, untouched and reproduces same-module). One consumer-coupled commit (the checker accept gates wwstage cgen, so neither half is independently testable). Narrow: slice-row + scalar only; fixed-array (#118) and struct-field (#129) stay separate. Both stages emit the correct cross-module symbols at the right tuple-slot offsets -> byte-identical (990-997 green). Pin 949_xmod_fnptr_const_run (distinct fns so a wrong reloc is caught + the SK_FN-gate axis). This was the last fold-6 cgen blocker; charclass_map is now unblocked. |
|||
| da30f10f70 |
wcc_ww/check: #47 gap-B tuple-with-tagged case-arm variant-match (align-up)
wwstage's checker rejected a `case let t: ((void|size),(void|size),
size) =>` arm against a (tuple|error) scrutinee ("case: not a variant
of scrutinee"), while cstage accepts and runs it. typeeqast's N_TTUPLE
arm recurses per-element, but a tagged element (void|size) is
N_TTAGGED -> fell to the conservative catch-all `return false`, so the
whole tuple-compare failed. typeeqast is the sole acceptance route
(casevariantpairmatch is N_TNAME-only).
Add an N_TTAGGED arm to typeeqast, sibling of N_TTUPLE, mirroring
cstage type.c:288-300 (type_eq TY_TAGGED): position-by-position
variant compare over the tagged node's .list (direct nodes, not
.lhs-wrapped). cstage's nullable-flag check is deliberately not ported
(resolved-Type property, no ww AST analogue; moot for case-match).
A spread variant (TK_ELLIPSIS) in the .list is loud-rejected rather
than compared: a naive streq would silently accept a `...ab` case that
cstage rejects (a new cs!=ww over-accept the bare arm introduced).
Flattening the spread is deferred (#115); until then it louds, matching
cstage.
ww-only (cstage already accepts); the gap-A cgen store landed in
|
|||
| c9cfa52624 |
wcc/check: #103/#108 inferred untyped-int defaults to int (8B), both stages
cstage type_default(TY_UNTYPED_INT) returned ty_i32 (4B): an unannotated `let x = <v>` / `let a = [<v>,..]` silently TRUNCATED any value > 2^31 (5000000000 -> 705032704) and strode inferred arrays at 4. wwstage kept the element raw untyped_int (size 0), which sized INCONSISTENTLY across cgen — the array STORE strode the 8 sentinel but letslotsize under- allocated the frame (SEGV) and cgindex strode the READ at 1. The two stages were each wrong differently; #263 polarity: cstage was the truncating side. int = machine word = 8B (Go-style, MEMORY project_int_machine_word_derived_limits); Hare lowers a flexible iconst to `int`, never a fixed i32 (ref/harec/src/types.c:835). Fix, one root, both stages (FUSE — the cs default + the ww concrete element must land together, else the inferred array is transient cs!=ww): - cmd/wcc/type.c type_default(TY_UNTYPED_INT) ty_i32 -> ty_int. The root; stops scalar AND array truncation at source. - cmd/wcc/check.c N_ARRLIT empty-elt fallback ty_i32 -> ty_int. Symmetric pair; count-0 array emits no stores, so byte-id-neutral. - selfhost/cmd/wcc/check.ww exprtype N_ARRLIT: default the inferred element's untyped flavor to concrete (untyped_int->int, _float->f64, _str->str, _rune->rune, _bool->bool, mirror cstage type_default), empty-elt "i32"->"int", and stamp the synthesized N_TARRAY's .type_ so slotsize / elemsizeofc / letslotsize read its real [N]int size via the type table (rule-13) — no letslotsize special-case (SSoT). combined.ww regen (check.ww embed): w6c + wwdump. ken v2 corpus re-census (160 files): EXACTLY 5 rows move, ALL CONVERGE (byte-id YES + run exit 0, none both-wrong, zero regression): m2_while #108 scalar via alias-bool loop m8_range1 #104 for-range elem over alias [4]int m8_range2 #104 over 2-level alias m8_slice1 #103 inferred array + alias-slice init m8_slice2 #103 + 2-level-alias slice + re-slice Bootstrap byte-id neutral (5 combined units w6c==w6c_ww; 0 bare inferred arrays in selfhost). Annotated controls untouched ([4]i32 stride-4, [4]int stride-8, byte-id). Pinned in test/wcc/813_arrlit_infer_elem_run (the 2 direct repros incl the >2^31 truncation teeth + all 5 movers + controls; test-unit 296). Closes #103 (inferred-array SEGV + truncation), #108 (cstage scalar untyped-int truncation), #104 (for-range elem alias i32-stamp), and the m8_slice []int-init acceptance divergence. |
|||
| fc50a27f3e |
cgen: #95 c3 reviewer-fold — is/as gate exact-only, no widening leak
c1/c2 widened flatvariantidxt (selfhost) with the chain + structural tag-synthesis arms and a >=2 ambiguity os.exit, scoped to the cgen WIDEN consumer. But flatvariantidxt is a choke-point: the wwstage is/as ACCEPTANCE gate (check.ww:4677, the #198 spread fallback) reuses it, so the widening leaked into checker acceptance — vs base |
|||
| 329481c920 |
wcc_ww/check: W3 #105 nested-arrlit gate chases the alias elem type
checkarrlitfits' nested recursion keyed on the raw elemtn kind; a named-alias element type ([2]row, row=[2]int) arrives as N_TNAME, so the inner overlong literal skipped the count+range checks and the module static-DATA route emitted silently TRUNCATED data (ken's m7c_global: DATAW 1,2,4,5 — exit-masked once the #60 read fix removed the segv; cstage loud-rejects every spelling via its typed-literal assignability net). #105: the W1 fill gate never runs on this route, severity raised post-#60. Fix: chase elemtn through resolvealias (transitive) at the recursion gate — alias spellings of any depth take the same checks as the direct shape at all four contexts funneling through the choke point (module let / local let / def / struct-field). A direct N_TARRAY passes through resolvealias unchanged, so accepted shapes are byte-identical base→tip (m7c_global_ok + exact-fit alias field/def/2lvl probed ASM-ID vs a base scratch build). The m7/m7b local overlong rows stay loud, now via the earlier count-naming checker diagnostic instead of the cgen #270-1c fatal. The out-of-range narrow inner element louds "array element out of range" exactly as the direct spelling already did on wwstage. 808_arrlit_overlong: 37 -> 50 checks (+1 accept control alias_exact_module = ken's m7c_global_ok with a byte-id cell, +4 loud flips alias_nested_{module,2lvl,def,field} pinning per-stage texts, +1 REVIEW AMENDMENT alias_nested_local pinning the m7/m7b text move — pre-fix ww was loud via the late cgen #270-1c fatal; the row reds if the diag regresses off the checker count text). 989 ratchet zero flips — no lib module-level literal trips the gate. Filed sibling, not folded: OUTER alias-of-array overlong (let g: arr = [5 elems], arr=[4]int) still ww-silent-truncates at the alias-blind call-site N_TARRAY gates; cs louds with the count text. |
|||
| bcd948de88 |
wcc_ww/check: c4 #80 bare-binder forrange dealiases the iterable's type expr
F2a batch-4 c4. RE-PROBED AFTER c1 per spec: still live at the c3 train base with a REBUILT w6c_ww (the mechanical tichase collapse didn't cover it — this read is AST-node-keyed, not tinfo-keyed). REPRO (.ai/scratch/repro_f2a1_b4.ww): `untyped_lit * rangevar` over a range-for of an alias-typed slice (`type slk = []int`) — wwstage checker dies "asserttyped: bin" at the binop; cs accepts and runs 0. TRACE: check.ww resolvewalk N_FORRANGE bare-binder arm — the binder's element type comes from kind-testing the scrutinee's type expr (N_TSLICE/N_TARRAY), but an alias-typed iterable arrives as N_TNAME: both tests miss, the binder falls to the N_FORRANGE fallback decl, stays untyped, and the first binop over it bails. cs twin types the binding at scope_define (check.c N_FORRANGE) — accepts. FIX (single site, the one the repro traces to, per grant): dealias via the existing resolvealias(unwrapbang(it)) idiom before the kind tests. The TUPLE-DESTRUCTURE arm carries the same unresolved tests but is NOT in-grant (fixing the bare arm is not a no-op, so the re-spelling clause does not apply) — FILED as task #97; currently double-masked bounded-loud (cs louds upstream at #270-1c so the alias-tuple-slice iterable is unconstructible on cs; ww asserttyped). Rows (944): rangevar_alias2 (the repro, cs0/ww-reject -> 0/0 byte-id) + rangevar_plain_ctl (non-alias control, held throughout). 944 202/202. Corpus: five-mains ww NEUTRAL vs base on identical inputs (checker-acceptance-only change; no alias range-for in corpus). combined.ww regens ride along. |
|||
| 10d886906f |
wcc_ww/check: c3 spread-member + &len/cap base walks chase the full alias chain
F2a batch-4 c3, the check.ww behavior pair (exact set). Both cs twins
ALREADY full-chase at this SHA — ww-only align-up, no fuse:
B1 (check.ww:2038 census line; peel was at :2041 post-c1): the
`...inner` spread member peeled ONE level, so a 2-level-alias inner
union escaped the TY_TAGGED splice test and rode as a SURFACE member —
the outer box sized off the inner union's own header (probe sp_alias2:
runtime 0/0 BOTH but byte-DIVERGE, ww frame $48 vs $32). cs twin
chases (check.c:755 type_chase_named, the spec's :660-667 cite moved).
Fix: vu = tichase(vu) under the isspread gate, mirroring cs's
`spread ? type_chase_named(vt) : vt`. sp_alias2 graduates 0/0 byte-id;
sp_alias1 1-level control held throughout.
B2 (check.ww:2273/:2275): the &x.len/&x.cap base walk hand-peeled
NAMED->PTR->NAMED one level per hop — 2-level alias bases fell out of
the slice/str detect and took the generic *opt typing. cs twin chases
both hops (check.c:1198-1201). Fix: tichase per hop, exact cs mirror.
B2 ROUTE-TRACE (flagged, not edited): the spec's "post 0/0" cell for
amplen2 is NOT achievable inside this grant — OBSERVED pre-fix that
ww dies LATER at the cgen ADDRESS tail ("unsupported address-of
shape") for ALL alias bases 1+ LEVEL (cs runs 0): task #96 (ken
b4-oracle), a cgenexpr.ww site outside c3's check.ww set. The chase
here aligns the stamped type only (latent until #96); rows pinned
LOUD-HOLD via the new K_RUN_CS_WWERR kind (cs builds+runs; the ww
build must keep FAILING loud with the pinned diagnostic — a silent
ww accept-and-run is the regression the bound row exists to catch),
citing #96; they graduate K_RUN with it. Also OBSERVED: the let-init
spelling (`let s: sl1 = [1,2,3]`) is blocked further upstream by the
rows use the param route.
Pin rows (944): sp_alias1 K_RUN hold, sp_alias2 K_RUN graduation,
amplen_plain K_RUN control hold, amplen1/amplen2/ampcap2
K_RUN_CS_WWERR LOUD-HOLD bound on #96 (ww leg asserts "unsupported
address-of shape"). 944: 196/196.
Corpus: five-mains NEUTRAL vs base on identical inputs, cs==ww holds
(both clusters zero-in-corpus). combined.ww regens ride along.
|
|||
| b26b3dbdc5 |
wcc_ww: c1 collapse plain TY_NAMED chase loops into tichase across cgenstmt/cgen/check — byte-id neutral
F2a batch-4 c1, mechanical, per rob's spec census (re-verified at
4adf914; check.ww/cgenstmt.ww cites were stable as predicted, the
two cgenutil [B3-SHIFT] cites moved +8: :2746->:2754, :2895->:2903).
42 conversions, every site eyes-classified plain nil+NAMED:
- cgenstmt.ww 26: one-line :149 :325 :352 :532 :551 :594 :715 :732
:938 :940 :1577 :1629 :1782 :2230 :2575 :3372 :3678 + multi-line
:986 :1912 :2287 :2291 :2702 :2759 :2823 :3037 :3715 (all plain,
no extra guards).
- cgen.ww 13: one-line :1505 :1541 :1658 :1691 :1697 :1947 :1951
:2065 :2109 :2118 + multi-line :2239 :2648 :2663 (incl. the
emitdefconstants struct/array arms — plain).
- check.ww 3: :1627 tupleelemslot, :1673 fieldslotsize, :4111 arrlit
elem chase; WHY comments kept. tichase visibility verified: same
`package wcc` (cgenstmt/cgen already call it) — no second helper.
Survivors (classified, untouched):
- cgenutil.ww :1301 tichase's own body (the accessor) + :2754
nullableptrtag single peel (batch-2 c3-B2 PROBE-CLEARED,
|
|||
| 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). |
|||
| f88dbb01e2 |
wcc_ww/check: inferred struct-lit let plants the synthesized TNAME — field(SB) name-leak + tagged-field assign bound (#24)
For an annotation-less `let p = pt{...}` checkletassign planted exprtype's
N_STRUCTLIT result — the struct decl's BODY node (N_TSTRUCT, per #66) — as
the let's type. Every cgen local-arm dispatch (cgdot read, cgassign
tagged-field store, the alias peel) is N_TNAME-keyed, so the body matched
no arm: field reads fell to the module-qualified fallback and emitted the
FIELD NAME as a global symbol (MOVQ f(SB) — link-fail, #211 name-leak
family; silent corruption if a same-named global exists), and a tagged-
field assign fell to the assign-resolver TY_TAGGED loud bound. Both PG5
wwstage symptoms, one root; plain structs leaked too. Normalizing the
inferred binding to the synthesized TNAME (mktname + tinfofornode stamp)
routes every consumer down the already-byte-id annotated path. cstage
needs no twin: check.c:1477 clet carries Sym.type (tinfo) and its
emission is annotation-invariant (probed). Test 811: 10 rows x 2 drivers
+ 10 asm-byte-id; pre-fix wwstage link-fails every unannotated row
(incl. the `...` autofill and parenthesized forms; nested s.f.g ran
but cs!=ww asm).
|
|||
| 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. |
|||
| ed62e8199f |
wcc_ww/check: tuple size() fold reads the type table — packed-sum C-t0 escape (#22 commit 0)
The wwstage size()/align() fold walks the AST (astsize), and its
N_TTUPLE arm still summed PACKED element sizes — C-t0 flipped the
checker type table (tupleelemslot) and cstage's N_TTUPLE to the
ratified slot layout but missed this second wwstage sizer.
size((u32,u32)) folded to 16 on cstage and 8 on wwstage: a silent
cs≠ww in every folded tuple-size constant, plus the recursive
escapes (a tuple inside struct/array size computation under the
fold). Runtime-confirmed at
|
|||
| 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. |
|||
| e091dfbdbe |
wcc-ww: assert/abort builtins — checker tag + cgen rt_abort lowering (#58)
wwstage had no EXPR_ASSERT-family intercept: the checker left bare assert/abort calls untyped (asserttyped gate 4 skipped them by design) and cgcall fell through to the regular call path, emitting CALL assert(SB) for a symbol that exists nowhere — link-fail. cstage was already correct (tag ty_err at check.c:1536-1572, lower inline via rt_abort at cgen.c:6618-6663). Mirror the same tag-then-lower pair: exprtype N_CALL stamps the call void and the callee TY_ERR behind the scopelookupprefer no-shadow gate (the isassertfam predicate), with the cstage arg diagnostics (cond must be bool, msg must be str, arity caps); cgcall keys on the TY_ERR tag and emits the identical CMPQ/JNE/rt_abort sequence. A user-shadowed assert/abort (same-module or cross-module, the #45 shape, task #14) stays untagged on the regular call path — byte-id for all existing lib code preserved. The cond check does NOT alias-peel: cstage compares ty_bool by identity (check.c:1560), so `type myb = bool` is rejected there; wwstage aligns down per rule 10 (a resolvealias here was accepting it — cs/ww accept-reject divergence). Widening both stages together belongs to the alias-peel choke-point arc (task #5, #47/#68). The resolvewalk N_IDENT resolution counter learns the builtin shape: an unshadowed abort/assert ident binds no sym BY DESIGN, so wwdump -r's zero-unresolved gate (990 probe 4) counts it resolved instead of failing builtin-using units. test 957: 13 rows — pass/fail/msg/bare abort (run exit + rt_abort stderr content; no-msg rows pin EMPTY stderr = the (NULL,0) shape), assert in an imported module, same-module + cross-module shadow controls, 5 checker rejects pinned on diagnostic CONTENT (shared substring; cstage prefixes pos, wwstage cerr is bare) incl. the alias-of-bool cond row pinning the rule-10 down-alignment; each positive row pins cstage run exit + cs==ww byte-id. On pre-fix master 11/13 rows trip (survivors = the two shadow controls). Residual (separate root, deferred diagnostic class): zero-arg assert() is not intercepted by either stage; cstage rejects via the generic undefined-ident path, wwstage's undefined-callee diagnostic is the class deferred behind wiring checkfile into w6c_ww. |
|||
| 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. |
|||
| 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. |
|||
| 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
|
|||
| 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. |
|||
| 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. |
|||
| 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. |
|||
| 1e081e28c6 |
wcc_ww/check: infer named-struct element for arrlit->slice assignability (fix #19)
wwstage over-rejected an inline `let g: []pt = [pt{..}, pt{..}]`. The
N_ARRLIT exprtype arm inferred its element type from the first element,
an N_STRUCTLIT, whose exprtype arm deliberately returns the struct BODY
(N_TSTRUCT) per #66. The array->slice isassignable arm then typeeqast's
the declared element (N_TNAME "pt") against that body and bails on the
TNAME-vs-TSTRUCT kind mismatch -> confident-false -> reject. The reject
is a KIND mismatch, not a nominal-compare weakness: typeeqast is already
streq-keyed for N_TNAME.
Narrow fix: when the first arrlit element is a named struct literal,
capture the NAMED type (mktname) so su.lhs matches the declared N_TNAME
shape, mirroring cstage's element inference. typeeqast and the
N_STRUCTLIT #66 body-return are untouched; non-named elements keep the
existing first-element shape. e.type_ via tinfofornode still resolves
[N]pt for cgen, so cstage/wwstage stay byte-identical.
Test 687 gains struct_pt (sum+len+cap == 14), struct_3f (mixed-width
u8/i64/i32 field offsets == 23), and struct_arrvar_local (the
array-VARIABLE form still accepts+runs; local scope since the
module-scope variable form is the deferred #22 link gap).
|
|||
| ad00114c7f |
wcc_ww/check: skip #258 array->slice desugar at module scope (fix #18)
The #258 borrow desugar lowers `let s: []T = arr` to a runtime `arr[0:len]` (N_SLICE over the array base) so the slice header is built at run time. That is only meaningful for a LOCAL let — a fn body executes the borrow. A module-level let is static data with no runtime to run the borrow; its rhs must stay the raw N_ARRLIT so cgen can materialize it as DATA. cstage splits this by checker: clet (the desugar site, check.c:1993) runs only from cstmt (local statements); module-level lets are checked in check_file pass-2 (check.c:2549) which never desugars. wwstage runs ONE checkletassign for both — function-body lets via resolvewalk's post-order walk (a block scope is pushed, c.cur != c.top) and top-level lets via checkfile pass-2 (no scope pushed, c.cur == c.top). Mirror cstage's split by gating the desugar call on `c.cur != c.top` (the same module-scope test as check.ww:184). The #130 module-level assignability check in checkletassign is untouched. Without this, a module-level `let g: []u8 = [1u8,2u8,3u8]` reached cgen as N_SLICE in wwstage but N_ARRLIT in cstage — the cs!=ww shape that blocked #10 part-a's wwstage data-emission. Locals stay byte-identical (named-array->slice still works, exit 8 both stages); selfhost combined .ww regenerated. |
|||
| 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. |
|||
| ff333ce62d |
wcc/check: route diagnostics through cerr(str) helper (Wave-2 str-lift)
57 piecewise stderr diagnostics in the checker spelled out os.write(2, m.ptr, NNu64) with hand-counted byte literals. Replace the 38 hand-counted literal sites and 19 var sites with a tool-local cerr(m: str) helper that takes .len off the str, eliminating the off-by-one hazard. All 38 prior hand-counts were already correct, so stderr is byte-identical. cerr lives in check.ww (not err.ww, which ports err.c for the dead C-driven path); regen w6c + wwdump combined.ww. |
|||
| 8c43363cfe |
wcc/check: tinfofornode + decl-kind if-ladders -> switch (Wave-2 structural)
tinfofornode: 11-rung type-node-kind if/else (N_TNAME..N_TTAGGED) -> switch (k); no default case preserves the old trailing-else-less non-match path (r stays nil -> post-switch cache-bind). N_TTAGGED's early return r; (the nullable (*T|void) fold) intact. decl-kind in checkfile: 4-rung (N_FNDECL/N_DEF/N_TYPEDECL/N_LET) -> switch (k). w6c + wwdump combined.ww regenerated. |
|||
| 90479fed68 |
w6c+wwstage: reject untyped empty-[] alloc — require context, loud cannot-infer (#3 B', subsumes #5)
An empty `[]` carries no element type; ww gets it only from a let annotation (the #45 retype). Both stages used to silently default the element to u8, and in value-form positions (return / call-arg) the lowering miscompiled — malloc(8) ignoring n, a 16B *u8|nomem where a 24B slice was expected (#5). Now every empty alloc that isn't a let-annotated binding fails to infer with a loud error, aligning ww DOWN to harec (ref/harec/src/check.c:1801-1802). Mechanism: clet / checkletassign flags the single alloc call node that a `let x: []T =` rescues (save/restore around the init walk); the alloc branch errors on any empty alloc that isn't that node. The #45 wide-T retype path is kept. wwstage needs an extra not-yet-stamped guard because resolvewalk re-types value nodes context-free after checkletassign. Tests: negative cstage-driver 729 (table-driven: bare-let, return, call-arg, assignment) + positive @test in attest_pass.ww exercising the u8 and the wide-i32 (#45) paths at runtime. Both stages reject symmetrically; byte-id verified on []u8 and []i32. |
|||
| bec1e7d6b0 |
w6c+wwstage: #264 read cached operand tinfo for match-expr yield type
The wwstage checker rejected a match-bound binder used in a `yield` arm
of a match-AS-EXPRESSION (`let v = match (x) { case let p: *T => yield
*p; ... }`) with asserttyped:un/bin/index; cstage compiled it.
resolvewalk stamps the yield operand's type_ during the in-scope N_MCASE
arm walk. exprtype's N_MATCH arm then derived the match's type by
re-running exprtype on the same operand to recover a type NODE — but the
arm binder's scope is already popped, so the re-derive returned nil and
the N_UN/N_BIN/N_INDEX restamp arms overwrote the good in-scope stamp
with nil. cstage never re-runs: match_yield_type reads the operand's
cached ->type (cmd/wcc/check.c:121).
Root fix (align wwstage UP): matchyieldtype now returns a *tinfo and, at
the post-walk call, READS the operand's cached node.type_ instead of
re-running exprtype — so no operand shape can be clobbered by
construction (deref/bin/index all vanish, no per-arm guards). The
exprtype N_MATCH consumer stamps e.type_ from that tinfo directly (no
tinfofornode round-trip). The pre-walk call (checkletassign L302 /
checkretassign L303 run before the in-scope arm walk, so the operand is
nil there) keeps the nil-safe re-derive — benign and load-bearing: it
types the void-arm literal so let/return-assign has a usable node. The
re-derived node (or btype for the bare-binder idiom) is carried back via
an out-param for the assignability check and for the N_MLET/N_MASSIGN
tuple-destructure consumers (`let (a,b) = match { case let t => yield t
}`, test 945). cstage is single-pass so its else is dead; eliminating
the pre-walk call is #279.
Supersedes the narrow N_UN non-clobber guard (removed — its match
consumer is gone). @test check_match_ptr_deref extended to pin the whole
operand class (deref / bin / slice-index / deref-then-field), dual-stage
(910 + 997) with correct runtime + cs==ww byte-id. The *[N]T ptr-to-
array index variant is blocked separately by #278. Both compiler-
imported combined.ww regenerated. smoke + test-unit (242) + 994 w6c_ww
byte-id (18 corpus incl. selfhost combined.ww) green.
|
|||
| e92708ecda |
w6c+wwstage: implicit [N]T->[]T array-to-slice coercion via desugar (#258)
Hare admits an array with a defined length wherever its element slice is
expected (assign / return / call-arg / init) as a borrow; ww rejected it
everywhere (the #108(c) exclusion), so base64 worked around the gap with
explicit a[0:n] slices.
type_assignable / isassignable now admit array->slice on an exact element
match (mirror ref/harec/src/types.c:1080-1097, the SLICE-dst arm). The four
acceptance sites route through one shared helper (desugar_arrayslice /
desugararrayslice) that rewrites the array expr to the explicit full slice
arr[0:len(arr)] — an N_SLICE over the array base. cgen is untouched: the
existing slice lowering (#252/#257/#135 made array bases, incl struct-field
arrays, correct) materialises the borrow header {.ptr=&arr[0], .len=N,
.cap=N}, byte-identically in both stages.
wwstage runs no general call-arg / N_ASSIGN typecheck, so checkassign +
desugarcallargs are added solely to route those two contexts through the
shared desugar (rule-10). desugarcallargs additionally loud-rejects an
element-MISMATCH array into a []T param, scoped to that shape so wwstage's
broader call-arg leniency is untouched.
953_arraytoslice_run covers the four contexts + a borrow-alias proof + the
i32/u8 element axis (dual-stage run + cs==ww byte-id), plus mismatch-reject
rows asserting both stages refuse [4]i32 -> []u8. Regen'd w6c + wwdump
combined.ww (#110).
|
|||
| d56b7ca946 |
w6c+wwstage: narrow int/rune array-literal elements to the declared type (#251)
`let a:[4]u8=[65,66,67,68]`, `def D:[4]u8=['A',..]`, and `enc{m=[65,..]}`
rejected with "init [4]i32 not assignable to declared [4]u8": an array
literal's element type came from the elements via type_default (int-lit
-> i32, rune-lit -> rune) with no declared-element-type propagation. The
scalar path already narrows (`let c:u8='A'`); only array aggregation at
the let/def/struct-field sites #130 (test 920) left unwired did not.
Fix = the int/rune analogue of coerce_floatlit, realised as the EXISTING
#130 accept-if-fits range-check — NOT a node-type restamp. cgen drives
the array element WIDTH from the declared type at every site (cgen.c
local-let lu->sub, emit_array_data d->type), so a restamp would be dead
code (the array literal keeps its [N]i32/[N]rune node type; the cs==ww
byte-id gate confirms the bytes emit u8-wide regardless). Per element:
foldable int/rune literal -> defcastfits range-check vs declared T
(in-range accept, out-of-range REJECT loud, rule-7); non-foldable ->
type_assignable / isassignable.
cstage (check.c): wire arrlit_init_fits into clet (local let),
struct-field-init, and def-init — the three sites the #130 module-let
path already covered.
wwstage (check.ww): factor checkletassign's inline #130 block into
checkarrlitfits and call it from the let path, the def path, and a
TARGETED array-field walk in the N_STRUCTLIT arm. This also closes a
pre-existing rule-7 wwstage over-accept: the def path ran NO init
assignability check and the N_STRUCTLIT head-stamp parks field
assignability (#23), so out-of-range / str array elements silently
over-accepted (a truncating miscompile) at those two sites. The
struct-field walk is the array-field accept-if-fits ONLY — it reuses the
stable N_TSTRUCT field-list walk (astoffset precedent), isolated from
the broader parked #23 field-assignability walk.
Regenerated w6c + wwdump combined.ww (embed check.ww). New test 951
covers let/def/struct-field x int/rune accept (run + cs==ww byte-id) and
out-of-range/str reject (both stages). test-unit 237 + smoke green.
|
|||
| dbc169a025 |
wwstage: reject error-type vs int comparison (#246)
cstage cbinop routes every comparison through unify_arith (cmd/wcc/check.c:952), which loud-rejects an error-typed operand paired with a differing type (e.g. strconv.invalid != i32). wwstage binoptype returned bool for comparisons without any unify step, so it silently accepted a program cstage rejects -- a rule-10 break (align the leaner-but-leniner wwstage DOWN to cstage). Scope the rejection to an error operand (varianterr) mismatched with the other (typeeqast) so the broad differing-types diagnostic -- whose typeeqast-vs-cstage-type_eq asymmetry risk could reject valid bootstrap code -- stays out of wwstage. Covers the whole comparison family (EQ/NEQ/LT/LE/GT/GE), all of which cstage routes through unify_arith. Found by impl-strconv3 writing the strconv test. Gate-blind: the bootstrap never compares an error type to an int, so byte-id stayed green while the stages disagreed on what's a valid program. test/wcc/949_errtype_compare.c: both drivers reject invalid !=/==/< i32 (K_BUILDERR); same-error-type and plain-int compares still accept on both stages + cs==ww byte-id (K_RUN). 12/12. |
|||
| 5d023c0ef0 |
w6c+wwstage: cgexpr materializes tuple rvalues + unwrap-shift for tuple-payload destructure (#241)
cgexpr could not produce a tuple VALUE, so a destructure / let bind of an
RVALUE tuple read garbage past the first element (cstage) or left an untyped
binder aborting wwstage's asserttyped gate — a DANGEROUS gate-blind cs!=ww,
and the strconv-int blocker (Hare's stoi64/stou64 require
`let (sign, u) = parseint(s, base)?`). Three feeders, all routed at the same
SysV register-return cursor the cgmlet/cgmassign consumers already read:
- an N_TUPLE literal fell to the `cgexpr_int(0)` / `MOVQ $0, AX` default;
- a tuple-typed IDENT loaded only word0 into AX (`yield t`, `return t`,
`let q = t`), leaving DX/CX stale;
- the `?`/`!` unwrap of a tuple-in-union payload lifted only word0->AX,
stranding word1 in CX (the scalar/str success ABI).
Fix (both stages, byte-identical per rule 10):
- cgexpr packs an N_TUPLE literal into the cursor (cg_tuple_lit_to_cursor /
cgtuplelittocursor — a byte-identical reuse of cgreturn's in-register
N_TUPLE arm) and a tuple IDENT from its slot at the register-ABI stride
(cg_tuple_slot_to_cursor / cgtupleslottocursor);
- the ?/! unwrap shifts a tuple success payload down one integer reg past
the tag (cg_tagged_tuple_payload_shift / cgtaggedtuplepayloadshift),
loud-stopping a float/slice/str payload element (the SysV per-eightbyte
tagged-tuple-payload classification is #243);
- wwstage's checker recovers the popped match-arm binder type for a
`yield <binder>` operand (matchyieldtype's scope-free fallback to the
arm's declared type), so the destructured binders stamp — cstage reads
the operand's already-stamped ->type, wwstage caches only a tinfo.
Over-cap rvalue-tuple materialisation (no slot to sret a bare expression
value into) loud-stops both stages — the #10 follow-up.
NOT closed (distinct root, deferred to #238/task #6): single-var
`let q = (true, 9u64)` then `q.N` — the N_LET tuple-init sz==16||32 gate
drops a narrow-first mixed tuple, and the N_DOT tuple-field PACKED-offset
reader disagrees with tuple_store's 8B stride. Not the rvalue-into-cursor
fix and not a strconv blocker (strconv destructures); documented at the test
header.
Test 945_rvalue_tuple_destructure_run: literal destructure, match-yield
destructure, and the ?-call strconv shape, each run + cs==ww byte-id on both
drivers (9 checks). Embedded w6c/wwdump combined.ww regenerated.
|
|||
| 6fc85f9aaf |
w6c+wwstage: construct + bind tuple-in-union payload (#242)
A mixed-scalar tuple WRAPPED IN A TAGGED UNION (the (neg, n) shape Hare's
strconv parseint returns, ((bool,u64)|invalid|overflow)) miscompiled three
ways, all gate-blind (no bootstrap tuple-in-union):
(a) cstage CONSTRUCTION: a tuple variant fell through the N_RETURN scalar
shuffle, which ZEROED tag + payload — the operands were never packed.
Route the tuple variant through the scratch-slot widen path; add a
TY_TUPLE arm to cg_widen_tagged_store that packs each element into the
union payload at the register-ABI 8B stride + sets the variant tag.
(b) wwstage CHECKER: `let (a,b)=t` over a plain tuple ident (the match-
bound union payload) left the un-annotated binders UNTYPED, so the bin
node reading them was untyped -> asserttyped abort. The element-type
distribution only fired for an N_CALL rhs. Consume the rhs tuple type
for ANY rhs (mirror cstage check.c:2017).
(c) BOTH stages DESTRUCTURE: the register-cursor receive assumes the rhs
left every element in AX/DX/CX (a call's tuple-return ABI). For a tuple
IDENT cgexpr loads only word0->AX, so the 2nd binder read a STALE DX.
Copy each element from the ident's slot at the 8B stride.
Construction is correct at ANY variant position (the resolved tag, not a
default 0); wwstage resolves it via the typeeq core (flatvariantidxt), not
taggedvariantindext whose str/slice shape-fallback would mask a mismatch.
Two rule-7 loud-stops cover shapes this slotted packing can't yet handle,
on BOTH stages, so neither silently miscompiles:
- a tuple with a SysV-eightbyte-sharing narrow pair (e.g. (i32,i32,u64)),
caught by the 8+payload > slot-size guard (the eightbyte tuple
classification is #243);
- a tuple built from a BARE LITERAL element (`true`/`false`, suffix-less
`7`). cstage's cg_tag_for_variant can't type the literal (#241), returns
-1, and loud-stops. wwstage types `true` as bool and `7` as untyped_int,
so flatvariantidxt WOULD resolve the variant — a program cstage rejects
but wwstage accepts is the cs!=ww divergence rule 10 forbids. wwstage
mirrors cstage's CONDITION (a bare-literal element), not its -1
mechanism, with an explicit guard that aligns the richer side DOWN. Lift
BOTH guards together when #241 lands cstage literal typing -> symmetric
accept.
Test 940_tuple_in_union: 4 K_RUN rows (variant 0, void arm, tuple at
variant 1 two ways) x cstage-run + wwstage-run + cs==ww byte-id, plus 2
K_BUILDERR rows (eightbyte-share, bare-literal) asserting a loud stop with
the #242 diagnostic on BOTH drivers = 16 ok.
|
|||
| 93d8ece740 |
wwstage: size struct tuple-field slot via fieldsize (#237)
The wwstage checker `fieldslotsize` (check.ww) summed each struct field's
SLOT width to stamp the enclosing struct's tinfo.slotsize, but had no
TY_TUPLE arm — a tuple-typed field fell through to the 8B default. So
`struct { f: ([]u8,[]u8) }` stamped slotsize=8 while size=48 (the natural
element sum, correct). A `let s: S` slot is allocated off ti.slotsize
(cgenutil.ww slotsize), so wwstage reserved an 8-byte frame slot for a
48-byte struct: a SILENT stack-corrupting miscompile.
cstage has no size/slotsize split — it sizes the field at f->type->size=48
throughout — so the stages diverged on the emitted frame ($16 wwstage vs
$64 cstage), invisible to a cstage-only check and caught only by cs==ww
byte-id (rule 10).
Add the TY_TUPLE arm (return the tuple's own slotsize, the per-element slot
sum already stamped at the N_TTUPLE arm with slices at 24 each). This
aligns the checker's field-slotsize with cgenutil.ww fieldsize, which
already returns the tuple's natural size (48). The stale comment claiming
"TY_TUPLE inside a struct currently defaults to 8 in cgenutil" is removed —
fieldsize stopped defaulting to 8 at the 2026-05-23 review.
Test 930 pins cs==ww .s byte-id for a struct with a tuple field (with and
without a leading scalar field, foff 0 and !=0); pure frame-size gate, no
runtime — the divergence is fully visible in the emitted assembly. No
selfhost source has a tuple-typed struct field, so the w6c/wwdump combined
amalgams regen with no asm change (byte-id-neutral bootstrap).
|
|||
| f8aebc045d |
wwstage: prefer curmod for bare-leaf value-ident in exprtype (#55, fixes #226)
exprtype's N_IDENT branch resolved a bare value-ident through the flat-scope scopelookup, which bucket-walks and returns whichever same-leaf symbol heads the bucket (the last-registered one). Under a foreign curmod that binds a same-named symbol from the wrong module and drags in its declaration's type: resolving `read` to io.read while checking os pulled io.read's (size|eof|error) return node, whose bare `error` then bound strconv.error instead of io.error. The mistyped union variant made the tagged-tag remap's flatvariantidxt return -1 (correctly: the union held the wrong type), collapsing the tag to 0 — the #226 fmt cs/ww asm divergence. Resolve through scopelookupprefer(c.cur, c.curmod, e.str), preferring the current module, mirroring cstage cmd/wcc/check.c:66 scope_lookup_prefer. Sibling bare-leaf sites already migrated: #56 (N_CALL callee), #53 (bare TNAME). #226 is thereby an instance of #55, not a nominal-identity gap: io.error is already a sound sym-cached singleton. The remaining bare-leaf sites (N_DOT-callee leaf, varianterr, scruttype) and the cgen-side cgident analogue are tracked separately. fmt's 777/780/781 stay STAGE_CS pending a separate spread-union residual. test/wcc/794: cross-module bare-leaf value-ident, reject->accept polarity (w6c_ww must accept the cstage-emitted combined); no byte-id assertion as the minimal value-ident also trips the open cgen-side cgident bug. |
|||
| 3d44f742a0 |
wwstage: expand spread variants in match check + size them off flattened members (#209)
The wwstage checker walked a match's raw AST variant list and never expanded a ...inner spread variant, so it rejected fmt's match over field = (...formattable | *mods) ('not a variant of scrutinee'). cstage's resolve_type flattens the spread at type-build. Mirror that in the AST exhaustiveness walk (casevariantin + a recursive checkvariantcovered): when a variant resolves to N_TTAGGED via a spread, recurse into its members. Additive + spread-gated -- typeeqast / casevariantpairmatch (#13) / casecovers untouched, so non-spread matches and 990-997 byte-id are unaffected. Also size a spread N_TTAGGED off each flattened member (mirror cstage check.c), dropping the inner union tag word (field 40B to 32B). Closes the #209 CHECKER reject; full fmt-byte-id still awaits cgen cluster #226 (io.read nominal-remap) + #227 (spread-widen ABI), so fmt tests stay cstage-only with retargeted comments. Adds test 792; regenerates w6c/wwdump combined.ww.
|
|||
| 419c896ad0 |
wwstage: match cross-module union variants by (module, leaf) pair (#13)
wwstage's checker rejected matching an imported union's variants cross-module: casevariantin/casecovers' typeeqast did a raw streq, so a union's bare variant "unsupported" failed to match the dotted case pattern "errors.unsupported" (cstage compares resolved-Type identity, qualifier-agnostic). Add a (module, leaf)-pair fallback after typeeqast: reduce both the case pattern and each variant to (module, leaf) and match on pair equality -- a dotted name keeps its own qualifier, a bare name takes the union's defining module (taggeddefmod, via the aliassym hop chain). This closes BOTH directions: the false-reject of valid cross-module match AND a false-accept of a foreign same-leaf qualifier (case othermod.foo vs errors.error now rejected, matching cstage). Handles the nested errors.error-in-io.error case (the dotted variant keeps mod=errors, not the union's mod=io). typeeqast stays the first check so currently-valid code is byte-id-unchanged; the pair-match fires only on the previously-rejected qualified-vs-bare mix. Wired into casevariantin, casecovers, and the is/as caller. Adds test/wcc/787 (cross-module positive, exhaustiveness, foreign-qualifier reject-guard, dotted-variant body). Unblocks #5's cross-module io.error/errors.error decomposition. rule-10 fix-up; #10-family (wwstage cross-module resolution). |
|||
| f6ac7fb2f8 |
wcc: accept bare &fn into a fn-pointer-alias slot via a caller-site gate (#206)
A bare `&fn_name` was not assignable into a `*reader` / `(*reader | void)` vtable field without an explicit cast: cstage type_eq on TY_NAMED is pointer-identity, so a structural `*fn(...)` referent never matched the named `*reader` variant; wwstage accepted it only via an accidental catch-all leniency. harec accepts bare &fn through hint-directed alias adoption at the address-of site (check.c:3594-3626) while keeping pointer assignability strictly nominal (types.c:1039-1066), so a materialized `*fn` value never launders across alias names. Mirror that decision without threading a type hint through the bottom-up cexpr: keep type_assignable / isassignable fully nominal, and add a caller-site helper (assignable_addrfn) at the assignment boundaries (let-init, struct-literal field-init, assign, return, call-arg, array element) that accepts iff the rhs is a DIRECT &-of-fn-ident and the destination (or exactly one tagged variant) is a pointer-to-fn-alias whose underlying fn signature structurally matches. A materialized `*fn` value, a distinct same-signature alias, and an ambiguous multi-variant target all stay rejected. Both stages share the rule; wwstage's lenient pointer-fn punt becomes a confident reject. ww has no methods, so a `value.leaf` slot is only ever a fn-pointer field and this never over-admits. The tightening surfaced a wwstage typeeqast gap: a TY_FN result that is a tuple (`*fn(...)(i32,i32)`) compared false where cstage type_eq handled it, newly rejecting a legitimate structural assign. Add the N_TTUPLE structural case (rule-10), restoring test 766. cgen-neutral (the cast was a no-op reinterpret); pre/post bootstrap .s zero-delta. Test 783 covers the positive paths (incl. a byte-id-clean three-field-vtable dispatcher) and the negatives. Tagged-slot negatives (ambiguous / tagged-laundering) are rejected on cstage but wwstage's separate `(X|void)` void-variant leniency (#214) still admits them; 783 pins them cstage-only, to graduate when #214 closes (required before wwstage becomes the authoritative selfhost checker). Note: `make clean && make test` is RED at HEAD on 4 alloc fixtures (700/748/758/915) via a pre-existing clean-build defect (#215, malloc vs rt_malloc); identical with or without this change, so bisect-clean for #206. |
|||
| 4e1181fd8c |
wcc: resolve value-receiver field-call type via callee, not global leaf (#208)
wwstage exprtype's N_CALL arm fell into a global-leaf scopelookup for an N_DOT callee with a value or chained receiver, binding whatever same-named global headed the scope bucket. Under a late-os combined.ww concat order this resolved io's `s.read(...)` to os.read (i64) instead of the field's fn type, so checkretassign confidently rejected a valid tagged return — an import-order-sensitive false positive. cstage resolves a call result solely from the callee expr's own type (check.c:1378-1433, mirroring harec check_autodereference 1566-1581); drop the global-leaf else-arm so value and chained receivers fall through to the existing fn-VALUE path at check.ww:2455. SK_USE module-qualified calls are unchanged. ww has no methods, so `value.leaf()` is only ever a fn-ptr field access; the global hit was never legitimate. Zero .s delta across all 5 bootstrap tools (the branch is dead in the bootstrap); test 776 graduates to both stages (os-late order, byte-identical). The fix unmasks a pre-existing wwstage cgen bug (#211): cgen also re-derives a call's return shape by name (fnretlookup), so a value-receiver field call whose leaf collides with a same-named global of a different register shape mis-resolves cs!=ww. Documented at the cgen site; pinned cstage-only by test/wcc/782 (graduates to STAGE_WW on #211 close). |
|||
| 487cf91f12 |
wcc: accept NAMED-variant nominal match at tagged→tagged subset (#205)
The tagged→tagged subset arm walked src's leaves against dst's flat variant list, so `let r: (size | eof | wrapper) = e` with e: wrapper REJECTED at cstage's checker — wrapper's leaves (unsupported, underread, nomem) aren't direct variants of dst. Wwstage's permissive tail accepted silently but cgen then miscompiled the tag (#199b layout- extension family, deferred). Mirror the concrete→tagged fix from #199 (α) at type.c:316: when src is a NAMED-tagged wrapper and dst has a direct NAMED-tagged variant equal to src, accept by nominal identity BEFORE the subset loop. Wwstage's isassignable mirrors the structural insertion before the existing `*confident = false; return true;` tail (deferred-tightening per #202). SSoT with `is`/`as` non-recursive variant lookup (#198 family). Cgen's tag-remap for the wrapper-as-whole case still maps src variants to dst tag 0 — the wrapped-slot layout for `dst.tag = variant_idx, dst.payload = src` is #199b future-work. Probe verifies checker-accept + runtime exit-clean only; does NOT inspect the resulting variant tag. Probe 774_tagged_widen_named_variant.c covers 5 rows: bug-repro, nested-wrapper, pure-leaf subset (regression), concrete-unrelated rejection (gate), branched callee. Two sibling cgen/checker bugs surfaced (wwstage cgwidentaggedstorebp ssz<slot_sz pad gap; wwstage isassignable !void-alias collapse) and documented inline at the probe-row comment, kept in #202 family. |
|||
| 3f4eeff7ff |
wcc: route is/as variant lookup through tinfo.params (#198)
checkisas walked the unflattened AST u.list via casevariantin (typeeqast
streq), so any variant introduced via a `...inner` spread was invisible
and rejected as "is/as: not a variant of operand". Repro:
type rsh = (size | io.eof | ...io.error);
let r: rsh = 42: size;
if (r is io.underread) ... -- pre-fix wwstage REJECTS
io.underread is in io.error.params, which tinfofornode splices into the
parent at L1827-1836, but the AST u.list still holds the single
`...io.error` entry that streq("io.underread", "io.error") rejects.
Route through flatvariantidxt — the same Phase-N helper #179 cgmatch
and #66 cgtagvariantidx already key off. Mirrors cstage cmd/wcc/check.c
:1662-1675 u->params + type_eq. Falls back to casevariantin AST walk
when tinfo isn't available (defensive — non-#198 path stays as-is).
project_tinfo_lossy_nominal: name-keying was the pre-Phase-N workaround
for tinfo lossy on nominal identity; typeeq inside flatvariantidxt now
handles NAMED ptr-id (#64), so the checker pair aligns with cgen on the
flattened-variant axis.
Closes the cgen-drain mini-cluster (#201 -> #199 -> #200 -> #198).
773_isas_spread_variant: 5 rows (spread_is_inline_variant,
direct_cross_mod_tagged, cross_mod_named_void, same_module_variant,
spread_as_inline_payload). Rows 2-4 byte-id; rows 1/5 skip byte-id due
to layout-asymmetry on `...wrapper` (cstage flattens at resolve_type,
wwstage computes maxsz off vt.size of the un-spliced alias) — sibling
not blocking the checker correctness fix.
|
|||
| 4d44242363 |
wcc: reject transitive nested-tagged widen at type_assignable (#199 α)
cgen has no wrapped-slot layout — the tagged-union slot is universally [tag:8B][payload:up_to_24B], single level. The recursive walk admitted let r: (size|io.eof|io.error) = u for u: io.underread (transitively in io.error.params); cg_tag_for_variant + taggedvariantindext don't recurse, returned -1, defaulted to tag=0, and the slot read back as variant 0 = size at runtime. Restores SSoT inside the checker pair: is / as / match variant lookup is already non-recursive (#198 sibling), and the LET-init / return / assign arms now agree. Aligns DOWN to the leaner side (rule-10 stage symmetry). ww-stricter than Hare; harec keeps the drill at ref/harec/src/types.c:702-739 (#199b is the deferred wrapped-slot layout port). Pre-flight audit (drew mandate): zero transitive-widen sites in lib/ + selfhost/ + cmd/ + examples/. No wrapper-tagged variant (io.error, strconv.error, fmt.field) is used as a variant of a wider union anywhere in bootstrap. Mechanical fix. Escape hatch for callers: spread (...wrapper) inlines the wrapper's flat variants into the parent set at parse time. Wwstage's gate additionally preserves the recursive drill on op == TK_ELLIPSIS because wwstage stays AST-keyed (cstage flattens at resolve_type). 771_widen_transitive: 5 rows (reject_transitive_widen, spread_alt_widen, direct_flat_variant, branched_callee_widen, wrapper_typed_widen). Row 2 is CS-only — wwstage's is / match on spread-expanded variants is open-bug #190/#198. |
|||
| ba044e206d |
wcc: route deref-call N_CALL to fn-VALUE fallback (#181)
Pre-fix the wwstage checker bailed asserttyped on the N_CALL whose
callee was N_UN TK_STAR over a *fn — selfhost/cmd/wcc/check.ww
exprtype's N_CALL arm only resolved IDENT/DOT-named callees and
early-returned nil for any other shape, leaving e.type_ unstamped
so the post-checker invariant fired. cstage worked because cexpr
recurses on the callee — TK_STAR's unop arm returns t->sub which
IS the TY_FN, no name path needed.
Fix: replace the `if (nm.len == 0) return nil` early-bail with
`if (nm.len > 0) { name-lookup }`, so non-named callees fall
through to the existing fn-VALUE fallback below (peel TPTR /
dealias to TFN / stamp the result type). Mirrors harec
check_autodereference at ref/harec/src/check.c:1566. cgen post
-#180+#185 already lowers the deref-call correctly, so lifting
the asserttyped bail is silent-SIGSEGV-safe per drew + ken.
Combined.ww regenerated for selfhost/cmd/{w6c,wwdump}/main
.combined.ww per #110 freshness gate.
Probe: test/wcc/766_star_fn_deref_call.c, 5 rows table-driven —
minimal / branched-callee / alias-chain / fn-with-args / fn
-tuple-return. Gate flip from 765: every row now gates BOTH
stages — cstage runtime, wwstage runtime, AND cs.s == ww.s byte
-id. This is the runtime coverage 765 deferred plus the symmetry
gate that proves both stages emit identical asm for the deref
-call shape. Closes the full c-cluster (#180 + #185 + #181 all 3
commits working together end-to-end).
|
|||
| 7031e0d713 |
wcc: typeeqast TY_FN compares result/params/variadic at AST layer (#94 fold-b)
Pre-fix master left N_TFN under typeeqast's conservative
"anything else fails" tail (selfhost/cmd/wcc/check.ww:748-751).
case-patterns spelled with a raw `*fn(...)` head — the io vtable
use case — tripped casevariantin / casecovers on every variant
compare, so a well-typed `match (v: tagged-of-fn-ptr) { case
*fn(...) => ... }` would not compile under wwstage.
Adds a TY_FN arm that mirrors harec STORAGE_FUNCTION
(ref/harec/src/types.c:589-615): recurse on the result type
(.lhs), iterate the param chain (.list of N_PARAM, descend each
.lhs), require the variadic flag (.op == TK_ELLIPSIS) to match
position-by-position, and require both chains to terminate
together. Param NAMES do not participate (harec analog), and the
C-variadic terminal sentinel (N_PARAM with .str == "...") is
handled defensively even though wwstage's parseparams doesn't
currently produce it. Attributes + default-param values are NOT
checked (drew-pre-approved, harec doesn't either).
cstage type.c:239's type_eq walks the same shape on the resolved
Type. typeeqast lives one layer below — a documented divergence
filed as project #178 for the harmonization fold; the in-source
comment cites #178.
Probe 763_typeeq_fn_ast.c locks 7 rows covering identical /
diff-return / diff-arity / diff-param-type / variadic / param-
name-only / io-vtable shapes across cstage + wwstage (14
fixtures). Pre-fix wwstage red-errors every row at the checker
("case: not a variant of scrutinee" + "match: variant not
handled"); post-fix all 14 compile and the tag-0 arm fires
(exit 7). Per-row .s byte-id is intentionally NOT gated — see
the probe header for the cgmatch N_TPTR-not-routed-to-
flatvariantidx sibling bug that drives the divergence on rows
b/c/d/e/g; orthogonal to this AST-layer typeeqast fold and not
swept per the brief's "do not sweep" instruction.
|
|||
| 719893743e |
wcc: arm asserttyped bail — a nil-typed value node is now fatal
The wwstage asserttyped pass only WARNED on a checked value-node with no result type_, a check-bail-discipline regression that let gate-blind nil-stamp miscompiles ship green (the whole #6 arc: tuple/struct/fn-ptr/enum/binding nil-stamps were all invisible to the byte-id gates). With every nil-gap class now stamped (module-qual calls, fn-ptr-field calls, computed enum value-exprs, for-range/massign binds) the bail can finally arm: warn -> os.exit(1). Exempt exactly the two legitimately-no-type value classes, each a positive cited assertion (never a residual warn): the EXPR_ASSERT family (abort/assert, guarded against a user shadow; harec check.c:877,893) and seeded pseudo-builtin callees (len/append/free/alloc/size — a structural nil-decl-SK_FN predicate, not a name-list). The pre-existing module-ref and dot-lhs filters stay: they identify access-path components that aren't value exprs (harec EXPR_ACCESS), not exemptions. Verified clean over the broadest net — the armed checker over all five self-build combined units (the full selfhost source) plus the 901 gap corpus — zero out-of-class bail; fails-loud confirmed (undeclared call, abort's args, a nil dot-base all bail). Checker-only: 990-997 byte-id hold. |
|||
| f892a91223 |
wcc: stamp for-range tuple-binds and massign blank via shared helper
For-range tuple-destructure binders (for (let (k,v) .. s)) and the tuple massign discard _ were left nil-typed: the for-range binders are N_IDENT use-sites and the _ slot, though unbound, has a real element type. Add a shared stamptuplebinds helper — one lockstep walk distributing an N_TTUPLE's per-element types onto a binder chain — refactoring the existing N_MLET destructure loop into it (behavior identical) and adding N_FORRANGE and N_MASSIGN call-sites. _ is STAMPED with its slot's element type (unbound is not untyped), not exempted. Mirrors harec create_unpack_bindings (ref/harec/src/check.c:1354-1419), the routine harec shares between let-unpack and the for-each header (:2308-2317). A prerequisite for arming the wwstage asserttyped bail. byte-id holds (cgen derives binder/elem widths structurally, never off type_; 990-997 green). Extends the 901 gap-corpus with 901_forrange_tuple.ww + 901_massign_blank.ww. |
|||
| 3252bd1709 |
wcc: stamp computed enum-member value-exprs
A computed enum member — B = A + 4, RW = R | W, sibling/chained backref — left its value-expr node nil-typed: enumvalfold folds the constant but never stamps the expr, and since enum members are not installed as scope idents the sibling backref resolves to nothing, so BOTH the N_BIN/N_UN wrapper and the backref N_IDENT go nil (literal members are fine). Stamp the value-expr subtree (only-nil) to the enum's underlying storage type via a new stampenumvals/stampnilexpr pass on the N_TENUM branch. Mirrors harec checking each member value-expr at the underlying type (ref/harec/src/check.c:4419). A prerequisite for arming the wwstage asserttyped bail. Checker-only — the value folds to a constant at every use site and in cgen, so the node's type_ is never read by codegen; 990-997 byte-id hold. Extends the 901 gap-corpus with 901_enum_corpus.ww. |