/* * wwi.c — `.wwi` export-data producer (w6c -I): a re-parseable ww-prototype * rendering of a package's EXPORTED surface. Since the sep-compile flip * (epic #22) this is the LIVE import path — the driver runs one `w6c -c -I` * per package and feeds each dep's `.wwi` to its importers as import scope; * the combined.ww amalgamator is gone. * * 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_rune(FILE *of, u64 cp) { /* #50: the lexer now reads \u/\U, so wide codepoints round-trip as * those escapes (write-twin of lex.c lexunicode). Codepoints <=0xff * keep the \xHH spelling they already round-tripped as. */ if (cp > 0xffff) { fprintf(of, "'\\U%08x'", (unsigned)cp); return; } if (cp > 0xff) { fprintf(of, "'\\u%04x'", (unsigned)cp); return; } 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: /* re-emit the `@packed` attr so the flag round-trips * through sep-compile (harec unparse/type.ha:122-126). */ fputs(t->packed ? "struct @packed { " : "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); /* #95: re-emit the spread marker purely syntactically; * the consumer's type-store flattens (ref/hare/hare/ * unparse/type.ha:290-300). */ if (e->op == TK_ELLIPSIS) 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"; int found = 0; 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; found = 1; break; } } /* #11: a decl-less / export-less primary body carries no * module-tagged decl, so the scan above finds nothing; fall back to * the primary module identity stamped on the N_FILE node at parse * time. A real root `package main` arrives via a bare module-reset * and leaves file->module NULL, so it stays "main". The detector is * scan-miss (`!found`), NOT pkg=="main": a body whose first tagged * decl legitimately leafs to "main" must keep that, and must match * selfhost's `found` flag byte-for-byte (rule 10). */ if (!found && file->module && file->module[0]) { const char *dot = strrchr(file->module, '.'); pkg = dot ? dot + 1 : file->module; } 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; }