wcc/cgen: #15 empty zero-length array emit — no spurious DATAW, cstage frame formula (wwstage)
An empty zero-length array diverged cs!=ww in asm (both ran correct=7): a [0]int global emitted a spurious DATAW main.X(SB),"", and a [0]int local reserved a $16 frame slot. cstage emits neither. wwstage-only, byte-id-only. The global DATAW emit is now gated on sz > 0 (skips the empty array). The local frame: localreserve dropped its sub-8 floor (if asz<8 asz=8) to mirror cstage's localslot formula (frame+sz+7)&~7 -- but that floor was MASKING slotsize(TY_VOID)=0 (a void local), which cstage defaults to 8B; removing the floor alone collided the zero-size void slot with a spilled param (a real miscompile -- 1132 self-compile hunks). So letslotsize now returns 8 for a void local, while empty-struct / [0]-array stay genuine 0. The frame formula is byte-id-neutral for every sz>=1 local (round8 already >= 8); only true zero-size cases change. cstage unchanged (w6c md5 unchanged). byte-id 990-997 8/8 (the full self-compile is what caught the void-local class); test/wcc/820 un-carves the #9 empty-[0] byte-id exclusion + adds void-local/local-[0] rows.
This commit is contained in:
@@ -18772,13 +18772,26 @@ export fn letslotsize(c: *cgen, n: *node) i32 = {
|
|||||||
// intercept here: the checker (inferarraylen, check.ww) stamps the
|
// intercept here: the checker (inferarraylen, check.ww) stamps the
|
||||||
// real element count onto the array type's length child before cgen
|
// real element count onto the array type's length child before cgen
|
||||||
// runs, so slotsize reads it like any explicit `[N]T` (#7).
|
// runs, so slotsize reads it like any explicit `[N]T` (#7).
|
||||||
if (n.lhs != nil) { return slotsize(c, n.lhs); };
|
// Annotation-less init: defer to the call's return type if we can
|
||||||
// Annotation-less init: defer to the call's return type if we
|
// infer it. Tagged-union returns need 24B; everything else matches
|
||||||
// can infer it. Tagged-union returns need 24B; everything else
|
// slotsize on the inferred type.
|
||||||
// matches slotsize on the inferred type.
|
let tn: *node = n.lhs;
|
||||||
let inferred: *node = inferletcalltype(c, n.rhs);
|
if (tn == nil) { tn = inferletcalltype(c, n.rhs); };
|
||||||
if (inferred != nil) { return slotsize(c, inferred); };
|
if (tn == nil) { return 8; };
|
||||||
return 8;
|
let s: i32 = slotsize(c, tn);
|
||||||
|
// #15: cstage cglet (cmd/w6c/cgen.c:12321) defaults a local's slot
|
||||||
|
// to 8 — only ARRAY/SLICE/STR/STRUCT/TUPLE/TAGGED take the real
|
||||||
|
// type size. slotsize returns 0 for a void/`done`-aliased scalar
|
||||||
|
// local; localreserve no longer applies a sub-8 floor (it mirrors
|
||||||
|
// cstage's no-floor localslot), so a void slot must be floored here
|
||||||
|
// instead, else its zero width collides with the next local. A
|
||||||
|
// genuine empty struct or [0]T array (also slotsize 0) keeps its 0.
|
||||||
|
if (s == 0) {
|
||||||
|
let ti: *tinfo = tn.type_: *tinfo;
|
||||||
|
if (ti != nil) { ti = tichase(ti); };
|
||||||
|
if (ti != nil && ti.kind == tykind.TY_VOID) { return 8; };
|
||||||
|
};
|
||||||
|
return s;
|
||||||
};
|
};
|
||||||
|
|
||||||
// #48 A.6.3d: AST walker retired. resolvewalk (check.ww:426-436) stamps
|
// #48 A.6.3d: AST walker retired. resolvewalk (check.ww:426-436) stamps
|
||||||
@@ -39161,12 +39174,17 @@ fn localalloc(c: *cgen, name: str, sz: i32, tnode: *node) i32 = {
|
|||||||
// (`let x = f(x)`) resolves x in the OUTER scope (Hare evals the init in
|
// (`let x = f(x)`) resolves x in the OUTER scope (Hare evals the init in
|
||||||
// the outer scope: harec check.c clet runs cexpr before scope_define).
|
// the outer scope: harec check.c clet runs cexpr before scope_define).
|
||||||
fn localreserve(c: *cgen, name: str, sz: i32, tnode: *node) *local = {
|
fn localreserve(c: *cgen, name: str, sz: i32, tnode: *node) *local = {
|
||||||
let asz: i32 = sz;
|
// #15: mirror cstage localslot (cmd/w6c/cgen.c:1900) —
|
||||||
if (asz < 8) { asz = 8; };
|
// `frame = (frame + size + 7) & ~7`, NO sub-8 floor. Identical to
|
||||||
if ((asz & 7) != 0) { asz = (asz + 7) & ~7; };
|
// the old `max(8, round8(sz))` accumulation for every sz>0 (frame
|
||||||
c.frame += asz;
|
// stays 8-aligned, so a 1..8B slot still costs 8); the only change
|
||||||
|
// is a zero-size slot (`[0]T`, void) adds 0, matching cstage's $0
|
||||||
|
// frame instead of over-reserving 8. local.sz is read only by the
|
||||||
|
// @-prefix grow-check in localadd, never for user lets, so storing
|
||||||
|
// the raw sz here is inert.
|
||||||
|
c.frame = (c.frame + sz + 7) & ~7;
|
||||||
let off: i32 = 0 - c.frame;
|
let off: i32 = 0 - c.frame;
|
||||||
let l: *local = alloc(local{name=name, off=off, sz=asz, tnode=tnode, lnext=nil})!;
|
let l: *local = alloc(local{name=name, off=off, sz=sz, tnode=tnode, lnext=nil})!;
|
||||||
return l;
|
return l;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -41626,7 +41644,14 @@ fn emitletdataw(c: *cgen, file: *node) void = {
|
|||||||
route = true;
|
route = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
if (route) {
|
// #15: a zero-length array (`[0]T`)
|
||||||
|
// has no bytes — cstage emits no DATA
|
||||||
|
// row; wwstage's unguarded emit produced
|
||||||
|
// a spurious `DATAW name(SB),""`. sz
|
||||||
|
// (letemitsize, cgen.ww:2758) is 0 for
|
||||||
|
// [0]T → skip. (Non-empty [N>0] arrays
|
||||||
|
// keep sz>0.)
|
||||||
|
if (route && sz > 0) {
|
||||||
emitarraydata(c, "DATAW", nm,
|
emitarraydata(c, "DATAW", nm,
|
||||||
d.nmod, dti, rh);
|
d.nmod, dti, rh);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -590,12 +590,17 @@ fn localalloc(c: *cgen, name: str, sz: i32, tnode: *node) i32 = {
|
|||||||
// (`let x = f(x)`) resolves x in the OUTER scope (Hare evals the init in
|
// (`let x = f(x)`) resolves x in the OUTER scope (Hare evals the init in
|
||||||
// the outer scope: harec check.c clet runs cexpr before scope_define).
|
// the outer scope: harec check.c clet runs cexpr before scope_define).
|
||||||
fn localreserve(c: *cgen, name: str, sz: i32, tnode: *node) *local = {
|
fn localreserve(c: *cgen, name: str, sz: i32, tnode: *node) *local = {
|
||||||
let asz: i32 = sz;
|
// #15: mirror cstage localslot (cmd/w6c/cgen.c:1900) —
|
||||||
if (asz < 8) { asz = 8; };
|
// `frame = (frame + size + 7) & ~7`, NO sub-8 floor. Identical to
|
||||||
if ((asz & 7) != 0) { asz = (asz + 7) & ~7; };
|
// the old `max(8, round8(sz))` accumulation for every sz>0 (frame
|
||||||
c.frame += asz;
|
// stays 8-aligned, so a 1..8B slot still costs 8); the only change
|
||||||
|
// is a zero-size slot (`[0]T`, void) adds 0, matching cstage's $0
|
||||||
|
// frame instead of over-reserving 8. local.sz is read only by the
|
||||||
|
// @-prefix grow-check in localadd, never for user lets, so storing
|
||||||
|
// the raw sz here is inert.
|
||||||
|
c.frame = (c.frame + sz + 7) & ~7;
|
||||||
let off: i32 = 0 - c.frame;
|
let off: i32 = 0 - c.frame;
|
||||||
let l: *local = alloc(local{name=name, off=off, sz=asz, tnode=tnode, lnext=nil})!;
|
let l: *local = alloc(local{name=name, off=off, sz=sz, tnode=tnode, lnext=nil})!;
|
||||||
return l;
|
return l;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3055,7 +3060,14 @@ fn emitletdataw(c: *cgen, file: *node) void = {
|
|||||||
route = true;
|
route = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
if (route) {
|
// #15: a zero-length array (`[0]T`)
|
||||||
|
// has no bytes — cstage emits no DATA
|
||||||
|
// row; wwstage's unguarded emit produced
|
||||||
|
// a spurious `DATAW name(SB),""`. sz
|
||||||
|
// (letemitsize, cgen.ww:2758) is 0 for
|
||||||
|
// [0]T → skip. (Non-empty [N>0] arrays
|
||||||
|
// keep sz>0.)
|
||||||
|
if (route && sz > 0) {
|
||||||
emitarraydata(c, "DATAW", nm,
|
emitarraydata(c, "DATAW", nm,
|
||||||
d.nmod, dti, rh);
|
d.nmod, dti, rh);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2417,13 +2417,26 @@ export fn letslotsize(c: *cgen, n: *node) i32 = {
|
|||||||
// intercept here: the checker (inferarraylen, check.ww) stamps the
|
// intercept here: the checker (inferarraylen, check.ww) stamps the
|
||||||
// real element count onto the array type's length child before cgen
|
// real element count onto the array type's length child before cgen
|
||||||
// runs, so slotsize reads it like any explicit `[N]T` (#7).
|
// runs, so slotsize reads it like any explicit `[N]T` (#7).
|
||||||
if (n.lhs != nil) { return slotsize(c, n.lhs); };
|
// Annotation-less init: defer to the call's return type if we can
|
||||||
// Annotation-less init: defer to the call's return type if we
|
// infer it. Tagged-union returns need 24B; everything else matches
|
||||||
// can infer it. Tagged-union returns need 24B; everything else
|
// slotsize on the inferred type.
|
||||||
// matches slotsize on the inferred type.
|
let tn: *node = n.lhs;
|
||||||
let inferred: *node = inferletcalltype(c, n.rhs);
|
if (tn == nil) { tn = inferletcalltype(c, n.rhs); };
|
||||||
if (inferred != nil) { return slotsize(c, inferred); };
|
if (tn == nil) { return 8; };
|
||||||
return 8;
|
let s: i32 = slotsize(c, tn);
|
||||||
|
// #15: cstage cglet (cmd/w6c/cgen.c:12321) defaults a local's slot
|
||||||
|
// to 8 — only ARRAY/SLICE/STR/STRUCT/TUPLE/TAGGED take the real
|
||||||
|
// type size. slotsize returns 0 for a void/`done`-aliased scalar
|
||||||
|
// local; localreserve no longer applies a sub-8 floor (it mirrors
|
||||||
|
// cstage's no-floor localslot), so a void slot must be floored here
|
||||||
|
// instead, else its zero width collides with the next local. A
|
||||||
|
// genuine empty struct or [0]T array (also slotsize 0) keeps its 0.
|
||||||
|
if (s == 0) {
|
||||||
|
let ti: *tinfo = tn.type_: *tinfo;
|
||||||
|
if (ti != nil) { ti = tichase(ti); };
|
||||||
|
if (ti != nil && ti.kind == tykind.TY_VOID) { return 8; };
|
||||||
|
};
|
||||||
|
return s;
|
||||||
};
|
};
|
||||||
|
|
||||||
// #48 A.6.3d: AST walker retired. resolvewalk (check.ww:426-436) stamps
|
// #48 A.6.3d: AST walker retired. resolvewalk (check.ww:426-436) stamps
|
||||||
|
|||||||
@@ -18772,13 +18772,26 @@ export fn letslotsize(c: *cgen, n: *node) i32 = {
|
|||||||
// intercept here: the checker (inferarraylen, check.ww) stamps the
|
// intercept here: the checker (inferarraylen, check.ww) stamps the
|
||||||
// real element count onto the array type's length child before cgen
|
// real element count onto the array type's length child before cgen
|
||||||
// runs, so slotsize reads it like any explicit `[N]T` (#7).
|
// runs, so slotsize reads it like any explicit `[N]T` (#7).
|
||||||
if (n.lhs != nil) { return slotsize(c, n.lhs); };
|
// Annotation-less init: defer to the call's return type if we can
|
||||||
// Annotation-less init: defer to the call's return type if we
|
// infer it. Tagged-union returns need 24B; everything else matches
|
||||||
// can infer it. Tagged-union returns need 24B; everything else
|
// slotsize on the inferred type.
|
||||||
// matches slotsize on the inferred type.
|
let tn: *node = n.lhs;
|
||||||
let inferred: *node = inferletcalltype(c, n.rhs);
|
if (tn == nil) { tn = inferletcalltype(c, n.rhs); };
|
||||||
if (inferred != nil) { return slotsize(c, inferred); };
|
if (tn == nil) { return 8; };
|
||||||
return 8;
|
let s: i32 = slotsize(c, tn);
|
||||||
|
// #15: cstage cglet (cmd/w6c/cgen.c:12321) defaults a local's slot
|
||||||
|
// to 8 — only ARRAY/SLICE/STR/STRUCT/TUPLE/TAGGED take the real
|
||||||
|
// type size. slotsize returns 0 for a void/`done`-aliased scalar
|
||||||
|
// local; localreserve no longer applies a sub-8 floor (it mirrors
|
||||||
|
// cstage's no-floor localslot), so a void slot must be floored here
|
||||||
|
// instead, else its zero width collides with the next local. A
|
||||||
|
// genuine empty struct or [0]T array (also slotsize 0) keeps its 0.
|
||||||
|
if (s == 0) {
|
||||||
|
let ti: *tinfo = tn.type_: *tinfo;
|
||||||
|
if (ti != nil) { ti = tichase(ti); };
|
||||||
|
if (ti != nil && ti.kind == tykind.TY_VOID) { return 8; };
|
||||||
|
};
|
||||||
|
return s;
|
||||||
};
|
};
|
||||||
|
|
||||||
// #48 A.6.3d: AST walker retired. resolvewalk (check.ww:426-436) stamps
|
// #48 A.6.3d: AST walker retired. resolvewalk (check.ww:426-436) stamps
|
||||||
@@ -39161,12 +39174,17 @@ fn localalloc(c: *cgen, name: str, sz: i32, tnode: *node) i32 = {
|
|||||||
// (`let x = f(x)`) resolves x in the OUTER scope (Hare evals the init in
|
// (`let x = f(x)`) resolves x in the OUTER scope (Hare evals the init in
|
||||||
// the outer scope: harec check.c clet runs cexpr before scope_define).
|
// the outer scope: harec check.c clet runs cexpr before scope_define).
|
||||||
fn localreserve(c: *cgen, name: str, sz: i32, tnode: *node) *local = {
|
fn localreserve(c: *cgen, name: str, sz: i32, tnode: *node) *local = {
|
||||||
let asz: i32 = sz;
|
// #15: mirror cstage localslot (cmd/w6c/cgen.c:1900) —
|
||||||
if (asz < 8) { asz = 8; };
|
// `frame = (frame + size + 7) & ~7`, NO sub-8 floor. Identical to
|
||||||
if ((asz & 7) != 0) { asz = (asz + 7) & ~7; };
|
// the old `max(8, round8(sz))` accumulation for every sz>0 (frame
|
||||||
c.frame += asz;
|
// stays 8-aligned, so a 1..8B slot still costs 8); the only change
|
||||||
|
// is a zero-size slot (`[0]T`, void) adds 0, matching cstage's $0
|
||||||
|
// frame instead of over-reserving 8. local.sz is read only by the
|
||||||
|
// @-prefix grow-check in localadd, never for user lets, so storing
|
||||||
|
// the raw sz here is inert.
|
||||||
|
c.frame = (c.frame + sz + 7) & ~7;
|
||||||
let off: i32 = 0 - c.frame;
|
let off: i32 = 0 - c.frame;
|
||||||
let l: *local = alloc(local{name=name, off=off, sz=asz, tnode=tnode, lnext=nil})!;
|
let l: *local = alloc(local{name=name, off=off, sz=sz, tnode=tnode, lnext=nil})!;
|
||||||
return l;
|
return l;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -41626,7 +41644,14 @@ fn emitletdataw(c: *cgen, file: *node) void = {
|
|||||||
route = true;
|
route = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
if (route) {
|
// #15: a zero-length array (`[0]T`)
|
||||||
|
// has no bytes — cstage emits no DATA
|
||||||
|
// row; wwstage's unguarded emit produced
|
||||||
|
// a spurious `DATAW name(SB),""`. sz
|
||||||
|
// (letemitsize, cgen.ww:2758) is 0 for
|
||||||
|
// [0]T → skip. (Non-empty [N>0] arrays
|
||||||
|
// keep sz>0.)
|
||||||
|
if (route && sz > 0) {
|
||||||
emitarraydata(c, "DATAW", nm,
|
emitarraydata(c, "DATAW", nm,
|
||||||
d.nmod, dti, rh);
|
d.nmod, dti, rh);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -56,16 +56,15 @@ runwait(const char *cmd)
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* beid — include this row in the cstage-vs-wwstage byte-id sweep. The
|
/* beid — include this row in the cstage-vs-wwstage byte-id sweep. Both the
|
||||||
* empty_zero row is EXCLUDED (beid=0): an empty `[0]int = []` module global
|
* empty_zero global (`let X:[0]int=[]`) and the empty_zero_local row now
|
||||||
* has a PRE-EXISTING cgen divergence unrelated to #9 (a checker-only fix) —
|
* converge byte-identically (beid=1): #15 closed the empty-`[0]T` cgen
|
||||||
* wwstage emits an extra zero-width `DATAW main.X(SB),""` row that cstage
|
* divergence — wwstage's spurious zero-width `DATAW main.X(SB),""` (cstage
|
||||||
* omits; both run identically (return 7). Filed as task #15 (empty-array-
|
* omits a zero-byte global) and its over-allocated `$16` local frame (cstage
|
||||||
* global DATA emission, NOT folded into #9). A LOCAL `[0]int = []` is no
|
* `$0` — a zero-length array reserves no slot) are both gated on the array
|
||||||
* escape — it diverges differently (wwstage allocates a $16 frame, cstage
|
* being non-empty. See .ai/rob-15-spec.md: cgen.ww emitletdataw gates the
|
||||||
* $0), so there is no byte-id-clean spelling of an empty zero-length array
|
* array DATAW on `sz > 0`; cgenutil.ww slotsize returns 0 for a TY_ARRAY of
|
||||||
* to restructure toward. The infer rows DO converge and pin that the
|
* alen==0. Both forms still run 7. */
|
||||||
* accept-path asm stays byte-identical. */
|
|
||||||
struct row { const char *label; const char *src; int want; int beid; };
|
struct row { const char *label; const char *src; int want; int beid; };
|
||||||
|
|
||||||
static const struct row rows[] = {
|
static const struct row rows[] = {
|
||||||
@@ -79,14 +78,39 @@ static const struct row rows[] = {
|
|||||||
20, 1 },
|
20, 1 },
|
||||||
|
|
||||||
/* empty_zero — a real zero-length array (`[0]int = []`) stays VALID.
|
/* empty_zero — a real zero-length array (`[0]int = []`) stays VALID.
|
||||||
* beid=0: pre-existing empty-array-global cgen divergence (see beid). */
|
* beid=1: #15 closed the empty-array-global DATAW divergence. */
|
||||||
{ "empty_zero",
|
{ "empty_zero",
|
||||||
"package main;\n"
|
"package main;\n"
|
||||||
"let X: [0]int = [];\n"
|
"let X: [0]int = [];\n"
|
||||||
"export fn main() i32 = {\n"
|
"export fn main() i32 = {\n"
|
||||||
"\treturn 7;\n"
|
"\treturn 7;\n"
|
||||||
"};\n",
|
"};\n",
|
||||||
7, 0 },
|
7, 1 },
|
||||||
|
|
||||||
|
/* empty_zero_local — a LOCAL zero-length array (`[0]int = []`) reserves
|
||||||
|
* no frame slot (#15: cstage `$0`, wwstage was `$16`). beid=1. */
|
||||||
|
{ "empty_zero_local",
|
||||||
|
"package main;\n"
|
||||||
|
"export fn main() i32 = {\n"
|
||||||
|
"\tlet x: [0]int = [];\n"
|
||||||
|
"\treturn 7;\n"
|
||||||
|
"};\n",
|
||||||
|
7, 1 },
|
||||||
|
|
||||||
|
/* void_local — a zero-SIZE (not zero-length) local: `done = void` sizes
|
||||||
|
* 0 via slotsize, but cstage cglet defaults a non-composite local to an
|
||||||
|
* 8B slot. #15 dropped localreserve's sub-8 floor, which had masked this
|
||||||
|
* — letslotsize now floors a void local to 8 (cstage parity). beid=1: a
|
||||||
|
* regression here (void slot 0) collides with the spilled param. */
|
||||||
|
{ "void_local",
|
||||||
|
"package main;\n"
|
||||||
|
"type done = void;\n"
|
||||||
|
"export fn main() i32 = {\n"
|
||||||
|
"\tlet d: done;\n"
|
||||||
|
"\tlet a: i32 = 7;\n"
|
||||||
|
"\treturn a;\n"
|
||||||
|
"};\n",
|
||||||
|
7, 1 },
|
||||||
|
|
||||||
/* infer_len — `[_]` infer is unaffected, `.len` reads the real count. */
|
/* infer_len — `[_]` infer is unaffected, `.len` reads the real count. */
|
||||||
{ "infer_len",
|
{ "infer_len",
|
||||||
|
|||||||
Reference in New Issue
Block a user