From c86c6a3bbf2d03702ae2724a5ac59a485c643cea Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sun, 14 Jun 2026 16:52:00 +0900 Subject: [PATCH] wcc/ww: match on a global value-struct tagged field reads g(SB) (#29) A match whose scrutinee is a tagged field of a GLOBAL value-struct read the tag/payload from the BP region (saved-BP + return-addr) instead of g(SB) and returned garbage. Both stages were identical-wrong, so the byte-id gate could not see it -- a gate-blind regression introduced by M1 (#25): M1's in-place N_DOT match arm uses localfind(base), which returns the 0 not-found sentinel for a global base, so 0+field.offset landed in the frame. Gate the in-place arm on a confirmed-local base -- `localfind(base)==0 && let_islet/isletvar(base)`, verbatim from cstage's own global test at cgen.c:2000 (both stages, same spelling). A global base now falls through to the existing spill path, which cgexprs the scrutinee and resolves g(SB). M1's local-field in-place ($32) path is untouched. Regenerates the w6c and wwdump combined.ww. Table-driven 841 test (global int/reassign/str-payload + a local-field M1 regression row), runtime-discriminating: pre-fix returns garbage, post-fix 42 on both stages; rob's direct-global-field spill caveat confirmed at runtime. --- Makefile | 7 + cmd/w6c/cgen.c | 10 +- selfhost/cmd/w6c/main.combined.ww | 21 ++- selfhost/cmd/wcc/cgenexpr.ww | 21 ++- selfhost/cmd/wwdump/main.combined.ww | 21 ++- test/wcc/841_match_global_field.c | 266 +++++++++++++++++++++++++++ 6 files changed, 328 insertions(+), 18 deletions(-) create mode 100644 test/wcc/841_match_global_field.c diff --git a/Makefile b/Makefile index 2116501a..4352d7d7 100644 --- a/Makefile +++ b/Makefile @@ -343,6 +343,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_tagged_widen_f64 \ $(BIN)/test_match_spill_pointer_payload \ $(BIN)/test_match_field_inplace \ + $(BIN)/test_match_global_field \ $(BIN)/test_struct_byval_param \ $(BIN)/test_struct_multi_return_scratch \ $(BIN)/test_signed_data_emit \ @@ -2125,6 +2126,12 @@ $(BIN)/test_match_field_inplace: test/wcc/831_match_field_inplace.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_match_global_field: test/wcc/841_match_global_field.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_struct_byval_param: test/wcc/717_struct_byval_param.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index d50a7769..5afa7170 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -10243,7 +10243,11 @@ cgexpr(Cg *c, Node *n, Local *locals) /* `match (p.field)` — point sl_off at the field's slot * inside the parent struct. The slot layout (tag at +0, * value words at +8/+16) is contiguous within the struct, - * so no spill is needed. */ + * so no spill is needed. #29: gated below on a + * CONFIRMED-LOCAL base — a global base (`match (g.field)`) + * has localfind==0, so 0+field.offset would land in the + * saved-BP/return-addr region; it falls through to the + * spill `else`, which resolves g(SB). */ Type *bt = s->lhs->type; Type *bu = type_chase_named(bt); Tfield *f = NULL; @@ -10254,8 +10258,8 @@ cgexpr(Cg *c, Node *n, Local *locals) } } } - if (f) { - int boff = localfind(locals, s->lhs->str); + int boff = f ? localfind(locals, s->lhs->str) : 0; + if (f && !(boff == 0 && let_islet(s->lhs->str))) { sl_off = boff + (int)f->offset; } else { /* fall back to spill — `match (h.e)` where diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index edfb423f..30c54a1b 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -26307,11 +26307,17 @@ fn cgmatch(c: *cgen, n: *node) void = { // type chases to TY_STRUCT and whose field resolves by name. // A *ptr-field base (`match (h.e)`, h:*struct) chases to // TY_PTR, the by-name scan misses, and it falls to the spill - // arm below — exactly as cstage, no separate deref guard. A - // global VALUE-struct base mis-resolves here in BOTH stages - // (shared latent #29), matched identically — no ww-only - // global guard. align-DOWN to cstage (rule 10): both stages - // emit TEXT $32 on the local-field case. + // arm below — exactly as cstage, no separate deref guard. + // #29: the in-place arm is also gated on a CONFIRMED-LOCAL + // base (bglobal below). A global VALUE-struct base + // (`match (g.field)`) has localfind==0, so the M1 base.off + // would land in the saved-BP/return-addr region — both + // stages emitted `MOVQ (BP),AX` (gate-blind both-wrong). The + // global-base predicate `localfind==0 && isletvar` (cstage + // twin: let_islet, cgen.c:2000) routes it through to the + // spill `else`, which resolves g(SB). align-DOWN to cstage + // (rule 10): both stages emit TEXT $32 on the local-field + // case and the same g(SB) spill on the global-field case. let mfld: *tfield = nil; if (scrut.kind == nkind.N_DOT && scrut.lhs != nil && scrut.lhs.kind == nkind.N_IDENT @@ -26328,7 +26334,12 @@ fn cgmatch(c: *cgen, n: *node) void = { }; }; }; + let bglobal: bool = false; if (mfld != nil) { + bglobal = (localfind(c, scrut.lhs.str) == 0) + && isletvar(c, scrut.lhs.str); + }; + if (mfld != nil && !bglobal) { let foff: i32 = mfld.offset: i32; scrutoff = localfind(c, scrut.lhs.str) + foff; scrutt = matchscrutt(c, scrut); diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 124f8df2..340cdd1a 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -2961,11 +2961,17 @@ fn cgmatch(c: *cgen, n: *node) void = { // type chases to TY_STRUCT and whose field resolves by name. // A *ptr-field base (`match (h.e)`, h:*struct) chases to // TY_PTR, the by-name scan misses, and it falls to the spill - // arm below — exactly as cstage, no separate deref guard. A - // global VALUE-struct base mis-resolves here in BOTH stages - // (shared latent #29), matched identically — no ww-only - // global guard. align-DOWN to cstage (rule 10): both stages - // emit TEXT $32 on the local-field case. + // arm below — exactly as cstage, no separate deref guard. + // #29: the in-place arm is also gated on a CONFIRMED-LOCAL + // base (bglobal below). A global VALUE-struct base + // (`match (g.field)`) has localfind==0, so the M1 base.off + // would land in the saved-BP/return-addr region — both + // stages emitted `MOVQ (BP),AX` (gate-blind both-wrong). The + // global-base predicate `localfind==0 && isletvar` (cstage + // twin: let_islet, cgen.c:2000) routes it through to the + // spill `else`, which resolves g(SB). align-DOWN to cstage + // (rule 10): both stages emit TEXT $32 on the local-field + // case and the same g(SB) spill on the global-field case. let mfld: *tfield = nil; if (scrut.kind == nkind.N_DOT && scrut.lhs != nil && scrut.lhs.kind == nkind.N_IDENT @@ -2982,7 +2988,12 @@ fn cgmatch(c: *cgen, n: *node) void = { }; }; }; + let bglobal: bool = false; if (mfld != nil) { + bglobal = (localfind(c, scrut.lhs.str) == 0) + && isletvar(c, scrut.lhs.str); + }; + if (mfld != nil && !bglobal) { let foff: i32 = mfld.offset: i32; scrutoff = localfind(c, scrut.lhs.str) + foff; scrutt = matchscrutt(c, scrut); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 48d8a1c6..27552226 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -26307,11 +26307,17 @@ fn cgmatch(c: *cgen, n: *node) void = { // type chases to TY_STRUCT and whose field resolves by name. // A *ptr-field base (`match (h.e)`, h:*struct) chases to // TY_PTR, the by-name scan misses, and it falls to the spill - // arm below — exactly as cstage, no separate deref guard. A - // global VALUE-struct base mis-resolves here in BOTH stages - // (shared latent #29), matched identically — no ww-only - // global guard. align-DOWN to cstage (rule 10): both stages - // emit TEXT $32 on the local-field case. + // arm below — exactly as cstage, no separate deref guard. + // #29: the in-place arm is also gated on a CONFIRMED-LOCAL + // base (bglobal below). A global VALUE-struct base + // (`match (g.field)`) has localfind==0, so the M1 base.off + // would land in the saved-BP/return-addr region — both + // stages emitted `MOVQ (BP),AX` (gate-blind both-wrong). The + // global-base predicate `localfind==0 && isletvar` (cstage + // twin: let_islet, cgen.c:2000) routes it through to the + // spill `else`, which resolves g(SB). align-DOWN to cstage + // (rule 10): both stages emit TEXT $32 on the local-field + // case and the same g(SB) spill on the global-field case. let mfld: *tfield = nil; if (scrut.kind == nkind.N_DOT && scrut.lhs != nil && scrut.lhs.kind == nkind.N_IDENT @@ -26328,7 +26334,12 @@ fn cgmatch(c: *cgen, n: *node) void = { }; }; }; + let bglobal: bool = false; if (mfld != nil) { + bglobal = (localfind(c, scrut.lhs.str) == 0) + && isletvar(c, scrut.lhs.str); + }; + if (mfld != nil && !bglobal) { let foff: i32 = mfld.offset: i32; scrutoff = localfind(c, scrut.lhs.str) + foff; scrutt = matchscrutt(c, scrut); diff --git a/test/wcc/841_match_global_field.c b/test/wcc/841_match_global_field.c new file mode 100644 index 00000000..b24e6d61 --- /dev/null +++ b/test/wcc/841_match_global_field.c @@ -0,0 +1,266 @@ +/* + * 841_match_global_field — match on a tagged-union struct-FIELD of a + * GLOBAL (module-level) VALUE struct: `match (g.t)`, g a `let g: box`. + * Task #29. + * + * GATE-BLIND both-wrong regression from M1 (#25). M1 added an in-place + * N_DOT match arm that points the dispatch slot at base.off + + * field.offset, computing base.off via localfind. For a LOCAL base that + * is the field's true frame slot ($32 in-place, correct). For a GLOBAL + * base localfind returns its 0/not-found sentinel, so 0 + field.offset + * lands in the saved-BP / return-addr region (BP+0/BP+8): both stages + * emit `MOVQ (BP),AX` and read garbage. Both stages were identical-wrong + * (byte-id GREEN), so only the RUNTIME oracle catches it — pre-fix + * match(g.t) returns 213 instead of 42. + * + * Fix (both stages, rule 10): gate the in-place arm on a confirmed-LOCAL + * base. A base is GLOBAL iff localfind(base)==0 && let_islet(base) + * (cstage) / isletvar(c, base) (wwstage) — the same idiom cstage uses at + * cgen.c:2000 (also 4707/4730/5393). A global base falls THROUGH to the + * existing spill `else`, which cgexprs the scrutinee and resolves g(SB) + * correctly. M1's LOCAL in-place path is unchanged (row (d) pins it). + * + * Rows pin the RUNTIME exit code on BOTH driver stages (cstage + + * wwstage) — the discriminator is the value, not byte-id, since both + * stages were identical-wrong. A cstage-vs-wwstage asm byte-identity + * check is kept per row so the symmetric (rule-10) property stays pinned + * after the guard lands in both stages. + * (a) global VALUE-struct int-variant field [spill, g(SB)] + * (b) global field bool->int payload remap [1-word variant] + * (c) global field str-payload (24B box) [3-word variant] + * (d) local VALUE-struct field, in place [M1 regression guard] + */ +#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; }; + +static const struct row rows[] = { + /* (a) GLOBAL value struct, field set to the int variant in main, + * matched. Pre-fix the in-place arm read BP+0 (213); post-fix the + * spill path resolves g(SB) and returns 42. */ + { "global_field_int", + "package main;\n" + "type box = struct { t: (int | bool), n: int };\n" + "let g: box = box { t = 0, n = 5 };\n" + "fn main() i32 = {\n" + " g.t = 42;\n" + " match (g.t) {\n" + " case let n: int => return n: i32;\n" + " case bool => return 1;\n" + " };\n" + "};\n", + 42 }, + /* (b) GLOBAL field reassigned, then matched; the int arm also reads + * the sibling field g.n to prove the surrounding global is + * undisturbed. Returns 7 + 5 = 12. The static initializer uses the + * ZERO payload (t = 0): a nonzero tagged-field payload in a static + * struct initializer trips a SEPARATE, deferred static-init DATA + * divergence (task #19/#30 — wwstage drops the payload word to 0, + * cstage emits it) that is orthogonal to the #29 match-cgen path + * exercised here; a zero payload is byte-identical across stages. */ + { "global_field_reassign_remap", + "package main;\n" + "type box = struct { t: (int | bool), n: int };\n" + "let g: box = box { t = 0, n = 5 };\n" + "fn main() i32 = {\n" + " g.t = 7;\n" + " match (g.t) {\n" + " case let n: int => return (n + g.n): i32;\n" + " case bool => return 1;\n" + " };\n" + "};\n", + 12 }, + /* (c) GLOBAL field, 24B str-payload variant (tag@+0, .ptr@+8, + * .len@+16). Returns len("hello") = 5. */ + { "global_field_str_payload_24B", + "package main;\n" + "type box = struct { t: (str | int), n: int };\n" + "let g: box = box { t = 0, n = 5 };\n" + "fn main() i32 = {\n" + " g.t = \"hello\";\n" + " match (g.t) {\n" + " case let s: str => return len(s): i32;\n" + " case int => return 99;\n" + " };\n" + "};\n", + 5 }, + /* (d) regression guard: a LOCAL value-struct field still reads in + * place (M1's $32 path stays). Returns 42. */ + { "local_field_inplace_guard", + "package main;\n" + "type box = struct { t: (int | bool), n: int };\n" + "fn main() i32 = {\n" + " let b: box = box { t = 0, n = 5 };\n" + " b.t = 42;\n" + " match (b.t) {\n" + " case let n: int => return n: i32;\n" + " case bool => return 1;\n" + " };\n" + "};\n", + 42 }, +}; + +static int +run_driver(const char *driver, const struct row *r, int i) +{ + char src[64], tmpdir[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/wcmgf_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/wcmgf_%d_d_%d", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s build %s", + tmpdir, driver, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: build via %s failed\n", + r->label, driver); + unlink(src); rmdir(tmpdir); + return -1; + } + + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + char outbin[128]; + 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(src); unlink(outbin); rmdir(tmpdir); + return got; +} + +/* asm_byte_identical — the guard lands in BOTH stages, so cstage vs + * wwstage text output must stay byte-for-byte identical on every row. */ +static int +asm_byte_identical(const char *bin, const struct row *r, int i) +{ + char src[64], cs[64], ws[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/wcmgf_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/wcmgf_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/wcmgf_asm_%d_%d_w.s", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c errored\n", r->label); + unlink(src); + return -1; + } + snprintf(cmd, sizeof cmd, "%s/w6c_ww -o %s %s 2>/dev/null", + bin, ws, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c_ww errored\n", r->label); + unlink(src); unlink(cs); + return -1; + } + + FILE *fc = fopen(cs, "rb"); + FILE *fw = fopen(ws, "rb"); + int rc = 0; + if (!fc || !fw) { + rc = -1; + } else { + for (;;) { + int a = fgetc(fc); + int b = fgetc(fw); + if (a != b) { rc = -1; break; } + if (a == EOF) break; + } + } + if (fc) fclose(fc); + if (fw) fclose(fw); + if (rc != 0) + fprintf(stderr, "row[%s]: cstage vs wwstage asm differs\n", + r->label); + unlink(src); unlink(cs); unlink(ws); + return rc; +} + +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_global_field: 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_global_field[%s][%s]: exit=%d want=%d\n", + drivers[d].name, rows[i].label, + got, rows[i].want); + fail++; + } + } + } + + if (access(wdrv, X_OK) == 0) { + for (int i = 0; i < n; i++) { + total++; + if (asm_byte_identical(bin, &rows[i], i) != 0) + fail++; + } + } + + if (fail) { + fprintf(stderr, + "match_global_field: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("match_global_field: %d/%d ok\n", total, total); + return 0; +}