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.