From db9edb7c39728fe7e4ef50b2c6ad95b306e80f68 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sun, 31 May 2026 23:34:16 +0900 Subject: [PATCH] wwstage: classify tagged call-source by stamped result type, not callee leaf name (#211) rhstaggedabicall keyed the tagged-vs-scalar call-source decision off the callee result type looked up by leaf NAME (fnretlookupmod), with the receiver variable used as the "module". A value-receiver fn-ptr field call s.f(...) whose leaf collides with a same-named global fn then mis-bound the global's register shape, so the source was misclassified as scalar and widened wrong: silent cs/ww asm divergence and wrong runtime. Read the checker-stamped N_CALL result type (src.type_) instead, mirroring the N_DOT sister branch. cstage already reads u->ret off the typed callee (cmd/wcc/check.c:1490) and harec selects by interned type id, not name (ref/harec/src/types.c:714). Graduates test/wcc/782 to STAGE_WW + byte_id. --- Makefile | 6 +- selfhost/cmd/w6c/main.combined.ww | 36 ++---- selfhost/cmd/wcc/cgenutil.ww | 36 ++---- selfhost/cmd/wwdump/main.combined.ww | 36 ++---- test/wcc/782_fieldfn_leaf_collide_run.c | 151 +++++++++++++++++------- 5 files changed, 143 insertions(+), 122 deletions(-) diff --git a/Makefile b/Makefile index c3e89c29..96335e0e 100644 --- a/Makefile +++ b/Makefile @@ -730,10 +730,12 @@ $(BIN)/test_fmt_handle_run: test/wcc/777_fmt_handle_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< -# 782 — cstage-only pin for #211 (cgen sibling of #208). Self-contained -# single-file probe (no lib imports); cstage driver only, so no ww_ww dep. +# 782 — #211 close (cgen sibling of #208). Self-contained single-file +# probe (no lib imports). Graduated STAGE_CS | STAGE_WW + byte-id on +# #211 close: build + run on both stages + cs.s == ww.s (rule-10). $(BIN)/test_fieldfn_leaf_collide_run: test/wcc/782_fieldfn_leaf_collide_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 $@ $< diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 7a5db3ef..db4d3bb7 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -17462,31 +17462,17 @@ fn rhstaggedident(c: *cgen, src: *node) *node = { fn rhstaggedabicall(c: *cgen, src: *node) bool = { if (src == nil) { return false; }; if (src.kind == nkind.N_CALL) { - let callee: *node = src.lhs; - if (callee != nil) { - let calleename: str; - calleename.ptr = nil; calleename.len = 0; - let cmod: str; - cmod.ptr = nil; cmod.len = 0; - if (callee.kind == nkind.N_IDENT) { - calleename = callee.str; - cmod = c.curmod; - }; - if (callee.kind == nkind.N_DOT) { - calleename = callee.str; - if (callee.lhs != nil) { - if (callee.lhs.kind == nkind.N_IDENT) { - cmod = callee.lhs.str; - }; - }; - }; - if (calleename.len > 0) { - let rtyp: *node = fnretlookupmod(c, calleename, cmod); - if (rtyp != nil) { - if (istaggedtype(c, rtyp)) { return true; }; - }; - }; - }; + // #211: a call's result type is the CALLEE's fn-type ret, which + // the checker stamps onto this N_CALL node (check.ww N_CALL: both + // the SK_FN path and the fn-VALUE/field path set e.type_ = the + // result tinfo). Read it directly — a value-receiver fn-ptr FIELD + // call `s.f(...)` keyed by the field leaf `f` with the receiver + // VARIABLE name as the "module" otherwise mis-binds a same-named + // GLOBAL fn of different register shape (silent cs≠ww). cstage's + // sister reads u->ret off the callee type (cmd/wcc/check.c:1433, + // :1490); harec selects by interned type id, not name (ref/harec/ + // src/types.c:714). Mirrors the N_DOT branch below. + if (typeistagged(src.type_: *tinfo)) { return true; }; return false; }; if (src.kind == nkind.N_INDEX) { diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 701d4a55..9e7a9e4b 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -2514,31 +2514,17 @@ fn rhstaggedident(c: *cgen, src: *node) *node = { fn rhstaggedabicall(c: *cgen, src: *node) bool = { if (src == nil) { return false; }; if (src.kind == nkind.N_CALL) { - let callee: *node = src.lhs; - if (callee != nil) { - let calleename: str; - calleename.ptr = nil; calleename.len = 0; - let cmod: str; - cmod.ptr = nil; cmod.len = 0; - if (callee.kind == nkind.N_IDENT) { - calleename = callee.str; - cmod = c.curmod; - }; - if (callee.kind == nkind.N_DOT) { - calleename = callee.str; - if (callee.lhs != nil) { - if (callee.lhs.kind == nkind.N_IDENT) { - cmod = callee.lhs.str; - }; - }; - }; - if (calleename.len > 0) { - let rtyp: *node = fnretlookupmod(c, calleename, cmod); - if (rtyp != nil) { - if (istaggedtype(c, rtyp)) { return true; }; - }; - }; - }; + // #211: a call's result type is the CALLEE's fn-type ret, which + // the checker stamps onto this N_CALL node (check.ww N_CALL: both + // the SK_FN path and the fn-VALUE/field path set e.type_ = the + // result tinfo). Read it directly — a value-receiver fn-ptr FIELD + // call `s.f(...)` keyed by the field leaf `f` with the receiver + // VARIABLE name as the "module" otherwise mis-binds a same-named + // GLOBAL fn of different register shape (silent cs≠ww). cstage's + // sister reads u->ret off the callee type (cmd/wcc/check.c:1433, + // :1490); harec selects by interned type id, not name (ref/harec/ + // src/types.c:714). Mirrors the N_DOT branch below. + if (typeistagged(src.type_: *tinfo)) { return true; }; return false; }; if (src.kind == nkind.N_INDEX) { diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 08f7fef9..29751068 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -17462,31 +17462,17 @@ fn rhstaggedident(c: *cgen, src: *node) *node = { fn rhstaggedabicall(c: *cgen, src: *node) bool = { if (src == nil) { return false; }; if (src.kind == nkind.N_CALL) { - let callee: *node = src.lhs; - if (callee != nil) { - let calleename: str; - calleename.ptr = nil; calleename.len = 0; - let cmod: str; - cmod.ptr = nil; cmod.len = 0; - if (callee.kind == nkind.N_IDENT) { - calleename = callee.str; - cmod = c.curmod; - }; - if (callee.kind == nkind.N_DOT) { - calleename = callee.str; - if (callee.lhs != nil) { - if (callee.lhs.kind == nkind.N_IDENT) { - cmod = callee.lhs.str; - }; - }; - }; - if (calleename.len > 0) { - let rtyp: *node = fnretlookupmod(c, calleename, cmod); - if (rtyp != nil) { - if (istaggedtype(c, rtyp)) { return true; }; - }; - }; - }; + // #211: a call's result type is the CALLEE's fn-type ret, which + // the checker stamps onto this N_CALL node (check.ww N_CALL: both + // the SK_FN path and the fn-VALUE/field path set e.type_ = the + // result tinfo). Read it directly — a value-receiver fn-ptr FIELD + // call `s.f(...)` keyed by the field leaf `f` with the receiver + // VARIABLE name as the "module" otherwise mis-binds a same-named + // GLOBAL fn of different register shape (silent cs≠ww). cstage's + // sister reads u->ret off the callee type (cmd/wcc/check.c:1433, + // :1490); harec selects by interned type id, not name (ref/harec/ + // src/types.c:714). Mirrors the N_DOT branch below. + if (typeistagged(src.type_: *tinfo)) { return true; }; return false; }; if (src.kind == nkind.N_INDEX) { diff --git a/test/wcc/782_fieldfn_leaf_collide_run.c b/test/wcc/782_fieldfn_leaf_collide_run.c index 0de644f4..090ab700 100644 --- a/test/wcc/782_fieldfn_leaf_collide_run.c +++ b/test/wcc/782_fieldfn_leaf_collide_run.c @@ -1,25 +1,28 @@ /* - * 782_fieldfn_leaf_collide_run — cstage-only pin for project #211, the - * cgen sibling of #208 (the checker fix). + * 782_fieldfn_leaf_collide_run — project #211 close, the cgen sibling of + * #208 (the checker fix). * * SHAPE: a value-receiver fn-pointer FIELD call `s.pull(...)` whose leaf * name `pull` COLLIDES with a same-module GLOBAL fn `pull` of a DIFFERENT * register shape — the field returns a tagged `(i64 | sentinel)` (2-word * AX=tag/DX=word0 ABI), the global returns a scalar `i64` (1-word AX). * cstage resolves the call result from the CALLEE's own type (the field's - * fn type), so it reads the tagged 2-word return correctly. wwstage cgen - * re-derives the return type by NAME (fnretlookup over the leaf, with the - * receiver VARIABLE name as the "module"), mis-binds the scalar global, - * and widens a 1-word AX into the tagged slot — a silent cs≠ww miscompile - * (wrong runtime + divergent .s). See selfhost/cmd/wcc/cgen.ww fnretlookup. + * fn type, cmd/wcc/check.c:1433/1490 `n->type = u->ret`), so it reads the + * tagged 2-word return correctly. PRE-#211 wwstage cgen re-derived the + * source shape by NAME (rhstaggedabicall → fnretlookupmod over the leaf, + * with the receiver VARIABLE name as the "module"), mis-bound the scalar + * global, and widened a 1-word AX into the tagged slot — a silent cs≠ww + * miscompile (wrong runtime + divergent .s). * - * WHY cstage-only (carve-out idiom, mirror of 777/780/781): exercising - * this row under wwstage would trip #211 (asm differs, runtime wrong), so - * STAGE_WW + byte_id are withheld until #211 closes. The row pins the - * cstage-correct behaviour (the spec) so #211's fix is a graduation, not - * a regression. #211 was MASKED until #208 landed: pre-#208 the wwstage - * checker rejected this shape ("is/as: operand is not a tagged union") - * before cgen ran, so the cgen path was unreachable. + * THE FIX (#211, align wwstage UP to cstage, structural): rhstaggedabicall + * (selfhost/cmd/wcc/cgenutil.ww) reads the checker-stamped result type off + * the N_CALL node (`src.type_`, which check.ww's N_CALL stamps to the + * callee fn-type's ret in both the SK_FN and the fn-VALUE/field paths) + * instead of the leaf-name lookup. Mirrors harec selecting by interned + * type id, not name (ref/harec/src/types.c:714). #211 was MASKED until + * #208 landed: pre-#208 the wwstage checker rejected this shape ("is/as: + * operand is not a tagged union") before cgen ran, so the cgen path was + * unreachable. * * row | what it pins * ---------------------+---------------------------------------------- @@ -30,10 +33,10 @@ * | global stays live (g = pull(3) = 14) so the * | collision is real, not dead-code-elided. * - * GATE POLARITY: must stay GREEN. A red means the cstage call-result - * resolution regressed on a value-receiver fn-ptr field call. - * - * GRADUATES to STAGE_CS | STAGE_WW + byte_id on #211 close. + * Graduated to STAGE_CS | STAGE_WW + byte_id on #211 close: builds + runs + * on both stages (exit 42) and asserts cs.s == ww.s (rule-10). A red means + * #211 regressed — wwstage re-derived the call-result shape by leaf name + * again, or the cstage call-result resolution broke. */ #include #include @@ -58,7 +61,7 @@ struct row { const char *src; int want_exit; int stage_mask; - int byte_id; + int byte_id; /* assert cs.s == ww.s */ }; static const struct row rows[] = { @@ -85,7 +88,7 @@ static const struct row rows[] = { " return 1i32;\n" "};\n", 42, - STAGE_CS, 0 }, + STAGE_CS | STAGE_WW, 1 }, }; static int @@ -99,12 +102,11 @@ write_source(const char *path, const char *src) } /* Per-row tmpdir cleanup. ww_ww writes intermediates next to the source - * (filed task #15); cstage ww does too. Sweep then rmdir. Mirror of - * 777's cleanup_tmp. */ + * (filed task #15); cstage ww does too. Sweep then rmdir. */ static void cleanup_tmp(const char *tmpdir, const char *base) { - char p[640]; + char p[1024]; snprintf(p, sizeof p, "%s/%s.ww", tmpdir, base); unlink(p); snprintf(p, sizeof p, "%s/%s.s", tmpdir, base); unlink(p); snprintf(p, sizeof p, "%s/%s.o", tmpdir, base); unlink(p); @@ -114,18 +116,18 @@ cleanup_tmp(const char *tmpdir, const char *base) } static int -build_via_driver(const char *driver, const char *tmpdir, const char *cwd, - const char *src) +build_via_driver(const char *driver, const char *tmpdir, const char *src) { char cmd[2048]; - snprintf(cmd, sizeof cmd, - "cd %s && timeout 180 %s build -I %s/lib %s 2>/dev/null", - tmpdir, driver, cwd, src); + snprintf(cmd, sizeof cmd, "cd %s && timeout 180 %s build %s 2>/dev/null", + tmpdir, driver, src); return runwait(cmd); } +/* run_row — build via driver, run the binary, return exit (or -1 on + * build failure). */ static int -run_row(const char *driver, const char *cwd, const struct row *r, int seq) +run_row(const char *driver, const struct row *r, int seq) { char tmpdir[256], src[512], base[64], outbin[768]; snprintf(tmpdir, sizeof tmpdir, "/tmp/ffl_%d_d_%d", getpid(), seq); @@ -134,8 +136,7 @@ run_row(const char *driver, const char *cwd, const struct row *r, int seq) mkdir(tmpdir, 0755); if (write_source(src, r->src) != 0) { cleanup_tmp(tmpdir, base); return -1; } int rc; - int br = build_via_driver(driver, tmpdir, cwd, src); - if (br == 0) { + if (build_via_driver(driver, tmpdir, src) == 0) { snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); rc = runwait(outbin); } else { @@ -145,6 +146,49 @@ run_row(const char *driver, const char *cwd, const struct row *r, int seq) return rc; } +/* asm_byte_identical — diff cstage vs wwstage .s. Parallel trees so + * ww_ww writing intermediates next to the source doesn't clobber the + * cstage .s (CLAUDE.md rule 14 phase split). Mirror of 783's helper. */ +static int +asm_byte_identical(const char *cdrv, const char *wdrv, const struct row *r, + int seq) +{ + char src[512], tdc[256], tdw[256], base[64], cs[512], ws[512]; + snprintf(tdc, sizeof tdc, "/tmp/ffl_%d_c_%d", getpid(), seq); + snprintf(tdw, sizeof tdw, "/tmp/ffl_%d_w_%d", getpid(), seq); + snprintf(base, sizeof base, "main782"); + mkdir(tdc, 0755); + mkdir(tdw, 0755); + snprintf(src, sizeof src, "%s/%s.ww", tdc, base); + if (write_source(src, r->src) != 0) { cleanup_tmp(tdc, base); cleanup_tmp(tdw, base); return -1; } + int rc = -1; + if (build_via_driver(cdrv, tdc, src) != 0) goto out; + snprintf(cs, sizeof cs, "%s/%s.s", tdc, base); + + snprintf(src, sizeof src, "%s/%s.ww", tdw, base); + if (write_source(src, r->src) != 0) goto out; + if (build_via_driver(wdrv, tdw, src) != 0) goto out; + snprintf(ws, sizeof ws, "%s/%s.s", tdw, base); + + FILE *fc = fopen(cs, "rb"); + FILE *fw = fopen(ws, "rb"); + if (fc && fw) { + rc = 0; + 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); +out: + cleanup_tmp(tdc, base); + cleanup_tmp(tdw, base); + return rc; +} + int main(void) { @@ -158,31 +202,48 @@ main(void) bin = absbin; } - char cdrv[640]; - snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + char cdrv[640], wdrv[640]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); int n = (int)(sizeof rows / sizeof rows[0]); - int total = 0, fail = 0; - int seq = 0; + int total = 0, fail = 0, seq = 0; + int wwpresent = (access(wdrv, X_OK) == 0); for (int i = 0; i < n; i++) { - /* #211: cstage-only carve-out. No STAGE_WW row until #211 - * (cgen name-keyed call-return mis-resolution) closes. */ - if (rows[i].stage_mask & STAGE_CS) { + const struct row *r = &rows[i]; + + if (r->stage_mask & STAGE_CS) { total++; - int got = run_row(cdrv, cwd, &rows[i], seq++); - if (got != rows[i].want_exit) { - fprintf(stderr, - "fieldfn_leaf_collide[cs][%s]: exit=%d want=%d\n", - rows[i].label, got, rows[i].want_exit); + int got = run_row(cdrv, r, seq++); + if (got != r->want_exit) { + fprintf(stderr, "fieldfn_leaf_collide[cs][%s]: exit=%d want=%d\n", + r->label, got, r->want_exit); fail++; } } + + if (wwpresent && (r->stage_mask & STAGE_WW)) { + total++; + int got = run_row(wdrv, r, seq++); + if (got != r->want_exit) { + fprintf(stderr, "fieldfn_leaf_collide[ww][%s]: exit=%d want=%d\n", + r->label, got, r->want_exit); + fail++; + } + if (r->byte_id) { + total++; + if (asm_byte_identical(cdrv, wdrv, r, seq++) != 0) { + fprintf(stderr, "fieldfn_leaf_collide[byte-id][%s]: cstage vs wwstage asm differs\n", + r->label); + fail++; + } + } + } } if (fail) { - fprintf(stderr, "fieldfn_leaf_collide: %d/%d fixtures failed\n", - fail, total); + fprintf(stderr, "fieldfn_leaf_collide: %d/%d checks failed\n", fail, total); return 1; } printf("fieldfn_leaf_collide: %d/%d ok\n", total, total);