plum

#treesitter#compiler#wasm

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

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


61ab95d — Peter John 2026-09-04T21:39:53+05:30
docs: trim README to a concise language feature tour
Files changed (1) hide show
  1. README.md +66 -186
README.md CHANGED
@@ -1,11 +1,12 @@
1
- # šŸ‘¾ Plum Programming Language
1
+ # šŸ‘¾ Plum
2
2
 
3
- - A statically typed, imperative programming language with ADT's (Algebraic Data Types) inspired by rust, gleam
3
+ A statically typed, imperative programming language with algebraic data types, inspired by Rust and Gleam.
4
- - The compiler is built upon the tree-sitter parser so has out of the box syntax highlighting support for helix and zed editor
5
- - Plans to be compiled to amd64,arm64, and riscv64 using QBE maintaining C-ABI compatibility
6
4
 
5
+ - Built on a [tree-sitter](tooling/tree-sitter-plum) grammar, with syntax highlighting for Helix and Zed out of the box
6
+ - Compiles to WebAssembly today, with plans to target amd64, arm64, and riscv64 via QBE (C-ABI compatible)
7
7
 
8
8
  ## Requirements
9
+
9
10
  ```sh
10
11
  node >= 23.1.0
11
12
  npm >= 10.9.0
@@ -13,38 +14,9 @@ qbe >= 1.2
13
14
  clang >= 16.0.0
14
15
  ```
15
16
 
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). That syntax isn't implemented yet — everything below is what the compiler accepts **today**. (Generic `[T]` class/trait/enum params are one piece of that future syntax that *has* landed already — see [Generics](#generics).)
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
- - [Testing](#testing)
34
- - [Types: records, traits, enums](#types-records-traits-enums)
35
- - [Generics](#generics)
36
- - [Closures](#closures)
37
- - [`self`, field access, and methods](#self-field-access-and-methods)
38
- - [Constructing values](#constructing-values)
39
- - [`match`](#match)
40
- - [String interpolation](#string-interpolation)
41
- - [`extern` functions and printing](#extern-functions-and-printing)
42
- - [Standard library highlights](#standard-library-highlights)
43
- - [Known gaps](#known-gaps)
44
-
45
- ### Layout and comments
17
+ ## Layout and comments
46
18
 
47
- Blocks are indentation-sensitive (2 spaces), like Python — there's no `{ }` for grouping statements. A `#` starts a line comment.
19
+ Blocks are indentation-sensitive (2 spaces), like Python — there's no `{ }` for grouping statements. `#` starts a line comment.
48
20
 
49
21
  ```plum
50
22
  # this is a comment
@@ -54,22 +26,17 @@ fun main() =
54
26
  x = x + 1
55
27
  ```
56
28
 
57
- ### Naming conventions
29
+ ## Naming conventions
58
30
 
31
+ | Case | Used for |
32
+ |---|---|
33
+ | `PascalCase` | types, traits, enums |
34
+ | `camelCase` | functions, methods, fields |
35
+ | `snake_case` | variables, params |
36
+ | `SCREAMING_SNAKE_CASE` | top-level constants |
59
- The lexer enforces case by construct — using the wrong case for a position is a parse error, not just a style nit:
37
+ | single uppercase letter (`T`, `K`, `V`) | generic type parameters |
60
38
 
61
- | Token | Case | Used for |
62
- |---|---|---|
63
- | `type_identifier` | `PascalCase` | type/trait/enum names, trait bounds |
64
- | `fn_identifier` | `camelCase` | function/method names, `.field`/`.method()` member names |
65
- | `var_identifier` | `snake_case` | variables, params, fields (also accepts camelCase — see below) |
66
- | `const_identifier` | `SCREAMING_SNAKE_CASE` | top-level constants |
67
- | `mod_identifier` | `snake_case`, single word | `module` declarations only (no `/`) |
68
- | `generic` | single uppercase letter (`T`, `K`, `V`, ...) | generic type parameters — see [Generics](#generics) |
69
-
70
- `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. A single uppercase letter is reserved for generics and is illegal as an ordinary `type_identifier` — you can't name a type `T`.
71
-
72
- ### Modules and imports
39
+ ## Modules and imports
73
40
 
74
41
  ```plum
75
42
  module basics
@@ -77,9 +44,7 @@ module basics
77
44
  import std/io
78
45
  ```
79
46
 
80
- `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.
81
-
82
- ### Constants
47
+ ## Constants
83
48
 
84
49
  ```plum
85
50
  MAX_RETRIES = 3
@@ -87,39 +52,30 @@ PI = 3.14159
87
52
  GREETING = "hello"
88
53
  ```
89
54
 
90
- Top-level only, `SCREAMING_SNAKE_CASE`, one expression.
91
-
92
- ### Literals
55
+ ## Literals
93
56
 
94
57
  ```plum
95
58
  dec = 42
96
59
  hex = 0xFF
97
60
  bin = 0b1010
98
61
  big = 1_000_000
99
- # underscores allowed as digit separators
100
62
  flt = 3.14
101
63
  flt2 = 12.0f
102
- # trailing f/F suffix
103
64
  exp = 6.022e23
104
65
  name = "plum"
105
- empty = ""
106
66
  escaped = "line one\nline two\ttabbed \"quoted\""
107
67
  yes = True
108
68
  no = False
109
69
  ```
110
70
 
111
- `True`/`False` are `Bool`'s two variants, declared as an ordinary `enum` in [`libs/std/Bool.plum`](libs/std/Bool.plum) — `import std/Bool` to use them (see [Standard library highlights](#standard-library-highlights)); there's no implicit prelude that pulls `Bool` in for you.
112
-
113
- Full example: [`examples/basics.plum`](examples/basics.plum), [`examples/strings.plum`](examples/strings.plum).
114
-
115
- ### Operators
71
+ ## Operators
116
72
 
117
73
  From tightest to loosest binding:
118
74
 
119
75
  | Precedence | Operators | Notes |
120
76
  |---|---|---|
121
77
  | highest | `.` | attribute access / method call |
122
- | | unary `+` `-` | |
78
+ | | `+` `-` | unary |
123
79
  | | `*` `/` `%` | |
124
80
  | | `+` `-` | |
125
81
  | | `<<` `>>` | |
@@ -132,23 +88,16 @@ From tightest to loosest binding:
132
88
  | | `\|\|` | |
133
89
  | lowest | `? :` | ternary |
134
90
 
135
- There's no range operator — `for i := range n` (see [Control flow](#control-flow)) is the only way to iterate a numeric range, and it's a dedicated statement form, not an expression built from `..`.
91
+ 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):
136
-
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:
138
92
 
139
93
  ```plum
140
94
  grouped = {1 + 2} * {3 - 1}
141
- # {expr} groups, like (expr) in most languages
142
95
  picked = cmp ? sum : bits
143
96
  negated = -sum
144
97
  inverted = !cmp
145
98
  ```
146
99
 
147
- Note: this language uses `{ }` for grouping a sub-expression, not `( )` — parens are reserved for call/constructor argument lists.
148
-
149
- Full example: [`examples/basics.plum`](examples/basics.plum).
150
-
151
- ### Variables and assignment
100
+ ## Variables and assignment
152
101
 
153
102
  ```plum
154
103
  x := 1
@@ -159,9 +108,7 @@ x = 2
159
108
  # reassigns an existing binding — type-checked against x's declared type
160
109
  ```
161
110
 
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.
163
-
164
- ### Control flow
111
+ ## Control flow
165
112
 
166
113
  ```plum
167
114
  if n < 0
@@ -174,23 +121,19 @@ else
174
121
  i := start
175
122
  while i > 0
176
123
  i = i - 1
124
+
177
125
  for i := range limit
178
126
  if i == 3
179
127
  continue
180
128
  if i == 8
181
129
  break
182
130
  total = total + i
183
- assert n > 0
184
- # traps at runtime if false
185
- todo
186
- # traps at runtime — marks a body as not yet implemented
187
- ```
188
-
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`.
190
131
 
132
+ assert n > 0 # traps at runtime if false
191
- Full example: [`examples/control_flow.plum`](examples/control_flow.plum).
133
+ todo # traps at runtime — marks a body as not yet implemented
134
+ ```
192
135
 
193
- ### Functions
136
+ ## Functions
194
137
 
195
138
  ```plum
196
139
  fun addInts(a: Int, b: Int) -> Int =
@@ -206,13 +149,9 @@ fun sumAll(nums: ...Int) -> Int = # variadic param
206
149
  todo
207
150
  ```
208
151
 
209
- 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).
210
-
211
- Full example: [`examples/functions.plum`](examples/functions.plum).
212
-
213
- ### Testing
152
+ ## Testing
214
153
 
215
- Zig-style `test` blocks are a native language feature — they're compiled and run only by `plum test`, never by `run`/`build`/`compile`:
154
+ Zig-style `test` blocks are a native language feature, compiled and run only by `plum test`:
216
155
 
217
156
  ```plum
218
157
  fun add(a: Int, b: Int) -> Int =
@@ -220,27 +159,16 @@ fun add(a: Int, b: Int) -> Int =
220
159
 
221
160
  test "add works"
222
161
  assert add(1, 2) == 3
223
- assert add(2, 2) == 5
224
- # fails, but the test keeps going
225
162
  ```
226
163
 
227
164
  ```sh
228
165
  $ plum test examples/testing.plum
229
- └─ add works ✘
166
+ └─ add works āœ”
230
- add(2, 2) == 5
231
- Expected: 5
232
- Actual: 4
233
167
 
234
- 0 passed, 1 failed
168
+ 1 passed, 0 failed
235
169
  ```
236
170
 
237
- `assert` is a single statement with two behaviors depending on context. Outside a `test` block, a failing `assert` traps immediately, everywhere in the language, the same as it always has (`assert n > 0` above). Inside a `test` block, it's non-fatal instead: a failure is recorded — the condition's own source text, plus (for a top-level `==`/`!=`/`<`/... comparison) each side's own runtime value via its `.toStr()`, labeled `Expected`/`Actual` (right/left) — and the rest of the block keeps running, so one `test` can report every failing assertion in it, not just the first. `plum test`'s output is a tree, one `ā”œā”€`/`└─` line per test with `āœ”`/`✘` (green/red in a real terminal), a failure listing each recorded assert underneath, followed by a `N passed, M failed` summary, with a non-zero exit code if anything failed. The actual/expected values aren't shown when either side is obviously a `Float` literal — string interpolation, which this reuses, doesn't support `Float` yet (see Known gaps) — a computed-`Float`-vs-computed-`Float` comparison with no literal on either side isn't caught by that guard, so it's the one remaining way this feature can't show values (the condition text itself is unaffected either way).
238
-
239
- Full example: [`examples/testing.plum`](examples/testing.plum).
240
-
241
- The compiler's own runtime-behavior regression suite lives as `test`/`assert` blocks co-located with the Plum source they exercise, not as a separate test tree — a stdlib-shaped case (e.g. string interpolation, a linked-list) sits at the bottom of the relevant `libs/std/*.plum` file, and a core-language-feature case (closures, generics, pattern matching, tail expressions, ...) sits at the bottom of the matching `examples/*.plum` file (`closures.plum`, `match.plum`, `functions.plum`, `types.plum`, `control_flow.plum`). All of it runs via [`scripts/test-plum.sh`](scripts/test-plum.sh) against the real `plum test` CLI, rather than as Rust unit tests re-describing the same Plum snippets.
242
-
243
- ### Types: records, traits, enums
171
+ ## Types: records, traits, enums
244
172
 
245
173
  ```plum
246
174
  type Point =
@@ -265,22 +193,14 @@ enum Option =
265
193
 
266
194
  enum Shape =
267
195
  | Circle(radius: Int) # named payload fields — Circle(radius: 5) or Circle(5)
268
- | Square(x: Int, y: Int) # Square(x: 1, y: 2) or Square(1, 2)
196
+ | Square(x: Int, y: Int)
269
197
 
270
- enum Step(n: Int) = # a shared param on the whole enum ...
198
+ enum Step(n: Int) = # a shared field on every variant ...
271
- | ReadMin(10) # ... each variant supplies its own discriminant value for it
199
+ | ReadMin(10) # ... each variant supplies its own value for it
272
200
  | ReadMax(20)
273
201
  ```
274
202
 
275
- An `enum(param: Type) = | Variant(value) ...` declares one shared field on every variant of the enum, with each variant giving its own literal value for that field instead of a payload — `match` still matches `ReadMin`/`ReadMax` by variant tag, but a method on the enum can read `self.n` back and get `10` or `20` depending which variant `self` is. See `Step`/`Step.toNumber` in [`examples/types.plum`](examples/types.plum).
276
-
277
- Full example: [`examples/types.plum`](examples/types.plum).
278
-
279
- `type X(TraitName, ...) = ...`'s trait claims are checked: `plum-checker` verifies `X` actually defines every method `TraitName` declares, with a matching param count/types and return type (a return/param type the trait itself can't parameterize, like `Err`'s `cause() -> Option`, matches any concrete instantiation an implementer declares, e.g. `Option[MyError]`). A trait name that isn't actually declared anywhere (`Comparable`/`Readable`/`Writable` are used this way in a few places in `libs/std` today) is silently unenforced rather than an error, since there's nothing real yet to check it against.
280
-
281
- ### Generics
282
-
283
- Generic type **parameters** are single uppercase letters (`T`, `K`, `V`, ...) — a single uppercase letter is a reserved token of its own (`generic`), distinct from an ordinary `PascalCase` `type_identifier`, so you can't name a real type `T`. A class/trait/enum declares its generic parameters with a bracketed `[Param, Param: Bound, ...]` list right after its name; an optional `: Bound` (or `: Bound1 + Bound2`) constrains a param to types implementing that trait.
203
+ ## Generics
284
204
 
285
205
  ```plum
286
206
  type Box[T] =
@@ -289,18 +209,13 @@ type Box[T] =
289
209
  trait Comparable[T: Ord] = # bounded generic param
290
210
  compareTo(other: T) -> Int
291
211
 
292
- fun wrap(value: T) -> Bool = # a bare uppercase letter in a param/return type is enough
212
+ fun wrap(value: T) -> Bool =
293
213
  True
294
- # free functions have no `[T]` declaration of their own
295
214
  ```
296
215
 
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.
298
-
299
- Constructing a generic class (`Foo[T](...)`) only ever accepts `name: value` keyword arguments or an empty argument list — there's no positional-argument form for a `class_call`, generic or not (see `Standard library highlights` below for how `Array[T]`'s API is shaped around this gap).
300
-
301
- Full example: [`examples/types.plum`](examples/types.plum), [`examples/functions.plum`](examples/functions.plum).
216
+ 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.
302
217
 
303
- ### Closures
218
+ ## Closures
304
219
 
305
220
  ```plum
306
221
  fun each(cb: fn(Int) -> Int) -> Int =
@@ -316,19 +231,11 @@ fun main() -> Int =
316
231
  each(|v| v * 3) + useCapturingClosure()
317
232
  ```
318
233
 
319
- A closure literal is `|params| body`: zero or more bare parameter names between pipes, followed by an expression or an indented block body. A parameter's type is inferred from context (e.g. the declared type of the `fn(...)` slot it's passed into) rather than annotated inline.
320
-
321
- A `fn(...)` / `fn(...) -> T` type annotates a closure-typed parameter or field: `fn(Int) -> Int` is a function from one `Int` to an `Int`; parameter types are positional only (no names). Closures compile to a `{table_index, env_pointer}` pair and are called via `call_indirect` through a function table, so a `fn(...)`-typed value can hold a closure literal, a plain top-level named function (`each(double)` — compiled as a zero-capture "trampoline" closure), or a closure nested inside another closure's body.
322
-
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.
324
-
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.
234
+ 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.
326
235
 
327
- Full example: [`examples/closures.plum`](examples/closures.plum).
236
+ ## `self`, field access, and methods
328
237
 
329
- ### `self`, field access, and methods
330
-
331
- A `fun` declared indented directly inside a `type`/`enum` body is a method on that type, with an implicit `self`:
238
+ A `fun` declared indented directly inside a `type`/`enum` body is a method, with an implicit `self`:
332
239
 
333
240
  ```plum
334
241
  type Cat =
@@ -342,98 +249,71 @@ type Cat =
342
249
  fun main() -> Int =
343
250
  c := Cat(name: "Whiskers", age: 3)
344
251
  a := c.getAge()
345
- # method call
346
252
  w := Wrapper(inner: c, tag: 1)
347
- w.inner.age
348
- # chained field access
253
+ w.inner.age # chained field access
349
254
  ```
350
255
 
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.
352
-
353
- Full example: [`examples/methods.plum`](examples/methods.plum).
354
-
355
- ### Constructing values
256
+ ## Constructing values
356
257
 
357
258
  ```plum
358
259
  Cat(name: "Whiskers", age: 3)
359
260
  ```
360
261
 
361
- Construct a `type` value by calling its name with `field: value` pairs (any order; every field must be provided).
262
+ Construct a `type` value by calling its name with `field: value` pairs (any order; every field required).
362
263
 
363
- ### `match`
264
+ ## `match`
364
265
 
365
266
  ```plum
366
267
  match n
367
- 0 => 1 # single-expression body — always written inline
268
+ 0 => 1
368
269
  x =>
369
- if x < 0 # a body that needs more than one line (here, an if/else)
270
+ if x < 0
370
271
  -1
371
- # is written as an indented block instead
372
272
  else
373
273
  2
374
274
 
375
- match b
376
- True => 1
377
- False => 0
378
-
379
275
  match opt
380
276
  Some(v) => v
381
277
  None => 0
382
278
 
383
279
  match book
384
- FantasyBook(title, _, hasMythicalCreatures) when hasMythicalCreatures => "Fantasy book \"{title}\" features mythical creatures"
280
+ FantasyBook(title, _, hasMythicalCreatures) when hasMythicalCreatures =>
281
+ "Fantasy book \"{title}\" features mythical creatures"
385
282
  FantasyBook(title, _, _) => "Fantasy book \"{title}\" has no mythical creatures"
386
283
  ```
387
284
 
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
-
390
- Full example: [`examples/match.plum`](examples/match.plum).
285
+ 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.
391
286
 
392
- ### String interpolation
287
+ ## String interpolation
393
288
 
394
289
  ```plum
395
290
  fun greet(name: Str) -> Str =
396
291
  "Hello, {name}!"
397
292
  ```
398
293
 
399
- `{expr}` inside a string interpolates any expression — a variable, literal, attribute access, `&&`/`||`/comparison, ternary, or a method call taking a closure literal. Parses, type-checks, and compiles for `Str`, `Int`, and `Bool` values; interpolating a `Float` reports a clear error (see below).
400
-
401
- Full example: [`examples/strings.plum`](examples/strings.plum).
294
+ `{expr}` interpolates a variable, literal, attribute access, `&&`/`||`/comparison, ternary, or a closure-taking method call.
402
295
 
403
- ### `extern` functions and printing
296
+ ## `extern` functions and printing
404
297
 
405
298
  ```plum
406
299
  extern fun printLn(s: Str)
407
300
  ```
408
301
 
409
- `extern fun` declares a function with no Plum body at all — no `=`, no `todo` — backed instead by a host-provided wasm import. wasm has no built-in notion of stdout, so this is how `printLn` exists: `plum-checker` requires an `extern` fn to have no body and no receiver (it can't be a method), and `plum-wasm-codegen` compiles it to a genuine wasm import (`plum::printLn`) instead of a normal function body. Both `plum run` (JIT-executes immediately under an embedded `wasmtime`) and `plum build` (AOT-compiles to wasm and links it into a standalone executable via the `plum-runtime` crate) supply this import; a bare `.wasm` file run through an unmodified `wasmtime` CLI will fail to instantiate for lack of it. `libs/std/os.plum` declares `printLn` for use via `import std/os`; [`examples/io.plum`](examples/io.plum) declares an equivalent local copy (avoiding pulling in every other extern `import std/os` would, so it stays the smallest possible example to run for a smoke test):
302
+ `extern fun` declares a function backed by a host-provided wasm import, with no Plum body. `libs/std/os.plum` declares `printLn` for use via `import std/os`.
410
303
 
411
- ```
304
+ ```sh
412
305
  $ plum run examples/io.plum
413
306
  hello from plum
414
- hello, world!
415
307
  ```
416
308
 
417
- ### Standard library highlights
418
-
419
- None of the types below are compiler-hardcoded (the checker/codegen doesn't know their names) except where called out — they're ordinary `libs/std/*.plum` source, imported like anything else and readable end to end.
420
-
421
- - **`Bool`** (`import std/Bool`) is an ordinary, import-gated `enum Bool = | True | False` — it used to be hardcoded into the checker/codegen (its variants and GC type seeded unconditionally, `True`/`False` special-cased in inference) but no longer is. A file that uses `True`/`False`/`Bool` must `import std/Bool` like any other type; there's no implicit prelude.
422
- - **`Str`** (`import std/Str`) is likewise an ordinary `type Str = data: Buffer` — a ref-counted-free wasm-gc struct wrapping a `Buffer`, not a compiler primitive. `Str` is a byte array under the hood, not an array of Unicode codepoints, but has a codepoint-aware layer on top for correctness on non-ASCII text: `runeLength()` (codepoint count, not byte count), `runeAt(i)` (the `i`-th codepoint as its own `Str`), `codePointAt(i)` (byte index -> codepoint value), and the free function `codePointToStr(cp)`. `reverse()` and word-splitting are codepoint-aware too, rather than shredding multi-byte characters.
423
- - **`Byte`** (`import std/Byte`) is a scalar unsigned 8-bit value (`Byte(x)` converts an `Int`, wrapping rather than trapping out of range — a compiler-recognized conversion like `Int(x)`/`Float(x)`). **`[]Byte`** is a fixed-length, mutable byte slice type (grammar: `seq("[", "]", type)`) — `Str`'s and `Buffer`'s own backing storage, and the only element type a slice type currently supports.
424
- - **`Buffer`** (`import std/Buffer`) is a growable, mutable byte sequence for building up a `Str` piece by piece (modeled on Go's `bytes.Buffer`): a `[]Byte` capacity plus a logical length, doubling on overflow, so a sequence of `n` writes is amortized O(n) rather than O(n²).
425
- - **`Array[T]`** (`import std/Array`) is a generic, growable array backed by a wasm-gc `array<anyref>`. `T` must be a reference type (a `type`/`enum`, `Str`, `List`, ...) — a primitive `T` (`Int`/`Float`/`Bool`/`Byte`) isn't supported, since every element is boxed as `anyref` and unboxed via `ref.cast`. `Array[T]()` is deliberately zero-arg (the [generic `class_call` positional-args gap](#generics) means there's no `Array[T](n)` constructor) — build a specific length by `push`ing onto an empty one.
426
- - **`Map[K: Hashable, V]`** (`import std/Map`) is a real bucketed hash table (`hash(k) % BUCKET_COUNT`, `Array[List[Pair[K, V]]]` as the bucket storage), not an association list — `Str.hash` (FNV-1a) and `Int.hash` (identity) are the two `Hashable` implementations `libs/std` provides today.
427
- - **`List[T]`** (`import std/List`) is a singly-linked list with `get`/`length`/`add`/`set`/`removeAt`/`remove`/`clear`/`reverse`/`each`/`map`/`reduce`/`sort`/`join`/`chunk`/`partition`, plus a `ToStr` implementation so `List[List[T]]` prints correctly too.
428
-
429
- ### Known gaps
309
+ ## Standard library highlights
430
310
 
311
+ - **`Bool`** (`import std/Bool`) — ordinary `enum Bool = | True | False`
431
- 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:
312
+ - **`Str`** (`import std/Str`) — a byte array with a codepoint-aware layer for Unicode correctness: `runeLength`, `runeAt`, `codePointAt`, `codePointToStr`
313
+ - **`Byte`** / **`[]Byte`** (`import std/Byte`) — a scalar unsigned 8-bit value and a fixed-length mutable byte slice
314
+ - **`Buffer`** (`import std/Buffer`) — a growable, mutable byte sequence for building up a `Str`, amortized O(n) writes
315
+ - **`Array[T]`** (`import std/Array`) — a generic, growable array (reference types only)
316
+ - **`Map[K: Hashable, V]`** (`import std/Map`) — a real bucketed hash table
317
+ - **`List[T]`** (`import std/List`) — a singly-linked list with `get`/`add`/`set`/`removeAt`/`remove`/`each`/`map`/`reduce`/`sort`/`join`/`chunk`/`partition`
432
318
 
433
- - `class_call` (constructing a `type` value, generic or not) only ever accepts `name: value` keyword arguments or an empty argument list — there's no positional-argument constructor syntax (`Foo(1, 2)` for a `type`, as opposed to an `enum` variant, isn't parseable). `Array[T]`'s API is deliberately shaped around this (see [Standard library highlights](#standard-library-highlights)).
434
- - interpolating a `Float` value in a string (`Str`/`Int`/`Bool` interpolation, and plain non-interpolated literals, all compile) — correct decimal formatting of a float is a substantial separate undertaking (something like Grisu/Ryu), scoped out for now
435
- - The generic-field-type gap that used to block `libs/std`'s real `List[T]`/`Node[T]` from compiling at all (a class/enum-variant field declared with a concrete instantiation of another generic type, e.g. `Node.next: Option[Node]`) is fixed — `List[T]`'s methods (`get`/`length`/`add`/`set`/`removeAt`/`remove`/`clear`/`reverse`/`each`/`map`/`reduce`/`sort`/`join`, including its `ToStr`-bounded dispatch) are exercised directly, at their real generic type, by the `test` blocks at the bottom of [`libs/std/List.plum`](libs/std/List.plum) itself (run via `plum test libs/std/List.plum`) — not a non-generic stand-in.
436
- - `Map[K, V]` previously hit a *different* bug here: a top-level function whose own declared return type was a fully-concrete generic instantiation (e.g. `fun makeMap() -> Map[Str, Int] = ...`) caused monomorphization to eagerly (and incorrectly) attempt to specialize an unrelated generic type's method (`Option.filter`) at an unresolved type variable, producing a spurious `expected Option, found Option$V` type error even though `filter` was never called anywhere in the program. Root cause: `maybeRewriteReturn` (in `plum-checker/src/monomorphize.rs`) treated ANY return type merely NAMING a known generic class/enum as still-unresolved and in need of rewriting from the body's inferred tail type — even when that name already carried concrete type args (`List[Str]`, `Map[Str, Int]`), clobbering an already-correct declared return type with a stale, unmangled one inferred through tables monomorphization hadn't finished refreshing yet. Fixed by only treating a BARE reference (no `[...]` at all, e.g. `-> Box`) as needing that rewrite; a companion fix pre-resolves every ordinary (non-generic) fn/method's signature into the type tables before any body rewriting begins, so cross-file call order no longer matters. `Option.andThen`/`Result.map`/`Result.mapErr` (a single extra generic, possibly nested inside a closure's return type) are now implemented and working. `Map.map` (needing TWO new generic params nested inside a closure's return type, on a class rather than an enum) is now implemented too, once `specializeFn` was fixed to also substitute a method's own extra generics inside its BODY (not just its params/return) — an explicit bracketed construction like `Map[X, Y](items: ...)` written directly in a method's own body previously kept the literal letters "X"/"Y" forever, silently minting bogus specializations that corrupted unrelated template compilation elsewhere in the program. A second, narrower gap remains: a closure-literal argument whose body does field access on its own param (`|p| cb(p.key, p.val)`) still can't type-check once more than one specialization of the same generic class shares that field name (e.g. both `Pair$Str$Int` and `Pair$Str$Str` declare `key`/`val`) — the checker's field-usage heuristic for inferring a closure param's class becomes ambiguous and gives up; `Map.map`'s own body sidesteps this by using a manual linked-list walk (matching `keys`/`values`/`each`'s existing style) instead of `List.map` with a field-accessing closure
437
- - A generic class/enum method whose return type nests that SAME class/enum inside itself (`List[T].chunk(self) -> List[List[T]]`) used to stack-overflow the compiler for every program merely importing the class, whether or not anything called the method — fixed; `List.chunk`/`List.partition` now ship and work as a working example of the pattern (build any inner `List[T]` value via an EXISTING ordinary method, e.g. `self.sublist(...)`, never a second bare `List(...)` construction in the same body, which is ambiguous with the method's own return-type fallback)
438
- - A ternary as a closure's own inline body (`|n| cond ? a : b`) used to parse as an OUTER ternary wrapping the whole closure instead (`(|n| cond) ? a : b`) — a grammar shift/reduce ambiguity where `closure`'s default precedence (0) beat `ternary_expression`'s deliberately-low `PREC.conditional` (-1) at the point the parser decides whether to keep extending the closure's body or reduce it as already-complete. Fixed by giving `closure` its own even-lower precedence (`PREC.closure = -2`, `tooling/tree-sitter-plum/grammar.js`) so the parser now always prefers letting a ternary (or anything else valid there) keep extending the closure's inline body — see `examples/closures.plum`'s `"closure with an inline ternary body runs correctly"` test.
439
- - `libs/std/Str.plum` is otherwise complete (including `endsWith`/`pad`/`truncate`, filled in alongside the `ToStr`-conformance work below), but three methods remain deliberately unimplemented `todo` stubs (they compile fine and trap at runtime if actually called, rather than failing to compile — a different flavor of gap than the rest of this section): `test`/`matchPattern`/`matchAll`/`replace`/`replaceAll`/`search` all need a real `Regex` type/engine, which doesn't exist anywhere in `libs/std` yet; `deburr` (diacritic stripping, `Ć©` -> `e`) needs a full Unicode decomposition table; `template` (lodash's `_.template`) needs the ability to compile and run Plum source from a `Str` value at runtime — there's no `eval`/dynamic-codegen capability in the language to build it on.
319
+ See [`examples/`](examples) for a runnable file per feature above, and [`libs/std/`](libs/std) for the standard library source.