selfhost+test: decompose user-struct by-value params (#11)
wwstage param-slot allocator dispatched isfloat/istagged/isslice/
isstr/catch-all and skipped TY_STRUCT. `fn(a: S, b: S)` where S is
16B emitted $16 frame (DI/SI only); cstage emits $32 (DI/SI/DX/CX)
per SysV ABI.
Two-site fix mirroring cmd/w6c/cgen.c:6820 (callee prologue) and
:4240 (caller push):
- New structparamsize(c, t) helper in cgenutil.ww resolves the
TY_STRUCT TNAME chain, returns totsize for sizes (0,16], else 0.
>16B drops to stack — bug-compat with cstage's <=16 gate.
- New struct arm in cgfnparams + matching cgfn pre-scan in
cgendecl.ww. nw = (size>8) ? 2 : 1; partial-fit stitch (idx=5
+ nw=2) emits one reg + one stack tail.
- New struct branch in pushargsrev N_IDENT arm: MOVQ + PUSHQ
high→low so cgcall's existing pop drains correctly.
Test 717: 4 rows × {cstage, wwstage, asm-id}. Headline 2×16B,
mixed 16B+8B (caller-side surface), str+struct regression guard,
partial-fit 5×i64+16B stitch.
This commit is contained in:
289
test/wcc/717_struct_byval_param.c
Normal file
289
test/wcc/717_struct_byval_param.c
Normal file
@@ -0,0 +1,289 @@
|
||||
/*
|
||||
* 717_struct_byval_param — wwstage param-spill ABI for user-defined
|
||||
* by-value struct parameters of size ≤ 16B. Sister to #9 (which
|
||||
* fixed an OVER-allocation in the match-scrutinee spill path);
|
||||
* this is the UNDER-allocation in the cgfn param-spill path.
|
||||
*
|
||||
* Pre-fix (#11): wwstage cgfnparams dispatched on TK_ELLIPSIS,
|
||||
* float, tagged, slice, str — then a catch-all 8B scalar arm.
|
||||
* User-defined struct params (TY_STRUCT after collectstructs)
|
||||
* had no branch and fell through to the 8B arm. For
|
||||
* `fn cmp(a: inst, b: inst)` with `inst = struct { sec: i64,
|
||||
* nsec: i64 }` cstage allocated `TEXT cmp,$32` and spilled all
|
||||
* four argregs DI/SI/DX/CX; wwstage allocated `TEXT cmp,$16`
|
||||
* and spilled only DI/SI — the second-half value words of each
|
||||
* arg (DX/CX) were never stored, and `b.sec`/`b.nsec` reads
|
||||
* trailed into the saved-BP word. Built-in `str` (also 16B,
|
||||
* ptr+len) routed correctly through the isstrtype arm, so the
|
||||
* bug was a TY_STRUCT type-dispatch miss, not a missing path.
|
||||
*
|
||||
* Fix (#11, wwstage-only per rule 10): add structparamsize in
|
||||
* cgenutil.ww (mirror cstage cgen.c struct_arg_size: returns the
|
||||
* struct's totsize for sizes in (0, 16], else 0). Add a struct
|
||||
* arm in cgfnparams between the str arm and the 8B catch-all,
|
||||
* matching cstage's struct_eb = (size > 8) ? 2 : 1 — including
|
||||
* the partial-fit stitch arm (idx=5, nw=2: one reg, one stack
|
||||
* tail). Mirror the same isstr branch in cgfn's pre-scan so the
|
||||
* frame reservation and emit stay lockstep.
|
||||
*
|
||||
* What this test pins:
|
||||
* - Asm byte-identity between cstage and wwstage for 2×16B
|
||||
* struct params, 1×16B struct + 1×8B struct (mixed
|
||||
* eightbyte counts), 1×16B struct + 1×str (regression guard
|
||||
* for the already-working str path), and 6 i64 + 1 16B
|
||||
* struct (partial-fit stitch case).
|
||||
* - Runtime end-to-end: each row's main returns a deterministic
|
||||
* exit code derived from the received struct's fields. A
|
||||
* regression in the spill ABI (e.g. CX→slot+8 missing) shows
|
||||
* up as a `got != want` exit-code mismatch.
|
||||
*
|
||||
* NOT covered:
|
||||
* - Structs > 16B by-value param: cstage's struct_arg_size
|
||||
* itself gates on size <= 16, so a 24B-struct param also
|
||||
* under-allocates in cstage (falls through to the 8B arm).
|
||||
* Both stages agree on the wrong behaviour, so it's bug-
|
||||
* compatible byte-identical; that's a separate task.
|
||||
* - Single-field i32-or-smaller struct: still 8B totsize after
|
||||
* wwstage's registerstruct rounds up, equivalent to a scalar
|
||||
* i64 in the spill path. The 1-field i64 row exercises the
|
||||
* nw=1 branch.
|
||||
*/
|
||||
#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. Canonical 2×16B struct shape (the original probe). The
|
||||
* comparator reads b.sec — that's the second-arg slot+0, which
|
||||
* pre-fix sat unwritten and aliased the caller's saved BP. */
|
||||
{ "two_16B_struct_cmp",
|
||||
"type inst = struct { sec: i64, nsec: i64 };\n"
|
||||
"fn cmp(a: inst, b: inst) i32 = {\n"
|
||||
" if (a.sec < b.sec) { return -1; };\n"
|
||||
" if (a.sec > b.sec) { return 1; };\n"
|
||||
" if (a.nsec < b.nsec) { return -1; };\n"
|
||||
" if (a.nsec > b.nsec) { return 1; };\n"
|
||||
" return 0;\n"
|
||||
"};\n"
|
||||
"fn main() i32 = {\n"
|
||||
" let x: inst = inst { sec = 5i64, nsec = 200i64 };\n"
|
||||
" let y: inst = inst { sec = 5i64, nsec = 100i64 };\n"
|
||||
" let r: i32 = cmp(x, y);\n"
|
||||
" if (r > 0) { return 7; };\n"
|
||||
" return 0;\n"
|
||||
"};\n",
|
||||
7 },
|
||||
/* 2. 16B struct + 8B struct (mixed eightbyte counts). a uses
|
||||
* DI/SI (2 eb), b uses DX (1 eb). Returns a.sec + a.nsec + b.v
|
||||
* = 10 + 20 + 30 = 60. */
|
||||
{ "mixed_16B_8B_struct",
|
||||
"type two = struct { sec: i64, nsec: i64 };\n"
|
||||
"type one = struct { v: i64 };\n"
|
||||
"fn sum(a: two, b: one) i32 = {\n"
|
||||
" return (a.sec + a.nsec + b.v): i32;\n"
|
||||
"};\n"
|
||||
"fn main() i32 = {\n"
|
||||
" let x: two = two { sec = 10i64, nsec = 20i64 };\n"
|
||||
" let y: one = one { v = 30i64 };\n"
|
||||
" return sum(x, y);\n"
|
||||
"};\n",
|
||||
60 },
|
||||
/* 3. 16B struct + 16B str (regression guard). The pre-fix
|
||||
* wwstage already handled str via the isstrtype arm; this row
|
||||
* pins that the new struct arm doesn't shadow it. struct in
|
||||
* DI/SI, str in DX/CX. Returns a.sec + s.len = 42 + 3 = 45. */
|
||||
{ "struct_plus_str",
|
||||
"type two = struct { sec: i64, nsec: i64 };\n"
|
||||
"fn pick(a: two, s: str) i32 = {\n"
|
||||
" return (a.sec + (s.len: i64)): i32;\n"
|
||||
"};\n"
|
||||
"fn main() i32 = {\n"
|
||||
" let x: two = two { sec = 42i64, nsec = 0i64 };\n"
|
||||
" return pick(x, \"abc\");\n"
|
||||
"};\n",
|
||||
45 },
|
||||
/* 4. Partial-fit stitch: 5 i64 args consume DI/SI/DX/CX/R8;
|
||||
* the 16B struct hits idx=5 with regs_left=1, nw=2 — one
|
||||
* half (struct word 0) lands in R9, the second half spills
|
||||
* onto the caller's stack at +16(BP). Wwstage's struct arm
|
||||
* stitches both into a contiguous local slot. Returns
|
||||
* sum(s0..s4) + x.a + x.b = 1+2+3+4+5+11+22 = 48. */
|
||||
{ "partial_fit_5i64_plus_16B",
|
||||
"type pair = struct { a: i64, b: i64 };\n"
|
||||
"fn manyfn(s0: i64, s1: i64, s2: i64, s3: i64, s4: i64,\n"
|
||||
" x: pair) i32 = {\n"
|
||||
" return (s0 + s1 + s2 + s3 + s4 + x.a + x.b): i32;\n"
|
||||
"};\n"
|
||||
"fn main() i32 = {\n"
|
||||
" let p: pair = pair { a = 11i64, b = 22i64 };\n"
|
||||
" return manyfn(1i64, 2i64, 3i64, 4i64, 5i64, p);\n"
|
||||
"};\n",
|
||||
48 },
|
||||
};
|
||||
|
||||
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/wcsbp_%d_%d.ww", getpid(), i);
|
||||
snprintf(tmpdir, sizeof tmpdir, "/tmp/wcsbp_%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_byte_identical — pin the spill prologue + frame size by
|
||||
* diffing the w6c vs w6c_ww text output. The whole point of #11
|
||||
* is that wwstage's frame stops under-bloating for user-defined
|
||||
* struct params, so the bytes must match. */
|
||||
static int
|
||||
asm_byte_identical(const char *bin, const struct row *r, int i)
|
||||
{
|
||||
char src[64], cs[64], ws[64], cmd[1024];
|
||||
snprintf(src, sizeof src, "/tmp/wcsbp_asm_%d_%d.ww", getpid(), i);
|
||||
snprintf(cs, sizeof cs, "/tmp/wcsbp_asm_%d_%d_c.s", getpid(), i);
|
||||
snprintf(ws, sizeof ws, "/tmp/wcsbp_asm_%d_%d_w.s", getpid(), i);
|
||||
|
||||
FILE *f = fopen(src, "wb");
|
||||
if (!f) return -1;
|
||||
fputs(r->src, f);
|
||||
fclose(f);
|
||||
|
||||
snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, src);
|
||||
if (runwait(cmd) != 0) {
|
||||
fprintf(stderr, "row[%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, "row[%s]: w6c_ww errored\n", r->label);
|
||||
unlink(src); unlink(cs);
|
||||
return -1;
|
||||
}
|
||||
|
||||
FILE *fc = fopen(cs, "rb");
|
||||
FILE *fw = fopen(ws, "rb");
|
||||
int rc = 0;
|
||||
if (!fc || !fw) {
|
||||
rc = -1;
|
||||
} else {
|
||||
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);
|
||||
if (rc != 0)
|
||||
fprintf(stderr, "row[%s]: cstage vs wwstage asm differs\n",
|
||||
r->label);
|
||||
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, "struct_byval_param: 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,
|
||||
"struct_byval_param[%s][%s]: exit=%d want=%d\n",
|
||||
drivers[d].name, rows[i].label,
|
||||
got, rows[i].want);
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (access(wdrv, X_OK) == 0) {
|
||||
for (int i = 0; i < n; i++) {
|
||||
total++;
|
||||
if (asm_byte_identical(bin, &rows[i], i) != 0)
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
|
||||
if (fail) {
|
||||
fprintf(stderr,
|
||||
"struct_byval_param: %d/%d fixtures failed\n",
|
||||
fail, total);
|
||||
return 1;
|
||||
}
|
||||
printf("struct_byval_param: %d/%d ok\n", total, total);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user