Mandatory inlined functions for unsized types and 'super lifetime?

Looks like your latest idea is to have multiple implicit "return references" – like &mut, but they start uninitialized and the only way to interact with them is to initialize them (which turns them into a regular &mut).

Why not approach this by explicitly adding such references, maybe called &return or &out? Or, actually, maybe this can be implemented in user code? I feel like I've read previous discussions of this idea, but unfortunately I can't remember them, and I'm not sure what search terms to use to find them again. But making it explicit would avoid the current issues where you need a special type of function that can only be inlined.

1 Like

But how do they know how much to supply?

I've been running in circles around a similar design in my mind countless times and never arrived to a solution which would look feasible.. The desire to return objects of an unknown size via stack from functions is not such a rare thing! Many people would have loved the ability..

Sorry, that's out of date: I realized after writing my initial post that inline isn't actually necessary, and both inlined and non-inlined functions can use these techniques.

Explicit is of course possible too. But that's quite a bit less ergonomic, and exposes implementation details unnecessarily.

Even if they don't need to be inlined, they're still special because they can't be used as function pointers or Fn trait objects with their as-written signatures.

1 Like

I understand, and I'm worried about how it will interact with unsafe, since it's conceptually very different from the rules of today's Rust. IOW, this is a problem that involves the human factor.

See my message above: Mandatory inlined functions for unsized types and 'super lifetime? - #8 by petertodd

You can statically analyse the total size of all let super requirements, in the same way that you can statically analyse the size of each stack frame.

Of course, this isn't magic: if you try to use this in a recursive function, compilation will obviously fail as you're effectively creating a recursive type of infinite size. But that exact same problem already exists in recursive functions returning impl Trait.

It's interesting that you mention impl Trait.
There does seem to be a degree of similarity here.

Okay, what if the result is needed not one but two stack frames up? Three? Forty two?

As I mentioned above, you can in fact get the size and alignment of the scratchpad space from the vtable, and allocate it with alloca prior to calling the function.

That said, impl Trait isn't compatible with either function pointers or Fn trait objects, and it's still quite useful. I'm ok with that trade-off.

That's my intent! If you actually have a fourty-two level deep call graph doing something useful, this could be very helpful in ensuring that you actually are returning values directly to the top-level caller, rather than repeatedly copying them over and over again all the way down the call graph. And, let super would let you be explicit about when to copy vs when to return to the original callee.

It's similar to how a very complex impl Future tree can result in one large structure, allocated in advance.

I'm probably missing this but doesn't the design just have exactly one level of super? Not 42? And how do we code/compile the case of returning one value 4 levels up and one value 1 level up?

1 Like

See: Mandatory inlined functions for unsized types and 'super lifetime? and Mandatory inlined functions for unsized types and 'super lifetime?

Similar to generators, it should be feasible to work out a nested set of enums/unions representing every possible part of the call graph, as well as within functions. Of course, for an initial, unoptimized, implementation you could also just have the equivalent of a struct with a field for every single let super statement, and every single function call's scratchpad space requirement.

Ah, I missed that. However, this doesn't work for the situations where the allocation needs to be more than one stack frame up, so you still have some unresolved questions about what your design wants to be.

Basically that means that the 'super lifetime if you call a Fn trait object isn't part of the outer lifetime. If you specifically are using this for lifetimes, that's just a hard limitation; if you are trying to return an unsized value, you'll just have to copy some data. I'm ok with 'super not being usable recursively on trait objects.

A

Well but suppose you have 3 functions fn a(), fn b() , fn c(). a calls b and b calls c: a->b->c.

c creates two unsized objects of size 100 and 1000. The object of size 100 actually needs to travel all the way up back to a. But the object of size 1000 only needs to travel up to b.

Does it really make sense to combine objects of size 100 and 1000 into one struct? Imagine we're extremely tight on stack and a after it has received that object of size 100 absolutely needs the other 1000 bytes for a different purpose.

I feel like this is an interesting direction of thinking but the proposed syntax and implementation don't allow a really multi-level control as one would indeed desire to have.

B

Also wouldn't all the examples so far have been well served with -> impl Trait or perhaps an enhanced -> impl Trait?

C

Suppose function f1 with super calls another function f2 with super. Suppose that call from f1 to f2 happens inside a loop.

Or function g1 with super calls a function g2 without super that calls function g3 with super. And suppose that call from g2 to g3 happens in a loop.

D

If these memory areas were somehow passed explicitly that would provide a nice opportunity to take several of them if needed and it would provide a sensible way to hand over the associated lifetime to the function being called.

The content of the area being passed in would remain completely hidden from the caller. The callee would still provide the knowledge of how much space is required similar to -> impl Trait

I'm struggling to imagine a sensible syntax for this however.. Also this would probably work better with &out and &own references..

D.1

How about this syntax?

fn foo<super 'a, super 'b>(...) -> R { // ?? both 'a and 'b  have to be used in type R ??
    let mut super 'a varA = ...;
    ...
    let mut super 'b varB = ...;
}

foo here takes two implicit pointers to scratch areas, one designated by 'a and another by 'b. Similarly to -> impl Trait the caller knows how much space to allocate for each. Possibly via function return type they get bound to some lifetimes in the caller. It is possible to "patch through" such a lifetime to a super lifetime of the caller itself thus allowing values to be returned two levels up. 'a and 'b also work as regular lifetimes within foo.

Frankly for that type of extremely constrained scenario there's a lot of tools that don't work. I'm not expecting to be able to easily allow ultra-fine-grained control like that. Now, you can imagine ways to specify it, like having different lifetime specifiers for different destinations, passing multiple "scratchpad" pointers to the function, etc. But in general I think for a situation that constrained you probably need explicit out arguments.

When I used to do microcontroller stuff I'd find myself often looking at the raw ASM verifying that the compiler actually did what I expected. :slight_smile:

Yes. Those are toy examples, not meant to illustrate actual problems.

I think the bigger question is actually how useful is this if &own references are implemented? Because in a lot of cases it'd probably suffice to start with an actual caller provided, object, and manipulate it. Not how my original example with Pin at the very top would actually worked quite nicely if you could pass it an owning reference.

I mean, returning a value from f2 to f1's caller can only happen once. So sounds like you're just overwriting a value repeatedly. There'd be some complexity in exactly when to call drop. But that's why in my examples above, I explicitly had functions returning &own references to make the logic clear, even though that's not actually needed.

I'd expect g2 to "break the chain" and allocate separate scratchpad space for the g2 call from its own stack space. Which would of course happen once per loop iteration.

I think any circumstance where you really need that careful control not only are you going to want to do this explicitly, you're also going to need compiler tooling to verify what stack frames might actually be generated. IIRC Rust does have that now in some form.

Note that an &out reference can be handled by a method on &own MaybeUninit<T>, especially if it gets support for unsized types. Eg you could imagine:

impl<T: ?Sized> MaybeUninit<T> {
    fn initialize(this: &own Self, value: &own T) -> &own T;
}

Getting a bit off topic, but implicit conversion from passing a T value to &own T would be really nice...

I think that definitely might have value. Though need to justify it against return arguments. Also, note how you might want to use super in destructuring:

let (super [u8; 100], [u8; 1000]) = make_pair();

Could lead to some surprising copying . But interesting to think about at least.

So what are the objects you'd like to return like this? So far I can think only about

  • -> impl Trait
  • arrays of a size which is statically known but hidden from the invoker
  • a combination of the two

I'm happy to indulge in an enjoyable rapid fire conversation but "serious" talk would need convincing examples I guess..

Sounds quite difficult to model with lifetimes. On the one hand your lifetime would be limited to one loop iteration. On the other hand you want to return the value to the caller of f1 so it outlives the whole stackframe of f1..

... and this sounds like quite an arbitrary decision :slight_smile: Wouldn't according to this logic adding super anywhere within g2 change its behavior dramatically?

I think trouble can be avoided if super is made a property of a let binding not part of datatype. Thus

let super ([u8; 100], [u8; 1000]) = make_pair(); // Ok
let (super [u8; 100], [u8; 1000]) = make_pair(); // Does not compile

To further advertise the syntax I'm now advocating - and which in my view solves the issues mentioned above - I've written something vaguely similar in it:

fn make_pair<super 'b, super 'c>() -> (&'b [u8], &'c [u8]) {
   ..
   let super 'b b = [u8; 100];
   let super 'c c = [u8; 1000];
   ...
   (&b, &c)
}
fn invoker<super 'a>() -> &'a[u8] {
   ...
   let (a, c) : (&'a [u8], &[u8]) = make_pair(); // am I even using correct Rust syntax?
   ...
   a
}
1 Like

Yeah, I think you're right that impl Trait does almost everything this could do. It'd be different if side effects from drop impls was acceptable. But I think multiple people have made good arguments that would be too magical, and I have to agree with them. With that restriction any drop impl needs to be visible in the return value, at which point in practice you end up having to use something like impl Trait because you so often need to capture the existence of some kind of drop impl, and without being able to support arbitrary sized types, the use-case is limited to a finite number of different varieties. That pattern is basically impl Trait returning an enum behind the scenes.

super still potentially makes sense for explicitly declarng how return value optimization should work, especially in the complex cases where you need it to work at multiple levels, and with destructuring. But that's not that common of a need.

My original inline fn idea still has merit for the truly arbitrarily sized type case. But there's a lot of desire to be able to support box, with a guarantee that no copying is done. My proposal doesn't actually address that directly.

I mean, a function skipping the use of let super has to break that chain, or where would it stop? :joy:

I think I agree with you. Though it's a pity that the meaning of a super 'a argument to a function isn't really that obvious at first glace. Maybe return 'a or ret 'a would be a more clear name? That'd establish that the purpose has something to do with returning a value.

Could this solve something that a 'callback' can't? In particular, a stack frame is a magical guarantee that the destructor is called. So what if we just preserve our own.

impl<T: ?Sized> Pin<T> {
    pub inline const fn with_pinned<R>(
        mut value: T,
        once: impl FnOnce(Pin<&mut T>) -> R,
    ) -> R {
        let pinned = pin_mut!(value); // Or rewrite the unsafe here..
        once(pinned)
    }
}

The Rust type system of function traits also ensures that the provided callback is both more flexible and safer to use than comparable constructions in other languages.

While you can transform anything to callbacks, that style is inconvenient. Being able to work with unsized and sized values in the same way would be much nicer.

1 Like

I'm pivoting again :slight_smile:

  • super 'a could solve some of what &out is intended for
  • possibly with more elegance

E.g. super 'a would allow to construct a relatively large object with references to itself directly in a grandparent stack frame. RAM would be passed in grandchild fully uninit.

  • imaginary &own could decide when drop gets called

One thing the suggested syntax is lacking compared to &out is caller's ability to optionally allocate that RAM in the heap instead of on the stack.