plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
d7e5ff4
— Peter John
2026-07-20T21:40:35+05:30
feat(plum-core): parse closure literals and fn(...) type annotations
- plum-checker/src/lib.rs +13 -0
- plum-checker/src/monomorphize.rs +28 -0
- plum-core/src/ast.rs +11 -0
- plum-core/src/parser.rs +36 -5
- plum-core/tests/parser_test.rs +69 -0
- plum-wasm-codegen/src/lib.rs +16 -0
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 ¶m.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 ¶m.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
|
-
|
|
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
|
}
|