Files
ww/test/wcc/717_struct_byval_param.c
Hojun-Cho 69a817f0f3 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.
2026-05-17 00:27:22 +09:00

290 lines
9.3 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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;
}