Files
ww/cmd/w6c/wwi.c
Hojun-Cho 10d005ef58 wcc/ww: serialize aggregate exported defs as value-less .wwi prototypes (BUG-2, #70)
The .wwi (separate-compile interface) producer could not serialize an
exported def whose initializer is a struct/array literal (N_STRUCTLIT/
N_ARRLIT) — `export def f64info: floatinfo = floatinfo{...}` aborted with
"unhandled const-expr node kind 15". Such a def is a DATA-global per the
#52 model, so its value lives once in the defining package's .o; the
interface needs only the type+symbol. Emit a value-less prototype
`export def X: T;` for aggregate-initializer defs; scalar fold-eligible
defs keep their value (the importer const-folds those). The parser gains
an optional-init arm so the importer can parse the prototype — value-less
`def X: T;` is now legal in any source, symmetric with the existing
bodyless-fn prototype `fn f();` (USER ruling: unconditional; a value-less
def with no defining .o is a loud undefined-symbol error at link, never
silent). Both stages; producer + parser fold into one commit (the
producer's output is unparseable without the parser arm).

M3-tail commit-6 prerequisite #2 (surfaced by the c6 scout). The
aggregate-def-field const-fold boundary is documented inline (#71). Gate
989_sepstructdef_run proves struct+array exported defs sep-build, link,
and run via external DATA refs, cs==ww, with a value-less .wwi.
2026-06-16 18:04:54 +09:00

537 lines
14 KiB
C

/*
* 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_rune(FILE *of, u64 cp)
{
/* The lexer's rune escapes stop at \xHH (no \u/\U — task #50), so a
* codepoint above 0xff can't render as a re-parseable rune literal;
* fail loud rather than emit a malformed one. No exported def names
* such a rune today (RUNE_MAX is written as an int-cast for the same
* reason). */
if (cp > 0xff)
fatal("wwi: rune codepoint U+%llx exceeds \\xHH (task #50)",
(unsigned long long)cp);
unsigned char c = (unsigned char)cp;
fputc('\'', of);
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 || c >= 0x7f) 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;
/* A bare rune literal in a const-expr (types::RUNE_MIN = '\0'); casts
* never reach here — the checker folds a const cast to an integer
* literal before the producer runs (RUNE_MAX `0x10ffff: rune` emits
* as the plain int 1114111). */
case N_RUNELIT: wwi_rune(of, e->uval); 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);
}
}
/* Only codegen/link-relevant attributes round-trip into the `.wwi`. Today
* that is exactly @symbol (the FFI link-symbol override, read back at cgen
* fficollect — dropping it makes sep-compile emit `CALL malloc` for a
* `@symbol("rt_malloc")` fn). Named for the class so @align/@offset would
* slot in here IF ww ever grows field-layout attributes — it has none
* today (task #47 report). @test never reaches a `.wwi` (test fns are not
* export-marked), so it needs no exclusion arm. */
static int
wwi_attr_relevant(const char *nm)
{
return nm && strcmp(nm, "symbol") == 0;
}
static void
wwi_attrs(FILE *of, Node *d)
{
for (Node *a = d->attr; a; a = a->next) {
if (a->kind != N_ATTR || !wwi_attr_relevant(a->str))
continue;
fputc('@', of);
fputs(a->str, of);
if (a->list) {
fputc('(', of);
for (Node *arg = a->list; arg; arg = arg->next) {
if (arg != a->list) fputs(", ", of);
wwi_expr(of, arg);
}
fputc(')', of);
}
fputc(' ', of);
}
}
static void
wwi_decl(FILE *of, Node *d)
{
switch (d->kind) {
case N_FNDECL:
wwi_attrs(of, d);
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);
/* An aggregate initializer (N_STRUCTLIT/N_ARRLIT) is a DATA-
* global (#52): emit a value-LESS prototype `export def X: T;`.
* The defining package's own .o emits the struct/array DATA; the
* importer registers the def by TYPE only (the DATA-suppression
* if(sep_mode && imported)continue at cgen.c is already in place)
* and field/element reads become external LEAQ refs the linker
* fills. Boundary (rule 7): this gives up importer-side const-
* fold of an aggregate def's FIELDS — a no-op, ww never folds
* struct-literal field access, and an aggregate-def field
* demanded in a const-fold context stays a LOUD error (task #71). */
if (d->rhs && (d->rhs->kind == N_STRUCTLIT
|| d->rhs->kind == N_ARRLIT)) {
fputs(";\n", of);
} else {
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;
}