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:
12
Makefile
12
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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
10
cmd/wcc/ww.h
10
cmd/wcc/ww.h
@@ -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*);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
268
test/wcc/989_sepdotpath_run.c
Normal file
268
test/wcc/989_sepdotpath_run.c
Normal file
@@ -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 <leaf>;` 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 — `<stem>.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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
Reference in New Issue
Block a user