diff --git a/Makefile b/Makefile index af136c94..d2d16ee2 100644 --- a/Makefile +++ b/Makefile @@ -322,6 +322,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_dot_aliased_ptr \ $(BIN)/test_return_tagged_forward \ $(BIN)/test_widen_transitive \ + $(BIN)/test_typeassert_nonident \ $(BIN)/test_use_promote_alias \ $(BIN)/test_field_signed $(BIN)/test_frame_argcount \ $(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \ @@ -660,6 +661,12 @@ $(BIN)/test_widen_transitive: test/wcc/771_widen_transitive.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_typeassert_nonident: test/wcc/772_typeassert_nonident.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 95165821..c728aa78 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -18010,6 +18010,41 @@ fn cgtypeassert(c: *cgen, n: *node) void = { scrutoff = lc.off; scrutt = resolvetagged(c, lc.tnode); }; + } else { + // Non-ident scrutinee (call result, arr[i], p.field, ?, + // etc.). Mirror cgmatch's spill (cgenexpr.ww:1422-1460) + // and cstage cmd/w6c/cgen.c:6300-6316: alloc an + // `@asrt_spill` slot sized via matchspillsz, evaluate + // the LHS, then copy the AX/DX/CX[/R8] return-ABI words + // into the slot so the tag-check + payload load indexes + // off memory like the IDENT path. Without this, scrutoff + // stayed 0 and the tag read fell on (BP) — the saved-BP + // word — and the payload read on +8(BP) — the return + // address. Bug #200. + scrutt = matchscrutt(c, lhs); + let spillsz: i32 = matchspillsz(c, scrutt); + scrutoff = localalloc(c, "@asrt_spill", spillsz, nil); + cgexpr(c, lhs); + emitline("\tMOVQ\tAX, "); + emitoff(scrutoff: i64); + emitline("(BP)\n"); + if (!isnullabletype(scrutt)) { + emitline("\tMOVQ\tDX, "); + emitoff((scrutoff + 8): i64); + emitline("(BP)\n"); + // Mirror cstage cmd/w6c/cgen.c:6313-6315: only CX + // → +16 when slot_size > 16. The 4-word case + // (R8 → +24, slot_size > 24) is the cgmatch shape + // (cgenexpr.ww:1454-1458, cmd/w6c/cgen.c:5988-5990) + // but cstage cgtypeassert omits it; preserve the + // asymmetry rather than diverge from rule 10 + // byte-id. Filed inline as a cstage twin task. + if (spillsz > 16) { + emitline("\tMOVQ\tCX, "); + emitoff((scrutoff + 16): i64); + emitline("(BP)\n"); + }; + }; }; }; let want: i32 = cgtagvariantidx(c, scrutt, n.rhs); diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 1d2b8433..d9ac1f9f 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -387,6 +387,41 @@ fn cgtypeassert(c: *cgen, n: *node) void = { scrutoff = lc.off; scrutt = resolvetagged(c, lc.tnode); }; + } else { + // Non-ident scrutinee (call result, arr[i], p.field, ?, + // etc.). Mirror cgmatch's spill (cgenexpr.ww:1422-1460) + // and cstage cmd/w6c/cgen.c:6300-6316: alloc an + // `@asrt_spill` slot sized via matchspillsz, evaluate + // the LHS, then copy the AX/DX/CX[/R8] return-ABI words + // into the slot so the tag-check + payload load indexes + // off memory like the IDENT path. Without this, scrutoff + // stayed 0 and the tag read fell on (BP) — the saved-BP + // word — and the payload read on +8(BP) — the return + // address. Bug #200. + scrutt = matchscrutt(c, lhs); + let spillsz: i32 = matchspillsz(c, scrutt); + scrutoff = localalloc(c, "@asrt_spill", spillsz, nil); + cgexpr(c, lhs); + emitline("\tMOVQ\tAX, "); + emitoff(scrutoff: i64); + emitline("(BP)\n"); + if (!isnullabletype(scrutt)) { + emitline("\tMOVQ\tDX, "); + emitoff((scrutoff + 8): i64); + emitline("(BP)\n"); + // Mirror cstage cmd/w6c/cgen.c:6313-6315: only CX + // → +16 when slot_size > 16. The 4-word case + // (R8 → +24, slot_size > 24) is the cgmatch shape + // (cgenexpr.ww:1454-1458, cmd/w6c/cgen.c:5988-5990) + // but cstage cgtypeassert omits it; preserve the + // asymmetry rather than diverge from rule 10 + // byte-id. Filed inline as a cstage twin task. + if (spillsz > 16) { + emitline("\tMOVQ\tCX, "); + emitoff((scrutoff + 16): i64); + emitline("(BP)\n"); + }; + }; }; }; let want: i32 = cgtagvariantidx(c, scrutt, n.rhs); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 785fb0a4..0b6a2fd9 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -18010,6 +18010,41 @@ fn cgtypeassert(c: *cgen, n: *node) void = { scrutoff = lc.off; scrutt = resolvetagged(c, lc.tnode); }; + } else { + // Non-ident scrutinee (call result, arr[i], p.field, ?, + // etc.). Mirror cgmatch's spill (cgenexpr.ww:1422-1460) + // and cstage cmd/w6c/cgen.c:6300-6316: alloc an + // `@asrt_spill` slot sized via matchspillsz, evaluate + // the LHS, then copy the AX/DX/CX[/R8] return-ABI words + // into the slot so the tag-check + payload load indexes + // off memory like the IDENT path. Without this, scrutoff + // stayed 0 and the tag read fell on (BP) — the saved-BP + // word — and the payload read on +8(BP) — the return + // address. Bug #200. + scrutt = matchscrutt(c, lhs); + let spillsz: i32 = matchspillsz(c, scrutt); + scrutoff = localalloc(c, "@asrt_spill", spillsz, nil); + cgexpr(c, lhs); + emitline("\tMOVQ\tAX, "); + emitoff(scrutoff: i64); + emitline("(BP)\n"); + if (!isnullabletype(scrutt)) { + emitline("\tMOVQ\tDX, "); + emitoff((scrutoff + 8): i64); + emitline("(BP)\n"); + // Mirror cstage cmd/w6c/cgen.c:6313-6315: only CX + // → +16 when slot_size > 16. The 4-word case + // (R8 → +24, slot_size > 24) is the cgmatch shape + // (cgenexpr.ww:1454-1458, cmd/w6c/cgen.c:5988-5990) + // but cstage cgtypeassert omits it; preserve the + // asymmetry rather than diverge from rule 10 + // byte-id. Filed inline as a cstage twin task. + if (spillsz > 16) { + emitline("\tMOVQ\tCX, "); + emitoff((scrutoff + 16): i64); + emitline("(BP)\n"); + }; + }; }; }; let want: i32 = cgtagvariantidx(c, scrutt, n.rhs); diff --git a/test/wcc/772_typeassert_nonident.c b/test/wcc/772_typeassert_nonident.c new file mode 100644 index 00000000..2f07505c --- /dev/null +++ b/test/wcc/772_typeassert_nonident.c @@ -0,0 +1,332 @@ +/* + * 772_typeassert_nonident — project #200: wwstage cgtypeassert non-IDENT + * scrutinee bug. The pre-fix `e as T` on a non-N_IDENT scrutinee (direct + * call result, arr[i], p.field, ?, paren-wrap of any of those) left + * scrutoff=0; the tag-load fell on (BP) — the saved-BP word — and the + * payload-load on +8(BP) — the return address — instead of the spilled + * tagged-return ABI words. wwstage produced garbage (exit 220 in the + * repro vs cstage's 42). + * + * Fix (selfhost/cmd/wcc/cgenexpr.ww:368-414 cgtypeassert): mirror cstage + * cmd/w6c/cgen.c:6300-6316 N_TYPEASSERT non-IDENT arm. Add an `else` + * branch after the existing N_IDENT path: resolve the tagged type via + * matchscrutt, alloc an `@asrt_spill` slot via matchspillsz/localalloc, + * cgexpr the LHS, then spill AX→+0 (tag), DX→+8 (word0), CX→+16 + * (word1) guarded on spill > 16. Mirrors cgmatch's spill (cgenexpr.ww: + * 1422-1460). The R8→+24 path (cgmatch shape) is intentionally omitted + * to mirror cstage's cgtypeassert exactly (rule-10 byte-id); the cstage + * twin gap is filed inline as a sibling task. + * + * Coverage (7 rows): + * 1. call_as_size — `f() as size`. The repro shape: direct call + * result, basic-size payload. Pre-fix + * wwstage: garbage AX from (BP). Post-fix: + * 42. + * 2. call_as_str — `f() as str`. Non-IDENT, multi-word payload + * (AX=ptr / BX=len). Tests CX→+16 spill + + * the `isstrtype` post-spill BX load. + * 3. call_as_namedvoid — `f() as nvariant` where nvariant is a + * named-void alias. Tag-check fires; the + * payload load is a 0-byte no-op (void + * variant has size 0; no consumer reads AX). + * Drew-add. + * 4. call_as_fnptr — `f() as *fn(i32) i32`. Fn-ptr variant + * (8B word0 = pointer). Round-trips through + * spill→tag-check→MOVQ +8 → AX. + * 5. call_as_u8 — `f() as u8`. Narrow scalar. The callee + * zero/sign-extended the value to 8B on + * return; MOVQ store + MOVQ read round-trips + * it. Ken-add: confirms no narrow-truncation + * on the payload load. + * 6. call_as_i16 — `f() as i16`. Narrow signed scalar. Tests + * negative round-trip through the spill slot + * (callee sign-extended to 8B; MOVQ + * preserves). Ken-add sibling of row 5. + * 7. branched_call_as — callee returns a runtime-chosen variant + * (the success branch); `f(true) as size`. + * Tests that the spill+tag-check path + * doesn't lose the callee's selected tag. + * + * Per-row gates: cstage runtime exit, wwstage runtime exit, cs.s == + * ww.s byte-identical (rule-10 stage symmetry). + * + * GATE POLARITY: must stay GREEN. A red on rows 1-4 or 7 means the + * spill path is broken (non-IDENT scrutinee returned to the BP/RIP + * read). Rows 5/6 red means narrow-scalar round-trip miscompiled at + * the payload load (would surface if a future SIB regresses MOVQ to a + * narrow load that drops the callee's extension). + */ +#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[] = { + { "call_as_size", + "package main;\n" + "fn f(b: bool) (size | str) = {\n" + " if (b) { return 42: size; };\n" + " return \"x\";\n" + "};\n" + "export fn main() i32 = {\n" + " let n = f(true) as size;\n" + " return n: i32;\n" + "};\n", + 42, + STAGE_CS | STAGE_WW }, + { "call_as_str", + "package main;\n" + "fn f(b: bool) (size | str) = {\n" + " if (b) { return \"AB\"; };\n" + " return 0: size;\n" + "};\n" + "export fn main() i32 = {\n" + " let s = f(true) as str;\n" + " return s.len: i32 + 40;\n" + "};\n", + 42, + STAGE_CS | STAGE_WW }, + { "call_as_namedvoid", + "package main;\n" + "type nv = void;\n" + "fn f(b: bool) (size | nv) = {\n" + " if (b) { return void: nv; };\n" + " return 0: size;\n" + "};\n" + "export fn main() i32 = {\n" + " let _ = f(true) as nv;\n" + " return 42;\n" + "};\n", + 42, + STAGE_CS | STAGE_WW }, + { "call_as_fnptr", + /* #193: explicit deref required to call a fn-ptr (`(*p)(args)`). + * The probe is wwstage cgtypeassert non-IDENT spill; deref-call + * shape is orthogonal. */ + "package main;\n" + "fn g(x: i32) i32 = { return x + 1; };\n" + "fn f(b: bool) (*fn(x: i32) i32 | size) = {\n" + " if (b) { return &g; };\n" + " return 0: size;\n" + "};\n" + "export fn main() i32 = {\n" + " let p = f(true) as *fn(x: i32) i32;\n" + " return (*p)(41);\n" + "};\n", + 42, + STAGE_CS | STAGE_WW }, + { "call_as_u8", + "package main;\n" + "fn f(b: bool) (u8 | str) = {\n" + " if (b) { return 42: u8; };\n" + " return \"x\";\n" + "};\n" + "export fn main() i32 = {\n" + " let n = f(true) as u8;\n" + " return n: i32;\n" + "};\n", + 42, + STAGE_CS | STAGE_WW }, + { "call_as_i16", + "package main;\n" + "fn f(b: bool) (i16 | str) = {\n" + " if (b) { return -1: i16; };\n" + " return \"x\";\n" + "};\n" + "export fn main() i32 = {\n" + " let n = f(true) as i16;\n" + " if (n: i32 == -1) { return 42; };\n" + " return 1;\n" + "};\n", + 42, + STAGE_CS | STAGE_WW }, + { "branched_call_as", + "package main;\n" + "fn f(b: bool) (size | str) = {\n" + " if (b) { return 42: size; };\n" + " return \"x\";\n" + "};\n" + "export fn main() i32 = {\n" + " let pick: bool = true;\n" + " let n = f(pick) as size;\n" + " return n: i32;\n" + "};\n", + 42, + STAGE_CS | STAGE_WW }, +}; + +static int +write_source(const char *path, const char *src) +{ + FILE *f = fopen(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[512], base[64], outbin[768]; + snprintf(tmpdir, sizeof tmpdir, "/tmp/tan_%d_d_%d", getpid(), seq); + snprintf(base, sizeof base, "main772"); + snprintf(src, sizeof src, "%s/%s.ww", tmpdir, base); + 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[512], tdc[256], tdw[256], base[64], cs[512], ws[512]; + snprintf(tdc, sizeof tdc, "/tmp/tan_%d_c_%d", getpid(), seq); + snprintf(tdw, sizeof tdw, "/tmp/tan_%d_w_%d", getpid(), seq); + snprintf(base, sizeof base, "main772"); + 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) +{ + 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], 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 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, + "typeassert_nonident[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, + "typeassert_nonident[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, + "typeassert_nonident[byte-id][%s]: cstage vs wwstage asm differs\n", + rows[i].label); + fail++; + } + } + } + + if (!wwpresent) + fprintf(stderr, "typeassert_nonident: skip wwstage (no %s)\n", wdrv); + + if (fail) { + fprintf(stderr, "typeassert_nonident: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("typeassert_nonident: %d/%d ok\n", total, total); + return 0; +}