plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
222801d
— Peter John
2026-07-19T22:03:42+05:30
docs: document the currently-implemented language syntax in README
- README.md +318 -1
- examples/basics.plum +2 -0
README.md
CHANGED
|
@@ -11,4 +11,321 @@ node >= 23.1.0
|
|
|
11
11
|
npm >= 10.9.0
|
|
12
12
|
qbe >= 1.2
|
|
13
13
|
clang >= 16.0.0
|
|
14
|
-
```
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Language Syntax
|
|
17
|
+
|
|
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.
|
|
19
|
+
|
|
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**.
|
|
21
|
+
|
|
22
|
+
### Table of contents
|
|
23
|
+
|
|
24
|
+
- [Layout and comments](#layout-and-comments)
|
|
25
|
+
- [Naming conventions](#naming-conventions)
|
|
26
|
+
- [Modules and imports](#modules-and-imports)
|
|
27
|
+
- [Constants](#constants)
|
|
28
|
+
- [Literals](#literals)
|
|
29
|
+
- [Operators](#operators)
|
|
30
|
+
- [Variables and assignment](#variables-and-assignment)
|
|
31
|
+
- [Control flow](#control-flow)
|
|
32
|
+
- [Functions](#functions)
|
|
33
|
+
- [Types: records, traits, enums](#types-records-traits-enums)
|
|
34
|
+
- [Generics](#generics)
|
|
35
|
+
- [`self`, field access, and methods](#self-field-access-and-methods)
|
|
36
|
+
- [Constructing values](#constructing-values)
|
|
37
|
+
- [`match`](#match)
|
|
38
|
+
- [String interpolation](#string-interpolation)
|
|
39
|
+
- [Known gaps](#known-gaps)
|
|
40
|
+
|
|
41
|
+
### Layout and comments
|
|
42
|
+
|
|
43
|
+
Blocks are indentation-sensitive (2 spaces), like Python — there's no `{ }` for grouping statements. A `#` starts a line comment.
|
|
44
|
+
|
|
45
|
+
```plum
|
|
46
|
+
# this is a comment
|
|
47
|
+
main() =
|
|
48
|
+
x = 1
|
|
49
|
+
if x > 0
|
|
50
|
+
x = x + 1
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Naming conventions
|
|
54
|
+
|
|
55
|
+
The lexer enforces case by construct — using the wrong case for a position is a parse error, not just a style nit:
|
|
56
|
+
|
|
57
|
+
| Token | Case | Used for |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| `type_identifier` | `PascalCase` | type/trait/enum names, generic bounds |
|
|
60
|
+
| `fn_identifier` | `camelCase` | function/method names, `.field`/`.method()` member names |
|
|
61
|
+
| `var_identifier` | `snake_case` | variables, params, fields (also accepts camelCase — see below) |
|
|
62
|
+
| `const_identifier` | `SCREAMING_SNAKE_CASE` | top-level constants |
|
|
63
|
+
| `mod_identifier` | `snake_case`, single word | `module` declarations only (no `/`) |
|
|
64
|
+
|
|
65
|
+
`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.
|
|
66
|
+
|
|
67
|
+
### Modules and imports
|
|
68
|
+
|
|
69
|
+
```plum
|
|
70
|
+
module basics
|
|
71
|
+
|
|
72
|
+
import std/io
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`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.
|
|
76
|
+
|
|
77
|
+
### Constants
|
|
78
|
+
|
|
79
|
+
```plum
|
|
80
|
+
MAX_RETRIES = 3
|
|
81
|
+
PI = 3.14159
|
|
82
|
+
GREETING = "hello"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Top-level only, `SCREAMING_SNAKE_CASE`, one expression.
|
|
86
|
+
|
|
87
|
+
### Literals
|
|
88
|
+
|
|
89
|
+
```plum
|
|
90
|
+
dec = 42
|
|
91
|
+
hex = 0xFF
|
|
92
|
+
bin = 0b1010
|
|
93
|
+
big = 1_000_000 # underscores allowed as digit separators
|
|
94
|
+
|
|
95
|
+
flt = 3.14
|
|
96
|
+
flt2 = 12.0f # trailing f/F suffix
|
|
97
|
+
exp = 6.022e23
|
|
98
|
+
|
|
99
|
+
name = "plum"
|
|
100
|
+
empty = ""
|
|
101
|
+
escaped = "line one\nline two\ttabbed \"quoted\""
|
|
102
|
+
|
|
103
|
+
yes = True
|
|
104
|
+
no = False
|
|
105
|
+
nothing = Nil
|
|
106
|
+
```
|
|
107
|
+
|
|
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. `Nil` parses and type-checks, but codegen doesn't have a runtime representation for it yet (see [Known gaps](#known-gaps)).
|
|
109
|
+
|
|
110
|
+
Full example: [`examples/basics.plum`](examples/basics.plum), [`examples/strings.plum`](examples/strings.plum).
|
|
111
|
+
|
|
112
|
+
### Operators
|
|
113
|
+
|
|
114
|
+
From tightest to loosest binding:
|
|
115
|
+
|
|
116
|
+
| Precedence | Operators | Notes |
|
|
117
|
+
|---|---|---|
|
|
118
|
+
| highest | `.` | attribute access / method call |
|
|
119
|
+
| | unary `+` `-` | |
|
|
120
|
+
| | `*` `/` `%` | |
|
|
121
|
+
| | `+` `-` | |
|
|
122
|
+
| | `<<` `>>` | |
|
|
123
|
+
| | `^` | |
|
|
124
|
+
| | `&` | |
|
|
125
|
+
| | `\|` | |
|
|
126
|
+
| | `<` `<=` `==` `!=` `>=` `>` `<>` | comparisons |
|
|
127
|
+
| | `!` | boolean not |
|
|
128
|
+
| | `&&` | |
|
|
129
|
+
| | `\|\|` | |
|
|
130
|
+
| | `..` | range (e.g. `for i in 0..5`) |
|
|
131
|
+
| lowest | `? :` | ternary |
|
|
132
|
+
|
|
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:
|
|
134
|
+
|
|
135
|
+
```plum
|
|
136
|
+
grouped = {1 + 2} * {3 - 1} # {expr} groups, like (expr) in most languages
|
|
137
|
+
picked = cmp ? sum : bits
|
|
138
|
+
negated = -sum
|
|
139
|
+
inverted = !cmp
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Note: this language uses `{ }` for grouping a sub-expression, not `( )` — parens are reserved for call/constructor argument lists.
|
|
143
|
+
|
|
144
|
+
Full example: [`examples/basics.plum`](examples/basics.plum).
|
|
145
|
+
|
|
146
|
+
### Variables and assignment
|
|
147
|
+
|
|
148
|
+
```plum
|
|
149
|
+
x = 1
|
|
150
|
+
a, b = 1, 2 # multiple targets, positionally paired with multiple values
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Assignment introduces a binding if `x` isn't already in scope, or updates it otherwise — there's no separate `let`/`var` keyword.
|
|
154
|
+
|
|
155
|
+
### Control flow
|
|
156
|
+
|
|
157
|
+
```plum
|
|
158
|
+
if n < 0
|
|
159
|
+
return "negative"
|
|
160
|
+
else if n == 0
|
|
161
|
+
return "zero"
|
|
162
|
+
else
|
|
163
|
+
return "positive"
|
|
164
|
+
|
|
165
|
+
while i > 0
|
|
166
|
+
i = i - 1
|
|
167
|
+
|
|
168
|
+
for i in 0..limit
|
|
169
|
+
if i == 3
|
|
170
|
+
continue
|
|
171
|
+
if i == 8
|
|
172
|
+
break
|
|
173
|
+
total = total + i
|
|
174
|
+
|
|
175
|
+
assert n > 0 # traps at runtime if false
|
|
176
|
+
todo # traps at runtime — marks a body as not yet implemented
|
|
177
|
+
```
|
|
178
|
+
|
|
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.
|
|
180
|
+
|
|
181
|
+
Full example: [`examples/control_flow.plum`](examples/control_flow.plum).
|
|
182
|
+
|
|
183
|
+
### Functions
|
|
184
|
+
|
|
185
|
+
```plum
|
|
186
|
+
addInts(a: Int, b: Int) -> Int =
|
|
187
|
+
a + b
|
|
188
|
+
|
|
189
|
+
greet() = # no return type => Unit
|
|
190
|
+
todo
|
|
191
|
+
|
|
192
|
+
withDefault(a: Int, step: Int = 1) -> Int =
|
|
193
|
+
a + step
|
|
194
|
+
|
|
195
|
+
sumAll(nums: ...Int) -> Int = # variadic param
|
|
196
|
+
todo
|
|
197
|
+
```
|
|
198
|
+
|
|
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).
|
|
200
|
+
|
|
201
|
+
Full example: [`examples/functions.plum`](examples/functions.plum).
|
|
202
|
+
|
|
203
|
+
### Types: records, traits, enums
|
|
204
|
+
|
|
205
|
+
```plum
|
|
206
|
+
type Point =
|
|
207
|
+
x: Int
|
|
208
|
+
y: Int
|
|
209
|
+
|
|
210
|
+
type Named(Stringable) = # implements Stringable
|
|
211
|
+
name: Str
|
|
212
|
+
|
|
213
|
+
trait Shape =
|
|
214
|
+
area() -> Float
|
|
215
|
+
perimeter() -> Float
|
|
216
|
+
|
|
217
|
+
enum Color =
|
|
218
|
+
| Red
|
|
219
|
+
| Green
|
|
220
|
+
| Blue
|
|
221
|
+
|
|
222
|
+
enum Option =
|
|
223
|
+
| Some(Int) # variant with payload
|
|
224
|
+
| None
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Full example: [`examples/types.plum`](examples/types.plum).
|
|
228
|
+
|
|
229
|
+
### Generics
|
|
230
|
+
|
|
231
|
+
Generic type parameters are single letters only: `a`, `b`, `c`, `d`.
|
|
232
|
+
|
|
233
|
+
```plum
|
|
234
|
+
type Box(a) =
|
|
235
|
+
value: a
|
|
236
|
+
|
|
237
|
+
trait Comparable(a: Ord) = # bounded generic param
|
|
238
|
+
compareTo(other: a) -> Int
|
|
239
|
+
|
|
240
|
+
wrap(value: a) -> Bool = # generic param type
|
|
241
|
+
True
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Generic **arguments** (instantiating a generic type) accept either bracket or paren syntax: `List[Int]` and `List(Int)` both parse. There's no monomorphization/codegen for user-defined generics yet — they type-check permissively but don't compile to wasm.
|
|
245
|
+
|
|
246
|
+
Full example: [`examples/types.plum`](examples/types.plum), [`examples/functions.plum`](examples/functions.plum).
|
|
247
|
+
|
|
248
|
+
### `self`, field access, and methods
|
|
249
|
+
|
|
250
|
+
A function declared as `name<Receiver>(...)` is a method on `Receiver`, with an implicit `self: Receiver`:
|
|
251
|
+
|
|
252
|
+
```plum
|
|
253
|
+
type Cat =
|
|
254
|
+
name: Str
|
|
255
|
+
age: Int
|
|
256
|
+
|
|
257
|
+
getAge<Cat>() -> Int =
|
|
258
|
+
self.age
|
|
259
|
+
|
|
260
|
+
birthday<Cat>() -> Int =
|
|
261
|
+
self.age + 1
|
|
262
|
+
|
|
263
|
+
main() -> Int =
|
|
264
|
+
c = Cat(name: "Whiskers", age: 3)
|
|
265
|
+
a = c.getAge() # method call
|
|
266
|
+
w = Wrapper(inner: c, tag: 1)
|
|
267
|
+
w.inner.age # chained field access
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Methods are dispatched by declared receiver type, not by name alone — two types can each define a method with the same name without colliding.
|
|
271
|
+
|
|
272
|
+
Full example: [`examples/methods.plum`](examples/methods.plum).
|
|
273
|
+
|
|
274
|
+
### Constructing values
|
|
275
|
+
|
|
276
|
+
```plum
|
|
277
|
+
Cat(name: "Whiskers", age: 3)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Construct a `type` value by calling its name with `field: value` pairs (any order; every field must be provided).
|
|
281
|
+
|
|
282
|
+
### `match`
|
|
283
|
+
|
|
284
|
+
```plum
|
|
285
|
+
match n
|
|
286
|
+
0 =>
|
|
287
|
+
"zero"
|
|
288
|
+
1 =>
|
|
289
|
+
"one"
|
|
290
|
+
_ =>
|
|
291
|
+
"many"
|
|
292
|
+
|
|
293
|
+
match b
|
|
294
|
+
True =>
|
|
295
|
+
1
|
|
296
|
+
False =>
|
|
297
|
+
0
|
|
298
|
+
|
|
299
|
+
match opt
|
|
300
|
+
Some(v) =>
|
|
301
|
+
v
|
|
302
|
+
None =>
|
|
303
|
+
0
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Case bodies are always an indented block — there's no `pattern => expr` one-liner form yet. 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), or `_` (wildcard). Multiple comma-separated subjects/patterns are accepted by the grammar but not yet lowered by codegen.
|
|
307
|
+
|
|
308
|
+
Full example: [`examples/match.plum`](examples/match.plum).
|
|
309
|
+
|
|
310
|
+
### String interpolation
|
|
311
|
+
|
|
312
|
+
```plum
|
|
313
|
+
greet(name: Str) -> Str =
|
|
314
|
+
"Hello, {name}!"
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
`{expr}` inside a string interpolates a single primary expression (a variable, literal, attribute access, etc). Parses and type-checks; codegen doesn't lower it yet (see below).
|
|
318
|
+
|
|
319
|
+
Full example: [`examples/strings.plum`](examples/strings.plum).
|
|
320
|
+
|
|
321
|
+
### Known gaps
|
|
322
|
+
|
|
323
|
+
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:
|
|
324
|
+
|
|
325
|
+
- string interpolation (plain, non-interpolated string literals do compile)
|
|
326
|
+
- `match` patterns other than integer literals, bindings, wildcard, and `True`/`False`; non-Bool enum-tag and constructor (`Some(v)`) patterns aren't lowered yet
|
|
327
|
+
- multi-subject `match` (`match a, b`)
|
|
328
|
+
- `Nil` as a value
|
|
329
|
+
- user-defined generics (they type-check but aren't monomorphized)
|
|
330
|
+
|
|
331
|
+
A few grammar rules (`try`, `closure`, the `except` external token) exist in `grammar.js` but aren't wired into any reachable rule yet, so they don't actually parse in context.
|
examples/basics.plum
CHANGED
|
@@ -10,8 +10,10 @@ main() =
|
|
|
10
10
|
dec = 42
|
|
11
11
|
hex = 0xFF
|
|
12
12
|
bin = 0b1010
|
|
13
|
+
big = 1_000_000
|
|
13
14
|
flt = 3.14
|
|
14
15
|
flt2 = 12.0f
|
|
16
|
+
exp = 6.022e23
|
|
15
17
|
name = "plum"
|
|
16
18
|
yes = True
|
|
17
19
|
no = False
|