From 9862f0c05ff2902704891ad998e43d2bb5ed411d Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Wed, 12 Aug 2026 03:46:47 +0900 Subject: [PATCH] cmd: build directory package test variants --- cmd/ww/main.c | 514 ++++++++++++++++++++++++++++------ docs/build-system.md | 61 ++++ internal/wwpackage/package.ww | 165 ++++------- selfhost/cmd/ww/main.ww | 444 +++++++++++++++++++++++------ 4 files changed, 914 insertions(+), 270 deletions(-) diff --git a/cmd/ww/main.c b/cmd/ww/main.c index eebc4f26..75d5be94 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -238,79 +238,226 @@ strs_cmp(const void *a, const void *b) return strcmp(sa, sb); } -/* Go's contract: only *_test.ww is a test source. A line-leading @test - * declaration anywhere else would be silently dropped by a non-T build - * (#6, the Hare model), so directory enumeration rejects it loudly. */ +static int sep_slurp(const char*, char**, u64*); + +/* Go's contract: only *_test.ww is a test source. Ask the compiler parser, + * rather than a textual attribute scan, whether a production source contains + * @test; otherwise valid whitespace/comments could silently drop a test. */ static int -file_has_line_test(const char *path) +source_has_test_decl(const char *path) { - char line[4096]; - FILE *f = fopen(path, "rb"); - if (f == NULL) return 0; - int found = 0; - while (fgets(line, sizeof line, f) != NULL) { - char *p = line; - while (*p == ' ' || *p == '\t' || *p == '\r') p++; - if (strncmp(p, "@test", 5) == 0 - && (p[5] == ' ' || p[5] == '\t')) { - found = 1; - break; - } + char *buf; + u64 len; + if (sep_slurp(path, &buf, &len) < 0) return -1; + /* Keep directory-loader package-clause diagnostics stable. The full + * parser reports its language-level "missing package clause" first; + * the imports-only pass owns the package-loader wording and also avoids + * stage-specific recovery diagnostics for a malformed clause. */ + Arena *ia = newarena(); + Lex il; + Parser ip; + lexinit(&il, ia, path, buf, len); + parserinit(&ip, ia, &il); + Node *imports = parseimports(&ip); + if (il.errs || ip.errs) { + freearena(ia); + free(buf); + return -1; } - fclose(f); + if (imports->module == NULL) { + Pos pp = { path, 1, 1 }; + errorf(pp, "invalid or missing package clause"); + freearena(ia); + free(buf); + return -1; + } + freearena(ia); + Arena *a = newarena(); + Lex l; + Parser p; + lexinit(&l, a, path, buf, len); + parserinit(&p, a, &l); + Node *file = parsefile(&p); + if (l.errs || p.errs) { + freearena(a); + free(buf); + return -1; + } + int found = 0; + for (Node *d = file->list; d != NULL && !found; d = d->next) + if (d->kind == N_FNDECL) + for (Node *at = d->attr; at != NULL; at = at->next) + if (at->str != NULL && strcmp(at->str, "test") == 0) { + found = 1; + break; + } + freearena(a); + free(buf); return found; } -/* A line-leading @test in a production source is diagnosed here and - * returns -2. This is the sole directory-membership discovery path; the - * owning seppkg retains the returned list. */ +#define SEP_VARIANT_PRODUCTION 0 +#define SEP_VARIANT_SAME_TEST 1 +#define SEP_VARIANT_EXTERNAL 2 +#define SEP_TEST_SUPPORT_MODULE "__wwtest" + +/* Test-file package classification uses the compiler's imports-only parser. + * The coordinator chooses variants, but the command owns which real source + * paths enter a package compilation. */ static int -enumerate_dir_ww(const char *dirpath, char ***out_files) +source_package_name(const char *path, char *out, size_t outsz) +{ + char *buf; + u64 len; + if (sep_slurp(path, &buf, &len) < 0) { + fprintf(stderr, "ww: cannot read %s\n", path); + return -1; + } + Arena *a = newarena(); + Lex l; + Parser p; + lexinit(&l, a, path, buf, len); + parserinit(&p, a, &l); + Node *imports = parseimports(&p); + if (l.errs || p.errs) { + freearena(a); + free(buf); + return -1; + } + if (imports->module == NULL) { + Pos pp = { path, 1, 1 }; + errorf(pp, "invalid or missing package clause"); + freearena(a); + free(buf); + return -1; + } + if (strlen(imports->module) >= outsz) { + errorf(imports->pos, "package name is too long"); + freearena(a); + free(buf); + return -1; + } + snprintf(out, outsz, "%s", imports->module); + freearena(a); + free(buf); + return 0; +} + +static int +source_list_add(char ***list, int *n, int *cap, const char *path) +{ + if (*n + 1 > *cap) { + int ncap = *cap ? *cap * 2 : 8; + char **next = realloc(*list, (size_t)ncap * sizeof *next); + if (next == NULL) return -1; + *list = next; + *cap = ncap; + } + char *copy = strdup(path); + if (copy == NULL) return -1; + (*list)[(*n)++] = copy; + return 0; +} + +static void +source_list_free(char **list, int n) +{ + for (int i = 0; i < n; i++) free(list[i]); + free(list); +} + +/* Production packages select production files only. A same-package test root + * selects production files followed by matching same-package test files; an + * external root selects only matching external-test files. Each partition is + * byte-sorted so compiler test discovery is deterministic without generated + * package amalgamation. */ +static int +enumerate_dir_ww(const char *dirpath, int variant, const char *test_package, + char ***out_files) { DIR *d = opendir(dirpath); if (d == NULL) { *out_files = NULL; return -1; } - char **arr = NULL; - int n = 0, cap = 0; + char **prod = NULL, **tests = NULL; + int nprod = 0, capprod = 0, ntests = 0, captests = 0; struct dirent *ent; while ((ent = readdir(d)) != NULL) { const char *nm = ent->d_name; size_t nl = strlen(nm); if (nl <= 3) continue; if (strcmp(nm + nl - 3, ".ww") != 0) continue; + int is_test = nl >= 8 && strcmp(nm + nl - 8, "_test.ww") == 0; + if (variant == SEP_VARIANT_PRODUCTION && is_test) continue; + if (variant == SEP_VARIANT_EXTERNAL && !is_test) continue; char path[2048]; snprintf(path, sizeof path, "%s/%s", dirpath, nm); - if (nl >= 8 && strcmp(nm + nl - 8, "_test.ww") == 0) - continue; struct stat st; if (lstat(path, &st) != 0 || !S_ISREG(st.st_mode)) { fprintf(stderr, "ww: %s: package source is not a regular file\n", path); - for (int i = 0; i < n; i++) free(arr[i]); - free(arr); + source_list_free(prod, nprod); + source_list_free(tests, ntests); closedir(d); *out_files = NULL; return -2; } - if (file_has_line_test(path)) { + int has_test = !is_test ? source_has_test_decl(path) : 0; + if (has_test < 0) { + source_list_free(prod, nprod); + source_list_free(tests, ntests); + closedir(d); + *out_files = NULL; + return -2; + } + if (has_test > 0) { fprintf(stderr, "ww: %s: @test declaration outside *_test.ww\n", path); - for (int i = 0; i < n; i++) free(arr[i]); - free(arr); + source_list_free(prod, nprod); + source_list_free(tests, ntests); closedir(d); *out_files = NULL; return -2; } - if (n + 1 > cap) { - cap = cap ? cap * 2 : 8; - arr = realloc(arr, cap * sizeof *arr); + if (is_test) { + char package[256]; + if (source_package_name(path, package, sizeof package) < 0) { + source_list_free(prod, nprod); + source_list_free(tests, ntests); + closedir(d); + *out_files = NULL; + return -2; + } + if (test_package == NULL || strcmp(package, test_package) != 0) + continue; + } + char ***list = is_test ? &tests : ∏ + int *n = is_test ? &ntests : &nprod; + int *cap = is_test ? &captests : &capprod; + if (source_list_add(list, n, cap, path) < 0) { + source_list_free(prod, nprod); + source_list_free(tests, ntests); + closedir(d); + *out_files = NULL; + return -2; } - arr[n++] = strdup(path); } closedir(d); - if (n > 1) qsort(arr, n, sizeof *arr, strs_cmp); - *out_files = arr; - return n; + if (nprod > 1) qsort(prod, (size_t)nprod, sizeof *prod, strs_cmp); + if (ntests > 1) qsort(tests, (size_t)ntests, sizeof *tests, strs_cmp); + int total = nprod + ntests; + char **all = total ? malloc((size_t)total * sizeof *all) : NULL; + if (total && all == NULL) { + source_list_free(prod, nprod); + source_list_free(tests, ntests); + *out_files = NULL; + return -2; + } + for (int i = 0; i < nprod; i++) all[i] = prod[i]; + for (int i = 0; i < ntests; i++) all[nprod + i] = tests[i]; + free(prod); + free(tests); + *out_files = all; + return total; } /* ww build — separate-compilation driver (task #46/c3). @@ -343,9 +490,12 @@ struct seppkg { char entry[1024]; /* resolved package dir (or file, for a file root) */ char canon[1024]; /* canonical location; never package identity */ char name[256]; /* validated declared name; directory packages only */ - char **sources; /* owned, byte-sorted production paths; dirs only */ + char test_package[256]; /* selected test package; root variants only */ + char **sources; /* owned, byte-sorted selected paths; dirs only */ int nsources; int is_dir; + int variant; /* SEP_VARIANT_*; dependencies are production */ + int test_support; /* compiler-generated -T support package */ int deps[SEP_MAXPKG]; /* direct-dep indices into sepgraph.pkg */ int ndeps; int color; /* tri-color DFS: 0 white, 1 gray, 2 black */ @@ -357,8 +507,8 @@ struct sepgraph { }; static int -sep_find_or_add(struct sepgraph *g, const char *path, const char *entry, - int is_dir) +sep_find_or_add_variant(struct sepgraph *g, const char *path, + const char *entry, int is_dir, int variant, const char *test_package) { if (strlen(path) >= sizeof g->pkg[0].path) { fprintf(stderr, "ww: package path is too long (limit %zu bytes)\n", @@ -376,8 +526,16 @@ sep_find_or_add(struct sepgraph *g, const char *path, const char *entry, return -1; } for (int i = 0; i < g->n; i++) { + int test_production_pair = i == 0 + && g->pkg[i].variant != SEP_VARIANT_PRODUCTION + && variant == SEP_VARIANT_PRODUCTION && is_dir + && g->pkg[i].is_dir + && strcmp(g->pkg[i].canon, canon) == 0; if (strcmp(g->pkg[i].path, path) == 0) { - if (strcmp(g->pkg[i].canon, canon) != 0) { + if (test_production_pair) + continue; + if (strcmp(g->pkg[i].canon, canon) != 0 + || g->pkg[i].variant != variant) { fprintf(stderr, "ww: package %s resolves to both %s and %s\n", path[0] ? path : "(root)", g->pkg[i].entry, @@ -389,6 +547,11 @@ sep_find_or_add(struct sepgraph *g, const char *path, const char *entry, return i; } if (strcmp(g->pkg[i].canon, canon) == 0) { + /* A test root and a production variant reached by its imports or + * runtime closure intentionally may share one directory. No other + * physical-directory alias is permitted. */ + if (test_production_pair) + continue; fprintf(stderr, "ww: package directory %s has identities %s and %s\n", entry, g->pkg[i].path[0] ? g->pkg[i].path : "(root)", @@ -409,7 +572,13 @@ sep_find_or_add(struct sepgraph *g, const char *path, const char *entry, snprintf(p->canon, sizeof p->canon, "%s", canon); free(canon); p->is_dir = is_dir; + p->variant = variant; + p->test_support = 0; p->name[0] = '\0'; + p->test_package[0] = '\0'; + if (test_package != NULL) + snprintf(p->test_package, sizeof p->test_package, "%s", + test_package); p->sources = NULL; p->nsources = 0; p->ndeps = 0; @@ -417,6 +586,14 @@ sep_find_or_add(struct sepgraph *g, const char *path, const char *entry, return g->n++; } +static int +sep_find_or_add(struct sepgraph *g, const char *path, const char *entry, + int is_dir) +{ + return sep_find_or_add_variant(g, path, entry, is_dir, + SEP_VARIANT_PRODUCTION, NULL); +} + /* Release the one package-owned directory-membership list. Every graph exit * funnels through this function; regular-file nodes own no source list. */ static void @@ -481,6 +658,30 @@ use_node_cmp(const void *a, const void *b) return x->pos.col - y->pos.col; } +static int +sep_external_production_name(const struct seppkg *pkg, const char *path, + int leaf_only) +{ + if (pkg->variant != SEP_VARIANT_EXTERNAL + || pkg->test_package[0] == '\0') + return 0; + const char *name = path; + if (leaf_only) { + const char *dot = strrchr(path, '.'); + if (dot != NULL) name = dot + 1; + } + size_t n = strlen(name); + size_t tn = strlen(pkg->test_package); + return tn == n + 5 && strncmp(pkg->test_package, name, n) == 0 + && strcmp(pkg->test_package + n, "_test") == 0; +} + +static int +sep_external_production_import(const struct seppkg *pkg, const char *path) +{ + return sep_external_production_name(pkg, path, 0); +} + /* A DIRECTORY import is a package boundary: add it as a direct dep of pkg * `pi`. A FILE import is an intra-package split — fold its imports into * `pi` (its bytes join pi's body at emit time). Collects package PATHS @@ -587,8 +788,18 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file, } char ipath[1024]; int is_dir = 0; - if (!locate_import(searchpath, path_form, ipath, sizeof ipath, - &is_dir)) { + int external_production = sep_external_production_import( + &g->pkg[pi], name); + int located = 0; + if (external_production) { + snprintf(ipath, sizeof ipath, "%s", g->pkg[pi].entry); + is_dir = 1; + located = 1; + } else { + located = locate_import(searchpath, path_form, ipath, + sizeof ipath, &is_dir); + } + if (!located) { const char *dot = strrchr(name, '.'); const char *leaf = dot ? dot + 1 : name; int inline_package = 0; @@ -613,7 +824,9 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file, } int self = strcmp(canon, g->pkg[pi].canon) == 0; free(canon); - if (self) { + if (self && sep_external_production_name(&g->pkg[pi], name, 1)) + external_production = 1; + if (self && !external_production) { const char *owner = g->pkg[pi].path[0] ? g->pkg[pi].path : g->pkg[pi].name; errorf(u->pos, "self-import: package '%s' cannot import itself", @@ -653,8 +866,10 @@ sep_load_pkg(struct sepgraph *g, int pi, const char *searchpath) struct ImportSet fv = {0}; int rc = 0; if (g->pkg[pi].is_dir) { + const char *test_package = g->pkg[pi].test_package[0] + ? g->pkg[pi].test_package : NULL; g->pkg[pi].nsources = enumerate_dir_ww(g->pkg[pi].entry, - &g->pkg[pi].sources); + g->pkg[pi].variant, test_package, &g->pkg[pi].sources); if (g->pkg[pi].nsources == -2) { rc = -1; /* diagnosed in enumerate_dir_ww */ } else if (g->pkg[pi].nsources < 0) { @@ -670,7 +885,8 @@ sep_load_pkg(struct sepgraph *g, int pi, const char *searchpath) for (int i = 0; i < g->pkg[pi].nsources && rc == 0; i++) rc = sep_scan_file(g, pi, g->pkg[pi].sources[i], searchpath, &fv, 1); - if (rc == 0 && g->pkg[pi].path[0] != '\0') { + if (rc == 0 && g->pkg[pi].path[0] != '\0' + && !g->pkg[pi].test_support) { const char *dot = strrchr(g->pkg[pi].path, '.'); const char *leaf = dot ? dot + 1 : g->pkg[pi].path; if (strcmp(g->pkg[pi].name, leaf) != 0) { @@ -751,7 +967,7 @@ sep_topo_visit(struct sepgraph *g, int pi, int *order, int *no, * body); FILE imports fold in (intra-package split). */ static int sep_emit_body(FILE *out, const char *path, struct ImportSet *visited, - const char *searchpath, const char *modpath) + const char *searchpath, const char *modpath, const struct seppkg *pkg) { if (import_seen(visited, path)) return 0; import_add(visited, path); @@ -788,6 +1004,8 @@ sep_emit_body(FILE *out, const char *path, struct ImportSet *visited, for (int i = 0; i < nuse; i++) { Node *u = uses[i]; const char *name = u->usepath ? u->usepath : u->str; + if (sep_external_production_import(pkg, name)) + continue; char path_form[1024]; import_path_form(name, path_form, sizeof path_form); char ipath[1024]; @@ -796,7 +1014,7 @@ sep_emit_body(FILE *out, const char *path, struct ImportSet *visited, &is_dir)) continue; if (!is_dir && sep_emit_body(out, ipath, visited, searchpath, - modpath) < 0) { + modpath, pkg) < 0) { free(uses); freearena(a); free(buf); @@ -866,10 +1084,10 @@ sep_compose_unit(struct sepgraph *g, int pi, const char *scratch, 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], &bodyvisit, - searchpath, g->pkg[pi].path); + searchpath, g->pkg[pi].path, &g->pkg[pi]); } else { bodyrc = sep_emit_body(u, g->pkg[pi].entry, &bodyvisit, searchpath, - g->pkg[pi].path); + g->pkg[pi].path, &g->pkg[pi]); } for (int i = 0; i < bodyvisit.n; i++) free(bodyvisit.paths[i]); free(bodyvisit.paths); @@ -1027,7 +1245,7 @@ copy_file_atomic(const char *src, const char *dst) static void workdir_stamp_text(char *buf, size_t bufsz, int is_test, int emit_asm) { - snprintf(buf, bufsz, "ww workdir fmt 2 mode %s asm %d\n", + snprintf(buf, bufsz, "ww workdir fmt 3 mode %s asm %d\n", is_test ? "test" : "build", emit_asm); } @@ -1041,7 +1259,8 @@ static int build_one_sep_impl(const char *src, int entry_is_dir, const char *root_identity, const char *out, const char *objstem, const char *extra_includes, const char *extra_libs, - const char *extra_libdirs, int package_only, int is_test, int emit_asm, + const char *extra_libdirs, int package_only, int is_test, + int root_variant, const char *test_package, int emit_asm, const char *workdir, char *scratchout, size_t scratchoutsz, struct sepgraph **graphout) { @@ -1062,6 +1281,7 @@ build_one_sep_impl(const char *src, int entry_is_dir, else if (access("lib", 0) == 0) srcdir = "lib"; else srcdir = libdir; } + const char *toolsrcdir = srcdir; char srcd[1024]; if (entry_is_dir) { snprintf(srcd, sizeof srcd, "%s", src); @@ -1145,27 +1365,68 @@ 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 root = sep_find_or_add(g, rootpath, src, entry_is_dir); + int root = sep_find_or_add_variant(g, rootpath, src, entry_is_dir, + root_variant, test_package); if (root < 0) return 1; - /* #79 (-T): lib/test is the synth main's `test.run` callee but @test - * files never `import test;`. Inject it as a direct dep of the root so - * sep_load_pkg pulls test + its transitive deps; the producer adds -T - * to the root and `test.run` links against test's `.a` — via the - * sep_scan_file dedup-guarded dep append. */ + 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`; + * when user source occupies that identity, the reserved graph alias keeps + * it distinct. The linker receives the same support archive closure. */ if (is_test) { char tpath[1024]; int tdir = 0; - if (locate_import(srcdir, "test", tpath, sizeof tpath, &tdir)) { - int ti = sep_find_or_add(g, "test", tpath, tdir); - if (ti < 0) return 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; + if (locate_import(toolsrcdir, "test", tpath, sizeof tpath, &tdir)) { + char *tc = realpath(tpath, NULL); + char *rc = entry_is_dir ? realpath(src, NULL) : NULL; + int root_is_support = tc != NULL && rc != NULL + && strcmp(tc, rc) == 0; + free(tc); + free(rc); + int collision = !root_is_support && test_package != NULL + && (strcmp(test_package, "test") == 0 + || strcmp(test_package, "test_test") == 0); + char userpath[1024]; + int userdir = 0; + if (!root_is_support && !collision + && locate_import(srcdir, "test", userpath, + sizeof userpath, &userdir)) { + (void)userdir; + tc = realpath(tpath, NULL); + char *uc = realpath(userpath, NULL); + if (tc != NULL && uc != NULL && strcmp(tc, uc) != 0) + collision = 1; + free(tc); + free(uc); + } + if (collision) test_support_module = SEP_TEST_SUPPORT_MODULE; + /* A same-test build of the runtime package already owns run + * and its source imports. An external test still needs the + * colocated production node, which is also its one support dep. */ + if (!root_is_support || root_variant == SEP_VARIANT_EXTERNAL) { + int ti = sep_find_or_add(g, test_support_module, tpath, + 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; + } } } if (sep_load_pkg(g, root, srcdir) < 0) return 1; + if (root_variant != SEP_VARIANT_PRODUCTION + && (test_package == NULL + || strcmp(g->pkg[root].name, test_package) != 0)) { + fprintf(stderr, + "ww: package-test selector does not match loaded package\n"); + return 1; + } int root_package = package_only; if (root_package && strcmp(g->pkg[root].name, "main") == 0) { fprintf(stderr, "ww: -p requires a non-main package\n"); @@ -1227,15 +1488,24 @@ build_one_sep_impl(const char *src, int entry_is_dir, * LOCAL type (the root is never imported), which the * export-check rejects. Skip -I for the root; its `.wwi` * is never consumed. */ - if (!needs_export) + if (!needs_export) { /* #79: the root carries -T under `ww test` * so w6c synthesizes the test main. Deps never * get -T. */ - snprintf(cmd, sizeof cmd, "%s %s-c -o %s %s", - c6, is_test ? "-T " : "", cs, cu); - else - snprintf(cmd, sizeof cmd, "%s -c -I %s -o %s %s", - c6, cw, cs, cu); + if (is_test) + snprintf(cmd, sizeof cmd, + "%s -T --test-support-module %s -c -o %s %s", + c6, test_support_module, cs, cu); + else + snprintf(cmd, sizeof cmd, "%s -c -o %s %s", + c6, cs, cu); + } else + snprintf(cmd, sizeof cmd, "%s %s%s%s-c -I %s -o %s %s", + c6, g->pkg[pi].test_support + ? "--test-support-module " : "", + g->pkg[pi].test_support ? test_support_module : "", + g->pkg[pi].test_support ? " " : "", + cw, cs, cu); if (run(cmd) != 0) { fprintf(stderr, "ww: w6c failed for %s\n", g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)"); @@ -1321,8 +1591,10 @@ build_one_sep_impl(const char *src, int entry_is_dir, /* reverse-topo link: root `.o` first (order[norder-1], force-loaded), * then transitive dep `.a` in reverse-topo order, then libwwrt.a — - * each archive selectively pulls only members satisfying a live - * undef. */ + * each archive selectively pulls only members satisfying a live undef. + * A same-test root already contains its production sources; if that + * production variant is also reached through the runtime closure, keep + * traversing its dependencies but do not link its duplicate archive. */ char rtargs[2048] = {0}; char rtpath[1024]; snprintf(rtpath, sizeof rtpath, "%s/libwwrt.a", libdir); @@ -1336,9 +1608,15 @@ build_one_sep_impl(const char *src, int entry_is_dir, } char objs[8192] = {0}; for (int oi = norder - 1; oi >= 0; oi--) { + int pi = order[oi]; + if (g->pkg[root].variant == SEP_VARIANT_SAME_TEST + && pi != root + && g->pkg[pi].variant == SEP_VARIANT_PRODUCTION + && strcmp(g->pkg[pi].canon, g->pkg[root].canon) == 0) + continue; char path[1024]; - sep_fname(g, order[oi], scratch, - order[oi] == root ? ".o" : ".a", path, sizeof path); + sep_fname(g, pi, scratch, + pi == root ? ".o" : ".a", path, sizeof path); size_t n = strlen(objs); snprintf(objs + n, sizeof objs - n, "%s%s", n ? " " : "", path); } @@ -1365,14 +1643,16 @@ static int build_one_sep(const char *src, int entry_is_dir, const char *root_identity, const char *out, const char *objstem, const char *extra_includes, const char *extra_libs, - const char *extra_libdirs, int package_only, int is_test, int emit_asm, + const char *extra_libdirs, int package_only, int is_test, + int root_variant, const char *test_package, int emit_asm, int keepscratch, const char *workdir) { char scratch[1100] = {0}; struct sepgraph *g = NULL; int r = build_one_sep_impl(src, entry_is_dir, root_identity, out, objstem, - extra_includes, extra_libs, extra_libdirs, package_only, is_test, emit_asm, - workdir, scratch, sizeof scratch, &g); + extra_includes, extra_libs, extra_libdirs, package_only, is_test, + root_variant, test_package, emit_asm, workdir, scratch, + sizeof scratch, &g); sep_graph_free(g); if (!keepscratch && scratch[0]) { size_t sl = strlen(scratch); @@ -1626,7 +1906,8 @@ do_build(int argc, char **argv) } const char *root_identity = package_only && !literal ? src : NULL; return build_one_sep(resolved, is_dir, root_identity, out, objstem, incs, libs, - libdirs, package_only, 0, emit_asm, 1, workdir); + libdirs, package_only, 0, SEP_VARIANT_PRODUCTION, NULL, emit_asm, + 1, workdir); } static int @@ -1658,7 +1939,7 @@ do_run(int argc, char **argv) /* The freshly acquired directory owns both the executable and the * adjacent main.sepwork tree. Nothing outside it is adopted or removed. */ if (build_one_sep(resolved, is_dir, NULL, tmp, tmp, incs, libs, libdirs, - 0, 0, 0, 0, NULL) != 0) { + 0, 0, SEP_VARIANT_PRODUCTION, NULL, 0, 0, NULL) != 0) { if (unlink(tmp) != 0 && errno != ENOENT) fputs("ww: cannot remove temporary output\n", stderr); if (rmdir(tmpdir) != 0) @@ -1704,6 +1985,8 @@ static int do_test(int argc, char **argv) { const char *src = NULL; + const char *package_test_kind = NULL; + const char *package_test_name = NULL; char incs[2048] = {0}; /* -c (Go's `go test -c`) builds the test binary without running it. * -S + -o stops after the lib/test-inclusive package `.s` @@ -1748,6 +2031,26 @@ do_test(int argc, char **argv) "%s%s", n ? ":" : "", dir); } else if (strcmp(argv[i], "-c") == 0) { compileonly = 1; + } else if (strcmp(argv[i], "--ww-package-test") == 0) { + if (i + 2 >= argc || package_test_kind != NULL) { + fprintf(stderr, + "ww test: --ww-package-test needs kind and package\n"); + return 2; + } + package_test_kind = argv[++i]; + package_test_name = argv[++i]; + size_t pn = strlen(package_test_name); + if ((strcmp(package_test_kind, "same") != 0 + && strcmp(package_test_kind, "external") != 0) + || pn == 0 || pn >= sizeof ((struct seppkg *)0)->name + || (strcmp(package_test_kind, "external") == 0 + && (pn <= 5 + || strcmp(package_test_name + pn - 5, + "_test") != 0))) { + fprintf(stderr, + "ww test: invalid --ww-package-test variant\n"); + return 2; + } } else if (strcmp(argv[i], "-S") == 0) { emit_asm = 1; } else if (strcmp(argv[i], "-list") == 0) { @@ -1798,12 +2101,22 @@ do_test(int argc, char **argv) fprintf(stderr, "ww test: -S needs -o\n"); return 2; } + if (package_test_kind != NULL && packageopts) { + fprintf(stderr, + "ww test: package-test variant rejects package options\n"); + return 2; + } /* Go's ./... form: a trailing "..." element is a package-tree * request for the coordinator, never a literal path — recognized * before stat, with the directory-mode rejects. */ size_t tlen = strlen(target); if (strcmp(target, "...") == 0 || (tlen >= 4 && strcmp(target + tlen - 4, "/...") == 0)) { + if (package_test_kind != NULL) { + fprintf(stderr, + "ww test: package-test variant needs one directory\n"); + return 2; + } if (emit_asm) { fprintf(stderr, "ww test: -S needs a single test file\n"); @@ -1852,6 +2165,18 @@ do_test(int argc, char **argv) "ww test: pattern needs a single test file\n"); return 2; } + if (package_test_kind != NULL) { + if (!compileonly || !outstem[0]) { + fprintf(stderr, + "ww test: package-test variant needs -c -o\n"); + return 2; + } + int variant = strcmp(package_test_kind, "same") == 0 + ? SEP_VARIANT_SAME_TEST : SEP_VARIANT_EXTERNAL; + return build_one_sep(resolved, 1, NULL, outstem, outstem, + incs, "", "", 0, 1, variant, package_test_name, 0, + 1, workdir); + } return exec_package_tests(argc, argv, src, resolved, 0); } if (packageopts) { @@ -1859,6 +2184,11 @@ do_test(int argc, char **argv) "ww test: package options need a directory\n"); return 2; } + if (package_test_kind != NULL) { + fprintf(stderr, + "ww test: package-test variant needs one directory\n"); + return 2; + } char tmpdir[1024] = {0}, tmp[1024]; const char *outp; int owntmp = !outstem[0] && !workdir[0]; @@ -1882,7 +2212,8 @@ do_test(int argc, char **argv) * source. An explicit -o names the caller-owned artifact stem. */ int br = build_one_sep(resolved, is_dir, NULL, outp, outstem[0] ? outstem : tmp, incs, "", "", 0, 1, - emit_asm, outstem[0] ? 1 : 0, workdir); + SEP_VARIANT_PRODUCTION, NULL, emit_asm, + outstem[0] ? 1 : 0, workdir); if (br != 0) { if (owntmp && unlink(outp) != 0 && errno != ENOENT) fputs("ww: cannot remove temporary output\n", stderr); @@ -1914,6 +2245,11 @@ do_test(int argc, char **argv) return rc; } if (S_ISREG(st.st_mode)) { + if (package_test_kind != NULL) { + fprintf(stderr, + "ww test: package-test variant needs one directory\n"); + return 2; + } if (packageopts) { fprintf(stderr, "ww test: package options need a directory\n"); @@ -1938,7 +2274,8 @@ do_test(int argc, char **argv) } /* See module-mode note: no-o scratch is redirected to /tmp. */ int br = build_one_sep(target, 0, NULL, outp, outstem[0] ? outstem : tmp, - incs, "", "", 0, 1, emit_asm, outstem[0] ? 1 : 0, workdir); + incs, "", "", 0, 1, SEP_VARIANT_PRODUCTION, NULL, emit_asm, + outstem[0] ? 1 : 0, workdir); if (br != 0) { if (owntmp && unlink(outp) != 0 && errno != ENOENT) fputs("ww: cannot remove temporary output\n", stderr); @@ -1987,6 +2324,17 @@ do_test(int argc, char **argv) fprintf(stderr, "ww test: pattern needs a single test file\n"); return 2; } + if (package_test_kind != NULL) { + if (!compileonly || !outstem[0]) { + fprintf(stderr, + "ww test: package-test variant needs -c -o\n"); + return 2; + } + int variant = strcmp(package_test_kind, "same") == 0 + ? SEP_VARIANT_SAME_TEST : SEP_VARIANT_EXTERNAL; + return build_one_sep(target, 1, NULL, outstem, outstem, incs, + "", "", 0, 1, variant, package_test_name, 0, 1, workdir); + } return exec_package_tests(argc, argv, src, NULL, src == NULL); } diff --git a/docs/build-system.md b/docs/build-system.md index 84d6f161..f1bee5ea 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -2839,6 +2839,67 @@ 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. +### 11.7 Implemented directory package-test slice + +Directory tests now enter that same local package loader and build path. The +supported manifest-free commands are `ww test DIR`, `ww test DIR/...`, and +their existing `-run`, `-filter`, `-list`, `-timeout-ms`, `-j`, `-c`, and `-w` +forms. `ww test -c -o test.bin DIR` names the result when the selected directory +has one test variant; the coordinator rejects one output name for a multi-variant +or recursive request. Explicit `ww test FILE` retains its compatibility path. + +The test coordinator still discovers requested directories, enumerates the +test package names, schedules independent binaries, executes them, and emits +captured results in byte-sorted package order. It no longer concatenates a +generated production/test root or resolves imports. Instead it asks the Cstage +or WWstage command to build one of two non-importable root variants from the +real directory: + +- `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. +- `external-test` selects only matching `package p_test` files. Its `import p` + is a direct edge to a distinct production node for the same canonical + directory. That node selects every production file, emits `p.wwi` and `p.a`, + and exposes no private declaration to the external root. + +Every non-root dependency is always 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. The compiler unit for each package +contains only its byte-sorted direct dependency `.wwi` artifacts; 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 independent test +root. The command resolves that edge from the selected toolchain source tree, +not the user search path. 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`. + +The selected test root and a production variant reached by its imports or test +runtime closure are the only sanctioned pair of 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 duplicate production archive +is omitted from its final link while that node's dependency archives remain in +the closure. All ordinary logical and physical package-identity collision +checks remain unchanged. Recursive +discovery groups by physical directory before sorting filenames, and persistent +coordinator work directories use an injective escaped `(directory, test +package)` key. Every `*_test.ww` package variant is built even when a file only +declares helpers and contains no `@test`, so its package clause and imports are +still checked by the shared loader. Every built variant is run, and a successful +compiler-owned dispatcher with empty output is reported as `[no tests]`. + +This ownership split follows Go 1.26.5's separation of production, +same-package-test, external-test, and generated test-main inputs in +[`cmd/go/internal/load/test.go`](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/go/internal/load/test.go), +while retaining WW's compiler-owned dispatcher. Direct compile dependencies and +the separately expanded link closure follow the boundary in +[`cmd/go/internal/work/action.go`](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/go/internal/work/action.go). + ## 12. Candidate architectures and hard-gate decision Five candidates were developed as coherent systems, not as feature bins. diff --git a/internal/wwpackage/package.ww b/internal/wwpackage/package.ww index a55e3a20..1876541f 100644 --- a/internal/wwpackage/package.ww +++ b/internal/wwpackage/package.ww @@ -12,7 +12,6 @@ type pkgsource = struct { dir: str, pkg: str, test: bool, - attest: bool, }; type pkgfolder = struct { @@ -20,18 +19,13 @@ type pkgfolder = struct { start: i32, end: i32, prodpkg: str, - hastests: bool, }; type pkggroup = struct { dir: str, pkg: str, - start: i32, - end: i32, - prodpkg: str, external: bool, root: str, - combined: str, workdir: str, bin: str, buildout: str, @@ -53,7 +47,6 @@ def PKGDONE: i32 = 3; // pkggroup.fail values recorded at launch, reported at ordered emission. def PKGFAILNONE: i32 = 0; def PKGFAILSETUP: i32 = 1; -def PKGFAILCOMPOSE: i32 = 2; def pkgpoll: time.duration = 1000000i64: time.duration; @@ -212,22 +205,6 @@ fn pkgclause(src: str, out: *str) bool = { return true; }; -fn pkgattest(src: str) bool = { - let i: i32 = 0; - for (i < src.len) { - for (i < src.len && (src[i] == ' ' || src[i] == '\t' || - src[i] == '\r')) { i += 1; }; - if (i + 5 < src.len && src[i] == '@' && src[i + 1] == 't' && - src[i + 2] == 'e' && src[i + 3] == 's' && src[i + 4] == 't' && - (src[i + 5] == ' ' || src[i + 5] == '\t')) { - return true; - }; - for (i < src.len && src[i] != '\n') { i += 1; }; - if (i < src.len) { i += 1; }; - }; - return false; -}; - fn pkgdirname(path: str) str = { let i: i32 = path.len - 1; for (i >= 0) { @@ -271,7 +248,6 @@ fn pkgisdir(path: str) bool = { fn pkgkeepfile(name: str) bool = { if (!strings.hassuffix(name, ".ww")) { return false; }; - if (strings.hassuffix(name, ".combined.ww")) { return false; }; return true; }; @@ -372,11 +348,19 @@ fn pkgdiscoverdir(path: str, st: *pkgdiscover, recurse: bool) void = { os.close(fd); }; +// Group paths by containing directory before filename order. A plain full-path +// sort can interleave a child directory between two files in its parent and +// split one package into multiple folder records during recursive discovery. fn pkgsort(ss: []str) void = { let i: i32 = 1; for (i < ss.len) { let j: i32 = i; - for (j > 0 && strings.compare(ss[j - 1], ss[j]) > 0) { + for (j > 0) { + let a: str = pkgdirname(ss[j - 1]); + let b: str = pkgdirname(ss[j]); + let c: int = strings.compare(a, b); + if (c == 0) { c = strings.compare(ss[j - 1], ss[j]); }; + if (c <= 0) { break; }; let t: str = ss[j]; ss[j] = ss[j - 1]; ss[j - 1] = t; @@ -505,46 +489,25 @@ fn pkgemitfile(path: str, fd: i32) bool = { return true; }; -fn pkgcombined(g: *pkggroup, srcs: []pkgsource) bool = { - let fd: i32 = os.open(g.combined, - os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 384i32); - if (fd < 0) { return false; }; - let ok: bool = true; - if (!g.external) { - let i: i32 = g.start; - for (i < g.end) { - if (!srcs[i].test) { - if (!pkgwrite(fd, "//ww:module-reset\n")) { ok = false; }; - let s: str; - if (!pkgread(srcs[i].path, &s) || !pkgwrite(fd, s) - || !pkgwrite(fd, "\n")) { ok = false; }; - }; - i += 1; - }; - }; - let i: i32 = g.start; - for (i < g.end) { - if (srcs[i].test && strings.compare(srcs[i].pkg, g.pkg) == 0) { - if (!pkgwrite(fd, "//ww:module-reset\n")) { ok = false; }; - let s: str; - if (!pkgread(srcs[i].path, &s) || !pkgwrite(fd, s) - || !pkgwrite(fd, "\n")) { ok = false; }; - }; +fn pkgworkescape(s: str) str = { + let out: []u8 = alloc([], (s.len * 2 + 1): u64)!; + let i: i32 = 0; + for (i < s.len) { + if (s[i] == '_') { + append(out, '_'); append(out, 'u'); + } else { if (s[i] == '/') { + append(out, '_'); append(out, 's'); + } else { + append(out, s[i]); + }; }; i += 1; }; - os.close(fd); - return ok; + return strings.frombytes(out); }; fn pkgworkkey(dir: str, pkg: str) str = { - let s: str = strings.dup(strings.concat(dir, "_", pkg)); - let b: []u8 = strings.toutf8(s); - let i: i32 = 0; - for (i < b.len) { - if (b[i] == '/') { b[i] = '_'; }; - i += 1; - }; - return strings.frombytes(b); + return strings.concat("d_", pkgworkescape(dir), "_p_", + pkgworkescape(pkg)); }; fn pkgsetpaths(g: *pkggroup, root: str, index: i32, @@ -552,7 +515,6 @@ fn pkgsetpaths(g: *pkggroup, root: str, index: i32, let num: str = strconv.i32tos(index, strconv.base.DEC); g.root = strings.concat(root, "/group-", num); if (!pkgmakedir(g.root)) { return false; }; - g.combined = strings.concat(g.root, "/package.ww"); // A caller-owned persistent workdir root keys one driver -w dir // per (dir, pkg) group; the driver's content-identity contract // owns every reuse decision, so this stays a pure path policy. @@ -615,14 +577,16 @@ fn pkgreportcommand(kind: str, g: *pkggroup, r: *exec.result) void = { fn pkgstartbuild(g: *pkggroup, builder: str, includes: []str, h: *exec.process) void = { - let ba: []str = alloc([], (11 + includes.len * 2): u64)!; + let ba: []str = alloc([], (13 + includes.len * 2): u64)!; append(ba, builder); append(ba, "test"); append(ba, "-c"); append(ba, "-o"); append(ba, g.bin); - append(ba, "-I"); - append(ba, g.dir); + append(ba, "--ww-package-test"); + if (g.external) { append(ba, "external"); } + else { append(ba, "same"); }; + append(ba, g.pkg); let ii: i32 = 0; for (ii < includes.len) { append(ba, "-I"); @@ -633,7 +597,7 @@ fn pkgstartbuild(g: *pkggroup, builder: str, includes: []str, append(ba, "-w"); append(ba, g.workdir); }; - append(ba, g.combined); + append(ba, g.dir); let bcmd: exec.command; bcmd.path = builder; bcmd.argv = ba; @@ -675,6 +639,12 @@ fn pkgbuildok(g: *pkggroup) bool = { && g.buildres.code == 0; }; +fn pkgrunok(g: *pkggroup) bool = { + return g.runres.errno == 0 && g.runres.cleanuperrno == 0 + && g.runres.termination == exec.termination.EXIT + && g.runres.code == 0; +}; + // Ordered emission: a group's captures, diagnostics, and verdict line are // written only here, strictly in group order, so concurrent scheduling // produces the byte stream sequential scheduling produced. @@ -683,10 +653,6 @@ fn pkgemitgroup(g: *pkggroup, tmproot: str, compileonly: bool) bool = { pkgfailpath(tmproot, "cannot create group temporary directory"); return false; }; - if (g.fail == PKGFAILCOMPOSE) { - pkgfailpath(g.combined, "cannot compose package test source"); - return false; - }; let buildcaptures: bool = pkgemitfile(g.buildout, os.STDOUT_FILENO); buildcaptures = pkgemitfile(g.builderr, os.STDERR_FILENO) && buildcaptures; if (!buildcaptures) { @@ -704,15 +670,24 @@ fn pkgemitgroup(g: *pkggroup, tmproot: str, compileonly: bool) bool = { pkgputln(os.STDOUT_FILENO, g.bin); return true; }; - let runcaptures: bool = pkgemitfile(g.runout, os.STDOUT_FILENO); - runcaptures = pkgemitfile(g.runerr, os.STDERR_FILENO) && runcaptures; - if (!runcaptures) { + let runstdout: str; + let runstderr: str; + if (!pkgread(g.runout, &runstdout) || !pkgread(g.runerr, &runstderr)) { pkgfailpath(g.root, "cannot read test capture"); return false; }; - if (g.runres.errno != 0 || g.runres.cleanuperrno != 0 - || g.runres.termination != exec.termination.EXIT - || g.runres.code != 0) { + // The compiler-owned dispatcher is the authority on whether a variant + // contains runnable tests. With an empty generated table, test.run returns + // zero without output; no coordinator-side source scan is involved. + if (pkgrunok(g) && runstdout.len == 0 && runstderr.len == 0) { + pkgput(os.STDOUT_FILENO, "? "); + pkgput(os.STDOUT_FILENO, g.dir); + pkgputln(os.STDOUT_FILENO, " [no tests]"); + return true; + }; + pkgput(os.STDOUT_FILENO, runstdout); + pkgput(os.STDERR_FILENO, runstderr); + if (!pkgrunok(g)) { pkgreportcommand("test", g, &g.runres); return false; }; @@ -874,15 +849,7 @@ export fn packagecommand(args: []str) int = { s.path = ds.paths[i]; s.dir = strings.dup(pkgdirname(ds.paths[i])); s.pkg = strings.dup(pn); - s.attest = pkgattest(body); s.test = strings.hassuffix(pkgbase(ds.paths[i]), "_test.ww"); - // Go's contract: only *_test.ww is a test source; @test - // anywhere else fails loudly rather than run or drop silently. - if (s.attest && !s.test) { - pkgfailpath(ds.paths[i], - "@test declaration outside *_test.ww"); - return 1; - }; append(srcs, s); i += 1; }; @@ -895,15 +862,9 @@ export fn packagecommand(args: []str) int = { f.start = i; f.end = i; f.prodpkg = ""; - f.hastests = false; for (f.end < srcs.len && strings.compare(srcs[f.end].dir, f.path) == 0) { - if (srcs[f.end].test) { - if (srcs[f.end].attest) { f.hastests = true; }; - } else if (f.prodpkg.len == 0) { + if (!srcs[f.end].test && f.prodpkg.len == 0) { f.prodpkg = srcs[f.end].pkg; - } else if (strings.compare(f.prodpkg, srcs[f.end].pkg) != 0) { - pkgfailpath(f.path, "production sources declare conflicting packages"); - return 1; }; f.end += 1; }; @@ -915,21 +876,16 @@ export fn packagecommand(args: []str) int = { i = 0; for (i < folders.len) { let f: pkgfolder = folders[i]; - if (!f.hastests) { - pkgput(os.STDOUT_FILENO, "? "); - pkgput(os.STDOUT_FILENO, f.path); - pkgputln(os.STDOUT_FILENO, " [no tests]"); - i += 1; - continue; - }; let externalpkg: str = ""; if (f.prodpkg.len != 0) { externalpkg = strings.concat(f.prodpkg, "_test"); }; let standalonepkg: str = ""; + let sawtestfile: bool = false; let j: i32 = f.start; for (j < f.end) { if (srcs[j].test) { + sawtestfile = true; if (f.prodpkg.len != 0 && strings.compare(srcs[j].pkg, f.prodpkg) != 0 && strings.compare(srcs[j].pkg, externalpkg) != 0) { @@ -947,7 +903,6 @@ export fn packagecommand(args: []str) int = { return 1; }; }; - if (!srcs[j].attest) { j += 1; continue; }; let found: bool = false; let k: i32 = 0; for (k < groups.len) { @@ -962,9 +917,6 @@ export fn packagecommand(args: []str) int = { let g: pkggroup; g.dir = f.path; g.pkg = srcs[j].pkg; - g.start = f.start; - g.end = f.end; - g.prodpkg = f.prodpkg; g.external = f.prodpkg.len != 0 && strings.compare(f.prodpkg, g.pkg) != 0; append(groups, g); @@ -972,6 +924,14 @@ export fn packagecommand(args: []str) int = { }; j += 1; }; + if (!sawtestfile) { + let g: pkggroup; + g.dir = f.path; + g.pkg = f.prodpkg; + if (g.pkg.len == 0) { g.pkg = srcs[f.start].pkg; }; + g.external = false; + append(groups, g); + }; i += 1; }; if (groups.len == 0) { return 0; }; @@ -1012,9 +972,6 @@ export fn packagecommand(args: []str) int = { g.fail = PKGFAILSETUP; g.state = PKGDONE; stopped = true; - } else if (!pkgcombined(g, srcs)) { - g.fail = PKGFAILCOMPOSE; - g.state = PKGDONE; } else { pkgstartbuild(g, builder, includes, &handles[launched]); diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index 374456fc..bac8195f 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -353,21 +353,20 @@ fn locateimport(dirs: *u8, name: *u8, namelen: u64, return nil; }; -// Go's contract: only *_test.ww is a test source. A line-leading @test -// declaration anywhere else would be silently dropped by a non-T build -// (#6, the Hare model), so directory enumeration rejects it loudly. -// This is the wwstage twin of cmd/ww/main.c:file_has_line_test. -fn dirfileattest(dirpath: *u8, name: *u8) bool = { +// Go's contract: only *_test.ww is a test source. Ask the compiler parser, +// rather than a textual attribute scan, whether a production source contains +// @test; otherwise valid whitespace/comments could silently drop a test. +fn dirfileattest(dirpath: *u8, name: *u8) i32 = { let path: *u8 = joinpath(dirpath, name); let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32); - if (fd < 0) { return false; }; + if (fd < 0) { return -1; }; let sr: (i64 | os.oserror) = os.filesize(fd); let n: i64 = -1i64; match (sr) { case let v: i64 => n = v; - case let e: os.oserror => { os.close(fd); return false; }; + case let e: os.oserror => { os.close(fd); return -1; }; }; - if (n <= 0i64) { os.close(fd); return false; }; + if (n < 0i64) { os.close(fd); return -1; }; let b: []u8 = alloc([], n: u64)!; b.len = n: i32; let rr: (i64 | os.oserror) = os.readall(fd, b.ptr, n: u64); @@ -375,29 +374,55 @@ fn dirfileattest(dirpath: *u8, name: *u8) bool = { let got: i64 = -1i64; match (rr) { case let v: i64 => got = v; - case let e: os.oserror => return false; + case let e: os.oserror => return -1; }; - if (got != n) { return false; }; - let i: i32 = 0; - for (i < b.len) { - for (i < b.len && (b[i] == ' ' || b[i] == '\t' - || b[i] == '\r')) { i += 1; }; - if (i + 5 < b.len && b[i] == '@' && b[i + 1] == 't' - && b[i + 2] == 'e' && b[i + 3] == 's' - && b[i + 4] == 't' - && (b[i + 5] == ' ' || b[i + 5] == '\t')) { - return true; + if (got != n) { return -1; }; + // Preserve the directory loader's normalized package-clause diagnostic + // before the full parser performs language-level recovery. This is also + // the C/WW parity boundary for malformed clauses. + let il: syntax.lex; + syntax.lexinit(&il, pathstr(path), b.ptr, n: u64); + let ips: syntax.parser; + syntax.parserinit(&ips, &il); + let imports: *syntax.node = syntax.parseimports(&ips); + if (il.errs > 0 || ips.errs > 0) { return -1; }; + if (imports.nmod.len == 0) { + cerrpos(pathstr(path), 1, 1); + cerr(": error: invalid or missing package clause\n"); + return -1; + }; + let l: syntax.lex; + syntax.lexinit(&l, pathstr(path), b.ptr, n: u64); + let ps: syntax.parser; + syntax.parserinit(&ps, &l); + let f: *syntax.node = syntax.parsefile(&ps); + if (l.errs > 0 || ps.errs > 0) { return -1; }; + let d: *syntax.node = f.list; + for (d != nil) { + if (d.kind == syntax.nkind.N_FNDECL) { + let at: *syntax.node = d.attr; + for (at != nil) { + if (at.kind == syntax.nkind.N_ATTR + && syntax.streq(at.str, "test")) { + return 1; + }; + at = at.next; + }; }; - for (i < b.len && b[i] != '\n') { i += 1; }; - if (i < b.len) { i += 1; }; + d = d.next; }; - return false; + return 0; }; -// Classify a directory entry for production enumeration: 1 keep, -// 0 skip (non-source or *_test.ww), -1 @test outside *_test.ww -// (caller diagnoses and fails). -fn dirfileclass(dirpath: *u8, name: *u8, nlen: u64) i32 = { +def SEP_VARIANT_PRODUCTION: i32 = 0; +def SEP_VARIANT_SAME_TEST: i32 = 1; +def SEP_VARIANT_EXTERNAL: i32 = 2; +def SEP_TEST_SUPPORT_MODULE: str = "__wwtest"; + +// Classify a selected directory entry: 1 production, 2 test, 0 skipped, +// -1 @test outside *_test.ww, -2 non-regular source. +fn dirfileclass(dirpath: *u8, name: *u8, nlen: u64, + variant: i32) i32 = { // nlen<=3 guard kept: a bare ".ww" (len 3) is rejected here but // would pass strings.hassuffix(".ww"); preserves cstage parity. if (nlen <= 3u64) { return 0; }; @@ -405,7 +430,9 @@ fn dirfileclass(dirpath: *u8, name: *u8, nlen: u64) i32 = { s.ptr = name; s.len = nlen: i32; if (!strings.hassuffix(s, ".ww")) { return 0; }; - if (strings.hassuffix(s, "_test.ww")) { return 0; }; + let istest: bool = strings.hassuffix(s, "_test.ww"); + if (istest && variant == SEP_VARIANT_PRODUCTION) { return 0; }; + if (!istest && variant == SEP_VARIANT_EXTERNAL) { return 0; }; let source: *u8 = joinpath(dirpath, name); let fi: os.filestat; let sr: (void | os.oserror) = os.lstat(&fi, pathstr(source)); @@ -422,10 +449,45 @@ fn dirfileclass(dirpath: *u8, name: *u8, nlen: u64) i32 = { cerr(": package source is not a regular file\n"); return -2; }; - if (dirfileattest(dirpath, name)) { return -1; }; + if (!istest) { + let attest: i32 = dirfileattest(dirpath, name); + if (attest < 0) { return -2; }; + if (attest > 0) { return -1; }; + }; + if (istest) { return 2; }; return 1; }; +fn dirpackagename(path: *u8) *u8 = { + let view: str; + view.ptr = path; + view.len = cstrlen(path): i32; + let bufp: *u8; + let blen: u64; + bufp, blen = slurp(path); + if (bufp == nil) { + cerr("ww: cannot read source\n"); + return nil; + }; + let l: syntax.lex; + syntax.lexinit(&l, view, bufp, blen); + let ps: syntax.parser; + syntax.parserinit(&ps, &l); + let imports: *syntax.node = syntax.parseimports(&ps); + if (l.errs > 0 || ps.errs > 0) { return nil; }; + if (imports.nmod.len == 0) { + cerrpos(view, 1, 1); + cerr(": error: invalid or missing package clause\n"); + return nil; + }; + if (imports.nmod.len >= 256) { + cerrpos(imports.file, imports.line, imports.col); + cerr(": error: package name is too long\n"); + return nil; + }; + return arenadupcstr(imports.nmod.ptr, imports.nmod.len: u64); +}; + // Rule-10 byte-id requires cstage and wwstage sort the same way; // memcmp is the locale-independent total order (mirrors // ref/hare/sort/cmp/cmp.ha strs). @@ -445,12 +507,10 @@ fn bytecmp(a: *u8, alen: u64, b: *u8, blen: u64) i32 = { return 0; }; -// enumeratedir — list production *.ww paths of `dirpath` (less *_test.ww -// test sources), byte-sort. A line-leading @test in any other source is -// diagnosed here and returns -2. Returns one exact pointer array of -// NUL-terminated full paths. This is the sole directory-membership -// discovery path; the owning seppkg retains the list. -fn enumeratedir(dirpath: *u8) (**u8, i32) = { +// Enumerate a production, same-test, or external-test directory variant. +// Production files precede matching test files; each partition is byte-sorted. +fn enumeratedir(dirpath: *u8, variant: i32, + testpackage: *u8) (**u8, i32) = { let fd: i32 = os.open(pathstr(dirpath), os.flag.RDONLY, 0i32); if (fd < 0) { return nil: **u8, -1; }; // #65: grow-dynamic (mirror cstage enumerate_dir_ww realloc-doubling, @@ -460,6 +520,7 @@ fn enumeratedir(dirpath: *u8) (**u8, i32) = { let cap: i32 = 8; let names: []*u8 = alloc([], cap: u64)!; let nlens: []u64 = alloc([], cap: u64)!; + let kinds: []i32 = alloc([], cap: u64)!; let n: i32 = 0; let buf: []u8 = alloc([], 8192u64)!; buf.len = 8192; @@ -473,7 +534,7 @@ fn enumeratedir(dirpath: *u8) (**u8, i32) = { let reclen: u64 = blo + (bhi * 256u64); let nm: *u8 = buf.ptr + off + 19u64; let nl: u64 = cstrlen(nm); - let cls: i32 = dirfileclass(dirpath, nm, nl); + let cls: i32 = dirfileclass(dirpath, nm, nl, variant); if (cls == -1) { cerr("ww: "); cerr(pathstr(joinpath(dirpath, nm))); @@ -486,23 +547,38 @@ fn enumeratedir(dirpath: *u8) (**u8, i32) = { return nil: **u8, -2; }; if (cls > 0) { + let full: *u8 = joinpath(dirpath, nm); + if (cls == 2) { + let pn: *u8 = dirpackagename(full); + if (pn == nil) { + os.close(fd); + return nil: **u8, -2; + }; + if (testpackage == nil || !cstreq(pn, testpackage)) { + off += reclen; + continue; + }; + }; if (n >= cap) { let ncap: i32 = cap * 2; let nn: []*u8 = alloc([], ncap: u64)!; let nl2: []u64 = alloc([], ncap: u64)!; + let nk: []i32 = alloc([], ncap: u64)!; let k: i32 = 0; for (k < n) { nn[k] = names[k]; nl2[k] = nlens[k]; + nk[k] = kinds[k]; k += 1; }; names = nn; nlens = nl2; + kinds = nk; cap = ncap; }; - let full: *u8 = joinpath(dirpath, nm); names[n] = full; nlens[n] = cstrlen(full); + kinds[n] = cls; n += 1; }; off += reclen; @@ -524,8 +600,12 @@ fn enumeratedir(dirpath: *u8) (**u8, i32) = { for (i < n) { let j: i32 = i; for (j > 0) { - if (bytecmp(names[j - 1], nlens[j - 1], - names[j], nlens[j]) <= 0) { j = 0; } + let c: i32 = kinds[j - 1] - kinds[j]; + if (c == 0) { + c = bytecmp(names[j - 1], nlens[j - 1], + names[j], nlens[j]); + }; + if (c <= 0) { j = 0; } else { let t: *u8 = names[j]; names[j] = names[j - 1]; @@ -533,6 +613,9 @@ fn enumeratedir(dirpath: *u8) (**u8, i32) = { let tl: u64 = nlens[j]; nlens[j] = nlens[j - 1]; nlens[j - 1] = tl; + let tk: i32 = kinds[j]; + kinds[j] = kinds[j - 1]; + kinds[j - 1] = tk; j -= 1; }; }; @@ -541,6 +624,7 @@ fn enumeratedir(dirpath: *u8) (**u8, i32) = { if (n == 0) { os.free(names.ptr: *void, (cap: u64) * (size(*u8): u64)); os.free(nlens.ptr: *void, (cap: u64) * (size(u64): u64)); + os.free(kinds.ptr: *void, (cap: u64) * (size(i32): u64)); return nil: **u8, 0; }; let exact: []*u8 = alloc([], n: u64)!; @@ -551,6 +635,7 @@ fn enumeratedir(dirpath: *u8) (**u8, i32) = { // shape correct for the driver's allocations. os.free(names.ptr: *void, (cap: u64) * (size(*u8): u64)); os.free(nlens.ptr: *void, (cap: u64) * (size(u64): u64)); + os.free(kinds.ptr: *void, (cap: u64) * (size(i32): u64)); return exact.ptr, n; }; @@ -637,9 +722,12 @@ type seppkg = struct { path: *u8, // dotted import path, NUL-term; root path[0]==0 entry: *u8, // resolved package dir (or file, file root), NUL-term name: *u8, // validated declared name; directory packages only - sources: **u8, // owned, byte-sorted production paths; dirs only + testpackage: *u8, + sources: **u8, // owned, byte-sorted selected paths; dirs only nsources: i32, isdir: i32, + variant: i32, + testsupport: bool, deps: []i32, // direct-dep indices into sepgraph.pkg ndeps: i32, color: i32, // tri-color DFS: 0 white, 1 gray, 2 black @@ -650,15 +738,26 @@ type sepgraph = struct { n: i32, }; -fn sepfindoradd(g: *sepgraph, path: *u8, entry: *u8, isdir: i32) i32 = { +fn sepfindoraddvariant(g: *sepgraph, path: *u8, entry: *u8, + isdir: i32, variant: i32, testpackage: *u8) i32 = { if (cstrlen(path) >= 256u64) { cerr("ww: package path is too long (limit 255 bytes)\n"); return -1; }; let i: i32 = 0; for (i < g.n) { + let testproductionpair: bool = i == 0 + && g.pkg[i].variant != SEP_VARIANT_PRODUCTION + && variant == SEP_VARIANT_PRODUCTION && isdir != 0 + && g.pkg[i].isdir != 0 + && os.samefile(pathstr(g.pkg[i].entry), pathstr(entry)); if (cstreq(g.pkg[i].path, path)) { - if (!os.samefile(pathstr(g.pkg[i].entry), pathstr(entry))) { + if (testproductionpair) { + i += 1; + continue; + }; + if (!os.samefile(pathstr(g.pkg[i].entry), pathstr(entry)) + || g.pkg[i].variant != variant) { cerr("ww: package "); cerr(pathstr(path)); cerr(" resolves to more than one location\n"); return -1; @@ -666,6 +765,10 @@ fn sepfindoradd(g: *sepgraph, path: *u8, entry: *u8, isdir: i32) i32 = { return i; }; if (os.samefile(pathstr(g.pkg[i].entry), pathstr(entry))) { + if (testproductionpair) { + i += 1; + continue; + }; cerr("ww: package directory "); cerr(pathstr(entry)); cerr(" has identities "); if (g.pkg[i].path[0u64] == 0u8) { cerr("(root)"); } @@ -687,9 +790,16 @@ fn sepfindoradd(g: *sepgraph, path: *u8, entry: *u8, isdir: i32) i32 = { g.pkg[g.n].path = arenadupcstr(path, plen); g.pkg[g.n].entry = arenadupcstr(entry, elen); g.pkg[g.n].name = nil; + g.pkg[g.n].testpackage = nil; + if (testpackage != nil) { + g.pkg[g.n].testpackage = arenadupcstr(testpackage, + cstrlen(testpackage)); + }; g.pkg[g.n].sources = nil; g.pkg[g.n].nsources = 0; g.pkg[g.n].isdir = isdir; + g.pkg[g.n].variant = variant; + g.pkg[g.n].testsupport = false; let dslot: []i32 = alloc([], SEP_MAXPKG: u64)!; dslot.len = SEP_MAXPKG; g.pkg[g.n].deps = dslot; @@ -700,6 +810,11 @@ fn sepfindoradd(g: *sepgraph, path: *u8, entry: *u8, isdir: i32) i32 = { return r; }; +fn sepfindoradd(g: *sepgraph, path: *u8, entry: *u8, isdir: i32) i32 = { + return sepfindoraddvariant(g, path, entry, isdir, + SEP_VARIANT_PRODUCTION, nil); +}; + // Release the package-owned directory-membership lists through one graph // cleanup function. rt_free is a no-op in today's no-free runtime, but this // records the same ownership boundary as the C bootstrap twin. @@ -741,6 +856,36 @@ fn sepfname(g: *sepgraph, pi: i32, scratch: *u8, suffix: str) *u8 = { return buf.ptr; }; +fn sepexternalname(pkg: *seppkg, path: *u8, n: u64, + leafonly: bool) bool = { + if (pkg.variant != SEP_VARIANT_EXTERNAL || pkg.testpackage == nil) { + return false; + }; + let begin: u64 = 0u64; + if (leafonly) { + let i: u64 = 0u64; + for (i < n) { + if (path[i] == '.') { begin = i + 1u64; }; + i += 1u64; + }; + }; + let leafn: u64 = n - begin; + let tn: u64 = cstrlen(pkg.testpackage); + if (tn != leafn + 5u64) { return false; }; + if (bytecmp(pkg.testpackage, leafn, path + begin, leafn) != 0) { + return false; + }; + return pkg.testpackage[leafn] == '_' + && pkg.testpackage[leafn + 1u64] == 't' + && pkg.testpackage[leafn + 2u64] == 'e' + && pkg.testpackage[leafn + 3u64] == 's' + && pkg.testpackage[leafn + 4u64] == 't'; +}; + +fn sepexternalproduction(pkg: *seppkg, path: *u8, n: u64) bool = { + return sepexternalname(pkg, path, n, false); +}; + // Scan one already-selected source file for its leading package clause // (when it is an owned directory source) and top-level imports. A DIRECTORY // import is a package boundary: add as a direct dep of pi. A FILE import is an @@ -776,6 +921,11 @@ fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8, && (ownedsource != 0 || g.pkg[pi].name == nil)) { let declared: *u8 = imports.nmod.ptr; let declaredn: u64 = imports.nmod.len: u64; + if (declaredn >= 256u64) { + cerrpos(imports.file, imports.line, imports.col); + cerr(": error: package name is too long\n"); + return -1; + }; if (g.pkg[pi].name == nil) { g.pkg[pi].name = arenadupcstr(declared, declaredn); } else { if (bytecmp(g.pkg[pi].name, cstrlen(g.pkg[pi].name), @@ -852,10 +1002,24 @@ fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8, return -1; }; let isdir: i32 = 0; - let ipath: *u8 = locateimport(searchpath, idp, idn, &isdir); + let externalproduction: bool = sepexternalproduction( + &g.pkg[pi], idp, idn); + let ipath: *u8 = nil; + if (externalproduction) { + ipath = g.pkg[pi].entry; + isdir = 1; + } else { + ipath = locateimport(searchpath, idp, idn, &isdir); + }; if (ipath != nil) { if (isdir != 0) { - if (os.samefile(pathstr(ipath), pathstr(g.pkg[pi].entry))) { + let self: bool = os.samefile(pathstr(ipath), + pathstr(g.pkg[pi].entry)); + if (self && sepexternalname(&g.pkg[pi], idp, idn, true)) { + externalproduction = true; + }; + if (self + && !externalproduction) { cerrpos(u.file, u.line, u.col); cerr(": error: self-import: package '"); if (g.pkg[pi].path[0u64] != 0u8) { @@ -931,7 +1095,8 @@ fn seploadpkg(g: *sepgraph, pi: i32, searchpath: *u8) i32 = { if (g.pkg[pi].isdir != 0) { let sources: **u8; let nsources: i32; - sources, nsources = enumeratedir(g.pkg[pi].entry); + sources, nsources = enumeratedir(g.pkg[pi].entry, + g.pkg[pi].variant, g.pkg[pi].testpackage); g.pkg[pi].sources = sources; g.pkg[pi].nsources = nsources; if (g.pkg[pi].nsources == -2) { @@ -956,7 +1121,8 @@ fn seploadpkg(g: *sepgraph, pi: i32, searchpath: *u8) i32 = { }; i += 1; }; - if (rc == 0 && g.pkg[pi].path[0u64] != 0u8) { + if (rc == 0 && g.pkg[pi].path[0u64] != 0u8 + && !g.pkg[pi].testsupport) { let plen: u64 = cstrlen(g.pkg[pi].path); let leaf: *u8 = g.pkg[pi].path; let j: u64 = 0u64; @@ -1056,7 +1222,7 @@ fn sepwriteall(fd: i32, buf: *u8, n: u64) bool = { }; fn sepemitbody(fd: i32, path: *u8, visit: *expctx, searchpath: *u8, - modpath: *u8) i32 = { + modpath: *u8, pkg: *seppkg) i32 = { let pview: str; pview.ptr = path; pview.len = cstrlen(path): i32; @@ -1114,12 +1280,16 @@ fn sepemitbody(fd: i32, path: *u8, visit: *expctx, searchpath: *u8, u = uses[ui]; let idp: *u8 = u.usepath.ptr; let idn: u64 = u.usepath.len: u64; + if (sepexternalproduction(pkg, idp, idn)) { + ui += 1; + continue; + }; let isdir: i32 = 0; let ipath: *u8 = locateimport(searchpath, idp, idn, &isdir); if (ipath != nil) { if (isdir == 0) { if (sepemitbody(fd, ipath, visit, searchpath, - modpath) < 0) { return -1; }; + modpath, pkg) < 0) { return -1; }; }; }; ui += 1; @@ -1195,12 +1365,12 @@ fn sepcomposeunit(g: *sepgraph, pi: i32, scratch: *u8, let i: i32 = 0; for (i < g.pkg[pi].nsources && bodyrc == 0) { bodyrc = sepemitbody(u, g.pkg[pi].sources[i], &bv, searchpath, - g.pkg[pi].path); + g.pkg[pi].path, &g.pkg[pi]); i += 1; }; } else { bodyrc = sepemitbody(u, g.pkg[pi].entry, &bv, searchpath, - g.pkg[pi].path); + g.pkg[pi].path, &g.pkg[pi]); }; if (os.close(u) != 0) { cerr("ww: cannot close package unit\n"); @@ -1405,14 +1575,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 2 mode test asm 1\n"; + return "ww workdir fmt 3 mode test asm 1\n"; }; - return "ww workdir fmt 2 mode test asm 0\n"; + return "ww workdir fmt 3 mode test asm 0\n"; }; if (emitasm != 0) { - return "ww workdir fmt 2 mode build asm 1\n"; + return "ww workdir fmt 3 mode build asm 1\n"; }; - return "ww workdir fmt 2 mode build asm 0\n"; + return "ww workdir fmt 3 mode build asm 0\n"; }; fn stampmatches(path: *u8, want: str) bool = { @@ -1460,7 +1630,8 @@ fn cerrpath(head: str, path: *u8, tail: str) void = { fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, rootidentity: *u8, out: *u8, objstem: *u8, incs: *u8, lf: *lflags, packageonly: i32, istest: i32, - emitasm: i32, workdir: *u8, scratchout: **u8, + rootvariant: i32, testpackage: *u8, emitasm: i32, + workdir: *u8, scratchout: **u8, graphout: **sepgraph) i32 = { let c6: *u8 = joinpathlit(selfdir, "w6c_ww"); let a6: *u8 = joinpathlit(selfdir, "w6a_ww"); @@ -1605,34 +1776,65 @@ 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 root: i32 = sepfindoradd(g, rootpath, src, entryisdir); + let root: i32 = sepfindoraddvariant(g, rootpath, src, entryisdir, + rootvariant, testpackage); if (root < 0) { return 1; }; - // #79 (-T): lib/test is the synth main's `test.run` callee but @test - // files never `import test;`. Inject it as a direct dep of the root so - // seploadpkg pulls test + its transitive deps; the producer adds -T to - // the root and `test.run` links against test's `.a` — via the - // sepscanfile dedup-guarded dep append. + 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`; + // when user source occupies that identity, the reserved graph alias keeps + // it distinct. The linker receives the same support archive closure. if (istest != 0) { let td: i32 = 0; - let tp: *u8 = locateimport(searchpath.ptr, "test".ptr, "test".len: u64, &td); + let tp: *u8 = locateimport(dotdotlib.ptr, "test".ptr, + "test".len: u64, &td); if (tp != nil) { - let ti: i32 = sepfindoradd(g, "test\0".ptr, tp, td); - if (ti < 0) { return 1; }; - 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; + let rootissupport: bool = entryisdir != 0 + && os.samefile(pathstr(tp), pathstr(src)); + let collision: bool = false; + if (!rootissupport && testpackage != nil) { + collision = cstreqlit(testpackage, "test") + || cstreqlit(testpackage, "test_test"); }; - if (!seen) { - if (g.pkg[root].ndeps < SEP_MAXPKG) { - g.pkg[root].deps[g.pkg[root].ndeps] = ti; - g.pkg[root].ndeps += 1; + if (!rootissupport && !collision) { + let ud: i32 = 0; + let up: *u8 = locateimport(searchpath.ptr, "test".ptr, + "test".len: u64, &ud); + if (up != nil && !os.samefile(pathstr(tp), pathstr(up))) { + collision = true; + }; + }; + if (collision) { testsupportmodule = SEP_TEST_SUPPORT_MODULE; }; + // A same-test build of the runtime package already owns run + // and its source imports. An external test still needs the + // colocated production node, also its one support dependency. + if (!rootissupport || rootvariant == SEP_VARIANT_EXTERNAL) { + let ti: i32 = sepfindoradd(g, + testsupportmodule.ptr, tp, td); + 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; + }; }; }; }; }; if (seploadpkg(g, root, searchpath.ptr) < 0) { return 1; }; + if (rootvariant != SEP_VARIANT_PRODUCTION + && (testpackage == nil || !cstreq(g.pkg[root].name, testpackage))) { + cerr("ww: package-test selector does not match loaded package\n"); + return 1; + }; let rootpackage: bool = packageonly != 0; if (rootpackage && cstreqlit(g.pkg[root].name, "main")) { cerr("ww: -p requires a non-main package\n"); @@ -1719,11 +1921,21 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, // #79: the root carries -T under `ww test` so w6c // synthesizes the test main; deps never get -T. let roott: bool = (pi == root) && (istest != 0); + let supportt: bool = g.pkg[pi].testsupport; let alen: u64 = 8u64; - if (!needsexport) { alen = 6u64; if (roott) { alen = 7u64; }; }; + if (!needsexport) { alen = 6u64; if (roott) { alen = 9u64; }; }; + if (supportt) { alen += 2u64; }; let argv: []str = alloc([], alen)!; append(argv, "w6c"); - if (roott) { append(argv, "-T"); }; + if (roott) { + append(argv, "-T"); + append(argv, "--test-support-module"); + append(argv, testsupportmodule); + }; + if (supportt) { + append(argv, "--test-support-module"); + append(argv, testsupportmodule); + }; append(argv, "-c"); if (needsexport) { append(argv, "-I"); @@ -1859,8 +2071,11 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, }; // Reverse-topo link: root `.o` first (order[norder-1]), dependency `.a` - // files after, then libwwrt.a (which still - // selectively pulls only the runtime members a live undef needs). + // files after, then libwwrt.a (which still selectively pulls only the + // runtime members a live undef needs). A same-test root already contains + // its production sources; if that production variant is also reached + // through the runtime closure, retain its dependencies but omit its + // duplicate archive. // argv: 3 fixed (w6l,-o,out) + one root object/archive per package // + 1 libwwrt + 2*nlibdirs + 2*nlibs + 1 nil. let nldirs: i32 = 0; @@ -1882,10 +2097,19 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, let pos: i32 = 3; let li: i32 = norder - 1; for (li >= 0) { + let pi: i32 = order[li]; + if (g.pkg[root].variant == SEP_VARIANT_SAME_TEST + && pi != root + && g.pkg[pi].variant == SEP_VARIANT_PRODUCTION + && os.samefile(pathstr(g.pkg[pi].entry), + pathstr(g.pkg[root].entry))) { + li -= 1; + continue; + }; // root: positional `.o` (force-load); deps: `.a` (selective). let suf: str = ".a"; - if (order[li] == root) { suf = ".o"; }; - largv[pos] = sepfname(g, order[li], scratch, suf); + if (pi == root) { suf = ".o"; }; + largv[pos] = sepfname(g, pi, scratch, suf); pos += 1; li -= 1; }; @@ -1934,12 +2158,14 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, rootidentity: *u8, out: *u8, objstem: *u8, incs: *u8, lf: *lflags, packageonly: i32, istest: i32, - emitasm: i32, keepscratch: i32, workdir: *u8) i32 = { + rootvariant: i32, testpackage: *u8, emitasm: i32, + keepscratch: i32, workdir: *u8) i32 = { let scratch: *u8 = nil; let g: *sepgraph = nil; let r: i32 = buildonesepimpl(selfdir, src, entryisdir, rootidentity, out, objstem, - incs, lf, packageonly, istest, emitasm, workdir, &scratch, &g); + incs, lf, packageonly, istest, rootvariant, testpackage, + emitasm, workdir, &scratch, &g); sepgraphfree(g); if (keepscratch == 0 && scratch != nil) { if (cstrendswithlit(scratch, ".sepwork")) { @@ -2248,7 +2474,8 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { if (packageonly != 0 && !requestedliteral) { rootidentity = src; }; return buildonesep(selfdir, resolved, isdir, rootidentity, out, objstem, incs.ptr, &lf, - packageonly, 0i32, emitasm, 1i32, workdir); + packageonly, 0i32, SEP_VARIANT_PRODUCTION, nil, + emitasm, 1i32, workdir); }; // Format the owned driver workspace /tmp/ into buf. Pid is @@ -2413,7 +2640,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { lf.nlibs = nlibs; // The freshly acquired directory owns both main and main.sepwork. if (buildonesep(selfdir, resolved, isdir, nil, outp, outp, incs.ptr, &lf, - 0i32, 0i32, 0i32, 0i32, nil) != 0) { + 0i32, 0i32, SEP_VARIANT_PRODUCTION, nil, 0i32, 0i32, nil) != 0) { let cleanrc: i32 = os.remove(pathstr(outp)); if (cleanrc != 0 && cleanrc != -2i32) { cerr("ww: cannot remove temporary output\n"); @@ -2503,7 +2730,7 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, let keep: i32 = 0; if (outstem != nil) { keep = 1; }; let bres: i32 = buildonesep(selfdir, src, 0, nil, outp, objstem, incs, &lf, - 0i32, 1i32, emitasm, keep, workdir); + 0i32, 1i32, SEP_VARIANT_PRODUCTION, nil, emitasm, keep, workdir); if (bres != 0) { if (owntmp) { let cleanrc: i32 = os.remove(pathstr(outp)); @@ -2590,6 +2817,8 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { let emitasm: i32 = 0; let outstem: *u8 = nil; let workdir: *u8 = nil; + let packagetestkind: *u8 = nil; + let packagetestname: *u8 = nil; let packageopts: bool = false; let afterdash: bool = false; let i: i32 = start; @@ -2600,6 +2829,27 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { if (cstreqlit(p, "--")) { packageopts = true; afterdash = true; i += 1; continue; }; + if (cstreqlit(p, "--ww-package-test")) { + if (i + 2 >= argc || packagetestkind != nil) { + cerr("ww test: --ww-package-test needs kind and package\n"); + return 2; + }; + packagetestkind = argv[i + 1]; + packagetestname = argv[i + 2]; + let pn: u64 = cstrlen(packagetestname); + if ((!cstreqlit(packagetestkind, "same") + && !cstreqlit(packagetestkind, "external")) + || pn == 0u64 || pn >= 256u64 + || (cstreqlit(packagetestkind, "external") + && (pn <= 5u64 + || !cstrendswithlit(packagetestname, + "_test")))) { + cerr("ww test: invalid --ww-package-test variant\n"); + return 2; + }; + i += 3; + continue; + }; if (p[1u64] == 73u8) { // '-I' let dir: *u8 = nil; if (p[2u64] != 0u8) { @@ -2679,6 +2929,10 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { cerr("ww test: -S needs -o\n"); return 2; }; + if (packagetestkind != nil && packageopts) { + cerr("ww test: package-test variant rejects package options\n"); + return 2; + }; // Go's ./... form: a trailing "..." element is a package-tree // request for the coordinator, never a literal path — recognized @@ -2692,6 +2946,10 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { && target[tlen - 1u64] == '.'; }; if (istree) { + if (packagetestkind != nil) { + cerr("ww test: package-test variant needs one directory\n"); + return 2; + }; if (emitasm != 0) { cerr("ww test: -S needs a single test file\n"); return 2; @@ -2733,6 +2991,10 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { }; }; if (isdir == 0) { + if (packagetestkind != nil) { + cerr("ww test: package-test variant needs one directory\n"); + return 2; + }; if (packageopts) { cerr("ww test: package options need a directory\n"); return 2; }; @@ -2752,6 +3014,22 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { cerr("ww test: pattern needs a single test file\n"); return 2; }; + if (packagetestkind != nil) { + if (compileonly == 0 || outstem == nil) { + cerr("ww test: package-test variant needs -c -o\n"); + return 2; + }; + let variant: i32 = SEP_VARIANT_EXTERNAL; + if (cstreqlit(packagetestkind, "same")) { + variant = SEP_VARIANT_SAME_TEST; + }; + let lf: lflags; + lf.libdirs = nil; lf.nlibdirs = 0; + lf.libs = nil; lf.nlibs = 0; + return buildonesep(selfdir, resolved, 1, nil, outstem, outstem, + incs.ptr, &lf, 0i32, 1i32, variant, packagetestname, + 0i32, 1i32, workdir); + }; let replacement: *u8 = nil; if (resolved != target) { replacement = resolved; }; return execpackagetests(selfdir, argv, argc, start, targetindex,