From 3c812faa08a8f992236cbed58e2bd29efcec4410 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 12 May 2026 11:56:51 +0900 Subject: [PATCH] w6c+selfhost: codegen for top-level mutable `let` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third step toward writable globals. The C cgen and its selfhost mirror now: - emit DATAW (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. --- Makefile | 6 +- cmd/w6c/cgen.c | 178 +++++++++++++++++++++- cmd/w6c/txt.c | 1 + selfhost/cmd/w6c/main.combined.ww | 217 ++++++++++++++++++++++++++- selfhost/cmd/wcc/cgen.ww | 161 ++++++++++++++++++++ selfhost/cmd/wcc/cgendecl.ww | 2 + selfhost/cmd/wcc/cgenexpr.ww | 54 ++++++- selfhost/cmd/wwdump/main.combined.ww | 217 ++++++++++++++++++++++++++- test/wcc/630_let_global.c | 118 +++++++++++++++ 9 files changed, 943 insertions(+), 11 deletions(-) create mode 100644 test/wcc/630_let_global.c diff --git a/Makefile b/Makefile index 054ae060..883408b5 100644 --- a/Makefile +++ b/Makefile @@ -214,7 +214,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_data_link \ $(BIN)/test_arch \ $(BIN)/test_e2e $(BIN)/test_ffi $(BIN)/test_dyn $(BIN)/test_stdlib \ - $(BIN)/test_at_test \ + $(BIN)/test_at_test $(BIN)/test_let_global \ $(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \ $(BIN)/test_w6c_ww $(BIN)/test_ww_ww $(BIN)/test_self_rebuild \ $(BIN)/test_dyn_ww $(BIN)/test_selfcheck $(BIN)/test_at_test_ww @@ -268,6 +268,10 @@ $(BIN)/test_at_test: test/wcc/910_at_test.c $(BIN)/ww $(BIN)/w6c $(BIN)/w6a \ $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_let_global: test/wcc/630_let_global.c $(BIN)/ww $(BIN)/w6c \ + $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_selfhost: test/wcc/990_selfhost.c $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww $(BIN)/wwdump $(BIN)/wwdump_ww $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 04da3fb2..008424e1 100644 --- a/cmd/w6c/cgen.c +++ b/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 → .. @@ -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; } diff --git a/cmd/w6c/txt.c b/cmd/w6c/txt.c index 9ea9cd41..ad9863f5 100644 --- a/cmd/w6c/txt.c +++ b/cmd/w6c/txt.c @@ -25,6 +25,7 @@ anames(int op) case A_NOP: return "NOP"; case A_TEXT: return "TEXT"; case A_DATA: return "DATA"; + case A_DATAW: return "DATAW"; case A_GLOBL: return "GLOBL"; case A_END: return "END"; case A_MOVQ: return "MOVQ"; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 04a44ca1..30bac62c 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -6678,6 +6678,13 @@ fn cgun(c: *cgen, n: *node) void = { emitline("(BP), AX\n"); return; }; + // Top-level mutable let — RIP-relative LEAQ. + if (isletvar(c, nm)) { + emitline("\tLEAQ\t"); + emitsymname(c, nm); + emitline("(SB), AX\n"); + return; + }; }; }; return; @@ -7253,7 +7260,52 @@ fn cgassign(c: *cgen, n: *node) void = { if (lhs.kind == nkind.N_IDENT) { let nm: str = lhs.str; let off: i32 = localfind(c, nm); - if (off == 0) { return; }; + if (off == 0) { + // Top-level let target: RIP-relative store + // for `=`, or load→combine→store for the + // compound forms. + if (!isletvar(c, nm)) { return; }; + cgexpr(c, n.rhs); + if (n.op == tkind.TK_ASSIGN) { + emitline("\tMOVQ\tAX, "); + emitsymname(c, nm); + emitline("(SB)\n"); + return; + }; + emitline("\tMOVQ\t"); + emitsymname(c, nm); + emitline("(SB), BX\n"); + let didcompound: bool = true; + if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tAX, BX\n"); } + else { if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tAX, BX\n"); } + else { if (n.op == tkind.TK_STAREQ) { emitline("\tIMULQ\tAX, BX\n"); } + else { if (n.op == tkind.TK_AMPEQ) { emitline("\tANDQ\tAX, BX\n"); } + else { if (n.op == tkind.TK_PIPEEQ) { emitline("\tORQ\tAX, BX\n"); } + else { if (n.op == tkind.TK_CARETEQ) { emitline("\tXORQ\tAX, BX\n"); } + else { if (n.op == tkind.TK_LSHIFTEQ) { + emitline("\tMOVQ\tAX, CX\n"); + emitline("\tSHLQ\tCX, BX\n"); + } + else { if (n.op == tkind.TK_RSHIFTEQ) { + emitline("\tMOVQ\tAX, CX\n"); + emitline("\tSHRQ\tCX, BX\n"); + } + else { + // Unsupported compound: store rhs + // directly. Mirrors the local path's + // fallback for TK_SLASHEQ etc. + didcompound = false; + emitline("\tMOVQ\tAX, "); + emitsymname(c, nm); + emitline("(SB)\n"); + };};};};};};};}; + if (didcompound) { + emitline("\tMOVQ\tBX, "); + emitsymname(c, nm); + emitline("(SB)\n"); + }; + return; + }; // Detect str-typed local — assignment must store both // halves (AX=ptr at +0, BX=len at +8). let lcstr: bool = false; @@ -8326,6 +8378,7 @@ export fn cgfile(c: *cgen, file: *node) void = { collectfnrets(c, file); fficollect(c, file); collectmods(c, file); + collectlets(c, file); let d: *node = file.list; for (d != nil) { if (d.kind == nkind.N_FNDECL) { @@ -8337,6 +8390,7 @@ export fn cgfile(c: *cgen, file: *node) void = { }; emitdatasection(c); emitdefconstants(c, file); + emitletdataw(c, file); }; // MODULE: wcc @@ -8661,6 +8715,7 @@ type cgen = struct { structs: *structinfo, enums: *enumtype, mods: *modent, // non-exported decls → originating module + lets: *letvar, // top-level mutable scalar `let` bindings fnname: str, fnret: *node, // declared return type of current fn (or nil) looptop: i32, @@ -8672,6 +8727,15 @@ type cgen = struct { deferbuf: **node, // stack of deferred exprs (LIFO at return) }; +// Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar. +// Populated alongside modents; consulted by cgassign and the TK_AMP +// path so reads/writes hit a RIP-relative DATAW slot instead of being +// silently dropped. Only scalar (≤8B) types make the list. +type letvar = struct { + name: str, + lvnext: *letvar, +}; + fn cgeninit(c: *cgen, a: *arena) void = { c.a = a; c.locals = nil; @@ -8889,6 +8953,157 @@ fn internstrlit(c: *cgen, bytes: str) str = { return lab; }; +// letscalarprim — recognise the bare type-name keywords whose values +// fit in an 8-byte .data slot and load back with a plain MOVQ. Float +// types deliberately excluded; they need MOVSS/MOVSD. Mirrors C +// `let_scalar_ok` for the unwrapped TY_* enumeration. Pointer types +// (N_TPTR) handle separately at the callsite. +fn letscalarprim(nm: str) bool = { + if (streq(nm, "bool")) { return true; }; + if (streq(nm, "rune")) { return true; }; + if (streq(nm, "i8")) { return true; }; + if (streq(nm, "i16")) { return true; }; + if (streq(nm, "i32")) { return true; }; + if (streq(nm, "i64")) { return true; }; + if (streq(nm, "u8")) { return true; }; + if (streq(nm, "u16")) { return true; }; + if (streq(nm, "u32")) { return true; }; + if (streq(nm, "u64")) { return true; }; + if (streq(nm, "int")) { return true; }; + if (streq(nm, "uint")) { return true; }; + if (streq(nm, "uintptr")) { return true; }; + return false; +}; + +// letscalarok — true iff the N_LET decl's declared type lands in the +// scalar set, walking type aliases. Pointer types are always ok. +fn letscalarok(c: *cgen, d: *node) bool = { + if (d == nil) { return false; }; + let t: *node = d.lhs; + for (t != nil) { + if (t.kind == nkind.N_TPTR) { return true; }; + if (t.kind != nkind.N_TNAME) { return false; }; + let nm: str = t.str; + if (letscalarprim(nm)) { return true; }; + // Resolve a `type x = y;` alias and look again. C cgen + // works on the resolved Type, so byte output diverges + // here without the walk. + let next: *node = aliaslookup(c, nm); + if (next == nil) { return false; }; + t = next; + }; + return false; +}; + +fn collectlets(c: *cgen, file: *node) void = { + c.lets = nil; + if (file == nil) { return; }; + let d: *node = file.list; + for (d != nil) { + if (d.kind == nkind.N_LET) { + let nm: str = d.str; + if (nm.len > 0) { + if (letscalarok(c, d)) { + let lv: *letvar = amalloc(c.a, 32u64): *letvar; + lv.name = nm; + lv.lvnext = c.lets; + c.lets = lv; + }; + }; + }; + d = d.next; + }; +}; + +fn isletvar(c: *cgen, name: str) bool = { + let lv: *letvar = c.lets; + for (lv != nil) { + if (streq(lv.name, name)) { return true; }; + lv = lv.lvnext; + }; + return false; +}; + +// emitletdataw — DATAW directive per top-level scalar `let`. Same +// 8-byte LE byte encoding as emitdefconstants; only the directive +// keyword differs ("DATAW" vs "DATA"). Mirrors cmd/w6c/cgen.c +// emit_lets — w6a routes DATAW into a writable .data section, and +// w6l covers it with a second R+W PT_LOAD. +fn emitletdataw(c: *cgen, file: *node) void = { + let d: *node = file.list; + for (d != nil) { + if (d.kind == nkind.N_LET) { + let nm: str = d.str; + if (nm.len > 0) { + if (letscalarok(c, d)) { + let v: u64 = 0u64; + let ok: bool = true; + if (d.rhs != nil) { + let r: *node = d.rhs; + for (r != nil) { + if (r.kind != nkind.N_CAST) { break; }; + r = r.lhs; + }; + ok = false; + if (r != nil) { + if (r.kind == nkind.N_INTLIT) { v = r.uval; ok = true; }; + if (r.kind == nkind.N_RUNELIT) { v = r.uval; ok = true; }; + if (r.kind == nkind.N_TRUE) { v = 1u64; ok = true; }; + if (r.kind == nkind.N_FALSE) { v = 0u64; ok = true; }; + if (r.kind == nkind.N_NIL) { v = 0u64; ok = true; }; + }; + }; + if (ok) { + emitline("DATAW "); + emitsymname(c, nm); + emitline("(SB),\""); + let i: i32 = 0; + let n: u64 = v; + for (i < 8) { + let b: u8 = (n & 255u64): u8; + n = n >> 8u64; + if (b == 34u8) { emitline("\\\""); } + else { if (b == 92u8) { emitline("\\\\"); } + else { + 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); + } else { + 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); + } else { + let bb: [1]u8; + bb[0] = b; + os.write(1, bb.ptr, 1u64); + }; + }; + };}; + i += 1; + }; + emitline("\"\n"); + }; + }; + }; + }; + d = d.next; + }; +}; + // emitdefconstants — DATA directive per top-level int-literal `def`. // 8 bytes little-endian to match what the C cgen emits. fn emitdefconstants(c: *cgen, file: *node) void = { diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index 065c2859..87d2430b 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -319,6 +319,7 @@ type cgen = struct { structs: *structinfo, enums: *enumtype, mods: *modent, // non-exported decls → originating module + lets: *letvar, // top-level mutable scalar `let` bindings fnname: str, fnret: *node, // declared return type of current fn (or nil) looptop: i32, @@ -330,6 +331,15 @@ type cgen = struct { deferbuf: **node, // stack of deferred exprs (LIFO at return) }; +// Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar. +// Populated alongside modents; consulted by cgassign and the TK_AMP +// path so reads/writes hit a RIP-relative DATAW slot instead of being +// silently dropped. Only scalar (≤8B) types make the list. +type letvar = struct { + name: str, + lvnext: *letvar, +}; + fn cgeninit(c: *cgen, a: *arena) void = { c.a = a; c.locals = nil; @@ -547,6 +557,157 @@ fn internstrlit(c: *cgen, bytes: str) str = { return lab; }; +// letscalarprim — recognise the bare type-name keywords whose values +// fit in an 8-byte .data slot and load back with a plain MOVQ. Float +// types deliberately excluded; they need MOVSS/MOVSD. Mirrors C +// `let_scalar_ok` for the unwrapped TY_* enumeration. Pointer types +// (N_TPTR) handle separately at the callsite. +fn letscalarprim(nm: str) bool = { + if (streq(nm, "bool")) { return true; }; + if (streq(nm, "rune")) { return true; }; + if (streq(nm, "i8")) { return true; }; + if (streq(nm, "i16")) { return true; }; + if (streq(nm, "i32")) { return true; }; + if (streq(nm, "i64")) { return true; }; + if (streq(nm, "u8")) { return true; }; + if (streq(nm, "u16")) { return true; }; + if (streq(nm, "u32")) { return true; }; + if (streq(nm, "u64")) { return true; }; + if (streq(nm, "int")) { return true; }; + if (streq(nm, "uint")) { return true; }; + if (streq(nm, "uintptr")) { return true; }; + return false; +}; + +// letscalarok — true iff the N_LET decl's declared type lands in the +// scalar set, walking type aliases. Pointer types are always ok. +fn letscalarok(c: *cgen, d: *node) bool = { + if (d == nil) { return false; }; + let t: *node = d.lhs; + for (t != nil) { + if (t.kind == nkind.N_TPTR) { return true; }; + if (t.kind != nkind.N_TNAME) { return false; }; + let nm: str = t.str; + if (letscalarprim(nm)) { return true; }; + // Resolve a `type x = y;` alias and look again. C cgen + // works on the resolved Type, so byte output diverges + // here without the walk. + let next: *node = aliaslookup(c, nm); + if (next == nil) { return false; }; + t = next; + }; + return false; +}; + +fn collectlets(c: *cgen, file: *node) void = { + c.lets = nil; + if (file == nil) { return; }; + let d: *node = file.list; + for (d != nil) { + if (d.kind == nkind.N_LET) { + let nm: str = d.str; + if (nm.len > 0) { + if (letscalarok(c, d)) { + let lv: *letvar = amalloc(c.a, 32u64): *letvar; + lv.name = nm; + lv.lvnext = c.lets; + c.lets = lv; + }; + }; + }; + d = d.next; + }; +}; + +fn isletvar(c: *cgen, name: str) bool = { + let lv: *letvar = c.lets; + for (lv != nil) { + if (streq(lv.name, name)) { return true; }; + lv = lv.lvnext; + }; + return false; +}; + +// emitletdataw — DATAW directive per top-level scalar `let`. Same +// 8-byte LE byte encoding as emitdefconstants; only the directive +// keyword differs ("DATAW" vs "DATA"). Mirrors cmd/w6c/cgen.c +// emit_lets — w6a routes DATAW into a writable .data section, and +// w6l covers it with a second R+W PT_LOAD. +fn emitletdataw(c: *cgen, file: *node) void = { + let d: *node = file.list; + for (d != nil) { + if (d.kind == nkind.N_LET) { + let nm: str = d.str; + if (nm.len > 0) { + if (letscalarok(c, d)) { + let v: u64 = 0u64; + let ok: bool = true; + if (d.rhs != nil) { + let r: *node = d.rhs; + for (r != nil) { + if (r.kind != nkind.N_CAST) { break; }; + r = r.lhs; + }; + ok = false; + if (r != nil) { + if (r.kind == nkind.N_INTLIT) { v = r.uval; ok = true; }; + if (r.kind == nkind.N_RUNELIT) { v = r.uval; ok = true; }; + if (r.kind == nkind.N_TRUE) { v = 1u64; ok = true; }; + if (r.kind == nkind.N_FALSE) { v = 0u64; ok = true; }; + if (r.kind == nkind.N_NIL) { v = 0u64; ok = true; }; + }; + }; + if (ok) { + emitline("DATAW "); + emitsymname(c, nm); + emitline("(SB),\""); + let i: i32 = 0; + let n: u64 = v; + for (i < 8) { + let b: u8 = (n & 255u64): u8; + n = n >> 8u64; + if (b == 34u8) { emitline("\\\""); } + else { if (b == 92u8) { emitline("\\\\"); } + else { + 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); + } else { + 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); + } else { + let bb: [1]u8; + bb[0] = b; + os.write(1, bb.ptr, 1u64); + }; + }; + };}; + i += 1; + }; + emitline("\"\n"); + }; + }; + }; + }; + d = d.next; + }; +}; + // emitdefconstants — DATA directive per top-level int-literal `def`. // 8 bytes little-endian to match what the C cgen emits. fn emitdefconstants(c: *cgen, file: *node) void = { diff --git a/selfhost/cmd/wcc/cgendecl.ww b/selfhost/cmd/wcc/cgendecl.ww index 0e859d98..5723e859 100644 --- a/selfhost/cmd/wcc/cgendecl.ww +++ b/selfhost/cmd/wcc/cgendecl.ww @@ -287,6 +287,7 @@ export fn cgfile(c: *cgen, file: *node) void = { collectfnrets(c, file); fficollect(c, file); collectmods(c, file); + collectlets(c, file); let d: *node = file.list; for (d != nil) { if (d.kind == nkind.N_FNDECL) { @@ -298,4 +299,5 @@ export fn cgfile(c: *cgen, file: *node) void = { }; emitdatasection(c); emitdefconstants(c, file); + emitletdataw(c, file); }; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index a8476962..62ece917 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -950,6 +950,13 @@ fn cgun(c: *cgen, n: *node) void = { emitline("(BP), AX\n"); return; }; + // Top-level mutable let — RIP-relative LEAQ. + if (isletvar(c, nm)) { + emitline("\tLEAQ\t"); + emitsymname(c, nm); + emitline("(SB), AX\n"); + return; + }; }; }; return; @@ -1525,7 +1532,52 @@ fn cgassign(c: *cgen, n: *node) void = { if (lhs.kind == nkind.N_IDENT) { let nm: str = lhs.str; let off: i32 = localfind(c, nm); - if (off == 0) { return; }; + if (off == 0) { + // Top-level let target: RIP-relative store + // for `=`, or load→combine→store for the + // compound forms. + if (!isletvar(c, nm)) { return; }; + cgexpr(c, n.rhs); + if (n.op == tkind.TK_ASSIGN) { + emitline("\tMOVQ\tAX, "); + emitsymname(c, nm); + emitline("(SB)\n"); + return; + }; + emitline("\tMOVQ\t"); + emitsymname(c, nm); + emitline("(SB), BX\n"); + let didcompound: bool = true; + if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tAX, BX\n"); } + else { if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tAX, BX\n"); } + else { if (n.op == tkind.TK_STAREQ) { emitline("\tIMULQ\tAX, BX\n"); } + else { if (n.op == tkind.TK_AMPEQ) { emitline("\tANDQ\tAX, BX\n"); } + else { if (n.op == tkind.TK_PIPEEQ) { emitline("\tORQ\tAX, BX\n"); } + else { if (n.op == tkind.TK_CARETEQ) { emitline("\tXORQ\tAX, BX\n"); } + else { if (n.op == tkind.TK_LSHIFTEQ) { + emitline("\tMOVQ\tAX, CX\n"); + emitline("\tSHLQ\tCX, BX\n"); + } + else { if (n.op == tkind.TK_RSHIFTEQ) { + emitline("\tMOVQ\tAX, CX\n"); + emitline("\tSHRQ\tCX, BX\n"); + } + else { + // Unsupported compound: store rhs + // directly. Mirrors the local path's + // fallback for TK_SLASHEQ etc. + didcompound = false; + emitline("\tMOVQ\tAX, "); + emitsymname(c, nm); + emitline("(SB)\n"); + };};};};};};};}; + if (didcompound) { + emitline("\tMOVQ\tBX, "); + emitsymname(c, nm); + emitline("(SB)\n"); + }; + return; + }; // Detect str-typed local — assignment must store both // halves (AX=ptr at +0, BX=len at +8). let lcstr: bool = false; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index c0c936b8..f823a591 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -6678,6 +6678,13 @@ fn cgun(c: *cgen, n: *node) void = { emitline("(BP), AX\n"); return; }; + // Top-level mutable let — RIP-relative LEAQ. + if (isletvar(c, nm)) { + emitline("\tLEAQ\t"); + emitsymname(c, nm); + emitline("(SB), AX\n"); + return; + }; }; }; return; @@ -7253,7 +7260,52 @@ fn cgassign(c: *cgen, n: *node) void = { if (lhs.kind == nkind.N_IDENT) { let nm: str = lhs.str; let off: i32 = localfind(c, nm); - if (off == 0) { return; }; + if (off == 0) { + // Top-level let target: RIP-relative store + // for `=`, or load→combine→store for the + // compound forms. + if (!isletvar(c, nm)) { return; }; + cgexpr(c, n.rhs); + if (n.op == tkind.TK_ASSIGN) { + emitline("\tMOVQ\tAX, "); + emitsymname(c, nm); + emitline("(SB)\n"); + return; + }; + emitline("\tMOVQ\t"); + emitsymname(c, nm); + emitline("(SB), BX\n"); + let didcompound: bool = true; + if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tAX, BX\n"); } + else { if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tAX, BX\n"); } + else { if (n.op == tkind.TK_STAREQ) { emitline("\tIMULQ\tAX, BX\n"); } + else { if (n.op == tkind.TK_AMPEQ) { emitline("\tANDQ\tAX, BX\n"); } + else { if (n.op == tkind.TK_PIPEEQ) { emitline("\tORQ\tAX, BX\n"); } + else { if (n.op == tkind.TK_CARETEQ) { emitline("\tXORQ\tAX, BX\n"); } + else { if (n.op == tkind.TK_LSHIFTEQ) { + emitline("\tMOVQ\tAX, CX\n"); + emitline("\tSHLQ\tCX, BX\n"); + } + else { if (n.op == tkind.TK_RSHIFTEQ) { + emitline("\tMOVQ\tAX, CX\n"); + emitline("\tSHRQ\tCX, BX\n"); + } + else { + // Unsupported compound: store rhs + // directly. Mirrors the local path's + // fallback for TK_SLASHEQ etc. + didcompound = false; + emitline("\tMOVQ\tAX, "); + emitsymname(c, nm); + emitline("(SB)\n"); + };};};};};};};}; + if (didcompound) { + emitline("\tMOVQ\tBX, "); + emitsymname(c, nm); + emitline("(SB)\n"); + }; + return; + }; // Detect str-typed local — assignment must store both // halves (AX=ptr at +0, BX=len at +8). let lcstr: bool = false; @@ -8326,6 +8378,7 @@ export fn cgfile(c: *cgen, file: *node) void = { collectfnrets(c, file); fficollect(c, file); collectmods(c, file); + collectlets(c, file); let d: *node = file.list; for (d != nil) { if (d.kind == nkind.N_FNDECL) { @@ -8337,6 +8390,7 @@ export fn cgfile(c: *cgen, file: *node) void = { }; emitdatasection(c); emitdefconstants(c, file); + emitletdataw(c, file); }; // MODULE: wcc @@ -8661,6 +8715,7 @@ type cgen = struct { structs: *structinfo, enums: *enumtype, mods: *modent, // non-exported decls → originating module + lets: *letvar, // top-level mutable scalar `let` bindings fnname: str, fnret: *node, // declared return type of current fn (or nil) looptop: i32, @@ -8672,6 +8727,15 @@ type cgen = struct { deferbuf: **node, // stack of deferred exprs (LIFO at return) }; +// Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar. +// Populated alongside modents; consulted by cgassign and the TK_AMP +// path so reads/writes hit a RIP-relative DATAW slot instead of being +// silently dropped. Only scalar (≤8B) types make the list. +type letvar = struct { + name: str, + lvnext: *letvar, +}; + fn cgeninit(c: *cgen, a: *arena) void = { c.a = a; c.locals = nil; @@ -8889,6 +8953,157 @@ fn internstrlit(c: *cgen, bytes: str) str = { return lab; }; +// letscalarprim — recognise the bare type-name keywords whose values +// fit in an 8-byte .data slot and load back with a plain MOVQ. Float +// types deliberately excluded; they need MOVSS/MOVSD. Mirrors C +// `let_scalar_ok` for the unwrapped TY_* enumeration. Pointer types +// (N_TPTR) handle separately at the callsite. +fn letscalarprim(nm: str) bool = { + if (streq(nm, "bool")) { return true; }; + if (streq(nm, "rune")) { return true; }; + if (streq(nm, "i8")) { return true; }; + if (streq(nm, "i16")) { return true; }; + if (streq(nm, "i32")) { return true; }; + if (streq(nm, "i64")) { return true; }; + if (streq(nm, "u8")) { return true; }; + if (streq(nm, "u16")) { return true; }; + if (streq(nm, "u32")) { return true; }; + if (streq(nm, "u64")) { return true; }; + if (streq(nm, "int")) { return true; }; + if (streq(nm, "uint")) { return true; }; + if (streq(nm, "uintptr")) { return true; }; + return false; +}; + +// letscalarok — true iff the N_LET decl's declared type lands in the +// scalar set, walking type aliases. Pointer types are always ok. +fn letscalarok(c: *cgen, d: *node) bool = { + if (d == nil) { return false; }; + let t: *node = d.lhs; + for (t != nil) { + if (t.kind == nkind.N_TPTR) { return true; }; + if (t.kind != nkind.N_TNAME) { return false; }; + let nm: str = t.str; + if (letscalarprim(nm)) { return true; }; + // Resolve a `type x = y;` alias and look again. C cgen + // works on the resolved Type, so byte output diverges + // here without the walk. + let next: *node = aliaslookup(c, nm); + if (next == nil) { return false; }; + t = next; + }; + return false; +}; + +fn collectlets(c: *cgen, file: *node) void = { + c.lets = nil; + if (file == nil) { return; }; + let d: *node = file.list; + for (d != nil) { + if (d.kind == nkind.N_LET) { + let nm: str = d.str; + if (nm.len > 0) { + if (letscalarok(c, d)) { + let lv: *letvar = amalloc(c.a, 32u64): *letvar; + lv.name = nm; + lv.lvnext = c.lets; + c.lets = lv; + }; + }; + }; + d = d.next; + }; +}; + +fn isletvar(c: *cgen, name: str) bool = { + let lv: *letvar = c.lets; + for (lv != nil) { + if (streq(lv.name, name)) { return true; }; + lv = lv.lvnext; + }; + return false; +}; + +// emitletdataw — DATAW directive per top-level scalar `let`. Same +// 8-byte LE byte encoding as emitdefconstants; only the directive +// keyword differs ("DATAW" vs "DATA"). Mirrors cmd/w6c/cgen.c +// emit_lets — w6a routes DATAW into a writable .data section, and +// w6l covers it with a second R+W PT_LOAD. +fn emitletdataw(c: *cgen, file: *node) void = { + let d: *node = file.list; + for (d != nil) { + if (d.kind == nkind.N_LET) { + let nm: str = d.str; + if (nm.len > 0) { + if (letscalarok(c, d)) { + let v: u64 = 0u64; + let ok: bool = true; + if (d.rhs != nil) { + let r: *node = d.rhs; + for (r != nil) { + if (r.kind != nkind.N_CAST) { break; }; + r = r.lhs; + }; + ok = false; + if (r != nil) { + if (r.kind == nkind.N_INTLIT) { v = r.uval; ok = true; }; + if (r.kind == nkind.N_RUNELIT) { v = r.uval; ok = true; }; + if (r.kind == nkind.N_TRUE) { v = 1u64; ok = true; }; + if (r.kind == nkind.N_FALSE) { v = 0u64; ok = true; }; + if (r.kind == nkind.N_NIL) { v = 0u64; ok = true; }; + }; + }; + if (ok) { + emitline("DATAW "); + emitsymname(c, nm); + emitline("(SB),\""); + let i: i32 = 0; + let n: u64 = v; + for (i < 8) { + let b: u8 = (n & 255u64): u8; + n = n >> 8u64; + if (b == 34u8) { emitline("\\\""); } + else { if (b == 92u8) { emitline("\\\\"); } + else { + 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); + } else { + 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); + } else { + let bb: [1]u8; + bb[0] = b; + os.write(1, bb.ptr, 1u64); + }; + }; + };}; + i += 1; + }; + emitline("\"\n"); + }; + }; + }; + }; + d = d.next; + }; +}; + // emitdefconstants — DATA directive per top-level int-literal `def`. // 8 bytes little-endian to match what the C cgen emits. fn emitdefconstants(c: *cgen, file: *node) void = { diff --git a/test/wcc/630_let_global.c b/test/wcc/630_let_global.c new file mode 100644 index 00000000..09451a3e --- /dev/null +++ b/test/wcc/630_let_global.c @@ -0,0 +1,118 @@ +/* + * 630_let_global — top-level mutable `let` end-to-end through the + * full Cstage pipeline (w6c → w6a → w6l). Each fixture is a tiny + * .ww program that exercises one path: literal-init read, plain + * assignment, compound assignment, and address-of. + * + * Exit code = the value the runtime feeds to `exit(2)`. main's + * return value flows into DI via rt/start.s and ends up as the + * shell's $?. + */ +#include +#include +#include +#include + +static int +run_exit(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return -1; +} + +static int +build_and_run(const char *bin, const char *prog) +{ + /* `ww run` builds with the driver (w6c → w6a → w6l + libwwrt.a) + * and executes the result. The exit code propagates back as the + * shell's $? so we just compare against the fixture's want. */ + char src[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/wwt_lg_%d.ww", getpid()); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(prog, f); + fclose(f); + + snprintf(cmd, sizeof cmd, "%s/ww run %s", bin, src); + int rc = run_exit(cmd); + unlink(src); + return rc; +} + +struct fixture { + const char *label; + const char *prog; + int want; +}; + +static const struct fixture fixtures[] = { + { + "read", + "let counter: i32 = 42;\n" + "fn main() i32 = { return counter; };\n", + 42, + }, + { + "assign", + "let counter: i32 = 0;\n" + "fn main() i32 = { counter = 99; return counter; };\n", + 99, + }, + { + "compound", + "let counter: i32 = 1;\n" + "fn inc() void = { counter += 1; };\n" + "fn main() i32 = { inc(); inc(); inc(); return counter; };\n", + 4, + }, + { + "zero-init", + /* No initialiser → zero-filled DATAW slot. */ + "let counter: i32;\n" + "fn main() i32 = { counter = 7; return counter; };\n", + 7, + }, + { + "addr-of", + "let counter: i32 = 11;\n" + "fn main() i32 = {\n" + "\tlet p: *i32 = &counter;\n" + "\t*p = 55;\n" + "\treturn counter;\n" + "};\n", + 55, + }, + { + "u64-read", + "let val: u64 = 123u64;\n" + "fn main() i32 = { return val: i32; };\n", + 123, + }, + { NULL, NULL, 0 } +}; + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + + int fail = 0, ran = 0; + for (int i = 0; fixtures[i].label; i++, ran++) { + int got = build_and_run(bin, fixtures[i].prog); + if (got != fixtures[i].want) { + fprintf(stderr, "let_global[%s]: exit=%d, want %d\n", + fixtures[i].label, got, fixtures[i].want); + fail++; + } + } + if (fail) { + fprintf(stderr, "let_global: %d/%d fixtures failed\n", fail, ran); + return 1; + } + printf("let_global: %d/%d ok\n", ran, ran); + return 0; +}