wwi: derive decl-less module's .wwi package leaf from parse-stamped path (#11)
wwi_emit took the .wwi `package` leaf from the first primary decl's module tag;
a fully empty primary module body (zero decls) had none, so the leaf stayed the
literal default "main" and the importer rejected it ("package main does not
match import path <leaf>"). The module identity is only available at parse time
(curmod is overwritten by imported //ww:module sections before emit), so stamp
the primary path onto the N_FILE node (TK_MODULE and TK_MODRESET rp!=NULL sites,
only-if-empty so a bare-reset `package main` root stays "main") and, when the
decl-scan finds no leaf, fall back to that stamped path. Symmetric cstage+
selfhost; both detect scan-miss via the same found-flag so the emitted .wwi
stays byte-identical.
Regression: test/wcc/989_wwileaf_run.c, table-driven over {empty body,
comment-only, nested a.b.c} decl-less shapes, non-vacuity proven.
This commit is contained in:
10
Makefile
10
Makefile
@@ -502,6 +502,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
|
||||
$(BIN)/test_dyn_ww $(BIN)/test_selfcheck \
|
||||
$(BIN)/test_attest_record $(BIN)/test_attest_drop \
|
||||
$(BIN)/test_declns_sep $(BIN)/test_modresetadj_run \
|
||||
$(BIN)/test_wwileaf_run \
|
||||
$(BIN)/test_wwispread_sep \
|
||||
$(BIN)/test_fmt_run $(BIN)/test_log_run $(BIN)/test_fnmatch_run \
|
||||
$(BIN)/test_shlex_run $(BIN)/test_getenv_run $(BIN)/test_dirs_run \
|
||||
@@ -2668,6 +2669,15 @@ $(BIN)/test_modresetadj_run: test/wcc/989_modresetadj_run.c \
|
||||
$(LIB)/libwwrt.a | $(BIN)
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
# #11 (BUG-C) — a decl-less primary module's `.wwi` `package` line must carry
|
||||
# the real leaf, not the default "main", else its importer rejects the dep.
|
||||
# Table-driven over empty/comment-only/nested-path shapes; feeds the composed
|
||||
# producer + importer units straight to w6c/w6c_ww (.wwi leaf + byte-id +
|
||||
# import resolves cs==ww). Frontends only — no asm/link/run leg.
|
||||
$(BIN)/test_wwileaf_run: test/wcc/989_wwileaf_run.c \
|
||||
$(BIN)/w6c $(BIN)/w6c_ww | $(BIN)
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
# #95 — .wwi producer must preserve the `...` union-spread marker; dir-package
|
||||
# `ww build --sep` + run, both driver stages, 2-arm + 3-arm shapes + cs==ww.
|
||||
$(BIN)/test_wwispread_sep: test/wcc/989_wwispread_sep.c $(BIN)/ww $(BIN)/ww_ww \
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/*
|
||||
* wwi.c — `.wwi` export-data producer (w6c -I). M2 DEAD-CODE: writes a
|
||||
* re-parseable ww-prototype rendering of a package's EXPORTED surface.
|
||||
* Nothing consumes `.wwi` yet (combined.ww stays the live path); the only
|
||||
* caller is the new -I flag, off on every existing invocation.
|
||||
* wwi.c — `.wwi` export-data producer (w6c -I): a re-parseable ww-prototype
|
||||
* rendering of a package's EXPORTED surface. Since the sep-compile flip
|
||||
* (epic #22) this is the LIVE import path — the driver runs one `w6c -c -I`
|
||||
* per package and feeds each dep's `.wwi` to its importers as import scope;
|
||||
* the combined.ww amalgamator is gone.
|
||||
*
|
||||
* Specs: .ai/rob-M2-spec.md (producer) + .ai/drew-M2-checkexported.md
|
||||
* (check_exported_type). Two load-bearing choices follow them:
|
||||
@@ -492,13 +493,27 @@ wwi_emit(Checker *c, FILE *of, Node *file)
|
||||
|
||||
/* package line: leaf of the first primary decl's module tag. */
|
||||
const char *pkg = "main";
|
||||
int found = 0;
|
||||
for (Node *d = file->list; d; d = d->next) {
|
||||
if (wwi_primary(d) && d->module && d->module[0]) {
|
||||
const char *dot = strrchr(d->module, '.');
|
||||
pkg = dot ? dot + 1 : d->module;
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
/* #11: a decl-less / export-less primary body carries no
|
||||
* module-tagged decl, so the scan above finds nothing; fall back to
|
||||
* the primary module identity stamped on the N_FILE node at parse
|
||||
* time. A real root `package main` arrives via a bare module-reset
|
||||
* and leaves file->module NULL, so it stays "main". The detector is
|
||||
* scan-miss (`!found`), NOT pkg=="main": a body whose first tagged
|
||||
* decl legitimately leafs to "main" must keep that, and must match
|
||||
* selfhost's `found` flag byte-for-byte (rule 10). */
|
||||
if (!found && file->module && file->module[0]) {
|
||||
const char *dot = strrchr(file->module, '.');
|
||||
pkg = dot ? dot + 1 : file->module;
|
||||
}
|
||||
fprintf(of, "package %s;\n", pkg);
|
||||
|
||||
/* imports — primary N_USE, byte-sorted by import path. */
|
||||
|
||||
@@ -1442,6 +1442,13 @@ parsefile(Parser *p)
|
||||
}
|
||||
} else {
|
||||
p->curmod = name;
|
||||
/* #11: stamp the primary module identity on the
|
||||
* N_FILE node so wwi_emit can derive the
|
||||
* `package` leaf even when the body carries zero
|
||||
* module-tagged decls. Primary identity only;
|
||||
* never the imported boundary (TK_MODPATH). */
|
||||
if (file->module == NULL)
|
||||
file->module = name;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -1478,6 +1485,13 @@ parsefile(Parser *p)
|
||||
if (rp != NULL) {
|
||||
p->curmod = rp;
|
||||
p->resetmod = rp;
|
||||
/* #11: path-carrying reset is a primary body
|
||||
* identity (sep); stamp it for the wwi leaf
|
||||
* fallback. A bare reset (rp==NULL) is the
|
||||
* root/package-less boundary and MUST keep the
|
||||
* "main" default — so do NOT stamp there. */
|
||||
if (file->module == NULL)
|
||||
file->module = rp;
|
||||
} else {
|
||||
p->curmod = NULL;
|
||||
p->resetmod = NULL;
|
||||
|
||||
@@ -448,6 +448,12 @@ export fn parsefile(p: *parser) *node = {
|
||||
};
|
||||
} else {
|
||||
p.curmod = name;
|
||||
// #11: stamp the primary module identity on the
|
||||
// N_FILE node so wwi_emit can derive the `package`
|
||||
// leaf even when the body carries zero module-tagged
|
||||
// decls. Primary identity only; never the imported
|
||||
// boundary (TK_MODPATH).
|
||||
if (f.nmod.len == 0) { f.nmod = name; };
|
||||
};
|
||||
continue;
|
||||
};
|
||||
@@ -483,6 +489,11 @@ export fn parsefile(p: *parser) *node = {
|
||||
if (rp.len != 0) {
|
||||
p.curmod = rp;
|
||||
p.resetmod = rp;
|
||||
// #11: path-carrying reset is a primary body identity
|
||||
// (sep); stamp it for the wwi leaf fallback. A bare
|
||||
// reset (rp empty) is the root/package-less boundary
|
||||
// and MUST keep the "main" default — so don't stamp.
|
||||
if (f.nmod.len == 0) { f.nmod = rp; };
|
||||
} else {
|
||||
let empty: str;
|
||||
empty.ptr = nil;
|
||||
|
||||
@@ -587,6 +587,7 @@ export fn wwiemit(c: *checker, file: *syntax.node, path: str) i32 = {
|
||||
|
||||
// package line: leaf of the first primary decl's module tag.
|
||||
let pkg: str = "main";
|
||||
let found: i32 = 0;
|
||||
let pd: *syntax.node = file.list;
|
||||
for (pd != nil) {
|
||||
if (wwiprimary(pd) && pd.nmod.len > 0) {
|
||||
@@ -604,11 +605,35 @@ export fn wwiemit(c: *checker, file: *syntax.node, path: str) i32 = {
|
||||
} else {
|
||||
pkg = pd.nmod;
|
||||
};
|
||||
found = 1;
|
||||
pd = nil;
|
||||
} else {
|
||||
pd = pd.next;
|
||||
};
|
||||
};
|
||||
// #11: a decl-less / export-less primary body carries no
|
||||
// module-tagged decl, so the scan above finds nothing; fall back to
|
||||
// the primary module identity stamped on the N_FILE node at parse
|
||||
// time. A real root `package main` arrives via a bare module-reset
|
||||
// and leaves file.nmod empty, so it stays "main". The detector is
|
||||
// scan-miss (found==0), NOT pkg=="main", to match cstage byte-for-
|
||||
// byte (rule 10) when a tagged decl legitimately leafs to "main".
|
||||
if (found == 0 && file.nmod.len > 0) {
|
||||
let dotidx: i32 = -1;
|
||||
let i: i32 = 0;
|
||||
for (i < file.nmod.len) {
|
||||
if (file.nmod[i] == 46u8) { dotidx = i; };
|
||||
i += 1;
|
||||
};
|
||||
if (dotidx >= 0) {
|
||||
let leaf: str;
|
||||
leaf.ptr = file.nmod.ptr + ((dotidx + 1): u64);
|
||||
leaf.len = file.nmod.len - dotidx - 1;
|
||||
pkg = leaf;
|
||||
} else {
|
||||
pkg = file.nmod;
|
||||
};
|
||||
};
|
||||
wputs(fd, "package ");
|
||||
wputs(fd, pkg);
|
||||
wputs(fd, ";\n");
|
||||
|
||||
273
test/wcc/989_wwileaf_run.c
Normal file
273
test/wcc/989_wwileaf_run.c
Normal file
@@ -0,0 +1,273 @@
|
||||
/*
|
||||
* 989_wwileaf_run — BUG-C (#11) regression pin: a decl-less / export-less
|
||||
* primary module's `.wwi` `package` line must carry the module's real leaf,
|
||||
* NOT the literal default "main".
|
||||
*
|
||||
* THE BUG: wwi_emit derived the `package` leaf by scanning for the first
|
||||
* PRIMARY decl bearing a module tag. A fully empty (or comment-only, or
|
||||
* export-less) body has NO such decl, so the scan fell through and the leaf
|
||||
* stayed the seed default "main". The dep's `.wwi` then read `package main;`
|
||||
* → when its importer composed the dep under `//ww:module <path>`, the
|
||||
* in-`.wwi` `package main` clause was validated against the import path and
|
||||
* REJECTED: "package main does not match import path <leaf>"
|
||||
* (cmd/wcc/parse.c, both stages). A decl-less module was simply unimportable.
|
||||
*
|
||||
* THE FIX (cmd/w6c/wwi.c + selfhost/cmd/wcc/wwi.ww + the two parse sites):
|
||||
* parse stamps the primary module identity on the N_FILE node (module/nmod,
|
||||
* "only if empty", never on the imported TK_MODPATH boundary nor the bare
|
||||
* root reset); wwi_emit falls back to that identity's leaf when the decl-scan
|
||||
* finds nothing (`!found` — NOT pkg=="main", so a body whose first tagged
|
||||
* decl legitimately leafs to "main" is unaffected and both stages decide
|
||||
* identically). A real root `package main` arrives via a bare reset, leaves
|
||||
* the stamp empty, and stays "main".
|
||||
*
|
||||
* Table-driven over the three decl-less shapes that all route the fallback:
|
||||
* an empty body, a comment-only body, and a NESTED dotted path (a.b.c → leaf
|
||||
* c, exercising the leaf-slice). Each row drives the REAL producer→importer
|
||||
* flow on BOTH stages:
|
||||
* (a) the dep `.wwi` `package` line == the real leaf (pre-fix: "main");
|
||||
* (b) the dep `.wwi` is byte-identical w6c vs w6c_ww (rule 10);
|
||||
* (c) a root that `import`s the dep RESOLVES on both stages (pre-fix:
|
||||
* hard REJECT) and the two importer `.s` are byte-identical (rule 10).
|
||||
*
|
||||
* The composed units are fed straight to w6c / w6c_ww (not `ww build`): the
|
||||
* producer-unit shape `//ww:module-reset <path>` + body is exactly what the
|
||||
* driver's sep_emit_body emits, so this pins the same path without the
|
||||
* driver's filesystem layout. Non-vacuity: neutering the wwi_emit fallback
|
||||
* reddens row (a) ("package main;") and row (c) (the import REJECT).
|
||||
*
|
||||
* Light wwstage-driver test (CLAUDE.md rule 14): every intermediate lands in
|
||||
* a private mkdtemp dir, rm -rf'd at exit — no /tmp scratch leak (task #8).
|
||||
* 989 prefix per the sep-gate precedent; twin of 989_modresetadj_run (#9).
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static int
|
||||
write_file(const char *path, const char *content)
|
||||
{
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) return -1;
|
||||
fputs(content, f);
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* First physical line of `path` (newline stripped) into `out`. */
|
||||
static int
|
||||
first_line(const char *path, char *out, size_t outsz)
|
||||
{
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return -1;
|
||||
if (fgets(out, (int)outsz, f) == NULL) { fclose(f); out[0] = '\0'; }
|
||||
fclose(f);
|
||||
size_t n = strlen(out);
|
||||
if (n && out[n - 1] == '\n') out[n - 1] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct row {
|
||||
const char *tag; /* scratch-file stem */
|
||||
const char *path; /* dotted module path on the //ww:module-reset */
|
||||
const char *leaf; /* the in-file `package <leaf>;` clause */
|
||||
const char *body; /* body after the clause (zero module-tagged decls) */
|
||||
const char *want; /* expected emitted `package <want>;` leaf */
|
||||
};
|
||||
|
||||
/* Each shape lacks any module-tagged decl, so wwi_emit's decl-scan misses
|
||||
* and the leaf must come from the parse-stamped N_FILE identity. */
|
||||
static const struct row rows[] = {
|
||||
{ "empty", "emptymod", "emptymod", "", "emptymod" },
|
||||
{ "comment", "cmod", "cmod", "// only a comment\n", "cmod" },
|
||||
{ "nested", "a.b.c", "c", "", "c" },
|
||||
};
|
||||
#define NROW ((int)(sizeof rows / sizeof rows[0]))
|
||||
#define RET 42
|
||||
|
||||
static int
|
||||
run_row(const char *bin, const char *dir, const struct row *r)
|
||||
{
|
||||
char prod[1024], cswwi[1024], wwwwi[1024], css[1024], wws[1024];
|
||||
char root[1024], rcss[1024], rwws[1024], unit[8192], cmd[16384];
|
||||
|
||||
snprintf(prod, sizeof prod, "%s/%s.prod.ww", dir, r->tag);
|
||||
snprintf(cswwi, sizeof cswwi, "%s/%s.cs.wwi", dir, r->tag);
|
||||
snprintf(wwwwi, sizeof wwwwi, "%s/%s.ww.wwi", dir, r->tag);
|
||||
snprintf(css, sizeof css, "%s/%s.cs.s", dir, r->tag);
|
||||
snprintf(wws, sizeof wws, "%s/%s.ww.s", dir, r->tag);
|
||||
snprintf(root, sizeof root, "%s/%s.root.ww", dir, r->tag);
|
||||
snprintf(rcss, sizeof rcss, "%s/%s.rcs.s", dir, r->tag);
|
||||
snprintf(rwws, sizeof rwws, "%s/%s.rww.s", dir, r->tag);
|
||||
|
||||
/* the producer unit — exactly the driver's sep_emit_body shape. */
|
||||
snprintf(unit, sizeof unit,
|
||||
"//ww:module-reset %s\n"
|
||||
"package %s;\n"
|
||||
"%s",
|
||||
r->path, r->leaf, r->body);
|
||||
if (write_file(prod, unit) != 0) {
|
||||
fprintf(stderr, "wwileaf[%s]: cannot write producer unit\n", r->tag);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Leg a — both stages emit the dep `.wwi`; the `package` leaf is the
|
||||
* module's real leaf, not the default "main". */
|
||||
snprintf(cmd, sizeof cmd,
|
||||
"%s/w6c -c -I %s -o %s %s 2>/dev/null", bin, cswwi, css, prod);
|
||||
if (runwait(cmd) != 0) {
|
||||
fprintf(stderr, "wwileaf[%s] FAIL: w6c producer errored\n", r->tag);
|
||||
return 1;
|
||||
}
|
||||
snprintf(cmd, sizeof cmd,
|
||||
"%s/w6c_ww -c -I %s -o %s %s 2>/dev/null", bin, wwwwi, wws, prod);
|
||||
if (runwait(cmd) != 0) {
|
||||
fprintf(stderr, "wwileaf[%s] FAIL: w6c_ww producer errored\n", r->tag);
|
||||
return 1;
|
||||
}
|
||||
char want[256], line[512];
|
||||
snprintf(want, sizeof want, "package %s;", r->want);
|
||||
if (first_line(cswwi, line, sizeof line) != 0 || strcmp(line, want) != 0) {
|
||||
fprintf(stderr,
|
||||
"wwileaf[%s] FAIL: .wwi package line '%s' want '%s' (BUG-C)\n",
|
||||
r->tag, line, want);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Leg b — the dep `.wwi` is byte-identical across stages (rule 10). */
|
||||
if (files_eq(cswwi, wwwwi) != 0) {
|
||||
fprintf(stderr,
|
||||
"wwileaf[%s] FAIL: w6c vs w6c_ww .wwi differ (rule 10)\n", r->tag);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Leg c — a root importing the dep RESOLVES on both stages (pre-fix:
|
||||
* "package main does not match import path %s" REJECT). The dep `.wwi`
|
||||
* is prepended under //ww:module <path>, exactly as the driver composes
|
||||
* an importer unit. */
|
||||
{
|
||||
char *wwi = NULL;
|
||||
size_t wlen = 0;
|
||||
if (slurp(cswwi, &wwi, &wlen) < 0) {
|
||||
fprintf(stderr, "wwileaf[%s]: cannot read dep .wwi\n", r->tag);
|
||||
return 1;
|
||||
}
|
||||
snprintf(unit, sizeof unit,
|
||||
"//ww:module %s\n"
|
||||
"%s\n"
|
||||
"//ww:module-reset\n"
|
||||
"package main;\n"
|
||||
"import %s;\n"
|
||||
"export fn main() i32 = { return %d; };\n",
|
||||
r->path, wwi, r->path, RET);
|
||||
free(wwi);
|
||||
}
|
||||
if (write_file(root, unit) != 0) {
|
||||
fprintf(stderr, "wwileaf[%s]: cannot write importer unit\n", r->tag);
|
||||
return 1;
|
||||
}
|
||||
snprintf(cmd, sizeof cmd,
|
||||
"%s/w6c -c -o %s %s 2>/dev/null", bin, rcss, root);
|
||||
if (runwait(cmd) != 0) {
|
||||
fprintf(stderr,
|
||||
"wwileaf[%s] FAIL: w6c rejected the import (BUG-C)\n", r->tag);
|
||||
return 1;
|
||||
}
|
||||
snprintf(cmd, sizeof cmd,
|
||||
"%s/w6c_ww -c -o %s %s 2>/dev/null", bin, rwws, root);
|
||||
if (runwait(cmd) != 0) {
|
||||
fprintf(stderr,
|
||||
"wwileaf[%s] FAIL: w6c_ww rejected the import (BUG-C)\n", r->tag);
|
||||
return 1;
|
||||
}
|
||||
if (files_eq(rcss, rwws) != 0) {
|
||||
fprintf(stderr,
|
||||
"wwileaf[%s] FAIL: importer .s differ cs vs ww (rule 10)\n",
|
||||
r->tag);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
main(void)
|
||||
{
|
||||
const char *bin = absbin();
|
||||
if (!bin) return 1;
|
||||
|
||||
char dir[] = "/tmp/wwileaf_XXXXXX";
|
||||
if (mkdtemp(dir) == NULL) {
|
||||
fprintf(stderr, "wwileaf: mkdtemp failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
int fail = 0;
|
||||
for (int i = 0; i < NROW; i++)
|
||||
fail |= run_row(bin, dir, &rows[i]);
|
||||
|
||||
char rm[2048];
|
||||
snprintf(rm, sizeof rm, "rm -rf %s", dir);
|
||||
(void)runwait(rm);
|
||||
|
||||
if (fail) return 1;
|
||||
printf("wwileaf: %d decl-less shapes — .wwi leaf derived, byte-id, "
|
||||
"import resolves cs==ww (#11)\n", NROW);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user