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); return strcmp(sa, sb);
} }
/* Go's contract: only *_test.ww is a test source. A line-leading @test static int sep_slurp(const char*, char**, u64*);
* declaration anywhere else would be silently dropped by a non-T build
* (#6, the Hare model), so directory enumeration rejects it loudly. */ /* 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 static int
file_has_line_test(const char *path) source_has_test_decl(const char *path)
{ {
char line[4096]; char *buf;
FILE *f = fopen(path, "rb"); u64 len;
if (f == NULL) return 0; if (sep_slurp(path, &buf, &len) < 0) return -1;
int found = 0; /* Keep directory-loader package-clause diagnostics stable. The full
while (fgets(line, sizeof line, f) != NULL) { * parser reports its language-level "missing package clause" first;
char *p = line; * the imports-only pass owns the package-loader wording and also avoids
while (*p == ' ' || *p == '\t' || *p == '\r') p++; * stage-specific recovery diagnostics for a malformed clause. */
if (strncmp(p, "@test", 5) == 0 Arena *ia = newarena();
&& (p[5] == ' ' || p[5] == '\t')) { Lex il;
found = 1; Parser ip;
break; 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; return found;
} }
/* A line-leading @test in a production source is diagnosed here and #define SEP_VARIANT_PRODUCTION 0
* returns -2. This is the sole directory-membership discovery path; the #define SEP_VARIANT_SAME_TEST 1
* owning seppkg retains the returned list. */ #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 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); DIR *d = opendir(dirpath);
if (d == NULL) { *out_files = NULL; return -1; } if (d == NULL) { *out_files = NULL; return -1; }
char **arr = NULL; char **prod = NULL, **tests = NULL;
int n = 0, cap = 0; int nprod = 0, capprod = 0, ntests = 0, captests = 0;
struct dirent *ent; struct dirent *ent;
while ((ent = readdir(d)) != NULL) { while ((ent = readdir(d)) != NULL) {
const char *nm = ent->d_name; const char *nm = ent->d_name;
size_t nl = strlen(nm); size_t nl = strlen(nm);
if (nl <= 3) continue; if (nl <= 3) continue;
if (strcmp(nm + nl - 3, ".ww") != 0) 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]; char path[2048];
snprintf(path, sizeof path, "%s/%s", dirpath, nm); snprintf(path, sizeof path, "%s/%s", dirpath, nm);
if (nl >= 8 && strcmp(nm + nl - 8, "_test.ww") == 0)
continue;
struct stat st; struct stat st;
if (lstat(path, &st) != 0 || !S_ISREG(st.st_mode)) { if (lstat(path, &st) != 0 || !S_ISREG(st.st_mode)) {
fprintf(stderr, fprintf(stderr,
"ww: %s: package source is not a regular file\n", path); "ww: %s: package source is not a regular file\n", path);
for (int i = 0; i < n; i++) free(arr[i]); source_list_free(prod, nprod);
free(arr); source_list_free(tests, ntests);
closedir(d); closedir(d);
*out_files = NULL; *out_files = NULL;
return -2; 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, fprintf(stderr,
"ww: %s: @test declaration outside *_test.ww\n", "ww: %s: @test declaration outside *_test.ww\n",
path); path);
for (int i = 0; i < n; i++) free(arr[i]); source_list_free(prod, nprod);
free(arr); source_list_free(tests, ntests);
closedir(d); closedir(d);
*out_files = NULL; *out_files = NULL;
return -2; return -2;
} }
if (n + 1 > cap) { if (is_test) {
cap = cap ? cap * 2 : 8; char package[256];
arr = realloc(arr, cap * sizeof *arr); 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); closedir(d);
if (n > 1) qsort(arr, n, sizeof *arr, strs_cmp); if (nprod > 1) qsort(prod, (size_t)nprod, sizeof *prod, strs_cmp);
*out_files = arr; if (ntests > 1) qsort(tests, (size_t)ntests, sizeof *tests, strs_cmp);
return n; 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). /* 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 entry[1024]; /* resolved package dir (or file, for a file root) */
char canon[1024]; /* canonical location; never package identity */ char canon[1024]; /* canonical location; never package identity */
char name[256]; /* validated declared name; directory packages only */ 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 nsources;
int is_dir; 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 deps[SEP_MAXPKG]; /* direct-dep indices into sepgraph.pkg */
int ndeps; int ndeps;
int color; /* tri-color DFS: 0 white, 1 gray, 2 black */ int color; /* tri-color DFS: 0 white, 1 gray, 2 black */
@@ -357,8 +507,8 @@ struct sepgraph {
}; };
static int static int
sep_find_or_add(struct sepgraph *g, const char *path, const char *entry, sep_find_or_add_variant(struct sepgraph *g, const char *path,
int is_dir) const char *entry, int is_dir, int variant, const char *test_package)
{ {
if (strlen(path) >= sizeof g->pkg[0].path) { if (strlen(path) >= sizeof g->pkg[0].path) {
fprintf(stderr, "ww: package path is too long (limit %zu bytes)\n", 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; return -1;
} }
for (int i = 0; i < g->n; i++) { 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].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, fprintf(stderr,
"ww: package %s resolves to both %s and %s\n", "ww: package %s resolves to both %s and %s\n",
path[0] ? path : "(root)", g->pkg[i].entry, 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; return i;
} }
if (strcmp(g->pkg[i].canon, canon) == 0) { 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, fprintf(stderr,
"ww: package directory %s has identities %s and %s\n", "ww: package directory %s has identities %s and %s\n",
entry, g->pkg[i].path[0] ? g->pkg[i].path : "(root)", 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); snprintf(p->canon, sizeof p->canon, "%s", canon);
free(canon); free(canon);
p->is_dir = is_dir; p->is_dir = is_dir;
p->variant = variant;
p->test_support = 0;
p->name[0] = '\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->sources = NULL;
p->nsources = 0; p->nsources = 0;
p->ndeps = 0; p->ndeps = 0;
@@ -417,6 +586,14 @@ sep_find_or_add(struct sepgraph *g, const char *path, const char *entry,
return g->n++; 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 /* Release the one package-owned directory-membership list. Every graph exit
* funnels through this function; regular-file nodes own no source list. */ * funnels through this function; regular-file nodes own no source list. */
static void static void
@@ -481,6 +658,30 @@ use_node_cmp(const void *a, const void *b)
return x->pos.col - y->pos.col; 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 /* 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`. 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 * `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]; char ipath[1024];
int is_dir = 0; int is_dir = 0;
if (!locate_import(searchpath, path_form, ipath, sizeof ipath, int external_production = sep_external_production_import(
&is_dir)) { &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 *dot = strrchr(name, '.');
const char *leaf = dot ? dot + 1 : name; const char *leaf = dot ? dot + 1 : name;
int inline_package = 0; 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; int self = strcmp(canon, g->pkg[pi].canon) == 0;
free(canon); 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] const char *owner = g->pkg[pi].path[0]
? g->pkg[pi].path : g->pkg[pi].name; ? g->pkg[pi].path : g->pkg[pi].name;
errorf(u->pos, "self-import: package '%s' cannot import itself", 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}; struct ImportSet fv = {0};
int rc = 0; int rc = 0;
if (g->pkg[pi].is_dir) { 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].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) { if (g->pkg[pi].nsources == -2) {
rc = -1; /* diagnosed in enumerate_dir_ww */ rc = -1; /* diagnosed in enumerate_dir_ww */
} else if (g->pkg[pi].nsources < 0) { } 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++) for (int i = 0; i < g->pkg[pi].nsources && rc == 0; i++)
rc = sep_scan_file(g, pi, g->pkg[pi].sources[i], rc = sep_scan_file(g, pi, g->pkg[pi].sources[i],
searchpath, &fv, 1); 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 *dot = strrchr(g->pkg[pi].path, '.');
const char *leaf = dot ? dot + 1 : g->pkg[pi].path; const char *leaf = dot ? dot + 1 : g->pkg[pi].path;
if (strcmp(g->pkg[pi].name, leaf) != 0) { 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). */ * body); FILE imports fold in (intra-package split). */
static int static int
sep_emit_body(FILE *out, const char *path, struct ImportSet *visited, 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; if (import_seen(visited, path)) return 0;
import_add(visited, path); 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++) { for (int i = 0; i < nuse; i++) {
Node *u = uses[i]; Node *u = uses[i];
const char *name = u->usepath ? u->usepath : u->str; const char *name = u->usepath ? u->usepath : u->str;
if (sep_external_production_import(pkg, name))
continue;
char path_form[1024]; char path_form[1024];
import_path_form(name, path_form, sizeof path_form); import_path_form(name, path_form, sizeof path_form);
char ipath[1024]; char ipath[1024];
@@ -796,7 +1014,7 @@ sep_emit_body(FILE *out, const char *path, struct ImportSet *visited,
&is_dir)) &is_dir))
continue; continue;
if (!is_dir && sep_emit_body(out, ipath, visited, searchpath, if (!is_dir && sep_emit_body(out, ipath, visited, searchpath,
modpath) < 0) { modpath, pkg) < 0) {
free(uses); free(uses);
freearena(a); freearena(a);
free(buf); free(buf);
@@ -866,10 +1084,10 @@ sep_compose_unit(struct sepgraph *g, int pi, const char *scratch,
if (g->pkg[pi].is_dir) { if (g->pkg[pi].is_dir) {
for (int i = 0; i < g->pkg[pi].nsources && bodyrc == 0; i++) for (int i = 0; i < g->pkg[pi].nsources && bodyrc == 0; i++)
bodyrc = sep_emit_body(u, g->pkg[pi].sources[i], &bodyvisit, 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 { } else {
bodyrc = sep_emit_body(u, g->pkg[pi].entry, &bodyvisit, searchpath, 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]); for (int i = 0; i < bodyvisit.n; i++) free(bodyvisit.paths[i]);
free(bodyvisit.paths); free(bodyvisit.paths);
@@ -1027,7 +1245,7 @@ copy_file_atomic(const char *src, const char *dst)
static void static void
workdir_stamp_text(char *buf, size_t bufsz, int is_test, int emit_asm) 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); is_test ? "test" : "build", emit_asm);
} }
@@ -1041,7 +1259,8 @@ static int
build_one_sep_impl(const char *src, int entry_is_dir, build_one_sep_impl(const char *src, int entry_is_dir,
const char *root_identity, const char *out, const char *root_identity, const char *out,
const char *objstem, const char *extra_includes, const char *extra_libs, 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, const char *workdir, char *scratchout, size_t scratchoutsz,
struct sepgraph **graphout) 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 if (access("lib", 0) == 0) srcdir = "lib";
else srcdir = libdir; else srcdir = libdir;
} }
const char *toolsrcdir = srcdir;
char srcd[1024]; char srcd[1024];
if (entry_is_dir) { if (entry_is_dir) {
snprintf(srcd, sizeof srcd, "%s", src); 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; if (graphout) *graphout = g;
const char *rootpath = package_only && root_identity const char *rootpath = package_only && root_identity
? 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; if (root < 0) return 1;
/* #79 (-T): lib/test is the synth main's `test.run` callee but @test const char *test_support_module = "test";
* files never `import test;`. Inject it as a direct dep of the root so /* -T generates a dispatcher whose support qualifier is selected by the
* sep_load_pkg pulls test + its transitive deps; the producer adds -T * command. Represent that compiler-generated requirement as a direct root
* to the root and `test.run` links against test's `.a` — via the * edge. It normally coalesces with an explicit toolchain `import test`;
* sep_scan_file dedup-guarded dep append. */ * when user source occupies that identity, the reserved graph alias keeps
* it distinct. The linker receives the same support archive closure. */
if (is_test) { if (is_test) {
char tpath[1024]; char tpath[1024];
int tdir = 0; int tdir = 0;
if (locate_import(srcdir, "test", tpath, sizeof tpath, &tdir)) { if (locate_import(toolsrcdir, "test", tpath, sizeof tpath, &tdir)) {
int ti = sep_find_or_add(g, "test", tpath, tdir); char *tc = realpath(tpath, NULL);
if (ti < 0) return 1; char *rc = entry_is_dir ? realpath(src, NULL) : NULL;
int seen = 0; int root_is_support = tc != NULL && rc != NULL
for (int k = 0; k < g->pkg[root].ndeps; k++) && strcmp(tc, rc) == 0;
if (g->pkg[root].deps[k] == ti) { seen = 1; break; } free(tc);
if (!seen && g->pkg[root].ndeps < SEP_MAXPKG) free(rc);
g->pkg[root].deps[g->pkg[root].ndeps++] = ti; 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 (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; int root_package = package_only;
if (root_package && strcmp(g->pkg[root].name, "main") == 0) { if (root_package && strcmp(g->pkg[root].name, "main") == 0) {
fprintf(stderr, "ww: -p requires a non-main package\n"); 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 * LOCAL type (the root is never imported), which the
* export-check rejects. Skip -I for the root; its `.wwi` * export-check rejects. Skip -I for the root; its `.wwi`
* is never consumed. */ * is never consumed. */
if (!needs_export) if (!needs_export) {
/* #79: the root carries -T under `ww test` /* #79: the root carries -T under `ww test`
* so w6c synthesizes the test main. Deps never * so w6c synthesizes the test main. Deps never
* get -T. */ * get -T. */
snprintf(cmd, sizeof cmd, "%s %s-c -o %s %s", if (is_test)
c6, is_test ? "-T " : "", cs, cu); snprintf(cmd, sizeof cmd,
else "%s -T --test-support-module %s -c -o %s %s",
snprintf(cmd, sizeof cmd, "%s -c -I %s -o %s %s", c6, test_support_module, cs, cu);
c6, cw, 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) { if (run(cmd) != 0) {
fprintf(stderr, "ww: w6c failed for %s\n", fprintf(stderr, "ww: w6c failed for %s\n",
g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)"); 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), /* reverse-topo link: root `.o` first (order[norder-1], force-loaded),
* then transitive dep `.a` in reverse-topo order, then libwwrt.a — * then transitive dep `.a` in reverse-topo order, then libwwrt.a —
* each archive selectively pulls only members satisfying a live * each archive selectively pulls only members satisfying a live undef.
* 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 rtargs[2048] = {0};
char rtpath[1024]; char rtpath[1024];
snprintf(rtpath, sizeof rtpath, "%s/libwwrt.a", libdir); 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}; char objs[8192] = {0};
for (int oi = norder - 1; oi >= 0; oi--) { 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]; char path[1024];
sep_fname(g, order[oi], scratch, sep_fname(g, pi, scratch,
order[oi] == root ? ".o" : ".a", path, sizeof path); pi == root ? ".o" : ".a", path, sizeof path);
size_t n = strlen(objs); size_t n = strlen(objs);
snprintf(objs + n, sizeof objs - n, "%s%s", n ? " " : "", path); 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, build_one_sep(const char *src, int entry_is_dir, const char *root_identity,
const char *out, const char *out,
const char *objstem, const char *extra_includes, const char *extra_libs, 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) int keepscratch, const char *workdir)
{ {
char scratch[1100] = {0}; char scratch[1100] = {0};
struct sepgraph *g = NULL; struct sepgraph *g = NULL;
int r = build_one_sep_impl(src, entry_is_dir, root_identity, out, objstem, 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, extra_includes, extra_libs, extra_libdirs, package_only, is_test,
workdir, scratch, sizeof scratch, &g); root_variant, test_package, emit_asm, workdir, scratch,
sizeof scratch, &g);
sep_graph_free(g); sep_graph_free(g);
if (!keepscratch && scratch[0]) { if (!keepscratch && scratch[0]) {
size_t sl = strlen(scratch); size_t sl = strlen(scratch);
@@ -1626,7 +1906,8 @@ do_build(int argc, char **argv)
} }
const char *root_identity = package_only && !literal ? src : NULL; const char *root_identity = package_only && !literal ? src : NULL;
return build_one_sep(resolved, is_dir, root_identity, out, objstem, incs, libs, 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 static int
@@ -1658,7 +1939,7 @@ do_run(int argc, char **argv)
/* The freshly acquired directory owns both the executable and the /* The freshly acquired directory owns both the executable and the
* adjacent main.sepwork tree. Nothing outside it is adopted or removed. */ * adjacent main.sepwork tree. Nothing outside it is adopted or removed. */
if (build_one_sep(resolved, is_dir, NULL, tmp, tmp, incs, libs, libdirs, 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) if (unlink(tmp) != 0 && errno != ENOENT)
fputs("ww: cannot remove temporary output\n", stderr); fputs("ww: cannot remove temporary output\n", stderr);
if (rmdir(tmpdir) != 0) if (rmdir(tmpdir) != 0)
@@ -1704,6 +1985,8 @@ static int
do_test(int argc, char **argv) do_test(int argc, char **argv)
{ {
const char *src = NULL; const char *src = NULL;
const char *package_test_kind = NULL;
const char *package_test_name = NULL;
char incs[2048] = {0}; char incs[2048] = {0};
/* -c (Go's `go test -c`) builds the test binary without running it. /* -c (Go's `go test -c`) builds the test binary without running it.
* -S + -o <stem> stops after the lib/test-inclusive package `.s` * -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); "%s%s", n ? ":" : "", dir);
} else if (strcmp(argv[i], "-c") == 0) { } else if (strcmp(argv[i], "-c") == 0) {
compileonly = 1; 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) { } else if (strcmp(argv[i], "-S") == 0) {
emit_asm = 1; emit_asm = 1;
} else if (strcmp(argv[i], "-list") == 0) { } 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"); fprintf(stderr, "ww test: -S needs -o\n");
return 2; 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 /* Go's ./... form: a trailing "..." element is a package-tree
* request for the coordinator, never a literal path — recognized * request for the coordinator, never a literal path — recognized
* before stat, with the directory-mode rejects. */ * before stat, with the directory-mode rejects. */
size_t tlen = strlen(target); size_t tlen = strlen(target);
if (strcmp(target, "...") == 0 || if (strcmp(target, "...") == 0 ||
(tlen >= 4 && strcmp(target + tlen - 4, "/...") == 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) { if (emit_asm) {
fprintf(stderr, fprintf(stderr,
"ww test: -S needs a single test file\n"); "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"); "ww test: pattern needs a single test file\n");
return 2; 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); return exec_package_tests(argc, argv, src, resolved, 0);
} }
if (packageopts) { if (packageopts) {
@@ -1859,6 +2184,11 @@ do_test(int argc, char **argv)
"ww test: package options need a directory\n"); "ww test: package options need a directory\n");
return 2; 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]; char tmpdir[1024] = {0}, tmp[1024];
const char *outp; const char *outp;
int owntmp = !outstem[0] && !workdir[0]; 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. */ * source. An explicit -o names the caller-owned artifact stem. */
int br = build_one_sep(resolved, is_dir, NULL, outp, int br = build_one_sep(resolved, is_dir, NULL, outp,
outstem[0] ? outstem : tmp, incs, "", "", 0, 1, 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 (br != 0) {
if (owntmp && unlink(outp) != 0 && errno != ENOENT) if (owntmp && unlink(outp) != 0 && errno != ENOENT)
fputs("ww: cannot remove temporary output\n", stderr); fputs("ww: cannot remove temporary output\n", stderr);
@@ -1914,6 +2245,11 @@ do_test(int argc, char **argv)
return rc; return rc;
} }
if (S_ISREG(st.st_mode)) { 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) { if (packageopts) {
fprintf(stderr, fprintf(stderr,
"ww test: package options need a directory\n"); "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. */ /* See module-mode note: no-o scratch is redirected to /tmp. */
int br = build_one_sep(target, 0, NULL, outp, outstem[0] ? outstem : 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 (br != 0) {
if (owntmp && unlink(outp) != 0 && errno != ENOENT) if (owntmp && unlink(outp) != 0 && errno != ENOENT)
fputs("ww: cannot remove temporary output\n", stderr); 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"); fprintf(stderr, "ww test: pattern needs a single test file\n");
return 2; 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); 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 runtime allocator without requiring an `rt.wwi` compiler input), while the
linker still receives every reachable package archive plus the runtime archive. 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 ## 12. Candidate architectures and hard-gate decision
Five candidates were developed as coherent systems, not as feature bins. Five candidates were developed as coherent systems, not as feature bins.

View File

@@ -12,7 +12,6 @@ type pkgsource = struct {
dir: str, dir: str,
pkg: str, pkg: str,
test: bool, test: bool,
attest: bool,
}; };
type pkgfolder = struct { type pkgfolder = struct {
@@ -20,18 +19,13 @@ type pkgfolder = struct {
start: i32, start: i32,
end: i32, end: i32,
prodpkg: str, prodpkg: str,
hastests: bool,
}; };
type pkggroup = struct { type pkggroup = struct {
dir: str, dir: str,
pkg: str, pkg: str,
start: i32,
end: i32,
prodpkg: str,
external: bool, external: bool,
root: str, root: str,
combined: str,
workdir: str, workdir: str,
bin: str, bin: str,
buildout: str, buildout: str,
@@ -53,7 +47,6 @@ def PKGDONE: i32 = 3;
// pkggroup.fail values recorded at launch, reported at ordered emission. // pkggroup.fail values recorded at launch, reported at ordered emission.
def PKGFAILNONE: i32 = 0; def PKGFAILNONE: i32 = 0;
def PKGFAILSETUP: i32 = 1; def PKGFAILSETUP: i32 = 1;
def PKGFAILCOMPOSE: i32 = 2;
def pkgpoll: time.duration = 1000000i64: time.duration; def pkgpoll: time.duration = 1000000i64: time.duration;
@@ -212,22 +205,6 @@ fn pkgclause(src: str, out: *str) bool = {
return true; 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 = { fn pkgdirname(path: str) str = {
let i: i32 = path.len - 1; let i: i32 = path.len - 1;
for (i >= 0) { for (i >= 0) {
@@ -271,7 +248,6 @@ fn pkgisdir(path: str) bool = {
fn pkgkeepfile(name: str) bool = { fn pkgkeepfile(name: str) bool = {
if (!strings.hassuffix(name, ".ww")) { return false; }; if (!strings.hassuffix(name, ".ww")) { return false; };
if (strings.hassuffix(name, ".combined.ww")) { return false; };
return true; return true;
}; };
@@ -372,11 +348,19 @@ fn pkgdiscoverdir(path: str, st: *pkgdiscover, recurse: bool) void = {
os.close(fd); 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 = { fn pkgsort(ss: []str) void = {
let i: i32 = 1; let i: i32 = 1;
for (i < ss.len) { for (i < ss.len) {
let j: i32 = i; 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]; let t: str = ss[j];
ss[j] = ss[j - 1]; ss[j] = ss[j - 1];
ss[j - 1] = t; ss[j - 1] = t;
@@ -505,46 +489,25 @@ fn pkgemitfile(path: str, fd: i32) bool = {
return true; return true;
}; };
fn pkgcombined(g: *pkggroup, srcs: []pkgsource) bool = { fn pkgworkescape(s: str) str = {
let fd: i32 = os.open(g.combined, let out: []u8 = alloc([], (s.len * 2 + 1): u64)!;
os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 384i32); let i: i32 = 0;
if (fd < 0) { return false; }; for (i < s.len) {
let ok: bool = true; if (s[i] == '_') {
if (!g.external) { append(out, '_'); append(out, 'u');
let i: i32 = g.start; } else { if (s[i] == '/') {
for (i < g.end) { append(out, '_'); append(out, 's');
if (!srcs[i].test) { } else {
if (!pkgwrite(fd, "//ww:module-reset\n")) { ok = false; }; append(out, s[i]);
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; };
};
i += 1; i += 1;
}; };
os.close(fd); return strings.frombytes(out);
return ok;
}; };
fn pkgworkkey(dir: str, pkg: str) str = { fn pkgworkkey(dir: str, pkg: str) str = {
let s: str = strings.dup(strings.concat(dir, "_", pkg)); return strings.concat("d_", pkgworkescape(dir), "_p_",
let b: []u8 = strings.toutf8(s); pkgworkescape(pkg));
let i: i32 = 0;
for (i < b.len) {
if (b[i] == '/') { b[i] = '_'; };
i += 1;
};
return strings.frombytes(b);
}; };
fn pkgsetpaths(g: *pkggroup, root: str, index: i32, 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); let num: str = strconv.i32tos(index, strconv.base.DEC);
g.root = strings.concat(root, "/group-", num); g.root = strings.concat(root, "/group-", num);
if (!pkgmakedir(g.root)) { return false; }; 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 // A caller-owned persistent workdir root keys one driver -w dir
// per (dir, pkg) group; the driver's content-identity contract // per (dir, pkg) group; the driver's content-identity contract
// owns every reuse decision, so this stays a pure path policy. // 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, fn pkgstartbuild(g: *pkggroup, builder: str, includes: []str,
h: *exec.process) void = { 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, builder);
append(ba, "test"); append(ba, "test");
append(ba, "-c"); append(ba, "-c");
append(ba, "-o"); append(ba, "-o");
append(ba, g.bin); append(ba, g.bin);
append(ba, "-I"); append(ba, "--ww-package-test");
append(ba, g.dir); if (g.external) { append(ba, "external"); }
else { append(ba, "same"); };
append(ba, g.pkg);
let ii: i32 = 0; let ii: i32 = 0;
for (ii < includes.len) { for (ii < includes.len) {
append(ba, "-I"); append(ba, "-I");
@@ -633,7 +597,7 @@ fn pkgstartbuild(g: *pkggroup, builder: str, includes: []str,
append(ba, "-w"); append(ba, "-w");
append(ba, g.workdir); append(ba, g.workdir);
}; };
append(ba, g.combined); append(ba, g.dir);
let bcmd: exec.command; let bcmd: exec.command;
bcmd.path = builder; bcmd.path = builder;
bcmd.argv = ba; bcmd.argv = ba;
@@ -675,6 +639,12 @@ fn pkgbuildok(g: *pkggroup) bool = {
&& g.buildres.code == 0; && 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 // Ordered emission: a group's captures, diagnostics, and verdict line are
// written only here, strictly in group order, so concurrent scheduling // written only here, strictly in group order, so concurrent scheduling
// produces the byte stream sequential scheduling produced. // 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"); pkgfailpath(tmproot, "cannot create group temporary directory");
return false; 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); let buildcaptures: bool = pkgemitfile(g.buildout, os.STDOUT_FILENO);
buildcaptures = pkgemitfile(g.builderr, os.STDERR_FILENO) && buildcaptures; buildcaptures = pkgemitfile(g.builderr, os.STDERR_FILENO) && buildcaptures;
if (!buildcaptures) { if (!buildcaptures) {
@@ -704,15 +670,24 @@ fn pkgemitgroup(g: *pkggroup, tmproot: str, compileonly: bool) bool = {
pkgputln(os.STDOUT_FILENO, g.bin); pkgputln(os.STDOUT_FILENO, g.bin);
return true; return true;
}; };
let runcaptures: bool = pkgemitfile(g.runout, os.STDOUT_FILENO); let runstdout: str;
runcaptures = pkgemitfile(g.runerr, os.STDERR_FILENO) && runcaptures; let runstderr: str;
if (!runcaptures) { if (!pkgread(g.runout, &runstdout) || !pkgread(g.runerr, &runstderr)) {
pkgfailpath(g.root, "cannot read test capture"); pkgfailpath(g.root, "cannot read test capture");
return false; return false;
}; };
if (g.runres.errno != 0 || g.runres.cleanuperrno != 0 // The compiler-owned dispatcher is the authority on whether a variant
|| g.runres.termination != exec.termination.EXIT // contains runnable tests. With an empty generated table, test.run returns
|| g.runres.code != 0) { // 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); pkgreportcommand("test", g, &g.runres);
return false; return false;
}; };
@@ -874,15 +849,7 @@ export fn packagecommand(args: []str) int = {
s.path = ds.paths[i]; s.path = ds.paths[i];
s.dir = strings.dup(pkgdirname(ds.paths[i])); s.dir = strings.dup(pkgdirname(ds.paths[i]));
s.pkg = strings.dup(pn); s.pkg = strings.dup(pn);
s.attest = pkgattest(body);
s.test = strings.hassuffix(pkgbase(ds.paths[i]), "_test.ww"); 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); append(srcs, s);
i += 1; i += 1;
}; };
@@ -895,15 +862,9 @@ export fn packagecommand(args: []str) int = {
f.start = i; f.start = i;
f.end = i; f.end = i;
f.prodpkg = ""; f.prodpkg = "";
f.hastests = false;
for (f.end < srcs.len && strings.compare(srcs[f.end].dir, f.path) == 0) { for (f.end < srcs.len && strings.compare(srcs[f.end].dir, f.path) == 0) {
if (srcs[f.end].test) { if (!srcs[f.end].test && f.prodpkg.len == 0) {
if (srcs[f.end].attest) { f.hastests = true; };
} else if (f.prodpkg.len == 0) {
f.prodpkg = srcs[f.end].pkg; 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; f.end += 1;
}; };
@@ -915,21 +876,16 @@ export fn packagecommand(args: []str) int = {
i = 0; i = 0;
for (i < folders.len) { for (i < folders.len) {
let f: pkgfolder = folders[i]; 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 = ""; let externalpkg: str = "";
if (f.prodpkg.len != 0) { if (f.prodpkg.len != 0) {
externalpkg = strings.concat(f.prodpkg, "_test"); externalpkg = strings.concat(f.prodpkg, "_test");
}; };
let standalonepkg: str = ""; let standalonepkg: str = "";
let sawtestfile: bool = false;
let j: i32 = f.start; let j: i32 = f.start;
for (j < f.end) { for (j < f.end) {
if (srcs[j].test) { if (srcs[j].test) {
sawtestfile = true;
if (f.prodpkg.len != 0 if (f.prodpkg.len != 0
&& strings.compare(srcs[j].pkg, f.prodpkg) != 0 && strings.compare(srcs[j].pkg, f.prodpkg) != 0
&& strings.compare(srcs[j].pkg, externalpkg) != 0) { && strings.compare(srcs[j].pkg, externalpkg) != 0) {
@@ -947,7 +903,6 @@ export fn packagecommand(args: []str) int = {
return 1; return 1;
}; };
}; };
if (!srcs[j].attest) { j += 1; continue; };
let found: bool = false; let found: bool = false;
let k: i32 = 0; let k: i32 = 0;
for (k < groups.len) { for (k < groups.len) {
@@ -962,9 +917,6 @@ export fn packagecommand(args: []str) int = {
let g: pkggroup; let g: pkggroup;
g.dir = f.path; g.dir = f.path;
g.pkg = srcs[j].pkg; g.pkg = srcs[j].pkg;
g.start = f.start;
g.end = f.end;
g.prodpkg = f.prodpkg;
g.external = f.prodpkg.len != 0 g.external = f.prodpkg.len != 0
&& strings.compare(f.prodpkg, g.pkg) != 0; && strings.compare(f.prodpkg, g.pkg) != 0;
append(groups, g); append(groups, g);
@@ -972,6 +924,14 @@ export fn packagecommand(args: []str) int = {
}; };
j += 1; 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; i += 1;
}; };
if (groups.len == 0) { return 0; }; if (groups.len == 0) { return 0; };
@@ -1012,9 +972,6 @@ export fn packagecommand(args: []str) int = {
g.fail = PKGFAILSETUP; g.fail = PKGFAILSETUP;
g.state = PKGDONE; g.state = PKGDONE;
stopped = true; stopped = true;
} else if (!pkgcombined(g, srcs)) {
g.fail = PKGFAILCOMPOSE;
g.state = PKGDONE;
} else { } else {
pkgstartbuild(g, builder, includes, pkgstartbuild(g, builder, includes,
&handles[launched]); &handles[launched]);

View File

@@ -353,21 +353,20 @@ fn locateimport(dirs: *u8, name: *u8, namelen: u64,
return nil; return nil;
}; };
// Go's contract: only *_test.ww is a test source. A line-leading @test // Go's contract: only *_test.ww is a test source. Ask the compiler parser,
// declaration anywhere else would be silently dropped by a non-T build // rather than a textual attribute scan, whether a production source contains
// (#6, the Hare model), so directory enumeration rejects it loudly. // @test; otherwise valid whitespace/comments could silently drop a test.
// This is the wwstage twin of cmd/ww/main.c:file_has_line_test. fn dirfileattest(dirpath: *u8, name: *u8) i32 = {
fn dirfileattest(dirpath: *u8, name: *u8) bool = {
let path: *u8 = joinpath(dirpath, name); let path: *u8 = joinpath(dirpath, name);
let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32); 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 sr: (i64 | os.oserror) = os.filesize(fd);
let n: i64 = -1i64; let n: i64 = -1i64;
match (sr) { match (sr) {
case let v: i64 => n = v; 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)!; let b: []u8 = alloc([], n: u64)!;
b.len = n: i32; b.len = n: i32;
let rr: (i64 | os.oserror) = os.readall(fd, b.ptr, n: u64); 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; let got: i64 = -1i64;
match (rr) { match (rr) {
case let v: i64 => got = v; 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; }; if (got != n) { return -1; };
let i: i32 = 0; // Preserve the directory loader's normalized package-clause diagnostic
for (i < b.len) { // before the full parser performs language-level recovery. This is also
for (i < b.len && (b[i] == ' ' || b[i] == '\t' // the C/WW parity boundary for malformed clauses.
|| b[i] == '\r')) { i += 1; }; let il: syntax.lex;
if (i + 5 < b.len && b[i] == '@' && b[i + 1] == 't' syntax.lexinit(&il, pathstr(path), b.ptr, n: u64);
&& b[i + 2] == 'e' && b[i + 3] == 's' let ips: syntax.parser;
&& b[i + 4] == 't' syntax.parserinit(&ips, &il);
&& (b[i + 5] == ' ' || b[i + 5] == '\t')) { let imports: *syntax.node = syntax.parseimports(&ips);
return true; 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; }; d = d.next;
if (i < b.len) { i += 1; };
}; };
return false; return 0;
}; };
// Classify a directory entry for production enumeration: 1 keep, def SEP_VARIANT_PRODUCTION: i32 = 0;
// 0 skip (non-source or *_test.ww), -1 @test outside *_test.ww def SEP_VARIANT_SAME_TEST: i32 = 1;
// (caller diagnoses and fails). def SEP_VARIANT_EXTERNAL: i32 = 2;
fn dirfileclass(dirpath: *u8, name: *u8, nlen: u64) i32 = { 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 // nlen<=3 guard kept: a bare ".ww" (len 3) is rejected here but
// would pass strings.hassuffix(".ww"); preserves cstage parity. // would pass strings.hassuffix(".ww"); preserves cstage parity.
if (nlen <= 3u64) { return 0; }; if (nlen <= 3u64) { return 0; };
@@ -405,7 +430,9 @@ fn dirfileclass(dirpath: *u8, name: *u8, nlen: u64) i32 = {
s.ptr = name; s.ptr = name;
s.len = nlen: i32; s.len = nlen: i32;
if (!strings.hassuffix(s, ".ww")) { return 0; }; 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 source: *u8 = joinpath(dirpath, name);
let fi: os.filestat; let fi: os.filestat;
let sr: (void | os.oserror) = os.lstat(&fi, pathstr(source)); 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"); cerr(": package source is not a regular file\n");
return -2; 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; 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; // Rule-10 byte-id requires cstage and wwstage sort the same way;
// memcmp is the locale-independent total order (mirrors // memcmp is the locale-independent total order (mirrors
// ref/hare/sort/cmp/cmp.ha strs). // ref/hare/sort/cmp/cmp.ha strs).
@@ -445,12 +507,10 @@ fn bytecmp(a: *u8, alen: u64, b: *u8, blen: u64) i32 = {
return 0; return 0;
}; };
// enumeratedir — list production *.ww paths of `dirpath` (less *_test.ww // Enumerate a production, same-test, or external-test directory variant.
// test sources), byte-sort. A line-leading @test in any other source is // Production files precede matching test files; each partition is byte-sorted.
// diagnosed here and returns -2. Returns one exact pointer array of fn enumeratedir(dirpath: *u8, variant: i32,
// NUL-terminated full paths. This is the sole directory-membership testpackage: *u8) (**u8, i32) = {
// discovery path; the owning seppkg retains the list.
fn enumeratedir(dirpath: *u8) (**u8, i32) = {
let fd: i32 = os.open(pathstr(dirpath), os.flag.RDONLY, 0i32); let fd: i32 = os.open(pathstr(dirpath), os.flag.RDONLY, 0i32);
if (fd < 0) { return nil: **u8, -1; }; if (fd < 0) { return nil: **u8, -1; };
// #65: grow-dynamic (mirror cstage enumerate_dir_ww realloc-doubling, // #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 cap: i32 = 8;
let names: []*u8 = alloc([], cap: u64)!; let names: []*u8 = alloc([], cap: u64)!;
let nlens: []u64 = alloc([], cap: u64)!; let nlens: []u64 = alloc([], cap: u64)!;
let kinds: []i32 = alloc([], cap: u64)!;
let n: i32 = 0; let n: i32 = 0;
let buf: []u8 = alloc([], 8192u64)!; let buf: []u8 = alloc([], 8192u64)!;
buf.len = 8192; buf.len = 8192;
@@ -473,7 +534,7 @@ fn enumeratedir(dirpath: *u8) (**u8, i32) = {
let reclen: u64 = blo + (bhi * 256u64); let reclen: u64 = blo + (bhi * 256u64);
let nm: *u8 = buf.ptr + off + 19u64; let nm: *u8 = buf.ptr + off + 19u64;
let nl: u64 = cstrlen(nm); let nl: u64 = cstrlen(nm);
let cls: i32 = dirfileclass(dirpath, nm, nl); let cls: i32 = dirfileclass(dirpath, nm, nl, variant);
if (cls == -1) { if (cls == -1) {
cerr("ww: "); cerr("ww: ");
cerr(pathstr(joinpath(dirpath, nm))); cerr(pathstr(joinpath(dirpath, nm)));
@@ -486,23 +547,38 @@ fn enumeratedir(dirpath: *u8) (**u8, i32) = {
return nil: **u8, -2; return nil: **u8, -2;
}; };
if (cls > 0) { 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) { if (n >= cap) {
let ncap: i32 = cap * 2; let ncap: i32 = cap * 2;
let nn: []*u8 = alloc([], ncap: u64)!; let nn: []*u8 = alloc([], ncap: u64)!;
let nl2: []u64 = alloc([], ncap: u64)!; let nl2: []u64 = alloc([], ncap: u64)!;
let nk: []i32 = alloc([], ncap: u64)!;
let k: i32 = 0; let k: i32 = 0;
for (k < n) { for (k < n) {
nn[k] = names[k]; nn[k] = names[k];
nl2[k] = nlens[k]; nl2[k] = nlens[k];
nk[k] = kinds[k];
k += 1; k += 1;
}; };
names = nn; names = nn;
nlens = nl2; nlens = nl2;
kinds = nk;
cap = ncap; cap = ncap;
}; };
let full: *u8 = joinpath(dirpath, nm);
names[n] = full; names[n] = full;
nlens[n] = cstrlen(full); nlens[n] = cstrlen(full);
kinds[n] = cls;
n += 1; n += 1;
}; };
off += reclen; off += reclen;
@@ -524,8 +600,12 @@ fn enumeratedir(dirpath: *u8) (**u8, i32) = {
for (i < n) { for (i < n) {
let j: i32 = i; let j: i32 = i;
for (j > 0) { for (j > 0) {
if (bytecmp(names[j - 1], nlens[j - 1], let c: i32 = kinds[j - 1] - kinds[j];
names[j], nlens[j]) <= 0) { j = 0; } if (c == 0) {
c = bytecmp(names[j - 1], nlens[j - 1],
names[j], nlens[j]);
};
if (c <= 0) { j = 0; }
else { else {
let t: *u8 = names[j]; let t: *u8 = names[j];
names[j] = names[j - 1]; names[j] = names[j - 1];
@@ -533,6 +613,9 @@ fn enumeratedir(dirpath: *u8) (**u8, i32) = {
let tl: u64 = nlens[j]; let tl: u64 = nlens[j];
nlens[j] = nlens[j - 1]; nlens[j] = nlens[j - 1];
nlens[j - 1] = tl; nlens[j - 1] = tl;
let tk: i32 = kinds[j];
kinds[j] = kinds[j - 1];
kinds[j - 1] = tk;
j -= 1; j -= 1;
}; };
}; };
@@ -541,6 +624,7 @@ fn enumeratedir(dirpath: *u8) (**u8, i32) = {
if (n == 0) { if (n == 0) {
os.free(names.ptr: *void, (cap: u64) * (size(*u8): u64)); os.free(names.ptr: *void, (cap: u64) * (size(*u8): u64));
os.free(nlens.ptr: *void, (cap: u64) * (size(u64): 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; return nil: **u8, 0;
}; };
let exact: []*u8 = alloc([], n: u64)!; let exact: []*u8 = alloc([], n: u64)!;
@@ -551,6 +635,7 @@ fn enumeratedir(dirpath: *u8) (**u8, i32) = {
// shape correct for the driver's allocations. // shape correct for the driver's allocations.
os.free(names.ptr: *void, (cap: u64) * (size(*u8): u64)); os.free(names.ptr: *void, (cap: u64) * (size(*u8): u64));
os.free(nlens.ptr: *void, (cap: u64) * (size(u64): 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; return exact.ptr, n;
}; };
@@ -637,9 +722,12 @@ type seppkg = struct {
path: *u8, // dotted import path, NUL-term; root path[0]==0 path: *u8, // dotted import path, NUL-term; root path[0]==0
entry: *u8, // resolved package dir (or file, file root), NUL-term entry: *u8, // resolved package dir (or file, file root), NUL-term
name: *u8, // validated declared name; directory packages only 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, nsources: i32,
isdir: i32, isdir: i32,
variant: i32,
testsupport: bool,
deps: []i32, // direct-dep indices into sepgraph.pkg deps: []i32, // direct-dep indices into sepgraph.pkg
ndeps: i32, ndeps: i32,
color: i32, // tri-color DFS: 0 white, 1 gray, 2 black color: i32, // tri-color DFS: 0 white, 1 gray, 2 black
@@ -650,15 +738,26 @@ type sepgraph = struct {
n: i32, 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) { if (cstrlen(path) >= 256u64) {
cerr("ww: package path is too long (limit 255 bytes)\n"); cerr("ww: package path is too long (limit 255 bytes)\n");
return -1; return -1;
}; };
let i: i32 = 0; let i: i32 = 0;
for (i < g.n) { 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 (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("ww: package "); cerr(pathstr(path));
cerr(" resolves to more than one location\n"); cerr(" resolves to more than one location\n");
return -1; return -1;
@@ -666,6 +765,10 @@ fn sepfindoradd(g: *sepgraph, path: *u8, entry: *u8, isdir: i32) i32 = {
return i; return i;
}; };
if (os.samefile(pathstr(g.pkg[i].entry), pathstr(entry))) { if (os.samefile(pathstr(g.pkg[i].entry), pathstr(entry))) {
if (testproductionpair) {
i += 1;
continue;
};
cerr("ww: package directory "); cerr(pathstr(entry)); cerr("ww: package directory "); cerr(pathstr(entry));
cerr(" has identities "); cerr(" has identities ");
if (g.pkg[i].path[0u64] == 0u8) { cerr("(root)"); } 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].path = arenadupcstr(path, plen);
g.pkg[g.n].entry = arenadupcstr(entry, elen); g.pkg[g.n].entry = arenadupcstr(entry, elen);
g.pkg[g.n].name = nil; 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].sources = nil;
g.pkg[g.n].nsources = 0; g.pkg[g.n].nsources = 0;
g.pkg[g.n].isdir = isdir; 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)!; let dslot: []i32 = alloc([], SEP_MAXPKG: u64)!;
dslot.len = SEP_MAXPKG; dslot.len = SEP_MAXPKG;
g.pkg[g.n].deps = dslot; g.pkg[g.n].deps = dslot;
@@ -700,6 +810,11 @@ fn sepfindoradd(g: *sepgraph, path: *u8, entry: *u8, isdir: i32) i32 = {
return r; 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 // 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 // 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. // 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; 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 // Scan one already-selected source file for its leading package clause
// (when it is an owned directory source) and top-level imports. A DIRECTORY // (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 // 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)) { && (ownedsource != 0 || g.pkg[pi].name == nil)) {
let declared: *u8 = imports.nmod.ptr; let declared: *u8 = imports.nmod.ptr;
let declaredn: u64 = imports.nmod.len: u64; 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) { if (g.pkg[pi].name == nil) {
g.pkg[pi].name = arenadupcstr(declared, declaredn); g.pkg[pi].name = arenadupcstr(declared, declaredn);
} else { if (bytecmp(g.pkg[pi].name, cstrlen(g.pkg[pi].name), } 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; return -1;
}; };
let isdir: i32 = 0; 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 (ipath != nil) {
if (isdir != 0) { 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); cerrpos(u.file, u.line, u.col);
cerr(": error: self-import: package '"); cerr(": error: self-import: package '");
if (g.pkg[pi].path[0u64] != 0u8) { 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) { if (g.pkg[pi].isdir != 0) {
let sources: **u8; let sources: **u8;
let nsources: i32; 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].sources = sources;
g.pkg[pi].nsources = nsources; g.pkg[pi].nsources = nsources;
if (g.pkg[pi].nsources == -2) { if (g.pkg[pi].nsources == -2) {
@@ -956,7 +1121,8 @@ fn seploadpkg(g: *sepgraph, pi: i32, searchpath: *u8) i32 = {
}; };
i += 1; 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 plen: u64 = cstrlen(g.pkg[pi].path);
let leaf: *u8 = g.pkg[pi].path; let leaf: *u8 = g.pkg[pi].path;
let j: u64 = 0u64; 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, fn sepemitbody(fd: i32, path: *u8, visit: *expctx, searchpath: *u8,
modpath: *u8) i32 = { modpath: *u8, pkg: *seppkg) i32 = {
let pview: str; let pview: str;
pview.ptr = path; pview.ptr = path;
pview.len = cstrlen(path): i32; pview.len = cstrlen(path): i32;
@@ -1114,12 +1280,16 @@ fn sepemitbody(fd: i32, path: *u8, visit: *expctx, searchpath: *u8,
u = uses[ui]; u = uses[ui];
let idp: *u8 = u.usepath.ptr; let idp: *u8 = u.usepath.ptr;
let idn: u64 = u.usepath.len: u64; let idn: u64 = u.usepath.len: u64;
if (sepexternalproduction(pkg, idp, idn)) {
ui += 1;
continue;
};
let isdir: i32 = 0; let isdir: i32 = 0;
let ipath: *u8 = locateimport(searchpath, idp, idn, &isdir); let ipath: *u8 = locateimport(searchpath, idp, idn, &isdir);
if (ipath != nil) { if (ipath != nil) {
if (isdir == 0) { if (isdir == 0) {
if (sepemitbody(fd, ipath, visit, searchpath, if (sepemitbody(fd, ipath, visit, searchpath,
modpath) < 0) { return -1; }; modpath, pkg) < 0) { return -1; };
}; };
}; };
ui += 1; ui += 1;
@@ -1195,12 +1365,12 @@ fn sepcomposeunit(g: *sepgraph, pi: i32, scratch: *u8,
let i: i32 = 0; let i: i32 = 0;
for (i < g.pkg[pi].nsources && bodyrc == 0) { for (i < g.pkg[pi].nsources && bodyrc == 0) {
bodyrc = sepemitbody(u, g.pkg[pi].sources[i], &bv, searchpath, bodyrc = sepemitbody(u, g.pkg[pi].sources[i], &bv, searchpath,
g.pkg[pi].path); g.pkg[pi].path, &g.pkg[pi]);
i += 1; i += 1;
}; };
} else { } else {
bodyrc = sepemitbody(u, g.pkg[pi].entry, &bv, searchpath, 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) { if (os.close(u) != 0) {
cerr("ww: cannot close package unit\n"); 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 = { fn workdirstamptext(istest: i32, emitasm: i32) str = {
if (istest != 0) { if (istest != 0) {
if (emitasm != 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) { 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 = { 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, fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
rootidentity: *u8, out: *u8, rootidentity: *u8, out: *u8,
objstem: *u8, incs: *u8, lf: *lflags, packageonly: i32, istest: i32, 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 = { graphout: **sepgraph) i32 = {
let c6: *u8 = joinpathlit(selfdir, "w6c_ww"); let c6: *u8 = joinpathlit(selfdir, "w6c_ww");
let a6: *u8 = joinpathlit(selfdir, "w6a_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; }; if (graphout != nil) { *graphout = g; };
let rootpath: *u8 = "\0".ptr; let rootpath: *u8 = "\0".ptr;
if (packageonly != 0 && rootidentity != nil) { rootpath = rootidentity; }; 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; }; if (root < 0) { return 1; };
// #79 (-T): lib/test is the synth main's `test.run` callee but @test let testsupportmodule: str = "test";
// files never `import test;`. Inject it as a direct dep of the root so // -T generates a dispatcher whose support qualifier is selected by the
// seploadpkg pulls test + its transitive deps; the producer adds -T to // command. Represent that compiler-generated requirement as a direct root
// the root and `test.run` links against test's `.a` — via the // edge. It normally coalesces with an explicit toolchain `import test`;
// sepscanfile dedup-guarded dep append. // when user source occupies that identity, the reserved graph alias keeps
// it distinct. The linker receives the same support archive closure.
if (istest != 0) { if (istest != 0) {
let td: i32 = 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) { if (tp != nil) {
let ti: i32 = sepfindoradd(g, "test\0".ptr, tp, td); let rootissupport: bool = entryisdir != 0
if (ti < 0) { return 1; }; && os.samefile(pathstr(tp), pathstr(src));
let seen: bool = false; let collision: bool = false;
let m: i32 = 0; if (!rootissupport && testpackage != nil) {
for (m < g.pkg[root].ndeps) { collision = cstreqlit(testpackage, "test")
if (g.pkg[root].deps[m] == ti) { seen = true; }; || cstreqlit(testpackage, "test_test");
m += 1;
}; };
if (!seen) { if (!rootissupport && !collision) {
if (g.pkg[root].ndeps < SEP_MAXPKG) { let ud: i32 = 0;
g.pkg[root].deps[g.pkg[root].ndeps] = ti; let up: *u8 = locateimport(searchpath.ptr, "test".ptr,
g.pkg[root].ndeps += 1; "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 (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; let rootpackage: bool = packageonly != 0;
if (rootpackage && cstreqlit(g.pkg[root].name, "main")) { if (rootpackage && cstreqlit(g.pkg[root].name, "main")) {
cerr("ww: -p requires a non-main package\n"); 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 // #79: the root carries -T under `ww test` so w6c
// synthesizes the test main; deps never get -T. // synthesizes the test main; deps never get -T.
let roott: bool = (pi == root) && (istest != 0); let roott: bool = (pi == root) && (istest != 0);
let supportt: bool = g.pkg[pi].testsupport;
let alen: u64 = 8u64; 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)!; let argv: []str = alloc([], alen)!;
append(argv, "w6c"); 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"); append(argv, "-c");
if (needsexport) { if (needsexport) {
append(argv, "-I"); 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` // Reverse-topo link: root `.o` first (order[norder-1]), dependency `.a`
// files after, then libwwrt.a (which still // files after, then libwwrt.a (which still selectively pulls only the
// selectively pulls only the runtime members a live undef needs). // 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 // argv: 3 fixed (w6l,-o,out) + one root object/archive per package
// + 1 libwwrt + 2*nlibdirs + 2*nlibs + 1 nil. // + 1 libwwrt + 2*nlibdirs + 2*nlibs + 1 nil.
let nldirs: i32 = 0; let nldirs: i32 = 0;
@@ -1882,10 +2097,19 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
let pos: i32 = 3; let pos: i32 = 3;
let li: i32 = norder - 1; let li: i32 = norder - 1;
for (li >= 0) { 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). // root: positional `.o` (force-load); deps: `.a` (selective).
let suf: str = ".a"; let suf: str = ".a";
if (order[li] == root) { suf = ".o"; }; if (pi == root) { suf = ".o"; };
largv[pos] = sepfname(g, order[li], scratch, suf); largv[pos] = sepfname(g, pi, scratch, suf);
pos += 1; pos += 1;
li -= 1; li -= 1;
}; };
@@ -1934,12 +2158,14 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32,
rootidentity: *u8, out: *u8, rootidentity: *u8, out: *u8,
objstem: *u8, incs: *u8, lf: *lflags, packageonly: i32, istest: i32, 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 scratch: *u8 = nil;
let g: *sepgraph = nil; let g: *sepgraph = nil;
let r: i32 = buildonesepimpl(selfdir, src, entryisdir, rootidentity, let r: i32 = buildonesepimpl(selfdir, src, entryisdir, rootidentity,
out, objstem, out, objstem,
incs, lf, packageonly, istest, emitasm, workdir, &scratch, &g); incs, lf, packageonly, istest, rootvariant, testpackage,
emitasm, workdir, &scratch, &g);
sepgraphfree(g); sepgraphfree(g);
if (keepscratch == 0 && scratch != nil) { if (keepscratch == 0 && scratch != nil) {
if (cstrendswithlit(scratch, ".sepwork")) { 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; }; if (packageonly != 0 && !requestedliteral) { rootidentity = src; };
return buildonesep(selfdir, resolved, isdir, rootidentity, return buildonesep(selfdir, resolved, isdir, rootidentity,
out, objstem, incs.ptr, &lf, 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 // 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; lf.nlibs = nlibs;
// The freshly acquired directory owns both main and main.sepwork. // The freshly acquired directory owns both main and main.sepwork.
if (buildonesep(selfdir, resolved, isdir, nil, outp, outp, incs.ptr, &lf, 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)); let cleanrc: i32 = os.remove(pathstr(outp));
if (cleanrc != 0 && cleanrc != -2i32) { if (cleanrc != 0 && cleanrc != -2i32) {
cerr("ww: cannot remove temporary output\n"); 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; let keep: i32 = 0;
if (outstem != nil) { keep = 1; }; if (outstem != nil) { keep = 1; };
let bres: i32 = buildonesep(selfdir, src, 0, nil, outp, objstem, incs, &lf, 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 (bres != 0) {
if (owntmp) { if (owntmp) {
let cleanrc: i32 = os.remove(pathstr(outp)); 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 emitasm: i32 = 0;
let outstem: *u8 = nil; let outstem: *u8 = nil;
let workdir: *u8 = nil; let workdir: *u8 = nil;
let packagetestkind: *u8 = nil;
let packagetestname: *u8 = nil;
let packageopts: bool = false; let packageopts: bool = false;
let afterdash: bool = false; let afterdash: bool = false;
let i: i32 = start; let i: i32 = start;
@@ -2600,6 +2829,27 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
if (cstreqlit(p, "--")) { if (cstreqlit(p, "--")) {
packageopts = true; afterdash = true; i += 1; continue; 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' if (p[1u64] == 73u8) { // '-I'
let dir: *u8 = nil; let dir: *u8 = nil;
if (p[2u64] != 0u8) { 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"); cerr("ww test: -S needs -o\n");
return 2; 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 // Go's ./... form: a trailing "..." element is a package-tree
// request for the coordinator, never a literal path — recognized // 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] == '.'; && target[tlen - 1u64] == '.';
}; };
if (istree) { if (istree) {
if (packagetestkind != nil) {
cerr("ww test: package-test variant needs one directory\n");
return 2;
};
if (emitasm != 0) { if (emitasm != 0) {
cerr("ww test: -S needs a single test file\n"); cerr("ww test: -S needs a single test file\n");
return 2; return 2;
@@ -2733,6 +2991,10 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
}; };
}; };
if (isdir == 0) { if (isdir == 0) {
if (packagetestkind != nil) {
cerr("ww test: package-test variant needs one directory\n");
return 2;
};
if (packageopts) { if (packageopts) {
cerr("ww test: package options need a directory\n"); return 2; 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"); cerr("ww test: pattern needs a single test file\n");
return 2; 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; let replacement: *u8 = nil;
if (resolved != target) { replacement = resolved; }; if (resolved != target) { replacement = resolved; };
return execpackagetests(selfdir, argv, argc, start, targetindex, return execpackagetests(selfdir, argv, argc, start, targetindex,