From b2ac8cbf81892d607de56d1081b99b77b2e7ed0e Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 28 May 2026 23:17:22 +0900 Subject: [PATCH] wcc: peel N_TNAME alias chain on cgdot receiver before lkind decision (#191) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `type vs = *vt; fn(s: vs) s.field` linked-failed in wwstage with `undefined reference to field' — cgdot read lc.tnode.kind without first walking N_TNAME aliases, so lkind stayed N_TNAME (not the underlying N_TPTR), structlookupchain missed (`vs` isn't a struct alias), and the lookup fell through to the SB-global fallback that emits `MOVQ (SB), AX`. Mirror cstage type_chase_named (cmd/w6c/cgen.c:144-155) via an aliaslookup loop, stopping at struct aliases so the existing direct-struct N_TNAME arm stays byte-id with pre-fix #22 callers. LOOP (not single-peel) — Phase-N builds N_TNAME chains (project_tinfo_lossy_nominal), depth-2+ aliases require iteration. Inner peel on the pointee is unnecessary: the existing structlookupchain already walks N_TNAME chains via aliaslookup (cgenutil.ww:1266-1276); row 4 of probe 769 proves the inner-chain depth-3 path stays green without an explicit inner peel. Probe test/wcc/769_dot_aliased_ptr.c covers 4 rows (fn-param read, let-binding read, double-alias receiver, pointee-alias chain), per-row runtime + byte-id gates. Files inline two sibling bugs surfaced during impl (cgassign write-side silent-drop, chained-N_DOT spine link-fail) plus a cstage checker assignability gap on chain-depth-2 aliases — all out-of-scope per rule 11 split. --- Makefile | 7 + selfhost/cmd/w6c/main.combined.ww | 20 +- selfhost/cmd/wcc/cgenexpr.ww | 20 +- selfhost/cmd/wwdump/main.combined.ww | 20 +- test/wcc/769_dot_aliased_ptr.c | 346 +++++++++++++++++++++++++++ 5 files changed, 407 insertions(+), 6 deletions(-) create mode 100644 test/wcc/769_dot_aliased_ptr.c diff --git a/Makefile b/Makefile index 37056351..f4d1ea2f 100644 --- a/Makefile +++ b/Makefile @@ -319,6 +319,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_star_fn_deref_call \ $(BIN)/test_match_variant_dispatch \ $(BIN)/test_io_types_run \ + $(BIN)/test_dot_aliased_ptr \ $(BIN)/test_use_promote_alias \ $(BIN)/test_field_signed $(BIN)/test_frame_argcount \ $(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \ @@ -639,6 +640,12 @@ $(BIN)/test_io_types_run: test/wcc/768_io_types_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_dot_aliased_ptr: test/wcc/769_dot_aliased_ptr.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_arrlit_str_full: test/wcc/711_arrlit_str_full.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 14d1bd1a..83d63ab4 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -19284,8 +19284,24 @@ fn cgdot(c: *cgen, n: *node) void = { let lc: *local = localfindnode(c, nm); if (lc != nil) { let tn: *node = lc.tnode; - let lkind: nkind = nkind.N_NONE; - if (tn != nil) { lkind = tn.kind; }; + // Receiver is a NAMED alias chain — peel via aliaslookup + // until tn exposes a non-N_TNAME kind (or a struct alias). + // Without this, `type vs = *vt` leaves lkind == N_TNAME and + // structlookupchain misses (vs is not a struct), so we fall + // through to the SB-global fallback and emit a wrong + // `MOVQ (SB), AX`. Mirror cstage type_chase_named + // (cmd/w6c/cgen.c:144-155); LOOP, not single-peel — Phase-N + // builds NAMED chains (project_tinfo_lossy_nominal). Stops + // at struct aliases so the existing direct-struct arm below + // stays byte-id with pre-fix #22 callers. #191. + for (tn != nil && tn.kind == nkind.N_TNAME) { + if (structlookup(c, tn.str) != nil) { break; }; + let nx: *node = aliaslookup(c, tn.str); + if (nx == nil) { break; }; + tn = nx; + }; + if (tn == nil) { return; }; + let lkind: nkind = tn.kind; // Pointer-to-struct: deref then field load. if (lkind == nkind.N_TPTR) { let inner: *node = tn.lhs; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index d80a83c4..1d2b8433 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -1672,8 +1672,24 @@ fn cgdot(c: *cgen, n: *node) void = { let lc: *local = localfindnode(c, nm); if (lc != nil) { let tn: *node = lc.tnode; - let lkind: nkind = nkind.N_NONE; - if (tn != nil) { lkind = tn.kind; }; + // Receiver is a NAMED alias chain — peel via aliaslookup + // until tn exposes a non-N_TNAME kind (or a struct alias). + // Without this, `type vs = *vt` leaves lkind == N_TNAME and + // structlookupchain misses (vs is not a struct), so we fall + // through to the SB-global fallback and emit a wrong + // `MOVQ (SB), AX`. Mirror cstage type_chase_named + // (cmd/w6c/cgen.c:144-155); LOOP, not single-peel — Phase-N + // builds NAMED chains (project_tinfo_lossy_nominal). Stops + // at struct aliases so the existing direct-struct arm below + // stays byte-id with pre-fix #22 callers. #191. + for (tn != nil && tn.kind == nkind.N_TNAME) { + if (structlookup(c, tn.str) != nil) { break; }; + let nx: *node = aliaslookup(c, tn.str); + if (nx == nil) { break; }; + tn = nx; + }; + if (tn == nil) { return; }; + let lkind: nkind = tn.kind; // Pointer-to-struct: deref then field load. if (lkind == nkind.N_TPTR) { let inner: *node = tn.lhs; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 8f6ea18c..d5aa2477 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -19284,8 +19284,24 @@ fn cgdot(c: *cgen, n: *node) void = { let lc: *local = localfindnode(c, nm); if (lc != nil) { let tn: *node = lc.tnode; - let lkind: nkind = nkind.N_NONE; - if (tn != nil) { lkind = tn.kind; }; + // Receiver is a NAMED alias chain — peel via aliaslookup + // until tn exposes a non-N_TNAME kind (or a struct alias). + // Without this, `type vs = *vt` leaves lkind == N_TNAME and + // structlookupchain misses (vs is not a struct), so we fall + // through to the SB-global fallback and emit a wrong + // `MOVQ (SB), AX`. Mirror cstage type_chase_named + // (cmd/w6c/cgen.c:144-155); LOOP, not single-peel — Phase-N + // builds NAMED chains (project_tinfo_lossy_nominal). Stops + // at struct aliases so the existing direct-struct arm below + // stays byte-id with pre-fix #22 callers. #191. + for (tn != nil && tn.kind == nkind.N_TNAME) { + if (structlookup(c, tn.str) != nil) { break; }; + let nx: *node = aliaslookup(c, tn.str); + if (nx == nil) { break; }; + tn = nx; + }; + if (tn == nil) { return; }; + let lkind: nkind = tn.kind; // Pointer-to-struct: deref then field load. if (lkind == nkind.N_TPTR) { let inner: *node = tn.lhs; diff --git a/test/wcc/769_dot_aliased_ptr.c b/test/wcc/769_dot_aliased_ptr.c new file mode 100644 index 00000000..62e4650b --- /dev/null +++ b/test/wcc/769_dot_aliased_ptr.c @@ -0,0 +1,346 @@ +/* + * 769_dot_aliased_ptr — project #191: wwstage cgdot N_DOT on an + * aliased-pointer receiver. `type vt = struct{...}; type vs = *vt; + * fn f(s: vs) { s.field }` — wwstage's cgdot read the receiver's + * tnode kind WITHOUT first peeling N_TNAME alias chains, so lkind + * stayed N_TNAME (not the underlying N_TPTR), structlookupchain + * missed (`vs` isn't a struct alias), and the lookup fell through + * to the SB-global fallback. The emitted asm was + * MOVQ field(SB), AX + * which links as an undefined symbol — runtime never reached. + * + * Cstage (cmd/w6c/cgen.c:7001-7004) uses type_chase_named to walk + * TY_NAMED.under to the underlying TY_PTR / TY_STRUCT before the + * kind-gated arms fire. Mirror: a loop via `aliaslookup(c, name)` + * at the top of cgdot's lc != nil branch, stopping at struct + * aliases so the existing direct-struct N_TNAME arm below stays + * byte-id with pre-fix #22 callers. + * + * Fix: selfhost/cmd/wcc/cgenexpr.ww cgdot, after `let tn = lc.tnode;` + * insert a peel loop before the lkind decision. LOOP not single- + * peel — Phase-N builds N_TNAME chains (memory + * project_tinfo_lossy_nominal), so depth-2+ aliases require + * iteration. Inner peel on the pointee is unnecessary because the + * existing `structlookupchain(c, inner)` already walks N_TNAME + * chains via aliaslookup (cgenutil.ww:1266-1276); row 4 below + * proves the inner-chain depth-3 path stays green without an + * explicit inner peel. + * + * Coverage (4 rows): + * 1. fn_param_read — `fn f(s: vs) { s.field }`. Single- + * alias receiver. Pre-fix: link + * failure on `field(SB)`. Post-fix: + * deref + offset load. The impl-e1 + * fold-blocker exact shape. + * 2. let_binding_read — `let s: vs = (&v): vs; s.field`. + * Single-alias via let-init; the + * explicit cast routes around the + * cstage checker assignability gap + * for chain-depth-1 (see row 3). + * 3. double_alias_read — `type pvt = *vt; type vs = pvt;` + * with `let s: vs = (&v): vs`. + * KEN'S LOOP-PEEL VERIFY GATE: + * chain depth 2 on the receiver, so + * a single-peel implementation would + * stop at N_TNAME("pvt") and miss + * via structlookupchain (which only + * bottoms out when aliaslookup + * returns an N_TNAME that + * structlookup hits — here the next + * hop is N_TPTR, breaking its + * inner-loop guard). The cast in + * the let-init dodges the cstage + * checker's "*vt not assignable to + * vs" through double aliases (see + * SIBLING below). + * 4. pointee_alias_chain — `type vti = vt; type vti2 = vti; + * type vs = *vti2;`. Receiver itself + * is single-aliased; the chain + * depth lives inside the pointee. + * DREW'S INNER-PEEL VERIFY GATE: + * proves structlookupchain's + * aliaslookup loop is sufficient + * without an explicit inner peel. + * If this row reds, the structural + * no-inner-peel claim above is + * wrong and the fix needs an + * inner-side peel too. + * + * Per-row gates: cstage runtime exit, wwstage runtime exit, + * cs.s == ww.s byte-identical (rule-10 stage symmetry). + * + * SIBLING BUGS surfaced during impl — NOT fixed here, filed for + * separate single-class commits (rule 11 split, rule 7 no + * workarounds, task spec "If you find sibling bugs in cgdot: file + * inline"): + * + * (A) cgassign N_DOT TK_ASSIGN on aliased-ptr receiver SILENTLY + * DROPS THE STORE. `s.field = 7` where s: vs = *vt emits + * nothing — wwstage MOVs the rhs into AX and discards. + * Cstage emits the correct `MOVQ off(BP), BX; MOVL AX, (BX)`. + * Same pre-peel pattern as cgdot, at cgenexpr.ww:5160-5168 + * (the `if (lkind == nkind.N_TPTR)` arm of the cgassign + * base-IDENT branch). Out of scope for #191 (read-side + * cgdot only); siblings to file as a follow-up class along + * with cgassign compound (+=, -=, etc.) — same shape, same + * drop. Repro: + * type vt = struct { field: i32 }; + * type vs = *vt; + * fn setit(s: vs) void = { s.field = 7; }; + * + * (B) Chained N_DOT spine (`p.i.x` where p: vs = *outer, outer + * has field i: inner, inner has field x) on aliased-ptr + * receiver also link-fails. The chained spine has dotlhs == + * N_DOT (not N_IDENT), so #191's branch doesn't fire; the + * chained-spine codepath has its own peel gap. Repro: + * type inner = struct { x: i32 }; + * type outer = struct { i: inner }; + * type vs = *outer; + * fn callit(p: vs) i32 = { return p.i.x; }; + * + * (C) Cstage checker rejects single-deep-alias assignability: + * `let s: vs = &v;` where vs = pvt = *vt errors with + * `init *vt not assignable to declared vs`. Workaround in + * row 3: `(&v): vs` explicit cast. The checker walks single + * aliases but not chains of depth >= 2. Not a wwstage cgen + * bug; cited as the reason row 3's let-init carries an + * explicit cast. + * + * GATE POLARITY: must stay GREEN. A red here means the cgdot + * outer-peel regressed (row 1/2), the loop-peel depth handling + * regressed (row 3), or the inner-chain structlookupchain path + * drifted (row 4). + */ +#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; +} + +#define STAGE_CS 1 +#define STAGE_WW 2 + +struct row { + const char *label; + const char *src; + int expected_exit; + int stage_mask; +}; + +static const struct row rows[] = { + { "fn_param_read", + "package main;\n" + "type vt = struct { field: i32 };\n" + "type vs = *vt;\n" + "fn callit(s: vs) i32 = { return s.field; };\n" + "export fn main() i32 = {\n" + " let v: vt; v.field = 42;\n" + " return callit(&v);\n" + "};\n", + 42, + STAGE_CS | STAGE_WW }, + { "let_binding_read", + "package main;\n" + "type vt = struct { field: i32 };\n" + "type vs = *vt;\n" + "export fn main() i32 = {\n" + " let v: vt; v.field = 42;\n" + " let s: vs = (&v): vs;\n" + " return s.field;\n" + "};\n", + 42, + STAGE_CS | STAGE_WW }, + { "double_alias_read", + "package main;\n" + "type vt = struct { field: i32 };\n" + "type pvt = *vt;\n" + "type vs = pvt;\n" + "export fn main() i32 = {\n" + " let v: vt; v.field = 42;\n" + " let s: vs = (&v): vs;\n" + " return s.field;\n" + "};\n", + 42, + STAGE_CS | STAGE_WW }, + { "pointee_alias_chain", + "package main;\n" + "type vt = struct { field: i32 };\n" + "type vti = vt;\n" + "type vti2 = vti;\n" + "type vs = *vti2;\n" + "fn callit(s: vs) i32 = { return s.field; };\n" + "export fn main() i32 = {\n" + " let v: vti2; v.field = 42;\n" + " return callit(&v);\n" + "};\n", + 42, + STAGE_CS | STAGE_WW }, +}; + +static int +write_source(const char *src_path, const char *src) +{ + FILE *f = fopen(src_path, "wb"); + if (!f) return -1; + fputs(src, f); + fclose(f); + return 0; +} + +static void +cleanup_tmp(const char *tmpdir, const char *base) +{ + char p[512]; + 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); + snprintf(p, sizeof p, "%s/%s.combined.ww", tmpdir, base); unlink(p); + snprintf(p, sizeof p, "%s/%s", tmpdir, base); unlink(p); + rmdir(tmpdir); +} + +static int +build_via_driver(const char *driver, const char *tmpdir, const char *src) +{ + char cmd[1024]; + snprintf(cmd, sizeof cmd, "cd %s && timeout 180 %s build %s 2>/dev/null", + tmpdir, driver, src); + return runwait(cmd); +} + +static int +run_row(const char *driver, const struct row *r, int seq) +{ + char tmpdir[256], src[256], base[64], outbin[512]; + snprintf(tmpdir, sizeof tmpdir, "/tmp/dap_%d_d_%d", getpid(), seq); + snprintf(src, sizeof src, "%s/main769.ww", tmpdir); + snprintf(base, sizeof base, "main769"); + mkdir(tmpdir, 0755); + if (write_source(src, r->src) != 0) { + cleanup_tmp(tmpdir, base); + return -1; + } + int rc = -1; + if (build_via_driver(driver, tmpdir, src) == 0) { + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + rc = runwait(outbin); + } + cleanup_tmp(tmpdir, base); + 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). */ +static int +asm_byte_identical(const char *cdrv, const char *wdrv, + const struct row *r, int seq) +{ + char src[256], tdc[256], tdw[256], base[64], cs[512], ws[512]; + snprintf(tdc, sizeof tdc, "/tmp/dap_%d_c_%d", getpid(), seq); + snprintf(tdw, sizeof tdw, "/tmp/dap_%d_w_%d", getpid(), seq); + snprintf(base, sizeof base, "main769"); + mkdir(tdc, 0755); + mkdir(tdw, 0755); + snprintf(src, sizeof src, "%s/main769.ww", tdc); + 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/main769.ww", tdw); + 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) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char cdrv[1024], wdrv[1024]; + 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 wwpresent = (access(wdrv, X_OK) == 0); + int seq = 0; + + for (int i = 0; i < n; i++) { + if (rows[i].stage_mask & STAGE_CS) { + total++; + int got = run_row(cdrv, &rows[i], seq++); + if (got != rows[i].expected_exit) { + fprintf(stderr, + "dot_aliased_ptr[cstage run][%s]: exit=%d want=%d\n", + rows[i].label, got, rows[i].expected_exit); + fail++; + } + } + if (wwpresent && (rows[i].stage_mask & STAGE_WW)) { + total++; + int got = run_row(wdrv, &rows[i], seq++); + if (got != rows[i].expected_exit) { + fprintf(stderr, + "dot_aliased_ptr[wwstage run][%s]: exit=%d want=%d\n", + rows[i].label, got, rows[i].expected_exit); + fail++; + } + total++; + if (asm_byte_identical(cdrv, wdrv, &rows[i], seq++) != 0) { + fprintf(stderr, + "dot_aliased_ptr[byte-id][%s]: cstage vs wwstage asm differs\n", + rows[i].label); + fail++; + } + } + } + + if (!wwpresent) + fprintf(stderr, "dot_aliased_ptr: skip wwstage (no %s)\n", wdrv); + + if (fail) { + fprintf(stderr, "dot_aliased_ptr: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("dot_aliased_ptr: %d/%d ok\n", total, total); + return 0; +}