plum

#treesitter#compiler#wasm

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

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


90983a5 — Peter John 2026-11-05T12:11:31+05:30
decisions: keep postfix ! as unwrap sugar; run D design and D1 plan
docs/decisions/2026-11-language-and-scene-decisions.md CHANGED
@@ -132,6 +132,7 @@ The user decided to simplify the language before the compiler is refactored furt
132
132
  - **Same plan, also decided 2026-11-04** (user: "1 to 6 all can be done"):
133
133
  1. Errors (revised later the same day, user: "Yes please"): error sets, `T!E` and `T?` as special types are REMOVED in favour of two ordinary std enums, `Option[T] = None | Some(value: T)` and `Result[T, E] = Ok(value: T) | Err(error: E)` with `E` an ordinary enum. `null` is removed (→ `None`). The syntax stays as parser sugar: `T?` → `Option[T]`, `E!T` / `error{..}!T` → `Result[T, E]` (an inline set becomes an ordinary enum), `try e` → early-return on `Err`/`None`, `a ?: b` → `unwrapOr`, `x?.f` → `map`, `catch` → `match`. No automatic error-set merging: different error enums meet through `mapErr`. This reverses the 2026-10-25 move away from Option/Result, deliberately, to remove error-set types and inference from the checker. Runs as run D, after A-C.
134
134
  - **Revised 2026-11-05** (user: "just move to Option/Result as return types and use one operation ? as syntax sugar", run right after run C): the ONLY sugar is postfix `?` on an `Option`/`Result` value inside a function returning `Option`/`Result` (early-return on `None`/`Err`, like Rust). Return types are written `Option[T]` / `Result[T, E]`. Removed as plain parse errors: `try`, `?:` (→ `.unwrapOr(..)`), `?.` (→ `?` then `.f`, or `.map(..)`), `catch` (→ `match`), `T?` and `E!T` type sugar, `error{..}` inline sets (→ an ordinary enum), `null` (→ `None`). `crash` stays (it is a panic, not error handling).
135
+ - **Also kept (user 2026-11-05, "Yes please"):** postfix `x!` as sugar for `x.unwrap()` on `Option`/`Result` (crashes on `None`/`Err`; works anywhere, chains like `?`). So the two sugars are `?` (propagate) and `!` (unwrap). `x := opt` becomes `Some(x) := opt`.
135
136
  2. `=` only reassigns; `:=` is the only declaration (no declare-on-first-use).
136
137
  3. Generators (`fun*`/`yield`) are removed; `Seq` and their std/example users (Seq.plum, Map, Int, List, functional/sequences examples) are rewritten on plain functions/loops.
137
138
  4. Variadics `...T` are removed; callers pass a list/slice literal (`List.init`, `Map(...)`).
docs/plans/2026-11-05-language-simplification-d.design.md ADDED
@@ -0,0 +1,37 @@
1
+ # Run D design: Option/Result with postfix `?`
2
+
3
+ Design study (2026-11-05) for decision record §13 item 1 (revised 2026-11-05). Run D is split into three plans run back to back on `master`:
4
+
5
+ - **D1 add** (`2026-11-05-language-simplification-d1.md`): std `Option`/`Result`, postfix `?` in grammar, parser, checker and codegen, coexisting with the old syntax. Baseline benchmark.
6
+ - **D2 ports**: std, examples, games and Rust test sources move to Option/Result.
7
+ - **D3 removal**: old syntax and machinery deleted (grammar/parser → codegen → checker → AST/passes/LSP, a forced chain), then verify.
8
+
9
+ ## Rulings (coordinator, AI orchestrator; user may override)
10
+ - `x!` (281 uses, mostly tests) STAYS as sugar for `x.unwrap()` (user 2026-11-05); `unwrap()` crashes on `None`/`Err`. Ports keep `x!` spellings.
11
+ - `x := opt` (plain-name bind in a condition, 83 uses) → `Some(x) := opt` (existing bind pattern).
12
+ - `Ok(Unit)` needs `Unit` as a value expression.
13
+ - `==` on Option/Result values is an error ("use match/isSome/unwrapOr"): enum `==` is identity today, so `Some(1) == Some(1)` would silently be false.
14
+ - Niche layout (`Option[R]` for a GC ref R as a nullable ref) is DEFERRED to the benchmark item. If D's numbers show >15% regression on for-over-List/Range or the stress fixtures, it goes to the top of that item.
15
+ - One error enum per module (`JsonError`, `IoError`, `HttpError`); across modules convert with `.mapErr`.
16
+ - While ports run, don't write `?.` (it still parses as the old safe navigation): bind `v := x?` then `v.f`.
17
+
18
+ ## Where the old machinery lives (to delete in D3)
19
+ - Grammar (~90 lines): PREC catch/elvis; `T?` and `Set!T` types (l.104-110); `error_set` (128-136); `error_decl` (228); `elvis_expression`, `catch_expression` (521-551); `error_value` (537); `null` (568, 872); `try_expression` (695); `bang_expression` (701); `safe_attribute` (704). Keep `bind_condition`/`bind_pattern`. Corpus to delete: optionals, try, zig_errors, safe_attribute; edit errors, type, annotated_decl; queries highlights/format; vscode tmLanguage.
20
+ - Parser (~230): ErrorSet item (139-150), optionalOf/error-union/errorSetParts (626-705), null (1194), parseCatch (1345), Expr::Try (1495), parseSafeAttribute (1504-1560), bang.
21
+ - AST (~70): Item::ErrorSet/ErrorSetDecl, ERROR_UNION_TYPE_NAME, ERROR_SET_TYPE_NAME, NULL_ERR_NAME, Expr::{Bang,Catch,ErrorValue}, CatchExpr, CatchHandler, BangExpr. `T?` is encoded as `error{null}!T` internally.
22
+ - plum-core passes (~200): arms in visit, drive (788, 1761), simplify (208), purity, uniqueness, closure_spec, closure_escape, rename, whistle, tail, trmc (192), unions, builtin_usage, loader, forms (`uniqueCatchBindersIn` 478-560).
23
+ - Checker (~1400): types.rs TErr/TErrName/ErrSet/FN_ERR_SETS/NAMED_ERR_SETS/errSetNames; subtyping.rs `atTop` (keep `nested`); state.rs err_acc/err_edges/err_sites; lib.rs errSetPartNames..bindFnSet (316-445), ERR_WRAP*/isOptional*/recordErrCoercion/errCoercionKinds/nullableErrPayload (457-617), errView/addToErrSet/unifyErr (694-830), fillErrorTables (1469-1622), checkErrorSets (1879-2169), inferErrorSets (2170-2285), recordErrSite/checkCatchBlock (4507-4610), Try/Catch/Bang/ErrorValue + plain-name Bind arms in inferExprInner (~4745+); TypedInfo err_sites/fn_error_sets/error_codes.
24
+ - Codegen (~750): layout 743-1135, err_box, ERR_WRAP coercions 6404-6450, 6451-6886 (ErrOperand, emitAbsent, emitErrReturn, compileErrTry/Bang/Catch/AndThen, compileTagName, optional compileBind, nullCompareOperand), walker arms 3647-3702, 3976-4010, 4264-4351, 5128.
25
+ - Host/runtime/game bridge: nothing (no extern returns T? or T!E). LSP: keyword lists (completion.rs 33,59,60; main.rs 1029-1033), Item::ErrorSet symbol (completion.rs 112).
26
+
27
+ ## How `?` works
28
+ Grammar: `question_expression` = primary + `token.immediate("?")` at PREC.call (shape of bang). Parser maps it onto the existing `Expr::Try`, so the ~15 passes that treat Try as an early exit stay correct. Checker Try arm: operand `Option[t]` needs the return type `Option[_]`; `Result[t, e]` needs `Result[_, e2]` with `unify(e, e2)` (error names `.mapErr(..)`); result type `t`; closures/tests drop RETURN_KEY so `?` there errors; a TErr operand keeps the old path while both coexist. Codegen: operand to a scratch local, `ref.test` Some/Ok; on failure Option returns the return type's None singleton, Result casts Err, reads `error`, `struct.new`s the return type's Err, returns; else cast + `struct.get value`.
29
+
30
+ ## Migration scope (D2)
31
+ ~1000 lines in ~50 .plum files (`null` 188, `catch` 134, `Set!T` 128, `error.X` 94, `x := opt` 83, `?:` 68, `try` 64, `?.` 10, `error` decls 5, `x!` 281, `.name()` 11), ~180 signatures. Largest: errors.plum 207, Json 155, Os 96, Slice 67, FunctionalScene 50, Map 47, generics 43, Scene 42, SceneInput 33, Ui 31, PureScene 26, Http 23, functional/structs_records 25. Implicit wraps (`return h` in a `T?` fn) become `Some(h)`/`Ok(h)`. Ordering: a module after its imports; the cycle Int↔Str↔Slice↔Float↔Array↔Buffer↔Byte↔Random↔Math↔Pair is one task together with `forms::lowerNext` (emit `Some(step)`) and Int's `Range.next`. Port waves: P1 core cycle + forms.rs + for_next_test; P2 List+Testing | Map+Set | Deque+Heap+Grid | Bool+Base64+Uuid | game/Affine+Vec2+Color+Game+Io; P3 Json | Os+Time | game/Input; P4 Http | functional/{FunctionalScene,Anim} | imperative/Scene | pure/Actions; P5 functional/Ui | pure/PureScene; P6 pure/SceneInput; examples by import; games + plum-game fixtures last. Rust test sources: RS1 codegen_tests.rs; RS2 e2e_types/{closure_optional,type_param_static}, e2e_tail/{error_tail,tail}, examples_test; RS3 checker_tests, features/{types,optional_unpinned,main}, fusion_pipeline_test; RS4 plum-core tests tail_test/simplify_test/record_init_test, src/tests_lower.rs, plum-cli/tests/game_web_test.rs. `error_union_tests.rs` is deleted in D3.
32
+
33
+ ## Removal (D3), forced chain
34
+ R1 grammar + parser removal (syntax errors for `-> Int?`, `try f()`, `x catch 0`, `a ?: b`, `error E = A | B`, `error{A}!Int`; `x?.f` parses as `(x?).f`) → R3 codegen removal → R2 checker removal (plain-name bind errors "use Some(x) := v") → R4 AST, passes, LSP → V verify (workspace, sweep, browser checks, benchmark vs D1's baseline).
35
+
36
+ ## Risks
37
+ Allocation: iterator `next() -> Option[Pair[T, Self]]` adds a Some cell per step; Scene `mut parent: Option[Actor]` reads cost ref.test+cast; Json Result per parse step; Map.get/Slice.get in game loops. `?` blocks drive/TRMC. Subtyping is invariant under generics: `Some(ship)` into `Option[Actor]` needs the expected type to flow into the argument. Http maps IoError/JsonError into HttpError itself. Generic statics `T.fromJson -> Result[T, JsonError]` (type_param_static).
docs/plans/2026-11-05-language-simplification-d1.md ADDED
@@ -0,0 +1,119 @@
1
+ # Language simplification, run D1: add Option/Result and postfix `?`
2
+
3
+ **Review plan:** docs/plans/2026-11-04-language-simplification.review.md
4
+ **Goal:** std `Option[T]`/`Result[T, E]` enums and postfix `?` (early return on None/Err) work end to end, coexisting with the old error syntax (ported in D2, removed in D3). Design: docs/plans/2026-11-05-language-simplification-d.design.md.
5
+ **Architecture:** plum-std, grammar, plum-core parser, plum-checker, plum-wasm-codegen.
6
+ **Tech stack:** Rust 2021, tree-sitter, Plum, wasm-gc.
7
+ **Detail:** contracts
8
+ **Target-min:** 30
9
+
10
+ ## Global constraints
11
+ - Work on `master` in the main checkout, default shared `target/` (no CARGO_TARGET_DIR: per-agent dirs filled the disk). Don't commit.
12
+ - No `cargo test` (hook): `cargo build --tests -p <crate>`, run binaries in `target/debug/deps` (newest by mtime). Plum: `cargo build -p plum-cli && target/debug/plum test <files>`; full sweep `target/debug/plum test plum-std plum-examples` (653 at start, must stay green).
13
+ - Grammar: `tree-sitter generate --disable-optimizations`. Never run `plum format` on real files.
14
+ - Old syntax (`T?`, `E!T`, `try`, `catch`, `?:`, `?.`, `null`, error sets) keeps working unchanged in D1.
15
+ - Rulings: `Option[T] = None | Some(value: T)`, `Result[T, E] = Ok(value: T) | Err(error: E)`. `?` on `Option[t]` only inside a fn returning `Option[_]`; on `Result[t, e]` only inside a fn returning `Result[_, e2]` with e == e2 (else error suggesting `.mapErr(..)`); `?` in a closure or test errors. `unwrap()` crashes on None/Err. `==` on Option/Result values is an error ("use match, isSome or unwrapOr"). `Unit` is a value expression (`Ok(Unit)`).
16
+
17
+ ## Task graph
18
+ ```mermaid
19
+ flowchart LR
20
+ T0[baseline benchmark]
21
+ T1[std Option + Result]
22
+ T2[grammar postfix ?]
23
+ T3[parser ?]
24
+ T4[checker ?]
25
+ T5[codegen ?]
26
+ T6[sweep]
27
+ %% taskgraph
28
+ T5 --> T6
29
+ T2 --> T3
30
+ T3 --> T4
31
+ T4 --> T5
32
+ T1 --> T5
33
+ ```
34
+ Ruling: sequential [chain] - grammar, parser, checker, codegen each consume the previous layer's output - cost: T3-T5 run alone.
35
+
36
+ ## Package A: std and grammar
37
+ **Probe entry:** run=plum test plum-std plum-examples
38
+
39
+ ### T0 baseline benchmark · Complexity 2 · Est. context 20k
40
+ **Owns:** docs/plans/2026-11-05-language-simplification-d.benchmark.md
41
+ **Components:** 1
42
+ **LOC owned:** 40
43
+ **Tests:** slow
44
+ **Model:** sonnet
45
+ **Produces:** baseline medians for D3's comparison
46
+ **Notes:** on today's compiler, before any D change lands (build `plum-cli` and the web-test once, release): a .plum probe file under the scratchpad timing for-over-List 1M, for-over-Range 1M, Map.get 1M, Json decode of ~100 KB (`target/debug/plum run` or `test`, 5 runs, median); `cargo run --release --manifest-path plum-game/web-test/Cargo.toml -- plum-game/web-test/fixtures/stress_scene.plum --measure 10 --uncapped --headed --json` and the same for stress_world.plum, 3 runs each, median, with `uptime` load. Keep the probe source in the doc.
47
+ **Test code:** the doc has a table with every probe, the command and the median.
48
+
49
+ ### T1 std Option and Result · Complexity 3 · Est. context 25k
50
+ **Owns:** plum-std/Option.plum, plum-std/Result.plum
51
+ **Components:** 1
52
+ **LOC owned:** 140
53
+ **Tests:** fast
54
+ **Model:** sonnet
55
+ **Produces:** std `Option[T]` and `Result[T, E]` enums with methods
56
+ e.g. `o: Option[Int] = None` -> `o.unwrapOr(7) == 7`; `Some(3).map(|x| x + 1).unwrapOr(0) == 4`
57
+ e.g. `r: Result[Int, Int] = Err(5)` -> `r.mapErr(|e| e + 1).isErr() == true`; `Ok(2).ok().isSome() == true`; `None.okOr(1).isErr() == true`
58
+ **Notes:** Option: map, andThen, unwrapOr, unwrap, isSome, isNone, okOr. Result: map, mapErr, andThen, unwrapOr, unwrap, isOk, isErr, ok. `unwrap` crashes with a clear message. The two files import each other (loader handles cycles). Use only syntax that exists today (no `?`).
59
+ **Test code:** `test` blocks for every method incl. the e.g. lines; `plum test plum-std/Option.plum plum-std/Result.plum` green.
60
+
61
+ ### T2 grammar: postfix ? · Complexity 3 · Est. context 25k
62
+ **Owns:** plum-tooling/tree-sitter-plum/grammar.js, plum-tooling/tree-sitter-plum/src/*.c, plum-tooling/tree-sitter-plum/src/*.json, plum-tooling/tree-sitter-plum/src/tree_sitter/*.h, plum-tooling/tree-sitter-plum/test/corpus/question.txt, plum-tooling/tree-sitter-plum/queries/plum/*.scm
63
+ **Components:** 1
64
+ **LOC owned:** 40
65
+ **Tests:** fast
66
+ **Model:** sonnet
67
+ **Produces:** `question_expression` node (primary + `token.immediate("?")`, PREC.call)
68
+ e.g. `f(x)?` -> (question_expression (call ...)); `a.b()?` -> question_expression over the method call
69
+ **Test code:** corpus: the e.g. lines; `x?.f` is still safe_attribute; `a ?: b` still elvis; `-> Int?` still a type; whole corpus green.
70
+
71
+ ## Package B: compiler
72
+ **Probe entry:** run=plum test plum-std plum-examples
73
+
74
+ ### T3 parser: ? to Expr::Try · Complexity 3 · Est. context 30k
75
+ **Owns:** plum-core/src/parser.rs, plum-core/tests/question_parse_test.rs
76
+ **Components:** 1
77
+ **LOC owned:** 40
78
+ **Tests:** fast
79
+ **Model:** sonnet
80
+ **Consumes:** `question_expression` node (primary + `token.immediate("?")`, PREC.call) (T2)
81
+ **Produces:** postfix `?` parses to the existing `Expr::Try`
82
+ e.g. `n * f()?` -> Binary(Mul, Var n, Try(FnCall f))
83
+ **Test code:** e.g. line; `a.b()?` -> Try(method call); all parser_test green.
84
+
85
+ ### T4 checker: ? on Option and Result · Complexity 5 · Est. context 40k
86
+ **Owns:** plum-checker/src/lib.rs, plum-checker/tests/question_tests.rs
87
+ **Components:** 1
88
+ **LOC owned:** 160
89
+ **Tests:** fast
90
+ **Model:** sonnet
91
+ **Consumes:** postfix `?` parses to the existing `Expr::Try` (T3)
92
+ **Produces:** checker types `?` on Option/Result, `Unit` values, rejects `==` on Option/Result
93
+ **Notes:** also postfix `x!` (existing `Expr::Bang`) on `Option[t]`/`Result[t, e]` types as `t` anywhere (sugar for `.unwrap()`; user 2026-11-05).
94
+ **Review Focus:** state: a TErr operand of Try/Bang keeps today's path; only `TApp("Option"|"Result")` operands take the new rules. Tests declare the two enums inline.
95
+ **Test code:** `?` on Option in an Option fn -> ok, type t; on Option in a Result fn -> error; `Result[Int, A]` `?` in a `Result[Int, B]` fn -> error naming mapErr; `?` in a closure -> error; `fun f(s: Ship) -> Option[Actor] = Some(s)` checks; `Ok(Unit)` checks; `Some(1) == Some(1)` -> the `==` error; `match r` with `Err(NoSuchPath) =>` nested patterns exhaustive; checker_tests green.
96
+
97
+ ### T5 codegen: ? on Option and Result · Complexity 5 · Est. context 40k
98
+ **Owns:** plum-wasm-codegen/src/lib.rs, plum-wasm-codegen/tests/e2e_types/question.rs, plum-wasm-codegen/tests/e2e_types/main.rs
99
+ **Components:** 1
100
+ **LOC owned:** 120
101
+ **Tests:** fast
102
+ **Model:** sonnet
103
+ **Consumes:** checker types `?` on Option/Result, `Unit` values, rejects `==` on Option/Result (T4) · std `Option[T]` and `Result[T, E]` enums with methods (T1)
104
+ **Produces:** `?` runs end to end
105
+ **Review Focus:** numeric: the Err is rebuilt in the return type's Err (`Result[U,E]` and `Result[T,E]` are distinct wasm types); None is the return type's singleton.
106
+ **Test code:** e2e_types/question.rs using std Option/Result: three chained `?` return the first Err; Option `?` returns None; `Ok(Unit)`; `1 + f()?` in expression position; a struct payload in Some; `Some(3)!` == 3 and `None!` crashes (Bang on Option/Result = unwrap), chained `f()!.g()!`. e2e_types green.
107
+
108
+ ### T6 D1 sweep · Complexity 2 · Est. context 20k
109
+ **Owns:** docs/plans/2026-11-05-language-simplification-d1.results.md
110
+ **Components:** 1
111
+ **LOC owned:** 30
112
+ **Tests:** slow
113
+ **Model:** sonnet
114
+ **Consumes:** `?` runs end to end (T5)
115
+ **Produces:** D1 results: every workspace test binary and the `plum test` sweep green
116
+ **Test code:** `cargo build --workspace --tests`, all binaries; `target/debug/plum test plum-std plum-examples` (653 + T1's tests); report red, don't fix.
117
+
118
+ ## Review Focus
119
+ - A: encoding, state. B: deep, numeric (early-return paths, wasm types of rebuilt Err).