wcc+w6c_ww: append through pointer-to-slice place via cgplaceaddr (FA1)

Re-key the append() lowering from BP-displacement assumptions onto a
resolver-provided header PLACE (task #15, the add_thread hard-blocker;
cgplaceaddr's third consumer after C1/C1.25). One mirrored choke-point,
two failure modes: cstage 0-defaulted sn_off for any non-ident target,
so 0(BP)/8(BP) became the "slice header" and rt_ensure corrupted the
CALLER frame (SIGSEGV); wwstage cgappend silently emitted nothing
(gate-blind cs!=ww).

cg_append_grow/cg_append_slot (mirror cgappendgrow/cgappendslot) factor
the 5 grow + 5 slot header-access sites. Ident-local targets keep the
legacy BP-disp emission byte-identical (probed across all 9 existing
source shapes, before/after .s). Non-ident targets resolve once through
cgplaceaddr and spill the header address to an @apphdrscr slot:
rt_ensure may realloc .ptr but never moves the header, so the slot
stays valid; every access reloads from it. The slot is allocated fresh
per append SITE, not cached per fn: a nested append-through-pointer
inside a value expression (match-yield arm) spills its own resolve, and
a shared slot would hand the outer grow/slot reloads the inner target's
header — silent cross-slice corruption (pinned by the reentrant_value
row). Indirect mode keys esz/element-kind/load-op off the
checker-stamped target tinfo (no declared tnode behind `*p`; the
#209/#211 discipline). Unwired target places die LOUD "#15: append()
target place unsupported (rule-7)" on BOTH stages — the
silent-corruption class is closed by construction.

The FA4/#35 boundary is unchanged: non-ident spread SOURCES stay loud
(pinned by a reject row). Surfaced pre-existing checker divergence
filed as task #34 (wwstage rejects global slice-lit let).

test/wcc/806: 14 runtime rows (element kinds x target shapes, spread,
narrow-signed spread load, cap-crossing realloc loop with branched
callee + caller-frame sentinels, deref-spine target, nested-append
reentrancy, direct-arm neutrality pin) + 2 exact-text reject rows,
both drivers + per-row cs==ww asm byte-id.
This commit is contained in:
2026-06-04 10:25:56 +09:00
parent 48df04a8ca
commit b630a7cf20
6 changed files with 1186 additions and 463 deletions

View File

@@ -102,6 +102,16 @@ static int cg_ntagscr;
* Cached per name per fn to mirror wwstage's localadd `@`-prefix
* dedup, else two struct appends in one fn diverge the frame. */
static int cg_appendscr;
/* FA1 (#15) @apphdrscr — 8B spill of the resolver-derived slice-header
* ADDRESS for append() through a non-ident-local target (`append(*p,
* v)`). rt_ensure may realloc .ptr but never moves the header, so the
* spilled address stays valid across the call; every access reloads
* from the slot because registers don't survive it. Allocated fresh
* per append SITE (no per-fn cache, no decl here): a nested
* append-through-pointer inside a value expression (match-yield arm)
* spills its own resolve, and a shared slot would feed the outer
* grow/slot reloads the INNER target's header — silent cross-slice
* corruption (806 reentrant_value row). */
/* System V AMD64 sret discipline (task #23). Plain TY_STRUCT returns
* with size > 24B are passed via a hidden first-arg pointer (RDI) to
* a caller-prealloc dest; the callee writes through that pointer and
@@ -2088,6 +2098,50 @@ cgplaceaddr(Cg *c, Node *n, int dst_reg, Local *locals)
return 0;
}
/* FA1 (#15): append() header-place access, cgplaceaddr's append
* consumer. direct = ident-local header in the frame (BP-disp — the
* legacy emission, kept byte-identical); indirect = header address
* pre-spilled to @apphdrscr by the resolver. grow = len+=1, &hdr→DI,
* esz→SI, CALL rt_ensure. In indirect mode the len bump goes through
* DI so the loaded address doubles as the call argument. */
static void
cg_append_grow(Cg *c, int direct, int off, int scr, int esz)
{
if (direct) {
ins2(c, A_ADDQ, aimm(1), amem(D_BP, off + 8));
ins2(c, A_LEAQ, amem(D_BP, off), areg(D_DI));
} else {
ins2(c, A_MOVQ, amem(D_BP, scr), areg(D_DI));
ins2(c, A_ADDQ, aimm(1), amem(D_DI, 8));
}
ins2(c, A_MOVQ, aimm(esz), areg(D_SI));
ins1(c, A_CALL, masym(c, "rt_ensure"));
}
/* Post-rt_ensure slot address: CX = (len-1)*esz, dst = .ptr + CX.
* Clobbers AX (the IMUL immediate) and CX, like the emission it
* replaces; dst must not be AX or CX. */
static void
cg_append_slot(Cg *c, int direct, int off, int scr, int esz, int dst)
{
if (direct) {
ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_CX));
} else {
ins2(c, A_MOVQ, amem(D_BP, scr), areg(dst));
ins2(c, A_MOVQ, amem(dst, 8), areg(D_CX));
}
ins2(c, A_SUBQ, aimm(1), areg(D_CX));
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_AX));
ins2(c, A_IMULQ, areg(D_AX), areg(D_CX));
}
if (direct)
ins2(c, A_MOVQ, amem(D_BP, off), areg(dst));
else
ins2(c, A_MOVQ, amem(dst, 0), areg(dst));
ins2(c, A_ADDQ, areg(D_CX), areg(dst));
}
/* cg_structlit_fill modes — see helper docstring. */
enum {
DST_BP = 0,
@@ -6842,6 +6896,12 @@ cgexpr(Cg *c, Node *n, Local *locals)
Type *esub = su ? su->sub : NULL;
int sn_off = (sn->kind == N_IDENT)
? localfind(locals, sn->str) : 0;
/* FA1 (#15): a non-ident-local target used to silently
* 0-default sn_off, making 0(BP)/8(BP) the "header" —
* rt_ensure then corrupted the caller frame. Non-direct
* targets resolve through cgplaceaddr below; a shape it
* can't address is loud. */
int sn_direct = sn_off != 0;
int store_op = fldstoreop(esub, esz);
/* #34 element-kind store dispatch: the scalar 1-word
* store below silently gutted every wide element
@@ -6858,6 +6918,22 @@ cgexpr(Cg *c, Node *n, Local *locals)
if (!el_wide && esz > 8)
fatal("#34: append() element kind "
"unsupported (rule-7)");
int sn_scr = 0;
if (!sn_direct) {
if (!cgplaceaddr(c, sn, D_BX, locals))
fatal("#15: append() target place "
"unsupported (rule-7)");
/* Fresh slot per SITE, not a per-fn cache: a
* nested append-through-pointer inside a value
* expression (match-yield arm) spills its own
* resolve; a shared slot would hand the outer
* grow/slot reloads the inner target's header.
* local_alloc never dedups by name. */
sn_scr = local_alloc(c, &locals,
"@apphdrscr", 8, cg_frame);
ins2(c, A_MOVQ, areg(D_BX),
amem(D_BP, sn_scr));
}
for (Node *vn = sn->next; vn; vn = vn->next) {
/* #34 review: a spread whose source is not a
* local ident used to fall PAST the spread arm
@@ -6897,10 +6973,8 @@ cgexpr(Cg *c, Node *n, Local *locals)
* from the slice headers after the
* call (i reloads from the counter
* slot; CX was clobbered). */
ins2(c, A_ADDQ, aimm(1), amem(D_BP, sn_off + 8));
ins2(c, A_LEAQ, amem(D_BP, sn_off), areg(D_DI));
ins2(c, A_MOVQ, aimm(esz), areg(D_SI));
ins1(c, A_CALL, masym(c, "rt_ensure"));
cg_append_grow(c, sn_direct, sn_off,
sn_scr, esz);
ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_CX));
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_AX));
@@ -6908,14 +6982,8 @@ cgexpr(Cg *c, Node *n, Local *locals)
}
ins2(c, A_MOVQ, amem(D_BP, it_off), areg(D_BX));
ins2(c, A_ADDQ, areg(D_CX), areg(D_BX));
ins2(c, A_MOVQ, amem(D_BP, sn_off + 8), areg(D_CX));
ins2(c, A_SUBQ, aimm(1), areg(D_CX));
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_AX));
ins2(c, A_IMULQ, areg(D_AX), areg(D_CX));
}
ins2(c, A_MOVQ, amem(D_BP, sn_off), areg(D_DX));
ins2(c, A_ADDQ, areg(D_CX), areg(D_DX));
cg_append_slot(c, sn_direct, sn_off,
sn_scr, esz, D_DX);
int k = 0;
for (; k + 8 <= esz; k += 8) {
ins2(c, A_MOVQ, amem(D_BX, k), areg(D_AX));
@@ -6952,18 +7020,10 @@ cgexpr(Cg *c, Node *n, Local *locals)
ins2(c, load_op, amem(D_BX, 0), areg(D_AX));
/* ensure + store one element */
ins1(c, A_PUSHQ, areg(D_AX));
ins2(c, A_ADDQ, aimm(1), amem(D_BP, sn_off + 8));
ins2(c, A_LEAQ, amem(D_BP, sn_off), areg(D_DI));
ins2(c, A_MOVQ, aimm(esz), areg(D_SI));
ins1(c, A_CALL, masym(c, "rt_ensure"));
ins2(c, A_MOVQ, amem(D_BP, sn_off + 8), areg(D_CX));
ins2(c, A_SUBQ, aimm(1), areg(D_CX));
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_AX));
ins2(c, A_IMULQ, areg(D_AX), areg(D_CX));
}
ins2(c, A_MOVQ, amem(D_BP, sn_off), areg(D_BX));
ins2(c, A_ADDQ, areg(D_CX), areg(D_BX));
cg_append_grow(c, sn_direct, sn_off,
sn_scr, esz);
cg_append_slot(c, sn_direct, sn_off,
sn_scr, esz, D_BX);
ins1(c, A_POPQ, areg(D_AX));
ins2(c, store_op, areg(D_AX), amem(D_BX, 0));
/* loop tail */
@@ -6983,16 +7043,10 @@ cgexpr(Cg *c, Node *n, Local *locals)
ins1(c, A_PUSHQ, areg(D_AX));
ins1(c, A_PUSHQ, areg(D_BX));
ins1(c, A_PUSHQ, areg(D_CX));
ins2(c, A_ADDQ, aimm(1), amem(D_BP, sn_off + 8));
ins2(c, A_LEAQ, amem(D_BP, sn_off), areg(D_DI));
ins2(c, A_MOVQ, aimm(esz), areg(D_SI));
ins1(c, A_CALL, masym(c, "rt_ensure"));
ins2(c, A_MOVQ, amem(D_BP, sn_off + 8), areg(D_CX));
ins2(c, A_SUBQ, aimm(1), areg(D_CX));
ins2(c, A_MOVQ, aimm(esz), areg(D_AX));
ins2(c, A_IMULQ, areg(D_AX), areg(D_CX));
ins2(c, A_MOVQ, amem(D_BP, sn_off), areg(D_DX));
ins2(c, A_ADDQ, areg(D_CX), areg(D_DX));
cg_append_grow(c, sn_direct, sn_off,
sn_scr, esz);
cg_append_slot(c, sn_direct, sn_off,
sn_scr, esz, D_DX);
ins1(c, A_POPQ, areg(D_CX));
ins1(c, A_POPQ, areg(D_BX));
ins1(c, A_POPQ, areg(D_AX));
@@ -7007,18 +7061,10 @@ cgexpr(Cg *c, Node *n, Local *locals)
* the dst pointer (tagged: the #12 widen
* choke-point cgexprs the value internally;
* struct: literal fill / ident word-copy). */
ins2(c, A_ADDQ, aimm(1), amem(D_BP, sn_off + 8));
ins2(c, A_LEAQ, amem(D_BP, sn_off), areg(D_DI));
ins2(c, A_MOVQ, aimm(esz), areg(D_SI));
ins1(c, A_CALL, masym(c, "rt_ensure"));
ins2(c, A_MOVQ, amem(D_BP, sn_off + 8), areg(D_CX));
ins2(c, A_SUBQ, aimm(1), areg(D_CX));
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_AX));
ins2(c, A_IMULQ, areg(D_AX), areg(D_CX));
}
ins2(c, A_MOVQ, amem(D_BP, sn_off), areg(D_BX));
ins2(c, A_ADDQ, areg(D_CX), areg(D_BX));
cg_append_grow(c, sn_direct, sn_off,
sn_scr, esz);
cg_append_slot(c, sn_direct, sn_off,
sn_scr, esz, D_BX);
if (el_tagged) {
cg_widen_tagged_store(c, &locals,
esub, vn, D_BX, 0, esz);
@@ -7069,18 +7115,10 @@ cgexpr(Cg *c, Node *n, Local *locals)
}
cgexpr(c, vn, locals); /* val → AX */
ins1(c, A_PUSHQ, areg(D_AX));
ins2(c, A_ADDQ, aimm(1), amem(D_BP, sn_off + 8));
ins2(c, A_LEAQ, amem(D_BP, sn_off), areg(D_DI));
ins2(c, A_MOVQ, aimm(esz), areg(D_SI));
ins1(c, A_CALL, masym(c, "rt_ensure"));
ins2(c, A_MOVQ, amem(D_BP, sn_off + 8), areg(D_CX));
ins2(c, A_SUBQ, aimm(1), areg(D_CX));
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_AX));
ins2(c, A_IMULQ, areg(D_AX), areg(D_CX));
}
ins2(c, A_MOVQ, amem(D_BP, sn_off), areg(D_BX));
ins2(c, A_ADDQ, areg(D_CX), areg(D_BX));
cg_append_grow(c, sn_direct, sn_off,
sn_scr, esz);
cg_append_slot(c, sn_direct, sn_off,
sn_scr, esz, D_BX);
ins1(c, A_POPQ, areg(D_AX));
ins2(c, store_op, areg(D_AX), amem(D_BX, 0));
}