wcc+w6c_ww: cgplaceaddr resolver — deref-base assign stores (F6)

(*ts)[i].field = v / OP= v (the regex run_thread hot shape, task #4)
compiled to NOTHING in both stages, byte-identically: the N_DOT lhs
roots at N_UN(STAR), so the arr[i].field arm (idxbase must be IDENT)
and the chained-ptr-field arm (base must be *struct) both miss and
the N_ASSIGN dispatch fell off the switch silently, rhs unevaluated.

cgplaceaddr (one per stage) is ADDRESS COMPUTATION ONLY — N_UN(STAR)
root, N_INDEX hop over a slice/array place (.ptr hop for slice),
N_DOT struct-field hop with one deref for a *struct base. Call-sites
keep their own emission: scalar fldstoreop store, str/slice 3-word
header store staged through DX, 10-op compound template with the
chained-ptr-field register roles. Ident-rooted spines stay with the
enumerated arms — verified asm-neutral over the 84 fold2b probe
sources against fresh master-HEAD binaries (7 diffs = the F6 family
now emitting stores; 2 verdict flips = aggregate-field stores, now
loud).

Silent dispatch tails go LOUD for N_DOT lvalues the resolver can't
address and for unresolved-identifier targets (cstage float-ident arm
aligned to wwstage's resolve-first order). Aggregate-field stores
loud-reject pending the follow-up resolver commit (task #23, ≤24B
N_CALL rhs split to #24). The non-DOT tail stays silent deliberately:
going loud there would asymmetrically surface the pre-existing
str-base element-store divergence — task #22, cited at both sites.

test/805: 17 rows x 2 drivers + 12 cs==ww byte-id fixtures — widths
(incl narrow-compound fldloadop sign/zero-extension), all 10 compound
ops (DIVQ/IDIVQ/SHLQ/SARQ/SHRQ), str + slice 3-word stores, *[N]T
base, runtime call index, ident-base neutrality pins, and 5 reject
rows asserting exact diagnostic text.
This commit is contained in:
2026-06-04 09:09:56 +09:00
parent c801aa7954
commit e3e6b5a820
6 changed files with 1679 additions and 10 deletions

View File

@@ -1989,6 +1989,88 @@ aggarg_srcaddr(Cg *c, Node *src, int dst, Local *locals)
return 0;
}
/* 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
* 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. */
static int
cgplaceaddr(Cg *c, Node *n, int dst_reg, Local *locals)
{
if (n == NULL) return 0;
if (n->kind == N_UN && n->op == TK_STAR) {
/* &(*e) is e's value — no load. */
cgexpr(c, n->lhs, locals);
ins2(c, A_MOVQ, areg(D_AX), areg(dst_reg));
return 1;
}
if (n->kind == N_INDEX) {
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;
Type *bu = type_chase_named(base->type);
if (bu == NULL) return 0;
if (bu->kind != TY_SLICE && bu->kind != TY_ARRAY)
return 0;
Type *et = type_chase_named(n->type);
if (et == NULL) return 0;
int esz = (int)et->size;
cgexpr(c, idx, locals);
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_CX));
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
}
ins1(c, A_PUSHQ, areg(D_AX));
if (!cgplaceaddr(c, base, dst_reg, locals)) return 0;
/* A slice place holds the {ptr,len,cap} header — the
* element base is its .ptr word; an array place IS the
* element storage. */
if (bu->kind == TY_SLICE)
ins2(c, A_MOVQ, amem(dst_reg, 0), areg(dst_reg));
ins1(c, A_POPQ, areg(D_AX));
ins2(c, A_ADDQ, areg(D_AX), areg(dst_reg));
return 1;
}
if (n->kind == N_DOT) {
Node *base = n->lhs;
if (base == NULL) return 0;
Type *bu = type_chase_named(base->type);
if (bu == NULL) return 0;
int viaptr = 0;
Type *st = NULL;
if (bu->kind == TY_PTR) {
Type *p = type_chase_named(bu->sub);
if (p && p->kind == TY_STRUCT) { st = p; viaptr = 1; }
} else if (bu->kind == TY_STRUCT) {
st = bu;
}
if (st == NULL) return 0;
Tfield *f = NULL;
for (Tfield *fl = st->fields; fl; fl = fl->next)
if (strcmp(fl->name, n->str) == 0) { f = fl; break; }
if (f == NULL) return 0;
if (!cgplaceaddr(c, base, dst_reg, locals)) return 0;
if (viaptr)
ins2(c, A_MOVQ, amem(dst_reg, 0), areg(dst_reg));
if ((int)f->offset != 0)
ins2(c, A_ADDQ, aimm((int)f->offset), areg(dst_reg));
return 1;
}
return 0;
}
/* cg_structlit_fill modes — see helper docstring. */
enum {
DST_BP = 0,
@@ -5167,7 +5249,14 @@ cgexpr(Cg *c, Node *n, Local *locals)
int divop = op_for(n, A_DIVSD, A_DIVSS);
int off = localfind(locals, n->lhs->str);
int isglobal = (off == 0) && let_islet(n->lhs->str);
if (off == 0 && !isglobal) break;
/* Loud twin of the IDENT-tail unresolved-name stop
* below (C1): wwstage resolves the name BEFORE its
* float dispatch, so a silent break here would make
* the stages disagree on the build verdict. */
if (off == 0 && !isglobal)
fatal("unsupported assign target: "
"unresolved identifier '%s'",
n->lhs->str);
if (n->op == TK_ASSIGN) {
if (off != 0) {
ins2(c, mvop, areg(D_X0), amem(D_BP, off));
@@ -5946,16 +6035,157 @@ cgexpr(Cg *c, Node *n, Local *locals)
}
}
}
/* F6 (cgplaceaddr, commit C1): an N_DOT lvalue none of the
* enumerated arms above matched — today the deref-rooted
* spine `(*p)[i].f = v` / `OP= v`. Base-address derivation
* routes through cgplaceaddr; the load/store emission stays
* here. Any N_DOT shape the resolver can't address dies
* LOUD below: the pre-C1 dispatch tail silently emitted
* NOTHING (rhs unevaluated) for every such shape. */
if (n->lhs && n->lhs->kind == N_DOT) {
Type *ft = n->lhs->type;
Type *fu = type_chase_named(ft);
int fsz = (int)(ft ? ft->size : 8);
int pa_isf32 = 0;
if (fld_isfloat(ft, &pa_isf32))
fatal("assign-resolver: float field not "
"wired (rule-7)");
if (fu && fu->kind == TY_TAGGED)
fatal("assign-resolver: tagged field not "
"wired (rule-7)");
if (fu && (fu->kind == TY_STRUCT
|| fu->kind == TY_ARRAY
|| fu->kind == TY_TUPLE))
fatal("assign-resolver: aggregate field not "
"wired (rule-7)");
if (fu && (fu->kind == TY_STR
|| fu->kind == TY_SLICE)) {
if (n->op != TK_ASSIGN)
fatal("assign-resolver: compound on "
"str/slice field not wired "
"(rule-7)");
/* str IS []u8: store the whole {ptr,len,cap}
* triple from (AX,BX,CX); the place address
* goes in DX so the three pops survive
* (#1/Phase 3). */
cgexpr(c, n->rhs, locals);
ins1(c, A_PUSHQ, areg(D_CX));
ins1(c, A_PUSHQ, areg(D_BX));
ins1(c, A_PUSHQ, areg(D_AX));
if (cgplaceaddr(c, n->lhs, D_DX, locals)) {
ins1(c, A_POPQ, areg(D_AX));
ins1(c, A_POPQ, areg(D_BX));
ins1(c, A_POPQ, areg(D_CX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_DX, 0));
ins2(c, A_MOVQ, areg(D_BX),
amem(D_DX, 8));
ins2(c, A_MOVQ, areg(D_CX),
amem(D_DX, 16));
break;
}
} else if (n->op == TK_ASSIGN) {
cgexpr(c, n->rhs, locals);
ins1(c, A_PUSHQ, areg(D_AX));
if (cgplaceaddr(c, n->lhs, D_BX, locals)) {
ins1(c, A_POPQ, areg(D_AX));
ins2(c, fldstoreop(ft, fsz),
areg(D_AX), amem(D_BX, 0));
break;
}
} else {
/* Compound: AX=old, CX=rhs, BX=addr — the
* same register roles as the chained-ptr-
* field compound template above. */
cgexpr(c, n->rhs, locals);
ins1(c, A_PUSHQ, areg(D_AX));
if (cgplaceaddr(c, n->lhs, D_BX, locals)) {
ins2(c, fldloadop(ft, fsz),
amem(D_BX, 0), areg(D_AX));
ins1(c, A_POPQ, areg(D_CX));
int unsignd = type_isunsigned(ft);
switch (n->op) {
case TK_PLUSEQ:
ins2(c, A_ADDQ, areg(D_CX),
areg(D_AX));
break;
case TK_MINUSEQ:
ins2(c, A_SUBQ, areg(D_CX),
areg(D_AX));
break;
case TK_STAREQ:
ins2(c, A_IMULQ, areg(D_CX),
areg(D_AX));
break;
case TK_AMPEQ:
ins2(c, A_ANDQ, areg(D_CX),
areg(D_AX));
break;
case TK_PIPEEQ:
ins2(c, A_ORQ, areg(D_CX),
areg(D_AX));
break;
case TK_CARETEQ:
ins2(c, A_XORQ, areg(D_CX),
areg(D_AX));
break;
case TK_SLASHEQ:
if (unsignd)
ins2(c, A_MOVQ,
aimm(0),
areg(D_DX));
else
ins0(c, A_CQO);
ins1(c, unsignd ? A_DIVQ
: A_IDIVQ, areg(D_CX));
break;
case TK_PERCENTEQ:
if (unsignd)
ins2(c, A_MOVQ,
aimm(0),
areg(D_DX));
else
ins0(c, A_CQO);
ins1(c, unsignd ? A_DIVQ
: A_IDIVQ, areg(D_CX));
ins2(c, A_MOVQ, areg(D_DX),
areg(D_AX));
break;
case TK_LSHIFTEQ:
ins2(c, A_SHLQ, areg(D_CX),
areg(D_AX));
break;
case TK_RSHIFTEQ:
ins2(c, unsignd ? A_SHRQ
: A_SARQ, areg(D_CX),
areg(D_AX));
break;
default:
fatal("assign-resolver: "
"unknown compound op "
"(rule-7)");
}
ins2(c, fldstoreop(ft, fsz),
areg(D_AX), amem(D_BX, 0));
break;
}
}
fatal("unsupported assign target shape");
}
if (n->lhs->kind == N_IDENT) {
int off = localfind(locals, n->lhs->str);
if (off == 0) {
/* Top-level let target — RIP-relative store
* (or load→combine→store for compound). Names
* we don't recognise as scalar lets fall through
* to the existing drop behaviour, which produces
* a clean link-time undefined-symbol error if
* the binding was ever supposed to exist. */
if (!let_islet(n->lhs->str)) break;
* (or load→combine→store for compound). A
* name that is neither a local nor a let
* dies LOUD: the pre-C1 break dropped the
* whole statement silently (no symbol was
* ever referenced, so not even a link error
* surfaced). */
if (!let_islet(n->lhs->str))
fatal("unsupported assign target: "
"unresolved identifier '%s'",
n->lhs->str);
cgexpr(c, n->rhs, locals);
if (n->op == TK_ASSIGN) {
ins2(c, A_MOVQ, areg(D_AX),
@@ -6113,6 +6343,13 @@ cgexpr(Cg *c, Node *n, Local *locals)
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off));
skip_assign_store: ;
}
/* C1 residual (task #22): a non-DOT lvalue no arm above
* matched still falls out SILENT here — the known member is
* the str-base element store family (`s[i] = v`: cstage
* drops, wwstage emits MOVB; pre-existing gate-blind
* divergence) plus tuple-member writes. The tail goes loud
* for the remaining kinds with #22, after the family gets a
* symmetric verdict. */
break;
}
case N_CALL: {