/* * 753_convwrap_audit — sentinel for task #17 (structural close of the * #4-trio convenience-wrapper N_DOT-arm audit). wwstage cgen* helpers * that take a *node callee and probe its return shape via bare-leaf * fnretlookup stripped the N_DOT module hint, same wedge shape as #16 * (callee_variadic_param, d9b0c90) through a different family of * consumers. Eight latent sites caught in the sweep: * * cgenstmt.ww:170-189 cgreturn forwardtagged probe * cgenstmt.ww:1077-1090 cgmlet tuple-return shape probe * cgenexpr.ww:1729-1742 cgdot fn-rvalue probe (`mod.fn` LEAQ) * cgenexpr.ww:155-176 cgtryprop succisstr probe * cgenexpr.ww:200-220 cgtryunw succisstr probe * cgenutil.ww:1528-1540 callsretsize (sret arg-prep) * cgenutil.ww:1776-1794 inferletcalltype (`let x = f()?` tnode) * cgenutil.ww:2894-2944 rhstaggedabicall N_CALL branch * * Each consumed callee.str (leaf) for both N_IDENT and N_DOT arms, * then routed through bare fnretlookup. Post-#4e the bare walk is * same-module-first then head-walk fallback — for a cross-module * N_DOT call from a caller whose c.curmod doesn't match either side, * the head walk returns whichever module's same-leaf fn sits at the * head of c.fnrets. When that head-side fn has a divergent return * shape (tagged-vs-scalar, str-vs-scalar pair, sret-vs-flat, etc.) * the consumer fires the wrong dispatch arm. * * Cstage carries no sister bug: cmd/w6c/cgen.c reads every callee * return shape from the typed `n->lhs->type` (TY_FN sig). Mirror of * #4d / #28 / #31 / #34 / #16: cstage sidesteps every bare-leaf * table. * * Pin: row 1 exercises cgmlet (cgenstmt.ww:1077-1090). alpha exports * a same-leaf `foo` returning (i64, str), beta exports a same-leaf * `foo` returning (i64, i64). Source order is beta first, main next, * alpha LAST — so alpha.foo prepends to the head of c.fnrets. Pre- * fix wwstage's cgmlet probe walks past main (no `foo`) into the * head and grabs alpha.foo's (i64, str) tuple, makes s0_is_str=false * but s1_is_str=true → fires the (scalar, str) emit branch (MOVQ DX * → b.ptr + MOVQ CX → b.len) for a beta.foo return that's actually * (AX=i64, DX=i64) — no CX defined on this ABI. Post-fix the probe * routes through fnretlookupmod(c, "foo", "beta") and the (scalar, * scalar) branch fires (MOVQ AX → a, MOVQ DX → b, no CX). * * Asserts: inside main.run TEXT sym before its first RET, no `MOVQ * CX,` appears — pre-fix wwstage emits the str.len store that has * no defined source on a (i64, i64) tuple return. cstage always * picks beta.foo via typed AST, so the same pattern check holds * on both stages. No byte-id check — `let a, b = call();` has a * pre-existing cosmetic cs-vs-ws divergence (cstage emits a * defensive PUSHQ/POPQ DX around the AX-store of `a` that wwstage * skips). Out of #17 scope. */ #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; const char *textsym; /* TEXT sym containing the call */ const char *want_imm; /* substring that MUST appear */ const char *bad_imm; /* substring that MUST NOT appear */ }; /* beta.foo first (registered first → tail of c.fnrets), main next, * alpha LAST (registered last → head). Pre-fix bare fnretlookup in * cgmlet falls through same-module to head walk → alpha.foo → its * (i64, str) shape drives the (scalar, str) emit branch (MOVQ CX, * — str.len store with no source defined by beta.foo's (i64, i64) * tuple ABI). Post-fix the probe goes through fnretlookupmod with * "beta" hint → beta.foo → scalar-pair branch (no CX). */ static const struct row rows[] = { { "cgmlet_tuple_modshadow", "package beta;\n" "export fn foo() (i64, i64) = { return (10i64, 20i64); };\n" "package main;\n" "import alpha;\n" "import beta;\n" "export fn run() i32 = {\n" "\tlet a, b = beta.foo();\n" "\tif (a != 10i64) { return 1; };\n" "\tif (b != 20i64) { return 2; };\n" "\treturn 0;\n" "};\n" "export fn main() i32 = { return run(); };\n" "package alpha;\n" "export fn foo() (i64, str) = { return (10i64, \"hi\"); };\n", "TEXT main.run", "MOVQ\tAX,", "MOVQ\tCX," }, }; static int slurp(const char *path, char *buf, size_t cap) { FILE *f = fopen(path, "rb"); if (!f) return -1; size_t n = fread(buf, 1, cap - 1, f); fclose(f); buf[n] = '\0'; return (int)n; } static int emit_s(const char *w6c, const struct row *r, int i, char *out_s, size_t cap) { char src[64], cmd[1024]; snprintf(src, sizeof src, "/tmp/cwa_%d_%d.ww", getpid(), i); snprintf(out_s, cap, "/tmp/cwa_%d_%d_%s.s", getpid(), i, w6c[strlen(w6c) - 1] == 'w' ? "ww" : "c"); FILE *f = fopen(src, "wb"); if (!f) return -1; fputs(r->src, f); fclose(f); snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, out_s, src); int rc = runwait(cmd); int cleanfail = 0; if (unlink(src) != 0 && errno != ENOENT) { perror(src); cleanfail = 1; } /* a failing compile can still leave a partial .s behind */ if (rc != 0 && unlink(out_s) != 0 && errno != ENOENT) { perror(out_s); cleanfail = 1; } if (cleanfail && rc == 0) rc = -1; return rc; } static int check_imm(const char *spath, const struct row *r, const char *stage) { char buf[1 << 14]; if (slurp(spath, buf, sizeof buf) < 0) { fprintf(stderr, "row[%s][%s]: cannot read %s\n", r->label, stage, spath); return -1; } const char *fn = strstr(buf, r->textsym); if (!fn) { fprintf(stderr, "row[%s][%s]: no %s in %s\n", r->label, stage, r->textsym, spath); return -1; } const char *ret = strstr(fn, "\tRET"); if (!ret) { fprintf(stderr, "row[%s][%s]: no RET inside %s\n", r->label, stage, r->textsym); return -1; } const char *good = strstr(fn, r->want_imm); if (!good || good >= ret) { fprintf(stderr, "row[%s][%s]: want_imm %s missing inside %s\n", r->label, stage, r->want_imm, r->textsym); return -1; } const char *bad = strstr(fn, r->bad_imm); if (bad && bad < ret) { fprintf(stderr, "row[%s][%s]: bad_imm %s present inside %s — wrong-module shape probe fired\n", r->label, stage, r->bad_imm, r->textsym); return -1; } return 0; } 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 w6c[640], w6c_ww[640]; snprintf(w6c, sizeof w6c, "%s/w6c", bin); snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); int have_ww = (access(w6c_ww, X_OK) == 0); int n = (int)(sizeof rows / sizeof rows[0]); int total = 0, fail = 0; int cleanfail = 0; for (int i = 0; i < n; i++) { char cs_path[128], ws_path[128]; if (emit_s(w6c, &rows[i], i, cs_path, sizeof cs_path) != 0) { fprintf(stderr, "convwrap_audit[cstage][%s]: w6c failed\n", rows[i].label); fail++; total++; continue; } total++; if (check_imm(cs_path, &rows[i], "cstage") != 0) fail++; if (!have_ww) { if (unlink(cs_path) != 0 && errno != ENOENT) { perror(cs_path); cleanfail = 1; } continue; } if (emit_s(w6c_ww, &rows[i], i, ws_path, sizeof ws_path) != 0) { fprintf(stderr, "convwrap_audit[wwstage][%s]: w6c_ww failed\n", rows[i].label); fail++; total++; if (unlink(cs_path) != 0 && errno != ENOENT) { perror(cs_path); cleanfail = 1; } continue; } total++; if (check_imm(ws_path, &rows[i], "wwstage") != 0) fail++; if (unlink(cs_path) != 0 && errno != ENOENT) { perror(cs_path); cleanfail = 1; } if (unlink(ws_path) != 0 && errno != ENOENT) { perror(ws_path); cleanfail = 1; } } /* a cleanup failure must not pass silently, but must not mask a * real assertion failure's own report either. */ if (cleanfail && fail == 0) { fprintf(stderr, "convwrap_audit: cleanup failed\n"); return 1; } if (fail) { fprintf(stderr, "convwrap_audit: %d/%d fixtures failed\n", fail, total); return 1; } printf("convwrap_audit: %d/%d ok\n", total, total); return 0; }