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

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

View File

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

View File

@@ -221,6 +221,10 @@ struct Lex {
const char *modpath; /* a `//ww:module <path>` 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 <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). */
};
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 <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. */
};
void parserinit(Parser*, Arena*, Lex*);

View File

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