Files
ww/cmd/ww/main.c
Hojun-Cho e06b871ab9 ww: add -w persistent workdir builds with content-identity reuse
A -w DIR workdir replaces the fresh .sepwork scratch with a caller-owned
persistent package-artifact tree. A package is reused only when its
freshly composed unit byte-equals the committed unit and byte copies of
the compiler/assembler recorded in the dir equal the live tools — pure
content identity, no mtimes, no hashes, every decision reproducible
with cmp against plain files. Recompiles stage at .new names and commit
by rename, unit strictly last, so an interrupted build forces a
recompile and can never leave a committed unit vouching for uncommitted
artifacts; .o/.a additionally reject zero size (ELF/ar are never
empty), while .s/.wwi accept legitimate empties (FFI-only rt). A mode
stamp pins the -T/-S shape and the artifact protocol revision. Classic
scratch keeps its exact acquire/refuse/cleanup contract; run rejects
-w; dir-mode test rejects -w; both driver stages implement identical
behavior and wording.
2026-08-08 03:51:45 +09:00

1889 lines
61 KiB
C

/*
* ww — the user-facing driver. Plan 9 cc(1) / Hare hare(1) analogue.
*
* Pipeline:
* ww build foo.ww → w6c foo.ww > foo.s ; w6a foo.s > foo.o ;
* w6l -o foo foo.o <runtime.o>
* ww run foo.ww → build then exec ./foo
*
* Tool paths default to siblings of $0 (so a fresh build runs out of
* out/bin/), and can be overridden with WW_W6C / WW_W6A / WW_W6L.
*/
#include "ww.h"
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <dirent.h>
#include <libgen.h>
static const char *usage =
"usage: ww [-V] <subcommand> [args...]\n"
" -V print version and exit\n"
" build [-S] [-w DIR] [-o FILE] [path] compile module; -S stops after package asm\n"
" run [path] ... build then exec, passing extra args to the program\n"
" test [-S -o STEM] [-w DIR] [options] [path] build/run tests; -S emits package asm\n"
" fmt <path> reformat ww source\n"
" version print version and exit\n"
"\n"
" path forms:\n"
" foo.ww literal file\n"
" foo search cwd, -I dirs, then $WW_LIB for foo.ww or foo/foo.ww\n"
" lib/foo directory: build lib/foo/foo.ww\n"
" . build the cwd's <basename>.ww\n";
static char *self_dir; /* directory containing this binary */
static const char *
toolpath(const char *envvar, const char *name)
{
const char *p = getenv(envvar);
if (p && p[0]) return p;
static char buf[1024];
snprintf(buf, sizeof buf, "%s/%s", self_dir, name);
return strdup(buf);
}
static int
run(const char *cmd)
{
int rc = system(cmd);
if (rc == -1) return -1;
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
return 1;
}
/* run_test_bin — exec the built test binary with an optional name-filter
* pattern as argv[1] (lib/test run() reads it via os.args). fork+execv
* (not system()) so glob metacharacters in the pattern reach the binary
* verbatim instead of being expanded by the shell. Mirrors the wwstage
* twin (selfhost/cmd/ww/main.ww runsingletest, which passes the same
* argv to os.exec.runstdio). #17 fnmatch filter. */
static int
run_test_bin(const char *bin, const char *pattern)
{
pid_t pid = fork();
if (pid < 0) { perror("ww: fork"); return -1; }
if (pid == 0) {
char *xargv[3];
xargv[0] = (char *)bin;
if (pattern) { xargv[1] = (char *)pattern; xargv[2] = NULL; }
else { xargv[1] = NULL; }
execv(bin, xargv);
perror("ww: exec");
_exit(127);
}
int status = 0;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) return WEXITSTATUS(status);
return 1;
}
/* Delegate package/directory testing to the native WW coordinator. Keep the
* old single-file path in this driver: wwtest itself builds each generated
* package root through `ww test -c ... package.ww`, so that file boundary also
* prevents delegation recursion. */
static int
exec_package_tests(int argc, char **argv, const char *target,
const char *resolved, int add_dot)
{
const char *override = getenv("WW_WWTEST");
char fallback[1024];
const char *prog = override && override[0] ? override : fallback;
if (prog == fallback)
snprintf(fallback, sizeof fallback, "%s/wwtest", self_dir);
char builder[1024];
snprintf(builder, sizeof builder, "%s/ww", self_dir);
char **xargv = calloc((size_t)argc + 6, sizeof *xargv);
if (xargv == NULL) {
fputs("ww test: cannot allocate package coordinator arguments\n",
stderr);
return 1;
}
int n = 0, dotted = 0;
xargv[n++] = (char *)prog;
xargv[n++] = "package";
xargv[n++] = "--ww-driver";
xargv[n++] = builder;
for (int i = 0; i < argc; i++) {
if (add_dot && !dotted && strcmp(argv[i], "--") == 0) {
xargv[n++] = ".";
dotted = 1;
}
xargv[n++] = (resolved && argv[i] == target)
? (char *)resolved : argv[i];
}
if (add_dot && !dotted) xargv[n++] = ".";
xargv[n] = NULL;
execv(prog, xargv); /* inherit the caller's environment */
fputs("ww test: cannot exec package test coordinator\n", stderr);
free(xargv);
return 1;
}
/* Set of imported module paths, kept on the heap. Used to break
* cycles in `use` resolution. Linear because typical imports are
* a handful per build. */
struct ImportSet {
char **paths;
int n, cap;
};
static int
import_seen(struct ImportSet *s, const char *path)
{
for (int i = 0; i < s->n; i++)
if (strcmp(s->paths[i], path) == 0) return 1;
return 0;
}
static void
import_add(struct ImportSet *s, const char *path)
{
if (s->n + 1 > s->cap) {
s->cap = s->cap ? s->cap * 2 : 8;
s->paths = realloc(s->paths, s->cap * sizeof *s->paths);
}
s->paths[s->n++] = strdup(path);
}
/* Translate dots in an `import` name to slashes for path lookup.
* `encoding.utf8` → `encoding/utf8`. Mirrors Hare hare(1)'s
* use-path → fs-path mapping (ref/hare/hare/module/srcs.ha:78
* builds the same shape via path::push per ident part). */
static void
import_path_form(const char *name, char *out, size_t outsz)
{
size_t i;
for (i = 0; i + 1 < outsz && name[i] != '\0'; i++)
out[i] = (name[i] == '.') ? '/' : name[i];
out[i] = '\0';
}
/* try <dir>/<path>/ as a directory (want_dir), else <dir>/<path>.ww as
* a file. Sets *is_dir on hit. Symmetric with wwstage locatein for
* byte-id driver output (rule 10). The legacy <dir>/<name>/<name>.ww
* form was dropped in task #22 — directory-as-module enumeration
* replaces it, mirroring ref/hare/hare/module/srcs.ha (Hare has no
* fallback matching `foo/foo.ha`; a module IS the directory). */
static int
locate_import_in(const char *dir, const char *path_form, char *out,
size_t outsz, int *is_dir, int want_dir)
{
struct stat st;
if (want_dir) {
snprintf(out, outsz, "%s/%s", dir, path_form);
if (stat(out, &st) == 0 && S_ISDIR(st.st_mode)) {
*is_dir = 1;
return 1;
}
return 0;
}
snprintf(out, outsz, "%s/%s.ww", dir, path_form);
if (access(out, 0) == 0) {
*is_dir = 0;
return 1;
}
return 0;
}
/* Walk a colon-separated dirlist trying to resolve `path_form`. Returns
* 1 on the first hit and writes the concrete path + dir/file marker.
*
* #98: "a module IS the directory" — a directory-package on ANY entry
* wins over a same-named sibling FILE on an EARLIER entry. The driver
* builds the searchpath srcd-first; a co-located `lib/<mod>/<mod>test.ww`
* entry makes srcd = lib/<mod>, so a self-named `import <mod>` would
* else file-hit the sibling lib/<mod>/<mod>.ww and fold it
* inline under the wrong module-reset → "package <mod> does not match
* import path <importer>". Two passes — directories first, files only
* if no directory matches anywhere — let lib/<mod>/ resolve as the dir
* while a genuine leaf package with no directory (e.g. lib/encoding/hex
* imported bare as `hex`, reachable only via its file in srcd) still
* resolves in the file pass. Latent: a dir-package now beats an
* earlier-entry same-named sibling FILE — loud-failing, none in the
* corpus; tracked as #101. */
static int
locate_import(const char *dirs, const char *path_form, char *out,
size_t outsz, int *is_dir)
{
for (int want_dir = 1; want_dir >= 0; want_dir--) {
const char *p = dirs;
while (*p) {
const char *e = strchr(p, ':');
size_t n = e ? (size_t)(e - p) : strlen(p);
if (n > 0 && n < outsz) {
char dir[1024];
if (n >= sizeof dir) n = sizeof dir - 1;
memcpy(dir, p, n);
dir[n] = '\0';
if (locate_import_in(dir, path_form, out,
outsz, is_dir, want_dir)) return 1;
}
if (!e) break;
p = e + 1;
}
}
return 0;
}
/* memcmp-based string compare for qsort. Byte-wise total order is
* locale-independent; rule-10 byte-id requires the two stages sort
* the same way. (strcmp would work today but Hare-fidelity points
* at memcmp via ref/hare/sort/cmp/cmp.ha:9.) */
static int
strs_cmp(const void *a, const void *b)
{
const char *sa = *(const char *const *)a;
const char *sb = *(const char *const *)b;
return strcmp(sa, sb);
}
/* A temporary migration rule keeps the pre-underscore corpus working without
* reserving every filename ending in "test.ww": only a real line-leading
* @test declaration makes a noncanonical source test-only. */
static int
file_has_line_test(const char *path)
{
char line[4096];
FILE *f = fopen(path, "rb");
if (f == NULL) return 0;
int found = 0;
while (fgets(line, sizeof line, f) != NULL) {
char *p = line;
while (*p == ' ' || *p == '\t' || *p == '\r') p++;
if (strncmp(p, "@test", 5) == 0
&& (p[5] == ' ' || p[5] == '\t')) {
found = 1;
break;
}
}
fclose(f);
return found;
}
/* enumerate_dir_ww — collect production *.ww paths in `dirpath`, excluding
* canonical *_test.ww plus the explicit line-leading-@test compatibility
* sources, then sort byte-wise. This is the sole directory-membership
* discovery path; the owning seppkg retains the returned list. */
static int
enumerate_dir_ww(const char *dirpath, char ***out_files)
{
DIR *d = opendir(dirpath);
if (d == NULL) { *out_files = NULL; return -1; }
char **arr = NULL;
int n = 0, cap = 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;
char path[2048];
snprintf(path, sizeof path, "%s/%s", dirpath, nm);
if ((nl >= 8 && strcmp(nm + nl - 8, "_test.ww") == 0)
|| file_has_line_test(path))
continue;
if (n + 1 > cap) {
cap = cap ? cap * 2 : 8;
arr = realloc(arr, cap * sizeof *arr);
}
arr[n++] = strdup(path);
}
closedir(d);
if (n > 1) qsort(arr, n, sizeof *arr, strs_cmp);
*out_files = arr;
return n;
}
/* ====================================================================
* ww build — separate-compilation driver (task #46/c3).
* ====================================================================
* This is the SOLE build path (E3-C1 flip, task #87): the legacy
* single-file amalgamator is gone. Each imported
* package's `.wwi` interface is materialized and every package is
* compiled on its own (`w6c -c`), then the `.o` set is flat-linked.
*
* Each w6c pass is BOTH consumer (reads dep `.wwi` as import scope) AND
* producer (writes this package's `.wwi` for its importers via -I), so
* a package's interface is materialized as a side effect of compiling
* it. Reverse-topo order guarantees a package's deps' `.wwi` exist
* before it compiles.
*
* The load-bearing rule (ken #56, rob-resolved): every dep is tagged by
* its FULL DOTTED import path on prepend (`//ww:module <path>`), so the
* definer's qualified symbol (#53) equals the consumer's qualified
* reference (#40) and the sep `.o`s link. A dep is NEVER bare-embedded.
*
* The prepend is the TRANSITIVE closure of a package's deps (lead-
* ratified, superseding rob-c3-spec §1.3 "direct deps"): a dep's public
* interface can name a transitive dep's type (os exposes time.instant),
* so the consuming unit needs the whole closure for name RESOLUTION —
* direct-deps-only does not type-check. This mirrors harec reading the
* transitive `.td` closure and is consistent with the current flat-unit
* transitive-namespace model (the visibility tighten is task #45,
* deferred post-M4).
*/
#define SEP_MAXPKG 256
struct seppkg {
char path[256]; /* dotted import path; "" == root/primary */
char entry[1024]; /* resolved package dir (or file, for a file root) */
char name[256]; /* validated declared name; directory packages only */
char **sources; /* owned, byte-sorted production paths; dirs only */
int nsources;
int is_dir;
int deps[SEP_MAXPKG]; /* direct-dep indices into sepgraph.pkg */
int ndeps;
int color; /* tri-color DFS: 0 white, 1 gray, 2 black */
};
struct sepgraph {
struct seppkg pkg[SEP_MAXPKG];
int n;
};
/* Find a package by dotted path, or add it. Returns its index, -1 full. */
static int
sep_find_or_add(struct sepgraph *g, const char *path, const char *entry,
int is_dir)
{
for (int i = 0; i < g->n; i++)
if (strcmp(g->pkg[i].path, path) == 0) return i;
if (g->n >= SEP_MAXPKG) {
fprintf(stderr, "ww: too many packages (limit %d)\n",
SEP_MAXPKG);
return -1;
}
struct seppkg *p = &g->pkg[g->n];
snprintf(p->path, sizeof p->path, "%s", path);
snprintf(p->entry, sizeof p->entry, "%s", entry);
p->is_dir = is_dir;
p->name[0] = '\0';
p->sources = NULL;
p->nsources = 0;
p->ndeps = 0;
p->color = 0;
return g->n++;
}
/* Release the one package-owned directory-membership list. Every graph exit
* funnels through this function; regular-file nodes own no source list. */
static void
sep_graph_free(struct sepgraph *g)
{
if (g == NULL) return;
for (int i = 0; i < g->n; i++) {
for (int j = 0; j < g->pkg[i].nsources; j++)
free(g->pkg[i].sources[j]);
free(g->pkg[i].sources);
}
free(g);
}
/* Sanitize a package's dotted path into a scratch-file basename. Dots
* stay (legal in filenames); the root's empty path becomes "__root". */
static void
sep_fname(const struct sepgraph *g, int pi, const char *scratch,
const char *suffix, char *out, size_t outsz)
{
const char *base = g->pkg[pi].path[0] ? g->pkg[pi].path : "__root";
snprintf(out, outsz, "%s/%s%s", scratch, base, suffix);
}
/* unit_has_package — does `path` declare `package <leaf>;` ANYWHERE?
* #16 ENFORCE-driver (rob A): distinguishes a genuinely-missing import
* from one satisfied by an INLINE package in the same unit. Unlike a
* first-package-decl scan, this checks every line — single-file
* multi-package fixtures carry several `package` decls. The
* comment-skip line scan + name match are uncapped. The
* wwstage twin unithaspackage must stay byte-identical (rule 10). */
static int
unit_has_package(const char *path, const char *leaf)
{
FILE *in = fopen(path, "rb");
if (in == NULL) return 0;
char line[2048];
int found = 0;
while (fgets(line, sizeof line, in)) {
const char *p = line;
while (*p == ' ' || *p == '\t') p++;
if (p[0] == '/' && p[1] == '/') continue;
if (strncmp(p, "package ", 8) != 0
&& strncmp(p, "package\t", 8) != 0) continue;
p += 8;
while (*p == ' ' || *p == '\t') p++;
size_t i = 0;
while (leaf[i] != '\0' && leaf[i] == p[i]) i++;
if (leaf[i] == '\0') {
char c = p[i];
if (c == ';' || c == ' ' || c == '\t'
|| c == '\n' || c == '\0') { found = 1; break; }
}
}
fclose(in);
return found;
}
static int
sep_ident_start(int c)
{
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_';
}
static int
sep_ident_continue(int c)
{
return sep_ident_start(c) || (c >= '0' && c <= '9');
}
/* Skip the whitespace and comments accepted before and within the leading
* package clause. This is deliberately only the loader's small header
* grammar, not a second compiler lexer. */
static int
sep_skip_space(const char *src, size_t n, size_t *off)
{
size_t i = *off;
for (;;) {
while (i < n && (src[i] == ' ' || src[i] == '\t'
|| src[i] == '\r' || src[i] == '\n'))
i++;
if (i + 1 < n && src[i] == '/' && src[i + 1] == '/') {
i += 2;
while (i < n && src[i] != '\n') i++;
continue;
}
if (i + 1 < n && src[i] == '/' && src[i + 1] == '*') {
i += 2;
while (i + 1 < n && !(src[i] == '*' && src[i + 1] == '/'))
i++;
if (i + 1 >= n) return -1;
i += 2;
continue;
}
break;
}
*off = i;
return 0;
}
/* Parse exactly the leading loader grammar `package ident;`. */
static int
sep_package_clause(const char *src, size_t n, char *name, size_t namesz)
{
static const char kw[] = "package";
size_t i = 0;
if (sep_skip_space(src, n, &i) < 0
|| i + sizeof kw - 1 >= n
|| memcmp(src + i, kw, sizeof kw - 1) != 0)
return -1;
i += sizeof kw - 1;
if (i >= n || (src[i] != ' ' && src[i] != '\t'
&& src[i] != '\r' && src[i] != '\n'))
return -1;
if (sep_skip_space(src, n, &i) < 0 || i >= n
|| !sep_ident_start((unsigned char)src[i]))
return -1;
size_t begin = i++;
while (i < n && sep_ident_continue((unsigned char)src[i])) i++;
size_t len = i - begin;
if (len + 1 > namesz || sep_skip_space(src, n, &i) < 0
|| i >= n || src[i] != ';')
return -1;
memcpy(name, src + begin, len);
name[len] = '\0';
return 0;
}
/* Scan one source file's already-selected bytes for its leading package
* clause (when it is an owned directory source) and top-level imports. 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
* rather than concatenating bytes the way the legacy amalgamator did
* (§1.1). */
static int
sep_scan_file(struct sepgraph *g, int pi, const char *file,
const char *searchpath, struct ImportSet *filevisit, int owned_source)
{
if (import_seen(filevisit, file)) return 0;
import_add(filevisit, file);
FILE *in = fopen(file, "rb");
if (in == NULL) {
fprintf(stderr, "ww: cannot read %s\n", file);
return -1;
}
if (fseek(in, 0, SEEK_END) != 0) { fclose(in); return -1; }
long flen = ftell(in);
if (flen < 0 || fseek(in, 0, SEEK_SET) != 0) {
fclose(in);
return -1;
}
char *buf = malloc((size_t)flen + 1);
if (buf == NULL) { fclose(in); return -1; }
if (fread(buf, 1, (size_t)flen, in) != (size_t)flen) {
free(buf);
fclose(in);
return -1;
}
buf[flen] = '\0';
fclose(in);
if (owned_source) {
char declared[256];
if (sep_package_clause(buf, (size_t)flen, declared,
sizeof declared) < 0) {
fprintf(stderr, "ww: %s: invalid or missing package clause\n",
file);
free(buf);
return -1;
}
struct seppkg *pkg = &g->pkg[pi];
if (pkg->name[0] == '\0')
snprintf(pkg->name, sizeof pkg->name, "%s", declared);
else if (strcmp(pkg->name, declared) != 0) {
fprintf(stderr,
"ww: %s: conflicting package names %s and %s\n",
pkg->entry, pkg->name, declared);
free(buf);
return -1;
}
}
int rc = 0;
for (size_t off = 0; off < (size_t)flen && rc == 0;) {
size_t end = off;
while (end < (size_t)flen && buf[end] != '\n') end++;
const char *p = buf + off;
const char *lineend = buf + end;
while (p < lineend && (*p == ' ' || *p == '\t')) p++;
if ((size_t)(lineend - p) < 7
|| (memcmp(p, "import ", 7) != 0
&& memcmp(p, "import\t", 7) != 0)) {
off = end < (size_t)flen ? end + 1 : end;
continue;
}
p += 7;
while (p < lineend && (*p == ' ' || *p == '\t')) p++;
char name[256] = {0};
int j = 0;
while (p < lineend && ((*p >= 'a' && *p <= 'z')
|| (*p >= 'A' && *p <= 'Z') || *p == '_' || *p == '.'
|| (*p >= '0' && *p <= '9')))
if (j + 1 < (int)sizeof name) name[j++] = *p++;
if (j == 0) {
off = end < (size_t)flen ? end + 1 : end;
continue;
}
char path_form[256];
import_path_form(name, path_form, sizeof path_form);
char ipath[1024];
int is_dir = 0;
/* #16 ENFORCE-driver: an unresolvable import is a hard error,
* not a silent skip — EXCEPT when the package is defined INLINE
* in the same unit (single-file multi-package; leaf = the last
* dotted component). E3-C1 (#87): the legacy amalgamator that
* used to own the genuine-missing case is gone, so the sep
* producer enforces it here (INV-2, by construction). */
if (!locate_import(searchpath, path_form, ipath, sizeof ipath,
&is_dir)) {
const char *dot = strrchr(name, '.');
const char *leaf = dot ? dot + 1 : name;
if (unit_has_package(file, leaf))
goto next_line; /* inline-satisfied */
fprintf(stderr, "ww: cannot find package %s\n", name);
rc = -1;
goto next_line;
}
if (is_dir) {
int di = sep_find_or_add(g, name, ipath, 1);
if (di < 0) { rc = -1; break; }
int seen = 0;
for (int k = 0; k < g->pkg[pi].ndeps; k++)
if (g->pkg[pi].deps[k] == di) { seen = 1; break; }
if (!seen) {
if (g->pkg[pi].ndeps >= SEP_MAXPKG) { rc = -1; break; }
g->pkg[pi].deps[g->pkg[pi].ndeps++] = di;
}
} else if (sep_scan_file(g, pi, ipath, searchpath, filevisit, 0) < 0) {
rc = -1; break;
}
next_line:
off = end < (size_t)flen ? end + 1 : end;
}
free(buf);
return rc;
}
/* Load package pi once: a directory node takes ownership of its sorted
* production paths, then the same stored list supplies package-name
* validation and dependency scanning. Recurse over the resulting edges.
* `color` doubles as a loaded marker here (2 == loaded); it is reset to
* white before the topo pass. */
static int
sep_load_pkg(struct sepgraph *g, int pi, const char *searchpath)
{
if (g->pkg[pi].color == 2) return 0;
g->pkg[pi].color = 2;
struct ImportSet fv = {0};
int rc = 0;
if (g->pkg[pi].is_dir) {
g->pkg[pi].nsources = enumerate_dir_ww(g->pkg[pi].entry,
&g->pkg[pi].sources);
if (g->pkg[pi].nsources < 0) {
fprintf(stderr, "ww: cannot read directory %s\n",
g->pkg[pi].entry);
rc = -1;
} else if (g->pkg[pi].nsources == 0) {
fprintf(stderr,
"ww: %s: directory contains no WW package sources\n",
g->pkg[pi].entry);
rc = -1;
}
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') {
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) {
fprintf(stderr,
"ww: package %s does not match import path %s\n",
g->pkg[pi].name, g->pkg[pi].path);
rc = -1;
}
}
} else {
rc = sep_scan_file(g, pi, g->pkg[pi].entry, searchpath, &fv, 0);
}
for (int i = 0; i < fv.n; i++) free(fv.paths[i]);
free(fv.paths);
if (rc < 0) return rc;
/* recurse into freshly-added deps (sep_find_or_add may have grown
* g->n during the scan; iterate by index). */
for (int k = 0; k < g->pkg[pi].ndeps; k++)
if (sep_load_pkg(g, g->pkg[pi].deps[k], searchpath) < 0)
return -1;
return 0;
}
/* DFS post-order over the dep DAG → reverse-topo (deps before importer),
* cite Hare gather (deps.ha:123). Tri-color: a gray back-edge is a loud
* dep-cycle reject naming the chain (Hare deps.ha:243); `stack[0..depth)`
* is the live DFS path, so the cycle runs from pi's first occurrence on
* it to the top, closing back on pi. */
static int
sep_topo_visit(struct sepgraph *g, int pi, int *order, int *no,
int *stack, int depth)
{
if (g->pkg[pi].color == 2) return 0;
if (g->pkg[pi].color == 1) {
int j = 0;
while (j < depth && stack[j] != pi) j++;
fprintf(stderr, "ww: dependency cycle: ");
for (int s = j; s < depth; s++)
fprintf(stderr, "%s -> ", g->pkg[stack[s]].path[0]
? g->pkg[stack[s]].path : "(root)");
fprintf(stderr, "%s\n",
g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)");
return -1;
}
g->pkg[pi].color = 1;
stack[depth] = pi;
for (int k = 0; k < g->pkg[pi].ndeps; k++)
if (sep_topo_visit(g, g->pkg[pi].deps[k], order, no,
stack, depth + 1) < 0)
return -1;
g->pkg[pi].color = 2;
order[(*no)++] = pi;
return 0;
}
/* Mark pi's transitive deps (excluding pi) in inset[]. */
static void
sep_mark_deps(struct sepgraph *g, int pi, char *inset)
{
for (int k = 0; k < g->pkg[pi].ndeps; k++) {
int di = g->pkg[pi].deps[k];
if (!inset[di]) { inset[di] = 1; sep_mark_deps(g, di, inset); }
}
}
/* Emit one of pi's own source files into the sep-unit under the
* //ww:module-reset primary boundary (so -c emits its decls, imported
* ==0). DIRECTORY imports are skipped (provided as `.wwi` ahead of the
* body); FILE imports fold in (intra-package split). */
static void
sep_emit_body(FILE *out, const char *path, struct ImportSet *visited,
const char *searchpath, const char *modpath)
{
if (import_seen(visited, path)) return;
import_add(visited, path);
FILE *in = fopen(path, "rb");
if (in == NULL) {
fprintf(stderr, "ww: cannot read %s\n", path);
return;
}
char line[2048];
while (fgets(line, sizeof line, in)) {
const char *p = line;
while (*p == ' ' || *p == '\t') p++;
if (strncmp(p, "import ", 7) != 0 && strncmp(p, "import\t", 7) != 0)
continue;
p += 7;
while (*p == ' ' || *p == '\t') p++;
char name[256] = {0};
int j = 0;
while ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z')
|| *p == '_' || *p == '.' || (*p >= '0' && *p <= '9'))
if (j + 1 < (int)sizeof name) name[j++] = *p++;
if (j == 0) continue;
char path_form[256];
import_path_form(name, path_form, sizeof path_form);
char ipath[1024];
int is_dir = 0;
if (!locate_import(searchpath, path_form, ipath, sizeof ipath,
&is_dir))
continue;
if (!is_dir)
sep_emit_body(out, ipath, visited, searchpath, modpath);
}
/* #57: tag the primary body by its full dotted import path so the
* definer mangles == the importer reference; a root build (path "")
* stays a bare reset (keeps bare main). */
if (modpath != NULL && modpath[0] != '\0')
fprintf(out, "//ww:module-reset %s\n", modpath);
else
fputs("//ww:module-reset\n", out);
rewind(in);
int ch;
while ((ch = fgetc(in)) != EOF) fputc(ch, out);
fputc('\n', out);
fclose(in);
}
/* Compose pi's sep-unit at `unitf`: the transitive-closure `.wwi`s
* (reverse-topo order, each tagged by its dotted path), then pi's own
* body under //ww:module-reset. Directory membership comes only from the
* package node loaded before planning. */
static int
sep_compose_unit(struct sepgraph *g, int pi, const char *scratch,
const int *order, int norder, const char *searchpath, const char *unitf)
{
FILE *u = fopen(unitf, "wb");
if (u == NULL) {
fprintf(stderr, "ww: cannot open %s\n", unitf);
return -1;
}
char inset[SEP_MAXPKG] = {0};
sep_mark_deps(g, pi, inset);
for (int oi = 0; oi < norder; oi++) {
int dj = order[oi];
if (dj == pi || !inset[dj]) continue;
char wwi[1024];
sep_fname(g, dj, scratch, ".wwi", wwi, sizeof wwi);
FILE *wf = fopen(wwi, "rb");
if (wf == NULL) {
fprintf(stderr, "ww: missing %s\n", wwi);
fclose(u);
return -1;
}
fprintf(u, "//ww:module %s\n", g->pkg[dj].path);
int ch;
while ((ch = fgetc(wf)) != EOF) fputc(ch, u);
fputc('\n', u);
fclose(wf);
}
struct ImportSet bodyvisit = {0};
if (g->pkg[pi].is_dir) {
for (int i = 0; i < g->pkg[pi].nsources; i++)
sep_emit_body(u, g->pkg[pi].sources[i], &bodyvisit,
searchpath, g->pkg[pi].path);
} else {
sep_emit_body(u, g->pkg[pi].entry, &bodyvisit, searchpath,
g->pkg[pi].path);
}
for (int i = 0; i < bodyvisit.n; i++) free(bodyvisit.paths[i]);
free(bodyvisit.paths);
fclose(u);
return 0;
}
/* archive_o — write a deterministic single-member SysV ar archive at
* `apath` wrapping the object at `objpath`. No armap / long-name table:
* w6l reads each member's ELF .symtab directly (obj.c elf_globals) and
* skips '/'-named members, so a package `.a` needs only the global magic,
* one 60-byte member header, and the `.o` bytes (newline-padded to even).
* Zeroed mtime/uid/gid + fixed mode + a fixed member name make the bytes
* a pure function of the `.o` content → cstage `.a` == wwstage `.a`
* (rule 10). The wwstage twin is archiveo (selfhost/cmd/ww/main.ww). */
static int
archive_o(const char *objpath, const char *apath)
{
FILE *in = fopen(objpath, "rb");
if (in == NULL) {
fprintf(stderr, "ww: cannot read %s\n", objpath);
return -1;
}
fseek(in, 0, SEEK_END);
long n = ftell(in);
fseek(in, 0, SEEK_SET);
if (n < 0) { fclose(in); return -1; }
unsigned char *buf = malloc((size_t)n);
if (buf == NULL) { fclose(in); return -1; }
if (fread(buf, 1, (size_t)n, in) != (size_t)n) {
free(buf); fclose(in); return -1;
}
fclose(in);
FILE *out = fopen(apath, "wb");
if (out == NULL) {
fprintf(stderr, "ww: cannot open %s\n", apath);
free(buf);
return -1;
}
fwrite("!<arch>\n", 1, 8, out);
/* ar(5) fixes each member header at 60 bytes; the offsets below
* address fields in that serialized header. */
char hdr[60];
memset(hdr, ' ', sizeof hdr);
memcpy(hdr + 0, "pkg.o/", 6); /* GNU short-name '/' terminator */
hdr[16] = '0'; /* mtime (zeroed → determinism) */
hdr[28] = '0'; /* uid (zeroed) */
hdr[34] = '0'; /* gid (zeroed) */
memcpy(hdr + 40, "100644", 6); /* mode (fixed octal) */
char sz[12];
int szn = snprintf(sz, sizeof sz, "%lu", (unsigned long)n);
memcpy(hdr + 48, sz, (size_t)szn);
hdr[58] = 0x60; /* member-header magic byte */
hdr[59] = 0x0a;
fwrite(hdr, 1, sizeof hdr, out);
fwrite(buf, 1, (size_t)n, out);
if (n & 1) fputc('\n', out); /* members are 2-byte aligned */
fclose(out);
free(buf);
return 0;
}
/* ---- -w workdir freshness ---------------------------------------------
* A `-w DIR` workdir is a caller-owned persistent package-artifact tree
* that replaces the fresh `.sepwork` scratch. Staleness is pure content
* identity, never mtime: a package is reused only when its freshly
* composed unit byte-equals the committed unit AND the tool copies
* recorded in the dir byte-equal the live tools — every decision is
* reproducible by hand with cmp(1) against plain files. Artifacts commit
* via temp + rename with the unit renamed last, so a killed build can
* never leave a committed unit vouching for uncommitted artifacts. The
* caller serializes invocations per workdir (Make target = one workdir)
* and `make clean` reclaims the state; the wwstage twin is the
* fileequal/copyfileatomic/workdirstamp group in selfhost/cmd/ww/main.ww. */
/* `.s`/`.wwi` may be legitimately empty (an FFI-only package like rt
* emits no text), so committed presence is their freshness test; the
* rename-commit protocol owns integrity. `.o`/`.a` are never empty
* (ELF/ar headers), so a zero size there is always a torn write. */
static int
file_is_reg(const char *path)
{
struct stat st;
return stat(path, &st) == 0 && S_ISREG(st.st_mode);
}
static int
file_size_nonzero(const char *path)
{
struct stat st;
return stat(path, &st) == 0 && S_ISREG(st.st_mode) && st.st_size > 0;
}
/* Byte equality of two files; absence or IO error is inequality. */
static int
file_equal(const char *a, const char *b)
{
FILE *fa = fopen(a, "rb");
if (fa == NULL) return 0;
FILE *fb = fopen(b, "rb");
if (fb == NULL) { fclose(fa); return 0; }
static char ba[65536], bb[65536];
int eq = 1;
for (;;) {
size_t na = fread(ba, 1, sizeof ba, fa);
size_t nb = fread(bb, 1, sizeof bb, fb);
if (na != nb || memcmp(ba, bb, na) != 0) { eq = 0; break; }
if (na < sizeof ba) {
if (ferror(fa) || ferror(fb)) eq = 0;
break;
}
}
fclose(fa); fclose(fb);
return eq;
}
/* Replace dst with src's bytes via temp + rename, so a torn write can
* never masquerade as a committed tool copy. */
static int
copy_file_atomic(const char *src, const char *dst)
{
char tmp[1100];
snprintf(tmp, sizeof tmp, "%s.new", dst);
FILE *in = fopen(src, "rb");
if (in == NULL) return -1;
FILE *out = fopen(tmp, "wb");
if (out == NULL) { fclose(in); return -1; }
static char buf[65536];
size_t n;
while ((n = fread(buf, 1, sizeof buf, in)) > 0)
if (fwrite(buf, 1, n, out) != n) {
fclose(in); fclose(out); return -1;
}
int bad = ferror(in);
fclose(in);
if (fclose(out) != 0 || bad) return -1;
return rename(tmp, dst);
}
/* The stamp pins the non-content build inputs a unit compare cannot see:
* the -T/-S shape of the producer pass and the artifact protocol
* revision (bump "fmt" when the unit/archive/commit format changes). */
static void
workdir_stamp_text(char *buf, size_t bufsz, int is_test, int emit_asm)
{
snprintf(buf, bufsz, "ww workdir fmt 1 mode %s asm %d\n",
is_test ? "test" : "build", emit_asm);
}
/* build_one_sep — discover_deps, reverse_topo,
* the transitive producer loop (one `w6c -c -I` per package, dep-first,
* each DEP `.o` wrapped in its own deterministic `.a`), then a
* reverse-topo `w6l` of the root `.o` + dep `.a` set + libwwrt.a. Side
* files land in a cold `<stem>.sepwork` dir, or under the persistent
* `-w` workdir with content-identity package reuse. */
static int
build_one_sep_impl(const char *src, int entry_is_dir, const char *out,
const char *objstem, const char *extra_includes, const char *extra_libs,
const char *extra_libdirs, int is_test, int emit_asm, const char *workdir,
char *scratchout, size_t scratchoutsz, struct sepgraph **graphout)
{
const char *c6 = toolpath("WW_W6C", "w6c");
const char *a6 = toolpath("WW_W6A", "w6a");
const char *l6 = toolpath("WW_W6L", "w6l");
const char *libdir = getenv("WW_LIB");
if (libdir == NULL || libdir[0] == 0) {
static char libbuf[1024];
snprintf(libbuf, sizeof libbuf, "%s/../lib", self_dir);
libdir = libbuf;
}
const char *srcdir = getenv("WW_SRCLIB");
static char srcbuf[1024];
if (srcdir == NULL || srcdir[0] == 0) {
snprintf(srcbuf, sizeof srcbuf, "%s/../../lib", self_dir);
if (access(srcbuf, 0) == 0) srcdir = srcbuf;
else if (access("lib", 0) == 0) srcdir = "lib";
else srcdir = libdir;
}
/* search path: source-dir, then -I dirs, then srcdir. */
char srcd[1024];
if (entry_is_dir) {
snprintf(srcd, sizeof srcd, "%s", src);
size_t n = strlen(srcd);
while (n > 1 && srcd[n-1] == '/') srcd[--n] = '\0';
} else {
const char *slash = strrchr(src, '/');
if (slash) {
size_t n = (size_t)(slash - src);
if (n >= sizeof srcd) n = sizeof srcd - 1;
memcpy(srcd, src, n);
srcd[n] = '\0';
} else { srcd[0] = '.'; srcd[1] = '\0'; }
}
static char searchpath[4096];
if (extra_includes && extra_includes[0])
snprintf(searchpath, sizeof searchpath, "%s:%s:%s",
srcd, extra_includes, srcdir);
else
snprintf(searchpath, sizeof searchpath, "%s:%s", srcd, srcdir);
srcdir = searchpath;
char stem[1024];
if (entry_is_dir) {
const char *b = strrchr(srcd, '/');
const char *base = b ? b + 1 : srcd;
snprintf(stem, sizeof stem, "%s/%s", srcd, base);
} else {
snprintf(stem, sizeof stem, "%s", src);
char *dot = strrchr(stem, '.');
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
}
const char *ostem = (objstem && objstem[0]) ? objstem : stem;
int warm = workdir != NULL && workdir[0] != 0;
char scratch[1100];
if (warm) {
struct stat wst;
if (stat(workdir, &wst) != 0 || !S_ISDIR(wst.st_mode)) {
fprintf(stderr, "ww: workdir %s is not a directory\n",
workdir);
return 1;
}
/* The workdir is caller-owned and persistent: no acquisition,
* no refusal, and scratchout stays empty so the wrapper never
* cleans it. */
snprintf(scratch, sizeof scratch, "%s", workdir);
} else {
snprintf(scratch, sizeof scratch, "%s.sepwork", ostem);
if (mkdir(scratch, 0755) != 0) {
fprintf(stderr, "ww: cannot create scratch %s\n", scratch);
return 1;
}
/* Hand the scratch path back only after mkdir succeeds. The
* wrapper therefore never removes a pre-existing path that this
* build failed to acquire. */
if (scratchout) snprintf(scratchout, scratchoutsz, "%s", scratch);
}
int stale_all = 0, stampok = 0;
char toolc[1200] = {0}, toola[1200] = {0}, stampf[1200] = {0};
char stampwant[128];
if (warm) {
snprintf(toolc, sizeof toolc, "%s/.wwtool.w6c", scratch);
snprintf(toola, sizeof toola, "%s/.wwtool.w6a", scratch);
snprintf(stampf, sizeof stampf, "%s/.wwtool.stamp", scratch);
workdir_stamp_text(stampwant, sizeof stampwant, is_test, emit_asm);
char got[128] = {0};
FILE *sf = fopen(stampf, "rb");
if (sf) {
size_t rn = fread(got, 1, sizeof got - 1, sf);
got[rn] = 0;
fclose(sf);
}
stampok = strcmp(stampwant, got) == 0;
if (!stampok || !file_equal(toolc, c6)
|| (!emit_asm && !file_equal(toola, a6)))
stale_all = 1;
}
struct sepgraph *g = calloc(1, sizeof *g);
if (g == NULL) return 1;
if (graphout) *graphout = g;
int root = sep_find_or_add(g, "", src, entry_is_dir);
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. */
if (is_test) {
char tpath[1024];
int tdir = 0;
if (locate_import(srcdir, "test", tpath, sizeof tpath, &tdir)) {
int ti = sep_find_or_add(g, "test", tpath, tdir);
if (ti < 0) return 1;
int seen = 0;
for (int k = 0; k < g->pkg[root].ndeps; k++)
if (g->pkg[root].deps[k] == ti) { seen = 1; break; }
if (!seen && g->pkg[root].ndeps < SEP_MAXPKG)
g->pkg[root].deps[g->pkg[root].ndeps++] = ti;
}
}
if (sep_load_pkg(g, root, srcdir) < 0) return 1;
for (int i = 0; i < g->n; i++) g->pkg[i].color = 0;
int *order = calloc((size_t)g->n, sizeof *order);
int *stack = calloc((size_t)g->n, sizeof *stack);
int norder = 0;
if (order == NULL || stack == NULL ||
sep_topo_visit(g, root, order, &norder, stack, 0) < 0) {
free(stack); free(order); return 1;
}
free(stack);
/* producer loop — dep-first, one `w6c -c -I` pass per package. */
for (int oi = 0; oi < norder; oi++) {
int pi = order[oi];
char unitf[1024], wwi[1024], asmf[1024], obj[1024], apath[1024];
char unitnew[1024], wwinew[1024], asmnew[1024], objnew[1024];
char anew[1024], cmd[8192];
sep_fname(g, pi, scratch, ".unit.ww", unitf, sizeof unitf);
sep_fname(g, pi, scratch, ".wwi", wwi, sizeof wwi);
sep_fname(g, pi, scratch, ".s", asmf, sizeof asmf);
sep_fname(g, pi, scratch, ".o", obj, sizeof obj);
sep_fname(g, pi, scratch, ".a", apath, sizeof apath);
sep_fname(g, pi, scratch, ".unit.new", unitnew, sizeof unitnew);
sep_fname(g, pi, scratch, ".wwi.new", wwinew, sizeof wwinew);
sep_fname(g, pi, scratch, ".s.new", asmnew, sizeof asmnew);
sep_fname(g, pi, scratch, ".o.new", objnew, sizeof objnew);
sep_fname(g, pi, scratch, ".a.new", anew, sizeof anew);
/* Warm mode compiles from staged `.new` paths and commits by
* rename; classic mode keeps its exact in-place paths. */
const char *cu = warm ? unitnew : unitf;
const char *cw = warm ? wwinew : wwi;
const char *cs = warm ? asmnew : asmf;
const char *co = warm ? objnew : obj;
const char *ca = warm ? anew : apath;
if (sep_compose_unit(g, pi, scratch, order, norder, srcdir,
cu) < 0) { free(order); return 1; }
if (warm && !stale_all && file_equal(unitnew, unitf)
&& file_is_reg(asmf)
&& (pi == root || file_is_reg(wwi))
&& (emit_asm || (file_size_nonzero(obj)
&& (pi == root || file_size_nonzero(apath))))) {
if (unlink(unitnew) != 0) {
fprintf(stderr, "ww: cannot remove %s\n",
unitnew);
free(order); return 1;
}
continue;
}
/* BUG-1 (#69): -I <wwi> is purely the root's UNUSED
* `.wwi` output path, but it triggers wwi_emit →
* check_exported_type on the root. A terminal binary's
* root legitimately has `export fn` over an unexported
* LOCAL type (the root is never imported), which the
* export-check rejects. Skip -I for the root; its `.wwi`
* is never consumed. */
if (pi == root)
/* #79: the root carries -T under `ww test`
* so w6c synthesizes the test main. Deps never
* get -T. */
snprintf(cmd, sizeof cmd, "%s %s-c -o %s %s",
c6, is_test ? "-T " : "", cs, cu);
else
snprintf(cmd, sizeof cmd, "%s -c -I %s -o %s %s",
c6, cw, cs, cu);
if (run(cmd) != 0) {
fprintf(stderr, "ww: w6c failed for %s\n",
g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)");
free(order); return 1;
}
if (!emit_asm) {
snprintf(cmd, sizeof cmd, "%s -o %s %s", a6, co, cs);
if (run(cmd) != 0) {
fprintf(stderr, "ww: w6a failed for %s\n",
g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)");
free(order); return 1;
}
}
/* wrap each DEP package's `.o` in its own deterministic `.a`
* (5a). The ROOT stays a positional `.o` (force-loaded — it's
* the build target, always fully linked), so `main` is defined
* before any archive is processed. The link consumes `.o`/`.a`,
* never `.wwi`. */
if (!emit_asm && pi != root) {
if (archive_o(co, ca) != 0) {
fprintf(stderr, "ww: archive failed for %s\n",
g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)");
free(order); return 1;
}
}
/* Commit order: artifacts before the unit that vouches for
* them, unit strictly last. */
if (warm) {
if ((pi != root && rename(wwinew, wwi) != 0)
|| rename(asmnew, asmf) != 0
|| (!emit_asm && rename(objnew, obj) != 0)
|| (!emit_asm && pi != root
&& rename(anew, apath) != 0)
|| rename(unitnew, unitf) != 0) {
fprintf(stderr, "ww: cannot commit %s\n",
g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)");
free(order); return 1;
}
}
}
/* Tool identity commits only after every package artifact it vouches
* for is itself committed; a killed pass leaves the old identity and
* forces a full recompile, never a false reuse. */
if (warm) {
if (!file_equal(toolc, c6)
&& copy_file_atomic(c6, toolc) != 0) {
fprintf(stderr, "ww: cannot record %s\n", toolc);
free(order); return 1;
}
if (!emit_asm && !file_equal(toola, a6)
&& copy_file_atomic(a6, toola) != 0) {
fprintf(stderr, "ww: cannot record %s\n", toola);
free(order); return 1;
}
if (!stampok) {
char stampnew[1300];
snprintf(stampnew, sizeof stampnew, "%s.new", stampf);
FILE *sf = fopen(stampnew, "wb");
int bad = sf == NULL || fputs(stampwant, sf) == EOF;
if (sf != NULL && fclose(sf) != 0) bad = 1;
if (bad || rename(stampnew, stampf) != 0) {
fprintf(stderr, "ww: cannot record %s\n",
stampf);
free(order); return 1;
}
}
}
if (emit_asm) { free(order); return 0; }
/* 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. */
char rtargs[2048] = {0};
char rtpath[1024];
snprintf(rtpath, sizeof rtpath, "%s/libwwrt.a", libdir);
if (access(rtpath, 0) == 0) {
snprintf(rtargs, sizeof rtargs, "%s", rtpath);
} else {
char a1[1024], a2[1024];
snprintf(a1, sizeof a1, "%s/../obj/rt/start.o", self_dir);
snprintf(a2, sizeof a2, "%s/../obj/rt/syscall.o", self_dir);
snprintf(rtargs, sizeof rtargs, "%s %s", a1, a2);
}
char objs[8192] = {0};
for (int oi = norder - 1; oi >= 0; oi--) {
char path[1024];
/* root: positional `.o` (force-load); deps: `.a` (selective). */
sep_fname(g, order[oi], scratch,
order[oi] == root ? ".o" : ".a", path, sizeof path);
size_t n = strlen(objs);
snprintf(objs + n, sizeof objs - n, "%s%s", n ? " " : "", path);
}
const char *libargs = (extra_libs && extra_libs[0]) ? extra_libs : "";
const char *libdirset = (extra_libdirs && extra_libdirs[0]) ? extra_libdirs : "";
char cmd[16384];
snprintf(cmd, sizeof cmd, "%s -o %s %s %s%s%s%s%s",
l6, out, objs, rtargs,
libdirset[0] ? " " : "", libdirset,
libargs[0] ? " " : "", libargs);
int rc = run(cmd);
free(order);
if (rc != 0) { fprintf(stderr, "ww: w6l failed\n"); return 1; }
return 0;
}
/* build_one_sep — thin wrapper over build_one_sep_impl. `ww build` and an
* explicit `ww test -o` retain caller-visible `.sepwork` artifacts; their
* caller owns that exact tree. `ww run` and a no-output single-file test use
* internal scratch and remove it on success and failure. One cleanup site
* covers every internal-scratch impl return. The path is nonempty only after
* this invocation successfully created the exact `.sepwork` tree. */
static int
build_one_sep(const char *src, int entry_is_dir, const char *out,
const char *objstem, const char *extra_includes, const char *extra_libs,
const char *extra_libdirs, int is_test, 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, out, objstem,
extra_includes, extra_libs, extra_libdirs, is_test, emit_asm,
workdir, scratch, sizeof scratch, &g);
sep_graph_free(g);
if (!keepscratch && scratch[0]) {
size_t sl = strlen(scratch);
if (sl > 8 && strcmp(scratch + sl - 8, ".sepwork") == 0) {
int cleanrc = 1;
pid_t pid = fork();
if (pid == 0) {
execl("/bin/rm", "rm", "-rf", "--", scratch,
(char *)NULL);
_exit(127);
}
if (pid > 0) {
int status = 0;
if (waitpid(pid, &status, 0) == pid &&
WIFEXITED(status))
cleanrc = WEXITSTATUS(status);
}
if (cleanrc != 0) {
fprintf(stderr, "ww: cannot remove scratch %s\n", scratch);
if (r == 0) r = 1;
}
}
}
return r;
}
static int
do_version(void)
{
printf("ww %s\n", WW_VERSION);
return 0;
}
/* Compose the standard module search path: cwd : <extra-includes> : $WW_LIB
* source dir. The `extra` string is colon-separated -I dirs from argv. */
static const char *
search_path(const char *extra, char *buf, size_t bufsz)
{
const char *libdir = getenv("WW_SRCLIB");
static char libbuf[1024];
if (libdir == NULL || libdir[0] == 0) {
libdir = getenv("WW_LIB");
}
if (libdir == NULL || libdir[0] == 0) {
snprintf(libbuf, sizeof libbuf, "%s/../../lib", self_dir);
if (access(libbuf, 0) == 0) libdir = libbuf;
else if (access("lib", 0) == 0) libdir = "lib";
else {
snprintf(libbuf, sizeof libbuf, "%s/../lib", self_dir);
libdir = libbuf;
}
}
if (extra && extra[0])
snprintf(buf, bufsz, ".:%s:%s", extra, libdir);
else
snprintf(buf, bufsz, ".:%s", libdir);
return buf;
}
/* basename_no_ext: last path segment with any trailing ".ww" stripped. */
static void
basename_no_ext(const char *path, char *out, size_t outsz)
{
const char *base = strrchr(path, '/');
base = base ? base + 1 : path;
snprintf(out, outsz, "%s", base);
char *dot = strrchr(out, '.');
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
}
/* resolve_module: turn a name into a concrete entry path.
* foo.ww → use as-is if it exists
* <existing dir> → returns the dir path (caller dir-enumerates)
* . → cwd as a directory
* foo (bare) → walk cwd:incs:WW_LIB; first hit is dir or file.
* Sets *is_dir on hit. Dir resolution drives directory-as-module
* enumeration in build_one_sep. */
static int
resolve_module(const char *name, const char *incs, char *out, size_t outsz,
int *is_dir)
{
struct stat st;
if (stat(name, &st) == 0) {
if (S_ISREG(st.st_mode)) {
snprintf(out, outsz, "%s", name);
*is_dir = 0;
return 1;
}
if (S_ISDIR(st.st_mode)) {
snprintf(out, outsz, "%s", name);
*is_dir = 1;
return 1;
}
}
char sp[4096];
search_path(incs, sp, sizeof sp);
char path_form[256];
import_path_form(name, path_form, sizeof path_form);
return locate_import(sp, path_form, out, outsz, is_dir);
}
/* Parse the standard -I/-L/-l/-o flags into incs/libdirs/libs/outpath. The
* first non-flag positional becomes *src_out. Returns the index past the last
* arg consumed for positionals (so callers can pick up trailing args), or -1
* if a flag is missing its argument (diagnostic already emitted). `cmd` names
* the subcommand for the diagnostic, byte-identical to the wwstage twin's
* per-subcommand wording (selfhost/cmd/ww/main.ww dobuild/dorun). */
static int
parse_build_flags(const char *cmd, int argc, char **argv,
char *incs, size_t incsz,
char *libdirs, size_t libdirsz,
char *libs, size_t libsz,
char *outpath, size_t outsz,
char *workdir, size_t workdirsz,
const char **src_out, int *emit_asm_out)
{
*src_out = NULL;
if (emit_asm_out) *emit_asm_out = 0;
int i = 0;
for (; i < argc; i++) {
if (strcmp(argv[i], "-S") == 0) {
if (emit_asm_out == NULL) {
fprintf(stderr, "ww %s: unknown flag\n", cmd);
return -1;
}
*emit_asm_out = 1;
} else if (strcmp(argv[i], "-w") == 0) {
if (workdir == NULL) {
fprintf(stderr, "ww %s: unknown flag\n", cmd);
return -1;
}
if (i + 1 >= argc) {
fprintf(stderr,
"ww %s: -w needs an argument\n", cmd);
return -1;
}
snprintf(workdir, workdirsz, "%s", argv[++i]);
} else if (strncmp(argv[i], "-w", 2) == 0 && argv[i][2]) {
if (workdir == NULL) {
fprintf(stderr, "ww %s: unknown flag\n", cmd);
return -1;
}
snprintf(workdir, workdirsz, "%s", argv[i] + 2);
} else if (strncmp(argv[i], "-l", 2) == 0 && argv[i][2]) {
size_t n = strlen(libs);
snprintf(libs + n, libsz - n,
"%s%s", n ? " " : "", argv[i]);
} else if (strcmp(argv[i], "-l") == 0) {
if (i + 1 >= argc) {
fprintf(stderr,
"ww %s: -l needs an argument\n", cmd);
return -1;
}
size_t n = strlen(libs);
snprintf(libs + n, libsz - n,
"%s-l%s", n ? " " : "", argv[++i]);
} else if (strcmp(argv[i], "-L") == 0) {
if (i + 1 >= argc) {
fprintf(stderr,
"ww %s: -L needs an argument\n", cmd);
return -1;
}
size_t n = strlen(libdirs);
snprintf(libdirs + n, libdirsz - n,
"%s-L%s", n ? " " : "", argv[++i]);
} else if (strncmp(argv[i], "-L", 2) == 0 && argv[i][2]) {
size_t n = strlen(libdirs);
snprintf(libdirs + n, libdirsz - n,
"%s%s", n ? " " : "", argv[i]);
} else if (strcmp(argv[i], "-I") == 0) {
if (i + 1 >= argc) {
fprintf(stderr,
"ww %s: -I needs an argument\n", cmd);
return -1;
}
size_t n = strlen(incs);
snprintf(incs + n, incsz - n,
"%s%s", n ? ":" : "", argv[++i]);
} else if (strncmp(argv[i], "-I", 2) == 0 && argv[i][2]) {
size_t n = strlen(incs);
snprintf(incs + n, incsz - n,
"%s%s", n ? ":" : "", argv[i] + 2);
} else if (strcmp(argv[i], "-o") == 0) {
if (i + 1 >= argc) {
fprintf(stderr,
"ww %s: -o needs an argument\n", cmd);
return -1;
}
snprintf(outpath, outsz, "%s", argv[++i]);
} else if (strncmp(argv[i], "-o", 2) == 0 && argv[i][2]) {
snprintf(outpath, outsz, "%s", argv[i] + 2);
} else if (argv[i][0] == '-') {
fprintf(stderr, "ww %s: unknown flag\n", cmd);
return -1;
} else if (*src_out == NULL) {
*src_out = argv[i];
} else {
break; /* leave remaining argv to caller (run-args) */
}
}
return i;
}
static int
do_build(int argc, char **argv)
{
const char *src = NULL;
char libs[2048] = {0};
char libdirs[2048] = {0};
char incs[2048] = {0};
char outflag[1024] = {0};
char workdir[1024] = {0};
int emit_asm = 0;
if (parse_build_flags("build", argc, argv, incs, sizeof incs,
libdirs, sizeof libdirs, libs, sizeof libs,
outflag, sizeof outflag, workdir, sizeof workdir,
&src, &emit_asm) < 0)
return 2;
if (src == NULL) src = "."; /* default: build cwd */
char resolved[1024];
int is_dir = 0;
if (!resolve_module(src, incs, resolved, sizeof resolved, &is_dir)) {
fprintf(stderr, "ww build: cannot find module %s\n", src);
return 1;
}
char out[1024];
const char *objstem = NULL;
if (outflag[0]) {
/* -o sets both the binary path and the intermediate stem so
* artifacts land beside the requested output (T3). */
snprintf(out, sizeof out, "%s", outflag);
objstem = out;
} else if (is_dir) {
char tmp[1024];
snprintf(tmp, sizeof tmp, "%s", resolved);
size_t n = strlen(tmp);
while (n > 1 && tmp[n-1] == '/') tmp[--n] = '\0';
const char *b = strrchr(tmp, '/');
snprintf(out, sizeof out, "%s", b ? b + 1 : tmp);
} else {
basename_no_ext(resolved, out, sizeof out);
}
return build_one_sep(resolved, is_dir, out, objstem, incs, libs,
libdirs, 0, emit_asm, 1, workdir);
}
static int
do_run(int argc, char **argv)
{
const char *src = NULL;
char libs[2048] = {0};
char libdirs[2048] = {0};
char incs[2048] = {0};
char outflag[1024] = {0}; /* -o accepted+ignored: run always uses the temp */
int next = parse_build_flags("run", argc, argv, incs, sizeof incs,
libdirs, sizeof libdirs, libs, sizeof libs,
outflag, sizeof outflag, NULL, 0, &src, NULL);
if (next < 0) return 2;
if (src == NULL) src = ".";
char resolved[1024];
int is_dir = 0;
if (!resolve_module(src, incs, resolved, sizeof resolved, &is_dir)) {
fprintf(stderr, "ww run: cannot find module %s\n", src);
return 1;
}
char tmpdir[1024], tmp[1024];
snprintf(tmpdir, sizeof tmpdir, "/tmp/ww_run_%d", getpid());
if (mkdir(tmpdir, 0700) != 0) {
fprintf(stderr, "ww: cannot create temporary directory %s\n", tmpdir);
return 1;
}
snprintf(tmp, sizeof tmp, "%s/main", tmpdir);
/* 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, tmp, tmp, incs, libs, libdirs, 0, 0,
0, NULL) != 0) {
if (unlink(tmp) != 0 && errno != ENOENT)
fputs("ww: cannot remove temporary output\n", stderr);
if (rmdir(tmpdir) != 0)
fputs("ww: cannot remove temporary directory\n", stderr);
return 1;
}
/* exec the built binary with any trailing argv as its argv. */
pid_t pid = fork();
if (pid < 0) {
perror("ww: fork");
if (unlink(tmp) != 0 && errno != ENOENT)
fputs("ww: cannot remove temporary output\n", stderr);
if (rmdir(tmpdir) != 0)
fputs("ww: cannot remove temporary directory\n", stderr);
return 1;
}
if (pid == 0) {
int n_extra = argc - next;
char **xargv = calloc((size_t)n_extra + 2, sizeof *xargv);
xargv[0] = tmp;
for (int i = 0; i < n_extra; i++) xargv[i+1] = argv[next + i];
xargv[n_extra+1] = NULL;
execv(tmp, xargv);
perror("ww: exec");
_exit(127);
}
int status = 0;
pid_t got;
do { got = waitpid(pid, &status, 0); } while (got < 0 && errno == EINTR);
int rc = got == pid && WIFEXITED(status) ? WEXITSTATUS(status) : 1;
if (got != pid) perror("ww: waitpid");
if (unlink(tmp) != 0 && errno != ENOENT) {
fputs("ww: cannot remove temporary output\n", stderr);
if (rc == 0) rc = 1;
}
if (rmdir(tmpdir) != 0) {
fputs("ww: cannot remove temporary directory\n", stderr);
if (rc == 0) rc = 1;
}
return rc;
}
static int
do_test(int argc, char **argv)
{
const char *src = 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`
* outputs are emitted. Both routes use build_one_sep's is_test bundle
* and T3 objstem redirect.
* -T stays internal to w6c; the driver never sees it. -l/-L carry no
* meaning for a test build, so they (and any unknown flag) are rejected
* rather than silently swallowed — byte-identical wording to the
* wwstage twin (selfhost/cmd/ww/main.ww dotest). */
int compileonly = 0;
int emit_asm = 0;
char outstem[1024] = {0};
char workdir[1024] = {0};
int packageopts = 0;
int afterdash = 0;
/* #17: an optional second positional after the target is a fnmatch
* name-filter pattern, forwarded to the test binary as argv[1]. Only
* meaningful for a single test file/module — rejected in dir mode. */
const char *pattern = NULL;
for (int i = 0; i < argc; i++) {
if (afterdash) continue;
if (argv[i][0] == '-') {
if (strcmp(argv[i], "--") == 0) {
packageopts = 1;
afterdash = 1;
continue;
}
if (argv[i][1] == 'I') {
const char *dir;
if (argv[i][2]) {
dir = argv[i] + 2;
} else {
if (i + 1 >= argc) {
fprintf(stderr,
"ww test: -I needs an argument\n");
return 2;
}
dir = argv[++i];
}
size_t n = strlen(incs);
snprintf(incs + n, sizeof incs - n,
"%s%s", n ? ":" : "", dir);
} else if (strcmp(argv[i], "-c") == 0) {
compileonly = 1;
} else if (strcmp(argv[i], "-S") == 0) {
emit_asm = 1;
} else if (strcmp(argv[i], "-list") == 0) {
packageopts = 1;
} else if (strcmp(argv[i], "-j") == 0 ||
strcmp(argv[i], "-run") == 0 ||
strcmp(argv[i], "-filter") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "ww test: %s needs an argument\n",
argv[i]);
return 2;
}
packageopts = 1;
i++;
} else if (strncmp(argv[i], "-timeout-ms=", 12) == 0 &&
argv[i][12] != '\0') {
packageopts = 1;
} else if (strcmp(argv[i], "-o") == 0) {
if (i + 1 >= argc) {
fprintf(stderr,
"ww test: -o needs an argument\n");
return 2;
}
snprintf(outstem, sizeof outstem, "%s", argv[++i]);
} else if (argv[i][1] == 'o' && argv[i][2]) {
snprintf(outstem, sizeof outstem, "%s", argv[i] + 2);
} else if (strcmp(argv[i], "-w") == 0) {
if (i + 1 >= argc) {
fprintf(stderr,
"ww test: -w needs an argument\n");
return 2;
}
snprintf(workdir, sizeof workdir, "%s", argv[++i]);
} else if (argv[i][1] == 'w' && argv[i][2]) {
snprintf(workdir, sizeof workdir, "%s", argv[i] + 2);
} else {
fprintf(stderr, "ww test: unknown flag\n");
return 2;
}
} else if (src == NULL) {
src = argv[i];
} else if (pattern == NULL) {
pattern = argv[i];
}
}
const char *target = src ? src : ".";
if (emit_asm && !outstem[0]) {
fprintf(stderr, "ww test: -S needs -o\n");
return 2;
}
struct stat st;
if (stat(target, &st) != 0) {
/* not a literal path — try module resolution and run as
* a single test program. */
char resolved[1024];
int is_dir = 0;
if (!resolve_module(target, incs, resolved, sizeof resolved,
&is_dir)) {
fprintf(stderr, "ww test: cannot find %s\n", target);
return 1;
}
if (is_dir) {
if (outstem[0]) {
fprintf(stderr,
"ww test: -c/-S/-o need a single test file\n");
return 2;
}
if (workdir[0]) {
fprintf(stderr,
"ww test: -w needs a single test file\n");
return 2;
}
if (pattern) {
fprintf(stderr,
"ww test: pattern needs a single test file\n");
return 2;
}
return exec_package_tests(argc, argv, src, resolved, 0);
}
if (packageopts) {
fprintf(stderr,
"ww test: package options need a directory\n");
return 2;
}
char tmpdir[1024] = {0}, tmp[1024];
const char *outp;
int owntmp = !outstem[0] && !workdir[0];
if (outstem[0]) outp = outstem;
else if (workdir[0]) {
/* The workdir owns the persistent test binary the same
* way it owns the package artifacts. */
snprintf(tmp, sizeof tmp, "%s/main", workdir);
outp = tmp;
} else {
snprintf(tmpdir, sizeof tmpdir, "/tmp/ww_test_%d", getpid());
if (mkdir(tmpdir, 0700) != 0) {
fprintf(stderr, "ww: cannot create temporary directory %s\n",
tmpdir);
return 1;
}
snprintf(tmp, sizeof tmp, "%s/main", tmpdir);
outp = tmp;
}
/* No-o redirects internal scratch to /tmp rather than beside the
* source. An explicit -o names the caller-owned artifact stem. */
int br = build_one_sep(resolved, is_dir, outp,
outstem[0] ? outstem : tmp, incs, "", "", 1,
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);
if (owntmp && rmdir(tmpdir) != 0)
fputs("ww: cannot remove temporary directory\n", stderr);
return 1;
}
if (compileonly || emit_asm) {
int cleanfail = 0;
if (owntmp && unlink(outp) != 0 && errno != ENOENT) {
fputs("ww: cannot remove temporary output\n", stderr);
cleanfail = 1;
}
if (owntmp && rmdir(tmpdir) != 0) {
fputs("ww: cannot remove temporary directory\n", stderr);
cleanfail = 1;
}
return cleanfail ? 1 : 0;
}
int rc = run_test_bin(outp, pattern);
if (owntmp && unlink(outp) != 0 && errno != ENOENT) {
fputs("ww: cannot remove temporary output\n", stderr);
if (rc == 0) rc = 1;
}
if (owntmp && rmdir(tmpdir) != 0) {
fputs("ww: cannot remove temporary directory\n", stderr);
if (rc == 0) rc = 1;
}
return rc;
}
if (S_ISREG(st.st_mode)) {
if (packageopts) {
fprintf(stderr,
"ww test: package options need a directory\n");
return 2;
}
/* single .ww file — build, then run unless -c (compile-only). */
char tmpdir[1024] = {0}, tmp[1024];
const char *outp;
int owntmp = !outstem[0] && !workdir[0];
if (outstem[0]) outp = outstem;
else if (workdir[0]) {
snprintf(tmp, sizeof tmp, "%s/main", workdir);
outp = tmp;
} else {
snprintf(tmpdir, sizeof tmpdir, "/tmp/ww_test_%d", getpid());
if (mkdir(tmpdir, 0700) != 0) {
fprintf(stderr, "ww: cannot create temporary directory %s\n",
tmpdir);
return 1;
}
snprintf(tmp, sizeof tmp, "%s/main", tmpdir);
outp = tmp;
}
/* See module-mode note: no-o scratch is redirected to /tmp. */
int br = build_one_sep(target, 0, outp, outstem[0] ? outstem : tmp,
incs, "", "", 1, 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);
if (owntmp && rmdir(tmpdir) != 0)
fputs("ww: cannot remove temporary directory\n", stderr);
return 1;
}
if (compileonly || emit_asm) {
int cleanfail = 0;
if (owntmp && unlink(outp) != 0 && errno != ENOENT) {
fputs("ww: cannot remove temporary output\n", stderr);
cleanfail = 1;
}
if (owntmp && rmdir(tmpdir) != 0) {
fputs("ww: cannot remove temporary directory\n", stderr);
cleanfail = 1;
}
return cleanfail ? 1 : 0;
}
int rc = run_test_bin(outp, pattern);
if (owntmp && unlink(outp) != 0 && errno != ENOENT) {
fputs("ww: cannot remove temporary output\n", stderr);
if (rc == 0) rc = 1;
}
if (owntmp && rmdir(tmpdir) != 0) {
fputs("ww: cannot remove temporary directory\n", stderr);
if (rc == 0) rc = 1;
}
return rc;
}
if (!S_ISDIR(st.st_mode)) {
fprintf(stderr, "ww test: %s is neither file nor directory\n", target);
return 1;
}
if (outstem[0]) {
fprintf(stderr, "ww test: -c/-S/-o need a single test file\n");
return 2;
}
if (workdir[0]) {
fprintf(stderr, "ww test: -w needs a single test file\n");
return 2;
}
/* The second bare positional remains the legacy single-file filter form;
* package filtering uses explicit -run/-filter options. */
if (pattern) {
fprintf(stderr, "ww test: pattern needs a single test file\n");
return 2;
}
return exec_package_tests(argc, argv, src, NULL, src == NULL);
}
static int
do_fmt(int argc, char **argv)
{
(void)argc; (void)argv;
fputs("ww: fmt: not implemented in this phase\n", stderr);
return 1;
}
int
main(int argc, char **argv)
{
if (argc >= 1) {
char buf[1024];
snprintf(buf, sizeof buf, "%s", argv[0]);
self_dir = strdup(dirname(buf));
}
if (argc < 2) { fputs(usage, stderr); return 2; }
const char *cmd = argv[1];
if (strcmp(cmd, "-V") == 0 || strcmp(cmd, "version") == 0)
return do_version();
if (strcmp(cmd, "-h") == 0 || strcmp(cmd, "--help") == 0) {
fputs(usage, stdout); return 0;
}
if (strcmp(cmd, "build") == 0) return do_build(argc - 2, argv + 2);
if (strcmp(cmd, "run") == 0) return do_run(argc - 2, argv + 2);
if (strcmp(cmd, "test") == 0) return do_test(argc - 2, argv + 2);
if (strcmp(cmd, "fmt") == 0) return do_fmt(argc - 2, argv + 2);
fprintf(stderr, "ww: unknown subcommand: %s\n", cmd);
fputs(usage, stderr);
return 2;
}