wcc,ww: flip driver to separate-compilation only; delete build_one amalgamator (M4 E3-C1, #22)

build_one_sep (per-package compile + .wwi interfaces + link) becomes the
sole build path. do_build/do_run/do_test and the ww twins all route
through it; --sep is now an accepted no-op and the run-rejects-sep guard
is removed.

Deleted the single-file amalgamator, both stages: build_one, expand,
expand_dir, peek_package (+ the wwstage twins + strictpkgmismatch).
unit_has_package is retained -- the sep scan loop's inline-package check
needs it. The sep-shared helpers (enumerate_dir_ww, locate_import*,
import_path_form, ImportSet, and ww counterparts) stay; they back the
surviving sep path.

Restores missing-package enforcement under sep by construction: the sep
scan loop loudly rejects an unresolvable import (cannot find package
<name>) unless the package is defined inline in the same unit -- matching
the deleted amalgamator and closing the silent-accept the flip would
otherwise introduce.

All 5 wwstage tools relink (each is built via the now-sep `ww build`);
emitted asm is byte-identical to the combined build per bootstrap input,
so the binary md5 delta is pure link layout, not codegen.

selfhost/cmd/ww/main.combined.ww is now stale and unregenerable (its
writer build_one is deleted); #90 deletes it next.

Test retargets folded in (rule-11 carve-out, #61/#133 precedent): each
asserts post-flip-only behavior, is un-pre-migratable unlike #93/#94/#103,
and splitting reddens one side. Closes #97.
- 989_slttypepref -> dir-package layout (xb imports xa so both same-leaf
  `invalid` types are in scope at xb.f); inline-multipackage was the
  amalgamator shape, deleted with the flip.
- 989_sepbuild_run -> run --sep now genuinely runs (exit 7), not the old
  loud-reject (exit 2); + a private per-pid WW_PKGCACHE so the cs/ww
  per-package byte-id compare on the shared real lib pkgs (rt/time/os) no
  longer races concurrent siblings on the global out/.pkgcache (the flip
  made sep the sole path, so every test now contends that cache).
- 737_direnum -> the deleted strictpkgmismatch "differs from" wording ->
  sep's "does not match import path" (shared substring, wwstage terser #68).
- 989_lib_byteid -> corpus-completeness scan excludes generated .sepwork
  scratch (the old `! -name '*.combined.ww'` exclude didn't cover the new
  sep artifact).
This commit is contained in:
2026-06-18 02:51:31 +09:00
parent f98033293e
commit 33edc386f1
6 changed files with 275 additions and 1047 deletions

View File

@@ -236,366 +236,13 @@ enumerate_dir_ww(const char *dirpath, char ***out_files)
return n;
}
static void expand(FILE *out, const char *path, struct ImportSet *visited,
const char *libdir, const char *modpath);
/* Scan `path` for its first non-comment-non-blank line; if it starts
* with `package <name>;` write the name into `out` (NUL-terminated)
* and return 1, else 0. Strict-same-package enforcement (task #23
* subset) is bundled here because the failure mode is dir-enum's
* own — a non-dir-enum compilation unit cannot trigger it. Hare's
* hare/module/srcs.ha:131 has the same constraint via the README
* gate; we encode it as same-package across all enumerated files. */
static int
peek_package(const char *path, char *out, size_t outsz)
{
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 == '\n' || *p == '\0') continue;
if (p[0] == '/' && p[1] == '/') continue;
if (strncmp(p, "package ", 8) != 0
&& strncmp(p, "package\t", 8) != 0) break;
p += 8;
while (*p == ' ' || *p == '\t') p++;
size_t i = 0;
while (i + 1 < outsz && ((p[i] >= 'a' && p[i] <= 'z')
|| (p[i] >= 'A' && p[i] <= 'Z')
|| p[i] == '_' || (p[i] >= '0' && p[i] <= '9')))
out[i] = p[i], i++;
out[i] = '\0';
found = (i > 0);
break;
}
fclose(in);
return found;
}
/* 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
* peek_package (stops at the FIRST package decl), this scans every line
* — single-file multi-package fixtures carry several `package` decls. The
* comment-skip line scan + name match mirror peek_package, uncapped. The
* wwstage twin unithaspackage must stay byte-identical (rule 10). */
static int
unit_has_package(const char *path, const char *leaf)
{
FILE *in = fopen(path, "rb");
if (in == NULL) return 0;
char line[2048];
int found = 0;
while (fgets(line, sizeof line, in)) {
const char *p = line;
while (*p == ' ' || *p == '\t') p++;
if (p[0] == '/' && p[1] == '/') continue;
if (strncmp(p, "package ", 8) != 0
&& strncmp(p, "package\t", 8) != 0) continue;
p += 8;
while (*p == ' ' || *p == '\t') p++;
size_t i = 0;
while (leaf[i] != '\0' && leaf[i] == p[i]) i++;
if (leaf[i] == '\0') {
char c = p[i];
if (c == ';' || c == ' ' || c == '\t'
|| c == '\n' || c == '\0') { found = 1; break; }
}
}
fclose(in);
return found;
}
/* expand_dir — enumerate <dirpath>/*.ww (skip *test.ww), byte-sort,
* recurse into each. Mirrors ref/hare/hare/module/srcs.ha:183
* `_findsrcs` minus tag handling. The visited set still keys on
* concrete file paths so multi-file modules are pulled once.
* Strict-same-package: all enumerated files must declare the same
* `package <name>;` (task #23 subset; failure mode native to
* dir-enum). */
static void
expand_dir(FILE *out, const char *dirpath, struct ImportSet *visited,
const char *libdir, const char *modpath)
{
char **files = NULL;
int n = enumerate_dir_ww(dirpath, &files);
char dirpkg[128] = {0};
for (int i = 0; i < n; i++) {
char fp[1024];
snprintf(fp, sizeof fp, "%s/%s", dirpath, files[i]);
char pkg[128];
if (peek_package(fp, pkg, sizeof pkg)) {
if (dirpkg[0] == '\0') {
snprintf(dirpkg, sizeof dirpkg, "%s", pkg);
} else if (strcmp(dirpkg, pkg) != 0) {
fprintf(stderr,
"ww: %s: package %s differs from %s in same module dir %s\n",
fp, pkg, dirpkg, dirpath);
exit(1);
}
}
expand(out, fp, visited, libdir, modpath);
free(files[i]);
}
free(files);
}
/* Recursively expand `path`: for each top-level `import IDENT;` we
* find, resolve the import and expand it first, then append our own
* bytes. Already-visited paths are skipped. Each source carries its
* own `package <name>;` declaration (the parser stamps decls from
* it). */
static void
expand(FILE *out, const char *path, struct ImportSet *visited,
const char *libdir, const char *modpath)
{
if (import_seen(visited, path)) return;
import_add(visited, path);
FILE *in = fopen(path, "rb");
if (in == NULL) {
fprintf(stderr, "ww: cannot read %s\n", path);
return;
}
char line[2048];
while (fgets(line, sizeof line, in)) {
const char *p = line;
while (*p == ' ' || *p == '\t') p++;
if (strncmp(p, "import ", 7) != 0 && strncmp(p, "import\t", 7) != 0)
continue;
p += 7;
while (*p == ' ' || *p == '\t') p++;
char name[256] = {0};
int j = 0;
while ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z')
|| *p == '_' || *p == '.' || (*p >= '0' && *p <= '9'))
if (j + 1 < (int)sizeof name) name[j++] = *p++;
if (j == 0) continue;
char path_form[256];
import_path_form(name, path_form, sizeof path_form);
char ipath[1024];
int is_dir = 0;
/* #16 ENFORCE-driver (rob A): an unresolvable import is a hard
* error, not a silent skip — the old `continue` let a typo'd/
* missing package drop its symbols and surface later as a
* confusing downstream failure. BUT a locate-miss is legal when
* the package is defined INLINE in the same unit (single-file
* multi-package: `package aa; ... package main; import aa;`) —
* the bundler can't pull it as a file but the checker binds it.
* So: miss + inline `package <leaf>` present -> silent skip;
* miss + not inline -> fatal. */
if (!locate_import(libdir, path_form, ipath, sizeof ipath,
&is_dir)) {
const char *dot = strrchr(name, '.');
const char *leaf = dot ? dot + 1 : name;
if (unit_has_package(path, leaf))
continue; /* inline-satisfied */
fprintf(stderr, "ww: cannot find package %s\n", name);
exit(1);
}
/* M1 #22 (isdir-gated, rob-ratified): a package IS a directory, so
* only DIRECTORY imports are package boundaries that path-mangle.
* A single-file import (`import opcodes;` → opcodes.ww declaring
* `package w6a`) is an intra-package file-split — it keeps its
* in-file `package` clause as its module (no directive). */
if (is_dir) expand_dir(out, ipath, visited, libdir, name);
else expand(out, ipath, visited, libdir, NULL);
}
/* #16 option-B: a package-less file's decls would otherwise inherit
* the preceding bundled module's sticky curmod (parser parse.c). Emit
* a curmod-reset boundary directive so the lexer/parser attribute the
* file to the primary module ("") — fixes the self-import false-fire
* and the leaked-prefix bug, codegen-neutral (bare symbols kept; not
* `package main`, which would main-prefix them). A packaged file's own
* `package` decl already sets curmod, so it needs nothing — keeping
* the directive out of every tracked combined.ww. (Task #11.) */
if (modpath != NULL && modpath[0] != '\0') {
/* M1 (#22): an import-reached file carries its full dotted
* import path so codegen mangles symbols on the path, not the
* leaf `package` clause. The directive's absence is the root
* marker (#32): root/primary files take the branch below. */
fprintf(out, "//ww:module %s\n", modpath);
} else {
/* Root/primary file: reset the bundle boundary so a preceding
* imported section's sticky pathmod (M1 #22) is cleared. A
* package-less file then stays primary ("") as before; a
* packaged primary's own `package` clause sets curmod fresh
* (pathmod now NULL → real clause, not an assertion). */
fputs("//ww:module-reset\n", out);
}
rewind(in);
int ch;
while ((ch = fgetc(in)) != EOF) fputc(ch, out);
fputc('\n', out);
fclose(in);
}
/* objstem (when non-NULL/non-empty) redirects the .s/.o/.combined.ww
* side files to live beside the build's OUTPUT instead of next to the
* source (task #15/T3). `ww run` and `ww build -o` pass it so concurrent
* builds never share the next-to-source fixed paths; the default
* (objstem == NULL) keeps the old next-to-source layout, load-bearing
* for make's tracked-combined.ww regen + the #110 freshness gate. */
static int
build_one(const char *src, int entry_is_dir, const char *out,
const char *objstem, const char *extra_includes, const char *extra_libs,
const char *extra_libdirs, int is_test)
{
const char *c6 = toolpath("WW_W6C", "w6c");
const char *a6 = toolpath("WW_W6A", "w6a");
const char *l6 = toolpath("WW_W6L", "w6l");
const char *libdir = getenv("WW_LIB");
if (libdir == NULL || libdir[0] == 0) {
static char libbuf[1024];
snprintf(libbuf, sizeof libbuf, "%s/../lib", self_dir);
libdir = libbuf;
}
const char *srcdir = getenv("WW_SRCLIB");
static char srcbuf[1024];
if (srcdir == NULL || srcdir[0] == 0) {
/* in-tree default: ../../lib relative to bin/ */
snprintf(srcbuf, sizeof srcbuf, "%s/../../lib", self_dir);
if (access(srcbuf, 0) == 0) srcdir = srcbuf;
else if (access("lib", 0) == 0) srcdir = "lib";
else srcdir = libdir;
}
/* Compose the search path: source-file's directory first, then any
* -I dirs, then srcdir. The source-dir lead matches Hare's CWD-first
* convention (its `hare test` is run from the module dir, making CWD
* == module-dir); our wrappers don't cd, so dirname(src) is the
* closest analog. Also matches cc -I. — source-dir wins ties over
* the system path. locate_import walks left-to-right.
*
* For a dir entry the source-dir IS src; for a file entry it's
* the dirname. */
char srcd[1024];
if (entry_is_dir) {
snprintf(srcd, sizeof srcd, "%s", src);
size_t n = strlen(srcd);
while (n > 1 && srcd[n-1] == '/') srcd[--n] = '\0';
} else {
const char *slash = strrchr(src, '/');
if (slash) {
size_t n = (size_t)(slash - src);
if (n >= sizeof srcd) n = sizeof srcd - 1;
memcpy(srcd, src, n);
srcd[n] = '\0';
} else {
srcd[0] = '.';
srcd[1] = '\0';
}
}
static char searchpath[4096];
if (extra_includes && extra_includes[0])
snprintf(searchpath, sizeof searchpath, "%s:%s:%s",
srcd, extra_includes, srcdir);
else
snprintf(searchpath, sizeof searchpath, "%s:%s", srcd, srcdir);
srcdir = searchpath;
/* Derive a stem for .s/.o/.combined.ww side files. For a file
* entry strip the .ww. For a dir entry use <dir>/<basename(dir)>
* so artifacts land inside the module directory. */
char stem[1024];
if (entry_is_dir) {
const char *b = strrchr(srcd, '/');
const char *base = b ? b + 1 : srcd;
snprintf(stem, sizeof stem, "%s/%s", srcd, base);
} else {
snprintf(stem, sizeof stem, "%s", src);
char *dot = strrchr(stem, '.');
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
}
const char *ostem = (objstem && objstem[0]) ? objstem : stem;
char asmf[1024], obj[1024], combined[1024];
snprintf(asmf, sizeof asmf, "%s.s", ostem);
snprintf(obj, sizeof obj, "%s.o", ostem);
snprintf(combined, sizeof combined, "%s.combined.ww", ostem);
/* Resolve imports by concatenating sources into a temp file. The
* compiler then sees one flat source. Dir entry → enumerate the
* module dir's *.ww (less *test.ww); file entry → start at the
* file. */
{
FILE *cf = fopen(combined, "wb");
if (cf == NULL) {
fprintf(stderr, "ww: cannot open %s\n", combined);
return 1;
}
struct ImportSet visited = {0};
/* #17 auto-bundle lib/test: the -T synth's main calls lib/test's
* run(), but @test files don't `import test;`. Pull it like an
* implicit import through the same filename-keyed expand path
* (the visited set dedupes if a fixture imports it explicitly).
* The wwstage twin (selfhost/cmd/ww/main.ww) mirrors this. */
if (is_test) {
char tpath[1024];
int tdir = 0;
if (locate_import(srcdir, "test", tpath, sizeof tpath, &tdir)) {
if (tdir) expand_dir(cf, tpath, &visited, srcdir, "test");
else expand(cf, tpath, &visited, srcdir, NULL);
}
}
if (entry_is_dir) expand_dir(cf, srcd, &visited, srcdir, NULL);
else expand(cf, src, &visited, srcdir, NULL);
fclose(cf);
for (int i = 0; i < visited.n; i++) free(visited.paths[i]);
free(visited.paths);
}
char cmd[4096];
snprintf(cmd, sizeof cmd, "%s %s-o %s %s", c6, is_test ? "-T " : "",
asmf, combined);
if (run(cmd) != 0) {
fprintf(stderr, "ww: w6c failed\n");
return 1;
}
snprintf(cmd, sizeof cmd, "%s -o %s %s", a6, obj, asmf);
if (run(cmd) != 0) {
fprintf(stderr, "ww: w6a failed\n");
return 1;
}
/* Link runtime: prefer libwwrt.a (selective archive pull) but
* fall back to start.o + syscall.o in the in-tree obj/ dir if
* we're running uninstalled. */
char rtargs[2048] = {0};
char path[1024];
snprintf(path, sizeof path, "%s/libwwrt.a", libdir);
if (access(path, 0) == 0) {
snprintf(rtargs, sizeof rtargs, "%s", path);
} else {
char a1[1024], a2[1024];
snprintf(a1, sizeof a1, "%s/../obj/rt/start.o", self_dir);
snprintf(a2, sizeof a2, "%s/../obj/rt/syscall.o", self_dir);
snprintf(rtargs, sizeof rtargs, "%s %s", a1, a2);
}
/* -L<dir> goes before -l<name> so w6l can resolve the latter. */
const char *libargs = (extra_libs && extra_libs[0]) ? extra_libs : "";
const char *libdirset = (extra_libdirs && extra_libdirs[0]) ? extra_libdirs : "";
snprintf(cmd, sizeof cmd, "%s -o %s %s %s%s%s%s%s",
l6, out, obj, rtargs,
libdirset[0] ? " " : "", libdirset,
libargs[0] ? " " : "", libargs);
if (run(cmd) != 0) {
fprintf(stderr, "ww: w6l failed\n");
return 1;
}
return 0;
}
/* ====================================================================
* ww build --sep — M3-tail separate-compilation driver (task #46/c3).
* ww build separate-compilation driver (task #46/c3).
* ====================================================================
* The `--sep` path materializes each imported package's `.wwi`
* interface and compiles every package on its own (`w6c -c`), then
* flat-links the `.o` set. combined.ww stays the DEFAULT live path;
* --sep is purely additive (no existing invocation reaches it).
* This is the SOLE build path (E3-C1 flip, task #87): the legacy
* single-file amalgamator (build_one/expand) is gone. Each imported
* package's `.wwi` interface is materialized and every package is
* compiled on its own (`w6c -c`), then the `.o` set is flat-linked.
*
* Each w6c pass is BOTH consumer (reads dep `.wwi` as import scope) AND
* producer (writes this package's `.wwi` for its importers via -I), so
@@ -664,6 +311,40 @@ 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
* peek_package (stops at the FIRST package decl), this scans every line
* — single-file multi-package fixtures carry several `package` decls. The
* comment-skip line scan + name match mirror peek_package, uncapped. The
* wwstage twin unithaspackage must stay byte-identical (rule 10). */
static int
unit_has_package(const char *path, const char *leaf)
{
FILE *in = fopen(path, "rb");
if (in == NULL) return 0;
char line[2048];
int found = 0;
while (fgets(line, sizeof line, in)) {
const char *p = line;
while (*p == ' ' || *p == '\t') p++;
if (p[0] == '/' && p[1] == '/') continue;
if (strncmp(p, "package ", 8) != 0
&& strncmp(p, "package\t", 8) != 0) continue;
p += 8;
while (*p == ' ' || *p == '\t') p++;
size_t i = 0;
while (leaf[i] != '\0' && leaf[i] == p[i]) i++;
if (leaf[i] == '\0') {
char c = p[i];
if (c == ';' || c == ' ' || c == '\t'
|| c == '\n' || c == '\0') { found = 1; break; }
}
}
fclose(in);
return found;
}
/* Scan one source file for top-level `import IDENT;`, resolving each. A
* DIRECTORY import is a package boundary: add it as a direct dep of pkg
* `pi`. A FILE import is an intra-package split — fold its imports into
@@ -699,12 +380,21 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file,
import_path_form(name, path_form, sizeof path_form);
char ipath[1024];
int is_dir = 0;
/* a locate-miss is an inline-satisfied (single-file multi-
* package) import; --sep targets directory packages, so the
* combined path owns that case. Skip, mirroring expand. */
/* #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: the combined path that used to own
* the genuine-missing case is gone, so the sep producer enforces
* it here (INV-2, by construction). Mirrors the deleted expand. */
if (!locate_import(searchpath, path_form, ipath, sizeof ipath,
&is_dir))
continue;
&is_dir)) {
const char *dot = strrchr(name, '.');
const char *leaf = dot ? dot + 1 : name;
if (unit_has_package(file, leaf))
continue; /* inline-satisfied */
fprintf(stderr, "ww: cannot find package %s\n", name);
exit(1);
}
if (is_dir) {
int di = sep_find_or_add(g, name, ipath, 1);
if (di < 0) { rc = -1; break; }
@@ -1470,24 +1160,17 @@ parse_build_flags(const char *cmd, int argc, char **argv,
char *libdirs, size_t libdirsz,
char *libs, size_t libsz,
char *outpath, size_t outsz,
const char **src_out, int *want_sep)
const char **src_out)
{
*src_out = NULL;
int i = 0;
for (; i < argc; i++) {
if (strcmp(argv[i], "--sep") == 0) {
/* M3-tail c3: separate-compilation path, build-only.
* `run` has no sep-compile-then-run path (out of #46
* commit-3 scope), so it LOUD-REJECTS rather than
* silently swallowing a typed flag — both stages reject
* identically (rule 10; want_sep==NULL marks the run
* caller). */
if (want_sep == NULL) {
fprintf(stderr,
"ww %s: --sep is only valid with build\n", cmd);
return -1;
}
*want_sep = 1;
/* E3-C1 flip: separate compilation is now the sole build
* path, so --sep no longer selects anything. Retained as an
* accepted no-op (every subcommand) so the existing --sep
* gate corpus keeps driving the default path. (Task #87.) */
;
} else if (strncmp(argv[i], "-l", 2) == 0 && argv[i][2]) {
size_t n = strlen(libs);
snprintf(libs + n, libsz - n,
@@ -1553,10 +1236,9 @@ do_build(int argc, char **argv)
char libdirs[2048] = {0};
char incs[2048] = {0};
char outflag[1024] = {0};
int want_sep = 0;
if (parse_build_flags("build", argc, argv, incs, sizeof incs,
libdirs, sizeof libdirs, libs, sizeof libs,
outflag, sizeof outflag, &src, &want_sep) < 0)
outflag, sizeof outflag, &src) < 0)
return 2;
if (src == NULL) src = "."; /* default: build cwd */
char resolved[1024];
@@ -1582,10 +1264,8 @@ do_build(int argc, char **argv)
} else {
basename_no_ext(resolved, out, sizeof out);
}
if (want_sep)
return build_one_sep(resolved, is_dir, out, objstem, incs,
libs, libdirs, 0);
return build_one(resolved, is_dir, out, objstem, incs, libs, libdirs, 0);
return build_one_sep(resolved, is_dir, out, objstem, incs, libs,
libdirs, 0);
}
static int
@@ -1598,7 +1278,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, &src, NULL);
outflag, sizeof outflag, &src);
if (next < 0) return 2;
if (src == NULL) src = ".";
char resolved[1024];
@@ -1611,7 +1291,7 @@ do_run(int argc, char **argv)
snprintf(tmp, sizeof tmp, "/tmp/ww_run_%d", getpid());
/* objstem = tmp → intermediates land at /tmp/ww_run_<pid>.{s,o,
* combined.ww}, never next to the source (T3). */
if (build_one(resolved, is_dir, tmp, tmp, incs, libs, libdirs, 0) != 0)
if (build_one_sep(resolved, is_dir, tmp, tmp, incs, libs, libdirs, 0) != 0)
return 1;
/* exec the built binary with any trailing argv as its argv. */
pid_t pid = fork();
@@ -1648,11 +1328,6 @@ do_test(int argc, char **argv)
* wwstage twin (selfhost/cmd/ww/main.ww dotest). */
int compileonly = 0;
char outstem[1024] = {0};
/* #79 E1: --sep routes a single-file/module test through the
* separate-compilation producer (build_one_sep, is_test=1) instead of
* the amalgamator. Additive (combined stays default); dir-mode --sep is
* deferred to E2. */
int use_sep = 0;
/* #17: an optional second positional after the target is a fnmatch
* name-filter pattern, forwarded to the test binary as argv[1]. Only
* meaningful for a single test file/module — rejected in dir mode. */
@@ -1660,7 +1335,8 @@ do_test(int argc, char **argv)
for (int i = 0; i < argc; i++) {
if (argv[i][0] == '-') {
if (strcmp(argv[i], "--sep") == 0) {
use_sep = 1;
/* E3-C1 flip: sep is the sole path; --sep is an
* accepted no-op (task #87). */
continue;
}
if (argv[i][1] == 'I') {
@@ -1715,11 +1391,8 @@ do_test(int argc, char **argv)
const char *outp;
if (outstem[0]) outp = outstem;
else { snprintf(tmp, sizeof tmp, "/tmp/ww_test_%d", getpid()); outp = tmp; }
int br = use_sep
? build_one_sep(resolved, is_dir, outp,
outstem[0] ? outstem : NULL, incs, "", "", 1)
: build_one(resolved, is_dir, outp,
outstem[0] ? outstem : NULL, incs, "", "", 1);
int br = build_one_sep(resolved, is_dir, outp,
outstem[0] ? outstem : NULL, incs, "", "", 1);
if (br != 0) return 1;
if (compileonly) return 0;
int rc = run_test_bin(outp, pattern);
@@ -1732,11 +1405,8 @@ do_test(int argc, char **argv)
const char *outp;
if (outstem[0]) outp = outstem;
else { snprintf(tmp, sizeof tmp, "/tmp/ww_test_%d", getpid()); outp = tmp; }
int br = use_sep
? build_one_sep(target, 0, outp, outstem[0] ? outstem : NULL,
incs, "", "", 1)
: build_one(target, 0, outp, outstem[0] ? outstem : NULL,
incs, "", "", 1);
int br = build_one_sep(target, 0, outp, outstem[0] ? outstem : NULL,
incs, "", "", 1);
if (br != 0) return 1;
if (compileonly) return 0;
int rc = run_test_bin(outp, pattern);
@@ -1751,13 +1421,6 @@ do_test(int argc, char **argv)
fprintf(stderr, "ww test: -c/-o need a single test file\n");
return 2;
}
/* #79 E1: dir-mode --sep is deferred to E2 — single-file/module is
* enough to prove the sep test path. Loud-reject rather than silently
* fall back to the combined per-file build. */
if (use_sep) {
fprintf(stderr, "ww test: --sep needs a single test file\n");
return 2;
}
/* #17: a name-filter pattern is per-binary; directory mode builds one
* binary per *_test.ww, so a single pattern can't sensibly route. */
if (pattern) {
@@ -1781,7 +1444,7 @@ do_test(int argc, char **argv)
snprintf(tmp, sizeof tmp, "/tmp/ww_test_%d_%d", getpid(), i);
const char *label = strrchr(files[i], '/');
label = label ? label + 1 : files[i];
int rc = build_one(files[i], 0, tmp, NULL, target, "", "", 1);
int rc = build_one_sep(files[i], 0, tmp, NULL, target, "", "", 1);
if (rc != 0) {
fprintf(stderr, "FAIL %s (build)\n", label);
fail++;

View File

@@ -181,12 +181,13 @@ fn procrun(path: *u8, argv: **u8) i32 = {
return code;
};
// ---- `use` resolution + source concatenation --------------------------
// ---- import resolution + visited-set -----------------------------------
//
// Recursive expansion: for each `use IDENT;` we find at the top of
// `path`, resolve via the colon-separated `dirs`, expand the imported
// file first, then append our own bytes. Already-visited paths are
// skipped (linear scan; typical builds visit a handful of modules).
// The separate-compilation producer scans each unit's top-of-file
// `import IDENT;` lines and resolves them via the colon-separated `dirs`
// search path. A per-scan visited set (linear; typical builds visit a
// handful of modules) breaks cycles. (E3-C1: the legacy single-file
// source concatenator was deleted — sep is the sole path. Task #87.)
type strnode = struct {
s: str,
@@ -509,307 +510,6 @@ fn scanuse(src: *u8, len: u64) (*u8, u64) = {
return src + idstart, idlen;
};
// expand — emit one file's bytes verbatim into the combined stream,
// after recursive-expanding its top-of-file `import X;` imports.
// Each source declares its own `package <name>;` (parser stamps
// decls).
fn expand(c: *expctx, pathcs: *u8, modpath: str) void = {
let plen: u64 = cstrlen(pathcs);
let view: str;
view.ptr = pathcs;
view.len = plen: i32;
let pathstr: str = strings.dup(view);
if (visitseen(c, pathstr)) { return; };
visitadd(c, pathstr);
let bufp: *u8;
let blen: u64;
bufp, blen = slurp(pathcs);
if (bufp == nil) {
cerr("ww: cannot read source\n");
return;
};
// Pass 1: scan top-of-file `import X;` lines, recursively expand.
let i: u64 = 0u64;
for (i < blen) {
let j: u64 = i;
for (j < blen) {
if (bufp[j] == 10u8) { break; }; // '\n'
j += 1u64;
};
let idp: *u8;
let idn: u64;
idp, idn = scanuse(bufp + i, j - i);
if (idp != nil) {
let isdir: i32 = 0;
let ipath: *u8 = locateimport(c.dirs, idp, idn,
&isdir);
if (ipath != nil) {
// M1 #22 (isdir-gated, rob-ratified): only DIRECTORY
// imports are package boundaries that path-mangle. A
// single-file import (`import opcodes;` → opcodes.ww
// declaring `package w6a`) is an intra-package file-split:
// it keeps its in-file `package` clause as its module
// (no directive → reset → package-clause mangling).
if (isdir != 0) {
let mv: str;
mv.ptr = idp;
mv.len = idn: i32;
let modstr: str = strings.dup(mv);
expanddir(c, ipath, modstr);
} else {
expand(c, ipath, "");
};
} 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 = the
// last dotted component of the import name; if an
// inline `package <leaf>` exists -> silent skip
// (the checker binds it), else fatal. cstage twin in
// cmd/ww/main.c; fatal text identical.
let lstart: u64 = 0u64;
let lk: u64 = 0u64;
for (lk < idn) {
if (idp[lk] == 46u8) { lstart = lk + 1u64; }; // '.'
lk += 1u64;
};
let leafp: *u8 = idp + lstart;
let leafn: u64 = idn - lstart;
if (!unithaspackage(bufp, blen, leafp, leafn)) {
cerr("ww: cannot find package ");
os.write(2, idp, idn);
cerr("\n");
os.exit(1);
};
};
};
i = j + 1u64;
};
// #16 option-B: a package-less file's decls would otherwise inherit
// the preceding bundled module's sticky curmod (parser parse.ww). Emit
// a curmod-reset boundary directive so the lexer/parser attribute the
// file to the primary module ("") — fixes the self-import false-fire
// and the leaked-prefix bug, codegen-neutral (bare symbols kept; not
// `package main`, which would main-prefix them). A packaged file's own
// `package` decl already sets curmod, so it needs nothing — keeping
// the directive out of every tracked combined.ww. (Task #11.)
if (modpath.len != 0) {
// M1 #22: import-reached file carries its full dotted path so
// codegen mangles symbols on the path, not the leaf clause.
let dm: str = "//ww:module ";
os.writeall(c.out, dm.ptr, dm.len: u64);
os.writeall(c.out, modpath.ptr, modpath.len: u64);
os.writeall(c.out, "\n".ptr, 1u64);
} else {
// Root/primary: always reset so a preceding imported section's
// sticky pathmod is cleared; a packaged primary's own `package`
// clause then sets curmod fresh (pathmod NULL → real clause).
let d: str = "//ww:module-reset\n";
os.writeall(c.out, d.ptr, d.len: u64);
};
os.writeall(c.out, bufp, blen);
os.writeall(c.out, "\n".ptr, 1u64);
};
// Scan `pathcs` for its first non-comment-non-blank line; if it
// starts with `package <name>;` return the package name as a fresh
// heap-allocated NUL-terminated *u8, else nil. Same shape as cstage
// peek_package.
//
// Reads the WHOLE file (via slurp), not a fixed prefix: cstage's
// peek_package scans line-by-line with fgets and no total cap, stopping
// at the first non-comment-non-blank line. A prior 2048-byte read cap
// here diverged from cstage on files whose `package` decl sits behind a
// >2048-byte comment header (strconv decimal/ftos/stof, memio) —
// returning nil and making the #16 D-i injection asymmetric (rule-10
// break, byte-id divergence in the regenerated combined). The corpus has
// no line >2047 chars, so a whole-file line scan matches cstage's
// per-line fgets byte-for-byte on every real input.
fn peekpackage(pathcs: *u8) *u8 = {
let bufp: *u8;
let nu: u64;
bufp, nu = slurp(pathcs);
if (bufp == nil) { return nil; };
let p: u64 = 0u64;
for (p < nu) {
let q: u64 = p;
for (q < nu) {
if (bufp[q] == 10u8) { break; }; // '\n'
q += 1u64;
};
let s: u64 = p;
for (s < q) {
if (bufp[s] != 32u8) {
if (bufp[s] != 9u8) { break; };
};
s += 1u64;
};
if (s < q) {
let line: str;
line.ptr = bufp + s;
line.len = (q - s): i32;
// hasprefix("//") subsumes the old s+1<q guard.
if (strings.hasprefix(line, "//")) {
p = q + 1u64;
continue;
};
// s+8<=q guard kept: hasprefix("package") needs only 7
// bytes, but the sep read at bufp[s+7] needs s+7 < q.
if (s + 8u64 <= q) {
if (strings.hasprefix(line, "package")) {
let sep: u8 = bufp[s + 7u64];
if (sep == 32u8) { }
else { if (sep != 9u8) { return nil; }; };
let t: u64 = s + 8u64;
for (t < q) {
if (bufp[t] != 32u8) {
if (bufp[t] != 9u8) { break; };
};
t += 1u64;
};
let start: u64 = t;
for (t < q) {
let ch: u8 = bufp[t];
let isalpha: bool = false;
if (ch >= 97u8) { if (ch <= 122u8) { isalpha = true; }; };
if (ch >= 65u8) { if (ch <= 90u8) { isalpha = true; }; };
if (ch >= 48u8) { if (ch <= 57u8) { isalpha = true; }; };
if (ch == 95u8) { isalpha = true; };
if (!isalpha) { break; };
t += 1u64;
};
let plen: u64 = t - start;
if (plen == 0u64) { return nil; };
let r: []u8 = alloc([], plen + 1u64)!;
let k: u64 = 0u64;
for (k < plen) { r[k] = bufp[start + k]; k += 1u64; };
r[plen] = 0u8;
return r.ptr;
};
};
return nil;
};
p = q + 1u64;
};
return nil;
};
// 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 (peekpackage stops there). 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;
};
// Strict-same-package error helper. Bundled here per task #22
// brief — failure mode is dir-enum's own.
fn strictpkgmismatch(file: *u8, pkg: *u8, dirpkg: *u8, dirpath: *u8) void = {
cerr("ww: ");
os.write(2, file, cstrlen(file));
cerr(": package ");
os.write(2, pkg, cstrlen(pkg));
cerr(" differs from ");
os.write(2, dirpkg, cstrlen(dirpkg));
cerr(" in same module dir ");
os.write(2, dirpath, cstrlen(dirpath));
cerr("\n");
os.exit(1);
};
// expanddir — enumerate <dirpath>/*.ww (skip *test.ww and
// *.combined.ww), byte-sort, recurse into each. Mirrors
// ref/hare/hare/module/srcs.ha:183 `_findsrcs` minus tag handling.
// The visited set keys on concrete file paths so multi-file modules
// are pulled once. Strict-same-package: all enumerated files must
// declare the same `package <name>;` (task #23 subset; failure
// mode native to dir-enum).
fn expanddir(c: *expctx, dirpath: *u8, modpath: str) void = {
let names: **u8;
let n: i32;
names, n = enumeratedir(dirpath);
let dlen: u64 = cstrlen(dirpath);
let dirpkg: *u8 = nil;
let i: i32 = 0;
for (i < n) {
let nlen: u64 = cstrlen(names[i]);
let fp: []u8 = alloc([], dlen + 1u64 + nlen + 1u64)!;
let k: u64 = 0u64;
for (k < dlen) { fp[k] = dirpath[k]; k += 1u64; };
fp[dlen] = 47u8; // '/'
k = 0u64;
for (k < nlen) { fp[dlen + 1u64 + k] = names[i][k]; k += 1u64; };
fp[dlen + 1u64 + nlen] = 0u8;
let pkg: *u8 = peekpackage(fp.ptr);
if (pkg != nil) {
if (dirpkg == nil) { dirpkg = pkg; }
else { if (!cstreq(dirpkg, pkg)) {
strictpkgmismatch(fp.ptr, pkg, dirpkg, dirpath);
}; };
};
expand(c, fp.ptr, modpath);
i += 1;
};
};
// ---- Build pipeline ---------------------------------------------------
// Strip the trailing ".ww" off `src` (a NUL-terminated path) into
@@ -933,6 +633,68 @@ 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 (peekpackage stops there). 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;
};
// Scan one source file for top-level `import IDENT;`. A DIRECTORY import
// is a package boundary: add as a direct dep of pi. A FILE import is an
// intra-package split: fold its imports into pi. Mirrors cstage
@@ -989,6 +751,29 @@ fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8,
return -1;
};
};
} 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: the combined
// path 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) {
if (idp[lk] == 46u8) { lstart = lk + 1u64; }; // '.'
lk += 1u64;
};
let leafp: *u8 = idp + lstart;
let leafn: u64 = idn - lstart;
if (!unithaspackage(bufp, blen, leafp, leafn)) {
cerr("ww: cannot find package ");
os.write(2, idp, idn);
cerr("\n");
os.exit(1);
};
};
};
i = j + 1u64;
@@ -1786,235 +1571,6 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
return 0;
};
// buildone — compile `src` (file or directory) into the executable
// named `out`.
// selfdir: NUL-terminated dir containing this driver and the
// wwstage tools (w6c_ww/w6a_ww/w6l_ww)
// src: NUL-terminated entry path (file or directory).
// entryisdir: non-zero when src is a module directory.
// out: NUL-terminated desired output path
// objstem: when non-nil, redirects the .s/.o/.combined.ww side
// files to live beside this stem instead of next to the
// source (T3 / task #15). `ww run` and `ww build -o` pass
// it so concurrent builds never share next-to-source fixed
// paths; nil keeps the old next-to-source layout (the make
// tracked-combined.ww regen + #110 gate depend on it). Twin
// of cmd/ww/main.c build_one's objstem.
// incs: NUL-terminated colon-list of -I dirs (may be empty)
// lf: extra linker flags (-L<dir>, -l<name>); may be nil
//
// The ww-side driver shells to the ww-side tools so a `ww_ww build`
// touches no C-built code at runtime. The C `ww` driver in cmd/ww/
// still drives the C-built w6c/w6a/w6l. Test 993 pins the two
// pipelines to byte-identical output on a corpus.
fn buildone(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, objstem: *u8, incs: *u8, lf: *lflags, istest: i32) i32 = {
let c6: *u8 = joinpathlit(selfdir, "w6c_ww");
let a6: *u8 = joinpathlit(selfdir, "w6a_ww");
let l6: *u8 = joinpathlit(selfdir, "w6l_ww");
// Default lib search path: <selfdir>/../../lib
let dotdotlib: []u8 = alloc([], (os.PATH_MAX: u64))!;
dotdotlib.len = os.PATH_MAX;
{
let off: u64 = cstrinto(dotdotlib.ptr, 0u64, selfdir);
off = strinto(dotdotlib.ptr, off, "/../../lib");
cstrseal(dotdotlib.ptr, off);
};
// Compute the source directory. For a file entry: bytes of `src`
// up to the last '/' (or "." when src has no '/'). For a dir
// entry: the dir itself (less trailing slashes). Hare's CWD-first
// convention assumes you're running from the module dir; our
// wrappers don't cd, so dirname(src) stands in as the closest
// analog. Source-dir wins ties over the system path (cc -I.).
let srcd: []u8 = alloc([], (os.PATH_MAX: u64))!;
srcd.len = os.PATH_MAX;
if (entryisdir != 0) {
let slen: u64 = cstrlen(src);
let k: u64 = 0u64;
for (k < slen) { srcd[k] = src[k]; k += 1u64; };
for (slen > 1u64) {
if (srcd[slen - 1u64] != 47u8) { break; };
slen -= 1u64;
};
srcd[slen] = 0u8;
} else {
let slen: u64 = cstrlen(src);
let last: u64 = slen;
let found: bool = false;
let i: u64 = slen;
for (i > 0u64) {
i -= 1u64;
if (src[i] == 47u8) { // '/'
last = i;
found = true;
i = 0u64;
};
};
if (found) {
let k: u64 = 0u64;
for (k < last) { srcd[k] = src[k]; k += 1u64; };
srcd[last] = 0u8;
} else {
srcd[0] = 46u8; // '.'
srcd[1] = 0u8;
};
};
// Compose searchpath: srcd + ':' + incs + ':' + dotdotlib.
let searchpath: []u8 = alloc([], (os.PATH_MAX: u64) * 3u64)!;
searchpath.len = ((os.PATH_MAX: u64) * 3u64): i32;
{
let off: u64 = cstrinto(searchpath.ptr, 0u64, srcd.ptr);
off = byteinto(searchpath.ptr, off, 58u8); // ':'
if (incs[0u64] != 0u8) {
off = cstrinto(searchpath.ptr, off, incs);
off = byteinto(searchpath.ptr, off, 58u8); // ':'
};
off = cstrinto(searchpath.ptr, off, dotdotlib.ptr);
cstrseal(searchpath.ptr, off);
};
// Stem for .s/.o/.combined.ww side files. Dir entry: <dir>/<base>;
// file entry: src stripped of .ww.
let stem: []u8 = alloc([], (os.PATH_MAX: u64))!;
stem.len = os.PATH_MAX;
if (entryisdir != 0) {
let dlen: u64 = cstrlen(srcd.ptr);
let bo: u64 = basenameoff(srcd.ptr, dlen);
let off: u64 = cstrinto(stem.ptr, 0u64, srcd.ptr);
stem[off] = 47u8; off += 1u64; // '/'
let i: u64 = bo;
for (i < dlen) { stem[off] = srcd[i]; off += 1u64; i += 1u64; };
cstrseal(stem.ptr, off);
} else {
makestem(stem.ptr, src);
};
let effstem: *u8 = stem.ptr;
if (objstem != nil) { effstem = objstem; };
let asmf: *u8 = appendlit(effstem, ".s");
let objf: *u8 = appendlit(effstem, ".o");
let combined: *u8 = appendlit(effstem, ".combined.ww");
// libwwrt.a path: <selfdir>/../lib/libwwrt.a
let libwwrt: []u8 = alloc([], (os.PATH_MAX: u64))!;
libwwrt.len = os.PATH_MAX;
{
let off: u64 = cstrinto(libwwrt.ptr, 0u64, selfdir);
off = strinto(libwwrt.ptr, off, "/../lib/libwwrt.a");
cstrseal(libwwrt.ptr, off);
};
// Step 1: expand imports into the combined file. Dir entry →
// enumerate the module dir; file entry → start at the file.
let cf: i32 = os.open(pathstr(combined), os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644
if (cf < 0) {
cerr("ww: cannot open combined\n");
return 1;
};
{
let c: expctx;
c.out = cf;
c.dirs = searchpath.ptr;
c.visit = nil;
// #17 auto-bundle lib/test: the -T synth's main calls lib/test's
// run(), but @test files don't `import test;`. Pull it like an
// implicit import through the same locate+expand path (the visit
// set dedupes a fixture that imports it explicitly). cstage twin
// in cmd/ww/main.c buildone.
if (istest != 0) {
let td: i32 = 0;
let tp: *u8 = locateimport(searchpath.ptr, "test".ptr, "test".len: u64, &td);
if (tp != nil) {
if (td != 0) { expanddir(&c, tp, "test"); }
else { expand(&c, tp, ""); };
};
};
if (entryisdir != 0) { expanddir(&c, srcd.ptr, ""); }
else { expand(&c, src, ""); };
};
os.close(cf);
// Step 2: w6c [-T] -o <stem>.s <stem>.combined.ww
{
let argv: []*u8 = alloc([], 6u64)!;
let p: i32 = 0;
argv[p] = "w6c\0".ptr; p += 1;
if (istest != 0) { argv[p] = "-T\0".ptr; p += 1; };
argv[p] = "-o\0".ptr; p += 1;
argv[p] = asmf; p += 1;
argv[p] = combined; p += 1;
argv[p] = nil; p += 1;
argv.len = p;
if (procrun(c6, argv.ptr) != 0) {
cerr("ww: w6c failed\n");
return 1;
};
};
// Step 3: w6a -o <stem>.o <stem>.s
{
let argv: []*u8 = alloc([], 5u64)!;
argv.len = 5;
argv[0] = "w6a\0".ptr;
argv[1] = "-o\0".ptr;
argv[2] = objf;
argv[3] = asmf;
argv[4] = nil;
if (procrun(a6, argv.ptr) != 0) {
cerr("ww: w6a failed\n");
return 1;
};
};
// Step 4: w6l -o <out> <stem>.o libwwrt.a [-L<dir>...] [-l<name>...]
{
let nldirs: i32 = 0;
let nllibs: i32 = 0;
let ldirs: **u8 = nil;
let llibs: **u8 = nil;
if (lf != nil) {
nldirs = lf.nlibdirs;
nllibs = lf.nlibs;
ldirs = lf.libdirs;
llibs = lf.libs;
};
// argv slots: 5 fixed (w6l, -o, out, objf, libwwrt)
// + 2 * nlibdirs (-L, dir)
// + 2 * nlibs (-l, name)
// + 1 nil terminator.
let total: i32 = 5 + 2 * nldirs + 2 * nllibs + 1;
let argv: []*u8 = alloc([], total: u64)!;
argv.len = total;
argv[0] = "w6l\0".ptr;
argv[1] = "-o\0".ptr;
argv[2] = out;
argv[3] = objf;
argv[4] = libwwrt.ptr;
let pos: i32 = 5;
let k: i32 = 0;
for (k < nldirs) {
argv[pos] = "-L\0".ptr;
argv[pos + 1] = ldirs[k];
pos += 2;
k += 1;
};
k = 0;
for (k < nllibs) {
argv[pos] = "-l\0".ptr;
argv[pos + 1] = llibs[k];
pos += 2;
k += 1;
};
argv[pos] = nil;
if (procrun(l6, argv.ptr) != 0) {
cerr("ww: w6l failed\n");
return 1;
};
};
return 0;
};
// ---- Module-by-name resolution ----------------------------------------
//
// Mirrors cmd/ww/main.c:resolvemodule. Maps a name like "foo", "lib/foo",
@@ -2154,7 +1710,6 @@ fn defaultoutpath(src: *u8) *u8 = {
fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
let src: *u8 = nil;
let wantsep: i32 = 0; // --sep: M3-tail separate-compilation path
let outflag: *u8 = nil; // -o target (binary + intermediate stem); T3
let incs: []u8 = alloc([], (os.PATH_MAX: u64) * 2u64)!;
incs.len = ((os.PATH_MAX: u64) * 2u64): i32;
@@ -2173,8 +1728,7 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
for (i < argc) {
let p: *u8 = argv[i];
if (p[0u64] == 45u8) { // '-'
if (cstreqlit(p, "--sep")) { // M3-tail separate-compile
wantsep = 1;
if (cstreqlit(p, "--sep")) { // E3-C1: sep is sole path; accepted no-op (#87)
} else { if (p[1u64] == 73u8) { // '-I'
let dir: *u8 = nil;
if (p[2u64] != 0u8) {
@@ -2290,10 +1844,7 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
lf.nlibdirs = nlibdirs;
lf.libs = libs.ptr;
lf.nlibs = nlibs;
if (wantsep != 0) {
return buildonesep(selfdir, resolved, isdir, out, objstem, incs.ptr, &lf, 0i32);
};
return buildone(selfdir, resolved, isdir, out, objstem, incs.ptr, &lf, 0i32);
return buildonesep(selfdir, resolved, isdir, out, objstem, incs.ptr, &lf, 0i32);
};
// Format the scratch path /tmp/ww_run_<pid> into buf. Returns NUL-
@@ -2348,9 +1899,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
else {
let p: *u8 = argv[i];
if (p[0u64] == 45u8) {
if (cstreqlit(p, "--sep")) { // build-only; run rejects (rule 10)
cerr("ww run: --sep is only valid with build\n");
return 2;
if (cstreqlit(p, "--sep")) { // E3-C1: sep is sole path; accepted no-op (#87)
} else { if (p[1u64] == 73u8) {
let dir: *u8 = nil;
if (p[2u64] != 0u8) {
@@ -2452,7 +2001,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
lf.nlibs = nlibs;
// objstem = tmp → intermediates at /tmp/ww_run_<pid>.{s,o,combined.ww},
// never next to the source (T3).
if (buildone(selfdir, resolved, isdir, tmp.ptr, tmp.ptr, incs.ptr, &lf, 0i32) != 0) {
if (buildonesep(selfdir, resolved, isdir, tmp.ptr, tmp.ptr, incs.ptr, &lf, 0i32) != 0) {
os.remove(pathstr(tmp.ptr));
return 1;
};
@@ -2482,7 +2031,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
// directory: open the dir, getdents64, build+run each *_test.ww,
// report ok/FAIL per file, return 0 iff all pass.
fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, outstem: *u8, pattern: *u8, usesep: i32) i32 = {
fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, outstem: *u8, pattern: *u8) i32 = {
let tmp: []u8 = alloc([], (os.PATH_MAX: u64))!;
tmp.len = os.PATH_MAX;
// -o redirects the binary + its combined (objstem, T3) to <stem>; the
@@ -2496,19 +2045,13 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, outstem: *
makeruntmp(tmp.ptr);
outp = tmp.ptr;
};
// #79 E1: --sep routes the test build through the separate-compilation
// producer (buildonesep, istest=1) instead of the amalgamator.
let bres: i32 = 0;
if (usesep != 0) {
let lf: lflags;
lf.libdirs = nil;
lf.nlibdirs = 0;
lf.libs = nil;
lf.nlibs = 0;
bres = buildonesep(selfdir, src, 0, outp, objstem, incs, &lf, 1i32);
} else {
bres = buildone(selfdir, src, 0, outp, objstem, incs, nil, 1i32);
};
// E3-C1: separate compilation is the sole build path (task #87).
let lf: lflags;
lf.libdirs = nil;
lf.nlibdirs = 0;
lf.libs = nil;
lf.nlibs = 0;
let bres: i32 = buildonesep(selfdir, src, 0, outp, objstem, incs, &lf, 1i32);
if (bres != 0) {
if (outstem == nil) { os.remove(pathstr(outp)); };
return 1;
@@ -2576,7 +2119,7 @@ fn rundirtests(selfdir: *u8, dir: *u8) i32 = {
let tmp: []u8 = alloc([], (os.PATH_MAX: u64))!;
tmp.len = os.PATH_MAX;
makeruntmp(tmp.ptr);
let bres: i32 = buildone(selfdir, path.ptr, 0, tmp.ptr, nil, tincs.ptr, nil, 1i32);
let bres: i32 = buildonesep(selfdir, path.ptr, 0, tmp.ptr, nil, tincs.ptr, nil, 1i32);
if (bres != 0) {
fail += 1;
cerr("FAIL ");
@@ -2639,16 +2182,11 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
// twin: cmd/ww/main.c do_test (error wording identical).
let compileonly: i32 = 0;
let outstem: *u8 = nil;
// #79 E1: --sep routes a single-file test through the separate-
// compilation producer (buildonesep, istest=1). Dir-mode --sep is
// deferred to E2. cstage twin: do_test use_sep.
let usesep: i32 = 0;
let i: i32 = start;
for (i < argc) {
let p: *u8 = argv[i];
if (p[0u64] == 45u8) { // '-'
if (cstreqlit(p, "--sep")) { // #79: separate-compile test
usesep = 1;
if (cstreqlit(p, "--sep")) { // E3-C1: sep is sole path; accepted no-op (#87)
} else { if (p[1u64] == 73u8) { // '-I'
let dir: *u8 = nil;
if (p[2u64] != 0u8) {
@@ -2698,7 +2236,7 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
// single-file mode: literal *.ww that exists
if (cstrendswithlit(target, ".ww")) {
if (os.access(pathstr(target), 0i32) == 0) {
return runsingletest(selfdir, target, incs.ptr, compileonly, outstem, patarg, usesep);
return runsingletest(selfdir, target, incs.ptr, compileonly, outstem, patarg);
};
};
@@ -2706,11 +2244,6 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
cerr("ww test: -c/-o need a single test file\n");
return 2;
};
// #79 E1: dir-mode --sep deferred to E2; single-file proves the path.
if (usesep != 0) {
cerr("ww test: --sep needs a single test file\n");
return 2;
};
// #17: a name-filter pattern is per-binary; dir mode builds one binary
// per *_test.ww, so a single pattern can't route. cstage twin parity.
if (patarg != nil) {

View File

@@ -88,8 +88,12 @@ main(void)
(void)tmp;
}
/* bad: mismatched package decls in same dir → strict-same-package
* error. Both stages must surface "differs from" in stderr. */
/* bad: mismatched package decls in same dir → import-path mismatch
* error. E3-C1: the amalgamator's strict-same-package "differs from"
* check is deleted; under sep w6c rejects the composed unit with
* "package <p> does not match import path <i>" (wwstage is terser per
* #68, so match the shared "does not match import path" substring).
* Both stages must surface it in stderr. */
{
const char *src = "test/wcc/data/direnum/bad_entry.ww";
snprintf(errp, sizeof errp, "/tmp/direnum_%d_bad.err", getpid());
@@ -102,8 +106,8 @@ main(void)
if (rc == 0) {
fprintf(stderr, "737[bad-cstage]: expected build failure, succeeded\n");
fail++;
} else if (!stderr_contains(errp, "differs from")) {
fprintf(stderr, "737[bad-cstage]: stderr missing 'differs from'\n");
} else if (!stderr_contains(errp, "does not match import path")) {
fprintf(stderr, "737[bad-cstage]: stderr missing 'does not match import path'\n");
fail++;
}
unlink(errp);
@@ -116,8 +120,8 @@ main(void)
if (rc == 0) {
fprintf(stderr, "737[bad-wwstage]: expected build failure, succeeded\n");
fail++;
} else if (!stderr_contains(errp, "differs from")) {
fprintf(stderr, "737[bad-wwstage]: stderr missing 'differs from'\n");
} else if (!stderr_contains(errp, "does not match import path")) {
fprintf(stderr, "737[bad-wwstage]: stderr missing 'does not match import path'\n");
fail++;
}
unlink(errp);
@@ -129,7 +133,7 @@ main(void)
* (#16): the old 2048-capped peek missed that decl, so the strict-
* same-package check skipped the file and the mismatch went
* undetected (build wrongly succeeded); the whole-file peek finds it
* → "differs from". Both stages. (reviewer-batcha pin.) */
* → "does not match import path". Both stages. (reviewer-batcha pin.) */
{
const char *src = "test/wcc/data/direnum/bad_deep_entry.ww";
snprintf(errp, sizeof errp, "/tmp/direnum_%d_baddeep.err", getpid());
@@ -142,8 +146,8 @@ main(void)
if (rc == 0) {
fprintf(stderr, "737[bad-deep-cstage]: expected build failure, succeeded\n");
fail++;
} else if (!stderr_contains(errp, "differs from")) {
fprintf(stderr, "737[bad-deep-cstage]: stderr missing 'differs from'\n");
} else if (!stderr_contains(errp, "does not match import path")) {
fprintf(stderr, "737[bad-deep-cstage]: stderr missing 'does not match import path'\n");
fail++;
}
unlink(errp);
@@ -156,8 +160,8 @@ main(void)
if (rc == 0) {
fprintf(stderr, "737[bad-deep-wwstage]: expected build failure, succeeded\n");
fail++;
} else if (!stderr_contains(errp, "differs from")) {
fprintf(stderr, "737[bad-deep-wwstage]: stderr missing 'differs from'\n");
} else if (!stderr_contains(errp, "does not match import path")) {
fprintf(stderr, "737[bad-deep-wwstage]: stderr missing 'does not match import path'\n");
fail++;
}
unlink(errp);

View File

@@ -421,8 +421,12 @@ static int
corpus_complete(const char *cwd)
{
char cmd[2048];
/* E3-C1: the flip routes lib @test builds through sep, which drops
* <stem>.sepwork/<pkg>.unit.ww scratch next to source; exclude it so
* the scan enrols real source dirs only, not generated work dirs. */
snprintf(cmd, sizeof cmd, "cd %s && find lib -name '*.ww' "
"! -name '*.combined.ww' | sed 's|/[^/]*$||' | sort -u", cwd);
"! -name '*.combined.ww' ! -path '*.sepwork/*' "
"| sed 's|/[^/]*$||' | sort -u", cwd);
FILE *p = popen(cmd, "r");
if (!p) return -1;
char dir[512];

View File

@@ -236,6 +236,18 @@ main(void)
runwait(cmd);
mkdir(td, 0755);
/* E3-C1: with the flip making sep the sole build path, every concurrent
* test now writes the global out/.pkgcache; this test's cs/ww per-package
* byte-id compare on the heavily-shared real lib pkgs (rt/time/os) then
* races a sibling building the same pkg. Pin a private per-pid cache so
* the cs and ww legs are isolated (system() builds inherit this env).
* pkgcache writes are non-atomic (cp -f), the real torn-read bug; this
* hermetic isolation is correct standalone, pending #104 (atomic
* temp+rename, both stages). */
char cachedir[80];
snprintf(cachedir, sizeof cachedir, "%s/pkgcache", td);
setenv("WW_PKGCACHE", cachedir, 1);
char rootww[1024];
snprintf(rootww, sizeof rootww, "%s/root.ww", td);
if (write_file(rootww, root_src)) { fail++; goto out; }
@@ -339,13 +351,11 @@ main(void)
}
}
/* run --sep symmetry (#46 c3): `run` has no sep-compile-then-run
* path (out of commit-3 scope), so BOTH stages must LOUD-REJECT
* `run --sep` identically (rule 10) — not one silently ignore it.
* The reject exits 2 (the driver's usage/flag-error code); assert
* cs and ww agree AND actually rejected (exit 2, not a build+run
* that happened to exit nonzero). This row is the gate-visible
* guard whose absence let the divergence hide. */
/* run --sep symmetry: E3-C1 makes sep the sole compile path, so
* `run` now genuinely sep-compiles-then-runs (the old "no sep run
* path → loud-reject exit 2" is gone with the amalgamator). BOTH
* stages must build+run the cross-boundary fixture identically
* (rule 10) and exit EXPECT_EXIT — `--sep` is an accepted no-op. */
{
int rc_cs, rc_ww;
snprintf(cmd, sizeof cmd,
@@ -354,9 +364,10 @@ main(void)
snprintf(cmd, sizeof cmd,
"%s/ww_ww run --sep %s >/dev/null 2>&1", bin, rootww);
rc_ww = runwait(cmd);
if (rc_cs != rc_ww || rc_cs != 2) {
if (rc_cs != rc_ww || rc_cs != EXPECT_EXIT) {
fprintf(stderr, "sepbuild FAIL: run --sep cs=%d ww=%d "
"(both must loud-reject, exit 2)\n", rc_cs, rc_ww);
"(both must run via sep, exit %d)\n", rc_cs, rc_ww,
EXPECT_EXIT);
fail++;
}
}

View File

@@ -26,14 +26,19 @@
* The shadow row was RED pre-c1 (ww resolved xa's !i8 -> exit 1, the wrong
* module's type under rc=0). The noshadow control pins the non-shadow path
* (scopelookupprefer lands on the SK_TYPE directly) so the fix can't
* perturb it. Single-file multi-package source is the sanctioned shape
* (cmd/ww/main.c).
* perturb it.
*
* E3-C1 retarget: the single-file multi-package source the original used
* is the amalgamator shape, deleted with the flip (ww is now strictly
* dir=module). Re-expressed as a dir-package tree: xb/ and xa/ both export
* `type invalid`, and xb IMPORTS xa so both `invalid` leaves are in scope
* during xb's sep-compile — the exact two-module same-leaf collision the
* scopelookuptype mod-preference fix governs. A regression flips to xa's
* !i8 -> exit 1.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
static int
@@ -45,62 +50,70 @@ runwait(const char *cmd)
return -1;
}
struct row { const char *label; const char *src; int want_exit; };
/* Only xb's param name varies between rows; xa and the root are shared.
* `xb_param` is the parameter spelling: "invalid" shadows the type leaf,
* "x" does not. */
struct row { const char *label; const char *xb_param; int want_exit; };
static const struct row rows[] = {
{ "shadow",
"package xb;\n"
"export type invalid = !i64;\n"
"export fn f(invalid: i32) i64 = { return size(invalid): i64; };\n"
"package xa;\n"
"export type invalid = !i8;\n"
"package main;\n"
"import xb;\n"
"import xa;\n"
"fn main() int = { return xb.f(0): int; };\n",
8 },
{ "noshadow",
"package xb;\n"
"export type invalid = !i64;\n"
"export fn f(x: i32) i64 = { return size(invalid): i64; };\n"
"package xa;\n"
"export type invalid = !i8;\n"
"package main;\n"
"import xb;\n"
"import xa;\n"
"fn main() int = { return xb.f(0): int; };\n",
8 },
{ "shadow", "invalid", 8 },
{ "noshadow", "x", 8 },
};
static int
write_file(const char *path, const char *content)
{
FILE *f = fopen(path, "wb");
if (!f) return -1;
fputs(content, f);
fclose(f);
return 0;
}
static int
run_build(const char *driver, const struct row *r, int i)
{
char src[64], tmpdir[64], cmd[1024];
snprintf(src, sizeof src, "/tmp/sltp_%d_%d.ww", getpid(), i);
snprintf(tmpdir, sizeof tmpdir, "/tmp/sltp_%d_d_%d", getpid(), i);
char dir[64], sub[128], path[256], xbsrc[512], cmd[1024];
snprintf(dir, sizeof dir, "/tmp/sltp_%d_d_%d", getpid(), i);
FILE *f = fopen(src, "wb");
if (!f) return -2;
fputs(r->src, f);
fclose(f);
snprintf(cmd, sizeof cmd, "rm -rf %s && mkdir -p %s/xa %s/xb",
dir, dir, dir);
if (runwait(cmd) != 0) return -2;
mkdir(tmpdir, 0755);
snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>/dev/null",
tmpdir, driver, src);
snprintf(path, sizeof path, "%s/xa/xa.ww", dir);
if (write_file(path, "package xa;\nexport type invalid = !i8;\n"))
return -2;
snprintf(sub, sizeof sub, "%s/xb/xb.ww", dir);
snprintf(xbsrc, sizeof xbsrc,
"package xb;\n"
"import xa;\n"
"export type invalid = !i64;\n"
"export fn f(%s: i32) i64 = { return size(invalid): i64; };\n",
r->xb_param);
if (write_file(sub, xbsrc)) return -2;
snprintf(path, sizeof path, "%s/root.ww", dir);
if (write_file(path,
"package main;\n"
"import xb;\n"
"import xa;\n"
"fn main() int = { return xb.f(0): int; };\n"))
return -2;
/* canonical dir-package build: cd into the package root, build the
* root unit; the binary lands in cwd (#22 dir=module). */
snprintf(cmd, sizeof cmd, "cd %s && %s build root.ww 2>/dev/null",
dir, driver);
int brc = runwait(cmd);
const char *base = strrchr(src, '/');
base = base ? base + 1 : src;
char outbin[128];
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
char *dot = strrchr(outbin, '.');
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
snprintf(outbin, sizeof outbin, "%s/root", dir);
int got = -1;
if (brc == 0) got = runwait(outbin);
unlink(src); unlink(outbin); rmdir(tmpdir);
snprintf(cmd, sizeof cmd, "rm -rf %s", dir);
runwait(cmd);
return brc == 0 ? got : -1;
}