Files
ww/test/wcc/744_letcopy_struct.c
Hojun-Cho d84704e389 cgen+test: copy struct >8B local-ident rhs in N_LET (#32)
let p2: T = p1; where T is a struct >8B and rhs is a local ident
silently dropped most of the copy. Cstage's N_LET fell past every
specialized rhs branch (str/tuple/tagged/structlit/call) without
matching the bare-ident case, then past the sz==8 fallback (false)
to the no-rhs zero-init (false: rhs present), emitting zero
instructions — the dest slot read fresh-stack zeros. Wwstage's
cglet fell to cgexpr+MOVQ AX which loads only the first qword
(cgident shape for struct ident), and for sz==16 slots the
str-init tail then stored a stale BX into +8. Reads after the
let saw whatever the stack held: silent partial copy.

Both stages now byte-copy src slot → dst slot per qword with
a sized tail (MOVL/MOVB) for natural sizes not 8-aligned.
Mirrors cg_widen_tagged_store's struct-ident payload copy.

744_letcopy_struct pins the four struct shapes (3×i32, i32+str,
i32+[]u8, i32+tagged) on asm-presence in both stages, cmp -s
byte-id, and runtime exit code via both drivers.

Scope: only N_IDENT rhs at the local-ident-found path. Filed as
siblings (no in-tree consumer today, bootstrap byte-id proves it):
  - N_DOT / N_INDEX / N_UN(deref) struct rhs.
  - Top-level (non-local) struct ident rhs.
  - TY_TUPLE same-shape ident-copy bug.

Row (a) uses tri{a=11, b=22, c=33} structlit init for p1 to
isolate this fix from STATUS-3 #15/#26c (no-rhs zero-init sz=12
vs sz=16 slot-padded divergence between stages, separate task).
Row (d) runtime check uses only p2.a to isolate from match-on-
tagged-field scrutinee spill divergence (same task).

118/118 ok. ww2 == ww3 == ww4 byte-id holds.
2026-05-18 23:01:09 +09:00

381 lines
11 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.
/*
* 744_letcopy_struct — sentinel for STATUS-6 #32. Class B-symmetric
* cgen miscompile: `let p2: T = p1;` where T is a struct >8B and rhs
* is a local ident silently zero-inits p2 (cstage emitted nothing for
* the copy; wwstage emitted a single MOVQ AX + stale BX from the
* sz==16 str-init tail). Reads after the let saw whatever the stack
* held — silent partial-copy / silent zero on a fresh frame.
*
* Both stages now byte-copy the source slot to the dest slot
* per-qword with a sized tail (MOVL/MOVB) for natural sizes not
* 8-aligned. Mirrors cg_widen_tagged_store's struct-ident payload
* copy. See cmd/w6c/cgen.c N_LET and selfhost/cmd/wcc/cgenstmt.ww
* cglet.
*
* Rows exercise the 4 struct shapes the task brief called out:
* (a) 3-field i32 (sz=12, MOVQ + MOVL tail).
* (b) i32 + str field (sz=24, 3 × MOVQ).
* (c) i32 + []u8 slice field (sz=32, 4 × MOVQ).
* (d) i32 + tagged (i32|str) field (sz=32, 4 × MOVQ).
*
* Each row pins:
* - asm-presence in both stages: produce body emits `min_loads`
* `MOVQ -K(BP), AX` source-slot loads (the bug pin — pre-fix
* cstage emitted 0; wwstage emitted 1 with garbage BX tail).
* - asm byte-id between stages (cmp -s on the produce-bearing .s).
* - runtime: produce() returns 0 (every field round-tripped).
*/
#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;
}
static int
slurp(const char *path, char *buf, size_t cap)
{
FILE *f = fopen(path, "rb");
if (!f) return -1;
size_t n = fread(buf, 1, cap - 1, f);
fclose(f);
buf[n] = '\0';
return (int)n;
}
static int
count_substr(const char *start, const char *end, const char *needle)
{
int n = 0;
size_t nlen = strlen(needle);
const char *p = start;
while (p + nlen <= end) {
if (memcmp(p, needle, nlen) == 0) { n++; p += nlen; }
else { p++; }
}
return n;
}
struct row {
const char *label;
/* Direct-w6c source: bare fn produce, no package/imports. */
const char *asm_src;
/* Driver source: `package main; import os; ...` runtime wrapper. */
const char *run_src;
/* Source-slot loads expected in produce: at least N for the copy.
* Pre-fix cstage emitted 0; pre-fix wwstage emitted 1 with stale BX.
* `MOVQ -K(BP), AX` is also emitted by p2.a / p2.s.len reads in the
* checks, so set min_loads to a value the copy alone surpasses. */
int min_loads;
};
static const struct row rows[] = {
/* (a) 3-field i32 struct, sz=12. Copy = 1 × MOVQ + 1 × MOVL.
* p1 inits via structlit to sidestep the pre-existing no-rhs
* zero-init divergence for non-8-multiple struct sizes
* (cstage uses natural sz=12 → MOVQ+MOVL, wwstage uses slot
* sz=16 → MOVQ+MOVQ). Tracked as the #15/#26c rule-10
* size-strategy convergence follow-up. */
{ "tri_i32",
"type tri = struct { a: i32, b: i32, c: i32 };\n"
"export fn produce() i32 = {\n"
" let p1: tri = tri{a=11, b=22, c=33};\n"
" let p2: tri = p1;\n"
" if (p2.a != 11) { return 11; };\n"
" if (p2.b != 22) { return 12; };\n"
" if (p2.c != 33) { return 13; };\n"
" return 0;\n"
"};\n",
"package main;\n"
"import os;\n"
"type tri = struct { a: i32, b: i32, c: i32 };\n"
"fn produce() i32 = {\n"
" let p1: tri = tri{a=11, b=22, c=33};\n"
" let p2: tri = p1;\n"
" if (p2.a != 11) { return 11; };\n"
" if (p2.b != 22) { return 12; };\n"
" if (p2.c != 33) { return 13; };\n"
" return 0;\n"
"};\n"
"export fn main() i32 = { os.exit(produce()); };\n",
1 },
/* (b) i32 + str field, sz=24. Copy = 3 × MOVQ. */
{ "field_str",
"type ws = struct { a: i32, s: str };\n"
"export fn produce() i32 = {\n"
" let p1: ws;\n"
" p1.a = 7; p1.s = \"hi\";\n"
" let p2: ws = p1;\n"
" if (p2.a != 7) { return 11; };\n"
" if (p2.s.len != 2) { return 12; };\n"
" return 0;\n"
"};\n",
"package main;\n"
"import os;\n"
"type ws = struct { a: i32, s: str };\n"
"fn produce() i32 = {\n"
" let p1: ws;\n"
" p1.a = 7; p1.s = \"hi\";\n"
" let p2: ws = p1;\n"
" if (p2.a != 7) { return 11; };\n"
" if (p2.s.len != 2) { return 12; };\n"
" return 0;\n"
"};\n"
"export fn main() i32 = { os.exit(produce()); };\n",
3 },
/* (c) i32 + []u8 slice field, sz=32. Copy = 4 × MOVQ. */
{ "field_slice",
"type wsl = struct { a: i32, b: []u8 };\n"
"export fn produce() i32 = {\n"
" let raw: [3]u8 = [1: u8, 2: u8, 3: u8];\n"
" let p1: wsl;\n"
" p1.a = 9; p1.b = raw[0:3];\n"
" let p2: wsl = p1;\n"
" if (p2.a != 9) { return 11; };\n"
" if (p2.b.len != 3) { return 12; };\n"
" return 0;\n"
"};\n",
"package main;\n"
"import os;\n"
"type wsl = struct { a: i32, b: []u8 };\n"
"fn produce() i32 = {\n"
" let raw: [3]u8 = [1: u8, 2: u8, 3: u8];\n"
" let p1: wsl;\n"
" p1.a = 9; p1.b = raw[0:3];\n"
" let p2: wsl = p1;\n"
" if (p2.a != 9) { return 11; };\n"
" if (p2.b.len != 3) { return 12; };\n"
" return 0;\n"
"};\n"
"export fn main() i32 = { os.exit(produce()); };\n",
4 },
/* (d) i32 + tagged (i32|str) field, sz=32. Copy = 4 × MOVQ.
* Runtime checks only p2.a; the asm load-count (>=4) and the
* byte-id gate together prove the full 32B slot copied — match
* on p2.t avoided because wwstage's match scrutinee spill grows
* the frame and reorders ABI loads vs cstage (pre-existing
* #15/#26c rule-10 size-strategy / match-spill divergence). */
{ "field_tagged",
"type tag = (i32 | str);\n"
"type wtg = struct { a: i32, t: tag };\n"
"export fn produce() i32 = {\n"
" let p1: wtg = wtg{a=5, t=42: tag};\n"
" let p2: wtg = p1;\n"
" if (p2.a != 5) { return 11; };\n"
" return 0;\n"
"};\n",
"package main;\n"
"import os;\n"
"type tag = (i32 | str);\n"
"type wtg = struct { a: i32, t: tag };\n"
"fn produce() i32 = {\n"
" let p1: wtg = wtg{a=5, t=42: tag};\n"
" let p2: wtg = p1;\n"
" if (p2.a != 5) { return 11; };\n"
" match (p2.t) {\n"
" case let x: i32 => if (x != 42) { return 12; };\n"
" case let s: str => return 13;\n"
" };\n"
" return 0;\n"
"};\n"
"export fn main() i32 = { os.exit(produce()); };\n",
4 },
};
/* Locate `TEXT produce` body in the asm file and count source-slot
* loads (`MOVQ\t-K(BP), AX`) inside it. Pre-fix cstage produce had 0;
* pre-fix wwstage produce had 1 (no tail). */
static int
check_copy_loads(const char *spath, const struct row *r)
{
static char buf[1 << 16];
if (slurp(spath, buf, sizeof buf) < 0) return -1;
const char *body = strstr(buf, "TEXT produce");
if (!body) {
fprintf(stderr, "row[%s]: no TEXT produce label in %s\n",
r->label, spath);
return -1;
}
/* End at the next TEXT (or EOF). */
const char *end = strstr(body + 1, "\nTEXT ");
if (!end) end = buf + strlen(buf);
int loads = count_substr(body, end, "MOVQ\t-");
if (loads < r->min_loads) {
fprintf(stderr,
"row[%s]: only %d `MOVQ -K(BP), AX` source loads in "
"produce; expected >= %d (copy missing)\n",
r->label, loads, r->min_loads);
return -1;
}
return 0;
}
static int
emit_s(const char *w6c, const struct row *r, int i, int stage,
char *out_s, size_t cap)
{
char src[64], cmd[1024];
snprintf(src, sizeof src, "/tmp/lcs_asm_%d_%d_%d.ww",
getpid(), i, stage);
snprintf(out_s, cap, "/tmp/lcs_asm_%d_%d_%d.s",
getpid(), i, stage);
FILE *f = fopen(src, "wb");
if (!f) return -1;
fputs(r->asm_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;
}
static int
run_driver(const char *driver, const struct row *r, int i, int stage)
{
char src[64], tmpdir[64], cmd[1024];
snprintf(src, sizeof src, "/tmp/lcs_run_%d_%d_%d.ww",
getpid(), i, stage);
snprintf(tmpdir, sizeof tmpdir, "/tmp/lcs_run_%d_d_%d_%d",
getpid(), i, stage);
FILE *f = fopen(src, "wb");
if (!f) return -1;
fputs(r->run_src, f);
fclose(f);
mkdir(tmpdir, 0755);
snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>/dev/null",
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);
/* Best-effort cleanup; the .s / .o / .combined.ww side files share
* the same stem and don't interfere with the next row. */
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 w6c[640], w6c_ww[640], cdrv[640], wdrv[640];
snprintf(w6c, sizeof w6c, "%s/w6c", bin);
snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin);
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin);
int have_ww = (access(w6c_ww, X_OK) == 0);
int have_wdrv = (access(wdrv, X_OK) == 0);
int n = (int)(sizeof rows / sizeof rows[0]);
int total = 0, fail = 0;
for (int i = 0; i < n; i++) {
const struct row *r = &rows[i];
char cs_path[160], ws_path[160];
/* cstage asm-presence. */
if (emit_s(w6c, r, i, 0, cs_path, sizeof cs_path) != 0) {
fprintf(stderr,
"letcopy_struct[cstage][%s]: w6c failed\n",
r->label);
fail++; total++; continue;
}
total++;
if (check_copy_loads(cs_path, r) != 0) fail++;
/* wwstage asm-presence + byte-id. */
if (have_ww) {
if (emit_s(w6c_ww, r, i, 1, ws_path, sizeof ws_path)
!= 0) {
fprintf(stderr,
"letcopy_struct[wwstage][%s]: "
"w6c_ww failed\n", r->label);
fail++; total++;
unlink(cs_path);
continue;
}
total++;
if (check_copy_loads(ws_path, r) != 0) fail++;
total++;
char cmd[512];
snprintf(cmd, sizeof cmd, "cmp -s %s %s",
cs_path, ws_path);
if (runwait(cmd) != 0) {
fprintf(stderr,
"letcopy_struct[%s]: cstage vs wwstage "
"asm differs\n", r->label);
fail++;
}
unlink(ws_path);
}
unlink(cs_path);
/* Runtime correctness via cstage driver. */
total++;
int got = run_driver(cdrv, r, i, 2);
if (got != 0) {
fprintf(stderr,
"letcopy_struct[ww/%s]: exit=%d want=0\n",
r->label, got);
fail++;
}
/* Runtime correctness via wwstage driver. */
if (have_wdrv) {
total++;
got = run_driver(wdrv, r, i, 3);
if (got != 0) {
fprintf(stderr,
"letcopy_struct[ww_ww/%s]: exit=%d want=0\n",
r->label, got);
fail++;
}
}
}
if (fail) {
fprintf(stderr,
"letcopy_struct: %d/%d fixtures failed\n", fail, total);
return 1;
}
printf("letcopy_struct: %d/%d ok\n", total, total);
return 0;
}