Files
ww/test/wcc/728_match_4arm_cross_module.c
Hojun-Cho b787641ef9 selfhost+test: route N_DOT match scrutinee through fnretlookupmod (#31)
Wwstage matchscrutt now mirrors cstage's typed-AST scrutinee-type
lookup for module-qualified mod.fn(...) callees, restoring per-arm
tag dispatch on cross-module shadowed-name 4-arm matches. Class A
runtime miscompile, silent across collectfnrets shadowing — was
the 8th unmask of session 5.

Pre-fix: wwstage's matchscrutt N_DOT branch (cgenutil.ww:2061)
called `fnretlookup(c, callee.str)` — name-only resolution.
collectfnrets prepends to c.fnrets, so when a caller fn (e.g.
lib/strings's `next`) shadows a callee fn-name (utf8's `next`),
the prepend chain has the caller's narrower tagged return at the
head. matchscrutt then resolved the scrutinee type to the WRONG
tagged shape, and variantindex lookups for arms past the
shadowing caller's variant count returned -1 → want=0 →
match-arm `CMPQ $0, AX` for arms 2 and 3 on a (rune | done |
more | invalid) probe. Effect: arms 2/3 silently unreachable
even when the runtime tag matched, falling through to default.

Cstage gets the scrutinee type via the checker-set callee type
on the N_DOT node, so picks the correct utf8.next return shape.

Polarity catalog: wwstage UNDER — fnretlookup missing module-
preferring discipline. **Third leaf in the same trio**: #27
(aliaslookupmod), #28 (fnparamslookupmod), #31 (fnretlookupmod).
Pattern is recurring; full graduation of all leaf-name lookups
to same-module-first is a candidate for STATUS-3 task #1
variant-widen consolidation refactor (deferred to next session
opener per rob).

Fix: new fnretlookupmod helper in cgen.ww (same-module-first
walk, fallback to existing first-match — cell-for-cell mirror
of fnparamslookupmod from #28). matchscrutt N_DOT branch
extracts `cmod` from callee.lhs.str and routes through the
helper. Other 13 fnretlookup callsites untouched per #28's
"fix only what has a real consumer" discipline. fnret.fmod
field + collectfnrets f.fmod assignment already landed in #28.

Surfaced by lib/strings commit-2 pre-flight: probe iter+next
shape calls utf8.next; the probe's own `fn next` shadows
utf8.next at the c.fnrets head. Bootstrap-stable because no
selfhost-corpus path shadows a fn name across modules with a
wider tagged return on the shadowed side; lib/strings.iter
pulling utf8.next under wwstage was the first exerciser.

Filed follow-up (NOT in scope here): #32 wwstage runtime stomp
on utf8.next via *iterator caller — separate Class A surfaced
by 929 direct utf8.next regression row design. #31's fix is
correct in isolation; #32 blocks lib/strings commit 2 (#30).

Tests:
  - 728_match_4arm_cross_module pins distinct CMPQ $K, AX tags
    in TEXT b.next via bitmap covering [0..arms), robust to
    arm ordering. Three cross-module shadowed-name shapes × cmp
    -s byte-id. Sentinel-flip-verified: revert fnretlookupmod
    route → 3/6 wwstage fixtures fail "arm K repeats tag $0
    (collapse)".
  - 929_match_4arm_cross_module_run runtime-pins 6 rows × 2
    stages per-arm exit-code shape: 3/4/5/6-arm boundary,
    mixed (i32|str|rune|u8), reverse arm-order in match source.
    Confirms bug follows fnretlookup-resolved type, not match
    source order.

102/102 ok. 995_self_rebuild stays green (ww2==ww3==ww4 byte-id).
2026-05-18 11:12:14 +09:00

288 lines
8.5 KiB
C

/*
* 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.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;
}
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 <fn>` 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 <fn>, 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;
}