cmd: build local package graphs

This commit is contained in:
2026-08-11 22:26:07 +09:00
parent db96422f74
commit 52f6d6f0d4
6 changed files with 1316 additions and 510 deletions

View File

@@ -256,11 +256,16 @@ $(BIN)/ww_ww: selfhost/cmd/ww/main.ww lib/os/exec/exec.ww \
lib/os/os.ww lib/rt/malloc.ww \ lib/os/os.ww lib/rt/malloc.ww \
lib/time/time.ww lib/types/types.ww \ lib/time/time.ww lib/types/types.ww \
lib/strings/strings.ww lib/bytes/bytes.ww lib/encoding/utf8/utf8.ww \ lib/strings/strings.ww lib/bytes/bytes.ww lib/encoding/utf8/utf8.ww \
lib/ww/syntax/lex.ww lib/ww/syntax/tok.ww lib/ww/syntax/ast.ww \
lib/ww/syntax/parse.ww lib/ww/syntax/expr.ww \
lib/ww/syntax/stmt.ww lib/ww/syntax/decl.ww \
lib/ww/syntax/typ.ww lib/ww/syntax/sym.ww \
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(LIB)/libwwrt.a | $(BIN) $(LIB)/libwwrt.a | $(BIN)
@mkdir -p $(WWBUILD)/ww_ww @mkdir -p $(WWBUILD)/ww_ww
@$(CURDIR)/$(BIN)/ww build -w $(WWBUILD)/ww_ww \ @$(CURDIR)/$(BIN)/ww build -w $(WWBUILD)/ww_ww \
-o $(WWBUILD)/ww_ww/main \ -o $(WWBUILD)/ww_ww/main \
-I $(CURDIR)/lib/ww \
$(CURDIR)/selfhost/cmd/ww/main.ww $(CURDIR)/selfhost/cmd/ww/main.ww
@mv $(WWBUILD)/ww_ww/main $@ @mv $(WWBUILD)/ww_ww/main $@
@@ -366,7 +371,8 @@ BYTEID_WW_TARGETS = $(BYTEID_WW_TESTS:%=wwtest/%)
# test-compiler beside the surviving residual carriers. # test-compiler beside the surviving residual carriers.
SEP_WW_TESTS = test/sep/sepbuild_test.ww test/sep/sepimport_test.ww \ SEP_WW_TESTS = test/sep/sepbuild_test.ww test/sep/sepimport_test.ww \
test/sep/seplink_test.ww test/sep/sepscratch_test.ww \ test/sep/seplink_test.ww test/sep/sepscratch_test.ww \
test/sep/septest_test.ww test/sep/m3sep_test.ww test/sep/septest_test.ww test/sep/m3sep_test.ww \
test/sep/localbuild_test.ww
SEP_WW_TARGETS = $(SEP_WW_TESTS:%=wwtest/%) SEP_WW_TARGETS = $(SEP_WW_TESTS:%=wwtest/%)
# Ww-native lib env/OS arranger observers: the env contracts of the # Ww-native lib env/OS arranger observers: the env contracts of the
# lib/os and lib/dirs suites (getenv cohorts, XDG cohorts, the dirs # lib/os and lib/dirs suites (getenv cohorts, XDG cohorts, the dirs

View File

@@ -4,6 +4,7 @@
* Tool paths default to siblings of $0 (so a fresh build runs out of * 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. * out/bin/), and can be overridden with WW_W6C / WW_W6A / WW_W6L.
*/ */
#define _XOPEN_SOURCE 700
#include "ww.h" #include "ww.h"
#include <errno.h> #include <errno.h>
#include <string.h> #include <string.h>
@@ -17,7 +18,7 @@
static const char *usage = static const char *usage =
"usage: ww [-V] <subcommand> [args...]\n" "usage: ww [-V] <subcommand> [args...]\n"
" -V print version and exit\n" " -V print version and exit\n"
" build [-S] [-w DIR] [-o FILE] [path] compile module; -S stops after package asm\n" " build [-p] [-S] [-w DIR] [-I DIR] [-o FILE] [path] build a local package graph\n"
" run [path] ... build then exec, passing extra args to the program\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" " test [-S -o STEM] [-w DIR] [options] [path] build/run tests; -S emits package asm\n"
" version print version and exit\n" " version print version and exit\n"
@@ -26,6 +27,7 @@ static const char *usage =
" foo.ww literal file\n" " foo.ww literal file\n"
" foo search cwd, -I dirs, then the source library for foo.ww or foo/\n" " foo search cwd, -I dirs, then the source library for foo.ww or foo/\n"
" lib/foo directory: build its package sources\n" " lib/foo directory: build its package sources\n"
" -p emits a non-main archive FILE + FILE.wwi\n"
" lib/... every package under lib, recursively (test only)\n" " lib/... every package under lib, recursively (test only)\n"
" . build the cwd's <basename>.ww\n"; " . build the cwd's <basename>.ww\n";
@@ -279,6 +281,16 @@ enumerate_dir_ww(const char *dirpath, char ***out_files)
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) if (nl >= 8 && strcmp(nm + nl - 8, "_test.ww") == 0)
continue; continue;
struct stat st;
if (lstat(path, &st) != 0 || !S_ISREG(st.st_mode)) {
fprintf(stderr,
"ww: %s: package source is not a regular file\n", path);
for (int i = 0; i < n; i++) free(arr[i]);
free(arr);
closedir(d);
*out_files = NULL;
return -2;
}
if (file_has_line_test(path)) { if (file_has_line_test(path)) {
fprintf(stderr, fprintf(stderr,
"ww: %s: @test declaration outside *_test.ww\n", "ww: %s: @test declaration outside *_test.ww\n",
@@ -324,15 +336,16 @@ enumerate_dir_ww(const char *dirpath, char ***out_files)
* interface can name a transitive dep's type (os exposes time.instant), * interface can name a transitive dep's type (os exposes time.instant),
* so the consuming unit needs the whole closure for name RESOLUTION — * so the consuming unit needs the whole closure for name RESOLUTION —
* direct-deps-only does not type-check. This mirrors harec reading the * direct-deps-only does not type-check. This mirrors harec reading the
* transitive `.td` closure and is consistent with the current flat-unit * transitive `.td` closure. The checker still scopes qualifier lookup to
* transitive-namespace model (the visibility tighten is task #45, * each source package's direct N_USE declarations, so carrying those type
* deferred post-M4). * facts does not make a transitive package source-visible.
*/ */
#define SEP_MAXPKG 256 #define SEP_MAXPKG 256
struct seppkg { struct seppkg {
char path[256]; /* dotted import path; "" == root/primary */ char path[256]; /* dotted import path; "" == root/primary */
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 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 **sources; /* owned, byte-sorted production paths; dirs only */
int nsources; int nsources;
@@ -351,16 +364,54 @@ static int
sep_find_or_add(struct sepgraph *g, const char *path, const char *entry, sep_find_or_add(struct sepgraph *g, const char *path, const char *entry,
int is_dir) int is_dir)
{ {
for (int i = 0; i < g->n; i++) if (strlen(path) >= sizeof g->pkg[0].path) {
if (strcmp(g->pkg[i].path, path) == 0) return i; fprintf(stderr, "ww: package path is too long (limit %zu bytes)\n",
sizeof g->pkg[0].path - 1);
return -1;
}
char *canon = realpath(entry, NULL);
if (canon == NULL) {
fprintf(stderr, "ww: cannot canonicalize package %s\n", entry);
return -1;
}
if (strlen(canon) >= sizeof g->pkg[0].canon) {
fprintf(stderr, "ww: canonical package path is too long\n");
free(canon);
return -1;
}
for (int i = 0; i < g->n; i++) {
if (strcmp(g->pkg[i].path, path) == 0) {
if (strcmp(g->pkg[i].canon, canon) != 0) {
fprintf(stderr,
"ww: package %s resolves to both %s and %s\n",
path[0] ? path : "(root)", g->pkg[i].entry,
entry);
free(canon);
return -1;
}
free(canon);
return i;
}
if (strcmp(g->pkg[i].canon, canon) == 0) {
fprintf(stderr,
"ww: package directory %s has identities %s and %s\n",
entry, g->pkg[i].path[0] ? g->pkg[i].path : "(root)",
path[0] ? path : "(root)");
free(canon);
return -1;
}
}
if (g->n >= SEP_MAXPKG) { if (g->n >= SEP_MAXPKG) {
fprintf(stderr, "ww: too many packages (limit %d)\n", fprintf(stderr, "ww: too many packages (limit %d)\n",
SEP_MAXPKG); SEP_MAXPKG);
free(canon);
return -1; return -1;
} }
struct seppkg *p = &g->pkg[g->n]; struct seppkg *p = &g->pkg[g->n];
snprintf(p->path, sizeof p->path, "%s", path); snprintf(p->path, sizeof p->path, "%s", path);
snprintf(p->entry, sizeof p->entry, "%s", entry); snprintf(p->entry, sizeof p->entry, "%s", entry);
snprintf(p->canon, sizeof p->canon, "%s", canon);
free(canon);
p->is_dir = is_dir; p->is_dir = is_dir;
p->name[0] = '\0'; p->name[0] = '\0';
p->sources = NULL; p->sources = NULL;
@@ -393,106 +444,45 @@ sep_fname(const struct sepgraph *g, int pi, const char *scratch,
snprintf(out, outsz, "%s/%s%s", scratch, base, suffix); 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 static int
unit_has_package(const char *path, const char *leaf) sep_slurp(const char *path, char **out, u64 *len)
{ {
FILE *in = fopen(path, "rb"); FILE *f = fopen(path, "rb");
if (in == NULL) return 0; if (f == NULL) return -1;
char line[2048]; if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return -1; }
int found = 0; long n = ftell(f);
while (fgets(line, sizeof line, in)) { if (n < 0 || fseek(f, 0, SEEK_SET) != 0) {
const char *p = line; fclose(f);
while (*p == ' ' || *p == '\t') p++; return -1;
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); char *buf = malloc((size_t)n + 1);
return found; if (buf == NULL) { fclose(f); return -1; }
} if (fread(buf, 1, (size_t)n, f) != (size_t)n) {
free(buf);
static int fclose(f);
sep_ident_start(int c) return -1;
{
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');
}
/* Deliberately only the loader's small header grammar for the leading
* package clause, 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; buf[n] = '\0';
if (fclose(f) != 0) {
free(buf);
return -1;
}
*out = buf;
*len = (u64)n;
return 0; return 0;
} }
static int static int
sep_package_clause(const char *src, size_t n, char *name, size_t namesz) use_node_cmp(const void *a, const void *b)
{ {
static const char kw[] = "package"; const Node *x = *(Node *const *)a;
size_t i = 0; const Node *y = *(Node *const *)b;
if (sep_skip_space(src, n, &i) < 0 const char *xp = x->usepath ? x->usepath : x->str;
|| i + sizeof kw - 1 >= n const char *yp = y->usepath ? y->usepath : y->str;
|| memcmp(src + i, kw, sizeof kw - 1) != 0) int r = strcmp(xp, yp);
return -1; if (r != 0) return r;
i += sizeof kw - 1; if (x->pos.line != y->pos.line) return x->pos.line - y->pos.line;
if (i >= n || (src[i] != ' ' && src[i] != '\t' return x->pos.col - y->pos.col;
&& 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;
} }
/* 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
@@ -506,33 +496,37 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file,
{ {
if (import_seen(filevisit, file)) return 0; if (import_seen(filevisit, file)) return 0;
import_add(filevisit, file); import_add(filevisit, file);
FILE *in = fopen(file, "rb"); char *buf;
if (in == NULL) { u64 len;
if (sep_slurp(file, &buf, &len) < 0) {
fprintf(stderr, "ww: cannot read %s\n", file); fprintf(stderr, "ww: cannot read %s\n", file);
return -1; return -1;
} }
if (fseek(in, 0, SEEK_END) != 0) { fclose(in); return -1; } Arena *a = newarena();
long flen = ftell(in); Lex l;
if (flen < 0 || fseek(in, 0, SEEK_SET) != 0) { Parser p;
fclose(in); lexinit(&l, a, file, buf, len);
return -1; parserinit(&p, a, &l);
} Node *imports = parseimports(&p);
char *buf = malloc((size_t)flen + 1); if (l.errs || p.errs) {
if (buf == NULL) { fclose(in); return -1; } freearena(a);
if (fread(buf, 1, (size_t)flen, in) != (size_t)flen) { free(buf);
return -1;
}
if (imports->module == NULL && owned_source) {
Pos pp = { file, 1, 1 };
errorf(pp, "invalid or missing package clause");
freearena(a);
free(buf); free(buf);
fclose(in);
return -1; return -1;
} }
buf[flen] = '\0';
fclose(in);
if (owned_source) { if (imports->module != NULL
char declared[256]; && (owned_source || g->pkg[pi].name[0] == '\0')) {
if (sep_package_clause(buf, (size_t)flen, declared, const char *declared = imports->module;
sizeof declared) < 0) { if (strlen(declared) >= sizeof g->pkg[pi].name) {
fprintf(stderr, "ww: %s: invalid or missing package clause\n", errorf(imports->pos, "package name is too long");
file); freearena(a);
free(buf); free(buf);
return -1; return -1;
} }
@@ -540,60 +534,97 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file,
if (pkg->name[0] == '\0') if (pkg->name[0] == '\0')
snprintf(pkg->name, sizeof pkg->name, "%s", declared); snprintf(pkg->name, sizeof pkg->name, "%s", declared);
else if (strcmp(pkg->name, declared) != 0) { else if (strcmp(pkg->name, declared) != 0) {
fprintf(stderr, errorf(imports->pos,
"ww: %s: conflicting package names %s and %s\n", "conflicting package names %s and %s in %s",
pkg->entry, pkg->name, declared); pkg->name, declared, pkg->entry);
freearena(a);
free(buf); free(buf);
return -1; return -1;
} }
} }
if (owned_source && g->pkg[pi].is_dir) {
for (Node *package = imports->body; package; package = package->next) {
if (strcmp(package->module, g->pkg[pi].name) != 0) {
errorf(package->pos,
"conflicting package names %s and %s in %s",
g->pkg[pi].name, package->module, g->pkg[pi].entry);
freearena(a);
free(buf);
return -1;
}
}
}
int nuse = 0;
for (Node *u = imports->list; u; u = u->next)
if (u->kind == N_USE) nuse++;
Node **uses = nuse ? malloc((size_t)nuse * sizeof *uses) : NULL;
if (nuse && uses == NULL) {
freearena(a);
free(buf);
return -1;
}
int ui = 0;
for (Node *u = imports->list; u; u = u->next)
if (u->kind == N_USE) uses[ui++] = u;
if (nuse > 1) qsort(uses, (size_t)nuse, sizeof *uses, use_node_cmp);
int rc = 0; int rc = 0;
for (size_t off = 0; off < (size_t)flen && rc == 0;) { const char *previous = NULL;
size_t end = off; for (int i = 0; i < nuse && rc == 0; i++) {
while (end < (size_t)flen && buf[end] != '\n') end++; Node *u = uses[i];
const char *p = buf + off; const char *name = u->usepath ? u->usepath : u->str;
const char *lineend = buf + end; if (previous && strcmp(previous, name) == 0) continue;
while (p < lineend && (*p == ' ' || *p == '\t')) p++; previous = name;
if ((size_t)(lineend - p) < 7 if (strlen(name) >= sizeof g->pkg[0].path) {
|| (memcmp(p, "import ", 7) != 0 errorf(u->pos, "import path is too long (limit %zu bytes)",
&& memcmp(p, "import\t", 7) != 0)) { sizeof g->pkg[0].path - 1);
off = end < (size_t)flen ? end + 1 : end; rc = -1;
continue; break;
} }
p += 7; char path_form[1024];
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); import_path_form(name, path_form, sizeof path_form);
if (strlen(name) + 1 > sizeof path_form) {
errorf(u->pos, "import path is too long");
rc = -1;
break;
}
char ipath[1024]; char ipath[1024];
int is_dir = 0; 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, if (!locate_import(searchpath, path_form, ipath, sizeof ipath,
&is_dir)) { &is_dir)) {
const char *dot = strrchr(name, '.'); const char *dot = strrchr(name, '.');
const char *leaf = dot ? dot + 1 : name; const char *leaf = dot ? dot + 1 : name;
if (unit_has_package(file, leaf)) int inline_package = 0;
goto next_line; /* inline-satisfied */ for (Node *package = imports->body; package;
fprintf(stderr, "ww: cannot find package %s\n", name); package = package->next)
if (strcmp(package->module, leaf) == 0) {
inline_package = 1;
break;
}
if (inline_package)
continue;
errorf(u->pos, "cannot find package %s", name);
rc = -1; rc = -1;
goto next_line; break;
} }
if (is_dir) { if (is_dir) {
char *canon = realpath(ipath, NULL);
if (canon == NULL) {
errorf(u->pos, "cannot canonicalize package '%s'", name);
rc = -1;
break;
}
int self = strcmp(canon, g->pkg[pi].canon) == 0;
free(canon);
if (self) {
const char *owner = g->pkg[pi].path[0]
? g->pkg[pi].path : g->pkg[pi].name;
errorf(u->pos, "self-import: package '%s' cannot import itself",
owner[0] ? owner : "(root)");
rc = -1;
break;
}
int di = sep_find_or_add(g, name, ipath, 1); int di = sep_find_or_add(g, name, ipath, 1);
if (di < 0) { rc = -1; break; } if (di < 0) { rc = -1; break; }
int seen = 0; int seen = 0;
@@ -606,9 +637,9 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file,
} else if (sep_scan_file(g, pi, ipath, searchpath, filevisit, 0) < 0) { } else if (sep_scan_file(g, pi, ipath, searchpath, filevisit, 0) < 0) {
rc = -1; break; rc = -1; break;
} }
next_line:
off = end < (size_t)flen ? end + 1 : end;
} }
free(uses);
freearena(a);
free(buf); free(buf);
return rc; return rc;
} }
@@ -659,6 +690,25 @@ sep_load_pkg(struct sepgraph *g, int pi, const char *searchpath)
for (int i = 0; i < fv.n; i++) free(fv.paths[i]); for (int i = 0; i < fv.n; i++) free(fv.paths[i]);
free(fv.paths); free(fv.paths);
if (rc < 0) return rc; if (rc < 0) return rc;
/* Give a non-main root its declared identity before recursively loading
* dependencies. A back-edge can then reuse node 0 and reach the normal
* cycle detector instead of looking like a location alias. Executable
* roots are reset to the bare-root identity after loading. */
if (pi == 0 && g->pkg[pi].path[0] == '\0'
&& g->pkg[pi].name[0] != '\0') {
size_t n = strlen(g->pkg[pi].name);
memcpy(g->pkg[pi].path, g->pkg[pi].name, n + 1);
}
for (int i = 1; i < g->pkg[pi].ndeps; i++) {
int v = g->pkg[pi].deps[i];
int j = i;
while (j > 0 && strcmp(g->pkg[g->pkg[pi].deps[j - 1]].path,
g->pkg[v].path) > 0) {
g->pkg[pi].deps[j] = g->pkg[pi].deps[j - 1];
j--;
}
g->pkg[pi].deps[j] = v;
}
/* recurse into freshly-added deps (sep_find_or_add may have grown /* recurse into freshly-added deps (sep_find_or_add may have grown
* g->n during the scan; iterate by index). */ * g->n during the scan; iterate by index). */
for (int k = 0; k < g->pkg[pi].ndeps; k++) for (int k = 0; k < g->pkg[pi].ndeps; k++)
@@ -712,53 +762,81 @@ sep_mark_deps(struct sepgraph *g, int pi, char *inset)
* //ww:module-reset primary boundary (so -c emits its decls, imported * //ww:module-reset primary boundary (so -c emits its decls, imported
* ==0). DIRECTORY imports are skipped (provided as `.wwi` ahead of the * ==0). DIRECTORY imports are skipped (provided as `.wwi` ahead of the
* body); FILE imports fold in (intra-package split). */ * body); FILE imports fold in (intra-package split). */
static void 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)
{ {
if (import_seen(visited, path)) return; if (import_seen(visited, path)) return 0;
import_add(visited, path); import_add(visited, path);
FILE *in = fopen(path, "rb"); char *buf;
if (in == NULL) { u64 len;
if (sep_slurp(path, &buf, &len) < 0) {
fprintf(stderr, "ww: cannot read %s\n", path); fprintf(stderr, "ww: cannot read %s\n", path);
return; return -1;
} }
char line[2048]; Arena *a = newarena();
while (fgets(line, sizeof line, in)) { Lex l;
const char *p = line; Parser p;
while (*p == ' ' || *p == '\t') p++; lexinit(&l, a, path, buf, len);
if (strncmp(p, "import ", 7) != 0 && strncmp(p, "import\t", 7) != 0) parserinit(&p, a, &l);
continue; Node *imports = parseimports(&p);
p += 7; if (l.errs || p.errs) {
while (*p == ' ' || *p == '\t') p++; freearena(a);
char name[256] = {0}; free(buf);
int j = 0; return -1;
while ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') }
|| *p == '_' || *p == '.' || (*p >= '0' && *p <= '9')) int nuse = 0;
if (j + 1 < (int)sizeof name) name[j++] = *p++; for (Node *u = imports->list; u; u = u->next)
if (j == 0) continue; if (u->kind == N_USE) nuse++;
char path_form[256]; Node **uses = nuse ? malloc((size_t)nuse * sizeof *uses) : NULL;
if (nuse && uses == NULL) {
freearena(a);
free(buf);
return -1;
}
int ui = 0;
for (Node *u = imports->list; u; u = u->next)
if (u->kind == N_USE) uses[ui++] = u;
if (nuse > 1) qsort(uses, (size_t)nuse, sizeof *uses, use_node_cmp);
for (int i = 0; i < nuse; i++) {
Node *u = uses[i];
const char *name = u->usepath ? u->usepath : u->str;
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];
int is_dir = 0; int is_dir = 0;
if (!locate_import(searchpath, path_form, ipath, sizeof ipath, if (!locate_import(searchpath, path_form, ipath, sizeof ipath,
&is_dir)) &is_dir))
continue; continue;
if (!is_dir) if (!is_dir && sep_emit_body(out, ipath, visited, searchpath,
sep_emit_body(out, ipath, visited, searchpath, modpath); modpath) < 0) {
free(uses);
freearena(a);
free(buf);
return -1;
}
} }
/* #57: tag the primary body by its full dotted import path so the /* #57: tag the primary body by its full dotted import path so the
* definer mangles == the importer reference; a root build (path "") * definer mangles == the importer reference; a root build (path "")
* stays a bare reset (keeps bare main). */ * stays a bare reset (keeps bare main). */
if (modpath != NULL && modpath[0] != '\0') int bad = 0;
fprintf(out, "//ww:module-reset %s\n", modpath); if (modpath != NULL && modpath[0] != '\0') {
else if (fprintf(out, "//ww:module-reset %s\n", modpath) < 0)
fputs("//ww:module-reset\n", out); bad = 1;
rewind(in); } else if (fputs("//ww:module-reset\n", out) == EOF) {
int ch; bad = 1;
while ((ch = fgetc(in)) != EOF) fputc(ch, out); }
fputc('\n', out); if (fwrite(buf, 1, (size_t)len, out) != (size_t)len
fclose(in); || fputc('\n', out) == EOF)
bad = 1;
free(uses);
freearena(a);
free(buf);
if (bad) {
fprintf(stderr, "ww: cannot write package unit\n");
return -1;
}
return 0;
} }
/* Compose pi's sep-unit at `unitf`: the transitive-closure `.wwi`s /* Compose pi's sep-unit at `unitf`: the transitive-closure `.wwi`s
@@ -787,25 +865,35 @@ sep_compose_unit(struct sepgraph *g, int pi, const char *scratch,
fclose(u); fclose(u);
return -1; return -1;
} }
fprintf(u, "//ww:module %s\n", g->pkg[dj].path); int bad = fprintf(u, "//ww:module %s\n", g->pkg[dj].path) < 0;
int ch; int ch;
while ((ch = fgetc(wf)) != EOF) fputc(ch, u); while (!bad && (ch = fgetc(wf)) != EOF)
fputc('\n', u); if (fputc(ch, u) == EOF) bad = 1;
fclose(wf); if (ferror(wf) || fputc('\n', u) == EOF) bad = 1;
if (fclose(wf) != 0) bad = 1;
if (bad) {
fprintf(stderr, "ww: cannot compose package unit %s\n", unitf);
fclose(u);
return -1;
}
} }
struct ImportSet bodyvisit = {0}; struct ImportSet bodyvisit = {0};
int bodyrc = 0;
if (g->pkg[pi].is_dir) { if (g->pkg[pi].is_dir) {
for (int i = 0; i < g->pkg[pi].nsources; i++) for (int i = 0; i < g->pkg[pi].nsources && bodyrc == 0; i++)
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);
} else { } else {
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);
} }
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);
fclose(u); if (fclose(u) != 0) {
return 0; fprintf(stderr, "ww: cannot close package unit %s\n", unitf);
return -1;
}
return bodyrc;
} }
/* archive_o — write a deterministic single-member SysV ar archive at /* archive_o — write a deterministic single-member SysV ar archive at
@@ -824,16 +912,18 @@ archive_o(const char *objpath, const char *apath)
fprintf(stderr, "ww: cannot read %s\n", objpath); fprintf(stderr, "ww: cannot read %s\n", objpath);
return -1; return -1;
} }
fseek(in, 0, SEEK_END); if (fseek(in, 0, SEEK_END) != 0) { fclose(in); return -1; }
long n = ftell(in); long n = ftell(in);
fseek(in, 0, SEEK_SET); if (n < 0 || fseek(in, 0, SEEK_SET) != 0) {
if (n < 0) { fclose(in); return -1; } fclose(in);
return -1;
}
unsigned char *buf = malloc((size_t)n); unsigned char *buf = malloc((size_t)n);
if (buf == NULL) { fclose(in); return -1; } if (buf == NULL) { fclose(in); return -1; }
if (fread(buf, 1, (size_t)n, in) != (size_t)n) { if (fread(buf, 1, (size_t)n, in) != (size_t)n) {
free(buf); fclose(in); return -1; free(buf); fclose(in); return -1;
} }
fclose(in); if (fclose(in) != 0) { free(buf); return -1; }
FILE *out = fopen(apath, "wb"); FILE *out = fopen(apath, "wb");
if (out == NULL) { if (out == NULL) {
@@ -841,7 +931,7 @@ archive_o(const char *objpath, const char *apath)
free(buf); free(buf);
return -1; return -1;
} }
fwrite("!<arch>\n", 1, 8, out); int bad = fwrite("!<arch>\n", 1, 8, out) != 8;
/* ar(5) fixes each member header at 60 bytes; the offsets below /* ar(5) fixes each member header at 60 bytes; the offsets below
* address fields in that serialized header. */ * address fields in that serialized header. */
char hdr[60]; char hdr[60];
@@ -853,14 +943,20 @@ archive_o(const char *objpath, const char *apath)
memcpy(hdr + 40, "100644", 6); /* mode (fixed octal) */ memcpy(hdr + 40, "100644", 6); /* mode (fixed octal) */
char sz[12]; char sz[12];
int szn = snprintf(sz, sizeof sz, "%lu", (unsigned long)n); int szn = snprintf(sz, sizeof sz, "%lu", (unsigned long)n);
memcpy(hdr + 48, sz, (size_t)szn); if (szn <= 0 || szn > 10) bad = 1;
else memcpy(hdr + 48, sz, (size_t)szn);
hdr[58] = 0x60; /* member-header magic byte */ hdr[58] = 0x60; /* member-header magic byte */
hdr[59] = 0x0a; hdr[59] = 0x0a;
fwrite(hdr, 1, sizeof hdr, out); if (fwrite(hdr, 1, sizeof hdr, out) != sizeof hdr
fwrite(buf, 1, (size_t)n, out); || fwrite(buf, 1, (size_t)n, out) != (size_t)n)
if (n & 1) fputc('\n', out); /* members are 2-byte aligned */ bad = 1;
fclose(out); if ((n & 1) && fputc('\n', out) == EOF) bad = 1;
if (fclose(out) != 0) bad = 1;
free(buf); free(buf);
if (bad) {
fprintf(stderr, "ww: cannot write archive %s\n", apath);
return -1;
}
return 0; return 0;
} }
@@ -958,10 +1054,12 @@ workdir_stamp_text(char *buf, size_t bufsz, int is_test, int emit_asm)
* files land in a cold `<stem>.sepwork` dir, or under the persistent * files land in a cold `<stem>.sepwork` dir, or under the persistent
* `-w` workdir with content-identity package reuse. */ * `-w` workdir with content-identity package reuse. */
static int static int
build_one_sep_impl(const char *src, int entry_is_dir, const char *out, build_one_sep_impl(const char *src, int entry_is_dir,
const char *root_identity, const char *out,
const char *objstem, const char *extra_includes, const char *extra_libs, const char *objstem, const char *extra_includes, const char *extra_libs,
const char *extra_libdirs, int is_test, int emit_asm, const char *workdir, const char *extra_libdirs, int package_only, int is_test, int emit_asm,
char *scratchout, size_t scratchoutsz, struct sepgraph **graphout) const char *workdir, char *scratchout, size_t scratchoutsz,
struct sepgraph **graphout)
{ {
const char *c6 = toolpath("WW_W6C", "w6c"); const char *c6 = toolpath("WW_W6C", "w6c");
const char *a6 = toolpath("WW_W6A", "w6a"); const char *a6 = toolpath("WW_W6A", "w6a");
@@ -1061,7 +1159,9 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out,
struct sepgraph *g = calloc(1, sizeof *g); struct sepgraph *g = calloc(1, sizeof *g);
if (g == NULL) return 1; if (g == NULL) return 1;
if (graphout) *graphout = g; if (graphout) *graphout = g;
int root = sep_find_or_add(g, "", src, entry_is_dir); const char *rootpath = package_only && root_identity
? root_identity : "";
int root = sep_find_or_add(g, rootpath, src, entry_is_dir);
if (root < 0) return 1; if (root < 0) return 1;
/* #79 (-T): lib/test is the synth main's `test.run` callee but @test /* #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 * files never `import test;`. Inject it as a direct dep of the root so
@@ -1082,6 +1182,11 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out,
} }
} }
if (sep_load_pkg(g, root, srcdir) < 0) return 1; if (sep_load_pkg(g, root, srcdir) < 0) return 1;
int root_package = package_only;
if (root_package && strcmp(g->pkg[root].name, "main") == 0) {
fprintf(stderr, "ww: -p requires a non-main package\n");
return 1;
}
for (int i = 0; i < g->n; i++) g->pkg[i].color = 0; for (int i = 0; i < g->n; i++) g->pkg[i].color = 0;
int *order = calloc((size_t)g->n, sizeof *order); int *order = calloc((size_t)g->n, sizeof *order);
int *stack = calloc((size_t)g->n, sizeof *stack); int *stack = calloc((size_t)g->n, sizeof *stack);
@@ -1091,6 +1196,7 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out,
free(stack); free(order); return 1; free(stack); free(order); return 1;
} }
free(stack); free(stack);
if (!root_package) g->pkg[root].path[0] = '\0';
for (int oi = 0; oi < norder; oi++) { for (int oi = 0; oi < norder; oi++) {
int pi = order[oi]; int pi = order[oi];
@@ -1114,13 +1220,15 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out,
const char *cs = warm ? asmnew : asmf; const char *cs = warm ? asmnew : asmf;
const char *co = warm ? objnew : obj; const char *co = warm ? objnew : obj;
const char *ca = warm ? anew : apath; const char *ca = warm ? anew : apath;
int needs_export = pi != root || root_package;
int needs_archive = pi != root || root_package;
if (sep_compose_unit(g, pi, scratch, order, norder, srcdir, if (sep_compose_unit(g, pi, scratch, order, norder, srcdir,
cu) < 0) { free(order); return 1; } cu) < 0) { free(order); return 1; }
if (warm && !stale_all && file_equal(unitnew, unitf) if (warm && !stale_all && file_equal(unitnew, unitf)
&& file_is_reg(asmf) && file_is_reg(asmf)
&& (pi == root || file_is_reg(wwi)) && (!needs_export || file_is_reg(wwi))
&& (emit_asm || (file_size_nonzero(obj) && (emit_asm || (file_size_nonzero(obj)
&& (pi == root || file_size_nonzero(apath))))) { && (!needs_archive || file_size_nonzero(apath))))) {
if (unlink(unitnew) != 0) { if (unlink(unitnew) != 0) {
fprintf(stderr, "ww: cannot remove %s\n", fprintf(stderr, "ww: cannot remove %s\n",
unitnew); unitnew);
@@ -1135,7 +1243,7 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out,
* 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 (pi == root) 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. */
@@ -1162,7 +1270,7 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out,
* the build target, always fully linked), so `main` is defined * the build target, always fully linked), so `main` is defined
* before any archive is processed. The link consumes `.o`/`.a`, * before any archive is processed. The link consumes `.o`/`.a`,
* never `.wwi`. */ * never `.wwi`. */
if (!emit_asm && pi != root) { if (!emit_asm && needs_archive) {
if (archive_o(co, ca) != 0) { if (archive_o(co, ca) != 0) {
fprintf(stderr, "ww: archive failed for %s\n", fprintf(stderr, "ww: archive failed for %s\n",
g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)"); g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)");
@@ -1172,10 +1280,10 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out,
/* Commit order: artifacts before the unit that vouches for /* Commit order: artifacts before the unit that vouches for
* them, unit strictly last. */ * them, unit strictly last. */
if (warm) { if (warm) {
if ((pi != root && rename(wwinew, wwi) != 0) if ((needs_export && rename(wwinew, wwi) != 0)
|| rename(asmnew, asmf) != 0 || rename(asmnew, asmf) != 0
|| (!emit_asm && rename(objnew, obj) != 0) || (!emit_asm && rename(objnew, obj) != 0)
|| (!emit_asm && pi != root || (!emit_asm && needs_archive
&& rename(anew, apath) != 0) && rename(anew, apath) != 0)
|| rename(unitnew, unitf) != 0) { || rename(unitnew, unitf) != 0) {
fprintf(stderr, "ww: cannot commit %s\n", fprintf(stderr, "ww: cannot commit %s\n",
@@ -1212,6 +1320,20 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out,
} }
} }
if (emit_asm) { free(order); return 0; } if (emit_asm) { free(order); return 0; }
if (root_package) {
char archive[1024], iface[1024], outiface[1100];
sep_fname(g, root, scratch, ".a", archive, sizeof archive);
sep_fname(g, root, scratch, ".wwi", iface, sizeof iface);
snprintf(outiface, sizeof outiface, "%s.wwi", out);
if (copy_file_atomic(archive, out) != 0
|| copy_file_atomic(iface, outiface) != 0) {
fprintf(stderr, "ww: cannot write package artifact %s\n", out);
free(order);
return 1;
}
free(order);
return 0;
}
/* 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 —
@@ -1256,15 +1378,16 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out,
* covers every internal-scratch impl return. The path is nonempty only after * covers every internal-scratch impl return. The path is nonempty only after
* this invocation successfully created the exact `.sepwork` tree. */ * this invocation successfully created the exact `.sepwork` tree. */
static int static int
build_one_sep(const char *src, int entry_is_dir, const char *out, build_one_sep(const char *src, int entry_is_dir, const char *root_identity,
const char *out,
const char *objstem, const char *extra_includes, const char *extra_libs, const char *objstem, const char *extra_includes, const char *extra_libs,
const char *extra_libdirs, int is_test, int emit_asm, int keepscratch, const char *extra_libdirs, int package_only, int is_test, int emit_asm,
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, out, objstem, int r = build_one_sep_impl(src, entry_is_dir, root_identity, out, objstem,
extra_includes, extra_libs, extra_libdirs, is_test, emit_asm, extra_includes, extra_libs, extra_libdirs, package_only, is_test, emit_asm,
workdir, scratch, sizeof scratch, &g); workdir, scratch, sizeof scratch, &g);
sep_graph_free(g); sep_graph_free(g);
if (!keepscratch && scratch[0]) { if (!keepscratch && scratch[0]) {
@@ -1371,10 +1494,11 @@ parse_build_flags(const char *cmd, int argc, char **argv,
char *libs, size_t libsz, char *libs, size_t libsz,
char *outpath, size_t outsz, char *outpath, size_t outsz,
char *workdir, size_t workdirsz, char *workdir, size_t workdirsz,
const char **src_out, int *emit_asm_out) const char **src_out, int *emit_asm_out, int *package_out)
{ {
*src_out = NULL; *src_out = NULL;
if (emit_asm_out) *emit_asm_out = 0; if (emit_asm_out) *emit_asm_out = 0;
if (package_out) *package_out = 0;
int i = 0; int i = 0;
for (; i < argc; i++) { for (; i < argc; i++) {
if (strcmp(argv[i], "-S") == 0) { if (strcmp(argv[i], "-S") == 0) {
@@ -1383,6 +1507,12 @@ parse_build_flags(const char *cmd, int argc, char **argv,
return -1; return -1;
} }
*emit_asm_out = 1; *emit_asm_out = 1;
} else if (strcmp(argv[i], "-p") == 0) {
if (package_out == NULL) {
fprintf(stderr, "ww %s: unknown flag\n", cmd);
return -1;
}
*package_out = 1;
} else if (strcmp(argv[i], "-w") == 0) { } else if (strcmp(argv[i], "-w") == 0) {
if (workdir == NULL) { if (workdir == NULL) {
fprintf(stderr, "ww %s: unknown flag\n", cmd); fprintf(stderr, "ww %s: unknown flag\n", cmd);
@@ -1470,18 +1600,29 @@ do_build(int argc, char **argv)
char outflag[1024] = {0}; char outflag[1024] = {0};
char workdir[1024] = {0}; char workdir[1024] = {0};
int emit_asm = 0; int emit_asm = 0;
int package_only = 0;
if (parse_build_flags("build", argc, argv, incs, sizeof incs, if (parse_build_flags("build", argc, argv, incs, sizeof incs,
libdirs, sizeof libdirs, libs, sizeof libs, libdirs, sizeof libdirs, libs, sizeof libs,
outflag, sizeof outflag, workdir, sizeof workdir, outflag, sizeof outflag, workdir, sizeof workdir,
&src, &emit_asm) < 0) &src, &emit_asm, &package_only) < 0)
return 2; return 2;
if (src == NULL) src = "."; if (src == NULL) src = ".";
if (package_only && emit_asm) {
fprintf(stderr, "ww build: -p and -S cannot be combined\n");
return 2;
}
struct stat requested;
int literal = stat(src, &requested) == 0;
char resolved[1024]; char resolved[1024];
int is_dir = 0; int is_dir = 0;
if (!resolve_module(src, incs, resolved, sizeof resolved, &is_dir)) { if (!resolve_module(src, incs, resolved, sizeof resolved, &is_dir)) {
fprintf(stderr, "ww build: cannot find module %s\n", src); fprintf(stderr, "ww build: cannot find module %s\n", src);
return 1; return 1;
} }
if (package_only && !is_dir) {
fprintf(stderr, "ww build: -p needs a package directory\n");
return 2;
}
char out[1024]; char out[1024];
const char *objstem = NULL; const char *objstem = NULL;
if (outflag[0]) { if (outflag[0]) {
@@ -1499,8 +1640,9 @@ do_build(int argc, char **argv)
} else { } else {
basename_no_ext(resolved, out, sizeof out); basename_no_ext(resolved, out, sizeof out);
} }
return build_one_sep(resolved, is_dir, out, objstem, incs, libs, const char *root_identity = package_only && !literal ? src : NULL;
libdirs, 0, emit_asm, 1, workdir); return build_one_sep(resolved, is_dir, root_identity, out, objstem, incs, libs,
libdirs, package_only, 0, emit_asm, 1, workdir);
} }
static int static int
@@ -1513,7 +1655,7 @@ do_run(int argc, char **argv)
char outflag[1024] = {0}; /* -o accepted+ignored: run always uses the temp */ char outflag[1024] = {0}; /* -o accepted+ignored: run always uses the temp */
int next = parse_build_flags("run", argc, argv, incs, sizeof incs, int next = parse_build_flags("run", argc, argv, incs, sizeof incs,
libdirs, sizeof libdirs, libs, sizeof libs, libdirs, sizeof libdirs, libs, sizeof libs,
outflag, sizeof outflag, NULL, 0, &src, NULL); outflag, sizeof outflag, NULL, 0, &src, NULL, NULL);
if (next < 0) return 2; if (next < 0) return 2;
if (src == NULL) src = "."; if (src == NULL) src = ".";
char resolved[1024]; char resolved[1024];
@@ -1531,8 +1673,8 @@ do_run(int argc, char **argv)
snprintf(tmp, sizeof tmp, "%s/main", tmpdir); snprintf(tmp, sizeof tmp, "%s/main", tmpdir);
/* 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, tmp, tmp, incs, libs, libdirs, 0, 0, if (build_one_sep(resolved, is_dir, NULL, tmp, tmp, incs, libs, libdirs,
0, NULL) != 0) { 0, 0, 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)
@@ -1754,8 +1896,8 @@ do_test(int argc, char **argv)
} }
/* No-o redirects internal scratch to /tmp rather than beside the /* No-o redirects internal scratch to /tmp rather than beside the
* 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, outp, int br = build_one_sep(resolved, is_dir, NULL, outp,
outstem[0] ? outstem : tmp, incs, "", "", 1, outstem[0] ? outstem : tmp, incs, "", "", 0, 1,
emit_asm, outstem[0] ? 1 : 0, workdir); 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)
@@ -1811,8 +1953,8 @@ do_test(int argc, char **argv)
outp = tmp; outp = tmp;
} }
/* 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, outp, outstem[0] ? outstem : tmp, int br = build_one_sep(target, 0, NULL, outp, outstem[0] ? outstem : tmp,
incs, "", "", 1, emit_asm, outstem[0] ? 1 : 0, workdir); incs, "", "", 0, 1, 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);

View File

@@ -2770,6 +2770,59 @@ The replacement deletes these concepts rather than emulating them indefinitely:
There will be no compatibility alias that silently translates an old import, There will be no compatibility alias that silently translates an old import,
interface, workdir, or link search into the new model. interface, workdir, or link search into the new model.
### 11.6 Implemented local package slice
The first executable package slice is intentionally smaller than the final
module design above. It is local, offline, and manifest-free. The supported
form is:
```sh
out/bin/ww build -I /work/acme -o app /work/acme/cmd/app
```
Every selected source uses the existing syntax:
```ww
package main;
import lib.math;
```
An import is translated from dots to path separators and resolved, with
directory packages preferred, through the entry package's directory, explicit
`-I` roots in command order, and the toolchain source-library root. There is no
network or manifest fallback. The loader uses the compiler frontend's
imports-only parser, unions duplicate imports, byte-sorts direct edges, interns
resolved directories by filesystem identity, and reports self-imports and
stable cycle chains before compilation.
A directory package consists of its immediate regular non-symlink `.ww` files,
excluding `*_test.ww`, in byte-sorted filename order. Every selected file must
declare the same package. An imported directory's declared package must equal
the final component of its import path; two logical identities for one physical
directory are rejected rather than compiled twice.
Packages compile serially in dependency-first postorder. The compiler emits the
existing deterministic `.wwi` interface for every importable package and only
exported declarations enter that interface. A source qualifier is visible only
when its owning package directly imports it; private members and transitive-only
qualifiers are compiler errors. For compatibility with public signatures that
name deeper types, composed compiler units still carry transitive interface type
facts for internal resolution, but those facts do not create source-visible
bare names, package qualifiers, or value bindings. Replacing that
source-like closure with a self-contained typed export encoding remains part of
the later export-format work, not package-loader semantics.
An ordinary root is linked with the full reachable object closure into the
requested executable (legacy WW programs may use a package name other than
`main`). `ww build -p -o lib.a DIR` explicitly requests a non-main package
product: it emits a deterministic archive at `lib.a` and its compiler interface
at `lib.a.wwi`, without invoking the linker. A logical target retains its full
identity (`ww build -p -I ROOT -o bar.a foo.bar` emits `foo.bar.*` symbols),
while a literal directory uses its declared leaf package. Package output
requires a directory and `-p` cannot be combined with assembly-only `-S`. Two
cold builds with identical inputs are required to produce byte-identical
requested products.
## 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

@@ -855,6 +855,27 @@ export fn stat(out: *filestat, path: str) (void | oserror) = {
fillfilestat(out, &k); fillfilestat(out, &k);
}; };
// samefile reports whether two existing paths resolve to the same kernel
// object. It follows terminal symlinks, like stat(2), and compares both the
// device and inode kept in the native stat record. Package loading uses this
// narrow primitive to intern directories by identity without exposing a
// platform-specific canonical pathname policy.
export fn samefile(a: str, b: str) bool = {
let ap: *u8 = kpath(a);
if (ap == nil: *u8) { return false; };
let ak: kstat;
let ar: i64 = syscall4(nr.NEWFSTATAT,
AT_FDCWD: i64, ap: i64, (&ak): i64, 0i64);
if (ar < 0) { return false; };
let bp: *u8 = kpath(b);
if (bp == nil: *u8) { return false; };
let bk: kstat;
let br: i64 = syscall4(nr.NEWFSTATAT,
AT_FDCWD: i64, bp: i64, (&bk): i64, 0i64);
if (br < 0) { return false; };
return ak.dev == bk.dev && ak.ino == bk.ino;
};
// lstat — like [[stat]] but does NOT follow a terminal symlink. // lstat — like [[stat]] but does NOT follow a terminal symlink.
// Mirrors Hare's sys::lstat (ref/hare/sys/+linux/stat.ha:57). // Mirrors Hare's sys::lstat (ref/hare/sys/+linux/stat.ha:57).
export fn lstat(out: *filestat, path: str) (void | oserror) = { export fn lstat(out: *filestat, path: str) (void | oserror) = {

View File

@@ -17,6 +17,7 @@ import os;
import os.exec; import os.exec;
import rt; import rt;
import strings; import strings;
import syntax;
// All path/string scratch buffers go on the runtime page allocator. // All path/string scratch buffers go on the runtime page allocator.
// One page is plenty for any path we build. PATH_MAX lives in lib/os // One page is plenty for any path we build. PATH_MAX lives in lib/os
@@ -33,6 +34,25 @@ fn cerr(m: str) void = {
os.write(2, m.ptr, m.len: u64); os.write(2, m.ptr, m.len: u64);
}; };
fn cerrnum(v: i32) void = {
let digits: [16]u8;
let n: i32 = 0;
let x: i32 = v;
if (x <= 0) { digits[n] = '0'; n += 1; }
else {
for (x > 0) {
digits[n] = ((x % 10) + ('0': i32)): u8;
n += 1;
x = x / 10;
};
};
for (n > 0) { n -= 1; os.write(2, &digits[n], 1u64); };
};
fn cerrpos(file: str, line: i32, col: i32) void = {
cerr(file); cerr(":"); cerrnum(line); cerr(":"); cerrnum(col);
};
fn cstrlen(p: *u8) u64 = { fn cstrlen(p: *u8) u64 = {
let n: u64 = 0u64; let n: u64 = 0u64;
for (p[n] != 0u8) { n += 1u64; }; for (p[n] != 0u8) { n += 1u64; };
@@ -386,6 +406,22 @@ fn dirfileclass(dirpath: *u8, name: *u8, nlen: u64) i32 = {
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; }; if (strings.hassuffix(s, "_test.ww")) { return 0; };
let source: *u8 = joinpath(dirpath, name);
let fi: os.filestat;
let sr: (void | os.oserror) = os.lstat(&fi, pathstr(source));
let regular: bool = false;
match (sr) {
case void => {
let t: u32 = (fi.mode: u32) & 61440u32;
if (t == os.mode.REG: u32) { regular = true; };
};
case let e: os.oserror => void;
};
if (!regular) {
cerr("ww: "); cerr(pathstr(source));
cerr(": package source is not a regular file\n");
return -2;
};
if (dirfileattest(dirpath, name)) { return -1; }; if (dirfileattest(dirpath, name)) { return -1; };
return 1; return 1;
}; };
@@ -438,13 +474,17 @@ fn enumeratedir(dirpath: *u8) (**u8, i32) = {
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);
if (cls < 0) { if (cls == -1) {
cerr("ww: "); cerr("ww: ");
cerr(pathstr(joinpath(dirpath, nm))); cerr(pathstr(joinpath(dirpath, nm)));
cerr(": @test declaration outside *_test.ww\n"); cerr(": @test declaration outside *_test.ww\n");
os.close(fd); os.close(fd);
return nil: **u8, -2; return nil: **u8, -2;
}; };
if (cls == -2) {
os.close(fd);
return nil: **u8, -2;
};
if (cls > 0) { if (cls > 0) {
if (n >= cap) { if (n >= cap) {
let ncap: i32 = cap * 2; let ncap: i32 = cap * 2;
@@ -527,59 +567,17 @@ fn slurp(pathcs: *u8) (*u8, u64) = {
let buf: []u8 = alloc([], nu + 1u64)!; let buf: []u8 = alloc([], nu + 1u64)!;
buf.len = (nu + 1u64): i32; buf.len = (nu + 1u64): i32;
let rr: (i64 | os.oserror) = os.readall(fd, buf.ptr, nu); let rr: (i64 | os.oserror) = os.readall(fd, buf.ptr, nu);
os.close(fd); let closed: i32 = os.close(fd);
let got: i64 = 0i64; let got: i64 = 0i64;
match (rr) { match (rr) {
case let v: i64 => got = v; case let v: i64 => got = v;
case let e: os.oserror => return nil, 0u64; case let e: os.oserror => return nil, 0u64;
}; };
if (got != n) { return nil, 0u64; }; if (got != n || closed != 0) { return nil, 0u64; };
buf[nu] = 0u8; buf[nu] = 0u8;
return buf.ptr, nu; return buf.ptr, nu;
}; };
fn isidentbyte(c: u8) bool = {
if (c >= 'a' && c <= 'z') { return true; };
if (c >= 'A' && c <= 'Z') { return true; };
if (c >= '0' && c <= '9') { return true; };
if (c == '_') { return true; };
if (c == '.') { return true; };
return false;
};
// Scan one `import IDENT;` line out of [start, end). Returns the start
// of the ident and its length, or (nil, 0) if no `import` here. The
// caller passes a slice of the source: src points at the line start.
fn scanuse(src: *u8, len: u64) (*u8, u64) = {
let i: u64 = 0u64;
for (i < len) {
if (src[i] != 32u8) { if (src[i] != 9u8) { break; }; };
i += 1u64;
};
// i+7>len guard kept: hasprefix("import") covers the 6 spell-out
// bytes, but the sep read at src[i+6] still needs i+6 < len.
if (i + 7u64 > len) { return nil, 0u64; };
let rest: str;
rest.ptr = src + i;
rest.len = (len - i): i32;
if (!strings.hasprefix(rest, "import")) { return nil, 0u64; };
let sep: u8 = src[i + 6u64];
if (sep != 32u8) { if (sep != 9u8) { return nil, 0u64; }; };
i += 7u64;
for (i < len) {
if (src[i] != 32u8) { if (src[i] != 9u8) { break; }; };
i += 1u64;
};
let idstart: u64 = i;
for (i < len) {
if (!isidentbyte(src[i])) { break; };
i += 1u64;
};
let idlen: u64 = i - idstart;
if (idlen == 0u64) { return nil, 0u64; };
return src + idstart, idlen;
};
fn makestem(stem: *u8, src: *u8) void = { fn makestem(stem: *u8, src: *u8) void = {
let n: u64 = cstrlen(src); let n: u64 = cstrlen(src);
let stop: u64 = n; let stop: u64 = n;
@@ -632,8 +630,10 @@ type lflags = struct {
// reference (#40) and the sep `.o`s link. The prepend is the TRANSITIVE // reference (#40) and the sep `.o`s link. The prepend is the TRANSITIVE
// closure of a package's deps (lead-ratified): a dep's interface can // closure of a package's deps (lead-ratified): a dep's interface can
// name a transitive dep's type, so the consuming unit needs the whole // name a transitive dep's type, so the consuming unit needs the whole
// closure for resolution. The unit composition is byte-identical to the // closure for resolution. Qualifier lookup remains scoped to the direct
// cstage driver (rule 10) so w6c/w6c_ww emit identical `.s`. // imports owned by each source package, so those type facts do not expose
// a transitive package. The unit composition is byte-identical to the cstage
// driver (rule 10) so w6c/w6c_ww emit identical `.s`.
def SEP_MAXPKG: i32 = 256; def SEP_MAXPKG: i32 = 256;
@@ -655,9 +655,30 @@ type sepgraph = struct {
}; };
fn sepfindoradd(g: *sepgraph, path: *u8, entry: *u8, isdir: i32) i32 = { fn sepfindoradd(g: *sepgraph, path: *u8, entry: *u8, isdir: i32) i32 = {
if (cstrlen(path) >= 256u64) {
cerr("ww: package path is too long (limit 255 bytes)\n");
return -1;
};
let i: i32 = 0; let i: i32 = 0;
for (i < g.n) { for (i < g.n) {
if (cstreq(g.pkg[i].path, path)) { return i; }; if (cstreq(g.pkg[i].path, path)) {
if (!os.samefile(pathstr(g.pkg[i].entry), pathstr(entry))) {
cerr("ww: package "); cerr(pathstr(path));
cerr(" resolves to more than one location\n");
return -1;
};
return i;
};
if (os.samefile(pathstr(g.pkg[i].entry), pathstr(entry))) {
cerr("ww: one package directory has identities ");
if (g.pkg[i].path[0u64] == 0u8) { cerr("(root)"); }
else { cerr(pathstr(g.pkg[i].path)); };
cerr(" and ");
if (path[0u64] == 0u8) { cerr("(root)"); }
else { cerr(pathstr(path)); };
cerr("\n");
return -1;
};
i += 1; i += 1;
}; };
if (g.n >= SEP_MAXPKG) { if (g.n >= SEP_MAXPKG) {
@@ -723,140 +744,6 @@ fn sepfname(g: *sepgraph, pi: i32, scratch: *u8, suffix: str) *u8 = {
return buf.ptr; return buf.ptr;
}; };
// unithaspackage — does the unit buffer declare `package <leaf>;` ANYWHERE?
// #16 ENFORCE-driver (rob A) cstage unit_has_package twin: distinguishes a
// genuinely-missing import from one satisfied by an INLINE package in the
// same single-file multi-package unit (`package aa; ... package main;
// import aa;`). Scans EVERY line (comment-skip) — not just the first
// package decl. Decision byte-identical to
// cstage so the skip/fatal choice + driver output match (rule 10).
fn unithaspackage(buf: *u8, buflen: u64, leafp: *u8, leafn: u64) bool = {
let p: u64 = 0u64;
for (p < buflen) {
let q: u64 = p;
for (q < buflen) { if (buf[q] == 10u8) { break; }; q += 1u64; };
let s: u64 = p;
for (s < q) {
if (buf[s] != 32u8) { if (buf[s] != 9u8) { break; }; };
s += 1u64;
};
if (s < q) {
let line: str;
line.ptr = buf + s;
line.len = (q - s): i32;
if (strings.hasprefix(line, "//")) { p = q + 1u64; continue; };
if (s + 8u64 <= q) {
if (strings.hasprefix(line, "package")) {
let sep: u8 = buf[s + 7u64];
let oksep: bool = false;
if (sep == 32u8) { oksep = true; }
else { if (sep == 9u8) { oksep = true; }; };
if (oksep) {
let t: u64 = s + 8u64;
for (t < q) {
if (buf[t] != 32u8) { if (buf[t] != 9u8) { break; }; };
t += 1u64;
};
let m: u64 = 0u64;
let eq: bool = true;
for (m < leafn) {
if (t + m >= q) { eq = false; break; };
if (buf[t + m] != leafp[m]) { eq = false; break; };
m += 1u64;
};
if (eq) {
let after: u64 = t + leafn;
let term: bool = false;
if (after >= q) { term = true; }
else {
let c: u8 = buf[after];
if (c == 59u8) { term = true; } // ';'
else { if (c == 32u8) { term = true; }
else { if (c == 9u8) { term = true; }; }; };
};
if (term) { return true; };
};
};
};
};
};
p = q + 1u64;
};
return false;
};
fn sepidentstart(c: u8) bool = {
if (c >= 'a' && c <= 'z') { return true; };
if (c >= 'A' && c <= 'Z') { return true; };
return c == '_';
};
fn sepidentcontinue(c: u8) bool = {
if (sepidentstart(c)) { return true; };
return 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.
fn sepskipspace(src: *u8, n: u64, start: u64, ok: *bool) u64 = {
let i: u64 = start;
*ok = true;
for (true) {
for (i < n && (src[i] == ' ' || src[i] == '\t'
|| src[i] == '\r' || src[i] == '\n')) { i += 1u64; };
if (i + 1u64 < n && src[i] == '/' && src[i + 1u64] == '/') {
i += 2u64;
for (i < n && src[i] != '\n') { i += 1u64; };
continue;
};
if (i + 1u64 < n && src[i] == '/' && src[i + 1u64] == '*') {
i += 2u64;
let closed: bool = false;
for (i + 1u64 < n) {
if (src[i] == '*' && src[i + 1u64] == '/') {
i += 2u64;
closed = true;
break;
};
i += 1u64;
};
if (!closed) { *ok = false; return i; };
continue;
};
break;
};
return i;
};
// Parse exactly the leading loader grammar `package ident;`.
fn seppackageclause(src: *u8, n: u64, outp: **u8, outn: *u64) bool = {
let ok: bool = true;
let i: u64 = sepskipspace(src, n, 0u64, &ok);
if (!ok || i + 7u64 >= n) { return false; };
let word: str = "package";
let j: i32 = 0;
for (j < word.len) {
let ju: u64 = j: u64;
if (src[i + ju] != word[j]) { return false; };
j += 1;
};
i += 7u64;
if (i >= n || !(src[i] == ' ' || src[i] == '\t'
|| src[i] == '\r' || src[i] == '\n')) { return false; };
i = sepskipspace(src, n, i, &ok);
if (!ok || i >= n || !sepidentstart(src[i])) { return false; };
let begin: u64 = i;
i += 1u64;
for (i < n && sepidentcontinue(src[i])) { i += 1u64; };
let end: u64 = i;
i = sepskipspace(src, n, i, &ok);
if (!ok || i >= n || src[i] != ';') { return false; };
*outp = src + begin;
*outn = end - begin;
return true;
};
// 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
@@ -877,44 +764,109 @@ fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8,
cerr("ww: cannot read source\n"); cerr("ww: cannot read source\n");
return -1; return -1;
}; };
if (ownedsource != 0) { let l: syntax.lex;
let declared: *u8 = nil; syntax.lexinit(&l, fdup, bufp, blen);
let declaredn: u64 = 0u64; let ps: syntax.parser;
if (!seppackageclause(bufp, blen, &declared, &declaredn)) { syntax.parserinit(&ps, &l);
cerr("ww: "); let imports: *syntax.node = syntax.parseimports(&ps);
cerr(pathstr(file)); if (l.errs > 0 || ps.errs > 0) { return -1; };
cerr(": invalid or missing package clause\n"); if (imports.nmod.len == 0 && ownedsource != 0) {
return -1; cerrpos(fdup, 1, 1);
}; cerr(": error: invalid or missing package clause\n");
return -1;
};
if (imports.nmod.len != 0
&& (ownedsource != 0 || g.pkg[pi].name == nil)) {
let declared: *u8 = imports.nmod.ptr;
let declaredn: u64 = imports.nmod.len: u64;
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),
declared, declaredn) != 0) { declared, declaredn) != 0) {
cerr("ww: "); cerrpos(imports.file, imports.line, imports.col);
cerr(pathstr(g.pkg[pi].entry)); cerr(": error: conflicting package names ");
cerr(": conflicting package names ");
cerr(pathstr(g.pkg[pi].name)); cerr(pathstr(g.pkg[pi].name));
cerr(" and "); cerr(" and ");
os.write(2, declared, declaredn); os.write(2, declared, declaredn);
cerr("\n"); cerr(" in "); cerr(pathstr(g.pkg[pi].entry)); cerr("\n");
return -1; return -1;
}; }; }; };
}; };
let i: u64 = 0u64; if (ownedsource != 0 && g.pkg[pi].isdir != 0) {
for (i < blen) { let pm: *syntax.node = imports.body;
let j: u64 = i; for (pm != nil) {
for (j < blen) { if (!syntax.streq(pm.nmod, pathstr(g.pkg[pi].name))) {
if (bufp[j] == 10u8) { break; }; // '\n' cerrpos(pm.file, pm.line, pm.col);
j += 1u64; cerr(": error: conflicting package names ");
cerr(pathstr(g.pkg[pi].name)); cerr(" and ");
cerr(pm.nmod); cerr(" in ");
cerr(pathstr(g.pkg[pi].entry)); cerr("\n");
return -1;
};
pm = pm.next;
}; };
let idp: *u8; };
let idn: u64; let nuse: i32 = 0;
idp, idn = scanuse(bufp + i, j - i); let u: *syntax.node = imports.list;
if (idp != nil) { for (u != nil) {
if (u.kind == syntax.nkind.N_USE) { nuse += 1; };
u = u.next;
};
let uses: []*syntax.node = [];
if (nuse > 0) {
let allocated: []*syntax.node = alloc([], nuse: u64)!;
uses = allocated;
uses.len = nuse;
};
let ui: i32 = 0;
u = imports.list;
for (u != nil) {
if (u.kind == syntax.nkind.N_USE) { uses[ui] = u; ui += 1; };
u = u.next;
};
let si: i32 = 1;
for (si < nuse) {
let sj: i32 = si;
for (sj > 0) {
if (strings.compare(uses[sj - 1].usepath,
uses[sj].usepath) <= 0) { sj = 0; }
else {
let t: *syntax.node = uses[sj];
uses[sj] = uses[sj - 1];
uses[sj - 1] = t;
sj -= 1;
};
};
si += 1;
};
let previous: str = "";
ui = 0;
for (ui < nuse) {
u = uses[ui];
let duplicate: bool = previous.len > 0
&& syntax.streq(previous, u.usepath);
if (!duplicate) {
previous = u.usepath;
let idp: *u8 = u.usepath.ptr;
let idn: u64 = u.usepath.len: u64;
if (idn >= 256u64) {
cerrpos(u.file, u.line, u.col);
cerr(": error: import path is too long (limit 255 bytes)\n");
return -1;
};
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 (os.samefile(pathstr(ipath), pathstr(g.pkg[pi].entry))) {
cerrpos(u.file, u.line, u.col);
cerr(": error: self-import: package '");
if (g.pkg[pi].path[0u64] != 0u8) {
cerr(pathstr(g.pkg[pi].path));
} else { cerr(pathstr(g.pkg[pi].name)); };
cerr("' cannot import itself\n");
return -1;
};
let nm: []u8 = alloc([], idn + 1u64)!; let nm: []u8 = alloc([], idn + 1u64)!;
let k: u64 = 0u64; let k: u64 = 0u64;
for (k < idn) { nm[k] = idp[k]; k += 1u64; }; for (k < idn) { nm[k] = idp[k]; k += 1u64; };
@@ -938,14 +890,6 @@ fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8,
}; };
}; };
} else { } else {
// #16 ENFORCE-driver (rob A): a locate-miss is legal
// when the package is defined INLINE in the same unit
// (single-file multi-package). leaf = last dotted
// component; inline `package <leaf>` -> skip (the
// checker binds it), else fatal. E3-C1 (#87): the
// legacy amalgamator that owned the genuine-missing
// case is gone, so the sep producer enforces it here
// (INV-2). cstage twin; fatal text identical.
let lstart: u64 = 0u64; let lstart: u64 = 0u64;
let lk: u64 = 0u64; let lk: u64 = 0u64;
for (lk < idn) { for (lk < idn) {
@@ -954,15 +898,23 @@ fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8,
}; };
let leafp: *u8 = idp + lstart; let leafp: *u8 = idp + lstart;
let leafn: u64 = idn - lstart; let leafn: u64 = idn - lstart;
if (!unithaspackage(bufp, blen, leafp, leafn)) { let inlinepackage: bool = false;
cerr("ww: cannot find package "); let pm: *syntax.node = imports.body;
for (pm != nil) {
if (bytecmp(pm.nmod.ptr, pm.nmod.len: u64,
leafp, leafn) == 0) { inlinepackage = true; };
pm = pm.next;
};
if (!inlinepackage) {
cerrpos(u.file, u.line, u.col);
cerr(": error: cannot find package ");
os.write(2, idp, idn); os.write(2, idp, idn);
cerr("\n"); cerr("\n");
return -1; return -1;
}; };
}; };
}; };
i = j + 1u64; ui += 1;
}; };
return 0; return 0;
}; };
@@ -1028,6 +980,24 @@ fn seploadpkg(g: *sepgraph, pi: i32, searchpath: *u8) i32 = {
rc = sepscanfile(g, pi, g.pkg[pi].entry, searchpath, &fv, 0); rc = sepscanfile(g, pi, g.pkg[pi].entry, searchpath, &fv, 0);
}; };
if (rc < 0) { return rc; }; if (rc < 0) { return rc; };
if (pi == 0 && g.pkg[pi].path[0u64] == 0u8
&& g.pkg[pi].name != nil) {
g.pkg[pi].path = arenadupcstr(g.pkg[pi].name,
cstrlen(g.pkg[pi].name));
};
let si: i32 = 1;
for (si < g.pkg[pi].ndeps) {
let v: i32 = g.pkg[pi].deps[si];
let sj: i32 = si;
for (sj > 0 && strings.compare(
pathstr(g.pkg[g.pkg[pi].deps[sj - 1]].path),
pathstr(g.pkg[v].path)) > 0) {
g.pkg[pi].deps[sj] = g.pkg[pi].deps[sj - 1];
sj -= 1;
};
g.pkg[pi].deps[sj] = v;
si += 1;
};
let k: i32 = 0; let k: i32 = 0;
for (k < g.pkg[pi].ndeps) { for (k < g.pkg[pi].ndeps) {
if (seploadpkg(g, g.pkg[pi].deps[k], searchpath) < 0) { return -1; }; if (seploadpkg(g, g.pkg[pi].deps[k], searchpath) < 0) { return -1; };
@@ -1093,55 +1063,106 @@ fn sepmarkdeps(g: *sepgraph, pi: i32, inset: []u8) void = {
// //ww:module-reset primary boundary (so -c emits its decls, imported // //ww:module-reset primary boundary (so -c emits its decls, imported
// ==0). DIRECTORY imports are skipped (provided as `.wwi` ahead); // ==0). DIRECTORY imports are skipped (provided as `.wwi` ahead);
// FILE imports fold in (intra-package split). // FILE imports fold in (intra-package split).
fn sepemitbody(fd: i32, path: *u8, visit: *expctx, searchpath: *u8, modpath: *u8) void = { fn sepwriteall(fd: i32, buf: *u8, n: u64) bool = {
match (os.writeall(fd, buf, n)) {
case let wrote: i64 => return wrote == n: i64;
case let e: os.oserror => return false;
};
};
fn sepemitbody(fd: i32, path: *u8, visit: *expctx, searchpath: *u8,
modpath: *u8) i32 = {
let pview: str; let pview: str;
pview.ptr = path; pview.ptr = path;
pview.len = cstrlen(path): i32; pview.len = cstrlen(path): i32;
let pdup: str = strings.dup(pview); let pdup: str = strings.dup(pview);
if (visitseen(visit, pdup)) { return; }; if (visitseen(visit, pdup)) { return 0; };
visitadd(visit, pdup); visitadd(visit, pdup);
let bufp: *u8; let bufp: *u8;
let blen: u64; let blen: u64;
bufp, blen = slurp(path); bufp, blen = slurp(path);
if (bufp == nil) { if (bufp == nil) {
cerr("ww: cannot read source\n"); cerr("ww: cannot read source\n");
return; return -1;
}; };
let i: u64 = 0u64; let l: syntax.lex;
for (i < blen) { syntax.lexinit(&l, pdup, bufp, blen);
let j: u64 = i; let ps: syntax.parser;
for (j < blen) { syntax.parserinit(&ps, &l);
if (bufp[j] == 10u8) { break; }; // '\n' let imports: *syntax.node = syntax.parseimports(&ps);
j += 1u64; if (l.errs > 0 || ps.errs > 0) { return -1; };
let nuse: i32 = 0;
let u: *syntax.node = imports.list;
for (u != nil) {
if (u.kind == syntax.nkind.N_USE) { nuse += 1; };
u = u.next;
};
let uses: []*syntax.node = [];
if (nuse > 0) {
let allocated: []*syntax.node = alloc([], nuse: u64)!;
uses = allocated;
uses.len = nuse;
};
let ui: i32 = 0;
u = imports.list;
for (u != nil) {
if (u.kind == syntax.nkind.N_USE) { uses[ui] = u; ui += 1; };
u = u.next;
};
let si: i32 = 1;
for (si < nuse) {
let sj: i32 = si;
for (sj > 0) {
if (strings.compare(uses[sj - 1].usepath,
uses[sj].usepath) <= 0) { sj = 0; }
else {
let t: *syntax.node = uses[sj];
uses[sj] = uses[sj - 1];
uses[sj - 1] = t;
sj -= 1;
};
}; };
let idp: *u8; si += 1;
let idn: u64; };
idp, idn = scanuse(bufp + i, j - i); ui = 0;
if (idp != nil) { for (ui < nuse) {
u = uses[ui];
let idp: *u8 = u.usepath.ptr;
let idn: u64 = u.usepath.len: u64;
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) {
sepemitbody(fd, ipath, visit, searchpath, modpath); if (sepemitbody(fd, ipath, visit, searchpath,
modpath) < 0) { return -1; };
}; };
}; };
}; ui += 1;
i = j + 1u64;
}; };
// #57: tag the primary body by its full dotted import path so the // #57: tag the primary body by its full dotted import path so the
// definer mangles == the importer reference; a root build (path "") // definer mangles == the importer reference; a root build (path "")
// stays a bare reset (keeps bare main). // stays a bare reset (keeps bare main).
if (modpath != nil && modpath[0u64] != 0u8) { if (modpath != nil && modpath[0u64] != 0u8) {
let dm: str = "//ww:module-reset "; let dm: str = "//ww:module-reset ";
os.writeall(fd, dm.ptr, dm.len: u64); if (!sepwriteall(fd, dm.ptr, dm.len: u64)
os.writeall(fd, modpath, cstrlen(modpath)); || !sepwriteall(fd, modpath, cstrlen(modpath))
os.writeall(fd, "\n".ptr, 1u64); || !sepwriteall(fd, "\n".ptr, 1u64)) {
cerr("ww: cannot write package unit\n");
return -1;
};
} else { } else {
let d: str = "//ww:module-reset\n"; let d: str = "//ww:module-reset\n";
os.writeall(fd, d.ptr, d.len: u64); if (!sepwriteall(fd, d.ptr, d.len: u64)) {
cerr("ww: cannot write package unit\n");
return -1;
};
}; };
os.writeall(fd, bufp, blen); if (!sepwriteall(fd, bufp, blen)
os.writeall(fd, "\n".ptr, 1u64); || !sepwriteall(fd, "\n".ptr, 1u64)) {
cerr("ww: cannot write package unit\n");
return -1;
};
return 0;
}; };
// Compose pi's sep-unit at `unitf`: the transitive-closure `.wwi`s // Compose pi's sep-unit at `unitf`: the transitive-closure `.wwi`s
@@ -1175,11 +1196,16 @@ fn sepcomposeunit(g: *sepgraph, pi: i32, scratch: *u8, order: []i32,
return -1; return -1;
}; };
let dm: str = "//ww:module "; let dm: str = "//ww:module ";
os.writeall(u, dm.ptr, dm.len: u64); if (!sepwriteall(u, dm.ptr, dm.len: u64)
os.writeall(u, g.pkg[dj].path, cstrlen(g.pkg[dj].path)); || !sepwriteall(u, g.pkg[dj].path,
os.writeall(u, "\n".ptr, 1u64); cstrlen(g.pkg[dj].path))
os.writeall(u, wb, wn); || !sepwriteall(u, "\n".ptr, 1u64)
os.writeall(u, "\n".ptr, 1u64); || !sepwriteall(u, wb, wn)
|| !sepwriteall(u, "\n".ptr, 1u64)) {
cerr("ww: cannot compose package unit\n");
os.close(u);
return -1;
};
}; };
}; };
oi += 1; oi += 1;
@@ -1188,18 +1214,23 @@ fn sepcomposeunit(g: *sepgraph, pi: i32, scratch: *u8, order: []i32,
bv.out = u; bv.out = u;
bv.dirs = searchpath; bv.dirs = searchpath;
bv.visit = nil; bv.visit = nil;
let bodyrc: i32 = 0;
if (g.pkg[pi].isdir != 0) { if (g.pkg[pi].isdir != 0) {
let i: i32 = 0; let i: i32 = 0;
for (i < g.pkg[pi].nsources) { for (i < g.pkg[pi].nsources && bodyrc == 0) {
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);
i += 1; i += 1;
}; };
} else { } else {
sepemitbody(u, g.pkg[pi].entry, &bv, searchpath, g.pkg[pi].path); bodyrc = sepemitbody(u, g.pkg[pi].entry, &bv, searchpath,
g.pkg[pi].path);
}; };
os.close(u); if (os.close(u) != 0) {
return 0; cerr("ww: cannot close package unit\n");
return -1;
};
return bodyrc;
}; };
// archiveo — twin of cmd/ww/main.c archive_o. Writes a deterministic // archiveo — twin of cmd/ww/main.c archive_o. Writes a deterministic
@@ -1268,8 +1299,12 @@ fn archiveo(objpath: *u8, apath: *u8) i32 = {
cerr("ww: cannot open archive\n"); cerr("ww: cannot open archive\n");
return -1; return -1;
}; };
os.writeall(fd, out, total); let bad: bool = !sepwriteall(fd, out, total);
os.close(fd); if (os.close(fd) != 0) { bad = true; };
if (bad) {
cerr("ww: cannot write archive\n");
return -1;
};
return 0; return 0;
}; };
@@ -1375,12 +1410,14 @@ fn copyfileatomic(src: *u8, dst: *u8) i32 = {
else { if (n == 0) { done = true; } else { if (n == 0) { done = true; }
else { else {
match (os.writeall(out, buf.ptr, n: u64)) { match (os.writeall(out, buf.ptr, n: u64)) {
case let w: i64 => void; case let w: i64 => {
if (w != n) { bad = true; done = true; };
};
case let e: os.oserror => { bad = true; done = true; }; case let e: os.oserror => { bad = true; done = true; };
}; };
}; }; }; };
}; };
os.close(in); if (os.close(in) != 0) { bad = true; };
if (os.close(out) != 0) { bad = true; }; if (os.close(out) != 0) { bad = true; };
if (bad) { return -1; }; if (bad) { return -1; };
return os.rename(pathstr(tmpp), pathstr(dst)); return os.rename(pathstr(tmpp), pathstr(dst));
@@ -1427,7 +1464,7 @@ fn writestampatomic(path: *u8, want: str) i32 = {
if (fd < 0) { return -1; }; if (fd < 0) { return -1; };
let bad: bool = false; let bad: bool = false;
match (os.writeall(fd, want.ptr, want.len: u64)) { match (os.writeall(fd, want.ptr, want.len: u64)) {
case let w: i64 => void; case let w: i64 => { if (w != want.len: i64) { bad = true; }; };
case let e: os.oserror => { bad = true; }; case let e: os.oserror => { bad = true; };
}; };
if (os.close(fd) != 0) { bad = true; }; if (os.close(fd) != 0) { bad = true; };
@@ -1444,9 +1481,11 @@ fn cerrpath(head: str, path: *u8, tail: str) void = {
cerr(tail); cerr(tail);
}; };
fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
objstem: *u8, incs: *u8, lf: *lflags, istest: i32, emitasm: i32, rootidentity: *u8, out: *u8,
workdir: *u8, scratchout: **u8, graphout: **sepgraph) i32 = { objstem: *u8, incs: *u8, lf: *lflags, packageonly: i32, istest: i32,
emitasm: i32, workdir: *u8, scratchout: **u8,
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");
let l6: *u8 = joinpathlit(selfdir, "w6l_ww"); let l6: *u8 = joinpathlit(selfdir, "w6l_ww");
@@ -1588,7 +1627,9 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
pkgslot.len = SEP_MAXPKG; pkgslot.len = SEP_MAXPKG;
let g: *sepgraph = alloc(sepgraph{pkg = pkgslot, n = 0})!; let g: *sepgraph = alloc(sepgraph{pkg = pkgslot, n = 0})!;
if (graphout != nil) { *graphout = g; }; if (graphout != nil) { *graphout = g; };
let root: i32 = sepfindoradd(g, "\0".ptr, src, entryisdir); let rootpath: *u8 = "\0".ptr;
if (packageonly != 0 && rootidentity != nil) { rootpath = rootidentity; };
let root: i32 = sepfindoradd(g, rootpath, src, entryisdir);
if (root < 0) { return 1; }; if (root < 0) { return 1; };
// #79 (-T): lib/test is the synth main's `test.run` callee but @test // #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 // files never `import test;`. Inject it as a direct dep of the root so
@@ -1616,6 +1657,11 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
}; };
}; };
if (seploadpkg(g, root, searchpath.ptr) < 0) { return 1; }; if (seploadpkg(g, root, searchpath.ptr) < 0) { return 1; };
let rootpackage: bool = packageonly != 0;
if (rootpackage && cstreqlit(g.pkg[root].name, "main")) {
cerr("ww: -p requires a non-main package\n");
return 1;
};
let ci: i32 = 0; let ci: i32 = 0;
for (ci < g.n) { g.pkg[ci].color = 0; ci += 1; }; for (ci < g.n) { g.pkg[ci].color = 0; ci += 1; };
@@ -1625,6 +1671,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
stack.len = g.n; stack.len = g.n;
let norder: i32 = 0; let norder: i32 = 0;
if (septopovisit(g, root, order, &norder, stack, 0) < 0) { return 1; }; if (septopovisit(g, root, order, &norder, stack, 0) < 0) { return 1; };
if (!rootpackage) { g.pkg[root].path = "\0".ptr; };
let oi: i32 = 0; let oi: i32 = 0;
for (oi < norder) { for (oi < norder) {
@@ -1650,6 +1697,8 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
cu = unitnew; cw = wwinew; cs = asmnew; cu = unitnew; cw = wwinew; cs = asmnew;
co = objnew; ca = anew; co = objnew; ca = anew;
}; };
let needsexport: bool = (pi != root) || rootpackage;
let needsarchive: bool = (pi != root) || rootpackage;
if (sepcomposeunit(g, pi, scratch, order, norder, searchpath.ptr, cu) < 0) { if (sepcomposeunit(g, pi, scratch, order, norder, searchpath.ptr, cu) < 0) {
return 1; return 1;
}; };
@@ -1659,7 +1708,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
fresh = fileequal(unitnew, unitf); fresh = fileequal(unitnew, unitf);
if (fresh) { fresh = fileisreg(asmf); }; if (fresh) { fresh = fileisreg(asmf); };
if (fresh) { if (fresh) {
if (pi != root) { if (needsexport) {
fresh = fileisreg(wwi); fresh = fileisreg(wwi);
}; };
}; };
@@ -1669,7 +1718,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
}; };
}; };
if (fresh) { if (fresh) {
if (emitasm == 0 && pi != root) { if (emitasm == 0 && needsarchive) {
fresh = filesizenonzero(apath); fresh = filesizenonzero(apath);
}; };
}; };
@@ -1695,12 +1744,12 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
// 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 alen: u64 = 8u64; let alen: u64 = 8u64;
if (pi == root) { alen = 6u64; if (roott) { alen = 7u64; }; }; if (!needsexport) { alen = 6u64; if (roott) { alen = 7u64; }; };
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, "-c"); append(argv, "-c");
if (pi != root) { if (needsexport) {
append(argv, "-I"); append(argv, "-I");
append(argv, pathstr(cw)); append(argv, pathstr(cw));
}; };
@@ -1744,7 +1793,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
// the build target), so `main` is defined before any archive is // the build target), so `main` is defined before any archive is
// processed. The link // processed. The link
// consumes `.o`/`.a`, never `.wwi`. // consumes `.o`/`.a`, never `.wwi`.
if (emitasm == 0 && pi != root) { if (emitasm == 0 && needsarchive) {
if (archiveo(co, ca) != 0) { if (archiveo(co, ca) != 0) {
cerr("ww: archive failed\n"); cerr("ww: archive failed\n");
return 1; return 1;
@@ -1754,7 +1803,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
// them, unit strictly last. // them, unit strictly last.
if (warm) { if (warm) {
let bad: bool = false; let bad: bool = false;
if (pi != root) { if (needsexport) {
if (os.rename(pathstr(wwinew), pathstr(wwi)) != 0) { if (os.rename(pathstr(wwinew), pathstr(wwi)) != 0) {
bad = true; bad = true;
}; };
@@ -1772,7 +1821,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
}; };
}; };
if (!bad) { if (!bad) {
if (emitasm == 0 && pi != root) { if (emitasm == 0 && needsarchive) {
if (os.rename(pathstr(anew), pathstr(apath)) != 0) { if (os.rename(pathstr(anew), pathstr(apath)) != 0) {
bad = true; bad = true;
}; };
@@ -1821,6 +1870,17 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
}; };
}; };
if (emitasm != 0) { return 0; }; if (emitasm != 0) { return 0; };
if (rootpackage) {
let archive: *u8 = sepfname(g, root, scratch, ".a");
let iface: *u8 = sepfname(g, root, scratch, ".wwi");
let outiface: *u8 = appendlit(out, ".wwi");
if (copyfileatomic(archive, out) != 0
|| copyfileatomic(iface, outiface) != 0) {
cerrpath("ww: cannot write package artifact ", out, "\n");
return 1;
};
return 0;
};
// 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
@@ -1895,13 +1955,15 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
// no-output single-file test remove internal scratch on success and failure. // no-output single-file test remove internal scratch on success and failure.
// The path is non-nil only after this invocation successfully created the // The path is non-nil only after this invocation successfully created the
// exact tree. Twin of the cstage build_one_sep wrapper. // exact tree. Twin of the cstage build_one_sep wrapper.
fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32,
objstem: *u8, incs: *u8, lf: *lflags, istest: i32, emitasm: i32, rootidentity: *u8, out: *u8,
keepscratch: i32, workdir: *u8) i32 = { objstem: *u8, incs: *u8, lf: *lflags, packageonly: i32, istest: i32,
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, out, objstem, let r: i32 = buildonesepimpl(selfdir, src, entryisdir, rootidentity,
incs, lf, istest, emitasm, workdir, &scratch, &g); out, objstem,
incs, lf, packageonly, istest, 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")) {
@@ -2001,7 +2063,7 @@ fn resolvemodule(selfdir: *u8, name: *u8, incs: *u8, isdir: *i32) *u8 = {
}; };
fn writeusage(fd: i32) void = { fn writeusage(fd: i32) void = {
let s: str = "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 version print version and exit\n\n path forms:\n foo.ww literal file\n foo search cwd, -I dirs, then the source library for foo.ww or foo/\n lib/foo directory: build its package sources\n lib/... every package under lib, recursively (test only)\n . build the cwd's <basename>.ww\n"; let s: str = "usage: ww [-V] <subcommand> [args...]\n -V print version and exit\n build [-p] [-S] [-w DIR] [-I DIR] [-o FILE] [path] build a local package graph\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 version print version and exit\n\n path forms:\n foo.ww literal file\n foo search cwd, -I dirs, then the source library for foo.ww or foo/\n lib/foo directory: build its package sources\n -p emits a non-main archive FILE + FILE.wwi\n lib/... every package under lib, recursively (test only)\n . build the cwd's <basename>.ww\n";
os.write(fd, s.ptr, s.len: u64); os.write(fd, s.ptr, s.len: u64);
}; };
@@ -2045,6 +2107,7 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
let outflag: *u8 = nil; // -o target (binary + intermediate stem); T3 let outflag: *u8 = nil; // -o target (binary + intermediate stem); T3
let workdir: *u8 = nil; // -w persistent package-artifact workdir let workdir: *u8 = nil; // -w persistent package-artifact workdir
let emitasm: i32 = 0; let emitasm: i32 = 0;
let packageonly: i32 = 0;
let incs: []u8 = alloc([], (os.PATH_MAX: u64) * 2u64)!; let incs: []u8 = alloc([], (os.PATH_MAX: u64) * 2u64)!;
incs.len = ((os.PATH_MAX: u64) * 2u64): i32; incs.len = ((os.PATH_MAX: u64) * 2u64): i32;
let incoff: u64 = 0u64; let incoff: u64 = 0u64;
@@ -2064,6 +2127,8 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
if (p[0u64] == 45u8) { // '-' if (p[0u64] == 45u8) { // '-'
if (cstreqlit(p, "-S")) { if (cstreqlit(p, "-S")) {
emitasm = 1; emitasm = 1;
} else { if (cstreqlit(p, "-p")) {
packageonly = 1;
} else { if (p[1u64] == 73u8) { // '-I' } else { if (p[1u64] == 73u8) { // '-I'
let dir: *u8 = nil; let dir: *u8 = nil;
if (p[2u64] != 0u8) { if (p[2u64] != 0u8) {
@@ -2143,7 +2208,7 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
} else { } else {
cerr("ww build: unknown flag\n"); cerr("ww build: unknown flag\n");
return 2; return 2;
}; }; }; }; }; }; }; }; }; }; }; }; };
} else { } else {
if (src == nil) { src = p; }; if (src == nil) { src = p; };
}; };
@@ -2154,12 +2219,26 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
let dot: [2]u8 = ['.': u8, 0u8]; let dot: [2]u8 = ['.': u8, 0u8];
src = &dot[0]; src = &dot[0];
}; };
if (packageonly != 0 && emitasm != 0) {
cerr("ww build: -p and -S cannot be combined\n");
return 2;
};
let requestedliteral: bool = false;
let requestedstat: os.filestat;
match (os.stat(&requestedstat, pathstr(src))) {
case void => requestedliteral = true;
case let e: os.oserror => void;
};
let isdir: i32 = 0; let isdir: i32 = 0;
let resolved: *u8 = resolvemodule(selfdir, src, incs.ptr, &isdir); let resolved: *u8 = resolvemodule(selfdir, src, incs.ptr, &isdir);
if (resolved == nil) { if (resolved == nil) {
cerr("ww build: cannot find module\n"); cerr("ww build: cannot find module\n");
return 1; return 1;
}; };
if (packageonly != 0 && isdir == 0) {
cerr("ww build: -p needs a package directory\n");
return 2;
};
let out: *u8 = nil; let out: *u8 = nil;
let objstem: *u8 = nil; let objstem: *u8 = nil;
if (outflag != nil) { if (outflag != nil) {
@@ -2189,8 +2268,11 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
lf.nlibdirs = nlibdirs; lf.nlibdirs = nlibdirs;
lf.libs = libs.ptr; lf.libs = libs.ptr;
lf.nlibs = nlibs; lf.nlibs = nlibs;
return buildonesep(selfdir, resolved, isdir, out, objstem, incs.ptr, &lf, let rootidentity: *u8 = nil;
0i32, emitasm, 1i32, workdir); if (packageonly != 0 && !requestedliteral) { rootidentity = src; };
return buildonesep(selfdir, resolved, isdir, rootidentity,
out, objstem, incs.ptr, &lf,
packageonly, 0i32, 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
@@ -2354,8 +2436,8 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
lf.libs = libs.ptr; lf.libs = libs.ptr;
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, outp, outp, incs.ptr, &lf, if (buildonesep(selfdir, resolved, isdir, nil, outp, outp, incs.ptr, &lf,
0i32, 0i32, 0i32, nil) != 0) { 0i32, 0i32, 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");
@@ -2444,8 +2526,8 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32,
lf.nlibs = 0; lf.nlibs = 0;
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, outp, objstem, incs, &lf, let bres: i32 = buildonesep(selfdir, src, 0, nil, outp, objstem, incs, &lf,
1i32, emitasm, keep, workdir); 0i32, 1i32, 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));

502
test/sep/localbuild_test.ww Normal file
View File

@@ -0,0 +1,502 @@
package localbuild_test;
// End-to-end local package builds through both production drivers. Fixtures
// are created at runtime so there is no manifest or hidden graph input: every
// edge below comes from an ordinary WW import declaration.
import os;
import os.exec;
import strings;
import testenv;
import time;
fn fail(label: str, why: str) void = {
let m: str = strings.concat("localbuild FAIL: ", label, " -- ", why,
"\n");
os.write(2, m.ptr, m.len: u64);
assert(false);
};
fn tmo() time.duration = {
return (240i64 * (time.second: i64)): time.duration;
};
fn mkdir(path: str) void = { assert(os.mkdir(path, 493) == 0); };
fn command(dir: str, name: str, argv: []str,
out: *testenv.commandout) void = {
testenv.runcommand(dir, dir, name, argv, tmo(), out);
};
fn code(dir: str, name: str, argv: []str) i32 = {
let co: testenv.commandout;
command(dir, name, argv, &co);
if (co.termination != exec.termination.EXIT) { return -1; };
return co.code;
};
fn build(dir: str, driver: str, tag: str, target: str, out: str) i32 = {
let av: []str = [testenv.driver(driver), "build", "-I", dir,
"-o", out, target];
return code(dir, strings.concat("build_", tag), av);
};
fn rejectstable(dir: str, label: str, target: str, needle: str) void = {
let drivers: []str = ["ww", "ww_ww", "ww", "ww_ww"];
let tags: []str = ["c1", "w1", "c2", "w2"];
let errors: []str = ["", "", "", ""];
let cwork: str = strings.concat(dir, "/work-", label, "-c");
let wwork: str = strings.concat(dir, "/work-", label, "-w");
mkdir(cwork); mkdir(wwork);
let i: i32 = 0;
for (i < drivers.len) {
let out: str = strings.concat(dir, "/reject-", label, "-", tags[i]);
let work: str = cwork;
if (i == 1 || i == 3) { work = wwork; };
let av: []str = [testenv.driver(drivers[i]), "build", "-I", dir,
"-w", work, "-o", out, target];
let co: testenv.commandout;
command(dir, strings.concat("reject_", label, "_", tags[i]), av,
&co);
if (co.termination != exec.termination.EXIT || co.code == 0) {
fail(label, strings.concat(drivers[i], " accepted invalid graph"));
};
if (!testenv.has(co.stderr, needle)) {
fail(label, strings.concat(drivers[i], " diagnostic missing '",
needle, "': ", co.stderr));
};
errors[i] = co.stderr;
i += 1;
};
if (!testenv.same(errors[0], errors[2])) {
fail(label, "C diagnostic changed across repeated builds");
};
if (!testenv.same(errors[1], errors[3])) {
fail(label, "WW diagnostic changed across repeated builds");
};
};
@test fn standalone_and_package_artifact() void = {
let td: str = testenv.fresh();
let app: str = strings.concat(td, "/app");
mkdir(app);
testenv.writefile(strings.concat(app, "/main.ww"), strings.concat(
"package main;\n",
"fn main() i32 = { return 23; };\n"));
// Ordinary builds must not accidentally select test-only sources.
testenv.writefile(strings.concat(app, "/broken_test.ww"),
"package other;\nthis is deliberately invalid\n");
let c1: str = strings.concat(td, "/standalone-c1");
let c2: str = strings.concat(td, "/standalone-c2");
let ww: str = strings.concat(td, "/standalone-ww");
if (build(td, "ww", "standalone_c1", app, c1) != 0
|| build(td, "ww", "standalone_c2", app, c2) != 0
|| build(td, "ww_ww", "standalone_ww", app, ww) != 0) {
fail("standalone", "manifest-free directory build failed");
};
let av1: []str = [c1];
let av2: []str = [ww];
if (code(td, "run_standalone_c", av1) != 23
|| code(td, "run_standalone_ww", av2) != 23) {
fail("standalone", "built executable returned the wrong value");
};
if (!testenv.same(testenv.readfile(c1), testenv.readfile(c2))) {
fail("standalone", "two clean builds were not byte-identical");
};
let lib: str = strings.concat(td, "/libonly");
mkdir(lib);
testenv.writefile(strings.concat(lib, "/lib.ww"), strings.concat(
"package libonly;\n",
"export fn shown() i32 = { return 7; };\n",
"fn hidden() i32 = { return 99; };\n"));
let ca: str = strings.concat(td, "/libonly-c.a");
let wa: str = strings.concat(td, "/libonly-w.a");
let cav: []str = [testenv.driver("ww"), "build", "-p", "-I", td,
"-o", ca, lib];
let wav: []str = [testenv.driver("ww_ww"), "build", "-p", "-I", td,
"-o", wa, lib];
if (code(td, "build_package_c", cav) != 0
|| code(td, "build_package_w", wav) != 0) {
fail("package-artifact", "non-main package build failed");
};
if (!testenv.exists(strings.concat(ca, ".wwi"))
|| !testenv.exists(strings.concat(wa, ".wwi"))) {
fail("package-artifact", "missing compiler export sidecar");
};
if (!testenv.same(testenv.readfile(ca), testenv.readfile(wa))
|| !testenv.same(testenv.readfile(strings.concat(ca, ".wwi")),
testenv.readfile(strings.concat(wa, ".wwi")))) {
fail("package-artifact", "C/WW package products differ");
};
let iface: str = testenv.readfile(strings.concat(ca, ".wwi"));
if (!testenv.has(iface, "shown") || testenv.has(iface, "hidden")) {
fail("package-artifact", "export interface crossed the private boundary");
};
testenv.clean(td);
};
fn writediamond(td: str, reverse: bool) str = {
let shared: str = strings.concat(td, "/shared");
let left: str = strings.concat(td, "/left");
let right: str = strings.concat(td, "/right");
let main: str = strings.concat(td, "/main");
if (reverse) { mkdir(left); mkdir(main); mkdir(shared); mkdir(right); }
else { mkdir(right); mkdir(shared); mkdir(main); mkdir(left); };
let zsrc: str = strings.concat(
"package shared;\n",
"fn hidden() i32 = { return 91; };\n");
let asrc: str = strings.concat(
"package shared;\n",
"export type token = struct { value: i32, };\n",
"export fn base() i32 = { return 3; };\n");
if (reverse) {
testenv.writefile(strings.concat(shared, "/a.ww"), asrc);
testenv.writefile(strings.concat(shared, "/z.ww"), zsrc);
} else {
testenv.writefile(strings.concat(shared, "/z.ww"), zsrc);
testenv.writefile(strings.concat(shared, "/a.ww"), asrc);
};
testenv.writefile(strings.concat(left, "/left.ww"), strings.concat(
"package left;\nimport shared;\nimport shared;\n",
"export fn value() i32 = { return shared.base() + 1; };\n"));
testenv.writefile(strings.concat(right, "/right.ww"), strings.concat(
"package right;\nimport shared;\n",
"export fn value() i32 = { return shared.base() + 2; };\n"));
testenv.writefile(strings.concat(main, "/main.ww"), strings.concat(
"package main;\nimport right;\nimport left;\n",
"fn main() i32 = { return left.value() + right.value(); };\n"));
return main;
};
@test fn direct_and_diamond() void = {
let td: str = testenv.fresh();
let treea: str = strings.concat(td, "/tree-a"); mkdir(treea);
let treeb: str = strings.concat(td, "/tree-b"); mkdir(treeb);
let main: str = writediamond(treea, false);
let shuffled: str = writediamond(treeb, true);
let drivers: []str = ["ww", "ww_ww"];
let tags: []str = ["c", "w"];
let i: i32 = 0;
for (i < drivers.len) {
let out: str = strings.concat(td, "/diamond-", tags[i]);
if (build(treea, drivers[i], strings.concat("diamond_", tags[i]),
main, out) != 0) {
fail("diamond", strings.concat(drivers[i], " build failed"));
};
let av: []str = [out];
if (code(td, strings.concat("run_diamond_", tags[i]), av) != 9) {
fail("diamond", "linked program returned the wrong value");
};
let scratch: str = strings.concat(out, ".sepwork/");
if (!testenv.exists(strings.concat(scratch, "shared.a"))
|| testenv.exists(strings.concat(scratch, "shared.shared.a"))) {
fail("diamond", "shared package was not interned exactly once");
};
let unit: str = testenv.readfile(strings.concat(scratch,
"__root.unit.ww"));
let lp: i32 = testenv.pos(unit, "//ww:module left\n");
let rp: i32 = testenv.pos(unit, "//ww:module right\n");
if (lp < 0 || rp < 0 || lp >= rp) {
fail("diamond", "dependency traversal did not byte-sort imports");
};
let sharedunit: str = testenv.readfile(strings.concat(scratch,
"shared.unit.ww"));
let ap: i32 = testenv.pos(sharedunit, "export type token");
let zp: i32 = testenv.pos(sharedunit, "fn hidden");
if (ap < 0 || zp < 0 || ap >= zp) {
fail("diamond", "package sources were not byte-sorted");
};
let leftiface: str = testenv.readfile(strings.concat(scratch,
"left.wwi"));
if (testenv.occurrences(leftiface, "import shared;") != 1) {
fail("diamond", "duplicate import escaped into export data");
};
i += 1;
};
let shuffledout: str = strings.concat(td, "/diamond-c-shuffled");
if (build(treeb, "ww", "diamond_c_shuffled", shuffled,
shuffledout) != 0) {
fail("diamond", "shuffled directory build failed");
};
let firstout: str = strings.concat(td, "/diamond-c");
if (!testenv.same(testenv.readfile(firstout),
testenv.readfile(shuffledout))) {
fail("diamond", "shuffled directory enumeration changed output");
};
let firstscratch: str = strings.concat(firstout, ".sepwork/");
let shuffledscratch: str = strings.concat(shuffledout, ".sepwork/");
if (!testenv.same(testenv.readfile(strings.concat(firstscratch,
"__root.unit.ww")), testenv.readfile(strings.concat(shuffledscratch,
"__root.unit.ww"))) || !testenv.same(testenv.readfile(strings.concat(
firstscratch, "shared.unit.ww")), testenv.readfile(strings.concat(
shuffledscratch, "shared.unit.ww")))) {
fail("diamond", "shuffled enumeration changed package units");
};
// Count actual compiler actions, rather than inferring interning from
// artifact names. The wrapper is selected through the production
// driver's existing tool override and records one row per w6c process.
let trace: str = strings.concat(td, "/compiler.trace");
let wrapper: str = strings.concat(td, "/trace-w6c.sh");
testenv.writefile(trace, "");
testenv.writefile(wrapper, strings.concat(
"printf '%s\\n' \"$*\" >> \"$WW_LOCALBUILD_TRACE\"\n",
"exec \"$WW_LOCALBUILD_W6C\" \"$@\"\n"));
let baseenv: []str = os.getenvs();
let env: []str = alloc([], (baseenv.len + 3): u64)!;
let ei: i32 = 0;
for (ei < baseenv.len) {
if (!strings.hasprefix(baseenv[ei], "WW_W6C=")
&& !strings.hasprefix(baseenv[ei], "WW_LOCALBUILD_TRACE=")
&& !strings.hasprefix(baseenv[ei], "WW_LOCALBUILD_W6C=")) {
append(env, baseenv[ei]);
};
ei += 1;
};
append(env, strings.concat("WW_W6C=/bin/sh ", wrapper));
append(env, strings.concat("WW_LOCALBUILD_TRACE=", trace));
append(env, strings.concat("WW_LOCALBUILD_W6C=",
testenv.driver("w6c")));
let countedout: str = strings.concat(td, "/diamond-counted");
let countedav: []str = [testenv.driver("ww"), "build", "-I", treea,
"-o", countedout, main];
let counted: testenv.commandout;
testenv.runcommandenv(td, td, "diamond_compile_count", countedav,
env, tmo(), &counted);
if (counted.termination != exec.termination.EXIT || counted.code != 0) {
fail("diamond", "instrumented build failed");
};
if (testenv.occurrences(testenv.readfile(trace), "shared.unit.ww") != 1) {
fail("diamond", "shared package was not compiled exactly once");
};
testenv.clean(td);
};
@test fn builtin_type_precedence() void = {
let td: str = testenv.fresh();
let dep: str = strings.concat(td, "/builtinshadow"); mkdir(dep);
let main: str = strings.concat(td, "/main"); mkdir(main);
testenv.writefile(strings.concat(dep, "/dep.ww"), strings.concat(
"package builtinshadow;\n",
"export type i32 = i8;\n",
"export fn width() i32 = { return size(i32): i32; };\n"));
testenv.writefile(strings.concat(main, "/main.ww"), strings.concat(
"package main;\nimport builtinshadow;\n",
"fn main() i32 = { return builtinshadow.width(); };\n"));
let drivers: []str = ["ww", "ww_ww"];
let tags: []str = ["c", "w"];
let outputs: []str = ["", ""];
let i: i32 = 0;
for (i < drivers.len) {
outputs[i] = strings.concat(td, "/builtin-", tags[i]);
if (build(td, drivers[i], strings.concat("builtin_", tags[i]),
main, outputs[i]) != 0) {
fail("builtin-type", strings.concat(drivers[i], " build failed"));
};
let av: []str = [outputs[i]];
if (code(td, strings.concat("run_builtin_", tags[i]), av) != 4) {
fail("builtin-type", strings.concat(drivers[i],
" let an imported type shadow intrinsic i32"));
};
i += 1;
};
if (!testenv.same(testenv.readfile(outputs[0]),
testenv.readfile(outputs[1]))) {
fail("builtin-type", "C/WW products differ");
};
testenv.clean(td);
};
@test fn export_visibility() void = {
let td: str = testenv.fresh();
let main: str = writediamond(td, false);
// The successful diamond already proves a direct exported declaration.
testenv.writefile(strings.concat(td, "/private.ww"), strings.concat(
"package main;\nimport shared;\n",
"fn main() i32 = { return shared.hidden(); };\n"));
rejectstable(td, "private", strings.concat(td, "/private.ww"),
"has no exported declaration 'hidden'");
testenv.writefile(strings.concat(td, "/transitive.ww"), strings.concat(
"package main;\nimport left;\n",
"fn main() i32 = { return shared.base(); };\n"));
rejectstable(td, "transitive", strings.concat(td, "/transitive.ww"),
"package 'shared' is not directly imported");
testenv.writefile(strings.concat(td, "/transitive-bare-value.ww"),
strings.concat("package main;\nimport left;\n",
"fn main() i32 = { return base(); };\n"));
rejectstable(td, "transitive-bare-value",
strings.concat(td, "/transitive-bare-value.ww"), "undefined: base");
testenv.writefile(strings.concat(td, "/transitive-bare-type.ww"),
strings.concat("package main;\nimport left;\n",
"fn main() i32 = { let x: token; return 0; };\n"));
rejectstable(td, "transitive-bare-type",
strings.concat(td, "/transitive-bare-type.ww"),
"unknown type 'token'");
// Keep the helper's main directory live so the graph also has a normal
// successful consumer in this fixture tree.
assert(testenv.exists(main));
testenv.clean(td);
};
@test fn import_diagnostics() void = {
let td: str = testenv.fresh();
let missing: str = strings.concat(td, "/missing"); mkdir(missing);
testenv.writefile(strings.concat(missing, "/main.ww"), strings.concat(
"package main;\nimport nowhere;\n",
"fn main() i32 = { return 0; };\n"));
rejectstable(td, "missing", missing, "cannot find package nowhere");
let self: str = strings.concat(td, "/self"); mkdir(self);
testenv.writefile(strings.concat(self, "/main.ww"), strings.concat(
"package main;\nimport self;\n",
"fn main() i32 = { return 0; };\n"));
rejectstable(td, "self", self, "self-import");
let malformed: str = strings.concat(td, "/malformed"); mkdir(malformed);
testenv.writefile(strings.concat(malformed, "/main.ww"), strings.concat(
"package main;\nimport ;\n",
"fn main() i32 = { return 0; };\n"));
rejectstable(td, "malformed", malformed,
"main.ww:2:8: error: expected identifier");
let attributed: str = strings.concat(td, "/attributed"); mkdir(attributed);
testenv.writefile(strings.concat(attributed, "/main.ww"), strings.concat(
"package main;\n@trace import nowhere;\n",
"fn main() i32 = { return 0; };\n"));
rejectstable(td, "attributed", attributed,
"import cannot be exported or attributed");
let exported: str = strings.concat(td, "/exported"); mkdir(exported);
testenv.writefile(strings.concat(exported, "/main.ww"), strings.concat(
"package main;\nexport import nowhere;\n",
"fn main() i32 = { return 0; };\n"));
rejectstable(td, "exported", exported,
"main.ww:2:8: error: import cannot be exported or attributed");
let conflict: str = strings.concat(td, "/conflict"); mkdir(conflict);
testenv.writefile(strings.concat(conflict, "/a.ww"),
"package main;\nfn main() i32 = { return 0; };\n");
testenv.writefile(strings.concat(conflict, "/z.ww"),
"package other;\nfn spare() void = { };\n");
rejectstable(td, "conflict", conflict, "conflicting package names");
let real: str = strings.concat(td, "/real"); mkdir(real);
testenv.writefile(strings.concat(real, "/real.ww"), strings.concat(
"package real;\n",
"export fn value() i32 = { return 1; };\n"));
let alias: str = strings.concat(td, "/alias");
assert(os.symlink(real, alias) == 0);
let aliasmain: str = strings.concat(td, "/alias-main"); mkdir(aliasmain);
testenv.writefile(strings.concat(aliasmain, "/main.ww"), strings.concat(
"package main;\nimport real;\nimport alias;\n",
"fn main() i32 = { return 0; };\n"));
rejectstable(td, "identity-alias", aliasmain, "identities");
testenv.clean(td);
};
@test fn package_artifact_identity_and_flag_contract() void = {
let td: str = testenv.fresh();
let root: str = strings.concat(td, "/root"); mkdir(root);
let foo: str = strings.concat(root, "/foo"); mkdir(foo);
let bar: str = strings.concat(foo, "/bar"); mkdir(bar);
testenv.writefile(strings.concat(bar, "/bar.ww"), strings.concat(
"package bar;\n",
"export fn nestedvalue() i32 = { return 41; };\n"));
let ca: str = strings.concat(td, "/nested-c.a");
let wa: str = strings.concat(td, "/nested-w.a");
let cav: []str = [testenv.driver("ww"), "build", "-p", "-I", root,
"-o", ca, "foo.bar"];
let wav: []str = [testenv.driver("ww_ww"), "build", "-p", "-I", root,
"-o", wa, "foo.bar"];
if (code(td, "nested_package_c", cav) != 0
|| code(td, "nested_package_w", wav) != 0) {
fail("package-identity", "nested logical package build failed");
};
if (!testenv.same(testenv.readfile(ca), testenv.readfile(wa))
|| !testenv.same(testenv.readfile(strings.concat(ca, ".wwi")),
testenv.readfile(strings.concat(wa, ".wwi")))) {
fail("package-identity", "nested C/WW package products differ");
};
let nestedasm: str = testenv.readfile(strings.concat(ca,
".sepwork/foo.bar.s"));
if (!testenv.has(nestedasm, "foo.bar.nestedvalue")) {
fail("package-identity", "archive lost the full logical import path");
};
let drivers: []str = ["ww", "ww_ww"];
let i: i32 = 0;
for (i < drivers.len) {
let badout: str = strings.concat(td, "/bad-flags-", drivers[i]);
let av: []str = [testenv.driver(drivers[i]), "build", "-p", "-S",
"-I", root, "-o", badout, "foo.bar"];
let co: testenv.commandout;
command(td, strings.concat("package_flag_contract_", drivers[i]),
av, &co);
if (co.termination != exec.termination.EXIT || co.code == 0
|| !testenv.has(co.stderr,
"ww build: -p and -S cannot be combined")) {
fail("package-flags", strings.concat(drivers[i],
" accepted -p -S"));
};
i += 1;
};
testenv.clean(td);
};
@test fn generated_test_hook_is_not_a_user_exemption() void = {
let td: str = testenv.fresh();
let src: str = strings.concat(td, "/case.ww");
testenv.writefile(src, strings.concat(
"package probe;\nimport test;\n",
"fn bad() void = { test.run(); };\n",
"@test fn one() void = {};\n"));
let compilers: []str = ["w6c", "w6c_ww"];
let i: i32 = 0;
for (i < compilers.len) {
let asm: str = strings.concat(td, "/case-", compilers[i], ".s");
let av: []str = [testenv.driver(compilers[i]), "-T", "-c", "-o",
asm, src];
let co: testenv.commandout;
command(td, strings.concat("test_hook_", compilers[i]), av, &co);
if (co.termination != exec.termination.EXIT || co.code == 0
|| !testenv.has(co.stderr,
"package 'test' has no exported declaration 'run'")) {
fail("test-hook", strings.concat(compilers[i],
" exempted a user-authored test.run"));
};
i += 1;
};
testenv.clean(td);
};
fn writecyclepkg(td: str, name: str, next: str) void = {
let dir: str = strings.concat(td, "/", name); mkdir(dir);
testenv.writefile(strings.concat(dir, "/p.ww"), strings.concat(
"package ", name, ";\nimport ", next, ";\n",
"export fn value() i32 = { return 1; };\n"));
};
@test fn cycle_diagnostics() void = {
let td: str = testenv.fresh();
let two: str = strings.concat(td, "/two-main"); mkdir(two);
testenv.writefile(strings.concat(two, "/main.ww"),
"package main;\nimport aa;\nfn main() i32 = { return 0; };\n");
writecyclepkg(td, "aa", "bb");
writecyclepkg(td, "bb", "aa");
rejectstable(td, "cycle-two", two,
"dependency cycle: aa -> bb -> aa");
rejectstable(td, "cycle-root", strings.concat(td, "/aa"),
"dependency cycle: aa -> bb -> aa");
// A second isolated root set avoids reusing the two-package graph.
let longroot: str = strings.concat(td, "/long-main"); mkdir(longroot);
testenv.writefile(strings.concat(longroot, "/main.ww"),
"package main;\nimport ca;\nfn main() i32 = { return 0; };\n");
writecyclepkg(td, "ca", "cb");
writecyclepkg(td, "cb", "cc");
writecyclepkg(td, "cc", "ca");
rejectstable(td, "cycle-long", longroot,
"dependency cycle: ca -> cb -> cc -> ca");
testenv.clean(td);
};