plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
36d9946
— Peter John
2026-11-01T22:44:09+05:30
docs: README restructure - one owner per example/rule, types sections reordered
README.md
CHANGED
|
@@ -18,8 +18,13 @@ import std/Seq
|
|
|
18
18
|
|
|
19
19
|
error ParseError = BadNum | Negative # a named error set
|
|
20
20
|
|
|
21
|
-
struct Circle(r: Float)
|
|
21
|
+
struct Circle(r: Float) = # small copy-semantics value type
|
|
22
|
+
fun area(self) -> Float = # a method
|
|
23
|
+
self.r * self.r * 3.14
|
|
24
|
+
|
|
22
|
-
record Rect(w: Float, h: Float)
|
|
25
|
+
record Rect(w: Float, h: Float) = # reference type
|
|
26
|
+
fun area(self) -> Float =
|
|
27
|
+
self.w * self.h
|
|
23
28
|
|
|
24
29
|
enum Shape = # a union: a Circle or a Rect
|
|
25
30
|
| Circle
|
|
@@ -27,12 +32,7 @@ enum Shape = # a union: a Circle or a Rect
|
|
|
27
32
|
|
|
28
33
|
SIDES_MAX = 4 # associated constant: Shape.SIDES_MAX
|
|
29
34
|
|
|
30
|
-
fun area(self) -> Float = # a method
|
|
31
|
-
match self
|
|
32
|
-
Circle(r) => r * r * 3.14
|
|
33
|
-
Rect(w, h) => w * h
|
|
34
|
-
|
|
35
|
-
struct Vec2(x: Float, y: Float) =
|
|
35
|
+
struct Vec2(x: Float, y: Float) =
|
|
36
36
|
ZERO = Vec2(x: 0.0, y: 0.0) # Vec2.ZERO
|
|
37
37
|
|
|
38
38
|
fun add(self, o: Vec2) -> Vec2 = # `a + b` calls a.add(b)
|
|
@@ -76,9 +76,9 @@ test "glance"
|
|
|
76
76
|
| declare with a type | `s: Shape := Circle(r: 2.0)` (only with `:=`, a value is required) |
|
|
77
77
|
| a block | indent by 2 spaces, no braces |
|
|
78
78
|
| group an expression | `{1 + 2} * 3` (braces; parentheses are for calls) |
|
|
79
|
-
| a conditional expression | `if c then a else b` (
|
|
79
|
+
| a conditional expression | `if c then a else b` (no `c ? a : b` ternary) |
|
|
80
80
|
| loop | `while c`, `for x := range src`, `for i := range n` |
|
|
81
|
-
| a union of types | `enum Shape = Circle \|
|
|
81
|
+
| a union of types | `enum Shape = Circle \| Rect` |
|
|
82
82
|
| a small value type | `struct Vec2(x: Float, y: Float)` |
|
|
83
83
|
| a record (reference type) | `record User(name: Str, age: Int)` |
|
|
84
84
|
| a method / static / constant | `fun area(self)` / `fun origin()` / `NAME = expr` inside the type body |
|
|
@@ -329,11 +329,11 @@ fun draw(shape: Shape) =
|
|
|
329
329
|
drawOther(shape)
|
|
330
330
|
```
|
|
331
331
|
|
|
332
|
-
A bare member name narrows the value inside the branch
|
|
332
|
+
A bare member name narrows the value inside the branch (see [Enums](#enums)):
|
|
333
333
|
|
|
334
334
|
```plum
|
|
335
335
|
fun width(s: Shape) -> Float =
|
|
336
|
-
if
|
|
336
|
+
if Rect := s
|
|
337
337
|
s.w
|
|
338
338
|
else
|
|
339
339
|
0.0
|
|
@@ -389,24 +389,51 @@ fun sumAll(nums: ...Int) -> Int = # variadic param
|
|
|
389
389
|
total
|
|
390
390
|
```
|
|
391
391
|
|
|
392
|
-
A function without a return type returns `Unit`; the last expression of a body is its value.
|
|
392
|
+
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)).
|
|
393
393
|
|
|
394
394
|
## Types: struct, record and enum
|
|
395
395
|
|
|
396
|
-
`
|
|
396
|
+
`struct` and `record` are product types with named fields (a value type and a reference type); an `enum` is a union of member types. The other declaration forms are `error Name = A | B` ([Error handling](#error-handling)) and `primitive Name =` ([Constants, statics and `primitive`](#constants-statics-and-primitive)).
|
|
397
|
+
|
|
398
|
+
### `struct` and `record`
|
|
399
|
+
|
|
400
|
+
`struct` declares a small, copy-semantics value type (a vector, a color, a rectangle) that is flattened into wasm locals instead of being heap-allocated. `record` declares a reference type. Both are declared with named fields:
|
|
401
|
+
|
|
402
|
+
```plum
|
|
403
|
+
struct Vec2(x: Float, y: Float)
|
|
404
|
+
|
|
405
|
+
struct Body(pos: Vec2, size: Vec2) # a field may be another struct
|
|
406
|
+
|
|
407
|
+
record User(name: Str, age: Int)
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
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 `=`.
|
|
397
411
|
|
|
398
|
-
|
|
412
|
+
```plum
|
|
413
|
+
a := Vec2(x: 1.0, y: 2.0)
|
|
414
|
+
b := a
|
|
415
|
+
b.x = 9.0 # a.x is still 1.0: a struct is copied
|
|
416
|
+
u := User(name: "ann", age: 3)
|
|
417
|
+
u2 := u
|
|
418
|
+
u2.age = 4 # u.age is now 4: a record is a heap object, shared on assignment
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
A `struct` has no generics; its fields are only `Int`, `Float`, `Bool` or other structs, and a struct can't contain itself. Values are copied on assignment and when passed, and are boxed inside `T?` and other generic slots. <!-- arrays-of-structs note --> A `record` may have any field types and may be generic (`record Pair[K, V](key: K, val: V)`). Methods and `match` work as for any enum. See [`plum-examples/values.plum`](plum-examples/values.plum).
|
|
399
422
|
|
|
400
423
|
### Enums
|
|
401
424
|
|
|
402
|
-
An `enum` lists the types it can hold. A member is a declared `struct` or `record`, a built-in (`Int`, `Float`, `Bool`, `Byte`), a std type (`Str`, `List[Json]`, `Map[Str, Json]`), or a bare name that is not a declared type, which becomes a payload-less tag. A short list fits on one line; a longer one, or one with methods, goes on indented `|` lines:
|
|
425
|
+
An `enum` lists the types it can hold. A member is a declared `struct` or `record`, a built-in (`Int`, `Float`, `Bool`, `Byte`), a std type (`Str`, `List[Json]`, `Map[Str, Json]`), or a bare name that is not a declared type, which becomes a payload-less tag. A short list fits on one line; a longer one, or one with methods, goes on indented `|` lines. Every member other than a tag is an existing type, and the enum adds no new constructor for it:
|
|
403
426
|
|
|
404
427
|
```plum
|
|
405
|
-
struct Circle(r: Float)
|
|
428
|
+
struct Circle(r: Float) =
|
|
429
|
+
fun area(self) -> Float =
|
|
430
|
+
self.r * self.r * 3.14
|
|
406
431
|
|
|
407
|
-
record
|
|
432
|
+
record Rect(w: Float, h: Float) =
|
|
433
|
+
fun area(self) -> Float =
|
|
434
|
+
self.w * self.h
|
|
408
435
|
|
|
409
|
-
enum Shape = Circle |
|
|
436
|
+
enum Shape = Circle | Rect
|
|
410
437
|
|
|
411
438
|
enum Color =
|
|
412
439
|
| Red
|
|
@@ -422,14 +449,12 @@ enum Json =
|
|
|
422
449
|
| Map[Str, Json]
|
|
423
450
|
```
|
|
424
451
|
|
|
425
|
-
Every member other than a tag is an existing type, and the enum adds no new constructor for it.
|
|
426
|
-
|
|
427
452
|
A member converts to its enum wherever the enum is expected: an argument, a `return`, a tail expression, `push`, a `T?` or a `!T` slot. Nothing is written at the call site:
|
|
428
453
|
|
|
429
454
|
```plum
|
|
430
455
|
fun make(big: Bool) -> Shape =
|
|
431
456
|
if big
|
|
432
|
-
|
|
457
|
+
Rect(w: 10.0, h: 10.0)
|
|
433
458
|
else
|
|
434
459
|
Circle(r: 1.0)
|
|
435
460
|
|
|
@@ -440,7 +465,7 @@ fun use() -> Str =
|
|
|
440
465
|
label(2.5)
|
|
441
466
|
```
|
|
442
467
|
|
|
443
|
-
A pattern names a member to take the value apart (`Circle(r)`) or, written as just the member's name, to narrow the subject to that type. In an arm that names `Float`, the subject is a `Float`; the narrowed subject must be a local, a parameter or `self`:
|
|
468
|
+
A pattern names a member to take the value apart (`Circle(r)`) or, written as just the member's name, to narrow the subject to that type, so the member's own fields and methods are available (`if Rect := s`, then `s.w`). In an arm that names `Float`, the subject is a `Float`; the narrowed subject must be a local, a parameter or `self`:
|
|
444
469
|
|
|
445
470
|
```plum
|
|
446
471
|
fun show(j: Json) -> Str =
|
|
@@ -453,13 +478,15 @@ fun show(j: Json) -> Str =
|
|
|
453
478
|
Map => "map"
|
|
454
479
|
```
|
|
455
480
|
|
|
456
|
-
A method that every member defines is callable on the enum itself, and the call goes to the member's own method (auto-dispatch):
|
|
481
|
+
A method that every member defines is callable on the enum itself, and the call goes to the member's own method (auto-dispatch). The enum can also define the method itself with a `match self` body, one per operation:
|
|
457
482
|
|
|
458
483
|
```plum
|
|
459
484
|
fun areaOf(s: Shape) -> Float =
|
|
460
485
|
s.area()
|
|
461
486
|
```
|
|
462
487
|
|
|
488
|
+
See [`plum-examples/shapes.plum`](plum-examples/shapes.plum) and [`plum-examples/types.plum`](plum-examples/types.plum).
|
|
489
|
+
|
|
463
490
|
One type can be a member of several enums. A `Circle` is a `Shape` and also a `Round`, and converts to whichever the context asks for:
|
|
464
491
|
|
|
465
492
|
```plum
|
|
@@ -468,7 +495,7 @@ enum Round = Circle | Oval
|
|
|
468
495
|
record Oval(a: Float)
|
|
469
496
|
```
|
|
470
497
|
|
|
471
|
-
A method call on a member value falls back to the enum when the member has no such method and exactly one enum it belongs to defines it (`
|
|
498
|
+
A method call on a member value falls back to the enum when the member has no such method and exactly one enum it belongs to defines it (`circle.perimeter()` calls `Shape.perimeter` when only the enum defines it); if two enums define it, the call is an error until you convert the value first.
|
|
472
499
|
|
|
473
500
|
A discriminant enum gives every tag a shared number:
|
|
474
501
|
|
|
@@ -499,11 +526,11 @@ fun main() -> Int =
|
|
|
499
526
|
a + w.inner.age # chained field access
|
|
500
527
|
```
|
|
501
528
|
|
|
502
|
-
A field of a `struct` or `record` is read with `.field`. On a union it is read after narrowing (
|
|
529
|
+
A field of a `struct` or `record` is read with `.field`. On a union it is read after narrowing (see [Enums](#enums)).
|
|
503
530
|
|
|
504
531
|
### Constructing values
|
|
505
532
|
|
|
506
|
-
Construct a `struct` or `record` by calling its name with `field: value` pairs (any order; every field required), or positionally:
|
|
533
|
+
Construct a `struct` or `record` 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:
|
|
507
534
|
|
|
508
535
|
```plum
|
|
509
536
|
cat := Cat(name: "Whiskers", age: 3)
|
|
@@ -528,7 +555,7 @@ match n
|
|
|
528
555
|
|
|
529
556
|
match shape
|
|
530
557
|
Circle(r) => r
|
|
531
|
-
|
|
558
|
+
Rect(w, h) => w * h
|
|
532
559
|
|
|
533
560
|
match book
|
|
534
561
|
FantasyBook(title, _, hasMythicalCreatures) when hasMythicalCreatures =>
|
|
@@ -538,52 +565,6 @@ match book
|
|
|
538
565
|
|
|
539
566
|
Patterns can be literals, a bare identifier (binds), a bare capitalized name (a member or tag of an enum, or an error name when matching an error value, also written `error.Name`; 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 [`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).
|
|
540
567
|
|
|
541
|
-
Mixed kinds of value live in one enum with a `match self` method per operation:
|
|
542
|
-
|
|
543
|
-
```plum
|
|
544
|
-
struct Circle(r: Float)
|
|
545
|
-
|
|
546
|
-
record Rect(w: Float, h: Float)
|
|
547
|
-
|
|
548
|
-
enum Shape =
|
|
549
|
-
| Circle
|
|
550
|
-
| Rect
|
|
551
|
-
|
|
552
|
-
fun area(self) -> Float =
|
|
553
|
-
match self
|
|
554
|
-
Circle(r) => r * r * 3.0
|
|
555
|
-
Rect(w, h) => w * h
|
|
556
|
-
```
|
|
557
|
-
|
|
558
|
-
See [`plum-examples/shapes.plum`](plum-examples/shapes.plum) and [`plum-examples/types.plum`](plum-examples/types.plum).
|
|
559
|
-
|
|
560
|
-
### `struct` and `record`
|
|
561
|
-
|
|
562
|
-
`struct` declares a small, copy-semantics value type (a vector, a color, a rectangle) that is flattened into wasm locals instead of being heap-allocated. `record` declares a reference type. Both are declared with named fields:
|
|
563
|
-
|
|
564
|
-
```plum
|
|
565
|
-
struct Vec2(x: Float, y: Float) =
|
|
566
|
-
fun add(self, o: Vec2) -> Vec2 =
|
|
567
|
-
Vec2(x: self.x + o.x, y: self.y + o.y)
|
|
568
|
-
|
|
569
|
-
struct Rect(pos: Vec2, size: Vec2) # a field may be another struct
|
|
570
|
-
|
|
571
|
-
record User(name: Str, age: Int)
|
|
572
|
-
```
|
|
573
|
-
|
|
574
|
-
The header ends with `=` only when an indented body follows: `struct Vec2(x: Float, y: Float)` with no body has no `=`.
|
|
575
|
-
|
|
576
|
-
```plum
|
|
577
|
-
a := Vec2(x: 1.0, y: 2.0)
|
|
578
|
-
b := a
|
|
579
|
-
b.x = 9.0 # a.x is still 1.0: a struct is copied
|
|
580
|
-
u := User(name: "ann", age: 3)
|
|
581
|
-
u2 := u
|
|
582
|
-
u2.age = 4 # u.age is now 4: a record is a heap object, shared on assignment
|
|
583
|
-
```
|
|
584
|
-
|
|
585
|
-
A `struct` has no generics; its fields are only `Int`, `Float`, `Bool` or other structs, and a struct can't contain itself. Values are copied on assignment and when passed, and are boxed only inside generic containers (`List[Vec2]`, `Vec2?`). A `record` may have any field types and may be generic (`record Pair[K, V](key: K, val: V)`). Methods and `match` work as for any enum. See [`plum-examples/values.plum`](plum-examples/values.plum).
|
|
586
|
-
|
|
587
568
|
## Generics
|
|
588
569
|
|
|
589
570
|
```plum
|
|
@@ -679,7 +660,7 @@ primitive Byte = # in plum-std/Byte.plum
|
|
|
679
660
|
Int(self)
|
|
680
661
|
```
|
|
681
662
|
|
|
682
|
-
The std library uses namespaces where free functions clearly belong together: `Math
|
|
663
|
+
The std library uses namespaces where free functions clearly belong together: `Math` (see [Standard library highlights](#standard-library-highlights)). `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).
|
|
683
664
|
|
|
684
665
|
## Closures
|
|
685
666
|
|
|
@@ -832,14 +813,12 @@ fun find(n: Int) -> Person? =
|
|
|
832
813
|
| Form | Meaning |
|
|
833
814
|
|---|---|
|
|
834
815
|
| `opt ?: default` | the value, or `default` when `opt` is null. The default may diverge: `opt ?: return null`, `opt ?: return error.Absent`, `opt ?: crash "msg"` |
|
|
835
|
-
| `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` |
|
|
816
|
+
| `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`. Patterns beyond a plain name: see [Pattern `if`](#pattern-if) |
|
|
836
817
|
| `while x := opt` + block | loops while the expression is non-null, re-evaluated each iteration: `while job := queue.next()` |
|
|
837
818
|
| `opt!` | the value, or crashes with `<callee> returned null at file:line:col` |
|
|
838
819
|
| `opt == null`, `opt != null` | presence test (either order) |
|
|
839
820
|
| `opt?.field`, `opt?.method()` | maps over a present value; null passes through (see [Safe navigation](#safe-navigation-)) |
|
|
840
821
|
|
|
841
|
-
`if` also takes patterns beyond a plain name (`if Circle(r) := shape`), and optional bindings work in `else if` chains: see [Pattern `if`](#pattern-if).
|
|
842
|
-
|
|
843
822
|
`?:` 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`).
|
|
844
823
|
|
|
845
824
|
```plum
|
|
@@ -1032,7 +1011,7 @@ hello, world!
|
|
|
1032
1011
|
- **`Map[K, V]`** (`import std/Map`) — `Map("a" => 1, "b" => 2)` builds a `Map[Str, Int]`; `Map[K, V]()` is empty. `k => v` is sugar for `Pair(k, v)` in any call's argument list, and a key can be any expression (`Map(n + 1 => x)`). A later duplicate key wins, as with `set`. See [`plum-examples/maps.plum`](plum-examples/maps.plum)
|
|
1033
1012
|
- **`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
|
|
1034
1013
|
- **`Seq[T]`** (`import std/Seq`) — the adapters and terminals of the `for` chains
|
|
1035
|
-
- **`Json`**, **`Base64`**, **`Uuid`**, **`Time`**, **`Random`** — parsing, encoding, ids, wall-clock time and a seeded deterministic generator (
|
|
1014
|
+
- **`Json`**, **`Base64`**, **`Uuid`**, **`Time`**, **`Random`** — parsing, encoding, ids, wall-clock time and a seeded deterministic generator (the `Random` statics use the wall clock)
|
|
1036
1015
|
- **`Os`**, **`Http`** — printing and the file system; HTTP requests through the host
|
|
1037
1016
|
- **`Testing`** (`import std/Testing`) — assertion helpers for `test` blocks
|
|
1038
1017
|
|