Files
ww/test/wcc/989_wwispread_sep.c
Hojun-Cho 24ca570a7e w6c,ww: re-emit ... union-spread marker in .wwi producer (M4 E3, #95)
The N_TTAGGED serializer emitted each variant via wwi_type but never
re-emitted the `...` prefix for TK_ELLIPSIS spread variants, so an
exported `(...inner | str)` round-tripped through .wwi as `(inner | str)`.
The consumer's checker then could not flatten inner's members into the
alias and variadic-assignability rejected bare members — under separate
compilation this broke fmt/log/getopt. Re-emit `...` before the variant
type, both stages; the producer stays purely syntactic (flatten/dedup
remain the consumer's type-store job, per ref/hare/hare/unparse/type.ha:290-300).

Gate: test/wcc/989_wwispread_sep.c — table-driven (2-arm + 3-arm spreads)
x both stages, asserts the marker survives the .wwi, the consumer binds
bare members under --sep (exit 0), and cs==ww .wwi byte-identity.
2026-06-18 09:52:19 +09:00

184 lines
5.9 KiB
C

/*
* 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 <pkg>.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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
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;
}