plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
5e995b7
— Peter John
2026-09-04T21:13:18+05:30
docs: reformat README code blocks to match real plum format output
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
|
-
|
|
99
|
+
# underscores allowed as digit separators
|
|
99
|
-
|
|
100
100
|
flt = 3.14
|
|
101
|
+
flt2 = 12.0f
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
155
|
+
# declares a new binding — errors if `x` is already in scope
|
|
156
|
+
a, b := 1, 2
|
|
155
|
-
|
|
157
|
+
# multiple targets, positionally paired with multiple values
|
|
156
|
-
|
|
158
|
+
x = 2
|
|
157
|
-
|
|
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
|
-
|
|
184
|
+
# traps at runtime if false
|
|
185
|
+
todo
|
|
184
|
-
|
|
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() =
|
|
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 =
|
|
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
|
-
|
|
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) =
|
|
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]
|
|
263
|
+
| Some[Int] # positional/generic payload — Some(5)
|
|
261
264
|
| None
|
|
262
265
|
|
|
263
266
|
enum Shape =
|
|
264
|
-
| Circle(radius: Int)
|
|
267
|
+
| Circle(radius: Int) # named payload fields — Circle(radius: 5) or Circle(5)
|
|
265
|
-
| Square(x: Int, y: Int)
|
|
268
|
+
| Square(x: Int, y: Int) # Square(x: 1, y: 2) or Square(1, 2)
|
|
266
269
|
|
|
267
|
-
enum Step(n: Int) =
|
|
270
|
+
enum Step(n: Int) = # a shared param on the whole enum ...
|
|
268
|
-
| ReadMin(10)
|
|
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] =
|
|
289
|
+
trait Comparable[T: Ord] = # bounded generic param
|
|
287
290
|
compareTo(other: T) -> Int
|
|
288
291
|
|
|
289
|
-
fun wrap(value: T) -> Bool =
|
|
292
|
+
fun wrap(value: T) -> Bool = # a bare uppercase letter in a param/return type is enough
|
|
293
|
+
True
|
|
290
|
-
|
|
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()
|
|
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
|
-
|
|
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 =>
|
|
367
|
+
0 => 1 # single-expression body — always written inline
|
|
364
|
-
|
|
368
|
+
x =>
|
|
365
|
-
|
|
369
|
+
if x < 0 # a body that needs more than one line (here, an if/else)
|
|
366
|
-
|
|
370
|
+
-1
|
|
367
|
-
|
|
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
|
-
|
|
385
|
+
FantasyBook(title, _, _) => "Fantasy book \"{title}\" has no mythical creatures"
|
|
386
386
|
```
|
|
387
387
|
|
|
388
|
-
A case body
|
|
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
|
|