lex: clear pending modpath on module-reset, fix empty-module stale-modpath miscompile (#9)
BUG-A #9: when an empty/export-less inlined module body produces `//ww:module <p>` immediately followed by `//ww:module-reset` in one skipws run, lexnext drained TK_MODRESET first then emitted the STALE TK_MODPATH past the reset boundary, re-binding pathmod=<p> so the root `package main` was validated against <p> -> wrong reject. Fix clears the pending modpath at the reset-RECOGNITION site in skipws (NOT lexnext drain): cstage l->modpath=NULL after both l->modreset=1 (cmd/wcc/lex.c); selfhost l.modpathset=0 after all three l.modreset=1 (lib/ww/syntax/lex.ww). Symmetric (rule-10): identical token streams, bootstrap byte-id holds. The normal RESET-then-PATH boundary is unaffected (clear is a no-op when no path is pending). Pin: test/wcc/989_modresetadj_run.c feeds the raw composed adjacency to w6c+w6c_ww (accept + byte-id), red-path-proven (reverting a clear reddens it). A separate wwi-producer defect surfaced (decl-less .wwi defaults to 'package main') filed as #11; not touched here.
This commit is contained in:
208
test/wcc/989_modresetadj_run.c
Normal file
208
test/wcc/989_modresetadj_run.c
Normal file
@@ -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 <p>` 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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user