plum

#treesitter#compiler#wasm

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

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


5617070 — Peter John 2026-11-02T14:02:19+05:30
T7: incidental files
plum-cli/tests/game_web_test.rs ADDED
@@ -0,0 +1,81 @@
1
+ #![allow(non_snake_case)]
2
+ use std::process::Command;
3
+
4
+ fn plumBin() -> std::path::PathBuf {
5
+ let mut path = std::env::current_exe().unwrap();
6
+ path.pop();
7
+ if path.ends_with("deps") {
8
+ path.pop();
9
+ }
10
+ path.push("plum");
11
+ path
12
+ }
13
+
14
+ #[test]
15
+ fn webBuildWritesTheDist() {
16
+ let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
17
+ .parent()
18
+ .unwrap();
19
+ let scratch = std::env::temp_dir().join(format!("plum_game_web_{}", std::process::id()));
20
+ let assets = scratch.join("assets");
21
+ let dist = scratch.join("dist");
22
+ // Stand-in asset files: the build copies them without reading them.
23
+ let sprites = [
24
+ "GUI_a", "GUIMain", "Houses_a", "Tileset", "Trees_a", "Units_a",
25
+ ];
26
+ std::fs::create_dir_all(assets.join("sprites")).unwrap();
27
+ std::fs::create_dir_all(assets.join("data/defines")).unwrap();
28
+ for s in sprites {
29
+ std::fs::write(assets.join(format!("sprites/{s}.rxx")), b"x").unwrap();
30
+ }
31
+ std::fs::write(assets.join("data/defines/mapelem.dat"), b"x").unwrap();
32
+ std::fs::write(assets.join("extra.map"), b"x").unwrap();
33
+
34
+ let out = Command::new(plumBin())
35
+ .current_dir(root)
36
+ .args([
37
+ "game",
38
+ "--web",
39
+ "plum-game/tests/fixtures/host_fixture.plum",
40
+ "-o",
41
+ ])
42
+ .arg(&dist)
43
+ .arg("--assets")
44
+ .arg(&assets)
45
+ .args(["--preload", "extra.map"])
46
+ .output()
47
+ .expect("failed to run plum");
48
+ let all = format!(
49
+ "{}{}",
50
+ String::from_utf8_lossy(&out.stdout),
51
+ String::from_utf8_lossy(&out.stderr)
52
+ );
53
+ assert!(out.status.success(), "plum game --web failed:\n{all}");
54
+
55
+ for f in [
56
+ "index.html",
57
+ "mq_js_bundle.js",
58
+ "plum_bridge.js",
59
+ "plum-game.wasm",
60
+ "plum.wasm",
61
+ "preload.txt",
62
+ ] {
63
+ assert!(dist.join(f).is_file(), "missing {f}");
64
+ }
65
+ let preload = std::fs::read_to_string(dist.join("preload.txt")).unwrap();
66
+ let lines: Vec<&str> = preload.lines().collect();
67
+ for s in sprites {
68
+ assert!(
69
+ lines.contains(&format!("sprites/{s}.rxx").as_str()),
70
+ "{s} not preloaded"
71
+ );
72
+ }
73
+ assert!(lines.contains(&"extra.map"));
74
+ for l in &lines {
75
+ assert!(
76
+ dist.join("assets").join(l).is_file(),
77
+ "assets/{l} not copied"
78
+ );
79
+ }
80
+ let _ = std::fs::remove_dir_all(&scratch);
81
+ }
plum-game/web/bridge_check.mjs ADDED
@@ -0,0 +1,74 @@
1
+ // Usage: node bridge_check.mjs <plum.wasm>
2
+ //
3
+ // Loads a compiled Plum game and checks that plum_bridge.js satisfies every
4
+ // import, that every pg_* export it calls exists in src/web.rs, and that the
5
+ // module runs init/update/draw against a stub host.
6
+ import { readFileSync } from "node:fs";
7
+ import { dirname, join } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ import vm from "node:vm";
10
+ import assert from "node:assert/strict";
11
+
12
+ const here = dirname(fileURLToPath(import.meta.url));
13
+ const wasmPath = process.argv[2];
14
+ if (!wasmPath) {
15
+ console.error("usage: node bridge_check.mjs <plum.wasm>");
16
+ process.exit(2);
17
+ }
18
+
19
+ vm.runInThisContext(readFileSync(join(here, "plum_bridge.js"), "utf8"));
20
+ const { makeImports, hostNames } = globalThis.PlumBridge;
21
+
22
+ // Every pg_* name the plugin calls is exported by the Rust host.
23
+ const webRs = readFileSync(join(here, "../src/web.rs"), "utf8");
24
+ const exported = new Set([...webRs.matchAll(/extern "C" fn (pg_\w+)/g)].map((m) => m[1]));
25
+ for (const n of hostNames) assert.ok(exported.has(n), `web.rs does not export ${n}`);
26
+
27
+ const module = await WebAssembly.compile(readFileSync(wasmPath));
28
+ const imports = WebAssembly.Module.imports(module).filter((i) => i.module === "plum");
29
+ const importObject = makeImports(module, stubHost(), { plum: null });
30
+ const missing = imports.filter((i) => typeof importObject.plum[i.name] !== "function");
31
+ assert.deepEqual(missing, [], "plum imports without a binding");
32
+
33
+ // Stub host: records calls; handles load as failures.
34
+ function stubHost() {
35
+ const memory = new WebAssembly.Memory({ initial: 4 });
36
+ let bump = 1024;
37
+ const calls = [];
38
+ const host = new Proxy(
39
+ { memory, calls, loaded: [] },
40
+ {
41
+ get(t, name) {
42
+ if (name in t) return t[name];
43
+ if (name === "pg_alloc") return (n) => ((bump += Math.max(n, 1)), bump - Math.max(n, 1));
44
+ if (name === "pg_free") return () => {};
45
+ return (...a) => {
46
+ calls.push([name, ...a]);
47
+ if (/load$/.test(name)) {
48
+ // (ptr, len): the Str arrived intact in host memory.
49
+ t.loaded.push(new TextDecoder().decode(new Uint8Array(memory.buffer, a[0], a[1])));
50
+ return -1;
51
+ }
52
+ if (/screen_(width|height)/.test(name)) return 800;
53
+ return 0;
54
+ };
55
+ },
56
+ },
57
+ );
58
+ return host;
59
+ }
60
+
61
+ const host = stubHost();
62
+ const ctx = { plum: null };
63
+ const instance = await WebAssembly.instantiate(module, makeImports(module, host, ctx));
64
+ ctx.plum = instance.exports;
65
+ for (const n of ["strLen", "strByte", "strNew", "strSetByte", "init", "update", "draw"]) {
66
+ assert.equal(typeof ctx.plum[n], "function", `plum.wasm has no ${n} export`);
67
+ }
68
+ const state = ctx.plum.init();
69
+ ctx.plum.update(state, 0.016);
70
+ ctx.plum.draw(state);
71
+ assert.ok(host.calls.length > 0, "draw made no host calls");
72
+ const loads = host.loaded;
73
+ assert.ok(loads.every((s) => /^[\w .\/-]+$/.test(s)), `garbled Str: ${JSON.stringify(loads)}`);
74
+ console.log(`ok: ${imports.length} plum imports bound, ${host.calls.length} host calls in one frame (loads: ${JSON.stringify(loads)})`);