plum

#treesitter#compiler#wasm

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

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


d7e5ff4Peter John 2026-07-20T21:40:35+05:30
feat(plum-core): parse closure literals and fn(...) type annotations
plum-checker/src/lib.rs CHANGED
@@ -115,6 +115,11 @@ pub fn build_global_tables(source: &ast::Source) -> (TypeEnv, ClassEnv, MethodEn
115
115
  match &p.ty {
116
116
  ast::ParamType::Type(t) => plum_type_from_ast(t),
117
117
  ast::ParamType::Variadic(t) => plum_type_from_ast(t),
118
+ ast::ParamType::Fn(params, ret) => {
119
+ let param_types = params.iter().map(plum_type_from_ast).collect();
120
+ let ret_ty = ret.as_ref().map(|r| plum_type_from_ast(r)).unwrap_or(PlumType::TUnit);
121
+ PlumType::TFun(param_types, Box::new(ret_ty))
122
+ }
118
123
  }
119
124
  }).collect();
120
125
  let ret = f.returns.as_ref()
@@ -179,6 +184,11 @@ fn check_fn(f: &ast::Fn, global_env: &TypeEnv, ctx: &CheckCtx) -> Vec<CheckError
179
184
  let ty = match &p.ty {
180
185
  ast::ParamType::Type(t) => plum_type_from_ast(t),
181
186
  ast::ParamType::Variadic(t) => plum_type_from_ast(t),
187
+ ast::ParamType::Fn(params, ret) => {
188
+ let param_types = params.iter().map(plum_type_from_ast).collect();
189
+ let ret_ty = ret.as_ref().map(|r| plum_type_from_ast(r)).unwrap_or(PlumType::TUnit);
190
+ PlumType::TFun(param_types, Box::new(ret_ty))
191
+ }
182
192
  };
183
193
  env.insert(p.name.clone(), TypeScheme::mono(ty));
184
194
  }
@@ -419,6 +429,9 @@ pub fn infer_expr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plu
419
429
  },
420
430
  },
421
431
  ast::Expr::Paren(inner) => infer_expr(inner, env, ctx),
432
+ // TODO: closures are not yet type-checked; treat as an unconstrained type
433
+ // for now so downstream code can compile against `Expr::Closure`.
434
+ ast::Expr::Closure(_) => Ok(PlumType::TVar("_".to_string())),
422
435
  ast::Expr::Not(inner) => {
423
436
  let t = infer_expr(inner, env, ctx)?;
424
437
  unify(&PlumType::TBool, &t)?;
plum-checker/src/monomorphize.rs CHANGED
@@ -33,6 +33,9 @@ pub fn fn_generic_params(f: &ast::Fn) -> Vec<String> {
33
33
  match &p.ty {
34
34
  ast::ParamType::Type(t) => consider(&t.name),
35
35
  ast::ParamType::Variadic(t) => consider(&t.name),
36
+ // TODO: closures/fn-value params don't yet participate in generic
37
+ // parameter inference.
38
+ ast::ParamType::Fn(_, _) => {}
36
39
  }
37
40
  }
38
41
  if let Some(r) = &f.returns {
@@ -140,6 +143,8 @@ pub fn specialize_fn(f: &ast::Fn, subst: &Substitution, mangled_name: &str, new_
140
143
  ty: match &p.ty {
141
144
  ast::ParamType::Type(t) => ast::ParamType::Type(substitute_type(t, subst)),
142
145
  ast::ParamType::Variadic(t) => ast::ParamType::Variadic(substitute_type(t, subst)),
146
+ // TODO: substitution into fn-value param/return types is not yet supported.
147
+ ast::ParamType::Fn(params, ret) => ast::ParamType::Fn(params.clone(), ret.clone()),
143
148
  },
144
149
  default: p.default.clone(),
145
150
  }).collect(),
@@ -246,6 +251,11 @@ impl<'a> Monomorphizer<'a> {
246
251
  let ty = match &p.ty {
247
252
  ast::ParamType::Type(t) => crate::plum_type_from_ast(t),
248
253
  ast::ParamType::Variadic(t) => crate::plum_type_from_ast(t),
254
+ ast::ParamType::Fn(params, ret) => {
255
+ let param_types = params.iter().map(crate::plum_type_from_ast).collect();
256
+ let ret_ty = ret.as_ref().map(|r| crate::plum_type_from_ast(r)).unwrap_or(PlumType::TUnit);
257
+ PlumType::TFun(param_types, Box::new(ret_ty))
258
+ }
249
259
  };
250
260
  env.insert(p.name.clone(), TypeScheme::mono(ty));
251
261
  }
@@ -436,6 +446,8 @@ impl<'a> Monomorphizer<'a> {
436
446
  let gp = match &param.ty {
437
447
  ast::ParamType::Type(t) => t.name.clone(),
438
448
  ast::ParamType::Variadic(t) => t.name.clone(),
449
+ // TODO: fn-value params don't yet resolve to a generic parameter.
450
+ ast::ParamType::Fn(_, _) => String::new(),
439
451
  };
440
452
  if params.contains(&gp) {
441
453
  let arg_expr = match arg {
@@ -472,6 +484,8 @@ impl<'a> Monomorphizer<'a> {
472
484
  let n = match &p.ty {
473
485
  ast::ParamType::Type(t) => &t.name,
474
486
  ast::ParamType::Variadic(t) => &t.name,
487
+ // TODO: fn-value params don't yet participate in bare-generic resolution.
488
+ ast::ParamType::Fn(_, _) => continue,
475
489
  };
476
490
  if (self.classes_generic.contains_key(n.as_str()) || self.enums_generic_by_name.contains_key(n.as_str()))
477
491
  && !names.iter().any(|x| x == n)
@@ -497,6 +511,8 @@ impl<'a> Monomorphizer<'a> {
497
511
  let n = match &param.ty {
498
512
  ast::ParamType::Type(t) => t.name.clone(),
499
513
  ast::ParamType::Variadic(t) => t.name.clone(),
514
+ // TODO: fn-value params don't yet resolve to a bare generic reference.
515
+ ast::ParamType::Fn(_, _) => String::new(),
500
516
  };
501
517
  if refs.contains(&n) {
502
518
  let arg_expr = match arg {
@@ -665,6 +681,8 @@ impl<'a> Monomorphizer<'a> {
665
681
  }
666
682
  ast::Expr::Int(_) | ast::Expr::Float(_)
667
683
  | ast::Expr::Self_ | ast::Expr::Var(_) | ast::Expr::TypeName(_) => {}
684
+ // TODO: closure bodies don't yet get rewritten for generic call sites.
685
+ ast::Expr::Closure(_) => {}
668
686
  }
669
687
  Ok(())
670
688
  }
@@ -775,6 +793,11 @@ pub fn monomorphize_source(source: &ast::Source) -> Result<ast::Source, String>
775
793
  let param_types: Vec<PlumType> = specialized_method.params.iter().map(|p| match &p.ty {
776
794
  ast::ParamType::Type(t) => crate::plum_type_from_ast(t),
777
795
  ast::ParamType::Variadic(t) => crate::plum_type_from_ast(t),
796
+ ast::ParamType::Fn(params, ret) => {
797
+ let param_types = params.iter().map(crate::plum_type_from_ast).collect();
798
+ let ret_ty = ret.as_ref().map(|r| crate::plum_type_from_ast(r)).unwrap_or(PlumType::TUnit);
799
+ PlumType::TFun(param_types, Box::new(ret_ty))
800
+ }
778
801
  }).collect();
779
802
  let ret = specialized_method.returns.as_ref()
780
803
  .map(|r| crate::plum_type_from_ast(&ast::Type { name: r.name.clone(), generics: vec![] }))
@@ -793,6 +816,11 @@ pub fn monomorphize_source(source: &ast::Source) -> Result<ast::Source, String>
793
816
  let param_types: Vec<PlumType> = specialized_fn.params.iter().map(|p| match &p.ty {
794
817
  ast::ParamType::Type(t) => crate::plum_type_from_ast(t),
795
818
  ast::ParamType::Variadic(t) => crate::plum_type_from_ast(t),
819
+ ast::ParamType::Fn(params, ret) => {
820
+ let param_types = params.iter().map(crate::plum_type_from_ast).collect();
821
+ let ret_ty = ret.as_ref().map(|r| crate::plum_type_from_ast(r)).unwrap_or(PlumType::TUnit);
822
+ PlumType::TFun(param_types, Box::new(ret_ty))
823
+ }
796
824
  }).collect();
797
825
  let ret = specialized_fn.returns.as_ref()
798
826
  .map(|r| crate::plum_type_from_ast(&ast::Type { name: r.name.clone(), generics: vec![] }))
plum-core/src/ast.rs CHANGED
@@ -101,6 +101,9 @@ pub struct Param {
101
101
  pub enum ParamType {
102
102
  Type(Type),
103
103
  Variadic(Type),
104
+ /// `fn(Int, Str) -> Bool` — a function-value type annotation. Positional types
105
+ /// only, no param names (types don't need names).
106
+ Fn(Vec<Type>, Option<Box<Type>>),
104
107
  }
105
108
 
106
109
  #[derive(Debug, Clone, PartialEq)]
@@ -128,6 +131,12 @@ pub struct Block {
128
131
  pub stmts: Vec<Stmt>,
129
132
  }
130
133
 
134
+ #[derive(Debug, Clone, PartialEq)]
135
+ pub struct Closure {
136
+ pub params: Vec<String>,
137
+ pub body: Block,
138
+ }
139
+
131
140
  #[derive(Debug, Clone, PartialEq)]
132
141
  pub enum Stmt {
133
142
  Assign(Assign),
@@ -231,6 +240,8 @@ pub enum Expr {
231
240
  Self_,
232
241
  Var(String),
233
242
  TypeName(String),
243
+ /// `|params| body`
244
+ Closure(Box<Closure>),
234
245
  }
235
246
 
236
247
  #[derive(Debug, Clone, PartialEq)]
plum-core/src/parser.rs CHANGED
@@ -251,17 +251,17 @@ impl<'a> AstParser<'a> {
251
251
  }
252
252
 
253
253
  fn parse_param(&self, node: Node) -> Param {
254
- // param: var_identifier ":" (type | variadic_type) ("=" expression)?
254
+ // param: var_identifier ":" (type | variadic_type | fn_value_type) ("=" expression)?
255
255
  let name = node.named_child(0).map(|n| self.text(n)).unwrap_or_default();
256
- let ty = node.named_child(1).map(|n| {
256
+ let ty = node.named_child(1).map(|n| match n.kind() {
257
- if n.kind() == "variadic_type" {
257
+ "variadic_type" => {
258
258
  let inner = n.named_child(0)
259
259
  .map(|t| self.parse_type(t))
260
260
  .unwrap_or(Type { name: String::new(), generics: vec![] });
261
261
  ParamType::Variadic(inner)
262
- } else {
263
- ParamType::Type(self.parse_type(n))
264
262
  }
263
+ "fn_value_type" => self.parse_fn_value_type(n),
264
+ _ => ParamType::Type(self.parse_type(n)),
265
265
  }).unwrap_or(ParamType::Type(Type { name: String::new(), generics: vec![] }));
266
266
  let default = node.named_child(2).map(|n| {
267
267
  let unwrapped = self.unwrap_expr_node(n);
@@ -270,6 +270,22 @@ impl<'a> AstParser<'a> {
270
270
  Param { name, ty, default }
271
271
  }
272
272
 
273
+ fn parse_fn_value_type(&self, node: Node) -> ParamType {
274
+ // fn_value_type: "fn" "(" field("params", type,*) ")" ("->" field("returns", type))?
275
+ // The "returns" field (if present) is a distinct field from "params", so the
276
+ // two are disambiguated unambiguously by field name, not by counting/position
277
+ // among same-kind "type" children — the same idiom `fn`'s own `returns` field
278
+ // already uses.
279
+ let returns_node = node.child_by_field_name("returns");
280
+ let param_types: Vec<Type> = self.children_of_kind(node, "type")
281
+ .into_iter()
282
+ .filter(|n| Some(*n) != returns_node)
283
+ .map(|n| self.parse_type(n))
284
+ .collect();
285
+ let ret = returns_node.map(|n| Box::new(self.parse_type(n)));
286
+ ParamType::Fn(param_types, ret)
287
+ }
288
+
273
289
  fn parse_type(&self, node: Node) -> Type {
274
290
  // type: type_identifier ("[" type,* "]")?
275
291
  // named_child(0) = type_identifier
@@ -497,10 +513,25 @@ impl<'a> AstParser<'a> {
497
513
  }
498
514
  "boolean_operator" => self.parse_bool_op(node),
499
515
  "ternary_expression" => self.parse_ternary(node),
516
+ "closure" => Expr::Closure(Box::new(self.parse_closure(node))),
500
517
  _ => self.parse_primary_expression(node),
501
518
  }
502
519
  }
503
520
 
521
+ fn parse_closure(&self, node: Node) -> Closure {
522
+ // closure: "|" var_identifier,* "|" body
523
+ let params: Vec<String> = self.children_of_kind(node, "var_identifier")
524
+ .into_iter()
525
+ .map(|n| self.text(n))
526
+ .collect();
527
+ let body = self.children_of_kind(node, "body")
528
+ .into_iter()
529
+ .next()
530
+ .map(|n| self.parse_block(n))
531
+ .unwrap_or(Block { stmts: vec![] });
532
+ Closure { params, body }
533
+ }
534
+
504
535
  pub fn parse_primary_expression(&self, node: Node) -> Expr {
505
536
  let node = self.unwrap_expr_node(node);
506
537
  match node.kind() {
plum-core/tests/parser_test.rs ADDED
@@ -0,0 +1,69 @@
1
+ use plum_core::ast::*;
2
+ use plum_core::AstParser;
3
+
4
+ fn parse(src: &str) -> Source {
5
+ let mut parser = tree_sitter::Parser::new();
6
+ parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap();
7
+ let tree = parser.parse(src, None).unwrap();
8
+ assert!(!tree.root_node().has_error(), "parse error:\n{}", tree.root_node().to_sexp());
9
+ let ap = AstParser::new(src);
10
+ ap.parse_source(tree.root_node())
11
+ }
12
+
13
+ fn only_fn(source: &Source) -> &Fn {
14
+ source.items.iter().find_map(|i| match i { Item::Fn(f) => Some(f), _ => None }).expect("expected a Fn item")
15
+ }
16
+
17
+ #[test]
18
+ fn closure_literal_parses_with_params_and_body() {
19
+ let src = "\
20
+ useClosure() -> Bool =
21
+ cb = |v|
22
+ True
23
+ cb(5)
24
+ ";
25
+ let source = parse(src);
26
+ let f = only_fn(&source);
27
+ let FnBody::Block(block) = &f.body else { panic!("expected a block body") };
28
+ let Stmt::Assign(assign) = &block.stmts[0] else { panic!("expected an assign statement") };
29
+ let Expr::Closure(closure) = &assign.values[0] else { panic!("expected a closure expression, got {:?}", assign.values[0]) };
30
+ assert_eq!(closure.params, vec!["v".to_string()]);
31
+ assert_eq!(closure.body.stmts.len(), 1);
32
+ }
33
+
34
+ #[test]
35
+ fn closure_literal_parses_with_no_params() {
36
+ let src = "\
37
+ useClosure() -> Bool =
38
+ cb = ||
39
+ True
40
+ cb()
41
+ ";
42
+ let source = parse(src);
43
+ let f = only_fn(&source);
44
+ let FnBody::Block(block) = &f.body else { panic!("expected a block body") };
45
+ let Stmt::Assign(assign) = &block.stmts[0] else { panic!("expected an assign statement") };
46
+ let Expr::Closure(closure) = &assign.values[0] else { panic!("expected a closure expression") };
47
+ assert!(closure.params.is_empty());
48
+ }
49
+
50
+ #[test]
51
+ fn fn_value_type_param_parses_with_positional_types_and_return() {
52
+ let src = "each(cb: fn(Int) -> Bool) -> Bool =\n True\n";
53
+ let source = parse(src);
54
+ let f = only_fn(&source);
55
+ let ParamType::Fn(param_types, ret) = &f.params[0].ty else { panic!("expected ParamType::Fn, got {:?}", f.params[0].ty) };
56
+ assert_eq!(param_types.len(), 1);
57
+ assert_eq!(param_types[0].name, "Int");
58
+ assert_eq!(ret.as_ref().map(|t| t.name.clone()), Some("Bool".to_string()));
59
+ }
60
+
61
+ #[test]
62
+ fn fn_value_type_param_parses_with_no_return() {
63
+ let src = "each(cb: fn(Int)) -> Bool =\n True\n";
64
+ let source = parse(src);
65
+ let f = only_fn(&source);
66
+ let ParamType::Fn(param_types, ret) = &f.params[0].ty else { panic!("expected ParamType::Fn") };
67
+ assert_eq!(param_types.len(), 1);
68
+ assert!(ret.is_none());
69
+ }
plum-wasm-codegen/src/lib.rs CHANGED
@@ -232,6 +232,9 @@ fn param_type_name(pt: &ast::ParamType) -> &str {
232
232
  match pt {
233
233
  ast::ParamType::Type(t) => t.name.as_str(),
234
234
  ast::ParamType::Variadic(t) => t.name.as_str(),
235
+ // TODO: fn-value params aren't modeled as a wasm value type yet; treat as
236
+ // an unmodeled type (pointer), same as a class instance.
237
+ ast::ParamType::Fn(_, _) => "Fn",
235
238
  }
236
239
  }
237
240
 
@@ -522,6 +525,9 @@ impl<'a> Collector<'a> {
522
525
  | ast::Expr::Self_
523
526
  | ast::Expr::Var(_)
524
527
  | ast::Expr::TypeName(_) => {}
528
+ // TODO: closures aren't compiled yet; codegen for closure bodies lands
529
+ // in a later task.
530
+ ast::Expr::Closure(_) => {}
525
531
  }
526
532
  }
527
533
 
@@ -545,6 +551,11 @@ fn compile_fn_body(f: &ast::Fn, ctx: &CompileCtx, state: &mut ModuleState) -> Re
545
551
  let ty = match &p.ty {
546
552
  ast::ParamType::Type(t) => plum_checker::plum_type_from_ast(t),
547
553
  ast::ParamType::Variadic(t) => plum_checker::plum_type_from_ast(t),
554
+ ast::ParamType::Fn(params, ret) => {
555
+ let param_types = params.iter().map(plum_checker::plum_type_from_ast).collect();
556
+ let ret_ty = ret.as_ref().map(|r| plum_checker::plum_type_from_ast(r)).unwrap_or(PlumType::TUnit);
557
+ PlumType::TFun(param_types, Box::new(ret_ty))
558
+ }
548
559
  };
549
560
  base_env.insert(p.name.clone(), TypeScheme::mono(ty));
550
561
  }
@@ -1390,6 +1401,11 @@ fn compile_expr(expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mu
1390
1401
  state.data_segments.push((offset, data));
1391
1402
  Instruction::I32Const(offset as i32).encode(body);
1392
1403
  }
1404
+ // TODO: closure codegen (capturing free variables, emitting an indirect-callable
1405
+ // value) lands in a later task.
1406
+ ast::Expr::Closure(_) => {
1407
+ return Err("codegen: closures are not yet supported".to_string());
1408
+ }
1393
1409
  }
1394
1410
  Ok(())
1395
1411
  }