plum
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 +6 -6
- plum-examples/chains.plum +0 -115
- plum-examples/constants.plum +0 -4
- plum-examples/dop_visitor.plum +0 -114
- plum-examples/error_propagation.plum +1 -1
- plum-examples/forms.plum +2 -126
- plum-examples/functions.plum +3 -78
- plum-examples/game_loop.plum +102 -0
- plum-examples/generics.plum +35 -0
- plum-examples/loops.plum +0 -10
- plum-examples/map_struct_values.plum +0 -21
- plum-examples/maps.plum +15 -14
- plum-examples/match.plum +226 -77
- plum-examples/methods.plum +0 -31
- plum-examples/oop_visitor.plum +0 -117
- plum-examples/optionals.plum +70 -0
- plum-examples/pattern_if.plum +0 -153
- plum-examples/safe_navigation.plum +0 -91
- plum-examples/seq.plum +92 -2
- plum-examples/shapes.plum +0 -30
- plum-examples/strings.plum +54 -2
- plum-examples/{values.plum → structs_records.plum} +104 -51
- plum-examples/typed_arrays.plum +0 -6
- plum-examples/types.plum +0 -199
- plum-examples/unions.plum +87 -17
- plum-lsp/src/main.rs +1 -1
- plum-std/Str.plum +0 -43
- plum-wasm-codegen/tests/examples_test.rs +10 -7
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/
|
|
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/
|
|
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/
|
|
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)
|
|
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
|
|
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)
|
|
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
|
|
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
|
-
#
|
|
1
|
+
# User-defined `fun*` forms with `yield`: sources, folds and methods written in
|
|
2
|
-
#
|
|
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,
|
|
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
|
-
# ----
|
|
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 "
|
|
18
|
+
test "Map[Int, MapPoint] struct values"
|
|
19
|
+
m := Map[Int, MapPoint]()
|
|
20
|
+
i := 0
|
|
21
|
+
while i < 500
|
|
16
|
-
|
|
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(
|
|
28
|
+
assert m.get(499)!.y == 2.0
|
|
18
|
-
assert m.length() ==
|
|
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
|
|
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
|
-
#
|
|
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
|
|
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
|
|
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
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
#
|
|
162
|
-
#
|
|
163
|
-
#
|
|
164
|
-
#
|
|
165
|
-
# (
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
|
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
|
-
|
|
281
|
+
assert describeNumber(0) == "zero"
|
|
200
|
-
assert
|
|
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
|
-
|
|
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
|
|
316
|
+
assert measure(Disc(Point(x: 3, y: 4))) == 7
|
|
233
|
-
assert
|
|
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
|
|
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
|
|
1
|
+
# Strings: string literals and interpolation `"{expr}"`, which accepts any
|
|
2
|
-
#
|
|
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
|
|
1
|
+
# Structs and records: `struct` is a small copy-semantics value type (flattened
|
|
2
|
-
#
|
|
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
|
|
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
|
-
#
|
|
11
|
+
# ---- struct: value semantics ----
|
|
12
|
-
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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 "
|
|
163
|
+
test "struct operators call add and mul"
|
|
142
|
-
|
|
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 "
|
|
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(
|
|
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 "
|
|
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 "
|
|
203
|
+
test "record field mutation and spread update"
|
|
181
|
-
|
|
204
|
+
p := Point(x: 1, y: 2)
|
|
205
|
+
p.x = 10
|
|
206
|
+
assert p.x == 10
|
|
182
|
-
assert
|
|
207
|
+
assert p.y == 2
|
|
183
|
-
|
|
184
|
-
|
|
208
|
+
p2 := Point(..p, x: 100)
|
|
185
|
-
assert
|
|
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
|
|
219
|
+
assert v2.y == 2.0
|
|
187
|
-
z := chain(false) ?: Vec2.zero()
|
|
188
|
-
assert
|
|
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
|
|
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
|
|
163
|
+
assert describeJv(JvNull) == "null"
|
|
116
|
-
assert
|
|
164
|
+
assert describeJv(7) == "int"
|
|
117
|
-
assert
|
|
165
|
+
assert describeJv(1.5) == "float"
|
|
118
|
-
assert
|
|
166
|
+
assert describeJv("hi") == "str hi"
|
|
119
167
|
js := List[Jv]()
|
|
120
168
|
js.push(1.0)
|
|
121
169
|
js.push(7)
|
|
122
|
-
assert
|
|
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
|
|
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 +
|
|
209
|
+
n = n + describeJv(x).length()
|
|
162
210
|
assert n == 3 + 5 + 5
|
|
163
211
|
|
|
164
|
-
test "
|
|
212
|
+
test "tags, tag as a parameter type, and discriminant values"
|
|
165
|
-
assert
|
|
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 ["
|
|
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
|
|
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("
|
|
66
|
+
assertCompiles("structs_records.plum");
|
|
69
67
|
}
|
|
70
68
|
|
|
71
|
-
//
|
|
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
|
|
75
|
+
fn unionsCompiles() {
|
|
78
|
-
assertCompiles("
|
|
76
|
+
assertCompiles("unions.plum");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#[test]
|
|
80
|
+
fn gameLoopCompiles() {
|
|
81
|
+
assertCompiles("game_loop.plum");
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
#[test]
|