From 1724ea086f212b70d8d6cf3ce94277e3dae2ef40 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Wed, 12 Aug 2026 22:00:02 +0900 Subject: [PATCH] build: make executable and test roots package actions --- cmd/ww/main.c | 246 +++++++++++++++++++-------- docs/build-system.md | 187 ++++++++++++--------- selfhost/cmd/ww/main.ww | 314 +++++++++++++++++++++++++---------- test/misc/packedwwi_test.ww | 6 +- test/package/package_test.ww | 134 +++++++++++---- test/sep/importdir_test.ww | 10 +- test/sep/localbuild_test.ww | 42 +++-- test/sep/sepbuild_test.ww | 82 ++++----- test/sep/sepimport_test.ww | 13 +- test/sep/seplink_test.ww | 44 ++--- test/wcc/992_w6l_ww.c | 8 +- 11 files changed, 722 insertions(+), 364 deletions(-) diff --git a/cmd/ww/main.c b/cmd/ww/main.c index 2de89c42..dea8ed13 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -322,9 +322,11 @@ source_has_test_decl(const char *path) #define SEP_VARIANT_PRODUCTION 0 #define SEP_VARIANT_SAME_TEST 1 #define SEP_VARIANT_EXTERNAL 2 +#define SEP_VARIANT_TEST_MAIN 3 #define SEP_ROLE_NORMAL 0 #define SEP_ROLE_EXTERNAL_PRODUCTION 1 #define SEP_ROLE_TEST_SUPPORT 2 +#define SEP_ROLE_GENERATED_MAIN 3 #define SEP_TEST_SUPPORT_MODULE "__wwtest" #define SEP_MAXPRODUCT 256 #define SEP_MAXCONTEXT (SEP_MAXPRODUCT + 1) @@ -508,7 +510,7 @@ enumerate_dir_ww(const char *dirpath, int variant, const char *test_package, #define SEP_MAXPKG 256 struct seppkg { - char path[256]; /* dotted import path; "" == root/primary */ + char path[256]; /* canonical dotted package identity */ char entry[1024]; /* resolved package dir (or file, for a file root) */ char canon[1024]; /* canonical location; never package identity */ char artifact[64]; /* non-importable product-root artifact key */ @@ -519,7 +521,9 @@ struct seppkg { int is_dir; int variant; /* SEP_VARIANT_*; dependencies are production */ int role; /* normal, external-production, or test support */ - int root; /* independently compiled/linkable product root */ + int root; /* requested package action (possibly a test variant) */ + int link_entry; /* package supplies the executable's bare main */ + int generated_main; /* compiler-owned generated test-main package */ int failed; /* discovery/compile failure reaches this action */ int test_support; /* compiler-generated -T support package */ int loaded; /* directory membership/name loaded exactly once */ @@ -554,6 +558,7 @@ struct sepproduct { int variant; int context; int root; + int variant_root; /* production-plus-test or external test package */ }; #define SEP_MAXLFLAGS 32 @@ -591,8 +596,17 @@ sep_find_or_add_variant(struct sepgraph *g, const char *path, if (root && g->pkg[i].root) { if (!same_location) continue; if (variant != g->pkg[i].variant) continue; + if (g->pkg[i].role == role + && strcmp(g->pkg[i].path, path) == 0 + && strcmp(g->pkg[i].test_package, + test_package ? test_package : "") == 0) { + /* One canonical directory variant is one compile action, + * even when more than one product requests it. */ + free(canon); + return i; + } fprintf(stderr, - "ww: duplicate package-test root %s\n", entry); + "ww: incompatible package-test roots %s\n", entry); free(canon); return -1; } @@ -693,6 +707,8 @@ sep_find_or_add_variant(struct sepgraph *g, const char *path, p->variant = variant; p->role = role; p->root = root; + p->link_entry = 0; + p->generated_main = 0; p->failed = 0; p->test_support = 0; p->loaded = 0; @@ -1135,6 +1151,62 @@ sep_dep_cmp(const struct sepgraph *g, int a, int b) return strcmp(g->pkg[a].artifact, g->pkg[b].artifact); } +/* Add the compiler-owned test main as a real package action. Its semantic + * identity and artifact key are distinct from every directory variant; its + * only dependencies are the selected test variant and dispatcher support. */ +static int +sep_add_generated_main(struct sepgraph *g, struct sepproduct *product, + int ordinal, int support) +{ + if (g->n >= SEP_MAXPKG) { + fprintf(stderr, "ww: too many packages (limit %d)\n", SEP_MAXPKG); + return -1; + } + int variant = product->variant_root; + if (variant < 0 || variant >= g->n) return -1; + char variantcanon[sizeof g->pkg[0].canon]; + char variantentry[sizeof g->pkg[0].entry]; + snprintf(variantcanon, sizeof variantcanon, "%s", g->pkg[variant].canon); + snprintf(variantentry, sizeof variantentry, "%s", g->pkg[variant].entry); + struct seppkg *p = &g->pkg[g->n]; + memset(p, 0, sizeof *p); + int pn = snprintf(p->path, sizeof p->path, + "__wwtestmain.%03d.main", ordinal); + int an = snprintf(p->artifact, sizeof p->artifact, + "__ww-test-%03d-main", ordinal); + int cn = snprintf(p->canon, sizeof p->canon, "%s#test-main-%03d", + variantcanon, ordinal); + if (pn < 0 || (size_t)pn >= sizeof p->path + || an < 0 || (size_t)an >= sizeof p->artifact + || cn < 0 || (size_t)cn >= sizeof p->canon) { + fprintf(stderr, "ww: generated test-main identity is too long\n"); + return -1; + } + snprintf(p->entry, sizeof p->entry, "%s", variantentry); + snprintf(p->name, sizeof p->name, "main"); + p->variant = SEP_VARIANT_TEST_MAIN; + p->role = SEP_ROLE_GENERATED_MAIN; + p->root = 1; + p->link_entry = 1; + p->generated_main = 1; + p->loaded = 1; + p->emit_context = product->context; + p->context_state[product->context] = 2; + p->deps[p->ndeps++] = variant; + if (support >= 0 && support != variant) + p->deps[p->ndeps++] = support; + for (int i = 1; i < p->ndeps; i++) { + int v = p->deps[i]; + int j = i; + while (j > 0 && sep_dep_cmp(g, p->deps[j - 1], v) > 0) { + p->deps[j] = p->deps[j - 1]; + j--; + } + p->deps[j] = v; + } + return g->n++; +} + /* Load one canonical package under one selected-root resolution context. * Source membership is owned once, but a shared package's imports are checked * under every context that reaches it. The first canonical binding set owns @@ -1224,10 +1296,11 @@ sep_load_pkg(struct sepgraph *g, int pi, int context) g->pkg[pi].failed = 1; return -1; } - /* Give a non-main root its declared identity before recursively loading - * dependencies. A back-edge can then reuse the root and reach the normal - * cycle detector instead of looking like a location alias. */ - if (g->pkg[pi].root && g->pkg[pi].path[0] == '\0' + /* Give a directory root its declared package identity before recursively + * loading dependencies. Explicit raw single-file compiler fixtures retain + * their historical anonymous multi-package boundary. */ + if (g->pkg[pi].root && g->pkg[pi].is_dir + && g->pkg[pi].path[0] == '\0' && g->pkg[pi].name[0] != '\0') { size_t n = strlen(g->pkg[pi].name); memcpy(g->pkg[pi].path, g->pkg[pi].name, n + 1); @@ -1358,7 +1431,15 @@ sep_compose_unit(struct sepgraph *g, int pi, const char *unitf) return -1; } int bodyrc = 0; - if (g->pkg[pi].is_dir) { + if (g->pkg[pi].generated_main) { + if (fprintf(u, "//ww:module-reset %s\npackage main;\n", + g->pkg[pi].path) < 0) + bodyrc = -1; + for (int i = 0; i < g->pkg[pi].ndeps && bodyrc == 0; i++) + if (fprintf(u, "import %s;\n", + g->pkg[g->pkg[pi].deps[i]].path) < 0) + bodyrc = -1; + } else if (g->pkg[pi].is_dir) { for (int i = 0; i < g->pkg[pi].nsources && bodyrc == 0; i++) bodyrc = sep_emit_body(u, g->pkg[pi].sources[i], g->pkg[pi].path); @@ -1536,7 +1617,7 @@ static void workdir_stamp_text(char *buf, size_t bufsz, int is_test, int emit_asm) { snprintf(buf, bufsz, "ww workdir fmt %d mode %s asm %d\n", - is_test ? 7 : 6, is_test ? "test" : "build", emit_asm); + is_test ? 8 : 7, is_test ? "test" : "build", emit_asm); } /* A stale global builder identity invalidates every committed unit voucher in @@ -1574,8 +1655,8 @@ invalidate_workdir_units(const char *scratch) * package universe, compile the dependency-first union once, then link each * root from its own complete reachable archive closure. The dependency-first * producer loop (one `w6c -c -I` per package, - * each DEP `.o` wrapped in its own deterministic `.a`), then a - * reverse-topo `w6l` of each root `.o` + dep `.a` set + libwwrt.a. Side + * each package `.o` wrapped in its own deterministic `.a`), then a + * reverse-topo `w6l` of each root `.a` + reachable `.a` set + libwwrt.a. Side * files land in a cold `.sepwork` dir, or under the persistent * `-w` workdir with content-identity package reuse. */ static int @@ -1696,6 +1777,8 @@ build_one_sep_impl(const char *src, int entry_is_dir, if (graphout) *graphout = g; const char *rootpath = package_only && root_identity ? root_identity : ""; + int support_for[SEP_MAXPRODUCT]; + for (int i = 0; i < nproducts; i++) support_for[i] = -1; for (int i = 0; i < nproducts; i++) { const char *entry = products[i].dir != NULL ? products[i].dir : src; @@ -1720,11 +1803,15 @@ build_one_sep_impl(const char *src, int entry_is_dir, entry_is_dir, products[i].variant, products[i].test_package, SEP_ROLE_NORMAL, products[i].artifact, 1); if (products[i].root < 0) return 1; + products[i].variant_root = products[i].root; + if (!is_test && !package_only) + g->pkg[products[i].root].link_entry = 1; } const char *test_support_module = "test"; /* -T generates a dispatcher whose support qualifier is selected by the - * command. Represent that compiler-generated requirement as a direct root - * edge. It normally coalesces with an explicit toolchain `import test`; + * command. Represent that compiler-generated requirement as a direct edge + * of the generated-main action. It normally coalesces with an explicit + * toolchain `import test`; * when user source occupies that identity, the reserved graph alias keeps * it distinct. The linker receives the same support archive closure. */ if (is_test) { @@ -1770,8 +1857,10 @@ build_one_sep_impl(const char *src, int entry_is_dir, * colocated production node, which is also its support dep. */ if (root_is_support && strcmp(test_support_module, "test") == 0 - && products[i].variant != SEP_VARIANT_EXTERNAL) + && products[i].variant != SEP_VARIANT_EXTERNAL) { + support_for[i] = root; continue; + } int ti; if (strcmp(test_support_module, SEP_TEST_SUPPORT_MODULE) == 0) @@ -1782,19 +1871,27 @@ build_one_sep_impl(const char *src, int entry_is_dir, tdir); if (ti < 0) return 1; g->pkg[ti].test_support = 1; - int seen = 0; - for (int k = 0; k < g->pkg[root].ndeps; k++) - if (g->pkg[root].deps[k] == ti) { - seen = 1; break; - } - if (!seen && g->pkg[root].ndeps < SEP_MAXPKG) - g->pkg[root].deps[g->pkg[root].ndeps++] = ti; + support_for[i] = ti; } free(tc); } } for (int i = 0; i < nproducts; i++) { - int root = products[i].root; + int root = products[i].variant_root; + /* Raw single-file test fixtures are the one retained non-directory + * exception: keep compiler-owned test-main synthesis in that action. + * Its support export is still an exact direct input. */ + if (is_test && !entry_is_dir) { + int support = support_for[i]; + if (support >= 0 && support != root) { + int seen = 0; + for (int k = 0; k < g->pkg[root].ndeps; k++) + if (g->pkg[root].deps[k] == support) seen = 1; + if (!seen) + g->pkg[root].deps[g->pkg[root].ndeps++] = support; + } + g->pkg[root].link_entry = 1; + } if (sep_load_pkg(g, root, products[i].context) < 0) { g->pkg[root].failed = 1; continue; @@ -1808,6 +1905,22 @@ build_one_sep_impl(const char *src, int entry_is_dir, g->pkg[root].failed = 1; } } + if (is_test && entry_is_dir) { + for (int i = 0; i < nproducts; i++) { + int variant = products[i].variant_root; + int support = support_for[i]; + if (support >= 0 && support != variant + && sep_load_pkg(g, support, products[i].context) < 0) + g->pkg[variant].failed = 1; + int mainpkg = sep_add_generated_main(g, &products[i], i, + support); + if (mainpkg < 0) return 1; + if (g->pkg[variant].failed + || (support >= 0 && g->pkg[support].failed)) + g->pkg[mainpkg].failed = 1; + products[i].root = mainpkg; + } + } if (sep_validate_artifact_paths(g, scratch) < 0) return 1; int root_package = package_only; @@ -1830,8 +1943,7 @@ build_one_sep_impl(const char *src, int entry_is_dir, for (int pi = 0; pi < g->n; pi++) g->pkg[pi].color = 0; int ignored = 0; if (sep_topo_visit(g, root, order, &ignored, stack, 0) < 0 - || sep_validate_module_closure(g, order, ignored, - root_package) < 0) + || sep_validate_module_closure(g, order, ignored, 1) < 0) g->pkg[root].failed = 1; } for (int pi = 0; pi < g->n; pi++) g->pkg[pi].color = 0; @@ -1843,11 +1955,6 @@ build_one_sep_impl(const char *src, int entry_is_dir, } } free(stack); - if (!root_package) - for (int i = 0; i < nproducts; i++) - if (g->pkg[products[i].root].path[0] != '\0') - g->pkg[products[i].root].path[0] = '\0'; - int any_failed = 0; for (int i = 0; i < nproducts; i++) if (g->pkg[products[i].root].failed) any_failed = 1; @@ -1882,8 +1989,6 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *cs = warm ? asmnew : asmf; const char *co = warm ? objnew : obj; const char *ca = warm ? anew : apath; - int needs_export = !g->pkg[pi].root || root_package; - int needs_archive = !g->pkg[pi].root || root_package; if (sep_compose_unit(g, pi, cu) < 0) { g->pkg[pi].failed = 1; any_failed = 1; @@ -1896,9 +2001,9 @@ build_one_sep_impl(const char *src, int entry_is_dir, if (warm && !stale_all && !deps_changed && file_equal(unitnew, unitf) && file_is_reg(asmf) - && (!needs_export || file_is_reg(wwi)) + && file_is_reg(wwi) && (emit_asm || (file_size_nonzero(obj) - && (!needs_archive || file_size_nonzero(apath))))) { + && file_size_nonzero(apath)))) { if (unlink(unitnew) != 0) { fprintf(stderr, "ww: cannot remove %s\n", unitnew); @@ -1907,13 +2012,6 @@ build_one_sep_impl(const char *src, int entry_is_dir, } continue; } - /* BUG-1 (#69): -I is purely the root's UNUSED - * `.wwi` output path, but it triggers wwi_emit → - * check_exported_type on the root. A terminal binary's - * root legitimately has `export fn` over an unexported - * LOCAL type (the root is never imported), which the - * export-check rejects. Skip -I for the root; its `.wwi` - * is never consumed. */ size_t cargvcap = (size_t)(12 + 3 * g->pkg[pi].ndeps); char **cargv = calloc(cargvcap, sizeof *cargv); char (*importfiles)[SEP_ARTIFACT_MAX] = NULL; @@ -1930,16 +2028,21 @@ build_one_sep_impl(const char *src, int entry_is_dir, } int cpos = 0; cargv[cpos++] = "w6c"; - if (!needs_export && is_test && g->pkg[pi].root) { - /* #79: the root carries -T under `ww test` - * so w6c synthesizes the test main. Deps never - * get -T. */ + if (g->pkg[pi].generated_main + || (is_test && g->pkg[pi].root && !g->pkg[pi].is_dir)) { cargv[cpos++] = "-T"; + cargv[cpos++] = "--entry"; cargv[cpos++] = "--test-support-module"; cargv[cpos++] = (char *)test_support_module; - } else if (needs_export && g->pkg[pi].test_support) { - cargv[cpos++] = "--test-support-module"; - cargv[cpos++] = (char *)test_support_module; + } else { + if (is_test && g->pkg[pi].root) + cargv[cpos++] = "--test-package"; + if (g->pkg[pi].link_entry) + cargv[cpos++] = "--entry"; + if (g->pkg[pi].test_support) { + cargv[cpos++] = "--test-support-module"; + cargv[cpos++] = (char *)test_support_module; + } } cargv[cpos++] = "-c"; for (int k = 0; k < g->pkg[pi].ndeps; k++) { @@ -1950,10 +2053,8 @@ build_one_sep_impl(const char *src, int entry_is_dir, cargv[cpos++] = g->pkg[dj].path; cargv[cpos++] = importfiles[k]; } - if (needs_export) { - cargv[cpos++] = "-I"; - cargv[cpos++] = (char *)cw; - } + cargv[cpos++] = "-I"; + cargv[cpos++] = (char *)cw; cargv[cpos++] = "-o"; cargv[cpos++] = (char *)cs; cargv[cpos++] = (char *)cu; @@ -1968,9 +2069,8 @@ build_one_sep_impl(const char *src, int entry_is_dir, any_failed = 1; continue; } - if (needs_export) - g->pkg[pi].export_changed = !warm - || !file_equal(wwinew, wwi); + g->pkg[pi].export_changed = !warm + || !file_equal(wwinew, wwi); if (!emit_asm) { char *aargv[] = {"w6a", "-o", (char *)co, (char *)cs, NULL}; @@ -1982,12 +2082,9 @@ build_one_sep_impl(const char *src, int entry_is_dir, continue; } } - /* wrap each DEP package's `.o` in its own deterministic `.a` - * (5a). The ROOT stays a positional `.o` (force-loaded — it's - * the build target, always fully linked), so `main` is defined - * before any archive is processed. The link consumes `.o`/`.a`, - * never `.wwi`. */ - if (!emit_asm && needs_archive) { + /* Every package action, including executable and generated-test roots, + * produces the existing deterministic single-member archive. */ + if (!emit_asm) { if (archive_o(co, ca) != 0) { fprintf(stderr, "ww: archive failed for %s\n", g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)"); @@ -1999,11 +2096,10 @@ build_one_sep_impl(const char *src, int entry_is_dir, /* Commit order: artifacts before the unit that vouches for * them, unit strictly last. */ if (warm) { - if ((needs_export && rename(wwinew, wwi) != 0) + if (rename(wwinew, wwi) != 0 || rename(asmnew, asmf) != 0 || (!emit_asm && rename(objnew, obj) != 0) - || (!emit_asm && needs_archive - && rename(anew, apath) != 0) + || (!emit_asm && rename(anew, apath) != 0) || rename(unitnew, unitf) != 0) { fprintf(stderr, "ww: cannot commit %s\n", g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)"); @@ -2066,10 +2162,10 @@ build_one_sep_impl(const char *src, int entry_is_dir, } free(order); - /* Each product gets its own reverse-topological closure: root `.o` first, - * then every transitively reachable dependency `.a`, then libwwrt.a. A - * same-test root already contains its production sources, so its colocated - * production archive is omitted without dropping that node's dependencies. */ + /* Each product gets its own reverse-topological archive closure: root `.a` + * first, then every transitively reachable package `.a`, then libwwrt.a. An + * internal test variant already contains production sources, so its + * colocated production archive is omitted without dropping dependencies. */ char rtpaths[2][1024]; int nrt = 1; snprintf(rtpaths[0], sizeof rtpaths[0], "%s/libwwrt.a", libdir); @@ -2084,6 +2180,7 @@ build_one_sep_impl(const char *src, int entry_is_dir, int nlibs = linkflags ? linkflags->nlibs : 0; for (int i = 0; i < nproducts; i++) { int root = products[i].root; + int variant_root = products[i].variant_root; if (g->pkg[root].failed) { any_failed = 1; continue; } for (int pi = 0; pi < g->n; pi++) g->pkg[pi].color = 0; int *linkorder = calloc((size_t)g->n, sizeof *linkorder); @@ -2110,14 +2207,15 @@ build_one_sep_impl(const char *src, int entry_is_dir, largv[pos++] = (char *)products[i].out; for (int oi = nlink - 1; oi >= 0; oi--) { int pi = linkorder[oi]; - if (g->pkg[root].variant == SEP_VARIANT_SAME_TEST - && pi != root + if (variant_root >= 0 + && g->pkg[variant_root].variant == SEP_VARIANT_SAME_TEST + && pi != variant_root && g->pkg[pi].variant == SEP_VARIANT_PRODUCTION && g->pkg[pi].role != SEP_ROLE_TEST_SUPPORT - && strcmp(g->pkg[pi].canon, g->pkg[root].canon) == 0) + && strcmp(g->pkg[pi].canon, + g->pkg[variant_root].canon) == 0) continue; - sep_fname(g, pi, scratch, - pi == root ? ".o" : ".a", linkpaths[npath], + sep_fname(g, pi, scratch, ".a", linkpaths[npath], sizeof linkpaths[npath]); largv[pos++] = linkpaths[npath++]; } @@ -2174,7 +2272,10 @@ build_one_sep(const char *src, int entry_is_dir, const char *root_identity, .artifact = {0}, .variant = root_variant, .root = -1, + .variant_root = -1, }; + if (!package_only) + snprintf(product.artifact, sizeof product.artifact, "__root"); int r = build_one_sep_impl(src, entry_is_dir, root_identity, out, objstem, extra_includes, linkflags, package_only, is_test, &product, 1, emit_asm, workdir, scratch, @@ -2612,6 +2713,7 @@ do_test(int argc, char **argv) products[nproducts].artifact[0] = '\0'; products[nproducts].variant = variant; products[nproducts].root = -1; + products[nproducts].variant_root = -1; nproducts++; } else if (strcmp(argv[i], "-S") == 0) { emit_asm = 1; diff --git a/docs/build-system.md b/docs/build-system.md index ffe291df..fdd3c381 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -2810,15 +2810,16 @@ the final component of its import path; two logical identities for one physical directory are rejected rather than compiled twice. Packages compile serially in dependency-first postorder. The compiler emits the -existing deterministic `.wwi` interface for every importable package. Its -primary section contains that package's byte-sorted direct imports and exported -declarations. The compiler then appends byte-sorted, origin-tagged sections for -only the exported foreign type and constant facts recursively reachable from -the primary public signatures. This makes each direct dependency interface -self-contained for the public type information its consumers need while -retaining the deeper declarations' original package identity. Checked fixed -array dimensions are emitted as numeric type facts, so a public layout never -requires exposing the private constant spelling that produced its length. +existing deterministic `.wwi` interface for every directory-package action, +including an executable root. Its primary section contains that package's +byte-sorted direct imports and exported declarations. The compiler then appends +byte-sorted, origin-tagged sections for only the foreign type and constant facts +recursively reachable from the primary public signatures. Reachable owner-local +private nominal types are carried without `export`: they make the export +self-contained for type checking, but qualified source lookup still rejects +their names. Checked fixed array dimensions are emitted as numeric type facts, +so a public layout never requires exposing the private constant spelling that +produced its length. A package compilation unit contains only that package's own byte-sorted sources and deterministic `//ww:module-reset` separators. Each **direct** import is a @@ -2835,19 +2836,25 @@ source-like `.wwi` syntax remains a transitional export encoding pending the binary `.wwe` format described above, but the separate direct-input ownership boundary is live in production Cstage and WWstage compilers and drivers. -An ordinary root is linked with the full reachable object closure into the -requested executable (legacy WW programs may use a package name other than -`main`). `ww build -p -o lib.a DIR` explicitly requests a non-main package -product: it emits a deterministic archive at `lib.a` and its compiler interface -at `lib.a.wwi`, without invoking the linker. A logical target retains its full +An ordinary executable directory root is one normal package action. Its +declared package identity tags its owner-only unit; it receives only direct +exports, emits `.wwi`, `.o`, and a deterministic `.a`, and is compiled exactly +once. The narrow compiler `--entry` flag controls only bare `main` codegen and is +independent of export production. The linker receives that root archive first, +then the complete reachable package-archive closure and runtime archive; it +never receives `.wwi`. The linkers seed `main` before archive selection, so the +existing WWAR member protocol needs no special root object or format change. + +`ww build -p -o lib.a DIR` explicitly requests a non-main package product: it +emits a deterministic archive at `lib.a` and its compiler interface at +`lib.a.wwi`, without invoking the linker. A logical target retains its full identity (`ww build -p -I ROOT -o bar.a foo.bar` emits `foo.bar.*` symbols), while a literal directory uses its declared leaf package. Package output requires a directory and `-p` cannot be combined with assembly-only `-S`. Two cold builds with identical inputs are required to produce byte-identical requested products. Compiler intrinsics keep their package-mode runtime ABI independent of transitive source interfaces (for example, `alloc` lowers to the -runtime allocator without requiring an `rt.wwi` compiler input), while the -linker still receives every reachable package archive plus the runtime archive. +runtime allocator without requiring an `rt.wwi` compiler input). ### 11.7 Implemented directory package-test slice @@ -2868,11 +2875,14 @@ semantic selections are only the directory and selected variant/package identities; it also carries output destinations, coordinator-private completion paths, import search roots, and the optional command-scoped work-directory policy. The command owns source selection, package loading, compiler inputs, -archive construction, and linking for every non-importable root: +archive construction, generated-main construction, and linking for every +package variant: -- `same-test` selects the byte-sorted production files followed by the - byte-sorted matching `package p` test files. They form one compiler unit, so - tests can use private production declarations. +- The ordinary production action selects the directory's byte-sorted + non-test files and is reused wherever that canonical package is imported. +- `same-test` is a distinct internal production-plus-test action. It selects + the byte-sorted production files followed by the byte-sorted matching + `package p` test files, so tests can use private production declarations. - `external-test` selects only matching `package p_test` files. Its `import p` is a direct edge to the canonical production action for that directory. That action compiles with module qualifier `p`, selects every production @@ -2882,17 +2892,23 @@ archive construction, and linking for every non-importable root: owning root, such as `__ww-test-001-external-production`; a normal import of the same `p` and canonical directory reuses that action rather than creating a second compilation. +- Each selected internal or external variant gets a distinct generated-main + package action. Its generated owner-only unit declares `package main` and + imports exactly the selected variant and test-support package. It consumes + exactly those direct `.wwi` artifacts, emits its own `.wwi/.o/.a`, and alone + receives compiler `-T --entry`. -The command loads all roots into one command-scoped package universe. Each root -retains an injective artifact key derived from its deterministic request ordinal -and variant, such as `__ww-test-000-same` or -`__ww-test-003-external`. Hyphens make that namespace illegal as a WW import -identity. Imports of the same canonical production directory intern to one -production node across every selected test directory. A deterministic -dependency-first traversal of the complete union therefore invokes the -compiler and archiver once for every reachable canonical production package, -even when many directory products need it. Each root is still compiled once -with its own selected sources and linked separately. The shared plan is +The command loads all variants into one command-scoped package universe. +Directory variants retain injective artifact keys such as +`__ww-test-000-same` or `__ww-test-003-external`; their generated mains use +matching `__ww-test-NNN-main` keys and distinct internal package identities. +Hyphens keep artifact keys illegal as WW import identities. Repeated requests +for the same canonical directory and variant reuse one compile action, and +imports of the same canonical production directory intern to one production +action across every selected test product. A deterministic dependency-first +traversal of the complete union therefore invokes the compiler and archiver +once per package variant, even when many products share it. Generated-main +actions remain product-specific and are linked separately. The shared plan is deliberately package-test-specific: it is not a generalized scheduler, action schema, cache, or protocol. @@ -2914,26 +2930,30 @@ missing product as a build failure. The single union build and completed test products share the coordinator's existing `-j` process bound; captured output is still emitted only in byte-sorted directory/package order. -Every non-root dependency is always a production variant, so dependency +Every source-imported dependency is a production variant, so dependency `*_test.ww` files never enter the graph. Imports that occur only in selected -test files add edges only to that test root. Each compiler unit contains only -the variant's owned source set, while its invocation receives only the -byte-sorted direct dependency `.wwi` artifacts as separate inputs. The final -test link still receives the root object and the complete reverse-topological -archive closure. The compiler-generated `-T` dispatcher owns the implicit -direct test-runtime support edge and remains embedded in each independently -compiled test root; it is the narrow test-main variant, not a -coordinator-generated graph package. The command-scoped plan compiles the -common runtime production package once for the complete test request. The -command resolves that edge from the selected toolchain source tree, not the -user search path; the support package's own imports are also loaded in that -toolchain context. Normally its graph +test files add edges only to that internal or external variant. Each compiler +unit contains only its action's owned source set, while its invocation receives +only the byte-sorted direct dependency `.wwi` artifacts as separate inputs. +Variant compiles receive `--test-package`, which validates and retains private +`@test` declarations as compiler-only export metadata without synthesizing an +entry point. The distinct generated-main action consumes that metadata from its +direct variant export and synthesizes the dispatcher with `-T`. + +The generated-main action, rather than the tested variant, owns the implicit +direct test-runtime support edge. The command-scoped plan compiles the common +support production package once for the complete test request. The command +resolves that edge from the selected toolchain source tree, not the user search +path; the support package's own imports are also loaded in that toolchain +context. Normally its graph qualifier is `test`, so an explicit source `import test` coalesces with the same canonical package. When a real user package occupies that identity, the command presents the runtime edge to the compiler under the reserved `__wwtest` qualifier. This keeps a production package named `test` available to external -tests while preserving raw `w6c -T` compatibility, whose default unresolved -qualifier remains `test`. +tests. Explicit raw single-file `ww test FILE` fixtures retain the narrow fused +`-T` compatibility path because an anonymous multi-package raw unit is not a +canonical directory package; that path still consumes support as a direct +export and emits a root `.wwi/.a`. The reserved support action and an ordinary source-imported toolchain `test` action may coexist in the command universe because their compiler qualifiers @@ -2947,24 +2967,25 @@ only narrow action-role exceptions: each product closure is checked to contain at most one importable action for a compiler module qualifier, so unrelated roles can never introduce duplicate linked package symbols. -The selected test roots and a production variant reached by their imports or -test-runtime closure are the only sanctioned graph nodes that may share a -physical directory. This also lets a toolchain package's own tests coexist with -the production variant required by the test runtime. A same-package root -already defines those production symbols, so the production archive is omitted -from that product's final link while the production node's dependency archives -remain in its closure. A reserved compiler-support action at that same physical -directory is still retained. The external product includes the production -archive. -Variant-only archives are never linked into the other product. All ordinary -logical and physical package-identity collision checks remain unchanged. -The Cstage linker, like the WWstage linker, passes the root object, every -reachable archive, the runtime, and explicit `-L`/`-l` values through a -structured argument vector; no fixed flattened command buffer can truncate a -large closure. WWstage emits joined `-Ldir` and `-lname` arguments accepted by -its native linker, while Cstage preserves the equivalent split forms. Generated -artifact paths are bounds-checked before any unit is opened, so distinct root -keys cannot alias by truncation. +The selected internal/external variants and a production action reached by +their imports or test-runtime closure are the sanctioned graph nodes that may +share a physical directory. This also lets a toolchain package's own tests +coexist with the production action required by the test runtime. An internal +variant already defines those production symbols, so the colocated production +archive is omitted from that product's final link while the production action's +dependency archives remain in the closure. The external product includes the +production archive. Variant-only archives are never linked into another +product. All ordinary logical and physical package-identity collision checks +remain unchanged. + +Both stage linkers receive the generated-main archive first, followed by the +complete reverse-topological reachable package-archive closure, runtime, and +explicit `-L`/`-l` values through a structured argument vector. No `.wwi` or +special root `.o` appears in linker argv, and no fixed flattened command buffer +can truncate a large closure. WWstage emits joined `-Ldir` and `-lname` +arguments accepted by its native linker, while Cstage preserves the equivalent +split forms. Generated artifact paths are bounds-checked before any unit is +opened, so distinct root keys cannot alias by truncation. Recursive discovery groups by physical directory before sorting filenames, and one stable escaped request key names the persistent command work directory. Every `*_test.ww` package variant is built even when a file only @@ -3021,7 +3042,7 @@ package, compiler actions receive direct dependency exports as individual arguments beside an owner-only `.unit.ww`, and links receive the complete per-root `.a` closure. Repository-native coverage wraps all three real stage tools at executable paths containing spaces, records every argument boundary, -inspects `.unit.ww`, `.wwi`, `.a`, and root object placement, runs the published +inspects `.unit.ww`, `.wwi`, `.a`, and root archive placement, runs the published binary, compares repeated Cstage/WWstage artifacts and traces, and removes one direct export at compiler entry to compare package-attributed diagnostics. @@ -3198,22 +3219,26 @@ schema or identity record. The exact-argv regression uses the real diamond `base -> {left,right} -> root`. It proves one compile per node; no input for `base`; only `base.wwi` for each middle node; only sorted `left.wwi` and -`right.wwi` for `root`; exact owner-only unit bytes; the complete three-archive -link closure; no link-time `.wwi`; exit status 42; and byte-identical units, -exports, archives, executables, and tool argument vectors across two clean +`right.wwi` for `root`; exact owner-only unit bytes; the complete four-package +link closure including the root archive; no link-time `.wwi`; exit status 42; +and byte-identical units, exports, archives, executables, and tool argument +vectors across two clean Cstage builds and two clean WWstage builds. The existing directory-package -variant regression checks exact direct inputs for internal and external -generated roots and the external-production action; both stages also compile -and run ordinary production and support actions with owner-only units and -byte-identical artifacts. +variant regression checks separate production, internal, external, and +generated-main actions, exact generated-main direct variant/support exports, +the external-production action, and archive-only link closures. Both stages +compile and run those actions with owner-only units and byte-identical +artifacts. -The pinned Go 1.26.5 implementation supplies the design boundary: +The pinned official Go 1.26.5 tag (commit +`c19862e5f8415b4f24b189d065ed739517c548ba`) supplies the design boundary: - Directory identity starts with `ImportDir`'s named directory ([`go/build/build.go`, lines 521–524](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/go/build/build.go#521)); directory reads are name-sorted ([lines 108–111](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/go/build/build.go#108)), lookup selects directory candidates ([lines 725–809](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/go/build/build.go#725)), and the selected directory is enumerated - ([lines 859–900](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/go/build/build.go#859)). + ([lines 859–900](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/go/build/build.go#859)). The sorted iteration filters files and appends each source to exactly one ordinary/internal/external-test bucket + ([lines 895–1036](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/go/build/build.go#895)). - The loader records direct imports ([`load/pkg.go`, lines 220–225](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/go/internal/load/pkg.go#220)), guarantees repeated cache loads return the same package pointer ([lines 633–636](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/go/internal/load/pkg.go#633)), keys reuse by canonical import path @@ -3221,22 +3246,26 @@ The pinned Go 1.26.5 implementation supplies the design boundary: ([lines 2776–2795](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/go/internal/load/pkg.go#2776)). - Build actions are interned by mode and package identity ([`work/action.go`, lines 437–447](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/go/internal/work/action.go#437)); compilation depends only on direct imports - ([lines 628–659](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/go/internal/work/action.go#628)), whereas link actions expand transitive dependencies + ([lines 628–659](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/go/internal/work/action.go#628)), and even package `main` receives that normal interned archive-producing compile action + ([lines 641–647](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/go/internal/work/action.go#641)). A link action places that main compile action at its first dependency and expands transitive dependencies ([lines 918–957](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/go/internal/work/action.go#918), [lines 1034–1068](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/go/internal/work/action.go#1034)). The executor derives compiler import inputs from those direct actions ([`work/exec.go`, lines 864–884](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/go/internal/work/exec.go#864)), passes them separately from source files - ([lines 928–930](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/go/internal/work/exec.go#928)), and separately emits the expanded linker closure - ([lines 1592–1647](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/go/internal/work/exec.go#1592)). + ([lines 928–930](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/go/internal/work/exec.go#928)), packs remaining objects into the package archive and records it as the built result + ([lines 1017–1033](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/go/internal/work/exec.go#1017)), then supplies the root archive and all expanded package inputs to linking + ([lines 1592–1624](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/go/internal/work/exec.go#1592), + [lines 1635–1647](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/go/internal/work/exec.go#1635)). - Tests preserve four package roles ([`load/test.go`, lines 85–102](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/go/internal/load/test.go#85)): internal production-plus-test ([lines 175–225](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/go/internal/load/test.go#175)), external test ([lines 228–265](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/go/internal/load/test.go#228)), and generated main - ([lines 272–293](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/go/internal/load/test.go#272)), with their distinct imports attached at + ([lines 272–293](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/go/internal/load/test.go#272)). Its support imports are loaded for that generated package + ([lines 307–332](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/go/internal/load/test.go#307)), with the selected internal/external variants attached as distinct direct imports at [lines 351–373](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/go/internal/load/test.go#351). - Unified export writing re-links, re-exports, and prunes facts ([`noder/unified.go`, lines 152–165](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/compile/internal/noder/unified.go#152)), finalizes self-contained data - ([lines 463–470](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/compile/internal/noder/unified.go#463)), and sorts declarations and bodies before serialization - ([lines 514–570](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/compile/internal/noder/unified.go#514)). Compiler import lookup opens the separately mapped artifact + ([lines 463–470](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/compile/internal/noder/unified.go#463)), selects export roots, and sorts declaration and body indices before serialization + ([lines 495–570](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/compile/internal/noder/unified.go#495)). Compiler import lookup opens the separately mapped artifact ([`noder/import.go`, lines 61–101](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/compile/internal/noder/import.go#61)) and decodes its exports independently of source parsing ([lines 170–225](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/compile/internal/noder/import.go#170)); `ReadPackage` consumes that package decoder ([`importer/ureader.go`, lines 28–62](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/compile/internal/importer/ureader.go#28)). diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index a1b6e5b5..925a3b89 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -447,9 +447,11 @@ fn dirfileattest(dirpath: *u8, name: *u8) i32 = { def SEP_VARIANT_PRODUCTION: i32 = 0; def SEP_VARIANT_SAME_TEST: i32 = 1; def SEP_VARIANT_EXTERNAL: i32 = 2; +def SEP_VARIANT_TEST_MAIN: i32 = 3; def SEP_ROLE_NORMAL: i32 = 0; def SEP_ROLE_EXTERNAL_PRODUCTION: i32 = 1; def SEP_ROLE_TEST_SUPPORT: i32 = 2; +def SEP_ROLE_GENERATED_MAIN: i32 = 3; def SEP_TEST_SUPPORT_MODULE: str = "__wwtest"; def SEP_MAXPRODUCT: i32 = 256; def SEP_MAXCONTEXT: i32 = 257; @@ -757,7 +759,7 @@ type sepbind = struct { }; type seppkg = struct { - path: *u8, // dotted import path, NUL-term; root path[0]==0 + path: *u8, // canonical dotted package identity, NUL-term entry: *u8, // resolved package dir (or file, file root), NUL-term artifact: *u8, // non-importable product-root artifact key name: *u8, // validated declared name; directory packages only @@ -768,6 +770,8 @@ type seppkg = struct { variant: i32, role: i32, root: bool, + linkentry: bool, + generatedmain: bool, failed: bool, testsupport: bool, loaded: bool, @@ -802,6 +806,7 @@ type sepproduct = struct { variant: i32, context: i32, root: i32, + variantroot: i32, }; fn sepfindoraddvariant(g: *sepgraph, path: *u8, entry: *u8, @@ -819,7 +824,17 @@ fn sepfindoraddvariant(g: *sepgraph, path: *u8, entry: *u8, if (root && g.pkg[i].root) { if (!samelocation) { i += 1; continue; }; if (variant != g.pkg[i].variant) { i += 1; continue; }; - cerr("ww: duplicate package-test root "); + let sametest: bool = testpackage == nil + && g.pkg[i].testpackage == nil; + if (testpackage != nil && g.pkg[i].testpackage != nil) { + sametest = cstreq(testpackage, g.pkg[i].testpackage); + }; + if (g.pkg[i].role == role && cstreq(g.pkg[i].path, path) + && sametest) { + // One directory variant is one compile action across products. + return i; + }; + cerr("ww: incompatible package-test roots "); cerr(pathstr(entry)); cerr("\n"); return -1; }; @@ -923,6 +938,8 @@ fn sepfindoraddvariant(g: *sepgraph, path: *u8, entry: *u8, g.pkg[g.n].variant = variant; g.pkg[g.n].role = role; g.pkg[g.n].root = root; + g.pkg[g.n].linkentry = false; + g.pkg[g.n].generatedmain = false; g.pkg[g.n].failed = false; g.pkg[g.n].testsupport = false; g.pkg[g.n].loaded = false; @@ -1356,6 +1373,79 @@ fn sepdepcmp(g: *sepgraph, a: i32, b: i32) i32 = { pathstr(g.pkg[b].artifact)): i32; }; +fn generatedmainpath(index: i32) *u8 = { + let buf: []u8 = alloc([], 64u64)!; + buf.len = 64; + let off: u64 = strinto(buf.ptr, 0u64, "__wwtestmain."); + buf[off] = (((index / 100) % 10) + 48): u8; off += 1u64; + buf[off] = (((index / 10) % 10) + 48): u8; off += 1u64; + buf[off] = ((index % 10) + 48): u8; off += 1u64; + off = strinto(buf.ptr, off, ".main"); + cstrseal(buf.ptr, off); + return buf.ptr; +}; + +// Materialize the generated test dispatcher as a normal package action. It +// owns a generated source unit and imports exactly its test variant/support. +fn sepaddgeneratedmain(g: *sepgraph, product: *sepproduct, ordinal: i32, + support: i32) i32 = { + if (g.n >= SEP_MAXPKG) { + cerr("ww: too many packages\n"); + return -1; + }; + let variant: i32 = product.variantroot; + if (variant < 0 || variant >= g.n) { return -1; }; + let p: *seppkg = &g.pkg[g.n]; + p.path = generatedmainpath(ordinal); + p.entry = g.pkg[variant].entry; + p.artifact = productartifact(ordinal, SEP_VARIANT_TEST_MAIN); + p.name = arenadupcstr("main\0".ptr, 4u64); + p.testpackage = nil; + p.sources = nil; + p.nsources = 0; + p.isdir = 0; + p.variant = SEP_VARIANT_TEST_MAIN; + p.role = SEP_ROLE_GENERATED_MAIN; + p.root = true; + p.linkentry = true; + p.generatedmain = true; + p.failed = false; + p.testsupport = false; + p.loaded = true; + p.exportchanged = false; + p.emitcontext = product.context; + let cslot: []u8 = alloc([], SEP_MAXCONTEXT: u64)!; + cslot.len = SEP_MAXCONTEXT; + p.contextstate = cslot; + p.contextstate[product.context] = 2u8; + let emptybindings: []sepbind; + p.bindings = emptybindings; + let dslot: []i32 = alloc([], SEP_MAXPKG: u64)!; + dslot.len = SEP_MAXPKG; + p.deps = dslot; + p.ndeps = 1; + p.deps[0] = variant; + if (support >= 0 && support != variant) { + p.deps[p.ndeps] = support; + p.ndeps += 1; + }; + let i: i32 = 1; + for (i < p.ndeps) { + let v: i32 = p.deps[i]; + let j: i32 = i; + for (j > 0 && sepdepcmp(g, p.deps[j - 1], v) > 0) { + p.deps[j] = p.deps[j - 1]; + j -= 1; + }; + p.deps[j] = v; + i += 1; + }; + p.color = 0; + let r: i32 = g.n; + g.n += 1; + return r; +}; + // Load pi once: a directory node takes ownership of its sorted production // paths, then every selected-root context verifies the same canonical import // bindings before the package is compiled once. @@ -1450,7 +1540,10 @@ fn seploadpkg(g: *sepgraph, pi: i32, context: i32) i32 = { g.pkg[pi].failed = true; return rc; }; - if (g.pkg[pi].root && g.pkg[pi].path[0u64] == 0u8 + // Directory roots acquire their canonical package identity. Explicit raw + // single-file compiler fixtures retain their anonymous multi-package reset. + if (g.pkg[pi].root && g.pkg[pi].isdir != 0 + && g.pkg[pi].path[0u64] == 0u8 && g.pkg[pi].name != nil) { g.pkg[pi].path = arenadupcstr(g.pkg[pi].name, cstrlen(g.pkg[pi].name)); @@ -1598,7 +1691,27 @@ fn sepcomposeunit(g: *sepgraph, pi: i32, unitf: *u8) i32 = { return -1; }; let bodyrc: i32 = 0; - if (g.pkg[pi].isdir != 0) { + if (g.pkg[pi].generatedmain) { + let head: str = "//ww:module-reset "; + let pkg: str = "\npackage main;\n"; + if (!sepwriteall(u, head.ptr, head.len: u64) + || !sepwriteall(u, g.pkg[pi].path, cstrlen(g.pkg[pi].path)) + || !sepwriteall(u, pkg.ptr, pkg.len: u64)) { + bodyrc = -1; + }; + let i: i32 = 0; + for (i < g.pkg[pi].ndeps && bodyrc == 0) { + let pre: str = "import "; + let end: str = ";\n"; + let dep: *u8 = g.pkg[g.pkg[pi].deps[i]].path; + if (!sepwriteall(u, pre.ptr, pre.len: u64) + || !sepwriteall(u, dep, cstrlen(dep)) + || !sepwriteall(u, end.ptr, end.len: u64)) { + bodyrc = -1; + }; + i += 1; + }; + } else { if (g.pkg[pi].isdir != 0) { let i: i32 = 0; for (i < g.pkg[pi].nsources && bodyrc == 0) { bodyrc = sepemitbody(u, g.pkg[pi].sources[i], g.pkg[pi].path); @@ -1606,7 +1719,7 @@ fn sepcomposeunit(g: *sepgraph, pi: i32, unitf: *u8) i32 = { }; } else { bodyrc = sepemitbody(u, g.pkg[pi].entry, g.pkg[pi].path); - }; + }; }; if (os.close(u) != 0) { cerr("ww: cannot close package unit\n"); return -1; @@ -1691,8 +1804,8 @@ fn archiveo(objpath: *u8, apath: *u8) i32 = { // buildonesep — discover deps, reverse-topo, // the dependency-first producer loop (one `w6c -c -I` per package, -// each dependency `.o` wrapped in its own deterministic per-package `.a`), then a -// reverse-topo `w6l` of the root `.o` + dependency `.a` set + libwwrt.a. +// each package `.o` wrapped in its own deterministic per-package `.a`), then a +// reverse-topo `w6l` of the root `.a` + reachable `.a` set + libwwrt.a. // Side files land in a cold `.sepwork` scratch dir. Twin of cstage // build_one_sep. @@ -1811,14 +1924,14 @@ fn copyfileatomic(src: *u8, dst: *u8) i32 = { fn workdirstamptext(istest: i32, emitasm: i32) str = { if (istest != 0) { if (emitasm != 0) { - return "ww workdir fmt 7 mode test asm 1\n"; + return "ww workdir fmt 8 mode test asm 1\n"; }; - return "ww workdir fmt 7 mode test asm 0\n"; + return "ww workdir fmt 8 mode test asm 0\n"; }; if (emitasm != 0) { - return "ww workdir fmt 6 mode build asm 1\n"; + return "ww workdir fmt 7 mode build asm 1\n"; }; - return "ww workdir fmt 6 mode build asm 0\n"; + return "ww workdir fmt 7 mode build asm 0\n"; }; fn stampmatches(path: *u8, want: str) bool = { @@ -2097,7 +2210,11 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, if (graphout != nil) { *graphout = g; }; let rootpath: *u8 = "\0".ptr; if (packageonly != 0 && rootidentity != nil) { rootpath = rootidentity; }; + let supportfor: []i32 = alloc([], nproducts: u64)!; + supportfor.len = nproducts; let producti: i32 = 0; + for (producti < nproducts) { supportfor[producti] = -1; producti += 1; }; + producti = 0; for (producti < nproducts) { let entry: *u8 = src; if (products[producti].dir != nil) { @@ -2113,12 +2230,16 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, products[producti].testpackage, SEP_ROLE_NORMAL, products[producti].artifact, true); if (products[producti].root < 0) { return 1; }; + products[producti].variantroot = products[producti].root; + if (istest == 0 && packageonly == 0) { + g.pkg[products[producti].root].linkentry = true; + }; producti += 1; }; let testsupportmodule: str = "test"; // -T generates a dispatcher whose support qualifier is selected by the - // command. Represent that compiler-generated requirement as a direct root - // edge. It normally coalesces with an explicit toolchain `import test`; + // command. Represent that requirement as a direct generated-main edge. It + // normally coalesces with an explicit toolchain `import test`; // when user source occupies that identity, the reserved graph alias keeps // it distinct. The linker receives the same support archive closure. if (istest != 0) { @@ -2168,6 +2289,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, if (rootissupport && syntax.streq(testsupportmodule, "test") && products[producti].variant != SEP_VARIANT_EXTERNAL) { + supportfor[producti] = root; producti += 1; continue; }; @@ -2181,25 +2303,32 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, }; if (ti < 0) { return 1; }; g.pkg[ti].testsupport = true; - let seen: bool = false; - let m: i32 = 0; - for (m < g.pkg[root].ndeps) { - if (g.pkg[root].deps[m] == ti) { seen = true; }; - m += 1; - }; - if (!seen) { - if (g.pkg[root].ndeps < SEP_MAXPKG) { - g.pkg[root].deps[g.pkg[root].ndeps] = ti; - g.pkg[root].ndeps += 1; - }; - }; + supportfor[producti] = ti; producti += 1; }; }; }; producti = 0; for (producti < nproducts) { - let root: i32 = products[producti].root; + let root: i32 = products[producti].variantroot; + // Raw single-file tests retain the explicit fixture exception: test-main + // synthesis stays in that action and support remains a direct export. + if (istest != 0 && entryisdir == 0) { + let support: i32 = supportfor[producti]; + if (support >= 0 && support != root) { + let seen: bool = false; + let sk: i32 = 0; + for (sk < g.pkg[root].ndeps) { + if (g.pkg[root].deps[sk] == support) { seen = true; }; + sk += 1; + }; + if (!seen) { + g.pkg[root].deps[g.pkg[root].ndeps] = support; + g.pkg[root].ndeps += 1; + }; + }; + g.pkg[root].linkentry = true; + }; if (seploadpkg(g, root, products[producti].context) < 0) { g.pkg[root].failed = true; producti += 1; @@ -2214,6 +2343,27 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, }; producti += 1; }; + if (istest != 0 && entryisdir != 0) { + producti = 0; + for (producti < nproducts) { + let variant: i32 = products[producti].variantroot; + let support: i32 = supportfor[producti]; + if (support >= 0 && support != variant) { + if (seploadpkg(g, support, products[producti].context) < 0) { + g.pkg[variant].failed = true; + }; + }; + let mainpkg: i32 = sepaddgeneratedmain(g, &products[producti], + producti, support); + if (mainpkg < 0) { return 1; }; + if (g.pkg[variant].failed + || (support >= 0 && g.pkg[support].failed)) { + g.pkg[mainpkg].failed = true; + }; + products[producti].root = mainpkg; + producti += 1; + }; + }; if (sepvalidateartifactpaths(g, scratch) < 0) { return 1; }; let rootpackage: bool = packageonly != 0; if (rootpackage && !g.pkg[products[0].root].failed @@ -2239,8 +2389,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, let ignored: i32 = 0; if (septopovisit(g, root, order, &ignored, stack, 0) < 0 - || sepvalidatemoduleclosure(g, order, ignored, - rootpackage) < 0) { + || sepvalidatemoduleclosure(g, order, ignored, true) < 0) { g.pkg[root].failed = true; }; }; @@ -2257,14 +2406,6 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, }; producti += 1; }; - if (!rootpackage) { - producti = 0; - for (producti < nproducts) { - g.pkg[products[producti].root].path = "\0".ptr; - producti += 1; - }; - }; - let anyfailed: bool = false; producti = 0; for (producti < nproducts) { @@ -2307,8 +2448,6 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, cu = unitnew; cw = wwinew; cs = asmnew; co = objnew; ca = anew; }; - let needsexport: bool = !g.pkg[pi].root || rootpackage; - let needsarchive: bool = !g.pkg[pi].root || rootpackage; if (sepcomposeunit(g, pi, cu) < 0) { g.pkg[pi].failed = true; anyfailed = true; @@ -2328,20 +2467,14 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, if (!staleall && !depschanged) { fresh = fileequal(unitnew, unitf); if (fresh) { fresh = fileisreg(asmf); }; - if (fresh) { - if (needsexport) { - fresh = fileisreg(wwi); - }; - }; + if (fresh) { fresh = fileisreg(wwi); }; if (fresh) { if (emitasm == 0) { fresh = filesizenonzero(objf); }; }; - if (fresh) { - if (emitasm == 0 && needsarchive) { - fresh = filesizenonzero(apath); - }; + if (fresh && emitasm == 0) { + fresh = filesizenonzero(apath); }; }; }; @@ -2355,31 +2488,34 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, continue; }; { - // BUG-1 (#69): -I is purely the root's UNUSED - // `.wwi` output path, but it triggers wwiemit -> - // checkexportedtype on the root. A terminal binary's - // root legitimately has `export fn` over an unexported - // LOCAL type (the root is never imported), which the - // export-check rejects. Build a shorter root argv - // without the -I/wwi pair; root's `.wwi` is unconsumed. - // #79: the root carries -T under `ww test` so w6c - // synthesizes the test main; deps never get -T. - let roott: bool = g.pkg[pi].root && (istest != 0); - let supportt: bool = g.pkg[pi].testsupport; + let rawtest: bool = (istest != 0) && g.pkg[pi].root + && g.pkg[pi].isdir == 0; + let gent: bool = g.pkg[pi].generatedmain || rawtest; + let testpkg: bool = g.pkg[pi].root && (istest != 0) && !gent; + let entry: bool = g.pkg[pi].linkentry; + let supportpkg: bool = g.pkg[pi].testsupport; let alen: u64 = 8u64; - if (!needsexport) { alen = 6u64; if (roott) { alen = 9u64; }; }; - if (supportt) { alen += 2u64; }; + if (gent) { alen += 4u64; } + else { + if (testpkg) { alen += 1u64; }; + if (entry) { alen += 1u64; }; + if (supportpkg) { alen += 2u64; }; + }; alen += (g.pkg[pi].ndeps: u64) * 3u64; let argv: []str = alloc([], alen)!; append(argv, "w6c"); - if (roott) { + if (gent) { append(argv, "-T"); + append(argv, "--entry"); append(argv, "--test-support-module"); append(argv, testsupportmodule); - }; - if (supportt) { - append(argv, "--test-support-module"); - append(argv, testsupportmodule); + } else { + if (testpkg) { append(argv, "--test-package"); }; + if (entry) { append(argv, "--entry"); }; + if (supportpkg) { + append(argv, "--test-support-module"); + append(argv, testsupportmodule); + }; }; append(argv, "-c"); let importk: i32 = 0; @@ -2390,10 +2526,8 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, append(argv, pathstr(sepfname(g, dj, scratch, ".wwi"))); importk += 1; }; - if (needsexport) { - append(argv, "-I"); - append(argv, pathstr(cw)); - }; + append(argv, "-I"); + append(argv, pathstr(cw)); append(argv, "-o"); append(argv, pathstr(cs)); append(argv, pathstr(cu)); @@ -2418,10 +2552,8 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, continue; }; }; - if (needsexport) { - if (!warm || !fileequal(wwinew, wwi)) { - g.pkg[pi].exportchanged = true; - }; + if (!warm || !fileequal(wwinew, wwi)) { + g.pkg[pi].exportchanged = true; }; if (emitasm == 0) { let argv: []str = alloc([], 4u64)!; @@ -2450,12 +2582,9 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, continue; }; }; - // Wrap each DEP package's `.o` in its own deterministic `.a` - // (5a). The ROOT stays a positional `.o` (force-loaded — it's - // the build target), so `main` is defined before any archive is - // processed. The link - // consumes `.o`/`.a`, never `.wwi`. - if (emitasm == 0 && needsarchive) { + // Every package action, including executable/generated roots, produces + // the existing deterministic single-member archive. + if (emitasm == 0) { if (archiveo(co, ca) != 0) { cerr("ww: archive failed\n"); g.pkg[pi].failed = true; @@ -2468,10 +2597,8 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, // them, unit strictly last. if (warm) { let bad: bool = false; - if (needsexport) { - if (os.rename(pathstr(wwinew), pathstr(wwi)) != 0) { - bad = true; - }; + if (os.rename(pathstr(wwinew), pathstr(wwi)) != 0) { + bad = true; }; if (!bad) { if (os.rename(pathstr(asmnew), pathstr(asmf)) != 0) { @@ -2486,7 +2613,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, }; }; if (!bad) { - if (emitasm == 0 && needsarchive) { + if (emitasm == 0) { if (os.rename(pathstr(anew), pathstr(apath)) != 0) { bad = true; }; @@ -2573,6 +2700,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, producti = 0; for (producti < nproducts) { let root: i32 = products[producti].root; + let variantroot: i32 = products[producti].variantroot; if (g.pkg[root].failed) { anyfailed = true; producti += 1; @@ -2598,18 +2726,17 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, let li: i32 = nlink - 1; for (li >= 0) { let pi: i32 = linkorder[li]; - if (g.pkg[root].variant == SEP_VARIANT_SAME_TEST - && pi != root + if (variantroot >= 0 + && g.pkg[variantroot].variant == SEP_VARIANT_SAME_TEST + && pi != variantroot && g.pkg[pi].variant == SEP_VARIANT_PRODUCTION && g.pkg[pi].role != SEP_ROLE_TEST_SUPPORT && os.samefile(pathstr(g.pkg[pi].entry), - pathstr(g.pkg[root].entry))) { + pathstr(g.pkg[variantroot].entry))) { li -= 1; continue; }; - let suf: str = ".a"; - if (pi == root) { suf = ".o"; }; - largv[pos] = sepfname(g, pi, scratch, suf); + largv[pos] = sepfname(g, pi, scratch, ".a"); pos += 1; li -= 1; }; @@ -2694,8 +2821,10 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, product.testpackage = testpackage; product.status = nil; product.artifact = nil; + if (packageonly == 0) { product.artifact = "__root\0".ptr; }; product.variant = rootvariant; product.root = -1; + product.variantroot = -1; let r: i32 = buildonesepimpl(selfdir, src, entryisdir, rootidentity, out, objstem, incs, lf, packageonly, istest, &product, 1, @@ -2755,9 +2884,11 @@ fn productartifact(index: i32, variant: i32) *u8 = { off = byteinto(buf.ptr, off, '-': u8); if (variant == SEP_VARIANT_SAME_TEST) { off = strinto(buf.ptr, off, "same"); + } else { if (variant == SEP_VARIANT_TEST_MAIN) { + off = strinto(buf.ptr, off, "main"); } else { off = strinto(buf.ptr, off, "external"); - }; + }; }; cstrseal(buf.ptr, off); return buf.ptr; }; @@ -3446,6 +3577,7 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { product.artifact = nil; product.variant = variant; product.root = -1; + product.variantroot = -1; append(products, product); i += 6; continue; diff --git a/test/misc/packedwwi_test.ww b/test/misc/packedwwi_test.ww index ca333d71..31e25218 100644 --- a/test/misc/packedwwi_test.ww +++ b/test/misc/packedwwi_test.ww @@ -14,9 +14,9 @@ package packedwwi_test; // type.ha:122-126). Both driver stages must build the -I tree and run // exit 91 (9*10+1; a dropped @packed gives 168). Strengthened over // the carrier: the re-emitted `struct @packed {` is also asserted in -// the retained sepwork pk2.wwi. A single-file two-package form -// produces NO .wwi (one __root unit), so the real -I tree is -// irreducible here. +// the retained sepwork pk2.wwi. A single-file two-package form produces +// only one raw `__root.wwi`, not an independently importable pk2 package, +// so the real directory-package -I tree is irreducible here. // // identityreject: assigning packed A to a structurally-identical // unpacked B — packed is type identity (harec types.c:621). STAGE- diff --git a/test/package/package_test.ww b/test/package/package_test.ww index 325c44dd..1d2cc090 100644 --- a/test/package/package_test.ww +++ b/test/package/package_test.ww @@ -786,6 +786,11 @@ fn workescape(s: str) str = { let referencesameunit: str = ""; let referenceexternalunit: str = ""; let referenceproductionunit: str = ""; + let packageactions: []str = ["__ww-test-000-same", + "__ww-test-000-main", "__ww-test-001-external", + "__ww-test-001-main"]; + let referenceactionexports: []str = ["", "", "", ""]; + let referenceactionarchives: []str = ["", "", "", ""]; let i: i32 = 0; for (i < drivers.len) { let av: []str = [driver(drivers[i]), "test", "-c", "-I", root, @@ -807,6 +812,10 @@ fn workescape(s: str) str = { "__ww-test-001-external.unit.ww")); let externalproduction: str = readfile(strings.concat(sharedwork, "__ww-test-001-external-production.unit.ww")); + let samemainunit: str = readfile(strings.concat(sharedwork, + "__ww-test-000-main.unit.ww")); + let externalmainunit: str = readfile(strings.concat(sharedwork, + "__ww-test-001-main.unit.ww")); assert(!os.exists(strings.concat(externalbin, ".sepwork"))); assert(!has(sameunit, "//ww:module ")); assert(os.exists(strings.concat(sharedwork, @@ -824,28 +833,43 @@ fn workescape(s: str) str = { assert(!has(externalproduction, "//ww:module ")); assert(!has(externalproduction, "SAME_TEST_SOURCE")); assert(!has(externalproduction, "EXTERNAL_TEST_SOURCE")); + assert(same(samemainunit, strings.concat( + "//ww:module-reset __wwtestmain.000.main\n", + "package main;\nimport pkg;\nimport test;\n"))); + assert(same(externalmainunit, strings.concat( + "//ww:module-reset __wwtestmain.001.main\n", + "package main;\nimport pkg_test;\nimport test;\n"))); + assert(!has(samemainunit, "PACKAGE_PRODUCTION")); + assert(!has(samemainunit, "SAME_TEST_SOURCE")); + assert(!has(externalmainunit, "EXTERNAL_TEST_SOURCE")); assert(!has(readfile(strings.concat(sharedwork, "api.unit.ww")), "TEST_DEPENDENCY_MUST_NOT_COMPILE")); let artifacts: []str = ["implementation", "api", "__ww-test-001-external-production", - "__same", "__external", "test"]; + "__same", "__external", "test", "__ww-test-000-same", + "__ww-test-000-main", "__ww-test-001-external", + "__ww-test-001-main"]; let ai: i32 = 0; for (ai < artifacts.len) { assert(os.exists(strings.concat(sharedwork, artifacts[ai], ".wwi"))); assert(os.exists(strings.concat(sharedwork, artifacts[ai], ".a"))); ai += 1; }; - assert(os.exists(strings.concat(sharedwork, "__ww-test-000-same.o"))); - assert(os.exists(strings.concat(sharedwork, - "__ww-test-001-external.o"))); - assert(!os.exists(strings.concat(sharedwork, - "__ww-test-000-same.wwi"))); - assert(!os.exists(strings.concat(sharedwork, - "__ww-test-001-external.wwi"))); - assert(!os.exists(strings.concat(sharedwork, - "__ww-test-000-same.a"))); - assert(!os.exists(strings.concat(sharedwork, - "__ww-test-001-external.a"))); + ai = 0; + for (ai < packageactions.len) { + let ex: str = readfile(strings.concat(sharedwork, + packageactions[ai], ".wwi")); + let ar: str = readfile(strings.concat(sharedwork, + packageactions[ai], ".a")); + if (i == 0) { + referenceactionexports[ai] = strings.dup(ex); + referenceactionarchives[ai] = strings.dup(ar); + } else { + assert(same(referenceactionexports[ai], ex)); + assert(same(referenceactionarchives[ai], ar)); + }; + ai += 1; + }; assert(os.exists(samebin)); assert(os.exists(externalbin)); if (i == 0) { @@ -862,7 +886,11 @@ fn workescape(s: str) str = { assert(occurrences(ctrace, "__ww-test-001-external.unit.ww") == 1); assert(occurrences(ctrace, - "-T --test-support-module") == 2); + "__ww-test-000-main.unit.ww") == 1); + assert(occurrences(ctrace, + "__ww-test-001-main.unit.ww") == 1); + assert(occurrences(ctrace, "-T --entry --test-support-module") == 2); + assert(occurrences(ctrace, "--test-package") == 2); let samecompile: str = linecontaining(ctrace, "__ww-test-000-same.unit.ww"); let externalcompile: str = linecontaining(ctrace, @@ -870,18 +898,36 @@ fn workescape(s: str) str = { let productioncompile: str = linecontaining(ctrace, "__ww-test-001-external-production.unit.ww"); assert(same(samecompile, strings.concat( - "-T --test-support-module test -c --import __same ", + "--test-package -c --import __same ", sharedwork, "__same.wwi --import api ", sharedwork, - "api.wwi --import test ", sharedwork, "test.wwi -o ", + "api.wwi -I ", sharedwork, "__ww-test-000-same.wwi -o ", sharedwork, "__ww-test-000-same.s ", sharedwork, "__ww-test-000-same.unit.ww"))); assert(same(externalcompile, strings.concat( - "-T --test-support-module test -c --import __external ", + "--test-package -c --import __external ", sharedwork, "__external.wwi --import pkg ", sharedwork, - "__ww-test-001-external-production.wwi --import test ", - sharedwork, "test.wwi -o ", sharedwork, + "__ww-test-001-external-production.wwi -I ", sharedwork, + "__ww-test-001-external.wwi -o ", sharedwork, "__ww-test-001-external.s ", sharedwork, "__ww-test-001-external.unit.ww"))); + let samemaincompile: str = linecontaining(ctrace, + "__ww-test-000-main.unit.ww"); + let externalmaincompile: str = linecontaining(ctrace, + "__ww-test-001-main.unit.ww"); + assert(same(samemaincompile, strings.concat( + "-T --entry --test-support-module test -c --import pkg ", + sharedwork, "__ww-test-000-same.wwi --import test ", + sharedwork, "test.wwi -I ", sharedwork, + "__ww-test-000-main.wwi -o ", sharedwork, + "__ww-test-000-main.s ", sharedwork, + "__ww-test-000-main.unit.ww"))); + assert(same(externalmaincompile, strings.concat( + "-T --entry --test-support-module test -c --import pkg_test ", + sharedwork, "__ww-test-001-external.wwi --import test ", + sharedwork, "test.wwi -I ", sharedwork, + "__ww-test-001-main.wwi -o ", sharedwork, + "__ww-test-001-main.s ", sharedwork, + "__ww-test-001-main.unit.ww"))); assert(same(productioncompile, strings.concat( "-c --import api ", sharedwork, "api.wwi -I ", sharedwork, "__ww-test-001-external-production.wwi -o ", sharedwork, @@ -897,12 +943,22 @@ fn workescape(s: str) str = { assert(has(samelink, strings.concat(sharedwork, "implementation.a"))); assert(has(samelink, strings.concat(sharedwork, "__same.a"))); + assert(has(samelink, strings.concat(sharedwork, + "__ww-test-000-main.a"))); + assert(has(samelink, strings.concat(sharedwork, + "__ww-test-000-same.a"))); assert(!has(samelink, strings.concat(sharedwork, "__ww-test-001-external-production.a"))); assert(!has(samelink, strings.concat(sharedwork, "__external.a"))); assert(has(externallink, strings.concat(sharedwork, "__ww-test-001-external-production.a"))); + assert(has(externallink, strings.concat(sharedwork, + "__ww-test-001-main.a"))); + assert(has(externallink, strings.concat(sharedwork, + "__ww-test-001-external.a"))); + assert(!has(samelink, ".wwi")); + assert(!has(externallink, ".wwi")); assert(has(externallink, strings.concat(sharedwork, "api.a"))); assert(has(externallink, strings.concat(sharedwork, "implementation.a"))); @@ -1176,6 +1232,8 @@ fn workescape(s: str) str = { let rootkeys: []str = ["__ww-test-000-same", "__ww-test-001-external", "__ww-test-002-same", "__ww-test-003-external"]; + let mainkeys: []str = ["__ww-test-000-main", "__ww-test-001-main", + "__ww-test-002-main", "__ww-test-003-main"]; let alphaartifact: str = "__ww-test-001-external-production"; let betaartifact: str = "__ww-test-003-external-production"; let expectedtests: []str = ["alpha_same_runs ... ok\n", @@ -1248,7 +1306,11 @@ fn workescape(s: str) str = { assert(!has(roots[3], "MULTIDIR_BETA_SAME")); let products: []str = ["leaf", "common", alphaartifact, betaartifact, "_alpha_same", "_alpha_external", "_beta_same", - "_beta_external", "test"]; + "_beta_external", "test", "__ww-test-000-same", + "__ww-test-001-external", "__ww-test-002-same", + "__ww-test-003-external", "__ww-test-000-main", + "__ww-test-001-main", "__ww-test-002-main", + "__ww-test-003-main"]; let pi: i32 = 0; for (pi < products.len) { assert(os.exists(strings.concat(sharedwork, products[pi], @@ -1260,8 +1322,11 @@ fn workescape(s: str) str = { let ri: i32 = 0; for (ri < rootkeys.len) { assert(os.exists(strings.concat(sharedwork, rootkeys[ri], ".o"))); - assert(!os.exists(strings.concat(sharedwork, rootkeys[ri], ".wwi"))); - assert(!os.exists(strings.concat(sharedwork, rootkeys[ri], ".a"))); + assert(os.exists(strings.concat(sharedwork, rootkeys[ri], ".wwi"))); + assert(os.exists(strings.concat(sharedwork, rootkeys[ri], ".a"))); + assert(os.exists(strings.concat(sharedwork, mainkeys[ri], ".unit.ww"))); + assert(os.exists(strings.concat(sharedwork, mainkeys[ri], ".wwi"))); + assert(os.exists(strings.concat(sharedwork, mainkeys[ri], ".a"))); assert(os.exists(bins[ri])); if (ri > 0) { assert(!os.exists(strings.concat(bins[ri], ".sepwork"))); @@ -1293,6 +1358,8 @@ fn workescape(s: str) str = { for (ri < rootkeys.len) { assert(occurrences(ctrace, strings.concat(rootkeys[ri], ".unit.ww")) == 1); + assert(occurrences(ctrace, strings.concat(mainkeys[ri], + ".unit.ww")) == 1); ri += 1; }; assert(pos(ctrace, "/leaf.unit.ww") @@ -1309,7 +1376,9 @@ fn workescape(s: str) str = { for (ri < bins.len) { let link: str = linecontaining(ltrace, strings.concat("-o ", bins[ri], " ")); - assert(has(link, strings.concat(sharedwork, rootkeys[ri], ".o"))); + assert(has(link, strings.concat(sharedwork, mainkeys[ri], ".a"))); + assert(has(link, strings.concat(sharedwork, rootkeys[ri], ".a"))); + assert(!has(link, ".wwi")); assert(has(link, strings.concat(sharedwork, "common.a"))); assert(has(link, strings.concat(sharedwork, "leaf.a"))); assert(has(link, strings.concat(sharedwork, "test.a"))); @@ -1399,6 +1468,8 @@ fn workescape(s: str) str = { for (ri < rootkeys.len) { assert(occurrences(wwtrace, strings.concat(rootkeys[ri], ".unit.ww")) == 1); + assert(occurrences(wwtrace, strings.concat(mainkeys[ri], + ".unit.ww")) == 1); ri += 1; }; assert(occurrences(readfile(wwlinkertrace), "\n") == 4); @@ -1747,7 +1818,7 @@ fn workescape(s: str) str = { let cbin: str = readfile(bin); let rootunit: str = readfile(strings.concat(work, "__ww-test-000-same.unit.ww")); - assert(has(rootunit, "//ww:module-reset\npackage target;")); + assert(has(rootunit, "//ww:module-reset target\npackage target;")); assert(!has(rootunit, "//ww:module ")); let linkargs: str = readfile(trace); assert(linkargs.len > 8192); @@ -2214,7 +2285,7 @@ fn workescape(s: str) str = { let workroot: str = strings.concat(bin, ".sepwork"); let work: str = strings.concat(workroot, "/"); let artifacts: []str = ["ww_root_parity_base_7f3", - "ww_root_parity_left_7f3", "ww_root_parity_right_7f3"]; + "ww_root_parity_left_7f3", "ww_root_parity_right_7f3", "__root"]; let unitartifacts: []str = ["ww_root_parity_base_7f3", "ww_root_parity_left_7f3", "ww_root_parity_right_7f3", "__root"]; let wantunits: []str = [strings.concat( @@ -2224,10 +2295,10 @@ fn workescape(s: str) str = { leftbody, "\n"), strings.concat("//ww:module-reset ww_root_parity_right_7f3\n", rightbody, "\n"), - strings.concat("//ww:module-reset\n", rootbody, "\n")]; + strings.concat("//ww:module-reset main\n", rootbody, "\n")]; let referenceunits: []str = ["", "", "", ""]; - let referenceexports: []str = ["", "", ""]; - let referencearchives: []str = ["", "", ""]; + let referenceexports: []str = ["", "", "", ""]; + let referencearchives: []str = ["", "", "", ""]; let referencebin: str = ""; let referencecompiler: str = ""; let referenceassembler: str = ""; @@ -2327,8 +2398,6 @@ fn workescape(s: str) str = { }; ai += 1; }; - assert(!os.exists(strings.concat(work, "__root.wwi"))); - assert(!os.exists(strings.concat(work, "__root.a"))); assert(os.exists(bin)); let ctrace: str = readfile(compilertrace); @@ -2351,11 +2420,12 @@ fn workescape(s: str) str = { "ww_root_parity_right_7f3.wwi><-o><", work, "ww_root_parity_right_7f3.s><", work, "ww_root_parity_right_7f3.unit.ww>"); - let rootline: str = strings.concat("BEGIN<-c><--import>", + let rootline: str = strings.concat("BEGIN<--entry><-c><--import>", "<", work, "ww_root_parity_left_7f3.wwi><--import>", "<", work, - "ww_root_parity_right_7f3.wwi><-o><", work, + "ww_root_parity_right_7f3.wwi><-I><", work, + "__root.wwi><-o><", work, "__root.s><", work, "__root.unit.ww>"); assert(same(linecontaining(ctrace, "ww_root_parity_base_7f3.unit.ww"), baseline)); @@ -2371,7 +2441,7 @@ fn workescape(s: str) str = { "ww_root_parity_left_7f3.s>"))); assert(occurrences(ltrace, "\n") == 1); assert(has(ltrace, strings.concat("BEGIN<-o><", bin, "><", work, - "__root.o>"))); + "__root.a>"))); ai = 0; for (ai < artifacts.len) { assert(occurrences(ltrace, strings.concat("<", work, artifacts[ai], diff --git a/test/sep/importdir_test.ww b/test/sep/importdir_test.ww index d0b851d0..40ebfd05 100644 --- a/test/sep/importdir_test.ww +++ b/test/sep/importdir_test.ww @@ -97,12 +97,14 @@ fn samefile(a: str, b: str, why: str) void = { || testenv.has(foounit, "//ww:module ")) { fail("foo unit is not exactly its sorted, owned source set"); }; - let wantroot: str = strings.concat("//ww:module-reset\n", appsrc, "\n"); + let wantroot: str = strings.concat("//ww:module-reset main\n", appsrc, + "\n"); if (!testenv.same(wantroot, testenv.readfile(strings.concat(cwork, "__root.unit.ww")))) { fail("root unit is not exactly its owned source"); }; - let keys: []str = ["example.base", "example.bar", "example.foo"]; + let keys: []str = ["example.base", "example.bar", "example.foo", + "__root"]; let suffixes: []str = [".unit.ww", ".wwi", ".a"]; i = 0; for (i < keys.len) { @@ -117,10 +119,6 @@ fn samefile(a: str, b: str, why: str) void = { }; i += 1; }; - samefile(strings.concat(cwork, "__root.unit.ww"), strings.concat(cwork2, - "__root.unit.ww"), "root unit changed across clean C builds"); - samefile(strings.concat(cwork, "__root.unit.ww"), strings.concat(wwork, - "__root.unit.ww"), "root unit differs between stages"); samefile(stems[0], stems[1], "C executables are not deterministic"); samefile(stems[0], stems[2], "C/WW executables differ"); diff --git a/test/sep/localbuild_test.ww b/test/sep/localbuild_test.ww index e7940f77..6d97ee5d 100644 --- a/test/sep/localbuild_test.ww +++ b/test/sep/localbuild_test.ww @@ -286,7 +286,8 @@ fn rejectstable(dir: str, label: str, target: str, needle: str) void = { || testenv.has(apiiface, "hidden")) { fail("self-contained", "api export leaked unrelated or private declarations"); }; - let expectedunit: str = strings.concat("//ww:module-reset\n", mainsrc, "\n"); + let expectedunit: str = strings.concat("//ww:module-reset main\n", + mainsrc, "\n"); if (!testenv.same(expectedunit, testenv.readfile(strings.concat(cwork, "__root.unit.ww"))) || !testenv.same(expectedunit, testenv.readfile(strings.concat(wwork, "__root.unit.ww")))) { @@ -405,7 +406,7 @@ fn writediamond(td: str, reverse: bool) str = { let unit: str = testenv.readfile(strings.concat(scratch, "__root.unit.ww")); let rootbody: str = testenv.readfile(strings.concat(main, "/main.ww")); - let wantroot: str = strings.concat("//ww:module-reset\n", rootbody, + let wantroot: str = strings.concat("//ww:module-reset main\n", rootbody, "\n"); if (!testenv.same(unit, wantroot) || testenv.has(unit, "//ww:module ")) { @@ -530,6 +531,8 @@ fn writediamond(td: str, reverse: bool) str = { let referenceunit: str = ""; let referencewwi: str = ""; let referencearchive: str = ""; + let referencerootwwi: str = ""; + let referencerootarchive: str = ""; let referencebin: str = ""; let referencecompiler: str = ""; let referenceassembler: str = ""; @@ -567,7 +570,7 @@ fn writediamond(td: str, reverse: bool) str = { "__root.unit.ww")); let depunit: str = testenv.readfile(strings.concat(work, "dep.unit.ww")); - if (!testenv.has(rootunit, "//ww:module-reset\npackage main;") + if (!testenv.has(rootunit, "//ww:module-reset main\npackage main;") || testenv.has(rootunit, "//ww:module ") || !testenv.has(depunit, "//ww:module-reset dep\npackage dep;") || testenv.has(depunit, "//ww:module ")) { @@ -578,8 +581,8 @@ fn writediamond(td: str, reverse: bool) str = { || !testenv.exists(strings.concat(work, "dep.wwi")) || !testenv.exists(strings.concat(work, "dep.a")) || !testenv.exists(strings.concat(work, "__root.o")) - || testenv.exists(strings.concat(work, "__root.wwi")) - || testenv.exists(strings.concat(work, "__root.a"))) { + || !testenv.exists(strings.concat(work, "__root.wwi")) + || !testenv.exists(strings.concat(work, "__root.a"))) { fail("library-roots", "package artifacts do not match root ownership"); }; let ctrace: str = testenv.readfile(compilertrace); @@ -603,6 +606,10 @@ fn writediamond(td: str, reverse: bool) str = { "BEGIN<-c><--import><", work, "leaf.wwi><-I><", work, "dep.wwi><-o><", work, "dep.s><", work, "dep.unit.ww>")) + || !testenv.has(ctrace, strings.concat( + "BEGIN<--entry><-c><--import><", work, + "dep.wwi><-I><", work, "__root.wwi><-o><", work, + "__root.s><", work, "__root.unit.ww>")) || testenv.occurrences(atrace, "\n") != 3 || !testenv.has(atrace, strings.concat("BEGIN<-o><", work, "dep.o><", work, "dep.s>"))) { @@ -610,7 +617,7 @@ fn writediamond(td: str, reverse: bool) str = { }; if (testenv.occurrences(ltrace, "\n") != 1 || !testenv.has(ltrace, strings.concat("BEGIN<-o><", bin, "><", - work, "__root.o>")) + work, "__root.a>")) || testenv.pos(ltrace, strings.concat("<", work, "dep.a>")) < 0 || testenv.pos(ltrace, strings.concat("<", work, "leaf.a>")) < testenv.pos(ltrace, strings.concat("<", work, "dep.a>")) @@ -643,6 +650,10 @@ fn writediamond(td: str, reverse: bool) str = { "dep.wwi"))); referencearchive = strings.dup(testenv.readfile(strings.concat(work, "dep.a"))); + referencerootwwi = strings.dup(testenv.readfile(strings.concat(work, + "__root.wwi"))); + referencerootarchive = strings.dup(testenv.readfile(strings.concat(work, + "__root.a"))); referencebin = strings.dup(testenv.readfile(bin)); referencecompiler = strings.dup(ctrace); referenceassembler = strings.dup(atrace); @@ -653,6 +664,10 @@ fn writediamond(td: str, reverse: bool) str = { work, "dep.wwi"))) || !testenv.same(referencearchive, testenv.readfile(strings.concat( work, "dep.a"))) + || !testenv.same(referencerootwwi, + testenv.readfile(strings.concat(work, "__root.wwi"))) + || !testenv.same(referencerootarchive, + testenv.readfile(strings.concat(work, "__root.a"))) || !testenv.same(referencebin, testenv.readfile(bin)) || !testenv.same(referencecompiler, ctrace) || !testenv.same(referenceassembler, atrace) @@ -723,7 +738,7 @@ fn writediamond(td: str, reverse: bool) str = { runav) != 42) { fail("library-roots", "empty override did not use default roots"); }; - if (!testenv.has(rootunit, "//ww:module-reset\npackage main;") + if (!testenv.has(rootunit, "//ww:module-reset main\npackage main;") || testenv.has(rootunit, "//ww:module ") || !testenv.exists(strings.concat(emptywork, "types.wwi")) || !testenv.exists(strings.concat(emptywork, "types.a")) @@ -870,7 +885,7 @@ fn writediamond(td: str, reverse: bool) str = { let depunitpath: str = strings.concat(work, "/dep.unit.ww"); let rootunit: str = testenv.readfile(rootunitpath); let depunit: str = testenv.readfile(depunitpath); - if (!testenv.has(rootunit, "//ww:module-reset\npackage main;") + if (!testenv.has(rootunit, "//ww:module-reset main\npackage main;") || testenv.has(rootunit, "//ww:module ") || !testenv.has(depunit, "//ww:module-reset dep\npackage dep;") || testenv.has(depunit, "//ww:module ")) { @@ -880,8 +895,8 @@ fn writediamond(td: str, reverse: bool) str = { || !testenv.exists(strings.concat(work, "/leaf.a")) || !testenv.exists(strings.concat(work, "/dep.wwi")) || !testenv.exists(strings.concat(work, "/dep.a")) - || testenv.exists(strings.concat(work, "/__root.wwi")) - || testenv.exists(strings.concat(work, "/__root.a")) + || !testenv.exists(strings.concat(work, "/__root.wwi")) + || !testenv.exists(strings.concat(work, "/__root.a")) || !testenv.same(testenv.readfile(strings.concat(work, "/.wwtool.ww")), testenv.readfile(copied[si])) || !testenv.same(testenv.readfile(strings.concat(work, @@ -890,7 +905,7 @@ fn writediamond(td: str, reverse: bool) str = { "/.wwtool.w6a")), testenv.readfile(assembler)) || !testenv.same(testenv.readfile(strings.concat(work, "/.wwtool.stamp")), - "ww workdir fmt 6 mode build asm 0\n")) { + "ww workdir fmt 7 mode build asm 0\n")) { fail("driver-identity", "persistent artifacts or identities are incomplete"); }; let coldwwi: str = testenv.readfile(strings.concat(work, "/dep.wwi")); @@ -905,7 +920,8 @@ fn writediamond(td: str, reverse: bool) str = { "/dep.wwi.new><-o><", work, "/dep.s.new><", work, "/dep.unit.new>")) || !testenv.has(coldcompiler, strings.concat( - "BEGIN<-c><--import><", work, "/dep.wwi><-o><", work, + "BEGIN<--entry><-c><--import><", work, + "/dep.wwi><-I><", work, "/__root.wwi.new><-o><", work, "/__root.s.new><", work, "/__root.unit.new>")) || testenv.occurrences(coldassembler, "\n") != 3 @@ -916,7 +932,7 @@ fn writediamond(td: str, reverse: bool) str = { }; if (testenv.occurrences(coldlinker, "\n") != 1 || !testenv.has(coldlinker, strings.concat("BEGIN<-o><", bin, - "><", work, "/__root.o>")) + "><", work, "/__root.a>")) || testenv.pos(coldlinker, strings.concat("<", work, "/dep.a>")) < 0 || testenv.pos(coldlinker, strings.concat("<", work, diff --git a/test/sep/sepbuild_test.ww b/test/sep/sepbuild_test.ww index 96243c53..d46c2139 100644 --- a/test/sep/sepbuild_test.ww +++ b/test/sep/sepbuild_test.ww @@ -8,19 +8,16 @@ package sepbuild_test; // // sepbuild (#46 commit-3) — build_one_sep END-TO-END on the real lib // chain root -> os -> {rt,time} (transitive-closure discovery + topo): -// build+run exit 7 both stages; {time,rt,os}.wwi + __root.s -// materialize (#69: the root is compiled without -I, so no -// __root.wwi); per-package .s/.wwi/.unit.ww and the final binary +// build+run exit 7 both stages; {time,rt,os,__root}.wwi/.a +// materialize; per-package .s/.wwi/.a/.unit.ww and the final binary // byte-id cs vs ww; every .unit.ww is the package's own sorted source // set; `ww run` routes through the same sole sep path (exit 7 both // stages). // -// seproot (#69 BUG-1) — a ROOT whose `export fn use(a: *t)` names an -// unexported local `type t` builds (exit 37 both stages) because the -// root is compiled WITHOUT the -I .wwi-producer flag: __root.wwi must -// NOT exist; replaying the driver's own __root.unit.ww plus c.wwi -// through w6c/w6c_ww with -I must REJECT (the isolated bug) while -c -// -o alone must accept; __root.s carries the exported fn. +// seproot — a ROOT whose `export fn use(a: *t)` reaches an unexported +// local `type t` builds and emits a deterministic self-contained .wwi/.a. +// The private type is carried as compiler export data without `export`, and +// an importing source still cannot qualify `main.t`. // // sepstructdef (#70 BUG-2) — a dep exporting aggregate-init defs // (struct-lit + array-lit) sep-builds and links (exit 20 both @@ -91,29 +88,25 @@ fn samefile(label: str, what: str, a: str, b: str) void = { s += 1; }; - // discovery/topo: the transitive package set materialized. #69: the - // root emits no .wwi (compiled without -I); its .s stands in. + // discovery/topo: every reachable package action materialized. let pkgs: []str = ["time", "rt", "os", "__root"]; let i: i32 = 0; for (i < pkgs.len) { - let suffix: str = ".wwi"; - if (testenv.same(pkgs[i], "__root")) { suffix = ".s"; }; if (!testenv.exists(strings.concat(td, "/prog.cs.sepwork/", - pkgs[i], suffix))) { - fail("sepbuild", strings.concat(pkgs[i], suffix, + pkgs[i], ".wwi")) || !testenv.exists(strings.concat(td, + "/prog.cs.sepwork/", pkgs[i], ".a"))) { + fail("sepbuild", strings.concat(pkgs[i], ".wwi/.a", " missing (discovery/topo)")); }; i += 1; }; // cs==ww (rule 10): per-package artifacts + the final binary - let sufs: []str = [".s", ".wwi", ".unit.ww"]; + let sufs: []str = [".s", ".wwi", ".a", ".unit.ww"]; let p: i32 = 0; for (p < pkgs.len) { let k: i32 = 0; for (k < sufs.len) { - if (testenv.same(pkgs[p], "__root") - && testenv.same(sufs[k], ".wwi")) { k += 1; continue; }; samefile("sepbuild", strings.concat(pkgs[p], sufs[k]), strings.concat(td, "/prog.cs.sepwork/", pkgs[p], sufs[k]), strings.concat(td, "/prog.ww.sepwork/", pkgs[p], sufs[k])); @@ -171,15 +164,13 @@ fn samefile(label: str, what: str, a: str, b: str) void = { s += 1; }; - // cs==ww; the root's .wwi is intentionally absent (asserted below) + // cs==ww for the complete package artifacts, including the root archive. let pkgs: []str = ["c", "__root"]; - let sufs: []str = [".s", ".wwi", ".unit.ww"]; + let sufs: []str = [".s", ".wwi", ".a", ".unit.ww"]; let p: i32 = 0; for (p < pkgs.len) { let k: i32 = 0; for (k < sufs.len) { - if (testenv.same(pkgs[p], "__root") - && testenv.same(sufs[k], ".wwi")) { k += 1; continue; }; samefile("seproot", strings.concat(pkgs[p], sufs[k]), strings.concat(td, "/prog.cs.sepwork/", pkgs[p], sufs[k]), strings.concat(td, "/prog.ww.sepwork/", pkgs[p], sufs[k])); @@ -190,38 +181,51 @@ fn samefile(label: str, what: str, a: str, b: str) void = { samefile("seproot", "the final binary", strings.concat(td, "/prog.cs"), strings.concat(td, "/prog.ww")); - // the fix: the root is compiled WITHOUT -I, so no __root.wwi + // The reachable private nominal is compiler data, not a public source name. let s2: i32 = 0; for (s2 < 2) { - if (testenv.exists(strings.concat(td, "/prog.", tags[s2], - ".sepwork/__root.wwi"))) { + let rootiface: str = strings.concat(td, "/prog.", tags[s2], + ".sepwork/__root.wwi"); + let ifacebytes: str = testenv.readfile(rootiface); + if (!testenv.has(ifacebytes, "type t = struct { v: i32 }") + || testenv.has(ifacebytes, "export type t") + || !testenv.has(ifacebytes, "export fn use(a: *t) i32;")) { fail("seproot", strings.concat(drvs[s2], - " produced __root.wwi (-I still passed for the root)")); + " did not carry a private reachable type as compiler data")); }; s2 += 1; }; - // replay the pre-fix invocation on the driver's own __root.unit.ww: - // with -I the export-check FIRES (the isolated bug); without it the - // post-fix invocation accepts. Both compilers. + // Replay the package compile with -I: both compilers accept and emit the + // same self-contained export. A source importer cannot name its private t. let unit: str = strings.concat(td, "/prog.cs.sepwork/__root.unit.ww"); let ciface: str = strings.concat(td, "/prog.cs.sepwork/c.wwi"); + let consumer: str = strings.concat(td, "/consumer.ww"); + testenv.writefile(consumer, strings.concat( + "package consumer;\n", + "import main;\n", + "fn forbidden(v: *main.t) void = {};\n")); let comps: []str = ["w6c", "w6c_ww"]; let c: i32 = 0; for (c < 2) { let wwi: str = strings.concat(td, "/nv.", comps[c], ".wwi"); let asmf: str = strings.concat(td, "/nv.", comps[c], ".s"); - let rav: []str = [testenv.driver(comps[c]), "-c", "--import", "c", - ciface, "-I", wwi, "-o", asmf, unit]; - if (runcode(td, strings.concat("nvI_", comps[c]), rav) == 0) { - fail("seproot", strings.concat(comps[c], " -c -I accepted the ", - "root export-over-unexported-type (vacuous gate)")); + let rav: []str = [testenv.driver(comps[c]), "--entry", "-c", + "--import", "c", ciface, "-I", wwi, "-o", asmf, unit]; + if (runcode(td, strings.concat("reexport_", comps[c]), rav) != 0) { + fail("seproot", strings.concat(comps[c], + " rejected the self-contained root export")); }; - let aav: []str = [testenv.driver(comps[c]), "-c", "--import", "c", - ciface, "-o", asmf, unit]; - if (runcode(td, strings.concat("nvO_", comps[c]), aav) != 0) { - fail("seproot", strings.concat(comps[c], " -c -o (no -I) ", - "rejected the root unit (post-fix invocation)")); + let rejectav: []str = [testenv.driver(comps[c]), "-c", "--import", + "main", wwi, "-o", asmf, consumer]; + let reject: testenv.commandout; + testenv.runcommand(td, td, strings.concat("private_", comps[c]), + rejectav, tmo(), &reject); + if (reject.termination != exec.termination.EXIT || reject.code == 0 + || !testenv.has(reject.stderr, + "package 'main' has no exported declaration 't'")) { + fail("seproot", strings.concat(comps[c], + " exposed the compiler-private type to source importers")); }; c += 1; }; diff --git a/test/sep/sepimport_test.ww b/test/sep/sepimport_test.ww index 005abc46..d49c00c3 100644 --- a/test/sep/sepimport_test.ww +++ b/test/sep/sepimport_test.ww @@ -69,7 +69,8 @@ fn cmpsepwork(label: str, csdir: str, wwdir: str) void = { let i: i32 = 0; for (i < names.len) { if (strings.hassuffix(names[i], ".s") - || strings.hassuffix(names[i], ".wwi")) { + || strings.hassuffix(names[i], ".wwi") + || strings.hassuffix(names[i], ".a")) { seen += 1; if (!testenv.same( testenv.readfile(strings.concat(csdir, "/", names[i])), @@ -81,7 +82,7 @@ fn cmpsepwork(label: str, csdir: str, wwdir: str) void = { i += 1; }; // an existing-but-empty sepwork would pass the loop vacuously - if (seen == 0) { fail(label, "no .s/.wwi in cs sepwork"); }; + if (seen == 0) { fail(label, "no package artifacts in cs sepwork"); }; }; @test fn coloimport() void = { @@ -210,11 +211,11 @@ fn depmainrow(label: str, entry: str, want: i32, deps: str, l += 1; }; - // the root emits no .wwi post-#69, so its suffix set omits .wwi - let parts: []str = ["aa.s", "aa.wwi", "aa.unit.ww", "__root.s", - "__root.unit.ww"]; + // Both the dependency and raw explicit root emit complete package artifacts. + let parts: []str = ["aa.s", "aa.wwi", "aa.a", "aa.unit.ww", + "__root.s", "__root.wwi", "__root.a", "__root.unit.ww"]; - // cs==ww (rule 10) per layout over the fixed 5-part table + // cs==ww (rule 10) per layout over the complete package-artifact table let l2: i32 = 0; for (l2 < 2) { let p: i32 = 0; diff --git a/test/sep/seplink_test.ww b/test/sep/seplink_test.ww index a81621f1..b4ab6129 100644 --- a/test/sep/seplink_test.ww +++ b/test/sep/seplink_test.ww @@ -4,12 +4,11 @@ package seplink_test; // retired native carriers test/wcc/989_separchive_run.c and // 989_sepcycle_dup.c; every assertion preserved. // -// archive (#46 commit-5a) — `ww build` wraps each DEP package's .o in -// a deterministic single-member .a and links the ROOT as a positional -// .o (force-loaded): build+run exit 7 both stages; __root.a absent, -// __root.o + helper.a present; cs helper.a == ww helper.a (rule 10, -// the .a byte-id substrate); 3 cold cstage rebuilds emit -// byte-identical helper.a (zeroed mtime/uid/gid, fixed mode/member — +// archive — `ww build` wraps every package action's .o in the existing +// deterministic single-member .a and links the root archive first: +// build+run exit 7 both stages; __root.a + helper.a present; both archives +// are byte-identical across stages; 3 cold cstage rebuilds emit +// byte-identical archives (zeroed mtime/uid/gid, fixed mode/member — // a floating byte would poison the content cache key). // // archivedup (#31 PASS 3) — two dep packages force the same link @@ -94,22 +93,20 @@ fn runcode(dir: str, name: str, argv: []str) i32 = { s += 1; }; - // layout: root stays a positional force-loaded .o, deps become .a - if (testenv.exists(strings.concat(td, "/prog.cs.sepwork/__root.a"))) { - fail("archive", "root wrapped in .a (should stay positional .o)"); - }; - if (!testenv.exists(strings.concat(td, "/prog.cs.sepwork/__root.o"))) { - fail("archive", "missing root .o"); - }; - if (!testenv.exists(strings.concat(td, "/prog.cs.sepwork/helper.a"))) { - fail("archive", "missing dep helper.a"); + // layout: root and dependency are both package archives. + if (!testenv.exists(strings.concat(td, "/prog.cs.sepwork/__root.a")) + || !testenv.exists(strings.concat(td, "/prog.cs.sepwork/helper.a"))) { + fail("archive", "missing root or dependency archive"); }; - // rule 10: the .a byte-id substrate + // rule 10: the complete .a byte-id substrate if (!testenv.same( testenv.readfile(strings.concat(td, "/prog.cs.sepwork/helper.a")), - testenv.readfile(strings.concat(td, "/prog.ww.sepwork/helper.a")))) { - fail("archive", "cs helper.a != ww helper.a (rule 10 .a byte-id)"); + testenv.readfile(strings.concat(td, "/prog.ww.sepwork/helper.a"))) + || !testenv.same( + testenv.readfile(strings.concat(td, "/prog.cs.sepwork/__root.a")), + testenv.readfile(strings.concat(td, "/prog.ww.sepwork/__root.a")))) { + fail("archive", "cs package archives != ww package archives"); }; // determinism: 3 cold cstage rebuilds -> byte-identical .a @@ -132,9 +129,18 @@ fn runcode(dir: str, name: str, argv: []str) i32 = { let a2: str = testenv.readfile(strings.concat(det2, ".sepwork/helper.a")); if (!testenv.same(a0, a1) || !testenv.same(a1, a2)) { - fail("archive", strings.concat(".a not deterministic across 3 ", + fail("archive", strings.concat("dependency .a not deterministic across 3 ", "builds (floating bytes poison the cache key)")); }; + let r0: str = testenv.readfile(strings.concat(det0, + ".sepwork/__root.a")); + let r1: str = testenv.readfile(strings.concat(det1, + ".sepwork/__root.a")); + let r2: str = testenv.readfile(strings.concat(det2, + ".sepwork/__root.a")); + if (!testenv.same(r0, r1) || !testenv.same(r1, r2)) { + fail("archive", "root .a not deterministic across 3 builds"); + }; testenv.clean(td); }; diff --git a/test/wcc/992_w6l_ww.c b/test/wcc/992_w6l_ww.c index 369f844f..ee0c6bcb 100644 --- a/test/wcc/992_w6l_ww.c +++ b/test/wcc/992_w6l_ww.c @@ -54,8 +54,8 @@ slurp(const char *path, char **outbuf, size_t *outlen) } /* #93 sep layout: the flip stops emitting a monolithic /main.o; - * the linkable unit is now the per-package .o + reverse-topo .a set the - * sep driver assembles (a raw `w6l .o *.a libwwrt.a` from the test + * the linkable unit is now the root .a + reverse-topo .a set the + * sep driver assembles (a raw `w6l .a *.a libwwrt.a` from the test * side fails — `undefined reference` — because it can't reproduce the * driver's topo order). Drive each real directory package through the * full sep build twice; both compile directly and differ only in the @@ -139,7 +139,7 @@ main(void) if (getcwd(cwd, sizeof cwd) == NULL) return 1; /* Real bootstrap-style links: each selfhost tool's full sep build - * (root .o + reverse-topo dep .a set + libwwrt.a). Both builds compile + * (root .a + reverse-topo dep .a set + libwwrt.a). Both builds compile * directly and differ only in the selected linker. (wwdump is excluded — * it imports the compiler-internal cmd packages syntax/check/cgen, which don't resolve * under the lib-path sep dep scan; w6a/w6l are self-contained @@ -157,6 +157,6 @@ main(void) return 1; } printf("w6l_ww: byte-identical to C w6l on %d selfhost tool links " - "(sep root .o + reverse-topo .a set + libwwrt.a)\n", n); + "(sep root .a + reverse-topo .a set + libwwrt.a)\n", n); return 0; }