wcc/ww: .wwi export-data producer + check_exported_type (#22 M2)
New `w6c -I <out.wwi>` 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).
This commit is contained in:
@@ -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*);
|
||||
|
||||
@@ -33,11 +33,14 @@ main(int argc, char **argv)
|
||||
{
|
||||
const char *src = NULL;
|
||||
const char *out = NULL;
|
||||
const char *wwiout = NULL; /* -I <out.wwi>: 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");
|
||||
|
||||
457
cmd/w6c/wwi.c
Normal file
457
cmd/w6c/wwi.c
Normal file
@@ -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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static int
|
||||
wwi_primary(Node *n)
|
||||
{
|
||||
/* imported == 1 marks a decl reached through a `//ww:module <path>`
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user