plum

#treesitter#compiler#wasm

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.