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; static LetVar *letvars;
/* Subset of types we know how to store in 8 bytes of .data and load /* Slot size for a top-level `let` of type t, or 0 if the type isn't
* back with a plain MOVQ. Floats need MOVSS/MOVSD; str/slice/struct/ * supported as a writable global yet. Floats (MOVSS/SD) and slice/
* tagged unions are multi-word; enums route through their storage * struct/tagged unions are deferred. enums route through their
* type. Keep this tight — extending it requires the matching load/ * storage type. Keep this tight — extending it requires the matching
* store code below. */ * load/store code below. */
static int static int
let_scalar_ok(Type *t) let_emit_size(Type *t)
{ {
if (t == NULL) return 0; if (t == NULL) return 0;
Type *u = (t->kind == TY_NAMED) ? t->under : t; 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_U8: case TY_U16: case TY_U32: case TY_U64:
case TY_INT: case TY_UINT: case TY_UINTPTR: case TY_INT: case TY_UINT: case TY_UINTPTR:
case TY_PTR: case TY_PTR:
return 1; return 8;
case TY_STR:
return 16; /* {ptr, len}; literal-strlit init NYI. */
default: default:
return 0; 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 static int
decl_has_ffisym(Node *d) decl_has_ffisym(Node *d)
{ {
@@ -476,7 +489,7 @@ let_collect(Cg *c, Node *file)
for (Node *d = file->list; d; d = d->next) { for (Node *d = file->list; d; d = d->next) {
if (d->kind != N_LET) continue; if (d->kind != N_LET) continue;
if (d->str == NULL || d->str[0] == '\0') 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); LetVar *lv = amalloc(c->a, sizeof *lv);
lv->name = d->str; lv->name = d->str;
lv->next = letvars; lv->next = letvars;
@@ -751,6 +764,15 @@ cgexpr(Cg *c, Node *n, Local *locals)
areg(D_BX)); areg(D_BX));
goto ident_done; 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)); ins2(c, A_MOVQ, masym(c, n->str), areg(D_AX));
} }
ident_done: ident_done:
@@ -1421,17 +1443,29 @@ cgexpr(Cg *c, Node *n, Local *locals)
} }
/* Plain `name = strexpr;` for a str-typed local. cgexpr leaves /* Plain `name = strexpr;` for a str-typed local. cgexpr leaves
* (AX=ptr, BX=len); store both halves at off+0 and off+8. * (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 if (n->lhs && n->lhs->kind == N_IDENT && n->op == TK_ASSIGN
&& n->lhs->type) { && n->lhs->type) {
Type *lt = n->lhs->type; Type *lt = n->lhs->type;
Type *lu = (lt && lt->kind == TY_NAMED) ? lt->under : lt; Type *lu = (lt && lt->kind == TY_NAMED) ? lt->under : lt;
if (lu && lu->kind == TY_STR) { if (lu && lu->kind == TY_STR) {
int off = localfind(locals, n->lhs->str); int off = localfind(locals, n->lhs->str);
if (off == 0) break; if (off != 0) {
cgexpr(c, n->rhs, locals); cgexpr(c, n->rhs, locals);
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0)); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0));
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 8)); 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; break;
} }
} }
@@ -2429,6 +2463,20 @@ cgexpr(Cg *c, Node *n, Local *locals)
} }
goto dot_done; 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); int delta = ptrfld ? 0 : (lenfld ? 8 : 16);
ins2(c, A_MOVQ, amem(D_BP, off + delta), 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); txt_emit(out, c->head);
} }
/* Emit a single 8-byte DATA/DATAW row for a scalar value. Shares /* Escape one byte for an asm string literal — the same rules
* the escape rules with emit_defs/emit_data so the .o bytes stay * emit_data and emit_defs already use. */
* stable. */ 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 static void
emit_data_row(FILE *out, const char *dir, const char *name, u64 v) emit_data_row(FILE *out, const char *dir, const char *name, u64 v)
{ {
fprintf(out, "%s %s(SB),\"", dir, name); fprintf(out, "%s %s(SB),\"", dir, name);
for (int i = 0; i < 8; i++) { for (int i = 0; i < 8; i++)
unsigned b = (unsigned)((v >> (i * 8)) & 0xff); emit_data_byte(out, (u8)((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);
}
fputs("\"\n", out); fputs("\"\n", out);
} }
/* Emit DATAW directives for top-level mutable `let` decls. Only scalar /* Emit `DIR NAME(SB),"<sz zero bytes>"`. Used for top-level str/
* types in `let_scalar_ok` are supported; the rest are silently * slice/struct lets without a baked-in initialiser — the slot is
* skipped at cgen and link with an undefined-symbol error if used. * pre-zeroed and the program writes the real value at runtime. */
* Initialisers must be integer/rune/bool/nil literals (or a `let` static void
* with no init, which zero-initialises). */ 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 static void
emit_lets(Cg *c, FILE *out, Node *file) emit_lets(Cg *c, FILE *out, Node *file)
{ {
for (Node *d = file->list; d; d = d->next) { for (Node *d = file->list; d; d = d->next) {
if (d->kind != N_LET) continue; if (d->kind != N_LET) continue;
if (d->str == NULL || d->str[0] == '\0') continue; if (d->str == NULL || d->str[0] == '\0') continue;
if (!let_scalar_ok(d->type)) continue; int sz = let_emit_size(d->type);
u64 v = 0; 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) { if (d->rhs != NULL) {
Node *r = d->rhs; Node *r = d->rhs;
while (r != NULL && r->kind == N_CAST) r = r->lhs; while (r != NULL && r->kind == N_CAST) r = r->lhs;
if (r == NULL) continue; if (r == NULL) continue;
if (r->kind == N_INTLIT) v = r->uval; int empty_str = (r->kind == N_STRLIT && r->strlen == 0);
else if (r->kind == N_RUNELIT) v = r->uval; if (r->kind != N_NIL && !empty_str)
else if (r->kind == N_TRUE) v = 1; continue;
else if (r->kind == N_FALSE) v = 0;
else if (r->kind == N_NIL) v = 0;
else continue; /* non-literal init: skip */
} }
emit_data_row(out, "DATAW", mod_mangle(c, d->str), v); emit_data_row_zero(out, "DATAW", mod_mangle(c, d->str), sz);
} }
} }

View File

@@ -6129,11 +6129,20 @@ fn cgident(c: *cgen, n: *node) void = {
return; return;
}; };
// Top-level mutable `let` — RIP-relative load from its DATAW // Top-level mutable `let` — RIP-relative load from its DATAW
// slot. Mirrors C cgen's catch-all `MOVQ masym(s), AX`. Names // slot. Mirrors C cgen's catch-all `MOVQ masym(s), AX` for
// that aren't lets either (typos, never-defined) flow through // scalar lets, plus the (LEAQ, MOVQ, MOVQ) sequence for str
// here too in C; the divergence today is bounded to scalar lets, // globals so both ptr and len land in (AX, BX). Names that
// which is what `isletvar` gates on. // aren't lets either (typos, never-defined) drop through to
// the silent return.
if (isletvar(c, nm)) { 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"); emitline("\tMOVQ\t");
emitsymname(c, nm); emitsymname(c, nm);
emitline("(SB), AX\n"); emitline("(SB), AX\n");
@@ -6599,6 +6608,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` // Module-qualified value reference: `mod.name` where `mod`
// is nkind.N_IDENT bound as skind.SK_USE and the leaf isn't a local. // 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 // Treat as a SB symbol — `MOVQ leaf(SB), AX`. Same fallback
@@ -7274,10 +7307,20 @@ fn cgassign(c: *cgen, n: *node) void = {
if (off == 0) { if (off == 0) {
// Top-level let target: RIP-relative store // Top-level let target: RIP-relative store
// for `=`, or load→combine→store for the // 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; }; if (!isletvar(c, nm)) { return; };
cgexpr(c, n.rhs); cgexpr(c, n.rhs);
if (n.op == tkind.TK_ASSIGN) { 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, "); emitline("\tMOVQ\tAX, ");
emitsymname(c, nm); emitsymname(c, nm);
emitline("(SB)\n"); emitline("(SB)\n");
@@ -8739,11 +8782,14 @@ type cgen = struct {
}; };
// Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar. // Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar.
// Populated alongside modents; consulted by cgassign and the TK_AMP // Populated alongside modents; consulted by cgassign, cgdot, cgident
// path so reads/writes hit a RIP-relative DATAW slot instead of being // and the TK_AMP path so reads/writes hit a RIP-relative DATAW slot
// silently dropped. Only scalar (≤8B) types make the list. // instead of being silently dropped. tnode is the declared type AST
// node — needed to distinguish scalar (8B) from str (16B) globals
// when picking the load/store sequence.
type letvar = struct { type letvar = struct {
name: str, name: str,
tnode: *node,
lvnext: *letvar, lvnext: *letvar,
}; };
@@ -8967,8 +9013,7 @@ fn internstrlit(c: *cgen, bytes: str) str = {
// letscalarprim — recognise the bare type-name keywords whose values // letscalarprim — recognise the bare type-name keywords whose values
// fit in an 8-byte .data slot and load back with a plain MOVQ. Float // fit in an 8-byte .data slot and load back with a plain MOVQ. Float
// types deliberately excluded; they need MOVSS/MOVSD. Mirrors C // types deliberately excluded; they need MOVSS/MOVSD. Mirrors C
// `let_scalar_ok` for the unwrapped TY_* enumeration. Pointer types // `let_emit_size`'s 8-byte branch.
// (N_TPTR) handle separately at the callsite.
fn letscalarprim(nm: str) bool = { fn letscalarprim(nm: str) bool = {
if (streq(nm, "bool")) { return true; }; if (streq(nm, "bool")) { return true; };
if (streq(nm, "rune")) { return true; }; if (streq(nm, "rune")) { return true; };
@@ -8986,24 +9031,25 @@ fn letscalarprim(nm: str) bool = {
return false; return false;
}; };
// letscalarok — true iff the N_LET decl's declared type lands in the // letemitsize — slot size in bytes for a top-level `let`, or 0 if
// scalar set, walking type aliases. Pointer types are always ok. // the type isn't yet supported as a writable global. Walks type
fn letscalarok(c: *cgen, d: *node) bool = { // aliases so byte output matches C cgen, which resolves Type kinds.
if (d == nil) { return false; }; // 8 → scalar (literal init supported)
// 16 → str (only zero-init / nil / "" supported)
fn letemitsize(c: *cgen, d: *node) i32 = {
if (d == nil) { return 0; };
let t: *node = d.lhs; let t: *node = d.lhs;
for (t != nil) { for (t != nil) {
if (t.kind == nkind.N_TPTR) { return true; }; if (t.kind == nkind.N_TPTR) { return 8; };
if (t.kind != nkind.N_TNAME) { return false; }; if (t.kind != nkind.N_TNAME) { return 0; };
let nm: str = t.str; let nm: str = t.str;
if (letscalarprim(nm)) { return true; }; if (letscalarprim(nm)) { return 8; };
// Resolve a `type x = y;` alias and look again. C cgen if (streq(nm, "str")) { return 16; };
// works on the resolved Type, so byte output diverges
// here without the walk.
let next: *node = aliaslookup(c, nm); let next: *node = aliaslookup(c, nm);
if (next == nil) { return false; }; if (next == nil) { return 0; };
t = next; t = next;
}; };
return false; return 0;
}; };
fn collectlets(c: *cgen, file: *node) void = { fn collectlets(c: *cgen, file: *node) void = {
@@ -9014,9 +9060,10 @@ fn collectlets(c: *cgen, file: *node) void = {
if (d.kind == nkind.N_LET) { if (d.kind == nkind.N_LET) {
let nm: str = d.str; let nm: str = d.str;
if (nm.len > 0) { if (nm.len > 0) {
if (letscalarok(c, d)) { if (letemitsize(c, d) > 0) {
let lv: *letvar = amalloc(c.a, 32u64): *letvar; let lv: *letvar = amalloc(c.a, 48u64): *letvar;
lv.name = nm; lv.name = nm;
lv.tnode = d.lhs;
lv.lvnext = c.lets; lv.lvnext = c.lets;
c.lets = lv; c.lets = lv;
}; };
@@ -9035,18 +9082,79 @@ fn isletvar(c: *cgen, name: str) bool = {
return false; return false;
}; };
// emitletdataw — DATAW directive per top-level scalar `let`. Same // letvarisstr — is the named top-level let a str global? Resolves
// 8-byte LE byte encoding as emitdefconstants; only the directive // aliases to mirror C cgen's `let_isstr`. Used by cgident/cgdot/
// keyword differs ("DATAW" vs "DATA"). Mirrors cmd/w6c/cgen.c // cgassign to pick the (LEAQ, MOVQ, MOVQ) sequence over the bare
// emit_lets — w6a routes DATAW into a writable .data section, and // MOVQ scalar load.
// w6l covers it with a second R+W PT_LOAD. fn letvarisstr(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 (streq(nm, "str")) { return true; };
let nx: *node = aliaslookup(c, nm);
if (nx == nil) { return false; };
t = nx;
};
return false;
};
lv = lv.lvnext;
};
return false;
};
// emitdatawbyte — write one byte of an asm string literal using
// the same escape rules as emitdefconstants / emitdatasection.
fn emitdatawbyte(b: u8) void = {
if (b == 34u8) { emitline("\\\""); return; };
if (b == 92u8) { emitline("\\\\"); return; };
if (b < 32u8) {
emitline("\\x");
let hi: u8 = b >> 4u8;
let lo: u8 = b & 15u8;
let bb: [2]u8;
if (hi < 10u8) { bb[0] = hi + 48u8; }
else { bb[0] = (hi - 10u8) + 97u8; };
if (lo < 10u8) { bb[1] = lo + 48u8; }
else { bb[1] = (lo - 10u8) + 97u8; };
os.write(1, bb.ptr, 2u64);
return;
};
if (b >= 127u8) {
emitline("\\x");
let hi: u8 = b >> 4u8;
let lo: u8 = b & 15u8;
let bb: [2]u8;
if (hi < 10u8) { bb[0] = hi + 48u8; }
else { bb[0] = (hi - 10u8) + 97u8; };
if (lo < 10u8) { bb[1] = lo + 48u8; }
else { bb[1] = (lo - 10u8) + 97u8; };
os.write(1, bb.ptr, 2u64);
return;
};
let bb: [1]u8;
bb[0] = b;
os.write(1, bb.ptr, 1u64);
};
// emitletdataw — DATAW directive per top-level `let` global.
// 8B scalar with int/rune/bool/nil literal init (or no init).
// 16B str with no init (or `nil` / `""`) — zero header; the
// program must assign a real strlit at runtime before
// using .ptr / .len.
// Non-literal scalar inits and non-empty strlit inits are skipped
// so the link surfaces an undefined-symbol error.
fn emitletdataw(c: *cgen, file: *node) void = { fn emitletdataw(c: *cgen, file: *node) void = {
let d: *node = file.list; let d: *node = file.list;
for (d != nil) { for (d != nil) {
if (d.kind == nkind.N_LET) { if (d.kind == nkind.N_LET) {
let nm: str = d.str; let nm: str = d.str;
if (nm.len > 0) { if (nm.len > 0) {
if (letscalarok(c, d)) { let sz: i32 = letemitsize(c, d);
if (sz == 8) {
let v: u64 = 0u64; let v: u64 = 0u64;
let ok: bool = true; let ok: bool = true;
if (d.rhs != nil) { if (d.rhs != nil) {
@@ -9073,37 +9181,35 @@ fn emitletdataw(c: *cgen, file: *node) void = {
for (i < 8) { for (i < 8) {
let b: u8 = (n & 255u64): u8; let b: u8 = (n & 255u64): u8;
n = n >> 8u64; n = n >> 8u64;
if (b == 34u8) { emitline("\\\""); } emitdatawbyte(b);
else { if (b == 92u8) { emitline("\\\\"); } i += 1;
else { };
if (b < 32u8) { emitline("\"\n");
emitline("\\x"); };
let hi: u8 = b >> 4u8; };
let lo: u8 = b & 15u8; if (sz == 16) {
let bb: [2]u8; let ok: bool = true;
if (hi < 10u8) { bb[0] = hi + 48u8; } if (d.rhs != nil) {
else { bb[0] = (hi - 10u8) + 97u8; }; let r: *node = d.rhs;
if (lo < 10u8) { bb[1] = lo + 48u8; } for (r != nil) {
else { bb[1] = (lo - 10u8) + 97u8; }; if (r.kind != nkind.N_CAST) { break; };
os.write(1, bb.ptr, 2u64); r = r.lhs;
} else { };
if (b >= 127u8) { ok = false;
emitline("\\x"); if (r != nil) {
let hi: u8 = b >> 4u8; if (r.kind == nkind.N_NIL) { ok = true; };
let lo: u8 = b & 15u8; if (r.kind == nkind.N_STRLIT) {
let bb: [2]u8; if (r.str.len == 0) { ok = true; };
if (hi < 10u8) { bb[0] = hi + 48u8; } };
else { bb[0] = (hi - 10u8) + 97u8; }; };
if (lo < 10u8) { bb[1] = lo + 48u8; } };
else { bb[1] = (lo - 10u8) + 97u8; }; if (ok) {
os.write(1, bb.ptr, 2u64); emitline("DATAW ");
} else { emitsymname(c, nm);
let bb: [1]u8; emitline("(SB),\"");
bb[0] = b; let i: i32 = 0;
os.write(1, bb.ptr, 1u64); for (i < 16) {
}; emitdatawbyte(0u8);
};
};};
i += 1; i += 1;
}; };
emitline("\"\n"); emitline("\"\n");

View File

@@ -332,11 +332,14 @@ type cgen = struct {
}; };
// Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar. // Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar.
// Populated alongside modents; consulted by cgassign and the TK_AMP // Populated alongside modents; consulted by cgassign, cgdot, cgident
// path so reads/writes hit a RIP-relative DATAW slot instead of being // and the TK_AMP path so reads/writes hit a RIP-relative DATAW slot
// silently dropped. Only scalar (≤8B) types make the list. // instead of being silently dropped. tnode is the declared type AST
// node — needed to distinguish scalar (8B) from str (16B) globals
// when picking the load/store sequence.
type letvar = struct { type letvar = struct {
name: str, name: str,
tnode: *node,
lvnext: *letvar, lvnext: *letvar,
}; };
@@ -560,8 +563,7 @@ fn internstrlit(c: *cgen, bytes: str) str = {
// letscalarprim — recognise the bare type-name keywords whose values // letscalarprim — recognise the bare type-name keywords whose values
// fit in an 8-byte .data slot and load back with a plain MOVQ. Float // fit in an 8-byte .data slot and load back with a plain MOVQ. Float
// types deliberately excluded; they need MOVSS/MOVSD. Mirrors C // types deliberately excluded; they need MOVSS/MOVSD. Mirrors C
// `let_scalar_ok` for the unwrapped TY_* enumeration. Pointer types // `let_emit_size`'s 8-byte branch.
// (N_TPTR) handle separately at the callsite.
fn letscalarprim(nm: str) bool = { fn letscalarprim(nm: str) bool = {
if (streq(nm, "bool")) { return true; }; if (streq(nm, "bool")) { return true; };
if (streq(nm, "rune")) { return true; }; if (streq(nm, "rune")) { return true; };
@@ -579,24 +581,25 @@ fn letscalarprim(nm: str) bool = {
return false; return false;
}; };
// letscalarok — true iff the N_LET decl's declared type lands in the // letemitsize — slot size in bytes for a top-level `let`, or 0 if
// scalar set, walking type aliases. Pointer types are always ok. // the type isn't yet supported as a writable global. Walks type
fn letscalarok(c: *cgen, d: *node) bool = { // aliases so byte output matches C cgen, which resolves Type kinds.
if (d == nil) { return false; }; // 8 → scalar (literal init supported)
// 16 → str (only zero-init / nil / "" supported)
fn letemitsize(c: *cgen, d: *node) i32 = {
if (d == nil) { return 0; };
let t: *node = d.lhs; let t: *node = d.lhs;
for (t != nil) { for (t != nil) {
if (t.kind == nkind.N_TPTR) { return true; }; if (t.kind == nkind.N_TPTR) { return 8; };
if (t.kind != nkind.N_TNAME) { return false; }; if (t.kind != nkind.N_TNAME) { return 0; };
let nm: str = t.str; let nm: str = t.str;
if (letscalarprim(nm)) { return true; }; if (letscalarprim(nm)) { return 8; };
// Resolve a `type x = y;` alias and look again. C cgen if (streq(nm, "str")) { return 16; };
// works on the resolved Type, so byte output diverges
// here without the walk.
let next: *node = aliaslookup(c, nm); let next: *node = aliaslookup(c, nm);
if (next == nil) { return false; }; if (next == nil) { return 0; };
t = next; t = next;
}; };
return false; return 0;
}; };
fn collectlets(c: *cgen, file: *node) void = { fn collectlets(c: *cgen, file: *node) void = {
@@ -607,9 +610,10 @@ fn collectlets(c: *cgen, file: *node) void = {
if (d.kind == nkind.N_LET) { if (d.kind == nkind.N_LET) {
let nm: str = d.str; let nm: str = d.str;
if (nm.len > 0) { if (nm.len > 0) {
if (letscalarok(c, d)) { if (letemitsize(c, d) > 0) {
let lv: *letvar = amalloc(c.a, 32u64): *letvar; let lv: *letvar = amalloc(c.a, 48u64): *letvar;
lv.name = nm; lv.name = nm;
lv.tnode = d.lhs;
lv.lvnext = c.lets; lv.lvnext = c.lets;
c.lets = lv; c.lets = lv;
}; };
@@ -628,18 +632,79 @@ fn isletvar(c: *cgen, name: str) bool = {
return false; return false;
}; };
// emitletdataw — DATAW directive per top-level scalar `let`. Same // letvarisstr — is the named top-level let a str global? Resolves
// 8-byte LE byte encoding as emitdefconstants; only the directive // aliases to mirror C cgen's `let_isstr`. Used by cgident/cgdot/
// keyword differs ("DATAW" vs "DATA"). Mirrors cmd/w6c/cgen.c // cgassign to pick the (LEAQ, MOVQ, MOVQ) sequence over the bare
// emit_lets — w6a routes DATAW into a writable .data section, and // MOVQ scalar load.
// w6l covers it with a second R+W PT_LOAD. fn letvarisstr(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 (streq(nm, "str")) { return true; };
let nx: *node = aliaslookup(c, nm);
if (nx == nil) { return false; };
t = nx;
};
return false;
};
lv = lv.lvnext;
};
return false;
};
// emitdatawbyte — write one byte of an asm string literal using
// the same escape rules as emitdefconstants / emitdatasection.
fn emitdatawbyte(b: u8) void = {
if (b == 34u8) { emitline("\\\""); return; };
if (b == 92u8) { emitline("\\\\"); return; };
if (b < 32u8) {
emitline("\\x");
let hi: u8 = b >> 4u8;
let lo: u8 = b & 15u8;
let bb: [2]u8;
if (hi < 10u8) { bb[0] = hi + 48u8; }
else { bb[0] = (hi - 10u8) + 97u8; };
if (lo < 10u8) { bb[1] = lo + 48u8; }
else { bb[1] = (lo - 10u8) + 97u8; };
os.write(1, bb.ptr, 2u64);
return;
};
if (b >= 127u8) {
emitline("\\x");
let hi: u8 = b >> 4u8;
let lo: u8 = b & 15u8;
let bb: [2]u8;
if (hi < 10u8) { bb[0] = hi + 48u8; }
else { bb[0] = (hi - 10u8) + 97u8; };
if (lo < 10u8) { bb[1] = lo + 48u8; }
else { bb[1] = (lo - 10u8) + 97u8; };
os.write(1, bb.ptr, 2u64);
return;
};
let bb: [1]u8;
bb[0] = b;
os.write(1, bb.ptr, 1u64);
};
// emitletdataw — DATAW directive per top-level `let` global.
// 8B scalar with int/rune/bool/nil literal init (or no init).
// 16B str with no init (or `nil` / `""`) — zero header; the
// program must assign a real strlit at runtime before
// using .ptr / .len.
// Non-literal scalar inits and non-empty strlit inits are skipped
// so the link surfaces an undefined-symbol error.
fn emitletdataw(c: *cgen, file: *node) void = { fn emitletdataw(c: *cgen, file: *node) void = {
let d: *node = file.list; let d: *node = file.list;
for (d != nil) { for (d != nil) {
if (d.kind == nkind.N_LET) { if (d.kind == nkind.N_LET) {
let nm: str = d.str; let nm: str = d.str;
if (nm.len > 0) { if (nm.len > 0) {
if (letscalarok(c, d)) { let sz: i32 = letemitsize(c, d);
if (sz == 8) {
let v: u64 = 0u64; let v: u64 = 0u64;
let ok: bool = true; let ok: bool = true;
if (d.rhs != nil) { if (d.rhs != nil) {
@@ -666,37 +731,35 @@ fn emitletdataw(c: *cgen, file: *node) void = {
for (i < 8) { for (i < 8) {
let b: u8 = (n & 255u64): u8; let b: u8 = (n & 255u64): u8;
n = n >> 8u64; n = n >> 8u64;
if (b == 34u8) { emitline("\\\""); } emitdatawbyte(b);
else { if (b == 92u8) { emitline("\\\\"); } i += 1;
else { };
if (b < 32u8) { emitline("\"\n");
emitline("\\x"); };
let hi: u8 = b >> 4u8; };
let lo: u8 = b & 15u8; if (sz == 16) {
let bb: [2]u8; let ok: bool = true;
if (hi < 10u8) { bb[0] = hi + 48u8; } if (d.rhs != nil) {
else { bb[0] = (hi - 10u8) + 97u8; }; let r: *node = d.rhs;
if (lo < 10u8) { bb[1] = lo + 48u8; } for (r != nil) {
else { bb[1] = (lo - 10u8) + 97u8; }; if (r.kind != nkind.N_CAST) { break; };
os.write(1, bb.ptr, 2u64); r = r.lhs;
} else { };
if (b >= 127u8) { ok = false;
emitline("\\x"); if (r != nil) {
let hi: u8 = b >> 4u8; if (r.kind == nkind.N_NIL) { ok = true; };
let lo: u8 = b & 15u8; if (r.kind == nkind.N_STRLIT) {
let bb: [2]u8; if (r.str.len == 0) { ok = true; };
if (hi < 10u8) { bb[0] = hi + 48u8; } };
else { bb[0] = (hi - 10u8) + 97u8; }; };
if (lo < 10u8) { bb[1] = lo + 48u8; } };
else { bb[1] = (lo - 10u8) + 97u8; }; if (ok) {
os.write(1, bb.ptr, 2u64); emitline("DATAW ");
} else { emitsymname(c, nm);
let bb: [1]u8; emitline("(SB),\"");
bb[0] = b; let i: i32 = 0;
os.write(1, bb.ptr, 1u64); for (i < 16) {
}; emitdatawbyte(0u8);
};
};};
i += 1; i += 1;
}; };
emitline("\"\n"); emitline("\"\n");

View File

@@ -401,11 +401,20 @@ fn cgident(c: *cgen, n: *node) void = {
return; return;
}; };
// Top-level mutable `let` — RIP-relative load from its DATAW // Top-level mutable `let` — RIP-relative load from its DATAW
// slot. Mirrors C cgen's catch-all `MOVQ masym(s), AX`. Names // slot. Mirrors C cgen's catch-all `MOVQ masym(s), AX` for
// that aren't lets either (typos, never-defined) flow through // scalar lets, plus the (LEAQ, MOVQ, MOVQ) sequence for str
// here too in C; the divergence today is bounded to scalar lets, // globals so both ptr and len land in (AX, BX). Names that
// which is what `isletvar` gates on. // aren't lets either (typos, never-defined) drop through to
// the silent return.
if (isletvar(c, nm)) { 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"); emitline("\tMOVQ\t");
emitsymname(c, nm); emitsymname(c, nm);
emitline("(SB), AX\n"); 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` // Module-qualified value reference: `mod.name` where `mod`
// is nkind.N_IDENT bound as skind.SK_USE and the leaf isn't a local. // 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 // 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) { if (off == 0) {
// Top-level let target: RIP-relative store // Top-level let target: RIP-relative store
// for `=`, or load→combine→store for the // 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; }; if (!isletvar(c, nm)) { return; };
cgexpr(c, n.rhs); cgexpr(c, n.rhs);
if (n.op == tkind.TK_ASSIGN) { 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, "); emitline("\tMOVQ\tAX, ");
emitsymname(c, nm); emitsymname(c, nm);
emitline("(SB)\n"); emitline("(SB)\n");

View File

@@ -6129,11 +6129,20 @@ fn cgident(c: *cgen, n: *node) void = {
return; return;
}; };
// Top-level mutable `let` — RIP-relative load from its DATAW // Top-level mutable `let` — RIP-relative load from its DATAW
// slot. Mirrors C cgen's catch-all `MOVQ masym(s), AX`. Names // slot. Mirrors C cgen's catch-all `MOVQ masym(s), AX` for
// that aren't lets either (typos, never-defined) flow through // scalar lets, plus the (LEAQ, MOVQ, MOVQ) sequence for str
// here too in C; the divergence today is bounded to scalar lets, // globals so both ptr and len land in (AX, BX). Names that
// which is what `isletvar` gates on. // aren't lets either (typos, never-defined) drop through to
// the silent return.
if (isletvar(c, nm)) { 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"); emitline("\tMOVQ\t");
emitsymname(c, nm); emitsymname(c, nm);
emitline("(SB), AX\n"); emitline("(SB), AX\n");
@@ -6599,6 +6608,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` // Module-qualified value reference: `mod.name` where `mod`
// is nkind.N_IDENT bound as skind.SK_USE and the leaf isn't a local. // 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 // Treat as a SB symbol — `MOVQ leaf(SB), AX`. Same fallback
@@ -7274,10 +7307,20 @@ fn cgassign(c: *cgen, n: *node) void = {
if (off == 0) { if (off == 0) {
// Top-level let target: RIP-relative store // Top-level let target: RIP-relative store
// for `=`, or load→combine→store for the // 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; }; if (!isletvar(c, nm)) { return; };
cgexpr(c, n.rhs); cgexpr(c, n.rhs);
if (n.op == tkind.TK_ASSIGN) { 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, "); emitline("\tMOVQ\tAX, ");
emitsymname(c, nm); emitsymname(c, nm);
emitline("(SB)\n"); emitline("(SB)\n");
@@ -8739,11 +8782,14 @@ type cgen = struct {
}; };
// Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar. // Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar.
// Populated alongside modents; consulted by cgassign and the TK_AMP // Populated alongside modents; consulted by cgassign, cgdot, cgident
// path so reads/writes hit a RIP-relative DATAW slot instead of being // and the TK_AMP path so reads/writes hit a RIP-relative DATAW slot
// silently dropped. Only scalar (≤8B) types make the list. // instead of being silently dropped. tnode is the declared type AST
// node — needed to distinguish scalar (8B) from str (16B) globals
// when picking the load/store sequence.
type letvar = struct { type letvar = struct {
name: str, name: str,
tnode: *node,
lvnext: *letvar, lvnext: *letvar,
}; };
@@ -8967,8 +9013,7 @@ fn internstrlit(c: *cgen, bytes: str) str = {
// letscalarprim — recognise the bare type-name keywords whose values // letscalarprim — recognise the bare type-name keywords whose values
// fit in an 8-byte .data slot and load back with a plain MOVQ. Float // fit in an 8-byte .data slot and load back with a plain MOVQ. Float
// types deliberately excluded; they need MOVSS/MOVSD. Mirrors C // types deliberately excluded; they need MOVSS/MOVSD. Mirrors C
// `let_scalar_ok` for the unwrapped TY_* enumeration. Pointer types // `let_emit_size`'s 8-byte branch.
// (N_TPTR) handle separately at the callsite.
fn letscalarprim(nm: str) bool = { fn letscalarprim(nm: str) bool = {
if (streq(nm, "bool")) { return true; }; if (streq(nm, "bool")) { return true; };
if (streq(nm, "rune")) { return true; }; if (streq(nm, "rune")) { return true; };
@@ -8986,24 +9031,25 @@ fn letscalarprim(nm: str) bool = {
return false; return false;
}; };
// letscalarok — true iff the N_LET decl's declared type lands in the // letemitsize — slot size in bytes for a top-level `let`, or 0 if
// scalar set, walking type aliases. Pointer types are always ok. // the type isn't yet supported as a writable global. Walks type
fn letscalarok(c: *cgen, d: *node) bool = { // aliases so byte output matches C cgen, which resolves Type kinds.
if (d == nil) { return false; }; // 8 → scalar (literal init supported)
// 16 → str (only zero-init / nil / "" supported)
fn letemitsize(c: *cgen, d: *node) i32 = {
if (d == nil) { return 0; };
let t: *node = d.lhs; let t: *node = d.lhs;
for (t != nil) { for (t != nil) {
if (t.kind == nkind.N_TPTR) { return true; }; if (t.kind == nkind.N_TPTR) { return 8; };
if (t.kind != nkind.N_TNAME) { return false; }; if (t.kind != nkind.N_TNAME) { return 0; };
let nm: str = t.str; let nm: str = t.str;
if (letscalarprim(nm)) { return true; }; if (letscalarprim(nm)) { return 8; };
// Resolve a `type x = y;` alias and look again. C cgen if (streq(nm, "str")) { return 16; };
// works on the resolved Type, so byte output diverges
// here without the walk.
let next: *node = aliaslookup(c, nm); let next: *node = aliaslookup(c, nm);
if (next == nil) { return false; }; if (next == nil) { return 0; };
t = next; t = next;
}; };
return false; return 0;
}; };
fn collectlets(c: *cgen, file: *node) void = { fn collectlets(c: *cgen, file: *node) void = {
@@ -9014,9 +9060,10 @@ fn collectlets(c: *cgen, file: *node) void = {
if (d.kind == nkind.N_LET) { if (d.kind == nkind.N_LET) {
let nm: str = d.str; let nm: str = d.str;
if (nm.len > 0) { if (nm.len > 0) {
if (letscalarok(c, d)) { if (letemitsize(c, d) > 0) {
let lv: *letvar = amalloc(c.a, 32u64): *letvar; let lv: *letvar = amalloc(c.a, 48u64): *letvar;
lv.name = nm; lv.name = nm;
lv.tnode = d.lhs;
lv.lvnext = c.lets; lv.lvnext = c.lets;
c.lets = lv; c.lets = lv;
}; };
@@ -9035,18 +9082,79 @@ fn isletvar(c: *cgen, name: str) bool = {
return false; return false;
}; };
// emitletdataw — DATAW directive per top-level scalar `let`. Same // letvarisstr — is the named top-level let a str global? Resolves
// 8-byte LE byte encoding as emitdefconstants; only the directive // aliases to mirror C cgen's `let_isstr`. Used by cgident/cgdot/
// keyword differs ("DATAW" vs "DATA"). Mirrors cmd/w6c/cgen.c // cgassign to pick the (LEAQ, MOVQ, MOVQ) sequence over the bare
// emit_lets — w6a routes DATAW into a writable .data section, and // MOVQ scalar load.
// w6l covers it with a second R+W PT_LOAD. fn letvarisstr(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 (streq(nm, "str")) { return true; };
let nx: *node = aliaslookup(c, nm);
if (nx == nil) { return false; };
t = nx;
};
return false;
};
lv = lv.lvnext;
};
return false;
};
// emitdatawbyte — write one byte of an asm string literal using
// the same escape rules as emitdefconstants / emitdatasection.
fn emitdatawbyte(b: u8) void = {
if (b == 34u8) { emitline("\\\""); return; };
if (b == 92u8) { emitline("\\\\"); return; };
if (b < 32u8) {
emitline("\\x");
let hi: u8 = b >> 4u8;
let lo: u8 = b & 15u8;
let bb: [2]u8;
if (hi < 10u8) { bb[0] = hi + 48u8; }
else { bb[0] = (hi - 10u8) + 97u8; };
if (lo < 10u8) { bb[1] = lo + 48u8; }
else { bb[1] = (lo - 10u8) + 97u8; };
os.write(1, bb.ptr, 2u64);
return;
};
if (b >= 127u8) {
emitline("\\x");
let hi: u8 = b >> 4u8;
let lo: u8 = b & 15u8;
let bb: [2]u8;
if (hi < 10u8) { bb[0] = hi + 48u8; }
else { bb[0] = (hi - 10u8) + 97u8; };
if (lo < 10u8) { bb[1] = lo + 48u8; }
else { bb[1] = (lo - 10u8) + 97u8; };
os.write(1, bb.ptr, 2u64);
return;
};
let bb: [1]u8;
bb[0] = b;
os.write(1, bb.ptr, 1u64);
};
// emitletdataw — DATAW directive per top-level `let` global.
// 8B scalar with int/rune/bool/nil literal init (or no init).
// 16B str with no init (or `nil` / `""`) — zero header; the
// program must assign a real strlit at runtime before
// using .ptr / .len.
// Non-literal scalar inits and non-empty strlit inits are skipped
// so the link surfaces an undefined-symbol error.
fn emitletdataw(c: *cgen, file: *node) void = { fn emitletdataw(c: *cgen, file: *node) void = {
let d: *node = file.list; let d: *node = file.list;
for (d != nil) { for (d != nil) {
if (d.kind == nkind.N_LET) { if (d.kind == nkind.N_LET) {
let nm: str = d.str; let nm: str = d.str;
if (nm.len > 0) { if (nm.len > 0) {
if (letscalarok(c, d)) { let sz: i32 = letemitsize(c, d);
if (sz == 8) {
let v: u64 = 0u64; let v: u64 = 0u64;
let ok: bool = true; let ok: bool = true;
if (d.rhs != nil) { if (d.rhs != nil) {
@@ -9073,37 +9181,35 @@ fn emitletdataw(c: *cgen, file: *node) void = {
for (i < 8) { for (i < 8) {
let b: u8 = (n & 255u64): u8; let b: u8 = (n & 255u64): u8;
n = n >> 8u64; n = n >> 8u64;
if (b == 34u8) { emitline("\\\""); } emitdatawbyte(b);
else { if (b == 92u8) { emitline("\\\\"); } i += 1;
else { };
if (b < 32u8) { emitline("\"\n");
emitline("\\x"); };
let hi: u8 = b >> 4u8; };
let lo: u8 = b & 15u8; if (sz == 16) {
let bb: [2]u8; let ok: bool = true;
if (hi < 10u8) { bb[0] = hi + 48u8; } if (d.rhs != nil) {
else { bb[0] = (hi - 10u8) + 97u8; }; let r: *node = d.rhs;
if (lo < 10u8) { bb[1] = lo + 48u8; } for (r != nil) {
else { bb[1] = (lo - 10u8) + 97u8; }; if (r.kind != nkind.N_CAST) { break; };
os.write(1, bb.ptr, 2u64); r = r.lhs;
} else { };
if (b >= 127u8) { ok = false;
emitline("\\x"); if (r != nil) {
let hi: u8 = b >> 4u8; if (r.kind == nkind.N_NIL) { ok = true; };
let lo: u8 = b & 15u8; if (r.kind == nkind.N_STRLIT) {
let bb: [2]u8; if (r.str.len == 0) { ok = true; };
if (hi < 10u8) { bb[0] = hi + 48u8; } };
else { bb[0] = (hi - 10u8) + 97u8; }; };
if (lo < 10u8) { bb[1] = lo + 48u8; } };
else { bb[1] = (lo - 10u8) + 97u8; }; if (ok) {
os.write(1, bb.ptr, 2u64); emitline("DATAW ");
} else { emitsymname(c, nm);
let bb: [1]u8; emitline("(SB),\"");
bb[0] = b; let i: i32 = 0;
os.write(1, bb.ptr, 1u64); for (i < 16) {
}; emitdatawbyte(0u8);
};
};};
i += 1; i += 1;
}; };
emitline("\"\n"); emitline("\"\n");

View File

@@ -91,6 +91,28 @@ static const struct fixture fixtures[] = {
"fn main() i32 = { return val: i32; };\n", "fn main() i32 = { return val: i32; };\n",
123, 123,
}, },
{
"str-zeroinit",
/* `let msg: str;` zero-inits the {ptr,len} slot. Assigning
* a strlit at runtime updates both halves; .len then reads
* the stored length. */
"let msg: str;\n"
"fn main() i32 = {\n"
"\tmsg = \"hello\";\n"
"\treturn msg.len: i32;\n"
"};\n",
5,
},
{
"str-reassign",
"let msg: str;\n"
"fn set(s: str) void = { msg = s; };\n"
"fn main() i32 = {\n"
"\tset(\"abcdefg\");\n"
"\treturn msg.len: i32;\n"
"};\n",
7,
},
{ NULL, NULL, 0 } { NULL, NULL, 0 }
}; };