plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
82b552d
— Peter John
2026-10-25T16:30:41+05:30
docs(examples): migrate examples, LSP, editor syntax and README to Try + tags
- README.md +41 -18
- plum-examples/dop_visitor.plum +2 -2
- plum-examples/error_propagation.plum +80 -44
- plum-examples/match.plum +13 -18
- plum-examples/oop_visitor.plum +2 -2
- plum-examples/safe_navigation.plum +13 -31
- plum-examples/types.plum +17 -14
- plum-lsp/src/completion.rs +1 -1
- plum-tooling/vscode-plum/syntaxes/plum.tmLanguage.json +5 -5
- plum-wasm-codegen/tests/examples_test.rs +8 -0
README.md
CHANGED
|
@@ -218,9 +218,9 @@ enum Color =
|
|
|
218
218
|
| Green
|
|
219
219
|
| Blue
|
|
220
220
|
|
|
221
|
-
enum
|
|
221
|
+
enum Maybe =
|
|
222
|
-
|
|
|
222
|
+
| Just(Int) # unnamed positional payload — Just(5)
|
|
223
|
-
|
|
|
223
|
+
| Nothing
|
|
224
224
|
|
|
225
225
|
enum Shape =
|
|
226
226
|
| Circle(radius: Int) # named payload fields — Circle(radius: 5) or Circle(5)
|
|
@@ -308,9 +308,9 @@ match n
|
|
|
308
308
|
else
|
|
309
309
|
2
|
|
310
310
|
|
|
311
|
-
match
|
|
311
|
+
match maybe
|
|
312
|
-
|
|
312
|
+
Just(v) => v
|
|
313
|
-
|
|
313
|
+
Nothing => 0
|
|
314
314
|
|
|
315
315
|
match book
|
|
316
316
|
FantasyBook(title, _, hasMythicalCreatures) when hasMythicalCreatures =>
|
|
@@ -318,7 +318,7 @@ match book
|
|
|
318
318
|
FantasyBook(title, _, _) => "Fantasy book \"{title}\" has no mythical creatures"
|
|
319
319
|
```
|
|
320
320
|
|
|
321
|
-
Patterns can be literals, a bare identifier (binds), a bare capitalized tag (compared), a constructor pattern (`
|
|
321
|
+
Patterns can be literals, a bare identifier (binds), a bare capitalized tag (compared), a constructor pattern (`Just(v)`, binding its argument), or `_` (wildcard). `match a, b, ...` supports multiple subjects, and a case can carry a `when <expr>` guard.
|
|
322
322
|
|
|
323
323
|
## String interpolation
|
|
324
324
|
|
|
@@ -342,30 +342,52 @@ $ plum run plum-examples/io.plum
|
|
|
342
342
|
hello from plum
|
|
343
343
|
```
|
|
344
344
|
|
|
345
|
-
## Error
|
|
345
|
+
## Error handling
|
|
346
346
|
|
|
347
|
-
`
|
|
347
|
+
There is no `Option` and no exceptions. A fallible function returns `Try[T, E]`, which is either `Ok(T)` or `Err(E)`. `E` is normally a *tag union* — a set of bare tags (optionally carrying a payload) that needs no declaration:
|
|
348
348
|
|
|
349
349
|
```plum
|
|
350
|
-
fun
|
|
350
|
+
fun parsePositive(s: Str) -> Try[Int, [BadNumStr, NegativeNum]] =
|
|
351
|
-
|
|
351
|
+
n := parseInt(s)?
|
|
352
|
+
if n < 0
|
|
352
|
-
|
|
353
|
+
return Err(NegativeNum)
|
|
353
|
-
return Ok(
|
|
354
|
+
return Ok(n)
|
|
354
355
|
```
|
|
355
356
|
|
|
356
|
-
|
|
357
|
+
A "missing value" is just an error tag: `List.get` fails with `[OutOfBounds]`, `first`/`last`/`pop` with `[ListWasEmpty]`, `Map.get` with `[KeyNotFound]`, `parseInt`/`parseFloat` with `[BadNumStr]`, `Json.fromStr` with `[InvalidJson(Str)]`.
|
|
358
|
+
|
|
359
|
+
`expr?` unwraps an `Ok`, or exits the enclosing function early with the `Err` value — Rust's `?`. Tag unions merge automatically across `?`: the function's declared union just has to cover every union it propagates:
|
|
360
|
+
|
|
361
|
+
```plum
|
|
362
|
+
fun numAt(s: Str, i: Int) -> Try[Int, [BadNumStr, OutOfBounds]] =
|
|
363
|
+
n := parseInt(s)? # [BadNumStr]
|
|
364
|
+
x := List(10, 20, 30).get(i)? # [OutOfBounds]
|
|
365
|
+
return Ok(n + x)
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
`expr ? |e| Tag` maps the error to a different tag before propagating it:
|
|
369
|
+
|
|
370
|
+
```plum
|
|
371
|
+
fun parseOrInvalid(s: Str) -> Try[Int, [InvalidInput]] =
|
|
372
|
+
n := parseInt(s) ? |e| InvalidInput
|
|
373
|
+
return Ok(n)
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
`left ?? right` is the plain-expression counterpart — no early return, just a fallback value if `left` is an `Err`:
|
|
357
377
|
|
|
358
378
|
```plum
|
|
359
379
|
fun sumOrZero(a: Str, b: Str) -> Int =
|
|
360
|
-
x := parsePositive(a) ?
|
|
380
|
+
x := {parsePositive(a) ?? 0}
|
|
361
|
-
y := parsePositive(b) ?
|
|
381
|
+
y := {parsePositive(b) ?? 0}
|
|
362
382
|
x + y
|
|
363
383
|
```
|
|
364
384
|
|
|
365
|
-
`
|
|
385
|
+
`crash "msg"` aborts the program with a message. Use it for bugs (a broken invariant), not for expected failures; `unwrap()` on an `Err` crashes the same way.
|
|
386
|
+
|
|
387
|
+
`obj?.field` / `obj?.method(args)` (safe navigation, Groovy/Kotlin-style) reaches into a `Try` value without unwrapping it — `Ok` maps the field or method access over the inner value and re-wraps it, `Err` passes through untouched:
|
|
366
388
|
|
|
367
389
|
```plum
|
|
368
|
-
fun cityName(person:
|
|
390
|
+
fun cityName(person: Try[Person, [Missing]]) -> Try[Str, [Missing]] =
|
|
369
391
|
person?.city?.name
|
|
370
392
|
```
|
|
371
393
|
|
|
@@ -374,6 +396,7 @@ It's sugar for `obj.map(|v| v.field)` — nothing more than that; chain it as ma
|
|
|
374
396
|
## Standard library highlights
|
|
375
397
|
|
|
376
398
|
- **`Bool`** (`import std/Bool`) — ordinary `enum Bool = | True | False`
|
|
399
|
+
- **`Try[T, E]`** (`import std/Try`) — `Ok(T)` or `Err(E)`; `isOk`/`isErr`/`unwrap`/`unwrapOr`/`unwrapOrElse`/`map`/`mapErr`/`andThen`
|
|
377
400
|
- **`Str`** (`import std/Str`) — a byte array with a codepoint-aware layer for Unicode correctness: `runeLength`, `runeAt`, `codePointAt`, `codePointToStr`
|
|
378
401
|
- **`Byte`** / **`[]Byte`** (`import std/Byte`) — a scalar unsigned 8-bit value and a fixed-length mutable byte slice
|
|
379
402
|
- **`Buffer`** (`import std/Buffer`) — a growable, mutable byte sequence for building up a `Str`, amortized O(n) writes
|
plum-examples/dop_visitor.plum
CHANGED
|
@@ -43,7 +43,7 @@ import std/Str
|
|
|
43
43
|
|
|
44
44
|
enum Rating =
|
|
45
45
|
| Good(name: Str)
|
|
46
|
-
|
|
|
46
|
+
| Poor(name: Str)
|
|
47
47
|
|
|
48
48
|
enum Book =
|
|
49
49
|
| FantasyBook(title: Str, pages: Int, hasMythicalCreatures: Bool)
|
|
@@ -107,6 +107,6 @@ test "non fiction book with a bad rating reports mixed reviews"
|
|
|
107
107
|
title: "Shaky Claims",
|
|
108
108
|
pages: 80,
|
|
109
109
|
rating1: Good(name: "Alice"),
|
|
110
|
-
rating2:
|
|
110
|
+
rating2: Poor(name: "Carol"),
|
|
111
111
|
)
|
|
112
112
|
assert collectInterestingInfo(book) == "Non-fiction book \"Shaky Claims\" has mixed reviews"
|
plum-examples/error_propagation.plum
CHANGED
|
@@ -1,57 +1,66 @@
|
|
|
1
|
-
import std/
|
|
1
|
+
import std/Try
|
|
2
|
-
import std/Option
|
|
3
2
|
import std/Bool
|
|
4
3
|
import std/Number
|
|
5
4
|
import std/Str
|
|
6
5
|
import std/List
|
|
7
6
|
|
|
8
|
-
# `
|
|
7
|
+
# `Try[T, E]` is either `Ok(T)` or `Err(E)`. `E` is normally a tag union such
|
|
9
|
-
#
|
|
8
|
+
# as `[NegativeNum, BadNumStr]`: a set of bare tags (optionally carrying a
|
|
10
|
-
# idea as Rust's `?`. The enclosing function's own return type must accept
|
|
11
|
-
#
|
|
9
|
+
# payload, e.g. `InvalidJson(Str)`) that needs no declaration. There is no
|
|
12
|
-
# `plum-checker`'s `inferExpr` on `Expr::Try`), so a mismatch there still
|
|
13
|
-
#
|
|
10
|
+
# `Option`: a missing value is just an error tag like `[OutOfBounds]`.
|
|
14
11
|
#
|
|
12
|
+
# `expr?` unwraps an `Ok`, or exits the enclosing function early with the
|
|
13
|
+
# `Err` value. The error unions of different calls merge automatically into
|
|
14
|
+
# the enclosing function's declared union, so `parseInt(s)?` (`[BadNumStr]`)
|
|
15
|
+
# and `xs.get(i)?` (`[OutOfBounds]`) can share one function.
|
|
16
|
+
#
|
|
17
|
+
# `expr ? |e| Tag` maps the error to a different tag before propagating it.
|
|
18
|
+
#
|
|
15
|
-
# `left ?
|
|
19
|
+
# `left ?? right` is the plain-expression counterpart: no early return,
|
|
20
|
+
# `right` is the fallback used when `left` is an `Err`.
|
|
21
|
+
#
|
|
16
|
-
#
|
|
22
|
+
# `crash "msg"` aborts the program with a message; use it for bugs, not for
|
|
17
|
-
# fully checked (unlike `?`) since there's no enclosing-return-type
|
|
18
|
-
#
|
|
23
|
+
# expected failures.
|
|
19
24
|
|
|
20
|
-
fun parsePositive(s: Str) ->
|
|
25
|
+
fun parsePositive(s: Str) -> Try[Int, [BadNumStr, NegativeNum]] =
|
|
21
26
|
n := parseInt(s)?
|
|
22
27
|
if n < 0
|
|
23
|
-
return Err(
|
|
28
|
+
return Err(NegativeNum)
|
|
24
29
|
return Ok(n)
|
|
25
30
|
|
|
26
|
-
fun sumTwo(a: Str, b: Str) ->
|
|
31
|
+
fun sumTwo(a: Str, b: Str) -> Try[Int, [BadNumStr, NegativeNum]] =
|
|
27
32
|
x := parsePositive(a)?
|
|
28
33
|
y := parsePositive(b)?
|
|
29
34
|
return Ok(x + y)
|
|
30
35
|
|
|
36
|
+
# Two calls with different error unions merge into [BadNumStr, OutOfBounds].
|
|
31
|
-
fun
|
|
37
|
+
fun numAt(s: Str, i: Int) -> Try[Int, [BadNumStr, OutOfBounds]] =
|
|
38
|
+
n := parseInt(s)?
|
|
39
|
+
xs := List(10, 20, 30)
|
|
32
|
-
|
|
40
|
+
x := xs.get(i)?
|
|
33
|
-
Some(v) =>
|
|
34
|
-
if v > 0
|
|
35
|
-
|
|
41
|
+
return Ok(n + x)
|
|
36
|
-
return None
|
|
37
|
-
None =>
|
|
38
|
-
return None
|
|
39
42
|
|
|
43
|
+
# `? |e| Tag` renames the error on the way out.
|
|
40
|
-
fun
|
|
44
|
+
fun parseOrInvalid(s: Str) -> Try[Int, [InvalidInput]] =
|
|
41
|
-
|
|
45
|
+
n := parseInt(s) ? |e| InvalidInput
|
|
42
|
-
return
|
|
46
|
+
return Ok(n)
|
|
43
47
|
|
|
44
|
-
# `left ?: right` — same Ok/Some-vs-Err/None narrowing as `?`, but a plain
|
|
45
|
-
|
|
48
|
+
fun doubledFirst(list: List[Int]) -> Try[Int, [ListWasEmpty]] =
|
|
46
|
-
|
|
49
|
+
a := list.first()?
|
|
50
|
+
return Ok(a * 2)
|
|
47
51
|
|
|
48
52
|
fun sumOrZero(a: Str, b: Str) -> Int =
|
|
49
|
-
x := parsePositive(a) ?
|
|
53
|
+
x := {parsePositive(a) ?? 0}
|
|
50
|
-
y := parsePositive(b) ?
|
|
54
|
+
y := {parsePositive(b) ?? 0}
|
|
51
55
|
x + y
|
|
52
56
|
|
|
53
57
|
fun firstOrDefault(list: List[Int], default: Int) -> Int =
|
|
54
|
-
list.
|
|
58
|
+
{list.first() ?? default}
|
|
59
|
+
|
|
60
|
+
fun mustBePositive(n: Int) -> Int =
|
|
61
|
+
if n < 0
|
|
62
|
+
crash "n must not be negative"
|
|
63
|
+
n
|
|
55
64
|
|
|
56
65
|
test "try operator unwraps Ok and propagates the value through two calls"
|
|
57
66
|
r := sumTwo("2", "3")
|
|
@@ -61,23 +70,50 @@ test "try operator unwraps Ok and propagates the value through two calls"
|
|
|
61
70
|
test "try operator exits early with Err, skipping the rest of the function"
|
|
62
71
|
r := sumTwo("2", "-3")
|
|
63
72
|
assert r.isErr()
|
|
73
|
+
match r
|
|
74
|
+
Ok(_) =>
|
|
75
|
+
assert False
|
|
64
|
-
|
|
76
|
+
Err(NegativeNum) =>
|
|
77
|
+
assert True
|
|
78
|
+
Err(BadNumStr) =>
|
|
79
|
+
assert False
|
|
65
80
|
|
|
66
|
-
test "try operator
|
|
81
|
+
test "try operator merges two different error unions"
|
|
67
|
-
|
|
82
|
+
assert numAt("5", 1).unwrap() == 25
|
|
68
|
-
|
|
83
|
+
match numAt("x", 1)
|
|
84
|
+
Ok(_) =>
|
|
69
|
-
|
|
85
|
+
assert False
|
|
86
|
+
Err(BadNumStr) =>
|
|
87
|
+
assert True
|
|
70
|
-
|
|
88
|
+
Err(OutOfBounds) =>
|
|
89
|
+
assert False
|
|
90
|
+
match numAt("5", 9)
|
|
91
|
+
Ok(_) =>
|
|
92
|
+
assert False
|
|
93
|
+
Err(BadNumStr) =>
|
|
94
|
+
assert False
|
|
95
|
+
Err(OutOfBounds) =>
|
|
96
|
+
assert True
|
|
71
97
|
|
|
72
|
-
test "try
|
|
98
|
+
test "try with a closure maps the error to another tag"
|
|
73
|
-
|
|
99
|
+
assert parseOrInvalid("7").unwrap() == 7
|
|
74
|
-
|
|
100
|
+
match parseOrInvalid("x")
|
|
101
|
+
Ok(_) =>
|
|
75
|
-
|
|
102
|
+
assert False
|
|
103
|
+
Err(InvalidInput) =>
|
|
104
|
+
assert True
|
|
76
105
|
|
|
106
|
+
test "try operator on a list that is empty yields ListWasEmpty"
|
|
107
|
+
assert doubledFirst(List(5)).unwrap() == 10
|
|
108
|
+
assert doubledFirst(List[Int]()).isErr()
|
|
109
|
+
|
|
77
|
-
test "
|
|
110
|
+
test "default operator unwraps Ok and falls back to 0 on Err"
|
|
78
111
|
assert sumOrZero("2", "3") == 5
|
|
79
112
|
assert sumOrZero("2", "-3") == 2
|
|
80
113
|
|
|
81
|
-
test "
|
|
114
|
+
test "default operator falls back to the given default on an empty list"
|
|
82
115
|
assert firstOrDefault(List(5), 99) == 5
|
|
83
116
|
assert firstOrDefault(List[Int](), 99) == 99
|
|
117
|
+
|
|
118
|
+
test "crash is not reached on the happy path"
|
|
119
|
+
assert mustBePositive(3) == 3
|
plum-examples/match.plum
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import std/
|
|
1
|
+
import std/Try
|
|
2
2
|
import std/Bool
|
|
3
3
|
import std/Number
|
|
4
4
|
import std/Str
|
|
@@ -29,16 +29,8 @@ fun describeColor(c: Color) -> Str =
|
|
|
29
29
|
Green => "green"
|
|
30
30
|
Blue => "blue"
|
|
31
31
|
|
|
32
|
-
# A dedicated, non-generic "maybe an Int"
|
|
32
|
+
# A dedicated, non-generic "maybe an Int", as opposed to the generic
|
|
33
|
-
# `
|
|
33
|
+
# `Try[T, E]` (used elsewhere in this file).
|
|
34
|
-
# constructing a bare payload-free variant (`Absent` here, `None` for a real
|
|
35
|
-
# generic enum) outside of a `match` pattern can't be disambiguated between
|
|
36
|
-
# multiple concrete instantiations of ITS enum from that expression alone
|
|
37
|
-
# (see the README's Generics section) — and since this file's forced-in
|
|
38
|
-
# stdlib prelude (`plum-core::loader::loadAndMerge`) uses `Option` at several
|
|
39
|
-
# OTHER concrete types internally, a bare `None` here genuinely IS
|
|
40
|
-
# ambiguous. `IntOpt` sidesteps that entirely by only ever having ONE
|
|
41
|
-
# possible instantiation to begin with.
|
|
42
34
|
enum IntOpt =
|
|
43
35
|
| Present(Int)
|
|
44
36
|
| Absent
|
|
@@ -100,10 +92,10 @@ fun colorCode(c: Color) -> Int =
|
|
|
100
92
|
Green => 2
|
|
101
93
|
Blue => 3
|
|
102
94
|
|
|
103
|
-
fun isSome(o:
|
|
95
|
+
fun isSome(o: Try[Int, [Missing]]) -> Int =
|
|
104
96
|
match o
|
|
105
|
-
|
|
97
|
+
Ok(_) => 1
|
|
106
|
-
|
|
98
|
+
Err(_) => 0
|
|
107
99
|
|
|
108
100
|
# Mirrors libs/std/bool.plum's `and`/`or`: `match a, b` against two Bool
|
|
109
101
|
# subjects, each case naming a tag pattern per position.
|
|
@@ -133,9 +125,12 @@ fun combine(a: Int, b: Int) -> Int =
|
|
|
133
125
|
x, 0 => x
|
|
134
126
|
x, y => x + y
|
|
135
127
|
|
|
128
|
+
fun okInt(n: Int) -> Try[Int, [Missing]] =
|
|
129
|
+
return Ok(n)
|
|
130
|
+
|
|
136
|
-
fun bothOptions(a:
|
|
131
|
+
fun bothOptions(a: Try[Int, [Missing]], b: Try[Int, [Missing]]) -> Int =
|
|
137
132
|
match a, b
|
|
138
|
-
|
|
133
|
+
Ok(x), Ok(y) => x + y
|
|
139
134
|
_, _ => 0
|
|
140
135
|
|
|
141
136
|
enum Point =
|
|
@@ -199,7 +194,7 @@ test "non bool bare tag pattern runs correctly"
|
|
|
199
194
|
assert colorCode(Green) == 2
|
|
200
195
|
|
|
201
196
|
test "constructor pattern wildcard field runs correctly"
|
|
202
|
-
assert isSome(
|
|
197
|
+
assert isSome(okInt(99)) == 1
|
|
203
198
|
|
|
204
199
|
test "constructor pattern does not misfire on payload free sibling"
|
|
205
200
|
assert describeOption(Absent) == 0
|
|
@@ -217,7 +212,7 @@ test "multi subject match with binding and wildcard runs correctly"
|
|
|
217
212
|
assert combine(3, 4) == 7
|
|
218
213
|
|
|
219
214
|
test "multi subject match with generic enum variant runs correctly"
|
|
220
|
-
assert bothOptions(
|
|
215
|
+
assert bothOptions(okInt(3), okInt(4)) == 7
|
|
221
216
|
|
|
222
217
|
test "match destructures a plain class the same way it destructures an enum variant"
|
|
223
218
|
assert Point(x: 3, y: 4).sum() == 7
|
plum-examples/oop_visitor.plum
CHANGED
|
@@ -36,7 +36,7 @@ import std/Str
|
|
|
36
36
|
|
|
37
37
|
enum Rating =
|
|
38
38
|
| Good(name: Str)
|
|
39
|
-
|
|
|
39
|
+
| Poor(name: Str)
|
|
40
40
|
|
|
41
41
|
trait Book =
|
|
42
42
|
collectInterestingInfo() -> Str
|
|
@@ -119,6 +119,6 @@ test "non fiction book with a bad rating reports mixed reviews"
|
|
|
119
119
|
title: "Shaky Claims",
|
|
120
120
|
pages: 80,
|
|
121
121
|
rating1: Good(name: "Alice"),
|
|
122
|
-
rating2:
|
|
122
|
+
rating2: Poor(name: "Carol"),
|
|
123
123
|
)
|
|
124
124
|
assert book.collectInterestingInfo() == "Non-fiction book \"Shaky Claims\" has mixed reviews"
|
plum-examples/safe_navigation.plum
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import std/
|
|
1
|
+
import std/Try
|
|
2
|
-
import std/Result
|
|
3
2
|
import std/Bool
|
|
4
3
|
import std/Number
|
|
5
4
|
import std/Str
|
|
6
5
|
# `obj?.field` / `obj?.method(args)` — Groovy/Kotlin-style safe navigation.
|
|
7
|
-
# Reaches into a `
|
|
6
|
+
# Reaches into a `Try`'s `Ok` without unwrapping it,
|
|
8
7
|
# mapping the field/method access over the inner value and re-wrapping the
|
|
9
|
-
# result; `Err`
|
|
8
|
+
# result; `Err` passes through untouched. Pure sugar for
|
|
10
9
|
# `obj.map(|v| v.field)` — desugared entirely in the parser (see
|
|
11
10
|
# `plum-core`'s `parseSafeAttribute`), so nothing past it needs to know this
|
|
12
11
|
# syntax exists.
|
|
@@ -17,43 +16,26 @@ enum Person =
|
|
|
17
16
|
fun birthday(self) -> Int =
|
|
18
17
|
self.age + 1
|
|
19
18
|
|
|
20
|
-
fun personName(p:
|
|
19
|
+
fun personName(p: Try[Person, [Missing]]) -> Try[Str, [Missing]] =
|
|
21
20
|
p?.name
|
|
22
21
|
|
|
23
|
-
fun personBirthday(p:
|
|
22
|
+
fun personBirthday(p: Try[Person, [Missing]]) -> Try[Int, [Missing]] =
|
|
24
23
|
p?.birthday()
|
|
25
24
|
|
|
26
|
-
fun personNameResult(p: Result[Person, Str]) -> Result[Str, Str] =
|
|
27
|
-
p?.name
|
|
28
|
-
|
|
29
|
-
fun somePerson() ->
|
|
25
|
+
fun somePerson() -> Try[Person, [Missing]] =
|
|
30
|
-
Some(Person(name: "Ann", age: 3))
|
|
31
|
-
|
|
32
|
-
fun noPerson() -> Option[Person] =
|
|
33
|
-
None
|
|
34
|
-
|
|
35
|
-
fun okPerson() -> Result[Person, Str] =
|
|
36
26
|
Ok(Person(name: "Ann", age: 3))
|
|
37
27
|
|
|
38
|
-
fun
|
|
28
|
+
fun noPerson() -> Try[Person, [Missing]] =
|
|
39
|
-
Err(
|
|
29
|
+
Err(Missing)
|
|
40
30
|
|
|
41
|
-
test "safe navigation maps a field access over
|
|
31
|
+
test "safe navigation maps a field access over Ok, passes Err through unchanged"
|
|
42
32
|
a := personName(somePerson())
|
|
43
|
-
assert a.
|
|
33
|
+
assert a.isOk()
|
|
44
34
|
assert a.unwrap() == "Ann"
|
|
45
35
|
b := personName(noPerson())
|
|
46
|
-
assert b.
|
|
36
|
+
assert b.isErr()
|
|
47
37
|
|
|
48
|
-
test "safe navigation maps a method call over
|
|
38
|
+
test "safe navigation maps a method call over Ok"
|
|
49
39
|
a := personBirthday(somePerson())
|
|
50
|
-
assert a.isSome()
|
|
51
|
-
assert a.unwrap() == 4
|
|
52
|
-
|
|
53
|
-
test "safe navigation maps a field access over Ok, passes Err through unchanged"
|
|
54
|
-
a := personNameResult(okPerson())
|
|
55
40
|
assert a.isOk()
|
|
56
|
-
assert a.unwrap() ==
|
|
41
|
+
assert a.unwrap() == 4
|
|
57
|
-
b := personNameResult(errPerson())
|
|
58
|
-
assert b.isErr()
|
|
59
|
-
assert b.unwrapErr() == "missing"
|
plum-examples/types.plum
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import std/
|
|
1
|
+
import std/Try
|
|
2
2
|
import std/Bool
|
|
3
3
|
import std/Str
|
|
4
4
|
import std/Number
|
|
@@ -60,11 +60,11 @@ enum Dog =
|
|
|
60
60
|
fun getAge(self) -> Int =
|
|
61
61
|
self.age
|
|
62
62
|
|
|
63
|
-
enum
|
|
63
|
+
enum IntPair =
|
|
64
|
-
|
|
|
64
|
+
| IntPair(a: Int, b: Int)
|
|
65
65
|
|
|
66
66
|
enum Wrapper =
|
|
67
|
-
| Wrapper(inner:
|
|
67
|
+
| Wrapper(inner: IntPair, tag: Int)
|
|
68
68
|
|
|
69
69
|
enum LoopBox =
|
|
70
70
|
| LoopBox(v: Int)
|
|
@@ -88,11 +88,14 @@ fun stepToNumber(s: Step) -> Int =
|
|
|
88
88
|
ReadMin => 1
|
|
89
89
|
ReadMax => 2
|
|
90
90
|
|
|
91
|
+
fun okIntForTypes(n: Int) -> Try[Int, [Missing]] =
|
|
92
|
+
return Ok(n)
|
|
93
|
+
|
|
91
|
-
fun
|
|
94
|
+
fun unwrapTryOr(o: Try[Int, [Missing]], default: Int) -> Int =
|
|
92
95
|
match o
|
|
93
|
-
|
|
96
|
+
Ok(v) =>
|
|
94
97
|
return v
|
|
95
|
-
|
|
98
|
+
Err(_) =>
|
|
96
99
|
return default
|
|
97
100
|
|
|
98
101
|
enum ShapeKind =
|
|
@@ -116,14 +119,14 @@ enum ShapeWithFields =
|
|
|
116
119
|
fun numberKind(n: Number) -> Str =
|
|
117
120
|
n.kind()
|
|
118
121
|
|
|
119
|
-
enum
|
|
122
|
+
enum TryBox =
|
|
120
|
-
|
|
|
123
|
+
| TryBox(value: Try[Int, [Missing]])
|
|
121
124
|
|
|
122
125
|
fun unwrap(default: Int) -> Int =
|
|
123
126
|
match self.value
|
|
124
|
-
|
|
127
|
+
Ok(v) =>
|
|
125
128
|
return v
|
|
126
|
-
|
|
129
|
+
Err(_) =>
|
|
127
130
|
return default
|
|
128
131
|
|
|
129
132
|
test "class field and method run correctly"
|
|
@@ -135,7 +138,7 @@ test "nested method declaration runs correctly"
|
|
|
135
138
|
assert d.getAge() == 7
|
|
136
139
|
|
|
137
140
|
test "nested class call runs correctly"
|
|
138
|
-
w := Wrapper(inner:
|
|
141
|
+
w := Wrapper(inner: IntPair(a: 11, b: 22), tag: 99)
|
|
139
142
|
assert w.inner.b == 22
|
|
140
143
|
|
|
141
144
|
test "repeated class call in a loop does not alias"
|
|
@@ -152,7 +155,7 @@ test "enum discriminant value matches by variant name correctly"
|
|
|
152
155
|
assert stepToNumber(ReadMin) * 10 + stepToNumber(ReadMax) == 12
|
|
153
156
|
|
|
154
157
|
test "payload variant construction compiles and runs"
|
|
155
|
-
assert
|
|
158
|
+
assert unwrapTryOr(okIntForTypes(7), 0) == 7
|
|
156
159
|
|
|
157
160
|
test "multi field variant construction compiles and runs"
|
|
158
161
|
assert area(Rect(3, 4)) == 12
|
|
@@ -194,7 +197,7 @@ test "a bare Int/Float value dispatches a method defined only on Number, via fal
|
|
|
194
197
|
assert {2.5}.kind() == "Float"
|
|
195
198
|
|
|
196
199
|
test "enum class field construct and destructure runs correctly"
|
|
197
|
-
b :=
|
|
200
|
+
b := TryBox(value: okIntForTypes(42))
|
|
198
201
|
assert b.unwrap(0) == 42
|
|
199
202
|
|
|
200
203
|
test "class field mutation and spread update run correctly"
|
plum-lsp/src/completion.rs
CHANGED
|
@@ -25,7 +25,7 @@ use plum_core::ast;
|
|
|
25
25
|
use tower_lsp::lsp_types::*;
|
|
26
26
|
|
|
27
27
|
const KEYWORDS: &[&str] = &[
|
|
28
|
-
"fun", "enum", "trait", "import", "module", "extern", "if", "else", "while", "for", "match",
|
|
28
|
+
"fun", "crash", "??", "enum", "trait", "import", "module", "extern", "if", "else", "while", "for", "match",
|
|
29
29
|
"case", "when", "break", "continue", "return", "todo", "assert", "test", "self", "True",
|
|
30
30
|
"False",
|
|
31
31
|
];
|
plum-tooling/vscode-plum/syntaxes/plum.tmLanguage.json
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
46
|
"name": "keyword.control.plum",
|
|
47
|
-
"match": "\\b(import|in|return|continue|break|match|when|if|else|while|for|assert|todo)\\b"
|
|
47
|
+
"match": "\\b(import|in|return|continue|break|match|when|if|else|while|for|assert|todo|crash)\\b"
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
"name": "keyword.operator.range.plum",
|
|
@@ -59,17 +59,17 @@
|
|
|
59
59
|
"match": "(==|!=|<=|>=|<|>)"
|
|
60
60
|
},
|
|
61
61
|
{
|
|
62
|
-
"comment": "
|
|
62
|
+
"comment": "default operator (Try unwrap-or-fallback) — checked before the bare `?` try-operator below, since `??` must win the longer match",
|
|
63
63
|
"name": "keyword.operator.elvis.plum",
|
|
64
|
-
"match": "\\?
|
|
64
|
+
"match": "\\?\\?"
|
|
65
65
|
},
|
|
66
66
|
{
|
|
67
|
-
"comment": "safe navigation (
|
|
67
|
+
"comment": "safe navigation (Try `.map` sugar) — checked before the bare `?` try-operator below, since `?.` must win the longer match",
|
|
68
68
|
"name": "keyword.operator.safenav.plum",
|
|
69
69
|
"match": "\\?\\."
|
|
70
70
|
},
|
|
71
71
|
{
|
|
72
|
-
"comment": "postfix try-operator (
|
|
72
|
+
"comment": "postfix try-operator (Try unwrap-or-early-return) — matched only when `?` glues directly to the preceding token with no space, `plum format`'s own convention for this postfix operator (unlike ternary's `?`, always spaced); ternary's own `?`/`:` are deliberately left unstyled here too, matching the tree-sitter highlights.scm, since a regex can't reliably tell them apart",
|
|
73
73
|
"name": "keyword.operator.try.plum",
|
|
74
74
|
"match": "(?<=[A-Za-z0-9_\\)\\]])\\?(?!:)(?!\\.)"
|
|
75
75
|
},
|
plum-wasm-codegen/tests/examples_test.rs
CHANGED
|
@@ -183,6 +183,14 @@ fn ioExampleCompilesAndRunsCorrectly() {
|
|
|
183
183
|
},
|
|
184
184
|
))
|
|
185
185
|
}
|
|
186
|
+
("plum", "crash") => {
|
|
187
|
+
let func_ty = imp.ty().func().cloned().expect("crash import should be a function");
|
|
188
|
+
wasmtime::Extern::Func(wasmtime::Func::new(
|
|
189
|
+
&mut store,
|
|
190
|
+
func_ty,
|
|
191
|
+
|_caller, _params, _results| Err(wasmtime::Error::msg("crash")),
|
|
192
|
+
))
|
|
193
|
+
}
|
|
186
194
|
(m, n) => panic!(
|
|
187
195
|
"io.plum test needs a stub for unexpected import {}::{}",
|
|
188
196
|
m, n
|