plum
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`.
|