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/time/time.ww lib/types/types.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 \
$(LIB)/libwwrt.a | $(BIN)
@mkdir -p $(WWBUILD)/ww_ww
@$(CURDIR)/$(BIN)/ww build -w $(WWBUILD)/ww_ww \
-o $(WWBUILD)/ww_ww/main \
-I $(CURDIR)/lib/ww \
$(CURDIR)/selfhost/cmd/ww/main.ww
@mv $(WWBUILD)/ww_ww/main $@
@@ -366,7 +371,8 @@ BYTEID_WW_TARGETS = $(BYTEID_WW_TESTS:%=wwtest/%)
# test-compiler beside the surviving residual carriers.
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/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/%)
# Ww-native lib env/OS arranger observers: the env contracts of the
# 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
* out/bin/), and can be overridden with WW_W6C / WW_W6A / WW_W6L.
*/
#define _XOPEN_SOURCE 700
#include "ww.h"
#include <errno.h>
#include <string.h>
@@ -17,7 +18,7 @@
static const char *usage =
"usage: ww [-V] <subcommand> [args...]\n"
" -V print version and exit\n"
" build [-S] [-w DIR] [-o FILE] [path] compile module; -S stops after package asm\n"
" 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"
@@ -26,6 +27,7 @@ static const char *usage =
" 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";
@@ -279,6 +281,16 @@ enumerate_dir_ww(const char *dirpath, char ***out_files)
snprintf(path, sizeof path, "%s/%s", dirpath, nm);
if (nl >= 8 && strcmp(nm + nl - 8, "_test.ww") == 0)
continue;
struct stat st;
if (lstat(path, &st) != 0 || !S_ISREG(st.st_mode)) {
fprintf(stderr,
"ww: %s: package source is not a regular file\n", path);
for (int i = 0; i < n; i++) free(arr[i]);
free(arr);
closedir(d);
*out_files = NULL;
return -2;
}
if (file_has_line_test(path)) {
fprintf(stderr,
"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),
* so the consuming unit needs the whole closure for name RESOLUTION —
* direct-deps-only does not type-check. This mirrors harec reading the
* transitive `.td` closure and is consistent with the current flat-unit
* transitive-namespace model (the visibility tighten is task #45,
* deferred post-M4).
* transitive `.td` closure. The checker still scopes qualifier lookup to
* each source package's direct N_USE declarations, so carrying those type
* facts does not make a transitive package source-visible.
*/
#define SEP_MAXPKG 256
struct seppkg {
char path[256]; /* dotted import path; "" == root/primary */
char entry[1024]; /* resolved package dir (or file, for a file root) */
char canon[1024]; /* canonical location; never package identity */
char name[256]; /* validated declared name; directory packages only */
char **sources; /* owned, byte-sorted production paths; dirs only */
int nsources;
@@ -351,16 +364,54 @@ static int
sep_find_or_add(struct sepgraph *g, const char *path, const char *entry,
int is_dir)
{
for (int i = 0; i < g->n; i++)
if (strcmp(g->pkg[i].path, path) == 0) return i;
if (strlen(path) >= sizeof g->pkg[0].path) {
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) {
fprintf(stderr, "ww: too many packages (limit %d)\n",
SEP_MAXPKG);
free(canon);
return -1;
}
struct seppkg *p = &g->pkg[g->n];
snprintf(p->path, sizeof p->path, "%s", path);
snprintf(p->entry, sizeof p->entry, "%s", entry);
snprintf(p->canon, sizeof p->canon, "%s", canon);
free(canon);
p->is_dir = is_dir;
p->name[0] = '\0';
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);
}
/* unit_has_package — does `path` declare `package <leaf>;` ANYWHERE?
* #16 ENFORCE-driver (rob A): distinguishes a genuinely-missing import
* from one satisfied by an INLINE package in the same unit. Unlike a
* first-package-decl scan, this checks every line — single-file
* multi-package fixtures carry several `package` decls. The
* comment-skip line scan + name match are uncapped. The
* wwstage twin unithaspackage must stay byte-identical (rule 10). */
static int
unit_has_package(const char *path, const char *leaf)
sep_slurp(const char *path, char **out, u64 *len)
{
FILE *in = fopen(path, "rb");
if (in == NULL) return 0;
char line[2048];
int found = 0;
while (fgets(line, sizeof line, in)) {
const char *p = line;
while (*p == ' ' || *p == '\t') p++;
if (p[0] == '/' && p[1] == '/') continue;
if (strncmp(p, "package ", 8) != 0
&& strncmp(p, "package\t", 8) != 0) continue;
p += 8;
while (*p == ' ' || *p == '\t') p++;
size_t i = 0;
while (leaf[i] != '\0' && leaf[i] == p[i]) i++;
if (leaf[i] == '\0') {
char c = p[i];
if (c == ';' || c == ' ' || c == '\t'
|| c == '\n' || c == '\0') { found = 1; break; }
}
FILE *f = fopen(path, "rb");
if (f == NULL) return -1;
if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return -1; }
long n = ftell(f);
if (n < 0 || fseek(f, 0, SEEK_SET) != 0) {
fclose(f);
return -1;
}
fclose(in);
return found;
}
static int
sep_ident_start(int c)
{
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_';
}
static int
sep_ident_continue(int c)
{
return sep_ident_start(c) || (c >= '0' && c <= '9');
}
/* 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;
char *buf = malloc((size_t)n + 1);
if (buf == NULL) { fclose(f); return -1; }
if (fread(buf, 1, (size_t)n, f) != (size_t)n) {
free(buf);
fclose(f);
return -1;
}
*off = i;
buf[n] = '\0';
if (fclose(f) != 0) {
free(buf);
return -1;
}
*out = buf;
*len = (u64)n;
return 0;
}
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";
size_t i = 0;
if (sep_skip_space(src, n, &i) < 0
|| i + sizeof kw - 1 >= n
|| memcmp(src + i, kw, sizeof kw - 1) != 0)
return -1;
i += sizeof kw - 1;
if (i >= n || (src[i] != ' ' && src[i] != '\t'
&& src[i] != '\r' && src[i] != '\n'))
return -1;
if (sep_skip_space(src, n, &i) < 0 || i >= n
|| !sep_ident_start((unsigned char)src[i]))
return -1;
size_t begin = i++;
while (i < n && sep_ident_continue((unsigned char)src[i])) i++;
size_t len = i - begin;
if (len + 1 > namesz || sep_skip_space(src, n, &i) < 0
|| i >= n || src[i] != ';')
return -1;
memcpy(name, src + begin, len);
name[len] = '\0';
return 0;
const Node *x = *(Node *const *)a;
const Node *y = *(Node *const *)b;
const char *xp = x->usepath ? x->usepath : x->str;
const char *yp = y->usepath ? y->usepath : y->str;
int r = strcmp(xp, yp);
if (r != 0) return r;
if (x->pos.line != y->pos.line) return x->pos.line - y->pos.line;
return x->pos.col - y->pos.col;
}
/* 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;
import_add(filevisit, file);
FILE *in = fopen(file, "rb");
if (in == NULL) {
char *buf;
u64 len;
if (sep_slurp(file, &buf, &len) < 0) {
fprintf(stderr, "ww: cannot read %s\n", file);
return -1;
}
if (fseek(in, 0, SEEK_END) != 0) { fclose(in); return -1; }
long flen = ftell(in);
if (flen < 0 || fseek(in, 0, SEEK_SET) != 0) {
fclose(in);
return -1;
}
char *buf = malloc((size_t)flen + 1);
if (buf == NULL) { fclose(in); return -1; }
if (fread(buf, 1, (size_t)flen, in) != (size_t)flen) {
Arena *a = newarena();
Lex l;
Parser p;
lexinit(&l, a, file, buf, len);
parserinit(&p, a, &l);
Node *imports = parseimports(&p);
if (l.errs || p.errs) {
freearena(a);
free(buf);
return -1;
}
if (imports->module == NULL && owned_source) {
Pos pp = { file, 1, 1 };
errorf(pp, "invalid or missing package clause");
freearena(a);
free(buf);
fclose(in);
return -1;
}
buf[flen] = '\0';
fclose(in);
if (owned_source) {
char declared[256];
if (sep_package_clause(buf, (size_t)flen, declared,
sizeof declared) < 0) {
fprintf(stderr, "ww: %s: invalid or missing package clause\n",
file);
if (imports->module != NULL
&& (owned_source || g->pkg[pi].name[0] == '\0')) {
const char *declared = imports->module;
if (strlen(declared) >= sizeof g->pkg[pi].name) {
errorf(imports->pos, "package name is too long");
freearena(a);
free(buf);
return -1;
}
@@ -540,60 +534,97 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file,
if (pkg->name[0] == '\0')
snprintf(pkg->name, sizeof pkg->name, "%s", declared);
else if (strcmp(pkg->name, declared) != 0) {
fprintf(stderr,
"ww: %s: conflicting package names %s and %s\n",
pkg->entry, pkg->name, declared);
errorf(imports->pos,
"conflicting package names %s and %s in %s",
pkg->name, declared, pkg->entry);
freearena(a);
free(buf);
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;
for (size_t off = 0; off < (size_t)flen && rc == 0;) {
size_t end = off;
while (end < (size_t)flen && buf[end] != '\n') end++;
const char *p = buf + off;
const char *lineend = buf + end;
while (p < lineend && (*p == ' ' || *p == '\t')) p++;
if ((size_t)(lineend - p) < 7
|| (memcmp(p, "import ", 7) != 0
&& memcmp(p, "import\t", 7) != 0)) {
off = end < (size_t)flen ? end + 1 : end;
continue;
const char *previous = NULL;
for (int i = 0; i < nuse && rc == 0; i++) {
Node *u = uses[i];
const char *name = u->usepath ? u->usepath : u->str;
if (previous && strcmp(previous, name) == 0) continue;
previous = name;
if (strlen(name) >= sizeof g->pkg[0].path) {
errorf(u->pos, "import path is too long (limit %zu bytes)",
sizeof g->pkg[0].path - 1);
rc = -1;
break;
}
p += 7;
while (p < lineend && (*p == ' ' || *p == '\t')) p++;
char name[256] = {0};
int j = 0;
while (p < lineend && ((*p >= 'a' && *p <= 'z')
|| (*p >= 'A' && *p <= 'Z') || *p == '_' || *p == '.'
|| (*p >= '0' && *p <= '9')))
if (j + 1 < (int)sizeof name) name[j++] = *p++;
if (j == 0) {
off = end < (size_t)flen ? end + 1 : end;
continue;
}
char path_form[256];
char path_form[1024];
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];
int is_dir = 0;
/* #16 ENFORCE-driver: an unresolvable import is a hard error,
* not a silent skip — EXCEPT when the package is defined INLINE
* in the same unit (single-file multi-package; leaf = the last
* dotted component). E3-C1 (#87): the legacy amalgamator that
* used to own the genuine-missing case is gone, so the sep
* producer enforces it here (INV-2, by construction). */
if (!locate_import(searchpath, path_form, ipath, sizeof ipath,
&is_dir)) {
const char *dot = strrchr(name, '.');
const char *leaf = dot ? dot + 1 : name;
if (unit_has_package(file, leaf))
goto next_line; /* inline-satisfied */
fprintf(stderr, "ww: cannot find package %s\n", name);
int inline_package = 0;
for (Node *package = imports->body; package;
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;
goto next_line;
break;
}
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);
if (di < 0) { rc = -1; break; }
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) {
rc = -1; break;
}
next_line:
off = end < (size_t)flen ? end + 1 : end;
}
free(uses);
freearena(a);
free(buf);
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]);
free(fv.paths);
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
* g->n during the scan; iterate by index). */
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
* ==0). DIRECTORY imports are skipped (provided as `.wwi` ahead of the
* body); FILE imports fold in (intra-package split). */
static void
static int
sep_emit_body(FILE *out, const char *path, struct ImportSet *visited,
const char *searchpath, const char *modpath)
{
if (import_seen(visited, path)) return;
if (import_seen(visited, path)) return 0;
import_add(visited, path);
FILE *in = fopen(path, "rb");
if (in == NULL) {
char *buf;
u64 len;
if (sep_slurp(path, &buf, &len) < 0) {
fprintf(stderr, "ww: cannot read %s\n", path);
return;
return -1;
}
char line[2048];
while (fgets(line, sizeof line, in)) {
const char *p = line;
while (*p == ' ' || *p == '\t') p++;
if (strncmp(p, "import ", 7) != 0 && strncmp(p, "import\t", 7) != 0)
continue;
p += 7;
while (*p == ' ' || *p == '\t') p++;
char name[256] = {0};
int j = 0;
while ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z')
|| *p == '_' || *p == '.' || (*p >= '0' && *p <= '9'))
if (j + 1 < (int)sizeof name) name[j++] = *p++;
if (j == 0) continue;
char path_form[256];
Arena *a = newarena();
Lex l;
Parser p;
lexinit(&l, a, path, buf, len);
parserinit(&p, a, &l);
Node *imports = parseimports(&p);
if (l.errs || p.errs) {
freearena(a);
free(buf);
return -1;
}
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);
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);
char ipath[1024];
int is_dir = 0;
if (!locate_import(searchpath, path_form, ipath, sizeof ipath,
&is_dir))
continue;
if (!is_dir)
sep_emit_body(out, ipath, visited, searchpath, modpath);
if (!is_dir && sep_emit_body(out, ipath, visited, searchpath,
modpath) < 0) {
free(uses);
freearena(a);
free(buf);
return -1;
}
}
/* #57: tag the primary body by its full dotted import path so the
* definer mangles == the importer reference; a root build (path "")
* stays a bare reset (keeps bare main). */
if (modpath != NULL && modpath[0] != '\0')
fprintf(out, "//ww:module-reset %s\n", modpath);
else
fputs("//ww:module-reset\n", out);
rewind(in);
int ch;
while ((ch = fgetc(in)) != EOF) fputc(ch, out);
fputc('\n', out);
fclose(in);
int bad = 0;
if (modpath != NULL && modpath[0] != '\0') {
if (fprintf(out, "//ww:module-reset %s\n", modpath) < 0)
bad = 1;
} else if (fputs("//ww:module-reset\n", out) == EOF) {
bad = 1;
}
if (fwrite(buf, 1, (size_t)len, out) != (size_t)len
|| 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
@@ -787,25 +865,35 @@ sep_compose_unit(struct sepgraph *g, int pi, const char *scratch,
fclose(u);
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;
while ((ch = fgetc(wf)) != EOF) fputc(ch, u);
fputc('\n', u);
fclose(wf);
while (!bad && (ch = fgetc(wf)) != EOF)
if (fputc(ch, u) == EOF) bad = 1;
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};
int bodyrc = 0;
if (g->pkg[pi].is_dir) {
for (int i = 0; i < g->pkg[pi].nsources; i++)
sep_emit_body(u, g->pkg[pi].sources[i], &bodyvisit,
for (int i = 0; i < g->pkg[pi].nsources && bodyrc == 0; i++)
bodyrc = sep_emit_body(u, g->pkg[pi].sources[i], &bodyvisit,
searchpath, g->pkg[pi].path);
} 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);
}
for (int i = 0; i < bodyvisit.n; i++) free(bodyvisit.paths[i]);
free(bodyvisit.paths);
fclose(u);
return 0;
if (fclose(u) != 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
@@ -824,16 +912,18 @@ archive_o(const char *objpath, const char *apath)
fprintf(stderr, "ww: cannot read %s\n", objpath);
return -1;
}
fseek(in, 0, SEEK_END);
if (fseek(in, 0, SEEK_END) != 0) { fclose(in); return -1; }
long n = ftell(in);
fseek(in, 0, SEEK_SET);
if (n < 0) { fclose(in); return -1; }
if (n < 0 || fseek(in, 0, SEEK_SET) != 0) {
fclose(in);
return -1;
}
unsigned char *buf = malloc((size_t)n);
if (buf == NULL) { fclose(in); return -1; }
if (fread(buf, 1, (size_t)n, in) != (size_t)n) {
free(buf); fclose(in); return -1;
}
fclose(in);
if (fclose(in) != 0) { free(buf); return -1; }
FILE *out = fopen(apath, "wb");
if (out == NULL) {
@@ -841,7 +931,7 @@ archive_o(const char *objpath, const char *apath)
free(buf);
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
* address fields in that serialized header. */
char hdr[60];
@@ -853,14 +943,20 @@ archive_o(const char *objpath, const char *apath)
memcpy(hdr + 40, "100644", 6); /* mode (fixed octal) */
char sz[12];
int szn = snprintf(sz, sizeof sz, "%lu", (unsigned long)n);
memcpy(hdr + 48, sz, (size_t)szn);
if (szn <= 0 || szn > 10) bad = 1;
else memcpy(hdr + 48, sz, (size_t)szn);
hdr[58] = 0x60; /* member-header magic byte */
hdr[59] = 0x0a;
fwrite(hdr, 1, sizeof hdr, out);
fwrite(buf, 1, (size_t)n, out);
if (n & 1) fputc('\n', out); /* members are 2-byte aligned */
fclose(out);
if (fwrite(hdr, 1, sizeof hdr, out) != sizeof hdr
|| fwrite(buf, 1, (size_t)n, out) != (size_t)n)
bad = 1;
if ((n & 1) && fputc('\n', out) == EOF) bad = 1;
if (fclose(out) != 0) bad = 1;
free(buf);
if (bad) {
fprintf(stderr, "ww: cannot write archive %s\n", apath);
return -1;
}
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
* `-w` workdir with content-identity package reuse. */
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 *extra_libdirs, int is_test, int emit_asm, const char *workdir,
char *scratchout, size_t scratchoutsz, struct sepgraph **graphout)
const char *extra_libdirs, int package_only, int is_test, int emit_asm,
const char *workdir, char *scratchout, size_t scratchoutsz,
struct sepgraph **graphout)
{
const char *c6 = toolpath("WW_W6C", "w6c");
const char *a6 = toolpath("WW_W6A", "w6a");
@@ -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);
if (g == NULL) return 1;
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;
/* #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
@@ -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;
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;
int *order = calloc((size_t)g->n, sizeof *order);
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);
if (!root_package) g->pkg[root].path[0] = '\0';
for (int oi = 0; oi < norder; 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 *co = warm ? objnew : obj;
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,
cu) < 0) { free(order); return 1; }
if (warm && !stale_all && file_equal(unitnew, unitf)
&& file_is_reg(asmf)
&& (pi == root || file_is_reg(wwi))
&& (!needs_export || file_is_reg(wwi))
&& (emit_asm || (file_size_nonzero(obj)
&& (pi == root || file_size_nonzero(apath))))) {
&& (!needs_archive || file_size_nonzero(apath))))) {
if (unlink(unitnew) != 0) {
fprintf(stderr, "ww: cannot remove %s\n",
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
* export-check rejects. Skip -I for the root; its `.wwi`
* is never consumed. */
if (pi == root)
if (!needs_export)
/* #79: the root carries -T under `ww test`
* so w6c synthesizes the test main. Deps never
* 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
* before any archive is processed. The link consumes `.o`/`.a`,
* never `.wwi`. */
if (!emit_asm && pi != root) {
if (!emit_asm && needs_archive) {
if (archive_o(co, ca) != 0) {
fprintf(stderr, "ww: archive failed for %s\n",
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
* them, unit strictly last. */
if (warm) {
if ((pi != root && rename(wwinew, wwi) != 0)
if ((needs_export && rename(wwinew, wwi) != 0)
|| rename(asmnew, asmf) != 0
|| (!emit_asm && rename(objnew, obj) != 0)
|| (!emit_asm && pi != root
|| (!emit_asm && needs_archive
&& rename(anew, apath) != 0)
|| rename(unitnew, unitf) != 0) {
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 (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),
* 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
* this invocation successfully created the exact `.sepwork` tree. */
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 *extra_libdirs, int is_test, int emit_asm, int keepscratch,
const char *workdir)
const char *extra_libdirs, int package_only, int is_test, int emit_asm,
int keepscratch, const char *workdir)
{
char scratch[1100] = {0};
struct sepgraph *g = NULL;
int r = build_one_sep_impl(src, entry_is_dir, out, objstem,
extra_includes, extra_libs, extra_libdirs, is_test, emit_asm,
int r = build_one_sep_impl(src, entry_is_dir, root_identity, out, objstem,
extra_includes, extra_libs, extra_libdirs, package_only, is_test, emit_asm,
workdir, scratch, sizeof scratch, &g);
sep_graph_free(g);
if (!keepscratch && scratch[0]) {
@@ -1371,10 +1494,11 @@ parse_build_flags(const char *cmd, int argc, char **argv,
char *libs, size_t libsz,
char *outpath, size_t outsz,
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;
if (emit_asm_out) *emit_asm_out = 0;
if (package_out) *package_out = 0;
int i = 0;
for (; i < argc; i++) {
if (strcmp(argv[i], "-S") == 0) {
@@ -1383,6 +1507,12 @@ parse_build_flags(const char *cmd, int argc, char **argv,
return -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) {
if (workdir == NULL) {
fprintf(stderr, "ww %s: unknown flag\n", cmd);
@@ -1470,18 +1600,29 @@ do_build(int argc, char **argv)
char outflag[1024] = {0};
char workdir[1024] = {0};
int emit_asm = 0;
int package_only = 0;
if (parse_build_flags("build", argc, argv, incs, sizeof incs,
libdirs, sizeof libdirs, libs, sizeof libs,
outflag, sizeof outflag, workdir, sizeof workdir,
&src, &emit_asm) < 0)
&src, &emit_asm, &package_only) < 0)
return 2;
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];
int is_dir = 0;
if (!resolve_module(src, incs, resolved, sizeof resolved, &is_dir)) {
fprintf(stderr, "ww build: cannot find module %s\n", src);
return 1;
}
if (package_only && !is_dir) {
fprintf(stderr, "ww build: -p needs a package directory\n");
return 2;
}
char out[1024];
const char *objstem = NULL;
if (outflag[0]) {
@@ -1499,8 +1640,9 @@ do_build(int argc, char **argv)
} else {
basename_no_ext(resolved, out, sizeof out);
}
return build_one_sep(resolved, is_dir, out, objstem, incs, libs,
libdirs, 0, emit_asm, 1, workdir);
const char *root_identity = package_only && !literal ? src : NULL;
return build_one_sep(resolved, is_dir, root_identity, out, objstem, incs, libs,
libdirs, package_only, 0, emit_asm, 1, workdir);
}
static int
@@ -1513,7 +1655,7 @@ do_run(int argc, char **argv)
char outflag[1024] = {0}; /* -o accepted+ignored: run always uses the temp */
int next = parse_build_flags("run", argc, argv, incs, sizeof incs,
libdirs, sizeof libdirs, libs, sizeof libs,
outflag, sizeof outflag, NULL, 0, &src, NULL);
outflag, sizeof outflag, NULL, 0, &src, NULL, NULL);
if (next < 0) return 2;
if (src == NULL) src = ".";
char resolved[1024];
@@ -1531,8 +1673,8 @@ do_run(int argc, char **argv)
snprintf(tmp, sizeof tmp, "%s/main", tmpdir);
/* The freshly acquired directory owns both the executable and the
* adjacent main.sepwork tree. Nothing outside it is adopted or removed. */
if (build_one_sep(resolved, is_dir, tmp, tmp, incs, libs, libdirs, 0, 0,
0, NULL) != 0) {
if (build_one_sep(resolved, is_dir, NULL, tmp, tmp, incs, libs, libdirs,
0, 0, 0, 0, NULL) != 0) {
if (unlink(tmp) != 0 && errno != ENOENT)
fputs("ww: cannot remove temporary output\n", stderr);
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
* source. An explicit -o names the caller-owned artifact stem. */
int br = build_one_sep(resolved, is_dir, outp,
outstem[0] ? outstem : tmp, incs, "", "", 1,
int br = build_one_sep(resolved, is_dir, NULL, outp,
outstem[0] ? outstem : tmp, incs, "", "", 0, 1,
emit_asm, outstem[0] ? 1 : 0, workdir);
if (br != 0) {
if (owntmp && unlink(outp) != 0 && errno != ENOENT)
@@ -1811,8 +1953,8 @@ do_test(int argc, char **argv)
outp = tmp;
}
/* See module-mode note: no-o scratch is redirected to /tmp. */
int br = build_one_sep(target, 0, outp, outstem[0] ? outstem : tmp,
incs, "", "", 1, emit_asm, outstem[0] ? 1 : 0, workdir);
int br = build_one_sep(target, 0, NULL, outp, outstem[0] ? outstem : tmp,
incs, "", "", 0, 1, emit_asm, outstem[0] ? 1 : 0, workdir);
if (br != 0) {
if (owntmp && unlink(outp) != 0 && errno != ENOENT)
fputs("ww: cannot remove temporary output\n", stderr);

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,
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
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);
};
// 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.
// Mirrors Hare's sys::lstat (ref/hare/sys/+linux/stat.ha:57).
export fn lstat(out: *filestat, path: str) (void | oserror) = {

View File

@@ -17,6 +17,7 @@ import os;
import os.exec;
import rt;
import strings;
import syntax;
// 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
@@ -33,6 +34,25 @@ fn cerr(m: str) void = {
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 = {
let n: u64 = 0u64;
for (p[n] != 0u8) { n += 1u64; };
@@ -386,6 +406,22 @@ fn dirfileclass(dirpath: *u8, name: *u8, nlen: u64) i32 = {
s.len = nlen: i32;
if (!strings.hassuffix(s, ".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; };
return 1;
};
@@ -438,13 +474,17 @@ fn enumeratedir(dirpath: *u8) (**u8, i32) = {
let nm: *u8 = buf.ptr + off + 19u64;
let nl: u64 = cstrlen(nm);
let cls: i32 = dirfileclass(dirpath, nm, nl);
if (cls < 0) {
if (cls == -1) {
cerr("ww: ");
cerr(pathstr(joinpath(dirpath, nm)));
cerr(": @test declaration outside *_test.ww\n");
os.close(fd);
return nil: **u8, -2;
};
if (cls == -2) {
os.close(fd);
return nil: **u8, -2;
};
if (cls > 0) {
if (n >= cap) {
let ncap: i32 = cap * 2;
@@ -527,59 +567,17 @@ fn slurp(pathcs: *u8) (*u8, u64) = {
let buf: []u8 = alloc([], nu + 1u64)!;
buf.len = (nu + 1u64): i32;
let rr: (i64 | os.oserror) = os.readall(fd, buf.ptr, nu);
os.close(fd);
let closed: i32 = os.close(fd);
let got: i64 = 0i64;
match (rr) {
case let v: i64 => got = v;
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;
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 = {
let n: u64 = cstrlen(src);
let stop: u64 = n;
@@ -632,8 +630,10 @@ type lflags = struct {
// 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
// name a transitive dep's type, so the consuming unit needs the whole
// closure for resolution. The unit composition is byte-identical to the
// cstage driver (rule 10) so w6c/w6c_ww emit identical `.s`.
// closure for resolution. Qualifier lookup remains scoped to the direct
// 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;
@@ -655,9 +655,30 @@ type sepgraph = struct {
};
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;
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;
};
if (g.n >= SEP_MAXPKG) {
@@ -723,140 +744,6 @@ fn sepfname(g: *sepgraph, pi: i32, scratch: *u8, suffix: str) *u8 = {
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
// (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
@@ -877,44 +764,109 @@ fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8,
cerr("ww: cannot read source\n");
return -1;
};
if (ownedsource != 0) {
let declared: *u8 = nil;
let declaredn: u64 = 0u64;
if (!seppackageclause(bufp, blen, &declared, &declaredn)) {
cerr("ww: ");
cerr(pathstr(file));
cerr(": invalid or missing package clause\n");
return -1;
};
let l: syntax.lex;
syntax.lexinit(&l, fdup, bufp, blen);
let ps: syntax.parser;
syntax.parserinit(&ps, &l);
let imports: *syntax.node = syntax.parseimports(&ps);
if (l.errs > 0 || ps.errs > 0) { return -1; };
if (imports.nmod.len == 0 && ownedsource != 0) {
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) {
g.pkg[pi].name = arenadupcstr(declared, declaredn);
} else { if (bytecmp(g.pkg[pi].name, cstrlen(g.pkg[pi].name),
declared, declaredn) != 0) {
cerr("ww: ");
cerr(pathstr(g.pkg[pi].entry));
cerr(": conflicting package names ");
cerrpos(imports.file, imports.line, imports.col);
cerr(": error: conflicting package names ");
cerr(pathstr(g.pkg[pi].name));
cerr(" and ");
os.write(2, declared, declaredn);
cerr("\n");
cerr(" in "); cerr(pathstr(g.pkg[pi].entry)); cerr("\n");
return -1;
}; };
};
let i: u64 = 0u64;
for (i < blen) {
let j: u64 = i;
for (j < blen) {
if (bufp[j] == 10u8) { break; }; // '\n'
j += 1u64;
if (ownedsource != 0 && g.pkg[pi].isdir != 0) {
let pm: *syntax.node = imports.body;
for (pm != nil) {
if (!syntax.streq(pm.nmod, pathstr(g.pkg[pi].name))) {
cerrpos(pm.file, pm.line, pm.col);
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;
idp, idn = scanuse(bufp + i, j - i);
if (idp != nil) {
};
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;
};
};
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 ipath: *u8 = locateimport(searchpath, idp, idn, &isdir);
if (ipath != nil) {
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 k: u64 = 0u64;
for (k < idn) { nm[k] = idp[k]; k += 1u64; };
@@ -938,14 +890,6 @@ fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8,
};
};
} 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 lk: u64 = 0u64;
for (lk < idn) {
@@ -954,15 +898,23 @@ fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8,
};
let leafp: *u8 = idp + lstart;
let leafn: u64 = idn - lstart;
if (!unithaspackage(bufp, blen, leafp, leafn)) {
cerr("ww: cannot find package ");
let inlinepackage: bool = false;
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);
cerr("\n");
return -1;
};
};
};
i = j + 1u64;
ui += 1;
};
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);
};
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;
for (k < g.pkg[pi].ndeps) {
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
// ==0). DIRECTORY imports are skipped (provided as `.wwi` ahead);
// 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;
pview.ptr = path;
pview.len = cstrlen(path): i32;
let pdup: str = strings.dup(pview);
if (visitseen(visit, pdup)) { return; };
if (visitseen(visit, pdup)) { return 0; };
visitadd(visit, pdup);
let bufp: *u8;
let blen: u64;
bufp, blen = slurp(path);
if (bufp == nil) {
cerr("ww: cannot read source\n");
return;
return -1;
};
let i: u64 = 0u64;
for (i < blen) {
let j: u64 = i;
for (j < blen) {
if (bufp[j] == 10u8) { break; }; // '\n'
j += 1u64;
let l: syntax.lex;
syntax.lexinit(&l, pdup, bufp, blen);
let ps: syntax.parser;
syntax.parserinit(&ps, &l);
let imports: *syntax.node = syntax.parseimports(&ps);
if (l.errs > 0 || ps.errs > 0) { return -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;
let idn: u64;
idp, idn = scanuse(bufp + i, j - i);
if (idp != nil) {
si += 1;
};
ui = 0;
for (ui < nuse) {
u = uses[ui];
let idp: *u8 = u.usepath.ptr;
let idn: u64 = u.usepath.len: u64;
let isdir: i32 = 0;
let ipath: *u8 = locateimport(searchpath, idp, idn, &isdir);
if (ipath != nil) {
if (isdir == 0) {
sepemitbody(fd, ipath, visit, searchpath, modpath);
if (sepemitbody(fd, ipath, visit, searchpath,
modpath) < 0) { return -1; };
};
};
};
i = j + 1u64;
ui += 1;
};
// #57: tag the primary body by its full dotted import path so the
// definer mangles == the importer reference; a root build (path "")
// stays a bare reset (keeps bare main).
if (modpath != nil && modpath[0u64] != 0u8) {
let dm: str = "//ww:module-reset ";
os.writeall(fd, dm.ptr, dm.len: u64);
os.writeall(fd, modpath, cstrlen(modpath));
os.writeall(fd, "\n".ptr, 1u64);
if (!sepwriteall(fd, dm.ptr, dm.len: u64)
|| !sepwriteall(fd, modpath, cstrlen(modpath))
|| !sepwriteall(fd, "\n".ptr, 1u64)) {
cerr("ww: cannot write package unit\n");
return -1;
};
} else {
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);
os.writeall(fd, "\n".ptr, 1u64);
if (!sepwriteall(fd, bufp, blen)
|| !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
@@ -1175,11 +1196,16 @@ fn sepcomposeunit(g: *sepgraph, pi: i32, scratch: *u8, order: []i32,
return -1;
};
let dm: str = "//ww:module ";
os.writeall(u, dm.ptr, dm.len: u64);
os.writeall(u, g.pkg[dj].path, cstrlen(g.pkg[dj].path));
os.writeall(u, "\n".ptr, 1u64);
os.writeall(u, wb, wn);
os.writeall(u, "\n".ptr, 1u64);
if (!sepwriteall(u, dm.ptr, dm.len: u64)
|| !sepwriteall(u, g.pkg[dj].path,
cstrlen(g.pkg[dj].path))
|| !sepwriteall(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;
@@ -1188,18 +1214,23 @@ fn sepcomposeunit(g: *sepgraph, pi: i32, scratch: *u8, order: []i32,
bv.out = u;
bv.dirs = searchpath;
bv.visit = nil;
let bodyrc: i32 = 0;
if (g.pkg[pi].isdir != 0) {
let i: i32 = 0;
for (i < g.pkg[pi].nsources) {
sepemitbody(u, g.pkg[pi].sources[i], &bv, searchpath,
for (i < g.pkg[pi].nsources && bodyrc == 0) {
bodyrc = sepemitbody(u, g.pkg[pi].sources[i], &bv, searchpath,
g.pkg[pi].path);
i += 1;
};
} 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);
return 0;
if (os.close(u) != 0) {
cerr("ww: cannot close package unit\n");
return -1;
};
return bodyrc;
};
// 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");
return -1;
};
os.writeall(fd, out, total);
os.close(fd);
let bad: bool = !sepwriteall(fd, out, total);
if (os.close(fd) != 0) { bad = true; };
if (bad) {
cerr("ww: cannot write archive\n");
return -1;
};
return 0;
};
@@ -1375,12 +1410,14 @@ fn copyfileatomic(src: *u8, dst: *u8) i32 = {
else { if (n == 0) { done = true; }
else {
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; };
};
}; };
};
os.close(in);
if (os.close(in) != 0) { bad = true; };
if (os.close(out) != 0) { bad = true; };
if (bad) { return -1; };
return os.rename(pathstr(tmpp), pathstr(dst));
@@ -1427,7 +1464,7 @@ fn writestampatomic(path: *u8, want: str) i32 = {
if (fd < 0) { return -1; };
let bad: bool = false;
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; };
};
if (os.close(fd) != 0) { bad = true; };
@@ -1444,9 +1481,11 @@ fn cerrpath(head: str, path: *u8, tail: str) void = {
cerr(tail);
};
fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
objstem: *u8, incs: *u8, lf: *lflags, istest: i32, emitasm: i32,
workdir: *u8, scratchout: **u8, graphout: **sepgraph) i32 = {
fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
rootidentity: *u8, out: *u8,
objstem: *u8, incs: *u8, lf: *lflags, packageonly: i32, istest: i32,
emitasm: i32, workdir: *u8, scratchout: **u8,
graphout: **sepgraph) i32 = {
let c6: *u8 = joinpathlit(selfdir, "w6c_ww");
let a6: *u8 = joinpathlit(selfdir, "w6a_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;
let g: *sepgraph = alloc(sepgraph{pkg = pkgslot, n = 0})!;
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; };
// #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
@@ -1616,6 +1657,11 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
};
};
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;
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;
let norder: i32 = 0;
if (septopovisit(g, root, order, &norder, stack, 0) < 0) { return 1; };
if (!rootpackage) { g.pkg[root].path = "\0".ptr; };
let oi: i32 = 0;
for (oi < norder) {
@@ -1650,6 +1697,8 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
cu = unitnew; cw = wwinew; cs = asmnew;
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) {
return 1;
};
@@ -1659,7 +1708,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
fresh = fileequal(unitnew, unitf);
if (fresh) { fresh = fileisreg(asmf); };
if (fresh) {
if (pi != root) {
if (needsexport) {
fresh = fileisreg(wwi);
};
};
@@ -1669,7 +1718,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
};
};
if (fresh) {
if (emitasm == 0 && pi != root) {
if (emitasm == 0 && needsarchive) {
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.
let roott: bool = (pi == root) && (istest != 0);
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)!;
append(argv, "w6c");
if (roott) { append(argv, "-T"); };
append(argv, "-c");
if (pi != root) {
if (needsexport) {
append(argv, "-I");
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
// processed. The link
// consumes `.o`/`.a`, never `.wwi`.
if (emitasm == 0 && pi != root) {
if (emitasm == 0 && needsarchive) {
if (archiveo(co, ca) != 0) {
cerr("ww: archive failed\n");
return 1;
@@ -1754,7 +1803,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
// them, unit strictly last.
if (warm) {
let bad: bool = false;
if (pi != root) {
if (needsexport) {
if (os.rename(pathstr(wwinew), pathstr(wwi)) != 0) {
bad = true;
};
@@ -1772,7 +1821,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
};
};
if (!bad) {
if (emitasm == 0 && pi != root) {
if (emitasm == 0 && needsarchive) {
if (os.rename(pathstr(anew), pathstr(apath)) != 0) {
bad = true;
};
@@ -1821,6 +1870,17 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
};
};
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`
// 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.
// The path is non-nil only after this invocation successfully created the
// exact tree. Twin of the cstage build_one_sep wrapper.
fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
objstem: *u8, incs: *u8, lf: *lflags, istest: i32, emitasm: i32,
keepscratch: i32, workdir: *u8) i32 = {
fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32,
rootidentity: *u8, out: *u8,
objstem: *u8, incs: *u8, lf: *lflags, packageonly: i32, istest: i32,
emitasm: i32, keepscratch: i32, workdir: *u8) i32 = {
let scratch: *u8 = nil;
let g: *sepgraph = nil;
let r: i32 = buildonesepimpl(selfdir, src, entryisdir, out, objstem,
incs, lf, istest, emitasm, workdir, &scratch, &g);
let r: i32 = buildonesepimpl(selfdir, src, entryisdir, rootidentity,
out, objstem,
incs, lf, packageonly, istest, emitasm, workdir, &scratch, &g);
sepgraphfree(g);
if (keepscratch == 0 && scratch != nil) {
if (cstrendswithlit(scratch, ".sepwork")) {
@@ -2001,7 +2063,7 @@ fn resolvemodule(selfdir: *u8, name: *u8, incs: *u8, isdir: *i32) *u8 = {
};
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);
};
@@ -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 workdir: *u8 = nil; // -w persistent package-artifact workdir
let emitasm: i32 = 0;
let packageonly: i32 = 0;
let incs: []u8 = alloc([], (os.PATH_MAX: u64) * 2u64)!;
incs.len = ((os.PATH_MAX: u64) * 2u64): i32;
let incoff: u64 = 0u64;
@@ -2064,6 +2127,8 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
if (p[0u64] == 45u8) { // '-'
if (cstreqlit(p, "-S")) {
emitasm = 1;
} else { if (cstreqlit(p, "-p")) {
packageonly = 1;
} else { if (p[1u64] == 73u8) { // '-I'
let dir: *u8 = nil;
if (p[2u64] != 0u8) {
@@ -2143,7 +2208,7 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
} else {
cerr("ww build: unknown flag\n");
return 2;
}; }; }; }; }; };
}; }; }; }; }; }; };
} else {
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];
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 resolved: *u8 = resolvemodule(selfdir, src, incs.ptr, &isdir);
if (resolved == nil) {
cerr("ww build: cannot find module\n");
return 1;
};
if (packageonly != 0 && isdir == 0) {
cerr("ww build: -p needs a package directory\n");
return 2;
};
let out: *u8 = nil;
let objstem: *u8 = nil;
if (outflag != nil) {
@@ -2189,8 +2268,11 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
lf.nlibdirs = nlibdirs;
lf.libs = libs.ptr;
lf.nlibs = nlibs;
return buildonesep(selfdir, resolved, isdir, out, objstem, incs.ptr, &lf,
0i32, emitasm, 1i32, workdir);
let rootidentity: *u8 = nil;
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
@@ -2354,8 +2436,8 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
lf.libs = libs.ptr;
lf.nlibs = nlibs;
// The freshly acquired directory owns both main and main.sepwork.
if (buildonesep(selfdir, resolved, isdir, outp, outp, incs.ptr, &lf,
0i32, 0i32, 0i32, nil) != 0) {
if (buildonesep(selfdir, resolved, isdir, nil, outp, outp, incs.ptr, &lf,
0i32, 0i32, 0i32, 0i32, nil) != 0) {
let cleanrc: i32 = os.remove(pathstr(outp));
if (cleanrc != 0 && cleanrc != -2i32) {
cerr("ww: cannot remove temporary output\n");
@@ -2444,8 +2526,8 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32,
lf.nlibs = 0;
let keep: i32 = 0;
if (outstem != nil) { keep = 1; };
let bres: i32 = buildonesep(selfdir, src, 0, outp, objstem, incs, &lf,
1i32, emitasm, keep, workdir);
let bres: i32 = buildonesep(selfdir, src, 0, nil, outp, objstem, incs, &lf,
0i32, 1i32, emitasm, keep, workdir);
if (bres != 0) {
if (owntmp) {
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);
};