plum
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
- docs/plans/2026-11-05-language-simplification-d2.md +10 -11
- docs/plans/2026-11-05-language-simplification-d2.results.md +24 -0
- plum-checker/src/lib.rs +43 -3
- plum-checker/tests/closure_hint_test.rs +55 -0
- plum-checker/tests/question_tests.rs +64 -0
- plum-core/src/drive.rs +22 -3
- plum-core/src/forms.rs +8 -5
- plum-core/tests/for_next_test.rs +10 -7
- plum-core/tests/fused_closure_generic_test.rs +28 -0
- plum-core/tests/record_init_test.rs +2 -0
- plum-core/tests/tail_test.rs +2 -0
- plum-examples/errors.plum +323 -281
- plum-examples/functional.plum +30 -29
- plum-examples/games/functional/game_state_view.plum +6 -2
- plum-examples/games/functional/kids/balloon_pop.plum +5 -3
- plum-examples/games/imperative/game_loop.plum +2 -1
- plum-examples/games/imperative/game_scene.plum +3 -2
- plum-examples/games/imperative/scene.plum +8 -7
- plum-examples/games/pure/game_pure_scene.plum +4 -3
- plum-examples/generics.plum +55 -48
- plum-examples/json.plum +16 -14
- plum-examples/list_literal_probe.plum +2 -1
- plum-examples/match.plum +32 -29
- plum-examples/numbers.plum +7 -5
- plum-examples/recursion.plum +14 -10
- plum-examples/sequences.plum +10 -9
- plum-examples/structs_records.plum +33 -31
- plum-examples/unions.plum +14 -10
- plum-game/web-test/fixtures/stress_pure_scene.plum +3 -2
- plum-game/web-test/fixtures/stress_state_view.plum +9 -5
- plum-std/Base64.plum +10 -5
- plum-std/Bool.plum +12 -7
- plum-std/Deque.plum +16 -15
- plum-std/Float.plum +9 -7
- plum-std/Heap.plum +10 -9
- plum-std/Http.plum +26 -16
- plum-std/Int.plum +12 -7
- plum-std/Json.plum +151 -139
- plum-std/List.plum +10 -9
- plum-std/Map.plum +8 -7
- plum-std/Os.plum +69 -53
- plum-std/Pair.plum +3 -2
- plum-std/Slice.plum +39 -38
- plum-std/Testing.plum +30 -21
- plum-std/Uuid.plum +14 -10
- plum-std/game/Affine.plum +9 -8
- plum-std/game/Input.plum +10 -9
- plum-std/game/functional/FunctionalScene.plum +66 -65
- plum-std/game/functional/Ui.plum +48 -44
- plum-std/game/imperative/Scene.plum +47 -46
- plum-std/game/pure/Actions.plum +15 -14
- plum-std/game/pure/PureScene.plum +33 -32
- plum-std/game/pure/SceneInput.plum +40 -36
- plum-wasm-codegen/src/lib.rs +54 -1
- plum-wasm-codegen/tests/codegen_tests.rs +51 -17
- plum-wasm-codegen/tests/e2e_tail/error_tail.rs +27 -0
- plum-wasm-codegen/tests/e2e_tail/tail.rs +2 -2
- plum-wasm-codegen/tests/e2e_types/closure_optional.rs +8 -8
- plum-wasm-codegen/tests/e2e_types/question.rs +16 -0
- 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:**
|
|
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:**
|
|
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:**
|
|
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:**
|
|
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:**
|
|
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:**
|
|
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:**
|
|
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:**
|
|
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:**
|
|
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:**
|
|
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
|
-
|
|
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(©, &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
|
-
|
|
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
|
-
|
|
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]
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
# ====
|
|
11
|
+
# ==== Result: a value or an error ====
|
|
10
12
|
|
|
11
|
-
# `
|
|
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
|
-
#
|
|
14
|
+
# an ordinary type `E`: usually one enum per module whose cases name what can go
|
|
16
|
-
#
|
|
15
|
+
# wrong (`NumError` in std/Int, `IoError` in std/Os, `JsonError` in std/Json).
|
|
17
|
-
#
|
|
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
|
-
# `
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
|
|
28
|
+
enum PositiveError =
|
|
40
|
-
n := try Int.parse(s)
|
|
41
|
-
|
|
29
|
+
| BadNumber
|
|
42
|
-
|
|
30
|
+
| NegativeNum
|
|
43
|
-
return n
|
|
44
31
|
|
|
45
|
-
fun
|
|
32
|
+
fun name(self) -> Str = match self
|
|
33
|
+
BadNumber => "BadNumber"
|
|
46
|
-
|
|
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
|
|
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) ->
|
|
59
|
+
fun numAt(s: Str, i: Int) -> Result[Int, NumAtError] =
|
|
52
|
-
n :=
|
|
60
|
+
n := Int.parse(s).mapErr(|e| BadDigits)?
|
|
53
61
|
xs := [10, 20, 30].toSlice()
|
|
54
|
-
x :=
|
|
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
|
|
65
|
+
fun numAtCode(s: Str, i: Int) -> Int = match numAt(s, i)
|
|
66
|
+
Ok(v) => v
|
|
67
|
+
Err(BadDigits) => 100
|
|
59
|
-
|
|
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]) ->
|
|
83
|
+
fun doubledFirst(list: Slice[Int]) -> Option[Int] =
|
|
63
|
-
a :=
|
|
84
|
+
a := list.first()?
|
|
64
|
-
a * 2
|
|
85
|
+
Some(a * 2)
|
|
65
86
|
|
|
66
87
|
fun sumOrZero(a: Str, b: Str) -> Int =
|
|
67
|
-
x :=
|
|
88
|
+
x := parsePositive(a).unwrapOr(0)
|
|
68
|
-
y :=
|
|
89
|
+
y := parsePositive(b).unwrapOr(0)
|
|
69
90
|
x + y
|
|
70
91
|
|
|
71
92
|
fun firstOrDefault(list: Slice[Int], default: Int) -> Int =
|
|
72
|
-
|
|
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
|
-
# `
|
|
100
|
+
# `?` is a postfix operator: `n * f()?` is `n * {f()?}`.
|
|
80
|
-
fun scaled(n: Int, s: Str) ->
|
|
101
|
+
fun scaled(n: Int, s: Str) -> Result[Int, NumError] =
|
|
81
|
-
n *
|
|
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) ->
|
|
121
|
+
fun parseAge(s: Str) -> Result[Int, AgeError] =
|
|
84
|
-
n :=
|
|
122
|
+
n := Int.parse(s).mapErr(|e| NotANumber)?
|
|
85
123
|
if n < 0
|
|
86
|
-
return
|
|
124
|
+
return Err(Negative)
|
|
87
125
|
if n > 150
|
|
88
|
-
return
|
|
126
|
+
return Err(TooOld)
|
|
89
|
-
n
|
|
127
|
+
Ok(n)
|
|
90
|
-
|
|
128
|
+
|
|
91
|
-
# Handling errors with `
|
|
129
|
+
# Handling errors with `match`: one arm per case (or `_`), and every arm
|
|
92
|
-
#
|
|
130
|
+
# produces the same type.
|
|
93
|
-
fun describeAge(s: Str) -> Str =
|
|
131
|
+
fun describeAge(s: Str) -> Str = match parseAge(s)
|
|
94
|
-
n
|
|
132
|
+
Ok(n) => "age {n}"
|
|
95
|
-
match e
|
|
96
|
-
BadNumStr =>
|
|
97
|
-
|
|
133
|
+
Err(NotANumber) => "not a number"
|
|
98
|
-
Negative =>
|
|
99
|
-
|
|
134
|
+
Err(Negative) => "can't be negative"
|
|
100
|
-
TooOld =>
|
|
101
|
-
|
|
135
|
+
Err(TooOld) => "too old"
|
|
102
|
-
|
|
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
|
-
|
|
148
|
+
Ok(n) => n
|
|
116
|
-
match e
|
|
117
|
-
|
|
149
|
+
Err(TooOld) => 150
|
|
118
|
-
|
|
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) ->
|
|
160
|
+
fun checkNonNeg(n: Int) -> Result[Unit, Neg] =
|
|
127
161
|
if n < 0
|
|
128
|
-
return
|
|
162
|
+
return Err(Neg(value: n))
|
|
163
|
+
Ok(Unit)
|
|
129
164
|
|
|
130
|
-
fun passCheck(n: Int) ->
|
|
165
|
+
fun passCheck(n: Int) -> Result[Unit, Neg] =
|
|
131
|
-
|
|
166
|
+
checkNonNeg(n)
|
|
132
167
|
|
|
133
|
-
fun runChecked(n: Int) ->
|
|
168
|
+
fun runChecked(n: Int) -> Result[Int, Neg] =
|
|
134
|
-
|
|
169
|
+
checkNonNeg(n)?
|
|
135
|
-
n
|
|
170
|
+
Ok(n)
|
|
136
171
|
|
|
137
|
-
fun negOrSeven(n: Int) ->
|
|
172
|
+
fun negOrSeven(n: Int) -> Result[Int, Neg] =
|
|
138
173
|
if n < 0
|
|
139
|
-
return
|
|
174
|
+
return Err(Neg(value: n))
|
|
140
|
-
n
|
|
175
|
+
Ok(n)
|
|
141
176
|
|
|
142
|
-
fun lookup(m: Map[Int,
|
|
177
|
+
fun lookup(m: Map[Int, Result[Int, Neg]]) -> Int =
|
|
143
|
-
v := m.get(1)
|
|
178
|
+
v := m.get(1).unwrapOr(negOrSeven(-1))
|
|
144
|
-
v
|
|
179
|
+
v.unwrapOr(0)
|
|
145
180
|
|
|
146
|
-
# `
|
|
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
|
|
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) ->
|
|
185
|
+
fun parse(s: Str) -> Result[Age, PositiveError] =
|
|
150
|
-
n :=
|
|
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]) ->
|
|
190
|
+
fun parseAll(xs: Slice[Str]) -> Result[Ages[T], PositiveError] =
|
|
155
191
|
out := Slice[T]()
|
|
156
192
|
for x := range xs
|
|
157
|
-
v :=
|
|
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]) ->
|
|
197
|
+
fun parseAges(xs: Slice[Str]) -> Result[Ages[Age], PositiveError] = Ages.parseAll(xs)
|
|
162
198
|
|
|
163
|
-
# ====
|
|
199
|
+
# ==== Option: a value or nothing ====
|
|
164
200
|
|
|
165
|
-
#
|
|
201
|
+
# `Option[T]` is `Some(value)` or `None`. A present value is always written
|
|
166
|
-
#
|
|
202
|
+
# `Some(v)`. An option is handled with `unwrapOr(d)`, `if Some(x) := opt`,
|
|
167
|
-
# `while x := opt`, `opt!` (crashes on
|
|
203
|
+
# `while Some(x) := opt`, `opt!` (crashes on `None`), `isSome()` and `isNone()`
|
|
168
|
-
#
|
|
204
|
+
# (`==` on an Option is an error: it would compare identity, not contents).
|
|
169
205
|
#
|
|
170
|
-
# `
|
|
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
|
-
#
|
|
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
|
|
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
|
|
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
|
-
|
|
222
|
+
None
|
|
188
223
|
|
|
189
|
-
fun orFive(n: Int
|
|
224
|
+
fun orFive(n: Option[Int] = None) -> Int =
|
|
190
|
-
n
|
|
225
|
+
n.unwrapOr(5)
|
|
191
226
|
|
|
227
|
+
# `okOr` names the error for `None`; `?` then propagates it.
|
|
192
|
-
fun
|
|
228
|
+
fun failOnNone(c: Bool) -> Result[Int, Str] =
|
|
193
|
-
n := maybeInt(c)
|
|
229
|
+
n := maybeInt(c).okOr("absent")?
|
|
194
|
-
n + 1
|
|
230
|
+
Ok(n + 1)
|
|
195
231
|
|
|
196
|
-
fun
|
|
232
|
+
fun noneOnNone(c: Bool) -> Option[Int] =
|
|
197
|
-
n := maybeInt(c)
|
|
233
|
+
n := maybeInt(c)?
|
|
198
|
-
n * 2
|
|
234
|
+
Some(n * 2)
|
|
199
235
|
|
|
200
|
-
fun chainDefault(a: Int
|
|
236
|
+
fun chainDefault(a: Option[Int], b: Option[Int]) -> Int =
|
|
201
|
-
a
|
|
237
|
+
a.unwrapOr(b.unwrapOr(99))
|
|
202
238
|
|
|
239
|
+
# A Result whose success value is an Option.
|
|
203
|
-
fun fallible(n: Int) ->
|
|
240
|
+
fun fallible(n: Int) -> Result[Option[Int], Str] =
|
|
204
241
|
if n < 0
|
|
205
|
-
return
|
|
242
|
+
return Err("bad")
|
|
206
243
|
if n == 0
|
|
207
|
-
return
|
|
244
|
+
return Ok(None)
|
|
208
|
-
n
|
|
245
|
+
Ok(Some(n))
|
|
209
246
|
|
|
210
|
-
fun
|
|
247
|
+
fun viaQuestion(n: Int) -> Result[Int, Str] =
|
|
211
|
-
v :=
|
|
248
|
+
v := fallible(n)?
|
|
212
|
-
v
|
|
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
|
-
|
|
252
|
+
items.get(i)
|
|
217
|
-
null
|
|
218
253
|
|
|
219
|
-
fun sparse() -> Slice[Int
|
|
254
|
+
fun sparse() -> Slice[Option[Int]] =
|
|
220
|
-
xs: List[Int
|
|
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
|
|
270
|
+
fun personName(p: Option[Person]) -> Option[Str] =
|
|
236
|
-
p
|
|
271
|
+
p.map(|v| v.name)
|
|
237
272
|
|
|
238
|
-
fun personBirthday(p: Person
|
|
273
|
+
fun personBirthday(p: Option[Person]) -> Option[Int] =
|
|
239
|
-
p
|
|
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
|
|
280
|
+
return None
|
|
246
281
|
|
|
247
|
-
fun noTarget() -> Target
|
|
282
|
+
fun noTarget() -> Option[Target] =
|
|
248
|
-
return
|
|
283
|
+
return None
|
|
249
284
|
|
|
250
|
-
fun maybeCount(n: 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
|
|
289
|
+
return None
|
|
255
290
|
|
|
256
|
-
fun targetName(u: User
|
|
291
|
+
fun targetName(u: Option[User]) -> Option[Str] =
|
|
257
|
-
u
|
|
292
|
+
u.andThen(|v| v.target).map(|t| t.name)
|
|
258
293
|
|
|
259
|
-
# ====
|
|
294
|
+
# ==== Result tests ====
|
|
260
295
|
|
|
261
|
-
test "
|
|
296
|
+
test "question mark unwraps the value and propagates it through two calls"
|
|
262
297
|
assert sumTwo("2", "3")! == 5
|
|
263
298
|
|
|
264
|
-
test "
|
|
299
|
+
test "question mark exits early with the error, skipping the rest of the function"
|
|
265
|
-
assert
|
|
300
|
+
assert sumTwo("2", "-3").unwrapOr(-1) == -1
|
|
266
301
|
assert errorText("-3") == "Negative"
|
|
267
302
|
|
|
268
|
-
test "
|
|
303
|
+
test "an Option and a Result with different error types share one error enum"
|
|
269
304
|
assert numAt("5", 1)! == 25
|
|
270
|
-
assert
|
|
305
|
+
assert numAt("x", 1).unwrapOr(-1) == -1
|
|
271
|
-
assert
|
|
306
|
+
assert numAt("5", 9).unwrapOr(-2) == -2
|
|
272
|
-
|
|
307
|
+
assert numAtCode("5", 9) == 200
|
|
273
|
-
match e
|
|
274
|
-
BadNumStr => 100
|
|
275
|
-
OutOfBounds => 200
|
|
276
|
-
assert
|
|
308
|
+
assert numAtCode("x", 1) == 100
|
|
277
|
-
|
|
309
|
+
assert numAtCode("5", 0) == 15
|
|
310
|
+
|
|
278
|
-
test "
|
|
311
|
+
test "mapErr maps the error to another type"
|
|
279
312
|
assert parseOrInvalid("7")! == 7
|
|
280
|
-
|
|
313
|
+
if Err(e) := parseOrInvalid("x")
|
|
281
314
|
assert e.name() == "InvalidInput"
|
|
282
|
-
|
|
315
|
+
else
|
|
283
|
-
|
|
316
|
+
assert false
|
|
284
317
|
|
|
285
|
-
test "
|
|
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
|
-
|
|
322
|
+
if Err(e) := parseAges(["3", "-1"].toSlice())
|
|
290
323
|
assert e.name() == "NegativeNum"
|
|
291
|
-
|
|
324
|
+
else
|
|
292
|
-
|
|
325
|
+
assert false
|
|
293
326
|
|
|
294
|
-
test "
|
|
327
|
+
test "question mark on an empty list yields None"
|
|
295
328
|
assert doubledFirst([5].toSlice())! == 10
|
|
296
|
-
assert
|
|
329
|
+
assert doubledFirst(Slice[Int]()).unwrapOr(-1) == -1
|
|
297
330
|
|
|
298
|
-
test "
|
|
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 "
|
|
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 "
|
|
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
|
-
#
|
|
369
|
+
# A Result can be the element of a collection: `xs.get(i)` is an
|
|
330
|
-
#
|
|
370
|
+
# `Option[Result[Int, E]]`, unwrapped one level at a time.
|
|
331
|
-
test "a list of
|
|
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
|
|
376
|
+
assert second.unwrapOr(0) == 0
|
|
337
|
-
fallback :=
|
|
377
|
+
fallback := results.get(9).unwrapOr(parsePositive("5"))
|
|
338
|
-
assert
|
|
378
|
+
assert fallback.unwrapOr(0) == 5
|
|
339
379
|
|
|
340
|
-
test "a Unit
|
|
380
|
+
test "a Unit result propagates with question mark and is handled with if Err"
|
|
341
|
-
assert
|
|
381
|
+
assert runChecked(4).unwrapOr(0) == 4
|
|
342
|
-
assert
|
|
382
|
+
assert runChecked(-1).unwrapOr(5) == 5
|
|
343
383
|
hits := 0
|
|
344
|
-
|
|
384
|
+
if Err(e) := passCheck(1)
|
|
345
385
|
hits = hits + 1
|
|
386
|
+
if Err(e) := passCheck(-1)
|
|
346
|
-
|
|
387
|
+
assert e.value == -1
|
|
347
388
|
hits = hits + 10
|
|
348
389
|
assert hits == 10
|
|
349
390
|
|
|
350
|
-
test "a Map can hold
|
|
391
|
+
test "a Map can hold results as values"
|
|
351
|
-
m: Map[Int,
|
|
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
|
-
# ====
|
|
398
|
+
# ==== Option tests ====
|
|
358
399
|
|
|
359
|
-
test "
|
|
400
|
+
test "none_return_absent"
|
|
360
|
-
assert maybeBox(false)
|
|
401
|
+
assert maybeBox(false).isNone()
|
|
361
|
-
assert maybeBox(true)
|
|
402
|
+
assert maybeBox(true).isSome()
|
|
362
|
-
assert maybeInt(false)
|
|
403
|
+
assert maybeInt(false).isNone()
|
|
363
|
-
assert
|
|
404
|
+
assert maybeInt(true).isSome()
|
|
364
405
|
|
|
365
|
-
test "
|
|
406
|
+
test "none_field_and_arg"
|
|
366
|
-
h := Holder(item:
|
|
407
|
+
h := Holder(item: None, count: None)
|
|
367
|
-
assert h.item
|
|
408
|
+
assert h.item.isNone()
|
|
368
|
-
assert h.count
|
|
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(
|
|
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 "
|
|
418
|
+
test "unwrapOr_default"
|
|
377
|
-
assert
|
|
419
|
+
assert maybeInt(false).unwrapOr(4) == 4
|
|
378
|
-
assert
|
|
420
|
+
assert maybeInt(true).unwrapOr(4) == 7
|
|
379
|
-
assert
|
|
421
|
+
assert maybeBox(false).map(|b| b.label).unwrapOr("none") == "none"
|
|
380
|
-
assert
|
|
422
|
+
assert maybeBox(true).map(|b| b.label).unwrapOr("none") == "in"
|
|
381
423
|
|
|
382
|
-
test "
|
|
424
|
+
test "okOr_turns_none_into_an_error"
|
|
383
|
-
assert
|
|
425
|
+
assert failOnNone(true).unwrapOr(0) == 8
|
|
384
|
-
assert
|
|
426
|
+
assert failOnNone(false).unwrapOr(0) == 0
|
|
385
427
|
|
|
386
|
-
test "
|
|
428
|
+
test "question_mark_returns_none"
|
|
387
|
-
assert
|
|
429
|
+
assert noneOnNone(true)! == 14
|
|
388
|
-
assert
|
|
430
|
+
assert noneOnNone(false).isNone()
|
|
389
431
|
|
|
390
|
-
test "
|
|
432
|
+
test "unwrapOr_chain"
|
|
391
|
-
assert chainDefault(
|
|
433
|
+
assert chainDefault(None, None) == 99
|
|
392
|
-
assert chainDefault(
|
|
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 "
|
|
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 "
|
|
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 "
|
|
463
|
+
test "map_over_option_flattens_with_andThen"
|
|
422
|
-
assert maybeBox(true)
|
|
464
|
+
assert maybeBox(true).map(|b| b.label)! == "in"
|
|
423
|
-
assert maybeBox(false)
|
|
465
|
+
assert maybeBox(false).map(|b| b.label).isNone()
|
|
424
466
|
|
|
425
|
-
test "
|
|
467
|
+
test "isNone_and_isSome"
|
|
426
468
|
b := maybeBox(false)
|
|
427
|
-
assert b
|
|
469
|
+
assert b.isNone()
|
|
428
470
|
n := maybeInt(true)
|
|
471
|
+
assert n.isSome()
|
|
429
|
-
assert
|
|
472
|
+
assert !n.isNone()
|
|
430
|
-
assert !{n == null}
|
|
431
473
|
|
|
432
|
-
test "
|
|
474
|
+
test "result_of_option"
|
|
433
|
-
assert
|
|
475
|
+
assert viaQuestion(5).unwrapOr(0) == 5
|
|
434
|
-
assert
|
|
476
|
+
assert viaQuestion(0).unwrapOr(0) == -1
|
|
435
|
-
assert
|
|
477
|
+
assert viaQuestion(-3).unwrapOr(100) == 100
|
|
436
478
|
x := fallible(0)!
|
|
437
|
-
assert x
|
|
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
|
|
487
|
+
test "reassigning an option local or field to None or a value"
|
|
446
488
|
o := maybeInt(true)
|
|
447
|
-
o =
|
|
489
|
+
o = None
|
|
448
|
-
assert o
|
|
490
|
+
assert o.isNone()
|
|
449
|
-
o = 4
|
|
491
|
+
o = Some(4)
|
|
450
492
|
assert o! == 4
|
|
451
493
|
b := maybeBox(true)
|
|
452
|
-
b =
|
|
494
|
+
b = None
|
|
453
|
-
assert b
|
|
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 =
|
|
497
|
+
h.count = None
|
|
456
|
-
h.item =
|
|
498
|
+
h.item = None
|
|
457
|
-
assert h.count
|
|
499
|
+
assert h.count.isNone()
|
|
458
|
-
assert h.item
|
|
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 "
|
|
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
|
|
508
|
+
assert personName(noPerson()).unwrapOr("gone") == "gone"
|
|
467
509
|
|
|
468
|
-
test "
|
|
510
|
+
test "map over an option maps a method call over a present value"
|
|
469
511
|
assert personBirthday(somePerson())! == 4
|
|
470
|
-
assert
|
|
512
|
+
assert personBirthday(noPerson()).unwrapOr(-1) == -1
|
|
471
513
|
|
|
472
|
-
test "
|
|
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
|
|
516
|
+
assert targetName(Some(full)).unwrapOr("none") == "home"
|
|
475
|
-
assert
|
|
517
|
+
assert targetName(Some(User(name: "v", target: noTarget()))).unwrapOr("none") == "none"
|
|
476
|
-
assert
|
|
518
|
+
assert targetName(noUser()).unwrapOr("none") == "none"
|
|
477
519
|
|
|
478
|
-
test "
|
|
520
|
+
test "unwrapOr and bang handle a present or computed default"
|
|
479
|
-
p := somePerson()
|
|
521
|
+
p := somePerson()!
|
|
480
522
|
assert p.age == 3
|
|
481
|
-
q := noPerson()
|
|
523
|
+
q := noPerson().unwrapOr(Person(name: "gone", age: 1))
|
|
482
524
|
assert q.name == "gone"
|
|
483
525
|
|
|
484
|
-
test "
|
|
526
|
+
test "None is the absent value in an argument, a list element and a default"
|
|
485
|
-
u := User(name: "w", target:
|
|
527
|
+
u := User(name: "w", target: None)
|
|
486
|
-
assert
|
|
528
|
+
assert targetName(Some(u)).unwrapOr("none") == "none"
|
|
487
529
|
xs := sparse()
|
|
488
|
-
second := xs.
|
|
530
|
+
second := xs.at(1)
|
|
489
|
-
assert
|
|
531
|
+
assert second.unwrapOr(9) == 9
|
|
490
|
-
first := xs.
|
|
532
|
+
first := xs.at(0)
|
|
491
533
|
assert first! == 1
|
|
492
|
-
assert
|
|
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` (
|
|
56
|
+
# A `for` source: `for x := range xs` steps with `next` (`None` = done).
|
|
56
|
-
fun next(self) -> Pair[T, Chain[T]]
|
|
57
|
+
fun next(self) -> Option[Pair[T, Chain[T]]] = match self
|
|
57
|
-
Empty =>
|
|
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
|
|
105
|
+
# The first element, or None for an empty list.
|
|
105
|
-
fun first(self) -> T
|
|
106
|
+
fun first(self) -> Option[T] = match self
|
|
106
|
-
Empty =>
|
|
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
|
|
199
|
+
fun smallest(self) -> Option[Int] = match self
|
|
199
|
-
Leaf =>
|
|
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
|
|
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(_, _, _) =>
|
|
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
|
|
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
|
|
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
|
|
329
|
+
fun firstHit(kids: Chain[Node], xf: Xf, px: Float, py: Float) -> Option[Int] = match kids
|
|
329
|
-
Empty =>
|
|
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
|
|
432
|
+
assert fromTo(7, 9).first().unwrapOr(0) == 7
|
|
432
|
-
assert
|
|
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
|
|
477
|
+
assert t2.smallest().unwrapOr(-1) == 1
|
|
477
|
-
assert
|
|
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
|
|
512
|
+
assert scene.findHit(identityXf(), 105.0, 55.0).unwrapOr(0) == 2
|
|
512
|
-
assert
|
|
513
|
+
assert scene.findHit(identityXf(), 115.0, 165.0).unwrapOr(0) == 5
|
|
513
|
-
assert
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
212
|
+
if Some(id) := view.hitId(x, y)
|
|
212
|
-
|
|
213
|
+
if Some(msg) := view.msgOf(id)
|
|
213
|
-
|
|
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.
|
|
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:
|
|
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(
|
|
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:
|
|
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(
|
|
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)
|
|
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()
|
|
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:
|
|
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,
|
|
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) ->
|
|
30
|
+
fun wrap(x: T) -> Result[T, GenError] =
|
|
24
|
-
x
|
|
31
|
+
Ok(x)
|
|
25
32
|
|
|
26
|
-
fun viaQ(x: T) ->
|
|
33
|
+
fun viaQ(x: T) -> Result[T, GenError] =
|
|
27
|
-
y :=
|
|
34
|
+
y := wrap(x)?
|
|
28
|
-
return y
|
|
35
|
+
return Ok(y)
|
|
29
36
|
|
|
30
|
-
fun failing() ->
|
|
37
|
+
fun failing() -> Result[Int, GenError] =
|
|
31
|
-
return
|
|
38
|
+
return Err(Boom)
|
|
32
39
|
|
|
33
|
-
fun viaQFailing() ->
|
|
40
|
+
fun viaQFailing() -> Result[Int, GenError] =
|
|
34
|
-
y :=
|
|
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) ->
|
|
50
|
+
fun readDeep(outer: Slice[Slice[Map[Str, Int]]], r: Int, c: Int, key: Str) -> Result[Int, GenError] =
|
|
44
|
-
row :=
|
|
51
|
+
row := outer.get(r).okOr(OutOfBounds)?
|
|
45
|
-
m :=
|
|
52
|
+
m := row.get(c).okOr(OutOfBounds)?
|
|
46
|
-
v :=
|
|
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
|
|
173
|
+
assert ints.get(0).unwrapOr(0) == 9223372036854775807
|
|
167
|
-
assert
|
|
174
|
+
assert ints.get(1).unwrapOr(0) == 0 - 9223372036854775807 - 1
|
|
168
|
-
assert
|
|
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 :=
|
|
180
|
+
nan := floats.get(0).unwrapOr(0.0)
|
|
174
181
|
assert nan != nan
|
|
175
|
-
assert 1.0 /
|
|
182
|
+
assert 1.0 / floats.get(1).unwrapOr(1.0) < 0.0
|
|
176
|
-
assert
|
|
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
|
|
190
|
+
assert mi.get("big").unwrapOr(0) == 9223372036854775807
|
|
184
|
-
assert
|
|
191
|
+
assert mi.get("small").unwrapOr(0) == 0 - 9223372036854775807 - 1
|
|
185
|
-
assert
|
|
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 :=
|
|
197
|
+
nan := mf.get("nan").unwrapOr(0.0)
|
|
191
198
|
assert nan != nan
|
|
192
|
-
assert 1.0 /
|
|
199
|
+
assert 1.0 / mf.get("negzero").unwrapOr(1.0) < 0.0
|
|
193
|
-
assert
|
|
200
|
+
assert mf.get("tiny").unwrapOr(0.0) == 5e-324
|
|
194
201
|
|
|
195
|
-
test "Int extremes survive
|
|
202
|
+
test "Int extremes survive a Result payload with ? and unwrapOr"
|
|
196
|
-
assert
|
|
203
|
+
assert wrap(9223372036854775807).unwrapOr(0) == 9223372036854775807
|
|
197
|
-
assert
|
|
204
|
+
assert viaQ(0 - 9223372036854775807 - 1).unwrapOr(0) == 0 - 9223372036854775807 - 1
|
|
198
|
-
assert
|
|
205
|
+
assert viaQ(9007199254740993).unwrapOr(0) == 9007199254740993
|
|
199
|
-
assert
|
|
206
|
+
assert viaQFailing().unwrapOr(42) == 42
|
|
200
207
|
|
|
201
|
-
test "Float specials survive
|
|
208
|
+
test "Float specials survive a Result payload with ? and unwrapOr"
|
|
202
|
-
nan :=
|
|
209
|
+
nan := viaQ(0.0 / 0.0).unwrapOr(0.0)
|
|
203
210
|
assert nan != nan
|
|
204
|
-
assert 1.0 /
|
|
211
|
+
assert 1.0 / viaQ(-0.0).unwrapOr(1.0) < 0.0
|
|
205
|
-
assert
|
|
212
|
+
assert viaQ(5e-324).unwrapOr(0.0) == 5e-324
|
|
206
|
-
assert
|
|
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
|
|
229
|
+
assert outer.get(0).unwrapOr(inner2).length() == 2
|
|
223
|
-
assert
|
|
230
|
+
assert readDeep(outer, 0, 0, "ok").unwrapOr(0 - 1) == 7
|
|
224
|
-
assert
|
|
231
|
+
assert readDeep(outer, 0, 0, "nokey").unwrapOr(98) == 98
|
|
225
|
-
assert
|
|
232
|
+
assert readDeep(outer, 0, 1, "big").unwrapOr(0) == 9223372036854775807
|
|
226
|
-
assert
|
|
233
|
+
assert readDeep(outer, 1, 0, "big").unwrapOr(0) == 9223372036854775807
|
|
227
|
-
assert
|
|
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:
|
|
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:
|
|
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) ->
|
|
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) ->
|
|
15
|
+
fun fromJson(j: Json) -> Result[House, JsonError] =
|
|
14
|
-
name :=
|
|
16
|
+
name := Str.fromJson(j.field("name")?)?
|
|
15
|
-
rooms :=
|
|
17
|
+
rooms := Int.fromJson(j.field("rooms")?)?
|
|
16
|
-
tags :=
|
|
18
|
+
tags := tagsOf(j.field("tags")?)?
|
|
17
|
-
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) ->
|
|
22
|
+
fun tagsOf(j: Json) -> Result[Slice[Str], JsonError] = j.into()
|
|
21
23
|
|
|
22
|
-
fun annexOf(j: Json) ->
|
|
24
|
+
fun annexOf(j: Json) -> Result[Slice[House], JsonError] = j.into()
|
|
23
25
|
|
|
24
|
-
fun parseHouse(s: Str) ->
|
|
26
|
+
fun parseHouse(s: Str) -> Result[House, JsonError] = Json.decode(s)
|
|
25
27
|
|
|
26
28
|
fun errorOf(s: Str) -> Str =
|
|
27
|
-
parseHouse(s)
|
|
29
|
+
match parseHouse(s)
|
|
30
|
+
Ok(h) => "ok"
|
|
28
|
-
|
|
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:
|
|
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
|
|
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
|
|
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) ->
|
|
200
|
+
fun parseSmall(s: Str) -> Result[Int, SmallError] =
|
|
197
|
-
n := Int.parse(s)
|
|
201
|
+
n := Int.parse(s).mapErr(|e| NotNum)?
|
|
198
202
|
if n > 9
|
|
199
|
-
return
|
|
203
|
+
return Err(TooBig)
|
|
200
|
-
n
|
|
204
|
+
Ok(n)
|
|
201
205
|
|
|
202
206
|
fun describe(s: Str) -> Str =
|
|
203
|
-
|
|
207
|
+
match parseSmall(s)
|
|
204
|
-
|
|
208
|
+
Ok(n) => "ok {n}"
|
|
205
|
-
|
|
209
|
+
Err(TooBig) => "too big"
|
|
206
|
-
|
|
210
|
+
Err(NotNum) => "not a number"
|
|
207
|
-
|
|
211
|
+
|
|
208
|
-
|
|
209
|
-
fun firstSome(a: 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
|
|
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),
|
|
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,
|
|
333
|
+
assert pick(Dot, None, 11) == "big"
|
|
331
|
-
assert pick(Rect(w: 1.0, h: 1.0),
|
|
334
|
+
assert pick(Rect(w: 1.0, h: 1.0), None, 0) == "rect"
|
|
332
|
-
assert pick(Dot,
|
|
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(
|
|
357
|
+
assert firstSome(None, Some(7)) == 8
|
|
355
|
-
assert firstSome(2, 7) == 2
|
|
358
|
+
assert firstSome(Some(2), Some(7)) == 2
|
|
356
|
-
assert firstSome(
|
|
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 "
|
|
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(
|
|
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
|
|
163
|
+
assert Int.parse("123").unwrapOr(-1) == 123
|
|
162
|
-
assert
|
|
164
|
+
assert Int.parse("-45").unwrapOr(1) == -45
|
|
163
|
-
assert
|
|
165
|
+
assert Float.parse("3.25").unwrapOr(-1.0f) == 3.25f
|
|
164
|
-
assert
|
|
166
|
+
assert Int.parse("12x").unwrapOr(-1) == -1
|
|
165
|
-
assert
|
|
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) ->
|
|
63
|
+
fun down(n: Int) -> Result[Int, DownError] =
|
|
60
64
|
if n < 0
|
|
61
|
-
return
|
|
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 "
|
|
139
|
+
test "result_loop_million"
|
|
136
|
-
assert
|
|
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
|
|
156
|
+
assert [1, 2, 3].get(1).unwrapOr(-1) == 2
|
|
153
|
-
assert [1, 2, 3].get(5)
|
|
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
|
|
170
|
+
assert xs.get(0).unwrapOr(-1) == 3
|
|
167
|
-
assert
|
|
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
|
|
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]
|
|
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
|
|
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
|
|
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
|
|
54
|
+
return None
|
|
54
|
-
step := self.src.next()
|
|
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
|
|
73
|
+
return None
|
|
72
74
|
|
|
73
|
-
fun chain(c: Bool) -> Vec2
|
|
75
|
+
fun chain(c: Bool) -> Option[Vec2] =
|
|
74
|
-
v := opt(c)
|
|
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
|
-
|
|
163
|
+
enum S1 = E1 | E2
|
|
162
164
|
|
|
163
|
-
fun fail(n: Int) ->
|
|
165
|
+
fun fail(n: Int) -> Result[Int, S1] =
|
|
164
166
|
if n == 1
|
|
165
|
-
return
|
|
167
|
+
return Err(E1)
|
|
166
168
|
if n == 2
|
|
167
|
-
return
|
|
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, `
|
|
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
|
-
|
|
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) ->
|
|
203
|
+
fun parseLocal(n: Int) -> Result[Int, LocalError] =
|
|
202
204
|
if n < 0
|
|
203
|
-
return
|
|
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
|
|
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)
|
|
397
|
+
a := fail(3).unwrapOr(0)
|
|
396
398
|
assert a == 3
|
|
397
|
-
b := fail(1)
|
|
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
|
|
418
|
+
o: Option[Int] := Some(4)
|
|
417
|
-
assert
|
|
419
|
+
assert o.unwrapOr(0) == 4
|
|
418
420
|
|
|
419
|
-
test "
|
|
421
|
+
test "optional_local_none"
|
|
420
|
-
o: Int
|
|
422
|
+
o: Option[Int] := None
|
|
421
|
-
assert
|
|
423
|
+
assert o.unwrapOr(9) == 9
|
|
422
|
-
o = 5
|
|
424
|
+
o = Some(5)
|
|
423
|
-
assert
|
|
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 "
|
|
447
|
+
test "named_error_enum_local"
|
|
446
|
-
ok:
|
|
448
|
+
ok: Result[Int, LocalError] := parseLocal(4)
|
|
447
|
-
bad:
|
|
449
|
+
bad: Result[Int, LocalError] := parseLocal(-1)
|
|
448
|
-
assert
|
|
450
|
+
assert ok.unwrapOr(0) == 8
|
|
449
|
-
assert
|
|
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) ->
|
|
128
|
+
fun pos(n: Int) -> Result[Slice[Jv], PosError] =
|
|
125
129
|
if n < 0
|
|
126
|
-
return
|
|
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) ->
|
|
136
|
+
fun posLen(n: Int) -> Result[Int, PosError] =
|
|
133
|
-
xs :=
|
|
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 "
|
|
183
|
+
test "variants_with_result"
|
|
180
|
-
assert
|
|
184
|
+
assert posLen(3).unwrapOr(0) == 2
|
|
181
|
-
assert
|
|
185
|
+
assert posLen(-1).unwrapOr(0) == 0
|
|
182
|
-
ys := pos(3)
|
|
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:
|
|
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:
|
|
87
|
+
Forever(anim: Seq(steps: [
|
|
87
|
-
Par(steps:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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) ->
|
|
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 = "=") ->
|
|
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
|
|
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) ->
|
|
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
|
-
|
|
24
|
+
Err(BadBoolStr)
|
|
20
25
|
|
|
21
26
|
test "Bool.parse accepts true/false and rejects everything else"
|
|
22
|
-
assert Bool.parse("true")
|
|
27
|
+
assert Bool.parse("true").unwrapOr(false)
|
|
23
|
-
assert !
|
|
28
|
+
assert !Bool.parse("false").unwrapOr(true)
|
|
24
|
-
assert !
|
|
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
|
|
51
|
+
# Removes and returns the front element, or None if empty.
|
|
51
|
-
fun popFront(self) ->
|
|
52
|
+
fun popFront(self) -> Option[T] =
|
|
52
53
|
if self.size == 0
|
|
53
|
-
return
|
|
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
|
|
60
|
+
# Removes and returns the back element, or None if empty.
|
|
60
|
-
fun popBack(self) ->
|
|
61
|
+
fun popBack(self) -> Option[T] =
|
|
61
62
|
if self.size == 0
|
|
62
|
-
return
|
|
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
|
|
67
|
+
# The `i`'th element from the front, or None if out of bounds.
|
|
67
|
-
fun get(self, i: Int) ->
|
|
68
|
+
fun get(self, i: Int) -> Option[T] =
|
|
68
69
|
if i < 0 || i >= self.size
|
|
69
|
-
return
|
|
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
|
|
120
|
+
assert d.popFront().unwrapOr(-1) == -1
|
|
120
|
-
assert
|
|
121
|
+
assert d.popBack().unwrapOr(-1) == -1
|
|
121
|
-
assert
|
|
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) ->
|
|
32
|
+
fun parse(s: Str) -> Result[Float, NumError] =
|
|
31
33
|
len := s.length()
|
|
32
34
|
if len == 0
|
|
33
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
64
|
+
# The smallest element without removing it, or None if empty.
|
|
64
|
-
fun peek(self) ->
|
|
65
|
+
fun peek(self) -> Option[T] =
|
|
65
66
|
if self.items.length() == 0
|
|
66
|
-
return
|
|
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
|
|
70
|
+
# Removes and returns the smallest element, or None if empty.
|
|
70
|
-
fun pop(self) ->
|
|
71
|
+
fun pop(self) -> Option[T] =
|
|
71
72
|
n := self.items.length()
|
|
72
73
|
if n == 0
|
|
73
|
-
return
|
|
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
|
|
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
|
-
|
|
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) ->
|
|
38
|
+
fun json(self) -> Result[Json, JsonError] =
|
|
30
|
-
return
|
|
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) ->
|
|
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)
|
|
95
|
+
flag := parts.get(0).unwrapOr("")
|
|
87
96
|
if flag != "1"
|
|
88
|
-
return
|
|
97
|
+
return Err(HttpFailed)
|
|
89
|
-
|
|
98
|
+
match Int.parse(parts.get(1).unwrapOr(""))
|
|
90
|
-
|
|
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]) ->
|
|
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]) ->
|
|
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]) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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())
|
|
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) ->
|
|
21
|
+
fun parse(s: Str) -> Result[Int, NumError] =
|
|
17
22
|
len := s.length()
|
|
18
23
|
if len == 0
|
|
19
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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) ->
|
|
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
|
-
|
|
58
|
+
match parser.parseDocument()
|
|
59
|
+
Ok(v) => Parsed(value: v)
|
|
48
|
-
|
|
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) ->
|
|
78
|
+
fun decode(s: Str) -> Result[T, JsonError] =
|
|
68
|
-
j :=
|
|
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) ->
|
|
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) ->
|
|
86
|
+
fun decodeWith(s: Str, f: fn(Json) -> Result[T, JsonError]) -> Result[T, JsonError] =
|
|
76
|
-
j :=
|
|
87
|
+
j := Json.fromStr(s)?
|
|
77
88
|
return f(j)
|
|
78
89
|
|
|
79
|
-
fun field(self, name: Str) ->
|
|
90
|
+
fun field(self, name: Str) -> Result[Json, JsonError] =
|
|
80
|
-
m :=
|
|
91
|
+
m := self.asMap()?
|
|
81
|
-
|
|
92
|
+
match m.get(name)
|
|
93
|
+
Some(v) => Ok(v)
|
|
82
|
-
|
|
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
|
-
|
|
97
|
+
match self.asMap()
|
|
87
|
-
return null
|
|
88
|
-
|
|
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) ->
|
|
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) ->
|
|
121
|
+
fun asBool(self) -> Result[Bool, JsonError] =
|
|
111
122
|
match self
|
|
112
|
-
JsonBool(value) => value
|
|
123
|
+
JsonBool(value) => Ok(value)
|
|
113
|
-
_ =>
|
|
124
|
+
_ => Err(WrongJsonType)
|
|
114
125
|
|
|
115
|
-
fun asFloat(self) ->
|
|
126
|
+
fun asFloat(self) -> Result[Float, JsonError] =
|
|
116
127
|
match self
|
|
117
|
-
JsonNum(value) => value
|
|
128
|
+
JsonNum(value) => Ok(value)
|
|
118
|
-
_ =>
|
|
129
|
+
_ => Err(WrongJsonType)
|
|
119
130
|
|
|
120
|
-
fun asStr(self) ->
|
|
131
|
+
fun asStr(self) -> Result[Str, JsonError] =
|
|
121
132
|
match self
|
|
122
|
-
JsonStr(value) => value
|
|
133
|
+
JsonStr(value) => Ok(value)
|
|
123
|
-
_ =>
|
|
134
|
+
_ => Err(WrongJsonType)
|
|
124
135
|
|
|
125
|
-
fun asList(self) ->
|
|
136
|
+
fun asList(self) -> Result[Slice[Json], JsonError] =
|
|
126
137
|
match self
|
|
127
|
-
JsonArr(items) => items
|
|
138
|
+
JsonArr(items) => Ok(items)
|
|
128
|
-
_ =>
|
|
139
|
+
_ => Err(WrongJsonType)
|
|
129
140
|
|
|
130
|
-
fun asMap(self) ->
|
|
141
|
+
fun asMap(self) -> Result[Map[Str, Json], JsonError] =
|
|
131
142
|
match self
|
|
132
|
-
JsonObj(fields) => fields
|
|
143
|
+
JsonObj(fields) => Ok(fields)
|
|
133
|
-
_ =>
|
|
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) ->
|
|
244
|
+
fun parseDocument(self) -> Result[Json, JsonError] =
|
|
229
245
|
self.skipSpace()
|
|
230
|
-
v :=
|
|
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
|
|
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) ->
|
|
270
|
+
fun parseValue(self) -> Result[Json, JsonError] =
|
|
255
271
|
self.skipSpace()
|
|
256
272
|
c := self.peek()
|
|
257
273
|
if c == 34
|
|
258
|
-
str :=
|
|
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
|
|
289
|
+
return Err(InvalidJson)
|
|
274
290
|
|
|
275
|
-
fun parseLiteral(self, lit: Str, value: 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
|
|
295
|
+
return Err(InvalidJson)
|
|
280
296
|
self.pos = self.pos + n
|
|
281
|
-
return value
|
|
297
|
+
return Ok(value)
|
|
282
298
|
|
|
283
|
-
fun parseNumber(self) ->
|
|
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
|
|
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
|
|
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
|
|
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) ->
|
|
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)
|
|
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))
|
|
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()))
|
|
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) ->
|
|
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
|
|
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
|
-
|
|
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) ->
|
|
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
|
|
383
|
+
return Err(InvalidJson)
|
|
374
384
|
self.advance()
|
|
385
|
+
Ok(Unit)
|
|
375
386
|
|
|
376
|
-
fun parseHex4(self) ->
|
|
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
|
|
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) ->
|
|
404
|
+
fun parseUnicodeEscape(self, buf: Buffer) -> Result[Unit, JsonError] =
|
|
394
|
-
cp :=
|
|
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 :=
|
|
409
|
+
low := self.parseHex4()?
|
|
399
410
|
if low < 0xDC00 || low > 0xDFFF
|
|
400
411
|
self.note("invalid low surrogate")
|
|
401
|
-
return
|
|
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) ->
|
|
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 :=
|
|
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
|
|
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) ->
|
|
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
|
|
452
|
+
return Err(InvalidJson)
|
|
441
|
-
key :=
|
|
453
|
+
key := self.parseString()?
|
|
442
454
|
self.skipSpace()
|
|
443
455
|
if self.peek() != 58
|
|
444
456
|
self.note("expected ':'")
|
|
445
|
-
return
|
|
457
|
+
return Err(InvalidJson)
|
|
446
458
|
self.advance()
|
|
447
459
|
self.skipSpace()
|
|
448
|
-
v :=
|
|
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
|
|
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) ->
|
|
499
|
+
fun fromJson(j: Json) -> Result[Int, JsonError] =
|
|
488
|
-
f :=
|
|
500
|
+
f := j.asFloat()?
|
|
489
501
|
if f >= 9007199254740992.0f || f <= -9007199254740992.0f
|
|
490
|
-
return
|
|
502
|
+
return Err(WrongJsonType)
|
|
491
503
|
n := Int(f)
|
|
492
504
|
if Float(n) != f
|
|
493
|
-
return
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
532
|
+
fun fromJson(j: Json) -> Result[Slice[T], JsonError] =
|
|
521
|
-
items :=
|
|
533
|
+
items := j.asList()?
|
|
522
534
|
out := Slice[T]()
|
|
523
535
|
for x := range items
|
|
524
|
-
out.push(
|
|
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) ->
|
|
547
|
+
fun fromJson(j: Json) -> Result[Map[Str, V], JsonError] =
|
|
536
|
-
pairs :=
|
|
548
|
+
pairs := j.asMap()?
|
|
537
549
|
out: Map[Str, V] := Map.empty()
|
|
538
550
|
for e := range pairs.entries()
|
|
539
|
-
out.set(e.first,
|
|
551
|
+
out.set(e.first, V.fromJson(e.second)?)
|
|
540
|
-
return out
|
|
552
|
+
return Ok(out)
|
|
541
553
|
|
|
542
|
-
fun jsonTestDecodeInt(s: Str) ->
|
|
554
|
+
fun jsonTestDecodeInt(s: Str) -> Result[Int, JsonError] = Json.decode(s)
|
|
543
555
|
|
|
544
|
-
fun jsonTestDecodeNested(s: Str) ->
|
|
556
|
+
fun jsonTestDecodeNested(s: Str) -> Result[Slice[Slice[Int]], JsonError] = Json.decode(s)
|
|
545
557
|
|
|
546
|
-
fun jsonTestDecodeMap(s: Str) ->
|
|
558
|
+
fun jsonTestDecodeMap(s: Str) -> Result[Map[Str, Int], JsonError] = Json.decode(s)
|
|
547
559
|
|
|
548
|
-
fun jsonTestDecodeStr(s: Str) ->
|
|
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) ->
|
|
568
|
+
fun fromJson(j: Json) -> Result[JsonTestHouse, JsonError] =
|
|
557
|
-
name :=
|
|
569
|
+
name := Str.fromJson(j.field("name")?)?
|
|
558
|
-
rooms :=
|
|
570
|
+
rooms := Int.fromJson(j.field("rooms")?)?
|
|
559
|
-
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) ->
|
|
574
|
+
fun jsonTestKidsOf(j: Json) -> Result[Slice[JsonTestHouse], JsonError] = j.into()
|
|
563
575
|
|
|
564
576
|
fun jsonTestHouseError(s: Str) -> Str =
|
|
565
|
-
jsonTestDecodeHouse(s)
|
|
577
|
+
match jsonTestDecodeHouse(s)
|
|
578
|
+
Ok(h) => "ok"
|
|
566
|
-
|
|
579
|
+
Err(e) => e.name()
|
|
567
|
-
"ok"
|
|
568
580
|
|
|
569
|
-
fun jsonTestDecodeHouse(s: Str) ->
|
|
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\": }}")
|
|
619
|
+
bad := Json.fromStr("{{\"a\": }}").unwrapOr(JsonNull)
|
|
608
620
|
assert bad.isNull()
|
|
609
|
-
assert
|
|
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
|
|
705
|
+
assert o.field("x").unwrapOr(JsonNull).isNull()
|
|
694
|
-
assert
|
|
706
|
+
assert l0.field("x").unwrapOr(JsonNull).isNull()
|
|
695
|
-
assert o.optField("a")
|
|
707
|
+
assert o.optField("a").isSome()
|
|
696
|
-
assert o.optField("x")
|
|
708
|
+
assert o.optField("x").isNone()
|
|
697
|
-
assert l0.optField("a")
|
|
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
|
|
713
|
+
assert jsonTestDecodeInt("3.5").unwrapOr(-1) == -1
|
|
702
|
-
assert
|
|
714
|
+
assert jsonTestDecodeInt("1e17").unwrapOr(-1) == -1
|
|
703
|
-
assert
|
|
715
|
+
assert jsonTestDecodeInt("9007199254740993").unwrapOr(-1) == -1
|
|
704
|
-
assert
|
|
716
|
+
assert jsonTestDecodeInt("[1]").unwrapOr(-1) == -1
|
|
705
717
|
assert jsonTestDecodeInt("-7")! == -7
|
|
706
|
-
assert
|
|
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
|
|
726
|
+
assert jsonTestDecodeNested("[[1,\"x\"]]").unwrapOr(Slice[Slice[Int]]()).length() == 0
|
|
715
727
|
|
|
716
728
|
test "map_non_object_rejected"
|
|
717
|
-
assert
|
|
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
|
|
750
|
+
assert jsonTestDecodeHouse("{{\"name\":\"x\"}}").unwrapOr(JsonTestHouse(name: "E", rooms: 0, kids: Slice[JsonTestHouse]())).name == "E"
|
|
739
|
-
assert
|
|
751
|
+
assert jsonTestDecodeHouse("{{oops").unwrapOr(JsonTestHouse(name: "E", rooms: 0, kids: Slice[JsonTestHouse]())).name == "E"
|
|
740
|
-
assert
|
|
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
|
|
29
|
+
# The element at index i, or None when out of range.
|
|
29
|
-
fun get(self, i: Int) -> T
|
|
30
|
+
fun get(self, i: Int) -> Option[T] = match self
|
|
30
|
-
Nil =>
|
|
31
|
+
Nil => None
|
|
31
|
-
Cons(h, t) => if i < 0 then
|
|
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)
|
|
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
|
|
61
|
+
# The iterator step: `Some(Pair(head, tail))`, or None once the list is done.
|
|
61
|
-
fun next(self) -> Pair[T, List[T]]
|
|
62
|
+
fun next(self) -> Option[Pair[T, List[T]]] = match self
|
|
62
|
-
Nil =>
|
|
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
|
|
78
|
+
# Gets the value for `k`, or None if absent.
|
|
78
|
-
fun get(self, k: K) ->
|
|
79
|
+
fun get(self, k: K) -> Option[V] =
|
|
79
80
|
slot := self.findSlot(k, self.hashOf(k))
|
|
80
81
|
if slot < 0
|
|
81
|
-
return
|
|
82
|
+
return None
|
|
82
|
-
|
|
83
|
+
Some(self.vals.get(self.index.get(slot) - 2))
|
|
83
84
|
|
|
84
|
-
# Gets the value for `k`, or
|
|
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)
|
|
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
|
-
|
|
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) ->
|
|
59
|
+
fun readFile(io: Io, path: Str) -> Result[Str, IoError] =
|
|
52
60
|
if !exists(io, path)
|
|
53
|
-
return
|
|
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) ->
|
|
65
|
+
fun writeFile(io: Io, path: Str, data: Str) -> Result[Unit, IoError] =
|
|
58
66
|
if io.rawWriteFile(path, data) != 1
|
|
59
|
-
return
|
|
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) ->
|
|
73
|
+
fun mkdir(io: Io, path: Str) -> Result[Unit, IoError] =
|
|
65
74
|
if io.rawMkdir(path) != 1
|
|
66
|
-
return
|
|
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) ->
|
|
79
|
+
fun remove(io: Io, path: Str) -> Result[Unit, IoError] =
|
|
70
80
|
if !exists(io, path)
|
|
71
|
-
return
|
|
81
|
+
return Err(NoSuchPath)
|
|
72
82
|
if io.rawRemove(path) != 1
|
|
73
|
-
return
|
|
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) ->
|
|
96
|
+
fun appendFile(io: Io, path: Str, data: Str) -> Result[Unit, IoError] =
|
|
86
97
|
if exists(io, path)
|
|
87
|
-
existing :=
|
|
98
|
+
existing := readFile(io, path)?
|
|
88
|
-
|
|
99
|
+
return writeFile(io, path, existing + data)
|
|
89
|
-
return
|
|
90
|
-
|
|
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) ->
|
|
104
|
+
fun chmod(io: Io, path: Str, mode: Int) -> Result[Unit, IoError] =
|
|
95
105
|
if io.rawChmod(path, mode) != 1
|
|
96
|
-
return
|
|
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) ->
|
|
114
|
+
fun copyFile(io: Io, src: Str, dest: Str) -> Result[Unit, IoError] =
|
|
104
|
-
data :=
|
|
115
|
+
data := readFile(io, src)?
|
|
105
|
-
|
|
116
|
+
writeFile(io, dest, data)
|
|
106
117
|
|
|
107
|
-
fun cp(io: Io, src: Str, dest: Str) ->
|
|
118
|
+
fun cp(io: Io, src: Str, dest: Str) -> Result[Unit, IoError] =
|
|
108
|
-
|
|
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) ->
|
|
124
|
+
fun lchmod(io: Io, path: Str, mode: Int) -> Result[Unit, IoError] =
|
|
114
|
-
|
|
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) ->
|
|
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
|
|
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) ->
|
|
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
|
-
|
|
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) ->
|
|
167
|
+
fun readdir(io: Io, path: Str) -> Result[Slice[Str], IoError] =
|
|
156
168
|
if !exists(io, path)
|
|
157
|
-
return
|
|
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) ->
|
|
180
|
+
fun readlink(io: Io, path: Str) -> Result[Str, IoError] =
|
|
169
181
|
if !exists(io, path)
|
|
170
|
-
return
|
|
182
|
+
return Err(NoSuchPath)
|
|
171
|
-
return io.rawReadlink(path)
|
|
183
|
+
return Ok(io.rawReadlink(path))
|
|
172
184
|
|
|
173
|
-
fun realpath(io: Io, path: Str) ->
|
|
185
|
+
fun realpath(io: Io, path: Str) -> Result[Str, IoError] =
|
|
174
186
|
if !exists(io, path)
|
|
175
|
-
return
|
|
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) ->
|
|
190
|
+
fun rename(io: Io, old_path: Str, new_path: Str) -> Result[Unit, IoError] =
|
|
179
191
|
if !exists(io, old_path)
|
|
180
|
-
return
|
|
192
|
+
return Err(NoSuchPath)
|
|
181
193
|
if io.rawRename(old_path, new_path) != 1
|
|
182
|
-
return
|
|
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) ->
|
|
198
|
+
fun rmdir(io: Io, path: Str) -> Result[Unit, IoError] =
|
|
186
199
|
if !exists(io, path)
|
|
187
|
-
return
|
|
200
|
+
return Err(NoSuchPath)
|
|
188
201
|
if io.rawRemoveDir(path) != 1
|
|
189
|
-
return
|
|
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) ->
|
|
213
|
+
fun symlink(io: Io, target: Str, path: Str) -> Result[Unit, IoError] =
|
|
200
214
|
if io.rawSymlink(target, path) != 1
|
|
201
|
-
return
|
|
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) ->
|
|
220
|
+
fun truncate(io: Io, path: Str, len: Int) -> Result[Unit, IoError] =
|
|
206
221
|
if !exists(io, path)
|
|
207
|
-
return
|
|
222
|
+
return Err(NoSuchPath)
|
|
208
223
|
if io.rawTruncate(path, len) != 1
|
|
209
|
-
return
|
|
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
|
|
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")
|
|
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")
|
|
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]
|
|
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) ->
|
|
29
|
+
fun get(self, i: Int) -> Option[T] =
|
|
29
30
|
if i < 0 || i >= self.size
|
|
30
|
-
return
|
|
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
|
|
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) ->
|
|
35
|
+
fun set(self, i: Int, v: T) -> Option[T] =
|
|
35
36
|
if i < 0 || i >= self.size
|
|
36
|
-
return
|
|
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
|
|
115
|
+
# removes and returns the last element, or None if empty
|
|
115
|
-
fun pop(self) ->
|
|
116
|
+
fun pop(self) -> Option[T] =
|
|
116
117
|
if self.size == 0
|
|
117
|
-
return
|
|
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
|
|
174
|
+
# Returns the first element equal to `search`, or None.
|
|
174
|
-
fun findValue(self, search: 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
|
|
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) ->
|
|
193
|
+
fun first(self) -> Option[T] =
|
|
193
194
|
if self.size == 0
|
|
194
|
-
return
|
|
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
|
|
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
|
|
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) ->
|
|
205
|
+
fun last(self) -> Option[T] =
|
|
205
206
|
if self.size == 0
|
|
206
|
-
return
|
|
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
|
|
226
|
+
# Returns one uniformly random element and the advanced generator, or None
|
|
226
|
-
#
|
|
227
|
+
# if `self` is empty.
|
|
227
|
-
fun sample(self, rng: Random) ->
|
|
228
|
+
fun sample(self, rng: Random) -> Option[Rolled[T]] =
|
|
228
229
|
if self.size == 0
|
|
229
|
-
return
|
|
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
|
|
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
|
|
367
|
+
assert sorted.get(0).unwrapOr(-1000) == 1
|
|
367
|
-
assert
|
|
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
|
|
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
|
|
416
|
+
assert l.pop().unwrapOr(-1) == -1
|
|
416
|
-
assert
|
|
417
|
+
assert l.get(0).unwrapOr(-1000) == -1000
|
|
417
418
|
e := l.get(0)
|
|
418
|
-
assert
|
|
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
|
|
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]`/`
|
|
23
|
+
# type (`Slice[T]`/`Result[T, E]`) whose OWN generic param is
|
|
22
|
-
# nested inside the param type.
|
|
24
|
+
# nested inside the param type. A Result is inspected with
|
|
23
|
-
# `
|
|
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:
|
|
61
|
+
fun expectSuccess(r: Result[T, E]) -> Bool = match r
|
|
59
|
-
|
|
62
|
+
Ok(_) => true
|
|
60
|
-
|
|
63
|
+
Err(_) => false
|
|
61
|
-
true
|
|
62
64
|
|
|
63
|
-
fun expectFailure(r:
|
|
65
|
+
fun expectFailure(r: Result[T, E]) -> Bool = match r
|
|
64
|
-
|
|
66
|
+
Ok(_) => false
|
|
65
|
-
|
|
67
|
+
Err(_) => true
|
|
66
|
-
false
|
|
67
68
|
|
|
68
69
|
# True when `r` failed with the error named `name`.
|
|
69
|
-
fun expectFailureNamed(r:
|
|
70
|
+
fun expectFailureNamed(r: Result[T, E], name: Str) -> Bool = match r
|
|
70
|
-
|
|
71
|
+
Ok(_) => false
|
|
71
|
-
|
|
72
|
+
Err(e) => e.name() == name
|
|
73
|
+
|
|
74
|
+
# The error enum the tests below use.
|
|
75
|
+
enum Oops =
|
|
76
|
+
| Bad
|
|
72
|
-
|
|
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 "
|
|
99
|
+
test "Results are inspected with unwrapOr and !"
|
|
91
|
-
ok:
|
|
100
|
+
ok: Result[Int, Oops] := Ok(5)
|
|
92
|
-
bad:
|
|
101
|
+
bad: Result[Int, Oops] := Err(Bad)
|
|
93
102
|
assert ok! == 5
|
|
94
|
-
assert
|
|
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:
|
|
116
|
+
ok: Result[Int, Oops] := Ok(5)
|
|
108
|
-
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) ->
|
|
53
|
+
fun fromStr(s: Str) -> Result[Uuid, UuidError] =
|
|
49
54
|
if s.length() != 36
|
|
50
|
-
return
|
|
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
|
|
61
|
+
return Err(InvalidUuid)
|
|
57
62
|
else
|
|
58
63
|
if hexValue(s.byteAt(i)) < 0
|
|
59
|
-
return
|
|
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
|
-
|
|
69
|
+
Ok(_) => true
|
|
65
|
-
|
|
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
|
|
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
|
|
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 "
|
|
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()
|
|
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
|
|
68
|
+
# The pointer position, or None for a key.
|
|
68
|
-
fun position(self) -> Vec2
|
|
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
|
-
_ =>
|
|
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()
|
|
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()
|
|
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
|
|
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
|
|
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(
|
|
119
|
+
self.hitIdWith(None, x, y)
|
|
119
120
|
|
|
120
|
-
# The message of the first Clickable with `id`, depth first, or
|
|
121
|
+
# The message of the first Clickable with `id`, depth first, or None.
|
|
121
|
-
fun msgOf(self, id: Int) -> M
|
|
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
|
-
_ =>
|
|
126
|
+
_ => None
|
|
126
127
|
|
|
127
|
-
fun msgOfKids(kids: List[SceneNode[M]], id: Int) -> M
|
|
128
|
+
fun msgOfKids(kids: List[SceneNode[M]], id: Int) -> Option[M] = match kids
|
|
128
|
-
Nil =>
|
|
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
|
|
189
|
+
# The bounds of the node drawn under `m`, or None when it has no extent.
|
|
189
|
-
fun boundsWith(self, m: Affine) -> Rect
|
|
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()),
|
|
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
|
|
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
|
|
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]
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
|
237
|
+
fun hitKids(kids: List[SceneNode[M]], i: Int, px: Float, py: Float) -> Option[Slice[Int]] = match kids
|
|
237
|
-
Nil =>
|
|
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
|
-
|
|
246
|
+
None
|
|
246
247
|
|
|
247
|
-
# The id of the innermost Clickable node whose subtree is hit at (px, py), or
|
|
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 (
|
|
249
|
+
# `outer` is the id of the nearest enclosing tag (None at the root).
|
|
249
|
-
fun hitIdWith(self, outer: Int
|
|
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
|
-
|
|
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)
|
|
260
|
+
if self.hitWith(px, py).isSome()
|
|
260
261
|
outer
|
|
261
262
|
else
|
|
262
|
-
|
|
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
|
|
266
|
+
fun hitIdKids(kids: List[SceneNode[M]], outer: Option[Int], px: Float, py: Float) -> Option[Int] = match kids
|
|
266
|
-
Nil =>
|
|
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
|
-
|
|
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)
|
|
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
|
|
384
|
+
assert g.msgOf(4).unwrapOr(-1) == 40
|
|
384
|
-
assert g.msgOf(5)
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
47
|
+
fun step(ui: UiState, input: Input, hit: Option[Int]) -> UiStep =
|
|
48
|
+
maybePos := input.position()
|
|
49
|
+
if maybePos.isNone()
|
|
47
|
-
|
|
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
|
|
54
|
+
cap: Option[Int] := ui.captured
|
|
51
55
|
if !ui.down
|
|
52
|
-
cap =
|
|
56
|
+
cap = None
|
|
53
|
-
hot: Int
|
|
57
|
+
hot: Option[Int] := ui.hot
|
|
54
58
|
down := ui.down
|
|
55
59
|
match input
|
|
56
60
|
PointerDown(_, _) =>
|
|
57
|
-
cap =
|
|
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
|
|
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 =
|
|
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),
|
|
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),
|
|
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),
|
|
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:
|
|
188
|
+
stale := UiState(x: 0.0, y: 0.0, down: false, hot: None, captured: Some(5))
|
|
185
|
-
s := Ui.step(stale, uiMove(50.0),
|
|
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),
|
|
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
|
|
224
|
+
assert ms.get(0).unwrapOr(-1) == 40
|
|
221
|
-
assert
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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(
|
|
601
|
+
assert same(k.parent.unwrap(), b)
|
|
601
602
|
b.remove(k)
|
|
602
603
|
assert b.children.length() == 0
|
|
603
|
-
assert k.parent
|
|
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
|
|
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(
|
|
664
|
+
assert same(stage.hit(90.0, 30.0).unwrap(), top)
|
|
664
|
-
assert same(
|
|
665
|
+
assert same(stage.hit(70.0, 10.0).unwrap(), low)
|
|
665
|
-
assert same(
|
|
666
|
+
assert same(stage.hit(60.0, 30.0).unwrap(), low)
|
|
666
|
-
assert stage.hit(10.0, 10.0)
|
|
667
|
+
assert stage.hit(10.0, 10.0).isNone()
|
|
667
|
-
assert stage.hit(150.0, 30.0)
|
|
668
|
+
assert stage.hit(150.0, 30.0).isNone()
|
|
668
669
|
top.visible = false
|
|
669
|
-
assert same(
|
|
670
|
+
assert same(stage.hit(90.0, 30.0).unwrap(), low)
|
|
670
671
|
g.visible = false
|
|
671
|
-
assert stage.hit(90.0, 30.0)
|
|
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(
|
|
682
|
+
assert same(stage.hit(10.0, 10.0).unwrap(), a)
|
|
682
|
-
assert same(
|
|
683
|
+
assert same(stage.hit(30.0, 5.0).unwrap(), inner)
|
|
683
|
-
assert same(
|
|
684
|
+
assert same(stage.hit(60.0, 40.0).unwrap(), b)
|
|
684
|
-
assert same(
|
|
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(
|
|
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)
|
|
808
|
+
assert stage.hit(107.0, 7.0).isNone()
|
|
808
809
|
k.scaleX = 4.0
|
|
809
810
|
k.originX = 10.0
|
|
810
|
-
assert same(
|
|
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)
|
|
815
|
+
assert stage.hit(107.0, 7.0).isNone()
|
|
815
816
|
stage.root.clearCullingArea()
|
|
816
|
-
assert same(
|
|
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`,
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
_ =>
|
|
109
|
+
_ => None
|
|
109
110
|
|
|
110
|
-
# The actor of a Custom node, or
|
|
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
|
-
_ =>
|
|
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
|
|
125
|
+
return None
|
|
125
126
|
m := parent * self.localMatrix()
|
|
126
127
|
inv := m.invert()
|
|
127
|
-
if inv
|
|
128
|
+
if inv.isNone()
|
|
128
|
-
return
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
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]
|
|
392
|
+
fun ttNoPath(r: Option[Slice[Int]]) -> Bool =
|
|
392
|
-
r
|
|
393
|
+
r.isNone()
|
|
393
394
|
|
|
394
|
-
fun ttPathEq(r: Slice[Int]
|
|
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()
|
|
559
|
+
assert s.children().isNone()
|
|
559
|
-
assert Node.group(2, 0.0, 0.0, 1.0, 1.0, [s].toSlice()).children()
|
|
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
|
|
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:
|
|
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
|
-
|
|
60
|
+
return StageHandled(stage: self, events: none, keys: Slice[Input]())
|
|
61
|
+
q := inv.unwrap()
|
|
58
62
|
world := e.through(q)
|
|
59
|
-
at := world.position()
|
|
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
|
|
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:
|
|
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,
|
|
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,
|
|
90
|
+
return self.deliver(p, world, true, None)
|
|
87
91
|
_ => 0
|
|
88
|
-
StageHandled(stage: self.withCapture(
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
278
|
+
assert hit.isSome()
|
|
275
|
-
h := Stage(root: t, camera: Affine.Identity, captured:
|
|
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:
|
|
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
|
|
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
|
|
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)
|
|
1378
|
+
a := l.get(0).unwrapOr(false)
|
|
1378
|
-
b := l.get(1)
|
|
1379
|
+
b := l.get(1).unwrapOr(true)
|
|
1379
|
-
c := l.get(2)
|
|
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
|
|
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) ->
|
|
1421
|
+
fun yes(ok: Bool) -> Result[Bool, NopeError] =
|
|
1418
1422
|
if ok
|
|
1419
|
-
true
|
|
1423
|
+
Ok(true)
|
|
1420
1424
|
else
|
|
1421
|
-
return
|
|
1425
|
+
return Err(Nope)
|
|
1422
1426
|
|
|
1423
|
-
fun chain(ok: Bool) ->
|
|
1427
|
+
fun chain(ok: Bool) -> Result[Bool, NopeError] =
|
|
1424
|
-
v :=
|
|
1428
|
+
v := yes(ok)?
|
|
1425
|
-
!v
|
|
1429
|
+
Ok(!v)
|
|
1426
1430
|
|
|
1427
1431
|
fun main() -> Int =
|
|
1428
|
-
a := chain(true)
|
|
1432
|
+
a := chain(true).unwrapOr(true)
|
|
1429
|
-
b := chain(false)
|
|
1433
|
+
b := chain(false).unwrapOr(true)
|
|
1430
|
-
p := Bool.parse(\"true\")
|
|
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
|
|
2217
|
+
fn bang_on_none_crashes_with_unwrap_message() {
|
|
2214
|
-
//
|
|
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("
|
|
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
|
|
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}
|
|
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
|
|
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
|
|
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
|
|
12
|
+
fn closure_returning_none_into_optional() {
|
|
13
|
-
let src = "import std/Int\n\nfun ap(f: fn() -> Int
|
|
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
|
|
19
|
-
let src = "import std/Int\nimport std/Str\n\nfun s(f: fn(Int) -> Str
|
|
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
|
|
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
|
|
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)
|
|
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) ->
|
|
87
|
+
fun parseIt(s: Str) -> Result[T, NumError] =
|
|
88
88
|
T.parse(s)
|
|
89
89
|
|
|
90
90
|
fun main() -> Int =
|
|
91
|
-
r:
|
|
91
|
+
r: Result[Int, NumError] := parseIt(\"41\")
|
|
92
|
-
n := r
|
|
92
|
+
n := r.unwrapOr(0)
|
|
93
|
-
return 0
|
|
94
93
|
j := n.toJson()
|
|
95
94
|
return n + 1
|
|
96
95
|
";
|