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).