plum

#treesitter#compiler#wasm

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

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


c3f68ddPeter 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).