plum

#treesitter#compiler#wasm

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

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


222801dPeter John 2026-07-19T22:03:42+05:30
docs: document the currently-implemented language syntax in README
Files changed (2) hide show
  1. README.md +318 -1
  2. 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