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.