diff --git a/Makefile b/Makefile index be84882e..98e77c30 100644 --- a/Makefile +++ b/Makefile @@ -501,7 +501,8 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_w6c_ww $(BIN)/test_ww_ww $(BIN)/test_self_rebuild \ $(BIN)/test_dyn_ww $(BIN)/test_selfcheck \ $(BIN)/test_attest_record $(BIN)/test_attest_drop \ - $(BIN)/test_declns_sep $(BIN)/test_wwispread_sep \ + $(BIN)/test_declns_sep $(BIN)/test_modresetadj_run \ + $(BIN)/test_wwispread_sep \ $(BIN)/test_fmt_run $(BIN)/test_log_run $(BIN)/test_fnmatch_run \ $(BIN)/test_shlex_run $(BIN)/test_getenv_run $(BIN)/test_dirs_run \ $(BIN)/test_dirs_toolong_run \ @@ -2657,6 +2658,15 @@ $(BIN)/test_declns_sep: test/wcc/989_declns_sep.c $(BIN)/ww $(BIN)/ww_ww \ $(BIN)/w6l $(BIN)/w6l_ww $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# #9 (BUG-A) — the lexer must clear a pending modpath when a `//ww:module-reset` +# is recognized in the same skipws run (empty/export-less inlined module body). +# Feeds the composed adjacency unit DIRECTLY to w6c/w6c_ww (the driver trips #11 +# first), then assembles+links+runs → all four frontend/asm/link tools + libwwrt. +$(BIN)/test_modresetadj_run: test/wcc/989_modresetadj_run.c \ + $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + # #95 — .wwi producer must preserve the `...` union-spread marker; dir-package # `ww build --sep` + run, both driver stages, 2-arm + 3-arm shapes + cs==ww. $(BIN)/test_wwispread_sep: test/wcc/989_wwispread_sep.c $(BIN)/ww $(BIN)/ww_ww \ diff --git a/cmd/wcc/lex.c b/cmd/wcc/lex.c index a3577379..5b645767 100644 --- a/cmd/wcc/lex.c +++ b/cmd/wcc/lex.c @@ -117,6 +117,9 @@ skipws(Lex *l) int af = lpeek(l, i + j); if (af == '\n' || af < 0) { l->modreset = 1; + l->modpath = NULL; /* #9: a reset supersedes a path opened + * earlier in this skipws run (empty/ + * export-less inlined module body) */ } else if (af == ' ' || af == '\t') { /* `//ww:module-reset ` @@ -136,6 +139,7 @@ skipws(Lex *l) && dch != '\t') k++; l->modreset = 1; + l->modpath = NULL; /* #9: see above — clear pending path */ if (k > s) l->modresetpath = astrndup(l->a, diff --git a/lib/ww/syntax/lex.ww b/lib/ww/syntax/lex.ww index 9ff7a2c8..3f64fbd8 100644 --- a/lib/ww/syntax/lex.ww +++ b/lib/ww/syntax/lex.ww @@ -172,8 +172,8 @@ fn skipws(l: *lex) bool = { if (rm) { let af: i32 = lpeek(l, (pre.len + rest.len): u64); - if (af == '\n') { l.modreset = 1; } - else { if (af < 0) { l.modreset = 1; } + if (af == '\n') { l.modreset = 1; l.modpathset = 0; } // #9: reset supersedes pending path (empty module body) + else { if (af < 0) { l.modreset = 1; l.modpathset = 0; } // #9: see above else { if (af == ' ' || af == '\t') { // `//ww:module-reset ` — sep // primary body tagged by its full @@ -199,6 +199,7 @@ fn skipws(l: *lex) bool = { k += 1u64; }; l.modreset = 1; + l.modpathset = 0; // #9: see above — clear pending path if (k > s0) { let view: str; view.ptr = diff --git a/test/wcc/989_modresetadj_run.c b/test/wcc/989_modresetadj_run.c new file mode 100644 index 00000000..43cc85e3 --- /dev/null +++ b/test/wcc/989_modresetadj_run.c @@ -0,0 +1,208 @@ +/* + * 989_modresetadj_run — BUG-A (#9) regression pin: the lexer must NOT + * leave a stale TK_MODPATH pending past a `//ww:module-reset`. + * + * THE BUG: `//ww:module

` and `//ww:module-reset` are both recognized + * inside skipws() in SOURCE ORDER, each setting a sticky lexer flag; + * lexnext() drains them in a FIXED order (modreset first, modpath second). + * When the two land in the SAME skipws run with no real token between them + * — i.e. `//ww:module e` directly followed by `//ww:module-reset`, which the + * sep driver emits for an empty / export-less inlined module body — the + * modreset drained first, then the STALE TK_MODPATH=e surfaced PAST the + * reset boundary, re-binding pathmod=e. The root `package main` was then + * validated against import path "e" → "package main does not match import + * path e" → HARD REJECT (cmd/wcc/parse.c:1437, both stages). + * + * THE FIX (cmd/wcc/lex.c + lib/ww/syntax/lex.ww, skipws): a reset + * recognized in source clears any modpath set earlier in the same skipws + * run. In the NORMAL non-empty boundary (reset THEN the next module's path) + * the clear is a no-op — the path is set after the reset and legitimately + * survives (the 990-997 byte-id gates cover that case). + * + * WHY a direct-frontend pin and not a `ww build` dir-fixture: the driver + * materializes an empty module's interface (`.wwi`) BEFORE composing the + * root unit, and wwi_emit defaults a decl-less module's package line to the + * literal "main" (task #11, a SEPARATE bug) — so a natural empty-module dir + * import never produces the directive ADJACENCY this bug needs; it trips #11 + * first. The pin therefore feeds the composed unit (with the adjacency) + * straight to w6c / w6c_ww, immune to #11. + * + * Three legs (the teeth): (1) w6c accepts the adjacency unit (pre-fix it + * REJECTS — the BUG-A teeth); (2) w6c_ww accepts it (rule-10 twin); + * (3) the two `.s` are byte-identical (rule-10 stage symmetry) and the + * assembled+linked program runs to its return value (the emitted code is + * valid, not just parse-clean). + * + * Light wwstage-driver test (CLAUDE.md rule 14): every intermediate is + * `-o`-redirected to /tmp, so it is phase-1 parallel-safe and never lands + * next to a source. Models 989_declns_sep + 994_w6c_ww conventions; 989 + * prefix per the sep-gate precedent (the 7xx range is exhausted). + */ +#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; +} + +static const char * +absbin(void) +{ + const char *b = getenv("BIN"); + if (!b) b = "out/bin"; + if (b[0] == '/') return b; + static char buf[2048]; + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return NULL; + snprintf(buf, sizeof buf, "%s/%s", cwd, b); + return buf; +} + +static int +slurp(const char *path, char **outbuf, size_t *outlen) +{ + FILE *f = fopen(path, "rb"); + if (!f) return -1; + fseek(f, 0, SEEK_END); + long n = ftell(f); + fseek(f, 0, SEEK_SET); + if (n < 0) { fclose(f); return -1; } + char *b = malloc((size_t)n + 1); + if (!b) { fclose(f); return -1; } + if (fread(b, 1, (size_t)n, f) != (size_t)n) { free(b); fclose(f); return -1; } + b[n] = '\0'; + fclose(f); + *outbuf = b; + *outlen = (size_t)n; + return 0; +} + +static int +files_eq(const char *a, const char *b) +{ + char *ba = NULL, *bb = NULL; + size_t na = 0, nb = 0; + if (slurp(a, &ba, &na) < 0 || slurp(b, &bb, &nb) < 0) { + free(ba); free(bb); + return -1; + } + int eq = (na == nb && memcmp(ba, bb, na) == 0); + free(ba); free(bb); + return eq ? 0 : 1; +} + +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; +} + +/* + * The composed sep-unit carrying the bug's trigger: `//ww:module e` + * (opens module e, sets the pending modpath) immediately followed — with + * no real token between — by `//ww:module-reset` (closes the empty body). + * Both directives land in ONE skipws run. The primary body then declares + * `package main` against the reset boundary; pre-fix the stale modpath=e + * survived and the clause was checked against "e". RET is distinctive so + * the run leg proves main actually executed. + */ +#define RET 42 +static const char UNIT[] = + "//ww:module e\n" + "//ww:module-reset\n" + "package main;\n" + "import e;\n" + "export fn main() i32 = { return 42; };\n"; + +int +main(void) +{ + const char *bin = absbin(); + if (!bin) return 1; + int pid = (int)getpid(); + + char unit[64], scs[64], sww[64], obj[64], prog[64], cmd[4096]; + snprintf(unit, sizeof unit, "/tmp/mradj_%d.unit.ww", pid); + snprintf(scs, sizeof scs, "/tmp/mradj_%d_cs.s", pid); + snprintf(sww, sizeof sww, "/tmp/mradj_%d_ww.s", pid); + snprintf(obj, sizeof obj, "/tmp/mradj_%d.o", pid); + snprintf(prog, sizeof prog, "/tmp/mradj_%d.bin", pid); + + int fail = 0; + + if (write_file(unit, UNIT) != 0) { + fprintf(stderr, "modresetadj: cannot write %s\n", unit); + return 1; + } + + /* Leg 1 — cstage w6c accepts the adjacency (pre-fix: REJECT). */ + snprintf(cmd, sizeof cmd, + "%s/w6c -c -o %s %s 2>/dev/null", bin, scs, unit); + if (runwait(cmd) != 0) { + fprintf(stderr, + "modresetadj FAIL: w6c rejected the adjacency unit (BUG-A)\n"); + fail++; + } + + /* Leg 2 — wwstage w6c_ww accepts it (rule-10 twin). */ + snprintf(cmd, sizeof cmd, + "%s/w6c_ww -c -o %s %s 2>/dev/null", bin, sww, unit); + if (runwait(cmd) != 0) { + fprintf(stderr, + "modresetadj FAIL: w6c_ww rejected the adjacency unit (BUG-A)\n"); + fail++; + } + + /* Leg 3a — the two .s are byte-identical (rule-10 stage symmetry). */ + if (!fail && files_eq(scs, sww) != 0) { + fprintf(stderr, + "modresetadj FAIL: w6c vs w6c_ww .s differ (rule 10)\n"); + fail++; + } + + /* Leg 3b — the emitted code assembles, links, and runs to RET. */ + if (!fail) { + snprintf(cmd, sizeof cmd, + "%s/w6a -o %s %s 2>/dev/null", bin, obj, scs); + if (runwait(cmd) != 0) { + fprintf(stderr, "modresetadj FAIL: w6a errored\n"); + fail++; + } + } + if (!fail) { + snprintf(cmd, sizeof cmd, + "%s/w6l -o %s %s %s/../lib/libwwrt.a 2>/dev/null", + bin, prog, obj, bin); + if (runwait(cmd) != 0) { + fprintf(stderr, "modresetadj FAIL: w6l errored\n"); + fail++; + } + } + if (!fail) { + int got = runwait(prog); + if (got != RET) { + fprintf(stderr, + "modresetadj FAIL: program exit=%d want=%d\n", got, RET); + fail++; + } + } + + unlink(unit); unlink(scs); unlink(sww); unlink(obj); unlink(prog); + + if (fail) return 1; + printf("modresetadj: w6c/w6c_ww accept the //ww:module + //ww:module-reset " + "adjacency, byte-identical .s, program runs to %d (#9)\n", RET); + return 0; +}