Files
ww/test/wcc/930_sret_narrow_field_run.c
Hojun-Cho 0d96196f90 cstage+test: walk fields for sret callee struct-copy width (#33)
Callee N_IDENT word-copy loop was driven off slot-padded rt->size;
trailing narrow field (e.g. bool@32 in 33B/40B struct) widened to
MOVQ at the loop tail, diverging from wwstage's natural-size MOVB.
Class A cgen divergence. 9th unmask of session 5, corpus-coverage-
blind on the cstage side — no in-tree lib struct had a narrow
(bool/u8/i8/i16) trailing field until lib/strings.iterator landed
`reverse: bool` per Hare's ref/hare/strings/iter.ha:8.

Pre-fix: cstage cgreturn's sret arm (cgen.c) used `int sz =
(int)rt->size` for its chained `while (k+8<=sz)` MOVQ-MOVL-MOVW-MOVB
copy loop. rt->size is slot-padded (8-rounded for downstream
frame alloc), e.g. 40B for {i32, []u8, bool}. Loop emitted MOVQ
at offset 32 covering the 1-byte bool tail plus 7 bytes of
padding into the caller's sret slot — clobbering the next 7 bytes
of caller frame on read-side. Wwstage's mirror loop drives off
sretretsize → structnaturalsize → max(foff+fsz) = 33B, so it
correctly stops at offset 32 and emits MOVB.

Polarity catalog: cstage OVER-WIDE — slot-padded size driving the
field-copy width. Convergence cstage → wwstage's structnaturalsize
discipline (rule 10 inverse: leaner-correct side wins).

Fix: compute natural size locally in the N_IDENT/N_STRUCTLIT sret
arm via walk over `rt->fields` (max foff+fsz), mirroring wwstage's
`structnaturalsize` (cgenutil.ww:1377). Frame allocation and
`cg_sret_retsize` (used for the caller-side @sretscr slot)
intentionally keep using rt->size — caller scratch sizing is
separate from callee per-field copy width.

Surfaced by lib/strings commit-2 (#30) iterator pre-flight when
the Hare-faithful `reverse: bool` field tripped the cstage-only
mis-width on 993/995 byte-id (strconv → strings → wwdump_ww +
w6c_ww). lib/strings c2 was stashed to land cleanly after this fix.

Note (out-of-scope): the chained MOV loop has no MOVW arm in
either stage, so a trailing i16 emits 2× MOVB at consecutive
offsets. Worth a future cleanup; both stages agree today.

Tests:
  - 730_sret_narrow_field pins narrow-MOV store + load width and
    no-MOVQ@trailing-offset assertions for bool / u8 / i16 / i32
    trailing fields in a 33B-natural struct. 4 rows × 5 sentinels
    = 20 fixtures. cmp -s cstage vs wwstage byte-id per row.
  - 930_sret_narrow_field_run runtime-pins bool true/false, u8
    high-bit, i16 negative, i32 negative, mixed (bool+i32+i64 after
    slice) — 6 scenarios × 2 stages = 12 rows.

104/104 ok. 995_self_rebuild stays green (ww2==ww3==ww4 byte-id).
2026-05-18 12:01:34 +09:00

231 lines
6.7 KiB
C

/*
* 930_sret_narrow_field_run — Class B semantic round-trip for the
* sret callee's narrow trailing-field copy (task #33). End-to-end
* pins that values cross the >24B struct-return boundary intact
* when the struct ends in a narrow primitive (bool / u8 / i16 /
* i32), including a mixed-width struct that interleaves several
* narrow fields after the 24B slice payload.
*
* Pre-fix cstage's N_IDENT word-copy tail used slot-padded
* `rt->size` and emitted MOVQ for the narrow trailing field —
* the narrow source slot was BP-relative low and bordered the
* @sretarg save (8B), so the 8-byte read at offset 32 swept in
* the saved RDI byte and either wrote garbage past the bool /
* misaligned the next stack slot at the callee's frame edge.
*
* Class A asm-presence is pinned at 730; this file catches
* shared miscompiles that asm byte-id can't see.
*/
#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[] = {
/* Trailing bool — the original surfacing case (lib/strings
* iterator shape). Pre-fix the bool round-tripped to a
* neighbour byte (the @sretarg low byte at -8(BP)), which on
* the local stack frame was 0x01 (the saved RDI pointer low
* byte). Test sets r=true and confirms the value survives. */
{ "bool_true",
"type t = struct { a: i32, s: []u8, r: bool };\n"
"fn mk() t = {\n"
" let v: t; let z: []u8;\n"
" v.s = z; v.a = 7; v.r = true;\n"
" return v;\n"
"};\n"
"export fn main() i32 = {\n"
" let x: t = mk();\n"
" if (x.a != 7) { return 1; };\n"
" if (!x.r) { return 2; };\n"
" return 0;\n"
"};\n",
0 },
{ "bool_false",
"type t = struct { a: i32, s: []u8, r: bool };\n"
"fn mk() t = {\n"
" let v: t; let z: []u8;\n"
" v.s = z; v.a = 9; v.r = false;\n"
" return v;\n"
"};\n"
"export fn main() i32 = {\n"
" let x: t = mk();\n"
" if (x.a != 9) { return 1; };\n"
" if (x.r) { return 2; };\n"
" return 0;\n"
"};\n",
0 },
/* Trailing u8 with a high bit set. Pre-fix MOVQ-load at the
* source slot's offset would clobber the high 7 bytes of the
* read into AX with whatever lived above the byte slot. */
{ "u8_high_bit",
"type t = struct { a: i32, s: []u8, r: u8 };\n"
"fn mk() t = {\n"
" let v: t; let z: []u8;\n"
" v.s = z; v.a = 0; v.r = 0xFFu8;\n"
" return v;\n"
"};\n"
"export fn main() i32 = {\n"
" let x: t = mk();\n"
" if (x.r != 0xFFu8) { return 1; };\n"
" return 0;\n"
"};\n",
0 },
/* Trailing i16 with a negative bit pattern: the MOVW path
* is exercised, and the receive site's sign extension must
* read only 2 bytes. */
{ "i16_negative",
"type t = struct { a: i32, s: []u8, r: i16 };\n"
"fn mk() t = {\n"
" let v: t; let z: []u8;\n"
" v.s = z; v.a = 0; v.r = -123i16;\n"
" return v;\n"
"};\n"
"export fn main() i32 = {\n"
" let x: t = mk();\n"
" if (x.r != -123i16) { return 1; };\n"
" return 0;\n"
"};\n",
0 },
/* Trailing i32 negative: exercises the MOVL-tail path of the
* fixed loop (k+4<=sz arm; pre-fix the k+8<=40 arm absorbed
* this offset too). */
{ "i32_negative",
"type t = struct { a: i32, s: []u8, r: i32 };\n"
"fn mk() t = {\n"
" let v: t; let z: []u8;\n"
" v.s = z; v.a = 0; v.r = -424242;\n"
" return v;\n"
"};\n"
"export fn main() i32 = {\n"
" let x: t = mk();\n"
" if (x.r != -424242) { return 1; };\n"
" return 0;\n"
"};\n",
0 },
/* Mixed-width narrows interleaved after the 24B slice: bool +
* i32 + i64 mix into one struct. All three trailing fields
* survive intact only if each emits its declared width per
* field's natural offset/size. */
{ "mixed_narrow_after_slice",
"type t = struct { s: []u8, r: bool, n: i32, k: i64 };\n"
"fn mk() t = {\n"
" let v: t; let z: []u8;\n"
" v.s = z; v.r = true; v.n = 1234567; v.k = 9876543210i64;\n"
" return v;\n"
"};\n"
"export fn main() i32 = {\n"
" let x: t = mk();\n"
" if (!x.r) { return 1; };\n"
" if (x.n != 1234567) { return 2; };\n"
" if (x.k != 9876543210i64) { return 3; };\n"
" return 0;\n"
"};\n",
0 },
};
static int
run_driver(const char *driver, const struct row *r, int i)
{
char src[96], tmpdir[96], cmd[1024];
snprintf(src, sizeof src, "/tmp/sret_nr_%d_%d.ww", getpid(), i);
snprintf(tmpdir, sizeof tmpdir, "/tmp/sret_nr_%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[160];
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,
"sret_narrow_field_run: 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,
"sret_narrow_field_run[%s][%s]: exit=%d want=%d\n",
drivers[d].name, rows[i].label,
got, rows[i].want);
fail++;
}
}
}
if (fail) {
fprintf(stderr,
"sret_narrow_field_run: %d/%d fixtures failed\n",
fail, total);
return 1;
}
printf("sret_narrow_field_run: %d/%d ok\n", total, total);
return 0;
}