plum

#treesitter#compiler#wasm

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

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


README.md
61ab95d 1
# 👾 Plum
c8e4165 2
61ab95d 3
A statically typed, imperative programming language with algebraic data types, inspired by Rust and Gleam.
fa31ba4 4
900c685 5
- 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`)
61ab95d 6
- Compiles to WebAssembly today, with plans to target amd64, arm64, and riscv64 via QBE (C-ABI compatible)
fa31ba4 7
fa31ba4 8
## Requirements
61ab95d 9
fa31ba4 10
```sh
fa31ba4 11
node >= 23.1.0
fa31ba4 12
npm >= 10.9.0
fa31ba4 13
qbe >= 1.2
fa31ba4 14
clang >= 16.0.0
222801d 15
```
222801d 16
2236941 17
## Installing the `plum` CLI
2236941 18
2236941 19
```sh
2236941 20
cargo install --path plum-cli
2236941 21
```
2236941 22
2236941 23
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.
2236941 24
a473b02 25
### Prebuilt binaries
a473b02 26
a473b02 27
No Rust toolchain needed — download the `plum` binary directly (it embeds `plum-lsp`, the tree-sitter grammar, and the VSCode extension bundle; `plum editor helix`/`plum editor vscode` extract them on install):
a473b02 28
a473b02 29
<!-- prebuilt-binaries:start -->
a473b02 30
| Platform | Download |
a473b02 31
| --- | --- |
a473b02 32
| macOS (Apple Silicon) | [plum-darwin-arm64](https://git.pyrossh.dev/releases/plum-cli/v0.1.0/plum-darwin-arm64) |
a473b02 33
| Linux (x86_64) | [plum-linux-x86_64](https://git.pyrossh.dev/releases/plum-cli/v0.1.0/plum-linux-x86_64) |
a473b02 34
| Windows (x86_64) | [plum-windows-x86_64.exe](https://git.pyrossh.dev/releases/plum-cli/v0.1.0/plum-windows-x86_64.exe) |
a473b02 35
a473b02 36
Make it executable and put it on your `PATH`, e.g. on macOS/Linux:
a473b02 37
a473b02 38
```sh
a473b02 39
curl -L -o plum https://git.pyrossh.dev/releases/plum-cli/v0.1.0/plum-darwin-arm64
a473b02 40
chmod +x plum
a473b02 41
mv plum ~/.cargo/bin/plum   # or anywhere else already on your PATH
a473b02 42
```
a473b02 43
<!-- prebuilt-binaries:end -->
a473b02 44
a473b02 45
Maintainers: `cargo make release` builds macOS/Linux/Windows binaries (Linux/Windows via [cross](https://github.com/cross-rs/cross), needs Docker), tags `v<version>`, regenerates the links above, and uploads all three to R2 (see `Makefile.toml`; needs [cargo-make](https://github.com/sagiegurari/cargo-make) and the `git` rclone remote configured for R2).
a473b02 46
61ab95d 47
## Layout and comments
222801d 48
61ab95d 49
Blocks are indentation-sensitive (2 spaces), like Python — there's no `{ }` for grouping statements. `#` starts a line comment.
222801d 50
222801d 51
```plum
222801d 52
# this is a comment
0fe3528 53
fun main() =
222801d 54
  x = 1
222801d 55
  if x > 0
222801d 56
    x = x + 1
222801d 57
```
222801d 58
61ab95d 59
## Naming conventions
222801d 60
61ab95d 61
| Case | Used for |
61ab95d 62
|---|---|
61ab95d 63
| `PascalCase` | types, traits, enums |
61ab95d 64
| `camelCase` | functions, methods, fields |
61ab95d 65
| `snake_case` | variables, params |
61ab95d 66
| `SCREAMING_SNAKE_CASE` | top-level constants |
61ab95d 67
| single uppercase letter (`T`, `K`, `V`) | generic type parameters |
222801d 68
61ab95d 69
## Modules and imports
222801d 70
222801d 71
```plum
222801d 72
module basics
222801d 73
222801d 74
import std/io
222801d 75
```
222801d 76
61ab95d 77
## Constants
222801d 78
222801d 79
```plum
222801d 80
MAX_RETRIES = 3
222801d 81
PI = 3.14159
222801d 82
GREETING = "hello"
222801d 83
```
222801d 84
61ab95d 85
## Literals
222801d 86
222801d 87
```plum
222801d 88
dec = 42
222801d 89
hex = 0xFF
222801d 90
bin = 0b1010
5e995b7 91
big = 1_000_000
222801d 92
flt = 3.14
5e995b7 93
flt2 = 12.0f
222801d 94
exp = 6.022e23
222801d 95
name = "plum"
222801d 96
escaped = "line one\nline two\ttabbed \"quoted\""
222801d 97
yes = True
222801d 98
no = False
222801d 99
```
222801d 100
61ab95d 101
## Operators
222801d 102
222801d 103
From tightest to loosest binding:
222801d 104
222801d 105
| Precedence | Operators | Notes |
222801d 106
|---|---|---|
43e5250 107
| highest | `.` `?.` | attribute access / method call, safe navigation |
61ab95d 108
| | `+` `-` | unary |
222801d 109
| | `*` `/` `%` | |
222801d 110
| | `+` `-` | |
222801d 111
| | `<<` `>>` | |
222801d 112
| | `^` | |
222801d 113
| | `&` | |
222801d 114
| | `\|` | |
222801d 115
| | `<` `<=` `==` `!=` `>=` `>` `<>` | comparisons |
222801d 116
| | `!` | boolean not |
222801d 117
| | `&&` | |
222801d 118
| | `\|\|` | |
222801d 119
| lowest | `? :` | ternary |
222801d 120
61ab95d 121
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):
222801d 122
222801d 123
```plum
5e995b7 124
grouped = {1 + 2} * {3 - 1}
222801d 125
picked = cmp ? sum : bits
222801d 126
negated = -sum
222801d 127
inverted = !cmp
222801d 128
```
222801d 129
61ab95d 130
## Variables and assignment
222801d 131
222801d 132
```plum
5e995b7 133
x := 1
5e995b7 134
# declares a new binding — errors if `x` is already in scope
5e995b7 135
a, b := 1, 2
5e995b7 136
# multiple targets, positionally paired with multiple values
5e995b7 137
x = 2
5e995b7 138
# reassigns an existing binding — type-checked against x's declared type
222801d 139
```
222801d 140
61ab95d 141
## Control flow
222801d 142
222801d 143
```plum
222801d 144
if n < 0
222801d 145
  return "negative"
222801d 146
else if n == 0
222801d 147
  return "zero"
222801d 148
else
222801d 149
  return "positive"
222801d 150
cc77764 151
i := start
222801d 152
while i > 0
222801d 153
  i = i - 1
61ab95d 154
cc77764 155
for i := range limit
222801d 156
  if i == 3
222801d 157
    continue
222801d 158
  if i == 8
222801d 159
    break
222801d 160
  total = total + i
222801d 161
61ab95d 162
assert n > 0    # traps at runtime if false
61ab95d 163
todo            # traps at runtime — marks a body as not yet implemented
61ab95d 164
```
222801d 165
61ab95d 166
## Functions
222801d 167
222801d 168
```plum
0fe3528 169
fun addInts(a: Int, b: Int) -> Int =
222801d 170
  a + b
222801d 171
5e995b7 172
fun greet() = # no return type => Unit
222801d 173
  todo
222801d 174
0fe3528 175
fun withDefault(a: Int, step: Int = 1) -> Int =
222801d 176
  a + step
222801d 177
5e995b7 178
fun sumAll(nums: ...Int) -> Int = # variadic param
222801d 179
  todo
222801d 180
```
222801d 181
61ab95d 182
## Testing
a271f34 183
61ab95d 184
Zig-style `test` blocks are a native language feature, compiled and run only by `plum test`:
a271f34 185
a271f34 186
```plum
a271f34 187
fun add(a: Int, b: Int) -> Int =
a271f34 188
  a + b
a271f34 189
a271f34 190
test "add works"
a9a0147 191
  assert add(1, 2) == 3
a271f34 192
```
a271f34 193
a271f34 194
```sh
984357b 195
$ plum test plum-examples/testing.plum
61ab95d 196
└─ add works ✔
a9a0147 197
61ab95d 198
1 passed, 0 failed
a271f34 199
```
a271f34 200
61ab95d 201
## Types: records, traits, enums
222801d 202
5f2f962 203
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:
5f2f962 204
222801d 205
```plum
5f2f962 206
enum Point =
5f2f962 207
  | Point(x: Int, y: Int)
222801d 208
5f2f962 209
enum Named(ToStr) = # implements ToStr
5f2f962 210
  | Named(name: Str)
222801d 211
222801d 212
trait Shape =
222801d 213
  area() -> Float
222801d 214
  perimeter() -> Float
222801d 215
222801d 216
enum Color =
222801d 217
  | Red
222801d 218
  | Green
222801d 219
  | Blue
222801d 220
222801d 221
enum Option =
287b97c 222
  | Some(Int) # unnamed positional payload — Some(5)
222801d 223
  | None
3a2119e 224
3a2119e 225
enum Shape =
5e995b7 226
  | Circle(radius: Int) # named payload fields — Circle(radius: 5) or Circle(5)
61ab95d 227
  | Square(x: Int, y: Int)
cc77764 228
61ab95d 229
enum Step(n: Int) = # a shared field on every variant ...
61ab95d 230
  | ReadMin(10) # ... each variant supplies its own value for it
cc77764 231
  | ReadMax(20)
222801d 232
```
222801d 233
61ab95d 234
## Generics
222801d 235
222801d 236
```plum
5f2f962 237
enum Box[T] =
5f2f962 238
  | Box(value: T)
222801d 239
5e995b7 240
trait Comparable[T: Ord] = # bounded generic param
cc77764 241
  compareTo(other: T) -> Int
222801d 242
61ab95d 243
fun wrap(value: T) -> Bool =
5e995b7 244
  True
222801d 245
```
222801d 246
61ab95d 247
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.
222801d 248
61ab95d 249
## Closures
6341c74 250
6341c74 251
```plum
0fe3528 252
fun each(cb: fn(Int) -> Int) -> Int =
6341c74 253
  cb(5)
6341c74 254
0fe3528 255
fun useCapturingClosure() -> Int =
5e995b7 256
  offset := 100
5e995b7 257
  cb := |v|
6341c74 258
    v + offset
6341c74 259
  cb(5)
6341c74 260
0fe3528 261
fun main() -> Int =
6341c74 262
  each(|v| v * 3) + useCapturingClosure()
6341c74 263
```
6341c74 264
61ab95d 265
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.
6341c74 266
61ab95d 267
## `self`, field access, and methods
6341c74 268
5f2f962 269
A `fun` declared indented directly inside an `enum` body is a method, with an implicit `self`:
222801d 270
222801d 271
```plum
5f2f962 272
enum Cat =
5f2f962 273
  | Cat(name: Str, age: Int)
41e0859 274
  fun getAge(self) -> Int =
41e0859 275
    self.age
41e0859 276
  fun birthday(self) -> Int =
41e0859 277
    self.age + 1
222801d 278
0fe3528 279
fun main() -> Int =
5e995b7 280
  c := Cat(name: "Whiskers", age: 3)
5e995b7 281
  a := c.getAge()
5e995b7 282
  w := Wrapper(inner: c, tag: 1)
61ab95d 283
  w.inner.age # chained field access
222801d 284
```
222801d 285
61ab95d 286
## Constructing values
222801d 287
222801d 288
```plum
222801d 289
Cat(name: "Whiskers", age: 3)
222801d 290
```
222801d 291
5f2f962 292
Construct a record-shaped enum value by calling its name with `field: value` pairs (any order; every field required).
5f2f962 293
5f2f962 294
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:
5f2f962 295
5f2f962 296
```plum
5f2f962 297
older := Cat(..c, age: c.age + 1)
5f2f962 298
```
222801d 299
61ab95d 300
## `match`
222801d 301
222801d 302
```plum
222801d 303
match n
61ab95d 304
  0 => 1
5e995b7 305
  x =>
61ab95d 306
    if x < 0
5e995b7 307
      -1
5e995b7 308
    else
5e995b7 309
      2
222801d 310
222801d 311
match opt
5e995b7 312
  Some(v) => v
5e995b7 313
  None => 0
cc77764 314
cc77764 315
match book
61ab95d 316
  FantasyBook(title, _, hasMythicalCreatures) when hasMythicalCreatures =>
61ab95d 317
    "Fantasy book \"{title}\" features mythical creatures"
5e995b7 318
  FantasyBook(title, _, _) => "Fantasy book \"{title}\" has no mythical creatures"
222801d 319
```
222801d 320
61ab95d 321
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.
222801d 322
61ab95d 323
## String interpolation
222801d 324
222801d 325
```plum
0fe3528 326
fun greet(name: Str) -> Str =
222801d 327
  "Hello, {name}!"
222801d 328
```
222801d 329
61ab95d 330
`{expr}` interpolates a variable, literal, attribute access, `&&`/`||`/comparison, ternary, or a closure-taking method call.
222801d 331
61ab95d 332
## `extern` functions and printing
a271f34 333
a271f34 334
```plum
a271f34 335
extern fun printLn(s: Str)
a271f34 336
```
a271f34 337
984357b 338
`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`.
a271f34 339
61ab95d 340
```sh
984357b 341
$ plum run plum-examples/io.plum
a271f34 342
hello from plum
a271f34 343
```
a271f34 344
af1776b 345
## Error propagation
af1776b 346
af1776b 347
`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 `?`:
af1776b 348
af1776b 349
```plum
af1776b 350
fun sumTwo(a: Str, b: Str) -> Result[Int, Str] =
af1776b 351
  x := parsePositive(a)?
af1776b 352
  y := parsePositive(b)?
af1776b 353
  return Ok(x + y)
af1776b 354
```
af1776b 355
ec5336f 356
`left ?: right` (elvis) is the plain-expression counterpart — no early return, just a fallback value if `left` is `Err`/`None`:
ec5336f 357
ec5336f 358
```plum
ec5336f 359
fun sumOrZero(a: Str, b: Str) -> Int =
ec5336f 360
  x := parsePositive(a) ?: 0
ec5336f 361
  y := parsePositive(b) ?: 0
ec5336f 362
  x + y
ec5336f 363
```
ec5336f 364
43e5250 365
`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:
43e5250 366
43e5250 367
```plum
43e5250 368
fun cityName(person: Option[Person]) -> Option[Str] =
43e5250 369
  person?.city?.name
43e5250 370
```
43e5250 371
43e5250 372
It's sugar for `obj.map(|v| v.field)` — nothing more than that; chain it as many times as needed.
43e5250 373
61ab95d 374
## Standard library highlights
222801d 375
61ab95d 376
- **`Bool`** (`import std/Bool`) — ordinary `enum Bool = | True | False`
61ab95d 377
- **`Str`** (`import std/Str`) — a byte array with a codepoint-aware layer for Unicode correctness: `runeLength`, `runeAt`, `codePointAt`, `codePointToStr`
61ab95d 378
- **`Byte`** / **`[]Byte`** (`import std/Byte`) — a scalar unsigned 8-bit value and a fixed-length mutable byte slice
61ab95d 379
- **`Buffer`** (`import std/Buffer`) — a growable, mutable byte sequence for building up a `Str`, amortized O(n) writes
61ab95d 380
- **`Array[T]`** (`import std/Array`) — a generic, growable array (reference types only)
61ab95d 381
- **`Map[K: Hashable, V]`** (`import std/Map`) — a real bucketed hash table
61ab95d 382
- **`List[T]`** (`import std/List`) — a singly-linked list with `get`/`add`/`set`/`removeAt`/`remove`/`each`/`map`/`reduce`/`sort`/`join`/`chunk`/`partition`
222801d 383
984357b 384
See [`plum-examples/`](plum-examples) for a runnable file per feature above, and [`plum-std/`](plum-std) for the standard library source.