plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
0e1801a
— Peter John
2026-07-19T21:52:28+05:30
test: add example programs covering the language's current syntax
- examples/basics.plum +28 -0
- examples/control_flow.plum +29 -0
- examples/functions.plum +17 -0
- examples/match.plum +45 -0
- examples/methods.plum +26 -0
- examples/strings.plum +11 -0
- examples/types.plum +25 -0
- plum-checker/tests/examples_test.rs +48 -0
- plum-wasm-codegen/tests/examples_test.rs +85 -0
examples/basics.plum
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module basics
|
|
2
|
+
|
|
3
|
+
import std/io
|
|
4
|
+
|
|
5
|
+
MAX_RETRIES = 3
|
|
6
|
+
PI = 3.14159
|
|
7
|
+
GREETING = "hello"
|
|
8
|
+
|
|
9
|
+
main() =
|
|
10
|
+
dec = 42
|
|
11
|
+
hex = 0xFF
|
|
12
|
+
bin = 0b1010
|
|
13
|
+
flt = 3.14
|
|
14
|
+
flt2 = 12.0f
|
|
15
|
+
name = "plum"
|
|
16
|
+
yes = True
|
|
17
|
+
no = False
|
|
18
|
+
|
|
19
|
+
sum = 1 + 2 * 3 - 4 / 2
|
|
20
|
+
bits = 0b1100 & 0b1010 | 0b0001
|
|
21
|
+
shifted = 1 << 4
|
|
22
|
+
cmp = sum > 0 && bits != 0 || False
|
|
23
|
+
grouped = {1 + 2} * {3 - 1}
|
|
24
|
+
picked = cmp ? sum : bits
|
|
25
|
+
negated = -sum
|
|
26
|
+
positive = +sum
|
|
27
|
+
inverted = !cmp
|
|
28
|
+
a, b = 1, 2
|
examples/control_flow.plum
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
loopSum(limit: Int) -> Int =
|
|
2
|
+
total = 0
|
|
3
|
+
for i in 0..limit
|
|
4
|
+
if i == 3
|
|
5
|
+
continue
|
|
6
|
+
if i == 8
|
|
7
|
+
break
|
|
8
|
+
total = total + i
|
|
9
|
+
return total
|
|
10
|
+
|
|
11
|
+
classify(n: Int) -> Str =
|
|
12
|
+
if n < 0
|
|
13
|
+
return "negative"
|
|
14
|
+
else if n == 0
|
|
15
|
+
return "zero"
|
|
16
|
+
else
|
|
17
|
+
return "positive"
|
|
18
|
+
|
|
19
|
+
countdown(start: Int) -> Int =
|
|
20
|
+
i = start
|
|
21
|
+
while i > 0
|
|
22
|
+
i = i - 1
|
|
23
|
+
return i
|
|
24
|
+
|
|
25
|
+
placeholder() =
|
|
26
|
+
todo
|
|
27
|
+
|
|
28
|
+
checkPositive(n: Int) =
|
|
29
|
+
assert n > 0
|
examples/functions.plum
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
addInts(a: Int, b: Int) -> Int =
|
|
2
|
+
a + b
|
|
3
|
+
|
|
4
|
+
greet() =
|
|
5
|
+
todo
|
|
6
|
+
|
|
7
|
+
withDefault(a: Int, step: Int = 1) -> Int =
|
|
8
|
+
a + step
|
|
9
|
+
|
|
10
|
+
sumAll(nums: ...Int) -> Int =
|
|
11
|
+
todo
|
|
12
|
+
|
|
13
|
+
wrap(value: a) -> Bool =
|
|
14
|
+
True
|
|
15
|
+
|
|
16
|
+
pair(first: a, second: b) -> Bool =
|
|
17
|
+
True
|
examples/match.plum
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
enum Color =
|
|
2
|
+
| Red
|
|
3
|
+
| Green
|
|
4
|
+
| Blue
|
|
5
|
+
|
|
6
|
+
enum Option =
|
|
7
|
+
| Some(Int)
|
|
8
|
+
| None
|
|
9
|
+
|
|
10
|
+
describeNumber(n: Int) -> Str =
|
|
11
|
+
match n
|
|
12
|
+
0 =>
|
|
13
|
+
"zero"
|
|
14
|
+
1 =>
|
|
15
|
+
"one"
|
|
16
|
+
_ =>
|
|
17
|
+
"many"
|
|
18
|
+
|
|
19
|
+
describeBool(b: Bool) -> Int =
|
|
20
|
+
match b
|
|
21
|
+
True =>
|
|
22
|
+
1
|
|
23
|
+
False =>
|
|
24
|
+
0
|
|
25
|
+
|
|
26
|
+
bindExample(n: Int) -> Int =
|
|
27
|
+
match n
|
|
28
|
+
x =>
|
|
29
|
+
x
|
|
30
|
+
|
|
31
|
+
describeColor(c: Color) -> Str =
|
|
32
|
+
match c
|
|
33
|
+
Red =>
|
|
34
|
+
"red"
|
|
35
|
+
Green =>
|
|
36
|
+
"green"
|
|
37
|
+
Blue =>
|
|
38
|
+
"blue"
|
|
39
|
+
|
|
40
|
+
describeOption(opt: Option) -> Int =
|
|
41
|
+
match opt
|
|
42
|
+
Some(v) =>
|
|
43
|
+
v
|
|
44
|
+
None =>
|
|
45
|
+
0
|
examples/methods.plum
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
type Cat =
|
|
2
|
+
name: Str
|
|
3
|
+
age: Int
|
|
4
|
+
|
|
5
|
+
getAge<Cat>() -> Int =
|
|
6
|
+
self.age
|
|
7
|
+
|
|
8
|
+
birthday<Cat>() -> Int =
|
|
9
|
+
self.age + 1
|
|
10
|
+
|
|
11
|
+
type Wrapper =
|
|
12
|
+
inner: Cat
|
|
13
|
+
tag: Int
|
|
14
|
+
|
|
15
|
+
innerAge<Wrapper>() -> Int =
|
|
16
|
+
self.inner.age
|
|
17
|
+
|
|
18
|
+
makeCat() -> Cat =
|
|
19
|
+
Cat(name: "Whiskers", age: 3)
|
|
20
|
+
|
|
21
|
+
main() -> Int =
|
|
22
|
+
c = makeCat()
|
|
23
|
+
a = c.getAge()
|
|
24
|
+
b = c.birthday()
|
|
25
|
+
w = Wrapper(inner: c, tag: 1)
|
|
26
|
+
a + b + w.innerAge()
|
examples/strings.plum
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
greet(name: Str) -> Str =
|
|
2
|
+
"Hello, {name}!"
|
|
3
|
+
|
|
4
|
+
report(count: Int, total: Int) -> Str =
|
|
5
|
+
"{count} of {total} complete"
|
|
6
|
+
|
|
7
|
+
empty() -> Str =
|
|
8
|
+
""
|
|
9
|
+
|
|
10
|
+
escaped() -> Str =
|
|
11
|
+
"line one\nline two\ttabbed \"quoted\""
|
examples/types.plum
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
type Point =
|
|
2
|
+
x: Int
|
|
3
|
+
y: Int
|
|
4
|
+
|
|
5
|
+
type Named(Stringable) =
|
|
6
|
+
name: Str
|
|
7
|
+
|
|
8
|
+
type Box(a) =
|
|
9
|
+
value: a
|
|
10
|
+
|
|
11
|
+
trait Shape =
|
|
12
|
+
area() -> Float
|
|
13
|
+
perimeter() -> Float
|
|
14
|
+
|
|
15
|
+
trait Comparable(a: Ord) =
|
|
16
|
+
compareTo(other: a) -> Int
|
|
17
|
+
|
|
18
|
+
enum Color =
|
|
19
|
+
| Red
|
|
20
|
+
| Green
|
|
21
|
+
| Blue
|
|
22
|
+
|
|
23
|
+
enum Option =
|
|
24
|
+
| Some(Int)
|
|
25
|
+
| None
|
plum-checker/tests/examples_test.rs
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
use plum_checker::check_source;
|
|
2
|
+
use plum_core::AstParser;
|
|
3
|
+
|
|
4
|
+
fn examples_dir() -> std::path::PathBuf {
|
|
5
|
+
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples")
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
fn example_files() -> Vec<std::path::PathBuf> {
|
|
9
|
+
let mut files: Vec<_> = std::fs::read_dir(examples_dir())
|
|
10
|
+
.expect("examples/ directory should exist")
|
|
11
|
+
.filter_map(|e| e.ok())
|
|
12
|
+
.map(|e| e.path())
|
|
13
|
+
.filter(|p| p.extension().and_then(|e| e.to_str()) == Some("plum"))
|
|
14
|
+
.collect();
|
|
15
|
+
files.sort();
|
|
16
|
+
assert!(!files.is_empty(), "examples/ should contain at least one .plum file");
|
|
17
|
+
files
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// Every example must parse with zero ERROR/MISSING nodes and pass `check_source`.
|
|
21
|
+
/// These files exist specifically to prove each piece of the currently-supported
|
|
22
|
+
/// grammar surface actually works end to end, not just in isolated unit tests.
|
|
23
|
+
#[test]
|
|
24
|
+
fn every_example_parses_and_type_checks() {
|
|
25
|
+
let mut parser = tree_sitter::Parser::new();
|
|
26
|
+
parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap();
|
|
27
|
+
|
|
28
|
+
for path in example_files() {
|
|
29
|
+
let src = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("failed to read {}: {}", path.display(), e));
|
|
30
|
+
let tree = parser.parse(&src, None).unwrap_or_else(|| panic!("failed to parse {}", path.display()));
|
|
31
|
+
assert!(
|
|
32
|
+
!tree.root_node().has_error(),
|
|
33
|
+
"{} has a parse error:\n{}",
|
|
34
|
+
path.display(),
|
|
35
|
+
tree.root_node().to_sexp()
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
let ap = AstParser::new(&src);
|
|
39
|
+
let source = ap.parse_source(tree.root_node());
|
|
40
|
+
let result = check_source(&source);
|
|
41
|
+
assert!(
|
|
42
|
+
result.is_ok(),
|
|
43
|
+
"{} failed type checking: {:?}",
|
|
44
|
+
path.display(),
|
|
45
|
+
result.err().map(|errs| errs.into_iter().map(|e| e.message).collect::<Vec<_>>())
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
plum-wasm-codegen/tests/examples_test.rs
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
use plum_wasm_codegen::compile_source;
|
|
2
|
+
use plum_core::AstParser;
|
|
3
|
+
|
|
4
|
+
fn examples_dir() -> std::path::PathBuf {
|
|
5
|
+
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples")
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
fn parse_file(name: &str) -> plum_core::ast::Source {
|
|
9
|
+
let path = examples_dir().join(name);
|
|
10
|
+
let src = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("failed to read {}: {}", path.display(), e));
|
|
11
|
+
let mut parser = tree_sitter::Parser::new();
|
|
12
|
+
parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap();
|
|
13
|
+
let tree = parser.parse(&src, None).unwrap_or_else(|| panic!("failed to parse {}", path.display()));
|
|
14
|
+
assert!(!tree.root_node().has_error(), "{} has a parse error", path.display());
|
|
15
|
+
let ap = AstParser::new(&src);
|
|
16
|
+
ap.parse_source(tree.root_node())
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
fn assert_compiles(name: &str) -> Vec<u8> {
|
|
20
|
+
let source = parse_file(name);
|
|
21
|
+
let bytes = compile_source(&source).unwrap_or_else(|e| panic!("{} failed to compile: {}", name, e));
|
|
22
|
+
let result = wasmparser::validate(&bytes);
|
|
23
|
+
assert!(result.is_ok(), "{} produced invalid wasm: {:?}", name, result.err());
|
|
24
|
+
bytes
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Examples that stick to currently-supported codegen features (primitives, control
|
|
28
|
+
/// flow, classes/methods) must actually compile to valid wasm — not just parse and
|
|
29
|
+
/// type-check.
|
|
30
|
+
#[test]
|
|
31
|
+
fn basics_compiles() {
|
|
32
|
+
assert_compiles("basics.plum");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#[test]
|
|
36
|
+
fn control_flow_compiles() {
|
|
37
|
+
assert_compiles("control_flow.plum");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#[test]
|
|
41
|
+
fn functions_compiles() {
|
|
42
|
+
assert_compiles("functions.plum");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#[test]
|
|
46
|
+
fn types_compiles() {
|
|
47
|
+
// Only class/trait/enum declarations, no function bodies to lower — should still
|
|
48
|
+
// produce a valid (if unexciting) module.
|
|
49
|
+
assert_compiles("types.plum");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#[test]
|
|
53
|
+
fn methods_compiles_and_runs_correctly() {
|
|
54
|
+
let bytes = assert_compiles("methods.plum");
|
|
55
|
+
|
|
56
|
+
let engine = wasmtime::Engine::default();
|
|
57
|
+
let module = wasmtime::Module::new(&engine, &bytes).expect("module should be loadable");
|
|
58
|
+
let mut store = wasmtime::Store::new(&engine, ());
|
|
59
|
+
let instance = wasmtime::Instance::new(&mut store, &module, &[]).expect("module should instantiate");
|
|
60
|
+
let main = instance
|
|
61
|
+
.get_typed_func::<(), i64>(&mut store, "main")
|
|
62
|
+
.expect("main should have signature () -> i64");
|
|
63
|
+
let result = main.call(&mut store, ()).expect("main should not trap");
|
|
64
|
+
// makeCat() -> Cat(age: 3); a = getAge() = 3; b = birthday() = 4;
|
|
65
|
+
// w.innerAge() = inner.age = 3 => 3 + 4 + 3 = 10
|
|
66
|
+
assert_eq!(result, 10);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// match.plum and strings.plum intentionally exercise syntax beyond what codegen
|
|
70
|
+
/// currently lowers (non-Bool enum-tag/constructor match patterns, string
|
|
71
|
+
/// interpolation) — they must fail loudly with a clear message, not silently
|
|
72
|
+
/// produce wrong wasm.
|
|
73
|
+
#[test]
|
|
74
|
+
fn match_example_reports_clear_unsupported_pattern_errors() {
|
|
75
|
+
let source = parse_file("match.plum");
|
|
76
|
+
let err = compile_source(&source).expect_err("non-Bool enum-tag patterns are not yet supported");
|
|
77
|
+
assert!(err.contains("enum variant pattern"), "got: {}", err);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#[test]
|
|
81
|
+
fn strings_example_reports_clear_interpolation_error() {
|
|
82
|
+
let source = parse_file("strings.plum");
|
|
83
|
+
let err = compile_source(&source).expect_err("string interpolation is not yet supported");
|
|
84
|
+
assert!(err.contains("interpolation"), "got: {}", err);
|
|
85
|
+
}
|