plum

#treesitter#compiler#wasm

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
Files changed (91) hide show
  1. README.md +107 -41
  2. docs/plans/2026-11-03-unboxed-variants.benchmark.md +23 -0
  3. docs/plans/2026-11-03-unboxed-variants.md +174 -0
  4. docs/plans/2026-11-03-unboxed-variants.progress.md +41 -0
  5. docs/plans/2026-11-03-unboxed-variants.report.json +363 -0
  6. docs/plans/2026-11-03-unboxed-variants.report.md +53 -0
  7. docs/plans/2026-11-03-unboxed-variants.review.md +69 -0
  8. plum-checker/src/lib.rs +17 -4
  9. plum-checker/src/ownership.rs +2 -2
  10. plum-checker/tests/checker_tests.rs +16 -16
  11. plum-checker/tests/inline_variants_tests.rs +1 -1
  12. plum-checker/tests/ownership_flow_test.rs +16 -8
  13. plum-checker/tests/ownership_moves_test.rs +1 -1
  14. plum-checker/tests/types_tests.rs +5 -5
  15. plum-checker/tests/unions_tests.rs +1 -1
  16. plum-core/src/forms.rs +11 -5
  17. plum-core/src/loader.rs +1 -1
  18. plum-core/tests/formatter_test.rs +8 -8
  19. plum-core/tests/inline_variants_test.rs +1 -1
  20. plum-core/tests/parser_test.rs +7 -7
  21. plum-core/tests/record_init_test.rs +2 -2
  22. plum-core/tests/traits_test.rs +2 -2
  23. plum-core/tests/trmc_test.rs +3 -3
  24. plum-core/tests/union_lower_test.rs +1 -1
  25. plum-core/tests/unions_test.rs +2 -2
  26. plum-examples/annotated_locals.plum +2 -2
  27. plum-examples/cons_list.plum +70 -0
  28. plum-examples/constants.plum +2 -2
  29. plum-examples/error_propagation.plum +17 -17
  30. plum-examples/forms.plum +16 -16
  31. plum-examples/functional.plum +91 -90
  32. plum-examples/game.plum +4 -4
  33. plum-examples/game_loop.plum +6 -6
  34. plum-examples/game_pure_scene.plum +20 -20
  35. plum-examples/game_scene.plum +6 -6
  36. plum-examples/game_state_view.plum +50 -51
  37. plum-examples/generics.plum +21 -21
  38. plum-examples/json.plum +7 -7
  39. plum-examples/loops.plum +10 -10
  40. plum-examples/optionals.plum +5 -5
  41. plum-examples/own_list.plum +5 -5
  42. plum-examples/scene.plum +7 -7
  43. plum-examples/seq.plum +19 -19
  44. plum-examples/strings.plum +2 -2
  45. plum-examples/struct_arrays.plum +8 -8
  46. plum-examples/struct_union_members.plum +4 -4
  47. plum-examples/structs_records.plum +1 -1
  48. plum-examples/tail_calls.plum +6 -6
  49. plum-examples/trait_scene.plum +4 -4
  50. plum-examples/traits.plum +2 -2
  51. plum-examples/unboxed_variants.plum +337 -0
  52. plum-examples/unions.plum +13 -13
  53. plum-game/web-test/fixtures/stress_pure_scene.plum +8 -8
  54. plum-game/web-test/fixtures/stress_scene.plum +1 -1
  55. plum-game/web-test/fixtures/stress_state_view.plum +17 -17
  56. plum-game/web-test/fixtures/stress_state_view_deep.plum +25 -25
  57. plum-game/web-test/fixtures/stress_world.plum +1 -1
  58. plum-lsp/src/main.rs +4 -4
  59. plum-std/Array.plum +2 -2
  60. plum-std/Deque.plum +1 -1
  61. plum-std/Heap.plum +4 -4
  62. plum-std/Http.plum +1 -1
  63. plum-std/Json.plum +30 -30
  64. plum-std/List.plum +136 -0
  65. plum-std/Map.plum +1 -1
  66. plum-std/Os.plum +3 -3
  67. plum-std/Seq.plum +16 -16
  68. plum-std/Set.plum +3 -3
  69. plum-std/Slice.plum +44 -44
  70. plum-std/Str.plum +5 -5
  71. plum-std/Testing.plum +16 -16
  72. plum-std/game/Actions.plum +16 -16
  73. plum-std/game/Picture.plum +25 -25
  74. plum-std/game/PureScene.plum +16 -16
  75. plum-std/game/Scene.plum +10 -10
  76. plum-std/game/SceneInput.plum +18 -18
  77. plum-std/game/SceneTree.plum +32 -32
  78. plum-wasm-codegen/src/lib.rs +137 -3
  79. plum-wasm-codegen/tests/codegen_tests.rs +41 -41
  80. plum-wasm-codegen/tests/enum_init_e2e_test.rs +148 -0
  81. plum-wasm-codegen/tests/examples_test.rs +3 -3
  82. plum-wasm-codegen/tests/inline_variants_e2e_test.rs +1 -1
  83. plum-wasm-codegen/tests/loop_break_e2e_test.rs +1 -1
  84. plum-wasm-codegen/tests/own_inplace_e2e_test.rs +1 -1
  85. plum-wasm-codegen/tests/record_embed_e2e_test.rs +1 -1
  86. plum-wasm-codegen/tests/record_init_e2e_test.rs +3 -3
  87. plum-wasm-codegen/tests/tail_calls_e2e_test.rs +1 -1
  88. plum-wasm-codegen/tests/traits_e2e_test.rs +11 -11
  89. plum-wasm-codegen/tests/type_param_static_test.rs +6 -6
  90. plum-wasm-codegen/tests/unboxed_variants_e2e_test.rs +252 -0
  91. 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/List
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: List[Int]) -> Int? = # `T?` is a T or null
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 List(1, 5, 9).map(), if x > 2
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(List(1, 3, 4)) ?: -1 # `?:` supplies a default for null
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(List(1, 3)) == null
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 := List(1, 2, 3)
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`, `List`, ... are available once the file imports `std/Int`, `std/Float`, `std/Str`, `std/List`, ...
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: List[Shape] := List[Shape]()
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 `List[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).
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`, `List[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:
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
- | List[Json]
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
- List => "list"
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 := List[Entity]()
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/List
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 := List(Cell(x: 1.0), Cell(x: 2.0))
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
- - **Lists:** `List.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).
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: List[Int] = List[Int]()
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): `List[Int]` and `List[Str]` are different code, and a `List[Float]` stores raw `f64`s.
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: List[T], num: Num[T]) -> T =
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(List(1, 2, 3), intNum) # 6
876
+ sumWith(Slice(1, 2, 3), intNum) # 6
811
- sumWith(List(1.5, 2.5), floatNum) # 4.0
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 `List` 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.
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. `List` 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).
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 `List`, `Array`, `Deque`, a variadic parameter, your own type);
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 `List` receiver);
975
- - **terminals** take the `for` body (the predicate, the mapping, the step) and must be the last call: `filter()` (`-> List[T]`), `map()` (`-> List[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()`.
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 `List`. The method chain below compiles to the same single loop as the hand-written one:
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`, `List`, ...) 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.
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: List[Int?]) -> Int =
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: `List.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: `List.head`, the `find()` terminal and `Map.getOrNull` return `T?`.
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 List(10, 20, 30).get(i) # error{OutOfBounds}
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 (`List[!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).
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 `List` element, a generic container) it is boxed into a `{value, code}` struct.
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 = List(Actions.forever(Actions.rotateBy(1.0, 1.0)))
1383
+ spr.actions = Slice(Actions.forever(Actions.rotateBy(1.0, 1.0)))
1318
1384
  sp: Actor := spr
1319
- kids := List[Actor]()
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 `List`, `Map` and `Str` are written on, with `Array.copy` and `Array.equal` for scalar ranges; `Grid[T]` is a flat 2D array
1378
- - **`List[T]`** (`import std/List`) — 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 `List` 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
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`, `List[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`.
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
- - `List[Int].fromJson(j)` does not parse; use `j.into()` or `Json.decode`.
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 `List`, a `List.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).
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 `List` and `Array` methods are function calls today. Inlining them removes the call and lets the wasm engine keep values in registers.
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
- && ctx
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
- /// `List[T]` locals whose element type `T` is known.
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 == "List" {
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 List[T] =
140
+ enum Slice[T] =
141
- | List(items: T)
141
+ | Slice(items: T)
142
142
 
143
- fun map(self, cb: fn(T) -> U) -> List[U] =
143
+ fun map(self, cb: fn(T) -> U) -> Slice[U] =
144
- List(items: cb(self.items))
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: List[Int]) -> List[Int] =\n xs.map(|n| n * 2)\n"),
197
+ lastFnBodyType("fun f(xs: Slice[Int]) -> Slice[Int] =\n xs.map(|n| n * 2)\n"),
198
- tapp("List", vec![PlumType::TInt])
198
+ tapp("Slice", vec![PlumType::TInt])
199
199
  );
200
200
  assert_eq!(
201
- lastFnBodyType("fun f(xs: List[Int]) -> List[Str] =\n xs.map(|n| \"{n}\")\n"),
201
+ lastFnBodyType("fun f(xs: Slice[Int]) -> Slice[Str] =\n xs.map(|n| \"{n}\")\n"),
202
- tapp("List", vec![plum_checker::types::strType()])
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: List[Int]) -> List[Int] =\n xs.map(|n| \"{{n}}\")\n",
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 := List(items: 1).map(|n| n * 2)\n b := List(items: \"x\").map(|s| s)\n 0\n",
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 == "List.map" && i.type_args.first() == Some(&t))
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() -> List[Box[Str]] =\n List(items: Box(value: 1))\n"), &["variant 'Box' field 'value': type mismatch"]),
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() -> List[Str] =\n List(items: 1).map(|x| x + 1)\n"), &["method 'List.map' arg 0: type mismatch"]),
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: List[Int]) -> List[Int] =\n xs.map(|n| \"{n}\")\n"), &["type mismatch"]),
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(List(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(List(1))\n i\n 0\n", &["sequence parameter 'src' can only be used as a for source"]),
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() -> List[Link[Int]] =\n List(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");
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: List[Sprite], ys: List[Sprite]) =\n ys.push(xs.at(0))\n");
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: List[Sprite], ys: List[Sprite]) =\n s := xs.at(0)\n ys.push(s)\n",
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: List[Sprite]) -> Float =\n xs.at(0).x\n");
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: List[Sprite]) =
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: List[Sprite])\n\nfun main(own h: Handled) -> List[Sprite] =\n match h\n Handled(a, _) => take(a)\n h.events\n",
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: List[Sprite])\n\nfun main(h: Handled, xs: List[Sprite]) =\n match h\n Handled(a, _) => xs.push(a)\n",
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: List[Sprite])\n\nfun main(h: Handled) -> Float =\n match h\n Handled(a, _) => a.x\n",
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: List[Sprite])\n\nfun main(own h: Handled) -> Float =\n match h\n Handled(a, _) =>\n t := take(a)\n a.x\n",
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: List[Sprite]) =\n xs.push(s)\n");
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("List".into(), vec![pinned_a(TParam("T".into()))]);
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(), "List[error{A}!Int]");
17
+ assert_eq!(out.to_string(), "Slice[error{A}!Int]");
18
- assert_eq!(out, TApp("List".into(), vec![pinned_a(TInt)]));
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("List".into(), vec![TInt])
48
+ TApp("Slice".into(), vec![TInt])
49
49
  ]
50
50
  )
51
51
  .to_string(),
52
- "Map[Str, List[Int]]"
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: List[Shape]) -> Int =\n 1\n\nfun go(cs: List[Circle]) -> Int =\n take(cs)", &["expected List[Shape], found List[Circle]"]),
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 `List[T]` standing for the sequence.
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: "List".to_string(),
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() == 2 && !self.reg.methodNames.is_empty() {
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, "entries".to_string())) {
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: "List".to_string(),
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 List[T] =` (any non-built-in name that is also a declared
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?) -> List[Int?] =\n a\n";
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("-> List[Int?] ="), "{first}");
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() -> !List[Int] =\n return error.A\n").expect("format");
254
+ let g = formatSource("fun f() -> !Slice[Int] =\n return error.A\n").expect("format");
255
- assert!(g.contains("-> !List[Int] ="), "{g}");
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 | List[Js] | Map[Str, Js]\n").expect("format");
299
+ let g = formatSource("enum Js = JsNull | Slice[Js] | Map[Str, Js]\n").expect("format");
300
300
  assert!(
301
- g.contains("enum Js = JsNull | List[Js] | Map[Str, Js]\n"),
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: List[Shape] := List[Shape]()\n f:fn(Int)->Int:=twice\n o: Int? := null\n";
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: List[Shape] := List[Shape]()\n"),
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: List[Cons[Int]]) -> Int = 1\n",
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() -> List[Int?] =\n 1\n");
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("List", vec![inner])));
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() -> !List[Int] =\n 1\n");
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, "List");
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 | List[Js] | Map[Str, Js]");
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: List[Shape] := List[Shape]()\n");
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()), ("List", 1));
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 = List[Int]()\n self.s = \"x\"\n self.b = true\n");
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, "List");
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: List[Act])\nrecord Nod(Act)\n");
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, "List");
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 record + its wrapper variant per cell, in one loop (two loops before).
306
+ // One struct.new per cell (the member is the enum subtype), in one loop.
307
- assert_eq!(structNewCount(&bytes, "map"), 2);
307
+ assert_eq!(structNewCount(&bytes, "map"), 1);
308
- assert_eq!(structNewCount(&bytes, "fromTo"), 2);
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: List[Int]) =\n match s\n Circle =>\n for s := range xs\n s.r\n",
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 List(n: Int)\nenum Js = List[Int] | List[Str]");
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 List members; a pattern 'List' would be ambiguous"]
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/List
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: List[Shape] := List[Shape]()
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/List
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 = List("a", "b", "c")
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/List
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 := List(10, 20, 30)
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: List[Int]) -> error{ListWasEmpty}!Int =
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: List[Int], default: Int) -> Int =
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: List[T]) =
151
+ record Ages[T](items: Slice[T]) =
152
- fun parseAll(xs: List[Str]) -> !Ages[T] =
152
+ fun parseAll(xs: Slice[Str]) -> !Ages[T] =
153
- out := List[T]()
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: List[Str]) -> !Ages[Age] = Ages.parseAll(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(List("3", "4"))!
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(List("3", "-1")) catch e
189
+ failed := parseAges(Slice("3", "-1")) catch e
190
190
  assert e.name() == "NegativeNum"
191
- Ages(List[Age]())
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(List(5))! == 10
195
+ assert doubledFirst(Slice(5))! == 10
196
- assert {doubledFirst(List[Int]()) catch -1} == -1
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(List(5), 99) == 5
203
+ assert firstOrDefault(Slice(5), 99) == 5
204
- assert firstOrDefault(List[Int](), 99) == 99
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 `List[!Int]` holds
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 := List(parsePositive("1"), parsePositive("-1"), parsePositive("x"))
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/List
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() -> List[Fighter] =
17
+ fun sampleFighters() -> Slice[Fighter] =
18
- List(Fighter(name: "a", hp: 10, dead: false), Fighter(name: "b", hp: 0, dead: true), Fighter(name: "c", hp: 7, dead: false))
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: List[T]) *T -> Int =
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: List[Int]) *Int -> Int =
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: List[Int]) -> Int =
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: List[Int]) -> List[Int] =
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: List[Int]) -> Int =
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(List(1, 2, 3, 4, 5)), if x % 2 == 1
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 := List(1, 2, 3, 4, 5)
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(List(1, 20, 30)) == 20
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 := List(1, 2, 3, 4, 5, 6)
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 := List(1, 2)
119
+ xs := Slice(1, 2)
120
- ys := List(10, 20)
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(List(1, 2, 3))
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(List(1, 2, 3)) == 12
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, `Cons(head: T, tail: Link[T])`. A variant is a constructor and a
5
+ # fields, `Link(head: T, tail: Chain[T])`. A variant is a constructor and a
6
- # pattern only; `Link[T]` is the only type, and fields are read by matching.
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. Link[T]: a cons list (`Nil` or `Cons(head, tail)`), combinators as methods on the enum
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
- # `Link[Node]` of children; acting, drawing, hit testing and event routing
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/List
27
+ import std/Slice
28
28
 
29
29
  # ---------------------------------------------------------------------------
30
- # 1. Link[T]: a cons list
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 (`Nil`, a tag) or a head value in front of another list.
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 Link[T] =
36
+ enum Chain[T] =
36
- | Nil
37
+ | Empty
37
- | Cons(head: T, tail: Link[T])
38
+ | Link(head: T, tail: Chain[T])
38
39
 
39
40
  fun length(self) -> Int = match self
40
- Nil => 0
41
+ Empty => 0
41
- Cons(_, t) => 1 + t.length()
42
+ Link(_, t) => 1 + t.length()
42
43
 
43
44
  fun isEmpty(self) -> Bool = match self
44
- Nil => true
45
+ Empty => true
45
- Cons(_, _) => false
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 Cons(h, t) := self
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 Cons(h, t) := cur
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) -> Link[B] = match self
65
+ fun map(self, f: fn(T) -> B) -> Chain[B] = match self
65
- Nil => Nil
66
+ Empty => Empty
66
- Cons(h, t) => Cons(head: f(h), tail: t.map(f))
67
+ Link(h, t) => Link(head: f(h), tail: t.map(f))
67
68
 
68
- fun filter(self, keep: fn(T) -> Bool) -> Link[T] = match self
69
+ fun filter(self, keep: fn(T) -> Bool) -> Chain[T] = match self
69
- Nil => Nil
70
+ Empty => Empty
70
- Cons(h, t) =>
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
- Nil => acc
77
+ Empty => acc
77
- Cons(h, t) => t.foldl(f(acc, h), f)
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
- Nil => acc
82
+ Empty => acc
82
- Cons(h, t) => f(h, t.foldr(acc, f))
83
+ Link(h, t) => f(h, t.foldr(acc, f))
83
84
 
84
- fun reverse(self) -> Link[T] =
85
+ fun reverse(self) -> Chain[T] =
85
- self.foldl(Nil, |acc, x| cons(x, acc))
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: Link[T]) -> Link[T] =
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
- Nil => false
93
+ Empty => false
93
- Cons(h, t) => p(h) || t.any(p)
94
+ Link(h, t) => p(h) || t.any(p)
94
95
 
95
96
  fun all(self, p: fn(T) -> Bool) -> Bool = match self
96
- Nil => true
97
+ Empty => true
97
- Cons(h, t) => p(h) && t.all(p)
98
+ Link(h, t) => p(h) && t.all(p)
98
99
 
99
- fun take(self, n: Int) -> Link[T] = match self
100
+ fun take(self, n: Int) -> Chain[T] = match self
100
- Nil => Nil
101
+ Empty => Empty
101
- Cons(h, t) => if n <= 0 then Nil else cons(h, t.take(n - 1))
102
+ Link(h, t) => if n <= 0 then Empty else cons(h, t.take(n - 1))
102
103
 
103
- fun drop(self, n: Int) -> Link[T] = match self
104
+ fun drop(self, n: Int) -> Chain[T] = match self
104
- Nil => Nil
105
+ Empty => Empty
105
- Cons(_, t) => if n <= 0 then self else t.drop(n - 1)
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
- Nil => null
110
+ Empty => null
110
- Cons(h, _) => h
111
+ Link(h, _) => h
111
112
 
112
113
  # Pairs up two lists, stopping at the shorter one.
113
- fun zip(self, ys: Link[B]) -> Link[Pair[T, B]] = match self
114
+ fun zip(self, ys: Chain[B]) -> Chain[Pair[T, B]] = match self
114
- Nil => Nil
115
+ Empty => Empty
115
- Cons(x, xt) =>
116
+ Link(x, xt) =>
116
117
  match ys
117
- Nil => Nil
118
+ Empty => Empty
118
- Cons(y, yt) => cons(Pair[T, B](fst: x, snd: y), xt.zip(yt))
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: Link[T]) -> Link[T] = Cons(head: h, tail: t)
124
+ fun cons(h: T, t: Chain[T]) -> Chain[T] = Link(head: h, tail: t)
124
125
 
125
- fun single(x: T) -> Link[T] = cons(x, Nil)
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) -> Link[Int] =
129
+ fun fromTo(a: Int, b: Int) -> Chain[Int] =
129
- if a >= b then Nil else cons(a, fromTo(a + 1, b))
130
+ if a >= b then Empty else cons(a, fromTo(a + 1, b))
130
131
 
131
- fun sum(xs: Link[Int]) -> Int =
132
+ fun sum(xs: Chain[Int]) -> Int =
132
133
  xs.foldl(0, |a, x| a + x)
133
134
 
134
- fun joinInts(xs: Link[Int], sep: Str) -> Str = match xs
135
+ fun joinInts(xs: Chain[Int], sep: Str) -> Str = match xs
135
- Nil => ""
136
+ Empty => ""
136
- Cons(h, t) =>
137
+ Link(h, t) =>
137
138
  match t
138
- Nil => "{h}"
139
+ Empty => "{h}"
139
- Cons(_, _) => "{h}{sep}{joinInts(t, sep)}"
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) -> Link[Int] =
201
+ fun inOrder(self) -> Chain[Int] =
201
- self.fold(Nil, |x, acc| cons(x, acc))
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: Link[Int]) -> Tree =
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: Link[Int]) -> Str = match xs
231
+ fun firstTwo(xs: Chain[Int]) -> Str = match xs
231
- Cons(a, Cons(b, _)) => "{a},{b}"
232
+ Link(a, Link(b, _)) => "{a},{b}"
232
- Cons(a, Nil) => "{a}"
233
+ Link(a, Empty) => "{a}"
233
- Nil => "empty"
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: Link[Node])
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) -> Link[DrawCmd] =
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(Nil, |k, acc| k.draw(xf).append(acc))
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: Link[Node], xf: Xf, px: Float, py: Float) -> Int? = match kids
334
+ fun firstHit(kids: Chain[Node], xf: Xf, px: Float, py: Float) -> Int? = match kids
334
- Nil => null
335
+ Empty => null
335
- Cons(k, rest) =>
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: Link[UiEvent])
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: Nil)
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: Link[Int]) -> Int =
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: Link[Int]) -> Int =
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: Link[Int]) -> Int =
383
+ fun sumEvensWhile(xs: Chain[Int]) -> Int =
383
384
  total := 0
384
385
  cur := xs
385
386
  while true
386
- if Cons(h, t) := cur
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 `Nil` has no element type of its own, so `len(Nil)` is an error ("cannot
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: Link[Int] := Nil`.
398
+ # infer T"); annotate it, as `out` is here, or `empty: Chain[Int] := Empty`.
398
- fun* collect(xs: Link[T]) *T -> Link[T] =
399
+ fun* collect(xs: Chain[T]) *T -> Chain[T] =
399
- out: Link[T] := Nil
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: Link[Int], n: Int) -> Int = match xs
407
+ fun countUp(xs: Chain[Int], n: Int) -> Int = match xs
407
- Nil => n
408
+ Empty => n
408
- Cons(_, t) => countUp(t, n + 1)
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: Link[Int]) -> Link[Int] =
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: Link[Int] := Nil
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, Nil)
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, Nil)))))))
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(Nil) == "empty"
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 Cons(e, _) := r.events
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: Link[Int] := Nil
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 := List()
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/List
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: List[Body],
40
+ orbs: Slice[Body],
41
- mines: List[Body],
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: List[Body](), mines: List[Body](), rng: rng, score: 0, best: 0,
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 `List`, which stores them
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/List
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: List[Particle], dt: Float) -> Int =
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: List[Particle]) -> Stats =
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 := List[Particle]()
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 := List[Particle]()
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/List
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: List[Action]())
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: List[Action](),
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: List[Action](),
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: List[Action](),
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: List[Action](),
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: List[Int], depth: Int) -> Actor =
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) -> List[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 := List[Actor]()
338
+ stars := Slice[Actor]()
339
339
  for i := range 40
340
340
  stars.push(Star.create(i))
341
- mines := List[Actor]()
341
+ mines := Slice[Actor]()
342
- orbs := List[Actor]()
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 := List[Actor]()
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 := List[Actor]()
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 := List[Actor]()
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: List[Input], events: List[UiEvent]) -> Stage =
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: List[Input]) -> Bool =
484
+ fun advance(w: World, ax: Float, ay: Float, dt: Float, inputs: Slice[Input]) -> Bool =
485
485
  w.clock = w.clock + dt
486
- events := List[UiEvent]()
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, List[Input]())
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 := List[Input]()
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 := List[Input]()
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/List
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: List[Mine],
249
+ mineList: Slice[Mine],
250
- orbList: List[Orb],
250
+ orbList: Slice[Orb],
251
251
  overlay: Group,
252
252
  scoreText: Text,
253
253
  bestText: Text,
254
- lifeIcons: List[Sprite],
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 := List[Sprite]()
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: List[Mine](), orbList: List[Orb](),
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 (recursive `Link` lists keyed by id), the score, the random seed and the screen
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 `Nodes`:
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: Link[Orb],
76
+ orbs: List[Orb],
76
- mines: Link[Mine],
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: Link[A], f: fn(A) -> B) -> Link[B] = match 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: Link[T]) -> Int = match 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: Link[T], x: T) -> Link[T] = match 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: Link[T], i: Int, x: T) -> Link[T] = match 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: Link[T], i: Int, dflt: T) -> T = match 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: Link[T], f: fn(T) -> SceneNode, rest: Nodes) -> Nodes = match 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) => More(head: f(h), tail: toNodes(t, f, rest))
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: Link[Orb], seed: Int)
225
+ record OrbsMade(orbs: List[Orb], seed: Int)
227
226
 
228
- record MinesMade(mines: Link[Mine], seed: Int)
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: Link[Orb], seed: Int, score: Int)
267
+ record Collected(orbs: List[Orb], seed: Int, score: Int)
269
268
 
270
- fun collectOrbs(orbs: Link[Orb], ship: ShipState, seed: Int, score: Int) -> Collected = match 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: Link[Mine], have: Int, want: Int, seed: Int, ship: ShipState) -> MinesMade =
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: Link[Mine], ship: ShipState) -> Bool = match 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) -> Nodes =
356
+ fun starNodes(i: Int, clock: Float) -> List[SceneNode] =
358
- if i >= 40 then End else More(head: starView(i, clock), tail: starNodes(i + 1, clock))
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: More(head: cell(sheet, 0.0 - Rules.Half, 0.0 - Rules.Half, Rules.Size, frameOf(k.clock, 4, 4.0), 2), tail: End))
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: More(
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: More(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: End)))
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: More(
376
+ children: Cons(
378
377
  head: Filter(effect: BlendFx(mode: Blend.Add), child: glow, target: 0 - 1),
379
- tail: More(head: cell(sheet, 0.0 - Rules.Half, 0.0 - Rules.Half, Rules.Size, frameOf(o.clock + o.phase, 4, 8.0), 1), tail: End)))
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: End)
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: More(
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: More(head: cell(s.sheet, 0.0 - Rules.Half, 0.0 - Rules.Half, Rules.Size, frameOf(s.clock, 4, 14.0), 0), tail: End)))
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) -> Nodes =
392
+ fun lifeNodes(i: Int, lives: Int, sheet: Int) -> List[SceneNode] =
394
393
  if i >= lives
395
- End
394
+ Nil
396
395
  else
397
- More(head: cell(sheet, Rules.ArenaW - 40.0 - Float(i) * 36.0, 12.0, 32.0, 0, 0), tail: lifeNodes(i + 1, lives, sheet))
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: More(
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: More(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))))
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: End)
409
+ return Group(transform: Identity, children: Nil)
411
410
  button := Group(
412
411
  transform: Identity,
413
- children: More(
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: More(head: Text(text: "Again", x: 372.0, y: 352.0, size: 24.0, color: Color.White), tail: End)))
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: More(
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: More(
419
+ tail: Cons(
421
420
  head: Text(text: "Game over", x: 300.0, y: 244.0, size: 48.0, color: Color.Red),
422
- tail: More(
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: More(head: button, tail: End)))))
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 := More(
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: More(
431
+ tail: Cons(
433
432
  head: starsView(s.clock),
434
- tail: More(
433
+ tail: Cons(
435
- head: Group(transform: Identity, children: toNodes(s.mines, |k| mineView(s.sheet, k), End)),
434
+ head: Group(transform: Identity, children: toNodes(s.mines, |k| mineView(s.sheet, k), Nil)),
436
- tail: More(
435
+ tail: Cons(
437
- head: Group(transform: Identity, children: toNodes(s.orbs, |o| orbView(s.sheet, o), End)),
436
+ head: Group(transform: Identity, children: toNodes(s.orbs, |o| orbView(s.sheet, o), Nil)),
438
- tail: More(head: shipView(s), tail: More(head: hudView(s), tail: More(head: overlayView(s), tail: End)))))))
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: Nodes) -> Int = match ks
554
+ fun countKids(ks: List[SceneNode]) -> Int = match ks
556
- End => 0
555
+ Nil => 0
557
- More(h, t) => countNodes(h) + countKids(t)
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/List
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 List slot, a Map value, an error-union payload) and unboxed on the way out. These
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: List[List[Map[Str, Int]]], r: Int, c: Int, key: Str) -> error{OutOfBounds, KeyNotFound}!Int =
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: List[T], num: Num[T]) -> T =
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 := List[Crate]()
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 := List[Int]()
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 := List[Float]()
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 := List[Map[Str, Int]]()
216
+ inner1 := Slice[Map[Str, Int]]()
217
217
  inner1.push(m1)
218
218
  inner1.push(m2)
219
- inner2 := List[Map[Str, Int]]()
219
+ inner2 := Slice[Map[Str, Int]]()
220
220
  inner2.push(m2)
221
- outer := List[List[Map[Str, Int]]]()
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 := List(1, 22, 333)
233
+ ints := Slice(1, 22, 333)
234
- strs := List[Str]()
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 := List[Int]()
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 := List[Circle]()
259
+ shapes := Slice[Circle]()
260
260
  shapes.push(Circle(r: 2.0))
261
261
  shapes.push(Circle(r: 3.0))
262
- names := List[Str]()
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(List(1, 2, 3), intNum) == 6
292
+ assert sumWith(Slice(1, 2, 3), intNum) == 6
293
- assert sumWith(List(1.5, 2.5), floatNum) == 4.0
293
+ assert sumWith(Slice(1.5, 2.5), floatNum) == 4.0
294
- assert sumWith(List("a", "b"), strNum) == "ab"
294
+ assert sumWith(Slice("a", "b"), strNum) == "ab"
295
295
 
296
296
  test "untyped_empty_list_field_access_after_push"
297
- out := List()
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 := List(List(1, 2), List(3))
304
+ r := Slice(Slice(1, 2), Slice(3))
305
305
  assert r.length() == 2
306
306
  assert r.at(0).length() == 2
307
- s := List(List("a"))
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/List
6
+ import std/Slice
7
7
  import std/Json
8
8
 
9
- record House(name: Str, rooms: Int, tags: List[Str], annex: List[House]) =
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!List[Str] = j.into()
20
+ fun tagsOf(j: Json) -> JsonError!Slice[Str] = j.into()
21
21
 
22
- fun annexOf(j: Json) -> JsonError!List[House] = j.into()
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 := List[Str]()
35
+ tags := Slice[Str]()
36
36
  tags.push("old")
37
- annex := List[House]()
37
+ annex := Slice[House]()
38
- annex.push(House(name: "shed", rooms: 1, tags: List[Str](), annex: List[House]()))
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/List
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: List[Int]) -> Int =
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: List[Int], limit: Int) -> Int =
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 := List(1, 2, 3, 4)
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 := List(10, 20, 30)
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 := List(1, 2, 3, 4, 5, 6)
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 := List(1, 2, 3, 4)
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 := List(1, 2, 3, 4, 5)
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 := List(1, 2)
67
+ xs := Slice(1, 2)
68
- ys := List(10, 20)
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/List
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: List[Int], i: Int) -> Int? =
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() -> List[Int?] =
59
+ fun sparse() -> Slice[Int?] =
60
- List(1, null, 3)
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 := List(3, 2, 1)
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/List
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 := List(Cell(x: 1.0), Cell(x: 2.0), Cell(x: 3.0))
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 := List(Cell(x: 1.0), Cell(x: 2.0))
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 := List(Cell(x: 5.0))
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 := List(Cell(x: 1.0), Cell(x: 2.0), Cell(x: 3.0))
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/List
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: List[Actor],
94
+ bodies: Slice[Actor],
95
- halos: List[Actor],
95
+ halos: Slice[Actor],
96
- names: List[Str],
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: List[Actor](), halos: List[Actor](), names: List[Str](),
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 := List("Ember", "Cinder", "Ashen", "Glacier")
140
+ planetNames := Slice("Ember", "Cinder", "Ashen", "Glacier")
141
- moons := List(0, 1, 2, 1)
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/List
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 List.
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() -> List[Soldier] =
40
+ fun army() -> Slice[Soldier] =
41
- List(
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 := List(1, 2, 3, 4, 5, 6)
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 := List(5, 6, 7, 8)
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 := List(5, 6, 7, 8)
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 := List(1, 2, 3, 4, 5, 6, 7)
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 := List(1, 2, 3)
78
+ xs := Slice(1, 2, 3)
79
- ys := List(10, 20, 30, 40)
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 := List(1, 2, 3)
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 := List(1, 2, 3, 4, 5)
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 := List(1, 2, 3, 4, 5, 6, 7, 8)
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 := List(1, 2, 3, 4)
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 := List(3, 8, 12)
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 := List(1, 2, 3, 4)
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 := List(1, 2, 3, 4)
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 := List(1, 2, 3, 4, 5, 6, 7, 8)
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 := List(1, 2, 3, 4, 5)
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/List
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 := List(1, 2, 3, 4)
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. `List`, `Grid` and friends get this for free.
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/List
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: List[Vec2], name: Str)
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 := List[Vec2]()
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 := List[Vec2]()
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: List[Vec2](), name: "w")
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 := List[Array[Vec2]]()
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 := List[Px]()
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/List
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: List[Step])
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: List[Step] := List[Step]()
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: List[Step] := List[Step]()
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: List[Int] = List[Int]()
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: Link[Int]) -> Link[Int] =
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) -> Link[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: Link[A], f: fn(A) -> B) -> Link[B] = match 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: Link[T], acc: A, f: fn(A, T) -> A) -> A = match 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: Link[T], acc: Int) -> Int = match 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 `List[Entity]`; the engine side knows only
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/List
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: List[Entity]) =
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: List[Entity] := List[Entity]()
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/List
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: List[Entity] := List[Entity]()
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 (`List[Jv]`) or bare tags. An enum
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/List
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 | List[Jv]
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
- List => "list"
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 | List[Jb]
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
- List => "l"
126
+ Slice => "l"
127
127
 
128
- fun pos(n: Int) -> !List[Jv] =
128
+ fun pos(n: Int) -> !Slice[Jv] =
129
129
  if n < 0
130
130
  return error.Neg
131
- xs := List[Jv]()
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 := List[Shape]()
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 := List[Jv]()
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 List[Jv]()
199
+ ys := pos(3) catch Slice[Jv]()
200
200
  assert describeJv(ys) == "list"
201
201
 
202
202
  test "union_with_for"
203
- xs := List[Jv]()
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: List[Book] := List[Book]()
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/List
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: List[Action](), tex: tex, dx: dx, dy: dy)
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: List[Action](), n: n, text: "n {n}")
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) -> List[Action] =
89
+ fun spinning(rate: Float) -> Slice[Action] =
90
- acts := List[Action]()
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 := List[Actor]()
107
+ root := Slice[Actor]()
108
108
  per := Counts.Sprites / Counts.Groups
109
109
  for gi := range Counts.Groups
110
- kids := List[Actor]()
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, List[Input]()).intoStage()
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/List
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 recursive `Link` lists (each spinner owns the `Link` of its 100 dancers);
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: Link[A], f: fn(A) -> B) -> Link[B] = match 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) -> Link[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: Link[T], ys: Link[T]) -> Link[T] = match 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: Link[T], i: Int, dflt: T) -> T = match 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: Link[T], f: fn(T) -> SceneNode, rest: Nodes) -> Nodes = match 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) => More(head: f(h), tail: toNodes(t, f, rest))
50
+ Cons(h, t) => Cons(head: f(h), tail: toNodes(t, f, rest))
51
51
 
52
- fun countNodes(ns: Nodes) -> Int = match ns
52
+ fun countNodes(ns: List[SceneNode]) -> Int = match ns
53
- End => 0
53
+ Nil => 0
54
- More(_, t) => 1 + countNodes(t)
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: Link[Dancer])
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: Link[Spinner], drifters: Link[Drifter], counters: Link[Counter])
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: More(head: Sprite(
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: End))
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), End))
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), End)
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 Link (each records its
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 recursive `Link` lists (each spinner owns the `Link` of its 100 dancers);
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: Link[A], f: fn(A) -> B) -> Link[B] = match 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) -> Link[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: Link[T], ys: Link[T]) -> Link[T] = match 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: Link[T], i: Int, dflt: T) -> T = match 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: Link[T], f: fn(T) -> SceneNode, rest: Nodes) -> Nodes = match 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) => More(head: f(h), tail: toNodes(t, f, rest))
52
+ Cons(h, t) => Cons(head: f(h), tail: toNodes(t, f, rest))
53
53
 
54
- fun countNodes(ns: Nodes) -> Int = match ns
54
+ fun countNodes(ns: List[SceneNode]) -> Int = match ns
55
- End => 0
55
+ Nil => 0
56
- More(_, t) => 1 + countNodes(t)
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: Link[Spinner], dancers: Link[Dancer], drifters: Link[Drifter], counters: Link[Counter])
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) -> Link[Dancer] =
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: More(head: Sprite(
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: End))
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: Link[Dancer], gi: Int) -> Nodes = match ds
132
+ fun groupNodes(sheet: Int, ds: List[Dancer], gi: Int) -> List[SceneNode] = match ds
133
- Nil => End
133
+ Nil => Nil
134
- Cons(h, t) => if h.gi == gi then More(head: dancerView(sheet, h), tail: groupNodes(sheet, t, gi)) else End
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: Link[Dancer], gi: Int) -> Link[Dancer] = match 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: Link[Dancer]) -> SceneNode =
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: Link[Spinner], ds: Link[Dancer], rest: Nodes) -> Nodes = match 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) => More(head: spinnerView(sheet, p, ds), tail: spinnerNodes(sheet, t, dropGroup(ds, p.id - 100), rest))
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), End)
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), End)) == 5000
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/List
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 List offers List's own methods AND the Seq methods reached
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 memberCompletionOnListIncludesSeqMethods() {
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/List\nimport std/Seq\n\nfun main() =\n xs := List(1, 2, 3)\n xs.zzzzzz\n";
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/List\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";
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
- # `List[T]` is built on this.
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`). `List`
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/List
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/List
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: List[T], key: fn(T) -> Float) =
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: List[Int](), key: |x| Float(x))
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: List[Float](), key: |x| 0.0 - x)
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/List
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/List
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 List[T] =`),
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/List/Map never
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 `List[Json]`/
23
+ # distinguish integers from floats the way Plum does), and `Slice[Json]`/
24
- # `Map[Str, Json]` nest `Json` itself — the same self-referential shape `List[T]`
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
- | List[Json]
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
- List => "[" + self.join(",") + "]"
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 := List[Json]()
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}!List[Json] =
125
+ fun asList(self) -> error{WrongJsonType}!Slice[Json] =
126
126
  match self
127
- List => self
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 `List`, which uses `join` for
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 := List[Json]()
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 List[T] =
515
+ primitive Slice[T] =
516
516
  # A JSON array of the elements' own `toJson()`.
517
517
  fun toJson(self) -> Json =
518
- out := List[Json]()
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!List[T] =
524
+ fun fromJson(j: Json) -> JsonError!Slice[T] =
525
525
  items := try j.asList()
526
- out := List[T]()
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!List[List[Int]] = Json.decode(s)
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 List of itself.
555
+ # worked example of the pattern), including a Slice of itself.
556
- record JsonTestHouse(name: Str, rooms: Int, kids: List[JsonTestHouse]) =
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!List[JsonTestHouse] = j.into()
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: List.toJson encodes an array"
687
+ test "extension: Slice.toJson encodes an array"
688
- assert List(1, 2, 3).toJson().toStr() == "[1.0,2.0,3.0]"
688
+ assert Slice(1, 2, 3).toJson().toStr() == "[1.0,2.0,3.0]"
689
689
 
690
- test "extension: nested List.toJson recurses through T.toJson"
690
+ test "extension: nested Slice.toJson recurses through T.toJson"
691
- assert List(List(1), List(2, 3)).toJson().toStr() == "[[1.0],[2.0,3.0]]"
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 List[List[Int]]()}.length() == 0
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: List[JsonTestHouse]())
729
+ kid := JsonTestHouse(name: "k\"1", rooms: 2, kids: Slice[JsonTestHouse]())
730
- kids := List[JsonTestHouse]()
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: List[JsonTestHouse]())}.name == "E"
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: List[JsonTestHouse]())}.name == "E"
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: List[JsonTestHouse]())}.name == "E"
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/List
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/List
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}!List[Str] =
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 := List[Str]()
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/List
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 List. See `plum-core/src/forms.rs` for the precise rules.
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 List, an Array, a Deque, ...
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 List.
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: List[B]) *(T, B) =
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) *List[T] =
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 -> List[T] =
88
+ fun* filter(self) *T -> Slice[T] =
89
- out := List()
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 -> List[U] =
96
+ fun* map(self) *T -> Slice[U] =
97
- out := List()
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 := List()
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 -> List[T] =
152
+ fun* firstN(self, n: Int) *T -> Slice[T] =
153
- out := List()
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 List (no body).
162
+ # `xs.take(3).toList()`: the elements as a Slice (no body).
163
- fun* toList(self) *T -> List[T] =
163
+ fun* toList(self) *T -> Slice[T] =
164
- out := List()
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/List
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) -> List[T] =
35
+ fun toList(self) -> Slice[T] =
36
- result := List[T]()
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 `List` over the same
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 List[T](data: Array[T], start: Int, size: Int, cap: Int) =
19
+ record Slice[T](data: Array[T], start: Int, size: Int, cap: Int) =
20
20
 
21
- # `List[T](1, 2, 3)` (or, for an empty list, `List[T]()` — explicit
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 `List.init(...)`.
25
+ # desugars both call shapes to `Slice.init(...)`.
26
- fun init(values: ...T) -> List[T] =
26
+ fun init(values: ...T) -> Slice[T] =
27
- l := List(data: Array[T](), start: 0, size: 0, cap: 0)
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) -> List[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: List[T]) -> Bool =
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) -> List[T] =
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 List(data: self.data, start: self.start + s, size: e - s, cap: e - s)
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) -> List[T] =
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) -> List[T] =
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) -> List[T] =
216
+ fun sublist(self, start: Int, end: Int) -> Slice[T] =
217
- result := List[T]()
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) -> List[T] =
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) -> List[T] =
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) -> List[List[T]] =
249
+ fun chunk(self, size: Int) -> Slice[Slice[T]] =
250
- result := List[List[T]]()
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, List[T]]`, but
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 := List(1, 2, 3, 4, 5)
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 := List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
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 "List[Int]() with no args runs init() and produces a working empty list"
308
+ test "Slice[Int]() with no args runs init() and produces a working empty list"
309
- l := List[Int]()
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 "List(values) with positional args runs init(values) directly, infers T from args"
315
+ test "Slice(values) with positional args runs init(values) directly, infers T from args"
316
- l := List(10, 20, 30)
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 = List(1, 2, 3)
322
+ a = Slice(1, 2, 3)
323
- b = List(1, 2, 3)
323
+ b = Slice(1, 2, 3)
324
- c = List(1, 2)
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 = List(3, 1, 4, 1, 5)
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 = List(1, 2, 3)
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 := List(1, 2, 3)
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 := List(1, 2, 3, 4, 5)
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 := List[Int]()
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 := List[Int]()
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 := List[Str]()
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 "List[Float] push/get round-trips"
397
+ test "Slice[Float] push/get round-trips"
398
- l := List[Float]()
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 := List(1, 2, 3, 4, 5)
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 := List(1, 2, 3, 4, 5)
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 := List(10, 20, 30, 40, 50, 60)
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 := List(9, 1, 5, 3, 7, 8)
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(List(1, 5, 3, 7))
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 := List(1, 2, 3, 4, 5)
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 := List[Int]()
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/List
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 `List` of codepoints/bytes would need a heap-allocated node PER
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 `List` is currently
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) -> List[Str] =
250
+ fun split(self, separator: Str, limit: Int) -> Slice[Str] =
251
- result := List[Str]()
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/List
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 (`List[T]`/`!T`) whose OWN generic param is
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`/`List.equals`: Plum's
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 `List[T]` comparison — see the module-level CAVEAT above for
52
+ # Elementwise `Slice[T]` comparison — see the module-level CAVEAT above for
53
- # what `List.equals`'s own `==` means when `T` is a class/enum.
53
+ # what `Slice.equals`'s own `==` means when `T` is a class/enum.
54
- fun expectArray(a: List[T], b: List[T]) -> Bool =
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 List.equals method"
82
+ test "list equality via the Slice.equals method"
83
- a := List(1, 2, 3)
83
+ a := Slice(1, 2, 3)
84
- b := List(1, 2, 3)
84
+ b := Slice(1, 2, 3)
85
- c := List(1, 2)
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 List[T]s elementwise"
95
+ test "expectArray compares two Slice[T]s elementwise"
96
- a := List(1, 2, 3)
96
+ a := Slice(1, 2, 3)
97
- b := List(1, 2, 3)
97
+ b := Slice(1, 2, 3)
98
- c := List(1, 2)
98
+ c := Slice(1, 2)
99
99
  assert Testing.expectArray(a, b)
100
100
  assert Testing.expectFalse(Testing.expectArray(a, c))
101
- d := List("x", "y")
101
+ d := Slice("x", "y")
102
- e := List("x", "y")
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/List
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: List[Action])
21
+ record Sequence(steps: Slice[Action])
22
22
 
23
- record Parallel(steps: List[Action])
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: List[Action], spare: Float)
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: List[Action]) -> Action =
51
+ fun sequence(steps: Slice[Action]) -> Action =
52
52
  Sequence(steps: steps)
53
53
 
54
- fun parallel(steps: List[Action]) -> Action =
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 := List[Action]()
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: List[Action], dt: Float) -> SteppedAll =
124
+ fun stepAll(p: Pose, acts: Slice[Action], dt: Float) -> SteppedAll =
125
125
  pose := p
126
- kept := List[Action]()
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(), List(Actions.moveBy(10.0, 0.0, 1.0)), 0.5)
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(), List(Actions.moveBy(10.0, 0.0, 1.0), Actions.rotateBy(1.0, 0.5)), 1.0)
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(), List[Action](), 0.016)
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(List(Actions.moveBy(10.0, 0.0, 0.5), Actions.moveBy(0.0, 10.0, 0.5)))
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(List[Action]()), 0.3)
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(List(Actions.delay(0.2), Actions.delay(0.5))), 1.0)
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(List(Actions.moveBy(1.0, 0.0, 0.1), Actions.delay(0.1))))
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
- # `Nodes` list, so every operation recurses over SceneNode and Nodes, passing
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: Nodes)
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: Nodes, m: Affine, acc: Rect?) -> Rect? = match kids
94
+ fun picBoundsKids(kids: List[SceneNode], m: Affine, acc: Rect?) -> Rect? = match kids
95
- End => acc
95
+ Nil => acc
96
- More(head, tail) =>
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) -> List[Int]? =
115
+ fun picLeaf(hit: Bool) -> Slice[Int]? =
116
116
  if hit
117
- return List[Int]()
117
+ return Slice[Int]()
118
118
  null
119
119
 
120
120
  # `i` followed by the path `sub`.
121
- fun picPrepend(i: Int, sub: List[Int]) -> List[Int] =
121
+ fun picPrepend(i: Int, sub: Slice[Int]) -> Slice[Int] =
122
- path := List(i)
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) -> List[Int]? = match node
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: Nodes, i: Int, px: Float, py: Float) -> List[Int]? = match kids
149
+ fun picHitKids(kids: List[SceneNode], i: Int, px: Float, py: Float) -> Slice[Int]? = match kids
150
- End => null
150
+ Nil => null
151
- More(head, tail) =>
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: Nodes, m: Affine, alpha: Float, target: Int) = match kids
184
+ fun picDrawKids(kids: List[SceneNode], m: Affine, alpha: Float, target: Int) = match kids
185
- End => 0
185
+ Nil => 0
186
- More(head, tail) =>
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) -> List[Int] =
227
+ fun hitPath(node: SceneNode, x: Float, y: Float) -> Slice[Int] =
228
228
  if p := picHit(node, x, y)
229
229
  return p
230
- List[Int]()
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: More(head: picRect(0.0, 0.0, 10.0, 10.0, Color.Red), tail: End))
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: More(head: picRect(0.0, 0.0, 10.0, 4.0, Color.Red), tail: End))
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: More(head: picRect(0.0, 0.0, 10.0, 10.0, Color.Blue), tail: End))
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: More(head: picRect(0.0, 0.0, 10.0, 10.0, Color.Red), tail: More(head: inner, tail: End)))
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: More(head: picRect(0.0, 0.0, 10.0, 10.0, Color.Red), tail: More(head: picRect(0.0, 0.0, 10.0, 10.0, Color.Blue), tail: End)))
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: More(head: picRect(0.0, 0.0, 10.0, 10.0, Color.Red), tail: End))
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/List
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: List[UiEvent]) =
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: List[Action]) =
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) -> List[Actor]? = null
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: List[UiEvent]())
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: List[Actor]) =
80
+ children: Slice[Actor]) =
81
81
 
82
82
 
83
- override fun kids(self) -> List[Actor]? = self.children
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 := List[UiEvent]()
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) -> List[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: List[Actor]) -> Group =
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: List[Action](), children: children)
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: List[Action](),
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: List[Action](), text: text, size: size)
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: List[Action](),
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) -> List[UiKind] =
210
+ fun psKinds(h: Handled) -> Slice[UiKind] =
211
- out := List[UiKind]()
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/List
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: List[Actor],
53
+ children: Slice[Actor],
54
54
  parent: Actor?,
55
- scratch: List[Actor],
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 = List[Actor]()
72
+ self.children = Slice[Actor]()
73
73
  self.parent = null
74
- self.scratch = List[Actor]()
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: List[Drawn]) =
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: List[Actor], d: Array[Actor], base: Int, lo: Int, a: Int, b: Int, before: fn(Actor, Actor) -> Bool) =
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) -> List[Drawn] =
453
+ fun flatten(root: Actor) -> Slice[Drawn] =
454
- out := List[Drawn]()
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 := List(0)
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/List
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: List[Int], id: Int)
29
+ record Capture(path: Slice[Int], id: Int)
30
30
 
31
- own record StageHandled(stage: Stage, events: List[UiEvent]) =
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 := List[UiEvent]()
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: List[Int], p: Pointer, c: Capture?) -> StageHandled =
77
+ fun deliver(own self, path: Slice[Int], p: Pointer, c: Capture?) -> StageHandled =
78
- events := List[UiEvent]()
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: List[Input]) -> StageHandled =
84
+ fun frame(own self, dt: Float, inputs: Slice[Input]) -> StageHandled =
85
85
  cur := self
86
- out := List[UiEvent]()
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: List[Int], b: List[Int]) -> Bool =
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) -> List[Input] =
113
+ fun pollPointer(wasDown: Bool) -> Slice[Input] =
114
114
  m := mousePos()
115
115
  down := isMouseDown(0)
116
- out := List[Input]()
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 := List[Actor]()
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: List[UiEvent]) -> Int =
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 := List[Actor]()
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, List[Actor]())
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 := List[Action]()
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 := List[Actor]()
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 := List[Input]()
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/List
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: List[Int]) -> List[Int] =
11
+ fun treePrepend(i: Int, rest: Slice[Int]) -> Slice[Int] =
12
- out := List[Int]()
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) -> List[Int]? =
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 List[Int]()
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: List[Int], depth: Int) -> Int =
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) -> List[Int]? =
69
+ fun findPath(a: Actor, id: Int) -> Slice[Int]? =
70
70
  if a.id == id
71
- return List[Int]()
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: List[Int], depth: Int, f: fn(Actor) -> Actor) -> Actor =
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: List[Int], depth: Int, e: Pointer) -> Handled =
99
+ fun route(own a: Actor, path: Slice[Int], depth: Int, e: Pointer) -> Handled =
100
- events := List[UiEvent]()
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: List[Int], depth: Int, e: Pointer, events: List[UiEvent]) -> Actor =
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 := List[Actor]()
123
+ inner := Slice[Actor]()
124
124
  inner.push(PureScene.sprite(4, 0, 0.0, 0.0, 5.0, 5.0))
125
- kids := List[Actor]()
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: List[Int]?) -> Bool =
132
+ fun ttNoPath(r: Slice[Int]?) -> Bool =
133
133
  r == null
134
134
 
135
- fun ttPathEq(r: List[Int]?, want: List[Int]) -> Bool =
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) -> List[Int] =
145
+ fun ttInts(a: Int, b: Int) -> Slice[Int] =
146
- out := List[Int]()
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 = List(Actions.moveBy(10.0, 0.0, 1.0))
156
+ s.actions = Slice(Actions.moveBy(10.0, 0.0, 1.0))
157
- kids := List[Actor]()
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 := List[Actor]()
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() -> List[Int] =
203
+ fun ttZero() -> Slice[Int] =
204
- out := List[Int]()
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 := List[Int]()
218
+ five := Slice[Int]()
219
219
  five.push(5)
220
220
  assert SceneTree.idAt(t, five, 0) == -1
221
- assert SceneTree.idAt(t, List[Int](), 0) == 1
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, List[Int]())
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, List[Int]())
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, List[Int]())
252
+ assert ttPathEq(r, Slice[Int]())
253
253
 
254
254
  test "topmost_child_wins"
255
- kids := List[Actor]()
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 := List[Int]()
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 := List[Actor]()
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 := List[Int]()
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 that extend it.
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
- !matches!(t, PlumType::TNamed(n) if subtyped.contains_key(n))
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 List needs) — all in one hand-encoded module,
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: List[T], cb: fn(T) -> U) -> List[U] =
1115
+ fun mapList(xs: Slice[T], cb: fn(T) -> U) -> Slice[U] =
1116
- result := List[U]()
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: List[T], acc: U, cb: fn(U, T) -> U) -> U =
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: List[T], keep: fn(T) -> Bool) -> List[T] =
1127
+ fun retainList(xs: Slice[T], keep: fn(T) -> Bool) -> Slice[T] =
1128
- result := List[T]()
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/List
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(List(1, 2), |n| n + 1)
1148
+ a := mapList(Slice(1, 2), |n| n + 1)
1149
- b := mapList(List(\"x\", \"y\"), |s| s + \"!\")
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(List(1, 2), |n| n + 1)
1177
+ a := mapList(Slice(1, 2), |n| n + 1)
1178
- b := mapList(List(3), |n| n * 2)
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
- List(1, 2).length()
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(List(1, 2), |n| n + 1).length()
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
- List(1, 2).length()
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/List
1388
+ import std/Slice
1389
1389
 
1390
1390
  fun main() -> Int =
1391
- l := List[Bool]()
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: List[Int], cb: fn(Int) -> Int) -> List[Int] =
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(List(1, 2, 3), |n| n * 10), 0, |a, b| a + b)
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: List[T], num: Num[T]) -> T =
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(List(1, 2, 3), intNum) == 6 && sumWith(List(1.5, 2.5), floatNum) == 4.0
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: List[T], key: fn(T) -> Int) =
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: List(5, 6), key: |n| n * 8)
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 = List(10, 20, 30)
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 := List(1, 2, 3, 4)
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 := List(1, 2, 3, 4)
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 := List(1, 2, 3, 4, 5, 6)
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 := List(1, 2, 3, 4, 5, 6)
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 := List(1, 2, 3, 4, 5, 6)
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: List[Int]) *(Int, Int) =
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 := List(4, 5, 6)
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: List[Int]) =
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: List(1, 2, 3))
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 := List(1, 2, 3, 4, 5, 6, 7, 8)
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 := List(1, 2, 3)
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 := List(1, 2, 3)
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/List
2410
+ import std/Slice
2411
2411
 
2412
- fun first(xs: List[Int]) -> Int =
2412
+ fun first(xs: Slice[Int]) -> Int =
2413
2413
  xs.first()!
2414
2414
 
2415
2415
  fun main() -> Int =
2416
- first(List[Int]())
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/List
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: List[error{Neg}!Int]) -> Int =
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 := List(chk(4))
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 `List.sample`/`shuffle` — see
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/List\n\nprimitive Table =\n",
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() -> List[Int] =\n return List(",
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/List
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/List\n\n";
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/List\n\n";
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/List\n\n";
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/List\n\n";
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: List[Int]) =
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 = List[Int]()
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/List\n\n";
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/List\n\n";
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: List[Entity] := List[Entity]()
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: List[Shape] := List[Shape]()
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: List[Actor], v: Int) =
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: List[Actor](), v: 100)
134
+ l1 := Leaf(children: Slice[Actor](), v: 100)
135
- l2 := Leaf(children: List[Actor](), v: 200)
135
+ l2 := Leaf(children: Slice[Actor](), v: 200)
136
- mk: List[Actor] := List[Actor]()
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: List[Actor] := List[Actor]()
140
+ rk: Slice[Actor] := Slice[Actor]()
141
141
  rk.push(m)
142
- rk.push(Leaf(children: List[Actor](), v: 1000))
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: List[Actor]) =
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: List[Actor](), n: 6, w: 3)
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/List
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: List[Int]) -> List[T] =
107
+ fun buildAll(ns: Slice[Int]) -> Slice[T] =
108
- out := List[T]()
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: List[Pt] := buildAll(List(1, 2, 3))
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/List
145
+ import std/Slice
146
146
  import std/Int
147
147
 
148
- primitive List[T] =
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/List
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/List\n",
93
+ "import std/Slice\n",
94
- "import std/List\n\nerror ParseError = Bad | Worse\n",
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: List[Shape]) -> Int =
103
+ fun take(xs: Slice[Shape]) -> Int =
104
104
  1
105
105
 
106
- fun go(cs: List[Circle]) -> Int =
106
+ fun go(cs: Slice[Circle]) -> Int =
107
107
  take(cs)
108
108
  ",
109
109
  );