plum

#treesitter#compiler#wasm

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
Files changed (1) hide show
  1. README.md +58 -79
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) # small copy-semantics value type
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) # reference type
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) = # small copy-semantics value type
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` (there is no `?:` ternary) |
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 \| Rectangle` |
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, so the member's own fields and methods are available:
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 Rectangle := s
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. A call can name its arguments only for constructors (`Cat(name: "Whiskers", age: 3)`); functions take positional arguments.
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
- `enum` is a union: a value that is one of several member types. `struct` and `record` are product types with named fields. A `struct` is a value type and a `record` is a reference type (see [`struct` and `record`](#struct-and-record)).
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
- The other declaration forms are `error Name = A | B` ([Error handling](#error-handling)) and `primitive Name =` ([Constants, statics and `primitive`](#constants-statics-and-primitive)).
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 Rectangle(w: Float, h: Float)
432
+ record Rect(w: Float, h: Float) =
433
+ fun area(self) -> Float =
434
+ self.w * self.h
408
435
 
409
- enum Shape = Circle | Rectangle
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
- Rectangle(w: 10.0, h: 10.0)
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 (`Circle(r: 1.5).area()` calls `Shape.area`); if two enums define it, the call is an error until you convert the value first.
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 (`if Rectangle := s`, then `s.w`).
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
- Rectangle(w, h) => w * h
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.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).
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 (`Random.float()` and `Random.intBelow(n)` use the wall clock)
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