plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
6341c74
— Peter John
2026-07-21T00:12:38+05:30
docs+test: closures complete; add example and update README
- README.md +30 -3
- examples/closures.plum +11 -0
- plum-wasm-codegen/tests/examples_test.rs +17 -0
README.md
CHANGED
|
@@ -244,6 +244,32 @@ Generic **arguments** (instantiating a generic type) accept either bracket or pa
|
|
|
244
244
|
|
|
245
245
|
Full example: [`examples/types.plum`](examples/types.plum), [`examples/functions.plum`](examples/functions.plum).
|
|
246
246
|
|
|
247
|
+
### Closures
|
|
248
|
+
|
|
249
|
+
```plum
|
|
250
|
+
each(cb: fn(Int) -> Int) -> Int =
|
|
251
|
+
cb(5)
|
|
252
|
+
|
|
253
|
+
useCapturingClosure() -> Int =
|
|
254
|
+
offset = 100
|
|
255
|
+
cb = |v|
|
|
256
|
+
v + offset
|
|
257
|
+
cb(5)
|
|
258
|
+
|
|
259
|
+
main() -> Int =
|
|
260
|
+
each(|v| v * 3) + useCapturingClosure()
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
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.
|
|
264
|
+
|
|
265
|
+
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 either a closure literal or (once that gap below is closed) a plain function.
|
|
266
|
+
|
|
267
|
+
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.
|
|
268
|
+
|
|
269
|
+
Closures parse and compile in two shapes: passed directly as a call argument (`each(|v| v * 3)`, single-line body only — see Known gaps), or assigned to a local first and called or passed on later (`cb = |v|\n v + offset` followed by `cb(5)` or `each(cb)`, which does support a multi-line indented body).
|
|
270
|
+
|
|
271
|
+
Full example: [`examples/closures.plum`](examples/closures.plum).
|
|
272
|
+
|
|
247
273
|
### `self`, field access, and methods
|
|
248
274
|
|
|
249
275
|
A function declared as `name<Receiver>(...)` is a method on `Receiver`, with an implicit `self: Receiver`:
|
|
@@ -323,6 +349,7 @@ Some things parse and type-check but don't compile to wasm yet — `plum-wasm-co
|
|
|
323
349
|
- string interpolation (plain, non-interpolated string literals do compile)
|
|
324
350
|
- multi-subject `match` (`match a, b`)
|
|
325
351
|
- nested constructor patterns inside `match` (`Some(Some(v))`) — a constructor pattern's own sub-patterns must be a bare binding or `_`
|
|
326
|
-
- `libs/std`'s actual `List`/`Map`/`Option`/`Result` still don't compile — they use several other unimplemented features (
|
|
327
|
-
|
|
328
|
-
|
|
352
|
+
- `libs/std`'s actual `List`/`Map`/`Option`/`Result` still don't compile — they use several other unimplemented features (`Nil`/optional chaining, decorators, colon-arrow return syntax) unrelated to generics, which are themselves now monomorphized and compiled correctly for the currently-documented generic syntax
|
|
353
|
+
- a closure literal used directly as a call argument only parses with a single-line body (`each(|v| v * 3)`); a multi-line indented body immediately followed by a closing `)` on the same line as the body's last statement (e.g. `each(|v|\n v)`) does not yet parse — the external scanner doesn't emit a `DEDENT` at a closing bracket. Assigning the closure to a local first (`cb = |v|\n v + offset` then `cb(...)` or `each(cb)`) does support a multi-line body and sidesteps this
|
|
354
|
+
- a closure created via assignment (not passed directly as a call argument) and later called at a concrete non-`Int` type (`Float` or a class/pointer type) can hit a wasm runtime trap due to a signature-resolution gap in `call_indirect` dispatch; this does not affect closures over `Int`, or closures passed directly as call arguments at any type. Tracked as a separate follow-up task
|
|
355
|
+
- a plain top-level named function (not a `|params| body` closure literal) cannot yet be used directly as a `fn(...)`-typed value (e.g. `each(double)`) — only closure literals are supported at closure-typed call sites today
|
examples/closures.plum
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
each(cb: fn(Int) -> Int) -> Int =
|
|
2
|
+
cb(5)
|
|
3
|
+
|
|
4
|
+
useCapturingClosure() -> Int =
|
|
5
|
+
offset = 100
|
|
6
|
+
cb = |v|
|
|
7
|
+
v + offset
|
|
8
|
+
cb(5)
|
|
9
|
+
|
|
10
|
+
main() -> Int =
|
|
11
|
+
each(|v| v * 3) + useCapturingClosure()
|
plum-wasm-codegen/tests/examples_test.rs
CHANGED
|
@@ -83,6 +83,23 @@ fn match_example_compiles_and_runs_correctly() {
|
|
|
83
83
|
assert_eq!(result, 5);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/// closures.plum exercises non-capturing closures, capturing closures (snapshot-by-value),
|
|
87
|
+
/// and closures passed directly as call arguments.
|
|
88
|
+
#[test]
|
|
89
|
+
fn closures_example_compiles_and_runs_correctly() {
|
|
90
|
+
let bytes = assert_compiles("closures.plum");
|
|
91
|
+
let engine = wasmtime::Engine::default();
|
|
92
|
+
let module = wasmtime::Module::new(&engine, &bytes).expect("module should be loadable");
|
|
93
|
+
let mut store = wasmtime::Store::new(&engine, ());
|
|
94
|
+
let instance = wasmtime::Instance::new(&mut store, &module, &[]).expect("module should instantiate");
|
|
95
|
+
let main = instance
|
|
96
|
+
.get_typed_func::<(), i64>(&mut store, "main")
|
|
97
|
+
.expect("main should have signature () -> i64");
|
|
98
|
+
// each(|v| v * 3) = 5 * 3 = 15; useCapturingClosure() = cb(5) with offset=100 snapshot
|
|
99
|
+
// captured at creation = 5 + 100 = 105; total = 15 + 105 = 120.
|
|
100
|
+
assert_eq!(main.call(&mut store, ()).expect("main should not trap"), 120);
|
|
101
|
+
}
|
|
102
|
+
|
|
86
103
|
/// strings.plum still exercises string interpolation, which remains unimplemented.
|
|
87
104
|
#[test]
|
|
88
105
|
fn strings_example_reports_clear_interpolation_error() {
|