From 6f3c896fea9ee6a40678bebab43cb950d346516e Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 12 Jun 2026 19:19:26 +0900 Subject: [PATCH] wcc/ww: cgtryprop/cgtryunw gain the nullable arm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Try-propagation on a nullable value fell to the tagged path and compared the pointer to a tag — the null check came out inverted. Mirror ww's own cgtypetest nullable fold (cgenexpr.ww:652) and the cstage twin (cgen.c:10366/10504). Review item #15. --- Makefile | 7 + selfhost/cmd/w6c/main.combined.ww | 31 ++++ selfhost/cmd/wcc/cgenexpr.ww | 31 ++++ selfhost/cmd/wwdump/main.combined.ww | 31 ++++ test/wcc/949_nullable_try_run.c | 246 +++++++++++++++++++++++++++ 5 files changed, 346 insertions(+) create mode 100644 test/wcc/949_nullable_try_run.c diff --git a/Makefile b/Makefile index 564e7752..d49d5f17 100644 --- a/Makefile +++ b/Makefile @@ -320,6 +320,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_tagged_call_arg \ $(BIN)/test_tagged_call_arg_run \ $(BIN)/test_tryprop_tag_remap_run \ + $(BIN)/test_nullable_try_run \ $(BIN)/test_nested_union_widen_run \ $(BIN)/test_sret_struct_return \ $(BIN)/test_sret_struct_return_run \ @@ -1764,6 +1765,12 @@ $(BIN)/test_nested_union_widen_run: test/wcc/925_nested_union_widen_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_nullable_try_run: test/wcc/949_nullable_try_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 e43c01e4..7f16efc9 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -23500,6 +23500,22 @@ fn cgtryprop(c: *cgen, n: *node) void = { // cursor here (was a silent word0 unwrap); call sources keep // the plain cgexpr emission byte-for-byte. cgtryunwcursor(c, n, "?"); + // Nullable `(*T | void)`: AX IS the pointer, not a tag. Non-null + // = success (any *T variant), null = error → propagate (RET with + // AX=0). The pre-fix path compared AX against successtag and so + // treated null as success (inverted). Mirrors cstage cgen.c + // N_TRYPROP nullable arm; ww's own cgtypetest carries the twin + // (cgenexpr.ww nullable fold). (task #15/F4) + if (isnullabletype(n.lhs)) { + let nl: str = mklabel(c, "tryprop_ok"); + emitline("\tCMPQ\t$0, AX\n"); + emitline("\tJNE\t"); + emitline(nl); + emitline("\n"); + emitline("\tMOVQ\tBP, SP\n\tPOPQ\tBP\n\tRET\n"); + emitlabel(nl); + return; + }; // AX = tag. If not the success tag, this is an error; pop frame and // RET. Success tag is dynamic (successtag / cstage cg_tagged_success_tag // parity, #52): 0 for success-first, the first non-error index for an @@ -23638,6 +23654,21 @@ fn cgtryunw(c: *cgen, n: *node) void = { }; // Family C (#35/#46): see the cgtryprop twin. cgtryunwcursor(c, n, "!"); + // Nullable `(*T | void)`: AX IS the pointer. Non-null = success + // (leave AX as-is), null = error → abort exit(1). The pre-fix + // path compared AX against successtag, treating null as success + // (inverted). Mirrors cstage cgen.c N_TRYUNW nullable arm; ww's + // own cgtypetest carries the twin. (task #15/F4) + if (isnullabletype(n.lhs)) { + let nl: str = mklabel(c, "tryunw_ok"); + emitline("\tCMPQ\t$0, AX\n"); + emitline("\tJNE\t"); + emitline(nl); + emitline("\n"); + emitline("\tMOVQ\t$1, DI\n\tMOVQ\t$60, AX\n\tSYSCALL\n"); + emitlabel(nl); + return; + }; let cl: str = mklabel(c, "tryunw_ok"); // Success tag is dynamic (successtag / cstage cg_tagged_success_tag // parity, #52): 0 for success-first, the first non-error index for an diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 8a8dc8f2..2b96a23f 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -349,6 +349,22 @@ fn cgtryprop(c: *cgen, n: *node) void = { // cursor here (was a silent word0 unwrap); call sources keep // the plain cgexpr emission byte-for-byte. cgtryunwcursor(c, n, "?"); + // Nullable `(*T | void)`: AX IS the pointer, not a tag. Non-null + // = success (any *T variant), null = error → propagate (RET with + // AX=0). The pre-fix path compared AX against successtag and so + // treated null as success (inverted). Mirrors cstage cgen.c + // N_TRYPROP nullable arm; ww's own cgtypetest carries the twin + // (cgenexpr.ww nullable fold). (task #15/F4) + if (isnullabletype(n.lhs)) { + let nl: str = mklabel(c, "tryprop_ok"); + emitline("\tCMPQ\t$0, AX\n"); + emitline("\tJNE\t"); + emitline(nl); + emitline("\n"); + emitline("\tMOVQ\tBP, SP\n\tPOPQ\tBP\n\tRET\n"); + emitlabel(nl); + return; + }; // AX = tag. If not the success tag, this is an error; pop frame and // RET. Success tag is dynamic (successtag / cstage cg_tagged_success_tag // parity, #52): 0 for success-first, the first non-error index for an @@ -487,6 +503,21 @@ fn cgtryunw(c: *cgen, n: *node) void = { }; // Family C (#35/#46): see the cgtryprop twin. cgtryunwcursor(c, n, "!"); + // Nullable `(*T | void)`: AX IS the pointer. Non-null = success + // (leave AX as-is), null = error → abort exit(1). The pre-fix + // path compared AX against successtag, treating null as success + // (inverted). Mirrors cstage cgen.c N_TRYUNW nullable arm; ww's + // own cgtypetest carries the twin. (task #15/F4) + if (isnullabletype(n.lhs)) { + let nl: str = mklabel(c, "tryunw_ok"); + emitline("\tCMPQ\t$0, AX\n"); + emitline("\tJNE\t"); + emitline(nl); + emitline("\n"); + emitline("\tMOVQ\t$1, DI\n\tMOVQ\t$60, AX\n\tSYSCALL\n"); + emitlabel(nl); + return; + }; let cl: str = mklabel(c, "tryunw_ok"); // Success tag is dynamic (successtag / cstage cg_tagged_success_tag // parity, #52): 0 for success-first, the first non-error index for an diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 718bb3ea..af2c5aaa 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -23500,6 +23500,22 @@ fn cgtryprop(c: *cgen, n: *node) void = { // cursor here (was a silent word0 unwrap); call sources keep // the plain cgexpr emission byte-for-byte. cgtryunwcursor(c, n, "?"); + // Nullable `(*T | void)`: AX IS the pointer, not a tag. Non-null + // = success (any *T variant), null = error → propagate (RET with + // AX=0). The pre-fix path compared AX against successtag and so + // treated null as success (inverted). Mirrors cstage cgen.c + // N_TRYPROP nullable arm; ww's own cgtypetest carries the twin + // (cgenexpr.ww nullable fold). (task #15/F4) + if (isnullabletype(n.lhs)) { + let nl: str = mklabel(c, "tryprop_ok"); + emitline("\tCMPQ\t$0, AX\n"); + emitline("\tJNE\t"); + emitline(nl); + emitline("\n"); + emitline("\tMOVQ\tBP, SP\n\tPOPQ\tBP\n\tRET\n"); + emitlabel(nl); + return; + }; // AX = tag. If not the success tag, this is an error; pop frame and // RET. Success tag is dynamic (successtag / cstage cg_tagged_success_tag // parity, #52): 0 for success-first, the first non-error index for an @@ -23638,6 +23654,21 @@ fn cgtryunw(c: *cgen, n: *node) void = { }; // Family C (#35/#46): see the cgtryprop twin. cgtryunwcursor(c, n, "!"); + // Nullable `(*T | void)`: AX IS the pointer. Non-null = success + // (leave AX as-is), null = error → abort exit(1). The pre-fix + // path compared AX against successtag, treating null as success + // (inverted). Mirrors cstage cgen.c N_TRYUNW nullable arm; ww's + // own cgtypetest carries the twin. (task #15/F4) + if (isnullabletype(n.lhs)) { + let nl: str = mklabel(c, "tryunw_ok"); + emitline("\tCMPQ\t$0, AX\n"); + emitline("\tJNE\t"); + emitline(nl); + emitline("\n"); + emitline("\tMOVQ\t$1, DI\n\tMOVQ\t$60, AX\n\tSYSCALL\n"); + emitlabel(nl); + return; + }; let cl: str = mklabel(c, "tryunw_ok"); // Success tag is dynamic (successtag / cstage cg_tagged_success_tag // parity, #52): 0 for success-first, the first non-error index for an diff --git a/test/wcc/949_nullable_try_run.c b/test/wcc/949_nullable_try_run.c new file mode 100644 index 00000000..23cb1c04 --- /dev/null +++ b/test/wcc/949_nullable_try_run.c @@ -0,0 +1,246 @@ +/* + * 949_nullable_try_run — the `?` / `!` try operators on a nullable + * `(*T | void)` operand must discriminate POINTER-vs-NULL, not + * tag-vs-success-index (report item #15, F4). + * + * Gate-blind hazard: for a nullable operand AX holds the POINTER and + * successtag()=0, so pre-fix wwstage cgtryprop/cgtryunw emitted + * `CMPQ $0,AX; JE ok` — treating NULL as success and a valid pointer + * as the error, exactly inverted. A valid pointer was early-propagated + * / aborted; a null pointer fell through to a deref (SEGV). cstage + * already carried the nullable arm (cmd/w6c/cgen.c N_TRYPROP/N_TRYUNW) + * and ww's own cgtypetest carries the matching nullable fold; the fix + * mirrors that arm into cgtryprop/cgtryunw. 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. tryprop_valid — `p?` on a VALID pointer must unwrap and continue + * (mutate through it). Pre-fix wwstage early-propagated → 50. + * 2. tryprop_null — `p?` on NULL must propagate (caller sees void). + * Pre-fix wwstage treated null as success → null deref SEGV. + * 3. tryunw_valid — `p!` on a VALID pointer must unwrap. Pre-fix + * wwstage aborted exit(1). + * 4. tryunw_null — `p!` on NULL must abort exit(1). Pre-fix wwstage + * treated null as success → null deref SEGV. + */ +#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[] = { + { "tryprop_valid", + "package main;\n" + "fn g(p: (*int|void)) (*int|void) = { let q = p?; *q = 7; return q; };\n" + "export fn main() i32 = {\n" + " let x: int = 1;\n" + " let r = g(&x);\n" + " if (x != 7) { return 50; };\n" + " return 0;\n" + "};\n", + 0 }, + { "tryprop_null", + "package main;\n" + "fn g(p: (*int|void)) (*int|void) = { let q = p?; *q = 99; return q; };\n" + "export fn main() i32 = {\n" + " let r = g(void);\n" + " match (r) {\n" + " case let q: *int => return 60;\n" + " case void => return 7;\n" + " };\n" + "};\n", + 7 }, + { "tryunw_valid", + "package main;\n" + "fn h(p: (*int|void)) int = { let q = p!; return *q; };\n" + "export fn main() i32 = {\n" + " let x: int = 42;\n" + " return h(&x): i32;\n" + "};\n", + 42 }, + { "tryunw_null", + "package main;\n" + "fn h(p: (*int|void)) int = { let q = p!; return *q; };\n" + "export fn main() i32 = {\n" + " let r = h(void);\n" + " return 50;\n" + "};\n", + 1 }, +}; + +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/ntry.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_try_run: %d/%d checks failed\n", fail, total); + return 1; + } + printf("nullable_try_run: %d/%d ok\n", total, total); + return 0; +}