plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
cc77764
— Peter John
2026-09-04T20:55:50+05:30
docs: update README for Bool/Str import-gating and stdlib additions
README.md
CHANGED
|
@@ -17,7 +17,7 @@ clang >= 16.0.0
|
|
|
17
17
|
|
|
18
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
19
|
|
|
20
|
-
> The [docs website](website/src/content/docs) describes a considerably more elaborate *future* syntax (`fn`, `record`, arrow return types, `{}` blocks
|
|
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
21
|
|
|
22
22
|
### Table of contents
|
|
23
23
|
|
|
@@ -33,11 +33,13 @@ This section documents the syntax currently implemented by the [tree-sitter gram
|
|
|
33
33
|
- [Testing](#testing)
|
|
34
34
|
- [Types: records, traits, enums](#types-records-traits-enums)
|
|
35
35
|
- [Generics](#generics)
|
|
36
|
+
- [Closures](#closures)
|
|
36
37
|
- [`self`, field access, and methods](#self-field-access-and-methods)
|
|
37
38
|
- [Constructing values](#constructing-values)
|
|
38
39
|
- [`match`](#match)
|
|
39
40
|
- [String interpolation](#string-interpolation)
|
|
40
41
|
- [`extern` functions and printing](#extern-functions-and-printing)
|
|
42
|
+
- [Standard library highlights](#standard-library-highlights)
|
|
41
43
|
- [Known gaps](#known-gaps)
|
|
42
44
|
|
|
43
45
|
### Layout and comments
|
|
@@ -58,13 +60,14 @@ The lexer enforces case by construct — using the wrong case for a position is
|
|
|
58
60
|
|
|
59
61
|
| Token | Case | Used for |
|
|
60
62
|
|---|---|---|
|
|
61
|
-
| `type_identifier` | `PascalCase` | type/trait/enum names,
|
|
63
|
+
| `type_identifier` | `PascalCase` | type/trait/enum names, trait bounds |
|
|
62
64
|
| `fn_identifier` | `camelCase` | function/method names, `.field`/`.method()` member names |
|
|
63
65
|
| `var_identifier` | `snake_case` | variables, params, fields (also accepts camelCase — see below) |
|
|
64
66
|
| `const_identifier` | `SCREAMING_SNAKE_CASE` | top-level constants |
|
|
65
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) |
|
|
66
69
|
|
|
67
|
-
`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.
|
|
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`.
|
|
68
71
|
|
|
69
72
|
### Modules and imports
|
|
70
73
|
|
|
@@ -106,7 +109,7 @@ yes = True
|
|
|
106
109
|
no = False
|
|
107
110
|
```
|
|
108
111
|
|
|
109
|
-
`True`/`False` are
|
|
112
|
+
`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.
|
|
110
113
|
|
|
111
114
|
Full example: [`examples/basics.plum`](examples/basics.plum), [`examples/strings.plum`](examples/strings.plum).
|
|
112
115
|
|
|
@@ -128,9 +131,10 @@ From tightest to loosest binding:
|
|
|
128
131
|
| | `!` | boolean not |
|
|
129
132
|
| | `&&` | |
|
|
130
133
|
| | `\|\|` | |
|
|
131
|
-
| | `..` | range (e.g. `for i in 0..5`) |
|
|
132
134
|
| lowest | `? :` | ternary |
|
|
133
135
|
|
|
136
|
+
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 `..`.
|
|
137
|
+
|
|
134
138
|
`!` 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:
|
|
135
139
|
|
|
136
140
|
```plum
|
|
@@ -147,11 +151,13 @@ Full example: [`examples/basics.plum`](examples/basics.plum).
|
|
|
147
151
|
### Variables and assignment
|
|
148
152
|
|
|
149
153
|
```plum
|
|
150
|
-
x = 1
|
|
154
|
+
x := 1 # declares a new binding — errors if `x` is already in scope
|
|
151
|
-
a, b = 1, 2 # multiple targets, positionally paired with multiple values
|
|
155
|
+
a, b := 1, 2 # multiple targets, positionally paired with multiple values
|
|
156
|
+
|
|
157
|
+
x = 2 # reassigns an existing binding — type-checked against x's declared type
|
|
152
158
|
```
|
|
153
159
|
|
|
154
|
-
|
|
160
|
+
`:=` (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.
|
|
155
161
|
|
|
156
162
|
### Control flow
|
|
157
163
|
|
|
@@ -163,10 +169,11 @@ else if n == 0
|
|
|
163
169
|
else
|
|
164
170
|
return "positive"
|
|
165
171
|
|
|
172
|
+
i := start
|
|
166
173
|
while i > 0
|
|
167
174
|
i = i - 1
|
|
168
175
|
|
|
169
|
-
for i
|
|
176
|
+
for i := range limit
|
|
170
177
|
if i == 3
|
|
171
178
|
continue
|
|
172
179
|
if i == 8
|
|
@@ -177,7 +184,7 @@ assert n > 0 # traps at runtime if false
|
|
|
177
184
|
todo # traps at runtime — marks a body as not yet implemented
|
|
178
185
|
```
|
|
179
186
|
|
|
180
|
-
`for
|
|
187
|
+
`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`.
|
|
181
188
|
|
|
182
189
|
Full example: [`examples/control_flow.plum`](examples/control_flow.plum).
|
|
183
190
|
|
|
@@ -256,29 +263,37 @@ enum Option =
|
|
|
256
263
|
enum Shape =
|
|
257
264
|
| Circle(radius: Int) # named payload fields — Circle(radius: 5) or Circle(5)
|
|
258
265
|
| Square(x: Int, y: Int) # Square(x: 1, y: 2) or Square(1, 2)
|
|
266
|
+
|
|
267
|
+
enum Step(n: Int) = # a shared param on the whole enum ...
|
|
268
|
+
| ReadMin(10) # ... each variant supplies its own discriminant value for it
|
|
269
|
+
| ReadMax(20)
|
|
259
270
|
```
|
|
260
271
|
|
|
272
|
+
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).
|
|
273
|
+
|
|
261
274
|
Full example: [`examples/types.plum`](examples/types.plum).
|
|
262
275
|
|
|
263
276
|
`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.
|
|
264
277
|
|
|
265
278
|
### Generics
|
|
266
279
|
|
|
267
|
-
Generic type parameters are single letters
|
|
280
|
+
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.
|
|
268
281
|
|
|
269
282
|
```plum
|
|
270
|
-
type Box
|
|
283
|
+
type Box[T] =
|
|
271
|
-
value:
|
|
284
|
+
value: T
|
|
272
285
|
|
|
273
|
-
trait Comparable
|
|
286
|
+
trait Comparable[T: Ord] = # bounded generic param
|
|
274
|
-
compareTo(other:
|
|
287
|
+
compareTo(other: T) -> Int
|
|
275
288
|
|
|
276
|
-
fun wrap(value:
|
|
289
|
+
fun wrap(value: T) -> Bool = # a bare uppercase letter in a param/return type is enough —
|
|
277
|
-
True
|
|
290
|
+
True # free functions have no `[T]` declaration of their own
|
|
278
291
|
```
|
|
279
292
|
|
|
280
293
|
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.
|
|
281
294
|
|
|
295
|
+
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).
|
|
296
|
+
|
|
282
297
|
Full example: [`examples/types.plum`](examples/types.plum), [`examples/functions.plum`](examples/functions.plum).
|
|
283
298
|
|
|
284
299
|
### Closures
|
|
@@ -362,9 +377,15 @@ match opt
|
|
|
362
377
|
v
|
|
363
378
|
None =>
|
|
364
379
|
0
|
|
380
|
+
|
|
381
|
+
match book
|
|
382
|
+
FantasyBook(title, _, hasMythicalCreatures) when hasMythicalCreatures =>
|
|
383
|
+
"Fantasy book \"{title}\" features mythical creatures"
|
|
384
|
+
FantasyBook(title, _, _) =>
|
|
385
|
+
"Fantasy book \"{title}\" has no mythical creatures"
|
|
365
386
|
```
|
|
366
387
|
|
|
367
|
-
A case body can be a single inline expression right after `=>`, or an indented block — pick whichever reads better for that arm. 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.
|
|
388
|
+
A case body can be a single inline expression right after `=>`, or an indented block — pick whichever reads better for that arm. 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.
|
|
368
389
|
|
|
369
390
|
Full example: [`examples/match.plum`](examples/match.plum).
|
|
370
391
|
|
|
@@ -385,7 +406,7 @@ Full example: [`examples/strings.plum`](examples/strings.plum).
|
|
|
385
406
|
extern fun printLn(s: Str)
|
|
386
407
|
```
|
|
387
408
|
|
|
388
|
-
`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
|
|
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):
|
|
389
410
|
|
|
390
411
|
```
|
|
391
412
|
$ plum run examples/io.plum
|
|
@@ -393,13 +414,26 @@ hello from plum
|
|
|
393
414
|
hello, world!
|
|
394
415
|
```
|
|
395
416
|
|
|
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
|
+
|
|
396
429
|
### Known gaps
|
|
397
430
|
|
|
398
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:
|
|
399
432
|
|
|
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)).
|
|
400
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
|
|
401
|
-
- 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/
|
|
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.
|
|
402
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
|
|
403
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)
|
|
404
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.
|
|
405
|
-
- `libs/std/
|
|
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.
|