plum

#treesitter#compiler#wasm

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

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


660674cPeter John 2026-07-19T22:17:37+05:30
fix(tree-sitter-plum): remove Nil/try/except, allow inline match bodies
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. `Nil` parses and type-checks, but codegen doesn't have a runtime representation for it yet (see [Known gaps](#known-gaps)).
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
- "zero"
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
- Case bodies are always an indented block — there's no `pattern => expr` one-liner form yet. 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.
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
- A few grammar rules (`try`, `closure`, the `except` external token) exist in `grammar.js` but aren't wired into any reachable rule yet, so they don't actually parse in context.
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
- .find(|n| n.kind() == "body")
442
+ if n.kind() == "body" {
441
- .map(|n| self.parse_block(*n))
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] || valid_symbols[EXCEPT])) {
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))))))))))))