plum
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
README.md
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
# š¾ Plum
|
|
1
|
+
# š¾ Plum
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
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.
|
|
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
|
-
##
|
|
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
|
-
|
|
37
|
+
| single uppercase letter (`T`, `K`, `V`) | generic type parameters |
|
|
60
38
|
|
|
61
|
-
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
| |
|
|
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`
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
133
|
+
todo # traps at runtime ā marks a body as not yet implemented
|
|
134
|
+
```
|
|
192
135
|
|
|
193
|
-
##
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
168
|
+
1 passed, 0 failed
|
|
235
169
|
```
|
|
236
170
|
|
|
237
|
-
|
|
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)
|
|
196
|
+
| Square(x: Int, y: Int)
|
|
269
197
|
|
|
270
|
-
enum Step(n: Int) = # a shared
|
|
198
|
+
enum Step(n: Int) = # a shared field on every variant ...
|
|
271
|
-
| ReadMin(10) # ... each variant supplies its own
|
|
199
|
+
| ReadMin(10) # ... each variant supplies its own value for it
|
|
272
200
|
| ReadMax(20)
|
|
273
201
|
```
|
|
274
202
|
|
|
275
|
-
|
|
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 =
|
|
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
|
|
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
|
-
##
|
|
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`
|
|
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
|
-
|
|
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
|
|
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
|
-
##
|
|
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
|
|
262
|
+
Construct a `type` value by calling its name with `field: value` pairs (any order; every field required).
|
|
362
263
|
|
|
363
|
-
##
|
|
264
|
+
## `match`
|
|
364
265
|
|
|
365
266
|
```plum
|
|
366
267
|
match n
|
|
367
|
-
0 => 1
|
|
268
|
+
0 => 1
|
|
368
269
|
x =>
|
|
369
|
-
if x < 0
|
|
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 =>
|
|
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
|
-
|
|
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
|
-
##
|
|
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}`
|
|
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
|
-
##
|
|
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
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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.
|