plum

#treesitter#compiler#wasm

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

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


1b220d2Peter John 2026-07-20T15:24:15+05:30
feat(plum-checker): core generics specialization primitives
plum-checker/src/lib.rs CHANGED
@@ -1,4 +1,5 @@
1
1
  pub mod types;
2
+ pub mod monomorphize;
2
3
 
3
4
  use std::collections::BTreeMap;
4
5
  use types::{PlumType, TypeEnv, TypeScheme, CheckError, CheckResult};
plum-checker/src/monomorphize.rs ADDED
@@ -0,0 +1,167 @@
1
+ use std::collections::BTreeMap;
2
+ use plum_core::ast;
3
+ use crate::types::PlumType;
4
+
5
+ /// A single lowercase letter (`a`, `b`, `c`, `d`, ...) is the grammar's only legal
6
+ /// spelling for a generic type parameter — this is how we recognize one, since
7
+ /// `ast::Fn` and `ast::Enum` (unlike `ast::Class`/`ast::Trait`) carry no explicit
8
+ /// generics declaration list.
9
+ pub fn is_generic_param_name(name: &str) -> bool {
10
+ let mut chars = name.chars();
11
+ match (chars.next(), chars.next()) {
12
+ (Some(c), None) => c.is_ascii_lowercase(),
13
+ _ => false,
14
+ }
15
+ }
16
+
17
+ /// The generic parameter names introduced by a `Class`, in declaration order.
18
+ pub fn class_generic_params(c: &ast::Class) -> Vec<String> {
19
+ c.generics.iter().map(|g| g.name.clone()).collect()
20
+ }
21
+
22
+ /// The generic parameter names implicitly introduced by a `Fn` — every distinct
23
+ /// single-lowercase-letter type name appearing in its params or return type, in
24
+ /// first-appearance order.
25
+ pub fn fn_generic_params(f: &ast::Fn) -> Vec<String> {
26
+ let mut names: Vec<String> = Vec::new();
27
+ let mut consider = |n: &str| {
28
+ if is_generic_param_name(n) && !names.iter().any(|x| x == n) {
29
+ names.push(n.to_string());
30
+ }
31
+ };
32
+ for p in &f.params {
33
+ match &p.ty {
34
+ ast::ParamType::Type(t) => consider(&t.name),
35
+ ast::ParamType::Variadic(t) => consider(&t.name),
36
+ }
37
+ }
38
+ if let Some(r) = &f.returns {
39
+ consider(&r.name);
40
+ }
41
+ names
42
+ }
43
+
44
+ /// The generic parameter names implicitly introduced by an `Enum` — every distinct
45
+ /// single-lowercase-letter variant field type name, in first-appearance order.
46
+ pub fn enum_generic_params(e: &ast::Enum) -> Vec<String> {
47
+ let mut names: Vec<String> = Vec::new();
48
+ for v in &e.variants {
49
+ for field_ty in &v.fields {
50
+ if is_generic_param_name(field_ty) && !names.iter().any(|x| x == field_ty) {
51
+ names.push(field_ty.clone());
52
+ }
53
+ }
54
+ }
55
+ names
56
+ }
57
+
58
+ /// A resolved binding from a generic item's parameter names to concrete types for
59
+ /// one instantiation site, e.g. `{"a": Int}` for `Box(value: 5)`.
60
+ #[derive(Debug, Clone)]
61
+ pub struct Substitution(pub BTreeMap<String, PlumType>);
62
+
63
+ impl Substitution {
64
+ fn get(&self, name: &str) -> Option<&PlumType> {
65
+ self.0.get(name)
66
+ }
67
+ }
68
+
69
+ /// Converts a resolved concrete `PlumType` back into the `ast::Type` shape needed
70
+ /// to substitute into a declared field/param/return type position. Only ever
71
+ /// called with types resolved from a real call-site argument's inferred type, so
72
+ /// `TVar`/`TFun` (which never arise from a concrete argument) are an internal-error
73
+ /// case rather than something this needs to model.
74
+ fn plum_type_to_ast_type(t: &PlumType) -> ast::Type {
75
+ let name = match t {
76
+ PlumType::TInt => "Int".to_string(),
77
+ PlumType::TFloat => "Float".to_string(),
78
+ PlumType::TBool => "Bool".to_string(),
79
+ PlumType::TStr => "Str".to_string(),
80
+ PlumType::TUnit => "Unit".to_string(),
81
+ PlumType::TNamed(n) => n.clone(),
82
+ PlumType::TVar(_) | PlumType::TFun(_, _) => t.to_string(),
83
+ };
84
+ ast::Type { name, generics: vec![] }
85
+ }
86
+
87
+ fn substitute_type(ty: &ast::Type, subst: &Substitution) -> ast::Type {
88
+ if ty.generics.is_empty() {
89
+ if let Some(concrete) = subst.get(&ty.name) {
90
+ return plum_type_to_ast_type(concrete);
91
+ }
92
+ }
93
+ ast::Type {
94
+ name: ty.name.clone(),
95
+ generics: ty.generics.iter().map(|g| substitute_type(g, subst)).collect(),
96
+ }
97
+ }
98
+
99
+ /// Mangles a generic item's base name and its resolved concrete type arguments
100
+ /// (in the item's own generic-parameter declaration order) into the internal name
101
+ /// used for its specialized copy, e.g. `Box` + `[Int]` -> `"Box$Int"`.
102
+ pub fn mangle(base: &str, type_args: &[PlumType]) -> String {
103
+ let mut out = base.to_string();
104
+ for t in type_args {
105
+ out.push('$');
106
+ out.push_str(&t.to_string());
107
+ }
108
+ out
109
+ }
110
+
111
+ /// Produces a concrete, specialized copy of a generic class under `mangled_name`,
112
+ /// substituting every field whose declared type names one of the class's generic
113
+ /// parameters with its resolved concrete type. The class's own `generics` list is
114
+ /// cleared on the copy (it is now fully concrete).
115
+ pub fn specialize_class(c: &ast::Class, subst: &Substitution, mangled_name: &str) -> ast::Class {
116
+ ast::Class {
117
+ name: mangled_name.to_string(),
118
+ implements: c.implements.clone(),
119
+ generics: vec![],
120
+ fields: c.fields.iter().map(|f| ast::Field {
121
+ name: f.name.clone(),
122
+ ty: substitute_type(&f.ty, subst),
123
+ }).collect(),
124
+ }
125
+ }
126
+
127
+ /// Produces a concrete, specialized copy of a generic function (or method) under
128
+ /// `mangled_name`. `new_type_param` overrides the receiver-type name for a method
129
+ /// whose receiver class was itself specialized (e.g. a method declared on `Box`
130
+ /// becomes a method on `Box$Int`); pass the original `f.type_param.clone()`
131
+ /// unchanged for a plain free function. The body is left structurally identical
132
+ /// here — its own call sites are rewritten separately (Task 2), since expressions
133
+ /// don't carry declared-type annotations the way fields/params/return types do.
134
+ pub fn specialize_fn(f: &ast::Fn, subst: &Substitution, mangled_name: &str, new_type_param: Option<String>) -> ast::Fn {
135
+ ast::Fn {
136
+ name: mangled_name.to_string(),
137
+ type_param: new_type_param,
138
+ params: f.params.iter().map(|p| ast::Param {
139
+ name: p.name.clone(),
140
+ ty: match &p.ty {
141
+ ast::ParamType::Type(t) => ast::ParamType::Type(substitute_type(t, subst)),
142
+ ast::ParamType::Variadic(t) => ast::ParamType::Variadic(substitute_type(t, subst)),
143
+ },
144
+ default: p.default.clone(),
145
+ }).collect(),
146
+ returns: f.returns.as_ref().map(|r| {
147
+ let substituted = substitute_type(&ast::Type { name: r.name.clone(), generics: vec![] }, subst);
148
+ ast::ReturnType { name: substituted.name, generics: vec![] }
149
+ }),
150
+ body: f.body.clone(),
151
+ }
152
+ }
153
+
154
+ /// Produces a concrete, specialized copy of a generic enum under `mangled_name`,
155
+ /// substituting every variant field type name that matches one of the enum's
156
+ /// generic parameters with its resolved concrete type's name.
157
+ pub fn specialize_enum(e: &ast::Enum, subst: &Substitution, mangled_name: &str) -> ast::Enum {
158
+ ast::Enum {
159
+ name: mangled_name.to_string(),
160
+ variants: e.variants.iter().map(|v| ast::EnumVariant {
161
+ name: v.name.clone(),
162
+ fields: v.fields.iter().map(|f| {
163
+ subst.get(f).map(|t| t.to_string()).unwrap_or_else(|| f.clone())
164
+ }).collect(),
165
+ }).collect(),
166
+ }
167
+ }
plum-checker/tests/monomorphize_tests.rs ADDED
@@ -0,0 +1,128 @@
1
+ use plum_checker::monomorphize::*;
2
+ use plum_checker::types::PlumType;
3
+ use plum_core::ast;
4
+
5
+ #[test]
6
+ fn is_generic_param_name_accepts_single_lowercase_letters_only() {
7
+ assert!(is_generic_param_name("a"));
8
+ assert!(is_generic_param_name("d"));
9
+ assert!(!is_generic_param_name("Int"));
10
+ assert!(!is_generic_param_name("ab"));
11
+ assert!(!is_generic_param_name("A"));
12
+ assert!(!is_generic_param_name(""));
13
+ }
14
+
15
+ #[test]
16
+ fn class_generic_params_reads_declared_generics_list() {
17
+ let c = ast::Class {
18
+ name: "Box".to_string(),
19
+ implements: vec![],
20
+ generics: vec![ast::GenericParam { name: "a".to_string(), bounds: vec![] }],
21
+ fields: vec![ast::Field { name: "value".to_string(), ty: ast::Type { name: "a".to_string(), generics: vec![] } }],
22
+ };
23
+ assert_eq!(class_generic_params(&c), vec!["a".to_string()]);
24
+ }
25
+
26
+ #[test]
27
+ fn fn_generic_params_detects_implicit_lowercase_letter_types_in_order() {
28
+ let f = ast::Fn {
29
+ name: "pair".to_string(),
30
+ type_param: None,
31
+ params: vec![
32
+ ast::Param { name: "first".to_string(), ty: ast::ParamType::Type(ast::Type { name: "a".to_string(), generics: vec![] }), default: None },
33
+ ast::Param { name: "second".to_string(), ty: ast::ParamType::Type(ast::Type { name: "b".to_string(), generics: vec![] }), default: None },
34
+ ],
35
+ returns: Some(ast::ReturnType { name: "Bool".to_string(), generics: vec![] }),
36
+ body: ast::FnBody::Block(ast::Block { stmts: vec![] }),
37
+ };
38
+ assert_eq!(fn_generic_params(&f), vec!["a".to_string(), "b".to_string()]);
39
+ }
40
+
41
+ #[test]
42
+ fn enum_generic_params_detects_implicit_lowercase_letter_variant_fields() {
43
+ let e = ast::Enum {
44
+ name: "Option".to_string(),
45
+ variants: vec![
46
+ ast::EnumVariant { name: "Some".to_string(), fields: vec!["a".to_string()] },
47
+ ast::EnumVariant { name: "None".to_string(), fields: vec![] },
48
+ ],
49
+ };
50
+ assert_eq!(enum_generic_params(&e), vec!["a".to_string()]);
51
+ }
52
+
53
+ #[test]
54
+ fn mangle_joins_base_name_and_type_args() {
55
+ assert_eq!(mangle("Box", &[PlumType::TInt]), "Box$Int");
56
+ assert_eq!(mangle("Pair", &[PlumType::TInt, PlumType::TStr]), "Pair$Int$Str");
57
+ assert_eq!(mangle("Green", &[]), "Green");
58
+ }
59
+
60
+ #[test]
61
+ fn specialize_class_substitutes_generic_field_types_and_clears_generics_list() {
62
+ let c = ast::Class {
63
+ name: "Box".to_string(),
64
+ implements: vec![],
65
+ generics: vec![ast::GenericParam { name: "a".to_string(), bounds: vec![] }],
66
+ fields: vec![ast::Field { name: "value".to_string(), ty: ast::Type { name: "a".to_string(), generics: vec![] } }],
67
+ };
68
+ let mut bindings = std::collections::BTreeMap::new();
69
+ bindings.insert("a".to_string(), PlumType::TInt);
70
+ let specialized = specialize_class(&c, &Substitution(bindings), "Box$Int");
71
+ assert_eq!(specialized.name, "Box$Int");
72
+ assert!(specialized.generics.is_empty());
73
+ assert_eq!(specialized.fields[0].ty.name, "Int");
74
+ }
75
+
76
+ #[test]
77
+ fn specialize_fn_substitutes_generic_param_and_return_types() {
78
+ let f = ast::Fn {
79
+ name: "wrap".to_string(),
80
+ type_param: None,
81
+ params: vec![ast::Param { name: "value".to_string(), ty: ast::ParamType::Type(ast::Type { name: "a".to_string(), generics: vec![] }), default: None }],
82
+ returns: Some(ast::ReturnType { name: "a".to_string(), generics: vec![] }),
83
+ body: ast::FnBody::Expr(ast::Expr::Var("value".to_string())),
84
+ };
85
+ let mut bindings = std::collections::BTreeMap::new();
86
+ bindings.insert("a".to_string(), PlumType::TInt);
87
+ let specialized = specialize_fn(&f, &Substitution(bindings), "wrap$Int", None);
88
+ assert_eq!(specialized.name, "wrap$Int");
89
+ match &specialized.params[0].ty {
90
+ ast::ParamType::Type(t) => assert_eq!(t.name, "Int"),
91
+ _ => panic!("expected ParamType::Type"),
92
+ }
93
+ assert_eq!(specialized.returns.unwrap().name, "Int");
94
+ }
95
+
96
+ #[test]
97
+ fn specialize_fn_sets_new_receiver_for_a_method() {
98
+ let f = ast::Fn {
99
+ name: "getValue".to_string(),
100
+ type_param: Some("Box".to_string()),
101
+ params: vec![],
102
+ returns: Some(ast::ReturnType { name: "a".to_string(), generics: vec![] }),
103
+ body: ast::FnBody::Expr(ast::Expr::Self_),
104
+ };
105
+ let mut bindings = std::collections::BTreeMap::new();
106
+ bindings.insert("a".to_string(), PlumType::TStr);
107
+ let specialized = specialize_fn(&f, &Substitution(bindings), "getValue", Some("Box$Str".to_string()));
108
+ assert_eq!(specialized.name, "getValue");
109
+ assert_eq!(specialized.type_param, Some("Box$Str".to_string()));
110
+ assert_eq!(specialized.returns.unwrap().name, "Str");
111
+ }
112
+
113
+ #[test]
114
+ fn specialize_enum_substitutes_generic_variant_field_names() {
115
+ let e = ast::Enum {
116
+ name: "Option".to_string(),
117
+ variants: vec![
118
+ ast::EnumVariant { name: "Some".to_string(), fields: vec!["a".to_string()] },
119
+ ast::EnumVariant { name: "None".to_string(), fields: vec![] },
120
+ ],
121
+ };
122
+ let mut bindings = std::collections::BTreeMap::new();
123
+ bindings.insert("a".to_string(), PlumType::TInt);
124
+ let specialized = specialize_enum(&e, &Substitution(bindings), "Option$Int");
125
+ assert_eq!(specialized.name, "Option$Int");
126
+ assert_eq!(specialized.variants[0].fields, vec!["Int".to_string()]);
127
+ assert!(specialized.variants[1].fields.is_empty());
128
+ }