diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 8c71cea2..5460a4e1 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -5810,6 +5810,178 @@ cgexpr(Cg *c, Node *n, Local *locals) amem(D_BX, foff + 8)); break; } + /* #11: an in-cap aggregate-returning CALL into + * an AGGREGATE field of an indexed element + * `arr[i].f = mk()`. The scalar default below + * stores only AX (eb0), dropping DX/CX — a SILENT + * both-stage field-drop, the field-of-indexed twin + * of C2c's whole-element arr[i]=mk() arm + * (cgen.c :6764). Scratch-first materialise of the + * AX/DX/CX return (not a PUSHQ spill — keeps the + * CALL at the frame's 16B alignment and survives an + * idx that itself contains a call), reuse the scalar + * arm's &arr[i]->BX address computation verbatim, + * then word-copy scratch to foff(BX). In-cap only + * (cg_sret_retsize==0); over-cap sret-into-field + * LOUD-STOPS (#11c/#234, task #8) and a float- + * bearing aggregate LOUD-STOPS (#165/#171 — a pure- + * float return eightbyte rides X0/X1 which the GP + * AX/DX/CX cursor cannot read). Mirrors wwstage + * cgenexpr.ww. */ + if (n->op == TK_ASSIGN && n->rhs + && n->rhs->kind == N_CALL) { + if (cg_sret_retsize(ft) > 0) + fatal("#11c/#234: over-cap " + "(sret) aggregate field " + "receive arr[i].f=mk() " + "unwired (cs!=ww; task #8)"); + /* struct_float_class mirrors the + * return-side SSE routing (#171a); a + * field typed DIRECTLY as a tuple misses + * it yet the bare-tuple return routes + * floats to tuple_sse_seq — guard it too + * so neither stage silently stores X0 + * garbage through the GP cursor. */ + int sclass11[2]; + int sse11 = struct_float_class(ft, + sclass11) > 0; + if (!sse11) { + Type *tu11 = + type_chase_named(ft); + if (tu11 && tu11->kind + == TY_TUPLE) + for (Tparam *p11 = + tu11->params; p11; + p11 = p11->next) { + int f32_11; + if (fld_isfloat( + p11->type, + &f32_11)) + sse11 = 1; + } + } + if (sse11) + fatal("#11/#165: float-bearing " + "aggregate field receive " + "arr[i].f=mk() unwired (SSE " + "return eightbyte; #171)"); + if (fsz > 8) { + /* A 3/5/6/7-byte sub-8 tail cannot be + * materialised by the single narrow MOV + * below — it stores ONE byte while the + * copy reads the full tail, dropping the + * rest from uninitialised scratch (a + * silent both-stage drop; empirically a + * 14B 7xi16 field loses f/g). The whole- + * element C2c sibling (:6764) shares this + * single-tail gap; until a general + * register->scratch tail (shift cascade) + * lands across BOTH sites, LOUD-STOP + * rather than silently drop — rule 7, the + * sibling of the over-cap/float stops + * above. Tails 0/1/2/4 are exact and flow + * through. Mirrors wwstage cgenexpr.ww. */ + int tl11 = fsz % 8; + if (tl11 == 3 || tl11 == 5 + || tl11 == 6 || tl11 == 7) + fatal("#11: aggregate field receive " + "arr[i].f=mk() with a 3/5/6/7-byte " + "sub-8 tail unwired (materialise " + "single-MOV under-stores; " + "C2c-shared)"); + int scr11 = cg_tagscr_slot(c, + &locals, fsz); + cgexpr(c, n->rhs, locals); + int regs11[3] = { D_AX, D_DX, + D_CX }; + int full11 = fsz / 8; + int tail11 = fsz % 8; + for (int i11 = 0; + i11 < full11; i11++) + ins2(c, A_MOVQ, + areg(regs11[i11]), + amem(D_BP, scr11 + + i11 * 8)); + if (tail11 > 0) { + int op11 = (tail11 == 4) + ? A_MOVL : (tail11 + == 2) ? A_MOVW + : A_MOVB; + ins2(c, op11, + areg(regs11[full11]), + amem(D_BP, scr11 + + full11 * 8)); + } + cgexpr(c, idx, locals); + if (esz > 1) { + ins2(c, A_MOVQ, + aimm(esz), + areg(D_CX)); + ins2(c, A_IMULQ, + areg(D_CX), + areg(D_AX)); + } + if (is_arr) + ins2(c, A_LEAQ, + amem(D_BP, off), + areg(D_BX)); + else + ins2(c, A_MOVQ, + amem(D_BP, off), + areg(D_BX)); + ins2(c, A_ADDQ, + areg(D_AX), areg(D_BX)); + if (viaptr) + ins2(c, A_MOVQ, + amem(D_BX, 0), + areg(D_BX)); + int k11 = 0; + for (; k11 + 8 <= fsz; + k11 += 8) { + ins2(c, A_MOVQ, + amem(D_BP, scr11 + + k11), areg(D_AX)); + ins2(c, A_MOVQ, + areg(D_AX), + amem(D_BX, + foff + k11)); + } + if (k11 + 4 <= fsz) { + ins2(c, A_MOVL, + amem(D_BP, scr11 + + k11), areg(D_AX)); + ins2(c, A_MOVL, + areg(D_AX), + amem(D_BX, + foff + k11)); + k11 += 4; + } + if (k11 + 2 <= fsz) { + ins2(c, A_MOVW, + amem(D_BP, scr11 + + k11), areg(D_AX)); + ins2(c, A_MOVW, + areg(D_AX), + amem(D_BX, + foff + k11)); + k11 += 2; + } + if (k11 + 1 <= fsz) { + ins2(c, A_MOVB, + amem(D_BP, scr11 + + k11), areg(D_AX)); + ins2(c, A_MOVB, + areg(D_AX), + amem(D_BX, + foff + k11)); + k11 += 1; + } + break; + } + /* fsz<=8 in-cap aggregate returns + * wholly in AX; the scalar default's + * single store is the correct receive. */ + } if (n->op == TK_ASSIGN) { cgexpr(c, n->rhs, locals); ins1(c, A_PUSHQ, diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index f6f5123f..28256854 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -9984,6 +9984,185 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { emitline("\n"); return; }; + // #11: an in-cap aggregate-returning CALL into + // an AGGREGATE field of an indexed element + // `arr[i].f = mk()`. The scalar default below + // stores only AX (eb0), dropping DX/CX — a + // SILENT both-stage field-drop, the field-of- + // indexed twin of C2c's whole-element + // arr[i]=mk() arm (cgenexpr.ww :9100). Scratch- + // first materialise of the AX/DX/CX return (not + // a PUSHQ spill — keeps the CALL at the frame's + // 16B alignment and survives an idx that itself + // contains a call), reuse the scalar arm's + // &arr[i]->BX address computation verbatim, then + // word-copy scratch to fi.foff(BX). tsz from the + // type table (fieldsize, rule 13). In-cap only + // (callsretsize==0); over-cap sret-into-field + // LOUD-STOPS (#11c/#234, task #8) and a float- + // bearing aggregate LOUD-STOPS (#165/#171 — a + // pure-float return eightbyte rides X0/X1 which + // the GP AX/DX/CX cursor cannot read). Mirrors + // cstage cgen.c. + if (n.op == syntax.tkind.TK_ASSIGN + && n.rhs != nil + && n.rhs.kind == syntax.nkind.N_CALL) { + if (callsretsize(c, n.rhs) > 0) { + let m11o: str = "#11c/#234: over-cap (sret) aggregate field receive arr[i].f=mk() unwired (cs!=ww; task #8)\n"; + os.write(2, m11o.ptr, m11o.len: u64); + os.exit(1); + }; + // structfloatclass mirrors the return- + // side SSE routing (cgenstmt.ww #171a); + // a field typed DIRECTLY as a tuple + // misses it (non-N_TNAME) yet the bare- + // tuple return routes floats to tupsse — + // guard it too so neither stage silently + // stores X0 garbage through the GP cursor. + let sse11: bool = structfloatclass(c, fi.tnode) != 0; + if (!sse11) { + let rt11: *syntax.node = resolvetype(c, fi.tnode); + if (rt11 != nil) { if (rt11.kind == syntax.nkind.N_TTUPLE) { + let q11: *syntax.node = rt11.list; + for (q11 != nil) { + if (isfloattype(c, q11.lhs)) { sse11 = true; }; + q11 = q11.next; + }; + };}; + }; + if (sse11) { + let m11f: str = "#11/#165: float-bearing aggregate field receive arr[i].f=mk() unwired (SSE return eightbyte; #171)\n"; + os.write(2, m11f.ptr, m11f.len: u64); + os.exit(1); + }; + // fi.fsz is the field's NATURAL size + // (tf.type_.size, cgenutil.ww :2736) — + // the byte-id twin of cstage's + // fsz=ft->size. fieldsize() returns the + // slot-PADDED size (16 for a 12B + // 3×i32), which both diverges from + // cstage AND a full-MOVQ tail on it + // would smash the next field (g at +12). + let tsz: i32 = fi.fsz; + if (tsz > 8) { + // A 3/5/6/7-byte sub-8 tail cannot + // be materialised by the single narrow + // MOV below — it stores ONE byte while + // the copy reads the full tail, dropping + // the rest from uninitialised scratch (a + // silent both-stage drop; empirically a + // 14B 7xi16 field loses f/g). The whole- + // element C2c sibling (:9100) shares this + // single-tail gap; until a general + // register->scratch tail (shift cascade) + // lands across BOTH sites, LOUD-STOP + // rather than silently drop — rule 7, the + // #11 sibling of the over-cap/float stops + // above. Tails 0/1/2/4 are exact and flow + // through. Mirrors cstage cgen.c. + let tl11: i32 = tsz % 8; + if (tl11 == 3 || tl11 == 5 || tl11 == 6 || tl11 == 7) { + let m11t: str = "#11: aggregate field receive arr[i].f=mk() with a 3/5/6/7-byte sub-8 tail unwired (materialise single-MOV under-stores; C2c-shared)\n"; + os.write(2, m11t.ptr, m11t.len: u64); + os.exit(1); + }; + let scr11: i32 = tagscradd(c, tsz); + cgexpr(c, n.rhs); + // AX/DX/CX -> scratch (C2c materialise) + let full11: i32 = tsz / 8; + let tail11: i32 = tsz % 8; + let wi11: i32 = 0; + for (wi11 < full11) { + let rn11: str = "AX"; + if (wi11 == 1) { rn11 = "DX"; } + else { if (wi11 == 2) { rn11 = "CX"; }; }; + emitline("\tMOVQ\t"); + emitline(rn11); + emitline(", "); + emitoff((scr11 + wi11 * 8): i64); + emitline("(BP)\n"); + wi11 += 1; + }; + if (tail11 > 0) { + let top11: str = "MOVB"; + if (tail11 == 4) { top11 = "MOVL"; } + else { if (tail11 == 2) { top11 = "MOVW"; }; }; + let treg11: str = "AX"; + if (full11 == 1) { treg11 = "DX"; } + else { if (full11 == 2) { treg11 = "CX"; }; }; + emitline("\t"); + emitline(top11); + emitline("\t"); + emitline(treg11); + emitline(", "); + emitoff((scr11 + full11 * 8): i64); + emitline("(BP)\n"); + }; + // &arr[i] -> BX (verbatim scalar arm) + cgexpr(c, idx); + if (esz > 1) { + emitline("\tMOVQ\t$"); + emitint(esz: i64); + emitline(", CX\n"); + emitline("\tIMULQ\tCX, AX\n"); + }; + if (baseisarray) { + emitline("\tLEAQ\t"); + emitoff(lc.off: i64); + emitline("(BP), BX\n"); + } else { + emitline("\tMOVQ\t"); + emitoff(lc.off: i64); + emitline("(BP), BX\n"); + }; + emitline("\tADDQ\tAX, BX\n"); + if (viaptr) { emitline("\tMOVQ\t(BX), BX\n"); }; + // word-copy scratch -> fi.foff(BX), + // tail-aware (C2c copy); foff on every + // eightbyte, sub-8 tail stays sub-8. + let kc11: i32 = 0; + for (kc11 + 8 <= tsz) { + emitline("\tMOVQ\t"); + emitoff((scr11 + kc11): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tAX, "); + emitdispreg((fi.foff + kc11): i64, "BX"); + emitline("\n"); + kc11 += 8; + }; + if (kc11 + 4 <= tsz) { + emitline("\tMOVL\t"); + emitoff((scr11 + kc11): i64); + emitline("(BP), AX\n"); + emitline("\tMOVL\tAX, "); + emitdispreg((fi.foff + kc11): i64, "BX"); + emitline("\n"); + kc11 += 4; + }; + if (kc11 + 2 <= tsz) { + emitline("\tMOVW\t"); + emitoff((scr11 + kc11): i64); + emitline("(BP), AX\n"); + emitline("\tMOVW\tAX, "); + emitdispreg((fi.foff + kc11): i64, "BX"); + emitline("\n"); + kc11 += 2; + }; + if (kc11 + 1 <= tsz) { + emitline("\tMOVB\t"); + emitoff((scr11 + kc11): i64); + emitline("(BP), AX\n"); + emitline("\tMOVB\tAX, "); + emitdispreg((fi.foff + kc11): i64, "BX"); + emitline("\n"); + kc11 += 1; + }; + return; + }; + // tsz<=8 in-cap aggregate returns wholly + // in AX; the scalar default's MOVQ/MOVL AX + // store is the correct 1-word receive. + }; // scalar plain `=` cgexpr(c, n.rhs); emitline("\tPUSHQ\tAX\n"); diff --git a/test/lang/idx_dot_aggret_recv_runonly_test.ww b/test/lang/idx_dot_aggret_recv_runonly_test.ww new file mode 100644 index 00000000..97452e67 --- /dev/null +++ b/test/lang/idx_dot_aggret_recv_runonly_test.ww @@ -0,0 +1,43 @@ +// idx_dot_aggret_recv_runonly_test — #11 sub-8-tail anti-clobber through a +// LOCAL [N] array. The fix's MOVL (not MOVQ) tail +// + foff-on-every-eightbyte are VALUE-verified here: the dropped eb1 would read +// the poison 9, and a tail WIDENED to MOVQ would write bytes 8..15 and smash the +// adjacent g at +12 — both caught by asserting f.c AND g. Exit-correct under +// BOTH stages. +// +// _runonly (excluded from the test-lang-byteid T2 gate) because a local +// [N] trips a SEPARATE PRE-EXISTING let-array slotsize cs!=ww +// FRAME divergence — ww sizes the array element by slotsize (S slot-padded to +// 24 -> [2]S=48), cstage by natural size (S=16 -> 32) — that PREDATES and is +// INDEPENDENT of #11: `let arr:[2]S; return arr[1].g` already diverges $32 vs +// $48 with no call involved. The #11 receive INSTRUCTIONS are byte-identical +// (both stages emit the MOVL tail); only the container's frame SIZE differs. +// The byte-id twin (idx_dot_aggret_recv_test) covers the same sub-8-tail shape +// via global-backed bases, where the slotsize bug does not bite. + +package idx_dot_aggret_recv_runonly_test; + +type t12 = struct { a: i32, b: i32, c: i32 }; +type s12 = struct { f: t12, g: i32 }; + +fn mk12() t12 = { return t12{a=10i32, b=20i32, c=30i32}; }; +fn one() i64 = { return 1i64; }; + +@test fn subtail_local_const() void = { + let a: [2]s12 = [s12{f=t12{a=9i32,b=9i32,c=9i32},g=9i32}, s12{f=t12{a=9i32,b=9i32,c=9i32},g=9i32}]; + a[1].f = mk12(); + assert(a[1].f.a == 10i32); + assert(a[1].f.b == 20i32); + assert(a[1].f.c == 30i32); + assert(a[1].g == 9i32); + assert(a[0].f.a == 9i32); +}; + +@test fn subtail_local_runtime() void = { + let a: [2]s12 = [s12{f=t12{a=9i32,b=9i32,c=9i32},g=9i32}, s12{f=t12{a=9i32,b=9i32,c=9i32},g=9i32}]; + a[one()].f = mk12(); + assert(a[1].f.a == 10i32); + assert(a[1].f.b == 20i32); + assert(a[1].f.c == 30i32); + assert(a[1].g == 9i32); +}; diff --git a/test/lang/idx_dot_aggret_recv_test.ww b/test/lang/idx_dot_aggret_recv_test.ww new file mode 100644 index 00000000..db24c192 --- /dev/null +++ b/test/lang/idx_dot_aggret_recv_test.ww @@ -0,0 +1,83 @@ +// idx_dot_aggret_recv_test — #11: an in-cap aggregate-returning CALL received +// into an AGGREGATE field of an INDEXED element `arr[i].f = mk()`. Pre-fix the +// N_DOT(N_INDEX) assign arm had no case for a struct/array/tuple field filled +// from an in-cap (<=24B, AX/DX/CX-return) call rhs: it fell to the 1-word +// scalar default — only AX (eb0) stored, DX/CX dropped. BOTH stages emitted +// byte-IDENTICAL wrong asm (gate-blind, the #263 both-wrong form). The field- +// of-indexed twin of C2c (idx_aggret_recv_test) — a DIFFERENT cgassign arm. +// Each @test POISON-seeds the field + its neighbour with sentinel 9 (distinct +// from 0 AND every expected value) and asserts EVERY member: a dropped word +// then reads 9 and fails. Covers full=2, full=3 (CX word), the sub-8-tail MOVL +// row (anti-clobber: the 12B field's tail MUST stay MOVL — a widened MOVQ would +// smash the adjacent g at +12), foff!=0, and bases [N]S / *[N]S / []S with +// const + runtime index. The sub-8-tail rows use a GLOBAL-backed base: a LOCAL +// [N] array trips a SEPARATE PRE-EXISTING let-array slotsize +// cs!=ww frame divergence (see the _runonly twin), independent of #11. T2 keeps +// the cs==ww net. + +package idx_dot_aggret_recv_test; + +type t16 = struct { a: i64, b: i64 }; +type t24 = struct { a: i64, b: i64, c: i64 }; +type t12 = struct { a: i32, b: i32, c: i32 }; +type s16 = struct { f: t16, g: i64 }; +type s24 = struct { f: t24, g: i64 }; +type sfnf = struct { g: i64, f: t16 }; +type s12 = struct { f: t12, g: i32 }; + +let g12p: [2]s12 = [s12{f=t12{a=9i32,b=9i32,c=9i32},g=9i32}, s12{f=t12{a=9i32,b=9i32,c=9i32},g=9i32}]; +let g12s: [2]s12 = [s12{f=t12{a=9i32,b=9i32,c=9i32},g=9i32}, s12{f=t12{a=9i32,b=9i32,c=9i32},g=9i32}]; + +fn mk16() t16 = { return t16{a=40i64, b=20i64}; }; +fn mk24() t24 = { return t24{a=100i64, b=20i64, c=3i64}; }; +fn mk12() t12 = { return t12{a=10i32, b=20i32, c=30i32}; }; +fn one() i64 = { return 1i64; }; + +// full=2, [N]S value-array local, const index, foff=0 +@test fn struct16_local_const() void = { + let a: [2]s16 = [s16{f=t16{a=9i64,b=9i64},g=9i64}, s16{f=t16{a=9i64,b=9i64},g=9i64}]; + a[1].f = mk16(); + assert(a[1].f.a == 40i64); + assert(a[1].f.b == 20i64); + assert(a[1].g == 9i64); + assert(a[0].f.a == 9i64); +}; + +// full=3 (CX word), [N]S value-array local, runtime index, foff=0 +@test fn struct24_local_runtime() void = { + let a: [2]s24 = [s24{f=t24{a=9i64,b=9i64,c=9i64},g=9i64}, s24{f=t24{a=9i64,b=9i64,c=9i64},g=9i64}]; + a[one()].f = mk24(); + assert(a[1].f.a == 100i64); + assert(a[1].f.b == 20i64); + assert(a[1].f.c == 3i64); + assert(a[1].g == 9i64); +}; + +// foff != 0 (f at offset 8), [N]S local, const index +@test fn fnotfirst_local() void = { + let a: [2]sfnf = [sfnf{g=9i64,f=t16{a=9i64,b=9i64}}, sfnf{g=9i64,f=t16{a=9i64,b=9i64}}]; + a[1].f = mk16(); + assert(a[1].f.a == 40i64); + assert(a[1].f.b == 20i64); + assert(a[1].g == 9i64); +}; + +// sub-8-tail (12B, MOVL tail), *[N]S -> global, const index +@test fn subtail_ptr_const() void = { + let p: *[2]s12 = &g12p; + p[1].f = mk12(); + assert(g12p[1].f.a == 10i32); + assert(g12p[1].f.b == 20i32); + assert(g12p[1].f.c == 30i32); + assert(g12p[1].g == 9i32); +}; + +// sub-8-tail (12B, MOVL tail), []S -> global, runtime index +@test fn subtail_slice_runtime() void = { + let sl: []s12 = g12s[0:2]; + sl[one()].f = mk12(); + assert(g12s[1].f.a == 10i32); + assert(g12s[1].f.b == 20i32); + assert(g12s[1].f.c == 30i32); + assert(g12s[1].g == 9i32); +}; diff --git a/test/wcc/data/idx_dot_aggret_float_loud/case.ww b/test/wcc/data/idx_dot_aggret_float_loud/case.ww new file mode 100644 index 00000000..94e42993 --- /dev/null +++ b/test/wcc/data/idx_dot_aggret_float_loud/case.ww @@ -0,0 +1,14 @@ +//ww:error "float-bearing aggregate field receive" +// #11/#165 tripwire: an in-cap aggregate-returning CALL into a FLOAT-bearing +// aggregate field of an indexed element. The return routes a float eightbyte to +// X0/X1, which the GP AX/DX/CX materialise cursor cannot read -> both stages +// LOUD-STOP (#171). SUCCESS = the float guard regressed to silent GP-garbage. +package main; +type ft = struct { x: f64, y: i64 }; +type sf = struct { f: ft, g: i64 }; +fn mk() ft = { return ft{x=1.5f64, y=7i64}; }; +export fn main() i32 = { + let arr: [2]sf; + arr[1].f = mk(); + return 0; +}; diff --git a/test/wcc/data/idx_dot_aggret_overcap_loud/case.ww b/test/wcc/data/idx_dot_aggret_overcap_loud/case.ww new file mode 100644 index 00000000..d1d06f85 --- /dev/null +++ b/test/wcc/data/idx_dot_aggret_overcap_loud/case.ww @@ -0,0 +1,15 @@ +//ww:error "over-cap (sret) aggregate field receive" +// #11c/#234 tripwire: an OVER-cap (>24B, sret-returning) CALL into an aggregate +// field of an indexed element. The result is written via a dest pointer, not the +// AX/DX/CX cursor, so the in-cap materialise cannot handle it -> both stages +// LOUD-STOP until the const-idx sret-into-field path is wired (task #8, rule 7). +// SUCCESS = the over-cap stop regressed to a 1-word silent drop (cs!=ww). +package main; +type big = struct { a:i64, b:i64, c:i64, d:i64 }; +type s = struct { f: big, g: i64 }; +fn mk() big = { return big{a=1i64,b=2i64,c=3i64,d=4i64}; }; +export fn main() i32 = { + let arr: [2]s; + arr[1].f = mk(); + return 0; +}; diff --git a/test/wcc/data/idx_dot_aggret_subtail_loud/case.ww b/test/wcc/data/idx_dot_aggret_subtail_loud/case.ww new file mode 100644 index 00000000..15ce6abb --- /dev/null +++ b/test/wcc/data/idx_dot_aggret_subtail_loud/case.ww @@ -0,0 +1,16 @@ +//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; +};