wcc: struct-composite let/def DATA emit via SSoT helper (#129 A.2)

Extract emit_struct_data + emit_struct_lit_bytes helpers (both stages,
mirrored) for module-level let/def with N_STRUCTLIT initializer. Walks
Tfield linked-list in declaration order, zero-fills padding via per-
field offset (rule 13, no hardcoded sizes), dispatches per field kind:
integer via fold_int_literal, float via inline bitcast + sign-XOR byte-
loop (A.1 shape, no INT64_MIN materialised — sibling #144), nested
struct via recursion (#145 inner-field-name-leak gates the test row).
Out-of-scope field kinds (str/slice/ptr/array) fatal loud per rule 7.

LOAD-side widened symmetric to A.1 precedent: cstage cgexpr N_DOT
direct-struct-ident + chained-N_DOT widened via new DefStruct registry
(def_isstructdef populated in let_collect); wwstage cgdot direct-struct-
global falls through to defvarstructinfo on letvarstructinfo nil
(defent.dtnode field added, populated in collectdefs). Both stages
materialise struct-def via LEAQ name(SB) same as struct-let.

Pre-existing cstage scalar 8B short-circuit at emit_lets caused silent
fold-fail-continue on 8B struct lits (`struct{i32,i32}`); gate now
excludes let_isstruct so 8B struct lits route through emit_struct_data.
Wwstage's `!issg` gate was already correct; symmetric ordering restored.

Closes (all bootstrap-NEUTRAL pre-impl; γ-cleanup #40 first consumer):
- emit_lets `is_struct continue` skip → struct lets emitted no DATA
- emit_defs no struct arm → struct defs emitted no DATA
- cstage cgexpr N_DOT for struct-def emitted MOVSXD (BP), AX (broken
  stack-frame read)
- cstage emit_lets sz==8 short-circuit silently skipped 8B struct lits

Test 918 (7 rows: let_int_struct / def_int_struct / let_float_field /
def_float_field / let_empty_struct / let_int_struct_8b / let_norhs_
struct_regression) registered. Make test: 181/181 incl. 990-997 byte-id
+ combined_ww_fresh.

Followups filed:
- #145 (task #41) — nested struct-lit inner field-name leaks as extern
- #42 — wwstage dotchainresolve missing defvarstructinfo lookup (A.2-
  scope-clean today; surfaces post-#145 nested-struct shapes)
- A.3 (task #39) — array static-init audit (parks shape-4 array-in-struct)
- γ-cleanup (task #40) — lib/math const-floatinfo re-fold, blocked-by A.2
This commit is contained in:
2026-05-27 05:04:05 +09:00
parent 1f8fcdc0ee
commit 0ed0b3933c
7 changed files with 1155 additions and 14 deletions

View File

@@ -698,6 +698,19 @@ struct LetVar {
};
static LetVar *letvars;
/* #129 A.2: struct-typed defs that now have DATA storage need the
* same LEAQ-and-field-offset N_DOT-load shape as struct-typed lets.
* Tracked separately so let_islet's existing callers (which gate
* scalar/float/str arms) don't pick up struct defs and re-route their
* narrow-load logic. */
typedef struct DefStruct DefStruct;
struct DefStruct {
const char *name;
Type *type;
DefStruct *next;
};
static DefStruct *defstructs;
/* Slot size for a top-level `let` of type t, or 0 if the type isn't
* supported as a writable global yet. Tagged unions are deferred.
* enums route through their storage type.
@@ -900,19 +913,45 @@ static void
let_collect(Cg *c, Node *file)
{
letvars = NULL;
defstructs = 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_emit_size(d->type) == 0) continue;
LetVar *lv = amalloc(c->a, sizeof *lv);
lv->name = d->str;
lv->type = d->type; /* #128b */
lv->next = letvars;
letvars = lv;
if (d->kind == N_LET) {
if (d->str == NULL || d->str[0] == '\0') continue;
if (let_emit_size(d->type) == 0) continue;
LetVar *lv = amalloc(c->a, sizeof *lv);
lv->name = d->str;
lv->type = d->type;
lv->next = letvars;
letvars = lv;
continue;
}
if (d->kind == N_DEF) {
/* #129 A.2: struct-typed defs now have DATA storage
* (emit_defs struct arm); register them so the N_DOT
* struct-let LEAQ-and-offset shape widens to cover
* them too. Other def kinds (int / float / str)
* stay on their existing load paths. */
if (d->str == NULL || d->str[0] == '\0') continue;
if (!let_isstruct(d->type)) continue;
DefStruct *ds = amalloc(c->a, sizeof *ds);
ds->name = d->str;
ds->type = d->type;
ds->next = defstructs;
defstructs = ds;
}
}
}
static int
def_isstructdef(const char *name)
{
if (name == NULL) return 0;
for (DefStruct *ds = defstructs; ds; ds = ds->next)
if (strcmp(ds->name, name) == 0) return 1;
return 0;
}
static int
let_islet(const char *name)
{
@@ -6117,7 +6156,11 @@ cgexpr(Cg *c, Node *n, Local *locals)
int base_reg = D_BP;
int base_disp = root_off;
int root_resolved = (root_off != 0);
if (!root_resolved && let_islet(cur->str)) {
/* #129 A.2: struct-typed defs (def_isstructdef)
* now have DATA storage and need the same
* LEAQ-and-offset shape as struct lets. */
if (!root_resolved && (let_islet(cur->str)
|| def_isstructdef(cur->str))) {
ins2(c, A_LEAQ,
masym(c, cur->str), areg(D_CX));
base_reg = D_CX;
@@ -6357,7 +6400,14 @@ cgexpr(Cg *c, Node *n, Local *locals)
int is_global = 0;
int base_reg = D_BP;
int base_disp = off;
if (off == 0 && let_islet(n->lhs->str)) {
/* #129 A.2: struct-typed defs now also resolve via
* LEAQ name(SB) (paralleling lets). Pre-A.2 the
* `def_isstructdef` arm fell through to the default
* BP-relative path with off=0, emitting `MOV (BP),`
* which reads the stack frame's first slot instead
* of the def's data section. */
if (off == 0 && (let_islet(n->lhs->str)
|| def_isstructdef(n->lhs->str))) {
ins2(c, A_LEAQ, masym(c, n->lhs->str), areg(D_CX));
is_global = 1;
base_reg = D_CX;
@@ -8473,6 +8523,137 @@ emit_floatlit_data(FILE *out, Cg *c, const char *directive,
return 1;
}
/* emit_struct_lit_bytes — emit the byte sequence for a struct-typed
* top-level let/def whose rhs is an N_STRUCTLIT (or NULL for bare
* no-rhs). Walks Tfield list in declaration order, zero-fills padding
* gaps via the offset table (rule 13), and dispatches per field type:
* integer/bool/nil via fold_int_literal, float via emit_floatlit_data's
* peel+bitcast core inlined, nested struct via recursion (the per-field
* inner literal lookup; nested-struct field-name-leak is a separate
* #145 bug filed against the parser/checker — the recursion is
* unblocked because emit-time field resolution goes through the type
* table, not the parser's symbol table). Array / str / slice / ptr-
* with-address fields are out of #129 A.2 scope — fatals loudly per
* rule-7 so a future consumer gets a precise stop rather than a
* silent zero-emit.
*
* Shared by emit_struct_data (#129 Phase A.2) below; broken out so the
* recursive call can recurse on the inner field bytes without re-
* opening the "DIR name(SB),\"" prefix. */
static int
emit_struct_lit_bytes(FILE *out, Cg *c, Type *t, Node *rhs, u64 base)
{
Type *u = (t && t->kind == TY_NAMED) ? t->under : t;
if (u == NULL || u->kind != TY_STRUCT) return 0;
u64 pos = base;
for (Tfield *f = u->fields; f != NULL; f = f->next) {
u64 fstart = base + f->offset;
while (pos < fstart) {
emit_data_byte(out, 0);
pos++;
}
Node *v = NULL;
if (rhs != NULL) {
for (Node *fn = rhs->list; fn != NULL; fn = fn->next) {
if (fn->str && f->name
&& strcmp(fn->str, f->name) == 0) {
v = fn->lhs;
break;
}
}
}
int fsz = (int)f->type->size;
if (v == NULL) {
for (int i = 0; i < fsz; i++) emit_data_byte(out, 0);
pos += (u64)fsz;
continue;
}
Node *vr = v;
while (vr != NULL && vr->kind == N_CAST) vr = vr->lhs;
Type *fu = (f->type && f->type->kind == TY_NAMED)
? f->type->under : f->type;
if (fu && fu->kind == TY_STRUCT) {
/* Recurse into nested struct lit. Pre-#145 the parser/
* checker has its own gap on inner-N_STRUCTLIT field
* name resolution; this emit recursion goes through
* the type table so it's correct in isolation. */
if (vr == NULL || vr->kind != N_STRUCTLIT)
fatal("emit_struct_lit_bytes: nested struct "
"field '%s' rhs is not N_STRUCTLIT "
"(#129 A.2)", f->name ? f->name : "?");
(void)emit_struct_lit_bytes(out, c, f->type, vr, fstart);
pos = fstart + (u64)fsz;
continue;
}
if (type_isfloat(f->type)) {
int isf32 = type_isf32(f->type);
u64 fv = 0;
int neg = 0;
Node *fr = vr;
if (fr != NULL && fr->kind == N_UN
&& (fr->op == TK_MINUS || fr->op == TK_PLUS)) {
if (fr->op == TK_MINUS) neg = 1;
fr = fr->lhs;
while (fr != NULL && fr->kind == N_CAST)
fr = fr->lhs;
}
if (fr == NULL || fr->kind != N_FLOATLIT)
fatal("emit_struct_lit_bytes: float field "
"'%s' rhs not foldable FLOATLIT (#129 A.2)",
f->name ? f->name : "?");
if (isf32) {
union { float f; u32 u; } x;
x.f = (float)fr->fval;
fv = (u64)x.u;
} else {
union { double d; u64 u; } x;
x.d = fr->fval;
fv = x.u;
}
for (int i = 0; i < fsz; i++) {
u8 b = (u8)((fv >> (i * 8)) & 0xff);
if (neg && i == fsz - 1) b = (u8)(b ^ 0x80);
emit_data_byte(out, b);
}
pos = fstart + (u64)fsz;
continue;
}
u64 iv = 0;
if (!fold_int_literal(vr, &iv))
fatal("emit_struct_lit_bytes: field '%s' rhs not a "
"foldable literal (str/slice/ptr/array fields "
"are out of #129 A.2 scope)",
f->name ? f->name : "?");
for (int i = 0; i < fsz; i++)
emit_data_byte(out, (u8)((iv >> (i * 8)) & 0xff));
pos = fstart + (u64)fsz;
}
/* Tail padding to t->size. */
u64 end = base + t->size;
while (pos < end) {
emit_data_byte(out, 0);
pos++;
}
return 1;
}
/* emit_struct_data — top-level wrapper that opens the DATA/DATAW
* directive and delegates the byte payload to emit_struct_lit_bytes.
* Shared SSoT between emit_lets's struct arm and emit_defs's struct
* arm (#129 Phase A.2, rule-12 sea-of-stars). Returns 1 on emit, 0 if
* the type isn't a struct. */
static int
emit_struct_data(FILE *out, Cg *c, const char *directive,
const char *name, Type *t, Node *rhs)
{
Type *u = (t && t->kind == TY_NAMED) ? t->under : t;
if (u == NULL || u->kind != TY_STRUCT) return 0;
fprintf(out, "%s %s(SB),\"", directive, mod_mangle(c, name));
emit_struct_lit_bytes(out, c, t, rhs, 0);
fputs("\"\n", out);
return 1;
}
static void
emit_lets(Cg *c, FILE *out, Node *file)
{
@@ -8486,7 +8667,14 @@ emit_lets(Cg *c, FILE *out, Node *file)
d->str, d->type, d->rhs);
continue;
}
if (sz == 8 && !let_isarray(d->type)) {
/* #129 A.2: gate `!let_isstruct` so an 8B struct lit
* (`struct { i32, i32 }`, `struct { f32, f32 }`, …) does
* NOT short-circuit through the scalar 8B `fold_int_literal`
* arm — fold-fail-`continue` would otherwise drop the let
* entirely, emitting no DATA and diverging from wwstage's
* emitletdataw (which gates its 8B scalar with `!issg`).
* Symmetric ordering with the wwstage struct arm. */
if (sz == 8 && !let_isarray(d->type) && !let_isstruct(d->type)) {
u64 v = 0;
if (d->rhs != NULL) {
Node *r = d->rhs;
@@ -8592,6 +8780,15 @@ emit_lets(Cg *c, FILE *out, Node *file)
int is_struct = let_isstruct(d->type);
int is_array = let_isarray(d->type);
int empty_str = (r->kind == N_STRLIT && r->strlen == 0);
/* #129 A.2: struct-typed let with N_STRUCTLIT rhs
* routes through the emit_struct_data SSoT. Pre-#129
* this fell through to `continue` and emit-NOTHING,
* so the link surfaced an undefined ref. */
if (is_struct && r->kind == N_STRUCTLIT) {
if (emit_struct_data(out, c, "DATAW", d->str,
d->type, r))
continue;
}
if (is_struct) continue;
if (is_array) continue;
if (r->kind != N_NIL && !empty_str) continue;
@@ -8639,6 +8836,16 @@ emit_defs(Cg *c, FILE *out, Node *file)
d->str, d->type, d->rhs);
continue;
}
/* #129 A.2: struct-typed def with N_STRUCTLIT rhs. Parallel
* to emit_lets's struct arm; uses DATA (read-only) directive.
* Without the LOAD-side widening below the def's address
* still wouldn't be reachable, but storage is the precondition
* for the LOAD path to find something. */
if (let_isstruct(d->type) && d->rhs->kind == N_STRUCTLIT) {
(void)emit_struct_data(out, c, "DATA",
d->str, d->type, d->rhs);
continue;
}
}
(void)c;
}