plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
1b220d2
— Peter John
2026-07-20T15:24:15+05:30
feat(plum-checker): core generics specialization primitives
- plum-checker/src/lib.rs +1 -0
- plum-checker/src/monomorphize.rs +167 -0
- plum-checker/tests/monomorphize_tests.rs +128 -0
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
|
+
}
|