Skip to content

Commit 8e54264

Browse files
committed
Merge feat/first-class-owned-fn: first-class storable owned Fn values
Lifts the parameter-position-only gate so owned Fn(...) is a first-class value (storable in structs/collections, bindable, returnable, callable). owned-only (noescape/read stay parameter-only); storable closures capture owned/Copy values (sound by move-ownership). Verified e2e at VM<->compiled parity with a PatternMatcher-shaped test. Also fixes a pre-existing RS0005 duplicate TensorError (metal.rssi vs tensor.rssi) that broke cold-cache compiled lowering.
2 parents c45fb13 + b5fbe03 commit 8e54264

11 files changed

Lines changed: 842 additions & 47 deletions

File tree

crates/rsscript/src/analyzer.rs

Lines changed: 187 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
48374931
fn 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

Comments
 (0)