Skip to content

Commit 4f08289

Browse files
committed
Infer return types as sendable for mutable methods
In addition to inferring return types as sendable for immutable methods, we now do the same for mutable methods _if and only if_ it's guaranteed the receiver won't be able to store _any_ aliases (i.e. borrows). When working with files and sockets (and similar types) this makes it easier to work with mutating methods, removing the need for manually recovering the values to owned values and then recovering them back to unique values. For example, it's now possible to write this: import std.fs.file (WriteOnlyFile) let file = recover WriteOnlyFile.new('/tmp/test.txt'.to_path).get file.write_string('hello').get Prior to this commit this wouldn't be possible as `write_string` is a mutable method, requiring you to write something along the lines of this instead: import std.fs.file (WriteOnlyFile) let mut file = recover { WriteOnlyFile.new('/tmp/test.txt'.to_path).get } file = recover { let file = recover file file.write_string('hello').get file } This fixes #589. Changelog: added
1 parent 278c9ea commit 4f08289

File tree

3 files changed

+103
-37
lines changed

3 files changed

+103
-37
lines changed

compiler/src/type_check/expressions.rs

+21-8
Original file line numberDiff line numberDiff line change
@@ -536,10 +536,12 @@ impl MethodCall {
536536
return;
537537
}
538538

539+
let immutable = self.method.is_immutable(&state.db);
540+
539541
// It's safe to pass `ref T` as an argument if all arguments and `self`
540542
// are immutable, as this prevents storing of the `ref T` in `self`,
541543
// thus violating the uniqueness constraints.
542-
let ref_safe = self.method.is_immutable(&state.db)
544+
let ref_safe = immutable
543545
&& self.check_sendable.iter().all(|(typ, _)| {
544546
typ.is_sendable(&state.db) || typ.is_sendable_ref(&state.db)
545547
});
@@ -566,13 +568,24 @@ impl MethodCall {
566568
return;
567569
}
568570

569-
// If `self` and all arguments are immutable, we allow owned return
570-
// values provided they don't contain any references. This is safe
571-
// because `self` can't have references to it (because it's immutable),
572-
// we can't "leak" a reference through the arguments (because they too
573-
// are immutable), and the returned value can't refer to `self` because
574-
// we don't allow references anywhere in the type or its sub types.
575-
let ret_sendable = if ref_safe {
571+
// In certain cases it's fine to allow non-unique owned values to be
572+
// returned, provided we can guarantee (based on what we know at the
573+
// call site) no uniqueness constaints are violated.
574+
//
575+
// For immutable methods, if all the arguments are sendable then
576+
// returned owned values can't be aliased by the callee.
577+
//
578+
// For mutable methods, we additionally require that the receiver can't
579+
// ever store aliases to the returned data. Since the receiver is likely
580+
// typed as `uni T` (which itself is sendable) we perform that check
581+
// against its owned counterpart.
582+
let ret_sendable = if ref_safe
583+
|| (!immutable
584+
&& self
585+
.receiver
586+
.as_owned(&state.db)
587+
.is_sendable_output(&state.db))
588+
{
576589
self.return_type.is_sendable_output(&state.db)
577590
} else {
578591
self.return_type.is_sendable(&state.db)

docs/source/getting-started/concurrency.md

+29-29
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ any values returned must be sendable for the method to be available. Since this
124124
is overly strict in many instances, the compiler relaxes this rule whenever it
125125
determines it's safe to do so. These exceptions are listed below.
126126

127-
### Immutable methods and arguments
127+
### Immutable methods
128128

129129
If a method isn't able to mutate its receiver because it's defined as `fn`
130130
instead of `fn mut`, it's safe to pass immutable borrows as arguments (which
@@ -155,69 +155,69 @@ its `user` argument can't be stored in the `uni User` value stored in `alice`.
155155
This _isn't_ possible if the method allows mutations (= it's an `fn mut` method)
156156
because that would allow it to store the `ref User` in `self`.
157157

158-
### Non-unique return values
158+
### Mutable methods
159159

160-
If the return type of a method is owned and not unique (e.g. `Array[String]`
161-
instead of `uni Array[String]`), the method _is_ available if it either doesn't
162-
specify any arguments, all arguments are immutable borrows or all arguments
163-
are sendable, _and_ the returned value doesn't contain any borrows:
160+
There's an exception to the previous rule when it comes to the use of mutating
161+
methods on unique receivers: if the compiler can guarantee the receiver can't
162+
store any aliases to the returned data, it's in fact safe to use a mutating
163+
method:
164164

165165
```inko
166166
type User {
167167
let @name: String
168168
let @friends: Array[String]
169169
170-
fn friends -> Array[String] {
171-
@friends.clone
170+
fn mut remove_last_friend -> Option[String] {
171+
@friends.pop
172172
}
173173
}
174174
175175
type async Main {
176176
fn async main {
177177
let alice = recover User(name: 'Alice', friends: ['Bob'])
178-
179-
alice.friends
178+
let friend = alice.remove_last_friend
180179
}
181180
}
182181
```
183182

184-
Here the call `alice.friends` is valid because:
183+
Here the call to `User.remove_last_friend` _is_ allowed because the `User` type
184+
doesn't store any borrows, nor do any sub values (e.g. the `Array` stored in the
185+
`friends` field).
185186

186-
1. `User.friends` is immutable
187-
1. `User.friends` doesn't accept any arguments
188-
1. Because of this the `Array[String]` value can only be created from within
189-
`User.friends` and no aliases to it can exist upon returning it
187+
### Non-unique return values
190188

191-
This isn't the case if `User.friends` is an `fn mut` method, because now the
192-
value might originate from `self`:
189+
If the return type of a method is owned and not unique (e.g. `Array[String]`
190+
instead of `uni Array[String]`), the method _is_ available if it either doesn't
191+
specify any arguments, all arguments are immutable borrows or all arguments
192+
are sendable, _and_ the returned value doesn't contain any borrows:
193193

194194
```inko
195195
type User {
196196
let @name: String
197-
let mut @friends: Array[String]
197+
let @friends: Array[String]
198198
199-
fn mut friends -> Array[String] {
200-
@friends := []
199+
fn friends -> Array[String] {
200+
@friends.clone
201201
}
202202
}
203203
204204
type async Main {
205205
fn async main {
206206
let alice = recover User(name: 'Alice', friends: ['Bob'])
207-
let bob = User(name: 'Bob', friends: ['Alice'])
208-
let friends = alice.friends
207+
208+
alice.friends
209209
}
210210
}
211211
```
212212

213-
If you try to build this, the compiler produces the following error:
213+
Here the call `alice.friends` is valid because:
214214

215-
```
216-
test.inko:14:25 error(invalid-call): the receiver of this call requires a sendable return type, but 'Array[String]' isn't sendable
217-
```
215+
1. `User.friends` is immutable
216+
1. `User.friends` doesn't accept any arguments
217+
1. Because of this the `Array[String]` value can only be created from within
218+
`User.friends` and no aliases to it can exist upon returning it
218219

219-
Similarly, the call isn't valid if the returned value contains borrows. For
220-
example:
220+
The call isn't valid if the returned value contains borrows. For example:
221221

222222
```inko
223223
type User {
@@ -287,7 +287,7 @@ type async Main {
287287
Here the call to `alice.wrap` is invalid because `Wrapper` defines a field of
288288
type `ref User`, which isn't sendable.
289289

290-
### Unsendable and unused return values
290+
### Unused return values
291291

292292
There's an exception to the above rule: if the returned value isn't sendable but
293293
also isn't used by the caller, the method _can_ be used:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
type HasBorrow {
2+
let @a: Array[Int]
3+
let @b: ref Array[Int]
4+
5+
fn static new -> Self {
6+
let a = [10, 20]
7+
let b = ref a
8+
9+
Self(a: a, b: b)
10+
}
11+
12+
fn immutable -> Result[Int, String] {
13+
Result.Ok(42)
14+
}
15+
16+
fn mut mutable -> Result[Int, String] {
17+
Result.Ok(42)
18+
}
19+
}
20+
21+
type NoBorrow {
22+
let @a: Array[Int]
23+
24+
fn static new -> Self {
25+
Self([10, 20])
26+
}
27+
28+
fn immutable -> Result[Int, String] {
29+
Result.Ok(42)
30+
}
31+
32+
fn mut mutable -> Result[Int, String] {
33+
Result.Ok(42)
34+
}
35+
}
36+
37+
fn example1(value: uni HasBorrow) {
38+
let _used = value.immutable
39+
}
40+
41+
fn example2(value: uni HasBorrow) {
42+
let _used = value.mutable
43+
}
44+
45+
fn example3(value: uni NoBorrow) {
46+
let _used = value.immutable
47+
}
48+
49+
fn example4(value: uni NoBorrow) {
50+
let _used = value.mutable
51+
}
52+
53+
# uni_with_mutable_methods.inko:42:21 error(invalid-call): the receiver of this call requires a sendable return type, but 'Result[Int, String]' isn't sendable

0 commit comments

Comments
 (0)