plum

#treesitter#compiler#wasm

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

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


1f77028 — Peter John 2026-11-06T16:42:31+05:30
Docs: game docs for plain games, one scene style
compiler/lsp/src/main.rs CHANGED
@@ -765,7 +765,6 @@ mod tests {
765
765
  "docs/language.md",
766
766
  "docs/std.md",
767
767
  "docs/game.md",
768
- "docs/game-scene-styles.md",
769
768
  "docs/tools.md",
770
769
  "docs/performance.md",
771
770
  ];
docs/backlog.md CHANGED
@@ -53,6 +53,8 @@ Open work, in the order the user queued it, plus smaller items found during buil
53
53
 
54
54
  ## Smaller open items
55
55
 
56
+ - **Go-to-definition in the LSP is gone** (2026-11-06): it only ever handled inherited members, which went with inheritance. A real one is wanted once modules land.
57
+ - **Leaked `plum run --no-open` servers** (2026-11-06): a test using `compiler/game/tests/fixtures/host_fixture.plum` left servers running for hours. Not found; the likely cause is `serveStartsAndAnswersWithTheRightContentTypes` in `compiler/cli/tests/cli/game_web_test.rs`, which only kills its child on the success path (the other spawning tests use a kill-on-drop guard).
56
58
  - **Locals named like functions, remaining cases** (found 2026-11-06, F-PUR-1 note, pre-existing, with or without fusion): in wasm-codegen, a flat local named like a free function traps or gives invalid wasm; a local in an embedded struct's `init` that captures a free call is silently wrong. The second goes away if inheritance is removed.
57
59
  - **Pattern-shadowed parameter gives invalid wasm** (found 2026-11-06, pre-existing, with or without fusion): a parameter shadowed by a pattern and read in a sibling arm compiles to wasm that won't load. Details in the game engine run's note `F-T6-2`.
58
60
  - **Closure self-call, still open** (`F-CLOSURE-1` fixed the typed-argument and arity checks; a self-call now errors in the checker when an argument does not fit a known parameter type). What stays: (a) a parameter with no declared type and no hint from a later call (`rec := |n| .. rec("x")` passed to `map`) has no type to check against; (b) `rec(None, [], n-1)` / `rec(Some(n), ..)` with untyped params still fail in codegen (invalid wasm or `type name 'Nil' is not yet supported as a value`), because the self-call's arguments are inferred with no expected type; checking them with the parameter as expected type fixes those but changes the wasm of programs that run today (e.g. a typed `Option[Int]` param called with `None`), so do it in a run that may re-baseline wasm; (c) a self-call from a nested closure (`outer := |n| inner := |m| outer(m-1) ..`) fails in codegen with `unknown function 'outer'`; (d) a typed declaration `f: fn(Int) -> Int := |n| ..` does not bind `f` inside its body; (e) a failed closure initialiser also reports a second `'rec' is not a function` at its first use.
docs/game-scene-styles.md DELETED
@@ -1,138 +0,0 @@
1
- # Choosing a scene style
2
-
3
- Plum ships three scene libraries. Each is a different answer to one question: where does your game's data live, and who changes it?
4
-
5
- | Style | Where data lives | How it changes | Good for |
6
- |---|---|---|---|
7
- | [Imperative](#imperative) | In objects in a tree | You change them in place | UI, and objects with their own behaviour |
8
- | [Pure](#pure) | In a tree of nodes | Each update returns the next tree | Timed animations, and rules you can test without a browser |
9
- | [State and view](#state-and-view) | In plain data, with no tree kept | `update` returns new data, and `render` draws it | Games that are simple to reason about |
10
-
11
- Not sure? Start with **state and view** for a small game, or **imperative** if you're used to engines with objects and scene graphs. For a very small arcade game you may not need one at all: see [drawing directly](game.md#a-minimal-game).
12
-
13
- ## Imperative
14
-
15
- A tree of actors, each with its own `act` and `draw`. Moving a parent moves everything inside it. Library: `std/game/imperative/Scene`.
16
-
17
- ```plum
18
- import std/Int
19
- import std/Float
20
- import std/Str
21
- import std/Slice
22
- import std/Option
23
- import std/Io
24
- import std/game/Game
25
- import std/game/Vec2
26
- import std/game/Color
27
- import std/game/Affine
28
- import std/game/Input
29
- import std/game/imperative/Scene
30
-
31
- struct Ball(Actor(x, y), speed: Float) =
32
- fun init(self) =
33
- self.width = 20.0
34
- self.height = 20.0
35
- self.color = Color.Red
36
-
37
- override fun act(self, dt: Float) =
38
- self.x = self.x + self.speed * dt
39
-
40
- override fun draw(self, canvas: Canvas, m: Affine, alpha: Float) =
41
- drawRectBox(canvas, m, alpha, self.width, self.height, self.color)
42
-
43
- struct MyGame(Game(engine), stage: Stage) =
44
- override fun update(self, dt: Float) =
45
- self.stage.act(dt)
46
-
47
- override fun draw(self) =
48
- self.canvas.clear(Color.Black)
49
- self.stage.draw(self.canvas)
50
-
51
- fun main(io: Io) -> Game =
52
- stage := Stage.create()
53
- stage.root.add(Ball(x: 0.0, y: 40.0, speed: 30.0))
54
- MyGame(engine: Engine.create(io), stage: stage)
55
- ```
56
-
57
- Useful extras: `toFront` and `toBack` for draw order, `sortChildren` to sort by depth each frame, and `setCullingArea` to skip what's off screen. Full example: [`game_scene.plum`](../examples/games/imperative/game_scene.plum).
58
-
59
- ## Pure
60
-
61
- Also a tree, but an update never changes a node it was given: it returns the next one. The compiler reuses memory where it can, so this doesn't mean copying everything. Timed animations are plain values called actions. Library: `std/game/pure`.
62
-
63
- ```plum
64
- import std/Int
65
- import std/Float
66
- import std/Str
67
- import std/List
68
- import std/Slice
69
- import std/Option
70
- import std/Io
71
- import std/game/Game
72
- import std/game/Vec2
73
- import std/game/Color
74
- import std/game/Input
75
- import std/game/pure/Actions
76
- import std/game/pure/PureScene
77
- import std/game/pure/SceneInput
78
-
79
- struct MyGame(Game(engine), mut stage: Stage) =
80
- override fun update(self, dt: Float) =
81
- self.stage = self.stage.frame(dt, self.input.events()).intoStage()
82
-
83
- override fun draw(self) =
84
- self.stage.draw(self.canvas)
85
-
86
- fun main(io: Io) -> Game =
87
- e := Engine.create(io)
88
- sprite := Node.sprite(2, e.assets.texture("ball.png"), 100.0, 100.0, 48.0, 48.0)
89
- spinning := Node(..sprite, actions: [Action.forever(Action.rotateBy(1.0, 1.0))].toSlice())
90
- root := Node.group(1, 0.0, 0.0, 800.0, 600.0, [spinning].toSlice())
91
- MyGame(engine: e, stage: Stage.create(root))
92
- ```
93
-
94
- Find a node by id with `findPath` and change it with `modify`. Full example: [`game_pure_scene.plum`](../examples/games/pure/game_pure_scene.plum).
95
-
96
- ## State and view
97
-
98
- Your game is plain data. `update` returns the next state, and `render` describes what to draw as a value. Nothing is kept between frames, so there's no tree to keep in sync. Library: `std/game/functional`.
99
-
100
- ```plum
101
- import std/Int
102
- import std/Float
103
- import std/Str
104
- import std/List
105
- import std/Slice
106
- import std/Option
107
- import std/Io
108
- import std/game/Game
109
- import std/game/Vec2
110
- import std/game/Color
111
- import std/game/functional/FunctionalScene
112
-
113
- enum Msg = NoMsg
114
-
115
- struct State(x: Float) =
116
- fun update(self, dt: Float) -> State =
117
- State(..self, x: self.x + 30.0 * dt)
118
-
119
- fun render(self) -> SceneNode[Msg] =
120
- RectShape(x: self.x, y: 40.0, w: 20.0, h: 20.0, color: Color.Red)
121
-
122
- struct MyGame(Game(engine), mut state: State) =
123
- override fun update(self, dt: Float) =
124
- self.state = self.state.update(dt)
125
-
126
- override fun draw(self) =
127
- self.canvas.clear(Color.Black)
128
- self.state.render().draw(self.canvas)
129
-
130
- fun main(io: Io) -> Game =
131
- MyGame(engine: Engine.create(io), state: State(x: 0.0))
132
- ```
133
-
134
- `Anim` describes motion as a function of time, and `Ui` handles buttons and pointer clicks, turning them into your `Msg` values. `State` holds no host value, so you can test all of your rules without a browser. Full example: [`game_state_view.plum`](../examples/games/functional/game_state_view.plum).
135
-
136
- ## Compare them
137
-
138
- The imperative, pure and state-and-view examples are the same game, Orb Run, written three ways. [`game.plum`](../examples/games/imperative/game.plum) is the same game drawn directly, with no scene library. Read them side by side to see how each one feels.
docs/game.md CHANGED
@@ -7,14 +7,19 @@ Write a game in Plum and run it in the browser on Plum's own WebGPU renderer. It
7
7
  ## Try it
8
8
 
9
9
  ```sh
10
- plum run examples/games/imperative/game.plum
10
+ plum run examples/games/orb_run.plum
11
11
  ```
12
12
 
13
13
  This builds the game, starts a local server and opens your browser. Steer with the arrow keys or WASD, collect the orbs and dodge the mines. Press a key first, because browsers only start sound after you interact with the page.
14
14
 
15
15
  ## A minimal game
16
16
 
17
+ A game is any type with two methods, and `main` returns it.
18
+
19
+ - `update(self, io: Io, dt: Float) -> T` returns the next version of the game. `dt` is the length of the last frame in seconds.
20
+ - `draw(self, io: Io)` draws the current version.
21
+
17
- A game is a struct that embeds `Game` and overrides `update` and `draw`. `main` builds it and returns it.
22
+ Each frame the host calls `update`, and then `draw` on what `update` returned. Nothing is changed in place.
18
23
 
19
24
  ```plum
20
25
  import std/Int
@@ -22,68 +27,58 @@ import std/Float
22
27
  import std/Str
23
28
  import std/Io
24
29
  import std/game/Game
25
- import std/game/Vec2
26
30
  import std/game/Color
27
31
 
28
- struct Ball(Game(engine), mut x: Float, sprite: Texture) =
32
+ struct Ball(x: Float, sprite: Texture) =
29
- override fun update(self, dt: Float) =
33
+ fun update(self, io: Io, dt: Float) -> Ball =
30
- if self.input.isKeyDown(Key.Right)
34
+ if io.isKeyDown(Key.Right)
31
- self.x = self.x + 100.0 * dt
35
+ Ball(x: self.x + 100.0 * dt, sprite: self.sprite)
36
+ else
37
+ self
32
38
 
33
- override fun draw(self) =
39
+ fun draw(self, io: Io) =
34
- self.canvas.clear(Color.Black)
40
+ io.clear(Color.Black)
35
- self.canvas.drawTexture(self.sprite, self.x, 100.0)
41
+ self.sprite.draw(io, self.x, 100.0)
36
42
 
37
- fun main(io: Io) -> Game =
43
+ fun main(io: Io) -> Ball =
38
- e := Engine.create(io)
39
- Ball(engine: e, x: 0.0, sprite: e.assets.texture("ball.png"))
44
+ Ball(x: 0.0, sprite: Texture.load(io, "ball.png"))
40
45
  ```
41
46
 
42
- `main` runs once, after your assets have loaded. `update` and `draw` then run every frame.
43
-
44
- `Engine.create(io)` builds the helpers your game uses, so you never pass `io` around in game code. Keep your rules in plain functions over plain data, like `steer(ship, dt)`. Those need no browser to test.
47
+ `main` runs once, after your assets have loaded. `update` and `draw` then run every frame. If `main` returns a type without these two methods, `plum run` runs it as an ordinary program instead.
45
48
 
46
- ## What a game can use
49
+ Keep your rules in plain functions over plain data, like `steer(ship, dt)`. Those need no browser to test.
47
50
 
48
- Inside `update` and `draw`, these are all on `self`.
51
+ ## What `io` gives you
49
52
 
50
- **`self.canvas`:** drawing
53
+ Everything the host provides is a method of `io`. The full list is in [`std/Io.plum`](../std/Io.plum); the key codes and the `Texture` and `Sound` handles are in [`std/game/Game.plum`](../std/game/Game.plum).
51
54
 
52
55
  | Group | Functions |
53
56
  |---|---|
54
- | Screen | `width()`, `height()`, `clear(color)` |
57
+ | Screen | `screenWidth()`, `screenHeight()`, `frameTime()`, `clear(color)` |
55
58
  | Shapes | `drawRect(x, y, w, h, c)`, `drawCircle(x, y, r, c)`, `drawLine(x1, y1, x2, y2, thickness, c)` |
56
59
  | Text | `drawText(s, x, y, size, c)` |
57
- | Images | `drawTexture(t, x, y)`, `drawTextureEx(t, sx, sy, sw, sh, x, y, w, h, rotation, flipX, tint)`, `textureWidth(t)`, `textureHeight(t)`, `setSmooth(t, smooth)` |
58
60
  | Camera | `setCamera(x, y, scale)`, `resetCamera()` |
59
- | Draw into an image | `createTexture(w, h)`, `renderTarget(t)`, `screenTarget()` |
60
61
  | Blending | `blendMode(m)` with `Blend.Normal`, `Blend.Add`, `Blend.Multiply`, `Blend.Screen` |
62
+ | Keyboard | `isKeyDown(k)`, `isKeyPressed(k)`, with `Key.Space`, `Key.KeyA`, `Key.Up`, `Key.Enter`, ... |
63
+ | Mouse and touch | `mousePos()`, `isMouseDown(b)`, `isMousePressed(b)`, `mouseWheel()`, with `Mouse.Left`, `Mouse.Right`, `Mouse.Middle` |
64
+ | Random | `randomSeed()` |
61
65
 
62
- **`self.input`:** keyboard, mouse and touch
66
+ Images and sounds are handles you load once, in `main`, and keep in your game. Do not load them every frame.
63
67
 
64
- | Function | What it gives you |
68
+ | Handle | Functions |
65
69
  |---|---|
66
- | `isKeyDown(k)`, `isKeyPressed(k)` | key state, with `Key.Space`, `Key.KeyA`, `Key.Up`, `Key.Enter`, ... |
67
- | `pointer()` | a `Pointer` with `pos`, `down`, `pressed`, `released`, for the mouse or one finger |
70
+ | `Texture.load(io, name)` | `draw(io, x, y)`, `width(io)`, `height(io)`, `setSmooth(io, smooth)` |
68
- | `isMouseDown(b)`, `isMousePressed(b)`, `mousePos()`, `mouseWheel()` | mouse state, with `Mouse.Left`, `Mouse.Right`, `Mouse.Middle` |
71
+ | `Texture.create(io, w, h)` | a blank texture; `renderTarget(io)` draws into it and `Texture.screen(io)` goes back to the screen |
69
- | `events()` | this frame's `InputEvent`s: `PointerDown`, `PointerUp`, `PointerMove`, `KeyDown`, `KeyUp` |
72
+ | `Sound.load(io, name)` | `play(io)`, `loop(io)`, `stop(io)`, `setVolume(io, v)` |
73
+ | `loadFile(io, name)` | the file's text |
70
74
 
71
- **`self.audio`:** `play(sound, volume)`, `loop(sound, volume)`, `stop(sound)`, `volume(sound, volume)`
75
+ A missing file stops the game and names the file, so typos show up straight away. For a texture region, rotation or flip, use `io.drawTextureEx`.
72
76
 
73
- **`self.assets`:** files from the assets folder, by name
77
+ Colors come from `Color.White`, `Color.Red` and so on, or `Color.rgb(r, g, b)`.
74
-
75
- | Function | What it gives you |
76
- |---|---|
77
- | `texture(name)` | a `Texture` |
78
- | `sound(name)` | a `Sound` |
79
- | `file(name)` | the file's text |
80
- | `exists(name)` | whether the file exists |
81
-
82
- A missing file stops the game and names the file, so typos show up straight away. Look an asset up once, in `main`, and keep the `Texture`, rather than looking it up every frame.
83
78
 
84
- **`self.clock`:** `time`, seconds since the start; `frame`, the frame count; `dt`, the last frame's length
79
+ ### Input as events
85
80
 
86
- Colors come from `Color.White`, `Color.Red` and so on, or `Color.rgb(r, g, b)`.
81
+ `InputEvent.poll(io, prev)` in [`std/game/Input.plum`](../std/game/Input.plum) is the one place that reads input. It returns this frame's `InputEvent`s (`PointerDown`, `PointerUp`, `PointerMove`, `KeyDown`, `KeyUp`) and the keys now held. A game that feeds every event into its state reads nothing else, so a recorded list of events replays the game exactly.
87
82
 
88
83
  ## Assets
89
84
 
@@ -95,18 +90,47 @@ While `plum run` serves a game, it watches your code and assets. Save a file and
95
90
 
96
91
  If the new code has a compile error, the old game keeps running and the error appears over it. Fix the error and the page reloads. The game restarts on each reload, so its state isn't kept. Turn this off with `--no-watch`.
97
92
 
98
- ## Ways to structure a game
93
+ ## Scenes
94
+
95
+ For drawing more than a few shapes, build a picture and draw it. [`std/game/Scene.plum`](../std/game/Scene.plum) has one scene style: `SceneNode[M]` is a plain value, a tree of `Group`, `Sprite`, `Text`, `RectShape`, `CircleShape`, `LineShape`, `Filter` and `Clickable` nodes. `render(self)` builds a fresh tree from your state every frame, and `draw(io)` draws it. Nothing in the tree is retained or changed in place.
99
96
 
100
- Drawing directly is enough for small games. For bigger ones, Plum includes three scene libraries. All of them are in [`std/game`](../std/game), and each has an example game.
97
+ A `Clickable` carries a message of type `M`. `hitId(x, y)` tells you which one is under a point, and `msgOf(id)` gives its message, so clicks go through your state like any other event.
101
98
 
99
+ ```plum
100
+ import std/Int
101
+ import std/Float
102
+ import std/Str
103
+ import std/Option
104
+ import std/List
105
+ import std/Io
106
+ import std/game/Game
107
+ import std/game/Color
108
+ import std/game/Scene
109
+
110
+ struct Counter(clicks: Int) =
102
- | Style | How it works | Good for | Example |
111
+ fun update(self, io: Io, dt: Float) -> Counter =
112
+ if !io.isMousePressed(Mouse.Left)
113
+ return self
103
- |---|---|---|---|
114
+ p := io.mousePos()
115
+ match self.render().hitId(p.x, p.y)
116
+ Some(id) => Counter(clicks: self.clicks + 1)
117
+ None => self
118
+
119
+ fun render(self) -> SceneNode[Int] =
120
+ Group(transform: Identity, children: [
104
- | **Direct drawing** | One state struct, and `draw` makes the calls each frame | small arcade games | [`game.plum`](../examples/games/imperative/game.plum) |
121
+ Clickable(id: 1, msg: 1, child: RectShape(x: 20.0, y: 20.0, w: 100.0, h: 40.0, color: Color.Red)),
105
- | **Imperative scene** | A tree of objects that you change in place | UI, and objects with their own behaviour | [`game_scene.plum`](../examples/games/imperative/game_scene.plum) |
122
+ Text(text: "clicks {self.clicks}", x: 20.0, y: 80.0, size: 16.0, color: Color.White)
123
+ ])
124
+
125
+ fun draw(self, io: Io) =
126
+ io.clear(Color.Black)
127
+ self.render().draw(io)
128
+
106
- | **Pure scene** | A tree where each update returns the next version | rules you can test without a browser | [`game_pure_scene.plum`](../examples/games/pure/game_pure_scene.plum) |
129
+ fun main(io: Io) -> Counter =
107
- | **State and view** | Plain data, and a `view` function draws it each frame | games that are simple to reason about | [`game_state_view.plum`](../examples/games/functional/game_state_view.plum) |
130
+ Counter(clicks: 0)
131
+ ```
108
132
 
109
- These four examples are the same game, so you can compare them side by side. [Choosing a scene style](game-scene-styles.md) shows a small program in each.
133
+ [`std/game/Ui.plum`](../std/game/Ui.plum) adds buttons on top of this, and [`std/game/Anim.plum`](../std/game/Anim.plum) adds animation by time. A full game written this way is [`orb_run.plum`](../examples/games/orb_run.plum); its header comment explains how it is put together.
110
134
 
111
135
  ## Testing a game
112
136
 
std/game/Input.plum CHANGED
@@ -13,7 +13,7 @@ import std/List
13
13
  # Player input shared by every scene style. `InputEvent.poll` is the only code that reads
14
14
  # input from the host: a game that feeds every polled `InputEvent` into its state reads
15
15
  # nothing else, so a recorded list of inputs replays the game exactly (see
16
- # docs/game-scene-styles.md, "Determinism").
16
+ # docs/game.md, "Input as events").
17
17
 
18
18
  # One input: a pointer event in screen pixels, or a key going down or up.
19
19
  enum InputEvent =