plum

#treesitter#compiler#wasm

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

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


2150594 — Peter John 2026-11-06T14:03:56+05:30
FBIP stage 1: pilot and verdict, and the simpler language plan
compiler/lsp/src/completion.rs CHANGED
@@ -37,6 +37,7 @@ const KEYWORDS: &[&str] = &[
37
37
  "extern",
38
38
  "export",
39
39
  "abstract",
40
+ "fip",
40
41
  "if",
41
42
  "then",
42
43
  "else",
@@ -656,4 +657,9 @@ mod tests {
656
657
  let t = plum_checker::withFnScope(&f, &[], || paramPlumType(&ast::ParamType::Type(ty("T", vec![]))));
657
658
  assert_eq!(t, PlumType::TParam("T".into()));
658
659
  }
660
+
661
+ #[test]
662
+ fn fipIsOfferedAsKeyword() {
663
+ assert!(keywordItems().iter().any(|i| i.label == "fip"));
664
+ }
659
665
  }
docs/backlog.md CHANGED
@@ -4,7 +4,7 @@ Open work, in the order the user queued it, plus smaller items found during buil
4
4
 
5
5
  ## Planned (in order)
6
6
 
7
- **Current order (user 2026-11-05):** explicit fn generics (done faa4d7e) → cleanup phase 3b (pass list, positioned errors; running) → Io primitive + extern methods → move Rust crates under `compiler/` → tooling + scripts moved under compiler/ (done) → cleanup phase 4 (done: node ids, typed codegen, `inferExpr` errors at spans; `docs/plans/2026-11-05-compiler-cleanup-phase4.results.md`) → phases 5 and 6 (done: file splits, type kinds, tests in Plum; `docs/plans/2026-11-05-compiler-cleanup-phase5-6.results.md`) → game engine refactor (done) → FBIP (user approved 2026-11-06; running, stage 1 first) → top-level functions with receivers and module namespaces (`docs/plans/2026-11-06-receiver-functions.review.md`, needs the user's decisions; replaces the "no top-level functions" note) → benchmark (incl. iterator unboxing / niche Option, run C game A/B) → FBIP (stop for user approval) → deterministic sim → kids' games. The items below keep their detail; this line wins on order.
7
+ **Current order (user 2026-11-05):** explicit fn generics (done faa4d7e) → cleanup phase 3b (pass list, positioned errors; running) → Io primitive + extern methods → move Rust crates under `compiler/` → tooling + scripts moved under compiler/ (done) → cleanup phase 4 (done: node ids, typed codegen, `inferExpr` errors at spans; `docs/plans/2026-11-05-compiler-cleanup-phase4.results.md`) → phases 5 and 6 (done: file splits, type kinds, tests in Plum; `docs/plans/2026-11-05-compiler-cleanup-phase5-6.results.md`) → game engine refactor (done) → FBIP stage 1 (done: switch, proof, `Array.with`, `fip fun`, pilot; `docs/plans/2026-11-06-fbip-stage1.results.md`) → a simpler language, then the rest of FBIP (`docs/plans/2026-11-06-simpler-language.review.md`, needs the user's decisions) → benchmark (incl. iterator unboxing / niche Option, run C game A/B) → deterministic sim → kids' games. The items below keep their detail; this line wins on order.
8
8
 
9
9
  1. **Compiler cleanup and architecture** — `docs/plans/2026-11-04-compiler-cleanup.review.md` (written in another session; meant to run after language-restrictions merged). Approved 2026-11-04; running on branch `superlite/compiler-cleanup`, phase 1 first.
10
10
  - **Explicit function generics** (user 2026-11-04: right after cleanup phase 2) — functions declare their generics (`fun map[B](...)`) instead of every single capital letter being a generic; allows longer generic names and one-letter concrete types/tags. Grammar change + about 140 signatures in std/examples. Phase 2's "generics by scope" makes this mostly a grammar + signature rewrite.
docs/plans/2026-11-06-fbip-stage1.progress.md ADDED
@@ -0,0 +1,28 @@
1
+ - start eeaa05c
2
+ Ruling: user approved FBIP, keep mut for shells and imperative scene; took my recommendations on static proof and no ownership types - cost: none
3
+ Ruling: two stages with a gate: compiler parts and a pilot first, 363 call sites only after the pilot shows in-place updates happen - cost: stage 2 planned later
4
+ Ruling: lint --max-task-min 40, predictor history skewed - cost: a task may run long
5
+ Ruling: skip Wave 0 - cost: none
6
+ - unblocked: dispatched T1 at 10:54:34; waiting on fixes: none
7
+ - unblocked: dispatched T2 at 11:06:24; waiting on fixes: none
8
+ - checkpoint T1 87c3bcc7d7 baseline and probes
9
+ - checkpoint T2 c64fbda376 no-fbip switch
10
+ - unblocked: dispatched T3 at 11:12:16; waiting on fixes: none
11
+ - checkpoint T3 8806e4b41e proof: methods and loops
12
+ - unblocked: dispatched T4 prime at 11:31:41; waiting on fixes: T3 review
13
+ - checkpoint T3 f3adc511da proof: methods and loops
14
+ Ruling: T3 review, about 150 hostile programs, found 2 miscompiles (a closure or callable field named like a function), both fixed - cost: none
15
+ Ruling: two more bugs it found go to a fixer rather than the backlog: a miscompile is not left open - cost: one fix round
16
+ - unblocked: dispatched T4 at 11:53:57; waiting on fixes: F-SIMP-1 F-DEF-1
17
+ Ruling: T4 sent back - a separate local-only proof and a public withUnique don't meet the ruling, and would make the pilot fail by construction; rework on the real proof, with the flag on the call and the choice in codegen - cost: one rework round
18
+ Ruling: F-DEF-1 was a phase 4 regression (any boxed default uninlined); fixed, defaultSingleton removed. A default reading a parameter or self is now rejected; it read the caller's local before; no repo program affected - cost: none, but note a language tightening
19
+ Ruling: F-PUR-1 sent to the same fixer: a miscompile is not left open - cost: one fix round
20
+ Ruling: second attack round before T5, because the proof's rules changed again in T4 - cost: one review
21
+ - unblocked: dispatched T5 at 13:15:17; waiting on fixes: none
22
+ - checkpoint T4 9cf601c77c Array.with
23
+ - unblocked: dispatched T5 at 13:17:18; waiting on fixes: none
24
+ - checkpoint T5 0e5261f915 fip fun
25
+ - unblocked: dispatched T6 at 13:40:26; waiting on fixes: none
26
+ - checkpoint T6 d04a22bfa3 pilot and verdict
27
+ Ruling: pilot verdict is stop stage 2 until the proof is per type; the user approved FBIP but stage 2 now depends on a language plan with user decisions, so stop here and ask - cost: FBIP stage 2 waits
28
+ Ruling: no separate final-reviewer for stage 1 - it ran twice on packet-only evidence with no finding; the proof had two attack rounds, about 1650 programs and a fuzzer instead - cost: a skipped process step, on purpose
docs/plans/2026-11-06-fbip-stage1.results.md ADDED
@@ -0,0 +1,130 @@
1
+ # FBIP stage 1: results and the verdict on stage 2
2
+
3
+ Taken 2026-11-06 on master, release build (`cargo build --release -p plum-cli`), `target/release/plum run --lib-path .`. Load average 1.6 to 3.8 during the runs (3.8 at the start, 3.2 at the end). The program clock has 1 ms resolution, so anything under 3 ms is a floor, not a measurement. Differences of 10 to 20 percent are noise.
4
+
5
+ ## What stage 1 built
6
+
7
+ The pieces below are in the tree and are not re-measured here (see the T2 to T5 results):
8
+
9
+ * `--no-fbip` on `plum run`, `build`, `test` (`PassOptions.fbip`): no spread or array write is marked unique.
10
+ * A stronger uniqueness proof (`compiler/core/src/uniqueness.rs`): method `self` and receivers, overrides, `x = x.m(..)` hand-over in loops and fields, functions returning their own unique argument, read-only methods.
11
+ * `Array.with(self, i, v) -> Array[T]`: copies unless the call is marked unique, then `Array.withUnique` writes in place.
12
+ * `fip fun`: a compile error, at the spot, when a spread or `Array.with` in it is not proved in place.
13
+ * Aliasing tests in `tests/fbip/` and a differential fuzzer (`tests/fbip/fuzz`).
14
+
15
+ T6 adds the pilot: `tests/fbip/vec.plum`, a persistent `struct Vec[T](data: Array[T], size: Int)` with `push`, `set`, `at`, `length`, growing by doubling, in plain Plum with a spread and `Array.with` and nothing else. Twelve shapes (`tests/fbip/vec_<shape>.plum`, each importing `vec.plum`, each with a small checked test and a `main` that times the big size), one combined program of all shapes (`vec_combined.plum`, `vec_combined_shared.plum`), the same on `Slice` (`slice_shapes.plum`), a unique-name copy (`uvec.plum`, `vec_unique_names.plum`) and a `fip fun` version (`vec_fip.plum`).
16
+
17
+ ## The central finding: names
18
+
19
+ The proof keys methods and fields by name across the whole program, and the standard library is part of the program. With the realistic names, `push`, `set`, `data` and `size`, nothing the pilot does is proved in place, in any shape, even the simplest (`v = v.push(i)` on a local). Reasons `--explain-fusion` gave for `Vec.push`, all from std code the program only imports:
20
+
21
+ * `push`: a call of `push` from `Str.split` on a `Slice` local (`result`), and calls from std's own `test` blocks on `Array`. The proof resolves `v.push(..)` to every method named `push`, so `Slice.push` and `Array.push` call sites count as callers of `Vec.push` with a value that is not unique.
22
+ * `set`: `Str.byteToStr` calls `out.set(..)` on an `Array`.
23
+ * `data`, `size`: `Slice` has fields of those names (`mut data`, `mut size`), written and passed on. Renaming only `push` and `set` is not enough: `Vec` was still copying on every push. All four names have to differ.
24
+
25
+ `at` and `length` do not matter (they never take part in an update).
26
+
27
+ Experiment, shape 1, 100,000 pushes (ms): realistic names 1281; `push` renamed only 1290; `set` renamed only 1278; `push`+`set` renamed 1276; `data`+`size`+`at`+`length` renamed 1277; `push`+`set`+`data`+`size` renamed 1; all unique 1.
28
+
29
+ So: only unique names work, and stage 2 as planned would fail in practice. A real `Slice`, `Map`, `Set` or `Heap` has to be named `push`, `set`, `data`. Also: the message for the failing case blames the caller (`v` is bound to a value that may be shared), not the name collision. `fip fun push` with the real names fails to compile, correctly, and the error is misleading for the same reason (tests/fbip: the failing text is `not in place: self may be shared by a caller: v is bound to a value that may be shared (call of push from fill)`; `v` is fine).
30
+
31
+ ## The table of shapes
32
+
33
+ Times in ms. "real" = realistic names; "unique" = unique names (`UVec`, `udata`, `upush`, ...). 100,000 pushes per run unless stated; a copying run is quadratic, so 100,000 is what finishes (about 1.3 s). "1M" = 1,000,000 pushes, run only where the unique-name version is proved. `Slice` is the same shape in `slice_shapes.plum` (it mutates in place today; its own file because `Slice.push` shares a name with `Vec.push`). Median of 3 for the 1M columns.
34
+
35
+ | # | shape | real: proved? | real ms | unique: proved? | unique ms | unique 1M | Slice 100k | Slice 1M |
36
+ |---|---|---|---|---|---|---|---|---|
37
+ | 1 | local in a loop `v = v.push(i)` | no | 1281 | yes | 1 | 10 | 1 | 7 |
38
+ | 2 | `mut` field, `self.items = self.items.push(x)` | no | 1267 | yes | 1 | 12 | 0-1 | 10 |
39
+ | 3 | field of a nested struct | no | 1251 | yes | 1 | 11 | 1 | 8 |
40
+ | 4 | helper `v = grow(v, i)` | no | 1272 | yes | 1 | 10 | 1 | 7-9 |
41
+ | 5 | generic: `Vec[Int]` and `Vec[Vec2]` | no | 4027 | yes | 2 | 21 | 2 | 15 |
42
+ | 6 | length read between updates | no | 1270 | yes | 1 | 10 | 1 | 7 |
43
+ | 7 | two Vecs in one struct | no | 2586 | yes | 2 | 21 | 1-2 | 15 |
44
+ | 8a | Vec of Vecs, rows built then pushed | no | 47 | no (reason A) | 46 | - | 1 | 10 |
45
+ | 8b | `rows.set(j, rows.at(j).push(i))` | no | (copies the row, inherent) | no | - | - | - | - |
46
+ | 9 | returned from a function, pushed by the caller | no | 1260 | no (reason B) | 1270 | - | 1 | 6 |
47
+ | 9b | returned from a function, only read by the caller | no | 2774 | yes | 2 | - | - | - |
48
+ | 10 | immutable state `S(..s, items: s.items.push(x))` | no | 1264 | no (reason C) | 1274 | - | 1 | 9 |
49
+ | 11 | 20,000 items x 200 frames through a state struct field | no | 22272 | no (reasons C, D) | 22178 | - | 25 | 25 |
50
+ | 11b | same, `Vec` in a local handed through a function | no | 24335 | yes | 31 | - | - | - |
51
+ | 11c | same, `mut` field `w.items = w.items.set(i, ..)` | no | 24590 | no (reasons D, E) | 22270 | - | - | - |
52
+ | 11d | same on a persistent `List`, `l = l.map(..)` | - | - | - | - | - | - | 15-19 |
53
+ | 12 | one value shared on purpose | copies, correct | 0 | copies, correct | 0 | - | - | - |
54
+
55
+ Results are the same with `--no-fbip` (the sweep checks the checksums), and the answers in shape 12 hold in every program.
56
+
57
+ Reasons the unique-name versions are not proved (verbatim from `--explain-fusion`):
58
+
59
+ * A (8a): `row` is passed to method `upush`, which may keep it. The row is a local that is pushed into the outer Vec at its last use. The proof treats passing a value as an argument as possibly keeping it, so `row` stops being unique, and then the `push` calls on `row` fail, and the proof is by name, so the push on the outer Vec fails too.
60
+ * B (9): `v` is bound to a value that may be shared (call of `upush` from run9); a call from make9 passes a unique value. `make9` builds `v` in a loop with `v = v.upush(i)` and returns it, but the caller does not take `make9(n)` as fresh. 9b (the caller only reads, or pushes onto another vec) is proved, so the failure is the caller's later `v = v.upush(..)` on a returned value.
61
+ * C (10, 11): field `items` is passed on without being written back (call of `upush`/`uset` from `step10`/`frame11`). The functional form `State(..s, items: s.items.push(x))` hands the field to a user method inside a spread. The proof accepts `S(..s, f: s.f.with(..))` for the intrinsic `Array.with`, but not the same shape through a user method. Four variants, each in its own program, all failed the same way: a spread through a function, a constructor without a spread, a spread inline in the loop, and a local `v := s.items.push(x)` then `S(items: v, ..)`.
62
+ * D (11, 11c): `v` is stored in field `items` (call of `upush` from run11). Building the Vec in a local and moving it into `World(items: v, ..)` at its last use makes `v` non-unique.
63
+ * E (11c): field `items` may be shared (call of `uset` from frame14), the `mut`-field form, once D has poisoned the field.
64
+
65
+ ### Poisoning between shapes
66
+
67
+ One shared name, one program. Unique names, shapes 1 to 7 (+13 or 15) in one program: all proved, 1 to 3 ms (`vec_unique_names.plum`, in the sweep). Adding any one of these to that program turns every other shape into a copy (1260 to 4000 ms): 8 (a Vec stored in a container), 9, 10, or 12 (a value shared on purpose, a test with `a.push` twice). A single shared use anywhere makes the method copy for every caller; the proof has no per-call-site copy (the header says so). Shape 12 alone is enough, so a program with one undo snapshot or one saved previous state loses in-place everywhere. In the realistic-name program (`vec_combined.plum`) every shape copies.
68
+
69
+ ### Other observations
70
+
71
+ * `bigger := self.data.resize(n)` then `Vec(data: bigger.with(..), ..)` reported `bigger is read after the update` although it is not (growth branch). Chaining `self.data.resize(n).with(..)` is proved; `vec.plum` uses that form. A proof false negative that costs one extra copy per growth.
72
+ * In `plum run` the standard library's `test` blocks are part of the analysis ("a call from test Array ..."). Calls in imported std tests poison names in a program that never runs those tests.
73
+ * Cost where it works: `Vec` in place is 10 ms per 1M pushes against `Slice` 7 ms (1.4x), and 31 ms against 25 ms for 4M indexed updates (shape 11b vs 11).
74
+ * A persistent `List` rebuilt each frame with `l.map(|x| x + 1)` does the 4M updates of shape 11 in 15 to 19 ms, as fast as in-place `Slice` (25 ms). Rebuilding whole collections per frame is already cheap; FBIP only matters for updating or pushing one element of a big collection.
75
+ * 11 copying: 22 s for 4M updates, 5.5 microseconds per update of a 20,000-element Vec (an 8-byte-per-slot copy each time). Updating a handful of elements per frame in a copying Vec is affordable (about 50 microseconds for ten); updating every item is not.
76
+
77
+ ## `fip fun`
78
+
79
+ `tests/fbip/vec_fip.plum`: `FVec` with `fip fun fpush` and `fip fun fset` (unique names) compiles and passes. A caller that holds the vec twice (`b := a; c := a.gpush(2)`) is refused with `type error: not in place: self may be shared by a caller: a is also held by b (call of gpush from keep)` at the call's line and column. That error is useful. The one for collisions is not: with the realistic names `fip fun push` is refused with `v is bound to a value that may be shared (call of push from fill)`, naming a local that is fine instead of the colliding `Slice.push` and `Str.split`. Another early failure while writing the test: `v := fill(20); v = v.fset(3, 99)` is refused (reason B above), correctly but surprisingly.
80
+
81
+ ## Call sites in std and examples
82
+
83
+ Method: `grep -nE '\.(push|pop|set|removeAt|clear|add)\('` over `std` and `examples` `.plum` files, then a script that skips comment lines and splits sites inside `test` blocks from the rest, and classifies the rest by receiver text and method (no type information, so some `set` sites are guesses between `Array.set` and `Map.set`). The earlier figure of 363 was a different pattern; this count is 477 sites, of which 287 are inside `test` blocks (they test the APIs, and are ported with them) and 190 are code:
84
+
85
+ | category | sites | would work (with unique names and today's proof) | would not |
86
+ |---|---|---|---|
87
+ | `push` on a local or parameter (a builder: `out := Slice[T](); loop out.push(..); out`) | 68 | most: shape 1/9b. Fail if the result is pushed onto after being returned (shape 9) or if the vec is stored in a container first (8a) | the 9 and 8a cases, an estimate of 10 to 15 sites |
88
+ | Array element write inside a data structure (`self.data.set(..)` in Slice, Map, Deque, Heap, Grid, Scene's sort) | 46 | shape 2: already `self.data = self.data.with(..)` on a `mut` field; but all of these types share field names `data`, `keys`, `buf`, `items`, so need per-type keying; `Slice.slice` shares `data` (see views) | about the same 46 until per-type |
89
+ | scene-node `add` (the mutable tree API `parent.add(child)`) | 30 | not applicable: a tree with shared children | not a collection update |
90
+ | `Map.set` / `Slice.set` | 12 | Map.set returns nothing today; a persistent Map is a separate rewrite | |
91
+ | `pop` / `removeAt` / `clear` (shrinking, return a value) | 11 | need a new API (the removed element and the new collection) | all need a redesign |
92
+ | `push` on a `mut` struct field (`self.mines.push(..)`, `w.orbs.push(..)`) | 10 | shape 2 and 3 | |
93
+ | `canvas.clear` / `io.clear`, math `add`, other | 12 | not collections | |
94
+ | `push` on `self` (collection internals) | 1 | | |
95
+
96
+ So of the 190 code sites, about 22 are not collection updates and 30 are the scene tree. Of the other 138 collection updates, about 124 (the builders, `mut`-field pushes and Array writes) are in shapes that work in isolation, and only if per-type keying exists and no shared use of the same collection API exists anywhere in the program; about 25 to 30 of the pushes and all 11 shrinking ops need a changed API or a proof extension. Nearly all of them are at the mercy of the name collision, and every one would be at the mercy of a single shared use.
97
+
98
+ ## Shared views: `Slice.slice`
99
+
100
+ `Slice.slice(i, j)` returns a `Slice` over the same `data` array with a different `start` and `size`; `set` on the view changes the parent. A persistent `Slice` cannot do that: two holders of one array make `data` non-unique, and the proof is by field name, so every `Slice` write would copy. Outside `std/Slice.plum`'s own tests, `.slice(` has one call in std and examples (a `Str.slice` test). `sublist` and `drop` already copy. So the sharing view is not used in real code and a persistent Slice can make `slice` copy (or drop it), losing nothing a program uses. A real read-only view (`SliceView`, borrows the array) would be a separate type with no write methods.
101
+
102
+ ## Differential and fuzz
103
+
104
+ * `target/debug/plum test std examples tests`: 961 passed, 0 failed, 209 files (786 before the plan). With `--no-fbip`, `--no-fusion` and the default, `target/debug/plum test tests/fbip` gives 144 passed, 0 failed, 28 files each.
105
+ * `bash tests/fbip/fuzz/fuzz.sh 1000 1100`: ok=101, diff=0, err=0.
106
+
107
+ ## VERDICT
108
+
109
+ **Stop stage 2 as planned. Go only after fixes, and the fixes are not small.** Porting `Slice`, `Map` and the rest to persistent APIs now would make them slower than today's mutating versions in the cases that matter (22 s for the game-update shape that `Slice` does in 25 ms), because none of the real names are proved.
110
+
111
+ The pilot does show the in-place machinery is sound and fast where it applies: with unique names, shapes 1 to 7 and 11b are in place at 1.4x the cost of `Slice`, the answers match with and without `--no-fbip`, and the fuzzer finds nothing. The work is in the proof's reach, not in `Array.with` or `fip fun`.
112
+
113
+ Fixes the proof needs, in order of how much they matter:
114
+
115
+ 1. **Per-type keying.** Without it nothing works under real names. The checker already resolves every method call and field access; record the resolved callee and the field's owner type per `NodeId` and key the proof's tables by (type, method) and (type, field) instead of by name. This also removes std's tests and `Str.split` from the picture. Cost: the checker emits a side table per instantiation, `uniqueness.rs` (about 2,000 lines) changes its `methodIdx` and `fields` keys and its method resolution; the aliasing tests stay valid. The largest single item.
116
+ 2. **No poisoning by a shared use.** A shared call site must only make that call site copy. The usual way is a second compiled copy of the method for shared receivers (`push` and `push#shared`), picked per call site by the proof; the cost is code size (each used method up to twice) and a proof that is per call site, not a fixpoint on the method. Without this, one snapshot in a program, which the deterministic simulation (replay, rewind) wants, turns all updates to copies.
117
+ 3. **Hand-over forms.** `S(..s, f: s.f.m(..))` through a user method (shape 10, the functional game's form); a unique local moved into a constructor field, an argument or a container at its last use (shapes 8a, 11); a function returning its own unique local counting as fresh to its caller (shape 9). All three are one idea, ownership moves at the last use, and each is a rule in the proof with an aliasing test. Cost: moderate; each form is local to one function.
118
+ 4. Smaller: the `bigger.with(..)` false negative; error messages that name the colliding method instead of blaming a local.
119
+
120
+ Rough sense of the order: 1 is a week of care, 2 and 3 are each smaller but 2 changes code generation. A stage 2 that ports only `Array`-backed types the engine controls, with unique-looking names, would work today but is exactly the workaround this pilot says real collections cannot use.
121
+
122
+ ## What this means for removing the imperative scene and `mut`
123
+
124
+ Not supported by the numbers, in either direction:
125
+
126
+ * `mut` fields and the imperative scene are the only shapes that are fast today under real names (`Slice` at 7 ms per 1M pushes, 25 ms for the 4M-update frame loop). Removing them and returning the next game from `update` would make every collection update go through `Vec`-style code that copies, 22 s in the frame loop, until fixes 1 to 3 land.
127
+ * But the functional style that rebuilds per frame is not the problem. `List.map` per frame does the same 4M updates in 15 to 19 ms, so a game that returns a new world each frame from `update`, built from lists and spreads of small structs, is already fast. What is slow is a big array or map kept across frames and updated at a few indices; those are the shapes the proof does not reach.
128
+ * The scene tree (30 `add` sites) is a mutable tree with shared children whatever the proof does; it is not a candidate for this mechanism.
129
+
130
+ Recommendation: keep `mut` and the imperative scene for now, as the approved review plan says. Decide the removal after fixes 1 and 2 and a second pilot that repeats shapes 10 and 11 under real names. If fix 1 turns out larger than expected, an alternative worth weighing is `fip`-checked, explicitly owned collections at the type level instead of a program-wide proof.
docs/plans/2026-11-06-fbip.benchmark.md CHANGED
@@ -50,3 +50,7 @@ Each probe has a `test` at size 1000 and the sweep covers them.
50
50
  | `plum build examples/games/functional/game_state_view.plum -o <scratch>` (release), 3 runs | 0.77, 0.75, 0.75 s |
51
51
  | `plum.wasm` for that game | 79,397 bytes |
52
52
  | `target/debug/plum test std examples tests` | 61 s wall (83 s user), 786 tests, 178 files, load 5 to 8 |
53
+
54
+ ## After stage 1 (T6)
55
+
56
+ The pilot persistent `Vec` and the same shapes on `Slice` are measured in docs/plans/2026-11-06-fbip-stage1.results.md (`tests/fbip/vec_*.plum`, `tests/fbip/slice_shapes.plum`). Release build, load 1.6 to 3.8. In short: `Slice` 1M pushes 7 ms, persistent `Vec` in place 10 ms with unique names, 1281 ms for 100k pushes (copying) with the realistic names `push`/`data`; per-frame update of 20,000 items for 200 frames: `Slice` 25 ms, `List.map` 15 to 19 ms, copying `Vec` 22 s, in-place `Vec` on a local 31 ms.
docs/plans/2026-11-06-receiver-functions.review.md DELETED
@@ -1,108 +0,0 @@
1
- # Top-level functions with receivers: review plan
2
-
3
- **Status:** planned, not approved. Runs after FBIP, because both rewrite the same files. Nothing is built until you say so.
4
- **Your idea (2026-11-06):** all functions are top level, written `fun Color.toStr(self)`. `primitive` is restricted to the standard library. Functions that need grouping go in their own module.
5
-
6
- ## Summary
7
-
8
- Today a method is written inside its type's body, and a group of related functions goes in a `primitive`. This plan gives one way to define a function: always at the top level, with the type in its name when it belongs to one.
9
-
10
- ```plum
11
- enum Color =
12
- | Red
13
- | Green
14
- | Blue
15
-
16
- fun Color.toStr(self) -> Str = match self
17
- Red => "Red"
18
- Green => "Green"
19
- Blue => "Blue"
20
- ```
21
-
22
- Calling code doesn't change: `c.toStr()` still works, and so does everything built on it, such as generics, operators, `for` and overrides. The compiler already lifts nested methods to top-level functions with a receiver type internally, so this is mostly a change to the grammar and the source files.
23
-
24
- ## What you will get
25
-
26
- - One way to write a function, and one less level of indentation everywhere.
27
- - A type's declaration shows only its fields or variants, so it's easy to read at a glance.
28
- - The definition looks like the call: `fun Vec2.add` and `Vec2.add` read the same way.
29
- - User code has no `primitive` to learn.
30
- - Modules do the grouping, like Gleam, which also gives Plum real namespaces.
31
-
32
- ## How it looks
33
-
34
- ```plum
35
- struct Vec2(x: Float, y: Float)
36
-
37
- fun Vec2.add(self, o: Vec2) -> Vec2 = # self: a method, called v.add(o)
38
- Vec2(x: self.x + o.x, y: self.y + o.y)
39
-
40
- fun Vec2.unit() -> Vec2 = # no self: a static, called Vec2.unit()
41
- Vec2(x: 1.0, y: 1.0)
42
-
43
- fun Box[T].map[B](self, f: fn(T) -> B) -> Box[B] = # generics
44
- Box(value: f(self.value))
45
-
46
- override fun Orbs.update(self, dt: Float) = ... # overrides
47
-
48
- abstract fun Game.draw(self) # abstract methods
49
-
50
- fun steer(ship: Ship, dt: Float) -> Ship = ... # a plain function
51
- ```
52
-
53
- ## The real work: modules need to become namespaces
54
-
55
- **This is the main finding, and the biggest part of the plan.** Grouping functions in a module only works if a module has its own names. Today it doesn't: the loader merges every imported file's items into one program, so all names share one global namespace. Two files can't both define `clamp`, as that's a "duplicate declaration" error, and there's no `math.clamp(x)` form. `Math.sqrt` only works today because `Math` is a `primitive`.
56
-
57
- So user code needs something like this:
58
-
59
- ```plum
60
- import app/rules
61
-
62
- fun main(io: Io) =
63
- n := rules.clamp(5, 0, 3)
64
- ```
65
-
66
- ## What will be built
67
-
68
- | Part | What it does | Size |
69
- |---|---|---|
70
- | Module namespaces | `import a/b` gives the name `b`, with `b.f()`, `b.T` and `b.Const`. Two modules can define the same name. Type methods come with their type | L |
71
- | Receiver syntax | `fun T.m(self)`, statics, generics, `override`, `abstract`, `extern` on `Io`, in the grammar, parser, formatter and LSP | M |
72
- | Top-level constants | `Vec2.Zero = ...` for a type, and `MaxRetries = 3` for a module (193 constants today, all inside bodies) | S |
73
- | Migration tool | A script rewrites nested methods to top level. About 1,050 functions: 741 in `std`, 252 in `examples`, 64 in `tests` | M |
74
- | User primitives | `Config`, `Grid`, `Id`, `Layout`, `Rules`, `Shelf` in the examples become types or modules, and `primitive` outside `std` becomes an error | S |
75
- | Remove the old form | A method inside a type body becomes a parse error with a pointer to the new form | S |
76
- | Docs | Language guide and examples | S |
77
-
78
- ## Decisions for you
79
-
80
- 1. **What about std's namespaces?** `Math`, `Os`, `Http`, `Testing`, `Base64`, `Ui`, `Key`, `Mouse` and `Blend` are `primitive` namespaces in `std`. You said `primitive` stays allowed in `std`, so `Math.sqrt` can stay as is. **I'd convert the plain namespaces to modules too,** keeping `primitive` for what it's truly for, declaring built-ins (`Int`, `Str`, `Io`, `Array`). Then `primitive` means one thing, and users see one model. That changes the case: `import std/math`, then `math.sqrt(x)`. It's more churn, though, and could be a later step.
81
-
82
- 2. **Must a method live in the same file as its type?** I'd say yes, like Go: there are no surprise methods from imports, and one file shows all of a type's behaviour. `std/Json` adds `toJson` to `Int`, `Str` and others, so std stays exempt, as it is today.
83
-
84
- 3. **How are names imported?** I'd start with `import a/b` then `b.f()`, like Gleam, plus types usable unqualified where the import names them. Does `import std/game/Game` still give plain `Game`, `Key` and `Canvas`? **I'd keep types and enum variants unqualified by import,** since `Game` and `Some` everywhere would otherwise get noisy, and require the module name only for plain functions and constants. Worth a look at real code before settling.
85
-
86
- 4. **Free functions in `main`'s file?** You earlier noted the opposite idea, that every function should belong to something. This plan replaces that note: plain functions stay, in modules. I'd also keep `steer(ship, dt)` style rules as plain functions.
87
-
88
- ## Risks
89
-
90
- - **Namespaces touch the loader, checker, codegen and LSP.** They're the risky part, since names are assumed global throughout. Internally, the simplest route is to mangle names per module when merging, so later passes stay unchanged.
91
- - **A large diff.** About 1,050 functions move, so review has to rely on tooling. The check is that example wasm stays byte-identical across the syntax migration, since the syntax changes and the program doesn't.
92
- - **Self-hosting and FBIP.** The FBIP `fip` keyword has to fit the new form, as `fip fun T.m(self)`, and stage 2's call-site rewrites should land first.
93
- - **Scattering.** Methods can drift away from their type within a file. `plum format` could keep them together if this becomes annoying.
94
-
95
- ## Out of scope
96
-
97
- Package management, visibility (`pub`/private), and multi-file types.
98
-
99
- ## How we will know it works
100
-
101
- - Wasm is byte-identical before and after the syntax migration, on every example.
102
- - Two modules can define the same function name and both work.
103
- - All tests pass, and the docs show one form only.
104
- - `primitive` outside `std` and a method inside a type body each give a clear error.
105
-
106
- ## Rough time and cost
107
-
108
- Two builds. Namespaces first, about 6 to 8 tasks, then receiver syntax and migration, about 5 to 6 tasks. Could land in either order, but namespaces first lets the migration move grouped functions straight into modules.
docs/plans/2026-11-06-simpler-language.review.md ADDED
@@ -0,0 +1,112 @@
1
+ # A simpler language: review plan
2
+
3
+ **Status:** waiting for your decisions. Nothing here is built. This replaces the receiver-functions plan, whose `fun Orb.update(self)` syntax you rejected.
4
+ **Evidence:** FBIP stage 1 results, docs/plans/2026-11-06-fbip-stage1.results.md.
5
+
6
+ ## Summary
7
+
8
+ Remove inheritance, two of the three scene styles, and `primitive` namespaces from user code. Give Plum real modules. Make games plain structs. Then finish FBIP, which gets much simpler once inheritance is gone.
9
+
10
+ The pilot showed that FBIP can't go ahead as planned: the in-place proof works by name across the whole program, so a collection with ordinary names like `push` and `data` is never updated in place. That makes the work here a prerequisite, not just a tidy-up.
11
+
12
+ ## What a game looks like
13
+
14
+ Methods stay indented under their type. Plain functions and constants live at the module level.
15
+
16
+ ```plum
17
+ import std/game/gfx
18
+ import std/game/input
19
+ import std/game/scene
20
+
21
+ ArenaW = 800.0
22
+
23
+ struct Ship(x: Float, vx: Float) =
24
+ fun update(self, ax: Float, dt: Float) -> Ship =
25
+ Ship(..self, x: clamp(self.x + self.vx * dt, ArenaW), vx: self.vx + ax * 300.0 * dt)
26
+
27
+ fun render(self, sheet: Texture) -> Node =
28
+ Sprite(tex: sheet, x: self.x, y: 300.0, w: 32.0, h: 32.0)
29
+
30
+ struct OrbRun(ship: Ship, sheet: Texture) =
31
+ fun update(self, io: Io, dt: Float) -> OrbRun =
32
+ OrbRun(..self, ship: self.ship.update(input.axis(io, Key.Left, Key.Right), dt))
33
+
34
+ fun draw(self, io: Io) =
35
+ gfx.clear(io, Color.Black)
36
+ self.ship.render(self.sheet).draw(io)
37
+
38
+ fun clamp(v: Float, max: Float) -> Float =
39
+ v.max(0.0).min(max)
40
+
41
+ fun main(io: Io) -> OrbRun =
42
+ OrbRun(ship: Ship(x: 0.0, vx: 0.0), sheet: gfx.loadTexture(io, "sprites.png"))
43
+ ```
44
+
45
+ A game is any type with `update` and `draw`, found by duck typing, as with Ebiten.
46
+
47
+ ## What will be built
48
+
49
+ In this order, because each step makes the next one easier.
50
+
51
+ | Step | Part | What it does | Size |
52
+ |---|---|---|---|
53
+ | 1 | Plain games | Any type with `update` and `draw` is a game, and `update` returns the next one. Ports every game and fixture | M |
54
+ | 1 | One scene style | Remove `std/game/imperative` and `std/game/pure` (about 2,250 lines) and their examples. State-and-view stays, as `std/game/scene` | M |
55
+ | 2 | No inheritance | Remove `abstract`, `override` and struct embedding: `embeds.rs`, subtyping, most of `ctors.rs`, override dispatch | L, mostly deletion |
56
+ | 3 | Modules | `import a/b` gives `b.f()` and `b.Const`. Two modules can reuse a name. Module-level constants. `extern` on module-level functions, with `io` first. `primitive` only for std's built-ins | L |
57
+ | 4 | Syntax | The changes in decision 3 below | M |
58
+ | 5 | Proof by type | The proof uses the checker's types, so `Vec.push` and `Slice.push` are separate. With no overrides, every call has exactly one target | L |
59
+ | 6 | Last-use moves | A local moved into a field or returned counts as handed over: `S(..s, f: s.f.push(x))` through a method, and a returned unique local | M |
60
+ | 7 | Shared and unique callers | One shared call no longer turns every call into a copy, by compiling a second version of the function | M |
61
+ | 8 | FBIP stage 2 | Run the pilot again. If it passes, port `Slice`, `Map`, `Buffer`, `Deque` and the rest | L |
62
+
63
+ ## What the pilot measured
64
+
65
+ 20,000 items updated for 200 frames, through a state struct field, on a release build:
66
+
67
+ | Version | Time |
68
+ |---|---|
69
+ | Today's `Slice` with `mut` | 25 ms |
70
+ | A persistent `List`, `l = l.map(..)` | 15 to 19 ms |
71
+ | A pilot `Vec` in a local, proved in place | 31 ms |
72
+ | A pilot `Vec` in a state struct field, copying because the proof fails | 22,000 ms |
73
+
74
+ Two things follow:
75
+ - **In-place works when it's proved,** close to `Slice`. When it isn't proved, it's about 900 times slower. That's the risk `fip fun` exists to catch.
76
+ - **A persistent `List` is already fast, even faster than `Slice`.** The state-and-view style holds lists, so games using it are safe now, before any of steps 5 to 8.
77
+
78
+ ## Decisions for you
79
+
80
+ 1. **Remove inheritance?** Recommended. It costs little in features (enums and structs of functions cover it), and it removes most of the difficulty in the proof.
81
+ 2. **Remove the imperative scene?** Recommended. If you'd rather keep a tree of objects you can change, the alternative is to keep it, written with `mut` fields and enums.
82
+ 3. **Which syntax changes?**
83
+ - **Block `if` as an expression, and no `return`.** Recommended, the most valuable and the most disruptive. Fusion skips any function with an early `return`, so this turns the optimiser on everywhere. `?` stays. Returning from inside a loop would need `fold` or a helper.
84
+ - **`self.items .= push(x)`** for `self.items = self.items.push(x)`. Recommended.
85
+ - **`State(..s, ship.x: 1.0)`**, paths in a spread. Recommended, because binding a field to a variable blocks the proof.
86
+ - `consume`, a move marker: only if steps 5 to 7 leave a real gap.
87
+ 4. **Should std's namespaces become modules?** `Math.sqrt` becomes `math.sqrt`, and so on. Recommended, so `primitive` means one thing: declaring a built-in.
88
+ 5. **How are names imported?** Recommended: types and enum variants are unqualified, and functions and constants use the module name: `Texture`, `Some`, but `gfx.rect`.
89
+ 6. **Remove `mut`?** Later. It becomes possible after step 8, if the shells no longer need it. Decide then, with numbers.
90
+
91
+ ## Risks
92
+
93
+ - **Silent copies are the top risk.** A shape the proof misses is about 900 times slower, with no error. `--explain-fusion` shows it and `fip fun` makes it an error, but a game that doesn't use `fip` just gets slow.
94
+ - **Steps 5 to 7 may not cover real game code.** The pilot re-run at step 8 decides that, and `List` remains a fast fallback either way.
95
+ - **Wrong proofs are silent bugs.** Each proof change gets the same attack as stage 1: hostile programs and the fuzzer in `tests/fbip/fuzz`, compared against `--no-fbip`. That found four real miscompiles in stage 1.
96
+ - **Losing the object model.** People from libGDX or Unity lose a familiar style, and very large worlds rely on fusion. Measure `stress_scene` and `stress_world` at step 1.
97
+ - **Modules touch every pass,** since names are assumed global throughout. Mangling names per module while merging keeps later passes unchanged.
98
+
99
+ ## Out of scope
100
+
101
+ Package management, visibility (`pub`/private), multi-touch, and splitting `Io`.
102
+
103
+ ## How we will know it works
104
+
105
+ - Each step keeps all tests green, compared against a recorded baseline.
106
+ - The pilot's table passes with realistic names, in particular a state struct field.
107
+ - `stress_scene` and `stress_world` are no slower than today, on a quiet machine.
108
+ - The compiler is smaller at the end than now.
109
+
110
+ ## Rough time and cost
111
+
112
+ Large, but steps 1 and 2 are mostly deletion. Each step is its own build and can be stopped at. Steps 1 to 4 are worth having even if FBIP stops there.
tests/fbip/slice_shapes.plum ADDED
@@ -0,0 +1,197 @@
1
+ import std/Int
2
+ import std/Str
3
+ import std/Float
4
+ import std/Time
5
+ import std/Io
6
+ import std/Slice
7
+ import std/List
8
+ import std/game/Vec2
9
+
10
+ # The comparison for tests/fbip/vec_*.plum: the same twelve shapes on `Slice`, which mutates in place
11
+ # today (`push`, `putAt`), and the per-frame update on a persistent `List` (`List.map`). Same
12
+ # checksums as the Vec versions. Kept in its own file because `Slice.push` and `Vec.push` share a
13
+ # name, which the uniqueness proof cannot tell apart.
14
+
15
+ fun s1(n: Int) -> Int =
16
+ v := Slice[Int]()
17
+ for i := range n
18
+ v.push(i)
19
+ v.length() + v.at(n - 1) + v.at(n / 2)
20
+
21
+ struct Bag2(items: Slice[Int], mut count: Int) =
22
+ fun add(self, x: Int) -> Unit =
23
+ self.items.push(x)
24
+ self.count = self.count + 1
25
+
26
+ fun s2(n: Int) -> Int =
27
+ b := Bag2(items: Slice[Int](), count: 0)
28
+ for i := range n
29
+ b.add(i)
30
+ b.items.length() + b.items.at(n - 1) + b.count
31
+
32
+ struct Inner3(items: Slice[Int])
33
+ struct Outer3(inner: Inner3, tag: Int)
34
+
35
+ fun s3(n: Int) -> Int =
36
+ o := Outer3(inner: Inner3(items: Slice[Int]()), tag: 5)
37
+ for i := range n
38
+ o.inner.items.push(i)
39
+ o.inner.items.length() + o.inner.items.at(n - 1) + o.tag
40
+
41
+ fun grow4(v: Slice[Int], x: Int) -> Slice[Int] =
42
+ v.push(x * 2)
43
+ v
44
+
45
+ fun s4(n: Int) -> Int =
46
+ v := Slice[Int]()
47
+ for i := range n
48
+ v = grow4(v, i)
49
+ v.length() + v.at(n - 1)
50
+
51
+ fun s5(n: Int) -> Int =
52
+ a := Slice[Int]()
53
+ b := Slice[Vec2]()
54
+ for i := range n
55
+ a.push(i)
56
+ b.push(Vec2(x: Float(i), y: 1.0))
57
+ r := a.length() + b.length() + a.at(n - 1)
58
+ if b.at(n - 1).x == Float(n - 1) then r else 0 - 1
59
+
60
+ fun s6(n: Int) -> Int =
61
+ v := Slice[Int]()
62
+ for i := range n
63
+ len := v.length()
64
+ v.push(len)
65
+ v.length() + v.at(n - 1)
66
+
67
+ struct Pair7(xs: Slice[Int], ys: Slice[Int])
68
+
69
+ fun s7(n: Int) -> Int =
70
+ p := Pair7(xs: Slice[Int](), ys: Slice[Int]())
71
+ for i := range n
72
+ p.xs.push(i)
73
+ p.ys.push(i * 2)
74
+ p.xs.length() + p.ys.length() + p.xs.at(n - 1) + p.ys.at(n - 1)
75
+
76
+ fun s8(n: Int) -> Int =
77
+ rows := Slice[Slice[Int]]()
78
+ for r := range n / 10
79
+ row := Slice[Int]()
80
+ for c := range 10
81
+ row.push(r * 10 + c)
82
+ rows.push(row)
83
+ rows.length() + rows.at(n / 10 - 1).at(9)
84
+
85
+ fun make9(n: Int) -> Slice[Int] =
86
+ v := Slice[Int]()
87
+ for i := range n
88
+ v.push(i)
89
+ v
90
+
91
+ fun s9(n: Int) -> Int =
92
+ v := make9(n)
93
+ for i := range n
94
+ v.push(i + n)
95
+ v.length() + v.at(2 * n - 1)
96
+
97
+ struct State10(items: Slice[Int], tick: Int)
98
+
99
+ fun step10(s: State10, x: Int) -> State10 =
100
+ s.items.push(x)
101
+ State10(..s, tick: s.tick + 1)
102
+
103
+ fun s10(n: Int) -> Int =
104
+ s := State10(items: Slice[Int](), tick: 0)
105
+ for i := range n
106
+ s = step10(s, i)
107
+ s.items.length() + s.items.at(n - 1) + s.tick
108
+
109
+ struct World11(items: Slice[Int], frame: Int)
110
+
111
+ fun frame11(w: World11, n: Int) -> World11 =
112
+ for i := range n
113
+ w.items.putAt(i, w.items.takeAt(i) + 1)
114
+ World11(..w, frame: w.frame + 1)
115
+
116
+ fun s11(n: Int, frames: Int) -> Int =
117
+ v := Slice[Int]()
118
+ for i := range n
119
+ v.push(0)
120
+ w := World11(items: v, frame: 0)
121
+ for f := range frames
122
+ w = frame11(w, n)
123
+ w.items.at(0) + w.items.at(n - 1) + w.frame
124
+
125
+ # the persistent alternative to Slice: a List, rebuilt each frame by `map`
126
+ fun s11list(n: Int, frames: Int) -> Int =
127
+ l: List[Int] := Nil
128
+ for i := range n
129
+ l = Cons(head: 0, tail: l)
130
+ for f := range frames
131
+ l = l.map(|x| x + 1)
132
+ l.at(0) + l.at(n - 1) + frames
133
+
134
+ fun main(io: Io) =
135
+ n := 100000
136
+ t := Time.now(io).toMillis()
137
+ r := s1(n)
138
+ u := Time.now(io).toMillis()
139
+ io.printLn("slice shape 1 result={r} ms={u - t}")
140
+ t = Time.now(io).toMillis()
141
+ r = s2(n)
142
+ u = Time.now(io).toMillis()
143
+ io.printLn("slice shape 2 result={r} ms={u - t}")
144
+ t = Time.now(io).toMillis()
145
+ r = s3(n)
146
+ u = Time.now(io).toMillis()
147
+ io.printLn("slice shape 3 result={r} ms={u - t}")
148
+ t = Time.now(io).toMillis()
149
+ r = s4(n)
150
+ u = Time.now(io).toMillis()
151
+ io.printLn("slice shape 4 result={r} ms={u - t}")
152
+ t = Time.now(io).toMillis()
153
+ r = s5(n)
154
+ u = Time.now(io).toMillis()
155
+ io.printLn("slice shape 5 result={r} ms={u - t}")
156
+ t = Time.now(io).toMillis()
157
+ r = s6(n)
158
+ u = Time.now(io).toMillis()
159
+ io.printLn("slice shape 6 result={r} ms={u - t}")
160
+ t = Time.now(io).toMillis()
161
+ r = s7(n)
162
+ u = Time.now(io).toMillis()
163
+ io.printLn("slice shape 7 result={r} ms={u - t}")
164
+ t = Time.now(io).toMillis()
165
+ r = s8(n)
166
+ u = Time.now(io).toMillis()
167
+ io.printLn("slice shape 8 result={r} ms={u - t}")
168
+ t = Time.now(io).toMillis()
169
+ r = s9(n / 2)
170
+ u = Time.now(io).toMillis()
171
+ io.printLn("slice shape 9 result={r} ms={u - t}")
172
+ t = Time.now(io).toMillis()
173
+ r = s10(n)
174
+ u = Time.now(io).toMillis()
175
+ io.printLn("slice shape 10 result={r} ms={u - t}")
176
+ t = Time.now(io).toMillis()
177
+ r = s11(20000, 200)
178
+ u = Time.now(io).toMillis()
179
+ io.printLn("slice shape 11 result={r} ms={u - t}")
180
+ t = Time.now(io).toMillis()
181
+ r = s11list(20000, 200)
182
+ u = Time.now(io).toMillis()
183
+ io.printLn("list map shape 11 result={r} ms={u - t}")
184
+
185
+ test "slice shapes agree with the vec shapes"
186
+ assert s1(1000) == 2499
187
+ assert s2(1000) == 2999
188
+ assert s3(1000) == 2004
189
+ assert s4(1000) == 2998
190
+ assert s5(1000) == 2999
191
+ assert s6(1000) == 1999
192
+ assert s7(1000) == 4997
193
+ assert s8(1000) == 1099
194
+ assert s9(500) == 1000 + 999
195
+ assert s10(1000) == 2999
196
+ assert s11(50, 5) == 15
197
+ assert s11list(50, 5) == 15
tests/fbip/uvec.plum ADDED
@@ -0,0 +1,22 @@
1
+ import std/Int
2
+ import std/Array
3
+ import std/Slice
4
+
5
+ # FBIP pilot, the unique-name copy of vec.plum: `udata`, `usize`, `upush`, `uset`, `uat`, `ulen`, names no std
6
+ # type uses, so the proof can see this type alone. Same code otherwise. Used by vec_unique_names.plum.
7
+
8
+ struct UVec[T](udata: Array[T], usize: Int) =
9
+ fun init() -> UVec[T] = UVec(udata: Array[T]().resize(0), usize: 0)
10
+
11
+ fun upush(self, v: T) -> UVec[T] =
12
+ if self.usize < self.udata.length()
13
+ UVec(..self, udata: self.udata.with(self.usize, v), usize: self.usize + 1)
14
+ else
15
+ UVec(udata: self.udata.resize(if self.usize == 0 then 8 else self.usize * 2).with(self.usize, v), usize: self.usize + 1)
16
+
17
+ fun uset(self, i: Int, v: T) -> UVec[T] =
18
+ UVec(..self, udata: self.udata.with(i, v))
19
+
20
+ fun uat(self, i: Int) -> T = self.udata.get(i)
21
+
22
+ fun ulen(self) -> Int = self.usize
tests/fbip/vec.plum ADDED
@@ -0,0 +1,24 @@
1
+ import std/Int
2
+ import std/Array
3
+ import std/Slice
4
+
5
+ # FBIP pilot: a persistent vector written only in Plum, with the realistic names a real
6
+ # collection would use (`data`, `size`, `push`, `set`, `at`, `length`). `push` and `set` return the
7
+ # new vector; they update in place only when the compiler proves nobody else holds the old one.
8
+ # Shapes that use it: tests/fbip/vec_*.plum. Results: docs/plans/2026-11-06-fbip-stage1.results.md.
9
+
10
+ struct Vec[T](data: Array[T], size: Int) =
11
+ fun init() -> Vec[T] = Vec(data: Array[T]().resize(0), size: 0)
12
+
13
+ fun push(self, v: T) -> Vec[T] =
14
+ if self.size < self.data.length()
15
+ Vec(..self, data: self.data.with(self.size, v), size: self.size + 1)
16
+ else
17
+ Vec(data: self.data.resize(if self.size == 0 then 8 else self.size * 2).with(self.size, v), size: self.size + 1)
18
+
19
+ fun set(self, i: Int, v: T) -> Vec[T] =
20
+ Vec(..self, data: self.data.with(i, v))
21
+
22
+ fun at(self, i: Int) -> T = self.data.get(i)
23
+
24
+ fun length(self) -> Int = self.size
tests/fbip/vec_combined.plum ADDED
@@ -0,0 +1,229 @@
1
+ import std/Int
2
+ import std/Str
3
+ import std/Float
4
+ import std/Time
5
+ import std/Io
6
+ import std/game/Vec2
7
+ import tests/fbip/vec
8
+
9
+ # Every shape in one program, to see whether one shape's calls poison another's.
10
+
11
+ # Shape 1: a Vec in a local, `v = v.push(i)` in a loop.
12
+
13
+ fun run1(n: Int) -> Int =
14
+ v := Vec[Int]()
15
+ for i := range n
16
+ v = v.push(i)
17
+ v.length() + v.at(n - 1) + v.at(n / 2)
18
+
19
+ # Shape 2: a `mut` field of a struct, `self.items = self.items.push(x)` in a method.
20
+
21
+ struct Bag2(mut items: Vec[Int], mut count: Int) =
22
+ fun add(self, x: Int) -> Unit =
23
+ self.items = self.items.push(x)
24
+ self.count = self.count + 1
25
+
26
+ fun run2(n: Int) -> Int =
27
+ b := Bag2(items: Vec[Int](), count: 0)
28
+ for i := range n
29
+ b.add(i)
30
+ b.items.length() + b.items.at(n - 1) + b.count
31
+
32
+ # Shape 3: a Vec in a field of a nested struct, `o.inner.items = o.inner.items.push(i)`.
33
+
34
+ struct Inner3(mut items: Vec[Int])
35
+ struct Outer3(inner: Inner3, tag: Int)
36
+
37
+ fun run3(n: Int) -> Int =
38
+ o := Outer3(inner: Inner3(items: Vec[Int]()), tag: 5)
39
+ for i := range n
40
+ o.inner.items = o.inner.items.push(i)
41
+ o.inner.items.length() + o.inner.items.at(n - 1) + o.tag
42
+
43
+ # Shape 4: a helper that takes the Vec and returns it, `v = grow(v, i)`.
44
+
45
+ fun grow4(v: Vec[Int], x: Int) -> Vec[Int] = v.push(x * 2)
46
+
47
+ fun run4(n: Int) -> Int =
48
+ v := Vec[Int]()
49
+ for i := range n
50
+ v = grow4(v, i)
51
+ v.length() + v.at(n - 1)
52
+
53
+ # Shape 5: Vec[Int] and Vec[Vec2] in one program.
54
+
55
+ fun run5(n: Int) -> Int =
56
+ a := Vec[Int]()
57
+ b := Vec[Vec2]()
58
+ for i := range n
59
+ a = a.push(i)
60
+ b = b.push(Vec2(x: Float(i), y: 1.0))
61
+ r := a.length() + b.length() + a.at(n - 1)
62
+ if b.at(n - 1).x == Float(n - 1) then r else 0 - 1
63
+
64
+ # Shape 6: the length is read between updates, `n := v.length(); v = v.push(n)`.
65
+
66
+ fun run6(n: Int) -> Int =
67
+ v := Vec[Int]()
68
+ for i := range n
69
+ len := v.length()
70
+ v = v.push(len)
71
+ v.length() + v.at(n - 1)
72
+
73
+ # Shape 7: two Vecs in one struct, both pushed.
74
+
75
+ struct Pair7(mut xs: Vec[Int], mut ys: Vec[Int])
76
+
77
+ fun run7(n: Int) -> Int =
78
+ p := Pair7(xs: Vec[Int](), ys: Vec[Int]())
79
+ for i := range n
80
+ p.xs = p.xs.push(i)
81
+ p.ys = p.ys.push(i * 2)
82
+ p.xs.length() + p.ys.length() + p.xs.at(n - 1) + p.ys.at(n - 1)
83
+
84
+ # Shape 8: a Vec of Vecs. 8a builds rows and pushes them; 8b pushes onto a row stored in the outer Vec (`rows = rows.set(j, rows.at(j).push(i))`).
85
+
86
+ fun run8(n: Int) -> Int =
87
+ rows := Vec[Vec[Int]]()
88
+ for r := range n / 10
89
+ row := Vec[Int]()
90
+ for c := range 10
91
+ row = row.push(r * 10 + c)
92
+ rows = rows.push(row)
93
+ rows.length() + rows.at(n / 10 - 1).at(9)
94
+
95
+ fun run8b(n: Int) -> Int =
96
+ rows := Vec[Vec[Int]]()
97
+ for r := range 10
98
+ rows = rows.push(Vec[Int]())
99
+ for i := range n
100
+ j := i % 10
101
+ rows = rows.set(j, rows.at(j).push(i))
102
+ rows.at(0).length() + rows.at(9).at(n / 10 - 1)
103
+
104
+ # Shape 9: built by one function, returned, then pushed by the caller.
105
+
106
+ fun make9(n: Int) -> Vec[Int] =
107
+ v := Vec[Int]()
108
+ for i := range n
109
+ v = v.push(i)
110
+ v
111
+
112
+ fun run9(n: Int) -> Int =
113
+ v := make9(n)
114
+ for i := range n
115
+ v = v.push(i + n)
116
+ v.length() + v.at(2 * n - 1)
117
+
118
+ # Shape 10: an immutable state struct, `State(..s, items: s.items.push(x))`, in a step function.
119
+
120
+ struct State10(items: Vec[Int], tick: Int)
121
+
122
+ fun step10(s: State10, x: Int) -> State10 =
123
+ State10(..s, items: s.items.push(x), tick: s.tick + 1)
124
+
125
+ fun run10(n: Int) -> Int =
126
+ s := State10(items: Vec[Int](), tick: 0)
127
+ for i := range n
128
+ s = step10(s, i)
129
+ s.items.length() + s.items.at(n - 1) + s.tick
130
+
131
+ # Shape 11: a game-like update, every item of a 20,000-item Vec updated per frame, through a struct field of an immutable state, for many frames.
132
+
133
+ struct World11(items: Vec[Int], frame: Int)
134
+
135
+ fun frame11(w: World11, n: Int) -> World11 =
136
+ for i := range n
137
+ w = World11(..w, items: w.items.set(i, w.items.at(i) + 1))
138
+ World11(..w, frame: w.frame + 1)
139
+
140
+ fun run11(n: Int, frames: Int) -> Int =
141
+ v := Vec[Int]()
142
+ for i := range n
143
+ v = v.push(0)
144
+ w := World11(items: v, frame: 0)
145
+ for f := range frames
146
+ w = frame11(w, n)
147
+ w.items.at(0) + w.items.at(n - 1) + w.frame
148
+
149
+ fun main(io: Io) =
150
+ t1 := Time.now(io).toMillis()
151
+ r1 := run1(100000)
152
+ u1 := Time.now(io).toMillis()
153
+ io.printLn("vec_combined shape 1 result={r1} ms={u1 - t1}")
154
+ t2 := Time.now(io).toMillis()
155
+ r2 := run2(100000)
156
+ u2 := Time.now(io).toMillis()
157
+ io.printLn("vec_combined shape 2 result={r2} ms={u2 - t2}")
158
+ t3 := Time.now(io).toMillis()
159
+ r3 := run3(100000)
160
+ u3 := Time.now(io).toMillis()
161
+ io.printLn("vec_combined shape 3 result={r3} ms={u3 - t3}")
162
+ t4 := Time.now(io).toMillis()
163
+ r4 := run4(100000)
164
+ u4 := Time.now(io).toMillis()
165
+ io.printLn("vec_combined shape 4 result={r4} ms={u4 - t4}")
166
+ t5 := Time.now(io).toMillis()
167
+ r5 := run5(100000)
168
+ u5 := Time.now(io).toMillis()
169
+ io.printLn("vec_combined shape 5 result={r5} ms={u5 - t5}")
170
+ t6 := Time.now(io).toMillis()
171
+ r6 := run6(100000)
172
+ u6 := Time.now(io).toMillis()
173
+ io.printLn("vec_combined shape 6 result={r6} ms={u6 - t6}")
174
+ t7 := Time.now(io).toMillis()
175
+ r7 := run7(100000)
176
+ u7 := Time.now(io).toMillis()
177
+ io.printLn("vec_combined shape 7 result={r7} ms={u7 - t7}")
178
+ t8 := Time.now(io).toMillis()
179
+ r8 := run8(100000) + run8b(2000)
180
+ u8 := Time.now(io).toMillis()
181
+ io.printLn("vec_combined shape 8 result={r8} ms={u8 - t8}")
182
+ t9 := Time.now(io).toMillis()
183
+ r9 := run9(50000)
184
+ u9 := Time.now(io).toMillis()
185
+ io.printLn("vec_combined shape 9 result={r9} ms={u9 - t9}")
186
+ t10 := Time.now(io).toMillis()
187
+ r10 := run10(100000)
188
+ u10 := Time.now(io).toMillis()
189
+ io.printLn("vec_combined shape 10 result={r10} ms={u10 - t10}")
190
+ t11 := Time.now(io).toMillis()
191
+ r11 := run11(20000, 200)
192
+ u11 := Time.now(io).toMillis()
193
+ io.printLn("vec_combined shape 11 result={r11} ms={u11 - t11}")
194
+
195
+ test "vec local"
196
+ assert run1(1000) == 2499
197
+
198
+ test "vec mutfield"
199
+ assert run2(1000) == 2999
200
+
201
+ test "vec nested"
202
+ assert run3(1000) == 2004
203
+
204
+ test "vec helper"
205
+ assert run4(1000) == 2998
206
+
207
+ test "vec generic"
208
+ assert run5(1000) == 2999
209
+
210
+ test "vec readbetween"
211
+ assert run6(1000) == 1999
212
+
213
+ test "vec twolists"
214
+ assert run7(1000) == 4997
215
+
216
+ test "vec listoflists"
217
+ assert run8(1000) == 1099
218
+
219
+ test "vec listoflists (2)"
220
+ assert run8b(1000) == 100 + 999
221
+
222
+ test "vec returned"
223
+ assert run9(500) == 1000 + 999
224
+
225
+ test "vec statespread"
226
+ assert run10(1000) == 2999
227
+
228
+ test "vec frames"
229
+ assert run11(50, 5) == 15
tests/fbip/vec_combined_shared.plum ADDED
@@ -0,0 +1,251 @@
1
+ import std/Int
2
+ import std/Str
3
+ import std/Float
4
+ import std/Time
5
+ import std/Io
6
+ import std/game/Vec2
7
+ import tests/fbip/vec
8
+
9
+ # Every shape in one program, to see whether one shape's calls poison another's.
10
+
11
+ # Shape 1: a Vec in a local, `v = v.push(i)` in a loop.
12
+
13
+ fun run1(n: Int) -> Int =
14
+ v := Vec[Int]()
15
+ for i := range n
16
+ v = v.push(i)
17
+ v.length() + v.at(n - 1) + v.at(n / 2)
18
+
19
+ # Shape 2: a `mut` field of a struct, `self.items = self.items.push(x)` in a method.
20
+
21
+ struct Bag2(mut items: Vec[Int], mut count: Int) =
22
+ fun add(self, x: Int) -> Unit =
23
+ self.items = self.items.push(x)
24
+ self.count = self.count + 1
25
+
26
+ fun run2(n: Int) -> Int =
27
+ b := Bag2(items: Vec[Int](), count: 0)
28
+ for i := range n
29
+ b.add(i)
30
+ b.items.length() + b.items.at(n - 1) + b.count
31
+
32
+ # Shape 3: a Vec in a field of a nested struct, `o.inner.items = o.inner.items.push(i)`.
33
+
34
+ struct Inner3(mut items: Vec[Int])
35
+ struct Outer3(inner: Inner3, tag: Int)
36
+
37
+ fun run3(n: Int) -> Int =
38
+ o := Outer3(inner: Inner3(items: Vec[Int]()), tag: 5)
39
+ for i := range n
40
+ o.inner.items = o.inner.items.push(i)
41
+ o.inner.items.length() + o.inner.items.at(n - 1) + o.tag
42
+
43
+ # Shape 4: a helper that takes the Vec and returns it, `v = grow(v, i)`.
44
+
45
+ fun grow4(v: Vec[Int], x: Int) -> Vec[Int] = v.push(x * 2)
46
+
47
+ fun run4(n: Int) -> Int =
48
+ v := Vec[Int]()
49
+ for i := range n
50
+ v = grow4(v, i)
51
+ v.length() + v.at(n - 1)
52
+
53
+ # Shape 5: Vec[Int] and Vec[Vec2] in one program.
54
+
55
+ fun run5(n: Int) -> Int =
56
+ a := Vec[Int]()
57
+ b := Vec[Vec2]()
58
+ for i := range n
59
+ a = a.push(i)
60
+ b = b.push(Vec2(x: Float(i), y: 1.0))
61
+ r := a.length() + b.length() + a.at(n - 1)
62
+ if b.at(n - 1).x == Float(n - 1) then r else 0 - 1
63
+
64
+ # Shape 6: the length is read between updates, `n := v.length(); v = v.push(n)`.
65
+
66
+ fun run6(n: Int) -> Int =
67
+ v := Vec[Int]()
68
+ for i := range n
69
+ len := v.length()
70
+ v = v.push(len)
71
+ v.length() + v.at(n - 1)
72
+
73
+ # Shape 7: two Vecs in one struct, both pushed.
74
+
75
+ struct Pair7(mut xs: Vec[Int], mut ys: Vec[Int])
76
+
77
+ fun run7(n: Int) -> Int =
78
+ p := Pair7(xs: Vec[Int](), ys: Vec[Int]())
79
+ for i := range n
80
+ p.xs = p.xs.push(i)
81
+ p.ys = p.ys.push(i * 2)
82
+ p.xs.length() + p.ys.length() + p.xs.at(n - 1) + p.ys.at(n - 1)
83
+
84
+ # Shape 8: a Vec of Vecs. 8a builds rows and pushes them; 8b pushes onto a row stored in the outer Vec (`rows = rows.set(j, rows.at(j).push(i))`).
85
+
86
+ fun run8(n: Int) -> Int =
87
+ rows := Vec[Vec[Int]]()
88
+ for r := range n / 10
89
+ row := Vec[Int]()
90
+ for c := range 10
91
+ row = row.push(r * 10 + c)
92
+ rows = rows.push(row)
93
+ rows.length() + rows.at(n / 10 - 1).at(9)
94
+
95
+ fun run8b(n: Int) -> Int =
96
+ rows := Vec[Vec[Int]]()
97
+ for r := range 10
98
+ rows = rows.push(Vec[Int]())
99
+ for i := range n
100
+ j := i % 10
101
+ rows = rows.set(j, rows.at(j).push(i))
102
+ rows.at(0).length() + rows.at(9).at(n / 10 - 1)
103
+
104
+ # Shape 9: built by one function, returned, then pushed by the caller.
105
+
106
+ fun make9(n: Int) -> Vec[Int] =
107
+ v := Vec[Int]()
108
+ for i := range n
109
+ v = v.push(i)
110
+ v
111
+
112
+ fun run9(n: Int) -> Int =
113
+ v := make9(n)
114
+ for i := range n
115
+ v = v.push(i + n)
116
+ v.length() + v.at(2 * n - 1)
117
+
118
+ # Shape 10: an immutable state struct, `State(..s, items: s.items.push(x))`, in a step function.
119
+
120
+ struct State10(items: Vec[Int], tick: Int)
121
+
122
+ fun step10(s: State10, x: Int) -> State10 =
123
+ State10(..s, items: s.items.push(x), tick: s.tick + 1)
124
+
125
+ fun run10(n: Int) -> Int =
126
+ s := State10(items: Vec[Int](), tick: 0)
127
+ for i := range n
128
+ s = step10(s, i)
129
+ s.items.length() + s.items.at(n - 1) + s.tick
130
+
131
+ # Shape 11: a game-like update, every item of a 20,000-item Vec updated per frame, through a struct field of an immutable state, for many frames.
132
+
133
+ struct World11(items: Vec[Int], frame: Int)
134
+
135
+ fun frame11(w: World11, n: Int) -> World11 =
136
+ for i := range n
137
+ w = World11(..w, items: w.items.set(i, w.items.at(i) + 1))
138
+ World11(..w, frame: w.frame + 1)
139
+
140
+ fun run11(n: Int, frames: Int) -> Int =
141
+ v := Vec[Int]()
142
+ for i := range n
143
+ v = v.push(0)
144
+ w := World11(items: v, frame: 0)
145
+ for f := range frames
146
+ w = frame11(w, n)
147
+ w.items.at(0) + w.items.at(n - 1) + w.frame
148
+
149
+ # Shape 12: one value shared on purpose. Every holder keeps its own contents; this is a correctness test.
150
+
151
+ fun run12() -> Int =
152
+ a := Vec[Int]()
153
+ for i := range 20
154
+ a = a.push(i)
155
+ b := a.push(100)
156
+ c := a.push(200)
157
+ d := a.set(0, 7)
158
+ e := b.set(1, 8)
159
+ ok := a.length() == 20 && a.at(0) == 0 && a.at(1) == 1
160
+ ok2 := b.length() == 21 && b.at(20) == 100 && b.at(1) == 1 && c.length() == 21 && c.at(20) == 200
161
+ ok3 := d.at(0) == 7 && e.at(1) == 8 && e.at(20) == 100 && b.at(0) == 0
162
+ if ok && ok2 && ok3 then 1 else 0
163
+
164
+ fun main(io: Io) =
165
+ t1 := Time.now(io).toMillis()
166
+ r1 := run1(100000)
167
+ u1 := Time.now(io).toMillis()
168
+ io.printLn("vec_combined shape 1 result={r1} ms={u1 - t1}")
169
+ t2 := Time.now(io).toMillis()
170
+ r2 := run2(100000)
171
+ u2 := Time.now(io).toMillis()
172
+ io.printLn("vec_combined shape 2 result={r2} ms={u2 - t2}")
173
+ t3 := Time.now(io).toMillis()
174
+ r3 := run3(100000)
175
+ u3 := Time.now(io).toMillis()
176
+ io.printLn("vec_combined shape 3 result={r3} ms={u3 - t3}")
177
+ t4 := Time.now(io).toMillis()
178
+ r4 := run4(100000)
179
+ u4 := Time.now(io).toMillis()
180
+ io.printLn("vec_combined shape 4 result={r4} ms={u4 - t4}")
181
+ t5 := Time.now(io).toMillis()
182
+ r5 := run5(100000)
183
+ u5 := Time.now(io).toMillis()
184
+ io.printLn("vec_combined shape 5 result={r5} ms={u5 - t5}")
185
+ t6 := Time.now(io).toMillis()
186
+ r6 := run6(100000)
187
+ u6 := Time.now(io).toMillis()
188
+ io.printLn("vec_combined shape 6 result={r6} ms={u6 - t6}")
189
+ t7 := Time.now(io).toMillis()
190
+ r7 := run7(100000)
191
+ u7 := Time.now(io).toMillis()
192
+ io.printLn("vec_combined shape 7 result={r7} ms={u7 - t7}")
193
+ t8 := Time.now(io).toMillis()
194
+ r8 := run8(100000) + run8b(2000)
195
+ u8 := Time.now(io).toMillis()
196
+ io.printLn("vec_combined shape 8 result={r8} ms={u8 - t8}")
197
+ t9 := Time.now(io).toMillis()
198
+ r9 := run9(50000)
199
+ u9 := Time.now(io).toMillis()
200
+ io.printLn("vec_combined shape 9 result={r9} ms={u9 - t9}")
201
+ t10 := Time.now(io).toMillis()
202
+ r10 := run10(100000)
203
+ u10 := Time.now(io).toMillis()
204
+ io.printLn("vec_combined shape 10 result={r10} ms={u10 - t10}")
205
+ t11 := Time.now(io).toMillis()
206
+ r11 := run11(20000, 200)
207
+ u11 := Time.now(io).toMillis()
208
+ io.printLn("vec_combined shape 11 result={r11} ms={u11 - t11}")
209
+ t12 := Time.now(io).toMillis()
210
+ r12 := run12()
211
+ u12 := Time.now(io).toMillis()
212
+ io.printLn("vec_combined shape 12 result={r12} ms={u12 - t12}")
213
+
214
+ test "vec local"
215
+ assert run1(1000) == 2499
216
+
217
+ test "vec mutfield"
218
+ assert run2(1000) == 2999
219
+
220
+ test "vec nested"
221
+ assert run3(1000) == 2004
222
+
223
+ test "vec helper"
224
+ assert run4(1000) == 2998
225
+
226
+ test "vec generic"
227
+ assert run5(1000) == 2999
228
+
229
+ test "vec readbetween"
230
+ assert run6(1000) == 1999
231
+
232
+ test "vec twolists"
233
+ assert run7(1000) == 4997
234
+
235
+ test "vec listoflists"
236
+ assert run8(1000) == 1099
237
+
238
+ test "vec listoflists (2)"
239
+ assert run8b(1000) == 100 + 999
240
+
241
+ test "vec returned"
242
+ assert run9(500) == 1000 + 999
243
+
244
+ test "vec statespread"
245
+ assert run10(1000) == 2999
246
+
247
+ test "vec frames"
248
+ assert run11(50, 5) == 15
249
+
250
+ test "vec shared"
251
+ assert run12() == 1
tests/fbip/vec_fip.plum ADDED
@@ -0,0 +1,33 @@
1
+ import std/Int
2
+ import std/Array
3
+
4
+ # `fip fun` on the pilot's shape: the compiler accepts a push/set written as a spread plus `Array.with`
5
+ # when it proves the update in place, and refuses the program (tests/fbip_u/ shows the error) when
6
+ # it does not. Names are unique (`fdata`, `fpush`) because the proof is by name.
7
+
8
+ struct FVec[T](fdata: Array[T], fsize: Int) =
9
+ fun init() -> FVec[T] = FVec(fdata: Array[T]().resize(0), fsize: 0)
10
+
11
+ fip fun fpush(self, v: T) -> FVec[T] =
12
+ if self.fsize < self.fdata.length()
13
+ FVec(..self, fdata: self.fdata.with(self.fsize, v), fsize: self.fsize + 1)
14
+ else
15
+ FVec(fdata: self.fdata.resize(if self.fsize == 0 then 8 else self.fsize * 2).with(self.fsize, v), fsize: self.fsize + 1)
16
+
17
+ fip fun fset(self, i: Int, v: T) -> FVec[T] =
18
+ FVec(..self, fdata: self.fdata.with(i, v))
19
+
20
+ fun fat(self, i: Int) -> T = self.fdata.get(i)
21
+
22
+ fun flen(self) -> Int = self.fsize
23
+
24
+ fun fill(n: Int) -> FVec[Int] =
25
+ v := FVec[Int]()
26
+ for i := range n
27
+ v = v.fpush(i)
28
+ v = v.fset(3, 99)
29
+ v
30
+
31
+ test "fip vec: push and set"
32
+ v := fill(20)
33
+ assert v.flen() == 20 && v.fat(3) == 99 && v.fat(19) == 19
tests/fbip/vec_frames.plum ADDED
@@ -0,0 +1,34 @@
1
+ import std/Int
2
+ import std/Str
3
+ import std/Float
4
+ import std/Time
5
+ import std/Io
6
+ import std/game/Vec2
7
+ import tests/fbip/vec
8
+
9
+ # Shape 11: a game-like update, every item of a 20,000-item Vec updated per frame, through a struct field of an immutable state, for many frames.
10
+
11
+ struct World11(items: Vec[Int], frame: Int)
12
+
13
+ fun frame11(w: World11, n: Int) -> World11 =
14
+ for i := range n
15
+ w = World11(..w, items: w.items.set(i, w.items.at(i) + 1))
16
+ World11(..w, frame: w.frame + 1)
17
+
18
+ fun run11(n: Int, frames: Int) -> Int =
19
+ v := Vec[Int]()
20
+ for i := range n
21
+ v = v.push(0)
22
+ w := World11(items: v, frame: 0)
23
+ for f := range frames
24
+ w = frame11(w, n)
25
+ w.items.at(0) + w.items.at(n - 1) + w.frame
26
+
27
+ fun main(io: Io) =
28
+ t11 := Time.now(io).toMillis()
29
+ r11 := run11(20000, 200)
30
+ u11 := Time.now(io).toMillis()
31
+ io.printLn("vec_frames shape 11 result={r11} ms={u11 - t11}")
32
+
33
+ test "vec frames"
34
+ assert run11(50, 5) == 15
tests/fbip/vec_frameslocal.plum ADDED
@@ -0,0 +1,31 @@
1
+ import std/Int
2
+ import std/Str
3
+ import std/Float
4
+ import std/Time
5
+ import std/Io
6
+ import std/game/Vec2
7
+ import tests/fbip/vec
8
+
9
+ # Shape 11b: the per-frame update on a Vec held in a local and handed through a function, no struct around it.
10
+
11
+ fun frame13(v: Vec[Int], n: Int) -> Vec[Int] =
12
+ for i := range n
13
+ v = v.set(i, v.at(i) + 1)
14
+ v
15
+
16
+ fun run13(n: Int, frames: Int) -> Int =
17
+ v := Vec[Int]()
18
+ for i := range n
19
+ v = v.push(0)
20
+ for f := range frames
21
+ v = frame13(v, n)
22
+ v.at(0) + v.at(n - 1)
23
+
24
+ fun main(io: Io) =
25
+ t13 := Time.now(io).toMillis()
26
+ r13 := run13(20000, 200)
27
+ u13 := Time.now(io).toMillis()
28
+ io.printLn("vec_frameslocal shape 13 result={r13} ms={u13 - t13}")
29
+
30
+ test "vec frameslocal"
31
+ assert run13(50, 5) == 10
tests/fbip/vec_framesmut.plum ADDED
@@ -0,0 +1,34 @@
1
+ import std/Int
2
+ import std/Str
3
+ import std/Float
4
+ import std/Time
5
+ import std/Io
6
+ import std/game/Vec2
7
+ import tests/fbip/vec
8
+
9
+ # Shape 11c: the per-frame update on a Vec in a `mut` field, `w.items = w.items.set(i, ..)`.
10
+
11
+ struct World14(mut items: Vec[Int], mut frame: Int)
12
+
13
+ fun frame14(w: World14, n: Int) -> Unit =
14
+ for i := range n
15
+ w.items = w.items.set(i, w.items.at(i) + 1)
16
+ w.frame = w.frame + 1
17
+
18
+ fun run14(n: Int, frames: Int) -> Int =
19
+ v := Vec[Int]()
20
+ for i := range n
21
+ v = v.push(0)
22
+ w := World14(items: v, frame: 0)
23
+ for f := range frames
24
+ frame14(w, n)
25
+ w.items.at(0) + w.items.at(n - 1) + w.frame
26
+
27
+ fun main(io: Io) =
28
+ t14 := Time.now(io).toMillis()
29
+ r14 := run14(20000, 200)
30
+ u14 := Time.now(io).toMillis()
31
+ io.printLn("vec_framesmut shape 14 result={r14} ms={u14 - t14}")
32
+
33
+ test "vec framesmut"
34
+ assert run14(50, 5) == 15
tests/fbip/vec_generic.plum ADDED
@@ -0,0 +1,27 @@
1
+ import std/Int
2
+ import std/Str
3
+ import std/Float
4
+ import std/Time
5
+ import std/Io
6
+ import std/game/Vec2
7
+ import tests/fbip/vec
8
+
9
+ # Shape 5: Vec[Int] and Vec[Vec2] in one program.
10
+
11
+ fun run5(n: Int) -> Int =
12
+ a := Vec[Int]()
13
+ b := Vec[Vec2]()
14
+ for i := range n
15
+ a = a.push(i)
16
+ b = b.push(Vec2(x: Float(i), y: 1.0))
17
+ r := a.length() + b.length() + a.at(n - 1)
18
+ if b.at(n - 1).x == Float(n - 1) then r else 0 - 1
19
+
20
+ fun main(io: Io) =
21
+ t5 := Time.now(io).toMillis()
22
+ r5 := run5(100000)
23
+ u5 := Time.now(io).toMillis()
24
+ io.printLn("vec_generic shape 5 result={r5} ms={u5 - t5}")
25
+
26
+ test "vec generic"
27
+ assert run5(1000) == 2999
tests/fbip/vec_helper.plum ADDED
@@ -0,0 +1,26 @@
1
+ import std/Int
2
+ import std/Str
3
+ import std/Float
4
+ import std/Time
5
+ import std/Io
6
+ import std/game/Vec2
7
+ import tests/fbip/vec
8
+
9
+ # Shape 4: a helper that takes the Vec and returns it, `v = grow(v, i)`.
10
+
11
+ fun grow4(v: Vec[Int], x: Int) -> Vec[Int] = v.push(x * 2)
12
+
13
+ fun run4(n: Int) -> Int =
14
+ v := Vec[Int]()
15
+ for i := range n
16
+ v = grow4(v, i)
17
+ v.length() + v.at(n - 1)
18
+
19
+ fun main(io: Io) =
20
+ t4 := Time.now(io).toMillis()
21
+ r4 := run4(100000)
22
+ u4 := Time.now(io).toMillis()
23
+ io.printLn("vec_helper shape 4 result={r4} ms={u4 - t4}")
24
+
25
+ test "vec helper"
26
+ assert run4(1000) == 2998
tests/fbip/vec_listoflists.plum ADDED
@@ -0,0 +1,39 @@
1
+ import std/Int
2
+ import std/Str
3
+ import std/Float
4
+ import std/Time
5
+ import std/Io
6
+ import std/game/Vec2
7
+ import tests/fbip/vec
8
+
9
+ # Shape 8: a Vec of Vecs. 8a builds rows and pushes them; 8b pushes onto a row stored in the outer Vec (`rows = rows.set(j, rows.at(j).push(i))`).
10
+
11
+ fun run8(n: Int) -> Int =
12
+ rows := Vec[Vec[Int]]()
13
+ for r := range n / 10
14
+ row := Vec[Int]()
15
+ for c := range 10
16
+ row = row.push(r * 10 + c)
17
+ rows = rows.push(row)
18
+ rows.length() + rows.at(n / 10 - 1).at(9)
19
+
20
+ fun run8b(n: Int) -> Int =
21
+ rows := Vec[Vec[Int]]()
22
+ for r := range 10
23
+ rows = rows.push(Vec[Int]())
24
+ for i := range n
25
+ j := i % 10
26
+ rows = rows.set(j, rows.at(j).push(i))
27
+ rows.at(0).length() + rows.at(9).at(n / 10 - 1)
28
+
29
+ fun main(io: Io) =
30
+ t8 := Time.now(io).toMillis()
31
+ r8 := run8(100000) + run8b(2000)
32
+ u8 := Time.now(io).toMillis()
33
+ io.printLn("vec_listoflists shape 8 result={r8} ms={u8 - t8}")
34
+
35
+ test "vec listoflists"
36
+ assert run8(1000) == 1099
37
+
38
+ test "vec listoflists (2)"
39
+ assert run8b(1000) == 100 + 999
tests/fbip/vec_local.plum ADDED
@@ -0,0 +1,24 @@
1
+ import std/Int
2
+ import std/Str
3
+ import std/Float
4
+ import std/Time
5
+ import std/Io
6
+ import std/game/Vec2
7
+ import tests/fbip/vec
8
+
9
+ # Shape 1: a Vec in a local, `v = v.push(i)` in a loop.
10
+
11
+ fun run1(n: Int) -> Int =
12
+ v := Vec[Int]()
13
+ for i := range n
14
+ v = v.push(i)
15
+ v.length() + v.at(n - 1) + v.at(n / 2)
16
+
17
+ fun main(io: Io) =
18
+ t1 := Time.now(io).toMillis()
19
+ r1 := run1(100000)
20
+ u1 := Time.now(io).toMillis()
21
+ io.printLn("vec_local shape 1 result={r1} ms={u1 - t1}")
22
+
23
+ test "vec local"
24
+ assert run1(1000) == 2499
tests/fbip/vec_mutfield.plum ADDED
@@ -0,0 +1,29 @@
1
+ import std/Int
2
+ import std/Str
3
+ import std/Float
4
+ import std/Time
5
+ import std/Io
6
+ import std/game/Vec2
7
+ import tests/fbip/vec
8
+
9
+ # Shape 2: a `mut` field of a struct, `self.items = self.items.push(x)` in a method.
10
+
11
+ struct Bag2(mut items: Vec[Int], mut count: Int) =
12
+ fun add(self, x: Int) -> Unit =
13
+ self.items = self.items.push(x)
14
+ self.count = self.count + 1
15
+
16
+ fun run2(n: Int) -> Int =
17
+ b := Bag2(items: Vec[Int](), count: 0)
18
+ for i := range n
19
+ b.add(i)
20
+ b.items.length() + b.items.at(n - 1) + b.count
21
+
22
+ fun main(io: Io) =
23
+ t2 := Time.now(io).toMillis()
24
+ r2 := run2(100000)
25
+ u2 := Time.now(io).toMillis()
26
+ io.printLn("vec_mutfield shape 2 result={r2} ms={u2 - t2}")
27
+
28
+ test "vec mutfield"
29
+ assert run2(1000) == 2999
tests/fbip/vec_nested.plum ADDED
@@ -0,0 +1,27 @@
1
+ import std/Int
2
+ import std/Str
3
+ import std/Float
4
+ import std/Time
5
+ import std/Io
6
+ import std/game/Vec2
7
+ import tests/fbip/vec
8
+
9
+ # Shape 3: a Vec in a field of a nested struct, `o.inner.items = o.inner.items.push(i)`.
10
+
11
+ struct Inner3(mut items: Vec[Int])
12
+ struct Outer3(inner: Inner3, tag: Int)
13
+
14
+ fun run3(n: Int) -> Int =
15
+ o := Outer3(inner: Inner3(items: Vec[Int]()), tag: 5)
16
+ for i := range n
17
+ o.inner.items = o.inner.items.push(i)
18
+ o.inner.items.length() + o.inner.items.at(n - 1) + o.tag
19
+
20
+ fun main(io: Io) =
21
+ t3 := Time.now(io).toMillis()
22
+ r3 := run3(100000)
23
+ u3 := Time.now(io).toMillis()
24
+ io.printLn("vec_nested shape 3 result={r3} ms={u3 - t3}")
25
+
26
+ test "vec nested"
27
+ assert run3(1000) == 2004
tests/fbip/vec_readbetween.plum ADDED
@@ -0,0 +1,25 @@
1
+ import std/Int
2
+ import std/Str
3
+ import std/Float
4
+ import std/Time
5
+ import std/Io
6
+ import std/game/Vec2
7
+ import tests/fbip/vec
8
+
9
+ # Shape 6: the length is read between updates, `n := v.length(); v = v.push(n)`.
10
+
11
+ fun run6(n: Int) -> Int =
12
+ v := Vec[Int]()
13
+ for i := range n
14
+ len := v.length()
15
+ v = v.push(len)
16
+ v.length() + v.at(n - 1)
17
+
18
+ fun main(io: Io) =
19
+ t6 := Time.now(io).toMillis()
20
+ r6 := run6(100000)
21
+ u6 := Time.now(io).toMillis()
22
+ io.printLn("vec_readbetween shape 6 result={r6} ms={u6 - t6}")
23
+
24
+ test "vec readbetween"
25
+ assert run6(1000) == 1999
tests/fbip/vec_returned.plum ADDED
@@ -0,0 +1,30 @@
1
+ import std/Int
2
+ import std/Str
3
+ import std/Float
4
+ import std/Time
5
+ import std/Io
6
+ import std/game/Vec2
7
+ import tests/fbip/vec
8
+
9
+ # Shape 9: built by one function, returned, then pushed by the caller.
10
+
11
+ fun make9(n: Int) -> Vec[Int] =
12
+ v := Vec[Int]()
13
+ for i := range n
14
+ v = v.push(i)
15
+ v
16
+
17
+ fun run9(n: Int) -> Int =
18
+ v := make9(n)
19
+ for i := range n
20
+ v = v.push(i + n)
21
+ v.length() + v.at(2 * n - 1)
22
+
23
+ fun main(io: Io) =
24
+ t9 := Time.now(io).toMillis()
25
+ r9 := run9(50000)
26
+ u9 := Time.now(io).toMillis()
27
+ io.printLn("vec_returned shape 9 result={r9} ms={u9 - t9}")
28
+
29
+ test "vec returned"
30
+ assert run9(500) == 1000 + 999
tests/fbip/vec_returned2.plum ADDED
@@ -0,0 +1,31 @@
1
+ import std/Int
2
+ import std/Str
3
+ import std/Float
4
+ import std/Time
5
+ import std/Io
6
+ import std/game/Vec2
7
+ import tests/fbip/vec
8
+
9
+ # Shape 9b: a function that returns a Vec it built in a local, called twice, the result pushed by the caller.
10
+
11
+ fun make15(n: Int) -> Vec[Int] =
12
+ v := Vec[Int]()
13
+ for i := range n
14
+ v = v.push(i)
15
+ v
16
+
17
+ fun run15(n: Int) -> Int =
18
+ v := make15(n)
19
+ w := Vec[Int]()
20
+ for i := range n
21
+ w = w.push(i)
22
+ v.length() + w.length()
23
+
24
+ fun main(io: Io) =
25
+ t15 := Time.now(io).toMillis()
26
+ r15 := run15(100000)
27
+ u15 := Time.now(io).toMillis()
28
+ io.printLn("vec_returned2 shape 15 result={r15} ms={u15 - t15}")
29
+
30
+ test "vec returned2"
31
+ assert run15(500) == 1000
tests/fbip/vec_shared.plum ADDED
@@ -0,0 +1,31 @@
1
+ import std/Int
2
+ import std/Str
3
+ import std/Float
4
+ import std/Time
5
+ import std/Io
6
+ import std/game/Vec2
7
+ import tests/fbip/vec
8
+
9
+ # Shape 12: one value shared on purpose. Every holder keeps its own contents; this is a correctness test.
10
+
11
+ fun run12() -> Int =
12
+ a := Vec[Int]()
13
+ for i := range 20
14
+ a = a.push(i)
15
+ b := a.push(100)
16
+ c := a.push(200)
17
+ d := a.set(0, 7)
18
+ e := b.set(1, 8)
19
+ ok := a.length() == 20 && a.at(0) == 0 && a.at(1) == 1
20
+ ok2 := b.length() == 21 && b.at(20) == 100 && b.at(1) == 1 && c.length() == 21 && c.at(20) == 200
21
+ ok3 := d.at(0) == 7 && e.at(1) == 8 && e.at(20) == 100 && b.at(0) == 0
22
+ if ok && ok2 && ok3 then 1 else 0
23
+
24
+ fun main(io: Io) =
25
+ t12 := Time.now(io).toMillis()
26
+ r12 := run12()
27
+ u12 := Time.now(io).toMillis()
28
+ io.printLn("vec_shared shape 12 result={r12} ms={u12 - t12}")
29
+
30
+ test "vec shared"
31
+ assert run12() == 1
tests/fbip/vec_statespread.plum ADDED
@@ -0,0 +1,29 @@
1
+ import std/Int
2
+ import std/Str
3
+ import std/Float
4
+ import std/Time
5
+ import std/Io
6
+ import std/game/Vec2
7
+ import tests/fbip/vec
8
+
9
+ # Shape 10: an immutable state struct, `State(..s, items: s.items.push(x))`, in a step function.
10
+
11
+ struct State10(items: Vec[Int], tick: Int)
12
+
13
+ fun step10(s: State10, x: Int) -> State10 =
14
+ State10(..s, items: s.items.push(x), tick: s.tick + 1)
15
+
16
+ fun run10(n: Int) -> Int =
17
+ s := State10(items: Vec[Int](), tick: 0)
18
+ for i := range n
19
+ s = step10(s, i)
20
+ s.items.length() + s.items.at(n - 1) + s.tick
21
+
22
+ fun main(io: Io) =
23
+ t10 := Time.now(io).toMillis()
24
+ r10 := run10(100000)
25
+ u10 := Time.now(io).toMillis()
26
+ io.printLn("vec_statespread shape 10 result={r10} ms={u10 - t10}")
27
+
28
+ test "vec statespread"
29
+ assert run10(1000) == 2999
tests/fbip/vec_twolists.plum ADDED
@@ -0,0 +1,27 @@
1
+ import std/Int
2
+ import std/Str
3
+ import std/Float
4
+ import std/Time
5
+ import std/Io
6
+ import std/game/Vec2
7
+ import tests/fbip/vec
8
+
9
+ # Shape 7: two Vecs in one struct, both pushed.
10
+
11
+ struct Pair7(mut xs: Vec[Int], mut ys: Vec[Int])
12
+
13
+ fun run7(n: Int) -> Int =
14
+ p := Pair7(xs: Vec[Int](), ys: Vec[Int]())
15
+ for i := range n
16
+ p.xs = p.xs.push(i)
17
+ p.ys = p.ys.push(i * 2)
18
+ p.xs.length() + p.ys.length() + p.xs.at(n - 1) + p.ys.at(n - 1)
19
+
20
+ fun main(io: Io) =
21
+ t7 := Time.now(io).toMillis()
22
+ r7 := run7(100000)
23
+ u7 := Time.now(io).toMillis()
24
+ io.printLn("vec_twolists shape 7 result={r7} ms={u7 - t7}")
25
+
26
+ test "vec twolists"
27
+ assert run7(1000) == 4997
tests/fbip/vec_unique_names.plum ADDED
@@ -0,0 +1,178 @@
1
+ import std/Int
2
+ import std/Str
3
+ import std/Float
4
+ import std/Time
5
+ import std/Io
6
+ import std/game/Vec2
7
+ import tests/fbip/uvec
8
+
9
+ # The shapes that are proved in place, in one program, with names no std type uses (`udata`, `upush`, ...). With `data`
10
+ # and `push` (vec_combined.plum) the same code copies on every push.
11
+
12
+ # Shape 1: a Vec in a local, `v = v.push(i)` in a loop.
13
+
14
+ fun run1(n: Int) -> Int =
15
+ v := UVec[Int]()
16
+ for i := range n
17
+ v = v.upush(i)
18
+ v.ulen() + v.uat(n - 1) + v.uat(n / 2)
19
+
20
+ # Shape 2: a `mut` field of a struct, `self.items = self.items.push(x)` in a method.
21
+
22
+ struct Bag2(mut items: UVec[Int], mut count: Int) =
23
+ fun add(self, x: Int) -> Unit =
24
+ self.items = self.items.upush(x)
25
+ self.count = self.count + 1
26
+
27
+ fun run2(n: Int) -> Int =
28
+ b := Bag2(items: UVec[Int](), count: 0)
29
+ for i := range n
30
+ b.add(i)
31
+ b.items.ulen() + b.items.uat(n - 1) + b.count
32
+
33
+ # Shape 3: a Vec in a field of a nested struct, `o.inner.items = o.inner.items.push(i)`.
34
+
35
+ struct Inner3(mut items: UVec[Int])
36
+ struct Outer3(inner: Inner3, tag: Int)
37
+
38
+ fun run3(n: Int) -> Int =
39
+ o := Outer3(inner: Inner3(items: UVec[Int]()), tag: 5)
40
+ for i := range n
41
+ o.inner.items = o.inner.items.upush(i)
42
+ o.inner.items.ulen() + o.inner.items.uat(n - 1) + o.tag
43
+
44
+ # Shape 4: a helper that takes the Vec and returns it, `v = grow(v, i)`.
45
+
46
+ fun grow4(v: UVec[Int], x: Int) -> UVec[Int] = v.upush(x * 2)
47
+
48
+ fun run4(n: Int) -> Int =
49
+ v := UVec[Int]()
50
+ for i := range n
51
+ v = grow4(v, i)
52
+ v.ulen() + v.uat(n - 1)
53
+
54
+ # Shape 5: Vec[Int] and Vec[Vec2] in one program.
55
+
56
+ fun run5(n: Int) -> Int =
57
+ a := UVec[Int]()
58
+ b := UVec[Vec2]()
59
+ for i := range n
60
+ a = a.upush(i)
61
+ b = b.upush(Vec2(x: Float(i), y: 1.0))
62
+ r := a.ulen() + b.ulen() + a.uat(n - 1)
63
+ if b.uat(n - 1).x == Float(n - 1) then r else 0 - 1
64
+
65
+ # Shape 6: the length is read between updates, `n := v.length(); v = v.push(n)`.
66
+
67
+ fun run6(n: Int) -> Int =
68
+ v := UVec[Int]()
69
+ for i := range n
70
+ len := v.ulen()
71
+ v = v.upush(len)
72
+ v.ulen() + v.uat(n - 1)
73
+
74
+ # Shape 7: two Vecs in one struct, both pushed.
75
+
76
+ struct Pair7(mut xs: UVec[Int], mut ys: UVec[Int])
77
+
78
+ fun run7(n: Int) -> Int =
79
+ p := Pair7(xs: UVec[Int](), ys: UVec[Int]())
80
+ for i := range n
81
+ p.xs = p.xs.upush(i)
82
+ p.ys = p.ys.upush(i * 2)
83
+ p.xs.ulen() + p.ys.ulen() + p.xs.uat(n - 1) + p.ys.uat(n - 1)
84
+
85
+ # Shape 11b: the per-frame update on a Vec held in a local and handed through a function, no struct around it.
86
+
87
+ fun frame13(v: UVec[Int], n: Int) -> UVec[Int] =
88
+ for i := range n
89
+ v = v.uset(i, v.uat(i) + 1)
90
+ v
91
+
92
+ fun run13(n: Int, frames: Int) -> Int =
93
+ v := UVec[Int]()
94
+ for i := range n
95
+ v = v.upush(0)
96
+ for f := range frames
97
+ v = frame13(v, n)
98
+ v.uat(0) + v.uat(n - 1)
99
+
100
+ # Shape 9b: a function that returns a Vec it built in a local, called twice, the result pushed by the caller.
101
+
102
+ fun make15(n: Int) -> UVec[Int] =
103
+ v := UVec[Int]()
104
+ for i := range n
105
+ v = v.upush(i)
106
+ v
107
+
108
+ fun run15(n: Int) -> Int =
109
+ v := make15(n)
110
+ w := UVec[Int]()
111
+ for i := range n
112
+ w = w.upush(i)
113
+ v.ulen() + w.ulen()
114
+
115
+ fun main(io: Io) =
116
+ t1 := Time.now(io).toMillis()
117
+ r1 := run1(100000)
118
+ u1 := Time.now(io).toMillis()
119
+ io.printLn("uvec_combined shape 1 result={r1} ms={u1 - t1}")
120
+ t2 := Time.now(io).toMillis()
121
+ r2 := run2(100000)
122
+ u2 := Time.now(io).toMillis()
123
+ io.printLn("uvec_combined shape 2 result={r2} ms={u2 - t2}")
124
+ t3 := Time.now(io).toMillis()
125
+ r3 := run3(100000)
126
+ u3 := Time.now(io).toMillis()
127
+ io.printLn("uvec_combined shape 3 result={r3} ms={u3 - t3}")
128
+ t4 := Time.now(io).toMillis()
129
+ r4 := run4(100000)
130
+ u4 := Time.now(io).toMillis()
131
+ io.printLn("uvec_combined shape 4 result={r4} ms={u4 - t4}")
132
+ t5 := Time.now(io).toMillis()
133
+ r5 := run5(100000)
134
+ u5 := Time.now(io).toMillis()
135
+ io.printLn("uvec_combined shape 5 result={r5} ms={u5 - t5}")
136
+ t6 := Time.now(io).toMillis()
137
+ r6 := run6(100000)
138
+ u6 := Time.now(io).toMillis()
139
+ io.printLn("uvec_combined shape 6 result={r6} ms={u6 - t6}")
140
+ t7 := Time.now(io).toMillis()
141
+ r7 := run7(100000)
142
+ u7 := Time.now(io).toMillis()
143
+ io.printLn("uvec_combined shape 7 result={r7} ms={u7 - t7}")
144
+ t13 := Time.now(io).toMillis()
145
+ r13 := run13(20000, 200)
146
+ u13 := Time.now(io).toMillis()
147
+ io.printLn("uvec_combined shape 13 result={r13} ms={u13 - t13}")
148
+ t15 := Time.now(io).toMillis()
149
+ r15 := run15(100000)
150
+ u15 := Time.now(io).toMillis()
151
+ io.printLn("uvec_combined shape 15 result={r15} ms={u15 - t15}")
152
+
153
+ test "vec local"
154
+ assert run1(1000) == 2499
155
+
156
+ test "vec mutfield"
157
+ assert run2(1000) == 2999
158
+
159
+ test "vec nested"
160
+ assert run3(1000) == 2004
161
+
162
+ test "vec helper"
163
+ assert run4(1000) == 2998
164
+
165
+ test "vec generic"
166
+ assert run5(1000) == 2999
167
+
168
+ test "vec readbetween"
169
+ assert run6(1000) == 1999
170
+
171
+ test "vec twolists"
172
+ assert run7(1000) == 4997
173
+
174
+ test "vec frameslocal"
175
+ assert run13(50, 5) == 10
176
+
177
+ test "vec returned2"
178
+ assert run15(500) == 1000