plum

#treesitter#compiler#wasm

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

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


5e995b7Peter John 2026-09-04T21:13:18+05:30
docs: reformat README code blocks to match real plum format output
Files changed (1) hide show
  1. README.md +53 -53
README.md CHANGED
@@ -95,16 +95,15 @@ Top-level only, `SCREAMING_SNAKE_CASE`, one expression.
95
95
  dec = 42
96
96
  hex = 0xFF
97
97
  bin = 0b1010
98
+ big = 1_000_000
98
- big = 1_000_000 # underscores allowed as digit separators
99
+ # underscores allowed as digit separators
99
-
100
100
  flt = 3.14
101
+ flt2 = 12.0f
101
- flt2 = 12.0f # trailing f/F suffix
102
+ # trailing f/F suffix
102
103
  exp = 6.022e23
103
-
104
104
  name = "plum"
105
105
  empty = ""
106
106
  escaped = "line one\nline two\ttabbed \"quoted\""
107
-
108
107
  yes = True
109
108
  no = False
110
109
  ```
@@ -138,7 +137,8 @@ There's no range operator — `for i := range n` (see [Control flow](#control-fl
138
137
  `!` 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:
139
138
 
140
139
  ```plum
140
+ grouped = {1 + 2} * {3 - 1}
141
- grouped = {1 + 2} * {3 - 1} # {expr} groups, like (expr) in most languages
141
+ # {expr} groups, like (expr) in most languages
142
142
  picked = cmp ? sum : bits
143
143
  negated = -sum
144
144
  inverted = !cmp
@@ -151,10 +151,12 @@ Full example: [`examples/basics.plum`](examples/basics.plum).
151
151
  ### Variables and assignment
152
152
 
153
153
  ```plum
154
+ x := 1
154
- x := 1 # declares a new binding — errors if `x` is already in scope
155
+ # declares a new binding — errors if `x` is already in scope
156
+ a, b := 1, 2
155
- a, b := 1, 2 # multiple targets, positionally paired with multiple values
157
+ # multiple targets, positionally paired with multiple values
156
-
158
+ x = 2
157
- x = 2 # reassigns an existing binding — type-checked against x's declared type
159
+ # reassigns an existing binding — type-checked against x's declared type
158
160
  ```
159
161
 
160
162
  `:=` (Go-style) always declares a fresh binding, and it's an error to `:=` a name that's already in scope. Bare `=` is kept for backward compatibility: on a name's first use it still declares (exactly like `:=`), but on every subsequent use it reassigns and is type-checked against the existing binding's type — it can no longer silently change a variable's type out from under it. New code should prefer `:=` for declarations and reserve `=` for reassignment, but both spellings work in either position on a first use.
@@ -172,16 +174,16 @@ else
172
174
  i := start
173
175
  while i > 0
174
176
  i = i - 1
175
-
176
177
  for i := range limit
177
178
  if i == 3
178
179
  continue
179
180
  if i == 8
180
181
  break
181
182
  total = total + i
182
-
183
+ assert n > 0
183
- assert n > 0 # traps at runtime if false
184
+ # traps at runtime if false
185
+ todo
184
- todo # traps at runtime — marks a body as not yet implemented
186
+ # traps at runtime — marks a body as not yet implemented
185
187
  ```
186
188
 
187
189
  `for i := range n` is the only `for`-loop form — it iterates `i` from `0` up to (excluding) `n`, where `n` is an `Int`-valued expression, and binds `i` as `Int`. There's no `in`/`..` range syntax and no general iterator protocol; the one other thing `range` accepts is a variadic function parameter (`fun sumAll(nums: ...Int) = for n := range nums ...`), which iterates its actual arguments and binds `n` as the variadic's element type instead of `Int`.
@@ -194,13 +196,13 @@ Full example: [`examples/control_flow.plum`](examples/control_flow.plum).
194
196
  fun addInts(a: Int, b: Int) -> Int =
195
197
  a + b
196
198
 
197
- fun greet() = # no return type => Unit
199
+ fun greet() = # no return type => Unit
198
200
  todo
199
201
 
200
202
  fun withDefault(a: Int, step: Int = 1) -> Int =
201
203
  a + step
202
204
 
203
- fun sumAll(nums: ...Int) -> Int = # variadic param
205
+ fun sumAll(nums: ...Int) -> Int = # variadic param
204
206
  todo
205
207
  ```
206
208
 
@@ -218,7 +220,8 @@ fun add(a: Int, b: Int) -> Int =
218
220
 
219
221
  test "add works"
220
222
  assert add(1, 2) == 3
223
+ assert add(2, 2) == 5
221
- assert add(2, 2) == 5 # fails, but the test keeps going
224
+ # fails, but the test keeps going
222
225
  ```
223
226
 
224
227
  ```sh
@@ -244,7 +247,7 @@ type Point =
244
247
  x: Int
245
248
  y: Int
246
249
 
247
- type Named(ToStr) = # implements ToStr
250
+ type Named(ToStr) = # implements ToStr
248
251
  name: Str
249
252
 
250
253
  trait Shape =
@@ -257,15 +260,15 @@ enum Color =
257
260
  | Blue
258
261
 
259
262
  enum Option =
260
- | Some[Int] # positional/generic payload — Some(5)
263
+ | Some[Int] # positional/generic payload — Some(5)
261
264
  | None
262
265
 
263
266
  enum Shape =
264
- | Circle(radius: Int) # named payload fields — Circle(radius: 5) or Circle(5)
267
+ | Circle(radius: Int) # named payload fields — Circle(radius: 5) or Circle(5)
265
- | Square(x: Int, y: Int) # Square(x: 1, y: 2) or Square(1, 2)
268
+ | Square(x: Int, y: Int) # Square(x: 1, y: 2) or Square(1, 2)
266
269
 
267
- enum Step(n: Int) = # a shared param on the whole enum ...
270
+ enum Step(n: Int) = # a shared param on the whole enum ...
268
- | ReadMin(10) # ... each variant supplies its own discriminant value for it
271
+ | ReadMin(10) # ... each variant supplies its own discriminant value for it
269
272
  | ReadMax(20)
270
273
  ```
271
274
 
@@ -283,11 +286,12 @@ Generic type **parameters** are single uppercase letters (`T`, `K`, `V`, ...)
283
286
  type Box[T] =
284
287
  value: T
285
288
 
286
- trait Comparable[T: Ord] = # bounded generic param
289
+ trait Comparable[T: Ord] = # bounded generic param
287
290
  compareTo(other: T) -> Int
288
291
 
289
- fun wrap(value: T) -> Bool = # a bare uppercase letter in a param/return type is enough
292
+ fun wrap(value: T) -> Bool = # a bare uppercase letter in a param/return type is enough
293
+ True
290
- True # free functions have no `[T]` declaration of their own
294
+ # free functions have no `[T]` declaration of their own
291
295
  ```
292
296
 
293
297
  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.
@@ -303,8 +307,8 @@ fun each(cb: fn(Int) -> Int) -> Int =
303
307
  cb(5)
304
308
 
305
309
  fun useCapturingClosure() -> Int =
306
- offset = 100
310
+ offset := 100
307
- cb = |v|
311
+ cb := |v|
308
312
  v + offset
309
313
  cb(5)
310
314
 
@@ -318,7 +322,7 @@ A `fn(...)` / `fn(...) -> T` type annotates a closure-typed parameter or field:
318
322
 
319
323
  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.
320
324
 
321
- 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.
325
+ 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.
322
326
 
323
327
  Full example: [`examples/closures.plum`](examples/closures.plum).
324
328
 
@@ -330,18 +334,18 @@ A `fun` declared indented directly inside a `type`/`enum` body is a method on th
330
334
  type Cat =
331
335
  name: Str
332
336
  age: Int
333
-
334
337
  fun getAge(self) -> Int =
335
338
  self.age
336
-
337
339
  fun birthday(self) -> Int =
338
340
  self.age + 1
339
341
 
340
342
  fun main() -> Int =
341
- c = Cat(name: "Whiskers", age: 3)
343
+ c := Cat(name: "Whiskers", age: 3)
342
- a = c.getAge() # method call
344
+ a := c.getAge()
345
+ # method call
343
- w = Wrapper(inner: c, tag: 1)
346
+ w := Wrapper(inner: c, tag: 1)
347
+ w.inner.age
344
- w.inner.age # chained field access
348
+ # chained field access
345
349
  ```
346
350
 
347
351
  Methods are dispatched by declared receiver type, not by name alone — two types can each define a method with the same name without colliding.
@@ -360,32 +364,28 @@ Construct a `type` value by calling its name with `field: value` pairs (any orde
360
364
 
361
365
  ```plum
362
366
  match n
363
- 0 => "zero" # inline body
367
+ 0 => 1 # single-expression body — always written inline
364
- 1 =>
368
+ x =>
365
- "one" # indented block body both forms are accepted
369
+ if x < 0 # a body that needs more than one line (here, an if/else)
366
- _ =>
370
+ -1
367
- "many"
371
+ # is written as an indented block instead
372
+ else
373
+ 2
368
374
 
369
375
  match b
370
- True =>
376
+ True => 1
371
- 1
372
- False =>
377
+ False => 0
373
- 0
374
378
 
375
379
  match opt
376
- Some(v) =>
380
+ Some(v) => v
377
- v
378
- None =>
381
+ None => 0
379
- 0
380
382
 
381
383
  match book
382
- FantasyBook(title, _, hasMythicalCreatures) when hasMythicalCreatures =>
384
+ FantasyBook(title, _, hasMythicalCreatures) when hasMythicalCreatures => "Fantasy book \"{title}\" features mythical creatures"
383
- "Fantasy book \"{title}\" features mythical creatures"
384
- FantasyBook(title, _, _) =>
385
- "Fantasy book \"{title}\" has no mythical creatures"
385
+ FantasyBook(title, _, _) => "Fantasy book \"{title}\" has no mythical creatures"
386
386
  ```
387
387
 
388
- 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. A case's pattern can also carry a `when <expr>` guard: the pattern must still match positionally for the guard to run at all, and a matching pattern whose guard evaluates `False` falls through to the *next* case (the same fallthrough an outright pattern mismatch gets), not to a different position within the same case. See [`examples/dop_visitor.plum`](examples/dop_visitor.plum) for a realistic use of guards.
388
+ A case body is a single inline expression right after `=>` whenever that's all it needs; an indented block is for anything that doesn't fit on one line (an `if`/`else`, a `return`, a nested `match`, several statements). 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. A case's pattern can also carry a `when <expr>` guard: the pattern must still match positionally for the guard to run at all, and a matching pattern whose guard evaluates `False` falls through to the *next* case (the same fallthrough an outright pattern mismatch gets), not to a different position within the same case. See [`examples/dop_visitor.plum`](examples/dop_visitor.plum) for a realistic use of guards.
389
389
 
390
390
  Full example: [`examples/match.plum`](examples/match.plum).
391
391