selfhost+test: single-source-of-truth @tagscr scratch reservation (#38)
Closes STATUS latent #1: @tagscr shared 24B reservation across the four tagged-scratch sites (cgreturn, pushargsrev, cgindex tagged-elem, pointer-rooted struct-field tagged write). Any fn that needed >24B (e.g. slice-in-tagged-field 32B) silently overflowed into the neighbor frame slot. Surfaced concretely as getopttest's errortable wwstage exit 16 after #37 fixed the upstream gaps. c.tagscrsz: i32 on the cgen struct is the single source of truth. tagscrbump(c, need) in scanlocals raises the max across all 4 reservation sites and returns the frame delta. All emit sites (cgreturn / pushargsrev / cgindex / cgwidentaggedstore pointer- rooted) read c.tagscrsz instead of hardcoded 24. Mirrors the existing cgwidentaggedstore precedent; @tagbase keeps its 8B scanseenmark dedup (always 8B, correct). Unmasked latent bug (now fixed): scanlocals's pointer-rooted struct- field tagged-write detection uses localfindnode(c, base.str) to resolve the *struct base. For `fn fill(h: *holder)`, h's scan-time stub from scanseenmark had tnode=nil, so the @tagscr reservation never fired. Pre-#38 the hardcoded 24B masked this; #38's correctly- sized slot exposed it. cgfn's param scan loop now sets c.locals.tnode = scanp.lhs after scanseenmark so localfindnode resolves param types at scan time. Test 714 (tagged_return_scratch): 4 rows × 2 stages = 8 fixtures. Direct adjacency repro; match-arm field-by-field read; **mixed- sizes-one-fn** (16B pushargsrev widen + 32B cgreturn widen in the same body — pins the lockstep invariant that a sibling site can't undersize the shared slot); call-site struct-payload widen. Row 3 specifically would regress if a future refactor ever forgets to route an emit site through c.tagscrsz. 982 getopt_run green through both stages (was the original surface); 995 self_rebuild byte-id holds.
This commit is contained in:
303
test/wcc/714_tagged_return_scratch.c
Normal file
303
test/wcc/714_tagged_return_scratch.c
Normal file
@@ -0,0 +1,303 @@
|
||||
/*
|
||||
* 714_tagged_return_scratch — wwstage @tagscr scratch slot sized
|
||||
* dynamically to the per-function max across all reservation sites.
|
||||
*
|
||||
* Pre-fix every site (cgreturn / pushargsrev / cgindex / cgassign-via
|
||||
* cgwidentaggedstore) hardcoded 24B for @tagscr. A `(void | T)` return
|
||||
* where T is 24B has slot_size = 32 (8B tag + 24B payload); the
|
||||
* outer zero loop in cgreturn (cgenstmt.ww) then walked 32 bytes
|
||||
* starting at the slot's BP offset, spilling 8B past the slot's end
|
||||
* into the adjacent local — typically the very `T` being returned.
|
||||
* The pre-zero stomped on its kind/flag bytes, so the subsequent
|
||||
* field-by-field copy out delivered zeros in the AX/DX/CX/R8 return
|
||||
* ABI's DX slot. cstage was unaffected (its inline mklabel(c, "tagscr")
|
||||
* allocates a fresh slot of the requested size per call site).
|
||||
*
|
||||
* Fix (single source of truth via c.tagscrsz):
|
||||
* - cgen.ww: cgen struct gains `tagscrsz: i32`; cgeninit resets.
|
||||
* - cgendecl.ww: scanlocals's four @tagscr-reservation sites call
|
||||
* tagscrbump(c, slotsize(c, T)) which raises c.tagscrsz to the
|
||||
* max needed and returns the delta to add to the frame total.
|
||||
* - cgenstmt.ww / cgenutil.ww / cgenexpr.ww: every emit-time
|
||||
* localadd("@tagscr", _, nil) passes c.tagscrsz so the first
|
||||
* allocation in the fn lands a slot sized for every later user.
|
||||
* - cgenutil.ww (cgwidentaggedstore pointer-rooted): switches from
|
||||
* local slot_sz to c.tagscrsz for the same lockstep reason.
|
||||
*
|
||||
* Closes STATUS latent #1 (`@tagscr` shared 24B reservation across all
|
||||
* tagged scratch sites). Surfaced through getopttest's errortable
|
||||
* scenario, where `tryparse` returns a 24B `error` value.
|
||||
*
|
||||
* Rows (runtime semantic round-trip; no asm byte-id — wwstage local
|
||||
* layouts legitimately differ from cstage's and 995_self_rebuild
|
||||
* covers cross-stage drift):
|
||||
*
|
||||
* row | what it pins
|
||||
* ----------------------------+----------------------------------
|
||||
* ret_struct24_adjacent | (void | err24); local `e: err`
|
||||
* | sits right above @tagscr; pre-zero
|
||||
* | clobbered e+0..e+7 → kind/flag came
|
||||
* | back zero. Post-fix: full payload
|
||||
* | round-trips through the tagged
|
||||
* | return.
|
||||
* ret_struct24_via_match | scrutinee match arm reads kind +
|
||||
* | flag + a + b from the case binding;
|
||||
* | exercises both the cgreturn write
|
||||
* | and the match-arm spill read.
|
||||
* ret_mixed_sizes_one_fn | rob's lockstep pin — one fn body
|
||||
* | holds TWO tagged-scratch sites at
|
||||
* | different sizes: a 16B pushargsrev
|
||||
* | widen (call into `(void | small)`)
|
||||
* | and a 32B cgreturn widen (return
|
||||
* | `(void | err24)`). scanlocals must
|
||||
* | raise c.tagscrsz from 16→32 via
|
||||
* | tagscrbump, and both emit sites must
|
||||
* | dedupe to the same 32B slot. The
|
||||
* | smaller pushargsrev site is the
|
||||
* | FIRST '@' localadd (so it allocates
|
||||
* | the slot); the larger cgreturn site
|
||||
* | dedupes by name. If either side ever
|
||||
* | regressed to a per-site size, the
|
||||
* | 32B cgreturn zero loop would walk
|
||||
* | past the 16B slot and clobber e+0..
|
||||
* | e+7 — same shape as row 1's adjacency
|
||||
* | repro, but now staged through the
|
||||
* | shared-slot path.
|
||||
* ret_struct24_param_widen | call-site struct-payload widen into
|
||||
* | a (void | err24) param uses the
|
||||
* | same @tagscr; pushargsrev's slot
|
||||
* | must hold all 32B.
|
||||
*/
|
||||
#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[] = {
|
||||
/* 1. Direct repro of #38. Local `e: err` (24B) is declared before
|
||||
* the return; scanlocals lays it out adjacent to @tagscr. Pre-fix
|
||||
* (24B @tagscr) the outer zero loop wrote 32B and clobbered e+0.
|
||||
* Returns 11 + 22 + 33 + 44 = 110. */
|
||||
{ "ret_struct24_adjacent",
|
||||
"type err = struct { kind: i32, flag: i32, a: i64, b: i64 };\n"
|
||||
"fn outer() (void | err) = {\n"
|
||||
" let e: err;\n"
|
||||
" e.kind = 11; e.flag = 22; e.a = 33i64; e.b = 44i64;\n"
|
||||
" return e;\n"
|
||||
"};\n"
|
||||
"fn main() i32 = {\n"
|
||||
" let r: (void | err) = outer();\n"
|
||||
" match (r) {\n"
|
||||
" case void => return 99;\n"
|
||||
" case let v: err => return v.kind + v.flag + (v.a: i32) + (v.b: i32);\n"
|
||||
" };\n"
|
||||
" return 0;\n"
|
||||
"};\n",
|
||||
110 },
|
||||
|
||||
/* 2. Same shape but caller iterates fields through the match
|
||||
* binding (no early-return-from-arm shortcut). Cross-checks that
|
||||
* the match-arm spill reads each field at the expected offset
|
||||
* after the tagged return ABI lands AX=tag, DX=kind+flag,
|
||||
* CX=a, R8=b. Expected: 7 (kind=1) + 8 (flag=2) + 9 (a=3)
|
||||
* + 10 (b=4) = 34. */
|
||||
{ "ret_struct24_via_match",
|
||||
"type err = struct { kind: i32, flag: i32, a: i64, b: i64 };\n"
|
||||
"fn make() (void | err) = {\n"
|
||||
" let e: err;\n"
|
||||
" e.kind = 1; e.flag = 2; e.a = 3i64; e.b = 4i64;\n"
|
||||
" return e;\n"
|
||||
"};\n"
|
||||
"fn main() i32 = {\n"
|
||||
" let r: (void | err) = make();\n"
|
||||
" let acc: i32 = 0;\n"
|
||||
" match (r) {\n"
|
||||
" case void => return 99;\n"
|
||||
" case let v: err => {\n"
|
||||
" acc += v.kind + 6;\n"
|
||||
" acc += v.flag + 6;\n"
|
||||
" acc += (v.a: i32) + 6;\n"
|
||||
" acc += (v.b: i32) + 6;\n"
|
||||
" };\n"
|
||||
" };\n"
|
||||
" return acc;\n"
|
||||
"};\n",
|
||||
34 },
|
||||
|
||||
/* 3. Rob's lockstep pin: TWO tagged-scratch sites of different
|
||||
* sizes in ONE fn body. `mixed` has a pushargsrev widen (16B
|
||||
* slot, bare call into a `(void | small)` param) and a cgreturn
|
||||
* widen (32B slot, returning `e: err`).
|
||||
*
|
||||
* Source order is chosen so the local layout pre-fix lands the
|
||||
* cgreturn zero-loop overflow on the returned value's own bytes.
|
||||
* `consume_small` returns void and is called bare (no let-bind),
|
||||
* so @tagscr is the LAST localalloc and `e` sits immediately
|
||||
* above it:
|
||||
*
|
||||
* localalloc s → -8 (8B)
|
||||
* localalloc e → -32 (24B; e.kind/flag at -32, e.a/-24, e.b/-16)
|
||||
* localadd @tagscr → -56 (pushargsrev hits first; pre-fix 24B slot)
|
||||
* cgreturn @tagscr → dedupes to -56
|
||||
* cgreturn zero loop walks rsz=32 → writes -56,-48,-40,-32:
|
||||
* -32 lands on e.kind/e.flag → both zero before
|
||||
* cgwidentaggedstore reads e to fill scratch → AX/DX ABI
|
||||
* ships kind=0 instead of 100.
|
||||
*
|
||||
* Post-fix: scanlocals raises c.tagscrsz from 16 → 32 via
|
||||
* tagscrbump across the two sites; localadd allocates a 32B
|
||||
* slot (-64), zero loop stays in bounds, e survives, main
|
||||
* returns 100. Also pins the '@'-prefix dedup contract: both
|
||||
* sites must see the same slot, not fork into separate
|
||||
* allocations of their own per-site sizes. */
|
||||
{ "ret_mixed_sizes_one_fn",
|
||||
"type err = struct { kind: i32, flag: i32, a: i64, b: i64 };\n"
|
||||
"type small = struct { v: i32 };\n"
|
||||
"fn consume_small(r: (void | small)) void = {\n"
|
||||
" match (r) {\n"
|
||||
" case void => return;\n"
|
||||
" case let v: small => return;\n"
|
||||
" };\n"
|
||||
"};\n"
|
||||
"fn mixed() (void | err) = {\n"
|
||||
" let s: small;\n"
|
||||
" s.v = 7;\n"
|
||||
" let e: err;\n"
|
||||
" e.kind = 100; e.flag = 0; e.a = 0i64; e.b = 0i64;\n"
|
||||
" consume_small(s);\n"
|
||||
" return e;\n"
|
||||
"};\n"
|
||||
"fn main() i32 = {\n"
|
||||
" let r: (void | err) = mixed();\n"
|
||||
" match (r) {\n"
|
||||
" case void => return 91;\n"
|
||||
" case let v: err => return v.kind;\n"
|
||||
" };\n"
|
||||
" return 0;\n"
|
||||
"};\n",
|
||||
100 },
|
||||
|
||||
/* 4. Call-site struct-payload widen: pass an err24 value to a fn
|
||||
* whose param is (void | err24). pushargsrev materialises the
|
||||
* payload in @tagscr (slot 32B) and pushes. Sister of row 1 on
|
||||
* the call-arg side. Expected: 50 (kind) + 60 (flag) = 110. */
|
||||
{ "ret_struct24_param_widen",
|
||||
"type err = struct { kind: i32, flag: i32, a: i64, b: i64 };\n"
|
||||
"fn consume(r: (void | err)) i32 = {\n"
|
||||
" match (r) {\n"
|
||||
" case void => return 91;\n"
|
||||
" case let v: err => return v.kind + v.flag;\n"
|
||||
" };\n"
|
||||
" return 0;\n"
|
||||
"};\n"
|
||||
"fn main() i32 = {\n"
|
||||
" let e: err;\n"
|
||||
" e.kind = 50; e.flag = 60; e.a = 0i64; e.b = 0i64;\n"
|
||||
" return consume(e);\n"
|
||||
"};\n",
|
||||
110 },
|
||||
};
|
||||
|
||||
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/wctrs_%d_%d.ww", getpid(), i);
|
||||
snprintf(tmpdir, sizeof tmpdir, "/tmp/wctrs_%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[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 cdrv[640];
|
||||
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
|
||||
char wdrv[640];
|
||||
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, "tagged_return_scratch: 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,
|
||||
"tagged_return_scratch[%s] row[%s]: exit=%d want=%d\n",
|
||||
drivers[d].name, rows[i].label, got,
|
||||
rows[i].want);
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fail) {
|
||||
fprintf(stderr,
|
||||
"tagged_return_scratch: %d/%d row(s) failed\n", fail, total);
|
||||
return 1;
|
||||
}
|
||||
printf("tagged_return_scratch: %d/%d ok\n", total, total);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user