From 0d96196f90fd6052ad0ba9133178f7133c3cfc03 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 18 May 2026 12:01:34 +0900 Subject: [PATCH] cstage+test: walk fields for sret callee struct-copy width (#33) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Callee N_IDENT word-copy loop was driven off slot-padded rt->size; trailing narrow field (e.g. bool@32 in 33B/40B struct) widened to MOVQ at the loop tail, diverging from wwstage's natural-size MOVB. Class A cgen divergence. 9th unmask of session 5, corpus-coverage- blind on the cstage side — no in-tree lib struct had a narrow (bool/u8/i8/i16) trailing field until lib/strings.iterator landed `reverse: bool` per Hare's ref/hare/strings/iter.ha:8. Pre-fix: cstage cgreturn's sret arm (cgen.c) used `int sz = (int)rt->size` for its chained `while (k+8<=sz)` MOVQ-MOVL-MOVW-MOVB copy loop. rt->size is slot-padded (8-rounded for downstream frame alloc), e.g. 40B for {i32, []u8, bool}. Loop emitted MOVQ at offset 32 covering the 1-byte bool tail plus 7 bytes of padding into the caller's sret slot — clobbering the next 7 bytes of caller frame on read-side. Wwstage's mirror loop drives off sretretsize → structnaturalsize → max(foff+fsz) = 33B, so it correctly stops at offset 32 and emits MOVB. Polarity catalog: cstage OVER-WIDE — slot-padded size driving the field-copy width. Convergence cstage → wwstage's structnaturalsize discipline (rule 10 inverse: leaner-correct side wins). Fix: compute natural size locally in the N_IDENT/N_STRUCTLIT sret arm via walk over `rt->fields` (max foff+fsz), mirroring wwstage's `structnaturalsize` (cgenutil.ww:1377). Frame allocation and `cg_sret_retsize` (used for the caller-side @sretscr slot) intentionally keep using rt->size — caller scratch sizing is separate from callee per-field copy width. Surfaced by lib/strings commit-2 (#30) iterator pre-flight when the Hare-faithful `reverse: bool` field tripped the cstage-only mis-width on 993/995 byte-id (strconv → strings → wwdump_ww + w6c_ww). lib/strings c2 was stashed to land cleanly after this fix. Note (out-of-scope): the chained MOV loop has no MOVW arm in either stage, so a trailing i16 emits 2× MOVB at consecutive offsets. Worth a future cleanup; both stages agree today. Tests: - 730_sret_narrow_field pins narrow-MOV store + load width and no-MOVQ@trailing-offset assertions for bool / u8 / i16 / i32 trailing fields in a 33B-natural struct. 4 rows × 5 sentinels = 20 fixtures. cmp -s cstage vs wwstage byte-id per row. - 930_sret_narrow_field_run runtime-pins bool true/false, u8 high-bit, i16 negative, i32 negative, mixed (bool+i32+i64 after slice) — 6 scenarios × 2 stages = 12 rows. 104/104 ok. 995_self_rebuild stays green (ww2==ww3==ww4 byte-id). --- Makefile | 12 ++ cmd/w6c/cgen.c | 15 +- test/wcc/730_sret_narrow_field.c | 276 +++++++++++++++++++++++++++ test/wcc/930_sret_narrow_field_run.c | 230 ++++++++++++++++++++++ 4 files changed, 532 insertions(+), 1 deletion(-) create mode 100644 test/wcc/730_sret_narrow_field.c create mode 100644 test/wcc/930_sret_narrow_field_run.c diff --git a/Makefile b/Makefile index 3b67082b..c23bf0b0 100644 --- a/Makefile +++ b/Makefile @@ -250,6 +250,8 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_tagged_call_arg_run \ $(BIN)/test_sret_struct_return \ $(BIN)/test_sret_struct_return_run \ + $(BIN)/test_sret_narrow_field \ + $(BIN)/test_sret_narrow_field_run \ $(BIN)/test_match_slice_variant \ $(BIN)/test_match_slice_variant_run \ $(BIN)/test_composite_call_arg \ @@ -556,6 +558,16 @@ $(BIN)/test_sret_struct_return_run: test/wcc/925_sret_struct_return_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_sret_narrow_field: test/wcc/730_sret_narrow_field.c \ + $(BIN)/w6c $(BIN)/w6c_ww | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + +$(BIN)/test_sret_narrow_field_run: test/wcc/930_sret_narrow_field_run.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_match_slice_variant: test/wcc/722_match_slice_variant.c \ $(BIN)/w6c $(BIN)/w6c_ww | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index f4e06afe..f5d38fb8 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -6611,7 +6611,20 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) && (int)rt->size > 24 && (n->lhs->kind == N_IDENT || n->lhs->kind == N_STRUCTLIT)) { - int sz = (int)rt->size; + /* Natural size = max(foff + fsz) over declared + * fields; mirrors selfhost cgenutil.ww + * structnaturalsize / sretretsize. Pre-fix this + * used the slot-padded rt->size, so a trailing + * narrow field (e.g. bool@32 in a 33B struct + * padded to 40B) widened to an 8B MOVQ at the + * loop tail — diverged from wwstage's MOVB + * tail. Task #33, Class A. */ + int sz = 0; + for (Tfield *fl = rt->fields; fl; fl = fl->next) { + int end = (int)fl->offset + (int)(fl->type + ? fl->type->size : 8); + if (end > sz) sz = end; + } if (n->lhs->kind == N_STRUCTLIT) { /* Delegate to the shared *-relative * fill helper. Same store sequence the diff --git a/test/wcc/730_sret_narrow_field.c b/test/wcc/730_sret_narrow_field.c new file mode 100644 index 00000000..ce816eaa --- /dev/null +++ b/test/wcc/730_sret_narrow_field.c @@ -0,0 +1,276 @@ +/* + * 730_sret_narrow_field — Class A asm-presence + byte-id for the + * sret callee's N_IDENT word-copy tail when the returned struct + * ends in a narrow primitive (bool / u8 / i8 / i16 / i32). + * + * Task #33. Cstage pre-fix used the slot-padded `rt->size` to drive + * the field-copy loop's MOVQ/MOVL/MOVB tail; for a struct whose + * natural size is unaligned (e.g. `{ i32, []u8, bool }` natural=33, + * padded=40), the trailing MOVQ at offset 32 widened a 1-byte bool + * into an 8-byte load+store — diverged from wwstage's correct MOVB + * (which used natural size via `sretretsize` / `structnaturalsize`). + * Corpus-coverage-blind: no in-tree stdlib struct had a bool field + * at any offset until lib/strings.iterator landed (Hare's + * `iterator { dec, reverse }` shape). + * + * Sentinel per row: in the `mk` body, between the final pre-RET + * "RAX = @sretarg load" pair (the SysV return-the-pointer + * discipline) and the first MOVQ word-store into (BX), assert + * the last copy instruction targets the narrow MOV width derived + * from the final field's type — NOT MOVQ. The trailing-field offset + * is hard-coded per row (structurally fixed) and we pin both the + * read-from-(BP) and store-to-(BX) sides. + * + * Plus byte-id between stages per row. + */ +#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; +} + +/* `off` = byte offset of the trailing narrow field inside the + * struct (= natural offset of that field). `mov` = expected mnemonic + * for the trailing field-copy at that offset (MOVB / MOVW / MOVL). + * Rows all share the shape `{ i32 a, []u8 s, NARROW r }` so a sits + * at 0..4, s at 8..32, r at 32..32+sz(r). */ +struct row { const char *label; const char *src; int off; const char *mov; }; + +static const struct row rows[] = { + { "trailing_bool", + "type t = struct { a: i32, s: []u8, r: bool };\n" + "export fn mk() t = {\n" + " let v: t; let z: []u8;\n" + " v.s = z; v.a = 0; v.r = false;\n" + " return v;\n" + "};\n" + "export fn main() i32 = {\n" + " let x: t = mk();\n" + " if (x.r) { return 1; };\n" + " return 0;\n" + "};\n", 32, "MOVB" }, + { "trailing_u8", + "type t = struct { a: i32, s: []u8, r: u8 };\n" + "export fn mk() t = {\n" + " let v: t; let z: []u8;\n" + " v.s = z; v.a = 0; v.r = 0u8;\n" + " return v;\n" + "};\n" + "export fn main() i32 = {\n" + " let x: t = mk();\n" + " if (x.r != 0u8) { return 1; };\n" + " return 0;\n" + "};\n", 32, "MOVB" }, + /* Trailing i16: natural size 34. The chained loop has only + * MOVQ/MOVL/MOVB arms (matching wwstage's identical chain), + * so a 2-byte tail falls through to 2× MOVB at offsets 32, 33. + * That's still narrow + byte-id; the bug-check is the negative + * "no MOVQ at offset 32". Promoting both stages to a MOVW arm + * is a separate refactor (preserves byte-id but out of #33). */ + { "trailing_i16", + "type t = struct { a: i32, s: []u8, r: i16 };\n" + "export fn mk() t = {\n" + " let v: t; let z: []u8;\n" + " v.s = z; v.a = 0; v.r = 0i16;\n" + " return v;\n" + "};\n" + "export fn main() i32 = {\n" + " let x: t = mk();\n" + " if (x.r != 0i16) { return 1; };\n" + " return 0;\n" + "};\n", 32, "MOVB" }, + { "trailing_i32", + "type t = struct { a: i32, s: []u8, r: i32 };\n" + "export fn mk() t = {\n" + " let v: t; let z: []u8;\n" + " v.s = z; v.a = 0; v.r = 0;\n" + " return v;\n" + "};\n" + "export fn main() i32 = {\n" + " let x: t = mk();\n" + " if (x.r != 0) { return 1; };\n" + " return 0;\n" + "};\n", 32, "MOVL" }, +}; + +static int +emit_s(const char *w6c, const struct row *r, int i, char *out_s, size_t cap) +{ + char src[96], cmd[1024]; + snprintf(src, sizeof src, "/tmp/sret_narrow_%d_%d.ww", getpid(), i); + snprintf(out_s, cap, "/tmp/sret_narrow_%d_%d_%s.s", + getpid(), i, w6c[strlen(w6c) - 1] == 'w' ? "ww" : "c"); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, out_s, src); + int rc = runwait(cmd); + unlink(src); + return rc; +} + +/* Asm-presence sentinel: inside mk's body (between `TEXT mk,` and + * the first `RET`), the trailing field-copy at offset `r->off` must + * use the narrow MOV mnemonic — both the read-from-(BP) and the + * store-to-(BX) lines. Pre-fix the cstage emitted MOVQ at that + * offset (8-byte over-wide) because the loop drove off slot-padded + * size; wwstage emitted the narrow MOV via natural size. */ +static int +check_narrow_mov(const char *path, const struct row *r) +{ + FILE *f = fopen(path, "rb"); + if (!f) return -1; + char line[1024]; + int in_mk = 0; + int saw_store = 0; + int saw_load = 0; + char store_needle[64], load_needle[64]; + snprintf(store_needle, sizeof store_needle, "%s\tAX, %d(BX)\n", + r->mov, r->off); + snprintf(load_needle, sizeof load_needle, "%d(BP), AX", + -16); /* not used as exact-match — see contains below */ + (void)load_needle; + while (fgets(line, sizeof line, f)) { + if (!in_mk) { + if (strstr(line, "TEXT mk,") + || strstr(line, "TEXT\tmk,")) + in_mk = 1; + continue; + } + if (strstr(line, "\tRET\n")) break; + if (strstr(line, store_needle)) saw_store = 1; + /* load: `\tMOV?\t-K(BP), AX\n` with the matching width. */ + char loadkey[16]; + snprintf(loadkey, sizeof loadkey, "%s\t", r->mov); + if (strstr(line, loadkey) && strstr(line, "(BP), AX")) + saw_load = 1; + } + fclose(f); + if (!saw_store) { + fprintf(stderr, + "row[%s]: expected `%s AX, %d(BX)` in mk body\n", + r->label, r->mov, r->off); + return -1; + } + if (!saw_load) { + fprintf(stderr, + "row[%s]: expected narrow `%s -K(BP), AX` load in mk\n", + r->label, r->mov); + return -1; + } + return 0; +} + +/* Negative sentinel: pre-fix bug pattern. cstage emitted + * `MOVQ AX, off>(BX)` for the narrow trailing field — assert + * absence in mk body. */ +static int +check_no_movq_at_off(const char *path, const struct row *r) +{ + FILE *f = fopen(path, "rb"); + if (!f) return -1; + char line[1024]; + int in_mk = 0; + char needle[64]; + snprintf(needle, sizeof needle, "MOVQ\tAX, %d(BX)\n", r->off); + int bad = 0; + while (fgets(line, sizeof line, f)) { + if (!in_mk) { + if (strstr(line, "TEXT mk,") + || strstr(line, "TEXT\tmk,")) + in_mk = 1; + continue; + } + if (strstr(line, "\tRET\n")) break; + if (strstr(line, needle)) { bad = 1; break; } + } + fclose(f); + if (bad) { + fprintf(stderr, + "row[%s]: unexpected `MOVQ AX, %d(BX)` (pre-#33" + " over-wide pattern) in mk\n", r->label, r->off); + return -1; + } + return 0; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[512]; + if (bin[0] != '/') { + char cwd[256]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char w6c[640], w6c_ww[640]; + snprintf(w6c, sizeof w6c, "%s/w6c", bin); + snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); + + int have_ww = (access(w6c_ww, X_OK) == 0); + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + + for (int i = 0; i < n; i++) { + char cs_path[128], ws_path[128]; + + if (emit_s(w6c, &rows[i], i, cs_path, sizeof cs_path) != 0) { + fprintf(stderr, "row[%s]: w6c failed\n", rows[i].label); + fail++; total++; continue; + } + total += 2; + if (check_narrow_mov(cs_path, &rows[i]) != 0) fail++; + if (check_no_movq_at_off(cs_path, &rows[i]) != 0) fail++; + + if (!have_ww) { unlink(cs_path); continue; } + + if (emit_s(w6c_ww, &rows[i], i, ws_path, sizeof ws_path) != 0) { + fprintf(stderr, + "row[%s]: w6c_ww failed\n", rows[i].label); + fail++; total++; + unlink(cs_path); + continue; + } + total += 2; + if (check_narrow_mov(ws_path, &rows[i]) != 0) fail++; + if (check_no_movq_at_off(ws_path, &rows[i]) != 0) fail++; + + total++; + char cmd[512]; + snprintf(cmd, sizeof cmd, "cmp -s %s %s", cs_path, ws_path); + if (runwait(cmd) != 0) { + fprintf(stderr, + "row[%s]: cstage vs wwstage asm differs\n", + rows[i].label); + fail++; + } + + unlink(cs_path); unlink(ws_path); + } + + if (fail) { + fprintf(stderr, + "sret_narrow_field: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("sret_narrow_field: %d/%d ok\n", total, total); + return 0; +} diff --git a/test/wcc/930_sret_narrow_field_run.c b/test/wcc/930_sret_narrow_field_run.c new file mode 100644 index 00000000..387b7d39 --- /dev/null +++ b/test/wcc/930_sret_narrow_field_run.c @@ -0,0 +1,230 @@ +/* + * 930_sret_narrow_field_run — Class B semantic round-trip for the + * sret callee's narrow trailing-field copy (task #33). End-to-end + * pins that values cross the >24B struct-return boundary intact + * when the struct ends in a narrow primitive (bool / u8 / i16 / + * i32), including a mixed-width struct that interleaves several + * narrow fields after the 24B slice payload. + * + * Pre-fix cstage's N_IDENT word-copy tail used slot-padded + * `rt->size` and emitted MOVQ for the narrow trailing field — + * the narrow source slot was BP-relative low and bordered the + * @sretarg save (8B), so the 8-byte read at offset 32 swept in + * the saved RDI byte and either wrote garbage past the bool / + * misaligned the next stack slot at the callee's frame edge. + * + * Class A asm-presence is pinned at 730; this file catches + * shared miscompiles that asm byte-id can't see. + */ +#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; }; + +static const struct row rows[] = { + /* Trailing bool — the original surfacing case (lib/strings + * iterator shape). Pre-fix the bool round-tripped to a + * neighbour byte (the @sretarg low byte at -8(BP)), which on + * the local stack frame was 0x01 (the saved RDI pointer low + * byte). Test sets r=true and confirms the value survives. */ + { "bool_true", + "type t = struct { a: i32, s: []u8, r: bool };\n" + "fn mk() t = {\n" + " let v: t; let z: []u8;\n" + " v.s = z; v.a = 7; v.r = true;\n" + " return v;\n" + "};\n" + "export fn main() i32 = {\n" + " let x: t = mk();\n" + " if (x.a != 7) { return 1; };\n" + " if (!x.r) { return 2; };\n" + " return 0;\n" + "};\n", + 0 }, + { "bool_false", + "type t = struct { a: i32, s: []u8, r: bool };\n" + "fn mk() t = {\n" + " let v: t; let z: []u8;\n" + " v.s = z; v.a = 9; v.r = false;\n" + " return v;\n" + "};\n" + "export fn main() i32 = {\n" + " let x: t = mk();\n" + " if (x.a != 9) { return 1; };\n" + " if (x.r) { return 2; };\n" + " return 0;\n" + "};\n", + 0 }, + /* Trailing u8 with a high bit set. Pre-fix MOVQ-load at the + * source slot's offset would clobber the high 7 bytes of the + * read into AX with whatever lived above the byte slot. */ + { "u8_high_bit", + "type t = struct { a: i32, s: []u8, r: u8 };\n" + "fn mk() t = {\n" + " let v: t; let z: []u8;\n" + " v.s = z; v.a = 0; v.r = 0xFFu8;\n" + " return v;\n" + "};\n" + "export fn main() i32 = {\n" + " let x: t = mk();\n" + " if (x.r != 0xFFu8) { return 1; };\n" + " return 0;\n" + "};\n", + 0 }, + /* Trailing i16 with a negative bit pattern: the MOVW path + * is exercised, and the receive site's sign extension must + * read only 2 bytes. */ + { "i16_negative", + "type t = struct { a: i32, s: []u8, r: i16 };\n" + "fn mk() t = {\n" + " let v: t; let z: []u8;\n" + " v.s = z; v.a = 0; v.r = -123i16;\n" + " return v;\n" + "};\n" + "export fn main() i32 = {\n" + " let x: t = mk();\n" + " if (x.r != -123i16) { return 1; };\n" + " return 0;\n" + "};\n", + 0 }, + /* Trailing i32 negative: exercises the MOVL-tail path of the + * fixed loop (k+4<=sz arm; pre-fix the k+8<=40 arm absorbed + * this offset too). */ + { "i32_negative", + "type t = struct { a: i32, s: []u8, r: i32 };\n" + "fn mk() t = {\n" + " let v: t; let z: []u8;\n" + " v.s = z; v.a = 0; v.r = -424242;\n" + " return v;\n" + "};\n" + "export fn main() i32 = {\n" + " let x: t = mk();\n" + " if (x.r != -424242) { return 1; };\n" + " return 0;\n" + "};\n", + 0 }, + /* Mixed-width narrows interleaved after the 24B slice: bool + + * i32 + i64 mix into one struct. All three trailing fields + * survive intact only if each emits its declared width per + * field's natural offset/size. */ + { "mixed_narrow_after_slice", + "type t = struct { s: []u8, r: bool, n: i32, k: i64 };\n" + "fn mk() t = {\n" + " let v: t; let z: []u8;\n" + " v.s = z; v.r = true; v.n = 1234567; v.k = 9876543210i64;\n" + " return v;\n" + "};\n" + "export fn main() i32 = {\n" + " let x: t = mk();\n" + " if (!x.r) { return 1; };\n" + " if (x.n != 1234567) { return 2; };\n" + " if (x.k != 9876543210i64) { return 3; };\n" + " return 0;\n" + "};\n", + 0 }, +}; + +static int +run_driver(const char *driver, const struct row *r, int i) +{ + char src[96], tmpdir[96], cmd[1024]; + snprintf(src, sizeof src, "/tmp/sret_nr_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/sret_nr_%d_d_%d", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s build %s", + tmpdir, driver, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: build via %s failed\n", + r->label, driver); + unlink(src); rmdir(tmpdir); + return -1; + } + + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + char outbin[160]; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + int got = runwait(outbin); + + unlink(src); unlink(outbin); rmdir(tmpdir); + return got; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[512]; + if (bin[0] != '/') { + char cwd[256]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char cdrv[640]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + char wdrv[640]; + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + struct { const char *name; const char *path; int gated_on_existence; } + 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_on_existence + && access(drivers[d].path, X_OK) != 0) { + fprintf(stderr, + "sret_narrow_field_run: skip %s (no %s)\n", + drivers[d].name, drivers[d].path); + continue; + } + for (int i = 0; i < n; i++) { + int got = run_driver(drivers[d].path, &rows[i], i); + total++; + if (got != rows[i].want) { + fprintf(stderr, + "sret_narrow_field_run[%s][%s]: exit=%d want=%d\n", + drivers[d].name, rows[i].label, + got, rows[i].want); + fail++; + } + } + } + + if (fail) { + fprintf(stderr, + "sret_narrow_field_run: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("sret_narrow_field_run: %d/%d ok\n", total, total); + return 0; +}