cstage+selfhost+test: zero unused ABI words in cgreturn variant-widen (#18)
cgreturn's variant-widen arm only filled the registers each variant's payload needed: scalar variants left CX and R8 stale; str variant left R8 stale. The receiver (cg_widen_tagged_store non-N_IDENT branch) writes all four ABI words to the dst slot unconditionally, so caller-side residue in CX/R8 (the array-index IMULQ being the canonical primer) landed at slot+16 and slot+24. Worker-fmtparser surfaced this through fprintf's loop body where array indexing primed CX and a 24B-return helper failed to clear it; bug isn't loop-specific — straight-line repro at /tmp/wcrs_repro/ repro8.ww confirms. Patch: emit `MOVQ $0, CX` after the variant's register shuffle when the slot exceeds 16B and the variant doesn't fill CX; same for R8 when the slot exceeds 24B. Symmetric across cstage cgen.c and wwstage cgenstmt.ww. Order: zero-MOVQs precede `MOVQ $tag, AX` so AX-as-staging stays safe. Inline comment at cg_widen_tagged_store non-N_IDENT branch documents the producer-zero contract. Test 707 (cgreturn_variant_zero): 6 rows × both stages = 12 fixtures. Covers scalar/bool/str returns after array-index priming in straight-line / single-loop body / nested-loop body / mixed-variant loop. Asm byte-identity check intentionally omitted; wwstage taggedvariantindex divergence on str/bool N_IDENT is filed as task #20. 995_self_rebuild covers the broader cross-stage drift surface. ww2 == ww3 == ww4 byte-identical post-fix. 67/67 green. Deferred (followups filed): #20 wwstage taggedvariantindex, #21 [N]str/[N]bool array-literal non-pointer-half writes, #22 consolidate variant-widen into uniform scratch-slot path.
This commit is contained in:
288
test/wcc/707_cgreturn_variant_zero.c
Normal file
288
test/wcc/707_cgreturn_variant_zero.c
Normal file
@@ -0,0 +1,288 @@
|
||||
/*
|
||||
* 707_cgreturn_variant_zero — tagged-return ABI variant-widen zero-pad
|
||||
* for unused AX/DX/CX/R8 words (task #18).
|
||||
*
|
||||
* Pre-fix: cgreturn's `!istagged && !isstruct` variant-widen arm only
|
||||
* filled the registers a given variant actually uses (scalar → DX;
|
||||
* str → DX, CX; slice → DX, CX, R8). The unused AX/CX/R8 words were
|
||||
* left holding whatever the caller's last write parked there. The
|
||||
* receive side (cg_widen_tagged_store call-source arm) writes
|
||||
* AX/DX/CX/R8 into the dst slot unconditionally, sized by the slot
|
||||
* total — so stale registers landed at slot+16 / slot+24 and silently
|
||||
* corrupted the tagged-union slot.
|
||||
*
|
||||
* The corruption only surfaced when the caller had primed CX/R8
|
||||
* shortly before the call. Array / slice indexing emits
|
||||
* `MOVQ $stride, CX; IMULQ CX, AX` — the canonical primer. Happens
|
||||
* once per index, including in for-loop bodies. Hence the original
|
||||
* "for-loop miscompile" framing; the bug is in fact context-free,
|
||||
* but an index expression in any straight-line code triggers it
|
||||
* just as well.
|
||||
*
|
||||
* Fix: in cgreturn's variant-widen arm, after the variant-specific
|
||||
* register shuffle and before the tag-into-AX, emit `MOVQ $0, CX` /
|
||||
* `MOVQ $0, R8` for variants that don't naturally fill those words,
|
||||
* conditional on the dst tagged-union slot size. Symmetric across
|
||||
* cstage cgen.c and wwstage cgenstmt.ww.
|
||||
*
|
||||
* What this test pins (runtime only):
|
||||
* - Every scalar-variant arm of a 24B tagged-union return zeroes
|
||||
* slot+16 (CX) regardless of caller priming.
|
||||
* - The str variant of a 24B slot still propagates .len correctly
|
||||
* into slot+16 (the fix's `if (rsz > 24)` guard skips the R8
|
||||
* zero for the 24B case, leaving CX/.len intact).
|
||||
* - Cross-shape probes: straight-line array-index call, in-loop
|
||||
* body call, nested-loop inner-body call.
|
||||
*
|
||||
* Asm byte-identity is NOT diffed here: wwstage has a pre-existing
|
||||
* gap in taggedvariantindex's str/bool-N_IDENT lookup (returns 0
|
||||
* regardless of declared variant), so non-i64 variant rows produce
|
||||
* a divergent `MOVQ $tag, AX`. The fix in this commit IS symmetric
|
||||
* (the new `MOVQ $0, CX` / `MOVQ $0, R8` lines match byte-for-byte
|
||||
* across stages); global bootstrap byte-identity at test 995
|
||||
* covers it. The wwstage variant-index gap is tracked separately.
|
||||
*
|
||||
* Test rows use `[N]i64` array indexing as the CX primer (well-
|
||||
* supported on both stages) to dodge a pre-existing cstage/wwstage
|
||||
* divergence in slice-creation reg scheduling.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
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[] = {
|
||||
/* 1. Straight-line array-index then scalar-variant 24B-tagged
|
||||
* call. Array indexing emits MOVQ $8, CX; IMULQ CX, AX — so CX
|
||||
* carries 8 into the call. Pre-fix: slot+16 = 8. Post-fix: 0. */
|
||||
{ "scalar_24B_after_arridx",
|
||||
"type ft = (i64 | str | bool);\n"
|
||||
"fn aski64(v: i64) ft = { return v; };\n"
|
||||
"fn main() i32 = {\n"
|
||||
" let arr: [3]i64 = [10i64, 20i64, 30i64];\n"
|
||||
" let i: i32 = 1;\n"
|
||||
" let v: i64 = arr[i];\n"
|
||||
" let a: ft = aski64(v);\n"
|
||||
" let p: u64 = (&a): u64;\n"
|
||||
" let p2: *i64 = (p + 16u64): *i64;\n"
|
||||
" let w: i64 = *p2;\n"
|
||||
" return w: i32;\n"
|
||||
"};\n",
|
||||
0 },
|
||||
/* 2. Bool variant of the same 24B slot. i64-array-index primer
|
||||
* (well-supported shape) -> derive bool -> call -> probe slot+16.
|
||||
* Pre-fix: stride leak. Post-fix: 0. */
|
||||
{ "bool_24B_after_arridx",
|
||||
"type ft = (i64 | str | bool);\n"
|
||||
"fn askbool(b: bool) ft = { return b; };\n"
|
||||
"fn main() i32 = {\n"
|
||||
" let arr: [3]i64 = [1i64, 0i64, 1i64];\n"
|
||||
" let i: i32 = 0;\n"
|
||||
" let v: i64 = arr[i];\n"
|
||||
" let b: bool = v != 0i64;\n"
|
||||
" let a: ft = askbool(b);\n"
|
||||
" let p: u64 = (&a): u64;\n"
|
||||
" let p2: *i64 = (p + 16u64): *i64;\n"
|
||||
" let w: i64 = *p2;\n"
|
||||
" return w: i32;\n"
|
||||
"};\n",
|
||||
0 },
|
||||
/* 3. Str variant of a 24B slot — sender already fills CX with
|
||||
* the len, so slot+16 was correct pre-fix too. Sanity row: must
|
||||
* keep working; the fix's `if (rsz > 24)` guard skips the R8
|
||||
* zero for the 24B case, leaving CX (and slot+16) at len. The
|
||||
* i64-array index primes CX; the str literal feeds askstr. */
|
||||
{ "str_24B_after_arridx",
|
||||
"type ft = (i64 | str | bool);\n"
|
||||
"fn askstr(s: str) ft = { return s; };\n"
|
||||
"fn main() i32 = {\n"
|
||||
" let arr: [3]i64 = [10i64, 20i64, 30i64];\n"
|
||||
" let i: i32 = 1;\n"
|
||||
" let _v: i64 = arr[i];\n"
|
||||
" let a: ft = askstr(\"world\");\n"
|
||||
" let p: u64 = (&a): u64;\n"
|
||||
" let p2: *i64 = (p + 16u64): *i64;\n"
|
||||
" let w: i64 = *p2;\n"
|
||||
" return w: i32;\n"
|
||||
"};\n",
|
||||
5 },
|
||||
/* 4. In-loop body: scalar variant call. Same shape as row 1,
|
||||
* but inside a `for (i < 3)` body where IMULQ primes CX
|
||||
* on every iteration. Pre-fix: slot+16 leaks 8 every iter;
|
||||
* sum-of-leaks > 0. Post-fix: 0. */
|
||||
{ "scalar_24B_in_loop",
|
||||
"type ft = (i64 | str | bool);\n"
|
||||
"fn aski64(v: i64) ft = { return v; };\n"
|
||||
"fn main() i32 = {\n"
|
||||
" let arr: [3]i64 = [10i64, 20i64, 30i64];\n"
|
||||
" let i: i32 = 0;\n"
|
||||
" let bad: i32 = 0;\n"
|
||||
" for (i < 3) {\n"
|
||||
" let v: i64 = arr[i];\n"
|
||||
" let a: ft = aski64(v);\n"
|
||||
" let p: u64 = (&a): u64;\n"
|
||||
" let p2: *i64 = (p + 16u64): *i64;\n"
|
||||
" let w: i64 = *p2;\n"
|
||||
" if (w != 0i64) { bad += 1; };\n"
|
||||
" i += 1;\n"
|
||||
" };\n"
|
||||
" return bad;\n"
|
||||
"};\n",
|
||||
0 },
|
||||
/* 5. Nested loop, inner-body scalar-variant call. Pins that the
|
||||
* fix doesn't depend on loop depth. Each inner iter primes CX. */
|
||||
{ "scalar_24B_in_nested_loop",
|
||||
"type ft = (i64 | str | bool);\n"
|
||||
"fn aski64(v: i64) ft = { return v; };\n"
|
||||
"fn main() i32 = {\n"
|
||||
" let arr: [2]i64 = [10i64, 20i64];\n"
|
||||
" let i: i32 = 0;\n"
|
||||
" let bad: i32 = 0;\n"
|
||||
" for (i < 2) {\n"
|
||||
" let j: i32 = 0;\n"
|
||||
" for (j < 2) {\n"
|
||||
" let v: i64 = arr[j];\n"
|
||||
" let a: ft = aski64(v);\n"
|
||||
" let p: u64 = (&a): u64;\n"
|
||||
" let p2: *i64 = (p + 16u64): *i64;\n"
|
||||
" let w: i64 = *p2;\n"
|
||||
" if (w != 0i64) { bad += 1; };\n"
|
||||
" j += 1;\n"
|
||||
" };\n"
|
||||
" i += 1;\n"
|
||||
" };\n"
|
||||
" return bad;\n"
|
||||
"};\n",
|
||||
0 },
|
||||
/* 6. Mixed loop: alternating str / i64 variants — pins that the
|
||||
* scalar-variant zero doesn't regress the str-variant CX fill
|
||||
* across iterations. iter-0 stores str (slot+16 = 5), iter-1
|
||||
* stores i64 (slot+16 = 0). bad counts slot+16 != expected. */
|
||||
{ "mixed_variants_in_loop",
|
||||
"type ft = (i64 | str | bool);\n"
|
||||
"fn askstr(s: str) ft = { return s; };\n"
|
||||
"fn aski64(v: i64) ft = { return v; };\n"
|
||||
"fn main() i32 = {\n"
|
||||
" let arr: [2]i64 = [10i64, 20i64];\n"
|
||||
" let i: i32 = 0;\n"
|
||||
" let bad: i32 = 0;\n"
|
||||
" for (i < 2) {\n"
|
||||
" let _v: i64 = arr[i];\n"
|
||||
" let a: ft;\n"
|
||||
" let expect: i64 = 0i64;\n"
|
||||
" if (i == 0) { a = askstr(\"world\"); expect = 5i64; };\n"
|
||||
" if (i == 1) { a = aski64(99i64); expect = 0i64; };\n"
|
||||
" let p: u64 = (&a): u64;\n"
|
||||
" let p2: *i64 = (p + 16u64): *i64;\n"
|
||||
" let w: i64 = *p2;\n"
|
||||
" if (w != expect) { bad += 1; };\n"
|
||||
" i += 1;\n"
|
||||
" };\n"
|
||||
" return bad;\n"
|
||||
"};\n",
|
||||
0 },
|
||||
};
|
||||
|
||||
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/wcrv_%d_%d.ww", getpid(), i);
|
||||
snprintf(tmpdir, sizeof tmpdir, "/tmp/wcrv_%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;
|
||||
}
|
||||
|
||||
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 cdrv[640];
|
||||
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
|
||||
char wdrv[640];
|
||||
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, "cgreturn_variant_zero: 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,
|
||||
"cgreturn_variant_zero[%s][%s]: exit=%d want=%d\n",
|
||||
drivers[d].name, rows[i].label,
|
||||
got, rows[i].want);
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fail) {
|
||||
fprintf(stderr,
|
||||
"cgreturn_variant_zero: %d/%d fixtures failed\n", fail, total);
|
||||
return 1;
|
||||
}
|
||||
printf("cgreturn_variant_zero: %d/%d ok\n", total, total);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user