wcc/check: #24 reject composite-element tuple (array/struct/tuple), declared+inferred, both stages

A tuple whose element chases to TY_ARRAY/STRUCT/TUPLE (>8B) silently
miscompiled both stages: t.0[i] read segfaulted and construction dropped
the payload into the 8B slot. Reject the type at resolution (DISP-B);
faithful inline layout deferred to #60. cstage resolve_type N_TTUPLE
(declared) + N_TUPLE expr (inferred literal, was a cstage-only silent
miscompile + cs!=ww asymmetry); wwstage tinfofornode covers both.
test/wcc/832 + 941 migrated.
This commit is contained in:
2026-06-09 17:29:11 +09:00
parent f1cd0a3555
commit 785fe342fa
6 changed files with 144 additions and 33 deletions

View File

@@ -725,6 +725,18 @@ resolve_type(Checker *c, Node *n)
* packed-tuple miscompile family (#32/#33/#48). */
if (tp->type) {
Type *eu = type_chase_named(tp->type);
/* #24: a composite element (array/struct/nested
* tuple >8B) cannot ride the 8B cursor slot — the
* #60 layout drops it on construction and segvs on
* t.N[i] read. Reject until #60/DISP-A inlines it.
* Hare allows it (harec type_store.c anon-struct). */
if (eu && (eu->kind == TY_ARRAY
|| eu->kind == TY_STRUCT
|| eu->kind == TY_TUPLE))
err(c, n->pos, "tuple element must be a "
"scalar, str, slice, or tagged-union "
"(composite element deferred to task "
"#60)");
if (eu && (eu->kind == TY_STR
|| eu->kind == TY_SLICE
|| eu->kind == TY_TAGGED))
@@ -2182,6 +2194,20 @@ cexpr(Checker *c, Node *n)
Type *ed = type_default(tp->type);
if (ed && ed->align > al) al = ed->align;
Type *eu = type_chase_named(ed);
/* #24: an INFERRED composite element (array/struct/
* nested tuple >8B) drops on construction + segvs on
* the t.N read, exactly as the explicit N_TTUPLE twin
* (resolve_type) — wwstage's tinfofornode choke-point
* catches declared AND inferred, so cstage must reject
* the inferred literal here too (rule-10 symmetry).
* Reject until #60/DISP-A inlines it. */
if (eu && (eu->kind == TY_ARRAY
|| eu->kind == TY_STRUCT
|| eu->kind == TY_TUPLE))
err(c, n->pos, "tuple element must be a "
"scalar, str, slice, or tagged-union "
"(composite element deferred to task "
"#60)");
if (eu && (eu->kind == TY_STR || eu->kind == TY_SLICE
|| eu->kind == TY_TAGGED))
sz += (eu->size + 7) & ~(u64)7;

View File

@@ -12245,6 +12245,18 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = {
let pt: *tinfo = tinfofornode(c, p.lhs);
// #62/#69: tuple-member value cycle — loud, cstage twin.
if (circularnamed(c, pt, p.lhs)) { pt = c.tc.tyerr; };
// #24: a composite element (array/struct/nested tuple
// >8B) cannot ride the 8B cursor slot — the #60 layout
// drops it on construction and segvs on t.N[i] read.
// Reject until #60/DISP-A inlines it; cstage twin.
let cu: *tinfo = tichase(pt);
if (cu != nil && (cu.kind == tykind.TY_ARRAY
|| cu.kind == tykind.TY_STRUCT
|| cu.kind == tykind.TY_TUPLE)) {
cerr("error: tuple element must be a scalar, str, slice, or tagged-union (composite element deferred to task #60)\n");
c.errs += 1;
pt = c.tc.tyerr;
};
let te: *ttupleelem = alloc(ttupleelem{type_=pt, offset=slottotal, tnext=nil})!;
if (teh == nil) { teh = te; } else { tet.tnext = te; };
tet = te;

View File

@@ -1964,6 +1964,18 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = {
let pt: *tinfo = tinfofornode(c, p.lhs);
// #62/#69: tuple-member value cycle — loud, cstage twin.
if (circularnamed(c, pt, p.lhs)) { pt = c.tc.tyerr; };
// #24: a composite element (array/struct/nested tuple
// >8B) cannot ride the 8B cursor slot — the #60 layout
// drops it on construction and segvs on t.N[i] read.
// Reject until #60/DISP-A inlines it; cstage twin.
let cu: *tinfo = tichase(pt);
if (cu != nil && (cu.kind == tykind.TY_ARRAY
|| cu.kind == tykind.TY_STRUCT
|| cu.kind == tykind.TY_TUPLE)) {
cerr("error: tuple element must be a scalar, str, slice, or tagged-union (composite element deferred to task #60)\n");
c.errs += 1;
pt = c.tc.tyerr;
};
let te: *ttupleelem = alloc(ttupleelem{type_=pt, offset=slottotal, tnext=nil})!;
if (teh == nil) { teh = te; } else { tet.tnext = te; };
tet = te;

View File

@@ -12245,6 +12245,18 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = {
let pt: *tinfo = tinfofornode(c, p.lhs);
// #62/#69: tuple-member value cycle — loud, cstage twin.
if (circularnamed(c, pt, p.lhs)) { pt = c.tc.tyerr; };
// #24: a composite element (array/struct/nested tuple
// >8B) cannot ride the 8B cursor slot — the #60 layout
// drops it on construction and segvs on t.N[i] read.
// Reject until #60/DISP-A inlines it; cstage twin.
let cu: *tinfo = tichase(pt);
if (cu != nil && (cu.kind == tykind.TY_ARRAY
|| cu.kind == tykind.TY_STRUCT
|| cu.kind == tykind.TY_TUPLE)) {
cerr("error: tuple element must be a scalar, str, slice, or tagged-union (composite element deferred to task #60)\n");
c.errs += 1;
pt = c.tc.tyerr;
};
let te: *ttupleelem = alloc(ttupleelem{type_=pt, offset=slottotal, tnext=nil})!;
if (teh == nil) { teh = te; } else { tet.tnext = te; };
tet = te;

View File

@@ -20,6 +20,17 @@
* asm; selfhost has no overlong tuple-elements, so 990-997 byte-id is
* untouched. Do NOT chase message parity.
*
* #24 (DISP-B broad reject, rob spec .ai/rob-24-spec.md): a tuple whose
* ELEMENT is a composite (array / struct / nested-tuple >8B) cannot ride the
* 8B cursor slot (#60 layout) — it silently DROPS on construction and SEGVs
* on the t.N[i] read. Both stages now REJECT such a type at N_TTUPLE
* resolution (kind ∈ {TY_ARRAY, TY_STRUCT, TY_TUPLE} after TY_NAMED chase),
* converting two silent miscompiles into one loud checker error. This FLIPS
* the former tuple_arr_exact / tuple_nested_exact positive controls to the
* neg table (their types are now outlawed) and adds slice/str/tagged-element
* positive controls proving DISP-B does NOT over-reject the inline-header
* kinds. Full inline support deferred to task #60 / DISP-A.
*
* neg row | shape | gate
* -------------------+----------------------------------------------+--------
* tuple_arr_over | let t:([2]int,i32)=([1,2,3],5) | b. FAIL
@@ -27,12 +38,16 @@
* tuple_in_tuple | let t:([2]int,([2]int,i32))=([..],([1,2,3],.))| #26 FAIL
* tuple_return_over | fn()([2]int,i32){return([1,2,3],5)} | #25 FAIL
* tuple_return_nested| fn()([2]int,([2]int,i32)){return(..,([..3],.))| #25 FAIL
* tuple_arr_exact | let t:([2]int,i32)=([1,2],5) | #24 FAIL
* tuple_nested_exact | let t:(i32,([2]int,i32))=(9,([3,4],7)) | #24 FAIL
* tuple_struct_elem | type P=struct{x:int}; let t:(P,i32)=(P{x=1},5)| #24 FAIL
*
* pos row | shape | want
* -------------------+----------------------------------------------+------
* tuple_arr_exact | let t:([2]int,i32)=([1,2],5); t.1 | 5
* tuple_scalar | let t:(i32,i32)=(1,2); t.1 | 2
* tuple_nested_exact | let t:(i32,([2]int,i32))=(9,([3,4],7)); t.0 | 9
* tuple_slice_elem | let t:([]u8,i32)=(a,5); t.1 | 5
* tuple_str_elem | let t:(str,i32)=("hi",7); t.1 | 7
* tuple_tagged_elem | let t:((void|size),i32)=(3,9); t.1 | 9
*/
#include <stdio.h>
#include <stdlib.h>
@@ -53,22 +68,7 @@ runwait(const char *cmd)
struct row { const char *label; const char *src; int want; };
static const struct row rows[] = {
/* exact-length array element in a tuple still ACCEPTS, builds + runs.
* Readout is the scalar t.1 (=5), NOT t.0[1]: indexing an array
* element THROUGH a tuple is a separate pre-existing cgen read bug
* that segfaults on BOTH stages (w6c byte-identical, so not a #20
* regression — filed). The point of this control is that the #20
* over-fill walk does NOT over-reject the valid exact-length tuple-
* with-array-element: the build must succeed and the program run. */
{ "tuple_arr_exact",
"package main;\n"
"export fn main() i32 = {\n"
"\tlet t: ([2]int, i32) = ([1, 2], 5);\n"
"\treturn t.1;\n"
"};\n",
5 },
/* a scalar-only tuple has no array element — the #20 walk no-ops. */
/* a scalar-only tuple — all elements ride the 8B slot. */
{ "tuple_scalar",
"package main;\n"
"export fn main() i32 = {\n"
@@ -77,19 +77,36 @@ static const struct row rows[] = {
"};\n",
2 },
/* #26 positive control — a VALID nested tuple with an exact-length
* inner [2]int must NOT be over-rejected by the new recursion arm.
* Readout is the top-level scalar t.0 (=9), NOT a leaf through the
* inner tuple / array element (those hit pre-existing cgen read bugs
* that miscompile on BOTH stages — filed, byte-id-blind, see the
* tuple_arr_exact note). The point here is that checktuplearrfits
* recurses the inner ([2]int,i32), runs the count check, and lets the
* exact-length build proceed: build succeeds + program runs. */
{ "tuple_nested_exact",
/* #24 positive control — a SLICE element is DISP-B-allowed (its 24B
* header rides the cursor). Must NOT be over-rejected. Readout is the
* scalar t.1 (=5). */
{ "tuple_slice_elem",
"package main;\n"
"export fn main() i32 = {\n"
"\tlet t: (i32, ([2]int, i32)) = (9, ([3, 4], 7));\n"
"\treturn t.0;\n"
"\tlet hb: [8]u8;\n"
"\tlet a: []u8; a.ptr = &hb[0]; a.len = 3; a.cap = 8;\n"
"\tlet t: ([]u8, i32) = (a, 5);\n"
"\treturn t.1;\n"
"};\n",
5 },
/* #24 positive control — a STR element is DISP-B-allowed (24B header).
* Readout is the scalar t.1 (=7). */
{ "tuple_str_elem",
"package main;\n"
"export fn main() i32 = {\n"
"\tlet t: (str, i32) = (\"hi\", 7);\n"
"\treturn t.1;\n"
"};\n",
7 },
/* #24 positive control — a TAGGED-UNION element is DISP-B-allowed (its
* tag+payload box rides the slot). Readout is the scalar t.1 (=9). */
{ "tuple_tagged_elem",
"package main;\n"
"export fn main() i32 = {\n"
"\tlet t: ((void | size), i32) = (3, 9);\n"
"\treturn t.1;\n"
"};\n",
9 },
};
@@ -137,6 +154,29 @@ static const char *neg[] = {
"\tlet t = f();\n"
"\treturn t.0[0]: i32;\n"
"};\n",
/* tuple_arr_exact (#24) — was a GREEN positive control; the DISP-B
* broad reject now OUTLAWS an ARRAY tuple element (silent-drop on
* construction + segv on t.0[i] read). MIGRATED to the neg table. */
"package main;\n"
"export fn main() i32 = {\n"
"\tlet t: ([2]int, i32) = ([1, 2], 5);\n"
"\treturn t.1;\n"
"};\n",
/* tuple_nested_exact (#24) — was a GREEN positive control; a NESTED
* TUPLE element is now outlawed by DISP-B. MIGRATED to the neg table. */
"package main;\n"
"export fn main() i32 = {\n"
"\tlet t: (i32, ([2]int, i32)) = (9, ([3, 4], 7));\n"
"\treturn t.0;\n"
"};\n",
/* tuple_struct_elem (#24) — a STRUCT tuple element is now a checker
* loud (was a cgen "unsupported field-read shape" loud). */
"package main;\n"
"type P = struct { x: int };\n"
"export fn main() i32 = {\n"
"\tlet t: (P, i32) = (P { x = 1 }, 5);\n"
"\treturn t.1;\n"
"};\n",
};
static int

View File

@@ -1368,8 +1368,11 @@ static const struct row rows[] = {
/* rule-7 (ken demand 1): a NESTED composite element (tuple-in-
* tuple here) occupies more than the one GP word the restage walk
* counts — pre-guard the checker accepted it and it ran WRONG
* (inner words skewed; wwstage SIGSEGV'd). Loud until a consumer
* motivates the wiring. */
* (inner words skewed; wwstage SIGSEGV'd). #24 now outlaws the
* composite-element tuple TYPE at resolution (both stages), so the
* reject fires here before the later "tuple arg element kind
* unsupported" arg-pass loud — experr migrated to the #24 text.
* Full inline support deferred to task #60. */
{ "t2_reject_nested_elem_arg",
"package main;\n"
"fn f(t: ((i64, i64), i64)) i64 = {\n"
@@ -1381,7 +1384,7 @@ static const struct row rows[] = {
" if (f(t) != 9) { return 1; };\n"
" return 0;\n"
"};\n", 0,
K_BUILDERR, "tuple arg element kind unsupported" },
K_BUILDERR, "tuple element must be a scalar" },
/* variadic-of-tuples ((i64,i64)...): `ts[0].0` is an indexed-slice
* tuple-element FIELD read — #121 leg (a) now resolves it (was LOUD
* "unsupported field-read shape"; the dispatch fired only for an
@@ -1920,13 +1923,19 @@ static const struct row rows[] = {
"};\n", 0, K_RUN, NULL },
/* nested tuple element: slot rule gives an inner tuple one 8B
* eightbyte (tuple_eslot parity) — pins the outer layout. */
/* #24: an INFERRED nested-tuple element `((4,2),36)` drops the inner
* tuple on construction (build-proved 76 not 42 reading t.0) and is now
* outlawed on BOTH stages — same DISP-B reject as 832's tuple_nested_-
* exact, but via the inferred-literal path (check.c N_TUPLE expr +
* wwstage tinfofornode). Migrated K_RUN -> K_BUILDERR. Full inline
* support deferred to task #60. */
{ "t44_inferred_nested",
"package main;\n"
"export fn main() i32 = {\n"
" let t = ((4, 2), 36);\n"
" if (t.1 != 36) { return 1; };\n"
" return 0;\n"
"};\n", 0, K_RUN, NULL },
"};\n", 0, K_BUILDERR, "tuple element must be a scalar" },
/* #57: an IN-CAP tuple literal's cursor fill keyed each element on
* its STAMPED type (element-constructed, check.c N_TUPLE), so a
* declared-TAGGED element from a concrete rvalue (`(5: size, 9)`)