wwstage: register error-structs in collectstructs so large-union struct-variant returns widen (#9)
collectstructs registered a struct only when the typedecl body is N_TSTRUCT, so an error-struct (type X = !struct{...}, whose body is N_TBANG{N_TSTRUCT}) never entered wwstage's c.structs table. The name-keyed structlookup then missed at the return-widen sites, and wwstage dropped the struct construction when returning a struct variant of a large (>4-eightbyte) union -- wrong runtime value and cs!=ww. cstage has no struct name-table (pure tinfo) and was correct. Peel the N_TBANG body in collectstructs so error-structs register; both existing cstage-mirrored widen arms then fire. Provably byte-id-inert: no committed source defines a !struct today. Adds test/wcc/785 (struct-variant return + named-void control, both-stage byte-id + runtime). The >4-eightbyte 5th-word truncation on return remains, symmetric (cs==ww) and unread by the tag/early-word path; #222's sret hidden-pointer cutover is the committed fix (table-retirement tracked as the wwstage->tinfo SSoT arc). Aligns wwstage up to cstage (rule-10).
This commit is contained in:
269
test/wcc/785_structvariant_largeunion_return.c
Normal file
269
test/wcc/785_structvariant_largeunion_return.c
Normal file
@@ -0,0 +1,269 @@
|
||||
/*
|
||||
* 785_structvariant_largeunion_return — project #9 close. Pins that
|
||||
* RETURNING a STRUCT variant of a LARGE (>4-eightbyte) tagged union
|
||||
* constructs the struct and lays it into the union payload slot, on
|
||||
* BOTH stages byte-identically (rule-10).
|
||||
*
|
||||
* THE BUG (wwstage-only, cs!=ww): wwstage's return-widen DISPATCH
|
||||
* (cgreturn needswiden -> cgwidentaggedstore, selfhost/cmd/wcc/
|
||||
* cgenstmt.ww + cgenutil.ww) is name-keyed via structlookup. An
|
||||
* error-struct (`type big = !struct{...}`, the errors.opaque_ shape)
|
||||
* was NEVER registered in c.structs — collectstructs only registered
|
||||
* a bare N_TSTRUCT body, not the N_TBANG-wrapped one — so structlookup
|
||||
* missed it, needswiden stayed false, and the return collapsed to a
|
||||
* register move (frame $32) that DROPPED both the `x.f = &impl` field
|
||||
* store and the 32B struct -> 40B union-payload copy (wrong runtime
|
||||
* value AND 34-line asm divergence vs cstage's correct $80 frame).
|
||||
* cstage is tinfo-based (peel src->type -> TY_STRUCT, cgen.c:2058) and
|
||||
* needs no table, so it built correctly.
|
||||
*
|
||||
* THE FIX (A1, collectstructs N_TBANG peel): register error-structs
|
||||
* like any struct so the existing already-cstage-mirrored widen arms
|
||||
* fire. Zero byte-id delta on the prior corpus (no committed source
|
||||
* defined a `!struct` before errors.opaque_). The strategic fix is
|
||||
* #222's sret cutover (deletes this name-keyed dispatch); #10 tracks
|
||||
* retiring the c.structs table.
|
||||
*
|
||||
* GATE-BLIND class: byte-id alone is necessary-not-sufficient — a
|
||||
* dropped store keeps cs==ww on self-consistent garbage. So the
|
||||
* struct_variant row READS BACK BOTH the tag (match arm) AND the
|
||||
* widened field (calls through z.f), and asserts the runtime exit.
|
||||
*
|
||||
* row | shape | exit | byte-id
|
||||
* -----------------+----------------------------------------+------+--------
|
||||
* struct_variant | mkbig: x.f=&impl; return x (struct | 42 | cs==ww
|
||||
* | variant of 40B union); main matches | |
|
||||
* | big, calls (*z.f)(&1)=1+41 — proves | |
|
||||
* | the field survived the widen | |
|
||||
* namedvoid_ctrl | mksmall: return y (named-void variant | 22 | cs==ww
|
||||
* | of the same 40B union) — the path | |
|
||||
* | that was correct pre-fix; guards we | |
|
||||
* | did not regress it | |
|
||||
*
|
||||
* GATE POLARITY: must stay GREEN. Red means the struct-variant widen
|
||||
* dropped its construction again, or the named-void variant regressed.
|
||||
* GRADUATES nothing — #222's residual 5th-eightbyte truncation on the
|
||||
* 40B return is symmetric (cs==ww) and unread by this match path.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.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;
|
||||
}
|
||||
|
||||
#define STAGE_CS 1
|
||||
#define STAGE_WW 2
|
||||
|
||||
struct row {
|
||||
const char *label;
|
||||
const char *src;
|
||||
int want_exit;
|
||||
int stage_mask;
|
||||
int byte_id;
|
||||
};
|
||||
|
||||
static const struct row rows[] = {
|
||||
/* POSITIVE byte-id + runtime: struct variant of a 40B union is
|
||||
* constructed, the &fn field stored, returned, then matched and
|
||||
* called through — exit proves the widened field survived. */
|
||||
{ "struct_variant",
|
||||
"package main;\n"
|
||||
"type a = !void; type b = !void;\n"
|
||||
"type big = !struct { f: *fn(p: *i32) i32, data: [3]u64 };\n"
|
||||
"type u = !(a | b | big);\n"
|
||||
"fn impl(p: *i32) i32 = { return *p + 41i32; };\n"
|
||||
"fn mkbig() u = { let x: big; x.f = (&impl): *fn(p: *i32) i32; return x; };\n"
|
||||
"export fn main() i32 = {\n"
|
||||
" let r = mkbig();\n"
|
||||
" match (r) {\n"
|
||||
" case a => return 90;\n"
|
||||
" case b => return 91;\n"
|
||||
" case let z: big => { let n: i32 = 1i32; let fp = z.f; return (*fp)(&n): i32; };\n"
|
||||
" };\n"
|
||||
"};\n",
|
||||
42, STAGE_CS | STAGE_WW, 1 },
|
||||
|
||||
/* CONTROL byte-id + runtime: named-void variant of the SAME 40B
|
||||
* union — the path that was already correct pre-fix. */
|
||||
{ "namedvoid_ctrl",
|
||||
"package main;\n"
|
||||
"type a = !void; type b = !void;\n"
|
||||
"type big = !struct { f: *fn(p: *i32) i32, data: [3]u64 };\n"
|
||||
"type u = !(a | b | big);\n"
|
||||
"fn mksmall() u = { let y: b; return y; };\n"
|
||||
"export fn main() i32 = {\n"
|
||||
" let r = mksmall();\n"
|
||||
" match (r) {\n"
|
||||
" case a => return 90;\n"
|
||||
" case b => return 22;\n"
|
||||
" case big => return 9;\n"
|
||||
" };\n"
|
||||
"};\n",
|
||||
22, STAGE_CS | STAGE_WW, 1 },
|
||||
};
|
||||
|
||||
static void
|
||||
cleanup_tmp(const char *tmpdir, const char *base)
|
||||
{
|
||||
char p[1024];
|
||||
snprintf(p, sizeof p, "%s/%s", tmpdir, base); unlink(p);
|
||||
snprintf(p, sizeof p, "%s/%s.ww", tmpdir, base); unlink(p);
|
||||
snprintf(p, sizeof p, "%s/%s.s", tmpdir, base); unlink(p);
|
||||
snprintf(p, sizeof p, "%s/%s.o", tmpdir, base); unlink(p);
|
||||
snprintf(p, sizeof p, "%s/%s.combined.ww", tmpdir, base); unlink(p);
|
||||
rmdir(tmpdir);
|
||||
}
|
||||
|
||||
static int
|
||||
write_source(const char *path, const char *src)
|
||||
{
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) return -1;
|
||||
fputs(src, f);
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
build_via_driver(const char *driver, const char *tmpdir, const char *src)
|
||||
{
|
||||
char cmd[2048];
|
||||
snprintf(cmd, sizeof cmd, "cd %s && timeout 180 %s build %s 2>/dev/null",
|
||||
tmpdir, driver, src);
|
||||
return runwait(cmd);
|
||||
}
|
||||
|
||||
/* run_row — build via driver, run the binary, return exit (or -1 on
|
||||
* build failure). */
|
||||
static int
|
||||
run_row(const char *driver, const struct row *r, int seq)
|
||||
{
|
||||
char tmpdir[256], src[512], base[64], outbin[768];
|
||||
snprintf(tmpdir, sizeof tmpdir, "/tmp/svlu_%d_d_%d", getpid(), seq);
|
||||
snprintf(base, sizeof base, "main785");
|
||||
snprintf(src, sizeof src, "%s/%s.ww", tmpdir, base);
|
||||
mkdir(tmpdir, 0755);
|
||||
if (write_source(src, r->src) != 0) { cleanup_tmp(tmpdir, base); return -1; }
|
||||
int rc;
|
||||
if (build_via_driver(driver, tmpdir, src) == 0) {
|
||||
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
|
||||
rc = runwait(outbin);
|
||||
} else {
|
||||
rc = -1;
|
||||
}
|
||||
cleanup_tmp(tmpdir, base);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* asm_byte_identical — diff cstage vs wwstage .s. Parallel trees so
|
||||
* ww_ww writing intermediates next to the source doesn't clobber the
|
||||
* cstage .s (CLAUDE.md rule 14 phase split). */
|
||||
static int
|
||||
asm_byte_identical(const char *cdrv, const char *wdrv, const struct row *r,
|
||||
int seq)
|
||||
{
|
||||
char src[512], tdc[256], tdw[256], base[64], cs[512], ws[512];
|
||||
snprintf(tdc, sizeof tdc, "/tmp/svlu_%d_c_%d", getpid(), seq);
|
||||
snprintf(tdw, sizeof tdw, "/tmp/svlu_%d_w_%d", getpid(), seq);
|
||||
snprintf(base, sizeof base, "main785");
|
||||
mkdir(tdc, 0755);
|
||||
mkdir(tdw, 0755);
|
||||
snprintf(src, sizeof src, "%s/%s.ww", tdc, base);
|
||||
if (write_source(src, r->src) != 0) { cleanup_tmp(tdc, base); cleanup_tmp(tdw, base); return -1; }
|
||||
int rc = -1;
|
||||
if (build_via_driver(cdrv, tdc, src) != 0) goto out;
|
||||
snprintf(cs, sizeof cs, "%s/%s.s", tdc, base);
|
||||
|
||||
snprintf(src, sizeof src, "%s/%s.ww", tdw, base);
|
||||
if (write_source(src, r->src) != 0) goto out;
|
||||
if (build_via_driver(wdrv, tdw, src) != 0) goto out;
|
||||
snprintf(ws, sizeof ws, "%s/%s.s", tdw, base);
|
||||
|
||||
FILE *fc = fopen(cs, "rb");
|
||||
FILE *fw = fopen(ws, "rb");
|
||||
if (fc && fw) {
|
||||
rc = 0;
|
||||
for (;;) {
|
||||
int a = fgetc(fc);
|
||||
int b = fgetc(fw);
|
||||
if (a != b) { rc = -1; break; }
|
||||
if (a == EOF) break;
|
||||
}
|
||||
}
|
||||
if (fc) fclose(fc);
|
||||
if (fw) fclose(fw);
|
||||
out:
|
||||
cleanup_tmp(tdc, base);
|
||||
cleanup_tmp(tdw, base);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int
|
||||
main(void)
|
||||
{
|
||||
const char *bin = getenv("BIN");
|
||||
if (!bin) bin = "out/bin";
|
||||
char cwd[256];
|
||||
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
|
||||
char absbin[512];
|
||||
if (bin[0] != '/') {
|
||||
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
|
||||
bin = absbin;
|
||||
}
|
||||
|
||||
char cdrv[640], wdrv[640];
|
||||
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
|
||||
snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin);
|
||||
|
||||
int n = (int)(sizeof rows / sizeof rows[0]);
|
||||
int total = 0, fail = 0, seq = 0;
|
||||
int wwpresent = (access(wdrv, X_OK) == 0);
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
const struct row *r = &rows[i];
|
||||
|
||||
if (r->stage_mask & STAGE_CS) {
|
||||
total++;
|
||||
int got = run_row(cdrv, r, seq++);
|
||||
if (got != r->want_exit) {
|
||||
fprintf(stderr, "structvariant[cs][%s]: exit=%d want=%d\n",
|
||||
r->label, got, r->want_exit);
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
|
||||
if (wwpresent && (r->stage_mask & STAGE_WW)) {
|
||||
total++;
|
||||
int got = run_row(wdrv, r, seq++);
|
||||
if (got != r->want_exit) {
|
||||
fprintf(stderr, "structvariant[ww][%s]: exit=%d want=%d\n",
|
||||
r->label, got, r->want_exit);
|
||||
fail++;
|
||||
}
|
||||
if (r->byte_id) {
|
||||
total++;
|
||||
if (asm_byte_identical(cdrv, wdrv, r, seq++) != 0) {
|
||||
fprintf(stderr, "structvariant[byte-id][%s]: cstage vs wwstage asm differs\n",
|
||||
r->label);
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fail) {
|
||||
fprintf(stderr, "structvariant: %d/%d checks failed\n", fail, total);
|
||||
return 1;
|
||||
}
|
||||
printf("structvariant: %d/%d ok\n", total, total);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user