diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 16d3512e..745ea4a2 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -398,10 +398,10 @@ struct LetVar { static LetVar *letvars; /* 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 struct/ - * tagged unions are deferred. enums route through their storage - * type. Keep this tight — extending it requires the matching - * load/store code below. */ + * supported as a writable global yet. Floats (MOVSS/SD) and 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_emit_size(Type *t) { @@ -419,6 +419,9 @@ let_emit_size(Type *t) return 16; /* {ptr, len}; literal-strlit init NYI. */ case TY_SLICE: return 24; /* {ptr, len, cap}; no init only. */ + case TY_STRUCT: + return (int)u->size; /* zero-init only; field reads/ + * scalar-field writes only. */ default: return 0; } @@ -445,6 +448,26 @@ let_isslice(Type *t) return u && u->kind == TY_SLICE; } +/* Is the unwrapped type a struct? Struct globals only support field + * access (read + plain `=` write for scalar fields). Whole-struct + * by-value flow through expressions isn't wired. */ +static int +let_isstruct(Type *t) +{ + if (t == NULL) return 0; + Type *u = (t->kind == TY_NAMED) ? t->under : t; + return u && u->kind == TY_STRUCT; +} + +/* Returns the unwrapped Type — handy when we need to walk struct + * fields. NULL if t is NULL or unresolved. */ +static Type * +type_unwrap(Type *t) +{ + if (t == NULL) return NULL; + return (t->kind == TY_NAMED) ? t->under : t; +} + static int decl_has_ffisym(Node *d) { @@ -1169,6 +1192,8 @@ cgexpr(Cg *c, Node *n, Local *locals) if (fsz == 1) { load_op = A_MOVZBQ; store_op = A_MOVB; } else if (fsz == 4) { load_op = signed_field ? A_MOVSXD : A_MOVL; store_op = A_MOVL; } int boff = localfind(locals, base->str); + int is_global = (boff == 0 && !via_ptr + && let_islet(base->str)); int foff = (int)f->offset; /* str-typed field: rhs cgexpr leaves (AX=ptr, BX=len); * store both halves at field+0 and field+8. The 8/16 @@ -1184,6 +1209,10 @@ cgexpr(Cg *c, Node *n, Local *locals) ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_CX)); ins2(c, A_MOVQ, areg(D_AX), amem(D_CX, foff + 0)); ins2(c, A_MOVQ, areg(D_BX), amem(D_CX, foff + 8)); + } else if (is_global) { + ins2(c, A_LEAQ, masym(c, base->str), areg(D_CX)); + ins2(c, A_MOVQ, areg(D_AX), amem(D_CX, foff + 0)); + ins2(c, A_MOVQ, areg(D_BX), amem(D_CX, foff + 8)); } else { ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, boff + foff + 0)); ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, boff + foff + 8)); @@ -1195,6 +1224,9 @@ cgexpr(Cg *c, Node *n, Local *locals) if (via_ptr) { ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX)); ins2(c, load_op, amem(D_BX, foff), areg(D_BX)); + } else if (is_global) { + ins2(c, A_LEAQ, masym(c, base->str), areg(D_BX)); + ins2(c, load_op, amem(D_BX, foff), areg(D_BX)); } else { ins2(c, load_op, amem(D_BP, boff + foff), areg(D_BX)); } @@ -1217,6 +1249,9 @@ cgexpr(Cg *c, Node *n, Local *locals) if (via_ptr) { ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX)); ins2(c, store_op, areg(D_AX), amem(D_BX, foff)); + } else if (is_global) { + ins2(c, A_LEAQ, masym(c, base->str), areg(D_BX)); + ins2(c, store_op, areg(D_AX), amem(D_BX, foff)); } else { ins2(c, store_op, areg(D_AX), amem(D_BP, boff + foff)); } @@ -2558,9 +2593,22 @@ cgexpr(Cg *c, Node *n, Local *locals) } break; } - /* real struct field: load at struct_off + field_off */ + /* real struct field: load at struct_base + field_off. + * Base is either a local frame slot (off(BP)) or a top- + * level let global (&name(SB) into CX); we resolve which + * once and then share the field-walk code. */ if (u && u->kind == TY_STRUCT && n->lhs->kind == N_IDENT) { int off = localfind(locals, n->lhs->str); + int is_global = 0; + int base_reg = D_BP; + int base_disp = off; + if (off == 0 && let_islet(n->lhs->str)) { + ins2(c, A_LEAQ, masym(c, n->lhs->str), areg(D_CX)); + is_global = 1; + base_reg = D_CX; + base_disp = 0; + } + (void)is_global; for (Tfield *f = u->fields; f; f = f->next) { if (strcmp(f->name, n->str) != 0) continue; /* str field: load (ptr, len) into (AX, BX) so the @@ -2569,10 +2617,10 @@ cgexpr(Cg *c, Node *n, Local *locals) ? f->type->under : f->type; if (str_fu && str_fu->kind == TY_STR) { ins2(c, A_MOVQ, - amem(D_BP, off + (int)f->offset + 0), + amem(base_reg, base_disp + (int)f->offset + 0), areg(D_AX)); ins2(c, A_MOVQ, - amem(D_BP, off + (int)f->offset + 8), + amem(base_reg, base_disp + (int)f->offset + 8), areg(D_BX)); break; } @@ -2585,7 +2633,7 @@ cgexpr(Cg *c, Node *n, Local *locals) if (fsz == 1) op = A_MOVZBQ; else if (fsz == 4) op = signed_field ? A_MOVSXD : A_MOVL; ins2(c, op, - amem(D_BP, off + (int)f->offset), + amem(base_reg, base_disp + (int)f->offset), areg(D_AX)); break; } @@ -3709,12 +3757,14 @@ emit_lets(Cg *c, FILE *out, Node *file) emit_data_row(out, "DATAW", mod_mangle(c, d->str), v); continue; } - /* Multi-word (str=16, slice=24). 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/slice at runtime - * to use it). */ - if (d->rhs != NULL) { + /* Multi-word (str=16, slice=24, struct=N). 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 / slice / field + * at runtime to use it). Struct literals as init are + * skipped (no compile-time eval), so a non-default init + * surfaces as an undefined-symbol link error. */ + if (d->rhs != NULL && !let_isstruct(d->type)) { Node *r = d->rhs; while (r != NULL && r->kind == N_CAST) r = r->lhs; if (r == NULL) continue; @@ -3722,6 +3772,8 @@ emit_lets(Cg *c, FILE *out, Node *file) if (r->kind != N_NIL && !empty_str) continue; } + if (d->rhs != NULL && let_isstruct(d->type)) + continue; emit_data_row_zero(out, "DATAW", mod_mangle(c, d->str), sz); } } diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 80f21ee8..3f788510 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -6645,6 +6645,42 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; }; + // Top-level struct global field read — LEAQ name(SB), CX then + // load at fi.foff(CX). Mirrors the local "Direct struct local" + // branch above, swapping the BP frame slot for the global VA. + // Field-width-aware op handles MOVQ / MOVL / MOVZBQ / MOVSXD. + if (lhs != nil) { + if (lhs.kind == nkind.N_IDENT) { + let si: *structinfo = letvarstructinfo(c, lhs.str); + if (si != nil) { + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (streq(fi.fname, fld)) { + emitline("\tLEAQ\t"); + emitsymname(c, lhs.str); + emitline("(SB), CX\n"); + if (isstrtype(c, fi.tnode)) { + emitline("\tMOVQ\t"); + emitdispreg(fi.foff: i64, "CX"); + emitline(", AX\n"); + emitline("\tMOVQ\t"); + emitdispreg((fi.foff + 8): i64, "CX"); + emitline(", BX\n"); + } else { + let op: str = fieldloadop(fi); + emitline("\t"); + emitline(op); + emitline("\t"); + emitdispreg(fi.foff: i64, "CX"); + emitline(", AX\n"); + }; + return; + }; + fi = fi.finext; + }; + }; + }; + }; // Module-qualified value reference: `mod.name` where `mod` // is nkind.N_IDENT bound as skind.SK_USE and the leaf isn't a local. // Treat as a SB symbol — `MOVQ leaf(SB), AX`. Same fallback @@ -7245,6 +7281,91 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; + // Top-level struct global field assignment: `g.f = expr;` and + // `g.f += expr;` for a scalar/str field. Reached when the local + // lookup miss but the IDENT base is a registered struct `let`. + // LEAQ name(SB) into BX/CX takes the place of the frame slot + // addressing the local branches use. Compound (PLUSEQ/MINUSEQ) + // follows the same load → push → eval → combine → store shape + // as the via-ptr local path. + if (lhs != nil) { + if (lhs.kind == nkind.N_DOT) { + let base: *node = lhs.lhs; + let fld: str = lhs.str; + if (base != nil) { + if (base.kind == nkind.N_IDENT) { + let bn: str = base.str; + if (localfindnode(c, bn) == nil) { + let si: *structinfo = letvarstructinfo(c, bn); + if (si != nil) { + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (streq(fi.fname, fld)) { + if (n.op == tkind.TK_ASSIGN) { + cgexpr(c, n.rhs); + if (isstrtype(c, fi.tnode)) { + emitline("\tLEAQ\t"); + emitsymname(c, bn); + emitline("(SB), CX\n"); + emitline("\tMOVQ\tAX, "); + emitdispreg(fi.foff: i64, "CX"); + emitline("\n"); + emitline("\tMOVQ\tBX, "); + emitdispreg((fi.foff + 8): i64, "CX"); + emitline("\n"); + return; + }; + let sop: str = fieldstoreop(fi); + emitline("\tLEAQ\t"); + emitsymname(c, bn); + emitline("(SB), BX\n"); + emitline("\t"); + emitline(sop); + emitline("\tAX, "); + emitdispreg(fi.foff: i64, "BX"); + emitline("\n"); + return; + }; + // Compound on scalar field: load + // → push → eval rhs → combine → + // store. cgexpr clobbers BX, so + // re-LEAQ for the store. + let lop: str = fieldloadop(fi); + emitline("\tLEAQ\t"); + emitsymname(c, bn); + emitline("(SB), BX\n"); + emitline("\t"); + emitline(lop); + emitline("\t"); + emitdispreg(fi.foff: i64, "BX"); + emitline(", BX\n"); + emitline("\tPUSHQ\tBX\n"); + cgexpr(c, n.rhs); + emitline("\tPOPQ\tBX\n"); + if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tBX, AX\n"); }; + if (n.op == tkind.TK_MINUSEQ) { + emitline("\tSUBQ\tAX, BX\n"); + emitline("\tMOVQ\tBX, AX\n"); + }; + let sop: str = fieldstoreop(fi); + emitline("\tLEAQ\t"); + emitsymname(c, bn); + emitline("(SB), BX\n"); + emitline("\t"); + emitline(sop); + emitline("\tAX, "); + emitdispreg(fi.foff: i64, "BX"); + emitline("\n"); + return; + }; + fi = fi.finext; + }; + }; + }; + }; + }; + }; + }; // Chained `.field = v` where `` itself is a chain // of dots resolving to a *struct. Mirrors the C cgen branch // added to close trap 1 (cmd/w6c/cgen.c). Without this, only @@ -9047,9 +9168,10 @@ fn letscalarprim(nm: str) bool = { // letemitsize — slot size in bytes for a top-level `let`, or 0 if // the type isn't yet supported as a writable global. Walks type // aliases so byte output matches C cgen, which resolves Type kinds. -// 8 → scalar (literal init supported) -// 16 → str (only zero-init / nil / "" supported) -// 24 → slice (only zero-init supported) +// 8 → scalar (literal init supported) +// 16 → str (only zero-init / nil / "" supported) +// 24 → slice (only zero-init supported) +// varies → struct (zero-init only; field reads/scalar-field writes) fn letemitsize(c: *cgen, d: *node) i32 = { if (d == nil) { return 0; }; let t: *node = d.lhs; @@ -9060,6 +9182,8 @@ fn letemitsize(c: *cgen, d: *node) i32 = { let nm: str = t.str; if (letscalarprim(nm)) { return 8; }; if (streq(nm, "str")) { return 16; }; + let si: *structinfo = structlookup(c, nm); + if (si != nil) { return si.totsize; }; let next: *node = aliaslookup(c, nm); if (next == nil) { return 0; }; t = next; @@ -9139,6 +9263,54 @@ fn letvarisslice(c: *cgen, name: str) bool = { return false; }; +// letvarisstruct — is the named top-level let a struct global? +// Struct globals use LEAQ name(SB), CX as the field-access base; the +// cgdot read and cgassign write paths branch on this to skip the +// frame-relative addressing they use for locals. +fn letvarisstruct(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 (structlookup(c, nm) != nil) { return true; }; + let nx: *node = aliaslookup(c, nm); + if (nx == nil) { return false; }; + t = nx; + }; + return false; + }; + lv = lv.lvnext; + }; + return false; +}; + +// letvarstructinfo — for a struct global, return its structinfo +// so the cgdot/cgassign paths can look up fields. nil if the let +// isn't a struct (or wasn't found). +fn letvarstructinfo(c: *cgen, name: str) *structinfo = { + 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 nil; }; + let nm: str = t.str; + let si: *structinfo = structlookup(c, nm); + if (si != nil) { return si; }; + let nx: *node = aliaslookup(c, nm); + if (nx == nil) { return nil; }; + t = nx; + }; + return nil; + }; + lv = lv.lvnext; + }; + return nil; +}; + // emitdatawbyte — write one byte of an asm string literal using // the same escape rules as emitdefconstants / emitdatasection. fn emitdatawbyte(b: u8) void = { @@ -9187,7 +9359,8 @@ fn emitletdataw(c: *cgen, file: *node) void = { let nm: str = d.str; if (nm.len > 0) { let sz: i32 = letemitsize(c, d); - if (sz == 8) { + let issg: bool = letvarisstruct(c, nm); + if (sz == 8 && !issg) { let v: u64 = 0u64; let ok: bool = true; if (d.rhs != nil) { @@ -9220,7 +9393,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { emitline("\"\n"); }; }; - if (sz == 16) { + if (sz == 16 && !issg) { let ok: bool = true; if (d.rhs != nil) { let r: *node = d.rhs; @@ -9248,7 +9421,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { emitline("\"\n"); }; }; - if (sz == 24) { + if (sz == 24 && !issg) { // Slice: zero-init only (no slice-literal // syntax to honour). Any rhs other than // `nil` is skipped → undefined symbol at @@ -9277,6 +9450,23 @@ fn emitletdataw(c: *cgen, file: *node) void = { emitline("\"\n"); }; }; + // Struct globals — any size, zero-init only. + // A struct literal init isn't compile-time + // evaluated yet; skip and the link will surface + // an undefined-symbol error if referenced. + if (issg) { + if (d.rhs == nil) { + emitline("DATAW "); + emitsymname(c, nm); + emitline("(SB),\""); + let i: i32 = 0; + for (i < sz) { + emitdatawbyte(0u8); + i += 1; + }; + emitline("\"\n"); + }; + }; }; }; d = d.next; diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index d42511a9..f75bc8b0 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -584,9 +584,10 @@ fn letscalarprim(nm: str) bool = { // letemitsize — slot size in bytes for a top-level `let`, or 0 if // the type isn't yet supported as a writable global. Walks type // aliases so byte output matches C cgen, which resolves Type kinds. -// 8 → scalar (literal init supported) -// 16 → str (only zero-init / nil / "" supported) -// 24 → slice (only zero-init supported) +// 8 → scalar (literal init supported) +// 16 → str (only zero-init / nil / "" supported) +// 24 → slice (only zero-init supported) +// varies → struct (zero-init only; field reads/scalar-field writes) fn letemitsize(c: *cgen, d: *node) i32 = { if (d == nil) { return 0; }; let t: *node = d.lhs; @@ -597,6 +598,8 @@ fn letemitsize(c: *cgen, d: *node) i32 = { let nm: str = t.str; if (letscalarprim(nm)) { return 8; }; if (streq(nm, "str")) { return 16; }; + let si: *structinfo = structlookup(c, nm); + if (si != nil) { return si.totsize; }; let next: *node = aliaslookup(c, nm); if (next == nil) { return 0; }; t = next; @@ -676,6 +679,54 @@ fn letvarisslice(c: *cgen, name: str) bool = { return false; }; +// letvarisstruct — is the named top-level let a struct global? +// Struct globals use LEAQ name(SB), CX as the field-access base; the +// cgdot read and cgassign write paths branch on this to skip the +// frame-relative addressing they use for locals. +fn letvarisstruct(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 (structlookup(c, nm) != nil) { return true; }; + let nx: *node = aliaslookup(c, nm); + if (nx == nil) { return false; }; + t = nx; + }; + return false; + }; + lv = lv.lvnext; + }; + return false; +}; + +// letvarstructinfo — for a struct global, return its structinfo +// so the cgdot/cgassign paths can look up fields. nil if the let +// isn't a struct (or wasn't found). +fn letvarstructinfo(c: *cgen, name: str) *structinfo = { + 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 nil; }; + let nm: str = t.str; + let si: *structinfo = structlookup(c, nm); + if (si != nil) { return si; }; + let nx: *node = aliaslookup(c, nm); + if (nx == nil) { return nil; }; + t = nx; + }; + return nil; + }; + lv = lv.lvnext; + }; + return nil; +}; + // emitdatawbyte — write one byte of an asm string literal using // the same escape rules as emitdefconstants / emitdatasection. fn emitdatawbyte(b: u8) void = { @@ -724,7 +775,8 @@ fn emitletdataw(c: *cgen, file: *node) void = { let nm: str = d.str; if (nm.len > 0) { let sz: i32 = letemitsize(c, d); - if (sz == 8) { + let issg: bool = letvarisstruct(c, nm); + if (sz == 8 && !issg) { let v: u64 = 0u64; let ok: bool = true; if (d.rhs != nil) { @@ -757,7 +809,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { emitline("\"\n"); }; }; - if (sz == 16) { + if (sz == 16 && !issg) { let ok: bool = true; if (d.rhs != nil) { let r: *node = d.rhs; @@ -785,7 +837,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { emitline("\"\n"); }; }; - if (sz == 24) { + if (sz == 24 && !issg) { // Slice: zero-init only (no slice-literal // syntax to honour). Any rhs other than // `nil` is skipped → undefined symbol at @@ -814,6 +866,23 @@ fn emitletdataw(c: *cgen, file: *node) void = { emitline("\"\n"); }; }; + // Struct globals — any size, zero-init only. + // A struct literal init isn't compile-time + // evaluated yet; skip and the link will surface + // an undefined-symbol error if referenced. + if (issg) { + if (d.rhs == nil) { + emitline("DATAW "); + emitsymname(c, nm); + emitline("(SB),\""); + let i: i32 = 0; + for (i < sz) { + emitdatawbyte(0u8); + i += 1; + }; + emitline("\"\n"); + }; + }; }; }; d = d.next; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 45e972a4..5207f9ff 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -917,6 +917,42 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; }; + // Top-level struct global field read — LEAQ name(SB), CX then + // load at fi.foff(CX). Mirrors the local "Direct struct local" + // branch above, swapping the BP frame slot for the global VA. + // Field-width-aware op handles MOVQ / MOVL / MOVZBQ / MOVSXD. + if (lhs != nil) { + if (lhs.kind == nkind.N_IDENT) { + let si: *structinfo = letvarstructinfo(c, lhs.str); + if (si != nil) { + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (streq(fi.fname, fld)) { + emitline("\tLEAQ\t"); + emitsymname(c, lhs.str); + emitline("(SB), CX\n"); + if (isstrtype(c, fi.tnode)) { + emitline("\tMOVQ\t"); + emitdispreg(fi.foff: i64, "CX"); + emitline(", AX\n"); + emitline("\tMOVQ\t"); + emitdispreg((fi.foff + 8): i64, "CX"); + emitline(", BX\n"); + } else { + let op: str = fieldloadop(fi); + emitline("\t"); + emitline(op); + emitline("\t"); + emitdispreg(fi.foff: i64, "CX"); + emitline(", AX\n"); + }; + return; + }; + fi = fi.finext; + }; + }; + }; + }; // Module-qualified value reference: `mod.name` where `mod` // is nkind.N_IDENT bound as skind.SK_USE and the leaf isn't a local. // Treat as a SB symbol — `MOVQ leaf(SB), AX`. Same fallback @@ -1517,6 +1553,91 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; + // Top-level struct global field assignment: `g.f = expr;` and + // `g.f += expr;` for a scalar/str field. Reached when the local + // lookup miss but the IDENT base is a registered struct `let`. + // LEAQ name(SB) into BX/CX takes the place of the frame slot + // addressing the local branches use. Compound (PLUSEQ/MINUSEQ) + // follows the same load → push → eval → combine → store shape + // as the via-ptr local path. + if (lhs != nil) { + if (lhs.kind == nkind.N_DOT) { + let base: *node = lhs.lhs; + let fld: str = lhs.str; + if (base != nil) { + if (base.kind == nkind.N_IDENT) { + let bn: str = base.str; + if (localfindnode(c, bn) == nil) { + let si: *structinfo = letvarstructinfo(c, bn); + if (si != nil) { + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (streq(fi.fname, fld)) { + if (n.op == tkind.TK_ASSIGN) { + cgexpr(c, n.rhs); + if (isstrtype(c, fi.tnode)) { + emitline("\tLEAQ\t"); + emitsymname(c, bn); + emitline("(SB), CX\n"); + emitline("\tMOVQ\tAX, "); + emitdispreg(fi.foff: i64, "CX"); + emitline("\n"); + emitline("\tMOVQ\tBX, "); + emitdispreg((fi.foff + 8): i64, "CX"); + emitline("\n"); + return; + }; + let sop: str = fieldstoreop(fi); + emitline("\tLEAQ\t"); + emitsymname(c, bn); + emitline("(SB), BX\n"); + emitline("\t"); + emitline(sop); + emitline("\tAX, "); + emitdispreg(fi.foff: i64, "BX"); + emitline("\n"); + return; + }; + // Compound on scalar field: load + // → push → eval rhs → combine → + // store. cgexpr clobbers BX, so + // re-LEAQ for the store. + let lop: str = fieldloadop(fi); + emitline("\tLEAQ\t"); + emitsymname(c, bn); + emitline("(SB), BX\n"); + emitline("\t"); + emitline(lop); + emitline("\t"); + emitdispreg(fi.foff: i64, "BX"); + emitline(", BX\n"); + emitline("\tPUSHQ\tBX\n"); + cgexpr(c, n.rhs); + emitline("\tPOPQ\tBX\n"); + if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tBX, AX\n"); }; + if (n.op == tkind.TK_MINUSEQ) { + emitline("\tSUBQ\tAX, BX\n"); + emitline("\tMOVQ\tBX, AX\n"); + }; + let sop: str = fieldstoreop(fi); + emitline("\tLEAQ\t"); + emitsymname(c, bn); + emitline("(SB), BX\n"); + emitline("\t"); + emitline(sop); + emitline("\tAX, "); + emitdispreg(fi.foff: i64, "BX"); + emitline("\n"); + return; + }; + fi = fi.finext; + }; + }; + }; + }; + }; + }; + }; // Chained `.field = v` where `` itself is a chain // of dots resolving to a *struct. Mirrors the C cgen branch // added to close trap 1 (cmd/w6c/cgen.c). Without this, only diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 48b31218..056fef29 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -6645,6 +6645,42 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; }; + // Top-level struct global field read — LEAQ name(SB), CX then + // load at fi.foff(CX). Mirrors the local "Direct struct local" + // branch above, swapping the BP frame slot for the global VA. + // Field-width-aware op handles MOVQ / MOVL / MOVZBQ / MOVSXD. + if (lhs != nil) { + if (lhs.kind == nkind.N_IDENT) { + let si: *structinfo = letvarstructinfo(c, lhs.str); + if (si != nil) { + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (streq(fi.fname, fld)) { + emitline("\tLEAQ\t"); + emitsymname(c, lhs.str); + emitline("(SB), CX\n"); + if (isstrtype(c, fi.tnode)) { + emitline("\tMOVQ\t"); + emitdispreg(fi.foff: i64, "CX"); + emitline(", AX\n"); + emitline("\tMOVQ\t"); + emitdispreg((fi.foff + 8): i64, "CX"); + emitline(", BX\n"); + } else { + let op: str = fieldloadop(fi); + emitline("\t"); + emitline(op); + emitline("\t"); + emitdispreg(fi.foff: i64, "CX"); + emitline(", AX\n"); + }; + return; + }; + fi = fi.finext; + }; + }; + }; + }; // Module-qualified value reference: `mod.name` where `mod` // is nkind.N_IDENT bound as skind.SK_USE and the leaf isn't a local. // Treat as a SB symbol — `MOVQ leaf(SB), AX`. Same fallback @@ -7245,6 +7281,91 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; + // Top-level struct global field assignment: `g.f = expr;` and + // `g.f += expr;` for a scalar/str field. Reached when the local + // lookup miss but the IDENT base is a registered struct `let`. + // LEAQ name(SB) into BX/CX takes the place of the frame slot + // addressing the local branches use. Compound (PLUSEQ/MINUSEQ) + // follows the same load → push → eval → combine → store shape + // as the via-ptr local path. + if (lhs != nil) { + if (lhs.kind == nkind.N_DOT) { + let base: *node = lhs.lhs; + let fld: str = lhs.str; + if (base != nil) { + if (base.kind == nkind.N_IDENT) { + let bn: str = base.str; + if (localfindnode(c, bn) == nil) { + let si: *structinfo = letvarstructinfo(c, bn); + if (si != nil) { + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (streq(fi.fname, fld)) { + if (n.op == tkind.TK_ASSIGN) { + cgexpr(c, n.rhs); + if (isstrtype(c, fi.tnode)) { + emitline("\tLEAQ\t"); + emitsymname(c, bn); + emitline("(SB), CX\n"); + emitline("\tMOVQ\tAX, "); + emitdispreg(fi.foff: i64, "CX"); + emitline("\n"); + emitline("\tMOVQ\tBX, "); + emitdispreg((fi.foff + 8): i64, "CX"); + emitline("\n"); + return; + }; + let sop: str = fieldstoreop(fi); + emitline("\tLEAQ\t"); + emitsymname(c, bn); + emitline("(SB), BX\n"); + emitline("\t"); + emitline(sop); + emitline("\tAX, "); + emitdispreg(fi.foff: i64, "BX"); + emitline("\n"); + return; + }; + // Compound on scalar field: load + // → push → eval rhs → combine → + // store. cgexpr clobbers BX, so + // re-LEAQ for the store. + let lop: str = fieldloadop(fi); + emitline("\tLEAQ\t"); + emitsymname(c, bn); + emitline("(SB), BX\n"); + emitline("\t"); + emitline(lop); + emitline("\t"); + emitdispreg(fi.foff: i64, "BX"); + emitline(", BX\n"); + emitline("\tPUSHQ\tBX\n"); + cgexpr(c, n.rhs); + emitline("\tPOPQ\tBX\n"); + if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tBX, AX\n"); }; + if (n.op == tkind.TK_MINUSEQ) { + emitline("\tSUBQ\tAX, BX\n"); + emitline("\tMOVQ\tBX, AX\n"); + }; + let sop: str = fieldstoreop(fi); + emitline("\tLEAQ\t"); + emitsymname(c, bn); + emitline("(SB), BX\n"); + emitline("\t"); + emitline(sop); + emitline("\tAX, "); + emitdispreg(fi.foff: i64, "BX"); + emitline("\n"); + return; + }; + fi = fi.finext; + }; + }; + }; + }; + }; + }; + }; // Chained `.field = v` where `` itself is a chain // of dots resolving to a *struct. Mirrors the C cgen branch // added to close trap 1 (cmd/w6c/cgen.c). Without this, only @@ -9047,9 +9168,10 @@ fn letscalarprim(nm: str) bool = { // letemitsize — slot size in bytes for a top-level `let`, or 0 if // the type isn't yet supported as a writable global. Walks type // aliases so byte output matches C cgen, which resolves Type kinds. -// 8 → scalar (literal init supported) -// 16 → str (only zero-init / nil / "" supported) -// 24 → slice (only zero-init supported) +// 8 → scalar (literal init supported) +// 16 → str (only zero-init / nil / "" supported) +// 24 → slice (only zero-init supported) +// varies → struct (zero-init only; field reads/scalar-field writes) fn letemitsize(c: *cgen, d: *node) i32 = { if (d == nil) { return 0; }; let t: *node = d.lhs; @@ -9060,6 +9182,8 @@ fn letemitsize(c: *cgen, d: *node) i32 = { let nm: str = t.str; if (letscalarprim(nm)) { return 8; }; if (streq(nm, "str")) { return 16; }; + let si: *structinfo = structlookup(c, nm); + if (si != nil) { return si.totsize; }; let next: *node = aliaslookup(c, nm); if (next == nil) { return 0; }; t = next; @@ -9139,6 +9263,54 @@ fn letvarisslice(c: *cgen, name: str) bool = { return false; }; +// letvarisstruct — is the named top-level let a struct global? +// Struct globals use LEAQ name(SB), CX as the field-access base; the +// cgdot read and cgassign write paths branch on this to skip the +// frame-relative addressing they use for locals. +fn letvarisstruct(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 (structlookup(c, nm) != nil) { return true; }; + let nx: *node = aliaslookup(c, nm); + if (nx == nil) { return false; }; + t = nx; + }; + return false; + }; + lv = lv.lvnext; + }; + return false; +}; + +// letvarstructinfo — for a struct global, return its structinfo +// so the cgdot/cgassign paths can look up fields. nil if the let +// isn't a struct (or wasn't found). +fn letvarstructinfo(c: *cgen, name: str) *structinfo = { + 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 nil; }; + let nm: str = t.str; + let si: *structinfo = structlookup(c, nm); + if (si != nil) { return si; }; + let nx: *node = aliaslookup(c, nm); + if (nx == nil) { return nil; }; + t = nx; + }; + return nil; + }; + lv = lv.lvnext; + }; + return nil; +}; + // emitdatawbyte — write one byte of an asm string literal using // the same escape rules as emitdefconstants / emitdatasection. fn emitdatawbyte(b: u8) void = { @@ -9187,7 +9359,8 @@ fn emitletdataw(c: *cgen, file: *node) void = { let nm: str = d.str; if (nm.len > 0) { let sz: i32 = letemitsize(c, d); - if (sz == 8) { + let issg: bool = letvarisstruct(c, nm); + if (sz == 8 && !issg) { let v: u64 = 0u64; let ok: bool = true; if (d.rhs != nil) { @@ -9220,7 +9393,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { emitline("\"\n"); }; }; - if (sz == 16) { + if (sz == 16 && !issg) { let ok: bool = true; if (d.rhs != nil) { let r: *node = d.rhs; @@ -9248,7 +9421,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { emitline("\"\n"); }; }; - if (sz == 24) { + if (sz == 24 && !issg) { // Slice: zero-init only (no slice-literal // syntax to honour). Any rhs other than // `nil` is skipped → undefined symbol at @@ -9277,6 +9450,23 @@ fn emitletdataw(c: *cgen, file: *node) void = { emitline("\"\n"); }; }; + // Struct globals — any size, zero-init only. + // A struct literal init isn't compile-time + // evaluated yet; skip and the link will surface + // an undefined-symbol error if referenced. + if (issg) { + if (d.rhs == nil) { + emitline("DATAW "); + emitsymname(c, nm); + emitline("(SB),\""); + let i: i32 = 0; + for (i < sz) { + emitdatawbyte(0u8); + i += 1; + }; + emitline("\"\n"); + }; + }; }; }; d = d.next; diff --git a/test/wcc/630_let_global.c b/test/wcc/630_let_global.c index e5280570..845f312b 100644 --- a/test/wcc/630_let_global.c +++ b/test/wcc/630_let_global.c @@ -138,6 +138,49 @@ static const struct fixture fixtures[] = { "};\n", 99, }, + { + "struct-field-readwrite", + /* Top-level struct global. Field writes hit the global + * via LEAQ name(SB) + offset; reads pull each field + * with the right width. */ + "type point = struct { x: i32, y: i32 };\n" + "let p: point;\n" + "fn main() i32 = {\n" + "\tp.x = 7;\n" + "\tp.y = 35;\n" + "\treturn p.x + p.y;\n" + "};\n", + 42, + }, + { + "struct-field-compound", + /* Compound += on a struct global field — load via + * LEAQ+disp, push, eval rhs, combine, store. */ + "type counter = struct { n: i64 };\n" + "let c: counter;\n" + "fn bump(d: i64) void = { c.n += d; };\n" + "fn main() i32 = {\n" + "\tbump(10i64);\n" + "\tbump(15i64);\n" + "\tbump(17i64);\n" + "\treturn c.n: i32;\n" + "};\n", + 42, + }, + { + "struct-narrow-field", + /* u8 field on a struct global. Read uses MOVZBQ, store + * uses MOVB. Pre-fix this would have stomped neighbouring + * bytes by emitting MOVQ. */ + "type packet = struct { hdr: u8, body: u32 };\n" + "let pkt: packet;\n" + "fn main() i32 = {\n" + "\tpkt.hdr = 42u8;\n" + "\tpkt.body = 999u32;\n" + "\treturn pkt.hdr: i32;\n" + "};\n", + 42, + }, { NULL, NULL, 0 } };