From 02eb867036a38bde92cd46aed7fdc900a88283d0 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 12 Jun 2026 19:22:51 +0900 Subject: [PATCH] wcc/ww: cgtypeassert gains the nullable arm 'as' on a nullable value compared the pointer itself to a tag (the missing arm). Mirror cgtypetest's nullable fold and the cstage twin (cgen.c:10694). Review item #17. --- Makefile | 7 + selfhost/cmd/w6c/main.combined.ww | 26 ++- selfhost/cmd/wcc/cgenexpr.ww | 26 ++- selfhost/cmd/wwdump/main.combined.ww | 26 ++- test/wcc/949_nullable_assert_run.c | 242 +++++++++++++++++++++++++++ 5 files changed, 324 insertions(+), 3 deletions(-) create mode 100644 test/wcc/949_nullable_assert_run.c diff --git a/Makefile b/Makefile index d49d5f17..0a8a7813 100644 --- a/Makefile +++ b/Makefile @@ -321,6 +321,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_tagged_call_arg_run \ $(BIN)/test_tryprop_tag_remap_run \ $(BIN)/test_nullable_try_run \ + $(BIN)/test_nullable_assert_run \ $(BIN)/test_nested_union_widen_run \ $(BIN)/test_sret_struct_return \ $(BIN)/test_sret_struct_return_run \ @@ -1771,6 +1772,12 @@ $(BIN)/test_nullable_try_run: test/wcc/949_nullable_try_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_nullable_assert_run: test/wcc/949_nullable_assert_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 $@ $< + $(BIN)/test_sret_struct_return: test/wcc/721_sret_struct_return.c \ $(BIN)/w6c $(BIN)/w6c_ww | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 7f16efc9..e64c96d0 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -24019,8 +24019,32 @@ fn cgtypeassert(c: *cgen, n: *node) void = { }; }; let want: i32 = cgtagvariantidx(c, scrutt, n.rhs); - if (want < 0) { want = 0; }; let okl: str = mklabel(c, "asrt_ok"); + // Nullable `(*T | void)`: the slot word IS the pointer, not a tag. + // The *T variant asserts non-null, the void variant asserts null; + // AX keeps the pointer on the ok path (no slot+8 unwrap — the 8B + // nullable slot has no second word). The pre-fix path compared the + // POINTER against `want` (so a real pointer aborted, null passed) + // and unwrapped a frame word past the slot. `want` stays RAW (no + // clamp), mirroring cstage cgen.c N_TYPEASSERT nullable arm and + // ww's own cgtypetest nullable fold (cgenexpr.ww). (task #17/F4) + if (isnullabletype(scrutt)) { + let ptrtag: i32 = nullableptrtag(scrutt); + emitline("\tMOVQ\t"); + emitoff(scrutoff: i64); + emitline("(BP), AX\n"); + emitline("\tCMPQ\t$0, AX\n"); + if (want == ptrtag) { + emitline("\tJNE\t"); + } else { + emitline("\tJE\t"); + }; + emitline(okl); + emitline("\n\tMOVQ\t$1, DI\n\tMOVQ\t$60, AX\n\tSYSCALL\n"); + emitlabel(okl); + return; + }; + if (want < 0) { want = 0; }; emitline("\tMOVQ\t"); emitoff(scrutoff: i64); emitline("(BP), AX\n"); diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 2b96a23f..be0250a2 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -868,8 +868,32 @@ fn cgtypeassert(c: *cgen, n: *node) void = { }; }; let want: i32 = cgtagvariantidx(c, scrutt, n.rhs); - if (want < 0) { want = 0; }; let okl: str = mklabel(c, "asrt_ok"); + // Nullable `(*T | void)`: the slot word IS the pointer, not a tag. + // The *T variant asserts non-null, the void variant asserts null; + // AX keeps the pointer on the ok path (no slot+8 unwrap — the 8B + // nullable slot has no second word). The pre-fix path compared the + // POINTER against `want` (so a real pointer aborted, null passed) + // and unwrapped a frame word past the slot. `want` stays RAW (no + // clamp), mirroring cstage cgen.c N_TYPEASSERT nullable arm and + // ww's own cgtypetest nullable fold (cgenexpr.ww). (task #17/F4) + if (isnullabletype(scrutt)) { + let ptrtag: i32 = nullableptrtag(scrutt); + emitline("\tMOVQ\t"); + emitoff(scrutoff: i64); + emitline("(BP), AX\n"); + emitline("\tCMPQ\t$0, AX\n"); + if (want == ptrtag) { + emitline("\tJNE\t"); + } else { + emitline("\tJE\t"); + }; + emitline(okl); + emitline("\n\tMOVQ\t$1, DI\n\tMOVQ\t$60, AX\n\tSYSCALL\n"); + emitlabel(okl); + return; + }; + if (want < 0) { want = 0; }; emitline("\tMOVQ\t"); emitoff(scrutoff: i64); emitline("(BP), AX\n"); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index af2c5aaa..40c4d947 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -24019,8 +24019,32 @@ fn cgtypeassert(c: *cgen, n: *node) void = { }; }; let want: i32 = cgtagvariantidx(c, scrutt, n.rhs); - if (want < 0) { want = 0; }; let okl: str = mklabel(c, "asrt_ok"); + // Nullable `(*T | void)`: the slot word IS the pointer, not a tag. + // The *T variant asserts non-null, the void variant asserts null; + // AX keeps the pointer on the ok path (no slot+8 unwrap — the 8B + // nullable slot has no second word). The pre-fix path compared the + // POINTER against `want` (so a real pointer aborted, null passed) + // and unwrapped a frame word past the slot. `want` stays RAW (no + // clamp), mirroring cstage cgen.c N_TYPEASSERT nullable arm and + // ww's own cgtypetest nullable fold (cgenexpr.ww). (task #17/F4) + if (isnullabletype(scrutt)) { + let ptrtag: i32 = nullableptrtag(scrutt); + emitline("\tMOVQ\t"); + emitoff(scrutoff: i64); + emitline("(BP), AX\n"); + emitline("\tCMPQ\t$0, AX\n"); + if (want == ptrtag) { + emitline("\tJNE\t"); + } else { + emitline("\tJE\t"); + }; + emitline(okl); + emitline("\n\tMOVQ\t$1, DI\n\tMOVQ\t$60, AX\n\tSYSCALL\n"); + emitlabel(okl); + return; + }; + if (want < 0) { want = 0; }; emitline("\tMOVQ\t"); emitoff(scrutoff: i64); emitline("(BP), AX\n"); diff --git a/test/wcc/949_nullable_assert_run.c b/test/wcc/949_nullable_assert_run.c new file mode 100644 index 00000000..f15fde8d --- /dev/null +++ b/test/wcc/949_nullable_assert_run.c @@ -0,0 +1,242 @@ +/* + * 949_nullable_assert_run — the `e as T` type assertion on a nullable + * `(*T | void)` operand must discriminate POINTER-vs-NULL, not compare + * the pointer value against a variant tag index (report item #17, F4). + * + * Gate-blind hazard: for a nullable operand the slot word IS the + * pointer (no tag word), so pre-fix wwstage cgtypeassert emitted + * `CMPQ $want,AX; JE ok` against the POINTER and then unwrapped a frame + * word past the 8B slot — a valid pointer aborted, a void value + * silently PASSED, and the unwrap read garbage. cstage already carried + * the nullable arm (cmd/w6c/cgen.c N_TYPEASSERT) and ww's own + * cgtypetest carries the matching nullable fold; the fix mirrors that + * arm into cgtypeassert. wwstage-only (cstage correct) → after the fix + * cs.s == ww.s, so the byte-id row witnesses rule-10 convergence. + * + * Two assertions per row: + * - RUNTIME: build with `ww` (cstage) and `ww_ww` (wwstage), run, + * compare exit code. This is what was wrong pre-fix. + * - ASM BYTE-ID: compile the same source through `w6c` and `w6c_ww` + * and require byte-identical .s (rule 10). + * + * Rows: + * 1. asrt_ptr_ok — valid ptr `as *int` must unwrap and deref. + * Pre-fix wwstage aborted exit(1) (CMPQ against the pointer). + * 2. asrt_void_fail — void value `as *int` must abort exit(1). + * Pre-fix wwstage silently PASSED (null == ptr-tag 0). + * 3. asrt_void_target_fail — valid ptr `as void` must abort exit(1) + * (non-null != the void variant). Exercises the void polarity. + * 4. asrt_void_target_ok — void value `as void` must succeed. + * Exercises the void polarity on the matching side. + */ +#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[] = { + { "asrt_ptr_ok", + "package main;\n" + "export fn main() i32 = {\n" + " let x: int = 42;\n" + " let p: (*int|void) = &x;\n" + " let q: *int = p as *int;\n" + " return (*q): i32;\n" + "};\n", + 42 }, + { "asrt_void_fail", + "package main;\n" + "export fn main() i32 = {\n" + " let p: (*int|void) = void;\n" + " let q = p as *int;\n" + " return 50;\n" + "};\n", + 1 }, + { "asrt_void_target_fail", + "package main;\n" + "export fn main() i32 = {\n" + " let x: int = 5;\n" + " let p: (*int|void) = &x;\n" + " let z = p as void;\n" + " return 7;\n" + "};\n", + 1 }, + { "asrt_void_target_ok", + "package main;\n" + "export fn main() i32 = {\n" + " let p: (*int|void) = void;\n" + " let z = p as void;\n" + " return 7;\n" + "};\n", + 7 }, +}; + +static int +write_src(const char *dir, const char *base, const struct row *r, char *out, + size_t outsz) +{ + snprintf(out, outsz, "%s/%s.ww", dir, base); + FILE *f = fopen(out, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + return 0; +} + +static int +build_run(const char *driver, const char *src, const char *workdir) +{ + char cmd[8192]; + snprintf(cmd, sizeof cmd, "cd %s && %s build %s > /dev/null 2>&1", + workdir, driver, src); + if (runwait(cmd) != 0) return -1; + + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + char outbin[1024]; + snprintf(outbin, sizeof outbin, "%s/%s", workdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + return runwait(outbin); +} + +static int +files_equal(const char *a, const char *b) +{ + FILE *fa = fopen(a, "rb"); + FILE *fb = fopen(b, "rb"); + if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return -1; } + int ca, cb, eq = 1; + do { + ca = fgetc(fa); + cb = fgetc(fb); + if (ca != cb) { eq = 0; break; } + } while (ca != EOF); + fclose(fa); + fclose(fb); + return eq; +} + +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], cw6[640], ww6[640]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + snprintf(cw6, sizeof cw6, "%s/w6c", bin); + snprintf(ww6, sizeof ww6, "%s/w6c_ww", bin); + + int have_ww = (access(wdrv, X_OK) == 0); + int have_w6cww = (access(ww6, X_OK) == 0); + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + + for (int i = 0; i < n; i++) { + const struct row *r = &rows[i]; + + char dir[] = "/tmp/nasr.XXXXXX"; + if (mkdtemp(dir) == NULL) { + fprintf(stderr, "row[%s]: mkdtemp failed\n", r->label); + fail++; total++; + continue; + } + + char src[1024]; + if (write_src(dir, "p", r, src, sizeof src) != 0) { + fprintf(stderr, "row[%s]: write src failed\n", r->label); + fail++; total++; + goto cleanup; + } + + if (have_w6cww) { + char css[1024], wss[1024], cmd[8192]; + snprintf(css, sizeof css, "%s/cs.s", dir); + snprintf(wss, sizeof wss, "%s/ww.s", dir); + snprintf(cmd, sizeof cmd, "%s -o %s %s > /dev/null 2>&1", + cw6, css, src); + int rc1 = runwait(cmd); + snprintf(cmd, sizeof cmd, "%s -o %s %s > /dev/null 2>&1", + ww6, wss, src); + int rc2 = runwait(cmd); + total++; + if (rc1 != 0 || rc2 != 0) { + fprintf(stderr, + "row[%s]: w6c/w6c_ww emit failed (%d/%d)\n", + r->label, rc1, rc2); + fail++; + } else if (files_equal(css, wss) != 1) { + fprintf(stderr, + "row[%s]: cs.s != ww.s (rule-10 break)\n", + r->label); + fail++; + } + } + + { + char wk[1024]; + snprintf(wk, sizeof wk, "%s/cs", dir); + mkdir(wk, 0755); + int got = build_run(cdrv, src, wk); + total++; + if (got != r->want) { + fprintf(stderr, + "row[%s][cstage]: exit=%d want=%d\n", + r->label, got, r->want); + fail++; + } + } + + if (have_ww) { + char wk[1024]; + snprintf(wk, sizeof wk, "%s/ww", dir); + mkdir(wk, 0755); + int got = build_run(wdrv, src, wk); + total++; + if (got != r->want) { + fprintf(stderr, + "row[%s][wwstage]: exit=%d want=%d\n", + r->label, got, r->want); + fail++; + } + } + +cleanup: + { + char rm[1100]; + snprintf(rm, sizeof rm, "rm -rf %s", dir); + runwait(rm); + } + } + + if (fail) { + fprintf(stderr, + "nullable_assert_run: %d/%d checks failed\n", fail, total); + return 1; + } + printf("nullable_assert_run: %d/%d ok\n", total, total); + return 0; +}