plum

#treesitter#compiler#wasm

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

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


5ecf1ab — Peter John 2026-11-05T22:33:48+05:30
Simplify the README and rewrite the docs
Files changed (7) hide show
  1. README.md +40 -1285
  2. compiler/lsp/src/main.rs +18 -8
  3. docs/game.md +93 -0
  4. docs/language.md +526 -0
  5. docs/performance.md +51 -0
  6. docs/std.md +166 -0
  7. docs/tools.md +52 -0
README.md CHANGED
@@ -1,126 +1,56 @@
1
1
  # 👾 Plum
2
2
 
3
- A statically typed, imperative programming language with algebraic data types, inspired by Rust, Zig, Go, Kotlin and Gleam.
3
+ A simple, statically typed programming language with enums, pattern matching and generics. It compiles to WebAssembly.
4
-
5
- - Built on a [tree-sitter](compiler/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 (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
-
8
- ## Language at a glance
9
4
 
10
5
  ```plum
11
- module glance
12
- import std/Os
6
+ import std/Io
13
- import std/Str
14
7
  import std/Int
15
8
  import std/Float
16
- import std/Slice
9
+ import std/Str
17
10
  import std/List
18
11
  import std/Option
19
- import std/Result
20
-
21
- enum ParseError = # one error enum per module
22
- | BadNum
23
- | Negative
24
12
 
25
- enum Shape = # an enum: a Circle or a Rect
13
+ enum Shape =
26
14
  | Circle(r: Float)
27
15
  | Rect(w: Float, h: Float)
28
16
 
29
- fun area(self) -> Float = # a method
17
+ fun area(self) -> Float =
30
18
  match self
31
19
  Circle(r) => r * r * 3.14
32
20
  Rect(w, h) => w * h
33
21
 
34
- SidesMax = 4 # associated constant: Shape.SidesMax
35
-
36
- struct Vec2(x: Float, y: Float) =
37
- Zero = Vec2(x: 0.0, y: 0.0) # Vec2.Zero
38
-
39
- fun add(self, o: Vec2) -> Vec2 = # `a + b` calls a.add(b)
40
- Vec2(x: self.x + o.x, y: self.y + o.y)
41
-
42
- fun parse(s: Str) -> Result[Int, ParseError] = # an Ok(Int), or an Err(ParseError)
43
- n := Int.parse(s).mapErr(|e| BadNum)? # `?` returns the Err from this function
44
- if n < 0
45
- return Err(Negative)
46
- Ok(n)
47
-
48
- fun double(s: Str) -> Result[Int, ParseError] =
49
- n := parse(s)? # `?` unwraps an Ok, or propagates the Err
50
- Ok(n * 2)
51
-
52
- fun firstEven(xs: List[Int]) -> Option[Int] = # an Option is None or Some(value)
22
+ fun firstEven(xs: List[Int]) -> Option[Int] =
53
- for x := range xs # the one loop form
23
+ for x := range xs
54
24
  if x % 2 == 0
55
25
  return Some(x)
56
26
  None
57
27
 
58
28
  fun main(io: Io) =
59
- total := 0
60
- for i := range 10 # an Int counts 0..9
61
- if i % 2 == 0
62
- total = total + i
63
- big := [1, 5, 9].filter(|x| x > 2).map(|x| x * 10) # a list literal
64
- e := firstEven([1, 3, 4]).unwrapOr(-1) # `unwrapOr` supplies a default for None
65
- n := parse("42").unwrapOr(0) # ...and for an Err
66
- pos := Vec2.Zero + Vec2(x: 1.0, y: 2.0)
29
+ shape := Rect(w: 2.0, h: 3.0)
30
+ even := firstEven([1, 3, 4]).unwrapOr(0)
67
- io.printLn("{total} {big.length()} {e} {n} {pos.y} {Shape.SidesMax}")
31
+ io.printLn("area {shape.area()}, first even {even}")
68
32
 
69
- test "glance"
33
+ test "shapes"
70
34
  assert Rect(w: 2.0, h: 3.0).area() == 6.0
71
- assert double("21").unwrapOr(0) == 42
72
- assert parse("-1").unwrapOr(7) == 7
73
35
  assert firstEven([1, 3]).isNone()
74
36
  ```
75
37
 
76
- | You want | Write |
77
- |---|---|
78
- | declare a variable / reassign | `x := 1` / `x = 2` |
79
- | declare with a type | `s: Shape := Circle(r: 2.0)` (only with `:=`, a value is required) |
80
- | drop a result on purpose | `_ := stack.pop()!` |
81
- | a block | indent by 2 spaces, no braces |
82
- | group an expression | `{1 + 2} * 3` (braces; parentheses are for calls) |
83
- | a conditional expression | `if c then a else b` (no `c ? a : b` ternary) |
84
- | loop | `while c`, `for x := range src` (an `Int`, a `Slice`/`Array`, or any type with `next(self) -> Option[Pair[T, Self]]`) |
85
- | a list / a map | `[1, 2, 3]` (a `List`, `[]` empty), `["a" => 1]` (a `Map`) |
86
- | an enum of variants | `enum Shape = Circle(r: Float) \| Rect(w: Float, h: Float)` |
87
- | a product type (the compiler picks flat or boxed) | `struct Vec2(x: Float, y: Float)`, `struct User(name: Str, age: Int)` |
88
- | a method / static / constant | `fun area(self)` / `fun origin()` / `Name = expr` inside the type body |
89
- | generics | `fun id[T](x: T) -> T`, `enum Box[T]`, declared lists, duck-typed, one copy per type |
90
- | operators on your types | define `add`, `sub`, `mul`, `div`, `neg`, `eq`, `lt`, `le`, `gt`, `ge` |
91
- | an optional | `Option[T]`: `None`, `Some(x)`, `opt.unwrapOr(default)`, `if Some(x) := opt`, `opt.map(f)`, `opt!` |
92
- | a fallible function | `-> Result[T, E]`, `return Err(MyError)`, `Ok(x)` |
93
- | propagate / default / crash | `f()?` / `f().unwrapOr(default)` / `f()!` |
94
- | an iterator | a type with `next(self) -> Option[Pair[T, Self]]`, e.g. `0.until(10).stepBy(3)` |
95
- | a namespace of constants and functions | `primitive Math =` |
96
- | a test | `test "name"` with `assert` |
97
- | a host call | a method of `Io`: `io.printLn(s)` (entry points take `io: Io`) |
98
- | a function the host calls | `export fun draw(io: Io, g: Game)` |
99
-
100
- Discarding the result of a call is a compile error, so `scores.push(3)` on a type whose `push` returns a new value cannot silently do nothing. Use the result, or drop it explicitly with `_ :=`:
101
-
102
- ```plum
103
- fun main() =
104
- list := [1, 2, 3].toSlice()
105
- _ := list.pop()! # evaluate, drop the removed element
106
- list.push(4) # fine: push returns Unit
107
- ```
108
-
109
- A call that returns `Unit` can be a bare statement, and so can the last expression of a function, which is its return value.
110
-
111
- ## Installing
112
-
113
- 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 `compiler/tooling/vscode-plum`); without them `plum editor vscode` still installs syntax highlighting.
114
-
115
- ```sh
116
- cargo install --path compiler/cli
117
- ```
38
+ ## Features
118
39
 
119
- Installs a release build as `plum` in `~/.cargo/bin`, which `cargo` puts on your `PATH` for you — no `sudo`/`/usr/local/bin` needed. Re-run this after pulling changes to the compiler; it doesn't auto-update. Use `cargo run -p plum-cli --` instead while actively developing the compiler itself.
40
+ - **Easy to read:** blocks are indentation, and there is one way to loop over anything.
41
+ - **Structs and enums:** `match` checks that you handled every case.
42
+ - **Generics:** write `fun id[T](x: T) -> T`, and each type gets its own fast copy.
43
+ - **No null, no exceptions:** absence is `Option`, failure is `Result`, and `?` passes an error up.
44
+ - **Closures and tail calls:** recursion runs in constant stack.
45
+ - **Tests built in:** add a `test` block next to your code and run `plum test`.
46
+ - **Visible side effects:** printing, files and network go through an `Io` value, so a function without one can't touch the outside world.
47
+ - **Fast by default:** the compiler removes temporary lists and updates values in place when it is safe, with no change to what your code means.
48
+ - **Games:** a built-in engine runs in the browser on WebGPU.
49
+ - **Editor support:** syntax highlighting and a language server for Helix and VSCode.
120
50
 
121
- ### Prebuilt binaries
51
+ Plum compiles to WebAssembly (wasm-gc), and `plum build` wraps the result in a native executable. True native targets are planned.
122
52
 
123
- No Rust toolchain needed — download the `plum` binary directly (it embeds `plum-lsp`, the tree-sitter grammar, and the VSCode extension bundle; `plum editor helix`/`plum editor vscode` extract them on install):
53
+ ## Install
124
54
 
125
55
  <!-- prebuilt-binaries:start -->
126
56
  | Platform | Download |
@@ -138,1202 +68,27 @@ mv plum ~/.cargo/bin/plum # or anywhere else already on your PATH
138
68
  ```
139
69
  <!-- prebuilt-binaries:end -->
140
70
 
141
- 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).
71
+ Or build from source with `cargo install --path compiler/cli`.
142
72
 
143
- ### Using the CLI
73
+ ## Use it
144
74
 
145
- ```sh
146
- plum run file.plum # compile and run under wasmtime
147
- plum test file.plum dir/ # run the `test` blocks of files and directories (recursive)
75
+ Run these from a checkout of this repository, so `import std/...` can find the standard library.
148
- plum compile file.plum # write file.wasm
149
- plum build file.plum # embed the module into a native executable
150
- plum format file.plum # rewrite in place (--check, --stdin)
151
- plum game file.plum # serve a game and open it in the browser (see Games); -o DIR writes a static build
152
- plum editor helix|vscode # install highlighting and the language server
153
- ```
154
-
155
- `import std/Name` resolves to `<lib-path>/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`).
156
-
157
- ### Run the example game
158
76
 
159
77
  ```sh
160
- cargo run -q -p plum-cli -- game examples/games/imperative/game.plum # or, after `cargo install --path compiler/cli`: plum game examples/games/imperative/game.plum
161
- ```
162
-
163
- This compiles the game, serves it on `http://127.0.0.1:8080/` (any free port if that one is taken) and opens it in your default browser. It needs a browser with WebGPU: Chrome or Edge, Safari 26+ or Firefox. Arrow keys or WASD steer the ship, collect the orbs, dodge the mines; Enter or R restarts after the game is over. Press a key first: browsers start audio only after a user gesture.
164
-
165
- ![the example game](docs/game.png)
166
-
167
- ## Layout and comments
168
-
169
- Blocks are indentation-sensitive (2 spaces), like Python — there's no `{ }` for grouping statements. `#` starts a line comment.
170
-
171
- ```plum
172
- # this is a comment
173
- fun main() =
174
- x := 1
175
- if x > 0
176
- x = x + 1
177
- ```
178
-
179
- ## Naming conventions
180
-
181
- | Case | Used for |
182
- |---|---|
183
- | `PascalCase` | types, enums, error sets, namespaces, associated constants (`Float.Pi`) |
184
- | `camelCase` | functions, methods, fields |
185
- | `snake_case` | variables, params |
186
- | `PascalCase` in a `[..]` list (`T`, `Elem`) | generic type parameters, named where declared |
187
-
188
- ## Modules and imports
189
-
190
- A file may start with `module name`, then its imports, then everything else; `test` blocks go last.
191
-
192
- ```plum
193
- module basics
194
-
195
- import std/Os
196
- ```
197
-
198
- Nothing is implicit: `Int`, `Float`, `Str`, `Slice`, ... are available once the file imports `std/Int`, `std/Float`, `std/Str`, `std/Slice`, ...
199
-
200
- ## Literals
201
-
202
- ```plum
203
- dec := 42
204
- hex := 0xFF
205
- bin := 0b1010
206
- big := 1_000_000
207
- flt := 3.14
208
- flt2 := 12.0f
209
- exp := 6.022e23
210
- name := "plum"
211
- escaped := "line one\nline two\ttabbed \"quoted\""
212
- yes := true
213
- no := false
214
- ```
215
-
216
- An integer literal that does not fit in an `Int` is a compile error (`-0x8000_0000_0000_0000` is `Int.Min`).
217
-
218
- ### String interpolation
219
-
220
- ```plum
221
- fun greet(name: Str) -> Str =
222
- "Hello, {name}!"
223
- ```
224
-
225
- `{expr}` interpolates any expression: a variable, literal, attribute access, method call, `&&`/`||`/comparison or an inline `if`.
226
-
227
- ## Operators
228
-
229
- From tightest to loosest binding:
230
-
231
- | Precedence | Operators | Notes |
232
- |---|---|---|
233
- | highest | `.` `?` `!` | attribute access / method call, propagation (`f()?`) and crash-unwrap (`f()!`) |
234
- | | `+` `-` | unary |
235
- | | `*` `/` `%` | |
236
- | | `+` `-` | |
237
- | | `<<` `>>` | |
238
- | | `^` | |
239
- | | `&` | |
240
- | | `\|` | |
241
- | | `<` `<=` `==` `!=` `>=` `>` `<>` | comparisons (`<>` is another spelling of `!=`) |
242
- | | `!` | boolean not |
243
- | | `&&` | |
244
- | | `\|\|` | |
245
- | lowest | `if c then a else b` | inline if (the else branch extends as far right as possible) |
246
-
247
- 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):
248
-
249
- ```plum
250
- sum := 3
251
- bits := 4
252
- cmp := true
253
- grouped := {1 + 2} * {3 - 1}
254
- picked := if cmp then sum else bits
255
- negated := -sum
256
- inverted := !cmp
78
+ plum run file.plum # compile and run
79
+ plum test file.plum # run its tests
80
+ plum build file.plum # make an executable
81
+ plum game file.plum # play a game in the browser
82
+ plum editor helix # set up your editor (or vscode)
257
83
  ```
258
84
 
259
- ### Operators on your own types
260
-
261
- On `Int`, `Float`, `Byte` and `Str` operators are built in. On any other (declared) type, an operator calls a method on the left operand, resolved statically per instantiation:
262
-
263
- | Operator | Method |
264
- |---|---|
265
- | `a + b`, `a - b`, `a * b`, `a / b` | `a.add(b)`, `sub`, `mul`, `div` |
266
- | `-a` | `a.neg()` |
267
- | `a == b`, `a != b` | `a.eq(b)`, `!a.eq(b)` |
268
- | `a < b`, `a <= b`, `a > b`, `a >= b` | `a.lt(b)`, `a.le(b)`, `a.gt(b)`, `a.ge(b)` |
269
-
270
- Each operator needs its own method (no derivation from `lt`/`eq`). The right operand may have a different type: `v * 2.0` calls `mul(self, s: Float)`. Using an operator without the method is an error such as `Vec2 has no operator '+' (define add)`, with the instantiation chain when it happens inside a generic. `==`/`!=` on a type with no `eq` keep their built-in meaning (variant identity).
271
-
272
- ```plum
273
- struct Vec2(x: Float, y: Float) =
274
- fun add(self, o: Vec2) -> Vec2 =
275
- Vec2(x: self.x + o.x, y: self.y + o.y)
276
-
277
- fun mul(self, s: Float) -> Vec2 =
278
- Vec2(x: self.x * s, y: self.y * s)
279
- ```
280
-
281
- See [`examples/numbers.plum`](examples/numbers.plum).
282
-
283
- ## Variables and assignment
284
-
285
- ```plum
286
- x := 1
287
- # declares a new binding — errors if `x` is already in scope
288
- a, b := 1, 2
289
- # multiple targets, positionally paired with multiple values
290
- x = 2
291
- # reassigns an existing binding — type-checked against x's declared type
292
- ```
293
-
294
- ### Annotated declarations
295
-
296
- `name: Type := value` fixes the local's type instead of inferring it from the value. The value is checked against the annotation exactly like an argument for a parameter of that type, so `Some(3)` fits an `Option[Int]` annotation; a later `name = value` converts the same way. Only `:=` takes an annotation (`s: Shape = v` is an error), a value is required (`total: Float` alone is an error), and there is a single target (`a, b: T := ...` is not allowed). `fn(..) -> T` types and a generic parameter in scope (`T`) work as annotations. It adds no numeric conversion: `n: Float := 3` is rejected just as `take(3)` is for a `Float` parameter.
297
-
298
- ```plum
299
- s: Shape := Circle(r: 2.0)
300
- xs: Slice[Shape] := Slice[Shape]()
301
- o: Option[Int] := None
302
- s = Rect(w: 1.0, h: 2.0) # checked, and converted, against Shape
303
- ```
304
-
305
- See [`examples/structs_records.plum`](examples/structs_records.plum).
306
-
307
- ## Control flow
308
-
309
- `if` is also an expression: `if c then a else b` (both branches are required and must have the same type; it chains, `if a then x else if b then y else z`). The block form below, with indented bodies and no `then`, is a statement.
310
-
311
- ```plum
312
- fun sign(n: Int) -> Int =
313
- if n < 0 then -1 else if n == 0 then 0 else 1
314
- ```
315
-
316
- ```plum
317
- if n < 0
318
- return "negative"
319
- else if n == 0
320
- return "zero"
321
- else
322
- return "positive"
323
-
324
- i := start
325
- while i > 0
326
- i = i - 1
327
-
328
- for i := range limit
329
- if i == 3
330
- continue
331
- if i == 8
332
- break
333
- total = total + i
334
-
335
- assert n > 0 # traps at runtime if false
336
- todo # traps at runtime — marks a body as not yet implemented
337
- ```
338
-
339
- `match` is covered under [Types](#match). To test a single case instead, see [Pattern `if`](#pattern-if) below.
340
-
341
- ### Pattern `if`
342
-
343
- `if Pattern := expr` tests one case and binds the pattern's names for that branch only. It uses the same patterns as `match` (constructor patterns with nested fields, literals, `_`, bare member, tag or error names), and reads like any other `if`/`else if`/`else` chain. A `, cond` after the value adds more conditions that already see the bindings. It works in `else if`, inline (`if Circle(r) := s then r else 0.0`) and in `while`:
344
-
345
- ```plum
346
- fun draw(shape: Shape) =
347
- if Circle(r) := shape
348
- drawCircle(r)
349
- else if Rect(w, h) := shape, w > h
350
- drawWide(w, h)
351
- else
352
- drawOther(shape)
353
- ```
354
-
355
- A bare member name narrows the value inside the branch (see [Enums](#enums)):
356
-
357
- ```plum
358
- fun width(s: Shape) -> Float =
359
- if Rect := s
360
- s.w
361
- else
362
- 0.0
363
- ```
364
-
365
- ```plum
366
- while Busy(task) := worker.state # loops while the pattern matches
367
- task.step()
368
- ```
369
-
370
- Use pattern `if` when only one or two cases need code and the rest are "do nothing" or one shared fallback, especially for early exits and guard-style checks. Use [`match`](#match) when every case needs handling: it is checked for exhaustiveness, so adding a variant later is a compile error until each `match` covers it, and pattern `if` is never checked that way. The same job both ways:
371
-
372
- ```plum
373
- # pattern if: one interesting case, early exit
374
- fun attack(unit: Unit) =
375
- if Soldier(hp, Weapon(dmg)) := unit, hp > 0
376
- enemy.hurt(dmg)
377
- ```
378
-
379
- ```plum
380
- # match: every job is handled, the compiler checks it
381
- match job
382
- Idle => rest()
383
- Busy(task) => task.step()
384
- Done(loot) => collect(loot)
385
- ```
386
-
387
- Details:
388
-
389
- - The bindings exist only in that branch's block (and in the `, cond`s after the pattern); using them after the `if`, or in a later `else` branch, is an error.
390
- - A pattern that cannot fail on its type (`Vec2(x, y) := v` on a struct) is allowed and always takes the branch.
391
- - `else if` chains can mix plain conditions, `Some(x) := opt` bindings (see [Option and Result](#option-and-result)) and patterns.
392
- - It works on results too: `if Err(TooBig) := r` tests one error variant.
393
-
394
- See [`examples/match.plum`](examples/match.plum).
395
-
396
- ## Functions
397
-
398
- ```plum
399
- fun addInts(a: Int, b: Int) -> Int =
400
- a + b
401
-
402
- fun greet() = # no return type => Unit
403
- todo
404
-
405
- fun withDefault(a: Int, step: Int = 1) -> Int =
406
- a + step
407
-
408
- fun sumAll(nums: List[Int]) -> Int = # no variadics: take a List, call as sumAll([1, 2])
409
- total := 0
410
- for v := range nums
411
- total = total + v
412
- total
413
- ```
414
-
415
- A body that is a single `match` can start on the signature line: `= match x` followed by the arms indented one level. It is the same as putting the `match` on the next line, for functions and methods alike:
416
-
417
- ```plum
418
- fun sign(n: Int) -> Int = match n
419
- 0 => 0
420
- _ => if n < 0 then -1 else 1
421
- ```
422
-
423
- A function without a return type returns `Unit`; the last expression of a body is its value. Functions take positional arguments; only constructors can name them (see [Constructing values](#constructing-values)).
424
-
425
- ## Types: struct and enum
85
+ ## Learn more
426
86
 
427
- `struct` is the one product type with named fields; an `enum` is a set of variants, each with its own named fields. The other declaration form is `primitive Name =` ([Constants, statics and `primitive`](#constants-statics-and-primitive)).
87
+ - [The language](docs/language.md): the full guide
88
+ - [The standard library](docs/std.md): strings, lists, maps, JSON, files and more
89
+ - [The game engine](docs/game.md): drawing, input, sound and scene graphs
90
+ - [Tools](docs/tools.md): every command and option, editors, building from source
91
+ - [Performance](docs/performance.md): how the compiler optimises your code
92
+ - [Examples](examples): a runnable file per feature, and example games
428
93
 
429
- ### `struct`
430
-
431
- A `struct` is declared with named fields. It may be generic, embed one other struct (its base), have `mut` fields, have no fields, and have a body of methods and constants:
432
-
433
- ```plum
434
- struct Vec2(x: Float, y: Float)
435
-
436
- struct Body(pos: Vec2, size: Vec2) # a field may be another struct
437
-
438
- struct User(name: Str, mut age: Int)
439
-
440
- struct Pair[K, V](key: K, val: V) # generic
441
-
442
- struct Marker() # no fields
443
- ```
444
-
445
- The header ends with `=` only when an indented body follows (methods, constants: see [Methods](#methods-self-and-field-access) and [Constants](#constants-statics-and-primitive)); `struct Vec2(x: Float, y: Float)` with no body has no `=`.
446
-
447
- The compiler chooses each struct's layout in one place (`plum_core::layout`; `plum run --explain` prints `layout <Name>: flat|boxed`). A struct is **flat** when it has at least one field, no generics, no embedding, no `mut` field and no `fun init(self)`, is not recursive, and every field is `Int`, `Float`, `Bool`, `Byte` or another flat struct. Every other struct is **boxed**: a heap object.
448
-
449
- ```plum
450
- a := Vec2(x: 1.0, y: 2.0)
451
- b := a
452
- b.x = 9.0 # a.x is still 1.0: a flat struct is copied
453
- u := User(name: "ann", age: 3)
454
- u2 := u
455
- u2.age = 4 # u.age is now 4: a boxed struct is shared on assignment
456
- ```
457
-
458
- A flat struct is copied on assignment and when passed, compares field by field with `==`, and is boxed inside `Option[T]` and other generic slots. Its fields can be assigned, since it is copied. Arrays of flat structs (`Array[Vec2]`, and so `Slice[Vec2]` and `Grid[Tile]`) are stored field by field, one native array per field, with no per-element allocation. A boxed struct's fields are immutable unless declared `mut` (`u.name = "x"` is an error), and its `==` is identity. To change an immutable field, build an updated value with a spread (`User(..u, name: "bob")`, see [In-place updates](#in-place-updates)). Methods and `match` work as for any enum. See [`examples/structs_records.plum`](examples/structs_records.plum).
459
-
460
- ### Enums
461
-
462
- An `enum` lists its variants. A variant is a payload-less tag (`Red`) or declares named fields inline (`Circle(r: Float)`). A short list fits on one line; a longer one, or one with methods, goes on indented `|` lines. A variant is a constructor and a pattern only, not a type: `Shape` is the only type, so there is no `Circle` to name in a signature or a field:
463
-
464
- ```plum
465
- enum Shape = Circle(r: Float) | Rect(w: Float, h: Float)
466
-
467
- enum Color =
468
- | Red
469
- | Green
470
- | Blue
471
-
472
- enum Json =
473
- | JsonNull
474
- | JsonBool(value: Bool)
475
- | JsonNum(value: Float)
476
- | JsonStr(value: Str)
477
- | JsonArr(items: Slice[Json])
478
- | JsonObj(fields: Map[Str, Json])
479
- ```
480
-
481
- Construct a variant with named arguments; the value has the enum's type (generics are inferred from the arguments or the expected type):
482
-
483
- ```plum
484
- fun make(big: Bool) -> Shape =
485
- if big
486
- Rect(w: 10.0, h: 10.0)
487
- else
488
- Circle(r: 1.0)
489
-
490
- fun label(j: Json) -> Str =
491
- "json"
492
-
493
- fun use() -> Str =
494
- label(JsonNum(value: 2.5))
495
- ```
496
-
497
- Read the fields with a pattern, since an inline variant has no `.field`. A `match` over an enum must be exhaustive:
498
-
499
- ```plum
500
- fun show(j: Json) -> Str =
501
- match j
502
- JsonNull => "null"
503
- JsonBool(value) => if value then "true" else "false"
504
- JsonNum(value) => value.toStr()
505
- JsonStr(value) => "\"{value}\""
506
- JsonArr(_) => "list"
507
- JsonObj(_) => "map"
508
- ```
509
-
510
- An enum defines methods with a `match self` body, one per operation:
511
-
512
- ```plum
513
- enum Shape =
514
- | Circle(r: Float)
515
- | Rect(w: Float, h: Float)
516
-
517
- fun area(self) -> Float =
518
- match self
519
- Circle(r) => r * r * 3.14
520
- Rect(w, h) => w * h
521
-
522
- fun areaOf(s: Shape) -> Float =
523
- s.area()
524
- ```
525
-
526
- See [`examples/unions.plum`](examples/unions.plum). Open-ended dispatch (new kinds added elsewhere) is done with [struct subtyping](#embedding-structs-and-override) instead.
527
-
528
- Variants can hold the enum recursively, which is how recursive data is written:
529
-
530
- ```plum
531
- enum Link[T] = Nil | Cons(head: T, tail: Link[T])
532
-
533
- enum Node =
534
- | Leaf(id: Int)
535
- | Group(id: Int, kids: Link[Node])
536
- ```
537
-
538
- Generics are inferred from the arguments or the expected type:
539
-
540
- ```plum
541
- fun length[T](xs: Link[T]) -> Int = match xs
542
- Nil => 0
543
- Cons(_, t) => 1 + length(t)
544
-
545
- fun twoLinks() -> Link[Int] =
546
- Cons(head: 1, tail: Cons(head: 2, tail: Nil))
547
- ```
548
-
549
- Using a variant as a type is an error, as are unnamed fields and reading a field with `.`:
550
-
551
- - `Cons is a variant of Link, not a type; use Link[...]`
552
- - `variant Some(T) needs named fields: Some(value: T)`
553
- - `read Cons's fields with a pattern: Cons(...)`
554
-
555
- ### Embedding structs and override
556
-
557
- A struct whose header lists exactly one other struct, first, embeds it as its base and is its subtype. It gains the base's fields, `init` and instance methods, converts to the base wherever the base is expected, and `override fun` picks the subtype's method at runtime (dispatch on the concrete type). The header names the base fields a caller passes (`Stage(camScale)`); the rest are set by the base `init`, which runs before the struct's own, or by the struct's `init`. Statics are not copied. A base method written without a body, `abstract fun m(self) -> T`, must be overridden by every subtype, and such a base cannot be constructed. See [`examples/subtyping.plum`](examples/subtyping.plum).
558
-
559
- ```plum
560
- struct Stage(camScale: Float) =
561
- fun init(self) =
562
- self.camX: Float = 0.0
563
- fun act(self, dt: Float) =
564
- self.camX = self.camX + dt
565
- fun draw(self, io: Io) =
566
- io.printLn("stage")
567
-
568
- struct World(Stage(camScale), score: Int) =
569
- override fun draw(self, io: Io) =
570
- self.Stage.draw(io) # the original, from Stage
571
- io.printLn("world")
572
- ```
573
-
574
- ```plum
575
- w := World(camScale: 2.0, score: 0)
576
- w.act(1.0) # inherited from Stage
577
- w.draw() # World.draw: prints "stage" then "world"
578
- ```
579
-
580
- Every method that implements an abstract method or replaces the base's method must be `override fun`; defining one without it is an error (`World.draw hides Stage.draw; mark it override`), and so is an `override fun` that overrides nothing. Inside a struct, `self.Owner.method(args)` calls the original from the base struct `Owner`.
581
-
582
- ### In-place updates
583
-
584
- A spread `T(..x, f: v)` is written into `x` in place when the compiler proves `x` unique (dead after the update, no other holder); `--explain-fusion` shows each such site as `unique spread at fn:line`. No keyword is needed:
585
-
586
- ```plum
587
- struct Cell(x: Float)
588
-
589
- fun bump(s: Cell) -> Cell = Cell(..s, x: s.x + 1.0)
590
- ```
591
-
592
- ### Methods, `self` and field access
593
-
594
- A `fun` declared indented directly inside an `enum` or `struct` body whose first parameter is `self` is a method, called on a value (`c.getAge()`). One without `self` is a static function, called on the type (`Cat.named("Tom")`), and can't use `self`. Calling a static on a value, or a method on the type, is an error:
595
-
596
- ```plum
597
- struct Cat(name: Str, age: Int) =
598
- fun getAge(self) -> Int =
599
- self.age
600
-
601
- fun birthday(self) -> Int =
602
- self.age + 1
603
-
604
- fun named(name: Str) -> Cat = # static: Cat.named("Tom")
605
- Cat(name: name, age: 0)
606
-
607
- struct Wrapper(inner: Cat, tag: Int)
608
-
609
- fun main() -> Int =
610
- c := Cat(name: "Whiskers", age: 3)
611
- a := c.getAge()
612
- w := Wrapper(inner: c, tag: 1)
613
- a + w.inner.age # chained field access
614
- ```
615
-
616
- A field of a `struct` is read with `.field`. On an enum, variant fields are read with a pattern (see [Enums](#enums)).
617
-
618
- ### Constructing values
619
-
620
- Construct a `struct` by calling its name with `field: value` pairs (any order; every field required), or positionally. This is the only call that can name its arguments:
621
-
622
- ```plum
623
- cat := Cat(name: "Whiskers", age: 3)
624
- ```
625
-
626
- Fields are immutable unless declared `mut` (`mut age: Int` allows `cat.age = cat.age + 1`), and a Gleam-style spread builds an updated value from an existing one, overriding just the fields you name:
627
-
628
- ```plum
629
- older := Cat(..cat, age: cat.age + 1)
630
- ```
631
-
632
- ### Constructors and init
633
-
634
- A struct's header is its constructor: the parameters a caller passes. A struct that embeds a base struct lists the base fields it exposes, `Actor(x, y)`; a bare `Actor` exposes none. Everything else is set in `fun init(self)`, which runs once per construction (the base's `init` first, then the struct's own):
635
-
636
- ```plum
637
- struct Bag(n: Int) =
638
- fun init(self) =
639
- self.label = "bag"
640
- self.items: Slice[Int] = Slice[Int]()
641
- self.cap: Int = self.n * 2
642
- ```
643
-
644
- ```plum
645
- bag := Bag(n: 9) # label "bag", items a fresh empty list, cap 18
646
- ```
647
-
648
- In a struct's `init`, a top-level `self.f = expr` that names no header or base field declares an internal field: its type comes from the literal, a constructor call or an annotation (`self.f: T = expr`), and it is not a constructor parameter. Naming anything but a parameter in a call is an error (`Bag has no constructor parameter cap`), every field must be passed or set in some `init`, and `self.method()` is allowed in `init` only after every field has a value. A fresh list per construction therefore comes from `init`, never from a shared default. Field defaults (`name: Type = expr`) do not exist.
649
-
650
- ### `match`
651
-
652
- ```plum
653
- match n
654
- 0 => 1
655
- x =>
656
- if x < 0
657
- -1
658
- else
659
- 2
660
-
661
- match shape
662
- Circle(r) => r
663
- Rect(w, h) => w * h
664
-
665
- match book
666
- FantasyBook(title, _, hasMythicalCreatures) when hasMythicalCreatures =>
667
- "Fantasy book \"{title}\" features mythical creatures"
668
- FantasyBook(title, _, _) => "Fantasy book \"{title}\" has no mythical creatures"
669
- ```
670
-
671
- Pattern names follow a casing rule: a lowercase name always binds, a Capitalised name must be a known member or tag (`unknown tag Foo` otherwise), and a lowercase name equal to a tag is an error (`did you mean Foo?`). Patterns can be literals, a bare identifier (binds), a bare capitalized name (a member or tag of an enum; compared), a constructor pattern (`Circle(r)`, binding its fields, possibly nested), a bare member name (`Float`, which narrows the subject to that type, see [Enums](#enums)), 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 [`examples/match.plum`](examples/match.plum).
672
-
673
- ### Enum `init`
674
-
675
- `TypeName(args)` on an enum calls its static `init`, the same call syntax as a struct. The result is any member of the enum:
676
-
677
- ```plum
678
- module shapes
679
- import std/Int
680
- import std/Str
681
-
682
- enum Shape =
683
- | Dot
684
- | Line(len: Int)
685
-
686
- fun init(n: Int) -> Shape = if n == 0 then Dot else Line(len: n)
687
-
688
- fun size(self) -> Int = match self
689
- Dot => 0
690
- Line(len) => len
691
-
692
- test "enum init"
693
- assert Shape(0).size() == 0
694
- assert Shape(7).size() == 7
695
- ```
696
-
697
- ## Lists, slices and arrays
698
-
699
- Three sequence types, from `std/List`, `std/Slice` and `std/Array`.
700
-
701
- **`List[T]`** is the std cons list: `Nil` or `Cons(head, tail)`. It is the default for recursive code, with pattern matching and the list literal `[1, 2, 3]` (`[]` is `Nil`). Each cell is one allocation. Self tail calls run as loops and a constructor-wrapped self call (`map`) is rewritten to a loop (tail modulo cons), so recursion over a million cells uses constant stack. `for x := range xs` works, but `at(i)` is O(i), so a plain index loop is quadratic.
702
-
703
- ```plum
704
- module lists
705
- import std/Int
706
- import std/Str
707
- import std/List
708
-
709
- fun sum(xs: List[Int], acc: Int) -> Int = match xs
710
- Nil => acc
711
- Cons(h, t) => sum(t, acc + h)
712
-
713
- test "list"
714
- xs := [1, 2, 3, 4]
715
- assert sum(xs, 0) == 10
716
- assert xs.map(|x| x * 2).length() == 4
717
- total := 0
718
- for x := range xs
719
- total = total + x
720
- assert total == 10
721
- ```
722
-
723
- **`Slice[T]`** is the growable view over an `Array` (the type formerly called `List`): indexed access, `push`; `Slice[T]()` is empty and `[1, 2, 3].toSlice()` copies a list literal.
724
-
725
- ```plum
726
- module slices
727
- import std/Int
728
- import std/Slice
729
-
730
- test "slice"
731
- s := [1, 2, 3].toSlice()
732
- s.push(4)
733
- assert s.length() == 4
734
- assert s.at(3) == 4
735
- ```
736
-
737
- **`Array[T]`** is the fixed-length array, unchanged: arrays of structs are stored field by field. See [`examples/arrays.plum`](examples/arrays.plum).
738
-
739
- ## Generics
740
-
741
- ```plum
742
- struct Box[T](value: T) =
743
- fun map[B](self, f: fn(T) -> B) -> Box[B] =
744
- Box(value: f(self.value))
745
-
746
- fun wrap[T](value: T) -> Bool =
747
- true
748
-
749
- fun firstOr[Elem](xs: List[Elem], d: Elem) -> Elem = match xs
750
- Nil => d
751
- Cons(h, _) => h
752
- ```
753
-
754
- Generics are declared, never guessed from spelling. A type declares its own with `struct Box[T]`; a function declares its own after its name, `fun wrap[T](..)`, `fun map[B](..)`. A method sees its owner's generics (`T` in `Box.map`) without redeclaring them, and redeclaring one is an error (`T is already a generic of Box`). A generic name can be any type name (`Elem`, `Key`); an undeclared name is an ordinary type, so using one that does not exist is an error. Single letters are not special: `struct P(x: Int)` and `enum Two = A | B` are fine. Generic types, methods, and free functions are duck-typed: see [How generics compile](#how-generics-compile).
755
-
756
- A generic call whose type parameters stay unknown is an error at the call, before codegen: `len(Nil)` gives `cannot infer T for len(Nil); annotate, e.g. empty: Link[Int] := Nil`. Annotate the value (`empty: Link[Int] := Nil`) so `T` is known.
757
-
758
- ### How generics compile
759
-
760
- Generics are duck-typed and compiled per type, as in Zig. A generic body is type-checked once per concrete instantiation, and each instantiation gets its own compiled copy with unboxed values (an array of its own element type, no boxing at generic edges): `Slice[Int]` and `Slice[Str]` are different code, and a `Slice[Float]` stores raw `f64`s.
761
-
762
- - **No bounds.** `[T]` carries no constraint. `fun show[T](x: T) -> Str = x.toStr()` works for every type that has a `toStr` method and fails at the use site otherwise.
763
- - **Errors point at the use site.** A missing method is reported against the concrete type, with the chain of instantiations that led there:
764
-
765
- ```
766
- in outer[Plain] (called at main.plum:9:3):
767
- in maxOf[Plain] (called at main.plum:5:3):
768
- Plain has no method 'lt'
769
- ```
770
-
771
- - **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.
772
- - **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).
773
- - **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:
774
-
775
- ```plum
776
- struct Num[T](add: fn(T, T) -> T, zero: T)
777
-
778
- fun sumWith[T](xs: Slice[T], num: Num[T]) -> T =
779
- acc := num.zero
780
- for x := range xs
781
- acc = num.add(acc, x)
782
- acc
783
- ```
784
-
785
- ```plum
786
- intNum := Num(add: |a, b| a + b, zero: 0)
787
- floatNum := Num(add: |a, b| a + b, zero: 0.0)
788
- sumWith([1, 2, 3].toSlice(), intNum) # 6
789
- sumWith([1.5, 2.5].toSlice(), floatNum) # 4.0
790
- ```
791
-
792
- `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 [`examples/generics.plum`](examples/generics.plum).
793
-
794
- ## How fusion works
795
-
796
- After type checking and closure specialisation, `typedSource` runs a supercompiler stage over the checked program, before lowering (`purity`, `simplify`, `drive`, `uniqueness`, `reuse` in `plum-core`), before tail-call rewriting. The result is re-checked; if the rewritten program does not check, the original is kept, so fusion can only change speed, never meaning.
797
-
798
- What it does:
799
-
800
- - **Fuses a pure producer into its consumer.** A function is a candidate only when it is expression-bodied: bindings plus one tail expression (`= match`, `= if`, `= Ctor(..)`, `= call`); block bodies with early returns are skipped (`--explain-fusion` says `not expression-bodied`). When a candidate is pure (no `Io` reachable, no writes outside its own fresh values) and its result is used once, by a function that is one `match` on that parameter, the call is specialised on the producer's constructors and the intermediate value is never built. `sumL(mapCells(xs))` becomes one loop.
801
- - **List chains.** `xs.map(f).filter(g).reduce(...)`-style chains on `List` fuse the same way, so the intermediate lists disappear (the reason `List.filter` is written in direct recursive style).
802
- - **View to draw.** `view(state).draw(io)` fuses the scene built by a pure `view` into the draw walk, so the frame's picture tree is not allocated.
803
- - **Uniqueness to in-place spreads.** `markUnique` proves that a spread source (`T(..x, f: v)`) is not used afterwards and not shared, and marks the construction `unique`: codegen then writes the fields into `x` instead of copying it.
804
- - **Reuse.** A same-shape rebuild of a value that is dead afterwards (a `match` arm that rebuilds the constructor it matched) reuses the old cell.
805
-
806
- Switches: `plum build|run|test|game --no-fusion` skips the whole stage; `--explain-fusion` prints each decision to stderr (`fusion: ...`, `unique spread at fn:line`, and why a call was not specialised).
807
-
808
- Limits:
809
-
810
- - A result that is shared (used twice, stored in a field, captured by a closure) is not fused.
811
- - Uniqueness is proved for locals and fields of fresh values only; field assignments are not yet marked.
812
- - Reuse does not go through tail-recursion-modulo-cons, which runs after it.
813
-
814
- ## Constants, statics and `primitive`
815
-
816
- There are no top-level constants and no free-standing statics. A constant lives inside a type's body (`enum`, `struct`) or a `primitive`; `Name = expr` declares an associated constant, and a `fun` without `self` is a static function. A constant's name is PascalCase, at least two characters, with no underscores and no two capitals in a row (`MaxRetries`, `Ln2`, `Log10E`). A constant is always read as `Type.Name`, from anywhere including inside the same body; a static is called as `Type.f(args)`, or by its bare name inside the same body. A constant is evaluated lazily, once (a heap value such as a `Slice` is cached and shared), and names are unique per type, so `Alpha.Flag` and `Beta.Flag` coexist. A constant may not share its name with a variant of its own type.
817
-
818
- ```plum
819
- enum Level =
820
- | Low
821
- | High
822
-
823
- Limit = 10 # Level.Limit
824
-
825
- fun cap(self, n: Int) -> Int =
826
- if n > Level.Limit then Level.Limit else n
827
-
828
- struct Vec2(x: Float, y: Float) =
829
- Zero = Vec2(x: 0.0, y: 0.0) # Vec2.Zero
830
-
831
- fun unit() -> Vec2 = # a static: Vec2.unit()
832
- Vec2(x: 1.0, y: 1.0)
833
- ```
834
-
835
- The built-in types carry theirs: `Int.Min`, `Int.Max`, `Float.Pi`, `Float.Tau`, `Float.E`, `Float.Epsilon`, `Float.Max`, ... and the statics `Int.parse(s)` and `Float.parse(s)`.
836
-
837
- ### `primitive`
838
-
839
- `primitive Name =` has two meanings, by name:
840
-
841
- - **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.Const` or `Name.f()`. Use an `enum` when you need markers or a set of cases.
842
- - **a built-in's API**: `primitive Int =`, `primitive Float =`, `primitive Bool =`, `primitive Str =`, `primitive Byte =`, and `primitive Array[T] =` declare the methods, 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 `std`; anywhere else it is an error (`primitive Int declares a built-in type and is only allowed in std`).
843
-
844
- ```plum
845
- primitive Config =
846
- MaxRetries = 3
847
- Greeting = "hello"
848
-
849
- fun doubled() -> Int =
850
- Config.MaxRetries * 2
851
-
852
- fun main() =
853
- n := Config.MaxRetries + Config.doubled()
854
- ```
855
-
856
- ```plum
857
- primitive Byte = # in std/Byte.plum
858
- fun toInt(self) -> Int =
859
- Int(self)
860
- ```
861
-
862
- The std library has no free functions: each function is a static of its type or of a `primitive` namespace, so `Math.sqrt(x)`, `Os.readFile(io, path)`, `Base64.encode(s)`, `Http.get(url)` and `Testing.expectEq(a, b)` (see [Standard library highlights](#standard-library-highlights)). `Time.now(io)`, `Uuid.v4(rng)`, `Uuid.fromStr(s)`, `Random.seeded(seed)` are statics of their types. `Random` is a plain value: `r.nextInt()`, `r.nextIntBelow(n)` and `r.nextFloat()` return a `Rolled` (the `value` and the next `rng`), so the same seed gives the same sequence; `io.randomSeed()` supplies a wall-clock seed. See [`examples/constants.plum`](examples/constants.plum) and [`examples/numbers.plum`](examples/numbers.plum).
863
-
864
- ## Closures
865
-
866
- ```plum
867
- fun each(cb: fn(Int) -> Int) -> Int =
868
- cb(5)
869
-
870
- fun useCapturingClosure() -> Int =
871
- offset := 100
872
- cb := |v|
873
- v + offset
874
- cb(5)
875
-
876
- fun main() -> Int =
877
- each(|v| v * 3) + useCapturingClosure()
878
- ```
879
-
880
- 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. `Slice` 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 [`examples/closures.plum`](examples/closures.plum).
881
-
882
- A closure parameter may carry a type, `|b: Box| b.get()`, and then it has that type (a mismatch with an expected `fn(..)` is a type error). An untyped closure stored in a local takes its parameter types from the first call of that local; if there is none, the error is `cannot infer the type of b; write |b: Box|`.
883
-
884
- A function may return a function; write the return type as `fn(A) -> B`. Bind the result and call it:
885
-
886
- ```plum
887
- fun adder(n: Int) -> fn(Int) -> Int =
888
- |x| x + n
889
-
890
- fun main() -> Int =
891
- add1 := adder(1)
892
- add1(2)
893
- ```
894
-
895
- Calling the result of a call directly (`adder(1)(2)`) is not supported yet.
896
-
897
- ### Automatic specialisation
898
-
899
- A closure literal or a function name passed directly to a `fn` parameter that the callee only calls (never stores, returns, captures or passes on) is specialised automatically: the compiler emits a copy of the callee with the closure body inlined, so there is no closure allocation and no indirect call. Captured variables become extra parameters of the copy. Escaping closures behave as before, and a callee gets at most 8 specialised copies before further call sites fall back to ordinary closure calls.
900
-
901
- ```plum
902
- fun twice(f: fn(Int) -> Int, x: Int) -> Int =
903
- f(f(x))
904
-
905
- test "specialised"
906
- k := 3
907
- assert twice(|v| v + k, 1) == 7
908
- ```
909
-
910
- ### Tail calls
911
-
912
- A call in tail position (the last thing a function does: the value of its body, of an `if`/`match` arm in tail position, or of a `return`) compiles to a real tail call, for direct calls, methods and closure calls, so tail-recursive loops run in constant stack and a 200000-step recursion is fine:
913
-
914
- ```plum
915
- fun countUp(n: Int, acc: Int) -> Int =
916
- if n <= 0 then acc else countUp(n - 1, acc + 1)
917
- ```
918
-
919
- A call whose result is then used (`1 + f(x)`) or converted on return (for example a plain value wrapped in `Some`, or an `Int` widened) is not a tail call and uses stack.
920
-
921
- ## Iteration
922
-
923
- `while` and `for` are the only loops, and `for x := range v` is the only `for` form (`_` ignores the value):
924
-
925
- ```plum
926
- for u := range units # a Slice or Array, by position
927
- total = total + u.hp
928
- for i := range 10 # an Int: 0, 1, ... 9
929
- total = total + i
930
- for i := range 2.until(10) # a Range (also to, downTo, stepBy)
931
- total = total + i
932
- for x := range [1, 2, 3] # a List
933
- total = total + x
934
- for e := range scores.entries() # a Map's entries: Pair(first: key, second: value)
935
- total = total + e.second
936
- for k := range scores.keys() # also values()
937
- io.printLn(k)
938
- ```
939
-
940
- Beyond `range n` (0 to n-1), the `Int` methods `a.until(b)` (a to b-1), `a.to(b)` (inclusive) and `a.downTo(b)` return a `Range`, and `r.stepBy(n)` changes its step (`for i := range 0.until(10).stepBy(3)`).
941
-
942
- ### The iteration protocol
943
-
944
- The source of a `for` is one of:
945
-
946
- - an `Int` n: the value counts 0, 1, ... n - 1;
947
- - a `Slice` or `Array`: by position;
948
- - any value whose type has `next(self) -> Option[Pair[T, Self]]`: `None` means done, otherwise `Some(Pair(first: value, second: rest))` binds the value and the loop continues with the rest. `List`, `Range` and your own types use this.
949
-
950
- A user-defined iterator is an ordinary type with `next`:
951
-
952
- ```plum
953
- struct Countdown(n: Int) =
954
- fun next(self) -> Option[Pair[Int, Countdown]] =
955
- if self.n <= 0 then None else Some(Pair(first: self.n, second: Countdown(n: self.n - 1)))
956
- ```
957
-
958
- There are no generators, no sequence chains and no `for` clauses: filter with an `if` in the body, bind with `name := expr` inside the body, and build a result with a `Slice` or with `List.map`/`filter`/`fold`:
959
-
960
- ```plum
961
- alive := Slice[Soldier]()
962
- for u := range units
963
- if u.team == me.team && u.hp > 0.0
964
- alive.push(u)
965
- ```
966
-
967
- See [`examples/sequences.plum`](examples/sequences.plum) and [`examples/control_flow.plum`](examples/control_flow.plum).
968
-
969
- ### Iteration cheat sheet
970
-
971
- | Go / Rust / streams | Plum |
972
- |---|---|
973
- | `for _, u := range units` / `for u in &units` | `for u := range units` |
974
- | `for i, u := range units` / `.iter().enumerate()` | `for i := range units.length()` then `u := units.at(i)` |
975
- | `for k, v := range m` / `for (k, v) in &m` | `for e := range m.entries()` then `e.first`, `e.second` |
976
- | `for i := 0; i < n; i++` / `for i in 0..n` | `for i := range n` or `range a.until(b)` |
977
- | `for i := n; i >= 0; i--` / `for i in (0..=n).rev()` | `for i := range n.downTo(0)` |
978
- | `for i := 0; i < n; i += 2` | `for i := range 0.until(n).stepBy(2)` |
979
- | `.iter().filter(p).collect()` | `xs.filter(p)` on a `List`, or a loop with an `if` that pushes to a `Slice` |
980
- | `.iter().map(f).collect()` | `xs.map(f)` on a `List` |
981
- | `.iter().fold(0, f)` | `xs.fold(0, f)` on a `List` |
982
- | `if p(x) { continue }` | `if` in the body, or `continue` |
983
- | generator / `yield` | a type with `next(self) -> Pair[T, Self]?` |
984
-
985
- ## Option and Result
986
-
987
- There is no `null`, no optional type syntax and no exceptions. Absence is `Option[T]` (`None` or `Some(value)`) and failure is `Result[T, E]` (`Ok(value)` or `Err(error)`). Both are ordinary enums from `std/Option` and `std/Result`, so they are matched, constructed and extended like any other enum. A plain `T` is never implicitly an `Option[T]`: write `Some(x)`.
988
-
989
- ```plum
990
- struct Person(name: Str, age: Int)
991
-
992
- fun find(n: Int) -> Option[Person] =
993
- if n > 0
994
- return Some(Person(name: "a", age: n))
995
- return None
996
- ```
997
-
998
- `Slice.get`, `head`, `last`, `first`, `pop`, `findValue`, `sample` and `Map.get` return an `Option`, because a missing element is not an error. Functions that fail for a reason return a `Result` with one error enum per module: `Int.parse`/`Float.parse` give `Result[Int, NumError]`, the `Os` file functions `Result[Str, IoError]` (`IoFailed`, `NoSuchPath`), `Json.fromStr` and `fromJson` `Result[T, JsonError]` (`InvalidJson`, `WrongJsonType`, `MissingField`).
999
-
1000
- | Form | Meaning |
1001
- |---|---|
1002
- | `Some(x) := opt` + block | runs the block with `x` bound when `opt` is `Some`; an optional `else` block runs otherwise. Also inline: `if Some(x) := opt then f(x) else 0`. See [Pattern `if`](#pattern-if) |
1003
- | `while Some(x) := opt` + block | loops while the expression is `Some`, re-evaluated each iteration |
1004
- | `match opt` | `Some(v) => ...`, `None => ...` (exhaustive); for a result, `Ok(v)` / `Err(e)` |
1005
- | `opt.unwrapOr(default)` | the value, or `default` when `None` (or `Err`) |
1006
- | `opt.map(f)`, `opt.andThen(f)` | map over a present value; `andThen` takes a function that itself returns an `Option` (or `Result`) |
1007
- | `res.mapErr(f)` | change the error value, e.g. into the calling module's error enum |
1008
- | `opt.okOr(err)`, `res.ok()` | convert between `Option` and `Result` |
1009
- | `x?` | propagate: the value of an `Ok`/`Some`, or return the `Err`/`None` from the enclosing function |
1010
- | `x!` | unwrap: the value, or crash with the source position (`file:line`) |
1011
-
1012
- ```plum
1013
- fun total(xs: Slice[Option[Int]]) -> Int =
1014
- sum := 0
1015
- for x := range xs
1016
- if Some(n) := x
1017
- sum = sum + n
1018
- sum
1019
-
1020
- fun firstOrZero(xs: List[Int]) -> Int =
1021
- xs.head().unwrapOr(0)
1022
- ```
1023
-
1024
- ### Propagating: `x?`
1025
-
1026
- `x?` is a postfix operator. On a `Result` it evaluates to the `Ok` value, or returns the `Err` from the enclosing function, which must itself return a `Result` with the same error type. On an `Option` it evaluates to the `Some` value, or returns `None` from a function returning an `Option`. Postfix operators and calls bind tighter than anything else, so `a.b()?.c()` is `{a.b()?}.c()`. To mix error types, convert with `mapErr` first.
1027
-
1028
- ```plum
1029
- enum ParseError =
1030
- | BadNumber
1031
- | Negative
1032
-
1033
- fun parsePositive(s: Str) -> Result[Int, ParseError] =
1034
- n := Int.parse(s).mapErr(|e| BadNumber)?
1035
- if n < 0
1036
- return Err(Negative)
1037
- Ok(n)
1038
-
1039
- fun sumTwo(a: Str, b: Str) -> Result[Int, ParseError] =
1040
- x := parsePositive(a)?
1041
- y := parsePositive(b)?
1042
- Ok(x + y)
1043
-
1044
- fun firstOf(xs: Slice[Int]) -> Option[Int] =
1045
- h := xs.head()?
1046
- Some(h * 2)
1047
- ```
1048
-
1049
- A function that has an `Option` in hand and wants an error uses `okOr`: `xs.head().okOr(Empty)?`.
1050
-
1051
- ### Handling in place
1052
-
1053
- `match` handles each case, with a default or a recovery per arm; `unwrapOr` supplies a plain default:
1054
-
1055
- ```plum
1056
- fun sumOrZero(a: Str, b: Str) -> Int =
1057
- x := parsePositive(a).unwrapOr(0)
1058
- y := parsePositive(b).unwrapOr(0)
1059
- x + y
1060
-
1061
- fun describeAge(s: Str) -> Str =
1062
- match parsePositive(s)
1063
- Ok(n) => "age {n}"
1064
- Err(BadNumber) => "not a number"
1065
- Err(Negative) => "can't be negative"
1066
- ```
1067
-
1068
- The patterns are positional, so a `match` over a `Result[T, ParseError]` is exhaustive once every variant of `ParseError` is covered; adding a variant points you at every `match` that has to deal with it. Cases in an error enum carry data like any other variant, and `name()`-style helpers are ordinary methods.
1069
-
1070
- ### Crashing: `x!` and `crash`
1071
-
1072
- `x!` unwraps an `Option` or `Result`, or crashes with a message that shows the source position (`file:line`). `!=` wins by maximal munch, so write `{f()!} == x`. `crash "msg"` aborts the same way. Use both for bugs (a broken invariant), not for expected failures; `unwrap()` is the method form.
1073
-
1074
- See [`examples/errors.plum`](examples/errors.plum) and [`examples/error_propagation.plum`](examples/error_propagation.plum).
1075
-
1076
- ## Testing
1077
-
1078
- Zig-style `test` blocks are a native language feature, compiled and run only by `plum test`. `plum test` takes files and directories (searched recursively) and runs only the tests declared in those files, not the ones in their imports:
1079
-
1080
- ```plum
1081
- fun add(a: Int, b: Int) -> Int =
1082
- a + b
1083
-
1084
- test "add works"
1085
- assert add(1, 2) == 3
1086
- ```
1087
-
1088
- ```sh
1089
- $ plum test examples/basics.plum
1090
- └─ add works ✔
1091
-
1092
- 1 passed, 0 failed
1093
- ```
1094
-
1095
- A failing `assert` prints the actual and expected values for `==` comparisons. `import std/Testing` adds small assertion helpers (`Testing.expectEq`, `Testing.expectTrue`, `Testing.expectSuccess`, ...). The `std` library and every file in `examples` carry their own tests: `plum test std examples`.
1096
-
1097
- ## `extern` functions and hosts
1098
-
1099
- ```plum
1100
- import std/Io
1101
-
1102
- fun main(io: Io) =
1103
- io.printLn("hello")
1104
- ```
1105
-
1106
- Every call that leaves the program (printing, files, clock, randomness, network, drawing, input, sound) is a method of the built-in `Io` capability, declared once as `extern fun` methods in `primitive Io` (`std/Io.plum`); an `Io` cannot be constructed in user code, so no host import is reachable without one. An entry point (`main`, an `export fun`, a `test`) may take it as its first parameter (a `test` gets `io` implicitly), and code passes it down only to the functions that do I/O, so a function with no `Io` reachable cannot touch the host. That is all purity is: there is no function colouring and no `pure` keyword. An `extern fun` method 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. The `io` parameter is not part of the wasm ABI. `plum run`, `plum test` and `plum build` provide the standard host functions (printing, `crash`, clock and random, files, HTTP).
1107
-
1108
- ```sh
1109
- $ plum run examples/basics.plum
1110
- hello from plum
1111
- hello, world!
1112
- ```
1113
-
1114
- ## `export fun`
1115
-
1116
- ```plum
1117
- export fun step(n: Int, v: Vec2) -> Vec2 =
1118
- Vec2(x: v.x + 1.0, y: v.y * 2.0)
1119
- ```
1120
-
1121
- `export fun` (top level only: not a method, not generic, no error-union result) is an ordinary Plum function that is also exported from the wasm module under its own name. Its boundary uses the `extern fun` ABI: `Int` is narrowed to `i32`, `Float` is `f64`, `Bool` is `i32`, a struct is flattened into its fields' values, and `Str` and structs are GC references. Plum code can still call it directly with the normal internal ABI. A function can't be both `extern` and `export`.
1122
-
1123
- Every module that imports `std/Str` also exports four helpers for hosts that can't read GC structs: `strLen(s) -> i32`, `strByte(s, i) -> i32`, `strNew(n) -> Str` (n zero bytes) and `strSetByte(s, i, b)` (for a fresh `strNew` result only).
1124
-
1125
- ## Games
1126
-
1127
- Games run in the browser on Plum's own WebGPU renderer (`compiler/game/web/plum_bridge.js`): the Plum module calls the bridge directly, one extern call per draw, and the bridge batches everything into one buffer upload per frame. A draw call binds up to 16 textures (each vertex carries its texture slot), so mixing atlases in y-sorted scenes costs a handful of draw calls; the canvas is `CSS size x devicePixelRatio` while every game coordinate (`screenWidth`, `mousePos`, the camera) stays in CSS pixels; text is drawn from a signed-distance-field glyph atlas, so it stays crisp at any size or zoom; and if the GPU device is lost the renderer requests a new one, re-uploads the textures and logs `GPU device lost (<reason>); restored` (render-target contents start transparent again). WebGPU is required (Chrome or Edge, Safari 26+, Firefox); there is no fallback. The bridge reads nothing from the wasm module's import or export lists (`WebAssembly.Module.imports` throws in Safari for typed GC references), so it loads the same way in Safari; `plum-web-test --browser webkit` checks that against Playwright's WebKit.
1128
-
1129
- `plum game file.plum [--assets DIR] [--port N] [--no-open]` compiles the game, serves it on 127.0.0.1 (port 8080, else any free port; `--port` picks one), prints the URL and opens the default browser unless `--no-open` is given. Ctrl-C stops it. `plum game file.plum -o dist [--assets DIR]` writes a static build instead and exits: `index.html`, `plum_bridge.js`, `plum.wasm`, `preload.txt` and an `assets/` copy of the whole asset directory (every file in it is listed in `preload.txt` and fetched before the first frame). Host that directory on any static file server; the browser needs wasm GC support. The asset root defaults to `assets` beside the source file.
1130
-
1131
- `examples/games/imperative/game.plum` is a small arcade game: steer a ship with the arrow keys or WASD, collect orbs, dodge mines (Enter restarts). It draws shapes, text, additive orb glow (`io.blendMode(Blend.Add)`) and an animated sprite sheet (`examples/games/assets/sprites.png`, generated by `examples/games/tools/make_sprites.py`) and plays music and sound effects (`examples/games/assets/*.wav`, generated by `examples/games/tools/make_sounds.py`); both generators live outside the asset dir so they are not shipped. For a bigger game built on this engine see the Knights and Merchants port in the sibling repo `../kam_plum` (`./run.sh` there). Its rules are plain functions with `test` blocks, so `plum test examples` covers them without a browser.
1132
-
1133
- **`std/game/imperative/Scene`** is a scene-graph library written in Plum (see [`examples/games/imperative/scene.plum`](examples/games/imperative/scene.plum)): a tree of actors under a `Stage`, with parent-relative position, rotation, scale and colour, per-actor `act`, `draw` and `hit` behaviour, a camera and hit testing; its demo renders a minimap into a texture with `textureCreate` and `renderTarget`. Use it for UI and other hierarchical scenes, where moving a parent should move everything in it. It is libGDX Scene2D-style subclassing on a base struct (a game's own world can embed the `Stage` struct to get its root, camera and `act`/`draw`, see [Embedding structs and override](#embedding-structs-and-override)): `Actor` is a base struct holding the position, size, origin, scale, rotation, colour, visibility, children and culling fields, and `Group`, `Sprite`, `Rect`, `Circle` and `Text` are structs embedding it. `Actor.init` sets every field (position and size 0.0, scale 1.0, white, visible, a fresh `children` list), so a struct exposes only what a caller passes (`struct Rect(Actor(width, height, color))`) and its own `init` sets the rest. A game adds its own kinds the same way and overrides what it needs:
1134
-
1135
- ```plum
1136
- struct Walker(Actor(x, y), speed: Float) =
1137
- fun init(self) =
1138
- self.width = 8.0
1139
- self.height = 8.0
1140
- self.color = Color.Red
1141
- override fun act(self, dt: Float) =
1142
- self.x = self.x + self.speed * dt
1143
- override fun draw(self, m: Affine, alpha: Float) =
1144
- drawRectBox(m, alpha, self.width, self.height, self.color)
1145
- ```
1146
-
1147
- ```plum
1148
- stage.root.add(Walker(x: 0.0, y: 40.0, speed: 30.0))
1149
- ```
1150
-
1151
- The default `act` acts the children (an override that keeps children calls `self.actChildren(dt)`), the default `draw` paints nothing and the default `hit(p)` tests the width by height box. Compare actors with `same(a, b)`, not `==` (actors are compared by identity). For a large world, keep the game objects in flat structs and lists instead (for example `enum Entity = Unit | House`) and draw them directly through `std/game/Game`.
1152
-
1153
- `std/game/imperative/Scene` follows libGDX Scene2D for draw order and culling. An actor's z-index is its position in its parent's child list (draw order is list order, hit testing goes in reverse):
1154
-
1155
- ```plum
1156
- a.zIndex() # index among its siblings, -1 without a parent
1157
- a.setZIndex(0) # move within the parent (clamped to 0..length-1)
1158
- a.toFront() # last child: drawn on top, hit first
1159
- a.toBack() # first child: drawn first
1160
-
1161
- world.sortChildren(|a, b| a.y < b.y) # stable, in place, no allocation once warm
1162
- world.setCullingArea(cam.x, cam.y, viewW, viewH) # in world's own coordinates
1163
- world.clearCullingArea()
1164
- ```
1165
-
1166
- `sortChildren(before)` is a natural merge sort with a scratch list kept on the actor: a list that is already sorted costs n - 1 comparisons and a nearly sorted one (a y-sorted layer re-sorted every frame) about 2n, with O(n log n) as the worst case; equal keys keep their order, so ties do not flicker. `setCullingArea` makes a group skip each direct child whose box (position and size, scaled about its origin; rotation ignored) lies outside the rectangle, in both `draw` and `hit`, with the child's whole subtree; children with no size (plain groups) are never culled. In [`compiler/game/web-test/BENCHMARK.md`](compiler/game/web-test/BENCHMARK.md) a 20,000-sprite y-sorted layer costs 1.15 ms per frame with culling and 4.20 ms without on the earlier closure-based `Node` (2.09 ms with culling on the `Actor` base struct).
1167
-
1168
- A layered world (a layer recipe for a large map) is a few groups under the stage root, drawn in order: `terrain` (static, culled), `world` (y-sorted every frame, culled), `effects` (culled) and `ui` (not culled, in screen space with a second `Stage` or camera). Each frame: `world.sortChildren(|a, b| a.y < b.y)`, then `setCullingArea` on `terrain`, `world` and `effects` from the camera view.
1169
-
1170
-
1171
- A game declares three exports. `G` is any struct and holds all game state (there are no globals):
1172
-
1173
- ```plum
1174
- export fun init(io: Io) -> G
1175
- export fun update(io: Io, g: G, dt: Float)
1176
- export fun draw(io: Io, g: G)
1177
- ```
1178
-
1179
- The page calls `init` once after the assets are loaded, then `update` and `draw` every frame. A game imports `std/Io`, `std/game/Game`, `std/game/Vec2` and `std/game/Color`; pure logic can import `std/game/Vec2` and `std/game/Color` alone. Under `plum test` and `plum run` the host functions are stubs that trap with `host function 'X' is not available here`, so `test` blocks work as long as they don't call the host.
1180
-
1181
- **Headless tests:** the `plum-game` crate (a standalone crate, wasmtime only) runs a compiled game with no browser against a `Recorder` backend that logs every host call as a string and answers input from scripted values, so `cargo test --manifest-path compiler/game/Cargo.toml` checks games' draw-call sequences (`compiler/game/tests/host_test.rs`). `plum-game <wasm> [--assets DIR] [--frames N]` does the same from the command line and prints the host calls of the last frame.
1182
-
1183
- **Immediate mode or Scene?** [`examples/games/imperative/game.plum`](examples/games/imperative/game.plum) is immediate mode: one state struct, a pure `update`, and `draw` issues the calls each frame; it suits flat arcade games with a few kinds of thing. [`examples/games/imperative/scene.plum`](examples/games/imperative/scene.plum) and [`examples/games/imperative/game_scene.plum`](examples/games/imperative/game_scene.plum) are retained mode on the `Actor` base struct: a tree with transforms, z-order, culling, hit-testing and per-kind `act`/`draw`, which suits UI, layered worlds and unit crowds. `game_scene.plum` is the same Orb Run as `game.plum` rebuilt on the scene graph, so you can compare the two styles on identical rules.
1184
-
1185
- ### Pure scene graph
1186
-
1187
- The pure scene graph is the same kind of tree as `std/game/imperative/Scene`, but a function never changes a value it was only given: updates take a node, change it in place when it is proved unique (see [In-place updates](#in-place-updates)) and hand it back, so no `clone()` is needed. Every node is a `Node` struct (a `Transform`, an id, colour, visibility, its running `actions` and a `Kind`: `Group`, `Sprite`, `Label`, `Button` or `Custom(actor)`); game behaviour is an `Actor` struct in a `Custom` node. The graph is three modules: `std/game/pure/Actions` (timed `Action`s such as `Action.forever(Action.rotateBy(..))`), `std/game/pure/PureScene` (`Node` and its methods: `Node.group`/`sprite`/`label`/`button`/`custom`, `update`, `drawTree`, `findHit`, `findPath`, `modify` and `route`) and `std/game/pure/SceneInput` (`Stage`, pointer capture, `Input.poll`). A game imports them by name:
1188
-
1189
- ```
1190
- import std/game/pure/Actions
1191
- import std/game/pure/PureScene
1192
- import std/game/pure/SceneInput
1193
- ```
1194
-
1195
- A minimal game holds a `Stage` and replaces it each frame:
1196
-
1197
- ```
1198
- struct World(mut stage: Stage, mut wasDown: Bool)
1199
-
1200
- export fun init(io: Io) -> World =
1201
- spr := Node.sprite(2, 0, 100.0, 100.0, 48.0, 48.0)
1202
- spinning := Node(..spr, actions: [Action.forever(Action.rotateBy(1.0, 1.0))].toSlice())
1203
- root := Node.group(1, 0.0, 0.0, 800.0, 600.0, [spinning].toSlice())
1204
- World(stage: Stage.create(root), wasDown: false)
1205
-
1206
- export fun update(io: Io, w: World, dt: Float) =
1207
- inputs := Input.poll(io, w.wasDown)
1208
- w.wasDown = io.isMouseDown(0)
1209
- w.stage = w.stage.frame(dt, inputs).intoStage()
1210
-
1211
- export fun draw(io: Io, w: World) =
1212
- w.stage.draw(io)
1213
- ```
1214
-
1215
- - **Immediate mode** ([`game.plum`](examples/games/imperative/game.plum)): one state struct and a `draw` that issues the calls every frame. Use it for flat arcade games with a few kinds of thing.
1216
- - **Scene** ([`game_scene.plum`](examples/games/imperative/game_scene.plum)): a retained tree of actors with `mut` fields. Use it when you want objects that change in place and a familiar `act`/`draw` object model.
1217
- - **Pure scene** ([`game_pure_scene.plum`](examples/games/pure/game_pure_scene.plum)): every update takes an actor and hands back an updated one (written in place when uniqueness proves it), pointer clicks arrive as `UiEvent`s, and the rules can be tested without a host. Use it when you want pure updates and tests that run without a game host.
1218
- - **State/view** ([`game_state_view.plum`](examples/games/functional/game_state_view.plum)): the game state is plain data and a `view` turns it into a `SceneNode` (`std/game/functional/FunctionalScene`), a recursive data type whose methods draw, measure and hit-test it. Use it when you want to draw from state instead of keeping a tree of actors.
1219
-
1220
- ### Functional scenes (state/view)
1221
-
1222
- Three scene styles ship, each in its own folder: `std/game/imperative/Scene` (actors with `mut` fields, in `examples/games/imperative`), `std/game/pure/*` (pure-update actors, in `examples/games/pure`) and `std/game/functional/*` (plain state plus a `view`, in `examples/games/functional`); the shared assets and generators are in `examples/games/assets` and `examples/games/tools`. The functional style is a loop of `State -> view -> draw`: the game state is plain data, `view(state)` builds a `SceneNode` (`std/game/functional/FunctionalScene`) and `draw` walks it. Nothing is retained between frames.
1223
-
1224
- The functional game is written in a component style: each kind of thing is a struct with an `update(self, ..)` that returns the next value and a `render(self, ..)` that returns its `SceneNode` (`Ship.update`/`Ship.render`, `Orb`, `Mine`, `Star`), and `State.render` composes them. `update` never changes a value it was given.
1225
-
1226
- - **`Anim`** describes motion as a function of time: `MoveBy`, `RotateBy`, `ScaleTo`, `Delay`, `Seq`, `Par` and `Forever`. `Anim.at(anim, t)` returns an `AnimPose` (dx, dy, rotation, scaleX, scaleY) for any `t`, so a view just applies the pose for `state.clock`.
1227
- - **`Ui`** is pure pointer state. `view.hitId(x, y)` finds the `Clickable(id, msg, child)` node under the pointer; `Ui.step(ui, input, hit)` returns a `UiStep` with the new `UiState` and the `UiEvent`s raised (`Press`, `Release`, `Click`, `Enter`, `Exit`, with pointer capture), and `Ui.messages(view, events)` turns the clicks into the game's own messages. `Ui.button(id, msg, label, x, y, w, h, ui)` builds a button that shows hover and pressed looks and sends `msg` when clicked. The picture is a `SceneNode[Msg]`, generic over the game's message type.
1228
-
1229
- From [`game_state_view.plum`](examples/games/functional/game_state_view.plum) (a method of `State`): `feed` runs `Ui.step` against the picture the player sees, then hands each event to `update`.
1230
-
1231
- ```
1232
- fun feed(self, input: Input) -> State =
1233
- pos := input.position()?
1234
- view := self.render()
1235
- r := Ui.step(self.ui, input, view.hitId(pos.x, pos.y))
1236
- Ui.messages(view, r.events).fold(State(..self, ui: r.ui), |s, m| s.update(m))
1237
- ```
1238
-
1239
- The button's id only identifies it for hover and press state (a boxed struct's `==` is identity today, structural `==` is a deferred item); what a click means is its message.
1240
-
1241
- **Browser check:** `cargo run --manifest-path compiler/game/web-test/Cargo.toml -- examples/games/imperative/game.plum --screenshot /tmp/orb-run.png` builds the static web version, serves it and drives headless Chromium (software WebGPU; `--browser webkit` drives Playwright's WebKit, the Safari engine, instead) through [playwright-rs](https://crates.io/crates/playwright-rs): it fails if the canvas stays blank or stops changing, or the page logs an error, and reports whether audio started. It also takes `--assets DIR`, `--require-audio`, `--measure SECS` (with `--json`, `draws` is the GPU draw calls per frame), `--headed`, `--dpr N` (a device pixel ratio of N), `--lose-device` (destroys the GPU device after a few frames and requires rendering to resume) and `--expect-pixel x,y,r,g,b[,tol]` (repeatable; the screenshot pixel at CSS (x, y)), `--expect-unsupported-webgpu` (hides `navigator.gpu` and requires the page's "WebGPU is not supported" message while plum.wasm still loads) and `--no-module-introspection` (makes `WebAssembly.Module.imports/exports/customSections` throw, as Safari does). `compiler/game/web-test/fixtures/` has `interleave.plum`, `text_sizes.plum` and `rendertarget.plum` for these. One-time setup: `cargo run --manifest-path compiler/game/web-test/Cargo.toml --example install-browsers`. See [`compiler/game/web-test/README.md`](compiler/game/web-test/README.md); `docs/game.png` above was made with `--screenshot`. [`compiler/game/web-test/BENCHMARK.md`](compiler/game/web-test/BENCHMARK.md) records the renderer benchmark that led to this design.
1242
-
1243
- **Asset root:** textures and files are read relative to the asset root. A path that escapes the root (`..` or absolute) fails like a missing file: `-1`, `""` or `false`. The headless runner's root is `--assets` or `assets` beside the wasm; it reads image headers for `textureWidth`/`textureHeight` and structs sounds without decoding them.
1244
-
1245
- **Sounds:** the browser decodes every `.wav` and `.ogg` file of the asset root before `init`, and `soundLoad` looks the decoded sound up by path with the same path rules as `textureLoad`. Browsers only start audio after a user gesture (a key press or click); sounds played before that are silent, and the music of the example begins once you press a key.
1246
-
1247
- **Host API** (methods of `Io`, called `io.drawRect(..)` etc.; `Key`, `Mouse` and `Blend` are in `std/game/Game`; texture and sound handles are `-1` on failure; coordinates are world units once a camera is set):
1248
-
1249
- | function | meaning |
1250
- |---|---|
1251
- | `screenWidth()`, `screenHeight()`, `frameTime()` | canvas size in CSS pixels (the unit of game coordinates, independent of the device pixel ratio), last frame time in seconds |
1252
- | `clear(c)` | fill the screen |
1253
- | `drawRect(x, y, w, h, c)`, `drawCircle(x, y, r, c)`, `drawLine(x1, y1, x2, y2, thickness, c)`, `drawText(s, x, y, size, c)` | shapes and text (`x, y` is the text's top-left, a circle's centre) |
1254
- | `setCamera(x, y, scale)`, `resetCamera()` | world point `(x, y)` maps to the screen's top-left, one world unit is `scale` pixels |
1255
- | `textureLoad(path)` | load an image (PNG at least) relative to the asset root |
1256
- | `textureWidth(t)`, `textureHeight(t)` | size in pixels (0 for a bad handle) |
1257
- | `textureCreate(w, h)` | a blank, transparent texture to draw into (`-1` if a side is below 1 or above 8192) |
1258
- | `renderTarget(t)` | following draws and `clear` go into texture `t`, or to the screen for `-1`; each target has its own camera (reset to its pixels when selected, the screen's is restored on return); contents persist across frames |
1259
- | `blendMode(m)` | `Blend.Normal`, `Blend.Add`, `Blend.Multiply` or `Blend.Screen` for the following draws; every frame starts with Normal |
1260
- | `textureSetSmooth(t, smooth)` | linear (`true`) or nearest (`false`, the default) filtering |
1261
- | `drawTexture(t, x, y, tint)` | the texture at its natural size, top-left at `(x, y)` |
1262
- | `drawTextureEx(t, sx, sy, sw, sh, x, y, w, h, rotation, flipX, tint)` | source rect `(sx, sy, sw, sh)` in texture pixels drawn into dest rect `(x, y, w, h)`, rotated `rotation` radians clockwise about the dest centre |
1263
- | `soundLoad(path)` | a handle for a WAV or OGG file relative to the asset root (`-1` on failure) |
1264
- | `soundPlay(s, volume)`, `soundLoop(s, volume)` | one-shot play (plays may overlap) and start looping; `volume` is 0..1 |
1265
- | `soundStop(s)`, `soundSetVolume(s, volume)` | stop every play of the sound; change its volume. A bad handle is a no-op |
1266
- | `fileExists(path)`, `fileLoad(path)` | whole file as a `Str` (`""` if missing), relative to the asset root |
1267
- | `isKeyDown(k)`, `isKeyPressed(k)`, `isMouseDown(b)`, `isMousePressed(b)` | input; `k` is a `Key` constant, `b` a `Mouse` constant |
1268
- | `mousePos()`, `mouseWheel()` | pointer in CSS pixels; wheel direction this frame (+ is up) |
1269
-
1270
- `Key` constants use GLFW numbering: `Key.Space`, `Key.KeyA` to `Key.KeyZ`, `Key.Num0` to `Key.Num9`, `Key.Escape`, `Key.Enter`, `Key.Tab`, `Key.Backspace`, `Key.Right`, `Key.Left`, `Key.Down`, `Key.Up`, `Key.LeftShift`, `Key.LeftControl`. `Mouse` has `Mouse.Left`, `Mouse.Right` and `Mouse.Middle`; `Blend` has `Blend.Normal`, `Blend.Add`, `Blend.Multiply` and `Blend.Screen`. `Color` holds RGBA channels in 0..1 with the constants `White`, `Black`, `Red`, `Green`, `Blue`, `Yellow` and `Transparent`, plus `Color.rgb(r, g, b)` and `withAlpha(a)`.
1271
-
1272
- ## Standard library highlights
1273
-
1274
- - **`Bool`** — `true`/`false`, `if`, `&&`/`||`/`!` and `==` are built in (no import); `toStr` (also used by `"{b}"` interpolation) and `Bool.parse("true")` need `import std/Bool`
1275
- - **`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`, ... (constants and `parse`: see [Constants](#constants-statics-and-primitive))
1276
- - **`Math`** (`import std/Math`) — a `primitive` namespace of floating-point functions: `Math.sqrt`, `Math.sin`, `Math.exp`, `Math.ln`, ...
1277
- - **`Str`** (`import std/Str`) — an immutable view of a packed byte array, with a codepoint-aware layer for Unicode correctness (`runeLength`, `runeAt`, `codePointAt`, `Str.codePointToStr(cp)`). Substrings (`sub`, `slice`, `trim*`, `split` parts) share their source's bytes instead of copying; `s.clone()` detaches one
1278
- - **`Byte`** (`import std/Byte`) — a scalar unsigned 8-bit value; `Array[Byte]` stores one byte per element
1279
- - **`Buffer`** (`import std/Buffer`) — a growable, mutable byte sequence for building up a `Str`, amortized O(n) writes
1280
- - **`Array[T]`** (`import std/Array`) — the compiler-intrinsic fixed-length array that `Slice`, `Map` and `Str` are written on, with `Array.copy` and `Array.equal` for scalar ranges; `Grid[T]` is a flat 2D array
1281
- - **`Slice[T]`** (`import std/Slice`) — a growable list built on `Array`, with `get`/`add`/`set`/`removeAt`/`remove`/`sort`/`join`/`chunk` (iteration and transformation via `for`). `xs.slice(i, j)` returns a view: a `Slice` over the same storage, with no copy. `set` on a view changes the original and a `push` on a view moves it to its own storage first; reordering the original in place (`removeAt`, `reverse`) is visible through its views, so use `sublist(i, j)` for a copy
1282
- - **`Map[K, V]`** (`import std/Map`) — `["a" => 1, "b" => 2]` builds a `Map[Str, Int]` (it is `Map.empty().with("a", 1).with("b", 2)`); `m: Map[K, V] := Map.empty()` is empty. A key can be any expression (`[n + 1 => x]`). A later duplicate key wins, as with `set`. See [`examples/maps.plum`](examples/maps.plum)
1283
- - **`Set[T]`**, **`Deque[T]`**, **`Heap[T]`** — a set over `Map`'s bucketed hash table, a ring-buffer double-ended queue and a key-ordered min-heap
1284
- - **`Json`**, **`Base64`**, **`Uuid`**, **`Time`**, **`Random`** — parsing, encoding, ids, wall-clock time and a seeded deterministic generator value (`Random.seeded(n)`; the wall-clock seed comes from `io.randomSeed()`)
1285
- - **`Os`**, **`Http`** — printing and the file system (`Os.readFile`, `Os.writeFile`, ...); HTTP requests through the host (`Http.get`, `Http.post`, ...)
1286
- - **`Testing`** (`import std/Testing`) — assertion helpers for `test` blocks
1287
-
1288
- See [`examples/`](examples) for a runnable file per feature above, and [`std/`](std) for the standard library source.
1289
-
1290
- ### JSON
1291
-
1292
- `import std/Json` adds `Json.encode(x)` and `Json.decode(s)`. A type encodes when it has `toJson(self) -> Json` and decodes when it has a static `fromJson(j: Json) -> Result[T, JsonError]`. `Int`, `Float`, `Bool`, `Str`, `Slice[T]` and `Map[K, V]` get both methods from `std/Json`, so they exist only in programs that import it. `Int.fromJson` rejects fractions and magnitudes of 2^53 or more. `decode` takes `T` from the annotation, and `JsonError` is `InvalidJson | WrongJsonType | MissingField`.
1293
-
1294
- ```plum
1295
- struct Point(x: Int, y: Int) =
1296
- fun toJson(self) -> Json =
1297
- Json.object(["x" => self.x.toJson(), "y" => self.y.toJson()])
1298
-
1299
- fun fromJson(j: Json) -> Result[Point, JsonError] =
1300
- x := Int.fromJson(j.field("x")?)?
1301
- y := Int.fromJson(j.field("y")?)?
1302
- return Ok(Point(x: x, y: y))
1303
- ```
1304
-
1305
- Using it:
1306
-
1307
- ```plum
1308
- text := Json.encode(Point(x: 1, y: 2))
1309
- p: Result[Point, JsonError] := Json.decode(text)
1310
- ```
1311
-
1312
- Also available: `Json.decodeWith(s, fn)`, `j.field(name)`, `j.optField(name)`, `Json.object(...)`, `Json.array(...)` and `j.into()` (which is `T.fromJson(j)`). Limits:
1313
-
1314
- - There are no explicit call-site type arguments: `Json.decode[House](s)` does not parse, so annotate the result instead.
1315
- - `Option[T]` has no built-in `toJson`/`fromJson` yet; write them by hand.
1316
- - `Slice[Int].fromJson(j)` does not parse; use `j.into()` or `Json.decode`.
1317
- - There is no derive: structs write `toJson`/`fromJson` by hand.
1318
-
1319
- See [`examples/json.plum`](examples/json.plum).
1320
-
1321
- ## Planned performance work
1322
-
1323
- These are compiler optimizations, not language changes; none of them changes what a program means or how it is written.
1324
-
1325
- - **No allocation for local lists and views.** A `Slice`, a `Slice.slice` view or a `Str` substring that never leaves its function (isn't stored, passed on or returned) could keep its fields in locals instead of allocating its small struct (escape analysis).
1326
- - **Loops read a list once.** `for x := range xs` could read `xs`'s storage, start and size once before the loop instead of on every step, and `xs.get(i)` loops could do the same when the body can't change `xs`.
1327
- - **Inlined `get`, `set` and `push`.** These small `Slice` and `Array` methods are function calls today. Inlining them removes the call and lets the wasm engine keep values in registers.
1328
- - **No placeholder `self` for statics.** A static such as `Vec2.zero()` still receives an unused `self` slot (one per field for a struct), which could be dropped from its signature.
1329
- - **Fewer boxed structs.** A flat struct stored in a boxed struct field, or carried in an error union is boxed (one allocation); it could be stored inline instead.
1330
- - **Host string reads.** When the host prints or writes a `Str`, it copies the whole backing array and then takes the `[start, start + len)` part; it could copy only that range.
1331
-
1332
- ## Editor support
1333
-
1334
- ```sh
1335
- plum editor helix # tree-sitter grammar, queries and plum-lsp for Helix
1336
- plum editor vscode # the VSCode extension (add --insiders for VSCode Insiders)
1337
- ```
1338
-
1339
- 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 [`compiler/tooling/`](compiler/tooling); everything is embedded in the `plum` binary, so the commands work from any directory. Reload the editor window afterwards.
94
+ ![the example game](docs/game.png)
compiler/lsp/src/main.rs CHANGED
@@ -1085,13 +1085,22 @@ mod tests {
1085
1085
  assert!(n > 10, "found only {n} examples");
1086
1086
  }
1087
1087
 
1088
- /// Every ```plum block in the README parses without a syntax diagnostic
1088
+ /// Every ```plum block in the README and docs parses without a syntax diagnostic
1089
1089
  /// (statement fragments are wrapped in a function first).
1090
1090
  #[test]
1091
- fn readmeSnippetsHaveNoSyntaxDiagnostics() {
1091
+ fn docSnippetsHaveNoSyntaxDiagnostics() {
1092
+ let files = [
1093
+ "README.md",
1094
+ "docs/language.md",
1095
+ "docs/std.md",
1096
+ "docs/game.md",
1097
+ "docs/tools.md",
1092
- let readme = std::fs::read_to_string(repoRoot().join("README.md")).unwrap();
1098
+ "docs/performance.md",
1099
+ ];
1093
1100
  let mut n = 0;
1101
+ for file in files {
1102
+ let text = std::fs::read_to_string(repoRoot().join(file)).unwrap();
1094
- let mut rest = readme.as_str();
1103
+ let mut rest = text.as_str();
1095
1104
  while let Some(i) = rest.find("```plum\n") {
1096
1105
  let body = &rest[i + 8..];
1097
1106
  let end = body.find("```").unwrap();
@@ -1137,7 +1146,7 @@ mod tests {
1137
1146
  let (items, stmts) = block.split_at(block.find("\nintNum").unwrap() + 1);
1138
1147
  assert!(
1139
1148
  Backend::syntaxDiagnostics(&Backend::parseTree(items), items).is_empty(),
1140
- "items:\n{items}"
1149
+ "{file} items:\n{items}"
1141
1150
  );
1142
1151
  let wrapped = format!(
1143
1152
  "fun frag() =\n{}",
@@ -1148,14 +1157,15 @@ mod tests {
1148
1157
  );
1149
1158
  assert!(
1150
1159
  Backend::syntaxDiagnostics(&Backend::parseTree(&wrapped), &wrapped).is_empty(),
1151
- "stmts:\n{stmts}"
1160
+ "{file} stmts:\n{stmts}"
1152
1161
  );
1153
1162
  } else {
1154
- assert!(d.is_empty(), "README snippet has syntax errors:\n{block}");
1163
+ assert!(d.is_empty(), "{file} snippet has syntax errors:\n{block}");
1155
1164
  }
1156
1165
  n += 1;
1157
1166
  }
1167
+ }
1158
- assert!(n > 20, "found only {n} README snippets");
1168
+ assert!(n > 20, "found only {n} doc snippets");
1159
1169
  }
1160
1170
 
1161
1171
  /// A type error is published at the position its `Diagnostic` reports (1-based there,
docs/game.md ADDED
@@ -0,0 +1,93 @@
1
+ # The game engine
2
+
3
+ Write a game in Plum and run it in the browser on Plum's own WebGPU renderer. It needs a browser with WebGPU: Chrome, Edge, Firefox or Safari 26+.
4
+
5
+ ![the example game](game.png)
6
+
7
+ ## Try it
8
+
9
+ ```sh
10
+ plum game examples/games/imperative/game.plum
11
+ ```
12
+
13
+ This builds the game, starts a local server and opens your browser. Steer with the arrow keys or WASD, collect the orbs and dodge the mines. Press a key first, because browsers only start sound after you interact with the page.
14
+
15
+ ## A minimal game
16
+
17
+ A game exports three functions. Your state goes in a struct of your choice, as there are no globals.
18
+
19
+ ```plum
20
+ import std/Int
21
+ import std/Float
22
+ import std/Str
23
+ import std/Io
24
+ import std/game/Game
25
+ import std/game/Vec2
26
+ import std/game/Color
27
+
28
+ struct World(mut x: Float)
29
+
30
+ export fun init(io: Io) -> World =
31
+ World(x: 0.0)
32
+
33
+ export fun update(io: Io, w: World, dt: Float) =
34
+ if io.isKeyDown(Key.Right)
35
+ w.x = w.x + 100.0 * dt
36
+
37
+ export fun draw(io: Io, w: World) =
38
+ io.clear(Color.Black)
39
+ io.drawRect(w.x, 100.0, 40.0, 40.0, Color.Red)
40
+ ```
41
+
42
+ `init` runs once after the assets load. `update` and `draw` then run every frame.
43
+
44
+ ## Drawing and input
45
+
46
+ These are methods on `io`, for example `io.drawRect(..)`.
47
+
48
+ | Group | Functions |
49
+ |---|---|
50
+ | Screen | `screenWidth()`, `screenHeight()`, `frameTime()`, `clear(color)` |
51
+ | Shapes | `drawRect(x, y, w, h, c)`, `drawCircle(x, y, r, c)`, `drawLine(x1, y1, x2, y2, thickness, c)` |
52
+ | Text | `drawText(s, x, y, size, c)` |
53
+ | Camera | `setCamera(x, y, scale)`, `resetCamera()` |
54
+ | Images | `textureLoad(path)`, `drawTexture(t, x, y, tint)`, `drawTextureEx(t, sx, sy, sw, sh, x, y, w, h, rotation, flipX, tint)`, `textureWidth(t)`, `textureHeight(t)`, `textureSetSmooth(t, smooth)` |
55
+ | Draw to an image | `textureCreate(w, h)`, `renderTarget(t)` (`-1` goes back to the screen) |
56
+ | Blending | `blendMode(m)` with `Blend.Normal`, `Blend.Add`, `Blend.Multiply`, `Blend.Screen` |
57
+ | Sound | `soundLoad(path)`, `soundPlay(s, volume)`, `soundLoop(s, volume)`, `soundStop(s)`, `soundSetVolume(s, volume)` |
58
+ | Keyboard | `isKeyDown(k)`, `isKeyPressed(k)` with `Key.Space`, `Key.KeyA`, `Key.Up`, `Key.Enter`, ... |
59
+ | Mouse | `mousePos()`, `mouseWheel()`, `isMouseDown(b)`, `isMousePressed(b)` with `Mouse.Left`, `Mouse.Right`, `Mouse.Middle` |
60
+ | Files | `fileExists(path)`, `fileLoad(path)` |
61
+
62
+ Colors come from `Color.White`, `Color.Red` and so on, or `Color.rgb(r, g, b)`. `textureLoad` and `soundLoad` return `-1` if the file can't be loaded.
63
+
64
+ ## Assets
65
+
66
+ Images and sounds are loaded from an `assets` folder next to your game, or from the folder you pass with `--assets`. WAV and OGG sounds are supported. Paths are relative to that folder and can't leave it.
67
+
68
+ ## Ways to structure a game
69
+
70
+ Drawing directly is enough for small games. For bigger ones, Plum includes three scene libraries. All of them are in [`std/game`](../std/game), and each has an example game.
71
+
72
+ | Style | How it works | Good for | Example |
73
+ |---|---|---|---|
74
+ | **Direct drawing** | One state struct, and `draw` makes the calls each frame | small arcade games | [`game.plum`](../examples/games/imperative/game.plum) |
75
+ | **Imperative scene** | A tree of objects that you change in place | UI, and objects with their own behaviour | [`game_scene.plum`](../examples/games/imperative/game_scene.plum) |
76
+ | **Pure scene** | A tree where each update returns the next version | rules you can test without a browser | [`game_pure_scene.plum`](../examples/games/pure/game_pure_scene.plum) |
77
+ | **State and view** | Plain data, and a `view` function draws it each frame | games that are simple to reason about | [`game_state_view.plum`](../examples/games/functional/game_state_view.plum) |
78
+
79
+ These four examples play the same kind of game, so you can compare them side by side. [Game scene styles](game-scene-styles.md) goes deeper into the differences, and [imperative vs functional](imperative-vs-functional.md) compares the code.
80
+
81
+ ## Testing a game
82
+
83
+ Put your rules in plain functions with `test` blocks, and `plum test` runs them with no browser. Calling a drawing function under `plum test` stops with an error, so keep those calls out of the code you test.
84
+
85
+ To check a game actually renders, the browser test tool runs it in headless Chromium. See [its README](../compiler/game/web-test/README.md).
86
+
87
+ ## Publishing
88
+
89
+ ```sh
90
+ plum game game.plum -o dist
91
+ ```
92
+
93
+ This writes a static site to `dist`, ready for any static file host.
docs/language.md ADDED
@@ -0,0 +1,526 @@
1
+ # The Plum language
2
+
3
+ A tour of the language, one idea at a time. Every feature also has a runnable file in [`examples/`](../examples).
4
+
5
+ - [First program](#first-program)
6
+ - [Basics](#basics)
7
+ - [Functions](#functions)
8
+ - [Control flow](#control-flow)
9
+ - [Structs](#structs)
10
+ - [Enums](#enums)
11
+ - [Pattern matching](#pattern-matching)
12
+ - [Option and Result](#option-and-result)
13
+ - [Lists, slices and maps](#lists-slices-and-maps)
14
+ - [Loops](#loops)
15
+ - [Closures](#closures)
16
+ - [Generics](#generics)
17
+ - [Methods, statics and constants](#methods-statics-and-constants)
18
+ - [Operators on your own types](#operators-on-your-own-types)
19
+ - [Inheritance](#inheritance)
20
+ - [Modules](#modules)
21
+ - [Tests](#tests)
22
+ - [Talking to the outside world](#talking-to-the-outside-world)
23
+
24
+ ## First program
25
+
26
+ ```plum
27
+ import std/Io
28
+ import std/Str
29
+
30
+ fun main(io: Io) =
31
+ io.printLn("Hello, Plum!")
32
+ ```
33
+
34
+ ```sh
35
+ plum run hello.plum
36
+ ```
37
+
38
+ ## Basics
39
+
40
+ **Blocks** use indentation (2 spaces), with no braces or semicolons. **Comments** start with `#`.
41
+
42
+ **Variables:** `:=` declares a new name and `=` changes an existing one.
43
+
44
+ ```plum
45
+ fun main() =
46
+ count := 1
47
+ count = count + 1
48
+ a, b := 1, 2
49
+ ratio: Float := 0.5 # with an explicit type
50
+ ```
51
+
52
+ **Values:**
53
+
54
+ | Type | Examples |
55
+ |---|---|
56
+ | `Int` | `42`, `0xFF`, `0b1010`, `1_000_000` |
57
+ | `Float` | `3.14`, `6.022e23` |
58
+ | `Bool` | `true`, `false` |
59
+ | `Str` | `"hello"`, `"line one\nline two"` |
60
+
61
+ **Strings** can hold any expression inside `{ }`:
62
+
63
+ ```plum
64
+ fun greet(name: Str) -> Str =
65
+ "Hello, {name}! Your name has {name.length()} letters."
66
+ ```
67
+
68
+ **Grouping** uses `{ }`, because `( )` is only for calls:
69
+
70
+ ```plum
71
+ fun total() -> Int =
72
+ {1 + 2} * 3
73
+ ```
74
+
75
+ **Names:**
76
+
77
+ | Style | Used for |
78
+ |---|---|
79
+ | `PascalCase` | types, enums, variants, constants |
80
+ | `camelCase` | functions, methods, fields |
81
+ | `snake_case` | variables, parameters |
82
+
83
+ **Unused results are an error.** This stops a call like `scores.push(3)` from silently doing nothing on a type where `push` returns a new value. To drop a result on purpose, write `_ := list.pop()!`.
84
+
85
+ ## Functions
86
+
87
+ The last expression is the return value. With no return type, a function returns `Unit`.
88
+
89
+ ```plum
90
+ fun add(a: Int, b: Int) -> Int =
91
+ a + b
92
+
93
+ fun step(a: Int, by: Int = 1) -> Int = # a default value
94
+ a + by
95
+
96
+ fun sign(n: Int) -> Int = match n # a body can be one match
97
+ 0 => 0
98
+ _ => if n < 0 then -1 else 1
99
+ ```
100
+
101
+ Calls in tail position run in constant stack, so deep recursion is safe:
102
+
103
+ ```plum
104
+ fun countUp(n: Int, acc: Int) -> Int =
105
+ if n <= 0 then acc else countUp(n - 1, acc + 1)
106
+ ```
107
+
108
+ ## Control flow
109
+
110
+ `if` comes in two forms. The block form is a statement, and the `then` form is an expression:
111
+
112
+ ```plum
113
+ fun describe(n: Int) -> Str =
114
+ if n < 0
115
+ return "negative"
116
+ else if n == 0
117
+ return "zero"
118
+ if n > 100 then "big" else "small"
119
+ ```
120
+
121
+ `while` loops as long as a condition holds, and `for` is covered under [Loops](#loops). `break` and `continue` work in both.
122
+
123
+ ```plum
124
+ fun halve(start: Int) -> Int =
125
+ i := start
126
+ while i > 1
127
+ i = i / 2
128
+ i
129
+ ```
130
+
131
+ `assert n > 0` stops the program if the condition is false. `todo` marks a body you haven't written yet, and stops the program if it is reached.
132
+
133
+ ## Structs
134
+
135
+ A struct is a set of named fields.
136
+
137
+ ```plum
138
+ struct User(name: Str, mut age: Int)
139
+
140
+ fun main() =
141
+ ann := User(name: "Ann", age: 30)
142
+ ann.age = 31 # allowed, because age is `mut`
143
+ older := User(..ann, age: 32) # a copy with one field changed
144
+ ```
145
+
146
+ Fields can't change unless they are marked `mut`. A spread, `User(..ann, age: 32)`, makes an updated value, and the compiler writes it in place when the old value isn't used again.
147
+
148
+ **Copied or shared?** A small struct made only of numbers, like `Vec2` below, is copied when assigned. Any other struct is shared: two names point at the same value.
149
+
150
+ ```plum
151
+ struct Vec2(x: Float, y: Float)
152
+
153
+ fun main() =
154
+ a := Vec2(x: 1.0, y: 2.0)
155
+ b := a
156
+ b.x = 9.0 # a.x is still 1.0
157
+ ```
158
+
159
+ **Setting up extra fields:** the header lists what callers pass. Anything else is set in `init`, which runs on construction.
160
+
161
+ ```plum
162
+ import std/Int
163
+ import std/Slice
164
+
165
+ struct Bag(size: Int) =
166
+ fun init(self) =
167
+ self.items: Slice[Int] = Slice[Int]()
168
+ self.capacity: Int = self.size * 2
169
+ ```
170
+
171
+ ## Enums
172
+
173
+ An enum is a value that is one of several variants. A variant can carry its own fields.
174
+
175
+ ```plum
176
+ enum Color = Red | Green | Blue
177
+
178
+ enum Shape =
179
+ | Circle(r: Float)
180
+ | Rect(w: Float, h: Float)
181
+
182
+ fun area(self) -> Float =
183
+ match self
184
+ Circle(r) => r * r * 3.14
185
+ Rect(w, h) => w * h
186
+
187
+ fun main() =
188
+ s := Rect(w: 2.0, h: 3.0)
189
+ a := s.area()
190
+ ```
191
+
192
+ `Shape` is the type. `Circle` and `Rect` are ways to build and match it, not types of their own, so you read a variant's fields with a pattern rather than with `.field`.
193
+
194
+ Enums can be recursive and generic:
195
+
196
+ ```plum
197
+ enum Tree[T] =
198
+ | Leaf
199
+ | Node(left: Tree[T], value: T, right: Tree[T])
200
+ ```
201
+
202
+ ## Pattern matching
203
+
204
+ `match` handles every case, and the compiler checks that none is missing. Add a variant later and each `match` that doesn't handle it becomes an error.
205
+
206
+ ```plum
207
+ enum Job =
208
+ | Idle
209
+ | Busy(task: Str)
210
+ | Done(score: Int)
211
+
212
+ fun describe(job: Job) -> Str =
213
+ match job
214
+ Idle => "resting"
215
+ Busy(task) => "working on {task}"
216
+ Done(score) when score > 90 => "great work"
217
+ Done(_) => "finished"
218
+ ```
219
+
220
+ Patterns can be literals, names that bind a value, variants with nested patterns, or `_` to ignore. A case can add a `when` condition. `match a, b` matches several values at once.
221
+
222
+ **Checking one case** is easier with `if`. The names it binds exist only inside that branch, and `, cond` adds a condition:
223
+
224
+ ```plum
225
+ enum Job =
226
+ | Idle
227
+ | Busy(task: Str, urgent: Bool)
228
+
229
+ fun urgentTask(job: Job) -> Str =
230
+ if Busy(task, urgent) := job, urgent
231
+ task
232
+ else
233
+ "none"
234
+ ```
235
+
236
+ The same form works as `while Pattern := value`.
237
+
238
+ Use `match` when every case needs handling, and `if` when only one or two do. Only `match` is checked for missing cases.
239
+
240
+ ## Option and Result
241
+
242
+ Plum has no `null` and no exceptions.
243
+
244
+ - `Option[T]` is a value that may be missing: `None` or `Some(x)`.
245
+ - `Result[T, E]` is an operation that may fail: `Ok(x)` or `Err(e)`.
246
+
247
+ Both are ordinary enums from `std/Option` and `std/Result`.
248
+
249
+ ```plum
250
+ import std/Int
251
+ import std/Str
252
+ import std/Option
253
+ import std/Result
254
+
255
+ enum AgeError =
256
+ | NotANumber
257
+ | Negative
258
+
259
+ fun parseAge(s: Str) -> Result[Int, AgeError] =
260
+ n := Int.parse(s).mapErr(|e| NotANumber)?
261
+ if n < 0
262
+ return Err(Negative)
263
+ Ok(n)
264
+
265
+ fun describe(s: Str) -> Str =
266
+ match parseAge(s)
267
+ Ok(n) => "age {n}"
268
+ Err(NotANumber) => "not a number"
269
+ Err(Negative) => "can't be negative"
270
+ ```
271
+
272
+ Each way of handling them:
273
+
274
+ | You write | It means |
275
+ |---|---|
276
+ | `x?` | Use the value, or return the `Err`/`None` from this function |
277
+ | `x!` | Use the value, or crash and report the file and line |
278
+ | `x.unwrapOr(d)` | Use the value, or the default `d` |
279
+ | `if Some(v) := x` | Run a block only when there is a value |
280
+ | `match x` | Handle each case |
281
+ | `x.map(f)`, `x.andThen(f)` | Transform a value that is present |
282
+ | `r.mapErr(f)` | Change the error, usually into your own error enum |
283
+ | `o.okOr(e)`, `r.ok()` | Convert between `Option` and `Result` |
284
+
285
+ A good habit is one error enum per module. Use `?` and `mapErr` for failures you expect. Save `!` and `crash "message"` for bugs.
286
+
287
+ ## Lists, slices and maps
288
+
289
+ | Type | Use it for | Build one |
290
+ |---|---|---|
291
+ | `List[T]` | recursion and pattern matching | `[1, 2, 3]` |
292
+ | `Slice[T]` | a growable array with fast indexing | `[1, 2, 3].toSlice()`, `Slice[Int]()` |
293
+ | `Array[T]` | a fixed-size array | `Array.filled(10, 0)` |
294
+ | `Map[K, V]` | key and value lookup | `["a" => 1, "b" => 2]` |
295
+
296
+ ```plum
297
+ import std/Int
298
+ import std/List
299
+ import std/Slice
300
+
301
+ fun main() =
302
+ big := [1, 5, 9].filter(|x| x > 2).map(|x| x * 10)
303
+
304
+ scores := Slice[Int]()
305
+ scores.push(4)
306
+ last := scores.last().unwrapOr(0)
307
+ ```
308
+
309
+ A `List` is `Nil` or `Cons(head, tail)`, so you can match on it. Indexing a `List` is slow, so reach for a `Slice` when you need it.
310
+
311
+ ## Loops
312
+
313
+ `for x := range source` is the only `for`.
314
+
315
+ ```plum
316
+ import std/Int
317
+ import std/Str
318
+ import std/List
319
+ import std/Map
320
+ import std/Slice
321
+
322
+ fun main() =
323
+ total := 0
324
+ for i := range 10 # 0, 1, ... 9
325
+ total = total + i
326
+ for i := range 2.until(10) # 2 to 9; also to, downTo, stepBy
327
+ total = total + i
328
+ for x := range [1, 2, 3] # a List, Slice or Array
329
+ total = total + x
330
+ for e := range ["a" => 1].entries() # a Map: e.first and e.second
331
+ total = total + e.second
332
+ ```
333
+
334
+ Make your own type loopable by giving it `next`. Return `None` when done, or `Some` of the value and the remaining sequence:
335
+
336
+ ```plum
337
+ import std/Int
338
+ import std/Str
339
+ import std/Option
340
+ import std/Pair
341
+
342
+ struct Countdown(n: Int) =
343
+ fun next(self) -> Option[Pair[Int, Countdown]] =
344
+ if self.n <= 0 then None else Some(Pair(first: self.n, second: Countdown(n: self.n - 1)))
345
+ ```
346
+
347
+ ## Closures
348
+
349
+ A closure is written `|params| body`. It copies the variables it uses when it is created.
350
+
351
+ ```plum
352
+ fun apply(f: fn(Int) -> Int, x: Int) -> Int =
353
+ f(x)
354
+
355
+ fun adder(n: Int) -> fn(Int) -> Int =
356
+ |x| x + n
357
+
358
+ fun main() -> Int =
359
+ offset := 100
360
+ a := apply(|v| v + offset, 5)
361
+ add1 := adder(1)
362
+ a + add1(2)
363
+ ```
364
+
365
+ Add a type when the compiler can't work one out: `|b: Box| b.get()`. Passing a closure straight to a function that just calls it costs nothing, because the compiler inlines it.
366
+
367
+ `adder(1)(2)` isn't supported yet, so bind the result to a name first.
368
+
369
+ ## Generics
370
+
371
+ Declare type parameters in `[ ]`. The name can be anything.
372
+
373
+ ```plum
374
+ struct Box[T](value: T) =
375
+ fun map[B](self, f: fn(T) -> B) -> Box[B] =
376
+ Box(value: f(self.value))
377
+
378
+ fun first[Elem](xs: List[Elem], fallback: Elem) -> Elem = match xs
379
+ Nil => fallback
380
+ Cons(h, _) => h
381
+ ```
382
+
383
+ Parameters have no constraints. If `show[T]` calls `x.toStr()`, it works for any type that has `toStr`, and any other type gets an error showing how the call got there:
384
+
385
+ ```
386
+ in outer[Plain] (called at main.plum:9:3):
387
+ in maxOf[Plain] (called at main.plum:5:3):
388
+ Plain has no method 'lt'
389
+ ```
390
+
391
+ Each type gets its own compiled copy, so a `Slice[Float]` stores plain floats with no boxing.
392
+
393
+ If the compiler can't work out a type parameter, annotate the value: `empty: List[Int] := Nil`.
394
+
395
+ ## Methods, statics and constants
396
+
397
+ Inside a type's body:
398
+
399
+ - a `fun` with `self` is a **method**, called on a value: `cat.age()`
400
+ - a `fun` without `self` is a **static**, called on the type: `Cat.named("Tom")`
401
+ - `Name = value` is a **constant**, read as `Type.Name`
402
+
403
+ ```plum
404
+ struct Vec2(x: Float, y: Float) =
405
+ Zero = Vec2(x: 0.0, y: 0.0)
406
+
407
+ fun unit() -> Vec2 =
408
+ Vec2(x: 1.0, y: 1.0)
409
+
410
+ fun sum(self) -> Float =
411
+ self.x + self.y
412
+ ```
413
+
414
+ There are no top-level constants. For a group of constants and helper functions with no data, use `primitive`:
415
+
416
+ ```plum
417
+ primitive Config =
418
+ MaxRetries = 3
419
+
420
+ fun doubled() -> Int =
421
+ Config.MaxRetries * 2
422
+ ```
423
+
424
+ ## Operators on your own types
425
+
426
+ Define the matching method and the operator works:
427
+
428
+ | Operator | Method |
429
+ |---|---|
430
+ | `a + b`, `a - b`, `a * b`, `a / b` | `add`, `sub`, `mul`, `div` |
431
+ | `-a` | `neg` |
432
+ | `a == b`, `a != b` | `eq` |
433
+ | `a < b`, `a <= b`, `a > b`, `a >= b` | `lt`, `le`, `gt`, `ge` |
434
+
435
+ ```plum
436
+ struct Vec2(x: Float, y: Float) =
437
+ fun add(self, o: Vec2) -> Vec2 =
438
+ Vec2(x: self.x + o.x, y: self.y + o.y)
439
+
440
+ fun mul(self, s: Float) -> Vec2 =
441
+ Vec2(x: self.x * s, y: self.y * s)
442
+ ```
443
+
444
+ ## Inheritance
445
+
446
+ A struct can embed one other struct by listing it first. It gets the base's fields and methods, and can be used anywhere the base is expected.
447
+
448
+ ```plum
449
+ import std/Io
450
+ import std/Int
451
+ import std/Str
452
+
453
+ struct Animal(name: Str) =
454
+ fun speak(self, io: Io) =
455
+ io.printLn("...")
456
+
457
+ struct Dog(Animal(name), tricks: Int) =
458
+ override fun speak(self, io: Io) =
459
+ io.printLn("Woof")
460
+ ```
461
+
462
+ The header names which base fields callers pass. `override fun` replaces a base method, and the right version is picked at run time. `self.Animal.speak(io)` calls the base's version.
463
+
464
+ `abstract fun` declares a method with no body. Every subtype must override it, and the base can't be constructed:
465
+
466
+ ```plum
467
+ struct Shape() =
468
+ abstract fun area(self) -> Float
469
+
470
+ struct Square(Shape, side: Float) =
471
+ override fun area(self) -> Float =
472
+ self.side * self.side
473
+ ```
474
+
475
+ ## Modules
476
+
477
+ A file starts with its imports. Nothing is implicit, not even `Int` or `Str` methods, and each file imports what it uses.
478
+
479
+ ```plum
480
+ module shapes
481
+
482
+ import std/Int
483
+ import std/Str
484
+ ```
485
+
486
+ `test` blocks go at the end of the file.
487
+
488
+ ## Tests
489
+
490
+ Tests live next to the code and run with `plum test`:
491
+
492
+ ```plum
493
+ fun add(a: Int, b: Int) -> Int =
494
+ a + b
495
+
496
+ test "add works"
497
+ assert add(1, 2) == 3
498
+ ```
499
+
500
+ ```sh
501
+ $ plum test math.plum
502
+ └─ add works ✔
503
+
504
+ 1 passed, 0 failed
505
+ ```
506
+
507
+ A failing `assert` on `==` prints both values. `plum test` accepts files and folders, and `std/Testing` adds helpers like `Testing.expectEq`.
508
+
509
+ ## Talking to the outside world
510
+
511
+ Everything that leaves your program goes through an `Io` value: printing, files, the clock, the network, drawing. You can't make an `Io` yourself. It is given to `main`, and you pass it to whatever needs it.
512
+
513
+ ```plum
514
+ import std/Io
515
+ import std/Str
516
+ import std/Os
517
+
518
+ fun main(io: Io) =
519
+ match Os.readFile(io, "notes.txt")
520
+ Ok(text) => io.printLn(text)
521
+ Err(e) => io.printLn("could not read the file")
522
+ ```
523
+
524
+ This means a function that doesn't receive an `Io` can't have outside effects, which makes it easy to test and lets the compiler optimise it.
525
+
526
+ `export fun` makes a top-level function callable from the host, such as a browser. See [the game engine](game.md) for how a game uses this.
docs/performance.md ADDED
@@ -0,0 +1,51 @@
1
+ # Performance
2
+
3
+ You can write clear code and let the compiler make it fast. These optimisations never change what your program does: if a rewritten program doesn't type-check, the compiler keeps the original.
4
+
5
+ ## What the compiler does
6
+
7
+ **Removes temporary lists.** This builds no intermediate lists, and runs as one loop:
8
+
9
+ ```plum
10
+ import std/Int
11
+ import std/List
12
+
13
+ fun total(xs: List[Int]) -> Int =
14
+ xs.map(|x| x * 2).filter(|x| x > 2).fold(0, |a, x| a + x)
15
+ ```
16
+
17
+ **Updates in place.** If a value isn't used again, a spread writes into it instead of copying:
18
+
19
+ ```plum
20
+ struct Cell(x: Float)
21
+
22
+ fun bump(s: Cell) -> Cell =
23
+ Cell(..s, x: s.x + 1.0)
24
+ ```
25
+
26
+ **Reuses memory.** Rebuilding a value of the same shape can reuse the old one.
27
+
28
+ **Inlines closures.** A closure passed straight to a function that only calls it becomes a direct copy of the code, with no allocation.
29
+
30
+ **Skips the picture tree.** In a game, `view(state).draw(io)` draws without building the scene in memory.
31
+
32
+ **Runs tail calls as loops.** Recursion in tail position uses no extra stack.
33
+
34
+ ## When it can't help
35
+
36
+ The first three need pure code, which means no `Io`, and a value that has one use. If a value is used twice, stored in a field or captured by a closure, it stays as written.
37
+
38
+ To see each decision, or turn the optimisations off:
39
+
40
+ ```sh
41
+ plum run --explain-fusion file.plum
42
+ plum run --no-fusion file.plum
43
+ ```
44
+
45
+ ## Planned
46
+
47
+ - Keep lists and substrings that stay inside one function out of the heap
48
+ - Read a list's storage once per loop, rather than on every step
49
+ - Inline `get`, `set` and `push`
50
+ - Store small structs inside other structs instead of separately
51
+ - Copy only the needed part of a string when printing it
docs/std.md ADDED
@@ -0,0 +1,166 @@
1
+ # The standard library
2
+
3
+ Import what you use: `import std/Str`, `import std/Slice`, and so on. The source is in [`std/`](../std), and it is plain Plum, so it's a good place to read how things work.
4
+
5
+ The [game engine](game.md) has its own page.
6
+
7
+ | Module | What it's for |
8
+ |---|---|
9
+ | [`Int`, `Float`, `Bool`, `Byte`](#numbers) | numbers and booleans |
10
+ | [`Math`](#numbers) | `sqrt`, `sin`, `exp` and friends |
11
+ | [`Str`, `Buffer`](#text) | text |
12
+ | [`List`, `Slice`, `Array`, `Grid`](#sequences) | sequences |
13
+ | [`Map`, `Set`, `Deque`, `Heap`](#collections) | other collections |
14
+ | [`Option`, `Result`](#option-and-result) | missing values and failures |
15
+ | [`Json`](#json) | reading and writing JSON |
16
+ | [`Os`, `Http`](#files-and-network) | files and the network |
17
+ | [`Time`, `Random`, `Uuid`, `Base64`](#utilities) | utilities |
18
+ | [`Testing`](#testing) | helpers for tests |
19
+
20
+ ## Numbers
21
+
22
+ ```plum
23
+ import std/Int
24
+ import std/Float
25
+ import std/Str
26
+ import std/Math
27
+
28
+ fun main() =
29
+ a := 5.max(3)
30
+ b := Float.Pi * 2.0
31
+ c := Math.sqrt(2.0)
32
+ d := Int.parse("42").unwrapOr(0)
33
+ ```
34
+
35
+ - `Int`: `abs`, `sign`, `min`, `max`, `pow`, `toStr`, `Int.parse`, `Int.Min`, `Int.Max`
36
+ - `Float`: `abs`, `sqrt`, `pow`, `floor`, `ceil`, `round`, `trunc`, `isNaN`, `toStr`, `Float.parse`, `Float.Pi`, `Float.Tau`, `Float.E`
37
+ - `Math`: `sqrt`, `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `atan2`, `exp`, `ln`, `hypot`, `cbrt`
38
+ - `Bool`: `toStr`, `Bool.parse`
39
+
40
+ ## Text
41
+
42
+ A `Str` can't be changed. Taking part of one shares the original's memory rather than copying it.
43
+
44
+ ```plum
45
+ import std/Int
46
+ import std/Str
47
+
48
+ fun main() =
49
+ name := " plum ".trim()
50
+ shout := name.upperCase()
51
+ twice := name.repeat(2)
52
+ yes := name.startsWith("pl")
53
+ ```
54
+
55
+ - **Search:** `contains`, `startsWith`, `endsWith`, `indexOf`
56
+ - **Cut:** `sub`, `slice`, `split(separator, limit)`, `trim`, `trimStart`, `trimEnd`, `truncate`
57
+ - **Change:** `replace`, `replaceAll`, `repeat`, `reverse`, `padStart`, `padEnd`
58
+ - **Case:** `upperCase`, `lowerCase`, `capitalize`, `camelCase`, `snakeCase`, `kebabCase`
59
+ - **Unicode:** `length` counts bytes; `runeLength` and `runeAt` count characters
60
+
61
+ To build a string in steps, use `Buffer`: `write`, `writeByte`, `toStr`.
62
+
63
+ ## Sequences
64
+
65
+ **`List`** suits recursion: `map`, `filter`, `fold`, `append`, `reverse`, `length`, `isEmpty`, `get`, `toSlice`.
66
+
67
+ **`Slice`** is a growable array:
68
+
69
+ ```plum
70
+ import std/Int
71
+ import std/List
72
+ import std/Slice
73
+
74
+ fun main() =
75
+ xs := [3, 1, 2].toSlice()
76
+ xs.push(4)
77
+ first := xs.get(0).unwrapOr(0) # Some(value), or None when out of range
78
+ second := xs.at(1) # the value, or crash when out of range
79
+ ```
80
+
81
+ - **Read:** `get`, `at`, `first`, `last`, `head`, `length`, `contains`, `findValue`
82
+ - **Change:** `push`, `pop`, `set`, `add`, `remove`, `removeAt`, `clear`, `sort`, `reverse`, `shuffle`
83
+ - **Other:** `slice` (a view that shares storage), `sublist` (a copy), `chunk`, `join`, `drop`
84
+
85
+ **`Array`** has a fixed size: `Array.filled(n, value)`, `get`, `set`, `length`. **`Grid`** is a 2D array: `Grid.filled(w, h, value)`, `get(x, y)`, `set(x, y, value)`.
86
+
87
+ ## Collections
88
+
89
+ ```plum
90
+ import std/Int
91
+ import std/Str
92
+ import std/Map
93
+
94
+ fun main() =
95
+ ages := ["ann" => 30, "bob" => 25]
96
+ ages.set("cy", 40)
97
+ ann := ages.get("ann").unwrapOr(0)
98
+ ```
99
+
100
+ - **`Map`:** `get`, `set`, `has`, `remove`, `keys`, `values`, `entries`, `length`. `Map.empty()` makes an empty one, but needs a type: `m: Map[Str, Int] := Map.empty()`
101
+ - **`Set`:** `add`, `has`, `remove`, `length`, `toList`
102
+ - **`Deque`:** a queue you can use from both ends: `pushBack`, `pushFront`, `popBack`, `popFront`
103
+ - **`Heap`:** always gives back the smallest item first: `push`, `pop`, `peek`
104
+
105
+ ## Option and Result
106
+
107
+ See [the language guide](language.md#option-and-result).
108
+
109
+ - **`Option`:** `isSome`, `isNone`, `unwrap`, `unwrapOr`, `map`, `andThen`, `okOr`
110
+ - **`Result`:** `isOk`, `isErr`, `unwrap`, `unwrapOr`, `map`, `andThen`, `mapErr`, `ok`
111
+
112
+ ## JSON
113
+
114
+ `Json.encode` and `Json.decode` convert your types. Numbers, `Bool`, `Str`, `Slice` and `Map` work already, and your own types need `toJson` and `fromJson`:
115
+
116
+ ```plum
117
+ import std/Int
118
+ import std/Str
119
+ import std/Map
120
+ import std/Json
121
+
122
+ struct Point(x: Int, y: Int) =
123
+ fun toJson(self) -> Json =
124
+ Json.object(["x" => self.x.toJson(), "y" => self.y.toJson()])
125
+
126
+ fun fromJson(j: Json) -> Result[Point, JsonError] =
127
+ x := Int.fromJson(j.field("x")?)?
128
+ y := Int.fromJson(j.field("y")?)?
129
+ Ok(Point(x: x, y: y))
130
+
131
+ fun main() =
132
+ text := Json.encode(Point(x: 1, y: 2))
133
+ p: Result[Point, JsonError] := Json.decode(text)
134
+ ```
135
+
136
+ The result's annotation tells `decode` which type to build. `Option` doesn't have JSON support yet, so write those conversions by hand.
137
+
138
+ ## Files and network
139
+
140
+ All of these need an `Io` and return a `Result`.
141
+
142
+ ```plum
143
+ import std/Io
144
+ import std/Str
145
+ import std/Os
146
+ import std/Http
147
+
148
+ fun main(io: Io) =
149
+ _ := Os.writeFile(io, "note.txt", "hello")
150
+ text := Os.readFile(io, "note.txt").unwrapOr("")
151
+ io.printLn(text)
152
+ ```
153
+
154
+ - **`Os`:** `readFile`, `writeFile`, `appendFile`, `exists`, `mkdir`, `remove`, `rename`, `copyFile`, `readdir`
155
+ - **`Http`:** `get`, `post`, `put`, `patch`, `delete`, `head`, `postJson`. A `Response` has `status`, `headers`, `body`, `ok()` and `json()`
156
+
157
+ ## Utilities
158
+
159
+ - **`Time`:** `Time.now(io)` and `Duration` values such as `Duration.seconds(5)`
160
+ - **`Random`:** `Random.seeded(n)`. Each call returns the value and the next generator, so the same seed always gives the same sequence. `io.randomSeed()` gives a seed from the clock
161
+ - **`Uuid`:** `Uuid.v4(rng)`, `Uuid.fromStr`, `Uuid.isValid`
162
+ - **`Base64`:** `Base64.encode`, `Base64.decode`, `Base64.encodeURL`
163
+
164
+ ## Testing
165
+
166
+ `assert` covers most tests. `std/Testing` adds `Testing.expectEq`, `expectNotEq`, `expectTrue`, `expectFalse`, `expectSuccess` and `expectFailure`.
docs/tools.md ADDED
@@ -0,0 +1,52 @@
1
+ # Tools
2
+
3
+ ## Commands
4
+
5
+ Run these from a checkout of this repository, or pass `--lib-path`, so `import std/...` can find the standard library.
6
+
7
+ | Command | What it does |
8
+ |---|---|
9
+ | `plum run file.plum` | Compile and run |
10
+ | `plum test file.plum dir/` | Run the `test` blocks in files and folders |
11
+ | `plum compile file.plum` | Write `file.wasm` |
12
+ | `plum build file.plum` | Make a native executable |
13
+ | `plum format file.plum` | Format the file in place (`--check` only checks, `--stdin` reads standard input) |
14
+ | `plum game file.plum` | Serve a game and open it in the browser |
15
+ | `plum editor helix` | Set up Helix |
16
+ | `plum editor vscode` | Set up VSCode (`--insiders` for VSCode Insiders) |
17
+
18
+ **Options for `plum game`:**
19
+
20
+ | Option | What it does |
21
+ |---|---|
22
+ | `--assets DIR` | Load assets from `DIR` |
23
+ | `--port N` | Use port `N` (default 8080, or any free port) |
24
+ | `--no-open` | Don't open the browser |
25
+ | `-o DIR` | Write a static site to `DIR` and exit |
26
+
27
+ **Options for looking at the compiler's work:**
28
+
29
+ | Option | What it does |
30
+ |---|---|
31
+ | `--no-fusion` | Turn off the optimisations described in [Performance](performance.md) |
32
+ | `--explain-fusion` | Print each optimisation decision and how each struct is stored |
33
+
34
+ ## Editors
35
+
36
+ `plum editor helix` and `plum editor vscode` install syntax highlighting and the language server, which gives you errors as you type and completion. Everything is built into the `plum` binary, so they work from any folder. Reload your editor afterwards.
37
+
38
+ ## Building from source
39
+
40
+ You need a recent stable Rust toolchain and a C compiler.
41
+
42
+ ```sh
43
+ cargo install --path compiler/cli
44
+ ```
45
+
46
+ This installs `plum` to `~/.cargo/bin`. Run it again after pulling changes. While working on the compiler, use `cargo run -p plum-cli --` instead.
47
+
48
+ To rebuild the VSCode extension, you also need `node` and `npm`: run `npm install && npm run build` in `compiler/tooling/vscode-plum`.
49
+
50
+ ## Releases
51
+
52
+ For maintainers: `cargo make release` builds the macOS, Linux and Windows binaries, tags the version, updates the download links in the README and uploads the binaries. It needs [cargo-make](https://github.com/sagiegurari/cargo-make), [cross](https://github.com/cross-rs/cross) with Docker, and the `git` rclone remote. See `Makefile.toml`.