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

@@ -584,9 +584,10 @@ fn letscalarprim(nm: str) bool = {
// letemitsize — slot size in bytes for a top-level `let`, or 0 if
// the type isn't yet supported as a writable global. Walks type
// aliases so byte output matches C cgen, which resolves Type kinds.
// 8 → scalar (literal init supported)
// 16 → str (only zero-init / nil / "" supported)
// 24 → slice (only zero-init supported)
// 8 → scalar (literal init supported)
// 16 → str (only zero-init / nil / "" supported)
// 24 → slice (only zero-init supported)
// varies → struct (zero-init only; field reads/scalar-field writes)
fn letemitsize(c: *cgen, d: *node) i32 = {
if (d == nil) { return 0; };
let t: *node = d.lhs;
@@ -597,6 +598,8 @@ fn letemitsize(c: *cgen, d: *node) i32 = {
let nm: str = t.str;
if (letscalarprim(nm)) { return 8; };
if (streq(nm, "str")) { return 16; };
let si: *structinfo = structlookup(c, nm);
if (si != nil) { return si.totsize; };
let next: *node = aliaslookup(c, nm);
if (next == nil) { return 0; };
t = next;
@@ -676,6 +679,54 @@ fn letvarisslice(c: *cgen, name: str) bool = {
return false;
};
// letvarisstruct — is the named top-level let a struct global?
// Struct globals use LEAQ name(SB), CX as the field-access base; the
// cgdot read and cgassign write paths branch on this to skip the
// frame-relative addressing they use for locals.
fn letvarisstruct(c: *cgen, name: str) bool = {
let lv: *letvar = c.lets;
for (lv != nil) {
if (streq(lv.name, name)) {
let t: *node = lv.tnode;
for (t != nil) {
if (t.kind != nkind.N_TNAME) { return false; };
let nm: str = t.str;
if (structlookup(c, nm) != nil) { return true; };
let nx: *node = aliaslookup(c, nm);
if (nx == nil) { return false; };
t = nx;
};
return false;
};
lv = lv.lvnext;
};
return false;
};
// letvarstructinfo — for a struct global, return its structinfo
// so the cgdot/cgassign paths can look up fields. nil if the let
// isn't a struct (or wasn't found).
fn letvarstructinfo(c: *cgen, name: str) *structinfo = {
let lv: *letvar = c.lets;
for (lv != nil) {
if (streq(lv.name, name)) {
let t: *node = lv.tnode;
for (t != nil) {
if (t.kind != nkind.N_TNAME) { return nil; };
let nm: str = t.str;
let si: *structinfo = structlookup(c, nm);
if (si != nil) { return si; };
let nx: *node = aliaslookup(c, nm);
if (nx == nil) { return nil; };
t = nx;
};
return nil;
};
lv = lv.lvnext;
};
return nil;
};
// emitdatawbyte — write one byte of an asm string literal using
// the same escape rules as emitdefconstants / emitdatasection.
fn emitdatawbyte(b: u8) void = {
@@ -724,7 +775,8 @@ fn emitletdataw(c: *cgen, file: *node) void = {
let nm: str = d.str;
if (nm.len > 0) {
let sz: i32 = letemitsize(c, d);
if (sz == 8) {
let issg: bool = letvarisstruct(c, nm);
if (sz == 8 && !issg) {
let v: u64 = 0u64;
let ok: bool = true;
if (d.rhs != nil) {
@@ -757,7 +809,7 @@ fn emitletdataw(c: *cgen, file: *node) void = {
emitline("\"\n");
};
};
if (sz == 16) {
if (sz == 16 && !issg) {
let ok: bool = true;
if (d.rhs != nil) {
let r: *node = d.rhs;
@@ -785,7 +837,7 @@ fn emitletdataw(c: *cgen, file: *node) void = {
emitline("\"\n");
};
};
if (sz == 24) {
if (sz == 24 && !issg) {
// Slice: zero-init only (no slice-literal
// syntax to honour). Any rhs other than
// `nil` is skipped → undefined symbol at
@@ -814,6 +866,23 @@ fn emitletdataw(c: *cgen, file: *node) void = {
emitline("\"\n");
};
};
// Struct globals — any size, zero-init only.
// A struct literal init isn't compile-time
// evaluated yet; skip and the link will surface
// an undefined-symbol error if referenced.
if (issg) {
if (d.rhs == nil) {
emitline("DATAW ");
emitsymname(c, nm);
emitline("(SB),\"");
let i: i32 = 0;
for (i < sz) {
emitdatawbyte(0u8);
i += 1;
};
emitline("\"\n");
};
};
};
};
d = d.next;