plum
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
- README.md +40 -1285
- compiler/lsp/src/main.rs +18 -8
- docs/game.md +93 -0
- docs/language.md +526 -0
- docs/performance.md +51 -0
- docs/std.md +166 -0
- docs/tools.md +52 -0
README.md
CHANGED
|
@@ -1,126 +1,56 @@
|
|
|
1
1
|
# 👾 Plum
|
|
2
2
|
|
|
3
|
-
A statically typed
|
|
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/
|
|
6
|
+
import std/Io
|
|
13
|
-
import std/Str
|
|
14
7
|
import std/Int
|
|
15
8
|
import std/Float
|
|
16
|
-
import std/
|
|
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 =
|
|
13
|
+
enum Shape =
|
|
26
14
|
| Circle(r: Float)
|
|
27
15
|
| Rect(w: Float, h: Float)
|
|
28
16
|
|
|
29
|
-
fun area(self) -> Float =
|
|
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] =
|
|
22
|
+
fun firstEven(xs: List[Int]) -> Option[Int] =
|
|
53
|
-
for x := range xs
|
|
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
|
-
|
|
29
|
+
shape := Rect(w: 2.0, h: 3.0)
|
|
30
|
+
even := firstEven([1, 3, 4]).unwrapOr(0)
|
|
67
|
-
io.printLn("
|
|
31
|
+
io.printLn("area {shape.area()}, first even {even}")
|
|
68
32
|
|
|
69
|
-
test "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
+
Or build from source with `cargo install --path compiler/cli`.
|
|
142
72
|
|
|
143
|
-
##
|
|
73
|
+
## Use it
|
|
144
74
|
|
|
145
|
-
```sh
|
|
146
|
-
plum run file.plum # compile and run under wasmtime
|
|
147
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-

|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+

|
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
|
|
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
|
-
|
|
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 =
|
|
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(), "
|
|
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}
|
|
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
|
+

|
|
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`.
|