From e8d3d89fefde55d83603ebf394129416e8ef7d46 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 15 Jun 2026 20:16:21 +0900 Subject: [PATCH] wcc/ww: .wwi export-data producer + check_exported_type (#22 M2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New `w6c -I ` flag (both stages) writes a re-parseable ww-prototype rendering of a package's EXPORTED surface. M2 dead-code: nothing consumes .wwi yet (combined.ww stays the live path); the flag is off on every existing invocation, so the 990-997 byte-id gates and all prior tests are unperturbed. The unparse walks the AST type-expr subtree (N_T* nodes), not the tinfo Type* (which collapses nominal pkg.Name identity). Deterministic output: package line, byte-sorted imports, byte-sorted decls — a pure function of the exported API. cmd/w6c/wwi.c + selfhost/cmd/wcc/wwi.ww emit byte-identical .wwi (new cross-stage byte-id substrate, rule 10). check_exported_type (drew) rides the producer entry, flag-gated: an exported signature naming a non-exported nominal is loud-rejected before any byte is written, identically on both stages. Ports harec check.c:4092-4168, recursing the type-AST and gating on the resolved SK_TYPE sym's decl export flag (Sym.exported is vestigial in both stages; the predeclared synthetic `nomem` decl carries no source position and is treated as a builtin leaf — cstage parity). Two wwstage checker AST-mutations are normalized to cstage's pristine view for byte-id: the N_TPARAM tuple-element wrapper (unwrapped) and the variadic `T...`→`[]T` param desugar (peeled). Gate 989_m2wwi_run: ascii/strings/getopt each produce a .wwi that re-parses (wwdump -a) and is cs==ww byte-identical; a private-type-leak fixture is rejected identically by both stages (non-vacuous check). --- Makefile | 9 +- cmd/w6c/gc.h | 5 + cmd/w6c/main.c | 16 + cmd/w6c/wwi.c | 457 +++++++++++++++++++++++ selfhost/cmd/w6c/main.combined.ww | 577 +++++++++++++++++++++++++++++- selfhost/cmd/w6c/main.ww | 19 +- selfhost/cmd/wcc/wwi.ww | 556 ++++++++++++++++++++++++++++ test/wcc/989_m2wwi_run.c | 330 +++++++++++++++++ 8 files changed, 1965 insertions(+), 4 deletions(-) create mode 100644 cmd/w6c/wwi.c create mode 100644 selfhost/cmd/wcc/wwi.ww create mode 100644 test/wcc/989_m2wwi_run.c diff --git a/Makefile b/Makefile index bfb8d09f..3d5cd149 100644 --- a/Makefile +++ b/Makefile @@ -43,7 +43,7 @@ WD_SRC = cmd/wwdump/main.c WD_OBJ = $(WD_SRC:cmd/wwdump/%.c=$(OBJ)/wwdump/%.o) W6C_SRC = cmd/w6c/main.c cmd/w6c/cgen.c cmd/w6c/txt.c cmd/w6c/swt.c \ - cmd/w6c/peep.c cmd/w6c/reg.c + cmd/w6c/peep.c cmd/w6c/reg.c cmd/w6c/wwi.c W6C_OBJ = $(W6C_SRC:cmd/w6c/%.c=$(OBJ)/w6c/%.o) W6A_SRC = cmd/w6a/main.c cmd/w6a/lex.c cmd/w6a/parse.c cmd/w6a/asm.c cmd/w6a/obj.c @@ -146,7 +146,7 @@ $(BIN)/wwdump_ww: selfhost/cmd/wwdump/main.ww \ $(BIN)/w6c_ww: selfhost/cmd/w6c/main.ww \ lib/ww/lex/lex.ww lib/ww/lex/tok.ww lib/ww/ast.ww \ lib/ww/parse/parse.ww lib/ww/parse/expr.ww lib/ww/parse/stmt.ww lib/ww/parse/decl.ww lib/ww/typ.ww lib/ww/sym.ww \ - selfhost/cmd/wcc/check.ww \ + selfhost/cmd/wcc/check.ww selfhost/cmd/wcc/wwi.ww \ selfhost/cmd/wcc/cgen.ww selfhost/cmd/wcc/cgenexpr.ww \ selfhost/cmd/wcc/cgenstmt.ww selfhost/cmd/wcc/cgenutil.ww \ selfhost/cmd/wcc/cgendecl.ww \ @@ -561,6 +561,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_siphash_run $(BIN)/test_sha256_run \ $(BIN)/test_regex_run \ $(BIN)/test_lib_byteid \ + $(BIN)/test_m2wwi_run \ $(BIN)/test_floatlit_run \ $(BIN)/test_checked_run \ $(BIN)/test_floatarr_run \ @@ -3082,6 +3083,10 @@ $(BIN)/test_lib_byteid: test/wcc/989_lib_byteid.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_m2wwi_run: test/wcc/989_m2wwi_run.c $(BIN)/ww $(BIN)/w6c \ + $(BIN)/w6c_ww $(BIN)/wwdump $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_floatlit_run: test/wcc/989_floatlit_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/cmd/w6c/gc.h b/cmd/w6c/gc.h index baceaa46..1c442b28 100644 --- a/cmd/w6c/gc.h +++ b/cmd/w6c/gc.h @@ -56,6 +56,11 @@ void emit(Cg*, Prog*); /* txt.c */ void txt_emit(FILE*, Prog *head); +/* wwi.c — `.wwi` export-data producer (w6c -I). M2 dead-code: writes a + * re-parseable ww-prototype rendering of the package's exported surface. + * Returns non-zero if check_exported_type rejects a dangling export. */ +int wwi_emit(Checker *c, FILE *of, Node *file); + /* swt.c, peep.c, reg.c — placeholders for now */ void peephole(Cg*); void regalloc_init(Cg*); diff --git a/cmd/w6c/main.c b/cmd/w6c/main.c index 89529a31..ec489a3f 100644 --- a/cmd/w6c/main.c +++ b/cmd/w6c/main.c @@ -33,11 +33,14 @@ main(int argc, char **argv) { const char *src = NULL; const char *out = NULL; + const char *wwiout = NULL; /* -I : M2 export-data producer */ int testmode = 0; for (int i = 1; i < argc; i++) { const char *a = argv[i]; if (strcmp(a, "-o") == 0 && i + 1 < argc) { out = argv[++i]; + } else if (strcmp(a, "-I") == 0 && i + 1 < argc) { + wwiout = argv[++i]; } else if (strcmp(a, "-T") == 0) { testmode = 1; } else if (a[0] == '-') { @@ -78,6 +81,19 @@ main(int argc, char **argv) check_file(&c, file); if (c.errs) return 1; + /* M2 export-data: write the `.wwi` after a clean check, before cgen. + * Dead on the live path (no existing invocation passes -I); the + * producer's check_exported_type may reject a dangling export. */ + if (wwiout) { + FILE *wf = fopen(wwiout, "wb"); + if (wf == NULL) { + fprintf(stderr, "w6c: cannot open %s\n", wwiout); + return 1; + } + if (wwi_emit(&c, wf, file) != 0) { fclose(wf); return 1; } + fclose(wf); + } + FILE *of = stdout; if (out) { of = fopen(out, "wb"); diff --git a/cmd/w6c/wwi.c b/cmd/w6c/wwi.c new file mode 100644 index 00000000..b3c083fc --- /dev/null +++ b/cmd/w6c/wwi.c @@ -0,0 +1,457 @@ +/* + * wwi.c — `.wwi` export-data producer (w6c -I). M2 DEAD-CODE: writes a + * re-parseable ww-prototype rendering of a package's EXPORTED surface. + * Nothing consumes `.wwi` yet (combined.ww stays the live path); the only + * caller is the new -I flag, off on every existing invocation. + * + * Specs: .ai/rob-M2-spec.md (producer) + .ai/drew-M2-checkexported.md + * (check_exported_type). Two load-bearing choices follow them: + * + * - Unparse walks the AST type-expr subtree (the N_T* nodes), NOT the + * checked Type* — tinfo collapses nominal pkg.Name identity (MEMORY + * tinfo_lossy_nominal). The AST preserves surface syntax + names. + * - check_exported_type rides the producer entry (flag-gated), so M2 + * stays dead on the normal `.s` path. It rejects exactly one thing: + * an exported signature naming a non-exported nominal type. + * + * A `.wwi` is ONE package's interface. The M2 gate feeds w6c a driver- + * combined unit (imports concatenated ahead of the target), so the emit + * filters to PRIMARY decls (imported == 0) — in the real per-package + * compile every decl is primary, so the filter is a no-op there. Output + * is a pure function of the exported API: package line, byte-sorted + * imports, byte-sorted decls (no map/hash iteration order). + */ +#include "gc.h" +#include +#include +#include + +static int +wwi_primary(Node *n) +{ + /* imported == 1 marks a decl reached through a `//ww:module ` + * boundary (an imported module's concatenated section). */ + return n && n->imported == 0; +} + +/* --- check_exported_type (drew) ------------------------------------- * + * Resolve an N_TNAME to its type sym WITHOUT the side effects of + * resolve_typename (no on-demand resolve, no double "unknown type" + * error). A primitive/keyword resolves to no SK_TYPE → leaf. By the + * time the producer runs, check_file has finished and c->cur == c->top. + */ +static Sym * +wwi_typesym(Checker *c, const char *nm) +{ + if (nm == NULL) return NULL; + Sym *s = scope_lookup_type(c->cur, NULL, nm); + if (s == NULL) { + const char *dot = strrchr(nm, '.'); + if (dot) + s = scope_lookup_type(c->cur, NULL, dot + 1); + } + if (s && s->kind == SK_TYPE) + return s; + return NULL; +} + +static int +wwi_check_type(Checker *c, Pos loc, Node *t) +{ + int bad = 0; + if (t == NULL) + return 0; + switch (t->kind) { + case N_TNAME: { + Sym *s = wwi_typesym(c, t->str); + /* Sym.exported is vestigial (the checker never sets it); the + * nominal's export status lives on its decl node, parser-set. + * No file-guard twin to wwstage's: cstage resolves `nomem` and + * the primitives through lookup_builtin (no scope SK_TYPE), so + * scope_lookup_type returns NULL for them and they never reach + * this reject — harec's STORAGE_NOMEM leaf-arm, by construction. */ + if (s && s->decl && s->decl->kind == N_TYPEDECL + && s->decl->export == 0) { + errorf(loc, "exported declaration references " + "unexported type '%s'", t->str); + bad = 1; + } + break; + } + case N_TPTR: + case N_TSLICE: + case N_TBANG: + case N_TCHAN: + bad |= wwi_check_type(c, loc, t->lhs); + break; + case N_TARRAY: + /* element only; the length is a const-expr, not a type. */ + bad |= wwi_check_type(c, loc, t->lhs); + break; + case N_TFN: + for (Node *p = t->list; p; p = p->next) + bad |= wwi_check_type(c, loc, p->lhs); + bad |= wwi_check_type(c, loc, t->lhs); + break; + case N_TSTRUCT: + for (Node *f = t->list; f; f = f->next) + bad |= wwi_check_type(c, loc, f->lhs); + break; + case N_TTAGGED: + case N_TTUPLE: + for (Node *e = t->list; e; e = e->next) + bad |= wwi_check_type(c, loc, e); + break; + case N_TENUM: + /* the inline enum body is the definition, not a reference; + * recurse only its storage type (members are values). */ + bad |= wwi_check_type(c, loc, t->lhs); + break; + default: + /* a non-type node in type position should not occur — leaf. */ + break; + } + return bad; +} + +static int +wwi_check_decl(Checker *c, Node *d) +{ + int bad = 0; + switch (d->kind) { + case N_FNDECL: + for (Node *p = d->list; p; p = p->next) + bad |= wwi_check_type(c, d->pos, p->lhs); + bad |= wwi_check_type(c, d->pos, d->lhs); /* ret */ + break; + case N_TYPEDECL: + bad |= wwi_check_type(c, d->pos, d->lhs); + break; + case N_DEF: + /* the declared type; the rhs const value is not a type. */ + bad |= wwi_check_type(c, d->pos, d->lhs); + break; + case N_LET: + bad |= wwi_check_type(c, d->pos, d->lhs); + break; + default: + break; + } + return bad; +} + +/* --- type-expr + const-expr unparser (rob §2.2/§2.4) ---------------- */ + +static void wwi_expr(FILE *of, Node *e); +static void wwi_type(FILE *of, Node *t); + +static void +wwi_quote(FILE *of, const char *s, u64 n) +{ + fputc('"', of); + for (u64 i = 0; i < n; i++) { + unsigned char c = (unsigned char)s[i]; + switch (c) { + case '"': fputs("\\\"", of); break; + case '\\': fputs("\\\\", of); break; + case '\n': fputs("\\n", of); break; + case '\t': fputs("\\t", of); break; + default: + if (c < 0x20) fprintf(of, "\\x%02x", c); + else fputc(c, of); + } + } + fputc('"', of); +} + +static void +wwi_param(FILE *of, Node *p) +{ + if (p->str && strcmp(p->str, "...") == 0) { /* C-style FFI `...` */ + fputs("...", of); + return; + } + if (p->str && p->str[0]) { + fputs(p->str, of); + fputs(": ", of); + } + wwi_type(of, p->lhs); + if (p->op == TK_ELLIPSIS) /* Hare `T...` variadic */ + fputs("...", of); +} + +static void +wwi_type(FILE *of, Node *t) +{ + if (t == NULL) { /* absent return type spells void */ + fputs("void", of); + return; + } + switch (t->kind) { + case N_TNAME: + fputs(t->str ? t->str : "void", of); + break; + case N_TPTR: + fputc('*', of); + wwi_type(of, t->lhs); + break; + case N_TSLICE: + fputs("[]", of); + wwi_type(of, t->lhs); + break; + case N_TARRAY: + fputc('[', of); + if (t->rhs) wwi_expr(of, t->rhs); + else fputc('_', of); + fputc(']', of); + wwi_type(of, t->lhs); + break; + case N_TBANG: + fputc('!', of); + wwi_type(of, t->lhs); + break; + case N_TCHAN: + fputs("chan ", of); + wwi_type(of, t->lhs); + break; + case N_TFN: + fputs("fn(", of); + for (Node *p = t->list; p; p = p->next) { + if (p != t->list) fputs(", ", of); + wwi_param(of, p); + } + fputs(") ", of); + wwi_type(of, t->lhs); + break; + case N_TSTRUCT: + fputs("struct { ", of); + for (Node *f = t->list; f; f = f->next) { + if (f != t->list) fputs(", ", of); + if (f->str && f->str[0]) { + fputs(f->str, of); + fputs(": ", of); + } + wwi_type(of, f->lhs); + } + fputs(" }", of); + break; + case N_TTUPLE: + fputc('(', of); + for (Node *e = t->list; e; e = e->next) { + if (e != t->list) fputs(", ", of); + wwi_type(of, e); + } + fputc(')', of); + break; + case N_TTAGGED: + fputc('(', of); + for (Node *e = t->list; e; e = e->next) { + if (e != t->list) fputs(" | ", of); + wwi_type(of, e); + } + fputc(')', of); + break; + case N_TENUM: + fputs("enum ", of); + if (t->lhs) { + wwi_type(of, t->lhs); + fputc(' ', of); + } + fputs("{ ", of); + for (Node *m = t->list; m; m = m->next) { + if (m != t->list) fputs(", ", of); + fputs(m->str, of); + if (m->lhs) { + fputs(" = ", of); + wwi_expr(of, m->lhs); + } + } + fputs(" }", of); + break; + default: + fatal("wwi: unhandled type node kind %d", t->kind); + } +} + +static void +wwi_expr(FILE *of, Node *e) +{ + if (e == NULL) + return; + switch (e->kind) { + case N_INTLIT: + fprintf(of, "%llu", (unsigned long long)e->uval); + if (e->tsuffix) fputs(e->tsuffix, of); + break; + case N_IDENT: + case N_TNAME: + fputs(e->str, of); + break; + case N_DOT: + wwi_expr(of, e->lhs); + fputc('.', of); + fputs(e->str, of); + break; + case N_TRUE: fputs("true", of); break; + case N_FALSE: fputs("false", of); break; + case N_NIL: fputs("nil", of); break; + case N_STRLIT: wwi_quote(of, e->str, e->strlen); break; + case N_BIN: + wwi_expr(of, e->lhs); + fprintf(of, " %s ", tokname(e->op)); + wwi_expr(of, e->rhs); + break; + case N_UN: + fputs(tokname(e->op), of); + wwi_expr(of, e->lhs); + break; + default: + fatal("wwi: unhandled const-expr node kind %d", e->kind); + } +} + +static void +wwi_decl(FILE *of, Node *d) +{ + switch (d->kind) { + case N_FNDECL: + fputs("export fn ", of); + fputs(d->str, of); + fputc('(', of); + for (Node *p = d->list; p; p = p->next) { + if (p != d->list) fputs(", ", of); + wwi_param(of, p); + } + fputs(") ", of); + wwi_type(of, d->lhs); + fputs(";\n", of); + break; + case N_TYPEDECL: + fputs("export type ", of); + fputs(d->str, of); + fputs(" = ", of); + wwi_type(of, d->lhs); + fputs(";\n", of); + break; + case N_DEF: + fputs("export def ", of); + fputs(d->str, of); + fputs(": ", of); + wwi_type(of, d->lhs); + fputs(" = ", of); + wwi_expr(of, d->rhs); + fputs(";\n", of); + break; + case N_LET: + fputs("export let ", of); + fputs(d->str, of); + fputs(": ", of); + if (d->lhs == NULL) + fatal("wwi: exported let '%s' has no declared type", + d->str); + wwi_type(of, d->lhs); + fputs(";\n", of); + break; + default: + break; + } +} + +/* --- deterministic ordering (rob §3) -------------------------------- */ + +struct declent { Node *d; int idx; }; +struct useent { const char *path; int idx; }; + +static int +declcmp(const void *a, const void *b) +{ + const struct declent *x = a, *y = b; + int r = strcmp(x->d->str, y->d->str); /* symbol name ONLY */ + if (r != 0) return r; + return x->idx - y->idx; /* total order, stable */ +} + +static int +usecmp(const void *a, const void *b) +{ + const struct useent *x = a, *y = b; + int r = strcmp(x->path, y->path); + if (r != 0) return r; + return x->idx - y->idx; +} + +static int +wwi_is_decl(Node *d) +{ + return d->kind == N_FNDECL || d->kind == N_TYPEDECL + || d->kind == N_DEF || d->kind == N_LET; +} + +int +wwi_emit(Checker *c, FILE *of, Node *file) +{ + /* §5: check_exported_type FIRST, before any byte is written — a + * producer without it can emit a dangling `.wwi`. */ + int bad = 0; + for (Node *d = file->list; d; d = d->next) { + if (!wwi_primary(d) || !d->export || !wwi_is_decl(d)) + continue; + bad |= wwi_check_decl(c, d); + } + if (bad) + return 1; + + /* package line: leaf of the first primary decl's module tag. */ + const char *pkg = "main"; + for (Node *d = file->list; d; d = d->next) { + if (wwi_primary(d) && d->module && d->module[0]) { + const char *dot = strrchr(d->module, '.'); + pkg = dot ? dot + 1 : d->module; + break; + } + } + fprintf(of, "package %s;\n", pkg); + + /* imports — primary N_USE, byte-sorted by import path. */ + int nuse = 0; + for (Node *u = file->list; u; u = u->next) + if (u->kind == N_USE && wwi_primary(u)) + nuse++; + if (nuse > 0) { + struct useent *us = malloc((size_t)nuse * sizeof *us); + int k = 0; + for (Node *u = file->list; u; u = u->next) { + if (u->kind != N_USE || !wwi_primary(u)) + continue; + us[k].path = u->usepath ? u->usepath : u->str; + us[k].idx = k; + k++; + } + qsort(us, (size_t)nuse, sizeof *us, usecmp); + for (int i = 0; i < nuse; i++) + fprintf(of, "import %s;\n", us[i].path); + free(us); + } + + /* decls — exported primary, byte-sorted by symbol name. */ + int ndecl = 0; + for (Node *d = file->list; d; d = d->next) + if (wwi_primary(d) && d->export && wwi_is_decl(d)) + ndecl++; + if (ndecl > 0) { + struct declent *ds = malloc((size_t)ndecl * sizeof *ds); + int k = 0; + for (Node *d = file->list; d; d = d->next) { + if (!wwi_primary(d) || !d->export || !wwi_is_decl(d)) + continue; + ds[k].d = d; + ds[k].idx = k; + k++; + } + qsort(ds, (size_t)ndecl, sizeof *ds, declcmp); + for (int i = 0; i < ndecl; i++) + wwi_decl(of, ds[i].d); + free(ds); + } + return 0; +} diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 0390dee5..4d9a66d9 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -45288,6 +45288,564 @@ export fn fargregname(i: i32) str = { return "?"; }; +//ww:module-reset +// wwi.ww — `.wwi` export-data producer (w6c_ww -I). M2 DEAD-CODE: writes +// a re-parseable ww-prototype rendering of a package's EXPORTED surface. +// Nothing consumes `.wwi` yet (combined.ww stays the live path); the only +// caller is the new -I flag, off on every existing invocation. +// +// wwstage twin of cmd/w6c/wwi.c — byte-identical output is a rule-10 +// requirement (`.wwi` is a new cross-stage byte-id substrate). Specs: +// .ai/rob-M2-spec.md + .ai/drew-M2-checkexported.md. +// +// - Unparse walks the AST type-expr subtree (the N_T* nodes), NOT the +// tinfo — tinfo collapses nominal pkg.Name identity. +// - check_exported_type rides the producer entry (flag-gated), so M2 +// stays dead on the normal `.s` path. It rejects exactly one thing: +// an exported signature naming a non-exported nominal type. +// - A `.wwi` is ONE package's interface; the M2 gate feeds a driver- +// combined unit, so the emit filters to PRIMARY decls (imported==0). + +// Imports mirror check.ww (os/tok/strconv only): node/nkind/sym/skind/ +// scopelookuptype/streq/checker/tkind resolve bare in the selfhost's flat +// bundled scope, exactly as check.ww references them. +package wcc; + +import os; +import tok; +import strconv; + +// --- byte writers ------------------------------------------------------ + +fn wputs(fd: i32, s: str) void = { + os.write(fd, s.ptr, s.len: u64); +}; + +fn wputb(fd: i32, b: u8) void = { + let buf: [1]u8; + buf[0] = b; + os.write(fd, buf.ptr, 1u64); +}; + +fn wquote(fd: i32, s: str) void = { + wputb(fd, '"'); + let i: i32 = 0; + for (i < s.len) { + let c: u8 = s[i]; + if (c == '"') { + wputs(fd, "\\\""); + } else { if (c == '\\') { + wputs(fd, "\\\\"); + } else { if (c == '\n') { + wputs(fd, "\\n"); + } else { if (c == '\t') { + wputs(fd, "\\t"); + } else { if (c < 32u8) { + let hi: u8 = c >> 4u8; + let lo: u8 = c & 15u8; + let h: u8 = 0u8; + let l: u8 = 0u8; + if (hi < 10u8) { h = hi + 48u8; } else { h = (hi - 10u8) + 97u8; }; + if (lo < 10u8) { l = lo + 48u8; } else { l = (lo - 10u8) + 97u8; }; + let buf: [4]u8; + buf[0] = 92u8; + buf[1] = 120u8; + buf[2] = h; + buf[3] = l; + os.write(fd, buf.ptr, 4u64); + } else { + wputb(fd, c); + };};};};}; + i += 1; + }; + wputb(fd, '"'); +}; + +// --- check_exported_type (drew) --------------------------------------- +// +// Resolve an N_TNAME to its type sym via the public sym helpers (mirror +// of cstage wwi_typesym / scope_lookup_type) WITHOUT the side effects of +// the checker's aliassym (no on-demand resolve, no double error). A +// primitive/keyword resolves to no SK_TYPE → leaf. By producer time +// checkfile has finished and c.cur == c.top. + +fn wwitypesym(c: *checker, nm: str) *sym = { + let empty: str; + let s: *sym = scopelookuptype(c.cur, empty, nm); + if (s == nil) { + let dotidx: i32 = -1; + let i: i32 = 0; + for (i < nm.len) { + if (nm[i] == 46u8) { dotidx = i; }; + i += 1; + }; + if (dotidx >= 0) { + let leaf: str; + leaf.ptr = nm.ptr + ((dotidx + 1): u64); + leaf.len = nm.len - dotidx - 1; + s = scopelookuptype(c.cur, empty, leaf); + }; + }; + if (s == nil) { return nil; }; + if (s.skind != skind.SK_TYPE) { return nil; }; + return s; +}; + +fn wwireject(d: *node, nm: str) void = { + wputs(2, d.file); + wputs(2, ":"); + wputs(2, strconv.u64tos(d.line: u64, strconv.base.DEC)); + wputs(2, ":"); + wputs(2, strconv.u64tos(d.col: u64, strconv.base.DEC)); + wputs(2, ": error: exported declaration references unexported type '"); + wputs(2, nm); + wputs(2, "'\n"); +}; + +// Returns 1 if a non-exported nominal was named (loud), else 0. +fn wwichecktype(c: *checker, d: *node, t: *node) i32 = { + if (t == nil) { return 0; }; + // rule-10: the wwstage checker wraps N_TTUPLE.list elements in + // N_TPARAM (ast.ww:101); cstage keeps the type-AST pristine. Unwrap + // transparently so the recursion sees the same shape cstage walks. + if (t.kind == nkind.N_TPARAM) { return wwichecktype(c, d, t.lhs); }; + let bad: i32 = 0; + if (t.kind == nkind.N_TNAME) { + let s: *sym = wwitypesym(c, t.str); + // sym.exported is vestigial (never set); the nominal's export + // status lives on its decl node, parser-set. + if (s != nil) { + if (s.decl != nil) { + if (s.decl.kind == nkind.N_TYPEDECL) { + // file.len == 0 ⇒ a checkinit-synthesized + // predeclared builtin (the ONLY empty-source + // decls: nomemdecl check.ww:126, the `void` + // TNAME check.ww:122), not a user nominal — ww's + // analogue of harec's STORAGE_NOMEM leaf-arm, and + // why cstage (lookup_builtin, no scope SK_TYPE) + // needs no such guard. + if (s.decl.file.len > 0) { + if (s.decl.exported == 0) { + wwireject(d, t.str); + bad = 1; + }; + }; + }; + }; + }; + } else { if ( + t.kind == nkind.N_TPTR || + t.kind == nkind.N_TSLICE || + t.kind == nkind.N_TBANG || + t.kind == nkind.N_TCHAN + ) { + bad = bad | wwichecktype(c, d, t.lhs); + } else { if (t.kind == nkind.N_TARRAY) { + // element only; the length is a const-expr, not a type. + bad = bad | wwichecktype(c, d, t.lhs); + } else { if (t.kind == nkind.N_TFN) { + let p: *node = t.list; + for (p != nil) { + bad = bad | wwichecktype(c, d, p.lhs); + p = p.next; + }; + bad = bad | wwichecktype(c, d, t.lhs); + } else { if (t.kind == nkind.N_TSTRUCT) { + let f: *node = t.list; + for (f != nil) { + bad = bad | wwichecktype(c, d, f.lhs); + f = f.next; + }; + } else { if (t.kind == nkind.N_TTAGGED || t.kind == nkind.N_TTUPLE) { + let e: *node = t.list; + for (e != nil) { + bad = bad | wwichecktype(c, d, e); + e = e.next; + }; + } else { if (t.kind == nkind.N_TENUM) { + // the inline enum body is the definition, not a reference; + // recurse only its storage type (members are values). + bad = bad | wwichecktype(c, d, t.lhs); + };};};};};};}; + return bad; +}; + +fn wwicheckdecl(c: *checker, d: *node) i32 = { + let bad: i32 = 0; + if (d.kind == nkind.N_FNDECL) { + let p: *node = d.list; + for (p != nil) { + bad = bad | wwichecktype(c, d, p.lhs); + p = p.next; + }; + bad = bad | wwichecktype(c, d, d.lhs); // ret + } else { if (d.kind == nkind.N_TYPEDECL) { + bad = bad | wwichecktype(c, d, d.lhs); + } else { if (d.kind == nkind.N_DEF) { + // the declared type; the rhs const value is not a type. + bad = bad | wwichecktype(c, d, d.lhs); + } else { if (d.kind == nkind.N_LET) { + bad = bad | wwichecktype(c, d, d.lhs); + };};};}; + return bad; +}; + +// --- type-expr + const-expr unparser (rob §2.2/§2.4) ------------------ + +fn wwiexpr(fd: i32, e: *node) void = { + if (e == nil) { return; }; + if (e.kind == nkind.N_INTLIT) { + wputs(fd, strconv.u64tos(e.uval, strconv.base.DEC)); + if (e.tsuffix.len > 0) { wputs(fd, e.tsuffix); }; + } else { if (e.kind == nkind.N_IDENT || e.kind == nkind.N_TNAME) { + wputs(fd, e.str); + } else { if (e.kind == nkind.N_DOT) { + wwiexpr(fd, e.lhs); + wputb(fd, '.'); + wputs(fd, e.str); + } else { if (e.kind == nkind.N_TRUE) { + wputs(fd, "true"); + } else { if (e.kind == nkind.N_FALSE) { + wputs(fd, "false"); + } else { if (e.kind == nkind.N_NIL) { + wputs(fd, "nil"); + } else { if (e.kind == nkind.N_STRLIT) { + wquote(fd, e.str); + } else { if (e.kind == nkind.N_BIN) { + wwiexpr(fd, e.lhs); + wputb(fd, 32u8); + wputs(fd, tokname(e.op)); + wputb(fd, 32u8); + wwiexpr(fd, e.rhs); + } else { if (e.kind == nkind.N_UN) { + wputs(fd, tokname(e.op)); + wwiexpr(fd, e.lhs); + } else { + wputs(2, "wwi: unhandled const-expr node kind\n"); + os.exit(1); + };};};};};};};};}; +}; + +fn wwiparam(fd: i32, p: *node) void = { + if (streq(p.str, "...")) { // C-style FFI `...` + wputs(fd, "..."); + return; + }; + if (p.str.len > 0) { + wputs(fd, p.str); + wputs(fd, ": "); + }; + // rule-10: the wwstage checker desugars a Hare variadic `T...` param + // in place to `[]T` (lhs becomes N_TSLICE); cstage leaves lhs == T. + // Peel the inserted slice so both stages emit the surface `T...`. + if (p.op == tkind.TK_ELLIPSIS && p.lhs != nil && p.lhs.kind == nkind.N_TSLICE) { + wwitype(fd, p.lhs.lhs); + } else { + wwitype(fd, p.lhs); + }; + if (p.op == tkind.TK_ELLIPSIS) { // Hare `T...` variadic + wputs(fd, "..."); + }; +}; + +fn wwitype(fd: i32, t: *node) void = { + if (t == nil) { // absent return type spells void + wputs(fd, "void"); + return; + }; + // rule-10: unwrap the wwstage-only N_TPARAM tuple-element wrapper + // (ast.ww:101) so the unparse matches cstage's pristine type-AST. + if (t.kind == nkind.N_TPARAM) { wwitype(fd, t.lhs); return; }; + if (t.kind == nkind.N_TNAME) { + if (t.str.len > 0) { wputs(fd, t.str); } else { wputs(fd, "void"); }; + } else { if (t.kind == nkind.N_TPTR) { + wputb(fd, '*'); + wwitype(fd, t.lhs); + } else { if (t.kind == nkind.N_TSLICE) { + wputs(fd, "[]"); + wwitype(fd, t.lhs); + } else { if (t.kind == nkind.N_TARRAY) { + wputb(fd, '['); + if (t.rhs != nil) { wwiexpr(fd, t.rhs); } else { wputb(fd, '_'); }; + wputb(fd, ']'); + wwitype(fd, t.lhs); + } else { if (t.kind == nkind.N_TBANG) { + wputb(fd, '!'); + wwitype(fd, t.lhs); + } else { if (t.kind == nkind.N_TCHAN) { + wputs(fd, "chan "); + wwitype(fd, t.lhs); + } else { if (t.kind == nkind.N_TFN) { + wputs(fd, "fn("); + let p: *node = t.list; + for (p != nil) { + if (p != t.list) { wputs(fd, ", "); }; + wwiparam(fd, p); + p = p.next; + }; + wputs(fd, ") "); + wwitype(fd, t.lhs); + } else { if (t.kind == nkind.N_TSTRUCT) { + wputs(fd, "struct { "); + let f: *node = t.list; + for (f != nil) { + if (f != t.list) { wputs(fd, ", "); }; + if (f.str.len > 0) { + wputs(fd, f.str); + wputs(fd, ": "); + }; + wwitype(fd, f.lhs); + f = f.next; + }; + wputs(fd, " }"); + } else { if (t.kind == nkind.N_TTUPLE) { + wputb(fd, '('); + let e: *node = t.list; + for (e != nil) { + if (e != t.list) { wputs(fd, ", "); }; + wwitype(fd, e); + e = e.next; + }; + wputb(fd, ')'); + } else { if (t.kind == nkind.N_TTAGGED) { + wputb(fd, '('); + let e: *node = t.list; + for (e != nil) { + if (e != t.list) { wputs(fd, " | "); }; + wwitype(fd, e); + e = e.next; + }; + wputb(fd, ')'); + } else { if (t.kind == nkind.N_TENUM) { + wputs(fd, "enum "); + if (t.lhs != nil) { + wwitype(fd, t.lhs); + wputb(fd, 32u8); + }; + wputs(fd, "{ "); + let m: *node = t.list; + for (m != nil) { + if (m != t.list) { wputs(fd, ", "); }; + wputs(fd, m.str); + if (m.lhs != nil) { + wputs(fd, " = "); + wwiexpr(fd, m.lhs); + }; + m = m.next; + }; + wputs(fd, " }"); + } else { + wputs(2, "wwi: unhandled type node kind\n"); + os.exit(1); + };};};};};};};};};};}; +}; + +fn wwidecl(fd: i32, d: *node) void = { + if (d.kind == nkind.N_FNDECL) { + wputs(fd, "export fn "); + wputs(fd, d.str); + wputb(fd, '('); + let p: *node = d.list; + for (p != nil) { + if (p != d.list) { wputs(fd, ", "); }; + wwiparam(fd, p); + p = p.next; + }; + wputs(fd, ") "); + wwitype(fd, d.lhs); + wputs(fd, ";\n"); + } else { if (d.kind == nkind.N_TYPEDECL) { + wputs(fd, "export type "); + wputs(fd, d.str); + wputs(fd, " = "); + wwitype(fd, d.lhs); + wputs(fd, ";\n"); + } else { if (d.kind == nkind.N_DEF) { + wputs(fd, "export def "); + wputs(fd, d.str); + wputs(fd, ": "); + wwitype(fd, d.lhs); + wputs(fd, " = "); + wwiexpr(fd, d.rhs); + wputs(fd, ";\n"); + } else { if (d.kind == nkind.N_LET) { + wputs(fd, "export let "); + wputs(fd, d.str); + wputs(fd, ": "); + if (d.lhs == nil) { + wputs(2, "wwi: exported let has no declared type\n"); + os.exit(1); + }; + wwitype(fd, d.lhs); + wputs(fd, ";\n"); + };};};}; +}; + +// --- deterministic ordering (rob §3) ---------------------------------- + +fn wwiprimary(n: *node) bool = { + // imported==1 marks a decl reached through a `//ww:module ` + // boundary (an imported module's concatenated section). + if (n == nil) { return false; }; + return n.imported == 0; +}; + +fn wwiisdecl(d: *node) bool = { + return d.kind == nkind.N_FNDECL || d.kind == nkind.N_TYPEDECL || + d.kind == nkind.N_DEF || d.kind == nkind.N_LET; +}; + +// strcmp — byte lexicographic, mirror C strcmp sign (<0/0/>0). Both +// stages key the sort identically, so the `.wwi` order is deterministic. +fn wwistrcmp(a: str, b: str) i32 = { + let i: i32 = 0; + for (i < a.len && i < b.len) { + let ca: i32 = a[i]: i32; + let cb: i32 = b[i]: i32; + if (ca != cb) { return ca - cb; }; + i += 1; + }; + return a.len - b.len; +}; + +// Selection sort over parallel (key, node) arrays. Total order keyed on +// the symbol name; ties broken by original index — so the result is +// stable regardless of any same-name collision, matching cstage's qsort +// + idx tiebreak. +fn wwisortdecls(keys: []str, nodes: []*node, n: i32) void = { + let i: i32 = 0; + for (i < n) { + let best: i32 = i; + let j: i32 = i + 1; + for (j < n) { + let r: i32 = wwistrcmp(keys[j], keys[best]); + if (r < 0) { best = j; }; + j += 1; + }; + if (best != i) { + let tk: str = keys[i]; keys[i] = keys[best]; keys[best] = tk; + let tn: *node = nodes[i]; nodes[i] = nodes[best]; nodes[best] = tn; + }; + i += 1; + }; +}; + +export fn wwiemit(c: *checker, file: *node, path: str) i32 = { + // §5: check_exported_type FIRST, before any byte — a producer + // without it can emit a dangling `.wwi`. + let bad: i32 = 0; + let d: *node = file.list; + for (d != nil) { + if (wwiprimary(d) && d.exported != 0 && wwiisdecl(d)) { + bad = bad | wwicheckdecl(c, d); + }; + d = d.next; + }; + if (bad != 0) { return 1i32; }; + + let fd: i32 = os.open(path, + os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 + if (fd < 0) { + wputs(2, "w6c: cannot open "); + wputs(2, path); + wputs(2, "\n"); + return 1i32; + }; + + // package line: leaf of the first primary decl's module tag. + let pkg: str = "main"; + let pd: *node = file.list; + for (pd != nil) { + if (wwiprimary(pd) && pd.nmod.len > 0) { + let dotidx: i32 = -1; + let i: i32 = 0; + for (i < pd.nmod.len) { + if (pd.nmod[i] == 46u8) { dotidx = i; }; + i += 1; + }; + if (dotidx >= 0) { + let leaf: str; + leaf.ptr = pd.nmod.ptr + ((dotidx + 1): u64); + leaf.len = pd.nmod.len - dotidx - 1; + pkg = leaf; + } else { + pkg = pd.nmod; + }; + pd = nil; + } else { + pd = pd.next; + }; + }; + wputs(fd, "package "); + wputs(fd, pkg); + wputs(fd, ";\n"); + + // imports — primary N_USE, byte-sorted by import path. + let nuse: i32 = 0; + let u: *node = file.list; + for (u != nil) { + if (u.kind == nkind.N_USE && wwiprimary(u)) { nuse += 1; }; + u = u.next; + }; + if (nuse > 0) { + let upaths: []str = alloc([], nuse: u64)!; + upaths.len = nuse; + let unodes: []*node = alloc([], nuse: u64)!; + unodes.len = nuse; + let k: i32 = 0; + u = file.list; + for (u != nil) { + if (u.kind == nkind.N_USE && wwiprimary(u)) { + if (u.usepath.len > 0) { upaths[k] = u.usepath; } else { upaths[k] = u.str; }; + unodes[k] = u; + k += 1; + }; + u = u.next; + }; + wwisortdecls(upaths, unodes, nuse); + let i: i32 = 0; + for (i < nuse) { + wputs(fd, "import "); + wputs(fd, upaths[i]); + wputs(fd, ";\n"); + i += 1; + }; + }; + + // decls — exported primary, byte-sorted by symbol name. + let ndecl: i32 = 0; + d = file.list; + for (d != nil) { + if (wwiprimary(d) && d.exported != 0 && wwiisdecl(d)) { ndecl += 1; }; + d = d.next; + }; + if (ndecl > 0) { + let dkeys: []str = alloc([], ndecl: u64)!; + dkeys.len = ndecl; + let dnodes: []*node = alloc([], ndecl: u64)!; + dnodes.len = ndecl; + let k: i32 = 0; + d = file.list; + for (d != nil) { + if (wwiprimary(d) && d.exported != 0 && wwiisdecl(d)) { + dkeys[k] = d.str; + dnodes[k] = d; + k += 1; + }; + d = d.next; + }; + wwisortdecls(dkeys, dnodes, ndecl); + let i: i32 = 0; + for (i < ndecl) { + wwidecl(fd, dnodes[i]); + i += 1; + }; + }; + + os.close(fd); + return 0i32; +}; + //ww:module-reset // selfhost/cmd/w6c/main.ww — port of cmd/w6c/main.c. // @@ -45314,6 +45872,7 @@ import typ; import sym; import check; import cgen; +import wwi; fn cstreq(a: *u8, lit: str) bool = { let n: u64 = lit.len: u64; @@ -45370,6 +45929,7 @@ fn slurp(path: *u8) (*u8, u64) = { export fn main(argc: i32, argv: **u8) i32 = { let src: *u8 = nil; let out: *u8 = nil; + let wwiout: *u8 = nil; // -I : M2 export-data producer let testmode: i32 = 0i32; // #15: `-T` test-mode let i: i32 = 1; @@ -45383,6 +45943,14 @@ export fn main(argc: i32, argv: **u8) i32 = { return 2; }; out = argv[i]; + } else { if (cstreq(a, "-I")) { + i += 1; + if (i >= argc) { + let m: str = "w6c: -I requires arg\n"; + os.write(2, m.ptr, m.len: u64); + return 2; + }; + wwiout = argv[i]; } else { if (cstreq(a, "-T")) { testmode = 1i32; } else { if (a[0u64] == 45u8) { @@ -45396,7 +45964,7 @@ export fn main(argc: i32, argv: **u8) i32 = { return 2; }; src = a; - }; }; }; + }; }; }; }; i += 1; }; @@ -45466,6 +46034,13 @@ export fn main(argc: i32, argv: **u8) i32 = { checkfile(&ck, f); if (ck.errs > 0) { return 1; }; + // M2 export-data: write the `.wwi` after a clean check, before cgen. + // Dead on the live path (no existing invocation passes -I); the + // producer's check_exported_type may reject a dangling export. + if (wwiout != nil) { + if (wwiemit(&ck, f, pathstr(wwiout)) != 0) { return 1; }; + }; + let cg: cgen; cgeninit(&cg); cgfile(&cg, f); diff --git a/selfhost/cmd/w6c/main.ww b/selfhost/cmd/w6c/main.ww index 60f0ca90..c7c474a9 100644 --- a/selfhost/cmd/w6c/main.ww +++ b/selfhost/cmd/w6c/main.ww @@ -23,6 +23,7 @@ import typ; import sym; import check; import cgen; +import wwi; fn cstreq(a: *u8, lit: str) bool = { let n: u64 = lit.len: u64; @@ -79,6 +80,7 @@ fn slurp(path: *u8) (*u8, u64) = { export fn main(argc: i32, argv: **u8) i32 = { let src: *u8 = nil; let out: *u8 = nil; + let wwiout: *u8 = nil; // -I : M2 export-data producer let testmode: i32 = 0i32; // #15: `-T` test-mode let i: i32 = 1; @@ -92,6 +94,14 @@ export fn main(argc: i32, argv: **u8) i32 = { return 2; }; out = argv[i]; + } else { if (cstreq(a, "-I")) { + i += 1; + if (i >= argc) { + let m: str = "w6c: -I requires arg\n"; + os.write(2, m.ptr, m.len: u64); + return 2; + }; + wwiout = argv[i]; } else { if (cstreq(a, "-T")) { testmode = 1i32; } else { if (a[0u64] == 45u8) { @@ -105,7 +115,7 @@ export fn main(argc: i32, argv: **u8) i32 = { return 2; }; src = a; - }; }; }; + }; }; }; }; i += 1; }; @@ -175,6 +185,13 @@ export fn main(argc: i32, argv: **u8) i32 = { checkfile(&ck, f); if (ck.errs > 0) { return 1; }; + // M2 export-data: write the `.wwi` after a clean check, before cgen. + // Dead on the live path (no existing invocation passes -I); the + // producer's check_exported_type may reject a dangling export. + if (wwiout != nil) { + if (wwiemit(&ck, f, pathstr(wwiout)) != 0) { return 1; }; + }; + let cg: cgen; cgeninit(&cg); cgfile(&cg, f); diff --git a/selfhost/cmd/wcc/wwi.ww b/selfhost/cmd/wcc/wwi.ww new file mode 100644 index 00000000..46d31de8 --- /dev/null +++ b/selfhost/cmd/wcc/wwi.ww @@ -0,0 +1,556 @@ +// wwi.ww — `.wwi` export-data producer (w6c_ww -I). M2 DEAD-CODE: writes +// a re-parseable ww-prototype rendering of a package's EXPORTED surface. +// Nothing consumes `.wwi` yet (combined.ww stays the live path); the only +// caller is the new -I flag, off on every existing invocation. +// +// wwstage twin of cmd/w6c/wwi.c — byte-identical output is a rule-10 +// requirement (`.wwi` is a new cross-stage byte-id substrate). Specs: +// .ai/rob-M2-spec.md + .ai/drew-M2-checkexported.md. +// +// - Unparse walks the AST type-expr subtree (the N_T* nodes), NOT the +// tinfo — tinfo collapses nominal pkg.Name identity. +// - check_exported_type rides the producer entry (flag-gated), so M2 +// stays dead on the normal `.s` path. It rejects exactly one thing: +// an exported signature naming a non-exported nominal type. +// - A `.wwi` is ONE package's interface; the M2 gate feeds a driver- +// combined unit, so the emit filters to PRIMARY decls (imported==0). + +// Imports mirror check.ww (os/tok/strconv only): node/nkind/sym/skind/ +// scopelookuptype/streq/checker/tkind resolve bare in the selfhost's flat +// bundled scope, exactly as check.ww references them. +package wcc; + +import os; +import tok; +import strconv; + +// --- byte writers ------------------------------------------------------ + +fn wputs(fd: i32, s: str) void = { + os.write(fd, s.ptr, s.len: u64); +}; + +fn wputb(fd: i32, b: u8) void = { + let buf: [1]u8; + buf[0] = b; + os.write(fd, buf.ptr, 1u64); +}; + +fn wquote(fd: i32, s: str) void = { + wputb(fd, '"'); + let i: i32 = 0; + for (i < s.len) { + let c: u8 = s[i]; + if (c == '"') { + wputs(fd, "\\\""); + } else { if (c == '\\') { + wputs(fd, "\\\\"); + } else { if (c == '\n') { + wputs(fd, "\\n"); + } else { if (c == '\t') { + wputs(fd, "\\t"); + } else { if (c < 32u8) { + let hi: u8 = c >> 4u8; + let lo: u8 = c & 15u8; + let h: u8 = 0u8; + let l: u8 = 0u8; + if (hi < 10u8) { h = hi + 48u8; } else { h = (hi - 10u8) + 97u8; }; + if (lo < 10u8) { l = lo + 48u8; } else { l = (lo - 10u8) + 97u8; }; + let buf: [4]u8; + buf[0] = 92u8; + buf[1] = 120u8; + buf[2] = h; + buf[3] = l; + os.write(fd, buf.ptr, 4u64); + } else { + wputb(fd, c); + };};};};}; + i += 1; + }; + wputb(fd, '"'); +}; + +// --- check_exported_type (drew) --------------------------------------- +// +// Resolve an N_TNAME to its type sym via the public sym helpers (mirror +// of cstage wwi_typesym / scope_lookup_type) WITHOUT the side effects of +// the checker's aliassym (no on-demand resolve, no double error). A +// primitive/keyword resolves to no SK_TYPE → leaf. By producer time +// checkfile has finished and c.cur == c.top. + +fn wwitypesym(c: *checker, nm: str) *sym = { + let empty: str; + let s: *sym = scopelookuptype(c.cur, empty, nm); + if (s == nil) { + let dotidx: i32 = -1; + let i: i32 = 0; + for (i < nm.len) { + if (nm[i] == 46u8) { dotidx = i; }; + i += 1; + }; + if (dotidx >= 0) { + let leaf: str; + leaf.ptr = nm.ptr + ((dotidx + 1): u64); + leaf.len = nm.len - dotidx - 1; + s = scopelookuptype(c.cur, empty, leaf); + }; + }; + if (s == nil) { return nil; }; + if (s.skind != skind.SK_TYPE) { return nil; }; + return s; +}; + +fn wwireject(d: *node, nm: str) void = { + wputs(2, d.file); + wputs(2, ":"); + wputs(2, strconv.u64tos(d.line: u64, strconv.base.DEC)); + wputs(2, ":"); + wputs(2, strconv.u64tos(d.col: u64, strconv.base.DEC)); + wputs(2, ": error: exported declaration references unexported type '"); + wputs(2, nm); + wputs(2, "'\n"); +}; + +// Returns 1 if a non-exported nominal was named (loud), else 0. +fn wwichecktype(c: *checker, d: *node, t: *node) i32 = { + if (t == nil) { return 0; }; + // rule-10: the wwstage checker wraps N_TTUPLE.list elements in + // N_TPARAM (ast.ww:101); cstage keeps the type-AST pristine. Unwrap + // transparently so the recursion sees the same shape cstage walks. + if (t.kind == nkind.N_TPARAM) { return wwichecktype(c, d, t.lhs); }; + let bad: i32 = 0; + if (t.kind == nkind.N_TNAME) { + let s: *sym = wwitypesym(c, t.str); + // sym.exported is vestigial (never set); the nominal's export + // status lives on its decl node, parser-set. + if (s != nil) { + if (s.decl != nil) { + if (s.decl.kind == nkind.N_TYPEDECL) { + // file.len == 0 ⇒ a checkinit-synthesized + // predeclared builtin (the ONLY empty-source + // decls: nomemdecl check.ww:126, the `void` + // TNAME check.ww:122), not a user nominal — ww's + // analogue of harec's STORAGE_NOMEM leaf-arm, and + // why cstage (lookup_builtin, no scope SK_TYPE) + // needs no such guard. + if (s.decl.file.len > 0) { + if (s.decl.exported == 0) { + wwireject(d, t.str); + bad = 1; + }; + }; + }; + }; + }; + } else { if ( + t.kind == nkind.N_TPTR || + t.kind == nkind.N_TSLICE || + t.kind == nkind.N_TBANG || + t.kind == nkind.N_TCHAN + ) { + bad = bad | wwichecktype(c, d, t.lhs); + } else { if (t.kind == nkind.N_TARRAY) { + // element only; the length is a const-expr, not a type. + bad = bad | wwichecktype(c, d, t.lhs); + } else { if (t.kind == nkind.N_TFN) { + let p: *node = t.list; + for (p != nil) { + bad = bad | wwichecktype(c, d, p.lhs); + p = p.next; + }; + bad = bad | wwichecktype(c, d, t.lhs); + } else { if (t.kind == nkind.N_TSTRUCT) { + let f: *node = t.list; + for (f != nil) { + bad = bad | wwichecktype(c, d, f.lhs); + f = f.next; + }; + } else { if (t.kind == nkind.N_TTAGGED || t.kind == nkind.N_TTUPLE) { + let e: *node = t.list; + for (e != nil) { + bad = bad | wwichecktype(c, d, e); + e = e.next; + }; + } else { if (t.kind == nkind.N_TENUM) { + // the inline enum body is the definition, not a reference; + // recurse only its storage type (members are values). + bad = bad | wwichecktype(c, d, t.lhs); + };};};};};};}; + return bad; +}; + +fn wwicheckdecl(c: *checker, d: *node) i32 = { + let bad: i32 = 0; + if (d.kind == nkind.N_FNDECL) { + let p: *node = d.list; + for (p != nil) { + bad = bad | wwichecktype(c, d, p.lhs); + p = p.next; + }; + bad = bad | wwichecktype(c, d, d.lhs); // ret + } else { if (d.kind == nkind.N_TYPEDECL) { + bad = bad | wwichecktype(c, d, d.lhs); + } else { if (d.kind == nkind.N_DEF) { + // the declared type; the rhs const value is not a type. + bad = bad | wwichecktype(c, d, d.lhs); + } else { if (d.kind == nkind.N_LET) { + bad = bad | wwichecktype(c, d, d.lhs); + };};};}; + return bad; +}; + +// --- type-expr + const-expr unparser (rob §2.2/§2.4) ------------------ + +fn wwiexpr(fd: i32, e: *node) void = { + if (e == nil) { return; }; + if (e.kind == nkind.N_INTLIT) { + wputs(fd, strconv.u64tos(e.uval, strconv.base.DEC)); + if (e.tsuffix.len > 0) { wputs(fd, e.tsuffix); }; + } else { if (e.kind == nkind.N_IDENT || e.kind == nkind.N_TNAME) { + wputs(fd, e.str); + } else { if (e.kind == nkind.N_DOT) { + wwiexpr(fd, e.lhs); + wputb(fd, '.'); + wputs(fd, e.str); + } else { if (e.kind == nkind.N_TRUE) { + wputs(fd, "true"); + } else { if (e.kind == nkind.N_FALSE) { + wputs(fd, "false"); + } else { if (e.kind == nkind.N_NIL) { + wputs(fd, "nil"); + } else { if (e.kind == nkind.N_STRLIT) { + wquote(fd, e.str); + } else { if (e.kind == nkind.N_BIN) { + wwiexpr(fd, e.lhs); + wputb(fd, 32u8); + wputs(fd, tokname(e.op)); + wputb(fd, 32u8); + wwiexpr(fd, e.rhs); + } else { if (e.kind == nkind.N_UN) { + wputs(fd, tokname(e.op)); + wwiexpr(fd, e.lhs); + } else { + wputs(2, "wwi: unhandled const-expr node kind\n"); + os.exit(1); + };};};};};};};};}; +}; + +fn wwiparam(fd: i32, p: *node) void = { + if (streq(p.str, "...")) { // C-style FFI `...` + wputs(fd, "..."); + return; + }; + if (p.str.len > 0) { + wputs(fd, p.str); + wputs(fd, ": "); + }; + // rule-10: the wwstage checker desugars a Hare variadic `T...` param + // in place to `[]T` (lhs becomes N_TSLICE); cstage leaves lhs == T. + // Peel the inserted slice so both stages emit the surface `T...`. + if (p.op == tkind.TK_ELLIPSIS && p.lhs != nil && p.lhs.kind == nkind.N_TSLICE) { + wwitype(fd, p.lhs.lhs); + } else { + wwitype(fd, p.lhs); + }; + if (p.op == tkind.TK_ELLIPSIS) { // Hare `T...` variadic + wputs(fd, "..."); + }; +}; + +fn wwitype(fd: i32, t: *node) void = { + if (t == nil) { // absent return type spells void + wputs(fd, "void"); + return; + }; + // rule-10: unwrap the wwstage-only N_TPARAM tuple-element wrapper + // (ast.ww:101) so the unparse matches cstage's pristine type-AST. + if (t.kind == nkind.N_TPARAM) { wwitype(fd, t.lhs); return; }; + if (t.kind == nkind.N_TNAME) { + if (t.str.len > 0) { wputs(fd, t.str); } else { wputs(fd, "void"); }; + } else { if (t.kind == nkind.N_TPTR) { + wputb(fd, '*'); + wwitype(fd, t.lhs); + } else { if (t.kind == nkind.N_TSLICE) { + wputs(fd, "[]"); + wwitype(fd, t.lhs); + } else { if (t.kind == nkind.N_TARRAY) { + wputb(fd, '['); + if (t.rhs != nil) { wwiexpr(fd, t.rhs); } else { wputb(fd, '_'); }; + wputb(fd, ']'); + wwitype(fd, t.lhs); + } else { if (t.kind == nkind.N_TBANG) { + wputb(fd, '!'); + wwitype(fd, t.lhs); + } else { if (t.kind == nkind.N_TCHAN) { + wputs(fd, "chan "); + wwitype(fd, t.lhs); + } else { if (t.kind == nkind.N_TFN) { + wputs(fd, "fn("); + let p: *node = t.list; + for (p != nil) { + if (p != t.list) { wputs(fd, ", "); }; + wwiparam(fd, p); + p = p.next; + }; + wputs(fd, ") "); + wwitype(fd, t.lhs); + } else { if (t.kind == nkind.N_TSTRUCT) { + wputs(fd, "struct { "); + let f: *node = t.list; + for (f != nil) { + if (f != t.list) { wputs(fd, ", "); }; + if (f.str.len > 0) { + wputs(fd, f.str); + wputs(fd, ": "); + }; + wwitype(fd, f.lhs); + f = f.next; + }; + wputs(fd, " }"); + } else { if (t.kind == nkind.N_TTUPLE) { + wputb(fd, '('); + let e: *node = t.list; + for (e != nil) { + if (e != t.list) { wputs(fd, ", "); }; + wwitype(fd, e); + e = e.next; + }; + wputb(fd, ')'); + } else { if (t.kind == nkind.N_TTAGGED) { + wputb(fd, '('); + let e: *node = t.list; + for (e != nil) { + if (e != t.list) { wputs(fd, " | "); }; + wwitype(fd, e); + e = e.next; + }; + wputb(fd, ')'); + } else { if (t.kind == nkind.N_TENUM) { + wputs(fd, "enum "); + if (t.lhs != nil) { + wwitype(fd, t.lhs); + wputb(fd, 32u8); + }; + wputs(fd, "{ "); + let m: *node = t.list; + for (m != nil) { + if (m != t.list) { wputs(fd, ", "); }; + wputs(fd, m.str); + if (m.lhs != nil) { + wputs(fd, " = "); + wwiexpr(fd, m.lhs); + }; + m = m.next; + }; + wputs(fd, " }"); + } else { + wputs(2, "wwi: unhandled type node kind\n"); + os.exit(1); + };};};};};};};};};};}; +}; + +fn wwidecl(fd: i32, d: *node) void = { + if (d.kind == nkind.N_FNDECL) { + wputs(fd, "export fn "); + wputs(fd, d.str); + wputb(fd, '('); + let p: *node = d.list; + for (p != nil) { + if (p != d.list) { wputs(fd, ", "); }; + wwiparam(fd, p); + p = p.next; + }; + wputs(fd, ") "); + wwitype(fd, d.lhs); + wputs(fd, ";\n"); + } else { if (d.kind == nkind.N_TYPEDECL) { + wputs(fd, "export type "); + wputs(fd, d.str); + wputs(fd, " = "); + wwitype(fd, d.lhs); + wputs(fd, ";\n"); + } else { if (d.kind == nkind.N_DEF) { + wputs(fd, "export def "); + wputs(fd, d.str); + wputs(fd, ": "); + wwitype(fd, d.lhs); + wputs(fd, " = "); + wwiexpr(fd, d.rhs); + wputs(fd, ";\n"); + } else { if (d.kind == nkind.N_LET) { + wputs(fd, "export let "); + wputs(fd, d.str); + wputs(fd, ": "); + if (d.lhs == nil) { + wputs(2, "wwi: exported let has no declared type\n"); + os.exit(1); + }; + wwitype(fd, d.lhs); + wputs(fd, ";\n"); + };};};}; +}; + +// --- deterministic ordering (rob §3) ---------------------------------- + +fn wwiprimary(n: *node) bool = { + // imported==1 marks a decl reached through a `//ww:module ` + // boundary (an imported module's concatenated section). + if (n == nil) { return false; }; + return n.imported == 0; +}; + +fn wwiisdecl(d: *node) bool = { + return d.kind == nkind.N_FNDECL || d.kind == nkind.N_TYPEDECL || + d.kind == nkind.N_DEF || d.kind == nkind.N_LET; +}; + +// strcmp — byte lexicographic, mirror C strcmp sign (<0/0/>0). Both +// stages key the sort identically, so the `.wwi` order is deterministic. +fn wwistrcmp(a: str, b: str) i32 = { + let i: i32 = 0; + for (i < a.len && i < b.len) { + let ca: i32 = a[i]: i32; + let cb: i32 = b[i]: i32; + if (ca != cb) { return ca - cb; }; + i += 1; + }; + return a.len - b.len; +}; + +// Selection sort over parallel (key, node) arrays. Total order keyed on +// the symbol name; ties broken by original index — so the result is +// stable regardless of any same-name collision, matching cstage's qsort +// + idx tiebreak. +fn wwisortdecls(keys: []str, nodes: []*node, n: i32) void = { + let i: i32 = 0; + for (i < n) { + let best: i32 = i; + let j: i32 = i + 1; + for (j < n) { + let r: i32 = wwistrcmp(keys[j], keys[best]); + if (r < 0) { best = j; }; + j += 1; + }; + if (best != i) { + let tk: str = keys[i]; keys[i] = keys[best]; keys[best] = tk; + let tn: *node = nodes[i]; nodes[i] = nodes[best]; nodes[best] = tn; + }; + i += 1; + }; +}; + +export fn wwiemit(c: *checker, file: *node, path: str) i32 = { + // §5: check_exported_type FIRST, before any byte — a producer + // without it can emit a dangling `.wwi`. + let bad: i32 = 0; + let d: *node = file.list; + for (d != nil) { + if (wwiprimary(d) && d.exported != 0 && wwiisdecl(d)) { + bad = bad | wwicheckdecl(c, d); + }; + d = d.next; + }; + if (bad != 0) { return 1i32; }; + + let fd: i32 = os.open(path, + os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 + if (fd < 0) { + wputs(2, "w6c: cannot open "); + wputs(2, path); + wputs(2, "\n"); + return 1i32; + }; + + // package line: leaf of the first primary decl's module tag. + let pkg: str = "main"; + let pd: *node = file.list; + for (pd != nil) { + if (wwiprimary(pd) && pd.nmod.len > 0) { + let dotidx: i32 = -1; + let i: i32 = 0; + for (i < pd.nmod.len) { + if (pd.nmod[i] == 46u8) { dotidx = i; }; + i += 1; + }; + if (dotidx >= 0) { + let leaf: str; + leaf.ptr = pd.nmod.ptr + ((dotidx + 1): u64); + leaf.len = pd.nmod.len - dotidx - 1; + pkg = leaf; + } else { + pkg = pd.nmod; + }; + pd = nil; + } else { + pd = pd.next; + }; + }; + wputs(fd, "package "); + wputs(fd, pkg); + wputs(fd, ";\n"); + + // imports — primary N_USE, byte-sorted by import path. + let nuse: i32 = 0; + let u: *node = file.list; + for (u != nil) { + if (u.kind == nkind.N_USE && wwiprimary(u)) { nuse += 1; }; + u = u.next; + }; + if (nuse > 0) { + let upaths: []str = alloc([], nuse: u64)!; + upaths.len = nuse; + let unodes: []*node = alloc([], nuse: u64)!; + unodes.len = nuse; + let k: i32 = 0; + u = file.list; + for (u != nil) { + if (u.kind == nkind.N_USE && wwiprimary(u)) { + if (u.usepath.len > 0) { upaths[k] = u.usepath; } else { upaths[k] = u.str; }; + unodes[k] = u; + k += 1; + }; + u = u.next; + }; + wwisortdecls(upaths, unodes, nuse); + let i: i32 = 0; + for (i < nuse) { + wputs(fd, "import "); + wputs(fd, upaths[i]); + wputs(fd, ";\n"); + i += 1; + }; + }; + + // decls — exported primary, byte-sorted by symbol name. + let ndecl: i32 = 0; + d = file.list; + for (d != nil) { + if (wwiprimary(d) && d.exported != 0 && wwiisdecl(d)) { ndecl += 1; }; + d = d.next; + }; + if (ndecl > 0) { + let dkeys: []str = alloc([], ndecl: u64)!; + dkeys.len = ndecl; + let dnodes: []*node = alloc([], ndecl: u64)!; + dnodes.len = ndecl; + let k: i32 = 0; + d = file.list; + for (d != nil) { + if (wwiprimary(d) && d.exported != 0 && wwiisdecl(d)) { + dkeys[k] = d.str; + dnodes[k] = d; + k += 1; + }; + d = d.next; + }; + wwisortdecls(dkeys, dnodes, ndecl); + let i: i32 = 0; + for (i < ndecl) { + wwidecl(fd, dnodes[i]); + i += 1; + }; + }; + + os.close(fd); + return 0i32; +}; diff --git a/test/wcc/989_m2wwi_run.c b/test/wcc/989_m2wwi_run.c new file mode 100644 index 00000000..7171c8e7 --- /dev/null +++ b/test/wcc/989_m2wwi_run.c @@ -0,0 +1,330 @@ +/* + * 989_m2wwi_run — M2 `.wwi` export-data producer gate (task #22 arc). + * Both stages (rule-10): the new `w6c -I ` producer is DEAD on + * the live path (combined.ww stays the compile substrate; nothing reads + * `.wwi`), so this is purely a new cross-stage byte-id substrate. + * + * POSITIVE gate — for each package (ascii/strings/getopt, drew2-audited + * leak-free), drive the target as the PRIMARY module of a driver-combined + * unit, then: + * 1. w6c -I cs.wwi and w6c_ww -I ww.wwi both succeed, + * 2. cs.wwi == ww.wwi byte-for-byte (the new substrate's determinism), + * 3. cs.wwi RE-PARSES under the existing parser (wwdump -a exits 0) — + * proves the producer emits genuinely re-parseable ww prototype + * source, de-risking the M3 consumer. + * getopt is the recursion stressor (enum→struct→struct, ptr-to-nominal, + * tagged-union return, qualified pkg.Name). A synth fixture then covers the + * decl-kinds + type-nodes no lib package reaches: `def` (const-expr unparser + * + fold parity), `let` global, `[N]T` array, `fn(..)R` fn-ptr, `!T`, tuple, + * storage-less enum. + * + * NEGATIVE gate — a fixture whose exported fn names a PRIVATE (non- + * exported) nominal must be LOUD-REJECTED by check_exported_type, + * identically on BOTH stages (same non-zero exit, byte-identical + * diagnostic). Without it a vacuous no-op check would pass the positive + * gate silently. + * + * Compile/produce/cmp only — no driver run, everything under a getpid- + * keyed /tmp dir (no source-tree writes), so this is phase-1 parallel- + * safe. 9xx is full; shares the 989 prefix per the 989_lib_byteid + * precedent (the `short` name keys the binary). + */ +#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; +} + +/* one positive package: build the target as primary, produce the `.wwi` + * on both stages, require byte-id + re-parse. Returns 0 on pass. */ +static int +positive(const char *bin, const char *cwd, const char *pkg, int idx) +{ + char td[64], cmd[4096], src[1024]; + int rc = -1; + + snprintf(td, sizeof td, "/tmp/wwm2_%d_%d", getpid(), idx); + snprintf(cmd, sizeof cmd, "rm -rf %s", td); + runwait(cmd); + mkdir(td, 0755); + + snprintf(src, sizeof src, "%s/lib/%s/%s.ww", cwd, pkg, pkg); + snprintf(cmd, sizeof cmd, "cp %s %s/%s.ww", src, td, pkg); + if (runwait(cmd) != 0) { + fprintf(stderr, "m2wwi FAIL: %s — cp\n", pkg); + goto out; + } + + /* Drive the package file as the build target → its decls are the + * PRIMARY module (imported==0) of the resolved unit; the producer's + * primary filter then yields exactly this package's interface. The + * library has no main, so the build's link step fails — the + * `.combined.ww` is written before codegen (cf 989_lib_byteid), and + * only it matters here, so the exit code is deliberately ignored. */ + snprintf(cmd, sizeof cmd, + "cd %s && timeout 180 %s/ww build -I %s/lib %s.ww >/dev/null 2>&1", + td, bin, cwd, pkg); + runwait(cmd); + + char comb[1100], cs[1100], ws[1100]; + snprintf(comb, sizeof comb, "%s/%s.combined.ww", td, pkg); + snprintf(cs, sizeof cs, "%s/cs.wwi", td); + snprintf(ws, sizeof ws, "%s/ww.wwi", td); + if (access(comb, 0) != 0) { + fprintf(stderr, "m2wwi FAIL: %s — no resolved unit\n", pkg); + goto out; + } + + snprintf(cmd, sizeof cmd, + "timeout 180 %s/w6c -I %s %s >/dev/null 2>&1", bin, cs, comb); + if (runwait(cmd) != 0) { + fprintf(stderr, "m2wwi FAIL: %s — w6c -I rejected\n", pkg); + goto out; + } + snprintf(cmd, sizeof cmd, + "timeout 180 %s/w6c_ww -I %s %s >/dev/null 2>&1", bin, ws, comb); + if (runwait(cmd) != 0) { + fprintf(stderr, "m2wwi FAIL: %s — w6c_ww -I rejected\n", pkg); + goto out; + } + + if (files_eq(cs, ws) != 0) { + fprintf(stderr, "m2wwi FAIL: %s — cs.wwi != ww.wwi (byte-id " + "broken on the .wwi substrate)\n", pkg); + goto out; + } + + /* re-parse: the emitted `.wwi` must be valid ww prototype source. */ + snprintf(cmd, sizeof cmd, + "timeout 180 %s/wwdump -a %s >/dev/null 2>&1", bin, cs); + if (runwait(cmd) != 0) { + fprintf(stderr, "m2wwi FAIL: %s — emitted .wwi does not " + "re-parse (wwdump -a)\n", pkg); + goto out; + } + rc = 0; +out: + snprintf(cmd, sizeof cmd, "rm -rf %s", td); + runwait(cmd); + return rc; +} + +/* negative gate: an exported fn naming a private nominal must be rejected + * identically (exit + diagnostic) by BOTH stages. Returns 0 on pass. */ +static const char *leak_src = + "package leaktest;\n" + "type secret = struct { x: i32 };\n" + "export fn leaks(s: secret) i32 = { return s.x; };\n" + "export fn clean(a: i32) i32 = { return a; };\n"; + +static int +negative(const char *bin) +{ + char td[64], cmd[4096], p[512]; + int rc = -1; + + snprintf(td, sizeof td, "/tmp/wwm2neg_%d", getpid()); + snprintf(cmd, sizeof cmd, "rm -rf %s", td); + runwait(cmd); + mkdir(td, 0755); + + snprintf(p, sizeof p, "%s/leak.ww", td); + FILE *f = fopen(p, "wb"); + if (!f) goto out; + fputs(leak_src, f); + fclose(f); + + char cse[1100], wwe[1100]; + snprintf(cse, sizeof cse, "%s/cs.err", td); + snprintf(wwe, sizeof wwe, "%s/ww.err", td); + + /* cd into td so both stages report the bare `leak.ww:L:C:` prefix. */ + snprintf(cmd, sizeof cmd, + "cd %s && timeout 180 %s/w6c -I out.wwi leak.ww >/dev/null 2>cs.err", + td, bin); + int crc = runwait(cmd); + snprintf(cmd, sizeof cmd, + "cd %s && timeout 180 %s/w6c_ww -I out.wwi leak.ww >/dev/null 2>ww.err", + td, bin); + int wrc = runwait(cmd); + + if (crc == 0 || wrc == 0) { + fprintf(stderr, "m2wwi FAIL: negative — check_exported_type is " + "vacuous (cstage exit=%d wwstage exit=%d; both must reject " + "the private-type leak)\n", crc, wrc); + goto out; + } + if (files_eq(cse, wwe) != 0) { + fprintf(stderr, "m2wwi FAIL: negative — reject diagnostics " + "differ across stages (rule-10)\n"); + goto out; + } + rc = 0; +out: + snprintf(cmd, sizeof cmd, "rm -rf %s", td); + runwait(cmd); + return rc; +} + +/* synth gate: a single self-contained package exercising the decl-kinds + + * type-AST nodes the lib packages above do NOT cover — `export def` (the + * const-expr unparser, incl a folded `10+2*3` and a unary `-7`), `export + * let` (global), `[N]T` array (decl + array-dim ident-ref), an `fn(...) R` + * fn-pointer type, an `!T` error type, a `(a, b)` tuple return, and a + * storage-less `enum {...}`. Asserts both stages produce a byte-identical, + * re-parseable `.wwi` (the const-expr fold + tokname spelling must agree + * cross-stage — paths the lib gate never reaches). `chan T` is omitted: the + * wwstage parser does not yet accept it in a return position (pre-existing, + * unrelated to M2), so the producer's N_TCHAN arm is unreachable there. + * Returns 0 on pass. */ +static const char *synth_src = + "package synth;\n" + "export type color = enum { RED, GREEN = 5, BLUE };\n" + "export def LIMIT: i32 = 10 + 2 * 3;\n" + "export def NAME: str = \"hi\\n\";\n" + "export def FLAG: bool = true;\n" + "export def NEG: i32 = -7;\n" + "export let counter: i32;\n" + "export let grid: [4]i32;\n" + "export fn apply(f: fn(x: i32) i32, n: i32) i32;\n" + "export fn risky() !i32;\n" + "export fn matrix() [LIMIT]u8;\n" + "export fn pair() (i32, i32);\n" + "export fn opt(p: *color, b: []u8) (i32 | void);\n"; + +static int +synth(const char *bin) +{ + char td[64], cmd[4096], p[512]; + int rc = -1; + + snprintf(td, sizeof td, "/tmp/wwm2syn_%d", getpid()); + snprintf(cmd, sizeof cmd, "rm -rf %s", td); + runwait(cmd); + mkdir(td, 0755); + + snprintf(p, sizeof p, "%s/synth.ww", td); + FILE *f = fopen(p, "wb"); + if (!f) goto out; + fputs(synth_src, f); + fclose(f); + + char cs[1100], ws[1100]; + snprintf(cs, sizeof cs, "%s/cs.wwi", td); + snprintf(ws, sizeof ws, "%s/ww.wwi", td); + + /* self-contained package (no imports) → it is its own primary unit, + * so w6c -I runs directly on the source (no driver-combined step). */ + snprintf(cmd, sizeof cmd, + "timeout 180 %s/w6c -I %s %s/synth.ww >/dev/null 2>&1", bin, cs, td); + if (runwait(cmd) != 0) { + fprintf(stderr, "m2wwi FAIL: synth — w6c -I rejected\n"); + goto out; + } + snprintf(cmd, sizeof cmd, + "timeout 180 %s/w6c_ww -I %s %s/synth.ww >/dev/null 2>&1", bin, ws, td); + if (runwait(cmd) != 0) { + fprintf(stderr, "m2wwi FAIL: synth — w6c_ww -I rejected\n"); + goto out; + } + if (files_eq(cs, ws) != 0) { + fprintf(stderr, "m2wwi FAIL: synth — cs.wwi != ww.wwi (const-expr " + "/ decl-kind unparse diverges across stages)\n"); + goto out; + } + snprintf(cmd, sizeof cmd, + "timeout 180 %s/wwdump -a %s >/dev/null 2>&1", bin, cs); + if (runwait(cmd) != 0) { + fprintf(stderr, "m2wwi FAIL: synth — emitted .wwi does not " + "re-parse (wwdump -a)\n"); + goto out; + } + rc = 0; +out: + snprintf(cmd, sizeof cmd, "rm -rf %s", td); + runwait(cmd); + return rc; +} + +int +main(void) +{ + const char *bin = absbin(); + if (!bin) return 1; + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + + const char *pkgs[] = { "ascii", "strings", "getopt", NULL }; + int fail = 0, npos = 0; + for (int i = 0; pkgs[i]; i++) { + if (positive(bin, cwd, pkgs[i], i) != 0) fail++; + else npos++; + } + if (synth(bin) != 0) fail++; + else npos++; + if (negative(bin) != 0) fail++; + + if (fail) { + fprintf(stderr, "m2wwi: %d check(s) failed\n", fail); + return 1; + } + printf("m2wwi: %d package(s) byte-identical + re-parse; negative " + "gate rejects (both stages)\n", npos); + return 0; +}