w6c+wwstage: route sret dest to the global symbol on struct-return into a global (#220)
Assigning a >24B by-value struct-return into a GLOBAL lvalue dropped the struct body: the sret dest was routed to a BP scratch temp and only the 8-byte return pointer was stored (`MOVQ AX, g(SB)`); the callee wrote the full struct to the scratch, which never reached the global. A BP-relative dest offset cannot name a global symbol. Pre-existing GATE-BLIND silent miscompile — both stages emit the same broken store, so byte-id (990-997) stays green while runtime is wrong — latent until the eFinal io surface put a global `cgoutstream: memio.stream` (>24B) on the path, where it made cgen.ww's self-built w6c_ww buffer every function body into a corrupt global (pos stayed 0) and emit prologue-only output. Fix, both stages, byte-identical: route the sret dest pointer to the global symbol so the callee writes the full struct through RDI straight into the global. cstage adds cg_sret_dest_sym, mirroring the existing str/slice global arm (skip the @sretscr scratch, emit `LEAQ masym(sym), DI`). wwstage carries the lhs IDENT node (sretdestnode) and emits `LEAQ name(SB), DI` via emitsymname — identical to cstage's symbol mangling, verified cs.s==ww.s on the probe and across 990-997. #211-family (by-value struct + global/pointer), but a distinct site: the cstage assignment-store into a global, not the wwstage call-return. N_LET-global static-init (`let g: T = mk()` at top level) is a separate, independently-broken path (#221) — link-fails for init-via-call, returns 0 for constant init — not the sret-receive gap and not on the eFinal path; deferred. test/wcc/940_global_sret_run: global assign (plus a branched callee to defeat const-fold), through-pointer mutation (the io vtable-callback shape that surfaced this), and local-init/assign regressions — runtime asserts on both stages (the net, since byte-id is gate-blind here) plus cs.s==ww.s. Discrimination confirmed by revert+rebuild: with the global arm disabled, global_assign emits the truncated store and exits 1.
This commit is contained in:
7
Makefile
7
Makefile
@@ -252,6 +252,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
|
|||||||
$(BIN)/test_nested_union_widen_run \
|
$(BIN)/test_nested_union_widen_run \
|
||||||
$(BIN)/test_sret_struct_return \
|
$(BIN)/test_sret_struct_return \
|
||||||
$(BIN)/test_sret_struct_return_run \
|
$(BIN)/test_sret_struct_return_run \
|
||||||
|
$(BIN)/test_global_sret_run \
|
||||||
$(BIN)/test_sret_narrow_field \
|
$(BIN)/test_sret_narrow_field \
|
||||||
$(BIN)/test_sret_narrow_field_run \
|
$(BIN)/test_sret_narrow_field_run \
|
||||||
$(BIN)/test_match_slice_variant \
|
$(BIN)/test_match_slice_variant \
|
||||||
@@ -846,6 +847,12 @@ $(BIN)/test_sret_struct_return_run: test/wcc/925_sret_struct_return_run.c \
|
|||||||
$(LIB)/libwwrt.a | $(BIN)
|
$(LIB)/libwwrt.a | $(BIN)
|
||||||
$(CC) $(CFLAGS) -o $@ $<
|
$(CC) $(CFLAGS) -o $@ $<
|
||||||
|
|
||||||
|
$(BIN)/test_global_sret_run: test/wcc/940_global_sret_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 $@ $<
|
||||||
|
|
||||||
$(BIN)/test_sret_narrow_field: test/wcc/730_sret_narrow_field.c \
|
$(BIN)/test_sret_narrow_field: test/wcc/730_sret_narrow_field.c \
|
||||||
$(BIN)/w6c $(BIN)/w6c_ww | $(BIN)
|
$(BIN)/w6c $(BIN)/w6c_ww | $(BIN)
|
||||||
$(CC) $(CFLAGS) -o $@ $<
|
$(CC) $(CFLAGS) -o $@ $<
|
||||||
|
|||||||
@@ -106,6 +106,11 @@ static int cg_tagscr_sz;
|
|||||||
* return value. No temporary in outer's frame. */
|
* return value. No temporary in outer's frame. */
|
||||||
static int cg_sret_arg_off;
|
static int cg_sret_arg_off;
|
||||||
static int cg_sret_dest_off;
|
static int cg_sret_dest_off;
|
||||||
|
/* #220: caller-side dest for an sret receive into a GLOBAL lvalue. A
|
||||||
|
* BP-relative i32 offset (cg_sret_dest_off) can't name a top-level let,
|
||||||
|
* so the symbol name is carried instead and emitted as LEAQ name(SB),DI.
|
||||||
|
* Mutually exclusive with cg_sret_dest_off. */
|
||||||
|
static const char *cg_sret_dest_sym;
|
||||||
static int cg_sretscr_off;
|
static int cg_sretscr_off;
|
||||||
static int cg_sretscr_sz;
|
static int cg_sretscr_sz;
|
||||||
static int cg_sret_forward;
|
static int cg_sret_forward;
|
||||||
@@ -4798,6 +4803,18 @@ cgexpr(Cg *c, Node *n, Local *locals)
|
|||||||
cg_sret_dest_off = 0;
|
cg_sret_dest_off = 0;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
/* #220: `g = f();` where g is a GLOBAL struct >24B.
|
||||||
|
* No BP slot to use as the sret dest, so route RDI
|
||||||
|
* to g's symbol address. Mirrors the str/slice
|
||||||
|
* global arm above (let_islet + LEAQ masym). The
|
||||||
|
* scalar fall-through below would emit a truncated
|
||||||
|
* 8-byte `MOVQ AX, g(SB)` and drop the struct body. */
|
||||||
|
if (let_islet(n->lhs->str)) {
|
||||||
|
cg_sret_dest_sym = n->lhs->str;
|
||||||
|
cgexpr(c, n->rhs, locals);
|
||||||
|
cg_sret_dest_sym = NULL;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/* Struct local reassignment: `s = expr;` where s is
|
/* Struct local reassignment: `s = expr;` where s is
|
||||||
* a TY_STRUCT local of size <=24B. Two rhs shapes,
|
* a TY_STRUCT local of size <=24B. Two rhs shapes,
|
||||||
@@ -5738,12 +5755,19 @@ cgexpr(Cg *c, Node *n, Local *locals)
|
|||||||
* left RDI free — we never popped a user arg into it. */
|
* left RDI free — we never popped a user arg into it. */
|
||||||
int sret_call_sz = 0;
|
int sret_call_sz = 0;
|
||||||
int sret_call_off = 0;
|
int sret_call_off = 0;
|
||||||
|
const char *sret_dest_sym = NULL; /* #220 */
|
||||||
{
|
{
|
||||||
Type *ret = (cu && cu->kind == TY_FN)
|
Type *ret = (cu && cu->kind == TY_FN)
|
||||||
? cu->ret : NULL;
|
? cu->ret : NULL;
|
||||||
sret_call_sz = cg_sret_retsize(ret);
|
sret_call_sz = cg_sret_retsize(ret);
|
||||||
}
|
}
|
||||||
if (sret_call_sz > 0) {
|
if (sret_call_sz > 0 && cg_sret_dest_sym != NULL) {
|
||||||
|
/* #220: GLOBAL dest — RDI gets LEAQ name(SB) below; no
|
||||||
|
* @sretscr slot needed (the callee writes the struct
|
||||||
|
* straight into g's storage). */
|
||||||
|
sret_dest_sym = cg_sret_dest_sym;
|
||||||
|
cg_sret_dest_sym = NULL;
|
||||||
|
} else if (sret_call_sz > 0) {
|
||||||
/* @sretscr is only needed when the result is dropped
|
/* @sretscr is only needed when the result is dropped
|
||||||
* (no `let x = f();` receiver wired the call's dest into
|
* (no `let x = f();` receiver wired the call's dest into
|
||||||
* cg_sret_dest_off). Allocate first-use per #15/#26c
|
* cg_sret_dest_off). Allocate first-use per #15/#26c
|
||||||
@@ -5953,6 +5977,10 @@ cgexpr(Cg *c, Node *n, Local *locals)
|
|||||||
amem(D_BP, cg_sret_arg_off),
|
amem(D_BP, cg_sret_arg_off),
|
||||||
areg(D_DI));
|
areg(D_DI));
|
||||||
cg_sret_forward = 0;
|
cg_sret_forward = 0;
|
||||||
|
} else if (sret_dest_sym != NULL) {
|
||||||
|
/* #220: sret into a GLOBAL — RDI = &g(SB). */
|
||||||
|
ins2(c, A_LEAQ, masym(c, sret_dest_sym),
|
||||||
|
areg(D_DI));
|
||||||
} else {
|
} else {
|
||||||
ins2(c, A_LEAQ,
|
ins2(c, A_LEAQ,
|
||||||
amem(D_BP, sret_call_off),
|
amem(D_BP, sret_call_off),
|
||||||
@@ -8947,6 +8975,7 @@ cgfn(Cg *c, FILE *out, Node *fn)
|
|||||||
cg_tagscr_sz = 0;
|
cg_tagscr_sz = 0;
|
||||||
cg_sret_arg_off = 0;
|
cg_sret_arg_off = 0;
|
||||||
cg_sret_dest_off = 0;
|
cg_sret_dest_off = 0;
|
||||||
|
cg_sret_dest_sym = NULL;
|
||||||
cg_sretscr_off = 0;
|
cg_sretscr_off = 0;
|
||||||
cg_sretscr_sz = 0;
|
cg_sretscr_sz = 0;
|
||||||
cg_sret_forward = 0;
|
cg_sret_forward = 0;
|
||||||
|
|||||||
@@ -22365,14 +22365,20 @@ fn cgcall(c: *cgen, n: *node) void = {
|
|||||||
// slot, sized at first use per #15/#26c.
|
// slot, sized at first use per #15/#26c.
|
||||||
let sretcs: i32 = callsretsize(c, n);
|
let sretcs: i32 = callsretsize(c, n);
|
||||||
let sretcalloff: i32 = 0;
|
let sretcalloff: i32 = 0;
|
||||||
|
// #220: GLOBAL dest — RDI gets `LEAQ name(SB)` below; no @sretscr
|
||||||
|
// slot (the callee writes the struct straight into g's storage).
|
||||||
|
let sretdestn: *node = nil;
|
||||||
if (sretcs > 0) {
|
if (sretcs > 0) {
|
||||||
if (c.sretdestoff != 0) {
|
if (c.sretdestnode != nil) {
|
||||||
|
sretdestn = c.sretdestnode;
|
||||||
|
c.sretdestnode = nil;
|
||||||
|
} else { if (c.sretdestoff != 0) {
|
||||||
sretcalloff = c.sretdestoff;
|
sretcalloff = c.sretdestoff;
|
||||||
c.sretdestoff = 0;
|
c.sretdestoff = 0;
|
||||||
} else {
|
} else {
|
||||||
sretcalloff = localadd(c, "@sretscr",
|
sretcalloff = localadd(c, "@sretscr",
|
||||||
sretcs, nil);
|
sretcs, nil);
|
||||||
};
|
};};
|
||||||
};
|
};
|
||||||
// Pop forward. Float args were pushed as 8 bytes from X0 via
|
// Pop forward. Float args were pushed as 8 bytes from X0 via
|
||||||
// SUBQ+MOVSD; pop into the XMM stream (X0..X7). Everything else
|
// SUBQ+MOVSD; pop into the XMM stream (X0..X7). Everything else
|
||||||
@@ -22644,11 +22650,16 @@ fn cgcall(c: *cgen, n: *node) void = {
|
|||||||
emitoff(sretargoff: i64);
|
emitoff(sretargoff: i64);
|
||||||
emitline("(BP), DI\n");
|
emitline("(BP), DI\n");
|
||||||
c.sretforward = 0;
|
c.sretforward = 0;
|
||||||
|
} else { if (sretdestn != nil) {
|
||||||
|
// #220: sret into a GLOBAL — RDI = &g(SB).
|
||||||
|
emitline("\tLEAQ\t");
|
||||||
|
emitsymname(c, sretdestn.str);
|
||||||
|
emitline("(SB), DI\n");
|
||||||
} else {
|
} else {
|
||||||
emitline("\tLEAQ\t");
|
emitline("\tLEAQ\t");
|
||||||
emitoff(sretcalloff: i64);
|
emitoff(sretcalloff: i64);
|
||||||
emitline("(BP), DI\n");
|
emitline("(BP), DI\n");
|
||||||
};
|
};};
|
||||||
};
|
};
|
||||||
if (isfnptrcall) {
|
if (isfnptrcall) {
|
||||||
// Load fn-ptr field value into AX; CALL AX. We emit the
|
// Load fn-ptr field value into AX; CALL AX. We emit the
|
||||||
@@ -25084,6 +25095,30 @@ fn cgassign(c: *cgen, n: *node) void = {
|
|||||||
emitline("\tX1, (CX)\n");
|
emitline("\tX1, (CX)\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
// #220: `g = f();` where g is a GLOBAL struct >24B.
|
||||||
|
// No BP slot for the sret dest, so route RDI to g's
|
||||||
|
// symbol; the callee writes the struct straight into
|
||||||
|
// g's storage. The scalar store below would emit a
|
||||||
|
// truncated `MOVQ AX, g(SB)` and drop the body. Mirror
|
||||||
|
// of the C cgen N_ASSIGN global arm (cmd/w6c/cgen.c).
|
||||||
|
if (n.op == tkind.TK_ASSIGN && n.rhs != nil
|
||||||
|
&& n.rhs.kind == nkind.N_CALL && lvftn != nil) {
|
||||||
|
if (lvftn.kind == nkind.N_TNAME) {
|
||||||
|
let gsi: *structinfo = structlookup(c, lvftn.str);
|
||||||
|
if (gsi != nil) {
|
||||||
|
let gsz: i32 = structabisize(gsi);
|
||||||
|
if (gsz > 24) {
|
||||||
|
let rscs: i32 = callsretsize(c, n.rhs);
|
||||||
|
if (rscs > 0) {
|
||||||
|
c.sretdestnode = lhs;
|
||||||
|
cgexpr(c, n.rhs);
|
||||||
|
c.sretdestnode = nil;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
cgexpr(c, n.rhs);
|
cgexpr(c, n.rhs);
|
||||||
if (n.op == tkind.TK_ASSIGN) {
|
if (n.op == tkind.TK_ASSIGN) {
|
||||||
// str/slice top-level let: str IS []u8, so both store the
|
// str/slice top-level let: str IS []u8, so both store the
|
||||||
@@ -28704,6 +28739,11 @@ type cgen = struct {
|
|||||||
// cgenexpr resolve `@sretarg` via localfind when they need the
|
// cgenexpr resolve `@sretarg` via localfind when they need the
|
||||||
// saved RDI.
|
// saved RDI.
|
||||||
sretdestoff: i32,
|
sretdestoff: i32,
|
||||||
|
// #220: sret receive into a GLOBAL lvalue. A BP-relative i32
|
||||||
|
// (sretdestoff) can't name a top-level let, so the lhs IDENT node
|
||||||
|
// is carried and emitted as `LEAQ name(SB), DI`. nil means no
|
||||||
|
// global receiver wired; mutually exclusive with sretdestoff.
|
||||||
|
sretdestnode: *node,
|
||||||
sretforward: i32,
|
sretforward: i32,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -28727,6 +28767,7 @@ fn cgeninit(c: *cgen) void = {
|
|||||||
c.labelseq = 0;
|
c.labelseq = 0;
|
||||||
c.varargseq = 0;
|
c.varargseq = 0;
|
||||||
c.sretdestoff = 0;
|
c.sretdestoff = 0;
|
||||||
|
c.sretdestnode = nil;
|
||||||
c.sretforward = 0;
|
c.sretforward = 0;
|
||||||
// Note: strlit_seq, strlits, ffis are *not* reset here; they
|
// Note: strlit_seq, strlits, ffis are *not* reset here; they
|
||||||
// persist across cgfn calls within one file. cgfile resets them
|
// persist across cgfn calls within one file. cgfile resets them
|
||||||
|
|||||||
@@ -505,6 +505,11 @@ type cgen = struct {
|
|||||||
// cgenexpr resolve `@sretarg` via localfind when they need the
|
// cgenexpr resolve `@sretarg` via localfind when they need the
|
||||||
// saved RDI.
|
// saved RDI.
|
||||||
sretdestoff: i32,
|
sretdestoff: i32,
|
||||||
|
// #220: sret receive into a GLOBAL lvalue. A BP-relative i32
|
||||||
|
// (sretdestoff) can't name a top-level let, so the lhs IDENT node
|
||||||
|
// is carried and emitted as `LEAQ name(SB), DI`. nil means no
|
||||||
|
// global receiver wired; mutually exclusive with sretdestoff.
|
||||||
|
sretdestnode: *node,
|
||||||
sretforward: i32,
|
sretforward: i32,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -528,6 +533,7 @@ fn cgeninit(c: *cgen) void = {
|
|||||||
c.labelseq = 0;
|
c.labelseq = 0;
|
||||||
c.varargseq = 0;
|
c.varargseq = 0;
|
||||||
c.sretdestoff = 0;
|
c.sretdestoff = 0;
|
||||||
|
c.sretdestnode = nil;
|
||||||
c.sretforward = 0;
|
c.sretforward = 0;
|
||||||
// Note: strlit_seq, strlits, ffis are *not* reset here; they
|
// Note: strlit_seq, strlits, ffis are *not* reset here; they
|
||||||
// persist across cgfn calls within one file. cgfile resets them
|
// persist across cgfn calls within one file. cgfile resets them
|
||||||
|
|||||||
@@ -4002,14 +4002,20 @@ fn cgcall(c: *cgen, n: *node) void = {
|
|||||||
// slot, sized at first use per #15/#26c.
|
// slot, sized at first use per #15/#26c.
|
||||||
let sretcs: i32 = callsretsize(c, n);
|
let sretcs: i32 = callsretsize(c, n);
|
||||||
let sretcalloff: i32 = 0;
|
let sretcalloff: i32 = 0;
|
||||||
|
// #220: GLOBAL dest — RDI gets `LEAQ name(SB)` below; no @sretscr
|
||||||
|
// slot (the callee writes the struct straight into g's storage).
|
||||||
|
let sretdestn: *node = nil;
|
||||||
if (sretcs > 0) {
|
if (sretcs > 0) {
|
||||||
if (c.sretdestoff != 0) {
|
if (c.sretdestnode != nil) {
|
||||||
|
sretdestn = c.sretdestnode;
|
||||||
|
c.sretdestnode = nil;
|
||||||
|
} else { if (c.sretdestoff != 0) {
|
||||||
sretcalloff = c.sretdestoff;
|
sretcalloff = c.sretdestoff;
|
||||||
c.sretdestoff = 0;
|
c.sretdestoff = 0;
|
||||||
} else {
|
} else {
|
||||||
sretcalloff = localadd(c, "@sretscr",
|
sretcalloff = localadd(c, "@sretscr",
|
||||||
sretcs, nil);
|
sretcs, nil);
|
||||||
};
|
};};
|
||||||
};
|
};
|
||||||
// Pop forward. Float args were pushed as 8 bytes from X0 via
|
// Pop forward. Float args were pushed as 8 bytes from X0 via
|
||||||
// SUBQ+MOVSD; pop into the XMM stream (X0..X7). Everything else
|
// SUBQ+MOVSD; pop into the XMM stream (X0..X7). Everything else
|
||||||
@@ -4281,11 +4287,16 @@ fn cgcall(c: *cgen, n: *node) void = {
|
|||||||
emitoff(sretargoff: i64);
|
emitoff(sretargoff: i64);
|
||||||
emitline("(BP), DI\n");
|
emitline("(BP), DI\n");
|
||||||
c.sretforward = 0;
|
c.sretforward = 0;
|
||||||
|
} else { if (sretdestn != nil) {
|
||||||
|
// #220: sret into a GLOBAL — RDI = &g(SB).
|
||||||
|
emitline("\tLEAQ\t");
|
||||||
|
emitsymname(c, sretdestn.str);
|
||||||
|
emitline("(SB), DI\n");
|
||||||
} else {
|
} else {
|
||||||
emitline("\tLEAQ\t");
|
emitline("\tLEAQ\t");
|
||||||
emitoff(sretcalloff: i64);
|
emitoff(sretcalloff: i64);
|
||||||
emitline("(BP), DI\n");
|
emitline("(BP), DI\n");
|
||||||
};
|
};};
|
||||||
};
|
};
|
||||||
if (isfnptrcall) {
|
if (isfnptrcall) {
|
||||||
// Load fn-ptr field value into AX; CALL AX. We emit the
|
// Load fn-ptr field value into AX; CALL AX. We emit the
|
||||||
@@ -6721,6 +6732,30 @@ fn cgassign(c: *cgen, n: *node) void = {
|
|||||||
emitline("\tX1, (CX)\n");
|
emitline("\tX1, (CX)\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
// #220: `g = f();` where g is a GLOBAL struct >24B.
|
||||||
|
// No BP slot for the sret dest, so route RDI to g's
|
||||||
|
// symbol; the callee writes the struct straight into
|
||||||
|
// g's storage. The scalar store below would emit a
|
||||||
|
// truncated `MOVQ AX, g(SB)` and drop the body. Mirror
|
||||||
|
// of the C cgen N_ASSIGN global arm (cmd/w6c/cgen.c).
|
||||||
|
if (n.op == tkind.TK_ASSIGN && n.rhs != nil
|
||||||
|
&& n.rhs.kind == nkind.N_CALL && lvftn != nil) {
|
||||||
|
if (lvftn.kind == nkind.N_TNAME) {
|
||||||
|
let gsi: *structinfo = structlookup(c, lvftn.str);
|
||||||
|
if (gsi != nil) {
|
||||||
|
let gsz: i32 = structabisize(gsi);
|
||||||
|
if (gsz > 24) {
|
||||||
|
let rscs: i32 = callsretsize(c, n.rhs);
|
||||||
|
if (rscs > 0) {
|
||||||
|
c.sretdestnode = lhs;
|
||||||
|
cgexpr(c, n.rhs);
|
||||||
|
c.sretdestnode = nil;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
cgexpr(c, n.rhs);
|
cgexpr(c, n.rhs);
|
||||||
if (n.op == tkind.TK_ASSIGN) {
|
if (n.op == tkind.TK_ASSIGN) {
|
||||||
// str/slice top-level let: str IS []u8, so both store the
|
// str/slice top-level let: str IS []u8, so both store the
|
||||||
|
|||||||
@@ -22365,14 +22365,20 @@ fn cgcall(c: *cgen, n: *node) void = {
|
|||||||
// slot, sized at first use per #15/#26c.
|
// slot, sized at first use per #15/#26c.
|
||||||
let sretcs: i32 = callsretsize(c, n);
|
let sretcs: i32 = callsretsize(c, n);
|
||||||
let sretcalloff: i32 = 0;
|
let sretcalloff: i32 = 0;
|
||||||
|
// #220: GLOBAL dest — RDI gets `LEAQ name(SB)` below; no @sretscr
|
||||||
|
// slot (the callee writes the struct straight into g's storage).
|
||||||
|
let sretdestn: *node = nil;
|
||||||
if (sretcs > 0) {
|
if (sretcs > 0) {
|
||||||
if (c.sretdestoff != 0) {
|
if (c.sretdestnode != nil) {
|
||||||
|
sretdestn = c.sretdestnode;
|
||||||
|
c.sretdestnode = nil;
|
||||||
|
} else { if (c.sretdestoff != 0) {
|
||||||
sretcalloff = c.sretdestoff;
|
sretcalloff = c.sretdestoff;
|
||||||
c.sretdestoff = 0;
|
c.sretdestoff = 0;
|
||||||
} else {
|
} else {
|
||||||
sretcalloff = localadd(c, "@sretscr",
|
sretcalloff = localadd(c, "@sretscr",
|
||||||
sretcs, nil);
|
sretcs, nil);
|
||||||
};
|
};};
|
||||||
};
|
};
|
||||||
// Pop forward. Float args were pushed as 8 bytes from X0 via
|
// Pop forward. Float args were pushed as 8 bytes from X0 via
|
||||||
// SUBQ+MOVSD; pop into the XMM stream (X0..X7). Everything else
|
// SUBQ+MOVSD; pop into the XMM stream (X0..X7). Everything else
|
||||||
@@ -22644,11 +22650,16 @@ fn cgcall(c: *cgen, n: *node) void = {
|
|||||||
emitoff(sretargoff: i64);
|
emitoff(sretargoff: i64);
|
||||||
emitline("(BP), DI\n");
|
emitline("(BP), DI\n");
|
||||||
c.sretforward = 0;
|
c.sretforward = 0;
|
||||||
|
} else { if (sretdestn != nil) {
|
||||||
|
// #220: sret into a GLOBAL — RDI = &g(SB).
|
||||||
|
emitline("\tLEAQ\t");
|
||||||
|
emitsymname(c, sretdestn.str);
|
||||||
|
emitline("(SB), DI\n");
|
||||||
} else {
|
} else {
|
||||||
emitline("\tLEAQ\t");
|
emitline("\tLEAQ\t");
|
||||||
emitoff(sretcalloff: i64);
|
emitoff(sretcalloff: i64);
|
||||||
emitline("(BP), DI\n");
|
emitline("(BP), DI\n");
|
||||||
};
|
};};
|
||||||
};
|
};
|
||||||
if (isfnptrcall) {
|
if (isfnptrcall) {
|
||||||
// Load fn-ptr field value into AX; CALL AX. We emit the
|
// Load fn-ptr field value into AX; CALL AX. We emit the
|
||||||
@@ -25084,6 +25095,30 @@ fn cgassign(c: *cgen, n: *node) void = {
|
|||||||
emitline("\tX1, (CX)\n");
|
emitline("\tX1, (CX)\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
// #220: `g = f();` where g is a GLOBAL struct >24B.
|
||||||
|
// No BP slot for the sret dest, so route RDI to g's
|
||||||
|
// symbol; the callee writes the struct straight into
|
||||||
|
// g's storage. The scalar store below would emit a
|
||||||
|
// truncated `MOVQ AX, g(SB)` and drop the body. Mirror
|
||||||
|
// of the C cgen N_ASSIGN global arm (cmd/w6c/cgen.c).
|
||||||
|
if (n.op == tkind.TK_ASSIGN && n.rhs != nil
|
||||||
|
&& n.rhs.kind == nkind.N_CALL && lvftn != nil) {
|
||||||
|
if (lvftn.kind == nkind.N_TNAME) {
|
||||||
|
let gsi: *structinfo = structlookup(c, lvftn.str);
|
||||||
|
if (gsi != nil) {
|
||||||
|
let gsz: i32 = structabisize(gsi);
|
||||||
|
if (gsz > 24) {
|
||||||
|
let rscs: i32 = callsretsize(c, n.rhs);
|
||||||
|
if (rscs > 0) {
|
||||||
|
c.sretdestnode = lhs;
|
||||||
|
cgexpr(c, n.rhs);
|
||||||
|
c.sretdestnode = nil;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
cgexpr(c, n.rhs);
|
cgexpr(c, n.rhs);
|
||||||
if (n.op == tkind.TK_ASSIGN) {
|
if (n.op == tkind.TK_ASSIGN) {
|
||||||
// str/slice top-level let: str IS []u8, so both store the
|
// str/slice top-level let: str IS []u8, so both store the
|
||||||
@@ -28704,6 +28739,11 @@ type cgen = struct {
|
|||||||
// cgenexpr resolve `@sretarg` via localfind when they need the
|
// cgenexpr resolve `@sretarg` via localfind when they need the
|
||||||
// saved RDI.
|
// saved RDI.
|
||||||
sretdestoff: i32,
|
sretdestoff: i32,
|
||||||
|
// #220: sret receive into a GLOBAL lvalue. A BP-relative i32
|
||||||
|
// (sretdestoff) can't name a top-level let, so the lhs IDENT node
|
||||||
|
// is carried and emitted as `LEAQ name(SB), DI`. nil means no
|
||||||
|
// global receiver wired; mutually exclusive with sretdestoff.
|
||||||
|
sretdestnode: *node,
|
||||||
sretforward: i32,
|
sretforward: i32,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -28727,6 +28767,7 @@ fn cgeninit(c: *cgen) void = {
|
|||||||
c.labelseq = 0;
|
c.labelseq = 0;
|
||||||
c.varargseq = 0;
|
c.varargseq = 0;
|
||||||
c.sretdestoff = 0;
|
c.sretdestoff = 0;
|
||||||
|
c.sretdestnode = nil;
|
||||||
c.sretforward = 0;
|
c.sretforward = 0;
|
||||||
// Note: strlit_seq, strlits, ffis are *not* reset here; they
|
// Note: strlit_seq, strlits, ffis are *not* reset here; they
|
||||||
// persist across cgfn calls within one file. cgfile resets them
|
// persist across cgfn calls within one file. cgfile resets them
|
||||||
|
|||||||
277
test/wcc/940_global_sret_run.c
Normal file
277
test/wcc/940_global_sret_run.c
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
/*
|
||||||
|
* 940_global_sret_run — project #220. sret (>24B by-value struct
|
||||||
|
* return) assigned into a GLOBAL lvalue must land the whole struct,
|
||||||
|
* not a truncated 8-byte store.
|
||||||
|
*
|
||||||
|
* Gate-blind family (sibling of #211): pre-fix BOTH stages emitted the
|
||||||
|
* SAME broken `MOVQ AX, g(SB)` (only the sret return pointer reached
|
||||||
|
* the global; the struct body, written to a scratch temp, was lost), so
|
||||||
|
* asm byte-id stayed GREEN while runtime was wrong — g.pos == 0 for the
|
||||||
|
* #94 io vtable cgoutstream pattern. The runtime rows below are the net.
|
||||||
|
*
|
||||||
|
* Surfacing case: the eFinal FLIP repointed cgen.ww's per-fn body output
|
||||||
|
* through a GLOBAL `let cgoutstream: memio.stream` (>24B) wired once via
|
||||||
|
* `cgoutstream = memio.dynamic()`; the truncated store left pos==0 so
|
||||||
|
* the buffered body never flushed and w6c_ww emitted prologue-only.
|
||||||
|
*
|
||||||
|
* The fix routes the sret dest pointer to the global's symbol address
|
||||||
|
* (LEAQ g(SB), DI) — zero-copy, the same discipline the local case
|
||||||
|
* already used (LEAQ off(BP), DI). Local rows pin no regression at the
|
||||||
|
* shared cgassign / cglet sret-receive sites.
|
||||||
|
*
|
||||||
|
* Each row runs on BOTH cstage (ww) and wwstage (ww_ww), and the
|
||||||
|
* generated asm is checked byte-identical (cs.s == ww.s, rule 10).
|
||||||
|
*/
|
||||||
|
#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[] = {
|
||||||
|
/* GLOBAL sret assign: `let g: quad;` at module scope, `g = mk()`
|
||||||
|
* in main. Pre-fix the global got only `MOVQ AX, g(SB)` and the
|
||||||
|
* field reads returned garbage. */
|
||||||
|
{ "global_assign",
|
||||||
|
"type quad = struct { a: i64, b: i64, c: i64, d: i64 };\n"
|
||||||
|
"fn mk(x: i64) quad = {\n"
|
||||||
|
" return quad { a = x, b = x + 1i64, c = x + 2i64, d = x + 3i64 };\n"
|
||||||
|
"};\n"
|
||||||
|
"let g: quad;\n"
|
||||||
|
"export fn main() i32 = {\n"
|
||||||
|
" g = mk(10i64);\n"
|
||||||
|
" if (g.a != 10i64) { return 1; };\n"
|
||||||
|
" if (g.b != 11i64) { return 2; };\n"
|
||||||
|
" if (g.c != 12i64) { return 3; };\n"
|
||||||
|
" if (g.d != 13i64) { return 4; };\n"
|
||||||
|
" return 0;\n"
|
||||||
|
"};\n",
|
||||||
|
0 },
|
||||||
|
/* GLOBAL sret with a BRANCHED callee (#105): the returned value
|
||||||
|
* is selected at runtime so a constant-fold coincidence can't mask
|
||||||
|
* the register/dest routing. */
|
||||||
|
{ "global_branched",
|
||||||
|
"type quad = struct { a: i64, b: i64, c: i64, d: i64 };\n"
|
||||||
|
"fn mk1() quad = { return quad { a = 1i64, b = 2i64, c = 3i64, d = 4i64 }; };\n"
|
||||||
|
"fn mk2() quad = { return quad { a = 50i64, b = 0i64, c = 0i64, d = 0i64 }; };\n"
|
||||||
|
"fn pick(w: i64) quad = {\n"
|
||||||
|
" if (w == 1i64) { return mk1(); };\n"
|
||||||
|
" return mk2();\n"
|
||||||
|
"};\n"
|
||||||
|
"let g: quad;\n"
|
||||||
|
"export fn main() i32 = {\n"
|
||||||
|
" g = pick(1i64);\n"
|
||||||
|
" if (g.a != 1i64) { return 1; };\n"
|
||||||
|
" if (g.b != 2i64) { return 2; };\n"
|
||||||
|
" if (g.c != 3i64) { return 3; };\n"
|
||||||
|
" if (g.d != 4i64) { return 4; };\n"
|
||||||
|
" return 0;\n"
|
||||||
|
"};\n",
|
||||||
|
0 },
|
||||||
|
/* GLOBAL mutated THROUGH A POINTER after the sret assign — the io
|
||||||
|
* vtable-callback shape that surfaced #220 (callback receives &g,
|
||||||
|
* mutates a field). */
|
||||||
|
{ "global_through_pointer",
|
||||||
|
"type quad = struct { a: i64, b: i64, c: i64, d: i64 };\n"
|
||||||
|
"fn mk(x: i64) quad = {\n"
|
||||||
|
" return quad { a = x, b = x + 1i64, c = x + 2i64, d = x + 3i64 };\n"
|
||||||
|
"};\n"
|
||||||
|
"fn bump(p: *quad) void = { p.a += 100i64; };\n"
|
||||||
|
"let g: quad;\n"
|
||||||
|
"export fn main() i32 = {\n"
|
||||||
|
" g = mk(7i64);\n"
|
||||||
|
" bump(&g);\n"
|
||||||
|
" if (g.a != 107i64) { return 1; };\n"
|
||||||
|
" if (g.b != 8i64) { return 2; };\n"
|
||||||
|
" if (g.c != 9i64) { return 3; };\n"
|
||||||
|
" if (g.d != 10i64) { return 4; };\n"
|
||||||
|
" return 0;\n"
|
||||||
|
"};\n",
|
||||||
|
0 },
|
||||||
|
/* LOCAL decl+assign regression: `let g: quad; g = mk();` routes
|
||||||
|
* through the same cgassign sret-receive site the fix touches. */
|
||||||
|
{ "local_assign_regression",
|
||||||
|
"type quad = struct { a: i64, b: i64, c: i64, d: i64 };\n"
|
||||||
|
"fn mk(x: i64) quad = {\n"
|
||||||
|
" return quad { a = x, b = x + 1i64, c = x + 2i64, d = x + 3i64 };\n"
|
||||||
|
"};\n"
|
||||||
|
"export fn main() i32 = {\n"
|
||||||
|
" let g: quad;\n"
|
||||||
|
" g = mk(20i64);\n"
|
||||||
|
" if (g.a != 20i64) { return 1; };\n"
|
||||||
|
" if (g.b != 21i64) { return 2; };\n"
|
||||||
|
" if (g.c != 22i64) { return 3; };\n"
|
||||||
|
" if (g.d != 23i64) { return 4; };\n"
|
||||||
|
" return 0;\n"
|
||||||
|
"};\n",
|
||||||
|
0 },
|
||||||
|
/* LOCAL let-init regression: `let g: quad = mk();` routes through
|
||||||
|
* cglet's sret-receive branch (the sister site). */
|
||||||
|
{ "local_init_regression",
|
||||||
|
"type quad = struct { a: i64, b: i64, c: i64, d: i64 };\n"
|
||||||
|
"fn mk(x: i64) quad = {\n"
|
||||||
|
" return quad { a = x, b = x + 1i64, c = x + 2i64, d = x + 3i64 };\n"
|
||||||
|
"};\n"
|
||||||
|
"export fn main() i32 = {\n"
|
||||||
|
" let g: quad = mk(30i64);\n"
|
||||||
|
" if (g.a != 30i64) { return 1; };\n"
|
||||||
|
" if (g.b != 31i64) { return 2; };\n"
|
||||||
|
" if (g.c != 32i64) { return 3; };\n"
|
||||||
|
" if (g.d != 33i64) { return 4; };\n"
|
||||||
|
" return 0;\n"
|
||||||
|
"};\n",
|
||||||
|
0 },
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Compile the combined.ww (left next to the source by `ww build`) with
|
||||||
|
* both compilers and assert byte-identical asm. Returns 0 ok, 1 differ,
|
||||||
|
* -1 harness error. */
|
||||||
|
static int
|
||||||
|
byteid(const char *bin, const char *combined, const char *dir, int i)
|
||||||
|
{
|
||||||
|
char cs[256], ws[256], cmd[1024];
|
||||||
|
snprintf(cs, sizeof cs, "%s/bid_cs_%d.s", dir, i);
|
||||||
|
snprintf(ws, sizeof ws, "%s/bid_ww_%d.s", dir, i);
|
||||||
|
|
||||||
|
snprintf(cmd, sizeof cmd, "%s/w6c %s > %s", bin, combined, cs);
|
||||||
|
if (runwait(cmd) != 0) return -1;
|
||||||
|
snprintf(cmd, sizeof cmd, "%s/w6c_ww %s > %s", bin, combined, ws);
|
||||||
|
if (runwait(cmd) != 0) return -1;
|
||||||
|
|
||||||
|
snprintf(cmd, sizeof cmd, "cmp -s %s %s", cs, ws);
|
||||||
|
int rc = runwait(cmd);
|
||||||
|
unlink(cs); unlink(ws);
|
||||||
|
return rc == 0 ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Build `r` with `driver` in `dir`, run the binary, return its exit
|
||||||
|
* code (or -1 on build failure). `combined_out` receives the path of
|
||||||
|
* the generated <stem>.combined.ww. */
|
||||||
|
static int
|
||||||
|
run_driver(const char *driver, const struct row *r, const char *dir,
|
||||||
|
int i, char *combined_out, size_t combined_sz)
|
||||||
|
{
|
||||||
|
char src[256], cmd[1024];
|
||||||
|
snprintf(src, sizeof src, "%s/gsret_%d.ww", dir, i);
|
||||||
|
|
||||||
|
FILE *f = fopen(src, "wb");
|
||||||
|
if (!f) return -1;
|
||||||
|
fputs(r->src, f);
|
||||||
|
fclose(f);
|
||||||
|
|
||||||
|
snprintf(cmd, sizeof cmd, "cd %s && %s build %s", dir, driver, src);
|
||||||
|
if (runwait(cmd) != 0) {
|
||||||
|
fprintf(stderr, "row[%s]: build via %s failed\n",
|
||||||
|
r->label, driver);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
char outbin[256];
|
||||||
|
snprintf(outbin, sizeof outbin, "%s/gsret_%d", dir, i);
|
||||||
|
if (combined_out)
|
||||||
|
snprintf(combined_out, combined_sz,
|
||||||
|
"%s/gsret_%d.combined.ww", dir, i);
|
||||||
|
return runwait(outbin);
|
||||||
|
}
|
||||||
|
|
||||||
|
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], wdrv[640];
|
||||||
|
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
|
||||||
|
snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin);
|
||||||
|
char wwc[640];
|
||||||
|
snprintf(wwc, sizeof wwc, "%s/w6c_ww", bin);
|
||||||
|
int have_ww = (access(wdrv, X_OK) == 0);
|
||||||
|
int have_wwc = (access(wwc, X_OK) == 0);
|
||||||
|
|
||||||
|
char dir[] = "/tmp/gsret_XXXXXX";
|
||||||
|
if (mkdtemp(dir) == NULL) {
|
||||||
|
perror("mkdtemp");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int n = (int)(sizeof rows / sizeof rows[0]);
|
||||||
|
int total = 0, fail = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
char combined[300];
|
||||||
|
combined[0] = '\0';
|
||||||
|
|
||||||
|
/* cstage: build + run + capture combined.ww path. */
|
||||||
|
int gc = run_driver(cdrv, &rows[i], dir, i,
|
||||||
|
combined, sizeof combined);
|
||||||
|
total++;
|
||||||
|
if (gc != rows[i].want) {
|
||||||
|
fprintf(stderr, "global_sret_run[cstage][%s]: "
|
||||||
|
"exit=%d want=%d\n", rows[i].label, gc,
|
||||||
|
rows[i].want);
|
||||||
|
fail++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* byte-id (rule 10): cs.s == ww.s on the same combined.ww. */
|
||||||
|
if (have_wwc && combined[0]) {
|
||||||
|
total++;
|
||||||
|
int bid = byteid(bin, combined, dir, i);
|
||||||
|
if (bid != 0) {
|
||||||
|
fprintf(stderr, "global_sret_run[byteid][%s]: "
|
||||||
|
"%s\n", rows[i].label,
|
||||||
|
bid == 1 ? "cs.s != ww.s" : "harness error");
|
||||||
|
fail++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* wwstage: build + run. */
|
||||||
|
if (have_ww) {
|
||||||
|
int gw = run_driver(wdrv, &rows[i], dir, i, NULL, 0);
|
||||||
|
total++;
|
||||||
|
if (gw != rows[i].want) {
|
||||||
|
fprintf(stderr, "global_sret_run[wwstage][%s]: "
|
||||||
|
"exit=%d want=%d\n", rows[i].label, gw,
|
||||||
|
rows[i].want);
|
||||||
|
fail++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
char p[256];
|
||||||
|
snprintf(p, sizeof p, "%s/gsret_%d.ww", dir, i); unlink(p);
|
||||||
|
snprintf(p, sizeof p, "%s/gsret_%d", dir, i); unlink(p);
|
||||||
|
snprintf(p, sizeof p, "%s/gsret_%d.combined.ww", dir, i); unlink(p);
|
||||||
|
snprintf(p, sizeof p, "%s/gsret_%d.s", dir, i); unlink(p);
|
||||||
|
snprintf(p, sizeof p, "%s/gsret_%d.o", dir, i); unlink(p);
|
||||||
|
}
|
||||||
|
rmdir(dir);
|
||||||
|
|
||||||
|
if (!have_ww)
|
||||||
|
fprintf(stderr, "global_sret_run: skip wwstage (no %s)\n", wdrv);
|
||||||
|
|
||||||
|
if (fail) {
|
||||||
|
fprintf(stderr, "global_sret_run: %d/%d checks failed\n",
|
||||||
|
fail, total);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("global_sret_run: %d/%d ok\n", total, total);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user