From ea913af38b801404c4fc5b02561e65f20f19ae58 Mon Sep 17 00:00:00 2001 From: jpereyra243-byte <337775728+jpereyra243-byte@users.noreply.github.com> Date: Wed, 28 Oct 2026 11:23:57 -0300 Subject: [PATCH] fix(hy-check): track multi-return and try-unwrapped auto-addr loans (fixes #2052) --- compiler/crates/hy-check/src/loans.rs | 217 +++++++++++++++++++++++++- 1 file changed, 215 insertions(+), 2 deletions(-) diff --git a/compiler/crates/hy-check/src/loans.rs b/compiler/crates/hy-check/src/loans.rs index a6bda65f8..ae6ecd6ef 100644 --- a/compiler/crates/hy-check/src/loans.rs +++ b/compiler/crates/hy-check/src/loans.rs @@ -2006,7 +2006,10 @@ impl<'a> Checker<'a> { /// A tuple call can carry a borrow in any result, not just the first. fn expr_has_borrow_shape(&self, st: &FnState, e: &Expr) -> bool { - let inner = unwrap_parens(e); + let mut inner = unwrap_parens(e); + while let Expr::Try { operand, .. } = inner { + inner = unwrap_parens(operand); + } if let Expr::Call { callee, .. } = inner { if let Some(results) = self.call_result_tys(st, callee) { return results.iter().any(|ty| { @@ -2019,6 +2022,17 @@ impl<'a> Checker<'a> { field_borrow_kind(&shape).is_some() || matches!(shape, TypeShape::Unknown) } + /// A multi-return call can return shared GC storage in any result, even + /// when result 0 is primitive. + fn call_any_result_may_share(&self, st: &FnState, callee: &Expr) -> bool { + self.call_result_tys(st, callee).is_some_and(|results| { + results.iter().any(|ty| { + let shape = self.shape_of_type(ty); + self.shape_may_share(&shape) || matches!(shape, TypeShape::Unknown) + }) + }) + } + pub(crate) fn expr_carries_borrow(&self, st: &FnState, e: &Expr) -> bool { let inner = unwrap_parens(e); if as_addr_of(inner).is_some() || contains_addr_of_in_literal(inner) { @@ -2234,7 +2248,10 @@ impl<'a> Checker<'a> { } Expr::Call { callee, args, .. } => { let call_res = self.shape_of_expr(st, inner); - if self.shape_may_share(&call_res) || self.expr_has_borrow_shape(st, inner) { + if self.shape_may_share(&call_res) + || self.expr_has_borrow_shape(st, inner) + || self.call_any_result_may_share(st, callee) + { for (idx, a) in args.iter().enumerate() { if !matches!(unwrap_parens(&a.value), Expr::Move { .. }) { let arg_shape = self.shape_of_expr(st, &a.value); @@ -8647,5 +8664,201 @@ fn main() { "expected HYB-E3103 for parenthesized &(x) stored in struct field" ); } + + #[test] + fn multi_return_auto_addr_pointer_receiver_and_param_share_storage() { + let src_recv_define = "\ +struct buf { + data [2]int +} + +fn (b *buf) slice_pair() (int, []int) { + return 0, b.data[:] +} + +fn worker(s []int, done chan bool) { + s[0] = 99 + done <- true +} + +fn main() { + b := buf{data: [2]int{1, 2}} + _, s := b.slice_pair() + done := make(chan bool, 1) + task worker(move s, done) + b.data[0] = 42 + <-done +} +"; + assert!( + check_codes(src_recv_define).iter().any(|c| c == "HYB-E3003"), + "expected HYB-E3003 for write to b after moving slice from multi-return *T receiver call" + ); + + let src_param_define = "\ +struct buf { + data [2]int +} + +fn take_slice_pair(b *buf) (int, []int) { + return 0, b.data[:] +} + +fn worker(s []int, done chan bool) { + s[0] = 99 + done <- true +} + +fn main() { + b := buf{data: [2]int{1, 2}} + _, s := take_slice_pair(b) + done := make(chan bool, 1) + task worker(move s, done) + b.data[0] = 42 + <-done +} +"; + assert!( + check_codes(src_param_define).iter().any(|c| c == "HYB-E3003"), + "expected HYB-E3003 for write to b after moving slice from multi-return *T param call" + ); + + let src_holder_assign = "\ +struct buf { + data [2]int +} + +struct holder { + s []int +} + +fn (b *buf) holder_pair() (int, holder) { + return 0, holder{s: b.data[:]} +} + +fn worker(h holder, done chan bool) { + h.s[0] = 99 + done <- true +} + +fn main() { + b := buf{data: [2]int{1, 2}} + var code int + var h holder + code, h = b.holder_pair() + _ = code + done := make(chan bool, 1) + task worker(move h, done) + b.data[0] = 42 + <-done +} +"; + assert!( + check_codes(src_holder_assign).iter().any(|c| c == "HYB-E3003"), + "expected HYB-E3003 for multi-value assign from *T receiver call returning struct with slice" + ); + + let src_param_launder = "\ +struct buf { + data [2]int +} + +fn (b *buf) slice_pair() (int, []int) { + return 0, b.data[:] +} + +fn worker(s []int, done chan bool) { + s[0] = 99 + done <- true +} + +fn launder(b buf, done chan bool) { + _, s := b.slice_pair() + task worker(move s, done) +} +"; + assert!( + check_codes(src_param_launder).iter().any(|c| c == "HYB-E3004"), + "expected HYB-E3004 for laundering non-owned parameter b via multi-return *T receiver call" + ); + + let src_const_write = "\ +struct buf { + data [2]int +} + +fn (b *buf) slice_pair() (int, []int) { + return 0, b.data[:] +} + +fn main() { + const cb = buf{data: [2]int{10, 20}} + _, s := cb.slice_pair() + s[0] = 77 +} +"; + assert!( + check_codes(src_const_write).iter().any(|c| c == "HYB-E3006"), + "expected HYB-E3006 for mutating const composite via multi-return *T receiver call" + ); + + let src_try_ptr_pair = "\ +struct cell { + v int +} + +fn (c *cell) ptr_pair_try() (int, *cell, error) { + return 0, c, nil +} + +fn worker(p *cell, done chan bool) { + p.v = 99 + done <- true +} + +fn run() error { + c := cell{v: 1} + _, p := c.ptr_pair_try()? + done := make(chan bool, 1) + task worker(move p, done) + c.v = 42 + <-done + return nil +} +"; + assert!( + check_codes(src_try_ptr_pair).iter().any(|c| c == "HYB-E3003"), + "expected HYB-E3003 for write to c after moving pointer from ?-unwrapped multi-return *T receiver call" + ); + + let src_nested_try = "\ +struct cell { + v int +} + +fn (c *cell) ptr_pair_try() (int, *cell, error) { + return 0, c, nil +} + +fn worker(p *cell, done chan bool) { + p.v = 99 + done <- true +} + +fn run() error { + c := cell{v: 1} + _, p := ((c.ptr_pair_try())?) + done := make(chan bool, 1) + task worker(move p, done) + c.v = 42 + <-done + return nil +} +"; + assert!( + check_codes(src_nested_try).iter().any(|c| c == "HYB-E3003"), + "expected HYB-E3003 for write to c after moving pointer from nested ?-unwrapped multi-return *T receiver call" + ); + } }