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).
288 lines
8.5 KiB
C
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;
|
|
}
|