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).
277 lines
8.2 KiB
C
277 lines
8.2 KiB
C
/*
|
||
* 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;
|
||
}
|