/* * 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 #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 }, }; /* nonzero on any cleanup failure; ENOENT tolerated because legs that * fail early never create the later artifacts. */ static int cleanup_tmp(const char *tmpdir, const char *base) { char p[1024]; int bad = 0; snprintf(p, sizeof p, "%s/%s", tmpdir, base); if (unlink(p) != 0 && errno != ENOENT) { perror(p); bad = 1; } snprintf(p, sizeof p, "%s/%s.ww", tmpdir, base); if (unlink(p) != 0 && errno != ENOENT) { perror(p); bad = 1; } snprintf(p, sizeof p, "rm -rf %s/%s.sepwork", tmpdir, base); if (runwait(p) != 0) { fprintf(stderr, "cleanup failed: %s\n", p); bad = 1; } snprintf(p, sizeof p, "%s/%s.o", tmpdir, base); if (unlink(p) != 0 && errno != ENOENT) { perror(p); bad = 1; } snprintf(p, sizeof p, "%s/%s.combined.ww", tmpdir, base); if (unlink(p) != 0 && errno != ENOENT) { perror(p); bad = 1; } if (rmdir(tmpdir) != 0) { perror(tmpdir); bad = 1; } return bad; } static int write_source(const char *path, const char *src) { FILE *f = fopen(path, "wb"); if (!f) return -1; if (fputs(src, f) == EOF) { fclose(f); return -1; } if (fclose(f) != 0) return -1; 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 * all outputs stay under tmpdir. */ snprintf(cmd, sizeof cmd, "cd %s && b='%s'; timeout 180 %s build -S " "-o \"${b%%.ww}\" \"$b\" 2>/dev/null", tmpdir, src, driver); return runwait(cmd); } /* 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"); int rc = -1, tdw_owned = 0, cleanfail = 0; if (mkdir(tdc, 0755) != 0) { perror(tdc); return -1; } if (mkdir(tdw, 0755) != 0) { perror(tdw); goto out; } tdw_owned = 1; snprintf(src, sizeof src, "%s/%s.ww", tdc, base); if (write_source(src, r->src) != 0) goto out; 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: cleanfail = cleanup_tmp(tdc, base); if (tdw_owned && cleanup_tmp(tdw, base) != 0) cleanfail = 1; /* a leaked tree fails an otherwise-green row; a real diff result * (rc == -1) is never overwritten (110's cleanfail shape). */ if (cleanfail && rc == 0) rc = -1; 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 (wwpresent && 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; }