Skip to content

Commit

Permalink
Make working with unique values easier
Browse files Browse the repository at this point in the history
`recover` expressions now have access to values defined outside of them.
These values are exposed as `uni ref T`/`uni mut T` and come with the
same requirements as `uni T` types, such as methods only being available
of their input and output is sendable. Fields in turn are also available
for `uni T` receivers or when typed as `uni T`, and are exposed the same
way.

The result is that it's easier to work with unique values, without the
need for further complicating the type system. For example, you can now
do this:

    import std::net::ip::IpAddress

    class Config {
      let @ip: uni IpAddress
    }

    fn example(config: ref Config) -> uni IpAddress {
      recover config.ip.clone
    }

In general this should remove the need for using `uni T` as return types
all over the place, while still allowing you to easily move values
between processes.

This fixes #528.

Changelog: added
  • Loading branch information
yorickpeterse committed May 27, 2023
1 parent f004de4 commit f8bd249
Show file tree
Hide file tree
Showing 10 changed files with 163 additions and 381 deletions.
19 changes: 0 additions & 19 deletions compiler/src/diagnostics.rs
Expand Up @@ -522,25 +522,6 @@ impl Diagnostics {
);
}

pub(crate) fn unsendable_field(
&mut self,
field_name: &str,
type_name: String,
file: PathBuf,
location: SourceLocation,
) {
self.error(
DiagnosticId::InvalidSymbol,
format!(
"The field '{}' can't be read as its type ('{}') \
isn't sendable",
field_name, type_name
),
file,
location,
);
}

pub(crate) fn unsendable_argument(
&mut self,
argument: String,
Expand Down
36 changes: 8 additions & 28 deletions compiler/src/type_check/expressions.rs
Expand Up @@ -3739,15 +3739,8 @@ impl<'a> CheckMethodBody<'a> {
returns = returns.as_mut(self.db_mut());
}

if receiver.require_sendable_arguments(self.db())
&& !returns.is_sendable(self.db())
{
self.state.diagnostics.unsendable_field(
name,
self.fmt(returns),
self.file(),
node.location.clone(),
);
if receiver.require_sendable_arguments(self.db()) {
returns = returns.as_uni_ref(self.db());
}

node.kind = CallKind::GetField(FieldInfo {
Expand Down Expand Up @@ -4260,28 +4253,15 @@ impl<'a> CheckMethodBody<'a> {
while let Some(current) = source {
if let Some(variable) = current.variables.variable(name) {
let var_type = variable.value_type(db);
let mut expose_as = var_type;

if crossed_uni && !var_type.is_sendable(db) {
self.state.diagnostics.error(
DiagnosticId::InvalidSymbol,
format!(
"The variable '{}' exists, but its type ('{}') \
prohibits it from being captured by a recover \
expression",
name,
self.fmt(var_type)
),
self.file(),
location.clone(),
);
if crossed_uni {
expose_as = expose_as.as_uni_ref(self.db());
}

let expose_as =
if captured && var_type.is_owned_or_uni(self.db()) {
var_type.as_mut(self.db())
} else {
var_type
};
if captured && var_type.is_owned_or_uni(self.db()) {
expose_as = expose_as.as_mut(self.db());
}

for closure in capturing {
let moving = closure.is_moving(self.db());
Expand Down
70 changes: 41 additions & 29 deletions docs/source/getting-started/memory-management.md
Expand Up @@ -140,26 +140,37 @@ Unlike Pony, Inko doesn't have a long list of (complicated) reference
capabilities, making it easier to use Inko while still achieving the same
safety guarantees.

Unique values are created using the `recover` expression. Within this
expression, outside variables are only visible if they are unique values or
value types; everything else is hidden. If the return value of a `recover`
expression is an owned value, it's turned into a unique value. If the value
already is a unique value, it's instead turned into an owned value.
Unique values are created using the `recover` expression, and the return value
of such an expression is turned from a `T` into `uni T`, or from a `uni T` into
a `T`; depending on what the original type is:

```inko
let a = [10, 20]
let b = recover a # Not possible, because `a` is owned and thus not visible
let c = recover [10, 20] # `c` is of type `uni Array[Int]`
let a = recover [10, 20] # => uni Array[Int]
let b = recover a # => Array[Int]
```

This is safe because of the following: if the only outside values we can refer
to are unique values, then any owned value returned must originate from inside
the `recover` expression. This in turn means any references created to it are
either stored inside the value (which is fine), or are discarded before the end
of the `recover` expression. That in turn means that after the `recover`
expression returns, we know for a fact no outside references to the unique value
exist, nor can the unique value contain any references to values stored outside
of itself.
Variables defined outside of the `recover` expression are exposed as `uni mut T`
or `uni ref T`, depending on what the original type is. Such values come with
the same restriction as `uni T` values, which are discussed in detail in the section
[Using unique values](#using-unique-values):

```inko
let nums = [10, 20, 30]
recover {
nums # => uni mut Array[Int]
[10]
}
```

Using `recover` we can statically guarantee it's safe to send values between
processes: if the only outside values we can refer to are unique values, then
any owned value returned must originate from inside the `recover` expression.
This in turn means any references created to it are either stored inside the
value (which is fine), or are discarded before the end of the `recover`
expression. That in turn means that after the `recover` expression returns, we
know for a fact no outside references to the unique value exist, nor can the
unique value contain any references to values stored outside of itself.

Values recovered using `recover` are moved, meaning that the old variable
containing the owned value is no longer available:
Expand All @@ -175,17 +186,17 @@ In general recovery is only needed when sending values between processes.

## Using unique values

When we said you can't create references to unique values this wasn't entirely
true: you _can_ create references to such values (known as
`ref uni T` or `mut uni T` references), but such references come with
restrictions to ensure correctness. For example, such references can't be
assigned to variables, nor are they allowed in explicit type signatures. This
means the only place they can be used in is temporary/intermediate expressions
not assigned to anything. This in turn means that it's safe to move a unique
value afterwards, because the references no longer exist.
Values of type `uni T`, `uni ref T` and `uni mut T` come with a variety of
restrictions to ensure their uniqueness constraints are maintained.

Values of type `uni ref T` and `uni mut T` can't be assigned to variables, nor
can you pass them to arguments that expect `ref T` or `mut T`. You also can't
use such types in type signatures. This means you can only use them as receivers
for method calls. As such, these kind of references don't violate the uniqueness
constraint of the `uni T` values they point to.

Both unique references and owned values allow you to call methods on them,
though this comes with a set of rules. These rules are as follows:
All three unique reference types allow you to call methods on values of such
types, provided the call meets the following criteria:

1. If a method takes any arguments and/or specifies a return type, these types
must be "sendable". If any of these types isn't sendable, the method isn't
Expand Down Expand Up @@ -228,18 +239,19 @@ import std::net::socket::TcpServer
class async Main {
fn async main {
let server = recover try! TcpServer
let server = recover TcpServer
.new(ip: IpAddress.v4(127, 0, 0, 1), port: 40_000)
.unwrap
let client = recover try! server.accept
let client = recover server.accept.unwrap
}
}
```

Here `server` is of type `uni TcpServer`. The expression `server.accept` is
valid because `server` is unique and thus we can see it, and because `accept`
meets rule two: it doesn't mutate its receiver, doesn't take any arguments, and
the types it throws/returns only store sendable types.
the types it returns only store sendable types.

Here's an example of something that isn't valid:

Expand Down
1 change: 0 additions & 1 deletion docs/vale/docs/too_wordy.yml
Expand Up @@ -130,7 +130,6 @@ tokens:
- it is essential
- it is
- it seems that
- it was
- magnitude
- methodology
- minimize
Expand Down

0 comments on commit f8bd249

Please sign in to comment.