/* * 989_wwispread_sep — #95 .wwi producer must preserve the `...` union-spread * marker when serializing a tagged-union type under `ww build --sep`. * * A spread variant `(...inner | str)` carries Node.op == TK_ELLIPSIS on the * variant node (parse.c:315-324). The N_TTAGGED serializer loop must re-emit * the `...` prefix purely syntactically (cstage wwi.c, selfhost wwi.ww) — * mirroring Hare's unparse (ref/hare/hare/unparse/type.ha:290-300), which * leaves flattening to the consumer's type-store. Before the fix the marker * was dropped: liba.wwi read back as `(inner | str)`, a consumer never * flattened inner's arms, and a bare i32/i64 member was rejected — so fmt / * log / getopt failed under the sep flip. (The old combined.ww path never * round-trips through .wwi, so it accepted; the bug was latent until the flip.) * * TABLE-DRIVEN over two spread shapes (2-arm `inner` and 3-arm `three`). For * each shape and each driver stage (cs `ww`, ww `ww_ww`) the test: * (a) builds the consumer via `ww build --sep` and runs it — clean compile * + exit 0 proves the imported alias's arms flattened into the spread * type (the consumer assigns BARE members the spread must expose); * (b) greps the produced .wwi and asserts the spread marker survived; * (c) cs==ww (rule 10): the .wwi from `ww` vs `ww_ww` must be byte-identical. * * NON-VACUITY: reverting the wwi.c/.ww edit reddens BOTH legs — the consumer * build fails with `init i32 not assignable to declared outer` AND the marker * grep finds nothing (verified by hand at fix time). * * Light wwstage-driver test (CLAUDE.md rule 14): every build's intermediates * are `-o`-redirected to a private /tmp dir, so it is phase-1 parallel-safe. * Models 989_declns_sep conventions; 989 prefix per the sep-gate precedent. */ #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; } 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; } static int files_eq(const char *a, const char *b) { char *ba = NULL, *bb = NULL; size_t na = 0, nb = 0; if (slurp(a, &ba, &na) < 0 || slurp(b, &bb, &nb) < 0) { free(ba); free(bb); return -1; } int eq = (na == nb && memcmp(ba, bb, na) == 0); free(ba); free(bb); return eq ? 0 : 1; } /* 0 if `needle` occurs in the file at `path`, else 1 (or -1 on read err). */ static int file_contains(const char *path, const char *needle) { char *b = NULL; size_t n = 0; if (slurp(path, &b, &n) < 0) return -1; int found = (strstr(b, needle) != NULL); free(b); return found ? 0 : 1; } struct shape { const char *name; /* label */ const char *main; /* consumer main.ww (imports the spread package) */ const char *wwi; /* produced interface basename for the imported pkg */ const char *marker; /* the spread marker that must survive serialization */ }; static const struct shape shapes[] = { { "twoarm", "test/wcc/data/wwispread/main.ww", "liba.wwi", "...inner" }, { "threearm", "test/wcc/data/wwispread3/main.ww", "libb.wwi", "...three" }, }; static const struct { const char *drv; const char *tag; } drivers[] = { { "ww", "cs" }, { "ww_ww", "ww" }, }; int main(void) { const char *bin = absbin(); if (!bin) { fprintf(stderr, "wwispread FAIL: getcwd\n"); return 1; } char td[64], cmd[4096]; snprintf(td, sizeof td, "/tmp/wwwwispread_%d", getpid()); snprintf(cmd, sizeof cmd, "rm -rf %s", td); runwait(cmd); mkdir(td, 0755); int fail = 0; for (size_t s = 0; s < sizeof shapes / sizeof shapes[0]; s++) { char wwi[2][256]; for (size_t d = 0; d < sizeof drivers / sizeof drivers[0]; d++) { char prog[160]; snprintf(prog, sizeof prog, "%s/%s.%s", td, shapes[s].name, drivers[d].tag); snprintf(cmd, sizeof cmd, "%s/%s build --sep -o %s %s 2>/dev/null", bin, drivers[d].drv, prog, shapes[s].main); if (runwait(cmd) != 0) { fprintf(stderr, "wwispread FAIL: %s build --sep %s " "(spread member not assignable → marker dropped?)\n", drivers[d].drv, shapes[s].name); fail++; continue; } int got = runwait(prog); if (got != 0) { fprintf(stderr, "wwispread FAIL: %s %s run exit=%d want=0\n", drivers[d].drv, shapes[s].name, got); fail++; } snprintf(wwi[d], sizeof wwi[d], "%s/%s.%s.sepwork/%s", td, shapes[s].name, drivers[d].tag, shapes[s].wwi); if (file_contains(wwi[d], shapes[s].marker) != 0) { fprintf(stderr, "wwispread FAIL: %s %s — %s missing `%s` " "(spread marker dropped in serializer)\n", drivers[d].drv, shapes[s].name, shapes[s].wwi, shapes[s].marker); fail++; } } /* cs==ww (rule 10): both stages emit the same interface text. */ if (files_eq(wwi[0], wwi[1]) != 0) { fprintf(stderr, "wwispread FAIL: %s cs!=ww for %s (rule 10)\n", shapes[s].name, shapes[s].wwi); fail++; } } snprintf(cmd, sizeof cmd, "rm -rf %s", td); runwait(cmd); if (fail) { fprintf(stderr, "wwispread: %d check(s) failed\n", fail); return 1; } printf("wwispread: `...` union-spread marker survives .wwi serialization " "(2-arm + 3-arm, both stages cs==ww), consumer flattens bare members " "→ exit 0 (#95)\n"); return 0; }