diff --git a/Makefile b/Makefile index b31e02db..1858677a 100644 --- a/Makefile +++ b/Makefile @@ -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 $@ $< diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 3c4f77e9..1a1fef8f 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -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; } diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 999256b3..7e26ee37 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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; diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index d3e06bcf..a544978e 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -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; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index e4a20938..107455a0 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -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) { diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 54fec17c..32b19ebc 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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; diff --git a/test/wcc/918_struct_composite_init_run.c b/test/wcc/918_struct_composite_init_run.c new file mode 100644 index 00000000..e4c2bb01 --- /dev/null +++ b/test/wcc/918_struct_composite_init_run.c @@ -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 +#include +#include +#include +#include +#include + +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; +}