w6c+selfhost: cg_widen_tagged_store basereg + N_ASSIGN tagged field (closes #26)

Extended cg_widen_tagged_store (cstage) / cgwidentaggedstore (wwstage)
to take a base_reg/basereg parameter so the primitive supports non-BP
destinations. Cstage extends body in-place via via_outer gate +
spill+scratch+copy-out; wwstage splits into wrapper (non-BP) +
cgwidentaggedstorebp (BP-only) to dodge the no-goto constraint. New
N_ASSIGN field TY_TAGGED branch routes through the primitive for all
rhs shapes.

Scope-adjacent: fieldsize recurses through N_TTAGGED via slotsize and
TNAME-aliased-to-tagged via aliaslookup. Needed for the test fixtures.

Wwstage read-side N_DOT-of-tagged-field source is filed as task #28;
test rows use mark-canary verification until that lands.
This commit is contained in:
2026-05-14 19:12:20 +09:00
parent 1726bcef18
commit a5919ed8da
8 changed files with 644 additions and 116 deletions

View File

@@ -423,6 +423,72 @@ static const struct row rows[] = {
" return 42;\n"
"};\n",
42 },
/* Whole-tagged rhs to struct tagged-union field (task #26). cgen
* N_ASSIGN had a TY_TAGGED branch that called cg_tag_for_variant
* with the rhs type — when the rhs was the tagged union itself
* (not a concrete variant), the function returned -1 and the
* branch wrote literal 0 as the tag plus only AX as the payload,
* dropping the original tag and trailing payload words. Fixed
* by routing through cg_widen_tagged_store, which copies all slot
* words from the source's tagged slot and runs cg_widen_tag_remap.
*
* Verification: the write must not bleed into the trailing `mark`
* field. Direct struct local. Reading x.e back via match is a
* separate code path (gated on a wwstage read-side bug for
* tagged N_DOT — filed below as a follow-up); these rows pin the
* write-side bug and let mark act as a canary for stray stores
* past the field boundary. */
{ "tagged_field_value_write_local_mark_canary",
"type ev = (i64 | i32);\n"
"type holder = struct { e: ev, mark: i32 };\n"
"fn main() i32 = {\n"
" let v: ev = 33: i32;\n"
" let x: holder;\n"
" x.mark = 0x3a3a3a3a;\n"
" x.e = v;\n"
" if (x.mark != 0x3a3a3a3a) { return 1; };\n"
" return 42;\n"
"};\n",
42 },
/* Same shape, concrete-variant str widening. The str payload
* occupies +8/+16 (ptr/len); a missing store to +16 (the pre-#26
* shape that wrote only AX) would leave .len at whatever the
* neighbouring memory held. mark canary at +24 catches a stray
* AX-only path that wrote past 16B into the mark slot. */
{ "tagged_field_value_write_str_variant_mark_canary",
"type ev = (i32 | str);\n"
"type holder = struct { e: ev, mark: i32 };\n"
"fn main() i32 = {\n"
" let x: holder;\n"
" x.mark = 0x5e5e5e5e;\n"
" x.e = (\"hello\": ev);\n"
" if (x.mark != 0x5e5e5e5e) { return 1; };\n"
" return 42;\n"
"};\n",
42 },
/* Pointer-rooted dst — `(*p).e = v` exercises the base-reg
* parameterisation added to cg_widen_tagged_store in #26. The
* wrapper spills BX (the *struct pointer), routes the body
* through a BP-rooted scratch, reloads BX, and word-copies the
* scratch to (BX, foff). Without that path the via_ptr arm of
* the new TY_TAGGED branch would either trample BX during cgexpr
* or write to a stale slot address. mark canary at +24 catches
* a stray spill that wrote past the e field. */
{ "tagged_field_value_write_via_ptr_mark_canary",
"type ev = (i64 | i32);\n"
"type holder = struct { e: ev, mark: i32 };\n"
"fn fill(h: *holder) void = {\n"
" let v: ev = 33: i32;\n"
" h.mark = 0x77777777;\n"
" h.e = v;\n"
"};\n"
"fn main() i32 = {\n"
" let x: holder;\n"
" fill(&x);\n"
" if (x.mark != 0x77777777) { return 1; };\n"
" return 42;\n"
"};\n",
42 },
};
static int