plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
09f6b34
— Peter John
2026-10-26T10:24:18+05:30
docs(plans): draft game engine plan (macroquad host, sized numbers, structs, SIMD)
docs/plans/2026-10-26-game-engine.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Plum for games (macroquad host, sized numbers, value structs, SIMD)
|
|
2
|
+
|
|
3
|
+
**Status:** draft. Planned only, not dispatched, because the user has more ideas to fold in first.
|
|
4
|
+
**Goal:** Let Plum write game logic and rendering, running on a macroquad-based host. Natively first, then in the browser.
|
|
5
|
+
**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.
|
|
6
|
+
**Tech stack:** Rust, macroquad, wasmtime (GC, multi-value and SIMD enabled), wasm-encoder.
|
|
7
|
+
**Detail:** tests
|
|
8
|
+
|
|
9
|
+
## Design
|
|
10
|
+
|
|
11
|
+
What the user decided:
|
|
12
|
+
- macroquad is the host engine, and Plum owns the game logic and rendering.
|
|
13
|
+
- Add sized primitives. `Float` stays f64 for now (the alternative was making `Float` f32).
|
|
14
|
+
- Value types come from a new `struct` syntax.
|
|
15
|
+
- Vectors and simd128.
|
|
16
|
+
|
|
17
|
+
1. **Sized numbers.** `F32`, `I32` and `U32` are new built-in primitives (wasm `f32`/`i32`). `Byte` stays as u8, `Int` as i64 and `Float` as f64.
|
|
18
|
+
- Ruling: `Float` is not changed to f32 (because Time millis, Json and parseFloat need f64; cost: game code writes `F32` explicitly).
|
|
19
|
+
- Arithmetic, comparisons and `toStr`/`parse*` exist per type, and explicit casts are `F32(x)`, `I32(x)` and so on.
|
|
20
|
+
- Each type boxes at generic edges with its own typeId (8, 9 and 10).
|
|
21
|
+
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.
|
|
22
|
+
3. **Value structs.** `struct Vec2(x: F32, y: F32)` has the same body syntax as a class, but has value semantics.
|
|
23
|
+
- In locals and params a struct is flattened into one wasm local/param per field, and it is returned with multi-value results.
|
|
24
|
+
- Assigning copies it. A field can be assigned in place (`v.x += 1.0`), which mutates only that binding.
|
|
25
|
+
- It is boxed (as a GC struct with a typeId) only when it flows into a `TParam`, trait slot, `List` element or closure capture.
|
|
26
|
+
- `==` compares fields. Methods take `self` by value. Structs can't be recursive.
|
|
27
|
+
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.
|
|
28
|
+
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`.
|
|
29
|
+
6. **`plum-game` host (native).** A new crate built on macroquad and wasmtime.
|
|
30
|
+
- 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)`.
|
|
31
|
+
- 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).
|
|
32
|
+
- The extern API lives in `plum-std/Game.plum`:
|
|
33
|
+
- `clear(c: Color)`, `drawRect(x, y, w, h: F32, c: Color)`, `drawCircle`, `drawLine`, `drawText(s: Str, x, y, size: F32, c: Color)`;
|
|
34
|
+
- `loadTexture(path: Str) -> U32` (preloaded by the host before `init`), `drawTexture(t: U32, x, y: F32, c: Color)`;
|
|
35
|
+
- `isKeyDown(k: Key) -> Bool`, `isKeyPressed`, `mousePos() -> Vec2`, `isMouseDown`, `screenSize() -> Vec2`;
|
|
36
|
+
- `playSound(s: U32)`, `setCamera2D(target: Vec2, zoom: F32)`.
|
|
37
|
+
- Structs cross the boundary flattened, so `Color` is 4 f32 params. `Key` is a discriminant enum whose values match macroquad's `KeyCode`.
|
|
38
|
+
- `plum game <file>` is a CLI subcommand that compiles the file and runs it in the host.
|
|
39
|
+
7. **Custom rendering (v1).**
|
|
40
|
+
- `drawMesh(vertices: List[Vertex], indices: List[U16], texture: U32)`. The host copies the data per call.
|
|
41
|
+
- `loadMaterial(vertexGlsl: Str, fragmentGlsl: Str) -> U32` and `useMaterial(m)` wrap macroquad's `Material`.
|
|
42
|
+
- Zero-copy linear-memory buffers are deferred until profiling shows a need.
|
|
43
|
+
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.
|
|
44
|
+
9. **Out of scope:** WebGPU, `StructArray` flat storage, Plum→shader compilation, async and audio streaming.
|
|
45
|
+
|
|
46
|
+
Success criteria:
|
|
47
|
+
- `plum-examples/game/pong.plum` runs natively at 60 fps through `plum game`.
|
|
48
|
+
- Vec/Mat math allocates nothing in `update`: check with a GC-count probe in the host.
|
|
49
|
+
- `Mat4 * Vec4` uses `f32x4` instructions (check the wasm with `wasm-tools print`).
|
|
50
|
+
|
|
51
|
+
## Global constraints
|
|
52
|
+
|
|
53
|
+
- Run `cargo test --workspace` and `cargo run -q -p plum-cli -- test plum-std plum-examples` after every task, and both must be green.
|
|
54
|
+
- typeIds: Bool box 7 (from the built-in Bool work), F32 8, I32 9, U32 10, F32x4 box 11. Declared types still start at 16.
|
|
55
|
+
- Enable wasmtime `wasm_simd`, `wasm_multi_value` and `wasm_gc` in every host.
|
|
56
|
+
- The grammar changes (`struct` keyword, operator-trait resolution needs none) are regenerated with `tree-sitter generate --disable-optimizations`.
|
|
57
|
+
|
|
58
|
+
## Main diagram
|
|
59
|
+
|
|
60
|
+
```mermaid
|
|
61
|
+
flowchart LR
|
|
62
|
+
S["game.plum: init/update/draw"] --> C["checker: F32/I32/U32, literal typing, struct, operator traits, F32x4"]
|
|
63
|
+
C --> G["codegen: flattened structs, multi-value, v128"]
|
|
64
|
+
G -->|"game.wasm"| H["plum-game: wasmtime + macroquad loop"]
|
|
65
|
+
H -->|"extern draw/input/audio"| M["macroquad"]
|
|
66
|
+
G -.->|"later"| B["browser: JS glue + macroquad wasm32"]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Tasks (sequential: each one's tests compile Plum through the previous)
|
|
70
|
+
|
|
71
|
+
```mermaid
|
|
72
|
+
flowchart LR
|
|
73
|
+
T1[sized numbers]
|
|
74
|
+
T2[literal typing]
|
|
75
|
+
T3[struct value types]
|
|
76
|
+
T4[operator traits]
|
|
77
|
+
T5[F32x4 + Math.plum]
|
|
78
|
+
T6[plum-game host + Game.plum]
|
|
79
|
+
T7[pong example + perf checks]
|
|
80
|
+
T8[browser host]
|
|
81
|
+
%% taskgraph
|
|
82
|
+
T1 --> T2
|
|
83
|
+
T2 --> T3
|
|
84
|
+
T3 --> T4
|
|
85
|
+
T4 --> T5
|
|
86
|
+
T3 --> T6
|
|
87
|
+
T5 --> T7
|
|
88
|
+
T6 --> T7
|
|
89
|
+
T7 --> T8
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### T1 sized numbers
|
|
93
|
+
Grammar unchanged, since these are type names. In checker/codegen, add F32/I32/U32 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` (`U32(4000000000) > U32(1)`); `boxed_sized_roundtrip` through `List[F32]`, `Try[U32,[A]]` and `ToStr`.
|
|
94
|
+
|
|
95
|
+
### T2 contextual literal typing
|
|
96
|
+
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.
|
|
97
|
+
|
|
98
|
+
### T3 struct value types
|
|
99
|
+
`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`.
|
|
100
|
+
|
|
101
|
+
### T4 operator traits
|
|
102
|
+
`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`.
|
|
103
|
+
|
|
104
|
+
### T5 F32x4 + Math.plum
|
|
105
|
+
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`).
|
|
106
|
+
|
|
107
|
+
### T6 plum-game host + Game.plum
|
|
108
|
+
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`.
|
|
109
|
+
|
|
110
|
+
### T7 pong example + perf checks
|
|
111
|
+
`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.
|
|
112
|
+
|
|
113
|
+
### T8 browser host
|
|
114
|
+
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.
|
|
115
|
+
|
|
116
|
+
## Open questions to settle before running
|
|
117
|
+
|
|
118
|
+
- Will the user's further ideas change the scope or order? This plan is still a draft.
|
|
119
|
+
- Should `Float` be renamed to `F64` (with `Int` → `I64`) for consistency, now that sized types exist?
|
|
120
|
+
- Should struct field mutation require `var` bindings, or is every binding mutable as today?
|