plum

#treesitter#compiler#wasm

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

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


1e95168 — Peter John 2026-11-02T13:45:48+05:30
T2: std Game API + import stubs
plum-cli/src/main.rs CHANGED
@@ -576,6 +576,24 @@ fn hostImports(
576
576
  },
577
577
  )))
578
578
  }
579
+ // A `plum` import this host doesn't define (the game API of `std/Game`)
580
+ // is stubbed with a function that traps when called, so a program
581
+ // that imports it can still run or test code that never calls it.
582
+ ("plum", n) => {
583
+ let func_ty = imp.ty().func().cloned().ok_or_else(|| {
584
+ anyhow::anyhow!("plum::{n} import must be a function")
585
+ })?;
586
+ let name = n.to_string();
587
+ Ok(wasmtime::Extern::Func(wasmtime::Func::new(
588
+ &mut *store,
589
+ func_ty,
590
+ move |_caller, _params, _results| {
591
+ Err(wasmtime::Error::msg(format!(
592
+ "host function '{name}' is not available here"
593
+ )))
594
+ },
595
+ )))
596
+ }
579
597
  (m, n) => anyhow::bail!("unsupported host import {m}::{n}"),
580
598
  }
581
599
  })
@@ -970,7 +988,7 @@ fn runTestFile(file: &std::path::Path, lib_path: &std::path::Path) -> Result<(us
970
988
  }
971
989
  }
972
990
  Err(trap) => {
973
- println!("{branch} {name} {} PANIC: {trap}", red("\u{2718}"));
991
+ println!("{branch} {name} {} PANIC: {trap:#}", red("\u{2718}"));
974
992
  failed += 1;
975
993
  }
976
994
  }
plum-cli/tests/host_stub_test.rs ADDED
@@ -0,0 +1,66 @@
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
+ fn runPlumTest(name: &str, source: &str) -> (bool, String) {
15
+ let dir = std::env::temp_dir().join(format!("plum_stub_{}_{}", name, std::process::id()));
16
+ std::fs::create_dir_all(&dir).unwrap();
17
+ let src = dir.join("game.plum");
18
+ std::fs::write(&src, source).unwrap();
19
+ let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
20
+ .parent()
21
+ .unwrap();
22
+ let out = Command::new(plumBin())
23
+ .current_dir(root)
24
+ .args(["test", src.to_str().unwrap()])
25
+ .output()
26
+ .expect("failed to run plum");
27
+ let all = format!(
28
+ "{}{}",
29
+ String::from_utf8_lossy(&out.stdout),
30
+ String::from_utf8_lossy(&out.stderr)
31
+ );
32
+ (out.status.success(), all)
33
+ }
34
+
35
+ #[test]
36
+ fn stubbed_host_import_unused_test_passes() {
37
+ let (ok, all) = runPlumTest(
38
+ "pass",
39
+ "import std/Int\nimport std/Game\n\ntest \"pure\"\n assert 1 + 1 == 2\n",
40
+ );
41
+ assert!(ok, "output: {all}");
42
+ assert!(all.contains("1 passed, 0 failed"), "output: {all}");
43
+ }
44
+
45
+ #[test]
46
+ fn stubbed_host_import_traps_when_called() {
47
+ let (ok, all) = runPlumTest(
48
+ "trap",
49
+ "import std/Int\nimport std/Float\nimport std/Game\n\ntest \"calls host\"\n assert screenWidth() > 0.0\n",
50
+ );
51
+ assert!(!ok, "output: {all}");
52
+ assert!(
53
+ all.contains("host function 'screenWidth' is not available here"),
54
+ "output: {all}"
55
+ );
56
+ }
57
+
58
+ #[test]
59
+ fn real_host_functions_still_work_next_to_stubs() {
60
+ let (ok, all) = runPlumTest(
61
+ "real",
62
+ "import std/Int\nimport std/Str\nimport std/Os\nimport std/Game\n\ntest \"real\"\n printLn(\"hello\")\n assert 1 == 1\n",
63
+ );
64
+ assert!(ok, "output: {all}");
65
+ assert!(all.contains("hello"), "output: {all}");
66
+ }
plum-std/Color.plum ADDED
@@ -0,0 +1,35 @@
1
+ module std
2
+ import std/Float
3
+
4
+ # An RGBA colour with each channel in 0..1.
5
+ struct Color(r: Float, g: Float, b: Float, a: Float) =
6
+
7
+ White = Color(r: 1.0, g: 1.0, b: 1.0, a: 1.0)
8
+ Black = Color(r: 0.0, g: 0.0, b: 0.0, a: 1.0)
9
+ Red = Color(r: 1.0, g: 0.0, b: 0.0, a: 1.0)
10
+ Green = Color(r: 0.0, g: 1.0, b: 0.0, a: 1.0)
11
+ Blue = Color(r: 0.0, g: 0.0, b: 1.0, a: 1.0)
12
+ Yellow = Color(r: 1.0, g: 1.0, b: 0.0, a: 1.0)
13
+ Transparent = Color(r: 0.0, g: 0.0, b: 0.0, a: 0.0)
14
+
15
+ # An opaque colour from its red, green and blue channels.
16
+ fun rgb(r: Float, g: Float, b: Float) -> Color =
17
+ Color(r: r, g: g, b: b, a: 1.0)
18
+
19
+ fun withAlpha(self, a: Float) -> Color =
20
+ Color(r: self.r, g: self.g, b: self.b, a: a)
21
+
22
+ fun eq(self, o: Color) -> Bool =
23
+ self.r == o.r && self.g == o.g && self.b == o.b && self.a == o.a
24
+
25
+ test "Color.rgb is opaque and withAlpha replaces only alpha"
26
+ assert Color.rgb(1.0, 0.5, 0.0) == Color(r: 1.0, g: 0.5, b: 0.0, a: 1.0)
27
+ assert Color.Red.withAlpha(0.25) == Color(r: 1.0, g: 0.0, b: 0.0, a: 0.25)
28
+
29
+ test "Color constants"
30
+ assert Color.White.r == 1.0
31
+ assert Color.Black.a == 1.0
32
+ assert Color.Transparent.a == 0.0
33
+ assert Color.Yellow.g == 1.0
34
+ assert Color.Green.g == 1.0
35
+ assert Color.Blue.b == 1.0
plum-std/Game.plum ADDED
@@ -0,0 +1,112 @@
1
+ module std
2
+ import std/Int
3
+ import std/Float
4
+ import std/Str
5
+ import std/Vec2
6
+ import std/Color
7
+
8
+ # The host API of a game: drawing, input, sprite atlases and KaM maps, all
9
+ # provided by the game host (`plum game`). Handles (`atlasLoad`, `mapLoad`)
10
+ # are -1 on failure. Coordinates are world units once `setCamera` is set.
11
+
12
+ extern fun screenWidth() -> Float
13
+ extern fun screenHeight() -> Float
14
+ extern fun frameTime() -> Float
15
+
16
+ extern fun clear(c: Color)
17
+ extern fun drawRect(x: Float, y: Float, w: Float, h: Float, c: Color)
18
+ extern fun drawLine(x1: Float, y1: Float, x2: Float, y2: Float, thickness: Float, c: Color)
19
+ extern fun drawText(s: Str, x: Float, y: Float, size: Float, c: Color)
20
+
21
+ extern fun setCamera(x: Float, y: Float, scale: Float)
22
+ extern fun resetCamera()
23
+
24
+ extern fun atlasLoad(name: Str) -> Int
25
+ extern fun spriteCount(atlas: Int) -> Int
26
+ extern fun spriteW(atlas: Int, idx: Int) -> Int
27
+ extern fun spriteH(atlas: Int, idx: Int) -> Int
28
+ extern fun spritePivotX(atlas: Int, idx: Int) -> Int
29
+ extern fun spritePivotY(atlas: Int, idx: Int) -> Int
30
+ extern fun spriteColor(atlas: Int, idx: Int) -> Color
31
+
32
+ extern fun drawSprite(atlas: Int, idx: Int, x: Float, y: Float, flipX: Bool, tint: Color)
33
+ extern fun drawTile(atlas: Int, idx: Int, x: Float, y: Float, size: Float, quarterTurns: Int, tint: Color)
34
+
35
+ extern fun isKeyDown(k: Int) -> Bool
36
+ extern fun isKeyPressed(k: Int) -> Bool
37
+ extern fun mousePos() -> Vec2
38
+ extern fun isMouseDown(b: Int) -> Bool
39
+ extern fun isMousePressed(b: Int) -> Bool
40
+ extern fun mouseWheel() -> Float
41
+
42
+ extern fun mapLoad(path: Str) -> Int
43
+ extern fun mapWidth(m: Int) -> Int
44
+ extern fun mapHeight(m: Int) -> Int
45
+ extern fun mapTerrain(m: Int, x: Int, y: Int) -> Int
46
+ extern fun mapRotation(m: Int, x: Int, y: Int) -> Int
47
+ extern fun mapElevation(m: Int, x: Int, y: Int) -> Int
48
+ extern fun mapObject(m: Int, x: Int, y: Int) -> Int
49
+
50
+ # Key codes (GLFW numbering) for `isKeyDown` / `isKeyPressed`.
51
+ # Letter keys are `KeyA`..`KeyZ` (a constant name needs two characters).
52
+ primitive Key =
53
+ Space = 32
54
+ KeyA = 65
55
+ KeyB = 66
56
+ KeyC = 67
57
+ KeyD = 68
58
+ KeyE = 69
59
+ KeyF = 70
60
+ KeyG = 71
61
+ KeyH = 72
62
+ KeyI = 73
63
+ KeyJ = 74
64
+ KeyK = 75
65
+ KeyL = 76
66
+ KeyM = 77
67
+ KeyN = 78
68
+ KeyO = 79
69
+ KeyP = 80
70
+ KeyQ = 81
71
+ KeyR = 82
72
+ KeyS = 83
73
+ KeyT = 84
74
+ KeyU = 85
75
+ KeyV = 86
76
+ KeyW = 87
77
+ KeyX = 88
78
+ KeyY = 89
79
+ KeyZ = 90
80
+ Num0 = 48
81
+ Num1 = 49
82
+ Num2 = 50
83
+ Num3 = 51
84
+ Num4 = 52
85
+ Num5 = 53
86
+ Num6 = 54
87
+ Num7 = 55
88
+ Num8 = 56
89
+ Num9 = 57
90
+ Escape = 256
91
+ Enter = 257
92
+ Tab = 258
93
+ Backspace = 259
94
+ Right = 262
95
+ Left = 263
96
+ Down = 264
97
+ Up = 265
98
+ LeftShift = 340
99
+ LeftControl = 341
100
+
101
+ # Mouse button numbers for `isMouseDown` / `isMousePressed`.
102
+ primitive Mouse =
103
+ Left = 0
104
+ Right = 1
105
+ Middle = 2
106
+
107
+ test "key and mouse constants use GLFW numbering"
108
+ assert Key.KeyW == 87
109
+ assert Key.Left == 263
110
+ assert Key.Num0 == 48
111
+ assert Key.LeftControl == 341
112
+ assert Mouse.Right == 1
plum-std/Vec2.plum ADDED
@@ -0,0 +1,43 @@
1
+ module std
2
+ import std/Float
3
+ import std/Math
4
+
5
+ # A 2D point or vector. Operators work through the methods below:
6
+ # `a + b` is `a.add(b)`, `a - b` is `a.sub(b)`, `a * s` is `a.mul(s)`,
7
+ # `-a` is `a.neg()` and `a == b` is `a.eq(b)`.
8
+ struct Vec2(x: Float, y: Float) =
9
+
10
+ Zero = Vec2(x: 0.0, y: 0.0)
11
+
12
+ fun add(self, o: Vec2) -> Vec2 =
13
+ Vec2(x: self.x + o.x, y: self.y + o.y)
14
+
15
+ fun sub(self, o: Vec2) -> Vec2 =
16
+ Vec2(x: self.x - o.x, y: self.y - o.y)
17
+
18
+ fun mul(self, s: Float) -> Vec2 =
19
+ Vec2(x: self.x * s, y: self.y * s)
20
+
21
+ fun neg(self) -> Vec2 =
22
+ Vec2(x: 0.0 - self.x, y: 0.0 - self.y)
23
+
24
+ fun eq(self, o: Vec2) -> Bool =
25
+ self.x == o.x && self.y == o.y
26
+
27
+ fun length(self) -> Float =
28
+ Math.sqrt(self.x * self.x + self.y * self.y)
29
+
30
+ fun distance(self, o: Vec2) -> Float =
31
+ self.sub(o).length()
32
+
33
+ test "Vec2 operators go through add, sub, mul, neg and eq"
34
+ r := Vec2(x: 1.0, y: 2.0) + Vec2(x: 3.0, y: 4.0) * 2.0
35
+ assert r == Vec2(x: 7.0, y: 10.0)
36
+ assert Vec2(x: 5.0, y: 5.0) - Vec2(x: 1.0, y: 2.0) == Vec2(x: 4.0, y: 3.0)
37
+ assert -Vec2(x: 1.0, y: -2.0) == Vec2(x: -1.0, y: 2.0)
38
+ assert Vec2(x: 1.0, y: 2.0) != Vec2(x: 1.0, y: 3.0)
39
+
40
+ test "Vec2 length, distance and Zero"
41
+ assert Vec2(x: 3.0, y: 4.0).length() == 5.0
42
+ assert Vec2(x: 1.0, y: 1.0).distance(Vec2(x: 4.0, y: 5.0)) == 5.0
43
+ assert Vec2.Zero == Vec2(x: 0.0, y: 0.0)