From f308818b4bb2f5621e7c101f4004d02fcceaff00 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 15 Jun 2026 17:37:18 +0900 Subject: [PATCH] wcc/ww: mangle imported symbols on dotted import path (#22 M1, #32) Switch symbol mangling from the import leaf clause to the full dotted import path for directory packages; single-file imports keep package-clause mangling (isdir-gate: imported<=>directory-import). The root build unit's fn main stays bare, every other top-level decl mangles, closing #31's duplicate-main hazard by construction (#32). Both stages, byte-identical. Single commit, not split: the bare rename (f244af3) is red on its own because it unmasks cross-module resolution gaps that do not reproduce pre-M1, so the fixes are intrinsic to making the rename correct. Included: wwstage fnret/fnparamslookupmod map import alias->path (#199b cross-module union-variant scrutinee resolved the wrong fn's union); cstage use_path prefers the referencing module's import for an ambiguous leaf alias (sha256 crypto.math vs strconv math). Tests table-driven: 989_m1mangle_run/_sym, 989_m1union_run (gate-visible per-arm exit codes + cs==ww byte-id). --- Makefile | 32 +++ cmd/w6c/cgen.c | 85 ++++++- cmd/wcc/check.c | 99 ++++++-- cmd/wcc/lex.c | 41 +++- cmd/wcc/parse.c | 48 +++- cmd/wcc/tok.c | 1 + cmd/wcc/ww.h | 23 +- cmd/ww/main.c | 42 ++-- lib/ww/ast.ww | 6 +- lib/ww/lex/lex.ww | 66 +++++- lib/ww/lex/tok.ww | 6 +- lib/ww/lex/toktest.ww | 6 +- lib/ww/parse/decl.ww | 7 + lib/ww/parse/parse.ww | 35 ++- selfhost/cmd/w6a/main.combined.ww | 13 + selfhost/cmd/w6c/main.combined.ww | 340 ++++++++++++++++++++++----- selfhost/cmd/w6l/main.combined.ww | 14 ++ selfhost/cmd/wcc/cgen.ww | 67 +++++- selfhost/cmd/wcc/cgendecl.ww | 12 +- selfhost/cmd/wcc/cgenexpr.ww | 18 +- selfhost/cmd/wcc/cgenutil.ww | 6 +- selfhost/cmd/wcc/check.ww | 80 +++++-- selfhost/cmd/ww/main.combined.ww | 51 +++- selfhost/cmd/ww/main.ww | 43 +++- selfhost/cmd/wwdump/main.combined.ww | 340 ++++++++++++++++++++++----- selfhost/test/smoke.combined.ww | 17 ++ test/wcc/764_amp_fn_ident.c | 8 +- test/wcc/989_m1mangle_run.c | 212 +++++++++++++++++ test/wcc/989_m1mangle_sym.c | 149 ++++++++++++ test/wcc/989_m1union_run.c | 225 ++++++++++++++++++ 30 files changed, 1851 insertions(+), 241 deletions(-) create mode 100644 test/wcc/989_m1mangle_run.c create mode 100644 test/wcc/989_m1mangle_sym.c create mode 100644 test/wcc/989_m1union_run.c diff --git a/Makefile b/Makefile index c69f8f5c..9bb54033 100644 --- a/Makefile +++ b/Makefile @@ -265,6 +265,9 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_libprecond_abort \ $(BIN)/test_idxarg_run \ $(BIN)/test_chainidx_run \ + $(BIN)/test_m1mangle_run \ + $(BIN)/test_m1mangle_sym \ + $(BIN)/test_m1union_run \ $(BIN)/test_tupfieldsize_run \ $(BIN)/test_tagtupfieldsize_run \ $(BIN)/test_nestfield_run \ @@ -817,6 +820,35 @@ $(BIN)/test_chainidx_run: test/wcc/989_chainidx_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# 989_m1mangle_run / _sym (M1, #22 + #32): path-qualified symbol mangling +# and root-unit entry detection. _run builds+runs a tiny package tree on +# BOTH stages (rule-10); _sym asserts the emitted symbol names + cs.s==ww.s. +$(BIN)/test_m1mangle_run: test/wcc/989_m1mangle_run.c \ + $(BIN)/ww $(BIN)/ww_ww \ + $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + +$(BIN)/test_m1mangle_sym: test/wcc/989_m1mangle_sym.c \ + $(BIN)/ww $(BIN)/ww_ww \ + $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + +# 989_m1union_run (M1, #199b): a `match` on a cross-module fn's tagged-union +# return must keep each nominal void-alias variant on a distinct tag. Builds+ +# runs the utf8.next 4-arm match per-arm on BOTH stages (rule-10) and pins +# cstage.s==wwstage.s on the all-arms program — the gate-VISIBLE regression +# teeth for the variant-collapse the doc flagged as byte-id-only. +$(BIN)/test_m1union_run: test/wcc/989_m1union_run.c \ + $(BIN)/ww $(BIN)/ww_ww \ + $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + # 989_tupfieldsize_run (F7-c4, #43): a for-range destructure over an array # of tuples must stride by the tuple's true size (a slice/str/tuple field # carries its full width). Builds+runs on BOTH driver twins (rule-10), the diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index e3e4978e..b9f606c8 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -1167,6 +1167,43 @@ struct Mod { }; static Mod *mod_map; +/* M1 #22: alias→import-path map built from the N_USE nodes, so a + * qualified-ref codegen hint (`utf8.decoderune`) keys mod_map on the + * dotted path the symbols are registered under, not the bare alias. + * For single-level packages alias == path, so this is a no-op there. */ +typedef struct Use Use; +struct Use { + const char *alias; + const char *path; + Use *next; +}; +static Use *use_map; + +/* + * Retained divergence (M1 #22): this map is file-global — when two + * modules in the same unit bind the same leaf alias to different paths + * (sha256's `import crypto.math` vs strconv's `import math`), the first + * match wins. The checker's twin (use_path, check.c) is module-scoped + * to fix exactly this for qualified-NAME resolution; the cgen mangle + * hint is NOT, because it is unreachable with an ambiguous alias: a + * cross-module qualified ref resolves only to an EXPORTED symbol, and + * exported non-fn decls stay bare (mod_collect skips them, so the hint + * is moot) while an exported-fn collision would require two same-leaf + * packages to export the same fn name AND a third caller — not present + * (the tree links cleanly). Left file-global to stay byte-identical + * with wwstage's symmetric usehint (cgen.ww), which is also file-global + * (rule 10). Module-scope both stages together if a real collision + * surfaces — filed as the M1 cgen-hint twin follow-up. + */ +static const char * +use_hint(const char *alias) +{ + if (alias == NULL) return alias; + for (Use *u = use_map; u; u = u->next) + if (strcmp(u->alias, alias) == 0) return u->path; + return alias; +} + /* Top-level `let` map. Populated alongside mod_map; consulted by the * N_IDENT store path and the &-of path to route reads/writes through * a RIP-relative reference rather than dropping them as the (pre- @@ -1383,8 +1420,18 @@ static void mod_collect(Cg *c, Node *file) { mod_map = NULL; + use_map = NULL; if (file == NULL) return; for (Node *d = file->list; d; d = d->next) { + /* M1 #22: record alias→path for the qualified-ref hint. */ + if (d->kind == N_USE && d->str && d->usepath) { + Use *u = amalloc(c->a, sizeof *u); + u->alias = d->str; + u->path = d->usepath; + u->next = use_map; + use_map = u; + continue; + } int isfn = (d->kind == N_FNDECL); int track = isfn || (d->kind == N_TYPEDECL) || (d->kind == N_DEF) || (d->kind == N_LET); @@ -1399,8 +1446,11 @@ mod_collect(Cg *c, Node *file) if (decl_has_ffisym(d)) continue; /* `main` is the linker entry-point convention. Even when not * marked `export`, it must keep its bare name so w6l can - * resolve `_start`'s `CALL main(SB)`. */ - if (d->str && strcmp(d->str, "main") == 0) continue; + * resolve `_start`'s `CALL main(SB)`. M1 #32: only the ROOT + * unit's main stays bare; an IMPORTED `fn main` mangles on its + * path (closes #31's dup-main by construction). */ + if (d->str && strcmp(d->str, "main") == 0 && !d->imported) + continue; Mod *m = amalloc(c->a, sizeof *m); m->name = d->str; m->module = d->module; @@ -4321,7 +4371,7 @@ cgexpr(Cg *c, Node *n, Local *locals) if (lu && lu->kind == TY_FN) ins2(c, A_LEAQ, mafn(c, opnd->str, - opnd->lhs->str), + use_hint(opnd->lhs->str)), areg(D_AX)); else /* #229: dotted-module value @@ -4330,7 +4380,7 @@ cgexpr(Cg *c, Node *n, Local *locals) * same-leaf collision. */ ins2(c, A_LEAQ, mahint(c, opnd->str, - opnd->lhs->str), + use_hint(opnd->lhs->str)), areg(D_AX)); break; } @@ -10181,7 +10231,8 @@ cgexpr(Cg *c, Node *n, Local *locals) * the bareword as the hint so cross-module * same-leaf exports resolve correctly. */ ins1(c, A_CALL, - mafn(c, n->lhs->str, n->lhs->lhs->str)); + mafn(c, n->lhs->str, + use_hint(n->lhs->lhs->str))); } else { cgexpr(c, n->lhs, locals); /* AX = fn ptr */ ins1(c, A_CALL, areg(D_AX)); @@ -11057,7 +11108,8 @@ cgexpr(Cg *c, Node *n, Local *locals) /* `mod.fn` address-of via N_DOT — pass the * module bareword as the disambiguation hint. */ ins2(c, A_LEAQ, - mafn(c, n->str, n->lhs->str), areg(D_AX)); + mafn(c, n->str, use_hint(n->lhs->str)), + areg(D_AX)); break; } { @@ -11073,7 +11125,7 @@ cgexpr(Cg *c, Node *n, Local *locals) for (s = sdefs; s; s = s->next) { if (strcmp(s->name, n->str) != 0) continue; - if (sdef_mod_match_hint(s, n->lhs->str)) + if (sdef_mod_match_hint(s, use_hint(n->lhs->str))) break; } if (s == NULL) { @@ -11105,10 +11157,12 @@ cgexpr(Cg *c, Node *n, Local *locals) * branch above already uses n->lhs->str via mafn. */ if (mqop == A_MOVQ) { ins2(c, A_MOVQ, - mahint(c, n->str, n->lhs->str), areg(D_AX)); + mahint(c, n->str, use_hint(n->lhs->str)), + areg(D_AX)); } else { ins2(c, A_LEAQ, - mahint(c, n->str, n->lhs->str), areg(D_CX)); + mahint(c, n->str, use_hint(n->lhs->str)), + areg(D_CX)); ins2(c, mqop, amem(D_CX, 0), areg(D_AX)); } goto dot_done; @@ -14920,8 +14974,15 @@ cgfn(Cg *c, FILE *out, Node *fn) /* TEXT directive comes first; framesize is filled at the end. */ Prog *text = newprog(c, A_TEXT); /* Mangle the label using the fn's own module as the hint — picks - * the right entry when multiple modules export the same leaf. */ - text->to = mafn(c, fn->str, c->cur_mod); + * the right entry when multiple modules export the same leaf. M1 #32: + * the ROOT-unit main (imported==0) is the bare `_start` entry — emit + * it bare directly, mirroring mod_collect's skip; without this its + * NULL cur_mod would fall through mafn's first-leaf match onto an + * IMPORTED package's now-registered `pkg.main`. */ + if (fn->str && strcmp(fn->str, "main") == 0 && !fn->imported) + text->to = asym("main"); + else + text->to = mafn(c, fn->str, c->cur_mod); text->from.offset = 0; /* framesize patched below */ emit(c, text); @@ -15988,7 +16049,7 @@ node_fnptr_sym(Cg *c, Node *ev) return NULL; Type *du = type_chase_named(opnd->type); if (du == NULL || du->kind != TY_FN) return NULL; - return mod_mangle_fn(c, opnd->str, opnd->lhs->str); + return mod_mangle_fn(c, opnd->str, use_hint(opnd->lhs->str)); } if (opnd == NULL || opnd->kind != N_IDENT) return NULL; Type *ou = type_chase_named(opnd->type); diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 4c5bbbf8..a40347e7 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -60,6 +60,7 @@ lookup_builtin(const char *name) } static const char *decl_mod(Node *file, Node *d); +static const char *use_path(Node *file, const char *curmod, const char *alias); static void resolve_typedecl(Checker *c, Node *d); static Type * @@ -84,9 +85,15 @@ resolve_typename(Checker *c, Node *n) size_t hl = (size_t)(dot - nm); if (hl < sizeof head) memcpy(head, nm, hl); Sym *m = scope_lookup(c->cur, head); - if (m && (m->kind == SK_USE || m->use_alias)) - s = scope_lookup_in_module(c->cur, head, + if (m && (m->kind == SK_USE || m->use_alias)) { + /* M1 #22: map the qualifier alias to its dotted + * import path (symbols are path-keyed). */ + const char *mk = use_path(c->file, c->cur_mod, + head); + if (mk == NULL) mk = head; + s = scope_lookup_in_module(c->cur, mk, dot + 1); + } } } if (s == NULL || s->kind != SK_TYPE) @@ -599,7 +606,10 @@ eval_def_const(Checker *c, Node *n, u64 *out, int depth) } case N_DOT: { if (n->lhs == NULL || n->lhs->kind != N_IDENT) return 0; - Sym *s = scope_lookup_in_module(c->cur, n->lhs->str, n->str); + /* M1 #22: map the qualifier alias to its dotted import path. */ + const char *mk = use_path(c->file, c->cur_mod, n->lhs->str); + if (mk == NULL) mk = n->lhs->str; + Sym *s = scope_lookup_in_module(c->cur, mk, n->str); if (s == NULL || s->kind != SK_DEF || s->decl == NULL || s->decl->rhs == NULL) return 0; @@ -1343,8 +1353,14 @@ cexpr(Checker *c, Node *n) * same-leaf-name types from different * imports (`bufio.stream`/`io.stream`) * disambiguate to the right one. */ + /* M1 #22: symbols are keyed on the dotted + * import path; map the alias the user wrote to + * that path before looking up the leaf. */ + const char *mk = use_path(c->file, c->cur_mod, + n->lhs->str); + if (mk == NULL) mk = n->lhs->str; Sym *fs = scope_lookup_in_module(c->cur, - n->lhs->str, n->str); + mk, n->str); if (fs) return n->type = fs->type; if (ms->kind == SK_USE) { @@ -2671,13 +2687,52 @@ decl_mod(Node *file, Node *d) { if (d == NULL || d->module == NULL || file == NULL) return NULL; for (Node *u = file->list; u; u = u->next) { - if (u->kind == N_USE && u->str - && strcmp(u->str, d->module) == 0) + /* M1 #22: a decl is imported iff some `use` directive's full + * dotted import path equals the decl's module (now the path, + * not the leaf). For single-level packages usepath == leaf so + * this is unchanged; nested packages (`encoding.utf8`) match + * here instead of on the bare leaf. */ + if (u->kind == N_USE && u->usepath + && strcmp(u->usepath, d->module) == 0) return d->module; } return NULL; } +/* + * use_path — map a `use` alias (leaf bareword the user writes, `utf8`) + * to the full dotted import path it binds (`encoding.utf8`), for the + * module-qualified resolution and the codegen hint (M1 #22, §2.4). For + * single-level packages usepath == alias so the result is unchanged. + * + * The alias→path map is NOT file-global: two modules in the same + * concatenated unit may bind the same leaf alias to different paths + * (sha256's `import crypto.math` and strconv's `import math` both bind + * alias `math`). The import declared in the SAME module as the + * reference (`curmod`) is the authoritative one; preferring it closes + * the cross-module mis-resolution a first-match scan caused. Falls back + * to any matching alias when the referencing module has no own import + * (single-occurrence case, unchanged). Returns NULL if no such `use`. + */ +static const char * +use_path(Node *file, const char *curmod, const char *alias) +{ + if (file == NULL || alias == NULL) return NULL; + const char *any = NULL; + for (Node *u = file->list; u; u = u->next) { + if (u->kind != N_USE || u->str == NULL + || strcmp(u->str, alias) != 0) + continue; + const char *p = u->usepath ? u->usepath : u->str; + int same = (u->module == NULL) ? (curmod == NULL) + : (curmod != NULL && strcmp(u->module, curmod) == 0); + if (same) + return p; + if (any == NULL) any = p; + } + return any; +} + /* * resolve_typedecl — resolve d's body into its installed TY_NAMED * placeholder. Reached from check_file's typedecl pass AND on demand @@ -2739,7 +2794,7 @@ src_imports(Node *file, const char *modtag, const char *name) * even though its module tag is also "fmt"; that directive * doesn't introduce a foreign module bareword and lib/fmt's * own `fn bsprintf(fmt: str, ...)` is not a shadow of it. */ - if (u->module && u->str && strcmp(u->module, u->str) == 0) + if (u->module && u->usepath && strcmp(u->module, u->usepath) == 0) continue; /* decl_mod normalises the raw `// MODULE:` tag back to NULL * for primary-source N_USEs (the primary's own tag won't @@ -2817,7 +2872,11 @@ check_file(Checker *c, Node *file) * (b)/(d) membership DEFERRED to task #8 (filename- * keyed pulls lack import->file->symbol provenance). */ const char *owner = decl_mod(file, d); - if (owner && owner[0] && strcmp(d->str, owner) == 0) + /* M1 #22: self-import ⟺ the imported path equals the + * use's own (owning) module path. Compares paths, not + * leaves, so nested packages are caught too. */ + if (owner && owner[0] && d->usepath + && strcmp(d->usepath, owner) == 0) err(c, d->pos, "self-import: package " "'%s' cannot import itself", owner); Sym *prev = scope_lookup_local(c->cur, d->str); @@ -2962,16 +3021,14 @@ check_file(Checker *c, Node *file) } c->cur_mod = NULL; - /* Program-global, name-only, cross-module uniqueness on `main`. - * `main` lowers to ONE bare entry symbol, so a second top-level - * decl named `main` (any kind, any package) collides with the - * entry at link time — today a silent segfault / link-fail in - * both stages. The (name, module) duplicate rejects above read a - * cross-package `foo.main` and the bare entry as distinct, so they - * miss this. Correct multi-main mangling (entry stays bare, the - * rest qualify) is deferred (task #32); reject loudly meanwhile - * (rule 7). Walks USER decls only — runs before the -T synth main - * is appended below — so a hosted-test build never false-counts. */ + /* Program-global uniqueness on the ENTRY `main`. M1 #32: the entry + * is the ROOT-unit main (imported==0) — it alone lowers to the bare + * `main` symbol w6l's _start calls. An IMPORTED package's `main` + * (imported==1) mangles on its path (`foo.bar.main`) and may coexist + * — closing the old dup-main collision by construction (#31). Two + * ROOT entries still collide on the bare symbol → reject loud (rule + * 7). Walks USER decls only — runs before the -T synth main is + * appended below — so a hosted-test build never false-counts. */ { Node *firstmain = NULL; for (Node *d = file->list; d; d = d->next) { @@ -2980,12 +3037,14 @@ check_file(Checker *c, Node *file) if (d->kind != N_FNDECL && d->kind != N_LET && d->kind != N_DEF && d->kind != N_TYPEDECL) continue; + if (d->imported) + continue; if (firstmain == NULL) { firstmain = d; continue; } - err(c, d->pos, "duplicate top-level main: only the " - "entry main may exist (task #32)"); + err(c, d->pos, "duplicate entry main: only one root " + "main may exist (#32)"); } } diff --git a/cmd/wcc/lex.c b/cmd/wcc/lex.c index 46b2d940..7fe2d69a 100644 --- a/cmd/wcc/lex.c +++ b/cmd/wcc/lex.c @@ -102,14 +102,38 @@ skipws(Lex *l) * then skipped like any comment. Mirrors the removed * `// MODULE:` lexer directive. */ { - static const char dir[] = "ww:module-reset"; + static const char pre[] = "ww:module"; size_t i = 0; - while (dir[i] && lpeek(l, i) == dir[i]) + while (pre[i] && lpeek(l, i) == pre[i]) i++; - if (dir[i] == '\0') { + if (pre[i] == '\0') { int nx = lpeek(l, i); - if (nx == '\n' || nx < 0) - l->modreset = 1; + if (nx == '-') { + static const char rest[] = "-reset"; + size_t j = 0; + while (rest[j] && lpeek(l, i + j) == rest[j]) + j++; + if (rest[j] == '\0') { + int af = lpeek(l, i + j); + if (af == '\n' || af < 0) + l->modreset = 1; + } + } else if (nx == ' ' || nx == '\t') { + /* `//ww:module ` — M1 import boundary. */ + size_t k = i; + while (lpeek(l, k) == ' ' + || lpeek(l, k) == '\t') + k++; + size_t s = k; + int ch; + while ((ch = lpeek(l, k)) >= 0 + && ch != '\n' && ch != '\r' + && ch != ' ' && ch != '\t') + k++; + if (k > s) + l->modpath = astrndup(l->a, + l->src + l->pos + s, k - s); + } } } while ((c = lpeek(l, 0)) >= 0 && c != '\n') @@ -403,6 +427,13 @@ lexnext(Lex *l) /* A `//ww:module-reset` seen in the skipped run surfaces as its own * token before the next real one (#16 option-B boundary reset). */ if (l->modreset) { l->modreset = 0; EMIT(TK_MODRESET); } + if (l->modpath) { + const char *mp = l->modpath; + l->modpath = NULL; + Tok _t = (Tok){ TK_MODPATH, start, NULL, 0, {0}, TK_NONE }; + _t.text = mp; _t.tlen = strlen(mp); + return _t; + } if (!more) { Tok t = (Tok){ TK_EOF, start, "", 0, {0}, TK_NONE }; return t; diff --git a/cmd/wcc/parse.c b/cmd/wcc/parse.c index 64869b69..69dc8945 100644 --- a/cmd/wcc/parse.c +++ b/cmd/wcc/parse.c @@ -1253,11 +1253,24 @@ parseuse(Parser *p) Pos pp = p->cur.pos; expect(p, TK_USE); Node *n = newnode(p->a, N_USE, pp); + /* M1 #22: accumulate the full dotted import path (n->module) so the + * checker can match decl identity on the path, while n->str stays the + * leaf alias the user writes (`utf8.x`). */ + char pathbuf[256]; + size_t pl = 0; const char *leaf = expectident(p); - while (accept(p, TK_DOT)) + for (size_t i = 0; leaf[i] && pl + 1 < sizeof pathbuf; i++) + pathbuf[pl++] = leaf[i]; + while (accept(p, TK_DOT)) { leaf = expectident(p); + if (pl + 1 < sizeof pathbuf) pathbuf[pl++] = '.'; + for (size_t i = 0; leaf[i] && pl + 1 < sizeof pathbuf; i++) + pathbuf[pl++] = leaf[i]; + } + pathbuf[pl] = '\0'; n->str = leaf; n->strlen = strlen(leaf); + n->usepath = astrndup(p->a, pathbuf, pl); expect(p, TK_SEMI); return n; } @@ -1360,7 +1373,32 @@ parsefile(Parser *p) advance(p); const char *name = expectident(p); expect(p, TK_SEMI); - p->curmod = name; + if (p->pathmod != NULL) { + /* M1 #22: while an import path is active the + * in-file `package` clause is an ASSERTION — its + * leaf must equal the path's last component; it + * does NOT overwrite the path-derived module. */ + const char *dot = strrchr(p->pathmod, '.'); + const char *last = dot ? dot + 1 : p->pathmod; + if (strcmp(name, last) != 0) { + errorf(p->cur.pos, + "package %s does not match import path %s", + name, p->pathmod); + p->errs++; + } + } else { + p->curmod = name; + } + continue; + } + /* `//ww:module ` — M1 #22 import boundary. The following + * file's decls mangle on the full dotted import path, not the + * leaf `package` clause, and are flagged imported (gates the + * root-only bare-`main` rule, #32). */ + if (p->cur.kind == TK_MODPATH) { + p->pathmod = p->cur.text; + p->curmod = p->cur.text; + advance(p); continue; } /* `//ww:module-reset` — bundle boundary before a package-less @@ -1374,6 +1412,7 @@ parsefile(Parser *p) if (p->cur.kind == TK_MODRESET) { advance(p); p->curmod = NULL; + p->pathmod = NULL; continue; } Node *attrs = parseattrs(p); @@ -1399,7 +1438,10 @@ parsefile(Parser *p) advance(p); continue; } - if (d != NULL) d->module = p->curmod; + if (d != NULL) { + d->module = p->curmod; + d->imported = (p->pathmod != NULL); + } if (head == NULL) head = d; else tail->next = d; tail = d; diff --git a/cmd/wcc/tok.c b/cmd/wcc/tok.c index 5f92bacc..15fc4d49 100644 --- a/cmd/wcc/tok.c +++ b/cmd/wcc/tok.c @@ -104,6 +104,7 @@ tokname(Tkind k) case TK_ENUM: return "enum"; case TK_MODULE: return "package"; case TK_MODRESET: return "//ww:module-reset"; + case TK_MODPATH: return "//ww:module"; case TK_LPAREN: return "("; case TK_RPAREN: return ")"; diff --git a/cmd/wcc/ww.h b/cmd/wcc/ww.h index b0a89f9d..8ebeffa1 100644 --- a/cmd/wcc/ww.h +++ b/cmd/wcc/ww.h @@ -184,6 +184,10 @@ typedef enum { * curmod to NULL before a package-less file's bytes * (#16 option-B; the package-less-entry attribution fix * that replaces the withdrawn `package main` inject). */ + TK_MODPATH, /* `//ww:module ` — driver import boundary: + * the following file's decls mangle on the full import + * path, not the leaf `package` clause (M1 #22). Token + * text carries the dotted path. */ TK_LAST /* sentinel for tables */ } Tkind; @@ -214,6 +218,9 @@ struct Lex { int modreset; /* a `//ww:module-reset` directive was seen in * the last skipped run; lexnext emits TK_MODRESET * before the next real token. */ + const char *modpath; /* a `//ww:module ` directive was seen in + * the last skipped run; lexnext emits TK_MODPATH + * carrying this dotted path (M1 #22). */ }; void lexinit(Lex*, Arena*, const char *file, const char *src, u64 len); @@ -340,7 +347,17 @@ struct Node { * decl's section in combined.ww. * NULL for nested nodes; only top- * level decls (fn/def/type/let) - * carry it. */ + * carry it. On an N_USE node this is + * the importing (owning) module. */ + const char *usepath; /* M1 #22: on an N_USE node, the full + * dotted IMPORT path (`encoding.utf8`) + * vs the leaf alias in `str`. Drives + * the path-keyed decl_mod match and the + * qualified-ref codegen hint. */ + int imported; /* M1 #22: decl reached through an + * `//ww:module ` import boundary + * (vs root/primary). Gates the root-only + * bare-`main` rule (#32). */ }; Node *newnode(Arena*, Nkind, Pos); @@ -358,6 +375,10 @@ struct Parser { const char *curmod; /* most-recent `module foo;` declaration — * stamped onto each top-level decl that * follows. */ + const char *pathmod; /* M1 #22: active `//ww:module ` dotted + * import path; while set, decls stamp + * module=pathmod and imported=1, and the + * in-file `package` clause is an assertion. */ }; void parserinit(Parser*, Arena*, Lex*); diff --git a/cmd/ww/main.c b/cmd/ww/main.c index a22af885..7ac93804 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -218,7 +218,7 @@ enumerate_dir_ww(const char *dirpath, char ***out_files) } static void expand(FILE *out, const char *path, struct ImportSet *visited, - const char *libdir); + const char *libdir, const char *modpath); /* Scan `path` for its first non-comment-non-blank line; if it starts * with `package ;` write the name into `out` (NUL-terminated) @@ -299,7 +299,7 @@ unit_has_package(const char *path, const char *leaf) * dir-enum). */ static void expand_dir(FILE *out, const char *dirpath, struct ImportSet *visited, - const char *libdir) + const char *libdir, const char *modpath) { char **files = NULL; int n = enumerate_dir_ww(dirpath, &files); @@ -318,7 +318,7 @@ expand_dir(FILE *out, const char *dirpath, struct ImportSet *visited, exit(1); } } - expand(out, fp, visited, libdir); + expand(out, fp, visited, libdir, modpath); free(files[i]); } free(files); @@ -331,7 +331,7 @@ expand_dir(FILE *out, const char *dirpath, struct ImportSet *visited, * it). */ static void expand(FILE *out, const char *path, struct ImportSet *visited, - const char *libdir) + const char *libdir, const char *modpath) { if (import_seen(visited, path)) return; import_add(visited, path); @@ -377,8 +377,13 @@ expand(FILE *out, const char *path, struct ImportSet *visited, fprintf(stderr, "ww: cannot find package %s\n", name); exit(1); } - if (is_dir) expand_dir(out, ipath, visited, libdir); - else expand(out, ipath, visited, libdir); + /* M1 #22 (isdir-gated, rob-ratified): a package IS a directory, so + * only DIRECTORY imports are package boundaries that path-mangle. + * A single-file import (`import opcodes;` → opcodes.ww declaring + * `package w6a`) is an intra-package file-split — it keeps its + * in-file `package` clause as its module (no directive). */ + if (is_dir) expand_dir(out, ipath, visited, libdir, name); + else expand(out, ipath, visited, libdir, NULL); } /* #16 option-B: a package-less file's decls would otherwise inherit @@ -389,10 +394,19 @@ expand(FILE *out, const char *path, struct ImportSet *visited, * `package main`, which would main-prefix them). A packaged file's own * `package` decl already sets curmod, so it needs nothing — keeping * the directive out of every tracked combined.ww. (Task #11.) */ - { - char pkg[128]; - if (!peek_package(path, pkg, sizeof pkg)) - fputs("//ww:module-reset\n", out); + if (modpath != NULL && modpath[0] != '\0') { + /* M1 (#22): an import-reached file carries its full dotted + * import path so codegen mangles symbols on the path, not the + * leaf `package` clause. The directive's absence is the root + * marker (#32): root/primary files take the branch below. */ + fprintf(out, "//ww:module %s\n", modpath); + } else { + /* Root/primary file: reset the bundle boundary so a preceding + * imported section's sticky pathmod (M1 #22) is cleared. A + * package-less file then stays primary ("") as before; a + * packaged primary's own `package` clause sets curmod fresh + * (pathmod now NULL → real clause, not an assertion). */ + fputs("//ww:module-reset\n", out); } rewind(in); int ch; @@ -505,12 +519,12 @@ build_one(const char *src, int entry_is_dir, const char *out, char tpath[1024]; int tdir = 0; if (locate_import(srcdir, "test", tpath, sizeof tpath, &tdir)) { - if (tdir) expand_dir(cf, tpath, &visited, srcdir); - else expand(cf, tpath, &visited, srcdir); + if (tdir) expand_dir(cf, tpath, &visited, srcdir, "test"); + else expand(cf, tpath, &visited, srcdir, NULL); } } - if (entry_is_dir) expand_dir(cf, srcd, &visited, srcdir); - else expand(cf, src, &visited, srcdir); + if (entry_is_dir) expand_dir(cf, srcd, &visited, srcdir, NULL); + else expand(cf, src, &visited, srcdir, NULL); fclose(cf); for (int i = 0; i < visited.n; i++) free(visited.paths[i]); free(visited.paths); diff --git a/lib/ww/ast.ww b/lib/ww/ast.ww index e6e6ac18..cdc0b900 100644 --- a/lib/ww/ast.ww +++ b/lib/ww/ast.ww @@ -137,13 +137,17 @@ type node = struct { type_: *void, // filled in by checker; type.ww treats it as *tinfo tsuffix: str, // typed numeric literal suffix ("i32", "u64", ...) nmod: str, // originating module from `// MODULE: foo`; "" if none + usepath: str, // on an N_USE: full dotted IMPORT path vs leaf alias + // in `str` (M1 #22); "" otherwise + imported: i32, // M1 #22: decl reached via `//ww:module ` import + // boundary (vs root/primary); gates root-only bare main }; export fn newnode(k: nkind, file: str, line: i32, col: i32) *node = { // fval cast-init: 990's wwdump TK_FLOAT diff requires this file // to tokenise identically through C and ww (lex.ww:382 has the // same workaround for the cstage %g-formats vs ww-skips divergence). - let n: *node = alloc(node{kind=k, file=file, line=line, col=col, op=tkind.TK_NONE, str="", uval=0u64, fval=0: f64, lhs=nil, rhs=nil, cond=nil, body=nil, els=nil, list=nil, next=nil, attr=nil, exported=0, type_=nil, tsuffix="", nmod=""})!; + let n: *node = alloc(node{kind=k, file=file, line=line, col=col, op=tkind.TK_NONE, str="", uval=0u64, fval=0: f64, lhs=nil, rhs=nil, cond=nil, body=nil, els=nil, list=nil, next=nil, attr=nil, exported=0, type_=nil, tsuffix="", nmod="", usepath="", imported=0})!; return n; }; diff --git a/lib/ww/lex/lex.ww b/lib/ww/lex/lex.ww index f71dc29b..fd4b0f5d 100644 --- a/lib/ww/lex/lex.ww +++ b/lib/ww/lex/lex.ww @@ -59,6 +59,10 @@ type lex = struct { // a `//ww:module-reset` directive was seen in the last skipped run; // lexnext emits TK_MODRESET before the next real token (#16 opt-B). modreset: i32, + // a `//ww:module ` directive was seen in the last skipped run; + // lexnext emits TK_MODPATH carrying this dotted path (M1 #22). + modpathset: i32, + modpath: str, }; export fn lexinit(l: *lex, file: str, src: *u8, len: u64) void = { @@ -70,6 +74,7 @@ export fn lexinit(l: *lex, file: str, src: *u8, len: u64) void = { l.col = 1; l.errs = 0; l.modreset = 0; + l.modpathset = 0; }; // srcb — byte at offset; helper that lifts the cast out of indexing. @@ -137,19 +142,62 @@ fn skipws(l: *lex) bool = { // The body is then skipped like any comment. // Mirrors cstage lex.c skipws. Compare via lpeek // (no consume) so the skip loop below is unchanged. - let dir: str = "ww:module-reset"; + let pre: str = "ww:module"; let di: i32 = 0; let matched: bool = true; - for (di < dir.len) { - if (lpeek(l, di: u64) != dir[di]: i32) { + for (di < pre.len) { + if (lpeek(l, di: u64) != pre[di]: i32) { matched = false; break; }; di += 1; }; if (matched) { - let nx: i32 = lpeek(l, dir.len: u64); - if (nx == '\n') { l.modreset = 1; } - else { if (nx < 0) { l.modreset = 1; }; }; + let nx: i32 = lpeek(l, pre.len: u64); + if (nx == '-') { + let rest: str = "-reset"; + let rj: i32 = 0; + let rm: bool = true; + for (rj < rest.len) { + if (lpeek(l, (pre.len + rj): u64) + != rest[rj]: i32) { + rm = false; break; + }; + rj += 1; + }; + if (rm) { + let af: i32 = lpeek(l, + (pre.len + rest.len): u64); + if (af == '\n') { l.modreset = 1; } + else { if (af < 0) { l.modreset = 1; }; }; + }; + } else { if (nx == ' ' || nx == '\t') { + // `//ww:module ` — M1 import boundary. + let k: u64 = pre.len: u64; + for (true) { + let sc: i32 = lpeek(l, k); + if (sc == ' ' || sc == '\t') { + k += 1u64; continue; + }; + break; + }; + let s0: u64 = k; + for (true) { + let pc: i32 = lpeek(l, k); + if (pc < 0) { break; }; + if (pc == '\n' || pc == '\r' + || pc == ' ' || pc == '\t') { + break; + }; + k += 1u64; + }; + if (k > s0) { + let view: str; + view.ptr = l.src + l.lpos + s0; + view.len = (k - s0): i32; + l.modpath = strings.dup(view); + l.modpathset = 1; + }; + }; }; }; for (true) { let cx: i32 = lpeek(l, 0u64); @@ -663,6 +711,12 @@ export fn lexnext(l: *lex, out: *tok) void = { emitsimple(&start, tkind.TK_MODRESET, out); return; }; + if (l.modpathset != 0) { + l.modpathset = 0; + emitsimple(&start, tkind.TK_MODPATH, out); + out.text = l.modpath; + return; + }; if (!more) { emitsimple(&start, tkind.TK_EOF, out); return; diff --git a/lib/ww/lex/tok.ww b/lib/ww/lex/tok.ww index b136f387..cafd8557 100644 --- a/lib/ww/lex/tok.ww +++ b/lib/ww/lex/tok.ww @@ -121,7 +121,10 @@ type tkind = enum i32 { TK_MODRESET = 87, // `//ww:module-reset` driver bundle boundary: // reset curmod to "" before a package-less file // (#16 option-B; cstage TK_MODRESET twin) - TK_LAST = 88, + TK_MODPATH = 88, // `//ww:module ` driver import + // boundary; decls mangle on the path, not the + // leaf `package` clause (M1 #22; cstage twin) + TK_LAST = 89, }; // ---- Pos / Tok -------------------------------------------------------- @@ -244,6 +247,7 @@ export fn tokname(k: tkind) str = { case tkind.TK_ENUM: return "enum"; case tkind.TK_MODULE: return "package"; case tkind.TK_MODRESET: return "//ww:module-reset"; + case tkind.TK_MODPATH: return "//ww:module"; case tkind.TK_LPAREN: return "("; case tkind.TK_RPAREN: return ")"; diff --git a/lib/ww/lex/toktest.ww b/lib/ww/lex/toktest.ww index 776d1aca..f338356c 100644 --- a/lib/ww/lex/toktest.ww +++ b/lib/ww/lex/toktest.ww @@ -129,11 +129,13 @@ fn checkname(k: tkind, want: str) void = { checkname(tkind.TK_FATARROW, "=>"); checkname(tkind.TK_MODRESET, "//ww:module-reset"); + checkname(tkind.TK_MODPATH, "//ww:module"); checkname(tkind.TK_LAST, ""); // Unknown kind → the post-switch fallback. TK_LAST is the highest - // named value (88); 89 is out of band, exercising the "" tail. - checkname(89: tkind, ""); + // named value (89, after TK_MODPATH=88 landed); 90 is out of band, + // exercising the "" tail. + checkname(90: tkind, ""); }; fn checkkw(s: str, want: tkind) void = { diff --git a/lib/ww/parse/decl.ww b/lib/ww/parse/decl.ww index b067dbee..7ac99a1c 100644 --- a/lib/ww/parse/decl.ww +++ b/lib/ww/parse/decl.ww @@ -3,6 +3,7 @@ package parse; import os; +import strings; import tok; // `import encoding.utf8;` — the driver resolves the dotted path to @@ -17,13 +18,19 @@ fn parseuse(p: *parser) *node = { advance(p); // past `use` let n: *node = newnode(nkind.N_USE, pf, pl, pc); n.nmod = p.curmod; + // M1 #22: accumulate the full dotted import path (n.usepath) for the + // checker's path-keyed module match; n.str stays the leaf alias the + // user writes (`utf8.x`). let leaf: str; expectident(p, &leaf); + let path: str = leaf; for (p.curkind == tkind.TK_DOT) { advance(p); // past `.` expectident(p, &leaf); + path = strings.concat(path, ".", leaf); }; n.str = leaf; + n.usepath = path; expecttok(p, tkind.TK_SEMI, "expected ';' after use"); return n; }; diff --git a/lib/ww/parse/parse.ww b/lib/ww/parse/parse.ww index 9dc830c2..2fe9b738 100644 --- a/lib/ww/parse/parse.ww +++ b/lib/ww/parse/parse.ww @@ -16,6 +16,7 @@ package parse; // dir-enum when callers `import parse;` (which dir-enums // lib/ww/parse/). import os; +import strings; import tok; type parser = struct { @@ -43,6 +44,10 @@ type parser = struct { // multi-file streams successive `module` decls mark per-file // section boundaries. Mirrors cstage Parser.curmod. curmod: str, + // M1 #22: active `//ww:module ` dotted import path. While set, + // decls stamp nmod=pathmod and imported=1, and the in-file `package` + // clause is an assertion. "" means inactive (root/primary). + pathmod: str, }; fn refill(p: *parser) void = { @@ -62,6 +67,7 @@ export fn parserinit(p: *parser, l: *lex) void = { p.l = l; p.errs = 0; p.nocast = 0; + p.pathmod = ""; refill(p); }; @@ -408,7 +414,30 @@ export fn parsefile(p: *parser) *node = { let name: str; expectident(p, &name); expecttok(p, tkind.TK_SEMI, "expected ';' after module name"); - p.curmod = name; + if (p.pathmod.len != 0) { + // M1 #22: while an import path is active the in-file + // `package` clause is an ASSERTION — its leaf must + // equal the path's last component; it does NOT + // overwrite the path-derived module. + let (pre, post) = strings.rcut(p.pathmod, "."); + let last: str = post; + if (post.len == 0) { last = p.pathmod; }; + if (strings.compare(name, last) != 0) { + errmsg(p, "package does not match import path"); + }; + } else { + p.curmod = name; + }; + continue; + }; + // `//ww:module ` — M1 #22 import boundary. The following + // file's decls mangle on the full dotted import path, not the + // leaf `package` clause, and are flagged imported (gates the + // root-only bare-`main` rule, #32). + if (p.curkind == tkind.TK_MODPATH) { + p.pathmod = p.curtext; + p.curmod = p.curtext; + advance(p); continue; }; // `//ww:module-reset` — bundle boundary before a package-less @@ -425,6 +454,7 @@ export fn parsefile(p: *parser) *node = { empty.ptr = nil; empty.len = 0; p.curmod = empty; + p.pathmod = ""; continue; }; let attrs = parseattrs(p); @@ -476,6 +506,9 @@ export fn parsefile(p: *parser) *node = { };};};};};}; if (d != nil) { + // M1 #22: flag decls reached via an import-path boundary + // (gates the root-only bare-`main` rule, #32). + if (p.pathmod.len != 0) { d.imported = 1; }; if (head == nil) { head = d; tail = d; diff --git a/selfhost/cmd/w6a/main.combined.ww b/selfhost/cmd/w6a/main.combined.ww index 643d77ec..904ad05f 100644 --- a/selfhost/cmd/w6a/main.combined.ww +++ b/selfhost/cmd/w6a/main.combined.ww @@ -1,3 +1,4 @@ +//ww:module time // time — clocks, instants, durations. Mirrors Hare's lib/time // (ref/hare/time/duration.ha, instant.ha, arithm.ha, // +linux/functions.ha). Calendar / date / strftime / timezone / @@ -95,6 +96,7 @@ export fn compare(a: instant, b: instant) i8 = { return 0i8; }; +//ww:module rt // rt — runtime primitives exposed to ww programs. // Mirrors Hare's rt:: module placement (ref/hare/rt/). @@ -118,6 +120,7 @@ package rt; // a future task (task #39). ref/hare/rt/malloc.ha:27. @symbol("rt_malloc") export fn malloc(n: u64) *void; +//ww:module os // os — process and filesystem facade. The body of each call lands // either in libwwrt.a (rt_syscall trampoline) or libc bindings, // depending on how the program was linked. @@ -856,6 +859,7 @@ export fn exists(path: str) bool = { return r >= 0i64; }; +//ww:module types // types — integer limits. Mirrors Hare's types::limits (I8_MAX, …) // platform-fixed for amd64. Numeric helpers live in lib/math, matching // Hare's split between types::limits and math::. @@ -900,6 +904,7 @@ def UINTPTR_MAX: uintptr = U64_MAX: uintptr; def RUNE_MIN: rune = '\0'; +//ww:module bytes // bytes — slice operations over []u8. Mirrors Hare's bytes module // (ref/hare/bytes/) for the in-tree subset: search/equality/prefix // helpers used by lib/encoding, lib/bufio, lib/memio. @@ -1451,6 +1456,7 @@ export fn rcut(in: []u8, delim: (u8 | []u8)) ([]u8, []u8) = { }; }; +//ww:module encoding.utf8 // encoding/utf8 — UTF-8 encode/decode. Hare port; see // ref/hare/encoding/utf8/{types,rune,encode,decode,decodetable}.ha. // @@ -1908,6 +1914,7 @@ export fn position(d: *decoder) i32 = { }; +//ww:module strings // strings — operations over str ({ptr,len}). Hare port; see // ref/hare/strings/. // @@ -2860,6 +2867,7 @@ export fn rpad(s: str, p: rune, maxlen: i32) str = { return frombytes(buf); }; +//ww:module-reset // selfhost/cmd/w6a/opcodes.ww — types + constants shared across the // w6a port. Mirrors cmd/w6a/a.h and cmd/w6c/6.out.h. @@ -3086,6 +3094,7 @@ type asm_ = struct { errs: i32, }; +//ww:module-reset // selfhost/cmd/w6a/lex.ww — port of cmd/w6a/lex.c. // // Character-level helpers for w6a's line-oriented parser. The parser @@ -3164,6 +3173,7 @@ export fn parsenum(p: *u8, n: u64) (i64, u64) = { return v, i; }; +//ww:module-reset // selfhost/cmd/w6a/parse.ww — port of cmd/w6a/parse.c. // // Line-oriented parser for the asm subset emitted by w6c. @@ -3764,6 +3774,7 @@ export fn parse(a: *asm_) i32 = { return a.errs; }; +//ww:module-reset // selfhost/cmd/w6a/asm.ww — port of cmd/w6a/asm.c. // // Encode the parsed aprog list into amd64 machine bytes, appending to @@ -4620,6 +4631,7 @@ export fn encode(a: *asm_) i32 = { return a.errs; }; +//ww:module-reset // selfhost/cmd/w6a/obj.ww — port of cmd/w6a/obj.c. // // Emit a tiny ELF64 relocatable object. Layout (in file order): @@ -5033,6 +5045,7 @@ export fn emitelf(a: *asm_, fd: i32) i32 = { return 0; }; +//ww:module-reset // selfhost/cmd/w6a/main.ww — port of cmd/w6a/main.c. // // w6a = amd64 assembler. Read .s, parse, encode, emit ELF .o. diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 43cce039..afbf35ec 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -1,3 +1,4 @@ +//ww:module time // time — clocks, instants, durations. Mirrors Hare's lib/time // (ref/hare/time/duration.ha, instant.ha, arithm.ha, // +linux/functions.ha). Calendar / date / strftime / timezone / @@ -95,6 +96,7 @@ export fn compare(a: instant, b: instant) i8 = { return 0i8; }; +//ww:module rt // rt — runtime primitives exposed to ww programs. // Mirrors Hare's rt:: module placement (ref/hare/rt/). @@ -118,6 +120,7 @@ package rt; // a future task (task #39). ref/hare/rt/malloc.ha:27. @symbol("rt_malloc") export fn malloc(n: u64) *void; +//ww:module os // os — process and filesystem facade. The body of each call lands // either in libwwrt.a (rt_syscall trampoline) or libc bindings, // depending on how the program was linked. @@ -856,6 +859,7 @@ export fn exists(path: str) bool = { return r >= 0i64; }; +//ww:module types // types — integer limits. Mirrors Hare's types::limits (I8_MAX, …) // platform-fixed for amd64. Numeric helpers live in lib/math, matching // Hare's split between types::limits and math::. @@ -900,6 +904,7 @@ def UINTPTR_MAX: uintptr = U64_MAX: uintptr; def RUNE_MIN: rune = '\0'; +//ww:module bytes // bytes — slice operations over []u8. Mirrors Hare's bytes module // (ref/hare/bytes/) for the in-tree subset: search/equality/prefix // helpers used by lib/encoding, lib/bufio, lib/memio. @@ -1451,6 +1456,7 @@ export fn rcut(in: []u8, delim: (u8 | []u8)) ([]u8, []u8) = { }; }; +//ww:module encoding.utf8 // encoding/utf8 — UTF-8 encode/decode. Hare port; see // ref/hare/encoding/utf8/{types,rune,encode,decode,decodetable}.ha. // @@ -1908,6 +1914,7 @@ export fn position(d: *decoder) i32 = { }; +//ww:module strings // strings — operations over str ({ptr,len}). Hare port; see // ref/hare/strings/. // @@ -2860,6 +2867,7 @@ export fn rpad(s: str, p: rune, maxlen: i32) str = { return frombytes(buf); }; +//ww:module strconv // strconv — arbitrary-precision decimal engine for float↔string // conversion. Mirrors ref/hare/strconv/decimal.ha (Hare in turn ports // Go's lib/strconv/decimal.go). Pure integer arithmetic; no f32/f64 @@ -3182,6 +3190,7 @@ fn decimal_round(d: *decimal) u64 = { return n; }; +//ww:module math // floats — f64 classification, sign, bit-reinterpret core, and the f64 // decompose half (subnormal-normalize + frexp). Ported from // ref/hare/math/floats.ha (fold-1: classify/sign/bits; fold-2a: @@ -3456,6 +3465,7 @@ export fn frexpf64(n: f64) (f64, i64) = { return (mantissa, exp); }; +//ww:module math // math — numeric helpers. Subset of Hare's math::; only the absolute- // value pair for the signed integer types we currently care about. The // return type is unsigned so that abs(I32_MIN) doesn't overflow. @@ -3472,6 +3482,7 @@ export fn absi64(n: i64) u64 = { return n: u64; }; +//ww:module strconv // strconv — float→string via Ryū (shortest round-trippable decimal). // Mirrors ref/hare/strconv/ftos_ryu.ha (the algorithm core) + // ref/hare/strconv/ftos.ha:432 (the f64tos driver). Ryū: Ulf Adams, @@ -4266,6 +4277,7 @@ export fn f32tos(n: f32) str = { return r; }; +//ww:module strconv // strconv — Ryū float→string lookup tables + bit-count constants. // Mirrors ref/hare/strconv/ftos_ryu.ha:159-222 byte-exact. Pure data // fold (strconv #106 fold-5): no logic, consumed by ftos.ww's @@ -4377,6 +4389,7 @@ let POW5_TABLE: [26]u64 = [ 59604644775390625u64, 298023223876953125u64, ]; +//ww:module ascii // ascii — rune-class predicates and case folding for the ASCII range. // Matches Hare's ascii::isdigit family (rune-taking signature). Runes // outside 0..127 always answer `false`. The lexer hot path uses these @@ -4581,6 +4594,7 @@ export fn strupper_buf(s: str, buf: []u8) (str | nomem) = { return strings.frombytes(buf); }; +//ww:module strconv // strconv — string-to-float. Mirrors ref/hare/strconv/stof.ha // (Hare in turn adapts Go): Eisel-Lemire fast path [1] with the // Simple-Decimal-Conversion slow path [2] (decimal.ww) as fallback. @@ -5280,6 +5294,7 @@ export fn stof32(s: str, b: base) (f32 | invalid | overflow) = { return 0: invalid; // unreachable (path-cov) }; +//ww:module strconv // strconv — stof/ftos lookup tables. Mirrors ref/hare/strconv/stof_data.ha // byte-exact. Pure-data fold (strconv #106 fold-2, was fold-3 before drew // re-sequenced 2026-05-26): no logic, exercised transitively when fold-3's @@ -5979,6 +5994,7 @@ let powers_of_ten: [596][2]u64 = [ [0x73832EEC6FFF3111u64, 0xD226FC195C6A2F8Cu64], ]; +//ww:module strconv // strconv — number↔string conversions. // // Mirrors Hare's strconv:: surface. The *tos functions return a @@ -6397,6 +6413,7 @@ export fn strerror(e: error) str = { return strings.dup(""); }; +//ww:module-reset // lib/ww/lex/tok.ww — port of cmd/wcc/tok.c plus the Tkind / // Tok / Pos shapes from cmd/wcc/ww.h. // @@ -6520,7 +6537,10 @@ type tkind = enum i32 { TK_MODRESET = 87, // `//ww:module-reset` driver bundle boundary: // reset curmod to "" before a package-less file // (#16 option-B; cstage TK_MODRESET twin) - TK_LAST = 88, + TK_MODPATH = 88, // `//ww:module ` driver import + // boundary; decls mangle on the path, not the + // leaf `package` clause (M1 #22; cstage twin) + TK_LAST = 89, }; // ---- Pos / Tok -------------------------------------------------------- @@ -6643,6 +6663,7 @@ export fn tokname(k: tkind) str = { case tkind.TK_ENUM: return "enum"; case tkind.TK_MODULE: return "package"; case tkind.TK_MODRESET: return "//ww:module-reset"; + case tkind.TK_MODPATH: return "//ww:module"; case tkind.TK_LPAREN: return "("; case tkind.TK_RPAREN: return ")"; @@ -6797,6 +6818,7 @@ export fn tokprint(fd: i32, t: *tok) void = { fputcbyte(fd, '\n'); }; +//ww:module lex // lib/ww/lex/lex.ww — port of cmd/wcc/lex.c. // // The DFA, the helpers, and the order of decisions all mirror the C @@ -6858,6 +6880,10 @@ type lex = struct { // a `//ww:module-reset` directive was seen in the last skipped run; // lexnext emits TK_MODRESET before the next real token (#16 opt-B). modreset: i32, + // a `//ww:module ` directive was seen in the last skipped run; + // lexnext emits TK_MODPATH carrying this dotted path (M1 #22). + modpathset: i32, + modpath: str, }; export fn lexinit(l: *lex, file: str, src: *u8, len: u64) void = { @@ -6869,6 +6895,7 @@ export fn lexinit(l: *lex, file: str, src: *u8, len: u64) void = { l.col = 1; l.errs = 0; l.modreset = 0; + l.modpathset = 0; }; // srcb — byte at offset; helper that lifts the cast out of indexing. @@ -6936,19 +6963,62 @@ fn skipws(l: *lex) bool = { // The body is then skipped like any comment. // Mirrors cstage lex.c skipws. Compare via lpeek // (no consume) so the skip loop below is unchanged. - let dir: str = "ww:module-reset"; + let pre: str = "ww:module"; let di: i32 = 0; let matched: bool = true; - for (di < dir.len) { - if (lpeek(l, di: u64) != dir[di]: i32) { + for (di < pre.len) { + if (lpeek(l, di: u64) != pre[di]: i32) { matched = false; break; }; di += 1; }; if (matched) { - let nx: i32 = lpeek(l, dir.len: u64); - if (nx == '\n') { l.modreset = 1; } - else { if (nx < 0) { l.modreset = 1; }; }; + let nx: i32 = lpeek(l, pre.len: u64); + if (nx == '-') { + let rest: str = "-reset"; + let rj: i32 = 0; + let rm: bool = true; + for (rj < rest.len) { + if (lpeek(l, (pre.len + rj): u64) + != rest[rj]: i32) { + rm = false; break; + }; + rj += 1; + }; + if (rm) { + let af: i32 = lpeek(l, + (pre.len + rest.len): u64); + if (af == '\n') { l.modreset = 1; } + else { if (af < 0) { l.modreset = 1; }; }; + }; + } else { if (nx == ' ' || nx == '\t') { + // `//ww:module ` — M1 import boundary. + let k: u64 = pre.len: u64; + for (true) { + let sc: i32 = lpeek(l, k); + if (sc == ' ' || sc == '\t') { + k += 1u64; continue; + }; + break; + }; + let s0: u64 = k; + for (true) { + let pc: i32 = lpeek(l, k); + if (pc < 0) { break; }; + if (pc == '\n' || pc == '\r' + || pc == ' ' || pc == '\t') { + break; + }; + k += 1u64; + }; + if (k > s0) { + let view: str; + view.ptr = l.src + l.lpos + s0; + view.len = (k - s0): i32; + l.modpath = strings.dup(view); + l.modpathset = 1; + }; + }; }; }; for (true) { let cx: i32 = lpeek(l, 0u64); @@ -7462,6 +7532,12 @@ export fn lexnext(l: *lex, out: *tok) void = { emitsimple(&start, tkind.TK_MODRESET, out); return; }; + if (l.modpathset != 0) { + l.modpathset = 0; + emitsimple(&start, tkind.TK_MODPATH, out); + out.text = l.modpath; + return; + }; if (!more) { emitsimple(&start, tkind.TK_EOF, out); return; @@ -7578,6 +7654,7 @@ export fn lexnext(l: *lex, out: *tok) void = { out.text = strings.dup(view); }; +//ww:module-reset // lib/ww/ast.ww — port of cmd/wcc/ast.c (Node defs + printer). // // Status: AST printer is fully ported. Constructor `newnode` is here. @@ -7717,13 +7794,17 @@ type node = struct { type_: *void, // filled in by checker; type.ww treats it as *tinfo tsuffix: str, // typed numeric literal suffix ("i32", "u64", ...) nmod: str, // originating module from `// MODULE: foo`; "" if none + usepath: str, // on an N_USE: full dotted IMPORT path vs leaf alias + // in `str` (M1 #22); "" otherwise + imported: i32, // M1 #22: decl reached via `//ww:module ` import + // boundary (vs root/primary); gates root-only bare main }; export fn newnode(k: nkind, file: str, line: i32, col: i32) *node = { // fval cast-init: 990's wwdump TK_FLOAT diff requires this file // to tokenise identically through C and ww (lex.ww:382 has the // same workaround for the cstage %g-formats vs ww-skips divergence). - let n: *node = alloc(node{kind=k, file=file, line=line, col=col, op=tkind.TK_NONE, str="", uval=0u64, fval=0: f64, lhs=nil, rhs=nil, cond=nil, body=nil, els=nil, list=nil, next=nil, attr=nil, exported=0, type_=nil, tsuffix="", nmod=""})!; + let n: *node = alloc(node{kind=k, file=file, line=line, col=col, op=tkind.TK_NONE, str="", uval=0u64, fval=0: f64, lhs=nil, rhs=nil, cond=nil, body=nil, els=nil, list=nil, next=nil, attr=nil, exported=0, type_=nil, tsuffix="", nmod="", usepath="", imported=0})!; return n; }; @@ -7965,11 +8046,13 @@ export fn astprint(fd: i32, n: *node) void = { pr(fd, n, 0); }; +//ww:module parse // lib/ww/parse/decl.ww — declaration parsing, split out of parse.ww. package parse; import os; +import strings; import tok; // `import encoding.utf8;` — the driver resolves the dotted path to @@ -7984,13 +8067,19 @@ fn parseuse(p: *parser) *node = { advance(p); // past `use` let n: *node = newnode(nkind.N_USE, pf, pl, pc); n.nmod = p.curmod; + // M1 #22: accumulate the full dotted import path (n.usepath) for the + // checker's path-keyed module match; n.str stays the leaf alias the + // user writes (`utf8.x`). let leaf: str; expectident(p, &leaf); + let path: str = leaf; for (p.curkind == tkind.TK_DOT) { advance(p); // past `.` expectident(p, &leaf); + path = strings.concat(path, ".", leaf); }; n.str = leaf; + n.usepath = path; expecttok(p, tkind.TK_SEMI, "expected ';' after use"); return n; }; @@ -8147,6 +8236,7 @@ fn parsetypedecl(p: *parser, exported: i32) *node = { }; +//ww:module parse // lib/ww/parse/expr.ww — expression parsing, split out of parse.ww. package parse; @@ -8616,6 +8706,7 @@ fn parseexpr(p: *parser) *node = { }; +//ww:module parse // lib/ww/parse/parse.ww — port of cmd/wcc/parse.c (entry + plumbing). // // Split into Hare-style submodule: parse.ww (here) holds the parser @@ -8634,6 +8725,7 @@ package parse; // dir-enum when callers `import parse;` (which dir-enums // lib/ww/parse/). import os; +import strings; import tok; type parser = struct { @@ -8661,6 +8753,10 @@ type parser = struct { // multi-file streams successive `module` decls mark per-file // section boundaries. Mirrors cstage Parser.curmod. curmod: str, + // M1 #22: active `//ww:module ` dotted import path. While set, + // decls stamp nmod=pathmod and imported=1, and the in-file `package` + // clause is an assertion. "" means inactive (root/primary). + pathmod: str, }; fn refill(p: *parser) void = { @@ -8680,6 +8776,7 @@ export fn parserinit(p: *parser, l: *lex) void = { p.l = l; p.errs = 0; p.nocast = 0; + p.pathmod = ""; refill(p); }; @@ -9026,7 +9123,30 @@ export fn parsefile(p: *parser) *node = { let name: str; expectident(p, &name); expecttok(p, tkind.TK_SEMI, "expected ';' after module name"); - p.curmod = name; + if (p.pathmod.len != 0) { + // M1 #22: while an import path is active the in-file + // `package` clause is an ASSERTION — its leaf must + // equal the path's last component; it does NOT + // overwrite the path-derived module. + let (pre, post) = strings.rcut(p.pathmod, "."); + let last: str = post; + if (post.len == 0) { last = p.pathmod; }; + if (strings.compare(name, last) != 0) { + errmsg(p, "package does not match import path"); + }; + } else { + p.curmod = name; + }; + continue; + }; + // `//ww:module ` — M1 #22 import boundary. The following + // file's decls mangle on the full dotted import path, not the + // leaf `package` clause, and are flagged imported (gates the + // root-only bare-`main` rule, #32). + if (p.curkind == tkind.TK_MODPATH) { + p.pathmod = p.curtext; + p.curmod = p.curtext; + advance(p); continue; }; // `//ww:module-reset` — bundle boundary before a package-less @@ -9043,6 +9163,7 @@ export fn parsefile(p: *parser) *node = { empty.ptr = nil; empty.len = 0; p.curmod = empty; + p.pathmod = ""; continue; }; let attrs = parseattrs(p); @@ -9094,6 +9215,9 @@ export fn parsefile(p: *parser) *node = { };};};};};}; if (d != nil) { + // M1 #22: flag decls reached via an import-path boundary + // (gates the root-only bare-`main` rule, #32). + if (p.pathmod.len != 0) { d.imported = 1; }; if (head == nil) { head = d; tail = d; @@ -9107,6 +9231,7 @@ export fn parsefile(p: *parser) *node = { return f; }; +//ww:module parse // lib/ww/parse/stmt.ww — statement parsing, split out of parse.ww. package parse; @@ -9521,6 +9646,7 @@ fn parsestmt(p: *parser) *node = { }; +//ww:module-reset // lib/ww/typ.ww — port of cmd/wcc/type.c. // // Status: full structural port. The C version uses module-globals for @@ -10137,6 +10263,7 @@ export fn typeeq(a: *tinfo, b: *tinfo) bool = { return true; // primitives match by kind alone }; +//ww:module-reset // lib/ww/sym.ww — port of cmd/wcc/sym.c. // // Per-scope hashtable, chained to the parent. Lookup walks up. @@ -10472,6 +10599,7 @@ export fn scopesamekeysym(s: *scope, name: str, mod: str) *sym = { return nil; }; +//ww:module-reset // selfhost/cmd/wcc/check.ww — minimal port of cmd/wcc/check.c. // // Status: name-resolution + primitive-type seeding only. Full type @@ -10638,14 +10766,48 @@ fn declmod(file: *node, d: *node) str = { if (file == nil) { return empty; }; let u: *node = file.list; for (u != nil) { + // M1 #22: a decl is imported iff some `use` directive's full + // dotted import path equals the decl's module (now the path). + // Single-level packages have usepath == leaf so this is + // unchanged; nested (`encoding.utf8`) match here, not on leaf. if (u.kind == nkind.N_USE) { - if (streq(u.str, d.nmod)) { return d.nmod; }; + if (streq(u.usepath, d.nmod)) { return d.nmod; }; }; u = u.next; }; return empty; }; +// usepath — map a `use` alias (leaf bareword the user writes, `utf8`) +// to the full dotted import path it binds (`encoding.utf8`), for the +// module-qualified resolution and codegen hint (M1 #22). Single-level +// packages have usepath == alias so the result is unchanged. The +// current tree has one occurrence per leaf, so the map is unambiguous. +fn usepathfor(file: *node, alias: str) str = { + let empty: str; + if (file == nil) { return empty; }; + if (alias.len == 0) { return empty; }; + let u: *node = file.list; + for (u != nil) { + if (u.kind == nkind.N_USE) { + if (streq(u.str, alias)) { + if (u.usepath.len != 0) { return u.usepath; }; + return u.str; + }; + }; + u = u.next; + }; + return empty; +}; + +// modkeyfor — usepathfor with leaf-alias fallback: the module key for a +// path-keyed scopelookupinmodule given the alias the user wrote (M1 #22). +fn modkeyfor(c: *checker, alias: str) str = { + let mk: str = usepathfor(c.file, alias); + if (mk.len == 0) { return alias; }; + return mk; +}; + // srcimports — does the source file that contributed decl-module // `modtag` carry `use ;`? Mirrors cstage's src_imports — // `modtag.len == 0` means primary, matching declmod's empty-str @@ -10662,7 +10824,7 @@ fn srcimports(file: *node, modtag: str, name: str) bool = { // module bareword and lib/fmt's own // `fn bsprintf(fmt: str, ...)` is not a shadow. if (u.nmod.len > 0) { - if (streq(u.nmod, u.str)) { + if (streq(u.nmod, u.usepath)) { u = u.next; continue; }; @@ -10751,7 +10913,9 @@ fn installdecl(c: *checker, file: *node, d: *node) void = { // pulls lack import->file->symbol provenance). Message byte-identical // to cstage check.c. if (k == nkind.N_USE) { - if (mod.len != 0 && streq(nm, mod)) { + // M1 #22: self-import ⟺ the imported path equals the use's own + // (owning) module path. Compares paths, not leaves. + if (mod.len != 0 && streq(d.usepath, mod)) { cerr("self-import: package '"); cerr(mod); cerr("' cannot import itself\n"); c.errs += 1i32; }; @@ -10960,7 +11124,7 @@ fn resolvewalk(c: *checker, n: *node) void = { let leaf: str; leaf.ptr = nm.ptr + (dot + 1): u64; leaf.len = nm.len - (dot + 1); - s = scopelookupinmodule(c.cur, head, leaf); + s = scopelookupinmodule(c.cur, modkeyfor(c, head), leaf); }; }; }; @@ -11358,7 +11522,7 @@ fn aliassym(c: *checker, n: *node) *sym = { let leaf: str; leaf.ptr = nm.ptr + ((dotidx + 1): u64); leaf.len = nm.len - dotidx - 1; - s = scopelookupinmodule(c.cur, head, leaf); + s = scopelookupinmodule(c.cur, modkeyfor(c, head), leaf); } else { // #53: same-module preference. Mirrors cstage // cmd/wcc/check.c:66 scope_lookup_prefer. Without this, @@ -11609,7 +11773,7 @@ fn scruttype(c: *checker, e: *node) *node = { if (e.kind == nkind.N_DOT) { if (e.lhs == nil) { return nil; }; if (e.lhs.kind != nkind.N_IDENT) { return nil; }; - let s: *sym = scopelookupinmodule(c.cur, e.lhs.str, e.str); + let s: *sym = scopelookupinmodule(c.cur, modkeyfor(c, e.lhs.str), e.str); if (s == nil) { return nil; }; if (s.decl == nil) { return nil; }; return s.decl.lhs; @@ -12148,7 +12312,7 @@ fn evaldefconst(c: *checker, n: *node, out: *u64, depth: i32) bool = { if (k == nkind.N_DOT) { if (n.lhs == nil) { return false; }; if (n.lhs.kind != nkind.N_IDENT) { return false; }; - let s: *sym = scopelookupinmodule(c.cur, n.lhs.str, n.str); + let s: *sym = scopelookupinmodule(c.cur, modkeyfor(c, n.lhs.str), n.str); if (s == nil) { return false; }; if (s.skind != skind.SK_DEF) { return false; }; if (s.decl == nil) { return false; }; @@ -13265,7 +13429,7 @@ fn unoptype(c: *checker, e: *node) *node = { // cstage's actual acceptance reason. if (e.lhs.kind == nkind.N_DOT) { if (e.lhs.lhs != nil && e.lhs.lhs.kind == nkind.N_IDENT) { - let fs: *sym = scopelookupinmodule(c.cur, e.lhs.lhs.str, e.lhs.str); + let fs: *sym = scopelookupinmodule(c.cur, modkeyfor(c, e.lhs.lhs.str), e.lhs.str); if (fs != nil) { if (fs.skind == skind.SK_FN) { if (fs.decl != nil) { @@ -13923,7 +14087,7 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { // global-leaf path) and harec check_autodereference // (ref/harec/src/check.c:1566-1581). if (ms != nil && (ms.skind == skind.SK_USE || ms.use_alias != 0i32)) { - s = scopelookupinmodule(c.cur, callee.lhs.str, nm); + s = scopelookupinmodule(c.cur, modkeyfor(c, callee.lhs.str), nm); }; }; if (s != nil) { if (s.skind == skind.SK_FN) { if (s.decl != nil) { @@ -13983,7 +14147,7 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { // qualified resolution and the lenient checker policy // keeps the silent miss documented at scruttype L656. if (ms.skind == skind.SK_USE || ms.use_alias != 0i32) { - let fs: *sym = scopelookupinmodule(c.cur, lhsn.str, e.str); + let fs: *sym = scopelookupinmodule(c.cur, modkeyfor(c, lhsn.str), e.str); if (fs != nil) { if (fs.decl != nil) { // #34: a module-qualified bare fn rvalue `mod.fn` types as // its FN TYPE (twin of the N_IDENT arm, :2688); decl.lhs is @@ -15840,7 +16004,7 @@ fn calleefndecl(c: *checker, callee: *node) *node = { }; if (ms != nil) { if (ms.skind == skind.SK_USE || ms.use_alias != 0i32) { - let fs: *sym = scopelookupinmodule(c.cur, callee.lhs.str, callee.str); + let fs: *sym = scopelookupinmodule(c.cur, modkeyfor(c, callee.lhs.str), callee.str); if (fs != nil) { if (fs.skind == skind.SK_FN) { return fs.decl; }; }; @@ -16794,29 +16958,25 @@ export fn checkfile(c: *checker, file: *node) void = { d = d.next; }; - // Program-global, name-only, cross-module uniqueness on `main`. - // `main` lowers to ONE bare entry symbol, so a second top-level - // decl named `main` (any kind, any package) collides with the - // entry at link time — today a silent segfault / link-fail in - // both stages. The (name, module) duplicate rejects in installtop - // read a cross-package `foo.main` and the bare entry as distinct, - // so they miss this. Correct multi-main mangling (entry stays bare, - // the rest qualify) is deferred (task #32); reject loudly meanwhile - // (rule 7). Walks USER decls only — runs before the -T synth main - // is appended below — so a hosted-test build never false-counts. - // Twin of cmd/wcc/check.c. + // Program-global uniqueness on the ENTRY `main`. M1 #32: the entry is + // the ROOT-unit main (imported==0) — it alone lowers to the bare + // `main` symbol. An IMPORTED package's `main` (imported==1) mangles on + // its path (`foo.bar.main`) and may coexist, closing the old dup-main + // collision by construction (#31). Two ROOT entries still collide on + // the bare symbol → reject loud (rule 7). Walks USER decls only — runs + // before the -T synth main is appended below. Twin of cmd/wcc/check.c. let firstmain: *node = nil; let mm: *node = file.list; for (mm != nil) { let ismain: bool = (mm.kind == nkind.N_FNDECL || mm.kind == nkind.N_LET || mm.kind == nkind.N_DEF || mm.kind == nkind.N_TYPEDECL) && streq(mm.str, "main"); - if (ismain) { + if (ismain && mm.imported == 0) { if (firstmain == nil) { firstmain = mm; } else { cerr(mm.file); - cerr(": error: duplicate top-level main: only the entry main may exist (task #32)\n"); + cerr(": error: duplicate entry main: only one root main may exist (#32)\n"); c.errs += 1; }; }; @@ -17146,6 +17306,7 @@ export fn checkfile(c: *checker, file: *node) void = { }; +//ww:module io // io — stream interface (Plan 9 Bio / Hare io::stream shape). // // No closures, no methods. A `stream` is a pointer to a `vtable` of @@ -17173,6 +17334,7 @@ export type eof = void; // underlying buffer-length type is i32 today. export type underread = !i32; +//ww:module io // stream — Hare-shaped vtable surface. Project #94 fold-eFinal. // // The single io stream surface (the fold-eFinal collapse retired the @@ -17396,6 +17558,7 @@ export fn empty() stream = { return &_empty_vt; }; +//ww:module errors // errors — domain-agnostic error types. Mirrors ref/hare/errors/. // // Named-void tagged-union variants, so `(T | errors.invalid | ...)` @@ -17532,6 +17695,7 @@ fn rt_strerror(op: *opaque_data) str = { return os.strerror(*e); }; +//ww:module io // types — error union, mode/whence enums, reader/writer/closer // fn-type aliases. Project #94 fold-eFinal; the fn-aliases target // `stream` (= `*vtable`, the single io surface). @@ -17617,6 +17781,7 @@ export type closer = fn(s: stream) (void | error); // `stream` directly, mirroring the reader/writer/closer aliases above. export type seeker = fn(s: stream, off: off, w: whence) (off | error); +//ww:module memio // memio — in-memory io stream. Project #94 fold-eFinal. // // Hare's memio:: surface, drop underscores. Two flavours behind a @@ -17922,6 +18087,7 @@ export fn borrowedread(s: *stream, amt: i32) ([]u8 | io.eof) = { return r; }; +//ww:module-reset // selfhost/cmd/wcc/cgenutil.ww — split out of cgen.ww. // // General helpers used across cgenexpr / cgenstmt / cgendecl: @@ -20091,10 +20257,14 @@ fn structlookup(c: *cgen, name: str) *structinfo = { let leaf: str; leaf.ptr = name.ptr + ((i + 1): u64); leaf.len = name.len - (i + 1); + // M1 #22: the embedded qualifier (`utf8`) is the use ALIAS; + // the struct's smod is now the dotted import PATH + // (`encoding.utf8`). Map alias→path before comparing. + let pkgmod: str = usehint(c, pkg); let b: *structinfo = c.structs; for (b != nil) { if (streq(b.sname, leaf)) { - if (streq(b.smod, pkg)) { + if (streq(b.smod, pkgmod)) { return b; }; }; @@ -23474,6 +23644,7 @@ fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *node, bpoff: i32) void = { cgstructlitfill(c, si, lit, 0, 0, "", bpoff); }; +//ww:module-reset // selfhost/cmd/wcc/cgenexpr.ww — split out of cgen.ww. // // cgexpr is a thin dispatcher over n.kind; each non-trivial branch @@ -27782,10 +27953,10 @@ fn cgdot(c: *cgen, n: *node) void = { // lhs.str is the explicit module hint so a same-leaf // def in another module (head of c.fnrets) can't shadow // the explicit qualifier (#17 N_DOT-arm omission audit). - let frt: *node = fnretlookupmod(c, fld, lhs.str); + let frt: *node = fnretlookupmod(c, fld, usehint(c, lhs.str)); if (frt != nil) { emitline("\tLEAQ\t"); - emitfnname(c, fld, lhs.str); + emitfnname(c, fld, usehint(c, lhs.str)); emitline("(SB), AX\n"); return; }; @@ -27797,7 +27968,7 @@ fn cgdot(c: *cgen, n: *node) void = { // the explicit module hint — a 3rd-module qualifier // `alpha.MSG` from gamma needs alpha (not c.curmod) // to beat a head-of-c.defs beta.MSG collision (#11). - let drhs: *node = deflookuprhsmod(c, fld, lhs.str); + let drhs: *node = deflookuprhsmod(c, fld, usehint(c, lhs.str)); if (drhs != nil) { if (drhs.kind == nkind.N_STRLIT) { let bytes: str = drhs.str; @@ -27818,11 +27989,11 @@ fn cgdot(c: *cgen, n: *node) void = { // threads lhs.str via emitfnname. if (streq(mqop, "MOVQ")) { emitline("\tMOVQ\t"); - emitsymnamehint(c, fld, lhs.str); + emitsymnamehint(c, fld, usehint(c, lhs.str)); emitline("(SB), AX\n"); } else { emitline("\tLEAQ\t"); - emitsymnamehint(c, fld, lhs.str); + emitsymnamehint(c, fld, usehint(c, lhs.str)); emitline("(SB), CX\n"); emitline("\t"); emitline(mqop); @@ -28702,10 +28873,10 @@ fn cgun(c: *cgen, n: *node) void = { // pre-#149 gap (file as #150-family). if (!isletvar(c, basenm) && !deflookup(c, basenm)) { let fld: str = opnd.str; - let frt: *node = fnretlookupmod(c, fld, basenm); + let frt: *node = fnretlookupmod(c, fld, usehint(c, basenm)); if (frt != nil) { emitline("\tLEAQ\t"); - emitfnname(c, fld, basenm); + emitfnname(c, fld, usehint(c, basenm)); emitline("(SB), AX\n"); return; }; @@ -28714,7 +28885,7 @@ fn cgun(c: *cgen, n: *node) void = { // &aa.v takes aa's global, not a // same-leaf collision. emitline("\tLEAQ\t"); - emitsymnamehint(c, fld, basenm); + emitsymnamehint(c, fld, usehint(c, basenm)); emitline("(SB), AX\n"); return; }; @@ -31703,7 +31874,7 @@ fn cgcall(c: *cgen, n: *node) void = { hint.len = 0; if (callee.lhs != nil) { if (callee.lhs.kind == nkind.N_IDENT) { - hint = callee.lhs.str; + hint = usehint(c, callee.lhs.str); }; }; emitfnname(c, calleename, hint); @@ -36013,6 +36184,7 @@ fn cgassign(c: *cgen, n: *node) void = { +//ww:module-reset // selfhost/cmd/wcc/cgenstmt.ww — split out of cgen.ww. // // cgstmt is a thin dispatcher over n.kind; each branch defers to a @@ -40362,6 +40534,7 @@ fn cgcontinue(c: *cgen, n: *node) void = { +//ww:module-reset // selfhost/cmd/wcc/cgendecl.ww — split out of cgen.ww. // // Houses the top-level emission glue: @@ -40966,9 +41139,17 @@ fn cgfn(c: *cgen, fn_: *node) void = { // Emit the TEXT label via emitfnname so the def site picks up the // same skip rule (FFI / `main` / empty-module) and the same module - // hint (this fn's own module) that the call sites use. + // hint (this fn's own module) that the call sites use. M1 #32: the + // ROOT-unit main (imported==0) is the bare `_start` entry — emit it + // bare directly, mirroring collectmods' skip; without this its + // fn_.nmod hint would fall through modlookupforfn's first-leaf match + // onto an IMPORTED package's now-registered `pkg.main`. emitline("TEXT "); - emitfnname(c, fn_.str, fn_.nmod); + if (streq(fn_.str, "main") && fn_.imported == 0) { + emitbytes(fn_.str.ptr, fn_.str.len: u64); + } else { + emitfnname(c, fn_.str, fn_.nmod); + }; emitline(",$"); emitint(frame: i64); emitline("\n"); @@ -41015,6 +41196,7 @@ export fn cgfile(c: *cgen, file: *node) void = { emitletdataw(c, file); }; +//ww:module-reset // selfhost/cmd/wcc/cgen.ww — port of cmd/w6c/cgen.c. // // Status: GROWING. Each subsystem we add is verified by `wwdump_ww -c` @@ -41140,10 +41322,13 @@ fn aliaslookup(c: *cgen, name: str) *node = { let leaf: str; leaf.ptr = name.ptr + ((i + 1): u64); leaf.len = name.len - (i + 1); + // M1 #22: map the embedded use ALIAS (`utf8`) to the + // dotted import PATH the decl's module now carries. + let pkgmod: str = usehint(c, pkg); let b: *aliasent = c.aliases; for (b != nil) { if (streq(b.aname, leaf)) { - if (streq(b.amod, pkg)) { + if (streq(b.amod, pkgmod)) { return b.target; }; }; @@ -41328,10 +41513,13 @@ fn enumlookup(c: *cgen, name: str) *enumtype = { let leaf: str; leaf.ptr = name.ptr + ((i + 1): u64); leaf.len = name.len - (i + 1); + // M1 #22: map the embedded use ALIAS (`utf8`) to the + // dotted import PATH the decl's module now carries. + let pkgmod: str = usehint(c, pkg); let b: *enumtype = c.enums; for (b != nil) { if (streq(b.ename, leaf)) { - if (streq(b.emod, pkg)) { + if (streq(b.emod, pkgmod)) { return b; }; }; @@ -41502,6 +41690,9 @@ type cgen = struct { enums: *enumtype, mods: *modent, // fn (any export status) + non-exported // let/def/type decls → originating module + uses: *modent, // M1 #22: N_USE alias → dotted import path, + // for the qualified-ref codegen hint + // (mname=alias, nmod=path) lets: *letvar, // top-level mutable scalar `let` bindings fnname: str, curmod: str, // current fn's `// MODULE: foo` directive (len=0 @@ -43877,7 +44068,7 @@ fn emittuplerowrelocs(c: *cgen, name: str, module: str, backing: bool, rowoff: i // the leaf with the MODULE ident; same-module `&fn` // stays on curmod. if (ev.lhs.kind == nkind.N_DOT) { - emitfnname(c, ev.lhs.str, ev.lhs.lhs.str); + emitfnname(c, ev.lhs.str, usehint(c, ev.lhs.lhs.str)); } else { emitfnname(c, ev.lhs.str, c.curmod); }; @@ -44064,7 +44255,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { // leaf with the MODULE ident; same-module `&fn` // stays on curmod. if (r.lhs.kind == nkind.N_DOT) { - emitfnname(c, r.lhs.str, r.lhs.lhs.str); + emitfnname(c, r.lhs.str, usehint(c, r.lhs.lhs.str)); } else { emitfnname(c, r.lhs.str, c.curmod); }; @@ -44535,11 +44726,24 @@ fn fnretlookup(c: *cgen, name: str) *node = { // the scrutinee tagged type to `(rune | done)` — flatvariantidx then // can't see arms 2/3 and collapses them onto tag 0 (task #31). fn fnretlookupmod(c: *cgen, name: str, mod: str) *node = { - if (mod.len > 0) { + // M1 #22 (#199b): the qualifier may be the import ALIAS the user + // wrote (`utf8`); fn decls register f.fmod under the dotted import + // PATH (`encoding.utf8`). Map alias->path so a nested-package callee + // matches its own module instead of falling back to the name-only + // pass — which a same-leaf caller-module fn (e.g. strings.next vs + // utf8.next) otherwise wins, resolving a match scrutinee to the + // caller's union and collapsing arms 2+. usehint is idempotent on a + // path / c.curmod (returns the input when no `use` matches), so the + // already-mapped callers (cgenexpr.ww:4309/5229) and the bare-ident + // c.curmod callers are unaffected. The choke-point twin of the + // struct/alias/enum usehint splitters (cgen.ww:128/319, + // cgenutil.ww:2173) — closes the whole fnret class by construction. + let mk: str = usehint(c, mod); + if (mk.len > 0) { let f: *fnret = c.fnrets; for (f != nil) { if (streq(f.fname, name)) { - if (streq(f.fmod, mod)) { return f.rtype; }; + if (streq(f.fmod, mk)) { return f.rtype; }; }; f = f.frnext; }; @@ -44599,11 +44803,15 @@ fn samemodfn(c: *cgen, name: str) bool = { // matching module is registered — mirrors aliaslookup's two-pass shape // (cgen.ww:75, fixed in #27). fn fnparamslookupmod(c: *cgen, name: str, mod: str) *node = { - if (mod.len > 0) { + // M1 #22 (#199b): map import alias -> dotted path, identical to + // fnretlookupmod (the param-side twin). usehint is idempotent on a + // path / c.curmod so existing callers are unaffected. + let mk: str = usehint(c, mod); + if (mk.len > 0) { let f: *fnret = c.fnrets; for (f != nil) { if (streq(f.fname, name)) { - if (streq(f.fmod, mod)) { return f.params; }; + if (streq(f.fmod, mk)) { return f.params; }; }; f = f.frnext; }; @@ -44783,9 +44991,17 @@ type modent = struct { fn collectmods(c: *cgen, file: *node) void = { c.mods = nil; + c.uses = nil; if (file == nil) { return; }; let d: *node = file.list; for (d != nil) { + // M1 #22: record alias→path for the qualified-ref hint. + if (d.kind == nkind.N_USE) { + if (d.usepath.len > 0) { + let um: *modent = alloc(modent{mname=d.str, nmod=d.usepath, mnext=c.uses})!; + c.uses = um; + }; + }; // Mirror collectfnrets' shape exactly (plain prepend in one // branch). Earlier nested-if/early-return variants tickled a // wwstage cgen bug that dropped most prepends. @@ -44803,7 +45019,9 @@ fn collectmods(c: *cgen, file: *node) void = { a = a.next; }; if (!isffi) { - if (!streq(d.str, "main")) { + // M1 #32: the ROOT main (imported==0) stays bare; + // an IMPORTED `fn main` mangles on its path. + if (!streq(d.str, "main") || d.imported != 0) { let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, mnext=c.mods})!; c.mods = m; }; @@ -44850,6 +45068,19 @@ fn modlookup(c: *cgen, name: str) str = { return empty; }; +// usehint — M1 #22: map a qualified-ref alias (`utf8`) to its dotted +// import path (`encoding.utf8`) so the codegen hint keys the path-keyed +// mods map. For single-level packages alias == path (no-op). Returns the +// alias unchanged when no matching `use` exists. +fn usehint(c: *cgen, alias: str) str = { + let m: *modent = c.uses; + for (m != nil) { + if (streq(m.mname, alias)) { return m.nmod; }; + m = m.mnext; + }; + return alias; +}; + // modlookupforfn — hint-aware lookup for fn names. Walks c.mods // preferring entries where module matches `hint`; falls back to the // first leaf-name match when nothing matches the hint (legacy single- @@ -45041,6 +45272,7 @@ export fn fargregname(i: i32) str = { return "?"; }; +//ww:module-reset // selfhost/cmd/w6c/main.ww — port of cmd/w6c/main.c. // // w6c = amd64 compiler. Read .ww, parse, codegen, emit Plan 9 amd64 diff --git a/selfhost/cmd/w6l/main.combined.ww b/selfhost/cmd/w6l/main.combined.ww index 3b4dad5c..22297431 100644 --- a/selfhost/cmd/w6l/main.combined.ww +++ b/selfhost/cmd/w6l/main.combined.ww @@ -1,3 +1,4 @@ +//ww:module time // time — clocks, instants, durations. Mirrors Hare's lib/time // (ref/hare/time/duration.ha, instant.ha, arithm.ha, // +linux/functions.ha). Calendar / date / strftime / timezone / @@ -95,6 +96,7 @@ export fn compare(a: instant, b: instant) i8 = { return 0i8; }; +//ww:module rt // rt — runtime primitives exposed to ww programs. // Mirrors Hare's rt:: module placement (ref/hare/rt/). @@ -118,6 +120,7 @@ package rt; // a future task (task #39). ref/hare/rt/malloc.ha:27. @symbol("rt_malloc") export fn malloc(n: u64) *void; +//ww:module os // os — process and filesystem facade. The body of each call lands // either in libwwrt.a (rt_syscall trampoline) or libc bindings, // depending on how the program was linked. @@ -856,6 +859,7 @@ export fn exists(path: str) bool = { return r >= 0i64; }; +//ww:module types // types — integer limits. Mirrors Hare's types::limits (I8_MAX, …) // platform-fixed for amd64. Numeric helpers live in lib/math, matching // Hare's split between types::limits and math::. @@ -900,6 +904,7 @@ def UINTPTR_MAX: uintptr = U64_MAX: uintptr; def RUNE_MIN: rune = '\0'; +//ww:module bytes // bytes — slice operations over []u8. Mirrors Hare's bytes module // (ref/hare/bytes/) for the in-tree subset: search/equality/prefix // helpers used by lib/encoding, lib/bufio, lib/memio. @@ -1451,6 +1456,7 @@ export fn rcut(in: []u8, delim: (u8 | []u8)) ([]u8, []u8) = { }; }; +//ww:module encoding.utf8 // encoding/utf8 — UTF-8 encode/decode. Hare port; see // ref/hare/encoding/utf8/{types,rune,encode,decode,decodetable}.ha. // @@ -1908,6 +1914,7 @@ export fn position(d: *decoder) i32 = { }; +//ww:module strings // strings — operations over str ({ptr,len}). Hare port; see // ref/hare/strings/. // @@ -2860,6 +2867,7 @@ export fn rpad(s: str, p: rune, maxlen: i32) str = { return frombytes(buf); }; +//ww:module-reset // selfhost/cmd/w6l/sym.ww — port of cmd/w6l/sym.c. // // Linker symbol table. Singly-linked list, usually a few hundred @@ -2961,6 +2969,7 @@ export fn lookup(l: *lnk, name: str) *lsym = { return nil; }; +//ww:module-reset // selfhost/cmd/w6l/obj.ww — port of cmd/w6l/obj.c. // // Loads relocatable ELF64 .o files emitted by w6a, appends .text to @@ -3580,6 +3589,7 @@ fn loadimage(l: *lnk, path: *u8, buf: *u8, len: u64) i32 = { return 0; }; +//ww:module-reset // selfhost/cmd/w6l/dyn.ww — port of cmd/w6l/dyn.c. // // Load a shared object (ET_DYN) so the linker knows which symbols it @@ -3983,6 +3993,7 @@ export fn soversion(so: *lso, name: str) str = { return result; }; +//ww:module-reset // selfhost/cmd/w6l/pass.ww — port of cmd/w6l/pass.c. // // Resolution + relocation. l_resolve flags every undefined symbol @@ -4115,6 +4126,7 @@ export fn relocate(l: *lnk, textva: u64, datava: u64) i32 = { return l.errs; }; +//ww:module-reset // selfhost/cmd/w6l/dynout.ww — port of cmd/w6l/dynout.c. // // Emit a dynamic-linked ELF executable. The shape is the simplest @@ -4867,6 +4879,7 @@ export fn emitdynelf(l: *lnk, fd: i32, base: u64, entry: u64) i32 = { return 0; }; +//ww:module-reset // selfhost/cmd/w6l/out.ww — port of cmd/w6l/out.c. // // Emit a static ELF64 executable. File layout (per the C original): @@ -5048,6 +5061,7 @@ export fn emitelf(l: *lnk, fd: i32, base: u64, entry: u64) i32 = { return 0; }; +//ww:module-reset // selfhost/cmd/w6l/main.ww — port of cmd/w6l/main.c. // // w6l = amd64 linker. Reads relocatable ELF .o files, SysV `ar` diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index e9f13bd4..646f5e7d 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -123,10 +123,13 @@ fn aliaslookup(c: *cgen, name: str) *node = { let leaf: str; leaf.ptr = name.ptr + ((i + 1): u64); leaf.len = name.len - (i + 1); + // M1 #22: map the embedded use ALIAS (`utf8`) to the + // dotted import PATH the decl's module now carries. + let pkgmod: str = usehint(c, pkg); let b: *aliasent = c.aliases; for (b != nil) { if (streq(b.aname, leaf)) { - if (streq(b.amod, pkg)) { + if (streq(b.amod, pkgmod)) { return b.target; }; }; @@ -311,10 +314,13 @@ fn enumlookup(c: *cgen, name: str) *enumtype = { let leaf: str; leaf.ptr = name.ptr + ((i + 1): u64); leaf.len = name.len - (i + 1); + // M1 #22: map the embedded use ALIAS (`utf8`) to the + // dotted import PATH the decl's module now carries. + let pkgmod: str = usehint(c, pkg); let b: *enumtype = c.enums; for (b != nil) { if (streq(b.ename, leaf)) { - if (streq(b.emod, pkg)) { + if (streq(b.emod, pkgmod)) { return b; }; }; @@ -485,6 +491,9 @@ type cgen = struct { enums: *enumtype, mods: *modent, // fn (any export status) + non-exported // let/def/type decls → originating module + uses: *modent, // M1 #22: N_USE alias → dotted import path, + // for the qualified-ref codegen hint + // (mname=alias, nmod=path) lets: *letvar, // top-level mutable scalar `let` bindings fnname: str, curmod: str, // current fn's `// MODULE: foo` directive (len=0 @@ -2860,7 +2869,7 @@ fn emittuplerowrelocs(c: *cgen, name: str, module: str, backing: bool, rowoff: i // the leaf with the MODULE ident; same-module `&fn` // stays on curmod. if (ev.lhs.kind == nkind.N_DOT) { - emitfnname(c, ev.lhs.str, ev.lhs.lhs.str); + emitfnname(c, ev.lhs.str, usehint(c, ev.lhs.lhs.str)); } else { emitfnname(c, ev.lhs.str, c.curmod); }; @@ -3047,7 +3056,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { // leaf with the MODULE ident; same-module `&fn` // stays on curmod. if (r.lhs.kind == nkind.N_DOT) { - emitfnname(c, r.lhs.str, r.lhs.lhs.str); + emitfnname(c, r.lhs.str, usehint(c, r.lhs.lhs.str)); } else { emitfnname(c, r.lhs.str, c.curmod); }; @@ -3518,11 +3527,24 @@ fn fnretlookup(c: *cgen, name: str) *node = { // the scrutinee tagged type to `(rune | done)` — flatvariantidx then // can't see arms 2/3 and collapses them onto tag 0 (task #31). fn fnretlookupmod(c: *cgen, name: str, mod: str) *node = { - if (mod.len > 0) { + // M1 #22 (#199b): the qualifier may be the import ALIAS the user + // wrote (`utf8`); fn decls register f.fmod under the dotted import + // PATH (`encoding.utf8`). Map alias->path so a nested-package callee + // matches its own module instead of falling back to the name-only + // pass — which a same-leaf caller-module fn (e.g. strings.next vs + // utf8.next) otherwise wins, resolving a match scrutinee to the + // caller's union and collapsing arms 2+. usehint is idempotent on a + // path / c.curmod (returns the input when no `use` matches), so the + // already-mapped callers (cgenexpr.ww:4309/5229) and the bare-ident + // c.curmod callers are unaffected. The choke-point twin of the + // struct/alias/enum usehint splitters (cgen.ww:128/319, + // cgenutil.ww:2173) — closes the whole fnret class by construction. + let mk: str = usehint(c, mod); + if (mk.len > 0) { let f: *fnret = c.fnrets; for (f != nil) { if (streq(f.fname, name)) { - if (streq(f.fmod, mod)) { return f.rtype; }; + if (streq(f.fmod, mk)) { return f.rtype; }; }; f = f.frnext; }; @@ -3582,11 +3604,15 @@ fn samemodfn(c: *cgen, name: str) bool = { // matching module is registered — mirrors aliaslookup's two-pass shape // (cgen.ww:75, fixed in #27). fn fnparamslookupmod(c: *cgen, name: str, mod: str) *node = { - if (mod.len > 0) { + // M1 #22 (#199b): map import alias -> dotted path, identical to + // fnretlookupmod (the param-side twin). usehint is idempotent on a + // path / c.curmod so existing callers are unaffected. + let mk: str = usehint(c, mod); + if (mk.len > 0) { let f: *fnret = c.fnrets; for (f != nil) { if (streq(f.fname, name)) { - if (streq(f.fmod, mod)) { return f.params; }; + if (streq(f.fmod, mk)) { return f.params; }; }; f = f.frnext; }; @@ -3766,9 +3792,17 @@ type modent = struct { fn collectmods(c: *cgen, file: *node) void = { c.mods = nil; + c.uses = nil; if (file == nil) { return; }; let d: *node = file.list; for (d != nil) { + // M1 #22: record alias→path for the qualified-ref hint. + if (d.kind == nkind.N_USE) { + if (d.usepath.len > 0) { + let um: *modent = alloc(modent{mname=d.str, nmod=d.usepath, mnext=c.uses})!; + c.uses = um; + }; + }; // Mirror collectfnrets' shape exactly (plain prepend in one // branch). Earlier nested-if/early-return variants tickled a // wwstage cgen bug that dropped most prepends. @@ -3786,7 +3820,9 @@ fn collectmods(c: *cgen, file: *node) void = { a = a.next; }; if (!isffi) { - if (!streq(d.str, "main")) { + // M1 #32: the ROOT main (imported==0) stays bare; + // an IMPORTED `fn main` mangles on its path. + if (!streq(d.str, "main") || d.imported != 0) { let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, mnext=c.mods})!; c.mods = m; }; @@ -3833,6 +3869,19 @@ fn modlookup(c: *cgen, name: str) str = { return empty; }; +// usehint — M1 #22: map a qualified-ref alias (`utf8`) to its dotted +// import path (`encoding.utf8`) so the codegen hint keys the path-keyed +// mods map. For single-level packages alias == path (no-op). Returns the +// alias unchanged when no matching `use` exists. +fn usehint(c: *cgen, alias: str) str = { + let m: *modent = c.uses; + for (m != nil) { + if (streq(m.mname, alias)) { return m.nmod; }; + m = m.mnext; + }; + return alias; +}; + // modlookupforfn — hint-aware lookup for fn names. Walks c.mods // preferring entries where module matches `hint`; falls back to the // first leaf-name match when nothing matches the hint (legacy single- diff --git a/selfhost/cmd/wcc/cgendecl.ww b/selfhost/cmd/wcc/cgendecl.ww index 2d1fdc37..72b5f7c8 100644 --- a/selfhost/cmd/wcc/cgendecl.ww +++ b/selfhost/cmd/wcc/cgendecl.ww @@ -602,9 +602,17 @@ fn cgfn(c: *cgen, fn_: *node) void = { // Emit the TEXT label via emitfnname so the def site picks up the // same skip rule (FFI / `main` / empty-module) and the same module - // hint (this fn's own module) that the call sites use. + // hint (this fn's own module) that the call sites use. M1 #32: the + // ROOT-unit main (imported==0) is the bare `_start` entry — emit it + // bare directly, mirroring collectmods' skip; without this its + // fn_.nmod hint would fall through modlookupforfn's first-leaf match + // onto an IMPORTED package's now-registered `pkg.main`. emitline("TEXT "); - emitfnname(c, fn_.str, fn_.nmod); + if (streq(fn_.str, "main") && fn_.imported == 0) { + emitbytes(fn_.str.ptr, fn_.str.len: u64); + } else { + emitfnname(c, fn_.str, fn_.nmod); + }; emitline(",$"); emitint(frame: i64); emitline("\n"); diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 340cdd1a..5e00f3f1 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -4306,10 +4306,10 @@ fn cgdot(c: *cgen, n: *node) void = { // lhs.str is the explicit module hint so a same-leaf // def in another module (head of c.fnrets) can't shadow // the explicit qualifier (#17 N_DOT-arm omission audit). - let frt: *node = fnretlookupmod(c, fld, lhs.str); + let frt: *node = fnretlookupmod(c, fld, usehint(c, lhs.str)); if (frt != nil) { emitline("\tLEAQ\t"); - emitfnname(c, fld, lhs.str); + emitfnname(c, fld, usehint(c, lhs.str)); emitline("(SB), AX\n"); return; }; @@ -4321,7 +4321,7 @@ fn cgdot(c: *cgen, n: *node) void = { // the explicit module hint — a 3rd-module qualifier // `alpha.MSG` from gamma needs alpha (not c.curmod) // to beat a head-of-c.defs beta.MSG collision (#11). - let drhs: *node = deflookuprhsmod(c, fld, lhs.str); + let drhs: *node = deflookuprhsmod(c, fld, usehint(c, lhs.str)); if (drhs != nil) { if (drhs.kind == nkind.N_STRLIT) { let bytes: str = drhs.str; @@ -4342,11 +4342,11 @@ fn cgdot(c: *cgen, n: *node) void = { // threads lhs.str via emitfnname. if (streq(mqop, "MOVQ")) { emitline("\tMOVQ\t"); - emitsymnamehint(c, fld, lhs.str); + emitsymnamehint(c, fld, usehint(c, lhs.str)); emitline("(SB), AX\n"); } else { emitline("\tLEAQ\t"); - emitsymnamehint(c, fld, lhs.str); + emitsymnamehint(c, fld, usehint(c, lhs.str)); emitline("(SB), CX\n"); emitline("\t"); emitline(mqop); @@ -5226,10 +5226,10 @@ fn cgun(c: *cgen, n: *node) void = { // pre-#149 gap (file as #150-family). if (!isletvar(c, basenm) && !deflookup(c, basenm)) { let fld: str = opnd.str; - let frt: *node = fnretlookupmod(c, fld, basenm); + let frt: *node = fnretlookupmod(c, fld, usehint(c, basenm)); if (frt != nil) { emitline("\tLEAQ\t"); - emitfnname(c, fld, basenm); + emitfnname(c, fld, usehint(c, basenm)); emitline("(SB), AX\n"); return; }; @@ -5238,7 +5238,7 @@ fn cgun(c: *cgen, n: *node) void = { // &aa.v takes aa's global, not a // same-leaf collision. emitline("\tLEAQ\t"); - emitsymnamehint(c, fld, basenm); + emitsymnamehint(c, fld, usehint(c, basenm)); emitline("(SB), AX\n"); return; }; @@ -8227,7 +8227,7 @@ fn cgcall(c: *cgen, n: *node) void = { hint.len = 0; if (callee.lhs != nil) { if (callee.lhs.kind == nkind.N_IDENT) { - hint = callee.lhs.str; + hint = usehint(c, callee.lhs.str); }; }; emitfnname(c, calleename, hint); diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index e26af541..b01ea6be 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -2167,10 +2167,14 @@ fn structlookup(c: *cgen, name: str) *structinfo = { let leaf: str; leaf.ptr = name.ptr + ((i + 1): u64); leaf.len = name.len - (i + 1); + // M1 #22: the embedded qualifier (`utf8`) is the use ALIAS; + // the struct's smod is now the dotted import PATH + // (`encoding.utf8`). Map alias→path before comparing. + let pkgmod: str = usehint(c, pkg); let b: *structinfo = c.structs; for (b != nil) { if (streq(b.sname, leaf)) { - if (streq(b.smod, pkg)) { + if (streq(b.smod, pkgmod)) { return b; }; }; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index a0075869..f502ca1d 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -164,14 +164,48 @@ fn declmod(file: *node, d: *node) str = { if (file == nil) { return empty; }; let u: *node = file.list; for (u != nil) { + // M1 #22: a decl is imported iff some `use` directive's full + // dotted import path equals the decl's module (now the path). + // Single-level packages have usepath == leaf so this is + // unchanged; nested (`encoding.utf8`) match here, not on leaf. if (u.kind == nkind.N_USE) { - if (streq(u.str, d.nmod)) { return d.nmod; }; + if (streq(u.usepath, d.nmod)) { return d.nmod; }; }; u = u.next; }; return empty; }; +// usepath — map a `use` alias (leaf bareword the user writes, `utf8`) +// to the full dotted import path it binds (`encoding.utf8`), for the +// module-qualified resolution and codegen hint (M1 #22). Single-level +// packages have usepath == alias so the result is unchanged. The +// current tree has one occurrence per leaf, so the map is unambiguous. +fn usepathfor(file: *node, alias: str) str = { + let empty: str; + if (file == nil) { return empty; }; + if (alias.len == 0) { return empty; }; + let u: *node = file.list; + for (u != nil) { + if (u.kind == nkind.N_USE) { + if (streq(u.str, alias)) { + if (u.usepath.len != 0) { return u.usepath; }; + return u.str; + }; + }; + u = u.next; + }; + return empty; +}; + +// modkeyfor — usepathfor with leaf-alias fallback: the module key for a +// path-keyed scopelookupinmodule given the alias the user wrote (M1 #22). +fn modkeyfor(c: *checker, alias: str) str = { + let mk: str = usepathfor(c.file, alias); + if (mk.len == 0) { return alias; }; + return mk; +}; + // srcimports — does the source file that contributed decl-module // `modtag` carry `use ;`? Mirrors cstage's src_imports — // `modtag.len == 0` means primary, matching declmod's empty-str @@ -188,7 +222,7 @@ fn srcimports(file: *node, modtag: str, name: str) bool = { // module bareword and lib/fmt's own // `fn bsprintf(fmt: str, ...)` is not a shadow. if (u.nmod.len > 0) { - if (streq(u.nmod, u.str)) { + if (streq(u.nmod, u.usepath)) { u = u.next; continue; }; @@ -277,7 +311,9 @@ fn installdecl(c: *checker, file: *node, d: *node) void = { // pulls lack import->file->symbol provenance). Message byte-identical // to cstage check.c. if (k == nkind.N_USE) { - if (mod.len != 0 && streq(nm, mod)) { + // M1 #22: self-import ⟺ the imported path equals the use's own + // (owning) module path. Compares paths, not leaves. + if (mod.len != 0 && streq(d.usepath, mod)) { cerr("self-import: package '"); cerr(mod); cerr("' cannot import itself\n"); c.errs += 1i32; }; @@ -486,7 +522,7 @@ fn resolvewalk(c: *checker, n: *node) void = { let leaf: str; leaf.ptr = nm.ptr + (dot + 1): u64; leaf.len = nm.len - (dot + 1); - s = scopelookupinmodule(c.cur, head, leaf); + s = scopelookupinmodule(c.cur, modkeyfor(c, head), leaf); }; }; }; @@ -884,7 +920,7 @@ fn aliassym(c: *checker, n: *node) *sym = { let leaf: str; leaf.ptr = nm.ptr + ((dotidx + 1): u64); leaf.len = nm.len - dotidx - 1; - s = scopelookupinmodule(c.cur, head, leaf); + s = scopelookupinmodule(c.cur, modkeyfor(c, head), leaf); } else { // #53: same-module preference. Mirrors cstage // cmd/wcc/check.c:66 scope_lookup_prefer. Without this, @@ -1135,7 +1171,7 @@ fn scruttype(c: *checker, e: *node) *node = { if (e.kind == nkind.N_DOT) { if (e.lhs == nil) { return nil; }; if (e.lhs.kind != nkind.N_IDENT) { return nil; }; - let s: *sym = scopelookupinmodule(c.cur, e.lhs.str, e.str); + let s: *sym = scopelookupinmodule(c.cur, modkeyfor(c, e.lhs.str), e.str); if (s == nil) { return nil; }; if (s.decl == nil) { return nil; }; return s.decl.lhs; @@ -1674,7 +1710,7 @@ fn evaldefconst(c: *checker, n: *node, out: *u64, depth: i32) bool = { if (k == nkind.N_DOT) { if (n.lhs == nil) { return false; }; if (n.lhs.kind != nkind.N_IDENT) { return false; }; - let s: *sym = scopelookupinmodule(c.cur, n.lhs.str, n.str); + let s: *sym = scopelookupinmodule(c.cur, modkeyfor(c, n.lhs.str), n.str); if (s == nil) { return false; }; if (s.skind != skind.SK_DEF) { return false; }; if (s.decl == nil) { return false; }; @@ -2791,7 +2827,7 @@ fn unoptype(c: *checker, e: *node) *node = { // cstage's actual acceptance reason. if (e.lhs.kind == nkind.N_DOT) { if (e.lhs.lhs != nil && e.lhs.lhs.kind == nkind.N_IDENT) { - let fs: *sym = scopelookupinmodule(c.cur, e.lhs.lhs.str, e.lhs.str); + let fs: *sym = scopelookupinmodule(c.cur, modkeyfor(c, e.lhs.lhs.str), e.lhs.str); if (fs != nil) { if (fs.skind == skind.SK_FN) { if (fs.decl != nil) { @@ -3449,7 +3485,7 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { // global-leaf path) and harec check_autodereference // (ref/harec/src/check.c:1566-1581). if (ms != nil && (ms.skind == skind.SK_USE || ms.use_alias != 0i32)) { - s = scopelookupinmodule(c.cur, callee.lhs.str, nm); + s = scopelookupinmodule(c.cur, modkeyfor(c, callee.lhs.str), nm); }; }; if (s != nil) { if (s.skind == skind.SK_FN) { if (s.decl != nil) { @@ -3509,7 +3545,7 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { // qualified resolution and the lenient checker policy // keeps the silent miss documented at scruttype L656. if (ms.skind == skind.SK_USE || ms.use_alias != 0i32) { - let fs: *sym = scopelookupinmodule(c.cur, lhsn.str, e.str); + let fs: *sym = scopelookupinmodule(c.cur, modkeyfor(c, lhsn.str), e.str); if (fs != nil) { if (fs.decl != nil) { // #34: a module-qualified bare fn rvalue `mod.fn` types as // its FN TYPE (twin of the N_IDENT arm, :2688); decl.lhs is @@ -5366,7 +5402,7 @@ fn calleefndecl(c: *checker, callee: *node) *node = { }; if (ms != nil) { if (ms.skind == skind.SK_USE || ms.use_alias != 0i32) { - let fs: *sym = scopelookupinmodule(c.cur, callee.lhs.str, callee.str); + let fs: *sym = scopelookupinmodule(c.cur, modkeyfor(c, callee.lhs.str), callee.str); if (fs != nil) { if (fs.skind == skind.SK_FN) { return fs.decl; }; }; @@ -6320,29 +6356,25 @@ export fn checkfile(c: *checker, file: *node) void = { d = d.next; }; - // Program-global, name-only, cross-module uniqueness on `main`. - // `main` lowers to ONE bare entry symbol, so a second top-level - // decl named `main` (any kind, any package) collides with the - // entry at link time — today a silent segfault / link-fail in - // both stages. The (name, module) duplicate rejects in installtop - // read a cross-package `foo.main` and the bare entry as distinct, - // so they miss this. Correct multi-main mangling (entry stays bare, - // the rest qualify) is deferred (task #32); reject loudly meanwhile - // (rule 7). Walks USER decls only — runs before the -T synth main - // is appended below — so a hosted-test build never false-counts. - // Twin of cmd/wcc/check.c. + // Program-global uniqueness on the ENTRY `main`. M1 #32: the entry is + // the ROOT-unit main (imported==0) — it alone lowers to the bare + // `main` symbol. An IMPORTED package's `main` (imported==1) mangles on + // its path (`foo.bar.main`) and may coexist, closing the old dup-main + // collision by construction (#31). Two ROOT entries still collide on + // the bare symbol → reject loud (rule 7). Walks USER decls only — runs + // before the -T synth main is appended below. Twin of cmd/wcc/check.c. let firstmain: *node = nil; let mm: *node = file.list; for (mm != nil) { let ismain: bool = (mm.kind == nkind.N_FNDECL || mm.kind == nkind.N_LET || mm.kind == nkind.N_DEF || mm.kind == nkind.N_TYPEDECL) && streq(mm.str, "main"); - if (ismain) { + if (ismain && mm.imported == 0) { if (firstmain == nil) { firstmain = mm; } else { cerr(mm.file); - cerr(": error: duplicate top-level main: only the entry main may exist (task #32)\n"); + cerr(": error: duplicate entry main: only one root main may exist (#32)\n"); c.errs += 1; }; }; diff --git a/selfhost/cmd/ww/main.combined.ww b/selfhost/cmd/ww/main.combined.ww index aac394e4..58a872f5 100644 --- a/selfhost/cmd/ww/main.combined.ww +++ b/selfhost/cmd/ww/main.combined.ww @@ -1,3 +1,4 @@ +//ww:module time // time — clocks, instants, durations. Mirrors Hare's lib/time // (ref/hare/time/duration.ha, instant.ha, arithm.ha, // +linux/functions.ha). Calendar / date / strftime / timezone / @@ -95,6 +96,7 @@ export fn compare(a: instant, b: instant) i8 = { return 0i8; }; +//ww:module rt // rt — runtime primitives exposed to ww programs. // Mirrors Hare's rt:: module placement (ref/hare/rt/). @@ -118,6 +120,7 @@ package rt; // a future task (task #39). ref/hare/rt/malloc.ha:27. @symbol("rt_malloc") export fn malloc(n: u64) *void; +//ww:module os // os — process and filesystem facade. The body of each call lands // either in libwwrt.a (rt_syscall trampoline) or libc bindings, // depending on how the program was linked. @@ -856,6 +859,7 @@ export fn exists(path: str) bool = { return r >= 0i64; }; +//ww:module types // types — integer limits. Mirrors Hare's types::limits (I8_MAX, …) // platform-fixed for amd64. Numeric helpers live in lib/math, matching // Hare's split between types::limits and math::. @@ -900,6 +904,7 @@ def UINTPTR_MAX: uintptr = U64_MAX: uintptr; def RUNE_MIN: rune = '\0'; +//ww:module bytes // bytes — slice operations over []u8. Mirrors Hare's bytes module // (ref/hare/bytes/) for the in-tree subset: search/equality/prefix // helpers used by lib/encoding, lib/bufio, lib/memio. @@ -1451,6 +1456,7 @@ export fn rcut(in: []u8, delim: (u8 | []u8)) ([]u8, []u8) = { }; }; +//ww:module encoding.utf8 // encoding/utf8 — UTF-8 encode/decode. Hare port; see // ref/hare/encoding/utf8/{types,rune,encode,decode,decodetable}.ha. // @@ -1908,6 +1914,7 @@ export fn position(d: *decoder) i32 = { }; +//ww:module strings // strings — operations over str ({ptr,len}). Hare port; see // ref/hare/strings/. // @@ -2860,6 +2867,7 @@ export fn rpad(s: str, p: rune, maxlen: i32) str = { return frombytes(buf); }; +//ww:module-reset // selfhost/cmd/ww/main.ww — port of cmd/ww/main.c. // // The user-facing driver. Plan 9 cc(1) / Hare hare(1) analogue: @@ -3367,7 +3375,7 @@ fn scanuse(src: *u8, len: u64) (*u8, u64) = { // after recursive-expanding its top-of-file `import X;` imports. // Each source declares its own `package ;` (parser stamps // decls). -fn expand(c: *expctx, pathcs: *u8) void = { +fn expand(c: *expctx, pathcs: *u8, modpath: str) void = { let plen: u64 = cstrlen(pathcs); let view: str; view.ptr = pathcs; @@ -3400,8 +3408,21 @@ fn expand(c: *expctx, pathcs: *u8) void = { let ipath: *u8 = locateimport(c.dirs, idp, idn, &isdir); if (ipath != nil) { - if (isdir != 0) { expanddir(c, ipath); } - else { expand(c, ipath); }; + // M1 #22 (isdir-gated, rob-ratified): only DIRECTORY + // imports are package boundaries that path-mangle. A + // single-file import (`import opcodes;` → opcodes.ww + // declaring `package w6a`) is an intra-package file-split: + // it keeps its in-file `package` clause as its module + // (no directive → reset → package-clause mangling). + if (isdir != 0) { + let mv: str; + mv.ptr = idp; + mv.len = idn: i32; + let modstr: str = strings.dup(mv); + expanddir(c, ipath, modstr); + } else { + expand(c, ipath, ""); + }; } else { // #16 ENFORCE-driver (rob A): a locate-miss is // legal when the package is defined INLINE in the @@ -3437,7 +3458,17 @@ fn expand(c: *expctx, pathcs: *u8) void = { // `package main`, which would main-prefix them). A packaged file's own // `package` decl already sets curmod, so it needs nothing — keeping // the directive out of every tracked combined.ww. (Task #11.) - if (peekpackage(pathcs) == nil) { + if (modpath.len != 0) { + // M1 #22: import-reached file carries its full dotted path so + // codegen mangles symbols on the path, not the leaf clause. + let dm: str = "//ww:module "; + os.writeall(c.out, dm.ptr, dm.len: u64); + os.writeall(c.out, modpath.ptr, modpath.len: u64); + os.writeall(c.out, "\n".ptr, 1u64); + } else { + // Root/primary: always reset so a preceding imported section's + // sticky pathmod is cleared; a packaged primary's own `package` + // clause then sets curmod fresh (pathmod NULL → real clause). let d: str = "//ww:module-reset\n"; os.writeall(c.out, d.ptr, d.len: u64); }; @@ -3613,7 +3644,7 @@ fn strictpkgmismatch(file: *u8, pkg: *u8, dirpkg: *u8, dirpath: *u8) void = { // are pulled once. Strict-same-package: all enumerated files must // declare the same `package ;` (task #23 subset; failure // mode native to dir-enum). -fn expanddir(c: *expctx, dirpath: *u8) void = { +fn expanddir(c: *expctx, dirpath: *u8, modpath: str) void = { let names: **u8; let n: i32; names, n = enumeratedir(dirpath); @@ -3636,7 +3667,7 @@ fn expanddir(c: *expctx, dirpath: *u8) void = { strictpkgmismatch(fp.ptr, pkg, dirpkg, dirpath); }; }; }; - expand(c, fp.ptr); + expand(c, fp.ptr, modpath); i += 1; }; }; @@ -3825,12 +3856,12 @@ fn buildone(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, objstem: *u8, inc let td: i32 = 0; let tp: *u8 = locateimport(searchpath.ptr, "test".ptr, "test".len: u64, &td); if (tp != nil) { - if (td != 0) { expanddir(&c, tp); } - else { expand(&c, tp); }; + if (td != 0) { expanddir(&c, tp, "test"); } + else { expand(&c, tp, ""); }; }; }; - if (entryisdir != 0) { expanddir(&c, srcd.ptr); } - else { expand(&c, src); }; + if (entryisdir != 0) { expanddir(&c, srcd.ptr, ""); } + else { expand(&c, src, ""); }; }; os.close(cf); diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index 607c9ab2..f4aca4f2 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -505,7 +505,7 @@ fn scanuse(src: *u8, len: u64) (*u8, u64) = { // after recursive-expanding its top-of-file `import X;` imports. // Each source declares its own `package ;` (parser stamps // decls). -fn expand(c: *expctx, pathcs: *u8) void = { +fn expand(c: *expctx, pathcs: *u8, modpath: str) void = { let plen: u64 = cstrlen(pathcs); let view: str; view.ptr = pathcs; @@ -538,8 +538,21 @@ fn expand(c: *expctx, pathcs: *u8) void = { let ipath: *u8 = locateimport(c.dirs, idp, idn, &isdir); if (ipath != nil) { - if (isdir != 0) { expanddir(c, ipath); } - else { expand(c, ipath); }; + // M1 #22 (isdir-gated, rob-ratified): only DIRECTORY + // imports are package boundaries that path-mangle. A + // single-file import (`import opcodes;` → opcodes.ww + // declaring `package w6a`) is an intra-package file-split: + // it keeps its in-file `package` clause as its module + // (no directive → reset → package-clause mangling). + if (isdir != 0) { + let mv: str; + mv.ptr = idp; + mv.len = idn: i32; + let modstr: str = strings.dup(mv); + expanddir(c, ipath, modstr); + } else { + expand(c, ipath, ""); + }; } else { // #16 ENFORCE-driver (rob A): a locate-miss is // legal when the package is defined INLINE in the @@ -575,7 +588,17 @@ fn expand(c: *expctx, pathcs: *u8) void = { // `package main`, which would main-prefix them). A packaged file's own // `package` decl already sets curmod, so it needs nothing — keeping // the directive out of every tracked combined.ww. (Task #11.) - if (peekpackage(pathcs) == nil) { + if (modpath.len != 0) { + // M1 #22: import-reached file carries its full dotted path so + // codegen mangles symbols on the path, not the leaf clause. + let dm: str = "//ww:module "; + os.writeall(c.out, dm.ptr, dm.len: u64); + os.writeall(c.out, modpath.ptr, modpath.len: u64); + os.writeall(c.out, "\n".ptr, 1u64); + } else { + // Root/primary: always reset so a preceding imported section's + // sticky pathmod is cleared; a packaged primary's own `package` + // clause then sets curmod fresh (pathmod NULL → real clause). let d: str = "//ww:module-reset\n"; os.writeall(c.out, d.ptr, d.len: u64); }; @@ -751,7 +774,7 @@ fn strictpkgmismatch(file: *u8, pkg: *u8, dirpkg: *u8, dirpath: *u8) void = { // are pulled once. Strict-same-package: all enumerated files must // declare the same `package ;` (task #23 subset; failure // mode native to dir-enum). -fn expanddir(c: *expctx, dirpath: *u8) void = { +fn expanddir(c: *expctx, dirpath: *u8, modpath: str) void = { let names: **u8; let n: i32; names, n = enumeratedir(dirpath); @@ -774,7 +797,7 @@ fn expanddir(c: *expctx, dirpath: *u8) void = { strictpkgmismatch(fp.ptr, pkg, dirpkg, dirpath); }; }; }; - expand(c, fp.ptr); + expand(c, fp.ptr, modpath); i += 1; }; }; @@ -963,12 +986,12 @@ fn buildone(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, objstem: *u8, inc let td: i32 = 0; let tp: *u8 = locateimport(searchpath.ptr, "test".ptr, "test".len: u64, &td); if (tp != nil) { - if (td != 0) { expanddir(&c, tp); } - else { expand(&c, tp); }; + if (td != 0) { expanddir(&c, tp, "test"); } + else { expand(&c, tp, ""); }; }; }; - if (entryisdir != 0) { expanddir(&c, srcd.ptr); } - else { expand(&c, src); }; + if (entryisdir != 0) { expanddir(&c, srcd.ptr, ""); } + else { expand(&c, src, ""); }; }; os.close(cf); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index d14a8a54..6f541b4e 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -1,3 +1,4 @@ +//ww:module time // time — clocks, instants, durations. Mirrors Hare's lib/time // (ref/hare/time/duration.ha, instant.ha, arithm.ha, // +linux/functions.ha). Calendar / date / strftime / timezone / @@ -95,6 +96,7 @@ export fn compare(a: instant, b: instant) i8 = { return 0i8; }; +//ww:module rt // rt — runtime primitives exposed to ww programs. // Mirrors Hare's rt:: module placement (ref/hare/rt/). @@ -118,6 +120,7 @@ package rt; // a future task (task #39). ref/hare/rt/malloc.ha:27. @symbol("rt_malloc") export fn malloc(n: u64) *void; +//ww:module os // os — process and filesystem facade. The body of each call lands // either in libwwrt.a (rt_syscall trampoline) or libc bindings, // depending on how the program was linked. @@ -856,6 +859,7 @@ export fn exists(path: str) bool = { return r >= 0i64; }; +//ww:module strconv // strconv — arbitrary-precision decimal engine for float↔string // conversion. Mirrors ref/hare/strconv/decimal.ha (Hare in turn ports // Go's lib/strconv/decimal.go). Pure integer arithmetic; no f32/f64 @@ -1178,6 +1182,7 @@ fn decimal_round(d: *decimal) u64 = { return n; }; +//ww:module math // floats — f64 classification, sign, bit-reinterpret core, and the f64 // decompose half (subnormal-normalize + frexp). Ported from // ref/hare/math/floats.ha (fold-1: classify/sign/bits; fold-2a: @@ -1452,6 +1457,7 @@ export fn frexpf64(n: f64) (f64, i64) = { return (mantissa, exp); }; +//ww:module math // math — numeric helpers. Subset of Hare's math::; only the absolute- // value pair for the signed integer types we currently care about. The // return type is unsigned so that abs(I32_MIN) doesn't overflow. @@ -1468,6 +1474,7 @@ export fn absi64(n: i64) u64 = { return n: u64; }; +//ww:module strconv // strconv — float→string via Ryū (shortest round-trippable decimal). // Mirrors ref/hare/strconv/ftos_ryu.ha (the algorithm core) + // ref/hare/strconv/ftos.ha:432 (the f64tos driver). Ryū: Ulf Adams, @@ -2262,6 +2269,7 @@ export fn f32tos(n: f32) str = { return r; }; +//ww:module strconv // strconv — Ryū float→string lookup tables + bit-count constants. // Mirrors ref/hare/strconv/ftos_ryu.ha:159-222 byte-exact. Pure data // fold (strconv #106 fold-5): no logic, consumed by ftos.ww's @@ -2373,6 +2381,7 @@ let POW5_TABLE: [26]u64 = [ 59604644775390625u64, 298023223876953125u64, ]; +//ww:module types // types — integer limits. Mirrors Hare's types::limits (I8_MAX, …) // platform-fixed for amd64. Numeric helpers live in lib/math, matching // Hare's split between types::limits and math::. @@ -2417,6 +2426,7 @@ def UINTPTR_MAX: uintptr = U64_MAX: uintptr; def RUNE_MIN: rune = '\0'; +//ww:module bytes // bytes — slice operations over []u8. Mirrors Hare's bytes module // (ref/hare/bytes/) for the in-tree subset: search/equality/prefix // helpers used by lib/encoding, lib/bufio, lib/memio. @@ -2968,6 +2978,7 @@ export fn rcut(in: []u8, delim: (u8 | []u8)) ([]u8, []u8) = { }; }; +//ww:module encoding.utf8 // encoding/utf8 — UTF-8 encode/decode. Hare port; see // ref/hare/encoding/utf8/{types,rune,encode,decode,decodetable}.ha. // @@ -3425,6 +3436,7 @@ export fn position(d: *decoder) i32 = { }; +//ww:module strings // strings — operations over str ({ptr,len}). Hare port; see // ref/hare/strings/. // @@ -4377,6 +4389,7 @@ export fn rpad(s: str, p: rune, maxlen: i32) str = { return frombytes(buf); }; +//ww:module ascii // ascii — rune-class predicates and case folding for the ASCII range. // Matches Hare's ascii::isdigit family (rune-taking signature). Runes // outside 0..127 always answer `false`. The lexer hot path uses these @@ -4581,6 +4594,7 @@ export fn strupper_buf(s: str, buf: []u8) (str | nomem) = { return strings.frombytes(buf); }; +//ww:module strconv // strconv — string-to-float. Mirrors ref/hare/strconv/stof.ha // (Hare in turn adapts Go): Eisel-Lemire fast path [1] with the // Simple-Decimal-Conversion slow path [2] (decimal.ww) as fallback. @@ -5280,6 +5294,7 @@ export fn stof32(s: str, b: base) (f32 | invalid | overflow) = { return 0: invalid; // unreachable (path-cov) }; +//ww:module strconv // strconv — stof/ftos lookup tables. Mirrors ref/hare/strconv/stof_data.ha // byte-exact. Pure-data fold (strconv #106 fold-2, was fold-3 before drew // re-sequenced 2026-05-26): no logic, exercised transitively when fold-3's @@ -5979,6 +5994,7 @@ let powers_of_ten: [596][2]u64 = [ [0x73832EEC6FFF3111u64, 0xD226FC195C6A2F8Cu64], ]; +//ww:module strconv // strconv — number↔string conversions. // // Mirrors Hare's strconv:: surface. The *tos functions return a @@ -6397,6 +6413,7 @@ export fn strerror(e: error) str = { return strings.dup(""); }; +//ww:module-reset // lib/ww/lex/tok.ww — port of cmd/wcc/tok.c plus the Tkind / // Tok / Pos shapes from cmd/wcc/ww.h. // @@ -6520,7 +6537,10 @@ type tkind = enum i32 { TK_MODRESET = 87, // `//ww:module-reset` driver bundle boundary: // reset curmod to "" before a package-less file // (#16 option-B; cstage TK_MODRESET twin) - TK_LAST = 88, + TK_MODPATH = 88, // `//ww:module ` driver import + // boundary; decls mangle on the path, not the + // leaf `package` clause (M1 #22; cstage twin) + TK_LAST = 89, }; // ---- Pos / Tok -------------------------------------------------------- @@ -6643,6 +6663,7 @@ export fn tokname(k: tkind) str = { case tkind.TK_ENUM: return "enum"; case tkind.TK_MODULE: return "package"; case tkind.TK_MODRESET: return "//ww:module-reset"; + case tkind.TK_MODPATH: return "//ww:module"; case tkind.TK_LPAREN: return "("; case tkind.TK_RPAREN: return ")"; @@ -6797,6 +6818,7 @@ export fn tokprint(fd: i32, t: *tok) void = { fputcbyte(fd, '\n'); }; +//ww:module lex // lib/ww/lex/lex.ww — port of cmd/wcc/lex.c. // // The DFA, the helpers, and the order of decisions all mirror the C @@ -6858,6 +6880,10 @@ type lex = struct { // a `//ww:module-reset` directive was seen in the last skipped run; // lexnext emits TK_MODRESET before the next real token (#16 opt-B). modreset: i32, + // a `//ww:module ` directive was seen in the last skipped run; + // lexnext emits TK_MODPATH carrying this dotted path (M1 #22). + modpathset: i32, + modpath: str, }; export fn lexinit(l: *lex, file: str, src: *u8, len: u64) void = { @@ -6869,6 +6895,7 @@ export fn lexinit(l: *lex, file: str, src: *u8, len: u64) void = { l.col = 1; l.errs = 0; l.modreset = 0; + l.modpathset = 0; }; // srcb — byte at offset; helper that lifts the cast out of indexing. @@ -6936,19 +6963,62 @@ fn skipws(l: *lex) bool = { // The body is then skipped like any comment. // Mirrors cstage lex.c skipws. Compare via lpeek // (no consume) so the skip loop below is unchanged. - let dir: str = "ww:module-reset"; + let pre: str = "ww:module"; let di: i32 = 0; let matched: bool = true; - for (di < dir.len) { - if (lpeek(l, di: u64) != dir[di]: i32) { + for (di < pre.len) { + if (lpeek(l, di: u64) != pre[di]: i32) { matched = false; break; }; di += 1; }; if (matched) { - let nx: i32 = lpeek(l, dir.len: u64); - if (nx == '\n') { l.modreset = 1; } - else { if (nx < 0) { l.modreset = 1; }; }; + let nx: i32 = lpeek(l, pre.len: u64); + if (nx == '-') { + let rest: str = "-reset"; + let rj: i32 = 0; + let rm: bool = true; + for (rj < rest.len) { + if (lpeek(l, (pre.len + rj): u64) + != rest[rj]: i32) { + rm = false; break; + }; + rj += 1; + }; + if (rm) { + let af: i32 = lpeek(l, + (pre.len + rest.len): u64); + if (af == '\n') { l.modreset = 1; } + else { if (af < 0) { l.modreset = 1; }; }; + }; + } else { if (nx == ' ' || nx == '\t') { + // `//ww:module ` — M1 import boundary. + let k: u64 = pre.len: u64; + for (true) { + let sc: i32 = lpeek(l, k); + if (sc == ' ' || sc == '\t') { + k += 1u64; continue; + }; + break; + }; + let s0: u64 = k; + for (true) { + let pc: i32 = lpeek(l, k); + if (pc < 0) { break; }; + if (pc == '\n' || pc == '\r' + || pc == ' ' || pc == '\t') { + break; + }; + k += 1u64; + }; + if (k > s0) { + let view: str; + view.ptr = l.src + l.lpos + s0; + view.len = (k - s0): i32; + l.modpath = strings.dup(view); + l.modpathset = 1; + }; + }; }; }; for (true) { let cx: i32 = lpeek(l, 0u64); @@ -7462,6 +7532,12 @@ export fn lexnext(l: *lex, out: *tok) void = { emitsimple(&start, tkind.TK_MODRESET, out); return; }; + if (l.modpathset != 0) { + l.modpathset = 0; + emitsimple(&start, tkind.TK_MODPATH, out); + out.text = l.modpath; + return; + }; if (!more) { emitsimple(&start, tkind.TK_EOF, out); return; @@ -7578,6 +7654,7 @@ export fn lexnext(l: *lex, out: *tok) void = { out.text = strings.dup(view); }; +//ww:module-reset // lib/ww/ast.ww — port of cmd/wcc/ast.c (Node defs + printer). // // Status: AST printer is fully ported. Constructor `newnode` is here. @@ -7717,13 +7794,17 @@ type node = struct { type_: *void, // filled in by checker; type.ww treats it as *tinfo tsuffix: str, // typed numeric literal suffix ("i32", "u64", ...) nmod: str, // originating module from `// MODULE: foo`; "" if none + usepath: str, // on an N_USE: full dotted IMPORT path vs leaf alias + // in `str` (M1 #22); "" otherwise + imported: i32, // M1 #22: decl reached via `//ww:module ` import + // boundary (vs root/primary); gates root-only bare main }; export fn newnode(k: nkind, file: str, line: i32, col: i32) *node = { // fval cast-init: 990's wwdump TK_FLOAT diff requires this file // to tokenise identically through C and ww (lex.ww:382 has the // same workaround for the cstage %g-formats vs ww-skips divergence). - let n: *node = alloc(node{kind=k, file=file, line=line, col=col, op=tkind.TK_NONE, str="", uval=0u64, fval=0: f64, lhs=nil, rhs=nil, cond=nil, body=nil, els=nil, list=nil, next=nil, attr=nil, exported=0, type_=nil, tsuffix="", nmod=""})!; + let n: *node = alloc(node{kind=k, file=file, line=line, col=col, op=tkind.TK_NONE, str="", uval=0u64, fval=0: f64, lhs=nil, rhs=nil, cond=nil, body=nil, els=nil, list=nil, next=nil, attr=nil, exported=0, type_=nil, tsuffix="", nmod="", usepath="", imported=0})!; return n; }; @@ -7965,11 +8046,13 @@ export fn astprint(fd: i32, n: *node) void = { pr(fd, n, 0); }; +//ww:module parse // lib/ww/parse/decl.ww — declaration parsing, split out of parse.ww. package parse; import os; +import strings; import tok; // `import encoding.utf8;` — the driver resolves the dotted path to @@ -7984,13 +8067,19 @@ fn parseuse(p: *parser) *node = { advance(p); // past `use` let n: *node = newnode(nkind.N_USE, pf, pl, pc); n.nmod = p.curmod; + // M1 #22: accumulate the full dotted import path (n.usepath) for the + // checker's path-keyed module match; n.str stays the leaf alias the + // user writes (`utf8.x`). let leaf: str; expectident(p, &leaf); + let path: str = leaf; for (p.curkind == tkind.TK_DOT) { advance(p); // past `.` expectident(p, &leaf); + path = strings.concat(path, ".", leaf); }; n.str = leaf; + n.usepath = path; expecttok(p, tkind.TK_SEMI, "expected ';' after use"); return n; }; @@ -8147,6 +8236,7 @@ fn parsetypedecl(p: *parser, exported: i32) *node = { }; +//ww:module parse // lib/ww/parse/expr.ww — expression parsing, split out of parse.ww. package parse; @@ -8616,6 +8706,7 @@ fn parseexpr(p: *parser) *node = { }; +//ww:module parse // lib/ww/parse/parse.ww — port of cmd/wcc/parse.c (entry + plumbing). // // Split into Hare-style submodule: parse.ww (here) holds the parser @@ -8634,6 +8725,7 @@ package parse; // dir-enum when callers `import parse;` (which dir-enums // lib/ww/parse/). import os; +import strings; import tok; type parser = struct { @@ -8661,6 +8753,10 @@ type parser = struct { // multi-file streams successive `module` decls mark per-file // section boundaries. Mirrors cstage Parser.curmod. curmod: str, + // M1 #22: active `//ww:module ` dotted import path. While set, + // decls stamp nmod=pathmod and imported=1, and the in-file `package` + // clause is an assertion. "" means inactive (root/primary). + pathmod: str, }; fn refill(p: *parser) void = { @@ -8680,6 +8776,7 @@ export fn parserinit(p: *parser, l: *lex) void = { p.l = l; p.errs = 0; p.nocast = 0; + p.pathmod = ""; refill(p); }; @@ -9026,7 +9123,30 @@ export fn parsefile(p: *parser) *node = { let name: str; expectident(p, &name); expecttok(p, tkind.TK_SEMI, "expected ';' after module name"); - p.curmod = name; + if (p.pathmod.len != 0) { + // M1 #22: while an import path is active the in-file + // `package` clause is an ASSERTION — its leaf must + // equal the path's last component; it does NOT + // overwrite the path-derived module. + let (pre, post) = strings.rcut(p.pathmod, "."); + let last: str = post; + if (post.len == 0) { last = p.pathmod; }; + if (strings.compare(name, last) != 0) { + errmsg(p, "package does not match import path"); + }; + } else { + p.curmod = name; + }; + continue; + }; + // `//ww:module ` — M1 #22 import boundary. The following + // file's decls mangle on the full dotted import path, not the + // leaf `package` clause, and are flagged imported (gates the + // root-only bare-`main` rule, #32). + if (p.curkind == tkind.TK_MODPATH) { + p.pathmod = p.curtext; + p.curmod = p.curtext; + advance(p); continue; }; // `//ww:module-reset` — bundle boundary before a package-less @@ -9043,6 +9163,7 @@ export fn parsefile(p: *parser) *node = { empty.ptr = nil; empty.len = 0; p.curmod = empty; + p.pathmod = ""; continue; }; let attrs = parseattrs(p); @@ -9094,6 +9215,9 @@ export fn parsefile(p: *parser) *node = { };};};};};}; if (d != nil) { + // M1 #22: flag decls reached via an import-path boundary + // (gates the root-only bare-`main` rule, #32). + if (p.pathmod.len != 0) { d.imported = 1; }; if (head == nil) { head = d; tail = d; @@ -9107,6 +9231,7 @@ export fn parsefile(p: *parser) *node = { return f; }; +//ww:module parse // lib/ww/parse/stmt.ww — statement parsing, split out of parse.ww. package parse; @@ -9521,6 +9646,7 @@ fn parsestmt(p: *parser) *node = { }; +//ww:module-reset // lib/ww/typ.ww — port of cmd/wcc/type.c. // // Status: full structural port. The C version uses module-globals for @@ -10137,6 +10263,7 @@ export fn typeeq(a: *tinfo, b: *tinfo) bool = { return true; // primitives match by kind alone }; +//ww:module-reset // lib/ww/sym.ww — port of cmd/wcc/sym.c. // // Per-scope hashtable, chained to the parent. Lookup walks up. @@ -10472,6 +10599,7 @@ export fn scopesamekeysym(s: *scope, name: str, mod: str) *sym = { return nil; }; +//ww:module-reset // selfhost/cmd/wcc/check.ww — minimal port of cmd/wcc/check.c. // // Status: name-resolution + primitive-type seeding only. Full type @@ -10638,14 +10766,48 @@ fn declmod(file: *node, d: *node) str = { if (file == nil) { return empty; }; let u: *node = file.list; for (u != nil) { + // M1 #22: a decl is imported iff some `use` directive's full + // dotted import path equals the decl's module (now the path). + // Single-level packages have usepath == leaf so this is + // unchanged; nested (`encoding.utf8`) match here, not on leaf. if (u.kind == nkind.N_USE) { - if (streq(u.str, d.nmod)) { return d.nmod; }; + if (streq(u.usepath, d.nmod)) { return d.nmod; }; }; u = u.next; }; return empty; }; +// usepath — map a `use` alias (leaf bareword the user writes, `utf8`) +// to the full dotted import path it binds (`encoding.utf8`), for the +// module-qualified resolution and codegen hint (M1 #22). Single-level +// packages have usepath == alias so the result is unchanged. The +// current tree has one occurrence per leaf, so the map is unambiguous. +fn usepathfor(file: *node, alias: str) str = { + let empty: str; + if (file == nil) { return empty; }; + if (alias.len == 0) { return empty; }; + let u: *node = file.list; + for (u != nil) { + if (u.kind == nkind.N_USE) { + if (streq(u.str, alias)) { + if (u.usepath.len != 0) { return u.usepath; }; + return u.str; + }; + }; + u = u.next; + }; + return empty; +}; + +// modkeyfor — usepathfor with leaf-alias fallback: the module key for a +// path-keyed scopelookupinmodule given the alias the user wrote (M1 #22). +fn modkeyfor(c: *checker, alias: str) str = { + let mk: str = usepathfor(c.file, alias); + if (mk.len == 0) { return alias; }; + return mk; +}; + // srcimports — does the source file that contributed decl-module // `modtag` carry `use ;`? Mirrors cstage's src_imports — // `modtag.len == 0` means primary, matching declmod's empty-str @@ -10662,7 +10824,7 @@ fn srcimports(file: *node, modtag: str, name: str) bool = { // module bareword and lib/fmt's own // `fn bsprintf(fmt: str, ...)` is not a shadow. if (u.nmod.len > 0) { - if (streq(u.nmod, u.str)) { + if (streq(u.nmod, u.usepath)) { u = u.next; continue; }; @@ -10751,7 +10913,9 @@ fn installdecl(c: *checker, file: *node, d: *node) void = { // pulls lack import->file->symbol provenance). Message byte-identical // to cstage check.c. if (k == nkind.N_USE) { - if (mod.len != 0 && streq(nm, mod)) { + // M1 #22: self-import ⟺ the imported path equals the use's own + // (owning) module path. Compares paths, not leaves. + if (mod.len != 0 && streq(d.usepath, mod)) { cerr("self-import: package '"); cerr(mod); cerr("' cannot import itself\n"); c.errs += 1i32; }; @@ -10960,7 +11124,7 @@ fn resolvewalk(c: *checker, n: *node) void = { let leaf: str; leaf.ptr = nm.ptr + (dot + 1): u64; leaf.len = nm.len - (dot + 1); - s = scopelookupinmodule(c.cur, head, leaf); + s = scopelookupinmodule(c.cur, modkeyfor(c, head), leaf); }; }; }; @@ -11358,7 +11522,7 @@ fn aliassym(c: *checker, n: *node) *sym = { let leaf: str; leaf.ptr = nm.ptr + ((dotidx + 1): u64); leaf.len = nm.len - dotidx - 1; - s = scopelookupinmodule(c.cur, head, leaf); + s = scopelookupinmodule(c.cur, modkeyfor(c, head), leaf); } else { // #53: same-module preference. Mirrors cstage // cmd/wcc/check.c:66 scope_lookup_prefer. Without this, @@ -11609,7 +11773,7 @@ fn scruttype(c: *checker, e: *node) *node = { if (e.kind == nkind.N_DOT) { if (e.lhs == nil) { return nil; }; if (e.lhs.kind != nkind.N_IDENT) { return nil; }; - let s: *sym = scopelookupinmodule(c.cur, e.lhs.str, e.str); + let s: *sym = scopelookupinmodule(c.cur, modkeyfor(c, e.lhs.str), e.str); if (s == nil) { return nil; }; if (s.decl == nil) { return nil; }; return s.decl.lhs; @@ -12148,7 +12312,7 @@ fn evaldefconst(c: *checker, n: *node, out: *u64, depth: i32) bool = { if (k == nkind.N_DOT) { if (n.lhs == nil) { return false; }; if (n.lhs.kind != nkind.N_IDENT) { return false; }; - let s: *sym = scopelookupinmodule(c.cur, n.lhs.str, n.str); + let s: *sym = scopelookupinmodule(c.cur, modkeyfor(c, n.lhs.str), n.str); if (s == nil) { return false; }; if (s.skind != skind.SK_DEF) { return false; }; if (s.decl == nil) { return false; }; @@ -13265,7 +13429,7 @@ fn unoptype(c: *checker, e: *node) *node = { // cstage's actual acceptance reason. if (e.lhs.kind == nkind.N_DOT) { if (e.lhs.lhs != nil && e.lhs.lhs.kind == nkind.N_IDENT) { - let fs: *sym = scopelookupinmodule(c.cur, e.lhs.lhs.str, e.lhs.str); + let fs: *sym = scopelookupinmodule(c.cur, modkeyfor(c, e.lhs.lhs.str), e.lhs.str); if (fs != nil) { if (fs.skind == skind.SK_FN) { if (fs.decl != nil) { @@ -13923,7 +14087,7 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { // global-leaf path) and harec check_autodereference // (ref/harec/src/check.c:1566-1581). if (ms != nil && (ms.skind == skind.SK_USE || ms.use_alias != 0i32)) { - s = scopelookupinmodule(c.cur, callee.lhs.str, nm); + s = scopelookupinmodule(c.cur, modkeyfor(c, callee.lhs.str), nm); }; }; if (s != nil) { if (s.skind == skind.SK_FN) { if (s.decl != nil) { @@ -13983,7 +14147,7 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { // qualified resolution and the lenient checker policy // keeps the silent miss documented at scruttype L656. if (ms.skind == skind.SK_USE || ms.use_alias != 0i32) { - let fs: *sym = scopelookupinmodule(c.cur, lhsn.str, e.str); + let fs: *sym = scopelookupinmodule(c.cur, modkeyfor(c, lhsn.str), e.str); if (fs != nil) { if (fs.decl != nil) { // #34: a module-qualified bare fn rvalue `mod.fn` types as // its FN TYPE (twin of the N_IDENT arm, :2688); decl.lhs is @@ -15840,7 +16004,7 @@ fn calleefndecl(c: *checker, callee: *node) *node = { }; if (ms != nil) { if (ms.skind == skind.SK_USE || ms.use_alias != 0i32) { - let fs: *sym = scopelookupinmodule(c.cur, callee.lhs.str, callee.str); + let fs: *sym = scopelookupinmodule(c.cur, modkeyfor(c, callee.lhs.str), callee.str); if (fs != nil) { if (fs.skind == skind.SK_FN) { return fs.decl; }; }; @@ -16794,29 +16958,25 @@ export fn checkfile(c: *checker, file: *node) void = { d = d.next; }; - // Program-global, name-only, cross-module uniqueness on `main`. - // `main` lowers to ONE bare entry symbol, so a second top-level - // decl named `main` (any kind, any package) collides with the - // entry at link time — today a silent segfault / link-fail in - // both stages. The (name, module) duplicate rejects in installtop - // read a cross-package `foo.main` and the bare entry as distinct, - // so they miss this. Correct multi-main mangling (entry stays bare, - // the rest qualify) is deferred (task #32); reject loudly meanwhile - // (rule 7). Walks USER decls only — runs before the -T synth main - // is appended below — so a hosted-test build never false-counts. - // Twin of cmd/wcc/check.c. + // Program-global uniqueness on the ENTRY `main`. M1 #32: the entry is + // the ROOT-unit main (imported==0) — it alone lowers to the bare + // `main` symbol. An IMPORTED package's `main` (imported==1) mangles on + // its path (`foo.bar.main`) and may coexist, closing the old dup-main + // collision by construction (#31). Two ROOT entries still collide on + // the bare symbol → reject loud (rule 7). Walks USER decls only — runs + // before the -T synth main is appended below. Twin of cmd/wcc/check.c. let firstmain: *node = nil; let mm: *node = file.list; for (mm != nil) { let ismain: bool = (mm.kind == nkind.N_FNDECL || mm.kind == nkind.N_LET || mm.kind == nkind.N_DEF || mm.kind == nkind.N_TYPEDECL) && streq(mm.str, "main"); - if (ismain) { + if (ismain && mm.imported == 0) { if (firstmain == nil) { firstmain = mm; } else { cerr(mm.file); - cerr(": error: duplicate top-level main: only the entry main may exist (task #32)\n"); + cerr(": error: duplicate entry main: only one root main may exist (#32)\n"); c.errs += 1; }; }; @@ -17146,6 +17306,7 @@ export fn checkfile(c: *checker, file: *node) void = { }; +//ww:module io // io — stream interface (Plan 9 Bio / Hare io::stream shape). // // No closures, no methods. A `stream` is a pointer to a `vtable` of @@ -17173,6 +17334,7 @@ export type eof = void; // underlying buffer-length type is i32 today. export type underread = !i32; +//ww:module io // stream — Hare-shaped vtable surface. Project #94 fold-eFinal. // // The single io stream surface (the fold-eFinal collapse retired the @@ -17396,6 +17558,7 @@ export fn empty() stream = { return &_empty_vt; }; +//ww:module errors // errors — domain-agnostic error types. Mirrors ref/hare/errors/. // // Named-void tagged-union variants, so `(T | errors.invalid | ...)` @@ -17532,6 +17695,7 @@ fn rt_strerror(op: *opaque_data) str = { return os.strerror(*e); }; +//ww:module io // types — error union, mode/whence enums, reader/writer/closer // fn-type aliases. Project #94 fold-eFinal; the fn-aliases target // `stream` (= `*vtable`, the single io surface). @@ -17617,6 +17781,7 @@ export type closer = fn(s: stream) (void | error); // `stream` directly, mirroring the reader/writer/closer aliases above. export type seeker = fn(s: stream, off: off, w: whence) (off | error); +//ww:module memio // memio — in-memory io stream. Project #94 fold-eFinal. // // Hare's memio:: surface, drop underscores. Two flavours behind a @@ -17922,6 +18087,7 @@ export fn borrowedread(s: *stream, amt: i32) ([]u8 | io.eof) = { return r; }; +//ww:module-reset // selfhost/cmd/wcc/cgenutil.ww — split out of cgen.ww. // // General helpers used across cgenexpr / cgenstmt / cgendecl: @@ -20091,10 +20257,14 @@ fn structlookup(c: *cgen, name: str) *structinfo = { let leaf: str; leaf.ptr = name.ptr + ((i + 1): u64); leaf.len = name.len - (i + 1); + // M1 #22: the embedded qualifier (`utf8`) is the use ALIAS; + // the struct's smod is now the dotted import PATH + // (`encoding.utf8`). Map alias→path before comparing. + let pkgmod: str = usehint(c, pkg); let b: *structinfo = c.structs; for (b != nil) { if (streq(b.sname, leaf)) { - if (streq(b.smod, pkg)) { + if (streq(b.smod, pkgmod)) { return b; }; }; @@ -23474,6 +23644,7 @@ fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *node, bpoff: i32) void = { cgstructlitfill(c, si, lit, 0, 0, "", bpoff); }; +//ww:module-reset // selfhost/cmd/wcc/cgenexpr.ww — split out of cgen.ww. // // cgexpr is a thin dispatcher over n.kind; each non-trivial branch @@ -27782,10 +27953,10 @@ fn cgdot(c: *cgen, n: *node) void = { // lhs.str is the explicit module hint so a same-leaf // def in another module (head of c.fnrets) can't shadow // the explicit qualifier (#17 N_DOT-arm omission audit). - let frt: *node = fnretlookupmod(c, fld, lhs.str); + let frt: *node = fnretlookupmod(c, fld, usehint(c, lhs.str)); if (frt != nil) { emitline("\tLEAQ\t"); - emitfnname(c, fld, lhs.str); + emitfnname(c, fld, usehint(c, lhs.str)); emitline("(SB), AX\n"); return; }; @@ -27797,7 +27968,7 @@ fn cgdot(c: *cgen, n: *node) void = { // the explicit module hint — a 3rd-module qualifier // `alpha.MSG` from gamma needs alpha (not c.curmod) // to beat a head-of-c.defs beta.MSG collision (#11). - let drhs: *node = deflookuprhsmod(c, fld, lhs.str); + let drhs: *node = deflookuprhsmod(c, fld, usehint(c, lhs.str)); if (drhs != nil) { if (drhs.kind == nkind.N_STRLIT) { let bytes: str = drhs.str; @@ -27818,11 +27989,11 @@ fn cgdot(c: *cgen, n: *node) void = { // threads lhs.str via emitfnname. if (streq(mqop, "MOVQ")) { emitline("\tMOVQ\t"); - emitsymnamehint(c, fld, lhs.str); + emitsymnamehint(c, fld, usehint(c, lhs.str)); emitline("(SB), AX\n"); } else { emitline("\tLEAQ\t"); - emitsymnamehint(c, fld, lhs.str); + emitsymnamehint(c, fld, usehint(c, lhs.str)); emitline("(SB), CX\n"); emitline("\t"); emitline(mqop); @@ -28702,10 +28873,10 @@ fn cgun(c: *cgen, n: *node) void = { // pre-#149 gap (file as #150-family). if (!isletvar(c, basenm) && !deflookup(c, basenm)) { let fld: str = opnd.str; - let frt: *node = fnretlookupmod(c, fld, basenm); + let frt: *node = fnretlookupmod(c, fld, usehint(c, basenm)); if (frt != nil) { emitline("\tLEAQ\t"); - emitfnname(c, fld, basenm); + emitfnname(c, fld, usehint(c, basenm)); emitline("(SB), AX\n"); return; }; @@ -28714,7 +28885,7 @@ fn cgun(c: *cgen, n: *node) void = { // &aa.v takes aa's global, not a // same-leaf collision. emitline("\tLEAQ\t"); - emitsymnamehint(c, fld, basenm); + emitsymnamehint(c, fld, usehint(c, basenm)); emitline("(SB), AX\n"); return; }; @@ -31703,7 +31874,7 @@ fn cgcall(c: *cgen, n: *node) void = { hint.len = 0; if (callee.lhs != nil) { if (callee.lhs.kind == nkind.N_IDENT) { - hint = callee.lhs.str; + hint = usehint(c, callee.lhs.str); }; }; emitfnname(c, calleename, hint); @@ -36013,6 +36184,7 @@ fn cgassign(c: *cgen, n: *node) void = { +//ww:module-reset // selfhost/cmd/wcc/cgenstmt.ww — split out of cgen.ww. // // cgstmt is a thin dispatcher over n.kind; each branch defers to a @@ -40362,6 +40534,7 @@ fn cgcontinue(c: *cgen, n: *node) void = { +//ww:module-reset // selfhost/cmd/wcc/cgendecl.ww — split out of cgen.ww. // // Houses the top-level emission glue: @@ -40966,9 +41139,17 @@ fn cgfn(c: *cgen, fn_: *node) void = { // Emit the TEXT label via emitfnname so the def site picks up the // same skip rule (FFI / `main` / empty-module) and the same module - // hint (this fn's own module) that the call sites use. + // hint (this fn's own module) that the call sites use. M1 #32: the + // ROOT-unit main (imported==0) is the bare `_start` entry — emit it + // bare directly, mirroring collectmods' skip; without this its + // fn_.nmod hint would fall through modlookupforfn's first-leaf match + // onto an IMPORTED package's now-registered `pkg.main`. emitline("TEXT "); - emitfnname(c, fn_.str, fn_.nmod); + if (streq(fn_.str, "main") && fn_.imported == 0) { + emitbytes(fn_.str.ptr, fn_.str.len: u64); + } else { + emitfnname(c, fn_.str, fn_.nmod); + }; emitline(",$"); emitint(frame: i64); emitline("\n"); @@ -41015,6 +41196,7 @@ export fn cgfile(c: *cgen, file: *node) void = { emitletdataw(c, file); }; +//ww:module-reset // selfhost/cmd/wcc/cgen.ww — port of cmd/w6c/cgen.c. // // Status: GROWING. Each subsystem we add is verified by `wwdump_ww -c` @@ -41140,10 +41322,13 @@ fn aliaslookup(c: *cgen, name: str) *node = { let leaf: str; leaf.ptr = name.ptr + ((i + 1): u64); leaf.len = name.len - (i + 1); + // M1 #22: map the embedded use ALIAS (`utf8`) to the + // dotted import PATH the decl's module now carries. + let pkgmod: str = usehint(c, pkg); let b: *aliasent = c.aliases; for (b != nil) { if (streq(b.aname, leaf)) { - if (streq(b.amod, pkg)) { + if (streq(b.amod, pkgmod)) { return b.target; }; }; @@ -41328,10 +41513,13 @@ fn enumlookup(c: *cgen, name: str) *enumtype = { let leaf: str; leaf.ptr = name.ptr + ((i + 1): u64); leaf.len = name.len - (i + 1); + // M1 #22: map the embedded use ALIAS (`utf8`) to the + // dotted import PATH the decl's module now carries. + let pkgmod: str = usehint(c, pkg); let b: *enumtype = c.enums; for (b != nil) { if (streq(b.ename, leaf)) { - if (streq(b.emod, pkg)) { + if (streq(b.emod, pkgmod)) { return b; }; }; @@ -41502,6 +41690,9 @@ type cgen = struct { enums: *enumtype, mods: *modent, // fn (any export status) + non-exported // let/def/type decls → originating module + uses: *modent, // M1 #22: N_USE alias → dotted import path, + // for the qualified-ref codegen hint + // (mname=alias, nmod=path) lets: *letvar, // top-level mutable scalar `let` bindings fnname: str, curmod: str, // current fn's `// MODULE: foo` directive (len=0 @@ -43877,7 +44068,7 @@ fn emittuplerowrelocs(c: *cgen, name: str, module: str, backing: bool, rowoff: i // the leaf with the MODULE ident; same-module `&fn` // stays on curmod. if (ev.lhs.kind == nkind.N_DOT) { - emitfnname(c, ev.lhs.str, ev.lhs.lhs.str); + emitfnname(c, ev.lhs.str, usehint(c, ev.lhs.lhs.str)); } else { emitfnname(c, ev.lhs.str, c.curmod); }; @@ -44064,7 +44255,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { // leaf with the MODULE ident; same-module `&fn` // stays on curmod. if (r.lhs.kind == nkind.N_DOT) { - emitfnname(c, r.lhs.str, r.lhs.lhs.str); + emitfnname(c, r.lhs.str, usehint(c, r.lhs.lhs.str)); } else { emitfnname(c, r.lhs.str, c.curmod); }; @@ -44535,11 +44726,24 @@ fn fnretlookup(c: *cgen, name: str) *node = { // the scrutinee tagged type to `(rune | done)` — flatvariantidx then // can't see arms 2/3 and collapses them onto tag 0 (task #31). fn fnretlookupmod(c: *cgen, name: str, mod: str) *node = { - if (mod.len > 0) { + // M1 #22 (#199b): the qualifier may be the import ALIAS the user + // wrote (`utf8`); fn decls register f.fmod under the dotted import + // PATH (`encoding.utf8`). Map alias->path so a nested-package callee + // matches its own module instead of falling back to the name-only + // pass — which a same-leaf caller-module fn (e.g. strings.next vs + // utf8.next) otherwise wins, resolving a match scrutinee to the + // caller's union and collapsing arms 2+. usehint is idempotent on a + // path / c.curmod (returns the input when no `use` matches), so the + // already-mapped callers (cgenexpr.ww:4309/5229) and the bare-ident + // c.curmod callers are unaffected. The choke-point twin of the + // struct/alias/enum usehint splitters (cgen.ww:128/319, + // cgenutil.ww:2173) — closes the whole fnret class by construction. + let mk: str = usehint(c, mod); + if (mk.len > 0) { let f: *fnret = c.fnrets; for (f != nil) { if (streq(f.fname, name)) { - if (streq(f.fmod, mod)) { return f.rtype; }; + if (streq(f.fmod, mk)) { return f.rtype; }; }; f = f.frnext; }; @@ -44599,11 +44803,15 @@ fn samemodfn(c: *cgen, name: str) bool = { // matching module is registered — mirrors aliaslookup's two-pass shape // (cgen.ww:75, fixed in #27). fn fnparamslookupmod(c: *cgen, name: str, mod: str) *node = { - if (mod.len > 0) { + // M1 #22 (#199b): map import alias -> dotted path, identical to + // fnretlookupmod (the param-side twin). usehint is idempotent on a + // path / c.curmod so existing callers are unaffected. + let mk: str = usehint(c, mod); + if (mk.len > 0) { let f: *fnret = c.fnrets; for (f != nil) { if (streq(f.fname, name)) { - if (streq(f.fmod, mod)) { return f.params; }; + if (streq(f.fmod, mk)) { return f.params; }; }; f = f.frnext; }; @@ -44783,9 +44991,17 @@ type modent = struct { fn collectmods(c: *cgen, file: *node) void = { c.mods = nil; + c.uses = nil; if (file == nil) { return; }; let d: *node = file.list; for (d != nil) { + // M1 #22: record alias→path for the qualified-ref hint. + if (d.kind == nkind.N_USE) { + if (d.usepath.len > 0) { + let um: *modent = alloc(modent{mname=d.str, nmod=d.usepath, mnext=c.uses})!; + c.uses = um; + }; + }; // Mirror collectfnrets' shape exactly (plain prepend in one // branch). Earlier nested-if/early-return variants tickled a // wwstage cgen bug that dropped most prepends. @@ -44803,7 +45019,9 @@ fn collectmods(c: *cgen, file: *node) void = { a = a.next; }; if (!isffi) { - if (!streq(d.str, "main")) { + // M1 #32: the ROOT main (imported==0) stays bare; + // an IMPORTED `fn main` mangles on its path. + if (!streq(d.str, "main") || d.imported != 0) { let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, mnext=c.mods})!; c.mods = m; }; @@ -44850,6 +45068,19 @@ fn modlookup(c: *cgen, name: str) str = { return empty; }; +// usehint — M1 #22: map a qualified-ref alias (`utf8`) to its dotted +// import path (`encoding.utf8`) so the codegen hint keys the path-keyed +// mods map. For single-level packages alias == path (no-op). Returns the +// alias unchanged when no matching `use` exists. +fn usehint(c: *cgen, alias: str) str = { + let m: *modent = c.uses; + for (m != nil) { + if (streq(m.mname, alias)) { return m.nmod; }; + m = m.mnext; + }; + return alias; +}; + // modlookupforfn — hint-aware lookup for fn names. Walks c.mods // preferring entries where module matches `hint`; falls back to the // first leaf-name match when nothing matches the hint (legacy single- @@ -45041,6 +45272,7 @@ export fn fargregname(i: i32) str = { return "?"; }; +//ww:module-reset // selfhost/cmd/wwdump/main.ww — ww-side port of cmd/wwdump/main.c. // // Reads a .ww file, runs the ww-side lexer, prints tokens through diff --git a/selfhost/test/smoke.combined.ww b/selfhost/test/smoke.combined.ww index 000f681c..cfe9aab8 100644 --- a/selfhost/test/smoke.combined.ww +++ b/selfhost/test/smoke.combined.ww @@ -1,3 +1,4 @@ +//ww:module time // time — clocks, instants, durations. Mirrors Hare's lib/time // (ref/hare/time/duration.ha, instant.ha, arithm.ha, // +linux/functions.ha). Calendar / date / strftime / timezone / @@ -95,6 +96,7 @@ export fn compare(a: instant, b: instant) i8 = { return 0i8; }; +//ww:module rt // rt — runtime primitives exposed to ww programs. // Mirrors Hare's rt:: module placement (ref/hare/rt/). @@ -118,6 +120,7 @@ package rt; // a future task (task #39). ref/hare/rt/malloc.ha:27. @symbol("rt_malloc") export fn malloc(n: u64) *void; +//ww:module os // os — process and filesystem facade. The body of each call lands // either in libwwrt.a (rt_syscall trampoline) or libc bindings, // depending on how the program was linked. @@ -856,6 +859,7 @@ export fn exists(path: str) bool = { return r >= 0i64; }; +//ww:module strconv // strconv — arbitrary-precision decimal engine for float↔string // conversion. Mirrors ref/hare/strconv/decimal.ha (Hare in turn ports // Go's lib/strconv/decimal.go). Pure integer arithmetic; no f32/f64 @@ -1178,6 +1182,7 @@ fn decimal_round(d: *decimal) u64 = { return n; }; +//ww:module math // floats — f64 classification, sign, bit-reinterpret core, and the f64 // decompose half (subnormal-normalize + frexp). Ported from // ref/hare/math/floats.ha (fold-1: classify/sign/bits; fold-2a: @@ -1452,6 +1457,7 @@ export fn frexpf64(n: f64) (f64, i64) = { return (mantissa, exp); }; +//ww:module math // math — numeric helpers. Subset of Hare's math::; only the absolute- // value pair for the signed integer types we currently care about. The // return type is unsigned so that abs(I32_MIN) doesn't overflow. @@ -1468,6 +1474,7 @@ export fn absi64(n: i64) u64 = { return n: u64; }; +//ww:module strconv // strconv — float→string via Ryū (shortest round-trippable decimal). // Mirrors ref/hare/strconv/ftos_ryu.ha (the algorithm core) + // ref/hare/strconv/ftos.ha:432 (the f64tos driver). Ryū: Ulf Adams, @@ -2262,6 +2269,7 @@ export fn f32tos(n: f32) str = { return r; }; +//ww:module strconv // strconv — Ryū float→string lookup tables + bit-count constants. // Mirrors ref/hare/strconv/ftos_ryu.ha:159-222 byte-exact. Pure data // fold (strconv #106 fold-5): no logic, consumed by ftos.ww's @@ -2373,6 +2381,7 @@ let POW5_TABLE: [26]u64 = [ 59604644775390625u64, 298023223876953125u64, ]; +//ww:module types // types — integer limits. Mirrors Hare's types::limits (I8_MAX, …) // platform-fixed for amd64. Numeric helpers live in lib/math, matching // Hare's split between types::limits and math::. @@ -2417,6 +2426,7 @@ def UINTPTR_MAX: uintptr = U64_MAX: uintptr; def RUNE_MIN: rune = '\0'; +//ww:module bytes // bytes — slice operations over []u8. Mirrors Hare's bytes module // (ref/hare/bytes/) for the in-tree subset: search/equality/prefix // helpers used by lib/encoding, lib/bufio, lib/memio. @@ -2968,6 +2978,7 @@ export fn rcut(in: []u8, delim: (u8 | []u8)) ([]u8, []u8) = { }; }; +//ww:module encoding.utf8 // encoding/utf8 — UTF-8 encode/decode. Hare port; see // ref/hare/encoding/utf8/{types,rune,encode,decode,decodetable}.ha. // @@ -3425,6 +3436,7 @@ export fn position(d: *decoder) i32 = { }; +//ww:module strings // strings — operations over str ({ptr,len}). Hare port; see // ref/hare/strings/. // @@ -4377,6 +4389,7 @@ export fn rpad(s: str, p: rune, maxlen: i32) str = { return frombytes(buf); }; +//ww:module ascii // ascii — rune-class predicates and case folding for the ASCII range. // Matches Hare's ascii::isdigit family (rune-taking signature). Runes // outside 0..127 always answer `false`. The lexer hot path uses these @@ -4581,6 +4594,7 @@ export fn strupper_buf(s: str, buf: []u8) (str | nomem) = { return strings.frombytes(buf); }; +//ww:module strconv // strconv — string-to-float. Mirrors ref/hare/strconv/stof.ha // (Hare in turn adapts Go): Eisel-Lemire fast path [1] with the // Simple-Decimal-Conversion slow path [2] (decimal.ww) as fallback. @@ -5280,6 +5294,7 @@ export fn stof32(s: str, b: base) (f32 | invalid | overflow) = { return 0: invalid; // unreachable (path-cov) }; +//ww:module strconv // strconv — stof/ftos lookup tables. Mirrors ref/hare/strconv/stof_data.ha // byte-exact. Pure-data fold (strconv #106 fold-2, was fold-3 before drew // re-sequenced 2026-05-26): no logic, exercised transitively when fold-3's @@ -5979,6 +5994,7 @@ let powers_of_ten: [596][2]u64 = [ [0x73832EEC6FFF3111u64, 0xD226FC195C6A2F8Cu64], ]; +//ww:module strconv // strconv — number↔string conversions. // // Mirrors Hare's strconv:: surface. The *tos functions return a @@ -6397,6 +6413,7 @@ export fn strerror(e: error) str = { return strings.dup(""); }; +//ww:module-reset // selfhost/test/smoke.ww — end-to-end smoke for the selfhost path. // // Exercises the patterns the real ww-side compiler port will use: diff --git a/test/wcc/764_amp_fn_ident.c b/test/wcc/764_amp_fn_ident.c index 183409ab..bcbb9fb7 100644 --- a/test/wcc/764_amp_fn_ident.c +++ b/test/wcc/764_amp_fn_ident.c @@ -144,7 +144,11 @@ static const struct row rows[] = { /* Cross-mod: cstage emits the LEAQ via N_DOT TK_AMP (already * working pre-#180). Wwstage bails asserttyped on the same * shape — filed as #184; this row stays cstage-only until that - * lifts. */ + * lifts. M1 #22: `wcamffn764mod` is a directory package, so its + * exported `somefn` path-mangles to `wcamffn764mod.somefn` (the + * pre-M1 bare `somefn` is gone, mirroring 989_m1mangle_sym); the + * &-of hint routes through use_hint and the LEAQ matches the + * mangled definition. */ { "cross_module", "import wcamffn764mod;\n" "export fn main() i32 = {\n" @@ -153,7 +157,7 @@ static const struct row rows[] = { "};\n", "wcamffn764mod", "export fn somefn(x: i32) i32 = { return x + 1; };\n", - "LEAQ\tsomefn(SB)", + "LEAQ\twcamffn764mod.somefn(SB)", STAGE_CS }, }; diff --git a/test/wcc/989_m1mangle_run.c b/test/wcc/989_m1mangle_run.c new file mode 100644 index 00000000..db97f212 --- /dev/null +++ b/test/wcc/989_m1mangle_run.c @@ -0,0 +1,212 @@ +/* + * 989_m1mangle_run — M1 (#22 + #32): path-qualified symbol mangling and + * root-unit entry detection. Table-driven, both stages (rule-10: cstage + * `ww` and wwstage `ww_ww` must AGREE and hit want_exit). + * + * Each row lays out a tiny package tree next to a root `main.ww` and + * builds+runs the root. Because a directory import path-mangles its + * symbols (`aa.bb.val` etc.), a broken hint or skip rule shows up as a + * link failure (-1) or wrong runtime value — observable end-to-end. + * + * row | proves + * -----------------+-------------------------------------------------- + * nested_call | DIRECTORY import path-mangles + the qualified-ref + * | hint resolves to the PATH (aa.bb.val), not the + * | leaf alias — cross-boundary call returns 42 + * imported_main | #32/#31: an imported pkg's `fn main` mangles + * | (aa.bb.main) instead of colliding with the bare + * | root entry; both link, root main runs → 7 + * priv_global | a module-PRIVATE value global mangles on the path + * | (aa.bb.pv) and the owning module resolves it → 9 + * single_level | control: a single-level package's symbols are + * | UNCHANGED (cc.v stays cc.v) — still resolves → 5 + * deep_nest | 2-level nest aa.bb.cc → aa.bb.cc.val path-mangle → 3 + */ +#include +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return -1; +} + +struct file { const char *path; const char *content; }; + +struct row { + const char *label; + struct file files[4]; /* {NULL,NULL}-terminated package files */ + const char *root; /* root main.ww content */ + int want_exit; +}; + +static const struct row rows[] = { + { "nested_call", + { { "aa/bb/bb.ww", + "package bb;\n" + "export fn val() int = { return 42; };\n" }, + { NULL, NULL } }, + "package main;\n" + "import aa.bb;\n" + "export fn main() int = { return bb.val(); };\n", + 42 }, + + { "imported_main", + { { "aa/bb/bb.ww", + "package bb;\n" + "fn main() int = { return 7; };\n" + "export fn run() int = { return main(); };\n" }, + { NULL, NULL } }, + "package main;\n" + "import aa.bb;\n" + "export fn main() int = { return bb.run(); };\n", + 7 }, + + { "priv_global", + { { "aa/bb/bb.ww", + "package bb;\n" + "let pv: int = 9;\n" + "export fn getpv() int = { return pv; };\n" }, + { NULL, NULL } }, + "package main;\n" + "import aa.bb;\n" + "export fn main() int = { return bb.getpv(); };\n", + 9 }, + + { "single_level", + { { "cc/cc.ww", + "package cc;\n" + "export fn v() int = { return 5; };\n" }, + { NULL, NULL } }, + "package main;\n" + "import cc;\n" + "export fn main() int = { return cc.v(); };\n", + 5 }, + + { "deep_nest", + { { "aa/bb/cc/cc.ww", + "package cc;\n" + "export fn val() int = { return 3; };\n" }, + { NULL, NULL } }, + "package main;\n" + "import aa.bb.cc;\n" + "export fn main() int = { return cc.val(); };\n", + 3 }, +}; + +/* write_file — create `dir/rel` (mkdir -p its parents) with `content`. */ +static int +write_file(const char *dir, const char *rel, const char *content) +{ + char path[512], cmd[1024]; + snprintf(path, sizeof path, "%s/%s", dir, rel); + /* mkdir -p the parent of `path` */ + char parent[512]; + snprintf(parent, sizeof parent, "%s", path); + char *slash = strrchr(parent, '/'); + if (slash) { + *slash = '\0'; + snprintf(cmd, sizeof cmd, "mkdir -p '%s'", parent); + if (runwait(cmd) != 0) return -1; + } + FILE *f = fopen(path, "wb"); + if (!f) return -1; + fputs(content, f); + fclose(f); + return 0; +} + +/* run_build — lay out the row's package tree + root under a temp dir, + * build+run the root via `driver`; return the binary's exit code, or -1 + * on a build failure. */ +static int +run_build(const char *driver, const struct row *r, int i) +{ + char dir[64], cmd[2048]; + snprintf(dir, sizeof dir, "/tmp/m1mangle_%d_%d", getpid(), i); + snprintf(cmd, sizeof cmd, "rm -rf '%s' && mkdir -p '%s'", dir, dir); + if (runwait(cmd) != 0) return -2; + + for (int k = 0; r->files[k].path; k++) + if (write_file(dir, r->files[k].path, r->files[k].content) != 0) + return -2; + if (write_file(dir, "main.ww", r->root) != 0) return -2; + + /* cd into the build dir so the source-dir-first search path finds the + * sibling package tree (mirrors Hare's CWD == module-dir). */ + snprintf(cmd, sizeof cmd, "cd '%s' && %s build main.ww 2>/dev/null", + dir, driver); + int brc = runwait(cmd); + + int got = -1; + if (brc == 0) { + char outbin[128]; + snprintf(outbin, sizeof outbin, "%s/main", dir); + got = runwait(outbin); + } + + snprintf(cmd, sizeof cmd, "rm -rf '%s'", dir); + runwait(cmd); + return brc == 0 ? got : -1; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char cdrv[1024], wdrv[1024]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + struct { const char *name; const char *drv; int gated; } + drivers[] = { + { "cstage", cdrv, 0 }, + { "wwstage", wdrv, 1 }, + { NULL, NULL, 0 }, + }; + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + + for (int d = 0; drivers[d].name; d++) { + if (drivers[d].gated && access(drivers[d].drv, X_OK) != 0) { + fprintf(stderr, "m1mangle_run: skip %s (no %s)\n", + drivers[d].name, drivers[d].drv); + continue; + } + for (int i = 0; i < n; i++) { + total++; + int got = run_build(drivers[d].drv, &rows[i], i); + if (got != rows[i].want_exit) { + fprintf(stderr, "m1mangle_run[%s][%s]: exit=%d " + "want=%d\n", drivers[d].name, rows[i].label, + got, rows[i].want_exit); + fail++; + } + } + } + + if (fail) { + fprintf(stderr, "m1mangle_run: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("m1mangle_run: %d/%d ok\n", total, total); + return 0; +} diff --git a/test/wcc/989_m1mangle_sym.c b/test/wcc/989_m1mangle_sym.c new file mode 100644 index 00000000..c160d2a1 --- /dev/null +++ b/test/wcc/989_m1mangle_sym.c @@ -0,0 +1,149 @@ +/* + * 989_m1mangle_sym — M1 (#22) symbol-name proof. Builds a fixture that + * imports the real nested DIRECTORY package `encoding.utf8` to `.s` on + * BOTH stages and asserts the emitted symbol table: + * + * needle | want | proves + * --------------------------+------+----------------------------------- + * "encoding.utf8.runesz" | yes | nested pkg path-mangles (the M1 + * | | DELTA: leaf `utf8` → path + * | | `encoding.utf8`) + * "TEXT main," | yes | root entry stays BARE (#32) + * "TEXT utf8.runesz," | no | the pre-M1 leaf-only mangle is gone + * + * Plus cstage.s == wwstage.s byte-for-byte on the re-baselined names + * (rule-10 — the cs==ww gate over the new symbols). + */ +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return -1; +} + +static int +file_has(const char *path, const char *needle) +{ + FILE *f = fopen(path, "rb"); + if (!f) return -1; + char line[4096]; + int found = 0; + while (fgets(line, sizeof line, f)) { + if (strstr(line, needle)) { found = 1; break; } + } + fclose(f); + return found; +} + +struct check { const char *needle; int want; }; + +static const struct check checks[] = { + { "encoding.utf8.runesz", 1 }, + { "TEXT main,", 1 }, + { "TEXT utf8.runesz,", 0 }, +}; + +/* build_s — build `src` to `.s` via `driver`; returns 0 on success. */ +static int +build_s(const char *driver, const char *src, const char *stem) +{ + char cmd[1024]; + snprintf(cmd, sizeof cmd, "%s build -o '%s' '%s' 2>/dev/null", + driver, stem, src); + return runwait(cmd); +} + +static int +checkall(const char *name, const char *sp) +{ + int fail = 0; + for (int i = 0; i < (int)(sizeof checks / sizeof checks[0]); i++) { + int got = file_has(sp, checks[i].needle); + if (got != checks[i].want) { + fprintf(stderr, "m1mangle_sym[%s]: '%s' present=%d want=%d\n", + name, checks[i].needle, got, checks[i].want); + fail++; + } + } + return fail; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + char cdrv[1024], wdrv[1024]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + char src[64]; + snprintf(src, sizeof src, "/tmp/m1sym_%d.ww", getpid()); + FILE *f = fopen(src, "wb"); + if (!f) return 1; + fputs("package main;\n" + "import encoding.utf8;\n" + "export fn main() int = { return utf8.runesz('A'): int; };\n", f); + fclose(f); + + int fail = 0; + + char cstem[64], cs[80]; + snprintf(cstem, sizeof cstem, "/tmp/m1sym_c_%d", getpid()); + snprintf(cs, sizeof cs, "%s.s", cstem); + if (build_s(cdrv, src, cstem) != 0) { + fprintf(stderr, "m1mangle_sym: cstage build failed\n"); + fail++; + } else { + fail += checkall("cstage", cs); + } + + int have_ww = (access(wdrv, X_OK) == 0); + char wstem[64], ws[80]; + snprintf(wstem, sizeof wstem, "/tmp/m1sym_w_%d", getpid()); + snprintf(ws, sizeof ws, "%s.s", wstem); + if (have_ww) { + if (build_s(wdrv, src, wstem) != 0) { + fprintf(stderr, "m1mangle_sym: wwstage build failed\n"); + fail++; + } else { + fail += checkall("wwstage", ws); + /* rule-10: cs.s == ww.s on the new mangled names. */ + char cmp[256]; + snprintf(cmp, sizeof cmp, "cmp -s '%s' '%s'", cs, ws); + if (runwait(cmp) != 0) { + fprintf(stderr, "m1mangle_sym: cstage.s != " + "wwstage.s (byte-id break)\n"); + fail++; + } + } + } else { + fprintf(stderr, "m1mangle_sym: skip wwstage (no ww_ww)\n"); + } + + char cmd[256]; + snprintf(cmd, sizeof cmd, "rm -f '%s' '%s'* '%s'*", src, cstem, wstem); + runwait(cmd); + + if (fail) { + fprintf(stderr, "m1mangle_sym: %d checks failed\n", fail); + return 1; + } + printf("m1mangle_sym: ok\n"); + return 0; +} diff --git a/test/wcc/989_m1union_run.c b/test/wcc/989_m1union_run.c new file mode 100644 index 00000000..701866a7 --- /dev/null +++ b/test/wcc/989_m1union_run.c @@ -0,0 +1,225 @@ +/* + * 989_m1union_run — M1 (#199b): a `match` on a tagged union returned by a + * CROSS-MODULE fn must keep each nominal variant on a DISTINCT tag. The + * canonical case is `utf8.next` (encoding.utf8), whose return + * `(rune | done | more | invalid)` has three structurally-identical + * void-alias variants (done/more = void, invalid = !void). Pre-#199b the + * wwstage collapsed done/more/invalid onto one tag (a byte-id-only + * miscompile: the doc's repro hit the rune arm and returned the same value + * either way). This test is GATE-VISIBLE: each arm maps to a distinct + * return code and the inputs deterministically hit each arm, so a collapse + * mis-routes an arm and changes the runtime exit — caught on BOTH stages. + * + * row | input | arm | want_exit + * -------------+------------------+-----------+---------- + * rune | [65] | rune | 1 + * done | [65] next twice | done | 2 + * more | [195] (lead-only)| more | 3 + * invalid | [255] | invalid | 4 + * all_arms | all four, mixed | (1,2,4,3) | 219 (1243 % 256) + * + * Plus a byte-id gate: cstage.s == wwstage.s on the all_arms program + * (rule-10 — the cs==ww gate over the cross-module match the doc flagged). + */ +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return -1; +} + +/* classify() is shared boilerplate: a cross-module match whose four arms + * each return a distinct code. Each row supplies only the main body. */ +#define PRELUDE \ + "package main;\n" \ + "import encoding.utf8;\n" \ + "fn classify(d: *utf8.decoder) int = {\n" \ + " match (utf8.next(d)) {\n" \ + " case let r: rune => return 1;\n" \ + " case let dn: utf8.done => return 2;\n" \ + " case let m: utf8.more => return 3;\n" \ + " case let e: utf8.invalid => return 4;\n" \ + " };\n" \ + " return 0;\n" \ + "};\n" + +struct row { + const char *label; + const char *body; /* main() body, appended to PRELUDE */ + int want_exit; +}; + +static const struct row rows[] = { + { "rune", + "export fn main() int = {\n" + " let s: []u8 = [65u8];\n" + " let d = utf8.decode(s);\n" + " return classify(&d);\n" + "};\n", 1 }, + { "done", + "export fn main() int = {\n" + " let s: []u8 = [65u8];\n" + " let d = utf8.decode(s);\n" + " classify(&d);\n" + " return classify(&d);\n" + "};\n", 2 }, + { "more", + "export fn main() int = {\n" + " let s: []u8 = [195u8];\n" + " let d = utf8.decode(s);\n" + " return classify(&d);\n" + "};\n", 3 }, + { "invalid", + "export fn main() int = {\n" + " let s: []u8 = [255u8];\n" + " let d = utf8.decode(s);\n" + " return classify(&d);\n" + "};\n", 4 }, + { "all_arms", + "export fn main() int = {\n" + " let g: []u8 = [65u8];\n" + " let dg = utf8.decode(g);\n" + " let a = classify(&dg);\n" + " let b = classify(&dg);\n" + " let bad: []u8 = [255u8];\n" + " let db = utf8.decode(bad);\n" + " let c = classify(&db);\n" + " let tr: []u8 = [195u8];\n" + " let dt = utf8.decode(tr);\n" + " let e = classify(&dt);\n" + " return a * 1000 + b * 100 + c * 10 + e;\n" + "};\n", 219 }, +}; + +/* write_src — PRELUDE + row body into . */ +static int +write_src(const char *path, const struct row *r) +{ + FILE *f = fopen(path, "wb"); + if (!f) return -1; + fputs(PRELUDE, f); + fputs(r->body, f); + fclose(f); + return 0; +} + +/* build_run — build `src` to `` binary via `driver`, run it; return + * the binary's exit code, or -1 on a build failure. */ +static int +build_run(const char *driver, const char *src, const char *stem) +{ + char cmd[1024]; + snprintf(cmd, sizeof cmd, "%s build -o '%s' '%s' 2>/dev/null", + driver, stem, src); + if (runwait(cmd) != 0) return -1; + return runwait(stem); +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + char cdrv[1024], wdrv[1024]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + struct { const char *name; const char *drv; int gated; } + drivers[] = { + { "cstage", cdrv, 0 }, + { "wwstage", wdrv, 1 }, + { NULL, NULL, 0 }, + }; + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + int pid = (int)getpid(); + + char src[80], stem[80]; + + for (int d = 0; drivers[d].name; d++) { + if (drivers[d].gated && access(drivers[d].drv, X_OK) != 0) { + fprintf(stderr, "m1union_run: skip %s (no %s)\n", + drivers[d].name, drivers[d].drv); + continue; + } + for (int i = 0; i < n; i++) { + total++; + snprintf(src, sizeof src, "/tmp/m1union_%d_%d_%d.ww", + pid, d, i); + snprintf(stem, sizeof stem, "/tmp/m1union_%d_%d_%d", + pid, d, i); + if (write_src(src, &rows[i]) != 0) { fail++; continue; } + int got = build_run(drivers[d].drv, src, stem); + if (got != rows[i].want_exit) { + fprintf(stderr, "m1union_run[%s][%s]: exit=%d " + "want=%d\n", drivers[d].name, rows[i].label, + got, rows[i].want_exit); + fail++; + } + char cmd[256]; + snprintf(cmd, sizeof cmd, "rm -f '%s' '%s' '%s.s' " + "'%s.o' '%s.combined.ww'", src, stem, stem, stem, + stem); + runwait(cmd); + } + } + + /* rule-10 byte-id gate on the all_arms cross-module match. */ + if (access(wdrv, X_OK) == 0) { + total++; + const struct row *r = &rows[n - 1]; /* all_arms */ + char csrc[80], cstem[80], cs[96], ws[96], cmd[512]; + snprintf(csrc, sizeof csrc, "/tmp/m1union_bid_%d.ww", pid); + snprintf(cstem, sizeof cstem, "/tmp/m1union_bid_c_%d", pid); + snprintf(cs, sizeof cs, "%s.s", cstem); + char wstem[80]; + snprintf(wstem, sizeof wstem, "/tmp/m1union_bid_w_%d", pid); + snprintf(ws, sizeof ws, "%s.s", wstem); + if (write_src(csrc, r) == 0) { + snprintf(cmd, sizeof cmd, "%s build -o '%s' '%s' " + "2>/dev/null", cdrv, cstem, csrc); + int cb = runwait(cmd); + snprintf(cmd, sizeof cmd, "%s build -o '%s' '%s' " + "2>/dev/null", wdrv, wstem, csrc); + int wb = runwait(cmd); + if (cb != 0 || wb != 0) { + fprintf(stderr, "m1union_run: byte-id build " + "failed (cstage=%d wwstage=%d)\n", cb, wb); + fail++; + } else { + snprintf(cmd, sizeof cmd, "cmp -s '%s' '%s'", + cs, ws); + if (runwait(cmd) != 0) { + fprintf(stderr, "m1union_run: cstage.s " + "!= wwstage.s (byte-id break)\n"); + fail++; + } + } + } + snprintf(cmd, sizeof cmd, "rm -f '%s' '%s'* '%s'*", csrc, + cstem, wstem); + runwait(cmd); + } + + if (fail) { + fprintf(stderr, "m1union_run: %d/%d failed\n", fail, total); + return 1; + } + printf("m1union_run: %d/%d ok\n", total, total); + return 0; +}