From 697e41311397523480407548626071f05f6259cc Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 16 Jun 2026 20:19:14 +0900 Subject: [PATCH] lib/ww/syntax: export the 16 public types consumed by the wcc backend (#72) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After the frontend consolidated into one syntax package, the wcc backend imports syntax and calls its exported fns — whose signatures reference types that were unexported. Producing syntax's .wwi interface re-triggered check_exported_type ("exported declaration references unexported type"): the residual of BUG-A at the one surviving syntax->wcc boundary. Export the 16 types that appear in syntax's wcc-facing public surface (directly in an exported signature, or via a recursively-referenced exported struct field): nkind, node, lex, tok, tkind, parser, scope, sym, skind, tinfo, tykind, tfield, tparam, ttupleelem, tctx, tinfocacheent. The set is minimal (unexporting any one re-breaks the producer) and complete; pos stays internal. Pure source change — exporting a type emits no code, so the bootstrap binaries are byte-identical (verified against a clean base build); only syntax's .wwi gains the type decls. Post-frontend-reorg residual (#74). syntax now sep-produces clean both stages. The separate concern of wcc's currently-unqualified refs to syntax symbols (#75) is a distinct follow-up. Gate 989_syntaxexport_run. --- Makefile | 13 ++ lib/ww/syntax/ast.ww | 4 +- lib/ww/syntax/lex.ww | 2 +- lib/ww/syntax/parse.ww | 2 +- lib/ww/syntax/sym.ww | 6 +- lib/ww/syntax/tok.ww | 4 +- lib/ww/syntax/typ.ww | 14 +- selfhost/cmd/w6c/main.combined.ww | 32 +-- selfhost/cmd/wwdump/main.combined.ww | 32 +-- test/wcc/989_syntaxexport_run.c | 282 +++++++++++++++++++++++++++ 10 files changed, 343 insertions(+), 48 deletions(-) create mode 100644 test/wcc/989_syntaxexport_run.c diff --git a/Makefile b/Makefile index 0ddf3e1a..5c9d6990 100644 --- a/Makefile +++ b/Makefile @@ -562,6 +562,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_sepbuild_run \ $(BIN)/test_sepdotpath_run \ $(BIN)/test_seproot_export_run \ + $(BIN)/test_syntaxexport_run \ $(BIN)/test_sepstructdef_run \ $(BIN)/test_sepcycle_dup \ $(BIN)/test_separchive_run \ @@ -3131,6 +3132,18 @@ $(BIN)/test_seproot_export_run: test/wcc/989_seproot_export_run.c $(BIN)/ww $(BI $(BIN)/w6l $(BIN)/w6l_ww $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# 989_syntaxexport_run — BUG-A residual (#72) at the surviving syntax->wcc +# boundary after the #74 consolidation: syntax's exported fns name frontend +# types, so the producer's check_exported_type (recursing exported-struct +# FIELDS too) rejects until those 16 types carry `export`. Drives BOTH driver +# stages on the real lib/ww/syntax via a qualified-ref root + replays the +# producer on the produced unit (both compilers, export-strip = the bug), so +# it needs both driver + both compiler + both linker stages + libwwrt. +$(BIN)/test_syntaxexport_run: test/wcc/989_syntaxexport_run.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 $@ $< + # 989_sepstructdef_run — sep-build of a dep exporting aggregate-initializer # defs (N_STRUCTLIT info / N_ARRLIT arr) (#70, BUG-2): the producer must emit a # value-LESS prototype `export def X: T;` for an aggregate-init def (DATA-global diff --git a/lib/ww/syntax/ast.ww b/lib/ww/syntax/ast.ww index 7f15cffb..e45c4779 100644 --- a/lib/ww/syntax/ast.ww +++ b/lib/ww/syntax/ast.ww @@ -21,7 +21,7 @@ import strconv; // explicit and must stay in sync — the 990_selfhost test diffs // astprint against the C side byte-for-byte. Tail-appended entries // (TYPETEST onward) preserve every prior N_* value. -type nkind = enum i32 { +export type nkind = enum i32 { N_NONE = 0, N_INTLIT = 1, @@ -115,7 +115,7 @@ type nkind = enum i32 { // ---- Node ------------------------------------------------------------- -type node = struct { +export type node = struct { kind: nkind, file: str, line: i32, diff --git a/lib/ww/syntax/lex.ww b/lib/ww/syntax/lex.ww index ce879cde..6bf7b149 100644 --- a/lib/ww/syntax/lex.ww +++ b/lib/ww/syntax/lex.ww @@ -46,7 +46,7 @@ fn hexval(c: rune) (i32 | void) = { return; }; -type lex = struct { +export type lex = struct { file: str, src: *u8, // raw bytes; not necessarily NUL-terminated srclen: u64, diff --git a/lib/ww/syntax/parse.ww b/lib/ww/syntax/parse.ww index 3cbd6121..8e5891fe 100644 --- a/lib/ww/syntax/parse.ww +++ b/lib/ww/syntax/parse.ww @@ -17,7 +17,7 @@ package syntax; import os; import strings; -type parser = struct { +export type parser = struct { l: *lex, errs: i32, // nocast: while inside `[...]` we treat ':' as the slice diff --git a/lib/ww/syntax/sym.ww b/lib/ww/syntax/sym.ww index 0a1e8d3e..5bc6b7e1 100644 --- a/lib/ww/syntax/sym.ww +++ b/lib/ww/syntax/sym.ww @@ -7,7 +7,7 @@ package syntax; // Symbol kinds — must stay numerically aligned with cmd/wcc/ww.h Skind. -type skind = enum i32 { +export type skind = enum i32 { SK_NONE = 0, SK_VAR = 1, SK_PARAM = 2, @@ -18,7 +18,7 @@ type skind = enum i32 { SK_FIELD = 7, }; -type sym = struct { +export type sym = struct { name: str, skind: skind, type_: *tinfo, @@ -43,7 +43,7 @@ type sym = struct { def NBUCKETS: i32 = 16; -type scope = struct { +export type scope = struct { parent: *scope, first: *sym, last: *sym, diff --git a/lib/ww/syntax/tok.ww b/lib/ww/syntax/tok.ww index 47b23b3a..a0550511 100644 --- a/lib/ww/syntax/tok.ww +++ b/lib/ww/syntax/tok.ww @@ -20,7 +20,7 @@ import strings; // explicit and must stay in sync — the 990_selfhost test diffs wwdump // output against the C side, byte for byte. -type tkind = enum i32 { +export type tkind = enum i32 { TK_NONE = 0, TK_EOF = 1, TK_ERR = 2, @@ -143,7 +143,7 @@ type pos = struct { col: i32, }; -type tok = struct { +export type tok = struct { kind: tkind, file: str, // path of the source the token came from line: i32, diff --git a/lib/ww/syntax/typ.ww b/lib/ww/syntax/typ.ww index 8dedc17a..83ae8195 100644 --- a/lib/ww/syntax/typ.ww +++ b/lib/ww/syntax/typ.ww @@ -18,7 +18,7 @@ import os; // Mirror of the C `TypeKind` enum in cmd/wcc/ww.h. Numeric values // are explicit and must stay in sync — the selfhost selfcheck and // typed-AST printers depend on matching numeric layout. -type tykind = enum i32 { +export type tykind = enum i32 { TY_NONE = 0, TY_VOID = 1, TY_BOOL = 2, @@ -69,14 +69,14 @@ def SIZE_UNDEFINED: u64 = 18446744073709551615; // ---- tinfo / tfield / tparam ----------------------------------------- -type tfield = struct { +export type tfield = struct { name: str, type_: *tinfo, offset: u64, tnext: *tfield, }; -type tparam = struct { +export type tparam = struct { name: str, type_: *tinfo, // #61a: per-variant `!T` error mark for TY_TAGGED variants. @@ -98,13 +98,13 @@ type tparam = struct { // sea-of-stars mirrors Hare's structural choice; the empty-name idiom // from #50's TTAGGED-on-tparam would conflate two semantic axes // (variants can be named; positionals never can). -type ttupleelem = struct { +export type ttupleelem = struct { type_: *tinfo, offset: u64, tnext: *ttupleelem, }; -type tinfo = struct { +export type tinfo = struct { kind: tykind, size: u64, align: u64, @@ -154,7 +154,7 @@ type tinfo = struct { // (rule-12): cnext chains WITHIN a bucket; lookup/bind hash then touch // only one bucket -> O(1) amortized. Identical lookup results (same // *tinfo for the same node), so emitted asm is byte-identical. -type tinfocacheent = struct { +export type tinfocacheent = struct { key: *node, val: *tinfo, cnext: *tinfocacheent, @@ -168,7 +168,7 @@ def NBUCKETS_TINFO: u64 = 8192u64; // ---- tctx — the box of primitive types ------------------------------- -type tctx = struct { +export type tctx = struct { tyvoid: *tinfo, tybool: *tinfo, tyrune: *tinfo, diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index db34166f..d7a5948e 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -6441,7 +6441,7 @@ import strconv; // explicit and must stay in sync — the 990_selfhost test diffs // astprint against the C side byte-for-byte. Tail-appended entries // (TYPETEST onward) preserve every prior N_* value. -type nkind = enum i32 { +export type nkind = enum i32 { N_NONE = 0, N_INTLIT = 1, @@ -6535,7 +6535,7 @@ type nkind = enum i32 { // ---- Node ------------------------------------------------------------- -type node = struct { +export type node = struct { kind: nkind, file: str, line: i32, @@ -7521,7 +7521,7 @@ fn hexval(c: rune) (i32 | void) = { return; }; -type lex = struct { +export type lex = struct { file: str, src: *u8, // raw bytes; not necessarily NUL-terminated srclen: u64, @@ -8372,7 +8372,7 @@ package syntax; import os; import strings; -type parser = struct { +export type parser = struct { l: *lex, errs: i32, // nocast: while inside `[...]` we treat ':' as the slice @@ -9323,7 +9323,7 @@ fn parsestmt(p: *parser) *node = { package syntax; // Symbol kinds — must stay numerically aligned with cmd/wcc/ww.h Skind. -type skind = enum i32 { +export type skind = enum i32 { SK_NONE = 0, SK_VAR = 1, SK_PARAM = 2, @@ -9334,7 +9334,7 @@ type skind = enum i32 { SK_FIELD = 7, }; -type sym = struct { +export type sym = struct { name: str, skind: skind, type_: *tinfo, @@ -9359,7 +9359,7 @@ type sym = struct { def NBUCKETS: i32 = 16; -type scope = struct { +export type scope = struct { parent: *scope, first: *sym, last: *sym, @@ -9671,7 +9671,7 @@ import strings; // explicit and must stay in sync — the 990_selfhost test diffs wwdump // output against the C side, byte for byte. -type tkind = enum i32 { +export type tkind = enum i32 { TK_NONE = 0, TK_EOF = 1, TK_ERR = 2, @@ -9794,7 +9794,7 @@ type pos = struct { col: i32, }; -type tok = struct { +export type tok = struct { kind: tkind, file: str, // path of the source the token came from line: i32, @@ -10074,7 +10074,7 @@ import os; // Mirror of the C `TypeKind` enum in cmd/wcc/ww.h. Numeric values // are explicit and must stay in sync — the selfhost selfcheck and // typed-AST printers depend on matching numeric layout. -type tykind = enum i32 { +export type tykind = enum i32 { TY_NONE = 0, TY_VOID = 1, TY_BOOL = 2, @@ -10125,14 +10125,14 @@ def SIZE_UNDEFINED: u64 = 18446744073709551615; // ---- tinfo / tfield / tparam ----------------------------------------- -type tfield = struct { +export type tfield = struct { name: str, type_: *tinfo, offset: u64, tnext: *tfield, }; -type tparam = struct { +export type tparam = struct { name: str, type_: *tinfo, // #61a: per-variant `!T` error mark for TY_TAGGED variants. @@ -10154,13 +10154,13 @@ type tparam = struct { // sea-of-stars mirrors Hare's structural choice; the empty-name idiom // from #50's TTAGGED-on-tparam would conflate two semantic axes // (variants can be named; positionals never can). -type ttupleelem = struct { +export type ttupleelem = struct { type_: *tinfo, offset: u64, tnext: *ttupleelem, }; -type tinfo = struct { +export type tinfo = struct { kind: tykind, size: u64, align: u64, @@ -10210,7 +10210,7 @@ type tinfo = struct { // (rule-12): cnext chains WITHIN a bucket; lookup/bind hash then touch // only one bucket -> O(1) amortized. Identical lookup results (same // *tinfo for the same node), so emitted asm is byte-identical. -type tinfocacheent = struct { +export type tinfocacheent = struct { key: *node, val: *tinfo, cnext: *tinfocacheent, @@ -10224,7 +10224,7 @@ def NBUCKETS_TINFO: u64 = 8192u64; // ---- tctx — the box of primitive types ------------------------------- -type tctx = struct { +export type tctx = struct { tyvoid: *tinfo, tybool: *tinfo, tyrune: *tinfo, diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index e109b7e5..80e9e680 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -6441,7 +6441,7 @@ import strconv; // explicit and must stay in sync — the 990_selfhost test diffs // astprint against the C side byte-for-byte. Tail-appended entries // (TYPETEST onward) preserve every prior N_* value. -type nkind = enum i32 { +export type nkind = enum i32 { N_NONE = 0, N_INTLIT = 1, @@ -6535,7 +6535,7 @@ type nkind = enum i32 { // ---- Node ------------------------------------------------------------- -type node = struct { +export type node = struct { kind: nkind, file: str, line: i32, @@ -7521,7 +7521,7 @@ fn hexval(c: rune) (i32 | void) = { return; }; -type lex = struct { +export type lex = struct { file: str, src: *u8, // raw bytes; not necessarily NUL-terminated srclen: u64, @@ -8372,7 +8372,7 @@ package syntax; import os; import strings; -type parser = struct { +export type parser = struct { l: *lex, errs: i32, // nocast: while inside `[...]` we treat ':' as the slice @@ -9323,7 +9323,7 @@ fn parsestmt(p: *parser) *node = { package syntax; // Symbol kinds — must stay numerically aligned with cmd/wcc/ww.h Skind. -type skind = enum i32 { +export type skind = enum i32 { SK_NONE = 0, SK_VAR = 1, SK_PARAM = 2, @@ -9334,7 +9334,7 @@ type skind = enum i32 { SK_FIELD = 7, }; -type sym = struct { +export type sym = struct { name: str, skind: skind, type_: *tinfo, @@ -9359,7 +9359,7 @@ type sym = struct { def NBUCKETS: i32 = 16; -type scope = struct { +export type scope = struct { parent: *scope, first: *sym, last: *sym, @@ -9671,7 +9671,7 @@ import strings; // explicit and must stay in sync — the 990_selfhost test diffs wwdump // output against the C side, byte for byte. -type tkind = enum i32 { +export type tkind = enum i32 { TK_NONE = 0, TK_EOF = 1, TK_ERR = 2, @@ -9794,7 +9794,7 @@ type pos = struct { col: i32, }; -type tok = struct { +export type tok = struct { kind: tkind, file: str, // path of the source the token came from line: i32, @@ -10074,7 +10074,7 @@ import os; // Mirror of the C `TypeKind` enum in cmd/wcc/ww.h. Numeric values // are explicit and must stay in sync — the selfhost selfcheck and // typed-AST printers depend on matching numeric layout. -type tykind = enum i32 { +export type tykind = enum i32 { TY_NONE = 0, TY_VOID = 1, TY_BOOL = 2, @@ -10125,14 +10125,14 @@ def SIZE_UNDEFINED: u64 = 18446744073709551615; // ---- tinfo / tfield / tparam ----------------------------------------- -type tfield = struct { +export type tfield = struct { name: str, type_: *tinfo, offset: u64, tnext: *tfield, }; -type tparam = struct { +export type tparam = struct { name: str, type_: *tinfo, // #61a: per-variant `!T` error mark for TY_TAGGED variants. @@ -10154,13 +10154,13 @@ type tparam = struct { // sea-of-stars mirrors Hare's structural choice; the empty-name idiom // from #50's TTAGGED-on-tparam would conflate two semantic axes // (variants can be named; positionals never can). -type ttupleelem = struct { +export type ttupleelem = struct { type_: *tinfo, offset: u64, tnext: *ttupleelem, }; -type tinfo = struct { +export type tinfo = struct { kind: tykind, size: u64, align: u64, @@ -10210,7 +10210,7 @@ type tinfo = struct { // (rule-12): cnext chains WITHIN a bucket; lookup/bind hash then touch // only one bucket -> O(1) amortized. Identical lookup results (same // *tinfo for the same node), so emitted asm is byte-identical. -type tinfocacheent = struct { +export type tinfocacheent = struct { key: *node, val: *tinfo, cnext: *tinfocacheent, @@ -10224,7 +10224,7 @@ def NBUCKETS_TINFO: u64 = 8192u64; // ---- tctx — the box of primitive types ------------------------------- -type tctx = struct { +export type tctx = struct { tyvoid: *tinfo, tybool: *tinfo, tyrune: *tinfo, diff --git a/test/wcc/989_syntaxexport_run.c b/test/wcc/989_syntaxexport_run.c new file mode 100644 index 00000000..81b37657 --- /dev/null +++ b/test/wcc/989_syntaxexport_run.c @@ -0,0 +1,282 @@ +/* + * 989_syntaxexport_run — BUG-A residual at the surviving syntax->wcc + * package boundary (#72), post the #74 frontend consolidation. + * + * Root cause it guards: `lib/ww/syntax/` exports the fns wcc calls, and + * those fns' signatures name frontend types (tok, tinfo, tctx, ...). When + * the `--sep` producer emits syntax's `.wwi`, check_exported_type recurses + * every exported decl — and, for an exported struct, its FIELDS too (the + * harec STORAGE_STRUCT precedent, wwi.c wwi_check_type N_TSTRUCT). Any + * referenced type that is itself unexported aborts the producer with + * "exported declaration references unexported type". Pre-fix, syntax's + * surface left 16 such types unexported, so syntax's `.wwi` never produced + * -> every wcc sep-build was blocked. Fix: `export` those 16 type decls + * (10 fn-signature-direct + 6 surfaced by the struct-field recursion: + * tykind/tfield/tparam/ttupleelem via tinfo, tctx via typesinit, and + * tinfocacheent via tctx). Exporting a TYPE emits no code -> the bug is + * byte-id-neutral; this gate proves the PRODUCER residual is closed. + * + * The consumer here uses QUALIFIED refs (`syntax.tokname`, `syntax.tkind`) + * to isolate the producer fix from the separate unqualified-ref question + * (#75). It happens to link + run end-to-end, which also shows the + * qualified-ref path resolves across the sep boundary. + * + * Asserts (all COLD — per-stage WW_PKGCACHE wipes the package cache): + * 1. Build + run, BOTH stages -> exit EXPECT_EXIT (`syntax.tokname( + * TK_INT)` == "int", len 3). Pre-fix the syntax producer exited 1, so + * the build never produced a binary. + * 2. syntax's `.wwi` is produced and carries the 16 `export type` decls. + * 3. cs==ww (rule 10): syntax's `.wwi`/`.s`/`.unit.ww` AND the final + * binary are byte-identical between `ww --sep` and `ww_ww --sep`. + * 4. NON-VACUITY (the bug, isolated, BOTH compilers): replay the producer + * `w6c -c -I` on the produced `syntax.unit.ww` -> exits 0 (exports + * present). On a copy with `export ` stripped from the type decls -> + * MUST exit non-zero (the export-check fires). Proven for w6c + w6c_ww. + * + * Light wwstage-driver test (CLAUDE.md rule 14): all intermediates are + * `-o`-redirected to /tmp, so it is phase-1 parallel-safe; it only READS + * the in-tree `lib/ww/syntax`. Models 989_seproot_export_run.c. + */ +#include +#include +#include +#include +#include +#include + +#define EXPECT_EXIT 3 + +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`, 1 if absent, -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; +} + +static int +write_file(const char *path, const char *body) +{ + FILE *f = fopen(path, "wb"); + if (!f) return -1; + fputs(body, f); + fclose(f); + return 0; +} + +int +main(void) +{ + const char *bin = absbin(); + if (!bin) return 1; + char root[1024]; + if (getcwd(root, sizeof root) == NULL) return 1; + + char td[64], cmd[8192]; + int fail = 0; + + snprintf(td, sizeof td, "/tmp/wwsynexp_%d", getpid()); + snprintf(cmd, sizeof cmd, "rm -rf %s", td); + runwait(cmd); + mkdir(td, 0755); + + /* Root: QUALIFIED refs into the real syntax package — `syntax.tokname` + * over `syntax.tkind`. The producer must serialize syntax's `.wwi` + * (exported fns over frontend types) WITHOUT the unexported-type abort. */ + char rootww[1024]; + snprintf(rootww, sizeof rootww, "%s/root.ww", td); + if (write_file(rootww, + "package main;\n" + "import syntax;\n" + "fn main() i32 = {\n" + "\tlet s: str = syntax.tokname(syntax.tkind.TK_INT);\n" + "\treturn s.len: i32;\n" + "};\n")) + { fail++; goto out; } + + struct { const char *drv, *tag; char prog[1024]; } + stg[] = { { "ww", "cs", {0} }, { "ww_ww", "ww", {0} } }; + + for (int s = 0; s < 2; s++) { + snprintf(stg[s].prog, sizeof stg[s].prog, "%s/prog.%s", td, stg[s].tag); + /* COLD: per-stage WW_PKGCACHE -> syntax compiles fresh, so the + * `.wwi`/`.s`/`.unit.ww` this gate inspects are always produced. */ + snprintf(cmd, sizeof cmd, + "WW_PKGCACHE='%s/cache.%s' timeout 240 %s/%s build --sep " + "-I %s/lib/ww -o %s %s >%s/build.%s.log 2>&1", + td, stg[s].tag, bin, stg[s].drv, root, stg[s].prog, rootww, + td, stg[s].tag); + if (runwait(cmd) != 0) { + fprintf(stderr, "syntaxexport FAIL: %s build --sep (pre-fix: syntax " + "producer rejects export-fn-over-unexported-type)\n", stg[s].drv); + fail++; + continue; + } + /* the producer abort must not appear in the build log. */ + char log[1024], wwi[1024]; + snprintf(log, sizeof log, "%s/build.%s.log", td, stg[s].tag); + if (file_contains(log, "unexported type") == 0) { + fprintf(stderr, "syntaxexport FAIL: %s build log shows " + "'unexported type' (producer residual not closed)\n", stg[s].drv); + fail++; + } + snprintf(wwi, sizeof wwi, "%s/prog.%s.sepwork/syntax.wwi", td, stg[s].tag); + if (access(wwi, 0) != 0) { + fprintf(stderr, "syntaxexport FAIL: %s produced no syntax.wwi\n", + stg[s].drv); + fail++; + } + int rc = runwait(stg[s].prog); + if (rc != EXPECT_EXIT) { + fprintf(stderr, "syntaxexport FAIL: %s prog exit=%d expected %d\n", + stg[s].drv, rc, EXPECT_EXIT); + fail++; + } + } + + /* syntax's `.wwi` carries the now-exported frontend types — spot-check + * a representative set spanning each surfacing path (fn-direct + the + * struct-field and tctx-field cascades). */ + { + char wwi[1024]; + snprintf(wwi, sizeof wwi, "%s/prog.cs.sepwork/syntax.wwi", td); + const char *want[] = { + "export type tok ", "export type tkind ", "export type node ", + "export type nkind ", "export type tinfo ", "export type tfield ", + "export type tctx ", "export type tinfocacheent ", + }; + for (int i = 0; i < (int)(sizeof want / sizeof want[0]); i++) + if (file_contains(wwi, want[i]) != 0) { + fprintf(stderr, "syntaxexport FAIL: syntax.wwi lacks '%s'\n", + want[i]); + fail++; + } + } + + /* cs==ww (rule 10): syntax's per-package artefacts + the final binary. */ + { + const char *suf[] = { ".wwi", ".s", ".unit.ww" }; + for (int k = 0; k < 3; k++) { + char a[1024], b[1024]; + snprintf(a, sizeof a, "%s/prog.cs.sepwork/syntax%s", td, suf[k]); + snprintf(b, sizeof b, "%s/prog.ww.sepwork/syntax%s", td, suf[k]); + if (files_eq(a, b) != 0) { + fprintf(stderr, "syntaxexport FAIL: cs!=ww for syntax%s " + "(rule 10)\n", suf[k]); + fail++; + } + } + if (files_eq(stg[0].prog, stg[1].prog) != 0) { + fprintf(stderr, "syntaxexport FAIL: cs exe != ww exe (rule 10)\n"); + fail++; + } + } + + /* NON-VACUITY (the bug, isolated): replay the producer on the produced + * `syntax.unit.ww`. With the exports present `w6c -c -I` exits 0; with + * `export ` stripped from the type decls it MUST exit non-zero (the + * export-check fires). Proven for BOTH compilers. */ + { + char unit[1024], ne[1024]; + snprintf(unit, sizeof unit, "%s/prog.cs.sepwork/syntax.unit.ww", td); + snprintf(ne, sizeof ne, "%s/syntax.noexport.ww", td); + snprintf(cmd, sizeof cmd, + "sed 's/export type /type /g' %s > %s", unit, ne); + if (runwait(cmd) != 0) { fail++; goto out; } + + struct { const char *comp; } cc[] = { { "w6c" }, { "w6c_ww" } }; + for (int j = 0; j < 2; j++) { + char wwi[1024], asmf[1024]; + snprintf(wwi, sizeof wwi, "%s/nv.%s.wwi", td, cc[j].comp); + snprintf(asmf, sizeof asmf, "%s/nv.%s.s", td, cc[j].comp); + snprintf(cmd, sizeof cmd, "%s/%s -c -I %s -o %s %s >/dev/null 2>&1", + bin, cc[j].comp, wwi, asmf, unit); + if (runwait(cmd) != 0) { + fprintf(stderr, "syntaxexport FAIL: %s -c -I rejected the " + "EXPORTED unit (post-fix producer must succeed)\n", + cc[j].comp); + fail++; + } + snprintf(cmd, sizeof cmd, "%s/%s -c -I %s -o %s %s >/dev/null 2>&1", + bin, cc[j].comp, wwi, asmf, ne); + if (runwait(cmd) == 0) { + fprintf(stderr, "syntaxexport FAIL: %s -c -I accepted the " + "UNEXPORTED-type unit (bug not reproduced -> vacuous " + "gate)\n", cc[j].comp); + fail++; + } + } + } + +out: + snprintf(cmd, sizeof cmd, "rm -rf %s", td); + runwait(cmd); + if (fail) { + fprintf(stderr, "syntaxexport: %d check(s) failed\n", fail); + return 1; + } + printf("syntaxexport: syntax->wcc boundary sep-PRODUCES clean (16 export " + "type decls) + qualified-ref consumer build+run (exit %d) + cs==ww " + "syntax.wwi/.s/.unit.ww/binary + non-vacuity (export strip rejects, " + "both compilers)\n", EXPECT_EXIT); + return 0; +}