plum

#treesitter#compiler#wasm

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

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


d7658cc — Peter John 2026-11-05T15:11:39+05:30
Language simplification run D2: port std, examples and tests to Option/Result
Files changed (60) hide show
  1. docs/plans/2026-11-05-language-simplification-d2.md +10 -11
  2. docs/plans/2026-11-05-language-simplification-d2.results.md +24 -0
  3. plum-checker/src/lib.rs +43 -3
  4. plum-checker/tests/closure_hint_test.rs +55 -0
  5. plum-checker/tests/question_tests.rs +64 -0
  6. plum-core/src/drive.rs +22 -3
  7. plum-core/src/forms.rs +8 -5
  8. plum-core/tests/for_next_test.rs +10 -7
  9. plum-core/tests/fused_closure_generic_test.rs +28 -0
  10. plum-core/tests/record_init_test.rs +2 -0
  11. plum-core/tests/tail_test.rs +2 -0
  12. plum-examples/errors.plum +323 -281
  13. plum-examples/functional.plum +30 -29
  14. plum-examples/games/functional/game_state_view.plum +6 -2
  15. plum-examples/games/functional/kids/balloon_pop.plum +5 -3
  16. plum-examples/games/imperative/game_loop.plum +2 -1
  17. plum-examples/games/imperative/game_scene.plum +3 -2
  18. plum-examples/games/imperative/scene.plum +8 -7
  19. plum-examples/games/pure/game_pure_scene.plum +4 -3
  20. plum-examples/generics.plum +55 -48
  21. plum-examples/json.plum +16 -14
  22. plum-examples/list_literal_probe.plum +2 -1
  23. plum-examples/match.plum +32 -29
  24. plum-examples/numbers.plum +7 -5
  25. plum-examples/recursion.plum +14 -10
  26. plum-examples/sequences.plum +10 -9
  27. plum-examples/structs_records.plum +33 -31
  28. plum-examples/unions.plum +14 -10
  29. plum-game/web-test/fixtures/stress_pure_scene.plum +3 -2
  30. plum-game/web-test/fixtures/stress_state_view.plum +9 -5
  31. plum-std/Base64.plum +10 -5
  32. plum-std/Bool.plum +12 -7
  33. plum-std/Deque.plum +16 -15
  34. plum-std/Float.plum +9 -7
  35. plum-std/Heap.plum +10 -9
  36. plum-std/Http.plum +26 -16
  37. plum-std/Int.plum +12 -7
  38. plum-std/Json.plum +151 -139
  39. plum-std/List.plum +10 -9
  40. plum-std/Map.plum +8 -7
  41. plum-std/Os.plum +69 -53
  42. plum-std/Pair.plum +3 -2
  43. plum-std/Slice.plum +39 -38
  44. plum-std/Testing.plum +30 -21
  45. plum-std/Uuid.plum +14 -10
  46. plum-std/game/Affine.plum +9 -8
  47. plum-std/game/Input.plum +10 -9
  48. plum-std/game/functional/FunctionalScene.plum +66 -65
  49. plum-std/game/functional/Ui.plum +48 -44
  50. plum-std/game/imperative/Scene.plum +47 -46
  51. plum-std/game/pure/Actions.plum +15 -14
  52. plum-std/game/pure/PureScene.plum +33 -32
  53. plum-std/game/pure/SceneInput.plum +40 -36
  54. plum-wasm-codegen/src/lib.rs +54 -1
  55. plum-wasm-codegen/tests/codegen_tests.rs +51 -17
  56. plum-wasm-codegen/tests/e2e_tail/error_tail.rs +27 -0
  57. plum-wasm-codegen/tests/e2e_tail/tail.rs +2 -2
  58. plum-wasm-codegen/tests/e2e_types/closure_optional.rs +8 -8
  59. plum-wasm-codegen/tests/e2e_types/question.rs +16 -0
  60. plum-wasm-codegen/tests/e2e_types/type_param_static.rs +5 -6
docs/plans/2026-11-05-language-simplification-d2.md CHANGED
@@ -94,7 +94,7 @@ e.g. `Slice.get(i)` / `List.first()` / `Str.find(..)` return `Option[..]`
94
94
  **Tests:** fast
95
95
  **Model:** sonnet
96
96
  **Consumes:** iterator protocol `next(self) -> Option[Pair[T, Self]]`; `for x := range v` lowers to `while Some(step) := it.next() { .. }` (T1)
97
- **Produces:** `Base64.decode(s) -> Result[Buffer, Base64Error]`, `Uuid.parse(s) -> Result[Uuid, UuidError]`, Testing helpers on Option
97
+ **Produces:** Testing, Bool, Base64 and Uuid ported (decode/parse return Result with a module error enum)
98
98
  **Test code:** `plum test` on the four files green.
99
99
 
100
100
  ### T3 Map, Set, Deque, Heap, Grid · Complexity 3 · Est. context 35k
@@ -104,8 +104,7 @@ e.g. `Slice.get(i)` / `List.first()` / `Str.find(..)` return `Option[..]`
104
104
  **Tests:** fast
105
105
  **Model:** sonnet
106
106
  **Consumes:** iterator protocol `next(self) -> Option[Pair[T, Self]]`; `for x := range v` lowers to `while Some(step) := it.next() { .. }` (T1)
107
- **Produces:** `Map.get(k) -> Option[V]`, `Deque.popFront() -> Option[..]`, `Heap.pop() -> Option[..]`, `Grid.get(x, y) -> Option[T]`
107
+ **Produces:** Map, Set, Deque, Heap and Grid ported (lookups and pops return Option)
108
- e.g. `Map.empty().with("a", 1).get("a")` -> `Some(1)`; `.get("b")` -> `None`
109
108
  **Test code:** `plum test` on the five files green.
110
109
 
111
110
  ### T4 game Affine and Input · Complexity 2 · Est. context 25k
@@ -115,7 +114,7 @@ e.g. `Map.empty().with("a", 1).get("a")` -> `Some(1)`; `.get("b")` -> `None`
115
114
  **Tests:** fast
116
115
  **Model:** sonnet
117
116
  **Consumes:** iterator protocol `next(self) -> Option[Pair[T, Self]]`; `for x := range v` lowers to `while Some(step) := it.next() { .. }` (T1)
118
- **Produces:** `Affine.inverse() -> Option[Affine]`, Input lookups return Option
117
+ **Produces:** game Affine, Input, Vec2, Color and Game ported (lookups return Option)
119
118
  **Test code:** `plum test` on the owned files green.
120
119
 
121
120
  ### T5 Json · Complexity 5 · Est. context 50k
@@ -124,7 +123,7 @@ e.g. `Map.empty().with("a", 1).get("a")` -> `Some(1)`; `.get("b")` -> `None`
124
123
  **LOC owned:** 150
125
124
  **Tests:** fast
126
125
  **Model:** sonnet
127
- **Consumes:** `Map.get(k) -> Option[V]`, `Deque.popFront() -> Option[..]`, `Heap.pop() -> Option[..]`, `Grid.get(x, y) -> Option[T]` (T3)
126
+ **Consumes:** Map, Set, Deque, Heap and Grid ported (lookups and pops return Option) (T3)
128
127
  **Produces:** `enum JsonError`; `Json.parse(s) -> Result[Json, JsonError]`; decoders `fromJson(j) -> Result[T, JsonError]`; field lookups `-> Option[Json]`
129
128
  e.g. `Json.parse("[1,2]")` -> `Ok(JsonArr(..))`; `Json.parse("[1,")` -> `Err(..)` carrying pos/text as today's error does
130
129
  **Notes:** keep the JsonParse/Failed shape where tests assert it, or map it into JsonError with the same fields. Generic statics `T.fromJson` keep working (type_param_static).
@@ -136,7 +135,7 @@ e.g. `Json.parse("[1,2]")` -> `Ok(JsonArr(..))`; `Json.parse("[1,")` -> `Err(..)
136
135
  **LOC owned:** 100
137
136
  **Tests:** fast
138
137
  **Model:** sonnet
139
- **Consumes:** `Base64.decode(s) -> Result[Buffer, Base64Error]`, `Uuid.parse(s) -> Result[Uuid, UuidError]`, Testing helpers on Option (T2)
138
+ **Consumes:** Testing, Bool, Base64 and Uuid ported (decode/parse return Result with a module error enum) (T2)
140
139
  **Produces:** `enum IoError` (e.g. `NoSuchPath | ..`); Os fs/env calls `-> Result[T, IoError]` or `Option[T]`
141
140
  e.g. `Os.readFile(io, "missing")` -> `Err(NoSuchPath)`
142
141
  **Test code:** `plum test` on the owned files green.
@@ -147,7 +146,7 @@ e.g. `Os.readFile(io, "missing")` -> `Err(NoSuchPath)`
147
146
  **LOC owned:** 60
148
147
  **Tests:** fast
149
148
  **Model:** sonnet
150
- **Consumes:** `Affine.inverse() -> Option[Affine]`, Input lookups return Option (T4)
149
+ **Consumes:** game Affine, Input, Vec2, Color and Game ported (lookups return Option) (T4)
151
150
  **Produces:** FunctionalScene lookups (find/hit-test/parent) return `Option[..]`
152
151
  **Test code:** `plum test` on the owned files green.
153
152
 
@@ -157,7 +156,7 @@ e.g. `Os.readFile(io, "missing")` -> `Err(NoSuchPath)`
157
156
  **LOC owned:** 60
158
157
  **Tests:** fast
159
158
  **Model:** sonnet
160
- **Consumes:** `Affine.inverse() -> Option[Affine]`, Input lookups return Option (T4)
159
+ **Consumes:** game Affine, Input, Vec2, Color and Game ported (lookups return Option) (T4)
161
160
  **Produces:** Scene `mut parent: Option[Actor]` and lookups returning `Option[Actor]`
162
161
  **Notes:** `Some(ship)` into `Option[Actor]` needs the expected type to flow into the argument; if the checker rejects it, log an incident and annotate (`p: Option[Actor] = Some(ship)`).
163
162
  **Test code:** `plum test plum-std/game/imperative/Scene.plum` green.
@@ -168,7 +167,7 @@ e.g. `Os.readFile(io, "missing")` -> `Err(NoSuchPath)`
168
167
  **LOC owned:** 20
169
168
  **Tests:** fast
170
169
  **Model:** sonnet
171
- **Consumes:** `Affine.inverse() -> Option[Affine]`, Input lookups return Option (T4)
170
+ **Consumes:** game Affine, Input, Vec2, Color and Game ported (lookups return Option) (T4)
172
171
  **Produces:** Actions lookups return `Option[..]`
173
172
  **Test code:** `plum test plum-std/game/pure/Actions.plum` green.
174
173
 
@@ -218,7 +217,7 @@ e.g. `Os.readFile(io, "missing")` -> `Err(NoSuchPath)`
218
217
  ### T14 examples: errors.plum · Complexity 4 · Est. context 45k
219
218
  **Owns:** plum-examples/errors.plum
220
219
  **Components:** 1
221
- **LOC owned:** 200
220
+ **LOC owned:** 160
222
221
  **Tests:** fast
223
222
  **Model:** sonnet
224
223
  **Consumes:** `enum IoError` (e.g. `NoSuchPath | ..`); Os fs/env calls `-> Result[T, IoError]` or `Option[T]` (T6)
@@ -232,7 +231,7 @@ e.g. `Os.readFile(io, "missing")` -> `Err(NoSuchPath)`
232
231
  **LOC owned:** 80
233
232
  **Tests:** fast
234
233
  **Model:** sonnet
235
- **Consumes:** `Base64.decode(s) -> Result[Buffer, Base64Error]`, `Uuid.parse(s) -> Result[Uuid, UuidError]`, Testing helpers on Option (T2)
234
+ **Consumes:** Testing, Bool, Base64 and Uuid ported (decode/parse return Result with a module error enum) (T2)
236
235
  **Produces:** examples A ported (own `next` fns return `Option[Pair[..]]`)
237
236
  **Test code:** `plum test` on the four files green.
238
237
 
docs/plans/2026-11-05-language-simplification-d2.results.md ADDED
@@ -0,0 +1,24 @@
1
+ # Run D2 results (2026-11-05)
2
+
3
+ All of plum-std, plum-examples (incl. games) and the plum-game web-test fixtures use std `Option`/`Result`, postfix `?` and `x!`; no `null`, `catch`, `try`, `?:`, `?.`, `T?`, `E!T`, `error` decls or `error.X` remain in .plum code (grep hits left are the JSON `null` literal and wasm comments). Old syntax still parses; D3 removes it.
4
+
5
+ ## API shape after the port
6
+ - Iterators: `next(self) -> Option[Pair[T, Self]]`; `for` lowers to `while Some(step) := it.next()`.
7
+ - Single-cause lookups return Option (Slice/List get/first/last/pop, Map.get, Deque/Heap pops, scene hit tests, Affine.invert, Input.position). `Grid.get` stays `T` (no failure path, hot loop).
8
+ - Multi-cause or data-carrying failures return `Result[_, ModError]`, one enum per module with `fun name(self) -> Str`: NumError (Int/Float.parse), BoolError, Base64Error, UuidError, JsonError, IoError (Os fs calls), HttpError. `Json.parse` keeps `Parsed | Failed(pos, text)`.
9
+ - `Map.getOrNull` survives as an alias of `get` (no callers left); D3 deletes it.
10
+
11
+ ## Compiler fixes found by the port
12
+ - Checker: a fn-ending `if`/`match` branch value was never checked against the return type (any type passed); now "return type mismatch" with "wrap it with Some(..)/Ok(..)" hints.
13
+ - Codegen: `None` as a param default panicked (default exprs are clones without checker types); `x!` crash lost its location — now "unwrap on Err(Variant) at file:line:col".
14
+ - Fusion: closure-specialisation receiver hints were always empty since run C (looked up on a clone; masked while `map` had one candidate) — restored with no compile-time cost; this may explain part of run C's unexplained game slowdown (benchmark item). Exposed `drive::dropDeadParams` dropping the only param that fixed a kept closure's input generic.
15
+ - Fixture stress_state_view.plum still used run-A-removed variadic `List(..)`; rewritten to `[..]`.
16
+
17
+ ## Sweep
18
+ `cargo build --workspace --tests` clean; all 63 test binaries green (fusion_alloc_test 3/3, codegen_tests 85, e2e_types 48, e2e_tail 33, question_tests 16, checker_tests 48); `plum test plum-std plum-examples` 662 passed, 65/65 files; every web-test fixture compiles.
19
+
20
+ ## Left for D3
21
+ Tests whose subject is the old syntax (listed in .superlite/progress.md, archived with this run): error_union_tests.rs, parser/formatter old-syntax tests, codegen_tests nullable/catch/inferred-set tests, error_tail error-union twins, checker_tests error-set/null tests, optional_unpinned, types.rs error-union display, simplify/drive error-tag pattern tests.
22
+
23
+ ## Backlog
24
+ `pos := match ..` as a declaration value doesn't parse; `Link(k, rest) => match ..` single-line nested match arm doesn't parse; single-variant enums are rejected (one-error types become structs); `fun g[T](..)` explicit fn generics (queued).
plum-checker/src/lib.rs CHANGED
@@ -912,7 +912,7 @@ fn unifyArg(
912
912
  actual_expr: &ast::Expr,
913
913
  ctx: &CheckCtx,
914
914
  ) -> Result<(), String> {
915
- unify(expected, actual, ctx)?;
915
+ unify(expected, actual, ctx).map_err(|e| format!("{}{}", e, wrapHint(expected, actual, ctx)))?;
916
916
  if let (PlumType::TNamed(variant_name), PlumType::TNamed(actual_name)) = (expected, actual) {
917
917
  if variant_name != actual_name
918
918
  && !subtyping::accepts(expected, actual)
@@ -928,6 +928,28 @@ fn unifyArg(
928
928
  Ok(())
929
929
  }
930
930
 
931
+ /// `: wrap it with Some(..)` / `Ok(..)` when a plain value meets an
932
+ /// `Option[T]` / `Result[T, E]` slot whose success type it fits.
933
+ fn wrapHint(expected: &PlumType, actual: &PlumType, ctx: &CheckCtx) -> &'static str {
934
+ let (ctor, inner) = match expected {
935
+ PlumType::TApp(n, xs) if n == "Option" && xs.len() == 1 => ("Some", &xs[0]),
936
+ PlumType::TApp(n, xs) if n == "Result" && xs.len() == 2 => ("Ok", &xs[0]),
937
+ _ => return "",
938
+ };
939
+ if matches!(actual, PlumType::TApp(n, _) if n == "Option" || n == "Result") {
940
+ return "";
941
+ }
942
+ // Side-effect free: `unify` of two error unions extends the fn's inferred
943
+ // error set, so that is restored after the probe.
944
+ let saved = withCheck(|s| s.err_acc.borrow().clone());
945
+ let fits = unify(inner, actual, ctx).is_ok();
946
+ withCheck(|s| *s.err_acc.borrow_mut() = saved);
947
+ if !fits {
948
+ return "";
949
+ }
950
+ if ctor == "Some" { ": wrap it with Some(..)" } else { ": wrap it with Ok(..)" }
951
+ }
952
+
931
953
  pub fn lookup(env: &TypeEnv, name: &str) -> Result<PlumType, String> {
932
954
  env.get(name)
933
955
  .map(|s| *s.body.clone())
@@ -1195,7 +1217,13 @@ pub fn typedSourceWith(source: &ast::Source, fusion: FusionOptions) -> CheckResu
1195
1217
  let mut pre = preUnionSource(source);
1196
1218
  // Closure specialisation: copies of callees with the closure argument inlined.
1197
1219
  {
1220
+ // `info` is keyed by the addresses of the program it checked (`lowered`, or
1221
+ // `source` itself), not of the clone `pre`; hints are keyed by call position,
1222
+ // so read them off the checked program and apply them to its twin `pre`.
1223
+ let hints = match info.lowered.clone() {
1224
+ Some(checked) => closureHints(&checked, &info),
1198
- let hints = closureHints(&pre, &info);
1225
+ None => closureHints(source, &info),
1226
+ };
1199
1227
  let mut copy = pre.clone();
1200
1228
  if !plum_core::closure_spec::specialiseWith(&mut copy, &hints).is_empty()
1201
1229
  && adoptPre(&copy, &mut result, &mut info)
@@ -3256,7 +3284,19 @@ fn checkStmtInner(
3256
3284
  }
3257
3285
  }
3258
3286
  }
3287
+ // The last value of an `if`/`match` branch is the value of the
3288
+ // enclosing block's tail slot (the fn's return, a catch block).
3259
- Ok(_) => {}
3289
+ Ok(t) => {
3290
+ if let Some(want) = &tail {
3291
+ if let Err(msg) = unifyArg(want, &t, e, ctx) {
3292
+ errors.push(CheckError::at(
3293
+ *line,
3294
+ *column,
3295
+ format!("fn '{}': return type mismatch: {}", fn_name, msg),
3296
+ ));
3297
+ }
3298
+ }
3299
+ }
3260
3300
  }
3261
3301
  }
3262
3302
  ast::Stmt::Assert(c) => match inferExpr(&c.cond, env, ctx) {
plum-checker/tests/closure_hint_test.rs ADDED
@@ -0,0 +1,55 @@
1
+ #![allow(non_snake_case)]
2
+ //! Closure specialisation resolves a method call by the receiver's checked type, not
3
+ //! by the method name being unique: `Box.apply` and `Bag.apply` both exist here.
4
+ use plum_checker::{typedSourceWith, FusionOptions};
5
+ use plum_core::ast::{Expr, Item};
6
+ use plum_core::visit::walkRef;
7
+ use plum_test_support::parse;
8
+
9
+ const TYPES: &str = "\
10
+ struct Box(v: Int) =
11
+ fun apply(self, f: fn(Int) -> Int) -> Int = f(self.v)
12
+
13
+ struct Bag(v: Int) =
14
+ fun apply(self, f: fn(Int) -> Int) -> Int = f(self.v) + 1
15
+
16
+ fun useBox(b: Box) -> Int = b.apply(|x| x + 1)
17
+ ";
18
+
19
+ /// Method names `useBox` calls, in the checked (and specialised) program fusion would see.
20
+ fn calledByUse(src: &str) -> Vec<String> {
21
+ let info = typedSourceWith(&parse(src), FusionOptions { enabled: false, explain: false }).expect("checks");
22
+ let low = info.lowered.as_ref().expect("closure specialisation adopted a rewrite");
23
+ let f = low
24
+ .items
25
+ .iter()
26
+ .find_map(|i| match i {
27
+ Item::Fn(f) if f.name == "useBox" => Some(f),
28
+ _ => None,
29
+ })
30
+ .expect("useBox");
31
+ let mut out = Vec::new();
32
+ let plum_core::ast::FnBody::Expr(e) = &f.body else { panic!("expression body") };
33
+ walkRef(e, &mut |x| {
34
+ if let Expr::Attribute(a) = x {
35
+ if let plum_core::ast::AttrKind::Method(c) = &a.attr {
36
+ out.push(c.name.clone());
37
+ }
38
+ }
39
+ });
40
+ out
41
+ }
42
+
43
+ #[test]
44
+ fn ambiguous_method_name_specialised_by_receiver_type() {
45
+ let calls = calledByUse(TYPES);
46
+ assert!(calls.iter().any(|n| n.starts_with("apply$c")), "Box.apply not specialised: {calls:?}");
47
+ }
48
+
49
+ #[test]
50
+ fn ambiguous_method_name_specialised_in_lowered_program() {
51
+ // a `for` loop makes the checker check a lowered copy, as for every std program
52
+ let src = format!("{TYPES}\nfun count(n: Int) -> Int =\n s := 0\n for i := range n\n s = s + i\n s\n");
53
+ let calls = calledByUse(&src);
54
+ assert!(calls.iter().any(|n| n.starts_with("apply$c")), "Box.apply not specialised: {calls:?}");
55
+ }
plum-checker/tests/question_tests.rs CHANGED
@@ -109,3 +109,67 @@ fn fn_typed_param_shadows_top_level_fn_arity() {
109
109
  // `f(v)` calls the param, not the two-param top-level `fun f`.
110
110
  ok("fun f(first: Int, second: Int) -> Int = first + second\n\nfun apply(f: fn(Int) -> Int, v: Int) -> Int = f(v)\n");
111
111
  }
112
+
113
+ #[test]
114
+ fn bare_value_into_option_return_errors() {
115
+ rejected("fun f(x: Int) -> Option[Int] = x", "wrap it with Some(..)");
116
+ rejected("fun f(o: Option[Int]) -> Option[Int] =\n match o\n Some(v) => v\n None => None", "Some(");
117
+ rejected("fun f(o: Option[Int]) -> Option[Int] =\n if Some(v) := o\n return v\n None", "Some(");
118
+ }
119
+
120
+ #[test]
121
+ fn bare_value_into_result_return_errors() {
122
+ rejected("fun f(x: Int) -> Result[Int, ErrA] = x", "wrap it with Ok(..)");
123
+ }
124
+
125
+ #[test]
126
+ fn bare_value_into_option_arg_and_annotation_errors() {
127
+ rejected("fun g(o: Option[Int]) -> Int = 1\nfun f() -> Int = g(1)", "wrap it with Some(..)");
128
+ rejected("fun f() -> Int =\n o: Option[Int] := 1\n 2", "wrap it with Some(..)");
129
+ rejected("fun f() -> Int =\n o: Option[Int] := None\n o = 1\n 2", "wrap it with Some(..)");
130
+ }
131
+
132
+ #[test]
133
+ fn bare_value_in_branch_tail_into_option_errors() {
134
+ // The FunctionalScene `hitKids` shape: a recursive static on a generic enum.
135
+ rejected(
136
+ "enum Tree[M] =\n | Leaf(m: M)\n | Node(kids: Option[Tree[M]])\n\n fun first(t: Option[Tree[M]], i: Int) -> Option[Int] = match t\n None => None\n Some(x) =>\n if Some(later) := Tree.first(None, i + 1)\n later\n else\n None\n\nfun g() -> Option[Int] = Tree.first(Some(Leaf(1)), 0)\n",
137
+ "wrap it with Some(..)",
138
+ );
139
+ rejected("fun f(o: Option[Int]) -> Option[Int] =\n if Some(later) := o\n later\n else\n None", "wrap it with Some(..)");
140
+ rejected("fun f(o: Option[Int]) -> Option[Int] = match o\n None => None\n Some(v) => v", "wrap it with Some(..)");
141
+ rejected("fun f(x: Int) -> Result[Int, ErrA] =\n if x > 1\n x\n else\n Err(Other)", "wrap it with Ok(..)");
142
+ rejected("fun f(x: Int) -> Int =\n if x > 1\n true\n else\n 2", "expected Int, found Bool");
143
+ ok("fun f(o: Option[Int]) -> Option[Int] =\n if Some(later) := o\n Some(later)\n else\n None");
144
+ }
145
+
146
+
147
+ fn errors(body: &str) -> Vec<String> {
148
+ run(body).err().unwrap_or_default()
149
+ }
150
+
151
+ #[test]
152
+ fn wrap_hint_into_generic_option_slot_is_one_error() {
153
+ let e = errors("fun g(x: T, o: Option[T]) -> Int = 1\nfun f() -> Int = g(1, 2)");
154
+ assert_eq!(e.len(), 1, "{e:?}");
155
+ assert!(e[0].contains("wrap it with Some(..)"), "{e:?}");
156
+ let e = errors("fun h(x: T) -> Option[T] = x\nfun f() -> Int =\n o := h(1)\n 2");
157
+ assert_eq!(e.len(), 1, "{e:?}");
158
+ assert!(e[0].contains("wrap it with Some(..)"), "{e:?}");
159
+ }
160
+
161
+ #[test]
162
+ fn non_final_branch_values_are_not_the_return_value() {
163
+ ok("fun f(x: Int) -> Option[Int] =\n if x > 1\n x\n else\n 2\n match x\n 1 => 3\n _ => 4\n Some(x)");
164
+ ok("fun f(x: Int) -> Option[Int] =\n y := 0\n if x > 1\n y = x\n y\n else\n 0\n None");
165
+ }
166
+
167
+ #[test]
168
+ fn branch_tail_mismatch_reported_once() {
169
+ let e = errors("fun f(o: Option[Int]) -> Option[Int] =\n if Some(v) := o\n v\n else\n None");
170
+ assert_eq!(e.len(), 1, "{e:?}");
171
+ let e = errors("fun f(o: Option[Int]) -> Option[Int] = match o\n None => None\n Some(v) => v");
172
+ assert_eq!(e.len(), 1, "{e:?}");
173
+ let e = errors("fun f(x: Int) -> Option[Int] =\n if x > 1\n if x > 2\n x\n else\n None\n else\n None");
174
+ assert_eq!(e.len(), 1, "{e:?}");
175
+ }
plum-core/src/drive.rs CHANGED
@@ -130,6 +130,23 @@ fn ptGens(p: &ParamType, out: &mut HashSet<String>) {
130
130
  }
131
131
  }
132
132
 
133
+ /// The generics a call argument for a parameter of type `p` fixes (`bound`), and those it
134
+ /// needs fixed elsewhere (`needs`): a fn type's parameter types, which an untyped closure
135
+ /// argument cannot supply. Its result type is inferred from the closure body.
136
+ fn bindingGens(p: &ParamType, bound: &mut HashSet<String>, needs: &mut HashSet<String>) {
137
+ let (ps, r): (&[Type], Option<&Type>) = match p {
138
+ ParamType::Fn(ps, r) => (ps, r.as_deref()),
139
+ ParamType::Type(t) => match t.asFn() {
140
+ Some((ps, r)) => (ps, Some(r)),
141
+ None => return collectGens(t, bound),
142
+ },
143
+ };
144
+ ps.iter().for_each(|t| collectGens(t, needs));
145
+ if let Some(r) = r {
146
+ collectGens(r, bound);
147
+ }
148
+ }
149
+
133
150
  fn unify(a: &Type, b: &Type, map: &mut HashMap<String, Type>) -> Option<()> {
134
151
  if a.generics.is_empty() && isGenericName(&a.name) {
135
152
  map.insert(a.name.clone(), b.clone());
@@ -546,14 +563,16 @@ impl Driver {
546
563
  }
547
564
  // A generic that only this parameter binds (a closure's result type) and the
548
565
  // return type mentions can no longer be inferred at the call once the
549
- // parameter is gone: keep the parameter then.
566
+ // parameter is gone: keep the parameter then. The same holds for a generic in
567
+ // the parameter types of a kept fn-typed parameter: an untyped closure
568
+ // argument (`|t| t.name`) cannot fix its own parameter types.
550
569
  let mut covered = HashSet::new();
570
+ let mut needed = HashSet::new();
551
571
  for (i, p) in f.params.iter().enumerate() {
552
572
  if !idx.contains(&i) {
553
- ptGens(&p.ty, &mut covered);
573
+ bindingGens(&p.ty, &mut covered, &mut needed);
554
574
  }
555
575
  }
556
- let mut needed = HashSet::new();
557
576
  if let Some(r) = &f.returns {
558
577
  collectGens(r, &mut needed);
559
578
  }
plum-core/src/forms.rs CHANGED
@@ -9,8 +9,8 @@
9
9
  //! What a loop lowers to depends on the type of its source:
10
10
  //!
11
11
  //! * `Int`: `i` counts 0, 1, ... n - 1.
12
- //! * a type with `next(self) -> Pair[T, Self]?`:
12
+ //! * a type with `next(self) -> Option[Pair[T, Self]]`:
13
- //! `it := v; while step := it.next() { x := step.first; it = step.second; .. }`.
13
+ //! `it := v; while Some(step) := it.next() { x := step.first; it = step.second; .. }`.
14
14
  //! * anything else: plain iteration over `length()` / `at(i)`, lowered to a
15
15
  //! `while` loop.
16
16
  //!
@@ -281,8 +281,8 @@ fn lowerInt(f: ForLoop) -> Vec<Stmt> {
281
281
  ]
282
282
  }
283
283
 
284
- /// `for x := range v` over a type with `next(self) -> Pair[T, Self]?`:
284
+ /// `for x := range v` over a type with `next(self) -> Option[Pair[T, Self]]`:
285
- /// `it := v; while step := it.next() { x := step.first; it = step.second; ... }`.
285
+ /// `it := v; while Some(step) := it.next() { x := step.first; it = step.second; ... }`.
286
286
  /// The iterator is rebound before the body, so a `continue` still advances it.
287
287
  fn lowerNext(f: ForLoop) -> Vec<Stmt> {
288
288
  let id = freshId();
@@ -300,7 +300,10 @@ fn lowerNext(f: ForLoop) -> Vec<Stmt> {
300
300
  inner.extend(f.body.stmts);
301
301
  let cond = Expr::Bind(Box::new(BindExpr {
302
302
  case: Case {
303
+ patterns: vec![CasePattern::Class {
304
+ name: "Some".to_string(),
303
- patterns: vec![CasePattern::Name(step.clone())],
305
+ fields: vec![CasePattern::Name(step.clone())],
306
+ }],
304
307
  guard: None,
305
308
  body: Block {
306
309
  stmts: vec![Stmt::Expr(Expr::BoolLit(true), f.line, f.column)],
plum-core/tests/for_next_test.rs CHANGED
@@ -1,7 +1,7 @@
1
1
  #![allow(non_snake_case)]
2
2
  //! The one loop form `for x := range v`: `v: Int` counts 0..v; any other `v`
3
- //! whose type has `next(self) -> Pair[T, Self]?` iterates (null = done, else
3
+ //! whose type has `next(self) -> Option[Pair[T, Self]]` iterates (None = done, else
4
- //! `Pair(value, rest)`, the iterator rebound to `rest` each step).
4
+ //! `Some(Pair(value, rest))`, the iterator rebound to `rest` each step).
5
5
  use plum_test_support::runInt;
6
6
 
7
7
  #[test]
@@ -40,13 +40,14 @@ fn list_next_steps() {
40
40
  import std/Int
41
41
  import std/List
42
42
  import std/Pair
43
+ import std/Option
43
44
 
44
45
  fun main() -> Int =
45
46
  xs := [5, 6]
46
47
  p := xs.next()!
47
48
  q := p.second.next()!
48
49
  r := q.second.next()
49
- p.first * 100 + q.first * 10 + {if r == null then 1 else 0}
50
+ p.first * 100 + q.first * 10 + {if r.isNone() then 1 else 0}
50
51
  ";
51
52
  assert_eq!(runInt("for_next_list_next", src), 561);
52
53
  }
@@ -56,10 +57,11 @@ fn range_user_struct_with_next() {
56
57
  let src = "\
57
58
  import std/Int
58
59
  import std/Pair
60
+ import std/Option
59
61
 
60
62
  struct Countdown(n: Int) =
61
- fun next(self) -> Pair[Int, Countdown]? =
63
+ fun next(self) -> Option[Pair[Int, Countdown]] =
62
- if self.n == 0 then null else Pair(first: self.n, second: Countdown(n: self.n - 1))
64
+ if self.n == 0 then None else Some(Pair(first: self.n, second: Countdown(n: self.n - 1)))
63
65
 
64
66
  fun main() -> Int =
65
67
  acc := 0
@@ -75,10 +77,11 @@ fn range_next_supports_continue_and_break() {
75
77
  let src = "\
76
78
  import std/Int
77
79
  import std/Pair
80
+ import std/Option
78
81
 
79
82
  struct Up(i: Int, n: Int) =
80
- fun next(self) -> Pair[Int, Up]? =
83
+ fun next(self) -> Option[Pair[Int, Up]] =
81
- if self.i >= self.n then null else Pair(first: self.i, second: Up(i: self.i + 1, n: self.n))
84
+ if self.i >= self.n then None else Some(Pair(first: self.i, second: Up(i: self.i + 1, n: self.n)))
82
85
 
83
86
  fun main() -> Int =
84
87
  acc := 0
plum-core/tests/fused_closure_generic_test.rs ADDED
@@ -0,0 +1,28 @@
1
+ #![allow(non_snake_case)]
2
+ //! A fused fn keeps a specialised-away closure parameter when it is the only thing that
3
+ //! fixes a generic another kept closure parameter takes as its input: `map(andThen(..))`
4
+ //! on `Option` fuses to `map$c0$andThen$c0$0(|t| t.name, u)`, whose `|t| ..` needs the
5
+ //! `B` that only `andThen`'s closure binds. Dropping it left the fused fn uninstantiable
6
+ //! ("unknown function" at codegen).
7
+ use plum_test_support::runInt;
8
+
9
+ #[test]
10
+ fn option_and_then_then_map_compiles_and_runs() {
11
+ let src = "\
12
+ import std/Str
13
+ import std/Int
14
+ import std/Option
15
+
16
+ struct Target(size: Int)
17
+ struct User(name: Str, target: Option[Target])
18
+
19
+ fun targetSize(u: Option[User]) -> Option[Int] =
20
+ u.andThen(|v| v.target).map(|t| t.size)
21
+
22
+ fun main() -> Int =
23
+ full := User(name: \"u\", target: Some(Target(size: 40)))
24
+ none: Option[User] := None
25
+ targetSize(Some(full)).unwrapOr(0) + targetSize(none).unwrapOr(2)
26
+ ";
27
+ assert_eq!(runInt("fused_closure_generic", src), 42);
28
+ }
plum-core/tests/record_init_test.rs CHANGED
@@ -117,6 +117,8 @@ fn init_with_params_rejected() {
117
117
  fn annotate_error_and_annotation() {
118
118
  let (_, e) = lower("struct Rec(a: Int) =\n fun init(self) =\n self.t = null\n");
119
119
  assert!(e.iter().any(|m| m.starts_with("annotate the type: self.t: T = ...")), "{e:?}");
120
+ let (_, e) = lower("struct Rec(a: Int) =\n fun init(self) =\n self.t: Option[Int] = None\n");
121
+ assert!(e.is_empty(), "{e:?}");
120
122
  let (s, e) = lower("struct Rec(a: Int) =\n fun init(self) =\n self.t: Int? = null\n self.l = Slice[Int]()\n self.s = \"x\"\n self.b = true\n");
121
123
  assert!(e.is_empty(), "{e:?}");
122
124
  let v = &enumOf(&s, "Rec").variants[0];
plum-core/tests/tail_test.rs CHANGED
@@ -59,6 +59,8 @@ fn not_tail_as_argument() {
59
59
  fn not_tail_inside_try() {
60
60
  let src = "fun f(n: Int) -> !Int =\n try g(n)\n";
61
61
  assert_eq!(sites(src, "f"), vec![]);
62
+ let src = "fun f(n: Int) -> Result[Int, Str] =\n g(n)?\n";
63
+ assert_eq!(sites(src, "f"), vec![]);
62
64
  let src = "fun f(n: Int) -> Int =\n g(n)!\n";
63
65
  assert_eq!(sites(src, "f"), vec![]);
64
66
  }
plum-examples/errors.plum CHANGED
@@ -1,107 +1,140 @@
1
- # Errors and optionals: error propagation and T?.
1
+ # Errors and absence: `Option[T]`, `Result[T, E]`, postfix `?` and `x!`.
2
2
  import std/Int
3
3
  import std/Float
4
4
  import std/Str
5
5
  import std/Slice
6
6
  import std/Map
7
7
  import std/List
8
+ import std/Option
9
+ import std/Result
8
10
 
9
- # ==== error propagation ====
11
+ # ==== Result: a value or an error ====
10
12
 
11
- # `!T` is "a `T` or an error". An error is a value written `error.Name` (for
13
+ # `Result[T, E]` is `Ok(value)` or `Err(err)`. A failure is an ordinary value of
12
- # example `return error.NegativeNum`): it needs no declaration and carries no
13
- # data. A function's error set is inferred from its body (every error value it
14
- # returns, plus the sets of the calls it propagates with `try`), or pinned in
15
- # front of the `!`: `error{A, B}!T` inline, `ParseError!T` for a set declared
14
+ # an ordinary type `E`: usually one enum per module whose cases name what can go
16
- # with `error ParseError = A | B`, or `(A || B)!T` to merge sets. Pin the set on
15
+ # wrong (`NumError` in std/Int, `IoError` in std/Os, `JsonError` in std/Json).
17
- # a public API, on a recursive function and on an `extern fun`. There is no
16
+ # Nothing is implicit: a success is written `Ok(n)`, a failure `Err(Case)`.
18
- # `Option`: an optional `T?` (see optionals.plum) is not an error.
19
17
  #
20
- # A plain `T` (a tail value or `return n`) is the success value; an error value
21
- # (`return error.X`) is the failure. A bare name is never an implicit failure.
22
- #
23
- # `try expr` unwraps the success value, or returns the error from the enclosing
18
+ # `r?` unwraps the `Ok` value, or returns the `Err` from the enclosing function
19
+ # (which must itself return a `Result` with the same error type). `r!` unwraps
20
+ # the value or crashes. `r.unwrapOr(d)` falls back to `d`. `r.map(f)` changes the
24
- # function. The sets of different calls merge automatically, so `try Int.parse(s)`
21
+ # success value, `r.andThen(f)` chains a step that can fail, and `r.mapErr(f)`
25
- # (`[BadNumStr]`) and `try xs.get(i)` (`[OutOfBounds]`) can share one function.
26
- #
27
- # `expr!` unwraps the success value, or crashes with a message naming the
28
- # callee, the error and the source position.
29
- #
30
- # `expr catch default` is the plain-expression counterpart: no early return,
31
- # `default` is used on any error. The default may diverge, Zig-style:
32
- # `Int.parse(s) catch return error.InvalidInput` returns that error from the function.
22
+ # changes the error type, which is how errors cross module boundaries.
33
- # `expr catch e` plus an indented block binds `e` to the error name; `match e`
34
- # is exhaustive over the exact set and `e.name()` is its text.
35
23
  #
36
24
  # `crash "msg"` aborts the program with a message; use it for bugs, not for
37
25
  # expected failures.
38
26
 
27
+ # What can go wrong with a positive number.
39
- fun parsePositive(s: Str) -> !Int =
28
+ enum PositiveError =
40
- n := try Int.parse(s)
41
- if n < 0
29
+ | BadNumber
42
- return error.NegativeNum
30
+ | NegativeNum
43
- return n
44
31
 
45
- fun sumTwo(a: Str, b: Str) -> !Int =
32
+ fun name(self) -> Str = match self
33
+ BadNumber => "BadNumber"
46
- x := try parsePositive(a)
34
+ NegativeNum => "NegativeNum"
47
- y := try parsePositive(b)
48
- x + y
49
35
 
36
+ # `Int.parse` fails with std/Int's `NumError`; `mapErr` turns it into ours.
37
+ fun parsePositive(s: Str) -> Result[Int, PositiveError] =
38
+ n := Int.parse(s).mapErr(|e| BadNumber)?
39
+ if n < 0
40
+ return Err(NegativeNum)
41
+ Ok(n)
42
+
43
+ # `?` returns the first error on the way out and skips the rest of the function.
44
+ fun sumTwo(a: Str, b: Str) -> Result[Int, PositiveError] =
45
+ x := parsePositive(a)?
46
+ y := parsePositive(b)?
47
+ Ok(x + y)
48
+
50
- # Two calls with different error sets merge into [BadNumStr, OutOfBounds].
49
+ # Two sources of failure with different types share one error enum.
50
+ enum NumAtError =
51
+ | BadDigits
52
+ | OutOfRange
53
+
54
+ fun name(self) -> Str = match self
55
+ BadDigits => "BadDigits"
56
+ OutOfRange => "OutOfRange"
57
+
58
+ # `okOr` turns an `Option` into a `Result` by naming the error for `None`.
51
- fun numAt(s: Str, i: Int) -> error{BadNumStr, OutOfBounds}!Int =
59
+ fun numAt(s: Str, i: Int) -> Result[Int, NumAtError] =
52
- n := try Int.parse(s)
60
+ n := Int.parse(s).mapErr(|e| BadDigits)?
53
61
  xs := [10, 20, 30].toSlice()
54
- x := try xs.get(i)
62
+ x := xs.get(i).okOr(OutOfRange)?
55
- n + x
63
+ Ok(n + x)
56
64
 
57
- # `catch return error.X` returns that error on the way out.
58
- fun parseOrInvalid(s: Str) -> error{InvalidInput}!Int =
65
+ fun numAtCode(s: Str, i: Int) -> Int = match numAt(s, i)
66
+ Ok(v) => v
67
+ Err(BadDigits) => 100
59
- n := Int.parse(s) catch return error.InvalidInput
68
+ Err(OutOfRange) => 200
60
- n
61
69
 
70
+ enum InputError =
71
+ | InvalidInput
72
+ | Other
73
+
74
+ fun name(self) -> Str = match self
75
+ InvalidInput => "InvalidInput"
76
+ Other => "Other"
77
+
78
+ # `mapErr` maps the error to another one.
79
+ fun parseOrInvalid(s: Str) -> Result[Int, InputError] =
80
+ Int.parse(s).mapErr(|e| InvalidInput)
81
+
82
+ # `?` on an `Option` returns `None`; the function returns an `Option`.
62
- fun doubledFirst(list: Slice[Int]) -> error{ListWasEmpty}!Int =
83
+ fun doubledFirst(list: Slice[Int]) -> Option[Int] =
63
- a := try list.first()
84
+ a := list.first()?
64
- a * 2
85
+ Some(a * 2)
65
86
 
66
87
  fun sumOrZero(a: Str, b: Str) -> Int =
67
- x := {parsePositive(a) catch 0}
88
+ x := parsePositive(a).unwrapOr(0)
68
- y := {parsePositive(b) catch 0}
89
+ y := parsePositive(b).unwrapOr(0)
69
90
  x + y
70
91
 
71
92
  fun firstOrDefault(list: Slice[Int], default: Int) -> Int =
72
- {list.first() catch default}
93
+ list.first().unwrapOr(default)
73
94
 
74
95
  fun mustBePositive(n: Int) -> Int =
75
96
  if n < 0
76
97
  crash "n must not be negative"
77
98
  n
78
99
 
79
- # `try` is a prefix operator at unary precedence: `n * try f()` is `n * {try f()}`.
100
+ # `?` is a postfix operator: `n * f()?` is `n * {f()?}`.
80
- fun scaled(n: Int, s: Str) -> error{BadNumStr}!Int =
101
+ fun scaled(n: Int, s: Str) -> Result[Int, NumError] =
81
- n * try Int.parse(s)
102
+ Ok(n * Int.parse(s)?)
103
+
104
+ # Mapping and chaining without leaving the Result.
105
+ fun doubledPositive(s: Str) -> Result[Int, PositiveError] =
106
+ parsePositive(s).map(|n| n * 2)
107
+
108
+ fun halfOfPositive(s: Str) -> Result[Int, PositiveError] =
109
+ parsePositive(s).andThen(|n| if n % 2 == 0 then Ok(n / 2) else Err(BadNumber))
110
+
111
+ enum AgeError =
112
+ | NotANumber
113
+ | Negative
114
+ | TooOld
82
115
 
116
+ fun name(self) -> Str = match self
117
+ NotANumber => "NotANumber"
118
+ Negative => "Negative"
119
+ TooOld => "TooOld"
120
+
83
- fun parseAge(s: Str) -> !Int =
121
+ fun parseAge(s: Str) -> Result[Int, AgeError] =
84
- n := try Int.parse(s)
122
+ n := Int.parse(s).mapErr(|e| NotANumber)?
85
123
  if n < 0
86
- return error.Negative
124
+ return Err(Negative)
87
125
  if n > 150
88
- return error.TooOld
126
+ return Err(TooOld)
89
- n
127
+ Ok(n)
90
-
128
+
91
- # Handling errors with `catch e`: `match e` needs one arm per name in the set
129
+ # Handling errors with `match`: one arm per case (or `_`), and every arm
92
- # (or `_`), and the block must produce the success type or diverge.
130
+ # produces the same type.
93
- fun describeAge(s: Str) -> Str =
131
+ fun describeAge(s: Str) -> Str = match parseAge(s)
94
- n := parseAge(s) catch e
132
+ Ok(n) => "age {n}"
95
- match e
96
- BadNumStr =>
97
- return "not a number"
133
+ Err(NotANumber) => "not a number"
98
- Negative =>
99
- return "can't be negative"
134
+ Err(Negative) => "can't be negative"
100
- TooOld =>
101
- return "too old"
135
+ Err(TooOld) => "too old"
102
- "age {n}"
136
+
103
-
104
- # A plain list of cases is an ordinary enum.
137
+ # A plain list of cases is an ordinary enum; a case can carry data.
105
138
  enum Failure = Timeout | Refused | BadRequest(v: Str)
106
139
 
107
140
  fun isRetryable(f: Failure) -> Bool =
@@ -111,113 +144,115 @@ fun isRetryable(f: Failure) -> Bool =
111
144
  BadRequest(_) => false
112
145
 
113
146
  # `_` catches whatever the earlier arms didn't.
114
- fun ageOrDefault(s: Str) -> Int =
147
+ fun ageOrDefault(s: Str) -> Int = match parseAge(s)
115
- parseAge(s) catch e
148
+ Ok(n) => n
116
- match e
117
- TooOld => 150
149
+ Err(TooOld) => 150
118
- _ => 0
150
+ Err(_) => 0
119
-
151
+
120
- # `e.name()` is the error's text.
152
+ # `e.name()` is the error's text, when the error enum defines it.
121
- fun errorText(s: Str) -> Str =
153
+ fun errorText(s: Str) -> Str = match parseAge(s)
122
- parseAge(s) catch e
123
- return e.name()
124
- "none"
154
+ Ok(_) => "none"
125
-
155
+ Err(e) => e.name()
156
+
157
+ # An error can be any type, including a struct that carries data.
158
+ struct Neg(value: Int)
159
+
126
- fun checkNonNeg(n: Int) -> error{Neg}!Unit =
160
+ fun checkNonNeg(n: Int) -> Result[Unit, Neg] =
127
161
  if n < 0
128
- return error.Neg
162
+ return Err(Neg(value: n))
163
+ Ok(Unit)
129
164
 
130
- fun passCheck(n: Int) -> error{Neg}!Unit =
165
+ fun passCheck(n: Int) -> Result[Unit, Neg] =
131
- try checkNonNeg(n)
166
+ checkNonNeg(n)
132
167
 
133
- fun runChecked(n: Int) -> error{Neg}!Int =
168
+ fun runChecked(n: Int) -> Result[Int, Neg] =
134
- try checkNonNeg(n)
169
+ checkNonNeg(n)?
135
- n
170
+ Ok(n)
136
171
 
137
- fun negOrSeven(n: Int) -> error{Neg}!Int =
172
+ fun negOrSeven(n: Int) -> Result[Int, Neg] =
138
173
  if n < 0
139
- return error.Neg
174
+ return Err(Neg(value: n))
140
- n
175
+ Ok(n)
141
176
 
142
- fun lookup(m: Map[Int, error{Neg}!Int]) -> Int =
177
+ fun lookup(m: Map[Int, Result[Int, Neg]]) -> Int =
143
- v := m.get(1) catch negOrSeven(-1)
178
+ v := m.get(1).unwrapOr(negOrSeven(-1))
144
- v catch 0
179
+ v.unwrapOr(0)
145
180
 
146
- # `try` on a type parameter's static method, bound with `:=` inside a loop: the
181
+ # `?` on a type parameter's static method, bound with `:=` inside a loop: the
147
- # operand's type is `T`, so it is checked per instantiation (here `T = Age`).
182
+ # operand's type is `Result[T, _]`, so it is checked per instantiation (here
183
+ # `T = Age`).
148
184
  struct Age(n: Int) =
149
- fun parse(s: Str) -> !Age =
185
+ fun parse(s: Str) -> Result[Age, PositiveError] =
150
- n := try parsePositive(s)
186
+ n := parsePositive(s)?
151
- return Age(n)
187
+ return Ok(Age(n))
152
188
 
153
189
  struct Ages[T](items: Slice[T]) =
154
- fun parseAll(xs: Slice[Str]) -> !Ages[T] =
190
+ fun parseAll(xs: Slice[Str]) -> Result[Ages[T], PositiveError] =
155
191
  out := Slice[T]()
156
192
  for x := range xs
157
- v := try T.parse(x)
193
+ v := T.parse(x)?
158
194
  out.push(v)
159
- return Ages(out)
195
+ return Ok(Ages(out))
160
196
 
161
- fun parseAges(xs: Slice[Str]) -> !Ages[Age] = Ages.parseAll(xs)
197
+ fun parseAges(xs: Slice[Str]) -> Result[Ages[Age], PositiveError] = Ages.parseAll(xs)
162
198
 
163
- # ==== optionals ====
199
+ # ==== Option: a value or nothing ====
164
200
 
165
- # Optionals: `T?` is a `T` or `null`. A plain `T` in a `T?` slot is the present
201
+ # `Option[T]` is `Some(value)` or `None`. A present value is always written
166
- # value. An optional is handled with `?:` (Kotlin elvis), `if x := opt`,
202
+ # `Some(v)`. An option is handled with `unwrapOr(d)`, `if Some(x) := opt`,
167
- # `while x := opt`, `opt!` (crashes on null) and `opt == null`; `catch` and `try`
203
+ # `while Some(x) := opt`, `opt!` (crashes on `None`), `isSome()` and `isNone()`
168
- # are for errors only. `!T?` is an error union whose success value is an optional.
204
+ # (`==` on an Option is an error: it would compare identity, not contents).
169
205
  #
170
- # `obj?.field` / `obj?.method(args)` is safe navigation: it reaches into an
206
+ # `opt.map(f)` reaches into an option without unwrapping it and passes `None`
207
+ # through; `opt.andThen(f)` does the same for a step that itself returns an
171
- # optional without unwrapping it and passes null through. A field that is itself
208
+ # option, so a chain of fields is `u.andThen(|v| v.target).map(|t| t.name)`.
172
- # a `T?` flattens, so `u?.target?.name` is one `Str?`. It is sugar for
173
- # `obj.andThen(|v| v.field)`.
174
209
 
175
210
  struct Box(label: Str)
176
211
 
177
- struct Holder(mut item: Box?, mut count: Int?)
212
+ struct Holder(mut item: Option[Box], mut count: Option[Int])
178
213
 
179
- fun maybeBox(c: Bool) -> Box? =
214
+ fun maybeBox(c: Bool) -> Option[Box] =
180
215
  if c
181
- return Box(label: "in")
216
+ return Some(Box(label: "in"))
182
- return null
217
+ return None
183
218
 
184
- fun maybeInt(c: Bool) -> Int? =
219
+ fun maybeInt(c: Bool) -> Option[Int] =
185
220
  if c
186
- return 7
221
+ return Some(7)
187
- null
222
+ None
188
223
 
189
- fun orFive(n: Int? = null) -> Int =
224
+ fun orFive(n: Option[Int] = None) -> Int =
190
- n ?: 5
225
+ n.unwrapOr(5)
191
226
 
227
+ # `okOr` names the error for `None`; `?` then propagates it.
192
- fun failOnNull(c: Bool) -> error{Absent}!Int =
228
+ fun failOnNone(c: Bool) -> Result[Int, Str] =
193
- n := maybeInt(c) ?: return error.Absent
229
+ n := maybeInt(c).okOr("absent")?
194
- n + 1
230
+ Ok(n + 1)
195
231
 
196
- fun nullOnNull(c: Bool) -> Int? =
232
+ fun noneOnNone(c: Bool) -> Option[Int] =
197
- n := maybeInt(c) ?: return null
233
+ n := maybeInt(c)?
198
- n * 2
234
+ Some(n * 2)
199
235
 
200
- fun chainDefault(a: Int?, b: Int?) -> Int =
236
+ fun chainDefault(a: Option[Int], b: Option[Int]) -> Int =
201
- a ?: b ?: 99
237
+ a.unwrapOr(b.unwrapOr(99))
202
238
 
239
+ # A Result whose success value is an Option.
203
- fun fallible(n: Int) -> error{Bad}!Int? =
240
+ fun fallible(n: Int) -> Result[Option[Int], Str] =
204
241
  if n < 0
205
- return error.Bad
242
+ return Err("bad")
206
243
  if n == 0
207
- return null
244
+ return Ok(None)
208
- n
245
+ Ok(Some(n))
209
246
 
210
- fun viaTry(n: Int) -> error{Bad}!Int =
247
+ fun viaQuestion(n: Int) -> Result[Int, Str] =
211
- v := try fallible(n)
248
+ v := fallible(n)?
212
- v ?: -1
249
+ Ok(v.unwrapOr(-1))
213
250
 
214
- fun nextItem(items: Slice[Int], i: Int) -> Int? =
251
+ fun nextItem(items: Slice[Int], i: Int) -> Option[Int] =
215
- if i < items.length()
216
- return items.get(i)!
252
+ items.get(i)
217
- null
218
253
 
219
- fun sparse() -> Slice[Int?] =
254
+ fun sparse() -> Slice[Option[Int]] =
220
- xs: List[Int?] := [1, null, 3]
255
+ xs: List[Option[Int]] := [Some(1), None, Some(3)]
221
256
  xs.toSlice()
222
257
 
223
258
  fun sumBig() -> Int =
@@ -230,76 +265,74 @@ struct Person(name: Str, age: Int) =
230
265
 
231
266
  struct Target(name: Str)
232
267
 
233
- struct User(name: Str, target: Target?)
268
+ struct User(name: Str, target: Option[Target])
234
269
 
235
- fun personName(p: Person?) -> Str? =
270
+ fun personName(p: Option[Person]) -> Option[Str] =
236
- p?.name
271
+ p.map(|v| v.name)
237
272
 
238
- fun personBirthday(p: Person?) -> Int? =
273
+ fun personBirthday(p: Option[Person]) -> Option[Int] =
239
- p?.birthday()
274
+ p.map(|v| v.birthday())
240
275
 
241
- fun somePerson() -> Person? =
276
+ fun somePerson() -> Option[Person] =
242
- Person(name: "Ann", age: 3)
277
+ Some(Person(name: "Ann", age: 3))
243
278
 
244
- fun noPerson() -> Person? =
279
+ fun noPerson() -> Option[Person] =
245
- return null
280
+ return None
246
281
 
247
- fun noTarget() -> Target? =
282
+ fun noTarget() -> Option[Target] =
248
- return null
283
+ return None
249
284
 
250
- fun maybeCount(n: Int? = null) -> Int? =
285
+ fun maybeCount(n: Option[Int] = None) -> Option[Int] =
251
286
  n
252
287
 
253
- fun noUser() -> User? =
288
+ fun noUser() -> Option[User] =
254
- return null
289
+ return None
255
290
 
256
- fun targetName(u: User?) -> Str? =
291
+ fun targetName(u: Option[User]) -> Option[Str] =
257
- u?.target?.name
292
+ u.andThen(|v| v.target).map(|t| t.name)
258
293
 
259
- # ==== error propagation tests ====
294
+ # ==== Result tests ====
260
295
 
261
- test "try operator unwraps the value and propagates it through two calls"
296
+ test "question mark unwraps the value and propagates it through two calls"
262
297
  assert sumTwo("2", "3")! == 5
263
298
 
264
- test "try operator exits early with the error, skipping the rest of the function"
299
+ test "question mark exits early with the error, skipping the rest of the function"
265
- assert {sumTwo("2", "-3") catch -1} == -1
300
+ assert sumTwo("2", "-3").unwrapOr(-1) == -1
266
301
  assert errorText("-3") == "Negative"
267
302
 
268
- test "try operator merges two different error sets"
303
+ test "an Option and a Result with different error types share one error enum"
269
304
  assert numAt("5", 1)! == 25
270
- assert {numAt("x", 1) catch -1} == -1
305
+ assert numAt("x", 1).unwrapOr(-1) == -1
271
- assert {numAt("5", 9) catch -2} == -2
306
+ assert numAt("5", 9).unwrapOr(-2) == -2
272
- r2 := numAt("5", 9) catch e
307
+ assert numAtCode("5", 9) == 200
273
- match e
274
- BadNumStr => 100
275
- OutOfBounds => 200
276
- assert r2 == 200
308
+ assert numAtCode("x", 1) == 100
277
-
309
+ assert numAtCode("5", 0) == 15
310
+
278
- test "catch return error.X maps the error to another name"
311
+ test "mapErr maps the error to another type"
279
312
  assert parseOrInvalid("7")! == 7
280
- name := parseOrInvalid("x") catch e
313
+ if Err(e) := parseOrInvalid("x")
281
314
  assert e.name() == "InvalidInput"
282
- 0
315
+ else
283
- assert name == 0
316
+ assert false
284
317
 
285
- test "try binds a declaration from a type parameter's static method in a loop"
318
+ test "question mark binds a declaration from a type parameter's static method in a loop"
286
319
  ages := parseAges(["3", "4"].toSlice())!
287
320
  assert ages.items.length() == 2
288
321
  assert ages.items.get(1)!.n == 4
289
- failed := parseAges(["3", "-1"].toSlice()) catch e
322
+ if Err(e) := parseAges(["3", "-1"].toSlice())
290
323
  assert e.name() == "NegativeNum"
291
- Ages(Slice[Age]())
324
+ else
292
- assert failed.items.length() == 0
325
+ assert false
293
326
 
294
- test "try operator on a list that is empty yields ListWasEmpty"
327
+ test "question mark on an empty list yields None"
295
328
  assert doubledFirst([5].toSlice())! == 10
296
- assert {doubledFirst(Slice[Int]()) catch -1} == -1
329
+ assert doubledFirst(Slice[Int]()).unwrapOr(-1) == -1
297
330
 
298
- test "catch default unwraps the value and falls back to 0 on an error"
331
+ test "unwrapOr falls back to 0 on an error"
299
332
  assert sumOrZero("2", "3") == 5
300
333
  assert sumOrZero("2", "-3") == 2
301
334
 
302
- test "catch default falls back to the given default on an empty list"
335
+ test "unwrapOr falls back to the given default on an empty list"
303
336
  assert firstOrDefault([5].toSlice(), 99) == 5
304
337
  assert firstOrDefault(Slice[Int](), 99) == 99
305
338
 
@@ -310,7 +343,14 @@ test "bang unwraps a success value"
310
343
  assert parseAge("30")! == 30
311
344
  assert scaled(2, "21")! == 42
312
345
 
346
+ test "map and andThen transform a success and leave an error alone"
347
+ assert doubledPositive("4")! == 8
348
+ assert doubledPositive("-4").unwrapOr(0) == 0
349
+ assert halfOfPositive("8")! == 4
350
+ assert halfOfPositive("7").unwrapOr(-1) == -1
351
+ assert halfOfPositive("x").unwrapOr(-2) == -2
352
+
313
- test "catch e handles each error name in its own arm"
353
+ test "match handles each error case in its own arm"
314
354
  assert describeAge("42") == "age 42"
315
355
  assert describeAge("abc") == "not a number"
316
356
  assert describeAge("-3") == "can't be negative"
@@ -326,115 +366,117 @@ test "match falls back to a wildcard arm"
326
366
  assert ageOrDefault("999") == 150
327
367
  assert ageOrDefault("x") == 0
328
368
 
329
- # An error union can be the success type of another (a `Slice[!Int]` holds
369
+ # A Result can be the element of a collection: `xs.get(i)` is an
330
- # per-element results): `xs.get(i)` is `!!Int`, unwrapped one level at a time.
370
+ # `Option[Result[Int, E]]`, unwrapped one level at a time.
331
- test "a list of error unions nests one level per access"
371
+ test "a list of results nests one level per access"
332
372
  results := [parsePositive("1"), parsePositive("-1"), parsePositive("x")].toSlice()
333
373
  first := results.get(0)!
334
374
  assert first! == 1
335
375
  second := results.get(1)!
336
- assert {second catch 0} == 0
376
+ assert second.unwrapOr(0) == 0
337
- fallback := {results.get(9) catch parsePositive("5")}
377
+ fallback := results.get(9).unwrapOr(parsePositive("5"))
338
- assert {fallback catch 0} == 5
378
+ assert fallback.unwrapOr(0) == 5
339
379
 
340
- test "a Unit error union propagates with try and is handled with catch"
380
+ test "a Unit result propagates with question mark and is handled with if Err"
341
- assert {runChecked(4) catch 0} == 4
381
+ assert runChecked(4).unwrapOr(0) == 4
342
- assert {runChecked(-1) catch 5} == 5
382
+ assert runChecked(-1).unwrapOr(5) == 5
343
383
  hits := 0
344
- passCheck(1) catch e
384
+ if Err(e) := passCheck(1)
345
385
  hits = hits + 1
386
+ if Err(e) := passCheck(-1)
346
- passCheck(-1) catch e
387
+ assert e.value == -1
347
388
  hits = hits + 10
348
389
  assert hits == 10
349
390
 
350
- test "a Map can hold error unions as values"
391
+ test "a Map can hold results as values"
351
- m: Map[Int, error{Neg}!Int] := Map.empty()
392
+ m: Map[Int, Result[Int, Neg]] := Map.empty()
352
393
  m.set(1, negOrSeven(7))
353
394
  assert lookup(m) == 7
354
395
  m.set(1, negOrSeven(-2))
355
396
  assert lookup(m) == 0
356
397
 
357
- # ==== optionals tests ====
398
+ # ==== Option tests ====
358
399
 
359
- test "null_return_absent"
400
+ test "none_return_absent"
360
- assert maybeBox(false) == null
401
+ assert maybeBox(false).isNone()
361
- assert maybeBox(true) != null
402
+ assert maybeBox(true).isSome()
362
- assert maybeInt(false) == null
403
+ assert maybeInt(false).isNone()
363
- assert null != maybeInt(true)
404
+ assert maybeInt(true).isSome()
364
405
 
365
- test "null_field_and_arg"
406
+ test "none_field_and_arg"
366
- h := Holder(item: null, count: null)
407
+ h := Holder(item: None, count: None)
367
- assert h.item == null
408
+ assert h.item.isNone()
368
- assert h.count == null
409
+ assert h.count.isNone()
369
- h2 := Holder(item: Box(label: "x"), count: 3)
410
+ h2 := Holder(item: Some(Box(label: "x")), count: Some(3))
370
411
  assert h2.count! == 3
371
- assert orFive(null) == 5
372
- assert orFive(8) == 8
412
+ assert orFive() == 5
413
+ assert orFive(None) == 5
414
+ assert orFive(Some(8)) == 8
373
415
  xs := sparse()
374
416
  assert xs.length() == 3
375
417
 
376
- test "elvis_default"
418
+ test "unwrapOr_default"
377
- assert {maybeInt(false) ?: 4} == 4
419
+ assert maybeInt(false).unwrapOr(4) == 4
378
- assert {maybeInt(true) ?: 4} == 7
420
+ assert maybeInt(true).unwrapOr(4) == 7
379
- assert {maybeBox(false)?.label ?: "none"} == "none"
421
+ assert maybeBox(false).map(|b| b.label).unwrapOr("none") == "none"
380
- assert {maybeBox(true)?.label ?: "none"} == "in"
422
+ assert maybeBox(true).map(|b| b.label).unwrapOr("none") == "in"
381
423
 
382
- test "elvis_return_error"
424
+ test "okOr_turns_none_into_an_error"
383
- assert {failOnNull(true) catch 0} == 8
425
+ assert failOnNone(true).unwrapOr(0) == 8
384
- assert {failOnNull(false) catch 0} == 0
426
+ assert failOnNone(false).unwrapOr(0) == 0
385
427
 
386
- test "elvis_return_null"
428
+ test "question_mark_returns_none"
387
- assert nullOnNull(true)! == 14
429
+ assert noneOnNone(true)! == 14
388
- assert nullOnNull(false) == null
430
+ assert noneOnNone(false).isNone()
389
431
 
390
- test "elvis_chain_right_assoc"
432
+ test "unwrapOr_chain"
391
- assert chainDefault(null, null) == 99
433
+ assert chainDefault(None, None) == 99
392
- assert chainDefault(null, 2) == 2
434
+ assert chainDefault(None, Some(2)) == 2
393
- assert chainDefault(1, 2) == 1
435
+ assert chainDefault(Some(1), Some(2)) == 1
394
436
 
395
- test "if_bind_optional"
437
+ test "if_bind_option"
396
438
  r := 0
397
- if x := maybeInt(true)
439
+ if Some(x) := maybeInt(true)
398
440
  r = x
399
441
  else
400
442
  r = -1
401
443
  assert r == 7
402
- if y := maybeInt(false)
444
+ if Some(y) := maybeInt(false)
403
445
  r = y
404
446
  else
405
447
  r = -1
406
448
  assert r == -1
407
- assert {if v := maybeInt(true) then v + 1 else 0} == 8
449
+ assert {if Some(v) := maybeInt(true) then v + 1 else 0} == 8
408
- assert {if w := maybeInt(false) then w + 1 else 0} == 0
450
+ assert {if Some(w) := maybeInt(false) then w + 1 else 0} == 0
409
- if b := maybeBox(true)
451
+ if Some(b) := maybeBox(true)
410
452
  assert b.label == "in"
411
453
 
412
- test "while_bind_optional"
454
+ test "while_bind_option"
413
455
  items := [3, 2, 1].toSlice()
414
456
  total := 0
415
457
  i := 0
416
- while v := nextItem(items, i)
458
+ while Some(v) := nextItem(items, i)
417
459
  total = total + v
418
460
  i = i + 1
419
461
  assert total == 6
420
462
 
421
- test "safe_nav_optional_flattens"
463
+ test "map_over_option_flattens_with_andThen"
422
- assert maybeBox(true)?.label! == "in"
464
+ assert maybeBox(true).map(|b| b.label)! == "in"
423
- assert maybeBox(false)?.label == null
465
+ assert maybeBox(false).map(|b| b.label).isNone()
424
466
 
425
- test "null_eq_ref and null_eq_int"
467
+ test "isNone_and_isSome"
426
468
  b := maybeBox(false)
427
- assert b == null
469
+ assert b.isNone()
428
470
  n := maybeInt(true)
471
+ assert n.isSome()
429
- assert n != null
472
+ assert !n.isNone()
430
- assert !{n == null}
431
473
 
432
- test "error_union_of_optional"
474
+ test "result_of_option"
433
- assert {viaTry(5) catch 0} == 5
475
+ assert viaQuestion(5).unwrapOr(0) == 5
434
- assert {viaTry(0) catch 0} == -1
476
+ assert viaQuestion(0).unwrapOr(0) == -1
435
- assert {viaTry(-3) catch 100} == 100
477
+ assert viaQuestion(-3).unwrapOr(100) == 100
436
478
  x := fallible(0)!
437
- assert x == null
479
+ assert x.isNone()
438
480
  y := fallible(4)!
439
481
  assert y! == 4
440
482
 
@@ -442,52 +484,52 @@ test "int_min_literal"
442
484
  assert Int.Min == -0x8000_0000_0000_0000
443
485
  assert sumBig() == -1
444
486
 
445
- test "reassigning an optional local or field to null or a value"
487
+ test "reassigning an option local or field to None or a value"
446
488
  o := maybeInt(true)
447
- o = null
489
+ o = None
448
- assert o == null
490
+ assert o.isNone()
449
- o = 4
491
+ o = Some(4)
450
492
  assert o! == 4
451
493
  b := maybeBox(true)
452
- b = null
494
+ b = None
453
- assert b == null
495
+ assert b.isNone()
454
- h := Holder(item: Box(label: "x"), count: 2)
496
+ h := Holder(item: Some(Box(label: "x")), count: Some(2))
455
- h.count = null
497
+ h.count = None
456
- h.item = null
498
+ h.item = None
457
- assert h.count == null
499
+ assert h.count.isNone()
458
- assert h.item == null
500
+ assert h.item.isNone()
459
- h.count = 5
501
+ h.count = Some(5)
460
- h.item = Box(label: "y")
502
+ h.item = Some(Box(label: "y"))
461
503
  assert h.count! == 5
462
504
  assert h.item!.label == "y"
463
505
 
464
- test "safe navigation maps a field access over a present value, passes absence through"
506
+ test "map over an option maps a field access over a present value, passes absence through"
465
507
  assert personName(somePerson())! == "Ann"
466
- assert {personName(noPerson()) ?: "gone"} == "gone"
508
+ assert personName(noPerson()).unwrapOr("gone") == "gone"
467
509
 
468
- test "safe navigation maps a method call over a present value"
510
+ test "map over an option maps a method call over a present value"
469
511
  assert personBirthday(somePerson())! == 4
470
- assert {personBirthday(noPerson()) ?: -1} == -1
512
+ assert personBirthday(noPerson()).unwrapOr(-1) == -1
471
513
 
472
- test "safe navigation chains through a nullable field"
514
+ test "andThen chains through an optional field"
473
- full := User(name: "u", target: Target(name: "home"))
515
+ full := User(name: "u", target: Some(Target(name: "home")))
474
- assert {targetName(full) ?: "none"} == "home"
516
+ assert targetName(Some(full)).unwrapOr("none") == "home"
475
- assert {targetName(User(name: "v", target: noTarget())) ?: "none"} == "none"
517
+ assert targetName(Some(User(name: "v", target: noTarget()))).unwrapOr("none") == "none"
476
- assert {targetName(noUser()) ?: "none"} == "none"
518
+ assert targetName(noUser()).unwrapOr("none") == "none"
477
519
 
478
- test "?: handles a nullable with a diverging or computed default"
520
+ test "unwrapOr and bang handle a present or computed default"
479
- p := somePerson() ?: crash "missing"
521
+ p := somePerson()!
480
522
  assert p.age == 3
481
- q := noPerson() ?: Person(name: "gone", age: 1)
523
+ q := noPerson().unwrapOr(Person(name: "gone", age: 1))
482
524
  assert q.name == "gone"
483
525
 
484
- test "null is the absent value in an argument, a list element and a default"
526
+ test "None is the absent value in an argument, a list element and a default"
485
- u := User(name: "w", target: null)
527
+ u := User(name: "w", target: None)
486
- assert {targetName(u) ?: "none"} == "none"
528
+ assert targetName(Some(u)).unwrapOr("none") == "none"
487
529
  xs := sparse()
488
- second := xs.get(1)!
530
+ second := xs.at(1)
489
- assert {second ?: 9} == 9
531
+ assert second.unwrapOr(9) == 9
490
- first := xs.get(0)!
532
+ first := xs.at(0)
491
533
  assert first! == 1
492
- assert {maybeCount() ?: 5} == 5
534
+ assert maybeCount(None).unwrapOr(5) == 5
493
- assert maybeCount(7)! == 7
535
+ assert maybeCount(Some(7))! == 7
plum-examples/functional.plum CHANGED
@@ -26,6 +26,7 @@ import std/Int
26
26
  import std/Float
27
27
  import std/Slice
28
28
  import std/Pair
29
+ import std/Option
29
30
 
30
31
  # ---------------------------------------------------------------------------
31
32
  # 1. Chain[T]: a cons list (std/List is the same shape, with variants Nil and Cons;
@@ -52,10 +53,10 @@ enum Chain[T] =
52
53
  f(h)
53
54
  t.each(f)
54
55
 
55
- # A `for` source: `for x := range xs` steps with `next` (null = done).
56
+ # A `for` source: `for x := range xs` steps with `next` (`None` = done).
56
- fun next(self) -> Pair[T, Chain[T]]? = match self
57
+ fun next(self) -> Option[Pair[T, Chain[T]]] = match self
57
- Empty => null
58
+ Empty => None
58
- Link(h, t) => Pair(first: h, second: t)
59
+ Link(h, t) => Some(Pair(first: h, second: t))
59
60
 
60
61
  # Structural recursion: rebuild the list with `f` applied to every element.
61
62
  fun map(self, f: fn(T) -> B) -> Chain[B] = match self
@@ -101,10 +102,10 @@ enum Chain[T] =
101
102
  Empty => Empty
102
103
  Link(_, t) => if n <= 0 then self else t.drop(n - 1)
103
104
 
104
- # The first element, or null for an empty list.
105
+ # The first element, or None for an empty list.
105
- fun first(self) -> T? = match self
106
+ fun first(self) -> Option[T] = match self
106
- Empty => null
107
+ Empty => None
107
- Link(h, _) => h
108
+ Link(h, _) => Some(h)
108
109
 
109
110
  # Pairs up two lists, stopping at the shorter one.
110
111
  fun zip(self, ys: Chain[B]) -> Chain[Pair[T, B]] = match self
@@ -195,9 +196,9 @@ enum Tree =
195
196
  fun inOrder(self) -> Chain[Int] =
196
197
  self.fold(Empty, |x, acc| cons(x, acc))
197
198
 
198
- fun smallest(self) -> Int? = match self
199
+ fun smallest(self) -> Option[Int] = match self
199
- Leaf => null
200
+ Leaf => None
200
- Branch(Leaf, x, _) => x
201
+ Branch(Leaf, x, _) => Some(x)
201
202
  Branch(l, _, _) => l.smallest()
202
203
 
203
204
  fun fromLink(xs: Chain[Int]) -> Tree =
@@ -306,30 +307,30 @@ enum Node =
306
307
  Button(id, _, _, _, _, pressed) => single(cmd(if pressed then "button-down" else "button", id, xf))
307
308
  Group(_, _, kids) => kids.foldr(Empty, |k, acc| k.draw(xf).append(acc))
308
309
 
309
- # The id of the topmost node under stage point (px, py), or null. Children drawn
310
+ # The id of the topmost node under stage point (px, py), or None. Children drawn
310
311
  # later are on top, so they are tried first (in reverse order).
311
- fun findHit(self, parent: Xf, px: Float, py: Float) -> Int? =
312
+ fun findHit(self, parent: Xf, px: Float, py: Float) -> Option[Int] =
312
313
  xf := thenXf(parent, self.xf())
313
314
  p := undo(xf, px, py)
314
315
  match self
315
316
  Sprite(_, _, w, h, _, _) => hitBox(self, p, w, h)
316
317
  Button(_, _, bw, bh, _, _) => hitBox(self, p, bw, bh)
317
- Label(_, _, _) => null
318
+ Label(_, _, _) => None
318
319
  Group(_, _, kids) => firstHit(kids.reverse(), xf, px, py)
319
320
 
320
321
  fun press(self) -> Node = match self
321
322
  Button(id, xf, w, h, text, _) => Button(id: id, xf: xf, w: w, h: h, text: text, pressed: true)
322
323
  _ => self
323
324
 
324
- # An inline `if` with `null` in the other branch is an optional: `Int?`.
325
+ # An `if` that picks between `Some(..)` and `None` builds an `Option[Int]`.
325
- fun hitBox(n: Node, p: Pt, w: Float, h: Float) -> Int? =
326
+ fun hitBox(n: Node, p: Pt, w: Float, h: Float) -> Option[Int] =
326
- if inBox(p, w, h) then n.id() else null
327
+ if inBox(p, w, h) then Some(n.id()) else None
327
328
 
328
- fun firstHit(kids: Chain[Node], xf: Xf, px: Float, py: Float) -> Int? = match kids
329
+ fun firstHit(kids: Chain[Node], xf: Xf, px: Float, py: Float) -> Option[Int] = match kids
329
- Empty => null
330
+ Empty => None
330
331
  Link(k, rest) =>
331
- if hit := k.findHit(xf, px, py)
332
+ if Some(hit) := k.findHit(xf, px, py)
332
- hit
333
+ Some(hit)
333
334
  else
334
335
  firstHit(rest, xf, px, py)
335
336
 
@@ -342,7 +343,7 @@ struct UiEvent(id: Int, kind: UiKind)
342
343
  struct Routed(node: Node, events: Chain[UiEvent])
343
344
 
344
345
  fun click(root: Node, px: Float, py: Float) -> Routed =
345
- if id := root.findHit(identityXf(), px, py)
346
+ if Some(id) := root.findHit(identityXf(), px, py)
346
347
  Routed(node: root.update(id, |n| n.press()), events: single(UiEvent(id: id, kind: Clicked)))
347
348
  else
348
349
  Routed(node: root, events: Empty)
@@ -428,8 +429,8 @@ test "link sharing"
428
429
  assert joinInts(tail, " ") == "2 3"
429
430
 
430
431
  test "link first and zip"
431
- assert {fromTo(7, 9).first() ?: 0} == 7
432
+ assert fromTo(7, 9).first().unwrapOr(0) == 7
432
- assert {fromTo(7, 7).first() ?: 0} == 0
433
+ assert fromTo(7, 7).first().unwrapOr(0) == 0
433
434
  ps := fromTo(1, 4).zip(fromTo(10, 20))
434
435
  assert ps.length() == 3
435
436
  assert ps.foldl(0, |a, p| a + p.first * p.second) == 1 * 10 + 2 * 11 + 3 * 12
@@ -473,8 +474,8 @@ test "persistent tree"
473
474
  assert !t.member(7)
474
475
  assert t.size() == 6
475
476
  assert t2.size() == 7
476
- assert {t2.smallest() ?: -1} == 1
477
+ assert t2.smallest().unwrapOr(-1) == 1
477
- assert {Leaf.smallest() ?: -1} == -1
478
+ assert Leaf.smallest().unwrapOr(-1) == -1
478
479
 
479
480
  test "pattern matching"
480
481
  assert Circle(r: 0.0).describe() == "point"
@@ -508,9 +509,9 @@ test "scene act is pure"
508
509
 
509
510
  test "scene hit test"
510
511
  scene := demoScene()
511
- assert {scene.findHit(identityXf(), 105.0, 55.0) ?: 0} == 2
512
+ assert scene.findHit(identityXf(), 105.0, 55.0).unwrapOr(0) == 2
512
- assert {scene.findHit(identityXf(), 115.0, 165.0) ?: 0} == 5
513
+ assert scene.findHit(identityXf(), 115.0, 165.0).unwrapOr(0) == 5
513
- assert {scene.findHit(identityXf(), 0.0, 0.0) ?: 0} == 0
514
+ assert scene.findHit(identityXf(), 0.0, 0.0).unwrapOr(0) == 0
514
515
 
515
516
  test "scene click routes an event"
516
517
  scene := demoScene()
plum-examples/games/functional/game_state_view.plum CHANGED
@@ -27,6 +27,7 @@ import std/Str
27
27
  import std/Int
28
28
  import std/Float
29
29
  import std/Slice
30
+ import std/Option
30
31
  import std/List
31
32
  import std/Math
32
33
  import std/Random
@@ -426,7 +427,10 @@ struct State(
426
427
  # One pointer input: finds what is under it in the current picture, runs the Ui,
427
428
  # and sends the messages of the buttons it clicked to `update`.
428
429
  fun feed(self, input: Input) -> State =
429
- pos := input.position() ?: return self
430
+ po := input.position()
431
+ if po.isNone()
432
+ return self
433
+ pos := po.unwrap()
430
434
  view := self.render()
431
435
  r := Ui.step(self.ui, input, view.hitId(pos.x, pos.y))
432
436
  Ui.messages(view, r.events).fold(State(..self, ui: r.ui), |s, m| s.update(m))
@@ -510,7 +514,7 @@ struct State(
510
514
 
511
515
  # True when (x, y) in screen pixels is on the restart button of the picture `render` builds.
512
516
  fun onRestart(self, x: Float, y: Float) -> Bool =
513
- {self.render().hitId(x, y) ?: -1} == Id.Restart
517
+ self.render().hitId(x, y).unwrapOr(-1) == Id.Restart
514
518
 
515
519
  # Enter or R going down.
516
520
  fun isRestartKey(e: Input) -> Bool = match e
plum-examples/games/functional/kids/balloon_pop.plum CHANGED
@@ -15,6 +15,7 @@ import std/Str
15
15
  import std/Int
16
16
  import std/Float
17
17
  import std/Slice
18
+ import std/Option
18
19
  import std/List
19
20
  import std/Math
20
21
  import std/Random
@@ -208,9 +209,10 @@ struct State(balloons: List[Balloon], rng: Random, pops: Int, clock: Float, part
208
209
 
209
210
  fun pressAt(self, x: Float, y: Float) -> State =
210
211
  view := self.render()
211
- id := view.hitId(x, y) ?: return self
212
+ if Some(id) := view.hitId(x, y)
212
- msg := view.msgOf(id) ?: return self
213
+ if Some(msg) := view.msgOf(id)
213
- self.update(msg)
214
+ return self.update(msg)
215
+ self
214
216
 
215
217
  # --- render ---
216
218
 
plum-examples/games/imperative/game_loop.plum CHANGED
@@ -7,6 +7,7 @@ import std/Str
7
7
  import std/Int
8
8
  import std/Float
9
9
  import std/Slice
10
+ import std/Option
10
11
  import std/Map
11
12
 
12
13
  struct Vec2(x: Float, y: Float) =
@@ -59,7 +60,7 @@ fun handle(world: Map[Int, Entity], e: Event) =
59
60
  Spawn(id, name) =>
60
61
  world.set(id, Entity(id: id, name: name, hp: 100))
61
62
  Damage(id, amount) =>
62
- if ent := world.getOrNull(id)
63
+ if Some(ent) := world.get(id)
63
64
  ent.hp = ent.hp - amount
64
65
  Despawn(id) =>
65
66
  world.remove(id)
plum-examples/games/imperative/game_scene.plum CHANGED
@@ -25,6 +25,7 @@ import std/Str
25
25
  import std/Int
26
26
  import std/Float
27
27
  import std/Slice
28
+ import std/Option
28
29
  import std/Math
29
30
  import std/Random
30
31
  import std/game/Vec2
@@ -297,7 +298,7 @@ struct World(
297
298
  overlay.add(Text.create("Press Enter to play again", 24.0, Color.White).at(270.0, 316.0))
298
299
  hud.add(overlay)
299
300
  w := World(
300
- camX: 0.0, camY: 0.0, camScale: 1.0, focus: null, ship: ship, mines: mines, orbs: orbs, mineList: Slice[Mine](), orbList: Slice[Orb](),
301
+ camX: 0.0, camY: 0.0, camScale: 1.0, focus: None, ship: ship, mines: mines, orbs: orbs, mineList: Slice[Mine](), orbList: Slice[Orb](),
301
302
  overlay: overlay, scoreText: scoreText, bestText: bestText, lifeIcons: icons,
302
303
  rng: Random.seeded(seed), score: 0, best: 0, lives: Rules.StartLives, over: false, clock: 0.0, held: Held.none(), sheet: sheet,
303
304
  pickupSound: -1, hitSound: -1, overSound: -1, musicSound: -1)
@@ -540,7 +541,7 @@ test "collecting an orb scores and respawns it elsewhere, still in the orbs laye
540
541
  assert Math.hypot(orb.x - w.ship.x, orb.y - w.ship.y) >= 120.0
541
542
  assert orb.x < 5000.0
542
543
  assert w.orbs.children.length() == Rules.OrbCount
543
- assert same({orb.parent ?: crash "orphaned"}, w.orbs)
544
+ assert same(orb.parent.unwrap(), w.orbs)
544
545
  assert w.scoreText.text == "Score 10"
545
546
 
546
547
  test "a mine costs one life and makes the ship flash through its own act"
plum-examples/games/imperative/scene.plum CHANGED
@@ -16,6 +16,7 @@ import std/Str
16
16
  import std/Int
17
17
  import std/Float
18
18
  import std/Slice
19
+ import std/Option
19
20
  import std/Math
20
21
  import std/game/Vec2
21
22
  import std/game/Color
@@ -100,7 +101,7 @@ struct Sky(
100
101
  sun: Spinner,
101
102
  label: Text,
102
103
  crowd: Group,
103
- mut front: Actor?,
104
+ mut front: Option[Actor],
104
105
  mut selected: Int,
105
106
  mut clock: Float,
106
107
  mut paused: Bool,
@@ -136,7 +137,7 @@ fun newSky(sheet: Int) -> Sky =
136
137
  label.y = 12.0
137
138
  sky := Sky(
138
139
  stage: stage, bodies: Slice[Actor](), halos: Slice[Actor](), names: Slice[Str](),
139
- sun: sun, label: label, crowd: Group(), front: null, selected: -1, clock: 0.0, paused: false, held: Held.none(), map: -1)
140
+ sun: sun, label: label, crowd: Group(), front: None, selected: -1, clock: 0.0, paused: false, held: Held.none(), map: -1)
140
141
  # faint orbit rings, added first so every body draws (and is hit) above them
141
142
  for i := range Layout.Planets
142
143
  r := 90.0 + Float(i) * 60.0
@@ -193,12 +194,12 @@ fun select(sky: Sky, i: Int) =
193
194
  # selected already, and clears the selection on empty space.
194
195
  fun clickAt(sky: Sky, x: Float, y: Float) =
195
196
  i := -1
196
- if n := sky.stage.hit(x, y)
197
+ if Some(n) := sky.stage.hit(x, y)
197
198
  i = indexOf(sky, n)
198
- if p := n.parent
199
+ if Some(p) := n.parent
199
200
  if p.children == sky.crowd.children
200
201
  n.toFront()
201
- sky.front = n
202
+ sky.front = Some(n)
202
203
  if i == sky.selected
203
204
  i = -1
204
205
  select(sky, i)
@@ -215,7 +216,7 @@ fun tick(sky: Sky, dt: Float) =
215
216
  sky.clock = sky.clock + dt
216
217
  sky.stage.act(dt)
217
218
  sky.crowd.sortChildren(|a, b| a.y < b.y)
218
- if f := sky.front
219
+ if Some(f) := sky.front
219
220
  f.toFront()
220
221
  sky.sun.srcX = Float(Int(sky.clock * 6.0) % 4) * Layout.Cell
221
222
 
@@ -428,4 +429,4 @@ test "walker_record: a Walker in a Stage moves via its own act and draws via its
428
429
  a: Actor := w
429
430
  a.act(1.0)
430
431
  assert near(w.x, 8.0)
431
- assert same({stage.hit(8.5, 5.0) ?: crash "miss"}, w)
432
+ assert same(stage.hit(8.5, 5.0).unwrap(), w)
plum-examples/games/pure/game_pure_scene.plum CHANGED
@@ -19,6 +19,7 @@ import std/Str
19
19
  import std/Int
20
20
  import std/Float
21
21
  import std/Slice
22
+ import std/Option
22
23
  import std/Math
23
24
  import std/Random
24
25
  import std/game/Vec2
@@ -270,11 +271,11 @@ fun nodeAt(a: Node, path: Slice[Int], depth: Int) -> Node =
270
271
 
271
272
  # The node with `id` below `root`, searched depth first, borrowed from the tree.
272
273
  fun find(root: Node, id: Int) -> Node =
273
- path := root.findPath(id) ?: crash "no such actor"
274
+ path := root.findPath(id).unwrap()
274
275
  nodeAt(root, path, 0)
275
276
 
276
277
  fun kidsOf(n: Node) -> Slice[Node] =
277
- n.children() ?: crash "not a group"
278
+ n.children().unwrap()
278
279
 
279
280
  fun setVisible(n: Node, v: Bool) -> Node =
280
281
  Node(..n, visible: v)
@@ -417,7 +418,7 @@ fun restart(w: World) =
417
418
  spots := startSpots(w.rng, w.ship)
418
419
  next := spots.rng
419
420
  w.rng = next
420
- w.stage = Stage(root: buildRoot(spots, w.sheet, w.best), camera: w.stage.camera, captured: null, focus: null)
421
+ w.stage = Stage(root: buildRoot(spots, w.sheet, w.best), camera: w.stage.camera, captured: None, focus: None)
421
422
 
422
423
  # Copies the score, best score, lives and game-over panel into the HUD actors.
423
424
  fun syncHud(w: World) =
plum-examples/generics.plum CHANGED
@@ -7,10 +7,12 @@ import std/Float
7
7
  import std/Slice
8
8
  import std/Map
9
9
  import std/List
10
+ import std/Option
11
+ import std/Result
10
12
 
11
13
  # Generics are erased: a `T` is a nullable object reference at run time, and
12
14
  # Int / Float values are boxed when they cross a generic edge (a `T` param,
13
- # a Slice slot, a Map value, an error-union payload) and unboxed on the way out. These
15
+ # a Slice slot, a Map value, a Result payload) and unboxed on the way out. These
14
16
  # tests push awkward values through every such edge and check they come back
15
17
  # bit-for-bit.
16
18
 
@@ -20,19 +22,24 @@ fun id(x: T) -> T =
20
22
  fun same(a: T, b: T) -> Bool =
21
23
  a == b
22
24
 
25
+ enum GenError =
26
+ | Boom
27
+ | OutOfBounds
28
+ | KeyNotFound
29
+
23
- fun wrap(x: T) -> error{A}!T =
30
+ fun wrap(x: T) -> Result[T, GenError] =
24
- x
31
+ Ok(x)
25
32
 
26
- fun viaQ(x: T) -> error{A}!T =
33
+ fun viaQ(x: T) -> Result[T, GenError] =
27
- y := try wrap(x)
34
+ y := wrap(x)?
28
- return y
35
+ return Ok(y)
29
36
 
30
- fun failing() -> error{A}!Int =
37
+ fun failing() -> Result[Int, GenError] =
31
- return error.A
38
+ return Err(Boom)
32
39
 
33
- fun viaQFailing() -> error{A}!Int =
40
+ fun viaQFailing() -> Result[Int, GenError] =
34
- y := try failing()
41
+ y := failing()?
35
- return y
42
+ return Ok(y)
36
43
 
37
44
  fun pick(x: T, f: fn(T) -> T) -> T =
38
45
  f(x)
@@ -40,11 +47,11 @@ fun pick(x: T, f: fn(T) -> T) -> T =
40
47
  fun twice(f: fn(T) -> T, x: T) -> T =
41
48
  f(f(x))
42
49
 
43
- fun readDeep(outer: Slice[Slice[Map[Str, Int]]], r: Int, c: Int, key: Str) -> error{OutOfBounds, KeyNotFound}!Int =
50
+ fun readDeep(outer: Slice[Slice[Map[Str, Int]]], r: Int, c: Int, key: Str) -> Result[Int, GenError] =
44
- row := try outer.get(r)
51
+ row := outer.get(r).okOr(OutOfBounds)?
45
- m := try row.get(c)
52
+ m := row.get(c).okOr(OutOfBounds)?
46
- v := try m.get(key)
53
+ v := m.get(key).okOr(KeyNotFound)?
47
- return v
54
+ return Ok(v)
48
55
 
49
56
  struct Circle(r: Float) =
50
57
 
@@ -117,11 +124,11 @@ struct Crate[T](value: T) =
117
124
  kept.push(c)
118
125
  kept.at(0).value
119
126
 
120
- struct Chain[T](v: T, next: Chain?) =
127
+ struct Chain[T](v: T, next: Option[Chain[T]]) =
121
128
 
122
129
  fun last(self) -> T =
123
130
  cur: Chain := self
124
- while n := cur.next
131
+ while Some(n) := cur.next
125
132
  cur = n
126
133
  cur.v
127
134
 
@@ -163,47 +170,47 @@ test "Int and Float extremes survive List push/get"
163
170
  ints.push(9223372036854775807)
164
171
  ints.push(0 - 9223372036854775807 - 1)
165
172
  ints.push(9007199254740993)
166
- assert {ints.get(0) catch 0} == 9223372036854775807
173
+ assert ints.get(0).unwrapOr(0) == 9223372036854775807
167
- assert {ints.get(1) catch 0} == 0 - 9223372036854775807 - 1
174
+ assert ints.get(1).unwrapOr(0) == 0 - 9223372036854775807 - 1
168
- assert {ints.get(2) catch 0} == 9007199254740993
175
+ assert ints.get(2).unwrapOr(0) == 9007199254740993
169
176
  floats := Slice[Float]()
170
177
  floats.push(0.0 / 0.0)
171
178
  floats.push(-0.0)
172
179
  floats.push(5e-324)
173
- nan := {floats.get(0) catch 0.0}
180
+ nan := floats.get(0).unwrapOr(0.0)
174
181
  assert nan != nan
175
- assert 1.0 / {floats.get(1) catch 1.0} < 0.0
182
+ assert 1.0 / floats.get(1).unwrapOr(1.0) < 0.0
176
- assert {floats.get(2) catch 0.0} == 5e-324
183
+ assert floats.get(2).unwrapOr(0.0) == 5e-324
177
184
 
178
185
  test "Int and Float extremes survive a Map[Str, V] value"
179
186
  mi: Map[Str, Int] := Map.empty()
180
187
  mi.set("big", 9223372036854775807)
181
188
  mi.set("small", 0 - 9223372036854775807 - 1)
182
189
  mi.set("odd", 9007199254740993)
183
- assert {mi.get("big") catch 0} == 9223372036854775807
190
+ assert mi.get("big").unwrapOr(0) == 9223372036854775807
184
- assert {mi.get("small") catch 0} == 0 - 9223372036854775807 - 1
191
+ assert mi.get("small").unwrapOr(0) == 0 - 9223372036854775807 - 1
185
- assert {mi.get("odd") catch 0} == 9007199254740993
192
+ assert mi.get("odd").unwrapOr(0) == 9007199254740993
186
193
  mf: Map[Str, Float] := Map.empty()
187
194
  mf.set("nan", 0.0 / 0.0)
188
195
  mf.set("negzero", -0.0)
189
196
  mf.set("tiny", 5e-324)
190
- nan := {mf.get("nan") catch 0.0}
197
+ nan := mf.get("nan").unwrapOr(0.0)
191
198
  assert nan != nan
192
- assert 1.0 / {mf.get("negzero") catch 1.0} < 0.0
199
+ assert 1.0 / mf.get("negzero").unwrapOr(1.0) < 0.0
193
- assert {mf.get("tiny") catch 0.0} == 5e-324
200
+ assert mf.get("tiny").unwrapOr(0.0) == 5e-324
194
201
 
195
- test "Int extremes survive an error-union payload with ? and catch"
202
+ test "Int extremes survive a Result payload with ? and unwrapOr"
196
- assert {wrap(9223372036854775807) catch 0} == 9223372036854775807
203
+ assert wrap(9223372036854775807).unwrapOr(0) == 9223372036854775807
197
- assert {viaQ(0 - 9223372036854775807 - 1) catch 0} == 0 - 9223372036854775807 - 1
204
+ assert viaQ(0 - 9223372036854775807 - 1).unwrapOr(0) == 0 - 9223372036854775807 - 1
198
- assert {viaQ(9007199254740993) catch 0} == 9007199254740993
205
+ assert viaQ(9007199254740993).unwrapOr(0) == 9007199254740993
199
- assert {viaQFailing() catch 42} == 42
206
+ assert viaQFailing().unwrapOr(42) == 42
200
207
 
201
- test "Float specials survive an error-union payload with ? and catch"
208
+ test "Float specials survive a Result payload with ? and unwrapOr"
202
- nan := {viaQ(0.0 / 0.0) catch 0.0}
209
+ nan := viaQ(0.0 / 0.0).unwrapOr(0.0)
203
210
  assert nan != nan
204
- assert 1.0 / {viaQ(-0.0) catch 1.0} < 0.0
211
+ assert 1.0 / viaQ(-0.0).unwrapOr(1.0) < 0.0
205
- assert {viaQ(5e-324) catch 0.0} == 5e-324
212
+ assert viaQ(5e-324).unwrapOr(0.0) == 5e-324
206
- assert {wrap(5e-324) catch 0.0} == 5e-324
213
+ assert wrap(5e-324).unwrapOr(0.0) == 5e-324
207
214
 
208
215
  # --- deep ----------------------------------------------------------------
209
216
 
@@ -219,12 +226,12 @@ test "List[List[Map[Str, Int]]] builds and reads back"
219
226
  outer.push(inner1)
220
227
  outer.push(inner2)
221
228
  assert outer.length() == 2
222
- assert {outer.get(0) catch inner2}.length() == 2
229
+ assert outer.get(0).unwrapOr(inner2).length() == 2
223
- assert {readDeep(outer, 0, 0, "ok") catch 0 - 1} == 7
230
+ assert readDeep(outer, 0, 0, "ok").unwrapOr(0 - 1) == 7
224
- assert {readDeep(outer, 0, 0, "nokey") catch 98} == 98
231
+ assert readDeep(outer, 0, 0, "nokey").unwrapOr(98) == 98
225
- assert {readDeep(outer, 0, 1, "big") catch 0} == 9223372036854775807
232
+ assert readDeep(outer, 0, 1, "big").unwrapOr(0) == 9223372036854775807
226
- assert {readDeep(outer, 1, 0, "big") catch 0} == 9223372036854775807
233
+ assert readDeep(outer, 1, 0, "big").unwrapOr(0) == 9223372036854775807
227
- assert {readDeep(outer, 2, 0, "big") catch 97} == 97
234
+ assert readDeep(outer, 2, 0, "big").unwrapOr(97) == 97
228
235
 
229
236
  test "a generic receiver's methods are chosen per element type"
230
237
  ints := [1, 22, 333].toSlice()
@@ -322,7 +329,7 @@ test "a bare generic type name inside its own methods means its applied form"
322
329
  assert Crate(value: "a").pick(Crate(value: "b"), false).value == "a"
323
330
  assert Crate(value: 7).viaLocal() == 7
324
331
  assert Crate(value: "x").viaLocal() == "x"
325
- assert Chain(v: 1, next: Chain(v: 2, next: Chain(v: 3, next: null))).last() == 3
332
+ assert Chain(v: 1, next: Some(Chain(v: 2, next: Some(Chain(v: 3, next: None))))).last() == 3
326
- assert Chain(v: "p", next: Chain(v: "q", next: null)).last() == "q"
333
+ assert Chain(v: "p", next: Some(Chain(v: "q", next: None))).last() == "q"
327
334
  assert stashedOr(Stashed(5), 0) == 5
328
335
  assert stashedOr(Unstashed, 9) == 9
plum-examples/json.plum CHANGED
@@ -1,32 +1,34 @@
1
1
  # JSON: `Json.encode(x)` and `Json.decode(s)`. A type with a `toJson(self) -> Json`
2
- # method encodes; a type with a static `fromJson(j: Json) -> JsonError!T` decodes.
2
+ # method encodes; a type with a static `fromJson(j: Json) -> Result[T, JsonError]` decodes.
3
3
  # `T` of `decode` comes from the annotation. Records write both by hand.
4
4
  import std/Int
5
5
  import std/Str
6
6
  import std/Slice
7
7
  import std/Json
8
+ import std/Option
9
+ import std/Result
8
10
 
9
11
  struct House(name: Str, rooms: Int, tags: Slice[Str], annex: Slice[House]) =
10
12
  fun toJson(self) -> Json =
11
13
  Json.object(["name" => self.name.toJson(), "rooms" => self.rooms.toJson(), "tags" => self.tags.toJson(), "annex" => self.annex.toJson()])
12
14
 
13
- fun fromJson(j: Json) -> JsonError!House =
15
+ fun fromJson(j: Json) -> Result[House, JsonError] =
14
- name := try Str.fromJson(try j.field("name"))
16
+ name := Str.fromJson(j.field("name")?)?
15
- rooms := try Int.fromJson(try j.field("rooms"))
17
+ rooms := Int.fromJson(j.field("rooms")?)?
16
- tags := try tagsOf(try j.field("tags"))
18
+ tags := tagsOf(j.field("tags")?)?
17
- annex := try annexOf(try j.field("annex"))
19
+ annex := annexOf(j.field("annex")?)?
18
- return House(name: name, rooms: rooms, tags: tags, annex: annex)
20
+ return Ok(House(name: name, rooms: rooms, tags: tags, annex: annex))
19
21
 
20
- fun tagsOf(j: Json) -> JsonError!Slice[Str] = j.into()
22
+ fun tagsOf(j: Json) -> Result[Slice[Str], JsonError] = j.into()
21
23
 
22
- fun annexOf(j: Json) -> JsonError!Slice[House] = j.into()
24
+ fun annexOf(j: Json) -> Result[Slice[House], JsonError] = j.into()
23
25
 
24
- fun parseHouse(s: Str) -> JsonError!House = Json.decode(s)
26
+ fun parseHouse(s: Str) -> Result[House, JsonError] = Json.decode(s)
25
27
 
26
28
  fun errorOf(s: Str) -> Str =
27
- parseHouse(s) catch e
29
+ match parseHouse(s)
30
+ Ok(h) => "ok"
28
- return e.name()
31
+ Err(e) => e.name()
29
- "ok"
30
32
 
31
33
  fun house(a: Pair[Str, Json], b: Pair[Str, Json]) -> Str =
32
34
  Json.object([a.first => a.second, b.first => b.second, "tags" => Json.array([]), "annex" => Json.array([])]).toStr()
@@ -38,7 +40,7 @@ test "nested round trip"
38
40
  annex.push(House(name: "shed", rooms: 1, tags: Slice[Str](), annex: Slice[House]()))
39
41
  h := House(name: "main", rooms: 5, tags: tags, annex: annex)
40
42
  text := Json.encode(h)
41
- back: JsonError!House := Json.decode(text)
43
+ back: Result[House, JsonError] := Json.decode(text)
42
44
  assert back!.name == "main"
43
45
  assert back!.tags.get(0)! == "old"
44
46
  assert back!.annex.get(0)!.name == "shed"
plum-examples/list_literal_probe.plum CHANGED
@@ -3,6 +3,7 @@ import std/Int
3
3
  import std/List
4
4
  import std/Map
5
5
  import std/Str
6
+ import std/Option
6
7
 
7
8
  test "list literal of ints"
8
9
  xs := [1, 2, 3]
@@ -22,4 +23,4 @@ test "nested list literal"
22
23
  test "map literal"
23
24
  m := ["a" => 1, "b" => 2]
24
25
  assert m.length() == 2
25
- assert {m.getOrNull("b") ?: 0} == 2
26
+ assert m.get("b").unwrapOr(0) == 2
plum-examples/match.plum CHANGED
@@ -6,6 +6,8 @@
6
6
  import std/Int
7
7
  import std/Float
8
8
  import std/Str
9
+ import std/Option
10
+ import std/Result
9
11
 
10
12
  enum Color =
11
13
  | Red
@@ -156,10 +158,10 @@ fun wide(s: Shape) -> Str =
156
158
  else
157
159
  "other"
158
160
 
159
- fun pick(s: Shape, o: Int?, n: Int) -> Str =
161
+ fun pick(s: Shape, o: Option[Int], n: Int) -> Str =
160
162
  if Circle(r) := s
161
163
  "circle"
162
- else if v := o
164
+ else if Some(v) := o
163
165
  "opt {v}"
164
166
  else if n > 10
165
167
  "big"
@@ -193,28 +195,29 @@ fun sumXY(v: Vec2) -> Int =
193
195
  return x + y
194
196
  0
195
197
 
198
+ enum SmallError = TooBig | NotNum
199
+
196
- fun parseSmall(s: Str) -> error{TooBig, NotNum}!Int =
200
+ fun parseSmall(s: Str) -> Result[Int, SmallError] =
197
- n := Int.parse(s) catch return error.NotNum
201
+ n := Int.parse(s).mapErr(|e| NotNum)?
198
202
  if n > 9
199
- return error.TooBig
203
+ return Err(TooBig)
200
- n
204
+ Ok(n)
201
205
 
202
206
  fun describe(s: Str) -> Str =
203
- n := parseSmall(s) catch e
207
+ match parseSmall(s)
204
- if TooBig := e
208
+ Ok(n) => "ok {n}"
205
- return "too big"
209
+ Err(TooBig) => "too big"
206
- return "not a number"
210
+ Err(NotNum) => "not a number"
207
- "ok {n}"
211
+
208
-
209
- fun firstSome(a: Int?, b: Int?) -> Int =
212
+ fun firstSome(a: Option[Int], b: Option[Int]) -> Int =
210
- if x := a
213
+ if Some(x) := a
211
214
  return x
212
- else if y := b
215
+ else if Some(y) := b
213
216
  return y + 1
214
217
  -1
215
218
 
216
- fun bigOnly(a: Int?) -> Int =
219
+ fun bigOnly(a: Option[Int]) -> Int =
217
- if x := a, x > 3
220
+ if Some(x) := a, x > 3
218
221
  return x
219
222
  0
220
223
 
@@ -325,11 +328,11 @@ test "if_pattern_enum_variant"
325
328
  assert area(Dot) == 0.0
326
329
 
327
330
  test "else_if_pattern_chain"
328
- assert pick(Circle(radius: 1.0), null, 0) == "circle"
331
+ assert pick(Circle(radius: 1.0), None, 0) == "circle"
329
- assert pick(Dot, 4, 0) == "opt 4"
332
+ assert pick(Dot, Some(4), 0) == "opt 4"
330
- assert pick(Dot, null, 11) == "big"
333
+ assert pick(Dot, None, 11) == "big"
331
- assert pick(Rect(w: 1.0, h: 1.0), null, 0) == "rect"
334
+ assert pick(Rect(w: 1.0, h: 1.0), None, 0) == "rect"
332
- assert pick(Dot, null, 0) == "none"
335
+ assert pick(Dot, None, 0) == "none"
333
336
 
334
337
  test "pattern_with_trailing_condition"
335
338
  assert wide(Rect(w: 3.0, h: 1.0)) == "wide"
@@ -351,22 +354,22 @@ test "while_pattern"
351
354
  assert countDown(Idle) == 0
352
355
 
353
356
  test "else_if_optional_binding"
354
- assert firstSome(null, 7) == 8
357
+ assert firstSome(None, Some(7)) == 8
355
- assert firstSome(2, 7) == 2
358
+ assert firstSome(Some(2), Some(7)) == 2
356
- assert firstSome(null, null) == -1
359
+ assert firstSome(None, None) == -1
357
360
 
358
361
  test "irrefutable_pattern_if_allowed"
359
362
  assert sumXY(Vec2(x: 3, y: 4)) == 7
360
363
 
361
- test "error_name_pattern_in_catch_block"
364
+ test "error_enum_patterns_in_match"
362
365
  assert describe("5") == "ok 5"
363
366
  assert describe("50") == "too big"
364
367
  assert describe("x") == "not a number"
365
368
 
366
369
  test "optional_binding_with_trailing_condition"
367
- assert bigOnly(5) == 5
370
+ assert bigOnly(Some(5)) == 5
368
- assert bigOnly(2) == 0
371
+ assert bigOnly(Some(2)) == 0
369
- assert bigOnly(null) == 0
372
+ assert bigOnly(None) == 0
370
373
 
371
374
  test "when guard picks between two cases of the same member"
372
375
  mythic := FantasyBook(title: "Dragon Tales", pages: 200, hasMythicalCreatures: true)
plum-examples/numbers.plum CHANGED
@@ -5,6 +5,8 @@ import std/Float
5
5
  import std/Math
6
6
  import std/Slice
7
7
  import std/List
8
+ import std/Option
9
+ import std/Result
8
10
 
9
11
  # ==== operators ====
10
12
 
@@ -158,11 +160,11 @@ test "toStr renders Int and Float correctly, including negatives and fractions"
158
160
  assert {-3.5}.toStr() == "-3.5"
159
161
 
160
162
  test "Int.parse/Float.parse round-trip a rendered number"
161
- assert {Int.parse("123") catch -1} == 123
163
+ assert Int.parse("123").unwrapOr(-1) == 123
162
- assert {Int.parse("-45") catch 1} == -45
164
+ assert Int.parse("-45").unwrapOr(1) == -45
163
- assert {Float.parse("3.25") catch -1.0f} == 3.25f
165
+ assert Float.parse("3.25").unwrapOr(-1.0f) == 3.25f
164
- assert {Int.parse("12x") catch -1} == -1
166
+ assert Int.parse("12x").unwrapOr(-1) == -1
165
- assert {Float.parse("") catch -1.0f} == -1.0f
167
+ assert Float.parse("").unwrapOr(-1.0f) == -1.0f
166
168
 
167
169
  test "Math namespace functions and hyperbolic methods work"
168
170
  assert Math.sin(0.0f) == 0.0f
plum-examples/recursion.plum CHANGED
@@ -5,6 +5,8 @@ import std/List
5
5
  import std/Str
6
6
  import std/Slice
7
7
  import std/Array
8
+ import std/Option
9
+ import std/Result
8
10
 
9
11
  # ==== tail calls ====
10
12
 
@@ -56,11 +58,13 @@ fun spin(c: Counter) -> Counter =
56
58
  fun depth(n: Int) -> Int =
57
59
  if n == 0 then 0 else 1 + depth(n - 1)
58
60
 
61
+ enum DownError = Neg
62
+
59
- fun down(n: Int) -> error{Neg}!Int =
63
+ fun down(n: Int) -> Result[Int, DownError] =
60
64
  if n < 0
61
- return error.Neg
65
+ return Err(Neg)
62
66
  if n == 0
63
- return 5
67
+ return Ok(5)
64
68
  down(n - 1)
65
69
 
66
70
  # ==== cons list ====
@@ -132,8 +136,8 @@ test "closure_body_tail_million"
132
136
  fin := |n: Int| sumTo(n, 0)
133
137
  assert fin(1000000) == 500000500000
134
138
 
135
- test "error_union_loop_million"
139
+ test "result_loop_million"
136
- assert { down(1000000) catch 99 } == 5
140
+ assert down(1000000).unwrapOr(99) == 5
137
141
 
138
142
  test "map_single_pass_million"
139
143
  ys := map(fromTo(0, 1000000), |x| x * 2)
@@ -149,8 +153,8 @@ test "tree_recursion_depth_recorded"
149
153
  test "init_sum"
150
154
  assert [1, 2, 3, 4].fold(0, |a, x| a + x) == 10
151
155
  assert [1, 2, 3].length() == 3
152
- assert { [1, 2, 3].get(1) ?: -1 } == 2
156
+ assert [1, 2, 3].get(1).unwrapOr(-1) == 2
153
- assert [1, 2, 3].get(5) == null
157
+ assert [1, 2, 3].get(5).isNone()
154
158
  nil: List[Int] := Nil
155
159
  assert nil.isEmpty()
156
160
  assert ![1].isEmpty()
@@ -163,8 +167,8 @@ test "map_filter_fold"
163
167
 
164
168
  test "reverse_append"
165
169
  xs := [1, 2, 3].reverse()
166
- assert { xs.get(0) ?: -1 } == 3
170
+ assert xs.get(0).unwrapOr(-1) == 3
167
- assert { xs.get(2) ?: -1 } == 1
171
+ assert xs.get(2).unwrapOr(-1) == 1
168
172
  ys := [1, 2].append([3, 4])
169
173
  assert ys.equals([1, 2, 3, 4])
170
174
 
@@ -197,7 +201,7 @@ test "cons_fold_million"
197
201
  xs := range(0, 1000000)
198
202
  assert xs.fold(0, |a, x| a + x) == 499999500000
199
203
  assert xs.filter(|x| x % 2 == 0).length() == 500000
200
- assert { xs.reverse().get(0) ?: -1 } == 999999
204
+ assert xs.reverse().get(0).unwrapOr(-1) == 999999
201
205
 
202
206
  test "equals_and_toStr"
203
207
  assert [1, 2, 3].equals([1, 2, 3])
plum-examples/sequences.plum CHANGED
@@ -7,15 +7,16 @@ import std/Pair
7
7
  import std/Str
8
8
  import std/Testing
9
9
  import std/List
10
+ import std/Option
10
11
 
11
12
  # ==== iterators ====
12
13
 
13
14
  # `for x := range v` works over:
14
15
  #
15
16
  # * an `Int` n: x counts 0, 1, ... n - 1;
16
- # * any value whose type has `next(self) -> Pair[T, Self]?`: null means done,
17
+ # * any value whose type has `next(self) -> Option[Pair[T, Self]]`: `None` means
17
- # otherwise `Pair(value, rest)` binds x to the value and continues with the
18
+ # done, otherwise `Some(Pair(value, rest))` binds x to the value and continues
18
- # rest (`List`, the `Range` from `Int.until/to/downTo/stepBy`, and your own
19
+ # with the rest (`List`, the `Range` from `Int.until/to/downTo/stepBy`, and your own
19
20
  # types);
20
21
  # * a `Slice` or `Array` (by position).
21
22
  #
@@ -42,17 +43,17 @@ fun army() -> Slice[Soldier] =
42
43
  # A user iterator: from, from - 1, ... 1.
43
44
  struct Countdown(from: Int) =
44
45
 
45
- fun next(self) -> Pair[Int, Countdown]? =
46
+ fun next(self) -> Option[Pair[Int, Countdown]] =
46
- if self.from <= 0 then null else Pair(first: self.from, second: Countdown(from: self.from - 1))
47
+ if self.from <= 0 then None else Some(Pair(first: self.from, second: Countdown(from: self.from - 1)))
47
48
 
48
49
  # A lazy adapter: the first `n` elements of a list, pulling nothing past them.
49
50
  struct TakeList[T](src: List[T], n: Int) =
50
51
 
51
- fun next(self) -> Pair[T, TakeList[T]]? =
52
+ fun next(self) -> Option[Pair[T, TakeList[T]]] =
52
53
  if self.n <= 0
53
- return null
54
+ return None
54
- step := self.src.next() ?: return null
55
+ step := self.src.next()?
55
- Pair(first: step.first, second: TakeList(src: step.second, n: self.n - 1))
56
+ Some(Pair(first: step.first, second: TakeList(src: step.second, n: self.n - 1)))
56
57
 
57
58
  # The 8 neighbours of (x, y), as one list of (x, y) pairs.
58
59
  fun neighbours(x: Int, y: Int) -> List[Pair[Int, Int]] =
plum-examples/structs_records.plum CHANGED
@@ -5,6 +5,8 @@ import std/List
5
5
  import std/Float
6
6
  import std/Map
7
7
  import std/Slice
8
+ import std/Option
9
+ import std/Result
8
10
 
9
11
  # ==== structs records ====
10
12
 
@@ -65,14 +67,14 @@ fun sumAll(vs: List[Vec2]) -> Float =
65
67
  t = t + v.x
66
68
  t
67
69
 
68
- fun opt(c: Bool) -> Vec2? =
70
+ fun opt(c: Bool) -> Option[Vec2] =
69
71
  if c
70
- return Vec2(x: 1.0, y: 1.0)
72
+ return Some(Vec2(x: 1.0, y: 1.0))
71
- return null
73
+ return None
72
74
 
73
- fun chain(c: Bool) -> Vec2? =
75
+ fun chain(c: Bool) -> Option[Vec2] =
74
- v := opt(c) ?: return null
76
+ v := opt(c)?
75
- Vec2(x: v.x + 1.0, y: v.y)
77
+ Some(Vec2(x: v.x + 1.0, y: v.y))
76
78
 
77
79
  fun makeVec(x: Float, y: Float) -> Vec2 =
78
80
  Vec2(x: x, y: y)
@@ -158,14 +160,14 @@ enum V2 =
158
160
  | B2
159
161
  K1 = 40
160
162
 
161
- error S1 = E1 | E2
163
+ enum S1 = E1 | E2
162
164
 
163
- fun fail(n: Int) -> S1!Int =
165
+ fun fail(n: Int) -> Result[Int, S1] =
164
166
  if n == 1
165
- return error.E1
167
+ return Err(E1)
166
168
  if n == 2
167
- return error.E2
169
+ return Err(E2)
168
- n
170
+ Ok(n)
169
171
 
170
172
  fun asInt(w: V2) -> Int =
171
173
  match w
@@ -186,22 +188,22 @@ fun isB2(w: V2) -> Int =
186
188
 
187
189
  # Annotated local declarations: `name: Type := value` fixes the local's type to
188
190
  # the annotation. The value converts to it exactly as it would for a parameter of
189
- # that type (a variant into its enum, `T` into `T?`, `null` into `T?`), and
191
+ # that type (a variant into its enum, `Some(x)` or `None` into an `Option[T]`), and
190
192
  # a later `name = value` is checked and converted against the same type.
191
193
 
192
194
  enum Shape = Circle(r: Float) | Region(w: Float, h: Float)
193
195
 
194
- error LocalError = Bad | Worse
196
+ enum LocalError = Bad | Worse
195
197
 
196
198
  fun area(s: Shape) -> Float =
197
199
  match s
198
200
  Circle(r) => r * r * 3.0
199
201
  Region(w, h) => w * h
200
202
 
201
- fun parseLocal(n: Int) -> LocalError!Int =
203
+ fun parseLocal(n: Int) -> Result[Int, LocalError] =
202
204
  if n < 0
203
- return error.Bad
205
+ return Err(Bad)
204
- n * 2
206
+ Ok(n * 2)
205
207
 
206
208
  fun twice(x: Int) -> Int = x * 2
207
209
 
@@ -283,7 +285,7 @@ test "struct in a Map and an optional"
283
285
  assert m.get("a")!.y == 2.0
284
286
  assert opt(true)!.x == 1.0
285
287
  assert chain(true)!.x == 2.0
286
- assert {chain(false) ?: Vec2.zero()}.x == 0.0
288
+ assert chain(false).unwrapOr(Vec2.zero()).x == 0.0
287
289
 
288
290
  test "struct field of a record is copied out"
289
291
  p := Player(pos: Vec2(x: 1.0, y: 2.0), hp: 3)
@@ -392,9 +394,9 @@ test "digit_variant_pattern_if"
392
394
  assert isB2(A1(n: 1)) == 0
393
395
 
394
396
  test "digit_error_names"
395
- a := fail(3) catch 0
397
+ a := fail(3).unwrapOr(0)
396
398
  assert a == 3
397
- b := fail(1) catch 9
399
+ b := fail(1).unwrapOr(9)
398
400
  assert b == 9
399
401
 
400
402
  test "digit_enum_const_still_works"
@@ -413,14 +415,14 @@ test "union_local_reassigned"
413
415
  assert area(s) == 6.0
414
416
 
415
417
  test "optional_local_present"
416
- o: Int? := 4
418
+ o: Option[Int] := Some(4)
417
- assert {o ?: 0} == 4
419
+ assert o.unwrapOr(0) == 4
418
420
 
419
- test "optional_local_null"
421
+ test "optional_local_none"
420
- o: Int? := null
422
+ o: Option[Int] := None
421
- assert {o ?: 9} == 9
423
+ assert o.unwrapOr(9) == 9
422
- o = 5
424
+ o = Some(5)
423
- assert {o ?: 9} == 5
425
+ assert o.unwrapOr(9) == 5
424
426
 
425
427
  test "list_local"
426
428
  xs: Slice[Shape] := Slice[Shape]()
@@ -442,8 +444,8 @@ test "generic_param_annotation"
442
444
  assert same(7) == 7
443
445
  assert same("a") == "a"
444
446
 
445
- test "named_error_set_local"
447
+ test "named_error_enum_local"
446
- ok: LocalError!Int := parseLocal(4)
448
+ ok: Result[Int, LocalError] := parseLocal(4)
447
- bad: LocalError!Int := parseLocal(-1)
449
+ bad: Result[Int, LocalError] := parseLocal(-1)
448
- assert {ok catch 0} == 8
450
+ assert ok.unwrapOr(0) == 8
449
- assert {bad catch 7} == 7
451
+ assert bad.unwrapOr(7) == 7
plum-examples/unions.plum CHANGED
@@ -8,6 +8,8 @@ import std/Str
8
8
  import std/Int
9
9
  import std/Float
10
10
  import std/Slice
11
+ import std/Option
12
+ import std/Result
11
13
 
12
14
  enum Shape =
13
15
  | Circle(r: Float)
@@ -121,17 +123,19 @@ fun showB(j: Jb) -> Str =
121
123
  BStr(_) => "s"
122
124
  BList(_) => "l"
123
125
 
126
+ enum PosError = Neg
127
+
124
- fun pos(n: Int) -> !Slice[Jv] =
128
+ fun pos(n: Int) -> Result[Slice[Jv], PosError] =
125
129
  if n < 0
126
- return error.Neg
130
+ return Err(Neg)
127
131
  xs := Slice[Jv]()
128
132
  xs.push(JInt(v: n))
129
133
  xs.push(JStr(v: "a"))
130
- xs
134
+ Ok(xs)
131
135
 
132
- fun posLen(n: Int) -> !Int =
136
+ fun posLen(n: Int) -> Result[Int, PosError] =
133
- xs := try pos(n)
137
+ xs := pos(n)?
134
- xs.length()
138
+ Ok(xs.length())
135
139
 
136
140
  test "variant_field_by_pattern"
137
141
  s := make(false)
@@ -176,10 +180,10 @@ test "bool_variant"
176
180
  assert showB(BFloat(v: 1.5)) == "f"
177
181
  assert "{showB(BBool(v: true))}" == "T"
178
182
 
179
- test "variants_in_try"
183
+ test "variants_with_result"
180
- assert {posLen(3) catch 0} == 2
184
+ assert posLen(3).unwrapOr(0) == 2
181
- assert {posLen(-1) catch 0} == 0
185
+ assert posLen(-1).unwrapOr(0) == 0
182
- ys := pos(3) catch Slice[Jv]()
186
+ ys := pos(3).unwrapOr(Slice[Jv]())
183
187
  assert describeJv(JList(v: ys)) == "list"
184
188
 
185
189
  test "variants_with_for"
plum-game/web-test/fixtures/stress_pure_scene.plum CHANGED
@@ -9,6 +9,7 @@ import std/Str
9
9
  import std/Int
10
10
  import std/Float
11
11
  import std/Slice
12
+ import std/Option
12
13
  import std/game/Vec2
13
14
  import std/game/Color
14
15
  import std/game/Affine
@@ -104,7 +105,7 @@ fun buildWorld(sheet: Int, white: Int) -> World =
104
105
  root.push(countLabel(30000 + i, i, Float(i * 30 % Counts.WorldW), Float(i * 17 % Counts.WorldH)))
105
106
  top := Node.group(1, 0.0, 0.0, Float(Counts.WorldW), Float(Counts.WorldH), root)
106
107
  cam := Affine(a: 0.8, b: 0.0, c: 0.0, d: 0.8, e: 0.0, f: 0.0)
107
- World(sheet: sheet, stage: Stage(root: top, camera: cam, captured: null, focus: null))
108
+ World(sheet: sheet, stage: Stage(root: top, camera: cam, captured: None, focus: None))
108
109
 
109
110
  fun whiteTexture(io: Io) -> Int =
110
111
  t := io.textureCreate(1, 1)
@@ -130,7 +131,7 @@ test "ten frames of update run"
130
131
  w := buildWorld(0, 0)
131
132
  for i := range 10
132
133
  update(w, 0.016)
133
- if k := w.stage.root.children()
134
+ if Some(k) := w.stage.root.children()
134
135
  assert k.length() == 50 + 500 + 200 + 50
135
136
  assert {k.at(50).x - 20.0}.abs() < 0.000001
136
137
  else
plum-game/web-test/fixtures/stress_state_view.plum CHANGED
@@ -17,6 +17,7 @@ import std/Str
17
17
  import std/Int
18
18
  import std/Float
19
19
  import std/Slice
20
+ import std/Option
20
21
  import std/List
21
22
  import std/game/Vec2
22
23
  import std/game/Color
@@ -83,9 +84,9 @@ struct Pulser(id: Int, x: Float, y: Float, start: Float, srcX: Float)
83
84
  struct State(sheet: Int, white: Int, spinners: List[Spinner], drifters: List[Drifter], counters: List[Counter], pulsers: List[Pulser], anim: Anim, clock: Float, ui: UiState, clicks: Int)
84
85
 
85
86
  fun pulseAnim() -> Anim =
86
- Forever(anim: Seq(steps: List(
87
+ Forever(anim: Seq(steps: [
87
- Par(steps: List(MoveBy(dx: 30.0, dy: 0.0, secs: 1.0), RotateBy(radians: 3.14159, secs: 1.0), ScaleTo(sx: 1.5, sy: 1.5, secs: 1.0))),
88
+ Par(steps: [MoveBy(dx: 30.0, dy: 0.0, secs: 1.0), RotateBy(radians: 3.14159, secs: 1.0), ScaleTo(sx: 1.5, sy: 1.5, secs: 1.0)]),
88
- Par(steps: List(MoveBy(dx: 0.0 - 30.0, dy: 0.0, secs: 1.0), ScaleTo(sx: 0.6666, sy: 0.6666, secs: 1.0))))))
89
+ Par(steps: [MoveBy(dx: 0.0 - 30.0, dy: 0.0, secs: 1.0), ScaleTo(sx: 0.6666, sy: 0.6666, secs: 1.0)])]))
89
90
 
90
91
  fun newPulser(i: Int) -> Pulser =
91
92
  Pulser(id: 50000 + i, x: Float(i * 97 % 1500), y: Float(i * 53 % 850), start: Float(i % 10) * 0.1, srcX: Float(i % 4 * 16))
@@ -144,7 +145,10 @@ fun countClicks(es: List[UiEvent]) -> Int = match es
144
145
 
145
146
  # Feeds one pointer input through Ui.step against the button layer.
146
147
  fun stepUi(s: State, input: Input) -> State =
147
- pos := input.position() ?: return s
148
+ po := input.position()
149
+ if po.isNone()
150
+ return s
151
+ pos := po.unwrap()
148
152
  hit := buttonsView(s.ui).hitId(pos.x, pos.y)
149
153
  r := Ui.step(s.ui, input, hit)
150
154
  State(
@@ -192,7 +196,7 @@ fun view(s: State) -> SceneNode[Int] =
192
196
  drifts := toNodes(s.drifters, |d| drifterView(s.white, d), texts)
193
197
  pulses := toNodes(s.pulsers, |p| pulserView(s.sheet, s.anim, s.clock, p), drifts)
194
198
  world: SceneNode[Int] := Group(transform: Scale(sx: 0.8, sy: 0.8), children: toNodes(s.spinners, |p| spinnerView(s.sheet, p), pulses))
195
- Group(transform: Identity, children: List(world, buttonsView(s.ui)))
199
+ Group(transform: Identity, children: [world, buttonsView(s.ui)])
196
200
 
197
201
  fun whiteTexture(io: Io) -> Int =
198
202
  t := io.textureCreate(1, 1)
plum-std/Base64.plum CHANGED
@@ -4,6 +4,8 @@ import std/Str
4
4
  import std/Buffer
5
5
  import std/Int
6
6
  import std/Float
7
+ import std/Option
8
+ import std/Result
7
9
 
8
10
  # The Base64 package contains support for doing Base64 binary-to-text encodings.
9
11
  #
@@ -14,6 +16,9 @@ import std/Float
14
16
  # byte array here (see `libs/std/str.plum`), so this is a plain byte string,
15
17
  # not a claim that the content is valid UTF-8 text.
16
18
 
19
+ # Why `Base64.decode` failed.
20
+ enum Base64Error = InvalidBase64
21
+
17
22
  primitive Base64 =
18
23
  # Encode for PEM (RFC 1421): 64-character lines.
19
24
  fun encodePEM(data: Str) -> Str =
@@ -98,13 +103,13 @@ primitive Base64 =
98
103
  b == 32 || b == 9 || b == 10 || b == 13
99
104
 
100
105
  # Decode for URLs (RFC 4648).
101
- fun decodeUrl(data: Str) -> error{InvalidBase64}!Str =
106
+ fun decodeUrl(data: Str) -> Result[Str, Base64Error] =
102
107
  return decode(data, "-", "_", "=")
103
108
 
104
109
  # Configurable decoding. The defaults are for RFC 4648. Missing padding is
105
110
  # not an error. Non-base64 data, other than whitespace (which can appear at
106
- # any point), fails with `InvalidBase64`.
111
+ # any point), fails with `Err(InvalidBase64)`.
107
- fun decode(data: Str, at62: Str = "+", at63: Str = "/", pad: Str = "=") -> error{InvalidBase64}!Str =
112
+ fun decode(data: Str, at62: Str = "+", at63: Str = "/", pad: Str = "=") -> Result[Str, Base64Error] =
108
113
  at62b := at62.byteAt(0)
109
114
  at63b := at63.byteAt(0)
110
115
  has_pad := pad.length() > 0
@@ -121,13 +126,13 @@ primitive Base64 =
121
126
  continue
122
127
  v := decodeByte(b, at62b, at63b)
123
128
  if v < 0
124
- return error.InvalidBase64
129
+ return Err(InvalidBase64)
125
130
  bits = {bits << 6} | v
126
131
  nbits = nbits + 6
127
132
  if nbits >= 8
128
133
  nbits = nbits - 8
129
134
  res.writeByte(Byte({bits >> nbits} & 0xFF))
130
- return res.toStr()
135
+ return Ok(res.toStr())
131
136
 
132
137
  test "statics are reached through the Base64 namespace and round-trip"
133
138
  assert Base64.decode(Base64.encodePEM("hello"))! == "hello"
plum-std/Bool.plum CHANGED
@@ -1,27 +1,32 @@
1
1
  module std
2
2
 
3
3
  import std/Str
4
+ import std/Option
5
+ import std/Result
4
6
 
5
7
  # The API of the built-in `Bool` type: `Bool.parse` and `toStr` (also used by
6
8
  # `"{b}"` interpolation). `true`/`false` and the operators are built in; the
7
9
  # methods need `import std/Bool`.
10
+ # Why `Bool.parse` failed.
11
+ enum BoolError = BadBoolStr
12
+
8
13
  primitive Bool =
9
14
  fun toStr(self) -> Str =
10
15
  if self then "true" else "false"
11
16
 
12
17
  # Parses exactly `"true"` or `"false"`.
13
- fun parse(s: Str) -> error{BadBoolStr}!Bool =
18
+ fun parse(s: Str) -> Result[Bool, BoolError] =
14
19
  if s == "true"
15
- true
20
+ Ok(true)
16
21
  else if s == "false"
17
- false
22
+ Ok(false)
18
23
  else
19
- return error.BadBoolStr
24
+ Err(BadBoolStr)
20
25
 
21
26
  test "Bool.parse accepts true/false and rejects everything else"
22
- assert Bool.parse("true") catch false
27
+ assert Bool.parse("true").unwrapOr(false)
23
- assert !{Bool.parse("false") catch true}
28
+ assert !Bool.parse("false").unwrapOr(true)
24
- assert !{Bool.parse("yes") catch false}
29
+ assert !Bool.parse("yes").unwrapOr(false)
25
30
 
26
31
  test "Bool.toStr is true/false, also through interpolation"
27
32
  t := true
plum-std/Deque.plum CHANGED
@@ -2,6 +2,7 @@ module std
2
2
 
3
3
  import std/Array
4
4
  import std/Slice
5
+ import std/Option
5
6
  import std/Int
6
7
  import std/Float
7
8
 
@@ -47,27 +48,27 @@ struct Deque[T](mut buf: Array[T], mut head: Int, mut size: Int) =
47
48
  self.buf.set(self.head, v)
48
49
  self.size = self.size + 1
49
50
 
50
- # Removes and returns the front element, or an error `DequeWasEmpty`.
51
+ # Removes and returns the front element, or None if empty.
51
- fun popFront(self) -> error{DequeWasEmpty}!T =
52
+ fun popFront(self) -> Option[T] =
52
53
  if self.size == 0
53
- return error.DequeWasEmpty
54
+ return None
54
55
  v := self.buf.get(self.head)
55
56
  self.head = {self.head + 1} % self.buf.length()
56
57
  self.size = self.size - 1
57
- v
58
+ Some(v)
58
59
 
59
- # Removes and returns the back element, or an error `DequeWasEmpty`.
60
+ # Removes and returns the back element, or None if empty.
60
- fun popBack(self) -> error{DequeWasEmpty}!T =
61
+ fun popBack(self) -> Option[T] =
61
62
  if self.size == 0
62
- return error.DequeWasEmpty
63
+ return None
63
64
  self.size = self.size - 1
64
- self.buf.get({self.head + self.size} % self.buf.length())
65
+ Some(self.buf.get({self.head + self.size} % self.buf.length()))
65
66
 
66
- # The `i`'th element from the front, or an error `OutOfBounds`.
67
+ # The `i`'th element from the front, or None if out of bounds.
67
- fun get(self, i: Int) -> error{OutOfBounds}!T =
68
+ fun get(self, i: Int) -> Option[T] =
68
69
  if i < 0 || i >= self.size
69
- return error.OutOfBounds
70
+ return None
70
- self.buf.get({self.head + i} % self.buf.length())
71
+ Some(self.buf.get({self.head + i} % self.buf.length()))
71
72
 
72
73
  # the `i`'th element from the front, unchecked (the `for x := range deque` protocol)
73
74
  fun at(self, i: Int) -> T =
@@ -116,6 +117,6 @@ test "pushFront builds the front in reverse order"
116
117
 
117
118
  test "pop on empty is an error"
118
119
  d := Deque[Int]()
119
- assert {d.popFront() catch -1} == -1
120
+ assert d.popFront().unwrapOr(-1) == -1
120
- assert {d.popBack() catch -1} == -1
121
+ assert d.popBack().unwrapOr(-1) == -1
121
- assert {d.get(0) catch -1} == -1
122
+ assert d.get(0).unwrapOr(-1) == -1
plum-std/Float.plum CHANGED
@@ -3,6 +3,8 @@ module std
3
3
  import std/Str
4
4
  import std/Int
5
5
  import std/Math
6
+ import std/Option
7
+ import std/Result
6
8
 
7
9
  # The API of the built-in `Float` type: associated constants, `Float.parse`
8
10
  # and the numeric methods (`x.abs()`, `x.sqrt()`, ...).
@@ -27,21 +29,21 @@ primitive Float =
27
29
 
28
30
  # Parses a decimal float, with an optional leading `+`/`-` and an optional
29
31
  # `.` fractional part (no exponent notation).
30
- fun parse(s: Str) -> error{BadNumStr}!Float =
32
+ fun parse(s: Str) -> Result[Float, NumError] =
31
33
  len := s.length()
32
34
  if len == 0
33
- return error.BadNumStr
35
+ return Err(BadNumStr)
34
36
  neg := s.byteAt(0) == 45
35
37
  start := if neg || s.byteAt(0) == 43 then 1 else 0
36
38
  if start >= len
37
- return error.BadNumStr
39
+ return Err(BadNumStr)
38
40
  int_part := 0.0f
39
41
  saw_digit := false
40
42
  i := start
41
43
  while i < len && s.byteAt(i) != 46
42
44
  b := s.byteAt(i)
43
45
  if b < 48 || b > 57
44
- return error.BadNumStr
46
+ return Err(BadNumStr)
45
47
  int_part = int_part * 10.0f + Float(b - 48)
46
48
  saw_digit = true
47
49
  i = i + 1
@@ -52,15 +54,15 @@ primitive Float =
52
54
  while i < len
53
55
  b := s.byteAt(i)
54
56
  if b < 48 || b > 57
55
- return error.BadNumStr
57
+ return Err(BadNumStr)
56
58
  frac_scale = frac_scale / 10.0f
57
59
  frac_part = frac_part + Float(b - 48) * frac_scale
58
60
  saw_digit = true
59
61
  i = i + 1
60
62
  if !saw_digit
61
- return error.BadNumStr
63
+ return Err(BadNumStr)
62
64
  value := int_part + frac_part
63
- return if neg then -value else value
65
+ return Ok(if neg then -value else value)
64
66
 
65
67
  fun abs(self) -> Float =
66
68
  if self < 0.0f then -self else self
plum-std/Heap.plum CHANGED
@@ -2,6 +2,7 @@ module std
2
2
 
3
3
  import std/Array
4
4
  import std/Slice
5
+ import std/Option
5
6
  import std/Int
6
7
  import std/Float
7
8
 
@@ -60,22 +61,22 @@ struct Heap[T](items: Slice[T], key: fn(T) -> Float) =
60
61
  self.items.push(v)
61
62
  self.siftUp(self.items.length() - 1)
62
63
 
63
- # The smallest element without removing it, or an error `HeapWasEmpty`.
64
+ # The smallest element without removing it, or None if empty.
64
- fun peek(self) -> error{HeapWasEmpty}!T =
65
+ fun peek(self) -> Option[T] =
65
66
  if self.items.length() == 0
66
- return error.HeapWasEmpty
67
+ return None
67
- self.items.data.get(0)
68
+ Some(self.items.data.get(0))
68
69
 
69
- # Removes and returns the smallest element, or an error `HeapWasEmpty`.
70
+ # Removes and returns the smallest element, or None if empty.
70
- fun pop(self) -> error{HeapWasEmpty}!T =
71
+ fun pop(self) -> Option[T] =
71
72
  n := self.items.length()
72
73
  if n == 0
73
- return error.HeapWasEmpty
74
+ return None
74
75
  top := self.items.data.get(0)
75
76
  self.items.data.set(0, self.items.data.get(n - 1))
76
77
  self.items.size = n - 1
77
78
  self.siftDown(0)
78
- top
79
+ Some(top)
79
80
 
80
81
  test "heap_pop_order: 1000 pseudo-random keys pop in ascending order"
81
82
  h := Heap[Int](items: Slice[Int](), key: |x| Float(x))
@@ -94,7 +95,7 @@ test "heap_pop_order: 1000 pseudo-random keys pop in ascending order"
94
95
  prev = v
95
96
  i = i + 1
96
97
  assert h.isEmpty()
97
- assert {h.pop() catch -1} == -1
98
+ assert h.pop().unwrapOr(-1) == -1
98
99
 
99
100
  test "peek shows the minimum and Float keys order records"
100
101
  h := Heap[Float](items: Slice[Float](), key: |x| 0.0 - x)
plum-std/Http.plum CHANGED
@@ -8,9 +8,18 @@ import std/Str
8
8
  import std/Int
9
9
  import std/Float
10
10
  import std/Io
11
+ import std/Option
12
+ import std/Result
11
13
 
12
14
  # Transport failure, or a response that is not valid HTTP.
15
+ enum HttpError =
16
+ | HttpFailed
17
+ | BadResponse
18
+
19
+ fun name(self) -> Str =
20
+ match self
21
+ HttpFailed => "HttpFailed"
13
- error HttpError = HttpFailed | BadResponse
22
+ BadResponse => "BadResponse"
14
23
 
15
24
  # An HTTP response: status code, response headers, and the raw response
16
25
  # body as a `Str` (a byte array, so binary bodies round-trip intact).
@@ -26,8 +35,8 @@ struct Response(status: Int, headers: Map[Str, Str], body: Str) =
26
35
 
27
36
  # Parses `body` as JSON. Fails the same way `Json.fromStr` does for a
28
37
  # response body that isn't valid JSON.
29
- fun json(self) -> error{InvalidJson}!Json =
38
+ fun json(self) -> Result[Json, JsonError] =
30
- return try Json.fromStr(self.body)
39
+ return Json.fromStr(self.body)
31
40
 
32
41
  # A single blocking network round trip — this VM has no async/event-loop
33
42
  # mechanism, so `rawHttpRequest` runs to completion before returning (same
@@ -77,40 +86,41 @@ primitive Http =
77
86
  # successful round trip, returned as a `Response` with that status (check
78
87
  # `Response.ok()`). Fails with `HttpFailed` for a transport error and
79
88
  # `BadResponse` for a reply whose status line can't be read.
80
- fun request(io: Io, method: Str, url: Str, headers: Map[Str, Str], body: Str) -> HttpError!Response =
89
+ fun request(io: Io, method: Str, url: Str, headers: Map[Str, Str], body: Str) -> Result[Response, HttpError] =
81
90
  raw := io.rawHttpRequest(method, url, encodeHeaders(headers), body)
82
91
  sep := raw.indexOf("\x00")
83
92
  meta := if sep >= 0 then raw.sub(0, sep) else raw
84
93
  resp_body := if sep >= 0 then raw.sub(sep + 1, raw.length()) else ""
85
94
  parts := meta.split("\x01", 3)
86
- flag := parts.get(0) catch ""
95
+ flag := parts.get(0).unwrapOr("")
87
96
  if flag != "1"
88
- return error.HttpFailed
97
+ return Err(HttpFailed)
89
- status := Int.parse(parts.get(1) catch "") catch return error.BadResponse
98
+ match Int.parse(parts.get(1).unwrapOr(""))
90
- return Response(status: status, headers: decodeHeaders(parts.get(2) catch ""), body: resp_body)
99
+ Ok(status) => Ok(Response(status: status, headers: decodeHeaders(parts.get(2).unwrapOr("")), body: resp_body))
100
+ Err(e) => Err(BadResponse)
91
101
 
92
- fun get(io: Io, url: Str, headers: Map[Str, Str]) -> HttpError!Response =
102
+ fun get(io: Io, url: Str, headers: Map[Str, Str]) -> Result[Response, HttpError] =
93
103
  return request(io, "GET", url, headers, "")
94
104
 
95
- fun head(io: Io, url: Str, headers: Map[Str, Str]) -> HttpError!Response =
105
+ fun head(io: Io, url: Str, headers: Map[Str, Str]) -> Result[Response, HttpError] =
96
106
  return request(io, "HEAD", url, headers, "")
97
107
 
98
- fun delete(io: Io, url: Str, headers: Map[Str, Str]) -> HttpError!Response =
108
+ fun delete(io: Io, url: Str, headers: Map[Str, Str]) -> Result[Response, HttpError] =
99
109
  return request(io, "DELETE", url, headers, "")
100
110
 
101
- fun post(io: Io, url: Str, headers: Map[Str, Str], body: Str) -> HttpError!Response =
111
+ fun post(io: Io, url: Str, headers: Map[Str, Str], body: Str) -> Result[Response, HttpError] =
102
112
  return request(io, "POST", url, headers, body)
103
113
 
104
- fun put(io: Io, url: Str, headers: Map[Str, Str], body: Str) -> HttpError!Response =
114
+ fun put(io: Io, url: Str, headers: Map[Str, Str], body: Str) -> Result[Response, HttpError] =
105
115
  return request(io, "PUT", url, headers, body)
106
116
 
107
- fun patch(io: Io, url: Str, headers: Map[Str, Str], body: Str) -> HttpError!Response =
117
+ fun patch(io: Io, url: Str, headers: Map[Str, Str], body: Str) -> Result[Response, HttpError] =
108
118
  return request(io, "PATCH", url, headers, body)
109
119
 
110
120
  # Convenience wrapper: sends `body` as a JSON `Str`, adding a
111
121
  # `Content-Type: application/json` header, and parses the response back as
112
122
  # JSON via `Response.json()`.
113
- fun postJson(io: Io, url: Str, headers: Map[Str, Str], body: Json) -> HttpError!Response =
123
+ fun postJson(io: Io, url: Str, headers: Map[Str, Str], body: Json) -> Result[Response, HttpError] =
114
124
  headers.set("Content-Type", "application/json")
115
125
  return post(io, url, headers, body.toStr())
116
126
 
@@ -142,5 +152,5 @@ test "post sends a body and the server echoes it back"
142
152
  assert j.asMap()!.get("data")!.asStr()! == "hello world"
143
153
 
144
154
  test "request fails for an unreachable host"
145
- resp := Http.get(io, "http://this-host-does-not-exist.invalid/", Http.emptyHeaders()) catch Response(status: 0, headers: Http.emptyHeaders(), body: "")
155
+ resp := Http.get(io, "http://this-host-does-not-exist.invalid/", Http.emptyHeaders()).unwrapOr(Response(status: 0, headers: Http.emptyHeaders(), body: ""))
146
156
  assert resp.status == 0
plum-std/Int.plum CHANGED
@@ -4,6 +4,11 @@ import std/Str
4
4
  import std/Float
5
5
  import std/Math
6
6
  import std/Pair
7
+ import std/Option
8
+ import std/Result
9
+
10
+ # Why `Int.parse` / `Float.parse` failed.
11
+ enum NumError = BadNumStr
7
12
 
8
13
  # The API of the built-in `Int` type: associated constants, `Int.parse`, the
9
14
  # numeric methods (`x.abs()`, `x.min(y)`, ...) and the `for i := range n` loop
@@ -13,23 +18,23 @@ primitive Int =
13
18
  Max = 0x7FFF_FFFF_FFFF_FFFF # Highest value of Int
14
19
 
15
20
  # Parses a decimal integer, with an optional leading `+`/`-`.
16
- fun parse(s: Str) -> error{BadNumStr}!Int =
21
+ fun parse(s: Str) -> Result[Int, NumError] =
17
22
  len := s.length()
18
23
  if len == 0
19
- return error.BadNumStr
24
+ return Err(BadNumStr)
20
25
  neg := s.byteAt(0) == 45
21
26
  start := if neg || s.byteAt(0) == 43 then 1 else 0
22
27
  if start >= len
23
- return error.BadNumStr
28
+ return Err(BadNumStr)
24
29
  value := 0
25
30
  i := start
26
31
  while i < len
27
32
  b := s.byteAt(i)
28
33
  if b < 48 || b > 57
29
- return error.BadNumStr
34
+ return Err(BadNumStr)
30
35
  value = value * 10 + b - 48
31
36
  i = i + 1
32
- return if neg then -value else value
37
+ return Ok(if neg then -value else value)
33
38
 
34
39
  fun abs(self) -> Int =
35
40
  if self < 0 then -self else self
@@ -197,9 +202,9 @@ primitive Int =
197
202
  # when `step` is negative. A `for x := range v` source via `next`.
198
203
  struct Range(start: Int, stop: Int, step: Int) =
199
204
 
200
- fun next(self) -> Pair[Int, Range]? =
205
+ fun next(self) -> Option[Pair[Int, Range]] =
201
206
  more := if self.step > 0 then self.start < self.stop else self.start > self.stop
202
- if more then Pair(first: self.start, second: Range(start: self.start + self.step, stop: self.stop, step: self.step)) else null
207
+ if more then Some(Pair(first: self.start, second: Range(start: self.start + self.step, stop: self.stop, step: self.step))) else None
203
208
 
204
209
  # Every `by`-th element, starting with the first (`by` > 0).
205
210
  fun stepBy(self, by: Int) -> Range =
plum-std/Json.plum CHANGED
@@ -7,6 +7,8 @@ import std/Int
7
7
  import std/Float
8
8
  import std/Str
9
9
  import std/Buffer
10
+ import std/Option
11
+ import std/Result
10
12
 
11
13
  # Placement rule for built-in `toJson` / `fromJson`: they live HERE, in this
12
14
  # file, as extension blocks at the bottom (`primitive Int =`, `primitive Slice[T] =`),
@@ -17,7 +19,16 @@ import std/Buffer
17
19
  # call `x.toJson()` / `T.fromJson(j)` per instantiation.
18
20
 
19
21
  # Why a JSON value could not be decoded into a type.
22
+ enum JsonError =
23
+ | InvalidJson
24
+ | WrongJsonType
25
+ | MissingField
26
+
27
+ fun name(self) -> Str =
28
+ match self
29
+ InvalidJson => "InvalidJson"
20
- error JsonError = InvalidJson | WrongJsonType | MissingField
30
+ WrongJsonType => "WrongJsonType"
31
+ MissingField => "MissingField"
21
32
 
22
33
 
23
34
  # A parsed JSON value. Numbers are always stored as `Float` (JSON doesn't
@@ -36,7 +47,7 @@ enum Json =
36
47
  # from surrounding whitespace) to be consumed. Called as `Json.fromStr(s)`,
37
48
  # matching `Int.fromStr`/`Float.fromStr`'s naming. Errors carry no data: to
38
49
  # learn where and why a parse failed use `Json.parse(s)` instead.
39
- fun fromStr(s: Str) -> error{InvalidJson}!Json =
50
+ fun fromStr(s: Str) -> Result[Json, JsonError] =
40
51
  parser := JsonParser(src: s, pos: 0, text: "")
41
52
  return parser.parseDocument()
42
53
 
@@ -44,9 +55,9 @@ enum Json =
44
55
  # document, or `Failed` with the position and message of the failure.
45
56
  fun parse(s: Str) -> JsonParse =
46
57
  parser := JsonParser(src: s, pos: 0, text: "")
47
- v := parser.parseDocument() catch e
58
+ match parser.parseDocument()
59
+ Ok(v) => Parsed(value: v)
48
- return Failed(pos: parser.pos, text: parser.text)
60
+ Err(e) => Failed(pos: parser.pos, text: parser.text)
49
- return Parsed(value: v)
50
61
 
51
62
  # Renders `self` back to its JSON text form. A literal `{`/`}` is written
52
63
  # doubled (`{{`/`}}`) — a single, un-doubled brace in a Plum string starts
@@ -64,28 +75,28 @@ enum Json =
64
75
  fun encode(x: T) -> Str = x.toJson().toStr()
65
76
 
66
77
  # `T` comes from the result annotation: `h: JsonError!House := Json.decode(s)`.
67
- fun decode(s: Str) -> JsonError!T =
78
+ fun decode(s: Str) -> Result[T, JsonError] =
68
- j := try Json.fromStr(s)
79
+ j := Json.fromStr(s)?
69
80
  return T.fromJson(j)
70
81
 
71
82
  # Decodes an already-parsed value; `T` comes from the result type.
72
- fun into(self) -> JsonError!T = T.fromJson(self)
83
+ fun into(self) -> Result[T, JsonError] = T.fromJson(self)
73
84
 
74
85
  # Explicit-decoder form: `Json.decodeWith(s, |j| House.fromJson(j))`.
75
- fun decodeWith(s: Str, f: fn(Json) -> JsonError!T) -> JsonError!T =
86
+ fun decodeWith(s: Str, f: fn(Json) -> Result[T, JsonError]) -> Result[T, JsonError] =
76
- j := try Json.fromStr(s)
87
+ j := Json.fromStr(s)?
77
88
  return f(j)
78
89
 
79
- fun field(self, name: Str) -> JsonError!Json =
90
+ fun field(self, name: Str) -> Result[Json, JsonError] =
80
- m := try self.asMap()
91
+ m := self.asMap()?
81
- v := m.get(name) catch e
92
+ match m.get(name)
93
+ Some(v) => Ok(v)
82
- return error.MissingField
94
+ None => Err(MissingField)
83
- return v
84
95
 
85
- fun optField(self, name: Str) -> Json? =
96
+ fun optField(self, name: Str) -> Option[Json] =
86
- m := self.asMap() catch e
97
+ match self.asMap()
87
- return null
88
- return m.getOrNull(name)
98
+ Ok(m) => m.get(name)
99
+ Err(e) => None
89
100
 
90
101
  # `Json.object(["a" => x, "b" => y])`: a map literal is already a Json object.
91
102
  fun object(fields: Map[Str, Json]) -> Json =
@@ -100,37 +111,37 @@ enum Json =
100
111
 
101
112
  fun toJson(self) -> Json = self
102
113
 
103
- fun fromJson(j: Json) -> JsonError!Json = j
114
+ fun fromJson(j: Json) -> Result[Json, JsonError] = Ok(j)
104
115
 
105
116
  fun isNull(self) -> Bool =
106
117
  match self
107
118
  JsonNull => true
108
119
  _ => false
109
120
 
110
- fun asBool(self) -> error{WrongJsonType}!Bool =
121
+ fun asBool(self) -> Result[Bool, JsonError] =
111
122
  match self
112
- JsonBool(value) => value
123
+ JsonBool(value) => Ok(value)
113
- _ => error.WrongJsonType
124
+ _ => Err(WrongJsonType)
114
125
 
115
- fun asFloat(self) -> error{WrongJsonType}!Float =
126
+ fun asFloat(self) -> Result[Float, JsonError] =
116
127
  match self
117
- JsonNum(value) => value
128
+ JsonNum(value) => Ok(value)
118
- _ => error.WrongJsonType
129
+ _ => Err(WrongJsonType)
119
130
 
120
- fun asStr(self) -> error{WrongJsonType}!Str =
131
+ fun asStr(self) -> Result[Str, JsonError] =
121
132
  match self
122
- JsonStr(value) => value
133
+ JsonStr(value) => Ok(value)
123
- _ => error.WrongJsonType
134
+ _ => Err(WrongJsonType)
124
135
 
125
- fun asList(self) -> error{WrongJsonType}!Slice[Json] =
136
+ fun asList(self) -> Result[Slice[Json], JsonError] =
126
137
  match self
127
- JsonArr(items) => items
138
+ JsonArr(items) => Ok(items)
128
- _ => error.WrongJsonType
139
+ _ => Err(WrongJsonType)
129
140
 
130
- fun asMap(self) -> error{WrongJsonType}!Map[Str, Json] =
141
+ fun asMap(self) -> Result[Map[Str, Json], JsonError] =
131
142
  match self
132
- JsonObj(fields) => fields
143
+ JsonObj(fields) => Ok(fields)
133
- _ => error.WrongJsonType
144
+ _ => Err(WrongJsonType)
134
145
 
135
146
  # `Map` has no built-in `toStr`/`join` (unlike `Slice`, which uses `join` for
136
147
  # `Json.toStr`'s `JsonList` case above) — a static taking the
@@ -221,18 +232,23 @@ struct JsonParser(src: Str, mut pos: Int, mut text: Str) =
221
232
  fun note(self, text: Str) -> Unit =
222
233
  self.text = text
223
234
 
235
+ # Notes "invalid number" and gives the error to fail with.
236
+ fun badNumber(self) -> JsonError =
237
+ self.note("invalid number")
238
+ InvalidJson
239
+
224
240
  fun message(self) -> Str =
225
241
  "json parse error at position {self.pos}: {self.text}"
226
242
 
227
243
  # The whole document: one value, then only whitespace.
228
- fun parseDocument(self) -> error{InvalidJson}!Json =
244
+ fun parseDocument(self) -> Result[Json, JsonError] =
229
245
  self.skipSpace()
230
- v := try self.parseValue()
246
+ v := self.parseValue()?
231
247
  self.skipSpace()
232
248
  if self.pos < self.src.length()
233
249
  self.note("unexpected trailing characters")
234
- return error.InvalidJson
250
+ return Err(InvalidJson)
235
- return v
251
+ return Ok(v)
236
252
 
237
253
  # The raw byte at the current position, or 0 (never a valid JSON byte) past
238
254
  # the end of `src` — lets every caller test for "end of input" the same way
@@ -251,12 +267,12 @@ struct JsonParser(src: Str, mut pos: Int, mut text: Str) =
251
267
  while isSpace(self.peek())
252
268
  self.advance()
253
269
 
254
- fun parseValue(self) -> error{InvalidJson}!Json =
270
+ fun parseValue(self) -> Result[Json, JsonError] =
255
271
  self.skipSpace()
256
272
  c := self.peek()
257
273
  if c == 34
258
- str := try self.parseString()
274
+ str := self.parseString()?
259
- return JsonStr(value: str)
275
+ return Ok(JsonStr(value: str))
260
276
  if c == 91
261
277
  return self.parseArray()
262
278
  if c == 123
@@ -270,30 +286,30 @@ struct JsonParser(src: Str, mut pos: Int, mut text: Str) =
270
286
  if c == 45 || isDigit(c)
271
287
  return self.parseNumber()
272
288
  self.note("unexpected character")
273
- return error.InvalidJson
289
+ return Err(InvalidJson)
274
290
 
275
- fun parseLiteral(self, lit: Str, value: Json) -> error{InvalidJson}!Json =
291
+ fun parseLiteral(self, lit: Str, value: Json) -> Result[Json, JsonError] =
276
292
  n := lit.length()
277
293
  if !Str.matchesAt(self.src, lit, self.pos, n)
278
294
  self.note("invalid literal, expected '{lit}'")
279
- return error.InvalidJson
295
+ return Err(InvalidJson)
280
296
  self.pos = self.pos + n
281
- return value
297
+ return Ok(value)
282
298
 
283
- fun parseNumber(self) -> error{InvalidJson}!Json =
299
+ fun parseNumber(self) -> Result[Json, JsonError] =
284
300
  start := self.pos
285
301
  if self.peek() == 45
286
302
  self.advance()
287
303
  if !isDigit(self.peek())
288
304
  self.note("invalid number")
289
- return error.InvalidJson
305
+ return Err(InvalidJson)
290
306
  while isDigit(self.peek())
291
307
  self.advance()
292
308
  if self.peek() == 46
293
309
  self.advance()
294
310
  if !isDigit(self.peek())
295
311
  self.note("invalid number")
296
- return error.InvalidJson
312
+ return Err(InvalidJson)
297
313
  while isDigit(self.peek())
298
314
  self.advance()
299
315
  if self.peek() == 101 || self.peek() == 69
@@ -302,7 +318,7 @@ struct JsonParser(src: Str, mut pos: Int, mut text: Str) =
302
318
  self.advance()
303
319
  if !isDigit(self.peek())
304
320
  self.note("invalid number")
305
- return error.InvalidJson
321
+ return Err(InvalidJson)
306
322
  while isDigit(self.peek())
307
323
  self.advance()
308
324
  return self.parseNumberText(self.src.sub(start, self.pos))
@@ -310,43 +326,37 @@ struct JsonParser(src: Str, mut pos: Int, mut text: Str) =
310
326
  # `Float.fromStr` (`libs/std/float.plum`) doesn't understand exponent
311
327
  # notation, so a number that has one is split into its mantissa and
312
328
  # exponent around the `e`/`E` and recombined here instead.
313
- fun parseNumberText(self, text: Str) -> error{InvalidJson}!Json =
329
+ fun parseNumberText(self, text: Str) -> Result[Json, JsonError] =
314
330
  e_index := exponentIndex(text)
315
331
  if e_index < 0
316
- f := Float.parse(text) catch e
332
+ f := Float.parse(text).mapErr(|e| self.badNumber())?
317
- self.note("invalid number")
318
- return error.InvalidJson
319
- return JsonNum(value: f)
333
+ return Ok(JsonNum(value: f))
320
- mv := Float.parse(text.sub(0, e_index)) catch e
334
+ mv := Float.parse(text.sub(0, e_index)).mapErr(|e| self.badNumber())?
321
- self.note("invalid number")
322
- return error.InvalidJson
323
- ev := Int.parse(text.sub(e_index + 1, text.length())) catch e
335
+ ev := Int.parse(text.sub(e_index + 1, text.length())).mapErr(|e| self.badNumber())?
324
- self.note("invalid number")
325
- return error.InvalidJson
326
- return JsonNum(value: mv * 10.pow(Float(ev)))
336
+ return Ok(JsonNum(value: mv * 10.pow(Float(ev))))
327
337
 
328
- fun parseString(self) -> error{InvalidJson}!Str =
338
+ fun parseString(self) -> Result[Str, JsonError] =
329
339
  self.advance()
330
340
  buf := Buffer()
331
341
  while true
332
342
  c := self.peek()
333
343
  if c == 0
334
344
  self.note("unterminated string")
335
- return error.InvalidJson
345
+ return Err(InvalidJson)
336
346
  if c == 34
337
347
  self.advance()
338
- return buf.toStr()
348
+ return Ok(buf.toStr())
339
349
  if c == 92
340
- try self.parseEscape(buf)
350
+ self.parseEscape(buf)?
341
351
  else
342
352
  n := self.src.codePointByteLength(self.pos)
343
353
  buf.write(self.src.sub(self.pos, self.pos + n))
344
354
  self.pos = self.pos + n
345
- return buf.toStr()
355
+ return Ok(buf.toStr())
346
356
 
347
357
  # Handles one escape sequence (the cursor is on the backslash), writing the
348
358
  # decoded text into `buf`.
349
- fun parseEscape(self, buf: Buffer) -> error{InvalidJson}!Unit =
359
+ fun parseEscape(self, buf: Buffer) -> Result[Unit, JsonError] =
350
360
  self.advance()
351
361
  esc := self.peek()
352
362
  if esc == 117
@@ -370,93 +380,95 @@ struct JsonParser(src: Str, mut pos: Int, mut text: Str) =
370
380
  buf.write("\t")
371
381
  else
372
382
  self.note("invalid escape character")
373
- return error.InvalidJson
383
+ return Err(InvalidJson)
374
384
  self.advance()
385
+ Ok(Unit)
375
386
 
376
- fun parseHex4(self) -> error{InvalidJson}!Int =
387
+ fun parseHex4(self) -> Result[Int, JsonError] =
377
388
  value := 0
378
389
  count := 0
379
390
  while count < 4
380
391
  d := hexDigitValue(self.peek())
381
392
  if d < 0
382
393
  self.note("invalid unicode escape")
383
- return error.InvalidJson
394
+ return Err(InvalidJson)
384
395
  self.advance()
385
396
  value = value * 16 + d
386
397
  count = count + 1
387
- return value
398
+ return Ok(value)
388
399
 
389
400
  # Decodes the four hex digits after `\u` (and, for a high surrogate
390
401
  # (`0xD800`..`0xDBFF`), the `\u` low surrogate (`0xDC00`..`0xDFFF`) that must
391
402
  # follow immediately; the pair encodes one codepoint above `0xFFFF`). Any
392
403
  # other codepoint is used as-is.
393
- fun parseUnicodeEscape(self, buf: Buffer) -> error{InvalidJson}!Unit =
404
+ fun parseUnicodeEscape(self, buf: Buffer) -> Result[Unit, JsonError] =
394
- cp := try self.parseHex4()
405
+ cp := self.parseHex4()?
395
406
  if cp >= 0xD800 && cp <= 0xDBFF && self.peek() == 92 && self.peekAt(1) == 117
396
407
  self.advance()
397
408
  self.advance()
398
- low := try self.parseHex4()
409
+ low := self.parseHex4()?
399
410
  if low < 0xDC00 || low > 0xDFFF
400
411
  self.note("invalid low surrogate")
401
- return error.InvalidJson
412
+ return Err(InvalidJson)
402
413
  combined := 0x10000 + {{cp - 0xD800} << 10} + {low - 0xDC00}
403
414
  buf.writeRune(combined)
404
- return
415
+ return Ok(Unit)
405
416
  buf.writeRune(cp)
417
+ Ok(Unit)
406
418
 
407
- fun parseArray(self) -> error{InvalidJson}!Json =
419
+ fun parseArray(self) -> Result[Json, JsonError] =
408
420
  self.advance()
409
421
  self.skipSpace()
410
422
  items := Slice[Json]()
411
423
  if self.peek() == 93
412
424
  self.advance()
413
- return JsonArr(items: items)
425
+ return Ok(JsonArr(items: items))
414
426
  while true
415
427
  self.skipSpace()
416
- v := try self.parseValue()
428
+ v := self.parseValue()?
417
429
  items.push(v)
418
430
  self.skipSpace()
419
431
  c := self.peek()
420
432
  if c == 93
421
433
  self.advance()
422
- return JsonArr(items: items)
434
+ return Ok(JsonArr(items: items))
423
435
  if c != 44
424
436
  self.note("expected ',' or ']'")
425
- return error.InvalidJson
437
+ return Err(InvalidJson)
426
438
  self.advance()
427
- return JsonArr(items: items)
439
+ return Ok(JsonArr(items: items))
428
440
 
429
- fun parseObject(self) -> error{InvalidJson}!Json =
441
+ fun parseObject(self) -> Result[Json, JsonError] =
430
442
  self.advance()
431
443
  self.skipSpace()
432
444
  pairs: Map[Str, Json] := Map.empty()
433
445
  if self.peek() == 125
434
446
  self.advance()
435
- return JsonObj(fields: pairs)
447
+ return Ok(JsonObj(fields: pairs))
436
448
  while true
437
449
  self.skipSpace()
438
450
  if self.peek() != 34
439
451
  self.note("expected string key")
440
- return error.InvalidJson
452
+ return Err(InvalidJson)
441
- key := try self.parseString()
453
+ key := self.parseString()?
442
454
  self.skipSpace()
443
455
  if self.peek() != 58
444
456
  self.note("expected ':'")
445
- return error.InvalidJson
457
+ return Err(InvalidJson)
446
458
  self.advance()
447
459
  self.skipSpace()
448
- v := try self.parseValue()
460
+ v := self.parseValue()?
449
461
  pairs.set(key, v)
450
462
  self.skipSpace()
451
463
  c := self.peek()
452
464
  if c == 125
453
465
  self.advance()
454
- return JsonObj(fields: pairs)
466
+ return Ok(JsonObj(fields: pairs))
455
467
  if c != 44
456
468
  self.note("expected ',' or '}}'")
457
- return error.InvalidJson
469
+ return Err(InvalidJson)
458
470
  self.advance()
459
- return JsonObj(fields: pairs)
471
+ return Ok(JsonObj(fields: pairs))
460
472
 
461
473
  fun exponentIndex(text: Str) -> Int =
462
474
  e := text.indexOf("e")
@@ -484,29 +496,29 @@ primitive Int =
484
496
  fun toJson(self) -> Json = JsonNum(value: Float(self))
485
497
 
486
498
  # Rejects a fraction and any magnitude of 2^53 or more (2^53+1 parses to 2^53, so the boundary is ambiguous).
487
- fun fromJson(j: Json) -> JsonError!Int =
499
+ fun fromJson(j: Json) -> Result[Int, JsonError] =
488
- f := try j.asFloat()
500
+ f := j.asFloat()?
489
501
  if f >= 9007199254740992.0f || f <= -9007199254740992.0f
490
- return error.WrongJsonType
502
+ return Err(WrongJsonType)
491
503
  n := Int(f)
492
504
  if Float(n) != f
493
- return error.WrongJsonType
505
+ return Err(WrongJsonType)
494
- return n
506
+ return Ok(n)
495
507
 
496
508
  primitive Float =
497
509
  fun toJson(self) -> Json = JsonNum(value: self)
498
510
 
499
- fun fromJson(j: Json) -> JsonError!Float = j.asFloat()
511
+ fun fromJson(j: Json) -> Result[Float, JsonError] = j.asFloat()
500
512
 
501
513
  primitive Bool =
502
514
  fun toJson(self) -> Json = JsonBool(value: self)
503
515
 
504
- fun fromJson(j: Json) -> JsonError!Bool = j.asBool()
516
+ fun fromJson(j: Json) -> Result[Bool, JsonError] = j.asBool()
505
517
 
506
518
  primitive Str =
507
519
  fun toJson(self) -> Json = JsonStr(value: self)
508
520
 
509
- fun fromJson(j: Json) -> JsonError!Str = j.asStr()
521
+ fun fromJson(j: Json) -> Result[Str, JsonError] = j.asStr()
510
522
 
511
523
  primitive Slice[T] =
512
524
  # A JSON array of the elements' own `toJson()`.
@@ -517,12 +529,12 @@ primitive Slice[T] =
517
529
  return JsonArr(items: out)
518
530
 
519
531
  # Decodes each element with `T.fromJson`.
520
- fun fromJson(j: Json) -> JsonError!Slice[T] =
532
+ fun fromJson(j: Json) -> Result[Slice[T], JsonError] =
521
- items := try j.asList()
533
+ items := j.asList()?
522
534
  out := Slice[T]()
523
535
  for x := range items
524
- out.push(try T.fromJson(x))
536
+ out.push(T.fromJson(x)?)
525
- return out
537
+ return Ok(out)
526
538
 
527
539
  primitive Map[K, V] =
528
540
  # A JSON object; keys must be `Str`.
@@ -532,20 +544,20 @@ primitive Map[K, V] =
532
544
  out.set(e.first, e.second.toJson())
533
545
  return JsonObj(fields: out)
534
546
 
535
- fun fromJson(j: Json) -> JsonError!Map[Str, V] =
547
+ fun fromJson(j: Json) -> Result[Map[Str, V], JsonError] =
536
- pairs := try j.asMap()
548
+ pairs := j.asMap()?
537
549
  out: Map[Str, V] := Map.empty()
538
550
  for e := range pairs.entries()
539
- out.set(e.first, try V.fromJson(e.second))
551
+ out.set(e.first, V.fromJson(e.second)?)
540
- return out
552
+ return Ok(out)
541
553
 
542
- fun jsonTestDecodeInt(s: Str) -> JsonError!Int = Json.decode(s)
554
+ fun jsonTestDecodeInt(s: Str) -> Result[Int, JsonError] = Json.decode(s)
543
555
 
544
- fun jsonTestDecodeNested(s: Str) -> JsonError!Slice[Slice[Int]] = Json.decode(s)
556
+ fun jsonTestDecodeNested(s: Str) -> Result[Slice[Slice[Int]], JsonError] = Json.decode(s)
545
557
 
546
- fun jsonTestDecodeMap(s: Str) -> JsonError!Map[Str, Int] = Json.decode(s)
558
+ fun jsonTestDecodeMap(s: Str) -> Result[Map[Str, Int], JsonError] = Json.decode(s)
547
559
 
548
- fun jsonTestDecodeStr(s: Str) -> JsonError!Str = Json.decode(s)
560
+ fun jsonTestDecodeStr(s: Str) -> Result[Str, JsonError] = Json.decode(s)
549
561
 
550
562
  # Fixture for the round-trip tests: a hand-written record codec (also a
551
563
  # worked example of the pattern), including a Slice of itself.
@@ -553,20 +565,20 @@ struct JsonTestHouse(name: Str, rooms: Int, kids: Slice[JsonTestHouse]) =
553
565
  fun toJson(self) -> Json =
554
566
  Json.object(["name" => self.name.toJson(), "rooms" => self.rooms.toJson(), "kids" => self.kids.toJson()])
555
567
 
556
- fun fromJson(j: Json) -> JsonError!JsonTestHouse =
568
+ fun fromJson(j: Json) -> Result[JsonTestHouse, JsonError] =
557
- name := try Str.fromJson(try j.field("name"))
569
+ name := Str.fromJson(j.field("name")?)?
558
- rooms := try Int.fromJson(try j.field("rooms"))
570
+ rooms := Int.fromJson(j.field("rooms")?)?
559
- kids := try jsonTestKidsOf(try j.field("kids"))
571
+ kids := jsonTestKidsOf(j.field("kids")?)?
560
- return JsonTestHouse(name: name, rooms: rooms, kids: kids)
572
+ return Ok(JsonTestHouse(name: name, rooms: rooms, kids: kids))
561
573
 
562
- fun jsonTestKidsOf(j: Json) -> JsonError!Slice[JsonTestHouse] = j.into()
574
+ fun jsonTestKidsOf(j: Json) -> Result[Slice[JsonTestHouse], JsonError] = j.into()
563
575
 
564
576
  fun jsonTestHouseError(s: Str) -> Str =
565
- jsonTestDecodeHouse(s) catch e
577
+ match jsonTestDecodeHouse(s)
578
+ Ok(h) => "ok"
566
- return e.name()
579
+ Err(e) => e.name()
567
- "ok"
568
580
 
569
- fun jsonTestDecodeHouse(s: Str) -> JsonError!JsonTestHouse = Json.decode(s)
581
+ fun jsonTestDecodeHouse(s: Str) -> Result[JsonTestHouse, JsonError] = Json.decode(s)
570
582
 
571
583
  test "parses null, true, and false"
572
584
  assert Json.fromStr("null")!.isNull()
@@ -604,9 +616,9 @@ test "parses a nested object"
604
616
  assert inner.get(2)!.asFloat()! == 3.0f
605
617
 
606
618
  test "reports an error on malformed input"
607
- bad := Json.fromStr("{{\"a\": }}") catch JsonNull
619
+ bad := Json.fromStr("{{\"a\": }}").unwrapOr(JsonNull)
608
620
  assert bad.isNull()
609
- assert {Json.fromStr("[1,") catch JsonNull}.isNull()
621
+ assert Json.fromStr("[1,").unwrapOr(JsonNull).isNull()
610
622
 
611
623
  test "parse reports where and why a document is invalid"
612
624
  match Json.parse("[1, x]")
@@ -690,20 +702,20 @@ test "api: field missing, wrong container, optField"
690
702
  o := Json.fromStr("{{\"a\":1}}")!
691
703
  l0 := Json.array([])
692
704
  assert o.field("a")!.asFloat()! == 1.0f
693
- assert {o.field("x") catch JsonNull}.isNull()
705
+ assert o.field("x").unwrapOr(JsonNull).isNull()
694
- assert {l0.field("x") catch JsonNull}.isNull()
706
+ assert l0.field("x").unwrapOr(JsonNull).isNull()
695
- assert o.optField("a") != null
707
+ assert o.optField("a").isSome()
696
- assert o.optField("x") == null
708
+ assert o.optField("x").isNone()
697
- assert l0.optField("a") == null
709
+ assert l0.optField("a").isNone()
698
710
  assert Json.object(["a" => Json.array([])]).toStr() == "{{\"a\":[]}}"
699
711
 
700
712
  test "int_fraction_rejected and int_over_2_53_rejected"
701
- assert {jsonTestDecodeInt("3.5") catch -1} == -1
713
+ assert jsonTestDecodeInt("3.5").unwrapOr(-1) == -1
702
- assert {jsonTestDecodeInt("1e17") catch -1} == -1
714
+ assert jsonTestDecodeInt("1e17").unwrapOr(-1) == -1
703
- assert {jsonTestDecodeInt("9007199254740993") catch -1} == -1
715
+ assert jsonTestDecodeInt("9007199254740993").unwrapOr(-1) == -1
704
- assert {jsonTestDecodeInt("[1]") catch -1} == -1
716
+ assert jsonTestDecodeInt("[1]").unwrapOr(-1) == -1
705
717
  assert jsonTestDecodeInt("-7")! == -7
706
- assert {jsonTestDecodeInt("9007199254740992") catch -1} == -1
718
+ assert jsonTestDecodeInt("9007199254740992").unwrapOr(-1) == -1
707
719
  assert jsonTestDecodeInt("9007199254740991")! == 9007199254740991
708
720
 
709
721
  test "nested_list_of_lists round-trips"
@@ -711,10 +723,10 @@ test "nested_list_of_lists round-trips"
711
723
  assert v.length() == 3
712
724
  assert v.get(0)!.get(1)! == 2
713
725
  assert Json.encode(v) == "[[1.0,2.0],[3.0],[]]"
714
- assert {jsonTestDecodeNested("[[1,\"x\"]]") catch Slice[Slice[Int]]()}.length() == 0
726
+ assert jsonTestDecodeNested("[[1,\"x\"]]").unwrapOr(Slice[Slice[Int]]()).length() == 0
715
727
 
716
728
  test "map_non_object_rejected"
717
- assert {jsonTestDecodeMap("[]") catch Map.empty()}.length() == 0
729
+ assert jsonTestDecodeMap("[]").unwrapOr(Map.empty()).length() == 0
718
730
  assert jsonTestDecodeMap("{{\"a\":2}}")!.get("a")! == 2
719
731
 
720
732
  test "str_unicode_roundtrip"
@@ -735,9 +747,9 @@ test "record round-trips including a list of itself"
735
747
  assert Json.encode(back) == text
736
748
 
737
749
  test "field_missing and decode_invalid"
738
- assert {jsonTestDecodeHouse("{{\"name\":\"x\"}}") catch JsonTestHouse(name: "E", rooms: 0, kids: Slice[JsonTestHouse]())}.name == "E"
750
+ assert jsonTestDecodeHouse("{{\"name\":\"x\"}}").unwrapOr(JsonTestHouse(name: "E", rooms: 0, kids: Slice[JsonTestHouse]())).name == "E"
739
- assert {jsonTestDecodeHouse("{{oops") catch JsonTestHouse(name: "E", rooms: 0, kids: Slice[JsonTestHouse]())}.name == "E"
751
+ assert jsonTestDecodeHouse("{{oops").unwrapOr(JsonTestHouse(name: "E", rooms: 0, kids: Slice[JsonTestHouse]())).name == "E"
740
- assert {jsonTestDecodeHouse("[1]") catch JsonTestHouse(name: "E", rooms: 0, kids: Slice[JsonTestHouse]())}.name == "E"
752
+ assert jsonTestDecodeHouse("[1]").unwrapOr(JsonTestHouse(name: "E", rooms: 0, kids: Slice[JsonTestHouse]())).name == "E"
741
753
 
742
754
  test "decodeWith uses an explicit decoder"
743
755
  h := Json.decodeWith("{{\"name\":\"w\",\"rooms\":1,\"kids\":[]}}", |j| JsonTestHouse.fromJson(j))!
plum-std/List.plum CHANGED
@@ -3,6 +3,7 @@ module std
3
3
  import std/Array
4
4
  import std/Buffer
5
5
  import std/Int
6
+ import std/Option
6
7
  import std/Pair
7
8
  import std/Slice
8
9
  import std/Str
@@ -25,13 +26,13 @@ enum List[T] =
25
26
 
26
27
  fun length(self) -> Int = self.lengthFrom(0)
27
28
 
28
- # The element at index i, or null when out of range.
29
+ # The element at index i, or None when out of range.
29
- fun get(self, i: Int) -> T? = match self
30
+ fun get(self, i: Int) -> Option[T] = match self
30
- Nil => null
31
+ Nil => None
31
- Cons(h, t) => if i < 0 then null else if i == 0 then h else t.get(i - 1)
32
+ Cons(h, t) => if i < 0 then None else if i == 0 then Some(h) else t.get(i - 1)
32
33
 
33
34
  # The element at index i (O(i) per call). Crashes out of range.
34
- fun at(self, i: Int) -> T = self.get(i) ?: crash "List.at out of range"
35
+ fun at(self, i: Int) -> T = self.get(i)!
35
36
 
36
37
  fun map(self, f: fn(T) -> B) -> List[B] = mapCells(self, f)
37
38
 
@@ -57,10 +58,10 @@ enum List[T] =
57
58
  fun append(self, ys: List[T]) -> List[T] =
58
59
  self.reverse().revOnto(ys)
59
60
 
60
- # The iterator step: `Pair(head, tail)`, or null once the list is done.
61
+ # The iterator step: `Some(Pair(head, tail))`, or None once the list is done.
61
- fun next(self) -> Pair[T, List[T]]? = match self
62
+ fun next(self) -> Option[Pair[T, List[T]]] = match self
62
- Nil => null
63
+ Nil => None
63
- Cons(h, t) => Pair(first: h, second: t)
64
+ Cons(h, t) => Some(Pair(first: h, second: t))
64
65
 
65
66
  # The elements of `xs` as a cons list, in order.
66
67
  fun fromSlice(xs: Slice[T]) -> List[T] =
plum-std/Map.plum CHANGED
@@ -7,6 +7,7 @@ import std/Float
7
7
  import std/Str
8
8
  import std/Buffer
9
9
  import std/Pair
10
+ import std/Option
10
11
 
11
12
  # A Map is an open-addressing hash table with an insertion-ordered dense entry
12
13
  # store (the layout of Python's dict):
@@ -74,16 +75,16 @@ struct Map[K, V](mut index: Array[Int], mut hashes: Array[Int], mut keys: Array[
74
75
  slot = {slot + 1} & mask
75
76
  return -1
76
77
 
77
- # Gets the value for `k`, or fails with `KeyNotFound` if absent.
78
+ # Gets the value for `k`, or None if absent.
78
- fun get(self, k: K) -> error{KeyNotFound}!V =
79
+ fun get(self, k: K) -> Option[V] =
79
80
  slot := self.findSlot(k, self.hashOf(k))
80
81
  if slot < 0
81
- return error.KeyNotFound
82
+ return None
82
- return self.vals.get(self.index.get(slot) - 2)
83
+ Some(self.vals.get(self.index.get(slot) - 2))
83
84
 
84
- # Gets the value for `k`, or null if absent.
85
+ # Gets the value for `k`, or None if absent (same as `get`; kept for callers not yet ported).
85
- fun getOrNull(self, k: K) -> V? =
86
+ fun getOrNull(self, k: K) -> Option[V] =
86
- return self.get(k) catch return null
87
+ return self.get(k)
87
88
 
88
89
  fun has(self, k: K) -> Bool =
89
90
  return self.findSlot(k, self.hashOf(k)) >= 0
plum-std/Os.plum CHANGED
@@ -7,9 +7,17 @@ import std/Random
7
7
  import std/Int
8
8
  import std/Float
9
9
  import std/Io
10
+ import std/Option
11
+ import std/Result
10
12
 
11
13
  # What the file functions that may hit either problem can fail with.
14
+ enum IoError =
15
+ | IoFailed
16
+ | NoSuchPath
17
+
18
+ fun name(self) -> Str = match self
19
+ IoFailed => "IoFailed"
12
- error IoError = IoFailed | NoSuchPath
20
+ NoSuchPath => "NoSuchPath"
13
21
 
14
22
  # Writes the specified data, followed by the current line terminator, to the
15
23
  # standard output stream. `extern` means this compiles to a genuine wasm import
@@ -48,29 +56,32 @@ primitive Os =
48
56
 
49
57
  # Reads the whole file at `path` as a `Str` (`Str` is a byte array here — see
50
58
  # `libs/std/str.plum` — so this works for binary content too, not just text).
51
- fun readFile(io: Io, path: Str) -> error{NoSuchPath}!Str =
59
+ fun readFile(io: Io, path: Str) -> Result[Str, IoError] =
52
60
  if !exists(io, path)
53
- return error.NoSuchPath
61
+ return Err(NoSuchPath)
54
- return io.rawReadFile(path)
62
+ return Ok(io.rawReadFile(path))
55
63
 
56
64
  # Writes `data` to `path`, replacing its previous contents (or creating it).
57
- fun writeFile(io: Io, path: Str, data: Str) -> error{IoFailed}!Unit =
65
+ fun writeFile(io: Io, path: Str, data: Str) -> Result[Unit, IoError] =
58
66
  if io.rawWriteFile(path, data) != 1
59
- return error.IoFailed
67
+ return Err(IoFailed)
68
+ Ok(Unit)
60
69
 
61
70
  fun exists(io: Io, path: Str) -> Bool =
62
71
  return io.rawExists(path) == 1
63
72
 
64
- fun mkdir(io: Io, path: Str) -> error{IoFailed}!Unit =
73
+ fun mkdir(io: Io, path: Str) -> Result[Unit, IoError] =
65
74
  if io.rawMkdir(path) != 1
66
- return error.IoFailed
75
+ return Err(IoFailed)
76
+ Ok(Unit)
67
77
 
68
78
  # Removes the file (not directory) at `path`.
69
- fun remove(io: Io, path: Str) -> IoError!Unit =
79
+ fun remove(io: Io, path: Str) -> Result[Unit, IoError] =
70
80
  if !exists(io, path)
71
- return error.NoSuchPath
81
+ return Err(NoSuchPath)
72
82
  if io.rawRemove(path) != 1
73
- return error.IoFailed
83
+ return Err(IoFailed)
84
+ Ok(Unit)
74
85
 
75
86
  # Whether `path` exists and is reachable — this VM has no notion of a
76
87
  # process' effective uid/gid, so (unlike POSIX `access(2)`) there is no mode
@@ -82,36 +93,36 @@ primitive Os =
82
93
  # contents) if it doesn't already exist. There's no host-level "open for
83
94
  # append" primitive, so this reads the existing bytes and writes the
84
95
  # concatenation back — not atomic, and O(existing file size) per call.
85
- fun appendFile(io: Io, path: Str, data: Str) -> IoError!Unit =
96
+ fun appendFile(io: Io, path: Str, data: Str) -> Result[Unit, IoError] =
86
97
  if exists(io, path)
87
- existing := try readFile(io, path)
98
+ existing := readFile(io, path)?
88
- try writeFile(io, path, existing + data)
99
+ return writeFile(io, path, existing + data)
89
- return
90
- try writeFile(io, path, data)
100
+ writeFile(io, path, data)
91
101
 
92
102
  # Unix permission bits only (e.g. `0o644`) — this VM has no Windows ACL
93
103
  # equivalent to fall back to.
94
- fun chmod(io: Io, path: Str, mode: Int) -> error{IoFailed}!Unit =
104
+ fun chmod(io: Io, path: Str, mode: Int) -> Result[Unit, IoError] =
95
105
  if io.rawChmod(path, mode) != 1
96
- return error.IoFailed
106
+ return Err(IoFailed)
107
+ Ok(Unit)
97
108
 
98
109
  # There is no host primitive for changing file ownership (it needs a uid/gid
99
110
  # syscall this VM doesn't expose), unlike the permission-bits-only `chmod`.
100
111
  fun chown(path: Str, uid: Int, gid: Int) =
101
112
  todo
102
113
 
103
- fun copyFile(io: Io, src: Str, dest: Str) -> IoError!Unit =
114
+ fun copyFile(io: Io, src: Str, dest: Str) -> Result[Unit, IoError] =
104
- data := try readFile(io, src)
115
+ data := readFile(io, src)?
105
- try writeFile(io, dest, data)
116
+ writeFile(io, dest, data)
106
117
 
107
- fun cp(io: Io, src: Str, dest: Str) -> IoError!Unit =
118
+ fun cp(io: Io, src: Str, dest: Str) -> Result[Unit, IoError] =
108
- try copyFile(io, src, dest)
119
+ copyFile(io, src, dest)
109
120
 
110
121
  # Symlinks aren't modeled separately from regular files/dirs by this VM's
111
122
  # host primitives (`rawChmod` always follows them), so this is the same as
112
123
  # `chmod`.
113
- fun lchmod(io: Io, path: Str, mode: Int) -> error{IoFailed}!Unit =
124
+ fun lchmod(io: Io, path: Str, mode: Int) -> Result[Unit, IoError] =
114
- try chmod(io, path, mode)
125
+ chmod(io, path, mode)
115
126
 
116
127
  # See `chown` — no uid/gid syscall exposed by this VM.
117
128
  fun lchown(path: Str, uid: Int, gid: Int) =
@@ -122,9 +133,10 @@ primitive Os =
122
133
  fun lutimes(path: Str, atime: Int, mtime: Int) =
123
134
  todo
124
135
 
125
- fun link(io: Io, existing_path: Str, new_path: Str) -> error{IoFailed}!Unit =
136
+ fun link(io: Io, existing_path: Str, new_path: Str) -> Result[Unit, IoError] =
126
137
  if io.rawLink(existing_path, new_path) != 1
127
- return error.IoFailed
138
+ return Err(IoFailed)
139
+ Ok(Unit)
128
140
 
129
141
  # Would need a real `Stat` struct type (size/mode/mtime/uid/gid/...) to
130
142
  # return anything useful — no such type exists in the stdlib yet.
@@ -134,12 +146,12 @@ primitive Os =
134
146
  # Creates a fresh directory named `prefix` followed by a random suffix,
135
147
  # retrying on the (astronomically unlikely) chance of a collision, and
136
148
  # returns the path actually created.
137
- fun mkdtemp(io: Io, prefix: Str) -> error{IoFailed}!Str =
149
+ fun mkdtemp(io: Io, prefix: Str) -> Result[Str, IoError] =
138
150
  path := prefix + Uuid.v4(Random.seeded(io.randomSeed())).value.toStr()
139
151
  if exists(io, path)
140
152
  return mkdtemp(io, prefix)
141
- try mkdir(io, path)
153
+ mkdir(io, path)?
142
- return path
154
+ return Ok(path)
143
155
 
144
156
  # Would need a host-side open-file-descriptor table (this VM's other file
145
157
  # ops are all one-shot: open+read/write+close in a single host call) plus a
@@ -152,9 +164,9 @@ primitive Os =
152
164
  todo
153
165
 
154
166
  # The names of `path`'s directory entries (not full paths).
155
- fun readdir(io: Io, path: Str) -> error{NoSuchPath}!Slice[Str] =
167
+ fun readdir(io: Io, path: Str) -> Result[Slice[Str], IoError] =
156
168
  if !exists(io, path)
157
- return error.NoSuchPath
169
+ return Err(NoSuchPath)
158
170
  # skipping empty names handles both a genuinely empty directory (whose
159
171
  # joined `""` would otherwise split into one bogus `""` entry) and
160
172
  # `rawReadDir`'s "unreadable" fallback (already ruled out by the `exists`
@@ -163,30 +175,32 @@ primitive Os =
163
175
  for n := range io.rawReadDir(path).split("\n", 0)
164
176
  if n != ""
165
177
  names.push(n)
166
- return names
178
+ return Ok(names)
167
179
 
168
- fun readlink(io: Io, path: Str) -> error{NoSuchPath}!Str =
180
+ fun readlink(io: Io, path: Str) -> Result[Str, IoError] =
169
181
  if !exists(io, path)
170
- return error.NoSuchPath
182
+ return Err(NoSuchPath)
171
- return io.rawReadlink(path)
183
+ return Ok(io.rawReadlink(path))
172
184
 
173
- fun realpath(io: Io, path: Str) -> error{NoSuchPath}!Str =
185
+ fun realpath(io: Io, path: Str) -> Result[Str, IoError] =
174
186
  if !exists(io, path)
175
- return error.NoSuchPath
187
+ return Err(NoSuchPath)
176
- return io.rawRealpath(path)
188
+ return Ok(io.rawRealpath(path))
177
189
 
178
- fun rename(io: Io, old_path: Str, new_path: Str) -> IoError!Unit =
190
+ fun rename(io: Io, old_path: Str, new_path: Str) -> Result[Unit, IoError] =
179
191
  if !exists(io, old_path)
180
- return error.NoSuchPath
192
+ return Err(NoSuchPath)
181
193
  if io.rawRename(old_path, new_path) != 1
182
- return error.IoFailed
194
+ return Err(IoFailed)
195
+ Ok(Unit)
183
196
 
184
197
  # Removes an empty directory. (`remove` above covers plain files.)
185
- fun rmdir(io: Io, path: Str) -> IoError!Unit =
198
+ fun rmdir(io: Io, path: Str) -> Result[Unit, IoError] =
186
199
  if !exists(io, path)
187
- return error.NoSuchPath
200
+ return Err(NoSuchPath)
188
201
  if io.rawRemoveDir(path) != 1
189
- return error.IoFailed
202
+ return Err(IoFailed)
203
+ Ok(Unit)
190
204
 
191
205
  # See `lstat` — needs a `Stat` struct type.
192
206
  fun stat(path: Str) =
@@ -196,17 +210,19 @@ primitive Os =
196
210
  fun statfs(path: Str) =
197
211
  todo
198
212
 
199
- fun symlink(io: Io, target: Str, path: Str) -> error{IoFailed}!Unit =
213
+ fun symlink(io: Io, target: Str, path: Str) -> Result[Unit, IoError] =
200
214
  if io.rawSymlink(target, path) != 1
201
- return error.IoFailed
215
+ return Err(IoFailed)
216
+ Ok(Unit)
202
217
 
203
218
  # Resizes the file at `path` to exactly `len` bytes — truncated if shorter,
204
219
  # zero-padded if longer.
205
- fun truncate(io: Io, path: Str, len: Int) -> IoError!Unit =
220
+ fun truncate(io: Io, path: Str, len: Int) -> Result[Unit, IoError] =
206
221
  if !exists(io, path)
207
- return error.NoSuchPath
222
+ return Err(NoSuchPath)
208
223
  if io.rawTruncate(path, len) != 1
209
- return error.IoFailed
224
+ return Err(IoFailed)
225
+ Ok(Unit)
210
226
 
211
227
  # See `lutimes` — same missing syscall.
212
228
  fun utimes(path: Str, atime: Int, mtime: Int) =
@@ -297,10 +313,10 @@ test "link creates a second name for the same file"
297
313
  Os.rmdir(io, dir)!
298
314
 
299
315
  test "readFile/remove/rmdir report errors for missing paths"
300
- assert {Os.readFile(io, "/tmp/plum_os_test_does_not_exist.txt") catch "gone"} == "gone"
316
+ assert Os.readFile(io, "/tmp/plum_os_test_does_not_exist.txt").unwrapOr("gone") == "gone"
301
317
  failed := 0
302
- Os.remove(io, "/tmp/plum_os_test_does_not_exist.txt") catch e
318
+ if Err(e) := Os.remove(io, "/tmp/plum_os_test_does_not_exist.txt")
303
319
  failed = failed + 1
304
- Os.rmdir(io, "/tmp/plum_os_test_does_not_exist_dir") catch e
320
+ if Err(e) := Os.rmdir(io, "/tmp/plum_os_test_does_not_exist_dir")
305
321
  failed = failed + 1
306
322
  assert failed == 2
plum-std/Pair.plum CHANGED
@@ -1,11 +1,12 @@
1
1
  module std
2
2
 
3
3
  import std/Str
4
+ import std/Option
4
5
 
5
6
  # A Pair groups two values (a key with its value in a Map) (`k => v` in a call is sugar for
6
7
  # `Pair(k, v)`). It is also the step of the loop protocol: a type with
7
- # `next(self) -> Pair[T, Self]?` is a `for x := range v` source (null = done,
8
+ # `next(self) -> Option[Pair[T, Self]]` is a `for x := range v` source (None = done,
8
- # else `Pair(value, rest)`).
9
+ # else `Some(Pair(value, rest))`).
9
10
  struct Pair[A, B](first: A, second: B) =
10
11
 
11
12
  fun toStr(self) -> Str =
plum-std/Slice.plum CHANGED
@@ -6,6 +6,7 @@ import std/Int
6
6
  import std/Float
7
7
  import std/Str
8
8
  import std/Random
9
+ import std/Option
9
10
 
10
11
  # A growable list over a backing `Array[T]`: element `i` is `data[start + i]`,
11
12
  # `size` slots are in use and `cap` slots (from `start`) may be written. Capacity
@@ -25,18 +26,18 @@ struct Slice[T](mut data: Array[T], mut start: Int, mut size: Int, mut cap: Int)
25
26
  Slice(data: Array[T](), start: 0, size: 0, cap: 0)
26
27
 
27
28
  # gets the element at i'th index of the list
28
- fun get(self, i: Int) -> error{OutOfBounds}!T =
29
+ fun get(self, i: Int) -> Option[T] =
29
30
  if i < 0 || i >= self.size
30
- return error.OutOfBounds
31
+ return None
31
- self.data.get(self.start + i)
32
+ Some(self.data.get(self.start + i))
32
33
 
33
- # sets the element at i'th index of the list, returning the old value (or fails with `OutOfBounds` if i is out of bounds)
34
+ # sets the element at i'th index of the list, returning the old value (or None if i is out of bounds)
34
- fun set(self, i: Int, v: T) -> error{OutOfBounds}!T =
35
+ fun set(self, i: Int, v: T) -> Option[T] =
35
36
  if i < 0 || i >= self.size
36
- return error.OutOfBounds
37
+ return None
37
38
  old := self.data.get(self.start + i)
38
39
  self.data.set(self.start + i, v)
39
- old
40
+ Some(old)
40
41
 
41
42
  # the element at index i, unchecked (the `for x := range list` protocol; i must be in range)
42
43
  fun at(self, i: Int) -> T =
@@ -111,12 +112,12 @@ struct Slice[T](mut data: Array[T], mut start: Int, mut size: Int, mut cap: Int)
111
112
  for v := range values
112
113
  self.push(v)
113
114
 
114
- # removes and returns the last element, or fails with `ListWasEmpty` if empty
115
+ # removes and returns the last element, or None if empty
115
- fun pop(self) -> error{ListWasEmpty}!T =
116
+ fun pop(self) -> Option[T] =
116
117
  if self.size == 0
117
- return error.ListWasEmpty
118
+ return None
118
119
  self.size = self.size - 1
119
- self.data.get(self.start + self.size)
120
+ Some(self.data.get(self.start + self.size))
120
121
 
121
122
  # removes the element at i'th index of the list (out of range is a no-op)
122
123
  fun removeAt(self, i: Int) =
@@ -170,15 +171,15 @@ struct Slice[T](mut data: Array[T], mut start: Int, mut size: Int, mut cap: Int)
170
171
  i = i + 1
171
172
  return result
172
173
 
173
- # Returns the first element equal to `search`, or fails with `NotFound`.
174
+ # Returns the first element equal to `search`, or None.
174
- fun findValue(self, search: T) -> error{NotFound}!T =
175
+ fun findValue(self, search: T) -> Option[T] =
175
176
  i := 0
176
177
  while i < self.size
177
178
  value := self.data.get(self.start + i)
178
179
  if value == search
179
- return value
180
+ return Some(value)
180
181
  i = i + 1
181
- return error.NotFound
182
+ return None
182
183
 
183
184
  fun contains(self, v: T) -> Bool =
184
185
  i := 0
@@ -189,22 +190,22 @@ struct Slice[T](mut data: Array[T], mut start: Int, mut size: Int, mut cap: Int)
189
190
  return false
190
191
 
191
192
  # returns the first element in the list
192
- fun first(self) -> error{ListWasEmpty}!T =
193
+ fun first(self) -> Option[T] =
193
194
  if self.size == 0
194
- return error.ListWasEmpty
195
+ return None
195
- self.data.get(self.start + 0)
196
+ Some(self.data.get(self.start + 0))
196
197
 
197
- # the first element, or null when the list is empty (the typed tail of `find`)
198
+ # the first element, or None when the list is empty (the typed tail of `find`)
198
- fun head(self) -> T? =
199
+ fun head(self) -> Option[T] =
199
200
  if self.size == 0
200
- return null
201
+ return None
201
- return self.data.get(self.start + 0)
202
+ return Some(self.data.get(self.start + 0))
202
203
 
203
204
  # returns the last element in the list
204
- fun last(self) -> error{ListWasEmpty}!T =
205
+ fun last(self) -> Option[T] =
205
206
  if self.size == 0
206
- return error.ListWasEmpty
207
+ return None
207
- self.data.get(self.start + self.size - 1)
208
+ Some(self.data.get(self.start + self.size - 1))
208
209
 
209
210
  # Returns a new list of the elements at index `[start, end)`, clamped into
210
211
  # range (a `start`/`end` outside `[0, length]` is clamped, not an error).
@@ -222,13 +223,13 @@ struct Slice[T](mut data: Array[T], mut start: Int, mut size: Int, mut cap: Int)
222
223
  fun drop(self, n: Int) -> Slice[T] =
223
224
  return self.sublist(n, self.size)
224
225
 
225
- # Returns one uniformly random element and the advanced generator, or fails with
226
+ # Returns one uniformly random element and the advanced generator, or None
226
- # `ListWasEmpty` if `self` is empty.
227
+ # if `self` is empty.
227
- fun sample(self, rng: Random) -> error{ListWasEmpty}!Rolled[T] =
228
+ fun sample(self, rng: Random) -> Option[Rolled[T]] =
228
229
  if self.size == 0
229
- return error.ListWasEmpty
230
+ return None
230
231
  d := rng.nextIntBelow(self.size)
231
- return Rolled(value: self.data.get(self.start + d.value), rng: d.rng)
232
+ return Some(Rolled(value: self.data.get(self.start + d.value), rng: d.rng))
232
233
 
233
234
  # Returns a new list with `self`'s elements in random order (Fisher-Yates) and the
234
235
  # advanced generator.
@@ -317,7 +318,7 @@ test "removing every node in a loop leaves an empty correctly functioning list"
317
318
  l.removeAt(0)
318
319
  i = i + 1
319
320
  assert l.length() == 0
320
- assert {l.get(0) catch -1000} == -1000
321
+ assert l.get(0).unwrapOr(-1000) == -1000
321
322
  l.add(Slice[Int]())
322
323
  l.push(42)
323
324
  assert l.get(0)! == 42
@@ -363,8 +364,8 @@ test "list sort orders elements correctly"
363
364
  l.push(1)
364
365
  l.push(5)
365
366
  sorted := l.sort(|a, b| a < b)
366
- assert {sorted.get(0) catch -1000} == 1
367
+ assert sorted.get(0).unwrapOr(-1000) == 1
367
- assert {sorted.get(4) catch -1000} == 5
368
+ assert sorted.get(4).unwrapOr(-1000) == 5
368
369
 
369
370
  test "list join renders elements correctly with separator"
370
371
  l := Slice[Int]()
@@ -404,7 +405,7 @@ test "list_get_set_push_pop: push 1000, get(999), pop until empty, get on empty
404
405
  i = i + 1
405
406
  assert l.length() == 1000
406
407
  assert l.get(999)! == 999
407
- assert {l.get(1000) catch -1000} == -1000
408
+ assert l.get(1000).unwrapOr(-1000) == -1000
408
409
  assert l.set(500, 7)! == 500
409
410
  assert l.get(500)! == 7
410
411
  assert l.pop()! == 999
@@ -412,10 +413,10 @@ test "list_get_set_push_pop: push 1000, get(999), pop until empty, get on empty
412
413
  while l.length() > 0
413
414
  _ := l.pop()!
414
415
  assert l.length() == 0
415
- assert {l.pop() catch -1} == -1
416
+ assert l.pop().unwrapOr(-1) == -1
416
- assert {l.get(0) catch -1000} == -1000
417
+ assert l.get(0).unwrapOr(-1000) == -1000
417
418
  e := l.get(0)
418
- assert {e catch -1} == -1
419
+ assert e.unwrapOr(-1) == -1
419
420
 
420
421
  test "list_growth: length stays exact across the doubling boundaries"
421
422
  l := Slice[Int]()
@@ -460,7 +461,7 @@ test "slice is a view: shares storage, no copy"
460
461
  assert ys.length() == 3
461
462
  assert ys.get(0)! == 2
462
463
  assert ys.get(2)! == 4
463
- assert {ys.get(3) catch -1} == -1
464
+ assert ys.get(3).unwrapOr(-1) == -1
464
465
  _ := ys.set(0, 20)!
465
466
  assert xs.get(1)! == 20
466
467
  _ := xs.set(3, 40)!
plum-std/Testing.plum CHANGED
@@ -5,6 +5,8 @@ import std/Int
5
5
  import std/Float
6
6
  import std/Str
7
7
  import std/List
8
+ import std/Option
9
+ import std/Result
8
10
 
9
11
  # Small jest/rspec-style assertion helpers for use inside `test` blocks, e.g.
10
12
  #
@@ -18,9 +20,10 @@ import std/List
18
20
  # assertion instead of a raw comparison.
19
21
  #
20
22
  # `expectArray`/`expectSuccess`/`expectFailure` below take a container
21
- # type (`Slice[T]`/`!T`) whose OWN generic param is
23
+ # type (`Slice[T]`/`Result[T, E]`) whose OWN generic param is
22
- # nested inside the param type. An error union is inspected with
24
+ # nested inside the param type. A Result is inspected with
23
- # `catch` (`r catch 0`, `r!`); `expectFailureNamed` also checks which error it is.
25
+ # `unwrapOr` (`r.unwrapOr(0)`, `r!`); `expectFailureNamed` also checks which error it is
26
+ # (the error enum provides `fun name(self) -> Str`).
24
27
  #
25
28
  # CAVEAT on `expectEq`/`expectNotEq`/`expectObject`/`Slice.equals`: Plum's
26
29
  # `==`/`!=` have no operator-overloading mechanism. For Int/Str/Bool/Float
@@ -55,21 +58,27 @@ primitive Testing =
55
58
  fun expectArray(a: Slice[T], b: Slice[T]) -> Bool =
56
59
  a.equals(b)
57
60
 
58
- fun expectSuccess(r: !T) -> Bool =
61
+ fun expectSuccess(r: Result[T, E]) -> Bool = match r
59
- r catch e
62
+ Ok(_) => true
60
- return false
63
+ Err(_) => false
61
- true
62
64
 
63
- fun expectFailure(r: !T) -> Bool =
65
+ fun expectFailure(r: Result[T, E]) -> Bool = match r
64
- r catch e
66
+ Ok(_) => false
65
- return true
67
+ Err(_) => true
66
- false
67
68
 
68
69
  # True when `r` failed with the error named `name`.
69
- fun expectFailureNamed(r: !T, name: Str) -> Bool =
70
+ fun expectFailureNamed(r: Result[T, E], name: Str) -> Bool = match r
70
- r catch e
71
+ Ok(_) => false
71
- return e.name() == name
72
+ Err(e) => e.name() == name
73
+
74
+ # The error enum the tests below use.
75
+ enum Oops =
76
+ | Bad
72
- false
77
+ | Worse
78
+
79
+ fun name(self) -> Str = match self
80
+ Bad => "Bad"
81
+ Worse => "Worse"
73
82
 
74
83
  test "expectTrue and expectFalse"
75
84
  assert Testing.expectTrue(1 == 1)
@@ -87,11 +96,11 @@ test "list equality via the Slice.equals method"
87
96
  assert a.equals(b)
88
97
  assert Testing.expectFalse(a.equals(c))
89
98
 
90
- test "error unions are inspected with catch and !"
99
+ test "Results are inspected with unwrapOr and !"
91
- ok: error{Bad}!Int := 5
100
+ ok: Result[Int, Oops] := Ok(5)
92
- bad: error{Bad}!Int := error.Bad
101
+ bad: Result[Int, Oops] := Err(Bad)
93
102
  assert ok! == 5
94
- assert {bad catch -1} == -1
103
+ assert bad.unwrapOr(-1) == -1
95
104
 
96
105
  test "expectArray compares two Slice[T]s elementwise"
97
106
  a := [1, 2, 3].toSlice()
@@ -104,8 +113,8 @@ test "expectArray compares two Slice[T]s elementwise"
104
113
  assert Testing.expectArray(d, e)
105
114
 
106
115
  test "expectSuccess/expectFailure check the right variant"
107
- ok: error{Bad}!Int := 5
116
+ ok: Result[Int, Oops] := Ok(5)
108
- bad: error{Bad}!Int := error.Bad
117
+ bad: Result[Int, Oops] := Err(Bad)
109
118
  assert Testing.expectSuccess(ok)
110
119
  assert Testing.expectFalse(Testing.expectSuccess(bad))
111
120
  assert Testing.expectFailure(bad)
plum-std/Uuid.plum CHANGED
@@ -4,9 +4,14 @@ import std/Int
4
4
  import std/Float
5
5
  import std/Str
6
6
  import std/Random
7
+ import std/Option
8
+ import std/Result
7
9
 
8
10
  # A UUID, stored as its canonical 36-character
9
11
  # `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` text form.
12
+ # Why `Uuid.fromStr` failed.
13
+ enum UuidError = InvalidUuid
14
+
10
15
  struct Uuid(value: Str) =
11
16
 
12
17
  fun toStr(self) -> Str =
@@ -44,26 +49,25 @@ struct Uuid(value: Str) =
44
49
  return Uuid(value: Uuid.Nil)
45
50
 
46
51
  # Parses the canonical `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` text form
47
- # (case-insensitive), failing with `InvalidUuid` if `s` isn't exactly that shape.
52
+ # (case-insensitive), failing with `Err(InvalidUuid)` if `s` isn't exactly that shape.
48
- fun fromStr(s: Str) -> error{InvalidUuid}!Uuid =
53
+ fun fromStr(s: Str) -> Result[Uuid, UuidError] =
49
54
  if s.length() != 36
50
- return error.InvalidUuid
55
+ return Err(InvalidUuid)
51
56
  i := 0
52
57
  while i < 36
53
58
  if i == 8 || i == 13 || i == 18 || i == 23
54
59
  # '-'
55
60
  if s.byteAt(i) != 45
56
- return error.InvalidUuid
61
+ return Err(InvalidUuid)
57
62
  else
58
63
  if hexValue(s.byteAt(i)) < 0
59
- return error.InvalidUuid
64
+ return Err(InvalidUuid)
60
65
  i = i + 1
61
- return Uuid(value: s.toLower())
66
+ return Ok(Uuid(value: s.toLower()))
62
67
 
63
- fun isValid(s: Str) -> Bool =
68
+ fun isValid(s: Str) -> Bool = match fromStr(s)
64
- fromStr(s) catch e
69
+ Ok(_) => true
65
- return false
70
+ Err(_) => false
66
- return true
67
71
 
68
72
  # A random (version 4, RFC 4122) UUID drawn from `rng`, and the advanced generator.
69
73
  fun v4(rng: Random) -> Rolled[Uuid] =
plum-std/game/Affine.plum CHANGED
@@ -3,6 +3,7 @@ module std
3
3
  import std/Float
4
4
  import std/Math
5
5
  import std/game/Vec2
6
+ import std/Option
6
7
 
7
8
  # A 2D affine transform: x' = a*x + c*y + e, y' = b*x + d*y + f.
8
9
  struct Affine(a: Float, b: Float, c: Float, d: Float, e: Float, f: Float) =
@@ -36,19 +37,19 @@ struct Affine(a: Float, b: Float, c: Float, d: Float, e: Float, f: Float) =
36
37
  e: 0.0 - {ia * self.e + ic * self.f},
37
38
  f: 0.0 - {ib * self.e + id * self.f})
38
39
 
39
- # The transform that undoes this one, or null if it is singular.
40
+ # The transform that undoes this one, or None if it is singular.
40
- fun invert(self) -> Affine? =
41
+ fun invert(self) -> Option[Affine] =
41
42
  det := self.a * self.d - self.b * self.c
42
43
  if det == 0.0 || {det * 0.0} != 0.0
43
- return null
44
+ return None
44
45
  ia := self.d / det
45
46
  ib := 0.0 - self.b / det
46
47
  ic := 0.0 - self.c / det
47
48
  id := self.a / det
48
- Affine(
49
+ Some(Affine(
49
50
  a: ia, b: ib, c: ic, d: id,
50
51
  e: 0.0 - {ia * self.e + ic * self.f},
51
- f: 0.0 - {ib * self.e + id * self.f})
52
+ f: 0.0 - {ib * self.e + id * self.f}))
52
53
 
53
54
  fun scaleX(self) -> Float =
54
55
  Math.hypot(self.a, self.b)
@@ -59,13 +60,13 @@ struct Affine(a: Float, b: Float, c: Float, d: Float, e: Float, f: Float) =
59
60
  fun rotation(self) -> Float =
60
61
  Math.atan2(self.b, self.a)
61
62
 
62
- test "invert_singular_is_null"
63
+ test "invert_singular_is_none"
63
64
  s := Affine(a: 0.0, b: 0.0, c: 0.0, d: 0.0, e: 1.0, f: 1.0)
64
- assert s.invert() == null
65
+ assert s.invert().isNone()
65
66
 
66
67
  test "invert_round_trip"
67
68
  m := Affine(a: 2.0, b: 0.0, c: 0.0, d: 2.0, e: 4.0, f: 0.0)
68
- if inv := m.invert()
69
+ if Some(inv) := m.invert()
69
70
  p := inv.apply(Vec2(x: 6.0, y: 2.0))
70
71
  assert {p.x - 1.0}.abs() < 1e-6
71
72
  assert {p.y - 1.0}.abs() < 1e-6
plum-std/game/Input.plum CHANGED
@@ -7,6 +7,7 @@ import std/game/Vec2
7
7
  import std/game/Affine
8
8
  import std/game/Game
9
9
  import std/Io
10
+ import std/Option
10
11
  import std/List
11
12
 
12
13
  # Player input shared by every scene style. `Input.poll` is the only code that reads
@@ -64,12 +65,12 @@ enum Input =
64
65
  out.push(Key.LeftControl)
65
66
  out
66
67
 
67
- # The pointer position, or null for a key.
68
+ # The pointer position, or None for a key.
68
- fun position(self) -> Vec2? = match self
69
+ fun position(self) -> Option[Vec2] = match self
69
- PointerDown(x, y) => Vec2(x: x, y: y)
70
+ PointerDown(x, y) => Some(Vec2(x: x, y: y))
70
- PointerUp(x, y) => Vec2(x: x, y: y)
71
+ PointerUp(x, y) => Some(Vec2(x: x, y: y))
71
- PointerMove(x, y) => Vec2(x: x, y: y)
72
+ PointerMove(x, y) => Some(Vec2(x: x, y: y))
72
- _ => null
73
+ _ => None
73
74
 
74
75
  # The input with its position seen through `m` (a key is unchanged).
75
76
  fun through(self, m: Affine) -> Input = match self
@@ -85,7 +86,7 @@ enum Input =
85
86
  _ => PointerMove(x: p.x, y: p.y)
86
87
 
87
88
  fun isPointer(self) -> Bool =
88
- self.position() != null
89
+ self.position().isSome()
89
90
 
90
91
  # What is held: the left mouse button and the keys that are down.
91
92
  struct Held(mouse: Bool, keys: Slice[Int]) =
@@ -158,11 +159,11 @@ test "key_codes_cover_every_key_constant"
158
159
 
159
160
  test "through_moves_pointers_only"
160
161
  m := Affine(a: 1.0, b: 0.0, c: 0.0, d: 1.0, e: 10.0, f: 0.0)
161
- if p := PointerDown(x: 1.0, y: 2.0).through(m).position()
162
+ if Some(p) := PointerDown(x: 1.0, y: 2.0).through(m).position()
162
163
  assert p.x == 11.0
163
164
  assert p.y == 2.0
164
165
  else
165
166
  assert false
166
- assert KeyDown(key: 5).through(m).position() == null
167
+ assert KeyDown(key: 5).through(m).position().isNone()
167
168
  assert !KeyDown(key: 5).isPointer()
168
169
  assert PointerMove(x: 0.0, y: 0.0).isPointer()
plum-std/game/functional/FunctionalScene.plum CHANGED
@@ -11,6 +11,7 @@ import std/game/Color
11
11
  import std/game/Affine
12
12
  import std/game/Game
13
13
  import std/Io
14
+ import std/Option
14
15
 
15
16
  # The functional scene: a `SceneNode[M]` is plain data (shapes, sprites, text, groups
16
17
  # with a transform, filters, clickable areas), rebuilt every frame and never mutated.
@@ -69,8 +70,8 @@ struct Rect(x: Float, y: Float, w: Float, h: Float) =
69
70
  Rect.through(lx, ly, {x1 - x2}.abs() + thickness, {y1 - y2}.abs() + thickness, m)
70
71
 
71
72
  # The smallest box holding `a` (when there is one) and `b`.
72
- fun union(a: Rect?, b: Rect) -> Rect =
73
+ fun union(a: Option[Rect], b: Rect) -> Rect =
73
- if ra := a
74
+ if Some(ra) := a
74
75
  x0 := ra.x.min(b.x)
75
76
  y0 := ra.y.min(b.y)
76
77
  x1 := {ra.x + ra.w}.max(b.x + b.w)
@@ -103,32 +104,32 @@ enum SceneNode[M] =
103
104
 
104
105
  # The bounding box of the scene in its own space (all zero when empty).
105
106
  fun bounds(self) -> Rect =
106
- if r := self.boundsWith(Affine.Identity)
107
+ if Some(r) := self.boundsWith(Affine.Identity)
107
108
  return r
108
109
  Rect(x: 0.0, y: 0.0, w: 0.0, h: 0.0)
109
110
 
110
111
  # Child indices from the root down to the topmost hit leaf; empty on a miss.
111
112
  fun hitPath(self, x: Float, y: Float) -> Slice[Int] =
112
- if p := self.hitWith(x, y)
113
+ if Some(p) := self.hitWith(x, y)
113
114
  return p
114
115
  Slice[Int]()
115
116
 
116
- # The id of the innermost Clickable node under (x, y), or null. Later siblings win.
117
+ # The id of the innermost Clickable node under (x, y), or None. Later siblings win.
117
- fun hitId(self, x: Float, y: Float) -> Int? =
118
+ fun hitId(self, x: Float, y: Float) -> Option[Int] =
118
- self.hitIdWith(null, x, y)
119
+ self.hitIdWith(None, x, y)
119
120
 
120
- # The message of the first Clickable with `id`, depth first, or null.
121
+ # The message of the first Clickable with `id`, depth first, or None.
121
- fun msgOf(self, id: Int) -> M? = match self
122
+ fun msgOf(self, id: Int) -> Option[M] = match self
122
- Clickable(cid, msg, child) => if cid == id then msg else child.msgOf(id)
123
+ Clickable(cid, msg, child) => if cid == id then Some(msg) else child.msgOf(id)
123
124
  Group(_, kids) => SceneNode.msgOfKids(kids, id)
124
125
  Filter(_, child, _) => child.msgOf(id)
125
- _ => null
126
+ _ => None
126
127
 
127
- fun msgOfKids(kids: List[SceneNode[M]], id: Int) -> M? = match kids
128
+ fun msgOfKids(kids: List[SceneNode[M]], id: Int) -> Option[M] = match kids
128
- Nil => null
129
+ Nil => None
129
130
  Cons(head, tail) =>
130
- if m := head.msgOf(id)
131
+ if Some(m) := head.msgOf(id)
131
- m
132
+ Some(m)
132
133
  else
133
134
  SceneNode.msgOfKids(tail, id)
134
135
 
@@ -172,7 +173,7 @@ enum SceneNode[M] =
172
173
  child.drawWith(io, m, alpha, target)
173
174
  io.blendMode(Blend.Normal)
174
175
  _ =>
175
- if b := child.boundsWith(m)
176
+ if Some(b) := child.boundsWith(m)
176
177
  if tex >= 0
177
178
  w := {Int(b.w) + 1}.min(io.textureWidth(tex))
178
179
  h := {Int(b.h) + 1}.min(io.textureHeight(tex))
@@ -185,98 +186,98 @@ enum SceneNode[M] =
185
186
 
186
187
  # --- bounds ---
187
188
 
188
- # The bounds of the node drawn under `m`, or null when it has no extent.
189
+ # The bounds of the node drawn under `m`, or None when it has no extent.
189
- fun boundsWith(self, m: Affine) -> Rect? = match self
190
+ fun boundsWith(self, m: Affine) -> Option[Rect] = match self
190
- RectShape(x, y, w, h, _) => Rect.through(x, y, w, h, m)
191
+ RectShape(x, y, w, h, _) => Some(Rect.through(x, y, w, h, m))
191
- CircleShape(cx, cy, r, _) => Rect.through(cx - r, cy - r, r * 2.0, r * 2.0, m)
192
+ CircleShape(cx, cy, r, _) => Some(Rect.through(cx - r, cy - r, r * 2.0, r * 2.0, m))
192
- LineShape(x1, y1, x2, y2, th, _) => Rect.throughLine(x1, y1, x2, y2, th, m)
193
+ LineShape(x1, y1, x2, y2, th, _) => Some(Rect.throughLine(x1, y1, x2, y2, th, m))
193
- Sprite(_, sx, sy, sw, sh, _, _, _, _, _) => Rect.through(sx, sy, sw, sh, m)
194
+ Sprite(_, sx, sy, sw, sh, _, _, _, _, _) => Some(Rect.through(sx, sy, sw, sh, m))
194
- Text(txt, tx, ty, size, _) => Rect.through(tx, ty, size * 0.6 * Float(txt.length()), size, m)
195
+ Text(txt, tx, ty, size, _) => Some(Rect.through(tx, ty, size * 0.6 * Float(txt.length()), size, m))
195
- Group(tr, kids) => SceneNode.boundsKids(kids, m.mul(tr.matrix()), null)
196
+ Group(tr, kids) => SceneNode.boundsKids(kids, m.mul(tr.matrix()), None)
196
197
  Filter(_, child, _) => child.boundsWith(m)
197
198
  Clickable(_, _, child) => child.boundsWith(m)
198
199
 
199
- fun boundsKids(kids: List[SceneNode[M]], m: Affine, acc: Rect?) -> Rect? = match kids
200
+ fun boundsKids(kids: List[SceneNode[M]], m: Affine, acc: Option[Rect]) -> Option[Rect] = match kids
200
201
  Nil => acc
201
202
  Cons(head, tail) =>
202
- if cr := head.boundsWith(m)
203
+ if Some(cr) := head.boundsWith(m)
203
- SceneNode.boundsKids(tail, m, Rect.union(acc, cr))
204
+ SceneNode.boundsKids(tail, m, Some(Rect.union(acc, cr)))
204
205
  else
205
206
  SceneNode.boundsKids(tail, m, acc)
206
207
 
207
208
  # --- hit testing ---
208
209
 
209
- # The child indices from the node down to the hit leaf, or null on a miss. A leaf
210
+ # The child indices from the node down to the hit leaf, or None on a miss. A leaf
210
211
  # that is hit is an empty path. (px, py) is in the node's own space.
211
- fun hitWith(self, px: Float, py: Float) -> Slice[Int]? = match self
212
+ fun hitWith(self, px: Float, py: Float) -> Option[Slice[Int]] = match self
212
213
  RectShape(x, y, w, h, _) => hitLeaf(Rect(x: x, y: y, w: w, h: h).contains(px, py))
213
214
  CircleShape(cx, cy, r, _) => hitLeaf(Math.hypot(px - cx, py - cy) <= r)
214
215
  LineShape(x1, y1, x2, y2, th, _) => hitLeaf(nearLine(x1, y1, x2, y2, th, px, py))
215
216
  Sprite(_, sx, sy, sw, sh, _, _, _, _, _) => hitLeaf(Rect(x: sx, y: sy, w: sw, h: sh).contains(px, py))
216
217
  Text(txt, tx, ty, size, _) => hitLeaf(Rect(x: tx, y: ty, w: size * 0.6 * Float(txt.length()), h: size).contains(px, py))
217
218
  Group(tr, kids) =>
218
- if gi := tr.matrix().invert()
219
+ if Some(gi) := tr.matrix().invert()
219
220
  lp := gi.apply(Vec2(x: px, y: py))
220
221
  SceneNode.hitKids(kids, 0, lp.x, lp.y)
221
222
  else
222
- null
223
+ None
223
224
  Filter(_, child, _) =>
224
- if fp := child.hitWith(px, py)
225
+ if Some(fp) := child.hitWith(px, py)
225
- hitPrepend(0, fp)
226
+ Some(hitPrepend(0, fp))
226
227
  else
227
- null
228
+ None
228
229
  Clickable(_, _, child) =>
229
- if tp := child.hitWith(px, py)
230
+ if Some(tp) := child.hitWith(px, py)
230
- hitPrepend(0, tp)
231
+ Some(hitPrepend(0, tp))
231
232
  else
232
- null
233
+ None
233
234
 
234
235
  # Tries the later children first (they are drawn on top); `i` is the index of
235
236
  # `kids`' head. The first hit found from the back wins.
236
- fun hitKids(kids: List[SceneNode[M]], i: Int, px: Float, py: Float) -> Slice[Int]? = match kids
237
+ fun hitKids(kids: List[SceneNode[M]], i: Int, px: Float, py: Float) -> Option[Slice[Int]] = match kids
237
- Nil => null
238
+ Nil => None
238
239
  Cons(head, tail) =>
239
- if later := SceneNode.hitKids(tail, i + 1, px, py)
240
+ if Some(later) := SceneNode.hitKids(tail, i + 1, px, py)
240
- later
241
+ Some(later)
241
242
  else
242
- if sub := head.hitWith(px, py)
243
+ if Some(sub) := head.hitWith(px, py)
243
- hitPrepend(i, sub)
244
+ Some(hitPrepend(i, sub))
244
245
  else
245
- null
246
+ None
246
247
 
247
- # The id of the innermost Clickable node whose subtree is hit at (px, py), or null.
248
+ # The id of the innermost Clickable node whose subtree is hit at (px, py), or None.
248
- # `outer` is the id of the nearest enclosing tag (null at the root).
249
+ # `outer` is the id of the nearest enclosing tag (None at the root).
249
- fun hitIdWith(self, outer: Int?, px: Float, py: Float) -> Int? = match self
250
+ fun hitIdWith(self, outer: Option[Int], px: Float, py: Float) -> Option[Int] = match self
250
251
  Group(tr, kids) =>
251
- if gi := tr.matrix().invert()
252
+ if Some(gi) := tr.matrix().invert()
252
253
  lp := gi.apply(Vec2(x: px, y: py))
253
254
  SceneNode.hitIdKids(kids, outer, lp.x, lp.y)
254
255
  else
255
- null
256
+ None
256
257
  Filter(_, child, _) => child.hitIdWith(outer, px, py)
257
- Clickable(id, _, child) => child.hitIdWith(id, px, py)
258
+ Clickable(id, _, child) => child.hitIdWith(Some(id), px, py)
258
259
  _ =>
259
- if self.hitWith(px, py) != null
260
+ if self.hitWith(px, py).isSome()
260
261
  outer
261
262
  else
262
- null
263
+ None
263
264
 
264
265
  # Later siblings are drawn on top, so they are tried first.
265
- fun hitIdKids(kids: List[SceneNode[M]], outer: Int?, px: Float, py: Float) -> Int? = match kids
266
+ fun hitIdKids(kids: List[SceneNode[M]], outer: Option[Int], px: Float, py: Float) -> Option[Int] = match kids
266
- Nil => null
267
+ Nil => None
267
268
  Cons(head, tail) =>
268
- if later := SceneNode.hitIdKids(tail, outer, px, py)
269
+ if Some(later) := SceneNode.hitIdKids(tail, outer, px, py)
269
- later
270
+ Some(later)
270
271
  else
271
272
  head.hitIdWith(outer, px, py)
272
273
 
273
274
  # Hit-test helpers that do not mention `M`. They are not static methods of
274
275
  # `SceneNode[M]` because a call like `SceneNode.leaf(..)` could not tell which `M`.
275
276
 
276
- fun hitLeaf(hit: Bool) -> Slice[Int]? =
277
+ fun hitLeaf(hit: Bool) -> Option[Slice[Int]] =
277
278
  if hit
278
- return Slice[Int]()
279
+ return Some(Slice[Int]())
279
- null
280
+ None
280
281
 
281
282
  # `i` followed by the path `sub`.
282
283
  fun hitPrepend(i: Int, sub: Slice[Int]) -> Slice[Int] =
@@ -362,23 +363,23 @@ test "hit_id_innermost_and_topmost"
362
363
  top := Clickable(id: 3, msg: 3 * 10, child: picRect(15.0, 15.0, 10.0, 10.0, Color.Red))
363
364
  g := Group(transform: Translate(dx: 1.0, dy: 0.0), children: Cons(head: outer, tail: Cons(head: top, tail: Nil)))
364
365
  a := g.hitId(6.0, 5.0)
365
- if va := a
366
+ if Some(va) := a
366
367
  assert va == 2
367
368
  else
368
369
  assert false
369
370
  b := g.hitId(19.0, 5.0)
370
- if vb := b
371
+ if Some(vb) := b
371
372
  assert vb == 1
372
373
  else
373
374
  assert false
374
375
  c := g.hitId(17.0, 17.0)
375
- if vc := c
376
+ if Some(vc) := c
376
377
  assert vc == 3
377
378
  else
378
379
  assert false
379
- assert g.hitId(100.0, 100.0) == null
380
+ assert g.hitId(100.0, 100.0).isNone()
380
381
 
381
382
  test "msg_of_finds_the_clickable_message"
382
383
  g := Group(transform: Identity, children: [picRect(0.0, 0.0, 1.0, 1.0, Color.Red), Clickable(id: 4, msg: 40, child: picRect(0.0, 0.0, 1.0, 1.0, Color.Red))])
383
- assert {g.msgOf(4) ?: -1} == 40
384
+ assert g.msgOf(4).unwrapOr(-1) == 40
384
- assert g.msgOf(5) == null
385
+ assert g.msgOf(5).isNone()
plum-std/game/functional/Ui.plum CHANGED
@@ -5,6 +5,7 @@ import std/Float
5
5
  import std/Str
6
6
  import std/Slice
7
7
  import std/List
8
+ import std/Option
8
9
  import std/game/Color
9
10
  import std/game/functional/FunctionalScene
10
11
  import std/game/Input
@@ -17,53 +18,56 @@ import std/game/Input
17
18
  # button rules as PureScene: Down captures the pressed id; while captured only
18
19
  # that id gets Enter/Exit; Up releases (Click only when still over it).
19
20
 
20
- struct UiState(x: Float, y: Float, down: Bool, hot: Int?, captured: Int?)
21
+ struct UiState(x: Float, y: Float, down: Bool, hot: Option[Int], captured: Option[Int])
21
22
 
22
23
  struct UiStep(ui: UiState, events: List[UiEvent])
23
24
 
24
- fun uiSame(a: Int?, b: Int?) -> Bool =
25
+ fun uiSame(a: Option[Int], b: Option[Int]) -> Bool =
25
- if va := a
26
+ if Some(va) := a
26
- if vb := b
27
+ if Some(vb) := b
27
28
  return va == vb
28
29
  return false
29
- if vb := b
30
+ if Some(vb) := b
30
31
  return false
31
32
  true
32
33
 
33
34
  # Exit for the old hot id and Enter for the new one when they differ.
34
- fun uiHover(out: Slice[UiEvent], old: Int?, now: Int?) =
35
+ fun uiHover(out: Slice[UiEvent], old: Option[Int], now: Option[Int]) =
35
36
  if !uiSame(old, now)
36
- if o := old
37
+ if Some(o) := old
37
38
  out.push(UiEvent(id: o, kind: Exit))
38
- if n := now
39
+ if Some(n) := now
39
40
  out.push(UiEvent(id: n, kind: Enter))
40
41
 
41
42
  primitive Ui =
42
43
  fun init() -> UiState =
43
- UiState(x: 0.0, y: 0.0, down: false, hot: null, captured: null)
44
+ UiState(x: 0.0, y: 0.0, down: false, hot: None, captured: None)
44
45
 
45
46
  # A key changes nothing here (keys go to the game).
46
- fun step(ui: UiState, input: Input, hit: Int?) -> UiStep =
47
+ fun step(ui: UiState, input: Input, hit: Option[Int]) -> UiStep =
48
+ maybePos := input.position()
49
+ if maybePos.isNone()
47
- pos := input.position() ?: return UiStep(ui: ui, events: Nil)
50
+ return UiStep(ui: ui, events: Nil)
51
+ pos := maybePos!
48
52
  out := Slice[UiEvent]()
49
53
  # a capture left over without a held button is stale
50
- cap: Int? := ui.captured
54
+ cap: Option[Int] := ui.captured
51
55
  if !ui.down
52
- cap = null
56
+ cap = None
53
- hot: Int? := ui.hot
57
+ hot: Option[Int] := ui.hot
54
58
  down := ui.down
55
59
  match input
56
60
  PointerDown(_, _) =>
57
- cap = null
61
+ cap = None
58
62
  uiHover(out, hot, hit)
59
63
  hot = hit
60
64
  down = true
61
- if h := hit
65
+ if Some(h) := hit
62
66
  out.push(UiEvent(id: h, kind: Press))
63
- cap = h
67
+ cap = Some(h)
64
68
  PointerMove(_, _) =>
65
- if c := cap
69
+ if Some(c) := cap
66
- now: Int? := null
70
+ now: Option[Int] := None
67
71
  if uiSame(hit, cap)
68
72
  now = cap
69
73
  uiHover(out, hot, now)
@@ -72,11 +76,11 @@ primitive Ui =
72
76
  uiHover(out, hot, hit)
73
77
  hot = hit
74
78
  PointerUp(_, _) =>
75
- if c := cap
79
+ if Some(c) := cap
76
80
  out.push(UiEvent(id: c, kind: Release))
77
81
  if uiSame(hit, cap)
78
82
  out.push(UiEvent(id: c, kind: Click))
79
- cap = null
83
+ cap = None
80
84
  hot = hit
81
85
  else
82
86
  uiHover(out, hot, hit)
@@ -86,12 +90,12 @@ primitive Ui =
86
90
  UiStep(ui: UiState(x: pos.x, y: pos.y, down: down, hot: hot, captured: cap), events: List.fromSlice(out))
87
91
 
88
92
  fun pressed(ui: UiState, id: Int) -> Bool =
89
- if c := ui.captured
93
+ if Some(c) := ui.captured
90
94
  return c == id && uiSame(ui.hot, ui.captured)
91
95
  false
92
96
 
93
97
  fun hovered(ui: UiState, id: Int) -> Bool =
94
- if h := ui.hot
98
+ if Some(h) := ui.hot
95
99
  return h == id
96
100
  false
97
101
 
@@ -103,7 +107,7 @@ primitive Ui =
103
107
  later := Ui.messages(view, rest)
104
108
  if e.kind != Click
105
109
  return later
106
- if m := view.msgOf(e.id)
110
+ if Some(m) := view.msgOf(e.id)
107
111
  return Cons(head: m, tail: later)
108
112
  later
109
113
 
@@ -148,12 +152,12 @@ fun uiButtonColor(n: SceneNode[Int]) -> Color = match n
148
152
  _ => Color.Black
149
153
 
150
154
  test "press_sets_capture"
151
- s1 := Ui.step(Ui.init(), uiDown(1.0), 5)
155
+ s1 := Ui.step(Ui.init(), uiDown(1.0), Some(5))
152
156
  assert s1.events.length() == 2
153
157
  assert uiIs(s1.events.at(0), 5, Enter)
154
158
  assert uiIs(s1.events.at(1), 5, Press)
155
159
  assert Ui.pressed(s1.ui, 5)
156
- s2 := Ui.step(s1.ui, uiUp(1.0), 5)
160
+ s2 := Ui.step(s1.ui, uiUp(1.0), Some(5))
157
161
  assert s2.events.length() == 2
158
162
  assert uiIs(s2.events.at(0), 5, Release)
159
163
  assert uiIs(s2.events.at(1), 5, Click)
@@ -161,53 +165,53 @@ test "press_sets_capture"
161
165
  assert Ui.hovered(s2.ui, 5)
162
166
 
163
167
  test "release_elsewhere_no_click"
164
- s1 := Ui.step(Ui.init(), uiDown(1.0), 5)
168
+ s1 := Ui.step(Ui.init(), uiDown(1.0), Some(5))
165
- s2 := Ui.step(s1.ui, uiMove(99.0), null)
169
+ s2 := Ui.step(s1.ui, uiMove(99.0), None)
166
170
  assert s2.events.length() == 1
167
171
  assert uiIs(s2.events.at(0), 5, Exit)
168
- s3 := Ui.step(s2.ui, uiUp(99.0), null)
172
+ s3 := Ui.step(s2.ui, uiUp(99.0), None)
169
173
  assert s3.events.length() == 1
170
174
  assert uiIs(s3.events.at(0), 5, Release)
171
175
  assert uiCount(s3, Click) == 0
172
176
 
173
177
  test "drag_off_and_back_one_click"
174
- s1 := Ui.step(Ui.init(), uiDown(1.0), 5)
178
+ s1 := Ui.step(Ui.init(), uiDown(1.0), Some(5))
175
- s2 := Ui.step(s1.ui, uiMove(99.0), null)
179
+ s2 := Ui.step(s1.ui, uiMove(99.0), None)
176
- s3 := Ui.step(s2.ui, uiMove(1.0), 5)
180
+ s3 := Ui.step(s2.ui, uiMove(1.0), Some(5))
177
181
  assert s3.events.length() == 1
178
182
  assert uiIs(s3.events.at(0), 5, Enter)
179
- s4 := Ui.step(s3.ui, uiUp(1.0), 5)
183
+ s4 := Ui.step(s3.ui, uiUp(1.0), Some(5))
180
184
  assert uiCount(s4, Click) == 1
181
185
  assert uiCount(s4, Release) == 1
182
186
 
183
187
  test "stale_capture_dropped"
184
- stale := UiState(x: 0.0, y: 0.0, down: false, hot: null, captured: 5)
188
+ stale := UiState(x: 0.0, y: 0.0, down: false, hot: None, captured: Some(5))
185
- s := Ui.step(stale, uiMove(50.0), null)
189
+ s := Ui.step(stale, uiMove(50.0), None)
186
190
  assert s.events.length() == 0
187
- if c := s.ui.captured
191
+ if Some(c) := s.ui.captured
188
192
  assert false
189
193
  assert !Ui.pressed(s.ui, 5)
190
194
 
191
195
  test "hover_without_press_routes_move"
192
- s1 := Ui.step(Ui.init(), uiMove(1.0), 5)
196
+ s1 := Ui.step(Ui.init(), uiMove(1.0), Some(5))
193
197
  assert s1.events.length() == 1
194
198
  assert uiIs(s1.events.at(0), 5, Enter)
195
199
  assert Ui.hovered(s1.ui, 5)
196
- s2 := Ui.step(s1.ui, uiMove(2.0), 5)
200
+ s2 := Ui.step(s1.ui, uiMove(2.0), Some(5))
197
201
  assert s2.events.length() == 0
198
- s3 := Ui.step(s2.ui, uiMove(99.0), null)
202
+ s3 := Ui.step(s2.ui, uiMove(99.0), None)
199
203
  assert s3.events.length() == 1
200
204
  assert uiIs(s3.events.at(0), 5, Exit)
201
205
  assert !Ui.hovered(s3.ui, 5)
202
206
 
203
207
  test "button_visual_reflects_state"
204
208
  idle := Ui.button(3, 30, "Go", 10.0, 20.0, 100.0, 40.0, Ui.init())
205
- hov := Ui.button(3, 30, "Go", 10.0, 20.0, 100.0, 40.0, Ui.step(Ui.init(), uiMove(1.0), 3).ui)
209
+ hov := Ui.button(3, 30, "Go", 10.0, 20.0, 100.0, 40.0, Ui.step(Ui.init(), uiMove(1.0), Some(3)).ui)
206
- prs := Ui.button(3, 30, "Go", 10.0, 20.0, 100.0, 40.0, Ui.step(Ui.init(), uiDown(1.0), 3).ui)
210
+ prs := Ui.button(3, 30, "Go", 10.0, 20.0, 100.0, 40.0, Ui.step(Ui.init(), uiDown(1.0), Some(3)).ui)
207
211
  assert uiButtonColor(idle).eq(uiIdleColor())
208
212
  assert uiButtonColor(hov).eq(uiHoverColor())
209
213
  assert uiButtonColor(prs).eq(uiPressedColor())
210
- if hid := idle.hitId(20.0, 30.0)
214
+ if Some(hid) := idle.hitId(20.0, 30.0)
211
215
  assert hid == 3
212
216
  else
213
217
  assert false
@@ -217,5 +221,5 @@ test "messages_are_the_clicked_buttons_messages"
217
221
  evs := [UiEvent(id: 4, kind: Press), UiEvent(id: 4, kind: Click), UiEvent(id: 9, kind: Click), UiEvent(id: 3, kind: Click)]
218
222
  ms := Ui.messages(view, evs)
219
223
  assert ms.length() == 2
220
- assert {ms.get(0) ?: -1} == 40
224
+ assert ms.get(0).unwrapOr(-1) == 40
221
- assert {ms.get(1) ?: -1} == 30
225
+ assert ms.get(1).unwrapOr(-1) == 30
plum-std/game/imperative/Scene.plum CHANGED
@@ -4,6 +4,7 @@ import std/Int
4
4
  import std/Float
5
5
  import std/Str
6
6
  import std/Slice
7
+ import std/Option
7
8
  import std/Math
8
9
  import std/game/Vec2
9
10
  import std/game/Color
@@ -57,7 +58,7 @@ struct Actor(
57
58
  mut color: Color,
58
59
  mut visible: Bool,
59
60
  mut children: Slice[Actor],
60
- mut parent: Actor?,
61
+ mut parent: Option[Actor],
61
62
  mut scratch: Slice[Actor],
62
63
  mut cull: Bool, mut cullX: Float, mut cullY: Float, mut cullW: Float, mut cullH: Float) =
63
64
 
@@ -76,7 +77,7 @@ struct Actor(
76
77
  self.color = Color.White
77
78
  self.visible = true
78
79
  self.children = Slice[Actor]()
79
- self.parent = null
80
+ self.parent = None
80
81
  self.scratch = Slice[Actor]()
81
82
  self.cull = false
82
83
  self.cullX = 0.0
@@ -134,9 +135,9 @@ struct Actor(
134
135
 
135
136
  # Appends a child (taking it from its previous parent) so it draws after its siblings.
136
137
  fun add(self, child: Actor) =
137
- if p := child.parent
138
+ if Some(p) := child.parent
138
139
  p.detach(child)
139
- child.parent = self
140
+ child.parent = Some(self)
140
141
  self.children.push(child)
141
142
 
142
143
  fun detach(self, child: Actor) =
@@ -145,26 +146,26 @@ struct Actor(
145
146
  self.children.removeAt(i)
146
147
 
147
148
  fun remove(self, child: Actor) =
148
- if p := child.parent
149
+ if Some(p) := child.parent
149
150
  if p == self
150
151
  self.detach(child)
151
- child.parent = null
152
+ child.parent = None
152
153
 
153
154
  fun clear(self) =
154
155
  for i := range self.children.length()
155
- self.children.at(i).parent = null
156
+ self.children.at(i).parent = None
156
157
  self.children.clear()
157
158
 
158
159
  # The actor's index among its siblings (its draw order), -1 without a parent.
159
160
  fun zIndex(self) -> Int =
160
- if p := self.parent
161
+ if Some(p) := self.parent
161
162
  return p.childIndex(self)
162
163
  -1
163
164
 
164
165
  # Moves the actor to index `i` of its parent's children (clamped to the valid
165
166
  # range), shifting the siblings in between. Without a parent it does nothing.
166
167
  fun setZIndex(self, i: Int) =
167
- if p := self.parent
168
+ if Some(p) := self.parent
168
169
  src := self.zIndex()
169
170
  dst := i
170
171
  if dst < 0
@@ -184,7 +185,7 @@ struct Actor(
184
185
 
185
186
  # Draws last (on top) and is hit first.
186
187
  fun toFront(self) =
187
- if p := self.parent
188
+ if Some(p) := self.parent
188
189
  self.setZIndex(p.children.length() - 1)
189
190
 
190
191
  # Draws first (underneath) and is hit last.
@@ -297,20 +298,20 @@ struct Actor(
297
298
 
298
299
  # The topmost visible actor under `p`, a point in the parent's coordinates:
299
300
  # children before the actor itself, later siblings before earlier ones.
300
- fun hitTree(self, p: Vec2) -> Actor? =
301
+ fun hitTree(self, p: Vec2) -> Option[Actor] =
301
302
  if !self.visible
302
- return null
303
+ return None
303
304
  q := self.local().inverse().apply(p)
304
305
  n := self.children.length()
305
306
  for i := range n
306
307
  child := self.children.at(n - 1 - i)
307
308
  if !self.culls(child)
308
- if found := child.hitTree(q)
309
+ if Some(found) := child.hitTree(q)
309
- return found
310
+ return Some(found)
310
311
  if self.hit(q)
311
312
  me: Actor := self
312
- return me
313
+ return Some(me)
313
- null
314
+ None
314
315
 
315
316
  # A point in this actor's coordinates, in the stage's.
316
317
  fun localToStage(self, p: Vec2) -> Vec2 =
@@ -451,7 +452,7 @@ fun localMatrix(n: Actor) -> Affine =
451
452
 
452
453
  # The actor's transform into stage coordinates.
453
454
  fun worldMatrix(n: Actor) -> Affine =
454
- if p := n.parent
455
+ if Some(p) := n.parent
455
456
  worldMatrix(p) * n.local()
456
457
  else
457
458
  n.local()
@@ -473,18 +474,18 @@ fun actNode(n: Actor, dt: Float) =
473
474
  n.act(dt)
474
475
 
475
476
  # The topmost visible actor under `p`, a point in the actor's parent's coordinates.
476
- fun hitNode(n: Actor, p: Vec2) -> Actor? =
477
+ fun hitNode(n: Actor, p: Vec2) -> Option[Actor] =
477
478
  n.hitTree(p)
478
479
 
479
480
  # A root actor plus a camera (the world point at the screen's top-left and the
480
481
  # pixels per world unit, as in `setCamera`).
481
- struct Stage(mut camX: Float, mut camY: Float, mut camScale: Float, mut focus: Actor?) =
482
+ struct Stage(mut camX: Float, mut camY: Float, mut camScale: Float, mut focus: Option[Actor]) =
482
483
 
483
484
  fun init(self) =
484
485
  self.root: Actor = Group()
485
486
 
486
487
  fun create() -> Stage =
487
- Stage(camX: 0.0, camY: 0.0, camScale: 1.0, focus: null)
488
+ Stage(camX: 0.0, camY: 0.0, camScale: 1.0, focus: None)
488
489
 
489
490
  # Runs every actor's `act`, parents before children, in insertion order.
490
491
  fun act(self, dt: Float) =
@@ -496,7 +497,7 @@ struct Stage(mut camX: Float, mut camY: Float, mut camScale: Float, mut focus: A
496
497
  io.resetCamera()
497
498
 
498
499
  # The topmost visible actor at a stage point.
499
- fun hit(self, x: Float, y: Float) -> Actor? =
500
+ fun hit(self, x: Float, y: Float) -> Option[Actor] =
500
501
  self.root.hitTree(Vec2(x: x, y: y))
501
502
 
502
503
  # Delivers one input and says whether an actor consumed it. A pointer input goes
@@ -504,24 +505,24 @@ struct Stage(mut camX: Float, mut camY: Float, mut camScale: Float, mut focus: A
504
505
  # returns true; a press also gives that actor the keyboard focus. A key goes to the
505
506
  # focused actor, then its parents. Unconsumed input is the game's to handle.
506
507
  fun input(self, e: Input) -> Bool =
507
- if p := e.position()
508
+ if Some(p) := e.position()
508
509
  sp := self.screenToStage(p.x, p.y)
509
510
  target := self.hit(sp.x, sp.y)
510
511
  match e
511
512
  PointerDown(_, _) =>
512
513
  self.focus = target
513
514
  _ => 0
514
- if a := target
515
+ if Some(a) := target
515
516
  return Stage.bubble(a, Input.pointerAt(e, sp))
516
517
  return false
517
- if f := self.focus
518
+ if Some(f) := self.focus
518
519
  return Stage.bubble(f, e)
519
520
  false
520
521
 
521
522
  # Offers `e` (in stage coordinates) to `a`, then each parent, until one consumes it.
522
523
  fun bubble(a: Actor, e: Input) -> Bool =
523
- cur: Actor? := a
524
+ cur: Option[Actor] := Some(a)
524
- while c := cur
525
+ while Some(c) := cur
525
526
  if c.onInput(e.through(worldMatrix(c).inverse()))
526
527
  return true
527
528
  cur = c.parent
@@ -537,7 +538,7 @@ struct Stage(mut camX: Float, mut camY: Float, mut camScale: Float, mut focus: A
537
538
  struct Catcher(Actor(width, height), mut got: Int, mut lastX: Float, eat: Bool) =
538
539
  override fun onInput(self, e: Input) -> Bool =
539
540
  self.got = self.got + 1
540
- if p := e.position()
541
+ if Some(p) := e.position()
541
542
  self.lastX = p.x
542
543
  self.eat
543
544
 
@@ -597,16 +598,16 @@ test "add reparents, remove and clear detach"
597
598
  b.add(k)
598
599
  assert a.children.length() == 0
599
600
  assert b.children.length() == 1
600
- assert same({k.parent ?: crash "no parent"}, b)
601
+ assert same(k.parent.unwrap(), b)
601
602
  b.remove(k)
602
603
  assert b.children.length() == 0
603
- assert k.parent == null
604
+ assert k.parent.isNone()
604
605
  b.add(k)
605
606
  extra := Group()
606
607
  b.add(extra)
607
608
  b.clear()
608
609
  assert b.children.length() == 0
609
- assert k.parent == null
610
+ assert k.parent.isNone()
610
611
 
611
612
  test "draw order is parent first, then children in insertion order"
612
613
  root := Group()
@@ -660,15 +661,15 @@ test "hit finds the topmost actor through rotated and scaled groups"
660
661
  g.add(top)
661
662
  stage := Stage.create()
662
663
  stage.root.add(g)
663
- assert same({stage.hit(90.0, 30.0) ?: crash "miss"}, top)
664
+ assert same(stage.hit(90.0, 30.0).unwrap(), top)
664
- assert same({stage.hit(70.0, 10.0) ?: crash "miss"}, low)
665
+ assert same(stage.hit(70.0, 10.0).unwrap(), low)
665
- assert same({stage.hit(60.0, 30.0) ?: crash "miss"}, low)
666
+ assert same(stage.hit(60.0, 30.0).unwrap(), low)
666
- assert stage.hit(10.0, 10.0) == null
667
+ assert stage.hit(10.0, 10.0).isNone()
667
- assert stage.hit(150.0, 30.0) == null
668
+ assert stage.hit(150.0, 30.0).isNone()
668
669
  top.visible = false
669
- assert same({stage.hit(90.0, 30.0) ?: crash "miss"}, low)
670
+ assert same(stage.hit(90.0, 30.0).unwrap(), low)
670
671
  g.visible = false
671
- assert stage.hit(90.0, 30.0) == null
672
+ assert stage.hit(90.0, 30.0).isNone()
672
673
 
673
674
  test "hit prefers later siblings and a child over its parent"
674
675
  stage := Stage.create()
@@ -678,10 +679,10 @@ test "hit prefers later siblings and a child over its parent"
678
679
  stage.root.add(b)
679
680
  inner := Rect.create(10.0, 10.0, Color.Green)
680
681
  b.add(inner)
681
- assert same({stage.hit(10.0, 10.0) ?: crash "miss"}, a)
682
+ assert same(stage.hit(10.0, 10.0).unwrap(), a)
682
- assert same({stage.hit(30.0, 5.0) ?: crash "miss"}, inner)
683
+ assert same(stage.hit(30.0, 5.0).unwrap(), inner)
683
- assert same({stage.hit(60.0, 40.0) ?: crash "miss"}, b)
684
+ assert same(stage.hit(60.0, 40.0).unwrap(), b)
684
- assert same({stage.hit(40.0, 10.0) ?: crash "miss"}, b)
685
+ assert same(stage.hit(40.0, 10.0).unwrap(), b)
685
686
 
686
687
  test "the default act runs the children's act, invisible ones included"
687
688
  stage := Stage.create()
@@ -748,7 +749,7 @@ test "sortChildren sorts 5000 random actors and updates hit order"
748
749
  assert g.children.length() == 5000
749
750
  for i := range 4999
750
751
  assert g.children.at(i).y <= g.children.at(i + 1).y
751
- assert same({g.children.at(10).parent ?: crash "orphan"}, g)
752
+ assert same(g.children.at(10).parent.unwrap(), g)
752
753
 
753
754
  test "sorting a nearly sorted list takes about n comparisons and reuses the scratch buffer"
754
755
  g := Group()
@@ -804,16 +805,16 @@ test "hit does not return culled children, and a scaled child near the edge is k
804
805
  k := Rect.create(10.0, 10.0, Color.Red).at(105.0, 5.0)
805
806
  stage.root.add(k)
806
807
  stage.root.setCullingArea(0.0, 0.0, 100.0, 100.0)
807
- assert stage.hit(107.0, 7.0) == null
808
+ assert stage.hit(107.0, 7.0).isNone()
808
809
  k.scaleX = 4.0
809
810
  k.originX = 10.0
810
- assert same({stage.hit(90.0, 7.0) ?: crash "miss"}, k)
811
+ assert same(stage.hit(90.0, 7.0).unwrap(), k)
811
812
  assert flatten(stage.root).length() == 2
812
813
  k.scaleX = 1.0
813
814
  k.originX = 0.0
814
- assert stage.hit(107.0, 7.0) == null
815
+ assert stage.hit(107.0, 7.0).isNone()
815
816
  stage.root.clearCullingArea()
816
- assert same({stage.hit(107.0, 7.0) ?: crash "miss"}, k)
817
+ assert same(stage.hit(107.0, 7.0).unwrap(), k)
817
818
 
818
819
  test "a child without a size is never culled"
819
820
  g := Group()
plum-std/game/pure/Actions.plum CHANGED
@@ -4,6 +4,7 @@ import std/Slice
4
4
  import std/Int
5
5
  import std/Float
6
6
  import std/List
7
+ import std/Option
7
8
 
8
9
  # scene2d-style actions as plain data plus a step method. `Action.step`
9
10
  # steps an action IN PLACE on its own record (a running action allocates nothing:
@@ -16,27 +17,27 @@ import std/List
16
17
  # The animated part of an actor's transform.
17
18
  struct Pose(x: Float, y: Float, rotation: Float, scaleX: Float, scaleY: Float)
18
19
 
19
- # One action. `elapsed` and `current` (the running lap of a `Forever`, null
20
+ # One action. `elapsed` and `current` (the running lap of a `Forever`, None
20
21
  # before it starts) are the mutable state; `kind` is the immutable description.
21
22
  # A `Forever`'s `template` is the pristine action restarted each lap.
22
- struct Action(mut elapsed: Float, mut current: Action?, kind: ActionKind) =
23
+ struct Action(mut elapsed: Float, mut current: Option[Action], kind: ActionKind) =
23
24
  fun moveBy(dx: Float, dy: Float, secs: Float) -> Action =
24
- Action(elapsed: 0.0, current: null, kind: MoveBy(dx: dx, dy: dy, duration: secs))
25
+ Action(elapsed: 0.0, current: None, kind: MoveBy(dx: dx, dy: dy, duration: secs))
25
26
 
26
27
  fun rotateBy(radians: Float, secs: Float) -> Action =
27
- Action(elapsed: 0.0, current: null, kind: RotateBy(radians: radians, duration: secs))
28
+ Action(elapsed: 0.0, current: None, kind: RotateBy(radians: radians, duration: secs))
28
29
 
29
30
  fun delay(secs: Float) -> Action =
30
- Action(elapsed: 0.0, current: null, kind: Delay(duration: secs))
31
+ Action(elapsed: 0.0, current: None, kind: Delay(duration: secs))
31
32
 
32
33
  fun sequence(steps: Slice[Action]) -> Action =
33
- Action(elapsed: 0.0, current: null, kind: Sequence(steps: steps))
34
+ Action(elapsed: 0.0, current: None, kind: Sequence(steps: steps))
34
35
 
35
36
  fun parallel(steps: Slice[Action]) -> Action =
36
- Action(elapsed: 0.0, current: null, kind: Parallel(steps: steps))
37
+ Action(elapsed: 0.0, current: None, kind: Parallel(steps: steps))
37
38
 
38
39
  fun forever(a: Action) -> Action =
39
- Action(elapsed: 0.0, current: a, kind: Forever(template: a.fresh()))
40
+ Action(elapsed: 0.0, current: Some(a), kind: Forever(template: a.fresh()))
40
41
 
41
42
  # An unstarted copy (elapsed 0, all steps present).
42
43
  fun fresh(self) -> Action =
@@ -54,7 +55,7 @@ struct Action(mut elapsed: Float, mut current: Action?, kind: ActionKind) =
54
55
  for i := range steps.length()
55
56
  out.push(steps.at(i).fresh())
56
57
  Action.parallel(out)
57
- Forever(template) => Action(elapsed: 0.0, current: template.fresh(), kind: Forever(template: template))
58
+ Forever(template) => Action(elapsed: 0.0, current: Some(template.fresh()), kind: Forever(template: template))
58
59
 
59
60
  # Steps this action by `dt` in place, from pose `p`.
60
61
  fun step(self, p: Pose, dt: Float) -> Step =
@@ -88,10 +89,10 @@ struct Action(mut elapsed: Float, mut current: Action?, kind: ActionKind) =
88
89
  Forever(template) =>
89
90
  # Restarts at most once per step, with the spare time, so a zero-length
90
91
  # template cannot spin forever inside one step.
91
- if a.current == null
92
+ if a.current.isNone()
92
- a.current = template.fresh()
93
+ a.current = Some(template.fresh())
93
94
  lap := template
94
- if c := a.current
95
+ if Some(c) := a.current
95
96
  lap = c
96
97
  s := lap.step(p, dt)
97
98
  if !s.done
@@ -99,9 +100,9 @@ struct Action(mut elapsed: Float, mut current: Action?, kind: ActionKind) =
99
100
  next := template.fresh()
100
101
  r := next.step(s.pose, s.spare)
101
102
  if r.done
102
- a.current = template.fresh()
103
+ a.current = Some(template.fresh())
103
104
  else
104
- a.current = next
105
+ a.current = Some(next)
105
106
  Step(pose: r.pose, spare: 0.0, done: false)
106
107
 
107
108
  # Steps every action side by side in place and drops the finished ones by
plum-std/game/pure/PureScene.plum CHANGED
@@ -4,6 +4,7 @@ import std/Int
4
4
  import std/Float
5
5
  import std/Str
6
6
  import std/Slice
7
+ import std/Option
7
8
  import std/Math
8
9
  import std/game/Vec2
9
10
  import std/game/Color
@@ -101,17 +102,17 @@ struct Node(
101
102
 
102
103
  # --- reading ---
103
104
 
104
- # The children of a group, or null for anything else.
105
+ # The children of a group, or None for anything else.
105
- fun children(self) -> Slice[Node]? =
106
+ fun children(self) -> Option[Slice[Node]] =
106
107
  match self.kind
107
- Group(children) => children
108
+ Group(children) => Some(children)
108
- _ => null
109
+ _ => None
109
110
 
110
- # The actor of a Custom node, or null for anything else.
111
+ # The actor of a Custom node, or None for anything else.
111
- fun actor(self) -> Actor? =
112
+ fun actor(self) -> Option[Actor] =
112
113
  match self.kind
113
- Custom(actor) => actor
114
+ Custom(actor) => Some(actor)
114
- _ => null
115
+ _ => None
115
116
 
116
117
  # Whether `p`, in the node's own coordinates, is on it.
117
118
  fun hit(self, p: Vec2) -> Bool =
@@ -119,28 +120,28 @@ struct Node(
119
120
 
120
121
  # The path (child indexes from this node) to the topmost actor under `p`, which
121
122
  # is in the coordinates of the parent; `parent` is that parent's stage matrix.
122
- fun findHit(self, parent: Affine, p: Vec2) -> Slice[Int]? =
123
+ fun findHit(self, parent: Affine, p: Vec2) -> Option[Slice[Int]] =
123
124
  if !self.visible || self.touchable == Disabled
124
- return null
125
+ return None
125
126
  m := parent * self.localMatrix()
126
127
  inv := m.invert()
127
- if inv == null
128
+ if inv.isNone()
128
- return null
129
+ return None
129
130
  lp := Vec2(x: 0.0, y: 0.0)
130
- if q := inv
131
+ if Some(q) := inv
131
132
  lp = q.apply(p)
132
133
  match self.kind
133
134
  Group(ks) =>
134
135
  i := ks.length() - 1
135
136
  while i >= 0
136
137
  sub := ks.at(i).findHit(m, p)
137
- if s := sub
138
+ if Some(s) := sub
138
- return Node.prepend(i, s)
139
+ return Some(Node.prepend(i, s))
139
140
  i = i - 1
140
141
  _ => 0
141
142
  if self.hit(lp)
142
- return Slice[Int]()
143
+ return Some(Slice[Int]())
143
- null
144
+ None
144
145
 
145
146
  # The id of the actor at `path` below this node, or -1 if the path leads nowhere.
146
147
  fun idAt(self, path: Slice[Int], depth: Int) -> Int =
@@ -154,18 +155,18 @@ struct Node(
154
155
  _ => 0
155
156
  -1
156
157
 
157
- # The path to the first actor with `id`, depth first; null if none.
158
+ # The path to the first actor with `id`, depth first; None if none.
158
- fun findPath(self, id: Int) -> Slice[Int]? =
159
+ fun findPath(self, id: Int) -> Option[Slice[Int]] =
159
160
  if self.id == id
160
- return Slice[Int]()
161
+ return Some(Slice[Int]())
161
162
  match self.kind
162
163
  Group(ks) =>
163
164
  for i := range ks.length()
164
165
  sub := ks.at(i).findPath(id)
165
- if s := sub
166
+ if Some(s) := sub
166
- return Node.prepend(i, s)
167
+ return Some(Node.prepend(i, s))
167
168
  _ => 0
168
- null
169
+ None
169
170
 
170
171
  # `i` followed by the path `rest`.
171
172
  fun prepend(i: Int, rest: Slice[Int]) -> Slice[Int] =
@@ -215,7 +216,7 @@ struct Node(
215
216
 
216
217
  # A Custom node's actor acts on it; every other node is itself.
217
218
  fun acted(self, dt: Float) -> Node =
218
- if actor := self.actor()
219
+ if Some(actor) := self.actor()
219
220
  return actor.act(self, dt)
220
221
  self
221
222
 
@@ -234,7 +235,7 @@ struct Node(
234
235
  # The tree where the first actor with `id` is replaced by `f` of it.
235
236
  fun modify(self, id: Int, f: fn(Node) -> Node) -> Node =
236
237
  path := self.findPath(id)
237
- if p := path
238
+ if Some(p) := path
238
239
  return self.modifyAt(p, 0, f)
239
240
  self
240
241
 
@@ -297,7 +298,7 @@ struct Node(
297
298
  # `route`, appending the raised events to `events` (the returned Handled holds it).
298
299
  fun routeInto(self, path: Slice[Int], depth: Int, e: Input, over: Bool, events: Slice[UiEvent]) -> Handled =
299
300
  local := e
300
- if q := self.localMatrix().invert()
301
+ if Some(q) := self.localMatrix().invert()
301
302
  local = e.through(q)
302
303
  consumed := false
303
304
  if depth < path.length()
@@ -388,11 +389,11 @@ fun ttFixture() -> Node =
388
389
  me: Node := root
389
390
  me
390
391
 
391
- fun ttNoPath(r: Slice[Int]?) -> Bool =
392
+ fun ttNoPath(r: Option[Slice[Int]]) -> Bool =
392
- r == null
393
+ r.isNone()
393
394
 
394
- fun ttPathEq(r: Slice[Int]?, want: Slice[Int]) -> Bool =
395
+ fun ttPathEq(r: Option[Slice[Int]], want: Slice[Int]) -> Bool =
395
- if p := r
396
+ if Some(p) := r
396
397
  if p.length() != want.length()
397
398
  return false
398
399
  for i := range p.length()
@@ -555,8 +556,8 @@ test "hit_box_and_children_of"
555
556
  s := Node.sprite(1, 0, 0.0, 0.0, 10.0, 10.0)
556
557
  assert s.hit(Vec2(x: 5.0, y: 5.0))
557
558
  assert !s.hit(Vec2(x: 11.0, y: 5.0))
558
- assert s.children() == null
559
+ assert s.children().isNone()
559
- assert Node.group(2, 0.0, 0.0, 1.0, 1.0, [s].toSlice()).children() != null
560
+ assert Node.group(2, 0.0, 0.0, 1.0, 1.0, [s].toSlice()).children().isSome()
560
561
 
561
562
  test "button_move_raises_enter_then_exit"
562
563
  b := Node.button(7, "go", 1, 2, 0.0, 0.0, 10.0, 10.0)
plum-std/game/pure/SceneInput.plum CHANGED
@@ -13,6 +13,7 @@ import std/game/pure/Actions
13
13
  import std/game/pure/PureScene
14
14
  import std/game/Input
15
15
  import std/List
16
+ import std/Option
16
17
 
17
18
  # The root of a pure scene tree, its camera, pointer capture and keyboard focus. A
18
19
  # Stage is an unique value: `input` and `frame` take it, change it in place and hand
@@ -43,55 +44,58 @@ struct StageHandled(stage: Stage, events: Slice[UiEvent], keys: Slice[Input]) =
43
44
  match self
44
45
  StageHandled(s, _, _) => s
45
46
 
46
- struct Stage(root: Node, camera: Affine, captured: Capture?, focus: Int?) =
47
+ struct Stage(root: Node, camera: Affine, captured: Option[Capture], focus: Option[Int]) =
47
48
 
48
49
  # A stage over `root` with no camera transform, nothing captured and no focus.
49
50
  fun create(root: Node) -> Stage =
50
- Stage(root: root, camera: Affine.Identity, captured: null, focus: null)
51
+ Stage(root: root, camera: Affine.Identity, captured: None, focus: None)
51
52
 
52
53
  # Handles one input: routes it, returns the new stage and the events raised.
53
54
  fun input(self, e: Input) -> StageHandled =
54
55
  if !e.isPointer()
55
56
  return self.key(e)
56
57
  none := Slice[UiEvent]()
58
+ inv := self.camera.invert()
59
+ if inv.isNone()
57
- q := self.camera.invert() ?: return StageHandled(stage: self, events: none, keys: Slice[Input]())
60
+ return StageHandled(stage: self, events: none, keys: Slice[Input]())
61
+ q := inv.unwrap()
58
62
  world := e.through(q)
59
- at := world.position() ?: Vec2(x: 0.0, y: 0.0)
63
+ at := world.position().unwrapOr(Vec2(x: 0.0, y: 0.0))
60
64
  hit := self.root.findHit(Affine.Identity, at)
61
65
  # A capture is valid only while its path still leads to its actor.
62
- cap: Capture? := null
66
+ cap: Option[Capture] := None
63
- if c := self.captured
67
+ if Some(c) := self.captured
64
68
  if self.root.idAt(c.path, 0) == c.id
65
- cap = c
69
+ cap = Some(c)
66
70
  match world
67
71
  PointerDown(_, _) =>
68
- if p := hit
72
+ if Some(p) := hit
69
73
  id := self.root.idAt(p, 0)
70
- return Stage(..self, focus: id).deliver(p, world, true, Capture(path: p, id: id))
74
+ return Stage(..self, focus: Some(id)).deliver(p, world, true, Some(Capture(path: p, id: id)))
71
- return StageHandled(stage: Stage(..self, captured: null, focus: null), events: none, keys: Slice[Input]())
75
+ return StageHandled(stage: Stage(..self, captured: None, focus: None), events: none, keys: Slice[Input]())
72
76
  _ => 0
73
- if c := cap
77
+ if Some(c) := cap
74
78
  over := false
75
- if p := hit
79
+ if Some(p) := hit
76
80
  over = c.within(p)
77
81
  match world
78
82
  PointerUp(_, _) =>
79
- return self.deliver(c.path, world, over, null)
83
+ return self.deliver(c.path, world, over, None)
80
84
  _ => 0
81
- return self.deliver(c.path, world, over, c)
85
+ return self.deliver(c.path, world, over, Some(c))
82
86
  # Hover without a press: route to whatever is under the pointer.
83
87
  match world
84
88
  PointerMove(_, _) =>
85
- if p := hit
89
+ if Some(p) := hit
86
- return self.deliver(p, world, true, null)
90
+ return self.deliver(p, world, true, None)
87
91
  _ => 0
88
- StageHandled(stage: self.withCapture(null), events: none, keys: Slice[Input]())
92
+ StageHandled(stage: self.withCapture(None), events: none, keys: Slice[Input]())
89
93
 
90
94
  # A key goes to the focused actor (then its ancestors); unconsumed, it is returned.
91
95
  fun key(self, e: Input) -> StageHandled =
92
96
  left := [e].toSlice()
93
- if f := self.focus
97
+ if Some(f) := self.focus
94
- if p := self.root.findPath(f)
98
+ if Some(p) := self.root.findPath(f)
95
99
  events := Slice[UiEvent]()
96
100
  match self
97
101
  Stage(root, camera, captured, focus) =>
@@ -101,10 +105,10 @@ struct Stage(root: Node, camera: Affine, captured: Capture?, focus: Int?) =
101
105
  return StageHandled(stage: Stage(root: h.intoNode(), camera: camera, captured: captured, focus: focus), events: events, keys: left)
102
106
  StageHandled(stage: self, events: Slice[UiEvent](), keys: left)
103
107
 
104
- fun withCapture(self, c: Capture?) -> Stage =
108
+ fun withCapture(self, c: Option[Capture]) -> Stage =
105
109
  Stage(..self, captured: c)
106
110
 
107
- fun deliver(self, path: Slice[Int], e: Input, over: Bool, c: Capture?) -> StageHandled =
111
+ fun deliver(self, path: Slice[Int], e: Input, over: Bool, c: Option[Capture]) -> StageHandled =
108
112
  events := Slice[UiEvent]()
109
113
  match self
110
114
  Stage(root, camera, _, focus) =>
@@ -137,12 +141,12 @@ fun inBtnTree() -> Node =
137
141
  g
138
142
 
139
143
  fun inBtnPressed(s: Stage) -> Bool =
140
- if k := s.root.children()
144
+ if Some(k) := s.root.children()
141
145
  return psIsPressed(k.at(0))
142
146
  false
143
147
 
144
148
  fun inBtnOver(s: Stage) -> Bool =
145
- if k := s.root.children()
149
+ if Some(k) := s.root.children()
146
150
  return inOverOf(k.at(0))
147
151
  false
148
152
 
@@ -194,7 +198,7 @@ fun inMoverTree() -> Node =
194
198
  g
195
199
 
196
200
  fun inFirstX(s: Stage) -> Float =
197
- if k := s.root.children()
201
+ if Some(k) := s.root.children()
198
202
  return k.at(0).x
199
203
  0.0 - 1.0
200
204
 
@@ -203,7 +207,7 @@ test "press_sets_capture"
203
207
  assert h.events.length() == 1
204
208
  assert h.events.at(0).kind == Press
205
209
  assert h.events.at(0).id == 5
206
- if c := h.stage.captured
210
+ if Some(c) := h.stage.captured
207
211
  assert c.id == 5
208
212
  else
209
213
  assert false
@@ -214,7 +218,7 @@ test "release_elsewhere_no_click"
214
218
  assert h2.events.length() == 1
215
219
  assert h2.events.at(0).kind == Release
216
220
  assert h2.events.at(0).id == 5
217
- assert h2.stage.captured == null
221
+ assert h2.stage.captured.isNone()
218
222
  assert !inBtnPressed(h2.stage)
219
223
 
220
224
  test "drag_off_and_back_one_click"
@@ -230,19 +234,19 @@ test "drag_off_and_back_one_click"
230
234
  test "stale_capture_dropped"
231
235
  h1 := Stage.create(inBtnTree()).input(inDown(20.0, 20.0))
232
236
  empty: Node := Node.group(1, 0.0, 0.0, 800.0, 600.0, Slice[Node]())
233
- swapped := Stage(root: empty, camera: h1.stage.camera, captured: h1.stage.captured, focus: null)
237
+ swapped := Stage(root: empty, camera: h1.stage.camera, captured: h1.stage.captured, focus: None)
234
238
  h2 := swapped.input(inUp(20.0, 20.0))
235
239
  assert h2.events.length() == 0
236
- assert h2.stage.captured == null
240
+ assert h2.stage.captured.isNone()
237
241
 
238
242
  test "hover_without_press_routes_move"
239
243
  h := Stage.create(inBtnTree()).input(inMove(20.0, 20.0))
240
244
  assert inBtnOver(h.stage)
241
- assert h.stage.captured == null
245
+ assert h.stage.captured.isNone()
242
246
 
243
247
  test "camera_maps_screen_to_world"
244
248
  cam := Affine(a: 2.0, b: 0.0, c: 0.0, d: 2.0, e: 0.0, f: 0.0)
245
- s := Stage(root: inBtnTree(), camera: cam, captured: null, focus: null)
249
+ s := Stage(root: inBtnTree(), camera: cam, captured: None, focus: None)
246
250
  h := s.input(inDown(40.0, 40.0))
247
251
  assert h.events.length() == 1
248
252
  assert h.events.at(0).kind == Press
@@ -262,7 +266,7 @@ test "frame_handles_inputs_then_updates"
262
266
  assert h.events.at(0).kind == Press
263
267
  assert h.events.at(1).kind == Release
264
268
  assert h.events.at(2).kind == Click
265
- if k := h.stage.root.children()
269
+ if Some(k) := h.stage.root.children()
266
270
  assert {k.at(1).x - 305.0}.abs() < 0.000001
267
271
  else
268
272
  assert false
@@ -271,23 +275,23 @@ test "hit_and_route_agree_for_rotated_scaled_group"
271
275
  # local (20,20) in the button's parent maps to world (100 - 2*20, 2*20) = (60,40)
272
276
  t := inXformTree()
273
277
  hit := t.findHit(Affine.Identity, Vec2(x: 60.0, y: 40.0))
274
- assert hit != null
278
+ assert hit.isSome()
275
- h := Stage(root: t, camera: Affine.Identity, captured: null, focus: null).input(inDown(60.0, 40.0))
279
+ h := Stage(root: t, camera: Affine.Identity, captured: None, focus: None).input(inDown(60.0, 40.0))
276
280
  assert h.events.length() == 1
277
281
  assert h.events.at(0).id == 5
278
- miss := Stage(root: inXformTree(), camera: Affine.Identity, captured: null, focus: null).input(inDown(20.0, 20.0))
282
+ miss := Stage(root: inXformTree(), camera: Affine.Identity, captured: None, focus: None).input(inDown(20.0, 20.0))
279
283
  assert miss.events.length() == 0
280
284
 
281
285
  test "input_changes_stage_in_place"
282
286
  h := Stage.create(inBtnTree()).input(inDown(20.0, 20.0))
283
- assert h.stage.captured != null
287
+ assert h.stage.captured.isSome()
284
288
  assert inBtnPressed(h.stage)
285
289
 
286
290
  test "custom_actor_gets_pointer_through_on_input"
287
291
  h := Stage.create(inMoverTree()).input(inDown(20.0, 20.0))
288
292
  assert h.events.length() == 1
289
293
  assert h.events.at(0).id == 8
290
- assert {h.stage.focus ?: -1} == 8
294
+ assert h.stage.focus.unwrapOr(-1) == 8
291
295
 
292
296
  test "focused_actor_consumes_its_key"
293
297
  h1 := Stage.create(inMoverTree()).input(inDown(20.0, 20.0))
plum-wasm-codegen/src/lib.rs CHANGED
@@ -6650,7 +6650,43 @@ fn compileEnumUnwrap(
6650
6650
  .func_ids
6651
6651
  .get(CRASH_FN_KEY)
6652
6652
  .ok_or_else(|| "internal codegen error: crash import missing".to_string())?;
6653
+ // `x!`: "unwrap on None at f.plum:l:c" / "unwrap on Err(Name) at ..".
6654
+ let at = match expr {
6655
+ ast::Expr::Bang(b) => {
6656
+ let file = withGen(|s| s.err_info.file.clone());
6657
+ let file = if file.is_empty() { "main.plum".to_string() } else { file };
6658
+ format!(" at {}:{}:{}", file, b.line, b.column)
6659
+ }
6660
+ _ => String::new(),
6661
+ };
6662
+ if kind == "Result" {
6663
+ // An enum error names its variant: test the payload against each one.
6664
+ let base = match &targs[1] {
6665
+ PlumType::TNamed(n) | PlumType::TApp(n, _) => Some(n.clone()),
6666
+ _ => None,
6667
+ };
6668
+ let err_variants: Vec<(String, u32)> = ctx
6669
+ .enum_variants
6670
+ .iter()
6671
+ .filter(|(_, info)| Some(&info.enum_name) == base.as_ref() && !info.is_value)
6672
+ .filter_map(|(v, _)| Some((v.clone(), ctx.gc_types.variantIdx(&targs[1], v)?)))
6673
+ .collect();
6674
+ if !err_variants.is_empty() {
6675
+ let err_idx = variant(bad)?;
6676
+ for (v, vidx) in &err_variants {
6677
+ Instruction::LocalGet(scratch).encode(body);
6678
+ Instruction::RefCastNonNull(HeapType::Concrete(err_idx)).encode(body);
6679
+ Instruction::StructGet { struct_type_index: err_idx, field_index: 0 }.encode(body);
6680
+ Instruction::RefTestNonNull(HeapType::Concrete(*vidx)).encode(body);
6681
+ Instruction::If(BlockType::Empty).encode(body);
6682
+ compileStaticString(&format!("unwrap on Err({}){}", v, at), body, state);
6683
+ Instruction::Call(crash_idx).encode(body);
6684
+ Instruction::Unreachable.encode(body);
6685
+ Instruction::End.encode(body);
6686
+ }
6687
+ }
6688
+ }
6653
- compileStaticString(&format!("unwrap on {}", bad), body, state);
6689
+ compileStaticString(&format!("unwrap on {}{}", bad, at), body, state);
6654
6690
  Instruction::Call(crash_idx).encode(body);
6655
6691
  Instruction::Unreachable.encode(body);
6656
6692
  }
@@ -8109,6 +8145,12 @@ fn compileDefaultArgs(
8109
8145
  };
8110
8146
  emitAbsent(d, &e.name, body)?
8111
8147
  }
8148
+ // A payload-free variant default (`o: Option[Int] = None`): the clone has
8149
+ // no recorded type, so take the singleton of the param's declared type.
8150
+ Some(d) if defaultSingleton(default_expr, d, ctx).is_some() => {
8151
+ let g = defaultSingleton(default_expr, d, ctx).expect("checked");
8152
+ Instruction::GlobalGet(g).encode(body);
8153
+ }
8112
8154
  Some(d) => compileExprInto(default_expr, d, body, ctx, state)?,
8113
8155
  None => compileExpr(default_expr, body, ctx, state)?,
8114
8156
  }
@@ -8116,6 +8158,17 @@ fn compileDefaultArgs(
8116
8158
  Ok(())
8117
8159
  }
8118
8160
 
8161
+ /// The singleton global of a payload-free variant default `n` of the param type `d`.
8162
+ fn defaultSingleton(default_expr: &ast::Expr, d: &PlumType, ctx: &LocalCtx) -> Option<u32> {
8163
+ let ast::Expr::TypeName(n) = default_expr else { return None };
8164
+ let info = ctx.enum_variants.get(n).filter(|i| i.field_types.is_empty())?;
8165
+ match d {
8166
+ PlumType::TNamed(e) | PlumType::TApp(e, _) if *e == info.enum_name => {}
8167
+ _ => return None,
8168
+ }
8169
+ ctx.singleton_globals.get(&(canonTy(d), n.clone())).copied()
8170
+ }
8171
+
8119
8172
  /// Constructs a value enum by pushing its flattened fields in declaration order. Each
8120
8173
  /// field comes from the same-named keyword argument, else the positional argument at its
8121
8174
  /// index, else (`Vec2(..v, x: 1.0)`) the spread source's field.
plum-wasm-codegen/tests/codegen_tests.rs CHANGED
@@ -1368,15 +1368,16 @@ import std/Str
1368
1368
  import std/Int
1369
1369
  import std/Float
1370
1370
  import std/Slice
1371
+ import std/Option
1371
1372
 
1372
1373
  fun main() -> Int =
1373
1374
  l := Slice[Bool]()
1374
1375
  l.push(true)
1375
1376
  l.push(false)
1376
1377
  l.push(true)
1377
- a := l.get(0) catch false
1378
+ a := l.get(0).unwrapOr(false)
1378
- b := l.get(1) catch true
1379
+ b := l.get(1).unwrapOr(true)
1379
- c := l.get(2) catch false
1380
+ c := l.get(2).unwrapOr(false)
1380
1381
  if a && !b && c && l.length() == 3
1381
1382
  return 1
1382
1383
  return 0
@@ -1407,27 +1408,30 @@ fun main() -> Int =
1407
1408
  }
1408
1409
 
1409
1410
  #[test]
1410
- fn builtin_bool_in_error_union_with_question_and_catch() {
1411
+ fn builtin_bool_in_result_with_question_and_unwrap_or() {
1411
1412
  let src = "\
1412
1413
  import std/Str
1413
1414
  import std/Bool
1414
1415
  import std/Int
1415
1416
  import std/Float
1417
+ import std/Result
1416
1418
 
1419
+ enum NopeError = Nope | Other
1420
+
1417
- fun yes(ok: Bool) -> error{Nope}!Bool =
1421
+ fun yes(ok: Bool) -> Result[Bool, NopeError] =
1418
1422
  if ok
1419
- true
1423
+ Ok(true)
1420
1424
  else
1421
- return error.Nope
1425
+ return Err(Nope)
1422
1426
 
1423
- fun chain(ok: Bool) -> error{Nope}!Bool =
1427
+ fun chain(ok: Bool) -> Result[Bool, NopeError] =
1424
- v := try yes(ok)
1428
+ v := yes(ok)?
1425
- !v
1429
+ Ok(!v)
1426
1430
 
1427
1431
  fun main() -> Int =
1428
- a := chain(true) catch true
1432
+ a := chain(true).unwrapOr(true)
1429
- b := chain(false) catch true
1433
+ b := chain(false).unwrapOr(true)
1430
- p := Bool.parse(\"true\") catch false
1434
+ p := Bool.parse(\"true\").unwrapOr(false)
1431
1435
  if !a && b && p
1432
1436
  return 1
1433
1437
  return 0
@@ -2210,8 +2214,8 @@ fun main() -> Int =
2210
2214
  }
2211
2215
 
2212
2216
  #[test]
2213
- fn bang_message_names_the_file_of_the_callee_fn() {
2217
+ fn bang_on_none_crashes_with_unwrap_message() {
2214
- // A `!` inside a std fn would name that file; here the user file's own fn does.
2218
+ // `!` on an Option unwraps it and crashes with the std unwrap message.
2215
2219
  let src = "\
2216
2220
  import std/Str
2217
2221
  import std/Int
@@ -2228,11 +2232,41 @@ fun main() -> Int =
2228
2232
  assert!(r.is_err());
2229
2233
  assert_eq!(msgs.len(), 1, "{msgs:?}");
2230
2234
  assert!(
2231
- msgs[0].starts_with("first failed with ListWasEmpty at "),
2235
+ msgs[0].starts_with("unwrap on None at "),
2232
2236
  "{}",
2233
2237
  msgs[0]
2234
2238
  );
2235
- assert!(msgs[0].contains("main.plum:"), "{}", msgs[0]);
2239
+ assert!(msgs[0].contains("main.plum:7:"), "{}", msgs[0]);
2240
+ }
2241
+
2242
+ #[test]
2243
+ fn bang_on_err_names_the_variant_and_location() {
2244
+ let src = "\
2245
+ import std/Str
2246
+ import std/Int
2247
+ import std/Result
2248
+
2249
+ enum Oops =
2250
+ | ListWasEmpty
2251
+ | Other(code: Int)
2252
+
2253
+ fun get(n: Int) -> Result[Int, Oops] =
2254
+ if n == 0
2255
+ Err(ListWasEmpty)
2256
+ else
2257
+ Err(Other(n))
2258
+
2259
+ fun main() -> Int =
2260
+ get(0)!
2261
+ ";
2262
+ let (r, msgs) = runWithCrash("bangerr", src);
2263
+ assert!(r.is_err());
2264
+ assert_eq!(msgs.len(), 1, "{msgs:?}");
2265
+ assert!(msgs[0].starts_with("unwrap on Err(ListWasEmpty) at "), "{}", msgs[0]);
2266
+ assert!(msgs[0].contains("main.plum:16:"), "{}", msgs[0]);
2267
+ let (r, msgs) = runWithCrash("bangerr2", &src.replace("get(0)!", "get(3)!"));
2268
+ assert!(r.is_err());
2269
+ assert!(msgs[0].starts_with("unwrap on Err(Other) at "), "{}", msgs[0]);
2236
2270
  }
2237
2271
 
2238
2272
  #[test]
plum-wasm-codegen/tests/e2e_tail/error_tail.rs CHANGED
@@ -51,3 +51,30 @@ fn different_error_sets_normal_call() {
51
51
  assert_eq!(runInt("different_error_sets_normal_call", &src), 7);
52
52
  }
53
53
 
54
+
55
+ // Result twins of the error-union tests above (the old `error{..}!T` forms go in D3).
56
+ const RHEAD: &str = "import std/Int\nimport std/Result\n\nenum NegError = Neg | Other\n\n";
57
+
58
+ #[test]
59
+ fn result_self_loop_million() {
60
+ let src = format!("{RHEAD}fun down(n: Int) -> Result[Int, NegError] =\n if n < 0\n return Err(Neg)\n if n == 0\n return Ok(5)\n down(n - 1)\n\nfun main() -> Int = down(1000000).unwrapOr(99)\n");
61
+ assert_eq!(runInt("result_self_loop_million", &src), 5);
62
+ }
63
+
64
+ #[test]
65
+ fn result_self_loop_returns_err() {
66
+ let src = format!("{RHEAD}fun down(n: Int) -> Result[Int, NegError] =\n if n == 0\n return Err(Neg)\n down(n - 1)\n\nfun main() -> Int = down(1000000).unwrapOr(42)\n");
67
+ assert_eq!(runInt("result_self_loop_returns_err", &src), 42);
68
+ }
69
+
70
+ #[test]
71
+ fn result_mutual_million() {
72
+ let src = format!("{RHEAD}fun ping(n: Int) -> Result[Int, NegError] =\n if n == 0\n return Ok(1)\n pong(n - 1)\n\nfun pong(n: Int) -> Result[Int, NegError] =\n if n == 0\n return Ok(2)\n ping(n - 1)\n\nfun main() -> Int = ping(1000000).unwrapOr(99)\n");
73
+ assert_eq!(runInt("result_mutual_million", &src), 1);
74
+ }
75
+
76
+ #[test]
77
+ fn question_in_tail_not_tail_call() {
78
+ let src = format!("{RHEAD}fun down(n: Int) -> Result[Int, NegError] =\n if n == 0\n return Ok(5)\n v := down(n - 1)?\n Ok(v)\n\nfun main() -> Int = down(100).unwrapOr(99)\n");
79
+ assert_eq!(runInt("question_in_tail_not_tail_call", &src), 5);
80
+ }
plum-wasm-codegen/tests/e2e_tail/tail.rs CHANGED
@@ -247,9 +247,9 @@ fn tail_call_in_match_arm() {
247
247
 
248
248
  #[test]
249
249
  fn non_tail_union_wrap_not_tail() {
250
- // The callee returns Int but the fn returns Int?: the call is wrapped on the way
250
+ // The callee returns Int but the fn returns Option[Int]: the call is wrapped on the way
251
251
  // out, so it must stay a normal call and still produce the right value.
252
- let src = format!("{HEAD}fun inner(n: Int) -> Int = n + 1\n\nfun outer(n: Int) -> Int? = inner(n)\n\nfun main() -> Int =\n v := outer(41)\n v ?: 0\n");
252
+ let src = format!("{HEAD}import std/Option\n\nfun inner(n: Int) -> Int = n + 1\n\nfun outer(n: Int) -> Option[Int] = Some(inner(n))\n\nfun main() -> Int =\n v := outer(41)\n v.unwrapOr(0)\n");
253
253
  assert_eq!(runInt("wrap", &src), 42);
254
254
  }
255
255
 
plum-wasm-codegen/tests/e2e_types/closure_optional.rs CHANGED
@@ -1,28 +1,28 @@
1
- //! F-T3-2: closures whose expected return is `T?` / `!T`, calls through an fn-typed
1
+ //! F-T3-2: closures whose expected return is `Option[T]` / `Result[T, E]`, calls through an fn-typed
2
2
  //! field returning `T?`, and static methods of a generic type that never pin `T`.
3
3
  use plum_test_support::*;
4
4
 
5
5
  #[test]
6
6
  fn closure_coerced_into_optional_return() {
7
- let src = "import std/Int\n\nfun ap(f: fn() -> Int?) -> Int =\n f() ?: 0\n\nfun main() -> Int =\n ap(|| 3)\n";
7
+ let src = "import std/Int\nimport std/Option\n\nfun ap(f: fn() -> Option[Int]) -> Int =\n f().unwrapOr(0)\n\nfun main() -> Int =\n ap(|| Some(3))\n";
8
8
  assert_eq!(runInt("opt_present", src), 3);
9
9
  }
10
10
 
11
11
  #[test]
12
- fn closure_returning_null_into_optional() {
12
+ fn closure_returning_none_into_optional() {
13
- let src = "import std/Int\n\nfun ap(f: fn() -> Int?) -> Int =\n f() ?: 7\n\nfun main() -> Int =\n ap(|| null)\n";
13
+ let src = "import std/Int\nimport std/Option\n\nfun ap(f: fn() -> Option[Int]) -> Int =\n f().unwrapOr(7)\n\nfun main() -> Int =\n ap(|| None)\n";
14
14
  assert_eq!(runInt("opt_null", src), 7);
15
15
  }
16
16
 
17
17
  #[test]
18
- fn closure_into_ref_optional_and_error_union() {
19
- let src = "import std/Int\nimport std/Str\n\nfun s(f: fn(Int) -> Str?) -> Str =\n f(1) ?: \"none\"\n\nfun e(f: fn(Int) -> error{Bad}!Int) -> Int =\n f(1) catch 9\n\nfun main() -> Int =\n b := s(|x| {if x > 0 then \"yes\" else null}).length()\n b2 := s(|x| \"ab\").length()\n c := e(|x| x + 1)\n d := e(|x| error.Bad)\n b * 1000 + b2 * 100 + c * 10 + d\n";
18
+ fn closure_into_ref_option_and_result() {
19
+ let src = "import std/Int\nimport std/Str\nimport std/Option\nimport std/Result\n\nenum BadError = Bad | Worse\n\nfun s(f: fn(Int) -> Option[Str]) -> Str =\n f(1).unwrapOr(\"none\")\n\nfun e(f: fn(Int) -> Result[Int, BadError]) -> Int =\n f(1).unwrapOr(9)\n\nfun main() -> Int =\n b := s(|x| {if x > 0 then Some(\"yes\") else None}).length()\n b2 := s(|x| Some(\"ab\")).length()\n c := e(|x| Ok(x + 1))\n d := e(|x| Err(Bad))\n b * 1000 + b2 * 100 + c * 10 + d\n";
20
20
  assert_eq!(runInt("opt_mixed", src), 3229);
21
21
  }
22
22
 
23
23
  #[test]
24
24
  fn fn_field_returning_optional() {
25
- let src = "import std/Int\n\nstruct Gen(next: fn() -> Int?)\n\nfun count(g: Gen) -> Int =\n g.next() ?: 0\n\nfun main() -> Int =\n k := 5\n count(Gen(next: || k))\n";
25
+ let src = "import std/Int\nimport std/Option\n\nstruct Gen(next: fn() -> Option[Int])\n\nfun count(g: Gen) -> Int =\n g.next().unwrapOr(0)\n\nfun main() -> Int =\n k := 5\n count(Gen(next: || Some(k)))\n";
26
26
  assert_eq!(runInt("field_opt", src), 5);
27
27
  }
28
28
 
@@ -34,6 +34,6 @@ fn static_method_of_generic_type_not_pinning_t() {
34
34
 
35
35
  #[test]
36
36
  fn seq_with_optional_next() {
37
- let src = "import std/Int\n\nstruct Seq[T](next: fn(Int) -> T?) =\n fun upTo(n: Int) -> Seq[Int] = Seq(next: |i| {if i < n then i * 2 else null})\n fun sum(self) -> Int =\n total := 0\n i := 0\n while v := self.next(i)\n total = total + v\n i = i + 1\n total\n\nfun main() -> Int =\n Seq.upTo(4).sum()\n";
37
+ let src = "import std/Int\nimport std/Option\n\nstruct Seq[T](next: fn(Int) -> Option[T]) =\n fun upTo(n: Int) -> Seq[Int] = Seq(next: |i| {if i < n then Some(i * 2) else None})\n fun sum(self) -> Int =\n total := 0\n i := 0\n while Some(v) := self.next(i)\n total = total + v\n i = i + 1\n total\n\nfun main() -> Int =\n Seq.upTo(4).sum()\n";
38
38
  assert_eq!(runInt("seq_opt", src), 12);
39
39
  }
plum-wasm-codegen/tests/e2e_types/question.rs CHANGED
@@ -134,3 +134,19 @@ fn specialised_arm_keeps_result_type() {
134
134
  );
135
135
  assert_eq!(runInt("q_spec_arm", &src), 1038);
136
136
  }
137
+
138
+ #[test]
139
+ fn none_as_param_default() {
140
+ let src = prog(
141
+ "fun f(n: Option[Int] = None) -> Option[Int] = n\n\nfun main() -> Int =\n a := f().unwrapOr(7)\n b := f(Some(3)).unwrapOr(0)\n a * 10 + b\n",
142
+ );
143
+ assert_eq!(runInt("q_none_default", &src), 73);
144
+ }
145
+
146
+ #[test]
147
+ fn none_as_generic_param_default() {
148
+ let src = prog(
149
+ "fun g(x: T, o: Option[T] = None) -> Option[T] = o\n\nfun main() -> Int =\n a := g(1).unwrapOr(7)\n b := g(1, Some(3)).unwrapOr(0)\n a * 10 + b\n",
150
+ );
151
+ assert_eq!(runInt("q_none_generic_default", &src), 73);
152
+ }
plum-wasm-codegen/tests/e2e_types/type_param_static.rs CHANGED
@@ -70,8 +70,7 @@ fun buildAll(ns: Slice[Int]) -> Slice[T] =
70
70
 
71
71
  fun main() -> Int =
72
72
  ps: Slice[Pt] := buildAll([1, 2, 3].toSlice())
73
- p := ps.get(2) catch e
73
+ p := ps.get(2).unwrapOr(Pt(x: 0))
74
- return 0
75
74
  return p.x
76
75
  ";
77
76
  assert_eq!(runInt("nested", src), 4);
@@ -83,14 +82,14 @@ fn static_on_builtin_method_and_json_cycle() {
83
82
  import std/Int
84
83
  import std/Str
85
84
  import std/Json
85
+ import std/Result
86
86
 
87
- fun parseIt(s: Str) -> error{BadNumStr}!T =
87
+ fun parseIt(s: Str) -> Result[T, NumError] =
88
88
  T.parse(s)
89
89
 
90
90
  fun main() -> Int =
91
- r: error{BadNumStr}!Int := parseIt(\"41\")
91
+ r: Result[Int, NumError] := parseIt(\"41\")
92
- n := r catch e
92
+ n := r.unwrapOr(0)
93
- return 0
94
93
  j := n.toJson()
95
94
  return n + 1
96
95
  ";