From 52f6d6f0d4676eb418756303d8f66172656bd846 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 11 Aug 2026 22:26:07 +0900 Subject: [PATCH] cmd: build local package graphs --- Makefile | 8 +- cmd/ww/main.c | 606 +++++++++++++++++++++------------- docs/build-system.md | 53 +++ lib/os/os.ww | 21 ++ selfhost/cmd/ww/main.ww | 636 ++++++++++++++++++++---------------- test/sep/localbuild_test.ww | 502 ++++++++++++++++++++++++++++ 6 files changed, 1316 insertions(+), 510 deletions(-) create mode 100644 test/sep/localbuild_test.ww diff --git a/Makefile b/Makefile index 982ed7e7..b83ad644 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/cmd/ww/main.c b/cmd/ww/main.c index f1a6f781..55903380 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -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 #include @@ -17,7 +18,7 @@ static const char *usage = "usage: ww [-V] [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 .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 ;` 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("!\n", 1, 8, out); + int bad = fwrite("!\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 `.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); diff --git a/docs/build-system.md b/docs/build-system.md index f1e26c45..fdfb38cd 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -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. diff --git a/lib/os/os.ww b/lib/os/os.ww index 73e9feda..cf0983e4 100644 --- a/lib/os/os.ww +++ b/lib/os/os.ww @@ -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) = { diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index c1400d50..7b192b4f 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -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 ;` 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 ` -> 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] [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 .ww\n"; + let s: str = "usage: ww [-V] [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 .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/ 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)); diff --git a/test/sep/localbuild_test.ww b/test/sep/localbuild_test.ww new file mode 100644 index 00000000..30f3ecc8 --- /dev/null +++ b/test/sep/localbuild_test.ww @@ -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); +};