wcc/ww: tag sep-built dotted-path packages by full path not leaf (#57)

A separately-compiled package's primary body was emitted under a bare
`//ww:module-reset`, so its own `package <leaf>;` 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 <path>` 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.
This commit is contained in:
2026-06-16 16:36:03 +09:00
parent 7b36f961a9
commit 747475174a
12 changed files with 639 additions and 58 deletions

View File

@@ -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 <path>` 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 <path>` — 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 <path>` — 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 <path>` 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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 <path>` 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 <path>` — 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 <path>` — 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 <path>` 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);