wcc+w6c_ww: typed dot-read resolver — kill both silent N_DOT fallbacks (F4)

A typed depth-2+ field chain behind an index/deref spine
(threads[0].cap.end, (*p)[i].f.g) aborted the cgdot chain walker
(N_IDENT roots only) and fell into the module-qualified-leaf fallback
— a silent `MOVQ <leaf>(SB)` global read of a colliding symbol
(p6min10 exit 66) or a link error. Single-dot reads behind a deref-
index ((*ts)[i].pc, (*p)[i].slicefield) fell into the bottom catch-all
instead, which is offset- and header-blind: offset-0 scalars read
correctly by COINCIDENCE, nonzero offsets and slice headers were
silently wrong in BOTH stages (FA3, pA5). `&threads[0].cap` dropped
the address-of silently and SEGFAULTed at the deref (task #6,
reviewer-A route).

cgplaceaddr (C1) grows an N_IDENT root (local / let / DATA-backed
def) and the N_INDEX base gate relaxes to recursion, so indexed-ident
and deref-rooted spines resolve; enumerated arms still dispatch first,
keeping every pre-C1 shape's asm. case N_DOT routes any TYPED read no
arm matched through the resolver (scalar fldloadop, float X0, str/
slice 3-word header, [N]T address); the module-leaf fallback is gated
to UNTYPED chains, the catch-all to untyped-str pseudo-fields, and
the TK_AMP tail is resolver-or-loud. Leaf kinds without a register
convention (tagged, aggregate) and unaddressable shapes die LOUD
(rule 7). wwstage mirrors symmetrically; two of its arm gaps must not
take the resolver (its sequence differs from cstage's arms — cs!=ww):
ident-indexed alias reads loud-cite C3 (task #8) and non-local-rooted
ptr-chains loud-cite task #37. A third verdict divergence is comment-
documented at the wwstage aggregate gate: cstage's let-init consumes
`let c = (*ts)[i].cap` BEFORE its N_DOT tail (emitting NO copy — the
F5 bug), so that shape cs-builds/ww-louds until the F5 let-copy lands
(task #7); absent from the gate corpus.

806 identroot_dot graduates from BUILD_FAIL: the C2 ident root makes
append(h.xs, v) through *holder resolve via C1.5's place consumer
(run-verified, byte-id). p6min9/p6min10 read-halves are fixed but the
probes stay blocked on the #36 literal under-copy this commit
unmasked (struct-ident field rhs copies 8B; repro filed with the
task).

test/805: +7 rows (typed depth-2 behind ident-index incl the 777
global-collision pin, deref-index, width/float/[N]u8 matrix, FA3
slice-field + .cap-behind-spine, &-route with compound-through-
pointer, C1's reject_tail graduated to stores, neutrality pins) and
+4 reject rows pinning the new loud texts; the C1.25 raw-byte
readbacks graduate to typed depth-2 reads.
This commit is contained in:
2026-06-04 10:51:56 +09:00
parent b630a7cf20
commit 76994a8279
6 changed files with 1112 additions and 220 deletions

View File

@@ -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;
}