From 5c8724845ab808984d9dd2aeb0a8c3c9d0711aba Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Wed, 13 May 2026 22:40:43 +0900 Subject: [PATCH] selfhost: cgfn pre-scan SysV-class accounting (closes #7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors cgen.c §5130-5223 / cgfnparams: reg-spill, tagged-partial-fit, pure-stack. Pure-stack does not bump cursor (cstage semantics). Fixes 16B over-allocation on 7+ arg functions; bootstrap stays byte-identical. Slice/str at reg/stack straddle is deferred to task #11 (cgfnparams doesn't stitch them either); pre-scan stays symmetric until then. --- Makefile | 8 +- selfhost/cmd/w6c/main.combined.ww | 67 ++++++-- selfhost/cmd/wcc/cgendecl.ww | 67 ++++++-- selfhost/cmd/wwdump/main.combined.ww | 67 ++++++-- test/wcc/670_frame_argcount.c | 236 +++++++++++++++++++++++++++ 5 files changed, 408 insertions(+), 37 deletions(-) create mode 100644 test/wcc/670_frame_argcount.c diff --git a/Makefile b/Makefile index e67d7668..8f062ab1 100644 --- a/Makefile +++ b/Makefile @@ -217,7 +217,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_e2e $(BIN)/test_ffi $(BIN)/test_dyn $(BIN)/test_stdlib \ $(BIN)/test_at_test $(BIN)/test_let_global \ $(BIN)/test_int_cast_signed $(BIN)/test_dot_chain \ - $(BIN)/test_field_signed \ + $(BIN)/test_field_signed $(BIN)/test_frame_argcount \ $(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \ $(BIN)/test_w6c_ww $(BIN)/test_ww_ww $(BIN)/test_self_rebuild \ $(BIN)/test_dyn_ww $(BIN)/test_selfcheck $(BIN)/test_at_test_ww @@ -296,6 +296,12 @@ $(BIN)/test_field_signed: test/wcc/660_field_signed.c $(BIN)/ww \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_frame_argcount: test/wcc/670_frame_argcount.c $(BIN)/ww \ + $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_selfhost: test/wcc/990_selfhost.c $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww $(BIN)/wwdump $(BIN)/wwdump_ww $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index ebf254b7..b009c840 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -13680,22 +13680,65 @@ fn cgfn(c: *cgen, fn_: *node) void = { os.write(1, nm.ptr, nm.len: u64); emitline(",$"); - // Pre-scan total frame: 24 bytes per slice param, 16 per str - // param, 8 per other param, plus per-let from scanlocals. - // Seed c.locals with param-name stubs so scanlocals dedups a - // re-declared `let ` in the body against the param's - // slot (matches C cgen). Stubs get cleared before emission. + // Pre-scan total frame: only count params that land in a local + // slot. SysV-class accounting; mirrors runtime walk in cstage + // cgen.c §5130-5223 and cgfnparams below. A stack-spilled param + // is addressed at a positive BP offset by cgfnparams (via + // localaddstack) and consumes no frame, so adding its size here + // would over-allocate. Seed c.locals with param-name stubs so + // scanlocals dedups a re-declared `let ` in the body + // against the param's slot (matches C cgen). Stubs get cleared + // before emission. let scanp: *node = fn_.list; let frame: i32 = 0; + let argi: i32 = 0; + let fargi: i32 = 0; for (scanp != nil) { if (scanp.kind == nkind.N_PARAM) { - // Hare-style variadic `T...`: param is []T inside - // the callee, so it occupies a 24B slice slot. - if (scanp.op == tkind.TK_ELLIPSIS) { frame += 24; } - else { if (istaggedtype(c, scanp.lhs)) { frame += slotsize(c, scanp.lhs); } - else { if (isslicetype(c, scanp.lhs)) { frame += 24; } - else { if (isstrtype(c, scanp.lhs)) { frame += 16; } - else { frame += 8; }; }; }; }; + let isvar: bool = scanp.op == tkind.TK_ELLIPSIS; + let isf: bool = false; + let istg: bool = false; + let issl: bool = false; + let isst: bool = false; + if (!isvar) { + isf = isfloattype(c, scanp.lhs); + istg = istaggedtype(c, scanp.lhs); + if (!isf && !istg) { + issl = isslicetype(c, scanp.lhs); + if (!issl) { isst = isstrtype(c, scanp.lhs); }; + }; + }; + let eb: i32 = 1; + let sz: i32 = 8; + if (isvar) { eb = 3; sz = 24; } + else { if (istg) { sz = slotsize(c, scanp.lhs); eb = sz / 8; } + else { if (issl) { eb = 3; sz = 24; } + else { if (isst) { eb = 2; sz = 16; } + else { if (isf) { + eb = 1; + sz = 8; + if (isf32type(c, scanp.lhs)) { sz = 4; }; + }; }; }; }; }; + let regs_left: i32 = 6 - argi; + if (isf) { regs_left = 8 - fargi; }; + if (regs_left >= eb) { + frame += sz; + if (isf) { fargi += 1; } + else { argi += eb; }; + } else { if (eb > 1 && regs_left > 0 && istg) { + // Tagged param straddles the reg/stack boundary; + // cgfnparams stitches the tail from positive BP + // offsets into a single local slot, so we still + // reserve the full size. Slice/str at the same + // straddle are *not* stitched by cgfnparams today + // (task #11) — once that's fixed the predicate + // here must widen symmetrically. + frame += sz; + argi = 6; + } else { + // Pure stack: lives at +BP(16+stkcursor*8); no + // local slot consumed. The reg cursor stays put. + }; }; scanseenmark(c, scanp.str); }; scanp = scanp.next; diff --git a/selfhost/cmd/wcc/cgendecl.ww b/selfhost/cmd/wcc/cgendecl.ww index 878ecb82..b82461a6 100644 --- a/selfhost/cmd/wcc/cgendecl.ww +++ b/selfhost/cmd/wcc/cgendecl.ww @@ -562,22 +562,65 @@ fn cgfn(c: *cgen, fn_: *node) void = { os.write(1, nm.ptr, nm.len: u64); emitline(",$"); - // Pre-scan total frame: 24 bytes per slice param, 16 per str - // param, 8 per other param, plus per-let from scanlocals. - // Seed c.locals with param-name stubs so scanlocals dedups a - // re-declared `let ` in the body against the param's - // slot (matches C cgen). Stubs get cleared before emission. + // Pre-scan total frame: only count params that land in a local + // slot. SysV-class accounting; mirrors runtime walk in cstage + // cgen.c §5130-5223 and cgfnparams below. A stack-spilled param + // is addressed at a positive BP offset by cgfnparams (via + // localaddstack) and consumes no frame, so adding its size here + // would over-allocate. Seed c.locals with param-name stubs so + // scanlocals dedups a re-declared `let ` in the body + // against the param's slot (matches C cgen). Stubs get cleared + // before emission. let scanp: *node = fn_.list; let frame: i32 = 0; + let argi: i32 = 0; + let fargi: i32 = 0; for (scanp != nil) { if (scanp.kind == nkind.N_PARAM) { - // Hare-style variadic `T...`: param is []T inside - // the callee, so it occupies a 24B slice slot. - if (scanp.op == tkind.TK_ELLIPSIS) { frame += 24; } - else { if (istaggedtype(c, scanp.lhs)) { frame += slotsize(c, scanp.lhs); } - else { if (isslicetype(c, scanp.lhs)) { frame += 24; } - else { if (isstrtype(c, scanp.lhs)) { frame += 16; } - else { frame += 8; }; }; }; }; + let isvar: bool = scanp.op == tkind.TK_ELLIPSIS; + let isf: bool = false; + let istg: bool = false; + let issl: bool = false; + let isst: bool = false; + if (!isvar) { + isf = isfloattype(c, scanp.lhs); + istg = istaggedtype(c, scanp.lhs); + if (!isf && !istg) { + issl = isslicetype(c, scanp.lhs); + if (!issl) { isst = isstrtype(c, scanp.lhs); }; + }; + }; + let eb: i32 = 1; + let sz: i32 = 8; + if (isvar) { eb = 3; sz = 24; } + else { if (istg) { sz = slotsize(c, scanp.lhs); eb = sz / 8; } + else { if (issl) { eb = 3; sz = 24; } + else { if (isst) { eb = 2; sz = 16; } + else { if (isf) { + eb = 1; + sz = 8; + if (isf32type(c, scanp.lhs)) { sz = 4; }; + }; }; }; }; }; + let regs_left: i32 = 6 - argi; + if (isf) { regs_left = 8 - fargi; }; + if (regs_left >= eb) { + frame += sz; + if (isf) { fargi += 1; } + else { argi += eb; }; + } else { if (eb > 1 && regs_left > 0 && istg) { + // Tagged param straddles the reg/stack boundary; + // cgfnparams stitches the tail from positive BP + // offsets into a single local slot, so we still + // reserve the full size. Slice/str at the same + // straddle are *not* stitched by cgfnparams today + // (task #11) — once that's fixed the predicate + // here must widen symmetrically. + frame += sz; + argi = 6; + } else { + // Pure stack: lives at +BP(16+stkcursor*8); no + // local slot consumed. The reg cursor stays put. + }; }; scanseenmark(c, scanp.str); }; scanp = scanp.next; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 41fe98cd..7bb6fac8 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -13680,22 +13680,65 @@ fn cgfn(c: *cgen, fn_: *node) void = { os.write(1, nm.ptr, nm.len: u64); emitline(",$"); - // Pre-scan total frame: 24 bytes per slice param, 16 per str - // param, 8 per other param, plus per-let from scanlocals. - // Seed c.locals with param-name stubs so scanlocals dedups a - // re-declared `let ` in the body against the param's - // slot (matches C cgen). Stubs get cleared before emission. + // Pre-scan total frame: only count params that land in a local + // slot. SysV-class accounting; mirrors runtime walk in cstage + // cgen.c §5130-5223 and cgfnparams below. A stack-spilled param + // is addressed at a positive BP offset by cgfnparams (via + // localaddstack) and consumes no frame, so adding its size here + // would over-allocate. Seed c.locals with param-name stubs so + // scanlocals dedups a re-declared `let ` in the body + // against the param's slot (matches C cgen). Stubs get cleared + // before emission. let scanp: *node = fn_.list; let frame: i32 = 0; + let argi: i32 = 0; + let fargi: i32 = 0; for (scanp != nil) { if (scanp.kind == nkind.N_PARAM) { - // Hare-style variadic `T...`: param is []T inside - // the callee, so it occupies a 24B slice slot. - if (scanp.op == tkind.TK_ELLIPSIS) { frame += 24; } - else { if (istaggedtype(c, scanp.lhs)) { frame += slotsize(c, scanp.lhs); } - else { if (isslicetype(c, scanp.lhs)) { frame += 24; } - else { if (isstrtype(c, scanp.lhs)) { frame += 16; } - else { frame += 8; }; }; }; }; + let isvar: bool = scanp.op == tkind.TK_ELLIPSIS; + let isf: bool = false; + let istg: bool = false; + let issl: bool = false; + let isst: bool = false; + if (!isvar) { + isf = isfloattype(c, scanp.lhs); + istg = istaggedtype(c, scanp.lhs); + if (!isf && !istg) { + issl = isslicetype(c, scanp.lhs); + if (!issl) { isst = isstrtype(c, scanp.lhs); }; + }; + }; + let eb: i32 = 1; + let sz: i32 = 8; + if (isvar) { eb = 3; sz = 24; } + else { if (istg) { sz = slotsize(c, scanp.lhs); eb = sz / 8; } + else { if (issl) { eb = 3; sz = 24; } + else { if (isst) { eb = 2; sz = 16; } + else { if (isf) { + eb = 1; + sz = 8; + if (isf32type(c, scanp.lhs)) { sz = 4; }; + }; }; }; }; }; + let regs_left: i32 = 6 - argi; + if (isf) { regs_left = 8 - fargi; }; + if (regs_left >= eb) { + frame += sz; + if (isf) { fargi += 1; } + else { argi += eb; }; + } else { if (eb > 1 && regs_left > 0 && istg) { + // Tagged param straddles the reg/stack boundary; + // cgfnparams stitches the tail from positive BP + // offsets into a single local slot, so we still + // reserve the full size. Slice/str at the same + // straddle are *not* stitched by cgfnparams today + // (task #11) — once that's fixed the predicate + // here must widen symmetrically. + frame += sz; + argi = 6; + } else { + // Pure stack: lives at +BP(16+stkcursor*8); no + // local slot consumed. The reg cursor stays put. + }; }; scanseenmark(c, scanp.str); }; scanp = scanp.next; diff --git a/test/wcc/670_frame_argcount.c b/test/wcc/670_frame_argcount.c new file mode 100644 index 00000000..11f5b802 --- /dev/null +++ b/test/wcc/670_frame_argcount.c @@ -0,0 +1,236 @@ +/* + * 670_frame_argcount — function-frame size for functions with more + * than six register-class args. wwstage's cgfn pre-scan used to add + * a full slot per param regardless of class, then round up — so a + * 7-arg i64 fn over-allocated by 16B (cstage $48, wwstage $64) and + * broke the bootstrap byte-identity gate (tests 993/994/995). Fix: + * pre-scan now mirrors cgfnparams' SysV class accounting (and cstage + * cgen.c §5130-5223), so a stack-spilled param adds 0 to the frame + * and never bumps the reg cursor. + * + * Each row asserts value correctness. Byte-identity is asserted at + * the bootstrap level by 993/994/995, so this file pins the runtime + * contract: a function that mixes reg and stack args must compute + * the same answer in both stages. + */ +#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[] = { + /* 7-arg i64: 6 regs + 1 stack. Pins the frame size that used to + * over-allocate by 16B in wwstage. Returns the sum (1..7 = 28). */ + { "i64_7args", + "fn s7(a: i64, b: i64, c: i64, d: i64, e: i64, f: i64, g: i64) i64 = {\n" + " return a + b + c + d + e + f + g;\n" + "};\n" + "fn main() i32 = {\n" + " let r: i64 = s7(1i64, 2i64, 3i64, 4i64, 5i64, 6i64, 7i64);\n" + " if (r == 28i64) { return 42; };\n" + " return 0;\n" + "};\n", + 42 }, + /* 8-arg i64: 6 regs + 2 stack. Stack-cursor advances twice. */ + { "i64_8args", + "fn s8(a: i64, b: i64, c: i64, d: i64, e: i64,\n" + " f: i64, g: i64, h: i64) i64 = {\n" + " return a + b + c + d + e + f + g + h;\n" + "};\n" + "fn main() i32 = {\n" + " let r: i64 = s8(1i64, 2i64, 3i64, 4i64,\n" + " 5i64, 6i64, 7i64, 8i64);\n" + " if (r == 36i64) { return 42; };\n" + " return 0;\n" + "};\n", + 42 }, + /* Mixed-signed sum: an i64 thread through 7 args. */ + { "i64_7args_signed", + "fn s7(a: i64, b: i64, c: i64, d: i64, e: i64, f: i64, g: i64) i64 = {\n" + " return a + b + c + d + e + f + g;\n" + "};\n" + "fn main() i32 = {\n" + " let r: i64 = s7(-1i64, -2i64, -3i64, -4i64, -5i64, -6i64, 49i64);\n" + " if (r == 28i64) { return 42; };\n" + " return 0;\n" + "};\n", + 42 }, + /* 9-arg f64: 8 XMM regs + 1 stack. Exercises fargi past 8. */ + { "f64_9args", + "fn s9f(a: f64, b: f64, c: f64, d: f64, e: f64,\n" + " f: f64, g: f64, h: f64, i: f64) f64 = {\n" + " return a + b + c + d + e + f + g + h + i;\n" + "};\n" + "fn main() i32 = {\n" + " let r: f64 = s9f(1.0, 2.0, 3.0, 4.0, 5.0,\n" + " 6.0, 7.0, 8.0, 9.0);\n" + " if ((r: i64) == 45i64) { return 42; };\n" + " return 0;\n" + "};\n", + 42 }, + /* Mixed: 5 ints fill ints 1..5, 3 floats fill XMM 1..3, then 4 + * more ints — int #6 goes to reg, ints #7..9 spill to stack. The + * int and float classes have independent cursors, so XMM stays + * at 3 even after the trailing 4 ints. */ + { "mixed_5i_3f_4i", + "fn mx(a: i64, b: i64, c: i64, d: i64, e: i64,\n" + " f1: f64, f2: f64, f3: f64,\n" + " g: i64, h: i64, i: i64, j: i64) i64 = {\n" + " return a + b + c + d + e\n" + " + (f1: i64) + (f2: i64) + (f3: i64)\n" + " + g + h + i + j;\n" + "};\n" + "fn main() i32 = {\n" + " let r: i64 = mx(1i64, 2i64, 3i64, 4i64, 5i64,\n" + " 6.0, 7.0, 8.0,\n" + " 9i64, 10i64, 11i64, 12i64);\n" + " if (r == 78i64) { return 42; };\n" + " return 0;\n" + "};\n", + 42 }, + /* Slice past the register window: 6 ints saturate argi, the + * slice (3 eightbytes) goes wholly to the stack at positive BP + * offsets via localaddstack. Verifies every element of the + * slice round-trips through a stack-passed slice arg. + * + * NOTE: a slice at the *straddle* boundary (arg #6 after 5 + * ints, so 1 reg + 2 stack words) currently mismatches between + * pushargsrev (greedy reg fill) and cgfnparams (no stitch for + * slice/str — only tagged). That's a separate pre-existing + * cgfnparams bug, tracked as task #11 (wwstage cgfnparams: + * slice/str at reg/stack straddle don't stitch). Once #11 is + * fixed, the cgfn pre-scan must be updated symmetrically and + * this fixture extended to cover the straddle case. + */ + { "slice_purestack_arg7", + "fn ss(a: i64, b: i64, c: i64, d: i64, e: i64, f: i64, xs: []i64) i64 = {\n" + " let i: i32 = 0;\n" + " let acc: i64 = a + b + c + d + e + f;\n" + " for (i < xs.len: i32) { acc = acc + xs[i]; i = i + 1; };\n" + " return acc;\n" + "};\n" + "fn main() i32 = {\n" + " let buf: [3]i64;\n" + " buf[0] = 100i64; buf[1] = 200i64; buf[2] = 300i64;\n" + " let s: []i64 = buf[0:3];\n" + " let r: i64 = ss(1i64, 2i64, 3i64, 4i64, 5i64, 6i64, s);\n" + " if (r == 621i64) { return 42; };\n" + " return 0;\n" + "};\n", + 42 }, + /* str past the register window: 6 ints saturate argi, the str + * (2 eightbytes) goes wholly to the stack via localaddstack. + * Both .ptr and .len must round-trip. */ + { "str_purestack_arg7", + "fn ts(a: i64, b: i64, c: i64, d: i64, e: i64, f: i64, s: str) i64 = {\n" + " return a + b + c + d + e + f + s.len: i64;\n" + "};\n" + "fn main() i32 = {\n" + " let r: i64 = ts(1i64, 2i64, 3i64, 4i64, 5i64, 6i64, \"hello\");\n" + " if (r == 26i64) { return 42; };\n" + " return 0;\n" + "};\n", + 42 }, +}; + +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/wwfa_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/wwfa_%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[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, "frame_argcount: 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, + "frame_argcount[%s][%s]: exit=%d want=%d\n", + drivers[d].name, rows[i].label, + got, rows[i].want); + fail++; + } + } + } + if (fail) { + fprintf(stderr, + "frame_argcount: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("frame_argcount: %d/%d ok\n", total, total); + return 0; +}