diff --git a/Makefile b/Makefile index f3db0929..daf96951 100644 --- a/Makefile +++ b/Makefile @@ -337,6 +337,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_amp_fn_assign_run \ $(BIN)/test_xmod_alias_struct_collide_run \ $(BIN)/test_structvariant_largeunion_return \ + $(BIN)/test_narrow_alias_deref_store \ $(BIN)/test_bufio_vstream_run \ $(BIN)/test_log_vstream_run \ $(BIN)/test_use_promote_alias \ @@ -751,6 +752,16 @@ $(BIN)/test_structvariant_largeunion_return: test/wcc/785_structvariant_largeuni $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# #11: narrow deref-store through *(!i32-alias) must emit MOVL (4B), not +# MOVQ (8B which over-writes the adjacent 4 bytes). Both-stage byte-id + +# runtime over-write guard (sentinel high word survives iff MOVL). +# Self-contained single-file probes via the driver, no lib imports. +$(BIN)/test_narrow_alias_deref_store: test/wcc/786_narrow_alias_deref_store.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_fmt_vstream_mods_run: test/wcc/780_fmt_vstream_mods_run.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 fcc315e8..79a7f0d4 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -22571,6 +22571,20 @@ fn cgassign(c: *cgen, n: *node) void = { else { if (streq(pe.str, "f32")) { elemfloat = true; elemf32 = true; } else { let ps: i32 = primsize(pe.str); + // #11: primsize is name-keyed and + // misses a `!`/enum/name alias + // (`type errno = !i32`); peel it to + // the underlying primitive width so + // the store narrows, as cstage's + // type-resolved pointee does + // (cgen.c:4647-4652). Residual: + // non-ident pointers + str/float-alias + // deref-store widths stay name-blind + // (#10 wwstage->tinfo SSoT). + if (ps == 0) { + let uns: bool = false; + typenodeprimresolved(c, pe, &ps, &uns); + }; if (ps == 1) { storeop = "MOVB"; } else { if (ps == 4) { storeop = "MOVL"; }; }; }; }; }; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 89c81f86..47defe5f 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -4427,6 +4427,20 @@ fn cgassign(c: *cgen, n: *node) void = { else { if (streq(pe.str, "f32")) { elemfloat = true; elemf32 = true; } else { let ps: i32 = primsize(pe.str); + // #11: primsize is name-keyed and + // misses a `!`/enum/name alias + // (`type errno = !i32`); peel it to + // the underlying primitive width so + // the store narrows, as cstage's + // type-resolved pointee does + // (cgen.c:4647-4652). Residual: + // non-ident pointers + str/float-alias + // deref-store widths stay name-blind + // (#10 wwstage->tinfo SSoT). + if (ps == 0) { + let uns: bool = false; + typenodeprimresolved(c, pe, &ps, &uns); + }; if (ps == 1) { storeop = "MOVB"; } else { if (ps == 4) { storeop = "MOVL"; }; }; }; }; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 02ac53d5..b9cb9098 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -22571,6 +22571,20 @@ fn cgassign(c: *cgen, n: *node) void = { else { if (streq(pe.str, "f32")) { elemfloat = true; elemf32 = true; } else { let ps: i32 = primsize(pe.str); + // #11: primsize is name-keyed and + // misses a `!`/enum/name alias + // (`type errno = !i32`); peel it to + // the underlying primitive width so + // the store narrows, as cstage's + // type-resolved pointee does + // (cgen.c:4647-4652). Residual: + // non-ident pointers + str/float-alias + // deref-store widths stay name-blind + // (#10 wwstage->tinfo SSoT). + if (ps == 0) { + let uns: bool = false; + typenodeprimresolved(c, pe, &ps, &uns); + }; if (ps == 1) { storeop = "MOVB"; } else { if (ps == 4) { storeop = "MOVL"; }; }; }; }; }; diff --git a/test/wcc/786_narrow_alias_deref_store.c b/test/wcc/786_narrow_alias_deref_store.c new file mode 100644 index 00000000..e6de1720 --- /dev/null +++ b/test/wcc/786_narrow_alias_deref_store.c @@ -0,0 +1,269 @@ +/* + * 786_narrow_alias_deref_store — project #11 close. Pins that a narrow + * deref-store through a pointer to a `!`-flagged integer ALIAS + * (`type errno = !i32`) narrows to the alias's underlying width on + * BOTH stages byte-identically (rule-10). + * + * THE BUG (wwstage-only, cs!=ww): the deref-store `*p=v` width logic + * (selfhost/cmd/wcc/cgenexpr.ww) was name-keyed — `primsize(pe.str)` + * on the syntactic pointee node. For `*errno` the node is N_TNAME + * "errno"; primsize("errno")==0 (not a primitive name), so it fell to + * the MOVQ (8-byte) default and wrote 8 bytes through a 4-byte pointee + * — clobbering the adjacent 4 bytes. cstage is type-resolved (peel the + * pointer's ->type NAMED -> TY_PTR -> sub -> NAMED -> i32, cgen.c:4647- + * 4652) so it emits MOVL (4-byte). Plain `*i32` was byte-id (primsize + * "i32"==4); only the alias missed the peel. + * + * THE FIX (#11): primsize-first, and when it returns 0 fall back to + * typenodeprimresolved (the existing N_TBANG/N_TENUM/N_TNAME alias + * walker) to reach the underlying width -> MOVL. The errno port's + * opaque_ store is exactly this `let p = (&x.data): *errno; *p = e;` + * ident-pointer shape. + * + * GATE-BLIND class (the int-cast-no-truncate family): a too-wide store + * is invisible unless something reads the over-written neighbour. So + * the runtime row pre-seeds the HIGH 4 bytes of a u64 with a sentinel, + * stores a small i32 through `*(!i32-alias)` into the LOW 4 bytes, and + * reads the high word back: 42 iff the sentinel survived (MOVL), 7 iff + * an 8-byte MOVQ zeroed it. Layout-independent (single u64, no field- + * packing assumption); the [3]u64 carrier keeps the local multi-word + * to dodge the unrelated single-slot zero-init divergence (#213). + * + * row | shape | exit | byte-id + * -----------------+----------------------------------------+------+-------- + * alias_narrow | *(!i32-alias) deref-store into the low | 42 | cs==ww + * | half of a sentinel'd u64; high word | | (MOVL) + * | read back proves no over-write | | + * plain_i32_ctrl | same via a plain *i32 (no alias) — the | 42 | cs==ww + * | path that already narrowed; guards | | (MOVL) + * | the non-alias case stays byte-id | | + * + * GATE POLARITY: must stay GREEN. Red means the alias narrow-store + * regressed to MOVQ (byte-id break + over-write), or the plain *i32 + * narrow regressed. + */ +#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 want_exit; + int stage_mask; + int byte_id; +}; + +static const struct row rows[] = { + /* POSITIVE byte-id + runtime: deref-store through *(!i32-alias). */ + { "alias_narrow", + "package main;\n" + "type errno = !i32;\n" + "type box = struct { data: [3]u64 };\n" + "fn store(v: errno) box = {\n" + " let b: box;\n" + " b.data[0] = 0xFFFFFFFF00000000u64;\n" + " let p = (&b.data): *errno;\n" + " *p = v;\n" + " return b;\n" + "};\n" + "export fn main() i32 = {\n" + " let r = store(7i32: errno);\n" + " let hi: u64 = r.data[0] >> 32u64;\n" + " if (hi == 0u64) { return 7; };\n" + " return 42;\n" + "};\n", + 42, STAGE_CS | STAGE_WW, 1 }, + + /* CONTROL byte-id + runtime: same shape via a PLAIN *i32 (no alias) + * — the path that already narrowed pre-fix; guards we did not break + * it. */ + { "plain_i32_ctrl", + "package main;\n" + "type box = struct { data: [3]u64 };\n" + "fn store(v: i32) box = {\n" + " let b: box;\n" + " b.data[0] = 0xFFFFFFFF00000000u64;\n" + " let p = (&b.data): *i32;\n" + " *p = v;\n" + " return b;\n" + "};\n" + "export fn main() i32 = {\n" + " let r = store(7i32);\n" + " let hi: u64 = r.data[0] >> 32u64;\n" + " if (hi == 0u64) { return 7; };\n" + " return 42;\n" + "};\n", + 42, STAGE_CS | STAGE_WW, 1 }, +}; + +static void +cleanup_tmp(const char *tmpdir, const char *base) +{ + char p[1024]; + snprintf(p, sizeof p, "%s/%s", tmpdir, base); unlink(p); + 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); + rmdir(tmpdir); +} + +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 int +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 %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 struct row *r, int seq) +{ + char tmpdir[256], src[512], base[64], outbin[768]; + snprintf(tmpdir, sizeof tmpdir, "/tmp/nas_%d_d_%d", getpid(), seq); + snprintf(base, sizeof base, "main786"); + 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; + if (build_via_driver(driver, tmpdir, src) == 0) { + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + rc = runwait(outbin); + } else { + rc = -1; + } + 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/nas_%d_c_%d", getpid(), seq); + snprintf(tdw, sizeof tdw, "/tmp/nas_%d_w_%d", getpid(), seq); + snprintf(base, sizeof base, "main786"); + 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 cwd[256]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + char absbin[512]; + if (bin[0] != '/') { + 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, seq = 0; + int wwpresent = (access(wdrv, X_OK) == 0); + + for (int i = 0; i < n; i++) { + const struct row *r = &rows[i]; + + if (r->stage_mask & STAGE_CS) { + total++; + int got = run_row(cdrv, r, seq++); + if (got != r->want_exit) { + fprintf(stderr, "narrow_alias[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, "narrow_alias[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, "narrow_alias[byte-id][%s]: cstage vs wwstage asm differs\n", + r->label); + fail++; + } + } + } + } + + if (fail) { + fprintf(stderr, "narrow_alias: %d/%d checks failed\n", fail, total); + return 1; + } + printf("narrow_alias: %d/%d ok\n", total, total); + return 0; +}