plum

#treesitter#compiler#wasm

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

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


2ccae10Peter John 2026-07-24T11:18:44+05:30
fix(plum-core): stop return type from being mistaken for method receiver
plum-core/src/parser.rs CHANGED
@@ -183,17 +183,30 @@ impl<'a> AstParser<'a> {
183
183
  // ---- functions --------------------------------------------------------
184
184
 
185
185
  fn parse_fn(&self, node: Node) -> Fn {
186
- // fn: fn_identifier type? "(" param,* ")" ("->" return_type)? "=" body_or_expr
186
+ // fn: fn_identifier type? "(" param,* ")" ("->" type)? "=" body_or_expr
187
- // Named children: fn_identifier, type?, param*, return_type?, body/expr
187
+ // Named children: fn_identifier, type?, param*, type?, body/expr
188
188
  let mut cursor = node.walk();
189
189
  let named: Vec<Node> = node.named_children(&mut cursor).collect();
190
190
 
191
191
  let name = named.first().map(|n| self.text(*n)).unwrap_or_default();
192
192
 
193
+ // The `<Cat>` receiver annotation and the function's `-> Type` return
194
+ // annotation both have kind "type" now, so they can't be told apart by
195
+ // kind alone. They're disambiguated by field instead: the grammar's
196
+ // `type` field (`field("type", optional(alias($.fn_type, $.type)))`)
197
+ // wraps only the receiver's own alias node, so `child_by_field_name`
198
+ // resolves it unambiguously.
199
+ //
200
+ // Note: the grammar's `returns` field, by contrast, wraps the whole
201
+ // `optional(seq("->", $.type))` — so `child_by_field_name("returns")`
202
+ // resolves to the anonymous `"->"` token (the first element of that
203
+ // seq), not the `type` node, and can't be used to find the return
204
+ // type here. Instead the return type is derived as "whichever `type`
205
+ // named child isn't the receiver".
206
+ let receiver_node = node.child_by_field_name("type");
207
+
193
208
  // type param — the `<Cat>` node has kind "type" and contains a type_identifier
194
- let type_param = named
209
+ let type_param = receiver_node
195
- .iter()
196
- .find(|n| n.kind() == "type")
197
210
  .and_then(|t| t.named_child(0))
198
211
  .map(|n| self.text(n));
199
212
 
@@ -203,7 +216,10 @@ impl<'a> AstParser<'a> {
203
216
  .map(|n| self.parse_param(*n))
204
217
  .collect();
205
218
 
219
+ let returns = named
220
+ .iter()
206
- let returns = node.child_by_field_name("returns").map(|n| self.parse_type(n));
221
+ .find(|n| n.kind() == "type" && Some(**n) != receiver_node)
222
+ .map(|n| self.parse_type(*n));
207
223
 
208
224
  // body is the last named child — it is either a `body` node (block)
209
225
  // or an expression node when the body is a single expression. The
plum-core/tests/parser_test.rs CHANGED
@@ -47,6 +47,24 @@ useClosure() -> Bool =
47
47
  assert!(closure.params.is_empty());
48
48
  }
49
49
 
50
+ #[test]
51
+ fn fn_without_receiver_but_with_return_type_has_no_type_param() {
52
+ let src = "main() -> Int =\n 0\n";
53
+ let source = parse(src);
54
+ let f = only_fn(&source);
55
+ assert_eq!(f.type_param, None, "return type must not be mistaken for a method receiver");
56
+ assert_eq!(f.returns.as_ref().map(|t| t.name.clone()), Some("Int".to_string()));
57
+ }
58
+
59
+ #[test]
60
+ fn fn_with_receiver_and_return_type_has_correct_type_param() {
61
+ let src = "toStr<Cat>() -> Str =\n \"x\"\n";
62
+ let source = parse(src);
63
+ let f = only_fn(&source);
64
+ assert_eq!(f.type_param, Some("Cat".to_string()));
65
+ assert_eq!(f.returns.as_ref().map(|t| t.name.clone()), Some("Str".to_string()));
66
+ }
67
+
50
68
  #[test]
51
69
  fn fn_value_type_param_parses_with_positional_types_and_return() {
52
70
  let src = "each(cb: fn(Int) -> Bool) -> Bool =\n True\n";