plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
660674c
— Peter John
2026-07-19T22:17:37+05:30
fix(tree-sitter-plum): remove Nil/try/except, allow inline match bodies
- README.md +5 -8
- plum-checker/tests/checker_tests.rs +9 -0
- plum-core/src/parser.rs +10 -3
- plum-wasm-codegen/tests/codegen_tests.rs +7 -0
- tooling/tree-sitter-plum/grammar.js +1 -3
- tooling/tree-sitter-plum/src/grammar.json +0 -0
- tooling/tree-sitter-plum/src/node-types.json +0 -0
- tooling/tree-sitter-plum/src/parser.c +0 -0
- tooling/tree-sitter-plum/src/scanner.c +1 -2
- tooling/tree-sitter-plum/test/corpus/literals.txt +0 -6
- tooling/tree-sitter-plum/test/corpus/match.txt +53 -0
README.md
CHANGED
|
@@ -102,10 +102,9 @@ escaped = "line one\nline two\ttabbed \"quoted\""
|
|
|
102
102
|
|
|
103
103
|
yes = True
|
|
104
104
|
no = False
|
|
105
|
-
nothing = Nil
|
|
106
105
|
```
|
|
107
106
|
|
|
108
|
-
`True`/`False` are built into the type checker/codegen as `Bool`'s two variants — you don't need to declare `enum Bool` yourself to use them.
|
|
107
|
+
`True`/`False` are built into the type checker/codegen as `Bool`'s two variants — you don't need to declare `enum Bool` yourself to use them.
|
|
109
108
|
|
|
110
109
|
Full example: [`examples/basics.plum`](examples/basics.plum), [`examples/strings.plum`](examples/strings.plum).
|
|
111
110
|
|
|
@@ -283,10 +282,9 @@ Construct a `type` value by calling its name with `field: value` pairs (any orde
|
|
|
283
282
|
|
|
284
283
|
```plum
|
|
285
284
|
match n
|
|
286
|
-
0 =>
|
|
287
|
-
|
|
285
|
+
0 => "zero" # inline body
|
|
288
286
|
1 =>
|
|
289
|
-
"one"
|
|
287
|
+
"one" # indented block body — both forms are accepted
|
|
290
288
|
_ =>
|
|
291
289
|
"many"
|
|
292
290
|
|
|
@@ -303,7 +301,7 @@ match opt
|
|
|
303
301
|
0
|
|
304
302
|
```
|
|
305
303
|
|
|
306
|
-
|
|
304
|
+
A case body can be a single inline expression right after `=>`, or an indented block — pick whichever reads better for that arm. Patterns can be: integer/float/string literals, a bare identifier (binds a new local to the subject's value), a bare capitalized tag (`True`, `False`, or any declared `enum` variant with no payload — compared, not bound), a constructor pattern (`Some(v)`, binding its argument), or `_` (wildcard). Multiple comma-separated subjects/patterns are accepted by the grammar but not yet lowered by codegen.
|
|
307
305
|
|
|
308
306
|
Full example: [`examples/match.plum`](examples/match.plum).
|
|
309
307
|
|
|
@@ -325,7 +323,6 @@ Some things parse and type-check but don't compile to wasm yet — `plum-wasm-co
|
|
|
325
323
|
- string interpolation (plain, non-interpolated string literals do compile)
|
|
326
324
|
- `match` patterns other than integer literals, bindings, wildcard, and `True`/`False`; non-Bool enum-tag and constructor (`Some(v)`) patterns aren't lowered yet
|
|
327
325
|
- multi-subject `match` (`match a, b`)
|
|
328
|
-
- `Nil` as a value
|
|
329
326
|
- user-defined generics (they type-check but aren't monomorphized)
|
|
330
327
|
|
|
331
|
-
|
|
328
|
+
`closure` (`|params| body`) exists in `grammar.js` but isn't wired into any reachable rule yet, so it doesn't actually parse in context.
|
plum-checker/tests/checker_tests.rs
CHANGED
|
@@ -163,6 +163,15 @@ fn match_binds_name_pattern_to_subject_type() {
|
|
|
163
163
|
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
#[test]
|
|
167
|
+
fn match_inline_case_body_type_checks() {
|
|
168
|
+
// Case bodies can be a single inline expression, not just an indented block.
|
|
169
|
+
let src = "main(a: Int) -> Int =\n match a\n 1 => 10\n _ => 0\n";
|
|
170
|
+
let source = parse(src);
|
|
171
|
+
let result = check_source(&source);
|
|
172
|
+
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
173
|
+
}
|
|
174
|
+
|
|
166
175
|
#[test]
|
|
167
176
|
fn match_true_false_are_variant_patterns_not_bindings_without_enum_decl() {
|
|
168
177
|
// True/False are built-in Bool variants — they must be recognized as tag
|
plum-core/src/parser.rs
CHANGED
|
@@ -429,7 +429,7 @@ impl<'a> AstParser<'a> {
|
|
|
429
429
|
}
|
|
430
430
|
|
|
431
431
|
fn parse_case(&self, node: Node) -> Case {
|
|
432
|
-
// case: commaSep1(case_pattern) "=>" body
|
|
432
|
+
// case: commaSep1(case_pattern) "=>" (expression | body)
|
|
433
433
|
let mut cursor = node.walk();
|
|
434
434
|
let named: Vec<Node> = node.named_children(&mut cursor).collect();
|
|
435
435
|
let patterns = named.iter()
|
|
@@ -437,8 +437,15 @@ impl<'a> AstParser<'a> {
|
|
|
437
437
|
.map(|n| self.parse_case_pattern(*n))
|
|
438
438
|
.collect();
|
|
439
439
|
let body = named.iter()
|
|
440
|
+
.find(|n| n.kind() != "case_pattern")
|
|
441
|
+
.map(|n| {
|
|
440
|
-
|
|
442
|
+
if n.kind() == "body" {
|
|
441
|
-
|
|
443
|
+
self.parse_block(*n)
|
|
444
|
+
} else {
|
|
445
|
+
let unwrapped = self.unwrap_expr_node(*n);
|
|
446
|
+
Block { stmts: vec![Stmt::Expr(self.parse_expression(unwrapped))] }
|
|
447
|
+
}
|
|
448
|
+
})
|
|
442
449
|
.unwrap_or(Block { stmts: vec![] });
|
|
443
450
|
Case { patterns, body }
|
|
444
451
|
}
|
plum-wasm-codegen/tests/codegen_tests.rs
CHANGED
|
@@ -138,6 +138,13 @@ fn match_binding_pattern_compiles() {
|
|
|
138
138
|
assert_valid(src);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
#[test]
|
|
142
|
+
fn match_inline_case_body_compiles() {
|
|
143
|
+
// Case bodies can be a single inline expression, not just an indented block.
|
|
144
|
+
let src = "main(a: Int) =\n match a\n 1 => 10\n _ => 0\n";
|
|
145
|
+
assert_valid(src);
|
|
146
|
+
}
|
|
147
|
+
|
|
141
148
|
#[test]
|
|
142
149
|
fn match_bool_variant_pattern_compiles() {
|
|
143
150
|
let src = "main(a: Bool) -> Int =\n match a\n True =>\n return 1\n False =>\n return 0\n";
|
tooling/tree-sitter-plum/grammar.js
CHANGED
|
@@ -51,7 +51,6 @@ module.exports = grammar({
|
|
|
51
51
|
']',
|
|
52
52
|
')',
|
|
53
53
|
'}',
|
|
54
|
-
'except',
|
|
55
54
|
],
|
|
56
55
|
conflicts: ($) => [],
|
|
57
56
|
inline: ($) => [$.generic_type, $.generic],
|
|
@@ -189,7 +188,6 @@ module.exports = grammar({
|
|
|
189
188
|
"=",
|
|
190
189
|
commaSep1($.expression),
|
|
191
190
|
),
|
|
192
|
-
try: ($) => prec.right(seq("try", optional($.fn_call))),
|
|
193
191
|
assert: ($) => seq("assert", $.expression),
|
|
194
192
|
return: ($) => prec.right(2, seq("return", optional($.expression))),
|
|
195
193
|
break: (_) => prec.left("break"),
|
|
@@ -233,7 +231,7 @@ module.exports = grammar({
|
|
|
233
231
|
),
|
|
234
232
|
),
|
|
235
233
|
|
|
236
|
-
case: ($) => seq(commaSep1($.case_pattern), "=>", field("body", $.body)),
|
|
234
|
+
case: ($) => seq(commaSep1($.case_pattern), "=>", field("body", choice($.expression, $.body))),
|
|
237
235
|
|
|
238
236
|
case_pattern: ($) =>
|
|
239
237
|
prec(
|
tooling/tree-sitter-plum/src/grammar.json
CHANGED
|
Binary file
|
tooling/tree-sitter-plum/src/node-types.json
CHANGED
|
Binary file
|
tooling/tree-sitter-plum/src/parser.c
CHANGED
|
Binary file
|
tooling/tree-sitter-plum/src/scanner.c
CHANGED
|
@@ -18,7 +18,6 @@ enum TokenType {
|
|
|
18
18
|
CLOSE_PAREN,
|
|
19
19
|
CLOSE_BRACKET,
|
|
20
20
|
CLOSE_BRACE,
|
|
21
|
-
EXCEPT,
|
|
22
21
|
};
|
|
23
22
|
|
|
24
23
|
typedef enum {
|
|
@@ -228,7 +227,7 @@ bool tree_sitter_plum_external_scanner_scan(void *payload, TSLexer *lexer, const
|
|
|
228
227
|
indent_length += 8;
|
|
229
228
|
skip(lexer);
|
|
230
229
|
} else if (lexer->lookahead == '#' && (valid_symbols[INDENT] || valid_symbols[DEDENT] ||
|
|
231
|
-
valid_symbols[NEWLINE]
|
|
230
|
+
valid_symbols[NEWLINE])) {
|
|
232
231
|
// If we haven't found an EOL yet,
|
|
233
232
|
// then this is a comment after an expression:
|
|
234
233
|
// foo = bar # comment
|
tooling/tree-sitter-plum/test/corpus/literals.txt
CHANGED
|
@@ -17,7 +17,6 @@ main() =
|
|
|
17
17
|
name = "plum"
|
|
18
18
|
a = True
|
|
19
19
|
b = False
|
|
20
|
-
c = Nil
|
|
21
20
|
sum = 1 + {{2 * 3} / 4}
|
|
22
21
|
# count = counter(10)
|
|
23
22
|
# e = {1 + 2}.mod(3).pow(2).sqrt()
|
|
@@ -111,11 +110,6 @@ main() =
|
|
|
111
110
|
(expression
|
|
112
111
|
(primary_expression
|
|
113
112
|
(type_identifier))))
|
|
114
|
-
(assign
|
|
115
|
-
(var_identifier)
|
|
116
|
-
(expression
|
|
117
|
-
(primary_expression
|
|
118
|
-
(type_identifier))))
|
|
119
113
|
(assign
|
|
120
114
|
(var_identifier)
|
|
121
115
|
(expression
|
tooling/tree-sitter-plum/test/corpus/match.txt
CHANGED
|
@@ -100,3 +100,56 @@ main() =
|
|
|
100
100
|
(expression
|
|
101
101
|
(primary_expression
|
|
102
102
|
(var_identifier))))))))))))
|
|
103
|
+
|
|
104
|
+
================================================================================
|
|
105
|
+
match - inline case bodies
|
|
106
|
+
================================================================================
|
|
107
|
+
|
|
108
|
+
main() =
|
|
109
|
+
match a
|
|
110
|
+
1 => printLn(a)
|
|
111
|
+
2 => printLn(b)
|
|
112
|
+
_ => printLn(c)
|
|
113
|
+
|
|
114
|
+
--------------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
(source
|
|
117
|
+
(fn
|
|
118
|
+
(fn_identifier)
|
|
119
|
+
(body
|
|
120
|
+
(match
|
|
121
|
+
(expression
|
|
122
|
+
(primary_expression
|
|
123
|
+
(var_identifier)))
|
|
124
|
+
(case
|
|
125
|
+
(case_pattern
|
|
126
|
+
(integer))
|
|
127
|
+
(expression
|
|
128
|
+
(primary_expression
|
|
129
|
+
(fn_call
|
|
130
|
+
(var_identifier)
|
|
131
|
+
(fn_argument_list
|
|
132
|
+
(expression
|
|
133
|
+
(primary_expression
|
|
134
|
+
(var_identifier))))))))
|
|
135
|
+
(case
|
|
136
|
+
(case_pattern
|
|
137
|
+
(integer))
|
|
138
|
+
(expression
|
|
139
|
+
(primary_expression
|
|
140
|
+
(fn_call
|
|
141
|
+
(var_identifier)
|
|
142
|
+
(fn_argument_list
|
|
143
|
+
(expression
|
|
144
|
+
(primary_expression
|
|
145
|
+
(var_identifier))))))))
|
|
146
|
+
(case
|
|
147
|
+
(case_pattern)
|
|
148
|
+
(expression
|
|
149
|
+
(primary_expression
|
|
150
|
+
(fn_call
|
|
151
|
+
(var_identifier)
|
|
152
|
+
(fn_argument_list
|
|
153
|
+
(expression
|
|
154
|
+
(primary_expression
|
|
155
|
+
(var_identifier))))))))))))
|