w6c+selfhost: codegen for top-level mutable let
Third step toward writable globals. The C cgen and its selfhost
mirror now:
- emit DATAW <name>(SB),"<8 LE bytes>" for every top-level `let`
whose type lands in the scalar set (i8..i64/u8..u64/bool/rune/
int/uint/uintptr/ptr; floats and multi-word types deferred);
- drop the "no writable .data" silent-drop guard at the N_IDENT
store path, replacing it with a RIP-relative MOVQ for `=` and
a load→combine→store sequence for the compound ops; and
- route `&name` through LEAQ name(SB) instead of dropping it.
Type aliases resolve via aliaslookup so `type counter = i32; let c:
counter = 0;` still emits a DATAW slot. Non-literal initialisers
silently skip, which surfaces as a clean undefined-symbol error if
the binding is ever referenced.
The selfhost mirror lands in the same commit because test 990
diffs the C cgen against wwdump_ww -c on err.ww (which has
top-level `let nerrors: i32 = 0; ... nerrors += 1;`). Any drift
between the two cgens makes 990 fail. Bootstrap stays at a fixed
point: ww2 == ww3 == ww4 byte-identical.
This commit is contained in:
178
cmd/w6c/cgen.c
178
cmd/w6c/cgen.c
@@ -386,6 +386,40 @@ struct Mod {
|
||||
};
|
||||
static Mod *mod_map;
|
||||
|
||||
/* Top-level `let` map. Populated alongside mod_map; consulted by the
|
||||
* N_IDENT store path and the &-of path to route reads/writes through
|
||||
* a RIP-relative reference rather than dropping them as the (pre-
|
||||
* writable-.data) compiler did. emit_lets emits a DATAW for each. */
|
||||
typedef struct LetVar LetVar;
|
||||
struct LetVar {
|
||||
const char *name;
|
||||
LetVar *next;
|
||||
};
|
||||
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. */
|
||||
static int
|
||||
let_scalar_ok(Type *t)
|
||||
{
|
||||
if (t == NULL) return 0;
|
||||
Type *u = (t->kind == TY_NAMED) ? t->under : t;
|
||||
if (u == NULL) return 0;
|
||||
switch (u->kind) {
|
||||
case TY_BOOL: case TY_RUNE:
|
||||
case TY_I8: case TY_I16: case TY_I32: case TY_I64:
|
||||
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;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
decl_has_ffisym(Node *d)
|
||||
{
|
||||
@@ -430,6 +464,35 @@ mod_lookup(const char *name)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Collect every top-level `let` whose declared type we can store
|
||||
* in a single .data slot. Names not in this map fall through to
|
||||
* the old "drop assignment" path; with a clear link-time
|
||||
* undefined-symbol error on any read. */
|
||||
static void
|
||||
let_collect(Cg *c, Node *file)
|
||||
{
|
||||
letvars = NULL;
|
||||
if (file == NULL) return;
|
||||
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;
|
||||
LetVar *lv = amalloc(c->a, sizeof *lv);
|
||||
lv->name = d->str;
|
||||
lv->next = letvars;
|
||||
letvars = lv;
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
let_islet(const char *name)
|
||||
{
|
||||
if (name == NULL) return 0;
|
||||
for (LetVar *lv = letvars; lv; lv = lv->next)
|
||||
if (strcmp(lv->name, name) == 0) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Mangle an AST identifier into its asm linker symbol:
|
||||
* - @symbol("...") binding wins (return mapped name).
|
||||
* - module-private decl → <module>.<name>.
|
||||
@@ -733,10 +796,17 @@ cgexpr(Cg *c, Node *n, Local *locals)
|
||||
break;
|
||||
}
|
||||
case TK_AMP: {
|
||||
/* address-of for an N_IDENT local */
|
||||
/* address-of for an N_IDENT: local frame slot first,
|
||||
* else a top-level mutable let (RIP-relative LEAQ).
|
||||
* Anything else (e.g. & on an undefined name) silently
|
||||
* drops, matching the pre-existing behaviour. */
|
||||
if (n->lhs->kind == N_IDENT) {
|
||||
int off = localfind(locals, n->lhs->str);
|
||||
ins2(c, A_LEAQ, amem(D_BP, off), areg(D_AX));
|
||||
if (off != 0) {
|
||||
ins2(c, A_LEAQ, amem(D_BP, off), areg(D_AX));
|
||||
} else if (let_islet(n->lhs->str)) {
|
||||
ins2(c, A_LEAQ, masym(c, n->lhs->str), areg(D_AX));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -1367,11 +1437,56 @@ cgexpr(Cg *c, Node *n, Local *locals)
|
||||
}
|
||||
if (n->lhs->kind == N_IDENT) {
|
||||
int off = localfind(locals, n->lhs->str);
|
||||
/* No local match: drop the whole assignment, including
|
||||
* the RHS evaluation. Top-level let mutation isn't
|
||||
* supported (no writable .data) and we don't want to
|
||||
* leak a dead `MOVQ $rhs, AX` into the output. */
|
||||
if (off == 0) break;
|
||||
if (off == 0) {
|
||||
/* Top-level let target — RIP-relative store
|
||||
* (or load→combine→store for compound). Names
|
||||
* we don't recognise as scalar lets fall through
|
||||
* to the existing drop behaviour, which produces
|
||||
* a clean link-time undefined-symbol error if
|
||||
* the binding was ever supposed to exist. */
|
||||
if (!let_islet(n->lhs->str)) break;
|
||||
cgexpr(c, n->rhs, locals);
|
||||
if (n->op == TK_ASSIGN) {
|
||||
ins2(c, A_MOVQ, areg(D_AX),
|
||||
masym(c, n->lhs->str));
|
||||
break;
|
||||
}
|
||||
/* Compound: BX = load; combine with AX; store
|
||||
* BX. The asm has no RIP-relative ADDQ/SUBQ
|
||||
* mem-form, so we use the explicit load→
|
||||
* combine→store sequence uniformly. */
|
||||
ins2(c, A_MOVQ, masym(c, n->lhs->str),
|
||||
areg(D_BX));
|
||||
int did_compound = 1;
|
||||
switch (n->op) {
|
||||
case TK_PLUSEQ: ins2(c, A_ADDQ, areg(D_AX), areg(D_BX)); break;
|
||||
case TK_MINUSEQ: ins2(c, A_SUBQ, areg(D_AX), areg(D_BX)); break;
|
||||
case TK_STAREQ: ins2(c, A_IMULQ, areg(D_AX), areg(D_BX)); break;
|
||||
case TK_AMPEQ: ins2(c, A_ANDQ, areg(D_AX), areg(D_BX)); break;
|
||||
case TK_PIPEEQ: ins2(c, A_ORQ, areg(D_AX), areg(D_BX)); break;
|
||||
case TK_CARETEQ: ins2(c, A_XORQ, areg(D_AX), areg(D_BX)); break;
|
||||
case TK_LSHIFTEQ:
|
||||
ins2(c, A_MOVQ, areg(D_AX), areg(D_CX));
|
||||
ins2(c, A_SHLQ, areg(D_CX), areg(D_BX));
|
||||
break;
|
||||
case TK_RSHIFTEQ:
|
||||
ins2(c, A_MOVQ, areg(D_AX), areg(D_CX));
|
||||
ins2(c, A_SHRQ, areg(D_CX), areg(D_BX));
|
||||
break;
|
||||
default:
|
||||
/* Unsupported compound op: store rhs
|
||||
* directly. Mirrors the local path's
|
||||
* fallback for TK_SLASHEQ etc. */
|
||||
did_compound = 0;
|
||||
ins2(c, A_MOVQ, areg(D_AX),
|
||||
masym(c, n->lhs->str));
|
||||
break;
|
||||
}
|
||||
if (did_compound)
|
||||
ins2(c, A_MOVQ, areg(D_BX),
|
||||
masym(c, n->lhs->str));
|
||||
break;
|
||||
}
|
||||
cgexpr(c, n->rhs, locals);
|
||||
if (n->op == TK_ASSIGN) {
|
||||
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off));
|
||||
@@ -3455,6 +3570,53 @@ 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. */
|
||||
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);
|
||||
}
|
||||
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). */
|
||||
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;
|
||||
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 */
|
||||
}
|
||||
emit_data_row(out, "DATAW", mod_mangle(c, d->str), v);
|
||||
}
|
||||
}
|
||||
|
||||
/* Emit DATA directives for top-level `def` constants whose value is
|
||||
* an integer/rune literal. The w6a side stores the bytes inside .text
|
||||
* and accesses are RIP-relative.
|
||||
@@ -3516,6 +3678,7 @@ cg_file(Cg *c, FILE *out, Node *file)
|
||||
ffi_collect(c, file);
|
||||
mod_collect(c, file);
|
||||
sdef_collect(c, file);
|
||||
let_collect(c, file);
|
||||
strlits = NULL;
|
||||
strlit_seq = 0;
|
||||
for (Node *d = file->list; d; d = d->next) {
|
||||
@@ -3524,6 +3687,7 @@ cg_file(Cg *c, FILE *out, Node *file)
|
||||
}
|
||||
emit_data(c, out);
|
||||
emit_defs(c, out, file);
|
||||
emit_lets(c, out, file);
|
||||
}
|
||||
|
||||
void peephole(Cg *c) { (void)c; }
|
||||
|
||||
Reference in New Issue
Block a user