From 4e1181fd8ceebff44a7d7f7f4ffa0d55fa1e7ce6 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 29 May 2026 15:27:50 +0900 Subject: [PATCH] wcc: resolve value-receiver field-call type via callee, not global leaf (#208) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wwstage exprtype's N_CALL arm fell into a global-leaf scopelookup for an N_DOT callee with a value or chained receiver, binding whatever same-named global headed the scope bucket. Under a late-os combined.ww concat order this resolved io's `s.read(...)` to os.read (i64) instead of the field's fn type, so checkretassign confidently rejected a valid tagged return — an import-order-sensitive false positive. cstage resolves a call result solely from the callee expr's own type (check.c:1378-1433, mirroring harec check_autodereference 1566-1581); drop the global-leaf else-arm so value and chained receivers fall through to the existing fn-VALUE path at check.ww:2455. SK_USE module-qualified calls are unchanged. ww has no methods, so `value.leaf()` is only ever a fn-ptr field access; the global hit was never legitimate. Zero .s delta across all 5 bootstrap tools (the branch is dead in the bootstrap); test 776 graduates to both stages (os-late order, byte-identical). The fix unmasks a pre-existing wwstage cgen bug (#211): cgen also re-derives a call's return shape by name (fnretlookup), so a value-receiver field call whose leaf collides with a same-named global of a different register shape mis-resolves cs!=ww. Documented at the cgen site; pinned cstage-only by test/wcc/782 (graduates to STAGE_WW on #211 close). --- Makefile | 8 + selfhost/cmd/w6c/main.combined.ww | 37 ++++- selfhost/cmd/wcc/cgen.ww | 20 +++ selfhost/cmd/wcc/check.ww | 17 ++- selfhost/cmd/wwdump/main.combined.ww | 37 ++++- test/wcc/776_memio_vstream_run.c | 41 ++--- test/wcc/782_fieldfn_leaf_collide_run.c | 190 ++++++++++++++++++++++++ 7 files changed, 317 insertions(+), 33 deletions(-) create mode 100644 test/wcc/782_fieldfn_leaf_collide_run.c diff --git a/Makefile b/Makefile index 745a1c54..074d4278 100644 --- a/Makefile +++ b/Makefile @@ -330,6 +330,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_fmt_vstream_run \ $(BIN)/test_fmt_vstream_mods_run \ $(BIN)/test_fmt_vstream_compositions_run \ + $(BIN)/test_fieldfn_leaf_collide_run \ $(BIN)/test_bufio_vstream_run \ $(BIN)/test_log_vstream_run \ $(BIN)/test_use_promote_alias \ @@ -708,6 +709,13 @@ $(BIN)/test_fmt_vstream_run: test/wcc/777_fmt_vstream_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# 782 — cstage-only pin for #211 (cgen sibling of #208). Self-contained +# single-file probe (no lib imports); cstage driver only, so no ww_ww dep. +$(BIN)/test_fieldfn_leaf_collide_run: test/wcc/782_fieldfn_leaf_collide_run.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_fmt_vstream_mods_run: test/wcc/780_fmt_vstream_mods_run.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index ebdcd887..1735f67b 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -12458,10 +12458,23 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { if (mu != nil) { ms = mu; }; }; }; + // #208: only a module-qualified callee (SK_USE receiver) + // resolves its leaf by name here. A value receiver + // (`s.read(...)`, `(*p).read()`, `a.b.read()`) is a + // fn-pointer FIELD call whose result type comes from the + // FIELD's fn type, not a global-leaf lookup. The old + // `scopelookup(c.cur, nm)` else-arm bound whichever + // same-leaf global fn headed the flat scope bucket — + // order-sensitive (os.read:i64 vs io.read:(i32|eof|closed) + // flipped by combined.ww concat order), yielding a + // false `return: not assignable`. Leaving s nil falls + // through to the fn-VALUE path below (exprtype(callee) → + // TPTR peel → TFN.ret), matching cstage cmd/wcc/check.c + // :1378-1433 (call result IS the callee type's ret; no + // global-leaf path) and harec check_autodereference + // (ref/harec/src/check.c:1566-1581). if (ms != nil && ms.skind == skind.SK_USE) { s = scopelookupinmodule(c.cur, callee.lhs.str, nm); - } else { - s = scopelookup(c.cur, nm); }; }; if (s != nil) { if (s.skind == skind.SK_FN) { if (s.decl != nil) { @@ -30087,6 +30100,26 @@ fn collectfnrets(c: *cgen, file: *node) void = { // return type (str-pair shuffle, tagged-union ABI, tuple destructure, // float ABI, sret slot sizing, fn-rvalue LEAQ, slice flow) fires // against the wrong-module shape. +// fnretlookup — the called fn's declared return type, keyed by NAME +// (same-module-first, then first leaf match). The receive sites that +// re-derive a call's result SHAPE from this (cglet tagged-store, +// cgwidentaggedstore scalar-vs-tagged classify, tuple/sret/unsigned +// arms) are correct only when the leaf name uniquely picks the callee. +// +// #211 (gate-blind cgen divergence, sibling of the #208 checker fix): a +// VALUE-receiver fn-pointer FIELD call `s.f(...)` reaches the receive +// sites keyed on the field leaf `f` with the receiver VARIABLE name as +// the "module" (not a real module), so this lookup mis-binds a same-named +// GLOBAL fn. When that global's register shape differs from the field's +// (scalar global vs tagged field), the slot is stored with the wrong ABI +// shape → cstage≠wwstage asm, silent miscompile. The sound fix derives +// the result from the FIELD's fn type / the checker-stamped n.type_ (as +// cstage does, cmd/wcc/check.c:1378-1433), not by leaf name. NO guard is +// added here: same-shape leaf collisions resolve by name legitimately +// today, and a discriminating guard would need the shape-compare that IS +// the fix. Masked until #208 landed (the checker rejected the shape +// before cgen ran). test/wcc/782 pins the cstage-correct runtime +// (cstage-only) and graduates to STAGE_WW on #211 close. fn fnretlookup(c: *cgen, name: str) *node = { let f: *fnret = c.fnrets; for (f != nil) { diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index c79ad340..f09689f6 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -2235,6 +2235,26 @@ fn collectfnrets(c: *cgen, file: *node) void = { // return type (str-pair shuffle, tagged-union ABI, tuple destructure, // float ABI, sret slot sizing, fn-rvalue LEAQ, slice flow) fires // against the wrong-module shape. +// fnretlookup — the called fn's declared return type, keyed by NAME +// (same-module-first, then first leaf match). The receive sites that +// re-derive a call's result SHAPE from this (cglet tagged-store, +// cgwidentaggedstore scalar-vs-tagged classify, tuple/sret/unsigned +// arms) are correct only when the leaf name uniquely picks the callee. +// +// #211 (gate-blind cgen divergence, sibling of the #208 checker fix): a +// VALUE-receiver fn-pointer FIELD call `s.f(...)` reaches the receive +// sites keyed on the field leaf `f` with the receiver VARIABLE name as +// the "module" (not a real module), so this lookup mis-binds a same-named +// GLOBAL fn. When that global's register shape differs from the field's +// (scalar global vs tagged field), the slot is stored with the wrong ABI +// shape → cstage≠wwstage asm, silent miscompile. The sound fix derives +// the result from the FIELD's fn type / the checker-stamped n.type_ (as +// cstage does, cmd/wcc/check.c:1378-1433), not by leaf name. NO guard is +// added here: same-shape leaf collisions resolve by name legitimately +// today, and a discriminating guard would need the shape-compare that IS +// the fix. Masked until #208 landed (the checker rejected the shape +// before cgen ran). test/wcc/782 pins the cstage-correct runtime +// (cstage-only) and graduates to STAGE_WW on #211 close. fn fnretlookup(c: *cgen, name: str) *node = { let f: *fnret = c.fnrets; for (f != nil) { diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 579600d2..25edd362 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -2428,10 +2428,23 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { if (mu != nil) { ms = mu; }; }; }; + // #208: only a module-qualified callee (SK_USE receiver) + // resolves its leaf by name here. A value receiver + // (`s.read(...)`, `(*p).read()`, `a.b.read()`) is a + // fn-pointer FIELD call whose result type comes from the + // FIELD's fn type, not a global-leaf lookup. The old + // `scopelookup(c.cur, nm)` else-arm bound whichever + // same-leaf global fn headed the flat scope bucket — + // order-sensitive (os.read:i64 vs io.read:(i32|eof|closed) + // flipped by combined.ww concat order), yielding a + // false `return: not assignable`. Leaving s nil falls + // through to the fn-VALUE path below (exprtype(callee) → + // TPTR peel → TFN.ret), matching cstage cmd/wcc/check.c + // :1378-1433 (call result IS the callee type's ret; no + // global-leaf path) and harec check_autodereference + // (ref/harec/src/check.c:1566-1581). if (ms != nil && ms.skind == skind.SK_USE) { s = scopelookupinmodule(c.cur, callee.lhs.str, nm); - } else { - s = scopelookup(c.cur, nm); }; }; if (s != nil) { if (s.skind == skind.SK_FN) { if (s.decl != nil) { diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 7be553db..d360a848 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -12458,10 +12458,23 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { if (mu != nil) { ms = mu; }; }; }; + // #208: only a module-qualified callee (SK_USE receiver) + // resolves its leaf by name here. A value receiver + // (`s.read(...)`, `(*p).read()`, `a.b.read()`) is a + // fn-pointer FIELD call whose result type comes from the + // FIELD's fn type, not a global-leaf lookup. The old + // `scopelookup(c.cur, nm)` else-arm bound whichever + // same-leaf global fn headed the flat scope bucket — + // order-sensitive (os.read:i64 vs io.read:(i32|eof|closed) + // flipped by combined.ww concat order), yielding a + // false `return: not assignable`. Leaving s nil falls + // through to the fn-VALUE path below (exprtype(callee) → + // TPTR peel → TFN.ret), matching cstage cmd/wcc/check.c + // :1378-1433 (call result IS the callee type's ret; no + // global-leaf path) and harec check_autodereference + // (ref/harec/src/check.c:1566-1581). if (ms != nil && ms.skind == skind.SK_USE) { s = scopelookupinmodule(c.cur, callee.lhs.str, nm); - } else { - s = scopelookup(c.cur, nm); }; }; if (s != nil) { if (s.skind == skind.SK_FN) { if (s.decl != nil) { @@ -30087,6 +30100,26 @@ fn collectfnrets(c: *cgen, file: *node) void = { // return type (str-pair shuffle, tagged-union ABI, tuple destructure, // float ABI, sret slot sizing, fn-rvalue LEAQ, slice flow) fires // against the wrong-module shape. +// fnretlookup — the called fn's declared return type, keyed by NAME +// (same-module-first, then first leaf match). The receive sites that +// re-derive a call's result SHAPE from this (cglet tagged-store, +// cgwidentaggedstore scalar-vs-tagged classify, tuple/sret/unsigned +// arms) are correct only when the leaf name uniquely picks the callee. +// +// #211 (gate-blind cgen divergence, sibling of the #208 checker fix): a +// VALUE-receiver fn-pointer FIELD call `s.f(...)` reaches the receive +// sites keyed on the field leaf `f` with the receiver VARIABLE name as +// the "module" (not a real module), so this lookup mis-binds a same-named +// GLOBAL fn. When that global's register shape differs from the field's +// (scalar global vs tagged field), the slot is stored with the wrong ABI +// shape → cstage≠wwstage asm, silent miscompile. The sound fix derives +// the result from the FIELD's fn type / the checker-stamped n.type_ (as +// cstage does, cmd/wcc/check.c:1378-1433), not by leaf name. NO guard is +// added here: same-shape leaf collisions resolve by name legitimately +// today, and a discriminating guard would need the shape-compare that IS +// the fix. Masked until #208 landed (the checker rejected the shape +// before cgen ran). test/wcc/782 pins the cstage-correct runtime +// (cstage-only) and graduates to STAGE_WW on #211 close. fn fnretlookup(c: *cgen, name: str) *node = { let f: *fnret = c.fnrets; for (f != nil) { diff --git a/test/wcc/776_memio_vstream_run.c b/test/wcc/776_memio_vstream_run.c index 2c77290f..64e78853 100644 --- a/test/wcc/776_memio_vstream_run.c +++ b/test/wcc/776_memio_vstream_run.c @@ -46,33 +46,20 @@ * | (mirror of 775's branched_readers * | row for the vtable-init side). * - * IMPORT-ORDER WORKAROUND (pre-existing wwstage gap, NOT introduced - * here): every row places `import os;` FIRST. ww_ww's combined.ww - * concatenation follows the import-discovery order; an os-late - * ordering (io → rt → os) trips the wwstage checker on os.tryread / - * trywrite / tryopen's bare `return r;` over a tagged return type - * (3 false-positive "return: not assignable (i64 → )"/"(i32 → )" - * errors). The os-first ordering matches selfhost's combined.ww - * (time → os → rt → …) where the checker resolves cleanly. The 980 - * memio_run / 990 / 995 byte-id gates do not hit this path because - * they either drive cs-only (980) or operate on a much wider type - * surface (990/995). Filed as a sibling task; bare `os` import in a - * small probe context is what surfaces it. Workaround drops out once - * the wwstage checker stops ordering-sensitively on bare-int return - * to tagged-int union. + * IMPORT ORDER (#208 CLOSED): every row imports `memio; io; os;` — + * os LAST. This is the formerly-failing os-late ordering. Pre-#208 it + * tripped the wwstage checker on io.read/write/close's `return s.read( + * s, buf)` (a fn-pointer field call) with 3 false "return: not + * assignable (i64 → )"/"(i32 → )" — because exprtype re-bound the field + * leaf to a same-named scalar global (os.read:i64) instead of the + * field's tagged fn type. #208 fixed exprtype to resolve value-receiver + * field calls from the field type (cmd/wcc/check.c:1378-1433 twin), so + * the order no longer matters and all 4 rows now run STAGE_CS|STAGE_WW + * byte-id. The earlier `import os;`-FIRST workaround is retired here. * * SIBLINGS (filed inline, NOT fixed here — fold-e2 is purely * additive over fold-e1's frozen io.* surface): * - * - WWSTAGE-IMPORT-ORDER: `import os;` must appear before `import - * memio;` / `import io;` in a small probe context. ww_ww's - * combined.ww concat order trips the wwstage checker's - * `(i64 | T)` / `(i32 | T)` return-assignability when bare - * int-returning fns (os.tryread / trywrite / tryopen) are - * checked before certain ordering-sensitive pre-resolved types. - * cstage accepts unconditionally. Symmetric to the wwstage - * ordering gaps already filed as #189 / #190 / #202. - * * - #173 (TRY-on-tagged-return both-stages broken) blocks * fixed_write_v from returning Hare's `nomem` when full; * vstream.ww surfaces 0 instead (memio.ww:155 OLD divergence). @@ -120,9 +107,9 @@ struct row { static const struct row rows[] = { { "fixed_read_5", "package main;\n" - "import os;\n" "import memio;\n" "import io;\n" + "import os;\n" "export fn main() i32 = {\n" " let buf: [8]u8;\n" " buf[0] = 65u8; buf[1] = 66u8; buf[2] = 67u8; buf[3] = 68u8;\n" @@ -144,9 +131,9 @@ static const struct row rows[] = { STAGE_CS | STAGE_WW, 1 }, { "dynamic_write_grow", "package main;\n" - "import os;\n" "import memio;\n" "import io;\n" + "import os;\n" "export fn main() i32 = {\n" " let r = memio.dynamic_vstream();\n" " let s: io.vstream = nil: *io.vtable;\n" @@ -167,9 +154,9 @@ static const struct row rows[] = { STAGE_CS | STAGE_WW, 1 }, { "dynamicfrom_alt_rw", "package main;\n" - "import os;\n" "import memio;\n" "import io;\n" + "import os;\n" "export fn main() i32 = {\n" " let seed: [4]u8;\n" " seed[0] = 1u8; seed[1] = 2u8; seed[2] = 3u8; seed[3] = 4u8;\n" @@ -195,9 +182,9 @@ static const struct row rows[] = { STAGE_CS | STAGE_WW, 1 }, { "branched_fixed", "package main;\n" - "import os;\n" "import memio;\n" "import io;\n" + "import os;\n" "export fn main() i32 = {\n" " let a: [3]u8;\n" " a[0] = 10u8; a[1] = 11u8; a[2] = 12u8;\n" diff --git a/test/wcc/782_fieldfn_leaf_collide_run.c b/test/wcc/782_fieldfn_leaf_collide_run.c new file mode 100644 index 00000000..0de644f4 --- /dev/null +++ b/test/wcc/782_fieldfn_leaf_collide_run.c @@ -0,0 +1,190 @@ +/* + * 782_fieldfn_leaf_collide_run — cstage-only pin for project #211, the + * cgen sibling of #208 (the checker fix). + * + * SHAPE: a value-receiver fn-pointer FIELD call `s.pull(...)` whose leaf + * name `pull` COLLIDES with a same-module GLOBAL fn `pull` of a DIFFERENT + * register shape — the field returns a tagged `(i64 | sentinel)` (2-word + * AX=tag/DX=word0 ABI), the global returns a scalar `i64` (1-word AX). + * cstage resolves the call result from the CALLEE's own type (the field's + * fn type), so it reads the tagged 2-word return correctly. wwstage cgen + * re-derives the return type by NAME (fnretlookup over the leaf, with the + * receiver VARIABLE name as the "module"), mis-binds the scalar global, + * and widens a 1-word AX into the tagged slot — a silent cs≠ww miscompile + * (wrong runtime + divergent .s). See selfhost/cmd/wcc/cgen.ww fnretlookup. + * + * WHY cstage-only (carve-out idiom, mirror of 777/780/781): exercising + * this row under wwstage would trip #211 (asm differs, runtime wrong), so + * STAGE_WW + byte_id are withheld until #211 closes. The row pins the + * cstage-correct behaviour (the spec) so #211's fix is a graduation, not + * a regression. #211 was MASKED until #208 landed: pre-#208 the wwstage + * checker rejected this shape ("is/as: operand is not a tagged union") + * before cgen ran, so the cgen path was unreachable. + * + * row | what it pins + * ---------------------+---------------------------------------------- + * field_vs_global_leaf | s.pull(&s,5) routes to the FIELD impl + * | (srcpull → 105), NOT the same-named global + * | `pull` (→ 14). `r is i64` / `r as i64` prove + * | the result is the tagged field type. The + * | global stays live (g = pull(3) = 14) so the + * | collision is real, not dead-code-elided. + * + * GATE POLARITY: must stay GREEN. A red means the cstage call-result + * resolution regressed on a value-receiver fn-ptr field call. + * + * GRADUATES to STAGE_CS | STAGE_WW + byte_id on #211 close. + */ +#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; +} + +#define STAGE_CS 1 +#define STAGE_WW 2 + +struct row { + const char *label; + const char *src; + int want_exit; + int stage_mask; + int byte_id; +}; + +static const struct row rows[] = { + { "field_vs_global_leaf", + "package main;\n" + "type sentinel = void;\n" + "fn pull(x: i64) i64 = {\n" + " return x + 11i64;\n" + "};\n" + "type src = struct {\n" + " pull: fn(s: *src, k: i64) (i64 | sentinel),\n" + "};\n" + "fn srcpull(s: *src, k: i64) (i64 | sentinel) = {\n" + " return k + 100i64;\n" + "};\n" + "export fn main() i32 = {\n" + " let s: src;\n" + " s.pull = srcpull;\n" + " let g: i64 = pull(3i64);\n" + " let r = s.pull(&s, 5i64);\n" + " let out: i64 = -1i64;\n" + " if (r is i64) { out = r as i64; };\n" + " if (out == 105i64 && g == 14i64) { return 42; };\n" + " return 1i32;\n" + "};\n", + 42, + STAGE_CS, 0 }, +}; + +static int +write_source(const char *path, const char *src) +{ + FILE *f = fopen(path, "wb"); + if (!f) return -1; + fputs(src, f); + fclose(f); + return 0; +} + +/* Per-row tmpdir cleanup. ww_ww writes intermediates next to the source + * (filed task #15); cstage ww does too. Sweep then rmdir. Mirror of + * 777's cleanup_tmp. */ +static void +cleanup_tmp(const char *tmpdir, const char *base) +{ + char p[640]; + snprintf(p, sizeof p, "%s/%s.ww", tmpdir, base); unlink(p); + snprintf(p, sizeof p, "%s/%s.s", tmpdir, base); unlink(p); + snprintf(p, sizeof p, "%s/%s.o", tmpdir, base); unlink(p); + snprintf(p, sizeof p, "%s/%s.combined.ww", tmpdir, base); unlink(p); + snprintf(p, sizeof p, "%s/%s", tmpdir, base); unlink(p); + rmdir(tmpdir); +} + +static int +build_via_driver(const char *driver, const char *tmpdir, const char *cwd, + const char *src) +{ + char cmd[2048]; + snprintf(cmd, sizeof cmd, + "cd %s && timeout 180 %s build -I %s/lib %s 2>/dev/null", + tmpdir, driver, cwd, src); + return runwait(cmd); +} + +static int +run_row(const char *driver, const char *cwd, const struct row *r, int seq) +{ + char tmpdir[256], src[512], base[64], outbin[768]; + snprintf(tmpdir, sizeof tmpdir, "/tmp/ffl_%d_d_%d", getpid(), seq); + snprintf(base, sizeof base, "main782"); + snprintf(src, sizeof src, "%s/%s.ww", tmpdir, base); + mkdir(tmpdir, 0755); + if (write_source(src, r->src) != 0) { cleanup_tmp(tmpdir, base); return -1; } + int rc; + int br = build_via_driver(driver, tmpdir, cwd, src); + if (br == 0) { + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + rc = runwait(outbin); + } else { + rc = -1; + } + cleanup_tmp(tmpdir, base); + return rc; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char cwd[256]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + char absbin[512]; + if (bin[0] != '/') { + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char cdrv[640]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + int seq = 0; + + for (int i = 0; i < n; i++) { + /* #211: cstage-only carve-out. No STAGE_WW row until #211 + * (cgen name-keyed call-return mis-resolution) closes. */ + if (rows[i].stage_mask & STAGE_CS) { + total++; + int got = run_row(cdrv, cwd, &rows[i], seq++); + if (got != rows[i].want_exit) { + fprintf(stderr, + "fieldfn_leaf_collide[cs][%s]: exit=%d want=%d\n", + rows[i].label, got, rows[i].want_exit); + fail++; + } + } + } + + if (fail) { + fprintf(stderr, "fieldfn_leaf_collide: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("fieldfn_leaf_collide: %d/%d ok\n", total, total); + return 0; +}