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

@@ -8012,10 +8012,15 @@ cgexpr(Cg *c, Node *n, Local *locals)
}
if (el_tagged || el_struct) {
/* #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). */
* 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,
@@ -8122,16 +8127,47 @@ cgexpr(Cg *c, Node *n, Local *locals)
}
aplace = 1;
}
if (el_tagged) {
/* 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). */
int tg_scr = local_alloc(c,
&locals, "@apptagscr", esz,
cg_frame);
ins2(c, A_XORQ, areg(D_AX),
areg(D_AX));
for (int zk = 0; zk < esz; zk += 8)
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, tg_scr + zk));
cg_widen_tagged_store(c, &locals,
esub, vn, D_BP, tg_scr, esz);
cg_append_grow(c, sn_direct, sn_off,
sn_scr, esz);
cg_append_slot(c, sn_direct, sn_off,
sn_scr, esz, D_BX);
for (int ck = 0; ck < esz; ck += 8) {
ins2(c, A_MOVQ,
amem(D_BP, tg_scr + ck),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BX, ck));
}
continue;
}
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);
continue;
}
if (vn->kind == N_STRUCTLIT) {
/* #59 (#50's eval-order kin):
* the literal's field exprs
* still eval POST-grow here —
* filed, not folded. */
if (cg_appendscr == 0)
cg_appendscr = local_alloc(c,
&locals, "@appendscr", 8,