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