cgen: type-key wwstage struct-layout at 3 sites via stamped tinfo (#21)

wwstage cgen resolved struct LAYOUT by bare-leaf name (structlookup /
structparamsize) at three caller-side sites — the by-value arg push
(cgenutil), the let-receive copy width (cgenstmt), and the field-read
offset (cgenexpr). Under a cross-module same-leaf collision (two modules
each exporting a `pair`, 16B vs 24B) the name lookup first-matches the
WRONG type, so the push dropped the 2nd eightbyte, the receive over-copied,
and the field read the wrong offset. cstage type-keys off the stamped
tinfo and is correct; this aligns wwstage UP to it (ww-only change).

Route all three sites through the stamped node.type_ via a new
structabisizetn(*tinfo) accessor (push + receive) and tichase(type_).fields
(field-read, structlookupchain removed). One commit (rule-11 carve-out):
the collision drives all three at once and no per-site fixture isolates, so
it cannot bisect-split. A scoped slice of the #209/#211 name-keyed-cgen
cluster retirement; the cgdot *struct-ptr/global and let-copy siblings stay
name-keyed and are filed (#31).

New table-driven test 793_xmod_struct_argpush_collide_run (4 scenarios:
push/recv/field over 16B and 12B tails) reddens under a revert of the three
cgen files. Full make test green (336 passed); make sizelint clean.
This commit is contained in:
2026-06-29 11:45:08 +09:00
parent 7b9488706b
commit 5ae6e3419e
5 changed files with 435 additions and 69 deletions

View File

@@ -430,6 +430,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
$(BIN)/test_amp_fn_assign_run \ $(BIN)/test_amp_fn_assign_run \
$(BIN)/test_type_value_shadow_run \ $(BIN)/test_type_value_shadow_run \
$(BIN)/test_xmod_alias_struct_collide_run \ $(BIN)/test_xmod_alias_struct_collide_run \
$(BIN)/test_xmod_struct_argpush_collide_run \
$(BIN)/test_xmod_variant_match \ $(BIN)/test_xmod_variant_match \
$(BIN)/test_xmod_qualstructlit_run \ $(BIN)/test_xmod_qualstructlit_run \
$(BIN)/test_spread_variant_match \ $(BIN)/test_spread_variant_match \
@@ -1521,6 +1522,16 @@ $(BIN)/test_xmod_alias_struct_collide_run: test/wcc/784_xmod_alias_struct_collid
$(LIB)/libwwrt.a | $(BIN) $(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $< $(CC) $(CFLAGS) -o $@ $<
# 793 #21/#224: inferred-let struct LOCAL whose leaf collides cross-module
# miscompiled the by-value arg push + recv + direct field read (all
# name-keyed); fix routes them off the stamped tinfo. Own 2-module fixtures
# in a private mktemp dir — not a selfhost-driver test.
$(BIN)/test_xmod_struct_argpush_collide_run: test/wcc/793_xmod_struct_argpush_collide_run.c \
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \
$(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
# #13: cross-module decomposition of an imported union's variants # #13: cross-module decomposition of an imported union's variants
# (`case pkg.a`). Pre-fix the wwstage checker rejected it; cstage built # (`case pkg.a`). Pre-fix the wwstage checker rejected it; cstage built
# it. cstage driver build + run pins routing; ww-sep build (w6c_ww must # it. cstage driver build + run pins routing; ww-sep build (w6c_ww must

View File

@@ -3512,9 +3512,13 @@ fn cgdot(c: *cgen, n: *syntax.node) void = {
// same-module struct (a genuine struct-value receiver); a // same-module struct (a genuine struct-value receiver); a
// same-module alias keeps peeling; a foreign leaf (in // same-module alias keeps peeling; a foreign leaf (in
// neither registry for c.curmod) falls back to the prior // neither registry for c.curmod) falls back to the prior
// any-module heuristic. The broader cross-module same-leaf // any-module heuristic. #21/#224: the direct-struct arm
// STRUCT name-keying at the direct-struct arm below is // below no longer name-keys — it resolves field offsets off
// filed separately as #224 (not FLIP-triggered). // the stamped struct tinfo (dotlhs.type_), closing the
// cross-module same-leaf STRUCT mis-read there. This peel +
// same-module break still scopes the *struct (N_TPTR) and
// array arms, which remain structlookupchain-keyed (out of
// the #21 scoped slice).
for (tn != nil && tn.kind == syntax.nkind.N_TNAME) { for (tn != nil && tn.kind == syntax.nkind.N_TNAME) {
if (structsamemod(c, tn.str) != nil) { break; }; if (structsamemod(c, tn.str) != nil) { break; };
let nx: *syntax.node = aliassamemod(c, tn.str); let nx: *syntax.node = aliassamemod(c, tn.str);
@@ -3567,69 +3571,77 @@ fn cgdot(c: *cgen, n: *syntax.node) void = {
}; };
// Direct struct local: field load at off+foff. // Direct struct local: field load at off+foff.
if (lkind == syntax.nkind.N_TNAME) { if (lkind == syntax.nkind.N_TNAME) {
// structlookupchain walks the alias chain on // #21/#224: resolve the field OFFSET + field type off
// miss so a transitively-aliased struct (`type // the checker-STAMPED struct tinfo
// b = a; a = struct`) still resolves to the // (tichase(dotlhs.type_).fields), NOT the name-keyed
// underlying fieldinfo (#22). // structlookupchain(tn). On a cross-module same-leaf
let si: *structinfo = structlookupchain(c, tn); // collision lc.tnode is a bare leaf that structlookup
if (si != nil) { // mis-resolves to a FOREIGN same-leaf struct → fields
let fi: *fieldinfo = si.fields; // read at the WRONG offsets / wrong load-op (the #224
for (fi != nil) { // direct-struct arm flagged at the peel-loop comment
let fn_: str = fi.fname; // above). The stamped tinfo carries the right layout
if (syntax.streq(fn_, fld)) { // regardless of leaf collision; mirrors cstage's
// tagged-union field: emit the AX=tag, // `t->fields` walk (cgen.c N_DOT, type-keyed) — align
// DX=word0, CX=word1[, R8=word2] load // ww UP. tfield {name, type_, offset} is the tinfo twin
// sequence so the match / let-init / // of fieldinfo {fname, tnode, foff}; the dispatch keys
// call-arg consumers see the same shape // off syntax.typeis* on the field tinfo, byte-id with
// as a tagged-returning fn. Pre-#28 fell // the prior is*type(fi.tnode)=typeis*(fi.tnode.type_).
// through to the scalar fieldloadop and let sbu: *syntax.tinfo = nil;
// only AX (tag) was loaded — payload if (dotlhs != nil) { sbu = tichase(dotlhs.type_: *syntax.tinfo); };
// words came from whatever the caller if (sbu != nil) { if (sbu.kind == syntax.tykind.TY_STRUCT) {
// left in DX/CX/R8. let tf: *syntax.tfield = sbu.fields;
if (istaggedtype(c, fi.tnode)) { for (tf != nil) {
let tsz: i32 = slotsize(c, fi.tnode); if (syntax.streq(tf.name, fld)) {
let foff: i32 = tf.offset: i32;
let ftraw: *syntax.tinfo = tf.type_;
// tagged-union field: AX=tag, DX=word0,
// CX=word1[, R8=word2]; slot = ti.size
// (slotsize's TAGGED arm, cgenutil.ww:2680).
if (syntax.typeistagged(ftraw)) {
let ftc: *syntax.tinfo = tichase(ftraw);
let tsz: i32 = 0;
if (ftc != nil) { tsz = ftc.size: i32; };
cgloadtaggedfield(c, "BP", cgloadtaggedfield(c, "BP",
lc.off + fi.foff, tsz, true); lc.off + foff, tsz, true);
return; return;
}; };
// str IS []u8 — same 3-word {ptr,len,cap} // str IS []u8 — 3-word {ptr,len,cap} into
// as a slice field: load (ptr, len, cap) // (AX,BX,CX). Base is BP so order is harmless.
// into (AX, BX, CX). Base is BP so no if (syntax.typeisstr(ftraw) || syntax.typeisslice(ftraw)) {
// aliasing — order doesn't matter. str
// folds onto the slice arm (#1/Phase 3
// collapse; cite cstage cgen.c N_DOT S1).
if (isstrtype(c, fi.tnode) || isslicetype(c, fi.tnode)) {
emitline("\tMOVQ\t"); emitline("\tMOVQ\t");
emitoff((lc.off + fi.foff): i64); emitoff((lc.off + foff): i64);
emitline("(BP), AX\n"); emitline("(BP), AX\n");
emitline("\tMOVQ\t"); emitline("\tMOVQ\t");
emitoff((lc.off + fi.foff + 8): i64); emitoff((lc.off + foff + 8): i64);
emitline("(BP), BX\n"); emitline("(BP), BX\n");
emitline("\tMOVQ\t"); emitline("\tMOVQ\t");
emitoff((lc.off + fi.foff + 16): i64); emitoff((lc.off + foff + 16): i64);
emitline("(BP), CX\n"); emitline("(BP), CX\n");
} else { if (isfloattype(c, fi.tnode)) { } else { if (syntax.typeisfloat(ftraw)) {
// f64/f32 field: route through X0. // f64/f32 field: route through X0.
let mov: str = "MOVSD"; let mov: str = "MOVSD";
if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; if (syntax.typeisf32(ftraw)) { mov = "MOVSS"; };
emitline("\t"); emitline("\t");
emitline(mov); emitline(mov);
emitline("\t"); emitline("\t");
emitoff((lc.off + fi.foff): i64); emitoff((lc.off + foff): i64);
emitline("(BP), X0\n"); emitline("(BP), X0\n");
} else { } else {
let op: str = fieldloadop(c, fi); let ftc: *syntax.tinfo = tichase(ftraw);
let fsz: i32 = 0;
if (ftc != nil) { fsz = ftc.size: i32; };
let op: str = loadopsz(syntax.typeissigned(ftraw), fsz);
emitline("\t"); emitline("\t");
emitline(op); emitline(op);
emitline("\t"); emitline("\t");
emitoff((lc.off + fi.foff): i64); emitoff((lc.off + foff): i64);
emitline("(BP), AX\n"); emitline("(BP), AX\n");
}; }; }; };
return; return;
}; };
fi = fi.finext; tf = tf.tnext;
}; };
}; }; };
}; };
// Array pseudo-fields: `.ptr` is the array's // Array pseudo-fields: `.ptr` is the array's
// address (LEAQ); `.len` is the static element // address (LEAQ); `.len` is the static element

View File

@@ -2583,29 +2583,38 @@ fn cgletbody(c: *cgen, n: *syntax.node, off: i32) void = {
}; };
}; };
if (rhs.kind == syntax.nkind.N_CALL) { if (rhs.kind == syntax.nkind.N_CALL) {
let sname: str;
sname.ptr = nil; sname.len = 0;
if (tn != nil) { if (tn != nil) {
if (tn.kind == syntax.nkind.N_TNAME) { if (tn.kind == syntax.nkind.N_TNAME) {
sname = tn.str;
};
};
if (sname.len > 0) {
let lsi: *structinfo = structlookup(c, sname);
if (lsi != nil) {
// ≤24B register RECV: the value arrives packed // ≤24B register RECV: the value arrives packed
// in AX/DX/CX, so size by the maxalign-rounded // in AX/DX/CX, so size by the maxalign-rounded
// ABI size (cstage lu->size), not the natural // ABI size (cstage lu->size), not the natural
// extent — see structabisize (#169). // extent — see structabisize (#169).
let lsz: i32 = structabisize(lsi); //
let tlm: i32 = lsz - (lsz / 8) * 8; // #21/#224: size off the checker-STAMPED tinfo
if (lsz <= 24) { // (structabisizetn = tichase(tn.type_).size, the
if (tlm == 0 || tlm == 1 // maxalign-rounded ABI size = check.ww:2467),
|| tlm == 2 || tlm == 4) { // NOT structlookup(tn.str). On a cross-module
cgexpr(c, rhs); // same-leaf collision the inferred-let's tn.str
cgaggregstore(c, "BP", off, lsz, true); // is a bare leaf that structlookup mis-resolves to
c.lastwasreturn = 0; // a FOREIGN same-leaf struct → the recv copied that
return; // struct's word count (a 24B foreign over-copies a
// 16B local, spilling into a neighbour slot). The
// stamped tinfo carries the right size regardless of
// collision; byte-id with structabisize on a
// resolving lookup. Mirrors the sibling array arm
// below (already tn.type_-keyed). cstage is
// type-keyed (lu->size) — align ww UP.
let lsz: i32 = structabisizetn(tn.type_: *syntax.tinfo);
if (lsz > 0) {
let tlm: i32 = lsz - (lsz / 8) * 8;
if (lsz <= 24) {
if (tlm == 0 || tlm == 1
|| tlm == 2 || tlm == 4) {
cgexpr(c, rhs);
cgaggregstore(c, "BP", off, lsz, true);
c.lastwasreturn = 0;
return;
};
}; };
}; };
}; };

View File

@@ -844,8 +844,18 @@ fn pushargsrev(c: *cgen, arg: *syntax.node, param: *syntax.node, memphase: bool,
// `cgexpr(c, arg)` + scalar PUSHQ AX — only the first // `cgexpr(c, arg)` + scalar PUSHQ AX — only the first
// 8B word made it across, and the callee's second-arg // 8B word made it across, and the callee's second-arg
// slots picked up the wrong neighbour's value. // slots picked up the wrong neighbour's value.
let stsz: i32 = structparamsize(c, lc.tnode); // #21/#224: COUNT the push off the checker-STAMPED tinfo
if (stsz > 0) { // (structabisizetn), not the name-keyed structparamsize(lc.tnode).
// On a cross-module same-leaf collision the inferred-let's tnode
// is a bare leaf that structlookup mis-resolves (returns 0 or a
// foreign struct >16B → 0), so the fast path was skipped and the
// arg dropped to the scalar single-PUSHQ default — word1 lost.
// cstage counts via struct_arg_size(args[i]->type) (TYPE-keyed),
// never colliding; align ww UP. The <=16B gate keeps >16B structs
// + arrays on the #271 aggregate arm below (the fast path emits
// at most 2 words).
let stsz: i32 = structabisizetn(arg.type_: *syntax.tinfo);
if (stsz > 0 && stsz <= 16) {
if (stsz > 8) { if (stsz > 8) {
emitline("\tMOVQ\t"); emitline("\tMOVQ\t");
emitoff((off + 8): i64); emitoff((off + 8): i64);
@@ -951,14 +961,15 @@ fn pushargsrev(c: *cgen, arg: *syntax.node, param: *syntax.node, memphase: bool,
// aggregate) and stack-imbalanced against the type-based drain. // aggregate) and stack-imbalanced against the type-based drain.
let aggsz: i32 = aggargsizetn(arg.type_: *syntax.tinfo); let aggsz: i32 = aggargsizetn(arg.type_: *syntax.tinfo);
if (aggsz > 0) { if (aggsz > 0) {
// Exclude a ≤16B-struct IDENT — it owns the structparamsize // Exclude a ≤16B-struct IDENT — it owns the structabisizetn
// fast path above (or, when a cross-module same-leaf collision // fast path above. That path is now TYPE-keyed (#21/#224): a
// makes the name-keyed structparamsize miss it, the scalar // cross-module same-leaf collision no longer misses (the prior
// default below, byte-id with cstage's 1-word struct push; // name-keyed structparamsize returned 0 → the arg dropped to the
// #784/#223). The exclusion is TYPE-keyed via the stamped // scalar default → word1 lost; #784/#223). The exclusion is
// tinfo, mirroring cstage node_isstructarg (struct_arg_size on // TYPE-keyed via the stamped tinfo, mirroring cstage
// args[i]->type) — a name-keyed gate here re-opens the #211/#13 // node_isstructarg (struct_arg_size on args[i]->type) — a
// name-keyed divergence the cstage type gate doesn't have. // name-keyed gate here re-opens the #211/#13 name-keyed
// divergence the cstage type gate doesn't have.
let structident: bool = false; let structident: bool = false;
if (arg.kind == syntax.nkind.N_IDENT) { if (arg.kind == syntax.nkind.N_IDENT) {
let st: *syntax.tinfo = arg.type_: *syntax.tinfo; let st: *syntax.tinfo = arg.type_: *syntax.tinfo;
@@ -2046,6 +2057,28 @@ fn structabisize(si: *structinfo) i32 = {
return (n + maxaln - 1) & ~(maxaln - 1); return (n + maxaln - 1) & ~(maxaln - 1);
}; };
// structabisizetn — the maxalign-rounded ABI size of a by-value STRUCT,
// read off the checker-STAMPED tinfo (.size), else 0. The tinfo twin of
// structabisize(*structinfo): check.ww:2467 computes the struct's
// `r.size = (off+maxalign-1)&~(maxalign-1)` — identical to structabisize's
// formula — so `tichase(t).size` IS the ABI size, byte-id with the
// name-keyed structabisize on a resolving lookup. The #21/#224 choke-point
// for the caller-side register-ABI sites (struct-arg push, inferred-let
// struct call-result recv) that previously keyed the COUNT through a
// name-keyed structlookup/structparamsize: a cross-module same-leaf
// collision makes that lookup return 0 (or a foreign struct's size), so the
// push under-counted and the recv over-copied. The stamped tinfo carries
// the right size regardless of leaf collision (the documented #211/#13/#784
// name-keyed cluster; align wwstage UP to cstage's type-keyed struct_arg_size
// / lu->size). STRUCT-only — arrays own their own #271/#267 arms.
fn structabisizetn(t: *syntax.tinfo) i32 = {
if (t == nil) { return 0; };
let u: *syntax.tinfo = tichase(t);
if (u == nil) { return 0; };
if (u.kind == syntax.tykind.TY_STRUCT) { return u.size: i32; };
return 0;
};
// sretretsize — if `t` ultimately denotes a plain TY_STRUCT > 24B, // sretretsize — if `t` ultimately denotes a plain TY_STRUCT > 24B,
// return its natural size; else 0. Tagged unions, tuples, str, // return its natural size; else 0. Tagged unions, tuples, str,
// slices, scalars route through their existing register-return ABIs // slices, scalars route through their existing register-return ABIs

View File

@@ -0,0 +1,301 @@
/*
* 793_xmod_struct_argpush_collide_run — project #21/#224 runtime + byte-id
* net. An inferred-let struct LOCAL whose type leaf collides with a
* DIFFERENT module's same-leaf struct miscompiled in wwstage at THREE
* caller-side register-ABI sites, all keyed off the name-resolved
* (collision-prone) struct instead of the checker-STAMPED tinfo:
*
* - push (cgenutil.ww pushargsrev): the by-value struct call-arg
* COUNT came from name-keyed structparamsize(lc.tnode). On the
* collision lc.tnode is a bare leaf structlookup mis-resolves to the
* foreign same-leaf struct (>16B → 0), so the fast path was skipped
* and the arg dropped to a scalar single-PUSHQ — the 2nd eightbyte
* (the sub-8 tail) was lost. [the original #21 site]
* - recv (cgenstmt.ww cgletbody): `let s = m1.mk()` sized the
* register receive via structlookup(tn.str) → the foreign struct's
* word count, OVER-copying a 24B foreign over a 16B local (spilling
* CX into a neighbour slot).
* - field (cgenexpr.ww cgdot direct-struct arm): `s.<field>` resolved
* the offset via structlookupchain(tn) → the foreign struct's layout,
* reading the field at the WRONG offset + wrong load-op. [#224, filed
* not-fixed by 784/#223]
*
* cstage is correct at all three — it keys on args[i]->type / lu->size /
* t->fields (the checker-STAMPED type), never a name re-lookup. Fix
* (wwstage-only align-UP, #21/#224, ONE commit): route all three off the
* stamped tinfo — structabisizetn(arg/tn.type_) for the push+recv COUNT,
* tichase(dotlhs.type_).fields (tfield) for the field offset+type.
*
* GATE-BLIND in the bootstrap (same class as 784/#223): the trigger needs
* a SECOND module contributing a same-leaf struct of a DIFFERENT size, so
* the name-keyed lookup mis-resolves. A single-module synthetic resolves
* correctly (structlookup's same-module pass) and stays byte-id — it does
* NOT redden. Hence this 2-module probe.
*
* row | m1.pair (local) | m2.pair (foreign) | site(s) | exit
* -----------------+----------------------+-------------------+---------------+-----
* recvpush_t16 | {u64,u16} 16B | 24B | recv + push | 107
* fieldread_t16 | {u64,u16} 16B | 24B | recv + field | 107
* combined_t16 | {u64,u16} 16B | 24B | recv+field+push| 114
* recvpush_t12 | {u32,u32,u32} 12B | 24B | recv + push | 6
*
* cstage `ww build --sep` + run pins runtime; the wwstage binary is run
* too (the bug WAS a wrong wwstage runtime value); raw cs.s vs ww.s cmp
* over the driver-produced per-package asm pins rule-10 byte-id (the
* discriminating net — pre-fix __root.s diverges at the push/recv/field).
*/
#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;
}
static int
slurp_eq(const char *a, const char *b)
{
FILE *fa = fopen(a, "rb");
FILE *fb = fopen(b, "rb");
if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return -1; }
int rc = 0;
for (;;) {
int ca = fgetc(fa);
int cb = fgetc(fb);
if (ca != cb) { rc = -1; break; }
if (ca == EOF) break;
}
fclose(fa); fclose(fb);
return rc;
}
struct file { const char *name; const char *src; };
struct scenario {
const char *label;
const struct file *files; /* name==NULL terminates */
int want_exit;
};
/* m2.pair is a 24B struct sharing the leaf `pair` with m1.pair — the
* name-keyed lookup's any-module fallback mis-resolves m1.pair to it. */
static const char m2_24[] =
"package m2;\n"
"export type pair = struct { hi: u64, mid: u64, lo: u64 };\n"
"export fn use2(p: pair) i32 = { return (p.hi + p.mid + p.lo): i32; };\n";
/* ---- recvpush_t16: inferred-let recv + by-value push (the #21 site) -- */
static const struct file recvpush_t16_files[] = {
{ "m1.ww",
"package m1;\n"
"export type pair = struct { hi: u64, lo: u16 };\n"
"export fn mk() pair = { return pair { hi = 100: u64, lo = 7: u16 }; };\n"
"export fn consume(p: pair) i32 = { return (p.hi + (p.lo: u64)): i32; };\n" },
{ "m2.ww", m2_24 },
{ "main.ww",
"package main;\n"
"import m1;\n"
"import m2;\n"
"fn main() i32 = {\n"
" let dummy: m2.pair;\n"
" dummy.hi = 0: u64;\n"
" let s = m1.mk();\n"
" return m1.consume(s);\n"
"};\n" },
{ NULL, NULL }
};
/* ---- fieldread_t16: inferred-let recv + direct field read (#224) ----- */
static const struct file fieldread_t16_files[] = {
{ "m1.ww",
"package m1;\n"
"export type pair = struct { hi: u64, lo: u16 };\n"
"export fn mk() pair = { return pair { hi = 100: u64, lo = 7: u16 }; };\n" },
{ "m2.ww", m2_24 },
{ "main.ww",
"package main;\n"
"import m1;\n"
"import m2;\n"
"fn main() i32 = {\n"
" let dummy: m2.pair;\n"
" dummy.hi = 0: u64;\n"
" let s = m1.mk();\n"
" return (s.hi + (s.lo: u64)): i32;\n"
"};\n" },
{ NULL, NULL }
};
/* ---- combined_t16: recv + field + push in one fn -------------------- */
static const struct file combined_t16_files[] = {
{ "m1.ww",
"package m1;\n"
"export type pair = struct { hi: u64, lo: u16 };\n"
"export fn mk() pair = { return pair { hi = 100: u64, lo = 7: u16 }; };\n"
"export fn consume(p: pair) i32 = { return (p.hi + (p.lo: u64)): i32; };\n" },
{ "m2.ww", m2_24 },
{ "main.ww",
"package main;\n"
"import m1;\n"
"import m2;\n"
"fn main() i32 = {\n"
" let dummy: m2.pair;\n"
" dummy.hi = 0: u64;\n"
" let s = m1.mk();\n"
" let x: i32 = (s.lo: i32);\n"
" return m1.consume(s) + x;\n"
"};\n" },
{ NULL, NULL }
};
/* ---- recvpush_t12: a maxalign-4 12B sub-8-tail tail shape ----------- */
static const struct file recvpush_t12_files[] = {
{ "m1.ww",
"package m1;\n"
"export type pair = struct { a: u32, b: u32, c: u32 };\n"
"export fn mk() pair = { return pair { a = 1: u32, b = 2: u32, c = 3: u32 }; };\n"
"export fn consume(p: pair) i32 = { return (p.a + p.b + p.c): i32; };\n" },
{ "m2.ww", m2_24 },
{ "main.ww",
"package main;\n"
"import m1;\n"
"import m2;\n"
"fn main() i32 = {\n"
" let dummy: m2.pair;\n"
" dummy.hi = 0: u64;\n"
" let s = m1.mk();\n"
" return m1.consume(s);\n"
"};\n" },
{ NULL, NULL }
};
static const struct scenario scenarios[] = {
{ "recvpush_t16", recvpush_t16_files, 107 },
{ "fieldread_t16", fieldread_t16_files, 107 },
{ "combined_t16", combined_t16_files, 114 },
{ "recvpush_t12", recvpush_t12_files, 6 },
};
static int
run_scenario(const char *cdrv, const char *wdrv, const struct scenario *sc)
{
/* Private fixture dir — the driver's srcd-first import search can't
* pick up a polluting same-name file (mirrors 784/#215). */
char dir[] = "/tmp/ww793_XXXXXX";
if (mkdtemp(dir) == NULL) {
fprintf(stderr, "793[%s]: mkdtemp failed\n", sc->label);
return -1;
}
char path[1024], cmd[4096];
int rc = 0;
for (int i = 0; sc->files[i].name; i++) {
snprintf(path, sizeof path, "%s/%s", dir, sc->files[i].name);
FILE *f = fopen(path, "wb");
if (!f) { fprintf(stderr, "793[%s]: write %s\n", sc->label,
sc->files[i].name); rc = -1; goto done; }
fputs(sc->files[i].src, f);
fclose(f);
}
/* cstage --sep build + run pins runtime; pin WW_PKGCACHE under the
* scratch dir so out/.pkgcache is untouched. */
snprintf(cmd, sizeof cmd,
"cd %s && WW_PKGCACHE=%s/pkgc_c %s build --sep -I %s -o %s/main %s/main.ww",
dir, dir, cdrv, dir, dir, dir);
if (runwait(cmd) != 0) {
fprintf(stderr, "793[%s]: cstage build failed\n", sc->label);
rc = -1; goto done;
}
snprintf(path, sizeof path, "%s/main", dir);
int gotc = runwait(path);
if (gotc != sc->want_exit) {
fprintf(stderr, "793[%s]: cstage exit %d, want %d\n",
sc->label, gotc, sc->want_exit);
rc = -1;
}
/* wwstage --sep build + run: the bug WAS a wrong wwstage runtime
* value (a dropped/over-copied field), so run the wwstage binary too. */
snprintf(cmd, sizeof cmd,
"cd %s && WW_PKGCACHE=%s/pkgc_w %s build --sep -I %s -o %s/mainww %s/main.ww",
dir, dir, wdrv, dir, dir, dir);
if (runwait(cmd) != 0) {
fprintf(stderr, "793[%s]: ww_ww build failed\n", sc->label);
rc = -1; goto done;
}
snprintf(path, sizeof path, "%s/mainww", dir);
int gotw = runwait(path);
if (gotw != sc->want_exit) {
fprintf(stderr, "793[%s]: wwstage exit %d, want %d\n",
sc->label, gotw, sc->want_exit);
rc = -1;
}
/* Byte-id net (the discriminator): per-package asm under
* <stem>.sepwork/<pkg>.s; concat (sorted glob, identical set both
* stages) for the compare. Pre-fix __root.s diverges. */
char cs_s[1024], ws_s[1024];
snprintf(cs_s, sizeof cs_s, "%s/all_cs.s", dir);
snprintf(ws_s, sizeof ws_s, "%s/all_ww.s", dir);
snprintf(cmd, sizeof cmd, "cat %s/main.sepwork/*.s > %s 2>/dev/null",
dir, cs_s); if (system(cmd)) {}
snprintf(cmd, sizeof cmd, "cat %s/mainww.sepwork/*.s > %s 2>/dev/null",
dir, ws_s); if (system(cmd)) {}
if (slurp_eq(cs_s, ws_s) != 0) {
fprintf(stderr, "793[%s]: cs.s/ww.s DIFFER (rule-10 byte-id "
"violation — #21/#224 regression)\n", sc->label);
rc = -1;
}
done:
snprintf(cmd, sizeof cmd, "rm -rf %s", dir);
(void)runwait(cmd);
return rc;
}
int
main(void)
{
const char *bin = getenv("BIN");
if (!bin) bin = "out/bin";
char absbin[2048];
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[2100], wdrv[2100];
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin);
if (access(wdrv, X_OK) != 0) {
fprintf(stderr, "793: ww_ww missing — cannot run the cs==ww "
"byte-id gate (the whole point of this test)\n");
return 1;
}
int n = (int)(sizeof scenarios / sizeof scenarios[0]);
int fail = 0;
for (int i = 0; i < n; i++) {
if (run_scenario(cdrv, wdrv, &scenarios[i]) != 0)
fail++;
}
if (fail) {
fprintf(stderr, "793 xmod_struct_argpush_collide: %d/%d "
"scenarios failed\n", fail, n);
return 1;
}
printf("xmod_struct_argpush_collide: %d/%d ok (cstage+wwstage run + "
"cs==ww byte-id)\n", n, n);
return 0;
}