w6c+selfhost: cgen N_DOT slice-field through *T root in call args (closes #29)

This commit is contained in:
2026-05-14 23:29:35 +09:00
parent 9706513e59
commit 6402d8deb7
7 changed files with 857 additions and 24 deletions

View File

@@ -0,0 +1,307 @@
/*
* 691_dot_slice_arg — pass `<expr>.slicefield` as a call argument
* (and return one too).
*
* Task #29: the cgen N_DOT-slice-field paths previously loaded only
* .ptr into AX, leaving BX/CX stale. The slice-arg push at the call
* site then pushed three words from AX/BX/CX, so .len/.cap silently
* came from whatever the prior expression left in those registers.
* Surfaced as `998_bufio_run`'s bstreamunread mid-read len mismatch.
*
* Each fixture passes (or returns) a slice header through a call and
* reads .len / .cap / .ptr on the callee side to prove all three
* words round-trip. Rows cover:
* - single dot through `*T` root (`p.sl`)
* - chained through `*T` root (`p.inner.sl`) — pins
* task #22's spine walker still works with the slice-leaf fix
* in place
* - local value-struct root, single dot (`o.sl`)
* - local value-struct root, chained (`o.inner.sl`)
* - direct slice arg (no dot) — baseline / negative
* control: confirms the existing fast-path still works
* - slice + int neighbor arg — pins call-arg eval
* order doesn't drop the integer slot beside the slice
* - slice mutation through `.ptr` — pins `.ptr` half
* still arrives correctly post-fix
* - N_RETURN of slice N_DOT (probe 3) — exercises the same
* cgen N_DOT slice-load path on the return-stmt site, not
* just the call-arg site
*
* Exercises both stages via `ww` (cstage) and `ww_ww` (wwstage).
*/
#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[] = {
/* Single dot through *T root. `slen(p.sl)` should see len=5,
* cap=8, ptr[0]=7 (we set arr[0]=7 in main). */
{ "ptr_root_slice_arg",
"type outer = struct { sl: []u8, x: i32 };\n"
"fn slen(s: []u8) i32 = { return s.len: i32; };\n"
"fn scap(s: []u8) i32 = { return s.cap: i32; };\n"
"fn sfirst(s: []u8) i32 = { return s[0]: i32; };\n"
"fn main() i32 = {\n"
" let arr: [8]u8; arr[0] = 7u8;\n"
" let o: outer;\n"
" o.sl.ptr = &arr[0]; o.sl.len = 5; o.sl.cap = 8;\n"
" let p: *outer = &o;\n"
" if (slen(p.sl) != 5) { return 1; };\n"
" if (scap(p.sl) != 8) { return 2; };\n"
" if (sfirst(p.sl) != 7) { return 3; };\n"
" return 42;\n"
"};\n",
42 },
/* Chained through *T root with a value-struct hop — the #22
* spine walker shape, leaf is a slice. */
{ "ptr_root_chained_slice_arg",
"type inner = struct { sl: []u8, pad: i32 };\n"
"type outer = struct { i: inner, tag: i32 };\n"
"fn slen(s: []u8) i32 = { return s.len: i32; };\n"
"fn scap(s: []u8) i32 = { return s.cap: i32; };\n"
"fn sfirst(s: []u8) i32 = { return s[0]: i32; };\n"
"fn main() i32 = {\n"
" let arr: [16]u8; arr[0] = 11u8;\n"
" let o: outer;\n"
" o.i.sl.ptr = &arr[0]; o.i.sl.len = 9; o.i.sl.cap = 16;\n"
" let p: *outer = &o;\n"
" if (slen(p.i.sl) != 9) { return 1; };\n"
" if (scap(p.i.sl) != 16) { return 2; };\n"
" if (sfirst(p.i.sl) != 11) { return 3; };\n"
" return 42;\n"
"};\n",
42 },
/* Local value-struct rooted, single dot. The previous bug was
* present here too — the "real struct field" cgen branch loaded
* only AX. */
{ "local_root_slice_arg",
"type outer = struct { sl: []u8, x: i32 };\n"
"fn slen(s: []u8) i32 = { return s.len: i32; };\n"
"fn scap(s: []u8) i32 = { return s.cap: i32; };\n"
"fn sfirst(s: []u8) i32 = { return s[0]: i32; };\n"
"fn main() i32 = {\n"
" let arr: [8]u8; arr[0] = 3u8;\n"
" let o: outer;\n"
" o.sl.ptr = &arr[0]; o.sl.len = 4; o.sl.cap = 8;\n"
" if (slen(o.sl) != 4) { return 1; };\n"
" if (scap(o.sl) != 8) { return 2; };\n"
" if (sfirst(o.sl) != 3) { return 3; };\n"
" return 42;\n"
"};\n",
42 },
/* Local value-struct rooted, chained value-struct hop. Spine
* walker without ptr_root. */
{ "local_root_chained_slice_arg",
"type inner = struct { sl: []u8, pad: i32 };\n"
"type outer = struct { i: inner, tag: i32 };\n"
"fn slen(s: []u8) i32 = { return s.len: i32; };\n"
"fn scap(s: []u8) i32 = { return s.cap: i32; };\n"
"fn main() i32 = {\n"
" let arr: [16]u8;\n"
" let o: outer;\n"
" o.i.sl.ptr = &arr[0]; o.i.sl.len = 6; o.i.sl.cap = 16;\n"
" if (slen(o.i.sl) != 6) { return 1; };\n"
" if (scap(o.i.sl) != 16) { return 2; };\n"
" return 42;\n"
"};\n",
42 },
/* Chained through a `*struct` mid-hop (`o.i.sl` where `i: *inner`).
* Distinct cgen site from rows 2/4: the spine walker (dotchain-
* resolve) only walks value-struct intermediate hops, so a *struct
* mid-hop falls through to the "chained N_DOT through *struct
* field" branch (cgen.c ~4832, cgenexpr.ww ~1834) — the fourth
* TY_SLICE site touched by this commit. AX is the *struct base
* after `cgexpr(o.i)`, so the load order must end with .ptr → AX. */
{ "ptr_field_chained_slice_arg",
"type inner = struct { sl: []u8, pad: i32 };\n"
"type outer = struct { i: *inner, tag: i32 };\n"
"fn slen(s: []u8) i32 = { return s.len: i32; };\n"
"fn scap(s: []u8) i32 = { return s.cap: i32; };\n"
"fn sfirst(s: []u8) i32 = { return s[0]: i32; };\n"
"fn main() i32 = {\n"
" let arr: [8]u8; arr[0] = 17u8;\n"
" let inn: inner;\n"
" inn.sl.ptr = &arr[0]; inn.sl.len = 3; inn.sl.cap = 8;\n"
" let o: outer; o.i = &inn; o.tag = 0;\n"
" if (slen(o.i.sl) != 3) { return 1; };\n"
" if (scap(o.i.sl) != 8) { return 2; };\n"
" if (sfirst(o.i.sl) != 17) { return 3; };\n"
" return 42;\n"
"};\n",
42 },
/* Negative control: direct slice arg, no dot. Pre-existing fast
* path; confirms the fix didn't disturb it. */
{ "direct_slice_arg_baseline",
"fn slen(s: []u8) i32 = { return s.len: i32; };\n"
"fn scap(s: []u8) i32 = { return s.cap: i32; };\n"
"fn sfirst(s: []u8) i32 = { return s[0]: i32; };\n"
"fn main() i32 = {\n"
" let arr: [8]u8; arr[0] = 9u8;\n"
" let s: []u8;\n"
" s.ptr = &arr[0]; s.len = 5; s.cap = 8;\n"
" if (slen(s) != 5) { return 1; };\n"
" if (scap(s) != 8) { return 2; };\n"
" if (sfirst(s) != 9) { return 3; };\n"
" return 42;\n"
"};\n",
42 },
/* Mixed args: pass a slice-field next to another arg, so the
* fix has to leave neighbouring arg-slot registers undisturbed.
* AX/BX/CX are clobbered while building the slice; the integer
* `tag` arg goes in DI on SysV, so this pins the call-arg eval
* order doesn't drop the tag. */
{ "ptr_root_slice_with_neighbor",
"type outer = struct { sl: []u8, x: i32 };\n"
"fn slen_tag(s: []u8, t: i32) i32 = { return s.len: i32 + t; };\n"
"fn main() i32 = {\n"
" let arr: [8]u8;\n"
" let o: outer;\n"
" o.sl.ptr = &arr[0]; o.sl.len = 5; o.sl.cap = 8;\n"
" let p: *outer = &o;\n"
" let v: i32 = slen_tag(p.sl, 37);\n"
" if (v != 42) { return 1; };\n"
" return 42;\n"
"};\n",
42 },
/* Mutation through the passed slice: callee writes via s[0],
* caller reads arr[0] after. Proves the .ptr half made it
* across — even before the fix this *would have* worked (only
* .len/.cap dropped), but co-testing it guards against a future
* regression that swaps ptr for len. */
{ "ptr_root_slice_mutate_ptr",
"type outer = struct { sl: []u8, x: i32 };\n"
"fn poke(s: []u8) void = { s[0] = 0x42u8; };\n"
"fn main() i32 = {\n"
" let arr: [8]u8; arr[0] = 0u8;\n"
" let o: outer;\n"
" o.sl.ptr = &arr[0]; o.sl.len = 1; o.sl.cap = 8;\n"
" let p: *outer = &o;\n"
" poke(p.sl);\n"
" if (arr[0]: i32 != 0x42) { return 1; };\n"
" return 42;\n"
"};\n",
42 },
/* Probe 3: N_RETURN of a slice N_DOT — a function returns
* `p.sl` directly. cgen's return-stmt site also has to leave
* (AX=ptr, BX=len, CX=cap), or the caller's `let s: []u8 =
* fetch(p)` will lose .len/.cap. Distinct site from the call-
* arg push but exercises the same cgen N_DOT slice-load path. */
{ "ptr_root_slice_return",
"type outer = struct { sl: []u8, x: i32 };\n"
"fn fetch(p: *outer) []u8 = { return p.sl; };\n"
"fn main() i32 = {\n"
" let arr: [8]u8; arr[0] = 19u8;\n"
" let o: outer;\n"
" o.sl.ptr = &arr[0]; o.sl.len = 6; o.sl.cap = 8;\n"
" let p: *outer = &o;\n"
" let s: []u8 = fetch(p);\n"
" if (s.len: i32 != 6) { return 1; };\n"
" if (s.cap: i32 != 8) { return 2; };\n"
" if (s[0]: i32 != 19) { return 3; };\n"
" return 42;\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/wdsa_%d_%d.ww", getpid(), i);
snprintf(tmpdir, sizeof tmpdir, "/tmp/wdsa_%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_slice_arg: 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_slice_arg[%s][%s]: exit=%d want=%d\n",
drivers[d].name, rows[i].label,
got, rows[i].want);
fail++;
}
}
}
if (fail) {
fprintf(stderr,
"dot_slice_arg: %d/%d fixtures failed\n", fail, total);
return 1;
}
printf("dot_slice_arg: %d/%d ok\n", total, total);
return 0;
}