Files
ww/test/wcc/730_sret_narrow_field.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

277 lines
8.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.
/*
* 730_sret_narrow_field — Class A asm-presence + byte-id for the
* sret callee's N_IDENT word-copy tail when the returned struct
* ends in a narrow primitive (bool / u8 / i8 / i16 / i32).
*
* Task #33. Cstage pre-fix used the slot-padded `rt->size` to drive
* the field-copy loop's MOVQ/MOVL/MOVB tail; for a struct whose
* natural size is unaligned (e.g. `{ i32, []u8, bool }` natural=33,
* padded=40), the trailing MOVQ at offset 32 widened a 1-byte bool
* into an 8-byte load+store — diverged from wwstage's correct MOVB
* (which used natural size via `sretretsize` / `structnaturalsize`).
* Corpus-coverage-blind: no in-tree stdlib struct had a bool field
* at any offset until lib/strings.iterator landed (Hare's
* `iterator { dec, reverse }` shape).
*
* Sentinel per row: in the `mk` body, between the final pre-RET
* "RAX = @sretarg load" pair (the SysV return-the-pointer
* discipline) and the first MOVQ word-store into (BX), assert
* the last copy instruction targets the narrow MOV width derived
* from the final field's type — NOT MOVQ. The trailing-field offset
* is hard-coded per row (structurally fixed) and we pin both the
* read-from-(BP) and store-to-(BX) sides.
*
* Plus byte-id between stages per row.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.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;
}
/* `off` = byte offset of the trailing narrow field inside the
* struct (= natural offset of that field). `mov` = expected mnemonic
* for the trailing field-copy at that offset (MOVB / MOVW / MOVL).
* Rows all share the shape `{ i32 a, []u8 s, NARROW r }` so a sits
* at 0..4, s at 8..32, r at 32..32+sz(r). */
struct row { const char *label; const char *src; int off; const char *mov; };
static const struct row rows[] = {
{ "trailing_bool",
"type t = struct { a: i32, s: []u8, r: bool };\n"
"export fn mk() t = {\n"
" let v: t; let z: []u8;\n"
" v.s = z; v.a = 0; v.r = false;\n"
" return v;\n"
"};\n"
"export fn main() i32 = {\n"
" let x: t = mk();\n"
" if (x.r) { return 1; };\n"
" return 0;\n"
"};\n", 32, "MOVB" },
{ "trailing_u8",
"type t = struct { a: i32, s: []u8, r: u8 };\n"
"export fn mk() t = {\n"
" let v: t; let z: []u8;\n"
" v.s = z; v.a = 0; v.r = 0u8;\n"
" return v;\n"
"};\n"
"export fn main() i32 = {\n"
" let x: t = mk();\n"
" if (x.r != 0u8) { return 1; };\n"
" return 0;\n"
"};\n", 32, "MOVB" },
/* Trailing i16: natural size 34. The chained loop has only
* MOVQ/MOVL/MOVB arms (matching wwstage's identical chain),
* so a 2-byte tail falls through to 2× MOVB at offsets 32, 33.
* That's still narrow + byte-id; the bug-check is the negative
* "no MOVQ at offset 32". Promoting both stages to a MOVW arm
* is a separate refactor (preserves byte-id but out of #33). */
{ "trailing_i16",
"type t = struct { a: i32, s: []u8, r: i16 };\n"
"export fn mk() t = {\n"
" let v: t; let z: []u8;\n"
" v.s = z; v.a = 0; v.r = 0i16;\n"
" return v;\n"
"};\n"
"export fn main() i32 = {\n"
" let x: t = mk();\n"
" if (x.r != 0i16) { return 1; };\n"
" return 0;\n"
"};\n", 32, "MOVB" },
{ "trailing_i32",
"type t = struct { a: i32, s: []u8, r: i32 };\n"
"export fn mk() t = {\n"
" let v: t; let z: []u8;\n"
" v.s = z; v.a = 0; v.r = 0;\n"
" return v;\n"
"};\n"
"export fn main() i32 = {\n"
" let x: t = mk();\n"
" if (x.r != 0) { return 1; };\n"
" return 0;\n"
"};\n", 32, "MOVL" },
};
static int
emit_s(const char *w6c, const struct row *r, int i, char *out_s, size_t cap)
{
char src[96], cmd[1024];
snprintf(src, sizeof src, "/tmp/sret_narrow_%d_%d.ww", getpid(), i);
snprintf(out_s, cap, "/tmp/sret_narrow_%d_%d_%s.s",
getpid(), i, w6c[strlen(w6c) - 1] == 'w' ? "ww" : "c");
FILE *f = fopen(src, "wb");
if (!f) return -1;
fputs(r->src, f);
fclose(f);
snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, out_s, src);
int rc = runwait(cmd);
unlink(src);
return rc;
}
/* Asm-presence sentinel: inside mk's body (between `TEXT mk,` and
* the first `RET`), the trailing field-copy at offset `r->off` must
* use the narrow MOV mnemonic — both the read-from-(BP) and the
* store-to-(BX) lines. Pre-fix the cstage emitted MOVQ at that
* offset (8-byte over-wide) because the loop drove off slot-padded
* size; wwstage emitted the narrow MOV via natural size. */
static int
check_narrow_mov(const char *path, const struct row *r)
{
FILE *f = fopen(path, "rb");
if (!f) return -1;
char line[1024];
int in_mk = 0;
int saw_store = 0;
int saw_load = 0;
char store_needle[64], load_needle[64];
snprintf(store_needle, sizeof store_needle, "%s\tAX, %d(BX)\n",
r->mov, r->off);
snprintf(load_needle, sizeof load_needle, "%d(BP), AX",
-16); /* not used as exact-match — see contains below */
(void)load_needle;
while (fgets(line, sizeof line, f)) {
if (!in_mk) {
if (strstr(line, "TEXT mk,")
|| strstr(line, "TEXT\tmk,"))
in_mk = 1;
continue;
}
if (strstr(line, "\tRET\n")) break;
if (strstr(line, store_needle)) saw_store = 1;
/* load: `\tMOV?\t-K(BP), AX\n` with the matching width. */
char loadkey[16];
snprintf(loadkey, sizeof loadkey, "%s\t", r->mov);
if (strstr(line, loadkey) && strstr(line, "(BP), AX"))
saw_load = 1;
}
fclose(f);
if (!saw_store) {
fprintf(stderr,
"row[%s]: expected `%s AX, %d(BX)` in mk body\n",
r->label, r->mov, r->off);
return -1;
}
if (!saw_load) {
fprintf(stderr,
"row[%s]: expected narrow `%s -K(BP), AX` load in mk\n",
r->label, r->mov);
return -1;
}
return 0;
}
/* Negative sentinel: pre-fix bug pattern. cstage emitted
* `MOVQ AX, <r->off>(BX)` for the narrow trailing field — assert
* absence in mk body. */
static int
check_no_movq_at_off(const char *path, const struct row *r)
{
FILE *f = fopen(path, "rb");
if (!f) return -1;
char line[1024];
int in_mk = 0;
char needle[64];
snprintf(needle, sizeof needle, "MOVQ\tAX, %d(BX)\n", r->off);
int bad = 0;
while (fgets(line, sizeof line, f)) {
if (!in_mk) {
if (strstr(line, "TEXT mk,")
|| strstr(line, "TEXT\tmk,"))
in_mk = 1;
continue;
}
if (strstr(line, "\tRET\n")) break;
if (strstr(line, needle)) { bad = 1; break; }
}
fclose(f);
if (bad) {
fprintf(stderr,
"row[%s]: unexpected `MOVQ AX, %d(BX)` (pre-#33"
" over-wide pattern) in mk\n", r->label, r->off);
return -1;
}
return 0;
}
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 w6c[640], w6c_ww[640];
snprintf(w6c, sizeof w6c, "%s/w6c", bin);
snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin);
int have_ww = (access(w6c_ww, X_OK) == 0);
int n = (int)(sizeof rows / sizeof rows[0]);
int total = 0, fail = 0;
for (int i = 0; i < n; i++) {
char cs_path[128], ws_path[128];
if (emit_s(w6c, &rows[i], i, cs_path, sizeof cs_path) != 0) {
fprintf(stderr, "row[%s]: w6c failed\n", rows[i].label);
fail++; total++; continue;
}
total += 2;
if (check_narrow_mov(cs_path, &rows[i]) != 0) fail++;
if (check_no_movq_at_off(cs_path, &rows[i]) != 0) fail++;
if (!have_ww) { unlink(cs_path); continue; }
if (emit_s(w6c_ww, &rows[i], i, ws_path, sizeof ws_path) != 0) {
fprintf(stderr,
"row[%s]: w6c_ww failed\n", rows[i].label);
fail++; total++;
unlink(cs_path);
continue;
}
total += 2;
if (check_narrow_mov(ws_path, &rows[i]) != 0) fail++;
if (check_no_movq_at_off(ws_path, &rows[i]) != 0) fail++;
total++;
char cmd[512];
snprintf(cmd, sizeof cmd, "cmp -s %s %s", cs_path, ws_path);
if (runwait(cmd) != 0) {
fprintf(stderr,
"row[%s]: cstage vs wwstage asm differs\n",
rows[i].label);
fail++;
}
unlink(cs_path); unlink(ws_path);
}
if (fail) {
fprintf(stderr,
"sret_narrow_field: %d/%d fixtures failed\n",
fail, total);
return 1;
}
printf("sret_narrow_field: %d/%d ok\n", total, total);
return 0;
}