From 533333bd1af6826dfd933c05827c8cd0c263940f Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sun, 14 Jun 2026 11:59:11 +0900 Subject: [PATCH] wcc/ww: match on a tagged struct-field scrutinee reads it in place (#25) wwstage cgmatch unconditionally spilled any non-ident match scrutinee -- including an addressable BP-relative N_DOT struct field -- into @match_spill and dispatched off the copy (frame $48); cstage reads such a field in place at its BP offset ($32). Both stages were already runtime-correct (latent rule-10 leanness, not a miscompile); this aligns wwstage down to cstage so the asm is byte-identical. The new in-place arm mirrors cstage cgen.c:10241-10296 verbatim: an N_DOT scrutinee with a bare N_IDENT base whose type chases to a value TY_STRUCT and whose field is found by name reads tag/payload at localfind(base)+field.offset. The *ptr-field and call-result cases stay on the spill path by construction (their base does not chase to TY_STRUCT) -- no extra guard. A global value-struct base mis-resolves identically in both stages (localfind returns 0); left untouched as a shared latent (#29), since a ww-only guard would break byte-id. Regenerates the w6c and wwdump combined.ww. Table-driven 831 test: 6 rows (local-field, *ptr-field, plain-ident, call-result, payload remap, str payload) x runtime-both-stages + cs-vs-ww byte-id. --- Makefile | 7 + selfhost/cmd/w6c/main.combined.ww | 36 ++++ selfhost/cmd/wcc/cgenexpr.ww | 36 ++++ selfhost/cmd/wwdump/main.combined.ww | 36 ++++ test/wcc/831_match_field_inplace.c | 300 +++++++++++++++++++++++++++ 5 files changed, 415 insertions(+) create mode 100644 test/wcc/831_match_field_inplace.c diff --git a/Makefile b/Makefile index ef3962ec..fa5d758f 100644 --- a/Makefile +++ b/Makefile @@ -341,6 +341,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_tagged_return_scratch \ $(BIN)/test_tagged_widen_f64 \ $(BIN)/test_match_spill_pointer_payload \ + $(BIN)/test_match_field_inplace \ $(BIN)/test_struct_byval_param \ $(BIN)/test_struct_multi_return_scratch \ $(BIN)/test_signed_data_emit \ @@ -2113,6 +2114,12 @@ $(BIN)/test_match_spill_pointer_payload: test/wcc/716_match_spill_pointer_payloa $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_match_field_inplace: test/wcc/831_match_field_inplace.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/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index b4986eeb..edfb423f 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -26298,6 +26298,41 @@ fn cgmatch(c: *cgen, n: *node) void = { }; }; }; } else { + // M1 (#25): match on a tagged field of a BARE-ident VALUE + // struct reads the box IN PLACE at base.off + field.offset + // — the box (tag@+0, word0@+8, word1@+16) is contiguous in + // the parent frame, so no @match_spill copy. Verbatim mirror + // of cstage N_MATCH's in-place arm (cmd/w6c/cgen.c:10241- + // 10296): gate on N_DOT with a bare-IDENT base whose stamped + // 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. + let mfld: *tfield = nil; + if (scrut.kind == nkind.N_DOT && scrut.lhs != nil + && scrut.lhs.kind == nkind.N_IDENT + && scrut.lhs.type_ != nil) { + let bu: *tinfo = tichase(scrut.lhs.type_: *tinfo); + if (bu != nil && bu.kind == tykind.TY_STRUCT) { + let fl: *tfield = bu.fields; + for (fl != nil) { + if (streq(fl.name, scrut.str)) { + mfld = fl; + break; + }; + fl = fl.tnext; + }; + }; + }; + if (mfld != nil) { + let foff: i32 = mfld.offset: i32; + scrutoff = localfind(c, scrut.lhs.str) + foff; + scrutt = matchscrutt(c, scrut); + } else { // Non-ident scrutinee (call result, arr[i], p.field, // ?, etc.). Spill into an `@match_spill` scratch slot // and dispatch off it. Tagged returns (N_CALL) follow @@ -26398,6 +26433,7 @@ fn cgmatch(c: *cgen, n: *node) void = { }; }; }; }; + }; }; }; let endl: str = mklabel(c, "match_end"); diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 78dfe76e..124f8df2 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -2952,6 +2952,41 @@ fn cgmatch(c: *cgen, n: *node) void = { }; }; }; } else { + // M1 (#25): match on a tagged field of a BARE-ident VALUE + // struct reads the box IN PLACE at base.off + field.offset + // — the box (tag@+0, word0@+8, word1@+16) is contiguous in + // the parent frame, so no @match_spill copy. Verbatim mirror + // of cstage N_MATCH's in-place arm (cmd/w6c/cgen.c:10241- + // 10296): gate on N_DOT with a bare-IDENT base whose stamped + // 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. + let mfld: *tfield = nil; + if (scrut.kind == nkind.N_DOT && scrut.lhs != nil + && scrut.lhs.kind == nkind.N_IDENT + && scrut.lhs.type_ != nil) { + let bu: *tinfo = tichase(scrut.lhs.type_: *tinfo); + if (bu != nil && bu.kind == tykind.TY_STRUCT) { + let fl: *tfield = bu.fields; + for (fl != nil) { + if (streq(fl.name, scrut.str)) { + mfld = fl; + break; + }; + fl = fl.tnext; + }; + }; + }; + if (mfld != nil) { + let foff: i32 = mfld.offset: i32; + scrutoff = localfind(c, scrut.lhs.str) + foff; + scrutt = matchscrutt(c, scrut); + } else { // Non-ident scrutinee (call result, arr[i], p.field, // ?, etc.). Spill into an `@match_spill` scratch slot // and dispatch off it. Tagged returns (N_CALL) follow @@ -3052,6 +3087,7 @@ fn cgmatch(c: *cgen, n: *node) void = { }; }; }; }; + }; }; }; let endl: str = mklabel(c, "match_end"); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 4332a4e7..48d8a1c6 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -26298,6 +26298,41 @@ fn cgmatch(c: *cgen, n: *node) void = { }; }; }; } else { + // M1 (#25): match on a tagged field of a BARE-ident VALUE + // struct reads the box IN PLACE at base.off + field.offset + // — the box (tag@+0, word0@+8, word1@+16) is contiguous in + // the parent frame, so no @match_spill copy. Verbatim mirror + // of cstage N_MATCH's in-place arm (cmd/w6c/cgen.c:10241- + // 10296): gate on N_DOT with a bare-IDENT base whose stamped + // 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. + let mfld: *tfield = nil; + if (scrut.kind == nkind.N_DOT && scrut.lhs != nil + && scrut.lhs.kind == nkind.N_IDENT + && scrut.lhs.type_ != nil) { + let bu: *tinfo = tichase(scrut.lhs.type_: *tinfo); + if (bu != nil && bu.kind == tykind.TY_STRUCT) { + let fl: *tfield = bu.fields; + for (fl != nil) { + if (streq(fl.name, scrut.str)) { + mfld = fl; + break; + }; + fl = fl.tnext; + }; + }; + }; + if (mfld != nil) { + let foff: i32 = mfld.offset: i32; + scrutoff = localfind(c, scrut.lhs.str) + foff; + scrutt = matchscrutt(c, scrut); + } else { // Non-ident scrutinee (call result, arr[i], p.field, // ?, etc.). Spill into an `@match_spill` scratch slot // and dispatch off it. Tagged returns (N_CALL) follow @@ -26398,6 +26433,7 @@ fn cgmatch(c: *cgen, n: *node) void = { }; }; }; }; + }; }; }; let endl: str = mklabel(c, "match_end"); diff --git a/test/wcc/831_match_field_inplace.c b/test/wcc/831_match_field_inplace.c new file mode 100644 index 00000000..9950d41e --- /dev/null +++ b/test/wcc/831_match_field_inplace.c @@ -0,0 +1,300 @@ +/* + * 831_match_field_inplace — match on a tagged-union struct-FIELD + * scrutinee. wwstage cgmatch reads an addressable BP-relative field + * (`match (b.t)`, b a local VALUE struct) IN PLACE at base.off + + * field.offset, dispatching off the field's own slot — no @match_spill + * copy. Verbatim mirror of cstage N_MATCH's in-place arm + * (cmd/w6c/cgen.c:10241-10296). Task #25 (M1). + * + * Pre-fix (wwstage): the non-ident cgmatch branch unconditionally + * spilled ANY non-ident scrutinee — including an addressable field — + * into @match_spill (+16B scratch → frame TEXT $48). cstage read the + * field where it lives (TEXT $32). Both stages were runtime-correct + * (ken's battery), so this was a latent rule-10 leanness gap, not a + * miscompile; the fix aligns wwstage DOWN to cstage so both emit + * byte-identical asm. + * + * Predicate (copied from cstage): in-place iff the scrutinee is an + * N_DOT whose lhs is a BARE N_IDENT with a stamped type, that type + * chases to TY_STRUCT, and the field resolves by name. Everything else + * — a *ptr-field base (`match (h.t)`, h:*struct, chases to TY_PTR so + * the by-name scan misses), a plain-local tagged ident, a call-result + * scrutinee — keeps its existing (in-place-ident / spill) path, + * unchanged. + * + * What this table pins, per row, across BOTH driver stages plus a + * cstage-vs-wwstage asm byte-identity check: + * (a) local VALUE-struct tagged field [NEW in-place arm] + * (b) *ptr-struct tagged field [stays on spill — TY_PTR] + * (c) plain-local tagged ident [unchanged in-place-ident] + * (d) call-result tagged scrutinee [spill] + * (e) bool->int payload remap on a field [width: 1-word variants] + * (f) str-payload (24B box) on a field [width: 3-word variant] + * + * The boundary, not just the new arm, is the subject: a regression + * that mis-routed (b)/(c)/(d) into the in-place arm, or failed to route + * (a)/(e)/(f) there, would break either the runtime exit code or the + * byte-identity. The $48->$32 frame convergence itself is ken's + * authoritative byte-id bind; a runtime test cannot observe frame size. + */ +#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) NEW in-place arm: local VALUE struct, field reassigned to + * the int variant, matched in place. Returns 42. */ + { "local_field_int_inplace", + "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 }, + /* (b) *ptr-struct field: base chases to TY_PTR, the by-name scan + * misses, scrutinee falls to the spill path — exactly as cstage, + * no separate deref guard. Returns 9. */ + { "ptr_field_stays_spill", + "package main;\n" + "type box = struct { t: (int | bool), n: int };\n" + "fn pick(h: *box) i32 = {\n" + " match (h.t) {\n" + " case let n: int => return n: i32;\n" + " case bool => return 1;\n" + " };\n" + "};\n" + "fn main() i32 = {\n" + " let b: box = box { t = 9, n = 5 };\n" + " return pick(&b);\n" + "};\n", + 9 }, + /* (c) plain-local tagged ident: unchanged in-place-ident path + * (not N_DOT). Returns 5. */ + { "plain_local_ident", + "package main;\n" + "fn main() i32 = {\n" + " let t: (int | bool) = 5;\n" + " match (t) {\n" + " case let n: int => return n: i32;\n" + " case bool => return 1;\n" + " };\n" + "};\n", + 5 }, + /* (d) call-result tagged scrutinee: not N_DOT, keeps the spill + * path. Returns 7. */ + { "call_result_spill", + "package main;\n" + "fn mk(b: bool) (int | bool) = {\n" + " if (b) { return 7; };\n" + " return false;\n" + "};\n" + "fn main() i32 = {\n" + " match (mk(true)) {\n" + " case let n: int => return n: i32;\n" + " case bool => return 1;\n" + " };\n" + "};\n", + 7 }, + /* (e) width: a field whose variant remaps from bool to int. The + * in-place arm reads tag + one payload word at the field slot; + * the int arm also reads a sibling field (b.n) to prove the + * surrounding struct frame is undisturbed. Returns 7 + 5 = 12. */ + { "field_bool_to_int_remap", + "package main;\n" + "type box = struct { t: (int | bool), n: int };\n" + "fn main() i32 = {\n" + " let b: box = box { t = true, n = 5 };\n" + " b.t = 7;\n" + " match (b.t) {\n" + " case let n: int => return (n + b.n): i32;\n" + " case bool => return 1;\n" + " };\n" + "};\n", + 12 }, + /* (f) width: a 24B str-payload variant read in place at the + * field slot (tag@+0, .ptr@+8, .len@+16). Returns len("hello") + * = 5. */ + { "field_str_payload_24B", + "package main;\n" + "type box = struct { t: (str | int), n: int };\n" + "fn main() i32 = {\n" + " let b: box = box { t = 0, n = 5 };\n" + " b.t = \"hello\";\n" + " match (b.t) {\n" + " case let s: str => return len(s): i32;\n" + " case int => return 99;\n" + " };\n" + "};\n", + 5 }, +}; + +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/wcmfi_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/wcmfi_%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 whole point of M1 is that wwstage's frame + * stops bloating for an addressable field scrutinee, so the cstage vs + * wwstage text output must match byte-for-byte 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/wcmfi_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/wcmfi_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/wcmfi_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_field_inplace: 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_field_inplace[%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_field_inplace: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("match_field_inplace: %d/%d ok\n", total, total); + return 0; +}