[Pre-RFC] Super parameter

RFC: Super Parameters

Summary

This RFC introduces super parameters, a mechanism to temporarily take ownership of a parameter and return it to the caller while creating borrows tied to its lifetime. This enables functions to:

  1. Mutate owned values and return borrows without blocking subsequent immutable borrows.
  2. Express precise FFI interactions (e.g., with garbage-collected systems).
  3. Lay groundwork for future super let patterns.

Motivation

Rust's ownership system prevents common patterns where a function needs to:

  • Return a borrow derived from a mutable reference (e.g., get_or_insert).
  • Temporarily transfer ownership for external interactions (e.g., FFI with GC).
  • Reassign values while keeping them valid for the caller.

Example: Today, HashMap::get_or_insert returns &T but blocks other immutable borrows due to the retained &mut self. With super parameters:

impl HashMap<K, V> {
    pub fn get_or_insert<'a>(super 'a mut self, key: K) -> &'a V {
        // Mutate `self` and return a borrow without blocking future borrows
    }
}

Guide-level Explanation

Basic Syntax

A super parameter is declared with super 'lt param: T, indicating:

  • The function takes ownership of param.
  • At function exit, param is returned to the caller, and any borrows from it must not exceed 'lt.
fn process<'a>(super 'a data: Data) -> &'a ProcessedData {
    // Use `data` mutably, then return a borrow
    &data.process()
}

Method Sugar

For methods, super 'a self is syntactic sugar for super 'a self: Self, not for self: super 'a Self (something similars to mut self rather than &mut self):

impl Widget {
    pub fn configure<'a>(super 'a self) -> &'a Self {
        // Reconfigure and return a reference
    }
}

Restrictions

  • No implicit moves: Moving a super parameter is forbidden unless the function is marked as super fn (see below).

super fn Functions

A super fn guarantees that panics will propagate to the caller without being caught (via catch_unwind), allowing moves:

// This super means, when panic, all parameter marked `super` must be drop if it's not being either moved or dropped, while this panic cannot be caught by its caller.
// For its caller, since it knows all super parameter are either dropped or moved by the super fn, the caller's panic will not either trigger double-free or unexpected memory leak.
super fn foo<'a>(super 'a foo: Foo) -> &'a Foo {
    if random_bool() {
        drop(foo);
        panic!("fine.");
    } else {
        foo = Foo::new();
    }
    return &foo
}

Reference-level Explanation

Type System Changes

  1. Ownership Transfer: Super parameters are owned by the function but must be valid at return.
  2. Lifetime Binding: The lifetime 'lt in super 'lt is tied to the returned borrows.
  3. Borrow Checker Rules:
  • During the function body, super parameters can be borrowed mutably or immutably.
  • All borrows derived from super parameters must not outlive 'lt.
  • At function exit, the super parameter must be fully initialized (if moved).

Code Generation

The compiler ensures:

  1. Super parameters are not dropped (ownership is returned to the caller).
  2. Borrows from super parameters respect 'lt.

FFI Example

Interacting with a garbage-collected system:

struct RContext;
impl RContext {
    pub fn alloc<'a>(super 'a self) -> GcRef<'a> {
        // Allocate an object that may be GC'd unless protected
    }
    
    pub fn protect<'a>(&self, obj: GcRef<'a>) -> ProtectedRef {
        // Safeguard from GC
    }
    
    pub fn get_status(&self) -> RStatus {
        // Get some status without triggering GC.
    }
}

Drawbacks

  1. Complexity: Adds new syntax and lifetime semantics.
  2. Learning Curve: Developers must understand when to use super parameters vs references.
  3. Panic Safety: Requires careful handling in super fn functions.

Rationale and Alternatives

Alternatives

  • Macros: Use macros to simulate ownership return, but they lack type-system integration.

Why Super Parameters?

  • Precision: Enables patterns impossible with current borrow rules.
  • FFI Safety: Provides lifetimes for resources managed externally (e.g., GC).

Prior Art

  • Rust RFC 2094 (Non-lexical lifetimes): Lays groundwork for finer-grained borrow analysis.
  • Swift inout Parameters: Modify passed values while retaining caller ownership.

Unresolved Questions

  1. Closures: Should FnSuper traits be introduced, or can super parameters work with existing Fn/FnMut?
  2. Syntax Sugar: Can we infer lifetimes in simple cases (e.g., fn foo(super param: T) -> &T)?
  3. Move Semantics: How to safely allow moves in non-super fn contexts?

Future Possibilities

  • Super Let: Combine with super let to reassign variables conditionally.
  • Panic Handlers: Extend super fn to allow custom drop logic during panics.
  • Trait Integration: Allow traits to specify super parameter semantics.

Acknowledge

This is a Pre-RFC for Super borrow, which is firstly generated by DeepSeek R1 with very informal Chinese prompt as follows, and I correct some misunderstandings.

Chinese prompt, in case you're really interested in: 请尝试用英语写一份Rust的RFC,使得Rust可以支持一个名为super parameter的特性。 RFC应包含Summary, Motivation, Guide-level explanation, Reference-level explanation, Drawbacks, Rationale and alternatives, Prior art以及Unresolved questions RFC的主要内容可以参考https://internals.rust-lang.org/t/discussion-super-borrow-for-super-let-placement-new-and-hybrid-borrow/22207,但考虑到RFC是这个discussion演化的结果,如果discussion中出现的内容与这里的内容矛盾,应尽量以这里的内容为主。但如果这个RFC中有错漏或者更好的实现,请直接修正。

这个名为super parameter的特性将支持Rust函数在接收一个在变量(称为super parameter)的所有权之后,在函数结束时将super parameter所有权返还调用者,借此允许Rust程序创建可以返还给调用者的临时借用 这个特性需要分别标记变量为super parameter,且显式标记返回变量时,super parameter的被借用状况。以下示例以super 'a和super 'a mut将函数标记为super parameter及其生命周期。

// 这里,`'a`的意思是,在函数返回时foo被借用,借用长度为'a,且foo在返回时至多被借用了'a
// 在这种语法下,函数可以获取foo的大部分所有权:可以在满足借用规则的情况下任意使用&mut foo与&foo。当且仅当foo为Copy时可以使用foo本身,
fn foo<'a>(super 'a foo: Foo) -> &'a Foo {
    Foo::modify(&mut foo); // 由于以及获得了foo的所有权,所以可以生成对foo的可变借用
    // drop(foo); // 我们需要在后文讨论是否允许move这个数值。我倾向于不允许无效化foo,因为无效化foo之后,如果在对foo重新赋值之前函数触发了panic,那么这个函数及其调用者都会受到影响。因此我们没办法捕捉这个函数产生的错误
    foo = Foo::new(); // 由于获得了所有权,我们可以直接用一个新的有效值代替原始数值
    return &foo;
}
// 有关self的语法糖:
impl Bar {
    pub fn sugar<'a>(super 'a self) -> &'a Self {todo!()}
    // 需注意,super 'a self的意思不是self: super 'a Self,而是像下面展示的那样
    pub fn full_signature<'a>(super 'a self: Self) -> &'a Self {todo!()}
    // 如果函数接收的不是self,可以这样标记:
    pub fn in_case_sugar_is_not_desired<'a>(super 'a self: Box<Self>) -> &'a Self {todo!()}
}

这个新特性的应用场景包括:

  1. 允许可变借用之后返回不可变借用,且不影响生成其他不可变借用: 将pub fn get_or_insert(&mut self, value: T) -> &T这种会影响继续不可变借用self的函数改成pub fn get_or_insert<'a>(mut 'a self, value: T) -> &'a T这种不影响继续不可变借用self的
  2. 允许写出更精细的FFI调用系统。比如,当我们需要与某个可能会GC的语言(比如R语言)进行交互时,我们可以将R语言的所有交互抽象为一个ZST类型,在调用R的FFI接口生成未被保护可能被GC的数值时,将返回数值的生命周期标记为'a。此时我们仍然可以调用不会触发GC的FFI接口,但不能继续调用那些会触发GC的接口:
struct RFfi;
impl RFfi {
    pub fn alloc_variable<'a>(super 'a self) -> Unprotected<'a> {todo!("calling unsafe FFI interface here.")}
    pub fn manual_gc(&mut self) {todo!("calling unsafe FFI interface here.")}
    pub fn protect<'a>(&self, val: Unprotected<'a>) -> Protected {todo!("calling unsafe FFI interface here.")}
}
  1. 与super let结合,提供另一种实现super let的可能:未来或许可以允许使用默认值以模拟super let

关于是否允许对super parameter执行move语义:

显然这段可能会panic的程序不能以任何形式被包在pub fn catch_unwind<F: FnOnce() -> R + UnwindSafe, R>(f: F) -> Result<R>中,因为按函数签名,foo将会返回一个有效的foo(生命周期至少有'a),但函数panic时,foo的状态并非有效的。

fn foo<'a>(super 'a foo: Foo) -> &'a Foo {
    if random_bool() {
        drop(foo);
        panic!("fine.");
    } else {
        foo = Foo::new();
    }
    return &foo
}

或许我们可以加入一个panic-free标记,由编译器保证函数会处理全部panic并正常返回,或者加入一个修饰函数的super符号,表示这个函数并不是UnwindSafe的,以此保证函数的panic不会被其调用者拦截,因而可以借此触发函数的panic。但考虑到pub super fn容易和pub(super) fn混淆,我们或许可以暂时不实现move super parameter的方法直到找到一个合适的标记。

// super fn的意思是,这个函数的panic必然会令其调用者panic
// 当super fn发生panic时,super fn应当负责drop全部还未drop的super parameter,并令调用者进入panic状态
// 如果调用者也是suoer fn,由于我们约定了super parameter的处理者是被调用的super fn,所以调用者不必(也不应该)继续drop这些super parameter
// 因此,在panic被最终处理时,所有可能无效的super parameter都会且只会被drop一次,且函数的错误处理必然停在某个不是super fn的位置。也由此,在panic处理结束时,余下的全部变量仍然都是有效的。
super fn foo<'a>(super 'a foo: Foo) -> &'a Foo {
    drop(foo);
    panic!("It's fine to panic, since the caller who owns foo is panic, too.");
}

super parameter与闭包: 我实在没想好是否要新增一个FnSuper<'a>,还是将FnSu与FnMut合并。

一些语法糖:

  1. 省略super: 尽管按照Rust当前语法,省略super不会造成任何歧义,但考虑到foo('a self)或许会与foo(&'a self)混淆。在不引起歧义的前提下,或许我们可以引入fn foo<'a>('a foo: Foo)这种语法作为语法糖来减小函数签名长度。
  2. 省略生命周期: 当super借用只有一个,且返回只存在一个生命周期时,生命周期可以省略:
fn foo(super self) -> &Self {todo!()}
fn bar(super bar: Bar) -> &Bar {todo!()}
// 下面这种情况,或许不应该自动推理:
// fn baz<'a>(super bar: Baz<'a>) -> &Baz<'a> {todo!()}
Original Result generated by DeepSeek R1 # RFC: Super Parameters

RFC Status

Summary

This RFC introduces super parameters, a mechanism to temporarily take ownership of a parameter and return it to the caller while creating borrows tied to its lifetime. This enables functions to:

  1. Mutate owned values and return borrows without blocking subsequent immutable borrows.
  2. Express precise FFI interactions (e.g., with garbage-collected systems).
  3. Lay groundwork for future super let patterns.

Motivation

Rust's ownership system prevents common patterns where a function needs to:

  • Return a borrow derived from a mutable reference (e.g., get_or_insert).
  • Temporarily transfer ownership for external interactions (e.g., FFI with GC).
  • Reassign values while keeping them valid for the caller.

Example: Today, HashMap::get_or_insert returns &T but blocks other immutable borrows due to the retained &mut self. With super parameters:

impl HashMap<K, V> {
    pub fn get_or_insert<'a>(super 'a mut self, key: K) -> &'a V {
        // Mutate `self` and return a borrow without blocking future borrows
    }
}

Guide-level Explanation

Basic Syntax

A super parameter is declared with super 'lt param: T, indicating:

  • The function takes ownership of param.
  • At function exit, param is returned to the caller, and any borrows from it must not exceed 'lt.
fn process<'a>(super 'a data: Data) -> &'a ProcessedData {
    // Use `data` mutably, then return a borrow
    &data.process()
}

Method Sugar

For methods, super 'a self is syntactic sugar for self: super 'a Self:

impl Widget {
    pub fn configure<'a>(super 'a mut self) -> &'a Self {
        // Reconfigure and return a reference
    }
}

Restrictions

  • No implicit moves: Moving a super parameter is forbidden unless the function is marked super fn (see below).
  • Panic safety: Functions not marked super fn must ensure all super parameters are valid on panic.

super fn Functions

A super fn guarantees that panics will propagate to the caller without being caught (via catch_unwind), allowing moves:

super fn risky_operation<'a>(super 'a data: Data) -> &'a Data {
    let new_data = generate_data();
    data = new_data; // Allowed because panics will not be caught
    &data
}

Reference-level Explanation

Type System Changes

  1. Ownership Transfer: Super parameters are owned by the function but must be valid at return.
  2. Lifetime Binding: The lifetime 'lt in super 'lt is tied to the returned borrows.
  3. Borrow Checker Rules:
    • During the function body, super parameters can be borrowed mutably or immutably.
    • All borrows derived from super parameters must not outlive 'lt.
    • At function exit, the super parameter must be fully initialized (if moved).

Code Generation

The compiler ensures:

  1. Super parameters are not dropped (ownership is returned to the caller).
  2. Borrows from super parameters respect 'lt.

FFI Example

Interacting with a garbage-collected system:

struct RContext;
impl RContext {
    pub fn alloc<'a>(super 'a self) -> GcRef<'a> {
        // Allocate an object that may be GC'd unless protected
    }
    
    pub fn protect<'a>(&self, obj: GcRef<'a>) -> ProtectedRef {
        // Safeguard from GC
    }
}

Drawbacks

  1. Complexity: Adds new syntax and lifetime semantics.
  2. Learning Curve: Developers must understand when to use super parameters vs references.
  3. Panic Safety: Requires careful handling in super fn functions.

Rationale and Alternatives

Alternatives

  • Explicit Reassignment: Require manual reassignment (e.g., param = T::new()), but this is error-prone.
  • Macros: Use macros to simulate ownership return, but they lack type-system integration.

Why Super Parameters?

  • Precision: Enables patterns impossible with current borrow rules.
  • FFI Safety: Provides lifetimes for resources managed externally (e.g., GC).

Prior Art

  • C++ Reference Parameters: Similar to taking a reference but with explicit lifetime control.
  • Rust RFC 2094 (Non-lexical lifetimes): Lays groundwork for finer-grained borrow analysis.
  • Swift inout Parameters: Modify passed values while retaining caller ownership.

Unresolved Questions

  1. Closures: Should FnSuper traits be introduced, or can super parameters work with existing Fn/FnMut?
  2. Syntax Sugar: Can we infer lifetimes in simple cases (e.g., fn foo(super param: T) -> &T)?
  3. Move Semantics: How to safely allow moves in non-super fn contexts?

Future Possibilities

  • Super Let: Combine with super let to reassign variables conditionally.
  • Panic Handlers: Extend super fn to allow custom drop logic during panics.
  • Trait Integration: Allow traits to specify super parameter semantics.
(DeepSeek Actually Generates things begin with ```markdown, Since the forum cannot render markdown, I deleted its first line and last line.)

Your get_or_insert example should already be solved by polonius, which is a project goal for 2025H1.

1 Like

IMHO this cannot be solved since changing function body should never be considered as a major change, and thus fn(&mut T)->&T cannot be considered as fn(&'short mut T)->&'long T.

Related discussion: [Discussion] Super borrow, for super let, placement new, and hybrid borrow - #4 by quinedot

1 Like

The get_or_insert case already works if you enable polonius (I think). It doesn't require looking into the body of the function. It just essentially reborrows a shared reference and ends the exclusive reference lifetime.

1 Like

I refuse to read an AI-generated pre-RFC, even if it’s just a pre-RFC, and even though I do appreciate your transparency on the process.

Please also note that our forum TOS has a rule against machine-generated content being posted, and we tend to use that rule to say AI-generated content isn’t accepted, at least over on users.rust-lang.org


Like, why do I have to read this nonesense, for instance?

The inout prior art of Swift is usually compared to to normal &mut T references in Rust, arguably. I’m not aware of any aspect of those that makes them more similar to this feature here, which seems pretty Rust specific and about lifetimes and/or about handling of truly uninitialized values during panics. Why calling out NLL should be of any relevance, isn’t clear at all.

There’s no point in reviewing a text in this form, nor calling it a Pre-RFC, if it’s still containing unreviewed AI hallucination.


This is also a major (an obvious) drawback: i.e. it’s re-creating a second (or maybe 3rd, if you count fn(T) -> T-style APIs) way of doing “inout” with vastly different syntax, comparatively little difference for many practical use-cases.

I have instead translated your prompt and will be responding mostly only to what I can read in there.

translated prompt (click to expand)

Please try to write an RFC in English that allows Rust to support a feature called super parameter.

The RFC should include the following sections: Summary, Motivation, Guide-level explanation, Reference-level explanation, Drawbacks, Rationale and alternatives, Prior art, and Unresolved questions.

The main content of the RFC can refer to https://internals.rust-lang.org/t/discussion-super-borrow-for-super-let-placement-new-and-hybrid-borrow/22207. However, since the RFC is considered an evolution of that discussion, if there is any conflict between the discussion and what is stated here, please give priority to what is stated here. If there are any errors or better implementations in this RFC, please directly correct them.


This feature, called super parameter, will allow a Rust function that receives ownership of a variable (called a super parameter) to return the ownership of that super parameter back to the caller when the function ends. By doing so, Rust programs can create temporary borrows that can be returned to the caller.

This feature requires marking variables as super parameters, and explicitly marking the borrowing state of the super parameter when returning the variable. In the examples below, super 'a and super 'a mut are used to mark the function as having a super parameter with a certain lifetime.

// Here, `'a` means that when the function returns, foo is borrowed with lifetime 'a, 
// and at most borrowed for 'a. In this syntax, the function can receive most of foo’s 
// ownership: it can freely use &mut foo and &foo under normal borrowing rules. 
// If and only if foo is Copy, it can use foo itself.

fn foo<'a>(super 'a foo: Foo) -> &'a Foo {
    Foo::modify(&mut foo); 
    // Since ownership of foo has been taken, we can create a mutable borrow of foo.

    // drop(foo); 
    // We need to discuss whether moving this value is allowed. 
    // I tend to disallow invalidating foo because if foo is dropped here, 
    // and if the function panics before reassigning foo, then both the function 
    // and its caller would be affected. We can’t safely handle errors arising in this manner.

    foo = Foo::new(); 
    // Because we have ownership, we can directly replace the original value with a new valid value.

    return &foo;
}

// Regarding the syntax sugar for self:
impl Bar {
    pub fn sugar<'a>(super 'a self) -> &'a Self {
        todo!()
    }

    // Note that super 'a self does not mean self: super 'a Self, 
    // but rather as shown below:
    pub fn full_signature<'a>(super 'a self: Self) -> &'a Self {
        todo!()
    }

    // If the function does not take self, you can mark it like this:
    pub fn in_case_sugar_is_not_desired<'a>(super 'a self: Box<Self>) -> &'a Self {
        todo!()
    }
}

Possible use cases of this new feature include:

  1. Allowing a mutable borrow to return an immutable borrow without affecting further immutable borrows:
    For example, changing pub fn get_or_insert(&mut self, value: T) -> &T—which affects subsequent immutable borrows of self—into pub fn get_or_insert<'a>(mut 'a self, value: T) -> &'a T, which does not interfere with continuing to borrow self immutably.

  2. Enabling a more fine-grained FFI call system.
    For instance, when interacting with a garbage-collected language (like R), we can encapsulate all interactions with a zero-sized type (ZST). When calling R’s FFI interface to generate an unprotected value that might be garbage-collected, we mark the returned value’s lifetime as 'a. We can still call FFI interfaces that do not trigger GC, but cannot call those that may trigger GC:

    struct RFfi;
    impl RFfi {
        pub fn alloc_variable<'a>(super 'a self) -> Unprotected<'a> {
            todo!("calling unsafe FFI interface here.")
        }
    
        pub fn manual_gc(&mut self) {
            todo!("calling unsafe FFI interface here.")
        }
    
        pub fn protect<'a>(&self, val: Unprotected<'a>) -> Protected {
            todo!("calling unsafe FFI interface here.")
        }
    }
    
  3. Integrating with super let, providing another way to implement super let by possibly allowing the use of default values in the future to simulate it.


On whether to allow move semantics for super parameters:

Clearly, the following snippet, which may panic, cannot be safely wrapped in something like pub fn catch_unwind<F: FnOnce() -> R + UnwindSafe, R>(f: F) -> Result<R> because, according to the function signature, foo must be returned valid (with at least lifetime 'a). However, if the function panics, foo’s validity is not guaranteed:

fn foo<'a>(super 'a foo: Foo) -> &'a Foo {
    if random_bool() {
        drop(foo);
        panic!("fine.");
    } else {
        foo = Foo::new();
    }
    return &foo;
}

One idea is introducing a panic-free marker that guarantees the function handles all panics and returns normally, or another marker (e.g., super fn) to indicate the function is not UnwindSafe. In that case, if the function panics, the caller cannot intercept it. However, because pub super fn could be confused with pub(super) fn, we might delay implementing move semantics for super parameters until a more appropriate marker is found.

// super fn means that if this function panics, the caller also panics.
// When super fn panics, it is responsible for dropping all super parameters 
// that haven’t been dropped yet, and ensures the caller also enters a panic state. 
// If the caller is also a super fn, it does not drop these parameters again. 
// By the time panic handling finishes, any super parameters that might be invalid 
// will have been dropped exactly once, and the error handling will stop at a non-super fn. 
// Consequently, all remaining variables are still valid.

super fn foo<'a>(super 'a foo: Foo) -> &'a Foo {
    drop(foo);
    panic!("It's fine to panic, since the caller who owns foo is panicking too.");
}

Super parameters and closures:

It’s uncertain whether a new trait such as FnSuper<'a> is needed or whether it should be combined with FnMut. More discussion is required on this point.


Some syntactic sugar:

  1. Omitting super:
    Although under current Rust syntax omitting super might not cause ambiguity, it could be confusing to see foo('a self) vs. foo(&'a self). Where there’s no confusion, we could introduce something like:

    fn foo<'a>('a foo: Foo)
    

    as a shorthand to reduce function signature length.

  2. Omitting lifetimes:
    If there is only one super borrow and only one return lifetime, you can omit the lifetime:

    fn foo(super self) -> &Self {
        todo!()
    }
    
    fn bar(super bar: Bar) -> &Bar {
        todo!()
    }
    
    // For a case like below, automatic inference might not be appropriate:
    // fn baz<'a>(super bar: Baz<'a>) -> &Baz<'a> { todo!() }
    

From what I read in this text, this feature covers two separate ideas I’ve seen discussed before:

  • a way to downgrade a borrow, first mutable, then immutable for longer
    • IIRC I’ve myself come across this thought e.g. in the larger picture of view types, e.g. you can declare some &{field_name} Type reference that only partially borrows, just the field; and this could naturally extend to choosing different mutability and different lifetimes for fields, e.g. &{'a field_1, 'b mut field_2} Type, which is only one step removed from giving the same field different lifetimes for mutable & immutable
    • on its own, such a generalization of &mut T references might be as simple as introducing a second lifetime, e.g. &'a mut<'b> T where 'a: 'b and 'b explains the time frame in which the reference is mutable; this could be an extension to existing &'a mut T by it desugaring to &'a mut<'a> T;
    • for more discussion, see e.g. this topic: Pre-RFC: Downgradable mutable borrows
      as well as other topics that it links to, and topics that it’s linked from
  • a way (marked partially optional - i.e. your “super fn”?) to transfer/take ownership out of a reference (while avoiding the usual issues around panic safety)
    • this comes up sometimes as certain possible flavors of &move/&own-style references, e.g. here’s a thread: Pre-RFC: Move references
    • this is also sometimes discussed as a possible feature of normal mutable references in a panic=abort configuration, e.g.: Allow moving out of mutable references with `panic=abort`
    • you superficially relate this feature to super let, however super let-like functionality, explicized into explicit parameters to determine the place the value goes would rather need some sort of &out-style reference; which in turn can probably already be somewhat modeled anyway, be it with APIs like the ones discussed in the &move-style references thread linked above, or something as simple as a &'a mut Option<T> which is to be called with a None value. And none of all of these approaches (the &out variable; the APIs in the other thread, or &'mut Option<…> or your super 'a self) actually prohibit
      eventual direct & owned access to the value in question;
      or the choice of where else (besides a stack frame) it might live,
      so this doesn’t actually fully meet existing fn-level super let discussions which were concerned with the pin! macro for an example. Sound pinning can allow neither: no eventual direct owned access to the value in question, nor the choice to have it stored somewhere else than directly on a stack frame (or an async block’s stack, which is properly pinned with drop guarantees, too)

Other than these ideas (&'a mut<'b> T or &move-flavored reference types), your proposal doesn’t introduce any new types though. You seem to instead be proposing a new style of function parameter that can’t be expressed in types. Not a first-class citizen. Maybe there’s good reason why it’s easier this way, but it would IMO feel a lot more “Rusty” to try to express such access through a type instead of through new magic around functions – function coloring with const or async is already much complexity, a new parameter type also seems nicer (if possible) in this regard.

  • your explanations aren’t very thorough on the limitations where and how exactly this style of function can be called, as far as I can tell. For instance, you seem to be providing not a single example where such a function is actually called. It sounds like it requires the caller to own the value… should clarify these things more: e.g. can you pass on a super parameter to another super-parameter-expecting function? etc…

  • the explanation on semantics of:

    a Rust function that receives ownership of a variable (called a super parameter) to return the ownership of that super parameter back to the caller when the function ends

    are a bit questionable. If you create a borrow within the function body and then literally return the ownership (i.e. a move?), that would kill the borrow

  • what is it about the use or manual return statements; is that somehow a requirement? Does the borrow always happen in the return? Or is this just coincidence and unidiomatic style?

11 Likes

On the nth read, only after catching up with prior replies, I realize that this link was to a relevant prior discussion. Perhaps partially my fault for not realizing that that’s a link to a different topic, but IDK…

That discussion already contains a mention of Pre-RFC: Downgradable mutable borrows

you also reply to it with some counter-points. Presenting such an alternative and discussing its downsides or differences would belong into the “Alternatives” section.

I also see the idea of getting help from AI comes from that thread – however I can only imagine @SkiFire13 had in mind was to use it for feedback / correction / improvement of phrasing or grammar, not to make up additional content :wink:

AI (ChatGPT in particular) was also how I translated your prompt. That’s another useful tool – language models are very good at writing a translation while keeping coherent wording, and an appropriate technical style.

4 Likes

Thank you for your carefully reply, I'll modify this pre-RFC in several days (since I've found super fn unnecessary. The super fn could drop all the super parameter when panics, thus no need to trigger panic to its caller directly).

Sorry for that I've omitted the default value section, which makes inout seems unrelated. I'm afraid this part is too complicated and thus not adding this section.

In case we want super-let, providing default value for super parameters is necessary and thus we need to specific whether a variable needs to be valid before the function executes.

The function might contains 3 parts:

fn foo<'a>(
    // normal part, described in this pre-RFC
        var0: NormalParameter, super 'a var1: SuperParameter
    ; // semicolon that indication the first part finishes here.
    // second part, 
        super 'a var2: SuperParameterWithDefaultValue, 
        super 'a var3: SuperParameterWithDefaultValue = SuperParameterWithDefaultValue::new(), // Actually, trailing comma is allowed in all the 3 parts.
    ; // semicolon that indication the second part finishes here.
    // shadowed part (for super let)
        super 'a var4: ShadowedSuperParameter
        super 'a var5: ShadowedSuperParameter = ShadowedSuperParameter::new()
) -> Return<'a> {todo!()}

The first part is just what I described in this pre-RFC, the second part and the third part has a default value thus sending these super parameters are optional.

The difference between second part and third part is that, sending parameter for the third(shadowed) part converts the function into an unsafe function (This should not be banned directly, since not controlling the lifetime of shadowed super parameters might lead to a memory leak (e.g., calling such function in loops might cause stack overflow since you always allocate new variable without deallocating.)). I have no idea whether we needs a forth part, it seems that 3 parts are sufficient for most users.

What's more, there should be 3 ways to send a default value:

  1. Provide the default value for the super parameter directly.
  2. Assign to the super parameter directly in the function's body.
  3. Use super let to initialize the value in case it is not provided.
fn default_values(
    ; // the first semicolon cannot be omitted, since it indicates all the following parameters has default values.
        super '_ a:Foo = Foo::new(), super '_ b:Foo, super '_ c:Foo,
    // the second semicolon can be omitted in case no variable needs to be shadowed.
) {
    // Here, only `a` is valid, `b` and `c` are regarded as invalid no metter whether a default value is provided.
    b = Foo::new(); // `b` is valid here. Actually any previous value sending to `b` is discarded (dropped if necessary) here. Maybe a `super` before `b` is needed to clarify we're checking whether `b` is valid here.
    super let c = { // if `c` is initialized, all the code should NOT be executed.
        Foo::new_with_variable(b.calculate())
    };
    // all the super parameters are valid here.
    super let b = Foo::new(); // since `b` already has a value, this super let is actually a no-op (should yields a warning?)
}

With these grammar, we could make super let with:

fn super_let<'a>(;;super 'a foo: Foo) -> &'a Foo {
    super let foo = Foo::new();
}

I found no difficult to allowing pass a super parameter to another super parameter. I'll adding this and all the other things in my pre-RFC in several days. Thank you again for your checks.

Is this controllable? I have methods taking &mut self and returning non-&mut that have to hold the &mut of the binding (to avoid deadlocks). In this commit, Comm::data and Comm::update must not be callable at the same time as the receiving side of the update method needs a lock on data to remove items from its channel. If the sender holds a lock (of any kind) on data, the channel may not be able to be cleared so that update can return.

This seems to be offtopic, if more discussion is needed I think that forking this thread should be considered

Since both data() and update() takes &'a mut self, AFAIU as long as there is an &'a somewhere in the returned type, it will prevent the creation of a new &mut self, thus preventing to call data() or update() until the &'a is dropped.

I think (I am almost certain) that the conjecture from @pitaj is simply not true[1]. As far as I remember, polonius has some relevance to "get_or_insert"-style API, but it isn't that you can call it in a "borrow downgrading" sort of way now, but instead that the body of such a function would become possible to implement in terms of get and insert with less issues.


  1. and either they misremembered, or they misunderstood what aspect of "get_or_insert" the feature in this thread is about ↩︎

2 Likes

Indeed, I was mistaken. -Zpolonius allows you to write get_or_insert, but will not downgrade the &mut borrow.

It touches many parts of the language – new function types and traits (so also needs define interactions with closures, async fn, traits and dyn, and ABI — does it work with ?Sized types?), new rules for unwinding (what's the cost of enforcing it? Is it compatible with inlining? will it have to unwind before calling panic handler, and how called functions will know how to unwind in the right way?), new kind of lifetime and something that looks like a binding mode or new argument syntax (why not a type of a reference with the regular lifetime syntax or a Box-like type?).

This feels forced on top of Rust, and not integrated with the language.

And it's trying many things at once. For get_or_init() I don't see any reason for moving self. Under the hood it gets and returns a reference, and the issue is only in checking safety of the loan downgrade, and it doesn't have to involve new unwinding semantics.

1 Like