From 5fb18876b989d3fb086b1a588c06e3c66ae94a90 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sun, 28 Jun 2026 10:43:34 +0900 Subject: [PATCH] check: lower an array-variable borrow into a union slice success variant (#17) --- cmd/wcc/check.c | 19 +++++ selfhost/cmd/wcc/check.ww | 19 +++++ test/lang/arrvar_union_slice_test.ww | 109 +++++++++++++++++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 test/lang/arrvar_union_slice_test.ww diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 0c7457bc..3fab75d1 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -1221,6 +1221,25 @@ desugar_arrayslice(Checker *c, Type *dst, Node *expr) return; Type *du = type_chase_named(dst); Type *su = type_chase_named(expr->type); + /* #17: the borrow target may be the SLICE success variant of a tagged + * union (`return/assign/let/f(arr)` into `([]T|e)`). Chase du to that + * variant so an array VARIABLE lowers to a full slice exactly as a + * bare []T dst does — the existing slice cgen then builds the full + * {ptr,len,cap} header and the union widen's slice arm wraps it, + * closing the silent len/cap drop. Mirrors #13's reject_arrlit_borrow + * TY_TAGGED chase (an array LITERAL has no outliving backing and is + * rejected there first; a variable has storage, so this borrow is + * legal). An array-typed variant is the separate #5/#60 reject. */ + if (du && du->kind == TY_TAGGED && su && su->kind == TY_ARRAY) { + for (Tparam *p = du->params; p; p = p->next) { + Type *pu = type_chase_named(p->type); + if (pu && pu->kind == TY_SLICE + && type_eq(pu->sub, su->sub)) { + du = pu; + break; + } + } + } if (du == NULL || su == NULL || du->kind != TY_SLICE || su->kind != TY_ARRAY) return; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 5e2794ba..f74a6470 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -5639,6 +5639,25 @@ fn desugararrayslice(c: *checker, dsttn: *syntax.node, srctn: *syntax.node, val: let su: *syntax.node = resolvealias(c, unwrapbang(srctn)); if (du == nil) { return val; }; if (su == nil) { return val; }; + // #17: the borrow target may be the SLICE success variant of a tagged + // union (`return/assign/let/f(arr)` into `([]T|e)`). Chase du to that + // variant so an array VARIABLE lowers to a full slice exactly as a bare + // []T dst does — the existing slice cgen then builds the full + // {ptr,len,cap} header and the union widen's slice arm wraps it, closing + // the silent len/cap drop. Mirrors #13's rejectarrlitborrow N_TTAGGED + // chase (an array LITERAL has no outliving backing and is rejected there + // first; a variable has storage, so this borrow is legal). + if (du.kind == syntax.nkind.N_TTAGGED && su.kind == syntax.nkind.N_TARRAY) { + let v: *syntax.node = du.list; + for (v != nil) { + let vu: *syntax.node = resolvealias(c, unwrapbang(v)); + if (vu != nil && vu.kind == syntax.nkind.N_TSLICE && typeeqast(c, vu.lhs, su.lhs)) { + du = vu; + break; + }; + v = v.next; + }; + }; if (du.kind != syntax.nkind.N_TSLICE) { return val; }; if (su.kind != syntax.nkind.N_TARRAY) { return val; }; if (!typeeqast(c, du.lhs, su.lhs)) { return val; }; diff --git a/test/lang/arrvar_union_slice_test.ww b/test/lang/arrvar_union_slice_test.ww new file mode 100644 index 00000000..ce264d84 --- /dev/null +++ b/test/lang/arrvar_union_slice_test.ww @@ -0,0 +1,109 @@ +// arrvar_union_slice_test — #17: an array VARIABLE coerced into the SLICE +// success variant of a tagged union (`let a:[N]i32; ... ([]i32|e)`) silently +// dropped .len/.cap (and .ptr) — a both-stage miscompile the byte-id gate is +// BLIND to (cstage and wwstage agreed and were BOTH wrong). +// +// Root cause was in the CHECKER, not cgen: desugar_arrayslice — the single +// #258 [N]T->[]T borrow lowering that rewrites the array source to a full +// slice `a[0:len(a)]` so the existing slice cgen builds the {ptr,len,cap} +// header — no-op'd when the borrow target chased to a tagged union (it gated +// `du->kind == TY_SLICE` only). The bare array source then reached cgen's +// scalar union-widen arm, which loaded the array's first 8 bytes as a single +// payload word and zeroed len/cap. The fix chases a TY_TAGGED dst to its slice +// success variant before lowering (the exact #13 reject_arrlit_borrow chase, +// applied to a LEGAL array-variable borrow rather than a reject) — closing the +// class by construction at the one shared choke-point, so RETURN / LET-init / +// ASSIGN / CALL-ARG all lower identically. +// +// Both stages emit identically-wrong asm pre-fix, so byte-id is blind — these +// VALUE asserts are the sole tooth: len, cap (the dropped words) AND first/last +// elements (proving .ptr too). Reverting either stage's desugar chase reverts +// to the scalar-widen drop and reddens. +// +// DISTINCT from #13/#18: those REJECT an illegal array-LITERAL temp borrowed +// into a union slice variant (a temp has no outliving backing). #17 is the +// opposite — an array VARIABLE HAS storage, so the borrow is legal and must +// WORK. A typed array literal here still loud-rejects (reject_arrlit_borrow +// runs first); only the variable desugars. +// +// multi_variant_form pins the variant SELECTION: `[4]i32` into a MULTI-slice +// union ([]u8|[]i32|e) must chase to the []i32 member by ELEMENT equality, not +// merely the first slice ([]u8). A naive first-N_TSLICE chase would pick []u8, +// fail the main desugar gate's element-eq guard, and no-op straight back to the +// len/cap drop. Faithful to harec tagged_select_subtype (types.c:702 — a UNIQUE +// assignable member; an array is element-eq to at most one slice variant). +// +// Inline @test fns, not a row-table: the cases vary in the COERCION-CONTEXT +// place shape (return / let-init / assign / call-arg), not in data values over +// one operation, so a row-array `[](in,exp){}` can't express them — and that +// form is itself blocked by cgen #111. Mirrors global_chained_unwrap_test's +// #16 pins. + +package arrvar_union_slice_test; + +type e = !i32; + +fn mk() ([]i32 | e) = { // RETURN: array var -> union slice + let a: [4]i32 = [11, 22, 33, 44]; + return a; +}; + +fn use_slice(s: ([]i32 | e)) i32 = { // receives the CALL-ARG widen + let v = s!; + return len(v): i32 + v.cap: i32 + v[0] + v[3]; +}; + +@test fn ret_form() void = { + let s = mk()!; + assert(len(s) == 4); // .len tooth + assert(s.cap == 4); // .cap tooth + assert(s[0] == 11); // .ptr teeth + assert(s[1] == 22); + assert(s[2] == 33); + assert(s[3] == 44); +}; + +@test fn let_form() void = { // LET-init: `let r:([]i32|e) = a;` + let a: [3]i32 = [7, 8, 9]; + let r: ([]i32 | e) = a; + let s = r!; + assert(len(s) == 3); + assert(s.cap == 3); + assert(s[0] == 7); + assert(s[2] == 9); +}; + +@test fn assign_form() void = { // ASSIGN: `r = a;` into a union + let a0: [2]i32 = [1, 1]; + let r: ([]i32 | e) = a0; + let a: [2]i32 = [5, 6]; + r = a; + let s = r!; + assert(len(s) == 2); + assert(s.cap == 2); + assert(s[0] == 5); + assert(s[1] == 6); +}; + +@test fn callarg_form() void = { // CALL-ARG: `f(a)` where f takes union + let a: [4]i32 = [11, 22, 33, 44]; + assert(use_slice(a) == 4 + 4 + 11 + 44); +}; + +fn mk_multi() ([]u8 | []i32 | e) = { // MULTI-slice union: must pick []i32, not leading []u8 + let a: [4]i32 = [11, 22, 33, 44]; + return a; +}; + +@test fn multi_variant_form() void = { // element-eq chase selects the RIGHT slice variant + let got: i32 = 0; + match (mk_multi()) { + case let s: []i32 => + got = len(s): i32 + s.cap: i32 + s[0] + s[3]; + case let b: []u8 => + got = -1; // wrong: chase mis-picked the leading slice + case let er: e => + got = -2; // wrong: chase fell through to the no-op drop + }; + assert(got == 4 + 4 + 11 + 44); +};