w6c+selfhost: str globals — 16B DATAW + (LEAQ, MOVQ, MOVQ) sequences

Extend top-level mutable `let` to cover str. The cgen now:

  - emits a 16-byte zero DATAW for `let s: str;` (and the trivial
    `nil` / `""` inits); a non-empty strlit init is skipped because
    a compile-time .data → .text reloc isn't supported yet, so the
    user gets a clean undefined-symbol error at link;
  - loads `s` as `(LEAQ s(SB), CX; MOVQ (CX), AX; MOVQ 8(CX), BX)`
    so the (AX=ptr, BX=len) pair convention is preserved;
  - stores via the same `&s` indirection for `s = expr;` and routes
    the `.ptr` / `.len` pseudo-field N_DOT branch through it; and
  - tracks the declared type on each LetVar so cgident / cgdot /
    cgassign pick the right load/store shape.

Selfhost cgen mirrors all four paths byte-for-byte; test 990
(cgen-match on err.ww) and tests 994/995 (self-rebuild) stay
green. 630_let_global gains two new fixtures (`let msg: str;` +
runtime assign, plus reassign from a helper).

Slice and struct globals still NYI — same scope deferred.
This commit is contained in:
2026-05-12 12:10:23 +09:00
parent 4bf1b56872
commit 208bdd25df
6 changed files with 648 additions and 222 deletions

View File

@@ -401,11 +401,20 @@ fn cgident(c: *cgen, n: *node) void = {
return;
};
// Top-level mutable `let` — RIP-relative load from its DATAW
// slot. Mirrors C cgen's catch-all `MOVQ masym(s), AX`. Names
// that aren't lets either (typos, never-defined) flow through
// here too in C; the divergence today is bounded to scalar lets,
// which is what `isletvar` gates on.
// slot. Mirrors C cgen's catch-all `MOVQ masym(s), AX` for
// scalar lets, plus the (LEAQ, MOVQ, MOVQ) sequence for str
// globals so both ptr and len land in (AX, BX). Names that
// aren't lets either (typos, never-defined) drop through to
// the silent return.
if (isletvar(c, nm)) {
if (letvarisstr(c, nm)) {
emitline("\tLEAQ\t");
emitsymname(c, nm);
emitline("(SB), CX\n");
emitline("\tMOVQ\t(CX), AX\n");
emitline("\tMOVQ\t8(CX), BX\n");
return;
};
emitline("\tMOVQ\t");
emitsymname(c, nm);
emitline("(SB), AX\n");
@@ -871,6 +880,30 @@ fn cgdot(c: *cgen, n: *node) void = {
};
};
};
// Top-level str global field access — load .ptr / .len via
// &name(SB) into CX, then MOVQ delta(CX), AX. Without this
// the module-qualified fallback below would mis-emit
// `MOVQ <field>(SB), AX`.
if (lhs != nil) {
if (lhs.kind == nkind.N_IDENT) {
if (isletvar(c, lhs.str)) {
if (letvarisstr(c, lhs.str)) {
let delta: i32 = -1;
if (streq(fld, "ptr")) { delta = 0; };
if (streq(fld, "len")) { delta = 8; };
if (delta >= 0) {
emitline("\tLEAQ\t");
emitsymname(c, lhs.str);
emitline("(SB), CX\n");
emitline("\tMOVQ\t");
emitdispreg(delta: i64, "CX");
emitline(", AX\n");
return;
};
};
};
};
};
// Module-qualified value reference: `mod.name` where `mod`
// is nkind.N_IDENT bound as skind.SK_USE and the leaf isn't a local.
// Treat as a SB symbol — `MOVQ leaf(SB), AX`. Same fallback
@@ -1546,10 +1579,20 @@ fn cgassign(c: *cgen, n: *node) void = {
if (off == 0) {
// Top-level let target: RIP-relative store
// for `=`, or load→combine→store for the
// compound forms.
// compound forms. For a str global, take its
// address into CX and store both halves; the
// asm has no `name+8(SB)` operand form.
if (!isletvar(c, nm)) { return; };
cgexpr(c, n.rhs);
if (n.op == tkind.TK_ASSIGN) {
if (letvarisstr(c, nm)) {
emitline("\tLEAQ\t");
emitsymname(c, nm);
emitline("(SB), CX\n");
emitline("\tMOVQ\tAX, (CX)\n");
emitline("\tMOVQ\tBX, 8(CX)\n");
return;
};
emitline("\tMOVQ\tAX, ");
emitsymname(c, nm);
emitline("(SB)\n");