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

@@ -397,13 +397,13 @@ struct LetVar {
};
static LetVar *letvars;
/* Subset of types we know how to store in 8 bytes of .data and load
* back with a plain MOVQ. Floats need MOVSS/MOVSD; str/slice/struct/
* tagged unions are multi-word; enums route through their storage
* type. Keep this tight — extending it requires the matching load/
* store code below. */
/* Slot size for a top-level `let` of type t, or 0 if the type isn't
* supported as a writable global yet. Floats (MOVSS/SD) and slice/
* struct/tagged unions are deferred. enums route through their
* storage type. Keep this tight — extending it requires the matching
* load/store code below. */
static int
let_scalar_ok(Type *t)
let_emit_size(Type *t)
{
if (t == NULL) return 0;
Type *u = (t->kind == TY_NAMED) ? t->under : t;
@@ -414,12 +414,25 @@ let_scalar_ok(Type *t)
case TY_U8: case TY_U16: case TY_U32: case TY_U64:
case TY_INT: case TY_UINT: case TY_UINTPTR:
case TY_PTR:
return 1;
return 8;
case TY_STR:
return 16; /* {ptr, len}; literal-strlit init NYI. */
default:
return 0;
}
}
/* Is the unwrapped type a str? Used by the load/store paths so the
* (AX, BX) pair convention is preserved for str globals, mirroring
* what we already do for str locals. */
static int
let_isstr(Type *t)
{
if (t == NULL) return 0;
Type *u = (t->kind == TY_NAMED) ? t->under : t;
return u && u->kind == TY_STR;
}
static int
decl_has_ffisym(Node *d)
{
@@ -476,7 +489,7 @@ let_collect(Cg *c, Node *file)
for (Node *d = file->list; d; d = d->next) {
if (d->kind != N_LET) continue;
if (d->str == NULL || d->str[0] == '\0') continue;
if (!let_scalar_ok(d->type)) continue;
if (let_emit_size(d->type) == 0) continue;
LetVar *lv = amalloc(c->a, sizeof *lv);
lv->name = d->str;
lv->next = letvars;
@@ -751,6 +764,15 @@ cgexpr(Cg *c, Node *n, Local *locals)
areg(D_BX));
goto ident_done;
}
if (let_islet(n->str) && let_isstr(n->type)) {
/* Top-level str global: load both halves via
* its address (the asm has no `name+8(SB)`
* operand form). */
ins2(c, A_LEAQ, masym(c, n->str), areg(D_CX));
ins2(c, A_MOVQ, amem(D_CX, 0), areg(D_AX));
ins2(c, A_MOVQ, amem(D_CX, 8), areg(D_BX));
goto ident_done;
}
ins2(c, A_MOVQ, masym(c, n->str), areg(D_AX));
}
ident_done:
@@ -1421,17 +1443,29 @@ cgexpr(Cg *c, Node *n, Local *locals)
}
/* Plain `name = strexpr;` for a str-typed local. cgexpr leaves
* (AX=ptr, BX=len); store both halves at off+0 and off+8.
* Mirrors the let-init shape so reassignment doesn't truncate. */
* Mirrors the let-init shape so reassignment doesn't truncate.
* Top-level str globals follow the same shape but go through
* &name(SB) since the asm has no `name+8(SB)` operand form. */
if (n->lhs && n->lhs->kind == N_IDENT && n->op == TK_ASSIGN
&& n->lhs->type) {
Type *lt = n->lhs->type;
Type *lu = (lt && lt->kind == TY_NAMED) ? lt->under : lt;
if (lu && lu->kind == TY_STR) {
int off = localfind(locals, n->lhs->str);
if (off == 0) break;
cgexpr(c, n->rhs, locals);
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0));
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 8));
if (off != 0) {
cgexpr(c, n->rhs, locals);
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0));
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 8));
break;
}
if (let_islet(n->lhs->str)) {
cgexpr(c, n->rhs, locals);
ins2(c, A_LEAQ, masym(c, n->lhs->str),
areg(D_CX));
ins2(c, A_MOVQ, areg(D_AX), amem(D_CX, 0));
ins2(c, A_MOVQ, areg(D_BX), amem(D_CX, 8));
break;
}
break;
}
}
@@ -2429,6 +2463,20 @@ cgexpr(Cg *c, Node *n, Local *locals)
}
goto dot_done;
}
/* Top-level str/slice `let` — load
* the field through &name(SB). Same
* pattern as the bare N_IDENT load. */
if (let_islet(n->lhs->str)) {
int delta = ptrfld ? 0
: (lenfld ? 8 : 16);
ins2(c, A_LEAQ,
masym(c, n->lhs->str),
areg(D_CX));
ins2(c, A_MOVQ,
amem(D_CX, delta),
areg(D_AX));
goto dot_done;
}
}
int delta = ptrfld ? 0 : (lenfld ? 8 : 16);
ins2(c, A_MOVQ, amem(D_BP, off + delta),
@@ -3570,50 +3618,88 @@ cgfn(Cg *c, FILE *out, Node *fn)
txt_emit(out, c->head);
}
/* Emit a single 8-byte DATA/DATAW row for a scalar value. Shares
* the escape rules with emit_defs/emit_data so the .o bytes stay
* stable. */
/* Escape one byte for an asm string literal — the same rules
* emit_data and emit_defs already use. */
static void
emit_data_byte(FILE *out, u8 b)
{
if (b == '"' || b == '\\')
fprintf(out, "\\%c", b);
else if (b < 0x20 || b >= 0x7f)
fprintf(out, "\\x%02x", b);
else
fputc(b, out);
}
/* Emit `DIR NAME(SB),"<8 LE bytes of v>"`. Used for scalar `def`
* constants (DATA) and scalar `let` globals (DATAW). */
static void
emit_data_row(FILE *out, const char *dir, const char *name, u64 v)
{
fprintf(out, "%s %s(SB),\"", dir, name);
for (int i = 0; i < 8; i++) {
unsigned b = (unsigned)((v >> (i * 8)) & 0xff);
if (b == '"' || b == '\\')
fprintf(out, "\\%c", b);
else if (b < 0x20 || b >= 0x7f)
fprintf(out, "\\x%02x", b);
else
fputc(b, out);
}
for (int i = 0; i < 8; i++)
emit_data_byte(out, (u8)((v >> (i * 8)) & 0xff));
fputs("\"\n", out);
}
/* Emit DATAW directives for top-level mutable `let` decls. Only scalar
* types in `let_scalar_ok` are supported; the rest are silently
* skipped at cgen and link with an undefined-symbol error if used.
* Initialisers must be integer/rune/bool/nil literals (or a `let`
* with no init, which zero-initialises). */
/* Emit `DIR NAME(SB),"<sz zero bytes>"`. Used for top-level str/
* slice/struct lets without a baked-in initialiser — the slot is
* pre-zeroed and the program writes the real value at runtime. */
static void
emit_data_row_zero(FILE *out, const char *dir, const char *name, int sz)
{
fprintf(out, "%s %s(SB),\"", dir, name);
for (int i = 0; i < sz; i++)
emit_data_byte(out, 0);
fputs("\"\n", out);
}
/* Emit DATAW directives for top-level mutable `let` decls.
*
* Scalar lets (8B): emit the literal value, or 0 if no init.
* Non-literal init: skip — undefined symbol surfaces at link time.
*
* str lets (16B): emit 16 zero bytes when there is no init (or
* the init is `nil` / `""`). A non-empty strlit init would need a
* compile-time .data → .text relocation (asm doesn't support that
* yet); we silently skip and let the link fail loudly. */
static void
emit_lets(Cg *c, FILE *out, Node *file)
{
for (Node *d = file->list; d; d = d->next) {
if (d->kind != N_LET) continue;
if (d->str == NULL || d->str[0] == '\0') continue;
if (!let_scalar_ok(d->type)) continue;
u64 v = 0;
int sz = let_emit_size(d->type);
if (sz == 0) continue;
if (sz == 8) {
u64 v = 0;
if (d->rhs != NULL) {
Node *r = d->rhs;
while (r != NULL && r->kind == N_CAST) r = r->lhs;
if (r == NULL) continue;
if (r->kind == N_INTLIT) v = r->uval;
else if (r->kind == N_RUNELIT) v = r->uval;
else if (r->kind == N_TRUE) v = 1;
else if (r->kind == N_FALSE) v = 0;
else if (r->kind == N_NIL) v = 0;
else continue;
}
emit_data_row(out, "DATAW", mod_mangle(c, d->str), v);
continue;
}
/* Multi-word (str, 16B). Only zero-init shapes are
* supported: no rhs, or `nil`, or `""` (which interns to
* a strlit but we still emit a zero header — the program
* has to assign a real strlit at runtime to use it). */
if (d->rhs != NULL) {
Node *r = d->rhs;
while (r != NULL && r->kind == N_CAST) r = r->lhs;
if (r == NULL) continue;
if (r->kind == N_INTLIT) v = r->uval;
else if (r->kind == N_RUNELIT) v = r->uval;
else if (r->kind == N_TRUE) v = 1;
else if (r->kind == N_FALSE) v = 0;
else if (r->kind == N_NIL) v = 0;
else continue; /* non-literal init: skip */
int empty_str = (r->kind == N_STRLIT && r->strlen == 0);
if (r->kind != N_NIL && !empty_str)
continue;
}
emit_data_row(out, "DATAW", mod_mangle(c, d->str), v);
emit_data_row_zero(out, "DATAW", mod_mangle(c, d->str), sz);
}
}