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).
This commit is contained in:
2026-06-15 17:37:18 +09:00
parent 64d6c15e41
commit f308818b4b
30 changed files with 1851 additions and 241 deletions

View File

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

View File

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

View File

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

View File

@@ -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, "<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 = {

View File

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

View File

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