From 747475174accb4ceeeb17d5df545c98894eccdb7 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 16 Jun 2026 16:36:03 +0900 Subject: [PATCH] wcc/ww: tag sep-built dotted-path packages by full path not leaf (#57) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A separately-compiled package's primary body was emitted under a bare `//ww:module-reset`, so its own `package ;` clause set curmod to the leaf (e.g. utf8) while the importer spliced the .wwi under the full `//ww:module encoding.utf8` — definer mangled `utf8.X`, importer wanted `encoding.utf8.X`, unresolved. Thread the dotted path through the directive: `//ww:module-reset ` sets curmod to the dotted path (imported stays 0, so the root `fn main` stays bare per #32), and the body's package clause is demoted to a leaf==last-component assertion instead of overwriting curmod. Aligns sep-build to the M1 path-mangle model; only the SEP emitter changes (the combined build_one arm is untouched, so all combined byte-id gates hold). Both stages mirrored. Commit-6 broad-soak prerequisite. Gate 989_sepdotpath_run sep-builds a 2-level dotted package and proves definer==importer qualification + single-component non-vacuity, cs==ww. --- Makefile | 12 ++ cmd/wcc/lex.c | 37 +++- cmd/wcc/parse.c | 32 +++- cmd/wcc/ww.h | 10 + cmd/ww/main.c | 22 ++- lib/ww/lex/lex.ww | 48 ++++- lib/ww/parse/parse.ww | 40 +++- selfhost/cmd/w6c/main.combined.ww | 88 ++++++++- selfhost/cmd/ww/main.combined.ww | 26 ++- selfhost/cmd/ww/main.ww | 26 ++- selfhost/cmd/wwdump/main.combined.ww | 88 ++++++++- test/wcc/989_sepdotpath_run.c | 268 +++++++++++++++++++++++++++ 12 files changed, 639 insertions(+), 58 deletions(-) create mode 100644 test/wcc/989_sepdotpath_run.c diff --git a/Makefile b/Makefile index 0dff4b20..6d1ab2e8 100644 --- a/Makefile +++ b/Makefile @@ -564,6 +564,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_m2wwi_run \ $(BIN)/test_m3sep_run \ $(BIN)/test_sepbuild_run \ + $(BIN)/test_sepdotpath_run \ $(BIN)/test_sepcycle_dup \ $(BIN)/test_separchive_run \ $(BIN)/test_pkgcache_run \ @@ -3110,6 +3111,17 @@ $(BIN)/test_sepbuild_run: test/wcc/989_sepbuild_run.c $(BIN)/ww $(BIN)/ww_ww \ $(BIN)/w6l $(BIN)/w6l_ww $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# 989_sepdotpath_run — sep-build of a DOTTED-path (multi-component) package +# (#57): the producer must tag a primary body by its full dotted import path +# (`//ww:module-reset a.b`), not the leaf `package` clause, so definer == +# importer. Drives BOTH driver stages (synthetic a.b + c graph) and re-greps +# the producer .s, so it needs both driver + both compiler + both linker +# stages + libwwrt for the link. +$(BIN)/test_sepdotpath_run: test/wcc/989_sepdotpath_run.c $(BIN)/ww $(BIN)/ww_ww \ + $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6a_ww \ + $(BIN)/w6l $(BIN)/w6l_ww $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + # 989_sepcycle_dup — M3-tail commit-4 negative gates (#46, #31): a loud # dep-cycle reject (both driver stages, cs==ww stderr) + the #31 # duplicate-symbol reject (both linker stages). Needs both driver stages, diff --git a/cmd/wcc/lex.c b/cmd/wcc/lex.c index 7fe2d69a..9da9a0da 100644 --- a/cmd/wcc/lex.c +++ b/cmd/wcc/lex.c @@ -115,8 +115,33 @@ skipws(Lex *l) j++; if (rest[j] == '\0') { int af = lpeek(l, i + j); - if (af == '\n' || af < 0) + if (af == '\n' || af < 0) { l->modreset = 1; + } else if (af == ' ' + || af == '\t') { + /* `//ww:module-reset ` + * — sep primary body tagged by + * its full dotted import path so + * definer == importer (#57). */ + size_t k = i + j; + while (lpeek(l, k) == ' ' + || lpeek(l, k) == '\t') + k++; + size_t s = k; + int dch; + while ((dch = lpeek(l, k)) >= 0 + && dch != '\n' + && dch != '\r' + && dch != ' ' + && dch != '\t') + k++; + l->modreset = 1; + if (k > s) + l->modresetpath = + astrndup(l->a, + l->src + l->pos + s, + k - s); + } } } else if (nx == ' ' || nx == '\t') { /* `//ww:module ` — M1 import boundary. */ @@ -426,7 +451,15 @@ lexnext(Lex *l) Pos start = lpos(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->modreset) { + l->modreset = 0; + const char *rp = l->modresetpath; + l->modresetpath = NULL; + /* path-carrying reset → text=path (#57); bare reset → text=NULL */ + Tok _t = (Tok){ TK_MODRESET, start, rp, rp ? strlen(rp) : 0, + {0}, TK_NONE }; + return _t; + } if (l->modpath) { const char *mp = l->modpath; l->modpath = NULL; diff --git a/cmd/wcc/parse.c b/cmd/wcc/parse.c index 69dc8945..5be3b361 100644 --- a/cmd/wcc/parse.c +++ b/cmd/wcc/parse.c @@ -1373,17 +1373,22 @@ parsefile(Parser *p) advance(p); const char *name = expectident(p); expect(p, TK_SEMI); - if (p->pathmod != NULL) { + if (p->pathmod != NULL || p->resetmod != 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; + * does NOT overwrite the path-derived module. + * #57 extends this to the sep primary-reset path + * (resetmod): the dotted reset path is the + * authoritative identity, the clause asserts. */ + const char *active = + p->pathmod ? p->pathmod : p->resetmod; + const char *dot = strrchr(active, '.'); + const char *last = dot ? dot + 1 : active; if (strcmp(name, last) != 0) { errorf(p->cur.pos, "package %s does not match import path %s", - name, p->pathmod); + name, active); p->errs++; } } else { @@ -1398,6 +1403,7 @@ parsefile(Parser *p) if (p->cur.kind == TK_MODPATH) { p->pathmod = p->cur.text; p->curmod = p->cur.text; + p->resetmod = NULL; advance(p); continue; } @@ -1410,9 +1416,23 @@ parsefile(Parser *p) * directive after a mid-file `package` would strip subsequent * decls to bare — that usage is deliberate-only. */ if (p->cur.kind == TK_MODRESET) { + /* #57: a path-carrying reset (sep primary body) mangles + * decls on the dotted path so definer == importer, but + * leaves imported==0 (curmod set, pathmod NULL) so -c + * primary-ness and the #32 bare-main rule are intact; + * the body's `package` clause then asserts (resetmod). + * A bare reset is the root/package-less boundary: curmod + * NULL → bare symbols, today's behavior. */ + const char *rp = p->cur.text; advance(p); - p->curmod = NULL; p->pathmod = NULL; + if (rp != NULL) { + p->curmod = rp; + p->resetmod = rp; + } else { + p->curmod = NULL; + p->resetmod = NULL; + } continue; } Node *attrs = parseattrs(p); diff --git a/cmd/wcc/ww.h b/cmd/wcc/ww.h index 8ebeffa1..88a055c3 100644 --- a/cmd/wcc/ww.h +++ b/cmd/wcc/ww.h @@ -221,6 +221,10 @@ struct Lex { const char *modpath; /* a `//ww:module ` directive was seen in * the last skipped run; lexnext emits TK_MODPATH * carrying this dotted path (M1 #22). */ + const char *modresetpath; /* a `//ww:module-reset ` directive + * was seen; the next TK_MODRESET carries this + * dotted path so the sep primary body mangles + * on the path, not its leaf clause (#57). */ }; void lexinit(Lex*, Arena*, const char *file, const char *src, u64 len); @@ -379,6 +383,12 @@ struct Parser { * import path; while set, decls stamp * module=pathmod and imported=1, and the * in-file `package` clause is an assertion. */ + const char *resetmod; /* #57: active `//ww:module-reset ` dotted + * path; mangles decls on the path WITHOUT + * imported=1 (primary-ness for -c and the #32 + * bare-main rule stay intact), and the in-file + * `package` clause asserts (leaf == last + * component) instead of overwriting curmod. */ }; void parserinit(Parser*, Arena*, Lex*); diff --git a/cmd/ww/main.c b/cmd/ww/main.c index fa2fb7e2..48c40fe7 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -787,7 +787,7 @@ sep_mark_deps(struct sepgraph *g, int pi, char *inset) * body); FILE imports fold in (intra-package split). */ static void sep_emit_body(FILE *out, const char *path, struct ImportSet *visited, - const char *searchpath) + const char *searchpath, const char *modpath) { if (import_seen(visited, path)) return; import_add(visited, path); @@ -818,9 +818,15 @@ sep_emit_body(FILE *out, const char *path, struct ImportSet *visited, &is_dir)) continue; if (!is_dir) - sep_emit_body(out, ipath, visited, searchpath); + sep_emit_body(out, ipath, visited, searchpath, modpath); } - fputs("//ww:module-reset\n", out); + /* #57: tag the primary body by its full dotted import path so the + * definer mangles == the importer reference; a root build (path "") + * stays a bare reset (keeps bare main). */ + if (modpath != NULL && modpath[0] != '\0') + fprintf(out, "//ww:module-reset %s\n", modpath); + else + fputs("//ww:module-reset\n", out); rewind(in); int ch; while ((ch = fgetc(in)) != EOF) fputc(ch, out); @@ -830,14 +836,14 @@ sep_emit_body(FILE *out, const char *path, struct ImportSet *visited, static void sep_emit_dir_body(FILE *out, const char *dir, struct ImportSet *visited, - const char *searchpath) + const char *searchpath, const char *modpath) { char **files = NULL; int n = enumerate_dir_ww(dir, &files); for (int i = 0; i < n; i++) { char fp[1024]; snprintf(fp, sizeof fp, "%s/%s", dir, files[i]); - sep_emit_body(out, fp, visited, searchpath); + sep_emit_body(out, fp, visited, searchpath, modpath); free(files[i]); } free(files); @@ -876,9 +882,11 @@ sep_compose_unit(struct sepgraph *g, int pi, const char *scratch, } struct ImportSet bodyvisit = {0}; if (g->pkg[pi].is_dir) - sep_emit_dir_body(u, g->pkg[pi].entry, &bodyvisit, searchpath); + sep_emit_dir_body(u, g->pkg[pi].entry, &bodyvisit, searchpath, + g->pkg[pi].path); else - sep_emit_body(u, g->pkg[pi].entry, &bodyvisit, searchpath); + sep_emit_body(u, g->pkg[pi].entry, &bodyvisit, searchpath, + g->pkg[pi].path); for (int i = 0; i < bodyvisit.n; i++) free(bodyvisit.paths[i]); free(bodyvisit.paths); fclose(u); diff --git a/lib/ww/lex/lex.ww b/lib/ww/lex/lex.ww index fd4b0f5d..049efc78 100644 --- a/lib/ww/lex/lex.ww +++ b/lib/ww/lex/lex.ww @@ -63,6 +63,11 @@ type lex = struct { // lexnext emits TK_MODPATH carrying this dotted path (M1 #22). modpathset: i32, modpath: str, + // a `//ww:module-reset ` directive was seen; the next TK_MODRESET + // carries this dotted path so the sep primary body mangles on the path, + // not its leaf clause (#57). + modresetpathset: i32, + modresetpath: str, }; export fn lexinit(l: *lex, file: str, src: *u8, len: u64) void = { @@ -75,6 +80,7 @@ export fn lexinit(l: *lex, file: str, src: *u8, len: u64) void = { l.errs = 0; l.modreset = 0; l.modpathset = 0; + l.modresetpathset = 0; }; // srcb — byte at offset; helper that lifts the cast out of indexing. @@ -168,7 +174,42 @@ fn skipws(l: *lex) bool = { 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 (af < 0) { l.modreset = 1; } + else { if (af == ' ' || af == '\t') { + // `//ww:module-reset ` — sep + // primary body tagged by its full + // dotted import path (#57). + let k: u64 = + (pre.len + rest.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; + }; + l.modreset = 1; + if (k > s0) { + let view: str; + view.ptr = + l.src + l.lpos + s0; + view.len = (k - s0): i32; + l.modresetpath = + strings.dup(view); + l.modresetpathset = 1; + }; + }; }; }; }; } else { if (nx == ' ' || nx == '\t') { // `//ww:module ` — M1 import boundary. @@ -709,6 +750,11 @@ export fn lexnext(l: *lex, out: *tok) void = { if (l.modreset != 0) { l.modreset = 0; emitsimple(&start, tkind.TK_MODRESET, out); + // path-carrying reset → text=path (#57); bare reset → text empty + if (l.modresetpathset != 0) { + l.modresetpathset = 0; + out.text = l.modresetpath; + }; return; }; if (l.modpathset != 0) { diff --git a/lib/ww/parse/parse.ww b/lib/ww/parse/parse.ww index 2fe9b738..c0cd2570 100644 --- a/lib/ww/parse/parse.ww +++ b/lib/ww/parse/parse.ww @@ -48,6 +48,11 @@ type parser = struct { // decls stamp nmod=pathmod and imported=1, and the in-file `package` // clause is an assertion. "" means inactive (root/primary). pathmod: str, + // #57: active `//ww:module-reset ` dotted path. Mangles decls on + // the path WITHOUT imported=1 (primary-ness for -c and the #32 bare-main + // rule stay intact), and the in-file `package` clause asserts (leaf == + // last component) instead of overwriting curmod. "" means inactive. + resetmod: str, }; fn refill(p: *parser) void = { @@ -68,6 +73,7 @@ export fn parserinit(p: *parser, l: *lex) void = { p.errs = 0; p.nocast = 0; p.pathmod = ""; + p.resetmod = ""; refill(p); }; @@ -414,14 +420,18 @@ export fn parsefile(p: *parser) *node = { let name: str; expectident(p, &name); expecttok(p, tkind.TK_SEMI, "expected ';' after module name"); - if (p.pathmod.len != 0) { + if (p.pathmod.len != 0 || p.resetmod.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, "."); + // overwrite the path-derived module. #57 extends this + // to the sep primary-reset path (resetmod): the dotted + // reset path is authoritative, the clause asserts. + let active: str = p.pathmod; + if (p.pathmod.len == 0) { active = p.resetmod; }; + let (pre, post) = strings.rcut(active, "."); let last: str = post; - if (post.len == 0) { last = p.pathmod; }; + if (post.len == 0) { last = active; }; if (strings.compare(name, last) != 0) { errmsg(p, "package does not match import path"); }; @@ -437,6 +447,7 @@ export fn parsefile(p: *parser) *node = { if (p.curkind == tkind.TK_MODPATH) { p.pathmod = p.curtext; p.curmod = p.curtext; + p.resetmod = ""; advance(p); continue; }; @@ -449,12 +460,25 @@ export fn parsefile(p: *parser) *node = { // directive after a mid-file `package` would strip subsequent // decls to bare — that usage is deliberate-only. if (p.curkind == tkind.TK_MODRESET) { + // #57: a path-carrying reset (sep primary body) mangles decls + // on the dotted path so definer == importer, but leaves + // imported==0 (curmod set, pathmod "") so -c primary-ness and + // the #32 bare-main rule are intact; the body's `package` + // clause then asserts (resetmod). A bare reset is the + // root/package-less boundary: curmod "" → bare, today's path. + let rp: str = p.curtext; advance(p); - let empty: str; - empty.ptr = nil; - empty.len = 0; - p.curmod = empty; p.pathmod = ""; + if (rp.len != 0) { + p.curmod = rp; + p.resetmod = rp; + } else { + let empty: str; + empty.ptr = nil; + empty.len = 0; + p.curmod = empty; + p.resetmod = ""; + }; continue; }; let attrs = parseattrs(p); diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 4f8e1cc1..d71e16a4 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -6888,6 +6888,11 @@ type lex = struct { // lexnext emits TK_MODPATH carrying this dotted path (M1 #22). modpathset: i32, modpath: str, + // a `//ww:module-reset ` directive was seen; the next TK_MODRESET + // carries this dotted path so the sep primary body mangles on the path, + // not its leaf clause (#57). + modresetpathset: i32, + modresetpath: str, }; export fn lexinit(l: *lex, file: str, src: *u8, len: u64) void = { @@ -6900,6 +6905,7 @@ export fn lexinit(l: *lex, file: str, src: *u8, len: u64) void = { l.errs = 0; l.modreset = 0; l.modpathset = 0; + l.modresetpathset = 0; }; // srcb — byte at offset; helper that lifts the cast out of indexing. @@ -6993,7 +6999,42 @@ fn skipws(l: *lex) bool = { 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 (af < 0) { l.modreset = 1; } + else { if (af == ' ' || af == '\t') { + // `//ww:module-reset ` — sep + // primary body tagged by its full + // dotted import path (#57). + let k: u64 = + (pre.len + rest.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; + }; + l.modreset = 1; + if (k > s0) { + let view: str; + view.ptr = + l.src + l.lpos + s0; + view.len = (k - s0): i32; + l.modresetpath = + strings.dup(view); + l.modresetpathset = 1; + }; + }; }; }; }; } else { if (nx == ' ' || nx == '\t') { // `//ww:module ` — M1 import boundary. @@ -7534,6 +7575,11 @@ export fn lexnext(l: *lex, out: *tok) void = { if (l.modreset != 0) { l.modreset = 0; emitsimple(&start, tkind.TK_MODRESET, out); + // path-carrying reset → text=path (#57); bare reset → text empty + if (l.modresetpathset != 0) { + l.modresetpathset = 0; + out.text = l.modresetpath; + }; return; }; if (l.modpathset != 0) { @@ -8761,6 +8807,11 @@ type parser = struct { // decls stamp nmod=pathmod and imported=1, and the in-file `package` // clause is an assertion. "" means inactive (root/primary). pathmod: str, + // #57: active `//ww:module-reset ` dotted path. Mangles decls on + // the path WITHOUT imported=1 (primary-ness for -c and the #32 bare-main + // rule stay intact), and the in-file `package` clause asserts (leaf == + // last component) instead of overwriting curmod. "" means inactive. + resetmod: str, }; fn refill(p: *parser) void = { @@ -8781,6 +8832,7 @@ export fn parserinit(p: *parser, l: *lex) void = { p.errs = 0; p.nocast = 0; p.pathmod = ""; + p.resetmod = ""; refill(p); }; @@ -9127,14 +9179,18 @@ export fn parsefile(p: *parser) *node = { let name: str; expectident(p, &name); expecttok(p, tkind.TK_SEMI, "expected ';' after module name"); - if (p.pathmod.len != 0) { + if (p.pathmod.len != 0 || p.resetmod.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, "."); + // overwrite the path-derived module. #57 extends this + // to the sep primary-reset path (resetmod): the dotted + // reset path is authoritative, the clause asserts. + let active: str = p.pathmod; + if (p.pathmod.len == 0) { active = p.resetmod; }; + let (pre, post) = strings.rcut(active, "."); let last: str = post; - if (post.len == 0) { last = p.pathmod; }; + if (post.len == 0) { last = active; }; if (strings.compare(name, last) != 0) { errmsg(p, "package does not match import path"); }; @@ -9150,6 +9206,7 @@ export fn parsefile(p: *parser) *node = { if (p.curkind == tkind.TK_MODPATH) { p.pathmod = p.curtext; p.curmod = p.curtext; + p.resetmod = ""; advance(p); continue; }; @@ -9162,12 +9219,25 @@ export fn parsefile(p: *parser) *node = { // directive after a mid-file `package` would strip subsequent // decls to bare — that usage is deliberate-only. if (p.curkind == tkind.TK_MODRESET) { + // #57: a path-carrying reset (sep primary body) mangles decls + // on the dotted path so definer == importer, but leaves + // imported==0 (curmod set, pathmod "") so -c primary-ness and + // the #32 bare-main rule are intact; the body's `package` + // clause then asserts (resetmod). A bare reset is the + // root/package-less boundary: curmod "" → bare, today's path. + let rp: str = p.curtext; advance(p); - let empty: str; - empty.ptr = nil; - empty.len = 0; - p.curmod = empty; p.pathmod = ""; + if (rp.len != 0) { + p.curmod = rp; + p.resetmod = rp; + } else { + let empty: str; + empty.ptr = nil; + empty.len = 0; + p.curmod = empty; + p.resetmod = ""; + }; continue; }; let attrs = parseattrs(p); diff --git a/selfhost/cmd/ww/main.combined.ww b/selfhost/cmd/ww/main.combined.ww index 47582589..f371449c 100644 --- a/selfhost/cmd/ww/main.combined.ww +++ b/selfhost/cmd/ww/main.combined.ww @@ -3965,7 +3965,7 @@ fn sepmarkdeps(g: *sepgraph, pi: i32, inset: []u8) void = { // //ww:module-reset primary boundary (so -c emits its decls, imported // ==0). DIRECTORY imports are skipped (provided as `.wwi` ahead); // FILE imports fold in (intra-package split). -fn sepemitbody(fd: i32, path: *u8, visit: *expctx, searchpath: *u8) void = { +fn sepemitbody(fd: i32, path: *u8, visit: *expctx, searchpath: *u8, modpath: *u8) void = { let pview: str; pview.ptr = path; pview.len = cstrlen(path): i32; @@ -3994,19 +3994,29 @@ fn sepemitbody(fd: i32, path: *u8, visit: *expctx, searchpath: *u8) void = { let ipath: *u8 = locateimport(searchpath, idp, idn, &isdir); if (ipath != nil) { if (isdir == 0) { - sepemitbody(fd, ipath, visit, searchpath); + sepemitbody(fd, ipath, visit, searchpath, modpath); }; }; }; i = j + 1u64; }; - let d: str = "//ww:module-reset\n"; - os.writeall(fd, d.ptr, d.len: u64); + // #57: tag the primary body by its full dotted import path so the + // definer mangles == the importer reference; a root build (path "") + // stays a bare reset (keeps bare main). + if (modpath != nil && modpath[0u64] != 0u8) { + let dm: str = "//ww:module-reset "; + os.writeall(fd, dm.ptr, dm.len: u64); + os.writeall(fd, modpath, cstrlen(modpath)); + os.writeall(fd, "\n".ptr, 1u64); + } else { + let d: str = "//ww:module-reset\n"; + os.writeall(fd, d.ptr, d.len: u64); + }; os.writeall(fd, bufp, blen); os.writeall(fd, "\n".ptr, 1u64); }; -fn sepemitdirbody(fd: i32, dir: *u8, visit: *expctx, searchpath: *u8) void = { +fn sepemitdirbody(fd: i32, dir: *u8, visit: *expctx, searchpath: *u8, modpath: *u8) void = { let names: **u8; let n: i32; names, n = enumeratedir(dir); @@ -4021,7 +4031,7 @@ fn sepemitdirbody(fd: i32, dir: *u8, visit: *expctx, searchpath: *u8) void = { k = 0u64; for (k < nlen) { fp[dlen + 1u64 + k] = names[i][k]; k += 1u64; }; fp[dlen + 1u64 + nlen] = 0u8; - sepemitbody(fd, fp.ptr, visit, searchpath); + sepemitbody(fd, fp.ptr, visit, searchpath, modpath); i += 1; }; }; @@ -4070,9 +4080,9 @@ fn sepcomposeunit(g: *sepgraph, pi: i32, scratch: *u8, order: []i32, bv.dirs = searchpath; bv.visit = nil; if (g.pkg[pi].isdir != 0) { - sepemitdirbody(u, g.pkg[pi].entry, &bv, searchpath); + sepemitdirbody(u, g.pkg[pi].entry, &bv, searchpath, g.pkg[pi].path); } else { - sepemitbody(u, g.pkg[pi].entry, &bv, searchpath); + sepemitbody(u, g.pkg[pi].entry, &bv, searchpath, g.pkg[pi].path); }; os.close(u); return 0; diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index 236ada1f..541cccc0 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -1091,7 +1091,7 @@ fn sepmarkdeps(g: *sepgraph, pi: i32, inset: []u8) void = { // //ww:module-reset primary boundary (so -c emits its decls, imported // ==0). DIRECTORY imports are skipped (provided as `.wwi` ahead); // FILE imports fold in (intra-package split). -fn sepemitbody(fd: i32, path: *u8, visit: *expctx, searchpath: *u8) void = { +fn sepemitbody(fd: i32, path: *u8, visit: *expctx, searchpath: *u8, modpath: *u8) void = { let pview: str; pview.ptr = path; pview.len = cstrlen(path): i32; @@ -1120,19 +1120,29 @@ fn sepemitbody(fd: i32, path: *u8, visit: *expctx, searchpath: *u8) void = { let ipath: *u8 = locateimport(searchpath, idp, idn, &isdir); if (ipath != nil) { if (isdir == 0) { - sepemitbody(fd, ipath, visit, searchpath); + sepemitbody(fd, ipath, visit, searchpath, modpath); }; }; }; i = j + 1u64; }; - let d: str = "//ww:module-reset\n"; - os.writeall(fd, d.ptr, d.len: u64); + // #57: tag the primary body by its full dotted import path so the + // definer mangles == the importer reference; a root build (path "") + // stays a bare reset (keeps bare main). + if (modpath != nil && modpath[0u64] != 0u8) { + let dm: str = "//ww:module-reset "; + os.writeall(fd, dm.ptr, dm.len: u64); + os.writeall(fd, modpath, cstrlen(modpath)); + os.writeall(fd, "\n".ptr, 1u64); + } else { + let d: str = "//ww:module-reset\n"; + os.writeall(fd, d.ptr, d.len: u64); + }; os.writeall(fd, bufp, blen); os.writeall(fd, "\n".ptr, 1u64); }; -fn sepemitdirbody(fd: i32, dir: *u8, visit: *expctx, searchpath: *u8) void = { +fn sepemitdirbody(fd: i32, dir: *u8, visit: *expctx, searchpath: *u8, modpath: *u8) void = { let names: **u8; let n: i32; names, n = enumeratedir(dir); @@ -1147,7 +1157,7 @@ fn sepemitdirbody(fd: i32, dir: *u8, visit: *expctx, searchpath: *u8) void = { k = 0u64; for (k < nlen) { fp[dlen + 1u64 + k] = names[i][k]; k += 1u64; }; fp[dlen + 1u64 + nlen] = 0u8; - sepemitbody(fd, fp.ptr, visit, searchpath); + sepemitbody(fd, fp.ptr, visit, searchpath, modpath); i += 1; }; }; @@ -1196,9 +1206,9 @@ fn sepcomposeunit(g: *sepgraph, pi: i32, scratch: *u8, order: []i32, bv.dirs = searchpath; bv.visit = nil; if (g.pkg[pi].isdir != 0) { - sepemitdirbody(u, g.pkg[pi].entry, &bv, searchpath); + sepemitdirbody(u, g.pkg[pi].entry, &bv, searchpath, g.pkg[pi].path); } else { - sepemitbody(u, g.pkg[pi].entry, &bv, searchpath); + sepemitbody(u, g.pkg[pi].entry, &bv, searchpath, g.pkg[pi].path); }; os.close(u); return 0; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index d34a00e5..9b442e28 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -6888,6 +6888,11 @@ type lex = struct { // lexnext emits TK_MODPATH carrying this dotted path (M1 #22). modpathset: i32, modpath: str, + // a `//ww:module-reset ` directive was seen; the next TK_MODRESET + // carries this dotted path so the sep primary body mangles on the path, + // not its leaf clause (#57). + modresetpathset: i32, + modresetpath: str, }; export fn lexinit(l: *lex, file: str, src: *u8, len: u64) void = { @@ -6900,6 +6905,7 @@ export fn lexinit(l: *lex, file: str, src: *u8, len: u64) void = { l.errs = 0; l.modreset = 0; l.modpathset = 0; + l.modresetpathset = 0; }; // srcb — byte at offset; helper that lifts the cast out of indexing. @@ -6993,7 +6999,42 @@ fn skipws(l: *lex) bool = { 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 (af < 0) { l.modreset = 1; } + else { if (af == ' ' || af == '\t') { + // `//ww:module-reset ` — sep + // primary body tagged by its full + // dotted import path (#57). + let k: u64 = + (pre.len + rest.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; + }; + l.modreset = 1; + if (k > s0) { + let view: str; + view.ptr = + l.src + l.lpos + s0; + view.len = (k - s0): i32; + l.modresetpath = + strings.dup(view); + l.modresetpathset = 1; + }; + }; }; }; }; } else { if (nx == ' ' || nx == '\t') { // `//ww:module ` — M1 import boundary. @@ -7534,6 +7575,11 @@ export fn lexnext(l: *lex, out: *tok) void = { if (l.modreset != 0) { l.modreset = 0; emitsimple(&start, tkind.TK_MODRESET, out); + // path-carrying reset → text=path (#57); bare reset → text empty + if (l.modresetpathset != 0) { + l.modresetpathset = 0; + out.text = l.modresetpath; + }; return; }; if (l.modpathset != 0) { @@ -8761,6 +8807,11 @@ type parser = struct { // decls stamp nmod=pathmod and imported=1, and the in-file `package` // clause is an assertion. "" means inactive (root/primary). pathmod: str, + // #57: active `//ww:module-reset ` dotted path. Mangles decls on + // the path WITHOUT imported=1 (primary-ness for -c and the #32 bare-main + // rule stay intact), and the in-file `package` clause asserts (leaf == + // last component) instead of overwriting curmod. "" means inactive. + resetmod: str, }; fn refill(p: *parser) void = { @@ -8781,6 +8832,7 @@ export fn parserinit(p: *parser, l: *lex) void = { p.errs = 0; p.nocast = 0; p.pathmod = ""; + p.resetmod = ""; refill(p); }; @@ -9127,14 +9179,18 @@ export fn parsefile(p: *parser) *node = { let name: str; expectident(p, &name); expecttok(p, tkind.TK_SEMI, "expected ';' after module name"); - if (p.pathmod.len != 0) { + if (p.pathmod.len != 0 || p.resetmod.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, "."); + // overwrite the path-derived module. #57 extends this + // to the sep primary-reset path (resetmod): the dotted + // reset path is authoritative, the clause asserts. + let active: str = p.pathmod; + if (p.pathmod.len == 0) { active = p.resetmod; }; + let (pre, post) = strings.rcut(active, "."); let last: str = post; - if (post.len == 0) { last = p.pathmod; }; + if (post.len == 0) { last = active; }; if (strings.compare(name, last) != 0) { errmsg(p, "package does not match import path"); }; @@ -9150,6 +9206,7 @@ export fn parsefile(p: *parser) *node = { if (p.curkind == tkind.TK_MODPATH) { p.pathmod = p.curtext; p.curmod = p.curtext; + p.resetmod = ""; advance(p); continue; }; @@ -9162,12 +9219,25 @@ export fn parsefile(p: *parser) *node = { // directive after a mid-file `package` would strip subsequent // decls to bare — that usage is deliberate-only. if (p.curkind == tkind.TK_MODRESET) { + // #57: a path-carrying reset (sep primary body) mangles decls + // on the dotted path so definer == importer, but leaves + // imported==0 (curmod set, pathmod "") so -c primary-ness and + // the #32 bare-main rule are intact; the body's `package` + // clause then asserts (resetmod). A bare reset is the + // root/package-less boundary: curmod "" → bare, today's path. + let rp: str = p.curtext; advance(p); - let empty: str; - empty.ptr = nil; - empty.len = 0; - p.curmod = empty; p.pathmod = ""; + if (rp.len != 0) { + p.curmod = rp; + p.resetmod = rp; + } else { + let empty: str; + empty.ptr = nil; + empty.len = 0; + p.curmod = empty; + p.resetmod = ""; + }; continue; }; let attrs = parseattrs(p); diff --git a/test/wcc/989_sepdotpath_run.c b/test/wcc/989_sepdotpath_run.c new file mode 100644 index 00000000..114e7f58 --- /dev/null +++ b/test/wcc/989_sepdotpath_run.c @@ -0,0 +1,268 @@ +/* + * 989_sepdotpath_run — sep-build of a DOTTED-path (multi-component) package + * (#57). Commit-6 broad-soak prereq: the real toolchain sep-builds packages + * like `encoding.utf8`, whose body must mangle on the full dotted import path + * (`encoding.utf8.X`), not the leaf `package` clause (`utf8.X`). + * + * Root cause it guards: the sep producer splices a package's own body under + * `//ww:module-reset` (so `-c` keeps imported==0). Pre-#57 that reset carried + * NO path, so the body's `package ;` clause set curmod to the LEAF — + * the DEFINER mangled `b.val` while every importer (spliced under + * `//ww:module a.b`) referenced `a.b.val` → unresolved at link. #57 threads + * the dotted path through the reset (`//ww:module-reset a.b`), demoting the + * leaf clause to an assertion. Single-COMPONENT packages were always clean + * (leaf == dotted path) — that invariant is the regression guard below. + * + * Graph (smallest that mixes both kinds): root -> { a.b (dotted), c (single) }. + * + * Asserts (all COLD — `.sepwork` scratch is wiped each run): + * 1. Build + run, BOTH stages → exit EXPECT_EXIT (the dotted-package symbol + * resolves + links; the program runs). Pre-#57 the link failed. + * 2. cs==ww (rule 10): per-package `.s`/`.wwi`/`.unit.ww` AND the final + * binary are byte-identical between `ww --sep` and `ww_ww --sep`. + * 3. NON-VACUITY (a) — definer==importer on the DOTTED form: the producer's + * `a.b.s` DEFINES `a.b.val` and the root's `__root.s` REFERENCES + * `a.b.val`. Pre-#57 the definer emitted the leaf `b.val` → the strings + * mismatched; this row would fail (and assert 1's link would fail). + * The reset directive in `a.b.unit.ww` carries the dotted path. + * 4. NON-VACUITY (b) — single-component regression guard: package `c` + * sep-builds in the SAME graph, defines `c.cval` (leaf == dotted), and + * cs==ww (covered by assert 2). Cross-commit byte-id of single-component + * output is additionally pinned by 989_sepbuild_run + the 990-997 gates. + * + * Light wwstage-driver test (CLAUDE.md rule 14): all intermediates are + * `-o`-redirected to /tmp, so it is phase-1 parallel-safe. Models + * 989_sepbuild_run.c conventions; 989 prefix per the sep-gate precedent. + */ +#include +#include +#include +#include +#include +#include + +#define EXPECT_EXIT 37 + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return 1; +} + +static const char * +absbin(void) +{ + const char *b = getenv("BIN"); + if (!b) b = "out/bin"; + if (b[0] == '/') return b; + static char buf[2048]; + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return NULL; + snprintf(buf, sizeof buf, "%s/%s", cwd, b); + return buf; +} + +static int +slurp(const char *path, char **outbuf, size_t *outlen) +{ + FILE *f = fopen(path, "rb"); + if (!f) return -1; + fseek(f, 0, SEEK_END); + long n = ftell(f); + fseek(f, 0, SEEK_SET); + if (n < 0) { fclose(f); return -1; } + char *b = malloc((size_t)n + 1); + if (!b) { fclose(f); return -1; } + if (fread(b, 1, (size_t)n, f) != (size_t)n) { free(b); fclose(f); return -1; } + b[n] = '\0'; + fclose(f); + *outbuf = b; + *outlen = (size_t)n; + return 0; +} + +static int +files_eq(const char *a, const char *b) +{ + char *ba = NULL, *bb = NULL; + size_t na = 0, nb = 0; + if (slurp(a, &ba, &na) < 0 || slurp(b, &bb, &nb) < 0) { + free(ba); free(bb); + return -1; + } + int eq = (na == nb && memcmp(ba, bb, na) == 0); + free(ba); free(bb); + return eq ? 0 : 1; +} + +/* 0 if `needle` occurs in the file at `path`, 1 if absent, -1 on read err. */ +static int +file_contains(const char *path, const char *needle) +{ + char *b = NULL; + size_t n = 0; + if (slurp(path, &b, &n) < 0) return -1; + int found = (strstr(b, needle) != NULL); + free(b); + return found ? 0 : 1; +} + +static int +write_file(const char *path, const char *body) +{ + FILE *f = fopen(path, "wb"); + if (!f) return -1; + fputs(body, f); + fclose(f); + return 0; +} + +int +main(void) +{ + const char *bin = absbin(); + if (!bin) return 1; + char td[64], cmd[8192], p[1024]; + int fail = 0; + + snprintf(td, sizeof td, "/tmp/wwsepdot_%d", getpid()); + snprintf(cmd, sizeof cmd, "rm -rf %s", td); + runwait(cmd); + mkdir(td, 0755); + + /* lib/a/b — a 2-level dotted package (clause `package b;`, dir a/b). */ + snprintf(p, sizeof p, "%s/lib", td); mkdir(p, 0755); + snprintf(p, sizeof p, "%s/lib/a", td); mkdir(p, 0755); + snprintf(p, sizeof p, "%s/lib/a/b", td); mkdir(p, 0755); + snprintf(p, sizeof p, "%s/lib/c", td); mkdir(p, 0755); + + snprintf(p, sizeof p, "%s/lib/a/b/mod.ww", td); + if (write_file(p, "package b;\nexport fn val() i32 = { return 42; };\n")) + { fail++; goto out; } + snprintf(p, sizeof p, "%s/lib/c/mod.ww", td); + if (write_file(p, "package c;\nexport fn cval() i32 = { return 5; };\n")) + { fail++; goto out; } + + char rootww[1024]; + snprintf(rootww, sizeof rootww, "%s/root.ww", td); + if (write_file(rootww, + "package main;\n" + "import a.b;\n" + "import c;\n" + "fn main() i32 = { return b.val() - c.cval(); };\n")) + { fail++; goto out; } + + /* Two driver stages and their scratch dirs (paths rebuilt from the + * small fixed `td` + stage tag → provably non-truncating snprintfs). */ + struct { const char *drv, *tag; char prog[1024]; } + stg[] = { { "ww", "cs", {0} }, { "ww_ww", "ww", {0} } }; + + for (int s = 0; s < 2; s++) { + snprintf(stg[s].prog, sizeof stg[s].prog, "%s/prog.%s", td, stg[s].tag); + /* Per-stage fresh WW_PKGCACHE → every package compiles COLD, so the + * `.s`/`.unit.ww` this gate inspects are always produced (a warm + * shared out/.pkgcache hit would skip them). Mirrors 989_pkgcache. */ + snprintf(cmd, sizeof cmd, + "WW_PKGCACHE='%s/cache.%s' timeout 240 %s/%s build --sep " + "-I %s/lib -o %s %s >/dev/null 2>&1", + td, stg[s].tag, bin, stg[s].drv, td, stg[s].prog, rootww); + if (runwait(cmd) != 0) { + fprintf(stderr, "sepdotpath FAIL: %s build --sep\n", stg[s].drv); + fail++; + continue; + } + int rc = runwait(stg[s].prog); + if (rc != EXPECT_EXIT) { + fprintf(stderr, "sepdotpath FAIL: %s prog exit=%d expected %d\n", + stg[s].drv, rc, EXPECT_EXIT); + fail++; + } + } + + /* The discovered package set materialized (dotted + single + root). */ + const char *pkgs[] = { "a.b", "c", "__root" }; + for (int i = 0; i < 3; i++) { + snprintf(p, sizeof p, "%s/prog.cs.sepwork/%s.wwi", td, pkgs[i]); + if (access(p, 0) != 0) { + fprintf(stderr, "sepdotpath FAIL: missing %s.wwi (discovery)\n", + pkgs[i]); + fail++; + } + } + + /* cs==ww (rule 10): per-package .s/.wwi/.unit.ww + final binary. */ + for (int i = 0; i < 3; i++) { + const char *suf[] = { ".s", ".wwi", ".unit.ww" }; + for (int k = 0; k < 3; k++) { + char a[1024], b[1024]; + snprintf(a, sizeof a, "%s/prog.%s.sepwork/%s%s", + td, stg[0].tag, pkgs[i], suf[k]); + snprintf(b, sizeof b, "%s/prog.%s.sepwork/%s%s", + td, stg[1].tag, pkgs[i], suf[k]); + if (files_eq(a, b) != 0) { + fprintf(stderr, "sepdotpath FAIL: cs!=ww for %s%s (rule 10)\n", + pkgs[i], suf[k]); + fail++; + } + } + } + if (files_eq(stg[0].prog, stg[1].prog) != 0) { + fprintf(stderr, "sepdotpath FAIL: cs exe != ww exe (rule 10)\n"); + fail++; + } + + /* NON-VACUITY (a): definer == importer on the DOTTED qualification. + * Pre-#57 the definer emitted the leaf `b.val` while the importer + * referenced `a.b.val` → mismatch + unresolved link. */ + { + char def_s[1024], ref_s[1024], unit[1024]; + snprintf(def_s, sizeof def_s, "%s/prog.cs.sepwork/a.b.s", td); + snprintf(ref_s, sizeof ref_s, "%s/prog.cs.sepwork/__root.s", td); + snprintf(unit, sizeof unit, "%s/prog.cs.sepwork/a.b.unit.ww", td); + if (file_contains(def_s, "TEXT a.b.val") != 0) { + fprintf(stderr, "sepdotpath FAIL: definer a.b.s lacks dotted " + "`TEXT a.b.val` (leaf-clause regression)\n"); + fail++; + } + if (file_contains(ref_s, "a.b.val") != 0) { + fprintf(stderr, "sepdotpath FAIL: importer __root.s lacks " + "reference to a.b.val\n"); + fail++; + } + if (file_contains(unit, "//ww:module-reset a.b") != 0) { + fprintf(stderr, "sepdotpath FAIL: a.b.unit.ww reset directive " + "does not carry the dotted path\n"); + fail++; + } + } + + /* NON-VACUITY (b): single-component `c` sep-builds in the SAME graph and + * defines its leaf==dotted symbol `c.cval` (curmod resolves to the same + * string with or without #57 → byte-id preserved). cs==ww already + * asserted above; cross-commit byte-id pinned by 989_sepbuild + 990-997. */ + { + char c_s[1024]; + snprintf(c_s, sizeof c_s, "%s/prog.cs.sepwork/c.s", td); + if (file_contains(c_s, "TEXT c.cval") != 0) { + fprintf(stderr, "sepdotpath FAIL: single-component c.s lacks " + "`TEXT c.cval` (regression)\n"); + fail++; + } + } + +out: + snprintf(cmd, sizeof cmd, "rm -rf %s", td); + runwait(cmd); + if (fail) { + fprintf(stderr, "sepdotpath: %d check(s) failed\n", fail); + return 1; + } + printf("sepdotpath: dotted `a.b` + single `c` via build_one_sep — " + "build+run (exit %d) + cs==ww per-pkg .s/.wwi/.unit + final binary " + "+ definer==importer on a.b.val + single-component c.cval intact\n", + EXPECT_EXIT); + return 0; +}