Source guide/control-flow.simd
1# Control Flow 2 3Control flow in Silo is built out of five keywords and two library 4words. Each of them has a clean stack contract: it consumes a known 5shape from the stack and leaves a known shape behind. The compiler 6checks that every branch of a conditional produces the same output 7shape — so you can't accidentally have an `:if` arm that leaves one 8value on the stack and an `:else` arm that leaves two. 9 10If you've used Rust, the analogues are close enough to read at sight: 11 12| Silo | Rust | 13|-------------------|------------------------------| 14| `:if … :else …` | `if … { … } else { … }` | 15| `:match` | `match` | 16| `:if-let` | `if let PAT = … { … } else { … }` | 17| `:let` | `let PAT = … else { … };` (bindings escape into scope) | 18| `:loop / :break` | `loop { … break; … }` | 19| `:ret` | `return` | 20| `:try` | `?` (the try operator) | 21| `while` | `while` (as a library fn) | 22| `times` | `for _ in 0..n { … }` (as a library fn) | 23 24## `:if` and `:else` 25 26`:if` pops a `Bool` off the stack. If it's `true`, the body runs; if 27`false`, the `:else` arm runs — or nothing happens if there's no 28`:else`. 29 30```silo 31:fn abs ( Int -> Int ) 32 dup 0 < :if neg :end 33:end 34``` 35 36Reading left to right: duplicate the top of the stack (so we still have 37it after the test), push `0`, compare with `<`, which consumes both 38numbers and pushes a `Bool`. `:if` pops that `Bool`; if it was `true`, 39`neg` runs. Either way the function leaves one `Int` on the stack. 40 41With an `:else` branch, you compute different values on each side: 42 43```silo 44:fn max ( Int Int -> Int ) 45 pop-> b pop-> a 46 a b > :if a :else b :end 47:end 48``` 49 50The compiler requires both arms to leave the stack in the same shape. 51If one arm pushed a `Str` and the other pushed an `Int`, the program 52would not compile. 53 54## `:match` 55 56`:match` pops the value on top of the stack and dispatches on its 57shape. Patterns cover the full range of the type — matching on an `Int` 58you can use literals, ranges, and a wildcard; matching on a union you 59destructure each variant. 60 61```silo 62:fn describe ( Int -> Str ) 63 :match 64 | 0 => "zero" 65 | 1 => "one" 66 | 2..10 => "a few" 67 | _ => "many" 68 :end 69:end 70``` 71 72Patterns are checked for **:gloss[exhaustiveness](./A1-glossary.simd#exhaustiveness)**: forgetting the `_` case 73above would be a compile error because integers outside the listed 74ranges would be unhandled. The full pattern grammar — unions, records, 75literals, ranges, wildcards, nested — has its own chapter 76([Pattern Matching](./08-pattern-matching.simd)). 77 78## `:if-let` and `:let` 79 80When you only care about one pattern — typically unwrapping an 81`Option` or a `Result` — a full `:match` is noisy. Silo borrows two 82constructs from Rust for these cases. 83 84**`:if-let`** is exactly Rust's `if let … else`: try a pattern, run 85one body on success, another on failure. The scrutinee sits on the 86stack beforehand. 87 88```silo 8942 Some :if-let Some v => 90 v 1 + 91:else 92 0 93:end 94# Leaves 43 on the stack. 95``` 96 97Both arms must produce matching stack shapes, same as `:if`. The 98construct desugars at parse time to a two-arm `:match` with a 99wildcard fallback, so the exhaustiveness story carries over 100automatically. 101 102**`:let`** is Rust's `let … else`: unwrap or diverge. On success, the 103pattern's bindings stay in scope *after* the `:end`. On failure, the 104`:else` branch must diverge — it has to call `:ret`, `:break`, 105`panic`, or otherwise not return normally — because there's no 106value to continue with. 107 108```silo 109:fn incr-or-bail ( (Option Int) -> Int ) 110 :let Some v :else 111 :ret # bail out; v never gets bound 112 :end 113 v 1 + # v is in scope here 114:end 115``` 116 117A more interesting case: unwrap or re-raise as `Err`: 118 119```silo 120:fn require-name ( (Option Str) -> (Result Str Str) ) 121 :let Some name :else 122 "missing name" Err :ret 123 :end 124 name Ok 125:end 126``` 127 128`:let` is how you write code that says "I expect this to succeed, and 129if it doesn't I have a clear exit strategy." It keeps the happy path 130unindented instead of burying the whole rest of the word inside a 131`:match` arm. 132 133## `:loop` and `:break` 134 135`:loop` is an unconditional loop. You exit it with `:break`: 136 137```silo 138:fn countdown ( Int -> ) +Console +Div 139 :loop 140 dup 0 <= :if drop :break :end 141 dup "{}" format print 142 1 - 143 :end 144:end 145``` 146 147The `+Div` annotation is the interesting bit. Silo is 148**:gloss[total by default](./A1-glossary.simd#total)**: every function 149must be proved to terminate on every input, and the compiler does 150the proving. A structural recursion where the argument shrinks is 151trivially total. But `:loop` with a runtime break condition can't 152be proved to terminate statically, so it carries the `+Div` 153(:gloss[divergence](./A1-glossary.simd#divergence)) effect. Callers of 154`countdown` see `+Div` in its signature and know it might loop 155forever. 156 157`+Div` is covered in detail in 158[the effects chapter](./10-effects.simd); for now, think of it as a 159compiler-visible note saying "I cannot guarantee this terminates". 160 161## `:ret` — early return 162 163Whatever is on the stack when the word finishes is its return value. 164`:ret` lets you finish earlier, from inside a nested block: 165 166```silo 167:fn find-first ( (Vec Int) Int -> (Option Int) ) 168 pop-> target pop-> vec 169 0..vec .len [ pop-> i 170 i vec .get unwrap target = :if 171 i Some :ret 172 :end 173 ] .for-each 174 None 175:end 176``` 177 178When the predicate fires, `i Some :ret` pushes `Some(i)` and returns. 179If the loop finishes without firing, control falls through to `None`, 180which becomes the return value. The stack shape at the point of `:ret` 181must match the word's declared output — that's how the compiler keeps 182the effect signature honest. 183 184## `:try` — unwrap or short-circuit 185 186`:try` is the Silo analogue of Rust's `?` operator. It unwraps an 187`(Ok val)` and continues with `val` on the stack, or short-circuits the 188enclosing word by returning the `(Err e)` unchanged: 189 190```silo 191:fn process ( Str -> (Result Int Str) ) 192 parse-int :try # if Err, returns Err immediately 193 2 * Ok # if Ok, continues with unwrapped value 194:end 195``` 196 197`:try` only works inside a word whose declared return type is itself 198a `(Result _ _)` with a compatible error type. The compiler checks 199this. If you reach for `:try` in a word that returns a plain value, 200it won't compile — you'll need `:match` to handle the `Err` case 201explicitly. 202 203## `while` and `times` — library words, not keywords 204 205Silo keeps the language surface small by moving most looping into the 206standard library. `while` and `times` are ordinary words that take 207quotations: 208 209```silo 210:fn gcd ( Int Int -> Int ) +Div 211 [ dup 0 ≠ ] [ swap over mod ] while drop 212:end 213 2145 [ "hello" print ] times 215``` 216 217`while` takes a condition quotation and a body quotation, both of type 218`[ -> Bool ]` and `[ -> ]` plus whatever effects. `times` takes a count 219and a body, calling the body that many times. They compose with the 220rest of the language exactly because they're not special — nothing 221stops you from defining your own. 222 223A much richer family of higher-order iteration primitives — `.map`, 224`.filter`, `.fold`, `.for-each`, `.reduce`, and friends — belongs to 225the `Foldable` and `Iterator` traits. They're covered in 226[the collections chapter](./12-collections.simd). 227 228## Key points 229 230- Every control-flow construct pops its condition or scrutinee from the 231 stack. Nothing is passed by implicit variable. 232- Every branch of an `:if`/`:else`/`:match` must leave the stack in the 233 same shape — the compiler enforces this. 234- `:match` is exhaustiveness-checked. Missing cases are a compile 235 error. 236- `:loop` needs `+Div` when the break condition is runtime-dependent. 237 Without `+Div`, Silo is total: your function is guaranteed to 238 terminate on every input. 239- `:ret` finishes the enclosing word with whatever's on the stack. 240 `:try` is `:ret`'s specialised cousin for `Result`-returning words. 241- `while`, `times`, `.map`, `.fold`, and so on are ordinary library 242 words, not keywords. 243 244The next chapter picks up from the `[ … ]` syntax used above — 245[quotations and higher-order functions](./04-quotations.simd).