w6c+selfhost: struct globals — zero-init DATAW + LEAQ-based field access

Extend top-level mutable `let` to cover structs. Same approach as
str / slice: take the field-access base through &name(SB) instead
of off(BP).

  - emit_lets / emitletdataw: emit `sizeof(T)` zero bytes for any
    struct global without a baked-in initialiser. Struct-literal
    init is skipped → undefined symbol at link if used;
  - cgdot read path: when the IDENT base's local lookup misses and
    the name is a struct let, LEAQ name(SB), CX and load the field
    at fi.foff(CX) with the width-aware op (MOVQ / MOVL /
    MOVZBQ / MOVSXD; MOVQ pair for str fields);
  - cgassign write path: parallel handling for plain `=` (incl. str
    fields) and the compound ops (+=, -=) via load → push → eval
    rhs → combine → store with a re-LEAQ between cgexpr clobbers.

Tagged-union fields on struct globals are unsupported in v1 — the
local path's tagged branch isn't generalised yet. Whole-struct
by-value flow through expressions remains NYI (matches the local
status). 630_let_global gains 3 fixtures (read/write, compound +=,
narrow u8 field); selfhost mirror keeps test 990 / 994 / 995 byte
identical; bootstrap fixed point holds.
This commit is contained in:
2026-05-12 12:26:21 +09:00
parent 97eb1fe20d
commit 00d1120441
6 changed files with 697 additions and 32 deletions

View File

@@ -138,6 +138,49 @@ static const struct fixture fixtures[] = {
"};\n",
99,
},
{
"struct-field-readwrite",
/* Top-level struct global. Field writes hit the global
* via LEAQ name(SB) + offset; reads pull each field
* with the right width. */
"type point = struct { x: i32, y: i32 };\n"
"let p: point;\n"
"fn main() i32 = {\n"
"\tp.x = 7;\n"
"\tp.y = 35;\n"
"\treturn p.x + p.y;\n"
"};\n",
42,
},
{
"struct-field-compound",
/* Compound += on a struct global field — load via
* LEAQ+disp, push, eval rhs, combine, store. */
"type counter = struct { n: i64 };\n"
"let c: counter;\n"
"fn bump(d: i64) void = { c.n += d; };\n"
"fn main() i32 = {\n"
"\tbump(10i64);\n"
"\tbump(15i64);\n"
"\tbump(17i64);\n"
"\treturn c.n: i32;\n"
"};\n",
42,
},
{
"struct-narrow-field",
/* u8 field on a struct global. Read uses MOVZBQ, store
* uses MOVB. Pre-fix this would have stomped neighbouring
* bytes by emitting MOVQ. */
"type packet = struct { hdr: u8, body: u32 };\n"
"let pkt: packet;\n"
"fn main() i32 = {\n"
"\tpkt.hdr = 42u8;\n"
"\tpkt.body = 999u32;\n"
"\treturn pkt.hdr: i32;\n"
"};\n",
42,
},
{ NULL, NULL, 0 }
};