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