Source guide/lifecycle.simd

1# Lifecycle: Modes, Dispose, and Memory
2
3Silo has no garbage collector. Memory is managed by a
4combination of **region allocation** (heap values live in a
5region that frees as a unit when the region goes away) and
6**Perceus reference counting** (per-value RC, elided and
7in-placed by the compiler wherever possible). The programmer
8writes ordinary code; the runtime handles the bookkeeping.
9
10Alongside the allocator there are two opt-in disciplines that
11let the type system enforce stricter lifetime invariants:
12**locality modes** and `:dispose`. This chapter covers all
13three.
14
15## Modes (locality)
16
17A mode is a tag on a type that restricts where its values can
18go. Modes are declared in parentheses after the keyword that
19introduces the type, next to visibility:
20
21```silo
22:record(pub local)  ReadGuard t  ...  :end
23:record(pub unique) Transaction k     :end
24:record(pub linear) CriticalResource  :end
25```
26
27Four modes, in order of increasing strictness:
28
29| Mode     | Escape scope? | Aliasable? | Implicit drop? |
30|----------|---------------|------------|----------------|
31| `global` | yes           | yes        | yes            |
32| `local`  | **no**        | yes        | yes            |
33| `unique` | no            | **no**     | yes            |
34| `linear` | no            | no         | **no**         |
35
36`global` is the default — ordinary values that behave the way
37you'd expect, with the compiler managing RC.
38
39### `local`
40
41A `local` value cannot escape the scope it was created in. It
42can't be returned, can't be stored in a longer-lived
43container, can't cross a thread boundary via `.spawn`. A
44textbook case is an RAII guard that must not outlive its
45region of acquisition:
46
47```silo
48:record(pub local) ReadGuard t
49  .lock (RwLock t)
50:end
51
52:fn use-guard ( (RwLock Int) -> Int ) +Concurrent
53  .read-lock pop-> guard         # guard is local
54  guard .get                      # OK — used on the stack
55:end
56```
57
58Trying to `:ret` a `ReadGuard`, store it in a longer-lived
59record, or pass it to `.spawn` is a compile error. Because
60the mode is known at the type level, the compiler catches
61every escape route.
62
63### `unique`
64
65`unique` adds "no aliasing" to `local`'s constraints. You
66can't `peek->` a unique value (which would create an alias);
67you can only `pop->` it, which consumes it. There is no
68implicit duplication — any shared use has to be an explicit
69pattern match or consumption.
70
71```silo
72:record(pub unique) Transaction k :end
73
74:fn use-tx ( (Transaction k) -> ) +Database
75  pop-> tx
76  tx "SELECT ..." .query pop-> tx pop-> cursor
77  cursor .fetch drop
78  tx .commit
79:end
80```
81
82`tx peek-> tx2` would be a compile error here — `Transaction`
83is unique and can't be aliased.
84
85### `linear`
86
87`linear` adds "must be consumed" on top of `unique`. A linear
88value is not allowed to be dropped implicitly; the compiler
89checks every control-flow path and requires an explicit
90`:dispose` method to finish it off:
91
92```silo
93:record(pub linear) CriticalResource :end
94
95:impl CriticalResource
96:dispose
97  .release ( Self -> ) +IO ... ;
98  .abort   ( Self -> ) +IO ... ;
99:end
100```
101
102No `_` default in the `:dispose` block means the linear
103discipline has no implicit exit — the compiler refuses to
104let a `CriticalResource` leave scope without a `.release` or
105`.abort` call on every path.
106
107```silo
108:fn process ( CriticalResource -> ) +IO
109  .use .release                   # explicit consumption — OK
110:end
111```
112
113Forgetting `.release` (or `.abort`) is a compile error.
114
115## `:dispose`
116
117`:dispose` blocks live inside an `:impl` and declare how a
118value can be consumed at the end of its scope. There are two
119kinds of `:dispose` method:
120
121- **Default** — `_ ( Self -> ) body ;`. Called by the compiler
122  when a value reaches end-of-scope without being explicitly
123  disposed. At most one default per `:dispose` block.
124- **Named** — `.method-name ( Self -> ) body ;`. Ordinary
125  methods you can call to consume the value explicitly.
126
127```silo
128:impl ReadGuard
129  .get ( Self -> Self t ) ... ;
130:dispose
131  _ ( Self -> ) unlock ;         # default: unlock on scope exit
132  .release ( Self -> ) unlock ;  # explicit alternative
133:end
134```
135
136For `ReadGuard`, either dropping it at scope end or calling
137`.release` unlocks — both go through the same underlying
138`unlock` word.
139
140For a `Transaction`, there is no default; every exit has to
141be named:
142
143```silo
144:impl Transaction
145  .query ( Self Str -> Self (Cursor k t) ) +Database ... ;
146:dispose
147  .commit   ( Self -> ) +Database ... ;
148  .rollback ( Self -> ) +Database ... ;
149:end
150```
151
152The lack of a default (`_`) pairs naturally with the `linear`
153mode: the type says "must be disposed", and the `:dispose`
154block says "here are the ways to do it".
155
156## Memory management
157
158Underneath modes and dispose, ordinary values are managed by a
159combination of **regions** and **Perceus**.
160
161A **region** is a chunk of memory allocated and freed as a
162unit. Each function call gets its own region; when the function
163returns, the region is freed wholesale. Faster than per-value
164`free`, and immune to use-after-free on values that stay
165within the region.
166
167**Perceus** is the reference-counting discipline. Most heap
168values carry an RC, but the compiler optimises it aggressively:
169
170- **Uniqueness fast path**: when the compiler can prove `RC=1`
171  at a mutation site, the mutation happens in place. A `.push`
172  on an unshared `Vec` does not allocate — it extends the
173  existing allocation.
174
175```silo
17642 my-vec .push              # mutates in-place when my-vec is unshared
177```
178
179- **Copy-on-write when shared**: when `RC>1`, the same
180  mutation allocates a fresh value so the shared version is
181  preserved:
182
183```silo
184my-vec peek-> shared         # RC becomes 2
18542 shared .push              # allocates a fresh Vec; my-vec unchanged
186```
187
188- **No user-facing dup/drop**. Silo has stack-shuffler words
189  `dup`/`drop`/`swap`/`over`/`rot`/`nip` (and their `/2`
190  variants, [chapter 2](./02-stack-and-words.simd)); what it
191  does **not** have is a user-facing way to manipulate
192  reference counts. The compiler inserts the RC operations
193  where they're needed, elides them where it can prove they're
194  unnecessary, and the programmer never sees them.
195
196The practical upshot: functional-looking code runs with
197imperative-looking performance most of the time. If the
198compiler can see your `Vec` isn't shared, `.push` mutates; if
199it can't, `.push` copies. Either way the semantics are
200pure-functional.
201
202## When to reach for modes
203
204Most code is `global`. The compiler handles everything —
205you're writing ordinary functional-looking code.
206
207Reach for `local` when a value genuinely must not escape
208(RAII guards, scoped capabilities, read-only borrows that
209shouldn't outlive the lock).
210
211Reach for `unique` when sharing would break an invariant
212(file descriptors, database transactions, anything where two
213aliases could step on each other).
214
215Reach for `linear` when forgetting to clean up is a bug that
216shouldn't be caught at runtime (resources with explicit
217commit/rollback, protocols with a mandated close step).
218
219The type system carries the discipline; there is no runtime
220tax.
221
222## Key points
223
224- Four locality modes: `global` (default),
225  :gloss[`local`](./A1-glossary.simd#linear-type) (can't escape),
226  `unique` (can't alias), `linear` (must consume explicitly).
227- `:dispose` blocks list the exit paths for a value. `_ ( Self
228  -> ) body ;` is the implicit-drop default; named methods
229  are explicit consumers.
230- Linear types have no default `_` — the compiler requires an
231  explicit named consumer on every control-flow path.
232- Memory is region-allocated; per-value RC is Perceus-style
233  with in-place mutation when `RC=1` and copy-on-write when
234  shared.
235- No user-facing dup/drop on references. Stack shufflers
236  exist for primitive reordering; RC is the compiler's job.
237
238Next: [concurrency](./18-concurrency.simd) — tasks, threads,
239sync primitives, and how the modes from this chapter enforce
240thread safety.