/* * 758_cgalloc_str_field — cgalloc N_STRUCTLIT str-field store (task #22) * and the cgalloc CALL-site ffiresolve parity (task #25 / #24-part-A). * * Pre-fix (#22): wwstage cgalloc's per-field walk routed str-typed * fields through the generic `MOVQ (SP), BX ; MOVQ AX, foff(BX)` path, * which landed only AX (str.ptr) and clobbered BX (str.len) with the * heap base. str.len silently stayed zero (rt_malloc is MAP_ANON-backed, * so the slot was zero-init rather than garbage — but still wrong). * 990 and 995 byte-identity didn't catch this because nothing in the * bootstrapped selfhost source uses `alloc(T { strfield = "..." })!`. * * Fix (#22): a str-field-typed branch mirrors cmd/w6c/cgen.c:4184-4190 * — route the heap base through CX so BX=len survives both stores * (ptr at foff+0, len at foff+8). Task #23 (slice/tagged/fn-pair * multi-word fields) is the broader follow-up; this row pins str. * * Pre-fix (#25): wwstage's cgalloc emitted a hardcoded `CALL * rt_malloc(SB)` while cstage routed the same site through * ffi_resolve("alloc"), so direct `w6c` vs `w6c_ww` on a fixture * without the @symbol decl in scope diverged (cstage: `CALL * alloc(SB)`; wwstage: `CALL rt_malloc(SB)`). The fix swaps both * cgenexpr.ww and cgenstmt.ww cgalloc CALL sites to * `ffiresolve(c, "malloc")`, aligning wwstage down to the leaner * cstage shape (CLAUDE.md rule 10). The `asm_rows` table below pins * the CALL line via single-file `w6c -o` / `w6c_ww -o`; `ww build` * combines lib/rt/malloc.ww into the fixture and would always supply * the @symbol decl, masking the regression. * * The runtime `rows` below run the generated binary under cstage and * (when present) wwstage; the exit code is the regression-catching * gate. The cstage row is the cross-check oracle. * * Full asm byte-identity is intentionally NOT checked here — the * `(BX)` (cstage txt.c omits zero displacement) vs `0(BX)` (wwstage * emitint(0) is unconditional) divergence still stands. The new str * branch follows the existing float / int branch shape and inherits * the same formatting; aligning all three with cstage is task #24 * part B. */ #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[] = { /* Singleton str field. Pre-fix p.s.len stayed 0; post-fix 5. */ { "alloc_str_singleton", "package main;\n" "import rt;\n" "type holder = struct { s: str };\n" "fn main() i32 = {\n" " let p: *holder = alloc(holder { s = \"hello\" })!;\n" " return p.s.len;\n" "};\n", 5 }, /* i32 field before str. Verifies field iteration restarts cleanly * after the str-field branch and that the i32 store path is * unaffected. Pre-fix: 100 + 0 = 100; post-fix: 100 + 2 = 102. */ { "alloc_i32_then_str", "package main;\n" "import rt;\n" "type holder = struct { n: i32, s: str };\n" "fn main() i32 = {\n" " let p: *holder = alloc(holder { n = 100, s = \"hi\" })!;\n" " return p.n + p.s.len;\n" "};\n", 102 }, /* str field before i32. Verifies foff>0 routing for the trailing * i32 store after the str-field branch. Pre-fix: 0 + 7 = 7; * post-fix: 5 + 7 = 12. */ { "alloc_str_then_i32", "package main;\n" "import rt;\n" "type holder = struct { s: str, n: i32 };\n" "fn main() i32 = {\n" " let p: *holder = alloc(holder { s = \"world\", n = 7 })!;\n" " return p.s.len + p.n;\n" "};\n", 12 }, /* Two str fields. Pre-fix both lens stayed 0; post-fix 3 + 6 = 9. * Pins that `fi = nil` advances correctly between str fields. */ { "alloc_two_str", "package main;\n" "import rt;\n" "type holder = struct { a: str, b: str };\n" "fn main() i32 = {\n" " let p: *holder = alloc(holder { a = \"foo\", b = \"barbaz\" })!;\n" " return p.a.len + p.b.len;\n" "};\n", 9 }, /* Float-then-str-then-int. Pre-fix str.len=0 so result is * 0 + 10 = 10; post-fix 3 + 10 = 13. Confirms the str-field * branch slots between the existing float and integer branches * without disrupting either. */ { "alloc_f64_str_i32", "package main;\n" "import rt;\n" "type holder = struct { f: f64, s: str, n: i32 };\n" "fn main() i32 = {\n" " let p: *holder = alloc(holder { f = 1.5f64, s = \"abc\", n = 10 })!;\n" " return p.s.len + p.n;\n" "};\n", 13 }, /* Read-back via p.s[i]: confirms str.ptr survived the store * (foff=0 case) while still pinning str.len. Pre-fix len=0 so * the loop body never executes; post-fix sum is 'h'+'i' = 209. * Indexes the string by hand to avoid pulling in stdlib. */ { "alloc_str_readback", "package main;\n" "import rt;\n" "type holder = struct { s: str };\n" "fn main() i32 = {\n" " let p: *holder = alloc(holder { s = \"hi\" })!;\n" " let sum: i32 = 0;\n" " let i: i32 = 0;\n" " for (i < p.s.len) {\n" " sum += p.s[i]: i32;\n" " i += 1;\n" " };\n" " return sum;\n" "};\n", 209 }, }; /* run_driver — compile r->src via the given driver and exec; return * the process exit code. Mirror of 703/704. */ static int run_driver(const char *driver, const struct row *r, int i) { char src[64], tmpdir[64], cmd[1024]; snprintf(src, sizeof src, "/tmp/wcas_%d_%d.ww", getpid(), i); snprintf(tmpdir, sizeof tmpdir, "/tmp/wcas_%d_d_%d", getpid(), i); FILE *f = fopen(src, "wb"); if (!f) return -1; fputs(r->src, f); fclose(f); mkdir(tmpdir, 0755); snprintf(cmd, sizeof cmd, "cd %s && %s build %s", tmpdir, driver, src); if (runwait(cmd) != 0) { fprintf(stderr, "row[%s]: build via %s failed\n", r->label, driver); unlink(src); rmdir(tmpdir); return -1; } const char *base = strrchr(src, '/'); base = base ? base + 1 : src; char outbin[128]; snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); char *dot = strrchr(outbin, '.'); if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; int got = runwait(outbin); unlink(src); unlink(outbin); rmdir(tmpdir); return got; } /* asm_row — fixture for the ffiresolve CALL-line check. `want_sym` * is the unqualified symbol expected inside `CALL\t(SB)` from * both stages' .s output. Single-file `w6c` compile (no `ww build` * combine), so the @symbol decl is only in scope when the fixture * writes one — that's how `noscope_*` rows pin the pre-fix wwstage * hardcoded-`rt_malloc` divergence and `withsym_*` rows pin that * ffiresolve actually hits when the decl is present. */ struct asm_row { const char *label; const char *src; const char *want_sym; }; static const struct asm_row asm_rows[] = { /* Int-field, no @symbol in scope. ffiresolve("malloc") misses the * ffi table and returns "malloc" unchanged on both stages. Pre-fix * wwstage hardcoded `CALL rt_malloc(SB)` → diverged from cstage's * ffi_resolve-mediated `CALL malloc(SB)`. */ { "noscope_intfield", "package main;\n" "type holder = struct { n: i32 };\n" "fn dummy() *holder = { return alloc(holder { n = 42 })!; };\n", "malloc" }, /* Str-field, no @symbol in scope. Confirms the str-field branch * (the #22 fix) still routes its CALL through ffiresolve, not a * stray hardcoded literal copied alongside the #22 emit. */ { "noscope_strfield", "package main;\n" "type holder = struct { s: str };\n" "fn dummy() *holder = { return alloc(holder { s = \"hi\" })!; };\n", "malloc" }, /* Two-step let-then-assign. cglet's CALL site (cgenstmt.ww:647) * is the second cgalloc emit point; this row covers it. */ { "noscope_let_then_assign", "package main;\n" "type holder = struct { n: i32 };\n" "fn dummy() *holder = {\n" " let p: *holder = alloc(holder { n = 0 })!;\n" " p.n = 7;\n" " return p;\n" "};\n", "malloc" }, /* Explicit @symbol decl in scope. ffiresolve("malloc") → "rt_malloc" * so both stages emit `CALL rt_malloc(SB)` — positive confirmation * that the ffi table lookup hits, complementing the noscope rows. */ { "withsym_intfield", "package main;\n" "@symbol(\"rt_malloc\") export fn malloc(n: u64) *void;\n" "type holder = struct { n: i32 };\n" "fn dummy() *holder = { return alloc(holder { n = 42 })!; };\n", "rt_malloc" }, }; /* asm_disp_row — pins the foff=0 displacement formatting (#24 part B). * cstage's txt.c prints `(REG)` for zero displacement (cmd/w6c/txt.c * prAdr D_INDIR); pre-fix wwstage emitted `0(REG)` via an unconditional * `emitint(fi.foff)`. Each row supplies a `want_line` that MUST appear * verbatim in BOTH stages' .s output. With the fix (4 emit sites routed * through `emitdispreg` SSoT) wwstage matches cstage; without it the * wwstage half fails because `0(REG)` is a different line. */ struct asm_disp_row { const char *label; const char *src; const char *want_line; }; static const struct asm_disp_row asm_disp_rows[] = { /* str at foff=0. Covers the str-field branch's first store * (cgenexpr.ww cgalloc, MOVQ AX, (CX)). The companion +8 store * always has a non-zero displacement so it's not the gate; the * .ptr store is. */ { "alloc_str_at_offset0", "package main;\n" "type holder = struct { s: str };\n" "fn dummy() *holder = { return alloc(holder { s = \"x\" })!; };\n", "\tMOVQ\tAX, (CX)\n" }, /* str at non-zero foff. Pins that the displacement IS emitted * (`8(CX)`) when foff != 0 — emitdispreg must not suppress * non-zero offsets too. Pre-fix and post-fix both pass this; it * guards against a future over-correction. */ { "alloc_str_at_nonzero_offset", "package main;\n" "type holder = struct { pad: i64, s: str };\n" "fn dummy() *holder = {\n" " return alloc(holder { pad = 0, s = \"x\" })!;\n" "};\n", "\tMOVQ\tAX, 8(CX)\n" }, /* Generic 8-byte field at foff=0 (non-str, non-float path). Covers * the `else` branch's `MOVQ AX, (BX)` store via fieldstoreop. */ { "alloc_int_at_offset0", "package main;\n" "type holder = struct { n: i64 };\n" "fn dummy() *holder = { return alloc(holder { n = 42 })!; };\n", "\tMOVQ\tAX, (BX)\n" }, /* f64 at foff=0. Covers the float branch's `MOVSD X0, (BX)` * store; mirrors the int row but routes through the MOVSD emit. */ { "alloc_f64_at_offset0", "package main;\n" "type holder = struct { f: f64 };\n" "fn dummy() *holder = { return alloc(holder { f = 1.0f64 })!; };\n", "\tMOVSD\tX0, (BX)\n" }, }; /* asm_call_check — compile via direct w6c / w6c_ww (no `ww build`), * extract the `CALL\t(SB)` line referencing alloc/rt_malloc from * each .s file, and verify (a) both stages emit the same line and * (b) the symbol matches r->want_sym. Returns 0 on success. */ static int asm_call_check(const char *bin, const struct asm_row *r, int i) { char src[64], cs[64], ws[64], cmd[1024]; snprintf(src, sizeof src, "/tmp/wcas_asm_%d_%d.ww", getpid(), i); snprintf(cs, sizeof cs, "/tmp/wcas_asm_%d_%d_c.s", getpid(), i); snprintf(ws, sizeof ws, "/tmp/wcas_asm_%d_%d_w.s", getpid(), i); FILE *f = fopen(src, "wb"); if (!f) return -1; fputs(r->src, f); fclose(f); int rc = 0; snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, src); if (runwait(cmd) != 0) { fprintf(stderr, "asm[%s]: w6c errored\n", r->label); unlink(src); return -1; } snprintf(cmd, sizeof cmd, "%s/w6c_ww -o %s %s 2>/dev/null", bin, ws, src); if (runwait(cmd) != 0) { fprintf(stderr, "asm[%s]: w6c_ww errored\n", r->label); unlink(src); unlink(cs); return -1; } char want[64]; snprintf(want, sizeof want, "\tCALL\t%s(SB)\n", r->want_sym); char cline[256] = {0}, wline[256] = {0}; FILE *fc = fopen(cs, "rb"); FILE *fw = fopen(ws, "rb"); if (!fc || !fw) { fprintf(stderr, "asm[%s]: open .s failed\n", r->label); rc = -1; } else { char buf[256]; while (fgets(buf, sizeof buf, fc)) { if (strstr(buf, "\tCALL\t") && (strstr(buf, "alloc(SB)") || strstr(buf, "rt_malloc(SB)"))) { strncpy(cline, buf, sizeof cline - 1); break; } } while (fgets(buf, sizeof buf, fw)) { if (strstr(buf, "\tCALL\t") && (strstr(buf, "alloc(SB)") || strstr(buf, "rt_malloc(SB)"))) { strncpy(wline, buf, sizeof wline - 1); break; } } if (cline[0] == '\0' || wline[0] == '\0') { fprintf(stderr, "asm[%s]: no CALL alloc line found (c=%d w=%d)\n", r->label, cline[0] != '\0', wline[0] != '\0'); rc = -1; } else if (strcmp(cline, wline) != 0) { /* strncpy may leave no '\n'; both lines came from * fgets so they include it. Strip for cleaner err. */ char *p; if ((p = strchr(cline, '\n'))) *p = '\0'; if ((p = strchr(wline, '\n'))) *p = '\0'; fprintf(stderr, "asm[%s]: cstage=<%s> wwstage=<%s>\n", r->label, cline, wline); rc = -1; } else if (strcmp(cline, want) != 0) { char *p; if ((p = strchr(cline, '\n'))) *p = '\0'; fprintf(stderr, "asm[%s]: got=<%s> want=<\tCALL\t%s(SB)>\n", r->label, cline, r->want_sym); rc = -1; } } if (fc) fclose(fc); if (fw) fclose(fw); unlink(src); unlink(cs); unlink(ws); return rc; } /* file_contains — true iff `path`'s contents contain `needle` as a * substring. Tab/newline-bearing needles match the literal byte sequence * the .s file holds, so `(CX)\n` does not collide with `0(CX)\n`. */ static int file_contains(const char *path, const char *needle) { FILE *f = fopen(path, "rb"); if (!f) return 0; fseek(f, 0, SEEK_END); long sz = ftell(f); if (sz < 0 || sz > (1<<20)) { fclose(f); return 0; } fseek(f, 0, SEEK_SET); char *buf = (char*)malloc((size_t)sz + 1); if (!buf) { fclose(f); return 0; } size_t got = fread(buf, 1, (size_t)sz, f); buf[got] = '\0'; fclose(f); int hit = strstr(buf, needle) != NULL; free(buf); return hit; } /* asm_disp_check — compile via direct w6c / w6c_ww and verify that the * literal `r->want_line` appears in BOTH stages' .s output. Pre-fix * wwstage substituted `0(REG)` for `(REG)` so the foff=0 rows fail on * the wwstage half; post-fix both halves carry the same text. */ static int asm_disp_check(const char *bin, const struct asm_disp_row *r, int i) { char src[64], cs[64], ws[64], cmd[1024]; snprintf(src, sizeof src, "/tmp/wcas_disp_%d_%d.ww", getpid(), i); snprintf(cs, sizeof cs, "/tmp/wcas_disp_%d_%d_c.s", getpid(), i); snprintf(ws, sizeof ws, "/tmp/wcas_disp_%d_%d_w.s", getpid(), i); FILE *f = fopen(src, "wb"); if (!f) return -1; fputs(r->src, f); fclose(f); int rc = 0; snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, src); if (runwait(cmd) != 0) { fprintf(stderr, "disp[%s]: w6c errored\n", r->label); unlink(src); return -1; } snprintf(cmd, sizeof cmd, "%s/w6c_ww -o %s %s 2>/dev/null", bin, ws, src); if (runwait(cmd) != 0) { fprintf(stderr, "disp[%s]: w6c_ww errored\n", r->label); unlink(src); unlink(cs); return -1; } int chit = file_contains(cs, r->want_line); int whit = file_contains(ws, r->want_line); if (!chit || !whit) { fprintf(stderr, "disp[%s]: want_line missing (cstage=%d wwstage=%d) " "want=<%s>\n", r->label, chit, whit, r->want_line); rc = -1; } unlink(src); unlink(cs); unlink(ws); return rc; } int main(void) { const char *bin = getenv("BIN"); if (!bin) bin = "out/bin"; char absbin[1024]; if (bin[0] != '/') { char cwd[1024]; if (getcwd(cwd, sizeof cwd) == NULL) return 1; snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); bin = absbin; } char cdrv[1024]; snprintf(cdrv, sizeof cdrv, "%s/ww", bin); char wdrv[1024]; snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); struct { const char *name; const char *path; int gated_on_existence; } drivers[] = { { "cstage", cdrv, 0 }, { "wwstage", wdrv, 1 }, { NULL, NULL, 0 }, }; int n = (int)(sizeof rows / sizeof rows[0]); int total = 0, fail = 0; for (int d = 0; drivers[d].name; d++) { if (drivers[d].gated_on_existence && access(drivers[d].path, X_OK) != 0) { fprintf(stderr, "cgalloc_str_field: skip %s (no %s)\n", drivers[d].name, drivers[d].path); continue; } for (int i = 0; i < n; i++) { int got = run_driver(drivers[d].path, &rows[i], i); total++; if (got != rows[i].want) { fprintf(stderr, "cgalloc_str_field[%s][%s]: exit=%d want=%d\n", drivers[d].name, rows[i].label, got, rows[i].want); fail++; } } } /* Asm CALL-line check via direct w6c / w6c_ww. Gated on w6c_ww * existence — when wwstage isn't built yet the cstage half alone * can't catch the divergence. */ char w6c_ww[1024]; snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); if (access(w6c_ww, X_OK) == 0) { int an = (int)(sizeof asm_rows / sizeof asm_rows[0]); for (int i = 0; i < an; i++) { total++; if (asm_call_check(bin, &asm_rows[i], i) != 0) fail++; } int dn = (int)(sizeof asm_disp_rows / sizeof asm_disp_rows[0]); for (int i = 0; i < dn; i++) { total++; if (asm_disp_check(bin, &asm_disp_rows[i], i) != 0) fail++; } } else { fprintf(stderr, "cgalloc_str_field: skip asm rows (no %s)\n", w6c_ww); } if (fail) { fprintf(stderr, "cgalloc_str_field: %d/%d fixtures failed\n", fail, total); return 1; } printf("cgalloc_str_field: %d/%d ok\n", total, total); return 0; }