From 76994a8279eea5b0dbd6ed1ec5317cd1fb69584a Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 4 Jun 2026 10:51:56 +0900 Subject: [PATCH] =?UTF-8?q?wcc+w6c=5Fww:=20typed=20dot-read=20resolver=20?= =?UTF-8?q?=E2=80=94=20kill=20both=20silent=20N=5FDOT=20fallbacks=20(F4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 (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. --- cmd/w6c/cgen.c | 126 +++++++++-- selfhost/cmd/w6c/main.combined.ww | 287 +++++++++++++++++++----- selfhost/cmd/wcc/cgenexpr.ww | 287 +++++++++++++++++++----- selfhost/cmd/wwdump/main.combined.ww | 287 +++++++++++++++++++----- test/wcc/805_placeaddr_store.c | 319 +++++++++++++++++++++++---- test/wcc/806_append_place.c | 26 ++- 6 files changed, 1112 insertions(+), 220 deletions(-) diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 613a1abd..5d4a7ff6 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -2019,12 +2019,13 @@ aggarg_srcaddr(Cg *c, Node *src, int dst, Local *locals) /* cgplaceaddr — compute the ADDRESS of an arbitrary place (lvalue) * expression into dst_reg; returns 1 when the shape is wired, 0 * otherwise (the caller loud-stops — rule 7, never a silent drop). - * F6 resolver, commit C1: only the deref-rooted spine is wired — - * `(*p)[i].f` as N_UN(STAR) root, N_INDEX hop over a slice/array - * place, N_DOT struct-field hop with one deref for a *struct base. - * Ident-rooted spines stay with the enumerated N_ASSIGN arms so this - * resolver never perturbs their asm; the F4 read-walker and F5 - * let-copy accrete here in follow-up commits. ADDRESS COMPUTATION + * F6 resolver, commit C1: `(*p)[i].f` as N_UN(STAR) root, N_INDEX hop + * over a slice/array place, N_DOT struct-field hop with one deref for + * a *struct base. C2 (F4 read-walker) adds the N_IDENT root (local / + * let / DATA-backed def) so indexed-ident spines (`threads[0].cap.end`) + * resolve too. Enumerated arms still win at every dispatch site (they + * are checked first), so shapes that worked pre-C1 keep their asm; the + * F5 let-copy accretes here in a follow-up commit. ADDRESS COMPUTATION * ONLY — every call-site keeps its own load/store/copy emission. * Clobbers AX/CX (cgexpr on index / pointer operands) and balances * its own PUSHQ/POPQ; dst_reg must not be AX or CX. */ @@ -2032,6 +2033,19 @@ static int cgplaceaddr(Cg *c, Node *n, int dst_reg, Local *locals) { if (n == NULL) return 0; + if (n->kind == N_IDENT) { + int off = localfind(locals, n->str); + if (off != 0) { + ins2(c, A_LEAQ, amem(D_BP, off), areg(dst_reg)); + return 1; + } + if (let_islet(n->str) || def_isstructdef(n->str) + || def_isarraydef(n->str)) { + ins2(c, A_LEAQ, masym(c, n->str), areg(dst_reg)); + return 1; + } + return 0; + } if (n->kind == N_UN && n->op == TK_STAR) { /* &(*e) is e's value — no load. */ cgexpr(c, n->lhs, locals); @@ -2042,11 +2056,10 @@ cgplaceaddr(Cg *c, Node *n, int dst_reg, Local *locals) Node *base = n->lhs; Node *idx = n->rhs; if (base == NULL || idx == NULL) return 0; - /* Deref base only: ident/dot index bases all have - * enumerated arms; routing them here would change - * their asm. */ - if (!(base->kind == N_UN && base->op == TK_STAR)) - return 0; + /* C2: any addressable base — recursion decides (deref / + * ident / dot / index spine). Ident-rooted shapes with + * enumerated arms never reach the resolver (those arms + * dispatch first), so their asm is untouched. */ Type *bu = type_chase_named(base->type); if (bu == NULL) return 0; if (bu->kind != TY_SLICE && bu->kind != TY_ARRAY) @@ -3809,9 +3822,18 @@ cgexpr(Cg *c, Node *n, Local *locals) ins2(c, A_ADDQ, areg(D_BX), areg(D_AX)); break; } - /* Other shapes (& on a complex expr): silent drop, - * mirrors the pre-existing fallback. */ - break; + /* C2 (F4 family, reviewer-A route): address-of through + * an indexed/deref dot spine (`&threads[0].cap`, + * `&(*p)[i].f`) — the spine walker above roots only at + * idents. Route the place address through cgplaceaddr + * (read-twin in case N_DOT). Any remaining shape dies + * LOUD: the pre-C2 silent drop left stale AX as the + * "address" — a gate-blind SEGFAULT at the deref. */ + if (cgplaceaddr(c, opnd, D_BX, locals)) { + ins2(c, A_MOVQ, areg(D_BX), areg(D_AX)); + break; + } + fatal("unsupported address-of shape"); } cgexpr(c, n->lhs, locals); switch (n->op) { @@ -9514,8 +9536,13 @@ cgexpr(Cg *c, Node *n, Local *locals) * linker surfaces a clean undefined-symbol error on the leaf * — mirrors the bare-N_IDENT unresolved fallback used by * single-segment N_DOTs. Keeps cstage / wwstage byte-aligned - * on the cgen-match isolation probes. */ - if (n->lhs && n->lhs->kind == N_DOT && n->str) { + * on the cgen-match isolation probes. C2 (F4): gated to + * UNTYPED chains only — pre-C2 it swallowed every unmatched + * dot-over-dot chain, turning a TYPED depth-2 read behind an + * index/deref spine (`threads[0].cap.end`) into a silent + * global read of a colliding leaf symbol (p6min10 exit 66). */ + if (n->lhs && n->lhs->kind == N_DOT && n->str + && (n->lhs->type == NULL || n->lhs->type == ty_err)) { ins2(c, A_MOVQ, masym(c, n->str), areg(D_AX)); break; } @@ -9524,10 +9551,69 @@ cgexpr(Cg *c, Node *n, Local *locals) * TY_STR, so it misses the typed slice/str gate above and * lands here. cgexpr leaves (AX=ptr, BX=len); `.ptr` keeps AX, * `.len` shuffles BX→AX. Mirrors wwstage cgdot's catch-all - * (selfhost/cmd/wcc/cgenexpr.ww). #14. */ - cgexpr(c, n->lhs, locals); - if (lenfld) - ins2(c, A_MOVQ, areg(D_BX), areg(D_AX)); + * (selfhost/cmd/wcc/cgenexpr.ww). #14. C2 (F4/FA3): gated to + * TY_UNTYPED_STR — pre-C2 this was the offset- and header- + * blind catch-all every unmatched typed dot fell into, so a + * nonzero-offset field behind a deref-index spine read the + * element's word 0 (`(*p)[i].slicefield` → 1-word wrong- + * offset read; offset-0 scalars worked by COINCIDENCE). */ + { + Type *cu = type_chase_named( + n->lhs ? n->lhs->type : NULL); + if (cu && cu->kind == TY_UNTYPED_STR) { + cgexpr(c, n->lhs, locals); + if (lenfld) + ins2(c, A_MOVQ, areg(D_BX), + areg(D_AX)); + break; + } + } + /* C2 read-resolver (F4 + FA3-cstage): a TYPED N_DOT read no + * enumerated arm matched — depth-2+ chains and slice/str/ + * scalar fields behind index/deref spines. Address via + * cgplaceaddr (the C1 resolver), leaf load emitted here by + * kind. Leaf kinds with no canonical register convention in + * expr position stay LOUD; any shape the resolver can't + * address dies LOUD (rule 7) — the pre-C2 tails guessed. */ + { + Type *rt = n->type; + Type *ru = type_chase_named(rt); + if (ru && ru->kind == TY_TAGGED) + fatal("read-resolver: tagged field read not " + "wired (rule-7)"); + if (ru && (ru->kind == TY_STRUCT + || ru->kind == TY_TUPLE)) + fatal("read-resolver: aggregate field read " + "not wired (rule-7)"); + if (!cgplaceaddr(c, n, D_BX, locals)) + fatal("unsupported field-read shape"); + int rd_isf32 = 0; + if (fld_isfloat(rt, &rd_isf32)) { + ins2(c, rd_isf32 ? A_MOVSS : A_MOVSD, + amem(D_BX, 0), areg(D_X0)); + goto dot_done; + } + if (ru && ru->kind == TY_ARRAY) { + /* `[N]T` leaf: leave the field ADDRESS — a + * base for an outer index, never a value + * (#270-1a semantics). */ + ins2(c, A_MOVQ, areg(D_BX), areg(D_AX)); + goto dot_done; + } + if (ru && (ru->kind == TY_STR + || ru->kind == TY_SLICE)) { + /* str IS []u8 — 3-word {ptr,len,cap} into + * (AX, BX, CX). BX is the place base, so + * load .len (which targets BX) LAST. */ + ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_AX)); + ins2(c, A_MOVQ, amem(D_BX, 16), areg(D_CX)); + ins2(c, A_MOVQ, amem(D_BX, 8), areg(D_BX)); + goto dot_done; + } + int rdsz = (int)(rt ? rt->size : 8); + ins2(c, fldloadop(rt, rdsz), amem(D_BX, 0), + areg(D_AX)); + } dot_done: break; } diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 5206ddf2..220a2061 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -21381,18 +21381,49 @@ fn aggargsrcaddr(c: *cgen, src: *node, dst: str) bool = { // expression into dstreg; returns true when the shape is wired, false // otherwise (the caller loud-stops — rule 7, never a silent drop). // F6 resolver, commit C1 — mirror of cstage cmd/w6c/cgen.c -// cgplaceaddr: only the deref-rooted spine is wired (`(*p)[i].f` as -// N_UN(STAR) root, N_INDEX hop over a slice/array place, N_DOT -// struct-field hop with one deref for a *struct base). All type keys -// come off the checker-STAMPED tinfo (.type_), never tnode names — -// the #209/#211 discipline. Ident-rooted spines stay with the -// enumerated cgassign arms so this resolver never perturbs their -// asm. ADDRESS COMPUTATION ONLY — every call-site keeps its own -// load/store/copy emission. Clobbers AX/CX (cgexpr on index / -// pointer operands) and balances its own PUSHQ/POPQ; dstreg must -// not be AX or CX. +// cgplaceaddr: `(*p)[i].f` as N_UN(STAR) root, N_INDEX hop over a +// slice/array place, N_DOT struct-field hop with one deref for a +// *struct base. C2 (F4 read-walker) adds the N_IDENT root (local / +// let / DATA-backed def) so indexed-ident spines resolve too. All +// type keys come off the checker-STAMPED tinfo (.type_), never tnode +// names — the #209/#211 discipline. Enumerated arms still win at +// every dispatch site (they are checked first), so shapes that worked +// pre-C1 keep their asm. ADDRESS COMPUTATION ONLY — every call-site +// keeps its own load/store/copy emission. Clobbers AX/CX (cgexpr on +// index / pointer operands) and balances its own PUSHQ/POPQ; dstreg +// must not be AX or CX. fn cgplaceaddr(c: *cgen, n: *node, dstreg: str) bool = { if (n == nil) { return false; }; + if (n.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, n.str); + if (lc != nil) { + emitline("\tLEAQ\t"); + emitoff(lc.off: i64); + emitline("(BP), "); + emitline(dstreg); + emitline("\n"); + return true; + }; + let gok: bool = isletvar(c, n.str); + if (!gok) { + if (defvarstructinfo(c, n.str) != nil) { gok = true; }; + }; + if (!gok) { + let dtn: *node = defvartnode(c, n.str); + if (dtn != nil) { + if (dtn.kind == nkind.N_TARRAY) { gok = true; }; + }; + }; + if (gok) { + emitline("\tLEAQ\t"); + emitsymname(c, n.str); + emitline("(SB), "); + emitline(dstreg); + emitline("\n"); + return true; + }; + return false; + }; if (n.kind == nkind.N_UN) { if (n.op != tkind.TK_STAR) { return false; }; // &(*e) is e's value — no load. @@ -21406,11 +21437,10 @@ fn cgplaceaddr(c: *cgen, n: *node, dstreg: str) bool = { let base: *node = n.lhs; let idx: *node = n.rhs; if (base == nil || idx == nil) { return false; }; - // Deref base only: ident/dot index bases all have - // enumerated arms; routing them here would change - // their asm. - if (base.kind != nkind.N_UN) { return false; }; - if (base.op != tkind.TK_STAR) { return false; }; + // C2: any addressable base — recursion decides (deref / + // ident / dot / index spine). Ident-rooted shapes with + // enumerated arms never reach the resolver (those arms + // dispatch first), so their asm is untouched. let bu: *tinfo = base.type_: *tinfo; for (bu != nil && bu.kind == tykind.TY_NAMED) { bu = bu.under; }; if (bu == nil) { return false; }; @@ -23318,35 +23348,57 @@ fn cgdot(c: *cgen, n: *node) void = { // expression — that leaves (AX=ptr, BX=len). Then `.ptr` returns // AX as is; `.len` shuffles BX→AX. cstage cgen.c was aligned UP // to this shuffle in #14 (it had returned the ptr for `.len`). - if (streq(fld, "ptr")) { cgexpr(c, lhs); return; }; - if (streq(fld, "len")) { - cgexpr(c, lhs); - emitline("\tMOVQ\tBX, AX\n"); - return; - }; - // .cap on a non-ident base (indexed element `t[i].cap`, call, - // dot-slice): cgexpr leaves the full {ptr,len,cap} header via - // cgslicehdr — shuffle CX→AX. The shuffle fires ONLY for a TYPED - // slice/str base (kind TY_SLICE/TY_STR after NAMED-chase) that is NOT - // a bare string literal: N_STRLIT's cgexpr loads only AX=ptr/BX=len - // (cgen.ww), never a CX cap, so `"abc".cap` must return AX unshuffled. - // cstage reaches that outcome by typing N_STRLIT as untyped_str - // (cgen.c:8456 catch-all, no cap shuffle); the wwstage checker types - // N_STRLIT as `str` instead (check.ww:2322 vs cstage check.c:1079 — - // divergence filed separately), so the TY_STR kind-gate alone would - // wrongly fire. The N_STRLIT exclusion keeps this byte-identical with - // cstage. #13 read-fix, sibling of the #20 store. - if (streq(fld, "cap")) { - cgexpr(c, lhs); - if (lhs != nil && lhs.kind != nkind.N_STRLIT) { - let lu: *tinfo = lhs.type_: *tinfo; - for (lu != nil && lu.kind == tykind.TY_NAMED) { lu = lu.under; }; - if (lu != nil && (lu.kind == tykind.TY_SLICE - || lu.kind == tykind.TY_STR)) { - emitline("\tMOVQ\tCX, AX\n"); + // C2 (F4): gated to a slice/str/untyped-str STAMPED base (or an + // N_STRLIT, which ww types as `str` anyway) — pre-C2 this arm was + // shape-blind, so a STRUCT field that merely shares a pseudo-field + // NAME behind a non-ident spine took the offset-blind cgexpr path + // while cstage (type-gated) routes it to the read-resolver. + { + let pbu: *tinfo = nil; + if (lhs != nil) { pbu = lhs.type_: *tinfo; }; + for (pbu != nil && pbu.kind == tykind.TY_NAMED) { pbu = pbu.under; }; + let pbok: bool = false; + if (pbu != nil) { + if (pbu.kind == tykind.TY_SLICE + || pbu.kind == tykind.TY_STR + || pbu.kind == tykind.TY_UNTYPED_STR) { pbok = true; }; + }; + if (lhs != nil) { + if (lhs.kind == nkind.N_STRLIT) { pbok = true; }; + }; + if (pbok) { + if (streq(fld, "ptr")) { cgexpr(c, lhs); return; }; + if (streq(fld, "len")) { + cgexpr(c, lhs); + emitline("\tMOVQ\tBX, AX\n"); + return; + }; + // .cap on a non-ident base (indexed element `t[i].cap`, + // call, dot-slice): cgexpr leaves the full {ptr,len,cap} + // header via cgslicehdr — shuffle CX→AX. The shuffle + // fires ONLY for a TYPED slice/str base (kind TY_SLICE/ + // TY_STR after NAMED-chase) that is NOT a bare string + // literal: N_STRLIT's cgexpr loads only AX=ptr/BX=len + // (cgen.ww), never a CX cap, so `"abc".cap` must return + // AX unshuffled. cstage reaches that outcome by typing + // N_STRLIT as untyped_str (cgen.c catch-all, no cap + // shuffle); the wwstage checker types N_STRLIT as `str` + // instead (check.ww:2322 vs cstage check.c:1079 — + // divergence filed separately), so the TY_STR kind-gate + // alone would wrongly fire. The N_STRLIT exclusion keeps + // this byte-identical with cstage. #13 read-fix, sibling + // of the #20 store. + if (streq(fld, "cap")) { + cgexpr(c, lhs); + if (lhs != nil && lhs.kind != nkind.N_STRLIT) { + if (pbu != nil && (pbu.kind == tykind.TY_SLICE + || pbu.kind == tykind.TY_STR)) { + emitline("\tMOVQ\tCX, AX\n"); + }; + }; + return; }; }; - return; }; // Chained struct-field-via-ptr-via-ptr access: // r.sym.val where r: *lrel, .sym: *lsym, .val: u64 @@ -23564,16 +23616,126 @@ fn cgdot(c: *cgen, n: *node) void = { // driver concatenation — the inner enum / struct hasn't been // seen). Emit `MOVQ (SB), AX` so the linker surfaces a // clean undefined-symbol error on the leaf. Mirror of - // cmd/w6c/cgen.c N_DOT nested fallback. + // cmd/w6c/cgen.c N_DOT nested fallback. C2 (F4): gated to UNTYPED + // chains only — pre-C2 it swallowed every unmatched dot-over-dot + // chain, turning a TYPED depth-2 read behind an index/deref spine + // into a silent global read of a colliding leaf symbol. if (lhs != nil) { if (lhs.kind == nkind.N_DOT) { - emitline("\tMOVQ\t"); - emitsymname(c, fld); - emitline("(SB), AX\n"); - return; + let sbt: *tinfo = lhs.type_: *tinfo; + if (sbt == nil || sbt.kind == tykind.TY_ERR) { + emitline("\tMOVQ\t"); + emitsymname(c, fld); + emitline("(SB), AX\n"); + return; + }; }; }; - return; + // C2/C3 boundary: `xs[i].fld` with an IDENT index base is the + // enumerated N_INDEX-base arm's territory (cstage handles it + // type-keyed); reaching HERE means wwstage's name-keyed arm above + // missed (the F10 alias-tnode class). Routing it through the + // read-resolver would emit a different (albeit correct) sequence + // than cstage's arm → cs≠ww. Loud until C3 re-keys that arm onto + // stamped tinfo (task #8). + if (lhs != nil) { + if (lhs.kind == nkind.N_INDEX) { + if (lhs.lhs != nil) { + if (lhs.lhs.kind == nkind.N_IDENT) { + let mg: str = "cgdot: ident-indexed field read unwired in wwstage (C3/task #8)\n"; + os.write(2, mg.ptr, mg.len: u64); + os.exit(1); + }; + }; + }; + }; + // cstage reads ptr-chained fields (`a.p.f`, any root) in its + // chained-*struct arm; wwstage's #70 mirror above is gated + // root-local — the uncovered remainder (global / indexed roots) + // must not take the resolver (its sequence differs from cstage's + // arm → cs≠ww). Loud; the wwstage alignment is filed as task + // #37. + if (lhs != nil) { + if (lhs.kind == nkind.N_DOT) { + let pgu: *tinfo = lhs.type_: *tinfo; + for (pgu != nil && pgu.kind == tykind.TY_NAMED) { pgu = pgu.under; }; + if (pgu != nil) { + if (pgu.kind == tykind.TY_PTR) { + let mp: str = "cgdot: ptr-chained field read unwired in wwstage (task #37)\n"; + os.write(2, mp.ptr, mp.len: u64); + os.exit(1); + }; + }; + }; + }; + // C2 read-resolver (F4 + FA3): a TYPED N_DOT read no enumerated + // arm matched — depth-2+ chains and slice/str/scalar fields behind + // index/deref spines. Address via cgplaceaddr (the C1 resolver), + // leaf load emitted here by kind. Leaf kinds with no canonical + // register convention in expr position stay LOUD; any shape the + // resolver can't address dies LOUD (rule 7) — the pre-C2 tail + // silently emitted NOTHING. Mirror of cstage cgen.c case N_DOT + // read-resolver tail. + { + let rdt: *tinfo = n.type_: *tinfo; + let rdu: *tinfo = rdt; + for (rdu != nil && rdu.kind == tykind.TY_NAMED) { rdu = rdu.under; }; + if (rdu != nil) { + if (rdu.kind == tykind.TY_TAGGED) { + let mt: str = "read-resolver: tagged field read not wired (rule-7)\n"; + os.write(2, mt.ptr, mt.len: u64); + os.exit(1); + }; + // ww-asymmetric in LET position: cstage's let-init + // consumes `let c = (*ts)[i].cap` BEFORE its N_DOT + // tail and emits NO copy (the F5 bug) — cs-builds/ + // ww-louds on that shape until the F5 let-copy + // lands (task #7). Absent from the gate corpus. + if (rdu.kind == tykind.TY_STRUCT + || rdu.kind == tykind.TY_TUPLE) { + let ma: str = "read-resolver: aggregate field read not wired (rule-7)\n"; + os.write(2, ma.ptr, ma.len: u64); + os.exit(1); + }; + }; + if (!cgplaceaddr(c, n, "BX")) { + let mu: str = "unsupported field-read shape\n"; + os.write(2, mu.ptr, mu.len: u64); + os.exit(1); + }; + if (typeisfloat(rdt)) { + let mov: str = "MOVSD"; + if (typeisf32(rdt)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t(BX), X0\n"); + return; + }; + if (rdu != nil && rdu.kind == tykind.TY_ARRAY) { + // `[N]T` leaf: leave the field ADDRESS — a base for + // an outer index, never a value (#270-1a semantics). + emitline("\tMOVQ\tBX, AX\n"); + return; + }; + if (rdu != nil && (rdu.kind == tykind.TY_STR + || rdu.kind == tykind.TY_SLICE)) { + // str IS []u8 — 3-word {ptr,len,cap} into (AX, BX, + // CX). BX is the place base, so load .len (which + // targets BX) LAST. + emitline("\tMOVQ\t(BX), AX\n"); + emitline("\tMOVQ\t16(BX), CX\n"); + emitline("\tMOVQ\t8(BX), BX\n"); + return; + }; + let lop: str = "MOVQ"; + if (rdt != nil) { + lop = loadopsz(typeissigned(rdt), rdt.slotsize: i32); + }; + emitline("\t"); + emitline(lop); + emitline("\t(BX), AX\n"); + return; + }; }; fn cgun(c: *cgen, n: *node) void = { @@ -23882,9 +24044,21 @@ fn cgun(c: *cgen, n: *node) void = { }; }; }; - // Fall through silently (mirrors cstage silent- - // drop fallback at the end of the TK_AMP block). - return; + // C2 (F4 family, reviewer-A route): address-of + // through an indexed/deref dot spine — the + // arms above root only at idents. Route the + // place address through cgplaceaddr (read-twin + // in cgdot). Any remaining shape dies LOUD: + // the pre-C2 silent drop left stale AX as the + // "address" — a gate-blind SEGFAULT at the + // deref. Mirror of cstage TK_AMP tail. + if (cgplaceaddr(c, opnd, "BX")) { + emitline("\tMOVQ\tBX, AX\n"); + return; + }; + let mam: str = "unsupported address-of shape\n"; + os.write(2, mam.ptr, mam.len: u64); + os.exit(1); }; if (opnd.kind == nkind.N_INDEX) { // &base[i] = base + i*esz, no dereference. @@ -23987,6 +24161,17 @@ fn cgun(c: *cgen, n: *node) void = { emitline("\tADDQ\tBX, AX\n"); return; }; + // C2: deref-rooted (`&(*p)`) and other non-ident + // operands — resolver-or-loud, the same tail as the + // N_DOT arm above (cstage has ONE shared tail for + // both). + if (cgplaceaddr(c, opnd, "BX")) { + emitline("\tMOVQ\tBX, AX\n"); + return; + }; + let mam2: str = "unsupported address-of shape\n"; + os.write(2, mam2.ptr, mam2.len: u64); + os.exit(1); }; return; }; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 563f262c..7b34e05b 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -1303,18 +1303,49 @@ fn aggargsrcaddr(c: *cgen, src: *node, dst: str) bool = { // expression into dstreg; returns true when the shape is wired, false // otherwise (the caller loud-stops — rule 7, never a silent drop). // F6 resolver, commit C1 — mirror of cstage cmd/w6c/cgen.c -// cgplaceaddr: only the deref-rooted spine is wired (`(*p)[i].f` as -// N_UN(STAR) root, N_INDEX hop over a slice/array place, N_DOT -// struct-field hop with one deref for a *struct base). All type keys -// come off the checker-STAMPED tinfo (.type_), never tnode names — -// the #209/#211 discipline. Ident-rooted spines stay with the -// enumerated cgassign arms so this resolver never perturbs their -// asm. ADDRESS COMPUTATION ONLY — every call-site keeps its own -// load/store/copy emission. Clobbers AX/CX (cgexpr on index / -// pointer operands) and balances its own PUSHQ/POPQ; dstreg must -// not be AX or CX. +// cgplaceaddr: `(*p)[i].f` as N_UN(STAR) root, N_INDEX hop over a +// slice/array place, N_DOT struct-field hop with one deref for a +// *struct base. C2 (F4 read-walker) adds the N_IDENT root (local / +// let / DATA-backed def) so indexed-ident spines resolve too. All +// type keys come off the checker-STAMPED tinfo (.type_), never tnode +// names — the #209/#211 discipline. Enumerated arms still win at +// every dispatch site (they are checked first), so shapes that worked +// pre-C1 keep their asm. ADDRESS COMPUTATION ONLY — every call-site +// keeps its own load/store/copy emission. Clobbers AX/CX (cgexpr on +// index / pointer operands) and balances its own PUSHQ/POPQ; dstreg +// must not be AX or CX. fn cgplaceaddr(c: *cgen, n: *node, dstreg: str) bool = { if (n == nil) { return false; }; + if (n.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, n.str); + if (lc != nil) { + emitline("\tLEAQ\t"); + emitoff(lc.off: i64); + emitline("(BP), "); + emitline(dstreg); + emitline("\n"); + return true; + }; + let gok: bool = isletvar(c, n.str); + if (!gok) { + if (defvarstructinfo(c, n.str) != nil) { gok = true; }; + }; + if (!gok) { + let dtn: *node = defvartnode(c, n.str); + if (dtn != nil) { + if (dtn.kind == nkind.N_TARRAY) { gok = true; }; + }; + }; + if (gok) { + emitline("\tLEAQ\t"); + emitsymname(c, n.str); + emitline("(SB), "); + emitline(dstreg); + emitline("\n"); + return true; + }; + return false; + }; if (n.kind == nkind.N_UN) { if (n.op != tkind.TK_STAR) { return false; }; // &(*e) is e's value — no load. @@ -1328,11 +1359,10 @@ fn cgplaceaddr(c: *cgen, n: *node, dstreg: str) bool = { let base: *node = n.lhs; let idx: *node = n.rhs; if (base == nil || idx == nil) { return false; }; - // Deref base only: ident/dot index bases all have - // enumerated arms; routing them here would change - // their asm. - if (base.kind != nkind.N_UN) { return false; }; - if (base.op != tkind.TK_STAR) { return false; }; + // C2: any addressable base — recursion decides (deref / + // ident / dot / index spine). Ident-rooted shapes with + // enumerated arms never reach the resolver (those arms + // dispatch first), so their asm is untouched. let bu: *tinfo = base.type_: *tinfo; for (bu != nil && bu.kind == tykind.TY_NAMED) { bu = bu.under; }; if (bu == nil) { return false; }; @@ -3240,35 +3270,57 @@ fn cgdot(c: *cgen, n: *node) void = { // expression — that leaves (AX=ptr, BX=len). Then `.ptr` returns // AX as is; `.len` shuffles BX→AX. cstage cgen.c was aligned UP // to this shuffle in #14 (it had returned the ptr for `.len`). - if (streq(fld, "ptr")) { cgexpr(c, lhs); return; }; - if (streq(fld, "len")) { - cgexpr(c, lhs); - emitline("\tMOVQ\tBX, AX\n"); - return; - }; - // .cap on a non-ident base (indexed element `t[i].cap`, call, - // dot-slice): cgexpr leaves the full {ptr,len,cap} header via - // cgslicehdr — shuffle CX→AX. The shuffle fires ONLY for a TYPED - // slice/str base (kind TY_SLICE/TY_STR after NAMED-chase) that is NOT - // a bare string literal: N_STRLIT's cgexpr loads only AX=ptr/BX=len - // (cgen.ww), never a CX cap, so `"abc".cap` must return AX unshuffled. - // cstage reaches that outcome by typing N_STRLIT as untyped_str - // (cgen.c:8456 catch-all, no cap shuffle); the wwstage checker types - // N_STRLIT as `str` instead (check.ww:2322 vs cstage check.c:1079 — - // divergence filed separately), so the TY_STR kind-gate alone would - // wrongly fire. The N_STRLIT exclusion keeps this byte-identical with - // cstage. #13 read-fix, sibling of the #20 store. - if (streq(fld, "cap")) { - cgexpr(c, lhs); - if (lhs != nil && lhs.kind != nkind.N_STRLIT) { - let lu: *tinfo = lhs.type_: *tinfo; - for (lu != nil && lu.kind == tykind.TY_NAMED) { lu = lu.under; }; - if (lu != nil && (lu.kind == tykind.TY_SLICE - || lu.kind == tykind.TY_STR)) { - emitline("\tMOVQ\tCX, AX\n"); + // C2 (F4): gated to a slice/str/untyped-str STAMPED base (or an + // N_STRLIT, which ww types as `str` anyway) — pre-C2 this arm was + // shape-blind, so a STRUCT field that merely shares a pseudo-field + // NAME behind a non-ident spine took the offset-blind cgexpr path + // while cstage (type-gated) routes it to the read-resolver. + { + let pbu: *tinfo = nil; + if (lhs != nil) { pbu = lhs.type_: *tinfo; }; + for (pbu != nil && pbu.kind == tykind.TY_NAMED) { pbu = pbu.under; }; + let pbok: bool = false; + if (pbu != nil) { + if (pbu.kind == tykind.TY_SLICE + || pbu.kind == tykind.TY_STR + || pbu.kind == tykind.TY_UNTYPED_STR) { pbok = true; }; + }; + if (lhs != nil) { + if (lhs.kind == nkind.N_STRLIT) { pbok = true; }; + }; + if (pbok) { + if (streq(fld, "ptr")) { cgexpr(c, lhs); return; }; + if (streq(fld, "len")) { + cgexpr(c, lhs); + emitline("\tMOVQ\tBX, AX\n"); + return; + }; + // .cap on a non-ident base (indexed element `t[i].cap`, + // call, dot-slice): cgexpr leaves the full {ptr,len,cap} + // header via cgslicehdr — shuffle CX→AX. The shuffle + // fires ONLY for a TYPED slice/str base (kind TY_SLICE/ + // TY_STR after NAMED-chase) that is NOT a bare string + // literal: N_STRLIT's cgexpr loads only AX=ptr/BX=len + // (cgen.ww), never a CX cap, so `"abc".cap` must return + // AX unshuffled. cstage reaches that outcome by typing + // N_STRLIT as untyped_str (cgen.c catch-all, no cap + // shuffle); the wwstage checker types N_STRLIT as `str` + // instead (check.ww:2322 vs cstage check.c:1079 — + // divergence filed separately), so the TY_STR kind-gate + // alone would wrongly fire. The N_STRLIT exclusion keeps + // this byte-identical with cstage. #13 read-fix, sibling + // of the #20 store. + if (streq(fld, "cap")) { + cgexpr(c, lhs); + if (lhs != nil && lhs.kind != nkind.N_STRLIT) { + if (pbu != nil && (pbu.kind == tykind.TY_SLICE + || pbu.kind == tykind.TY_STR)) { + emitline("\tMOVQ\tCX, AX\n"); + }; + }; + return; }; }; - return; }; // Chained struct-field-via-ptr-via-ptr access: // r.sym.val where r: *lrel, .sym: *lsym, .val: u64 @@ -3486,16 +3538,126 @@ fn cgdot(c: *cgen, n: *node) void = { // driver concatenation — the inner enum / struct hasn't been // seen). Emit `MOVQ (SB), AX` so the linker surfaces a // clean undefined-symbol error on the leaf. Mirror of - // cmd/w6c/cgen.c N_DOT nested fallback. + // cmd/w6c/cgen.c N_DOT nested fallback. C2 (F4): gated to UNTYPED + // chains only — pre-C2 it swallowed every unmatched dot-over-dot + // chain, turning a TYPED depth-2 read behind an index/deref spine + // into a silent global read of a colliding leaf symbol. if (lhs != nil) { if (lhs.kind == nkind.N_DOT) { - emitline("\tMOVQ\t"); - emitsymname(c, fld); - emitline("(SB), AX\n"); - return; + let sbt: *tinfo = lhs.type_: *tinfo; + if (sbt == nil || sbt.kind == tykind.TY_ERR) { + emitline("\tMOVQ\t"); + emitsymname(c, fld); + emitline("(SB), AX\n"); + return; + }; }; }; - return; + // C2/C3 boundary: `xs[i].fld` with an IDENT index base is the + // enumerated N_INDEX-base arm's territory (cstage handles it + // type-keyed); reaching HERE means wwstage's name-keyed arm above + // missed (the F10 alias-tnode class). Routing it through the + // read-resolver would emit a different (albeit correct) sequence + // than cstage's arm → cs≠ww. Loud until C3 re-keys that arm onto + // stamped tinfo (task #8). + if (lhs != nil) { + if (lhs.kind == nkind.N_INDEX) { + if (lhs.lhs != nil) { + if (lhs.lhs.kind == nkind.N_IDENT) { + let mg: str = "cgdot: ident-indexed field read unwired in wwstage (C3/task #8)\n"; + os.write(2, mg.ptr, mg.len: u64); + os.exit(1); + }; + }; + }; + }; + // cstage reads ptr-chained fields (`a.p.f`, any root) in its + // chained-*struct arm; wwstage's #70 mirror above is gated + // root-local — the uncovered remainder (global / indexed roots) + // must not take the resolver (its sequence differs from cstage's + // arm → cs≠ww). Loud; the wwstage alignment is filed as task + // #37. + if (lhs != nil) { + if (lhs.kind == nkind.N_DOT) { + let pgu: *tinfo = lhs.type_: *tinfo; + for (pgu != nil && pgu.kind == tykind.TY_NAMED) { pgu = pgu.under; }; + if (pgu != nil) { + if (pgu.kind == tykind.TY_PTR) { + let mp: str = "cgdot: ptr-chained field read unwired in wwstage (task #37)\n"; + os.write(2, mp.ptr, mp.len: u64); + os.exit(1); + }; + }; + }; + }; + // C2 read-resolver (F4 + FA3): a TYPED N_DOT read no enumerated + // arm matched — depth-2+ chains and slice/str/scalar fields behind + // index/deref spines. Address via cgplaceaddr (the C1 resolver), + // leaf load emitted here by kind. Leaf kinds with no canonical + // register convention in expr position stay LOUD; any shape the + // resolver can't address dies LOUD (rule 7) — the pre-C2 tail + // silently emitted NOTHING. Mirror of cstage cgen.c case N_DOT + // read-resolver tail. + { + let rdt: *tinfo = n.type_: *tinfo; + let rdu: *tinfo = rdt; + for (rdu != nil && rdu.kind == tykind.TY_NAMED) { rdu = rdu.under; }; + if (rdu != nil) { + if (rdu.kind == tykind.TY_TAGGED) { + let mt: str = "read-resolver: tagged field read not wired (rule-7)\n"; + os.write(2, mt.ptr, mt.len: u64); + os.exit(1); + }; + // ww-asymmetric in LET position: cstage's let-init + // consumes `let c = (*ts)[i].cap` BEFORE its N_DOT + // tail and emits NO copy (the F5 bug) — cs-builds/ + // ww-louds on that shape until the F5 let-copy + // lands (task #7). Absent from the gate corpus. + if (rdu.kind == tykind.TY_STRUCT + || rdu.kind == tykind.TY_TUPLE) { + let ma: str = "read-resolver: aggregate field read not wired (rule-7)\n"; + os.write(2, ma.ptr, ma.len: u64); + os.exit(1); + }; + }; + if (!cgplaceaddr(c, n, "BX")) { + let mu: str = "unsupported field-read shape\n"; + os.write(2, mu.ptr, mu.len: u64); + os.exit(1); + }; + if (typeisfloat(rdt)) { + let mov: str = "MOVSD"; + if (typeisf32(rdt)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t(BX), X0\n"); + return; + }; + if (rdu != nil && rdu.kind == tykind.TY_ARRAY) { + // `[N]T` leaf: leave the field ADDRESS — a base for + // an outer index, never a value (#270-1a semantics). + emitline("\tMOVQ\tBX, AX\n"); + return; + }; + if (rdu != nil && (rdu.kind == tykind.TY_STR + || rdu.kind == tykind.TY_SLICE)) { + // str IS []u8 — 3-word {ptr,len,cap} into (AX, BX, + // CX). BX is the place base, so load .len (which + // targets BX) LAST. + emitline("\tMOVQ\t(BX), AX\n"); + emitline("\tMOVQ\t16(BX), CX\n"); + emitline("\tMOVQ\t8(BX), BX\n"); + return; + }; + let lop: str = "MOVQ"; + if (rdt != nil) { + lop = loadopsz(typeissigned(rdt), rdt.slotsize: i32); + }; + emitline("\t"); + emitline(lop); + emitline("\t(BX), AX\n"); + return; + }; }; fn cgun(c: *cgen, n: *node) void = { @@ -3804,9 +3966,21 @@ fn cgun(c: *cgen, n: *node) void = { }; }; }; - // Fall through silently (mirrors cstage silent- - // drop fallback at the end of the TK_AMP block). - return; + // C2 (F4 family, reviewer-A route): address-of + // through an indexed/deref dot spine — the + // arms above root only at idents. Route the + // place address through cgplaceaddr (read-twin + // in cgdot). Any remaining shape dies LOUD: + // the pre-C2 silent drop left stale AX as the + // "address" — a gate-blind SEGFAULT at the + // deref. Mirror of cstage TK_AMP tail. + if (cgplaceaddr(c, opnd, "BX")) { + emitline("\tMOVQ\tBX, AX\n"); + return; + }; + let mam: str = "unsupported address-of shape\n"; + os.write(2, mam.ptr, mam.len: u64); + os.exit(1); }; if (opnd.kind == nkind.N_INDEX) { // &base[i] = base + i*esz, no dereference. @@ -3909,6 +4083,17 @@ fn cgun(c: *cgen, n: *node) void = { emitline("\tADDQ\tBX, AX\n"); return; }; + // C2: deref-rooted (`&(*p)`) and other non-ident + // operands — resolver-or-loud, the same tail as the + // N_DOT arm above (cstage has ONE shared tail for + // both). + if (cgplaceaddr(c, opnd, "BX")) { + emitline("\tMOVQ\tBX, AX\n"); + return; + }; + let mam2: str = "unsupported address-of shape\n"; + os.write(2, mam2.ptr, mam2.len: u64); + os.exit(1); }; return; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index aecd5cb3..bf61b94f 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -21381,18 +21381,49 @@ fn aggargsrcaddr(c: *cgen, src: *node, dst: str) bool = { // expression into dstreg; returns true when the shape is wired, false // otherwise (the caller loud-stops — rule 7, never a silent drop). // F6 resolver, commit C1 — mirror of cstage cmd/w6c/cgen.c -// cgplaceaddr: only the deref-rooted spine is wired (`(*p)[i].f` as -// N_UN(STAR) root, N_INDEX hop over a slice/array place, N_DOT -// struct-field hop with one deref for a *struct base). All type keys -// come off the checker-STAMPED tinfo (.type_), never tnode names — -// the #209/#211 discipline. Ident-rooted spines stay with the -// enumerated cgassign arms so this resolver never perturbs their -// asm. ADDRESS COMPUTATION ONLY — every call-site keeps its own -// load/store/copy emission. Clobbers AX/CX (cgexpr on index / -// pointer operands) and balances its own PUSHQ/POPQ; dstreg must -// not be AX or CX. +// cgplaceaddr: `(*p)[i].f` as N_UN(STAR) root, N_INDEX hop over a +// slice/array place, N_DOT struct-field hop with one deref for a +// *struct base. C2 (F4 read-walker) adds the N_IDENT root (local / +// let / DATA-backed def) so indexed-ident spines resolve too. All +// type keys come off the checker-STAMPED tinfo (.type_), never tnode +// names — the #209/#211 discipline. Enumerated arms still win at +// every dispatch site (they are checked first), so shapes that worked +// pre-C1 keep their asm. ADDRESS COMPUTATION ONLY — every call-site +// keeps its own load/store/copy emission. Clobbers AX/CX (cgexpr on +// index / pointer operands) and balances its own PUSHQ/POPQ; dstreg +// must not be AX or CX. fn cgplaceaddr(c: *cgen, n: *node, dstreg: str) bool = { if (n == nil) { return false; }; + if (n.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, n.str); + if (lc != nil) { + emitline("\tLEAQ\t"); + emitoff(lc.off: i64); + emitline("(BP), "); + emitline(dstreg); + emitline("\n"); + return true; + }; + let gok: bool = isletvar(c, n.str); + if (!gok) { + if (defvarstructinfo(c, n.str) != nil) { gok = true; }; + }; + if (!gok) { + let dtn: *node = defvartnode(c, n.str); + if (dtn != nil) { + if (dtn.kind == nkind.N_TARRAY) { gok = true; }; + }; + }; + if (gok) { + emitline("\tLEAQ\t"); + emitsymname(c, n.str); + emitline("(SB), "); + emitline(dstreg); + emitline("\n"); + return true; + }; + return false; + }; if (n.kind == nkind.N_UN) { if (n.op != tkind.TK_STAR) { return false; }; // &(*e) is e's value — no load. @@ -21406,11 +21437,10 @@ fn cgplaceaddr(c: *cgen, n: *node, dstreg: str) bool = { let base: *node = n.lhs; let idx: *node = n.rhs; if (base == nil || idx == nil) { return false; }; - // Deref base only: ident/dot index bases all have - // enumerated arms; routing them here would change - // their asm. - if (base.kind != nkind.N_UN) { return false; }; - if (base.op != tkind.TK_STAR) { return false; }; + // C2: any addressable base — recursion decides (deref / + // ident / dot / index spine). Ident-rooted shapes with + // enumerated arms never reach the resolver (those arms + // dispatch first), so their asm is untouched. let bu: *tinfo = base.type_: *tinfo; for (bu != nil && bu.kind == tykind.TY_NAMED) { bu = bu.under; }; if (bu == nil) { return false; }; @@ -23318,35 +23348,57 @@ fn cgdot(c: *cgen, n: *node) void = { // expression — that leaves (AX=ptr, BX=len). Then `.ptr` returns // AX as is; `.len` shuffles BX→AX. cstage cgen.c was aligned UP // to this shuffle in #14 (it had returned the ptr for `.len`). - if (streq(fld, "ptr")) { cgexpr(c, lhs); return; }; - if (streq(fld, "len")) { - cgexpr(c, lhs); - emitline("\tMOVQ\tBX, AX\n"); - return; - }; - // .cap on a non-ident base (indexed element `t[i].cap`, call, - // dot-slice): cgexpr leaves the full {ptr,len,cap} header via - // cgslicehdr — shuffle CX→AX. The shuffle fires ONLY for a TYPED - // slice/str base (kind TY_SLICE/TY_STR after NAMED-chase) that is NOT - // a bare string literal: N_STRLIT's cgexpr loads only AX=ptr/BX=len - // (cgen.ww), never a CX cap, so `"abc".cap` must return AX unshuffled. - // cstage reaches that outcome by typing N_STRLIT as untyped_str - // (cgen.c:8456 catch-all, no cap shuffle); the wwstage checker types - // N_STRLIT as `str` instead (check.ww:2322 vs cstage check.c:1079 — - // divergence filed separately), so the TY_STR kind-gate alone would - // wrongly fire. The N_STRLIT exclusion keeps this byte-identical with - // cstage. #13 read-fix, sibling of the #20 store. - if (streq(fld, "cap")) { - cgexpr(c, lhs); - if (lhs != nil && lhs.kind != nkind.N_STRLIT) { - let lu: *tinfo = lhs.type_: *tinfo; - for (lu != nil && lu.kind == tykind.TY_NAMED) { lu = lu.under; }; - if (lu != nil && (lu.kind == tykind.TY_SLICE - || lu.kind == tykind.TY_STR)) { - emitline("\tMOVQ\tCX, AX\n"); + // C2 (F4): gated to a slice/str/untyped-str STAMPED base (or an + // N_STRLIT, which ww types as `str` anyway) — pre-C2 this arm was + // shape-blind, so a STRUCT field that merely shares a pseudo-field + // NAME behind a non-ident spine took the offset-blind cgexpr path + // while cstage (type-gated) routes it to the read-resolver. + { + let pbu: *tinfo = nil; + if (lhs != nil) { pbu = lhs.type_: *tinfo; }; + for (pbu != nil && pbu.kind == tykind.TY_NAMED) { pbu = pbu.under; }; + let pbok: bool = false; + if (pbu != nil) { + if (pbu.kind == tykind.TY_SLICE + || pbu.kind == tykind.TY_STR + || pbu.kind == tykind.TY_UNTYPED_STR) { pbok = true; }; + }; + if (lhs != nil) { + if (lhs.kind == nkind.N_STRLIT) { pbok = true; }; + }; + if (pbok) { + if (streq(fld, "ptr")) { cgexpr(c, lhs); return; }; + if (streq(fld, "len")) { + cgexpr(c, lhs); + emitline("\tMOVQ\tBX, AX\n"); + return; + }; + // .cap on a non-ident base (indexed element `t[i].cap`, + // call, dot-slice): cgexpr leaves the full {ptr,len,cap} + // header via cgslicehdr — shuffle CX→AX. The shuffle + // fires ONLY for a TYPED slice/str base (kind TY_SLICE/ + // TY_STR after NAMED-chase) that is NOT a bare string + // literal: N_STRLIT's cgexpr loads only AX=ptr/BX=len + // (cgen.ww), never a CX cap, so `"abc".cap` must return + // AX unshuffled. cstage reaches that outcome by typing + // N_STRLIT as untyped_str (cgen.c catch-all, no cap + // shuffle); the wwstage checker types N_STRLIT as `str` + // instead (check.ww:2322 vs cstage check.c:1079 — + // divergence filed separately), so the TY_STR kind-gate + // alone would wrongly fire. The N_STRLIT exclusion keeps + // this byte-identical with cstage. #13 read-fix, sibling + // of the #20 store. + if (streq(fld, "cap")) { + cgexpr(c, lhs); + if (lhs != nil && lhs.kind != nkind.N_STRLIT) { + if (pbu != nil && (pbu.kind == tykind.TY_SLICE + || pbu.kind == tykind.TY_STR)) { + emitline("\tMOVQ\tCX, AX\n"); + }; + }; + return; }; }; - return; }; // Chained struct-field-via-ptr-via-ptr access: // r.sym.val where r: *lrel, .sym: *lsym, .val: u64 @@ -23564,16 +23616,126 @@ fn cgdot(c: *cgen, n: *node) void = { // driver concatenation — the inner enum / struct hasn't been // seen). Emit `MOVQ (SB), AX` so the linker surfaces a // clean undefined-symbol error on the leaf. Mirror of - // cmd/w6c/cgen.c N_DOT nested fallback. + // cmd/w6c/cgen.c N_DOT nested fallback. C2 (F4): gated to UNTYPED + // chains only — pre-C2 it swallowed every unmatched dot-over-dot + // chain, turning a TYPED depth-2 read behind an index/deref spine + // into a silent global read of a colliding leaf symbol. if (lhs != nil) { if (lhs.kind == nkind.N_DOT) { - emitline("\tMOVQ\t"); - emitsymname(c, fld); - emitline("(SB), AX\n"); - return; + let sbt: *tinfo = lhs.type_: *tinfo; + if (sbt == nil || sbt.kind == tykind.TY_ERR) { + emitline("\tMOVQ\t"); + emitsymname(c, fld); + emitline("(SB), AX\n"); + return; + }; }; }; - return; + // C2/C3 boundary: `xs[i].fld` with an IDENT index base is the + // enumerated N_INDEX-base arm's territory (cstage handles it + // type-keyed); reaching HERE means wwstage's name-keyed arm above + // missed (the F10 alias-tnode class). Routing it through the + // read-resolver would emit a different (albeit correct) sequence + // than cstage's arm → cs≠ww. Loud until C3 re-keys that arm onto + // stamped tinfo (task #8). + if (lhs != nil) { + if (lhs.kind == nkind.N_INDEX) { + if (lhs.lhs != nil) { + if (lhs.lhs.kind == nkind.N_IDENT) { + let mg: str = "cgdot: ident-indexed field read unwired in wwstage (C3/task #8)\n"; + os.write(2, mg.ptr, mg.len: u64); + os.exit(1); + }; + }; + }; + }; + // cstage reads ptr-chained fields (`a.p.f`, any root) in its + // chained-*struct arm; wwstage's #70 mirror above is gated + // root-local — the uncovered remainder (global / indexed roots) + // must not take the resolver (its sequence differs from cstage's + // arm → cs≠ww). Loud; the wwstage alignment is filed as task + // #37. + if (lhs != nil) { + if (lhs.kind == nkind.N_DOT) { + let pgu: *tinfo = lhs.type_: *tinfo; + for (pgu != nil && pgu.kind == tykind.TY_NAMED) { pgu = pgu.under; }; + if (pgu != nil) { + if (pgu.kind == tykind.TY_PTR) { + let mp: str = "cgdot: ptr-chained field read unwired in wwstage (task #37)\n"; + os.write(2, mp.ptr, mp.len: u64); + os.exit(1); + }; + }; + }; + }; + // C2 read-resolver (F4 + FA3): a TYPED N_DOT read no enumerated + // arm matched — depth-2+ chains and slice/str/scalar fields behind + // index/deref spines. Address via cgplaceaddr (the C1 resolver), + // leaf load emitted here by kind. Leaf kinds with no canonical + // register convention in expr position stay LOUD; any shape the + // resolver can't address dies LOUD (rule 7) — the pre-C2 tail + // silently emitted NOTHING. Mirror of cstage cgen.c case N_DOT + // read-resolver tail. + { + let rdt: *tinfo = n.type_: *tinfo; + let rdu: *tinfo = rdt; + for (rdu != nil && rdu.kind == tykind.TY_NAMED) { rdu = rdu.under; }; + if (rdu != nil) { + if (rdu.kind == tykind.TY_TAGGED) { + let mt: str = "read-resolver: tagged field read not wired (rule-7)\n"; + os.write(2, mt.ptr, mt.len: u64); + os.exit(1); + }; + // ww-asymmetric in LET position: cstage's let-init + // consumes `let c = (*ts)[i].cap` BEFORE its N_DOT + // tail and emits NO copy (the F5 bug) — cs-builds/ + // ww-louds on that shape until the F5 let-copy + // lands (task #7). Absent from the gate corpus. + if (rdu.kind == tykind.TY_STRUCT + || rdu.kind == tykind.TY_TUPLE) { + let ma: str = "read-resolver: aggregate field read not wired (rule-7)\n"; + os.write(2, ma.ptr, ma.len: u64); + os.exit(1); + }; + }; + if (!cgplaceaddr(c, n, "BX")) { + let mu: str = "unsupported field-read shape\n"; + os.write(2, mu.ptr, mu.len: u64); + os.exit(1); + }; + if (typeisfloat(rdt)) { + let mov: str = "MOVSD"; + if (typeisf32(rdt)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t(BX), X0\n"); + return; + }; + if (rdu != nil && rdu.kind == tykind.TY_ARRAY) { + // `[N]T` leaf: leave the field ADDRESS — a base for + // an outer index, never a value (#270-1a semantics). + emitline("\tMOVQ\tBX, AX\n"); + return; + }; + if (rdu != nil && (rdu.kind == tykind.TY_STR + || rdu.kind == tykind.TY_SLICE)) { + // str IS []u8 — 3-word {ptr,len,cap} into (AX, BX, + // CX). BX is the place base, so load .len (which + // targets BX) LAST. + emitline("\tMOVQ\t(BX), AX\n"); + emitline("\tMOVQ\t16(BX), CX\n"); + emitline("\tMOVQ\t8(BX), BX\n"); + return; + }; + let lop: str = "MOVQ"; + if (rdt != nil) { + lop = loadopsz(typeissigned(rdt), rdt.slotsize: i32); + }; + emitline("\t"); + emitline(lop); + emitline("\t(BX), AX\n"); + return; + }; }; fn cgun(c: *cgen, n: *node) void = { @@ -23882,9 +24044,21 @@ fn cgun(c: *cgen, n: *node) void = { }; }; }; - // Fall through silently (mirrors cstage silent- - // drop fallback at the end of the TK_AMP block). - return; + // C2 (F4 family, reviewer-A route): address-of + // through an indexed/deref dot spine — the + // arms above root only at idents. Route the + // place address through cgplaceaddr (read-twin + // in cgdot). Any remaining shape dies LOUD: + // the pre-C2 silent drop left stale AX as the + // "address" — a gate-blind SEGFAULT at the + // deref. Mirror of cstage TK_AMP tail. + if (cgplaceaddr(c, opnd, "BX")) { + emitline("\tMOVQ\tBX, AX\n"); + return; + }; + let mam: str = "unsupported address-of shape\n"; + os.write(2, mam.ptr, mam.len: u64); + os.exit(1); }; if (opnd.kind == nkind.N_INDEX) { // &base[i] = base + i*esz, no dereference. @@ -23987,6 +24161,17 @@ fn cgun(c: *cgen, n: *node) void = { emitline("\tADDQ\tBX, AX\n"); return; }; + // C2: deref-rooted (`&(*p)`) and other non-ident + // operands — resolver-or-loud, the same tail as the + // N_DOT arm above (cstage has ONE shared tail for + // both). + if (cgplaceaddr(c, opnd, "BX")) { + emitline("\tMOVQ\tBX, AX\n"); + return; + }; + let mam2: str = "unsupported address-of shape\n"; + os.write(2, mam2.ptr, mam2.len: u64); + os.exit(1); }; return; }; diff --git a/test/wcc/805_placeaddr_store.c b/test/wcc/805_placeaddr_store.c index 9efc6a6c..8d86ae8a 100644 --- a/test/wcc/805_placeaddr_store.c +++ b/test/wcc/805_placeaddr_store.c @@ -24,6 +24,21 @@ * diagnostics — the reject rows pin the exact text on BOTH stages. * Non-DOT lvalue tail residue is task #22. * + * C2 (F4 + FA3-cstage, task #6): the READ side. cgplaceaddr grew an + * N_IDENT root and recursion-driven N_INDEX bases; case N_DOT routes + * every TYPED read no enumerated arm matched through the resolver, and + * BOTH silent fallbacks died: the module-leaf `MOVQ (SB)` is + * gated to UNTYPED chains (pre-C2 it swallowed any unmatched dot chain + * — a silent global read of a colliding symbol), and the offset-blind + * cgexpr catch-all is gated to untyped-str pseudo-fields (pre-C2 a + * nonzero-offset field behind a deref-index spine read element word 0; + * offset-0 scalars worked by coincidence). TK_AMP's silent tail is the + * same resolver-or-loud (`&threads[0].cap` used to SEGFAULT on deref, + * reviewer-A route). The C1.25 raw-byte readbacks graduate to typed + * depth-2 reads below. wwstage cgdot/cgun mirror symmetrically; its + * name-keyed ident-indexed arm gap stays LOUD (C3/task #8 re-key), as + * does the ptr-chained global-root remainder (task #37). + * * row | shape | want * --------------------+----------------------------------------+------ * store_size_neighbor | (*ts)[1].pc = 9, elem 0 intact | 19 @@ -55,7 +70,22 @@ * reject_float | f64 field store | BUILD_FAIL * reject_tagged | tagged-union field store | BUILD_FAIL * reject_str_compound | (*ts)[i].name += — str/slice compound | BUILD_FAIL - * reject_tail | h.arr[1].pc (resolver-unwired N_DOT) | BUILD_FAIL + * arr_field_idx_store | h.arr[1].pc = / += (C1's reject_tail, | 58 + * | graduated by the C2 resolver arms) | + * read_depth2_idx | ts[i].cap.start/.end/.content.len — | 59 + * | typed depth-2 behind an IDENT index | + * read_depth2_deref | (*ts)[i].cap.start + offset-0 .pc | 60 + * read_widths_spine | i8..u32/f64/f32/[3]u8 behind (*p)[i] | 61 + * read_slicefield | let h = (*p)[i].xs (FA3/pA5) + .cap | 62 + * amp_spine | &ts[1].cap / &(*p)[0].cap — deref, | 63 + * | compound through the pointer | + * neutral_reads | ident-root chain, "abc".len, (*p).f — | 64 + * | untouched-arm asm-neutrality pins | + * reject_read_tagged | (*ts)[i].tg read behind spine | BUILD_FAIL + * reject_read_callbase| mk()[0].pc read (unaddressable base) | BUILD_FAIL + * reject_assign_callbs| mk()[0].pc = (assign-tail text keeps | BUILD_FAIL + * | its carrier post arr_field_idx_store) | + * reject_addrof_callbs| &mk()[0].pc (TK_AMP loud tail) | BUILD_FAIL * * BUILD_FAIL rows also assert the diagnostic TEXT (stderr substring, * both stages) — a build that fails for any other reason (parse error, @@ -383,14 +413,10 @@ static const struct row rows[] = { * through a FRESH per-use @placescr materialise + word-copy; the * `...` setter pins the autofill zero loop; neighbour element + * sibling field readbacks pin the stride and copy length. */ - /* Readback is RAW bytes over ts.ptr (the 804 tagged_56b - * precedent): a depth-2 read behind an index (ts[1].cap.start) - * is the F4 walker gap (task #6) and link-fails today on the - * cgen.c:9038 global-leaf fallback — typed readbacks graduate - * with the C2 read-walker. Layout: t={pc@0,cap@8}, size 48; - * capture={content(str ptr@0,len@8,cap@16),start@24,end@32}. - * Elem1 base 48 → content.len byte 64, start 80, end 88; - * elem0 → 16/32/40. Stored values fit one byte. */ + /* Readback was RAW bytes over ts.ptr until C2 — a depth-2 read + * behind an index (ts[1].cap.start) was the F4 walker gap (task + * #6) and link-failed on the cgen.c global-leaf fallback. C2 + * graduation: typed depth-2 readbacks (the documented note). */ { "agg_structlit_40b", "package main;\n" "type capture = struct { content: str, start: size, end: size };\n" @@ -407,24 +433,23 @@ static const struct row rows[] = { "\tappend(ts, t { pc = 7, cap = z });\n" "\tappend(ts, t { pc = 8, cap = z });\n" "\tsetcap(&ts, 1);\n" - "\tlet bp: *u8 = ts.ptr: *u8;\n" - "\tif (bp[64] != 3u8) { return 1; };\n" - "\tif (bp[80] != 2u8) { return 2; };\n" - "\tif (bp[88] != 4u8) { return 3; };\n" + "\tif (ts[1].cap.content.len != 3) { return 1; };\n" + "\tif (ts[1].cap.start != 2) { return 2; };\n" + "\tif (ts[1].cap.end != 4) { return 3; };\n" "\tif (ts[1].pc != 8) { return 4; };\n" - "\tif (bp[16] != 0u8) { return 5; };\n" + "\tif (ts[0].cap.content.len != 0) { return 5; };\n" "\tif (ts[0].pc != 7) { return 6; };\n" "\tclearcap(&ts, 1);\n" - "\tif (bp[64] != 1u8) { return 7; };\n" - "\tif (bp[80] != 0u8) { return 8; };\n" - "\tif (bp[88] != 0u8) { return 9; };\n" + "\tif (ts[1].cap.content.len != 1) { return 7; };\n" + "\tif (ts[1].cap.start != 0) { return 8; };\n" + "\tif (ts[1].cap.end != 0) { return 9; };\n" "\treturn 52;\n" "};\n", 52, NULL }, /* Addressable aggregate sources via the #265/#268 dispatch: - * local ident (LEAQ slot) and deref (*pc). Raw-byte readback per - * the agg_structlit_40b layout note (F4 blocks typed depth-2). */ + * local ident (LEAQ slot) and deref (*pc). Typed depth-2 + * readbacks since C2 (were raw bytes over ts.ptr). */ { "agg_from_ident_deref", "package main;\n" "type capture = struct { content: str, start: size, end: size };\n" @@ -442,16 +467,15 @@ static const struct row rows[] = { "\tappend(ts, t { pc = 8, cap = z });\n" "\tlet a: capture = capture { content = \"ab\", start = 1, end = 3 };\n" "\tcopycap(&ts, 1, a);\n" - "\tlet bp: *u8 = ts.ptr: *u8;\n" - "\tif (bp[64] != 2u8) { return 1; };\n" - "\tif (bp[80] != 1u8) { return 2; };\n" - "\tif (bp[88] != 3u8) { return 3; };\n" + "\tif (ts[1].cap.content.len != 2) { return 1; };\n" + "\tif (ts[1].cap.start != 1) { return 2; };\n" + "\tif (ts[1].cap.end != 3) { return 3; };\n" "\tlet b: capture = capture { content = \"wxyz\", start = 5, end = 9 };\n" "\tderefcap(&ts, 0, &b);\n" - "\tif (bp[16] != 4u8) { return 4; };\n" - "\tif (bp[32] != 5u8) { return 5; };\n" - "\tif (bp[40] != 9u8) { return 6; };\n" - "\tif (bp[80] != 1u8) { return 7; };\n" + "\tif (ts[0].cap.content.len != 4) { return 4; };\n" + "\tif (ts[0].cap.start != 5) { return 5; };\n" + "\tif (ts[0].cap.end != 9) { return 6; };\n" + "\tif (ts[1].cap.start != 1) { return 7; };\n" "\treturn 53;\n" "};\n", 53, NULL }, @@ -524,13 +548,12 @@ static const struct row rows[] = { "\tappend(ts, t { pc = 7, cap = z });\n" "\tappend(ts, t { pc = 8, cap = z });\n" "\tsettwo(&ts);\n" - "\tlet bp: *u8 = ts.ptr: *u8;\n" - "\tif (bp[16] != 2u8) { return 1; };\n" - "\tif (bp[32] != 1u8) { return 2; };\n" - "\tif (bp[40] != 2u8) { return 3; };\n" - "\tif (bp[64] != 3u8) { return 4; };\n" - "\tif (bp[80] != 3u8) { return 5; };\n" - "\tif (bp[88] != 4u8) { return 6; };\n" + "\tif (ts[0].cap.content.len != 2) { return 1; };\n" + "\tif (ts[0].cap.start != 1) { return 2; };\n" + "\tif (ts[0].cap.end != 2) { return 3; };\n" + "\tif (ts[1].cap.content.len != 3) { return 4; };\n" + "\tif (ts[1].cap.start != 3) { return 5; };\n" + "\tif (ts[1].cap.end != 4) { return 6; };\n" "\treturn 56;\n" "};\n", 56, NULL }, @@ -655,19 +678,239 @@ static const struct row rows[] = { BUILD_FAIL, "assign-resolver: compound on str/slice field not wired (rule-7)" }, - /* An N_DOT lvalue the C1 resolver does not wire (index base is a - * dot chain, not a deref) — pre-C1 this was a SILENT no-op store; - * the loud dispatch tail is the close-by-construction net. */ - { "reject_tail", + /* C1's reject_tail (index base is a dot chain), graduated by the + * C2 resolver arms (N_IDENT root + recursive N_INDEX bases) — + * store, compound, readback, and neighbour-intact pins. */ + { "arr_field_idx_store", "package main;\n" "type t = struct { pc: size, matched: bool };\n" "type holder = struct { arr: [3]t, n: i64 };\n" "export fn main() i32 = {\n" "\tlet h: holder = holder { n = 0, ... };\n" "\th.arr[1].pc = 5;\n" + "\th.arr[1].pc += 2;\n" + "\tif (h.arr[1].pc != 7) { return 1; };\n" + "\tif (h.arr[0].pc != 0) { return 2; };\n" + "\tif (h.arr[2].pc != 0) { return 3; };\n" + "\treturn 58;\n" + "};\n", + 58, NULL }, + + /* C2 (F4): typed depth-2 reads behind an IDENT-indexed root — + * the p6min9/p6min10 read shape (nonzero offsets, both elements, + * pseudo .len behind the spine, and the global-collision check: + * a module global named `end` must NOT be read). Elements are + * built with NESTED literals, not `cap = rc1` ident rhs — the + * probes' own construction shape is the #36 under-copy (struct- + * ident field rhs copies 8B), which keeps p6min9/p6min10 + * themselves blocked on #36 with their read-half fixed here. */ + { "read_depth2_idx", + "package main;\n" + "type capture = struct { content: str, start: size, end: size };\n" + "type t = struct { pc: size, cap: capture };\n" + "let end: size = 777;\n" + "export fn main() i32 = {\n" + "\tlet ts: []t = [];\n" + "\tappend(ts, t { pc = 5, cap = capture { content = \"abc\", start = 11, end = 13 } });\n" + "\tappend(ts, t { pc = 6, cap = capture { content = \"wy\", start = 21, end = 23 } });\n" + "\tif (ts[0].cap.end == 777) { return 66; };\n" + "\tif (ts[0].cap.end != 13) { return 1; };\n" + "\tif (ts[0].cap.start != 11) { return 2; };\n" + "\tif (ts[1].cap.end != 23) { return 3; };\n" + "\tif (ts[1].cap.content.len != 2) { return 4; };\n" + "\tif (ts[1].pc != 6) { return 5; };\n" + "\treturn 59;\n" + "};\n", + 59, NULL }, + + /* C2 (F4): the same chain behind a DEREF-rooted index — plus the + * offset-0 scalar (.pc) that pre-C2 cstage read correctly only by + * COINCIDENCE through the offset-blind catch-all (ken's FA3 + * correction); now it routes through the resolver in both + * stages. */ + { "read_depth2_deref", + "package main;\n" + "type capture = struct { content: str, start: size, end: size };\n" + "type t = struct { pc: size, cap: capture };\n" + "fn rd(ts: *[]t) i32 = {\n" + "\tif ((*ts)[0].pc != 5) { return 1; };\n" + "\tif ((*ts)[0].cap.start != 11) { return 2; };\n" + "\tif ((*ts)[0].cap.end != 13) { return 3; };\n" + "\tif ((*ts)[1].cap.end != 23) { return 4; };\n" + "\treturn 0;\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet ts: []t = [];\n" + "\tappend(ts, t { pc = 5, cap = capture { content = \"abc\", start = 11, end = 13 } });\n" + "\tappend(ts, t { pc = 6, cap = capture { content = \"wy\", start = 21, end = 23 } });\n" + "\tlet rc: i32 = rd(&ts);\n" + "\tif (rc != 0) { return rc; };\n" + "\treturn 60;\n" + "};\n", + 60, NULL }, + + /* C2 (F4): leaf-kind matrix behind the deref-index spine — narrow + * signed/unsigned (fldloadop sign/zero extension), f64/f32 via + * X0, and an `[N]u8` field leaf (address semantics, outer index + * applies). */ + { "read_widths_spine", + "package main;\n" + "type inner = struct { b: i8, ub: u8, w: i16, uw: u16, d: i32, ud: u32, f: f64, g: f32, arr: [3]u8 };\n" + "type box = struct { pad: size, it: inner };\n" + "export fn main() i32 = {\n" + "\tlet bs: []box = [];\n" + "\tappend(bs, box { pad = 1, it = inner { b = -5i8, ub = 200u8, w = -300i16, uw = 60000u16, d = -70000, ud = 4000000000u32, f = 1.5, g = 2.5f32, arr = [7u8, 8u8, 9u8] } });\n" + "\tlet p: *[]box = &bs;\n" + "\tif ((*p)[0].it.b != -5i8) { return 1; };\n" + "\tif ((*p)[0].it.ub != 200u8) { return 2; };\n" + "\tif ((*p)[0].it.w != -300i16) { return 3; };\n" + "\tif ((*p)[0].it.uw != 60000u16) { return 4; };\n" + "\tif ((*p)[0].it.d != -70000) { return 5; };\n" + "\tif ((*p)[0].it.ud != 4000000000u32) { return 6; };\n" + "\tif ((*p)[0].it.f != 1.5) { return 7; };\n" + "\tif ((*p)[0].it.g != 2.5f32) { return 8; };\n" + "\tif ((*p)[0].it.arr[1] != 8u8) { return 9; };\n" + "\treturn 61;\n" + "};\n", + 61, NULL }, + + /* FA3/pA5 graduated: 24B slice-header field read behind a deref- + * index spine — pre-C2 cstage took the offset-blind catch-all + * (one wrong-offset MOVQ + stale BX/CX as the header), wwstage + * silently emitted NOTHING. Plus the .cap pseudo behind the same + * spine (recurses through the resolver). */ + { "read_slicefield", + "package main;\n" + "type box = struct { pc: size, xs: []size };\n" + "fn grab(p: *[]box, i: size) i32 = {\n" + "\tlet h: []size = (*p)[i].xs;\n" + "\tif (len(h) != 2) { return 1; };\n" + "\tif (h[0] != (7: size)) { return 2; };\n" + "\tif (h[1] != (8: size)) { return 3; };\n" + "\tif ((*p)[i].xs.cap < 2) { return 4; };\n" + "\treturn 0;\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet src: []size = [];\n" + "\tappend(src, (7: size));\n" + "\tappend(src, (8: size));\n" + "\tlet bs: []box = [];\n" + "\tappend(bs, box { pc = 1, xs = src });\n" + "\tlet rc: i32 = grab(&bs, 0);\n" + "\tif (rc != 0) { return rc; };\n" + "\treturn 62;\n" + "};\n", + 62, NULL }, + + /* Reviewer-A route: address-of a field through an indexed / + * deref-indexed spine, then deref + compound through the pointer. + * Pre-C2 BOTH stages silently dropped the & (stale AX as the + * address — gate-blind SEGFAULT). */ + { "amp_spine", + "package main;\n" + "type capture = struct { content: str, start: size, end: size };\n" + "type t = struct { pc: size, cap: capture };\n" + "fn bump(cp: *capture) void = { cp.end += 100; };\n" + "export fn main() i32 = {\n" + "\tlet ts: []t = [];\n" + "\tappend(ts, t { pc = 5, cap = capture { content = \"abc\", start = 11, end = 13 } });\n" + "\tappend(ts, t { pc = 6, cap = capture { content = \"wy\", start = 21, end = 23 } });\n" + "\tlet cp: *capture = &ts[1].cap;\n" + "\tif (cp.start != 21) { return 1; };\n" + "\tbump(cp);\n" + "\tif (ts[1].cap.end != 123) { return 2; };\n" + "\tlet pp: *[]t = &ts;\n" + "\tlet dp: *capture = &(*pp)[0].cap;\n" + "\tif (dp.end != 13) { return 3; };\n" + "\treturn 63;\n" + "};\n", + 63, NULL }, + + /* Asm-neutrality pins for the arms the C2 gates must NOT disturb: + * ident-rooted value chain (chain walker), `(*p).f` retarget, + * untyped-str literal pseudo-fields (the gated catch-all's only + * legitimate tenant). A tuple-positional pin is blocked by the + * pre-existing #33 wwstage tuple-let word-2 drop (filed). */ + { "neutral_reads", + "package main;\n" + "type capture = struct { content: str, start: size, end: size };\n" + "type t = struct { pc: size, cap: capture };\n" + "fn pcof(p: *t) size = { return (*p).pc; };\n" + "export fn main() i32 = {\n" + "\tlet x: t = t { pc = 9, cap = capture { content = \"hi\", start = 1, end = 2 } };\n" + "\tif (x.cap.start != 1) { return 1; };\n" + "\tif (x.cap.content.len != 2) { return 2; };\n" + "\tif (pcof(&x) != 9) { return 3; };\n" + "\tif (\"abc\".len != 3) { return 4; };\n" + "\treturn 64;\n" + "};\n", + 64, NULL }, + + /* Tagged leaf behind a spine: no canonical register convention + * is emitted by the read-resolver yet — must die LOUD, not load + * 8 of 16+ bytes (rule 7). */ + { "reject_read_tagged", + "package main;\n" + "type v = (i64 | bool);\n" + "type t = struct { pc: size, tg: v };\n" + "fn rd(ts: *[]t, i: size) i64 = {\n" + "\tlet x: v = (*ts)[i].tg;\n" + "\treturn 1;\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet ts: []t = [];\n" + "\treturn rd(&ts, 0): i32;\n" + "};\n", + BUILD_FAIL, "read-resolver: tagged field read not wired (rule-7)" }, + + /* A call-result index base has no storage address — the resolver + * must refuse and the read tail must be LOUD (pre-C2: cstage + * emitted an offset-blind garbage read, wwstage nothing). */ + { "reject_read_callbase", + "package main;\n" + "type t = struct { pc: size, matched: bool };\n" + "fn mk() []t = {\n" + "\tlet ts: []t = [];\n" + "\tappend(ts, t { pc = 3, matched = true });\n" + "\treturn ts;\n" + "};\n" + "export fn main() i32 = {\n" + "\tif (mk()[0].pc != 3) { return 1; };\n" + "\treturn 0;\n" + "};\n", + BUILD_FAIL, "unsupported field-read shape" }, + + /* The assign-tail diagnostic keeps a carrier now that C1's + * reject_tail shape (h.arr[1].pc) graduated: a call-base lvalue + * is unaddressable. */ + { "reject_assign_callbase", + "package main;\n" + "type t = struct { pc: size, matched: bool };\n" + "fn mk() []t = {\n" + "\tlet ts: []t = [];\n" + "\tappend(ts, t { pc = 3, matched = true });\n" + "\treturn ts;\n" + "};\n" + "export fn main() i32 = {\n" + "\tmk()[0].pc = 2;\n" "\treturn 0;\n" "};\n", BUILD_FAIL, "unsupported assign target shape" }, + + /* TK_AMP loud tail (the C2 replacement for the silent drop). */ + { "reject_addrof_callbase", + "package main;\n" + "type t = struct { pc: size, matched: bool };\n" + "fn mk() []t = {\n" + "\tlet ts: []t = [];\n" + "\tappend(ts, t { pc = 3, matched = true });\n" + "\treturn ts;\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet p: *size = &mk()[0].pc;\n" + "\treturn (*p): i32;\n" + "};\n", + BUILD_FAIL, "unsupported address-of shape" }, }; /* errlog_has — the build-failure stderr must carry the row's expected diff --git a/test/wcc/806_append_place.c b/test/wcc/806_append_place.c index 40c29e44..6c8e88bf 100644 --- a/test/wcc/806_append_place.c +++ b/test/wcc/806_append_place.c @@ -42,8 +42,8 @@ * | — nested indirect append inside the | * | outer's value expr; pins the per-SITE | * | @apphdrscr slot (a shared slot clobbers)| - * reject_identroot_dot | h.xs via *holder param (resolver ident | BUILD_FAIL - * | root unwired — was silent corruption) | + * identroot_dot | h.xs via *holder param — C1.5's reject, | 73 + * | graduated by C2's resolver ident root | * reject_spread_src | non-ident spread SOURCE through a deref | BUILD_FAIL * | target (the #35 designed boundary) | * @@ -372,20 +372,28 @@ static const struct row rows[] = { "};\n", 33, NULL }, - /* Ident-rooted dot target (h.xs through *holder): the resolver - * does not wire ident roots (they belong to the enumerated arms; - * none exists for append yet) — pre-fix this shape silently - * corrupted the frame in cstage; now it must die LOUD. */ - { "reject_identroot_dot", + /* Ident-rooted dot target (h.xs through *holder): C1.5's loud + * boundary (the resolver had no ident root then; pre-FA1 this + * shape silently corrupted the frame in cstage), graduated by + * C2's cgplaceaddr N_IDENT root — the *holder base derefs once + * inside the N_DOT hop and the header place lands on &h.xs. + * Readbacks use the .len pseudo, not len(): len() of a non-tuple + * N_DOT is the pre-existing F2 enumeration gap (task #10). */ + { "identroot_dot", "package main;\n" "type holder = struct { tag: i64, xs: []i64 };\n" "fn addfield(h: *holder, v: i64) void = { append(h.xs, v); };\n" "export fn main() i32 = {\n" "\tlet hl: holder = holder { tag = 2, xs = [] };\n" "\taddfield(&hl, 9);\n" - "\treturn 0;\n" + "\taddfield(&hl, 11);\n" + "\tif (hl.xs[0] != 9) { return 1; };\n" + "\tif (hl.xs[1] != 11) { return 2; };\n" + "\tif (hl.xs.len != 2) { return 3; };\n" + "\tif (hl.tag != 2) { return 4; };\n" + "\treturn 73;\n" "};\n", - BUILD_FAIL, "#15: append() target place unsupported (rule-7)" }, + 73, NULL }, /* The FA4 designed boundary (task #35, old #37): a spread SOURCE that is * not an ident local must stay loud even now that the deref