diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 34d32823..1a184429 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -1172,6 +1172,16 @@ ffi_resolve(const char *ident) return ident; } +/* Package compilation must not acquire a hidden source dependency on a + * transitive `rt` interface merely so the compiler intrinsic `alloc` learns + * its linker spelling. The WW runtime ABI owns this name in -c mode. Keep + * raw w6c's declaration-driven behavior for compatibility and FFI tests. */ +static const char * +alloc_resolve(Cg *c) +{ + return c->sep_mode ? "rt_malloc" : ffi_resolve("malloc"); +} + static void ffi_collect(Cg *c, Node *file) { @@ -9199,7 +9209,7 @@ cgexpr(Cg *c, Node *n, Local *locals) char *alloc_ok = mklabel(c, "alloc_ok"); char *alloc_done = mklabel(c, "alloc_done"); ins2(c, A_MOVQ, aimm(sz), areg(D_DI)); - ins1(c, A_CALL, asym(ffi_resolve("malloc"))); + ins1(c, A_CALL, asym(alloc_resolve(c))); ins2(c, A_CMPQ, aimm(0), areg(D_AX)); ins1(c, A_JNE, abranch(alloc_ok)); ins2(c, A_MOVQ, aimm(1), areg(D_AX)); @@ -13536,7 +13546,7 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) ins2(c, A_IMULQ, areg(D_BX), areg(D_AX)); } ins2(c, A_MOVQ, areg(D_AX), areg(D_DI)); - ins1(c, A_CALL, asym(ffi_resolve("malloc"))); + ins1(c, A_CALL, asym(alloc_resolve(c))); if (via_tryunw) { char *ok = mklabel(c, "tryunw_ok"); ins2(c, A_CMPQ, aimm(0), areg(D_AX)); diff --git a/cmd/w6c/wwi.c b/cmd/w6c/wwi.c index 148ca053..3c14bbf0 100644 --- a/cmd/w6c/wwi.c +++ b/cmd/w6c/wwi.c @@ -15,12 +15,13 @@ * 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). + * A `.wwi` is ONE package's self-contained interface. The primary section + * is followed by compiler-owned `//ww:module ` sections for exported + * foreign type/const facts recursively needed by the public surface. The + * package driver therefore only needs to pass a package's DIRECT imports; + * the interface itself carries the selected public type closure. Output is + * a pure function of the exported API: byte-sorted imports, declarations, + * fact owners and fact names (no map/hash iteration order). */ #include "gc.h" #include @@ -41,24 +42,78 @@ wwi_primary(Node *n) * leaf. By the time the producer runs, check_file has finished and * c->cur == c->top. */ +static int +wwi_mod_eq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) return a == b; + return strcmp(a, b) == 0; +} + +/* The import alias map is source-owner-local. Imported `.wwi` sections are + * flat in the parser, so consulting every N_USE without this owner filter + * would let one dependency accidentally resolve another dependency's alias. */ +static const char * +wwi_use_path(Checker *c, const char *owner, const char *alias) +{ + if (owner && alias) { + const char *dot = strrchr(owner, '.'); + const char *leaf = dot ? dot + 1 : owner; + if (strcmp(alias, leaf) == 0) + return owner; + } + for (Node *u = c->file->list; u; u = u->next) { + if (u->kind != N_USE || u->str == NULL + || strcmp(u->str, alias) != 0) + continue; + int same = owner == NULL ? u->imported == 0 + : u->imported != 0 && wwi_mod_eq(u->module, owner); + if (same) + return u->usepath ? u->usepath : u->str; + } + return NULL; +} + +static int +wwi_direct_mod_visible(Checker *c, const char *owner, const char *mod) +{ + if (mod == NULL || mod[0] == '\0') return 0; + const char *dot = strrchr(mod, '.'); + const char *alias = dot ? dot + 1 : mod; + const char *path = wwi_use_path(c, owner, alias); + return path != NULL && strcmp(path, mod) == 0; +} + static Sym * -wwi_typesym(Checker *c, const char *nm) +wwi_typesym(Checker *c, const char *owner, const char *nm) { if (nm == NULL) return NULL; const char *dot = strrchr(nm, '.'); Sym *s = NULL; if (dot) { size_t n = (size_t)(dot - nm); - for (Node *u = c->file->list; u; u = u->next) { - if (u->kind != N_USE || !wwi_primary(u) || u->str == NULL - || strlen(u->str) != n || strncmp(u->str, nm, n) != 0) - continue; - const char *mod = u->usepath ? u->usepath : u->str; + char *alias = malloc(n + 1); + if (alias == NULL) fatal("wwi: out of memory"); + memcpy(alias, nm, n); + alias[n] = '\0'; + const char *mod = wwi_use_path(c, owner, alias); + if (mod) s = scope_lookup_in_module(c->cur, mod, dot + 1); - break; - } + free(alias); } else { - s = scope_lookup_type(c->cur, NULL, nm); + s = scope_lookup_type(c->cur, owner, nm); + if (s == NULL) { + for (Scope *p = c->cur; p; p = p->parent) { + for (Sym *b = p->first; b; b = b->next) { + if (b->kind == SK_TYPE + && strcmp(b->name, nm) == 0 + && wwi_direct_mod_visible(c, owner, b->mod)) { + s = b; + break; + } + } + if (s != NULL) break; + } + } } if (s && s->kind == SK_TYPE) return s; @@ -66,14 +121,14 @@ wwi_typesym(Checker *c, const char *nm) } static int -wwi_check_type(Checker *c, Pos loc, Node *t) +wwi_check_type(Checker *c, const char *owner, 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 *s = wwi_typesym(c, owner, 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 @@ -92,30 +147,30 @@ wwi_check_type(Checker *c, Pos loc, Node *t) case N_TSLICE: case N_TBANG: case N_TCHAN: - bad |= wwi_check_type(c, loc, t->lhs); + bad |= wwi_check_type(c, owner, 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); + bad |= wwi_check_type(c, owner, 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); + bad |= wwi_check_type(c, owner, loc, p->lhs); + bad |= wwi_check_type(c, owner, loc, t->lhs); break; case N_TSTRUCT: for (Node *f = t->list; f; f = f->next) - bad |= wwi_check_type(c, loc, f->lhs); + bad |= wwi_check_type(c, owner, 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); + bad |= wwi_check_type(c, owner, 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); + bad |= wwi_check_type(c, owner, loc, t->lhs); break; default: /* a non-type node in type position should not occur — leaf. */ @@ -131,18 +186,18 @@ wwi_check_decl(Checker *c, Node *d) 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 */ + bad |= wwi_check_type(c, NULL, d->pos, p->lhs); + bad |= wwi_check_type(c, NULL, d->pos, d->lhs); /* ret */ break; case N_TYPEDECL: - bad |= wwi_check_type(c, d->pos, d->lhs); + bad |= wwi_check_type(c, NULL, 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); + bad |= wwi_check_type(c, NULL, d->pos, d->lhs); break; case N_LET: - bad |= wwi_check_type(c, d->pos, d->lhs); + bad |= wwi_check_type(c, NULL, d->pos, d->lhs); break; default: break; @@ -357,6 +412,13 @@ wwi_expr(FILE *of, Node *e) fputs(tokname(e->op), of); wwi_expr(of, e->lhs); break; + case N_CAST: + fputc('(', of); + wwi_expr(of, e->lhs); + fputs(": ", of); + wwi_type(of, e->rhs); + fputc(')', of); + break; default: fatal("wwi: unhandled const-expr node kind %d", e->kind); } @@ -434,7 +496,7 @@ wwi_decl(FILE *of, Node *d) * 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 + if (d->rhs == NULL || (d->rhs->kind == N_STRUCTLIT || d->rhs->kind == N_ARRLIT)) { fputs(";\n", of); } else { @@ -462,6 +524,16 @@ wwi_decl(FILE *of, Node *d) struct declent { Node *d; int idx; }; struct useent { const char *path; int idx; }; +struct factent { Node *d; const char *mod; int idx; }; + +struct factset { + Checker *c; + struct factent *facts; + int nfacts, capfacts; + struct factent *seen; + int nseen, capseen; + int bad; +}; static int declcmp(const void *a, const void *b) @@ -481,6 +553,20 @@ usecmp(const void *a, const void *b) return x->idx - y->idx; } +static int +factcmp(const void *a, const void *b) +{ + const struct factent *x = a, *y = b; + int r = strcmp(x->mod, y->mod); + if (r != 0) return r; + int xr = x->d->kind == N_TYPEDECL ? 0 : 1; + int yr = y->d->kind == N_TYPEDECL ? 0 : 1; + if (xr != yr) return xr - yr; + r = strcmp(x->d->str, y->d->str); + if (r != 0) return r; + return x->idx - y->idx; +} + static int wwi_is_decl(Node *d) { @@ -488,6 +574,214 @@ wwi_is_decl(Node *d) || d->kind == N_DEF || d->kind == N_LET; } +static int +wwi_fact_same(struct factent *f, const char *mod, Node *d) +{ + return wwi_mod_eq(f->mod, mod) && f->d->kind == d->kind + && strcmp(f->d->str, d->str) == 0; +} + +static void +wwi_fact_grow(struct factent **v, int *cap, int need) +{ + if (*cap >= need) return; + int ncap = *cap ? *cap * 2 : 16; + while (ncap < need) ncap *= 2; + struct factent *nv = realloc(*v, (size_t)ncap * sizeof *nv); + if (nv == NULL) fatal("wwi: out of memory"); + *v = nv; + *cap = ncap; +} + +static Sym * +wwi_valuesym(Checker *c, const char *owner, const char *name) +{ + for (Scope *p = c->top; p; p = p->parent) + for (Sym *s = p->first; s; s = s->next) + if (s->kind == SK_DEF && strcmp(s->name, name) == 0 + && wwi_mod_eq(s->mod, owner)) + return s; + for (Scope *p = c->top; p; p = p->parent) + for (Sym *s = p->first; s; s = s->next) + if (s->kind == SK_DEF && strcmp(s->name, name) == 0 + && wwi_direct_mod_visible(c, owner, s->mod)) + return s; + return NULL; +} + +static void wwi_collect_decl(struct factset*, const char*, Node*); +static void wwi_collect_type(struct factset*, const char*, Node*); + +static void +wwi_collect_expr(struct factset *fs, const char *owner, Node *e) +{ + if (e == NULL) return; + Sym *s = NULL; + if (e->kind == N_IDENT) { + s = wwi_valuesym(fs->c, owner, e->str); + } else if (e->kind == N_DOT && e->lhs + && e->lhs->kind == N_IDENT) { + const char *mod = wwi_use_path(fs->c, owner, e->lhs->str); + if (mod) s = wwi_valuesym(fs->c, mod, e->str); + } + if (s && s->decl && s->decl->kind == N_DEF) { + if (!s->decl->export) { + errorf(e->pos, "exported declaration references " + "unexported def '%s'", e->str); + fs->bad = 1; + return; + } + wwi_collect_decl(fs, s->mod, s->decl); + return; + } + if (e->kind == N_BIN) { + wwi_collect_expr(fs, owner, e->lhs); + wwi_collect_expr(fs, owner, e->rhs); + } else if (e->kind == N_UN) { + wwi_collect_expr(fs, owner, e->lhs); + } else if (e->kind == N_CAST) { + wwi_collect_expr(fs, owner, e->lhs); + wwi_collect_type(fs, owner, e->rhs); + } +} + +static void +wwi_collect_type(struct factset *fs, const char *owner, Node *t) +{ + if (t == NULL) return; + switch (t->kind) { + case N_TNAME: { + Sym *s = wwi_typesym(fs->c, owner, t->str); + if (s && s->decl && s->decl->kind == N_TYPEDECL) + wwi_collect_decl(fs, s->mod, s->decl); + break; + } + case N_TPTR: + case N_TSLICE: + case N_TBANG: + case N_TCHAN: + wwi_collect_type(fs, owner, t->lhs); + break; + case N_TARRAY: + /* Array length is part of the resolved type identity, not a source + * name dependency. Canonicalize it so private constants stay private + * and a consumer never needs an implementation def to size the type. */ + if (t->rhs != NULL && t->rhs->kind != N_INTLIT) { + u64 len; + if (!check_eval_const(fs->c, t->rhs, owner, &len)) { + errorf(t->pos, "cannot encode array dimension"); + fs->bad = 1; + } else { + Node *e = t->rhs; + e->kind = N_INTLIT; + e->uval = len; + e->tsuffix = NULL; + e->lhs = e->rhs = e->list = NULL; + } + } + wwi_collect_type(fs, owner, t->lhs); + break; + case N_TFN: + for (Node *p = t->list; p; p = p->next) + wwi_collect_type(fs, owner, p->lhs); + wwi_collect_type(fs, owner, t->lhs); + break; + case N_TSTRUCT: + for (Node *f = t->list; f; f = f->next) + wwi_collect_type(fs, owner, f->lhs); + break; + case N_TTAGGED: + case N_TTUPLE: + for (Node *e = t->list; e; e = e->next) + wwi_collect_type(fs, owner, e); + break; + case N_TENUM: + wwi_collect_type(fs, owner, t->lhs); + /* Member identifiers are enum-local prior-sibling references, not + * package defs; the complete member list already carries their facts. */ + break; + default: + break; + } +} + +static void +wwi_collect_decl(struct factset *fs, const char *owner, Node *d) +{ + for (int i = 0; i < fs->nseen; i++) + if (wwi_fact_same(&fs->seen[i], owner, d)) + return; + wwi_fact_grow(&fs->seen, &fs->capseen, fs->nseen + 1); + fs->seen[fs->nseen] = (struct factent){d, owner, fs->nseen}; + fs->nseen++; + + if (owner != NULL) { + if (!d->export) { + errorf(d->pos, "exported declaration references unexported " + "%s '%s'", d->kind == N_TYPEDECL ? "type" : "def", + d->str); + fs->bad = 1; + return; + } + wwi_fact_grow(&fs->facts, &fs->capfacts, fs->nfacts + 1); + fs->facts[fs->nfacts] = (struct factent){d, owner, fs->nfacts}; + fs->nfacts++; + } + + switch (d->kind) { + case N_FNDECL: + for (Node *p = d->list; p; p = p->next) + wwi_collect_type(fs, owner, p->lhs); + wwi_collect_type(fs, owner, d->lhs); + break; + case N_TYPEDECL: + wwi_collect_type(fs, owner, d->lhs); + break; + case N_DEF: + wwi_collect_type(fs, owner, d->lhs); + wwi_collect_expr(fs, owner, d->rhs); + break; + case N_LET: + wwi_collect_type(fs, owner, d->lhs); + break; + default: + break; + } +} + +static int +wwi_use_owned(Node *u, const char *owner) +{ + return u->kind == N_USE && u->imported != 0 + && wwi_mod_eq(u->module, owner); +} + +static void +wwi_emit_fact_imports(FILE *of, Node *file, const char *owner) +{ + int nuse = 0; + for (Node *u = file->list; u; u = u->next) + if (wwi_use_owned(u, owner)) nuse++; + if (nuse == 0) return; + struct useent *us = malloc((size_t)nuse * sizeof *us); + if (us == NULL) fatal("wwi: out of memory"); + int k = 0; + for (Node *u = file->list; u; u = u->next) { + if (!wwi_use_owned(u, owner)) continue; + us[k].path = u->usepath ? u->usepath : u->str; + us[k].idx = k; + k++; + } + qsort(us, (size_t)nuse, sizeof *us, usecmp); + const char *previous = NULL; + for (int i = 0; i < nuse; i++) { + if (previous && strcmp(previous, us[i].path) == 0) continue; + fprintf(of, "import %s;\n", us[i].path); + previous = us[i].path; + } + free(us); +} + int wwi_emit(Checker *c, FILE *of, Node *file) { @@ -502,6 +796,22 @@ wwi_emit(Checker *c, FILE *of, Node *file) if (bad) return 1; + /* Relocate the exported API's reachable foreign type facts into this + * package's own interface. The driver remains deliberately ignorant of + * the representation and will pass only this artifact for a direct dep. */ + struct factset fs = {.c = c}; + for (Node *d = file->list; d; d = d->next) { + if (!wwi_primary(d) || !d->export || !wwi_is_decl(d)) continue; + wwi_collect_decl(&fs, NULL, d); + } + if (fs.bad) { + free(fs.facts); + free(fs.seen); + return 1; + } + if (fs.nfacts > 1) + qsort(fs.facts, (size_t)fs.nfacts, sizeof *fs.facts, factcmp); + /* package line: leaf of the first primary decl's module tag. */ const char *pkg = "main"; int found = 0; @@ -573,5 +883,24 @@ wwi_emit(Checker *c, FILE *of, Node *file) wwi_decl(of, ds[i].d); free(ds); } + + /* Compiler-owned public fact closure. A module marker changes semantic + * ownership without making the namespace a source import of the eventual + * consumer; direct visibility continues to come solely from its own N_USE. */ + const char *lastmod = NULL; + for (int i = 0; i < fs.nfacts; i++) { + struct factent *f = &fs.facts[i]; + if (lastmod == NULL || strcmp(lastmod, f->mod) != 0) { + const char *dot = strrchr(f->mod, '.'); + const char *leaf = dot ? dot + 1 : f->mod; + fprintf(of, "//ww:module %s\n", f->mod); + fprintf(of, "package %s;\n", leaf); + wwi_emit_fact_imports(of, file, f->mod); + lastmod = f->mod; + } + wwi_decl(of, f->d); + } + free(fs.facts); + free(fs.seen); return 0; } diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 21fec9b8..31670fce 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -631,6 +631,16 @@ eval_def_const(Checker *c, Node *n, u64 *out, int depth) } } +int +check_eval_const(Checker *c, Node *n, const char *owner, u64 *out) +{ + const char *saved = c->cur_mod; + c->cur_mod = owner; + int ok = eval_def_const(c, n, out, 0); + c->cur_mod = saved; + return ok; +} + /* stamp_intlit — rewrite a const-folded def rhs in place to the * literal it evaluates to, preserving the node's cexpr-resolved type * so the downstream DATA-row emit width and the invariant checks see @@ -3083,6 +3093,27 @@ check_module_shadow(Checker *c, const char *name, Pos pos, kindstr, name, name); } +/* Self-contained `.wwi` files can carry the same origin-owned type/const + * fact through two direct dependencies (a diamond), or alongside a direct + * import of that origin. In strict package mode those compiler-generated + * declarations describe one canonical package symbol: reuse the first exact + * (module, kind, name) binding so nominal Type pointer identity is preserved. + * Raw w6c mode deliberately keeps the historical duplicate diagnostics. */ +static Sym * +same_import_fact(Checker *c, Node *d, const char *mod, Skind kind) +{ + if (!c->sep_mode || d == NULL || !d->imported || !d->export + || mod == NULL || mod[0] == '\0') + return NULL; + if (kind != SK_TYPE && kind != SK_DEF) + return NULL; + Sym *s = scope_lookup_in_module(c->cur, mod, d->str); + if (s == NULL || s->kind != kind || s->decl == NULL || s->decl == d + || !s->decl->imported || !s->decl->export) + return NULL; + return s; +} + void check_file(Checker *c, Node *file) { @@ -3145,9 +3176,14 @@ check_file(Checker *c, Node *file) continue; } if (d->kind != N_TYPEDECL) continue; + const char *mod = decl_mod(file, d); + Sym *fact = same_import_fact(c, d, mod, SK_TYPE); + if (fact != NULL) { + d->type = fact->type; + continue; + } Type *named = type_named(c->a, d->str, NULL); Sym *prev = scope_lookup_local(c->cur, d->str); - const char *mod = decl_mod(file, d); if (prev && prev->kind == SK_USE) { /* `use mod; ... type mod = ...;` — promote the * SK_USE to the type symbol but remember it was @@ -3175,6 +3211,8 @@ check_file(Checker *c, Node *file) if (d->kind != N_DEF) continue; c->cur_mod = decl_mod(file, d); const char *mod = decl_mod(file, d); + if (same_import_fact(c, d, mod, SK_DEF) != NULL) + continue; Sym *prev = scope_lookup_local(c->cur, d->str); if (prev && prev->kind == SK_USE) { prev->kind = SK_DEF; prev->decl = d; @@ -3200,9 +3238,14 @@ check_file(Checker *c, Node *file) break; case N_DEF: { Type *t = resolve_type(c, d->lhs); + const char *mod = decl_mod(file, d); + Sym *fact = same_import_fact(c, d, mod, SK_DEF); + if (fact != NULL) { + d->type = fact->type ? fact->type : t; + break; + } d->type = t; Sym *prev = scope_lookup_local(c->cur, d->str); - const char *mod = decl_mod(file, d); if (prev && prev->kind == SK_DEF && prev->decl == d) { /* #141: the foldable stub bound before type-body * resolution; fill in its now-resolved type. */ diff --git a/cmd/wcc/ww.h b/cmd/wcc/ww.h index adc1e8e9..41d3c3f3 100644 --- a/cmd/wcc/ww.h +++ b/cmd/wcc/ww.h @@ -609,4 +609,9 @@ void check_file(Checker*, Node *file); * (eval_enum_value's leaf delegation, emit_defs's DATA-row gate). */ int fold_int_literal(Node*, u64*); +/* Re-evaluate a checked integer constant under the declaration owner's + * import scope. The compiler export writer uses this to canonicalize array + * dimensions without serializing source-level constant dependencies. */ +int check_eval_const(Checker*, Node*, const char *owner, u64*); + #endif /* WW_H */ diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index b125f6f1..bc054c8e 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -4012,6 +4012,14 @@ fn ffiresolve(c: *cgen, ident: str) str = { return ident; }; +// The compiler intrinsic `alloc` has a package-mode runtime ABI independent +// of whichever transitive interfaces happen to be present. Raw w6c remains +// declaration-driven so existing @symbol/FFI behavior is unchanged. +fn allocresolve(c: *cgen) str = { + if (c.sepmode != 0) { return "rt_malloc"; }; + return ffiresolve(c, "malloc"); +}; + fn argregname(i: i32) str = { if (i == 0) { return "DI"; }; if (i == 1) { return "SI"; }; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index d4b18d67..2834feee 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -6242,7 +6242,7 @@ fn cgalloc(c: *cgen, n: *syntax.node) void = { emitint(sz: i64); emitline(", DI\n"); emitline("\tCALL\t"); - emitline(ffiresolve(c, "malloc")); + emitline(allocresolve(c)); emitline("(SB)\n"); emitline("\tCMPQ\t$0, AX\n"); emitline("\tJNE\t"); emitline(okl); emitline("\n"); diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index 802218f8..ad294373 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -2276,7 +2276,7 @@ fn cgletbody(c: *cgen, n: *syntax.node, off: i32) void = { }; emitline("\tMOVQ\tAX, DI\n"); emitline("\tCALL\t"); - emitline(ffiresolve(c, "malloc")); + emitline(allocresolve(c)); emitline("(SB)\n"); if (viatryunw) { let okl: str = mklabel(c, "tryunw_ok"); diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index ff6ba9f8..db610233 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -518,6 +518,21 @@ fn installdecl(c: *checker, file: *syntax.node, d: *syntax.node) void = { // checkinit-synthesized N_TYPEDECL with an empty .file) — user decls // always carry their parsed source file. fn installtop(c: *checker, d: *syntax.node, nm: str, mod: str, k: syntax.skind, kind: str) void = { + // A self-contained interface can carry the same origin-owned type/def + // fact through both arms of a dependency diamond. In -c package mode, + // reuse the first exact compiler-export binding so all references obtain + // one nominal tinfo identity. Raw w6c keeps strict duplicate diagnostics. + if (c.sepmode != 0 && d.imported != 0 && d.exported != 0 && mod.len > 0) { + if (k == syntax.skind.SK_TYPE || k == syntax.skind.SK_DEF) { + let same: *syntax.sym = syntax.scopesamekeysym(c.top, nm, mod); + if (same != nil) { + if (same.skind == k && same.decl != nil && same.decl != d + && same.decl.imported != 0 && same.decl.exported != 0) { + return; + }; + }; + }; + }; // #30: a top-level value/type decl whose leaf ALSO names an imported // module PROMOTES that same-leaf SK_USE in place — one correctly-kinded // sym carrying use_alias=1, so bare refs (call/structlit/var) resolve to @@ -638,6 +653,17 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = { // `use IDENT;` — name is a module label, not a free ident. if (k == syntax.nkind.N_USE) { return; }; + // `_ = rhs` is a discard assignment. The parser represents `_` as an + // empty identifier; it is an lvalue marker, not an unresolved package + // value. Evaluate the rhs for its normal checks and never send the blank + // lhs through ordinary identifier resolution (cstage N_ASSIGN twin). + if (k == syntax.nkind.N_ASSIGN && n.lhs != nil) { + if (n.lhs.kind == syntax.nkind.N_IDENT && n.lhs.str.len == 0) { + if (n.rhs != nil) { resolvewalk(c, n.rhs); }; + return; + }; + }; + if (k == syntax.nkind.N_IDENT) { let nm: str = n.str; if (nm.len > 0) { @@ -663,6 +689,12 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = { }; if (k == syntax.nkind.N_TNAME) { + // A declaration-owned type reference may be revisited when its AST + // is propagated as an inferred consumer type. Its canonical tinfo was + // resolved while c.curmod named the declaring package; retain that + // compiler fact instead of reinterpreting the spelling as consumer + // source (which would incorrectly require a transitive import). + if (c.sepmode != 0 && n.type_ != nil) { c.nresolved += 1; return; }; let nm: str = n.str; if (nm.len > 0) { let s: *syntax.sym = lookupvisibletype(c, nm); @@ -695,7 +727,7 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = { }; }; if (s == nil && !builtin) { - if (syntax.scopelookup(c.cur, nm) != nil) { + if (c.sepmode != 0 || syntax.scopelookup(c.cur, nm) != nil) { cerr(n.file); cerr(":"); cerr(strconv.i32tos(n.line, strconv.base.DEC)); cerr(":"); cerr(strconv.i32tos(n.col, strconv.base.DEC)); @@ -981,7 +1013,15 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = { }; }; let l: *syntax.node = n.list; - for (l != nil) { resolvewalk(c, l); l = l.next; }; + for (l != nil) { + // A blank multi-assignment target is the same discard marker as + // `_ = rhs`, not a value lookup. stamptuplebinds below still + // stamps its slot from the rhs tuple for downstream invariants. + if (l.kind != syntax.nkind.N_IDENT || l.str.len != 0) { + resolvewalk(c, l); + }; + l = l.next; + }; stamptuplebinds(c, n.list, pt, false, ""); return; }; @@ -1006,6 +1046,13 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = { return; }; + // Enum member value identifiers name prior siblings in the same enum; + // they are not package-scope values. The enum-specific evaluator below + // validates and folds the complete member list, then stampenumvals gives + // every expression node its storage type. Do not feed those identifiers + // through the ordinary package/local expression lookup. + if (k == syntax.nkind.N_TENUMMEMBER) { return; }; + // Walk children (mirroring ast.ww's printer descent order). if (n.attr != nil) { resolvewalk(c, n.attr); }; if (n.lhs != nil) { resolvewalk(c, n.lhs); }; @@ -1724,6 +1771,22 @@ fn astunsized(c: *checker, t: *syntax.node) bool = { // call use it). The #241 `yield ` fallback (the dominant // match-bind-then-yield idiom, Hare's parseint `case let t => yield t`) // stays, now resolving btype to a tinfo. +fn exprusesname(n: *syntax.node, name: str) bool = { + if (n == nil || name.len == 0) { return false; }; + if (n.kind == syntax.nkind.N_IDENT && syntax.streq(n.str, name)) { + return true; + }; + if (exprusesname(n.lhs, name) || exprusesname(n.rhs, name) + || exprusesname(n.cond, name) || exprusesname(n.body, name) + || exprusesname(n.els, name)) { return true; }; + let e: *syntax.node = n.list; + for (e != nil) { + if (exprusesname(e, name)) { return true; }; + e = e.next; + }; + return false; +}; + fn matchyieldtype(c: *checker, body: *syntax.node, bname: str, btype: *syntax.node, nodeout: **syntax.node) *syntax.tinfo = { if (body == nil) { return nil; }; @@ -1746,6 +1809,19 @@ fn matchyieldtype(c: *checker, body: *syntax.node, bname: str, btype: *syntax.no }; return body.lhs.type_: *syntax.tinfo; }; + // The pre-walk runs before the N_MCASE scope exists. Resolve the + // declared arm binder from the case node directly; calling exprtype + // first would misclassify this valid local as an undefined package + // name now that -c rejects genuinely absent transitive values. + if (body.lhs.kind == syntax.nkind.N_IDENT && bname.len > 0 + && syntax.streq(body.lhs.str, bname)) { + *nodeout = btype; + return tinfofornode(c, btype); + }; + // A derived expression such as `yield sub.len` also depends on the + // arm scope. The pre-walk cannot type it yet; defer to the normal + // in-scope N_MCASE walk, whose cached type_ the post-walk reads. + if (exprusesname(body.lhs, bname)) { return nil; }; let t: *syntax.node = exprtype(c, body.lhs, nil); if (t != nil) { *nodeout = t; @@ -2602,7 +2678,14 @@ fn tinfofornode(c: *checker, n: *syntax.node) *syntax.tinfo = { let named: *syntax.tinfo = syntax.typenamed(s.name, nil); s.type_ = named; named.resolving = 1; + // Resolve a named declaration's body in the package that + // owns it. A direct dependency's fact may be demanded while + // walking a consumer-owned type; keeping the consumer module + // here can bind bare names in the fact to the wrong package. + let savedmod: str = c.curmod; + c.curmod = s.mod; let under: *syntax.tinfo = tinfofornode(c, body); + c.curmod = savedmod; // #62/#69: alias-root cycle (`type a = b; // type b = a` / `type a = a`) — checked // BEFORE clearing the flag so self-aliases @@ -3733,11 +3816,15 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = { // L2439, #53 at L688. Tracked in the cluster note at L685-687. let s: *syntax.sym = lookupvisible(c, e.str); if (s == nil) { + // abort/assert are dedicated compiler builtins and intentionally + // have no callee symbol; the surrounding N_CALL arm validates and + // stamps them. Package-mode undefined checks must not preempt it. + if (isassertfam(c, e)) { return nil; }; // A same-named flattened symbol that fails lookupvisible is a // transitive implementation fact, not an unresolved external. // Diagnose it like cstage's N_IDENT path and stamp tyerr so // later call checking does not obscure the causal error. - if (syntax.scopelookup(c.cur, e.str) != nil) { + if (c.sepmode != 0 || syntax.scopelookup(c.cur, e.str) != nil) { cerr(e.file); cerr(":"); cerr(strconv.i32tos(e.line, strconv.base.DEC)); cerr(":"); cerr(strconv.i32tos(e.col, strconv.base.DEC)); diff --git a/selfhost/cmd/wcc/wwi.ww b/selfhost/cmd/wcc/wwi.ww index da9c2334..334f87dc 100644 --- a/selfhost/cmd/wcc/wwi.ww +++ b/selfhost/cmd/wcc/wwi.ww @@ -13,8 +13,9 @@ // - check_exported_type rides the producer entry (flag-gated), off 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 emit filters to PRIMARY -// decls (imported==0). +// - A `.wwi` is ONE package's self-contained interface. Its primary +// section is followed by compiler-owned origin sections containing the +// exported foreign type/const facts reachable from the public surface. package wcc; @@ -72,8 +73,67 @@ fn wquote(fd: i32, s: str) void = { // 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) *syntax.sym = { +fn wwimodeq(a: str, b: str) bool = { + if (a.len == 0 || b.len == 0) { return a.len == b.len; }; + return syntax.streq(a, b); +}; + +// Map an import alias in the source package that owns the reference. The +// flattened parser file contains every imported interface's N_USE nodes, so +// this owner filter is what prevents cross-package alias capture. +fn wwiusepath(c: *checker, owner: str, alias: str) str = { + if (owner.len > 0) { + let dotidx: i32 = -1; + let i: i32 = 0; + for (i < owner.len) { + if (owner[i] == 46u8) { dotidx = i; }; + i += 1; + }; + let leaf: str = owner; + if (dotidx >= 0) { + leaf.ptr = owner.ptr + ((dotidx + 1): u64); + leaf.len = owner.len - dotidx - 1; + }; + if (syntax.streq(alias, leaf)) { return owner; }; + }; + let u: *syntax.node = c.file.list; + for (u != nil) { + if (u.kind == syntax.nkind.N_USE && syntax.streq(u.str, alias)) { + let same: bool = false; + if (owner.len == 0) { + same = u.imported == 0; + } else { + same = u.imported != 0 && syntax.streq(u.nmod, owner); + }; + if (same) { + if (u.usepath.len > 0) { return u.usepath; }; + return u.str; + }; + }; + u = u.next; + }; let empty: str; + return empty; +}; + +fn wwidirectmodvisible(c: *checker, owner: str, mod: str) bool = { + if (mod.len == 0) { return false; }; + let dotidx: i32 = -1; + let i: i32 = 0; + for (i < mod.len) { + if (mod[i] == 46u8) { dotidx = i; }; + i += 1; + }; + let alias: str = mod; + if (dotidx >= 0) { + alias.ptr = mod.ptr + ((dotidx + 1): u64); + alias.len = mod.len - dotidx - 1; + }; + let path: str = wwiusepath(c, owner, alias); + return path.len > 0 && syntax.streq(path, mod); +}; + +fn wwitypesym(c: *checker, owner: str, nm: str) *syntax.sym = { let dotidx: i32 = -1; let i: i32 = 0; for (i < nm.len) { @@ -88,20 +148,28 @@ fn wwitypesym(c: *checker, nm: str) *syntax.sym = { let leaf: str; leaf.ptr = nm.ptr + ((dotidx + 1): u64); leaf.len = nm.len - dotidx - 1; - let u: *syntax.node = c.file.list; - for (u != nil) { - if (u.kind == syntax.nkind.N_USE && wwiprimary(u)) { - if (syntax.streq(u.str, head)) { - let mod: str = u.usepath; - if (mod.len == 0) { mod = u.str; }; - s = syntax.scopelookupinmodule(c.cur, mod, leaf); - break; - }; - }; - u = u.next; + let mod: str = wwiusepath(c, owner, head); + if (mod.len > 0) { + s = syntax.scopelookupinmodule(c.cur, mod, leaf); }; } else { - s = syntax.scopelookuptype(c.cur, empty, nm); + s = syntax.scopelookuptype(c.cur, owner, nm); + if (s == nil) { + let p: *syntax.scope = c.cur; + for (p != nil && s == nil) { + let b: *syntax.sym = p.first; + for (b != nil) { + if (b.skind == syntax.skind.SK_TYPE + && syntax.streq(b.name, nm) + && wwidirectmodvisible(c, owner, b.mod)) { + s = b; + break; + }; + b = b.snext; + }; + p = p.parent; + }; + }; }; if (s == nil) { return nil; }; if (s.skind != syntax.skind.SK_TYPE) { return nil; }; @@ -120,15 +188,15 @@ fn wwireject(d: *syntax.node, nm: str) void = { }; // Returns 1 if a non-exported nominal was named (loud), else 0. -fn wwichecktype(c: *checker, d: *syntax.node, t: *syntax.node) i32 = { +fn wwichecktype(c: *checker, owner: str, d: *syntax.node, t: *syntax.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 == syntax.nkind.N_TPARAM) { return wwichecktype(c, d, t.lhs); }; + if (t.kind == syntax.nkind.N_TPARAM) { return wwichecktype(c, owner, d, t.lhs); }; let bad: i32 = 0; if (t.kind == syntax.nkind.N_TNAME) { - let s: *syntax.sym = wwitypesym(c, t.str); + let s: *syntax.sym = wwitypesym(c, owner, t.str); // sym.exported is vestigial (never set); the nominal's export // status lives on its decl node, parser-set. if (s != nil) { @@ -156,53 +224,54 @@ fn wwichecktype(c: *checker, d: *syntax.node, t: *syntax.node) i32 = { t.kind == syntax.nkind.N_TBANG || t.kind == syntax.nkind.N_TCHAN ) { - bad = bad | wwichecktype(c, d, t.lhs); + bad = bad | wwichecktype(c, owner, d, t.lhs); } else { if (t.kind == syntax.nkind.N_TARRAY) { // element only; the length is a const-expr, not a type. - bad = bad | wwichecktype(c, d, t.lhs); + bad = bad | wwichecktype(c, owner, d, t.lhs); } else { if (t.kind == syntax.nkind.N_TFN) { let p: *syntax.node = t.list; for (p != nil) { - bad = bad | wwichecktype(c, d, p.lhs); + bad = bad | wwichecktype(c, owner, d, p.lhs); p = p.next; }; - bad = bad | wwichecktype(c, d, t.lhs); + bad = bad | wwichecktype(c, owner, d, t.lhs); } else { if (t.kind == syntax.nkind.N_TSTRUCT) { let f: *syntax.node = t.list; for (f != nil) { - bad = bad | wwichecktype(c, d, f.lhs); + bad = bad | wwichecktype(c, owner, d, f.lhs); f = f.next; }; } else { if (t.kind == syntax.nkind.N_TTAGGED || t.kind == syntax.nkind.N_TTUPLE) { let e: *syntax.node = t.list; for (e != nil) { - bad = bad | wwichecktype(c, d, e); + bad = bad | wwichecktype(c, owner, d, e); e = e.next; }; } else { if (t.kind == syntax.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); + bad = bad | wwichecktype(c, owner, d, t.lhs); };};};};};};}; return bad; }; fn wwicheckdecl(c: *checker, d: *syntax.node) i32 = { + let owner: str; let bad: i32 = 0; if (d.kind == syntax.nkind.N_FNDECL) { let p: *syntax.node = d.list; for (p != nil) { - bad = bad | wwichecktype(c, d, p.lhs); + bad = bad | wwichecktype(c, owner, d, p.lhs); p = p.next; }; - bad = bad | wwichecktype(c, d, d.lhs); + bad = bad | wwichecktype(c, owner, d, d.lhs); } else { if (d.kind == syntax.nkind.N_TYPEDECL) { - bad = bad | wwichecktype(c, d, d.lhs); + bad = bad | wwichecktype(c, owner, d, d.lhs); } else { if (d.kind == syntax.nkind.N_DEF) { // the declared type; the rhs const value is not a type. - bad = bad | wwichecktype(c, d, d.lhs); + bad = bad | wwichecktype(c, owner, d, d.lhs); } else { if (d.kind == syntax.nkind.N_LET) { - bad = bad | wwichecktype(c, d, d.lhs); + bad = bad | wwichecktype(c, owner, d, d.lhs); };};};}; return bad; }; @@ -303,10 +372,16 @@ fn wwiexpr(fd: i32, e: *syntax.node) void = { } else { if (e.kind == syntax.nkind.N_UN) { wputs(fd, syntax.tokname(e.op)); wwiexpr(fd, e.lhs); + } else { if (e.kind == syntax.nkind.N_CAST) { + wputb(fd, '('); + wwiexpr(fd, e.lhs); + wputs(fd, ": "); + wwitype(fd, e.rhs); + wputb(fd, ')'); } else { wputs(2, "wwi: unhandled const-expr node kind\n"); os.exit(1); - };};};};};};};};};}; + };};};};};};};};};};}; }; fn wwiparam(fd: i32, p: *syntax.node) void = { @@ -501,7 +576,7 @@ fn wwidecl(fd: i32, d: *syntax.node) void = { // 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 (#71). - if (d.rhs != nil && (d.rhs.kind == syntax.nkind.N_STRUCTLIT || + if (d.rhs == nil || (d.rhs.kind == syntax.nkind.N_STRUCTLIT || d.rhs.kind == syntax.nkind.N_ARRLIT)) { wputs(fd, ";\n"); } else { @@ -569,6 +644,265 @@ fn wwisortdecls(keys: []str, nodes: []*syntax.node, n: i32) void = { }; }; +// Compiler-owned reachable facts. Parallel arrays keep the self-hosted +// representation narrow and make the deterministic ordering explicit. +type wwifactset = struct { + c: *checker, + factnodes: []*syntax.node, + factmods: []str, + factranks: []i32, + nfacts: i32, + seennodes: []*syntax.node, + seenmods: []str, + seenranks: []i32, + nseen: i32, + bad: i32, +}; + +fn wwifactrank(d: *syntax.node) i32 = { + if (d.kind == syntax.nkind.N_TYPEDECL) { return 0i32; }; + if (d.kind == syntax.nkind.N_DEF) { return 1i32; }; + if (d.kind == syntax.nkind.N_FNDECL) { return 2i32; }; + return 3i32; +}; + +fn wwifactsame(mod: str, rank: i32, d: *syntax.node, + smod: str, srank: i32, sd: *syntax.node) bool = { + return rank == srank && wwimodeq(mod, smod) && syntax.streq(d.str, sd.str); +}; + +fn wwifactvaluesym(c: *checker, owner: str, name: str) *syntax.sym = { + let p: *syntax.scope = c.top; + for (p != nil) { + let s: *syntax.sym = p.first; + for (s != nil) { + if (s.skind == syntax.skind.SK_DEF && syntax.streq(s.name, name) + && wwimodeq(s.mod, owner)) { return s; }; + s = s.snext; + }; + p = p.parent; + }; + p = c.top; + for (p != nil) { + let s: *syntax.sym = p.first; + for (s != nil) { + if (s.skind == syntax.skind.SK_DEF && syntax.streq(s.name, name) + && wwidirectmodvisible(c, owner, s.mod)) { return s; }; + s = s.snext; + }; + p = p.parent; + }; + return nil; +}; + +fn wwifactreject(d: *syntax.node, kind: str, name: 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 "); + wputs(2, kind); + wputs(2, " '"); + wputs(2, name); + wputs(2, "'\n"); +}; + +fn wwiencodearrayreject(d: *syntax.node) 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: cannot encode array dimension\n"); +}; + +fn wwicollectdecl(fs: *wwifactset, owner: str, d: *syntax.node) void = { + let rank: i32 = wwifactrank(d); + let i: i32 = 0; + for (i < fs.nseen) { + if (wwifactsame(owner, rank, d, fs.seenmods[i], + fs.seenranks[i], fs.seennodes[i])) { return; }; + i += 1; + }; + fs.seennodes[fs.nseen] = d; + fs.seenmods[fs.nseen] = owner; + fs.seenranks[fs.nseen] = rank; + fs.nseen += 1; + + if (owner.len > 0) { + if (d.exported == 0) { + let kind: str = "def"; + if (d.kind == syntax.nkind.N_TYPEDECL) { kind = "type"; }; + wwifactreject(d, kind, d.str); + fs.bad = 1; + return; + }; + fs.factnodes[fs.nfacts] = d; + fs.factmods[fs.nfacts] = owner; + fs.factranks[fs.nfacts] = rank; + fs.nfacts += 1; + }; + + if (d.kind == syntax.nkind.N_FNDECL) { + let p: *syntax.node = d.list; + for (p != nil) { wwicollecttype(fs, owner, p.lhs); p = p.next; }; + wwicollecttype(fs, owner, d.lhs); + } else { if (d.kind == syntax.nkind.N_TYPEDECL) { + wwicollecttype(fs, owner, d.lhs); + } else { if (d.kind == syntax.nkind.N_DEF) { + wwicollecttype(fs, owner, d.lhs); + wwicollectexpr(fs, owner, d.rhs); + } else { if (d.kind == syntax.nkind.N_LET) { + wwicollecttype(fs, owner, d.lhs); + };};};}; +}; + +fn wwicollectexpr(fs: *wwifactset, owner: str, e: *syntax.node) void = { + if (e == nil) { return; }; + let s: *syntax.sym = nil; + if (e.kind == syntax.nkind.N_IDENT) { + s = wwifactvaluesym(fs.c, owner, e.str); + } else { if (e.kind == syntax.nkind.N_DOT && e.lhs != nil) { + if (e.lhs.kind == syntax.nkind.N_IDENT) { + let mod: str = wwiusepath(fs.c, owner, e.lhs.str); + if (mod.len > 0) { s = wwifactvaluesym(fs.c, mod, e.str); }; + }; + }; }; + if (s != nil && s.decl != nil) { + if (s.decl.exported == 0) { + wwifactreject(e, "def", e.str); + fs.bad = 1; + return; + }; + wwicollectdecl(fs, s.mod, s.decl); + return; + }; + if (e.kind == syntax.nkind.N_BIN) { + wwicollectexpr(fs, owner, e.lhs); + wwicollectexpr(fs, owner, e.rhs); + } else { if (e.kind == syntax.nkind.N_UN) { + wwicollectexpr(fs, owner, e.lhs); + } else { if (e.kind == syntax.nkind.N_CAST) { + wwicollectexpr(fs, owner, e.lhs); + wwicollecttype(fs, owner, e.rhs); + }; }; }; +}; + +fn wwievalconst(fs: *wwifactset, owner: str, e: *syntax.node, + out: *u64) bool = { + let saved: str = fs.c.curmod; + fs.c.curmod = owner; + let ok: bool = evaldefconst(fs.c, e, out, 0); + fs.c.curmod = saved; + return ok; +}; + +fn wwicollecttype(fs: *wwifactset, owner: str, t: *syntax.node) void = { + if (t == nil) { return; }; + if (t.kind == syntax.nkind.N_TPARAM) { wwicollecttype(fs, owner, t.lhs); return; }; + if (t.kind == syntax.nkind.N_TNAME) { + let s: *syntax.sym = wwitypesym(fs.c, owner, t.str); + if (s != nil && s.decl != nil) { + if (s.decl.kind == syntax.nkind.N_TYPEDECL) { + wwicollectdecl(fs, s.mod, s.decl); + }; + }; + } else { if ( + t.kind == syntax.nkind.N_TPTR || t.kind == syntax.nkind.N_TSLICE || + t.kind == syntax.nkind.N_TBANG || t.kind == syntax.nkind.N_TCHAN + ) { + wwicollecttype(fs, owner, t.lhs); + } else { if (t.kind == syntax.nkind.N_TARRAY) { + // Array length is resolved type identity, not a source name + // dependency. Canonicalize it so private constants stay private + // and consumers never need implementation defs merely for layout. + if (t.rhs != nil && t.rhs.kind != syntax.nkind.N_INTLIT) { + let len: u64 = 0u64; + if (!wwievalconst(fs, owner, t.rhs, &len)) { + wwiencodearrayreject(t); + fs.bad = 1; + } else { + let e: *syntax.node = t.rhs; + e.kind = syntax.nkind.N_INTLIT; + e.uval = len; + e.lhs = nil; e.rhs = nil; e.list = nil; + let empty: str; e.tsuffix = empty; + }; + }; + wwicollecttype(fs, owner, t.lhs); + } else { if (t.kind == syntax.nkind.N_TFN) { + let p: *syntax.node = t.list; + for (p != nil) { wwicollecttype(fs, owner, p.lhs); p = p.next; }; + wwicollecttype(fs, owner, t.lhs); + } else { if (t.kind == syntax.nkind.N_TSTRUCT) { + let f: *syntax.node = t.list; + for (f != nil) { wwicollecttype(fs, owner, f.lhs); f = f.next; }; + } else { if (t.kind == syntax.nkind.N_TTAGGED || t.kind == syntax.nkind.N_TTUPLE) { + let e: *syntax.node = t.list; + for (e != nil) { wwicollecttype(fs, owner, e); e = e.next; }; + } else { if (t.kind == syntax.nkind.N_TENUM) { + wwicollecttype(fs, owner, t.lhs); + // Member identifiers refer to prior siblings in this enum, not + // package defs; the declaration already carries the whole list. + };};};};};};}; +}; + +fn wwisortfacts(fs: *wwifactset) void = { + let i: i32 = 0; + for (i < fs.nfacts) { + let best: i32 = i; + let j: i32 = i + 1; + for (j < fs.nfacts) { + let r: i32 = wwistrcmp(fs.factmods[j], fs.factmods[best]); + if (r == 0) { r = fs.factranks[j] - fs.factranks[best]; }; + if (r == 0) { r = wwistrcmp(fs.factnodes[j].str, fs.factnodes[best].str); }; + if (r < 0) { best = j; }; + j += 1; + }; + if (best != i) { + let tn: *syntax.node = fs.factnodes[i]; fs.factnodes[i] = fs.factnodes[best]; fs.factnodes[best] = tn; + let tm: str = fs.factmods[i]; fs.factmods[i] = fs.factmods[best]; fs.factmods[best] = tm; + let tr: i32 = fs.factranks[i]; fs.factranks[i] = fs.factranks[best]; fs.factranks[best] = tr; + }; + i += 1; + }; +}; + +fn wwiowneduse(u: *syntax.node, owner: str) bool = { + return u.kind == syntax.nkind.N_USE && u.imported != 0 + && syntax.streq(u.nmod, owner); +}; + +fn wwiemitfactimports(fd: i32, file: *syntax.node, owner: str) void = { + let nuse: i32 = 0; + let u: *syntax.node = file.list; + for (u != nil) { if (wwiowneduse(u, owner)) { nuse += 1; }; u = u.next; }; + if (nuse == 0) { return; }; + let paths: []str = alloc([], nuse: u64)!; paths.len = nuse; + let nodes: []*syntax.node = alloc([], nuse: u64)!; nodes.len = nuse; + let k: i32 = 0; + u = file.list; + for (u != nil) { + if (wwiowneduse(u, owner)) { + if (u.usepath.len > 0) { paths[k] = u.usepath; } else { paths[k] = u.str; }; + nodes[k] = u; + k += 1; + }; + u = u.next; + }; + wwisortdecls(paths, nodes, nuse); + let previous: str; + let i: i32 = 0; + for (i < nuse) { + if (previous.len == 0 || !syntax.streq(previous, paths[i])) { + wputs(fd, "import "); wputs(fd, paths[i]); wputs(fd, ";\n"); + previous = paths[i]; + }; + i += 1; + }; +}; + export fn wwiemit(c: *checker, file: *syntax.node, path: str) i32 = { // §5: check_exported_type FIRST, before any byte — a producer // without it can emit a dangling `.wwi`. @@ -582,6 +916,34 @@ export fn wwiemit(c: *checker, file: *syntax.node, path: str) i32 = { }; if (bad != 0) { return 1i32; }; + // The complete closure cannot contain more declarations than the checked + // compilation unit. Allocate once; collection deduplicates by semantic + // (owner, kind, name), then sorting supplies the byte-stable order. + let nall: i32 = 0; + d = file.list; + for (d != nil) { if (wwiisdecl(d)) { nall += 1; }; d = d.next; }; + if (nall == 0) { nall = 1; }; + let fs: wwifactset; + fs.c = c; + let factnodes: []*syntax.node = alloc([], nall: u64)!; factnodes.len = nall; + let factmods: []str = alloc([], nall: u64)!; factmods.len = nall; + let factranks: []i32 = alloc([], nall: u64)!; factranks.len = nall; + let seennodes: []*syntax.node = alloc([], nall: u64)!; seennodes.len = nall; + let seenmods: []str = alloc([], nall: u64)!; seenmods.len = nall; + let seenranks: []i32 = alloc([], nall: u64)!; seenranks.len = nall; + fs.factnodes = factnodes; fs.factmods = factmods; fs.factranks = factranks; + fs.seennodes = seennodes; fs.seenmods = seenmods; fs.seenranks = seenranks; + let primary: str; + d = file.list; + for (d != nil) { + if (wwiprimary(d) && d.exported != 0 && wwiisdecl(d)) { + wwicollectdecl(&fs, primary, d); + }; + d = d.next; + }; + if (fs.bad != 0) { return 1i32; }; + wwisortfacts(&fs); + let fd: i32 = os.open(path, os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 if (fd < 0) { @@ -710,6 +1072,30 @@ export fn wwiemit(c: *checker, file: *syntax.node, path: str) i32 = { }; }; + // Compiler-owned public fact closure. Origin markers preserve nominal + // ownership but do not create source imports in the eventual consumer. + let lastmod: str; + let fi: i32 = 0; + for (fi < fs.nfacts) { + let mod: str = fs.factmods[fi]; + if (lastmod.len == 0 || !syntax.streq(lastmod, mod)) { + wputs(fd, "//ww:module "); wputs(fd, mod); wputs(fd, "\n"); + let dotidx: i32 = -1; + let mi: i32 = 0; + for (mi < mod.len) { if (mod[mi] == 46u8) { dotidx = mi; }; mi += 1; }; + let leaf: str = mod; + if (dotidx >= 0) { + leaf.ptr = mod.ptr + ((dotidx + 1): u64); + leaf.len = mod.len - dotidx - 1; + }; + wputs(fd, "package "); wputs(fd, leaf); wputs(fd, ";\n"); + wwiemitfactimports(fd, file, mod); + lastmod = mod; + }; + wwidecl(fd, fs.factnodes[fi]); + fi += 1; + }; + os.close(fd); return 0i32; }; diff --git a/test/byteid/wwi_test.ww b/test/byteid/wwi_test.ww index cc67db16..e3080472 100644 --- a/test/byteid/wwi_test.ww +++ b/test/byteid/wwi_test.ww @@ -8,8 +8,8 @@ package wwi_test; // 10): the `w6c -I ` producer IS the live import path, and // `.wwi` is a cross-stage byte-id substrate pinned directly. POSITIVE: // for ascii/strings/getopt (drew2-audited leak-free), drive the target -// as the PRIMARY module of a driver-combined unit, then (1) w6c -I and -// w6c_ww -I both succeed, (2) cs.wwi == ww.wwi byte-for-byte, (3) the +// as the PRIMARY module of a driver-combined unit, then (1) w6c -c -I and +// w6c_ww -c -I both succeed, (2) cs.wwi == ww.wwi byte-for-byte, (3) the // emitted .wwi re-parses (wwdump -a exit 0). getopt is the recursion // stressor. A synth fixture covers the decl-kinds + type-nodes no lib // package reaches (def const-expr fold, let global, [N]T, fn-ptr, !T, @@ -94,10 +94,13 @@ fn m2positive(pkg: str) void = { let cs: str = strings.concat(td, "/cs.wwi"); let ws: str = strings.concat(td, "/ww.wwi"); - let cav: []str = [testenv.driver("w6c"), "-I", cs, comb]; - if (!runok(td, "w6c", cav)) { fail(pkg, "w6c -I rejected"); }; - let wav: []str = [testenv.driver("w6c_ww"), "-I", ws, comb]; - if (!runok(td, "w6c_ww", wav)) { fail(pkg, "w6c_ww -I rejected"); }; + // The driver-composed unit uses package separators and may contain the + // same compiler-owned origin fact through multiple direct interfaces; + // exact fact interning is intentionally tied to -c package mode. + let cav: []str = [testenv.driver("w6c"), "-c", "-I", cs, comb]; + if (!runok(td, "w6c", cav)) { fail(pkg, "w6c -c -I rejected"); }; + let wav: []str = [testenv.driver("w6c_ww"), "-c", "-I", ws, comb]; + if (!runok(td, "w6c_ww", wav)) { fail(pkg, "w6c_ww -c -I rejected"); }; if (!testenv.same(testenv.readfile(cs), testenv.readfile(ws))) { fail(pkg, "cs.wwi != ww.wwi (byte-id broken on the .wwi substrate)"); @@ -125,7 +128,8 @@ fn m2positive(pkg: str) void = { let td: str = testenv.fresh(); let src: str = strings.concat( "package synth;\n", - "export type color = enum { RED, GREEN = 5, BLUE };\n", + "def GREEN: i32 = 99;\n", + "export type color = enum { RED, GREEN = 5, BLUE = GREEN + 1 };\n", "export def LIMIT: i32 = 10 + 2 * 3;\n", "export def NAME: str = \"hi\\n\";\n", "export def FLAG: bool = true;\n", @@ -154,8 +158,15 @@ fn m2positive(pkg: str) void = { "cs.wwi != ww.wwi (const-expr / decl-kind unparse diverges)"); }; if (!testenv.has(testenv.readfile(cs), - "export let grouped: [((1 + 2) * 3)]u8;")) { - fail("synth", ".wwi changed grouped array-dimension semantics"); + "export let grouped: [9]u8;") + || !testenv.has(testenv.readfile(cs), + "export fn matrix() [16]u8;")) { + fail("synth", ".wwi did not canonicalize checked array dimensions"); + }; + if (!testenv.has(testenv.readfile(cs), + "export type color = enum { RED, GREEN = 5, BLUE = (GREEN + 1) };") + || testenv.has(testenv.readfile(cs), "def GREEN: i32 = 99")) { + fail("synth", ".wwi confused an enum sibling with a package def"); }; // #47: the @symbol attribute must survive the round-trip verbatim. if (!testenv.has(testenv.readfile(cs), @@ -195,10 +206,14 @@ fn m2positive(pkg: str) void = { if (!testenv.same(testenv.readfile(cs), testenv.readfile(ws))) { fail("qualified", "cs.wwi != ww.wwi"); }; - if (!testenv.has(testenv.readfile(cs), - "export fn use(x: dep.Clash) i32;")) { + let body: str = testenv.readfile(cs); + if (!testenv.has(body, "export fn use(x: dep.Clash) i32;")) { fail("qualified", ".wwi dropped the qualified signature"); }; + if (!testenv.has(body, "//ww:module a.dep\n") + || !testenv.has(body, "export type Clash = struct { x: i32 };")) { + fail("qualified", ".wwi omitted the signature's origin-owned type fact"); + }; let dav: []str = [testenv.driver("wwdump"), "-a", cs]; if (!runok(td, "wwdump", dav)) { fail("qualified", "emitted .wwi does not re-parse");