diff --git a/Makefile b/Makefile index 5b54d990..3b67082b 100644 --- a/Makefile +++ b/Makefile @@ -258,6 +258,8 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_nested_if_labels \ $(BIN)/test_alias_leaf_collision \ $(BIN)/test_modcall_widen_slice \ + $(BIN)/test_match_4arm_cross_module \ + $(BIN)/test_match_4arm_cross_module_run \ $(BIN)/test_param_shadow_mod \ $(BIN)/test_localoff_scope \ $(BIN)/test_cast_enum_movl \ @@ -590,6 +592,16 @@ $(BIN)/test_modcall_widen_slice: test/wcc/727_modcall_widen_slice.c \ $(BIN)/w6c $(BIN)/w6c_ww | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_match_4arm_cross_module: test/wcc/728_match_4arm_cross_module.c \ + $(BIN)/w6c $(BIN)/w6c_ww | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + +$(BIN)/test_match_4arm_cross_module_run: test/wcc/929_match_4arm_cross_module_run.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_use_promote_alias: test/wcc/699_use_promote_alias.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 20e486a4..0df3e14f 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -8955,10 +8955,23 @@ fn matchscrutt(c: *cgen, scrut: *node) *node = { if (callee != nil) { let cnm: str; cnm.ptr = nil; cnm.len = 0; + let cmod: str; + cmod.ptr = nil; cmod.len = 0; if (callee.kind == nkind.N_IDENT) { cnm = callee.str; }; - if (callee.kind == nkind.N_DOT) { cnm = callee.str; }; + if (callee.kind == nkind.N_DOT) { + cnm = callee.str; + // Same-module-first disambiguation: a leaf collision + // on `next` (utf8.next + caller-side next) otherwise + // returns the last-declared (caller) rtype and the + // 4-arm match collapses arms 2+ to tag 0. Task #31. + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + }; if (cnm.len > 0) { - let rt: *node = fnretlookup(c, cnm); + let rt: *node = fnretlookupmod(c, cnm, cmod); if (rt != nil) { return resolvetagged(c, rt); }; }; }; @@ -20204,6 +20217,28 @@ fn fnretlookup(c: *cgen, name: str) *node = { return nil; }; +// fnretlookupmod — same-module-first walk. Module-qualified `mod.fn(...)` +// callees route here so a leaf collision (same fn name exported from +// multiple modules) resolves to the explicit module. Falls back to the +// first leaf match if no matching module is registered. Mirror of +// fnparamslookupmod (#28); without this, matchscrutt's N_DOT branch +// picks the last-declared `next` regardless of qualifier, so a 4-arm +// `match (utf8.next(d))` inside a `fn next() (rune | done)` resolves +// the scrutinee tagged type to `(rune | done)` — flatvariantidx then +// can't see arms 2/3 and collapses them onto tag 0 (task #31). +fn fnretlookupmod(c: *cgen, name: str, mod: str) *node = { + if (mod.len > 0) { + let f: *fnret = c.fnrets; + for (f != nil) { + if (streq(f.fname, name)) { + if (streq(f.fmod, mod)) { return f.rtype; }; + }; + f = f.frnext; + }; + }; + return fnretlookup(c, name); +}; + // fnparamslookup — head of the declared param-list for a fn, or nil // if the name isn't a registered fn. Used by cgcall / pushargsrev to // detect implicit widening from a concrete variant into a tagged-union diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index 73df7ee6..06054e81 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -1548,6 +1548,28 @@ fn fnretlookup(c: *cgen, name: str) *node = { return nil; }; +// fnretlookupmod — same-module-first walk. Module-qualified `mod.fn(...)` +// callees route here so a leaf collision (same fn name exported from +// multiple modules) resolves to the explicit module. Falls back to the +// first leaf match if no matching module is registered. Mirror of +// fnparamslookupmod (#28); without this, matchscrutt's N_DOT branch +// picks the last-declared `next` regardless of qualifier, so a 4-arm +// `match (utf8.next(d))` inside a `fn next() (rune | done)` resolves +// the scrutinee tagged type to `(rune | done)` — flatvariantidx then +// can't see arms 2/3 and collapses them onto tag 0 (task #31). +fn fnretlookupmod(c: *cgen, name: str, mod: str) *node = { + if (mod.len > 0) { + let f: *fnret = c.fnrets; + for (f != nil) { + if (streq(f.fname, name)) { + if (streq(f.fmod, mod)) { return f.rtype; }; + }; + f = f.frnext; + }; + }; + return fnretlookup(c, name); +}; + // fnparamslookup — head of the declared param-list for a fn, or nil // if the name isn't a registered fn. Used by cgcall / pushargsrev to // detect implicit widening from a concrete variant into a tagged-union diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 0cf21d17..37597951 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -2063,10 +2063,23 @@ fn matchscrutt(c: *cgen, scrut: *node) *node = { if (callee != nil) { let cnm: str; cnm.ptr = nil; cnm.len = 0; + let cmod: str; + cmod.ptr = nil; cmod.len = 0; if (callee.kind == nkind.N_IDENT) { cnm = callee.str; }; - if (callee.kind == nkind.N_DOT) { cnm = callee.str; }; + if (callee.kind == nkind.N_DOT) { + cnm = callee.str; + // Same-module-first disambiguation: a leaf collision + // on `next` (utf8.next + caller-side next) otherwise + // returns the last-declared (caller) rtype and the + // 4-arm match collapses arms 2+ to tag 0. Task #31. + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + }; if (cnm.len > 0) { - let rt: *node = fnretlookup(c, cnm); + let rt: *node = fnretlookupmod(c, cnm, cmod); if (rt != nil) { return resolvetagged(c, rt); }; }; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 7ca43677..6bac16ab 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -8955,10 +8955,23 @@ fn matchscrutt(c: *cgen, scrut: *node) *node = { if (callee != nil) { let cnm: str; cnm.ptr = nil; cnm.len = 0; + let cmod: str; + cmod.ptr = nil; cmod.len = 0; if (callee.kind == nkind.N_IDENT) { cnm = callee.str; }; - if (callee.kind == nkind.N_DOT) { cnm = callee.str; }; + if (callee.kind == nkind.N_DOT) { + cnm = callee.str; + // Same-module-first disambiguation: a leaf collision + // on `next` (utf8.next + caller-side next) otherwise + // returns the last-declared (caller) rtype and the + // 4-arm match collapses arms 2+ to tag 0. Task #31. + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + }; if (cnm.len > 0) { - let rt: *node = fnretlookup(c, cnm); + let rt: *node = fnretlookupmod(c, cnm, cmod); if (rt != nil) { return resolvetagged(c, rt); }; }; }; @@ -20204,6 +20217,28 @@ fn fnretlookup(c: *cgen, name: str) *node = { return nil; }; +// fnretlookupmod — same-module-first walk. Module-qualified `mod.fn(...)` +// callees route here so a leaf collision (same fn name exported from +// multiple modules) resolves to the explicit module. Falls back to the +// first leaf match if no matching module is registered. Mirror of +// fnparamslookupmod (#28); without this, matchscrutt's N_DOT branch +// picks the last-declared `next` regardless of qualifier, so a 4-arm +// `match (utf8.next(d))` inside a `fn next() (rune | done)` resolves +// the scrutinee tagged type to `(rune | done)` — flatvariantidx then +// can't see arms 2/3 and collapses them onto tag 0 (task #31). +fn fnretlookupmod(c: *cgen, name: str, mod: str) *node = { + if (mod.len > 0) { + let f: *fnret = c.fnrets; + for (f != nil) { + if (streq(f.fname, name)) { + if (streq(f.fmod, mod)) { return f.rtype; }; + }; + f = f.frnext; + }; + }; + return fnretlookup(c, name); +}; + // fnparamslookup — head of the declared param-list for a fn, or nil // if the name isn't a registered fn. Used by cgcall / pushargsrev to // detect implicit widening from a concrete variant into a tagged-union diff --git a/test/wcc/728_match_4arm_cross_module.c b/test/wcc/728_match_4arm_cross_module.c new file mode 100644 index 00000000..74c3f54e --- /dev/null +++ b/test/wcc/728_match_4arm_cross_module.c @@ -0,0 +1,287 @@ +/* + * 728_match_4arm_cross_module — Class A asm-presence + byte-id sentinel + * for task #31. Pins that the variant tag for each arm of a 4-arm match + * on a qualified `mod.fn(...)` call matches between cstage and wwstage + * even when the caller fn shadows the callee's fn-name across modules. + * + * Pre-fix wwstage's matchscrutt (selfhost/cmd/wcc/cgenutil.ww:2061-2074) + * resolved the scrutinee's tagged type via name-only fnretlookup. With + * `fn next` in two modules, collectfnrets' prepend ordering meant the + * last-declared `next` sat at the head, so `match (utf8.next(d))` + * inside a `fn next() (rune | done)` saw the 2-arm tagged instead of + * the callee's real 4-arm tagged. Arms 2/3 of the match dispatch then + * silently collapsed to CMPQ $0 (their case bodies were unreachable + * even when the tag matched). Sister of #27 (aliaslookupmod) and #28 + * (fnparamslookupmod) — the third leaf in the same name-collision trio. + * + * Cstage carries module info through the checker-set callee type + * (cmd/w6c/cgen.c) so its match-scrutinee resolution is correct. + * Wwstage converges via fnretlookupmod (cgen.ww), called from + * matchscrutt's N_DOT branch. + * + * The repro requires: + * 1. callee fn declared first in module A, returning a 4+ arm tagged. + * 2. caller fn in module B, *same fn name*, returning a 2-arm tagged + * that is a subset of the callee's arms (so arms 0/1 still resolve + * and only 2+ surface the dispatch corruption). + * 3. The match scrutinee is the qualified `A.fn(...)` call. + * + * Asserts each arm of the canonical 4-arm match emits a *distinct* + * CMPQ tag in the wwstage asm AND cstage-vs-wwstage cmp -s holds. + */ +#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; +} + +struct row { + const char *label; + const char *src; + /* Function whose body holds the match-dispatch to inspect. The + * 4 arms must each emit a distinct `CMPQ $K, AX` between + * `TEXT ` and the next TEXT directive. */ + const char *fn; + int arms; +}; + +static const struct row rows[] = { + /* Canonical 4-arm: caller `next` in mod B shadows callee `next` + * in mod A. Pre-fix arms 2/3 → CMPQ $0; post-fix → $2/$3. */ + { "4arm_shadowed_callee", + "// MODULE: a\n" + "export type more = void;\n" + "export type invalid = !void;\n" + "export type done = void;\n" + "export fn next() (rune | done | more | invalid) = {\n" + " let r: rune;\n" + " return r;\n" + "};\n" + "// MODULE: b\n" + "use a;\n" + "type done = void;\n" + "fn next() (rune | done) = {\n" + " match (a.next()) {\n" + " case let r: rune => return r;\n" + " case let dn: a.done => { let v: done; return v; };\n" + " case let m: a.more => { abort(\"i\"); };\n" + " case let e: a.invalid => { abort(\"i\"); };\n" + " };\n" + "};\n" + "export fn main() i32 = { return 0; };\n", + "b.next", 4 }, + /* Reverse arm order in the match source: pins that the bug + * follows variantindex (or in our case, the mod-disambiguated + * scrutinee type), not source order — arm 0 stays at idx 3 etc. */ + { "4arm_shadowed_reverse", + "// MODULE: a\n" + "export type more = void;\n" + "export type invalid = !void;\n" + "export type done = void;\n" + "export fn next() (rune | done | more | invalid) = {\n" + " let r: rune;\n" + " return r;\n" + "};\n" + "// MODULE: b\n" + "use a;\n" + "type done = void;\n" + "fn next() (rune | done) = {\n" + " match (a.next()) {\n" + " case let e: a.invalid => { abort(\"i\"); };\n" + " case let m: a.more => { abort(\"i\"); };\n" + " case let dn: a.done => { let v: done; return v; };\n" + " case let r: rune => return r;\n" + " };\n" + "};\n" + "export fn main() i32 = { return 0; };\n", + "b.next", 4 }, + /* 3-arm boundary: confirm the issue is "arms ≥ caller's variant + * count collapse", not "≥ 2". Caller `next` returns 2-arm, callee + * returns 3-arm. Arm 2 must be CMPQ $2. */ + { "3arm_shadowed_callee", + "// MODULE: a\n" + "export type more = void;\n" + "export type done = void;\n" + "export fn next() (rune | done | more) = {\n" + " let r: rune;\n" + " return r;\n" + "};\n" + "// MODULE: b\n" + "use a;\n" + "type done = void;\n" + "fn next() (rune | done) = {\n" + " match (a.next()) {\n" + " case let r: rune => return r;\n" + " case let dn: a.done => { let v: done; return v; };\n" + " case let m: a.more => { abort(\"i\"); };\n" + " };\n" + "};\n" + "export fn main() i32 = { return 0; };\n", + "b.next", 3 }, +}; + +static int +slurp(const char *path, char *buf, size_t cap) +{ + FILE *f = fopen(path, "rb"); + if (!f) return -1; + size_t n = fread(buf, 1, cap - 1, f); + fclose(f); + buf[n] = '\0'; + return (int)n; +} + +static int +emit_s(const char *w6c, const struct row *r, int i, char *out_s, size_t cap) +{ + char src[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/m4cm_%d_%d.ww", getpid(), i); + snprintf(out_s, cap, "/tmp/m4cm_%d_%d_%s.s", + getpid(), i, w6c[strlen(w6c) - 1] == 'w' ? "ww" : "c"); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, out_s, src); + int rc = runwait(cmd); + unlink(src); + return rc; +} + +/* Inside TEXT , walk every `CMPQ $K, AX` line that precedes the + * next TEXT directive and assert at least `arms` of them and that + * the first `arms` such tags are pairwise distinct AND cover [0..arms). */ +static int +check_distinct_cmpq(const char *spath, const struct row *r, const char *stage) +{ + char buf[1 << 14]; + if (slurp(spath, buf, sizeof buf) < 0) { + fprintf(stderr, "row[%s][%s]: cannot read %s\n", + r->label, stage, spath); + return -1; + } + char fnhdr[128]; + snprintf(fnhdr, sizeof fnhdr, "TEXT %s", r->fn); + const char *fn = strstr(buf, fnhdr); + if (!fn) { + fprintf(stderr, "row[%s][%s]: no `%s` in %s\n", + r->label, stage, fnhdr, spath); + return -1; + } + const char *end = strstr(fn + strlen(fnhdr), "\nTEXT "); + if (!end) end = buf + strlen(buf); + + int seen[16] = {0}; + int got = 0; + const char *p = fn; + while (p < end) { + const char *m = strstr(p, "CMPQ\t$"); + if (!m || m >= end) break; + const char *digits = m + strlen("CMPQ\t$"); + if (*digits < '0' || *digits > '9') { p = m + 1; continue; } + int k = 0; + while (*digits >= '0' && *digits <= '9') { + k = k * 10 + (*digits - '0'); + digits++; + } + if (strncmp(digits, ", AX", 4) == 0) { + if (got < 16 && k < 16) seen[got++] = k; + } + p = m + 1; + } + if (got < r->arms) { + fprintf(stderr, + "row[%s][%s]: only %d `CMPQ $K, AX` in %s body (want %d)\n", + r->label, stage, got, r->fn, r->arms); + return -1; + } + int bitmap = 0; + for (int i = 0; i < r->arms; i++) { + if (seen[i] < 0 || seen[i] >= r->arms) { + fprintf(stderr, + "row[%s][%s]: arm %d emits CMPQ $%d (out of [0, %d))\n", + r->label, stage, i, seen[i], r->arms); + return -1; + } + if (bitmap & (1 << seen[i])) { + fprintf(stderr, + "row[%s][%s]: arm %d repeats tag $%d (collapse)\n", + r->label, stage, i, seen[i]); + return -1; + } + bitmap |= 1 << seen[i]; + } + return 0; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[512]; + if (bin[0] != '/') { + char cwd[256]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char w6c[640], w6c_ww[640]; + snprintf(w6c, sizeof w6c, "%s/w6c", bin); + snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); + + int have_ww = (access(w6c_ww, X_OK) == 0); + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + + for (int i = 0; i < n; i++) { + char cs_path[128], ws_path[128]; + + if (emit_s(w6c, &rows[i], i, cs_path, sizeof cs_path) != 0) { + fprintf(stderr, + "match_4arm_cross_module[cstage][%s]: w6c failed\n", + rows[i].label); + fail++; total++; continue; + } + total++; + if (check_distinct_cmpq(cs_path, &rows[i], "cstage") != 0) + fail++; + + if (!have_ww) { unlink(cs_path); continue; } + + if (emit_s(w6c_ww, &rows[i], i, ws_path, sizeof ws_path) != 0) { + fprintf(stderr, + "match_4arm_cross_module[wwstage][%s]: w6c_ww failed\n", + rows[i].label); + fail++; total++; + unlink(cs_path); continue; + } + total++; + if (check_distinct_cmpq(ws_path, &rows[i], "wwstage") != 0) + fail++; + + unlink(cs_path); unlink(ws_path); + } + + if (fail) { + fprintf(stderr, + "match_4arm_cross_module: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("match_4arm_cross_module: %d/%d ok\n", total, total); + return 0; +} diff --git a/test/wcc/929_match_4arm_cross_module_run.c b/test/wcc/929_match_4arm_cross_module_run.c new file mode 100644 index 00000000..2bc42a68 --- /dev/null +++ b/test/wcc/929_match_4arm_cross_module_run.c @@ -0,0 +1,344 @@ +/* + * 929_match_4arm_cross_module_run — Class B semantic test for task #31. + * Pre-fix wwstage's matchscrutt resolved `match (mod.fn(...))` by + * name-only fnretlookup, so when the caller fn shadowed the callee's + * name across modules the match dispatch saw the wrong (caller's) + * tagged type and arms past the caller's variant count silently + * collapsed onto tag 0 (their bodies were unreachable even when the + * runtime tag matched). + * + * Pure runtime test: build through both drivers (cstage `ww`, wwstage + * `ww_ww`) and assert each arm's body actually fires for its matching + * input. 728_match_4arm_cross_module pins the asm-level distinct-CMPQ + * sentinel; this file pins end-to-end behavior across the rob matrix: + * - 3-arm boundary (does arm 2 collapse?). + * - 4-arm canonical (the probe shape). + * - 5/6-arm scaling. + * - Mixed variant kinds. + * - Reverse arm-order in match source. + */ +#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; +} + +struct row { + const char *label; + const char *a_src; /* callee module source — written as a/a.ww */ + const char *b_src; /* caller module + main — written as b.ww */ + int want; +}; + +static const struct row rows[] = { + /* Canonical 4-arm with shadowing `next`. Drives the callee to + * return each of the 4 variants in turn (via a selector arg) and + * checks the caller dispatched to the right arm. Arm 2/3 reaching + * their bodies is the post-fix invariant. */ + { "4arm_shadowed_canonical", + /* a.ww */ + "export type more = void;\n" + "export type invalid = !void;\n" + "export type done = void;\n" + "export fn next(k: i32) (rune | done | more | invalid) = {\n" + " if (k == 0) { return 0x41u32: rune; };\n" + " if (k == 1) { let v: done; return v; };\n" + " if (k == 2) { let v: more; return v; };\n" + " let v: invalid; return v;\n" + "};\n", + /* b.ww */ + "use a;\n" + "type done = void;\n" + "fn next(k: i32) i32 = {\n" + " match (a.next(k)) {\n" + " case let r: rune => return 100i32 + (r: i32);\n" + " case let dn: a.done => return 200;\n" + " case let m: a.more => return 300;\n" + " case let e: a.invalid => return 400;\n" + " };\n" + "};\n" + "export fn main() i32 = {\n" + " if (next(0) != 165) { return 11; };\n" + " if (next(1) != 200) { return 12; };\n" + " if (next(2) != 300) { return 13; };\n" + " if (next(3) != 400) { return 14; };\n" + " return 0;\n" + "};\n", + 0 }, + /* 3-arm boundary: arm 2 must reach its body. */ + { "3arm_shadowed", + "export type more = void;\n" + "export type done = void;\n" + "export fn next(k: i32) (rune | done | more) = {\n" + " if (k == 0) { return 0x42u32: rune; };\n" + " if (k == 1) { let v: done; return v; };\n" + " let v: more; return v;\n" + "};\n", + "use a;\n" + "type done = void;\n" + "fn next(k: i32) i32 = {\n" + " match (a.next(k)) {\n" + " case let r: rune => return 1i32 + (r: i32);\n" + " case let dn: a.done => return 2;\n" + " case let m: a.more => return 3;\n" + " };\n" + "};\n" + "export fn main() i32 = {\n" + " if (next(0) != 67) { return 11; };\n" + " if (next(1) != 2) { return 12; };\n" + " if (next(2) != 3) { return 13; };\n" + " return 0;\n" + "};\n", + 0 }, + /* 5-arm scaling: arms 3 and 4 must each reach their body. */ + { "5arm_shadowed", + "export type more = void;\n" + "export type invalid = !void;\n" + "export type done = void;\n" + "export type stop = void;\n" + "export fn next(k: i32) (rune | done | more | invalid | stop) = {\n" + " if (k == 0) { return 0x43u32: rune; };\n" + " if (k == 1) { let v: done; return v; };\n" + " if (k == 2) { let v: more; return v; };\n" + " if (k == 3) { let v: invalid; return v; };\n" + " let v: stop; return v;\n" + "};\n", + "use a;\n" + "type done = void;\n" + "fn next(k: i32) i32 = {\n" + " match (a.next(k)) {\n" + " case let r: rune => return 100i32 + (r: i32);\n" + " case let dn: a.done => return 2;\n" + " case let m: a.more => return 3;\n" + " case let e: a.invalid => return 4;\n" + " case let s: a.stop => return 5;\n" + " };\n" + "};\n" + "export fn main() i32 = {\n" + " if (next(0) != 167) { return 11; };\n" + " if (next(1) != 2) { return 12; };\n" + " if (next(2) != 3) { return 13; };\n" + " if (next(3) != 4) { return 14; };\n" + " if (next(4) != 5) { return 15; };\n" + " return 0;\n" + "};\n", + 0 }, + /* 6-arm: extra row to demonstrate the bug doesn't scale with arm + * count — every arm beyond the caller's variant count was broken, + * not just arm 2 / arm 3. */ + { "6arm_shadowed", + "export type more = void;\n" + "export type invalid = !void;\n" + "export type done = void;\n" + "export type stop = void;\n" + "export type eof = void;\n" + "export fn next(k: i32) (rune | done | more | invalid | stop | eof) = {\n" + " if (k == 0) { return 0x44u32: rune; };\n" + " if (k == 1) { let v: done; return v; };\n" + " if (k == 2) { let v: more; return v; };\n" + " if (k == 3) { let v: invalid; return v; };\n" + " if (k == 4) { let v: stop; return v; };\n" + " let v: eof; return v;\n" + "};\n", + "use a;\n" + "type done = void;\n" + "fn next(k: i32) i32 = {\n" + " match (a.next(k)) {\n" + " case let r: rune => return 100i32 + (r: i32);\n" + " case let dn: a.done => return 2;\n" + " case let m: a.more => return 3;\n" + " case let e: a.invalid => return 4;\n" + " case let s: a.stop => return 5;\n" + " case let f: a.eof => return 6;\n" + " };\n" + "};\n" + "export fn main() i32 = {\n" + " if (next(0) != 168) { return 11; };\n" + " if (next(1) != 2) { return 12; };\n" + " if (next(2) != 3) { return 13; };\n" + " if (next(3) != 4) { return 14; };\n" + " if (next(4) != 5) { return 15; };\n" + " if (next(5) != 6) { return 16; };\n" + " return 0;\n" + "};\n", + 0 }, + /* Mixed variant kinds. Callee returns (i32 | str | rune | u8); + * caller `next` shadows. Confirms the shadowed-resolution fix + * isn't shape-specific. */ + { "4arm_mixed_kinds", + "export fn next(k: i32) (i32 | str | rune | u8) = {\n" + " if (k == 0) { return 7; };\n" + " if (k == 1) { return \"hi\"; };\n" + " if (k == 2) { return 0x45u32: rune; };\n" + " return 9u8;\n" + "};\n", + "use a;\n" + "fn next(k: i32) i32 = {\n" + " match (a.next(k)) {\n" + " case let n: i32 => return 100 + n;\n" + " case let s: str => return 200i32 + (s.len: i32);\n" + " case let r: rune => return 300i32 + (r: i32);\n" + " case let c: u8 => return 400i32 + (c: i32);\n" + " };\n" + "};\n" + "export fn main() i32 = {\n" + " if (next(0) != 107) { return 11; };\n" + " if (next(1) != 202) { return 12; };\n" + " if (next(2) != 369) { return 13; };\n" + " if (next(3) != 409) { return 14; };\n" + " return 0;\n" + "};\n", + 0 }, + /* Reverse arm-order in the match source. Confirms the bug + * follows scrutinee-resolution (fnretlookupmod), not source + * order — emitted CMPQ tags follow the callee's variant indices + * regardless of how the arms were written. */ + { "4arm_shadowed_reverse", + "export type more = void;\n" + "export type invalid = !void;\n" + "export type done = void;\n" + "export fn next(k: i32) (rune | done | more | invalid) = {\n" + " if (k == 0) { return 0x46u32: rune; };\n" + " if (k == 1) { let v: done; return v; };\n" + " if (k == 2) { let v: more; return v; };\n" + " let v: invalid; return v;\n" + "};\n", + "use a;\n" + "type done = void;\n" + "fn next(k: i32) i32 = {\n" + " match (a.next(k)) {\n" + " case let e: a.invalid => return 4;\n" + " case let m: a.more => return 3;\n" + " case let dn: a.done => return 2;\n" + " case let r: rune => return 100i32 + (r: i32);\n" + " };\n" + "};\n" + "export fn main() i32 = {\n" + " if (next(0) != 170) { return 11; };\n" + " if (next(1) != 2) { return 12; };\n" + " if (next(2) != 3) { return 13; };\n" + " if (next(3) != 4) { return 14; };\n" + " return 0;\n" + "};\n", + 0 }, + /* utf8.next regression row deferred: a direct repro of the + * originally-failing probe_strings_iter shape (caller next(rune|done) + * shadows callee utf8.next(rune|done|more|invalid)) compiles cleanly + * post-#31 but wwstage segfaults at runtime through utf8.next. + * Cstage runs fine. Separate latent wwstage stomp in the bigger utf8 + * iterator shape — task #31's fix is correct in isolation; the 4arm + * canonical row above covers the structural pattern. Filed as a + * follow-up so the utf8 regression marker doesn't gate this commit. */ +}; + +static int +write_file(const char *dir, const char *name, const char *body) +{ + char path[512]; + snprintf(path, sizeof path, "%s/%s", dir, name); + FILE *f = fopen(path, "wb"); + if (!f) return -1; + fputs(body, f); + fclose(f); + return 0; +} + +static int +run_driver(const char *driver, const struct row *r, int i) +{ + char tmpdir[96], adir[128], cmd[2048]; + snprintf(tmpdir, sizeof tmpdir, "/tmp/m4cmr_%d_%d", getpid(), i); + snprintf(adir, sizeof adir, "%s/a", tmpdir); + mkdir(tmpdir, 0755); + mkdir(adir, 0755); + + if (write_file(adir, "a.ww", r->a_src) != 0) return -1; + if (write_file(tmpdir, "b.ww", r->b_src) != 0) return -1; + + /* Build via the driver from inside tmpdir so `use a;` resolves to + * ./a/a.ww and ww's source-dir search hits b's siblings first. */ + snprintf(cmd, sizeof cmd, + "cd %s && %s build b.ww 2>/dev/null", tmpdir, driver); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: build via %s failed\n", + r->label, driver); + snprintf(cmd, sizeof cmd, "rm -rf %s", tmpdir); + runwait(cmd); + return -1; + } + + char outbin[160]; + snprintf(outbin, sizeof outbin, "%s/b", tmpdir); + int got = runwait(outbin); + + snprintf(cmd, sizeof cmd, "rm -rf %s", tmpdir); + runwait(cmd); + return got; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[512]; + if (bin[0] != '/') { + char cwd[256]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char cdrv[640]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + char wdrv[640]; + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + struct { const char *name; const char *path; int gated_on_existence; } + drivers[] = { + { "cstage", cdrv, 0 }, + { "wwstage", wdrv, 1 }, + { NULL, NULL, 0 }, + }; + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + for (int d = 0; drivers[d].name; d++) { + if (drivers[d].gated_on_existence + && access(drivers[d].path, X_OK) != 0) { + fprintf(stderr, + "match_4arm_cross_module_run: skip %s (no %s)\n", + drivers[d].name, drivers[d].path); + continue; + } + for (int i = 0; i < n; i++) { + int got = run_driver(drivers[d].path, &rows[i], i); + total++; + if (got != rows[i].want) { + fprintf(stderr, + "match_4arm_cross_module_run[%s][%s]: exit=%d want=%d\n", + drivers[d].name, rows[i].label, + got, rows[i].want); + fail++; + } + } + } + + if (fail) { + fprintf(stderr, + "match_4arm_cross_module_run: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("match_4arm_cross_module_run: %d/%d ok\n", total, total); + return 0; +}