plum

#treesitter#compiler#wasm

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

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


1140b98 — Peter John 2026-11-02T17:01:16+05:30
README: browser-only games flow, std/Scene guidance, headless runner, browser check
Files changed (1) hide show
  1. README.md +13 -16
README.md CHANGED
@@ -146,7 +146,7 @@ plum test file.plum dir/ # run the `test` blocks of files and directories (
146
146
  plum compile file.plum # write file.wasm
147
147
  plum build file.plum # embed the module into a native executable
148
148
  plum format file.plum # rewrite in place (--check, --stdin)
149
- plum game file.plum # run a game (see Games); --web writes a static web build
149
+ plum game file.plum # serve a game and open it in the browser (see Games); -o DIR writes a static build
150
150
  plum editor helix|vscode # install highlighting and the language server
151
151
  ```
152
152
 
@@ -158,12 +158,7 @@ plum editor helix|vscode # install highlighting and the language server
158
158
  cargo run -q -p plum-cli -- game plum-examples/game.plum # or, after `cargo install --path plum-cli`: plum game plum-examples/game.plum
159
159
  ```
160
160
 
161
- Arrow keys or WASD steer the ship, collect the orbs, dodge the mines; Enter or R restarts after the game is over. The first run builds the `plum-game` host in release mode, which takes a while. To play in a browser instead (needs `rustup target add wasm32-unknown-unknown` once):
162
-
163
- ```sh
164
- plum game --web plum-examples/game.plum -o /tmp/orb-run
165
- python3 -m http.server -d /tmp/orb-run 8080 # then open http://localhost:8080
166
- ```
161
+ This compiles the game, serves it on `http://127.0.0.1:8080/` (any free port if that one is taken) and opens it in your default browser. It needs a browser with WebGPU: Chrome or Edge, Safari 26+ or Firefox. Arrow keys or WASD steer the ship, collect the orbs, dodge the mines; Enter or R restarts after the game is over. Press a key first: browsers start audio only after a user gesture.
167
162
 
168
163
  ![the example game](docs/game.png)
169
164
 
@@ -1042,11 +1037,13 @@ Every module that imports `std/Str` also exports four helpers for hosts that can
1042
1037
 
1043
1038
  ## Games
1044
1039
 
1045
- `plum game file.plum` compiles a game and runs it in a native window (macroquad, with the module under wasmtime). `plum game --web file.plum [-o dist] [--assets DIR]` writes a static web build instead: `index.html`, `mq_js_bundle.js`, `plum_bridge.js`, `plum-game.wasm`, `plum.wasm`, `preload.txt` and an `assets/` copy of the whole asset directory (every file in it is listed in `preload.txt` and fetched before the first frame; `dist` defaults to `dist` beside the source file). Serve it from any static host; the browser needs wasm GC support. The web build needs `rustup target add wasm32-unknown-unknown`.
1040
+ Games run in the browser on Plum's own WebGPU renderer (`plum-game/web/plum_bridge.js`): the Plum module calls the bridge directly, one extern call per draw, and the bridge batches everything into one buffer upload per frame. WebGPU is required (Chrome or Edge, Safari 26+, Firefox); there is no fallback.
1041
+
1042
+ `plum game file.plum [--assets DIR] [--port N] [--no-open]` compiles the game, serves it on 127.0.0.1 (port 8080, else any free port; `--port` picks one), prints the URL and opens the default browser unless `--no-open` is given. Ctrl-C stops it. `plum game file.plum -o dist [--assets DIR]` writes a static build instead and exits: `index.html`, `plum_bridge.js`, `plum.wasm`, `preload.txt` and an `assets/` copy of the whole asset directory (every file in it is listed in `preload.txt` and fetched before the first frame). Host that directory on any static file server; the browser needs wasm GC support. The asset root defaults to `assets` beside the source file.
1046
1043
 
1047
- `plum game --web --renderer pixi file.plum` (experimental; `--renderer macroquad` is the default) writes a PixiJS build instead: `index.html`, `plum_bridge_pixi.js`, the vendored PixiJS 8 (`pixi.min.js`), `plum.wasm`, `preload.txt` and `assets/`, with no host wasm, so it needs no wasm32 target; Plum calls JS directly and Pixi uses WebGPU where the browser has it, else WebGL. See [`plum-game/web-test/BENCHMARK.md`](plum-game/web-test/BENCHMARK.md) for numbers against macroquad.
1044
+ `plum-examples/game.plum` is a small arcade game: steer a ship with the arrow keys or WASD, collect orbs, dodge mines (Enter restarts). It draws shapes, text and an animated sprite sheet (`plum-examples/assets/sprites.png`, generated by `plum-examples/tools/make_sprites.py`) and plays music and sound effects (`plum-examples/assets/*.wav`, generated by `plum-examples/tools/make_sounds.py`); both generators live outside the asset dir so they are not shipped. For a bigger game built on this engine see the Knights and Merchants port in the sibling repo `../kam_plum` (`./run.sh` there). Its rules are plain functions with `test` blocks, so `plum test plum-examples` covers them without a browser.
1048
1045
 
1049
- `plum game plum-examples/game.plum` runs the example, a small arcade game: steer a ship with the arrow keys or WASD, collect orbs, dodge mines (Enter restarts). It draws shapes, text and an animated sprite sheet (`plum-examples/assets/sprites.png`, generated by `plum-examples/tools/make_sprites.py`) and plays music and sound effects (`plum-examples/assets/*.wav`, generated by `plum-examples/tools/make_sounds.py`); both generators live outside the asset dir so they are not shipped. For a bigger game built on this engine see the Knights and Merchants port in the sibling repo `../kam_plum` (`./run.sh` there). Its rules are plain functions with `test` blocks, so `plum test plum-examples` covers them without a window.
1046
+ **`std/Scene`** is a scene-graph library written in Plum (see [`plum-examples/scene.plum`](plum-examples/scene.plum)): a tree of `Node`s (group, sprite, rect, circle, text) under a `Stage`, with parent-relative position, rotation, scale and colour, per-node `act` behaviours, a camera and hit testing. Use it for UI and other hierarchical scenes, where moving a parent should move everything in it. For a large world, keep the game objects in flat records and lists instead (for example `enum Entity = Unit | House` with an `update` and a `draw` that the enum dispatches to its variants) and draw them directly through `std/Game`; a tree of thousands of nodes costs a closure call and a matrix composition per node every frame.
1050
1047
 
1051
1048
  A game declares three exports. `G` is any record and holds all game state (there are no globals):
1052
1049
 
@@ -1056,21 +1053,21 @@ export fun update(g: G, dt: Float)
1056
1053
  export fun draw(g: G)
1057
1054
  ```
1058
1055
 
1059
- The host calls `init` once after the window exists, then `update` and `draw` every frame. A game imports `std/Game`, `std/Vec2` and `std/Color`; pure logic can import `std/Vec2` and `std/Color` alone. Under `plum test` and `plum run` the host functions are stubs that trap with `host function 'X' is not available here`, so `test` blocks work as long as they don't call the host.
1056
+ The page calls `init` once after the assets are loaded, then `update` and `draw` every frame. A game imports `std/Game`, `std/Vec2` and `std/Color`; pure logic can import `std/Vec2` and `std/Color` alone. Under `plum test` and `plum run` the host functions are stubs that trap with `host function 'X' is not available here`, so `test` blocks work as long as they don't call the host.
1060
1057
 
1061
- **Headless check:** `PLUM_GAME_CAPTURE=out.png plum-game game.wasm` runs `PLUM_GAME_FRAMES` frames (default 30), saves the framebuffer as an upright PNG and exits 0.
1058
+ **Headless tests:** the `plum-game` crate (a standalone crate, wasmtime only) runs a compiled game with no browser against a `Recorder` backend that logs every host call as a string and answers input from scripted values, so `cargo test --manifest-path plum-game/Cargo.toml` checks games' draw-call sequences (`plum-game/tests/host_test.rs`). `plum-game <wasm> [--assets DIR] [--frames N]` does the same from the command line and prints the host calls of the last frame.
1062
1059
 
1063
- **Browser check:** `cargo run --manifest-path plum-game/web-test/Cargo.toml -- plum-examples/game.plum --screenshot /tmp/orb-run.png` builds the web version, serves it and drives headless Chromium through [playwright-rs](https://crates.io/crates/playwright-rs): it fails if the canvas stays blank or stops changing, or the page logs an error, and reports whether audio started. One-time setup: `cargo run --manifest-path plum-game/web-test/Cargo.toml --example install-browsers`. See [`plum-game/web-test/README.md`](plum-game/web-test/README.md).
1060
+ **Browser check:** `cargo run --manifest-path plum-game/web-test/Cargo.toml -- plum-examples/game.plum --screenshot /tmp/orb-run.png` builds the static web version, serves it and drives headless Chromium (software WebGPU) through [playwright-rs](https://crates.io/crates/playwright-rs): it fails if the canvas stays blank or stops changing, or the page logs an error, and reports whether audio started. It also takes `--assets DIR`, `--require-audio`, `--measure SECS` and `--headed`. One-time setup: `cargo run --manifest-path plum-game/web-test/Cargo.toml --example install-browsers`. See [`plum-game/web-test/README.md`](plum-game/web-test/README.md); `docs/game.png` above was made with `--screenshot`. [`plum-game/web-test/BENCHMARK.md`](plum-game/web-test/BENCHMARK.md) records the renderer benchmark that led to this design.
1064
1061
 
1065
- **Asset root:** `plum-game <wasm> [--assets DIR]` reads textures and files relative to `DIR`, default `assets` beside the wasm (`$PLUM_GAME_ASSETS` overrides that default). `plum game file.plum` passes `assets` beside the source file. A path that escapes the root (`..` or absolute) fails like a missing file: `-1`, `""` or `false`.
1062
+ **Asset root:** textures and files are read relative to the asset root. A path that escapes the root (`..` or absolute) fails like a missing file: `-1`, `""` or `false`. The headless runner's root is `--assets` or `assets` beside the wasm; it reads image headers for `textureWidth`/`textureHeight` and records sounds without decoding them.
1066
1063
 
1067
- **Sounds:** macroquad decodes sounds asynchronously (always on the web), so the host decodes every `.wav` and `.ogg` file in the asset root before it calls `init` (natively it walks the asset directory, in the browser it uses the preloaded files); `soundLoad` then looks the decoded sound up by path with the same path-escape rules as `textureLoad`. Browsers only start audio after a user gesture (a key press or click); sounds played before that are silent, and macroquad's audio plugin resumes playback on the first gesture, so the example's music begins once you press a key.
1064
+ **Sounds:** the browser decodes every `.wav` and `.ogg` file of the asset root before `init`, and `soundLoad` looks the decoded sound up by path with the same path rules as `textureLoad`. Browsers only start audio after a user gesture (a key press or click); sounds played before that are silent, and the music of the example begins once you press a key.
1068
1065
 
1069
1066
  **Host API** (`std/Game`; texture and sound handles are `-1` on failure; coordinates are world units once a camera is set):
1070
1067
 
1071
1068
  | function | meaning |
1072
1069
  |---|---|
1073
- | `screenWidth()`, `screenHeight()`, `frameTime()` | window size in pixels, last frame time in seconds |
1070
+ | `screenWidth()`, `screenHeight()`, `frameTime()` | canvas size in pixels, last frame time in seconds |
1074
1071
  | `clear(c)` | fill the screen |
1075
1072
  | `drawRect(x, y, w, h, c)`, `drawCircle(x, y, r, c)`, `drawLine(x1, y1, x2, y2, thickness, c)`, `drawText(s, x, y, size, c)` | shapes and text (`x, y` is the text's top-left, a circle's centre) |
1076
1073
  | `setCamera(x, y, scale)`, `resetCamera()` | world point `(x, y)` maps to the screen's top-left, one world unit is `scale` pixels |