plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


c8e9c42 — Peter John 2026-10-30T19:20:30+05:30
docs: refresh README, plans and example descriptions for current language
README.md CHANGED
@@ -1,20 +1,98 @@
1
1
  # 👾 Plum
2
2
 
3
- A statically typed, imperative programming language with algebraic data types, inspired by Rust and Gleam.
3
+ A statically typed, imperative programming language with algebraic data types, inspired by Rust, Zig, Go, Kotlin and Gleam.
4
4
 
5
- - Built on a [tree-sitter](plum-tooling/tree-sitter-plum) grammar, with syntax highlighting for Helix and VSCode out of the box (`plum editor helix` / `plum editor vscode`)
5
+ - Built on a [tree-sitter](plum-tooling/tree-sitter-plum) grammar, with syntax highlighting and a language server for Helix and VSCode out of the box (`plum editor helix` / `plum editor vscode`)
6
- - Compiles to WebAssembly today, with plans to target amd64, arm64, and riscv64 via QBE (C-ABI compatible)
6
+ - Compiles to WebAssembly (wasm-gc) and runs under wasmtime; `plum build` embeds the module into a native executable. Native targets (amd64, arm64, riscv64 via QBE, C-ABI compatible) are planned.
7
7
 
8
- ## Requirements
8
+ ## Language at a glance
9
9
 
10
- ```sh
10
+ ```plum
11
+ module glance
12
+ import std/Os
11
- node >= 23.1.0
13
+ import std/Str
12
- npm >= 10.9.0
14
+ import std/Int
13
- qbe >= 1.2
15
+ import std/Float
16
+ import std/List
17
+ import std/Seq
18
+
19
+ error ParseError = BadNum | Negative # a named error set
20
+
21
+ enum Shape = # a sum type; payload fields are named
14
- clang >= 16.0.0
22
+ | Circle(r: Float)
15
- ```
23
+ | Rect(w: Float, h: Float)
16
24
 
17
- ## Installing the `plum` CLI
25
+ SIDES_MAX = 4 # associated constant: Shape.SIDES_MAX
26
+
27
+ fun area(self) -> Float = # a method
28
+ match self
29
+ Circle(r) => r * r * 3.14
30
+ Rect(w, h) => w * h
31
+
32
+ value enum Vec2 = # small copy-semantics value type
33
+ | Vec2(x: Float, y: Float)
34
+
35
+ ZERO = Vec2(x: 0.0, y: 0.0) # Vec2.ZERO
36
+
37
+ fun add(self, o: Vec2) -> Vec2 = # `a + b` calls a.add(b)
38
+ Vec2(x: self.x + o.x, y: self.y + o.y)
39
+
40
+ fun parse(s: Str) -> ParseError!Int = # an Int, or a ParseError
41
+ n := Int.parse(s) catch return error.BadNum
42
+ if n < 0
43
+ return error.Negative
44
+ n
45
+
46
+ fun double(s: Str) -> error{BadNum, Negative}!Int =
47
+ n := try parse(s) # `try` propagates the error
48
+ n * 2
49
+
50
+ fun firstEven(xs: List[Int]) -> Int? = # `T?` is a T or null
51
+ for x := range xs.find() # a Seq terminal; the body is the predicate
52
+ x % 2 == 0
53
+
54
+ fun main() =
55
+ total := 0
56
+ for i := range 10, if i % 2 == 0 # range, with a `, if` filter clause
57
+ total = total + i
58
+ big := for x := range List(1, 5, 9).map(), if x > 2
59
+ x * 10 # the body is the mapped value
60
+ e := firstEven(List(1, 3, 4)) ?: -1 # `?:` supplies a default for null
61
+ n := parse("42") catch 0 # `catch` supplies a default for an error
62
+ pos := Vec2.ZERO + Vec2(x: 1.0, y: 2.0)
63
+ printLn("{total} {big.length()} {e} {n} {pos.y} {Shape.SIDES_MAX}")
64
+
65
+ test "glance"
66
+ assert Rect(w: 2.0, h: 3.0).area() == 6.0
67
+ assert {double("21") catch 0} == 42
68
+ assert {parse("-1") catch 7} == 7
69
+ assert firstEven(List(1, 3)) == null
70
+ ```
71
+
72
+ | You want | Write |
73
+ |---|---|
74
+ | declare a variable / reassign | `x := 1` / `x = 2` |
75
+ | a block | indent by 2 spaces, no braces |
76
+ | group an expression | `{1 + 2} * 3` (braces; parentheses are for calls) |
77
+ | a conditional expression | `if c then a else b` (there is no `?:` ternary) |
78
+ | loop | `while c`, `for x := range src`, `for i := range n` |
79
+ | a sum type / record | `enum Shape = \| Circle(r: Float) \| Rect(w: Float, h: Float)` |
80
+ | a small value type | `value enum Vec2 = \| Vec2(x: Float, y: Float)` |
81
+ | a method / static / constant | `fun area(self)` / `fun origin()` / `NAME = expr` inside the type body |
82
+ | generics | `fun id(x: T) -> T`, `enum Box[T]`, duck-typed, one copy per type |
83
+ | operators on your types | define `add`, `sub`, `mul`, `div`, `neg`, `eq`, `lt`, `le`, `gt`, `ge` |
84
+ | an optional | `T?`, `null`, `opt ?: default`, `if x := opt`, `opt?.field`, `opt!` |
85
+ | a fallible function | `-> !T`, `-> error{A, B}!T`, `-> ParseError!T`, `return error.A` |
86
+ | propagate / default / crash | `try f()` / `f() catch default` / `f()!` |
87
+ | an iterator | `fun* name(..) *T` with `yield`, used as a `for` source |
88
+ | a sequence chain | `for x := range xs.skip(1).take(3).map()` with the mapped value as the body |
89
+ | a namespace of constants and functions | `primitive Math =` |
90
+ | a test | `test "name"` with `assert` |
91
+ | host function | `extern fun printLn(s: Str)` |
92
+
93
+ ## Installing
94
+
95
+ Prebuilt binaries need nothing else. To build from source you need a recent stable Rust toolchain and a C compiler (for the tree-sitter parser). `node` and `npm` are only needed to rebuild the VSCode extension bundle (`npm install && npm run build` in `plum-tooling/vscode-plum`); without them `plum editor vscode` still installs syntax highlighting.
18
96
 
19
97
  ```sh
20
98
  cargo install --path plum-cli
@@ -44,6 +122,19 @@ mv plum ~/.cargo/bin/plum # or anywhere else already on your PATH
44
122
 
45
123
  Maintainers: `cargo make release` builds macOS/Linux/Windows binaries (Linux/Windows via [cross](https://github.com/cross-rs/cross), needs Docker), tags `v<version>`, regenerates the links above, and uploads all three to R2 (see `Makefile.toml`; needs [cargo-make](https://github.com/sagiegurari/cargo-make) and the `git` rclone remote configured for R2).
46
124
 
125
+ ### Using the CLI
126
+
127
+ ```sh
128
+ plum run file.plum # compile and run under wasmtime
129
+ plum test file.plum dir/ # run the `test` blocks of files and directories (recursive)
130
+ plum compile file.plum # write file.wasm
131
+ plum build file.plum # embed the module into a native executable
132
+ plum format file.plum # rewrite in place (--check, --stdin)
133
+ plum editor helix|vscode # install highlighting and the language server
134
+ ```
135
+
136
+ `import std/Name` resolves to `<lib-path>/plum-std/Name.plum`, and `--lib-path` defaults to the current directory, so run the commands from a checkout of this repository (or pass `--lib-path`).
137
+
47
138
  ## Layout and comments
48
139
 
49
140
  Blocks are indentation-sensitive (2 spaces), like Python — there's no `{ }` for grouping statements. `#` starts a line comment.
@@ -60,7 +151,7 @@ fun main() =
60
151
 
61
152
  | Case | Used for |
62
153
  |---|---|
63
- | `PascalCase` | types, enums |
154
+ | `PascalCase` | types, enums, error sets, namespaces |
64
155
  | `camelCase` | functions, methods, fields |
65
156
  | `snake_case` | variables, params |
66
157
  | `SCREAMING_SNAKE_CASE` | associated constants (`Float.PI`) |
@@ -68,12 +159,16 @@ fun main() =
68
159
 
69
160
  ## Modules and imports
70
161
 
162
+ A file may start with `module name`, then its imports, then everything else; `test` blocks go last.
163
+
71
164
  ```plum
72
165
  module basics
73
166
 
74
167
  import std/Os
75
168
  ```
76
169
 
170
+ Nothing is implicit: `Int`, `Float`, `Str`, `List`, ... are available once the file imports `std/Int`, `std/Float`, `std/Str`, `std/List`, ...
171
+
77
172
  ## Literals
78
173
 
79
174
  ```plum
@@ -90,6 +185,17 @@ yes := true
90
185
  no := false
91
186
  ```
92
187
 
188
+ An integer literal that does not fit in an `Int` is a compile error (`-0x8000_0000_0000_0000` is `Int.MIN`).
189
+
190
+ ### String interpolation
191
+
192
+ ```plum
193
+ fun greet(name: Str) -> Str =
194
+ "Hello, {name}!"
195
+ ```
196
+
197
+ `{expr}` interpolates any expression: a variable, literal, attribute access, method call, `&&`/`||`/comparison or an inline `if`.
198
+
93
199
  ## Operators
94
200
 
95
201
  From tightest to loosest binding:
@@ -104,7 +210,7 @@ From tightest to loosest binding:
104
210
  | | `^` | |
105
211
  | | `&` | |
106
212
  | | `\|` | |
107
- | | `<` `<=` `==` `!=` `>=` `>` `<>` | comparisons |
213
+ | | `<` `<=` `==` `!=` `>=` `>` `<>` | comparisons (`<>` is another spelling of `!=`) |
108
214
  | | `!` | boolean not |
109
215
  | | `&&` | |
110
216
  | | `\|\|` | |
@@ -112,9 +218,12 @@ From tightest to loosest binding:
112
218
  | | `if c then a else b` | inline if (the else branch extends as far right as possible) |
113
219
  | lowest | `catch` | `x catch default` (the default extends as far right as possible) |
114
220
 
115
- There's no range operator — every `for` is `for <binders> := range <source>`: `for i := range n` counts 0 to n-1, and the `Int` methods `a.until(b)` (a to b-1), `a.to(b)` (inclusive) and `a.downTo(b)` give other ranges (`for i := range 1.to(10)`). `{ }` is used to group a sub-expression (not `( )`, which is reserved for calls):
221
+ There's no range operator — every `for` is `for <binders> := range <source>` (see [Iteration](#iteration)). `{ }` is used to group a sub-expression (not `( )`, which is reserved for calls):
116
222
 
117
223
  ```plum
224
+ sum := 3
225
+ bits := 4
226
+ cmp := true
118
227
  grouped := {1 + 2} * {3 - 1}
119
228
  picked := if cmp then sum else bits
120
229
  negated := -sum
@@ -190,6 +299,8 @@ assert n > 0 # traps at runtime if false
190
299
  todo # traps at runtime — marks a body as not yet implemented
191
300
  ```
192
301
 
302
+ `match` is covered under [Types](#match).
303
+
193
304
  ## Functions
194
305
 
195
306
  ```plum
@@ -203,212 +314,123 @@ fun withDefault(a: Int, step: Int = 1) -> Int =
203
314
  a + step
204
315
 
205
316
  fun sumAll(nums: ...Int) -> Int = # variadic param
317
+ total := 0
318
+ for v := range nums
319
+ total = total + v
206
- todo
320
+ total
207
321
  ```
208
322
 
209
- ### Iteration: `for`, `fun*` and `Seq`
210
-
211
- `while` and `for` are the only loops. A `for` has binders, a source, optional clauses and a body:
212
-
213
- ```plum
214
- for u := range units # a list
215
- total = total + u.hp
216
- for i := range 10 # an Int: 0, 1, ... 9
217
- total = total + i
218
- for i := range 2.until(10) # an Int fun* method (also to, downTo)
219
- total = total + i
220
- for i, u := range units # two binders: (position, element)
221
- printLn("{i}: {u.name}")
222
- for _, v := range scores # a Map: (key, value); `_` ignores a value
223
- total = total + v
224
- for k := range scores.keys() # a fun* method of Map (also values(), entries())
225
- printLn(k)
226
- ```
227
-
228
- #### The iteration protocol
229
-
230
- The protocol is resolved on the type of the source, after type inference:
231
-
232
- - one binder: the type has `length()` and `at(i)` (a `List`, `Array`, `Deque`, a variadic parameter, your own type);
233
- - two binders: if the type declares a `fun* entries(self) *(A, B)` method it is used (`Map` does: `(key, value)`), otherwise `(position, element)` over `length()`/`at(i)`;
234
- - `x.name(args)` where the type of `x` declares `fun* name(self, ..)`: the method is expanded like any `fun*`, with `self` bound to `x`.
235
-
236
- A `for` takes clauses after the source: `, if cond` skips an element and `, name := expr` binds a value for the later clauses and the body. A `for` is an expression when its source has a result (`-> R`), with the body's last expression as the value of each step:
237
-
238
- ```plum
239
- alive := for u := range units.filter(), if u.team == me.team
240
- u.hp > 0.0
241
- ```
323
+ A function without a return type returns `Unit`; the last expression of a body is its value. A call can name its arguments only for constructors (`Cat(name: "Whiskers", age: 3)`); functions take positional arguments.
242
324
 
243
- #### `fun*` iterators
325
+ ## Types: enums, records and value enums
244
326
 
245
- A `fun*` is an iterator. `*T` (or `*(A, B)`) is what each `yield` hands to the `for` binders, `-> R` is the optional result of the whole `for`. `yield a, b` runs the `for` body; as an expression (`if yield x`, `acc = yield acc, x`) its value is the body's last expression. A `fun*` is expanded at the use site (hygienic, one loop, no closure, no `call_indirect`); it can't be recursive, can't `return`, and can only be used as a `for` source. `break` and `continue` in the body act on the innermost loop around the `yield`. A `fun*` declared inside a `type`/`enum` body takes `self` and is used as `xs.name()`.
327
+ `enum` is the one declaration form for data. A record type is just a single-variant enum whose one variant shares the enum's own name:
246
328
 
247
329
  ```plum
248
- primitive Int =
330
+ enum Point =
249
- fun* until(self, end: Int) *Int =
331
+ | Point(x: Int, y: Int)
250
- i := self
251
- while i < end
252
- cur := i
253
- i = i + 1
254
- yield cur
255
- ```
256
332
 
333
+ enum Named =
257
- #### `Seq[T]` and the chain methods
334
+ | Named(name: Str)
258
335
 
259
- `Seq[T]` is a compiler primitive and `*T` is sugar for it. A `Seq` never exists at runtime: it can't be stored, returned or captured, it only flows into a `for` source or into the next method call. The methods live in [`plum-std/Seq.plum`](plum-std/Seq.plum) under `primitive Seq[T]`; `xs.m()` where the type of `xs` has no `m` of its own and is iterable (`length()`/`at(i)`, or an `entries()` method) resolves to `Seq.m`, so they work on every list-like type:
336
+ enum Color =
337
+ | Red
338
+ | Green
339
+ | Blue
260
340
 
261
- - **adapters** reshape a sequence, have no result and may appear anywhere in a chain: `take(n)`, `skip(n)`, `stepBy(n)`, `zip(other)`, `reversed()`, `chunks(n)` (the last three need a `List` receiver);
262
- - **terminals** take the `for` body (the predicate, the mapping, the step) and must be the last call: `filter()` (`-> List[T]`), `map()` (`-> List[U]`), `find()` (`-> T?`), `fold(init)` (binders `acc, x`), `count()`, `any()`, `all()`, `each()`, `firstN(n)` (the first n elements the body accepts), and `toList()`, which has no body and needs no `for`: `ys := xs.skip(1).take(3).toList()`.
341
+ enum Maybe =
342
+ | Just(Int) # unnamed positional payload — Just(5)
343
+ | Nothing
263
344
 
345
+ enum Shape =
264
- A terminal in the middle of a chain is an error (`'filter' needs the for body; it must be the last call in the chain`), and so is storing an adapter's result (`s := xs.take(2)`).
346
+ | Circle(radius: Int) # named payload fields — Circle(radius: 5) or Circle(5)
347
+ | Square(x: Int, y: Int)
265
348
 
266
- ```plum
349
+ enum Step(n: Int) = # a shared field on every variant ...
267
- # the first five living units' names, as one fused loop
350
+ | ReadMin(10) # ... each variant supplies its own value for it
268
- names := for u := range units.take(5).map(), if u.hp > 0.0
269
- u.name
351
+ | ReadMax(20)
270
352
  ```
271
353
 
272
- Performance guarantee: a whole chain is ONE loop with no closure allocation, no `call_indirect` and no intermediate `List`. The method chain below compiles to the same single loop as the hand-written one:
354
+ The other declaration forms are `value enum` (below), `error Name = A | B` ([Error handling](#error-handling)) and `primitive Name =` ([Constants, statics and `primitive`](#constants-statics-and-primitive)).
273
355
 
274
- ```plum
275
- total := for acc, u := range units.skip(1).take(10).fold(0.0), if u.team == me.team
356
+ ### Methods, `self` and field access
276
- acc + u.hp
277
357
 
278
- total := 0.0 # the loop you would have written by hand
279
- seen := 0
280
- i := 0
281
- while i < units.length()
282
- u := units.at(i)
283
- i = i + 1
284
- if seen >= 1 && seen < 11
285
- if u.team == me.team
286
- total = total + u.hp
287
- seen = seen + 1
288
- ```
289
-
290
- List and Map have no closure-taking iteration methods; closures remain in the language for passing behavior around as values (`Num` below). See [`plum-examples/seq.plum`](plum-examples/seq.plum) (one test per adapter and terminal), [`plum-examples/chains.plum`](plum-examples/chains.plum) and [`plum-examples/forms.plum`](plum-examples/forms.plum).
291
-
292
- #### Iteration cheat sheet
293
-
294
- | Go / Rust / streams | Plum |
295
- |---|---|
296
- | `for _, u := range units` / `for u in &units` | `for u := range units` |
297
- | `for i, u := range units` / `.iter().enumerate()` | `for i, u := range units` |
298
- | `for k, v := range m` / `for (k, v) in &m` | `for k, v := range m` (identical to Go) |
299
- | `for i := 0; i < n; i++` / `for i in 0..n` | `for i := range n` or `range a.until(b)` |
300
- | `for i := n; i >= 0; i--` / `for i in (0..=n).rev()` | `for i := range n.downTo(0)` |
301
- | `for i := 0; i < n; i += 2` | `for i := range 0.until(n).stepBy(2)` |
302
- | `.iter().filter(p).collect()` / `stream.filter(p)` | `for u := range units.filter()` then the predicate as the body |
303
- | `.iter().map(f).collect()` / `stream.map(f)` | `for u := range units.map()` then the mapped value as the body |
304
- | `.iter().find(p)` / `findFirst(p)` | `for u := range units.find()` (`-> T?`) |
305
- | `.iter().fold(0, f)` / `reduce(0, f)` | `for acc, u := range units.fold(0)` |
306
- | `.iter().take(n).skip(m)` / `limit(n).skip(m)` | `units.skip(m).take(n)` |
307
- | `.iter().step_by(n)` / `.rev()` / `.zip(o)` / `chunks(n)` | `stepBy(n)` / `reversed()` / `zip(o)` / `chunks(n)` |
308
- | `.count()` / `.any(p)` / `.all(p)` / `forEach(f)` | `count()` / `any()` / `all()` / `each()` |
309
- | `if p(x) { continue }` | `, if cond` clause after the source |
310
- | generator / `yield` | `fun* name(..) *T` with `yield` |
311
-
312
- ## Constants, statics and `primitive`
313
-
314
- There are no top-level constants and no free-standing statics. A constant lives inside a type's body (`enum`, `value enum`) or a `primitive`; `NAME = expr` declares an associated constant, and a `fun` without `self` is a static function. Both are reached through the type, `Type.NAME` and `Type.f(args)`, from anywhere, and by their bare name inside the same body. A constant is evaluated lazily, once (a heap value such as a `List` is cached and shared), and names are unique per type, so `Alpha.X` and `Beta.X` coexist.
358
+ A `fun` declared indented directly inside an `enum` body is a method; its first parameter is `self`:
315
359
 
316
360
  ```plum
317
- enum Level =
361
+ enum Cat =
318
- | Low
319
- | High
320
-
321
- LIMIT = 10 # Level.LIMIT
362
+ | Cat(name: Str, age: Int)
322
363
 
323
- fun cap(self, n: Int) -> Int =
364
+ fun getAge(self) -> Int =
324
- if n > LIMIT then LIMIT else n
365
+ self.age
325
366
 
326
- value enum Vec2 =
327
- | Vec2(x: Float, y: Float)
367
+ fun birthday(self) -> Int =
368
+ self.age + 1
328
369
 
370
+ enum Wrapper =
329
- ZERO = Vec2(x: 0.0, y: 0.0) # Vec2.ZERO
371
+ | Wrapper(inner: Cat, tag: Int)
330
372
 
373
+ fun main() -> Int =
331
- fun unit() -> Vec2 = # a static: Vec2.unit()
374
+ c := Cat(name: "Whiskers", age: 3)
332
- Vec2(x: 1.0, y: 1.0)
375
+ a := c.getAge()
376
+ w := Wrapper(inner: c, tag: 1)
377
+ a + w.inner.age # chained field access
333
378
  ```
334
379
 
335
- The built-in types carry theirs: `Int.MIN`, `Int.MAX`, `Float.PI`, `Float.TAU`, `Float.EPSILON`, `Float.MAX`, ... and the statics `Int.parse(s)` and `Float.parse(s)`.
380
+ A named field of any enum can be read with `.field`, even on a multi-variant enum (the field must have a unique owning variant; a checked downcast does the access).
336
381
 
337
- #### `primitive`
382
+ ### Constructing values
338
383
 
339
- `primitive Name =` has two meanings, by name:
340
-
341
- - **a namespace**: any name that is not a built-in type declares a stateless namespace of constants and static functions, usable anywhere. It has no `self`, no values and no fields, so `Name` is not a type and can't be constructed or used as a value; it only appears as `Name.NAME` or `Name.f()`. Use an `enum` when you need markers or a set of cases.
342
- - **a built-in's API**: `primitive Int =`, `primitive Float =`, `primitive Bool =`, `primitive Str =`, `primitive Byte =`, `primitive ByteSlice =`, `primitive Array[T] =` and `primitive Seq[T] =` declare the methods (`fun` and `fun*`), statics and constants of a compiler built-in, without declaring a dummy `enum X = | X` just to hang them on. This is only allowed in `plum-std`; anywhere else it is an error (`primitive Int declares a built-in type and is only allowed in std`).
384
+ Construct a record-shaped enum value by calling its name with `field: value` pairs (any order; every field required), or positionally:
343
385
 
344
386
  ```plum
345
- primitive Config =
346
- MAX_RETRIES = 3
347
- GREETING = "hello"
387
+ cat := Cat(name: "Whiskers", age: 3)
348
-
349
- fun retries() -> Int =
350
- MAX_RETRIES
351
-
352
- fun main() =
353
- n := Config.MAX_RETRIES + Config.retries()
354
388
  ```
355
389
 
390
+ Fields are mutable (`cat.age = cat.age + 1`), and a Gleam-style spread updates a copy from an existing value, overriding just the fields you name:
391
+
356
392
  ```plum
357
- primitive Byte = # in plum-std/Byte.plum
358
- fun toInt(self) -> Int =
393
+ older := Cat(..cat, age: cat.age + 1)
359
- Int(self)
360
394
  ```
361
395
 
362
- The std library uses namespaces where free functions clearly belong together: `Math.sqrt(x)`, `Math.sin(x)`, ... (`import std/Math`). `Time.now()`, `Uuid.v4()`, `Uuid.fromStr(s)`, `Random.float()` and `Random.intBelow(n)` are statics of their types. See [`plum-examples/constants.plum`](plum-examples/constants.plum) and [`plum-examples/numbers.plum`](plum-examples/numbers.plum).
363
-
364
- ## Testing
365
-
366
- Zig-style `test` blocks are a native language feature, compiled and run only by `plum test`:
396
+ ### `match`
367
397
 
368
398
  ```plum
369
- fun add(a: Int, b: Int) -> Int =
370
- a + b
399
+ match n
371
-
372
- test "add works"
400
+ 0 => 1
401
+ x =>
373
- assert add(1, 2) == 3
402
+ if x < 0
403
+ -1
374
- ```
404
+ else
405
+ 2
375
406
 
376
- ```sh
407
+ match maybe
377
- $ plum test plum-examples/testing.plum
378
- └─ add works ✔
408
+ Just(v) => v
409
+ Nothing => 0
379
410
 
380
- 1 passed, 0 failed
411
+ match book
412
+ FantasyBook(title, _, hasMythicalCreatures) when hasMythicalCreatures =>
413
+ "Fantasy book \"{title}\" features mythical creatures"
414
+ FantasyBook(title, _, _) => "Fantasy book \"{title}\" has no mythical creatures"
381
415
  ```
382
416
 
383
- ## Types: records and enums
417
+ Patterns can be literals, a bare identifier (binds), a bare capitalized name (an enum variant, or an error name when matching an error value, also written `error.Name`; compared), a constructor pattern (`Just(v)`, binding its argument, possibly nested), or `_` (wildcard). `match a, b, ...` supports multiple subjects, and a case can carry a `when <expr>` guard. A `match` over an enum must be exhaustive. See [`plum-examples/match.plum`](plum-examples/match.plum), [`plum-examples/dop_visitor.plum`](plum-examples/dop_visitor.plum) and [`plum-examples/oop_visitor.plum`](plum-examples/oop_visitor.plum).
384
418
 
385
- There's a single declaration form, `enum` — a record type is just a single-variant enum whose one variant shares the enum's own name:
419
+ Mixed kinds of value live in one enum with a `match self` method per operation:
386
420
 
387
421
  ```plum
388
- enum Point =
389
- | Point(x: Int, y: Int)
390
-
391
- enum Named =
392
- | Named(name: Str)
393
-
394
- enum Color =
395
- | Red
396
- | Green
397
- | Blue
398
-
399
- enum Maybe =
400
- | Just(Int) # unnamed positional payload — Just(5)
401
- | Nothing
402
-
403
422
  enum Shape =
404
- | Circle(radius: Int) # named payload fields — Circle(radius: 5) or Circle(5)
423
+ | Circle(r: Float)
405
- | Square(x: Int, y: Int)
424
+ | Rect(w: Float, h: Float)
406
425
 
407
- enum Step(n: Int) = # a shared field on every variant ...
426
+ fun area(self) -> Float =
408
- | ReadMin(10) # ... each variant supplies its own value for it
409
- | ReadMax(20)
427
+ match self
428
+ Circle(r) => r * r * 3.0
429
+ Rect(w, h) => w * h
410
430
  ```
411
431
 
432
+ See [`plum-examples/shapes.plum`](plum-examples/shapes.plum) and [`plum-examples/types.plum`](plum-examples/types.plum).
433
+
412
434
  ### `value enum`
413
435
 
414
436
  `value enum` declares a small, copy-semantics value type (a vector, a color, a rectangle) that is flattened into wasm locals instead of being heap-allocated:
@@ -436,7 +458,7 @@ fun wrap(value: T) -> Bool =
436
458
  true
437
459
  ```
438
460
 
439
- Generic types, methods, and free functions are duck-typed and compiled once per distinct instantiation. `List[Int]` and `List(Int)` both parse as generic arguments. See [How generics compile](#how-generics-compile).
461
+ A free function's generic parameters are its single-uppercase-letter type names (`T`, `U`); a type declares its own with `enum Box[T]`. Generic types, methods, and free functions are duck-typed and compiled once per distinct instantiation. See [How generics compile](#how-generics-compile).
440
462
 
441
463
  ### How generics compile
442
464
 
@@ -453,6 +475,7 @@ in outer[Plain] (called at main.plum:9:3):
453
475
 
454
476
  - **Unreachable generics are not emitted.** A generic that no root (a non-generic fn or a test) reaches is neither checked per type nor compiled.
455
477
  - **Typed arrays are native.** Each instantiation stores its elements in an array of its own element type, with no boxing at generic edges.
478
+ - **Bare generic types need type arguments.** `p: Parcel` is an error when `Parcel` is generic; write `p: Parcel[Int]` (or `Parcel[T]` inside a generic).
456
479
  - **Explicit behavior values.** When a generic needs behavior that is not a method on `T`, pass it explicitly, like a Zig comptime parameter or a Haskell dictionary:
457
480
 
458
481
  ```plum
@@ -464,7 +487,9 @@ fun sumWith(xs: List[T], num: Num[T]) -> T =
464
487
  for x := range xs
465
488
  acc = num.add(acc, x)
466
489
  acc
490
+ ```
467
491
 
492
+ ```plum
468
493
  intNum := Num(add: |a, b| a + b, zero: 0)
469
494
  floatNum := Num(add: |a, b| a + b, zero: 0.0)
470
495
  sumWith(List(1, 2, 3), intNum) # 6
@@ -473,22 +498,57 @@ sumWith(List(1.5, 2.5), floatNum) # 4.0
473
498
 
474
499
  `num.add(a, b)` calls the closure stored in the fn-typed field `add`; if the type also declares a method named `add`, the method wins. See `num_record_passed_explicitly` in [`plum-examples/generics.plum`](plum-examples/generics.plum).
475
500
 
476
- - **Mixed types are enums.** To hold several kinds of value in one list, declare an enum and `match self`:
501
+ ## Constants, statics and `primitive`
502
+
503
+ There are no top-level constants and no free-standing statics. A constant lives inside a type's body (`enum`, `value enum`) or a `primitive`; `NAME = expr` declares an associated constant, and a `fun` without `self` is a static function. Both are reached through the type, `Type.NAME` and `Type.f(args)`, from anywhere, and by their bare name inside the same body. A constant is evaluated lazily, once (a heap value such as a `List` is cached and shared), and names are unique per type, so `Alpha.X` and `Beta.X` coexist.
477
504
 
478
505
  ```plum
479
- enum Shape =
506
+ enum Level =
507
+ | Low
480
- | Circle(r: Float)
508
+ | High
481
- | Rect(w: Float, h: Float)
482
509
 
510
+ LIMIT = 10 # Level.LIMIT
511
+
483
- fun area(self) -> Float =
512
+ fun cap(self, n: Int) -> Int =
513
+ if n > LIMIT then LIMIT else n
514
+
484
- match self
515
+ value enum Vec2 =
485
- Circle(r) => r * r * 3.0
516
+ | Vec2(x: Float, y: Float)
517
+
518
+ ZERO = Vec2(x: 0.0, y: 0.0) # Vec2.ZERO
519
+
520
+ fun unit() -> Vec2 = # a static: Vec2.unit()
486
- Rect(w, h) => w * h
521
+ Vec2(x: 1.0, y: 1.0)
487
522
  ```
488
523
 
489
- See [`plum-examples/shapes.plum`](plum-examples/shapes.plum).
524
+ The built-in types carry theirs: `Int.MIN`, `Int.MAX`, `Float.PI`, `Float.TAU`, `Float.EPSILON`, `Float.MAX`, ... and the statics `Int.parse(s)` and `Float.parse(s)`.
490
525
 
491
- - **Bare generic types need type arguments.** `p: Parcel` is an error when `Parcel` is generic; write `p: Parcel[Int]` (or `Parcel[T]` inside a generic).
526
+ ### `primitive`
527
+
528
+ `primitive Name =` has two meanings, by name:
529
+
530
+ - **a namespace**: any name that is not a built-in type declares a stateless namespace of constants and static functions, usable anywhere. It has no `self`, no values and no fields, so `Name` is not a type and can't be constructed or used as a value; it only appears as `Name.NAME` or `Name.f()`. Use an `enum` when you need markers or a set of cases.
531
+ - **a built-in's API**: `primitive Int =`, `primitive Float =`, `primitive Bool =`, `primitive Str =`, `primitive Byte =`, `primitive ByteSlice =`, `primitive Array[T] =` and `primitive Seq[T] =` declare the methods (`fun` and `fun*`), statics and constants of a compiler built-in, without declaring a dummy `enum X = | X` just to hang them on. This is only allowed in `plum-std`; anywhere else it is an error (`primitive Int declares a built-in type and is only allowed in std`).
532
+
533
+ ```plum
534
+ primitive Config =
535
+ MAX_RETRIES = 3
536
+ GREETING = "hello"
537
+
538
+ fun doubled() -> Int =
539
+ MAX_RETRIES * 2
540
+
541
+ fun main() =
542
+ n := Config.MAX_RETRIES + Config.doubled()
543
+ ```
544
+
545
+ ```plum
546
+ primitive Byte = # in plum-std/Byte.plum
547
+ fun toInt(self) -> Int =
548
+ Int(self)
549
+ ```
550
+
551
+ The std library uses namespaces where free functions clearly belong together: `Math.sqrt(x)`, `Math.sin(x)`, ... (`import std/Math`). `Time.now()`, `Uuid.v4()`, `Uuid.fromStr(s)`, `Random.float()` and `Random.intBelow(n)` are statics of their types. See [`plum-examples/constants.plum`](plum-examples/constants.plum) and [`plum-examples/numbers.plum`](plum-examples/numbers.plum).
492
552
 
493
553
  ## Closures
494
554
 
@@ -506,86 +566,180 @@ fun main() -> Int =
506
566
  each(|v| v * 3) + useCapturingClosure()
507
567
  ```
508
568
 
509
- A closure literal is `|params| body`. Capture is snapshot-by-value: a closure copies the outer variables it references at creation time, not re-read live later. A `fn(...) -> T` type annotates a closure-typed parameter or field.
569
+ A closure literal is `|params| body`. Capture is snapshot-by-value: a closure copies the outer variables it references at creation time, not re-read live later. A `fn(...) -> T` type annotates a closure-typed parameter or field, and a named function can be passed wherever a `fn` value is expected. `List` and `Map` have no closure-taking iteration methods (use `for`, below); closures are for passing behavior around as values (see `Num` under [Generics](#how-generics-compile)). See [`plum-examples/closures.plum`](plum-examples/closures.plum).
510
570
 
511
- ## `self`, field access, and methods
571
+ ## Iteration
512
572
 
513
- A `fun` declared indented directly inside an `enum` body is a method, with an implicit `self`:
573
+ `while` and `for` are the only loops. A `for` has binders, a source, optional clauses and a body:
514
574
 
515
575
  ```plum
516
- enum Cat =
517
- | Cat(name: Str, age: Int)
518
- fun getAge(self) -> Int =
519
- self.age
520
- fun birthday(self) -> Int =
521
- self.age + 1
522
-
523
- fun main() -> Int =
524
- c := Cat(name: "Whiskers", age: 3)
576
+ for u := range units # a list
525
- a := c.getAge()
577
+ total = total + u.hp
526
- w := Wrapper(inner: c, tag: 1)
578
+ for i := range 10 # an Int: 0, 1, ... 9
579
+ total = total + i
580
+ for i := range 2.until(10) # an Int fun* method (also to, downTo)
581
+ total = total + i
582
+ for i, u := range units # two binders: (position, element)
527
- w.inner.age # chained field access
583
+ printLn("{i}: {u.name}")
584
+ for _, v := range scores # a Map: (key, value); `_` ignores a value
585
+ total = total + v
586
+ for k := range scores.keys() # a fun* method of Map (also values(), entries())
587
+ printLn(k)
528
588
  ```
529
589
 
530
- ## Constructing values
590
+ `for i := range n` counts 0 to n-1, and the `Int` methods `a.until(b)` (a to b-1), `a.to(b)` (inclusive) and `a.downTo(b)` give other ranges (`for i := range 1.to(10)`).
591
+
592
+ ### The iteration protocol
593
+
594
+ The protocol is resolved on the type of the source, after type inference:
595
+
596
+ - one binder: the type has `length()` and `at(i)` (a `List`, `Array`, `Deque`, a variadic parameter, your own type);
597
+ - two binders: if the type declares a `fun* entries(self) *(A, B)` method it is used (`Map` does: `(key, value)`), otherwise `(position, element)` over `length()`/`at(i)`;
598
+ - `x.name(args)` where the type of `x` declares `fun* name(self, ..)`: the method is expanded like any `fun*`, with `self` bound to `x`.
599
+
600
+ A `for` takes clauses after the source: `, if cond` skips an element and `, name := expr` binds a value for the later clauses and the body. A `for` is an expression when its source has a result (`-> R`), with the body's last expression as the value of each step:
531
601
 
532
602
  ```plum
533
- Cat(name: "Whiskers", age: 3)
603
+ alive := for u := range units.filter(), if u.team == me.team
604
+ u.hp > 0.0
534
605
  ```
535
606
 
536
- Construct a record-shaped enum value by calling its name with `field: value` pairs (any order; every field required).
607
+ ### `fun*` iterators
537
608
 
538
- Fields are mutable (`c.age = c.age + 1`), and a Gleam-style spread updates a copy from an existing value, overriding just the fields you name:
609
+ A `fun*` is an iterator. `*T` (or `*(A, B)`) is what each `yield` hands to the `for` binders, `-> R` is the optional result of the whole `for`. `yield a, b` runs the `for` body; as an expression (`if yield x`, `acc = yield acc, x`) its value is the body's last expression. A `fun*` is expanded at the use site (hygienic, one loop, no closure, no `call_indirect`); it can't be recursive, can't `return`, and can only be used as a `for` source. `break` and `continue` in the body act on the innermost loop around the `yield`. A `fun*` declared inside an `enum` body takes `self` and is used as `xs.name()`.
539
610
 
540
611
  ```plum
612
+ primitive Int =
541
- older := Cat(..c, age: c.age + 1)
613
+ fun* until(self, end: Int) *Int =
614
+ i := self
615
+ while i < end
616
+ cur := i
617
+ i = i + 1
618
+ yield cur
542
619
  ```
543
620
 
544
- ## `match`
621
+ A user-defined one:
545
622
 
546
623
  ```plum
547
- match n
548
- 0 => 1
624
+ fun* neighbours(x: Int, y: Int) *(Int, Int) =
549
- x =>
550
- if x < 0
625
+ for dx := range 3
626
+ for dy := range 3
627
+ if dx != 1 || dy != 1
551
- -1
628
+ yield x + dx - 1, y + dy - 1
552
- else
629
+ ```
553
- 2
554
630
 
555
- match maybe
556
- Just(v) => v
557
- Nothing => 0
631
+ ### `Seq[T]` and the chain methods
558
632
 
559
- match book
560
- FantasyBook(title, _, hasMythicalCreatures) when hasMythicalCreatures =>
561
- "Fantasy book \"{title}\" features mythical creatures"
562
- FantasyBook(title, _, _) => "Fantasy book \"{title}\" has no mythical creatures"
633
+ `Seq[T]` is a compiler primitive and `*T` is sugar for it. A `Seq` never exists at runtime: it can't be stored, returned or captured, it only flows into a `for` source or into the next method call. The methods live in [`plum-std/Seq.plum`](plum-std/Seq.plum) under `primitive Seq[T]`; `xs.m()` where the type of `xs` has no `m` of its own and is iterable (`length()`/`at(i)`, or an `entries()` method) resolves to `Seq.m`, so they work on every list-like type:
634
+
635
+ - **adapters** reshape a sequence, have no result and may appear anywhere in a chain: `take(n)`, `skip(n)`, `stepBy(n)`, `zip(other)`, `reversed()`, `chunks(n)` (the last three need a `List` receiver);
636
+ - **terminals** take the `for` body (the predicate, the mapping, the step) and must be the last call: `filter()` (`-> List[T]`), `map()` (`-> List[U]`), `find()` (`-> T?`), `fold(init)` (binders `acc, x`), `count()`, `any()`, `all()`, `each()`, `firstN(n)` (the first n elements the body accepts), and `toList()`, which has no body and needs no `for`: `ys := xs.skip(1).take(3).toList()`.
637
+
638
+ A terminal in the middle of a chain is an error (`'filter' needs the for body; it must be the last call in the chain`), and so is storing an adapter's result (`s := xs.take(2)`).
639
+
640
+ ```plum
641
+ # the first five living units' names, as one fused loop
642
+ names := for u := range units.take(5).map(), if u.hp > 0.0
643
+ u.name
563
644
  ```
564
645
 
565
- Patterns can be literals, a bare identifier (binds), a bare capitalized name (an enum variant, or an error name when matching an error value, also written `error.Name`; compared), a constructor pattern (`Just(v)`, binding its argument), or `_` (wildcard). `match a, b, ...` supports multiple subjects, and a case can carry a `when <expr>` guard.
646
+ Performance guarantee: a whole chain is ONE loop with no closure allocation, no `call_indirect` and no intermediate `List`. The method chain below compiles to the same single loop as the hand-written one:
566
647
 
648
+ ```plum
649
+ total := for acc, u := range units.skip(1).take(10).fold(0.0), if u.team == me.team
567
- ## String interpolation
650
+ acc + u.hp
651
+ ```
568
652
 
569
653
  ```plum
654
+ total := 0.0 # the loop you would have written by hand
655
+ seen := 0
656
+ i := 0
657
+ while i < units.length()
658
+ u := units.at(i)
659
+ i = i + 1
570
- fun greet(name: Str) -> Str =
660
+ if seen >= 1 && seen < 11
571
- "Hello, {name}!"
661
+ if u.team == me.team
662
+ total = total + u.hp
663
+ seen = seen + 1
572
664
  ```
573
665
 
574
- `{expr}` interpolates any expression: a variable, literal, attribute access, method call, `&&`/`||`/comparison or an inline `if`.
666
+ See [`plum-examples/seq.plum`](plum-examples/seq.plum) (one test per adapter and terminal), [`plum-examples/chains.plum`](plum-examples/chains.plum), [`plum-examples/forms.plum`](plum-examples/forms.plum) and [`plum-examples/loops.plum`](plum-examples/loops.plum).
575
667
 
576
- ## `extern` functions and printing
668
+ ### Iteration cheat sheet
669
+
670
+ | Go / Rust / streams | Plum |
671
+ |---|---|
672
+ | `for _, u := range units` / `for u in &units` | `for u := range units` |
673
+ | `for i, u := range units` / `.iter().enumerate()` | `for i, u := range units` |
674
+ | `for k, v := range m` / `for (k, v) in &m` | `for k, v := range m` (identical to Go) |
675
+ | `for i := 0; i < n; i++` / `for i in 0..n` | `for i := range n` or `range a.until(b)` |
676
+ | `for i := n; i >= 0; i--` / `for i in (0..=n).rev()` | `for i := range n.downTo(0)` |
677
+ | `for i := 0; i < n; i += 2` | `for i := range 0.until(n).stepBy(2)` |
678
+ | `.iter().filter(p).collect()` / `stream.filter(p)` | `for u := range units.filter()` then the predicate as the body |
679
+ | `.iter().map(f).collect()` / `stream.map(f)` | `for u := range units.map()` then the mapped value as the body |
680
+ | `.iter().find(p)` / `findFirst(p)` | `for u := range units.find()` (`-> T?`) |
681
+ | `.iter().fold(0, f)` / `reduce(0, f)` | `for acc, u := range units.fold(0)` |
682
+ | `.iter().take(n).skip(m)` / `limit(n).skip(m)` | `units.skip(m).take(n)` |
683
+ | `.iter().step_by(n)` / `.rev()` / `.zip(o)` / `chunks(n)` | `stepBy(n)` / `reversed()` / `zip(o)` / `chunks(n)` |
684
+ | `.count()` / `.any(p)` / `.all(p)` / `forEach(f)` | `count()` / `any()` / `all()` / `each()` |
685
+ | `if p(x) { continue }` | `, if cond` clause after the source |
686
+ | generator / `yield` | `fun* name(..) *T` with `yield` |
687
+
688
+ ## Optionals
689
+
690
+ An optional is `T?`: a `T`, or `null`. It is its own type, not an error: it carries no error name, and `catch` and `try` do not apply to it. A plain `T` in a `T?` slot is the present value, and `null` is the absent value wherever a `T?` is expected (a call or constructor argument, a `return`, a tail, a list element, a parameter default such as `n: Int? = null`). `null` with nothing to say what it is an optional of is an error (`x := null`: `cannot infer the type of null`). For a reference type (a class, enum, `Str`, `List`, ...) a `T?` is stored as a nullable reference, so a present value costs no allocation; for `Int`, `Float`, `Bool` and value enums it is the value plus a present flag. A doubled `T??` is not allowed.
577
691
 
578
692
  ```plum
693
+ enum Person =
579
- extern fun printLn(s: Str)
694
+ | Person(name: Str, age: Int)
695
+
696
+ fun find(n: Int) -> Person? =
697
+ if n > 0
698
+ return Person(name: "a", age: n)
699
+ return null
580
700
  ```
581
701
 
582
- `extern fun` declares a function backed by a host-provided wasm import, with no Plum body. At the boundary `Int` is narrowed to a wasm `i32` (the rest of the language uses 64-bit `Int`), so extern parameters and results are 32-bit. `plum-std/Os.plum` declares `printLn` for use via `import std/Os`.
702
+ | Form | Meaning |
703
+ |---|---|
704
+ | `opt ?: default` | the value, or `default` when `opt` is null. The default may diverge: `opt ?: return null`, `opt ?: return error.Absent`, `opt ?: crash "msg"` |
705
+ | `if x := opt` + block | runs the block with `x: T` bound when present (an optional `else` block runs on null). Also inline: `if x := opt then f(x) else 0` |
706
+ | `while x := opt` + block | loops while the expression is non-null, re-evaluated each iteration: `while job := queue.next()` |
707
+ | `opt!` | the value, or crashes with `<callee> returned null at file:line:col` |
708
+ | `opt == null`, `opt != null` | presence test (either order) |
709
+ | `opt?.field`, `opt?.method()` | maps over a present value; null passes through. A field that is itself `U?` flattens |
710
+
711
+ `?:` binds looser than comparison, arithmetic and `||`, tighter than `if`/`catch`, and is right-associative, so `a ?: b ?: c` is `a ?: {b ?: c}`. `catch` is for error unions and `?:` for optionals; using the wrong one is a type error (`catch is for errors; use ?:`, `?: is for optionals; use catch`).
583
712
 
584
- ```sh
713
+ ```plum
714
+ fun port(cfg: Config) -> error{Unconfigured}!Int =
715
+ p := cfg.port ?: return error.Unconfigured # absence becomes an error
716
+ p
717
+
585
- $ plum run plum-examples/io.plum
718
+ fun total(xs: List[Int?]) -> Int =
719
+ sum := 0
720
+ for x := range xs
721
+ if n := x
586
- hello from plum
722
+ sum = sum + n
723
+ sum
587
724
  ```
588
725
 
726
+ ### Safe navigation: `?.`
727
+
728
+ `obj?.field` / `obj?.method(args)` (safe navigation, Groovy/Kotlin-style) reaches into an optional (or an error union) without unwrapping it -- a present value maps the field or method access over the inner value and re-wraps it, null (or an error) passes through untouched. A field that is itself a `T?` flattens, so a chain is one `?` value, not a nested one:
729
+
730
+ ```plum
731
+ enum Target =
732
+ | Target(name: Str)
733
+
734
+ enum User =
735
+ | User(name: Str, target: Target?)
736
+
737
+ fun targetName(u: User?) -> Str? =
738
+ u?.target?.name
739
+ ```
740
+
741
+ It's sugar for `obj.andThen(|v| v.field)` -- nothing more than that; chain it as many times as needed. See [`plum-examples/optionals.plum`](plum-examples/optionals.plum) and [`plum-examples/safe_navigation.plum`](plum-examples/safe_navigation.plum).
742
+
589
743
  ## Error handling
590
744
 
591
745
  There are no exceptions. A fallible function returns `!T` (Zig-style: a `T`, *or* an error). An error is an ordinary value written `error.Name` -- `return error.NegativeNum` -- that needs no declaration and carries no data; return details in a normal value instead. Error names are global, and the *set* of names a function can fail with is inferred from its body:
@@ -694,68 +848,70 @@ enum Failure =
694
848
  | BadRequest(Str)
695
849
  ```
696
850
 
697
- ### Optionals: `T?`
851
+ An error union can have an optional as its success value: `!T?` (`error{Bad}!Int?`). `try x` gives the `T?`, and `?:` then handles the null: `v := try parse(s)` followed by `v ?: 0`. An error union can also be the success type of another (`List[!Int].get(i)` is a `!!Int`): unwrap it one level at a time with `!`, `try` or `catch`. See [`plum-examples/error_propagation.plum`](plum-examples/error_propagation.plum).
698
852
 
699
- An optional is `T?`: a `T`, or `null`. It is its own type, not an error: it carries no error name, and `catch` and `try` do not apply to it. A plain `T` in a `T?` slot is the present value, and `null` is the absent value wherever a `T?` is expected (a call or constructor argument, a `return`, a tail, a list element, a parameter default such as `n: Int? = null`). `null` with nothing to say what it is an optional of is an error (`x := null`: `cannot infer the type of null`). For a reference type (a class, enum, `Str`, `List`, ...) a `T?` is stored as a nullable reference, so a present value costs no allocation; for `Int`, `Float`, `Bool` and value enums it is the value plus a present flag. A doubled `T??` is not allowed.
853
+ ### Runtime representation
700
854
 
701
- ```plum
702
- fun find(n: Int) -> Person? =
703
- if n > 0
704
- return Person(name: "a", age: n)
705
- return null
706
- ```
855
+ Each error name gets a global `i32` code, interned in sorted order from 1 (0 means success). An optional `T?` of a value type is `(T..., i32)` too (the `i32` is 0 when present, and the code of `null` when absent), and of a reference type a nullable reference. A `!T` result is a multi-value return `(T..., i32)`, so neither success nor failure allocates; stored in a heap slot (a field, a `List` element, a generic container) it is boxed into a `{value, code}` struct.
707
856
 
708
- | Form | Meaning |
857
+ ## Testing
709
- |---|---|
710
- | `opt ?: default` | the value, or `default` when `opt` is null. The default may diverge: `opt ?: return null`, `opt ?: return error.Absent`, `opt ?: crash "msg"` |
711
- | `if x := opt` + block | runs the block with `x: T` bound when present (an optional `else` block runs on null). Also inline: `if x := opt then f(x) else 0` |
712
- | `while x := opt` + block | loops while the expression is non-null, re-evaluated each iteration: `while job := queue.next()` |
713
- | `opt!` | the value, or crashes with `<callee> returned null at file:line:col` |
714
- | `opt == null`, `opt != null` | presence test (either order) |
715
- | `opt?.field`, `opt?.method()` | maps over a present value; null passes through. A field that is itself `U?` flattens |
716
858
 
717
- `?:` binds looser than comparison, arithmetic and `||`, tighter than `if`/`catch`, and is right-associative, so `a ?: b ?: c` is `a ?: {b ?: c}`. `catch` is for error unions and `?:` for optionals; using the wrong one is a type error (`catch is for errors; use ?:`, `?: is for optionals; use catch`).
859
+ Zig-style `test` blocks are a native language feature, compiled and run only by `plum test`. Test blocks go at the end of the file; `plum test` takes files and directories (searched recursively):
718
860
 
719
861
  ```plum
720
- fun port(cfg: Config) -> error{Unconfigured}!Int =
862
+ fun add(a: Int, b: Int) -> Int =
721
- p := cfg.port ?: return error.Unconfigured # absence becomes an error
722
- p
863
+ a + b
723
864
 
724
- fun total(xs: List[Int?]) -> Int =
865
+ test "add works"
725
- sum := 0
726
- for x := range xs
866
+ assert add(1, 2) == 3
727
- if n := x
728
- sum = sum + n
729
- sum
730
867
  ```
731
868
 
732
- An error union can have an optional as its success value: `!T?` (`error{Bad}!Int?`). `try x` gives the `T?`, and `?:` then handles the null: `v := try parse(s)` followed by `v ?: 0`. An error union can also be the success type of another (`List[!Int].get(i)` is a `!!Int`): unwrap it one level at a time with `!`, `try` or `catch`.
869
+ ```sh
870
+ $ plum test plum-examples/testing.plum
871
+ └─ add works ✔
733
872
 
734
- ### Safe navigation: `?.`
873
+ 1 passed, 0 failed
874
+ ```
735
875
 
736
- `obj?.field` / `obj?.method(args)` (safe navigation, Groovy/Kotlin-style) reaches into an optional (or an error union) without unwrapping it -- a present value maps the field or method access over the inner value and re-wraps it, null (or an error) passes through untouched. A field that is itself a `T?` flattens, so a chain is one `?` value, not a nested one:
876
+ A failing `assert` prints the actual and expected values for `==` comparisons. `import std/Testing` adds small assertion helpers (`expectEq`, `expectTrue`, `expectSome`, ...). The `plum-std` library and every file in `plum-examples` carry their own tests: `plum test plum-std plum-examples`.
877
+
878
+ ## `extern` functions and hosts
737
879
 
738
880
  ```plum
739
- fun cityName(person: Person?) -> Str? =
881
+ extern fun printLn(s: Str)
740
- person?.city?.name
741
882
  ```
742
883
 
743
- It's sugar for `obj.andThen(|v| v.field)` -- nothing more than that; chain it as many times as needed.
744
-
745
- ### Runtime representation
884
+ `extern fun` declares a function backed by a host-provided wasm import (module `plum`, the function's name), with no Plum body. At the boundary `Int` is narrowed to a wasm `i32` (the rest of the language uses 64-bit `Int`), so extern parameters and results are 32-bit, and an extern can't return an error union. `plum-std/Os.plum` declares `printLn` for use via `import std/Os`; `plum run`, `plum test` and `plum build` provide the standard host functions (printing, `crash`, clock and random, files, HTTP).
746
885
 
747
- Each error name gets a global `i32` code, interned in sorted order from 1 (0 means success). An optional `T?` of a value type is `(T..., i32)` too (the `i32` is 0 when present, and the code of `null` when absent), and of a reference type a nullable reference. A `!T` result is a multi-value return `(T..., i32)`, so neither success nor failure allocates; stored in a heap slot (a field, a `List` element, a generic container) it is boxed into a `{value, code}` struct.
886
+ ```sh
887
+ $ plum run plum-examples/io.plum
888
+ hello from plum
889
+ hello, world!
890
+ ```
748
891
 
749
892
  ## Standard library highlights
750
893
 
751
894
  - **`Bool`** — built in (no import); `true`/`false`, `toStr` is a compiler intrinsic, `"true".parseBool()` (in `std/Str`) parses it
752
- - **`Str`** (`import std/Str`) — a byte array with a codepoint-aware layer for Unicode correctness: `runeLength`, `runeAt`, `codePointAt`, `codePointToStr`
753
- - **`Byte`** / **`[]Byte`** (`import std/Byte`) — a scalar unsigned 8-bit value and a fixed-length mutable byte slice
754
- - **`Buffer`** (`import std/Buffer`) — a growable, mutable byte sequence for building up a `Str`, amortized O(n) writes
755
- - **`Array[T]`** (`import std/Array`) — the compiler-intrinsic generic array that `List` and `Map` are written on
756
895
  - **`Int`** / **`Float`** (`import std/Int`, `import std/Float`) — built-in numbers with their API in `primitive Int` / `primitive Float`: `abs`, `sign`, `min`, `max`, `sqrt`, `pow`, `floor`, `round`, `toStr`, ..., the constants `Int.MAX`, `Float.PI`, ..., and `Int.parse` / `Float.parse`
757
896
  - **`Math`** (`import std/Math`) — a `primitive` namespace of floating-point functions: `Math.sqrt`, `Math.sin`, `Math.exp`, `Math.ln`, ...
897
+ - **`Str`** (`import std/Str`) — a byte array with a codepoint-aware layer for Unicode correctness: `runeLength`, `runeAt`, `codePointAt`, `codePointToStr`
898
+ - **`Byte`** / **`ByteSlice`** (`import std/Byte`, `import std/ByteSlice`) — a scalar unsigned 8-bit value and a fixed-length mutable byte slice
758
- - **`Map[K, V]`** (`import std/Map`) — a real bucketed hash table
899
+ - **`Buffer`** (`import std/Buffer`) — a growable, mutable byte sequence for building up a `Str`, amortized O(n) writes
900
+ - **`Array[T]`** (`import std/Array`) — the compiler-intrinsic generic array that `List` and `Map` are written on; `Grid[T]` is a flat 2D array
759
901
  - **`List[T]`** (`import std/List`) — a growable list built on `Array`, with `get`/`add`/`set`/`removeAt`/`remove`/`sort`/`join`/`chunk` (iteration and transformation via `for`)
902
+ - **`Map[K, V]`**, **`Set[T]`**, **`Deque[T]`**, **`Heap[T]`** — a bucketed hash table, a set over it, a ring-buffer double-ended queue and a key-ordered min-heap
903
+ - **`Seq[T]`** (`import std/Seq`) — the adapters and terminals of the `for` chains
904
+ - **`Json`**, **`Base64`**, **`Uuid`**, **`Time`**, **`Random`** — parsing, encoding, ids, wall-clock time and a seeded deterministic generator (`Random.float()` and `Random.intBelow(n)` use the wall clock)
905
+ - **`Os`**, **`Http`** — printing and the file system; HTTP requests through the host
906
+ - **`Testing`** (`import std/Testing`) — assertion helpers for `test` blocks
760
907
 
761
908
  See [`plum-examples/`](plum-examples) for a runnable file per feature above, and [`plum-std/`](plum-std) for the standard library source.
909
+
910
+ ## Editor support
911
+
912
+ ```sh
913
+ plum editor helix # tree-sitter grammar, queries and plum-lsp for Helix
914
+ plum editor vscode # the VSCode extension (add --insiders for VSCode Insiders)
915
+ ```
916
+
917
+ Both install syntax highlighting and the `plum-lsp` language server (syntax and type diagnostics, keyword and member completion). The grammar, queries and VSCode extension live in [`plum-tooling/`](plum-tooling); everything is embedded in the `plum` binary, so the commands work from any directory. Reload the editor window afterwards.
docs/plans/2026-10-26-game-engine.md CHANGED
@@ -43,7 +43,7 @@ Survey of `../kam_macroquad`:
43
43
  - **Constants:** large top-level constant tables must parse and be usable in expressions. A probe file with `LIMIT = 10` followed by a `type` failed to parse (cause not yet confirmed), and the notes record a SCREAMING_CASE constant gotcha. The fix needs tests with 1k-entry tables.
44
44
  - **Seeded RNG** (`Random(seed: Int)` with xorshift/PCG) for deterministic AI and simulation.
45
45
  - Lazy iteration is deferred. Hot loops use `for … range`, and it's revisited only if profiling shows `map`/`filter` allocation in `update`.
46
- 5. **Value enums** (`value enum Vec2 = | Vec2(x: Float, y: Float)`, `Color`, `TilePos`). Exactly one constructor named like the type, fields only `Int`/`Float`/`Bool` or other value enums (no `Str`/`List`, no recursion). Lowering: flattened locals and params, multi-value returns, copy semantics and in-place field assignment. They are boxed only inside `List`/`Map`/`Try`, and by then they belong to a generic copy, so a `List[Vec2]` copy stores them as GC structs.
46
+ 5. **Value enums** (`value enum Vec2 = | Vec2(x: Float, y: Float)`, `Color`, `TilePos`). Exactly one constructor named like the type, fields only `Int`/`Float`/`Bool` or other value enums (no `Str`/`List`, no recursion). Lowering: flattened locals and params, multi-value returns, copy semantics and in-place field assignment. They are boxed only inside `List`/`Map`/error unions/optionals, and by then they belong to a generic copy, so a `List[Vec2]` copy stores them as GC structs.
47
47
  6. **Duck-typed operators** on value enums (`add`/`sub`/`mul`/`neg`/`eq`): `pos + dir * speed`.
48
48
  7. **SIMD (late):** `f32x4` (wasm v128) and `Mat4`/`Vec4` in `plum-std/Math.plum`. KaM is 2D, so this is low priority for the port and kept for future 3D work.
49
49
  8. **Port scope (simplified KaM):**
docs/plans/2026-10-26-zig-generics-and-numbers.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Zig-style generics, no traits (Int/Float stay 64-bit)
2
2
 
3
- **Status:** running (user: "make all the language changes accordingly").
3
+ **Status:** completed and merged to master (966bdab, b911c78).
4
4
  **Goal:**
5
5
  - Replace traits and erased generics with Zig-style duck-typed generics: no bounds, a body checked per instantiation at the use site, and one compiled copy per type.
6
6
  - Use enums for mixed-type values.
docs/plans/2026-10-27-optionals-try.md CHANGED
@@ -1,3 +1,7 @@
1
+ # Optionals and prefix `try`
2
+
3
+ **Status:** completed and merged to master (b911c78).
4
+
1
5
  ## Task NL (user decisions; runs after task N): optionals with `null`, Kotlin elvis `?:`, `if x := opt`, `while x := opt`, `!` unwrap
2
6
 
3
7
  User: drop `error.Missing` and `Missing` entirely; the compiler handles null; add Zig-like sugar.
docs/plans/2026-10-27-statics-primitive.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Namespaced constants and statics: `primitive`, consts and statics in type bodies, `Number` removed
2
2
 
3
- **Status:** approved by the user. It runs after the audit and the two fixes (`?` in closures, the formatter).
3
+ **Status:** completed and merged to master (1c1bfe8).
4
4
 
5
5
  ## User decisions
6
6
  - Remove `enum Number = | Int | Float`. Its methods move to `extend Int` and `extend Float`, and the compiler's primitive-wrapping hack (`primitiveWrapVariant`, the implicit Int/Float→Number wrap) is deleted.
docs/plans/2026-10-27-zig-errors.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Zig-style error handling: `!T`, postfix `?` and `!`, `catch`, `fail`
2
2
 
3
- **Status:** running (user approved). **Superseded in part by `2026-10-27-zig-error-sets.md`:** `fail X` became `return error.X`, `!T [A, B]` became `error{A, B}!T` / `Set!T`, and generics are whitespace-free again; the pinned-set syntax and `fail` described below no longer exist. Before E1, an audit of the docs, samples, highlighting and LSP for the for/extend/Seq changes.
3
+ **Status:** completed and merged to master (0dc0e16, 6463f46). **Superseded in part by `2026-10-27-zig-error-sets.md`:** `fail X` became `return error.X`, `!T [A, B]` became `error{A, B}!T` / `Set!T`, and generics are whitespace-free again; the pinned-set syntax and `fail` described below no longer exist. Before E1, an audit of the docs, samples, highlighting and LSP for the for/extend/Seq changes.
4
4
  **Goal:** replace the overlapping spellings (`Try`/`Ok`/`Err`, `Int ! [..]`, postfix `?`, `??`, `? |e| X`, auto-wrapped bare tags) with one Zig-style model. The runtime stays the same: an error union is still a Try-shaped value whose error side is a structural tag union.
5
5
 
6
6
  ## Design (user decisions: postfix `?` propagates and postfix `!` crashes, replacing the earlier `try`-prefix choice; catch default / catch e block; inferred `!T` with optional explicit set)
plum-examples/chains.plum CHANGED
@@ -1,3 +1,5 @@
1
+ # Sequence chaining: combines collection adapters (skip, take, map, filter)
2
+ # into single-pass loops via `std/Seq`.
1
3
  import std/Str
2
4
  import std/Int
3
5
  import std/Float
plum-examples/closures.plum CHANGED
@@ -1,3 +1,5 @@
1
+ # Closures: function-typed parameters (`fn(...) -> R`), closure literals (`|x| expr`),
2
+ # and lexical variable capture from enclosing scopes.
1
3
  import std/Str
2
4
  import std/Int
3
5
  import std/Float
plum-examples/control_flow.plum CHANGED
@@ -1,3 +1,4 @@
1
+ # Control flow: `if`/`else` branching, `while` loops, and `for ... := range` iteration.
1
2
  import std/Str
2
3
  import std/Int
3
4
  import std/Float
plum-examples/forms.plum CHANGED
@@ -1,3 +1,5 @@
1
+ # Fused loop forms: `for` loops with `, if cond` filter clauses and inline
2
+ # mapping expressions, plus custom `fun*` generators with `yield`.
1
3
  import std/Str
2
4
  import std/Int
3
5
  import std/Float
plum-examples/functions.plum CHANGED
@@ -1,3 +1,5 @@
1
+ # Functions: top-level function declarations, typed arguments, return expressions,
2
+ # and recursion.
1
3
  import std/Str
2
4
  import std/Int
3
5
  import std/Float
plum-examples/generics.plum CHANGED
@@ -1,3 +1,5 @@
1
+ # Generics: duck-typed parameterized functions and types (`Box[T]`),
2
+ # specialized per concrete type without runtime boxing or trait bounds.
1
3
  module generics
2
4
  import std/Str
3
5
  import std/Int
plum-examples/loops.plum CHANGED
@@ -1,3 +1,5 @@
1
+ # Loops: `while` condition loops, `for ... := range` iteration over ranges and
2
+ # collections, with inline filtering and mapping.
1
3
  import std/Str
2
4
  import std/Int
3
5
  import std/Float
plum-examples/match.plum CHANGED
@@ -1,3 +1,5 @@
1
+ # Pattern matching: exhaustive `match` expressions over enum variants, integer
2
+ # values, and wildcard `_` patterns with guards.
1
3
  import std/Int
2
4
  import std/Float
3
5
  import std/Str
plum-examples/methods.plum CHANGED
@@ -1,3 +1,5 @@
1
+ # Methods: defining methods inside type bodies with `self`, accessing and
2
+ # mutating fields, and calling methods on instances.
1
3
  import std/Str
2
4
  import std/Int
3
5
  import std/Float
plum-examples/seq.plum CHANGED
@@ -1,3 +1,5 @@
1
+ # Sequence adapters and terminals: using `std/Seq` methods (`take`, `skip`,
2
+ # `map`, `filter`, `find`, `fold`) to process collections in `for` loops.
1
3
  import std/Int
2
4
  import std/Float
3
5
  import std/List
plum-examples/strings.plum CHANGED
@@ -1,3 +1,5 @@
1
+ # Strings: string literals, interpolation `"{var}"`, and Unicode codepoint/rune
2
+ # inspection methods.
1
3
  import std/Int
2
4
  import std/Float
3
5
  import std/Str
plum-examples/testing.plum CHANGED
@@ -1,3 +1,5 @@
1
+ # Testing: native `test "name"` blocks and `assert` statements executed by
2
+ # the `plum test` runner.
1
3
  import std/Str
2
4
  import std/Int
3
5
  import std/Float
plum-examples/typed_arrays.plum CHANGED
@@ -1,3 +1,5 @@
1
+ # Typed arrays: unboxed native `Array[Int]` and `Array[Float]` lowering to
2
+ # WebAssembly primitive arrays, along with flat 2D `Grid[T]`.
1
3
  module typed_arrays
2
4
  import std/Int
3
5
  import std/Float
plum-examples/types.plum CHANGED
@@ -1,3 +1,5 @@
1
+ # Types: algebraic data types via `enum`, named and positional payload fields,
2
+ # and shared fields across variants.
1
3
  import std/Str
2
4
  import std/Int
3
5
  import std/Float
plum-examples/values.plum CHANGED
@@ -1,3 +1,5 @@
1
+ # Value enums: small copy-semantics value types declared with `value enum`,
2
+ # lowered without heap allocation, supporting duck-typed operators.
1
3
  module values
2
4
  import std/Str
3
5
  import std/Int
plum-std/Regex.mi DELETED
@@ -1 +0,0 @@
1
- TBD