Skip to content
153 changes: 153 additions & 0 deletions text/0000-recover-with-receiver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
- Feature Name: recover-with-receiver
- Start Date: 2020-08-26
- RFC PR: (leave this empty)
- Pony Issue: (leave this empty)

# Summary

This feature will expand recover syntax to allow general usage of
recovery as appears in automatic receiver recovery. The change
will make some use cases possible, while improving the performance
or ergonomics of some other use cases.

# Motivation

Currently, Pony supports two forms of recovery, `recover` blocks,
as well as automatic receiver recovery. In some cases, these are
equivalent. If we have a single variable `x: T iso`, then we can
temporarily use it as another capability inside a recover block,
with something like:
```
x = recover
let x_ref = consume ref x
// do something with x ...
x_ref
end
```
Alternatively, if the action being taken is precisely a ref method call,
then automatic receiver recovery can be used if the
arguments and return types meet the isolation guarantees for `x`.
```
x.foo(y, z)
```
But this can't be used for every type of action, it needs to be those
actions thought of by the original class developer. We can add these methods,
but this is anti-modular.

This automatic receiver recovery syntax also works for expressions more complicated than a single variable of course.
The recover block is less flexible. The recover block method can be used only when the thing being modified is a mutable location.
It can be used with var fields, but not with let or embed. It can be used when we have update methods, but not with getters alone.
```
// defined elsehwere
class Foo
fun box getSomething(): this->Bar ref
...
fun box values(): this->FooIterator ref

class FooHolder
embed foo: Foo iso = Foo
// or let

fun ref doSomethingWithFoo() =>
// error, iso->ref = tag
foo.getSomething().somethingElse()

// try to recover to use foo as ref: error, can't assign
foo = recover
... consume foo
end
end
```
We might also have read-only methods. Imagine we take in an Iter over iso objects. We don't want to be coupled to
the class used, such as Array, and allow a generic, potentially chained iterator.
```
class UsesIter[T: SomeInterface]
fun process(iter: Iterator[T]) =>
// want to call a few complicated methods on T
// if T might be unique, we can't store in a variable,
// so we want to recover, but we can't!

// error, not a subtype
let next: T = iter.next()?

// ????
iter.next() = recover
...
end

// works... but only if we can
// express *all* of the things we want
// to do as multiple methods
// still anti-modular!
iter.next()?.foo().>bar()
end
```

This RFC will add a syntax to expand the design of recover blocks to allow a receiver, subsuming automatic receiver recovery.
In both cases above, the recover with receiver may be used in order to temporarily use these values as ref, allowing free
usage of methods, without requiring that the methods were defined ahead of time in the interface or class, and without
requiring extra potentially erroring accesses or allocating and swapping new values via update methods.

# Detailed design

We will add new syntactic forms to allow recover blocks based around an existing receiver expression.

```
e.recover | x =>
e
end
```
and a shorthand
```
e.recover
e
end
```
Where in both cases, `e` is an expression, and `x` is a variable binding. In the second case, the first `e` should be either a variable or a field access.
Inside the body of the recover block, the variable `x` will be bound as a `let` binding. For the shorthand, the name of this variable will be the name
of the variable that the expression is, or the rightmost field name.

The capability of the new binding will depend on the capability of the expression. If it is a unique capability, `iso` or `trn`, then the resulting capability
will be the strongest aliasable type: `ref`. If it is any self-aliasing capability `k`, then the resulting capability will be `k`.
Acknowledging that there may be better choices available, at this time `iso^` or `trn^` will take the capability `ref` and act identically to their
non-ephemeral counterparts. Any variables syntactically present in the receiver expression are considered in-use for the duration of the block and cannot be consumed or re-assigned.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that strong enough of a restriction? Is it possible that some unsafe chicanery could be done with destructive assign to fields of an in-use variable, even if the in-use variable is not consumed?

Copy link
Author

@jasoncarr0 jasoncarr0 Aug 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, you're right that this wasn't written nearly as restrictive as it should be. This condition should not deviate from the soundness guaranteed by normal function calls and by recover blocks. In order to not stretch beyond what would be available to a function call, we can require that the capability be iso^, val or tag.

After looking back at ponylang/ponyc#3596 I'm not convinced that the cases we gave were unsound (an iso temporary is compatible with an iso variable in the short-term, we just need to conflate them for consumes), but that this "potentially aliases" notion is a bit more complex than just the variables which originally existed. So unless there's a very clever solution here I think we should stick with the restrictions above of requiring iso^ and not iso expressions, as by its nature iso^ can never conflict with anything.

Copy link
Author

@jasoncarr0 jasoncarr0 Aug 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may not be clear what the restriction to iso^ does, it means that we cannot call any mutable methods, and hence cannot do anything else that could invalidate. We still should block consuming/assigning the variables themselves, as we would in the case of a function call (and if it is not the case that we do so, then we can log a soundness bug).


The body of the recover expression will be type-checked similarly to how recover blocks are checked today, with two exceptions. The block will have
a capability associated with it, and instead of restricting to sendable variable usage, they are restricted to capabilities which are safe-to-write.
In practice, the only special case here is writing `trn` to `trn. The result of the recover block will be adapted in the the viewpoint of the
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just leaving a note to discuss this (trn recovers) properly. I'm becoming less and less sure that this would be the best for Pony.

recover block. This subsumes the existing conditions of being either unused or safe to extract.

If the receiver capability is not a unique cap (`iso` or `trn`), then this environment is always treated as `ref` and there are no restrictions on used or returned variables.

For a method call to a `ref` method, it is treated as being wrapped in an implicit receiver recovery block. That is,
`x.f(y, z)` can be de-sugared to `x.recover x.f(y, z) end`, using the shorthand syntax above.

For implementation, each recover block will have an optional receiver and a capability of the recover (note that this capability is different than the return capability of a regular recover block). Until the adoption of the more permissive viewpoint adaptation for ephemerals, we will have to treat recover blocks without receiver a special case. A sensible choice would be to mark all such blocks as capability `iso^`. When checking expressions for the recover block, sendability restrictions will be checked relative to the block. Return values would be checked with viewpoint adaptation as specified, except for standard recover blocks, which will use existing rules.

# How We Teach This

We can refer to this feature as either reciever recovery or recovery with receiver. The section on recover blocks will be modified with an additional section to
reflect the new type of recover blocks. In this setting we may wish to make a footnote as to `trn` receivers having looser isolation requirements.
Examples should reflect some of the previously impossible use cases above, as this helps in explaining usage of isolated capabilities in data structures.

The existing cases of automatic recovery, when calling ref methods, and constructors, will be presented together as conveniences.

# How We Test This

This will require additional tests for different receivers and both of the unique capabilities. Existing tests around automatic receiver recovery should be maintained and should continue to pass.

# Drawbacks

Why should we *not* do this? Things you might want to note:

* This may frontload recovery concepts slightly sooner for learners, rather than just presenting receiver recovery for functions
* Generic technical costs of new features

# Alternatives

We may try to expand automated recovery to handle more cases like the above, at the cost of a lack of simplicity.

# Unresolved questions

The syntax may still need work.
Research has not fully caught up to more powerful recovery mechanisms as a general detail.