From ff7f31992f9ccdb1f491142bcd248df4a5baeddc Mon Sep 17 00:00:00 2001 From: Abdullah Azmi Date: Mon, 19 Oct 2026 01:57:39 +0000 Subject: [PATCH 1/2] fix(hy-check): prevent option[T] TypeShape erasure on pointers, sends, receives, and range loops (Fixes #753) --- compiler/crates/hy-check/src/loans.rs | 9 +- compiler/crates/hy-check/src/range_iter.rs | 19 +++ compiler/crates/hy-check/src/recv.rs | 19 +++ compiler/crates/hy-check/src/sendability.rs | 29 ++++ compiler/crates/hy-check/src/shape.rs | 43 +++++- .../hy-check/tests/recv_operand_soundness.rs | 127 ++++++++++++++++++ 6 files changed, 241 insertions(+), 5 deletions(-) diff --git a/compiler/crates/hy-check/src/loans.rs b/compiler/crates/hy-check/src/loans.rs index cc845c11b..ac1012be1 100644 --- a/compiler/crates/hy-check/src/loans.rs +++ b/compiler/crates/hy-check/src/loans.rs @@ -2067,7 +2067,9 @@ impl<'a> Checker<'a> { if let Some(ty) = &b.ty { let resolved = self.resolve_type_alias(ty); match &resolved { - hy_syntax::TypeExpr::Array { .. } => return true, + hy_syntax::TypeExpr::Array { .. } | hy_syntax::TypeExpr::Option { .. } => { + return true + } hy_syntax::TypeExpr::Named { name: tname, .. } => { let base = Self::base_named_type_str(tname); if self.structs.contains_key(base) { @@ -5133,7 +5135,10 @@ impl<'a> Checker<'a> { managed: bool, ) { let shape = self.shape_of_expr(st, subject); - let ty = match self.ty_of_expr(st, subject) { + let ty = match self + .ty_of_expr(st, subject) + .map(|t| self.resolve_type_alias(&t)) + { Some(hy_syntax::TypeExpr::Option { elem, .. }) => Some(*elem), other => other, }; diff --git a/compiler/crates/hy-check/src/range_iter.rs b/compiler/crates/hy-check/src/range_iter.rs index 2ab98fcfc..781d0a41e 100644 --- a/compiler/crates/hy-check/src/range_iter.rs +++ b/compiler/crates/hy-check/src/range_iter.rs @@ -388,6 +388,25 @@ impl<'a> Checker<'a> { /// sides treat an unknown operand. A TYPE contract, not a sendability /// doubt — `managed` does not suppress it. pub(crate) fn check_range_iterable(&mut self, st: &FnState, expr: &Expr, span: Span) { + if self.expr_is_option(st, expr) { + let ty_str = self + .ty_of_expr(st, expr) + .map(|t| t.render()) + .unwrap_or_else(|| "option".to_string()); + let msg = format!( + "cannot range over `{}` (`{}`) — `for x in e` needs a slice, array, string, map, or channel, not an `option` wrapper (LX13)", + expr.render(), + ty_str + ); + let notes = vec![ + "why: `option[T]` lowers to a value struct in Go (`hyb_opt`), which Go cannot range over".to_string(), + "fix: unwrap the `option` with `match` (`match opt { some(xs) => for x in xs { ... }, none => {} }`) before ranging".to_string(), + ]; + self.diags.push( + Diagnostic::error("HYB-E2009", self.path.clone(), span, msg).with_notes(notes), + ); + return; + } // Go: "array, pointer to array, slice, string, map, or channel" — // only pointer-to-ARRAY is rangeable (`*struct` / `*slice` / // `*string` are not), so normalize exactly one Pointer level for diff --git a/compiler/crates/hy-check/src/recv.rs b/compiler/crates/hy-check/src/recv.rs index 990213270..352023356 100644 --- a/compiler/crates/hy-check/src/recv.rs +++ b/compiler/crates/hy-check/src/recv.rs @@ -340,6 +340,25 @@ impl<'a> Checker<'a> { /// side, an ill-typed source is not a sendability doubt — `managed` /// does not suppress it. pub(crate) fn check_recv_operand(&mut self, st: &FnState, chan: &Expr, span: Span) -> bool { + if self.expr_is_option(st, chan) { + let ty_str = self + .ty_of_expr(st, chan) + .map(|t| t.render()) + .unwrap_or_else(|| "option".to_string()); + let msg = format!( + "cannot receive from `{}` (`{}`) — a receive source must be a channel, not an `option` wrapper (O4)", + chan.render(), + ty_str + ); + let notes = vec![ + "why: `option[chan T]` lowers to a value struct in Go (`hyb_opt`), not a channel; receiving directly from an `option` fails `go build`".to_string(), + "fix: unwrap the `option` with `match` (`match opt { some(ch) => <-ch, none => {} }`) before receiving".to_string(), + ]; + self.diags.push( + Diagnostic::error("HYB-E2008", self.path.clone(), span, msg).with_notes(notes), + ); + return false; + } match self.shape_of_expr(st, chan) { TypeShape::Chan(_) => { if let Some(ty) = self.recv_chan_type_expr(st, chan) { diff --git a/compiler/crates/hy-check/src/sendability.rs b/compiler/crates/hy-check/src/sendability.rs index f658af916..1eceb435f 100644 --- a/compiler/crates/hy-check/src/sendability.rs +++ b/compiler/crates/hy-check/src/sendability.rs @@ -434,6 +434,16 @@ impl<'a> Checker<'a> { }), }); } + if self.expr_is_option(st, operand) { + return Outcome::Rejected(RejectInfo { + value: expr.render(), + ty: format!("*{}", self.binding_ty_str(b)), + notes: vec![format!( + "`&{}` lends a mutable `option` wrapper — a live borrow of mutable shared state, which is not one of the sendable classes (O4)", + root + )], + }); + } match &b.shape { // `&hits` where hits is atomic-typed — the mediated route (OQ6) TypeShape::Atomic => Outcome::Ok, @@ -832,6 +842,25 @@ impl<'a> Checker<'a> { /// usual); false emits the diagnostic and the caller skips the value /// check (the site is ill-typed — no cascade). pub(crate) fn check_send_target(&mut self, st: &FnState, chan: &Expr, span: Span) -> bool { + if self.expr_is_option(st, chan) { + let ty_str = self + .ty_of_expr(st, chan) + .map(|t| t.render()) + .unwrap_or_else(|| "option".to_string()); + let msg = format!( + "cannot send to `{}` (`{}`) — a send target must be a channel, not an `option` wrapper (O4)", + chan.render(), + ty_str + ); + let notes = vec![ + "why: `option[chan T]` lowers to a value struct in Go (`hyb_opt`), not a channel; sending directly to an `option` fails `go build`".to_string(), + "fix: unwrap the `option` with `match` (`match opt { some(ch) => ch <- v, none => {} }`) before sending".to_string(), + ]; + self.diags.push( + Diagnostic::error("HYB-E3007", self.path.clone(), span, msg).with_notes(notes), + ); + return false; + } match self.shape_of_expr(st, chan) { TypeShape::Chan(_) => { if let Some(ty) = self.ty_of_expr(st, chan) { diff --git a/compiler/crates/hy-check/src/shape.rs b/compiler/crates/hy-check/src/shape.rs index e24f61369..4b1503ca3 100644 --- a/compiler/crates/hy-check/src/shape.rs +++ b/compiler/crates/hy-check/src/shape.rs @@ -32,7 +32,11 @@ impl<'a> Checker<'a> { match ty { TypeExpr::Named { name, .. } => self.named_shape_depth(name, depth), TypeExpr::Pointer { to, .. } => { - TypeShape::Pointer(Box::new(self.shape_of_type_depth(to, depth + 1))) + if self.ty_is_option(to) { + TypeShape::Pointer(Box::new(TypeShape::Struct(None))) + } else { + TypeShape::Pointer(Box::new(self.shape_of_type_depth(to, depth + 1))) + } } TypeExpr::Slice { elem, .. } => { TypeShape::Slice(Box::new(self.shape_of_type_depth(elem, depth + 1))) @@ -229,12 +233,20 @@ impl<'a> Checker<'a> { } Expr::Unary { op, operand, .. } => match op { hy_syntax::UnOp::Addr => { - TypeShape::Pointer(Box::new(self.shape_of_expr(st, operand))) + if self.expr_is_option(st, operand) { + TypeShape::Pointer(Box::new(TypeShape::Struct(None))) + } else { + TypeShape::Pointer(Box::new(self.shape_of_expr(st, operand))) + } } _ => TypeShape::Primitive, }, Expr::AddrOf { operand, .. } => { - TypeShape::Pointer(Box::new(self.shape_of_expr(st, operand))) + if self.expr_is_option(st, operand) { + TypeShape::Pointer(Box::new(TypeShape::Struct(None))) + } else { + TypeShape::Pointer(Box::new(self.shape_of_expr(st, operand))) + } } Expr::Move { operand, .. } | Expr::Try { operand, .. } @@ -579,6 +591,31 @@ impl<'a> Checker<'a> { cur } + pub(crate) fn ty_is_option(&self, ty: &TypeExpr) -> bool { + matches!(self.resolve_type_alias(ty), TypeExpr::Option { .. }) + } + + pub(crate) fn expr_is_option(&self, st: &FnState, expr: &Expr) -> bool { + if let Some(ty) = self.ty_of_expr(st, expr) { + if self.ty_is_option(&ty) { + return true; + } + } + match crate::model::unwrap_parens(expr) { + Expr::Move { operand, .. } | Expr::Try { operand, .. } => { + self.expr_is_option(st, operand) + } + Expr::Receive { chan, .. } => self + .ty_of_expr(st, chan) + .map(|ct| match self.resolve_type_alias(&ct) { + TypeExpr::Chan { elem, .. } => self.ty_is_option(&elem), + _ => false, + }) + .unwrap_or(false), + _ => false, + } + } + /// The as-written type of an initializer, when directly available /// (literals, aliases, local-fn results). `None` = use the shape render. pub(crate) fn ty_of_expr(&self, st: &FnState, expr: &Expr) -> Option { diff --git a/compiler/crates/hy-check/tests/recv_operand_soundness.rs b/compiler/crates/hy-check/tests/recv_operand_soundness.rs index 9c7bf2b33..e4339b840 100644 --- a/compiler/crates/hy-check/tests/recv_operand_soundness.rs +++ b/compiler/crates/hy-check/tests/recv_operand_soundness.rs @@ -196,3 +196,130 @@ fn drain(opt option[chan chan<- int]) { ); assert_has(&r_select, "HYB-E3008"); } + +#[test] +fn addr_of_option_chan_or_atomic_across_task_and_chan_rejected() { + let r_task_chan = check( + r#"package main + +fn worker(p *option[chan int]) { + _ = p +} + +fn main() { + ch := make(chan int, 1) + opt := some(ch) + task worker(&opt) +} +"#, + ); + assert_has(&r_task_chan, "HYB-E3001"); + + let r_send_atomic = check( + r#"package main +import "sync/atomic" + +fn main() { + var a atomic.int64 + opt := some(a) + out := make(chan *option[atomic.int64], 1) + out <- &opt +} +"#, + ); + assert_has(&r_send_atomic, "HYB-E3002"); +} + +#[test] +fn pointer_to_option_atomic_in_struct_field_and_return_rejected() { + let r = check( + r#"package main +import "sync/atomic" + +struct holder { + p *option[atomic.int64] +} + +fn escape() *option[atomic.int64] { + var a atomic.int64 + opt := some(a) + p := &opt + return p +} +"#, + ); + assert_has(&r, "HYB-E3103"); + assert_has(&r, "HYB-E3104"); +} + +#[test] +fn send_recv_and_range_directly_on_option_rejected() { + let r_send = check( + r#"package main + +fn bad_send(opt option[chan int]) { + opt <- 1 +} +"#, + ); + assert_has(&r_send, "HYB-E3007"); + + let r_recv = check( + r#"package main + +fn bad_recv(opt option[chan int]) { + x := <-opt + _ = x +} +"#, + ); + assert_has(&r_recv, "HYB-E2008"); + + let r_range = check( + r#"package main + +fn bad_range(opt_xs option[[]int], opt_arr option[[2]int]) { + for _, x in opt_xs { + _ = x + } + for _, y in &opt_arr { + _ = y + } +} +"#, + ); + let codes = error_codes(&r_range); + assert!( + codes.iter().filter(|c| *c == "HYB-E2009").count() >= 2, + "expected HYB-E2009 for both opt_xs and &opt_arr, got {:?}", + r_range.diagnostics + ); +} + +#[test] +fn option_chan_by_value_and_match_unwrapped_ops_accepted() { + assert_accepts( + r#"package main + +type opt_ch_t = option[chan int] + +fn worker(opt opt_ch_t) { + match opt { + some(ch): { + ch <- 1 + v := <-ch + _ = v + } + none: {} + } +} + +fn main() { + ch := make(chan int, 1) + var opt opt_ch_t = some(ch) + task worker(opt) +} +"#, + ); +} + From 1fa49e879997830d7c5af7dbeec19532cad8a94c Mon Sep 17 00:00:00 2001 From: Abdullah Azmi Date: Mon, 19 Oct 2026 02:34:48 +0000 Subject: [PATCH 2/2] fix(hy-check): preserve option TypeExpr across receive, select, range, none rebind, and new(alias) (#753) --- compiler/crates/hy-check/src/closures.rs | 14 +- compiler/crates/hy-check/src/emit_guard.rs | 20 +- compiler/crates/hy-check/src/lib.rs | 14 +- compiler/crates/hy-check/src/loans.rs | 19 +- compiler/crates/hy-check/src/shape.rs | 227 +++++++++++++++--- .../hy-check/tests/recv_operand_soundness.rs | 109 +++++++++ 6 files changed, 347 insertions(+), 56 deletions(-) diff --git a/compiler/crates/hy-check/src/closures.rs b/compiler/crates/hy-check/src/closures.rs index bf3c4db02..4e0c20a3f 100644 --- a/compiler/crates/hy-check/src/closures.rs +++ b/compiler/crates/hy-check/src/closures.rs @@ -908,14 +908,21 @@ fn check_stmt_boundaries<'a>(ck: &mut Checker<'_>, st: &mut FnState, stmt: &'a S TypeShape::Chan(e) => *e, _ => TypeShape::Unknown, }; + let elem_ty = ck.ty_of_expr(st, chan).and_then(|ct| { + match ck.resolve_type_alias(&ct) { + hy_syntax::TypeExpr::Chan { elem, .. } => Some(*elem), + _ => None, + } + }); for (i, n) in names.iter().enumerate() { let shape = if i == 0 { elem.clone() } else { TypeShape::Primitive }; + let ty = if i == 0 { elem_ty.clone() } else { None }; mark_nested_overwritten(st, n, nested); - ck.define_binding(st, n, shape, None, false, managed, false); + ck.define_binding(st, n, shape, ty, false, managed, false); } sync_nested_moves(st, nested); } @@ -987,11 +994,12 @@ fn check_stmt_boundaries<'a>(ck: &mut Checker<'_>, st: &mut FnState, stmt: &'a S collect_expr_direct_fnlits(expr, st, nested); let is_chan = matches!(ck.shape_of_expr(st, expr), TypeShape::Chan(_)); let shapes = ck.iterate_var_shapes(st, expr, vars.len()); - for (name, shape) in vars.iter().zip(shapes) { + let tys = ck.iterate_var_tys(st, expr, vars.len()); + for ((name, shape), ty) in vars.iter().zip(shapes).zip(tys) { let is_ref = shape.is_reference_alias(); let may_share = ck.shape_may_share(&shape); mark_nested_overwritten(st, name, nested); - ck.define_binding(st, name, shape, None, false, managed, false); + ck.define_binding(st, name, shape, ty, false, managed, false); if !is_chan { if may_share { ck.record_may_share(st, name, expr); diff --git a/compiler/crates/hy-check/src/emit_guard.rs b/compiler/crates/hy-check/src/emit_guard.rs index f47e6d7a7..50ab90c35 100644 --- a/compiler/crates/hy-check/src/emit_guard.rs +++ b/compiler/crates/hy-check/src/emit_guard.rs @@ -219,8 +219,9 @@ impl<'a> Checker<'a> { self.check_expr_emit_guard(st, expr); if deep { let shapes = self.iterate_var_shapes(st, expr, vars.len()); - for (name, shape) in vars.iter().zip(shapes) { - self.define_binding(st, name, shape, None, false, false, false); + let tys = self.iterate_var_tys(st, expr, vars.len()); + for ((name, shape), ty) in vars.iter().zip(shapes).zip(tys) { + self.define_binding(st, name, shape, ty, false, false, false); } } } @@ -321,13 +322,20 @@ impl<'a> Checker<'a> { TypeShape::Chan(e) => *e, _ => TypeShape::Unknown, }; + let elem_ty = self.ty_of_expr(st, chan).and_then(|ct| { + match self.resolve_type_alias(&ct) { + TypeExpr::Chan { elem, .. } => Some(*elem), + _ => None, + } + }); for (i, n) in names.iter().enumerate() { let shape = if i == 0 { elem.clone() } else { TypeShape::Primitive }; - self.define_binding(st, n, shape, None, false, false, false); + let ty = if i == 0 { elem_ty.clone() } else { None }; + self.define_binding(st, n, shape, ty, false, false, false); } } } @@ -367,6 +375,12 @@ impl<'a> Checker<'a> { let ty = l.ty.clone().or_else(|| self.ty_of_expr(st, val)); self.define_binding(st, name, shape, ty, immutable, false, is_let); } + } else if l.values.len() == 1 && l.names.len() > 1 { + let shapes = self.multi_value_shapes(st, &l.values[0], l.names.len()); + let tys = self.multi_value_tys(st, &l.values[0], l.names.len()); + for ((name, shape), ty) in l.names.iter().zip(shapes).zip(tys) { + self.define_binding(st, name, shape, ty, immutable, false, is_let); + } } } diff --git a/compiler/crates/hy-check/src/lib.rs b/compiler/crates/hy-check/src/lib.rs index 0619fe8ee..8560c7f9f 100644 --- a/compiler/crates/hy-check/src/lib.rs +++ b/compiler/crates/hy-check/src/lib.rs @@ -452,13 +452,20 @@ fn check_fn(ck: &mut Checker<'_>, f: &FnDecl) { TypeShape::Chan(e) => *e, _ => TypeShape::Unknown, }; + let elem_ty = ck.ty_of_expr(&st, chan).and_then(|ct| { + match ck.resolve_type_alias(&ct) { + hy_syntax::TypeExpr::Chan { elem, .. } => Some(*elem), + _ => None, + } + }); for (i, n) in names.iter().enumerate() { let shape = if i == 0 { elem.clone() } else { TypeShape::Primitive }; - ck.define_binding(&mut st, n, shape, None, false, managed, false); + let ty = if i == 0 { elem_ty.clone() } else { None }; + ck.define_binding(&mut st, n, shape, ty, false, managed, false); } } walk::ScopeEvent::MatchBind { name, subject } => { @@ -699,10 +706,11 @@ fn check_fn(ck: &mut Checker<'_>, f: &FnDecl) { ); let is_chan = matches!(ck.shape_of_expr(&st, expr), TypeShape::Chan(_)); let shapes = ck.iterate_var_shapes(&st, expr, vars.len()); - for (name, shape) in vars.iter().zip(shapes) { + let tys = ck.iterate_var_tys(&st, expr, vars.len()); + for ((name, shape), ty) in vars.iter().zip(shapes).zip(tys) { let is_ref = shape.is_reference_alias(); let may_share = ck.shape_may_share(&shape); - ck.define_binding(&mut st, name, shape, None, false, managed, false); + ck.define_binding(&mut st, name, shape, ty, false, managed, false); if !is_chan { if may_share { ck.record_may_share(&mut st, name, expr); diff --git a/compiler/crates/hy-check/src/loans.rs b/compiler/crates/hy-check/src/loans.rs index ac1012be1..4675fe29c 100644 --- a/compiler/crates/hy-check/src/loans.rs +++ b/compiler/crates/hy-check/src/loans.rs @@ -5556,15 +5556,18 @@ impl<'a> Checker<'a> { } if let Some(b) = st.env_lookup_mut(name) { + let keep_existing_option_ty = matches!(info.shape, TypeShape::Unknown) + && b.ty.as_ref().is_some_and(|t| self.ty_is_option(t)); if !matches!(info.shape, TypeShape::Unknown) || b.ty.is_none() { b.shape = info.shape; } - if info.ty.is_some() - || b.ty.is_none() - || matches!( - b.ty.as_ref().map(|t| self.resolve_type_alias(t)), - Some(hy_syntax::TypeExpr::Fn { .. }) - ) + if !keep_existing_option_ty + && (info.ty.is_some() + || b.ty.is_none() + || matches!( + b.ty.as_ref().map(|t| self.resolve_type_alias(t)), + Some(hy_syntax::TypeExpr::Fn { .. }) + )) { b.ty = info.ty; } @@ -5679,10 +5682,12 @@ impl<'a> Checker<'a> { self.preserve_peers_before_sever(st, name, false); } if let Some(b) = st.env_lookup_mut(name) { + let keep_existing_option_ty = matches!(shape, TypeShape::Unknown) + && b.ty.as_ref().is_some_and(|t| self.ty_is_option(t)); if !matches!(shape, TypeShape::Unknown) || b.ty.is_none() { b.shape = shape.clone(); } - if ty.is_some() || b.ty.is_none() { + if !keep_existing_option_ty && (ty.is_some() || b.ty.is_none()) { b.ty = ty; } b.moved = None; diff --git a/compiler/crates/hy-check/src/shape.rs b/compiler/crates/hy-check/src/shape.rs index 4b1503ca3..cc2d22c8f 100644 --- a/compiler/crates/hy-check/src/shape.rs +++ b/compiler/crates/hy-check/src/shape.rs @@ -364,33 +364,48 @@ impl<'a> Checker<'a> { // check-site. The pointee is inferred when the type argument // resolves; otherwise Unknown — still a non-sendable pointer. "new" => { - let pointee = match args - .first() - .map(|a| crate::model::unwrap_parens(&a.value)) - { - // `new(pool_stats)` / `new((pool_stats))` / `new(alias)` — the - // argument is a type name, not a value; resolve structs, type - // aliases, primitives, and foreign types via `named_shape`. - Some(Expr::Ident { name, .. }) => self.named_shape(name), - Some(Expr::Field { base, name, .. }) => { - if let Expr::Ident { name: pkg, .. } = - crate::model::unwrap_parens(base) - { - self.named_shape(&format!("{}.{}", pkg, name)) - } else { - TypeShape::Unknown + let first_arg = + args.first().map(|a| crate::model::unwrap_parens(&a.value)); + let is_option_pointee = match first_arg { + Some(Expr::Ident { name, .. }) => self + .type_aliases + .get(name.as_str()) + .is_some_and(|t| self.ty_is_option(t)), + Some(Expr::Index { base, .. }) => matches!( + crate::model::unwrap_parens(base), + Expr::Ident { name, .. } if name == "option" + ), + _ => false, + }; + let pointee = if is_option_pointee { + TypeShape::Struct(None) + } else { + match first_arg { + // `new(pool_stats)` / `new((pool_stats))` / `new(alias)` — the + // argument is a type name, not a value; resolve structs, type + // aliases, primitives, and foreign types via `named_shape`. + Some(Expr::Ident { name, .. }) => self.named_shape(name), + Some(Expr::Field { base, name, .. }) => { + if let Expr::Ident { name: pkg, .. } = + crate::model::unwrap_parens(base) + { + self.named_shape(&format!("{}.{}", pkg, name)) + } else { + TypeShape::Unknown + } } - } - Some(Expr::Index { base, .. }) => { - if let Expr::Ident { name, .. } = crate::model::unwrap_parens(base) - { - self.named_shape(name) - } else { - TypeShape::Unknown + Some(Expr::Index { base, .. }) => { + if let Expr::Ident { name, .. } = + crate::model::unwrap_parens(base) + { + self.named_shape(name) + } else { + TypeShape::Unknown + } } + Some(other) => self.shape_of_expr(st, other), + None => TypeShape::Unknown, } - Some(other) => self.shape_of_expr(st, other), - None => TypeShape::Unknown, }; TypeShape::Pointer(Box::new(pointee)) } @@ -560,6 +575,38 @@ impl<'a> Checker<'a> { } } + /// Source types of the variables bound by `for v[, i] in `, when directly available. + pub(crate) fn iterate_var_tys( + &self, + st: &FnState, + expr: &Expr, + n_vars: usize, + ) -> Vec> { + let Some(ty) = self.ty_of_expr(st, expr) else { + return vec![None; n_vars.max(1)]; + }; + let one = |v: TypeExpr| { + if n_vars >= 2 { + vec![None, Some(v)] + } else { + vec![Some(v)] + } + }; + match self.resolve_type_alias(&ty) { + TypeExpr::Slice { elem, .. } + | TypeExpr::Array { elem, .. } + | TypeExpr::Chan { elem, .. } => one(*elem), + TypeExpr::Map { key, value, .. } => { + if n_vars >= 2 { + vec![Some(*key), Some(*value)] + } else { + vec![Some(*value)] + } + } + _ => vec![None; n_vars.max(1)], + } + } + // ----------------------------------------------------------------------- // Source types for diagnostics // ----------------------------------------------------------------------- @@ -691,6 +738,34 @@ impl<'a> Checker<'a> { _ => None, } } + Expr::Slice { base, span, .. } => { + let base_ty = self.ty_of_expr(st, base)?; + match self.resolve_type_alias(&base_ty) { + TypeExpr::Slice { elem, .. } | TypeExpr::Array { elem, .. } => { + Some(TypeExpr::Slice { elem, span: *span }) + } + _ => None, + } + } + Expr::Receive { chan, .. } => { + let chan_ty = self.ty_of_expr(st, chan)?; + match self.resolve_type_alias(&chan_ty) { + TypeExpr::Chan { elem, .. } => Some(*elem), + _ => None, + } + } + Expr::AddrOf { operand, span } + | Expr::Unary { + op: hy_syntax::UnOp::Addr, + operand, + span, + } => { + let inner = self.ty_of_expr(st, operand)?; + Some(TypeExpr::Pointer { + to: Box::new(inner), + span: *span, + }) + } Expr::StructLit { ty, .. } => ty.clone(), Expr::MapLit { key, value, span, .. @@ -722,6 +797,17 @@ impl<'a> Checker<'a> { span: *span, }) } + Expr::SliceLit { + elem: None, + items, + span, + } => { + let first_ty = items.first().and_then(|it| self.ty_of_expr(st, it))?; + Some(TypeExpr::Slice { + elem: Box::new(first_ty), + span: *span, + }) + } Expr::FnLit { params, results, @@ -759,13 +845,32 @@ impl<'a> Checker<'a> { span: *span, }); } - if name == "make" { + if name == "append" { return args.first().and_then(|a| self.ty_of_expr(st, &a.value)); } + if name == "make" { + return args.first().and_then(|a| { + let arg = crate::model::unwrap_parens(&a.value); + if let Expr::Ident { + name: tname, + span: tspan, + } = arg + { + if self.type_aliases.contains_key(tname.as_str()) { + return Some(TypeExpr::Named { + name: tname.clone(), + span: *tspan, + }); + } + } + self.ty_of_expr(st, arg) + }); + } if args.len() == 1 { - let is_type_name = crate::const_overflow::fixed_width_int_range(name).is_some() - || crate::const_init::is_conversion_type_name(name) - || self.type_aliases.contains_key(name); + let is_type_name = + crate::const_overflow::fixed_width_int_range(name).is_some() + || crate::const_init::is_conversion_type_name(name) + || self.type_aliases.contains_key(name); if is_type_name && !self.fns.contains_key(name.as_str()) { return Some(TypeExpr::Named { name: name.clone(), @@ -1179,12 +1284,27 @@ impl<'a> Checker<'a> { _ => None, }, _ => None, - })?; - self.method_decls - .get(&(recv_base, head.to_string())) - .map(|f| f.results.iter().map(|r| r.ty.clone()).collect()) + }); + if let Some(rb) = recv_base { + if let Some(f) = self.method_decls.get(&(rb, head.to_string())) { + return Some(f.results.iter().map(|r| r.ty.clone()).collect()); + } + } + let callee_ty = self.ty_of_expr(st, callee)?; + if let TypeExpr::Fn { results, .. } = self.resolve_type_alias(&callee_ty) { + Some(results) + } else { + None + } + } + other => { + let callee_ty = self.ty_of_expr(st, other)?; + if let TypeExpr::Fn { results, .. } = self.resolve_type_alias(&callee_ty) { + Some(results) + } else { + None + } } - _ => None, } } @@ -1196,7 +1316,10 @@ impl<'a> Checker<'a> { value: &Expr, n_names: usize, ) -> Vec { - let v = crate::model::unwrap_parens(value); + let mut v = crate::model::unwrap_parens(value); + while let Expr::Move { operand, .. } = v { + v = crate::model::unwrap_parens(operand); + } if n_names == 2 { if let Expr::Index { base, .. } = v { if let TypeShape::Map(_, val) = self.shape_of_expr(st, base) { @@ -1214,7 +1337,13 @@ impl<'a> Checker<'a> { } } let (call_expr, is_try) = match v { - Expr::Try { operand, .. } => (crate::model::unwrap_parens(operand), true), + Expr::Try { operand, .. } => { + let mut inner = crate::model::unwrap_parens(operand); + while let Expr::Move { operand: op, .. } = inner { + inner = crate::model::unwrap_parens(op); + } + (inner, true) + } other => (other, false), }; if let Expr::Call { callee, .. } = call_expr { @@ -1237,23 +1366,41 @@ impl<'a> Checker<'a> { } /// As-written source types for a multi-name define/assign from one value - /// (`a, b := f()`, `a, b = f()?`, `v, ok := m[k]`), when directly available. + /// (`a, b := f()`, `a, b = f()?`, `v, ok := m[k]`, `v, ok := <-ch`), when directly available. pub(crate) fn multi_value_tys( &self, st: &FnState, value: &Expr, n_names: usize, ) -> Vec> { - let v = crate::model::unwrap_parens(value); + let mut v = crate::model::unwrap_parens(value); + while let Expr::Move { operand, .. } = v { + v = crate::model::unwrap_parens(operand); + } if n_names == 2 { if let Expr::Index { base, .. } = v { - if let Some(TypeExpr::Map { value: val_ty, .. }) = self.ty_of_expr(st, base) { - return vec![Some(*val_ty), None]; + if let Some(base_ty) = self.ty_of_expr(st, base) { + if let TypeExpr::Map { value: val_ty, .. } = self.resolve_type_alias(&base_ty) { + return vec![Some(*val_ty), None]; + } + } + } + if let Expr::Receive { chan, .. } = v { + if let Some(chan_ty) = self.ty_of_expr(st, chan) { + if let TypeExpr::Chan { elem, .. } = self.resolve_type_alias(&chan_ty) { + return vec![Some(*elem), None]; + } } } } let (call_expr, is_try) = match v { - Expr::Try { operand, .. } => (crate::model::unwrap_parens(operand), true), + Expr::Try { operand, .. } => { + let mut inner = crate::model::unwrap_parens(operand); + while let Expr::Move { operand: op, .. } = inner { + inner = crate::model::unwrap_parens(op); + } + (inner, true) + } other => (other, false), }; if let Expr::Call { callee, .. } = call_expr { diff --git a/compiler/crates/hy-check/tests/recv_operand_soundness.rs b/compiler/crates/hy-check/tests/recv_operand_soundness.rs index e4339b840..3240d2162 100644 --- a/compiler/crates/hy-check/tests/recv_operand_soundness.rs +++ b/compiler/crates/hy-check/tests/recv_operand_soundness.rs @@ -323,3 +323,112 @@ fn main() { ); } +#[test] +fn option_from_chan_receive_select_and_range_preserves_option_wrapper() { + let r_recv = check( + r#"package main + +fn worker(p *option[chan int]) { + _ = p +} + +fn bad_recv_local(ch chan option[chan int]) { + opt := <-ch + opt <- 1 + x := <-opt + _ = x + task worker(&opt) + opt2, ok := <-ch + _ = ok + opt2 <- 2 +} +"#, + ); + assert_has(&r_recv, "HYB-E3007"); + assert_has(&r_recv, "HYB-E2008"); + assert_has(&r_recv, "HYB-E3001"); + + let r_select = check( + r#"package main + +fn worker(p *option[chan int]) { + _ = p +} + +fn bad_select_recv(ch chan option[chan int]) { + select { + opt := <-ch: { + opt <- 1 + x := <-opt + _ = x + task worker(&opt) + } + default: {} + } +} +"#, + ); + assert_has(&r_select, "HYB-E3007"); + assert_has(&r_select, "HYB-E2008"); + assert_has(&r_select, "HYB-E3001"); + + let r_for = check( + r#"package main + +fn worker(p *option[chan int]) { + _ = p +} + +fn bad_for_range(xs []option[chan int]) { + for _, opt in xs { + opt <- 1 + x := <-opt + _ = x + task worker(&opt) + } +} +"#, + ); + assert_has(&r_for, "HYB-E3007"); + assert_has(&r_for, "HYB-E2008"); + assert_has(&r_for, "HYB-E3001"); +} + +#[test] +fn nested_option_reassigned_to_none_and_new_option_alias_rejected() { + let r_nested = check( + r#"package main + +fn bad_nested() { + var opt option[option[chan int]] = some(some(make(chan int, 1))) + opt = none + match opt { + some(inner): { + inner <- 1 + } + none: {} + } +} +"#, + ); + assert_has(&r_nested, "HYB-E3007"); + + let r_new_alias = check( + r#"package main +import "sync/atomic" + +type opt_atomic = option[atomic.int64] + +fn worker(p *opt_atomic) { + _ = p +} + +fn bad_new_alias() { + p := new(opt_atomic) + task worker(p) +} +"#, + ); + assert_has(&r_new_alias, "HYB-E3001"); +} +