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.
This commit is contained in:
183
test/wcc/989_wwispread_sep.c
Normal file
183
test/wcc/989_wwispread_sep.c
Normal file
@@ -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 <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;
|
||||
}
|
||||
8
test/wcc/data/wwispread/liba/lib.ww
Normal file
8
test/wcc/data/wwispread/liba/lib.ww
Normal file
@@ -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);
|
||||
13
test/wcc/data/wwispread/main.ww
Normal file
13
test/wcc/data/wwispread/main.ww
Normal file
@@ -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;
|
||||
};
|
||||
6
test/wcc/data/wwispread3/libb/lib.ww
Normal file
6
test/wcc/data/wwispread3/libb/lib.ww
Normal file
@@ -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);
|
||||
13
test/wcc/data/wwispread3/main.ww
Normal file
13
test/wcc/data/wwispread3/main.ww
Normal file
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user