plum

#treesitter#compiler#wasm

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

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


34b61a0 — Peter John 2026-10-26T10:45:12+05:30
docs(plans): game plan becomes browser-first simplified KaM port
Files changed (1) hide show
  1. docs/plans/2026-10-26-game-engine.md +141 -93
docs/plans/2026-10-26-game-engine.md CHANGED
@@ -1,121 +1,169 @@
1
- # Plum for games (macroquad host, sized numbers, value structs, SIMD)
1
+ # Plum for games: browser-first macroquad host, port of a simplified KaM
2
2
 
3
- **Status:** draft. Planned only, not dispatched, because the user has more ideas to fold in first.
4
- **Superseded in part:** Design item 1 with T1 (sized numbers), and T4 (operator traits), now live in `2026-10-26-zig-generics-and-numbers.md`. That plan has the native wasm value types i32/i64/f32/f64 with i32/f32 literal defaults (lowercase; no U8/i32 — unsigned via explicit ops, packed bytes via `Bytes`), and no traits: operators are duck-typed methods, and enums replace trait types.
5
- **Goal:** Let Plum write game logic and rendering, running on a macroquad-based host. Natively first, then in the browser.
6
- **Architecture:** Plum source → checker → erased wasm-gc codegen → `plum-game`, a Rust crate built on macroquad and wasmtime that drives `init`/`update`/`draw` each frame.
7
- **Tech stack:** Rust, macroquad, wasmtime (GC, multi-value and SIMD enabled), wasm-encoder.
3
+ **Status:** draft. Planned only and not run; the user has more ideas to fold in first.
4
+ **Depends on:** `2026-10-26-zig-generics-and-numbers.md`, which supplies the native `i32 i64 f32 f64 bool` types with i32/f32 literal defaults, duck-typed per-type-copy generics, no traits (enums plus duck-typed operator methods), and `Bytes`. That plan runs first.
5
+ **Goal:** Port `../kam_macroquad`, a 98k-line Rust/macroquad remake of Knights and Merchants that already runs in the browser via wasm, to Plum as a **simplified** game. There is no mission scripting (rhai) and the rules are simpler, but animations and sounds stay identical. It runs in the browser first.
6
+ **Architecture:** the Rust/macroquad host keeps asset decoding, audio and the window, compiled to wasm32 for the web or native. The Plum wasm-gc module owns game state, rules, pathfinding, AI, animation timing and per-frame draw calls. On the web the two modules talk through JS (miniquad plugin), and natively through wasmtime.
8
7
  **Detail:** tests
9
8
 
10
9
  ## Design
11
10
 
12
11
  What the user decided:
13
- - macroquad is the host engine, and Plum owns the game logic and rendering.
14
- - Add sized primitives. `Float` stays f64 for now (the alternative was making `Float` f32).
15
- - Value types come from a new `struct` syntax.
16
- - Vectors and simd128.
17
-
18
- 1. **Sized numbers.** `f32`, `i32` and `i32` are new built-in primitives (wasm `f32`/`i32`). `Byte` stays as u8, `Int` as i64 and `Float` as f64.
19
- - Ruling: `Float` is not changed to f32 (because Time millis, Json and parseFloat need f64; cost: game code writes `f32` explicitly).
20
- - Arithmetic, comparisons and `toStr`/`parse*` exist per type, and explicit casts are `f32(x)`, `i32(x)` and so on.
21
- - Each type boxes at generic edges with its own typeId (8, 9 and 10).
22
- 2. **Contextual literal typing.** A numeric literal takes its type from the expected type (annotation, param, field, operand) where that type is a sized type. So `x: f32 = 1.5`, `drawRect(10, 20, 32, 32)` and `v.x * 0.5` need no casts. With no expected type, literals default to `Int`/`Float` as today. A literal that doesn't fit the target (e.g. `i32 = 3_000_000_000`) is a compile error.
23
- 3. **Value structs.** `struct Vec2(x: f32, y: f32)` has the same body syntax as a class, but has value semantics.
24
- - In locals and params a struct is flattened into one wasm local/param per field, and it is returned with multi-value results.
25
- - Assigning copies it. A field can be assigned in place (`v.x += 1.0`), which mutates only that binding.
26
- - It is boxed (as a GC struct with a typeId) only when it flows into a `TParam`, trait slot, `List` element or closure capture.
27
- - `==` compares fields. Methods take `self` by value. Structs can't be recursive.
28
- 4. **Operator traits.** `trait Add = add(other: Self) -> Self` (plus `Sub`, `Mul`, `Div`, `Neg` and `Eq`) maps `+ - * / -x ==` on non-primitive operands to those methods. Calls are resolved statically and are never dyn dispatched. `Mul` may also take a scalar RHS (`v * 2.0`) through an overload on the param type.
29
- 5. **SIMD.** `f32x4` is a primitive (wasm `v128`) with lane-wise `+ - * /`, `f32x4.splat(x)`, `.x/.y/.z/.w` lane access, `dot`, `min`, `max` and `shuffle`. `plum-std/Math.plum` builds `Vec4`, `Quat` and `Mat4` (four `f32x4` columns) on top of it. `Vec2`/`Vec3` stay scalar structs, plus `lerp`, `clamp`, `perspective`, `lookAt` and `ortho`.
30
- 6. **`plum-game` host (native).** A new crate built on macroquad and wasmtime.
31
- - It loads `game.wasm`, calls the exported `init() -> Game` once, keeps the returned ref (a wasmtime `Rooted`), and every frame calls `update(g, dt: f32)` and `draw(g)`.
32
- - Ruling: no global mutable state and no new language feature (because the state lives in the host; cost: the `Game` ref is passed on every call).
33
- - The extern API lives in `plum-std/Game.plum`:
34
- - `clear(c: Color)`, `drawRect(x, y, w, h: f32, c: Color)`, `drawCircle`, `drawLine`, `drawText(s: Str, x, y, size: f32, c: Color)`;
35
- - `loadTexture(path: Str) -> i32` (preloaded by the host before `init`), `drawTexture(t: i32, x, y: f32, c: Color)`;
36
- - `isKeyDown(k: Key) -> Bool`, `isKeyPressed`, `mousePos() -> Vec2`, `isMouseDown`, `screenSize() -> Vec2`;
37
- - `playSound(s: i32)`, `setCamera2D(target: Vec2, zoom: f32)`.
38
- - Structs cross the boundary flattened, so `Color` is 4 f32 params. `Key` is a discriminant enum whose values match macroquad's `KeyCode`.
39
- - `plum game <file>` is a CLI subcommand that compiles the file and runs it in the host.
40
- 7. **Custom rendering (v1).**
41
- - `drawMesh(vertices: List[Vertex], indices: List[i32], texture: i32)`. The host copies the data per call.
42
- - `loadMaterial(vertexGlsl: Str, fragmentGlsl: Str) -> i32` and `useMaterial(m)` wrap macroquad's `Material`.
43
- - Zero-copy linear-memory buffers are deferred until profiling shows a need.
44
- 8. **Browser host (later).** The JS glue instantiates Plum's wasm-gc module in the browser and forwards its imports to macroquad's own wasm32 build through JS. It is the same extern API, so game code doesn't change.
45
- 9. **Out of scope:** WebGPU, `StructArray` flat storage, Plum→shader compilation, async and audio streaming.
12
+ - macroquad is the host, and Plum handles game logic plus rendering.
13
+ - `struct` value types and simd128 are wanted.
14
+ - Port KaM, with no scripting engine and simpler rules but the same animations and sounds.
15
+ - It must work in the browser, as the Rust version does today.
16
+
17
+ Survey of `../kam_macroquad`:
18
+ - 98k LOC, 356 structs, 180 enums, 864 `match`.
19
+ - ~1.9k `&mut self` methods, 967 tests, ~350 macroquad calls.
20
+ - Collections: `Vec` ×997, `Option` ×814, `HashMap`/`Set`/`BTree`/`VecDeque`/`BinaryHeap` ×145.
21
+ - Closures and iterator chains ×~3.7k each.
22
+ - Numbers: `i32` ×5.2k, `f32` ×1.8k, `u8`/`u16`/`u32`/`usize` ×5.7k (mostly binary asset parsing).
23
+ - Traits are small: 7 traits, 63 impls, 40 `dyn`.
24
+ - The asset side uses flate2, postcard, RON and serde.
25
+
26
+ 1. **Split of responsibilities.** Ruling: the host keeps asset decoding (`.rx` sprites/atlases, `kamdat`, flate2, map files), the serde/postcard/RON formats and audio, because that removes almost all the unsigned/binary-parsing and serialization code from the port. The cost is that the host stays Rust and is game-specific.
27
+ - The host exposes **handles**: a sprite is `(atlas: i32, rect index: i32)`, and sounds and maps are `i32` ids. Animations stay identical because Plum gets the same frame tables, read through host imports.
28
+ 2. **Browser host first.**
29
+ - `plum-game` is a Rust crate on macroquad. For the web it compiles to wasm32 with macroquad's JS loader.
30
+ - A `plum_bridge.js` miniquad plugin instantiates Plum's wasm-gc module, wires Plum's `extern fun` imports to functions exported from the macroquad wasm, and has macroquad's loop call Plum's `init`, `update(g, dt: f32)` and `draw(g)`.
31
+ - The native build runs the same Plum module under wasmtime (GC, multi-value and SIMD on), with the same extern API.
32
+ - Ruling: game state lives in the Plum `Game` ref, held by the host and passed to every call, so no mutable globals are needed.
33
+ 3. **Extern API (`plum-std/Game.plum`).**
34
+ - Window: `screenSize() -> Vec2` and `frameTime() -> f32`.
35
+ - Drawing: `clear(c)`, `drawSprite(atlas, rect, x, y: f32, flipX: bool, tint: Color)`, `drawRect`, `drawLine`, `drawText(s: Str, x, y, size: f32, c)`, `setCamera2D(target: Vec2, zoom: f32)` and `resetCamera()`.
36
+ - Input: `isKeyDown(k: Key)`, `isKeyPressed`, `mousePos() -> Vec2`, `isMouseDown(b: MouseButton)` and `mouseWheel() -> f32`.
37
+ - Audio: `playSound(id: i32, x, y: f32)` (positional, as in KaM), `playMusic(id)` and `stopMusic()`.
38
+ - Assets, as host lookups by name: `atlasId`, `animFrames(unitKind, action, dir) -> Bytes` (packed frame indices) and `animStep`, `loadMap(name) -> MapData`, which copies terrain into Plum `Array[i32]`s once.
39
+ - `Color` and `Vec2` are structs flattened across the boundary. `Key` and `MouseButton` are enums matching macroquad's codes.
40
+ 4. **Language additions this port needs** (beyond the Zig-generics plan and items 5–7 below):
41
+ - **Unboxed typed arrays.** With copies per type, `Array[i32]` and `Array[f32]` lower to native `array (mut i32)` and `array (mut f32)`. These are for tile grids, fog of war, passability and path costs. Add `Array.filled(n, v)` and a 2D index helper.
42
+ - **Collections in std:** `Set[T]`, `Deque[T]` (ring buffer) and `Heap[T]` (binary min-heap with a key fn, for A*), all Plum code over `Array`.
43
+ - **Constants:** large top-level constant tables must parse and be usable in expressions. A probe file with `LIMIT = 10` followed by a `type` failed to parse (cause not yet confirmed), and the notes record a SCREAMING_CASE constant gotcha. The fix needs tests with 1k-entry tables.
44
+ - **Seeded RNG** (`Random(seed: i64)` with xorshift/PCG) for deterministic AI and simulation.
45
+ - Lazy iteration is deferred. Hot loops use `for … range`, and it's revisited only if profiling shows `map`/`filter` allocation in `update`.
46
+ 5. **Value structs** (`struct Vec2(x: f32, y: f32)`, `Color`, `TilePos(x: i32, y: i32)`): flattened locals and params, multi-value returns, copy semantics and in-place field assignment. They are boxed only inside `List`/`Map`/`Try`, and by then they belong to a generic copy, so a `List[Vec2]` copy stores them as GC structs.
47
+ 6. **Duck-typed operators** on structs (`add`/`sub`/`mul`/`neg`/`eq`): `pos + dir * speed`.
48
+ 7. **SIMD (late):** `f32x4` (wasm v128) and `Mat4`/`Vec4` in `plum-std/Math.plum`. KaM is 2D, so this is low priority for the port and kept for future 3D work.
49
+ 8. **Port scope (simplified KaM):**
50
+ - Kept:
51
+ - map and terrain rendering with camera and zoom;
52
+ - units with directional walk/work/fight animations and their sounds;
53
+ - A* pathfinding on the tile grid;
54
+ - buildings placed and constructed with their animations;
55
+ - a simplified economy: woodcutter → sawmill → storehouse, serfs carrying wares;
56
+ - simple melee combat;
57
+ - a basic AI that builds and attacks;
58
+ - selection and commands with the mouse;
59
+ - minimap;
60
+ - win and lose conditions.
61
+ - Dropped: rhai mission scripts, campaign/mission scripting, multiplayer/results screens, the map editor, the full KaM economy tree and saves (maybe Json later).
62
+ 9. **Out of scope:** WebGPU, Plum→shader compilation, porting the asset decoders, and threads.
46
63
 
47
64
  Success criteria:
48
- - `plum-examples/game/pong.plum` runs natively at 60 fps through `plum game`.
65
+ - The simplified game runs in Chrome/Firefox via `plum game --web`, served as a static folder, at 60 fps on a 128×128 map with 300 units.
66
+ - Animations and sounds match the Rust version's for the kept unit and building types (side-by-side capture).
49
- - Vec/Mat math allocates nothing in `update`: check with a GC-count probe in the host.
67
+ - Zero GC allocation in `update` steady-state outside unit spawn and death, measured with a host GC counter in the native build.
50
- - `Mat4 * Vec4` uses `f32x4` instructions (check the wasm with `wasm-tools print`).
51
68
 
52
69
  ## Global constraints
53
70
 
54
- - Run `cargo test --workspace` and `cargo run -q -p plum-cli -- test plum-std plum-examples` after every task, and both must be green.
71
+ - Both suites (`cargo test --workspace`, `plum test plum-std plum-examples`) are green after every task.
55
- - typeIds: Bool box 7 (from the built-in Bool work), f32 8, i32 9, i32 10, f32x4 box 11. Declared types still start at 16.
56
- - Enable wasmtime `wasm_simd`, `wasm_multi_value` and `wasm_gc` in every host.
57
- - The grammar changes (`struct` keyword, operator-trait resolution needs none) are regenerated with `tree-sitter generate --disable-optimizations`.
72
+ - The `plum-game` crate builds for `wasm32-unknown-unknown` (web) and native. CI runs the native headless tests.
73
+ - The asset files are read from `../kam_macroquad/assets` through the host. They are never copied into this repo, because of licensing.
74
+ - Every Plum API uses only `i32 i64 f32 f64 bool` scalars. There are no unsigned types.
58
75
 
59
- ## Main diagram
76
+ ## Tasks
60
77
 
61
78
  ```mermaid
62
79
  flowchart LR
63
- S["game.plum: init/update/draw"] --> C["checker: f32/i32/i32, literal typing, struct, operator traits, f32x4"]
80
+ L1[typed arrays + collections + consts + RNG]
81
+ L2[struct value types]
82
+ L3[duck-typed operators]
83
+ H1[plum-game host native + Game.plum]
84
+ H2[browser bridge]
85
+ P1[map + camera]
86
+ P2[units + anims + sounds]
87
+ P3[pathfinding]
88
+ P4[buildings + economy]
89
+ P5[combat + AI + win/lose]
64
- C --> G["codegen: flattened structs, multi-value, v128"]
90
+ S1["f32x4 + Math.plum (later)"]
91
+ %% taskgraph
92
+ L1 --> L2
93
+ L2 --> L3
65
- G -->|"game.wasm"| H["plum-game: wasmtime + macroquad loop"]
94
+ L2 --> H1
66
- H -->|"extern draw/input/audio"| M["macroquad"]
95
+ H1 --> H2
96
+ H2 --> P1
97
+ L3 --> P1
67
- G -.->|"later"| B["browser: JS glue + macroquad wasm32"]
98
+ P1 --> P2
99
+ P2 --> P3
100
+ P3 --> P4
101
+ P4 --> P5
102
+ L3 --> S1
68
103
  ```
69
104
 
105
+ ### L1 typed arrays, collections, constants, RNG
70
- ## Tasks (sequential: each one's tests compile Plum through the previous)
106
+ - Native `Array[i32]`/`Array[f32]` (wasm-inspected), `Array.filled` and the 2D helper.
107
+ - `Set`, `Deque` and `Heap` in std.
108
+ - Fix top-level constant parsing and use.
109
+ - `Random(seed)`.
71
110
 
111
+ Tests:
112
+ - `array_i32_native` (the wasm declares `array (mut i32)`);
113
+ - `heap_pop_order` on 1000 random keys;
72
- ```mermaid
114
+ - `deque_wraparound`;
73
- flowchart LR
74
- T1[sized numbers]
115
+ - `set_dedup`;
75
- T2[literal typing]
116
+ - `const_table_1000_entries`;
117
+ - `random_seed_deterministic` (same seed gives the same first 100 values).
118
+
76
- T3[struct value types]
119
+ ### L2 struct value types
120
+ Grammar `struct`, flattened lowering, multi-value returns, copy semantics, field assignment, recursion rejected. Tests:
77
- T4[operator traits]
121
+ - `struct_copy_semantics`;
122
+ - `struct_multi_value_return`;
123
+ - `struct_in_list_roundtrip`;
124
+ - `struct_eq_fieldwise`;
125
+ - `recursive_struct_rejected`.
126
+
127
+ ### L3 duck-typed operators
128
+ `a + b` → `a.add(b)` and so on, resolved per instantiation. Tests:
78
- T5[f32x4 + Math.plum]
129
+ - `vec2_add_mul_scalar`;
130
+ - `neg`;
131
+ - `missing_operator_method_error` with the use-site chain.
132
+
79
- T6[plum-game host + Game.plum]
133
+ ### H1 `plum-game` native host + Game.plum
134
+ - A macroquad + wasmtime crate with the `init`/`update`/`draw` loop.
135
+ - Game.plum externs as in Design item 3.
136
+ - Asset handles backed by kam_macroquad's existing loaders (reused as a path dependency on its asset modules).
80
- T7[pong example + perf checks]
137
+ - `plum game <file>`.
138
+
81
- T8[browser host]
139
+ Tests: a headless mock recorder for call order and args; `color_flattened`; `mousePos_multi_value`; `anim_frames_match_rust` for 3 unit kinds.
82
- %% taskgraph
83
- T1 --> T2
84
- T2 --> T3
85
- T3 --> T4
86
- T4 --> T5
87
- T3 --> T6
88
- T5 --> T7
89
- T6 --> T7
90
- T7 --> T8
91
- ```
92
140
 
93
- ### T1 sized numbers
94
- Grammar unchanged, since these are type names. In checker/codegen, add f32/i32/i32 types, arithmetic and comparison per type, casts, boxing ids 8–10, and `Number.plum` methods (`toStr`, `parseF32`…). **Tests:** `f32_precision` (`f32(0.1) + f32(0.2)` prints `0.3` and its f32 bits are exact); `i32_wraps` (`i32(2147483647) + i32(1) == i32(-2147483648)`); `u32_compare_unsigned` (`i32(4000000000) > i32(1)`); `boxed_sized_roundtrip` through `List[f32]`, `Try[i32,[A]]` and `ToStr`.
141
+ ### H2 browser bridge
142
+ - `plum_bridge.js` miniquad plugin.
143
+ - `plum game --web <file>` writes `dist/` (index.html, the macroquad wasm, the plum wasm, the bridge).
95
144
 
96
- ### T2 contextual literal typing
97
- In the checker, an expected sized type flows into literals. In codegen, literals are emitted at that width. **Tests:** `x: f32 = 1.5` gets no Float→f32 error; `drawRect(10, 20, 32, 32)` with f32 params; `v.x * 0.5`; overflow `i32 = 3000000000` is an error naming the type; an unannotated `1.5` is still Float.
145
+ Check: a demo scene (sprites, sound, input) runs in Chrome and Firefox. Playwright headless-Chrome smoke test: canvas pixels change across frames and there are no console errors.
98
146
 
99
- ### T3 struct value types
100
- `struct` keyword in the grammar and parser (`ast::Item::Struct`). Checker: value semantics, no recursion, field assignment on bindings. Codegen: flatten to locals/params, multi-value returns, box at generic and trait edges. **Tests:** `struct_copy_semantics` (`b := a; b.x = 9.0; a.x == 1.0`); `struct_multi_value_return` (`fun mid(a, b: Vec2) -> Vec2`); `struct_in_list_boxes_roundtrip`; `struct_eq_fieldwise`; `recursive_struct_rejected`.
147
+ ### P1 map + camera
148
+ Load a KaM map via the host, terrain into `Array[i32]`, tile rendering, camera pan/zoom, minimap. Check: renders the same as the Rust version on 2 maps (screenshot diff tolerance).
101
149
 
102
- ### T4 operator traits
103
- `Add/Sub/Mul/Div/Neg/Eq` traits in `plum-std/Ops.plum`. The checker maps binary/unary ops on non-primitive operands to those methods, with static dispatch, scalar-RHS overloads, and errors like `Vec2 does not implement Add`. **Tests:** `vec2_add_mul_scalar` (`(Vec2(1,2) + Vec2(3,4)) * 2.0 == Vec2(8, 12)`); `neg`; `missing_op_trait_error`.
150
+ ### P2 units + animations + sounds
151
+ Unit structs, directional animation state machine, walk interpolation, selection, positional sounds. Tests: the animation frame sequence for walk dir 3 equals the Rust table, and the sound fires on the step frames.
104
152
 
105
- ### T5 f32x4 + Math.plum
106
- v128 primitive, lane ops, `splat`/`dot`/`shuffle`, boxing id 11. `Math.plum`: Vec2, Vec3, Vec4, Quat, Mat4 (`mul`, `transpose`, `inverse`, `perspective`, `lookAt`, `ortho`, `translate`, `rotate`, `scale`), `lerp`, `clamp`. **Tests:** `mat4_identity_mul`; `mat4_inverse_roundtrip` (within 1e-5); `quat_rotate_90deg` (within 1e-6); `simd_ops_emitted` (the wasm contains `f32x4.mul`).
153
+ ### P3 pathfinding
154
+ A* over passability with `Heap`, path smoothing as in `src/pathfinding.rs` (simplified), unit move orders. Tests: shortest-path length on 5 fixture grids matches the Rust implementation, and 300 units repathing stays under 2 ms per frame (native).
107
155
 
108
- ### T6 plum-game host + Game.plum
109
- A new `plum-game` crate (macroquad + wasmtime). `Game.plum` holds the extern API (Design item 6). `plum game <file>` in the CLI. **Tests:** a headless host test (macroquad behind a trait, with a mock recorder) asserting that `init`/`update`/`draw` call order and arguments are recorded; `color_flattened_to_4_f32`; `mousePos_multi_value`.
156
+ ### P4 buildings + economy
157
+ Placement, construction stages with animations, woodcutter → sawmill → storehouse, serfs carrying wares. Tests: a deterministic 2-minute sim produces N planks (fixed seed); building stages advance at the Rust timings.
110
158
 
111
- ### T7 pong example + perf checks
112
- `plum-examples/game/pong.plum` with paddles, ball, score and `drawText`. **Checks:** 60 fps native, zero GC allocations in `update` after warm-up (from the host's GC count), and `f32x4` present in the `Mat4` code path.
159
+ ### P5 combat + AI + win/lose
160
+ Melee combat with animations and sounds, a basic AI that builds and attacks, and victory/defeat. Check: an AI-vs-AI game ends deterministically for a fixed seed. The full game plays in the browser at the success-criteria scale.
113
161
 
114
- ### T8 browser host
162
+ ### S1 f32x4 + Math.plum (later)
115
- JS glue plus macroquad's wasm32 build, the same `Game.plum` API, and `plum game --web <file>` producing a static `index.html`. **Check:** pong runs in Chrome and Firefox.
163
+ v128, lane ops, `Vec4`/`Quat`/`Mat4`. Tests: `mat4_inverse_roundtrip`, and `simd_ops_emitted`. Not needed for the 2D port.
116
164
 
117
- ## Open questions to settle before running
165
+ ## Open questions
118
166
 
119
- - Will the user's further ideas change the scope or order? This plan is still a draft.
167
+ - Will the user's further ideas change scope or order? This is still a draft.
168
+ - Which unit and building types make the "kept" list? The default above is the wood chain plus militia/axe fighters.
120
- - Should `Float` be renamed to `f64` (with `Int` → `I64`) for consistency, now that sized types exist?
169
+ - Is reusing kam_macroquad's asset modules as a path dependency acceptable, or should the host vendor a minimal decoder?
121
- - Should struct field mutation require `var` bindings, or is every binding mutable as today?