From 4ff79bff0f3cfc6d49e06e27182acf48acef68e5 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Wed, 10 Jun 2026 15:19:10 +0900 Subject: [PATCH] ww: loud fatal on unresolvable import, inline-package aware (#16 ENFORCE-driver) Both driver twins: an import that neither locates as a file nor is satisfied by an inline 'package ' declaration in the unit is now fatal "ww: cannot find package " (was a silent skip that masked dead imports and typos). The inline scan is a new every-line helper on the uncapped comment-skip core - peekpackage stops at the first decl, and single-file multi-package fixtures declare several. 949 pins both branches (miss->fatal, inline->build+run); 993 adds ww_ww parity. --- Makefile | 6 +- cmd/ww/main.c | 53 ++++++++++++- selfhost/cmd/ww/main.combined.ww | 84 ++++++++++++++++++++ selfhost/cmd/ww/main.ww | 84 ++++++++++++++++++++ test/wcc/949_missingpkg.c | 131 +++++++++++++++++++++++++++++++ test/wcc/993_ww_ww.c | 44 +++++++++++ 6 files changed, 399 insertions(+), 3 deletions(-) create mode 100644 test/wcc/949_missingpkg.c diff --git a/Makefile b/Makefile index 386816fd..79d27ef4 100644 --- a/Makefile +++ b/Makefile @@ -234,7 +234,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_w6l $(BIN)/test_data_link \ $(BIN)/test_arch \ $(BIN)/test_e2e $(BIN)/test_ffi $(BIN)/test_dyn $(BIN)/test_stdlib \ - $(BIN)/test_at_test $(BIN)/test_selfimport \ + $(BIN)/test_at_test $(BIN)/test_selfimport $(BIN)/test_missingpkg \ $(BIN)/test_let_global $(BIN)/test_def_neg_global \ $(BIN)/test_def_const_fold \ $(BIN)/test_int_cast_signed $(BIN)/test_dot_chain \ @@ -599,6 +599,10 @@ $(BIN)/test_selfimport: test/wcc/948_selfimport.c $(BIN)/w6c $(BIN)/w6c_ww \ | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_missingpkg: test/wcc/949_missingpkg.c $(BIN)/ww $(BIN)/w6c \ + $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_let_global: test/wcc/630_let_global.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/cmd/ww/main.c b/cmd/ww/main.c index 15a0b2f9..4aec6a78 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -230,6 +230,40 @@ peek_package(const char *path, char *out, size_t outsz) return found; } +/* 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 + * 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 /*.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 @@ -299,9 +333,24 @@ expand(FILE *out, const char *path, struct ImportSet *visited, 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 ` present -> silent skip; + * miss + not inline -> fatal. */ if (!locate_import(libdir, path_form, ipath, sizeof ipath, - &is_dir)) - continue; /* silently skip if not found */ + &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); + } if (is_dir) expand_dir(out, ipath, visited, libdir); else expand(out, ipath, visited, libdir); } diff --git a/selfhost/cmd/ww/main.combined.ww b/selfhost/cmd/ww/main.combined.ww index 06b34791..c4798617 100644 --- a/selfhost/cmd/ww/main.combined.ww +++ b/selfhost/cmd/ww/main.combined.ww @@ -3310,6 +3310,28 @@ fn expand(c: *expctx, pathcs: *u8) void = { if (ipath != nil) { if (isdir != 0) { expanddir(c, ipath); } 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 ` 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; @@ -3415,6 +3437,68 @@ fn peekpackage(pathcs: *u8) *u8 = { return nil; }; +// 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 (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 = { diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index 16ba831c..1b31a382 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -524,6 +524,28 @@ fn expand(c: *expctx, pathcs: *u8) void = { if (ipath != nil) { if (isdir != 0) { expanddir(c, ipath); } 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 ` 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; @@ -629,6 +651,68 @@ fn peekpackage(pathcs: *u8) *u8 = { return nil; }; +// 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 (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 = { diff --git a/test/wcc/949_missingpkg.c b/test/wcc/949_missingpkg.c new file mode 100644 index 00000000..ab9b648e --- /dev/null +++ b/test/wcc/949_missingpkg.c @@ -0,0 +1,131 @@ +/* + * 949_missingpkg — #16 ENFORCE-driver, BOTH branches (rob A inline-aware): + * miss — `import nosuchpkg;` with no inline package → the cstage `ww` + * driver exits nonzero + "ww: cannot find package nosuchpkg" + * (was a silent `continue` masking a typo'd/missing package). + * inline — a single-file multi-package unit (`package aa; … package + * main; import aa; …`) names a package the bundler can't pull + * as a file but the checker binds inline; the locate-miss is + * inline-satisfied → ww BUILDS + runs green (exit 7). Pins the + * skip branch so the fatal can't regress into over-firing on + * the legitimate inline-package import-crutch pattern. + * + * Cstage driver in a 9xx (rule-14: only WWSTAGE-driver tests are pinned to + * 950/990-997). The ww_ww twin + cstage/wwstage parity live in 993_ww_ww. + * Fixtures staged in /tmp so driver intermediates never touch the tree. + */ +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return 1; +} + +/* miss branch: `ww build` a missing import → nonzero + "cannot find + * package nosuchpkg" on stderr. Returns 0 on success. */ +static int +run_miss(const char *bin) +{ + int pid = getpid(); + char src[64], errf[64], cmd[2048], line[4096]; + snprintf(src, sizeof src, "/tmp/mp949_miss_%d.ww", pid); + snprintf(errf, sizeof errf, "/tmp/mp949_miss_%d.err", pid); + FILE *f = fopen(src, "w"); + if (!f) { fprintf(stderr, "949 FAIL: stage miss\n"); return 1; } + fputs("import nosuchpkg;\n" + "export fn main() i32 = { return 0; };\n", f); + fclose(f); + + snprintf(cmd, sizeof cmd, "%s/ww build %s -o /dev/null 2>%s", + bin, src, errf); + int rc = runwait(cmd); + if (rc == 0) { + fprintf(stderr, "949 FAIL: ww accepted a missing import\n"); + unlink(src); unlink(errf); + return 1; + } + int found = 0; + f = fopen(errf, "r"); + if (f) { + while (fgets(line, sizeof line, f)) + if (strstr(line, "cannot find package nosuchpkg")) { + found = 1; break; + } + fclose(f); + } + unlink(src); unlink(errf); + if (!found) { + fprintf(stderr, "949 FAIL: no 'cannot find package nosuchpkg' " + "on stderr\n"); + return 1; + } + return 0; +} + +/* inline branch: a single-file multi-package unit whose `import aa` is + * satisfied by the inline `package aa` must BUILD (locate-miss → inline + * scan → skip) and run to exit 7. Returns 0 on success. */ +static int +run_inline(const char *bin) +{ + int pid = getpid(); + char src[64], outb[64], cmd[2048]; + snprintf(src, sizeof src, "/tmp/mp949_inl_%d.ww", pid); + snprintf(outb, sizeof outb, "/tmp/mp949_inl_%d", pid); + FILE *f = fopen(src, "w"); + if (!f) { fprintf(stderr, "949 FAIL: stage inline\n"); return 1; } + fputs("package aa;\n" + "export fn getv() i32 = { return 7; };\n" + "package main;\n" + "import aa;\n" + "export fn main() i32 = { return aa.getv(); };\n", f); + fclose(f); + + /* `ww build` emits the binary by basename in the cwd, so build from + * /tmp to keep both source and output out of the tree. */ + snprintf(cmd, sizeof cmd, + "cd /tmp && %s/ww build mp949_inl_%d.ww >/dev/null 2>&1", bin, pid); + int rc = runwait(cmd); + if (rc != 0) { + fprintf(stderr, "949 FAIL: inline-package build failed " + "(inline-skip branch regressed)\n"); + unlink(src); + return 1; + } + int got = runwait(outb); + unlink(src); unlink(outb); + if (got != 7) { + fprintf(stderr, "949 FAIL: inline-package exit=%d want=7\n", got); + return 1; + } + return 0; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + int fail = 0; + fail += run_miss(bin); + fail += run_inline(bin); + if (fail) return 1; + printf("missing-package: miss->fatal + inline->skip(build+run) both pinned\n"); + return 0; +} diff --git a/test/wcc/993_ww_ww.c b/test/wcc/993_ww_ww.c index 66998769..11329a9a 100644 --- a/test/wcc/993_ww_ww.c +++ b/test/wcc/993_ww_ww.c @@ -13,6 +13,7 @@ */ #include #include +#include #include #include @@ -156,6 +157,44 @@ run_build_fail(const char *bin, const char *driver) return rc; } +/* #16 ENFORCE-driver: an unresolvable import is a hard error, not a silent + * skip — the driver must exit nonzero AND print "ww: cannot find package + * ". Both drivers must agree (rule-10 wording). Returns 0 if the + * driver loud-fails as expected, 1 otherwise. (The ww_ww half is why this + * lives in 993, the rule-14-safe wwstage-driver range.) */ +static int +run_missing_pkg(const char *bin, const char *driver) +{ + char src[64], errf[64]; + snprintf(src, sizeof src, "/tmp/ww_mp_%s_%d.ww", driver, getpid()); + snprintf(errf, sizeof errf, "/tmp/ww_mp_%s_%d.err", driver, getpid()); + FILE *f = fopen(src, "w"); + if (!f) return 1; + fputs("import nosuchpkg;\nexport fn main() i32 = { return 0; };\n", f); + fclose(f); + + char cmd[256]; + snprintf(cmd, sizeof cmd, "%s/%s build %s 2>%s", bin, driver, src, errf); + int rc = runwait(cmd); + + int found = 0; + FILE *e = fopen(errf, "rb"); + if (e) { + char buf[4096]; + size_t n = fread(buf, 1, sizeof buf - 1, e); + fclose(e); + buf[n] = '\0'; + found = strstr(buf, "cannot find package nosuchpkg") != NULL; + } + unlink(src); unlink(errf); + if (rc == 0 || !found) { + fprintf(stderr, "ww_ww FAIL: %s missing-pkg rc=%d found=%d " + "(want nonzero + 'cannot find package')\n", driver, rc, found); + return 1; + } + return 0; +} + int main(void) { @@ -250,6 +289,11 @@ main(void) rcfail++; } + /* #16 ENFORCE-driver: a missing import package loud-fails (nonzero + + * "cannot find package") on BOTH drivers — identical wording. */ + rcfail += run_missing_pkg(bin, "ww"); + rcfail += run_missing_pkg(bin, "ww_ww"); + if (fail || rcfail) { fprintf(stderr, "ww_ww: %d/%d diff(s) failed, %d exit-code " "row(s) failed\n", fail, n, rcfail);