wcc/cgen: #150 by-value module-global struct-arg base — load main.g(SB) all words (both stages)

Passing a module-global struct by value -- let g: pt = pt{...}; take(g)
-- was silently miscompiled, mirror-opposite on the two stages. cstage's
by-value struct-arg arm hit localfind(g)->0 and read 2 words from the
frame (MOVQ (BP)), never main.g(SB) -> returned garbage. wwstage used the
correct main.g(SB) base but fell through to the scalar single-PUSHQ
default, pushing one word for a 2-word struct -> dropped a field.

Both stages now take the off==0 global branch: LEAQ main.NAME(SB) and copy
all struct-size/8 eightbytes (reusing the GAP-A.ptr/#231 global-base
predicate), converging to one byte-identical sequence. The local path
(off!=0) is unchanged; >16B aggregates (#271) already resolved globals.

Commit A of the cluster; the cstage-only inferred-global-type Sym-repoint
(every let g = ... module-global yields <nil> downstream) is Commit B
(#18). Slice/str global-by-value args have the same wwstage field-drop --
filed (#10 G-valglobal-arg; struct closed here). byte-id 990-997 8/8.
test/wcc/822 table-driven, byte-id per stage.
This commit is contained in:
2026-06-08 20:51:01 +09:00
parent 03fc7c7abe
commit 5c3764828f
6 changed files with 372 additions and 0 deletions

View File

@@ -17043,6 +17043,39 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = {
return rest + nw;
};
};
// #150: a module-global by-value struct arg. localfindnode
// above misses it (a global is not a frame slot), and the #271
// arm below excludes a ≤16B struct ident (structident=true), so
// pre-fix it fell through to the scalar single-PUSHQ default and
// silently dropped word1. cstage's mirror-opposite bug read the
// FRAME (localfind→0 off==0 footgun); both stages converge here
// on the global-base load: LEAQ name(SB) into BX, then copy ALL
// eightbytes high→low (the #256/#129-A.2 struct-global shape; the
// isletvar||deflookup gate is the let_islet||def_isstructdef twin
// from dotchainaddr). The slot-word count is the stamped tinfo
// size (the type table, byte-id with cstage struct_arg_size).
if (lc == nil) {
let gst: *tinfo = arg.type_: *tinfo;
gst = tichase(gst);
if (gst != nil) { if (gst.kind == tykind.TY_STRUCT) {
let gsz: i32 = gst.size: i32;
if (gsz > 0 && gsz <= 16
&& (isletvar(c, nm) || deflookup(c, nm))) {
emitline("\tLEAQ\t");
emitsymname(c, nm);
emitline("(SB), BX\n");
if (gsz > 8) {
emitline("\tMOVQ\t8(BX), AX\n");
emitline("\tPUSHQ\tAX\n");
};
emitline("\tMOVQ\t(BX), AX\n");
emitline("\tPUSHQ\tAX\n");
let gnw: i32 = 1;
if (gsz > 8) { gnw = 2; };
return rest + gnw;
};
}; };
};
};
// #271: aggregate (struct/array) arg from any source the ≤16B
// struct-IDENT fast path above doesn't cover — a 16B struct from a

View File

@@ -723,6 +723,39 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = {
return rest + nw;
};
};
// #150: a module-global by-value struct arg. localfindnode
// above misses it (a global is not a frame slot), and the #271
// arm below excludes a ≤16B struct ident (structident=true), so
// pre-fix it fell through to the scalar single-PUSHQ default and
// silently dropped word1. cstage's mirror-opposite bug read the
// FRAME (localfind→0 off==0 footgun); both stages converge here
// on the global-base load: LEAQ name(SB) into BX, then copy ALL
// eightbytes high→low (the #256/#129-A.2 struct-global shape; the
// isletvar||deflookup gate is the let_islet||def_isstructdef twin
// from dotchainaddr). The slot-word count is the stamped tinfo
// size (the type table, byte-id with cstage struct_arg_size).
if (lc == nil) {
let gst: *tinfo = arg.type_: *tinfo;
gst = tichase(gst);
if (gst != nil) { if (gst.kind == tykind.TY_STRUCT) {
let gsz: i32 = gst.size: i32;
if (gsz > 0 && gsz <= 16
&& (isletvar(c, nm) || deflookup(c, nm))) {
emitline("\tLEAQ\t");
emitsymname(c, nm);
emitline("(SB), BX\n");
if (gsz > 8) {
emitline("\tMOVQ\t8(BX), AX\n");
emitline("\tPUSHQ\tAX\n");
};
emitline("\tMOVQ\t(BX), AX\n");
emitline("\tPUSHQ\tAX\n");
let gnw: i32 = 1;
if (gsz > 8) { gnw = 2; };
return rest + gnw;
};
}; };
};
};
// #271: aggregate (struct/array) arg from any source the ≤16B
// struct-IDENT fast path above doesn't cover — a 16B struct from a

View File

@@ -17043,6 +17043,39 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = {
return rest + nw;
};
};
// #150: a module-global by-value struct arg. localfindnode
// above misses it (a global is not a frame slot), and the #271
// arm below excludes a ≤16B struct ident (structident=true), so
// pre-fix it fell through to the scalar single-PUSHQ default and
// silently dropped word1. cstage's mirror-opposite bug read the
// FRAME (localfind→0 off==0 footgun); both stages converge here
// on the global-base load: LEAQ name(SB) into BX, then copy ALL
// eightbytes high→low (the #256/#129-A.2 struct-global shape; the
// isletvar||deflookup gate is the let_islet||def_isstructdef twin
// from dotchainaddr). The slot-word count is the stamped tinfo
// size (the type table, byte-id with cstage struct_arg_size).
if (lc == nil) {
let gst: *tinfo = arg.type_: *tinfo;
gst = tichase(gst);
if (gst != nil) { if (gst.kind == tykind.TY_STRUCT) {
let gsz: i32 = gst.size: i32;
if (gsz > 0 && gsz <= 16
&& (isletvar(c, nm) || deflookup(c, nm))) {
emitline("\tLEAQ\t");
emitsymname(c, nm);
emitline("(SB), BX\n");
if (gsz > 8) {
emitline("\tMOVQ\t8(BX), AX\n");
emitline("\tPUSHQ\tAX\n");
};
emitline("\tMOVQ\t(BX), AX\n");
emitline("\tPUSHQ\tAX\n");
let gnw: i32 = 1;
if (gsz > 8) { gnw = 2; };
return rest + gnw;
};
}; };
};
};
// #271: aggregate (struct/array) arg from any source the ≤16B
// struct-IDENT fast path above doesn't cover — a 16B struct from a