plum

#treesitter#compiler#wasm

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 CHANGED
@@ -218,9 +218,9 @@ enum Color =
218
218
  | Green
219
219
  | Blue
220
220
 
221
- enum Option =
221
+ enum Maybe =
222
- | Some(Int) # unnamed positional payload — Some(5)
222
+ | Just(Int) # unnamed positional payload — Just(5)
223
- | None
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 opt
311
+ match maybe
312
- Some(v) => v
312
+ Just(v) => v
313
- None => 0
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 (`Some(v)`, binding its argument), or `_` (wildcard). `match a, b, ...` supports multiple subjects, and a case can carry a `when <expr>` guard.
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 propagation
345
+ ## Error handling
346
346
 
347
- `expr?` unwraps a `Result`'s `Ok` or an `Option`'s `Some`, or exits the enclosing function early with the `Err`/`None` value as-is otherwise — Rust's `?`:
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 sumTwo(a: Str, b: Str) -> Result[Int, Str] =
350
+ fun parsePositive(s: Str) -> Try[Int, [BadNumStr, NegativeNum]] =
351
- x := parsePositive(a)?
351
+ n := parseInt(s)?
352
+ if n < 0
352
- y := parsePositive(b)?
353
+ return Err(NegativeNum)
353
- return Ok(x + y)
354
+ return Ok(n)
354
355
  ```
355
356
 
356
- `left ?: right` (elvis) is the plain-expression counterpart — no early return, just a fallback value if `left` is `Err`/`None`:
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) ?: 0
380
+ x := {parsePositive(a) ?? 0}
361
- y := parsePositive(b) ?: 0
381
+ y := {parsePositive(b) ?? 0}
362
382
  x + y
363
383
  ```
364
384
 
365
- `obj?.field` / `obj?.method(args)` (safe navigation, Groovy/Kotlin-style) reaches into a `Result`/`Option` value without unwrapping it — `Some`/`Ok` maps the field or method access over the inner value and re-wraps it, `None`/`Err` passes through untouched:
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: Option[Person]) -> Option[Str] =
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
- | Bad(name: Str)
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: Bad(name: "Carol"),
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/Result
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
- # `expr?` unwraps a `Result`'s `Ok` or an `Option`'s `Some`, or exits the
7
+ # `Try[T, E]` is either `Ok(T)` or `Err(E)`. `E` is normally a tag union such
9
- # enclosing function early with the `Err`/`None` value as-is otherwise — same
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
- # whatever gets returned early; that isn't checked until codegen (see
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
- # surfaces as a clear compile error, just a less precise one.
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 ?: right` (elvis) is the plain-expression counterpart: same Ok/Some
19
+ # `left ?? right` is the plain-expression counterpart: no early return,
20
+ # `right` is the fallback used when `left` is an `Err`.
21
+ #
16
- # vs Err/None narrowing, but no early return — `right` is a fallback value,
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
- # uncertainty to be permissive about.
23
+ # expected failures.
19
24
 
20
- fun parsePositive(s: Str) -> Result[Int, Str] =
25
+ fun parsePositive(s: Str) -> Try[Int, [BadNumStr, NegativeNum]] =
21
26
  n := parseInt(s)?
22
27
  if n < 0
23
- return Err("negative")
28
+ return Err(NegativeNum)
24
29
  return Ok(n)
25
30
 
26
- fun sumTwo(a: Str, b: Str) -> Result[Int, 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 firstPositive(list: List[Int]) -> Option[Int] =
37
+ fun numAt(s: Str, i: Int) -> Try[Int, [BadNumStr, OutOfBounds]] =
38
+ n := parseInt(s)?
39
+ xs := List(10, 20, 30)
32
- match list.get(0)
40
+ x := xs.get(i)?
33
- Some(v) =>
34
- if v > 0
35
- return Some(v)
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 doubledFirstPositive(list: List[Int]) -> Option[Int] =
44
+ fun parseOrInvalid(s: Str) -> Try[Int, [InvalidInput]] =
41
- a := firstPositive(list)?
45
+ n := parseInt(s) ? |e| InvalidInput
42
- return Some(a * 2)
46
+ return Ok(n)
43
47
 
44
- # `left ?: right` — same Ok/Some-vs-Err/None narrowing as `?`, but a plain
45
- # expression (no early return): `right` is the fallback value, unified against
48
+ fun doubledFirst(list: List[Int]) -> Try[Int, [ListWasEmpty]] =
46
- # the success field's type by the checker.
49
+ a := list.first()?
50
+ return Ok(a * 2)
47
51
 
48
52
  fun sumOrZero(a: Str, b: Str) -> Int =
49
- x := parsePositive(a) ?: 0
53
+ x := {parsePositive(a) ?? 0}
50
- y := parsePositive(b) ?: 0
54
+ y := {parsePositive(b) ?? 0}
51
55
  x + y
52
56
 
53
57
  fun firstOrDefault(list: List[Int], default: Int) -> Int =
54
- list.get(0) ?: default
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
- assert r.unwrapErr() == "negative"
76
+ Err(NegativeNum) =>
77
+ assert True
78
+ Err(BadNumStr) =>
79
+ assert False
65
80
 
66
- test "try operator unwraps Some and propagates the value"
81
+ test "try operator merges two different error unions"
67
- l := List(5)
82
+ assert numAt("5", 1).unwrap() == 25
68
- r := doubledFirstPositive(l)
83
+ match numAt("x", 1)
84
+ Ok(_) =>
69
- assert r.isSome()
85
+ assert False
86
+ Err(BadNumStr) =>
87
+ assert True
70
- assert r.unwrap() == 10
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 operator exits early with None, skipping the rest of the function"
98
+ test "try with a closure maps the error to another tag"
73
- l := List[Int]()
99
+ assert parseOrInvalid("7").unwrap() == 7
74
- r := doubledFirstPositive(l)
100
+ match parseOrInvalid("x")
101
+ Ok(_) =>
75
- assert r.isNone()
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 "elvis operator unwraps Ok and falls back to 0 on Err"
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 "elvis operator unwraps Some and falls back to the given default on None"
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/Option
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" — as opposed to the real, generic
32
+ # A dedicated, non-generic "maybe an Int", as opposed to the generic
33
- # `Option[T]` (`import std/option` above, used elsewhere in this file):
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: Option) -> Int =
95
+ fun isSome(o: Try[Int, [Missing]]) -> Int =
104
96
  match o
105
- Some(_) => 1
97
+ Ok(_) => 1
106
- None => 0
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: Option, b: Option) -> Int =
131
+ fun bothOptions(a: Try[Int, [Missing]], b: Try[Int, [Missing]]) -> Int =
137
132
  match a, b
138
- Some(x), Some(y) => x + y
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(Some(99)) == 1
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(Some(3), Some(4)) == 7
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
- | Bad(name: Str)
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: Bad(name: "Carol"),
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/Option
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 `Result`'s `Ok`/an `Option`'s `Some` without unwrapping it,
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`/`None` passes through untouched. Pure sugar for
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: Option[Person]) -> Option[Str] =
19
+ fun personName(p: Try[Person, [Missing]]) -> Try[Str, [Missing]] =
21
20
  p?.name
22
21
 
23
- fun personBirthday(p: Option[Person]) -> Option[Int] =
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() -> Option[Person] =
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 errPerson() -> Result[Person, Str] =
28
+ fun noPerson() -> Try[Person, [Missing]] =
39
- Err("missing")
29
+ Err(Missing)
40
30
 
41
- test "safe navigation maps a field access over Some, passes None through unchanged"
31
+ test "safe navigation maps a field access over Ok, passes Err through unchanged"
42
32
  a := personName(somePerson())
43
- assert a.isSome()
33
+ assert a.isOk()
44
34
  assert a.unwrap() == "Ann"
45
35
  b := personName(noPerson())
46
- assert b.isNone()
36
+ assert b.isErr()
47
37
 
48
- test "safe navigation maps a method call over Some"
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() == "Ann"
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/Option
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 Pair =
63
+ enum IntPair =
64
- | Pair(a: Int, b: Int)
64
+ | IntPair(a: Int, b: Int)
65
65
 
66
66
  enum Wrapper =
67
- | Wrapper(inner: Pair, tag: Int)
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 unwrapOptionOr(o: Option[Int], default: Int) -> Int =
94
+ fun unwrapTryOr(o: Try[Int, [Missing]], default: Int) -> Int =
92
95
  match o
93
- Some(v) =>
96
+ Ok(v) =>
94
97
  return v
95
- None =>
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 OptionBox =
122
+ enum TryBox =
120
- | OptionBox(value: Option[Int])
123
+ | TryBox(value: Try[Int, [Missing]])
121
124
 
122
125
  fun unwrap(default: Int) -> Int =
123
126
  match self.value
124
- Some(v) =>
127
+ Ok(v) =>
125
128
  return v
126
- None =>
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: Pair(a: 11, b: 22), tag: 99)
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 unwrapOptionOr(Some(7), 0) == 7
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 := OptionBox(value: Some(42))
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": "elvis (Result/Option unwrap-or-fallback) — checked before the bare `?` try-operator below, since `?:` must win the longer match",
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 (Result/Option `.map` sugar) — checked before the bare `?` try-operator below, since `?.` must win the longer match",
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 (Result/Option 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",
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