From d099c29b869416b170c03dff3ce74f108e693ce7 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 4 Jun 2026 05:21:46 +0900 Subject: [PATCH] w6c_ww: matchscrutt resolves non-ident index bases via stamped type (fix #48) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-#48 wwstage matchscrutt's N_INDEX arm required ibase.kind == N_IDENT; an index over any other base (match (h.xs[i]) = N_INDEX over N_DOT, the regex fold-2a re.insts[i] shape) returned nil, so cgmatch dispatched with scrutt=nil — every case arm's variant index clamped to 0 (CMPQ $0) and @match_spill fell to the 16B default. SILENT cs≠ww runtime-wrong (cstage N_MATCH reads the checker-stamped s->type for every scrutinee shape, cmd/w6c/cgen.c:7510). The non-ident-base arm now returns the scrutinee node itself behind an istaggedtype gate — the stamped-carrier pattern of the #67 N_DOT arm and the #45 cgtypetest fix — so any base shape resolves the element's tagged tinfo for both variant indices and spill sizing. Same-class load half, one commit per the #133-expanded precedent: cgindex's generic-fallback tagged-element load was the only arm missing the slot>24 R8 word (both ident arms and cstage cgen.c:9106-9117 have it), so a >24B-slot element via a non-ident base under-read the cursor and the now-correctly-sized spill stored stale R8. 928_match_nonident_idx_run pins the repro shape (field-base slice index, all variants both polarities), the regex shape (56B-slot inst-like union, payload reads within the 32B cursor per #43), and ident/array/slice ident-base controls — per row cs==ww byte-id + runtime via both drivers. w6c/wwdump combined.ww regen'd via canonical make; selfhost corpus hand-cmp'd cs==ww both stages. Pre-existing siblings surfaced while probing, NOT folded (rule 11), reported for filing: (a) cgindex element classification skips N_CALL bases entirely (mk()[0] — wrong esz + not tagged-classified, cs≠ww, runtime-wrong, also non-match contexts); (b) `as` on a non-ident carrier still clamps the variant to 0 (cgtagvariantidx's N_TTAGGED node gate rejects the stamped carrier; byte-identical to master, the #200 spill fix covered only slot sizing). --- Makefile | 7 + selfhost/cmd/w6c/main.combined.ww | 22 ++- selfhost/cmd/wcc/cgenexpr.ww | 9 +- selfhost/cmd/wcc/cgenutil.ww | 13 +- selfhost/cmd/wwdump/main.combined.ww | 22 ++- test/wcc/928_match_nonident_idx_run.c | 268 ++++++++++++++++++++++++++ 6 files changed, 335 insertions(+), 6 deletions(-) create mode 100644 test/wcc/928_match_nonident_idx_run.c diff --git a/Makefile b/Makefile index 8fd3b6a2..cf47140d 100644 --- a/Makefile +++ b/Makefile @@ -282,6 +282,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_tagged_sret_run \ $(BIN)/test_tagscr_sizes_run \ $(BIN)/test_is_nonident_run \ + $(BIN)/test_match_nonident_idx_run \ $(BIN)/test_global_sret_run \ $(BIN)/test_sret_narrow_field \ $(BIN)/test_sret_narrow_field_run \ @@ -1185,6 +1186,12 @@ $(BIN)/test_is_nonident_run: test/wcc/927_is_nonident_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_match_nonident_idx_run: test/wcc/928_match_nonident_idx_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_global_sret_run: test/wcc/940_global_sret_run.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 38ac9b8d..c550a25d 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -17790,7 +17790,18 @@ fn matchscrutt(c: *cgen, scrut: *node) *node = { if (k == nkind.N_INDEX) { let ibase: *node = scrut.lhs; if (ibase == nil) { return nil; }; - if (ibase.kind != nkind.N_IDENT) { return nil; }; + if (ibase.kind != nkind.N_IDENT) { + // #48: non-ident index base (s.field[i], call()[i], + // nested). Only idents carry a declared tnode to walk, + // so resolve from the checker-stamped element tinfo + // instead — the #45/#67 stamped-carrier pattern; cgmatch + // gates on istaggedtype and reads scrutt.type_ directly. + // cstage N_MATCH reads s->type for every scrutinee shape + // (cmd/w6c/cgen.c:7510). Pre-#48 this returned nil and + // the variant clamped to 0 + @match_spill mis-sized. + if (istaggedtype(c, scrut)) { return scrut; }; + return nil; + }; let bl: *local = localfindnode(c, ibase.str); let btn: *node = nil; if (bl != nil) { btn = bl.tnode; } @@ -21468,8 +21479,15 @@ fn cgindex(c: *cgen, n: *node) void = { }; if (elem_tagged) { // AX holds the element address. Copy to BX (loading slot+0 - // into AX clobbers it), then read slot words. + // into AX clobbers it), then read slot words. #48: the >24 + // R8 word was missing ONLY in this fallback arm (both ident + // arms have it) — a >24B-slot element via a non-ident base + // under-read the cursor and the match spill stored stale R8. + // Mirrors cstage cgen.c:9106-9117. emitline("\tMOVQ\tAX, BX\n"); + if (elem_slot_sz > 24) { + emitline("\tMOVQ\t24(BX), R8\n"); + }; if (elem_slot_sz > 16) { emitline("\tMOVQ\t16(BX), CX\n"); }; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index de0f08bf..41b61f2c 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -1609,8 +1609,15 @@ fn cgindex(c: *cgen, n: *node) void = { }; if (elem_tagged) { // AX holds the element address. Copy to BX (loading slot+0 - // into AX clobbers it), then read slot words. + // into AX clobbers it), then read slot words. #48: the >24 + // R8 word was missing ONLY in this fallback arm (both ident + // arms have it) — a >24B-slot element via a non-ident base + // under-read the cursor and the match spill stored stale R8. + // Mirrors cstage cgen.c:9106-9117. emitline("\tMOVQ\tAX, BX\n"); + if (elem_slot_sz > 24) { + emitline("\tMOVQ\t24(BX), R8\n"); + }; if (elem_slot_sz > 16) { emitline("\tMOVQ\t16(BX), CX\n"); }; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 5f978c31..845d7425 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -2098,7 +2098,18 @@ fn matchscrutt(c: *cgen, scrut: *node) *node = { if (k == nkind.N_INDEX) { let ibase: *node = scrut.lhs; if (ibase == nil) { return nil; }; - if (ibase.kind != nkind.N_IDENT) { return nil; }; + if (ibase.kind != nkind.N_IDENT) { + // #48: non-ident index base (s.field[i], call()[i], + // nested). Only idents carry a declared tnode to walk, + // so resolve from the checker-stamped element tinfo + // instead — the #45/#67 stamped-carrier pattern; cgmatch + // gates on istaggedtype and reads scrutt.type_ directly. + // cstage N_MATCH reads s->type for every scrutinee shape + // (cmd/w6c/cgen.c:7510). Pre-#48 this returned nil and + // the variant clamped to 0 + @match_spill mis-sized. + if (istaggedtype(c, scrut)) { return scrut; }; + return nil; + }; let bl: *local = localfindnode(c, ibase.str); let btn: *node = nil; if (bl != nil) { btn = bl.tnode; } diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 91721e52..250a6c6c 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -17790,7 +17790,18 @@ fn matchscrutt(c: *cgen, scrut: *node) *node = { if (k == nkind.N_INDEX) { let ibase: *node = scrut.lhs; if (ibase == nil) { return nil; }; - if (ibase.kind != nkind.N_IDENT) { return nil; }; + if (ibase.kind != nkind.N_IDENT) { + // #48: non-ident index base (s.field[i], call()[i], + // nested). Only idents carry a declared tnode to walk, + // so resolve from the checker-stamped element tinfo + // instead — the #45/#67 stamped-carrier pattern; cgmatch + // gates on istaggedtype and reads scrutt.type_ directly. + // cstage N_MATCH reads s->type for every scrutinee shape + // (cmd/w6c/cgen.c:7510). Pre-#48 this returned nil and + // the variant clamped to 0 + @match_spill mis-sized. + if (istaggedtype(c, scrut)) { return scrut; }; + return nil; + }; let bl: *local = localfindnode(c, ibase.str); let btn: *node = nil; if (bl != nil) { btn = bl.tnode; } @@ -21468,8 +21479,15 @@ fn cgindex(c: *cgen, n: *node) void = { }; if (elem_tagged) { // AX holds the element address. Copy to BX (loading slot+0 - // into AX clobbers it), then read slot words. + // into AX clobbers it), then read slot words. #48: the >24 + // R8 word was missing ONLY in this fallback arm (both ident + // arms have it) — a >24B-slot element via a non-ident base + // under-read the cursor and the match spill stored stale R8. + // Mirrors cstage cgen.c:9106-9117. emitline("\tMOVQ\tAX, BX\n"); + if (elem_slot_sz > 24) { + emitline("\tMOVQ\t24(BX), R8\n"); + }; if (elem_slot_sz > 16) { emitline("\tMOVQ\t16(BX), CX\n"); }; diff --git a/test/wcc/928_match_nonident_idx_run.c b/test/wcc/928_match_nonident_idx_run.c new file mode 100644 index 00000000..628595bb --- /dev/null +++ b/test/wcc/928_match_nonident_idx_run.c @@ -0,0 +1,268 @@ +/* + * 928_match_nonident_idx_run — `match` on an N_INDEX scrutinee whose + * BASE is not an ident (#48). + * + * Pre-#48 wwstage matchscrutt's N_INDEX arm required ibase.kind == + * N_IDENT; an index over a struct field (`match (h.xs[i])` = N_INDEX + * over N_DOT) returned nil → cgmatch dispatched with scrutt = nil: + * every case arm's variant index clamped to 0 (CMPQ $0) and the + * @match_spill slot fell to the 16B default. SILENT cs≠ww miscompile + * (cstage N_MATCH reads the checker-stamped s->type for every + * scrutinee shape, cmd/w6c/cgen.c:7510); regex fold-2a's + * `match (re.insts[i])` is the consumer that surfaced it. The fix + * resolves the element type from the stamped .type_ (the #45/#67 + * stamped-carrier pattern), plus the load-half twin: cgindex's + * generic-fallback tagged-element load was the only arm missing the + * `slot > 24` R8 word (cgen.c:9106-9117), so a >24B-slot element via + * a non-ident base under-read the cursor. + * + * Rows pin: the discovering repro shape (field-base slice index, all + * three variants matched both ways), the regex shape (56B-slot + * inst-like union over r.insts[i]; payload reads stay within the 32B + * cursor — words past R8 are #43's deferred residual), and the ident + * scrutinee / ident-base array + slice index controls (byte-id at + * master per the #48 scoping probes; cs is untouched by the fix, so + * the per-row cs==ww cmp pins them unchanged). A call-BASE index + * (`mk()[0]`) is NOT covered: cgindex's element classification (esz + + * tagged) skips N_CALL bases — pre-existing sibling, filed separately. + * Per row: w6c vs w6c_ww byte-id (rule 10 — cstage is the + * runtime-correct reference) + runtime via both drivers. + */ +#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; + int want; +}; + +#define TU_TYPES \ + "type va = void;\n" \ + "type vb = bool;\n" \ + "type vc = size;\n" \ + "type tu = (va | vb | vc);\n" + +static const struct row rows[] = { + /* The discovering repro: slice field of a struct, indexed. + * Pre-#48 wwstage dispatched the vb element to the FIRST arm + * regardless of pattern (tag compared against 0) — exit 1. The + * third match pins arm-order independence (variant 1 matched + * from a non-first arm). */ + { "field_slice_idx", + TU_TYPES + "type holder = struct { xs: []tu, n: size };\n" + "export fn main() i32 = {\n" + " let sl: []tu = [(true: vb), ((7: size): vc)];\n" + " let h: holder;\n" + " h.xs = sl;\n" + " h.n = 2;\n" + " match (h.xs[0]) {\n" + " case let b: vb => { if (!(b: bool)) { return 2; }; };\n" + " case => return 1;\n" + " };\n" + " match (h.xs[1]) {\n" + " case let s: vc => { if ((s: size) != 7) { return 4; }; };\n" + " case => return 3;\n" + " };\n" + " match (h.xs[0]) {\n" + " case va => return 5;\n" + " case let b: vb => { if (!(b: bool)) { return 6; }; };\n" + " case vc => return 7;\n" + " };\n" + " return 0;\n" + "};\n", + 0 }, + /* The regex fold-2a shape: 56B-slot inst-like union (8B tag + + * 48B widest payload) matched over a struct-field slice index. + * The big variant drives slot/spill sizing past 24B (the R8 + * word both in cgindex's fallback load and cgmatch's spill); + * matched payloads read word 1 only — within the 32B cursor. */ + { "regex_inst_56b", + "type big = struct { a: i64, b: i64, c: i64, d: i64, e: i64, f: i64 };\n" + "type ilit = struct { r: i64 };\n" + "type imatch = void;\n" + "type inst = (big | ilit | imatch);\n" + "type re = struct { insts: []inst, n: i64 };\n" + "export fn main() i32 = {\n" + " let sl: []inst;\n" + " let l: ilit = ilit { r = 65 };\n" + " let v: inst = l;\n" + " append(sl, v);\n" + " let m: inst = void: imatch;\n" + " append(sl, m);\n" + " let r: re;\n" + " r.insts = sl;\n" + " r.n = 2;\n" + " match (r.insts[0]) {\n" + " case let x: ilit => { if (x.r != 65) { return 2; }; };\n" + " case => return 1;\n" + " };\n" + " match (r.insts[1]) {\n" + " case imatch => { };\n" + " case => return 3;\n" + " };\n" + " match (r.insts[0]) {\n" + " case big => return 4;\n" + " case let x: ilit => { if (x.r != 65) { return 5; }; };\n" + " case imatch => return 6;\n" + " };\n" + " return 0;\n" + "};\n", + 0 }, + /* Controls: ident scrutinee + ident-base array and slice index + * — the pre-existing matchscrutt/cgmatch paths the fix must not + * disturb (asm byte-id at master per the scoping probes). */ + { "ident_and_identbase_controls", + TU_TYPES + "export fn main() i32 = {\n" + " let v: tu = (true: vb);\n" + " match (v) {\n" + " case let b: vb => { if (!(b: bool)) { return 2; }; };\n" + " case => return 1;\n" + " };\n" + " let arr: [2]tu = [(true: vb), ((7: size): vc)];\n" + " match (arr[1]) {\n" + " case let s: vc => { if ((s: size) != 7) { return 4; }; };\n" + " case => return 3;\n" + " };\n" + " let sl: []tu = [(true: vb), ((7: size): vc)];\n" + " match (sl[0]) {\n" + " case let b: vb => { if (!(b: bool)) { return 6; }; };\n" + " case => return 5;\n" + " };\n" + " return 0;\n" + "};\n", + 0 }, +}; + +static const char *g_bin; + +static int +compile_s(const char *tool, const char *src, const char *outpath) +{ + char cmd[1024]; + snprintf(cmd, sizeof cmd, "%s/%s %s > %s 2>&1", + g_bin, tool, src, outpath); + return runwait(cmd); +} + +static int +file_eq(const char *a, const char *b) +{ + char cmd[1024]; + snprintf(cmd, sizeof cmd, "cmp -s %s %s", a, b); + return runwait(cmd) == 0; +} + +static int +run_driver(const char *driver, const char *src, const char *label) +{ + char tmpdir[128], cmd[1024]; + snprintf(tmpdir, sizeof tmpdir, "/tmp/matchnidx_%d_d", getpid()); + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s/%s build %s >/dev/null 2>&1", + tmpdir, g_bin, driver, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: build via %s failed\n", + label, driver); + return -1; + } + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + char outbin[256]; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + int got = runwait(outbin); + unlink(outbin); + rmdir(tmpdir); + return got; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + static 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; + } + g_bin = bin; + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + for (int i = 0; i < n; i++) { + const struct row *r = &rows[i]; + char src[128], cs_s[128], ww_s[128]; + snprintf(src, sizeof src, "/tmp/matchnidx_%d_%d.ww", + getpid(), i); + snprintf(cs_s, sizeof cs_s, "/tmp/matchnidx_%d_%d_cs.s", + getpid(), i); + snprintf(ww_s, sizeof ww_s, "/tmp/matchnidx_%d_%d_ww.s", + getpid(), i); + FILE *f = fopen(src, "wb"); + if (!f) return 1; + fputs("package main;\n\n", f); + fputs(r->src, f); + fclose(f); + + total++; + int cs_rc = compile_s("w6c", src, cs_s); + int ww_rc = compile_s("w6c_ww", src, ww_s); + if (cs_rc != 0 || ww_rc != 0) { + fprintf(stderr, "FAIL row[%s]: compile rc cs=%d " + "ww=%d\n", r->label, cs_rc, ww_rc); + fail++; + unlink(src); unlink(cs_s); unlink(ww_s); + continue; + } + if (!file_eq(cs_s, ww_s)) { + fprintf(stderr, "FAIL row[%s]: cs != ww .s\n", + r->label); + fail++; + } + int got_cs = run_driver("ww", src, r->label); + if (got_cs != r->want) { + fprintf(stderr, "FAIL row[%s] cstage: want %d " + "got %d\n", r->label, r->want, got_cs); + fail++; + } + char wwdrv[600]; + snprintf(wwdrv, sizeof wwdrv, "%s/ww_ww", g_bin); + if (access(wwdrv, X_OK) == 0) { + int got_ww = run_driver("ww_ww", src, r->label); + if (got_ww != r->want) { + fprintf(stderr, "FAIL row[%s] wwstage: " + "want %d got %d\n", + r->label, r->want, got_ww); + fail++; + } + } + unlink(src); unlink(cs_s); unlink(ww_s); + } + if (fail) { + fprintf(stderr, "match_nonident_idx_run: %d/%d rows failed\n", + fail, total); + return 1; + } + printf("match_nonident_idx_run: %d rows ok\n", total); + return 0; +}