Source guide/variadic-and-reflection.simd

1# Variadic Functions and Reflection
2
3Two separate-but-related features that share this chapter because
4they lean on the same trick — the compiler statically knowing a
5count or a shape that most languages only figure out at runtime.
6
7**Variadic functions** let a word take a statically-known *number*
8of arguments without hard-coding it. **Reflection** lets you
9write code that depends on a type's shape — field names, variant
10counts — with the inspection happening at compile time and no
11runtime representation needed. The first is about how many
12values; the second is about what values. Both are checked before
13your program runs.
14
15## Variadic input
16
17A word's **arity** — how many values it consumes — is part of
18its name, written as a `/N` suffix. Declaring a variadic word
19uses a range where the `N` would normally be:
20
21```silo
22:fn sum/1.. ( ..AnyInt -> AnyInt )
23  pop-> len
24  len 1 - [ drop + ] times
25:end
26```
27
28`sum/1..` means "sum takes at least one argument." Callers
29specify the arity at the call site with a concrete count:
30
31```silo
323 5 sum/2                    # ⌊8⌉
331 2 3 4 sum/4                # ⌊10⌉
34```
35
36The compiler checks the count is within the declared range. The
37`..AnyInt` on the input side of the signature is the type-level
38way of saying "N values of type AnyInt", matched to the arity
39the caller asked for.
40
41The range forms that appear in a declaration suffix:
42
43| Suffix      | Meaning                                           |
44|-------------|---------------------------------------------------|
45| `/..`       | zero or more                                      |
46| `/1..`      | one or more                                       |
47| `/2..10`    | between 2 and 10 (inclusive)                      |
48
49A single-point range `/N..N` is legal but an anti-pattern — a
50word that always takes a fixed number of values should just
51be an ordinary `:fn` with the inputs listed explicitly in the
52stack effect:
53
54```silo
55:fn triple ( AnyInt AnyInt AnyInt -> AnyInt )
56  + +
57:end
58```
59
60Reach for variadic syntax only when the arity genuinely
61varies at the call site.
62
63### Calling a variadic word
64
65Call sites pick a concrete arity with a bare `/N`:
66
67```silo
683 5 sum/2                      # ⌊8⌉      — 2 arguments
691 2 3 4 sum/4                  # ⌊10⌉    — 4 arguments
70```
71
72The compiler checks `N` falls within the range declared at
73definition time.
74
75## Variadic output
76
77A word can also produce a statically-known *number* of outputs,
78by putting `..Type` on the output side. The callee pushes the
79values, then pushes the count last:
80
81```silo
82:fn unpack/1.. ( (Vec a | n) -> ..a )
83  ...                         # push elements, then push the count
84:end
85```
86
87Callers use `/N` to specify how many outputs to expect:
88
89```silo
90my-vec unpack/3 pop-> c pop-> b pop-> a
91```
92
93This is the mechanism behind `.collect`. A quotation pushes
94some number of values; the compiler counts them statically;
95`.collect` reads the count and builds a `Vec` of exactly that
96length:
97
98```silo
99[ 1 2 3 ] .collect             # ⌊[1 2 3]⌉ : (Vec Int | 3)
100[ "a" "b" ] .collect           # ⌊["a" "b"]⌉ : (Vec Str | 2)
101```
102
103Output variadics are what make `.collect` able to return a
104length-indexed `Vec` — the length isn't "whatever the runtime
105produces", it's a compile-time constant.
106
107## Variadic type parameters
108
109The `/N..` suffix works on record declarations too, producing
110**types** parameterised by a variable number of other types:
111
112```silo
113:record Tensor/1.. elem (..AnyUInt)
114  .data Bytes
115:end
116
117(Tensor F64 3 4 5)             # 3D tensor, dims 3×4×5
118(Tensor F64 28 28)             # 2D tensor, dims 28×28
119```
120
121`Tensor/1..` takes one ordinary type parameter (`elem`) and one
122or more `AnyUInt`s (the dimensions). Each concrete application
123fills in a specific rank.
124
125This is how anything rank-polymorphic — matrices, tensors,
126heterogeneous tuples — gets expressed without baking in a fixed
127rank.
128
129## Compile-time reflection
130
131`:type-info(T)` is a compile-time expression that produces a
132`TypeInfo` value describing the shape of `T`. The `TypeInfo` is
133erased at runtime — the compiler inlines the answer — so there
134is no runtime cost. You get full shape information with zero
135footprint.
136
137```silo
138:fn type-name ( a -> Str )
139  drop :type-info(a) :match
140    | RecordInfo info =>
141      info .name
142    | UnionInfo info =>
143      info .name
144    | IntInfo _ =>
145      "Int"
146    | _ =>
147      "unknown"
148  :end
149:end
150
15142 type-name                   # ⌊"Int"⌉
1522.0 1.0 Point type-name        # ⌊"Point"⌉
153```
154
155`TypeInfo` is a union — `RecordInfo`, `UnionInfo`, `EnumInfo`,
156`IntInfo`, `FloatInfo`, `FnInfo`, and so on — each variant
157carrying a record that describes its shape. For a record, that
158includes the name, the fields (with names and types), and any
159derives on it.
160
161You use reflection by pattern-matching on the `:type-info`
162result, same as any other union:
163
164```silo
165:fn field-count ( a -> Int )
166  drop :type-info(a) :match
167    | RecordInfo info =>
168      info .fields .len
169    | _ =>
170      0
171  :end
172:end
173```
174
175`:type-info` on a **value's type** (the `a` above, consumed by
176`drop`) is the common case. `:type-info(SomeConcreteType)`
177reflects on a specific named type even when you don't have an
178instance.
179
180## Reflection-driven serialisation
181
182Reflection is the substrate for things that would otherwise need
183a `Serialize` / `Deserialize` trait hierarchy. Silo's standard
184serialisation libraries (JSON, CBOR, Styx, etc.) walk
185`:type-info(T)` at compile time, produce a specialised
186serialiser for each concrete type, and monomorphise it down to
187the structural shape you'd have written by hand.
188
189The practical consequence: there is no `:derive Serialize` on
190your record types. You just call the serialiser; it reflects on
191the shape; the compiler inlines the code. Add a field to a
192record and the serialiser picks it up automatically next
193compile, with no trait impls to update.
194
195The same substrate drives the `:derive` lists you've already
196seen — `[Eq Hash Debug]` on a record declaration walks the
197record's `:type-info` at compile time and generates the
198field-by-field implementations. Everything derivable is
199derived by reflection.
200
201## When you actually use variadics
202
203Most code doesn't declare variadic `:fn`s. The common pattern
204is to **call** variadic words (`vec/3`, `sum/4`) without
205defining your own, and to use output variadics implicitly via
206`.collect`.
207
208When you do define a variadic, it's usually because the
209alternative — a list of positional arguments wrapped in a
210record — would be noisier than the caller's `/N` annotation.
211Reach for variadics when the arity is a natural parameter at
212the call site.
213
214## Key points
215
216- `:fn name/N..` declares a variadic word. Range forms cover
217  `/..` (zero+), `/1..` (one+), `/M..N` (bounded), `/N` (exact).
218- `..Type` on either side of the signature is a variadic
219  slot; the caller's `/N` suffix fixes the count.
220- :gloss[Output variadics](./A1-glossary.simd#output-variadic) are
221  how `.collect` returns length-indexed `Vec`s — the count is
222  compile-time.
223- Records can be variadic too (`:record Tensor/1..`), giving
224  you rank-polymorphic types without a fixed rank.
225- `:type-info(T)` is a compile-time reflection primitive
226  producing a `TypeInfo` union. Zero runtime cost.
227- Serialisation, `:derive` lists, and any structural-code-gen
228  in the standard library are built on `:type-info`, not on
229  a `Serialize`-like trait hierarchy.
230
231Next: [macros](./20-macros.simd) — how to extend the compiler
232with your own syntactic transformations.