@@ -1326,7 +1326,9 @@ impl Analyzer<'_> {
13261326 self . check_unsupported_syntax_type_ref ( & param. ty , true , true ) ;
13271327 }
13281328 if let Some ( return_ty) = & function. return_ty {
1329- self . check_unsupported_syntax_type_ref ( return_ty, false , false ) ;
1329+ // Return type is a storable position: `owned Fn(...)` may be
1330+ // returned (first-class), but `noescape` may not escape.
1331+ self . check_unsupported_syntax_type_ref ( return_ty, false , true ) ;
13301332 }
13311333 self . check_unsupported_syntax_block ( & function. body ) ;
13321334 }
@@ -1364,7 +1366,9 @@ impl Analyzer<'_> {
13641366 ) ;
13651367 }
13661368 for field in & type_decl. fields {
1367- self . check_unsupported_syntax_type_ref ( & field. ty , false , false ) ;
1369+ // Struct/class fields are storable positions: an `owned Fn`
1370+ // field is first-class; `noescape` fields are rejected.
1371+ self . check_unsupported_syntax_type_ref ( & field. ty , false , true ) ;
13681372 }
13691373 if type_decl. kind != TypeKind :: Resource
13701374 && let Some ( drop_body) = & type_decl. drop_body
@@ -1622,24 +1626,41 @@ impl Analyzer<'_> {
16221626 map
16231627 }
16241628
1629+ /// Validate `owned`/`noescape` Fn placement.
1630+ ///
1631+ /// Soundness boundary (RSS principle):
1632+ /// - `owned Fn(...)` is a FIRST-CLASS value: allowed as a direct parameter
1633+ /// AND in storable positions (generic argument, struct field, `let`/
1634+ /// `local` binding type, function return type). A stored/escaping owned
1635+ /// closure may only capture owned (move) or `Copy` values, so it cannot
1636+ /// dangle — the capture-soundness checks elsewhere enforce that.
1637+ /// - `noescape Fn(...)` stays PARAMETER-ONLY. A noescape callback may
1638+ /// borrow-capture, so letting it be stored/returned would let a borrow
1639+ /// escape. It is rejected anywhere except a direct function parameter.
1640+ ///
1641+ /// `allow_noescape` is true only at a direct parameter position and never
1642+ /// propagates into nested type positions. `allow_owned` is true at a
1643+ /// parameter position and at every storable position, and propagates into
1644+ /// nested positions (a `List<owned Fn(...)>`, an `owned Fn` field, an
1645+ /// `owned Fn` return, or an `owned Fn` returning/taking another `owned Fn`).
16251646 fn check_unsupported_syntax_type_ref (
16261647 & mut self ,
16271648 ty : & TypeRef ,
1628- allow_noescape_param : bool ,
1629- allow_owned_param : bool ,
1649+ allow_noescape : bool ,
1650+ allow_owned : bool ,
16301651 ) {
1631- if ty. is_noescape && ( !allow_noescape_param || ty. name != "Fn" ) {
1652+ if ty. is_noescape && ( !allow_noescape || ty. name != "Fn" ) {
16321653 self . unsupported_syntax (
16331654 ty. span . clone ( ) ,
16341655 "unsupported noescape position" ,
16351656 "`noescape Fn(...)` is only supported as a direct function parameter type." ,
16361657 ) ;
16371658 }
1638- if ty. is_owned && ( !allow_owned_param || ty. name != "Fn" ) {
1659+ if ty. is_owned && ( !allow_owned || ty. name != "Fn" ) {
16391660 self . unsupported_syntax (
16401661 ty. span . clone ( ) ,
16411662 "unsupported owned position" ,
1642- "`owned Fn(...)` is only supported as a direct function parameter type." ,
1663+ "`owned Fn(...)` is supported as a function parameter and in storable positions (generic argument, struct field, binding, or return type) ." ,
16431664 ) ;
16441665 }
16451666 for span in & ty. malformed_arg_spans {
@@ -1649,8 +1670,16 @@ impl Analyzer<'_> {
16491670 "Type arguments must be valid type references; empty or unsupported type argument slots are not allowed." ,
16501671 ) ;
16511672 }
1673+ // `owned` Fn stays first-class through nested positions; `noescape`
1674+ // never does (it is strictly a direct-parameter capability).
16521675 for arg in & ty. args {
1653- self . check_unsupported_syntax_type_ref ( arg, false , false ) ;
1676+ self . check_unsupported_syntax_type_ref ( arg, false , allow_owned) ;
1677+ }
1678+ for param in & ty. fn_params {
1679+ self . check_unsupported_syntax_type_ref ( param, false , allow_owned) ;
1680+ }
1681+ if let Some ( ret) = & ty. fn_return {
1682+ self . check_unsupported_syntax_type_ref ( ret, false , allow_owned) ;
16541683 }
16551684 }
16561685
@@ -4834,6 +4863,71 @@ fn generic_namespace_args(namespace: &str) -> Option<(&str, Vec<&str>)> {
48344863 Some ( ( root, split_top_level_type_args ( args) ) )
48354864}
48364865
4866+ /// The top-level parameter slices of a `Fn(...)` type string, e.g.
4867+ /// `owned Fn(read UOp, mut Ctx) -> Option<UOp>` → `["read UOp", "mut Ctx"]`.
4868+ /// Returns `None` when the string is not a `Fn` type.
4869+ fn fn_type_params ( type_name : & str ) -> Option < Vec < & str > > {
4870+ let trimmed = type_name. trim ( ) ;
4871+ let after_prefix = trimmed
4872+ . strip_prefix ( "fresh " )
4873+ . unwrap_or ( trimmed)
4874+ . trim_start ( ) ;
4875+ let after_prefix = after_prefix
4876+ . strip_prefix ( "owned " )
4877+ . or_else ( || after_prefix. strip_prefix ( "noescape " ) )
4878+ . unwrap_or ( after_prefix)
4879+ . trim_start ( ) ;
4880+ let inner = after_prefix. strip_prefix ( "Fn(" ) ?;
4881+ // Find the matching close paren of the parameter list.
4882+ let mut depth = 1usize ;
4883+ let mut end = None ;
4884+ for ( index, ch) in inner. char_indices ( ) {
4885+ match ch {
4886+ '(' | '<' => depth += 1 ,
4887+ ')' | '>' => {
4888+ depth -= 1 ;
4889+ if depth == 0 {
4890+ end = Some ( index) ;
4891+ break ;
4892+ }
4893+ }
4894+ _ => { }
4895+ }
4896+ }
4897+ let params = & inner[ ..end?] ;
4898+ if params. trim ( ) . is_empty ( ) {
4899+ return Some ( Vec :: new ( ) ) ;
4900+ }
4901+ Some ( split_top_level_type_args ( params) )
4902+ }
4903+
4904+ /// The declared effect of each parameter in a `Fn(...)` type string, in order.
4905+ fn fn_type_param_effects ( type_name : & str ) -> Vec < Option < DataEffect > > {
4906+ fn_type_params ( type_name)
4907+ . unwrap_or_default ( )
4908+ . into_iter ( )
4909+ . map ( |param| match param. split_whitespace ( ) . next ( ) {
4910+ Some ( "read" ) => Some ( DataEffect :: Read ) ,
4911+ Some ( "mut" ) => Some ( DataEffect :: Mut ) ,
4912+ Some ( "take" ) => Some ( DataEffect :: Take ) ,
4913+ _ => None ,
4914+ } )
4915+ . collect ( )
4916+ }
4917+
4918+ /// The bare type name of the `index`-th parameter of a `Fn(...)` type string,
4919+ /// with any leading effect keyword stripped (`mut Ctx` → `Ctx`).
4920+ fn fn_type_param_type_name ( type_name : & str , index : usize ) -> Option < String > {
4921+ let params = fn_type_params ( type_name) ?;
4922+ let param = params. get ( index) ?. trim ( ) ;
4923+ let bare = param
4924+ . strip_prefix ( "read " )
4925+ . or_else ( || param. strip_prefix ( "mut " ) )
4926+ . or_else ( || param. strip_prefix ( "take " ) )
4927+ . unwrap_or ( param) ;
4928+ Some ( bare. trim ( ) . to_string ( ) )
4929+ }
4930+
48374931fn effect_name ( effect : & EffectDecl ) -> & str {
48384932 match effect {
48394933 EffectDecl :: Name ( name) | EffectDecl :: Retains ( name) => name,
@@ -5354,12 +5448,7 @@ impl<'a> AssignChecker<'a> {
53545448 fn expr ( & mut self , expr : & Expr ) {
53555449 match expr {
53565450 Expr :: Closure { params, body, .. } => {
5357- self . push_scope ( ) ;
5358- for param in params {
5359- self . insert ( param. clone ( ) , AssignBinding :: ImmutableLocal , None ) ;
5360- }
5361- self . block ( body) ;
5362- self . pop_scope ( ) ;
5451+ self . closure ( params, body, None ) ;
53635452 }
53645453 Expr :: Match { value, arms, .. } => {
53655454 self . expr ( value) ;
@@ -5374,9 +5463,21 @@ impl<'a> AssignChecker<'a> {
53745463 self . expr ( base) ;
53755464 self . expr ( index) ;
53765465 }
5377- Expr :: Call { args, .. } => {
5378- for arg in args {
5379- self . expr ( & arg. value ) ;
5466+ Expr :: Call { callee, args, .. } => {
5467+ for ( index, arg) in args. iter ( ) . enumerate ( ) {
5468+ // A closure passed where an `owned Fn(.., mut T, ..)` is
5469+ // expected (struct/sum field or function parameter) binds
5470+ // that closure parameter as a `mut` parameter, so the body
5471+ // may update its fields/elements — mirroring how a `mut`
5472+ // function parameter behaves. The effects come from the
5473+ // expected Fn type's declared parameter effects.
5474+ if let Expr :: Closure { params, body, .. } = & arg. value {
5475+ let expected_fn =
5476+ self . expected_fn_type_for_call_arg ( callee, arg. name . as_deref ( ) , index) ;
5477+ self . closure ( params, body, expected_fn. as_deref ( ) ) ;
5478+ } else {
5479+ self . expr ( & arg. value ) ;
5480+ }
53805481 }
53815482 }
53825483 Expr :: Effect { value, .. }
@@ -5400,6 +5501,75 @@ impl<'a> AssignChecker<'a> {
54005501 }
54015502 }
54025503
5504+ /// Validate a closure body in its own scope. Each closure parameter is an
5505+ /// immutable local by default; when the closure fills a slot typed
5506+ /// `... Fn(.., mut T, ..) -> ..`, the corresponding parameter is bound as a
5507+ /// `mut` parameter so the body may update its fields/elements (e.g. a stored
5508+ /// rule's `mut Ctx` parameter). The parameter binding itself stays
5509+ /// non-rebindable, exactly like a `mut` function parameter.
5510+ fn closure ( & mut self , params : & [ String ] , body : & Block , expected_fn_type : Option < & str > ) {
5511+ let param_effects = expected_fn_type. map ( fn_type_param_effects) . unwrap_or_default ( ) ;
5512+ self . push_scope ( ) ;
5513+ for ( index, param) in params. iter ( ) . enumerate ( ) {
5514+ let binding = if matches ! ( param_effects. get( index) , Some ( Some ( DataEffect :: Mut ) ) ) {
5515+ AssignBinding :: MutParam
5516+ } else {
5517+ AssignBinding :: ImmutableLocal
5518+ } ;
5519+ let type_name = param_effects
5520+ . get ( index)
5521+ . and_then ( |_| fn_type_param_type_name ( expected_fn_type?, index) ) ;
5522+ self . insert ( param. clone ( ) , binding, type_name) ;
5523+ }
5524+ self . block ( body) ;
5525+ self . pop_scope ( ) ;
5526+ }
5527+
5528+ /// The declared type of a call argument's target parameter/field, when the
5529+ /// callee is a known struct/sum constructor or a resolved function. Used to
5530+ /// thread the expected `Fn` type (and its parameter effects) into a closure
5531+ /// argument's scope.
5532+ fn expected_fn_type_for_call_arg (
5533+ & self ,
5534+ callee : & Callee ,
5535+ arg_name : Option < & str > ,
5536+ index : usize ,
5537+ ) -> Option < String > {
5538+ // Constructor field types (`RwRule(fxn: <closure>)`).
5539+ if let Callee :: Name ( name) = callee {
5540+ let root = type_root_name ( name) ;
5541+ if let Some ( type_info) = self . hir . type_info ( root) {
5542+ let field = match arg_name {
5543+ Some ( field_name) => type_info. fields . get ( field_name) ,
5544+ None => type_info. fields_ordered . get ( index) ,
5545+ } ;
5546+ if let Some ( field) = field {
5547+ return Some ( field. type_name . clone ( ) ) ;
5548+ }
5549+ }
5550+ if let Some ( fields) = self . hir . sum_variant_fields ( root) {
5551+ let field = match arg_name {
5552+ Some ( field_name) => fields. iter ( ) . find ( |f| f. name == field_name) ,
5553+ None => fields. get ( index) ,
5554+ } ;
5555+ if let Some ( field) = field {
5556+ return Some ( field. type_name . clone ( ) ) ;
5557+ }
5558+ }
5559+ }
5560+ // Resolved function/method parameter types.
5561+ if let CallResolution :: Resolved { signature, .. } = self . hir . resolve_call ( callee) {
5562+ let param = match arg_name {
5563+ Some ( param_name) => signature. params . iter ( ) . find ( |p| p. name == param_name) ,
5564+ None => signature. params . get ( index) ,
5565+ } ;
5566+ if let Some ( param) = param {
5567+ return Some ( param. type_name . clone ( ) ) ;
5568+ }
5569+ }
5570+ None
5571+ }
5572+
54035573 fn match_arms ( & mut self , arms : & [ crate :: syntax:: ast:: MatchArm ] ) {
54045574 for arm in arms {
54055575 self . push_scope ( ) ;
0 commit comments