cgen: store the full register into padded scratch for a 3/5/6/7-byte aggregate tail (#10)
The in-cap aggregate-receive materialise emitted a single narrow tail MOV that fell to MOVB for a 3/5/6/7-byte sub-8 tail, storing one byte while the scratch->dest copy read the full tail from uninitialised scratch — silently dropping members at the C2c whole-element arm (arr[i]=mk()) and loud-stopping at the #11 field arm. The scratch slot is ceil-8 padded (local_alloc/localadd round to 8) and the copy reads only tsz bytes, so flipping the tail default MOVB->MOVQ stores the full register harmlessly into the slot's own pad (in-bounds for in-cap <=24B); 1/2/4-byte tails stay byte-identical. Both stages symmetric. Removes the now-redundant #11 sub-8-tail loud-stop (keeps the float #165 and over-cap #234 loud-stops). The same narrow-tail materialise recurs at 6 other cstage sites (task #14). Retires the obsolete idx_dot_aggret_subtail_loud //ww:error fixture (both stages now compile the case) and converts it to a positive cstage run-test; the struct-field shape is byte-id-divergent only via the pre-existing #9 frame-size bug, so the value pin uses array-field shapes. Value-asserting, reddens under each stage's independent revert.
This commit is contained in:
82
test/lang/aggregate_tail_test.ww
Normal file
82
test/lang/aggregate_tail_test.ww
Normal file
@@ -0,0 +1,82 @@
|
||||
// aggregate_tail_test — in-cap aggregate-receive sub-8 tail materialise (#10).
|
||||
// An in-cap (<=24B) aggregate-returning CALL received into an INDEXED dest is a
|
||||
// two-step: (1) materialise the AX/DX/CX return regs into a frame scratch, (2)
|
||||
// word-copy scratch -> dest. Step 1's sub-8 TAIL stored a single narrow MOV
|
||||
// picked by (tail==4)?MOVL:(tail==2)?MOVW:MOVB, so for tail in {3,5,6,7} it fell
|
||||
// to MOVB: one byte written while the copy reads the FULL tail, so the high tail
|
||||
// bytes stayed uninitialised => dropped members. Two arms shared the gap — the
|
||||
// C2c whole-element arr[i]=mk() (SILENT both stages, byte-id-BLIND: both emitted
|
||||
// IDENTICAL wrong asm) and the #11 field-of-indexed arr[i].f=mk() (LOUD-STOPped).
|
||||
// The fix stores the FULL register (MOVQ) into the ceil-8-padded scratch; the
|
||||
// over-stored bytes die in the pad and the copy reads only the real size.
|
||||
// A repro element must be align-<=2 UNPADDED: [7]i16 = 14B keeps a real tail-6;
|
||||
// an i64-bearing struct pads to 16B (tail 0) and never trips it. PRIMITIVE-only
|
||||
// asserts (deref_callarg lineage): a dropped word fails. The C2c value assert is
|
||||
// the SOLE tooth there (byte-id blind); the #11 case re-arms a compile-error
|
||||
// loud-stop on revert. Distinct nonzero per-element values so a stale read (the
|
||||
// fully-dropped members read uninitialised, 0 on a fresh frame) mismatches.
|
||||
|
||||
package aggregate_tail_test;
|
||||
|
||||
type s = struct { f: [7]i16, g: i16 }; // f at off 0 (14B tail-6), g at off 14
|
||||
|
||||
fn mk7() [7]i16 = { return [10i16, 20i16, 30i16, 40i16, 50i16, 60i16, 70i16]; };
|
||||
fn mk11() [11]u8 = { return [1u8,2u8,3u8,4u8,5u8,6u8,7u8,8u8,9u8,10u8,11u8]; };
|
||||
fn mk13() [13]u8 = { return [1u8,2u8,3u8,4u8,5u8,6u8,7u8,8u8,9u8,10u8,11u8,12u8,13u8]; };
|
||||
fn mk15() [15]u8 = { return [1u8,2u8,3u8,4u8,5u8,6u8,7u8,8u8,9u8,10u8,11u8,12u8,13u8,14u8,15u8]; };
|
||||
|
||||
@test fn c2c_whole_element() void = {
|
||||
let arr: [3][7]i16;
|
||||
arr[1] = mk7();
|
||||
assert(arr[1][0] == 10i16);
|
||||
assert(arr[1][1] == 20i16);
|
||||
assert(arr[1][2] == 30i16);
|
||||
assert(arr[1][3] == 40i16);
|
||||
assert(arr[1][4] == 50i16); // eb1 byte 0-1
|
||||
assert(arr[1][5] == 60i16); // eb1 byte 2-3 — dropped by the buggy MOVB tail
|
||||
assert(arr[1][6] == 70i16); // eb1 byte 4-5 — dropped (fully-uninit tooth)
|
||||
};
|
||||
|
||||
@test fn field_of_indexed() void = {
|
||||
let arr: [3]s;
|
||||
arr[1].g = 999i16; // neighbour set first
|
||||
arr[1].f = mk7();
|
||||
assert(arr[1].f[0] == 10i16);
|
||||
assert(arr[1].f[4] == 50i16);
|
||||
assert(arr[1].f[5] == 60i16);
|
||||
assert(arr[1].f[6] == 70i16);
|
||||
assert(arr[1].g == 999i16); // tail MOVQ writes the padded SCRATCH, not g
|
||||
};
|
||||
|
||||
// The STRUCT-field-of-indexed variant ({t14,pad} struct field) is NOT pinned here:
|
||||
// #10's materialise is byte-id clean for it, but the [N]box local frame size
|
||||
// diverges cs!=ww (task #9, slotsize-vs-natural) — benign (both stages correct)
|
||||
// yet byte-id-RED, so it stays out of the T2 corpus. Positive cstage coverage of
|
||||
// that shape lives in test/wcc/data/idx_dot_aggret_subtail_run (converted from the
|
||||
// retired loud-stop tripwire).
|
||||
|
||||
@test fn tail3() void = {
|
||||
let a: [2][11]u8;
|
||||
a[1] = mk11();
|
||||
assert(a[1][0] == 1u8);
|
||||
assert(a[1][7] == 8u8);
|
||||
assert(a[1][8] == 9u8); // tail byte 0 (MOVB wrote only this)
|
||||
assert(a[1][9] == 10u8); // tail byte 1 — dropped
|
||||
assert(a[1][10] == 11u8); // tail byte 2 — dropped
|
||||
};
|
||||
|
||||
@test fn tail5() void = {
|
||||
let a: [2][13]u8;
|
||||
a[1] = mk13();
|
||||
assert(a[1][0] == 1u8);
|
||||
assert(a[1][8] == 9u8);
|
||||
assert(a[1][12] == 13u8); // tail byte 4 — dropped
|
||||
};
|
||||
|
||||
@test fn tail7() void = {
|
||||
let a: [2][15]u8;
|
||||
a[1] = mk15();
|
||||
assert(a[1][0] == 1u8);
|
||||
assert(a[1][8] == 9u8);
|
||||
assert(a[1][14] == 15u8); // tail byte 6 — dropped
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
//ww:error "3/5/6/7-byte sub-8 tail unwired"
|
||||
// #11 tripwire: an in-cap aggregate-returning CALL into an aggregate field of
|
||||
// an indexed element where the field has a 3/5/6/7-byte sub-8 tail (here 14B
|
||||
// 7xi16, tail=6). The materialise's single narrow MOV stores only one tail byte
|
||||
// while the copy reads the full tail -> a SILENT both-stage member drop. Both
|
||||
// stages LOUD-STOP until a general register->scratch tail lands (rule 7,
|
||||
// C2c-shared). SUCCESS = the loud-stop regressed to a silent miscompile.
|
||||
package main;
|
||||
type t14 = struct { a:i16,b:i16,c:i16,d:i16,e:i16,f:i16,g:i16 };
|
||||
type s14 = struct { x: t14, pad: i16 };
|
||||
fn mk() t14 = { return t14{a=1i16,b=2i16,c=3i16,d=4i16,e=5i16,f=6i16,g=7i16}; };
|
||||
export fn main() i32 = {
|
||||
let arr: [2]s14;
|
||||
arr[1].x = mk();
|
||||
return 0;
|
||||
};
|
||||
29
test/wcc/data/idx_dot_aggret_subtail_run/case.ww
Normal file
29
test/wcc/data/idx_dot_aggret_subtail_run/case.ww
Normal file
@@ -0,0 +1,29 @@
|
||||
//ww:run
|
||||
// #10 positive (converted from the retired idx_dot_aggret_subtail_loud tripwire):
|
||||
// an in-cap aggregate-returning CALL into a 14B-tail-6 STRUCT field of an indexed
|
||||
// element (arr[i].x = mk()). Pre-#10 both stages LOUD-STOPped this 3/5/6/7-tail
|
||||
// materialise; #10 stores the FULL register into the ceil-8-padded scratch, so the
|
||||
// over-stored bytes die in the pad and every member round-trips. main returns 0
|
||||
// only if all members AND the neighbour pad survived (else a distinct nonzero).
|
||||
// cstage-only RUN by harness design (T1): this shape's [N]s14 local frame size
|
||||
// diverges cs!=ww (task #9, slotsize-vs-natural, benign — both stages compute
|
||||
// correct values) so it cannot enter the T2 byte-id corpus. Reverting #10 re-arms
|
||||
// the loud-stop -> this case fails to compile (rc!=0) -> reddens.
|
||||
package main;
|
||||
type t14 = struct { a:i16,b:i16,c:i16,d:i16,e:i16,f:i16,g:i16 };
|
||||
type s14 = struct { x: t14, pad: i16 };
|
||||
fn mk() t14 = { return t14{a=10i16,b=20i16,c=30i16,d=40i16,e=50i16,f=60i16,g=70i16}; };
|
||||
export fn main() i32 = {
|
||||
let arr: [2]s14;
|
||||
arr[1].pad = 999i16;
|
||||
arr[1].x = mk();
|
||||
if (arr[1].x.a != 10i16) { return 1; };
|
||||
if (arr[1].x.b != 20i16) { return 2; };
|
||||
if (arr[1].x.c != 30i16) { return 3; };
|
||||
if (arr[1].x.d != 40i16) { return 4; };
|
||||
if (arr[1].x.e != 50i16) { return 5; };
|
||||
if (arr[1].x.f != 60i16) { return 6; }; // tail byte 0-1
|
||||
if (arr[1].x.g != 70i16) { return 7; }; // tail byte 2-3 (dropped pre-#10)
|
||||
if (arr[1].pad != 999i16) { return 8; }; // tail MOVQ must not smash pad
|
||||
return 0;
|
||||
};
|
||||
Reference in New Issue
Block a user