plum

#treesitter#compiler#wasm

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

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


bb358d8 — Peter John 2026-10-27T18:10:58+05:30
docs(plans): optionals (null, ?:, if/while binding), try prefix, literal overflow fix
docs/plans/2026-10-27-optionals-try.md ADDED
@@ -0,0 +1,56 @@
1
+ ## Task NL (user decisions; runs after task N): optionals with `null`, Kotlin elvis `?:`, `if x := opt`, `while x := opt`, `!` unwrap
2
+
3
+ User: drop `error.Missing` and `Missing` entirely; the compiler handles null; add Zig-like sugar.
4
+
5
+ 1. **Optional type.**
6
+ - `T?` is a distinct optional type. It is no longer `error{Missing}!T`, and `Missing` is removed (no special error name, no hints).
7
+ - `null` is a keyword literal, valid wherever a `T?` is expected (return, tail, argument, field, default, list element, annotated binding). A `null` with no expected optional is an error: `cannot infer the type of null`.
8
+ - A plain `T` in a `T?` slot is the present value.
9
+ 2. **Sugar** (Kotlin elvis plus Zig-style binding `if`/`while`):
10
+ - `opt ?: default` (Kotlin elvis, chosen by the user over `orelse`/`??`); `opt ?: return …` (any diverging expression: `return null`, `return error.X`, `crash "…"`). Elvis binds looser than comparison and arithmetic and tighter than assignment. It is right-associative: `a ?: b ?: c`.
11
+ - `if x := opt` + block [`else` block], where `x: T` is bound only in the then-block. It also works as an inline if: `if x := opt then f(x) else 0`.
12
+ - `while x := opt` + block, which loops until the expression gives null and is re-evaluated each iteration (`while job := queue.next()`).
13
+ - `opt!` crashes with `<callee> returned null at file:line:col`, reusing the postfix-bang path. User decision: a single `!` works for both optionals and errors; there is no `!!`.
14
+ - `opt?.field` / `opt?.method()` give `U?`, flattening.
15
+ - `opt == null` / `opt != null` (both orders).
16
+ 3. **Combining.**
17
+ - `!T?` (an error union of an optional) is allowed and stays nested: `try x` gives `T?`, and `?:` then handles the null.
18
+ - `?: return error.X` turns absence into an error.
19
+ - `catch` applies only to error unions, and `?:` only to optionals. Each is a type error on the other: `catch is for errors; use ?:` / `?: is for optionals; use catch`.
20
+ 4. **Runtime is unchanged:** `ref null` for ref T, and `(T leaves…, i32 present-flag)` for primitive or value T, as today's T? struct or multi-value layout, but with a flag instead of an error code.
21
+ 5. **Std and migration.**
22
+ - Every std API returning "maybe absent" uses `T?` where absence is not an error. For example `List.find`, a `Map.getOrNull` style lookup, and `Seq.find` all return `T?`.
23
+ - Keep `!T` where callers should see a named error (`List.get` keeps `error{OutOfBounds}!T`).
24
+ - Migrate std, examples, README (a new "Optionals" section next to Error handling), embedded Rust tests and corpus.
25
+ - Highlighting: `null` as constant.language and `?:` as an operator, in highlights.scm and the VS Code grammar; LSP keywords (+null); reinstall the editors.
26
+
27
+ Tests:
28
+ - `null_return_absent` and `null_field_and_arg`;
29
+ - `null_without_context_error`;
30
+ - `elvis_default`, `elvis_return_error`, `elvis_return_null` and `elvis_chain_right_assoc`;
31
+ - `if_bind_optional` (inline too) and `while_bind_optional`;
32
+ - `bang_on_null_crashes_message`;
33
+ - `safe_nav_optional_flattens`;
34
+ - `null_eq_ref` and `null_eq_int`;
35
+ - `catch_on_optional_rejected` and `elvis_on_error_rejected`;
36
+ - `error_union_of_optional`.
37
+
38
+ ## NL addendum (user decision): `try` prefix replaces postfix `?` for error propagation; no ternary
39
+
40
+ - `try expr` propagates an error from `expr` (an error union): it unwraps the success value or returns the error from the enclosing fn, merging it into the fn's set. It has the same semantics as today's postfix `?`.
41
+ - Precedence is Zig's unary prefix operator, so postfix operators and calls bind tighter: `try a.b().c()` is `try (a.b().c())`, `n * try f()` is `n * (try f())`, and `try f() + 1` is `(try f()) + 1`.
42
+ - Postfix `?` for propagation is DELETED, with no hints.
43
+ - `?` now appears only in optionals: `T?`, `?.` and `?:`. The ternary is NOT reintroduced (user: confusing). The inline `if c then a else b` is the only conditional expression.
44
+ - `try` on an optional is a type error (`try is for errors; use ?: return null`). `try` inside a `fun*` gets the existing propagation message. `try` inside a closure stays unsupported (residual).
45
+ - The rest is unchanged: postfix `!` crashes (errors and nulls), `catch` handles errors, `?:` handles nulls.
46
+ - Migrate every postfix `?` in std, examples, README, embedded Rust tests and corpus to `try`. Highlighting: `try` keyword; LSP keywords (+try).
47
+
48
+ Tests:
49
+ - `try_propagates`;
50
+ - `try_precedence_unary` (`n * try f()`, `try a.b().c()`);
51
+ - `postfix_question_removed` (plain parse error);
52
+ - `try_on_optional_rejected`;
53
+ - `try_merges_error_sets`.
54
+
55
+ ## NL extra fix
56
+ - Integer literal overflow: `parseInteger` in plum-core/src/parser.rs falls back to `unwrap_or(0)`, so out-of-range literals such as `0x8000_0000_0000_0000` silently become 0. Make that a compile error (`integer literal out of range for Int`), allowing `-0x8000_0000_0000_0000` (i64::MIN) through negation of a literal. Restore `Int.MIN = -0x8000_0000_0000_0000`. Tests: `int_literal_overflow_error` and `int_min_literal`.