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

@@ -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 <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 = {
@@ -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 <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.
@@ -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) {

View File

@@ -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 <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 = {
@@ -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);