plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
c3f68dd
— Peter John
2026-07-24T12:05:52+05:30
docs: add design spec for guard-clause match arms
cmake.hs
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
struct Cat {
|
|
2
|
-
val name: Str
|
|
3
|
-
val age: Int
|
|
4
|
-
|
|
5
|
-
fn init(allocator: *Allocator) Cat {
|
|
6
|
-
return Cat {
|
|
7
|
-
name: Str.alloc(allocator)
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
enum ReadError =
|
|
13
|
-
| Eof
|
|
14
|
-
| Closed
|
|
15
|
-
|
|
16
|
-
enum WriteError =
|
|
17
|
-
| Eof
|
|
18
|
-
| Closed
|
|
19
|
-
|
|
20
|
-
fn getTime(dt: DetermineTime) Int? =
|
|
21
|
-
if dt == 123 then
|
|
22
|
-
Some(dt)
|
|
23
|
-
else
|
|
24
|
-
None
|
|
25
|
-
|
|
26
|
-
fn compare(x: Int y: Int) -> Ordering = match
|
|
27
|
-
| x > y = GT
|
|
28
|
-
| x == y = EQ
|
|
29
|
-
| x < y = LT
|
|
30
|
-
|
|
31
|
-
fn countThrows(n: Int) -> Array<Array<Int>> = do
|
|
32
|
-
x <- 1 .. 6
|
|
33
|
-
y <- 1 .. 6
|
|
34
|
-
if x + y == n then
|
|
35
|
-
[x, y]
|
|
36
|
-
else
|
|
37
|
-
empty
|
|
38
|
-
|
|
39
|
-
getTime()
|
|
40
|
-
|> putLnStr
|
|
41
|
-
|> Task.err
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
# for is a builder function which exposes continue/break methods
|
|
45
|
-
for (items) \d ->
|
|
46
|
-
if d == "5" then
|
|
47
|
-
break()
|
|
48
|
-
putLnStr(d)
|
docs/superpowers/specs/2026-07-24-guard-clause-match-design.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Guard-Clause Match Arms — Design Spec
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Add a second form of `match` expression: one with no subject expression, where
|
|
6
|
+
each arm is a boolean guard condition instead of a value pattern, e.g.:
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
compare(x: Int, y: Int) -> Ordering =
|
|
10
|
+
match
|
|
11
|
+
| x > y => GT
|
|
12
|
+
| x == y => EQ
|
|
13
|
+
| _ => LT
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
This lets a function express a cascade of independent boolean conditions
|
|
17
|
+
without threading a placeholder subject through `match` or writing an
|
|
18
|
+
`if`/`else if`/`else` chain by hand. It is purely additive: the existing
|
|
19
|
+
subject-ful `match` form (`match expr \n pattern => body`) is completely
|
|
20
|
+
unchanged.
|
|
21
|
+
|
|
22
|
+
## Non-goals
|
|
23
|
+
|
|
24
|
+
- No new stdlib types (no `Ordering` enum) — `GT`/`EQ`/`LT` in the example
|
|
25
|
+
above are illustrative; a caller brings their own type.
|
|
26
|
+
- No exhaustiveness *proof* for guard matches — guard conditions are
|
|
27
|
+
arbitrary runtime booleans, not statically enumerable like an enum's
|
|
28
|
+
variants, so there is no compile-time exhaustiveness check for this form
|
|
29
|
+
(mirrors how today's pattern-match exhaustiveness check is itself only a
|
|
30
|
+
codegen-time affordance for enums with no wildcard, not a hard error).
|
|
31
|
+
- No change to the `fn` keyword, parameter syntax, or case-body arrow
|
|
32
|
+
(`=>`) — this spec is scoped to guard-clause arms only, reusing every
|
|
33
|
+
other piece of existing syntax as-is.
|
|
34
|
+
|
|
35
|
+
## Current state
|
|
36
|
+
|
|
37
|
+
- `match`'s grammar (`tooling/tree-sitter-plum/grammar.js`, `match` rule) requires
|
|
38
|
+
one or more subject expressions (`commaSep1(field("subject", $.expression))`,
|
|
39
|
+
no `optional()`); there is no subject-less form today.
|
|
40
|
+
- `case`'s patterns (`case_pattern` rule) are literals, constructor patterns,
|
|
41
|
+
dotted names, bare names, or `_` — never a full boolean expression like `x > y`.
|
|
42
|
+
- `plum-core/src/ast.rs`'s `CasePattern` enum has no variant for an arbitrary
|
|
43
|
+
expression.
|
|
44
|
+
- `plum-checker`'s `check_match` type-checks each case's patterns against the
|
|
45
|
+
subject types 1:1, erroring if the counts don't match; there is no code path
|
|
46
|
+
for zero subjects today.
|
|
47
|
+
- `plum-wasm-codegen`'s `compile_match` always evaluates and scratch-stores
|
|
48
|
+
every subject, then dispatches per-pattern-kind (int/string/float/class/name/
|
|
49
|
+
wildcard) with constructor-fallthrough logic — considerably more machinery
|
|
50
|
+
than a guard match needs.
|
|
51
|
+
|
|
52
|
+
## Design
|
|
53
|
+
|
|
54
|
+
### Grammar
|
|
55
|
+
|
|
56
|
+
`match` gains a second alternative via `choice`, disambiguated by whether the
|
|
57
|
+
line has a subject expression before the indent:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
match: ($) =>
|
|
61
|
+
prec.left(
|
|
62
|
+
choice(
|
|
63
|
+
seq(
|
|
64
|
+
"match",
|
|
65
|
+
commaSep1(field("subject", $.expression)),
|
|
66
|
+
$._indent,
|
|
67
|
+
repeat(field("case", $.case)),
|
|
68
|
+
$._dedent,
|
|
69
|
+
),
|
|
70
|
+
seq(
|
|
71
|
+
"match",
|
|
72
|
+
$._indent,
|
|
73
|
+
repeat(field("case", $.guard_case)),
|
|
74
|
+
$._dedent,
|
|
75
|
+
),
|
|
76
|
+
),
|
|
77
|
+
),
|
|
78
|
+
|
|
79
|
+
guard_case: ($) =>
|
|
80
|
+
seq(
|
|
81
|
+
"|",
|
|
82
|
+
field("guard", choice($.expression, "_")),
|
|
83
|
+
"=>",
|
|
84
|
+
field("body", choice($.expression, $.body)),
|
|
85
|
+
),
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The existing `case`/`case_pattern`/`class_pattern` rules are untouched. Guard
|
|
89
|
+
arms are a structurally distinct rule (`guard_case`), not a new
|
|
90
|
+
`case_pattern` alternative — a guard is a full expression (`x > y`, `x == y`),
|
|
91
|
+
never a literal/constructor/name pattern, so keeping the two arm kinds as
|
|
92
|
+
separate grammar rules avoids any ambiguity between "is this a pattern or an
|
|
93
|
+
expression" at parse time.
|
|
94
|
+
|
|
95
|
+
### AST
|
|
96
|
+
|
|
97
|
+
One new variant on the existing `CasePattern` enum
|
|
98
|
+
(`plum-core/src/ast.rs`):
|
|
99
|
+
|
|
100
|
+
```rust
|
|
101
|
+
pub enum CasePattern {
|
|
102
|
+
Class { name: String, fields: Vec<CasePattern> },
|
|
103
|
+
String(String),
|
|
104
|
+
Int(i64),
|
|
105
|
+
Float(f64),
|
|
106
|
+
Name(String),
|
|
107
|
+
Wildcard,
|
|
108
|
+
Guard(Expr),
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
No new top-level struct. A subject-less `match` parses to the existing
|
|
113
|
+
`Match { subjects: vec![], cases }` shape, where each `Case` has exactly one
|
|
114
|
+
pattern: either `CasePattern::Guard(expr)` for a boolean condition, or
|
|
115
|
+
`CasePattern::Wildcard` for a bare `_` guard arm (the parser recognizes the
|
|
116
|
+
literal `_` token in guard position and emits the existing `Wildcard`
|
|
117
|
+
variant — not a `Guard` wrapping a trivial "true" expression — so every other
|
|
118
|
+
part of the pipeline that already knows how to handle `Wildcard` needs no new
|
|
119
|
+
knowledge of "guard-flavored wildcards").
|
|
120
|
+
|
|
121
|
+
`plum-core/src/parser.rs` gains a `guard_case`-parsing branch (parallel to
|
|
122
|
+
today's `case` parsing) that builds a `Case` with one `CasePattern::Guard` or
|
|
123
|
+
`CasePattern::Wildcard` pattern from each `guard_case` node, and `parse_match`
|
|
124
|
+
is extended so an empty subject list is valid (today's subject-collection
|
|
125
|
+
logic already just slices "named children before the first case node," so an
|
|
126
|
+
empty slice naturally falls out — no structural change needed there, only the
|
|
127
|
+
grammar's `optional`-equivalent `choice` unlocks it).
|
|
128
|
+
|
|
129
|
+
### Checker
|
|
130
|
+
|
|
131
|
+
`check_match` (`plum-checker/src/lib.rs`) branches on `m.subjects.is_empty()`:
|
|
132
|
+
|
|
133
|
+
- **Subject-ful (today's path, unchanged):** infer each subject's type, zip
|
|
134
|
+
patterns against subject types 1:1, `check_pattern` as today.
|
|
135
|
+
- **Guard (new path):** for each case (exactly one pattern per case, enforced
|
|
136
|
+
by the grammar), if the pattern is `CasePattern::Guard(expr)`, infer `expr`'s
|
|
137
|
+
type via `infer_expr` and unify it against `PlumType::TBool`, producing a
|
|
138
|
+
type error if a guard isn't boolean (e.g. `| x + 1 => ...`). If the pattern
|
|
139
|
+
is `CasePattern::Wildcard`, no type-check is needed (matches everything, same
|
|
140
|
+
as today's wildcard-pattern meaning). Every arm's body is still
|
|
141
|
+
`check_block`-ed against `declared_ret` exactly as today — this already
|
|
142
|
+
enforces that all arms return a consistent type, no new "unify across arms"
|
|
143
|
+
step is needed.
|
|
144
|
+
|
|
145
|
+
### Codegen
|
|
146
|
+
|
|
147
|
+
`plum-wasm-codegen` compiles a guard match as an if/else-if chain, reusing
|
|
148
|
+
whatever codegen the `if`/`else if`/`else` statement already uses (not routed
|
|
149
|
+
through `compile_match_arms_multi`'s subject-scratch-slot machinery, which
|
|
150
|
+
this form has no use for — no subject to evaluate/store, no destructuring, no
|
|
151
|
+
per-pattern-kind branching). Each `CasePattern::Guard(expr)` arm compiles its
|
|
152
|
+
guard as the `if`/`else if` condition and its body as that branch's body; a
|
|
153
|
+
`CasePattern::Wildcard` arm (if present) becomes the chain's trailing `else`.
|
|
154
|
+
If no `Wildcard` arm is present and every guard evaluates false at runtime,
|
|
155
|
+
the chain falls through to `Unreachable` (a trap) — consistent with today's
|
|
156
|
+
non-exhaustive pattern-match behavior, just determined at runtime instead of
|
|
157
|
+
compile time, since guard conditions aren't statically enumerable.
|
|
158
|
+
|
|
159
|
+
### Testing strategy
|
|
160
|
+
|
|
161
|
+
- Tree-sitter corpus (`tooling/tree-sitter-plum/test/corpus/match.txt`): add
|
|
162
|
+
cases for a subject-less guard match (with and without a trailing `_`
|
|
163
|
+
arm), verifying the new `guard_case` node shape.
|
|
164
|
+
- `plum-core` parser tests: add coverage for the new `guard_case` parsing
|
|
165
|
+
path (note: the survey found no dedicated existing `match` test in
|
|
166
|
+
`parser_test.rs` today — this is a good opportunity to add the first one,
|
|
167
|
+
scoped to this feature).
|
|
168
|
+
- `plum-checker` tests: a guard match with all-boolean guards type-checks;
|
|
169
|
+
a non-boolean guard (e.g. `| x + 1 => ...`) produces a type error; a guard
|
|
170
|
+
match with inconsistent arm-body return types produces the same kind of
|
|
171
|
+
error today's subject-ful match already produces for that case.
|
|
172
|
+
- `plum-wasm-codegen` tests: a guard match with a `_` fallback runs and
|
|
173
|
+
returns the expected arm's value for several inputs; a guard match with NO
|
|
174
|
+
`_` fallback traps at runtime when no guard is true (mirrors the existing
|
|
175
|
+
non-exhaustive-match trap test pattern already used for enum matches,
|
|
176
|
+
per the survey's note on `codegen_tests.rs`'s existing non-exhaustive-match
|
|
177
|
+
coverage).
|