Source guide/data.simd

1# Data Types
2
3Silo has five ways to declare a named data type. Each serves a
4different job; the five are complementary rather than overlapping.
5If you've used Rust, the shapes will be familiar:
6
7| Silo       | Rust analogue             | Role                               |
8|------------|---------------------------|------------------------------------|
9| `:record`  | `struct` with named fields | Product type: one of each          |
10| `:union`   | `enum` with data variants  | Sum type: one of several           |
11| `:enum`    | C-style `enum`             | Integer-backed finite set          |
12| `:alias`   | `type` alias               | Transparent synonym                |
13| `:type`    | `struct Wrapper(Inner)`   | Distinct :gloss[newtype](./A1-glossary.simd#newtype) |
14
15On top of those five keywords Silo adds two more:
16
17| Silo       | Role                                                |
18|------------|-----------------------------------------------------|
19| `[…]` after the name | :gloss[Derive list](./A1-glossary.simd#derive-list) for auto-impls |
20| `:pred`    | Compile-time :gloss[predicate](./A1-glossary.simd#predicate) over a type's shape |
21
22This chapter goes through each in turn.
23
24## Records
25
26A `:record` is a product type — a bundle of named fields. Fields are
27listed one per line, each with `.name` followed by its type.
28
29```silo
30:record Point
31  .x F64
32  .y F64
33:end
34```
35
36You construct a record by pushing the fields in **reverse
37declaration order**, then the type name. The constructor pops
38top-of-stack first and assigns it to the first-declared field:
39
40```silo
412.0 1.0 Point                  # ⌊Point{ x ↦ 1.0  y ↦ 2.0 }⌉
42# pops top (1.0) → .x (first declared), pops next (2.0) → .y
43```
44
45Read the source right-to-left and you get the same visual order
46as the field declarations: "1.0 (for x), 2.0 (for y)".
47
48Access a field with `.name`, and do a functional update
49(returning a new record, not mutating) with `.name<-`:
50
51```silo
522.0 1.0 Point pop-> p
53p .x                           # ⌊1.0⌉
54p 3.0 .x<-                     # ⌊Point{ x ↦ 3.0  y ↦ 2.0 }⌉
55```
56
57`.x<-` takes the old record and a new value for `x` and produces the
58updated record. The field selector and the field updater are just
59ordinary words — nothing special about the dot syntax.
60
61## Unions
62
63A `:union` is a sum type: a value that is one of several named
64variants. Each variant lists its payload's types, with optional field
65names for destructuring convenience.
66
67```silo
68:union Shape
69  | Circle F64
70  | Rect .width F64 .height F64
71:end
72```
73
74You construct a variant by pushing its payload in reverse
75declaration order (same convention as records), then the
76variant name:
77
78```silo
795.0 Circle                     # ⌊Circle(5.0)⌉
804.0 3.0 Rect                   # ⌊Rect(3.0 4.0)⌉
81# pops 3.0 → .width, pops 4.0 → .height
82# display shows payload in pop order: width first, then height
83```
84
85You take a union apart with `:match`. Inside a match arm, the
86variant's fields bind to names:
87
88```silo
89:fn area ( Shape -> F64 )
90  :match
91    | Circle r =>
92      r r * 3.14159 *
93    | Rect .width w .height h =>
94      w h *
95  :end
96:end
97```
98
99`Circle r` binds the single payload value to `r`.
100`Rect .width w .height h` destructures by field name — the declared
101field names in the union make this work.
102
103## Enums
104
105An `:enum` is a union where every variant is empty: a finite
106integer-backed set, like a C-style enum or a Rust unit-only enum.
107Variants are lowercase, unlike union variants:
108
109```silo
110:enum Direction
111  | north
112  | south
113  | east
114  | west
115:end
116```
117
118You access a variant with `(TypeName .variant)`:
119
120```silo
121(Direction .north) pop-> d
122```
123
124Enums exist as a separate keyword (and not just as a union of empty
125variants) because they carry extra guarantees: the integer backing is
126stable, they're cheap to hash, they derive `Eq`/`Ord`/`Hash` for free,
127and they're the right type for anything that genuinely is a small
128finite set of tags.
129
130## Aliases and newtypes
131
132`:alias` makes a **transparent** synonym. The compiler treats both
133names as the same type everywhere:
134
135```silo
136:alias Meters F64
137```
138
139Now `Meters` and `F64` are interchangeable. A function taking
140`Meters` accepts any `F64`, and vice versa. Aliases are useful for
141readability; they don't add any type-level distinction.
142
143`:type` makes a **distinct** wrapping. Even if two newtypes have
144the same underlying representation, the compiler won't let you mix
145them:
146
147```silo
148:type UserId (Int 0..)
149:type PostId (Int 0..)
150```
151
152A `UserId` is not a `PostId`, even though both are non-negative
153`Int`s at runtime. This is the type-level equivalent of a tagged
154wrapper, with zero runtime cost — the :gloss[newtype](./A1-glossary.simd#newtype)
155is an artefact of the type system only.
156
157Use an alias when the name is just for documentation; use a newtype
158when passing the wrong one should be a compile error.
159
160## Derive lists
161
162Every type declaration can include a **derive list** in square
163brackets before the fields:
164
165```silo
166:record Color [Eq Hash Debug]
167  .r U8
168  .g U8
169  .b U8
170:end
171```
172
173Each name in the list is a trait the compiler will auto-implement
174for the type. Most traits that can be derived generate their
175implementation mechanically from the declared field or variant
176structure — `Eq` compares field-by-field, `Hash` hashes each field,
177`Debug` prints using the structural layout.
178
179Two special tokens control inheritance on newtypes:
180
181```silo
182:type MyWidget Widget [* -Display]
183```
184
185`*` means "derive every trait the wrapped type derives"; `-Display`
186opts out of a specific one. This is useful when you want a newtype
187to inherit almost everything from the underlying type but override
188one particular trait.
189
190Derives are covered in more detail in the
191[traits chapter](./09-traits.simd).
192
193## Predicates
194
195A `:pred` declares a compile-time :gloss[predicate](./A1-glossary.simd#predicate)
196over a type's shape. The predicate has no runtime representation —
197it's purely a restriction the compiler checks when you use the
198predicate as a constraint.
199
200```silo
201:pred Signed   ( (Int (RangeBoth min max)) ) { (min 0 <) }
202:pred Unsigned ( (Int (RangeBoth min max)) ) { (min 0 >=) }
203:pred NonZero  ( (Int (RangeBoth min max)) ) { (min 0 >) }
204:pred ByteSized ( (Int (RangeBoth min max)) ) { ((max min -) 2^8 <) }
205```
206
207Read `:pred Signed ( (Int (RangeBoth min max)) ) { (min 0 <) }` as
208"the predicate `Signed` matches any type of the form
209`(Int (RangeBoth min max))`, and holds when `min < 0`". The pattern
210destructures the type; the `{ … }` is the compile-time expression
211that decides whether the predicate holds.
212
213This is one of the things that makes Silo dependently typed without
214needing to write proofs. `I8` is `(Int -128..128)`, so
215`(Signed I8)` unifies `min = -128`, `max = 128`, and checks
216`-128 < 0` — which is `true`. `(Signed U8)` would try `min = 0`,
217fail `0 < 0`, and be rejected at compile time.
218
219You use predicates as constraints on generic words, the same way you
220use trait bounds:
221
222```silo
223:fn negate ( val -> val ) { (Signed val) }
224  0 swap -
225:end
226
22742:I8 negate                   # OK — I8 is Signed
228# 42:U8 negate                 # compile error: (Signed U8) not satisfied
229```
230
231Predicates are for **gatekeeping**, not dispatch. Use traits when
232you need different code for different types; use predicates when you
233need to stop unsuitable types from ever reaching a given function.
234The full story on predicates, including the relationship to
235refinement types, is in [the type system chapter](./06-types.simd).
236
237## Key points
238
239- Records, unions, enums, aliases, and newtypes cover every
240  standard shape of named data.
241- Records use named-field destructuring, construction in
242  declaration order, and `.name` / `.name<-` for access and
243  functional update.
244- Union variants are destructured in `:match`. Declared field names
245  in a variant let you match by name.
246- Enums are cheap integer-backed finite sets; lowercase variants
247  distinguish them from unions.
248- `:alias` makes names interchangeable; `:type` makes them
249  distinct, at zero runtime cost.
250- Every declaration can carry a derive list `[Trait1 Trait2 …]`
251  for auto-implementations; `*` and `-` tokens control inheritance
252  on newtypes.
253- `:pred` declares a compile-time constraint over a type's shape,
254  usable as a bound on generic functions. Predicates cost nothing
255  at runtime.
256
257The next chapter goes deeper on
258[the type system](./06-types.simd) — generics, higher-kinded types,
259dependent types, and how it all fits together.