diff --git a/compiler/crates/hy-check/src/closures.rs b/compiler/crates/hy-check/src/closures.rs index bf3c4db0..4822e03c 100644 --- a/compiler/crates/hy-check/src/closures.rs +++ b/compiler/crates/hy-check/src/closures.rs @@ -1141,8 +1141,9 @@ fn check_stmt_boundaries<'a>(ck: &mut Checker<'_>, st: &mut FnState, stmt: &'a S if targets.len() == values.len() { for (t, v) in targets.iter().zip(values.iter()) { if matches!(t.kind, hy_syntax::PlaceKind::Field { .. }) { + let shape = ck.shape_of_expr(st, v); let is_borrow = crate::model::as_addr_of(v).is_some() - || matches!(ck.shape_of_expr(st, v), TypeShape::BorrowHandle(_)); + || ck.field_borrow_kind(&shape).is_some(); if is_borrow { ck.emit_a6_field_borrow_error(t); } @@ -1152,7 +1153,7 @@ fn check_stmt_boundaries<'a>(ck: &mut Checker<'_>, st: &mut FnState, stmt: &'a S let shapes = ck.multi_value_shapes(st, &values[0], targets.len()); for (t, shape) in targets.iter().zip(shapes.iter()) { if matches!(t.kind, hy_syntax::PlaceKind::Field { .. }) - && matches!(shape, TypeShape::BorrowHandle(_)) + && ck.field_borrow_kind(shape).is_some() { ck.emit_a6_field_borrow_error(t); } diff --git a/compiler/crates/hy-check/src/emit_guard.rs b/compiler/crates/hy-check/src/emit_guard.rs index acf3219b..41f39ab6 100644 --- a/compiler/crates/hy-check/src/emit_guard.rs +++ b/compiler/crates/hy-check/src/emit_guard.rs @@ -410,13 +410,8 @@ impl<'a> Checker<'a> { self.check_type_expr_with_state(Some(st), t); } if let Some(TypeExpr::Named { name, .. }) = ty { - let mut sname = name.clone(); - while let Some(TypeExpr::Named { name: target, .. }) = - self.type_aliases.get(&sname) - { - sname = target.clone(); - } - if let Some(struct_fields) = self.structs.get(&sname).cloned() { + let sname = name.clone(); + if let Some(struct_fields) = self.struct_fields_for_sname(&sname) { for f in fields { if let Some((_, f_ty)) = struct_fields.iter().find(|(fname, _)| fname == &f.name) @@ -755,8 +750,8 @@ impl<'a> Checker<'a> { return None; // value cycle — Go rejects the type itself elsewhere } seen.push(name.clone()); - let fields = self.structs.get(name)?; - for (_, ft) in fields { + let fields = self.struct_fields_for_sname(name)?; + for (_, ft) in &fields { let fs = self.shape_of_type(ft); if let Some(n) = self.incomparable_rec(&fs, seen) { return Some(n); @@ -957,7 +952,21 @@ impl<'a> Checker<'a> { self.check_type_expr_with_state(st, r); } } - TypeExpr::Named { .. } => {} + TypeExpr::Named { name, span } => { + if let Some((base, type_args)) = Self::parse_generic_type_args(name) { + for arg in &type_args { + self.check_type_expr_with_state(st, arg); + } + if self.structs.contains_key(base) { + if let Some(fields) = self.struct_fields_for_sname(name) { + let sname = base.to_string(); + for (fname, fty) in fields { + self.emit_struct_field_borrow_diag(&sname, &fname, &fty, *span); + } + } + } + } + } } } diff --git a/compiler/crates/hy-check/src/freeze.rs b/compiler/crates/hy-check/src/freeze.rs index 4d8ce9eb..2d04f4f7 100644 --- a/compiler/crates/hy-check/src/freeze.rs +++ b/compiler/crates/hy-check/src/freeze.rs @@ -1074,16 +1074,17 @@ impl<'a> Checker<'a> { self.shape_is_proven_pure_value(inner, depth + 1) } crate::model::TypeShape::Struct(Some(sname)) => { + let base = Self::base_named_type_str(sname); if self.file.decls.iter().any(|d| { matches!( d, hy_syntax::Decl::Struct(s) - if s.name == *sname && !s.type_params.is_empty() + if s.name == base && !s.type_params.is_empty() ) }) { return false; } - let Some(fields) = self.structs.get(sname.as_str()) else { + let Some(fields) = self.struct_fields_for_sname(sname) else { return false; }; fields.iter().all(|(_, fty)| { diff --git a/compiler/crates/hy-check/src/loans.rs b/compiler/crates/hy-check/src/loans.rs index 25637bb9..e8d1828f 100644 --- a/compiler/crates/hy-check/src/loans.rs +++ b/compiler/crates/hy-check/src/loans.rs @@ -672,9 +672,9 @@ impl<'a> Checker<'a> { return; } let recv_sname = match recv { - TypeShape::Struct(Some(s)) => Some(s.clone()), + TypeShape::Struct(Some(s)) => Some(Self::base_named_type_str(s).to_string()), TypeShape::Pointer(inner) => match &**inner { - TypeShape::Struct(Some(s)) => Some(s.clone()), + TypeShape::Struct(Some(s)) => Some(Self::base_named_type_str(s).to_string()), _ => None, }, _ => None, @@ -1968,7 +1968,7 @@ impl<'a> Checker<'a> { } Expr::Field { base, .. } | Expr::Index { base, .. } => { let sh = self.shape_of_expr(st, e); - if field_borrow_kind(&sh).is_some() || matches!(sh, TypeShape::Unknown) { + if self.field_borrow_kind(&sh).is_some() || matches!(sh, TypeShape::Unknown) { self.collect_expr_addr_of_roots(st, base, out); } } @@ -2009,12 +2009,12 @@ impl<'a> Checker<'a> { if let Some(results) = self.call_result_tys(st, callee) { return results.iter().any(|ty| { let shape = self.shape_of_type(ty); - field_borrow_kind(&shape).is_some() || matches!(shape, TypeShape::Unknown) + self.field_borrow_kind(&shape).is_some() || matches!(shape, TypeShape::Unknown) }); } } let shape = self.shape_of_expr(st, inner); - field_borrow_kind(&shape).is_some() || matches!(shape, TypeShape::Unknown) + self.field_borrow_kind(&shape).is_some() || matches!(shape, TypeShape::Unknown) } pub(crate) fn expr_carries_borrow(&self, st: &FnState, e: &Expr) -> bool { @@ -3707,7 +3707,7 @@ impl<'a> Checker<'a> { if name == "_" { continue; } - let borrow_slot = field_borrow_kind(&shape).is_some() || matches!(shape, TypeShape::Unknown); + let borrow_slot = self.field_borrow_kind(&shape).is_some() || matches!(shape, TypeShape::Unknown); if (l.names.len() == l.values.len() || borrow_slot) && self.expr_carries_borrow(st, value) { st.borrow_bindings.insert(name.clone()); } else { @@ -3774,7 +3774,7 @@ impl<'a> Checker<'a> { continue; } let borrow_slot = - field_borrow_kind(&shape).is_some() || matches!(shape, TypeShape::Unknown); + self.field_borrow_kind(&shape).is_some() || matches!(shape, TypeShape::Unknown); let inner = unwrap_parens(v); let is_borrow = as_addr_of(inner).is_some() || contains_addr_of_in_literal(inner) @@ -5352,7 +5352,7 @@ impl<'a> Checker<'a> { if matches!(t.kind, PlaceKind::Field { .. }) { let shape = self.shape_of_expr(st, v); let is_borrow = as_addr_of(v).is_some() - || field_borrow_kind(&shape).is_some(); + || self.field_borrow_kind(&shape).is_some(); if is_borrow { self.emit_a6_field_borrow_error(t); } @@ -5362,7 +5362,7 @@ impl<'a> Checker<'a> { let shapes = self.multi_value_shapes(st, &values[0], targets.len()); for (t, shape) in targets.iter().zip(shapes.iter()) { if matches!(t.kind, PlaceKind::Field { .. }) - && field_borrow_kind(shape).is_some() + && self.field_borrow_kind(shape).is_some() { self.emit_a6_field_borrow_error(t); } @@ -6484,9 +6484,9 @@ impl<'a> Checker<'a> { } = callee { let recv_base = match self.shape_of_expr(st, base) { - TypeShape::Struct(Some(s)) => Some(s), + TypeShape::Struct(Some(s)) => Some(Self::base_named_type_str(&s).to_string()), TypeShape::Pointer(inner) => match *inner { - TypeShape::Struct(Some(s)) => Some(s), + TypeShape::Struct(Some(s)) => Some(Self::base_named_type_str(&s).to_string()), _ => None, }, _ => None, @@ -6610,9 +6610,13 @@ impl<'a> Checker<'a> { (false, true) } else { let recv_base = match self.shape_of_expr(st, base) { - TypeShape::Struct(Some(s)) => Some(s), + TypeShape::Struct(Some(s)) => { + Some(Self::base_named_type_str(&s).to_string()) + } TypeShape::Pointer(inner) => match *inner { - TypeShape::Struct(Some(s)) => Some(s), + TypeShape::Struct(Some(s)) => { + Some(Self::base_named_type_str(&s).to_string()) + } _ => None, }, _ => None, @@ -7326,37 +7330,32 @@ impl<'a> Checker<'a> { } PlaceKind::Ident(name) => { let b = self.lookup_binding(st, name)?; - let mut cur = b.ty.as_ref()?; + let mut cur = b.ty.clone()?; steps.reverse(); for step in steps { while let hy_syntax::TypeExpr::Pointer { to, .. } = cur { - cur = to.as_ref(); + cur = *to; } match step { Step::Index => match cur { hy_syntax::TypeExpr::Array { elem, .. } | hy_syntax::TypeExpr::Slice { elem, .. } => { - cur = elem.as_ref(); + cur = *elem; } hy_syntax::TypeExpr::Map { value, .. } => { - cur = value.as_ref(); - } - _ => return None, - }, - Step::Field(field_name) => match cur { - hy_syntax::TypeExpr::Named { - name: struct_name, .. - } => { - let fields = self.structs.get(struct_name)?; - let (_, field_ty) = - fields.iter().find(|(f, _)| f == field_name)?; - cur = field_ty; + cur = *value; } _ => return None, }, + Step::Field(field_name) => { + let fields = self.struct_fields_for_type(&cur)?; + let (_, field_ty) = + fields.into_iter().find(|(f, _)| f == field_name)?; + cur = field_ty; + } } } - return Some(cur.clone()); + return Some(cur); } } } @@ -7776,46 +7775,103 @@ impl<'a> Checker<'a> { for (sname, fields) in decls { for (fname, fty) in fields { - let shape = self.shape_of_type(&fty); - match field_borrow_kind(&shape) { - Some("borrow-handle") => { - let msg = format!( - "struct field `{}.{}` has borrow-handle type `{}`", - sname, - fname, - fty.render() - ); - let notes = vec![ - "fields hold owned values, GC values, ids, or own-handles — never borrows (O12): a stored borrow is the shape that would force lifetime annotations".to_string(), - "process the element inside the loop body (the loan dies at each iteration's last use), or store the index or a copy and re-view through the handle later".to_string(), - ]; - let span = fty.span(); - self.diags.push( - Diagnostic::error("HYB-E3103", self.path.clone(), span, msg) - .with_notes(notes), - ); - } - Some("pointer") => { - // O12's own example (`cur &node`): pointer fields are - // borrows — store an id or the value instead. - let msg = format!( - "struct field `{}.{}` has pointer type `{}` — fields never hold borrows", - sname, - fname, - fty.render() - ); - let notes = vec![ - "a pointer field is a stored borrow (O12): store an id/index and re-borrow at the use site, or store the value".to_string(), - ]; - let span = fty.span(); - self.diags.push( - Diagnostic::error("HYB-E3103", self.path.clone(), span, msg) - .with_notes(notes), - ); + self.emit_struct_field_borrow_diag(&sname, &fname, &fty, fty.span()); + } + } + } + + pub(crate) fn emit_struct_field_borrow_diag( + &mut self, + sname: &str, + fname: &str, + fty: &hy_syntax::TypeExpr, + span: Span, + ) { + let shape = self.shape_of_type(fty); + match self.field_borrow_kind(&shape) { + Some("borrow-handle") => { + let msg = format!( + "struct field `{}.{}` has borrow-handle type `{}`", + sname, + fname, + fty.render() + ); + let notes = vec![ + "fields hold owned values, GC values, ids, or own-handles — never borrows (O12): a stored borrow is the shape that would force lifetime annotations".to_string(), + "process the element inside the loop body (the loan dies at each iteration's last use), or store the index or a copy and re-view through the handle later".to_string(), + ]; + if !self + .diags + .iter() + .any(|d| d.code == "HYB-E3103" && d.span == span) + { + self.diags.push( + Diagnostic::error("HYB-E3103", self.path.clone(), span, msg) + .with_notes(notes), + ); + } + } + Some("pointer") => { + // O12's own example (`cur &node`): pointer fields are + // borrows — store an id or the value instead. + let msg = format!( + "struct field `{}.{}` has pointer type `{}` — fields never hold borrows", + sname, + fname, + fty.render() + ); + let notes = vec![ + "a pointer field is a stored borrow (O12): store an id/index and re-borrow at the use site, or store the value".to_string(), + ]; + if !self + .diags + .iter() + .any(|d| d.code == "HYB-E3103" && d.span == span) + { + self.diags.push( + Diagnostic::error("HYB-E3103", self.path.clone(), span, msg) + .with_notes(notes), + ); + } + } + _ => {} + } + } + + pub(crate) fn field_borrow_kind(&self, shape: &TypeShape) -> Option<&'static str> { + let mut seen = Vec::new(); + self.field_borrow_kind_rec(shape, &mut seen) + } + + fn field_borrow_kind_rec( + &self, + shape: &TypeShape, + seen: &mut Vec, + ) -> Option<&'static str> { + match shape { + TypeShape::BorrowHandle(_) => Some("borrow-handle"), + TypeShape::Pointer(inner) if !matches!(**inner, TypeShape::Atomic) => Some("pointer"), + TypeShape::Slice(elem) | TypeShape::Array(elem) => { + self.field_borrow_kind_rec(elem, seen) + } + TypeShape::Map(k, v) => self + .field_borrow_kind_rec(k, seen) + .or_else(|| self.field_borrow_kind_rec(v, seen)), + TypeShape::Struct(Some(sname)) => { + if seen.iter().any(|s| s == sname) { + return None; + } + seen.push(sname.clone()); + let fields = self.struct_fields_for_sname(sname)?; + for (_, fty) in &fields { + let fshape = self.shape_of_type(fty); + if let Some(k) = self.field_borrow_kind_rec(&fshape, seen) { + return Some(k); } - _ => {} } + None } + _ => None, } } @@ -7849,16 +7905,6 @@ impl<'a> Checker<'a> { } } -fn field_borrow_kind(shape: &TypeShape) -> Option<&'static str> { - match shape { - TypeShape::BorrowHandle(_) => Some("borrow-handle"), - TypeShape::Pointer(inner) if !matches!(**inner, TypeShape::Atomic) => Some("pointer"), - TypeShape::Slice(elem) | TypeShape::Array(elem) => field_borrow_kind(elem), - TypeShape::Map(k, v) => field_borrow_kind(k).or_else(|| field_borrow_kind(v)), - _ => None, - } -} - #[cfg(test)] mod tests { use crate::{check_file, Verdict}; diff --git a/compiler/crates/hy-check/src/sendability.rs b/compiler/crates/hy-check/src/sendability.rs index f658af91..7846843e 100644 --- a/compiler/crates/hy-check/src/sendability.rs +++ b/compiler/crates/hy-check/src/sendability.rs @@ -168,7 +168,7 @@ impl<'a> Checker<'a> { return false; } seen.push(sname.clone()); - let Some(fields) = self.structs.get(sname) else { + let Some(fields) = self.struct_fields_for_sname(sname) else { return false; }; fields @@ -200,7 +200,7 @@ impl<'a> Checker<'a> { return false; } seen.push(sname.clone()); - let Some(fields) = self.structs.get(sname) else { + let Some(fields) = self.struct_fields_for_sname(sname) else { return false; }; fields @@ -227,7 +227,7 @@ impl<'a> Checker<'a> { return false; } seen.push(sname.clone()); - let Some(fields) = self.structs.get(sname) else { + let Some(fields) = self.struct_fields_for_sname(sname) else { return false; }; fields @@ -656,9 +656,11 @@ impl<'a> Checker<'a> { .. } => { let recv_base = match self.shape_of_expr(st, base) { - TypeShape::Struct(Some(s)) => Some(s), + TypeShape::Struct(Some(s)) => Some(Self::base_named_type_str(&s).to_string()), TypeShape::Pointer(inner) => match *inner { - TypeShape::Struct(Some(s)) => Some(s), + TypeShape::Struct(Some(s)) => { + Some(Self::base_named_type_str(&s).to_string()) + } _ => None, }, _ => None, @@ -687,7 +689,9 @@ impl<'a> Checker<'a> { (Struct(None), _) | (_, Struct(None)) => true, (ForeignOpaque(_), _) | (_, ForeignOpaque(_)) => true, (Primitive, Primitive) | (Str, Str) | (Atomic, Atomic) => true, - (Struct(Some(a)), Struct(Some(b))) => a == b, + (Struct(Some(a)), Struct(Some(b))) => { + Self::base_named_type_str(a) == Self::base_named_type_str(b) + } (Map(_, _), Map(_, _)) => true, (Slice(_), Slice(_)) | (Array(_), Array(_)) => true, // Go converts between slice and array of the same element class @@ -1011,7 +1015,7 @@ impl<'a> Checker<'a> { _ => None, } }) { - if let Some(def) = self.structs.get(&sname).cloned() { + if let Some(def) = self.struct_fields_for_sname(&sname) { for f in fields { if let Some((_, fty)) = def.iter().find(|(n, _)| n == &f.name) { if Checker::is_nil_literal(&f.value) { @@ -2184,7 +2188,7 @@ impl<'a> Checker<'a> { } } - fn parse_generic_type_args(name: &str) -> Option<(&str, Vec)> { + pub(crate) fn parse_generic_type_args(name: &str) -> Option<(&str, Vec)> { let open = name.find('[')?; if !name.ends_with(']') || open == 0 { return None; @@ -2225,7 +2229,7 @@ impl<'a> Checker<'a> { Some((base, parsed)) } - fn substitute_type_params( + pub(crate) fn substitute_type_params( ty: &hy_syntax::TypeExpr, subst: &std::collections::HashMap, ) -> hy_syntax::TypeExpr { @@ -2308,23 +2312,29 @@ impl<'a> Checker<'a> { cur } - fn struct_fields_for_type( + pub(crate) fn struct_fields_for_sname( &self, - ty: &hy_syntax::TypeExpr, + sname: &str, ) -> Option> { - let peeled = self.peel_pointer_and_alias(ty); - let hy_syntax::TypeExpr::Named { name, .. } = &peeled else { - return None; - }; - let sname = Self::base_named_type_str(name); - let fields = self.structs.get(sname)?.clone(); - if let Some((_, type_args)) = Self::parse_generic_type_args(name) { - if let Some(sdecl) = self.file.decls.iter().find_map(|d| match d { - hy_syntax::Decl::Struct(s) if s.name == sname => Some(s), - _ => None, - }) { + let mut cur = sname.to_string(); + for _ in 0..8 { + let base = Self::base_named_type_str(&cur); + if self.structs.contains_key(base) { + break; + } + match self.type_aliases.get(base) { + Some(hy_syntax::TypeExpr::Named { name, .. }) if name != &cur => { + cur = name.clone(); + } + _ => break, + } + } + let base = Self::base_named_type_str(&cur); + let fields = self.structs.get(base)?.clone(); + if let Some((_, type_args)) = Self::parse_generic_type_args(&cur) { + if let Some(tparams) = self.struct_type_params.get(base) { let mut subst = std::collections::HashMap::new(); - for (tp, arg) in sdecl.type_params.iter().zip(type_args.iter()) { + for (tp, arg) in tparams.iter().zip(type_args.iter()) { subst.insert(tp.name.clone(), arg.clone()); } if !subst.is_empty() { @@ -2340,6 +2350,17 @@ impl<'a> Checker<'a> { Some(fields) } + pub(crate) fn struct_fields_for_type( + &self, + ty: &hy_syntax::TypeExpr, + ) -> Option> { + let peeled = self.peel_pointer_and_alias(ty); + let hy_syntax::TypeExpr::Named { name, .. } = &peeled else { + return None; + }; + self.struct_fields_for_sname(name) + } + fn infer_type_param_bindings( &self, param_ty: &hy_syntax::TypeExpr, diff --git a/compiler/crates/hy-check/src/shape.rs b/compiler/crates/hy-check/src/shape.rs index e24f6136..31da613a 100644 --- a/compiler/crates/hy-check/src/shape.rs +++ b/compiler/crates/hy-check/src/shape.rs @@ -80,7 +80,7 @@ impl<'a> Checker<'a> { let base = Self::base_named_type_str(name); // User structs in package scope take precedence over predeclared types. if self.structs.contains_key(base) { - return TypeShape::Struct(Some(base.to_string())); + return TypeShape::Struct(Some(name.to_string())); } // User type aliases take precedence over predeclared types. if let Some(target) = self.type_aliases.get(base) { @@ -130,8 +130,7 @@ impl<'a> Checker<'a> { match shape { TypeShape::Array(elem) => self.shape_may_share_at(elem, depth + 1), TypeShape::Struct(Some(name)) => self - .structs - .get(name) + .struct_fields_for_sname(name) .map(|fields| { fields .iter() @@ -169,8 +168,7 @@ impl<'a> Checker<'a> { || self.shape_carries_handle_at(v, depth + 1) } TypeShape::Struct(Some(name)) => self - .structs - .get(name) + .struct_fields_for_sname(name) .map(|fields| { fields.iter().any(|(_, ty)| { self.shape_carries_handle_at(&self.shape_of_type(ty), depth + 1) @@ -184,8 +182,7 @@ impl<'a> Checker<'a> { /// Does this struct decl guard something with a `sync.*` lock? (Used for /// the contested-04 note: "Tier 0 does not credit locks at spawn sites".) pub(crate) fn struct_has_mutex_field(&self, sname: &str) -> bool { - self.structs - .get(sname) + self.struct_fields_for_sname(sname) .map(|fields| { fields.iter().any(|(_, ty)| { matches!( @@ -301,10 +298,9 @@ impl<'a> Checker<'a> { pub(crate) fn field_shape(&self, base: &TypeShape, field: &str) -> TypeShape { match base { TypeShape::Struct(Some(sname)) => self - .structs - .get(sname) - .and_then(|fs| fs.iter().find(|(n, _)| n == field)) - .map(|(_, t)| self.shape_of_type(t)) + .struct_fields_for_sname(sname) + .and_then(|fs| fs.into_iter().find(|(n, _)| n == field)) + .map(|(_, t)| self.shape_of_type(&t)) .unwrap_or(TypeShape::Unknown), TypeShape::Pointer(inner) => self.field_shape(inner, field), _ => TypeShape::Unknown, @@ -369,10 +365,9 @@ impl<'a> Checker<'a> { TypeShape::Unknown } } - Some(Expr::Index { base, .. }) => { - if let Expr::Ident { name, .. } = crate::model::unwrap_parens(base) - { - self.named_shape(name) + Some(idx_expr @ Expr::Index { base, .. }) => { + if matches!(crate::model::unwrap_parens(base), Expr::Ident { .. }) { + self.named_shape(&idx_expr.render()) } else { TypeShape::Unknown } @@ -640,11 +635,9 @@ impl<'a> Checker<'a> { }); } } - let sname = Self::base_named_type(&resolved)?; - self.structs - .get(&sname) - .and_then(|fs| fs.iter().find(|(n, _)| n == name)) - .map(|(_, t)| t.clone()) + self.struct_fields_for_type(&resolved) + .and_then(|fs| fs.into_iter().find(|(n, _)| n == name)) + .map(|(_, t)| t) } Expr::Index { base, .. } => { let base_ty = self.ty_of_expr(st, base)?; @@ -833,8 +826,8 @@ impl<'a> Checker<'a> { base )); } - if let Some(fields) = self.structs.get(base) { - for (fname, fty) in fields { + if let Some(fields) = self.struct_fields_for_sname(name) { + for (fname, fty) in &fields { if let Some(inner) = self.no_zero_value_reason(fty, depth + 1) { return Some(format!( "struct `{}` has no zero value because field `{}` ({})", diff --git a/compiler/crates/hy-check/tests/generic_struct_type_shape_issue_775.rs b/compiler/crates/hy-check/tests/generic_struct_type_shape_issue_775.rs new file mode 100644 index 00000000..24a64a74 --- /dev/null +++ b/compiler/crates/hy-check/tests/generic_struct_type_shape_issue_775.rs @@ -0,0 +1,321 @@ +//! Regression tests for issue #775: +//! Generic struct instantiations (`box[T]`, `pair[K, V]`) preserve their full +//! instantiated type arguments in `TypeShape::Struct(Some("box[T]"))` and +//! substitute type parameters when resolving field types across `shape_may_share`, +//! `field_shape`, `shape_holds_closure`, `shape_holds_unmovable_leaf`, +//! `shape_is_const_sendable`, `field_borrow_kind`, `ty_of_expr`, and +//! `place_target_ty`. + +use hy_check::{check_file, Verdict}; +use hy_syntax::parse_file; + +fn check(src: &str) -> hy_check::CheckResult { + let file = parse_file("test.hy", src).expect("parse should succeed"); + check_file(&file) +} + +#[test] +fn generic_struct_shallow_copy_shares_slice_and_map_interior_issue_775() { + let res_slice = check( + r#" +type box[T any] struct { + v T +} + +fn worker_slice(b box[[]int]) { + b.v[0] = 99 +} + +fn main() { + b1 := box[[]int]{v: []int{1, 2, 3}} + b2 := b1 + task worker_slice(move b1) + b2.v[0] = 42 +} +"#, + ); + assert_eq!( + res_slice.verdict, + Verdict::Reject, + "expected Reject, got: {:?}", + res_slice.diagnostics + ); + assert!( + res_slice.diagnostics.iter().any(|d| d.code == "HYB-E3003"), + "expected HYB-E3003 on post-move write through shallow copy `b2`, got: {:?}", + res_slice.diagnostics + ); + + let res_map = check( + r#" +type box[T any] struct { + v T +} + +fn worker_map(b box[map[string]int]) { + b.v["k"] = 99 +} + +fn main() { + b1 := box[map[string]int]{v: map[string]int{"k": 1}} + b2 := b1 + task worker_map(move b1) + b2.v["k"] = 42 +} +"#, + ); + assert_eq!( + res_map.verdict, + Verdict::Reject, + "expected Reject, got: {:?}", + res_map.diagnostics + ); + assert!( + res_map.diagnostics.iter().any(|d| d.code == "HYB-E3003"), + "expected HYB-E3003 on post-move map write through `b2`, got: {:?}", + res_map.diagnostics + ); +} + +#[test] +fn generic_struct_field_projection_propagates_alias_and_non_owned_origin_issue_775() { + let res_alias = check( + r#" +type box[T any] struct { + v T +} + +fn worker(b box[[]int]) { + b.v[0] = 1 +} + +fn main() { + b := box[[]int]{v: []int{1, 2}} + s := b.v + task worker(move b) + s[0] = 2 +} +"#, + ); + assert_eq!( + res_alias.verdict, + Verdict::Reject, + "expected Reject, got: {:?}", + res_alias.diagnostics + ); + assert!( + res_alias.diagnostics.iter().any(|d| d.code == "HYB-E3003"), + "expected HYB-E3003 on `s[0] = 2` after moving `b`, got: {:?}", + res_alias.diagnostics + ); + + let res_param = check( + r#" +type box[T any] struct { + v T +} + +fn worker(s []int) { + s[0] = 1 +} + +fn leak(b box[[]int]) { + s := b.v + task worker(move s) +} +"#, + ); + assert_eq!( + res_param.verdict, + Verdict::Reject, + "expected Reject, got: {:?}", + res_param.diagnostics + ); + assert!( + res_param.diagnostics.iter().any(|d| d.code == "HYB-E3004"), + "expected HYB-E3004 when moving slice projected from non-owned generic struct param, got: {:?}", + res_param.diagnostics + ); +} + +#[test] +fn generic_struct_const_binding_rejects_interior_write_and_task_send_issue_775() { + let res_const_write = check( + r#" +type box[T any] struct { + v T +} + +fn mutate_const() { + const b = box[[]int]{v: []int{1, 2}} + s := b.v + s[0] = 99 +} +"#, + ); + assert_eq!( + res_const_write.verdict, + Verdict::Reject, + "expected Reject, got: {:?}", + res_const_write.diagnostics + ); + assert!( + res_const_write + .diagnostics + .iter() + .any(|d| d.code == "HYB-E3006"), + "expected HYB-E3006 on interior write to `const b` via `s := b.v`, got: {:?}", + res_const_write.diagnostics + ); + + let res_const_send = check( + r#" +type box[T any] struct { + v T +} + +fn worker(b box[[]int]) { + b.v[0] = 99 +} + +fn main() { + const b = box[[]int]{v: []int{1, 2}} + task worker(b) +} +"#, + ); + assert_eq!( + res_const_send.verdict, + Verdict::Reject, + "expected Reject, got: {:?}", + res_const_send.diagnostics + ); + assert!( + res_const_send + .diagnostics + .iter() + .any(|d| d.code == "HYB-E3001"), + "expected HYB-E3001 when sending `const box[[]int]` without `move`, got: {:?}", + res_const_send.diagnostics + ); +} + +#[test] +fn generic_struct_holding_closure_or_opaque_rejected_at_move_boundary_issue_775() { + let res_closure = check( + r#" +type box[T any] struct { + v T +} + +fn worker(b box[fn()]) { + b.v() +} + +fn main() { + x := 0 + b := box[fn()]{v: fn() { x = 1 }} + task worker(move b) +} +"#, + ); + assert_eq!( + res_closure.verdict, + Verdict::Reject, + "expected Reject, got: {:?}", + res_closure.diagnostics + ); + assert!( + res_closure + .diagnostics + .iter() + .any(|d| d.code == "HYB-E3004"), + "expected HYB-E3004 when moving `box[fn()]` across a task boundary, got: {:?}", + res_closure.diagnostics + ); + + let res_opaque = check( + r#" +import "sync" + +type box[T any] struct { + v T +} + +fn worker(b box[sync.Mutex]) {} + +fn main() { + b := box[sync.Mutex]{} + task worker(move b) +} +"#, + ); + assert_eq!( + res_opaque.verdict, + Verdict::Reject, + "expected Reject, got: {:?}", + res_opaque.diagnostics + ); + assert!( + res_opaque.diagnostics.iter().any(|d| d.code == "HYB-E3004"), + "expected HYB-E3004 when moving `box[sync.Mutex]` across a task boundary, got: {:?}", + res_opaque.diagnostics + ); +} + +#[test] +fn generic_struct_instantiated_with_pointer_or_borrow_handle_rejected_issue_775() { + let res_ptr = check( + r#" +type box[T any] struct { + v T +} + +fn bad() { + x := 1 + b := box[*int]{v: &x} + _ = b +} +"#, + ); + assert_eq!( + res_ptr.verdict, + Verdict::Reject, + "expected Reject, got: {:?}", + res_ptr.diagnostics + ); + assert!( + res_ptr.diagnostics.iter().any(|d| d.code == "HYB-E3103"), + "expected HYB-E3103 on `box[*int]`, got: {:?}", + res_ptr.diagnostics + ); +} + +#[test] +fn pure_value_generic_struct_copy_and_const_send_still_accepted_issue_775() { + let res = check( + r#" +type box[T any] struct { + v T +} + +fn worker(b box[int]) {} + +fn main() { + b1 := box[int]{v: 10} + b2 := b1 + task worker(move b1) + b2.v = 20 + + const cb = box[int]{v: 42} + task worker(cb) +} +"#, + ); + assert_eq!( + res.verdict, + Verdict::Accept, + "expected Accept for pure-value `box[int]`, got: {:?}", + res.diagnostics + ); +}