Files
ww/test/wcc/650_dot_chain.c
Hojun-Cho d66aef382a w6c+selfhost: cgen *T-rooted chained N_DOT (closes #22)
Spine walker now accepts *T root at the last hop (cur->lhs->kind ==
N_IDENT, pu->kind == TY_PTR), substitutes pointee struct, emits
MOVQ off(BP), CX before offset arithmetic. Symmetric in N_DOT and
N_ASSIGN. Distinct gate from existing mid-chain *T-field branch
(cgen.c:4615) — no shadow.

Unblocks task #18 (bufio writer first-field embed). Pre-existing
*T-field mid-chain path unchanged.
2026-05-14 02:44:57 +09:00

435 lines
14 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.
/*
* 650_dot_chain — chained dotted access through value-struct fields
* must compile to a single load/store at (base + sum-of-offsets),
* not silently lower to an undefined global symbol or shuffle stale
* registers. Drew filed this from worker-bufio when the Hare-shaped
* `scanner` embed in lib/bufio tripped the cgen.
*
* Each fixture exercises a different chain shape. The repro from
* drew's bug report is row[0]; the rest pin: 3-deep chains, slice
* pseudo-field tails (`s.buf.len`), global-rooted chains, and
* mixed leaf widths (u8 / i32 / u32). `&o.i.a` address-of and i8
* sign-extend leaves are deferred (tasks #9 / #10).
*
* Exercises both stages via `ww` (cstage) and `ww_ww` (wwstage)
* when present, so a regression on either side is caught here.
*/
#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[] = {
/* Drew's repro: 2-deep value-struct read AND write. Returns
* o.i.a (=10) + o.x (=5) = 15. */
{ "drew_2deep_read_write",
"type inner = struct { a: i32, b: i32, c: i32 };\n"
"type outer = struct { i: inner, x: i32 };\n"
"fn main() i32 = {\n"
" let o: outer; o.i.a = 10; o.x = 5;\n"
" return o.i.a + o.x;\n"
"};\n",
15 },
/* 3-deep value-struct chain. Proves the spine walker is loop-
* shaped, not hardcoded to depth 2. */
{ "value_struct_3deep",
"type a3 = struct { a: i32 };\n"
"type a2 = struct { a: a3 };\n"
"type a1 = struct { a: a2 };\n"
"fn main() i32 = {\n"
" let v: a1;\n"
" v.a.a.a = 7;\n"
" return v.a.a.a;\n"
"};\n",
7 },
/* Slice pseudo-field tail through a value-struct field: write
* .ptr/.len/.cap on s.buf where buf: []u8 is a struct field.
* Read back .len through the same chain. */
{ "slice_field_pseudo",
"type bag = struct { buf: []u8, x: i32 };\n"
"fn main() i32 = {\n"
" let arr: [8]u8;\n"
" let i: i32 = 0;\n"
" for (i < 8) { arr[i] = i: u8; i = i + 1; };\n"
" let b: bag;\n"
" b.buf.ptr = &arr[0];\n"
" b.buf.len = 5;\n"
" b.buf.cap = 8;\n"
" b.x = 0;\n"
" return b.buf.len;\n"
"};\n",
5 },
/* u8 leaf through a chained struct: round-trips a single byte
* unchanged (no narrow cast in play — direct MOVB write +
* MOVZBQ read). Pins the leaf-width dispatch in the walker. */
{ "u8_leaf_through_chain",
"type holder = struct { val: u8, pad: u8 };\n"
"type box = struct { h: holder, tag: i32 };\n"
"fn main() i32 = {\n"
" let b: box;\n"
" b.h.val = 99u8;\n"
" b.h.pad = 0u8;\n"
" b.tag = 0;\n"
" return b.h.val: i32;\n"
"};\n",
99 },
/* i32 leaf through a chained struct, value > 255 to confirm the
* MOVL store width (not silently narrowed to MOVB). Returns 42
* iff the full 4-byte value round-trips. */
{ "i32_leaf_through_chain",
"type holder = struct { val: i32, pad: i32 };\n"
"type box = struct { h: holder, tag: i32 };\n"
"fn main() i32 = {\n"
" let b: box;\n"
" b.h.val = 0x12345;\n"
" b.h.pad = 0;\n"
" b.tag = 0;\n"
" if (b.h.val == 0x12345) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
/* u32 leaf through a chained struct. Distinct write + read at
* the chained leaf, value chosen so the MOVL store + load
* round-trip preserves all four bytes. */
{ "u32_leaf_through_chain",
"type holder = struct { val: u32, pad: u32 };\n"
"type box = struct { h: holder, tag: i32 };\n"
"fn main() i32 = {\n"
" let b: box;\n"
" b.h.val = 4321u32;\n"
" b.h.pad = 0u32;\n"
" b.tag = 0;\n"
" if (b.h.val == 4321u32) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
/* Global-rooted chain: write+read through a top-level `let g`.
* Pins the LEAQ name(SB), CX → MOVQ disp(CX) path on both
* stages (the local-rooted rows above pin the BP-relative
* path). 11 + 3 = 14. */
{ "global_root_chain",
"type inner = struct { a: i32, b: i32 };\n"
"type outer = struct { i: inner, x: i32 };\n"
"let g: outer;\n"
"fn main() i32 = {\n"
" g.i.a = 11; g.x = 3;\n"
" return g.i.a + g.x;\n"
"};\n",
14 },
/* `*T` parameter, single-dot read: `fn f(o: *outer) i32 = o.f`.
* Already worked via the existing pointer-to-struct-field
* branch; pinned here so future refactors don't regress it. */
{ "ptr_root_single_read",
"type outer = struct { f: i32, g: i32 };\n"
"fn get(o: *outer) i32 = { return o.f; };\n"
"fn main() i32 = {\n"
" let x: outer; x.f = 42; x.g = 0;\n"
" return get(&x);\n"
"};\n",
42 },
/* `*T` parameter, single-dot write: `fn f(o: *outer) void = …`.
* Already worked via the via_ptr branch in N_ASSIGN; pinned
* for the same reason. */
{ "ptr_root_single_write",
"type outer = struct { f: i32, g: i32 };\n"
"fn set(o: *outer) void = { o.f = 42; };\n"
"fn main() i32 = {\n"
" let x: outer; x.f = 0; x.g = 0;\n"
" set(&x);\n"
" return x.f;\n"
"};\n",
42 },
/* Drew's report shape — `*T` parameter, 2-deep read.
* Used to lower to `MOVQ a(SB), AX` (link-time undefined ref).
* Now the spine walker dereferences the root pointer. */
{ "ptr_root_2deep_read",
"type inner = struct { a: i32, b: i32 };\n"
"type outer = struct { i: inner, x: i32 };\n"
"fn get(o: *outer) i32 = { return o.i.a; };\n"
"fn main() i32 = {\n"
" let v: outer; v.i.a = 7; v.i.b = 0; v.x = 0;\n"
" return get(&v);\n"
"};\n",
7 },
/* `*T` parameter, 2-deep write — bufio.init shape.
* Previously silently dropped; now MOVQ off(BP), CX +
* store at total_off(CX). */
{ "ptr_root_2deep_write",
"type inner = struct { a: i32, b: i32 };\n"
"type outer = struct { i: inner, x: i32 };\n"
"fn set(o: *outer) void = { o.i.a = 5; };\n"
"fn main() i32 = {\n"
" let v: outer; v.i.a = 0; v.i.b = 0; v.x = 0;\n"
" set(&v);\n"
" return v.i.a;\n"
"};\n",
5 },
/* 3-deep `*T`-rooted: proves the spine walker loops past the
* pointer-root hop and isn't hardcoded at depth 2. */
{ "ptr_root_3deep",
"type a3 = struct { x: i32 };\n"
"type a2 = struct { a: a3 };\n"
"type a1 = struct { a: a2 };\n"
"fn put(p: *a1) void = { p.a.a.x = 9; };\n"
"fn get(p: *a1) i32 = { return p.a.a.x; };\n"
"fn main() i32 = {\n"
" let v: a1; v.a.a.x = 0;\n"
" put(&v);\n"
" return get(&v);\n"
"};\n",
9 },
/* `*T` *local* (not just param) — same emit path through
* localfind, but exercised separately for symmetry. */
{ "ptr_root_local",
"type inner = struct { a: i32, b: i32 };\n"
"type outer = struct { i: inner, x: i32 };\n"
"fn main() i32 = {\n"
" let v: outer; v.i.a = 0; v.i.b = 0; v.x = 0;\n"
" let p: *outer = &v;\n"
" p.i.a = 13;\n"
" return p.i.a;\n"
"};\n",
13 },
/* str leaf field through a `*T`-rooted chain — exercises the
* (AX=ptr, BX=len) convention through the CX-base load. */
{ "ptr_root_str_leaf",
"type holder = struct { s: str, pad: i32 };\n"
"type box = struct { h: holder, tag: i32 };\n"
"fn put(b: *box) void = { b.h.s = \"abc\"; };\n"
"fn main() i32 = {\n"
" let b: box;\n"
" let empty: str; b.h.s = empty; b.h.pad = 0; b.tag = 0;\n"
" put(&b);\n"
" if (b.h.s.len == 3) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
/* slice pseudo-field on a `*T`-rooted chain. .len of an empty
* slice round-trips through the CX-base load. */
{ "ptr_root_slice_pseudo",
"type bag = struct { buf: []u8, x: i32 };\n"
"fn setlen(b: *bag, n: i32) void = { b.buf.len = n; };\n"
"fn main() i32 = {\n"
" let b: bag;\n"
" let nope: []u8; b.buf = nope; b.x = 0;\n"
" setlen(&b, 5);\n"
" return b.buf.len: i32;\n"
"};\n",
5 },
/* f64 leaf field through a `*T`-rooted chain — exercises the
* X0 spill / MOVSD store path off the CX base. */
{ "ptr_root_f64_leaf",
"type holder = struct { v: f64, pad: i32 };\n"
"type box = struct { h: holder, tag: i32 };\n"
"fn put(b: *box) void = { b.h.v = 0.5f64; };\n"
"fn main() i32 = {\n"
" let b: box; b.h.v = 0.0f64; b.h.pad = 0; b.tag = 0;\n"
" put(&b);\n"
" if (b.h.v == 0.5f64) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
/* Sub-word leaf widths through a `*T`-rooted chain — pins
* fldloadop / fldstoreop on the CX-base path. u8/i8/u16/i16/i32
* all stored then read back through the inner pointer. */
{ "ptr_root_subword_u8",
"type holder = struct { v: u8, pad: u8 };\n"
"type box = struct { h: holder, tag: i32 };\n"
"fn put(b: *box) void = { b.h.v = 99u8; };\n"
"fn main() i32 = {\n"
" let b: box; b.h.v = 0u8; b.h.pad = 0u8; b.tag = 0;\n"
" put(&b);\n"
" return b.h.v: i32;\n"
"};\n",
99 },
{ "ptr_root_subword_i32",
"type holder = struct { v: i32, pad: i32 };\n"
"type box = struct { h: holder, tag: i32 };\n"
"fn put(b: *box) void = { b.h.v = 0x12345; };\n"
"fn main() i32 = {\n"
" let b: box; b.h.v = 0; b.h.pad = 0; b.tag = 0;\n"
" put(&b);\n"
" if (b.h.v == 0x12345) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
{ "ptr_root_subword_i16",
"type holder = struct { v: i16, pad: i16 };\n"
"type box = struct { h: holder, tag: i32 };\n"
"fn put(b: *box) void = { b.h.v = 4321i16; };\n"
"fn main() i32 = {\n"
" let b: box; b.h.v = 0i16; b.h.pad = 0i16; b.tag = 0;\n"
" put(&b);\n"
" if (b.h.v == 4321i16) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
/* Negative signed leaf widths through a `*T`-rooted chain.
* Stored value's sign bit must round-trip — pins localloadop /
* fldloadop sign-extension on the CX-base path so `*T`-rooted
* chains don't silently widen to a positive bit pattern. i8, i16
* and i32 each store 7 and check `< 0` after the chained read. */
{ "ptr_root_subword_i8_neg",
"type holder = struct { v: i8, pad: i8 };\n"
"type box = struct { h: holder, tag: i32 };\n"
"fn put(b: *box) void = { b.h.v = -7i8; };\n"
"fn main() i32 = {\n"
" let b: box; b.h.v = 0i8; b.h.pad = 0i8; b.tag = 0;\n"
" put(&b);\n"
" let r: i32 = b.h.v: i32;\n"
" if (r == -7) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
{ "ptr_root_subword_i16_neg",
"type holder = struct { v: i16, pad: i16 };\n"
"type box = struct { h: holder, tag: i32 };\n"
"fn put(b: *box) void = { b.h.v = -7i16; };\n"
"fn main() i32 = {\n"
" let b: box; b.h.v = 0i16; b.h.pad = 0i16; b.tag = 0;\n"
" put(&b);\n"
" let r: i32 = b.h.v: i32;\n"
" if (r == -7) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
{ "ptr_root_subword_i32_neg",
"type holder = struct { v: i32, pad: i32 };\n"
"type box = struct { h: holder, tag: i32 };\n"
"fn put(b: *box) void = { b.h.v = -7; };\n"
"fn main() i32 = {\n"
" let b: box; b.h.v = 0; b.h.pad = 0; b.tag = 0;\n"
" put(&b);\n"
" if (b.h.v < 0) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
/* Round-trip across the aliasing seam: caller writes via the
* local value, callee reads via the `*T`-rooted chain — and
* vice versa. Pins that both branches index the SAME slot
* (same field offsets resolved off the right base). */
{ "ptr_root_roundtrip_local_write_ptr_read",
"type inner = struct { a: i32, b: i32 };\n"
"type outer = struct { i: inner, x: i32 };\n"
"fn get(o: *outer) i32 = { return o.i.a; };\n"
"fn main() i32 = {\n"
" let o: outer; o.i.a = 0; o.i.b = 0; o.x = 0;\n"
" let p: *outer = &o;\n"
" o.i.a = 42;\n"
" return get(p);\n"
"};\n",
42 },
{ "ptr_root_roundtrip_ptr_write_local_read",
"type inner = struct { a: i32, b: i32 };\n"
"type outer = struct { i: inner, x: i32 };\n"
"fn main() i32 = {\n"
" let o: outer; o.i.a = 0; o.i.b = 0; o.x = 0;\n"
" let p: *outer = &o;\n"
" p.i.a = 42;\n"
" return o.i.a;\n"
"};\n",
42 },
};
static int
run_driver(const char *driver, const struct row *r, int i)
{
char src[64], tmpdir[64], cmd[1024];
snprintf(src, sizeof src, "/tmp/wdc_%d_%d.ww", getpid(), i);
snprintf(tmpdir, sizeof tmpdir, "/tmp/wdc_%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[128];
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[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, "dot_chain: 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,
"dot_chain[%s][%s]: exit=%d want=%d\n",
drivers[d].name, rows[i].label,
got, rows[i].want);
fail++;
}
}
}
if (fail) {
fprintf(stderr,
"dot_chain: %d/%d fixtures failed\n", fail, total);
return 1;
}
printf("dot_chain: %d/%d ok\n", total, total);
return 0;
}