diff --git a/Makefile b/Makefile index 7dc1287b..6877dc32 100644 --- a/Makefile +++ b/Makefile @@ -262,6 +262,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_slttypepref_run \ $(BIN)/test_defercap_run \ $(BIN)/test_loopcap_run \ + $(BIN)/test_enumcap_run \ $(BIN)/test_ampfncollide_run \ $(BIN)/test_trycallcollide_run \ $(BIN)/test_gunsigned_run \ @@ -817,6 +818,17 @@ $(BIN)/test_loopcap_run: test/wcc/989_loopcap_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# 989_enumcap_run (#65, F13 c3): the driver's directory enumerator grows +# dynamically (mirror cstage realloc-doubling) instead of silently capping at +# 256 files. Builds on both driver twins and asserts a byte-identical +# combined.ww on a >256-file module (rule-10). See the test header. +$(BIN)/test_enumcap_run: test/wcc/989_enumcap_run.c \ + $(BIN)/ww $(BIN)/ww_ww \ + $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + # 989_ampfncollide_run (#4, c2): `&fn` synthesis (unoptype TK_AMP + # assignableaddrfn) prefers the current module's fn when a same-leaf fn is # declared in a later module. Builds/rejects on BOTH driver twins (rule-10). diff --git a/selfhost/cmd/ww/main.combined.ww b/selfhost/cmd/ww/main.combined.ww index 0ae09dc9..e2e4eb91 100644 --- a/selfhost/cmd/ww/main.combined.ww +++ b/selfhost/cmd/ww/main.combined.ww @@ -3199,9 +3199,13 @@ fn bytecmp(a: *u8, alen: u64, b: *u8, blen: u64) i32 = { fn enumeratedir(dirpath: *u8) (**u8, i32) = { let fd: i32 = os.open(pathstr(dirpath), os.flag.RDONLY, 0i32); if (fd < 0) { return nil: **u8, 0; }; - let maxnames: i32 = 256; - let names: []*u8 = alloc([], maxnames: u64)!; - let nlens: []u64 = alloc([], maxnames: u64)!; + // #65: grow-dynamic (mirror cstage enumerate_dir_ww realloc-doubling, + // cmd/ww/main.c:209). The old fixed 256-name cap silently dropped every + // eligible file past it, diverging the combined.ww from cstage on a + // module dir with >256 sources. + let cap: i32 = 8; + let names: []*u8 = alloc([], cap: u64)!; + let nlens: []u64 = alloc([], cap: u64)!; let n: i32 = 0; let buf: []u8 = alloc([], 8192u64)!; buf.len = 8192; @@ -3216,15 +3220,27 @@ fn enumeratedir(dirpath: *u8) (**u8, i32) = { let nm: *u8 = buf.ptr + off + 19u64; let nl: u64 = cstrlen(nm); if (dirfilekeep(nm, nl)) { - if (n < maxnames) { - let cp: []u8 = alloc([], nl + 1u64)!; - let i: u64 = 0u64; - for (i < nl) { cp[i] = nm[i]; i += 1u64; }; - cp[nl] = 0u8; - names[n] = cp.ptr; - nlens[n] = nl; - n += 1; + if (n >= cap) { + let ncap: i32 = cap * 2; + let nn: []*u8 = alloc([], ncap: u64)!; + let nl2: []u64 = alloc([], ncap: u64)!; + let k: i32 = 0; + for (k < n) { + nn[k] = names[k]; + nl2[k] = nlens[k]; + k += 1; + }; + names = nn; + nlens = nl2; + cap = ncap; }; + let cp: []u8 = alloc([], nl + 1u64)!; + let i: u64 = 0u64; + for (i < nl) { cp[i] = nm[i]; i += 1u64; }; + cp[nl] = 0u8; + names[n] = cp.ptr; + nlens[n] = nl; + n += 1; }; off += reclen; }; diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index 982bb11e..b04caf4a 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -361,9 +361,13 @@ fn bytecmp(a: *u8, alen: u64, b: *u8, blen: u64) i32 = { fn enumeratedir(dirpath: *u8) (**u8, i32) = { let fd: i32 = os.open(pathstr(dirpath), os.flag.RDONLY, 0i32); if (fd < 0) { return nil: **u8, 0; }; - let maxnames: i32 = 256; - let names: []*u8 = alloc([], maxnames: u64)!; - let nlens: []u64 = alloc([], maxnames: u64)!; + // #65: grow-dynamic (mirror cstage enumerate_dir_ww realloc-doubling, + // cmd/ww/main.c:209). The old fixed 256-name cap silently dropped every + // eligible file past it, diverging the combined.ww from cstage on a + // module dir with >256 sources. + let cap: i32 = 8; + let names: []*u8 = alloc([], cap: u64)!; + let nlens: []u64 = alloc([], cap: u64)!; let n: i32 = 0; let buf: []u8 = alloc([], 8192u64)!; buf.len = 8192; @@ -378,15 +382,27 @@ fn enumeratedir(dirpath: *u8) (**u8, i32) = { let nm: *u8 = buf.ptr + off + 19u64; let nl: u64 = cstrlen(nm); if (dirfilekeep(nm, nl)) { - if (n < maxnames) { - let cp: []u8 = alloc([], nl + 1u64)!; - let i: u64 = 0u64; - for (i < nl) { cp[i] = nm[i]; i += 1u64; }; - cp[nl] = 0u8; - names[n] = cp.ptr; - nlens[n] = nl; - n += 1; + if (n >= cap) { + let ncap: i32 = cap * 2; + let nn: []*u8 = alloc([], ncap: u64)!; + let nl2: []u64 = alloc([], ncap: u64)!; + let k: i32 = 0; + for (k < n) { + nn[k] = names[k]; + nl2[k] = nlens[k]; + k += 1; + }; + names = nn; + nlens = nl2; + cap = ncap; }; + let cp: []u8 = alloc([], nl + 1u64)!; + let i: u64 = 0u64; + for (i < nl) { cp[i] = nm[i]; i += 1u64; }; + cp[nl] = 0u8; + names[n] = cp.ptr; + nlens[n] = nl; + n += 1; }; off += reclen; }; diff --git a/test/wcc/989_enumcap_run.c b/test/wcc/989_enumcap_run.c new file mode 100644 index 00000000..59f20eb9 --- /dev/null +++ b/test/wcc/989_enumcap_run.c @@ -0,0 +1,155 @@ +/* + * 989_enumcap_run (#65, F13 c3) — the driver's directory enumerator must + * bundle EVERY eligible *.ww source, not silently cap at 256. + * + * THE BUG (wwstage driver only, cat-A silent wrong combined.ww under rc=0): + * enumeratedir (selfhost/cmd/ww/main.ww) capped at maxnames=256 and the + * `if (n < maxnames)` guard silently discarded every further eligible file — + * no diagnostic. cstage enumerate_dir_ww (cmd/ww/main.c:209) grows via + * realloc doubling, unbounded. A module dir with >256 sources bundled all of + * them under cstage vs 256 under wwstage, a silently divergent combined.ww + * (and binary) under rc=0 — invisible to the byte-id gates (no in-tree + * module nears 256 files). THE FIX (CAP-SEMANTICS rule: the cstage twin grows, + * so grow-dynamic): mirror the realloc-doubling growth in enumeratedir. + * + * row | files | result (cs==ww) + * --------+-------+------------------------------------------ + * big_300 | 300 | both bundle 300, combined.ww byte-id + * small_5 | 5 | both bundle 5, combined.ww byte-id (control) + * + * big_300 was RED pre-c3 (ww bundled 256, cs 300 — divergent combined.ww). + * small_5 pins the common ≤cap path stays byte-identical. + */ +#include +#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; +} + +/* Count '^package bigmod;' lines in a file. -1 on open failure. */ +static int +count_pkgs(const char *path) +{ + FILE *f = fopen(path, "rb"); + if (!f) return -1; + char line[256]; + int n = 0; + while (fgets(line, sizeof line, f)) + if (strncmp(line, "package bigmod;", 15) == 0) n++; + fclose(f); + return n; +} + +/* Returns 0 if both drivers produced a combined.ww with `nfiles` package + * lines AND byte-identical content; non-zero (and prints) otherwise. */ +static int +one_row(const char *label, const char *cdrv, const char *wdrv, int nfiles, + int have_ww) +{ + char dir[64], main_ww[128], cstem[128], wstem[128]; + char ccomb[160], wcomb[160], cmd[1024], fn[160]; + snprintf(dir, sizeof dir, "/tmp/encap_%d_%s", getpid(), label); + snprintf(cmd, sizeof cmd, "rm -rf %s && mkdir -p %s/bigmod", dir, dir); + system(cmd); + + for (int i = 0; i < nfiles; i++) { + snprintf(fn, sizeof fn, "%s/bigmod/f%03d.ww", dir, i); + FILE *f = fopen(fn, "wb"); + if (!f) return 1; + fprintf(f, "package bigmod;\nexport fn f%03d() i32 = " + "{ return %d; };\n", i, i); + fclose(f); + } + snprintf(main_ww, sizeof main_ww, "%s/main.ww", dir); + FILE *mf = fopen(main_ww, "wb"); + if (!mf) return 1; + fputs("package main;\nimport bigmod;\nfn main() void = {};\n", mf); + fclose(mf); + + snprintf(cstem, sizeof cstem, "%s/X_c", dir); + snprintf(wstem, sizeof wstem, "%s/X_w", dir); + snprintf(ccomb, sizeof ccomb, "%s.combined.ww", cstem); + snprintf(wcomb, sizeof wcomb, "%s.combined.ww", wstem); + + int fail = 0; + snprintf(cmd, sizeof cmd, "%s build -o %s %s 2>/dev/null", + cdrv, cstem, main_ww); + if (runwait(cmd) != 0) { + fprintf(stderr, "enumcap[cstage][%s]: build failed\n", label); + fail++; + } + int cn = count_pkgs(ccomb); + if (cn != nfiles) { + fprintf(stderr, "enumcap[cstage][%s]: bundled %d want %d\n", + label, cn, nfiles); + fail++; + } + + if (have_ww) { + snprintf(cmd, sizeof cmd, "%s build -o %s %s 2>/dev/null", + wdrv, wstem, main_ww); + if (runwait(cmd) != 0) { + fprintf(stderr, "enumcap[wwstage][%s]: build failed\n", label); + fail++; + } + int wn = count_pkgs(wcomb); + if (wn != nfiles) { + fprintf(stderr, "enumcap[wwstage][%s]: bundled %d want %d " + "(silent enumeratedir cap — #65)\n", label, wn, nfiles); + fail++; + } + /* the decisive rule-10 assertion: identical combined.ww */ + snprintf(cmd, sizeof cmd, "cmp -s %s %s", ccomb, wcomb); + if (runwait(cmd) != 0) { + fprintf(stderr, "enumcap[%s]: cs/ww combined.ww differ (#65)\n", + label); + fail++; + } + } + + snprintf(cmd, sizeof cmd, "rm -rf %s", dir); + system(cmd); + return fail; +} + +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; + } + + char cdrv[1024], wdrv[1024]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + int have_ww = (access(wdrv, X_OK) == 0); + if (!have_ww) + fprintf(stderr, "enumcap: skip wwstage (no %s)\n", wdrv); + + int fail = 0, total = 2; + fail += one_row("big_300", cdrv, wdrv, 300, have_ww); + fail += one_row("small_5", cdrv, wdrv, 5, have_ww); + + if (fail) { + fprintf(stderr, "enumcap_run: %d check(s) failed\n", fail); + return 1; + } + printf("enumcap_run: %d/%d ok\n", total, total); + return 0; +}