plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
README.md
# 👾 Plum
A statically typed, imperative programming language with algebraic data types, inspired by Rust and Gleam.
- Built on a [tree-sitter](plum-tooling/tree-sitter-plum) grammar, with syntax highlighting for Helix and VSCode out of the box (`plum editor helix` / `plum editor vscode`)
- Compiles to WebAssembly today, with plans to target amd64, arm64, and riscv64 via QBE (C-ABI compatible)
## Requirements
```sh
node >= 23.1.0
npm >= 10.9.0
qbe >= 1.2
clang >= 16.0.0
```
## Installing the `plum` CLI
```sh
cargo install --path plum-cli
```
Installs a release build as `plum` in `~/.cargo/bin`, which `cargo` puts on your `PATH` for you — no `sudo`/`/usr/local/bin` needed. Re-run this after pulling changes to the compiler; it doesn't auto-update. Use `cargo run -p plum-cli --` instead while actively developing the compiler itself.
## Layout and comments
Blocks are indentation-sensitive (2 spaces), like Python — there's no `{ }` for grouping statements. `#` starts a line comment.
```plum
# this is a comment
fun main() =
x = 1
if x > 0
x = x + 1
```
## Naming conventions
| Case | Used for |
|---|---|
| `PascalCase` | types, traits, enums |
| `camelCase` | functions, methods, fields |
| `snake_case` | variables, params |
| `SCREAMING_SNAKE_CASE` | top-level constants |
| single uppercase letter (`T`, `K`, `V`) | generic type parameters |
## Modules and imports
```plum
module basics
import std/io
```
## Constants
```plum
MAX_RETRIES = 3
PI = 3.14159
GREETING = "hello"
```
## Literals
```plum
dec = 42
hex = 0xFF
bin = 0b1010
big = 1_000_000
flt = 3.14
flt2 = 12.0f
exp = 6.022e23
name = "plum"
escaped = "line one\nline two\ttabbed \"quoted\""
yes = True
no = False
```
## Operators
From tightest to loosest binding:
| Precedence | Operators | Notes |
|---|---|---|
| highest | `.` `?.` | attribute access / method call, safe navigation |
| | `+` `-` | unary |
| | `*` `/` `%` | |
| | `+` `-` | |
| | `<<` `>>` | |
| | `^` | |
| | `&` | |
| | `\|` | |
| | `<` `<=` `==` `!=` `>=` `>` `<>` | comparisons |
| | `!` | boolean not |
| | `&&` | |
| | `\|\|` | |
| lowest | `? :` | ternary |
There's no range operator — `for i := range n` is the only way to iterate a numeric range. `{ }` is used to group a sub-expression (not `( )`, which is reserved for calls):
```plum
grouped = {1 + 2} * {3 - 1}
picked = cmp ? sum : bits
negated = -sum
inverted = !cmp
```
## Variables and assignment
```plum
x := 1
# declares a new binding — errors if `x` is already in scope
a, b := 1, 2
# multiple targets, positionally paired with multiple values
x = 2
# reassigns an existing binding — type-checked against x's declared type
```
## Control flow
```plum
if n < 0
return "negative"
else if n == 0
return "zero"
else
return "positive"
i := start
while i > 0
i = i - 1
for i := range limit
if i == 3
continue
if i == 8
break
total = total + i
assert n > 0 # traps at runtime if false
todo # traps at runtime — marks a body as not yet implemented
```
## Functions
```plum
fun addInts(a: Int, b: Int) -> Int =
a + b
fun greet() = # no return type => Unit
todo
fun withDefault(a: Int, step: Int = 1) -> Int =
a + step
fun sumAll(nums: ...Int) -> Int = # variadic param
todo
```
## Testing
Zig-style `test` blocks are a native language feature, compiled and run only by `plum test`:
```plum
fun add(a: Int, b: Int) -> Int =
a + b
test "add works"
assert add(1, 2) == 3
```
```sh
$ plum test plum-examples/testing.plum
└─ add works ✔
1 passed, 0 failed
```
## Types: records, traits, enums
There's a single declaration form, `enum` — a record type is just a single-variant enum whose one variant shares the enum's own name:
```plum
enum Point =
| Point(x: Int, y: Int)
enum Named(ToStr) = # implements ToStr
| Named(name: Str)
trait Shape =
area() -> Float
perimeter() -> Float
enum Color =
| Red
| Green
| Blue
enum Option =
| Some(Int) # unnamed positional payload — Some(5)
| None
enum Shape =
| Circle(radius: Int) # named payload fields — Circle(radius: 5) or Circle(5)
| Square(x: Int, y: Int)
enum Step(n: Int) = # a shared field on every variant ...
| ReadMin(10) # ... each variant supplies its own value for it
| ReadMax(20)
```
## Generics
```plum
enum Box[T] =
| Box(value: T)
trait Comparable[T: Ord] = # bounded generic param
compareTo(other: T) -> Int
fun wrap(value: T) -> Bool =
True
```
Generic types, methods, and free functions are monomorphized: each concrete instantiation used in the program gets its own specialized copy. `List[Int]` and `List(Int)` both parse as generic arguments.
## Closures
```plum
fun each(cb: fn(Int) -> Int) -> Int =
cb(5)
fun useCapturingClosure() -> Int =
offset := 100
cb := |v|
v + offset
cb(5)
fun main() -> Int =
each(|v| v * 3) + useCapturingClosure()
```
A closure literal is `|params| body`. Capture is snapshot-by-value: a closure copies the outer variables it references at creation time, not re-read live later. A `fn(...) -> T` type annotates a closure-typed parameter or field.
## `self`, field access, and methods
A `fun` declared indented directly inside an `enum` body is a method, with an implicit `self`:
```plum
enum Cat =
| Cat(name: Str, age: Int)
fun getAge(self) -> Int =
self.age
fun birthday(self) -> Int =
self.age + 1
fun main() -> Int =
c := Cat(name: "Whiskers", age: 3)
a := c.getAge()
w := Wrapper(inner: c, tag: 1)
w.inner.age # chained field access
```
## Constructing values
```plum
Cat(name: "Whiskers", age: 3)
```
Construct a record-shaped enum value by calling its name with `field: value` pairs (any order; every field required).
Fields are mutable (`c.age = c.age + 1`), and a Gleam-style spread updates a copy from an existing value, overriding just the fields you name:
```plum
older := Cat(..c, age: c.age + 1)
```
## `match`
```plum
match n
0 => 1
x =>
if x < 0
-1
else
2
match opt
Some(v) => v
None => 0
match book
FantasyBook(title, _, hasMythicalCreatures) when hasMythicalCreatures =>
"Fantasy book \"{title}\" features mythical creatures"
FantasyBook(title, _, _) => "Fantasy book \"{title}\" has no mythical creatures"
```
Patterns can be literals, a bare identifier (binds), a bare capitalized tag (compared), a constructor pattern (`Some(v)`, binding its argument), or `_` (wildcard). `match a, b, ...` supports multiple subjects, and a case can carry a `when <expr>` guard.
## String interpolation
```plum
fun greet(name: Str) -> Str =
"Hello, {name}!"
```
`{expr}` interpolates a variable, literal, attribute access, `&&`/`||`/comparison, ternary, or a closure-taking method call.
## `extern` functions and printing
```plum
extern fun printLn(s: Str)
```
`extern fun` declares a function backed by a host-provided wasm import, with no Plum body. `plum-std/Os.plum` declares `printLn` for use via `import std/os`.
```sh
$ plum run plum-examples/io.plum
hello from plum
```
## Error propagation
`expr?` unwraps a `Result`'s `Ok` or an `Option`'s `Some`, or exits the enclosing function early with the `Err`/`None` value as-is otherwise — Rust's `?`:
```plum
fun sumTwo(a: Str, b: Str) -> Result[Int, Str] =
x := parsePositive(a)?
y := parsePositive(b)?
return Ok(x + y)
```
`left ?: right` (elvis) is the plain-expression counterpart — no early return, just a fallback value if `left` is `Err`/`None`:
```plum
fun sumOrZero(a: Str, b: Str) -> Int =
x := parsePositive(a) ?: 0
y := parsePositive(b) ?: 0
x + y
```
`obj?.field` / `obj?.method(args)` (safe navigation, Groovy/Kotlin-style) reaches into a `Result`/`Option` value without unwrapping it — `Some`/`Ok` maps the field or method access over the inner value and re-wraps it, `None`/`Err` passes through untouched:
```plum
fun cityName(person: Option[Person]) -> Option[Str] =
person?.city?.name
```
It's sugar for `obj.map(|v| v.field)` — nothing more than that; chain it as many times as needed.
## Standard library highlights
- **`Bool`** (`import std/Bool`) — ordinary `enum Bool = | True | False`
- **`Str`** (`import std/Str`) — a byte array with a codepoint-aware layer for Unicode correctness: `runeLength`, `runeAt`, `codePointAt`, `codePointToStr`
- **`Byte`** / **`[]Byte`** (`import std/Byte`) — a scalar unsigned 8-bit value and a fixed-length mutable byte slice
- **`Buffer`** (`import std/Buffer`) — a growable, mutable byte sequence for building up a `Str`, amortized O(n) writes
- **`Array[T]`** (`import std/Array`) — a generic, growable array (reference types only)
- **`Map[K: Hashable, V]`** (`import std/Map`) — a real bucketed hash table
- **`List[T]`** (`import std/List`) — a singly-linked list with `get`/`add`/`set`/`removeAt`/`remove`/`each`/`map`/`reduce`/`sort`/`join`/`chunk`/`partition`
See [`plum-examples/`](plum-examples) for a runnable file per feature above, and [`plum-std/`](plum-std) for the standard library source.