plum

#treesitter#compiler#wasm

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

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


e7acf3c — Peter John 2026-11-06T14:51:56+05:30
Simpler language T1: baseline benchmark of master
docs/plans/2026-11-06-simpler-language.benchmark.md ADDED
@@ -0,0 +1,110 @@
1
+ # Benchmark: baseline on master
2
+
3
+ Taken 2026-11-06 on `master` at 33fe838, before any change of the `simpler-language` branch. The tree is exported to `.superlite/baseline-master/` (`git archive master`, not a worktree) and built there with its own `target/`. Every number below comes from that tree's own binaries:
4
+
5
+ - `.superlite/baseline-master/target/release/plum` (release)
6
+ - `.superlite/baseline-master/compiler/game/web-test/target/release/plum-web-test` (release)
7
+
8
+ Machine: Apple M3 Pro, macOS. Later tasks compare against this file.
9
+
10
+ ## How the games were run
11
+
12
+ Command, from the baseline root: `plum-web-test compiler/game/web-test/fixtures/<name>.plum --measure 5 --json`. Headless Chromium with software WebGPU (backend `gpu-webgpu`), 60 fps vsync-capped, about 298 frames measured. The same command and shape as `docs/plans/2026-11-05-game-engine.results.md`.
13
+
14
+ `plum-web-test` runs `cargo build -p plum-cli` and then `target/debug/plum build`, so the game wasm was built by the baseline's debug `plum`. The compiler output does not depend on the build profile. Compile times below use the release `plum`.
15
+
16
+ ## Which fixtures draw what
17
+
18
+ Fixtures are in `compiler/game/web-test/fixtures/`.
19
+
20
+ | fixture | scene style | scene |
21
+ |---|---|---|
22
+ | `stress` | none, immediate drawing on `Game` | A: 5000 sprites, 500 rects, 200 circles, 50 texts. Rects and circles drawn as shapes. |
23
+ | `stress_scene` | imperative (`std/game/imperative/Scene`, actors that embed `Actor`) | A: 5000 sprites in 50 rotating groups, 500 rect actors, 200 circle actors, 50 text actors |
24
+ | `stress_pure_scene` | pure scene graph (`std/game/pure/PureScene`), not imperative and not state-and-view | A, but rects and circles are sprites of a 1x1 white texture |
25
+ | `stress_state_view` | state and view (`std/game/functional/FunctionalScene`) | A as `stress_pure_scene`, plus 200 animated sprites (pulsers) and 20 UI buttons |
26
+ | `stress_state_view_deep` | state and view | like `stress_state_view`, but all 5000 dancers in one list, so `step` recurses 5000 deep |
27
+ | `stress_world` | imperative (`std/game/imperative/Scene`) | B: 20000 sprites in one y-sorted layer, culled to the view. A different scene from all the others. |
28
+
29
+ Answers for the comparison:
30
+
31
+ - Imperative: `stress_scene` and `stress_world`. State and view: `stress_state_view` and `stress_state_view_deep`. `stress_pure_scene` is a third style.
32
+ - Same scene: `stress_scene`, `stress_pure_scene` and `stress_state_view` draw the same 5000 / 500 / 200 / 50 scene (A), but with differences. Pure and state-and-view draw rects and circles as white-texture sprites, not shapes. `stress_state_view` also has 220 extra entities (200 pulsers, 20 buttons).
33
+ - No state-and-view fixture matches `stress_world`.
34
+ - The only exact imperative to state-and-view pair is therefore `stress_scene` against `stress_state_view`, with the two differences above. A fair functional port of `stress_scene` and of `stress_world` has to be written in T14.
35
+
36
+ ## Game results
37
+
38
+ Milliseconds per frame, mean over 5 s. Load average is the 1 minute value from `uptime` at the start and end of each run. The machine was busy for the whole session (load 7 to 12), so these are noisier than the 2026-11-05 numbers (load 2 to 5).
39
+
40
+ | fixture | run | load | update | draw | render |
41
+ |---|---|---|---|---|---|
42
+ | stress | 1 | 10.4 to 11.1 | 0.013 | 0.663 | 0.321 |
43
+ | stress | 2 | 11.1 to 11.7 | 0.012 | 0.667 | 0.306 |
44
+ | stress | 3 | 11.7 to 11.7 | 0.011 | 0.678 | 0.322 |
45
+ | stress_scene | 1 | 11.7 to 11.6 | 0.074 | 1.339 | 0.332 |
46
+ | stress_scene | 2 | 11.6 to 10.9 | 0.075 | 1.368 | 0.330 |
47
+ | stress_scene | 3 | 10.9 to 10.1 | 0.072 | 1.362 | 0.339 |
48
+ | stress_world | 1 | 10.1 to 9.0 | 0.573 | 0.621 | 0.143 |
49
+ | stress_world | 2 | 9.0 to 7.4 | 0.630 | 0.635 | 0.157 |
50
+ | stress_world | 3 | 7.4 to 7.4 | 0.636 | 0.699 | 0.143 |
51
+ | stress_pure_scene | 1 | 7.4 to 8.8 | 0.227 | 1.253 | 0.239 |
52
+ | stress_pure_scene | 2 | 8.8 to 8.3 | 0.242 | 1.265 | 0.261 |
53
+ | stress_pure_scene | 3 | 8.3 to 9.1 | 0.244 | 1.243 | 0.245 |
54
+ | stress_state_view | 1 | 9.1 to 10.1 | 0.057 | 1.545 | 0.239 |
55
+ | stress_state_view | 2 | 10.1 to 10.3 | 0.068 | 1.583 | 0.262 |
56
+ | stress_state_view | 3 | 10.3 to 9.4 | 0.074 | 1.593 | 0.264 |
57
+ | stress_state_view_deep | 1 | 9.4 to 9.1 | 0.042 | 1.279 | 0.240 |
58
+ | stress_state_view_deep | 2 | 9.1 to 8.4 | 0.059 | 1.338 | 0.255 |
59
+ | stress_state_view_deep | 3 | 8.4 to 9.0 | 0.052 | 1.300 | 0.261 |
60
+
61
+ Means of the three runs:
62
+
63
+ | fixture | update | draw | render |
64
+ |---|---|---|---|
65
+ | stress | 0.012 | 0.669 | 0.316 |
66
+ | stress_scene | 0.074 | 1.356 | 0.334 |
67
+ | stress_world | 0.613 | 0.652 | 0.148 |
68
+ | stress_pure_scene | 0.238 | 1.254 | 0.248 |
69
+ | stress_state_view | 0.066 | 1.574 | 0.255 |
70
+ | stress_state_view_deep | 0.051 | 1.306 | 0.252 |
71
+
72
+ Noise:
73
+
74
+ - Every run drew 1 call per frame and held 60 fps (59.6 to 60.0), so frame time says nothing here; only the update, draw and render split does.
75
+ - Run to run spread inside one fixture: draw within about 3% (stress_world 12%), render within 5% (stress_world 9%), update within 3% for stress_scene, 10% for stress_world, 7% for stress_pure_scene, and up to 30% for stress_state_view and stress_state_view_deep (values near 0.05 ms, close to the 0.1 ms timer resolution).
76
+ - Against 2026-11-05 (load 2 to 5) draw is 0 to 8% higher and update on stress_scene 25% higher (0.0585 to 0.074), so this load adds a few percent to draw and more to the smallest update times. Treat a difference under about 10% on draw, or under 0.02 ms on update, as noise. Re-run both sides back to back before drawing a conclusion near that size.
77
+ - The final comparison should run baseline and branch alternately, not in two blocks.
78
+
79
+ ## Compile time, size, sweep, code size
80
+
81
+ Load 8.4 at the start. Release `plum compile`.
82
+
83
+ | item | value |
84
+ |---|---|
85
+ | `stress_scene.plum` compile, 3 runs | 0.54, 0.52, 0.53 s |
86
+ | `.wasm` size | stress 34249, stress_scene 93014, stress_world 69024, stress_pure_scene 67122, stress_state_view 62720, stress_state_view_deep 51765 bytes |
87
+ | wasm sha256 (first 16 hex) | stress 1255dce1ae400be7, stress_scene dd7276d2fc05aa49, stress_world e8c3995c33331bd3, stress_pure_scene 7cb024b3efb69703, stress_state_view dd75f435320dad13, stress_state_view_deep 156b109e75574f07 |
88
+ | sweep `plum test std examples tests` | 209/209 files, 961 tests, 0 failed; 25.9 s and 25.0 s wall (2 runs) |
89
+ | `std/` | 9377 lines in 40 `.plum` files |
90
+ | `compiler/` Rust | 52311 lines in all `.rs` files (35811 of them under `src/`), excluding `target/` |
91
+
92
+ `plum compile` has no `--no-fusion`; only `run` and `build` do, so no no-fusion size was taken.
93
+
94
+ ## Loop probes
95
+
96
+ File: `scratchpad/loops_fixed.plum`, a copy of `loops2.plum` with the timer arguments the right way round (`t1.since(t0)`), so times are positive. 50,000,000 iterations each; `plum run` (wasmtime), release `plum`. Load average at the runs: 10.4 (1 minute).
97
+
98
+ Five probes: `for` with a `total =` accumulator, `while`, a tail-recursive `foldTo` with a closure, `repeatUntil` over a struct state, and `findFirst` (pure, returns `Option[Int]`).
99
+
100
+ | probe | default, 3 runs (ms) | `--no-fusion`, 3 runs (ms) | answer |
101
+ |---|---|---|---|
102
+ | for | 13, 14, 14 | 15, 14, 14 | 2499999950000000 |
103
+ | while | 14, 14, 14 | 13, 15, 14 | same |
104
+ | fold | 14, 14, 14 | 14, 14, 13 | same |
105
+ | until | 14, 14, 14 | 14, 14, 15 | same |
106
+ | find | 0, 0, 0 | 14, 14, 14 | 49999999 |
107
+
108
+ - Under the default build, `find` reads 0 ms because fusion moves the pure `findFirst` call past the timer, to where its answer is printed. The answer is printed in the fixed file, and with `--no-fusion` the call runs at the right place and takes 14 ms, the same as the others. Use the `--no-fusion` figure for `find`.
109
+ - All five forms take 13 to 15 ms for 50 million iterations (about 0.28 ns each), so the compiler flattens `for`, `while`, the closure fold and the struct state loop to the same code on this machine. Timer resolution is 1 ms. The earlier "loops +75%" figure does not show up in these probes.
110
+ - Spread is 1 to 2 ms, which is about 10%; the forms cannot be told apart.