plum

#treesitter#compiler#wasm

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

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


11faab6 — Peter John 2026-11-01T16:38:57+05:30
docs(plans): map literal constructor plan
docs/plans/2026-11-01-map-literal.md ADDED
@@ -0,0 +1,130 @@
1
+ # Map constructor with `key => value` pairs
2
+
3
+ **Review plan:** docs/plans/2026-11-01-map-literal.review.md
4
+ **Goal:** `Map("a" => 1, "b" => 2)` builds a `Map[Str, Int]`; `k => v` in any call argument list is sugar for the positional argument `Pair(k, v)`.
5
+ **Architecture:** parser-level desugar (tree-sitter `pair_argument` key widened to `expression`; `Arg::Pair` deleted) + `Map.init(entries: ...Pair[K, V])` variadic, reusing the `Type(args)` → `Type.init(args)` sugar `List` already uses.
6
+ **Tech stack:** Rust workspace (plum-core, plum-checker, plum-wasm-codegen, plum-cli), tree-sitter grammar, Plum std library.
7
+ **Spec:** docs/plans/2026-11-01-map-literal.review.md
8
+ **Detail:** contracts
9
+ **Target-min:** 11
10
+
11
+ ## Global constraints
12
+ - Regenerate tree-sitter only with `cd plum-tooling/tree-sitter-plum && npx --yes tree-sitter-cli generate --disable-optimizations` (the default optimizer silently breaks unrelated parses), then `npx --yes tree-sitter-cli test`.
13
+ - Before trusting a build, `touch` an edited `.rs` file: `cargo build --quiet` can skip recompiling here. Plum tests: `cargo build --quiet && ./target/debug/plum test <file-or-dir>`.
14
+ - Never run `plum format` on real repo files to "check": it rewrites in place. Use a scratch copy.
15
+ - Checker test `parse()` helpers don't assert no-error; new checker tests must assert the source parses cleanly.
16
+ - Duplicate keys: last wins (same as `Map.set`). `Map[Str, Int]()` must keep working (variadic-only init accepts zero args).
17
+ - Rust test command: `cargo test -p <crate>`; Plum test command: `./target/debug/plum test <path>`.
18
+
19
+ ```mermaid
20
+ flowchart LR
21
+ G["grammar.js pair_argument key: expression"] --> P["parser.rs: k => v -> Positional(FnCall Pair(k, v))"]
22
+ P --> C["checker: Map.init(...Pair[K,V]) infers K, V"]
23
+ M["Map.plum: fun init(entries: ...Pair[K, V])"] --> C
24
+ C --> W["codegen: existing variadic init call"]
25
+ ```
26
+
27
+ ## File structure
28
+ - `plum-tooling/tree-sitter-plum/grammar.js`, `src/` (generated), `test/corpus/assign.txt`: syntax of `k => v`.
29
+ - `plum-core/src/parser.rs`: desugars `pair_argument` to `Pair(k, v)`.
30
+ - `plum-core/src/ast.rs`: `Arg` loses its `Pair` variant.
31
+ - `plum-core/src/builtin_usage.rs`, `plum-core/src/forms.rs`, `plum-checker/src/lib.rs`, `plum-wasm-codegen/src/lib.rs`: drop dead `Arg::Pair` arms; the checker adds the missing-`Pair` hint.
32
+ - `plum-std/Map.plum`: variadic `init`.
33
+ - `plum-examples/maps.plum` (new), `plum-examples/{generics,values,seq}.plum`, `README.md`: usage and docs.
34
+
35
+ ## Task graph
36
+ ```mermaid
37
+ flowchart LR
38
+ T1[pair syntax + desugar]
39
+ T2[Map.init variadic]
40
+ T3[examples + docs]
41
+ %% taskgraph
42
+ T1 --> T3
43
+ T2 --> T3
44
+ ```
45
+
46
+ ```mermaid
47
+ %% schedule
48
+ gantt
49
+ title Predicted schedule (mm:ss)
50
+ dateFormat X
51
+ axisFormat %M:%S
52
+ section lane 1
53
+ T1 pair syntax + desugar :t1, 0, 132s
54
+ T3 examples + docs :t3, 132, 125s
55
+ section lane 2
56
+ T2 Map.init variadic :t2, 0, 98s
57
+ ```
58
+
59
+ critical path: T1 → T3 (4.3 min) · peak width 2 · predicted build 4.3 min · predicted total ≈ 7.8 min (target ✓)
60
+
61
+ ## Predicted schedule
62
+
63
+ ## Package: syntax
64
+ **Probe entry:** run=./target/debug/plum test plum-examples/maps.plum
65
+
66
+ ### T1 pair syntax + desugar · Complexity 5 (O-1) · Est. context 45k
67
+ **Owns:** plum-tooling/tree-sitter-plum/grammar.js, plum-tooling/tree-sitter-plum/src/parser.c, plum-tooling/tree-sitter-plum/src/grammar.json, plum-tooling/tree-sitter-plum/src/node-types.json, plum-tooling/tree-sitter-plum/test/corpus/assign.txt, plum-core/src/parser.rs, plum-core/src/ast.rs, plum-core/src/builtin_usage.rs, plum-core/src/forms.rs, plum-core/tests/parser_test.rs, plum-checker/src/lib.rs, plum-wasm-codegen/src/lib.rs
68
+ **Components:** 2
69
+ **LOC owned:** 140
70
+ **Tests:** fast
71
+ **Kind:** design
72
+ **Model:** sonnet
73
+ **Est. time:** 3 min
74
+ **Consumes:** none
75
+ **Produces:** parser.parseArg(node: pair_argument) -> Arg
76
+ e.g. `f("a" => 1)` -> `Arg::Positional(Expr::FnCall{name: "Pair", args: [Positional(Str "a"), Positional(Int 1)]})`
77
+ e.g. `f(n + 1 => x)` -> `Arg::Positional(FnCall Pair(Binary(+, n, 1), Var x))`
78
+ e.g. `f("a" => g("b" => 2))` -> the outer Pair's value is `FnCall g(Positional(FnCall Pair("b", 2)))`
79
+ **Review Focus:** `pair_key_any_expression`: `Map(1 => "x", k => "y", n + 1 => "z")` parses with no error node; `match_in_closure_arg_not_pair`: corpus case `f(|s| match s\n A => 1\n _ => 2)` still parses as a match inside a closure, not as a pair; `pair_trailing_comma`: `Map("a" => 1,)` parses; `undefined_pair_hint`: checking `x := f("a" => 1)` with no std/Map import gives an error containing `import std/Map`.
80
+ **Test code:** parser_test.rs gets `pairArgDesugarsToPairCall` (the 3 e.g. lines above, asserted on the AST), `pairArgMixedWithPositional` (`f(1, "a" => 2)` -> `[Positional(Int 1), Positional(FnCall Pair("a", 2))]`), `pairArgTrailingComma`. Corpus: update `assign.txt` expected trees for the `mapOf(...)` lines (now `pair_argument` with `expression` key), add the int key, variable key and match-in-closure cases. Checker hint: a unit or integration test in plum-checker asserting the `import std/Map` hint.
81
+ **Steps:** 1 failing parser tests, 2 grammar edit + regenerate (with `--disable-optimizations`) + corpus, 3 parser desugar, remove `Arg::Pair` everywhere (compiler-guided), add the hint where the undefined name is `Pair`, 4 `cargo test -p plum-core -p plum-checker -p plum-wasm-codegen` green, `npx --yes tree-sitter-cli test` green.
82
+ **Notes:** the desugar keeps the call's line and column so diagnostics point at the pair. The key is parsed through `unwrapExprNode` + `parseExpression` like the value. Put the hint at the checker's `undefined name` site (lib.rs ~886) only when the name is `Pair`: append `` — `k => v` builds a std Pair; import std/Map``.
83
+
84
+ ## Package: std
85
+ **Probe entry:** run=./target/debug/plum test plum-std/Map.plum
86
+
87
+ ### T2 Map.init variadic · Complexity 3 (O-1) · Est. context 30k
88
+ **Owns:** plum-std/Map.plum
89
+ **Components:** 1
90
+ **LOC owned:** 70
91
+ **Tests:** fast
92
+ **Kind:** design
93
+ **Model:** sonnet
94
+ **Est. time:** 2 min
95
+ **Consumes:** none
96
+ **Produces:** Map.init(entries: ...Pair[K, V]) -> Map[K, V]
97
+ e.g. `Map(Pair("a", 1), Pair("b", 2)).length()` -> `2`
98
+ e.g. `Map(Pair("a", 1), Pair("a", 9)).get("a")` -> `9`
99
+ e.g. `Map[Str, Int]().isEmpty()` -> `True`
100
+ **Review Focus:** `init_infers_kv_from_pairs`: `m := Map(Pair("a", 1))`, then `m.set("b", 2)` type-checks with no explicit generics; `init_many_entries_cross_buckets`: 20 `Pair(i.toStr(), i)` built via a variadic call of 20 literal pairs all read back; `init_int_keys`: `Map(Pair(1, "x"), Pair(2, "y")).get(2)` -> `"y"`; `init_nested_value`: `Map(Pair("a", Map(Pair("x", 1))))` reads back 1.
101
+ **Test code:** `test` blocks in Map.plum named after the Review Focus cases plus the 3 e.g. lines, using explicit `Pair(...)` only (the `=>` form is T3's).
102
+ **Steps:** 1 failing test blocks, 2 run `./target/debug/plum test plum-std/Map.plum`: FAIL, 3 change `init` to build the buckets then `set` each entry, 4 run: PASS, plus `./target/debug/plum test plum-std plum-examples` green.
103
+ **Notes:** if K/V inference through `...Pair[K, V]` fails in the checker (`bindMatch` not descending into `TApp` args), report BLOCKED with the exact error. Don't patch the checker: T1 owns `plum-checker/src/lib.rs`.
104
+
105
+ ## Package: docs
106
+ **Probe entry:** run=./target/debug/plum test plum-examples
107
+
108
+ ### T3 examples + docs · Complexity 2 (O-1) · Est. context 30k
109
+ **Owns:** plum-examples/maps.plum, plum-examples/generics.plum, plum-examples/values.plum, plum-examples/seq.plum, README.md
110
+ **Components:** 2
111
+ **LOC owned:** 90
112
+ **Tests:** fast
113
+ **Kind:** design
114
+ **Model:** sonnet
115
+ **Est. time:** 2 min
116
+ **Consumes:** none
117
+ **Produces:** none
118
+ **Review Focus:** `maps_example_arrow_form`: `Map("ann" => 3, "bob" => 5).get("bob")` -> 5; `maps_mixed_pair_and_arrow`: `Map(Pair("a", 1), "b" => 2).length()` -> 2; `maps_duplicate_last_wins`: `Map("a" => 1, "a" => 2).get("a")` -> 2; `maps_nested`: `Map("a" => Map("x" => 1))`; `maps_int_keys`: `Map(1 => "one", 2 => "two").get(1)` -> "one".
119
+ **Test code:** new `plum-examples/maps.plum` with a header comment and one `test` block per Review Focus case. In generics/values/seq.plum, rewrite only tests whose Map is built from literal `set` calls right after `Map[K, V]()`; keep tests that exercise `set` itself.
120
+ **Steps:** 1 write maps.plum, 2 `./target/debug/plum test plum-examples` green, 3 README: next to the `List(...)` intro add the `Map("a" => 1, ...)` form with one line on `k => v` = `Pair(k, v)` and last-wins, and add `maps.plum` to the examples list if one exists.
121
+ **Notes:** docs describe the final behavior only, no history.
122
+
123
+ ## Review Focus
124
+ - syntax (library/parser): deep, encoding. Risks: `=>` vs match-arm ambiguity inside closures in arguments; generated-parser drift from a non-`--disable-optimizations` regen; error-node silently accepted. Cases are in T1.
125
+ - std (library): state, numeric. Risks: K/V inference through a generic enum inside a variadic; duplicate-key semantics. Cases are in T2.
126
+ - docs: protocol (examples run). Cases are in T3.
127
+
128
+ ## Self-review
129
+ Spec coverage: 'What exists today' is background, reflected in T1's Notes. 'Work, in order' maps to T1 (step 1), T2 (step 2) and T3 (step 3), with the final review run by the coordinator. 'Risks to watch' is the Review Focus section above. 'Not in scope (possible follow-ups)' has no task, by design.
130
+ Every review-plan section maps to a task: grammar and parser to T1, Map.init and the hint split T2/T1, docs to T3. There are no placeholders. Consumes match Produces. Edges are T1→T3 and T2→T3 only, because T3's tests execute both. No two parallel tasks share a file: T1 owns the checker lib.rs, which is why the hint is in T1.
docs/plans/2026-11-01-map-literal.review.md ADDED
@@ -0,0 +1,61 @@
1
+ # Map constructor with `key => value` pairs (review plan)
2
+
3
+ **Status:** awaiting approval.
4
+
5
+ ## Goal
6
+ Build a map in one expression, the same way `List(1, 2, 3)` builds a list:
7
+
8
+ ```plum
9
+ scores := Map("ann" => 3, "bob" => 5) # Map[Str, Int], K and V inferred
10
+ byId := Map(1 => "one", 2 => "two") # any key expression, not only strings
11
+ empty := Map[Str, Int]() # still works, unchanged
12
+ nested := Map("a" => Map("x" => 1)) # Map[Str, Map[Str, Int]]
13
+ ```
14
+
15
+ ## What exists today
16
+ - The grammar already has a `pair_argument` (`"a" => 1`) in call argument lists, but only a **string literal** can be the key.
17
+ - The parser turns it into `Arg::Pair { key: String, value }`. After that, every stage (checker, codegen, forms, builtin usage) **ignores the key** and treats the value as an ordinary positional arg. Today `Map("a" => 1, "b" => 2)` fails with `Map.init: expected 0 args, got 2`.
18
+ - `Pair[K, V]` is already a std type (`enum Pair[K, V] = | Pair(key: K, val: V)` in `plum-std/Map.plum`), and the Map buckets store it.
19
+ - `List(1, 2, 3)` works because `Type(args...)` is sugar for `Type.init(args...)`, and `List.init(values: ...T)` is variadic. A variadic-only `init` also accepts zero args, which is why `List[Int]()` works.
20
+
21
+ ## Design
22
+ 1. **`k => v` is sugar for `Pair(k, v)`.** In any call's argument list, `k => v` means exactly the positional argument `Pair(k, v)`. Nothing about it is Map-specific, so you can write `Map(Pair("a", 1), "b" => 2)`, or pass pairs to your own `fun f(ps: ...Pair[K, V])`.
23
+ 2. **Keys can be any expression.** The grammar's `pair_argument` key changes from `string` to `expression` (`n + 1 => x`, `id => name`, `Color.Red => 3`).
24
+ 3. **Desugar in the parser.** The parser emits `Arg::Positional(<Pair(k, v) call>)` and the `Arg::Pair` variant is **deleted**. The checker, codegen, LSP and forms then never see a special case: they type-check and compile an ordinary generic enum construction. Five `Arg::Pair` match arms go away.
25
+ 4. **`Map.init(entries: ...Pair[K, V])`** replaces `Map.init()`. It builds the buckets as before, then calls `set` once per entry, so a **duplicate key means the last one wins**, matching `set`. `Map[Str, Int]()` keeps working through the zero-arg variadic rule.
26
+ 5. **Inference.** K and V come from the pair arguments through the existing variadic binding, the same path `List(1, 2)` uses for `T`. An empty `Map()` without generics is still an error (as `List()` is). Pairs with mixed key or value types give a normal type error that names the arg index.
27
+ 6. **Scope.** `Pair` lives in `std/Map`. If `=>` is used where `Pair` isn't imported, the error says ``"`k => v` builds a std Pair — import std/Map"`` instead of a bare "undefined name Pair".
28
+
29
+ ```mermaid
30
+ flowchart LR
31
+ src["Map(&quot;a&quot; => 1)"] --> ts["tree-sitter pair_argument (key: expression)"]
32
+ ts --> parser["parser: Arg::Positional(Pair(&quot;a&quot;, 1))"]
33
+ parser --> checker["checker: Map.init(...Pair[K,V]) infers K=Str, V=Int"]
34
+ checker --> codegen["codegen: unchanged variadic init call"]
35
+ ```
36
+
37
+ ## Work, in order
38
+ 1. **Grammar and parser.** Widen the `pair_argument` key to `expression`. Regenerate tree-sitter **with `--disable-optimizations`** (the default optimizer silently breaks unrelated parses). Update the `assign.txt` corpus fixture. Desugar to `Pair(...)` in `parser.rs`, remove `Arg::Pair` and its five match arms, and add parser tests (string key, int key, variable key, nested pair value, trailing comma, mixed with positional). Check that `format.scm` keeps spaces around `=>`, and confirm it is idempotent repo-wide. Don't run `plum format` on real files to check this, because it rewrites them in place.
39
+ 2. **Std and checker.** Make `Map.init` variadic over `Pair[K, V]` and add the friendly missing-`Pair` error. Add `plum-std/Map.plum` test blocks:
40
+ - inferred `Map[Str, Int]`;
41
+ - `Int` keys;
42
+ - duplicate key, last wins;
43
+ - `Map[Str, Int]()` still empty;
44
+ - a nested `Map` value;
45
+ - 20 entries across buckets that all read back;
46
+ - mixed `Pair(...)` and `=>`.
47
+
48
+ Add checker tests: a mixed value type is an error, and so is `Map()` with no generics.
49
+ 3. **Docs.** Add a README Map example next to the `List(...)` one, and use the new form in `plum-examples` where a test currently builds a map with repeated `m.set` calls (`generics.plum`, `values.plum`, `seq.plum`).
50
+ 4. **Final review**, then `plum test plum-std plum-examples` and `cargo test` on the release build.
51
+
52
+ ## Risks to watch
53
+ - **Generic inference through a generic enum in a variadic** (`...Pair[K, V]` from `Pair("a", 1)`). `List` only proves `...T`, so this needs a test first. If `bindMatch` doesn't descend into the `TApp` args, that is the fix.
54
+ - **Grammar conflicts**: `=>` is also the match-arm arrow. Arg lists are separate from match bodies, but a `match` inside a closure inside a call arg needs a corpus case.
55
+ - **Unrelated, seen while probing:** `target/debug/plum` currently fails every Map test with `integer literal out of range for Int` (the release binary passes). It is probably the uncommitted WIP in `plum-checker`/`plum-wasm-codegen`/`loader.rs`. It needs resolving before the build, or tests will look broken.
56
+
57
+ ## Not in scope (possible follow-ups)
58
+ - `Set("a", "b")`, which is the same variadic-`init` change.
59
+ - A `{ "a": 1 }` literal syntax.
60
+ - Map `toStr` printing as `Map("a" => 1)`.
61
+ - Map resizing.