Skip to content

Commit e1e4fb8

Browse files
authored
portable lazy allows nonportable thunk (#3436)
* portable lazy allows nonportable thunk * add documentation * improve documentation * add examples * improve comments in test * say "not stronger"
1 parent c30ec74 commit e1e4fb8

File tree

3 files changed

+53
-19
lines changed

3 files changed

+53
-19
lines changed
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
The goal of this document is to be a reasonably complete reference to the mode system in
2+
OCaml.
3+
4+
<!-- CR zqian: For a gentler introduction, see [the introduction](intro.md). -->
5+
6+
The mode system in the compiler tracks various properties of values, so that certain
7+
performance-enhancing operations can be performed safely. For example:
8+
- Locality tracks escaping. See [the local allocations reference](../local/reference.md)
9+
- Uniqueness and linearity tracks aliasing. See [the uniqueness reference](../uniqueness/reference.md)
10+
- Portability and contention tracks inter-thread sharing.
11+
<!-- CR zqian: reference for portability and contention -->
12+
13+
# Lazy
14+
`lazy e` contains a thunk that evaluates `e`, as well as a mutable cell to store the
15+
result of `e`. Upon construction, the mode of `lazy e` cannot be stronger than `e`. For
16+
example, if `e` is `nonportable`, then `lazy e` cannot be `portable`. Upon destruction
17+
(forcing a lazy value), the result cannot be stronger than the mode of lazy value. For
18+
example, forcing a `nonportable` lazy value cannot give a `portable` result. Additionally,
19+
forcing a lazy value involves accessing the mutable cell and thus requires the lazy value
20+
to be `uncontended`.
21+
22+
Currently, the above rules don't apply to the locality axis, because both the result and
23+
the lazy value are heap-allocated, so they are always `global`.
24+
25+
Additionally, upon construction, the comonadic fragment of `lazy e` cannot be stronger
26+
than the thunk. The thunk is checked as `fun () -> e`, potentially closing over variables,
27+
which weakens its comonadic fragment. This rule doesn't apply to several axes:
28+
- The thunk is always heap-allocated so always `global`.
29+
- Since the thunk is only evaluated if the lazy value is `uncontended`, one can construct
30+
a lazy value at `portable` even if the thunk is `nonportable` (e.g., closing over
31+
`uncontended` or `nonportable` values). For example, the following is allowed:
32+
```ocaml
33+
let r = ref 0 in
34+
let l @ portable = lazy (r := 42) in
35+
```
36+
- Since the thunk runs at most once even if the lazy value is forced multiple times, one
37+
can construct the lazy value at `many` even if the thunk is `once` (e.g., closing over
38+
`unique` or `once` values). For example, the following is allowed:
39+
```ocaml
40+
let r = { x = 0 } in
41+
let l @ many = lazy (overwrite_ r with { x = 42 })
42+
```

testsuite/tests/typing-modes/lazy.ml

+8-19
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,7 @@ let foo (x @ local) =
4343
val foo : local_ 'a lazy_t -> 'a = <fun>
4444
|}]
4545

46-
(* one can construct portable lazy, if both the thunk and the result are
47-
portable *)
46+
(* one can construct [portable] lazy only if the result is [portable] *)
4847
let foo () =
4948
let l = lazy (let x @ nonportable = fun x -> x in x) in
5049
use_portable l
@@ -55,32 +54,21 @@ Line 3, characters 17-18:
5554
Error: This value is "nonportable" but expected to be "portable".
5655
|}]
5756

57+
(* thunk is evaluated only when [uncontended] lazy is forced, so the thunk can be
58+
[nonportable] even if the lazy is [portable]. *)
5859
let foo (x @ nonportable) =
5960
let l = lazy (let _ = x in ()) in
6061
use_portable l
6162
[%%expect{|
62-
Line 3, characters 17-18:
63-
3 | use_portable l
64-
^
65-
Error: This value is "nonportable" but expected to be "portable".
66-
|}]
67-
68-
let foo (x @ portable) =
69-
let l = lazy (let _ = x in let y = fun () -> () in y) in
70-
use_portable l
71-
[%%expect{|
72-
val foo : 'a @ portable -> unit = <fun>
63+
val foo : 'a -> unit = <fun>
7364
|}]
7465

75-
(* inside a portable lazy, things are available as contended *)
66+
(* For the same reason, [portable] lazy can close over things at [uncontended]. *)
7667
let foo (x @ uncontended) =
77-
let l @ portable = lazy ( let x' @ uncontended = x in ()) in
68+
let l @ portable = lazy ( let _x @ uncontended = x in ()) in
7869
use_portable l
7970
[%%expect{|
80-
Line 2, characters 53-54:
81-
2 | let l @ portable = lazy ( let x' @ uncontended = x in ()) in
82-
^
83-
Error: This value is "contended" but expected to be "uncontended".
71+
val foo : 'a -> unit = <fun>
8472
|}]
8573

8674
(* Portable lazy gives portable result *)
@@ -91,6 +79,7 @@ let foo (x @ portable) =
9179
val foo : 'a lazy_t @ portable -> unit = <fun>
9280
|}]
9381

82+
(* Nonportable lazy gives nonportable result *)
9483
let foo (x @ nonportable) =
9584
match x with
9685
| lazy r -> use_portable x

typing/typecore.ml

+3
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,9 @@ let mode_lazy expected_mode =
551551
(* The thunk is evaluated only once, so we only require it to be [once],
552552
even if the [lazy] is [many]. *)
553553
|> Value.join_with (Comonadic Linearity) Linearity.Const.Once
554+
(* The thunk is evaluated only when the [lazy] is [uncontended], so we only require it
555+
to be [nonportable], even if the [lazy] is [portable]. *)
556+
|> Value.join_with (Comonadic Portability) Portability.Const.Nonportable
554557
in
555558
{expected_mode with locality_context = Some Lazy }, closure_mode
556559

0 commit comments

Comments
 (0)