From 7b6f24adeab59a93a580bfb621248aa0b5d132ca Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 18 Jun 2026 12:47:41 +0900 Subject: [PATCH] w6c,ww: mangle an imported package's `fn main` under separate compilation (M4 E3, #99) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bare-`main` carve-out (which keeps the link entry's main unmangled) keyed on `leaf == "main" && imported == 0`. Under the combined path a dependency's body folds in with imported==1, so only the root's main stayed bare. Under separate compilation each package is its own unit and a dependency's body carries a path-mangling module-reset but imported==0 (#57) — so an imported `fn main` matched the carve-out, emitted a bare `TEXT main`, and collided with the root entry (`w6l: duplicate symbol main`). The combined path was unaffected, so this only surfaced under sep. Gate the carve-out with sep_isdep = (wwiout != NULL): the producer emits a .wwi output only for dependency units, never for the root/link-entry unit (root stripped, #69), symmetric on both stages. Only the root unit's main now stays bare; an imported package's main mangles on its import path (e.g. aa.bb.main). Both stages. Gate: test/wcc/989_depmain_sep.c (table-driven, dotted + single-component shapes, both stages; asserts the mangled dep main + a single bare root main + cs==ww byte-id; combined path stays neutral). --- Makefile | 14 ++ cmd/w6c/cgen.c | 18 +- cmd/w6c/gc.h | 8 + cmd/w6c/main.c | 4 + selfhost/cmd/w6c/main.combined.ww | 24 ++- selfhost/cmd/w6c/main.ww | 4 + selfhost/cmd/wcc/cgen.ww | 15 +- selfhost/cmd/wcc/cgendecl.ww | 5 +- selfhost/cmd/wwdump/main.combined.ww | 20 +- test/wcc/989_depmain_sep.c | 285 +++++++++++++++++++++++++++ test/wcc/data/depmain99/aa/bb/bb.ww | 3 + test/wcc/data/depmain99/cc/cc.ww | 3 + test/wcc/data/depmain99/main.ww | 3 + test/wcc/data/depmain99/main_cc.ww | 3 + 14 files changed, 399 insertions(+), 10 deletions(-) create mode 100644 test/wcc/989_depmain_sep.c create mode 100644 test/wcc/data/depmain99/aa/bb/bb.ww create mode 100644 test/wcc/data/depmain99/cc/cc.ww create mode 100644 test/wcc/data/depmain99/main.ww create mode 100644 test/wcc/data/depmain99/main_cc.ww diff --git a/Makefile b/Makefile index fdf24e80..bb2f93fb 100644 --- a/Makefile +++ b/Makefile @@ -573,6 +573,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_c6soak_run \ $(BIN)/test_septest_run \ $(BIN)/test_coloimport_sep \ + $(BIN)/test_depmain_sep \ $(BIN)/test_floatlit_run \ $(BIN)/test_checked_run \ $(BIN)/test_floatarr_run \ @@ -3270,6 +3271,19 @@ $(BIN)/test_coloimport_sep: test/wcc/989_coloimport_sep.c $(BIN)/ww $(BIN)/ww_ww $(BIN)/w6l $(BIN)/w6l_ww $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# 989_depmain_sep — #99 E3 flip-blocker: an imported dir-package's non-exported +# `fn main` must mangle on its import path under `ww build --sep`, not emit a +# bare `TEXT main` that collides with the root unit's entry (= w6l dup-symbol). +# Drives BOTH driver stages on dotted- and single-component import fixtures, +# asserting sep build+link+run exit (pre-fix the bare-main collision makes w6l +# reject = the non-vacuity teeth), the dep's main mangles with exactly one bare +# root main, cs==ww per-pkg .s/.wwi (rule 10), and combined unaffected. Needs +# both driver + compiler + asm + linker stages. +$(BIN)/test_depmain_sep: test/wcc/989_depmain_sep.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 $@ $< + $(BIN)/test_floatlit_run: test/wcc/989_floatlit_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index aa13d0f0..32ddc3d4 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -1477,8 +1477,14 @@ mod_collect(Cg *c, Node *file) * primary's decls are MODULED "main", so main is NOT bare here; * skip it (cgfn force-emits the bare `main` label). M1 #32: only * the ROOT main (imported==0) stays bare; an IMPORTED `fn main` - * mangles on its path (closes #31's dup-main by construction). */ - if (d->str && strcmp(d->str, "main") == 0 && !d->imported) + * mangles on its path (closes #31's dup-main by construction). + * #99: under sep a dep unit's main is imported==0 too (its body + * is composed with a path-carrying `//ww:module-reset`, #57), so + * imported==0 no longer means "root unit" per-unit. Gate on + * !sep_isdep (wwiout==NULL <=> root/link-entry unit, #69) so only + * the root's main stays bare; a dep's main mangles on its path. */ + if (d->str && strcmp(d->str, "main") == 0 && !d->imported + && !c->sep_isdep) continue; Mod *m = amalloc(c->a, sizeof *m); m->name = d->str; @@ -14960,8 +14966,12 @@ cgfn(Cg *c, FILE *out, Node *fn) * NOT a bare-module decl — removing this carve-out mangles it to * `main.main` (undefined `main`). #84's bare-module handling is the * orthogonal axis (a package-LESS `//ww:module-reset` fn, cur_mod - * NULL), resolved in mod_lookup_for_fn, NOT here. */ - if (fn->str && strcmp(fn->str, "main") == 0 && !fn->imported) + * NULL), resolved in mod_lookup_for_fn, NOT here. #99: gate on + * !sep_isdep — under sep a dep unit's main is imported==0 (path- + * carrying `//ww:module-reset`, #57); only the root/link-entry unit + * (wwiout==NULL, #69) keeps the bare label. */ + if (fn->str && strcmp(fn->str, "main") == 0 && !fn->imported + && !c->sep_isdep) text->to = asym("main"); else text->to = mafn(c, fn->str, c->cur_mod); diff --git a/cmd/w6c/gc.h b/cmd/w6c/gc.h index 1221b8e3..f8799405 100644 --- a/cmd/w6c/gc.h +++ b/cmd/w6c/gc.h @@ -54,6 +54,14 @@ struct Cg { * the dep's real definition (link collision). * Off on the combined path (every existing * invocation) so M3 is a pure addition. */ + int sep_isdep; /* #99: this unit is a sep DEPENDENCY, not the + * root/link-entry unit. Set from `wwiout != NULL` + * in main: the producer passes -I (.wwi output) + * to DEP units only — the root's .wwi is stripped + * (#69), so wwiout==NULL <=> root/link-entry unit. + * Gates the bare-`main` carve-out: a dep's `fn + * main` must mangle on its path like any decl; + * only the root entry stays bare. */ }; /* cgen.c */ diff --git a/cmd/w6c/main.c b/cmd/w6c/main.c index 01161068..30213857 100644 --- a/cmd/w6c/main.c +++ b/cmd/w6c/main.c @@ -110,6 +110,10 @@ main(int argc, char **argv) cg_init(&cg, a); cg.sep_mode = sepmode; + /* #99: wwiout != NULL <=> this is a sep DEP unit (the producer passes + * -I to deps only; the root's .wwi is stripped per #69). Gates the + * bare-`main` carve-out so only the root/link-entry main stays bare. */ + cg.sep_isdep = (wwiout != NULL); cg_file(&cg, of, file); if (of != stdout) fclose(of); diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 8d06e1f6..3dce3b36 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -41326,8 +41326,11 @@ fn cgfn(c: *cgen, fn_: *syntax.node) void = { // (undefined `main`). #84's bare-module handling is the orthogonal // axis (a package-LESS `//ww:module-reset` fn, nmod empty), resolved // in modlookupforfn, NOT here. + // #99: gate on sepisdep — under sep a dep unit's main is imported==0 + // (path-carrying `//ww:module-reset`, #57); only the root/link-entry + // unit (wwiout==nil, #69) keeps the bare label. emitline("TEXT "); - if (syntax.streq(fn_.str, "main") && fn_.imported == 0) { + if (syntax.streq(fn_.str, "main") && fn_.imported == 0 && c.sepisdep == 0) { emitbytes(fn_.str.ptr, fn_.str.len: u64); } else { emitfnname(c, fn_.str, fn_.nmod); @@ -41932,6 +41935,14 @@ type cgen = struct { // must survive to the post-loop emitletdataw/emitdefconstants pass, // like strlits/ffis. Symmetric with cstage Cg.sep_mode. sepmode: i32, + // #99: this unit is a sep DEPENDENCY, not the root/link-entry unit. + // Set from `wwiout != nil` in main — the producer passes -I (.wwi + // output) to DEP units only; the root's .wwi is stripped (#69), so + // wwiout==nil <=> root/link-entry unit. Gates the bare-`main` carve- + // out: a dep's `fn main` mangles on its path like any decl; only the + // root entry stays bare. Like sepmode, NOT reset by cgeninit (per-fn). + // Symmetric with cstage Cg.sep_isdep. + sepisdep: i32, }; // Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar. @@ -45263,7 +45274,12 @@ fn collectmods(c: *cgen, file: *syntax.node) void = { // label). M1 #32: only ROOT main (imported==0) stays bare; // an IMPORTED `fn main` mangles on its path. Mirrors // cstage cgen.c mod_collect. - if (!syntax.streq(d.str, "main") || d.imported != 0) { + // #99: under sep a dep unit's main is imported==0 too (its + // body is composed with a path-carrying `//ww:module-reset`, + // #57), so imported==0 no longer means "root unit" per-unit. + // sepisdep (wwiout==nil <=> root/link-entry unit, #69) + // re-mangles a dep's main; only the root's stays bare. + if (!syntax.streq(d.str, "main") || d.imported != 0 || c.sepisdep != 0) { let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!; c.mods = m; }; @@ -46339,6 +46355,10 @@ export fn main(argc: i32, argv: **u8) i32 = { let cg: cgen; cgeninit(&cg); cg.sepmode = sepmode; + // #99: wwiout != nil <=> this is a sep DEP unit (the producer passes + // -I to deps only; the root's .wwi is stripped per #69). Gates the + // bare-`main` carve-out so only the root/link-entry main stays bare. + if (wwiout != nil) { cg.sepisdep = 1i32; }; cgfile(&cg, f); return 0; }; diff --git a/selfhost/cmd/w6c/main.ww b/selfhost/cmd/w6c/main.ww index 84937add..94a0f774 100644 --- a/selfhost/cmd/w6c/main.ww +++ b/selfhost/cmd/w6c/main.ww @@ -195,6 +195,10 @@ export fn main(argc: i32, argv: **u8) i32 = { let cg: cgen; cgeninit(&cg); cg.sepmode = sepmode; + // #99: wwiout != nil <=> this is a sep DEP unit (the producer passes + // -I to deps only; the root's .wwi is stripped per #69). Gates the + // bare-`main` carve-out so only the root/link-entry main stays bare. + if (wwiout != nil) { cg.sepisdep = 1i32; }; cgfile(&cg, f); return 0; }; diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index c8f40d65..5da2b6d4 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -544,6 +544,14 @@ type cgen = struct { // must survive to the post-loop emitletdataw/emitdefconstants pass, // like strlits/ffis. Symmetric with cstage Cg.sep_mode. sepmode: i32, + // #99: this unit is a sep DEPENDENCY, not the root/link-entry unit. + // Set from `wwiout != nil` in main — the producer passes -I (.wwi + // output) to DEP units only; the root's .wwi is stripped (#69), so + // wwiout==nil <=> root/link-entry unit. Gates the bare-`main` carve- + // out: a dep's `fn main` mangles on its path like any decl; only the + // root entry stays bare. Like sepmode, NOT reset by cgeninit (per-fn). + // Symmetric with cstage Cg.sep_isdep. + sepisdep: i32, }; // Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar. @@ -3875,7 +3883,12 @@ fn collectmods(c: *cgen, file: *syntax.node) void = { // label). M1 #32: only ROOT main (imported==0) stays bare; // an IMPORTED `fn main` mangles on its path. Mirrors // cstage cgen.c mod_collect. - if (!syntax.streq(d.str, "main") || d.imported != 0) { + // #99: under sep a dep unit's main is imported==0 too (its + // body is composed with a path-carrying `//ww:module-reset`, + // #57), so imported==0 no longer means "root unit" per-unit. + // sepisdep (wwiout==nil <=> root/link-entry unit, #69) + // re-mangles a dep's main; only the root's stays bare. + if (!syntax.streq(d.str, "main") || d.imported != 0 || c.sepisdep != 0) { let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!; c.mods = m; }; diff --git a/selfhost/cmd/wcc/cgendecl.ww b/selfhost/cmd/wcc/cgendecl.ww index 96b81c28..9bb66099 100644 --- a/selfhost/cmd/wcc/cgendecl.ww +++ b/selfhost/cmd/wcc/cgendecl.ww @@ -607,8 +607,11 @@ fn cgfn(c: *cgen, fn_: *syntax.node) void = { // (undefined `main`). #84's bare-module handling is the orthogonal // axis (a package-LESS `//ww:module-reset` fn, nmod empty), resolved // in modlookupforfn, NOT here. + // #99: gate on sepisdep — under sep a dep unit's main is imported==0 + // (path-carrying `//ww:module-reset`, #57); only the root/link-entry + // unit (wwiout==nil, #69) keeps the bare label. emitline("TEXT "); - if (syntax.streq(fn_.str, "main") && fn_.imported == 0) { + if (syntax.streq(fn_.str, "main") && fn_.imported == 0 && c.sepisdep == 0) { emitbytes(fn_.str.ptr, fn_.str.len: u64); } else { emitfnname(c, fn_.str, fn_.nmod); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 75ed2839..a15e69f8 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -41326,8 +41326,11 @@ fn cgfn(c: *cgen, fn_: *syntax.node) void = { // (undefined `main`). #84's bare-module handling is the orthogonal // axis (a package-LESS `//ww:module-reset` fn, nmod empty), resolved // in modlookupforfn, NOT here. + // #99: gate on sepisdep — under sep a dep unit's main is imported==0 + // (path-carrying `//ww:module-reset`, #57); only the root/link-entry + // unit (wwiout==nil, #69) keeps the bare label. emitline("TEXT "); - if (syntax.streq(fn_.str, "main") && fn_.imported == 0) { + if (syntax.streq(fn_.str, "main") && fn_.imported == 0 && c.sepisdep == 0) { emitbytes(fn_.str.ptr, fn_.str.len: u64); } else { emitfnname(c, fn_.str, fn_.nmod); @@ -41932,6 +41935,14 @@ type cgen = struct { // must survive to the post-loop emitletdataw/emitdefconstants pass, // like strlits/ffis. Symmetric with cstage Cg.sep_mode. sepmode: i32, + // #99: this unit is a sep DEPENDENCY, not the root/link-entry unit. + // Set from `wwiout != nil` in main — the producer passes -I (.wwi + // output) to DEP units only; the root's .wwi is stripped (#69), so + // wwiout==nil <=> root/link-entry unit. Gates the bare-`main` carve- + // out: a dep's `fn main` mangles on its path like any decl; only the + // root entry stays bare. Like sepmode, NOT reset by cgeninit (per-fn). + // Symmetric with cstage Cg.sep_isdep. + sepisdep: i32, }; // Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar. @@ -45263,7 +45274,12 @@ fn collectmods(c: *cgen, file: *syntax.node) void = { // label). M1 #32: only ROOT main (imported==0) stays bare; // an IMPORTED `fn main` mangles on its path. Mirrors // cstage cgen.c mod_collect. - if (!syntax.streq(d.str, "main") || d.imported != 0) { + // #99: under sep a dep unit's main is imported==0 too (its + // body is composed with a path-carrying `//ww:module-reset`, + // #57), so imported==0 no longer means "root unit" per-unit. + // sepisdep (wwiout==nil <=> root/link-entry unit, #69) + // re-mangles a dep's main; only the root's stays bare. + if (!syntax.streq(d.str, "main") || d.imported != 0 || c.sepisdep != 0) { let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!; c.mods = m; }; diff --git a/test/wcc/989_depmain_sep.c b/test/wcc/989_depmain_sep.c new file mode 100644 index 00000000..2631066b --- /dev/null +++ b/test/wcc/989_depmain_sep.c @@ -0,0 +1,285 @@ +/* + * 989_depmain_sep — #99 E3 flip-blocker: an IMPORTED dir-package's non- + * exported `fn main` must mangle on its import path under `ww build --sep`, + * not emit a bare `TEXT main` that collides with the root unit's entry. + * + * The bare-`main` carve-out (cgen mod_collect + cgfn label, both stages) + * keys on `leaf=="main" && imported==0`. Under COMBINED that predicate is + * exact — only the root's main is imported==0. Under SEP each package is its + * own w6c unit, composed with a path-carrying `//ww:module-reset ` + * (#57) that mangles decls but leaves imported==0 — so a dep unit's `fn + * main` was imported==0 too, the carve-out fired, and it emitted a bare + * `TEXT main` → `w6l: duplicate symbol main` against the root's real main. + * + * The fix adds a NON-ROOT gate `&& !sep_isdep` to the carve-out. sep_isdep + * is set from `wwiout != NULL`: the producer passes -I (the .wwi output) to + * DEP units ONLY — the root/link-entry unit's .wwi is stripped (#69) — so + * `wwiout==NULL` is the exact "this is the root/link-entry unit" oracle. A + * dep's main now mangles on its path; only the root's stays bare. + * + * Fixtures (test/wcc/data/depmain99/, srcd-relative dir-package resolution): + * main.ww package main, import aa.bb — root; calls bb.run() → 7. + * aa/bb/bb.ww package bb, `fn main`→7 + `export fn run` calls it — the + * DOTTED-path imported dep whose main must mangle aa.bb.main. + * main_cc.ww package main, import cc — root; calls cc.run() → 9. + * cc/cc.ww package cc, `fn main`→9 + `export fn run` — the SINGLE- + * component imported dep whose main must mangle cc.main. + * + * Asserts (both driver stages, COLD per-stage WW_PKGCACHE): + * 1. SEP build+link+run exit == the dep's main's return — pre-fix the + * bare-main collision makes w6l reject. This is the non-vacuity teeth + * (verified: revert `&& !sep_isdep` → `w6l: duplicate symbol main`). + * 2. MANGLED — the dep's .s shows `TEXT .main`; exactly ONE bare + * `TEXT main,` across the whole sep build (the root's __root.s). + * 3. cs==ww (rule 10) — per-package .s/.wwi byte-identical across the + * cstage `ww` and wwstage `ww_ww` sep-drivers. + * 4. COMBINED unaffected — `ww build` (no --sep) still links+runs to the + * same exit on both stages (the fix must not change combined behavior). + * + * Light wwstage-driver test (CLAUDE.md rule 14): every intermediate is + * `-o`-redirected to /tmp, so phase-1 parallel-safe. Models 989_coloimport_sep + * conventions; 989 prefix per the sep-gate precedent. + */ +#include +#include +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return -2; +} + +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; +} + +/* cs==ww over a sep build's per-package output: every .s/.wwi the cstage + * driver produced in `csdir` must be byte-identical to the wwstage driver's + * same-named file in `wwdir`. Returns the count of mismatches. */ +static int +cmp_sepwork(const char *csdir, const char *wwdir, const char *label) +{ + DIR *d = opendir(csdir); + if (!d) { + fprintf(stderr, "depmain FAIL: %s — no cs sepwork %s\n", label, csdir); + return 1; + } + int bad = 0, seen = 0; + struct dirent *ent; + while ((ent = readdir(d)) != NULL) { + const char *nm = ent->d_name; + size_t nl = strlen(nm); + int is_s = (nl > 2 && strcmp(nm + nl - 2, ".s") == 0); + int is_wwi = (nl > 4 && strcmp(nm + nl - 4, ".wwi") == 0); + if (!is_s && !is_wwi) continue; + seen++; + char a[2048], b[2048]; + snprintf(a, sizeof a, "%s/%s", csdir, nm); + snprintf(b, sizeof b, "%s/%s", wwdir, nm); + if (files_eq(a, b) != 0) { + fprintf(stderr, "depmain FAIL: %s — cs!=ww for %s (rule 10)\n", + label, nm); + bad++; + } + } + closedir(d); + if (seen == 0) { + fprintf(stderr, "depmain FAIL: %s — no .s/.wwi in %s\n", label, csdir); + bad++; + } + return bad; +} + +/* count_bare_main — number of bare `TEXT main,` labels across every .s in + * `dir`. Exactly 1 is correct (the root entry); the bug produces 2 (the + * dep's mis-bared main collides). */ +static int +count_bare_main(const char *dir) +{ + DIR *d = opendir(dir); + if (!d) return -1; + int n = 0; + struct dirent *ent; + while ((ent = readdir(d)) != NULL) { + const char *nm = ent->d_name; + size_t nl = strlen(nm); + if (!(nl > 2 && strcmp(nm + nl - 2, ".s") == 0)) continue; + char p[2048]; + snprintf(p, sizeof p, "%s/%s", dir, nm); + char *b = NULL; + size_t bn = 0; + if (slurp(p, &b, &bn) < 0) continue; + /* a bare label is `TEXT main,` at column 0 of any line. */ + for (char *s = b; (s = strstr(s, "TEXT main,")) != NULL; s++) + if (s == b || s[-1] == '\n') n++; + free(b); + } + closedir(d); + return n; +} + +/* file_has — 0 if `needle` occurs in `path`, 1 if absent, -1 on read err. */ +static int +file_has(const char *path, const char *needle) +{ + char *b = NULL; + size_t bn = 0; + if (slurp(path, &b, &bn) < 0) return -1; + int found = (strstr(b, needle) != NULL); + free(b); + return found ? 0 : 1; +} + +struct tcase { + const char *label; + const char *entry; /* root entry built under --sep */ + int exit; /* the dep main's return, == the program exit */ + const char *deps; /* the dep's .s basename in .sepwork */ + const char *mangled; /* the mangled TEXT label the dep must emit */ +}; + +static struct tcase cases[] = { + /* dotted-path import: dep main mangles on the full path. */ + { "aabb", "test/wcc/data/depmain99/main.ww", 7, + "aa.bb.s", "TEXT aa.bb.main," }, + /* single-component import: dep main mangles on the leaf package. */ + { "cc", "test/wcc/data/depmain99/main_cc.ww", 9, + "cc.s", "TEXT cc.main," }, + { NULL, NULL, 0, NULL, NULL }, +}; + +int +main(void) +{ + const char *bin = absbin(); + if (!bin) return 1; + + char td[64], cmd[8192]; + int fail = 0; + snprintf(td, sizeof td, "/tmp/wwdepmain_%d", getpid()); + snprintf(cmd, sizeof cmd, "rm -rf %s", td); + runwait(cmd); + mkdir(td, 0755); + + for (int i = 0; cases[i].label; i++) { + struct tcase *t = &cases[i]; + + struct { const char *drv, *tag; int rc; } + stg[] = { { "ww", "cs", -1 }, { "ww_ww", "ww", -1 } }; + + /* 1. SEP build+link+run exit == the dep main's return, both stages. */ + for (int s = 0; s < 2; s++) { + char prog[1024]; + snprintf(prog, sizeof prog, "%s/%s.%s.bin", td, t->label, stg[s].tag); + snprintf(cmd, sizeof cmd, + "WW_PKGCACHE='%s/cache.%s.%s' %s/%s build --sep -o %s %s " + ">/dev/null 2>&1 && %s", + td, t->label, stg[s].tag, bin, stg[s].drv, prog, t->entry, prog); + stg[s].rc = runwait(cmd); + if (stg[s].rc != t->exit) { + fprintf(stderr, "depmain FAIL: %s sep %s exit=%d (expected %d)\n", + t->label, stg[s].tag, stg[s].rc, t->exit); + fail++; + } + } + + char csdir[1024], wwdir[1024]; + snprintf(csdir, sizeof csdir, "%s/%s.cs.bin.sepwork", td, t->label); + snprintf(wwdir, sizeof wwdir, "%s/%s.ww.bin.sepwork", td, t->label); + + /* 2. MANGLED: the dep's .s emits `TEXT .main`; exactly one bare + * `TEXT main,` across the whole sep build (the root's __root.s). */ + char deps[2048]; + snprintf(deps, sizeof deps, "%s/%s", csdir, t->deps); + if (file_has(deps, t->mangled) != 0) { + fprintf(stderr, "depmain FAIL: %s — %s lacks `%s` (dep main not " + "mangled)\n", t->label, deps, t->mangled); + fail++; + } + int nbare = count_bare_main(csdir); + if (nbare != 1) { + fprintf(stderr, "depmain FAIL: %s — %d bare `TEXT main,` in %s " + "(expected 1, the root entry)\n", t->label, nbare, csdir); + fail++; + } + + /* 3. cs==ww (rule 10): per-package .s/.wwi byte-identical. */ + fail += cmp_sepwork(csdir, wwdir, t->label); + + /* 4. COMBINED unaffected: `ww build` (no --sep) still runs to exit. */ + for (int s = 0; s < 2; s++) { + char prog[1024]; + snprintf(prog, sizeof prog, "%s/%s.%s.cmb", td, t->label, stg[s].tag); + snprintf(cmd, sizeof cmd, + "%s/%s build -o %s %s >/dev/null 2>&1 && %s", + bin, stg[s].drv, prog, t->entry, prog); + int rc = runwait(cmd); + if (rc != t->exit) { + fprintf(stderr, "depmain FAIL: %s combined %s exit=%d " + "(expected %d)\n", t->label, stg[s].tag, rc, t->exit); + fail++; + } + } + } + + snprintf(cmd, sizeof cmd, "rm -rf %s", td); + runwait(cmd); + if (fail) { + fprintf(stderr, "depmain: %d check(s) failed\n", fail); + return 1; + } + printf("depmain: imported dir-package `fn main` mangles on its import path " + "under `ww build --sep` (only the root entry stays bare) on both driver " + "stages + cs==ww per-pkg .s/.wwi + combined unaffected\n"); + return 0; +} diff --git a/test/wcc/data/depmain99/aa/bb/bb.ww b/test/wcc/data/depmain99/aa/bb/bb.ww new file mode 100644 index 00000000..e64ffc6c --- /dev/null +++ b/test/wcc/data/depmain99/aa/bb/bb.ww @@ -0,0 +1,3 @@ +package bb; +fn main() int = { return 7; }; +export fn run() int = { return main(); }; diff --git a/test/wcc/data/depmain99/cc/cc.ww b/test/wcc/data/depmain99/cc/cc.ww new file mode 100644 index 00000000..27127989 --- /dev/null +++ b/test/wcc/data/depmain99/cc/cc.ww @@ -0,0 +1,3 @@ +package cc; +fn main() int = { return 9; }; +export fn run() int = { return main(); }; diff --git a/test/wcc/data/depmain99/main.ww b/test/wcc/data/depmain99/main.ww new file mode 100644 index 00000000..83721a02 --- /dev/null +++ b/test/wcc/data/depmain99/main.ww @@ -0,0 +1,3 @@ +package main; +import aa.bb; +export fn main() int = { return bb.run(); }; diff --git a/test/wcc/data/depmain99/main_cc.ww b/test/wcc/data/depmain99/main_cc.ww new file mode 100644 index 00000000..33230d2e --- /dev/null +++ b/test/wcc/data/depmain99/main_cc.ww @@ -0,0 +1,3 @@ +package main; +import cc; +export fn main() int = { return cc.run(); };