Files
ww/test/wcc/717_struct_byval_param.c
Hojun-Cho 7b9488706b parse: enforce strict-package — reject package-less files (#24a)
Flip the soft-default to a hard "missing package clause" error symmetrically in
both stages (cmd/wcc/parse.c + lib/ww/syntax/parse.ww): the first real decl of a
primary section with empty pathmod/resetmod and no seen clause is now rejected.
Closes the documented soft-default divergence (the 63-wrapper carve-out).

The gate flip can't be split from the migration it breaks, so this is one atomic
commit: ~80 test/wcc wrappers gain `package main;` via a shared wwtestpkg.h
helper, 6 data fixtures plus 17 asm-grep assertions update for the bare->main.<leaf>
root-helper mangle shift, and rt/ declares `package rt;` with @symbol pinning the
bare rt_ensure/rt_malloc linker names.

Root mangling narrows: the executable entry `main` stays bare (existing
carve-out), but root helper symbols become main.X. The #84 cluster is rewritten
to assert main.run distinct from aa.run/test.run; its cgen fix and bare machinery
are retained — still load-bearing for package-less module-reset deps. New
table-driven test 782_strict_package.c (6 rows, both stages).

Retiring //ww:module-reset is deferred to #24b: it is load-bearing (clears the
.wwi pathmod so the body's package clause asserts), not a vestige; fusing its
removal here would be a silent mismatch.

All byte-id gates green; full make test reports "all 335 tests passed".
2026-06-29 03:55:26 +09:00

285 lines
9.2 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 <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include "wwtestpkg.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 tmpdir[64], src[128], outbin[128], rmcmd[160], cmd[1024];
snprintf(tmpdir, sizeof tmpdir, "/tmp/wcsbp_%d_d_%d", getpid(), i);
mkdir(tmpdir, 0755);
snprintf(src, sizeof src, "%s/wcsbp_%d_%d.ww", tmpdir, getpid(), i);
snprintf(outbin, sizeof outbin, "%s/wcsbp_%d_%d", tmpdir, getpid(), i);
snprintf(rmcmd, sizeof rmcmd, "rm -rf %s", tmpdir);
FILE *f = fopen(src, "wb");
if (!f) { runwait(rmcmd); return -1; }
wwtest_fputs(r->src, f);
fclose(f);
snprintf(cmd, sizeof cmd, "%s build -o %s %s", driver, outbin, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: build via %s failed\n",
r->label, driver);
runwait(rmcmd);
return -1;
}
int got = runwait(outbin);
runwait(rmcmd);
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;
wwtest_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;
}