plum

#treesitter#compiler#wasm

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

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


6341c74Peter John 2026-07-21T00:12:38+05:30
docs+test: closures complete; add example and update README
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 (closures, `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
327
-
328
- `closure` (`|params| body`) exists in `grammar.js` but isn't wired into any reachable rule yet, so it doesn't actually parse in context.
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() {