plum

#treesitter#compiler#wasm

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

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


README.md
fa31ba4 1
# 👾 Plum Programming Language
c8e4165 2
fa31ba4 3
- A statically typed, imperative programming language with ADT's (Algebraic Data Types) inspired by rust, gleam
fa31ba4 4
- The compiler is built upon the tree-sitter parser so has out of the box syntax highlighting support for helix and zed editor
fa31ba4 5
- Plans to be compiled to amd64,arm64, and riscv64 using QBE maintaining C-ABI compatibility
fa31ba4 6
fa31ba4 7
fa31ba4 8
## Requirements
fa31ba4 9
```sh
fa31ba4 10
node >= 23.1.0
fa31ba4 11
npm >= 10.9.0
fa31ba4 12
qbe >= 1.2
fa31ba4 13
clang >= 16.0.0
222801d 14
```
222801d 15
222801d 16
## Language Syntax
222801d 17
222801d 18
This section documents the syntax currently implemented by the [tree-sitter grammar](tooling/tree-sitter-plum/grammar.js), [`plum-checker`](plum-checker), and [`plum-wasm-codegen`](plum-wasm-codegen). Every construct below has a runnable, verified example in [`examples/`](examples) — the two test suites [`plum-checker/tests/examples_test.rs`](plum-checker/tests/examples_test.rs) and [`plum-wasm-codegen/tests/examples_test.rs`](plum-wasm-codegen/tests/examples_test.rs) parse, type-check, and (where noted) compile-and-execute every file in that directory, so this documentation can't silently drift out of sync with what actually works.
222801d 19
222801d 20
> The [docs website](website/src/content/docs) describes a considerably more elaborate *future* syntax (`fn`, `record`, arrow return types, `{}` blocks, generic `[T]` params). That syntax isn't implemented yet — everything below is what the compiler accepts **today**.
222801d 21
222801d 22
### Table of contents
222801d 23
222801d 24
- [Layout and comments](#layout-and-comments)
222801d 25
- [Naming conventions](#naming-conventions)
222801d 26
- [Modules and imports](#modules-and-imports)
222801d 27
- [Constants](#constants)
222801d 28
- [Literals](#literals)
222801d 29
- [Operators](#operators)
222801d 30
- [Variables and assignment](#variables-and-assignment)
222801d 31
- [Control flow](#control-flow)
222801d 32
- [Functions](#functions)
222801d 33
- [Types: records, traits, enums](#types-records-traits-enums)
222801d 34
- [Generics](#generics)
222801d 35
- [`self`, field access, and methods](#self-field-access-and-methods)
222801d 36
- [Constructing values](#constructing-values)
222801d 37
- [`match`](#match)
222801d 38
- [String interpolation](#string-interpolation)
0000000 39
- [`extern` functions and printing](#extern-functions-and-printing)
222801d 40
- [Known gaps](#known-gaps)
222801d 41
222801d 42
### Layout and comments
222801d 43
222801d 44
Blocks are indentation-sensitive (2 spaces), like Python — there's no `{ }` for grouping statements. A `#` starts a line comment.
222801d 45
222801d 46
```plum
222801d 47
# this is a comment
0fe3528 48
fun main() =
222801d 49
  x = 1
222801d 50
  if x > 0
222801d 51
    x = x + 1
222801d 52
```
222801d 53
222801d 54
### Naming conventions
222801d 55
222801d 56
The lexer enforces case by construct — using the wrong case for a position is a parse error, not just a style nit:
222801d 57
222801d 58
| Token | Case | Used for |
222801d 59
|---|---|---|
222801d 60
| `type_identifier` | `PascalCase` | type/trait/enum names, generic bounds |
222801d 61
| `fn_identifier` | `camelCase` | function/method names, `.field`/`.method()` member names |
222801d 62
| `var_identifier` | `snake_case` | variables, params, fields (also accepts camelCase — see below) |
222801d 63
| `const_identifier` | `SCREAMING_SNAKE_CASE` | top-level constants |
222801d 64
| `mod_identifier` | `snake_case`, single word | `module` declarations only (no `/`) |
222801d 65
222801d 66
`var_identifier`'s regex is deliberately a superset of `fn_identifier`'s (it allows both `_` and mixed case) so that a bare function call like `factorial(x)` — an all-lowercase, no-underscore name — isn't ambiguous between "a variable" and "a function" at the lexer level.
222801d 67
222801d 68
### Modules and imports
222801d 69
222801d 70
```plum
222801d 71
module basics
222801d 72
222801d 73
import std/io
222801d 74
```
222801d 75
222801d 76
`module` takes exactly one identifier (no path segments). `import` takes a `/`-separated path; there's no module resolution yet — it's parsed but not loaded.
222801d 77
222801d 78
### Constants
222801d 79
222801d 80
```plum
222801d 81
MAX_RETRIES = 3
222801d 82
PI = 3.14159
222801d 83
GREETING = "hello"
222801d 84
```
222801d 85
222801d 86
Top-level only, `SCREAMING_SNAKE_CASE`, one expression.
222801d 87
222801d 88
### Literals
222801d 89
222801d 90
```plum
222801d 91
dec = 42
222801d 92
hex = 0xFF
222801d 93
bin = 0b1010
222801d 94
big = 1_000_000       # underscores allowed as digit separators
222801d 95
222801d 96
flt = 3.14
222801d 97
flt2 = 12.0f          # trailing f/F suffix
222801d 98
exp = 6.022e23
222801d 99
222801d 100
name = "plum"
222801d 101
empty = ""
222801d 102
escaped = "line one\nline two\ttabbed \"quoted\""
222801d 103
222801d 104
yes = True
222801d 105
no = False
222801d 106
```
222801d 107
660674c 108
`True`/`False` are built into the type checker/codegen as `Bool`'s two variants — you don't need to declare `enum Bool` yourself to use them.
222801d 109
222801d 110
Full example: [`examples/basics.plum`](examples/basics.plum), [`examples/strings.plum`](examples/strings.plum).
222801d 111
222801d 112
### Operators
222801d 113
222801d 114
From tightest to loosest binding:
222801d 115
222801d 116
| Precedence | Operators | Notes |
222801d 117
|---|---|---|
222801d 118
| highest | `.` | attribute access / method call |
222801d 119
| | unary `+` `-` | |
222801d 120
| | `*` `/` `%` | |
222801d 121
| | `+` `-` | |
222801d 122
| | `<<` `>>` | |
222801d 123
| | `^` | |
222801d 124
| | `&` | |
222801d 125
| | `\|` | |
222801d 126
| | `<` `<=` `==` `!=` `>=` `>` `<>` | comparisons |
222801d 127
| | `!` | boolean not |
222801d 128
| | `&&` | |
222801d 129
| | `\|\|` | |
222801d 130
| | `..` | range (e.g. `for i in 0..5`) |
222801d 131
| lowest | `? :` | ternary |
222801d 132
222801d 133
`!` binds **looser** than comparisons — `!a == b` means `!(a == b)`, not `(!a) == b`. Whenever you mix categories (especially with `!`), group explicitly rather than relying on memorized precedence:
222801d 134
222801d 135
```plum
222801d 136
grouped = {1 + 2} * {3 - 1}    # {expr} groups, like (expr) in most languages
222801d 137
picked = cmp ? sum : bits
222801d 138
negated = -sum
222801d 139
inverted = !cmp
222801d 140
```
222801d 141
222801d 142
Note: this language uses `{ }` for grouping a sub-expression, not `( )` — parens are reserved for call/constructor argument lists.
222801d 143
222801d 144
Full example: [`examples/basics.plum`](examples/basics.plum).
222801d 145
222801d 146
### Variables and assignment
222801d 147
222801d 148
```plum
222801d 149
x = 1
222801d 150
a, b = 1, 2          # multiple targets, positionally paired with multiple values
222801d 151
```
222801d 152
222801d 153
Assignment introduces a binding if `x` isn't already in scope, or updates it otherwise — there's no separate `let`/`var` keyword.
222801d 154
222801d 155
### Control flow
222801d 156
222801d 157
```plum
222801d 158
if n < 0
222801d 159
  return "negative"
222801d 160
else if n == 0
222801d 161
  return "zero"
222801d 162
else
222801d 163
  return "positive"
222801d 164
222801d 165
while i > 0
222801d 166
  i = i - 1
222801d 167
222801d 168
for i in 0..limit
222801d 169
  if i == 3
222801d 170
    continue
222801d 171
  if i == 8
222801d 172
    break
222801d 173
  total = total + i
222801d 174
222801d 175
assert n > 0    # traps at runtime if false
222801d 176
todo            # traps at runtime — marks a body as not yet implemented
222801d 177
```
222801d 178
222801d 179
`for x in <range>` only accepts a range expression (`a..b`) or another primary expression on the right — there's no iterator protocol yet.
222801d 180
222801d 181
Full example: [`examples/control_flow.plum`](examples/control_flow.plum).
222801d 182
222801d 183
### Functions
222801d 184
222801d 185
```plum
0fe3528 186
fun addInts(a: Int, b: Int) -> Int =
222801d 187
  a + b
222801d 188
0fe3528 189
fun greet() =                        # no return type => Unit
222801d 190
  todo
222801d 191
0fe3528 192
fun withDefault(a: Int, step: Int = 1) -> Int =
222801d 193
  a + step
222801d 194
0fe3528 195
fun sumAll(nums: ...Int) -> Int =    # variadic param
222801d 196
  todo
222801d 197
```
222801d 198
222801d 199
The body always follows `=` on the next, indented line (`= expr` on the same line is grammatically accepted too, but every example and test in this repo uses the indented form — prefer it for consistency).
222801d 200
222801d 201
Full example: [`examples/functions.plum`](examples/functions.plum).
222801d 202
222801d 203
### Types: records, traits, enums
222801d 204
222801d 205
```plum
222801d 206
type Point =
222801d 207
  x: Int
222801d 208
  y: Int
222801d 209
222801d 210
type Named(Stringable) =    # implements Stringable
222801d 211
  name: Str
222801d 212
222801d 213
trait Shape =
222801d 214
  area() -> Float
222801d 215
  perimeter() -> Float
222801d 216
222801d 217
enum Color =
222801d 218
  | Red
222801d 219
  | Green
222801d 220
  | Blue
222801d 221
222801d 222
enum Option =
222801d 223
  | Some(Int)              # variant with payload
222801d 224
  | None
222801d 225
```
222801d 226
222801d 227
Full example: [`examples/types.plum`](examples/types.plum).
222801d 228
222801d 229
### Generics
222801d 230
222801d 231
Generic type parameters are single letters only: `a`, `b`, `c`, `d`.
222801d 232
222801d 233
```plum
222801d 234
type Box(a) =
222801d 235
  value: a
222801d 236
222801d 237
trait Comparable(a: Ord) =    # bounded generic param
222801d 238
  compareTo(other: a) -> Int
222801d 239
0fe3528 240
fun wrap(value: a) -> Bool =      # generic param type
222801d 241
  True
222801d 242
```
222801d 243
2216237 244
Generic **arguments** (instantiating a generic type) accept either bracket or paren syntax: `List[Int]` and `List(Int)` both parse. User-defined generics (classes, their methods, free functions, and enums) are monomorphized: each concrete-type-argument combination actually used in the program gets its own specialized, fully-concrete copy, which then type-checks and compiles to wasm through the normal, unmodified pipeline. See `useWrap`/`usePair` in [`examples/functions.plum`](examples/functions.plum) and `makeIntBox`/`makeStrBox` in [`examples/types.plum`](examples/types.plum) for real instantiation sites. Generic enums support any number of concrete instantiations coexisting in one program (variant names are mangled per instantiation, e.g. `Some` -> `Some$Int`/`Some$Str`, internally — invisible to user code). One narrower residual limitation: a payload-free variant (e.g. `None`) used as a bare value *outside* of a `match` pattern can't be disambiguated between multiple concrete instantiations of its enum from that expression alone; constructing via a payload-carrying sibling (`Some(5)`) and matching (`Some(v) => ...`, `None => ...`) is fully supported and is the overwhelmingly common usage pattern.
222801d 245
222801d 246
Full example: [`examples/types.plum`](examples/types.plum), [`examples/functions.plum`](examples/functions.plum).
222801d 247
6341c74 248
### Closures
6341c74 249
6341c74 250
```plum
0fe3528 251
fun each(cb: fn(Int) -> Int) -> Int =
6341c74 252
  cb(5)
6341c74 253
0fe3528 254
fun useCapturingClosure() -> Int =
6341c74 255
  offset = 100
6341c74 256
  cb = |v|
6341c74 257
    v + offset
6341c74 258
  cb(5)
6341c74 259
0fe3528 260
fun main() -> Int =
6341c74 261
  each(|v| v * 3) + useCapturingClosure()
6341c74 262
```
6341c74 263
6341c74 264
A closure literal is `|params| body`: zero or more bare parameter names between pipes, followed by an expression or an indented block body. A parameter's type is inferred from context (e.g. the declared type of the `fn(...)` slot it's passed into) rather than annotated inline.
6341c74 265
35af6cf 266
A `fn(...)` / `fn(...) -> T` type annotates a closure-typed parameter or field: `fn(Int) -> Int` is a function from one `Int` to an `Int`; parameter types are positional only (no names). Closures compile to a `{table_index, env_pointer}` pair and are called via `call_indirect` through a function table, so a `fn(...)`-typed value can hold a closure literal, a plain top-level named function (`each(double)` — compiled as a zero-capture "trampoline" closure), or a closure nested inside another closure's body.
6341c74 267
6341c74 268
Capture is **snapshot-by-value**: any outer variable a closure body references is copied into the closure's environment at the moment the closure literal is evaluated, not re-read live later. Reassigning the captured variable afterward does not change what the closure sees.
6341c74 269
35af6cf 270
Closures parse and compile in two shapes: passed directly as a call argument (`each(|v| v * 3)`), or assigned to a local first and called or passed on later (`cb = |v|\n  v + offset` followed by `cb(5)` or `each(cb)`). Both support a multi-line indented body.
6341c74 271
6341c74 272
Full example: [`examples/closures.plum`](examples/closures.plum).
6341c74 273
222801d 274
### `self`, field access, and methods
222801d 275
41e0859 276
A `fun` declared indented directly inside a `type`/`enum` body is a method on that type, with an implicit `self`:
222801d 277
222801d 278
```plum
222801d 279
type Cat =
222801d 280
  name: Str
222801d 281
  age: Int
222801d 282
41e0859 283
  fun getAge(self) -> Int =
41e0859 284
    self.age
222801d 285
41e0859 286
  fun birthday(self) -> Int =
41e0859 287
    self.age + 1
222801d 288
0fe3528 289
fun main() -> Int =
222801d 290
  c = Cat(name: "Whiskers", age: 3)
222801d 291
  a = c.getAge()        # method call
222801d 292
  w = Wrapper(inner: c, tag: 1)
222801d 293
  w.inner.age            # chained field access
222801d 294
```
222801d 295
222801d 296
Methods are dispatched by declared receiver type, not by name alone — two types can each define a method with the same name without colliding.
222801d 297
222801d 298
Full example: [`examples/methods.plum`](examples/methods.plum).
222801d 299
222801d 300
### Constructing values
222801d 301
222801d 302
```plum
222801d 303
Cat(name: "Whiskers", age: 3)
222801d 304
```
222801d 305
222801d 306
Construct a `type` value by calling its name with `field: value` pairs (any order; every field must be provided).
222801d 307
222801d 308
### `match`
222801d 309
222801d 310
```plum
222801d 311
match n
660674c 312
  0 => "zero"       # inline body
222801d 313
  1 =>
660674c 314
    "one"           # indented block body — both forms are accepted
222801d 315
  _ =>
222801d 316
    "many"
222801d 317
222801d 318
match b
222801d 319
  True =>
222801d 320
    1
222801d 321
  False =>
222801d 322
    0
222801d 323
222801d 324
match opt
222801d 325
  Some(v) =>
222801d 326
    v
222801d 327
  None =>
222801d 328
    0
222801d 329
```
222801d 330
35af6cf 331
A case body can be a single inline expression right after `=>`, or an indented block — pick whichever reads better for that arm. Patterns can be: integer/float/string literals, a bare identifier (binds a new local to the subject's value), a bare capitalized tag (`True`, `False`, or any declared `enum` variant with no payload — compared, not bound), a constructor pattern (`Some(v)`, binding its argument — sub-patterns can themselves be constructor patterns, e.g. `Wrap(Some(v))`), or `_` (wildcard). `match a, b, ...` against multiple comma-separated subjects is also supported — each case supplies one pattern per subject, and every position must match for that case to apply.
222801d 332
222801d 333
Full example: [`examples/match.plum`](examples/match.plum).
222801d 334
222801d 335
### String interpolation
222801d 336
222801d 337
```plum
0fe3528 338
fun greet(name: Str) -> Str =
222801d 339
  "Hello, {name}!"
222801d 340
```
222801d 341
35af6cf 342
`{expr}` inside a string interpolates a single primary expression (a variable, literal, attribute access, etc). Parses, type-checks, and compiles for `Str`, `Int`, and `Bool` values; interpolating a `Float` reports a clear error (see below).
222801d 343
222801d 344
Full example: [`examples/strings.plum`](examples/strings.plum).
222801d 345
0000000 346
### `extern` functions and printing
0000000 347
0000000 348
```plum
0000000 349
extern fun printLn(s: Str)
0000000 350
```
0000000 351
0000000 352
`extern fun` declares a function with no Plum body at all — no `=`, no `todo` — backed instead by a host-provided wasm import. wasm has no built-in notion of stdout, so this is how `printLn` exists: `plum-checker` requires an `extern` fn to have no body and no receiver (it can't be a method), and `plum-wasm-codegen` compiles it to a genuine wasm import (`plum::printLn`) instead of a normal function body. Both `plum run` (JIT-executes immediately under an embedded `wasmtime`) and `plum build` (AOT-compiles to wasm and links it into a standalone executable via the `plum-runtime` crate) supply this import; a bare `.wasm` file run through an unmodified `wasmtime` CLI will fail to instantiate for lack of it. `libs/std/os.plum` declares `printLn` for use via `import std/os`; [`examples/io.plum`](examples/io.plum) declares an equivalent local copy (avoiding the import, so it stays runnable by the single-file test harnesses in `plum-checker`/`plum-wasm-codegen`) and is the smallest example to run for a smoke test:
0000000 353
0000000 354
```
0000000 355
$ plum run examples/io.plum
0000000 356
hello from plum
0000000 357
hello, world!
0000000 358
```
0000000 359
222801d 360
### Known gaps
222801d 361
222801d 362
Some things parse and type-check but don't compile to wasm yet — `plum-wasm-codegen` reports a clear error rather than silently producing wrong code:
222801d 363
35af6cf 364
- interpolating a `Float` value in a string (`Str`/`Int`/`Bool` interpolation, and plain non-interpolated literals, all compile) — correct decimal formatting of a float is a substantial separate undertaking (something like Grisu/Ryu), scoped out for now
ecd2178 365
- `libs/std`'s actual `List`/`Map` still don't compile — `List`'s methods (`get`/`length`/`add`/`set`/`removeAt`/`remove`/`clear`/`reverse`/`each`/`map`) are all implemented now, but a class or enum-variant field declared with a concrete instantiation of another generic type (`Node.next: Option[Node]`, needed by `List`/`Node`'s own linked-list shape) doesn't survive monomorphization — `plumTypeFromAst` drops generic type arguments when building field types, so once the referenced generic type is specialized (and its unspecialized original removed), the field is left pointing at a name that no longer exists. This is a `plum-checker` generics gap, not a codegen one — the underlying wasm-gc struct/array machinery it would need is fully implemented and tested (see `plum-wasm-codegen/tests/codegen_tests.rs`'s `listAddSetRemoveAtRemoveClearReverseAllWorkCorrectly` and `removingEveryNodeInALoopLeavesAnEmptyCorrectlyFunctioningList`, which port `List`'s methods onto an equivalent non-generic shape to prove it). Separately, `List`'s own `join` method (and `Map`) reference a `Buffer` type and trait-bounded dispatch (`Stringable`) that don't exist yet — `plum-checker` doesn't process trait declarations at all currently