From 9767ff8ffff11f057215da5b2b72d515319d7ff7 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sun, 14 Jun 2026 18:11:28 +0900 Subject: [PATCH] wcc/ww: emit correct (tag,payload) for a tagged value in static-init (#19) A tagged-union value nested in module-level array/struct static-init mis-emitted in both stages: the lit-bytes emitters had no TY_TAGGED arm, so a tagged element/field fell to the int path and the payload landed in the TAG word -- match then read the wrong variant. The zero-placeholder idiom (today the only way to declare a tagged global: zero-init in static, write at runtime) was correct only by accident (int-variant zero folds to (0,0), which equals the right (tag0,0)). Extract a raw-byte core emittaggedbytes/emit_tagged_bytes -- variant tag@+0, int payload@+8, zero-pad to the slot size; no directive, no offset, no reloc -- and refactor the scalar tagged emitter to delegate to it (byte-id-neutral). Add a TY_TAGGED member branch to the array and struct lit-bytes emitters (both stages) that calls the core at the existing full-slot stride, before the int fallthrough. Zero stays (0,0) byte-identical; a non-zero element/field now emits (tag,payload) correctly. A wide (str/slice) or struct/>8B payload nested in an aggregate needs reloc-at-member-offset machinery the aggregate byte-emitters don't have, so it is loud-rejected (rule 7), deferred to #30; the existing slice-of-tagged static-init reject is unchanged. Regenerates the w6c and wwdump combined.ww. Table-driven 843 test: non-zero array/struct (pre-fix returned the wrong variant), the non-tag-0 bool-variant edge, byte-id-neutral zero-placeholder rows, and wide-payload reject rows; each run row also pins cs-vs-ww asm. --- Makefile | 7 + cmd/w6c/cgen.c | 116 ++++++++- selfhost/cmd/w6c/main.combined.ww | 130 ++++++++-- selfhost/cmd/wcc/cgen.ww | 130 ++++++++-- selfhost/cmd/wwdump/main.combined.ww | 130 ++++++++-- test/wcc/843_tagged_staticinit.c | 371 +++++++++++++++++++++++++++ 6 files changed, 831 insertions(+), 53 deletions(-) create mode 100644 test/wcc/843_tagged_staticinit.c diff --git a/Makefile b/Makefile index 8f7f47fb..40717e95 100644 --- a/Makefile +++ b/Makefile @@ -247,6 +247,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_arr_strslice_elem \ $(BIN)/test_arr_tagged_elem \ $(BIN)/test_dup_main_reject \ + $(BIN)/test_tagged_staticinit \ $(BIN)/test_arr_infer_len \ $(BIN)/test_arr_cap_reject \ $(BIN)/test_tagged_subset_reject \ @@ -1438,6 +1439,12 @@ $(BIN)/test_dup_main_reject: test/wcc/842_dup_main_reject.c $(BIN)/ww \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_tagged_staticinit: test/wcc/843_tagged_staticinit.c $(BIN)/ww \ + $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_slice_str_global_zero: test/wcc/686_slice_str_global_zero.c $(BIN)/ww \ $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 5afa7170..e3e4978e 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -15315,6 +15315,11 @@ emit_floatlit_data(FILE *out, Cg *c, const char *directive, * A.2 shape-15 park). Defined further down. */ static int emit_array_lit_bytes(FILE *out, Cg *c, Type *t, Node *rhs, int emit_phase); +/* Forward declaration: the struct/array member emitters call the shared + * tagged-payload core (#19 option A), defined alongside emit_tagged_data + * further down. */ +static int emit_tagged_bytes(FILE *out, Type *u, Node *rhs, int sz, + int emit_phase); /* 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 @@ -15368,6 +15373,22 @@ emit_struct_lit_bytes(FILE *out, Cg *c, Type *t, Node *rhs, u64 base) Node *vr = v; while (vr != NULL && vr->kind == N_CAST) vr = vr->lhs; Type *fu = type_chase_named(f->type); + /* #19 option A: a non-nullable tagged-union field rides the + * shared (tag,payload) core at the field slot size, mirroring the + * scalar tagged global — NOT the int emitter, which mis-folds the + * payload into the tag word. Wide/struct/non-foldable payload + * loud-rejects (task #30 sub-item, rule 7). v is non-NULL here + * (the absent-field zero-fill is handled above). */ + if (fu && fu->kind == TY_TAGGED && !fu->nullable) { + if (!emit_tagged_bytes(out, fu, v, fsz, 1)) + fatal("emit_struct_lit_bytes: tagged-union " + "struct-field '%s' static-init needs a " + "zero/int payload; wide (str/slice) or struct " + "payload is deferred (task #30, rule 7)", + f->name ? f->name : "?"); + pos = fstart + (u64)fsz; + continue; + } 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 @@ -15504,6 +15525,46 @@ emit_array_lit_bytes(FILE *out, Cg *c, Type *t, Node *rhs, int emit_phase) int esz = etype ? (int)etype->size : 1; int alen = (int)u->alen; + /* #19 option A: a non-nullable tagged-union element rides the shared + * (tag,payload) core at the full slot stride esz, mirroring the scalar + * tagged global — NOT the int emitter. A wide/struct/non-foldable + * payload element loud-rejects (task #30 sub-item, rule 7). A nullable + * `(*T|void)` element is a 1-word fold, not a tag box — left to the + * existing int path (task #15). */ + if (eu && eu->kind == TY_TAGGED && !eu->nullable) { + int idx = 0; + Node *last_ev = NULL; + for (Node *e = rhs->list; e && idx < alen; e = e->next) { + if (e->kind == N_FIELD && e->str + && strcmp(e->str, "...") == 0) + break; + if (!emit_tagged_bytes(out, eu, e, esz, 0)) + fatal("emit_array_lit_bytes: tagged-union array " + "element static-init needs a zero/int " + "payload; wide (str/slice) or struct payload " + "is deferred (task #30, rule 7)"); + last_ev = e; + idx++; + } + if (emit_phase == 0) return 1; + idx = 0; + int repeat = 0; + for (Node *e = rhs->list; e && idx < alen; e = e->next) { + if (e->kind == N_FIELD && e->str + && strcmp(e->str, "...") == 0) { repeat = 1; break; } + emit_tagged_bytes(out, eu, e, esz, 1); + idx++; + } + for (; idx < alen; idx++) { + if (repeat && last_ev) + emit_tagged_bytes(out, eu, last_ev, esz, 1); + else + for (int b = 0; b < esz; b++) + emit_data_byte(out, 0); + } + return 1; + } + if (eu && eu->kind == TY_STRUCT) { /* Validate: every element must be N_STRUCTLIT (after N_CAST * peel). */ @@ -15803,6 +15864,47 @@ emit_strarray_data(FILE *out, Cg *c, const char *directive, return 1; } +/* emit_tagged_bytes — raw sz-byte static-init payload for a tagged-union + * value: variant tag@+0 (8B), int payload@+8 (8B), zero-pad to sz. NO + * directive open/close, NO reloc — emits exactly sz bytes via + * emit_data_byte at the current cursor. Handles zero (NULL rhs) + a + * foldable int payload only; a wide (str/slice) payload needs a DATAR the + * raw core cannot place inside an already-open aggregate directive, and a + * struct/non-foldable payload has no scalar form — both return 0 WITHOUT + * emitting, and the caller loud-rejects (task #30 wide/struct sub-item). + * `u` is the type-chased TY_TAGGED union type. emit_phase 0 = validate + * only; 1 = emit. rob's EXTRACT ruling (#19 option A): the scalar wrapper + * emit_tagged_data and the aggregate member branches (emit_array_lit_bytes + * / emit_struct_lit_bytes) share this raw core so a nested tagged member + * rides the SAME (tag,payload) SSoT as a scalar tagged global. Mirror of + * wwstage emittaggedbytes. */ +static int +emit_tagged_bytes(FILE *out, Type *u, Node *rhs, int sz, int emit_phase) +{ + if (u == NULL || u->kind != TY_TAGGED) return 0; + if (rhs == NULL) { + if (emit_phase) + for (int i = 0; i < sz; i++) emit_data_byte(out, 0); + return 1; + } + Node *r = rhs; + while (r != NULL && r->kind == N_CAST) r = r->lhs; + if (r == NULL) return 0; + int tag = cg_tag_for_variant(u, r->type); + if (tag < 0) return 0; + if (type_isstr(r->type) || type_isslice(r->type)) return 0; + u64 v; + if (!fold_int_literal(r, &v)) return 0; + if (emit_phase) { + for (int i = 0; i < 8; i++) + emit_data_byte(out, (u8)(((u64)tag >> (i * 8)) & 0xff)); + for (int i = 0; i < 8; i++) + emit_data_byte(out, (u8)((v >> (i * 8)) & 0xff)); + for (int i = 16; i < sz; i++) emit_data_byte(out, 0); + } + return 1; +} + /* emit_tagged_data — module-level `let g: (T0 | T1 | ...) = v;` static * init (#87). The static DATA must byte-MIRROR a runtime LOCAL tagged box * of the same type (the SSoT pin, rob §3): tag word at +0 (the const- @@ -15823,7 +15925,7 @@ emit_tagged_data(FILE *out, Cg *c, const char *name, const char *module, const char *sym = mod_mangle_value(c, name, module); if (rhs == NULL) { fprintf(out, "DATAW %s(SB),\"", sym); - for (int i = 0; i < sz; i++) emit_data_byte(out, 0); + emit_tagged_bytes(out, u, rhs, sz, 1); fputs("\"\n", out); return 1; } @@ -15855,14 +15957,12 @@ emit_tagged_data(FILE *out, Cg *c, const char *name, const char *module, } return 1; } - u64 v; - if (!fold_int_literal(r, &v)) return 0; + /* int/zero payload via the shared raw core; validate (phase 0) + * BEFORE opening the directive so a non-foldable rhs returns 0 + * without leaving a half-written DATAW. */ + if (!emit_tagged_bytes(out, u, rhs, sz, 0)) return 0; fprintf(out, "DATAW %s(SB),\"", sym); - for (int i = 0; i < 8; i++) - emit_data_byte(out, (u8)(((u64)tag >> (i * 8)) & 0xff)); - for (int i = 0; i < 8; i++) - emit_data_byte(out, (u8)((v >> (i * 8)) & 0xff)); - for (int i = 16; i < sz; i++) emit_data_byte(out, 0); + emit_tagged_bytes(out, u, rhs, sz, 1); fputs("\"\n", out); return 1; } diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 647b6243..8d2eee96 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -42673,6 +42673,21 @@ fn emitstructlitbytes(c: *cgen, structt: *tinfo, rhs: *node, for (vr != nil && vr.kind == nkind.N_CAST) { vr = vr.lhs; }; let fu: *tinfo = f.type_; fu = tichase(fu); + // #19 option A: a non-nullable tagged-union field rides the shared + // (tag,payload) core at the field slot size, mirroring the scalar + // tagged global — NOT the int emitter. Wide/struct/non-foldable + // payload loud-rejects (task #30 sub-item, rule 7). v is non-nil + // here (the absent-field zero-fill is handled above). + if (fu != nil && fu.kind == tykind.TY_TAGGED && !typeisnullable(fu)) { + if (!emittaggedbytes(c, f.type_, v, fsz, 1)) { + let m: str = "emitstructlitbytes: tagged-union struct-field static-init needs a zero/int payload; wide (str/slice) or struct payload is deferred (task #30, rule 7)\n"; + os.write(2, m.ptr, m.len: u64); + os.exit(1); + }; + pos = fstart + fsz: u64; + f = f.tnext; + continue; + }; if (fu != nil && fu.kind == tykind.TY_STRUCT) { if (vr == nil) { let m: str = "emitstructlitbytes: nested struct field rhs nil (#129 A.2)\n"; @@ -42830,6 +42845,54 @@ fn emitarraylitbytes(c: *cgen, arrt: *tinfo, rhs: *node, let eu: *tinfo = au.sub; eu = tichase(eu); + // #19 option A: a non-nullable tagged-union element rides the shared + // (tag,payload) core (emittaggedbytes) at the full slot stride esz, + // mirroring the scalar tagged global — NOT the int emitter, which + // mis-folds the payload into the tag word. A wide/struct/non-foldable + // payload element loud-rejects (task #30 sub-item, rule 7). A nullable + // `(*T|void)` element is a 1-word fold, not a tag box — left to the + // existing int path (task #15). + if (eu != nil && eu.kind == tykind.TY_TAGGED && !typeisnullable(eu)) { + let idx: i32 = 0; + let last_ev: *node = nil; + let e: *node = rhs.list; + for (e != nil && idx < alen) { + if (e.kind == nkind.N_FIELD) { + if (streq(e.str, "...")) { break; }; + }; + if (!emittaggedbytes(c, au.sub, e, esz, 0)) { + let m: str = "emitarraylitbytes: tagged-union array element static-init needs a zero/int payload; wide (str/slice) or struct payload is deferred (task #30, rule 7)\n"; + os.write(2, m.ptr, m.len: u64); + os.exit(1); + }; + last_ev = e; + idx += 1; + e = e.next; + }; + if (emit_phase == 0) { return true; }; + idx = 0; + let repeat: bool = false; + e = rhs.list; + for (e != nil && idx < alen) { + if (e.kind == nkind.N_FIELD) { + if (streq(e.str, "...")) { repeat = true; break; }; + }; + emittaggedbytes(c, au.sub, e, esz, 1); + idx += 1; + e = e.next; + }; + for (idx < alen) { + if (repeat && last_ev != nil) { + emittaggedbytes(c, au.sub, last_ev, esz, 1); + } else { + let bb: i32 = 0; + for (bb < esz) { emitdatawbyte(0u8); bb += 1; }; + }; + idx += 1; + }; + return true; + }; + if (eu != nil && eu.kind == tykind.TY_STRUCT) { // Validate: every element must be N_STRUCTLIT (after N_CAST). let idx: i32 = 0; @@ -43397,6 +43460,52 @@ fn emitslicedata(c: *cgen, name: str, module: str, slt: *tinfo, emitline(".d(SB)\n"); }; +// emittaggedbytes — raw sz-byte static-init payload for a tagged-union +// value: variant tag@+0 (8B), int payload@+8 (8B), zero-pad to sz. NO +// directive open/close, NO reloc — emits exactly sz bytes via +// emitdatawbyte at the current cursor. Handles zero (nil rhs) + a +// foldable int payload only; a wide (str/slice) payload needs a DATAR the +// raw core cannot place inside an already-open aggregate directive, and a +// struct/non-foldable payload has no scalar form — both return false +// WITHOUT emitting, and the caller loud-rejects (task #30 wide/struct +// sub-item). `tti` is the union tinfo (NAMED-peeled + TY_TAGGED-gated +// internally by flatvariantidxt). emit_phase 0 = validate only; 1 = emit. +// rob's EXTRACT ruling (#19 option A): the scalar wrapper emittaggeddata +// and the aggregate member branches (emitarraylitbytes/emitstructlitbytes) +// share this raw core so a nested tagged member rides the SAME +// (tag,payload) SSoT as a scalar tagged global. Mirror of cstage +// emit_tagged_bytes. +fn emittaggedbytes(c: *cgen, tti: *tinfo, rhs: *node, sz: i32, emit_phase: i32) bool = { + if (tti == nil) { return false; }; + if (rhs == nil) { + if (emit_phase == 1) { + let zi: i32 = 0; + for (zi < sz) { emitdatawbyte(0u8); zi += 1; }; + }; + return true; + }; + let r: *node = rhs; + for (r != nil && r.kind == nkind.N_CAST) { r = r.lhs; }; + if (r == nil) { return false; }; + let tag: i32 = flatvariantidxt(tti, r.type_: *tinfo, false); + if (tag < 0) { return false; }; + if (nodeisstr(c, r) || nodeisslice(c, r)) { return false; }; + let v: u64 = 0u64; + if (!foldintliteral(r, &v)) { return false; }; + if (emit_phase == 1) { + let acc: u64 = 0u64; + let i: i32 = 0; + acc = tag: u64; + for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; + acc = v; + i = 0; + for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; + i = 16; + for (i < sz) { emitdatawbyte(0u8); i += 1; }; + }; + return true; +}; + // emittaggeddata — module-level `let g: (T0 | T1 | ...) = v;` static // init (#87). Byte-MIRRORS a runtime LOCAL tagged box (rob §3 SSoT pin): // tag word at +0 (the const-selected variant index, taggedvariantindex — @@ -43412,8 +43521,7 @@ fn emittaggeddata(c: *cgen, name: str, module: str, tt: *node, rhs: *node, sz: i emitline("DATAW "); emitsymnamehint(c, name, module); emitline("(SB),\""); - let zi: i32 = 0; - for (zi < sz) { emitdatawbyte(0u8); zi += 1; }; + emittaggedbytes(c, tt.type_: *tinfo, rhs, sz, 1); emitline("\"\n"); return true; }; @@ -43475,22 +43583,14 @@ fn emittaggeddata(c: *cgen, name: str, module: str, tt: *node, rhs: *node, sz: i }; return true; }; - let v: u64 = 0u64; - if (!foldintliteral(r, &v)) { return false; }; + // int/zero payload via the shared raw core; validate (phase 0) BEFORE + // opening the directive so a non-foldable rhs returns false without + // leaving a half-written DATAW. + if (!emittaggedbytes(c, tt.type_: *tinfo, rhs, sz, 0)) { return false; }; emitline("DATAW "); emitsymnamehint(c, name, module); emitline("(SB),\""); - // tag@0 - acc = tag: u64; - i = 0; - for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; - // payload@8 - acc = v; - i = 0; - for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; - // pad to sz - i = 16; - for (i < sz) { emitdatawbyte(0u8); i += 1; }; + emittaggedbytes(c, tt.type_: *tinfo, rhs, sz, 1); emitline("\"\n"); return true; }; diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index 7cb2911b..e9f13bd4 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -1757,6 +1757,21 @@ fn emitstructlitbytes(c: *cgen, structt: *tinfo, rhs: *node, for (vr != nil && vr.kind == nkind.N_CAST) { vr = vr.lhs; }; let fu: *tinfo = f.type_; fu = tichase(fu); + // #19 option A: a non-nullable tagged-union field rides the shared + // (tag,payload) core at the field slot size, mirroring the scalar + // tagged global — NOT the int emitter. Wide/struct/non-foldable + // payload loud-rejects (task #30 sub-item, rule 7). v is non-nil + // here (the absent-field zero-fill is handled above). + if (fu != nil && fu.kind == tykind.TY_TAGGED && !typeisnullable(fu)) { + if (!emittaggedbytes(c, f.type_, v, fsz, 1)) { + let m: str = "emitstructlitbytes: tagged-union struct-field static-init needs a zero/int payload; wide (str/slice) or struct payload is deferred (task #30, rule 7)\n"; + os.write(2, m.ptr, m.len: u64); + os.exit(1); + }; + pos = fstart + fsz: u64; + f = f.tnext; + continue; + }; if (fu != nil && fu.kind == tykind.TY_STRUCT) { if (vr == nil) { let m: str = "emitstructlitbytes: nested struct field rhs nil (#129 A.2)\n"; @@ -1914,6 +1929,54 @@ fn emitarraylitbytes(c: *cgen, arrt: *tinfo, rhs: *node, let eu: *tinfo = au.sub; eu = tichase(eu); + // #19 option A: a non-nullable tagged-union element rides the shared + // (tag,payload) core (emittaggedbytes) at the full slot stride esz, + // mirroring the scalar tagged global — NOT the int emitter, which + // mis-folds the payload into the tag word. A wide/struct/non-foldable + // payload element loud-rejects (task #30 sub-item, rule 7). A nullable + // `(*T|void)` element is a 1-word fold, not a tag box — left to the + // existing int path (task #15). + if (eu != nil && eu.kind == tykind.TY_TAGGED && !typeisnullable(eu)) { + let idx: i32 = 0; + let last_ev: *node = nil; + let e: *node = rhs.list; + for (e != nil && idx < alen) { + if (e.kind == nkind.N_FIELD) { + if (streq(e.str, "...")) { break; }; + }; + if (!emittaggedbytes(c, au.sub, e, esz, 0)) { + let m: str = "emitarraylitbytes: tagged-union array element static-init needs a zero/int payload; wide (str/slice) or struct payload is deferred (task #30, rule 7)\n"; + os.write(2, m.ptr, m.len: u64); + os.exit(1); + }; + last_ev = e; + idx += 1; + e = e.next; + }; + if (emit_phase == 0) { return true; }; + idx = 0; + let repeat: bool = false; + e = rhs.list; + for (e != nil && idx < alen) { + if (e.kind == nkind.N_FIELD) { + if (streq(e.str, "...")) { repeat = true; break; }; + }; + emittaggedbytes(c, au.sub, e, esz, 1); + idx += 1; + e = e.next; + }; + for (idx < alen) { + if (repeat && last_ev != nil) { + emittaggedbytes(c, au.sub, last_ev, esz, 1); + } else { + let bb: i32 = 0; + for (bb < esz) { emitdatawbyte(0u8); bb += 1; }; + }; + idx += 1; + }; + return true; + }; + if (eu != nil && eu.kind == tykind.TY_STRUCT) { // Validate: every element must be N_STRUCTLIT (after N_CAST). let idx: i32 = 0; @@ -2481,6 +2544,52 @@ fn emitslicedata(c: *cgen, name: str, module: str, slt: *tinfo, emitline(".d(SB)\n"); }; +// emittaggedbytes — raw sz-byte static-init payload for a tagged-union +// value: variant tag@+0 (8B), int payload@+8 (8B), zero-pad to sz. NO +// directive open/close, NO reloc — emits exactly sz bytes via +// emitdatawbyte at the current cursor. Handles zero (nil rhs) + a +// foldable int payload only; a wide (str/slice) payload needs a DATAR the +// raw core cannot place inside an already-open aggregate directive, and a +// struct/non-foldable payload has no scalar form — both return false +// WITHOUT emitting, and the caller loud-rejects (task #30 wide/struct +// sub-item). `tti` is the union tinfo (NAMED-peeled + TY_TAGGED-gated +// internally by flatvariantidxt). emit_phase 0 = validate only; 1 = emit. +// rob's EXTRACT ruling (#19 option A): the scalar wrapper emittaggeddata +// and the aggregate member branches (emitarraylitbytes/emitstructlitbytes) +// share this raw core so a nested tagged member rides the SAME +// (tag,payload) SSoT as a scalar tagged global. Mirror of cstage +// emit_tagged_bytes. +fn emittaggedbytes(c: *cgen, tti: *tinfo, rhs: *node, sz: i32, emit_phase: i32) bool = { + if (tti == nil) { return false; }; + if (rhs == nil) { + if (emit_phase == 1) { + let zi: i32 = 0; + for (zi < sz) { emitdatawbyte(0u8); zi += 1; }; + }; + return true; + }; + let r: *node = rhs; + for (r != nil && r.kind == nkind.N_CAST) { r = r.lhs; }; + if (r == nil) { return false; }; + let tag: i32 = flatvariantidxt(tti, r.type_: *tinfo, false); + if (tag < 0) { return false; }; + if (nodeisstr(c, r) || nodeisslice(c, r)) { return false; }; + let v: u64 = 0u64; + if (!foldintliteral(r, &v)) { return false; }; + if (emit_phase == 1) { + let acc: u64 = 0u64; + let i: i32 = 0; + acc = tag: u64; + for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; + acc = v; + i = 0; + for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; + i = 16; + for (i < sz) { emitdatawbyte(0u8); i += 1; }; + }; + return true; +}; + // emittaggeddata — module-level `let g: (T0 | T1 | ...) = v;` static // init (#87). Byte-MIRRORS a runtime LOCAL tagged box (rob §3 SSoT pin): // tag word at +0 (the const-selected variant index, taggedvariantindex — @@ -2496,8 +2605,7 @@ fn emittaggeddata(c: *cgen, name: str, module: str, tt: *node, rhs: *node, sz: i emitline("DATAW "); emitsymnamehint(c, name, module); emitline("(SB),\""); - let zi: i32 = 0; - for (zi < sz) { emitdatawbyte(0u8); zi += 1; }; + emittaggedbytes(c, tt.type_: *tinfo, rhs, sz, 1); emitline("\"\n"); return true; }; @@ -2559,22 +2667,14 @@ fn emittaggeddata(c: *cgen, name: str, module: str, tt: *node, rhs: *node, sz: i }; return true; }; - let v: u64 = 0u64; - if (!foldintliteral(r, &v)) { return false; }; + // int/zero payload via the shared raw core; validate (phase 0) BEFORE + // opening the directive so a non-foldable rhs returns false without + // leaving a half-written DATAW. + if (!emittaggedbytes(c, tt.type_: *tinfo, rhs, sz, 0)) { return false; }; emitline("DATAW "); emitsymnamehint(c, name, module); emitline("(SB),\""); - // tag@0 - acc = tag: u64; - i = 0; - for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; - // payload@8 - acc = v; - i = 0; - for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; - // pad to sz - i = 16; - for (i < sz) { emitdatawbyte(0u8); i += 1; }; + emittaggedbytes(c, tt.type_: *tinfo, rhs, sz, 1); emitline("\"\n"); return true; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index fcce2e30..0d6a2e6d 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -42673,6 +42673,21 @@ fn emitstructlitbytes(c: *cgen, structt: *tinfo, rhs: *node, for (vr != nil && vr.kind == nkind.N_CAST) { vr = vr.lhs; }; let fu: *tinfo = f.type_; fu = tichase(fu); + // #19 option A: a non-nullable tagged-union field rides the shared + // (tag,payload) core at the field slot size, mirroring the scalar + // tagged global — NOT the int emitter. Wide/struct/non-foldable + // payload loud-rejects (task #30 sub-item, rule 7). v is non-nil + // here (the absent-field zero-fill is handled above). + if (fu != nil && fu.kind == tykind.TY_TAGGED && !typeisnullable(fu)) { + if (!emittaggedbytes(c, f.type_, v, fsz, 1)) { + let m: str = "emitstructlitbytes: tagged-union struct-field static-init needs a zero/int payload; wide (str/slice) or struct payload is deferred (task #30, rule 7)\n"; + os.write(2, m.ptr, m.len: u64); + os.exit(1); + }; + pos = fstart + fsz: u64; + f = f.tnext; + continue; + }; if (fu != nil && fu.kind == tykind.TY_STRUCT) { if (vr == nil) { let m: str = "emitstructlitbytes: nested struct field rhs nil (#129 A.2)\n"; @@ -42830,6 +42845,54 @@ fn emitarraylitbytes(c: *cgen, arrt: *tinfo, rhs: *node, let eu: *tinfo = au.sub; eu = tichase(eu); + // #19 option A: a non-nullable tagged-union element rides the shared + // (tag,payload) core (emittaggedbytes) at the full slot stride esz, + // mirroring the scalar tagged global — NOT the int emitter, which + // mis-folds the payload into the tag word. A wide/struct/non-foldable + // payload element loud-rejects (task #30 sub-item, rule 7). A nullable + // `(*T|void)` element is a 1-word fold, not a tag box — left to the + // existing int path (task #15). + if (eu != nil && eu.kind == tykind.TY_TAGGED && !typeisnullable(eu)) { + let idx: i32 = 0; + let last_ev: *node = nil; + let e: *node = rhs.list; + for (e != nil && idx < alen) { + if (e.kind == nkind.N_FIELD) { + if (streq(e.str, "...")) { break; }; + }; + if (!emittaggedbytes(c, au.sub, e, esz, 0)) { + let m: str = "emitarraylitbytes: tagged-union array element static-init needs a zero/int payload; wide (str/slice) or struct payload is deferred (task #30, rule 7)\n"; + os.write(2, m.ptr, m.len: u64); + os.exit(1); + }; + last_ev = e; + idx += 1; + e = e.next; + }; + if (emit_phase == 0) { return true; }; + idx = 0; + let repeat: bool = false; + e = rhs.list; + for (e != nil && idx < alen) { + if (e.kind == nkind.N_FIELD) { + if (streq(e.str, "...")) { repeat = true; break; }; + }; + emittaggedbytes(c, au.sub, e, esz, 1); + idx += 1; + e = e.next; + }; + for (idx < alen) { + if (repeat && last_ev != nil) { + emittaggedbytes(c, au.sub, last_ev, esz, 1); + } else { + let bb: i32 = 0; + for (bb < esz) { emitdatawbyte(0u8); bb += 1; }; + }; + idx += 1; + }; + return true; + }; + if (eu != nil && eu.kind == tykind.TY_STRUCT) { // Validate: every element must be N_STRUCTLIT (after N_CAST). let idx: i32 = 0; @@ -43397,6 +43460,52 @@ fn emitslicedata(c: *cgen, name: str, module: str, slt: *tinfo, emitline(".d(SB)\n"); }; +// emittaggedbytes — raw sz-byte static-init payload for a tagged-union +// value: variant tag@+0 (8B), int payload@+8 (8B), zero-pad to sz. NO +// directive open/close, NO reloc — emits exactly sz bytes via +// emitdatawbyte at the current cursor. Handles zero (nil rhs) + a +// foldable int payload only; a wide (str/slice) payload needs a DATAR the +// raw core cannot place inside an already-open aggregate directive, and a +// struct/non-foldable payload has no scalar form — both return false +// WITHOUT emitting, and the caller loud-rejects (task #30 wide/struct +// sub-item). `tti` is the union tinfo (NAMED-peeled + TY_TAGGED-gated +// internally by flatvariantidxt). emit_phase 0 = validate only; 1 = emit. +// rob's EXTRACT ruling (#19 option A): the scalar wrapper emittaggeddata +// and the aggregate member branches (emitarraylitbytes/emitstructlitbytes) +// share this raw core so a nested tagged member rides the SAME +// (tag,payload) SSoT as a scalar tagged global. Mirror of cstage +// emit_tagged_bytes. +fn emittaggedbytes(c: *cgen, tti: *tinfo, rhs: *node, sz: i32, emit_phase: i32) bool = { + if (tti == nil) { return false; }; + if (rhs == nil) { + if (emit_phase == 1) { + let zi: i32 = 0; + for (zi < sz) { emitdatawbyte(0u8); zi += 1; }; + }; + return true; + }; + let r: *node = rhs; + for (r != nil && r.kind == nkind.N_CAST) { r = r.lhs; }; + if (r == nil) { return false; }; + let tag: i32 = flatvariantidxt(tti, r.type_: *tinfo, false); + if (tag < 0) { return false; }; + if (nodeisstr(c, r) || nodeisslice(c, r)) { return false; }; + let v: u64 = 0u64; + if (!foldintliteral(r, &v)) { return false; }; + if (emit_phase == 1) { + let acc: u64 = 0u64; + let i: i32 = 0; + acc = tag: u64; + for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; + acc = v; + i = 0; + for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; + i = 16; + for (i < sz) { emitdatawbyte(0u8); i += 1; }; + }; + return true; +}; + // emittaggeddata — module-level `let g: (T0 | T1 | ...) = v;` static // init (#87). Byte-MIRRORS a runtime LOCAL tagged box (rob §3 SSoT pin): // tag word at +0 (the const-selected variant index, taggedvariantindex — @@ -43412,8 +43521,7 @@ fn emittaggeddata(c: *cgen, name: str, module: str, tt: *node, rhs: *node, sz: i emitline("DATAW "); emitsymnamehint(c, name, module); emitline("(SB),\""); - let zi: i32 = 0; - for (zi < sz) { emitdatawbyte(0u8); zi += 1; }; + emittaggedbytes(c, tt.type_: *tinfo, rhs, sz, 1); emitline("\"\n"); return true; }; @@ -43475,22 +43583,14 @@ fn emittaggeddata(c: *cgen, name: str, module: str, tt: *node, rhs: *node, sz: i }; return true; }; - let v: u64 = 0u64; - if (!foldintliteral(r, &v)) { return false; }; + // int/zero payload via the shared raw core; validate (phase 0) BEFORE + // opening the directive so a non-foldable rhs returns false without + // leaving a half-written DATAW. + if (!emittaggedbytes(c, tt.type_: *tinfo, rhs, sz, 0)) { return false; }; emitline("DATAW "); emitsymnamehint(c, name, module); emitline("(SB),\""); - // tag@0 - acc = tag: u64; - i = 0; - for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; - // payload@8 - acc = v; - i = 0; - for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; - // pad to sz - i = 16; - for (i < sz) { emitdatawbyte(0u8); i += 1; }; + emittaggedbytes(c, tt.type_: *tinfo, rhs, sz, 1); emitline("\"\n"); return true; }; diff --git a/test/wcc/843_tagged_staticinit.c b/test/wcc/843_tagged_staticinit.c new file mode 100644 index 00000000..e81bf0e7 --- /dev/null +++ b/test/wcc/843_tagged_staticinit.c @@ -0,0 +1,371 @@ +/* + * 843_tagged_staticinit — #19 option A (rob's EXTRACT ruling). A tagged- + * union VALUE nested in module-level static-init DATA (an array element or + * a struct field) now emits the correct (tag@+0, payload@+8) box, the same + * SSoT a SCALAR tagged global and a runtime LOCAL box use. Pre-fix the + * nested member fell to the INT emitter, which mis-folded the payload into + * the tag word — cstage (value, value), wwstage (value, 0); NEITHER is + * (tag, value). Both BUILT and ran WRONG. + * + * The fix extracts a shared raw-byte core emit_tagged_bytes / emittaggedbytes + * (tag@0, int payload@8, zero-pad) from the scalar emitter, and the array/ + * struct member emitters call it at the full slot stride. A WIDE (str/slice) + * payload nested in an aggregate needs a reloc the raw core cannot place + * mid-directive, and a struct/non-foldable payload has no scalar form — + * both LOUD-REJECT (deferred, task #30; mirror of the slice-of-tagged + * reject). Only zero + int payload is implemented, which is exactly what + * the corpus exercises. + * + * row | shape | kind + * ---------------------+----------------------------------------+------ + * array_nonzero | let gs:[2](int|bool)=[42,7]; gs[0] | RUN 42 + * array_bool_variant | let gs:[2](int|bool)=[7,true]; gs[1] | RUN 2 + * | (NON-tag-0: tag=1,payload=1 — pre-fix | + * | (1,0) read bool=false -> 3) | + * struct_nonzero | let g:box{t:(int|bool)}=box{t=55};g.t | RUN 55 + * array_zero_then_set | let gs:[2](int|bool)=[0,0]; gs[0]=42 | RUN 42 + * | (zero-placeholder idiom — byte-id- | + * | neutral build, mirrors 841/989) | + * struct_zero_then_set | let g:box{t=0}; g.t=42 | RUN 42 + * plain_struct_ok | non-tagged struct static-init | RUN 9 + * wide_array_str | let gs:[2](int|str)=["hi",0] | REJECT + * wide_struct_str | let g:sbox{s:(int|str)}=sbox{s="hi"} | REJECT + * + * DISCRIMINATION: pre-fix array_nonzero BUILT and match(gs[0]) returned 1 + * (the match fallthrough — the payload 42 sat in the TAG word, matching no + * variant); post-fix it returns 42. array_bool_variant pins the NON-tag-0 + * edge (tag != payload word): pre-fix (1,0) read the bool payload as 0 + * (false) -> 3; post-fix (1,1) -> true -> 2. RUN rows also assert cstage/ + * wwstage asm byte-id (rule 10). The reject diagnostic core text is shared + * across stages (cstage adds the harness "ww: " prefix). NNN<950, self- + * contained (/tmp, no imports). + */ +#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 expect_build; /* 1 = build+run to want_exit; 0 = must REJECT */ + int want_exit; + const char *diag; /* reject core substring (NULL for RUN rows) */ +}; + +static const struct row rows[] = { + /* RUN — non-zero int payload in an array element: now correct. */ + { "array_nonzero", + "package main;\n" + "let gs: [2](int | bool) = [42, 7];\n" + "export fn main() int = {\n" + "\tmatch (gs[0]) {\n" + "\tcase let n: int => return n;\n" + "\tcase bool => return 99;\n" + "\t};\n" + "\treturn 1;\n" + "};\n", + 1, 42, NULL }, + + /* RUN — NON-tag-0 edge: a bool element (variant index 1, payload 1). + * Pre-fix the int-fold emitted (1, 0) so the bool payload read as 0 + * (false) -> 3; post-fix (1, 1) -> true -> 2. */ + { "array_bool_variant", + "package main;\n" + "let gs: [2](int | bool) = [7, true];\n" + "export fn main() int = {\n" + "\tmatch (gs[1]) {\n" + "\tcase let n: int => return 1;\n" + "\tcase let b: bool => { if (b) { return 2; }; return 3; };\n" + "\t};\n" + "\treturn 4;\n" + "};\n", + 1, 2, NULL }, + + /* RUN — non-zero int payload in a struct field: now correct. */ + { "struct_nonzero", + "package main;\n" + "type box = struct { t: (int | bool), n: int };\n" + "let g: box = box { t = 55, n = 5 };\n" + "fn main() i32 = {\n" + "\tmatch (g.t) {\n" + "\tcase let v: int => return v: i32;\n" + "\tcase bool => return 7;\n" + "\t};\n" + "\treturn 1;\n" + "};\n", + 1, 55, NULL }, + + /* RUN — zero-placeholder array idiom (mirror 989_taggedglobalindex): + * static-init [0,0] is byte-id-neutral, runtime write then matches. */ + { "array_zero_then_set", + "package main;\n" + "let gs: [2](int | bool) = [0, 0];\n" + "export fn main() int = {\n" + "\tgs[0] = 42;\n" + "\tmatch (gs[0]) {\n" + "\tcase let n: int => return n;\n" + "\tcase bool => return 9;\n" + "\t};\n" + "\treturn 1;\n" + "};\n", + 1, 42, NULL }, + + /* RUN — zero-placeholder struct idiom (mirror 841): {t=0} byte-id- + * neutral, runtime write then matches. */ + { "struct_zero_then_set", + "package main;\n" + "type box = struct { t: (int | bool), n: int };\n" + "let g: box = box { t = 0, n = 5 };\n" + "fn main() i32 = {\n" + "\tg.t = 42;\n" + "\tmatch (g.t) {\n" + "\tcase let v: int => return v: i32;\n" + "\tcase bool => return 7;\n" + "\t};\n" + "\treturn 1;\n" + "};\n", + 1, 42, NULL }, + + /* RUN — non-tagged struct static-init still emits + runs. 9. */ + { "plain_struct_ok", + "package main;\n" + "type pt = struct { x: int, y: int };\n" + "let p: pt = pt { x = 5, y = 9 };\n" + "export fn main() int = {\n" + "\treturn p.y;\n" + "};\n", + 1, 9, NULL }, + + /* REJECT — wide (str) payload tagged element in an array static-init: + * the reloc-in-aggregate case is deferred (task #30). */ + { "wide_array_str", + "package main;\n" + "let gs: [2](int | str) = [\"hi\", 0];\n" + "export fn main() int = {\n" + "\treturn 0;\n" + "};\n", + 0, 0, + "tagged-union array element static-init needs a zero/int payload" }, + + /* REJECT — wide (str) payload tagged field in a struct static-init. */ + { "wide_struct_str", + "package main;\n" + "type sbox = struct { s: (int | str) };\n" + "let g: sbox = sbox { s = \"hi\" };\n" + "export fn main() int = {\n" + "\treturn 0;\n" + "};\n", + 0, 0, + "tagged-union struct-field" }, +}; + +static int +run_build(const char *driver, const struct row *r, int i) +{ + char src[64], tmpdir[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/tsi_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/tsi_%d_d_%d", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -2; + fputs(r->src, f); + fclose(f); + + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>/dev/null", + tmpdir, driver, src); + int brc = runwait(cmd); + + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + char outbin[128]; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + + int got = -1; + if (brc == 0) got = runwait(outbin); + + unlink(src); unlink(outbin); rmdir(tmpdir); + return brc == 0 ? got : -1; +} + +/* A reject row must (a) fail to build and (b) emit the shared diagnostic + * core text. Returns 0 on the expected reject, -1 otherwise. */ +static int +build_should_reject(const char *driver, const struct row *r, int i) +{ + char s[64], tmpdir[64], errf[80], cmd[1280]; + snprintf(s, sizeof s, "/tmp/tsn_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/tsn_%d_d_%d", getpid(), i); + snprintf(errf, sizeof errf, "/tmp/tsn_%d_%d.err", getpid(), i); + + FILE *f = fopen(s, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>%s", + tmpdir, driver, s, errf); + int rc = runwait(cmd); + + int have_diag = 0; + FILE *e = fopen(errf, "rb"); + if (e) { + char buf[4096]; + size_t n = fread(buf, 1, sizeof buf - 1, e); + buf[n] = '\0'; + fclose(e); + have_diag = (strstr(buf, r->diag) != NULL); + } + + unlink(s); unlink(errf); + const char *base = strrchr(s, '/'); + base = base ? base + 1 : s; + char outbin[128]; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + unlink(outbin); + rmdir(tmpdir); + + /* build must NOT succeed AND the shared diagnostic must appear. */ + return (rc != 0 && have_diag) ? 0 : -1; +} + +/* asm_byte_identical — w6c vs w6c_ww .s for the same source must match. */ +static int +asm_byte_identical(const char *bin, const struct row *r, int i) +{ + char src[64], cs[64], ws[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/tsi_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/tsi_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/tsi_asm_%d_%d_w.s", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c errored\n", r->label); + unlink(src); + return -1; + } + snprintf(cmd, sizeof cmd, "%s/w6c_ww -o %s %s 2>/dev/null", + bin, ws, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c_ww errored\n", r->label); + unlink(src); unlink(cs); + return -1; + } + + FILE *fc = fopen(cs, "rb"); + FILE *fw = fopen(ws, "rb"); + int rc = 0; + if (!fc || !fw) { + rc = -1; + } else { + for (;;) { + int a = fgetc(fc); + int b = fgetc(fw); + if (a != b) { rc = -1; break; } + if (a == EOF) break; + } + } + if (fc) fclose(fc); + if (fw) fclose(fw); + if (rc != 0) + fprintf(stderr, "row[%s]: cstage vs wwstage asm differs\n", + r->label); + unlink(src); unlink(cs); unlink(ws); + 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 cdrv[1024], wdrv[1024]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + struct { const char *name; const char *drv; int gated; } + drivers[] = { + { "cstage", cdrv, 0 }, + { "wwstage", wdrv, 1 }, + { NULL, NULL, 0 }, + }; + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + + for (int d = 0; drivers[d].name; d++) { + if (drivers[d].gated && access(drivers[d].drv, X_OK) != 0) { + fprintf(stderr, "tagged_staticinit: skip %s (no %s)\n", + drivers[d].name, drivers[d].drv); + continue; + } + for (int i = 0; i < n; i++) { + total++; + if (rows[i].expect_build) { + int got = run_build(drivers[d].drv, &rows[i], i); + if (got != rows[i].want_exit) { + fprintf(stderr, "tagged_staticinit[%s][%s]: " + "exit=%d want=%d\n", drivers[d].name, + rows[i].label, got, rows[i].want_exit); + fail++; + } + } else { + if (build_should_reject(drivers[d].drv, &rows[i], + 100 + i) != 0) { + fprintf(stderr, "tagged_staticinit[%s][%s]: " + "expected a loud reject with \"%s\"\n", + drivers[d].name, rows[i].label, rows[i].diag); + fail++; + } + } + } + } + + /* RUN rows must be cstage/wwstage asm byte-id (rule 10). */ + if (access(wdrv, X_OK) == 0) { + for (int i = 0; i < n; i++) { + if (!rows[i].expect_build) continue; + total++; + if (asm_byte_identical(bin, &rows[i], i) != 0) + fail++; + } + } + + if (fail) { + fprintf(stderr, "tagged_staticinit: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("tagged_staticinit: %d/%d ok\n", total, total); + return 0; +}