plum
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 +19 -1
- plum-cli/tests/host_stub_test.rs +66 -0
- plum-std/Color.plum +35 -0
- plum-std/Game.plum +112 -0
- plum-std/Vec2.plum +43 -0
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)
|