cgen: #50 append/insert tagged-dst VALUE boxes PRE-grow, both stages

The #34 value-store dispatch's tagged arm ran the widen choke-point
(cg_widen_tagged_store / cgwidentaggedstore — which cgexprs the value
internally) AFTER cg_append_grow, so a value expression reading the
destination (xs.len) saw post-grow state. Hare evaluates arguments
before the call's effects; the scalar/str/slice arms already ordered
value-first. insert() inherited the bug through its desugar-to-append
(9861f73) — one boxing choke-point, so this is the whole fix. Box the
value into a fresh per-site @apptagscr frame scratch pre-grow (zero,
BP-rooted widen store), then raw-copy the finished box into the new
slot post-grow.

Consumer evidence: regex fold-5b's {,0} rows (reviewer-5b's mutant;
ken's corrected-root matrix /tmp/ken_silent, f50v4_tagged exit 15 both
stages, byte-id, gate-blind). lib/regex.ww:643-647's pre-bound
split_target workaround comment GRADUATES with this commit; the
comment update itself rides the next regex touch per the standing
rule.

test/807: 57 -> 84 fixtures. New rows: tagged_pregrow_val (f50v4
exact), scalar_pregrow_val (no-regress control), tagged_selfref_val,
tagged_str_payload, tagged_regex_minrep (the {,0} shape standalone),
tagged_append_pregrow_val (direct append, the fix site),
tagged_realloc_selfref_loop (ken k50a: old-base read across actual
rt_ensure base moves), tagged_seq_positions (ken k50b: sequenced
inserts at 0/mid, each len-reading) and tagged_void_variant (ken k50c:
tag-only box through the fresh scratch). Mutation at e8977a4 fails
exactly the five eval-order-discriminating tagged rows, both stages.

The STRUCT-LITERAL value arm keeps its post-grow field-expr fill —
same eval-order class, different arm; filed (ww-core #59), cited
at-site, not folded.
This commit is contained in:
2026-06-05 09:08:02 +09:00
parent 24e02b259c
commit 80e7096f25
5 changed files with 399 additions and 31 deletions

View File

@@ -26243,10 +26243,14 @@ fn cgappend(c: *cgen, n: *node) void = {
continue;
};
if (eltagged || elstruct) {
// #34: no register form survives rt_ensure for these
// grow FIRST, then fill through the dst pointer
// (tagged: the #12 widen choke-point cgexprs the value
// internally; struct: literal fill / ident word-copy).
// #34: no register form survives rt_ensure for these.
// struct: grow FIRST, then fill through the dst pointer
// (literal fill / ident word-copy). tagged: #50 — the
// #12 widen choke-point cgexprs the value internally,
// so boxing must run PRE-grow (Hare's argument order:
// a `xs.len` read in v sees the pre-append len, like
// the scalar arm); box into a frame scratch, grow,
// raw-copy the finished box in.
// #49 (#35's single-element sibling): a place-chain
// source (indexed field `threads[i].root_capture`
// regex.ha:819, deref spine, computed index) SPLITS
@@ -26372,14 +26376,43 @@ fn cgappend(c: *cgen, n: *node) void = {
};
aplace = true;
};
cgappendgrow(c, sndirect, sn_off, snscr, esz);
cgappendslot(c, sndirect, sn_off, snscr, esz, "BX");
if (eltagged) {
cgwidentaggedstore(c, esubti, vn, "BX", 0, esz);
// Fresh slot per SITE, not the shared per-size
// scratch: the box must stay live across
// rt_ensure, and a nested append inside the
// value expression would clobber a dedup'd
// slot (the @apphdrscr rationale; #25/#31).
let tgscr: i32 = localalloc(c, "@apptagscr", esz, nil);
emitline("\tXORQ\tAX, AX\n");
let zk: i32 = 0;
for (zk < esz) {
emitline("\tMOVQ\tAX, ");
emitoff((tgscr + zk): i64);
emitline("(BP)\n");
zk += 8;
};
cgwidentaggedstore(c, esubti, vn, "BP", tgscr, esz);
cgappendgrow(c, sndirect, sn_off, snscr, esz);
cgappendslot(c, sndirect, sn_off, snscr, esz, "BX");
let ck: i32 = 0;
for (ck < esz) {
emitline("\tMOVQ\t");
emitoff((tgscr + ck): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(ck: i64, "BX");
emitline("\n");
ck += 8;
};
vn = vn.next;
continue;
};
cgappendgrow(c, sndirect, sn_off, snscr, esz);
cgappendslot(c, sndirect, sn_off, snscr, esz, "BX");
if (vn.kind == nkind.N_STRUCTLIT) {
// #59 (#50's eval-order kin): the literal's field
// exprs still eval POST-grow here — filed, not
// folded.
let scroff: i32 = localadd(c, "@appendscr", 8, nil);
emitline("\tMOVQ\tBX, ");
emitoff(scroff: i64);

View File

@@ -5399,10 +5399,14 @@ fn cgappend(c: *cgen, n: *node) void = {
continue;
};
if (eltagged || elstruct) {
// #34: no register form survives rt_ensure for these
// grow FIRST, then fill through the dst pointer
// (tagged: the #12 widen choke-point cgexprs the value
// internally; struct: literal fill / ident word-copy).
// #34: no register form survives rt_ensure for these.
// struct: grow FIRST, then fill through the dst pointer
// (literal fill / ident word-copy). tagged: #50 — the
// #12 widen choke-point cgexprs the value internally,
// so boxing must run PRE-grow (Hare's argument order:
// a `xs.len` read in v sees the pre-append len, like
// the scalar arm); box into a frame scratch, grow,
// raw-copy the finished box in.
// #49 (#35's single-element sibling): a place-chain
// source (indexed field `threads[i].root_capture`
// regex.ha:819, deref spine, computed index) SPLITS
@@ -5528,14 +5532,43 @@ fn cgappend(c: *cgen, n: *node) void = {
};
aplace = true;
};
cgappendgrow(c, sndirect, sn_off, snscr, esz);
cgappendslot(c, sndirect, sn_off, snscr, esz, "BX");
if (eltagged) {
cgwidentaggedstore(c, esubti, vn, "BX", 0, esz);
// Fresh slot per SITE, not the shared per-size
// scratch: the box must stay live across
// rt_ensure, and a nested append inside the
// value expression would clobber a dedup'd
// slot (the @apphdrscr rationale; #25/#31).
let tgscr: i32 = localalloc(c, "@apptagscr", esz, nil);
emitline("\tXORQ\tAX, AX\n");
let zk: i32 = 0;
for (zk < esz) {
emitline("\tMOVQ\tAX, ");
emitoff((tgscr + zk): i64);
emitline("(BP)\n");
zk += 8;
};
cgwidentaggedstore(c, esubti, vn, "BP", tgscr, esz);
cgappendgrow(c, sndirect, sn_off, snscr, esz);
cgappendslot(c, sndirect, sn_off, snscr, esz, "BX");
let ck: i32 = 0;
for (ck < esz) {
emitline("\tMOVQ\t");
emitoff((tgscr + ck): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(ck: i64, "BX");
emitline("\n");
ck += 8;
};
vn = vn.next;
continue;
};
cgappendgrow(c, sndirect, sn_off, snscr, esz);
cgappendslot(c, sndirect, sn_off, snscr, esz, "BX");
if (vn.kind == nkind.N_STRUCTLIT) {
// #59 (#50's eval-order kin): the literal's field
// exprs still eval POST-grow here — filed, not
// folded.
let scroff: i32 = localadd(c, "@appendscr", 8, nil);
emitline("\tMOVQ\tBX, ");
emitoff(scroff: i64);

View File

@@ -26243,10 +26243,14 @@ fn cgappend(c: *cgen, n: *node) void = {
continue;
};
if (eltagged || elstruct) {
// #34: no register form survives rt_ensure for these
// grow FIRST, then fill through the dst pointer
// (tagged: the #12 widen choke-point cgexprs the value
// internally; struct: literal fill / ident word-copy).
// #34: no register form survives rt_ensure for these.
// struct: grow FIRST, then fill through the dst pointer
// (literal fill / ident word-copy). tagged: #50 — the
// #12 widen choke-point cgexprs the value internally,
// so boxing must run PRE-grow (Hare's argument order:
// a `xs.len` read in v sees the pre-append len, like
// the scalar arm); box into a frame scratch, grow,
// raw-copy the finished box in.
// #49 (#35's single-element sibling): a place-chain
// source (indexed field `threads[i].root_capture`
// regex.ha:819, deref spine, computed index) SPLITS
@@ -26372,14 +26376,43 @@ fn cgappend(c: *cgen, n: *node) void = {
};
aplace = true;
};
cgappendgrow(c, sndirect, sn_off, snscr, esz);
cgappendslot(c, sndirect, sn_off, snscr, esz, "BX");
if (eltagged) {
cgwidentaggedstore(c, esubti, vn, "BX", 0, esz);
// Fresh slot per SITE, not the shared per-size
// scratch: the box must stay live across
// rt_ensure, and a nested append inside the
// value expression would clobber a dedup'd
// slot (the @apphdrscr rationale; #25/#31).
let tgscr: i32 = localalloc(c, "@apptagscr", esz, nil);
emitline("\tXORQ\tAX, AX\n");
let zk: i32 = 0;
for (zk < esz) {
emitline("\tMOVQ\tAX, ");
emitoff((tgscr + zk): i64);
emitline("(BP)\n");
zk += 8;
};
cgwidentaggedstore(c, esubti, vn, "BP", tgscr, esz);
cgappendgrow(c, sndirect, sn_off, snscr, esz);
cgappendslot(c, sndirect, sn_off, snscr, esz, "BX");
let ck: i32 = 0;
for (ck < esz) {
emitline("\tMOVQ\t");
emitoff((tgscr + ck): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(ck: i64, "BX");
emitline("\n");
ck += 8;
};
vn = vn.next;
continue;
};
cgappendgrow(c, sndirect, sn_off, snscr, esz);
cgappendslot(c, sndirect, sn_off, snscr, esz, "BX");
if (vn.kind == nkind.N_STRUCTLIT) {
// #59 (#50's eval-order kin): the literal's field
// exprs still eval POST-grow here — filed, not
// folded.
let scroff: i32 = localadd(c, "@appendscr", 8, nil);
emitline("\tMOVQ\tBX, ");
emitoff(scroff: i64);