lib/ww/syntax: export the 16 public types consumed by the wcc backend (#72)

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.
This commit is contained in:
2026-06-16 20:19:14 +09:00
parent 7a8acfb952
commit 697e413113
10 changed files with 343 additions and 48 deletions

View File

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