Mandatory inlined functions for unsized types and 'super lifetime?

I have re-read the thread and I think I have been missing some point. As @elidupree says we can already return a reference or data by using Cows.

  • If the issue is that we want to allocate a different type than the one we want to reference, then we could generalize Cow. For example to allocate (A,B) and get &A we could impl CowFor<A> for (A,B). This could help with the comment
  • If the issue are that we do not get enough return value optimizations then we could help the compiler with annotations to hint what variables could be allocated in the caller stack, but without changing any meaning.
  • If the issue is that we are too lazy to write Cows or find them to pollute the programming logic, then we could make some macros to wrap them automatically.
  • If the issue is specific to unsized types, as mentioned by @petertodd then I have yet to see more clearly the intended use.

Although I have mixed them before, now I think these are different issues and should be addressed separately.

Sorry, I was referring to dhm's reply.

What specifically does this enable? I had been reading the existential impl Sized as mostly sugar (to avoid needing to write out more verbose types, and to make sugar easier to write), rather than fundamentally changing much?

1 Like

Do you mean to say it could work with inlining and alloca-s?

Without allocas I don't see how it could work.. f2() allocates an object in its parent's frame and returns a reference to it. The reference is stored in a Vec and outlives a single loop iteration. This is done an unknown number of times. It wouldn't be safe to always return reference to the same object - the vector would end up containing duplicate references. And we wouldn't know how large a buffer to allocate if we wanted to allocate a new object on each iteration of the loop..

  • -> impl Trait already exists
  • -> [T] could similarly exist

What has been suggested in this thread is a little more general: a way to create several (potentially linked) objects in (grand)-grand-..parents' frames. If that is useful enough or not is a different question.

I guess my example implied some extra niceties:

  • the total buffer size is automatically computed for the caller and used in the callee
  • the caller does not know nor care what types exactly the callee allocates in the buffer
  • multiple values of different types can be constructed in the same buffer without unsafe code
  • the buffer does not need to be initialized by the caller

...but indeed the sprit is very similar to passing in a &mut[..]

Oh sorry, I misunderstood the definition of f2() that @dhm was using: I thought it was referring to returning logical ownership, not a borrowed reference.

Yes, I would expect returning a borrowed reference to not work, even with alloca. Or to be precise, if we choose semantics where it does work, the lifetime of that reference can't escape the loop body. There's actually a very simple reason for this:

fn f2<super 'a, T>() -> &'a T;

What has ownership of T? Something has to own the T value for the reference to be valid. Meanwhile I'd expect this to work, because push is taking ownership of a T value once per iteration:

fn f2<super 'a, T>() -> &'a own T {
    /* ... */
}

fn foo(n: usize) -> Vec<T> {
    for _ in 0 .. n {
        v.push(f2());
    }
}

I think the most clear semantics for this is via &'a own T, which makes clear what is responsible for running drop.

Second, in implementations that do support alloca, I'd suggest ensuring that alloca can only be called once per function call site, and the effect of that call must be undone prior to that function call site happening again. That would ensure that total stack usage is bounded even in loops. Though actually implementing that rule could be tricky.... I haven't thought about this super carefully. But it feels like modulo references, you can at least always come up with some sequence of data moves to undo the effect of an alloca with the above rule. But needs more thought.

Hmm.. I hoped it would be fairly simple. Say, for each super lifetime we parameterize a function invocation with in the caller

  • make sure that this lifetime outlives no enclosing loop
  • limit all closures that it outlives but is enclosed by to implementing FnOnce

Anything else really?

1 Like

Can't yet put a finger on it.. but it seems this sort of buffers/existentials could help support anonymous enum types for example to return errors Zig-style.

Yes this doesn't take trait objects into account yet.. But if you limit yourselves to monomorphisations only it seems entirely feasible to examine your whole call graph and compute what is the biggest number of bytes you need to store an Error which can arise at this point. Then some parent level function could provide the buffer to allocate such errors in, say on its stack.. so that they could be returned by value..

Yes the idea is still very rough

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.