diff --git a/Makefile b/Makefile index a102574a..7f8fa844 100644 --- a/Makefile +++ b/Makefile @@ -537,7 +537,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_w6c_ww $(BIN)/test_ww_ww $(BIN)/test_self_rebuild \ $(BIN)/test_dyn_ww $(BIN)/test_selfcheck \ $(BIN)/test_attest_record $(BIN)/test_attest_drop \ - $(BIN)/test_declns_sep \ + $(BIN)/test_declns_sep $(BIN)/test_wwispread_sep \ $(BIN)/test_fmt_run $(BIN)/test_log_run $(BIN)/test_fnmatch_run \ $(BIN)/test_shlex_run $(BIN)/test_getenv_run $(BIN)/test_dirs_run \ $(BIN)/test_dirs_toolong_run \ @@ -2967,6 +2967,13 @@ $(BIN)/test_declns_sep: test/wcc/989_declns_sep.c $(BIN)/ww $(BIN)/ww_ww \ $(BIN)/w6l $(BIN)/w6l_ww $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# #95 — .wwi producer must preserve the `...` union-spread marker; dir-package +# `ww build --sep` + run, both driver stages, 2-arm + 3-arm shapes + cs==ww. +$(BIN)/test_wwispread_sep: test/wcc/989_wwispread_sep.c $(BIN)/ww $(BIN)/ww_ww \ + $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6a_ww \ + $(BIN)/w6l $(BIN)/w6l_ww $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_fmt_run: test/wcc/970_fmt_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/cmd/w6c/wwi.c b/cmd/w6c/wwi.c index 12f1cff2..07e207f1 100644 --- a/cmd/w6c/wwi.c +++ b/cmd/w6c/wwi.c @@ -272,6 +272,11 @@ wwi_type(FILE *of, Node *t) fputc('(', of); for (Node *e = t->list; e; e = e->next) { if (e != t->list) fputs(" | ", of); + /* #95: re-emit the spread marker purely syntactically; + * the consumer's type-store flattens (ref/hare/hare/ + * unparse/type.ha:290-300). */ + if (e->op == TK_ELLIPSIS) + fputs("...", of); wwi_type(of, e); } fputc(')', of); diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index a5a629b2..8d06e1f6 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -45855,6 +45855,10 @@ fn wwitype(fd: i32, t: *syntax.node) void = { let e: *syntax.node = t.list; for (e != nil) { if (e != t.list) { wputs(fd, " | "); }; + // #95: re-emit the spread marker purely syntactically; + // the consumer's type-store flattens + // (ref/hare/hare/unparse/type.ha:290-300). + if (e.op == syntax.tkind.TK_ELLIPSIS) { wputs(fd, "..."); }; wwitype(fd, e); e = e.next; }; diff --git a/selfhost/cmd/wcc/wwi.ww b/selfhost/cmd/wcc/wwi.ww index 2d2edff9..1bff1722 100644 --- a/selfhost/cmd/wcc/wwi.ww +++ b/selfhost/cmd/wcc/wwi.ww @@ -365,6 +365,10 @@ fn wwitype(fd: i32, t: *syntax.node) void = { let e: *syntax.node = t.list; for (e != nil) { if (e != t.list) { wputs(fd, " | "); }; + // #95: re-emit the spread marker purely syntactically; + // the consumer's type-store flattens + // (ref/hare/hare/unparse/type.ha:290-300). + if (e.op == syntax.tkind.TK_ELLIPSIS) { wputs(fd, "..."); }; wwitype(fd, e); e = e.next; }; diff --git a/test/wcc/989_wwispread_sep.c b/test/wcc/989_wwispread_sep.c new file mode 100644 index 00000000..ca3bc75c --- /dev/null +++ b/test/wcc/989_wwispread_sep.c @@ -0,0 +1,183 @@ +/* + * 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; +} diff --git a/test/wcc/data/wwispread/liba/lib.ww b/test/wcc/data/wwispread/liba/lib.ww new file mode 100644 index 00000000..b10d6556 --- /dev/null +++ b/test/wcc/data/wwispread/liba/lib.ww @@ -0,0 +1,8 @@ +package liba; + +// `outer` spreads the named alias `inner` (a 2-arm union). The `...` marker +// must survive serialization to liba.wwi so a consumer flattens inner's arms +// into outer (#95). Without it, outer reads back as (inner | str) and a bare +// i32/i64 is not assignable. +export type inner = (i32 | i64); +export type outer = (...inner | str); diff --git a/test/wcc/data/wwispread/main.ww b/test/wcc/data/wwispread/main.ww new file mode 100644 index 00000000..05428bda --- /dev/null +++ b/test/wcc/data/wwispread/main.ww @@ -0,0 +1,13 @@ +package main; + +import liba; + +// Each assignment binds a BARE member of inner (i32, i64) or the outer-direct +// arm (str) to liba.outer — typechecks ONLY if the spread flattened inner's +// arms into outer across the liba.wwi round-trip (#95). +export fn main() void = { + let x: liba.outer = 42i32; + x = 99i64; + x = "hi"; + return; +}; diff --git a/test/wcc/data/wwispread3/libb/lib.ww b/test/wcc/data/wwispread3/libb/lib.ww new file mode 100644 index 00000000..3dde5a42 --- /dev/null +++ b/test/wcc/data/wwispread3/libb/lib.ww @@ -0,0 +1,6 @@ +package libb; + +// 3-arm spread: proves ALL of three's arms (i32, i64, rune) flatten into +// wide, not just the first (#95). +export type three = (i32 | i64 | rune); +export type wide = (...three | str); diff --git a/test/wcc/data/wwispread3/main.ww b/test/wcc/data/wwispread3/main.ww new file mode 100644 index 00000000..8c8d9352 --- /dev/null +++ b/test/wcc/data/wwispread3/main.ww @@ -0,0 +1,13 @@ +package main; + +import libb; + +// Bind each of three's arms (i32, i64, rune) plus the str arm to libb.wide: +// typechecks ONLY if all three flattened across the libb.wwi round-trip (#95). +export fn main() void = { + let a: libb.wide = 1i32; + a = 2i64; + a = 9i32: rune; + a = "z"; + return; +};