plum

#treesitter#compiler#wasm

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

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


cfed449 — Peter John 2026-11-01T23:05:13+05:30
examples: one file per concept, visitors removed, game_loop added; duplicate Plum tests removed (434 -> 375)
README.md CHANGED
@@ -368,7 +368,7 @@ Details:
368
368
  - `else if` chains can mix plain conditions, optional bindings (`if x := opt`, see [Optionals](#optionals)) and patterns. A plain lowercase name over a `T?` is the optional binding; any other pattern is a pattern test.
369
369
  - It works on error values too: inside a `catch e` block, `if TooBig := e` tests the error name.
370
370
 
371
- See [`plum-examples/pattern_if.plum`](plum-examples/pattern_if.plum).
371
+ See [`plum-examples/match.plum`](plum-examples/match.plum).
372
372
 
373
373
  ## Functions
374
374
 
@@ -418,7 +418,7 @@ u2 := u
418
418
  u2.age = 4 # u.age is now 4: a record is a heap object, shared on assignment
419
419
  ```
420
420
 
421
- A `struct` has no generics; its fields are only `Int`, `Float`, `Bool`, `Byte` 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 (`Array[Vec2]`, and so `List[Vec2]` and `Grid[Tile]`) are stored field by field, one native array per field, with no per-element allocation. 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).
421
+ A `struct` has no generics; its fields are only `Int`, `Float`, `Bool`, `Byte` 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 (`Array[Vec2]`, and so `List[Vec2]` and `Grid[Tile]`) are stored field by field, one native array per field, with no per-element allocation. 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/structs_records.plum`](plum-examples/structs_records.plum).
422
422
 
423
423
  ### Enums
424
424
 
@@ -485,7 +485,7 @@ fun areaOf(s: Shape) -> Float =
485
485
  s.area()
486
486
  ```
487
487
 
488
- See [`plum-examples/shapes.plum`](plum-examples/shapes.plum) and [`plum-examples/types.plum`](plum-examples/types.plum).
488
+ See [`plum-examples/unions.plum`](plum-examples/unions.plum).
489
489
 
490
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:
491
491
 
@@ -563,7 +563,7 @@ match book
563
563
  FantasyBook(title, _, _) => "Fantasy book \"{title}\" has no mythical creatures"
564
564
  ```
565
565
 
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).
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).
567
567
 
568
568
  ## Generics
569
569
 
@@ -775,7 +775,7 @@ while i < units.length()
775
775
  seen = seen + 1
776
776
  ```
777
777
 
778
- 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).
778
+ See [`plum-examples/seq.plum`](plum-examples/seq.plum) (one test per adapter and terminal, plus fused chains), [`plum-examples/forms.plum`](plum-examples/forms.plum) (user-defined `fun*` forms) and [`plum-examples/loops.plum`](plum-examples/loops.plum).
779
779
 
780
780
  ### Iteration cheat sheet
781
781
 
@@ -847,7 +847,7 @@ fun targetName(u: User?) -> Str? =
847
847
  u?.target?.name
848
848
  ```
849
849
 
850
- 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).
850
+ 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).
851
851
 
852
852
  ## Error handling
853
853
 
plum-examples/chains.plum DELETED
@@ -1,115 +0,0 @@
1
- # Sequence chaining: combines collection adapters (skip, take, map, filter)
2
- # into single-pass loops via `std/Seq`.
3
- import std/Str
4
- import std/Int
5
- import std/Float
6
- import std/List
7
- import std/Testing
8
- import std/Seq
9
-
10
- # Closure-free chains. A method chain such as
11
- #
12
- # keep my team's units, take their hp, sum it
13
- #
14
- # becomes ONE `for` over the sequence:
15
- #
16
- # * per-element stages are header clauses: filter = `, if cond`,
17
- # map = `, name := expr`;
18
- # * the terminal step is the outer form plus its body (fold, map, find, count,
19
- # any, all);
20
- # * body-less stages nest as `fun*` sources (take, skip, zip, reversed,
21
- # chunks, stepBy), and always fuse into the same single loop;
22
- # * materializing stages (sort, group) are separate statements.
23
- #
24
- # No closure is allocated and there is no call_indirect: the wasm is the loop you
25
- # would have written by hand. See plum-std/Seq.plum.
26
-
27
- record Soldier(name: Str, team: Int, hp: Float, x: Float, y: Float)
28
-
29
- fun dist2(ax: Float, ay: Float, bx: Float, by: Float) -> Float =
30
- dx := ax - bx
31
- dy := ay - by
32
- dx * dx + dy * dy
33
-
34
- fun army() -> List[Soldier] =
35
- List(
36
- Soldier(name: "a", team: 1, hp: 10.0, x: 0.0, y: 0.0),
37
- Soldier(name: "b", team: 2, hp: 0.0, x: 1.0, y: 1.0),
38
- Soldier(name: "c", team: 1, hp: 7.0, x: 5.0, y: 5.0),
39
- Soldier(name: "d", team: 2, hp: 3.0, x: 2.0, y: 0.0),
40
- Soldier(name: "e", team: 1, hp: 4.0, x: 9.0, y: 9.0),
41
- Soldier(name: "f", team: 2, hp: 8.0, x: 1.0, y: 0.0),
42
- )
43
-
44
- test "sum of hp of my team: filter + map + sum is a fold with clauses"
45
- units := army()
46
- me := units.at(0)
47
- # units.filter(team == me).map(hp).sum()
48
- total := for acc, u := range units.fold(0.0), if u.team == me.team, hp := u.hp
49
- acc + hp
50
- assert total == 21.0
51
-
52
- test "names of living units within range: filter + map + filter + map"
53
- units := army()
54
- me := units.at(0)
55
- reach := 5.0
56
- names := for u := range units.map(), if u.hp > 0.0, d := dist2(u.x, u.y, me.x, me.y), if d < reach
57
- u.name
58
- assert names.length() == 3
59
- assert names.at(0) == "a"
60
- assert names.at(1) == "d"
61
- assert names.at(2) == "f"
62
-
63
- test "first enemy in range: filter + find"
64
- units := army()
65
- me := units.at(0)
66
- near := for u := range units.find(), if u.team != me.team
67
- dist2(u.x, u.y, me.x, me.y) < 4.0
68
- first := near ?: me
69
- assert first.name == "b"
70
-
71
- test "take + map: the first five living units"
72
- units := army()
73
- lines := for u := range units.take(5).map(), if u.hp > 0.0
74
- u.name
75
- assert lines.length() == 4
76
- assert lines.at(0) == "a"
77
- assert lines.at(3) == "e"
78
-
79
- test "two binders over a list are (position, element)"
80
- units := army()
81
- hits := 0
82
- for i, u := range units, if u.hp > 0.0
83
- hits = hits + i
84
- assert hits > 0
85
-
86
- test "count and any over a chain"
87
- units := army()
88
- alive := for u := range units.skip(1).count(), if u.team == 2
89
- u.hp > 0.0
90
- assert alive == 2
91
- anyBig := for u := range units.stepBy(2).any()
92
- u.hp > 9.0
93
- assert anyBig
94
-
95
- test "reversed and chunks"
96
- units := army()
97
- last := for u := range units.reversed().take(2).map()
98
- u.name
99
- assert last.at(0) == "f"
100
- assert last.at(1) == "e"
101
- sizes := for c := range units.chunks(4).map()
102
- c.length()
103
- assert sizes.length() == 2
104
- assert sizes.at(0) == 4
105
- assert sizes.at(1) == 2
106
-
107
- test "materializing stages are separate statements"
108
- units := army()
109
- alive := for u := range units.filter()
110
- u.hp > 0.0
111
- assert alive.length() == 5
112
- # a later stage runs over the materialized list
113
- hps := for u := range alive.map(), if u.team == 1
114
- u.hp
115
- assert hps.length() == 3
plum-examples/constants.plum CHANGED
@@ -55,7 +55,3 @@ test "the same const name can live on two types"
55
55
  test "a struct can hold a constant instance of itself"
56
56
  assert Vec2.ZERO.x == 0.0
57
57
  assert Tile.origin().x == 0
58
-
59
- test "built-in types carry their constants"
60
- assert Float.PI > 3.14 && Float.PI < 3.15
61
- assert Int.MAX > 0
plum-examples/dop_visitor.plum DELETED
@@ -1,114 +0,0 @@
1
- # Data-oriented alternative to the Visitor pattern.
2
- # Based on: https://wimdetroyer.com/blog/visitor-pattern-in-dop
3
- #
4
- # Instead of a Visitable/BookVisitor double-dispatch hierarchy, the shape of
5
- # the data is declared once with a sealed `enum` and every operation on it is
6
- # a single exhaustive `match`. Two features make this read close to the
7
- # article's Java (sealed interfaces implemented directly by records, switch
8
- # with `when` guards and deconstruction):
9
- #
10
- # - Each variant is its own `record` declaration (e.g. `record
11
- # FantasyBook(title: Str, pages: Int, hasMythicalCreatures: Bool)`), listed
12
- # as a member of the union enum (`enum Book = FantasyBook | ScifiBook | ...`).
13
- # Construct with named fields (`FantasyBook(title: ..., ...)`) or
14
- # positionally, and a match arm destructures each member by position
15
- # (`FantasyBook(title, pages, hasMythicalCreatures)`) or by name.
16
- # - A `when <expr>` guard on a match case (`FantasyBook(title, _,
17
- # hasMythicalCreatures) when hasMythicalCreatures => ...`) stands in for
18
- # Java's guarded switch patterns; a guard that evaluates false falls
19
- # through to the next case, same as a genuine pattern mismatch.
20
- #
21
- # The checker's exhaustiveness check on enums stands in for the compile-time
22
- # guarantee sealed interfaces + switch give in Java.
23
- #
24
- # DOP vs. OOP (see the one-`enum`-per-shape rewrite in
25
- # `oop_visitor.plum`):
26
- # - Adding a new operation (a new function that matches on `Book`) touches
27
- # one place here, and the exhaustiveness checker forces every variant to
28
- # be handled. In the OOP version it means adding a new method to every
29
- # shape enum, with nothing catching a variant you forgot to update.
30
- # - Adding a new variant instead means touching every `match` on `Book`
31
- # here (again caught by exhaustiveness if missed); the OOP version adds
32
- # it in one new, self-contained `enum`, untouched elsewhere.
33
- # - So DOP fits best when operations change often and the shapes are
34
- # stable; OOP fits best when shapes multiply and operations are stable.
35
- # plum has no dynamic dispatch through an interface (see
36
- # `oop_visitor.plum`'s header), which tilts things further toward DOP
37
- # for anything that needs to treat the shapes polymorphically.
38
-
39
- import std/Os
40
- import std/Int
41
- import std/Float
42
- import std/Str
43
-
44
- enum Rating = Good | Bad
45
-
46
- record Good(name: Str)
47
- record Bad(name: Str)
48
-
49
- enum Book = FantasyBook | ScifiBook | ChildrensTaleBook | NonFictionBook
50
-
51
- record FantasyBook(title: Str, pages: Int, hasMythicalCreatures: Bool)
52
- record ScifiBook(title: Str, pages: Int, theme: Str)
53
- record ChildrensTaleBook(title: Str, pages: Int, moralLesson: Str)
54
- record NonFictionBook(title: Str, pages: Int, rating1: Rating, rating2: Rating)
55
-
56
- fun collectInterestingInfo(book: Book) -> Str =
57
- match book
58
- FantasyBook(title, _, hasMythicalCreatures) when hasMythicalCreatures => "Fantasy book \"{title}\" features mythical creatures"
59
- FantasyBook(title, _, _) => "Fantasy book \"{title}\" has no mythical creatures"
60
- ScifiBook(title, _, theme) when theme == "space exploration" => "Scifi book \"{title}\" explores space exploration"
61
- ScifiBook(title, _, theme) => "Scifi book \"{title}\" has theme {theme}"
62
- ChildrensTaleBook(title, pages, _) when pages == 0 => "Childrens book \"{title}\" is empty"
63
- ChildrensTaleBook(title, _, moralLesson) => "Childrens book \"{title}\" teaches: {moralLesson}"
64
- NonFictionBook(title, _, rating1, rating2) =>
65
- match rating1, rating2
66
- Good(name1), Good(name2) => "Non-fiction book \"{title}\" praised by {name1} and {name2}"
67
- _, _ => "Non-fiction book \"{title}\" has mixed reviews"
68
-
69
- fun describeFantasy() -> Str =
70
- collectInterestingInfo(FantasyBook(title: "Dragon Tales", pages: 200, hasMythicalCreatures: true))
71
-
72
- fun main() =
73
- printLn(describeFantasy())
74
-
75
- test "fantasy book with mythical creatures is called out"
76
- assert describeFantasy() == "Fantasy book \"Dragon Tales\" features mythical creatures"
77
-
78
- test "fantasy book without mythical creatures is called out"
79
- book := FantasyBook(title: "Plain Fantasy", pages: 150, hasMythicalCreatures: false)
80
- assert collectInterestingInfo(book) == "Fantasy book \"Plain Fantasy\" has no mythical creatures"
81
-
82
- test "scifi book with space exploration theme is called out"
83
- book := ScifiBook(title: "Starbound", pages: 300, theme: "space exploration")
84
- assert collectInterestingInfo(book) == "Scifi book \"Starbound\" explores space exploration"
85
-
86
- test "scifi book with other theme falls through to generic case"
87
- book := ScifiBook(title: "Timeloop", pages: 250, theme: "time travel")
88
- assert collectInterestingInfo(book) == "Scifi book \"Timeloop\" has theme time travel"
89
-
90
- test "empty childrens book is handled via a when guard on pages"
91
- book := ChildrensTaleBook(title: "Nothing", pages: 0, moralLesson: "none")
92
- assert collectInterestingInfo(book) == "Childrens book \"Nothing\" is empty"
93
-
94
- test "childrens book with pages reports its moral lesson"
95
- book := ChildrensTaleBook(title: "The Tortoise", pages: 40, moralLesson: "patience wins")
96
- assert collectInterestingInfo(book) == "Childrens book \"The Tortoise\" teaches: patience wins"
97
-
98
- test "non fiction book praised by both reviewers via nested deconstruction"
99
- book := NonFictionBook(
100
- title: "Hidden Facts",
101
- pages: 120,
102
- rating1: Good(name: "Alice"),
103
- rating2: Good(name: "Bob"),
104
- )
105
- assert collectInterestingInfo(book) == "Non-fiction book \"Hidden Facts\" praised by Alice and Bob"
106
-
107
- test "non fiction book with a bad rating reports mixed reviews"
108
- book := NonFictionBook(
109
- title: "Shaky Claims",
110
- pages: 80,
111
- rating1: Good(name: "Alice"),
112
- rating2: Bad(name: "Carol"),
113
- )
114
- assert collectInterestingInfo(book) == "Non-fiction book \"Shaky Claims\" has mixed reviews"
plum-examples/error_propagation.plum CHANGED
@@ -10,7 +10,7 @@ import std/List
10
10
  # front of the `!`: `error{A, B}!T` inline, `ParseError!T` for a set declared
11
11
  # with `error ParseError = A | B`, or `(A || B)!T` to merge sets. Pin the set on
12
12
  # a public API, on a recursive function and on an `extern fun`. There is no
13
- # `Option`: an optional `T?` (see safe_navigation.plum) is not an error.
13
+ # `Option`: an optional `T?` (see optionals.plum) is not an error.
14
14
  #
15
15
  # A plain `T` (a tail value or `return n`) is the success value; an error value
16
16
  # (`return error.X`) is the failure. A bare name is never an implicit failure.
plum-examples/forms.plum CHANGED
@@ -1,5 +1,5 @@
1
- # Fused loop forms: `for` loops with `, if cond` filter clauses and inline
1
+ # User-defined `fun*` forms with `yield`: sources, folds and methods written in
2
- # mapping expressions, plus custom `fun*` generators with `yield`.
2
+ # plum and expanded at the call site (the built-in forms are in seq.plum).
3
3
  import std/Str
4
4
  import std/Int
5
5
  import std/Float
@@ -73,69 +73,6 @@ record Countdown(from: Int) =
73
73
  i = i - 1
74
74
  yield cur, cur * cur
75
75
 
76
- test "filter_form: keeps the elements whose body is true"
77
- fighters := sampleFighters()
78
- alive := for u := range fighters.filter()
79
- !u.dead
80
- assert alive.length() == 2
81
- assert alive.at(1).name == "c"
82
-
83
- test "map_with_clauses_fused: clauses filter, the body maps"
84
- xs := List(1, 2, 3, 4, 5, 6)
85
- squares := for x := range xs.map(), if x % 2 == 0, sq := x * x
86
- sq + 1
87
- assert squares.length() == 3
88
- assert squares.at(0) == 5
89
- assert squares.at(2) == 37
90
-
91
- test "fold_two_binders: acc, x"
92
- xs := List(1, 2, 3, 4)
93
- total := for acc, x := range xs.fold(100)
94
- acc + x
95
- assert total == 110
96
- best := for acc, x := range xs.fold(0), if x != 3
97
- if acc > x then acc else x
98
- assert best == 4
99
-
100
- test "find_returns_nullable: first match or null"
101
- xs := List(5, 8, 12, 20)
102
- big := for x := range xs.find()
103
- x > 10
104
- got := big ?: -1
105
- assert got == 12
106
- none := for x := range xs.find()
107
- x > 100
108
- missing := none ?: -1
109
- assert missing == -1
110
-
111
- test "any, all, count"
112
- xs := List(1, 2, 3, 4)
113
- some3 := for x := range xs.any()
114
- x == 3
115
- assert some3
116
- allSmall := for x := range xs.all()
117
- x < 4
118
- assert !allSmall
119
- n := for x := range xs.count(), if x > 1
120
- x % 2 == 0
121
- assert n == 2
122
-
123
- test "zip and each, and two binders over a list give (position, element)"
124
- xs := List(10, 20, 30)
125
- ys := List(1, 2, 3, 4)
126
- acc := 0
127
- for i, x := range xs
128
- acc = acc + i * x
129
- assert acc == 80
130
- dot := 0
131
- for a, b := range xs.zip(ys)
132
- dot = dot + a * b
133
- assert dot == 140
134
- seen := 0
135
- for x := range xs.each()
136
- seen = seen + 1
137
- assert seen == 3
138
-
139
76
  test "user_form_sumBy"
140
77
  fighters := sampleFighters()
141
78
  hp := for u := range sumBy(fighters)
@@ -195,54 +132,6 @@ test "return for-form: the form's value is returned"
195
132
  test "tail for-form: the last statement of a fn is its value"
196
133
  assert tailForm(List(1, 2, 3)) == 12
197
134
 
198
- test "until: start inclusive, end exclusive, continue still advances"
199
- total := 0
200
- for i := range 2.until(6)
201
- total = total + i
202
- assert total == 14
203
- odd := 0
204
- for i := range 10, if i % 2 == 1
205
- odd = odd + 1
206
- assert odd == 5
207
- skipped := 0
208
- for i := range 6
209
- if i % 2 == 0
210
- continue
211
- skipped = skipped + i
212
- assert skipped == 9
213
- none := 0
214
- for i := range 5.until(5)
215
- none = none + 1
216
- assert none == 0
217
-
218
- test "yield_value_drives_filter: the body's value decides"
219
- xs := List(1, 2, 3, 4, 5, 6)
220
- even := for x := range xs.filter()
221
- x % 2 == 0
222
- assert even.length() == 3
223
- assert even.at(2) == 6
224
-
225
- test "fold_yield_two: yield acc, x"
226
- xs := List(1, 2, 3, 4)
227
- product := for acc, x := range xs.fold(1)
228
- acc * x
229
- assert product == 24
230
-
231
- test "nested_fun_star_forms: adapters take sequences"
232
- xs := List(10, 20, 30, 40, 50)
233
- got := for p := range xs.take(3).map()
234
- p + 1
235
- assert got.length() == 3
236
- assert got.at(2) == 31
237
- idx := 0
238
- for x := range xs.skip(2)
239
- idx = idx + x
240
- assert idx == 30 + 40 + 50
241
- names := for x := range xs.take(2).map(), if x > 10
242
- x * 2
243
- assert names.length() == 1
244
- assert names.at(0) == 40
245
-
246
135
  test "fun_star_method_uses_self"
247
136
  c := Countdown(from: 4)
248
137
  seen := 0
@@ -264,16 +153,3 @@ test "two_binder_uses_entries_method: a custom type's entries()"
264
153
  total = total + sq
265
154
  assert total == 6014 + 14
266
155
 
267
- test "two_binder_defaults_to_position: List gives (i, x)"
268
- xs := List(10, 20, 30)
269
- acc := 0
270
- for i, x := range xs
271
- acc = acc + i * x
272
- assert acc == 80
273
- n := 0
274
- for _, x := range xs
275
- n = n + x
276
- assert n == 60
277
- for _ := range 3
278
- n = n + 1
279
- assert n == 63
plum-examples/functions.plum CHANGED
@@ -1,5 +1,5 @@
1
- # Functions: top-level function declarations, typed arguments, return expressions,
1
+ # Functions: top-level function declarations, typed arguments, default and
2
- # and recursion.
2
+ # variadic parameters, return expressions, and recursion.
3
3
  import std/Str
4
4
  import std/Int
5
5
  import std/Float
@@ -31,7 +31,7 @@ fun useWrap() -> Bool =
31
31
  fun usePair() -> Bool =
32
32
  pair(1, "x")
33
33
 
34
- # ---- generics, variadics, and misc regression tests ----
34
+ # ---- recursion, defaults and variadics ----
35
35
 
36
36
  fun factorial(x: Int) -> Int =
37
37
  if x < 2
@@ -45,56 +45,6 @@ fun factorial(x: Int) -> Int =
45
45
  fun double(n: Int) -> Int =
46
46
  n * 2
47
47
 
48
- record GenericBox[T](value: T) =
49
-
50
- fun getIntValue() -> T =
51
- self.value
52
-
53
- fun useGenericBox() -> Int =
54
- b := GenericBox(value: 7)
55
- b.getIntValue()
56
-
57
- fun identity(value: T) -> T =
58
- value
59
-
60
- record GenericBox2[T](value: T) =
61
-
62
- fun getValue() -> T =
63
- self.value
64
-
65
- fun doubled(value: Int) -> Int =
66
- identity(value) + identity(value)
67
-
68
- enum Parcel[T] = Delivered[T] | Lost
69
-
70
- record Delivered[T](v: T)
71
-
72
- fun contentsOr(p: Parcel[Int], default: Int) -> Int =
73
- match p
74
- Delivered(v) => v
75
- Lost => default
76
-
77
- # `Str.length()` is not a real, working method — the point of this test is
78
- # that `Parcel[Str]` coexists with `Parcel[Int]` and both run correctly, not
79
- # string processing, so the Str arm returns a fixed literal.
80
- fun intContentsOr(p: Parcel[Int], default: Int) -> Int =
81
- match p
82
- Delivered(v) => v
83
- Lost => default
84
-
85
- fun strContentsOr(p: Parcel[Str], default: Int) -> Int =
86
- match p
87
- Delivered(v) => 4
88
- Lost => default
89
-
90
- record GenericBox3[T](value: T) =
91
-
92
- fun getBoxValue() -> T =
93
- self.value
94
-
95
- fun sumBox(b: GenericBox3[Int]) -> Int =
96
- b.getBoxValue()
97
-
98
48
  fun combineVariadic(prefix: Int, rest: ...Int) -> Int =
99
49
  prefix
100
50
 
@@ -104,31 +54,6 @@ test "factorial runs correctly"
104
54
  test "lowercase single word function call runs correctly"
105
55
  assert double(21) == 42
106
56
 
107
- test "generic class specialized at two types does not alias"
108
- assert useGenericBox() == 7
109
-
110
- test "generic function called at multiple concrete types runs correctly"
111
- assert identity(5) + identity(37) == 42
112
-
113
- test "generic method on generic class runs correctly"
114
- b := GenericBox2(value: 9)
115
- assert b.getValue() == 9
116
-
117
- test "transitively generic call chain runs correctly"
118
- assert doubled(21) == 42
119
-
120
- test "generic enum specialized and matched runs correctly"
121
- assert contentsOr(Delivered(13), 0) == 13
122
-
123
- test "generic enum multiple instantiations coexist and run correctly"
124
- assert intContentsOr(Delivered(13), 0) + strContentsOr(Delivered("abcd"), 0) == 17
125
-
126
- test "same bare generic enum param function called multiple times runs correctly"
127
- assert contentsOr(Delivered(5), 0) + contentsOr(Delivered(37), 0) == 42
128
-
129
- test "ordinary function with bare generic class param runs correctly"
130
- assert sumBox(GenericBox3(value: 11)) == 11
131
-
132
57
  test "variadic call with varying trailing arg counts runs correctly"
133
58
  a := combineVariadic(10)
134
59
  b := combineVariadic(20, 1)
plum-examples/game_loop.plum ADDED
@@ -0,0 +1,102 @@
1
+ # A tiny game update loop. Particles are structs in a `List`, which stores them
2
+ # field by field (struct-of-arrays), so 100 frames of updates allocate nothing per
3
+ # particle; dead ones are removed by swap-remove. Entities live in a `Map[Int,
4
+ # Entity]` and are changed by events handled with `match`.
5
+ module game_loop
6
+ import std/Str
7
+ import std/Int
8
+ import std/Float
9
+ import std/List
10
+ import std/Map
11
+
12
+ struct Vec2(x: Float, y: Float) =
13
+
14
+ fun add(self, o: Vec2) -> Vec2 =
15
+ Vec2(x: self.x + o.x, y: self.y + o.y)
16
+
17
+ fun mul(self, s: Float) -> Vec2 =
18
+ Vec2(x: self.x * s, y: self.y * s)
19
+
20
+ struct Particle(pos: Vec2, vel: Vec2, life: Int)
21
+
22
+ record Entity(id: Int, name: Str, hp: Int)
23
+
24
+ record Spawn(id: Int, name: Str)
25
+
26
+ record Damage(id: Int, amount: Int)
27
+
28
+ record Despawn(id: Int)
29
+
30
+ enum Event = Spawn | Damage | Despawn
31
+
32
+ record Stats(spawned: Int, removed: Int)
33
+
34
+ # Move every particle one step; remove the dead by swap-remove (the last
35
+ # particle takes the freed slot, so the list never shifts). Returns how many died.
36
+ fun step(ps: List[Particle], dt: Float) -> Int =
37
+ removed := 0
38
+ i := 0
39
+ while i < ps.length()
40
+ p := ps.at(i)
41
+ p.pos = p.pos.add(p.vel.mul(dt))
42
+ p.life = p.life - 1
43
+ if p.life <= 0
44
+ last := ps.at(ps.length() - 1)
45
+ ps.set(i, last)!
46
+ ps.pop()!
47
+ removed = removed + 1
48
+ else
49
+ ps.set(i, p)!
50
+ i = i + 1
51
+ removed
52
+
53
+ fun run(frames: Int, ps: List[Particle]) -> Stats =
54
+ spawned := 0
55
+ removed := 0
56
+ for frame := range frames
57
+ if frame % 10 == 0
58
+ ps.push(Particle(pos: Vec2(x: 0.0, y: 0.0), vel: Vec2(x: 2.0, y: 1.0), life: 25))
59
+ spawned = spawned + 1
60
+ removed = removed + step(ps, 0.5)
61
+ Stats(spawned: spawned, removed: removed)
62
+
63
+ fun handle(world: Map[Int, Entity], e: Event) =
64
+ match e
65
+ Spawn(id, name) =>
66
+ world.set(id, Entity(id: id, name: name, hp: 100))
67
+ Damage(id, amount) =>
68
+ if ent := world.getOrNull(id)
69
+ ent.hp = ent.hp - amount
70
+ Despawn(id) =>
71
+ world.remove(id)
72
+
73
+ test "100 frames: particles spawn, move and are swap-removed when dead"
74
+ ps := List[Particle]()
75
+ stats := run(100, ps)
76
+ assert stats.spawned == 10
77
+ assert stats.removed == 8
78
+ assert ps.length() == 2
79
+
80
+ test "survivors have moved and aged the expected amount"
81
+ ps := List[Particle]()
82
+ run(100, ps)
83
+ x := 0.0
84
+ life := 0
85
+ for p := range ps
86
+ x = x + p.pos.x
87
+ life = life + p.life
88
+ # spawned on frames 80 and 90: 20 and 10 steps of +1.0, 5 and 15 life left
89
+ assert x == 30.0
90
+ assert life == 20
91
+
92
+ test "events change the entity map"
93
+ world := Map[Int, Entity]()
94
+ handle(world, Spawn(id: 1, name: "orc"))
95
+ handle(world, Spawn(id: 2, name: "elf"))
96
+ handle(world, Damage(id: 1, amount: 30))
97
+ handle(world, Damage(id: 9, amount: 5))
98
+ assert world.get(1)!.hp == 70
99
+ assert world.get(2)!.name == "elf"
100
+ handle(world, Despawn(id: 2))
101
+ assert world.length() == 1
102
+ assert !world.has(2)
plum-examples/generics.plum CHANGED
@@ -78,6 +78,30 @@ fun sumWith(xs: List[T], num: Num[T]) -> T =
78
78
  acc = num.add(acc, x)
79
79
  acc
80
80
 
81
+ # --- generic records and enums ----------------------------------------------
82
+
83
+ record GenericBox[T](value: T) =
84
+
85
+ fun getValue(self) -> T =
86
+ self.value
87
+
88
+ fun unboxed(b: GenericBox[Int]) -> Int =
89
+ b.getValue()
90
+
91
+ enum Parcel[T] = Delivered[T] | Lost
92
+
93
+ record Delivered[T](v: T)
94
+
95
+ fun intContentsOr(p: Parcel[Int], default: Int) -> Int =
96
+ match p
97
+ Delivered(v) => v
98
+ Lost => default
99
+
100
+ fun strContentsOr(p: Parcel[Str], default: Int) -> Int =
101
+ match p
102
+ Delivered(v) => v.length()
103
+ Lost => default
104
+
81
105
  test "Int extremes survive a generic fn"
82
106
  big := 9223372036854775807
83
107
  small := 0 - 9223372036854775807 - 1
@@ -244,3 +268,14 @@ test "nested generic constructor call as an argument"
244
268
  assert r.at(0).length() == 2
245
269
  s := List(List("a"))
246
270
  assert s.at(0).at(0) == "a"
271
+
272
+ test "a generic record is specialized per type and its methods run"
273
+ assert GenericBox(value: 9).getValue() == 9
274
+ assert GenericBox(value: "s").getValue() == "s"
275
+ assert unboxed(GenericBox(value: 11)) == 11
276
+
277
+ test "a generic enum is specialized per type and matched"
278
+ assert intContentsOr(Delivered(13), 0) == 13
279
+ assert intContentsOr(Lost, 5) == 5
280
+ assert strContentsOr(Delivered("abcd"), 0) == 4
281
+ assert intContentsOr(Delivered(13), 0) + strContentsOr(Delivered("abcd"), 0) == 17
plum-examples/loops.plum CHANGED
@@ -63,16 +63,6 @@ test "break_continue_return_semantics"
63
63
  assert firstOver(xs, 3) == 4
64
64
  assert firstOver(xs, 9) == -1
65
65
 
66
- test "times and until loops"
67
- total := 0
68
- for i := range 5
69
- total = total + i
70
- assert total == 10
71
- count := 0
72
- for i := range 4, if i == 1
73
- count = count + 1
74
- assert count == 1
75
-
76
66
  test "nested iterable loops"
77
67
  xs := List(1, 2)
78
68
  ys := List(10, 20)
plum-examples/map_struct_values.plum DELETED
@@ -1,21 +0,0 @@
1
- # Map with struct values (moved out of plum-std/Map.plum so MapPoint
2
- # does not become a global type for every importer of std/Map).
3
- import std/Map
4
- import std/Int
5
- import std/Float
6
- import std/Str
7
-
8
- struct MapPoint(x: Float, y: Float)
9
-
10
- test "Map[Int, MapPoint] struct values"
11
- m := Map[Int, MapPoint]()
12
- i := 0
13
- while i < 500
14
- m.set(i, MapPoint(x: Float(i), y: Float(i) * 2.0))
15
- i = i + 1
16
- v := m.get(499)!
17
- assert v.x == 499.0
18
- assert v.y == 998.0
19
- m.set(499, MapPoint(x: 1.0, y: 2.0))
20
- assert m.get(499)!.y == 2.0
21
- assert m.length() == 500
plum-examples/maps.plum CHANGED
@@ -3,6 +3,9 @@
3
3
  import std/Int
4
4
  import std/Str
5
5
  import std/Map
6
+ import std/Float
7
+
8
+ struct MapPoint(x: Float, y: Float)
6
9
 
7
10
  test "maps_example_arrow_form"
8
11
  m := Map("ann" => 3, "bob" => 5)
@@ -12,17 +15,15 @@ test "maps_example_arrow_form"
12
15
  test "maps_mixed_pair_and_arrow"
13
16
  assert Map(Pair("a", 1), "b" => 2).length() == 2
14
17
 
15
- test "maps_duplicate_last_wins"
18
+ test "Map[Int, MapPoint] struct values"
19
+ m := Map[Int, MapPoint]()
20
+ i := 0
21
+ while i < 500
16
- m := Map("a" => 1, "a" => 2)
22
+ m.set(i, MapPoint(x: Float(i), y: Float(i) * 2.0))
23
+ i = i + 1
24
+ v := m.get(499)!
25
+ assert v.x == 499.0
26
+ assert v.y == 998.0
27
+ m.set(499, MapPoint(x: 1.0, y: 2.0))
17
- assert m.get("a")! == 2
28
+ assert m.get(499)!.y == 2.0
18
- assert m.length() == 1
29
+ assert m.length() == 500
19
-
20
- test "maps_nested"
21
- m := Map("a" => Map("x" => 1))
22
- assert m.get("a")!.get("x")! == 1
23
-
24
- test "maps_int_keys"
25
- assert Map(1 => "one", 2 => "two").get(1)! == "one"
26
-
27
- test "maps_empty_with_explicit_generics"
28
- assert Map[Str, Int]().isEmpty()
plum-examples/match.plum CHANGED
@@ -1,5 +1,8 @@
1
- # Pattern matching: exhaustive `match` expressions over enum variants, integer
1
+ # Pattern matching. `match` is exhaustive over an enum and supports literals,
2
+ # bindings, wildcards, constructor patterns (nested), multiple subjects and
3
+ # `when` guards. Pattern `if` (`if Circle(r) := shape`) tests one case with the
4
+ # same patterns and reads like an ordinary `if` chain; use `match` when every
2
- # values, and wildcard `_` patterns with guards.
5
+ # case needs handling and you want the exhaustiveness check.
3
6
  import std/Int
4
7
  import std/Float
5
8
  import std/Str
@@ -20,18 +23,13 @@ fun describeBool(b: Bool) -> Int =
20
23
  true => 1
21
24
  false => 0
22
25
 
23
- fun bindExample(n: Int) -> Int =
24
- match n
25
- x => x
26
-
27
26
  fun describeColor(c: Color) -> Str =
28
27
  match c
29
28
  Red => "red"
30
29
  Green => "green"
31
30
  Blue => "blue"
32
31
 
33
- # A dedicated, non-generic "maybe an Int", as opposed to the generic
32
+ # A non-generic "maybe an Int", next to the generic `Reading[T]` below.
34
- # generic `Reading[T]` (used elsewhere in this file).
35
33
  enum IntOpt = Present | Absent
36
34
 
37
35
  record Present(v: Int)
@@ -41,11 +39,6 @@ fun describeOption(opt: IntOpt) -> Int =
41
39
  Present(v) => v
42
40
  Absent => 0
43
41
 
44
- fun main() -> Int =
45
- describeOption(Present(5))
46
-
47
- # ---- pattern matching regression tests ----
48
-
49
42
  enum Nested = Wrap | Empty
50
43
 
51
44
  record Wrap(v: IntOpt)
@@ -84,25 +77,12 @@ fun unwrapTwice(o: RecOption) -> Int =
84
77
  RNone => 3
85
78
  _ => 0
86
79
 
87
- fun classifyForWildcardTest(a: Int) -> Int =
88
- match a
89
- 1 => 100
90
- 2 => 200
91
- _ => 0
92
-
93
- fun colorCode(c: Color) -> Int =
94
- match c
95
- Red => 1
96
- Green => 2
97
- Blue => 3
98
-
99
80
  fun isSome(o: Reading[Int]) -> Int =
100
81
  match o
101
82
  Sample(_) => 1
102
83
  NoSignal => 0
103
84
 
104
- # Mirrors libs/std/bool.plum's `and`/`or`: `match a, b` against two Bool
105
- # subjects, each case naming a tag pattern per position.
85
+ # `match a, b` against two subjects, each case giving one pattern per position.
106
86
  fun and(a: Bool, b: Bool) -> Bool =
107
87
  match a, b
108
88
  true, true => true
@@ -146,37 +126,143 @@ record Point(x: Int, y: Int) =
146
126
  Point(x, 0) => "on x axis"
147
127
  Point(_, _) => "elsewhere"
148
128
 
149
- enum Shape =
150
- | Circle
151
- | Square
152
-
153
- fun measure(self) -> Int =
154
- match self
155
- Circle(Point(x, y)) => x + y
156
- Square(Point(x, y)) => x * y
157
-
158
- record Circle(v: Point)
159
- record Square(v: Point)
160
-
161
- # Named payload fields (`Ring(radius: Int)`) alongside the
162
- # unnamed-positional-payload form (`Circle(Point)` above) — lets a
163
- # multi-field variant like `Rect` name each field instead of leaving them as
164
- # anonymous positional types. Pattern matching is unaffected either way
165
- # (still positional, by declaration order — `Rect(w, h)`, not `Rect(w:, h:)`).
166
- enum Figure =
167
- | Ring
168
- | Rect
169
-
170
- fun area(self) -> Int =
171
- match self
172
- Ring(radius) => radius * radius
173
- Rect(w, h) => w * h
174
-
175
- record Ring(radius: Int)
176
- record Rect(w: Int, h: Int)
177
-
178
- test "match example computes correctly"
179
- assert main() == 5
129
+ enum Placed = Disc | Plate
130
+
131
+ record Disc(v: Point)
132
+
133
+ record Plate(v: Point)
134
+
135
+ fun measure(p: Placed) -> Int =
136
+ match p
137
+ Disc(Point(x, y)) => x + y
138
+ Plate(Point(x, y)) => x * y
139
+
140
+ # ---- pattern `if` ----
141
+ # Pattern `if`: `if Pattern := expr` binds the pattern's names for that branch.
142
+ #
143
+ # if Circle(r) := shape # one case, bindings scoped to this branch
144
+ # if Rect(w, h) := shape, w > h # `, cond` also sees the bindings
145
+ # while Busy(t) := worker.state # loops while the pattern matches
146
+ # if Circle(r) := s then r else 0.0
147
+ #
148
+ # A plain name over an optional (`x := opt`) keeps its meaning: bind when present.
149
+
150
+ enum Shape = Circle | Rect | Dot
151
+
152
+ record Circle(radius: Float)
153
+ record Rect(w: Float, h: Float)
154
+
155
+ struct Vec2(x: Int, y: Int)
156
+
157
+ enum Job = Idle | Busy
158
+
159
+ record Busy(left: Int)
160
+
161
+ record Soldier(hp: Int, shape: Shape)
162
+
163
+ fun area(s: Shape) -> Float =
164
+ if Circle(r) := s
165
+ return 3.0 * r * r
166
+ else if Rect(w, h) := s
167
+ return w * h
168
+ 0.0
169
+
170
+ fun wide(s: Shape) -> Str =
171
+ if Rect(w, h) := s, w > h
172
+ "wide"
173
+ else if Rect(w, h) := s, h > w
174
+ "tall"
175
+ else
176
+ "other"
177
+
178
+ fun pick(s: Shape, o: Int?, n: Int) -> Str =
179
+ if Circle(r) := s
180
+ "circle"
181
+ else if v := o
182
+ "opt {v}"
183
+ else if n > 10
184
+ "big"
185
+ else if Rect(w, h) := s
186
+ "rect"
187
+ else
188
+ "none"
189
+
190
+ fun radiusOr(s: Shape) -> Float =
191
+ if Circle(r) := s then r else 0.0
192
+
193
+ fun shapeOf(u: Soldier) -> Str =
194
+ if Soldier(hp, Circle(r)) := u, hp > 0
195
+ "live circle {hp}"
196
+ else
197
+ "other"
198
+
199
+ fun countDown(j: Job) -> Int =
200
+ steps := 0
201
+ state := j
202
+ while Busy(n) := state
203
+ steps = steps + 1
204
+ if n > 1
205
+ state = Busy(left: n - 1)
206
+ else
207
+ state = Idle
208
+ steps
209
+
210
+ fun sumXY(v: Vec2) -> Int =
211
+ if Vec2(x, y) := v
212
+ return x + y
213
+ 0
214
+
215
+ fun parseSmall(s: Str) -> error{TooBig, NotNum}!Int =
216
+ n := Int.parse(s) catch return error.NotNum
217
+ if n > 9
218
+ return error.TooBig
219
+ n
220
+
221
+ fun describe(s: Str) -> Str =
222
+ n := parseSmall(s) catch e
223
+ if TooBig := e
224
+ return "too big"
225
+ return "not a number"
226
+ "ok {n}"
227
+
228
+ fun firstSome(a: Int?, b: Int?) -> Int =
229
+ if x := a
230
+ return x
231
+ else if y := b
232
+ return y + 1
233
+ -1
234
+
235
+ fun bigOnly(a: Int?) -> Int =
236
+ if x := a, x > 3
237
+ return x
238
+ 0
239
+
240
+ # ---- guards: `when <expr>` ----
241
+
242
+ enum Rating = Good | Bad
243
+
244
+ record Good(name: Str)
245
+ record Bad(name: Str)
246
+
247
+ enum Book = FantasyBook | ScifiBook | ChildrensTaleBook | NonFictionBook
248
+
249
+ record FantasyBook(title: Str, pages: Int, hasMythicalCreatures: Bool)
250
+ record ScifiBook(title: Str, pages: Int, theme: Str)
251
+ record ChildrensTaleBook(title: Str, pages: Int, moralLesson: Str)
252
+ record NonFictionBook(title: Str, pages: Int, rating1: Rating, rating2: Rating)
253
+
254
+ fun bookInfo(book: Book) -> Str =
255
+ match book
256
+ FantasyBook(title, _, hasMythicalCreatures) when hasMythicalCreatures => "Fantasy book \"{title}\" features mythical creatures"
257
+ FantasyBook(title, _, _) => "Fantasy book \"{title}\" has no mythical creatures"
258
+ ScifiBook(title, _, theme) when theme == "space exploration" => "Scifi book \"{title}\" explores space exploration"
259
+ ScifiBook(title, _, theme) => "Scifi book \"{title}\" has theme {theme}"
260
+ ChildrensTaleBook(title, pages, _) when pages == 0 => "Childrens book \"{title}\" is empty"
261
+ ChildrensTaleBook(title, _, moralLesson) => "Childrens book \"{title}\" teaches: {moralLesson}"
262
+ NonFictionBook(title, _, rating1, rating2) =>
263
+ match rating1, rating2
264
+ Good(name1), Good(name2) => "Non-fiction book \"{title}\" praised by {name1} and {name2}"
265
+ _, _ => "Non-fiction book \"{title}\" has mixed reviews"
180
266
 
181
267
  test "nested constructor pattern matches and binds runs correctly"
182
268
  assert unwrapNested(Wrap(Present(5))) == 5
@@ -190,20 +276,18 @@ test "nested constructor pattern against a specialized generic enum runs correct
190
276
  test "doubly nested constructor pattern runs correctly"
191
277
  assert unwrapTwice(RSome(RSome(RNone))) == 1
192
278
 
193
- test "match int and wildcard run correctly"
279
+ test "match literals, bool and wildcard"
194
- assert classifyForWildcardTest(2) == 200
195
-
196
- test "match bool variant pattern runs correctly"
197
280
  assert describeBool(false) == 0
198
-
199
- test "non bool bare tag pattern runs correctly"
281
+ assert describeNumber(0) == "zero"
200
- assert colorCode(Green) == 2
282
+ assert describeNumber(1) == "one"
283
+ assert describeNumber(2) == "many"
201
284
 
202
285
  test "constructor pattern wildcard field runs correctly"
203
286
  assert isSome(Sample(99)) == 1
204
-
287
+ assert isSome(NoSignal) == 0
205
- test "constructor pattern does not misfire on payload free sibling"
288
+ assert describeOption(Present(5)) == 5
206
289
  assert describeOption(Absent) == 0
290
+ assert describeColor(Green) == "green"
207
291
 
208
292
  test "multi subject match with enum tags runs correctly"
209
293
  assert andOrCheck() == 1
@@ -229,16 +313,8 @@ test "match on a plain class supports literal/wildcard sub-patterns and case fal
229
313
  assert Point(x: 5, y: 5).classify() == "elsewhere"
230
314
 
231
315
  test "a plain class nested inside an enum variant pattern destructures correctly"
232
- assert Circle(Point(x: 3, y: 4)).measure() == 7
316
+ assert measure(Disc(Point(x: 3, y: 4))) == 7
233
- assert Square(Point(x: 3, y: 4)).measure() == 12
317
+ assert measure(Plate(Point(x: 3, y: 4))) == 12
234
-
235
- test "named-payload enum variant constructs via named args and destructures by position"
236
- assert Ring(radius: 5).area() == 25
237
- assert Rect(w: 3, h: 4).area() == 12
238
-
239
- test "named-payload enum variant still supports positional construction too"
240
- assert Ring(5).area() == 25
241
- assert Rect(3, 4).area() == 12
242
318
 
243
319
  test "a pattern name reused with different types in one fn"
244
320
  r := Sample(1)
@@ -253,3 +329,76 @@ test "a pattern name reused with different types in one fn"
253
329
  assert v == "z"
254
330
  _ =>
255
331
  assert false
332
+
333
+ test "if_pattern_enum_variant"
334
+ assert area(Circle(radius: 2.0)) == 12.0
335
+ assert area(Rect(w: 2.0, h: 3.0)) == 6.0
336
+ assert area(Dot) == 0.0
337
+
338
+ test "else_if_pattern_chain"
339
+ assert pick(Circle(radius: 1.0), null, 0) == "circle"
340
+ assert pick(Dot, 4, 0) == "opt 4"
341
+ assert pick(Dot, null, 11) == "big"
342
+ assert pick(Rect(w: 1.0, h: 1.0), null, 0) == "rect"
343
+ assert pick(Dot, null, 0) == "none"
344
+
345
+ test "pattern_with_trailing_condition"
346
+ assert wide(Rect(w: 3.0, h: 1.0)) == "wide"
347
+ assert wide(Rect(w: 1.0, h: 3.0)) == "tall"
348
+ assert wide(Rect(w: 1.0, h: 1.0)) == "other"
349
+ assert wide(Dot) == "other"
350
+
351
+ test "nested_pattern_if"
352
+ assert shapeOf(Soldier(hp: 5, shape: Circle(radius: 1.0))) == "live circle 5"
353
+ assert shapeOf(Soldier(hp: 0, shape: Circle(radius: 1.0))) == "other"
354
+ assert shapeOf(Soldier(hp: 5, shape: Dot)) == "other"
355
+
356
+ test "inline_pattern_if"
357
+ assert radiusOr(Circle(radius: 2.5)) == 2.5
358
+ assert radiusOr(Dot) == 0.0
359
+
360
+ test "while_pattern"
361
+ assert countDown(Busy(left: 3)) == 3
362
+ assert countDown(Idle) == 0
363
+
364
+ test "else_if_optional_binding"
365
+ assert firstSome(null, 7) == 8
366
+ assert firstSome(2, 7) == 2
367
+ assert firstSome(null, null) == -1
368
+
369
+ test "irrefutable_pattern_if_allowed"
370
+ assert sumXY(Vec2(x: 3, y: 4)) == 7
371
+
372
+ test "error_name_pattern_in_catch_block"
373
+ assert describe("5") == "ok 5"
374
+ assert describe("50") == "too big"
375
+ assert describe("x") == "not a number"
376
+
377
+ test "optional_binding_with_trailing_condition"
378
+ assert bigOnly(5) == 5
379
+ assert bigOnly(2) == 0
380
+ assert bigOnly(null) == 0
381
+
382
+ test "when guard picks between two cases of the same member"
383
+ mythic := FantasyBook(title: "Dragon Tales", pages: 200, hasMythicalCreatures: true)
384
+ plain := FantasyBook(title: "Plain Fantasy", pages: 150, hasMythicalCreatures: false)
385
+ assert bookInfo(mythic) == "Fantasy book \"Dragon Tales\" features mythical creatures"
386
+ assert bookInfo(plain) == "Fantasy book \"Plain Fantasy\" has no mythical creatures"
387
+
388
+ test "a false guard falls through to the next case"
389
+ space := ScifiBook(title: "Starbound", pages: 300, theme: "space exploration")
390
+ other := ScifiBook(title: "Timeloop", pages: 250, theme: "time travel")
391
+ assert bookInfo(space) == "Scifi book \"Starbound\" explores space exploration"
392
+ assert bookInfo(other) == "Scifi book \"Timeloop\" has theme time travel"
393
+
394
+ test "a guard can test a field of the matched member"
395
+ empty := ChildrensTaleBook(title: "Nothing", pages: 0, moralLesson: "none")
396
+ tale := ChildrensTaleBook(title: "The Tortoise", pages: 40, moralLesson: "patience wins")
397
+ assert bookInfo(empty) == "Childrens book \"Nothing\" is empty"
398
+ assert bookInfo(tale) == "Childrens book \"The Tortoise\" teaches: patience wins"
399
+
400
+ test "nested constructor patterns and a nested multi-subject match"
401
+ good := NonFictionBook(title: "Hidden Facts", pages: 120, rating1: Good(name: "Alice"), rating2: Good(name: "Bob"))
402
+ mixed := NonFictionBook(title: "Shaky Claims", pages: 80, rating1: Good(name: "Alice"), rating2: Bad(name: "Carol"))
403
+ assert bookInfo(good) == "Non-fiction book \"Hidden Facts\" praised by Alice and Bob"
404
+ assert bookInfo(mixed) == "Non-fiction book \"Shaky Claims\" has mixed reviews"
plum-examples/methods.plum DELETED
@@ -1,31 +0,0 @@
1
- # Methods: defining methods inside type bodies with `self`, accessing and
2
- # mutating fields, and calling methods on instances.
3
- import std/Str
4
- import std/Int
5
- import std/Float
6
-
7
- record Cat(name: Str, age: Int) =
8
-
9
- fun getAge(self) -> Int =
10
- self.age
11
-
12
- fun birthday(self) -> Int =
13
- self.age + 1
14
-
15
- record Wrapper(inner: Cat, tag: Int) =
16
-
17
- fun innerAge(self) -> Int =
18
- self.inner.age
19
-
20
- fun makeCat() -> Cat =
21
- Cat(name: "Whiskers", age: 3)
22
-
23
- fun main() -> Int =
24
- c := makeCat()
25
- a := c.getAge()
26
- b := c.birthday()
27
- w := Wrapper(inner: c, tag: 1)
28
- a + b + w.innerAge()
29
-
30
- test "methods example computes correctly"
31
- assert main() == 10
plum-examples/oop_visitor.plum DELETED
@@ -1,117 +0,0 @@
1
- # Classic OOP alternative to the DOP version in `dop_visitor.plum`: instead of
2
- # one sealed `enum` + one exhaustive `match`, each book shape gets its own
3
- # single-variant `enum` and defines its own `collectInterestingInfo` method —
4
- # the direct translation of the blog post's Java (sealed interface implemented
5
- # by records, each overriding a method).
6
- # Based on: https://wimdetroyer.com/blog/visitor-pattern-in-dop
7
- #
8
- # One real limitation this example runs into: plum has no interfaces and no
9
- # runtime dispatch. Generics are duck-typed per instantiation, so a function
10
- # can't take a bare `book: Book` parameter and call
11
- # `book.collectInterestingInfo()` polymorphically across different concrete
12
- # types, and there's no way to hold a heterogeneous list of `Book`s. So
13
- # unlike the Java original (or the enum/match version), there's no single
14
- # `describe(book: Book)` entry point here — each concrete type's method is
15
- # called directly at its own call site.
16
- #
17
- # OOP vs. DOP (see the `enum`/`match` version in `dop_visitor.plum`):
18
- # - Adding a new variant (a new single-variant `enum`) is self-contained
19
- # here and doesn't touch existing types. In the DOP version it means
20
- # adding a new arm to every `match` on `Book` (caught by the checker's
21
- # exhaustiveness check if missed).
22
- # - Adding a new operation instead means adding a method to every shape
23
- # enum here, with nothing enforcing that you updated all of them; the DOP
24
- # version adds one new function with one exhaustive `match`.
25
- # - So OOP fits best when shapes multiply and operations are stable; DOP
26
- # fits best when operations change often and the shapes are stable.
27
- # plum's lack of dynamic dispatch through an interface (see above)
28
- # removes OOP's usual "call through one interface" benefit entirely,
29
- # which is a real cost specific to this language.
30
-
31
- import std/Os
32
- import std/Int
33
- import std/Float
34
- import std/Str
35
-
36
- enum Rating = Good | Bad
37
-
38
- record Good(name: Str)
39
- record Bad(name: Str)
40
-
41
- record FantasyBook(title: Str, pages: Int, hasMythicalCreatures: Bool) =
42
-
43
- fun collectInterestingInfo(self) -> Str =
44
- if self.hasMythicalCreatures
45
- "Fantasy book \"{self.title}\" features mythical creatures"
46
- else
47
- "Fantasy book \"{self.title}\" has no mythical creatures"
48
-
49
- record ScifiBook(title: Str, pages: Int, theme: Str) =
50
-
51
- fun collectInterestingInfo(self) -> Str =
52
- if self.theme == "space exploration"
53
- "Scifi book \"{self.title}\" explores space exploration"
54
- else
55
- "Scifi book \"{self.title}\" has theme {self.theme}"
56
-
57
- record ChildrensTaleBook(title: Str, pages: Int, moralLesson: Str) =
58
-
59
- fun collectInterestingInfo(self) -> Str =
60
- if self.pages == 0
61
- "Childrens book \"{self.title}\" is empty"
62
- else
63
- "Childrens book \"{self.title}\" teaches: {self.moralLesson}"
64
-
65
- record NonFictionBook(title: Str, pages: Int, rating1: Rating, rating2: Rating) =
66
-
67
- fun collectInterestingInfo(self) -> Str =
68
- match self.rating1, self.rating2
69
- Good(name1), Good(name2) => "Non-fiction book \"{self.title}\" praised by {name1} and {name2}"
70
- _, _ => "Non-fiction book \"{self.title}\" has mixed reviews"
71
-
72
- fun describeFantasy() -> Str =
73
- FantasyBook(title: "Dragon Tales", pages: 200, hasMythicalCreatures: true).collectInterestingInfo()
74
-
75
- fun main() =
76
- printLn(describeFantasy())
77
-
78
- test "fantasy book with mythical creatures is called out"
79
- assert describeFantasy() == "Fantasy book \"Dragon Tales\" features mythical creatures"
80
-
81
- test "fantasy book without mythical creatures is called out"
82
- book := FantasyBook(title: "Plain Fantasy", pages: 150, hasMythicalCreatures: false)
83
- assert book.collectInterestingInfo() == "Fantasy book \"Plain Fantasy\" has no mythical creatures"
84
-
85
- test "scifi book with space exploration theme is called out"
86
- book := ScifiBook(title: "Starbound", pages: 300, theme: "space exploration")
87
- assert book.collectInterestingInfo() == "Scifi book \"Starbound\" explores space exploration"
88
-
89
- test "scifi book with other theme falls through to generic case"
90
- book := ScifiBook(title: "Timeloop", pages: 250, theme: "time travel")
91
- assert book.collectInterestingInfo() == "Scifi book \"Timeloop\" has theme time travel"
92
-
93
- test "empty childrens book is handled via a plain if on pages"
94
- book := ChildrensTaleBook(title: "Nothing", pages: 0, moralLesson: "none")
95
- assert book.collectInterestingInfo() == "Childrens book \"Nothing\" is empty"
96
-
97
- test "childrens book with pages reports its moral lesson"
98
- book := ChildrensTaleBook(title: "The Tortoise", pages: 40, moralLesson: "patience wins")
99
- assert book.collectInterestingInfo() == "Childrens book \"The Tortoise\" teaches: patience wins"
100
-
101
- test "non fiction book praised by both reviewers via nested match"
102
- book := NonFictionBook(
103
- title: "Hidden Facts",
104
- pages: 120,
105
- rating1: Good(name: "Alice"),
106
- rating2: Good(name: "Bob"),
107
- )
108
- assert book.collectInterestingInfo() == "Non-fiction book \"Hidden Facts\" praised by Alice and Bob"
109
-
110
- test "non fiction book with a bad rating reports mixed reviews"
111
- book := NonFictionBook(
112
- title: "Shaky Claims",
113
- pages: 80,
114
- rating1: Good(name: "Alice"),
115
- rating2: Bad(name: "Carol"),
116
- )
117
- assert book.collectInterestingInfo() == "Non-fiction book \"Shaky Claims\" has mixed reviews"
plum-examples/optionals.plum CHANGED
@@ -1,10 +1,16 @@
1
1
  import std/Int
2
2
  import std/Str
3
3
  import std/List
4
+ import std/Float
4
5
  # Optionals: `T?` is a `T` or `null`. A plain `T` in a `T?` slot is the present
5
6
  # value. An optional is handled with `?:` (Kotlin elvis), `if x := opt`,
6
7
  # `while x := opt`, `opt!` (crashes on null) and `opt == null`; `catch` and `try`
7
8
  # are for errors only. `!T?` is an error union whose success value is an optional.
9
+ #
10
+ # `obj?.field` / `obj?.method(args)` is safe navigation: it reaches into an
11
+ # optional without unwrapping it and passes null through. A field that is itself
12
+ # a `T?` flattens, so `u?.target?.name` is one `Str?`. It is sugar for
13
+ # `obj.andThen(|v| v.field)`.
8
14
 
9
15
  record Box(label: Str)
10
16
 
@@ -56,6 +62,39 @@ fun sparse() -> List[Int?] =
56
62
  fun sumBig() -> Int =
57
63
  Int.MAX + Int.MIN
58
64
 
65
+ record Person(name: Str, age: Int) =
66
+
67
+ fun birthday(self) -> Int =
68
+ self.age + 1
69
+
70
+ record Target(name: Str)
71
+
72
+ record User(name: Str, target: Target?)
73
+
74
+ fun personName(p: Person?) -> Str? =
75
+ p?.name
76
+
77
+ fun personBirthday(p: Person?) -> Int? =
78
+ p?.birthday()
79
+
80
+ fun somePerson() -> Person? =
81
+ Person(name: "Ann", age: 3)
82
+
83
+ fun noPerson() -> Person? =
84
+ return null
85
+
86
+ fun noTarget() -> Target? =
87
+ return null
88
+
89
+ fun maybeCount(n: Int? = null) -> Int? =
90
+ n
91
+
92
+ fun noUser() -> User? =
93
+ return null
94
+
95
+ fun targetName(u: User?) -> Str? =
96
+ u?.target?.name
97
+
59
98
  test "null_return_absent"
60
99
  assert maybeBox(false) == null
61
100
  assert maybeBox(true) != null
@@ -160,3 +199,34 @@ test "reassigning an optional local or field to null or a value"
160
199
  h.item = Box(label: "y")
161
200
  assert h.count! == 5
162
201
  assert h.item!.label == "y"
202
+
203
+ test "safe navigation maps a field access over a present value, passes absence through"
204
+ assert personName(somePerson())! == "Ann"
205
+ assert {personName(noPerson()) ?: "gone"} == "gone"
206
+
207
+ test "safe navigation maps a method call over a present value"
208
+ assert personBirthday(somePerson())! == 4
209
+ assert {personBirthday(noPerson()) ?: -1} == -1
210
+
211
+ test "safe navigation chains through a nullable field"
212
+ full := User(name: "u", target: Target(name: "home"))
213
+ assert {targetName(full) ?: "none"} == "home"
214
+ assert {targetName(User(name: "v", target: noTarget())) ?: "none"} == "none"
215
+ assert {targetName(noUser()) ?: "none"} == "none"
216
+
217
+ test "?: handles a nullable with a diverging or computed default"
218
+ p := somePerson() ?: crash "missing"
219
+ assert p.age == 3
220
+ q := noPerson() ?: Person(name: "gone", age: 1)
221
+ assert q.name == "gone"
222
+
223
+ test "null is the absent value in an argument, a list element and a default"
224
+ u := User(name: "w", target: null)
225
+ assert {targetName(u) ?: "none"} == "none"
226
+ xs := sparse()
227
+ second := xs.get(1)!
228
+ assert {second ?: 9} == 9
229
+ first := xs.get(0)!
230
+ assert first! == 1
231
+ assert {maybeCount() ?: 5} == 5
232
+ assert maybeCount(7)! == 7
plum-examples/pattern_if.plum DELETED
@@ -1,153 +0,0 @@
1
- import std/Int
2
- import std/Float
3
- import std/Str
4
- # Pattern `if`: `if Pattern := expr` tests one case of an enum (or a literal, an
5
- # error name, a nested pattern) and binds the pattern's names for that branch.
6
- # It reads like an ordinary `if`/`else if`/`else` chain. Use `match` when every
7
- # case needs handling and you want the exhaustiveness check.
8
- #
9
- # if Circle(r) := shape # one case, bindings scoped to this branch
10
- # if Rect(w, h) := shape, w > h # `, cond` also sees the bindings
11
- # while Busy(t) := worker.state # loops while the pattern matches
12
- # if Circle(r) := s then r else 0.0
13
- #
14
- # A plain name over an optional (`x := opt`) keeps its meaning: bind when present.
15
-
16
- enum Shape = Circle | Rect | Dot
17
-
18
- record Circle(radius: Float)
19
- record Rect(w: Float, h: Float)
20
-
21
- struct Vec2(x: Int, y: Int)
22
-
23
- enum Job = Idle | Busy
24
-
25
- record Busy(left: Int)
26
-
27
- record Soldier(hp: Int, shape: Shape)
28
-
29
- fun area(s: Shape) -> Float =
30
- if Circle(r) := s
31
- return 3.0 * r * r
32
- else if Rect(w, h) := s
33
- return w * h
34
- 0.0
35
-
36
- fun wide(s: Shape) -> Str =
37
- if Rect(w, h) := s, w > h
38
- "wide"
39
- else if Rect(w, h) := s, h > w
40
- "tall"
41
- else
42
- "other"
43
-
44
- fun pick(s: Shape, o: Int?, n: Int) -> Str =
45
- if Circle(r) := s
46
- "circle"
47
- else if v := o
48
- "opt {v}"
49
- else if n > 10
50
- "big"
51
- else if Rect(w, h) := s
52
- "rect"
53
- else
54
- "none"
55
-
56
- fun radiusOr(s: Shape) -> Float =
57
- if Circle(r) := s then r else 0.0
58
-
59
- fun shapeOf(u: Soldier) -> Str =
60
- if Soldier(hp, Circle(r)) := u, hp > 0
61
- "live circle {hp}"
62
- else
63
- "other"
64
-
65
- fun countDown(j: Job) -> Int =
66
- steps := 0
67
- state := j
68
- while Busy(n) := state
69
- steps = steps + 1
70
- if n > 1
71
- state = Busy(left: n - 1)
72
- else
73
- state = Idle
74
- steps
75
-
76
- fun sumXY(v: Vec2) -> Int =
77
- if Vec2(x, y) := v
78
- return x + y
79
- 0
80
-
81
- fun parseSmall(s: Str) -> error{TooBig, NotNum}!Int =
82
- n := Int.parse(s) catch return error.NotNum
83
- if n > 9
84
- return error.TooBig
85
- n
86
-
87
- fun describe(s: Str) -> Str =
88
- n := parseSmall(s) catch e
89
- if TooBig := e
90
- return "too big"
91
- return "not a number"
92
- "ok {n}"
93
-
94
- fun firstSome(a: Int?, b: Int?) -> Int =
95
- if x := a
96
- return x
97
- else if y := b
98
- return y + 1
99
- -1
100
-
101
- fun bigOnly(a: Int?) -> Int =
102
- if x := a, x > 3
103
- return x
104
- 0
105
-
106
- test "if_pattern_enum_variant"
107
- assert area(Circle(radius: 2.0)) == 12.0
108
- assert area(Rect(w: 2.0, h: 3.0)) == 6.0
109
- assert area(Dot) == 0.0
110
-
111
- test "else_if_pattern_chain"
112
- assert pick(Circle(radius: 1.0), null, 0) == "circle"
113
- assert pick(Dot, 4, 0) == "opt 4"
114
- assert pick(Dot, null, 11) == "big"
115
- assert pick(Rect(w: 1.0, h: 1.0), null, 0) == "rect"
116
- assert pick(Dot, null, 0) == "none"
117
-
118
- test "pattern_with_trailing_condition"
119
- assert wide(Rect(w: 3.0, h: 1.0)) == "wide"
120
- assert wide(Rect(w: 1.0, h: 3.0)) == "tall"
121
- assert wide(Rect(w: 1.0, h: 1.0)) == "other"
122
- assert wide(Dot) == "other"
123
-
124
- test "nested_pattern_if"
125
- assert shapeOf(Soldier(hp: 5, shape: Circle(radius: 1.0))) == "live circle 5"
126
- assert shapeOf(Soldier(hp: 0, shape: Circle(radius: 1.0))) == "other"
127
- assert shapeOf(Soldier(hp: 5, shape: Dot)) == "other"
128
-
129
- test "inline_pattern_if"
130
- assert radiusOr(Circle(radius: 2.5)) == 2.5
131
- assert radiusOr(Dot) == 0.0
132
-
133
- test "while_pattern"
134
- assert countDown(Busy(left: 3)) == 3
135
- assert countDown(Idle) == 0
136
-
137
- test "else_if_optional_binding"
138
- assert firstSome(null, 7) == 8
139
- assert firstSome(2, 7) == 2
140
- assert firstSome(null, null) == -1
141
-
142
- test "irrefutable_pattern_if_allowed"
143
- assert sumXY(Vec2(x: 3, y: 4)) == 7
144
-
145
- test "error_name_pattern_in_catch_block"
146
- assert describe("5") == "ok 5"
147
- assert describe("50") == "too big"
148
- assert describe("x") == "not a number"
149
-
150
- test "optional_binding_with_trailing_condition"
151
- assert bigOnly(5) == 5
152
- assert bigOnly(2) == 0
153
- assert bigOnly(null) == 0
plum-examples/safe_navigation.plum DELETED
@@ -1,91 +0,0 @@
1
- import std/Int
2
- import std/Float
3
- import std/Str
4
- import std/List
5
- # `T?` is an optional: a `T` or `null`. A plain `T` where a `T?` is expected is
6
- # the present value. For a reference type (class, enum, Str, List, ...) a `T?` is
7
- # stored as a nullable reference, so a present value costs no allocation. An
8
- # optional is handled with `?:`, `!`, `if x := opt` and `== null`; `catch` and
9
- # `try` are for errors only.
10
- #
11
- # `null` is a plain VALUE wherever a `T?` is expected (a call or constructor
12
- # argument, a list element, a default, `return null`).
13
- #
14
- # `obj?.field` / `obj?.method(args)` -- Groovy/Kotlin-style safe navigation.
15
- # Reaches into an optional's value without unwrapping it and re-wraps the
16
- # result; null passes through untouched. A field that is itself a `T?`
17
- # flattens, so `u?.target?.name` is one `Str?`, not a nested one.
18
- # Pure sugar for `obj.andThen(|v| v.field)`, desugared entirely in the parser
19
- # (see `plum-core`'s `parseSafeAttribute`).
20
-
21
- record Person(name: Str, age: Int) =
22
-
23
- fun birthday(self) -> Int =
24
- self.age + 1
25
-
26
- record Target(name: Str)
27
-
28
- record User(name: Str, target: Target?)
29
-
30
- fun personName(p: Person?) -> Str? =
31
- p?.name
32
-
33
- fun personBirthday(p: Person?) -> Int? =
34
- p?.birthday()
35
-
36
- fun somePerson() -> Person? =
37
- Person(name: "Ann", age: 3)
38
-
39
- fun noPerson() -> Person? =
40
- return null
41
-
42
- fun noTarget() -> Target? =
43
- return null
44
-
45
- fun maybeCount(n: Int? = null) -> Int? =
46
- n
47
-
48
- fun sparse() -> List[Int?] =
49
- List(1, null, 3)
50
-
51
- fun noUser() -> User? =
52
- return null
53
-
54
- fun targetName(u: User?) -> Str? =
55
- u?.target?.name
56
-
57
- test "safe navigation maps a field access over a present value, passes absence through"
58
- assert personName(somePerson())! == "Ann"
59
- assert {personName(noPerson()) ?: "gone"} == "gone"
60
-
61
- test "safe navigation maps a method call over a present value"
62
- assert personBirthday(somePerson())! == 4
63
- assert {personBirthday(noPerson()) ?: -1} == -1
64
-
65
- test "safe navigation chains through a nullable field"
66
- full := User(name: "u", target: Target(name: "home"))
67
- assert {targetName(full) ?: "none"} == "home"
68
- assert {targetName(User(name: "v", target: noTarget())) ?: "none"} == "none"
69
- assert {targetName(noUser()) ?: "none"} == "none"
70
-
71
- test "?: gives a default for a missing value"
72
- assert {personName(noPerson()) ?: "nobody"} == "nobody"
73
- assert {personName(somePerson()) ?: "nobody"} == "Ann"
74
-
75
- test "?: handles a nullable with a diverging or computed default"
76
- p := somePerson() ?: crash "missing"
77
- assert p.age == 3
78
- q := noPerson() ?: Person(name: "gone", age: 1)
79
- assert q.name == "gone"
80
-
81
- test "null is the absent value in an argument, a list element and a default"
82
- u := User(name: "w", target: null)
83
- assert {targetName(u) ?: "none"} == "none"
84
- xs := sparse()
85
- assert xs.length() == 3
86
- second := xs.get(1)!
87
- assert {second ?: 9} == 9
88
- first := xs.get(0)!
89
- assert first! == 1
90
- assert {maybeCount() ?: 5} == 5
91
- assert maybeCount(7)! == 7
plum-examples/seq.plum CHANGED
@@ -1,5 +1,5 @@
1
- # Sequence adapters and terminals: using `std/Seq` methods (`take`, `skip`,
1
+ # Sequence adapters, terminals and chains: using `std/Seq` methods (`take`,
2
- # `map`, `filter`, `find`, `fold`) to process collections in `for` loops.
2
+ # `skip`, `map`, `filter`, `find`, `fold`) to process collections in `for` loops.
3
3
  import std/Int
4
4
  import std/Float
5
5
  import std/List
@@ -13,6 +13,40 @@ import std/Str
13
13
  # count, any, all, each, firstN, toList) take the `for` body and end the chain.
14
14
  # A whole chain is ONE loop: no closure, no intermediate List.
15
15
 
16
+ # Closure-free chains. A method chain such as
17
+ #
18
+ # keep my team's units, take their hp, sum it
19
+ #
20
+ # becomes ONE `for` over the sequence:
21
+ #
22
+ # * per-element stages are header clauses: filter = `, if cond`,
23
+ # map = `, name := expr`;
24
+ # * the terminal step is the outer form plus its body (fold, map, find, count,
25
+ # any, all);
26
+ # * body-less stages nest as `fun*` sources (take, skip, zip, reversed,
27
+ # chunks, stepBy), and always fuse into the same single loop;
28
+ # * materializing stages (sort, group) are separate statements.
29
+ #
30
+ # No closure is allocated and there is no call_indirect: the wasm is the loop you
31
+ # would have written by hand. See plum-std/Seq.plum.
32
+
33
+ record Soldier(name: Str, team: Int, hp: Float, x: Float, y: Float)
34
+
35
+ fun dist2(ax: Float, ay: Float, bx: Float, by: Float) -> Float =
36
+ dx := ax - bx
37
+ dy := ay - by
38
+ dx * dx + dy * dy
39
+
40
+ fun army() -> List[Soldier] =
41
+ List(
42
+ Soldier(name: "a", team: 1, hp: 10.0, x: 0.0, y: 0.0),
43
+ Soldier(name: "b", team: 2, hp: 0.0, x: 1.0, y: 1.0),
44
+ Soldier(name: "c", team: 1, hp: 7.0, x: 5.0, y: 5.0),
45
+ Soldier(name: "d", team: 2, hp: 3.0, x: 2.0, y: 0.0),
46
+ Soldier(name: "e", team: 1, hp: 4.0, x: 9.0, y: 9.0),
47
+ Soldier(name: "f", team: 2, hp: 8.0, x: 1.0, y: 0.0),
48
+ )
49
+
16
50
  test "seq_method_filter: a List has no filter of its own, so Seq.filter is used"
17
51
  xs := List(1, 2, 3, 4, 5, 6)
18
52
  evens := for x := range xs.filter()
@@ -144,3 +178,59 @@ test "range_map_entries: two binders over a Map"
144
178
  for k, v := range m
145
179
  total = total + k * v
146
180
  assert total == 50
181
+
182
+ # ---- chains: several stages fused into one loop ----
183
+
184
+ test "sum of hp of my team: filter + map + sum is a fold with clauses"
185
+ units := army()
186
+ me := units.at(0)
187
+ # units.filter(team == me).map(hp).sum()
188
+ total := for acc, u := range units.fold(0.0), if u.team == me.team, hp := u.hp
189
+ acc + hp
190
+ assert total == 21.0
191
+
192
+ test "names of living units within range: filter + map + filter + map"
193
+ units := army()
194
+ me := units.at(0)
195
+ reach := 5.0
196
+ names := for u := range units.map(), if u.hp > 0.0, d := dist2(u.x, u.y, me.x, me.y), if d < reach
197
+ u.name
198
+ assert names.length() == 3
199
+ assert names.at(0) == "a"
200
+ assert names.at(1) == "d"
201
+ assert names.at(2) == "f"
202
+
203
+ test "first enemy in range: filter + find"
204
+ units := army()
205
+ me := units.at(0)
206
+ near := for u := range units.find(), if u.team != me.team
207
+ dist2(u.x, u.y, me.x, me.y) < 4.0
208
+ first := near ?: me
209
+ assert first.name == "b"
210
+
211
+ test "take + map: the first five living units"
212
+ units := army()
213
+ lines := for u := range units.take(5).map(), if u.hp > 0.0
214
+ u.name
215
+ assert lines.length() == 4
216
+ assert lines.at(0) == "a"
217
+ assert lines.at(3) == "e"
218
+
219
+ test "count and any over a chain"
220
+ units := army()
221
+ alive := for u := range units.skip(1).count(), if u.team == 2
222
+ u.hp > 0.0
223
+ assert alive == 2
224
+ anyBig := for u := range units.stepBy(2).any()
225
+ u.hp > 9.0
226
+ assert anyBig
227
+
228
+ test "materializing stages are separate statements"
229
+ units := army()
230
+ alive := for u := range units.filter()
231
+ u.hp > 0.0
232
+ assert alive.length() == 5
233
+ # a later stage runs over the materialized list
234
+ hps := for u := range alive.map(), if u.team == 1
235
+ u.hp
236
+ assert hps.length() == 3
plum-examples/shapes.plum DELETED
@@ -1,30 +0,0 @@
1
- import std/Str
2
- import std/Int
3
- import std/Float
4
-
5
- # A closed set of shapes as one enum: mixed kinds live in a single type and
6
- # `match` picks the behavior per variant.
7
- enum Shape =
8
- | Circle
9
- | Rect
10
-
11
- fun area(self) -> Float =
12
- match self
13
- Circle(r) => r * r * 3.0
14
- Rect(w, h) => w * h
15
-
16
- fun name(self) -> Str =
17
- match self
18
- Circle(r) => "circle"
19
- Rect(w, h) => "rect"
20
-
21
- record Circle(r: Float)
22
- record Rect(w: Float, h: Float)
23
-
24
- test "area dispatches on the variant"
25
- assert Circle(r: 1.5).area() == 6.75
26
- assert Rect(w: 2.0, h: 3.0).area() == 6.0
27
-
28
- test "name dispatches on the variant"
29
- assert Circle(r: 1.0).name() == "circle"
30
- assert Rect(w: 1.0, h: 1.0).name() == "rect"
plum-examples/strings.plum CHANGED
@@ -1,8 +1,9 @@
1
- # Strings: string literals, interpolation `"{var}"`, and Unicode codepoint/rune
1
+ # Strings: string literals and interpolation `"{expr}"`, which accepts any
2
- # inspection methods.
2
+ # expression (a variable, `&&`/`||`, a method call, an inline `if`).
3
3
  import std/Int
4
4
  import std/Float
5
5
  import std/Str
6
+ import std/List
6
7
 
7
8
  fun greet(name: Str) -> Str =
8
9
  "Hello, {name}!"
@@ -15,3 +16,54 @@ fun empty() -> Str =
15
16
 
16
17
  fun escaped() -> Str =
17
18
  "line one\nline two\ttabbed \"quoted\""
19
+
20
+ test "string interpolation of an int runs correctly"
21
+ x := 42
22
+ assert "{x}" == "42"
23
+
24
+
25
+ test "string interpolation of a negative int runs correctly"
26
+ x := -7
27
+ assert "{x}" == "-7"
28
+
29
+
30
+ test "string interpolation of zero runs correctly"
31
+ x := 0
32
+ assert "{x}" == "0"
33
+
34
+
35
+ test "string interpolation with surrounding text and multiple interps runs correctly"
36
+ count := 3
37
+ total := 10
38
+ assert "{count} of {total} complete" == "3 of 10 complete"
39
+
40
+
41
+ test "string interpolation of a str runs correctly"
42
+ assert greet("World") == "Hello, World!"
43
+
44
+
45
+ test "string interpolation of a bool runs correctly"
46
+ b := true
47
+ assert "is {b}" == "is true"
48
+
49
+
50
+ test "string interpolation supports a boolean expression, not just a bare variable"
51
+ c1 := true
52
+ c2 := true
53
+ c3 := false
54
+ assert "{c1 && c2}" == "true"
55
+ assert "{c1 && c3}" == "false"
56
+ assert "{c1 || c3}" == "true"
57
+
58
+
59
+ test "string interpolation supports a method call taking a closure literal"
60
+ l := List(1, 2, 3, 4)
61
+ assert "{l.findValue(3)!}" == "3"
62
+ assert "{l.contains(100)}" == "false"
63
+
64
+
65
+ test "string interpolation supports an inline if expression"
66
+ n := 5
67
+ assert "{if n > 0 then "positive" else "non-positive"}" == "positive"
68
+
69
+ # ---- Str method regression tests ----
plum-examples/{values.plum → structs_records.plum} RENAMED
@@ -1,15 +1,15 @@
1
- # Structs: small copy-semantics value types declared with `struct`,
1
+ # Structs and records: `struct` is a small copy-semantics value type (flattened
2
- # lowered without heap allocation, supporting duck-typed operators.
2
+ # into wasm locals, no heap allocation); `record` is a reference type shared on
3
+ # assignment. Both have named fields, methods, constants, positional or named
4
+ # construction, field mutation and spread update (`Point(..p, x: 1)`).
3
- module values
5
+ module structs_records
4
6
  import std/Str
5
7
  import std/Int
6
8
  import std/Float
7
- import std/List
8
9
  import std/Map
9
10
 
10
- # `struct`: one constructor named like the type, fields Int/Float/Bool or
11
- # other structs. Values are copied on assignment/pass and flattened into
11
+ # ---- struct: value semantics ----
12
- # wasm locals; they are boxed only inside generic containers.
12
+
13
13
  struct Vec2(x: Float, y: Float) =
14
14
 
15
15
  fun add(self, o: Vec2) -> Vec2 =
@@ -79,19 +79,51 @@ fun bump(v: Vec2) -> Float =
79
79
  v.x = v.x + 100.0
80
80
  v.x
81
81
 
82
+ # ---- record: reference semantics ----
83
+
84
+ record Point(x: Int, y: Int)
85
+
86
+ # `self` is implicit inside a record body, or can be written out.
87
+ record Cat(name: Str, age: Int) =
88
+
89
+ fun getAge() -> Int =
90
+ self.age
91
+
92
+ fun birthday(self) -> Int =
93
+ self.age + 1
94
+
95
+ record Wrapper(inner: Cat, tag: Int) =
96
+
97
+ fun innerAge(self) -> Int =
98
+ self.inner.age
99
+
100
+ record LoopBox(v: Int)
101
+
102
+ fun sumLoopBoxes() -> Int =
103
+ total := 0
104
+ for i := range 5
105
+ b := LoopBox(v: i)
106
+ total = total + b.v
107
+ return total
108
+
109
+ fun methodsTotal() -> Int =
110
+ c := Cat(name: "Whiskers", age: 3)
111
+ w := Wrapper(inner: c, tag: 1)
112
+ c.getAge() + c.birthday() + w.innerAge()
113
+
82
- test "value_copy_semantics"
114
+ test "struct assignment copies"
83
115
  a := Vec2(x: 1.0, y: 2.0)
84
116
  b := a
85
117
  b.x = 9.0
86
118
  assert a.x == 1.0
87
119
  assert b.x == 9.0
88
120
 
89
- test "value_param_is_a_copy"
121
+ test "struct param is a copy"
90
122
  a := Vec2(x: 1.0, y: 2.0)
91
123
  assert bump(a) == 101.0
92
124
  assert a.x == 1.0
93
125
 
94
- test "value_multi_value_return"
126
+ test "struct returns"
95
127
  v := makeVec(3.0, 4.0)
96
128
  assert v.x == 3.0
97
129
  assert v.y == 4.0
@@ -99,37 +131,27 @@ test "value_multi_value_return"
99
131
  assert s.x == 4.0
100
132
  assert s.y == 3.0
101
133
 
102
- test "value_method_calls"
134
+ test "struct methods and statics"
103
135
  v := Vec2(x: 3.0, y: 4.0)
104
136
  assert v.len2() == 25.0
105
137
  assert v.add(Vec2(x: 1.0, y: 1.0)).x == 4.0
138
+ assert Vec2.zero().x == 0.0
106
139
 
107
- test "value_in_list_roundtrip"
108
- l := List[Vec2]()
109
- l.push(Vec2(x: 1.0, y: 2.0))
110
- l.push(Vec2(x: 3.0, y: 4.0))
111
- assert l.length() == 2
112
- first := l.get(0)!
113
- second := l.get(1)!
114
- assert first.x == 1.0
115
- assert second.y == 4.0
116
-
117
- test "value_eq_fieldwise"
140
+ test "struct equality is field-wise"
118
141
  a := Vec2(x: 1.0, y: 2.0)
119
142
  assert a == Vec2(x: 1.0, y: 2.0)
120
143
  assert a != Vec2(x: 1.0, y: 3.0)
121
144
 
122
- test "value_nested_flatten"
145
+ test "nested structs flatten and copy deeply"
123
146
  r := Rect(pos: Vec2(x: 1.0, y: 2.0), size: Vec2(x: 3.0, y: 4.0))
124
147
  assert r.pos.y == 2.0
125
- assert r.size.x == 3.0
126
148
  assert r.area() == 12.0
127
149
  r2 := r
128
150
  r2.pos.x = 7.0
129
151
  assert r.pos.x == 1.0
130
152
  assert r2.pos.x == 7.0
131
153
 
132
- test "value_mixed_fields"
154
+ test "struct with Int, Bool and struct fields"
133
155
  c := Cell(pos: Vec2(x: 1.0, y: 2.0), solid: true, tint: Color(r: 1, g: 2, b: 3, a: 4))
134
156
  assert c.solid
135
157
  assert c.tint.b == 3
@@ -138,37 +160,32 @@ test "value_mixed_fields"
138
160
  assert c.tint.b == 3
139
161
  assert d.tint.b == 30
140
162
 
141
- test "value_operator_add_mul"
163
+ test "struct operators call add and mul"
142
- a := Vec2(x: 1.0, y: 2.0)
164
+ r := Vec2(x: 1.0, y: 2.0) + Vec2(x: 3.0, y: 4.0) * 2.0
143
- b := Vec2(x: 3.0, y: 4.0)
144
- r := a + b * 2.0
145
165
  assert r.x == 7.0
146
166
  assert r.y == 10.0
147
167
 
148
- test "if_and_ternary"
168
+ test "struct through if, ternary, match, variadic and closure"
149
169
  a := Vec2(x: 1.0, y: 2.0)
150
170
  b := Vec2(x: 3.0, y: 4.0)
151
171
  assert pick(true, a, b).x == 1.0
152
172
  assert pick(false, a, b).x == 3.0
153
173
  assert pickT(false, a, b).y == 4.0
154
-
155
- test "match_destructure"
156
- assert viaMatch(Vec2(x: 1.0, y: 2.0)) == 3.0
174
+ assert viaMatch(a) == 3.0
157
-
158
- test "variadic"
159
175
  assert sumAll(Vec2(x: 1.0, y: 0.0), Vec2(x: 2.0, y: 0.0)) == 3.0
160
-
161
- test "static_and_zero"
162
- z := Vec2.zero()
163
- assert z.x == 0.0
164
-
165
- test "closure_value"
166
176
  base := Vec2(x: 10.0, y: 0.0)
167
177
  r := applyTo(Vec2(x: 1.0, y: 5.0), |v| v + base)
168
178
  assert r.x == 11.0
169
179
  assert r.y == 5.0
170
180
 
171
- test "heap_field"
181
+ test "struct in a Map and an optional"
182
+ m := Map("a" => Vec2(x: 1.0, y: 2.0))
183
+ assert m.get("a")!.y == 2.0
184
+ assert opt(true)!.x == 1.0
185
+ assert chain(true)!.x == 2.0
186
+ assert {chain(false) ?: Vec2.zero()}.x == 0.0
187
+
188
+ test "struct field of a record is copied out"
172
189
  p := Player(pos: Vec2(x: 1.0, y: 2.0), hp: 3)
173
190
  q := p.pos
174
191
  q.x = 50.0
@@ -177,12 +194,48 @@ test "heap_field"
177
194
  assert p.pos.x == 50.0
178
195
  assert p.hp == 3
179
196
 
197
+ test "record assignment shares the object"
198
+ p := Point(x: 1, y: 2)
199
+ q := p
200
+ q.x = 5
201
+ assert p.x == 5
202
+
180
- test "map_value"
203
+ test "record field mutation and spread update"
181
- m := Map("a" => Vec2(x: 1.0, y: 2.0))
204
+ p := Point(x: 1, y: 2)
205
+ p.x = 10
206
+ assert p.x == 10
182
- assert m.get("a")!.y == 2.0
207
+ assert p.y == 2
183
-
184
- test "nullable"
208
+ p2 := Point(..p, x: 100)
185
- assert opt(true)!.x == 1.0
209
+ assert p2.x == 100
210
+ assert p2.y == 2
211
+ # the spread source is untouched by the update it fed
212
+ assert p.x == 10
213
+
214
+ test "struct field mutation and spread update"
215
+ v := Vec2(x: 1.0, y: 2.0)
216
+ v.x = 10.0
217
+ v2 := Vec2(..v, x: 100.0)
218
+ assert v2.x == 100.0
186
- assert chain(true)!.x == 2.0
219
+ assert v2.y == 2.0
187
- z := chain(false) ?: Vec2.zero()
188
- assert z.x == 0.0
220
+ assert v.x == 10.0
221
+
222
+ test "record construction is named or positional"
223
+ assert Point(x: 3, y: 4).y == 4
224
+ assert Point(3, 4).y == 4
225
+
226
+ test "record methods, with and without an explicit self"
227
+ c := Cat(name: "x", age: 7)
228
+ assert c.getAge() == 7
229
+ assert c.birthday() == 8
230
+ assert methodsTotal() == 3 + 4 + 3
231
+
232
+ test "nested record field access"
233
+ w := Wrapper(inner: Cat(name: "x", age: 7), tag: 99)
234
+ assert w.inner.age == 7
235
+ assert w.tag == 99
236
+
237
+ test "records built in a loop do not alias"
238
+ # Regression test: records are bump-allocated at *runtime*, not at a
239
+ # compile-time-fixed address; otherwise every iteration's `LoopBox(...)`
240
+ # would alias the same memory and this would sum to 20 instead of 10.
241
+ assert sumLoopBoxes() == 10
plum-examples/typed_arrays.plum CHANGED
@@ -37,9 +37,3 @@ test "array_int_native: filled arrays hold their values"
37
37
  assert sumInts(10) == 30
38
38
  assert sumFloats(4) == 2.0
39
39
  assert countTrue(5) == 2
40
-
41
- test "Grid[Int] indexes (x, y) row-major"
42
- g := Grid.filled(3, 2, 0)
43
- g.set(2, 1, 7)
44
- assert g.get(2, 1) == 7
45
- assert g.data.get(5) == 7
plum-examples/types.plum DELETED
@@ -1,199 +0,0 @@
1
- # Types: algebraic data types via `enum`, named and positional payload fields,
2
- # and shared fields across variants.
3
- import std/Str
4
- import std/Int
5
- import std/Float
6
- import std/Seq
7
-
8
- record Point(x: Int, y: Int)
9
-
10
- record Named(name: Str) =
11
-
12
- fun toStr(self) -> Str =
13
- self.name
14
-
15
- record Box[T](value: T)
16
-
17
- enum Color =
18
- | Red
19
- | Green
20
- | Blue
21
-
22
- # A bare enum variant name can be used directly as a type: `v: Red` means
23
- # "a `Color` value that is specifically the `Red` variant".
24
- fun stringifyColor(v: Red) -> Str =
25
- "Red"
26
-
27
- fun makeIntBox() -> Box[Int] =
28
- Box(value: 5)
29
-
30
- fun makeStrBox() -> Box[Str] =
31
- Box(value: "x")
32
-
33
- # ---- record-shaped and sum-type enum regression tests ----
34
-
35
- record Cat(name: Str, age: Int) =
36
-
37
- fun getAge() -> Int =
38
- self.age
39
-
40
- record Dog(name: Str, age: Int) =
41
-
42
- fun getAge(self) -> Int =
43
- self.age
44
-
45
- record IntPair(a: Int, b: Int)
46
-
47
- record Wrapper(inner: IntPair, tag: Int)
48
-
49
- record LoopBox(v: Int)
50
-
51
- fun sumLoopBoxes() -> Int =
52
- total := 0
53
- for i := range 5
54
- b := LoopBox(v: i)
55
- total = total + b.v
56
- return total
57
-
58
- enum Step(n: Int) =
59
- | ReadMin(10)
60
- | ReadMax(20)
61
-
62
- fun toNumber(self) -> Int =
63
- self.n
64
-
65
- fun stepToNumber(s: Step) -> Int =
66
- match s
67
- ReadMin => 1
68
- ReadMax => 2
69
-
70
- fun unwrapOrDefault(o: Int?, default: Int) -> Int =
71
- o ?: default
72
-
73
- enum ShapeKind = Rect | Circle
74
-
75
- record Rect(v0: Int, v1: Int)
76
- record Circle(v: Int)
77
-
78
- fun area(s: ShapeKind) -> Int =
79
- match s
80
- Rect(w, h) =>
81
- return w * h
82
- Circle(r) =>
83
- return r * r
84
-
85
- record Vec2(x: Int, y: Int)
86
-
87
- enum ShapeWithFields = CircleField | SquareField
88
-
89
- record CircleField(radius: Int)
90
- record SquareField(side: Int)
91
-
92
- record MaybeBox(value: Int?) =
93
-
94
- fun unwrap(default: Int) -> Int =
95
- self.value ?: default
96
-
97
- test "class field and method run correctly"
98
- c := Cat(name: "x", age: 7)
99
- assert c.getAge() == 7
100
-
101
- test "nested method declaration runs correctly"
102
- d := Dog(name: "x", age: 7)
103
- assert d.getAge() == 7
104
-
105
- test "nested class call runs correctly"
106
- w := Wrapper(inner: IntPair(a: 11, b: 22), tag: 99)
107
- assert w.inner.b == 22
108
-
109
- test "repeated class call in a loop does not alias"
110
- # Regression test: class instances are bump-allocated at *runtime* (via a
111
- # mutable wasm global), not at a compile-time-fixed address — otherwise
112
- # every iteration's `LoopBox(...)` would alias the same memory and this
113
- # would sum to 5*4=20 instead of 0+1+2+3+4=10.
114
- assert sumLoopBoxes() == 10
115
-
116
- test "enum discriminant value field access runs correctly for each variant"
117
- assert ReadMin.toNumber() * 100 + ReadMax.toNumber() == 1020
118
-
119
- test "enum discriminant value matches by variant name correctly"
120
- assert stepToNumber(ReadMin) * 10 + stepToNumber(ReadMax) == 12
121
-
122
- test "a nullable Int parameter compiles and runs"
123
- assert unwrapOrDefault(7, 0) == 7
124
-
125
- test "multi field variant construction compiles and runs"
126
- assert area(Rect(3, 4)) == 12
127
-
128
- test "single-variant named-payload enum field access works like a class"
129
- # `Vec2` has exactly one variant, so its fields unambiguously describe
130
- # every `Vec2` value — `.x`/`.y` resolve directly, no `match` needed,
131
- # for both named and positional construction.
132
- named := Vec2(x: 1, y: 2)
133
- positional := Vec2(3, 4)
134
- assert named.x == 1
135
- assert named.y == 2
136
- assert positional.x == 3
137
- assert positional.y == 4
138
-
139
- test "named-payload field access on a multi-variant enum value works via a checked downcast"
140
- # Unlike `Vec2` above, `ShapeWithFields` has more than one variant — `.field`
141
- # here isn't statically provable to always succeed the way it is on a
142
- # single-variant enum. It's still allowed because `radius`/`side` each
143
- # belong to exactly one variant (no ambiguity) — codegen compiles it as a
144
- # ref.cast down to that one variant's own struct, which would trap at
145
- # runtime if the value were ever the OTHER variant instead.
146
- c := CircleField(radius: 5)
147
- s := SquareField(side: 9)
148
- assert c.radius == 5
149
- assert s.side == 9
150
-
151
- test "enum variant used directly as a type checks and runs correctly"
152
- assert stringifyColor(Red) == "Red"
153
-
154
- test "enum class field construct and destructure runs correctly"
155
- b := MaybeBox(value: 42)
156
- assert b.unwrap(0) == 42
157
-
158
- test "class field mutation and spread update run correctly"
159
- p := Point(x: 1, y: 2)
160
- p.x = 10
161
- assert p.x == 10
162
- assert p.y == 2
163
- p2 := Point(..p, x: 100)
164
- assert p2.x == 100
165
- assert p2.y == 2
166
- # the spread source is untouched by the update it fed
167
- assert p.x == 10
168
-
169
- test "single-variant named-payload enum field mutation and spread update run correctly"
170
- v := Vec2(x: 1, y: 2)
171
- v.x = 10
172
- assert v.x == 10
173
- assert v.y == 2
174
- v2 := Vec2(..v, x: 100)
175
- assert v2.x == 100
176
- assert v2.y == 2
177
- assert v.x == 10
178
-
179
- test "multi-variant enum field mutation and spread update run correctly"
180
- # Same checked-downcast idiom `.field` reads already use on a
181
- # multi-variant enum's uniquely-owned field name (see the test above
182
- # about `ShapeWithFields`) — traps at runtime if the value is ever the
183
- # OTHER variant, no static proof required.
184
- c := CircleField(radius: 5)
185
- c.radius = 9
186
- assert c.radius == 9
187
- c2 := CircleField(..c, radius: 50)
188
- assert c2.radius == 50
189
- assert c.radius == 9
190
-
191
- test "gc type registry produces a well formed type section alongside bump allocator codegen"
192
- # Task 1 Step 5 of the wasm-gc migration plan: the wasm-gc type registry
193
- # emits a well-formed type section — a struct type per class, a
194
- # supertype+subtypes set per enum, and a shared Str array type — even
195
- # though the rest of a compiled module uses a different representation.
196
- # This proves the still-untouched bump-allocator codegen actually runs
197
- # correctly alongside it, not just that it compiles.
198
- c := Cat(name: "x", age: 7)
199
- assert c.getAge() == 7
plum-examples/unions.plum CHANGED
@@ -2,7 +2,10 @@
2
2
  # A member converts to the enum where one is expected, a bare member name in a
3
3
  # pattern narrows the subject, and a method every member defines is callable on
4
4
  # the enum (auto-dispatch). Members can be structs, records, primitives
5
- # (`Int | Float | Str`), generic instances (`List[Jv]`) or bare tags.
5
+ # (`Int | Float | Str`), generic instances (`List[Jv]`) or bare tags. An enum
6
+ # can also define its own methods (a `match self` body), carry a shared
7
+ # discriminant field (`enum Step(n: Int)`), and one type can belong to several
8
+ # enums.
6
9
  module unions
7
10
  import std/Str
8
11
  import std/Int
@@ -40,7 +43,7 @@ fun leaf(v: Int) -> Tree[Int] = Leaf(v: v)
40
43
 
41
44
  fun node(l: Tree[Int], r: Tree[Int]) -> Tree[Int] = Node[Int](left: l, right: r)
42
45
 
43
- fun describe(j: Jv) -> Str =
46
+ fun describeJv(j: Jv) -> Str =
44
47
  match j
45
48
  JvNull => "null"
46
49
  Int => "int"
@@ -61,6 +64,57 @@ fun make(big: Bool) -> Shape =
61
64
  else
62
65
  Circle(r: 1.0)
63
66
 
67
+ # Tags and variant-as-type: a bare name that is not a declared type is a tag, and
68
+ # a tag can be used as a parameter type (`v: Red` accepts only `Red`).
69
+ enum Color = Red | Green | Blue
70
+
71
+ fun redName(v: Red) -> Str = "Red"
72
+
73
+ # A discriminant enum gives every tag a shared number.
74
+ enum Step(n: Int) =
75
+ | ReadMin(10)
76
+ | ReadMax(20)
77
+
78
+ fun toNumber(self) -> Int =
79
+ self.n
80
+
81
+ fun stepIndex(s: Step) -> Int =
82
+ match s
83
+ ReadMin => 1
84
+ ReadMax => 2
85
+
86
+ # An enum may define a method itself with a `match self` body.
87
+ record Ball(r: Float)
88
+
89
+ record Slab(w: Float, h: Float)
90
+
91
+ enum Body =
92
+ | Ball
93
+ | Slab
94
+
95
+ fun area(self) -> Float =
96
+ match self
97
+ Ball(r) => r * r * 3.0
98
+ Slab(w, h) => w * h
99
+
100
+ # One example, two ways: auto-dispatch (every member defines `describe`, so it is
101
+ # callable on the enum) and an explicit `match` over the members.
102
+ record Novel(title: Str, pages: Int) =
103
+ fun describe(self) -> Str = "novel {self.title}"
104
+
105
+ record Atlas(title: Str, regions: Int) =
106
+ fun describe(self) -> Str = "atlas {self.title}"
107
+
108
+ enum Book = Novel | Atlas
109
+
110
+ fun describeAuto(b: Book) -> Str =
111
+ b.describe()
112
+
113
+ fun describeMatch(b: Book) -> Str =
114
+ match b
115
+ Novel(title, _) => "novel {title}"
116
+ Atlas(title, _) => "atlas {title}"
117
+
64
118
  enum Jb = BNull | Bool | Float | Str | List[Jb]
65
119
 
66
120
  fun showB(j: Jb) -> Str =
@@ -102,24 +156,18 @@ test "list_of_shapes"
102
156
  sum = sum + x.area()
103
157
  assert sum == 9.0
104
158
 
105
- test "push_converts"
106
- xs := List[Shape]()
107
- xs.push(Circle(r: 2.0))
108
- xs.push(Rectangle(w: 1.0, h: 1.0))
109
- assert xs.length() == 2
110
-
111
159
  test "tree_generic"
112
160
  assert total(node(leaf(1), node(leaf(2), leaf(3)))) == 6
113
161
 
114
162
  test "json_like_primitives"
115
- assert describe(JvNull) == "null"
163
+ assert describeJv(JvNull) == "null"
116
- assert describe(7) == "int"
164
+ assert describeJv(7) == "int"
117
- assert describe(1.5) == "float"
165
+ assert describeJv(1.5) == "float"
118
- assert describe("hi") == "str hi"
166
+ assert describeJv("hi") == "str hi"
119
167
  js := List[Jv]()
120
168
  js.push(1.0)
121
169
  js.push(7)
122
- assert describe(js) == "list"
170
+ assert describeJv(js) == "list"
123
171
 
124
172
  test "two_enums_one_type"
125
173
  c := Circle(r: 2.0)
@@ -149,7 +197,7 @@ test "union_in_try"
149
197
  assert {posLen(3) catch 0} == 2
150
198
  assert {posLen(-1) catch 0} == 0
151
199
  ys := pos(3) catch List[Jv]()
152
- assert describe(ys) == "list"
200
+ assert describeJv(ys) == "list"
153
201
 
154
202
  test "union_with_for"
155
203
  xs := List[Jv]()
@@ -158,8 +206,30 @@ test "union_with_for"
158
206
  xs.push(2.5)
159
207
  n := 0
160
208
  for x := range xs
161
- n = n + describe(x).length()
209
+ n = n + describeJv(x).length()
162
210
  assert n == 3 + 5 + 5
163
211
 
164
- test "union_toplevel_plain"
212
+ test "tags, tag as a parameter type, and discriminant values"
165
- assert describe(JvNull) == "null"
213
+ assert redName(Red) == "Red"
214
+ c: Color := Green
215
+ if Green := c
216
+ assert true
217
+ else
218
+ assert false
219
+ assert ReadMin.toNumber() * 100 + ReadMax.toNumber() == 1020
220
+ assert stepIndex(ReadMin) * 10 + stepIndex(ReadMax) == 12
221
+
222
+ test "enum defines its own method over its members"
223
+ assert Ball(r: 1.5).area() == 6.75
224
+ assert Slab(w: 2.0, h: 3.0).area() == 6.0
225
+ b: Body := Slab(w: 1.0, h: 2.0)
226
+ assert b.area() == 2.0
227
+
228
+ test "Book both ways: auto-dispatch and match agree"
229
+ books: List[Book] := List[Book]()
230
+ books.push(Novel(title: "Dune", pages: 600))
231
+ books.push(Atlas(title: "World", regions: 7))
232
+ for b := range books
233
+ assert describeAuto(b) == describeMatch(b)
234
+ assert describeAuto(Novel(title: "Dune", pages: 600)) == "novel Dune"
235
+ assert describeMatch(Atlas(title: "World", regions: 7)) == "atlas World"
plum-lsp/src/main.rs CHANGED
@@ -390,7 +390,7 @@ mod tests {
390
390
 
391
391
  #[test]
392
392
  fn valuesAndLoopsExamplesHaveNoDiagnostics() {
393
- for f in ["values.plum", "loops.plum", "chains.plum", "forms.plum"] {
393
+ for f in ["structs_records.plum", "loops.plum", "seq.plum", "forms.plum", "match.plum", "unions.plum", "game_loop.plum"] {
394
394
  let path = repoRoot().join("plum-examples").join(f);
395
395
  let text = std::fs::read_to_string(&path).unwrap();
396
396
  let tree = Backend::parseTree(&text);
plum-std/Str.plum CHANGED
@@ -646,30 +646,6 @@ fun bigAscii(n: Int, b: Int) -> Str =
646
646
  i = i + 1
647
647
  return buf.toStr()
648
648
 
649
- test "string interpolation of an int runs correctly"
650
- x := 42
651
- assert "{x}" == "42"
652
-
653
- test "string interpolation of a negative int runs correctly"
654
- x := -7
655
- assert "{x}" == "-7"
656
-
657
- test "string interpolation of zero runs correctly"
658
- x := 0
659
- assert "{x}" == "0"
660
-
661
- test "string interpolation with surrounding text and multiple interps runs correctly"
662
- count := 3
663
- total := 10
664
- assert "{count} of {total} complete" == "3 of 10 complete"
665
-
666
- test "string interpolation of a str runs correctly"
667
- assert greetForInterpolationTest("World") == "Hello, World!"
668
-
669
- test "string interpolation of a bool runs correctly"
670
- b := true
671
- assert "is {b}" == "is true"
672
-
673
649
  test "split divides on every occurrence of the separator"
674
650
  parts := "a,b,c".split(",", 0)
675
651
  assert parts.length() == 3
@@ -695,25 +671,6 @@ test "split of an empty string returns a single empty element"
695
671
  assert parts.length() == 1
696
672
  assert parts.join("|") == ""
697
673
 
698
- test "string interpolation supports a boolean expression, not just a bare variable"
699
- c1 := true
700
- c2 := true
701
- c3 := false
702
- assert "{c1 && c2}" == "true"
703
- assert "{c1 && c3}" == "false"
704
- assert "{c1 || c3}" == "true"
705
-
706
- test "string interpolation supports a method call taking a closure literal"
707
- l := List(1, 2, 3, 4)
708
- assert "{l.findValue(3)!}" == "3"
709
- assert "{l.contains(100)}" == "false"
710
-
711
- test "string interpolation supports an inline if expression"
712
- n := 5
713
- assert "{if n > 0 then "positive" else "non-positive"}" == "positive"
714
-
715
- # ---- Str method regression tests ----
716
-
717
674
  test "endsWith matches a real suffix and rejects a non-suffix or an over-long search"
718
675
  assert "hello.plum".endsWith(".plum") == true
719
676
  assert "hello.plum".endsWith(".rs") == false
plum-wasm-codegen/tests/examples_test.rs CHANGED
@@ -62,20 +62,23 @@ fn functionsCompiles() {
62
62
  }
63
63
 
64
64
  #[test]
65
- fn typesCompiles() {
65
+ fn structsRecordsCompiles() {
66
- // Only class/trait/enum declarations, no function bodies to lower — should still
67
- // produce a valid (if unexciting) module.
68
- assertCompiles("types.plum");
66
+ assertCompiles("structs_records.plum");
69
67
  }
70
68
 
71
- // methods.plum/match.plum/closures.plum's actual runtime-correctness checks
69
+ // unions.plum/match.plum/closures.plum's actual runtime-correctness checks
72
70
  // now live as native `test`/`expect` blocks inside those example files
73
71
  // themselves (run via `plum test plum-examples/<name>.plum`, see
74
72
  // scripts/test-plum.sh) — `assertCompiles` below still exercises that each
75
73
  // still compiles to valid wasm via this crate's direct (non-CLI) path.
76
74
  #[test]
77
- fn methodsCompiles() {
75
+ fn unionsCompiles() {
78
- assertCompiles("methods.plum");
76
+ assertCompiles("unions.plum");
77
+ }
78
+
79
+ #[test]
80
+ fn gameLoopCompiles() {
81
+ assertCompiles("game_loop.plum");
79
82
  }
80
83
 
81
84
  #[test]