/* * 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, "rm -rf %s/%s.sepwork %s/pkgc", tmpdir, base, tmpdir); if (system(p)) {} /* best-effort */ 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]; /* #93 sep layout: emit asm to .sepwork/__root.s; pin * WW_PKGCACHE under tmpdir so out/.pkgcache is untouched. */ snprintf(cmd, sizeof cmd, "cd %s && b='%s'; WW_PKGCACHE=%s/pkgc timeout 180 %s build --sep " "-o \"${b%%.ww}\" \"$b\" 2>/dev/null", tmpdir, src, tmpdir, driver); 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.sepwork/__root.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.sepwork/__root.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; }