diff --git a/Makefile b/Makefile index bb2e383e..718e557c 100644 --- a/Makefile +++ b/Makefile @@ -365,7 +365,8 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_opaque_guards \ $(BIN)/test_opaque_assign_cast_run \ $(BIN)/test_sort_run \ - $(BIN)/test_bufio_run $(BIN)/test_random_run + $(BIN)/test_bufio_run $(BIN)/test_random_run \ + $(BIN)/test_asserttyped_gap $(BIN)/test_smoke: test/wcc/000_smoke.c $(LIB)/libwcc.a | $(BIN) $(CC) $(CFLAGS) $(INCS) -o $@ $< -L$(LIB) -lwcc @@ -992,6 +993,9 @@ $(BIN)/test_self_rebuild: test/wcc/995_self_rebuild.c $(BIN)/ww_ww \ $(BIN)/test_selfcheck: test/wcc/950_selfcheck.c $(BIN)/wwdump_ww | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_asserttyped_gap: test/wcc/901_asserttyped_gap.c $(BIN)/wwdump_ww | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_dyn_ww: test/wcc/996_dyn_ww.c $(BIN)/ww $(BIN)/w6l $(BIN)/w6l_ww \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/test/wcc/901_asserttyped_gap.c b/test/wcc/901_asserttyped_gap.c new file mode 100644 index 00000000..0c07c193 --- /dev/null +++ b/test/wcc/901_asserttyped_gap.c @@ -0,0 +1,156 @@ +/* + * 901_asserttyped_gap — ww-stage checker nil-type gap-corpus net. + * + * The live `make test` stdlib _run suite drives the CSTAGE `ww` + * (cmd/ check.c), which has no asserttyped pass; 990_selfhost feeds + * the ww-stage dumper only -t/-a (tokens/ast), never the checker. So + * the wwstage checker's asserttyped diagnostics (check.ww:3442 — the + * #15 / A.6.2.1e post-checker nil-type invariant gate, currently a + * soft warn) are UNOBSERVED by every other test. Re-arming asserttyped + * to bail would ship green while the known nil-stamp gaps stayed + * latent (the bootstrap-coverage trap). + * + * This probe runs the wwstage checker (wwdump_ww -c) over the + * gap-bearing combined.ww corpus, counts the per-file asserttyped + * diagnostics on stderr, and pins each count against the manifest + * below. A fresh nil-gap (count up) or a regressed fixed class (count + * up from 0) fails loud; a fold that closes a class drives its count + * down, which fails until the manifest is edited to match — i.e. the + * manifest is the expected-fail list that shrinks per fold and reaches + * all-zero exactly when the bail is safe to arm. + * + * Gap classes (counts verified empirically at this revision): + * A module-qual N_DOT call result checked_test 0 (closed) + * B fn-ptr struct-field call smoke 3 + * C abort intrinsic callee utf8 8 + * D module-leaf == type/fn name fnmatch 2 + * D module-leaf == type/fn name random 16 + * + * Exit code of wwdump_ww is intentionally not gated: the diagnostics + * land on stderr regardless of the run's success, and arming the bail + * will itself flip that exit code — the stderr line count is the one + * signal stable across the whole fold sequence. + */ +#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; +} + +static const char * +absbin(void) +{ + const char *b = getenv("BIN"); + if (!b) b = "out/bin"; + if (b[0] == '/') return b; + static char buf[2048]; + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return NULL; + snprintf(buf, sizeof buf, "%s/%s", cwd, b); + return buf; +} + +static int +slurp(const char *path, char **outbuf, size_t *outlen) +{ + FILE *f = fopen(path, "rb"); + if (!f) return -1; + fseek(f, 0, SEEK_END); + long n = ftell(f); + fseek(f, 0, SEEK_SET); + if (n < 0) { fclose(f); return -1; } + char *b = malloc((size_t)n + 1); + if (!b) { fclose(f); return -1; } + if (fread(b, 1, (size_t)n, f) != (size_t)n) { free(b); fclose(f); return -1; } + b[n] = '\0'; + fclose(f); + *outbuf = b; + *outlen = (size_t)n; + return 0; +} + +/* Count line-leading "asserttyped:" diagnostics; each warn is one such + * line and no other wwdump output carries the prefix. */ +static int +count_asserttyped(const char *path) +{ + char *b = NULL; + size_t n = 0; + if (slurp(path, &b, &n) < 0) return -1; + const char *needle = "asserttyped:"; + size_t nl = strlen(needle); + int c = 0; + for (size_t i = 0; i + nl <= n; i++) { + if ((i == 0 || b[i - 1] == '\n') && memcmp(b + i, needle, nl) == 0) + c++; + } + free(b); + return c; +} + +int +main(void) +{ + const char *bin = absbin(); + if (!bin) return 1; + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + + struct { const char *rel; const char *cls; int want; } manifest[] = { + { "lib/math/checked/checked_test.combined.ww", + "A module-qual N_DOT call result", 0 }, + { "selfhost/test/smoke.combined.ww", + "B fn-ptr struct-field call", 3 }, + { "lib/encoding/utf8/utf8.combined.ww", + "C abort intrinsic callee", 8 }, + { "lib/fnmatch/fnmatchtest.combined.ww", + "D module-leaf == type/fn name", 2 }, + { "lib/math/random/random_test.combined.ww", + "D module-leaf == type/fn name", 16 }, + { NULL, NULL, 0 }, + }; + + int fail = 0, n = 0; + for (int i = 0; manifest[i].rel; i++) { + char src[2048], errf[64], cmd[4096]; + snprintf(src, sizeof src, "%s/%s", cwd, manifest[i].rel); + snprintf(errf, sizeof errf, "/tmp/atgap_%d_%d.err", getpid(), i); + snprintf(cmd, sizeof cmd, + "timeout 180 %s/wwdump_ww -c %s >/dev/null 2>%s", + bin, src, errf); + runwait(cmd); + int got = count_asserttyped(errf); + n++; + if (got != manifest[i].want) { + fprintf(stderr, + "asserttyped_gap FAIL: %s [%s] expected %d, got %d\n", + manifest[i].rel, manifest[i].cls, + manifest[i].want, got); + char dump[4096]; + snprintf(dump, sizeof dump, + "grep '^asserttyped:' %s 1>&2", errf); + runwait(dump); + fail++; + } + unlink(errf); + } + + if (fail) { + fprintf(stderr, + "asserttyped_gap: %d/%d fixture(s) drifted from manifest\n", + fail, n); + return 1; + } + printf("asserttyped_gap: ww-stage checker warn set matches manifest " + "on %d gap-corpus fixtures (B+C+D pinned, A closed)\n", n); + return 0; +}