cmd: build directory package test variants

This commit is contained in:
2026-08-12 03:46:47 +09:00
parent e59881cb30
commit 9862f0c05f
4 changed files with 914 additions and 270 deletions

View File

@@ -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;
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;
}
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;
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')) {
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;
}
}
fclose(f);
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 : &prod;
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 (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 (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);
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 -I %s -o %s %s",
c6, cw, cs, cu);
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 <stem> 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);
}

View File

@@ -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.

View File

@@ -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; };
};
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;
};
};
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; };
};
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]);

View File

@@ -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;
};
for (i < b.len && b[i] != '\n') { i += 1; };
if (i < b.len) { i += 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;
};
return false;
at = at.next;
};
};
d = d.next;
};
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,19 +1776,44 @@ 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);
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 (!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) {
@@ -1632,7 +1828,13 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
};
};
};
};
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/<prefix><pid> 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,