plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
e04834f
— Peter John
2026-11-03T18:37:07+05:30
Unboxed variants, enum init, List -> Slice, std cons List
- README.md +107 -41
- docs/plans/2026-11-03-unboxed-variants.benchmark.md +23 -0
- docs/plans/2026-11-03-unboxed-variants.md +174 -0
- docs/plans/2026-11-03-unboxed-variants.progress.md +41 -0
- docs/plans/2026-11-03-unboxed-variants.report.json +363 -0
- docs/plans/2026-11-03-unboxed-variants.report.md +53 -0
- docs/plans/2026-11-03-unboxed-variants.review.md +69 -0
- plum-checker/src/lib.rs +17 -4
- plum-checker/src/ownership.rs +2 -2
- plum-checker/tests/checker_tests.rs +16 -16
- plum-checker/tests/inline_variants_tests.rs +1 -1
- plum-checker/tests/ownership_flow_test.rs +16 -8
- plum-checker/tests/ownership_moves_test.rs +1 -1
- plum-checker/tests/types_tests.rs +5 -5
- plum-checker/tests/unions_tests.rs +1 -1
- plum-core/src/forms.rs +11 -5
- plum-core/src/loader.rs +1 -1
- plum-core/tests/formatter_test.rs +8 -8
- plum-core/tests/inline_variants_test.rs +1 -1
- plum-core/tests/parser_test.rs +7 -7
- plum-core/tests/record_init_test.rs +2 -2
- plum-core/tests/traits_test.rs +2 -2
- plum-core/tests/trmc_test.rs +3 -3
- plum-core/tests/union_lower_test.rs +1 -1
- plum-core/tests/unions_test.rs +2 -2
- plum-examples/annotated_locals.plum +2 -2
- plum-examples/cons_list.plum +70 -0
- plum-examples/constants.plum +2 -2
- plum-examples/error_propagation.plum +17 -17
- plum-examples/forms.plum +16 -16
- plum-examples/functional.plum +91 -90
- plum-examples/game.plum +4 -4
- plum-examples/game_loop.plum +6 -6
- plum-examples/game_pure_scene.plum +20 -20
- plum-examples/game_scene.plum +6 -6
- plum-examples/game_state_view.plum +50 -51
- plum-examples/generics.plum +21 -21
- plum-examples/json.plum +7 -7
- plum-examples/loops.plum +10 -10
- plum-examples/optionals.plum +5 -5
- plum-examples/own_list.plum +5 -5
- plum-examples/scene.plum +7 -7
- plum-examples/seq.plum +19 -19
- plum-examples/strings.plum +2 -2
- plum-examples/struct_arrays.plum +8 -8
- plum-examples/struct_union_members.plum +4 -4
- plum-examples/structs_records.plum +1 -1
- plum-examples/tail_calls.plum +6 -6
- plum-examples/trait_scene.plum +4 -4
- plum-examples/traits.plum +2 -2
- plum-examples/unboxed_variants.plum +337 -0
- plum-examples/unions.plum +13 -13
- plum-game/web-test/fixtures/stress_pure_scene.plum +8 -8
- plum-game/web-test/fixtures/stress_scene.plum +1 -1
- plum-game/web-test/fixtures/stress_state_view.plum +17 -17
- plum-game/web-test/fixtures/stress_state_view_deep.plum +25 -25
- plum-game/web-test/fixtures/stress_world.plum +1 -1
- plum-lsp/src/main.rs +4 -4
- plum-std/Array.plum +2 -2
- plum-std/Deque.plum +1 -1
- plum-std/Heap.plum +4 -4
- plum-std/Http.plum +1 -1
- plum-std/Json.plum +30 -30
- plum-std/List.plum +136 -0
- plum-std/Map.plum +1 -1
- plum-std/Os.plum +3 -3
- plum-std/Seq.plum +16 -16
- plum-std/Set.plum +3 -3
- plum-std/Slice.plum +44 -44
- plum-std/Str.plum +5 -5
- plum-std/Testing.plum +16 -16
- plum-std/game/Actions.plum +16 -16
- plum-std/game/Picture.plum +25 -25
- plum-std/game/PureScene.plum +16 -16
- plum-std/game/Scene.plum +10 -10
- plum-std/game/SceneInput.plum +18 -18
- plum-std/game/SceneTree.plum +32 -32
- plum-wasm-codegen/src/lib.rs +137 -3
- plum-wasm-codegen/tests/codegen_tests.rs +41 -41
- plum-wasm-codegen/tests/enum_init_e2e_test.rs +148 -0
- plum-wasm-codegen/tests/examples_test.rs +3 -3
- plum-wasm-codegen/tests/inline_variants_e2e_test.rs +1 -1
- plum-wasm-codegen/tests/loop_break_e2e_test.rs +1 -1
- plum-wasm-codegen/tests/own_inplace_e2e_test.rs +1 -1
- plum-wasm-codegen/tests/record_embed_e2e_test.rs +1 -1
- plum-wasm-codegen/tests/record_init_e2e_test.rs +3 -3
- plum-wasm-codegen/tests/tail_calls_e2e_test.rs +1 -1
- plum-wasm-codegen/tests/traits_e2e_test.rs +11 -11
- plum-wasm-codegen/tests/type_param_static_test.rs +6 -6
- plum-wasm-codegen/tests/unboxed_variants_e2e_test.rs +252 -0
- plum-wasm-codegen/tests/unions_e2e_test.rs +5 -5
README.md
CHANGED
|
@@ -13,7 +13,7 @@ import std/Os
|
|
|
13
13
|
import std/Str
|
|
14
14
|
import std/Int
|
|
15
15
|
import std/Float
|
|
16
|
-
import std/
|
|
16
|
+
import std/Slice
|
|
17
17
|
import std/Seq
|
|
18
18
|
|
|
19
19
|
error ParseError = BadNum | Negative # a named error set
|
|
@@ -48,7 +48,7 @@ fun double(s: Str) -> error{BadNum, Negative}!Int =
|
|
|
48
48
|
n := try parse(s) # `try` propagates the error
|
|
49
49
|
n * 2
|
|
50
50
|
|
|
51
|
-
fun firstEven(xs:
|
|
51
|
+
fun firstEven(xs: Slice[Int]) -> Int? = # `T?` is a T or null
|
|
52
52
|
for x := range xs.find() # a Seq terminal; the body is the predicate
|
|
53
53
|
x % 2 == 0
|
|
54
54
|
|
|
@@ -56,9 +56,9 @@ fun main() =
|
|
|
56
56
|
total := 0
|
|
57
57
|
for i := range 10, if i % 2 == 0 # range, with a `, if` filter clause
|
|
58
58
|
total = total + i
|
|
59
|
-
big := for x := range
|
|
59
|
+
big := for x := range Slice(1, 5, 9).map(), if x > 2
|
|
60
60
|
x * 10 # the body is the mapped value
|
|
61
|
-
e := firstEven(
|
|
61
|
+
e := firstEven(Slice(1, 3, 4)) ?: -1 # `?:` supplies a default for null
|
|
62
62
|
n := parse("42") catch 0 # `catch` supplies a default for an error
|
|
63
63
|
pos := Vec2.Zero + Vec2(x: 1.0, y: 2.0)
|
|
64
64
|
printLn("{total} {big.length()} {e} {n} {pos.y} {Shape.SidesMax}")
|
|
@@ -67,7 +67,7 @@ test "glance"
|
|
|
67
67
|
assert Rect(w: 2.0, h: 3.0).area() == 6.0
|
|
68
68
|
assert {double("21") catch 0} == 42
|
|
69
69
|
assert {parse("-1") catch 7} == 7
|
|
70
|
-
assert firstEven(
|
|
70
|
+
assert firstEven(Slice(1, 3)) == null
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
| You want | Write |
|
|
@@ -99,7 +99,7 @@ Discarding the result of a call is a compile error, so `scores.push(3)` on a typ
|
|
|
99
99
|
|
|
100
100
|
```plum
|
|
101
101
|
fun main() =
|
|
102
|
-
list :=
|
|
102
|
+
list := Slice(1, 2, 3)
|
|
103
103
|
_ := list.pop()! # evaluate, drop the removed element
|
|
104
104
|
list.push(4) # fine: push returns Unit
|
|
105
105
|
```
|
|
@@ -193,7 +193,7 @@ module basics
|
|
|
193
193
|
import std/Os
|
|
194
194
|
```
|
|
195
195
|
|
|
196
|
-
Nothing is implicit: `Int`, `Float`, `Str`, `
|
|
196
|
+
Nothing is implicit: `Int`, `Float`, `Str`, `Slice`, ... are available once the file imports `std/Int`, `std/Float`, `std/Str`, `std/Slice`, ...
|
|
197
197
|
|
|
198
198
|
## Literals
|
|
199
199
|
|
|
@@ -297,7 +297,7 @@ x = 2
|
|
|
297
297
|
|
|
298
298
|
```plum
|
|
299
299
|
s: Shape := Circle(r: 2.0)
|
|
300
|
-
xs:
|
|
300
|
+
xs: Slice[Shape] := Slice[Shape]()
|
|
301
301
|
o: Int? := null
|
|
302
302
|
s = Rect(w: 1.0, h: 2.0) # checked, and converted, against Shape
|
|
303
303
|
```
|
|
@@ -449,11 +449,11 @@ u2 := u
|
|
|
449
449
|
u2.age = 4 # u.age is now 4: a record is a heap object, shared on assignment
|
|
450
450
|
```
|
|
451
451
|
|
|
452
|
-
A `struct` has no generics; its fields are only `Int`, `Float`, `Bool`, `Byte` or other structs, and a struct can't contain itself. Values are copied on assignment and when passed, and are boxed inside `T?` and other generic slots. Arrays of structs (`Array[Vec2]`, and so `
|
|
452
|
+
A `struct` has no generics; its fields are only `Int`, `Float`, `Bool`, `Byte` or other structs, and a struct can't contain itself. Values are copied on assignment and when passed, and are boxed inside `T?` and other generic slots. Arrays of structs (`Array[Vec2]`, and so `Slice[Vec2]` and `Grid[Tile]`) are stored field by field, one native array per field, with no per-element allocation. A `record` may have any field types and may be generic (`record Pair[K, V](key: K, val: V)`). Methods and `match` work as for any enum. See [`plum-examples/structs_records.plum`](plum-examples/structs_records.plum).
|
|
453
453
|
|
|
454
454
|
### Enums
|
|
455
455
|
|
|
456
|
-
An `enum` lists the types it can hold. A member is a declared `struct` or `record`, a built-in (`Int`, `Float`, `Bool`, `Byte`), a std type (`Str`, `
|
|
456
|
+
An `enum` lists the types it can hold. A member is a declared `struct` or `record`, a built-in (`Int`, `Float`, `Bool`, `Byte`), a std type (`Str`, `Slice[Json]`, `Map[Str, Json]`), or a bare name that is not a declared type, which becomes a payload-less tag. A short list fits on one line; a longer one, or one with methods, goes on indented `|` lines. Every member other than a tag is an existing type, and the enum adds no new constructor for it:
|
|
457
457
|
|
|
458
458
|
```plum
|
|
459
459
|
struct Circle(r: Float) =
|
|
@@ -476,7 +476,7 @@ enum Json =
|
|
|
476
476
|
| Bool
|
|
477
477
|
| Float
|
|
478
478
|
| Str
|
|
479
|
-
|
|
|
479
|
+
| Slice[Json]
|
|
480
480
|
| Map[Str, Json]
|
|
481
481
|
```
|
|
482
482
|
|
|
@@ -505,7 +505,7 @@ fun show(j: Json) -> Str =
|
|
|
505
505
|
Bool => if j then "true" else "false"
|
|
506
506
|
Float => j.toStr()
|
|
507
507
|
Str => "\"{j}\""
|
|
508
|
-
|
|
508
|
+
Slice => "list"
|
|
509
509
|
Map => "map"
|
|
510
510
|
```
|
|
511
511
|
|
|
@@ -585,7 +585,7 @@ record House(Entity(pos, z), Clickable(hits), kind: Int) =
|
|
|
585
585
|
```
|
|
586
586
|
|
|
587
587
|
```plum
|
|
588
|
-
es :=
|
|
588
|
+
es := Slice[Entity]()
|
|
589
589
|
es.push(House(pos: p, z: 0, hits: 0, kind: 1)) # House converts to Entity
|
|
590
590
|
for e := range es
|
|
591
591
|
e.update(dt) # runs House.update
|
|
@@ -638,7 +638,7 @@ An `own record` (or `own trait`) is a value with a single owner, so the compiler
|
|
|
638
638
|
```plum
|
|
639
639
|
import std/Int
|
|
640
640
|
import std/Float
|
|
641
|
-
import std/
|
|
641
|
+
import std/Slice
|
|
642
642
|
|
|
643
643
|
own record Cell(x: Float) =
|
|
644
644
|
fun step(own self) -> Cell = Cell(..self, x: self.x + 1.0)
|
|
@@ -662,7 +662,7 @@ test "own"
|
|
|
662
662
|
w := World(cell: Cell(x: 0.0))
|
|
663
663
|
advance(w)
|
|
664
664
|
assert {w.cell.x - 1.0}.abs() < 1e-6
|
|
665
|
-
xs :=
|
|
665
|
+
xs := Slice(Cell(x: 1.0), Cell(x: 2.0))
|
|
666
666
|
ys := xs.mapInPlace(|k| Cell(..k, x: k.x + 1.0))
|
|
667
667
|
assert {ys.at(1).x - 3.0}.abs() < 1e-6
|
|
668
668
|
```
|
|
@@ -670,7 +670,7 @@ test "own"
|
|
|
670
670
|
- **What moves:** passing a value to an `own` parameter, calling an `own self` method, returning it, storing it in a field or list, and `match`ing on an owned value (the match consumes it). **What borrows:** plain parameters and plain `self`, and reads such as `s.x`.
|
|
671
671
|
- **Spread writes in place:** `Cell(..s, x: s.x + 1.0)` where `s` is owned reuses `s`'s storage and writes the changed fields into it, so no new record is allocated. Spreading a borrowed or ordinary record still copies.
|
|
672
672
|
- **Swap form:** you can't move a field out of a record, so replace it with a method call that takes and returns it: `w.cell = w.cell.step()` (in a game, `w.stage = w.stage.frame(dt, inputs).intoStage()`).
|
|
673
|
-
- **
|
|
673
|
+
- **Slices:** `Slice.mapInPlace` rewrites every element, and `takeAt(i)` / `putAt(i, v)` move one element out and back. See [`plum-examples/own_list.plum`](plum-examples/own_list.plum).
|
|
674
674
|
|
|
675
675
|
The ownership checker reports mistakes at compile time:
|
|
676
676
|
|
|
@@ -731,7 +731,7 @@ A `struct`'s constructor takes all its fields. A `record`'s header is its constr
|
|
|
731
731
|
record Bag(n: Int) =
|
|
732
732
|
fun init(self) =
|
|
733
733
|
self.label = "bag"
|
|
734
|
-
self.items:
|
|
734
|
+
self.items: Slice[Int] = Slice[Int]()
|
|
735
735
|
self.cap: Int = self.n * 2
|
|
736
736
|
```
|
|
737
737
|
|
|
@@ -764,6 +764,72 @@ match book
|
|
|
764
764
|
|
|
765
765
|
Patterns can be literals, a bare identifier (binds), a bare capitalized name (a member or tag of an enum, or an error name when matching an error value, also written `error.Name`; compared), a constructor pattern (`Circle(r)`, binding its fields, possibly nested), a bare member name (`Float`, which narrows the subject to that type, see [Enums](#enums)), or `_` (wildcard). `match a, b, ...` supports multiple subjects, and a case can carry a `when <expr>` guard. A `match` over an enum must be exhaustive. See [`plum-examples/match.plum`](plum-examples/match.plum).
|
|
766
766
|
|
|
767
|
+
### Enum `init`
|
|
768
|
+
|
|
769
|
+
`TypeName(args)` on an enum calls its static `init`, the same call syntax as a record. The result is any member of the enum. This is how `List(1, 2, 3)` builds a cons list:
|
|
770
|
+
|
|
771
|
+
```plum
|
|
772
|
+
module shapes
|
|
773
|
+
import std/Int
|
|
774
|
+
import std/Str
|
|
775
|
+
|
|
776
|
+
enum Shape =
|
|
777
|
+
| Dot
|
|
778
|
+
| Line(len: Int)
|
|
779
|
+
|
|
780
|
+
fun init(n: Int) -> Shape = if n == 0 then Dot else Line(len: n)
|
|
781
|
+
|
|
782
|
+
fun size(self) -> Int = match self
|
|
783
|
+
Dot => 0
|
|
784
|
+
Line(len) => len
|
|
785
|
+
|
|
786
|
+
test "enum init"
|
|
787
|
+
assert Shape(0).size() == 0
|
|
788
|
+
assert Shape(7).size() == 7
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
## Lists, slices and arrays
|
|
792
|
+
|
|
793
|
+
Three sequence types, from `std/List`, `std/Slice` and `std/Array`.
|
|
794
|
+
|
|
795
|
+
**`List[T]`** is the std cons list: `Nil` or `Cons(head, tail)`. It is the default for recursive code, with pattern matching and `List(1, 2, 3)` (an enum `init`). Each cell is one allocation. Self tail calls run as loops and a constructor-wrapped self call (`map`) is rewritten to a loop (tail modulo cons), so recursion over a million cells uses constant stack. `for x := range xs` works, but `at(i)` is O(i), so a plain index loop is quadratic.
|
|
796
|
+
|
|
797
|
+
```plum
|
|
798
|
+
module lists
|
|
799
|
+
import std/Int
|
|
800
|
+
import std/Str
|
|
801
|
+
import std/List
|
|
802
|
+
|
|
803
|
+
fun sum(xs: List[Int], acc: Int) -> Int = match xs
|
|
804
|
+
Nil => acc
|
|
805
|
+
Cons(h, t) => sum(t, acc + h)
|
|
806
|
+
|
|
807
|
+
test "list"
|
|
808
|
+
xs := List(1, 2, 3, 4)
|
|
809
|
+
assert sum(xs, 0) == 10
|
|
810
|
+
assert xs.map(|x| x * 2).length() == 4
|
|
811
|
+
total := 0
|
|
812
|
+
for x := range xs
|
|
813
|
+
total = total + x
|
|
814
|
+
assert total == 10
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
**`Slice[T]`** is the growable view over an `Array` (the type formerly called `List`): indexed access, `push`, `Slice(1, 2, 3)`.
|
|
818
|
+
|
|
819
|
+
```plum
|
|
820
|
+
module slices
|
|
821
|
+
import std/Int
|
|
822
|
+
import std/Slice
|
|
823
|
+
|
|
824
|
+
test "slice"
|
|
825
|
+
s := Slice(1, 2, 3)
|
|
826
|
+
s.push(4)
|
|
827
|
+
assert s.length() == 4
|
|
828
|
+
assert s.at(3) == 4
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
**`Array[T]`** is the fixed-length array, unchanged: arrays of structs are stored field by field. See [`plum-examples/struct_arrays.plum`](plum-examples/struct_arrays.plum).
|
|
832
|
+
|
|
767
833
|
## Generics
|
|
768
834
|
|
|
769
835
|
```plum
|
|
@@ -779,7 +845,7 @@ A generic call whose type parameters stay unknown is an error at the call, befor
|
|
|
779
845
|
|
|
780
846
|
### How generics compile
|
|
781
847
|
|
|
782
|
-
Generics are duck-typed and compiled per type, as in Zig. A generic body is type-checked once per concrete instantiation, and each instantiation gets its own compiled copy with unboxed values (an array of its own element type, no boxing at generic edges): `
|
|
848
|
+
Generics are duck-typed and compiled per type, as in Zig. A generic body is type-checked once per concrete instantiation, and each instantiation gets its own compiled copy with unboxed values (an array of its own element type, no boxing at generic edges): `Slice[Int]` and `Slice[Str]` are different code, and a `Slice[Float]` stores raw `f64`s.
|
|
783
849
|
|
|
784
850
|
- **No bounds.** `[T]` carries no constraint. `fun show(x: T) -> Str = x.toStr()` works for every type that has a `toStr` method and fails at the use site otherwise.
|
|
785
851
|
- **Errors point at the use site.** A missing method is reported against the concrete type, with the chain of instantiations that led there:
|
|
@@ -797,7 +863,7 @@ in outer[Plain] (called at main.plum:9:3):
|
|
|
797
863
|
```plum
|
|
798
864
|
record Num[T](add: fn(T, T) -> T, zero: T)
|
|
799
865
|
|
|
800
|
-
fun sumWith(xs:
|
|
866
|
+
fun sumWith(xs: Slice[T], num: Num[T]) -> T =
|
|
801
867
|
acc := num.zero
|
|
802
868
|
for x := range xs
|
|
803
869
|
acc = num.add(acc, x)
|
|
@@ -807,15 +873,15 @@ fun sumWith(xs: List[T], num: Num[T]) -> T =
|
|
|
807
873
|
```plum
|
|
808
874
|
intNum := Num(add: |a, b| a + b, zero: 0)
|
|
809
875
|
floatNum := Num(add: |a, b| a + b, zero: 0.0)
|
|
810
|
-
sumWith(
|
|
876
|
+
sumWith(Slice(1, 2, 3), intNum) # 6
|
|
811
|
-
sumWith(
|
|
877
|
+
sumWith(Slice(1.5, 2.5), floatNum) # 4.0
|
|
812
878
|
```
|
|
813
879
|
|
|
814
880
|
`num.add(a, b)` calls the closure stored in the fn-typed field `add`; if the type also declares a method named `add`, the method wins. See `num_record_passed_explicitly` in [`plum-examples/generics.plum`](plum-examples/generics.plum).
|
|
815
881
|
|
|
816
882
|
## Constants, statics and `primitive`
|
|
817
883
|
|
|
818
|
-
There are no top-level constants and no free-standing statics. A constant lives inside a type's body (`enum`, `struct`, `record`) or a `primitive`; `Name = expr` declares an associated constant, and a `fun` without `self` is a static function. A constant's name is PascalCase, at least two characters, with no underscores and no two capitals in a row (`MaxRetries`, `Ln2`, `Log10E`). A constant is always read as `Type.Name`, from anywhere including inside the same body; a static is called as `Type.f(args)`, or by its bare name inside the same body. A constant is evaluated lazily, once (a heap value such as a `
|
|
884
|
+
There are no top-level constants and no free-standing statics. A constant lives inside a type's body (`enum`, `struct`, `record`) or a `primitive`; `Name = expr` declares an associated constant, and a `fun` without `self` is a static function. A constant's name is PascalCase, at least two characters, with no underscores and no two capitals in a row (`MaxRetries`, `Ln2`, `Log10E`). A constant is always read as `Type.Name`, from anywhere including inside the same body; a static is called as `Type.f(args)`, or by its bare name inside the same body. A constant is evaluated lazily, once (a heap value such as a `Slice` is cached and shared), and names are unique per type, so `Alpha.Flag` and `Beta.Flag` coexist. A constant may not share its name with a variant of its own type.
|
|
819
885
|
|
|
820
886
|
```plum
|
|
821
887
|
enum Level =
|
|
@@ -879,7 +945,7 @@ fun main() -> Int =
|
|
|
879
945
|
each(|v| v * 3) + useCapturingClosure()
|
|
880
946
|
```
|
|
881
947
|
|
|
882
|
-
A closure literal is `|params| body`. Capture is snapshot-by-value: a closure copies the outer variables it references at creation time, not re-read live later. A `fn(...) -> T` type annotates a closure-typed parameter or field, and a named function can be passed wherever a `fn` value is expected. `
|
|
948
|
+
A closure literal is `|params| body`. Capture is snapshot-by-value: a closure copies the outer variables it references at creation time, not re-read live later. A `fn(...) -> T` type annotates a closure-typed parameter or field, and a named function can be passed wherever a `fn` value is expected. `Slice` and `Map` have no closure-taking iteration methods (use `for`, below); closures are for passing behavior around as values (see `Num` under [Generics](#how-generics-compile)). See [`plum-examples/closures.plum`](plum-examples/closures.plum).
|
|
883
949
|
|
|
884
950
|
A closure parameter may carry a type, `|b: Box| b.get()`, and then it has that type (a mismatch with an expected `fn(..)` is a type error). An untyped closure stored in a local takes its parameter types from the first call of that local; if there is none, the error is `cannot infer the type of b; write |b: Box|`.
|
|
885
951
|
|
|
@@ -932,7 +998,7 @@ Beyond `range n` (0 to n-1), the `Int` methods `a.until(b)` (a to b-1), `a.to(b)
|
|
|
932
998
|
|
|
933
999
|
The protocol is resolved on the type of the source, after type inference:
|
|
934
1000
|
|
|
935
|
-
- one binder: the type has `length()` and `at(i)` (a `
|
|
1001
|
+
- one binder: the type has `length()` and `at(i)` (a `Slice`, `Array`, `Deque`, a variadic parameter, your own type);
|
|
936
1002
|
- two binders: if the type declares a `fun* entries(self) *(A, B)` method it is used (`Map` does: `(key, value)`), otherwise `(position, element)` over `length()`/`at(i)`;
|
|
937
1003
|
- `x.name(args)` where the type of `x` declares `fun* name(self, ..)`: the method is expanded like any `fun*`, with `self` bound to `x`.
|
|
938
1004
|
|
|
@@ -971,8 +1037,8 @@ fun* neighbours(x: Int, y: Int) *(Int, Int) =
|
|
|
971
1037
|
|
|
972
1038
|
`Seq[T]` is a compiler primitive and `*T` is sugar for it. A `Seq` never exists at runtime: it can't be stored, returned or captured, it only flows into a `for` source or into the next method call. The methods live in [`plum-std/Seq.plum`](plum-std/Seq.plum) under `primitive Seq[T]`; `xs.m()` where the type of `xs` has no `m` of its own and is iterable (`length()`/`at(i)`, or an `entries()` method) resolves to `Seq.m`, so they work on every list-like type:
|
|
973
1039
|
|
|
974
|
-
- **adapters** reshape a sequence, have no result and may appear anywhere in a chain: `take(n)`, `skip(n)`, `stepBy(n)`, `zip(other)`, `reversed()`, `chunks(n)` (the last three need a `
|
|
975
|
-
- **terminals** take the `for` body (the predicate, the mapping, the step) and must be the last call: `filter()` (`->
|
|
1040
|
+
- **adapters** reshape a sequence, have no result and may appear anywhere in a chain: `take(n)`, `skip(n)`, `stepBy(n)`, `zip(other)`, `reversed()`, `chunks(n)` (the last three need a `Slice` receiver);
|
|
1041
|
+
- **terminals** take the `for` body (the predicate, the mapping, the step) and must be the last call: `filter()` (`-> Slice[T]`), `map()` (`-> Slice[U]`), `find()` (`-> T?`), `fold(init)` (binders `acc, x`), `count()`, `any()`, `all()`, `each()`, `firstN(n)` (the first n elements the body accepts), and `toList()`, which has no body and needs no `for`: `ys := xs.skip(1).take(3).toList()`.
|
|
976
1042
|
|
|
977
1043
|
A terminal in the middle of a chain is an error (`'filter' needs the for body; it must be the last call in the chain`), and so is storing an adapter's result (`s := xs.take(2)`).
|
|
978
1044
|
|
|
@@ -982,7 +1048,7 @@ names := for u := range units.take(5).map(), if u.hp > 0.0
|
|
|
982
1048
|
u.name
|
|
983
1049
|
```
|
|
984
1050
|
|
|
985
|
-
Performance guarantee: a whole chain is ONE loop with no closure allocation, no `call_indirect` and no intermediate `
|
|
1051
|
+
Performance guarantee: a whole chain is ONE loop with no closure allocation, no `call_indirect` and no intermediate `Slice`. The method chain below compiles to the same single loop as the hand-written one:
|
|
986
1052
|
|
|
987
1053
|
```plum
|
|
988
1054
|
total := for acc, u := range units.skip(1).take(10).fold(0.0), if u.team == me.team
|
|
@@ -1026,7 +1092,7 @@ See [`plum-examples/seq.plum`](plum-examples/seq.plum) (one test per adapter and
|
|
|
1026
1092
|
|
|
1027
1093
|
## Optionals
|
|
1028
1094
|
|
|
1029
|
-
An optional is `T?`: a `T`, or `null`. It is its own type, not an error: it carries no error name, and `catch` and `try` do not apply to it. A plain `T` in a `T?` slot is the present value, and `null` is the absent value wherever a `T?` is expected (a call or constructor argument, a `return`, a tail, a list element, a parameter default such as `n: Int? = null`). An inline `if`/`match` whose branches are a `T` and `null` is a `T?`: `if inBox(p) then n.id() else null` is an `Int?`. `null` with nothing to say what it is an optional of is an error (`x := null`: `cannot infer the type of null`). For a reference type (a class, enum, `Str`, `
|
|
1095
|
+
An optional is `T?`: a `T`, or `null`. It is its own type, not an error: it carries no error name, and `catch` and `try` do not apply to it. A plain `T` in a `T?` slot is the present value, and `null` is the absent value wherever a `T?` is expected (a call or constructor argument, a `return`, a tail, a list element, a parameter default such as `n: Int? = null`). An inline `if`/`match` whose branches are a `T` and `null` is a `T?`: `if inBox(p) then n.id() else null` is an `Int?`. `null` with nothing to say what it is an optional of is an error (`x := null`: `cannot infer the type of null`). For a reference type (a class, enum, `Str`, `Slice`, ...) a `T?` is stored as a nullable reference, so a present value costs no allocation; for `Int`, `Float`, `Bool` and structs it is the value plus a present flag, `(T..., i32)` (the `i32` is 0 when present, and the code of `null` when absent). A doubled `T??` is not allowed.
|
|
1030
1096
|
|
|
1031
1097
|
```plum
|
|
1032
1098
|
record Person(name: Str, age: Int)
|
|
@@ -1053,7 +1119,7 @@ fun port(cfg: Config) -> error{Unconfigured}!Int =
|
|
|
1053
1119
|
p := cfg.port ?: return error.Unconfigured # absence becomes an error
|
|
1054
1120
|
p
|
|
1055
1121
|
|
|
1056
|
-
fun total(xs:
|
|
1122
|
+
fun total(xs: Slice[Int?]) -> Int =
|
|
1057
1123
|
sum := 0
|
|
1058
1124
|
for x := range xs
|
|
1059
1125
|
if n := x
|
|
@@ -1120,7 +1186,7 @@ primitive Int =
|
|
|
1120
1186
|
fun parse(s: Str) -> error{BadNumStr}!Int
|
|
1121
1187
|
```
|
|
1122
1188
|
|
|
1123
|
-
The std signatures: `
|
|
1189
|
+
The std signatures: `Slice.get` fails with `OutOfBounds`, `first`/`last`/`pop`/`sample` with `ListWasEmpty`, `findValue` with `NotFound`, `Map.get` with `KeyNotFound`, `Int.parse`/`Float.parse` with `BadNumStr`, `Bool.parse` with `BadBoolStr`, `Json.fromStr` with `InvalidJson` (`Json.parse` returns where and why as an ordinary value), the `Os` file functions with `NoSuchPath`/`IoFailed` (the set `IoError` where a function can hit both), `Http.request` with `HttpFailed`/`BadResponse` (the set `HttpError`), `Uuid.fromStr` with `InvalidUuid`. Where absence is not an error the result is an optional instead: `Slice.head`, the `find()` terminal and `Map.getOrNull` return `T?`.
|
|
1124
1190
|
|
|
1125
1191
|
### Propagating: `try expr`
|
|
1126
1192
|
|
|
@@ -1129,7 +1195,7 @@ The std signatures: `List.get` fails with `OutOfBounds`, `first`/`last`/`pop`/`s
|
|
|
1129
1195
|
```plum
|
|
1130
1196
|
fun numAt(s: Str, i: Int) -> error{BadNumStr, OutOfBounds}!Int =
|
|
1131
1197
|
n := try Int.parse(s) # error{BadNumStr}
|
|
1132
|
-
x := try
|
|
1198
|
+
x := try Slice(10, 20, 30).get(i) # error{OutOfBounds}
|
|
1133
1199
|
n + x
|
|
1134
1200
|
```
|
|
1135
1201
|
|
|
@@ -1184,11 +1250,11 @@ enum Failure =
|
|
|
1184
1250
|
| BadRequest
|
|
1185
1251
|
```
|
|
1186
1252
|
|
|
1187
|
-
An error union can have an optional as its success value: `!T?` (`error{Bad}!Int?`). `try x` gives the `T?`, and `?:` then handles the null: `v := try parse(s)` followed by `v ?: 0`. An error union can also be the success type of another (`
|
|
1253
|
+
An error union can have an optional as its success value: `!T?` (`error{Bad}!Int?`). `try x` gives the `T?`, and `?:` then handles the null: `v := try parse(s)` followed by `v ?: 0`. An error union can also be the success type of another (`Slice[!Int].get(i)` is a `!!Int`): unwrap it one level at a time with `!`, `try` or `catch`. See [`plum-examples/error_propagation.plum`](plum-examples/error_propagation.plum).
|
|
1188
1254
|
|
|
1189
1255
|
### Runtime representation
|
|
1190
1256
|
|
|
1191
|
-
Each error name gets a global `i32` code, interned in sorted order from 1 (0 means success). An optional `T?` uses the same layout (see [Optionals](#optionals)). A `!T` result is a multi-value return `(T..., i32)`, so neither success nor failure allocates; stored in a heap slot (a field, a `
|
|
1257
|
+
Each error name gets a global `i32` code, interned in sorted order from 1 (0 means success). An optional `T?` uses the same layout (see [Optionals](#optionals)). A `!T` result is a multi-value return `(T..., i32)`, so neither success nor failure allocates; stored in a heap slot (a field, a `Slice` element, a generic container) it is boxed into a `{value, code}` struct.
|
|
1192
1258
|
|
|
1193
1259
|
## Testing
|
|
1194
1260
|
|
|
@@ -1314,9 +1380,9 @@ record World(stage: Stage, wasDown: Bool)
|
|
|
1314
1380
|
|
|
1315
1381
|
export fun init() -> World =
|
|
1316
1382
|
spr := PureScene.sprite(2, 0, 100.0, 100.0, 48.0, 48.0)
|
|
1317
|
-
spr.actions =
|
|
1383
|
+
spr.actions = Slice(Actions.forever(Actions.rotateBy(1.0, 1.0)))
|
|
1318
1384
|
sp: Actor := spr
|
|
1319
|
-
kids :=
|
|
1385
|
+
kids := Slice[Actor]()
|
|
1320
1386
|
kids.push(sp)
|
|
1321
1387
|
root: Actor := PureScene.group(1, 0.0, 0.0, 800.0, 600.0, kids)
|
|
1322
1388
|
World(stage: SceneInput.stage(root), wasDown: false)
|
|
@@ -1374,8 +1440,8 @@ export fun draw(w: World) =
|
|
|
1374
1440
|
- **`Str`** (`import std/Str`) — an immutable view of a packed byte array, with a codepoint-aware layer for Unicode correctness (`runeLength`, `runeAt`, `codePointAt`, `Str.codePointToStr(cp)`). Substrings (`sub`, `slice`, `trim*`, `split` parts) share their source's bytes instead of copying; `s.clone()` detaches one
|
|
1375
1441
|
- **`Byte`** (`import std/Byte`) — a scalar unsigned 8-bit value; `Array[Byte]` stores one byte per element
|
|
1376
1442
|
- **`Buffer`** (`import std/Buffer`) — a growable, mutable byte sequence for building up a `Str`, amortized O(n) writes
|
|
1377
|
-
- **`Array[T]`** (`import std/Array`) — the compiler-intrinsic fixed-length array that `
|
|
1378
|
-
- **`
|
|
1443
|
+
- **`Array[T]`** (`import std/Array`) — the compiler-intrinsic fixed-length array that `Slice`, `Map` and `Str` are written on, with `Array.copy` and `Array.equal` for scalar ranges; `Grid[T]` is a flat 2D array
|
|
1444
|
+
- **`Slice[T]`** (`import std/Slice`) — a growable list built on `Array`, with `get`/`add`/`set`/`removeAt`/`remove`/`sort`/`join`/`chunk` (iteration and transformation via `for`). `xs.slice(i, j)` returns a view: a `Slice` over the same storage, with no copy. `set` on a view changes the original and a `push` on a view moves it to its own storage first; reordering the original in place (`removeAt`, `reverse`) is visible through its views, so use `sublist(i, j)` for a copy
|
|
1379
1445
|
- **`Map[K, V]`** (`import std/Map`) — `Map("a" => 1, "b" => 2)` builds a `Map[Str, Int]`; `Map[K, V]()` is empty. `k => v` is sugar for `Pair(k, v)` in any call's argument list, and a key can be any expression (`Map(n + 1 => x)`). A later duplicate key wins, as with `set`. See [`plum-examples/maps.plum`](plum-examples/maps.plum)
|
|
1380
1446
|
- **`Set[T]`**, **`Deque[T]`**, **`Heap[T]`** — a set over `Map`'s bucketed hash table, a ring-buffer double-ended queue and a key-ordered min-heap
|
|
1381
1447
|
- **`Seq[T]`** (`import std/Seq`) — the adapters and terminals of the `for` chains
|
|
@@ -1387,7 +1453,7 @@ See [`plum-examples/`](plum-examples) for a runnable file per feature above, and
|
|
|
1387
1453
|
|
|
1388
1454
|
### JSON
|
|
1389
1455
|
|
|
1390
|
-
`import std/Json` adds `Json.encode(x)` and `Json.decode(s)`. A type encodes when it has `toJson(self) -> Json` and decodes when it has a static `fromJson(j: Json) -> JsonError!T`. `Int`, `Float`, `Bool`, `Str`, `
|
|
1456
|
+
`import std/Json` adds `Json.encode(x)` and `Json.decode(s)`. A type encodes when it has `toJson(self) -> Json` and decodes when it has a static `fromJson(j: Json) -> JsonError!T`. `Int`, `Float`, `Bool`, `Str`, `Slice[T]` and `Map[K, V]` get both methods from `std/Json`, so they exist only in programs that import it. `Int.fromJson` rejects fractions and magnitudes of 2^53 or more. `decode` takes `T` from the annotation, and `JsonError` is `InvalidJson | WrongJsonType | MissingField`.
|
|
1391
1457
|
|
|
1392
1458
|
```plum
|
|
1393
1459
|
record Point(x: Int, y: Int) =
|
|
@@ -1411,7 +1477,7 @@ Also available: `Json.decodeWith(s, fn)`, `j.field(name)`, `j.optField(name)`, `
|
|
|
1411
1477
|
|
|
1412
1478
|
- There are no explicit call-site type arguments: `Json.decode[House](s)` does not parse, so annotate the result instead.
|
|
1413
1479
|
- Optionals `T?` have no built-in `toJson`/`fromJson` yet; write them by hand.
|
|
1414
|
-
- `
|
|
1480
|
+
- `Slice[Int].fromJson(j)` does not parse; use `j.into()` or `Json.decode`.
|
|
1415
1481
|
- Inside a generic static, prefer `try`/`!` over `catch return error.X` (a known codegen gap).
|
|
1416
1482
|
- There is no derive: records write `toJson`/`fromJson` by hand.
|
|
1417
1483
|
|
|
@@ -1421,9 +1487,9 @@ See [`plum-examples/json.plum`](plum-examples/json.plum).
|
|
|
1421
1487
|
|
|
1422
1488
|
These are compiler optimizations, not language changes; none of them changes what a program means or how it is written.
|
|
1423
1489
|
|
|
1424
|
-
- **No allocation for local lists and views.** A `
|
|
1490
|
+
- **No allocation for local lists and views.** A `Slice`, a `Slice.slice` view or a `Str` substring that never leaves its function (isn't stored, passed on or returned) could keep its fields in locals instead of allocating its small record (escape analysis).
|
|
1425
1491
|
- **Loops read a list once.** `for x := range xs` could read `xs`'s storage, start and size once before the loop instead of on every step, and `xs.get(i)` loops could do the same when the body can't change `xs`.
|
|
1426
|
-
- **Inlined `get`, `set` and `push`.** These small `
|
|
1492
|
+
- **Inlined `get`, `set` and `push`.** These small `Slice` and `Array` methods are function calls today. Inlining them removes the call and lets the wasm engine keep values in registers.
|
|
1427
1493
|
- **No placeholder `self` for statics.** A static such as `Vec2.zero()` still receives an unused `self` slot (one per field for a struct), which could be dropped from its signature.
|
|
1428
1494
|
- **Fewer boxed structs.** A struct stored in a record field, passed as a variadic argument or carried in an error union is boxed (one allocation); it could be stored inline instead.
|
|
1429
1495
|
- **Host string reads.** When the host prints or writes a `Str`, it copies the whole backing array and then takes the `[start, start + len)` part; it could copy only that range.
|
docs/plans/2026-11-03-unboxed-variants.benchmark.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Unboxed variants benchmark
|
|
2
|
+
|
|
3
|
+
Date 2026-11-03. Same probe shape as `2026-11-03-gc-report.md`: `fromTo` (tail modulo cons) then `length`, and an
|
|
4
|
+
accumulator `build` then `fold` sum, over std `List[Int]` (Nil/Cons, one `struct.new` per cell). Dev build of
|
|
5
|
+
plum-cli (wasmtime crates at opt-level 3), wall time of the whole `plum test <file>` run (about 100 ms of it is
|
|
6
|
+
compile), 3 runs each, median. Probes: /private/tmp/claude-501/t9/.
|
|
7
|
+
|
|
8
|
+
| Probe | Before (boxed Link, gc-report) | After (unboxed List), runs | After median |
|
|
9
|
+
|---|---|---|---|
|
|
10
|
+
| fromTo+length 100k | 1830 ms (cold) | 149, 150, 147 | 149 ms |
|
|
11
|
+
| fromTo+length 1M | 1238 ms | 192, 188, 194 | 192 ms |
|
|
12
|
+
| build+sum 100k | - | 145, 149, 149 | 149 ms |
|
|
13
|
+
| build+sum 1M | ~280 ms | 202, 201, 194 | 201 ms |
|
|
14
|
+
| `plum test cons_list.plum tail_calls.plum` | - | 2083, 2107, 2098 | 2098 ms |
|
|
15
|
+
|
|
16
|
+
## Verdict
|
|
17
|
+
|
|
18
|
+
Unboxed variants make the 1M-cell build+length about 6.4x faster (1,238 ms to 192 ms, of which about 100 ms is
|
|
19
|
+
compile), and the superlinear growth is gone: 100k to 1M now costs 1.3x for 10x the cells, where it was far worse
|
|
20
|
+
before. build+sum improves less (280 to 201 ms) because it was already cheap. One allocation per cell (instead of
|
|
21
|
+
a variant box plus a payload) halves live objects, so the semi-space copying collector has far less to copy. The
|
|
22
|
+
two test files (about 2.1 s) are dominated by their many other tests and are given for reference only; no
|
|
23
|
+
"before" figure was recorded for them.
|
docs/plans/2026-11-03-unboxed-variants.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Unboxed variants, enum constructors, List -> Slice, cons List
|
|
2
|
+
|
|
3
|
+
**Review plan:** docs/plans/2026-11-03-unboxed-variants.review.md
|
|
4
|
+
**Goal:** rename the growable `List` to `Slice` everywhere; `EnumName(args)` calls the enum's static `init`; inline-variant and union-enum members become wasm-gc subtypes of the enum (one allocation per value); a new std cons `List[T]`; recursive code moves to it. `Array` unchanged.
|
|
5
|
+
**Architecture:** rename first (std file, compiler special cases, all users); enum init routing in checker+codegen; unboxed layout reuses the subtyped-trait path in plum-wasm-codegen; then plum-std/List.plum is the cons list.
|
|
6
|
+
**Tech stack:** Rust (plum-core, plum-checker, plum-wasm-codegen, plum-lsp), Plum std/examples, plum-game fixtures.
|
|
7
|
+
**Spec:** docs/plans/2026-11-03-unboxed-variants.review.md
|
|
8
|
+
**Detail:** contracts
|
|
9
|
+
**Target-min:** 60
|
|
10
|
+
|
|
11
|
+
## Global constraints
|
|
12
|
+
- Work only in /Users/pyrossh/Code/plum-pure-scene-values (branch pure-scene-values, base 614ddf5). Never touch /Users/pyrossh/Code/plum or ../kam_plum.
|
|
13
|
+
- Plum: `touch plum-cli/src/main.rs && cargo build --quiet -p plum-cli && ./target/debug/plum test <files>`; sweep `./target/debug/plum test plum-std plum-examples` (611 green, ~21 s). Rust: `cargo test --manifest-path <crate>/Cargo.toml [--test name]`. Known failing: plum-checker `generic_funstar_wired` (untracked repro) — ignore.
|
|
14
|
+
- Rename rule: the type `List` (growable record over Array) becomes `Slice`; file plum-std/List.plum becomes plum-std/Slice.plum; imports `std/List` -> `std/Slice`; `List(...)`, `List[T]`, `List.x` -> `Slice...`. Method names, behaviour and test names stay the same except where a test name literally says "list" about the type (keep names unless they would mislead). Do not rename the identifier `Link` or unrelated words (`listen`, `ListItem` in LSP protocol types).
|
|
15
|
+
- The new cons List must not exist until the rename is complete (no stray old use may type-check against it).
|
|
16
|
+
- Never run `plum format` on real files. Grammar is unaffected.
|
|
17
|
+
|
|
18
|
+
```mermaid
|
|
19
|
+
flowchart LR
|
|
20
|
+
R1["rename: compiler + std"] --> R2["rename: examples + fixtures"]
|
|
21
|
+
R1 --> R3["rename: Rust tests + README"]
|
|
22
|
+
EI["enum init routing"] --> L5["unboxed: construct"] --> L6["unboxed: match/access"]
|
|
23
|
+
R2 --> CL["std cons List"]
|
|
24
|
+
R3 --> CL
|
|
25
|
+
EI --> CL
|
|
26
|
+
CL --> MV["move recursive code"]
|
|
27
|
+
L6 --> PD["perf + docs"]
|
|
28
|
+
MV --> PD
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
File structure: each task's Owns. Spec coverage: What you will get (T1-T9), What will be built (T1-T9), How it fits together (diagram), Decisions you can change (defaults in constraints), Risks (R1 T1-T3, R2 T7, R3 T5/T6), Out of scope (not built), How we will know it works (T1-T9), Rough time and cost (schedule).
|
|
32
|
+
|
|
33
|
+
## Task graph
|
|
34
|
+
```mermaid
|
|
35
|
+
flowchart LR
|
|
36
|
+
T1[rename compiler std]
|
|
37
|
+
T2[rename examples fixtures]
|
|
38
|
+
T3[rename tests readme]
|
|
39
|
+
T4[enum init routing]
|
|
40
|
+
T5[unboxed construct]
|
|
41
|
+
T6[unboxed use]
|
|
42
|
+
T7[std cons List]
|
|
43
|
+
T8[move recursive code]
|
|
44
|
+
T9[perf and docs]
|
|
45
|
+
%% taskgraph
|
|
46
|
+
T1 --> T2
|
|
47
|
+
T1 --> T3
|
|
48
|
+
T4 --> T5
|
|
49
|
+
T5 --> T6
|
|
50
|
+
T2 --> T7
|
|
51
|
+
T3 --> T7
|
|
52
|
+
T4 --> T7
|
|
53
|
+
T7 --> T8
|
|
54
|
+
T6 --> T9
|
|
55
|
+
T8 --> T9
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Rename
|
|
59
|
+
|
|
60
|
+
### T1 rename List to Slice: compiler and std · Complexity 4 · Est. context 50k
|
|
61
|
+
**Owns:** plum-std/List.plum, plum-std/Slice.plum, plum-std/*.plum, plum-std/game/*.plum, plum-core/src/forms.rs, plum-core/src/loader.rs, plum-checker/src/ownership.rs, plum-lsp/src/main.rs, plum-lsp/src/completion.rs
|
|
62
|
+
**Components:** 2
|
|
63
|
+
**LOC owned:** 150
|
|
64
|
+
**Tests:** fast
|
|
65
|
+
**Kind:** design
|
|
66
|
+
**Model:** sonnet
|
|
67
|
+
**Consumes:** none
|
|
68
|
+
**Produces:** none
|
|
69
|
+
**Review Focus:** encoding R1: `git mv plum-std/List.plum plum-std/Slice.plum`, `record Slice[T]`; every std file and std/game file updated; forms.rs builds `Slice` where it built `List` (for-chains, collect); ownership.rs R2 rule on `Slice`; LSP completions/hover names.
|
|
70
|
+
**Test code:** `./target/debug/plum test plum-std` all green (same counts as before the rename); `cargo build --workspace`; plum-core/plum-checker test binaries green except tests whose embedded sources say `List` (T3 fixes those — list them).
|
|
71
|
+
|
|
72
|
+
### T2 rename List to Slice: examples and fixtures · Complexity 2 · Est. context 35k
|
|
73
|
+
**Owns:** plum-examples/*.plum, plum-game/web-test/fixtures/*.plum, plum-game/examples/*.plum
|
|
74
|
+
**Components:** 1
|
|
75
|
+
**LOC owned:** 120
|
|
76
|
+
**Tests:** fast
|
|
77
|
+
**Kind:** mechanical
|
|
78
|
+
**Model:** haiku
|
|
79
|
+
**Consumes:** none
|
|
80
|
+
**Produces:** none
|
|
81
|
+
**Review Focus:** encoding: only the type rename; `Link` and game logic untouched.
|
|
82
|
+
**Test code:** `./target/debug/plum test plum-examples` green with the same test count (611 total with std); every fixture under plum-game/web-test/fixtures compiles (`./target/debug/plum compile <f>`).
|
|
83
|
+
|
|
84
|
+
### T3 rename List to Slice: Rust tests and README · Complexity 2 · Est. context 35k
|
|
85
|
+
**Owns:** plum-core/tests/*.rs, plum-checker/tests/*.rs, plum-wasm-codegen/tests/*.rs, plum-game/tests/*.rs, README.md
|
|
86
|
+
**Components:** 1
|
|
87
|
+
**LOC owned:** 120
|
|
88
|
+
**Tests:** fast
|
|
89
|
+
**Kind:** mechanical
|
|
90
|
+
**Model:** haiku
|
|
91
|
+
**Consumes:** none
|
|
92
|
+
**Produces:** none
|
|
93
|
+
**Review Focus:** encoding: embedded Plum sources and expected messages renamed; README prose says Slice for the growable type and keeps Array text.
|
|
94
|
+
**Test code:** every test binary in plum-core, plum-checker, plum-wasm-codegen, plum-game green (except generic_funstar_wired); README code blocks mentioning `List` updated.
|
|
95
|
+
|
|
96
|
+
## Enum constructors and layout
|
|
97
|
+
|
|
98
|
+
### T4 enum init routing · Complexity 4 · Est. context 45k
|
|
99
|
+
**Owns:** plum-checker/src/lib.rs, plum-wasm-codegen/src/lib.rs, plum-wasm-codegen/tests/enum_init_e2e_test.rs
|
|
100
|
+
**Components:** 1
|
|
101
|
+
**LOC owned:** 120
|
|
102
|
+
**Tests:** fast
|
|
103
|
+
**Model:** sonnet
|
|
104
|
+
**Consumes:** none
|
|
105
|
+
**Produces:** EnumName(args) resolves to the enum's static `init`
|
|
106
|
+
e.g. `Link(1, 2, 3, 4).sum()` -> `10` (enum Link with `fun init(values: ...T) -> Link[T]`)
|
|
107
|
+
e.g. `Shape(1)` on an enum without init -> error `Shape has no init; construct a variant or declare `fun init``
|
|
108
|
+
**Review Focus:** state: variant names win over init; generic T from args or expected type (`xs: Link[Int] = Link()` is Nil); fixed-param init works; today's codegen error `unknown function 'Link'` gone.
|
|
109
|
+
**Test code:** enum_init_e2e_test.rs: `variadic_init_builds_list`, `empty_init_with_annotation`, `fixed_param_init`, `no_init_error_message`, `variant_name_wins`, `generic_inferred_from_args`. Use an enum named `Link` in tests (the std List rename is in flight).
|
|
110
|
+
|
|
111
|
+
### T5 unboxed variants: layout and construction · Complexity 6 · Est. context 65k
|
|
112
|
+
**Owns:** plum-wasm-codegen/src/lib.rs, plum-wasm-codegen/tests/unboxed_variants_e2e_test.rs
|
|
113
|
+
**Components:** 1
|
|
114
|
+
**LOC owned:** 180
|
|
115
|
+
**Tests:** fast
|
|
116
|
+
**Model:** sonnet
|
|
117
|
+
**Consumes:** none
|
|
118
|
+
**Produces:** none
|
|
119
|
+
**Review Focus:** state R3: inline-variant hidden records and union-enum member records become wasm-gc subtypes of the enum supertype (reuse the subtyped-trait path, `subtypedTraits` / hidden member variants ~l.1043); no hidden wrapper struct is emitted; nullary tags unchanged; generics per instantiation.
|
|
120
|
+
**Test code:** unboxed_variants_e2e_test.rs: `cons_one_struct_new` (wasmparser: constructing `Cons(head: 1, tail: Nil)` emits one struct.new), `union_member_one_struct_new` (`enum Shape = Circle | Rect`), `generic_link_instantiations_separate`, `own_record_member_ok`. Existing union/inline-variant tests may fail on match/access until T6 — list them; construction-only tests must pass.
|
|
121
|
+
|
|
122
|
+
### T6 unboxed variants: match, access, equality, toStr, Json · Complexity 6 · Est. context 65k
|
|
123
|
+
**Owns:** plum-wasm-codegen/src/lib.rs, plum-wasm-codegen/tests/unboxed_variants_e2e_test.rs, plum-core/tests/trmc_test.rs
|
|
124
|
+
**Components:** 1
|
|
125
|
+
**LOC owned:** 180
|
|
126
|
+
**Tests:** fast
|
|
127
|
+
**Model:** sonnet
|
|
128
|
+
**Consumes:** none
|
|
129
|
+
**Produces:** none
|
|
130
|
+
**Review Focus:** state: match via br_on_cast / ref.test on the member struct; field read one struct.get; equality, toStr, Json encode/decode, trait dispatch on union members all on the new layout.
|
|
131
|
+
**Test code:** add `match_member_fields`, `nested_enum_match`, `equality_and_toStr`, `json_round_trip_union` to unboxed_variants_e2e_test.rs; trmc_test struct.new-per-cell assertion 2 -> 1; whole plum-wasm-codegen suite, plum-core trmc_test and the full Plum sweep green.
|
|
132
|
+
|
|
133
|
+
## Cons List
|
|
134
|
+
|
|
135
|
+
### T7 std cons List · Complexity 4 · Est. context 45k
|
|
136
|
+
**Owns:** plum-std/List.plum, plum-examples/cons_list.plum
|
|
137
|
+
**Components:** 1
|
|
138
|
+
**LOC owned:** 170
|
|
139
|
+
**Tests:** fast
|
|
140
|
+
**Model:** sonnet
|
|
141
|
+
**Consumes:** EnumName(args) resolves to the enum's static `init`
|
|
142
|
+
**Produces:** std/List: `enum List[T] = Nil | Cons(head: T, tail: List[T])` with init, map, filter, fold, reverse, append, length, at, isEmpty, toStr, equals, a `fun*` iterator, toSlice, toArray; `Slice.toList()` added in plum-std/Slice.plum (allowed edit)
|
|
143
|
+
e.g. `List(1, 2, 3, 4).fold(0, |a, x| a + x)` -> `10`
|
|
144
|
+
e.g. `List(1, 2, 3).map(|x| x * 2).toSlice()` -> `Slice(2, 4, 6)`
|
|
145
|
+
**Review Focus:** deep R2: recursive definitions that tail-call or TRMC (constant stack at 1M); `for x := range xs` works; no old-List use type-checks against it (grep plum-std plum-examples for `List` outside this file and cons_list.plum: expect none).
|
|
146
|
+
**Test code:** cons_list.plum: `init_sum`, `map_filter_fold`, `reverse_append`, `for_range`, `to_slice_round_trip`, `to_array`, `map_million` (1,000,000), `fold_million`, `equals_and_toStr`.
|
|
147
|
+
|
|
148
|
+
### T8 move recursive code to std List · Complexity 4 · Est. context 50k
|
|
149
|
+
**Owns:** plum-std/game/Picture.plum, plum-examples/game_state_view.plum, plum-examples/functional.plum, plum-examples/tail_calls.plum, plum-game/web-test/fixtures/stress_state_view.plum, plum-game/web-test/fixtures/stress_state_view_deep.plum
|
|
150
|
+
**Components:** 2
|
|
151
|
+
**LOC owned:** 160
|
|
152
|
+
**Tests:** fast
|
|
153
|
+
**Model:** sonnet
|
|
154
|
+
**Consumes:** none
|
|
155
|
+
**Produces:** none
|
|
156
|
+
**Review Focus:** state: local `Link`/`Nodes` types replaced by std `List` (Picture children `List[SceneNode]`); same test names and values; functional.plum keeps teaching value (its own Link section may stay as the "how it's built" example if removing it loses that — say which).
|
|
157
|
+
**Test code:** all six files green with unchanged test names/values; `cargo test --manifest-path plum-game/web-test/Cargo.toml --test web_check state_view` passes.
|
|
158
|
+
|
|
159
|
+
## Wrap-up
|
|
160
|
+
|
|
161
|
+
### T9 perf and docs · Complexity 2 · Est. context 30k
|
|
162
|
+
**Owns:** README.md, docs/plans/2026-11-03-unboxed-variants.benchmark.md
|
|
163
|
+
**Components:** 1
|
|
164
|
+
**LOC owned:** 80
|
|
165
|
+
**Tests:** fast
|
|
166
|
+
**Model:** sonnet
|
|
167
|
+
**Consumes:** none
|
|
168
|
+
**Produces:** none
|
|
169
|
+
**Review Focus:** numeric: 1M `List` build+walk timed before (git stash-free: use the T4 checkpoint binary or note) and after; README sections: List (cons, default for recursion), Slice (growable view over Array), Array (fixed, unchanged), `Type(...)` calls init for enums.
|
|
170
|
+
**Test code:** README examples compile (scratch file under /private/tmp/claude-501/); benchmark doc with the timing table.
|
|
171
|
+
|
|
172
|
+
## Review Focus
|
|
173
|
+
- compiler: deep, state, encoding. Risks: incomplete rename; layout regressions in match/Json/toStr; init vs variant resolution.
|
|
174
|
+
**Probe entry:** run=./target/debug/plum test
|
docs/plans/2026-11-03-unboxed-variants.progress.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Unboxed variants / Slice / cons List run
|
|
2
|
+
- start 614ddf5
|
|
3
|
+
- Ruling: skip T0 Wave 0 - no Plum/Rust stub generation - cost if wrong: none
|
|
4
|
+
- unblocked: dispatched T1 T4 at 18:15:08
|
|
5
|
+
- checkpoint T1 5a9780d17e rename List to Slice: compiler and std
|
|
6
|
+
- T1 checkpointed 5a9780d (std + compiler special cases; plum-std 224/0)
|
|
7
|
+
- unblocked: dispatched T2 T3 at 18:17:17
|
|
8
|
+
- Ruling: delete stale probe fixture stress_pure_scene_inplace.plum - unchecked in-place writes now rejected by the ownership checker (Actor is own); its result is recorded in own-record.benchmark.md; stays in git history - cost if wrong: re-create from history
|
|
9
|
+
- checkpoint T2 00cc295e01 rename List to Slice: examples and fixtures
|
|
10
|
+
- checkpoint T4 646ad682c8 enum init routing
|
|
11
|
+
- T4 checkpointed 646ad68 (enum init routing)
|
|
12
|
+
- F-T4-1 queued: bare `out := Nil` in a generic fn emits invalid wasm (type mismatch ref null) - fixer after T6 (codegen file busy)
|
|
13
|
+
- unblocked: dispatched T5 at 18:20:47; T3 running
|
|
14
|
+
- checkpoint T3 817c58420e rename List to Slice: Rust tests and README
|
|
15
|
+
- T3 checkpointed 817c584 (Rust tests + README; checker 159/1 known, codegen 186/0, host 17)
|
|
16
|
+
- unblocked: dispatched T7 at 18:21:15 (T5 running)
|
|
17
|
+
- checkpoint T7 34f6456e45 std cons List
|
|
18
|
+
- T7 checkpointed 34f6456: std cons List (9/9 incl 1M). Concerns: Cons/Nil now global std names (local Link enums collide -> T8 migrates); for-range over List quadratic (F-T7-1 fixer); TRMC doesn't fire on methods -> map delegates to exported free fn mapCells (follow-up)
|
|
19
|
+
- unblocked: dispatched T8 + F-T7-1 fixer at 18:25:26 (T5 running)
|
|
20
|
+
- checkpoint T5 b881bc7f8e unboxed variants: layout and construction
|
|
21
|
+
- T5 checkpointed (unboxed layout: member records subtypes of enum supertype, no wrapper; 4 tests pass; matching already works via trait-member path)
|
|
22
|
+
- Ruling: Slice must not import List (remove Slice.toList, add List.fromSlice) - std Cons/Nil leaked into every Slice user, 17 codegen tests failed to load - cost if wrong: one less convenience method
|
|
23
|
+
- F-T7-2 to fixer-sonnet at 18:25:47
|
|
24
|
+
- unblocked: dispatched T6 at 18:26:02
|
|
25
|
+
- checkpoint T8 1d2cc48c6c move recursive code to std List
|
|
26
|
+
- T8 checkpointed: Picture/state_view/fixtures/tail_calls on std List; functional.plum keeps its own Chain = Empty | Link teaching section; sweep 620/0, web_check state_view ok
|
|
27
|
+
- checkpoint T1 ca5805e887 rename List to Slice: compiler and std
|
|
28
|
+
- F-T7-1 FIXED: for x := range xs uses fun* items when declared (std List); 1M sum 0.2s
|
|
29
|
+
- checkpoint T7 dba185aa2b std cons List
|
|
30
|
+
- F-T7-2 FIXED (Slice no longer imports List; List.fromSlice). Watch: codegen_tests terminal_mid_chain_error (filter().take) failing - check after T6 (possible forms items lookup)
|
|
31
|
+
- checkpoint T6 a035d2a6dc unboxed variants: match, access, equality, toStr, Json
|
|
32
|
+
- T6 checkpointed a035d2a: no codegen change needed for access/eq/toStr/Json; trmc 1 struct.new/cell; 8 unboxed tests. Notes: == on records/enums is reference identity; Json.decode into a union fails ('members disagree') - follow-ups
|
|
33
|
+
- F-T7-1b: terminal_mid_chain_error regressed (error text) by items lookup -> back to same fixer
|
|
34
|
+
- unblocked: dispatched T9 at 18:30:03
|
|
35
|
+
- checkpoint T9 ee2f11856b perf and docs
|
|
36
|
+
- T9 checkpointed: 1M fromTo+length 1238 -> 192 ms (6.4x); README List/Slice/Array + enum init
|
|
37
|
+
- checkpoint T1 601fb9237d rename List to Slice: compiler and std
|
|
38
|
+
- F-T7-1b FIXED: resolve() returns early when fatal, mid-chain error text restored
|
|
39
|
+
- Final review CHANGES_REQUESTED: deep + state probe categories unverified -> F-FR-1 fixer-sonnet-high (permanent tests in plum-examples/unboxed_variants.plum)
|
|
40
|
+
- tests full passed=80 failed=1 skipped=0 seconds=39.9
|
|
41
|
+
- F-FR-1 FIXED: 9 deep/state probes in plum-examples/unboxed_variants.plum, no defects; full suite only known generic_funstar_wired. Final review findings addressed.
|
docs/plans/2026-11-03-unboxed-variants.report.json
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
{
|
|
2
|
+
"projection_note": "projection: fixed forecast saved at lint 2026-11-03T18:14:44",
|
|
3
|
+
"headline": {
|
|
4
|
+
"wall_min": {
|
|
5
|
+
"projected": 29.698,
|
|
6
|
+
"actual": 16.277
|
|
7
|
+
},
|
|
8
|
+
"cost_usd_agents": {
|
|
9
|
+
"projected": 0.888557,
|
|
10
|
+
"actual": 2.2806
|
|
11
|
+
},
|
|
12
|
+
"task_min": {
|
|
13
|
+
"projected": 54.511,
|
|
14
|
+
"actual": 27.99
|
|
15
|
+
},
|
|
16
|
+
"agents": {
|
|
17
|
+
"projected": 9,
|
|
18
|
+
"actual": 24
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"feature": null,
|
|
22
|
+
"cost_split": {
|
|
23
|
+
"agents_usd": 1.965493,
|
|
24
|
+
"rework_usd": 0.315107,
|
|
25
|
+
"coordinator_usd": 44.341197,
|
|
26
|
+
"total_usd": 46.621797
|
|
27
|
+
},
|
|
28
|
+
"concurrency": {
|
|
29
|
+
"peak": 3,
|
|
30
|
+
"projected_peak": 3
|
|
31
|
+
},
|
|
32
|
+
"critical_path": {
|
|
33
|
+
"actual": [
|
|
34
|
+
"T1",
|
|
35
|
+
"T3",
|
|
36
|
+
"T7"
|
|
37
|
+
],
|
|
38
|
+
"projected": [
|
|
39
|
+
"T1",
|
|
40
|
+
"T2",
|
|
41
|
+
"T7",
|
|
42
|
+
"T8",
|
|
43
|
+
"T9"
|
|
44
|
+
],
|
|
45
|
+
"projected_min": 29.698
|
|
46
|
+
},
|
|
47
|
+
"model_mix": {
|
|
48
|
+
"sonnet": {
|
|
49
|
+
"tasks": 8,
|
|
50
|
+
"cost_usd": 2.259574
|
|
51
|
+
},
|
|
52
|
+
"haiku": {
|
|
53
|
+
"tasks": 1,
|
|
54
|
+
"cost_usd": 0.021026
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"coordinator_cost_by_model": {
|
|
58
|
+
"opus": 44.341197
|
|
59
|
+
},
|
|
60
|
+
"quality": {
|
|
61
|
+
"tests": {
|
|
62
|
+
"passed": 80,
|
|
63
|
+
"failed": 1,
|
|
64
|
+
"skipped": 0,
|
|
65
|
+
"seconds": 39.9,
|
|
66
|
+
"mode": "full",
|
|
67
|
+
"failures": [
|
|
68
|
+
"-::generic_funstar_wired: failed"
|
|
69
|
+
],
|
|
70
|
+
"skipped_tests": []
|
|
71
|
+
},
|
|
72
|
+
"first_time_pass": 0,
|
|
73
|
+
"first_time_n": 0,
|
|
74
|
+
"rework_dispatches": 2,
|
|
75
|
+
"open_incidents": 1
|
|
76
|
+
},
|
|
77
|
+
"tasks": [
|
|
78
|
+
{
|
|
79
|
+
"task": "T1",
|
|
80
|
+
"model": "sonnet",
|
|
81
|
+
"pred_min": 7.016,
|
|
82
|
+
"actual_min": 1.73,
|
|
83
|
+
"delta": {
|
|
84
|
+
"abs": -5.286,
|
|
85
|
+
"pct": -75.3
|
|
86
|
+
},
|
|
87
|
+
"cost_usd": 0.119092,
|
|
88
|
+
"first_time_pass": null
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"task": "T8",
|
|
92
|
+
"model": "sonnet",
|
|
93
|
+
"pred_min": 7.367,
|
|
94
|
+
"actual_min": 2.417,
|
|
95
|
+
"delta": {
|
|
96
|
+
"abs": -4.9503,
|
|
97
|
+
"pct": -67.2
|
|
98
|
+
},
|
|
99
|
+
"cost_usd": 0.201661,
|
|
100
|
+
"first_time_pass": null
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"task": "T6",
|
|
104
|
+
"model": "sonnet",
|
|
105
|
+
"pred_min": 7.746,
|
|
106
|
+
"actual_min": 3.307,
|
|
107
|
+
"delta": {
|
|
108
|
+
"abs": -4.4393,
|
|
109
|
+
"pct": -57.3
|
|
110
|
+
},
|
|
111
|
+
"cost_usd": 0.308496,
|
|
112
|
+
"first_time_pass": null
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"task": "T7",
|
|
116
|
+
"model": "sonnet",
|
|
117
|
+
"pred_min": 7.396,
|
|
118
|
+
"actual_min": 3.617,
|
|
119
|
+
"delta": {
|
|
120
|
+
"abs": -3.7793,
|
|
121
|
+
"pct": -51.1
|
|
122
|
+
},
|
|
123
|
+
"cost_usd": 0.599778,
|
|
124
|
+
"first_time_pass": null
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"task": "T5",
|
|
128
|
+
"model": "sonnet",
|
|
129
|
+
"pred_min": 7.746,
|
|
130
|
+
"actual_min": 4.338,
|
|
131
|
+
"delta": {
|
|
132
|
+
"abs": -3.4077,
|
|
133
|
+
"pct": -44.0
|
|
134
|
+
},
|
|
135
|
+
"cost_usd": 0.48981,
|
|
136
|
+
"first_time_pass": null
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"task": "T9",
|
|
140
|
+
"model": "sonnet",
|
|
141
|
+
"pred_min": 4.24,
|
|
142
|
+
"actual_min": 1.07,
|
|
143
|
+
"delta": {
|
|
144
|
+
"abs": -3.17,
|
|
145
|
+
"pct": -74.8
|
|
146
|
+
},
|
|
147
|
+
"cost_usd": 0.133305,
|
|
148
|
+
"first_time_pass": null
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"task": "T2",
|
|
152
|
+
"model": "sonnet",
|
|
153
|
+
"pred_min": 3.679,
|
|
154
|
+
"actual_min": 2.51,
|
|
155
|
+
"delta": {
|
|
156
|
+
"abs": -1.169,
|
|
157
|
+
"pct": -31.8
|
|
158
|
+
},
|
|
159
|
+
"cost_usd": 0.133804,
|
|
160
|
+
"first_time_pass": null
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"task": "T4",
|
|
164
|
+
"model": "sonnet",
|
|
165
|
+
"pred_min": 5.642,
|
|
166
|
+
"actual_min": 5.353,
|
|
167
|
+
"delta": {
|
|
168
|
+
"abs": -0.2887,
|
|
169
|
+
"pct": -5.1
|
|
170
|
+
},
|
|
171
|
+
"cost_usd": 0.273628,
|
|
172
|
+
"first_time_pass": null
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"task": "T3",
|
|
176
|
+
"model": "haiku",
|
|
177
|
+
"pred_min": 3.679,
|
|
178
|
+
"actual_min": 3.648,
|
|
179
|
+
"delta": {
|
|
180
|
+
"abs": -0.0307,
|
|
181
|
+
"pct": -0.8
|
|
182
|
+
},
|
|
183
|
+
"cost_usd": 0.021026,
|
|
184
|
+
"first_time_pass": null
|
|
185
|
+
}
|
|
186
|
+
],
|
|
187
|
+
"cost_table": {
|
|
188
|
+
"source": "est",
|
|
189
|
+
"usd": 2.8374,
|
|
190
|
+
"tokens": 8182748,
|
|
191
|
+
"wall_min": 328846987.1948,
|
|
192
|
+
"rows": [
|
|
193
|
+
{
|
|
194
|
+
"agent": "a032d5337feb97389",
|
|
195
|
+
"model": "sonnet",
|
|
196
|
+
"wall_min": 2.4293,
|
|
197
|
+
"tokens": 422013,
|
|
198
|
+
"usd": 0.2017
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"agent": "a1f5fcda3239f9191",
|
|
202
|
+
"model": "sonnet",
|
|
203
|
+
"wall_min": 1.735,
|
|
204
|
+
"tokens": 216036,
|
|
205
|
+
"usd": 0.1191
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"agent": "a1fb8294a41bfce90",
|
|
209
|
+
"model": "sonnet",
|
|
210
|
+
"wall_min": 3.1865,
|
|
211
|
+
"tokens": 153822,
|
|
212
|
+
"usd": 0.0828
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
"agent": "a2d17c86204e4ed8e",
|
|
216
|
+
"model": "sonnet",
|
|
217
|
+
"wall_min": 29895181.6431,
|
|
218
|
+
"tokens": 0,
|
|
219
|
+
"usd": 0.0
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
"agent": "a2f7f372bcc1913c7",
|
|
223
|
+
"model": "sonnet",
|
|
224
|
+
"wall_min": 29895165.2396,
|
|
225
|
+
"tokens": 0,
|
|
226
|
+
"usd": 0.0
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
"agent": "a37b5a6af63f1a35f",
|
|
230
|
+
"model": "sonnet",
|
|
231
|
+
"wall_min": 4.1535,
|
|
232
|
+
"tokens": 1512901,
|
|
233
|
+
"usd": 0.4462
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"agent": "a38cfc8fbcccfabf6",
|
|
237
|
+
"model": "sonnet",
|
|
238
|
+
"wall_min": 29895178.0764,
|
|
239
|
+
"tokens": 0,
|
|
240
|
+
"usd": 0.0
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
"agent": "a622bd7aabd02b433",
|
|
244
|
+
"model": "opus",
|
|
245
|
+
"wall_min": 0.1906,
|
|
246
|
+
"tokens": 31675,
|
|
247
|
+
"usd": 0.1106
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"agent": "a71ba03ecb85f82fb",
|
|
251
|
+
"model": "sonnet",
|
|
252
|
+
"wall_min": 2.5291,
|
|
253
|
+
"tokens": 385313,
|
|
254
|
+
"usd": 0.1338
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
"agent": "a78e2c30b9e5bd17e",
|
|
258
|
+
"model": "sonnet",
|
|
259
|
+
"wall_min": 5.9922,
|
|
260
|
+
"tokens": 754025,
|
|
261
|
+
"usd": 0.2323
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
"agent": "ab29ed3c92962597d",
|
|
265
|
+
"model": "sonnet",
|
|
266
|
+
"wall_min": 4.3458,
|
|
267
|
+
"tokens": 1336407,
|
|
268
|
+
"usd": 0.4898
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
"agent": "aba6257bd097fbcb1",
|
|
272
|
+
"model": "sonnet",
|
|
273
|
+
"wall_min": 29895167.3565,
|
|
274
|
+
"tokens": 0,
|
|
275
|
+
"usd": 0.0
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
"agent": "abc8fd2829ba26b9b",
|
|
279
|
+
"model": "sonnet",
|
|
280
|
+
"wall_min": 29895179.1512,
|
|
281
|
+
"tokens": 0,
|
|
282
|
+
"usd": 0.0
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
"agent": "abde52fdf4975b0da",
|
|
286
|
+
"model": "sonnet",
|
|
287
|
+
"wall_min": 29895180.1306,
|
|
288
|
+
"tokens": 0,
|
|
289
|
+
"usd": 0.0
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
"agent": "ac304e1fc35702cc2",
|
|
293
|
+
"model": "sonnet",
|
|
294
|
+
"wall_min": 5.3626,
|
|
295
|
+
"tokens": 690457,
|
|
296
|
+
"usd": 0.2736
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
"agent": "ac3b989eb4e21cc20",
|
|
300
|
+
"model": "sonnet",
|
|
301
|
+
"wall_min": 1.0814,
|
|
302
|
+
"tokens": 224911,
|
|
303
|
+
"usd": 0.1333
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
"agent": "ac3e029b64cf4ecaa",
|
|
307
|
+
"model": "haiku",
|
|
308
|
+
"wall_min": 3.6506,
|
|
309
|
+
"tokens": 807479,
|
|
310
|
+
"usd": 0.021
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
"agent": "ac46f3266ab816bb3",
|
|
314
|
+
"model": "sonnet",
|
|
315
|
+
"wall_min": 29895178.9264,
|
|
316
|
+
"tokens": 0,
|
|
317
|
+
"usd": 0.0
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
"agent": "ac5d6b29fb74e5a15",
|
|
321
|
+
"model": "sonnet",
|
|
322
|
+
"wall_min": 3.632,
|
|
323
|
+
"tokens": 836690,
|
|
324
|
+
"usd": 0.2847
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
"agent": "ad4ad08885dbf63fc",
|
|
328
|
+
"model": "sonnet",
|
|
329
|
+
"wall_min": 29895166.2977,
|
|
330
|
+
"tokens": 0,
|
|
331
|
+
"usd": 0.0
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
"agent": "ade129f06dbb62201",
|
|
335
|
+
"model": "sonnet",
|
|
336
|
+
"wall_min": 29895182.1857,
|
|
337
|
+
"tokens": 0,
|
|
338
|
+
"usd": 0.0
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
"agent": "adf8dc300c27c5d5f",
|
|
342
|
+
"model": "sonnet",
|
|
343
|
+
"wall_min": 29895185.31,
|
|
344
|
+
"tokens": 0,
|
|
345
|
+
"usd": 0.0
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
"agent": "af12c9f024ff3664d",
|
|
349
|
+
"model": "sonnet",
|
|
350
|
+
"wall_min": 29895181.2657,
|
|
351
|
+
"tokens": 0,
|
|
352
|
+
"usd": 0.0
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
"agent": "afdafaeb17521b00e",
|
|
356
|
+
"model": "sonnet",
|
|
357
|
+
"wall_min": 3.3233,
|
|
358
|
+
"tokens": 811019,
|
|
359
|
+
"usd": 0.3085
|
|
360
|
+
}
|
|
361
|
+
]
|
|
362
|
+
}
|
|
363
|
+
}
|
docs/plans/2026-11-03-unboxed-variants.report.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# superlite build report
|
|
2
|
+
|
|
3
|
+
projection: fixed forecast saved at lint 2026-11-03T18:14:44
|
|
4
|
+
|
|
5
|
+
Projected Actual Delta
|
|
6
|
+
wall-clock (min) 29.7 16.3 -13.4 (-45%)
|
|
7
|
+
agent cost (USD, +rework) 0.89 2.28 +1.39 (+157%)
|
|
8
|
+
task-minutes 54.5 28.0 -26.5 (-49%)
|
|
9
|
+
agents 9 24 +15 (+167%)
|
|
10
|
+
cost split: agents $1.97, rework $0.32, coordinator $44.34, total $46.62
|
|
11
|
+
concurrency: peak 3 at once (projected 3); critical path actual T1 T3 T7; projected T1 > T2 > T7 > T8 > T9 (29.7 min)
|
|
12
|
+
model mix: haiku 1 tasks $0.02, sonnet 8 tasks $2.26, coordinator opus $44.34
|
|
13
|
+
quality: tests 80 passed, 1 failed; first-time pass n/a; rework dispatches 2; open incidents 1
|
|
14
|
+
|
|
15
|
+
task model pred actual delta cost
|
|
16
|
+
T1 sonnet 7.0 1.7 -5.3 (-75%) $0.119
|
|
17
|
+
T8 sonnet 7.4 2.4 -5.0 (-67%) $0.202
|
|
18
|
+
T6 sonnet 7.7 3.3 -4.4 (-57%) $0.308
|
|
19
|
+
T7 sonnet 7.4 3.6 -3.8 (-51%) $0.600
|
|
20
|
+
T5 sonnet 7.7 4.3 -3.4 (-44%) $0.490
|
|
21
|
+
(+4 smaller misses; `superlite-cli report --json` lists all)
|
|
22
|
+
|
|
23
|
+
agent cost table:
|
|
24
|
+
| agent | model | wall_min | tokens | usd |
|
|
25
|
+
| --- | --- | --- | --- | --- |
|
|
26
|
+
| a032d5337feb97389 | sonnet | 2.4293 | 422013 | 0.2017 |
|
|
27
|
+
| a1f5fcda3239f9191 | sonnet | 1.735 | 216036 | 0.1191 |
|
|
28
|
+
| a1fb8294a41bfce90 | sonnet | 3.1865 | 153822 | 0.0828 |
|
|
29
|
+
| a2d17c86204e4ed8e | sonnet | 29895181.6431 | 0 | 0.0 |
|
|
30
|
+
| a2f7f372bcc1913c7 | sonnet | 29895165.2396 | 0 | 0.0 |
|
|
31
|
+
| a37b5a6af63f1a35f | sonnet | 4.1535 | 1512901 | 0.4462 |
|
|
32
|
+
| a38cfc8fbcccfabf6 | sonnet | 29895178.0764 | 0 | 0.0 |
|
|
33
|
+
| a622bd7aabd02b433 | opus | 0.1906 | 31675 | 0.1106 |
|
|
34
|
+
| a71ba03ecb85f82fb | sonnet | 2.5291 | 385313 | 0.1338 |
|
|
35
|
+
| a78e2c30b9e5bd17e | sonnet | 5.9922 | 754025 | 0.2323 |
|
|
36
|
+
| ab29ed3c92962597d | sonnet | 4.3458 | 1336407 | 0.4898 |
|
|
37
|
+
| aba6257bd097fbcb1 | sonnet | 29895167.3565 | 0 | 0.0 |
|
|
38
|
+
| abc8fd2829ba26b9b | sonnet | 29895179.1512 | 0 | 0.0 |
|
|
39
|
+
| abde52fdf4975b0da | sonnet | 29895180.1306 | 0 | 0.0 |
|
|
40
|
+
| ac304e1fc35702cc2 | sonnet | 5.3626 | 690457 | 0.2736 |
|
|
41
|
+
| ac3b989eb4e21cc20 | sonnet | 1.0814 | 224911 | 0.1333 |
|
|
42
|
+
| ac3e029b64cf4ecaa | haiku | 3.6506 | 807479 | 0.021 |
|
|
43
|
+
| ac46f3266ab816bb3 | sonnet | 29895178.9264 | 0 | 0.0 |
|
|
44
|
+
| ac5d6b29fb74e5a15 | sonnet | 3.632 | 836690 | 0.2847 |
|
|
45
|
+
| ad4ad08885dbf63fc | sonnet | 29895166.2977 | 0 | 0.0 |
|
|
46
|
+
| ade129f06dbb62201 | sonnet | 29895182.1857 | 0 | 0.0 |
|
|
47
|
+
| adf8dc300c27c5d5f | sonnet | 29895185.31 | 0 | 0.0 |
|
|
48
|
+
| af12c9f024ff3664d | sonnet | 29895181.2657 | 0 | 0.0 |
|
|
49
|
+
| afdafaeb17521b00e | sonnet | 3.3233 | 811019 | 0.3085 |
|
|
50
|
+
| totals | | 328846987.1948 | 8182748 | 2.8374 |
|
|
51
|
+
cost source: est
|
|
52
|
+
|
|
53
|
+
takeaway: ran 45% faster than projected; cost 157% over; biggest miss T1
|
docs/plans/2026-11-03-unboxed-variants.review.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Unboxed variants, enum constructors, and List / Slice: review plan
|
|
2
|
+
|
|
3
|
+
**Builds on:** branch `pure-scene-values` at 614ddf5: tail calls round 2, single-pass TRMC, wasmtime at opt-level 3 in dev, 611 Plum tests. Related: docs/plans/2026-11-02-go-slices.review.md, an older Go-slices idea that this plan supersedes.
|
|
4
|
+
|
|
5
|
+
## Summary
|
|
6
|
+
Three parts. `Array` is unchanged: a fixed-length, contiguous wasm-gc array, like Go, C and Rust arrays.
|
|
7
|
+
|
|
8
|
+
1. **Unboxed variants (performance).** An inline variant (`Cons(head: T, tail: ..)`) and a union-enum member (`enum Shape = Circle | Rect`) are currently a hidden record wrapped in a hidden variant struct: two allocations per value. Subtyped traits already avoid this, with the member record as a wasm-gc subtype of the supertype. This part applies that layout to inline-variant and union enums.
|
|
9
|
+
2. **Enum constructors, Pyret style.** `List(1, 2, 3)` on an enum calls its static `fun init(values: ...T)`, the same way records already reach `init`. Today the checker accepts that call on an enum, but codegen fails with `unknown function`.
|
|
10
|
+
3. **Renames (user decision).**
|
|
11
|
+
- Today's growable `List[T]` becomes **`Slice[T]`**: the same record over an `Array`, with capacity growth and views. It is kept, and every use is renamed.
|
|
12
|
+
- The cons list (`Link` in the examples) becomes the new std **`List[T]`**: `enum List[T] = Nil | Cons(head: T, tail: List[T])` in `plum-std/List.plum`. It's the default list for recursive, pattern-matched code.
|
|
13
|
+
|
|
14
|
+
## What you will get
|
|
15
|
+
- **One allocation per variant value.** That covers every `Cons`, every union member, and every TRMC cell (trmc_test drops from 2 struct.new per cell to 1). Matching is one `br_on_cast`, and a field read is one `struct.get`.
|
|
16
|
+
- **`Type(args)` reaches `init` for enums.** Variadic or fixed `init`, generic `T` inferred from the arguments or the expected type. An enum without `init` gives the error ``X has no init; construct a variant or declare `fun init` ``. A variant name always wins.
|
|
17
|
+
- **std `List[T]` as a cons list:**
|
|
18
|
+
- `List(1, 2, 3)`, `Nil`, `Cons(head:, tail:)`;
|
|
19
|
+
- recursive `map`, `filter`, `fold`, `reverse`, `append`, `length`, `at`, `isEmpty`, `toStr`, `equals`;
|
|
20
|
+
- a `for x := range xs` iterator, so for-chains work;
|
|
21
|
+
- `toSlice()` and `toArray()`, with `Slice.toList()` going the other way.
|
|
22
|
+
|
|
23
|
+
Tail calls and TRMC keep these in constant stack.
|
|
24
|
+
- **`Slice[T]`:** today's List, renamed across std, examples, fixtures, tests, the README, the LSP and editor grammars, with no behaviour change. std functions that return a growable list today (`Str.split`, `Map.keys`/`values`, `Json` arrays, `readdir`, …) return `Slice`.
|
|
25
|
+
- **`Array[T]` unchanged** (user decision). Adding capacity would wrap it in a struct, costing an extra load on every get/set and changing what `length` means. `Slice` remains the growable type, like Go's `[]T` or Rust's `Vec`.
|
|
26
|
+
- **Recursive code moves to the new `List`.** Picture's `Nodes`, the state/view game's `Link`, functional.plum and tail_calls.plum drop their local `Link` types.
|
|
27
|
+
|
|
28
|
+
## What will be built
|
|
29
|
+
| Part | What it does | Size |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| Rename List -> Slice | Mechanical rename in std (List.plum -> Slice.plum, every user), checker and codegen special cases, examples, fixtures, Rust tests, README, LSP, editor grammar | L, mechanical, split by area |
|
|
32
|
+
| std cons List | plum-std/List.plum: enum + init + combinators + iterator + conversions; tests | M |
|
|
33
|
+
| Enum init routing | Checker and codegen: `EnumName(args)` resolves to the enum's `init`; inference; the clear error | S |
|
|
34
|
+
| Unboxed layout | Codegen: member structs as subtypes of the enum supertype; construction, then match / access / equality / toStr / Json | L, split in two |
|
|
35
|
+
| Move recursive code | Picture, game_state_view, stress fixtures, functional.plum, tail_calls.plum use std List | M |
|
|
36
|
+
| Tests, perf, docs | e2e per part; 1M list build and walk before and after; README sections for List / Slice / Array | S |
|
|
37
|
+
|
|
38
|
+
## How it fits together
|
|
39
|
+
```mermaid
|
|
40
|
+
flowchart LR
|
|
41
|
+
RN["rename List -> Slice"] --> CL["new std List (cons)"]
|
|
42
|
+
EI["enum init routing"] --> CL
|
|
43
|
+
UV["unboxed variants"] --> CL
|
|
44
|
+
CL --> MV["recursive code uses List"]
|
|
45
|
+
AR["Array (unchanged)"] --> SL["Slice: growable view over Array"]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Decisions you can change
|
|
49
|
+
- **What std returns.** Default: everything that returns `List` today returns `Slice` (a pure rename). Alternative: a few return the new cons `List` (for example `Str.split`).
|
|
50
|
+
- **`List(…)` call sites in existing code.** Default: after the rename every old `List(…)` becomes `Slice(…)`, and `List(…)` means the cons list. Alternative: also keep a deprecated `List` alias for one release. Not recommended, because the name now means the cons list.
|
|
51
|
+
|
|
52
|
+
## Risks
|
|
53
|
+
- **R1: rename breadth.** `List` is special-cased in the checker and codegen (for-loops, variadics, Json, Str, intrinsics). The rename is done in tested areas, one area per task, each green before the next.
|
|
54
|
+
- **R2: the new List shares a name with the old one.** The rename must fully finish before the cons List exists, so that no stray old `List` use type-checks against the new type by accident.
|
|
55
|
+
- **R3: the unboxed layout is wide.** It reuses the subtyped-trait path and is gated by the full suite.
|
|
56
|
+
|
|
57
|
+
## Out of scope
|
|
58
|
+
- The supercompiler (the next plan).
|
|
59
|
+
- Bracket literals and list patterns (dropped).
|
|
60
|
+
- kam_plum (outside this repo) needs the same `List` -> `Slice` rename. It's noted for you, not done here.
|
|
61
|
+
|
|
62
|
+
## How we will know it works
|
|
63
|
+
- After the rename, every suite stays green with no test changes other than names.
|
|
64
|
+
- New `List`: `List(1, 2, 3, 4)` sums to 10; map, filter, fold and reverse at 1M; `for` over a List; round-trips through `toSlice()` and `toArray()`.
|
|
65
|
+
- Unboxed variants: one struct.new per `Cons`; trmc at 1 per cell; 1M build and walk is about 2x faster than today.
|
|
66
|
+
- Every suite stays green (611+ Plum, Rust crates, host parity, browser checks), and README examples compile.
|
|
67
|
+
|
|
68
|
+
## Rough time and cost
|
|
69
|
+
About 8 parts. The rename comes first, then the cons List (after enum init routing and the unboxed layout), then the code moves. Rough agent time is 80–110 minutes.
|
plum-checker/src/lib.rs
CHANGED
|
@@ -5117,13 +5117,26 @@ fn inferExprInner(
|
|
|
5117
5117
|
// type's name is sugar for `Type.init(1, 2, 3)` (see the `ClassCall`
|
|
5118
5118
|
// zero-arg `Type()` handling); checked BEFORE the ordinary
|
|
5119
5119
|
// positional-variant-construction case just below.
|
|
5120
|
+
// An ENUM's own name (not one of its variants) routes the same way:
|
|
5121
|
+
// `Link(1, 2)` is `Link.init(1, 2)`; variant names win (they are in
|
|
5122
|
+
// `enum_variants` under their own key).
|
|
5123
|
+
let isEnumName = !ctx.enum_variants.contains_key(&call.name)
|
|
5124
|
+
&& ctx.enum_variants.values().any(|i| i.enum_name == call.name);
|
|
5125
|
+
let hasInit = ctx
|
|
5126
|
+
.methods
|
|
5127
|
+
.contains_key(&(call.name.clone(), "init".to_string()));
|
|
5128
|
+
if isEnumName && !hasInit {
|
|
5129
|
+
return Err(format!(
|
|
5130
|
+
"{} has no init; construct a variant or declare `fun init`",
|
|
5131
|
+
call.name
|
|
5132
|
+
));
|
|
5133
|
+
}
|
|
5120
|
-
if ctx
|
|
5134
|
+
if (ctx
|
|
5121
5135
|
.enum_variants
|
|
5122
5136
|
.get(&call.name)
|
|
5123
5137
|
.is_some_and(|info| info.enum_name == call.name)
|
|
5138
|
+
|| isEnumName)
|
|
5124
|
-
&&
|
|
5139
|
+
&& hasInit
|
|
5125
|
-
.methods
|
|
5126
|
-
.contains_key(&(call.name.clone(), "init".to_string()))
|
|
5127
5140
|
{
|
|
5128
5141
|
// The receiver is a temporary (never recorded); the ARGUMENTS stay
|
|
5129
5142
|
// the original nodes so their types are recorded for codegen.
|
plum-checker/src/ownership.rs
CHANGED
|
@@ -157,7 +157,7 @@ struct Cx<'a> {
|
|
|
157
157
|
selfTy: Option<String>,
|
|
158
158
|
/// Declared types of every local / param, own or not (for field typing).
|
|
159
159
|
tys: HashMap<String, String>,
|
|
160
|
-
/// `
|
|
160
|
+
/// `Slice[T]` locals whose element type `T` is known.
|
|
161
161
|
elems: HashMap<String, String>,
|
|
162
162
|
traits: HashMap<String, &'a TraitDecl>,
|
|
163
163
|
/// The current path ended in return/crash/break/continue.
|
|
@@ -222,7 +222,7 @@ impl<'a> Cx<'a> {
|
|
|
222
222
|
|
|
223
223
|
fn noteType(&mut self, name: &str, t: &Type) {
|
|
224
224
|
self.tys.insert(name.to_string(), t.name.clone());
|
|
225
|
-
if t.name == "
|
|
225
|
+
if t.name == "Slice" {
|
|
226
226
|
if let Some(e) = t.generics.first() {
|
|
227
227
|
if self.own.contains(&e.name) {
|
|
228
228
|
self.elems.insert(name.to_string(), e.name.clone());
|
plum-checker/tests/checker_tests.rs
CHANGED
|
@@ -137,11 +137,11 @@ fn errText(r: Result<(), Vec<CheckError>>) -> String {
|
|
|
137
137
|
// ---- structural generics (T2) ----
|
|
138
138
|
|
|
139
139
|
const GENERIC_DEFS: &str = "\
|
|
140
|
-
enum
|
|
140
|
+
enum Slice[T] =
|
|
141
|
-
|
|
|
141
|
+
| Slice(items: T)
|
|
142
142
|
|
|
143
|
-
fun map(self, cb: fn(T) -> U) ->
|
|
143
|
+
fun map(self, cb: fn(T) -> U) -> Slice[U] =
|
|
144
|
-
|
|
144
|
+
Slice(items: cb(self.items))
|
|
145
145
|
|
|
146
146
|
enum Box[T] =
|
|
147
147
|
| Box(value: T)
|
|
@@ -194,16 +194,16 @@ fn lastFnBodyType(src: &str) -> PlumType {
|
|
|
194
194
|
#[test]
|
|
195
195
|
fn method_own_generic_instantiated_per_call() {
|
|
196
196
|
assert_eq!(
|
|
197
|
-
lastFnBodyType("fun f(xs:
|
|
197
|
+
lastFnBodyType("fun f(xs: Slice[Int]) -> Slice[Int] =\n xs.map(|n| n * 2)\n"),
|
|
198
|
-
tapp("
|
|
198
|
+
tapp("Slice", vec![PlumType::TInt])
|
|
199
199
|
);
|
|
200
200
|
assert_eq!(
|
|
201
|
-
lastFnBodyType("fun f(xs:
|
|
201
|
+
lastFnBodyType("fun f(xs: Slice[Int]) -> Slice[Str] =\n xs.map(|n| \"{n}\")\n"),
|
|
202
|
-
tapp("
|
|
202
|
+
tapp("Slice", vec![plum_checker::types::strType()])
|
|
203
203
|
);
|
|
204
204
|
// The two instantiations are independent: Str result where Int is declared is an error.
|
|
205
205
|
let bad = checkSource(&parseClean(&format!(
|
|
206
|
-
"{}fun f(xs:
|
|
206
|
+
"{}fun f(xs: Slice[Int]) -> Slice[Int] =\n xs.map(|n| \"{{n}}\")\n",
|
|
207
207
|
GENERIC_DEFS
|
|
208
208
|
)));
|
|
209
209
|
assert!(errText(bad).contains("type mismatch"));
|
|
@@ -259,14 +259,14 @@ fn nested_instantiation_chain() {
|
|
|
259
259
|
#[test]
|
|
260
260
|
fn instantiation_set_listed() {
|
|
261
261
|
let src = format!(
|
|
262
|
-
"{}fun main() -> Int =\n a :=
|
|
262
|
+
"{}fun main() -> Int =\n a := Slice(items: 1).map(|n| n * 2)\n b := Slice(items: \"x\").map(|s| s)\n 0\n",
|
|
263
263
|
GENERIC_DEFS
|
|
264
264
|
);
|
|
265
265
|
let info = plum_checker::typedSource(&parseClean(&src)).unwrap_or_else(|e| panic!("{:?}", e));
|
|
266
266
|
let has = |t: PlumType| {
|
|
267
267
|
info.instantiations
|
|
268
268
|
.iter()
|
|
269
|
-
.any(|i| i.item == "
|
|
269
|
+
.any(|i| i.item == "Slice.map" && i.type_args.first() == Some(&t))
|
|
270
270
|
};
|
|
271
271
|
assert!(
|
|
272
272
|
has(PlumType::TInt) && has(plum_checker::types::strType()),
|
|
@@ -717,9 +717,9 @@ fn rejects_bad_generics_and_duck_typed_calls() {
|
|
|
717
717
|
r("fun identity(value: T) -> T =\n value\n\nfun a() -> Str =\n identity(5)\n", &["expected Str, found Int"]),
|
|
718
718
|
r("fun f(x: T) -> Int =\n x\n\nfun main() -> Int =\n f(\"s\")\n", &["in f[Str]"]),
|
|
719
719
|
r("enum Box[T] =\n | Box(value: T)\n\n fun get(self) -> Int =\n self.value\n\nfun main() -> Int =\n Box(value: \"s\").get()\n", &["in Box.get[Str]"]),
|
|
720
|
-
r(&generic("fun f() ->
|
|
720
|
+
r(&generic("fun f() -> Slice[Box[Str]] =\n Slice(items: Box(value: 1))\n"), &["variant 'Box' field 'value': type mismatch"]),
|
|
721
|
-
r(&generic("fun f() ->
|
|
721
|
+
r(&generic("fun f() -> Slice[Str] =\n Slice(items: 1).map(|x| x + 1)\n"), &["method 'Slice.map' arg 0: type mismatch"]),
|
|
722
|
-
r(&generic("fun f(xs:
|
|
722
|
+
r(&generic("fun f(xs: Slice[Int]) -> Slice[Int] =\n xs.map(|n| \"{n}\")\n"), &["type mismatch"]),
|
|
723
723
|
r("enum Parcel[T] =\n | Delivered(T)\n | Lost\n\nfun contentsOr(p: Parcel, default: Int) -> Int =\n match p\n Delivered(v) => v\n Lost => default\n", &["generic type 'Parcel' needs type arguments, e.g. Parcel[T]"]),
|
|
724
724
|
// error sets are covariant in generic arguments, not contravariant
|
|
725
725
|
r(&generic("fun narrow(b: Box[error{A}!Int]) -> Int =\n 1\n\nfun bad(b: Box[error{A, B}!Int]) -> Int =\n narrow(b)\n"), &["is not in the pinned error set error{A}"]),
|
|
@@ -744,8 +744,8 @@ fn rejects_bad_for_forms_and_fun_stars() {
|
|
|
744
744
|
r("fun main() -> Int =\n yield 1\n 0\n", &["yield is only allowed inside a fun*"]),
|
|
745
745
|
r("fun* broken(n: Int) *Int =\n yield n + nope\n\nfun main() -> Int =\n 0\n", &["fun* 'broken'", "nope"]),
|
|
746
746
|
r(&format!("{tw}fun main() -> Int =\n for i := range twice(4)\n undefinedName\n 0\n"), &["undefinedName"]),
|
|
747
|
-
r("fun* leak(src: *T) *T =\n saved := src\n for x := range saved\n yield x\n\nfun main() -> Int =\n for i := range leak(
|
|
748
|
-
r("fun* leak(src: *T) *T =\n for x := range src\n yield x\n src\n\nfun main() -> Int =\n for i := range leak(
|
|
747
|
+
r("fun* leak(src: *T) *T =\n saved := src\n for x := range saved\n yield x\n\nfun main() -> Int =\n for i := range leak(Slice(1))\n i\n 0\n", &["sequence parameter 'src' can only be used as a for source"]),
|
|
748
|
+
r("fun* leak(src: *T) *T =\n for x := range src\n yield x\n src\n\nfun main() -> Int =\n for i := range leak(Slice(1))\n i\n 0\n", &["sequence parameter 'src' can only be used as a for source"]),
|
|
749
749
|
]);
|
|
750
750
|
}
|
|
751
751
|
|
plum-checker/tests/inline_variants_tests.rs
CHANGED
|
@@ -90,7 +90,7 @@ fn explicit_type_args() {
|
|
|
90
90
|
fn generic_and_nested_construction() {
|
|
91
91
|
ok("fun cons(h: T, t: Link[T]) -> Link[T] =\n Cons(head: h, tail: t)");
|
|
92
92
|
ok("fun mk() -> Link[Int] =\n Cons(head: 1, tail: Cons(head: 2, tail: Nil))");
|
|
93
|
-
ok("fun mk() ->
|
|
93
|
+
ok("fun mk() -> Slice[Link[Int]] =\n Slice(Cons(head: 1, tail: Nil), Nil)\n\nfun pick(b: Bool) -> Link[Int] =\n if b\n Cons(head: 1, tail: Nil)\n else\n Nil");
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
#[test]
|
plum-checker/tests/ownership_flow_test.rs
CHANGED
|
@@ -135,19 +135,19 @@ fn embedding_own_makes_host_own() {
|
|
|
135
135
|
|
|
136
136
|
#[test]
|
|
137
137
|
fn list_copy_of_own_rejected() {
|
|
138
|
-
let t = texts("fun main(xs:
|
|
138
|
+
let t = texts("fun main(xs: Slice[Sprite], ys: Slice[Sprite]) =\n ys.push(xs.at(0))\n");
|
|
139
139
|
assert!(
|
|
140
140
|
t.iter().any(|m| m.starts_with("cannot store borrowed \"xs.at")),
|
|
141
141
|
"{t:?}"
|
|
142
142
|
);
|
|
143
143
|
let t = texts(
|
|
144
|
-
"fun main(xs:
|
|
144
|
+
"fun main(xs: Slice[Sprite], ys: Slice[Sprite]) =\n s := xs.at(0)\n ys.push(s)\n",
|
|
145
145
|
);
|
|
146
146
|
assert!(
|
|
147
147
|
t.iter().any(|m| m.starts_with("cannot store borrowed \"s\"")),
|
|
148
148
|
"{t:?}"
|
|
149
149
|
);
|
|
150
|
-
let ok = texts("fun main(xs:
|
|
150
|
+
let ok = texts("fun main(xs: Slice[Sprite]) -> Float =\n xs.at(0).x\n");
|
|
151
151
|
assert!(ok.is_empty(), "{ok:?}");
|
|
152
152
|
}
|
|
153
153
|
|
|
@@ -335,7 +335,7 @@ fn own_trait_field_write_on_own_self_ok() {
|
|
|
335
335
|
}
|
|
336
336
|
|
|
337
337
|
const HANDLED: &str = "\
|
|
338
|
-
own record Handled(actor: Sprite, events:
|
|
338
|
+
own record Handled(actor: Sprite, events: Slice[Sprite]) =
|
|
339
339
|
fun intoActor(own self) -> Sprite =
|
|
340
340
|
match self
|
|
341
341
|
Handled(a, _) => a
|
|
@@ -350,7 +350,7 @@ fn match_own_scrutinee_moves_out_ok() {
|
|
|
350
350
|
#[test]
|
|
351
351
|
fn match_own_scrutinee_then_use_rejected() {
|
|
352
352
|
let t = texts(
|
|
353
|
-
"own record Handled(actor: Sprite, events:
|
|
353
|
+
"own record Handled(actor: Sprite, events: Slice[Sprite])\n\nfun main(own h: Handled) -> Slice[Sprite] =\n match h\n Handled(a, _) => take(a)\n h.events\n",
|
|
354
354
|
);
|
|
355
355
|
assert!(hasMoved(&t, "h"), "{t:?}");
|
|
356
356
|
}
|
|
@@ -358,11 +358,11 @@ fn match_own_scrutinee_then_use_rejected() {
|
|
|
358
358
|
#[test]
|
|
359
359
|
fn match_borrowed_scrutinee_store_rejected() {
|
|
360
360
|
let t = texts(
|
|
361
|
-
"own record Handled(actor: Sprite, events:
|
|
361
|
+
"own record Handled(actor: Sprite, events: Slice[Sprite])\n\nfun main(h: Handled, xs: Slice[Sprite]) =\n match h\n Handled(a, _) => xs.push(a)\n",
|
|
362
362
|
);
|
|
363
363
|
assert!(t.iter().any(|m| m.starts_with("cannot store borrowed \"a\"")), "{t:?}");
|
|
364
364
|
let ok = texts(
|
|
365
|
-
"own record Handled(actor: Sprite, events:
|
|
365
|
+
"own record Handled(actor: Sprite, events: Slice[Sprite])\n\nfun main(h: Handled) -> Float =\n match h\n Handled(a, _) => a.x\n",
|
|
366
366
|
);
|
|
367
367
|
assert!(ok.is_empty(), "{ok:?}");
|
|
368
368
|
}
|
|
@@ -370,7 +370,15 @@ fn match_borrowed_scrutinee_store_rejected() {
|
|
|
370
370
|
#[test]
|
|
371
371
|
fn match_pattern_name_use_after_move() {
|
|
372
372
|
let t = texts(
|
|
373
|
-
"own record Handled(actor: Sprite, events:
|
|
373
|
+
"own record Handled(actor: Sprite, events: Slice[Sprite])\n\nfun main(own h: Handled) -> Float =\n match h\n Handled(a, _) =>\n t := take(a)\n a.x\n",
|
|
374
374
|
);
|
|
375
375
|
assert!(hasMoved(&t, "a"), "{t:?}");
|
|
376
376
|
}
|
|
377
|
+
|
|
378
|
+
#[test]
|
|
379
|
+
fn own_union_member_use_after_move() {
|
|
380
|
+
let t = texts_raw(
|
|
381
|
+
"own record Token(id: Int)\nown record Stamp(id: Int)\nenum Mark = Token | Stamp\n\nfun takeMark(own m: Mark) -> Mark = m\n\nfun main() -> Int =\n t := Token(id: 3)\n m: Mark := takeMark(t)\n t.id\n",
|
|
382
|
+
);
|
|
383
|
+
assert!(hasMoved(&t, "t"), "{t:?}");
|
|
384
|
+
}
|
plum-checker/tests/ownership_moves_test.rs
CHANGED
|
@@ -89,7 +89,7 @@ fn borrow_reads_do_not_move() {
|
|
|
89
89
|
|
|
90
90
|
#[test]
|
|
91
91
|
fn store_borrow_rejected() {
|
|
92
|
-
let t = texts("fun keep(s: Sprite, xs:
|
|
92
|
+
let t = texts("fun keep(s: Sprite, xs: Slice[Sprite]) =\n xs.push(s)\n");
|
|
93
93
|
assert!(
|
|
94
94
|
t.contains(&"cannot store borrowed \"s\"; pass it with own or move it".to_string()),
|
|
95
95
|
"{t:?}"
|
plum-checker/tests/types_tests.rs
CHANGED
|
@@ -10,12 +10,12 @@ fn pinned_a(t: plum_checker::types::PlumType) -> plum_checker::types::PlumType {
|
|
|
10
10
|
|
|
11
11
|
#[test]
|
|
12
12
|
fn substitute_nested() {
|
|
13
|
-
let ty = TApp("
|
|
13
|
+
let ty = TApp("Slice".into(), vec![pinned_a(TParam("T".into()))]);
|
|
14
14
|
let mut s = BTreeMap::new();
|
|
15
15
|
s.insert("T".to_string(), TInt);
|
|
16
16
|
let out = substitute(&ty, &s);
|
|
17
|
-
assert_eq!(out.to_string(), "
|
|
17
|
+
assert_eq!(out.to_string(), "Slice[error{A}!Int]");
|
|
18
|
-
assert_eq!(out, TApp("
|
|
18
|
+
assert_eq!(out, TApp("Slice".into(), vec![pinned_a(TInt)]));
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
#[test]
|
|
@@ -45,10 +45,10 @@ fn display_round_trips() {
|
|
|
45
45
|
"Map".into(),
|
|
46
46
|
vec![
|
|
47
47
|
plum_checker::types::strType(),
|
|
48
|
-
TApp("
|
|
48
|
+
TApp("Slice".into(), vec![TInt])
|
|
49
49
|
]
|
|
50
50
|
)
|
|
51
51
|
.to_string(),
|
|
52
|
-
"Map[Str,
|
|
52
|
+
"Map[Str, Slice[Int]]"
|
|
53
53
|
);
|
|
54
54
|
}
|
plum-checker/tests/unions_tests.rs
CHANGED
|
@@ -317,7 +317,7 @@ fn rejects_bad_member_coercions() {
|
|
|
317
317
|
(COERCE, "fun mk() -> Shape =\n Oval(a: 1.0)", &["expected Shape, found Oval"]),
|
|
318
318
|
(COERCE, "fun take(s: Shape) -> Int =\n 1\n\nfun go() -> Int =\n take(Oval(a: 1.0))", &["expected Shape, found Oval"]),
|
|
319
319
|
(COERCE, "fun take(s: Shape) -> Int =\n 1\n\nfun go() -> Int =\n take(\"x\")", &["expected Shape, found Str"]),
|
|
320
|
-
(COERCE, "fun take(xs:
|
|
320
|
+
(COERCE, "fun take(xs: Slice[Shape]) -> Int =\n 1\n\nfun go(cs: Slice[Circle]) -> Int =\n take(cs)", &["expected Slice[Shape], found Slice[Circle]"]),
|
|
321
321
|
// annotated declarations
|
|
322
322
|
(COERCE, "fun go() -> Int =\n s: Int := \"x\"\n 1", &["assign 's'", "mismatch"]),
|
|
323
323
|
(COERCE, "fun go() -> Int =\n s: Shape = Circle(r: 1.0)\n 1", &["annotated declarations use :="]),
|
plum-core/src/forms.rs
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
//! expression is the value of the `yield`.
|
|
18
18
|
//! * `for k, v := range xs` where the type of `xs` declares `fun* entries(self)`: the
|
|
19
19
|
//! same as `for k, v := range xs.entries()`.
|
|
20
|
+
//! * `for x := range xs` where the type of `xs` declares `fun* items(self)` (std `List`,
|
|
21
|
+
//! whose `at(i)` is O(n)): the same as `for x := range xs.items()`.
|
|
20
22
|
//! * anything else: plain iteration over `length()` / `at(i)`, lowered to a
|
|
21
23
|
//! `while` loop; with two binders they are `(position, element)`.
|
|
22
24
|
//!
|
|
@@ -213,7 +215,7 @@ pub fn prepareForms(source: &mut Source) {
|
|
|
213
215
|
}
|
|
214
216
|
|
|
215
217
|
/// The check copy of an `extend Seq[T]` method: a free fn whose first parameter
|
|
216
|
-
/// `self` is a `
|
|
218
|
+
/// `self` is a `Slice[T]` standing for the sequence.
|
|
217
219
|
fn seqCheckCopy(f: &Fn) -> Fn {
|
|
218
220
|
let mut c = f.clone();
|
|
219
221
|
c.name = format!("{}_{}", SEQ, f.name);
|
|
@@ -224,7 +226,7 @@ fn seqCheckCopy(f: &Fn) -> Fn {
|
|
|
224
226
|
isOwn: false,
|
|
225
227
|
name: "self".to_string(),
|
|
226
228
|
ty: ParamType::Type(Type {
|
|
227
|
-
name: "
|
|
229
|
+
name: "Slice".to_string(),
|
|
228
230
|
generics: vec![Type {
|
|
229
231
|
name: "T".to_string(),
|
|
230
232
|
generics: vec![],
|
|
@@ -789,10 +791,14 @@ impl Lowerer<'_> {
|
|
|
789
791
|
}
|
|
790
792
|
_ => {}
|
|
791
793
|
}
|
|
794
|
+
if self.fatal {
|
|
795
|
+
return None;
|
|
796
|
+
}
|
|
792
|
-
if fl.binders.len()
|
|
797
|
+
if matches!(fl.binders.len(), 1 | 2) && !self.reg.methodNames.is_empty() {
|
|
798
|
+
let want = if fl.binders.len() == 2 { "entries" } else { "items" };
|
|
793
799
|
match (self.typeOf)(&fl.source) {
|
|
794
800
|
Some(ti) => {
|
|
795
|
-
if let Some(f) = self.reg.methods.get(&(ti.name,
|
|
801
|
+
if let Some(f) = self.reg.methods.get(&(ti.name, want.to_string())) {
|
|
796
802
|
return Some((f.clone(), Some(fl.source.clone()), vec![]));
|
|
797
803
|
}
|
|
798
804
|
}
|
|
@@ -1799,7 +1805,7 @@ fn checkCopy(f: &Fn) -> Fn {
|
|
|
1799
1805
|
for p in &mut c.params {
|
|
1800
1806
|
if let ParamType::Seq(t) = &p.ty {
|
|
1801
1807
|
p.ty = ParamType::Type(Type {
|
|
1802
|
-
name: "
|
|
1808
|
+
name: "Slice".to_string(),
|
|
1803
1809
|
generics: vec![t.clone()],
|
|
1804
1810
|
});
|
|
1805
1811
|
}
|
plum-core/src/loader.rs
CHANGED
|
@@ -101,7 +101,7 @@ pub fn loadAndMergeWithOverrides(
|
|
|
101
101
|
Ok(merged)
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
/// A std `primitive
|
|
104
|
+
/// A std `primitive Slice[T] =` (any non-built-in name that is also a declared
|
|
105
105
|
/// enum/record) is an EXTENSION: its `fun`s are methods of that type (a
|
|
106
106
|
/// duplicate method name is already a collision error) and it is not a
|
|
107
107
|
/// stateless namespace, so it leaves the namespace list. Outside std this
|
plum-core/tests/formatter_test.rs
CHANGED
|
@@ -199,10 +199,10 @@ fn formatsErrorSyntax() {
|
|
|
199
199
|
|
|
200
200
|
#[test]
|
|
201
201
|
fn formatsTypeSugar() {
|
|
202
|
-
let input = "fun f(a: Person?,b: Int?) ->
|
|
202
|
+
let input = "fun f(a: Person?,b: Int?) -> Slice[Int?] =\n a\n";
|
|
203
203
|
let first = formatSource(input).expect("formatSource should succeed");
|
|
204
204
|
assert!(first.contains("a: Person?, b: Int?"), "{first}");
|
|
205
|
-
assert!(first.contains("->
|
|
205
|
+
assert!(first.contains("-> Slice[Int?] ="), "{first}");
|
|
206
206
|
assert_eq!(
|
|
207
207
|
first,
|
|
208
208
|
formatSource(&first).expect("second pass"),
|
|
@@ -251,8 +251,8 @@ fn formatsZigStyleErrors() {
|
|
|
251
251
|
assert!(out.contains("y := h(s) catch e\n return 0"), "{out}");
|
|
252
252
|
assert!(out.contains(" return error.Bad"), "{out}");
|
|
253
253
|
assert_eq!(formatSource(&out).expect("second"), out, "idempotent");
|
|
254
|
-
let g = formatSource("fun f() -> !
|
|
254
|
+
let g = formatSource("fun f() -> !Slice[Int] =\n return error.A\n").expect("format");
|
|
255
|
-
assert!(g.contains("-> !
|
|
255
|
+
assert!(g.contains("-> !Slice[Int] ="), "{g}");
|
|
256
256
|
let h = formatSource(
|
|
257
257
|
"error ParseError=A|B\n\nfun f() -> (ParseError||error{ C,D })!Int =\n return error.A\n",
|
|
258
258
|
)
|
|
@@ -296,23 +296,23 @@ fn formatOneLineEnum() {
|
|
|
296
296
|
);
|
|
297
297
|
let second = formatSource(&first).expect("second");
|
|
298
298
|
assert_eq!(first, second);
|
|
299
|
-
let g = formatSource("enum Js = JsNull |
|
|
299
|
+
let g = formatSource("enum Js = JsNull | Slice[Js] | Map[Str, Js]\n").expect("format");
|
|
300
300
|
assert!(
|
|
301
|
-
g.contains("enum Js = JsNull |
|
|
301
|
+
g.contains("enum Js = JsNull | Slice[Js] | Map[Str, Js]\n"),
|
|
302
302
|
"got: {g:?}"
|
|
303
303
|
);
|
|
304
304
|
}
|
|
305
305
|
|
|
306
306
|
#[test]
|
|
307
307
|
fn formatAnnotatedDeclaration() {
|
|
308
|
-
let input = "fun main() =\n s:Shape:=Circle(r: 2.0)\n xs:
|
|
308
|
+
let input = "fun main() =\n s:Shape:=Circle(r: 2.0)\n xs: Slice[Shape] := Slice[Shape]()\n f:fn(Int)->Int:=twice\n o: Int? := null\n";
|
|
309
309
|
let first = formatSource(input).expect("format");
|
|
310
310
|
assert!(
|
|
311
311
|
first.contains(" s: Shape := Circle(r: 2.0)\n"),
|
|
312
312
|
"got: {first:?}"
|
|
313
313
|
);
|
|
314
314
|
assert!(
|
|
315
|
-
first.contains(" xs:
|
|
315
|
+
first.contains(" xs: Slice[Shape] := Slice[Shape]()\n"),
|
|
316
316
|
"got: {first:?}"
|
|
317
317
|
);
|
|
318
318
|
assert!(
|
plum-core/tests/inline_variants_test.rs
CHANGED
|
@@ -110,7 +110,7 @@ fn type_position_errors() {
|
|
|
110
110
|
"fun fa(cc: Cons[Int]) -> Int = 1\n",
|
|
111
111
|
"fun fa() -> Cons[Int] = todo\n",
|
|
112
112
|
"record Box(cc: Cons[Int])\n",
|
|
113
|
-
"fun fa(cc:
|
|
113
|
+
"fun fa(cc: Slice[Cons[Int]]) -> Int = 1\n",
|
|
114
114
|
"fun fa(gg: fn(Cons[Int]) -> Int) -> Int = 1\n",
|
|
115
115
|
"fun fa() -> Int =\n cc: Cons[Int] := todo\n 1\n",
|
|
116
116
|
] {
|
plum-core/tests/parser_test.rs
CHANGED
|
@@ -347,13 +347,13 @@ fn nullable_sugar_parses_to_pinned_null_error_union() {
|
|
|
347
347
|
|
|
348
348
|
#[test]
|
|
349
349
|
fn nested_nullable_generic_argument() {
|
|
350
|
-
let source = parse("fun f() ->
|
|
350
|
+
let source = parse("fun f() -> Slice[Int?] =\n 1\n");
|
|
351
351
|
let f = onlyFn(&source);
|
|
352
352
|
let inner = ty(
|
|
353
353
|
"!",
|
|
354
354
|
vec![ty("Int", vec![]), ty("error", vec![ty("null", vec![])])],
|
|
355
355
|
);
|
|
356
|
-
assert_eq!(f.returns, Some(ty("
|
|
356
|
+
assert_eq!(f.returns, Some(ty("Slice", vec![inner])));
|
|
357
357
|
}
|
|
358
358
|
|
|
359
359
|
#[test]
|
|
@@ -417,9 +417,9 @@ fn error_union_type_inferred_and_pinned() {
|
|
|
417
417
|
.map(|g| g.name.as_str())
|
|
418
418
|
.collect();
|
|
419
419
|
assert_eq!(names, ["BadNumStr", "Other"]);
|
|
420
|
-
let t = retType("fun f() -> !
|
|
420
|
+
let t = retType("fun f() -> !Slice[Int] =\n 1\n");
|
|
421
421
|
assert_eq!(t.generics.len(), 1);
|
|
422
|
-
assert_eq!(t.generics[0].name, "
|
|
422
|
+
assert_eq!(t.generics[0].name, "Slice");
|
|
423
423
|
assert_eq!(t.generics[0].generics[0].name, "Int");
|
|
424
424
|
}
|
|
425
425
|
|
|
@@ -955,7 +955,7 @@ fn unionEnumMultiLineGenericMember() {
|
|
|
955
955
|
|
|
956
956
|
#[test]
|
|
957
957
|
fn one_line_generic_member() {
|
|
958
|
-
let source = parseRaw("enum Js = JsNull |
|
|
958
|
+
let source = parseRaw("enum Js = JsNull | Slice[Js] | Map[Str, Js]");
|
|
959
959
|
let e = onlyEnum(&source);
|
|
960
960
|
assert_eq!(memberOf(&e.variants[0]).generics.len(), 0);
|
|
961
961
|
assert_eq!(memberOf(&e.variants[1]).generics.len(), 1);
|
|
@@ -996,9 +996,9 @@ fn annotatedDeclarationCarriesItsType() {
|
|
|
996
996
|
|
|
997
997
|
#[test]
|
|
998
998
|
fn annotatedDeclarationTypes() {
|
|
999
|
-
let a = firstAssign("fun f() =\n xs:
|
|
999
|
+
let a = firstAssign("fun f() =\n xs: Slice[Shape] := Slice[Shape]()\n");
|
|
1000
1000
|
let t = a.ty.unwrap();
|
|
1001
|
-
assert_eq!((t.name.as_str(), t.generics.len()), ("
|
|
1001
|
+
assert_eq!((t.name.as_str(), t.generics.len()), ("Slice", 1));
|
|
1002
1002
|
let a = firstAssign("fun f() =\n g: fn(Int) -> Int := |n| n + 1\n");
|
|
1003
1003
|
assert!(a.ty.unwrap().asFn().is_some());
|
|
1004
1004
|
let a = firstAssign("fun f() =\n o: Int? := null\n");
|
plum-core/tests/record_init_test.rs
CHANGED
|
@@ -124,11 +124,11 @@ fn init_with_params_rejected() {
|
|
|
124
124
|
fn annotate_error_and_annotation() {
|
|
125
125
|
let (_, e) = lower("record Rec(a: Int) =\n fun init(self) =\n self.t = null\n");
|
|
126
126
|
assert!(e.iter().any(|m| m.starts_with("annotate the type: self.t: T = ...")), "{e:?}");
|
|
127
|
-
let (s, e) = lower("record Rec(a: Int) =\n fun init(self) =\n self.t: Int? = null\n self.l =
|
|
127
|
+
let (s, e) = lower("record 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");
|
|
128
128
|
assert!(e.is_empty(), "{e:?}");
|
|
129
129
|
let v = &enumOf(&s, "Rec").variants[0];
|
|
130
130
|
assert_eq!(v.field_names, vec!["a", "t", "l", "s", "b"]);
|
|
131
|
-
assert_eq!(v.fields[2].name, "
|
|
131
|
+
assert_eq!(v.fields[2].name, "Slice");
|
|
132
132
|
assert_eq!(v.fields[3].name, "Str");
|
|
133
133
|
assert_eq!(v.fields[4].name, "Bool");
|
|
134
134
|
}
|
plum-core/tests/traits_test.rs
CHANGED
|
@@ -140,10 +140,10 @@ fn struct_embed() {
|
|
|
140
140
|
|
|
141
141
|
#[test]
|
|
142
142
|
fn self_recursive_field() {
|
|
143
|
-
let mut s = parseRaw("trait Act(children:
|
|
143
|
+
let mut s = parseRaw("trait Act(children: Slice[Act])\nrecord Nod(Act)\n");
|
|
144
144
|
assert!(lowerTraits(&mut s).is_empty());
|
|
145
145
|
let t = &enumOf(&s, "Nod").variants[0].fields[0];
|
|
146
|
-
assert_eq!(t.name, "
|
|
146
|
+
assert_eq!(t.name, "Slice");
|
|
147
147
|
assert_eq!(t.generics[0].name, "Act");
|
|
148
148
|
}
|
|
149
149
|
|
plum-core/tests/trmc_test.rs
CHANGED
|
@@ -303,9 +303,9 @@ fn structNewCount(wasm: &[u8], name: &str) -> usize {
|
|
|
303
303
|
fn map_single_pass_one_alloc_per_cell() {
|
|
304
304
|
let p = prog("fun main() -> Int = count(map(fromTo(0, 3), |x| x + 1))\n");
|
|
305
305
|
let bytes = compileSource(&load("single", &p)).unwrap();
|
|
306
|
-
// One
|
|
306
|
+
// One struct.new per cell (the member is the enum subtype), in one loop.
|
|
307
|
-
assert_eq!(structNewCount(&bytes, "map"),
|
|
307
|
+
assert_eq!(structNewCount(&bytes, "map"), 1);
|
|
308
|
-
assert_eq!(structNewCount(&bytes, "fromTo"),
|
|
308
|
+
assert_eq!(structNewCount(&bytes, "fromTo"), 1);
|
|
309
309
|
}
|
|
310
310
|
|
|
311
311
|
#[test]
|
plum-core/tests/union_lower_test.rs
CHANGED
|
@@ -468,7 +468,7 @@ fn narrow_does_not_leak_into_later_items() {
|
|
|
468
468
|
#[test]
|
|
469
469
|
fn narrow_for_clause_bind_shadows() {
|
|
470
470
|
let mut s = parse(
|
|
471
|
-
"fun f(s: Shape, xs:
|
|
471
|
+
"fun f(s: Shape, xs: Slice[Int]) =\n match s\n Circle =>\n for s := range xs\n s.r\n",
|
|
472
472
|
);
|
|
473
473
|
let plan = narrowPlan(&s, "f", "s", "Shape__Circle", "s__n1");
|
|
474
474
|
applyPlan(&mut s, &plan);
|
plum-core/tests/unions_test.rs
CHANGED
|
@@ -78,10 +78,10 @@ fn recordsOtherKinds() {
|
|
|
78
78
|
|
|
79
79
|
#[test]
|
|
80
80
|
fn dup_base_name() {
|
|
81
|
-
let mut s = parse("struct
|
|
81
|
+
let mut s = parse("struct Slice(n: Int)\nenum Js = Slice[Int] | Slice[Str]");
|
|
82
82
|
assert_eq!(
|
|
83
83
|
resolveMembers(&mut s),
|
|
84
|
-
vec!["enum Js has two
|
|
84
|
+
vec!["enum Js has two Slice members; a pattern 'Slice' would be ambiguous"]
|
|
85
85
|
);
|
|
86
86
|
}
|
|
87
87
|
|
plum-examples/annotated_locals.plum
CHANGED
|
@@ -6,7 +6,7 @@ module annotated_locals
|
|
|
6
6
|
import std/Str
|
|
7
7
|
import std/Int
|
|
8
8
|
import std/Float
|
|
9
|
-
import std/
|
|
9
|
+
import std/Slice
|
|
10
10
|
|
|
11
11
|
record Circle(r: Float)
|
|
12
12
|
|
|
@@ -52,7 +52,7 @@ test "optional_local_null"
|
|
|
52
52
|
assert {o ?: 9} == 5
|
|
53
53
|
|
|
54
54
|
test "list_local"
|
|
55
|
-
xs:
|
|
55
|
+
xs: Slice[Shape] := Slice[Shape]()
|
|
56
56
|
xs.push(Circle(r: 1.0))
|
|
57
57
|
xs.push(Rect(w: 1.0, h: 1.0))
|
|
58
58
|
assert xs.length() == 2
|
plum-examples/cons_list.plum
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# The std cons List: constant stack at 1,000,000 elements.
|
|
2
|
+
module cons_list
|
|
3
|
+
import std/Int
|
|
4
|
+
import std/Str
|
|
5
|
+
import std/Slice
|
|
6
|
+
import std/Array
|
|
7
|
+
import std/List
|
|
8
|
+
|
|
9
|
+
fun range(a: Int, b: Int) -> List[Int] =
|
|
10
|
+
if a >= b then Nil else Cons(head: a, tail: range(a + 1, b))
|
|
11
|
+
|
|
12
|
+
test "init_sum"
|
|
13
|
+
assert List(1, 2, 3, 4).fold(0, |a, x| a + x) == 10
|
|
14
|
+
assert List(1, 2, 3).length() == 3
|
|
15
|
+
assert { List(1, 2, 3).get(1) ?: -1 } == 2
|
|
16
|
+
assert List(1, 2, 3).get(5) == null
|
|
17
|
+
nil: List[Int] := Nil
|
|
18
|
+
assert nil.isEmpty()
|
|
19
|
+
assert !List(1).isEmpty()
|
|
20
|
+
|
|
21
|
+
test "map_filter_fold"
|
|
22
|
+
xs := List(1, 2, 3, 4, 5, 6)
|
|
23
|
+
ys := xs.map(|x| x * 2).filter(|x| x % 3 == 0)
|
|
24
|
+
assert ys.length() == 2
|
|
25
|
+
assert ys.fold(0, |a, x| a + x) == 18
|
|
26
|
+
|
|
27
|
+
test "reverse_append"
|
|
28
|
+
xs := List(1, 2, 3).reverse()
|
|
29
|
+
assert { xs.get(0) ?: -1 } == 3
|
|
30
|
+
assert { xs.get(2) ?: -1 } == 1
|
|
31
|
+
ys := List(1, 2).append(List(3, 4))
|
|
32
|
+
assert ys.equals(List(1, 2, 3, 4))
|
|
33
|
+
|
|
34
|
+
test "for_range"
|
|
35
|
+
total := 0
|
|
36
|
+
for x := range List(1, 2, 3, 4)
|
|
37
|
+
total = total + x
|
|
38
|
+
assert total == 10
|
|
39
|
+
big := 0
|
|
40
|
+
for x := range range(0, 1000000).items()
|
|
41
|
+
big = big + x
|
|
42
|
+
assert big == 499999500000
|
|
43
|
+
|
|
44
|
+
test "to_slice_round_trip"
|
|
45
|
+
s := List(1, 2, 3).map(|x| x * 2).toSlice()
|
|
46
|
+
assert s.equals(Slice(2, 4, 6))
|
|
47
|
+
assert List.fromSlice(s).equals(List(2, 4, 6))
|
|
48
|
+
|
|
49
|
+
test "to_array"
|
|
50
|
+
a := List(7, 8, 9).toArray()
|
|
51
|
+
assert a.length() == 3
|
|
52
|
+
assert a.get(2) == 9
|
|
53
|
+
|
|
54
|
+
test "map_million"
|
|
55
|
+
ys := range(0, 1000000).map(|x| x + 1)
|
|
56
|
+
assert ys.length() == 1000000
|
|
57
|
+
assert ys.fold(0, |a, x| a + x) == 500000500000
|
|
58
|
+
|
|
59
|
+
test "fold_million"
|
|
60
|
+
xs := range(0, 1000000)
|
|
61
|
+
assert xs.fold(0, |a, x| a + x) == 499999500000
|
|
62
|
+
assert xs.filter(|x| x % 2 == 0).length() == 500000
|
|
63
|
+
assert { xs.reverse().get(0) ?: -1 } == 999999
|
|
64
|
+
|
|
65
|
+
test "equals_and_toStr"
|
|
66
|
+
assert List(1, 2, 3).equals(List(1, 2, 3))
|
|
67
|
+
assert !List(1, 2, 3).equals(List(1, 2))
|
|
68
|
+
assert !List(1, 2).equals(List(1, 2, 3))
|
|
69
|
+
assert !List(1, 2, 3).equals(List(1, 2, 4))
|
|
70
|
+
assert List(1, 2, 3).toStr() == "[1,2,3]"
|
plum-examples/constants.plum
CHANGED
|
@@ -2,7 +2,7 @@ module constants
|
|
|
2
2
|
import std/Int
|
|
3
3
|
import std/Float
|
|
4
4
|
import std/Str
|
|
5
|
-
import std/
|
|
5
|
+
import std/Slice
|
|
6
6
|
|
|
7
7
|
# Constants and static functions live inside a type's body or a `primitive`
|
|
8
8
|
# namespace — there are no top-level constants. `Name = expr` declares an
|
|
@@ -42,7 +42,7 @@ fun scaled(n: Float) -> Float = n * Grid.CellScale
|
|
|
42
42
|
|
|
43
43
|
primitive Shelf =
|
|
44
44
|
Sides = 4
|
|
45
|
-
Names =
|
|
45
|
+
Names = Slice("a", "b", "c")
|
|
46
46
|
|
|
47
47
|
fun sidesTwice() -> Int =
|
|
48
48
|
twice(Shelf.Sides)
|
plum-examples/error_propagation.plum
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import std/Int
|
|
2
2
|
import std/Float
|
|
3
3
|
import std/Str
|
|
4
|
-
import std/
|
|
4
|
+
import std/Slice
|
|
5
5
|
import std/Map
|
|
6
6
|
|
|
7
7
|
# `!T` is "a `T` or an error". An error is a value written `error.Name` (for
|
|
@@ -46,7 +46,7 @@ fun sumTwo(a: Str, b: Str) -> !Int =
|
|
|
46
46
|
# Two calls with different error sets merge into [BadNumStr, OutOfBounds].
|
|
47
47
|
fun numAt(s: Str, i: Int) -> error{BadNumStr, OutOfBounds}!Int =
|
|
48
48
|
n := try Int.parse(s)
|
|
49
|
-
xs :=
|
|
49
|
+
xs := Slice(10, 20, 30)
|
|
50
50
|
x := try xs.get(i)
|
|
51
51
|
n + x
|
|
52
52
|
|
|
@@ -55,7 +55,7 @@ fun parseOrInvalid(s: Str) -> error{InvalidInput}!Int =
|
|
|
55
55
|
n := Int.parse(s) catch return error.InvalidInput
|
|
56
56
|
n
|
|
57
57
|
|
|
58
|
-
fun doubledFirst(list:
|
|
58
|
+
fun doubledFirst(list: Slice[Int]) -> error{ListWasEmpty}!Int =
|
|
59
59
|
a := try list.first()
|
|
60
60
|
a * 2
|
|
61
61
|
|
|
@@ -64,7 +64,7 @@ fun sumOrZero(a: Str, b: Str) -> Int =
|
|
|
64
64
|
y := {parsePositive(b) catch 0}
|
|
65
65
|
x + y
|
|
66
66
|
|
|
67
|
-
fun firstOrDefault(list:
|
|
67
|
+
fun firstOrDefault(list: Slice[Int], default: Int) -> Int =
|
|
68
68
|
{list.first() catch default}
|
|
69
69
|
|
|
70
70
|
fun mustBePositive(n: Int) -> Int =
|
|
@@ -148,15 +148,15 @@ record Age(n: Int) =
|
|
|
148
148
|
n := try parsePositive(s)
|
|
149
149
|
return Age(n)
|
|
150
150
|
|
|
151
|
-
record Ages[T](items:
|
|
151
|
+
record Ages[T](items: Slice[T]) =
|
|
152
|
-
fun parseAll(xs:
|
|
152
|
+
fun parseAll(xs: Slice[Str]) -> !Ages[T] =
|
|
153
|
-
out :=
|
|
153
|
+
out := Slice[T]()
|
|
154
154
|
for x := range xs
|
|
155
155
|
v := try T.parse(x)
|
|
156
156
|
out.push(v)
|
|
157
157
|
return Ages(out)
|
|
158
158
|
|
|
159
|
-
fun parseAges(xs:
|
|
159
|
+
fun parseAges(xs: Slice[Str]) -> !Ages[Age] = Ages.parseAll(xs)
|
|
160
160
|
|
|
161
161
|
test "try operator unwraps the value and propagates it through two calls"
|
|
162
162
|
assert sumTwo("2", "3")! == 5
|
|
@@ -183,25 +183,25 @@ test "catch return error.X maps the error to another name"
|
|
|
183
183
|
assert name == 0
|
|
184
184
|
|
|
185
185
|
test "try binds a declaration from a type parameter's static method in a loop"
|
|
186
|
-
ages := parseAges(
|
|
186
|
+
ages := parseAges(Slice("3", "4"))!
|
|
187
187
|
assert ages.items.length() == 2
|
|
188
188
|
assert ages.items.get(1)!.n == 4
|
|
189
|
-
failed := parseAges(
|
|
189
|
+
failed := parseAges(Slice("3", "-1")) catch e
|
|
190
190
|
assert e.name() == "NegativeNum"
|
|
191
|
-
Ages(
|
|
191
|
+
Ages(Slice[Age]())
|
|
192
192
|
assert failed.items.length() == 0
|
|
193
193
|
|
|
194
194
|
test "try operator on a list that is empty yields ListWasEmpty"
|
|
195
|
-
assert doubledFirst(
|
|
195
|
+
assert doubledFirst(Slice(5))! == 10
|
|
196
|
-
assert {doubledFirst(
|
|
196
|
+
assert {doubledFirst(Slice[Int]()) catch -1} == -1
|
|
197
197
|
|
|
198
198
|
test "catch default unwraps the value and falls back to 0 on an error"
|
|
199
199
|
assert sumOrZero("2", "3") == 5
|
|
200
200
|
assert sumOrZero("2", "-3") == 2
|
|
201
201
|
|
|
202
202
|
test "catch default falls back to the given default on an empty list"
|
|
203
|
-
assert firstOrDefault(
|
|
203
|
+
assert firstOrDefault(Slice(5), 99) == 5
|
|
204
|
-
assert firstOrDefault(
|
|
204
|
+
assert firstOrDefault(Slice[Int](), 99) == 99
|
|
205
205
|
|
|
206
206
|
test "crash is not reached on the happy path"
|
|
207
207
|
assert mustBePositive(3) == 3
|
|
@@ -226,10 +226,10 @@ test "match falls back to a wildcard arm"
|
|
|
226
226
|
assert ageOrDefault("999") == 150
|
|
227
227
|
assert ageOrDefault("x") == 0
|
|
228
228
|
|
|
229
|
-
# An error union can be the success type of another (a `
|
|
229
|
+
# An error union can be the success type of another (a `Slice[!Int]` holds
|
|
230
230
|
# per-element results): `xs.get(i)` is `!!Int`, unwrapped one level at a time.
|
|
231
231
|
test "a list of error unions nests one level per access"
|
|
232
|
-
results :=
|
|
232
|
+
results := Slice(parsePositive("1"), parsePositive("-1"), parsePositive("x"))
|
|
233
233
|
first := results.get(0)!
|
|
234
234
|
assert first! == 1
|
|
235
235
|
second := results.get(1)!
|
plum-examples/forms.plum
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import std/Str
|
|
4
4
|
import std/Int
|
|
5
5
|
import std/Float
|
|
6
|
-
import std/
|
|
6
|
+
import std/Slice
|
|
7
7
|
import std/Testing
|
|
8
8
|
import std/Seq
|
|
9
9
|
|
|
@@ -14,11 +14,11 @@ import std/Seq
|
|
|
14
14
|
|
|
15
15
|
record Fighter(name: Str, hp: Int, dead: Bool)
|
|
16
16
|
|
|
17
|
-
fun sampleFighters() ->
|
|
17
|
+
fun sampleFighters() -> Slice[Fighter] =
|
|
18
|
-
|
|
18
|
+
Slice(Fighter(name: "a", hp: 10, dead: false), Fighter(name: "b", hp: 0, dead: true), Fighter(name: "c", hp: 7, dead: false))
|
|
19
19
|
|
|
20
20
|
# a user form: the sum of the body's value over a list
|
|
21
|
-
fun* sumBy(xs:
|
|
21
|
+
fun* sumBy(xs: Slice[T]) *T -> Int =
|
|
22
22
|
total := 0
|
|
23
23
|
for x := range xs
|
|
24
24
|
v := yield x
|
|
@@ -33,24 +33,24 @@ fun* neighbours(x: Int, y: Int) *(Int, Int) =
|
|
|
33
33
|
yield x + dx - 1, y + dy - 1
|
|
34
34
|
|
|
35
35
|
# a user form built from another form: `yield` sits inside `each`'s body
|
|
36
|
-
fun* sumEvens(xs:
|
|
36
|
+
fun* sumEvens(xs: Slice[Int]) *Int -> Int =
|
|
37
37
|
total := 0
|
|
38
38
|
for x := range xs.each(), if x % 2 == 0
|
|
39
39
|
v := yield x
|
|
40
40
|
total = total + v
|
|
41
41
|
total
|
|
42
42
|
|
|
43
|
-
fun firstBig(xs:
|
|
43
|
+
fun firstBig(xs: Slice[Int]) -> Int =
|
|
44
44
|
for x := range xs.each()
|
|
45
45
|
if x > 10
|
|
46
46
|
return x
|
|
47
47
|
return -1
|
|
48
48
|
|
|
49
|
-
fun returnedForm(xs:
|
|
49
|
+
fun returnedForm(xs: Slice[Int]) -> Slice[Int] =
|
|
50
50
|
return for x := range xs.map()
|
|
51
51
|
x + 1
|
|
52
52
|
|
|
53
|
-
fun tailForm(xs:
|
|
53
|
+
fun tailForm(xs: Slice[Int]) -> Int =
|
|
54
54
|
for x := range sumBy(xs)
|
|
55
55
|
x * 2
|
|
56
56
|
|
|
@@ -78,7 +78,7 @@ test "user_form_sumBy"
|
|
|
78
78
|
hp := for u := range sumBy(fighters)
|
|
79
79
|
u.hp
|
|
80
80
|
assert hp == 17
|
|
81
|
-
odd := for x := range sumBy(
|
|
81
|
+
odd := for x := range sumBy(Slice(1, 2, 3, 4, 5)), if x % 2 == 1
|
|
82
82
|
x
|
|
83
83
|
assert odd == 9
|
|
84
84
|
|
|
@@ -92,7 +92,7 @@ test "user_form_neighbours_grid: two binders, statement body"
|
|
|
92
92
|
assert sum == 15
|
|
93
93
|
|
|
94
94
|
test "break_continue_return_semantics"
|
|
95
|
-
xs :=
|
|
95
|
+
xs := Slice(1, 2, 3, 4, 5)
|
|
96
96
|
total := 0
|
|
97
97
|
for x := range xs.each()
|
|
98
98
|
if x == 2
|
|
@@ -101,7 +101,7 @@ test "break_continue_return_semantics"
|
|
|
101
101
|
break
|
|
102
102
|
total = total + x
|
|
103
103
|
assert total == 8
|
|
104
|
-
assert firstBig(
|
|
104
|
+
assert firstBig(Slice(1, 20, 30)) == 20
|
|
105
105
|
assert firstBig(xs) == -1
|
|
106
106
|
prefix := for x := range xs.filter()
|
|
107
107
|
if x == 4
|
|
@@ -110,14 +110,14 @@ test "break_continue_return_semantics"
|
|
|
110
110
|
assert prefix.length() == 2
|
|
111
111
|
|
|
112
112
|
test "composed form: a form that uses another form"
|
|
113
|
-
xs :=
|
|
113
|
+
xs := Slice(1, 2, 3, 4, 5, 6)
|
|
114
114
|
total := for x := range sumEvens(xs)
|
|
115
115
|
x * 10
|
|
116
116
|
assert total == 120
|
|
117
117
|
|
|
118
118
|
test "forms nest: a form inside a form body"
|
|
119
|
-
xs :=
|
|
119
|
+
xs := Slice(1, 2)
|
|
120
|
-
ys :=
|
|
120
|
+
ys := Slice(10, 20)
|
|
121
121
|
total := for a := range sumBy(xs)
|
|
122
122
|
inner := for b := range sumBy(ys)
|
|
123
123
|
a * b
|
|
@@ -125,12 +125,12 @@ test "forms nest: a form inside a form body"
|
|
|
125
125
|
assert total == 90
|
|
126
126
|
|
|
127
127
|
test "return for-form: the form's value is returned"
|
|
128
|
-
ys := returnedForm(
|
|
128
|
+
ys := returnedForm(Slice(1, 2, 3))
|
|
129
129
|
assert ys.length() == 3
|
|
130
130
|
assert ys.at(2) == 4
|
|
131
131
|
|
|
132
132
|
test "tail for-form: the last statement of a fn is its value"
|
|
133
|
-
assert tailForm(
|
|
133
|
+
assert tailForm(Slice(1, 2, 3)) == 12
|
|
134
134
|
|
|
135
135
|
test "fun_star_method_uses_self"
|
|
136
136
|
c := Countdown(from: 4)
|
plum-examples/functional.plum
CHANGED
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
#
|
|
3
3
|
# Everything here is a value: nothing is mutated after it is built. Data is made
|
|
4
4
|
# of recursive sum types (`enum`) whose variants are declared inline with named
|
|
5
|
-
# fields, `
|
|
5
|
+
# fields, `Link(head: T, tail: Chain[T])`. A variant is a constructor and a
|
|
6
|
-
# pattern only; `
|
|
6
|
+
# pattern only; `Chain[T]` is the only type, and fields are read by matching.
|
|
7
7
|
# Every operation is a method on its enum, a recursive function whose body is
|
|
8
8
|
# `= match self` with the arms indented below, returning a new value. Unchanged
|
|
9
9
|
# parts are shared.
|
|
10
10
|
#
|
|
11
|
-
# 1.
|
|
11
|
+
# 1. Chain[T]: a cons list (`Empty` or `Link(head, tail)`), combinators as methods on the enum
|
|
12
12
|
# 2. Functions as values: closures, composition, currying, pipelines
|
|
13
13
|
# 3. Tree: a persistent binary search tree, operations as methods on the enum
|
|
14
14
|
# 4. Pattern matching: nested patterns, guards, pattern `if`, optionals
|
|
15
15
|
# 5. A pure scene graph: a recursive `Node` tree whose groups hold a
|
|
16
|
-
# `
|
|
16
|
+
# `Chain[Node]` of children; acting, drawing, hit testing and event routing
|
|
17
17
|
# are all methods from an old tree to a new one
|
|
18
18
|
#
|
|
19
19
|
# 6. Loops, functionally: the same job as methods, a `for` and a `while`
|
|
@@ -24,29 +24,30 @@ module functional
|
|
|
24
24
|
import std/Str
|
|
25
25
|
import std/Int
|
|
26
26
|
import std/Float
|
|
27
|
-
import std/
|
|
27
|
+
import std/Slice
|
|
28
28
|
|
|
29
29
|
# ---------------------------------------------------------------------------
|
|
30
|
-
# 1.
|
|
30
|
+
# 1. Chain[T]: a cons list (std/List is the same shape, with variants Nil and Cons;
|
|
31
|
+
# renamed here to Empty and Link only so this file can define its own)
|
|
31
32
|
# ---------------------------------------------------------------------------
|
|
32
33
|
|
|
33
|
-
# A list is either empty (`
|
|
34
|
+
# A list is either empty (`Empty`, a tag) or a head value in front of another list.
|
|
34
35
|
# Its operations are methods on the enum.
|
|
35
|
-
enum
|
|
36
|
+
enum Chain[T] =
|
|
36
|
-
|
|
|
37
|
+
| Empty
|
|
37
|
-
|
|
|
38
|
+
| Link(head: T, tail: Chain[T])
|
|
38
39
|
|
|
39
40
|
fun length(self) -> Int = match self
|
|
40
|
-
|
|
41
|
+
Empty => 0
|
|
41
|
-
|
|
42
|
+
Link(_, t) => 1 + t.length()
|
|
42
43
|
|
|
43
44
|
fun isEmpty(self) -> Bool = match self
|
|
44
|
-
|
|
45
|
+
Empty => true
|
|
45
|
-
|
|
46
|
+
Link(_, _) => false
|
|
46
47
|
|
|
47
48
|
# Runs `f` on every element, in order, for its effect.
|
|
48
49
|
fun each(self, f: fn(T)) -> Unit =
|
|
49
|
-
if
|
|
50
|
+
if Link(h, t) := self
|
|
50
51
|
f(h)
|
|
51
52
|
t.each(f)
|
|
52
53
|
|
|
@@ -54,89 +55,89 @@ enum Link[T] =
|
|
|
54
55
|
fun* items(self) *T =
|
|
55
56
|
cur := self
|
|
56
57
|
while true
|
|
57
|
-
if
|
|
58
|
+
if Link(h, t) := cur
|
|
58
59
|
cur = t
|
|
59
60
|
yield h
|
|
60
61
|
else
|
|
61
62
|
break
|
|
62
63
|
|
|
63
64
|
# Structural recursion: rebuild the list with `f` applied to every element.
|
|
64
|
-
fun map(self, f: fn(T) -> B) ->
|
|
65
|
+
fun map(self, f: fn(T) -> B) -> Chain[B] = match self
|
|
65
|
-
|
|
66
|
+
Empty => Empty
|
|
66
|
-
|
|
67
|
+
Link(h, t) => Link(head: f(h), tail: t.map(f))
|
|
67
68
|
|
|
68
|
-
fun filter(self, keep: fn(T) -> Bool) ->
|
|
69
|
+
fun filter(self, keep: fn(T) -> Bool) -> Chain[T] = match self
|
|
69
|
-
|
|
70
|
+
Empty => Empty
|
|
70
|
-
|
|
71
|
+
Link(h, t) =>
|
|
71
72
|
rest := t.filter(keep)
|
|
72
73
|
if keep(h) then cons(h, rest) else rest
|
|
73
74
|
|
|
74
75
|
# Left fold: `f(f(f(acc, x0), x1), x2)`. Tail recursive in shape.
|
|
75
76
|
fun foldl(self, acc: A, f: fn(A, T) -> A) -> A = match self
|
|
76
|
-
|
|
77
|
+
Empty => acc
|
|
77
|
-
|
|
78
|
+
Link(h, t) => t.foldl(f(acc, h), f)
|
|
78
79
|
|
|
79
80
|
# Right fold: `f(x0, f(x1, f(x2, acc)))`. Keeps the list's order when rebuilding.
|
|
80
81
|
fun foldr(self, acc: A, f: fn(T, A) -> A) -> A = match self
|
|
81
|
-
|
|
82
|
+
Empty => acc
|
|
82
|
-
|
|
83
|
+
Link(h, t) => f(h, t.foldr(acc, f))
|
|
83
84
|
|
|
84
|
-
fun reverse(self) ->
|
|
85
|
+
fun reverse(self) -> Chain[T] =
|
|
85
|
-
self.foldl(
|
|
86
|
+
self.foldl(Empty, |acc, x| cons(x, acc))
|
|
86
87
|
|
|
87
88
|
# `self` followed by `ys`. Only `self` is rebuilt; `ys` is shared.
|
|
88
|
-
fun append(self, ys:
|
|
89
|
+
fun append(self, ys: Chain[T]) -> Chain[T] =
|
|
89
90
|
self.foldr(ys, |x, acc| cons(x, acc))
|
|
90
91
|
|
|
91
92
|
fun any(self, p: fn(T) -> Bool) -> Bool = match self
|
|
92
|
-
|
|
93
|
+
Empty => false
|
|
93
|
-
|
|
94
|
+
Link(h, t) => p(h) || t.any(p)
|
|
94
95
|
|
|
95
96
|
fun all(self, p: fn(T) -> Bool) -> Bool = match self
|
|
96
|
-
|
|
97
|
+
Empty => true
|
|
97
|
-
|
|
98
|
+
Link(h, t) => p(h) && t.all(p)
|
|
98
99
|
|
|
99
|
-
fun take(self, n: Int) ->
|
|
100
|
+
fun take(self, n: Int) -> Chain[T] = match self
|
|
100
|
-
|
|
101
|
+
Empty => Empty
|
|
101
|
-
|
|
102
|
+
Link(h, t) => if n <= 0 then Empty else cons(h, t.take(n - 1))
|
|
102
103
|
|
|
103
|
-
fun drop(self, n: Int) ->
|
|
104
|
+
fun drop(self, n: Int) -> Chain[T] = match self
|
|
104
|
-
|
|
105
|
+
Empty => Empty
|
|
105
|
-
|
|
106
|
+
Link(_, t) => if n <= 0 then self else t.drop(n - 1)
|
|
106
107
|
|
|
107
108
|
# The first element, or null for an empty list.
|
|
108
109
|
fun first(self) -> T? = match self
|
|
109
|
-
|
|
110
|
+
Empty => null
|
|
110
|
-
|
|
111
|
+
Link(h, _) => h
|
|
111
112
|
|
|
112
113
|
# Pairs up two lists, stopping at the shorter one.
|
|
113
|
-
fun zip(self, ys:
|
|
114
|
+
fun zip(self, ys: Chain[B]) -> Chain[Pair[T, B]] = match self
|
|
114
|
-
|
|
115
|
+
Empty => Empty
|
|
115
|
-
|
|
116
|
+
Link(x, xt) =>
|
|
116
117
|
match ys
|
|
117
|
-
|
|
118
|
+
Empty => Empty
|
|
118
|
-
|
|
119
|
+
Link(y, yt) => cons(Pair[T, B](fst: x, snd: y), xt.zip(yt))
|
|
119
120
|
|
|
120
121
|
record Pair[A, B](fst: A, snd: B)
|
|
121
122
|
|
|
122
123
|
# Prepends `h`: O(1), and `t` is shared, not copied.
|
|
123
|
-
fun cons(h: T, t:
|
|
124
|
+
fun cons(h: T, t: Chain[T]) -> Chain[T] = Link(head: h, tail: t)
|
|
124
125
|
|
|
125
|
-
fun single(x: T) ->
|
|
126
|
+
fun single(x: T) -> Chain[T] = cons(x, Empty)
|
|
126
127
|
|
|
127
128
|
# The integers from `a` up to but not including `b`.
|
|
128
|
-
fun fromTo(a: Int, b: Int) ->
|
|
129
|
+
fun fromTo(a: Int, b: Int) -> Chain[Int] =
|
|
129
|
-
if a >= b then
|
|
130
|
+
if a >= b then Empty else cons(a, fromTo(a + 1, b))
|
|
130
131
|
|
|
131
|
-
fun sum(xs:
|
|
132
|
+
fun sum(xs: Chain[Int]) -> Int =
|
|
132
133
|
xs.foldl(0, |a, x| a + x)
|
|
133
134
|
|
|
134
|
-
fun joinInts(xs:
|
|
135
|
+
fun joinInts(xs: Chain[Int], sep: Str) -> Str = match xs
|
|
135
|
-
|
|
136
|
+
Empty => ""
|
|
136
|
-
|
|
137
|
+
Link(h, t) =>
|
|
137
138
|
match t
|
|
138
|
-
|
|
139
|
+
Empty => "{h}"
|
|
139
|
-
|
|
140
|
+
Link(_, _) => "{h}{sep}{joinInts(t, sep)}"
|
|
140
141
|
|
|
141
142
|
# ---------------------------------------------------------------------------
|
|
142
143
|
# 2. Functions as values
|
|
@@ -197,15 +198,15 @@ enum Tree =
|
|
|
197
198
|
Leaf => acc
|
|
198
199
|
Branch(l, x, r) => l.fold(f(x, r.fold(acc, f)), f)
|
|
199
200
|
|
|
200
|
-
fun inOrder(self) ->
|
|
201
|
+
fun inOrder(self) -> Chain[Int] =
|
|
201
|
-
self.fold(
|
|
202
|
+
self.fold(Empty, |x, acc| cons(x, acc))
|
|
202
203
|
|
|
203
204
|
fun smallest(self) -> Int? = match self
|
|
204
205
|
Leaf => null
|
|
205
206
|
Branch(Leaf, x, _) => x
|
|
206
207
|
Branch(l, _, _) => l.smallest()
|
|
207
208
|
|
|
208
|
-
fun fromLink(xs:
|
|
209
|
+
fun fromLink(xs: Chain[Int]) -> Tree =
|
|
209
210
|
xs.foldl(Leaf, |t, x| t.insert(x))
|
|
210
211
|
|
|
211
212
|
# ---------------------------------------------------------------------------
|
|
@@ -227,10 +228,10 @@ enum Shape =
|
|
|
227
228
|
if Rect(w, h) := self, w > h then true else false
|
|
228
229
|
|
|
229
230
|
# Nested patterns look two levels into a list.
|
|
230
|
-
fun firstTwo(xs:
|
|
231
|
+
fun firstTwo(xs: Chain[Int]) -> Str = match xs
|
|
231
|
-
|
|
232
|
+
Link(a, Link(b, _)) => "{a},{b}"
|
|
232
|
-
|
|
233
|
+
Link(a, Empty) => "{a}"
|
|
233
|
-
|
|
234
|
+
Empty => "empty"
|
|
234
235
|
|
|
235
236
|
# ---------------------------------------------------------------------------
|
|
236
237
|
# 5. A pure scene graph
|
|
@@ -267,7 +268,7 @@ enum Node =
|
|
|
267
268
|
| Sprite(id: Int, xf: Xf, w: Float, h: Float, image: Str, spin: Float)
|
|
268
269
|
| Label(id: Int, xf: Xf, text: Str)
|
|
269
270
|
| Button(id: Int, xf: Xf, w: Float, h: Float, text: Str, pressed: Bool)
|
|
270
|
-
| Group(id: Int, xf: Xf, children:
|
|
271
|
+
| Group(id: Int, xf: Xf, children: Chain[Node])
|
|
271
272
|
|
|
272
273
|
fun id(self) -> Int = match self
|
|
273
274
|
Sprite(id, _, _, _, _, _) => id
|
|
@@ -303,13 +304,13 @@ enum Node =
|
|
|
303
304
|
_ => self
|
|
304
305
|
|
|
305
306
|
# Depth first, parents before children, so later commands draw on top.
|
|
306
|
-
fun draw(self, parent: Xf) ->
|
|
307
|
+
fun draw(self, parent: Xf) -> Chain[DrawCmd] =
|
|
307
308
|
xf := thenXf(parent, self.xf())
|
|
308
309
|
match self
|
|
309
310
|
Sprite(id, _, _, _, _, _) => single(cmd("sprite", id, xf))
|
|
310
311
|
Label(id, _, _) => single(cmd("label", id, xf))
|
|
311
312
|
Button(id, _, _, _, _, pressed) => single(cmd(if pressed then "button-down" else "button", id, xf))
|
|
312
|
-
Group(_, _, kids) => kids.foldr(
|
|
313
|
+
Group(_, _, kids) => kids.foldr(Empty, |k, acc| k.draw(xf).append(acc))
|
|
313
314
|
|
|
314
315
|
# The id of the topmost node under stage point (px, py), or null. Children drawn
|
|
315
316
|
# later are on top, so they are tried first (in reverse order).
|
|
@@ -330,9 +331,9 @@ enum Node =
|
|
|
330
331
|
fun hitBox(n: Node, p: Pt, w: Float, h: Float) -> Int? =
|
|
331
332
|
if inBox(p, w, h) then n.id() else null
|
|
332
333
|
|
|
333
|
-
fun firstHit(kids:
|
|
334
|
+
fun firstHit(kids: Chain[Node], xf: Xf, px: Float, py: Float) -> Int? = match kids
|
|
334
|
-
|
|
335
|
+
Empty => null
|
|
335
|
-
|
|
336
|
+
Link(k, rest) =>
|
|
336
337
|
if hit := k.findHit(xf, px, py)
|
|
337
338
|
hit
|
|
338
339
|
else
|
|
@@ -344,13 +345,13 @@ enum UiKind = Clicked | Pressed
|
|
|
344
345
|
|
|
345
346
|
record UiEvent(id: Int, kind: UiKind)
|
|
346
347
|
|
|
347
|
-
record Routed(node: Node, events:
|
|
348
|
+
record Routed(node: Node, events: Chain[UiEvent])
|
|
348
349
|
|
|
349
350
|
fun click(root: Node, px: Float, py: Float) -> Routed =
|
|
350
351
|
if id := root.findHit(identityXf(), px, py)
|
|
351
352
|
Routed(node: root.update(id, |n| n.press()), events: single(UiEvent(id: id, kind: Clicked)))
|
|
352
353
|
else
|
|
353
|
-
Routed(node: root, events:
|
|
354
|
+
Routed(node: root, events: Empty)
|
|
354
355
|
|
|
355
356
|
# A small scene: a group placed at (100, 50) holding a moving ship, a title and a
|
|
356
357
|
# play button inside a nested, half-size menu group.
|
|
@@ -368,22 +369,22 @@ fun demoScene() -> Node =
|
|
|
368
369
|
# One job (the sum of the even numbers), written three ways. They all agree.
|
|
369
370
|
#
|
|
370
371
|
# 1. Methods: `filter` then `foldl`, each a recursive method on the list.
|
|
371
|
-
fun sumEvensMethods(xs:
|
|
372
|
+
fun sumEvensMethods(xs: Chain[Int]) -> Int =
|
|
372
373
|
xs.filter(|x| x % 2 == 0).foldl(0, |a, x| a + x)
|
|
373
374
|
|
|
374
375
|
# 2. A `for` over the list's `items()` form, with an `if` clause as the filter.
|
|
375
|
-
fun sumEvensFor(xs:
|
|
376
|
+
fun sumEvensFor(xs: Chain[Int]) -> Int =
|
|
376
377
|
total := 0
|
|
377
378
|
for x := range xs.items(), if x % 2 == 0
|
|
378
379
|
total = total + x
|
|
379
380
|
total
|
|
380
381
|
|
|
381
382
|
# 3. A `while` loop with a local cursor, taking the list apart with a pattern.
|
|
382
|
-
fun sumEvensWhile(xs:
|
|
383
|
+
fun sumEvensWhile(xs: Chain[Int]) -> Int =
|
|
383
384
|
total := 0
|
|
384
385
|
cur := xs
|
|
385
386
|
while true
|
|
386
|
-
if
|
|
387
|
+
if Link(h, t) := cur
|
|
387
388
|
if h % 2 == 0
|
|
388
389
|
total = total + h
|
|
389
390
|
cur = t
|
|
@@ -393,22 +394,22 @@ fun sumEvensWhile(xs: Link[Int]) -> Int =
|
|
|
393
394
|
|
|
394
395
|
# A `for` is an expression when its source has a result: this form rebuilds a
|
|
395
396
|
# list from the body's values, like `map`. It is generic: `T` is bound per call.
|
|
396
|
-
# A bare `
|
|
397
|
+
# A bare `Empty` has no element type of its own, so `len(Empty)` is an error ("cannot
|
|
397
|
-
# infer T"); annotate it, as `out` is here, or `empty:
|
|
398
|
+
# infer T"); annotate it, as `out` is here, or `empty: Chain[Int] := Empty`.
|
|
398
|
-
fun* collect(xs:
|
|
399
|
+
fun* collect(xs: Chain[T]) *T -> Chain[T] =
|
|
399
|
-
out:
|
|
400
|
+
out: Chain[T] := Empty
|
|
400
401
|
for x := range xs.items()
|
|
401
402
|
v := yield x
|
|
402
403
|
out = cons(v, out)
|
|
403
404
|
out.reverse()
|
|
404
405
|
|
|
405
406
|
# Tail recursive: the recursive call is the last thing done, so it is a jump.
|
|
406
|
-
fun countUp(xs:
|
|
407
|
+
fun countUp(xs: Chain[Int], n: Int) -> Int = match xs
|
|
407
|
-
|
|
408
|
+
Empty => n
|
|
408
|
-
|
|
409
|
+
Link(_, t) => countUp(t, n + 1)
|
|
409
410
|
|
|
410
411
|
# Builds `[n-1 .. 0]` with an accumulator, again as a tail call.
|
|
411
|
-
fun build(n: Int, acc:
|
|
412
|
+
fun build(n: Int, acc: Chain[Int]) -> Chain[Int] =
|
|
412
413
|
if n <= 0 then acc else build(n - 1, cons(n - 1, acc))
|
|
413
414
|
|
|
414
415
|
# ---------------------------------------------------------------------------
|
|
@@ -419,7 +420,7 @@ test "link basics"
|
|
|
419
420
|
xs := fromTo(1, 6)
|
|
420
421
|
assert xs.length() == 5
|
|
421
422
|
assert joinInts(xs, ",") == "1,2,3,4,5"
|
|
422
|
-
empty:
|
|
423
|
+
empty: Chain[Int] := Empty
|
|
423
424
|
assert empty.isEmpty()
|
|
424
425
|
assert !xs.isEmpty()
|
|
425
426
|
assert sum(xs) == 15
|
|
@@ -472,12 +473,12 @@ test "functions returning functions"
|
|
|
472
473
|
|
|
473
474
|
test "deep list tail calls"
|
|
474
475
|
# 200000 elements: counting needs tail calls, or the stack overflows.
|
|
475
|
-
big := build(200000,
|
|
476
|
+
big := build(200000, Empty)
|
|
476
477
|
assert countUp(big, 0) == 200000
|
|
477
478
|
assert sum(big) == 19999900000
|
|
478
479
|
|
|
479
480
|
test "persistent tree"
|
|
480
|
-
t := fromLink(cons(5, cons(3, cons(8, cons(1, cons(4, cons(9,
|
|
481
|
+
t := fromLink(cons(5, cons(3, cons(8, cons(1, cons(4, cons(9, Empty)))))))
|
|
481
482
|
assert t.size() == 6
|
|
482
483
|
assert t.depth() == 3
|
|
483
484
|
assert t.member(4)
|
|
@@ -500,7 +501,7 @@ test "pattern matching"
|
|
|
500
501
|
assert !Circle(r: 1.0).wide()
|
|
501
502
|
assert firstTwo(fromTo(1, 5)) == "1,2"
|
|
502
503
|
assert firstTwo(single(7)) == "7"
|
|
503
|
-
assert firstTwo(
|
|
504
|
+
assert firstTwo(Empty) == "empty"
|
|
504
505
|
|
|
505
506
|
test "scene count and draw"
|
|
506
507
|
scene := demoScene()
|
|
@@ -531,7 +532,7 @@ test "scene click routes an event"
|
|
|
531
532
|
scene := demoScene()
|
|
532
533
|
r := click(scene, 115.0, 165.0)
|
|
533
534
|
assert r.events.length() == 1
|
|
534
|
-
if
|
|
535
|
+
if Link(e, _) := r.events
|
|
535
536
|
assert e.id == 5
|
|
536
537
|
assert e.kind == Clicked
|
|
537
538
|
kinds := r.node.draw(identityXf()).foldr("", |c, acc| c.kind + " " + acc)
|
|
@@ -546,13 +547,13 @@ test "loops three ways"
|
|
|
546
547
|
assert sumEvensMethods(xs) == 30
|
|
547
548
|
assert sumEvensFor(xs) == 30
|
|
548
549
|
assert sumEvensWhile(xs) == 30
|
|
549
|
-
empty:
|
|
550
|
+
empty: Chain[Int] := Empty
|
|
550
551
|
assert sumEvensFor(empty) == 0
|
|
551
552
|
assert sumEvensWhile(empty) == 0
|
|
552
553
|
|
|
553
554
|
test "loops build lists too"
|
|
554
555
|
xs := fromTo(1, 5)
|
|
555
|
-
seen :=
|
|
556
|
+
seen := Slice()
|
|
556
557
|
xs.each(|x| seen.push(x * 2))
|
|
557
558
|
assert seen.length() == 4
|
|
558
559
|
doubled := for x := range collect(xs), if x > 1
|
plum-examples/game.plum
CHANGED
|
@@ -10,7 +10,7 @@ module game
|
|
|
10
10
|
import std/Str
|
|
11
11
|
import std/Int
|
|
12
12
|
import std/Float
|
|
13
|
-
import std/
|
|
13
|
+
import std/Slice
|
|
14
14
|
import std/Math
|
|
15
15
|
import std/Random
|
|
16
16
|
import std/game/Vec2
|
|
@@ -37,8 +37,8 @@ record Body(x: Float, y: Float, vx: Float, vy: Float, angle: Float)
|
|
|
37
37
|
|
|
38
38
|
record World(
|
|
39
39
|
ship: Body,
|
|
40
|
-
orbs:
|
|
40
|
+
orbs: Slice[Body],
|
|
41
|
-
mines:
|
|
41
|
+
mines: Slice[Body],
|
|
42
42
|
rng: Random,
|
|
43
43
|
score: Int,
|
|
44
44
|
best: Int,
|
|
@@ -56,7 +56,7 @@ record World(
|
|
|
56
56
|
rng := Random(seed: seed)
|
|
57
57
|
ship := Body(x: Rules.ArenaW / 2.0, y: Rules.ArenaH / 2.0, vx: 0.0, vy: 0.0, angle: 0.0)
|
|
58
58
|
w := World(
|
|
59
|
-
ship: ship, orbs:
|
|
59
|
+
ship: ship, orbs: Slice[Body](), mines: Slice[Body](), rng: rng, score: 0, best: 0,
|
|
60
60
|
lives: Rules.StartLives, hurt: 0.0, clock: 0.0, over: false, sheet: sheet,
|
|
61
61
|
pickupSound: -1, hitSound: -1, overSound: -1, musicSound: -1)
|
|
62
62
|
for i := range Rules.OrbCount
|
plum-examples/game_loop.plum
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# A tiny game update loop. Particles are structs in a `
|
|
1
|
+
# A tiny game update loop. Particles are structs in a `Slice`, which stores them
|
|
2
2
|
# field by field (struct-of-arrays), so 100 frames of updates allocate nothing per
|
|
3
3
|
# particle; dead ones are removed by swap-remove. Entities live in a `Map[Int,
|
|
4
4
|
# Entity]` and are changed by events handled with `match`.
|
|
@@ -6,7 +6,7 @@ module game_loop
|
|
|
6
6
|
import std/Str
|
|
7
7
|
import std/Int
|
|
8
8
|
import std/Float
|
|
9
|
-
import std/
|
|
9
|
+
import std/Slice
|
|
10
10
|
import std/Map
|
|
11
11
|
|
|
12
12
|
struct Vec2(x: Float, y: Float) =
|
|
@@ -33,7 +33,7 @@ record Stats(spawned: Int, removed: Int)
|
|
|
33
33
|
|
|
34
34
|
# Move every particle one step; remove the dead by swap-remove (the last
|
|
35
35
|
# particle takes the freed slot, so the list never shifts). Returns how many died.
|
|
36
|
-
fun step(ps:
|
|
36
|
+
fun step(ps: Slice[Particle], dt: Float) -> Int =
|
|
37
37
|
removed := 0
|
|
38
38
|
i := 0
|
|
39
39
|
while i < ps.length()
|
|
@@ -50,7 +50,7 @@ fun step(ps: List[Particle], dt: Float) -> Int =
|
|
|
50
50
|
i = i + 1
|
|
51
51
|
removed
|
|
52
52
|
|
|
53
|
-
fun run(frames: Int, ps:
|
|
53
|
+
fun run(frames: Int, ps: Slice[Particle]) -> Stats =
|
|
54
54
|
spawned := 0
|
|
55
55
|
removed := 0
|
|
56
56
|
for frame := range frames
|
|
@@ -71,14 +71,14 @@ fun handle(world: Map[Int, Entity], e: Event) =
|
|
|
71
71
|
world.remove(id)
|
|
72
72
|
|
|
73
73
|
test "100 frames: particles spawn, move and are swap-removed when dead"
|
|
74
|
-
ps :=
|
|
74
|
+
ps := Slice[Particle]()
|
|
75
75
|
stats := run(100, ps)
|
|
76
76
|
assert stats.spawned == 10
|
|
77
77
|
assert stats.removed == 8
|
|
78
78
|
assert ps.length() == 2
|
|
79
79
|
|
|
80
80
|
test "survivors have moved and aged the expected amount"
|
|
81
|
-
ps :=
|
|
81
|
+
ps := Slice[Particle]()
|
|
82
82
|
_ := run(100, ps)
|
|
83
83
|
x := 0.0
|
|
84
84
|
life := 0
|
plum-examples/game_pure_scene.plum
CHANGED
|
@@ -17,7 +17,7 @@ module game_pure_scene
|
|
|
17
17
|
import std/Str
|
|
18
18
|
import std/Int
|
|
19
19
|
import std/Float
|
|
20
|
-
import std/
|
|
20
|
+
import std/Slice
|
|
21
21
|
import std/Math
|
|
22
22
|
import std/Random
|
|
23
23
|
import std/game/Vec2
|
|
@@ -158,7 +158,7 @@ own record Panel(
|
|
|
158
158
|
Panel(
|
|
159
159
|
id: id, x: x, y: y, width: w, height: h, originX: 0.0, originY: 0.0,
|
|
160
160
|
scaleX: 1.0, scaleY: 1.0, rotation: 0.0, color: c, visible: true,
|
|
161
|
-
touchable: Disabled, actions:
|
|
161
|
+
touchable: Disabled, actions: Slice[Action]())
|
|
162
162
|
|
|
163
163
|
|
|
164
164
|
override fun draw(self, m: Affine, tint: Color) =
|
|
@@ -174,7 +174,7 @@ own record Ship(
|
|
|
174
174
|
Ship(
|
|
175
175
|
id: Id.Ship, x: s.x - Rules.Half, y: s.y - Rules.Half, width: Rules.Size, height: Rules.Size,
|
|
176
176
|
originX: Rules.Half, originY: Rules.Half, scaleX: 1.0, scaleY: 1.0, rotation: 0.0,
|
|
177
|
-
color: Color.White, visible: true, touchable: Disabled, actions:
|
|
177
|
+
color: Color.White, visible: true, touchable: Disabled, actions: Slice[Action](),
|
|
178
178
|
clock: 0.0, tex: sheet)
|
|
179
179
|
|
|
180
180
|
|
|
@@ -197,7 +197,7 @@ own record Orb(
|
|
|
197
197
|
Orb(
|
|
198
198
|
id: id, x: at.x - Rules.Half, y: at.y - Rules.Half, width: Rules.Size, height: Rules.Size,
|
|
199
199
|
originX: Rules.Half, originY: Rules.Half, scaleX: 1.0, scaleY: 1.0, rotation: 0.0,
|
|
200
|
-
color: Color.White, visible: true, touchable: Disabled, actions:
|
|
200
|
+
color: Color.White, visible: true, touchable: Disabled, actions: Slice[Action](),
|
|
201
201
|
vx: at.vx, vy: at.vy, phase: phase, clock: 0.0, tex: sheet)
|
|
202
202
|
|
|
203
203
|
|
|
@@ -232,7 +232,7 @@ own record Mine(
|
|
|
232
232
|
Mine(
|
|
233
233
|
id: id, x: at.x - Rules.Half, y: at.y - Rules.Half, width: Rules.Size, height: Rules.Size,
|
|
234
234
|
originX: Rules.Half, originY: Rules.Half, scaleX: 1.0, scaleY: 1.0, rotation: 0.0,
|
|
235
|
-
color: Color.White, visible: true, touchable: Disabled, actions:
|
|
235
|
+
color: Color.White, visible: true, touchable: Disabled, actions: Slice[Action](),
|
|
236
236
|
vx: at.vx, vy: at.vy, spin: spin, clock: 0.0, tex: sheet)
|
|
237
237
|
|
|
238
238
|
|
|
@@ -263,7 +263,7 @@ own record Star(
|
|
|
263
263
|
Star(
|
|
264
264
|
id: Id.Star + i, x: Float({i * 7919} % 800) - r, y: Float({i * 104729} % 600) - r,
|
|
265
265
|
width: r * 2.0, height: r * 2.0, originX: 0.0, originY: 0.0, scaleX: 1.0, scaleY: 1.0,
|
|
266
|
-
rotation: 0.0, color: Color.White, visible: true, touchable: Disabled, actions:
|
|
266
|
+
rotation: 0.0, color: Color.White, visible: true, touchable: Disabled, actions: Slice[Action](),
|
|
267
267
|
seed: Float(i), clock: 0.0)
|
|
268
268
|
|
|
269
269
|
|
|
@@ -280,7 +280,7 @@ own record Star(
|
|
|
280
280
|
# --- reading and changing the tree ---
|
|
281
281
|
|
|
282
282
|
# The child of `a` reached by `path` (not empty), borrowed from the tree.
|
|
283
|
-
fun nodeAt(a: Actor, path:
|
|
283
|
+
fun nodeAt(a: Actor, path: Slice[Int], depth: Int) -> Actor =
|
|
284
284
|
ks := kidsOf(a)
|
|
285
285
|
i := path.at(depth)
|
|
286
286
|
if depth + 1 >= path.length()
|
|
@@ -292,7 +292,7 @@ fun find(root: Actor, id: Int) -> Actor =
|
|
|
292
292
|
path := SceneTree.findPath(root, id) ?: crash "no such actor"
|
|
293
293
|
nodeAt(root, path, 0)
|
|
294
294
|
|
|
295
|
-
fun kidsOf(a: Actor) ->
|
|
295
|
+
fun kidsOf(a: Actor) -> Slice[Actor] =
|
|
296
296
|
a.kids() ?: crash "not a group"
|
|
297
297
|
|
|
298
298
|
fun setVisible(own a: Actor, v: Bool) -> Actor =
|
|
@@ -335,16 +335,16 @@ fun icon(sheet: Int, i: Int) -> Actor =
|
|
|
335
335
|
# game-over overlay with its restart button.
|
|
336
336
|
fun buildRoot(rng: Random, sheet: Int, best: Int) -> Actor =
|
|
337
337
|
ship := newShip()
|
|
338
|
-
stars :=
|
|
338
|
+
stars := Slice[Actor]()
|
|
339
339
|
for i := range 40
|
|
340
340
|
stars.push(Star.create(i))
|
|
341
|
-
mines :=
|
|
341
|
+
mines := Slice[Actor]()
|
|
342
|
-
orbs :=
|
|
342
|
+
orbs := Slice[Actor]()
|
|
343
343
|
for i := range Rules.OrbCount
|
|
344
344
|
orbs.push(Orb.create(sheet, Float(i) * 0.1, Id.Orb + i, spawn(rng, ship.x, ship.y, 120.0, 40.0)))
|
|
345
345
|
for i := range mineCount(0)
|
|
346
346
|
mines.push(Mine.create(sheet, Id.Mine + i, spawn(rng, ship.x, ship.y, 220.0, 90.0), 2.0))
|
|
347
|
-
over :=
|
|
347
|
+
over := Slice[Actor]()
|
|
348
348
|
over.push(Panel.create(Id.OverlayPanel, 0.0, 230.0, Rules.ArenaW, 170.0, Color(r: 0.0, g: 0.0, b: 0.0, a: 0.6)))
|
|
349
349
|
over.push(hudLabel(Id.OverTitle, "Game over", 300.0, 244.0, 48.0, Color.Red))
|
|
350
350
|
over.push(hudLabel(Id.OverHint, "Press Enter to play again", 270.0, 300.0, 24.0, Color.White))
|
|
@@ -354,13 +354,13 @@ fun buildRoot(rng: Random, sheet: Int, best: Int) -> Actor =
|
|
|
354
354
|
over.push(btn)
|
|
355
355
|
overlay := PureScene.group(Id.Overlay, 0.0, 0.0, 0.0, 0.0, over)
|
|
356
356
|
overlay.visible = false
|
|
357
|
-
hud :=
|
|
357
|
+
hud := Slice[Actor]()
|
|
358
358
|
hud.push(hudLabel(Id.Score, "Score 0", 16.0, 12.0, 28.0, Color.White))
|
|
359
359
|
hud.push(hudLabel(Id.Best, "Best {best}", 16.0, 44.0, 20.0, Color.Yellow))
|
|
360
360
|
for i := range Rules.StartLives
|
|
361
361
|
hud.push(icon(sheet, i))
|
|
362
362
|
hud.push(overlay)
|
|
363
|
-
layers :=
|
|
363
|
+
layers := Slice[Actor]()
|
|
364
364
|
layers.push(Panel.create(Id.Backdrop, 0.0, 0.0, Rules.ArenaW, Rules.ArenaH, Color.rgb(0.07, 0.08, 0.16)))
|
|
365
365
|
layers.push(PureScene.group(Id.StarLayer, 0.0, 0.0, 0.0, 0.0, stars))
|
|
366
366
|
layers.push(PureScene.group(Id.MineLayer, 0.0, 0.0, 0.0, 0.0, mines))
|
|
@@ -472,7 +472,7 @@ fun overlaps(ax: Float, ay: Float, ra: Float, bx: Float, by: Float, rb: Float) -
|
|
|
472
472
|
Math.hypot(ax - bx, ay - by) < ra + rb
|
|
473
473
|
|
|
474
474
|
# The stage after `inputs` and `dt`, appending the events it raised to `events`.
|
|
475
|
-
fun frameInto(own s: Stage, dt: Float, inputs:
|
|
475
|
+
fun frameInto(own s: Stage, dt: Float, inputs: Slice[Input], events: Slice[UiEvent]) -> Stage =
|
|
476
476
|
h := s.frame(dt, inputs)
|
|
477
477
|
for i := range h.events.length()
|
|
478
478
|
events.push(h.events.at(i))
|
|
@@ -481,9 +481,9 @@ fun frameInto(own s: Stage, dt: Float, inputs: List[Input], events: List[UiEvent
|
|
|
481
481
|
# One frame: the stage takes the pointer `inputs` and advances every actor by `dt`
|
|
482
482
|
# (a click on the restart button restarts), then the rules look at the result. Once
|
|
483
483
|
# the game is over the actors only animate. Returns true when it restarted.
|
|
484
|
-
fun advance(w: World, ax: Float, ay: Float, dt: Float, inputs:
|
|
484
|
+
fun advance(w: World, ax: Float, ay: Float, dt: Float, inputs: Slice[Input]) -> Bool =
|
|
485
485
|
w.clock = w.clock + dt
|
|
486
|
-
events :=
|
|
486
|
+
events := Slice[UiEvent]()
|
|
487
487
|
w.stage = frameInto(w.stage, dt, inputs, events)
|
|
488
488
|
for i := range events.length()
|
|
489
489
|
e := events.at(i)
|
|
@@ -532,7 +532,7 @@ fun advance(w: World, ax: Float, ay: Float, dt: Float, inputs: List[Input]) -> B
|
|
|
532
532
|
|
|
533
533
|
# One simulation step without pointer input.
|
|
534
534
|
fun step(w: World, ax: Float, ay: Float, dt: Float) -> Bool =
|
|
535
|
-
advance(w, ax, ay, dt,
|
|
535
|
+
advance(w, ax, ay, dt, Slice[Input]())
|
|
536
536
|
|
|
537
537
|
# The camera that fits the arena into a screen of `sw` by `sh` pixels, centred.
|
|
538
538
|
fun fitCamera(sw: Float, sh: Float) -> Affine =
|
|
@@ -826,7 +826,7 @@ test "restart_button_click_restarts"
|
|
|
826
826
|
assert find(w.stage.root, Id.Restart).visible
|
|
827
827
|
down := Input(kind: PointerDown, x: 400.0, y: 364.0)
|
|
828
828
|
up := Input(kind: PointerUp, x: 400.0, y: 364.0)
|
|
829
|
-
taps :=
|
|
829
|
+
taps := Slice[Input]()
|
|
830
830
|
taps.push(down)
|
|
831
831
|
taps.push(up)
|
|
832
832
|
restarted := advance(w, 0.0, 0.0, 0.016, taps)
|
|
@@ -839,7 +839,7 @@ test "restart_button_click_restarts"
|
|
|
839
839
|
test "a click on the restart button while playing does nothing"
|
|
840
840
|
w := emptyWorld()
|
|
841
841
|
w.score = 30
|
|
842
|
-
taps :=
|
|
842
|
+
taps := Slice[Input]()
|
|
843
843
|
taps.push(Input(kind: PointerDown, x: 400.0, y: 364.0))
|
|
844
844
|
taps.push(Input(kind: PointerUp, x: 400.0, y: 364.0))
|
|
845
845
|
restarted := advance(w, 0.0, 0.0, 0.016, taps)
|
plum-examples/game_scene.plum
CHANGED
|
@@ -24,7 +24,7 @@ module game_scene
|
|
|
24
24
|
import std/Str
|
|
25
25
|
import std/Int
|
|
26
26
|
import std/Float
|
|
27
|
-
import std/
|
|
27
|
+
import std/Slice
|
|
28
28
|
import std/Math
|
|
29
29
|
import std/Random
|
|
30
30
|
import std/game/Vec2
|
|
@@ -246,12 +246,12 @@ record World(
|
|
|
246
246
|
ship: Ship,
|
|
247
247
|
mines: Group,
|
|
248
248
|
orbs: Group,
|
|
249
|
-
mineList:
|
|
249
|
+
mineList: Slice[Mine],
|
|
250
|
-
orbList:
|
|
250
|
+
orbList: Slice[Orb],
|
|
251
251
|
overlay: Group,
|
|
252
252
|
scoreText: Text,
|
|
253
253
|
bestText: Text,
|
|
254
|
-
lifeIcons:
|
|
254
|
+
lifeIcons: Slice[Sprite],
|
|
255
255
|
rng: Random,
|
|
256
256
|
score: Int,
|
|
257
257
|
best: Int,
|
|
@@ -281,7 +281,7 @@ record World(
|
|
|
281
281
|
bestText.y = 44.0
|
|
282
282
|
hud.add(scoreText)
|
|
283
283
|
hud.add(bestText)
|
|
284
|
-
icons :=
|
|
284
|
+
icons := Slice[Sprite]()
|
|
285
285
|
for i := range Rules.StartLives
|
|
286
286
|
icon := Sprite.create(sheet, 0.0, 0.0, Rules.Cell, Rules.Cell, 32.0, 32.0)
|
|
287
287
|
icon.x = Rules.ArenaW - 40.0 - Float(i) * 36.0
|
|
@@ -295,7 +295,7 @@ record World(
|
|
|
295
295
|
overlay.add(Text.create("Press Enter to play again", 24.0, Color.White).at(270.0, 316.0))
|
|
296
296
|
hud.add(overlay)
|
|
297
297
|
w := World(
|
|
298
|
-
camScale: 1.0, ship: ship, mines: mines, orbs: orbs, mineList:
|
|
298
|
+
camScale: 1.0, ship: ship, mines: mines, orbs: orbs, mineList: Slice[Mine](), orbList: Slice[Orb](),
|
|
299
299
|
overlay: overlay, scoreText: scoreText, bestText: bestText, lifeIcons: icons,
|
|
300
300
|
rng: rng, score: 0, best: 0, lives: Rules.StartLives, over: false, clock: 0.0, sheet: sheet,
|
|
301
301
|
pickupSound: -1, hitSound: -1, overSound: -1, musicSound: -1)
|
plum-examples/game_state_view.plum
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
# "Again" button restarts after the game is over.
|
|
6
6
|
#
|
|
7
7
|
# The game is three plain pieces. `State` is plain data: the ship, the orbs and the
|
|
8
|
-
# mines (
|
|
8
|
+
# mines (cons `List` lists keyed by id), the score, the random seed and the screen
|
|
9
9
|
# size. `Msg` is what can happen to it (a tick of time, a resize, a restart), an enum
|
|
10
10
|
# with inline variants. `step(state, msg)` is pure and returns the next `State`;
|
|
11
11
|
# `view(state)` is pure too and builds a fresh `SceneNode` picture every frame, which
|
|
12
|
-
# `Picture.draw` interprets. Both are structural recursion over `Link` and `
|
|
12
|
+
# `Picture.draw` interprets. Both are structural recursion over `Link` and `List[SceneNode]`:
|
|
13
13
|
# no loop runs in `step` or `view`. There is no
|
|
14
14
|
# retained scene: the restart button is found by `Picture.hitPath` on the picture
|
|
15
15
|
# the player is looking at. The `World` record is the host shell: it holds the
|
|
@@ -18,6 +18,7 @@ module game_state_view
|
|
|
18
18
|
import std/Str
|
|
19
19
|
import std/Int
|
|
20
20
|
import std/Float
|
|
21
|
+
import std/Slice
|
|
21
22
|
import std/List
|
|
22
23
|
import std/Math
|
|
23
24
|
import std/game/Vec2
|
|
@@ -72,8 +73,8 @@ struct Mine(id: Int, x: Float, y: Float, vx: Float, vy: Float, rotation: Float,
|
|
|
72
73
|
# the view needs: the sprite sheet and the render target of the game-over filter.
|
|
73
74
|
record State(
|
|
74
75
|
ship: ShipState,
|
|
75
|
-
orbs:
|
|
76
|
+
orbs: List[Orb],
|
|
76
|
-
mines:
|
|
77
|
+
mines: List[Mine],
|
|
77
78
|
seed: Int,
|
|
78
79
|
score: Int,
|
|
79
80
|
best: Int,
|
|
@@ -92,36 +93,34 @@ enum Msg =
|
|
|
92
93
|
| Resize(w: Float, h: Float)
|
|
93
94
|
| Restart(seed: Int)
|
|
94
95
|
|
|
95
|
-
# A recursive list: the empty `Nil`, or a head in front of another list.
|
|
96
|
-
enum Link[T] = Nil | Cons(head: T, tail: Link[T])
|
|
97
96
|
|
|
98
|
-
fun mapLink(xs:
|
|
97
|
+
fun mapLink(xs: List[A], f: fn(A) -> B) -> List[B] = match xs
|
|
99
98
|
Nil => Nil
|
|
100
99
|
Cons(h, t) => Cons(head: f(h), tail: mapLink(t, f))
|
|
101
100
|
|
|
102
|
-
fun lenLink(xs:
|
|
101
|
+
fun lenLink(xs: List[T]) -> Int = match xs
|
|
103
102
|
Nil => 0
|
|
104
103
|
Cons(_, t) => 1 + lenLink(t)
|
|
105
104
|
|
|
106
105
|
# `xs` with `x` added at the end.
|
|
107
|
-
fun snoc(xs:
|
|
106
|
+
fun snoc(xs: List[T], x: T) -> List[T] = match xs
|
|
108
107
|
Nil => Cons(head: x, tail: Nil)
|
|
109
108
|
Cons(h, t) => Cons(head: h, tail: snoc(t, x))
|
|
110
109
|
|
|
111
110
|
# `xs` with the element at `i` replaced by `x`.
|
|
112
|
-
fun replaceNth(xs:
|
|
111
|
+
fun replaceNth(xs: List[T], i: Int, x: T) -> List[T] = match xs
|
|
113
112
|
Nil => Nil
|
|
114
113
|
Cons(h, t) => if i <= 0 then Cons(head: x, tail: t) else Cons(head: h, tail: replaceNth(t, i - 1, x))
|
|
115
114
|
|
|
116
115
|
# The element at `i`, or `dflt` past the end.
|
|
117
|
-
fun nthOr(xs:
|
|
116
|
+
fun nthOr(xs: List[T], i: Int, dflt: T) -> T = match xs
|
|
118
117
|
Nil => dflt
|
|
119
118
|
Cons(h, t) => if i <= 0 then h else nthOr(t, i - 1, dflt)
|
|
120
119
|
|
|
121
120
|
# The nodes f(x) for every x of `xs`, followed by `rest`.
|
|
122
|
-
fun toNodes(xs:
|
|
121
|
+
fun toNodes(xs: List[T], f: fn(T) -> SceneNode, rest: List[SceneNode]) -> List[SceneNode] = match xs
|
|
123
122
|
Nil => rest
|
|
124
|
-
Cons(h, t) =>
|
|
123
|
+
Cons(h, t) => Cons(head: f(h), tail: toNodes(t, f, rest))
|
|
125
124
|
|
|
126
125
|
# --- small pure helpers ---
|
|
127
126
|
|
|
@@ -223,9 +222,9 @@ fun mineMove(k: Mine, dt: Float) -> Mine =
|
|
|
223
222
|
# --- the state ---
|
|
224
223
|
|
|
225
224
|
# A built list of orbs and the seed to continue from.
|
|
226
|
-
record OrbsMade(orbs:
|
|
225
|
+
record OrbsMade(orbs: List[Orb], seed: Int)
|
|
227
226
|
|
|
228
|
-
record MinesMade(mines:
|
|
227
|
+
record MinesMade(mines: List[Mine], seed: Int)
|
|
229
228
|
|
|
230
229
|
# Orbs `i` and up to OrbCount, each at a random spot away from the ship.
|
|
231
230
|
fun makeOrbs(i: Int, seed: Int, ship: ShipState) -> OrbsMade =
|
|
@@ -265,9 +264,9 @@ fun freeze(s: State) -> State =
|
|
|
265
264
|
n
|
|
266
265
|
|
|
267
266
|
# The orbs after the ship collected the ones it touches, with the score and seed.
|
|
268
|
-
record Collected(orbs:
|
|
267
|
+
record Collected(orbs: List[Orb], seed: Int, score: Int)
|
|
269
268
|
|
|
270
|
-
fun collectOrbs(orbs:
|
|
269
|
+
fun collectOrbs(orbs: List[Orb], ship: ShipState, seed: Int, score: Int) -> Collected = match orbs
|
|
271
270
|
Nil => Collected(orbs: Nil, seed: seed, score: score)
|
|
272
271
|
Cons(o, rest) =>
|
|
273
272
|
if overlaps(ship.x, ship.y, Rules.ShipRadius, o.x, o.y, Rules.OrbRadius)
|
|
@@ -280,14 +279,14 @@ fun collectOrbs(orbs: Link[Orb], ship: ShipState, seed: Int, score: Int) -> Coll
|
|
|
280
279
|
Collected(orbs: Cons(head: o, tail: kept.orbs), seed: kept.seed, score: kept.score)
|
|
281
280
|
|
|
282
281
|
# `mines` grown (new mines go at the end) until there are `want`.
|
|
283
|
-
fun growMines(mines:
|
|
282
|
+
fun growMines(mines: List[Mine], have: Int, want: Int, seed: Int, ship: ShipState) -> MinesMade =
|
|
284
283
|
if have >= want
|
|
285
284
|
return MinesMade(mines: mines, seed: seed)
|
|
286
285
|
sp := spawn(seed, ship.x, ship.y, 220.0, 90.0)
|
|
287
286
|
added := snoc(mines, mineAt(Id.Mine + have, 0.0, 2.0, 0.0, Motion(x: sp.x, y: sp.y, vx: sp.vx, vy: sp.vy)))
|
|
288
287
|
growMines(added, have + 1, want, sp.seed, ship)
|
|
289
288
|
|
|
290
|
-
fun touchesMine(mines:
|
|
289
|
+
fun touchesMine(mines: List[Mine], ship: ShipState) -> Bool = match mines
|
|
291
290
|
Nil => false
|
|
292
291
|
Cons(k, rest) => overlaps(ship.x, ship.y, Rules.ShipRadius, k.x, k.y, Rules.MineRadius) || touchesMine(rest, ship)
|
|
293
292
|
|
|
@@ -354,8 +353,8 @@ fun starView(i: Int, clock: Float) -> SceneNode =
|
|
|
354
353
|
CircleShape(cx: Float({i * 7919} % 800), cy: Float({i * 104729} % 600), r: r, color: Color(r: 0.7, g: 0.8, b: 1.0, a: twinkle))
|
|
355
354
|
|
|
356
355
|
# The stars `i` and up to 40.
|
|
357
|
-
fun starNodes(i: Int, clock: Float) ->
|
|
356
|
+
fun starNodes(i: Int, clock: Float) -> List[SceneNode] =
|
|
358
|
-
if i >= 40 then
|
|
357
|
+
if i >= 40 then Nil else Cons(head: starView(i, clock), tail: starNodes(i + 1, clock))
|
|
359
358
|
|
|
360
359
|
fun starsView(clock: Float) -> SceneNode =
|
|
361
360
|
Group(transform: Identity, children: starNodes(0, clock))
|
|
@@ -363,79 +362,79 @@ fun starsView(clock: Float) -> SceneNode =
|
|
|
363
362
|
fun mineView(sheet: Int, k: Mine) -> SceneNode =
|
|
364
363
|
Group(
|
|
365
364
|
transform: Compose(first: Translate(dx: k.x, dy: k.y), then: Rotate(radians: k.rotation)),
|
|
366
|
-
children:
|
|
365
|
+
children: Cons(head: cell(sheet, 0.0 - Rules.Half, 0.0 - Rules.Half, Rules.Size, frameOf(k.clock, 4, 4.0), 2), tail: Nil))
|
|
367
366
|
|
|
368
367
|
# The glow is drawn additively, then the sprite on top.
|
|
369
368
|
fun orbView(sheet: Int, o: Orb) -> SceneNode =
|
|
370
369
|
glow := Group(
|
|
371
370
|
transform: Identity,
|
|
372
|
-
children:
|
|
371
|
+
children: Cons(
|
|
373
372
|
head: CircleShape(cx: 0.0, cy: 0.0, r: 26.0, color: Color(r: 0.35, g: 0.22, b: 0.0, a: 0.5)),
|
|
374
|
-
tail:
|
|
373
|
+
tail: Cons(head: CircleShape(cx: 0.0, cy: 0.0, r: 17.0, color: Color(r: 0.6, g: 0.4, b: 0.05, a: 0.5)), tail: Nil)))
|
|
375
374
|
Group(
|
|
376
375
|
transform: Translate(dx: o.x, dy: o.y),
|
|
377
|
-
children:
|
|
376
|
+
children: Cons(
|
|
378
377
|
head: Filter(effect: BlendFx(mode: Blend.Add), child: glow, target: 0 - 1),
|
|
379
|
-
tail:
|
|
378
|
+
tail: Cons(head: cell(sheet, 0.0 - Rules.Half, 0.0 - Rules.Half, Rules.Size, frameOf(o.clock + o.phase, 4, 8.0), 1), tail: Nil)))
|
|
380
379
|
|
|
381
380
|
# The ship blinks while hurt and is gone once the game is over (an empty group keeps the slots).
|
|
382
381
|
fun shipView(s: State) -> SceneNode =
|
|
383
382
|
blink := s.ship.hurt > 0.0 && frameOf(s.clock, 2, 10.0) == 0
|
|
384
383
|
if s.over || blink
|
|
385
|
-
return Group(transform: Identity, children:
|
|
384
|
+
return Group(transform: Identity, children: Nil)
|
|
386
385
|
Group(
|
|
387
386
|
transform: Compose(first: Translate(dx: s.ship.x, dy: s.ship.y), then: Rotate(radians: s.ship.rotation)),
|
|
388
|
-
children:
|
|
387
|
+
children: Cons(
|
|
389
388
|
head: CircleShape(cx: 0.0, cy: 0.0, r: 17.0, color: Color(r: 0.4, g: 0.8, b: 1.0, a: 0.12)),
|
|
390
|
-
tail:
|
|
389
|
+
tail: Cons(head: cell(s.sheet, 0.0 - Rules.Half, 0.0 - Rules.Half, Rules.Size, frameOf(s.clock, 4, 14.0), 0), tail: Nil)))
|
|
391
390
|
|
|
392
391
|
# One life icon per remaining life, from the right edge going left.
|
|
393
|
-
fun lifeNodes(i: Int, lives: Int, sheet: Int) ->
|
|
392
|
+
fun lifeNodes(i: Int, lives: Int, sheet: Int) -> List[SceneNode] =
|
|
394
393
|
if i >= lives
|
|
395
|
-
|
|
394
|
+
Nil
|
|
396
395
|
else
|
|
397
|
-
|
|
396
|
+
Cons(head: cell(sheet, Rules.ArenaW - 40.0 - Float(i) * 36.0, 12.0, 32.0, 0, 0), tail: lifeNodes(i + 1, lives, sheet))
|
|
398
397
|
|
|
399
398
|
fun hudView(s: State) -> SceneNode =
|
|
400
399
|
Group(
|
|
401
400
|
transform: Identity,
|
|
402
|
-
children:
|
|
401
|
+
children: Cons(
|
|
403
402
|
head: Text(text: "Score {s.score}", x: 16.0, y: 12.0, size: 28.0, color: Color.White),
|
|
404
|
-
tail:
|
|
403
|
+
tail: Cons(head: Text(text: "Best {s.best.max(s.score)}", x: 16.0, y: 44.0, size: 20.0, color: Color.Yellow), tail: lifeNodes(0, s.lives, s.sheet))))
|
|
405
404
|
|
|
406
405
|
# The game-over panel with the restart button, faded in through a render-target filter.
|
|
407
406
|
# While playing it is an empty group, so nothing in it can be hit.
|
|
408
407
|
fun overlayView(s: State) -> SceneNode =
|
|
409
408
|
if !s.over
|
|
410
|
-
return Group(transform: Identity, children:
|
|
409
|
+
return Group(transform: Identity, children: Nil)
|
|
411
410
|
button := Group(
|
|
412
411
|
transform: Identity,
|
|
413
|
-
children:
|
|
412
|
+
children: Cons(
|
|
414
413
|
head: RectShape(x: 340.0, y: 342.0, w: 120.0, h: 44.0, color: Color(r: 0.5, g: 0.8, b: 1.0, a: 0.9)),
|
|
415
|
-
tail:
|
|
414
|
+
tail: Cons(head: Text(text: "Again", x: 372.0, y: 352.0, size: 24.0, color: Color.White), tail: Nil)))
|
|
416
415
|
panel := Group(
|
|
417
416
|
transform: Identity,
|
|
418
|
-
children:
|
|
417
|
+
children: Cons(
|
|
419
418
|
head: RectShape(x: 0.0, y: 230.0, w: Rules.ArenaW, h: 170.0, color: Color(r: 0.0, g: 0.0, b: 0.0, a: 0.6)),
|
|
420
|
-
tail:
|
|
419
|
+
tail: Cons(
|
|
421
420
|
head: Text(text: "Game over", x: 300.0, y: 244.0, size: 48.0, color: Color.Red),
|
|
422
|
-
tail:
|
|
421
|
+
tail: Cons(
|
|
423
422
|
head: Text(text: "Press Enter to play again", x: 270.0, y: 300.0, size: 24.0, color: Color.White),
|
|
424
|
-
tail:
|
|
423
|
+
tail: Cons(head: button, tail: Nil)))))
|
|
425
424
|
Filter(effect: Alpha(alpha: {s.overClock * 2.0}.min(1.0)), child: panel, target: s.target)
|
|
426
425
|
|
|
427
426
|
# The whole picture, in drawing order, under the camera that fits the arena on screen.
|
|
428
427
|
fun view(s: State) -> SceneNode =
|
|
429
428
|
z := zoomOf(s)
|
|
430
|
-
layers :=
|
|
429
|
+
layers := Cons(
|
|
431
430
|
head: RectShape(x: 0.0, y: 0.0, w: Rules.ArenaW, h: Rules.ArenaH, color: Color.rgb(0.07, 0.08, 0.16)),
|
|
432
|
-
tail:
|
|
431
|
+
tail: Cons(
|
|
433
432
|
head: starsView(s.clock),
|
|
434
|
-
tail:
|
|
433
|
+
tail: Cons(
|
|
435
|
-
head: Group(transform: Identity, children: toNodes(s.mines, |k| mineView(s.sheet, k),
|
|
434
|
+
head: Group(transform: Identity, children: toNodes(s.mines, |k| mineView(s.sheet, k), Nil)),
|
|
436
|
-
tail:
|
|
435
|
+
tail: Cons(
|
|
437
|
-
head: Group(transform: Identity, children: toNodes(s.orbs, |o| orbView(s.sheet, o),
|
|
436
|
+
head: Group(transform: Identity, children: toNodes(s.orbs, |o| orbView(s.sheet, o), Nil)),
|
|
438
|
-
tail:
|
|
437
|
+
tail: Cons(head: shipView(s), tail: Cons(head: hudView(s), tail: Cons(head: overlayView(s), tail: Nil)))))))
|
|
439
438
|
Group(transform: Compose(first: Translate(dx: offsetX(s), dy: offsetY(s)), then: Scale(sx: z, sy: z)), children: layers)
|
|
440
439
|
|
|
441
440
|
# True when (x, y) in screen pixels is on the restart button of the picture `view` builds.
|
|
@@ -552,9 +551,9 @@ fun countNodes(n: SceneNode) -> Int = match n
|
|
|
552
551
|
Filter(_, child, _) => 1 + countNodes(child)
|
|
553
552
|
_ => 1
|
|
554
553
|
|
|
555
|
-
fun countKids(ks:
|
|
554
|
+
fun countKids(ks: List[SceneNode]) -> Int = match ks
|
|
556
|
-
|
|
555
|
+
Nil => 0
|
|
557
|
-
|
|
556
|
+
Cons(h, t) => countNodes(h) + countKids(t)
|
|
558
557
|
|
|
559
558
|
test "steering accelerates and drag slows the ship"
|
|
560
559
|
s := shipStep(newShip(), 1.0, 0.0, 0.1)
|
plum-examples/generics.plum
CHANGED
|
@@ -4,12 +4,12 @@ module generics
|
|
|
4
4
|
import std/Str
|
|
5
5
|
import std/Int
|
|
6
6
|
import std/Float
|
|
7
|
-
import std/
|
|
7
|
+
import std/Slice
|
|
8
8
|
import std/Map
|
|
9
9
|
|
|
10
10
|
# Generics are erased: a `T` is a nullable object reference at run time, and
|
|
11
11
|
# Int / Float values are boxed when they cross a generic edge (a `T` param,
|
|
12
|
-
# a
|
|
12
|
+
# a Slice slot, a Map value, an error-union payload) and unboxed on the way out. These
|
|
13
13
|
# tests push awkward values through every such edge and check they come back
|
|
14
14
|
# bit-for-bit.
|
|
15
15
|
|
|
@@ -39,7 +39,7 @@ fun pick(x: T, f: fn(T) -> T) -> T =
|
|
|
39
39
|
fun twice(f: fn(T) -> T, x: T) -> T =
|
|
40
40
|
f(f(x))
|
|
41
41
|
|
|
42
|
-
fun readDeep(outer:
|
|
42
|
+
fun readDeep(outer: Slice[Slice[Map[Str, Int]]], r: Int, c: Int, key: Str) -> error{OutOfBounds, KeyNotFound}!Int =
|
|
43
43
|
row := try outer.get(r)
|
|
44
44
|
m := try row.get(c)
|
|
45
45
|
v := try m.get(key)
|
|
@@ -72,7 +72,7 @@ fun nameOf(s: T) -> Str =
|
|
|
72
72
|
# the closure stored in the `add` field (a method named `add` would win).
|
|
73
73
|
record Num[T](add: fn(T, T) -> T, zero: T)
|
|
74
74
|
|
|
75
|
-
fun sumWith(xs:
|
|
75
|
+
fun sumWith(xs: Slice[T], num: Num[T]) -> T =
|
|
76
76
|
acc := num.zero
|
|
77
77
|
for x := range xs
|
|
78
78
|
acc = num.add(acc, x)
|
|
@@ -114,7 +114,7 @@ record Crate[T](value: T) =
|
|
|
114
114
|
|
|
115
115
|
fun viaLocal(self) -> T =
|
|
116
116
|
c: Crate := self
|
|
117
|
-
kept :=
|
|
117
|
+
kept := Slice[Crate]()
|
|
118
118
|
kept.push(c)
|
|
119
119
|
kept.at(0).value
|
|
120
120
|
|
|
@@ -162,14 +162,14 @@ test "Float specials survive a generic fn"
|
|
|
162
162
|
assert id(1.5) == 1.5
|
|
163
163
|
|
|
164
164
|
test "Int and Float extremes survive List push/get"
|
|
165
|
-
ints :=
|
|
165
|
+
ints := Slice[Int]()
|
|
166
166
|
ints.push(9223372036854775807)
|
|
167
167
|
ints.push(0 - 9223372036854775807 - 1)
|
|
168
168
|
ints.push(9007199254740993)
|
|
169
169
|
assert {ints.get(0) catch 0} == 9223372036854775807
|
|
170
170
|
assert {ints.get(1) catch 0} == 0 - 9223372036854775807 - 1
|
|
171
171
|
assert {ints.get(2) catch 0} == 9007199254740993
|
|
172
|
-
floats :=
|
|
172
|
+
floats := Slice[Float]()
|
|
173
173
|
floats.push(0.0 / 0.0)
|
|
174
174
|
floats.push(-0.0)
|
|
175
175
|
floats.push(5e-324)
|
|
@@ -213,12 +213,12 @@ test "Float specials survive an error-union payload with ? and catch"
|
|
|
213
213
|
test "List[List[Map[Str, Int]]] builds and reads back"
|
|
214
214
|
m1 := Map("ok" => 7)
|
|
215
215
|
m2 := Map("big" => 9223372036854775807)
|
|
216
|
-
inner1 :=
|
|
216
|
+
inner1 := Slice[Map[Str, Int]]()
|
|
217
217
|
inner1.push(m1)
|
|
218
218
|
inner1.push(m2)
|
|
219
|
-
inner2 :=
|
|
219
|
+
inner2 := Slice[Map[Str, Int]]()
|
|
220
220
|
inner2.push(m2)
|
|
221
|
-
outer :=
|
|
221
|
+
outer := Slice[Slice[Map[Str, Int]]]()
|
|
222
222
|
outer.push(inner1)
|
|
223
223
|
outer.push(inner2)
|
|
224
224
|
assert outer.length() == 2
|
|
@@ -230,12 +230,12 @@ test "List[List[Map[Str, Int]]] builds and reads back"
|
|
|
230
230
|
assert {readDeep(outer, 2, 0, "big") catch 97} == 97
|
|
231
231
|
|
|
232
232
|
test "a generic receiver's methods are chosen per element type"
|
|
233
|
-
ints :=
|
|
233
|
+
ints := Slice(1, 22, 333)
|
|
234
|
-
strs :=
|
|
234
|
+
strs := Slice[Str]()
|
|
235
235
|
for n := range ints
|
|
236
236
|
strs.push("n{n}")
|
|
237
237
|
assert strs.join(",") == "n1,n22,n333"
|
|
238
|
-
lens :=
|
|
238
|
+
lens := Slice[Int]()
|
|
239
239
|
for s := range strs
|
|
240
240
|
lens.push(s.length())
|
|
241
241
|
assert lens.join(",") == "2,3,4"
|
|
@@ -256,10 +256,10 @@ test "closures nested three deep capture Int and Str locals through generic para
|
|
|
256
256
|
test "a duck-typed generic fn dispatches statically per instantiation"
|
|
257
257
|
assert nameOf(Circle(r: 1.0)) == "circle"
|
|
258
258
|
assert nameOf(Square(s: 1.0)) == "square"
|
|
259
|
-
shapes :=
|
|
259
|
+
shapes := Slice[Circle]()
|
|
260
260
|
shapes.push(Circle(r: 2.0))
|
|
261
261
|
shapes.push(Circle(r: 3.0))
|
|
262
|
-
names :=
|
|
262
|
+
names := Slice[Str]()
|
|
263
263
|
total := 0.0
|
|
264
264
|
for s := range shapes
|
|
265
265
|
names.push(s.name())
|
|
@@ -289,22 +289,22 @@ test "num_record_passed_explicitly"
|
|
|
289
289
|
intNum := Num(add: |a, b| a + b, zero: 0)
|
|
290
290
|
floatNum := Num(add: |a, b| a + b, zero: 0.0)
|
|
291
291
|
strNum := Num(add: |a, b| a + b, zero: "")
|
|
292
|
-
assert sumWith(
|
|
292
|
+
assert sumWith(Slice(1, 2, 3), intNum) == 6
|
|
293
|
-
assert sumWith(
|
|
293
|
+
assert sumWith(Slice(1.5, 2.5), floatNum) == 4.0
|
|
294
|
-
assert sumWith(
|
|
294
|
+
assert sumWith(Slice("a", "b"), strNum) == "ab"
|
|
295
295
|
|
|
296
296
|
test "untyped_empty_list_field_access_after_push"
|
|
297
|
-
out :=
|
|
297
|
+
out := Slice()
|
|
298
298
|
out.push(Circle(r: 3.0))
|
|
299
299
|
assert out.at(0).r == 3.0
|
|
300
300
|
first := out.at(0)
|
|
301
301
|
assert first.r == 3.0
|
|
302
302
|
|
|
303
303
|
test "nested generic constructor call as an argument"
|
|
304
|
-
r :=
|
|
304
|
+
r := Slice(Slice(1, 2), Slice(3))
|
|
305
305
|
assert r.length() == 2
|
|
306
306
|
assert r.at(0).length() == 2
|
|
307
|
-
s :=
|
|
307
|
+
s := Slice(Slice("a"))
|
|
308
308
|
assert s.at(0).at(0) == "a"
|
|
309
309
|
|
|
310
310
|
test "a generic record is specialized per type and its methods run"
|
plum-examples/json.plum
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
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
|
-
import std/
|
|
6
|
+
import std/Slice
|
|
7
7
|
import std/Json
|
|
8
8
|
|
|
9
|
-
record House(name: Str, rooms: Int, tags:
|
|
9
|
+
record House(name: Str, rooms: Int, tags: Slice[Str], annex: Slice[House]) =
|
|
10
10
|
fun toJson(self) -> Json =
|
|
11
11
|
Json.object("name" => self.name.toJson(), "rooms" => self.rooms.toJson(), "tags" => self.tags.toJson(), "annex" => self.annex.toJson())
|
|
12
12
|
|
|
@@ -17,9 +17,9 @@ record House(name: Str, rooms: Int, tags: List[Str], annex: List[House]) =
|
|
|
17
17
|
annex := try annexOf(try j.field("annex"))
|
|
18
18
|
return House(name: name, rooms: rooms, tags: tags, annex: annex)
|
|
19
19
|
|
|
20
|
-
fun tagsOf(j: Json) -> JsonError!
|
|
20
|
+
fun tagsOf(j: Json) -> JsonError!Slice[Str] = j.into()
|
|
21
21
|
|
|
22
|
-
fun annexOf(j: Json) -> JsonError!
|
|
22
|
+
fun annexOf(j: Json) -> JsonError!Slice[House] = j.into()
|
|
23
23
|
|
|
24
24
|
fun parseHouse(s: Str) -> JsonError!House = Json.decode(s)
|
|
25
25
|
|
|
@@ -32,10 +32,10 @@ fun house(a: Pair[Str, Json], b: Pair[Str, Json]) -> Str =
|
|
|
32
32
|
Json.object(a, b, "tags" => Json.array(), "annex" => Json.array()).toStr()
|
|
33
33
|
|
|
34
34
|
test "nested round trip"
|
|
35
|
-
tags :=
|
|
35
|
+
tags := Slice[Str]()
|
|
36
36
|
tags.push("old")
|
|
37
|
-
annex :=
|
|
37
|
+
annex := Slice[House]()
|
|
38
|
-
annex.push(House(name: "shed", rooms: 1, tags:
|
|
38
|
+
annex.push(House(name: "shed", rooms: 1, tags: Slice[Str](), annex: Slice[House]()))
|
|
39
39
|
h := House(name: "main", rooms: 5, tags: tags, annex: annex)
|
|
40
40
|
text := Json.encode(h)
|
|
41
41
|
back: JsonError!House := Json.decode(text)
|
plum-examples/loops.plum
CHANGED
|
@@ -3,25 +3,25 @@
|
|
|
3
3
|
import std/Str
|
|
4
4
|
import std/Int
|
|
5
5
|
import std/Float
|
|
6
|
-
import std/
|
|
6
|
+
import std/Slice
|
|
7
7
|
import std/Array
|
|
8
8
|
import std/Testing
|
|
9
9
|
import std/Seq
|
|
10
10
|
|
|
11
|
-
fun sumList(xs:
|
|
11
|
+
fun sumList(xs: Slice[Int]) -> Int =
|
|
12
12
|
total := 0
|
|
13
13
|
for x := range xs
|
|
14
14
|
total = total + x
|
|
15
15
|
return total
|
|
16
16
|
|
|
17
|
-
fun firstOver(xs:
|
|
17
|
+
fun firstOver(xs: Slice[Int], limit: Int) -> Int =
|
|
18
18
|
for x := range xs
|
|
19
19
|
if x > limit
|
|
20
20
|
return x
|
|
21
21
|
return -1
|
|
22
22
|
|
|
23
23
|
test "plain_iterable_for: List and Array"
|
|
24
|
-
xs :=
|
|
24
|
+
xs := Slice(1, 2, 3, 4)
|
|
25
25
|
assert sumList(xs) == 10
|
|
26
26
|
arr := Array.filled(3, 7)
|
|
27
27
|
total := 0
|
|
@@ -30,28 +30,28 @@ test "plain_iterable_for: List and Array"
|
|
|
30
30
|
assert total == 21
|
|
31
31
|
|
|
32
32
|
test "index_value_binders: i, x"
|
|
33
|
-
xs :=
|
|
33
|
+
xs := Slice(10, 20, 30)
|
|
34
34
|
acc := 0
|
|
35
35
|
for i, x := range xs
|
|
36
36
|
acc = acc + i * x
|
|
37
37
|
assert acc == 80
|
|
38
38
|
|
|
39
39
|
test "clause_if_keeps_when_true: if cond keeps the element, false skips it"
|
|
40
|
-
xs :=
|
|
40
|
+
xs := Slice(1, 2, 3, 4, 5, 6)
|
|
41
41
|
total := 0
|
|
42
42
|
for x := range xs, if x % 2 == 0
|
|
43
43
|
total = total + x
|
|
44
44
|
assert total == 12
|
|
45
45
|
|
|
46
46
|
test "clause_bind_scoped: name := expr is visible to later clauses and body"
|
|
47
|
-
xs :=
|
|
47
|
+
xs := Slice(1, 2, 3, 4)
|
|
48
48
|
total := 0
|
|
49
49
|
for x := range xs, sq := x * x, if sq > 9
|
|
50
50
|
total = total + sq
|
|
51
51
|
assert total == 16
|
|
52
52
|
|
|
53
53
|
test "break_continue_return_semantics"
|
|
54
|
-
xs :=
|
|
54
|
+
xs := Slice(1, 2, 3, 4, 5)
|
|
55
55
|
total := 0
|
|
56
56
|
for x := range xs
|
|
57
57
|
if x == 2
|
|
@@ -64,8 +64,8 @@ test "break_continue_return_semantics"
|
|
|
64
64
|
assert firstOver(xs, 9) == -1
|
|
65
65
|
|
|
66
66
|
test "nested iterable loops"
|
|
67
|
-
xs :=
|
|
67
|
+
xs := Slice(1, 2)
|
|
68
|
-
ys :=
|
|
68
|
+
ys := Slice(10, 20)
|
|
69
69
|
total := 0
|
|
70
70
|
for x := range xs
|
|
71
71
|
for y := range ys
|
plum-examples/optionals.plum
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import std/Int
|
|
2
2
|
import std/Str
|
|
3
|
-
import std/
|
|
3
|
+
import std/Slice
|
|
4
4
|
import std/Float
|
|
5
5
|
# Optionals: `T?` is a `T` or `null`. A plain `T` in a `T?` slot is the present
|
|
6
6
|
# value. An optional is handled with `?:` (Kotlin elvis), `if x := opt`,
|
|
@@ -51,13 +51,13 @@ fun viaTry(n: Int) -> error{Bad}!Int =
|
|
|
51
51
|
v := try fallible(n)
|
|
52
52
|
v ?: -1
|
|
53
53
|
|
|
54
|
-
fun nextItem(items:
|
|
54
|
+
fun nextItem(items: Slice[Int], i: Int) -> Int? =
|
|
55
55
|
if i < items.length()
|
|
56
56
|
return items.get(i)!
|
|
57
57
|
null
|
|
58
58
|
|
|
59
|
-
fun sparse() ->
|
|
59
|
+
fun sparse() -> Slice[Int?] =
|
|
60
|
-
|
|
60
|
+
Slice(1, null, 3)
|
|
61
61
|
|
|
62
62
|
fun sumBig() -> Int =
|
|
63
63
|
Int.Max + Int.Min
|
|
@@ -149,7 +149,7 @@ test "if_bind_optional"
|
|
|
149
149
|
assert b.label == "in"
|
|
150
150
|
|
|
151
151
|
test "while_bind_optional"
|
|
152
|
-
items :=
|
|
152
|
+
items := Slice(3, 2, 1)
|
|
153
153
|
total := 0
|
|
154
154
|
i := 0
|
|
155
155
|
while v := nextItem(items, i)
|
plum-examples/own_list.plum
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import std/Int
|
|
2
2
|
import std/Float
|
|
3
|
-
import std/
|
|
3
|
+
import std/Slice
|
|
4
4
|
|
|
5
5
|
own record Cell(x: Float)
|
|
6
6
|
|
|
7
7
|
test "map_in_place_updates_all"
|
|
8
|
-
xs :=
|
|
8
|
+
xs := Slice(Cell(x: 1.0), Cell(x: 2.0), Cell(x: 3.0))
|
|
9
9
|
ys := xs.mapInPlace(|c| Cell(..c, x: c.x + 1.0))
|
|
10
10
|
assert ys.length() == 3
|
|
11
11
|
assert {ys.at(0).x - 2.0}.abs() < 1e-6
|
|
@@ -13,7 +13,7 @@ test "map_in_place_updates_all"
|
|
|
13
13
|
assert {ys.at(2).x - 4.0}.abs() < 1e-6
|
|
14
14
|
|
|
15
15
|
test "take_then_put_round_trip"
|
|
16
|
-
xs :=
|
|
16
|
+
xs := Slice(Cell(x: 1.0), Cell(x: 2.0))
|
|
17
17
|
c := xs.takeAt(1)
|
|
18
18
|
d := Cell(..c, x: c.x + 10.0)
|
|
19
19
|
xs.putAt(1, d)
|
|
@@ -22,12 +22,12 @@ test "take_then_put_round_trip"
|
|
|
22
22
|
assert {xs.at(0).x - 1.0}.abs() < 1e-6
|
|
23
23
|
|
|
24
24
|
test "at_borrows"
|
|
25
|
-
xs :=
|
|
25
|
+
xs := Slice(Cell(x: 5.0))
|
|
26
26
|
assert {xs.at(0).x - 5.0}.abs() < 1e-6
|
|
27
27
|
assert {xs.at(0).x - 5.0}.abs() < 1e-6
|
|
28
28
|
|
|
29
29
|
test "put_after_take_keeps_length"
|
|
30
|
-
xs :=
|
|
30
|
+
xs := Slice(Cell(x: 1.0), Cell(x: 2.0), Cell(x: 3.0))
|
|
31
31
|
c := xs.takeAt(0)
|
|
32
32
|
xs.putAt(0, c)
|
|
33
33
|
assert xs.length() == 3
|
plum-examples/scene.plum
CHANGED
|
@@ -15,7 +15,7 @@ module scene
|
|
|
15
15
|
import std/Str
|
|
16
16
|
import std/Int
|
|
17
17
|
import std/Float
|
|
18
|
-
import std/
|
|
18
|
+
import std/Slice
|
|
19
19
|
import std/Math
|
|
20
20
|
import std/game/Vec2
|
|
21
21
|
import std/game/Color
|
|
@@ -91,9 +91,9 @@ record Walker(Actor, pace: Float, phase: Float, tex: Int, srcX: Float) =
|
|
|
91
91
|
# planet and moon, each planet followed by its moons), as are `halos` (the glow shown when selected).
|
|
92
92
|
record Sky(
|
|
93
93
|
stage: Stage,
|
|
94
|
-
bodies:
|
|
94
|
+
bodies: Slice[Actor],
|
|
95
|
-
halos:
|
|
95
|
+
halos: Slice[Actor],
|
|
96
|
-
names:
|
|
96
|
+
names: Slice[Str],
|
|
97
97
|
sun: Spinner,
|
|
98
98
|
label: Text,
|
|
99
99
|
crowd: Group,
|
|
@@ -131,14 +131,14 @@ fun newSky(sheet: Int) -> Sky =
|
|
|
131
131
|
label.x = 16.0
|
|
132
132
|
label.y = 12.0
|
|
133
133
|
sky := Sky(
|
|
134
|
-
stage: stage, bodies:
|
|
134
|
+
stage: stage, bodies: Slice[Actor](), halos: Slice[Actor](), names: Slice[Str](),
|
|
135
135
|
sun: sun, label: label, crowd: Group(), front: null, selected: -1, clock: 0.0, paused: false, map: -1)
|
|
136
136
|
# faint orbit rings, added first so every body draws (and is hit) above them
|
|
137
137
|
for i := range Layout.Planets
|
|
138
138
|
r := 90.0 + Float(i) * 60.0
|
|
139
139
|
stage.root.add(Circle.create(r, Color(r: 1.0, g: 1.0, b: 1.0, a: 0.04)).at(Layout.CenterX - r, Layout.CenterY - r))
|
|
140
|
-
planetNames :=
|
|
140
|
+
planetNames := Slice("Ember", "Cinder", "Ashen", "Glacier")
|
|
141
|
-
moons :=
|
|
141
|
+
moons := Slice(0, 1, 2, 1)
|
|
142
142
|
for i := range Layout.Planets
|
|
143
143
|
orbit := Orbit(speed: 0.9 / Float(1 + i)).at(Layout.CenterX, Layout.CenterY)
|
|
144
144
|
planet := addBody(sky, sheet, planetNames.at(i), 2, i, 40.0, 1.5).at(90.0 + Float(i) * 60.0, 0.0)
|
plum-examples/seq.plum
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# `skip`, `map`, `filter`, `find`, `fold`) to process collections in `for` loops.
|
|
3
3
|
import std/Int
|
|
4
4
|
import std/Float
|
|
5
|
-
import std/
|
|
5
|
+
import std/Slice
|
|
6
6
|
import std/Seq
|
|
7
7
|
import std/Map
|
|
8
8
|
import std/Str
|
|
@@ -11,7 +11,7 @@ import std/Str
|
|
|
11
11
|
# `for` source. Adapters (take, skip, stepBy, zip, reversed, chunks) reshape the
|
|
12
12
|
# sequence and may appear anywhere in a chain; terminals (filter, map, find, fold,
|
|
13
13
|
# count, any, all, each, firstN, toList) take the `for` body and end the chain.
|
|
14
|
-
# A whole chain is ONE loop: no closure, no intermediate
|
|
14
|
+
# A whole chain is ONE loop: no closure, no intermediate Slice.
|
|
15
15
|
|
|
16
16
|
# Closure-free chains. A method chain such as
|
|
17
17
|
#
|
|
@@ -37,8 +37,8 @@ fun dist2(ax: Float, ay: Float, bx: Float, by: Float) -> Float =
|
|
|
37
37
|
dy := ay - by
|
|
38
38
|
dx * dx + dy * dy
|
|
39
39
|
|
|
40
|
-
fun army() ->
|
|
40
|
+
fun army() -> Slice[Soldier] =
|
|
41
|
-
|
|
41
|
+
Slice(
|
|
42
42
|
Soldier(name: "a", team: 1, hp: 10.0, x: 0.0, y: 0.0),
|
|
43
43
|
Soldier(name: "b", team: 2, hp: 0.0, x: 1.0, y: 1.0),
|
|
44
44
|
Soldier(name: "c", team: 1, hp: 7.0, x: 5.0, y: 5.0),
|
|
@@ -48,70 +48,70 @@ fun army() -> List[Soldier] =
|
|
|
48
48
|
)
|
|
49
49
|
|
|
50
50
|
test "seq_method_filter: a List has no filter of its own, so Seq.filter is used"
|
|
51
|
-
xs :=
|
|
51
|
+
xs := Slice(1, 2, 3, 4, 5, 6)
|
|
52
52
|
evens := for x := range xs.filter()
|
|
53
53
|
x % 2 == 0
|
|
54
54
|
assert evens.length() == 3
|
|
55
55
|
assert evens.at(2) == 6
|
|
56
56
|
|
|
57
57
|
test "adapter take"
|
|
58
|
-
xs :=
|
|
58
|
+
xs := Slice(5, 6, 7, 8)
|
|
59
59
|
got := for x := range xs.take(2).map()
|
|
60
60
|
x * 10
|
|
61
61
|
assert got.length() == 2
|
|
62
62
|
assert got.at(1) == 60
|
|
63
63
|
|
|
64
64
|
test "adapter skip"
|
|
65
|
-
xs :=
|
|
65
|
+
xs := Slice(5, 6, 7, 8)
|
|
66
66
|
n := 0
|
|
67
67
|
for x := range xs.skip(2)
|
|
68
68
|
n = n * 10 + x
|
|
69
69
|
assert n == 78
|
|
70
70
|
|
|
71
71
|
test "adapter stepBy"
|
|
72
|
-
xs :=
|
|
72
|
+
xs := Slice(1, 2, 3, 4, 5, 6, 7)
|
|
73
73
|
n := for x := range xs.stepBy(3).count()
|
|
74
74
|
true
|
|
75
75
|
assert n == 3
|
|
76
76
|
|
|
77
77
|
test "adapter zip"
|
|
78
|
-
xs :=
|
|
78
|
+
xs := Slice(1, 2, 3)
|
|
79
|
-
ys :=
|
|
79
|
+
ys := Slice(10, 20, 30, 40)
|
|
80
80
|
dot := 0
|
|
81
81
|
for a, b := range xs.zip(ys)
|
|
82
82
|
dot = dot + a * b
|
|
83
83
|
assert dot == 140
|
|
84
84
|
|
|
85
85
|
test "adapter reversed"
|
|
86
|
-
xs :=
|
|
86
|
+
xs := Slice(1, 2, 3)
|
|
87
87
|
n := 0
|
|
88
88
|
for x := range xs.reversed()
|
|
89
89
|
n = n * 10 + x
|
|
90
90
|
assert n == 321
|
|
91
91
|
|
|
92
92
|
test "adapter chunks"
|
|
93
|
-
xs :=
|
|
93
|
+
xs := Slice(1, 2, 3, 4, 5)
|
|
94
94
|
sizes := for c := range xs.chunks(2).map()
|
|
95
95
|
c.length()
|
|
96
96
|
assert sizes.length() == 3
|
|
97
97
|
assert sizes.at(2) == 1
|
|
98
98
|
|
|
99
99
|
test "seq_chain_take_filter_fused: take then filter"
|
|
100
|
-
xs :=
|
|
100
|
+
xs := Slice(1, 2, 3, 4, 5, 6, 7, 8)
|
|
101
101
|
got := for x := range xs.take(6).filter(), if x > 1
|
|
102
102
|
x % 2 == 0
|
|
103
103
|
assert got.length() == 3
|
|
104
104
|
assert got.at(0) == 2
|
|
105
105
|
|
|
106
106
|
test "terminal map with clauses"
|
|
107
|
-
xs :=
|
|
107
|
+
xs := Slice(1, 2, 3, 4)
|
|
108
108
|
got := for x := range xs.map(), if x % 2 == 1, sq := x * x
|
|
109
109
|
sq + 1
|
|
110
110
|
assert got.length() == 2
|
|
111
111
|
assert got.at(1) == 10
|
|
112
112
|
|
|
113
113
|
test "terminal find"
|
|
114
|
-
xs :=
|
|
114
|
+
xs := Slice(3, 8, 12)
|
|
115
115
|
big := for x := range xs.find()
|
|
116
116
|
x > 5
|
|
117
117
|
b := big ?: 0
|
|
@@ -122,13 +122,13 @@ test "terminal find"
|
|
|
122
122
|
assert c == -1
|
|
123
123
|
|
|
124
124
|
test "terminal fold"
|
|
125
|
-
xs :=
|
|
125
|
+
xs := Slice(1, 2, 3, 4)
|
|
126
126
|
sum := for acc, x := range xs.fold(100)
|
|
127
127
|
acc + x
|
|
128
128
|
assert sum == 110
|
|
129
129
|
|
|
130
130
|
test "terminals count any all each"
|
|
131
|
-
xs :=
|
|
131
|
+
xs := Slice(1, 2, 3, 4)
|
|
132
132
|
n := for x := range xs.count()
|
|
133
133
|
x > 1
|
|
134
134
|
assert n == 3
|
|
@@ -144,7 +144,7 @@ test "terminals count any all each"
|
|
|
144
144
|
assert seen == 10
|
|
145
145
|
|
|
146
146
|
test "firstN_predicate: the first n elements the body accepts"
|
|
147
|
-
xs :=
|
|
147
|
+
xs := Slice(1, 2, 3, 4, 5, 6, 7, 8)
|
|
148
148
|
got := for x := range xs.firstN(3)
|
|
149
149
|
x % 2 == 0
|
|
150
150
|
assert got.length() == 3
|
|
@@ -154,7 +154,7 @@ test "firstN_predicate: the first n elements the body accepts"
|
|
|
154
154
|
assert few.length() == 2
|
|
155
155
|
|
|
156
156
|
test "toList_no_body: a chain ends in a List without a for"
|
|
157
|
-
xs :=
|
|
157
|
+
xs := Slice(1, 2, 3, 4, 5)
|
|
158
158
|
ys := xs.skip(1).take(3).toList()
|
|
159
159
|
assert ys.length() == 3
|
|
160
160
|
assert ys.at(0) == 2
|
plum-examples/strings.plum
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import std/Int
|
|
4
4
|
import std/Float
|
|
5
5
|
import std/Str
|
|
6
|
-
import std/
|
|
6
|
+
import std/Slice
|
|
7
7
|
|
|
8
8
|
fun greet(name: Str) -> Str =
|
|
9
9
|
"Hello, {name}!"
|
|
@@ -57,7 +57,7 @@ test "string interpolation supports a boolean expression, not just a bare variab
|
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
test "string interpolation supports a method call taking a closure literal"
|
|
60
|
-
l :=
|
|
60
|
+
l := Slice(1, 2, 3, 4)
|
|
61
61
|
assert "{l.findValue(3)!}" == "3"
|
|
62
62
|
assert "{l.contains(100)}" == "false"
|
|
63
63
|
|
plum-examples/struct_arrays.plum
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Arrays of structs are stored field by field (struct-of-arrays): `Array[Vec2]`
|
|
2
2
|
# is one object holding an `f64` array for `x` and one for `y`, so `get`/`set`
|
|
3
|
-
# allocate nothing per element. `
|
|
3
|
+
# allocate nothing per element. `Slice`, `Grid` and friends get this for free.
|
|
4
4
|
module struct_arrays
|
|
5
5
|
import std/Str
|
|
6
6
|
import std/Int
|
|
7
7
|
import std/Float
|
|
8
8
|
import std/Array
|
|
9
|
-
import std/
|
|
9
|
+
import std/Slice
|
|
10
10
|
import std/Grid
|
|
11
11
|
|
|
12
12
|
struct Vec2(x: Float, y: Float)
|
|
@@ -18,7 +18,7 @@ struct Rect(pos: Vec2, size: Vec2) =
|
|
|
18
18
|
|
|
19
19
|
struct Tile(id: Int, solid: Bool, weight: Float)
|
|
20
20
|
|
|
21
|
-
record World(points:
|
|
21
|
+
record World(points: Slice[Vec2], name: Str)
|
|
22
22
|
|
|
23
23
|
struct One(n: Int)
|
|
24
24
|
struct Flags(a: Bool, b: Bool, c: Int)
|
|
@@ -41,7 +41,7 @@ fun mkPx(v: Int) -> Px =
|
|
|
41
41
|
Px(r: Byte(v), g: Byte(v + 1), b: Byte(v + 2), a: Byte(255))
|
|
42
42
|
|
|
43
43
|
test "List[Vec2] push/get/set round-trips across growth"
|
|
44
|
-
l :=
|
|
44
|
+
l := Slice[Vec2]()
|
|
45
45
|
for i := range 100
|
|
46
46
|
l.push(Vec2(x: Float(i), y: Float(i) * 2.0))
|
|
47
47
|
assert l.length() == 100
|
|
@@ -86,7 +86,7 @@ test "struct with Int, Bool and Float leaves"
|
|
|
86
86
|
assert !b.get(3).solid
|
|
87
87
|
|
|
88
88
|
test "copy semantics: a value read from the array is a copy until set"
|
|
89
|
-
l :=
|
|
89
|
+
l := Slice[Vec2]()
|
|
90
90
|
l.push(Vec2(x: 1.0, y: 1.0))
|
|
91
91
|
v := l.get(0)!
|
|
92
92
|
v = Vec2(x: 99.0, y: 99.0)
|
|
@@ -121,7 +121,7 @@ test "Grid[Tile] get/set"
|
|
|
121
121
|
assert g.get(1, 1).weight == 0.5
|
|
122
122
|
|
|
123
123
|
test "a List[Vec2] inside a record field"
|
|
124
|
-
w := World(points:
|
|
124
|
+
w := World(points: Slice[Vec2](), name: "w")
|
|
125
125
|
w.points.push(Vec2(x: 1.0, y: 2.0))
|
|
126
126
|
w.points.push(Vec2(x: 3.0, y: 4.0))
|
|
127
127
|
assert w.points.length() == 2
|
|
@@ -171,7 +171,7 @@ test "probe generic first, equality"
|
|
|
171
171
|
test "probe nullable and list and error"
|
|
172
172
|
n: Array[Vec2]? := Array[Vec2]().push(Vec2(x: 3.0, y: 3.0))
|
|
173
173
|
assert n!.get(0).x == 3.0
|
|
174
|
-
l :=
|
|
174
|
+
l := Slice[Array[Vec2]]()
|
|
175
175
|
l.push(Array[Vec2]().push(Vec2(x: 4.0, y: 4.0)))
|
|
176
176
|
assert l.get(0)!.get(0).x == 4.0
|
|
177
177
|
r := mk()!
|
|
@@ -203,7 +203,7 @@ test "Byte struct fields: copy, compare, store"
|
|
|
203
203
|
a := Array[Px]().push(p).push(mkPx(20))
|
|
204
204
|
assert a.get(1).g == Byte(21)
|
|
205
205
|
assert a.get(0) == q
|
|
206
|
-
l :=
|
|
206
|
+
l := Slice[Px]()
|
|
207
207
|
for i := range 50
|
|
208
208
|
l.push(mkPx(i))
|
|
209
209
|
assert l.get(37)!.b == Byte(39)
|
plum-examples/struct_union_members.plum
CHANGED
|
@@ -6,7 +6,7 @@ module struct_union_members
|
|
|
6
6
|
import std/Str
|
|
7
7
|
import std/Int
|
|
8
8
|
import std/Float
|
|
9
|
-
import std/
|
|
9
|
+
import std/Slice
|
|
10
10
|
|
|
11
11
|
enum Step = Move | Wait | Chain
|
|
12
12
|
|
|
@@ -14,7 +14,7 @@ struct Move(dx: Float, left: Float)
|
|
|
14
14
|
|
|
15
15
|
struct Wait(left: Float)
|
|
16
16
|
|
|
17
|
-
record Chain(steps:
|
|
17
|
+
record Chain(steps: Slice[Step])
|
|
18
18
|
|
|
19
19
|
trait Thing(x: Float, tag: Int) =
|
|
20
20
|
fun clone(self) -> Thing
|
|
@@ -39,7 +39,7 @@ fun copyOut(s: Step) -> Move =
|
|
|
39
39
|
_ => Move(dx: 0.0, left: 0.0)
|
|
40
40
|
|
|
41
41
|
test "struct_member_through_list"
|
|
42
|
-
steps:
|
|
42
|
+
steps: Slice[Step] := Slice[Step]()
|
|
43
43
|
steps.push(Move(1.0, 2.0))
|
|
44
44
|
steps.push(Wait(3.0))
|
|
45
45
|
c := Chain(steps: steps)
|
|
@@ -53,7 +53,7 @@ test "struct_member_spread_copy"
|
|
|
53
53
|
assert m.left == 2.0
|
|
54
54
|
|
|
55
55
|
test "struct_member_match_copy_is_independent"
|
|
56
|
-
steps:
|
|
56
|
+
steps: Slice[Step] := Slice[Step]()
|
|
57
57
|
steps.push(Move(dx: 1.0, left: 2.0))
|
|
58
58
|
mv := copyOut(steps.at(0))
|
|
59
59
|
mv.left = 9.0
|
plum-examples/structs_records.plum
CHANGED
|
@@ -118,7 +118,7 @@ record Slab(bounds: Rect, id: Int)
|
|
|
118
118
|
record Bag(n: Int) =
|
|
119
119
|
fun init(self) =
|
|
120
120
|
self.label = "bag"
|
|
121
|
-
self.items:
|
|
121
|
+
self.items: Slice[Int] = Slice[Int]()
|
|
122
122
|
self.cap: Int = self.n * 2
|
|
123
123
|
|
|
124
124
|
# ---- embedding records and override ----
|
plum-examples/tail_calls.plum
CHANGED
|
@@ -6,27 +6,27 @@
|
|
|
6
6
|
module tail_calls
|
|
7
7
|
import std/Int
|
|
8
8
|
import std/Float
|
|
9
|
+
import std/List
|
|
9
10
|
|
|
10
|
-
enum Link[T] = Nil | Cons(head: T, tail: Link[T])
|
|
11
11
|
|
|
12
12
|
fun sumTo(n: Int, acc: Int) -> Int =
|
|
13
13
|
if n == 0 then acc else sumTo(n - 1, acc + n)
|
|
14
14
|
|
|
15
|
-
fun build(n: Int, acc:
|
|
15
|
+
fun build(n: Int, acc: List[Int]) -> List[Int] =
|
|
16
16
|
if n == 0 then acc else build(n - 1, Cons(head: n, tail: acc))
|
|
17
17
|
|
|
18
|
-
fun fromTo(a: Int, b: Int) ->
|
|
18
|
+
fun fromTo(a: Int, b: Int) -> List[Int] =
|
|
19
19
|
if a >= b then Nil else Cons(head: a, tail: fromTo(a + 1, b))
|
|
20
20
|
|
|
21
|
-
fun map(xs:
|
|
21
|
+
fun map(xs: List[A], f: fn(A) -> B) -> List[B] = match xs
|
|
22
22
|
Nil => Nil
|
|
23
23
|
Cons(h, t) => Cons(head: f(h), tail: map(t, f))
|
|
24
24
|
|
|
25
|
-
fun foldl(xs:
|
|
25
|
+
fun foldl(xs: List[T], acc: A, f: fn(A, T) -> A) -> A = match xs
|
|
26
26
|
Nil => acc
|
|
27
27
|
Cons(h, t) => foldl(t, f(acc, h), f)
|
|
28
28
|
|
|
29
|
-
fun length(xs:
|
|
29
|
+
fun length(xs: List[T], acc: Int) -> Int = match xs
|
|
30
30
|
Nil => acc
|
|
31
31
|
Cons(_, t) => length(t, acc + 1)
|
|
32
32
|
|
plum-examples/trait_scene.plum
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
# A tiny pure scene: a `Scene` holds `
|
|
1
|
+
# A tiny pure scene: a `Scene` holds `Slice[Entity]`; the engine side knows only
|
|
2
2
|
# the trait, the records fill it in. No game host needed.
|
|
3
3
|
module trait_scene
|
|
4
4
|
import std/Str
|
|
5
5
|
import std/Int
|
|
6
|
-
import std/
|
|
6
|
+
import std/Slice
|
|
7
7
|
|
|
8
8
|
trait Entity(x: Int, y: Int) =
|
|
9
9
|
fun update(self, dt: Int)
|
|
@@ -20,7 +20,7 @@ record Walker(Entity(x, y), speed: Int) =
|
|
|
20
20
|
self.x = self.x + self.speed * dt
|
|
21
21
|
override fun draw(self) -> Int = self.x
|
|
22
22
|
|
|
23
|
-
record Scene(entities:
|
|
23
|
+
record Scene(entities: Slice[Entity]) =
|
|
24
24
|
fun update(self, dt: Int) =
|
|
25
25
|
for e := range self.entities
|
|
26
26
|
e.update(dt)
|
|
@@ -31,7 +31,7 @@ record Scene(entities: List[Entity]) =
|
|
|
31
31
|
total
|
|
32
32
|
|
|
33
33
|
fun makeScene() -> Scene =
|
|
34
|
-
es:
|
|
34
|
+
es: Slice[Entity] := Slice[Entity]()
|
|
35
35
|
es.push(House(x: 0, y: 0, floors: 2))
|
|
36
36
|
es.push(Walker(x: 3, y: 1, speed: 2))
|
|
37
37
|
Scene(entities: es)
|
plum-examples/traits.plum
CHANGED
|
@@ -8,7 +8,7 @@ module traits
|
|
|
8
8
|
import std/Str
|
|
9
9
|
import std/Int
|
|
10
10
|
import std/Float
|
|
11
|
-
import std/
|
|
11
|
+
import std/Slice
|
|
12
12
|
|
|
13
13
|
struct Vec2(x: Int, y: Int) =
|
|
14
14
|
fun add(self, o: Vec2) -> Vec2 = Vec2(x: self.x + o.x, y: self.y + o.y)
|
|
@@ -62,7 +62,7 @@ test "fields_through_trait"
|
|
|
62
62
|
assert h.z == 4
|
|
63
63
|
|
|
64
64
|
test "list_of_entities_dispatch"
|
|
65
|
-
es:
|
|
65
|
+
es: Slice[Entity] := Slice[Entity]()
|
|
66
66
|
es.push(house())
|
|
67
67
|
es.push(tree())
|
|
68
68
|
assert es.at(0).update(2) == 6
|
plum-examples/unboxed_variants.plum
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# Probes for unboxed variants (union-enum members and inline variants are
|
|
2
|
+
# subtypes of the enum): deep nesting, Json round trips, toStr, a million-long
|
|
3
|
+
# List of unions, member mutation, reassignment across members, own record
|
|
4
|
+
# members, and enum `init` versus variant-name resolution.
|
|
5
|
+
module unboxed_variants
|
|
6
|
+
import std/Str
|
|
7
|
+
import std/Int
|
|
8
|
+
import std/Float
|
|
9
|
+
import std/Json
|
|
10
|
+
import std/Slice
|
|
11
|
+
import std/List
|
|
12
|
+
|
|
13
|
+
# Six levels of unions, each wrapping the one below.
|
|
14
|
+
record Leaf(v: Int) =
|
|
15
|
+
fun toJson(self) -> Json = Json.object("v" => self.v.toJson())
|
|
16
|
+
|
|
17
|
+
fun fromJson(j: Json) -> JsonError!Leaf =
|
|
18
|
+
return Leaf(v: try Int.fromJson(try j.field("v")))
|
|
19
|
+
|
|
20
|
+
fun toStr(self) -> Str = "L{self.v}"
|
|
21
|
+
|
|
22
|
+
record Stub(n: Int) =
|
|
23
|
+
fun toJson(self) -> Json = Json.object("n" => self.n.toJson())
|
|
24
|
+
|
|
25
|
+
fun fromJson(j: Json) -> JsonError!Stub =
|
|
26
|
+
return Stub(n: try Int.fromJson(try j.field("n")))
|
|
27
|
+
|
|
28
|
+
fun toStr(self) -> Str = "S{self.n}"
|
|
29
|
+
|
|
30
|
+
enum U0 = Leaf | Stub
|
|
31
|
+
|
|
32
|
+
record W1(in: U0) =
|
|
33
|
+
fun toJson(self) -> Json = Json.object("in" => self.in.toJson())
|
|
34
|
+
|
|
35
|
+
fun fromJson(j: Json) -> JsonError!W1 =
|
|
36
|
+
return W1(in: try u0Of(try j.field("in")))
|
|
37
|
+
|
|
38
|
+
fun toStr(self) -> Str = "W1({self.in})"
|
|
39
|
+
|
|
40
|
+
record Z1(k: Int) =
|
|
41
|
+
fun toJson(self) -> Json = Json.object("k" => self.k.toJson())
|
|
42
|
+
|
|
43
|
+
fun fromJson(j: Json) -> JsonError!Z1 =
|
|
44
|
+
return Z1(k: try Int.fromJson(try j.field("k")))
|
|
45
|
+
|
|
46
|
+
fun toStr(self) -> Str = "Z1"
|
|
47
|
+
|
|
48
|
+
enum U1 = W1 | Z1
|
|
49
|
+
|
|
50
|
+
record W2(in: U1) =
|
|
51
|
+
fun toJson(self) -> Json = Json.object("in" => self.in.toJson())
|
|
52
|
+
|
|
53
|
+
fun fromJson(j: Json) -> JsonError!W2 =
|
|
54
|
+
return W2(in: try u1Of(try j.field("in")))
|
|
55
|
+
|
|
56
|
+
fun toStr(self) -> Str = "W2({self.in})"
|
|
57
|
+
|
|
58
|
+
record Z2(k: Int) =
|
|
59
|
+
fun toJson(self) -> Json = Json.object("k" => self.k.toJson())
|
|
60
|
+
|
|
61
|
+
fun toStr(self) -> Str = "Z2"
|
|
62
|
+
|
|
63
|
+
enum U2 = W2 | Z2
|
|
64
|
+
|
|
65
|
+
record W3(in: U2) =
|
|
66
|
+
fun toJson(self) -> Json = Json.object("in" => self.in.toJson())
|
|
67
|
+
|
|
68
|
+
fun fromJson(j: Json) -> JsonError!W3 =
|
|
69
|
+
return W3(in: try u2Of(try j.field("in")))
|
|
70
|
+
|
|
71
|
+
fun toStr(self) -> Str = "W3({self.in})"
|
|
72
|
+
|
|
73
|
+
record Z3(k: Int) =
|
|
74
|
+
fun toJson(self) -> Json = Json.object("k" => self.k.toJson())
|
|
75
|
+
|
|
76
|
+
fun toStr(self) -> Str = "Z3"
|
|
77
|
+
|
|
78
|
+
enum U3 = W3 | Z3
|
|
79
|
+
|
|
80
|
+
record W4(in: U3) =
|
|
81
|
+
fun toJson(self) -> Json = Json.object("in" => self.in.toJson())
|
|
82
|
+
|
|
83
|
+
fun fromJson(j: Json) -> JsonError!W4 =
|
|
84
|
+
return W4(in: try u3Of(try j.field("in")))
|
|
85
|
+
|
|
86
|
+
fun toStr(self) -> Str = "W4({self.in})"
|
|
87
|
+
|
|
88
|
+
record Z4(k: Int) =
|
|
89
|
+
fun toJson(self) -> Json = Json.object("k" => self.k.toJson())
|
|
90
|
+
|
|
91
|
+
fun toStr(self) -> Str = "Z4"
|
|
92
|
+
|
|
93
|
+
enum U4 = W4 | Z4
|
|
94
|
+
|
|
95
|
+
record W5(in: U4) =
|
|
96
|
+
fun toJson(self) -> Json = Json.object("in" => self.in.toJson())
|
|
97
|
+
|
|
98
|
+
fun fromJson(j: Json) -> JsonError!W5 =
|
|
99
|
+
return W5(in: try u4Of(try j.field("in")))
|
|
100
|
+
|
|
101
|
+
fun toStr(self) -> Str = "W5({self.in})"
|
|
102
|
+
|
|
103
|
+
record Z5(k: Int) =
|
|
104
|
+
fun toJson(self) -> Json = Json.object("k" => self.k.toJson())
|
|
105
|
+
|
|
106
|
+
fun toStr(self) -> Str = "Z5"
|
|
107
|
+
|
|
108
|
+
enum U5 = W5 | Z5
|
|
109
|
+
|
|
110
|
+
# Decoders: the union itself cannot be decoded, so decode the member that was
|
|
111
|
+
# encoded (every level here is a W, whose `in` is the only field).
|
|
112
|
+
fun u0Of(j: Json) -> JsonError!U0 =
|
|
113
|
+
l: JsonError!Leaf := j.into()
|
|
114
|
+
return l!
|
|
115
|
+
|
|
116
|
+
fun u1Of(j: Json) -> JsonError!U1 =
|
|
117
|
+
w: JsonError!W1 := j.into()
|
|
118
|
+
return w!
|
|
119
|
+
|
|
120
|
+
fun u2Of(j: Json) -> JsonError!U2 =
|
|
121
|
+
w: JsonError!W2 := j.into()
|
|
122
|
+
return w!
|
|
123
|
+
|
|
124
|
+
fun u3Of(j: Json) -> JsonError!U3 =
|
|
125
|
+
w: JsonError!W3 := j.into()
|
|
126
|
+
return w!
|
|
127
|
+
|
|
128
|
+
fun u4Of(j: Json) -> JsonError!U4 =
|
|
129
|
+
w: JsonError!W4 := j.into()
|
|
130
|
+
return w!
|
|
131
|
+
|
|
132
|
+
fun deepLeaf(v: Int) -> U5 =
|
|
133
|
+
l0: U0 := Leaf(v: v)
|
|
134
|
+
l1: U1 := W1(in: l0)
|
|
135
|
+
l2: U2 := W2(in: l1)
|
|
136
|
+
l3: U3 := W3(in: l2)
|
|
137
|
+
l4: U4 := W4(in: l3)
|
|
138
|
+
W5(in: l4)
|
|
139
|
+
|
|
140
|
+
fun deepStub(n: Int) -> U5 =
|
|
141
|
+
l0: U0 := Stub(n: n)
|
|
142
|
+
l1: U1 := W1(in: l0)
|
|
143
|
+
l2: U2 := W2(in: l1)
|
|
144
|
+
l3: U3 := W3(in: l2)
|
|
145
|
+
l4: U4 := W4(in: l3)
|
|
146
|
+
W5(in: l4)
|
|
147
|
+
|
|
148
|
+
fun dig(u: U5) -> Int =
|
|
149
|
+
match u
|
|
150
|
+
W5(a) =>
|
|
151
|
+
match a
|
|
152
|
+
W4(b) =>
|
|
153
|
+
match b
|
|
154
|
+
W3(c) =>
|
|
155
|
+
match c
|
|
156
|
+
W2(d) =>
|
|
157
|
+
match d
|
|
158
|
+
W1(e) =>
|
|
159
|
+
match e
|
|
160
|
+
Leaf(v) => v
|
|
161
|
+
Stub(n) => -n
|
|
162
|
+
Z1(k) => 1000 + k
|
|
163
|
+
Z2(k) => 2000 + k
|
|
164
|
+
Z3(k) => 3000 + k
|
|
165
|
+
Z4(k) => 4000 + k
|
|
166
|
+
Z5(k) => 5000 + k
|
|
167
|
+
|
|
168
|
+
fun roundTrip(v: Int) -> Int =
|
|
169
|
+
u := deepLeaf(v)
|
|
170
|
+
text := Json.encode(u)
|
|
171
|
+
back: JsonError!W5 := Json.decode(text)
|
|
172
|
+
if Json.encode(back!) == text then 1 else 0
|
|
173
|
+
|
|
174
|
+
fun leaves(n: Int) -> List[Int] =
|
|
175
|
+
if n == 0 then Nil else Cons(head: n, tail: leaves(n - 1))
|
|
176
|
+
|
|
177
|
+
# State.
|
|
178
|
+
record Circle(r: Float)
|
|
179
|
+
|
|
180
|
+
record Rect(w: Float, h: Float)
|
|
181
|
+
|
|
182
|
+
enum Shape = Circle | Rect
|
|
183
|
+
|
|
184
|
+
struct Disc(r: Float)
|
|
185
|
+
|
|
186
|
+
enum Round = Disc | Rect
|
|
187
|
+
|
|
188
|
+
fun roundArea(m: Round) -> Float =
|
|
189
|
+
match m
|
|
190
|
+
Disc(r) => r * r * 3.0
|
|
191
|
+
Rect(w, h) => w * h
|
|
192
|
+
|
|
193
|
+
fun area(s: Shape) -> Float =
|
|
194
|
+
match s
|
|
195
|
+
Circle(r) => r * r * 3.0
|
|
196
|
+
Rect(w, h) => w * h
|
|
197
|
+
|
|
198
|
+
fun kind(s: Shape) -> Int =
|
|
199
|
+
match s
|
|
200
|
+
Circle(_) => 1
|
|
201
|
+
Rect(_, _) => 10
|
|
202
|
+
|
|
203
|
+
own record Token(id: Int)
|
|
204
|
+
|
|
205
|
+
own record Stamp(id: Int, note: Str)
|
|
206
|
+
|
|
207
|
+
enum Mark = Token | Stamp
|
|
208
|
+
|
|
209
|
+
fun takeMark(own m: Mark) -> Mark = m
|
|
210
|
+
|
|
211
|
+
fun markId(m: Mark) -> Int =
|
|
212
|
+
match m
|
|
213
|
+
Token(id) => id
|
|
214
|
+
Stamp(id, _) => id + 1000
|
|
215
|
+
|
|
216
|
+
# Enum `init` versus variant names.
|
|
217
|
+
enum Cell =
|
|
218
|
+
| Empty
|
|
219
|
+
| Full(count: Int, label: Str)
|
|
220
|
+
|
|
221
|
+
fun init(n: Int) -> Cell = if n <= 0 then Empty else Full(count: n, label: "n{n}")
|
|
222
|
+
|
|
223
|
+
fun size(self) -> Int =
|
|
224
|
+
match self
|
|
225
|
+
Empty => 0
|
|
226
|
+
Full(c, _) => c
|
|
227
|
+
|
|
228
|
+
fun describe(c: Cell) -> Str =
|
|
229
|
+
match c
|
|
230
|
+
Full(n, l) => "{n}{l}"
|
|
231
|
+
Empty => "empty"
|
|
232
|
+
|
|
233
|
+
test "deep_nested_match"
|
|
234
|
+
assert dig(deepLeaf(42)) == 42
|
|
235
|
+
assert dig(deepStub(7)) == -7
|
|
236
|
+
assert dig(Z5(k: 1)) == 5001
|
|
237
|
+
z4: U4 := Z4(k: 2)
|
|
238
|
+
assert dig(W5(in: z4)) == 4002
|
|
239
|
+
z3: U3 := Z3(k: 3)
|
|
240
|
+
w4: U4 := W4(in: z3)
|
|
241
|
+
assert dig(W5(in: w4)) == 3003
|
|
242
|
+
z2: U2 := Z2(k: 4)
|
|
243
|
+
w3: U3 := W3(in: z2)
|
|
244
|
+
w4b: U4 := W4(in: w3)
|
|
245
|
+
assert dig(W5(in: w4b)) == 2004
|
|
246
|
+
z1: U1 := Z1(k: 5)
|
|
247
|
+
w2: U2 := W2(in: z1)
|
|
248
|
+
w3b: U3 := W3(in: w2)
|
|
249
|
+
w4c: U4 := W4(in: w3b)
|
|
250
|
+
assert dig(W5(in: w4c)) == 1005
|
|
251
|
+
|
|
252
|
+
test "deep_toStr"
|
|
253
|
+
assert deepLeaf(9).toStr() == "W5(W4(W3(W2(W1(L9)))))"
|
|
254
|
+
assert "{deepStub(3)}" == "W5(W4(W3(W2(W1(S3)))))"
|
|
255
|
+
|
|
256
|
+
test "deep_json_round_trip"
|
|
257
|
+
u := deepLeaf(5)
|
|
258
|
+
text := Json.encode(u)
|
|
259
|
+
assert text == "{{\"in\":{{\"in\":{{\"in\":{{\"in\":{{\"in\":{{\"v\":5.0}}}}}}}}}}}}"
|
|
260
|
+
back: JsonError!W5 := Json.decode(text)
|
|
261
|
+
assert back!.toStr() == "W5(W4(W3(W2(W1(L5)))))"
|
|
262
|
+
assert Json.encode(back!) == text
|
|
263
|
+
assert roundTrip(77) == 1
|
|
264
|
+
|
|
265
|
+
# A list of union values, a million long, mapped and folded in constant stack.
|
|
266
|
+
test "deep_list_of_unions_million"
|
|
267
|
+
xs := leaves(1000000).map(|n| deepLeaf(n))
|
|
268
|
+
assert xs.length() == 1000000
|
|
269
|
+
total := xs.fold(0, |acc, u| acc + dig(u))
|
|
270
|
+
assert total == 500000500000
|
|
271
|
+
small := List(deepLeaf(1), deepStub(2)).map(|u| dig(u))
|
|
272
|
+
assert small.toStr() == "[1,-2]"
|
|
273
|
+
|
|
274
|
+
test "member_field_mutation_visible"
|
|
275
|
+
c := Circle(r: 1.0)
|
|
276
|
+
c.r = 4.0
|
|
277
|
+
s: Shape := c
|
|
278
|
+
assert area(s) == 48.0
|
|
279
|
+
r := Rect(w: 2.0, h: 3.0)
|
|
280
|
+
s2: Shape := r
|
|
281
|
+
r.w = 10.0
|
|
282
|
+
assert area(s2) == 30.0
|
|
283
|
+
d := Disc(r: 1.0)
|
|
284
|
+
m: Round := d
|
|
285
|
+
d.r = 9.0
|
|
286
|
+
assert roundArea(m) == 3.0
|
|
287
|
+
assert d.r == 9.0
|
|
288
|
+
r.w = 1.0
|
|
289
|
+
assert area(s2) == 3.0
|
|
290
|
+
|
|
291
|
+
test "member_field_assigned_after_narrowing"
|
|
292
|
+
s: Shape := Rect(w: 2.0, h: 3.0)
|
|
293
|
+
if Rect := s
|
|
294
|
+
s.w = 7.0
|
|
295
|
+
assert area(s) == 21.0
|
|
296
|
+
t: Shape := Circle(r: 1.0)
|
|
297
|
+
if Circle := t
|
|
298
|
+
t.r = 2.0
|
|
299
|
+
assert area(t) == 12.0
|
|
300
|
+
|
|
301
|
+
test "reassign_union_local_across_members"
|
|
302
|
+
s: Shape := Circle(r: 1.0)
|
|
303
|
+
assert area(s) == 3.0
|
|
304
|
+
s = Rect(w: 2.0, h: 5.0)
|
|
305
|
+
assert area(s) == 10.0
|
|
306
|
+
s = Circle(r: 2.0)
|
|
307
|
+
assert area(s) == 12.0
|
|
308
|
+
n := 0
|
|
309
|
+
for i := range 4
|
|
310
|
+
if i % 2 == 0
|
|
311
|
+
s = Rect(w: 1.0, h: 1.0)
|
|
312
|
+
else
|
|
313
|
+
s = Circle(r: 1.0)
|
|
314
|
+
n = n + kind(s)
|
|
315
|
+
assert n == 22
|
|
316
|
+
|
|
317
|
+
test "own_record_member_moves"
|
|
318
|
+
t := Token(id: 3)
|
|
319
|
+
m: Mark := takeMark(t)
|
|
320
|
+
assert markId(m) == 3
|
|
321
|
+
s := Stamp(id: 4, note: "x")
|
|
322
|
+
m2: Mark := takeMark(s)
|
|
323
|
+
assert markId(m2) == 1004
|
|
324
|
+
m3 := takeMark(m2)
|
|
325
|
+
assert markId(m3) == 1004
|
|
326
|
+
|
|
327
|
+
test "init_and_variant_name_resolution"
|
|
328
|
+
a := Cell(5)
|
|
329
|
+
assert a.size() == 5
|
|
330
|
+
b := Cell(0)
|
|
331
|
+
assert b.size() == 0
|
|
332
|
+
c := Full(count: 9, label: "nine")
|
|
333
|
+
assert c.size() == 9
|
|
334
|
+
d := Full(label: "swapped", count: 2)
|
|
335
|
+
assert d.size() == 2
|
|
336
|
+
e: Cell := Full(label: "q", count: 7)
|
|
337
|
+
assert describe(e) == "7q"
|
plum-examples/unions.plum
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# A member converts to the enum where one is expected, a bare member name in a
|
|
3
3
|
# pattern narrows the subject, and a method every member defines is callable on
|
|
4
4
|
# the enum (auto-dispatch). Members can be structs, records, primitives
|
|
5
|
-
# (`Int | Float | Str`), generic instances (`
|
|
5
|
+
# (`Int | Float | Str`), generic instances (`Slice[Jv]`) or bare tags. An enum
|
|
6
6
|
# can also define its own methods (a `match self` body), carry a shared
|
|
7
7
|
# discriminant field (`enum Step(n: Int)`), and one type can belong to several
|
|
8
8
|
# enums.
|
|
@@ -10,7 +10,7 @@ module unions
|
|
|
10
10
|
import std/Str
|
|
11
11
|
import std/Int
|
|
12
12
|
import std/Float
|
|
13
|
-
import std/
|
|
13
|
+
import std/Slice
|
|
14
14
|
|
|
15
15
|
struct Circle(r: Float) =
|
|
16
16
|
fun area(self) -> Float = self.r * self.r * 3.0
|
|
@@ -32,7 +32,7 @@ record Leaf(v: Int)
|
|
|
32
32
|
|
|
33
33
|
record Node[T](left: Tree[T], right: Tree[T])
|
|
34
34
|
|
|
35
|
-
enum Jv = JvNull | Int | Float | Str |
|
|
35
|
+
enum Jv = JvNull | Int | Float | Str | Slice[Jv]
|
|
36
36
|
|
|
37
37
|
fun total(t: Tree[Int]) -> Int =
|
|
38
38
|
match t
|
|
@@ -49,7 +49,7 @@ fun describeJv(j: Jv) -> Str =
|
|
|
49
49
|
Int => "int"
|
|
50
50
|
Float => "float"
|
|
51
51
|
Str => "str {j}"
|
|
52
|
-
|
|
52
|
+
Slice => "list"
|
|
53
53
|
|
|
54
54
|
fun areaOf(s: Shape) -> Float = s.area()
|
|
55
55
|
|
|
@@ -115,7 +115,7 @@ fun describeMatch(b: Book) -> Str =
|
|
|
115
115
|
Novel(title, _) => "novel {title}"
|
|
116
116
|
Atlas(title, _) => "atlas {title}"
|
|
117
117
|
|
|
118
|
-
enum Jb = BNull | Bool | Float | Str |
|
|
118
|
+
enum Jb = BNull | Bool | Float | Str | Slice[Jb]
|
|
119
119
|
|
|
120
120
|
fun showB(j: Jb) -> Str =
|
|
121
121
|
match j
|
|
@@ -123,12 +123,12 @@ fun showB(j: Jb) -> Str =
|
|
|
123
123
|
Bool => if j then "T" else "F"
|
|
124
124
|
Float => "f"
|
|
125
125
|
Str => "s"
|
|
126
|
-
|
|
126
|
+
Slice => "l"
|
|
127
127
|
|
|
128
|
-
fun pos(n: Int) -> !
|
|
128
|
+
fun pos(n: Int) -> !Slice[Jv] =
|
|
129
129
|
if n < 0
|
|
130
130
|
return error.Neg
|
|
131
|
-
xs :=
|
|
131
|
+
xs := Slice[Jv]()
|
|
132
132
|
xs.push(n)
|
|
133
133
|
xs.push("a")
|
|
134
134
|
xs
|
|
@@ -148,7 +148,7 @@ test "struct_member_copy"
|
|
|
148
148
|
assert false
|
|
149
149
|
|
|
150
150
|
test "list_of_shapes"
|
|
151
|
-
xs :=
|
|
151
|
+
xs := Slice[Shape]()
|
|
152
152
|
xs.push(Circle(r: 1.0))
|
|
153
153
|
xs.push(Rectangle(w: 2.0, h: 3.0))
|
|
154
154
|
sum := 0.0
|
|
@@ -164,7 +164,7 @@ test "json_like_primitives"
|
|
|
164
164
|
assert describeJv(7) == "int"
|
|
165
165
|
assert describeJv(1.5) == "float"
|
|
166
166
|
assert describeJv("hi") == "str hi"
|
|
167
|
-
js :=
|
|
167
|
+
js := Slice[Jv]()
|
|
168
168
|
js.push(1.0)
|
|
169
169
|
js.push(7)
|
|
170
170
|
assert describeJv(js) == "list"
|
|
@@ -196,11 +196,11 @@ test "bool_member"
|
|
|
196
196
|
test "union_in_try"
|
|
197
197
|
assert {posLen(3) catch 0} == 2
|
|
198
198
|
assert {posLen(-1) catch 0} == 0
|
|
199
|
-
ys := pos(3) catch
|
|
199
|
+
ys := pos(3) catch Slice[Jv]()
|
|
200
200
|
assert describeJv(ys) == "list"
|
|
201
201
|
|
|
202
202
|
test "union_with_for"
|
|
203
|
-
xs :=
|
|
203
|
+
xs := Slice[Jv]()
|
|
204
204
|
xs.push(1)
|
|
205
205
|
xs.push("a")
|
|
206
206
|
xs.push(2.5)
|
|
@@ -226,7 +226,7 @@ test "enum defines its own method over its members"
|
|
|
226
226
|
assert b.area() == 2.0
|
|
227
227
|
|
|
228
228
|
test "Book both ways: auto-dispatch and match agree"
|
|
229
|
-
books:
|
|
229
|
+
books: Slice[Book] := Slice[Book]()
|
|
230
230
|
books.push(Novel(title: "Dune", pages: 600))
|
|
231
231
|
books.push(Atlas(title: "World", regions: 7))
|
|
232
232
|
for b := range books
|
plum-game/web-test/fixtures/stress_pure_scene.plum
CHANGED
|
@@ -8,7 +8,7 @@ module stress_pure_scene
|
|
|
8
8
|
import std/Str
|
|
9
9
|
import std/Int
|
|
10
10
|
import std/Float
|
|
11
|
-
import std/
|
|
11
|
+
import std/Slice
|
|
12
12
|
import std/game/Vec2
|
|
13
13
|
import std/game/Color
|
|
14
14
|
import std/game/Affine
|
|
@@ -74,7 +74,7 @@ fun drifter(id: Int, tex: Int, c: Color, x: Float, y: Float, w: Float, h: Float,
|
|
|
74
74
|
d := Drifter(
|
|
75
75
|
id: id, x: x, y: y, width: w, height: h, originX: 0.0, originY: 0.0,
|
|
76
76
|
scaleX: 1.0, scaleY: 1.0, rotation: 0.0, color: c, visible: true,
|
|
77
|
-
touchable: Disabled, actions:
|
|
77
|
+
touchable: Disabled, actions: Slice[Action](), tex: tex, dx: dx, dy: dy)
|
|
78
78
|
me: Actor := d
|
|
79
79
|
me
|
|
80
80
|
|
|
@@ -82,12 +82,12 @@ fun countLabel(id: Int, n: Int, x: Float, y: Float) -> Actor =
|
|
|
82
82
|
l := CountLabel(
|
|
83
83
|
id: id, x: x, y: y, width: 60.0, height: 14.0, originX: 0.0, originY: 0.0,
|
|
84
84
|
scaleX: 1.0, scaleY: 1.0, rotation: 0.0, color: Color.White, visible: true,
|
|
85
|
-
touchable: Disabled, actions:
|
|
85
|
+
touchable: Disabled, actions: Slice[Action](), n: n, text: "n {n}")
|
|
86
86
|
me: Actor := l
|
|
87
87
|
me
|
|
88
88
|
|
|
89
|
-
fun spinning(rate: Float) ->
|
|
89
|
+
fun spinning(rate: Float) -> Slice[Action] =
|
|
90
|
-
acts :=
|
|
90
|
+
acts := Slice[Action]()
|
|
91
91
|
acts.push(Actions.forever(Actions.rotateBy(rate, 1.0)))
|
|
92
92
|
acts
|
|
93
93
|
|
|
@@ -104,10 +104,10 @@ fun dancer(id: Int, sheet: Int, j: Int, i: Int) -> Actor =
|
|
|
104
104
|
me
|
|
105
105
|
|
|
106
106
|
fun buildWorld(sheet: Int, white: Int) -> World =
|
|
107
|
-
root :=
|
|
107
|
+
root := Slice[Actor]()
|
|
108
108
|
per := Counts.Sprites / Counts.Groups
|
|
109
109
|
for gi := range Counts.Groups
|
|
110
|
-
kids :=
|
|
110
|
+
kids := Slice[Actor]()
|
|
111
111
|
for j := range per
|
|
112
112
|
i := gi * per + j
|
|
113
113
|
kids.push(dancer(1000 + i, sheet, j, i))
|
|
@@ -138,7 +138,7 @@ export fun init() -> World =
|
|
|
138
138
|
buildWorld(textureLoad("sprites.png"), whiteTexture())
|
|
139
139
|
|
|
140
140
|
fun frameStage(own s: Stage, dt: Float) -> Stage =
|
|
141
|
-
s.frame(dt,
|
|
141
|
+
s.frame(dt, Slice[Input]()).intoStage()
|
|
142
142
|
|
|
143
143
|
export fun update(w: World, dt: Float) =
|
|
144
144
|
w.stage = frameStage(w.stage, dt)
|
plum-game/web-test/fixtures/stress_scene.plum
CHANGED
|
@@ -6,7 +6,7 @@ module stress_scene
|
|
|
6
6
|
import std/Str
|
|
7
7
|
import std/Int
|
|
8
8
|
import std/Float
|
|
9
|
-
import std/
|
|
9
|
+
import std/Slice
|
|
10
10
|
import std/game/Vec2
|
|
11
11
|
import std/game/Color
|
|
12
12
|
import std/game/Game
|
plum-game/web-test/fixtures/stress_state_view.plum
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# The stress scene of stress_pure_scene.plum written as state and view, now recursive:
|
|
2
2
|
# 5000 sprites in 50 slowly rotating groups, 500 rects and 200 circles (sprites of a
|
|
3
3
|
# 1x1 white texture), 50 texts with a changing number, drawn through a 0.8 camera.
|
|
4
|
-
# `State` is
|
|
4
|
+
# `State` is cons `List` lists (each spinner owns the `List` of its 100 dancers);
|
|
5
5
|
# `step` is pure, a structural recursion that returns the next state; `view` is pure
|
|
6
6
|
# and rebuilds the whole SceneNode tree every frame with recursive functions, which
|
|
7
7
|
# `Picture.draw` interprets. Nothing is retained between frames but `State`. The
|
|
@@ -10,6 +10,7 @@ module stress_state_view
|
|
|
10
10
|
import std/Str
|
|
11
11
|
import std/Int
|
|
12
12
|
import std/Float
|
|
13
|
+
import std/Slice
|
|
13
14
|
import std/List
|
|
14
15
|
import std/game/Vec2
|
|
15
16
|
import std/game/Color
|
|
@@ -25,39 +26,38 @@ primitive Counts =
|
|
|
25
26
|
WorldW = 1600
|
|
26
27
|
WorldH = 900
|
|
27
28
|
|
|
28
|
-
enum Link[T] = Nil | Cons(head: T, tail: Link[T])
|
|
29
29
|
|
|
30
|
-
fun mapLink(xs:
|
|
30
|
+
fun mapLink(xs: List[A], f: fn(A) -> B) -> List[B] = match xs
|
|
31
31
|
Nil => Nil
|
|
32
32
|
Cons(h, t) => Cons(head: f(h), tail: mapLink(t, f))
|
|
33
33
|
|
|
34
34
|
# The list f(i), f(i + 1) ... f(n - 1).
|
|
35
|
-
fun rangeLink(i: Int, n: Int, f: fn(Int) -> T) ->
|
|
35
|
+
fun rangeLink(i: Int, n: Int, f: fn(Int) -> T) -> List[T] =
|
|
36
36
|
if i >= n then Nil else Cons(head: f(i), tail: rangeLink(i + 1, n, f))
|
|
37
37
|
|
|
38
|
-
fun appendLink(xs:
|
|
38
|
+
fun appendLink(xs: List[T], ys: List[T]) -> List[T] = match xs
|
|
39
39
|
Nil => ys
|
|
40
40
|
Cons(h, t) => Cons(head: h, tail: appendLink(t, ys))
|
|
41
41
|
|
|
42
42
|
# The element at `i`, or `dflt` past the end.
|
|
43
|
-
fun nthOr(xs:
|
|
43
|
+
fun nthOr(xs: List[T], i: Int, dflt: T) -> T = match xs
|
|
44
44
|
Nil => dflt
|
|
45
45
|
Cons(h, t) => if i <= 0 then h else nthOr(t, i - 1, dflt)
|
|
46
46
|
|
|
47
47
|
# The nodes f(x) for every x of `xs`, followed by `rest`.
|
|
48
|
-
fun toNodes(xs:
|
|
48
|
+
fun toNodes(xs: List[T], f: fn(T) -> SceneNode, rest: List[SceneNode]) -> List[SceneNode] = match xs
|
|
49
49
|
Nil => rest
|
|
50
|
-
Cons(h, t) =>
|
|
50
|
+
Cons(h, t) => Cons(head: f(h), tail: toNodes(t, f, rest))
|
|
51
51
|
|
|
52
|
-
fun countNodes(ns:
|
|
52
|
+
fun countNodes(ns: List[SceneNode]) -> Int = match ns
|
|
53
|
-
|
|
53
|
+
Nil => 0
|
|
54
|
-
|
|
54
|
+
Cons(_, t) => 1 + countNodes(t)
|
|
55
55
|
|
|
56
56
|
# A sprite of the sheet inside its group, spinning about its centre.
|
|
57
57
|
struct Dancer(id: Int, x: Float, y: Float, rotation: Float, srcX: Float)
|
|
58
58
|
|
|
59
59
|
# A rotating group of dancers: top-left (x, y), pivot (75, 85).
|
|
60
|
-
record Spinner(id: Int, x: Float, y: Float, rotation: Float, dancers:
|
|
60
|
+
record Spinner(id: Int, x: Float, y: Float, rotation: Float, dancers: List[Dancer])
|
|
61
61
|
|
|
62
62
|
# A square of the white texture that moves by (dx, dy) per frame and wraps.
|
|
63
63
|
struct Drifter(id: Int, x: Float, y: Float, w: Float, h: Float, dx: Float, dy: Float, color: Color)
|
|
@@ -65,7 +65,7 @@ struct Drifter(id: Int, x: Float, y: Float, w: Float, h: Float, dx: Float, dy: F
|
|
|
65
65
|
# Shows "n <count>" and counts up each frame.
|
|
66
66
|
struct Counter(id: Int, x: Float, y: Float, n: Int)
|
|
67
67
|
|
|
68
|
-
record State(sheet: Int, white: Int, spinners:
|
|
68
|
+
record State(sheet: Int, white: Int, spinners: List[Spinner], drifters: List[Drifter], counters: List[Counter])
|
|
69
69
|
|
|
70
70
|
fun hue(i: Int) -> Color =
|
|
71
71
|
Color(r: Float(i % 7) / 7.0, g: Float(i % 5) / 5.0, b: 0.4 + Float(i % 3) / 5.0, a: 0.8)
|
|
@@ -118,14 +118,14 @@ fun step(s: State, dt: Float) -> State =
|
|
|
118
118
|
fun dancerView(sheet: Int, d: Dancer) -> SceneNode =
|
|
119
119
|
Group(
|
|
120
120
|
transform: Compose(first: Translate(dx: d.x + 8.0, dy: d.y + 8.0), then: Compose(first: Rotate(radians: d.rotation), then: Translate(dx: 0.0 - 8.0, dy: 0.0 - 8.0))),
|
|
121
|
-
children:
|
|
121
|
+
children: Cons(head: Sprite(
|
|
122
122
|
tex: sheet, x: 0.0, y: 0.0, w: 16.0, h: 16.0, srcX: d.srcX, srcY: 0.0, srcW: 16.0, srcH: 16.0,
|
|
123
|
-
tint: Color.White), tail:
|
|
123
|
+
tint: Color.White), tail: Nil))
|
|
124
124
|
|
|
125
125
|
fun spinnerView(sheet: Int, p: Spinner) -> SceneNode =
|
|
126
126
|
Group(
|
|
127
127
|
transform: Compose(first: Translate(dx: p.x + 75.0, dy: p.y + 85.0), then: Compose(first: Rotate(radians: p.rotation), then: Translate(dx: 0.0 - 75.0, dy: 0.0 - 85.0))),
|
|
128
|
-
children: toNodes(p.dancers, |d| dancerView(sheet, d),
|
|
128
|
+
children: toNodes(p.dancers, |d| dancerView(sheet, d), Nil))
|
|
129
129
|
|
|
130
130
|
fun drifterView(white: Int, d: Drifter) -> SceneNode =
|
|
131
131
|
Sprite(
|
|
@@ -136,7 +136,7 @@ fun counterView(c: Counter) -> SceneNode =
|
|
|
136
136
|
Text(text: "n {c.n}", x: c.x, y: c.y, size: 14.0, color: Color.White)
|
|
137
137
|
|
|
138
138
|
fun view(s: State) -> SceneNode =
|
|
139
|
-
texts := toNodes(s.counters, |c| counterView(c),
|
|
139
|
+
texts := toNodes(s.counters, |c| counterView(c), Nil)
|
|
140
140
|
drifts := toNodes(s.drifters, |d| drifterView(s.white, d), texts)
|
|
141
141
|
Group(transform: Scale(sx: 0.8, sy: 0.8), children: toNodes(s.spinners, |p| spinnerView(s.sheet, p), drifts))
|
|
142
142
|
|
plum-game/web-test/fixtures/stress_state_view_deep.plum
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
# Variant of stress_state_view.plum: all 5000 dancers live in ONE
|
|
1
|
+
# Variant of stress_state_view.plum: all 5000 dancers live in ONE List (each records its
|
|
2
2
|
# spinner index), so step recurses 5000 deep. Original header follows.
|
|
3
3
|
# The stress scene of stress_pure_scene.plum written as state and view, now recursive:
|
|
4
4
|
# 5000 sprites in 50 slowly rotating groups, 500 rects and 200 circles (sprites of a
|
|
5
5
|
# 1x1 white texture), 50 texts with a changing number, drawn through a 0.8 camera.
|
|
6
|
-
# `State` is
|
|
6
|
+
# `State` is cons `List` lists (each spinner owns the `List` of its 100 dancers);
|
|
7
7
|
# `step` is pure, a structural recursion that returns the next state; `view` is pure
|
|
8
8
|
# and rebuilds the whole SceneNode tree every frame with recursive functions, which
|
|
9
9
|
# `Picture.draw` interprets. Nothing is retained between frames but `State`. The
|
|
@@ -12,6 +12,7 @@ module stress_state_view
|
|
|
12
12
|
import std/Str
|
|
13
13
|
import std/Int
|
|
14
14
|
import std/Float
|
|
15
|
+
import std/Slice
|
|
15
16
|
import std/List
|
|
16
17
|
import std/game/Vec2
|
|
17
18
|
import std/game/Color
|
|
@@ -27,33 +28,32 @@ primitive Counts =
|
|
|
27
28
|
WorldW = 1600
|
|
28
29
|
WorldH = 900
|
|
29
30
|
|
|
30
|
-
enum Link[T] = Nil | Cons(head: T, tail: Link[T])
|
|
31
31
|
|
|
32
|
-
fun mapLink(xs:
|
|
32
|
+
fun mapLink(xs: List[A], f: fn(A) -> B) -> List[B] = match xs
|
|
33
33
|
Nil => Nil
|
|
34
34
|
Cons(h, t) => Cons(head: f(h), tail: mapLink(t, f))
|
|
35
35
|
|
|
36
36
|
# The list f(i), f(i + 1) ... f(n - 1).
|
|
37
|
-
fun rangeLink(i: Int, n: Int, f: fn(Int) -> T) ->
|
|
37
|
+
fun rangeLink(i: Int, n: Int, f: fn(Int) -> T) -> List[T] =
|
|
38
38
|
if i >= n then Nil else Cons(head: f(i), tail: rangeLink(i + 1, n, f))
|
|
39
39
|
|
|
40
|
-
fun appendLink(xs:
|
|
40
|
+
fun appendLink(xs: List[T], ys: List[T]) -> List[T] = match xs
|
|
41
41
|
Nil => ys
|
|
42
42
|
Cons(h, t) => Cons(head: h, tail: appendLink(t, ys))
|
|
43
43
|
|
|
44
44
|
# The element at `i`, or `dflt` past the end.
|
|
45
|
-
fun nthOr(xs:
|
|
45
|
+
fun nthOr(xs: List[T], i: Int, dflt: T) -> T = match xs
|
|
46
46
|
Nil => dflt
|
|
47
47
|
Cons(h, t) => if i <= 0 then h else nthOr(t, i - 1, dflt)
|
|
48
48
|
|
|
49
49
|
# The nodes f(x) for every x of `xs`, followed by `rest`.
|
|
50
|
-
fun toNodes(xs:
|
|
50
|
+
fun toNodes(xs: List[T], f: fn(T) -> SceneNode, rest: List[SceneNode]) -> List[SceneNode] = match xs
|
|
51
51
|
Nil => rest
|
|
52
|
-
Cons(h, t) =>
|
|
52
|
+
Cons(h, t) => Cons(head: f(h), tail: toNodes(t, f, rest))
|
|
53
53
|
|
|
54
|
-
fun countNodes(ns:
|
|
54
|
+
fun countNodes(ns: List[SceneNode]) -> Int = match ns
|
|
55
|
-
|
|
55
|
+
Nil => 0
|
|
56
|
-
|
|
56
|
+
Cons(_, t) => 1 + countNodes(t)
|
|
57
57
|
|
|
58
58
|
# A sprite of the sheet inside its group, spinning about its centre.
|
|
59
59
|
struct Dancer(gi: Int, id: Int, x: Float, y: Float, rotation: Float, srcX: Float)
|
|
@@ -67,7 +67,7 @@ struct Drifter(id: Int, x: Float, y: Float, w: Float, h: Float, dx: Float, dy: F
|
|
|
67
67
|
# Shows "n <count>" and counts up each frame.
|
|
68
68
|
struct Counter(id: Int, x: Float, y: Float, n: Int)
|
|
69
69
|
|
|
70
|
-
record State(sheet: Int, white: Int, spinners:
|
|
70
|
+
record State(sheet: Int, white: Int, spinners: List[Spinner], dancers: List[Dancer], drifters: List[Drifter], counters: List[Counter])
|
|
71
71
|
|
|
72
72
|
fun hue(i: Int) -> Color =
|
|
73
73
|
Color(r: Float(i % 7) / 7.0, g: Float(i % 5) / 5.0, b: 0.4 + Float(i % 3) / 5.0, a: 0.8)
|
|
@@ -83,7 +83,7 @@ fun newSpinner(gi: Int) -> Spinner =
|
|
|
83
83
|
Spinner(id: 100 + gi, x: Float(gi % 10 * 160), y: Float(gi / 10 * 180), rotation: 0.0)
|
|
84
84
|
|
|
85
85
|
# All dancers, group after group, built by recursion over the spinner index.
|
|
86
|
-
fun allDancers(gi: Int) ->
|
|
86
|
+
fun allDancers(gi: Int) -> List[Dancer] =
|
|
87
87
|
if gi >= Counts.Groups then Nil else appendLink(rangeLink(0, Counts.Sprites / Counts.Groups, |j| newDancer(gi, j)), allDancers(gi + 1))
|
|
88
88
|
|
|
89
89
|
fun newState(sheet: Int, white: Int) -> State =
|
|
@@ -124,29 +124,29 @@ fun step(s: State, dt: Float) -> State =
|
|
|
124
124
|
fun dancerView(sheet: Int, d: Dancer) -> SceneNode =
|
|
125
125
|
Group(
|
|
126
126
|
transform: Compose(first: Translate(dx: d.x + 8.0, dy: d.y + 8.0), then: Compose(first: Rotate(radians: d.rotation), then: Translate(dx: 0.0 - 8.0, dy: 0.0 - 8.0))),
|
|
127
|
-
children:
|
|
127
|
+
children: Cons(head: Sprite(
|
|
128
128
|
tex: sheet, x: 0.0, y: 0.0, w: 16.0, h: 16.0, srcX: d.srcX, srcY: 0.0, srcW: 16.0, srcH: 16.0,
|
|
129
|
-
tint: Color.White), tail:
|
|
129
|
+
tint: Color.White), tail: Nil))
|
|
130
130
|
|
|
131
131
|
# The leading dancers of `ds` that belong to group `gi`, as nodes.
|
|
132
|
-
fun groupNodes(sheet: Int, ds:
|
|
132
|
+
fun groupNodes(sheet: Int, ds: List[Dancer], gi: Int) -> List[SceneNode] = match ds
|
|
133
|
-
Nil =>
|
|
133
|
+
Nil => Nil
|
|
134
|
-
Cons(h, t) => if h.gi == gi then
|
|
134
|
+
Cons(h, t) => if h.gi == gi then Cons(head: dancerView(sheet, h), tail: groupNodes(sheet, t, gi)) else Nil
|
|
135
135
|
|
|
136
136
|
# `ds` without its leading dancers of group `gi`.
|
|
137
|
-
fun dropGroup(ds:
|
|
137
|
+
fun dropGroup(ds: List[Dancer], gi: Int) -> List[Dancer] = match ds
|
|
138
138
|
Nil => Nil
|
|
139
139
|
Cons(h, t) => if h.gi == gi then dropGroup(t, gi) else ds
|
|
140
140
|
|
|
141
|
-
fun spinnerView(sheet: Int, p: Spinner, ds:
|
|
141
|
+
fun spinnerView(sheet: Int, p: Spinner, ds: List[Dancer]) -> SceneNode =
|
|
142
142
|
Group(
|
|
143
143
|
transform: Compose(first: Translate(dx: p.x + 75.0, dy: p.y + 85.0), then: Compose(first: Rotate(radians: p.rotation), then: Translate(dx: 0.0 - 75.0, dy: 0.0 - 85.0))),
|
|
144
144
|
children: groupNodes(sheet, ds, p.id - 100))
|
|
145
145
|
|
|
146
146
|
# Spinner groups in order, each taking its dancers from the front of the one list.
|
|
147
|
-
fun spinnerNodes(sheet: Int, ps:
|
|
147
|
+
fun spinnerNodes(sheet: Int, ps: List[Spinner], ds: List[Dancer], rest: List[SceneNode]) -> List[SceneNode] = match ps
|
|
148
148
|
Nil => rest
|
|
149
|
-
Cons(p, t) =>
|
|
149
|
+
Cons(p, t) => Cons(head: spinnerView(sheet, p, ds), tail: spinnerNodes(sheet, t, dropGroup(ds, p.id - 100), rest))
|
|
150
150
|
|
|
151
151
|
fun drifterView(white: Int, d: Drifter) -> SceneNode =
|
|
152
152
|
Sprite(
|
|
@@ -157,7 +157,7 @@ fun counterView(c: Counter) -> SceneNode =
|
|
|
157
157
|
Text(text: "n {c.n}", x: c.x, y: c.y, size: 14.0, color: Color.White)
|
|
158
158
|
|
|
159
159
|
fun view(s: State) -> SceneNode =
|
|
160
|
-
texts := toNodes(s.counters, |c| counterView(c),
|
|
160
|
+
texts := toNodes(s.counters, |c| counterView(c), Nil)
|
|
161
161
|
drifts := toNodes(s.drifters, |d| drifterView(s.white, d), texts)
|
|
162
162
|
Group(transform: Scale(sx: 0.8, sy: 0.8), children: spinnerNodes(s.sheet, s.spinners, s.dancers, drifts))
|
|
163
163
|
|
|
@@ -199,4 +199,4 @@ test "view has one node per actor"
|
|
|
199
199
|
|
|
200
200
|
test "dancers in one list 5000"
|
|
201
201
|
s := newState(0, 0)
|
|
202
|
-
assert countNodes(toNodes(s.dancers, |d| dancerView(0, d),
|
|
202
|
+
assert countNodes(toNodes(s.dancers, |d| dancerView(0, d), Nil)) == 5000
|
plum-game/web-test/fixtures/stress_world.plum
CHANGED
|
@@ -8,7 +8,7 @@ import std/Str
|
|
|
8
8
|
import std/Int
|
|
9
9
|
import std/Float
|
|
10
10
|
import std/Math
|
|
11
|
-
import std/
|
|
11
|
+
import std/Slice
|
|
12
12
|
import std/game/Vec2
|
|
13
13
|
import std/game/Color
|
|
14
14
|
import std/game/Game
|
plum-lsp/src/main.rs
CHANGED
|
@@ -942,14 +942,14 @@ mod tests {
|
|
|
942
942
|
}
|
|
943
943
|
}
|
|
944
944
|
|
|
945
|
-
/// `xs.` on a
|
|
945
|
+
/// `xs.` on a Slice offers Slice's own methods AND the Seq methods reached
|
|
946
946
|
/// through the iterable fallback.
|
|
947
947
|
#[test]
|
|
948
|
-
fn
|
|
948
|
+
fn memberCompletionOnSliceIncludesSeqMethods() {
|
|
949
949
|
let dir = std::env::temp_dir().join(format!("plum-lsp-test-{}", std::process::id()));
|
|
950
950
|
std::fs::create_dir_all(&dir).unwrap();
|
|
951
951
|
let path = dir.join("t.plum");
|
|
952
|
-
let text = "module t\nimport std/Int\nimport std/Float\nimport std/
|
|
952
|
+
let text = "module t\nimport std/Int\nimport std/Float\nimport std/Slice\nimport std/Seq\n\nfun main() =\n xs := Slice(1, 2, 3)\n xs.zzzzzz\n";
|
|
953
953
|
std::fs::write(&path, text).unwrap();
|
|
954
954
|
let items = memberCompletionFor(&path, &repoRoot(), text, 8, "xs");
|
|
955
955
|
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
|
|
@@ -989,7 +989,7 @@ mod tests {
|
|
|
989
989
|
let dir = std::env::temp_dir().join(format!("plum-lsp-fsm-{}", std::process::id()));
|
|
990
990
|
std::fs::create_dir_all(&dir).unwrap();
|
|
991
991
|
let path = dir.join("t.plum");
|
|
992
|
-
let text = "module t\nimport std/Int\nimport std/Float\nimport std/Str\nimport std/
|
|
992
|
+
let text = "module t\nimport std/Int\nimport std/Float\nimport std/Str\nimport std/Slice\nimport std/Map\n\nfun main() =\n f := 1.5\n f.zzzzzz\n s := \"x\"\n s.zzzzzz\n m := Map[Str, Int]()\n m.zzzzzz\n";
|
|
993
993
|
std::fs::write(&path, text).unwrap();
|
|
994
994
|
let cases: [(usize, &str, &[&str]); 3] = [
|
|
995
995
|
(9, "f", &["sqrt", "toStr"]),
|
plum-std/Array.plum
CHANGED
|
@@ -23,7 +23,7 @@ import std/Byte
|
|
|
23
23
|
# stay arrays of references.) `init`/`get`/`set`/`length`/
|
|
24
24
|
# `push`/`resize` are compiler intrinsics (see `compileIntrinsicFnBody`) —
|
|
25
25
|
# there's no way to express raw array allocation/indexing in Plum source itself.
|
|
26
|
-
# `
|
|
26
|
+
# `Slice[T]` is built on this.
|
|
27
27
|
#
|
|
28
28
|
# `init()` is deliberately zero-arg (no `Array[T](n)` constructor exists) —
|
|
29
29
|
# `class_call`'s grammar (`Type[Generics](...)`) only ever accepts `name:
|
|
@@ -67,7 +67,7 @@ primitive Array[T] =
|
|
|
67
67
|
todo
|
|
68
68
|
|
|
69
69
|
# Returns a NEW array of length `n` holding `self`'s first `min(length, n)`
|
|
70
|
-
# elements (the remaining slots null-shaped, like `init`/`push`). `
|
|
70
|
+
# elements (the remaining slots null-shaped, like `init`/`push`). `Slice`
|
|
71
71
|
# grows by doubling through this.
|
|
72
72
|
fun resize(self, n: Int) -> Array[T] =
|
|
73
73
|
todo
|
plum-std/Deque.plum
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module std
|
|
2
2
|
import std/Array
|
|
3
|
-
import std/
|
|
3
|
+
import std/Slice
|
|
4
4
|
import std/Int
|
|
5
5
|
import std/Float
|
|
6
6
|
|
plum-std/Heap.plum
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module std
|
|
2
2
|
import std/Array
|
|
3
|
-
import std/
|
|
3
|
+
import std/Slice
|
|
4
4
|
import std/Int
|
|
5
5
|
import std/Float
|
|
6
6
|
|
|
@@ -11,7 +11,7 @@ import std/Float
|
|
|
11
11
|
# comparison, so keep it cheap and don't mutate an element's key while it is in
|
|
12
12
|
# the heap. `push`/`pop` are O(log n); `peek`/`length` are O(1). Equal keys pop in
|
|
13
13
|
# no guaranteed order.
|
|
14
|
-
record Heap[T](items:
|
|
14
|
+
record Heap[T](items: Slice[T], key: fn(T) -> Float) =
|
|
15
15
|
|
|
16
16
|
fun length(self) -> Int =
|
|
17
17
|
return self.items.length()
|
|
@@ -77,7 +77,7 @@ record Heap[T](items: List[T], key: fn(T) -> Float) =
|
|
|
77
77
|
top
|
|
78
78
|
|
|
79
79
|
test "heap_pop_order: 1000 pseudo-random keys pop in ascending order"
|
|
80
|
-
h := Heap[Int](items:
|
|
80
|
+
h := Heap[Int](items: Slice[Int](), key: |x| Float(x))
|
|
81
81
|
x := 12345
|
|
82
82
|
i := 0
|
|
83
83
|
while i < 1000
|
|
@@ -96,7 +96,7 @@ test "heap_pop_order: 1000 pseudo-random keys pop in ascending order"
|
|
|
96
96
|
assert {h.pop() catch -1} == -1
|
|
97
97
|
|
|
98
98
|
test "peek shows the minimum and Float keys order records"
|
|
99
|
-
h := Heap[Float](items:
|
|
99
|
+
h := Heap[Float](items: Slice[Float](), key: |x| 0.0 - x)
|
|
100
100
|
h.push(1.5)
|
|
101
101
|
h.push(9.5)
|
|
102
102
|
h.push(4.0)
|
plum-std/Http.plum
CHANGED
|
@@ -2,7 +2,7 @@ module std
|
|
|
2
2
|
|
|
3
3
|
import std/Buffer
|
|
4
4
|
import std/Json
|
|
5
|
-
import std/
|
|
5
|
+
import std/Slice
|
|
6
6
|
import std/Map
|
|
7
7
|
import std/Str
|
|
8
8
|
import std/Int
|
plum-std/Json.plum
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module std
|
|
2
2
|
|
|
3
|
-
import std/
|
|
3
|
+
import std/Slice
|
|
4
4
|
import std/Map
|
|
5
5
|
import std/Int
|
|
6
6
|
import std/Float
|
|
@@ -8,9 +8,9 @@ import std/Str
|
|
|
8
8
|
import std/Buffer
|
|
9
9
|
|
|
10
10
|
# Placement rule for built-in `toJson` / `fromJson`: they live HERE, in this
|
|
11
|
-
# file, as extension blocks at the bottom (`primitive Int =`, `primitive
|
|
11
|
+
# file, as extension blocks at the bottom (`primitive Int =`, `primitive Slice[T] =`),
|
|
12
12
|
# holding only `fun`s. The loader merges them into the type's methods, so they
|
|
13
|
-
# exist only in programs that import std/Json, and Int/Str/
|
|
13
|
+
# exist only in programs that import std/Json, and Int/Str/Slice/Map never
|
|
14
14
|
# import Json (no cycle, no Pair/Bool drag-in). Std-only; a duplicate method
|
|
15
15
|
# name is a collision error. `Json.encode` / `Json.decode` stay duck-typed and
|
|
16
16
|
# call `x.toJson()` / `T.fromJson(j)` per instantiation.
|
|
@@ -20,15 +20,15 @@ error JsonError = InvalidJson | WrongJsonType | MissingField
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
# A parsed JSON value. Numbers are always stored as `Float` (JSON doesn't
|
|
23
|
-
# distinguish integers from floats the way Plum does), and `
|
|
23
|
+
# distinguish integers from floats the way Plum does), and `Slice[Json]`/
|
|
24
|
-
# `Map[Str, Json]` nest `Json` itself — the same self-referential shape `
|
|
24
|
+
# `Map[Str, Json]` nest `Json` itself — the same self-referential shape `Slice[T]`
|
|
25
25
|
# already uses internally for `Node[T].next: Link[Node[T]]`.
|
|
26
26
|
enum Json =
|
|
27
27
|
| JsonNull
|
|
28
28
|
| Bool
|
|
29
29
|
| Float
|
|
30
30
|
| Str
|
|
31
|
-
|
|
|
31
|
+
| Slice[Json]
|
|
32
32
|
| Map[Str, Json]
|
|
33
33
|
|
|
34
34
|
# Parses `s` as a single JSON document, requiring the whole string (aside
|
|
@@ -56,7 +56,7 @@ enum Json =
|
|
|
56
56
|
Bool => if self then "true" else "false"
|
|
57
57
|
Float => self.toStr()
|
|
58
58
|
Str => "\"" + escapeJsonStr(self) + "\""
|
|
59
|
-
|
|
59
|
+
Slice => "[" + self.join(",") + "]"
|
|
60
60
|
Map => "{{" + jsonMapToStr(self) + "}}"
|
|
61
61
|
|
|
62
62
|
# ---- encode / decode API ----
|
|
@@ -93,7 +93,7 @@ enum Json =
|
|
|
93
93
|
return m
|
|
94
94
|
|
|
95
95
|
fun array(items: ...Json) -> Json =
|
|
96
|
-
out :=
|
|
96
|
+
out := Slice[Json]()
|
|
97
97
|
for x := range items
|
|
98
98
|
out.push(x)
|
|
99
99
|
return out
|
|
@@ -122,9 +122,9 @@ enum Json =
|
|
|
122
122
|
Str => self
|
|
123
123
|
_ => error.WrongJsonType
|
|
124
124
|
|
|
125
|
-
fun asList(self) -> error{WrongJsonType}!
|
|
125
|
+
fun asList(self) -> error{WrongJsonType}!Slice[Json] =
|
|
126
126
|
match self
|
|
127
|
-
|
|
127
|
+
Slice => self
|
|
128
128
|
_ => error.WrongJsonType
|
|
129
129
|
|
|
130
130
|
fun asMap(self) -> error{WrongJsonType}!Map[Str, Json] =
|
|
@@ -132,7 +132,7 @@ enum Json =
|
|
|
132
132
|
Map => self
|
|
133
133
|
_ => error.WrongJsonType
|
|
134
134
|
|
|
135
|
-
# `Map` has no built-in `toStr`/`join` (unlike `
|
|
135
|
+
# `Map` has no built-in `toStr`/`join` (unlike `Slice`, which uses `join` for
|
|
136
136
|
# `Json.toStr`'s `JsonList` case above) — a static taking the
|
|
137
137
|
# `Buffer` as a captured-by-reference parameter instead of a closure sidesteps
|
|
138
138
|
# `libs/std/list.plum`'s `join` note on closures: reassigning a CAPTURED
|
|
@@ -411,7 +411,7 @@ record JsonParser(src: Str, pos: Int, text: Str) =
|
|
|
411
411
|
fun parseArray(self) -> error{InvalidJson}!Json =
|
|
412
412
|
self.advance()
|
|
413
413
|
self.skipSpace()
|
|
414
|
-
items :=
|
|
414
|
+
items := Slice[Json]()
|
|
415
415
|
if self.peek() == 93
|
|
416
416
|
self.advance()
|
|
417
417
|
return items
|
|
@@ -512,18 +512,18 @@ primitive Str =
|
|
|
512
512
|
|
|
513
513
|
fun fromJson(j: Json) -> JsonError!Str = j.asStr()
|
|
514
514
|
|
|
515
|
-
primitive
|
|
515
|
+
primitive Slice[T] =
|
|
516
516
|
# A JSON array of the elements' own `toJson()`.
|
|
517
517
|
fun toJson(self) -> Json =
|
|
518
|
-
out :=
|
|
518
|
+
out := Slice[Json]()
|
|
519
519
|
for x := range self
|
|
520
520
|
out.push(x.toJson())
|
|
521
521
|
return out
|
|
522
522
|
|
|
523
523
|
# Decodes each element with `T.fromJson`.
|
|
524
|
-
fun fromJson(j: Json) -> JsonError!
|
|
524
|
+
fun fromJson(j: Json) -> JsonError!Slice[T] =
|
|
525
525
|
items := try j.asList()
|
|
526
|
-
out :=
|
|
526
|
+
out := Slice[T]()
|
|
527
527
|
for x := range items
|
|
528
528
|
out.push(try T.fromJson(x))
|
|
529
529
|
return out
|
|
@@ -545,15 +545,15 @@ primitive Map[K, V] =
|
|
|
545
545
|
|
|
546
546
|
fun jsonTestDecodeInt(s: Str) -> JsonError!Int = Json.decode(s)
|
|
547
547
|
|
|
548
|
-
fun jsonTestDecodeNested(s: Str) -> JsonError!
|
|
548
|
+
fun jsonTestDecodeNested(s: Str) -> JsonError!Slice[Slice[Int]] = Json.decode(s)
|
|
549
549
|
|
|
550
550
|
fun jsonTestDecodeMap(s: Str) -> JsonError!Map[Str, Int] = Json.decode(s)
|
|
551
551
|
|
|
552
552
|
fun jsonTestDecodeStr(s: Str) -> JsonError!Str = Json.decode(s)
|
|
553
553
|
|
|
554
554
|
# Fixture for the round-trip tests: a hand-written record codec (also a
|
|
555
|
-
# worked example of the pattern), including a
|
|
555
|
+
# worked example of the pattern), including a Slice of itself.
|
|
556
|
-
record JsonTestHouse(name: Str, rooms: Int, kids:
|
|
556
|
+
record JsonTestHouse(name: Str, rooms: Int, kids: Slice[JsonTestHouse]) =
|
|
557
557
|
fun toJson(self) -> Json =
|
|
558
558
|
Json.object(Pair("name", self.name.toJson()), Pair("rooms", self.rooms.toJson()), Pair("kids", self.kids.toJson()))
|
|
559
559
|
|
|
@@ -563,7 +563,7 @@ record JsonTestHouse(name: Str, rooms: Int, kids: List[JsonTestHouse]) =
|
|
|
563
563
|
kids := try jsonTestKidsOf(try j.field("kids"))
|
|
564
564
|
return JsonTestHouse(name: name, rooms: rooms, kids: kids)
|
|
565
565
|
|
|
566
|
-
fun jsonTestKidsOf(j: Json) -> JsonError!
|
|
566
|
+
fun jsonTestKidsOf(j: Json) -> JsonError!Slice[JsonTestHouse] = j.into()
|
|
567
567
|
|
|
568
568
|
fun jsonTestHouseError(s: Str) -> Str =
|
|
569
569
|
jsonTestDecodeHouse(s) catch e
|
|
@@ -684,11 +684,11 @@ test "escapes a 50000 character string of quotes and newlines"
|
|
|
684
684
|
test "extension: Int.toJson encodes a number"
|
|
685
685
|
assert 42.toJson().toStr() == "42.0"
|
|
686
686
|
|
|
687
|
-
test "extension:
|
|
687
|
+
test "extension: Slice.toJson encodes an array"
|
|
688
|
-
assert
|
|
688
|
+
assert Slice(1, 2, 3).toJson().toStr() == "[1.0,2.0,3.0]"
|
|
689
689
|
|
|
690
|
-
test "extension: nested
|
|
690
|
+
test "extension: nested Slice.toJson recurses through T.toJson"
|
|
691
|
-
assert
|
|
691
|
+
assert Slice(Slice(1), Slice(2, 3)).toJson().toStr() == "[[1.0],[2.0,3.0]]"
|
|
692
692
|
|
|
693
693
|
test "api: field missing, wrong container, optField"
|
|
694
694
|
o := Json.fromStr("{{\"a\":1}}")!
|
|
@@ -715,7 +715,7 @@ test "nested_list_of_lists round-trips"
|
|
|
715
715
|
assert v.length() == 3
|
|
716
716
|
assert v.get(0)!.get(1)! == 2
|
|
717
717
|
assert Json.encode(v) == "[[1.0,2.0],[3.0],[]]"
|
|
718
|
-
assert {jsonTestDecodeNested("[[1,\"x\"]]") catch
|
|
718
|
+
assert {jsonTestDecodeNested("[[1,\"x\"]]") catch Slice[Slice[Int]]()}.length() == 0
|
|
719
719
|
|
|
720
720
|
test "map_non_object_rejected"
|
|
721
721
|
assert {jsonTestDecodeMap("[]") catch Map[Str, Int]()}.length() == 0
|
|
@@ -726,8 +726,8 @@ test "str_unicode_roundtrip"
|
|
|
726
726
|
assert jsonTestDecodeStr(Json.encode(s))! == s
|
|
727
727
|
|
|
728
728
|
test "record round-trips including a list of itself"
|
|
729
|
-
kid := JsonTestHouse(name: "k\"1", rooms: 2, kids:
|
|
729
|
+
kid := JsonTestHouse(name: "k\"1", rooms: 2, kids: Slice[JsonTestHouse]())
|
|
730
|
-
kids :=
|
|
730
|
+
kids := Slice[JsonTestHouse]()
|
|
731
731
|
kids.push(kid)
|
|
732
732
|
h := JsonTestHouse(name: "main\n", rooms: 5, kids: kids)
|
|
733
733
|
text := Json.encode(h)
|
|
@@ -739,9 +739,9 @@ test "record round-trips including a list of itself"
|
|
|
739
739
|
assert Json.encode(back) == text
|
|
740
740
|
|
|
741
741
|
test "field_missing and decode_invalid"
|
|
742
|
-
assert {jsonTestDecodeHouse("{{\"name\":\"x\"}}") catch JsonTestHouse(name: "E", rooms: 0, kids:
|
|
742
|
+
assert {jsonTestDecodeHouse("{{\"name\":\"x\"}}") catch JsonTestHouse(name: "E", rooms: 0, kids: Slice[JsonTestHouse]())}.name == "E"
|
|
743
|
-
assert {jsonTestDecodeHouse("{{oops") catch JsonTestHouse(name: "E", rooms: 0, kids:
|
|
743
|
+
assert {jsonTestDecodeHouse("{{oops") catch JsonTestHouse(name: "E", rooms: 0, kids: Slice[JsonTestHouse]())}.name == "E"
|
|
744
|
-
assert {jsonTestDecodeHouse("[1]") catch JsonTestHouse(name: "E", rooms: 0, kids:
|
|
744
|
+
assert {jsonTestDecodeHouse("[1]") catch JsonTestHouse(name: "E", rooms: 0, kids: Slice[JsonTestHouse]())}.name == "E"
|
|
745
745
|
|
|
746
746
|
test "decodeWith uses an explicit decoder"
|
|
747
747
|
h := Json.decodeWith("{{\"name\":\"w\",\"rooms\":1,\"kids\":[]}}", |j| JsonTestHouse.fromJson(j))!
|
plum-std/List.plum
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
module std
|
|
2
|
+
|
|
3
|
+
import std/Array
|
|
4
|
+
import std/Buffer
|
|
5
|
+
import std/Int
|
|
6
|
+
import std/Slice
|
|
7
|
+
import std/Str
|
|
8
|
+
|
|
9
|
+
# A cons list: `Nil` or a head in front of another list. Operations are written
|
|
10
|
+
# so they run in constant stack: self tail calls become loops, a constructor
|
|
11
|
+
# wrapped single self call (`map`) is rewritten to a loop (TRMC), and `filter`
|
|
12
|
+
# builds through an accumulator that is reversed at the end.
|
|
13
|
+
enum List[T] =
|
|
14
|
+
| Nil
|
|
15
|
+
| Cons(head: T, tail: List[T])
|
|
16
|
+
|
|
17
|
+
# `List(1, 2, 3)`: built back to front so each cell is allocated once.
|
|
18
|
+
fun init(values: ...T) -> List[T] =
|
|
19
|
+
out: List[T] := Nil
|
|
20
|
+
i := values.length() - 1
|
|
21
|
+
while i >= 0
|
|
22
|
+
out = Cons(head: values.at(i), tail: out)
|
|
23
|
+
i = i - 1
|
|
24
|
+
return out
|
|
25
|
+
|
|
26
|
+
fun isEmpty(self) -> Bool = match self
|
|
27
|
+
Nil => true
|
|
28
|
+
Cons(_, _) => false
|
|
29
|
+
|
|
30
|
+
fun lengthFrom(self, acc: Int) -> Int = match self
|
|
31
|
+
Nil => acc
|
|
32
|
+
Cons(_, t) => t.lengthFrom(acc + 1)
|
|
33
|
+
|
|
34
|
+
fun length(self) -> Int = self.lengthFrom(0)
|
|
35
|
+
|
|
36
|
+
# The element at index i, or null when out of range.
|
|
37
|
+
fun get(self, i: Int) -> T? = match self
|
|
38
|
+
Nil => null
|
|
39
|
+
Cons(h, t) => if i < 0 then null else if i == 0 then h else t.get(i - 1)
|
|
40
|
+
|
|
41
|
+
# The element at index i (the `for x := range xs` protocol, O(i) per call, so a
|
|
42
|
+
# plain `for` is quadratic: prefer `for x := range xs.items()`). Crashes out of range.
|
|
43
|
+
fun at(self, i: Int) -> T = self.get(i) ?: crash "List.at out of range"
|
|
44
|
+
|
|
45
|
+
fun map(self, f: fn(T) -> B) -> List[B] = mapCells(self, f)
|
|
46
|
+
|
|
47
|
+
# Left fold: `f(f(f(acc, x0), x1), x2)`.
|
|
48
|
+
fun fold(self, acc: A, f: fn(A, T) -> A) -> A = match self
|
|
49
|
+
Nil => acc
|
|
50
|
+
Cons(h, t) => t.fold(f(acc, h), f)
|
|
51
|
+
|
|
52
|
+
# Pushes the elements of `self` onto `acc` one by one (so they come out reversed).
|
|
53
|
+
fun revOnto(self, acc: List[T]) -> List[T] = match self
|
|
54
|
+
Nil => acc
|
|
55
|
+
Cons(h, t) => t.revOnto(Cons(head: h, tail: acc))
|
|
56
|
+
|
|
57
|
+
fun reverse(self) -> List[T] =
|
|
58
|
+
nil: List[T] := Nil
|
|
59
|
+
self.revOnto(nil)
|
|
60
|
+
|
|
61
|
+
fun keepRev(self, keep: fn(T) -> Bool, acc: List[T]) -> List[T] = match self
|
|
62
|
+
Nil => acc
|
|
63
|
+
Cons(h, t) => t.keepRev(keep, if keep(h) then Cons(head: h, tail: acc) else acc)
|
|
64
|
+
|
|
65
|
+
fun filter(self, keep: fn(T) -> Bool) -> List[T] =
|
|
66
|
+
nil: List[T] := Nil
|
|
67
|
+
self.keepRev(keep, nil).reverse()
|
|
68
|
+
|
|
69
|
+
# `self` followed by `ys`. Only `self` is rebuilt; `ys` is shared.
|
|
70
|
+
fun append(self, ys: List[T]) -> List[T] =
|
|
71
|
+
self.reverse().revOnto(ys)
|
|
72
|
+
|
|
73
|
+
# A `for` source: `for x := range xs`.
|
|
74
|
+
fun* items(self) *T =
|
|
75
|
+
cur := self
|
|
76
|
+
while true
|
|
77
|
+
if Cons(h, t) := cur
|
|
78
|
+
cur = t
|
|
79
|
+
yield h
|
|
80
|
+
else
|
|
81
|
+
break
|
|
82
|
+
|
|
83
|
+
# The elements of `xs` as a cons list, in order.
|
|
84
|
+
fun fromSlice(xs: Slice[T]) -> List[T] =
|
|
85
|
+
out: List[T] := Nil
|
|
86
|
+
i := xs.length() - 1
|
|
87
|
+
while i >= 0
|
|
88
|
+
out = Cons(head: xs.at(i), tail: out)
|
|
89
|
+
i = i - 1
|
|
90
|
+
return out
|
|
91
|
+
|
|
92
|
+
fun toSlice(self) -> Slice[T] =
|
|
93
|
+
out := Slice()
|
|
94
|
+
for x := range self.items()
|
|
95
|
+
out.push(x)
|
|
96
|
+
return out
|
|
97
|
+
|
|
98
|
+
fun toArray(self) -> Array[T] =
|
|
99
|
+
out := Array[T]()
|
|
100
|
+
for x := range self.items()
|
|
101
|
+
out = out.push(x)
|
|
102
|
+
return out
|
|
103
|
+
|
|
104
|
+
fun toStr(self) -> Str =
|
|
105
|
+
res := Buffer()
|
|
106
|
+
res.write("[")
|
|
107
|
+
first := true
|
|
108
|
+
for x := range self.items()
|
|
109
|
+
if !first
|
|
110
|
+
res.write(",")
|
|
111
|
+
first = false
|
|
112
|
+
res.write(x.toStr())
|
|
113
|
+
res.write("]")
|
|
114
|
+
return res.toStr()
|
|
115
|
+
|
|
116
|
+
# Elementwise comparison via `==`.
|
|
117
|
+
fun equals(self, other: List[T]) -> Bool =
|
|
118
|
+
a := self
|
|
119
|
+
b := other
|
|
120
|
+
while true
|
|
121
|
+
if Cons(h, t) := a
|
|
122
|
+
if Cons(h2, t2) := b
|
|
123
|
+
if h != h2
|
|
124
|
+
return false
|
|
125
|
+
a = t
|
|
126
|
+
b = t2
|
|
127
|
+
else
|
|
128
|
+
return false
|
|
129
|
+
else
|
|
130
|
+
return b.isEmpty()
|
|
131
|
+
return true
|
|
132
|
+
|
|
133
|
+
# `map`'s loop: a constructor wrapped self call, rewritten to a loop (TRMC).
|
|
134
|
+
fun mapCells(xs: List[A], f: fn(A) -> B) -> List[B] = match xs
|
|
135
|
+
Nil => Nil
|
|
136
|
+
Cons(h, t) => Cons(head: f(h), tail: mapCells(t, f))
|
plum-std/Map.plum
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
module std
|
|
2
|
-
import std/
|
|
2
|
+
import std/Slice
|
|
3
3
|
import std/Array
|
|
4
4
|
import std/Int
|
|
5
5
|
import std/Float
|
plum-std/Os.plum
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
module std
|
|
2
|
-
import std/
|
|
2
|
+
import std/Slice
|
|
3
3
|
import std/Str
|
|
4
4
|
import std/Uuid
|
|
5
5
|
import std/Int
|
|
@@ -164,14 +164,14 @@ primitive Os =
|
|
|
164
164
|
todo
|
|
165
165
|
|
|
166
166
|
# The names of `path`'s directory entries (not full paths).
|
|
167
|
-
fun readdir(path: Str) -> error{NoSuchPath}!
|
|
167
|
+
fun readdir(path: Str) -> error{NoSuchPath}!Slice[Str] =
|
|
168
168
|
if !exists(path)
|
|
169
169
|
return error.NoSuchPath
|
|
170
170
|
# skipping empty names handles both a genuinely empty directory (whose
|
|
171
171
|
# joined `""` would otherwise split into one bogus `""` entry) and
|
|
172
172
|
# `rawReadDir`'s "unreadable" fallback (already ruled out by the `exists`
|
|
173
173
|
# check above) the same way — no directory entry is ever named `""`.
|
|
174
|
-
names :=
|
|
174
|
+
names := Slice[Str]()
|
|
175
175
|
for n := range rawReadDir(path).split("\n", 0)
|
|
176
176
|
if n != ""
|
|
177
177
|
names.push(n)
|
plum-std/Seq.plum
CHANGED
|
@@ -2,18 +2,18 @@ module std
|
|
|
2
2
|
|
|
3
3
|
import std/Int
|
|
4
4
|
import std/Float
|
|
5
|
-
import std/
|
|
5
|
+
import std/Slice
|
|
6
6
|
|
|
7
7
|
# Sequences. `xs.filter()` in a `for` source reads like a method chain but is
|
|
8
8
|
# expanded at the use site into ONE loop: no closure, no `call_indirect`, no
|
|
9
|
-
# intermediate
|
|
9
|
+
# intermediate Slice. See `plum-core/src/forms.rs` for the precise rules.
|
|
10
10
|
#
|
|
11
11
|
# * `Seq[T]` is a compiler primitive; `*T` is sugar for it. A Seq never exists
|
|
12
12
|
# at runtime: it can't be stored, returned or captured, only flow into a
|
|
13
13
|
# `for` source or the next method call.
|
|
14
14
|
# * The methods below are declared with `extend Seq[T]`. `xs.m()` where the type
|
|
15
15
|
# of `xs` has no `m` of its own and is iterable (`length()`/`at(i)`) resolves
|
|
16
|
-
# to `Seq.m`, so `units.filter()` works on a
|
|
16
|
+
# to `Seq.m`, so `units.filter()` works on a Slice, an Array, a Deque, ...
|
|
17
17
|
# * ADAPTERS reshape a sequence and have no result (`take`, `skip`, `stepBy`,
|
|
18
18
|
# `zip`, `reversed`, `chunks`): they may appear anywhere in a chain.
|
|
19
19
|
# * TERMINALS declare a result `-> R` and take the `for` body (`filter`, `map`,
|
|
@@ -26,7 +26,7 @@ import std/List
|
|
|
26
26
|
# `return` returns from the enclosing fn.
|
|
27
27
|
# * `self` of an adapter or terminal that only uses it as a `for` source is the
|
|
28
28
|
# whole upstream chain, which is how a chain fuses. `zip`, `reversed` and
|
|
29
|
-
# `chunks` need random access, so their receiver must be a
|
|
29
|
+
# `chunks` need random access, so their receiver must be a Slice.
|
|
30
30
|
|
|
31
31
|
primitive Seq[T] =
|
|
32
32
|
# ---- adapters ---------------------------------------------------------------
|
|
@@ -59,7 +59,7 @@ primitive Seq[T] =
|
|
|
59
59
|
yield x
|
|
60
60
|
|
|
61
61
|
# `for a, b := range xs.zip(ys)`: pairs up to the shorter length.
|
|
62
|
-
fun* zip(self, ys:
|
|
62
|
+
fun* zip(self, ys: Slice[B]) *(T, B) =
|
|
63
63
|
n := self.length()
|
|
64
64
|
m := ys.length()
|
|
65
65
|
k := if n < m then n else m
|
|
@@ -73,7 +73,7 @@ primitive Seq[T] =
|
|
|
73
73
|
yield self.at(n - 1 - i)
|
|
74
74
|
|
|
75
75
|
# Consecutive pieces of `size` elements (the last may be shorter).
|
|
76
|
-
fun* chunks(self, size: Int) *
|
|
76
|
+
fun* chunks(self, size: Int) *Slice[T] =
|
|
77
77
|
n := self.length()
|
|
78
78
|
start := 0
|
|
79
79
|
while start < n
|
|
@@ -85,16 +85,16 @@ primitive Seq[T] =
|
|
|
85
85
|
# ---- terminals --------------------------------------------------------------
|
|
86
86
|
|
|
87
87
|
# The elements for which the body is true.
|
|
88
|
-
fun* filter(self) *T ->
|
|
88
|
+
fun* filter(self) *T -> Slice[T] =
|
|
89
|
-
out :=
|
|
89
|
+
out := Slice()
|
|
90
90
|
for x := range self
|
|
91
91
|
if yield x
|
|
92
92
|
out.push(x)
|
|
93
93
|
out
|
|
94
94
|
|
|
95
95
|
# The body's value for every element.
|
|
96
|
-
fun* map(self) *T ->
|
|
96
|
+
fun* map(self) *T -> Slice[U] =
|
|
97
|
-
out :=
|
|
97
|
+
out := Slice()
|
|
98
98
|
for x := range self
|
|
99
99
|
v := yield x
|
|
100
100
|
out.push(v)
|
|
@@ -102,7 +102,7 @@ primitive Seq[T] =
|
|
|
102
102
|
|
|
103
103
|
# The first element for which the body is true, or null. Stops there.
|
|
104
104
|
fun* find(self) *T -> T? =
|
|
105
|
-
hit :=
|
|
105
|
+
hit := Slice()
|
|
106
106
|
for x := range self
|
|
107
107
|
if yield x
|
|
108
108
|
hit.push(x)
|
|
@@ -149,8 +149,8 @@ primitive Seq[T] =
|
|
|
149
149
|
yield x
|
|
150
150
|
|
|
151
151
|
# The first `n` elements for which the body is true. Stops there.
|
|
152
|
-
fun* firstN(self, n: Int) *T ->
|
|
152
|
+
fun* firstN(self, n: Int) *T -> Slice[T] =
|
|
153
|
-
out :=
|
|
153
|
+
out := Slice()
|
|
154
154
|
if n > 0
|
|
155
155
|
for x := range self
|
|
156
156
|
if yield x
|
|
@@ -159,9 +159,9 @@ primitive Seq[T] =
|
|
|
159
159
|
break
|
|
160
160
|
out
|
|
161
161
|
|
|
162
|
-
# `xs.take(3).toList()`: the elements as a
|
|
162
|
+
# `xs.take(3).toList()`: the elements as a Slice (no body).
|
|
163
|
-
fun* toList(self) *T ->
|
|
163
|
+
fun* toList(self) *T -> Slice[T] =
|
|
164
|
-
out :=
|
|
164
|
+
out := Slice()
|
|
165
165
|
for x := range self
|
|
166
166
|
yield x
|
|
167
167
|
out.push(x)
|
plum-std/Set.plum
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module std
|
|
2
2
|
import std/Map
|
|
3
|
-
import std/
|
|
3
|
+
import std/Slice
|
|
4
4
|
import std/Int
|
|
5
5
|
import std/Float
|
|
6
6
|
|
|
@@ -32,8 +32,8 @@ record Set[T](map: Map[T, Bool]) =
|
|
|
32
32
|
self.map.clear()
|
|
33
33
|
|
|
34
34
|
# Every member, in insertion order.
|
|
35
|
-
fun toList(self) ->
|
|
35
|
+
fun toList(self) -> Slice[T] =
|
|
36
|
-
result :=
|
|
36
|
+
result := Slice[T]()
|
|
37
37
|
for k := range self.map.keys()
|
|
38
38
|
result.push(k)
|
|
39
39
|
return result
|
plum-std/Slice.plum
CHANGED
|
@@ -9,22 +9,22 @@ import std/Str
|
|
|
9
9
|
# A growable list over a backing `Array[T]`: element `i` is `data[start + i]`,
|
|
10
10
|
# `size` slots are in use and `cap` slots (from `start`) may be written. Capacity
|
|
11
11
|
# doubles when `push` runs out of room, so `push` is O(1) amortized and
|
|
12
|
-
# `get`/`set` are O(1). `slice(i, j)` returns a VIEW: a new `
|
|
12
|
+
# `get`/`set` are O(1). `slice(i, j)` returns a VIEW: a new `Slice` over the same
|
|
13
13
|
# array (no copy) whose `cap` equals its `size`, so `set` on a view writes the
|
|
14
14
|
# parent's storage while a `push` on a view moves it to a fresh array and never
|
|
15
15
|
# overwrites the parent. In-place reordering (`removeAt`, `reverse`) of a list
|
|
16
16
|
# is visible through its views; `sublist` copies. The core operations (`init`, `length`, `get`, `set`,
|
|
17
17
|
# `push`, `pop`, `clear`) sit directly on the `Array` intrinsics; everything
|
|
18
18
|
# else (`map`, `retain`, `reduce`, `chunk`, ...) is ordinary Plum on top of them.
|
|
19
|
-
record
|
|
19
|
+
record Slice[T](data: Array[T], start: Int, size: Int, cap: Int) =
|
|
20
20
|
|
|
21
|
-
# `
|
|
21
|
+
# `Slice[T](1, 2, 3)` (or, for an empty list, `Slice[T]()` — explicit
|
|
22
22
|
# generics required either way, since nothing else in a call this shape
|
|
23
23
|
# pins down `T`) constructs a list via this variadic `init` — see
|
|
24
24
|
# `plum-checker`/`plum-wasm-codegen`'s `ClassCall`/`FnCall` handling, which
|
|
25
|
-
# desugars both call shapes to `
|
|
25
|
+
# desugars both call shapes to `Slice.init(...)`.
|
|
26
|
-
fun init(values: ...T) ->
|
|
26
|
+
fun init(values: ...T) -> Slice[T] =
|
|
27
|
-
l :=
|
|
27
|
+
l := Slice(data: Array[T](), start: 0, size: 0, cap: 0)
|
|
28
28
|
for v := range values
|
|
29
29
|
l.push(v)
|
|
30
30
|
return l
|
|
@@ -48,7 +48,7 @@ record List[T](data: Array[T], start: Int, size: Int, cap: Int) =
|
|
|
48
48
|
self.data.get(self.start + i)
|
|
49
49
|
|
|
50
50
|
# Rewrites every slot with `f(old)` and returns the same list (no new list).
|
|
51
|
-
fun mapInPlace(own self, f: fn(T) -> T) ->
|
|
51
|
+
fun mapInPlace(own self, f: fn(T) -> T) -> Slice[T] =
|
|
52
52
|
i := 0
|
|
53
53
|
while i < self.size
|
|
54
54
|
self.data.set(self.start + i, f(self.data.get(self.start + i)))
|
|
@@ -71,7 +71,7 @@ record List[T](data: Array[T], start: Int, size: Int, cap: Int) =
|
|
|
71
71
|
# Elementwise comparison via `==` — see the caveat on `expectEq` in
|
|
72
72
|
# `libs/std/testing.plum` for what that means when `T` is a class/enum
|
|
73
73
|
# without its own `equals` (reference identity, not structural equality).
|
|
74
|
-
fun equals(self, other:
|
|
74
|
+
fun equals(self, other: Slice[T]) -> Bool =
|
|
75
75
|
if self.length() != other.length()
|
|
76
76
|
return false
|
|
77
77
|
i := 0
|
|
@@ -106,10 +106,10 @@ record List[T](data: Array[T], start: Int, size: Int, cap: Int) =
|
|
|
106
106
|
# A view of the elements at `[i, j)`, clamped into range: shares this list's
|
|
107
107
|
# storage (no copy). `set` on the view changes this list; `push` on the view
|
|
108
108
|
# copies it out first.
|
|
109
|
-
fun slice(self, i: Int, j: Int) ->
|
|
109
|
+
fun slice(self, i: Int, j: Int) -> Slice[T] =
|
|
110
110
|
s := if i < 0 then 0 else if i > self.size then self.size else i
|
|
111
111
|
e := if j > self.size then self.size else if j < s then s else j
|
|
112
|
-
return
|
|
112
|
+
return Slice(data: self.data, start: self.start + s, size: e - s, cap: e - s)
|
|
113
113
|
|
|
114
114
|
# adds the specified elements to the end of the list
|
|
115
115
|
fun add(self, values: ...T) =
|
|
@@ -147,7 +147,7 @@ record List[T](data: Array[T], start: Int, size: Int, cap: Int) =
|
|
|
147
147
|
self.size = 0
|
|
148
148
|
|
|
149
149
|
# reverses the list in place, returning it.
|
|
150
|
-
fun reverse(self) ->
|
|
150
|
+
fun reverse(self) -> Slice[T] =
|
|
151
151
|
i := 0
|
|
152
152
|
j := self.size - 1
|
|
153
153
|
while i < j
|
|
@@ -160,7 +160,7 @@ record List[T](data: Array[T], start: Int, size: Int, cap: Int) =
|
|
|
160
160
|
|
|
161
161
|
# Returns a NEW list with `self`'s elements sorted, using `before(a, b)` as
|
|
162
162
|
# the "does a belong before b" comparator. Insertion sort (O(n^2)).
|
|
163
|
-
fun sort(self, before: fn(T, T) -> Bool) ->
|
|
163
|
+
fun sort(self, before: fn(T, T) -> Bool) -> Slice[T] =
|
|
164
164
|
result := self.sublist(0, self.size)
|
|
165
165
|
i := 1
|
|
166
166
|
while i < result.size
|
|
@@ -213,8 +213,8 @@ record List[T](data: Array[T], start: Int, size: Int, cap: Int) =
|
|
|
213
213
|
|
|
214
214
|
# Returns a new list of the elements at index `[start, end)`, clamped into
|
|
215
215
|
# range (a `start`/`end` outside `[0, length]` is clamped, not an error).
|
|
216
|
-
fun sublist(self, start: Int, end: Int) ->
|
|
216
|
+
fun sublist(self, start: Int, end: Int) -> Slice[T] =
|
|
217
|
-
result :=
|
|
217
|
+
result := Slice[T]()
|
|
218
218
|
s := if start < 0 then 0 else start
|
|
219
219
|
e := if end > self.size then self.size else end
|
|
220
220
|
i := s
|
|
@@ -224,7 +224,7 @@ record List[T](data: Array[T], start: Int, size: Int, cap: Int) =
|
|
|
224
224
|
return result
|
|
225
225
|
|
|
226
226
|
# Every element AFTER the first `n` (a copy; `xs.skip(n)` in a `for` is the lazy form).
|
|
227
|
-
fun drop(self, n: Int) ->
|
|
227
|
+
fun drop(self, n: Int) -> Slice[T] =
|
|
228
228
|
return self.sublist(n, self.size)
|
|
229
229
|
|
|
230
230
|
# Returns one uniformly random element, or fails with `ListWasEmpty` if `self` is empty.
|
|
@@ -234,7 +234,7 @@ record List[T](data: Array[T], start: Int, size: Int, cap: Int) =
|
|
|
234
234
|
return self.data.get(self.start + Random.intBelow(self.size))
|
|
235
235
|
|
|
236
236
|
# Returns a new list with `self`'s elements in random order (Fisher-Yates).
|
|
237
|
-
fun shuffle(self) ->
|
|
237
|
+
fun shuffle(self) -> Slice[T] =
|
|
238
238
|
result := self.sublist(0, self.size)
|
|
239
239
|
i := result.size - 1
|
|
240
240
|
while i > 0
|
|
@@ -246,8 +246,8 @@ record List[T](data: Array[T], start: Int, size: Int, cap: Int) =
|
|
|
246
246
|
return result
|
|
247
247
|
|
|
248
248
|
# Splits into consecutive sublists of at most `size` elements each.
|
|
249
|
-
fun chunk(self, size: Int) ->
|
|
249
|
+
fun chunk(self, size: Int) -> Slice[Slice[T]] =
|
|
250
|
-
result :=
|
|
250
|
+
result := Slice[Slice[T]]()
|
|
251
251
|
if size <= 0
|
|
252
252
|
return result
|
|
253
253
|
i := 0
|
|
@@ -257,7 +257,7 @@ record List[T](data: Array[T], start: Int, size: Int, cap: Int) =
|
|
|
257
257
|
i = i + size
|
|
258
258
|
return result
|
|
259
259
|
|
|
260
|
-
# Grouping by an arbitrary key type would need `Map[K,
|
|
260
|
+
# Grouping by an arbitrary key type would need `Map[K, Slice[T]]`, but
|
|
261
261
|
# `Map.plum` depends on this file. Not implemented.
|
|
262
262
|
fun groupBy(self) =
|
|
263
263
|
todo
|
|
@@ -278,7 +278,7 @@ record List[T](data: Array[T], start: Int, size: Int, cap: Int) =
|
|
|
278
278
|
# ---- regression tests ----
|
|
279
279
|
|
|
280
280
|
test "list add set remove at remove clear reverse all work correctly"
|
|
281
|
-
l :=
|
|
281
|
+
l := Slice(1, 2, 3, 4, 5)
|
|
282
282
|
assert l.length() == 5
|
|
283
283
|
assert l.get(0)! == 1
|
|
284
284
|
assert l.get(4)! == 5
|
|
@@ -295,7 +295,7 @@ test "list add set remove at remove clear reverse all work correctly"
|
|
|
295
295
|
assert l.length() == 0
|
|
296
296
|
|
|
297
297
|
test "removing every node in a loop leaves an empty correctly functioning list"
|
|
298
|
-
l :=
|
|
298
|
+
l := Slice(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
|
299
299
|
i := 0
|
|
300
300
|
while i < 10
|
|
301
301
|
l.removeAt(0)
|
|
@@ -305,44 +305,44 @@ test "removing every node in a loop leaves an empty correctly functioning list"
|
|
|
305
305
|
l.add(42)
|
|
306
306
|
assert l.get(0)! == 42
|
|
307
307
|
|
|
308
|
-
test "
|
|
308
|
+
test "Slice[Int]() with no args runs init() and produces a working empty list"
|
|
309
|
-
l :=
|
|
309
|
+
l := Slice[Int]()
|
|
310
310
|
assert l.length() == 0
|
|
311
311
|
l.add(7)
|
|
312
312
|
assert l.length() == 1
|
|
313
313
|
assert l.get(0)! == 7
|
|
314
314
|
|
|
315
|
-
test "
|
|
315
|
+
test "Slice(values) with positional args runs init(values) directly, infers T from args"
|
|
316
|
-
l :=
|
|
316
|
+
l := Slice(10, 20, 30)
|
|
317
317
|
assert l.length() == 3
|
|
318
318
|
assert l.get(0)! == 10
|
|
319
319
|
assert l.get(2)! == 30
|
|
320
320
|
|
|
321
321
|
test "list equals compares elementwise"
|
|
322
|
-
a =
|
|
322
|
+
a = Slice(1, 2, 3)
|
|
323
|
-
b =
|
|
323
|
+
b = Slice(1, 2, 3)
|
|
324
|
-
c =
|
|
324
|
+
c = Slice(1, 2)
|
|
325
325
|
assert a.equals(b)
|
|
326
326
|
assert !a.equals(c)
|
|
327
327
|
|
|
328
328
|
test "list sort orders elements correctly"
|
|
329
|
-
l =
|
|
329
|
+
l = Slice(3, 1, 4, 1, 5)
|
|
330
330
|
sorted = l.sort(|a, b| a < b)
|
|
331
331
|
assert {sorted.get(0) catch -1000} == 1
|
|
332
332
|
assert {sorted.get(4) catch -1000} == 5
|
|
333
333
|
|
|
334
334
|
test "list join renders elements correctly with separator"
|
|
335
|
-
l =
|
|
335
|
+
l = Slice(1, 2, 3)
|
|
336
336
|
assert l.join(",") == "1,2,3"
|
|
337
337
|
|
|
338
338
|
test "list toStr formats a plain list and a nested list of lists"
|
|
339
|
-
l :=
|
|
339
|
+
l := Slice(1, 2, 3)
|
|
340
340
|
assert l.toStr() == "[1,2,3]"
|
|
341
341
|
chunks := l.chunk(2)
|
|
342
342
|
assert chunks.toStr() == "[[1,2],[3]]"
|
|
343
343
|
|
|
344
344
|
test "chunk splits into sublists of at most size elements each"
|
|
345
|
-
l :=
|
|
345
|
+
l := Slice(1, 2, 3, 4, 5)
|
|
346
346
|
chunks := l.chunk(2)
|
|
347
347
|
assert chunks.length() == 3
|
|
348
348
|
assert chunks.get(0)!.join(",") == "1,2"
|
|
@@ -351,7 +351,7 @@ test "chunk splits into sublists of at most size elements each"
|
|
|
351
351
|
assert l.chunk(0).length() == 0
|
|
352
352
|
|
|
353
353
|
test "list_get_set_push_pop: push 1000, get(999), pop until empty, get on empty is an error"
|
|
354
|
-
l :=
|
|
354
|
+
l := Slice[Int]()
|
|
355
355
|
i := 0
|
|
356
356
|
while i < 1000
|
|
357
357
|
l.push(i)
|
|
@@ -372,7 +372,7 @@ test "list_get_set_push_pop: push 1000, get(999), pop until empty, get on empty
|
|
|
372
372
|
assert {e catch -1} == -1
|
|
373
373
|
|
|
374
374
|
test "list_growth: length stays exact across the doubling boundaries"
|
|
375
|
-
l :=
|
|
375
|
+
l := Slice[Int]()
|
|
376
376
|
i := 0
|
|
377
377
|
while i < 1025
|
|
378
378
|
l.push(i)
|
|
@@ -386,7 +386,7 @@ test "list_growth: length stays exact across the doubling boundaries"
|
|
|
386
386
|
assert l.get(1024)! == 1024
|
|
387
387
|
|
|
388
388
|
test "list of strings keeps its elements through growth"
|
|
389
|
-
l :=
|
|
389
|
+
l := Slice[Str]()
|
|
390
390
|
i := 0
|
|
391
391
|
while i < 40
|
|
392
392
|
l.push("s{i}")
|
|
@@ -394,8 +394,8 @@ test "list of strings keeps its elements through growth"
|
|
|
394
394
|
assert l.get(39)! == "s39"
|
|
395
395
|
assert l.get(16)! == "s16"
|
|
396
396
|
|
|
397
|
-
test "
|
|
397
|
+
test "Slice[Float] push/get round-trips"
|
|
398
|
-
l :=
|
|
398
|
+
l := Slice[Float]()
|
|
399
399
|
i := 0
|
|
400
400
|
while i < 1000
|
|
401
401
|
l.push(Float(i) + 0.5)
|
|
@@ -404,7 +404,7 @@ test "List[Float] push/get round-trips"
|
|
|
404
404
|
assert l.get(999)! == 999.5
|
|
405
405
|
|
|
406
406
|
test "slice is a view: shares storage, no copy"
|
|
407
|
-
xs :=
|
|
407
|
+
xs := Slice(1, 2, 3, 4, 5)
|
|
408
408
|
ys := xs.slice(1, 4)
|
|
409
409
|
assert ys.length() == 3
|
|
410
410
|
assert ys.get(0)! == 2
|
|
@@ -416,7 +416,7 @@ test "slice is a view: shares storage, no copy"
|
|
|
416
416
|
assert ys.get(2)! == 40
|
|
417
417
|
|
|
418
418
|
test "push on a view copies it out and never overwrites the parent"
|
|
419
|
-
xs :=
|
|
419
|
+
xs := Slice(1, 2, 3, 4, 5)
|
|
420
420
|
ys := xs.slice(0, 2)
|
|
421
421
|
ys.push(99)
|
|
422
422
|
assert ys.toStr() == "[1,2,99]"
|
|
@@ -425,7 +425,7 @@ test "push on a view copies it out and never overwrites the parent"
|
|
|
425
425
|
assert xs.get(0)! == 1
|
|
426
426
|
|
|
427
427
|
test "a view of a view, clamping, and empty views"
|
|
428
|
-
xs :=
|
|
428
|
+
xs := Slice(10, 20, 30, 40, 50, 60)
|
|
429
429
|
ys := xs.slice(1, 5).slice(1, 3)
|
|
430
430
|
assert ys.toStr() == "[30,40]"
|
|
431
431
|
assert xs.slice(-3, 2).toStr() == "[10,20]"
|
|
@@ -434,7 +434,7 @@ test "a view of a view, clamping, and empty views"
|
|
|
434
434
|
assert xs.slice(6, 6).length() == 0
|
|
435
435
|
|
|
436
436
|
test "every method indexes from the view's start"
|
|
437
|
-
xs :=
|
|
437
|
+
xs := Slice(9, 1, 5, 3, 7, 8)
|
|
438
438
|
v := xs.slice(1, 5)
|
|
439
439
|
assert v.first()! == 1
|
|
440
440
|
assert v.last()! == 7
|
|
@@ -442,7 +442,7 @@ test "every method indexes from the view's start"
|
|
|
442
442
|
assert v.contains(5) && !v.contains(9) && !v.contains(8)
|
|
443
443
|
assert v.findValue(3)! == 3
|
|
444
444
|
assert v.sort(|a, b| a < b).toStr() == "[1,3,5,7]"
|
|
445
|
-
assert v.equals(
|
|
445
|
+
assert v.equals(Slice(1, 5, 3, 7))
|
|
446
446
|
assert v.sublist(1, 3).toStr() == "[5,3]"
|
|
447
447
|
assert v.drop(2).toStr() == "[3,7]"
|
|
448
448
|
assert v.chunk(3).length() == 2
|
|
@@ -456,7 +456,7 @@ test "every method indexes from the view's start"
|
|
|
456
456
|
assert xs.length() == 6
|
|
457
457
|
|
|
458
458
|
test "in-place reordering through a view is visible in the parent"
|
|
459
|
-
xs :=
|
|
459
|
+
xs := Slice(1, 2, 3, 4, 5)
|
|
460
460
|
v := xs.slice(1, 4)
|
|
461
461
|
_ := v.reverse()
|
|
462
462
|
assert xs.toStr() == "[1,4,3,2,5]"
|
|
@@ -465,7 +465,7 @@ test "in-place reordering through a view is visible in the parent"
|
|
|
465
465
|
assert xs.toStr() == "[1,3,2,2,5]"
|
|
466
466
|
|
|
467
467
|
test "a growing list keeps working after a view is taken"
|
|
468
|
-
xs :=
|
|
468
|
+
xs := Slice[Int]()
|
|
469
469
|
for i := range 20
|
|
470
470
|
xs.push(i)
|
|
471
471
|
v := xs.slice(0, 3)
|
plum-std/Str.plum
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module std
|
|
2
2
|
|
|
3
|
-
import std/
|
|
3
|
+
import std/Slice
|
|
4
4
|
import std/Buffer
|
|
5
5
|
import std/Int
|
|
6
6
|
import std/Float
|
|
@@ -56,9 +56,9 @@ record Str(data: Array[Byte], start: Int, len: Int) =
|
|
|
56
56
|
# with them can land in the MIDDLE of a multi-byte character. This is the
|
|
57
57
|
# same design real languages with an efficient string type use (Go, Rust:
|
|
58
58
|
# a byte buffer, with separate codepoint-aware iteration built on top) —
|
|
59
|
-
# a `
|
|
59
|
+
# a `Slice` of codepoints/bytes would need a heap-allocated node PER
|
|
60
60
|
# CHARACTER with only sequential access, several orders of magnitude
|
|
61
|
-
# slower for something as basic as string length, and `
|
|
61
|
+
# slower for something as basic as string length, and `Slice` is currently
|
|
62
62
|
# broken for any generic-with-args field regardless (see
|
|
63
63
|
# `plum_generic_field_type_gap`). These three methods are that
|
|
64
64
|
# "codepoint-aware iteration built on top" layer:
|
|
@@ -247,8 +247,8 @@ record Str(data: Array[Byte], start: Int, len: Int) =
|
|
|
247
247
|
# `limit`, with the last element holding everything left unsplit — Go's
|
|
248
248
|
# `strings.SplitN` semantics, not JS's (which instead discards anything
|
|
249
249
|
# past the limit). An empty `separator` splits into individual bytes.
|
|
250
|
-
fun split(self, separator: Str, limit: Int) ->
|
|
250
|
+
fun split(self, separator: Str, limit: Int) -> Slice[Str] =
|
|
251
|
-
result :=
|
|
251
|
+
result := Slice[Str]()
|
|
252
252
|
len := self.length()
|
|
253
253
|
sep_len := separator.length()
|
|
254
254
|
if sep_len == 0
|
plum-std/Testing.plum
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module std
|
|
2
2
|
|
|
3
|
-
import std/
|
|
3
|
+
import std/Slice
|
|
4
4
|
import std/Int
|
|
5
5
|
import std/Float
|
|
6
6
|
import std/Str
|
|
@@ -17,11 +17,11 @@ import std/Str
|
|
|
17
17
|
# assertion instead of a raw comparison.
|
|
18
18
|
#
|
|
19
19
|
# `expectArray`/`expectSuccess`/`expectFailure` below take a container
|
|
20
|
-
# type (`
|
|
20
|
+
# type (`Slice[T]`/`!T`) whose OWN generic param is
|
|
21
21
|
# nested inside the param type. An error union is inspected with
|
|
22
22
|
# `catch` (`r catch 0`, `r!`); `expectFailureNamed` also checks which error it is.
|
|
23
23
|
#
|
|
24
|
-
# CAVEAT on `expectEq`/`expectNotEq`/`expectObject`/`
|
|
24
|
+
# CAVEAT on `expectEq`/`expectNotEq`/`expectObject`/`Slice.equals`: Plum's
|
|
25
25
|
# `==`/`!=` have no operator-overloading mechanism. For Int/Str/Bool/Float
|
|
26
26
|
# they do real value comparison (Str is content-compared, not
|
|
27
27
|
# reference-compared). For class/enum instances there is no such override,
|
|
@@ -49,9 +49,9 @@ primitive Testing =
|
|
|
49
49
|
fun expectObject(a: T, b: T) -> Bool =
|
|
50
50
|
a == b
|
|
51
51
|
|
|
52
|
-
# Elementwise `
|
|
52
|
+
# Elementwise `Slice[T]` comparison — see the module-level CAVEAT above for
|
|
53
|
-
# what `
|
|
53
|
+
# what `Slice.equals`'s own `==` means when `T` is a class/enum.
|
|
54
|
-
fun expectArray(a:
|
|
54
|
+
fun expectArray(a: Slice[T], b: Slice[T]) -> Bool =
|
|
55
55
|
a.equals(b)
|
|
56
56
|
|
|
57
57
|
fun expectSuccess(r: !T) -> Bool =
|
|
@@ -79,10 +79,10 @@ test "expectEq and expectNotEq"
|
|
|
79
79
|
assert Testing.expectEq("ab" + "c", "abc")
|
|
80
80
|
assert Testing.expectNotEq(2 + 2, 5)
|
|
81
81
|
|
|
82
|
-
test "list equality via the
|
|
82
|
+
test "list equality via the Slice.equals method"
|
|
83
|
-
a :=
|
|
83
|
+
a := Slice(1, 2, 3)
|
|
84
|
-
b :=
|
|
84
|
+
b := Slice(1, 2, 3)
|
|
85
|
-
c :=
|
|
85
|
+
c := Slice(1, 2)
|
|
86
86
|
assert a.equals(b)
|
|
87
87
|
assert Testing.expectFalse(a.equals(c))
|
|
88
88
|
|
|
@@ -92,14 +92,14 @@ test "error unions are inspected with catch and !"
|
|
|
92
92
|
assert ok! == 5
|
|
93
93
|
assert {bad catch -1} == -1
|
|
94
94
|
|
|
95
|
-
test "expectArray compares two
|
|
95
|
+
test "expectArray compares two Slice[T]s elementwise"
|
|
96
|
-
a :=
|
|
96
|
+
a := Slice(1, 2, 3)
|
|
97
|
-
b :=
|
|
97
|
+
b := Slice(1, 2, 3)
|
|
98
|
-
c :=
|
|
98
|
+
c := Slice(1, 2)
|
|
99
99
|
assert Testing.expectArray(a, b)
|
|
100
100
|
assert Testing.expectFalse(Testing.expectArray(a, c))
|
|
101
|
-
d :=
|
|
101
|
+
d := Slice("x", "y")
|
|
102
|
-
e :=
|
|
102
|
+
e := Slice("x", "y")
|
|
103
103
|
assert Testing.expectArray(d, e)
|
|
104
104
|
|
|
105
105
|
test "expectSuccess/expectFailure check the right variant"
|
plum-std/game/Actions.plum
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
module std
|
|
2
|
-
import std/
|
|
2
|
+
import std/Slice
|
|
3
3
|
import std/Int
|
|
4
4
|
import std/Float
|
|
5
5
|
|
|
@@ -18,9 +18,9 @@ struct RotateBy(radians: Float, duration: Float, elapsed: Float)
|
|
|
18
18
|
|
|
19
19
|
struct Delay(duration: Float, elapsed: Float)
|
|
20
20
|
|
|
21
|
-
record Sequence(steps:
|
|
21
|
+
record Sequence(steps: Slice[Action])
|
|
22
22
|
|
|
23
|
-
record Parallel(steps:
|
|
23
|
+
record Parallel(steps: Slice[Action])
|
|
24
24
|
|
|
25
25
|
# `template` is the action restarted each lap, `current` the lap in progress.
|
|
26
26
|
record Forever(template: Action, current: Action)
|
|
@@ -36,7 +36,7 @@ record Stepped(pose: Pose, left: Action?, spare: Float)
|
|
|
36
36
|
|
|
37
37
|
# The result of stepping actions side by side: the unfinished ones, and the
|
|
38
38
|
# smallest spare time among the finished ones.
|
|
39
|
-
record SteppedAll(pose: Pose, kept:
|
|
39
|
+
record SteppedAll(pose: Pose, kept: Slice[Action], spare: Float)
|
|
40
40
|
|
|
41
41
|
primitive Actions =
|
|
42
42
|
fun moveBy(dx: Float, dy: Float, secs: Float) -> Action =
|
|
@@ -48,10 +48,10 @@ primitive Actions =
|
|
|
48
48
|
fun delay(secs: Float) -> Action =
|
|
49
49
|
Delay(duration: secs, elapsed: 0.0)
|
|
50
50
|
|
|
51
|
-
fun sequence(steps:
|
|
51
|
+
fun sequence(steps: Slice[Action]) -> Action =
|
|
52
52
|
Sequence(steps: steps)
|
|
53
53
|
|
|
54
|
-
fun parallel(steps:
|
|
54
|
+
fun parallel(steps: Slice[Action]) -> Action =
|
|
55
55
|
Parallel(steps: steps)
|
|
56
56
|
|
|
57
57
|
fun forever(a: Action) -> Action =
|
|
@@ -91,7 +91,7 @@ primitive Actions =
|
|
|
91
91
|
s := Actions.stepAction(p, a.steps.at(0), dt)
|
|
92
92
|
rest := a.steps.drop(1)
|
|
93
93
|
if cur := s.left
|
|
94
|
-
kept :=
|
|
94
|
+
kept := Slice[Action]()
|
|
95
95
|
kept.push(cur)
|
|
96
96
|
for i := range rest.length()
|
|
97
97
|
kept.push(rest.at(i))
|
|
@@ -121,9 +121,9 @@ primitive Actions =
|
|
|
121
121
|
again: Action := Forever(template: a.template, current: lap)
|
|
122
122
|
Stepped(pose: r.pose, left: again, spare: 0.0)
|
|
123
123
|
|
|
124
|
-
fun stepAll(p: Pose, acts:
|
|
124
|
+
fun stepAll(p: Pose, acts: Slice[Action], dt: Float) -> SteppedAll =
|
|
125
125
|
pose := p
|
|
126
|
-
kept :=
|
|
126
|
+
kept := Slice[Action]()
|
|
127
127
|
spare := dt
|
|
128
128
|
for i := range acts.length()
|
|
129
129
|
s := Actions.stepAction(pose, acts.at(i), dt)
|
|
@@ -148,20 +148,20 @@ fun actPose0() -> Pose =
|
|
|
148
148
|
Pose(x: 0.0, y: 0.0, rotation: 0.0, scaleX: 1.0, scaleY: 1.0)
|
|
149
149
|
|
|
150
150
|
test "stepAll moves partway and keeps the action"
|
|
151
|
-
r := Actions.stepAll(actPose0(),
|
|
151
|
+
r := Actions.stepAll(actPose0(), Slice(Actions.moveBy(10.0, 0.0, 1.0)), 0.5)
|
|
152
152
|
assert actNear(r.pose.x, 5.0)
|
|
153
153
|
assert r.kept.length() == 1
|
|
154
154
|
assert actNear(r.spare, 0.0)
|
|
155
155
|
|
|
156
156
|
test "stepAll finishes every action and reports no spare"
|
|
157
|
-
r := Actions.stepAll(actPose0(),
|
|
157
|
+
r := Actions.stepAll(actPose0(), Slice(Actions.moveBy(10.0, 0.0, 1.0), Actions.rotateBy(1.0, 0.5)), 1.0)
|
|
158
158
|
assert actNear(r.pose.x, 10.0)
|
|
159
159
|
assert actNear(r.pose.rotation, 1.0)
|
|
160
160
|
assert r.kept.length() == 0
|
|
161
161
|
assert actNear(r.spare, 0.0)
|
|
162
162
|
|
|
163
163
|
test "stepAll with no actions is a no-op"
|
|
164
|
-
r := Actions.stepAll(actPose0(),
|
|
164
|
+
r := Actions.stepAll(actPose0(), Slice[Action](), 0.016)
|
|
165
165
|
assert r.kept.length() == 0
|
|
166
166
|
assert actNear(r.spare, 0.016)
|
|
167
167
|
assert actNear(r.pose.x, 0.0)
|
|
@@ -184,7 +184,7 @@ test "forever_rotate_no_drift"
|
|
|
184
184
|
assert actNear(p.rotation, 10.0)
|
|
185
185
|
|
|
186
186
|
test "sequence_spills_spare_time"
|
|
187
|
-
a := Actions.sequence(
|
|
187
|
+
a := Actions.sequence(Slice(Actions.moveBy(10.0, 0.0, 0.5), Actions.moveBy(0.0, 10.0, 0.5)))
|
|
188
188
|
r := Actions.stepAction(actPose0(), a, 0.75)
|
|
189
189
|
assert actNear(r.pose.x, 10.0)
|
|
190
190
|
assert actNear(r.pose.y, 5.0)
|
|
@@ -195,12 +195,12 @@ test "sequence_spills_spare_time"
|
|
|
195
195
|
assert false
|
|
196
196
|
|
|
197
197
|
test "empty_sequence_finishes_with_full_spare"
|
|
198
|
-
r := Actions.stepAction(actPose0(), Actions.sequence(
|
|
198
|
+
r := Actions.stepAction(actPose0(), Actions.sequence(Slice[Action]()), 0.3)
|
|
199
199
|
assert r.left == null
|
|
200
200
|
assert actNear(r.spare, 0.3)
|
|
201
201
|
|
|
202
202
|
test "parallel_spare_is_min_of_finished"
|
|
203
|
-
r := Actions.stepAction(actPose0(), Actions.parallel(
|
|
203
|
+
r := Actions.stepAction(actPose0(), Actions.parallel(Slice(Actions.delay(0.2), Actions.delay(0.5))), 1.0)
|
|
204
204
|
assert r.left == null
|
|
205
205
|
assert actNear(r.spare, 0.5)
|
|
206
206
|
|
|
@@ -209,7 +209,7 @@ test "forever_zero_length_template_terminates"
|
|
|
209
209
|
assert r.left != null
|
|
210
210
|
|
|
211
211
|
test "forever of a nested sequence keeps looping"
|
|
212
|
-
a := Actions.forever(Actions.sequence(
|
|
212
|
+
a := Actions.forever(Actions.sequence(Slice(Actions.moveBy(1.0, 0.0, 0.1), Actions.delay(0.1))))
|
|
213
213
|
p := actPose0()
|
|
214
214
|
for i := range 100
|
|
215
215
|
r := Actions.stepAction(p, a, 0.05)
|
plum-std/game/Picture.plum
CHANGED
|
@@ -2,6 +2,7 @@ module std
|
|
|
2
2
|
import std/Int
|
|
3
3
|
import std/Float
|
|
4
4
|
import std/Str
|
|
5
|
+
import std/Slice
|
|
5
6
|
import std/List
|
|
6
7
|
import std/Math
|
|
7
8
|
import std/game/Vec2
|
|
@@ -12,7 +13,7 @@ import std/game/Game
|
|
|
12
13
|
# A recursive picture: a SceneNode is plain data (shapes, sprites, text, groups
|
|
13
14
|
# with a transform, filters) and `Picture` is a set of structurally recursive
|
|
14
15
|
# functions that draw it, measure it and hit-test it. Children are a recursive
|
|
15
|
-
# `
|
|
16
|
+
# `List[SceneNode]` list, so every operation recurses over SceneNode and List[SceneNode], passing
|
|
16
17
|
# the parent matrix and alpha down as arguments. Nothing here mutates the picture.
|
|
17
18
|
# `Compose(first, then)` applies `then` first, then `first` (`first` is the outermost).
|
|
18
19
|
|
|
@@ -36,7 +37,7 @@ enum Effect =
|
|
|
36
37
|
# `target` bounds what the filter can show: child pixels beyond it are cut off.
|
|
37
38
|
# A negative target draws nothing.
|
|
38
39
|
enum SceneNode =
|
|
39
|
-
| Group(transform: Transform, children:
|
|
40
|
+
| Group(transform: Transform, children: List[SceneNode])
|
|
40
41
|
| Filter(effect: Effect, child: SceneNode, target: Int)
|
|
41
42
|
| Sprite(tex: Int, x: Float, y: Float, w: Float, h: Float, srcX: Float, srcY: Float, srcW: Float, srcH: Float, tint: Color)
|
|
42
43
|
| Text(text: Str, x: Float, y: Float, size: Float, color: Color)
|
|
@@ -44,7 +45,6 @@ enum SceneNode =
|
|
|
44
45
|
| CircleShape(cx: Float, cy: Float, r: Float, color: Color)
|
|
45
46
|
| LineShape(x1: Float, y1: Float, x2: Float, y2: Float, thickness: Float, color: Color)
|
|
46
47
|
|
|
47
|
-
enum Nodes = End | More(head: SceneNode, tail: Nodes)
|
|
48
48
|
|
|
49
49
|
struct Rect(x: Float, y: Float, w: Float, h: Float)
|
|
50
50
|
|
|
@@ -91,9 +91,9 @@ fun picBounds(node: SceneNode, m: Affine) -> Rect? = match node
|
|
|
91
91
|
Group(tr, kids) => picBoundsKids(kids, m.mul(picMatrix(tr)), null)
|
|
92
92
|
Filter(_, child, _) => picBounds(child, m)
|
|
93
93
|
|
|
94
|
-
fun picBoundsKids(kids:
|
|
94
|
+
fun picBoundsKids(kids: List[SceneNode], m: Affine, acc: Rect?) -> Rect? = match kids
|
|
95
|
-
|
|
95
|
+
Nil => acc
|
|
96
|
-
|
|
96
|
+
Cons(head, tail) =>
|
|
97
97
|
if cr := picBounds(head, m)
|
|
98
98
|
picBoundsKids(tail, m, picUnion(acc, cr))
|
|
99
99
|
else
|
|
@@ -112,21 +112,21 @@ fun picNearLine(x1: Float, y1: Float, x2: Float, y2: Float, thickness: Float, px
|
|
|
112
112
|
qy := y1 + dy * tc
|
|
113
113
|
Math.hypot(px - qx, py - qy) <= thickness.max(1.0) / 2.0
|
|
114
114
|
|
|
115
|
-
fun picLeaf(hit: Bool) ->
|
|
115
|
+
fun picLeaf(hit: Bool) -> Slice[Int]? =
|
|
116
116
|
if hit
|
|
117
|
-
return
|
|
117
|
+
return Slice[Int]()
|
|
118
118
|
null
|
|
119
119
|
|
|
120
120
|
# `i` followed by the path `sub`.
|
|
121
|
-
fun picPrepend(i: Int, sub:
|
|
121
|
+
fun picPrepend(i: Int, sub: Slice[Int]) -> Slice[Int] =
|
|
122
|
-
path :=
|
|
122
|
+
path := Slice(i)
|
|
123
123
|
for j := range sub.length()
|
|
124
124
|
path.push(sub.at(j))
|
|
125
125
|
path
|
|
126
126
|
|
|
127
127
|
# The child indices from `node` down to the hit leaf, or null on a miss. A leaf
|
|
128
128
|
# that is hit is an empty path. (px, py) is in `node`'s own space.
|
|
129
|
-
fun picHit(node: SceneNode, px: Float, py: Float) ->
|
|
129
|
+
fun picHit(node: SceneNode, px: Float, py: Float) -> Slice[Int]? = match node
|
|
130
130
|
RectShape(x, y, w, h, _) => picLeaf(picInBox(x, y, w, h, px, py))
|
|
131
131
|
CircleShape(cx, cy, r, _) => picLeaf(Math.hypot(px - cx, py - cy) <= r)
|
|
132
132
|
LineShape(x1, y1, x2, y2, th, _) => picLeaf(picNearLine(x1, y1, x2, y2, th, px, py))
|
|
@@ -146,9 +146,9 @@ fun picHit(node: SceneNode, px: Float, py: Float) -> List[Int]? = match node
|
|
|
146
146
|
|
|
147
147
|
# Tries the later children first (they are drawn on top); `i` is the index of
|
|
148
148
|
# `kids`' head. The first hit found from the back wins.
|
|
149
|
-
fun picHitKids(kids:
|
|
149
|
+
fun picHitKids(kids: List[SceneNode], i: Int, px: Float, py: Float) -> Slice[Int]? = match kids
|
|
150
|
-
|
|
150
|
+
Nil => null
|
|
151
|
-
|
|
151
|
+
Cons(head, tail) =>
|
|
152
152
|
if later := picHitKids(tail, i + 1, px, py)
|
|
153
153
|
later
|
|
154
154
|
else
|
|
@@ -181,9 +181,9 @@ fun picDraw(node: SceneNode, m: Affine, alpha: Float, target: Int) = match node
|
|
|
181
181
|
Group(tr, kids) => picDrawKids(kids, m.mul(picMatrix(tr)), alpha, target)
|
|
182
182
|
Filter(effect, child, tex) => picFilter(effect, child, tex, m, alpha, target)
|
|
183
183
|
|
|
184
|
-
fun picDrawKids(kids:
|
|
184
|
+
fun picDrawKids(kids: List[SceneNode], m: Affine, alpha: Float, target: Int) = match kids
|
|
185
|
-
|
|
185
|
+
Nil => 0
|
|
186
|
-
|
|
186
|
+
Cons(head, tail) =>
|
|
187
187
|
picDraw(head, m, alpha, target)
|
|
188
188
|
picDrawKids(tail, m, alpha, target)
|
|
189
189
|
|
|
@@ -224,10 +224,10 @@ primitive Picture =
|
|
|
224
224
|
Rect(x: 0.0, y: 0.0, w: 0.0, h: 0.0)
|
|
225
225
|
|
|
226
226
|
# Child indices from the root down to the topmost hit leaf; empty on a miss.
|
|
227
|
-
fun hitPath(node: SceneNode, x: Float, y: Float) ->
|
|
227
|
+
fun hitPath(node: SceneNode, x: Float, y: Float) -> Slice[Int] =
|
|
228
228
|
if p := picHit(node, x, y)
|
|
229
229
|
return p
|
|
230
|
-
|
|
230
|
+
Slice[Int]()
|
|
231
231
|
|
|
232
232
|
fun picNear(a: Float, b: Float) -> Bool =
|
|
233
233
|
{a - b}.abs() < 1e-6
|
|
@@ -236,7 +236,7 @@ fun picRect(x: Float, y: Float, w: Float, h: Float, c: Color) -> SceneNode =
|
|
|
236
236
|
RectShape(x: x, y: y, w: w, h: h, color: c)
|
|
237
237
|
|
|
238
238
|
test "bounds_translate_scale"
|
|
239
|
-
g := Group(transform: Compose(first: Translate(dx: 5.0, dy: 5.0), then: Scale(sx: 2.0, sy: 2.0)), children:
|
|
239
|
+
g := Group(transform: Compose(first: Translate(dx: 5.0, dy: 5.0), then: Scale(sx: 2.0, sy: 2.0)), children: Cons(head: picRect(0.0, 0.0, 10.0, 10.0, Color.Red), tail: Nil))
|
|
240
240
|
r := Picture.bounds(g)
|
|
241
241
|
assert picNear(r.x, 5.0)
|
|
242
242
|
assert picNear(r.y, 5.0)
|
|
@@ -244,7 +244,7 @@ test "bounds_translate_scale"
|
|
|
244
244
|
assert picNear(r.h, 20.0)
|
|
245
245
|
|
|
246
246
|
test "bounds_rotate_quarter"
|
|
247
|
-
g := Group(transform: Rotate(radians: 1.5707963267948966), children:
|
|
247
|
+
g := Group(transform: Rotate(radians: 1.5707963267948966), children: Cons(head: picRect(0.0, 0.0, 10.0, 4.0, Color.Red), tail: Nil))
|
|
248
248
|
r := Picture.bounds(g)
|
|
249
249
|
assert picNear(r.x, 0.0 - 4.0)
|
|
250
250
|
assert picNear(r.y, 0.0)
|
|
@@ -252,8 +252,8 @@ test "bounds_rotate_quarter"
|
|
|
252
252
|
assert picNear(r.h, 10.0)
|
|
253
253
|
|
|
254
254
|
test "hit_path_nested"
|
|
255
|
-
inner := Group(transform: Translate(dx: 10.0, dy: 0.0), children:
|
|
255
|
+
inner := Group(transform: Translate(dx: 10.0, dy: 0.0), children: Cons(head: picRect(0.0, 0.0, 10.0, 10.0, Color.Blue), tail: Nil))
|
|
256
|
-
g := Group(transform: Identity, children:
|
|
256
|
+
g := Group(transform: Identity, children: Cons(head: picRect(0.0, 0.0, 10.0, 10.0, Color.Red), tail: Cons(head: inner, tail: Nil)))
|
|
257
257
|
p := Picture.hitPath(g, 15.0, 5.0)
|
|
258
258
|
assert p.length() == 2
|
|
259
259
|
assert p.at(0) == 1
|
|
@@ -263,13 +263,13 @@ test "hit_path_nested"
|
|
|
263
263
|
assert q.at(0) == 0
|
|
264
264
|
|
|
265
265
|
test "hit_path_last_child_wins"
|
|
266
|
-
g := Group(transform: Identity, children:
|
|
266
|
+
g := Group(transform: Identity, children: Cons(head: picRect(0.0, 0.0, 10.0, 10.0, Color.Red), tail: Cons(head: picRect(0.0, 0.0, 10.0, 10.0, Color.Blue), tail: Nil)))
|
|
267
267
|
p := Picture.hitPath(g, 5.0, 5.0)
|
|
268
268
|
assert p.length() == 1
|
|
269
269
|
assert p.at(0) == 1
|
|
270
270
|
|
|
271
271
|
test "hit_miss_empty"
|
|
272
|
-
g := Group(transform: Identity, children:
|
|
272
|
+
g := Group(transform: Identity, children: Cons(head: picRect(0.0, 0.0, 10.0, 10.0, Color.Red), tail: Nil))
|
|
273
273
|
assert Picture.hitPath(g, 50.0, 50.0).length() == 0
|
|
274
274
|
|
|
275
275
|
test "filter_bounds_is_child_bounds"
|
plum-std/game/PureScene.plum
CHANGED
|
@@ -2,7 +2,7 @@ module std
|
|
|
2
2
|
import std/Int
|
|
3
3
|
import std/Float
|
|
4
4
|
import std/Str
|
|
5
|
-
import std/
|
|
5
|
+
import std/Slice
|
|
6
6
|
import std/Math
|
|
7
7
|
import std/game/Vec2
|
|
8
8
|
import std/game/Color
|
|
@@ -39,7 +39,7 @@ enum UiKind =
|
|
|
39
39
|
record UiEvent(id: Int, kind: UiKind)
|
|
40
40
|
|
|
41
41
|
# The actor after handling an event, and the events it raised.
|
|
42
|
-
own record Handled(actor: Actor, events:
|
|
42
|
+
own record Handled(actor: Actor, events: Slice[UiEvent]) =
|
|
43
43
|
|
|
44
44
|
# The actor, moved out; read `events` first, they stay behind.
|
|
45
45
|
fun intoActor(own self) -> Actor =
|
|
@@ -55,13 +55,13 @@ own trait Actor(
|
|
|
55
55
|
color: Color,
|
|
56
56
|
visible: Bool,
|
|
57
57
|
touchable: Touchable,
|
|
58
|
-
actions:
|
|
58
|
+
actions: Slice[Action]) =
|
|
59
59
|
|
|
60
60
|
# This actor after `dt` seconds of its own behaviour; the default is itself.
|
|
61
61
|
fun act(own self, dt: Float) -> Actor = self
|
|
62
62
|
|
|
63
63
|
# The children of a group; null for a leaf.
|
|
64
|
-
fun kids(self) ->
|
|
64
|
+
fun kids(self) -> Slice[Actor]? = null
|
|
65
65
|
|
|
66
66
|
# Paints this actor only, at its stage transform `m` with the combined `tint`.
|
|
67
67
|
fun draw(self, m: Affine, tint: Color) =
|
|
@@ -73,14 +73,14 @@ own trait Actor(
|
|
|
73
73
|
|
|
74
74
|
# Handles a pointer event and returns the new actor with the events it raised.
|
|
75
75
|
fun onPointer(own self, e: Pointer) -> Handled =
|
|
76
|
-
Handled(actor: self, events:
|
|
76
|
+
Handled(actor: self, events: Slice[UiEvent]())
|
|
77
77
|
|
|
78
78
|
own record Group(
|
|
79
79
|
Actor(id, x, y, width, height, originX, originY, scaleX, scaleY, rotation, color, visible, touchable, actions),
|
|
80
|
-
children:
|
|
80
|
+
children: Slice[Actor]) =
|
|
81
81
|
|
|
82
82
|
|
|
83
|
-
override fun kids(self) ->
|
|
83
|
+
override fun kids(self) -> Slice[Actor]? = self.children
|
|
84
84
|
|
|
85
85
|
own record Sprite(
|
|
86
86
|
Actor(id, x, y, width, height, originX, originY, scaleX, scaleY, rotation, color, visible, touchable, actions),
|
|
@@ -114,7 +114,7 @@ own record Button(
|
|
|
114
114
|
pureDrawText(m.mul(Affine(a: 1.0, b: 0.0, c: 0.0, d: 1.0, e: ox, f: oy)), tint, self.text, size)
|
|
115
115
|
|
|
116
116
|
override fun onPointer(own self, e: Pointer) -> Handled =
|
|
117
|
-
ev :=
|
|
117
|
+
ev := Slice[UiEvent]()
|
|
118
118
|
id := self.id
|
|
119
119
|
wasOver := self.over
|
|
120
120
|
wasPressed := self.pressed
|
|
@@ -178,37 +178,37 @@ primitive PureScene =
|
|
|
178
178
|
PureScene.drawTree(g.at(i), m, t)
|
|
179
179
|
|
|
180
180
|
# The children of a group, or null for a leaf.
|
|
181
|
-
fun childrenOf(a: Actor) ->
|
|
181
|
+
fun childrenOf(a: Actor) -> Slice[Actor]? =
|
|
182
182
|
a.kids()
|
|
183
183
|
|
|
184
|
-
fun group(id: Int, x: Float, y: Float, w: Float, h: Float, children:
|
|
184
|
+
fun group(id: Int, x: Float, y: Float, w: Float, h: Float, children: Slice[Actor]) -> Group =
|
|
185
185
|
Group(
|
|
186
186
|
id: id, x: x, y: y, width: w, height: h, originX: 0.0, originY: 0.0,
|
|
187
187
|
scaleX: 1.0, scaleY: 1.0, rotation: 0.0, color: Color.White, visible: true,
|
|
188
|
-
touchable: Enabled, actions:
|
|
188
|
+
touchable: Enabled, actions: Slice[Action](), children: children)
|
|
189
189
|
|
|
190
190
|
fun sprite(id: Int, tex: Int, x: Float, y: Float, w: Float, h: Float) -> Sprite =
|
|
191
191
|
Sprite(
|
|
192
192
|
id: id, x: x, y: y, width: w, height: h, originX: 0.0, originY: 0.0,
|
|
193
193
|
scaleX: 1.0, scaleY: 1.0, rotation: 0.0, color: Color.White, visible: true,
|
|
194
|
-
touchable: Enabled, actions:
|
|
194
|
+
touchable: Enabled, actions: Slice[Action](),
|
|
195
195
|
tex: tex, srcX: 0.0, srcY: 0.0, srcW: 0.0, srcH: 0.0)
|
|
196
196
|
|
|
197
197
|
fun label(id: Int, text: Str, x: Float, y: Float, size: Float) -> Label =
|
|
198
198
|
Label(
|
|
199
199
|
id: id, x: x, y: y, width: size * 0.6 * Float(text.length()), height: size, originX: 0.0, originY: 0.0,
|
|
200
200
|
scaleX: 1.0, scaleY: 1.0, rotation: 0.0, color: Color.White, visible: true,
|
|
201
|
-
touchable: Disabled, actions:
|
|
201
|
+
touchable: Disabled, actions: Slice[Action](), text: text, size: size)
|
|
202
202
|
|
|
203
203
|
fun button(id: Int, text: Str, upTex: Int, downTex: Int, x: Float, y: Float, w: Float, h: Float) -> Button =
|
|
204
204
|
Button(
|
|
205
205
|
id: id, x: x, y: y, width: w, height: h, originX: 0.0, originY: 0.0,
|
|
206
206
|
scaleX: 1.0, scaleY: 1.0, rotation: 0.0, color: Color.White, visible: true,
|
|
207
|
-
touchable: Enabled, actions:
|
|
207
|
+
touchable: Enabled, actions: Slice[Action](),
|
|
208
208
|
text: text, upTex: upTex, downTex: downTex, pressed: false, over: false)
|
|
209
209
|
|
|
210
|
-
fun psKinds(h: Handled) ->
|
|
210
|
+
fun psKinds(h: Handled) -> Slice[UiKind] =
|
|
211
|
-
out :=
|
|
211
|
+
out := Slice[UiKind]()
|
|
212
212
|
for i := range h.events.length()
|
|
213
213
|
out.push(h.events.at(i).kind)
|
|
214
214
|
out
|
plum-std/game/Scene.plum
CHANGED
|
@@ -2,7 +2,7 @@ module std
|
|
|
2
2
|
import std/Int
|
|
3
3
|
import std/Float
|
|
4
4
|
import std/Str
|
|
5
|
-
import std/
|
|
5
|
+
import std/Slice
|
|
6
6
|
import std/Math
|
|
7
7
|
import std/game/Vec2
|
|
8
8
|
import std/game/Color
|
|
@@ -50,9 +50,9 @@ trait Actor(
|
|
|
50
50
|
rotation: Float,
|
|
51
51
|
color: Color,
|
|
52
52
|
visible: Bool,
|
|
53
|
-
children:
|
|
53
|
+
children: Slice[Actor],
|
|
54
54
|
parent: Actor?,
|
|
55
|
-
scratch:
|
|
55
|
+
scratch: Slice[Actor],
|
|
56
56
|
cull: Bool, cullX: Float, cullY: Float, cullW: Float, cullH: Float) =
|
|
57
57
|
|
|
58
58
|
# Runs for every construction, before the record's own `init`. A record's header
|
|
@@ -69,9 +69,9 @@ trait Actor(
|
|
|
69
69
|
self.rotation = 0.0
|
|
70
70
|
self.color = Color.White
|
|
71
71
|
self.visible = true
|
|
72
|
-
self.children =
|
|
72
|
+
self.children = Slice[Actor]()
|
|
73
73
|
self.parent = null
|
|
74
|
-
self.scratch =
|
|
74
|
+
self.scratch = Slice[Actor]()
|
|
75
75
|
self.cull = false
|
|
76
76
|
self.cullX = 0.0
|
|
77
77
|
self.cullY = 0.0
|
|
@@ -271,7 +271,7 @@ trait Actor(
|
|
|
271
271
|
if !self.culls(kid)
|
|
272
272
|
kid.drawTree(m, a)
|
|
273
273
|
|
|
274
|
-
fun collect(self, parent: Affine, alpha: Float, out:
|
|
274
|
+
fun collect(self, parent: Affine, alpha: Float, out: Slice[Drawn]) =
|
|
275
275
|
if !self.visible
|
|
276
276
|
return
|
|
277
277
|
m := parent * self.local()
|
|
@@ -388,7 +388,7 @@ fun sortRunEnd(d: Array[Actor], base: Int, i: Int, n: Int, before: fn(Actor, Act
|
|
|
388
388
|
# Merges the sorted runs [lo, a) and [a, b) of `d`, stably. The left elements that
|
|
389
389
|
# already precede the first right one and the right elements that already follow
|
|
390
390
|
# the last left one are found by binary search and left alone.
|
|
391
|
-
fun sortMerge(scratch:
|
|
391
|
+
fun sortMerge(scratch: Slice[Actor], d: Array[Actor], base: Int, lo: Int, a: Int, b: Int, before: fn(Actor, Actor) -> Bool) =
|
|
392
392
|
first := d.get(base + a)
|
|
393
393
|
l := lo
|
|
394
394
|
h := a
|
|
@@ -450,8 +450,8 @@ record Drawn(node: Actor, m: Affine, alpha: Float)
|
|
|
450
450
|
|
|
451
451
|
# What `draw` would draw, in order: a parent before its children, siblings in
|
|
452
452
|
# list order, invisible and culled subtrees left out.
|
|
453
|
-
fun flatten(root: Actor) ->
|
|
453
|
+
fun flatten(root: Actor) -> Slice[Drawn] =
|
|
454
|
-
out :=
|
|
454
|
+
out := Slice[Drawn]()
|
|
455
455
|
root.collect(Affine.Identity, 1.0, out)
|
|
456
456
|
out
|
|
457
457
|
|
|
@@ -709,7 +709,7 @@ test "sorting a nearly sorted list takes about n comparisons and reuses the scra
|
|
|
709
709
|
g := Group()
|
|
710
710
|
for i := range 5000
|
|
711
711
|
g.add(Rect.create(1.0, 1.0, Color.Red).at(0.0, Float(i)))
|
|
712
|
-
c :=
|
|
712
|
+
c := Slice(0)
|
|
713
713
|
less: fn(Actor, Actor) -> Bool := |a, b|
|
|
714
714
|
_ := c.set(0, c.at(0) + 1)
|
|
715
715
|
a.y < b.y
|
plum-std/game/SceneInput.plum
CHANGED
|
@@ -2,7 +2,7 @@ module std
|
|
|
2
2
|
import std/Int
|
|
3
3
|
import std/Float
|
|
4
4
|
import std/Str
|
|
5
|
-
import std/
|
|
5
|
+
import std/Slice
|
|
6
6
|
import std/game/Vec2
|
|
7
7
|
import std/game/Color
|
|
8
8
|
import std/game/Affine
|
|
@@ -26,9 +26,9 @@ enum InputKind =
|
|
|
26
26
|
# A pointer input in screen coordinates.
|
|
27
27
|
record Input(kind: InputKind, x: Float, y: Float)
|
|
28
28
|
|
|
29
|
-
record Capture(path:
|
|
29
|
+
record Capture(path: Slice[Int], id: Int)
|
|
30
30
|
|
|
31
|
-
own record StageHandled(stage: Stage, events:
|
|
31
|
+
own record StageHandled(stage: Stage, events: Slice[UiEvent]) =
|
|
32
32
|
|
|
33
33
|
# The stage, moved out; read `events` first, they stay behind.
|
|
34
34
|
fun intoStage(own self) -> Stage =
|
|
@@ -39,7 +39,7 @@ own record Stage(root: Actor, camera: Affine, captured: Capture?) =
|
|
|
39
39
|
|
|
40
40
|
# Handles one input: routes it, returns the new stage and the events raised.
|
|
41
41
|
fun input(own self, e: Input) -> StageHandled =
|
|
42
|
-
none :=
|
|
42
|
+
none := Slice[UiEvent]()
|
|
43
43
|
inv := self.camera.invert()
|
|
44
44
|
if inv == null
|
|
45
45
|
return StageHandled(stage: self, events: none)
|
|
@@ -74,16 +74,16 @@ own record Stage(root: Actor, camera: Affine, captured: Capture?) =
|
|
|
74
74
|
self.captured = c
|
|
75
75
|
self
|
|
76
76
|
|
|
77
|
-
fun deliver(own self, path:
|
|
77
|
+
fun deliver(own self, path: Slice[Int], p: Pointer, c: Capture?) -> StageHandled =
|
|
78
|
-
events :=
|
|
78
|
+
events := Slice[UiEvent]()
|
|
79
79
|
self.root = SceneTree.routeInto(self.root, path, 0, p, events)
|
|
80
80
|
self.captured = c
|
|
81
81
|
StageHandled(stage: self, events: events)
|
|
82
82
|
|
|
83
83
|
# Handles the inputs in order, then advances the tree by `dt`.
|
|
84
|
-
fun frame(own self, dt: Float, inputs:
|
|
84
|
+
fun frame(own self, dt: Float, inputs: Slice[Input]) -> StageHandled =
|
|
85
85
|
cur := self
|
|
86
|
-
out :=
|
|
86
|
+
out := Slice[UiEvent]()
|
|
87
87
|
for i := range inputs.length()
|
|
88
88
|
h := cur.input(inputs.at(i))
|
|
89
89
|
for j := range h.events.length()
|
|
@@ -96,7 +96,7 @@ own record Stage(root: Actor, camera: Affine, captured: Capture?) =
|
|
|
96
96
|
fun draw(self) =
|
|
97
97
|
PureScene.drawTree(self.root, self.camera, Color.White)
|
|
98
98
|
|
|
99
|
-
fun inputIsPrefix(a:
|
|
99
|
+
fun inputIsPrefix(a: Slice[Int], b: Slice[Int]) -> Bool =
|
|
100
100
|
if a.length() > b.length()
|
|
101
101
|
return false
|
|
102
102
|
for i := range a.length()
|
|
@@ -110,10 +110,10 @@ primitive SceneInput =
|
|
|
110
110
|
|
|
111
111
|
# This frame's pointer inputs from the host mouse: a press, a release, or a
|
|
112
112
|
# move. `wasDown` is whether the button was down last frame.
|
|
113
|
-
fun pollPointer(wasDown: Bool) ->
|
|
113
|
+
fun pollPointer(wasDown: Bool) -> Slice[Input] =
|
|
114
114
|
m := mousePos()
|
|
115
115
|
down := isMouseDown(0)
|
|
116
|
-
out :=
|
|
116
|
+
out := Slice[Input]()
|
|
117
117
|
if down && !wasDown
|
|
118
118
|
out.push(Input(kind: PointerDown, x: m.x, y: m.y))
|
|
119
119
|
else if !down && wasDown
|
|
@@ -124,7 +124,7 @@ primitive SceneInput =
|
|
|
124
124
|
|
|
125
125
|
fun inBtnTree() -> Actor =
|
|
126
126
|
b: Actor := PureScene.button(5, "Go", 0, 1, 10.0, 10.0, 100.0, 40.0)
|
|
127
|
-
kids :=
|
|
127
|
+
kids := Slice[Actor]()
|
|
128
128
|
kids.push(b)
|
|
129
129
|
g: Actor := PureScene.group(1, 0.0, 0.0, 800.0, 600.0, kids)
|
|
130
130
|
g
|
|
@@ -153,7 +153,7 @@ fun inUp(x: Float, y: Float) -> Input =
|
|
|
153
153
|
fun inMove(x: Float, y: Float) -> Input =
|
|
154
154
|
Input(kind: PointerMove, x: x, y: y)
|
|
155
155
|
|
|
156
|
-
fun inClicks(events:
|
|
156
|
+
fun inClicks(events: Slice[UiEvent]) -> Int =
|
|
157
157
|
n := 0
|
|
158
158
|
for i := range events.length()
|
|
159
159
|
if events.at(i).kind == Click
|
|
@@ -162,7 +162,7 @@ fun inClicks(events: List[UiEvent]) -> Int =
|
|
|
162
162
|
|
|
163
163
|
fun inXformTree() -> Actor =
|
|
164
164
|
b: Actor := PureScene.button(5, "Go", 0, 1, 10.0, 10.0, 100.0, 40.0)
|
|
165
|
-
kids :=
|
|
165
|
+
kids := Slice[Actor]()
|
|
166
166
|
kids.push(b)
|
|
167
167
|
g := PureScene.group(1, 100.0, 0.0, 800.0, 600.0, kids)
|
|
168
168
|
g.rotation = 1.5707963267948966
|
|
@@ -202,7 +202,7 @@ test "drag_off_and_back_one_click"
|
|
|
202
202
|
|
|
203
203
|
test "stale_capture_dropped"
|
|
204
204
|
h1 := SceneInput.stage(inBtnTree()).input(inDown(20.0, 20.0))
|
|
205
|
-
empty: Actor := PureScene.group(1, 0.0, 0.0, 800.0, 600.0,
|
|
205
|
+
empty: Actor := PureScene.group(1, 0.0, 0.0, 800.0, 600.0, Slice[Actor]())
|
|
206
206
|
swapped := Stage(root: empty, camera: h1.stage.camera, captured: h1.stage.captured)
|
|
207
207
|
h2 := swapped.input(inUp(20.0, 20.0))
|
|
208
208
|
assert h2.events.length() == 0
|
|
@@ -223,16 +223,16 @@ test "camera_maps_screen_to_world"
|
|
|
223
223
|
|
|
224
224
|
test "frame_handles_inputs_then_updates"
|
|
225
225
|
spr := PureScene.sprite(6, 0, 300.0, 300.0, 10.0, 10.0)
|
|
226
|
-
acts :=
|
|
226
|
+
acts := Slice[Action]()
|
|
227
227
|
acts.push(Actions.moveBy(10.0, 0.0, 1.0))
|
|
228
228
|
spr.actions = acts
|
|
229
229
|
b: Actor := PureScene.button(5, "Go", 0, 1, 10.0, 10.0, 100.0, 40.0)
|
|
230
230
|
sp: Actor := spr
|
|
231
|
-
kids :=
|
|
231
|
+
kids := Slice[Actor]()
|
|
232
232
|
kids.push(b)
|
|
233
233
|
kids.push(sp)
|
|
234
234
|
root: Actor := PureScene.group(1, 0.0, 0.0, 800.0, 600.0, kids)
|
|
235
|
-
ins :=
|
|
235
|
+
ins := Slice[Input]()
|
|
236
236
|
ins.push(inDown(20.0, 20.0))
|
|
237
237
|
ins.push(inUp(20.0, 20.0))
|
|
238
238
|
h := SceneInput.stage(root).frame(0.5, ins)
|
plum-std/game/SceneTree.plum
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
module std
|
|
2
2
|
import std/Int
|
|
3
3
|
import std/Float
|
|
4
|
-
import std/
|
|
4
|
+
import std/Slice
|
|
5
5
|
import std/Str
|
|
6
6
|
import std/game/Vec2
|
|
7
7
|
import std/game/Affine
|
|
8
8
|
import std/game/Actions
|
|
9
9
|
import std/game/PureScene
|
|
10
10
|
|
|
11
|
-
fun treePrepend(i: Int, rest:
|
|
11
|
+
fun treePrepend(i: Int, rest: Slice[Int]) -> Slice[Int] =
|
|
12
|
-
out :=
|
|
12
|
+
out := Slice[Int]()
|
|
13
13
|
out.push(i)
|
|
14
14
|
for j := range rest.length()
|
|
15
15
|
out.push(rest.at(j))
|
|
@@ -34,7 +34,7 @@ primitive SceneTree =
|
|
|
34
34
|
|
|
35
35
|
# The path (child indexes from `a`) to the topmost actor under `p`, which is
|
|
36
36
|
# in the coordinates of `a`'s parent; `parent` is that parent's stage matrix.
|
|
37
|
-
fun findHit(a: Actor, parent: Affine, p: Vec2) ->
|
|
37
|
+
fun findHit(a: Actor, parent: Affine, p: Vec2) -> Slice[Int]? =
|
|
38
38
|
if !a.visible || a.touchable == Disabled
|
|
39
39
|
return null
|
|
40
40
|
m := parent * PureScene.localMatrix(a)
|
|
@@ -52,11 +52,11 @@ primitive SceneTree =
|
|
|
52
52
|
return treePrepend(i, s)
|
|
53
53
|
i = i - 1
|
|
54
54
|
if a.hit(lp)
|
|
55
|
-
return
|
|
55
|
+
return Slice[Int]()
|
|
56
56
|
null
|
|
57
57
|
|
|
58
58
|
# The id of the actor at `path` below `a`, or -1 if the path leads nowhere.
|
|
59
|
-
fun idAt(a: Actor, path:
|
|
59
|
+
fun idAt(a: Actor, path: Slice[Int], depth: Int) -> Int =
|
|
60
60
|
if depth >= path.length()
|
|
61
61
|
return a.id
|
|
62
62
|
if ks := a.kids()
|
|
@@ -66,9 +66,9 @@ primitive SceneTree =
|
|
|
66
66
|
-1
|
|
67
67
|
|
|
68
68
|
# The path to the first actor with `id`, depth first; null if none.
|
|
69
|
-
fun findPath(a: Actor, id: Int) ->
|
|
69
|
+
fun findPath(a: Actor, id: Int) -> Slice[Int]? =
|
|
70
70
|
if a.id == id
|
|
71
|
-
return
|
|
71
|
+
return Slice[Int]()
|
|
72
72
|
if ks := a.kids()
|
|
73
73
|
for i := range ks.length()
|
|
74
74
|
sub := SceneTree.findPath(ks.at(i), id)
|
|
@@ -83,7 +83,7 @@ primitive SceneTree =
|
|
|
83
83
|
return SceneTree.modifyAt(a, p, 0, f)
|
|
84
84
|
a
|
|
85
85
|
|
|
86
|
-
fun modifyAt(own a: Actor, path:
|
|
86
|
+
fun modifyAt(own a: Actor, path: Slice[Int], depth: Int, f: fn(Actor) -> Actor) -> Actor =
|
|
87
87
|
if depth >= path.length()
|
|
88
88
|
return f(a)
|
|
89
89
|
if ks := a.kids()
|
|
@@ -96,13 +96,13 @@ primitive SceneTree =
|
|
|
96
96
|
# Delivers `e` (in the coordinates of `a`'s parent) along `path`: the child on
|
|
97
97
|
# the path first, then `a` itself. A path that runs out reaches the deepest
|
|
98
98
|
# actor it can.
|
|
99
|
-
fun route(own a: Actor, path:
|
|
99
|
+
fun route(own a: Actor, path: Slice[Int], depth: Int, e: Pointer) -> Handled =
|
|
100
|
-
events :=
|
|
100
|
+
events := Slice[UiEvent]()
|
|
101
101
|
r := SceneTree.routeInto(a, path, depth, e, events)
|
|
102
102
|
Handled(actor: r, events: events)
|
|
103
103
|
|
|
104
104
|
# `route`, appending the raised events to `events` and returning the actor.
|
|
105
|
-
fun routeInto(own a: Actor, path:
|
|
105
|
+
fun routeInto(own a: Actor, path: Slice[Int], depth: Int, e: Pointer, events: Slice[UiEvent]) -> Actor =
|
|
106
106
|
lp := Vec2(x: e.x, y: e.y)
|
|
107
107
|
inv := PureScene.localMatrix(a).invert()
|
|
108
108
|
if q := inv
|
|
@@ -120,19 +120,19 @@ primitive SceneTree =
|
|
|
120
120
|
h.intoActor()
|
|
121
121
|
|
|
122
122
|
fun ttFixture() -> Actor =
|
|
123
|
-
inner :=
|
|
123
|
+
inner := Slice[Actor]()
|
|
124
124
|
inner.push(PureScene.sprite(4, 0, 0.0, 0.0, 5.0, 5.0))
|
|
125
|
-
kids :=
|
|
125
|
+
kids := Slice[Actor]()
|
|
126
126
|
kids.push(PureScene.sprite(2, 0, 0.0, 0.0, 10.0, 10.0))
|
|
127
127
|
kids.push(PureScene.group(3, 20.0, 0.0, 10.0, 10.0, inner))
|
|
128
128
|
root := PureScene.group(1, 0.0, 0.0, 100.0, 100.0, kids)
|
|
129
129
|
me: Actor := root
|
|
130
130
|
me
|
|
131
131
|
|
|
132
|
-
fun ttNoPath(r:
|
|
132
|
+
fun ttNoPath(r: Slice[Int]?) -> Bool =
|
|
133
133
|
r == null
|
|
134
134
|
|
|
135
|
-
fun ttPathEq(r:
|
|
135
|
+
fun ttPathEq(r: Slice[Int]?, want: Slice[Int]) -> Bool =
|
|
136
136
|
if p := r
|
|
137
137
|
if p.length() != want.length()
|
|
138
138
|
return false
|
|
@@ -142,8 +142,8 @@ fun ttPathEq(r: List[Int]?, want: List[Int]) -> Bool =
|
|
|
142
142
|
return true
|
|
143
143
|
false
|
|
144
144
|
|
|
145
|
-
fun ttInts(a: Int, b: Int) ->
|
|
145
|
+
fun ttInts(a: Int, b: Int) -> Slice[Int] =
|
|
146
|
-
out :=
|
|
146
|
+
out := Slice[Int]()
|
|
147
147
|
out.push(a)
|
|
148
148
|
out.push(b)
|
|
149
149
|
out
|
|
@@ -153,8 +153,8 @@ fun ttAt(x: Float, y: Float) -> Vec2 =
|
|
|
153
153
|
|
|
154
154
|
fun ttMovingTree() -> Actor =
|
|
155
155
|
s := PureScene.sprite(2, 0, 0.0, 0.0, 10.0, 10.0)
|
|
156
|
-
s.actions =
|
|
156
|
+
s.actions = Slice(Actions.moveBy(10.0, 0.0, 1.0))
|
|
157
|
-
kids :=
|
|
157
|
+
kids := Slice[Actor]()
|
|
158
158
|
kids.push(s)
|
|
159
159
|
root := PureScene.group(1, 0.0, 0.0, 100.0, 100.0, kids)
|
|
160
160
|
me: Actor := root
|
|
@@ -194,14 +194,14 @@ fun ttKidX(a: Actor, i: Int) -> Float =
|
|
|
194
194
|
0.0 - 1.0
|
|
195
195
|
|
|
196
196
|
fun ttButtonRoot() -> Actor =
|
|
197
|
-
kids :=
|
|
197
|
+
kids := Slice[Actor]()
|
|
198
198
|
kids.push(PureScene.button(5, "Go", 0, 1, 10.0, 10.0, 100.0, 40.0))
|
|
199
199
|
root := PureScene.group(1, 0.0, 0.0, 800.0, 600.0, kids)
|
|
200
200
|
me: Actor := root
|
|
201
201
|
me
|
|
202
202
|
|
|
203
|
-
fun ttZero() ->
|
|
203
|
+
fun ttZero() -> Slice[Int] =
|
|
204
|
-
out :=
|
|
204
|
+
out := Slice[Int]()
|
|
205
205
|
out.push(0)
|
|
206
206
|
out
|
|
207
207
|
|
|
@@ -215,10 +215,10 @@ test "find_hit_miss"
|
|
|
215
215
|
test "id_at_paths"
|
|
216
216
|
t := ttFixture()
|
|
217
217
|
assert SceneTree.idAt(t, ttInts(1, 0), 0) == 4
|
|
218
|
-
five :=
|
|
218
|
+
five := Slice[Int]()
|
|
219
219
|
five.push(5)
|
|
220
220
|
assert SceneTree.idAt(t, five, 0) == -1
|
|
221
|
-
assert SceneTree.idAt(t,
|
|
221
|
+
assert SceneTree.idAt(t, Slice[Int](), 0) == 1
|
|
222
222
|
|
|
223
223
|
test "find_path_ids"
|
|
224
224
|
t := ttFixture()
|
|
@@ -239,31 +239,31 @@ test "update_changes_tree_in_place"
|
|
|
239
239
|
test "disabled_group_hides_subtree"
|
|
240
240
|
t := ttSetTouch(ttFixture(), 3, Disabled)
|
|
241
241
|
r := SceneTree.findHit(t, Affine.Identity, ttAt(21.0, 1.0))
|
|
242
|
-
assert ttPathEq(r,
|
|
242
|
+
assert ttPathEq(r, Slice[Int]())
|
|
243
243
|
|
|
244
244
|
test "children_only_group_not_hit_itself"
|
|
245
245
|
t := ttSetTouch(ttFixture(), 3, ChildrenOnly)
|
|
246
246
|
r := SceneTree.findHit(t, Affine.Identity, ttAt(28.0, 8.0))
|
|
247
|
-
assert ttPathEq(r,
|
|
247
|
+
assert ttPathEq(r, Slice[Int]())
|
|
248
248
|
|
|
249
249
|
test "scale_zero_not_hit"
|
|
250
250
|
t := SceneTree.modify(ttFixture(), 2, |a| ttSetScaleX(a))
|
|
251
251
|
r := SceneTree.findHit(t, Affine.Identity, ttAt(1.0, 1.0))
|
|
252
|
-
assert ttPathEq(r,
|
|
252
|
+
assert ttPathEq(r, Slice[Int]())
|
|
253
253
|
|
|
254
254
|
test "topmost_child_wins"
|
|
255
|
-
kids :=
|
|
255
|
+
kids := Slice[Actor]()
|
|
256
256
|
kids.push(PureScene.sprite(2, 0, 0.0, 0.0, 10.0, 10.0))
|
|
257
257
|
kids.push(PureScene.sprite(3, 0, 0.0, 0.0, 10.0, 10.0))
|
|
258
258
|
root := PureScene.group(1, 0.0, 0.0, 100.0, 100.0, kids)
|
|
259
259
|
me: Actor := root
|
|
260
260
|
r := SceneTree.findHit(me, Affine.Identity, ttAt(1.0, 1.0))
|
|
261
|
-
want :=
|
|
261
|
+
want := Slice[Int]()
|
|
262
262
|
want.push(1)
|
|
263
263
|
assert ttPathEq(r, want)
|
|
264
264
|
|
|
265
265
|
test "modify_changes_first_match_only"
|
|
266
|
-
kids :=
|
|
266
|
+
kids := Slice[Actor]()
|
|
267
267
|
kids.push(PureScene.sprite(7, 0, 0.0, 0.0, 10.0, 10.0))
|
|
268
268
|
kids.push(PureScene.sprite(7, 0, 0.0, 0.0, 10.0, 10.0))
|
|
269
269
|
root := PureScene.group(1, 0.0, 0.0, 100.0, 100.0, kids)
|
|
@@ -286,7 +286,7 @@ test "route_child_first_then_group"
|
|
|
286
286
|
assert h2.events.at(1).id == 5
|
|
287
287
|
|
|
288
288
|
test "stale_path_reaches_group_only"
|
|
289
|
-
nine :=
|
|
289
|
+
nine := Slice[Int]()
|
|
290
290
|
nine.push(9)
|
|
291
291
|
h := SceneTree.route(ttButtonRoot(), nine, 0, Pointer(kind: Down, x: 20.0, y: 20.0, over: true))
|
|
292
292
|
assert h.events.length() == 0
|
plum-wasm-codegen/src/lib.rs
CHANGED
|
@@ -1206,6 +1206,67 @@ fn addTraitParams(
|
|
|
1206
1206
|
}
|
|
1207
1207
|
}
|
|
1208
1208
|
|
|
1209
|
+
/// A union-enum member or inline-variant record laid out as a wasm-gc subtype of
|
|
1210
|
+
/// its enum (no wrapper struct): `record` is the member record, `hidden` the
|
|
1211
|
+
/// hidden wrapper variant of `enumName` that now resolves to the record's struct.
|
|
1212
|
+
/// `memberArgs` are the enum generics the member's type arguments name, in order.
|
|
1213
|
+
struct UnboxedMember {
|
|
1214
|
+
enumName: String,
|
|
1215
|
+
hidden: String,
|
|
1216
|
+
record: String,
|
|
1217
|
+
memberArgs: Vec<String>,
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
/// Members eligible for the unboxed layout: records claimed by exactly one enum
|
|
1221
|
+
/// (a struct has one supertype), not already subtypes of a trait, whose enum has
|
|
1222
|
+
/// no shared params, and whose type arguments are exactly the enum's generics.
|
|
1223
|
+
fn unboxedMembers(
|
|
1224
|
+
source: &ast::Source,
|
|
1225
|
+
enum_decls: &HashMap<String, &ast::Enum>,
|
|
1226
|
+
parent_of: &HashMap<String, String>,
|
|
1227
|
+
) -> Vec<UnboxedMember> {
|
|
1228
|
+
let mut found: Vec<UnboxedMember> = Vec::new();
|
|
1229
|
+
for item in &source.items {
|
|
1230
|
+
let ast::Item::Enum(e) = item else { continue };
|
|
1231
|
+
if e.isValue || e.isRecord || e.isTrait || !e.params.is_empty() {
|
|
1232
|
+
continue;
|
|
1233
|
+
}
|
|
1234
|
+
for v in &e.variants {
|
|
1235
|
+
let Some(m) = &v.member else { continue };
|
|
1236
|
+
if v.name != plum_core::unions::hiddenVariant(&e.name, &m.name) {
|
|
1237
|
+
continue;
|
|
1238
|
+
}
|
|
1239
|
+
let Some(r) = enum_decls.get(&m.name) else { continue };
|
|
1240
|
+
if !r.isRecord || r.isValue || parent_of.contains_key(&m.name) {
|
|
1241
|
+
continue;
|
|
1242
|
+
}
|
|
1243
|
+
if r.generics.len() != m.generics.len() {
|
|
1244
|
+
continue;
|
|
1245
|
+
}
|
|
1246
|
+
let names: Vec<String> = m.generics.iter().map(|g| g.name.clone()).collect();
|
|
1247
|
+
let plain = m.generics.iter().all(|g| g.generics.is_empty())
|
|
1248
|
+
&& names.iter().all(|n| e.generics.iter().any(|p| &p.name == n));
|
|
1249
|
+
let covers = e.generics.iter().all(|p| names.contains(&p.name));
|
|
1250
|
+
let distinct = names.iter().collect::<HashSet<_>>().len() == names.len();
|
|
1251
|
+
if !(plain && covers && distinct) {
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
found.push(UnboxedMember {
|
|
1255
|
+
enumName: e.name.clone(),
|
|
1256
|
+
hidden: v.name.clone(),
|
|
1257
|
+
record: m.name.clone(),
|
|
1258
|
+
memberArgs: names,
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
let mut count: HashMap<String, usize> = HashMap::new();
|
|
1263
|
+
for u in &found {
|
|
1264
|
+
*count.entry(u.record.clone()).or_default() += 1;
|
|
1265
|
+
}
|
|
1266
|
+
found.retain(|u| count[&u.record] == 1);
|
|
1267
|
+
found
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1209
1270
|
/// Builds the wasm-gc type registry: one struct family per non-generic enum and per
|
|
1210
1271
|
/// distinct concrete instance (`apps`, e.g. `List[Int]`) of a generic enum, keyed by
|
|
1211
1272
|
/// the structural `PlumType`; `Array[T]` instances become native wasm arrays of `T`'s
|
|
@@ -1262,6 +1323,7 @@ fn buildGcTypeRegistry(
|
|
|
1262
1323
|
instances.push(PlumType::TNamed(e.name.clone()));
|
|
1263
1324
|
}
|
|
1264
1325
|
}
|
|
1326
|
+
let unboxed = unboxedMembers(source, &enum_decls, &parent_of);
|
|
1265
1327
|
let mut arrays: Vec<PlumType> = Vec::new();
|
|
1266
1328
|
for a in apps {
|
|
1267
1329
|
if let PlumType::TApp(n, _) = a {
|
|
@@ -1272,9 +1334,48 @@ fn buildGcTypeRegistry(
|
|
|
1272
1334
|
}
|
|
1273
1335
|
}
|
|
1274
1336
|
}
|
|
1337
|
+
// Unboxed members: member record instance -> (enum instance, hidden variant).
|
|
1338
|
+
// Every claimed member instance of an included enum instance must exist.
|
|
1339
|
+
let mut member_parent: HashMap<PlumType, (PlumType, String)> = HashMap::new();
|
|
1340
|
+
let mut k = 0;
|
|
1341
|
+
while k < instances.len() {
|
|
1342
|
+
let ty = instances[k].clone();
|
|
1343
|
+
k += 1;
|
|
1344
|
+
let Some(n) = plum_checker::nominalName(&ty) else { continue };
|
|
1345
|
+
for (hidden, um) in unboxed.iter().filter(|u| u.enumName == n).map(|u| (&u.hidden, u)) {
|
|
1346
|
+
let args: Vec<PlumType> = match &ty {
|
|
1347
|
+
PlumType::TApp(_, args) => um
|
|
1348
|
+
.memberArgs
|
|
1349
|
+
.iter()
|
|
1350
|
+
.map(|g| {
|
|
1351
|
+
let i = enum_decls[n].generics.iter().position(|p| &p.name == g).unwrap();
|
|
1352
|
+
args[i].clone()
|
|
1353
|
+
})
|
|
1354
|
+
.collect(),
|
|
1355
|
+
_ => Vec::new(),
|
|
1356
|
+
};
|
|
1357
|
+
let mty = if args.is_empty() {
|
|
1358
|
+
PlumType::TNamed(um.record.clone())
|
|
1359
|
+
} else {
|
|
1360
|
+
PlumType::TApp(um.record.clone(), args)
|
|
1361
|
+
};
|
|
1362
|
+
if !instances.contains(&mty) {
|
|
1363
|
+
instances.push(mty.clone());
|
|
1364
|
+
}
|
|
1365
|
+
member_parent.insert(mty, (ty.clone(), hidden.clone()));
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1275
|
-
// A trait's base struct must precede the records
|
|
1368
|
+
// A trait's base struct, then unboxed enum supers, must precede the records
|
|
1369
|
+
// that extend them.
|
|
1276
1370
|
instances.sort_by_key(|t| {
|
|
1371
|
+
let n = plum_checker::nominalName(t).unwrap_or("");
|
|
1277
|
-
|
|
1372
|
+
if subtyped.contains_key(n) {
|
|
1373
|
+
0
|
|
1374
|
+
} else if unboxed.iter().any(|u| u.enumName == n) {
|
|
1375
|
+
1
|
|
1376
|
+
} else {
|
|
1377
|
+
2
|
|
1378
|
+
}
|
|
1278
1379
|
});
|
|
1279
1380
|
for ty in &instances {
|
|
1280
1381
|
let n = plum_checker::nominalName(ty).unwrap().to_string();
|
|
@@ -1294,6 +1395,9 @@ fn buildGcTypeRegistry(
|
|
|
1294
1395
|
continue;
|
|
1295
1396
|
}
|
|
1296
1397
|
for v in &e.variants {
|
|
1398
|
+
if unboxed.iter().any(|u| u.enumName == n && u.hidden == v.name) {
|
|
1399
|
+
continue;
|
|
1400
|
+
}
|
|
1297
1401
|
let idx = slots.len() as u32;
|
|
1298
1402
|
slots.push(Slot::Variant(ty.clone(), v.name.clone()));
|
|
1299
1403
|
match ty {
|
|
@@ -1325,6 +1429,18 @@ fn buildGcTypeRegistry(
|
|
|
1325
1429
|
}
|
|
1326
1430
|
}
|
|
1327
1431
|
}
|
|
1432
|
+
for (mty, (ety, hidden)) in &member_parent {
|
|
1433
|
+
let Some(idx) = reg.classIdx(mty) else { continue };
|
|
1434
|
+
match ety {
|
|
1435
|
+
PlumType::TNamed(_) => {
|
|
1436
|
+
reg.variant_type_idx.insert(hidden.clone(), idx);
|
|
1437
|
+
}
|
|
1438
|
+
_ => {
|
|
1439
|
+
reg.app_variant.insert((ety.clone(), hidden.clone()), idx);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
reg.trait_members.insert(hidden.clone());
|
|
1443
|
+
}
|
|
1328
1444
|
reg.instances = instances.clone();
|
|
1329
1445
|
for t in errs {
|
|
1330
1446
|
if !reg.err_box.contains_key(t) {
|
|
@@ -1443,6 +1559,11 @@ fn buildGcTypeRegistry(
|
|
|
1443
1559
|
PlumType::TNamed(n) if *vname == *n && parent_of.contains_key(n) => {
|
|
1444
1560
|
reg.enum_super_type_idx[&parent_of[n]]
|
|
1445
1561
|
}
|
|
1562
|
+
_ if plum_checker::nominalName(ty) == Some(vname.as_str())
|
|
1563
|
+
&& member_parent.contains_key(ty) =>
|
|
1564
|
+
{
|
|
1565
|
+
reg.superIdx(&member_parent[ty].0).expect("enum supertype slot")
|
|
1566
|
+
}
|
|
1446
1567
|
_ => reg.superIdx(ty).expect("enum supertype slot"),
|
|
1447
1568
|
};
|
|
1448
1569
|
let mut field_types: Vec<FieldType> = Vec::new();
|
|
@@ -7537,10 +7658,12 @@ fn compileExprCore(
|
|
|
7537
7658
|
// positional-variant-construction case just below — see the matching
|
|
7538
7659
|
// comment in `plum-checker`'s `inferExpr` for why a record-shaped
|
|
7539
7660
|
// enum's own name needs this precedence.
|
|
7540
|
-
} else if ctx
|
|
7661
|
+
} else if (ctx
|
|
7541
7662
|
.enum_variants
|
|
7542
7663
|
.get(&call.name)
|
|
7543
7664
|
.is_some_and(|info| info.enum_name == call.name)
|
|
7665
|
+
|| (!ctx.enum_variants.contains_key(&call.name)
|
|
7666
|
+
&& ctx.enum_variants.values().any(|i| i.enum_name == call.name)))
|
|
7544
7667
|
&& ctx
|
|
7545
7668
|
.methods
|
|
7546
7669
|
.contains_key(&(call.name.clone(), "init".to_string()))
|
|
@@ -8371,6 +8494,17 @@ fn compileVariantConstruction(
|
|
|
8371
8494
|
call.name, ety
|
|
8372
8495
|
)
|
|
8373
8496
|
})?;
|
|
8497
|
+
if ctx.gc_types.trait_members.contains(&call.name) && call.args.len() == 1 {
|
|
8498
|
+
// The member record IS the enum value: no wrapper struct.
|
|
8499
|
+
let arg = match &call.args[0] {
|
|
8500
|
+
ast::Arg::Positional(e) => e,
|
|
8501
|
+
ast::Arg::Keyword { value, .. } => value,
|
|
8502
|
+
};
|
|
8503
|
+
return match field_tys.first() {
|
|
8504
|
+
Some(d) => compileExprIntoHeap(arg, d, body, ctx, state),
|
|
8505
|
+
None => compileExpr(arg, body, ctx, state),
|
|
8506
|
+
};
|
|
8507
|
+
}
|
|
8374
8508
|
if info.field_types.is_empty() {
|
|
8375
8509
|
// Pre-allocated singleton (this migration plan's Decision 2) — not a fresh
|
|
8376
8510
|
// struct.new per reference.
|
plum-wasm-codegen/tests/codegen_tests.rs
CHANGED
|
@@ -518,7 +518,7 @@ fn wasmtimeGcConfigSupportsSubtypingRefTestAndNullableSelfReferentialFields() {
|
|
|
518
518
|
// Deeper empirical check: enum-variant subtyping (abstract supertype + concrete
|
|
519
519
|
// subtypes in one `rec` group), ref.test-based dispatch, ref.cast to narrow to a
|
|
520
520
|
// subtype, and a nullable field that references the struct's OWN type (the
|
|
521
|
-
// Node.next: Link[Node] shape
|
|
521
|
+
// Node.next: Link[Node] shape Slice needs) — all in one hand-encoded module,
|
|
522
522
|
// bypassing plum entirely.
|
|
523
523
|
use wasm_encoder::*;
|
|
524
524
|
|
|
@@ -1112,20 +1112,20 @@ fn compiledKeys(name: &str, src: &str) -> (Vec<u8>, Vec<String>) {
|
|
|
1112
1112
|
}
|
|
1113
1113
|
|
|
1114
1114
|
const LIST_FNS: &str = "\
|
|
1115
|
-
fun mapList(xs:
|
|
1115
|
+
fun mapList(xs: Slice[T], cb: fn(T) -> U) -> Slice[U] =
|
|
1116
|
-
result :=
|
|
1116
|
+
result := Slice[U]()
|
|
1117
1117
|
for x := range xs
|
|
1118
1118
|
result.push(cb(x))
|
|
1119
1119
|
result
|
|
1120
1120
|
|
|
1121
|
-
fun reduceList(xs:
|
|
1121
|
+
fun reduceList(xs: Slice[T], acc: U, cb: fn(U, T) -> U) -> U =
|
|
1122
1122
|
result := acc
|
|
1123
1123
|
for x := range xs
|
|
1124
1124
|
result = cb(result, x)
|
|
1125
1125
|
result
|
|
1126
1126
|
|
|
1127
|
-
fun retainList(xs:
|
|
1127
|
+
fun retainList(xs: Slice[T], keep: fn(T) -> Bool) -> Slice[T] =
|
|
1128
|
-
result :=
|
|
1128
|
+
result := Slice[T]()
|
|
1129
1129
|
for x := range xs
|
|
1130
1130
|
if keep(x)
|
|
1131
1131
|
result.push(x)
|
|
@@ -1137,7 +1137,7 @@ const STD_HEAD: &str = "\
|
|
|
1137
1137
|
import std/Str
|
|
1138
1138
|
import std/Int
|
|
1139
1139
|
import std/Float
|
|
1140
|
-
import std/
|
|
1140
|
+
import std/Slice
|
|
1141
1141
|
";
|
|
1142
1142
|
|
|
1143
1143
|
#[test]
|
|
@@ -1145,8 +1145,8 @@ fn one_copy_per_distinct_type() {
|
|
|
1145
1145
|
let src = format!(
|
|
1146
1146
|
"{}{}\
|
|
1147
1147
|
fun main() -> Int =
|
|
1148
|
-
a := mapList(
|
|
1148
|
+
a := mapList(Slice(1, 2), |n| n + 1)
|
|
1149
|
-
b := mapList(
|
|
1149
|
+
b := mapList(Slice(\"x\", \"y\"), |s| s + \"!\")
|
|
1150
1150
|
a.length() + b.length()
|
|
1151
1151
|
",
|
|
1152
1152
|
STD_HEAD, LIST_FNS
|
|
@@ -1174,8 +1174,8 @@ fun main() -> Int =
|
|
|
1174
1174
|
let src = format!(
|
|
1175
1175
|
"{}{}\
|
|
1176
1176
|
fun main() -> Int =
|
|
1177
|
-
a := mapList(
|
|
1177
|
+
a := mapList(Slice(1, 2), |n| n + 1)
|
|
1178
|
-
b := mapList(
|
|
1178
|
+
b := mapList(Slice(3), |n| n * 2)
|
|
1179
1179
|
a.length() + b.length()
|
|
1180
1180
|
",
|
|
1181
1181
|
STD_HEAD, LIST_FNS
|
|
@@ -1193,7 +1193,7 @@ fun neverUsed(x: T) -> T =
|
|
|
1193
1193
|
x
|
|
1194
1194
|
|
|
1195
1195
|
fun main() -> Int =
|
|
1196
|
-
|
|
1196
|
+
Slice(1, 2).length()
|
|
1197
1197
|
",
|
|
1198
1198
|
STD_HEAD
|
|
1199
1199
|
);
|
|
@@ -1254,7 +1254,7 @@ fn no_boxes_in_list_int_map() {
|
|
|
1254
1254
|
let src = format!(
|
|
1255
1255
|
"{}{}\
|
|
1256
1256
|
fun main() -> Int =
|
|
1257
|
-
mapList(
|
|
1257
|
+
mapList(Slice(1, 2), |n| n + 1).length()
|
|
1258
1258
|
",
|
|
1259
1259
|
STD_HEAD, LIST_FNS
|
|
1260
1260
|
);
|
|
@@ -1307,7 +1307,7 @@ fn array_int_native() {
|
|
|
1307
1307
|
let src = format!(
|
|
1308
1308
|
"{}\
|
|
1309
1309
|
fun main() -> Int =
|
|
1310
|
-
|
|
1310
|
+
Slice(1, 2).length()
|
|
1311
1311
|
",
|
|
1312
1312
|
STD_HEAD
|
|
1313
1313
|
);
|
|
@@ -1385,10 +1385,10 @@ fn builtin_bool_list_round_trip() {
|
|
|
1385
1385
|
import std/Str
|
|
1386
1386
|
import std/Int
|
|
1387
1387
|
import std/Float
|
|
1388
|
-
import std/
|
|
1388
|
+
import std/Slice
|
|
1389
1389
|
|
|
1390
1390
|
fun main() -> Int =
|
|
1391
|
-
l :=
|
|
1391
|
+
l := Slice[Bool]()
|
|
1392
1392
|
l.push(true)
|
|
1393
1393
|
l.push(false)
|
|
1394
1394
|
l.push(true)
|
|
@@ -1605,11 +1605,11 @@ fun main() -> Int =
|
|
|
1605
1605
|
fn closure_calls_fn_param() {
|
|
1606
1606
|
let src = format!(
|
|
1607
1607
|
"{}{}\
|
|
1608
|
-
fun apply(xs:
|
|
1608
|
+
fun apply(xs: Slice[Int], cb: fn(Int) -> Int) -> Slice[Int] =
|
|
1609
1609
|
mapList(xs, |x| cb(x))
|
|
1610
1610
|
|
|
1611
1611
|
fun main() -> Int =
|
|
1612
|
-
reduceList(apply(
|
|
1612
|
+
reduceList(apply(Slice(1, 2, 3), |n| n * 10), 0, |a, b| a + b)
|
|
1613
1613
|
",
|
|
1614
1614
|
STD_HEAD, LIST_FNS
|
|
1615
1615
|
);
|
|
@@ -1622,13 +1622,13 @@ fn record_of_fns_passed_explicitly() {
|
|
|
1622
1622
|
"{}{}\
|
|
1623
1623
|
record Num[T](add: fn(T, T) -> T, zero: T)
|
|
1624
1624
|
|
|
1625
|
-
fun sumWith(xs:
|
|
1625
|
+
fun sumWith(xs: Slice[T], num: Num[T]) -> T =
|
|
1626
1626
|
reduceList(xs, num.zero, |a, b| num.add(a, b))
|
|
1627
1627
|
|
|
1628
1628
|
fun main() -> Int =
|
|
1629
1629
|
intNum := Num(add: |a, b| a + b, zero: 0)
|
|
1630
1630
|
floatNum := Num(add: |a, b| a + b, zero: 0.0)
|
|
1631
|
-
if sumWith(
|
|
1631
|
+
if sumWith(Slice(1, 2, 3), intNum) == 6 && sumWith(Slice(1.5, 2.5), floatNum) == 4.0
|
|
1632
1632
|
return 1
|
|
1633
1633
|
return 0
|
|
1634
1634
|
",
|
|
@@ -1811,13 +1811,13 @@ fn fn_field_nested_call_arg() {
|
|
|
1811
1811
|
"{}\
|
|
1812
1812
|
import std/Array
|
|
1813
1813
|
|
|
1814
|
-
record Keyed[T](items:
|
|
1814
|
+
record Keyed[T](items: Slice[T], key: fn(T) -> Int) =
|
|
1815
1815
|
|
|
1816
1816
|
fun first(self) -> Int =
|
|
1817
1817
|
self.key(self.items.data.get(0))
|
|
1818
1818
|
|
|
1819
1819
|
fun main() -> Int =
|
|
1820
|
-
k := Keyed[Int](items:
|
|
1820
|
+
k := Keyed[Int](items: Slice(5, 6), key: |n| n * 8)
|
|
1821
1821
|
k.first() + 2
|
|
1822
1822
|
",
|
|
1823
1823
|
STD_HEAD
|
|
@@ -1830,7 +1830,7 @@ fn const_list_table() {
|
|
|
1830
1830
|
let src = format!(
|
|
1831
1831
|
"{}{}\
|
|
1832
1832
|
primitive Tables =
|
|
1833
|
-
Values =
|
|
1833
|
+
Values = Slice(10, 20, 30)
|
|
1834
1834
|
|
|
1835
1835
|
fun total() -> Int =
|
|
1836
1836
|
reduceList(Tables.Values, 0, |a, b| a + b)
|
|
@@ -1886,7 +1886,7 @@ fn filter_form_no_closure() {
|
|
|
1886
1886
|
let form = format!(
|
|
1887
1887
|
"{head}\
|
|
1888
1888
|
fun main() -> Int =
|
|
1889
|
-
units :=
|
|
1889
|
+
units := Slice(1, 2, 3, 4)
|
|
1890
1890
|
alive := for u := range units.filter()
|
|
1891
1891
|
u > 2
|
|
1892
1892
|
alive.length()
|
|
@@ -1899,7 +1899,7 @@ fun main() -> Int =
|
|
|
1899
1899
|
let closure = format!(
|
|
1900
1900
|
"{STD_HEAD}\
|
|
1901
1901
|
fun main() -> Int =
|
|
1902
|
-
units :=
|
|
1902
|
+
units := Slice(1, 2, 3, 4)
|
|
1903
1903
|
alive := retainList(units, |u| u > 2)
|
|
1904
1904
|
alive.length()
|
|
1905
1905
|
{LIST_FNS}"
|
|
@@ -1927,7 +1927,7 @@ fn map_with_clauses_fused() {
|
|
|
1927
1927
|
let src = format!(
|
|
1928
1928
|
"{STD_HEAD}import std/Seq\n\
|
|
1929
1929
|
fun main() -> Int =
|
|
1930
|
-
xs :=
|
|
1930
|
+
xs := Slice(1, 2, 3, 4, 5, 6)
|
|
1931
1931
|
ys := for x := range xs.map(), if x % 2 == 0, sq := x * x
|
|
1932
1932
|
sq + 1
|
|
1933
1933
|
ys.length()
|
|
@@ -1981,7 +1981,7 @@ fn no_closure_in_expansion() {
|
|
|
1981
1981
|
let src = format!(
|
|
1982
1982
|
"{}\
|
|
1983
1983
|
fun main() -> Int =
|
|
1984
|
-
xs :=
|
|
1984
|
+
xs := Slice(1, 2, 3, 4, 5, 6)
|
|
1985
1985
|
total := for acc, x := range xs.fold(0), if x % 2 == 0
|
|
1986
1986
|
acc + x
|
|
1987
1987
|
total
|
|
@@ -2005,7 +2005,7 @@ fn seq_param_take_fused() {
|
|
|
2005
2005
|
let src = format!(
|
|
2006
2006
|
"{}\
|
|
2007
2007
|
fun main() -> Int =
|
|
2008
|
-
xs :=
|
|
2008
|
+
xs := Slice(1, 2, 3, 4, 5, 6)
|
|
2009
2009
|
ys := for x := range xs.take(3).map()
|
|
2010
2010
|
x * 10
|
|
2011
2011
|
ys.at(0) + ys.at(1) + ys.at(2) + ys.length()
|
|
@@ -2028,12 +2028,12 @@ fn pack_binder_passes_through_map() {
|
|
|
2028
2028
|
// map over a two-value source: map's single name `x` holds both values and is passed to yield
|
|
2029
2029
|
let src = format!(
|
|
2030
2030
|
"{}\
|
|
2031
|
-
fun* pairs(src:
|
|
2031
|
+
fun* pairs(src: Slice[Int]) *(Int, Int) =
|
|
2032
2032
|
for i, x := range src
|
|
2033
2033
|
yield i, x
|
|
2034
2034
|
|
|
2035
2035
|
fun main() -> Int =
|
|
2036
|
-
xs :=
|
|
2036
|
+
xs := Slice(4, 5, 6)
|
|
2037
2037
|
total := 0
|
|
2038
2038
|
for i, x := range pairs(xs).map()
|
|
2039
2039
|
total = total + i * x
|
|
@@ -2050,14 +2050,14 @@ fn fun_star_method_no_closure() {
|
|
|
2050
2050
|
// A `fun*` method is expanded at the use site: no closure, no call_indirect, `self` bound once.
|
|
2051
2051
|
let src = format!(
|
|
2052
2052
|
"{}\
|
|
2053
|
-
record Bag(items:
|
|
2053
|
+
record Bag(items: Slice[Int]) =
|
|
2054
2054
|
|
|
2055
2055
|
fun* doubled(self) *Int =
|
|
2056
2056
|
for x := range self.items
|
|
2057
2057
|
yield x * 2
|
|
2058
2058
|
|
|
2059
2059
|
fun main() -> Int =
|
|
2060
|
-
b := Bag(items:
|
|
2060
|
+
b := Bag(items: Slice(1, 2, 3))
|
|
2061
2061
|
total := 0
|
|
2062
2062
|
for d := range b.doubled()
|
|
2063
2063
|
total = total + d
|
|
@@ -2099,7 +2099,7 @@ fn seq_chain_take_filter_fused() {
|
|
|
2099
2099
|
let src = format!(
|
|
2100
2100
|
"{}\
|
|
2101
2101
|
fun main() -> Int =
|
|
2102
|
-
xs :=
|
|
2102
|
+
xs := Slice(1, 2, 3, 4, 5, 6, 7, 8)
|
|
2103
2103
|
got := for x := range xs.take(6).filter()
|
|
2104
2104
|
x % 2 == 0
|
|
2105
2105
|
got.length()
|
|
@@ -2122,7 +2122,7 @@ fn terminal_mid_chain_error() {
|
|
|
2122
2122
|
let src = format!(
|
|
2123
2123
|
"{}\
|
|
2124
2124
|
fun main() -> Int =
|
|
2125
|
-
xs :=
|
|
2125
|
+
xs := Slice(1, 2, 3)
|
|
2126
2126
|
got := for x := range xs.filter().take(2)
|
|
2127
2127
|
x
|
|
2128
2128
|
0
|
|
@@ -2141,7 +2141,7 @@ fn seq_cannot_be_stored() {
|
|
|
2141
2141
|
let src = format!(
|
|
2142
2142
|
"{}\
|
|
2143
2143
|
fun main() -> Int =
|
|
2144
|
-
xs :=
|
|
2144
|
+
xs := Slice(1, 2, 3)
|
|
2145
2145
|
s := xs.take(2)
|
|
2146
2146
|
0
|
|
2147
2147
|
",
|
|
@@ -2407,13 +2407,13 @@ fn bang_message_names_the_file_of_the_callee_fn() {
|
|
|
2407
2407
|
import std/Str
|
|
2408
2408
|
import std/Int
|
|
2409
2409
|
import std/Float
|
|
2410
|
-
import std/
|
|
2410
|
+
import std/Slice
|
|
2411
2411
|
|
|
2412
|
-
fun first(xs:
|
|
2412
|
+
fun first(xs: Slice[Int]) -> Int =
|
|
2413
2413
|
xs.first()!
|
|
2414
2414
|
|
|
2415
2415
|
fun main() -> Int =
|
|
2416
|
-
first(
|
|
2416
|
+
first(Slice[Int]())
|
|
2417
2417
|
";
|
|
2418
2418
|
let (r, msgs) = runWithCrash("bangfile", src);
|
|
2419
2419
|
assert!(r.is_err());
|
|
@@ -2491,20 +2491,20 @@ fn list_of_error_unions_across_sets_valid_wasm() {
|
|
|
2491
2491
|
import std/Str
|
|
2492
2492
|
import std/Int
|
|
2493
2493
|
import std/Float
|
|
2494
|
-
import std/
|
|
2494
|
+
import std/Slice
|
|
2495
2495
|
|
|
2496
2496
|
fun chk(n: Int) -> !Int =
|
|
2497
2497
|
if n < 0
|
|
2498
2498
|
return error.Neg
|
|
2499
2499
|
n
|
|
2500
2500
|
|
|
2501
|
-
fun total(xs:
|
|
2501
|
+
fun total(xs: Slice[error{Neg}!Int]) -> Int =
|
|
2502
2502
|
a := xs.at(0) catch 0
|
|
2503
2503
|
b := xs.at(1) catch 100
|
|
2504
2504
|
a + b
|
|
2505
2505
|
|
|
2506
2506
|
fun main() -> Int =
|
|
2507
|
-
xs :=
|
|
2507
|
+
xs := Slice(chk(4))
|
|
2508
2508
|
xs.push(chk(-1))
|
|
2509
2509
|
total(xs)
|
|
2510
2510
|
";
|
plum-wasm-codegen/tests/enum_init_e2e_test.rs
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#![allow(non_snake_case)]
|
|
2
|
+
use plum_wasm_codegen::compileSource;
|
|
3
|
+
|
|
4
|
+
fn gcEngine() -> wasmtime::Engine {
|
|
5
|
+
let mut config = wasmtime::Config::new();
|
|
6
|
+
config.wasm_gc(true);
|
|
7
|
+
config.wasm_function_references(true);
|
|
8
|
+
wasmtime::Engine::new(&config).expect("engine with GC config should construct")
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
fn repoRoot() -> std::path::PathBuf {
|
|
12
|
+
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
fn load(name: &str, src: &str) -> Result<plum_core::ast::Source, String> {
|
|
16
|
+
let dir = std::env::temp_dir().join(format!("plum_enum_init_{}_{}", name, std::process::id()));
|
|
17
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
18
|
+
let path = dir.join("main.plum");
|
|
19
|
+
std::fs::write(&path, src).unwrap();
|
|
20
|
+
let r = plum_core::loadAndMerge(&path, &repoRoot());
|
|
21
|
+
let _ = std::fs::remove_dir_all(&dir);
|
|
22
|
+
r.map_err(|e| format!("{e}"))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Compiles `src` through the real pipeline and runs its `main() -> Int`.
|
|
26
|
+
fn runInt(name: &str, src: &str) -> i64 {
|
|
27
|
+
let source = load(name, src).expect("load");
|
|
28
|
+
let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
29
|
+
wasmparser::validate(&bytes).expect("valid wasm");
|
|
30
|
+
let engine = gcEngine();
|
|
31
|
+
let module = wasmtime::Module::new(&engine, &bytes).unwrap();
|
|
32
|
+
let mut store = wasmtime::Store::new(&engine, ());
|
|
33
|
+
let mut linker = wasmtime::Linker::new(&engine);
|
|
34
|
+
linker
|
|
35
|
+
.define_unknown_imports_as_default_values(&mut store, &module)
|
|
36
|
+
.unwrap();
|
|
37
|
+
let instance = linker.instantiate(&mut store, &module).unwrap();
|
|
38
|
+
let main = instance
|
|
39
|
+
.get_typed_func::<(), i64>(&mut store, "main")
|
|
40
|
+
.unwrap();
|
|
41
|
+
main.call(&mut store, ()).unwrap()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
const LINK: &str = "enum Link[T] =
|
|
47
|
+
| Nil
|
|
48
|
+
| Cons(head: T, tail: Link[T])
|
|
49
|
+
|
|
50
|
+
fun init(values: ...T) -> Link[T] =
|
|
51
|
+
out: Link[T] := Nil
|
|
52
|
+
i := values.length() - 1
|
|
53
|
+
while i >= 0
|
|
54
|
+
out = Cons(head: values.at(i), tail: out)
|
|
55
|
+
i = i - 1
|
|
56
|
+
out
|
|
57
|
+
|
|
58
|
+
fun length(self) -> Int = match self
|
|
59
|
+
Nil => 0
|
|
60
|
+
Cons(_, t) => 1 + t.length()
|
|
61
|
+
|
|
62
|
+
fun sum(self) -> Int = match self
|
|
63
|
+
Nil => 0
|
|
64
|
+
Cons(h, t) => h + t.sum()
|
|
65
|
+
|
|
66
|
+
";
|
|
67
|
+
|
|
68
|
+
fn prog(body: &str) -> String {
|
|
69
|
+
format!("import std/Str\nimport std/Int\n\n{body}")
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#[test]
|
|
73
|
+
fn variadic_init_builds_list() {
|
|
74
|
+
let src = prog(&format!("{LINK}fun main() -> Int =
|
|
75
|
+
xs := Link(1, 2, 3, 4)
|
|
76
|
+
xs.sum() * 10 + xs.length()
|
|
77
|
+
"));
|
|
78
|
+
assert_eq!(runInt("variadic", &src), 104);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#[test]
|
|
82
|
+
fn empty_init_with_annotation() {
|
|
83
|
+
let src = prog(&format!("{LINK}fun main() -> Int =
|
|
84
|
+
xs: Link[Int] := Link()
|
|
85
|
+
xs.length() + 7
|
|
86
|
+
"));
|
|
87
|
+
assert_eq!(runInt("empty", &src), 7);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#[test]
|
|
91
|
+
fn fixed_param_init() {
|
|
92
|
+
let src = prog("enum Pair2 =
|
|
93
|
+
| Zero
|
|
94
|
+
| Two(a: Int, b: Int)
|
|
95
|
+
|
|
96
|
+
fun init(a: Int, b: Int) -> Pair2 = Two(a: a, b: b)
|
|
97
|
+
|
|
98
|
+
fun total(self) -> Int = match self
|
|
99
|
+
Zero => 0
|
|
100
|
+
Two(a, b) => a + b
|
|
101
|
+
|
|
102
|
+
fun main() -> Int =
|
|
103
|
+
Pair2(3, 4).total()
|
|
104
|
+
");
|
|
105
|
+
assert_eq!(runInt("fixed", &src), 7);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#[test]
|
|
109
|
+
fn no_init_error_message() {
|
|
110
|
+
let src = prog("enum Shape =
|
|
111
|
+
| Dot
|
|
112
|
+
| Sq(side: Int)
|
|
113
|
+
|
|
114
|
+
fun main() -> Int =
|
|
115
|
+
s := Shape(1)
|
|
116
|
+
0
|
|
117
|
+
");
|
|
118
|
+
let err = match load("noinit", &src) {
|
|
119
|
+
Err(e) => e,
|
|
120
|
+
Ok(source) => match compileSource(&source) {
|
|
121
|
+
Err(e) => e,
|
|
122
|
+
Ok(_) => panic!("expected error"),
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
assert!(
|
|
126
|
+
err.contains("Shape has no init; construct a variant or declare `fun init`"),
|
|
127
|
+
"{err}"
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#[test]
|
|
132
|
+
fn variant_name_wins() {
|
|
133
|
+
let src = prog(&format!("{LINK}fun main() -> Int =
|
|
134
|
+
xs := Cons(head: 5, tail: Link(1, 2))
|
|
135
|
+
xs.sum() * 10 + xs.length()
|
|
136
|
+
"));
|
|
137
|
+
assert_eq!(runInt("variantwins", &src), 83);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#[test]
|
|
141
|
+
fn generic_inferred_from_args() {
|
|
142
|
+
let src = prog(&format!("{LINK}fun total(xs: Link[Int]) -> Int = xs.sum()
|
|
143
|
+
|
|
144
|
+
fun main() -> Int =
|
|
145
|
+
total(Link(5, 6)) + Link(\"a\", \"b\").length()
|
|
146
|
+
"));
|
|
147
|
+
assert_eq!(runInt("infer", &src), 13);
|
|
148
|
+
}
|
plum-wasm-codegen/tests/examples_test.rs
CHANGED
|
@@ -98,7 +98,7 @@ fn ioExampleCompilesAndRunsCorrectly() {
|
|
|
98
98
|
|
|
99
99
|
// `io.plum` now also transitively pulls in `rawRandomHalf` (via the
|
|
100
100
|
// always-implicit `std/str` prelude — `str` -> `list` -> `int`, which
|
|
101
|
-
// declares it for `
|
|
101
|
+
// declares it for `Slice.sample`/`shuffle` — see
|
|
102
102
|
// `plum-core::loader::loadAndMerge`), even though this test never
|
|
103
103
|
// actually calls anything that uses it. Wire up a harmless stub so the
|
|
104
104
|
// module can still instantiate; every import must be satisfied in
|
|
@@ -205,7 +205,7 @@ fn array_int_native() {
|
|
|
205
205
|
#[test]
|
|
206
206
|
fn const_table_1000_entries() {
|
|
207
207
|
let mut src = String::from(
|
|
208
|
-
"module t\nimport std/Int\nimport std/Float\nimport std/
|
|
208
|
+
"module t\nimport std/Int\nimport std/Float\nimport std/Slice\n\nprimitive Table =\n",
|
|
209
209
|
);
|
|
210
210
|
for i in 0..500 {
|
|
211
211
|
src.push_str(&format!(" E{} = {}\n", i, i * 3 + 1));
|
|
@@ -216,7 +216,7 @@ fn const_table_1000_entries() {
|
|
|
216
216
|
src.push_str(&format!(" E{} = {}\n", i, i * 3 + 1));
|
|
217
217
|
}
|
|
218
218
|
src.push_str(
|
|
219
|
-
" Flag = true\n Double = Table.E0 * 2 + 1\n\nfun table() ->
|
|
219
|
+
" Flag = true\n Double = Table.E0 * 2 + 1\n\nfun table() -> Slice[Int] =\n return Slice(",
|
|
220
220
|
);
|
|
221
221
|
let items: Vec<String> = (0..1000).map(|i| (i * i).to_string()).collect();
|
|
222
222
|
src.push_str(&items.join(", "));
|
plum-wasm-codegen/tests/inline_variants_e2e_test.rs
CHANGED
|
@@ -52,7 +52,7 @@ fn checkErrors(name: &str, src: &str) -> Vec<String> {
|
|
|
52
52
|
const HEAD: &str = "\
|
|
53
53
|
import std/Str
|
|
54
54
|
import std/Int
|
|
55
|
-
import std/
|
|
55
|
+
import std/Slice
|
|
56
56
|
|
|
57
57
|
enum Link[T] = Nil | Cons(head: T, tail: Link[T])
|
|
58
58
|
|
plum-wasm-codegen/tests/loop_break_e2e_test.rs
CHANGED
|
@@ -44,7 +44,7 @@ fn runInt(name: &str, src: &str) -> i64 {
|
|
|
44
44
|
main.call(&mut store, ()).unwrap()
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
const PRE: &str = "import std/Str\nimport std/Int\nimport std/Float\nimport std/
|
|
47
|
+
const PRE: &str = "import std/Str\nimport std/Int\nimport std/Float\nimport std/Slice\n\n";
|
|
48
48
|
|
|
49
49
|
fn prog(body: &str) -> String {
|
|
50
50
|
format!("{PRE}{body}")
|
plum-wasm-codegen/tests/own_inplace_e2e_test.rs
CHANGED
|
@@ -61,7 +61,7 @@ fn structNewCount(name: &str, src: &str) -> usize {
|
|
|
61
61
|
n
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
const PRE: &str = "import std/Str\nimport std/Int\nimport std/Float\nimport std/
|
|
64
|
+
const PRE: &str = "import std/Str\nimport std/Int\nimport std/Float\nimport std/Slice\n\n";
|
|
65
65
|
|
|
66
66
|
fn prog(body: &str) -> String {
|
|
67
67
|
format!("{PRE}{body}")
|
plum-wasm-codegen/tests/record_embed_e2e_test.rs
CHANGED
|
@@ -42,7 +42,7 @@ fn runInt(name: &str, src: &str) -> i64 {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
const PRE: &str = "import std/Str\nimport std/Int\nimport std/Float\nimport std/
|
|
45
|
+
const PRE: &str = "import std/Str\nimport std/Int\nimport std/Float\nimport std/Slice\n\n";
|
|
46
46
|
|
|
47
47
|
fn prog(body: &str) -> String {
|
|
48
48
|
format!("{PRE}{body}")
|
plum-wasm-codegen/tests/record_init_e2e_test.rs
CHANGED
|
@@ -42,17 +42,17 @@ fn runInt(name: &str, src: &str) -> i64 {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
const PRE: &str = "import std/Str\nimport std/Int\nimport std/Float\nimport std/
|
|
45
|
+
const PRE: &str = "import std/Str\nimport std/Int\nimport std/Float\nimport std/Slice\n\n";
|
|
46
46
|
|
|
47
47
|
fn prog(body: &str) -> String {
|
|
48
48
|
format!("{PRE}{body}")
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
const ACTOR: &str = "trait Actor(px: Int, py: Int, width: Int, height: Int, kids:
|
|
51
|
+
const ACTOR: &str = "trait Actor(px: Int, py: Int, width: Int, height: Int, kids: Slice[Int]) =
|
|
52
52
|
fun init(self) =
|
|
53
53
|
self.width = 10
|
|
54
54
|
self.height = self.width * 2
|
|
55
|
-
self.kids =
|
|
55
|
+
self.kids = Slice[Int]()
|
|
56
56
|
fun area(self) -> Int = self.width * self.height
|
|
57
57
|
|
|
58
58
|
";
|
plum-wasm-codegen/tests/tail_calls_e2e_test.rs
CHANGED
|
@@ -44,7 +44,7 @@ fn runInt(name: &str, src: &str) -> i64 {
|
|
|
44
44
|
main.call(&mut store, ()).unwrap()
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
const PRE: &str = "import std/Str\nimport std/Int\nimport std/Float\nimport std/
|
|
47
|
+
const PRE: &str = "import std/Str\nimport std/Int\nimport std/Float\nimport std/Slice\n\n";
|
|
48
48
|
|
|
49
49
|
fn prog(body: &str) -> String {
|
|
50
50
|
format!("{PRE}{body}")
|
plum-wasm-codegen/tests/traits_e2e_test.rs
CHANGED
|
@@ -42,7 +42,7 @@ fn runInt(name: &str, src: &str) -> i64 {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
const PRE: &str = "import std/Str\nimport std/Int\nimport std/Float\nimport std/
|
|
45
|
+
const PRE: &str = "import std/Str\nimport std/Int\nimport std/Float\nimport std/Slice\n\n";
|
|
46
46
|
|
|
47
47
|
fn prog(body: &str) -> String {
|
|
48
48
|
format!("{PRE}{body}")
|
|
@@ -74,7 +74,7 @@ record Tree(Entity(pos, z), age: Int) =
|
|
|
74
74
|
|
|
75
75
|
fun main() -> Int =
|
|
76
76
|
h := House(pos: Vec2(x: 1, y: 2), z: 0, hits: 0, kind: 3)
|
|
77
|
-
es:
|
|
77
|
+
es: Slice[Entity] := Slice[Entity]()
|
|
78
78
|
es.push(h)
|
|
79
79
|
es.push(Tree(pos: Vec2(x: 5, y: 5), z: 1, age: 10))
|
|
80
80
|
total := 0
|
|
@@ -102,7 +102,7 @@ record Ci(Shape(n), r: Int) =
|
|
|
102
102
|
override fun draw(self) -> Int = self.r + 1
|
|
103
103
|
|
|
104
104
|
fun main() -> Int =
|
|
105
|
-
xs:
|
|
105
|
+
xs: Slice[Shape] := Slice[Shape]()
|
|
106
106
|
xs.push(Sq(n: 0, s: 5))
|
|
107
107
|
xs.push(Ci(n: 0, r: 20))
|
|
108
108
|
xs.at(0).twice() * 1000 + xs.at(1).twice()
|
|
@@ -113,7 +113,7 @@ fun main() -> Int =
|
|
|
113
113
|
#[test]
|
|
114
114
|
fn three_level_actor_tree() {
|
|
115
115
|
let src = prog("\
|
|
116
|
-
trait Actor(children:
|
|
116
|
+
trait Actor(children: Slice[Actor], v: Int) =
|
|
117
117
|
fun own(self) -> Int
|
|
118
118
|
fun total(self) -> Int =
|
|
119
119
|
sum := self.own()
|
|
@@ -131,15 +131,15 @@ record Leaf(Actor(children, v)) =
|
|
|
131
131
|
override fun own(self) -> Int = self.v
|
|
132
132
|
|
|
133
133
|
fun main() -> Int =
|
|
134
|
-
l1 := Leaf(children:
|
|
134
|
+
l1 := Leaf(children: Slice[Actor](), v: 100)
|
|
135
|
-
l2 := Leaf(children:
|
|
135
|
+
l2 := Leaf(children: Slice[Actor](), v: 200)
|
|
136
|
-
mk:
|
|
136
|
+
mk: Slice[Actor] := Slice[Actor]()
|
|
137
137
|
mk.push(l1)
|
|
138
138
|
mk.push(l2)
|
|
139
139
|
m := Mid(children: mk, v: 0, w: 10)
|
|
140
|
-
rk:
|
|
140
|
+
rk: Slice[Actor] := Slice[Actor]()
|
|
141
141
|
rk.push(m)
|
|
142
|
-
rk.push(Leaf(children:
|
|
142
|
+
rk.push(Leaf(children: Slice[Actor](), v: 1000))
|
|
143
143
|
r := Root(children: rk, v: 0)
|
|
144
144
|
r.total()
|
|
145
145
|
");
|
|
@@ -355,7 +355,7 @@ fun main() -> Int =
|
|
|
355
355
|
#[test]
|
|
356
356
|
fn subtyped_with_boxed_second_self_list_and_eq() {
|
|
357
357
|
let src = prog("\
|
|
358
|
-
trait Actor(x: Int, kids:
|
|
358
|
+
trait Actor(x: Int, kids: Slice[Actor]) =
|
|
359
359
|
fun hit(self) -> Int
|
|
360
360
|
|
|
361
361
|
trait Extra(n: Int) =
|
|
@@ -366,7 +366,7 @@ record Dot(Actor(x, kids), Extra(n), w: Int) =
|
|
|
366
366
|
override fun b(self) -> Int = self.n
|
|
367
367
|
|
|
368
368
|
fun main() -> Int =
|
|
369
|
-
d := Dot(x: 1, kids:
|
|
369
|
+
d := Dot(x: 1, kids: Slice[Actor](), n: 6, w: 3)
|
|
370
370
|
a: Actor := d
|
|
371
371
|
e: Extra := d
|
|
372
372
|
e.n = 8
|
plum-wasm-codegen/tests/type_param_static_test.rs
CHANGED
|
@@ -99,19 +99,19 @@ fun main() -> Int =
|
|
|
99
99
|
fn nested_generic() {
|
|
100
100
|
let src = "\
|
|
101
101
|
import std/Int
|
|
102
|
-
import std/
|
|
102
|
+
import std/Slice
|
|
103
103
|
|
|
104
104
|
record Pt(x: Int) =
|
|
105
105
|
fun make(n: Int) -> Pt = Pt(x: n + 1)
|
|
106
106
|
|
|
107
|
-
fun buildAll(ns:
|
|
107
|
+
fun buildAll(ns: Slice[Int]) -> Slice[T] =
|
|
108
|
-
out :=
|
|
108
|
+
out := Slice[T]()
|
|
109
109
|
for n := range ns
|
|
110
110
|
out.push(T.make(n))
|
|
111
111
|
return out
|
|
112
112
|
|
|
113
113
|
fun main() -> Int =
|
|
114
|
-
ps:
|
|
114
|
+
ps: Slice[Pt] := buildAll(Slice(1, 2, 3))
|
|
115
115
|
p := ps.get(2) catch e
|
|
116
116
|
return 0
|
|
117
117
|
return p.x
|
|
@@ -142,10 +142,10 @@ fun main() -> Int =
|
|
|
142
142
|
#[test]
|
|
143
143
|
fn extension_outside_std_is_rejected() {
|
|
144
144
|
let src = "\
|
|
145
|
-
import std/
|
|
145
|
+
import std/Slice
|
|
146
146
|
import std/Int
|
|
147
147
|
|
|
148
|
-
primitive
|
|
148
|
+
primitive Slice[T] =
|
|
149
149
|
fun extra(self) -> Int = 1
|
|
150
150
|
|
|
151
151
|
fun main() -> Int = 0
|
plum-wasm-codegen/tests/unboxed_variants_e2e_test.rs
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#![allow(non_snake_case)]
|
|
2
|
+
//! Unboxed layout: union-enum members and inline variants are wasm-gc subtypes
|
|
3
|
+
//! of the enum, so constructing one allocates exactly one struct.
|
|
4
|
+
use plum_wasm_codegen::compileSource;
|
|
5
|
+
use wasmparser::{Operator, Parser, Payload};
|
|
6
|
+
|
|
7
|
+
fn repoRoot() -> std::path::PathBuf {
|
|
8
|
+
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
fn compile(name: &str, src: &str) -> Vec<u8> {
|
|
12
|
+
let dir = std::env::temp_dir().join(format!("plum_unboxed_{}_{}", name, std::process::id()));
|
|
13
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
14
|
+
let path = dir.join("main.plum");
|
|
15
|
+
std::fs::write(&path, src).unwrap();
|
|
16
|
+
let r = plum_core::loadAndMerge(&path, &repoRoot());
|
|
17
|
+
let _ = std::fs::remove_dir_all(&dir);
|
|
18
|
+
let source = r.unwrap_or_else(|e| panic!("load failed: {e}"));
|
|
19
|
+
let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
20
|
+
wasmparser::validate(&bytes).expect("valid wasm");
|
|
21
|
+
bytes
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// Number of `struct.new` instructions in the exported `main` function.
|
|
25
|
+
fn structNewsInMain(bytes: &[u8]) -> usize {
|
|
26
|
+
let mut imports = 0u32;
|
|
27
|
+
let mut mainIdx = None;
|
|
28
|
+
let mut bodies = Vec::new();
|
|
29
|
+
for p in Parser::new(0).parse_all(bytes) {
|
|
30
|
+
match p.unwrap() {
|
|
31
|
+
Payload::ImportSection(s) => {
|
|
32
|
+
for i in s {
|
|
33
|
+
if matches!(i.unwrap().ty, wasmparser::TypeRef::Func(_)) {
|
|
34
|
+
imports += 1;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
Payload::ExportSection(s) => {
|
|
39
|
+
for e in s {
|
|
40
|
+
let e = e.unwrap();
|
|
41
|
+
if e.name == "main" {
|
|
42
|
+
mainIdx = Some(e.index);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
Payload::CodeSectionEntry(b) => bodies.push(b),
|
|
47
|
+
_ => {}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
let body = &bodies[(mainIdx.expect("main export") - imports) as usize];
|
|
51
|
+
let mut n = 0;
|
|
52
|
+
let mut r = body.get_operators_reader().unwrap();
|
|
53
|
+
while !r.eof() {
|
|
54
|
+
if matches!(r.read().unwrap(), Operator::StructNew { .. }) {
|
|
55
|
+
n += 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
n
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fn runInt(name: &str, src: &str) -> i64 {
|
|
62
|
+
let bytes = compile(name, src);
|
|
63
|
+
let mut config = wasmtime::Config::new();
|
|
64
|
+
config.wasm_gc(true);
|
|
65
|
+
config.wasm_function_references(true);
|
|
66
|
+
let engine = wasmtime::Engine::new(&config).expect("engine");
|
|
67
|
+
let module = wasmtime::Module::new(&engine, &bytes).unwrap();
|
|
68
|
+
let mut store = wasmtime::Store::new(&engine, ());
|
|
69
|
+
let mut linker = wasmtime::Linker::new(&engine);
|
|
70
|
+
linker
|
|
71
|
+
.define_unknown_imports_as_default_values(&mut store, &module)
|
|
72
|
+
.unwrap();
|
|
73
|
+
let instance = linker.instantiate(&mut store, &module).unwrap();
|
|
74
|
+
let main = instance
|
|
75
|
+
.get_typed_func::<(), i64>(&mut store, "main")
|
|
76
|
+
.unwrap();
|
|
77
|
+
main.call(&mut store, ()).unwrap()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const HEAD: &str = "\
|
|
81
|
+
import std/Str
|
|
82
|
+
import std/Int
|
|
83
|
+
import std/Float
|
|
84
|
+
|
|
85
|
+
";
|
|
86
|
+
|
|
87
|
+
#[test]
|
|
88
|
+
fn cons_one_struct_new() {
|
|
89
|
+
let src = format!(
|
|
90
|
+
"{HEAD}\
|
|
91
|
+
enum Link[T] = End | Pair(head: T, tail: Link[T])
|
|
92
|
+
|
|
93
|
+
fun main() -> Int =
|
|
94
|
+
xs: Link[Int] := Pair(head: 1, tail: End)
|
|
95
|
+
1
|
|
96
|
+
"
|
|
97
|
+
);
|
|
98
|
+
assert_eq!(structNewsInMain(&compile("cons1", &src)), 1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#[test]
|
|
102
|
+
fn union_member_one_struct_new() {
|
|
103
|
+
let src = format!(
|
|
104
|
+
"{HEAD}\
|
|
105
|
+
record Circle(r: Float)
|
|
106
|
+
record Rect(w: Float, h: Float)
|
|
107
|
+
enum Shape = Circle | Rect
|
|
108
|
+
|
|
109
|
+
fun main() -> Int =
|
|
110
|
+
s: Shape := Circle(r: 2.0)
|
|
111
|
+
t: Shape := Rect(w: 1.0, h: 2.0)
|
|
112
|
+
1
|
|
113
|
+
"
|
|
114
|
+
);
|
|
115
|
+
assert_eq!(structNewsInMain(&compile("union1", &src)), 2);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#[test]
|
|
119
|
+
fn generic_link_instantiations_separate() {
|
|
120
|
+
let src = format!(
|
|
121
|
+
"{HEAD}\
|
|
122
|
+
enum Link[T] = End | Pair(head: T, tail: Link[T])
|
|
123
|
+
|
|
124
|
+
fun main() -> Int =
|
|
125
|
+
a: Link[Int] := Pair(head: 1, tail: End)
|
|
126
|
+
b: Link[Int] := Pair(head: 2, tail: a)
|
|
127
|
+
c: Link[Float] := Pair(head: 1.5, tail: End)
|
|
128
|
+
1
|
|
129
|
+
"
|
|
130
|
+
);
|
|
131
|
+
assert_eq!(structNewsInMain(&compile("generic", &src)), 3);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#[test]
|
|
135
|
+
fn own_record_member_ok() {
|
|
136
|
+
let src = format!(
|
|
137
|
+
"{HEAD}\
|
|
138
|
+
own record Node(v: Int)
|
|
139
|
+
record Leaf(v: Int)
|
|
140
|
+
enum Tree = Node | Leaf
|
|
141
|
+
|
|
142
|
+
fun main() -> Int =
|
|
143
|
+
t: Tree := Node(v: 3)
|
|
144
|
+
1
|
|
145
|
+
"
|
|
146
|
+
);
|
|
147
|
+
assert_eq!(structNewsInMain(&compile("own", &src)), 1);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#[test]
|
|
151
|
+
fn match_member_fields() {
|
|
152
|
+
let src = format!(
|
|
153
|
+
"{HEAD}\
|
|
154
|
+
record Circle(r: Int)
|
|
155
|
+
record Rect(w: Int, h: Int)
|
|
156
|
+
enum Shape = Circle | Rect
|
|
157
|
+
|
|
158
|
+
fun area(s: Shape) -> Int =
|
|
159
|
+
match s
|
|
160
|
+
Circle(r) => r * r
|
|
161
|
+
Rect(w, h) => w * h
|
|
162
|
+
|
|
163
|
+
fun main() -> Int =
|
|
164
|
+
area(Circle(r: 3)) * 100 + area(Rect(w: 2, h: 5))
|
|
165
|
+
"
|
|
166
|
+
);
|
|
167
|
+
assert_eq!(runInt("match_fields", &src), 910);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
#[test]
|
|
171
|
+
fn nested_enum_match() {
|
|
172
|
+
let src = format!(
|
|
173
|
+
"{HEAD}\
|
|
174
|
+
enum Link[T] = End | Pair(head: T, tail: Link[T])
|
|
175
|
+
|
|
176
|
+
fun sum(l: Link[Int]) -> Int =
|
|
177
|
+
match l
|
|
178
|
+
End => 0
|
|
179
|
+
Pair(head, tail) => head + sum(tail)
|
|
180
|
+
|
|
181
|
+
fun second(l: Link[Int]) -> Int =
|
|
182
|
+
match l
|
|
183
|
+
Pair(_, Pair(h, _)) => h
|
|
184
|
+
_ => -1
|
|
185
|
+
|
|
186
|
+
fun main() -> Int =
|
|
187
|
+
l: Link[Int] := Pair(head: 1, tail: Pair(head: 20, tail: Pair(head: 300, tail: End)))
|
|
188
|
+
sum(l) * 1000 + second(l)
|
|
189
|
+
"
|
|
190
|
+
);
|
|
191
|
+
assert_eq!(runInt("nested", &src), 321020);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
#[test]
|
|
195
|
+
fn equality_and_toStr() {
|
|
196
|
+
let src = format!(
|
|
197
|
+
"{HEAD}\
|
|
198
|
+
record Circle(r: Int) =
|
|
199
|
+
fun toStr(self) -> Str = \"Circle\"
|
|
200
|
+
|
|
201
|
+
record Rect(w: Int, h: Int) =
|
|
202
|
+
fun toStr(self) -> Str = \"Rect\"
|
|
203
|
+
|
|
204
|
+
enum Shape = Circle | Rect
|
|
205
|
+
|
|
206
|
+
fun main() -> Int =
|
|
207
|
+
a: Shape := Circle(r: 2)
|
|
208
|
+
b: Shape := a
|
|
209
|
+
c: Shape := Rect(w: 1, h: 2)
|
|
210
|
+
x := if a == b then 1 else 0
|
|
211
|
+
y := if a != c then 10 else 0
|
|
212
|
+
z := if c.toStr() == \"Rect\" then 100 else 0
|
|
213
|
+
x + y + z
|
|
214
|
+
"
|
|
215
|
+
);
|
|
216
|
+
assert_eq!(runInt("eq_tostr", &src), 111);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
#[test]
|
|
220
|
+
fn json_round_trip_union() {
|
|
221
|
+
let src = format!(
|
|
222
|
+
"{HEAD}\
|
|
223
|
+
import std/Json
|
|
224
|
+
|
|
225
|
+
record Circle(r: Int) =
|
|
226
|
+
fun toJson(self) -> Json = Json.object(\"r\" => self.r.toJson())
|
|
227
|
+
|
|
228
|
+
fun fromJson(j: Json) -> JsonError!Circle =
|
|
229
|
+
return Circle(r: try Int.fromJson(try j.field(\"r\")))
|
|
230
|
+
|
|
231
|
+
record Rect(w: Int, h: Int) =
|
|
232
|
+
fun toJson(self) -> Json = Json.object(\"w\" => self.w.toJson(), \"h\" => self.h.toJson())
|
|
233
|
+
|
|
234
|
+
fun fromJson(j: Json) -> JsonError!Rect =
|
|
235
|
+
return Rect(w: try Int.fromJson(try j.field(\"w\")), h: try Int.fromJson(try j.field(\"h\")))
|
|
236
|
+
|
|
237
|
+
enum Shape = Circle | Rect
|
|
238
|
+
|
|
239
|
+
fun main() -> Int =
|
|
240
|
+
a: Shape := Circle(r: 7)
|
|
241
|
+
b: Shape := Rect(w: 3, h: 4)
|
|
242
|
+
ja := Json.encode(a)
|
|
243
|
+
jb := Json.encode(b)
|
|
244
|
+
a2: JsonError!Circle := Json.decode(ja)
|
|
245
|
+
b2: JsonError!Rect := Json.decode(jb)
|
|
246
|
+
s2: Shape := b2!
|
|
247
|
+
ok := Json.encode(a2!) == ja && Json.encode(s2) == jb
|
|
248
|
+
if ok then 1 else 0
|
|
249
|
+
"
|
|
250
|
+
);
|
|
251
|
+
assert_eq!(runInt("json_union", &src), 1);
|
|
252
|
+
}
|
plum-wasm-codegen/tests/unions_e2e_test.rs
CHANGED
|
@@ -53,7 +53,7 @@ const DEFS: &str = "\
|
|
|
53
53
|
import std/Str
|
|
54
54
|
import std/Int
|
|
55
55
|
import std/Float
|
|
56
|
-
import std/
|
|
56
|
+
import std/Slice
|
|
57
57
|
|
|
58
58
|
struct Circle(r: Float) =
|
|
59
59
|
fun area(self) -> Float = self.r * self.r * 3.0
|
|
@@ -90,8 +90,8 @@ fun main() -> Int =
|
|
|
90
90
|
",
|
|
91
91
|
);
|
|
92
92
|
let src = src.replace(
|
|
93
|
-
"import std/
|
|
93
|
+
"import std/Slice\n",
|
|
94
|
-
"import std/
|
|
94
|
+
"import std/Slice\n\nerror ParseError = Bad | Worse\n",
|
|
95
95
|
);
|
|
96
96
|
assert_eq!(runInt("optional", &src), 19);
|
|
97
97
|
}
|
|
@@ -100,10 +100,10 @@ fun main() -> Int =
|
|
|
100
100
|
fn invariant_list_rejected() {
|
|
101
101
|
let src = withDefs(
|
|
102
102
|
"\
|
|
103
|
-
fun take(xs:
|
|
103
|
+
fun take(xs: Slice[Shape]) -> Int =
|
|
104
104
|
1
|
|
105
105
|
|
|
106
|
-
fun go(cs:
|
|
106
|
+
fun go(cs: Slice[Circle]) -> Int =
|
|
107
107
|
take(cs)
|
|
108
108
|
",
|
|
109
109
|
);
|