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

@@ -340,6 +340,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
$(BIN)/test_arr_module_index_run \
$(BIN)/test_arr_float_call_index_run \
$(BIN)/test_def_float_lit_run \
$(BIN)/test_struct_composite_init_run \
$(BIN)/test_f64cgen_run \
$(BIN)/test_f64crossmod_run \
$(BIN)/test_tuprecv_run \
@@ -1153,6 +1154,11 @@ $(BIN)/test_def_float_lit_run: test/wcc/917_def_float_lit_run.c \
$(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
$(BIN)/test_struct_composite_init_run: test/wcc/918_struct_composite_init_run.c \
$(BIN)/ww $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l \
$(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
$(BIN)/test_f64cgen_run: test/wcc/951_f64cgen_run.c $(BIN)/ww $(BIN)/w6c \
$(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<

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;
}

View File

@@ -16583,9 +16583,15 @@ fn cgdot(c: *cgen, n: *node) void = {
// 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.
// #129 A.2: also handles struct-typed `def`s via defvarstructinfo;
// emitstructdata gives them DATA storage at name(SB), and this
// LEAQ-and-offset shape mirrors the let path. Pre-A.2 the def
// fell through to the integer-let MOVQ catch-all (reading garbage
// from the wrong offset).
if (lhs != nil) {
if (lhs.kind == nkind.N_IDENT) {
let si: *structinfo = letvarstructinfo(c, lhs.str);
if (si == nil) { si = defvarstructinfo(c, lhs.str); };
if (si != nil) {
let fi: *fieldinfo = si.fields;
for (fi != nil) {
@@ -24903,6 +24909,36 @@ fn letvarstructinfo(c: *cgen, name: str) *structinfo = {
return nil;
};
// defvarstructinfo — sister of letvarstructinfo for top-level struct
// `def`s. #129 A.2 adds DATA storage for struct-typed defs; the
// LOAD-side cgdot direct-struct-global branch needs to resolve the
// def's structinfo the same way it resolves a let's, so the field-
// offset arithmetic + LEAQ name(SB) routing fires. Walks c.defs and
// the type-spec node (defent.dtnode), aliaslookup-chasing TY_NAMED
// through to the underlying struct name. Returns nil for non-struct
// defs (int/float/str — those use the existing emitsymname-based
// paths).
fn defvarstructinfo(c: *cgen, name: str) *structinfo = {
let e: *defent = c.defs;
for (e != nil) {
if (streq(e.dname, name)) {
let t: *node = e.dtnode;
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;
};
e = e.dnext;
};
return nil;
};
// emitdatawbyte — write one byte of an asm string literal using
// the same escape rules as emitdefconstants / emitdatasection.
fn emitdatawbyte(b: u8) void = {
@@ -25061,6 +25097,160 @@ fn emitfloatlitdata(c: *cgen, directive: str, name: str,
return true;
};
// emitstructlitbytes — payload of a struct-typed top-level let/def
// with N_STRUCTLIT rhs. Walks structt.fields, zero-fills padding via
// the per-field offset (rule 13), dispatches per field type:
// foldintliteral for int/bool/nil, inline bitcast+sign-XOR for float,
// recursive call for nested struct. Other field kinds (str / slice /
// ptr-with-address / array) are out of #129 A.2 scope — rule-7 aborts
// loud rather than silently emitting wrong bytes. Mirror of cstage
// emit_struct_lit_bytes. `base` offsets the field-start computation
// so the recursive call walks an inner struct's fields within its
// outer parent's byte stream.
fn emitstructlitbytes(c: *cgen, structt: *tinfo, rhs: *node,
base: u64) bool = {
let su: *tinfo = structt;
for (su != nil && su.kind == tykind.TY_NAMED) { su = su.under; };
if (su == nil) { return false; };
if (su.kind != tykind.TY_STRUCT) { return false; };
let pos: u64 = base;
let f: *tfield = su.fields;
for (f != nil) {
let fstart: u64 = base + f.offset;
for (pos < fstart) {
emitdatawbyte(0u8);
pos = pos + 1u64;
};
let v: *node = nil;
if (rhs != nil) {
let fnod: *node = rhs.list;
for (fnod != nil) {
if (streq(fnod.str, f.name)) {
v = fnod.lhs;
break;
};
fnod = fnod.next;
};
};
let fsz: i32 = f.type_.size: i32;
if (v == nil) {
let i: i32 = 0;
for (i < fsz) {
emitdatawbyte(0u8);
i = i + 1;
};
pos = fstart + fsz: u64;
f = f.tnext;
continue;
};
let vr: *node = v;
for (vr != nil && vr.kind == nkind.N_CAST) { vr = vr.lhs; };
let fu: *tinfo = f.type_;
for (fu != nil && fu.kind == tykind.TY_NAMED) { fu = fu.under; };
if (fu != nil && fu.kind == tykind.TY_STRUCT) {
if (vr == nil) {
let m: str = "emitstructlitbytes: nested struct field rhs nil (#129 A.2)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
if (vr.kind != nkind.N_STRUCTLIT) {
let m: str = "emitstructlitbytes: nested struct rhs not N_STRUCTLIT (#129 A.2)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
emitstructlitbytes(c, f.type_, vr, fstart);
pos = fstart + fsz: u64;
f = f.tnext;
continue;
};
if (typeisfloat(f.type_)) {
let isf32: bool = (fsz == 4);
let neg: bool = false;
let fr: *node = vr;
if (fr != nil) { if (fr.kind == nkind.N_UN) {
if (fr.op == tkind.TK_MINUS) {
neg = true;
fr = fr.lhs;
for (fr != nil && fr.kind == nkind.N_CAST) { fr = fr.lhs; };
} else { if (fr.op == tkind.TK_PLUS) {
fr = fr.lhs;
for (fr != nil && fr.kind == nkind.N_CAST) { fr = fr.lhs; };
};};
};};
if (fr == nil) {
let m: str = "emitstructlitbytes: float field rhs nil (#129 A.2)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
if (fr.kind != nkind.N_FLOATLIT) {
let m: str = "emitstructlitbytes: float field rhs not FLOATLIT (#129 A.2)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
let bits: u64 = fr.uval;
if (isf32) {
let dv: f64 = *((&bits): *f64);
let fv: f32 = (dv: f32);
let uv: u32 = *((&fv): *u32);
bits = uv: u64;
};
let i: i32 = 0;
let nb: u64 = bits;
for (i < fsz) {
let b: u8 = (nb & 255u64): u8;
if (neg) {
if (i == fsz - 1) { b = b ^ 128u8; };
};
emitdatawbyte(b);
nb = nb >> 8u64;
i = i + 1;
};
pos = fstart + fsz: u64;
f = f.tnext;
continue;
};
let iv: u64 = 0u64;
if (!foldintliteral(vr, &iv)) {
let m: str = "emitstructlitbytes: field rhs not foldable (str/slice/ptr/array out of #129 A.2 scope)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
let i: i32 = 0;
let nb: u64 = iv;
for (i < fsz) {
emitdatawbyte((nb & 255u64): u8);
nb = nb >> 8u64;
i = i + 1;
};
pos = fstart + fsz: u64;
f = f.tnext;
};
let endpos: u64 = base + structt.size;
for (pos < endpos) {
emitdatawbyte(0u8);
pos = pos + 1u64;
};
return true;
};
// emitstructdata — top-level wrapper. Opens the DATA/DATAW directive
// then delegates to emitstructlitbytes. Shared between emitletdataw
// struct arm and emitdefconstants struct arm (#129 A.2).
fn emitstructdata(c: *cgen, directive: str, name: str,
structt: *tinfo, rhs: *node) bool = {
let su: *tinfo = structt;
for (su != nil && su.kind == tykind.TY_NAMED) { su = su.under; };
if (su == nil) { return false; };
if (su.kind != tykind.TY_STRUCT) { return false; };
emitline(directive);
emitline(" ");
emitsymname(c, name);
emitline("(SB),\"");
emitstructlitbytes(c, structt, rhs, 0u64);
emitline("\"\n");
return true;
};
fn emitletdataw(c: *cgen, file: *node) void = {
let d: *node = file.list;
for (d != nil) {
@@ -25080,6 +25270,21 @@ fn emitletdataw(c: *cgen, file: *node) void = {
emitfloatlitdata(c, "DATAW", nm, fsz,
d.rhs);
};
// #129 A.2: struct-typed let with N_STRUCTLIT rhs
// routes through the emitstructdata SSoT helper.
// Pre-A.2 emitletdataw had no struct arm, so the
// declaration fell out of the .data section and
// the link surfaced an undefined-symbol error.
if (issg) {
let r: *node = d.rhs;
if (r != nil) {
if (r.kind == nkind.N_STRUCTLIT) {
let st: *tinfo = d.lhs.type_: *tinfo;
emitstructdata(c, "DATAW", nm,
st, r);
};
};
};
// Skip the scalar 8B path when the global is a
// fixed-size array that just happens to sum to 8
// bytes (e.g. [4]u16, [8]u8) — the array path
@@ -25341,6 +25546,25 @@ fn emitdefconstants(c: *cgen, file: *node) void = {
if (dfsz > 0) {
emitfloatlitdata(c, "DATA", d.str,
dfsz, d.rhs);
} else {
// #129 A.2: struct-typed def with N_STRUCTLIT
// rhs. The checker stamps d.lhs.type_ with the
// struct's tinfo; helper peels TY_NAMED. Parallel
// to emitletdataw struct arm; uses DATA (read-
// only) directive.
if (r != nil) { if (r.kind == nkind.N_STRUCTLIT) {
let st: *tinfo = d.lhs.type_: *tinfo;
let su: *tinfo = st;
for (su != nil && su.kind == tykind.TY_NAMED) {
su = su.under;
};
if (su != nil) {
if (su.kind == tykind.TY_STRUCT) {
emitstructdata(c, "DATA",
d.str, st, r);
};
};
};};
};
};
if (ok) {
@@ -25606,6 +25830,9 @@ type defent = struct {
dname: str,
dmod: str, // originating module (`// MODULE: foo`), or empty
drhs: *node,
dtnode: *node, // #129 A.2: type-spec node (d.lhs); needed for
// struct-def structinfo lookup at the cgdot
// LOAD-side widening site.
dnext: *defent,
};
@@ -25614,7 +25841,7 @@ fn collectdefs(c: *cgen, file: *node) void = {
let d: *node = file.list;
for (d != nil) {
if (d.kind == nkind.N_DEF) {
let e: *defent = alloc(defent{dname=d.str, dmod=d.nmod, drhs=d.rhs, dnext=c.defs})!;
let e: *defent = alloc(defent{dname=d.str, dmod=d.nmod, drhs=d.rhs, dtnode=d.lhs, dnext=c.defs})!;
c.defs = e;
};
d = d.next;

View File

@@ -1114,6 +1114,36 @@ fn letvarstructinfo(c: *cgen, name: str) *structinfo = {
return nil;
};
// defvarstructinfo — sister of letvarstructinfo for top-level struct
// `def`s. #129 A.2 adds DATA storage for struct-typed defs; the
// LOAD-side cgdot direct-struct-global branch needs to resolve the
// def's structinfo the same way it resolves a let's, so the field-
// offset arithmetic + LEAQ name(SB) routing fires. Walks c.defs and
// the type-spec node (defent.dtnode), aliaslookup-chasing TY_NAMED
// through to the underlying struct name. Returns nil for non-struct
// defs (int/float/str — those use the existing emitsymname-based
// paths).
fn defvarstructinfo(c: *cgen, name: str) *structinfo = {
let e: *defent = c.defs;
for (e != nil) {
if (streq(e.dname, name)) {
let t: *node = e.dtnode;
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;
};
e = e.dnext;
};
return nil;
};
// emitdatawbyte — write one byte of an asm string literal using
// the same escape rules as emitdefconstants / emitdatasection.
fn emitdatawbyte(b: u8) void = {
@@ -1272,6 +1302,160 @@ fn emitfloatlitdata(c: *cgen, directive: str, name: str,
return true;
};
// emitstructlitbytes — payload of a struct-typed top-level let/def
// with N_STRUCTLIT rhs. Walks structt.fields, zero-fills padding via
// the per-field offset (rule 13), dispatches per field type:
// foldintliteral for int/bool/nil, inline bitcast+sign-XOR for float,
// recursive call for nested struct. Other field kinds (str / slice /
// ptr-with-address / array) are out of #129 A.2 scope — rule-7 aborts
// loud rather than silently emitting wrong bytes. Mirror of cstage
// emit_struct_lit_bytes. `base` offsets the field-start computation
// so the recursive call walks an inner struct's fields within its
// outer parent's byte stream.
fn emitstructlitbytes(c: *cgen, structt: *tinfo, rhs: *node,
base: u64) bool = {
let su: *tinfo = structt;
for (su != nil && su.kind == tykind.TY_NAMED) { su = su.under; };
if (su == nil) { return false; };
if (su.kind != tykind.TY_STRUCT) { return false; };
let pos: u64 = base;
let f: *tfield = su.fields;
for (f != nil) {
let fstart: u64 = base + f.offset;
for (pos < fstart) {
emitdatawbyte(0u8);
pos = pos + 1u64;
};
let v: *node = nil;
if (rhs != nil) {
let fnod: *node = rhs.list;
for (fnod != nil) {
if (streq(fnod.str, f.name)) {
v = fnod.lhs;
break;
};
fnod = fnod.next;
};
};
let fsz: i32 = f.type_.size: i32;
if (v == nil) {
let i: i32 = 0;
for (i < fsz) {
emitdatawbyte(0u8);
i = i + 1;
};
pos = fstart + fsz: u64;
f = f.tnext;
continue;
};
let vr: *node = v;
for (vr != nil && vr.kind == nkind.N_CAST) { vr = vr.lhs; };
let fu: *tinfo = f.type_;
for (fu != nil && fu.kind == tykind.TY_NAMED) { fu = fu.under; };
if (fu != nil && fu.kind == tykind.TY_STRUCT) {
if (vr == nil) {
let m: str = "emitstructlitbytes: nested struct field rhs nil (#129 A.2)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
if (vr.kind != nkind.N_STRUCTLIT) {
let m: str = "emitstructlitbytes: nested struct rhs not N_STRUCTLIT (#129 A.2)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
emitstructlitbytes(c, f.type_, vr, fstart);
pos = fstart + fsz: u64;
f = f.tnext;
continue;
};
if (typeisfloat(f.type_)) {
let isf32: bool = (fsz == 4);
let neg: bool = false;
let fr: *node = vr;
if (fr != nil) { if (fr.kind == nkind.N_UN) {
if (fr.op == tkind.TK_MINUS) {
neg = true;
fr = fr.lhs;
for (fr != nil && fr.kind == nkind.N_CAST) { fr = fr.lhs; };
} else { if (fr.op == tkind.TK_PLUS) {
fr = fr.lhs;
for (fr != nil && fr.kind == nkind.N_CAST) { fr = fr.lhs; };
};};
};};
if (fr == nil) {
let m: str = "emitstructlitbytes: float field rhs nil (#129 A.2)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
if (fr.kind != nkind.N_FLOATLIT) {
let m: str = "emitstructlitbytes: float field rhs not FLOATLIT (#129 A.2)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
let bits: u64 = fr.uval;
if (isf32) {
let dv: f64 = *((&bits): *f64);
let fv: f32 = (dv: f32);
let uv: u32 = *((&fv): *u32);
bits = uv: u64;
};
let i: i32 = 0;
let nb: u64 = bits;
for (i < fsz) {
let b: u8 = (nb & 255u64): u8;
if (neg) {
if (i == fsz - 1) { b = b ^ 128u8; };
};
emitdatawbyte(b);
nb = nb >> 8u64;
i = i + 1;
};
pos = fstart + fsz: u64;
f = f.tnext;
continue;
};
let iv: u64 = 0u64;
if (!foldintliteral(vr, &iv)) {
let m: str = "emitstructlitbytes: field rhs not foldable (str/slice/ptr/array out of #129 A.2 scope)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
let i: i32 = 0;
let nb: u64 = iv;
for (i < fsz) {
emitdatawbyte((nb & 255u64): u8);
nb = nb >> 8u64;
i = i + 1;
};
pos = fstart + fsz: u64;
f = f.tnext;
};
let endpos: u64 = base + structt.size;
for (pos < endpos) {
emitdatawbyte(0u8);
pos = pos + 1u64;
};
return true;
};
// emitstructdata — top-level wrapper. Opens the DATA/DATAW directive
// then delegates to emitstructlitbytes. Shared between emitletdataw
// struct arm and emitdefconstants struct arm (#129 A.2).
fn emitstructdata(c: *cgen, directive: str, name: str,
structt: *tinfo, rhs: *node) bool = {
let su: *tinfo = structt;
for (su != nil && su.kind == tykind.TY_NAMED) { su = su.under; };
if (su == nil) { return false; };
if (su.kind != tykind.TY_STRUCT) { return false; };
emitline(directive);
emitline(" ");
emitsymname(c, name);
emitline("(SB),\"");
emitstructlitbytes(c, structt, rhs, 0u64);
emitline("\"\n");
return true;
};
fn emitletdataw(c: *cgen, file: *node) void = {
let d: *node = file.list;
for (d != nil) {
@@ -1291,6 +1475,21 @@ fn emitletdataw(c: *cgen, file: *node) void = {
emitfloatlitdata(c, "DATAW", nm, fsz,
d.rhs);
};
// #129 A.2: struct-typed let with N_STRUCTLIT rhs
// routes through the emitstructdata SSoT helper.
// Pre-A.2 emitletdataw had no struct arm, so the
// declaration fell out of the .data section and
// the link surfaced an undefined-symbol error.
if (issg) {
let r: *node = d.rhs;
if (r != nil) {
if (r.kind == nkind.N_STRUCTLIT) {
let st: *tinfo = d.lhs.type_: *tinfo;
emitstructdata(c, "DATAW", nm,
st, r);
};
};
};
// Skip the scalar 8B path when the global is a
// fixed-size array that just happens to sum to 8
// bytes (e.g. [4]u16, [8]u8) — the array path
@@ -1552,6 +1751,25 @@ fn emitdefconstants(c: *cgen, file: *node) void = {
if (dfsz > 0) {
emitfloatlitdata(c, "DATA", d.str,
dfsz, d.rhs);
} else {
// #129 A.2: struct-typed def with N_STRUCTLIT
// rhs. The checker stamps d.lhs.type_ with the
// struct's tinfo; helper peels TY_NAMED. Parallel
// to emitletdataw struct arm; uses DATA (read-
// only) directive.
if (r != nil) { if (r.kind == nkind.N_STRUCTLIT) {
let st: *tinfo = d.lhs.type_: *tinfo;
let su: *tinfo = st;
for (su != nil && su.kind == tykind.TY_NAMED) {
su = su.under;
};
if (su != nil) {
if (su.kind == tykind.TY_STRUCT) {
emitstructdata(c, "DATA",
d.str, st, r);
};
};
};};
};
};
if (ok) {
@@ -1817,6 +2035,9 @@ type defent = struct {
dname: str,
dmod: str, // originating module (`// MODULE: foo`), or empty
drhs: *node,
dtnode: *node, // #129 A.2: type-spec node (d.lhs); needed for
// struct-def structinfo lookup at the cgdot
// LOAD-side widening site.
dnext: *defent,
};
@@ -1825,7 +2046,7 @@ fn collectdefs(c: *cgen, file: *node) void = {
let d: *node = file.list;
for (d != nil) {
if (d.kind == nkind.N_DEF) {
let e: *defent = alloc(defent{dname=d.str, dmod=d.nmod, drhs=d.rhs, dnext=c.defs})!;
let e: *defent = alloc(defent{dname=d.str, dmod=d.nmod, drhs=d.rhs, dtnode=d.lhs, dnext=c.defs})!;
c.defs = e;
};
d = d.next;

View File

@@ -1964,9 +1964,15 @@ fn cgdot(c: *cgen, n: *node) void = {
// 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.
// #129 A.2: also handles struct-typed `def`s via defvarstructinfo;
// emitstructdata gives them DATA storage at name(SB), and this
// LEAQ-and-offset shape mirrors the let path. Pre-A.2 the def
// fell through to the integer-let MOVQ catch-all (reading garbage
// from the wrong offset).
if (lhs != nil) {
if (lhs.kind == nkind.N_IDENT) {
let si: *structinfo = letvarstructinfo(c, lhs.str);
if (si == nil) { si = defvarstructinfo(c, lhs.str); };
if (si != nil) {
let fi: *fieldinfo = si.fields;
for (fi != nil) {

View File

@@ -16583,9 +16583,15 @@ fn cgdot(c: *cgen, n: *node) void = {
// 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.
// #129 A.2: also handles struct-typed `def`s via defvarstructinfo;
// emitstructdata gives them DATA storage at name(SB), and this
// LEAQ-and-offset shape mirrors the let path. Pre-A.2 the def
// fell through to the integer-let MOVQ catch-all (reading garbage
// from the wrong offset).
if (lhs != nil) {
if (lhs.kind == nkind.N_IDENT) {
let si: *structinfo = letvarstructinfo(c, lhs.str);
if (si == nil) { si = defvarstructinfo(c, lhs.str); };
if (si != nil) {
let fi: *fieldinfo = si.fields;
for (fi != nil) {
@@ -24903,6 +24909,36 @@ fn letvarstructinfo(c: *cgen, name: str) *structinfo = {
return nil;
};
// defvarstructinfo — sister of letvarstructinfo for top-level struct
// `def`s. #129 A.2 adds DATA storage for struct-typed defs; the
// LOAD-side cgdot direct-struct-global branch needs to resolve the
// def's structinfo the same way it resolves a let's, so the field-
// offset arithmetic + LEAQ name(SB) routing fires. Walks c.defs and
// the type-spec node (defent.dtnode), aliaslookup-chasing TY_NAMED
// through to the underlying struct name. Returns nil for non-struct
// defs (int/float/str — those use the existing emitsymname-based
// paths).
fn defvarstructinfo(c: *cgen, name: str) *structinfo = {
let e: *defent = c.defs;
for (e != nil) {
if (streq(e.dname, name)) {
let t: *node = e.dtnode;
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;
};
e = e.dnext;
};
return nil;
};
// emitdatawbyte — write one byte of an asm string literal using
// the same escape rules as emitdefconstants / emitdatasection.
fn emitdatawbyte(b: u8) void = {
@@ -25061,6 +25097,160 @@ fn emitfloatlitdata(c: *cgen, directive: str, name: str,
return true;
};
// emitstructlitbytes — payload of a struct-typed top-level let/def
// with N_STRUCTLIT rhs. Walks structt.fields, zero-fills padding via
// the per-field offset (rule 13), dispatches per field type:
// foldintliteral for int/bool/nil, inline bitcast+sign-XOR for float,
// recursive call for nested struct. Other field kinds (str / slice /
// ptr-with-address / array) are out of #129 A.2 scope — rule-7 aborts
// loud rather than silently emitting wrong bytes. Mirror of cstage
// emit_struct_lit_bytes. `base` offsets the field-start computation
// so the recursive call walks an inner struct's fields within its
// outer parent's byte stream.
fn emitstructlitbytes(c: *cgen, structt: *tinfo, rhs: *node,
base: u64) bool = {
let su: *tinfo = structt;
for (su != nil && su.kind == tykind.TY_NAMED) { su = su.under; };
if (su == nil) { return false; };
if (su.kind != tykind.TY_STRUCT) { return false; };
let pos: u64 = base;
let f: *tfield = su.fields;
for (f != nil) {
let fstart: u64 = base + f.offset;
for (pos < fstart) {
emitdatawbyte(0u8);
pos = pos + 1u64;
};
let v: *node = nil;
if (rhs != nil) {
let fnod: *node = rhs.list;
for (fnod != nil) {
if (streq(fnod.str, f.name)) {
v = fnod.lhs;
break;
};
fnod = fnod.next;
};
};
let fsz: i32 = f.type_.size: i32;
if (v == nil) {
let i: i32 = 0;
for (i < fsz) {
emitdatawbyte(0u8);
i = i + 1;
};
pos = fstart + fsz: u64;
f = f.tnext;
continue;
};
let vr: *node = v;
for (vr != nil && vr.kind == nkind.N_CAST) { vr = vr.lhs; };
let fu: *tinfo = f.type_;
for (fu != nil && fu.kind == tykind.TY_NAMED) { fu = fu.under; };
if (fu != nil && fu.kind == tykind.TY_STRUCT) {
if (vr == nil) {
let m: str = "emitstructlitbytes: nested struct field rhs nil (#129 A.2)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
if (vr.kind != nkind.N_STRUCTLIT) {
let m: str = "emitstructlitbytes: nested struct rhs not N_STRUCTLIT (#129 A.2)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
emitstructlitbytes(c, f.type_, vr, fstart);
pos = fstart + fsz: u64;
f = f.tnext;
continue;
};
if (typeisfloat(f.type_)) {
let isf32: bool = (fsz == 4);
let neg: bool = false;
let fr: *node = vr;
if (fr != nil) { if (fr.kind == nkind.N_UN) {
if (fr.op == tkind.TK_MINUS) {
neg = true;
fr = fr.lhs;
for (fr != nil && fr.kind == nkind.N_CAST) { fr = fr.lhs; };
} else { if (fr.op == tkind.TK_PLUS) {
fr = fr.lhs;
for (fr != nil && fr.kind == nkind.N_CAST) { fr = fr.lhs; };
};};
};};
if (fr == nil) {
let m: str = "emitstructlitbytes: float field rhs nil (#129 A.2)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
if (fr.kind != nkind.N_FLOATLIT) {
let m: str = "emitstructlitbytes: float field rhs not FLOATLIT (#129 A.2)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
let bits: u64 = fr.uval;
if (isf32) {
let dv: f64 = *((&bits): *f64);
let fv: f32 = (dv: f32);
let uv: u32 = *((&fv): *u32);
bits = uv: u64;
};
let i: i32 = 0;
let nb: u64 = bits;
for (i < fsz) {
let b: u8 = (nb & 255u64): u8;
if (neg) {
if (i == fsz - 1) { b = b ^ 128u8; };
};
emitdatawbyte(b);
nb = nb >> 8u64;
i = i + 1;
};
pos = fstart + fsz: u64;
f = f.tnext;
continue;
};
let iv: u64 = 0u64;
if (!foldintliteral(vr, &iv)) {
let m: str = "emitstructlitbytes: field rhs not foldable (str/slice/ptr/array out of #129 A.2 scope)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
let i: i32 = 0;
let nb: u64 = iv;
for (i < fsz) {
emitdatawbyte((nb & 255u64): u8);
nb = nb >> 8u64;
i = i + 1;
};
pos = fstart + fsz: u64;
f = f.tnext;
};
let endpos: u64 = base + structt.size;
for (pos < endpos) {
emitdatawbyte(0u8);
pos = pos + 1u64;
};
return true;
};
// emitstructdata — top-level wrapper. Opens the DATA/DATAW directive
// then delegates to emitstructlitbytes. Shared between emitletdataw
// struct arm and emitdefconstants struct arm (#129 A.2).
fn emitstructdata(c: *cgen, directive: str, name: str,
structt: *tinfo, rhs: *node) bool = {
let su: *tinfo = structt;
for (su != nil && su.kind == tykind.TY_NAMED) { su = su.under; };
if (su == nil) { return false; };
if (su.kind != tykind.TY_STRUCT) { return false; };
emitline(directive);
emitline(" ");
emitsymname(c, name);
emitline("(SB),\"");
emitstructlitbytes(c, structt, rhs, 0u64);
emitline("\"\n");
return true;
};
fn emitletdataw(c: *cgen, file: *node) void = {
let d: *node = file.list;
for (d != nil) {
@@ -25080,6 +25270,21 @@ fn emitletdataw(c: *cgen, file: *node) void = {
emitfloatlitdata(c, "DATAW", nm, fsz,
d.rhs);
};
// #129 A.2: struct-typed let with N_STRUCTLIT rhs
// routes through the emitstructdata SSoT helper.
// Pre-A.2 emitletdataw had no struct arm, so the
// declaration fell out of the .data section and
// the link surfaced an undefined-symbol error.
if (issg) {
let r: *node = d.rhs;
if (r != nil) {
if (r.kind == nkind.N_STRUCTLIT) {
let st: *tinfo = d.lhs.type_: *tinfo;
emitstructdata(c, "DATAW", nm,
st, r);
};
};
};
// Skip the scalar 8B path when the global is a
// fixed-size array that just happens to sum to 8
// bytes (e.g. [4]u16, [8]u8) — the array path
@@ -25341,6 +25546,25 @@ fn emitdefconstants(c: *cgen, file: *node) void = {
if (dfsz > 0) {
emitfloatlitdata(c, "DATA", d.str,
dfsz, d.rhs);
} else {
// #129 A.2: struct-typed def with N_STRUCTLIT
// rhs. The checker stamps d.lhs.type_ with the
// struct's tinfo; helper peels TY_NAMED. Parallel
// to emitletdataw struct arm; uses DATA (read-
// only) directive.
if (r != nil) { if (r.kind == nkind.N_STRUCTLIT) {
let st: *tinfo = d.lhs.type_: *tinfo;
let su: *tinfo = st;
for (su != nil && su.kind == tykind.TY_NAMED) {
su = su.under;
};
if (su != nil) {
if (su.kind == tykind.TY_STRUCT) {
emitstructdata(c, "DATA",
d.str, st, r);
};
};
};};
};
};
if (ok) {
@@ -25606,6 +25830,9 @@ type defent = struct {
dname: str,
dmod: str, // originating module (`// MODULE: foo`), or empty
drhs: *node,
dtnode: *node, // #129 A.2: type-spec node (d.lhs); needed for
// struct-def structinfo lookup at the cgdot
// LOAD-side widening site.
dnext: *defent,
};
@@ -25614,7 +25841,7 @@ fn collectdefs(c: *cgen, file: *node) void = {
let d: *node = file.list;
for (d != nil) {
if (d.kind == nkind.N_DEF) {
let e: *defent = alloc(defent{dname=d.str, dmod=d.nmod, drhs=d.rhs, dnext=c.defs})!;
let e: *defent = alloc(defent{dname=d.str, dmod=d.nmod, drhs=d.rhs, dtnode=d.lhs, dnext=c.defs})!;
c.defs = e;
};
d = d.next;

View File

@@ -0,0 +1,247 @@
/*
* 918_struct_composite_init_run — runtime + byte-id net for #129
* Phase A.2: module-level `let`/`def` with composite-struct
* initializer silently emitted undefined ref OR wrong bytes pre-fix.
*
* Pre-A.2 failure modes:
* - cstage emit_lets / wwstage emitletdataw skipped struct-typed lets
* entirely (`if (is_struct) continue;` / parallel) — link surfaced
* `undefined reference to 'main.NAME'`.
* - cstage emit_defs / wwstage emitdefconstants had no struct arm —
* same undef ref for def, plus a SEPARATE LOAD-side cgen bug
* emitting `MOVSXD (BP), AX` (reading stack frame byte 0) when the
* LOAD did get past the link.
*
* Phase A.2 fix (per A.1 SSoT-helper precedent):
* - cstage: emit_struct_data + emit_struct_lit_bytes helpers walk
* Tfield list in declaration order, zero-fill padding via per-
* field offsets (rule 13), dispatch per field type. Float-field
* bytes inlined (mirroring A.1's emit_floatlit_data shape but
* localised so the byte loop covers padding too). Nested struct
* recurses. Out-of-scope field kinds (array / str / slice / ptr)
* fatal loud per rule-7.
* - cstage: emit_lets + emit_defs gain struct arms routing through
* the helper.
* - cstage: LOAD-side widening at `if (u && u->kind == TY_STRUCT
* && lhs->kind == N_IDENT)` arm — the `let_islet`-gated LEAQ
* name(SB) shape now also fires for struct defs via the new
* `def_isstructdef` registry (mirror of letvars).
* - wwstage: parallel emitstructdata + emitstructlitbytes helpers,
* defent.dtnode field, defvarstructinfo (cgdot LOAD widening).
*
* Bootstrap NEUTRAL: zero `let/def: T = T{...}` consumers in lib/ or
* selfhost/. γ-cleanup is the first consumer (lib/math:floatinfo).
*
* Rows cover all A.2-scope shapes:
* - (a) `let CFG: cfg_t = cfg_t{a=1, b=2u64};` — plain mixed-int let
* - (b) `def CFG: cfg_t = cfg_t{a=1, b=2u64};` — plain mixed-int def
* (exercises LOAD-widening; both stages)
* - (c) `let F: ft = ft{a=1.5, b=99u32};` — let with float field
* - (d) `def F: ft = ft{a=1.5, b=99u32};` — def with float field
* (storage + LOAD + float-narrow path together)
* - (e) `let Z: zt = zt{};` — empty struct-lit zero-fill
* - (f) `let CFG: cfg_t;` — regression: no-rhs (pre-existing path
* unchanged)
*
* Each row carries (a) cstage `ww build` + run asserting exit code
* and (b) w6c vs w6c_ww `.s` cmp (rule-10 byte-id).
*
* Nested-struct shape (#3 in design report) is OMITTED here — the
* helper implements the recursion but #145 (parser/checker inner-
* literal field-name leak) blocks end-to-end correctness; nested
* row deferred until #145 lands.
*
* Array-in-struct shape parked to Phase A.3 boundary.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
static int
runwait(const char *cmd)
{
int rc = system(cmd);
if (rc == -1) return -1;
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
return -1;
}
struct row { const char *label; const char *src; int want_exit; };
static const struct row rows[] = {
{ "let_int_struct",
"package main;\n"
"type cfg_t = struct { a: i32, b: u64 };\n"
"let CFG: cfg_t = cfg_t{a=1, b=2u64};\n"
"export fn main() i32 = {\n"
" return CFG.a;\n"
"};\n", 1 },
{ "def_int_struct",
"package main;\n"
"type cfg_t = struct { a: i32, b: u64 };\n"
"def CFG: cfg_t = cfg_t{a=1, b=2u64};\n"
"export fn main() i32 = {\n"
" return CFG.a;\n"
"};\n", 1 },
{ "let_float_field",
"package main;\n"
"type ft = struct { a: f64, b: u32 };\n"
"let F: ft = ft{a=1.5, b=99u32};\n"
"export fn main() i32 = {\n"
" return (F.a: i32);\n"
"};\n", 1 },
{ "def_float_field",
"package main;\n"
"type ft = struct { a: f64, b: u32 };\n"
"def F: ft = ft{a=1.5, b=99u32};\n"
"export fn main() i32 = {\n"
" return (F.a: i32);\n"
"};\n", 1 },
{ "let_empty_struct",
"package main;\n"
"type zt = struct { a: i32, b: u64 };\n"
"let Z: zt = zt{};\n"
"export fn main() i32 = {\n"
" return Z.a;\n"
"};\n", 0 },
/* 8B struct hits the cstage emit_lets scalar-8B short-circuit
* (sz==8 fold_int_literal arm) — without the `!let_isstruct`
* gate the struct lit fold-fails and the let drops entirely,
* emitting no DATA. Both stages now route via the struct arm. */
{ "let_int_struct_8b",
"package main;\n"
"type s8 = struct { a: i32, b: i32 };\n"
"let X: s8 = s8{a=7, b=42};\n"
"export fn main() i32 = {\n"
" return X.a;\n"
"};\n", 7 },
/* Regression: no-rhs path unchanged (emit_data_row_zero / wwstage
* parallel). */
{ "let_norhs_struct",
"package main;\n"
"type cfg_t = struct { a: i32, b: u64 };\n"
"let CFG: cfg_t;\n"
"export fn main() i32 = {\n"
" return CFG.a;\n"
"};\n", 0 },
{ NULL, NULL, 0 }
};
static int
slurp_eq(const char *a, const char *b)
{
FILE *fa = fopen(a, "rb");
FILE *fb = fopen(b, "rb");
if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return -1; }
int rc = 0;
for (;;) {
int ca = fgetc(fa);
int cb = fgetc(fb);
if (ca != cb) { rc = -1; break; }
if (ca == EOF) break;
}
fclose(fa); fclose(fb);
return rc;
}
int
main(void)
{
const char *bin = getenv("BIN");
if (!bin) bin = "out/bin";
char absbin[1024];
if (bin[0] != '/') {
char cwd[1024];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
bin = absbin;
}
char w6c[1100], w6c_ww[1100];
snprintf(w6c, sizeof w6c, "%s/w6c", bin);
snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin);
if (access(w6c_ww, X_OK) != 0) {
fprintf(stderr, "strcomp: w6c_ww missing — cannot run "
"the cs==ww byte-id gate (the whole point of this test)\n");
return 1;
}
int n = 0, fail = 0;
for (int i = 0; rows[i].src; i++, n++) {
char src[64];
snprintf(src, sizeof src, "/tmp/wwstrc_%d_%d.ww", getpid(), i);
FILE *f = fopen(src, "wb");
if (f == NULL) { fail++; continue; }
fputs(rows[i].src, f);
fclose(f);
char tmpdir[64];
snprintf(tmpdir, sizeof tmpdir, "/tmp/wwstrc_%d_d_%d",
getpid(), i);
mkdir(tmpdir, 0755);
char cmd[2048];
snprintf(cmd, sizeof cmd, "cd %s && %s/ww build %s",
tmpdir, bin, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: cstage build failed\n",
rows[i].label);
fail++;
unlink(src); rmdir(tmpdir);
continue;
}
char outbin[128];
const char *base = strrchr(src, '/');
base = base ? base + 1 : src;
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
char *dot = strrchr(outbin, '.');
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
int got = runwait(outbin);
if (got != rows[i].want_exit) {
fprintf(stderr, "row[%s]: cstage exit %d, want %d\n",
rows[i].label, got, rows[i].want_exit);
fail++;
}
unlink(outbin); rmdir(tmpdir);
char cs_s[64], ws_s[64];
snprintf(cs_s, sizeof cs_s, "/tmp/wwstrc_%d_%d_cs.s",
getpid(), i);
snprintf(ws_s, sizeof ws_s, "/tmp/wwstrc_%d_%d_ww.s",
getpid(), i);
snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null",
w6c, cs_s, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: w6c failed\n", rows[i].label);
fail++; unlink(src); continue;
}
snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null",
w6c_ww, ws_s, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: w6c_ww failed\n",
rows[i].label);
fail++; unlink(src); unlink(cs_s); continue;
}
if (slurp_eq(cs_s, ws_s) != 0) {
fprintf(stderr,
"row[%s]: cstage/wwstage .s DIFFER (rule-10 "
"byte-id violation)\n", rows[i].label);
fail++;
}
unlink(src); unlink(cs_s); unlink(ws_s);
}
if (fail) {
fprintf(stderr, "%d/%d struct-composite tests failed\n", fail, n);
return 1;
}
printf("strcomp: %d/%d ok (cstage run + cs==ww byte-id)\n",
n, n);
return 0;
}