plum

#treesitter#compiler#wasm

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

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


d83c36b — Peter John 2026-11-02T19:40:47+05:30
T2: trait lowering
plum-core/src/ast.rs CHANGED
@@ -25,6 +25,8 @@ pub struct Source {
25
25
  /// Every `primitive <Name> =` block of this file. The loader allows one
26
26
  /// naming a compiler built-in only in std; any other name is a namespace.
27
27
  pub primitives: Vec<PrimitiveDecl>,
28
+ /// Traits removed by `lowerTraits` (their unions replace them in `items`).
29
+ pub traits: Vec<TraitDecl>,
28
30
  }
29
31
 
30
32
  /// A `primitive Name =` block.
plum-core/src/lib.rs CHANGED
@@ -8,6 +8,7 @@ pub mod formatter;
8
8
  pub mod forms;
9
9
  pub mod loader;
10
10
  pub mod parser;
11
+ pub mod traits;
11
12
  pub mod union_lower;
12
13
  pub mod unions;
13
14
 
plum-core/src/loader.rs CHANGED
@@ -77,7 +77,12 @@ pub fn loadAndMergeWithOverrides(
77
77
  stars: Vec::new(),
78
78
  formLoops: false,
79
79
  primitives: prims,
80
+ traits: Vec::new(),
80
81
  };
82
+ let traitErrors = crate::traits::lowerTraits(&mut merged);
83
+ if !traitErrors.is_empty() {
84
+ return Err(traitErrors.join("\n"));
85
+ }
81
86
  let memberErrors = crate::unions::resolveMembers(&mut merged);
82
87
  if !memberErrors.is_empty() {
83
88
  return Err(memberErrors.join("\n"));
plum-core/src/parser.rs CHANGED
@@ -174,6 +174,7 @@ impl<'a> AstParser<'a> {
174
174
  stars: Vec::new(),
175
175
  formLoops: false,
176
176
  primitives,
177
+ traits: Vec::new(),
177
178
  }
178
179
  }
179
180
 
plum-core/src/traits.rs ADDED
@@ -0,0 +1,252 @@
1
+ //! Trait lowering: copies each trait's fields and missing default methods into
2
+ //! every record that embeds it, adds `__get_<f>`/`__set_<f>` accessors, and
3
+ //! replaces the trait with a union enum (`isTrait: true`) over its embedders.
4
+ use std::collections::HashMap;
5
+
6
+ use crate::ast::*;
7
+
8
+ pub fn getter(field: &str) -> String {
9
+ format!("__get_{}", field)
10
+ }
11
+
12
+ pub fn setter(field: &str) -> String {
13
+ format!("__set_{}", field)
14
+ }
15
+
16
+ fn accessor(
17
+ name: String,
18
+ record: &str,
19
+ file: &str,
20
+ t: &TraitDecl,
21
+ params: Vec<Param>,
22
+ returns: Option<Type>,
23
+ body: FnBody,
24
+ ) -> Fn {
25
+ Fn {
26
+ name,
27
+ file: file.to_string(),
28
+ line: t.line,
29
+ column: t.column,
30
+ type_param: Some(record.to_string()),
31
+ is_extern: false,
32
+ is_export: false,
33
+ is_star: false,
34
+ has_self: true,
35
+ params,
36
+ yields: None,
37
+ returns,
38
+ body,
39
+ }
40
+ }
41
+
42
+ fn selfField(field: &str) -> Expr {
43
+ Expr::Attribute(Box::new(AttributeExpr {
44
+ object: Expr::Self_,
45
+ attr: AttrKind::Field(field.to_string()),
46
+ }))
47
+ }
48
+
49
+ /// Lowers every `trait` of `source`. Returns error messages (empty on success).
50
+ /// Idempotent: a second call finds no traits and no embeds and does nothing.
51
+ pub fn lowerTraits(source: &mut Source) -> Vec<String> {
52
+ let mut errors = Vec::new();
53
+ let traits: HashMap<String, TraitDecl> = source
54
+ .items
55
+ .iter()
56
+ .filter_map(|i| match i {
57
+ Item::Trait(t) => Some((t.name.clone(), t.clone())),
58
+ _ => None,
59
+ })
60
+ .collect();
61
+ let mut embedders: HashMap<String, Vec<String>> = HashMap::new();
62
+ let mut newFns: Vec<Item> = Vec::new();
63
+ // Record names and the file of any of their methods (for accessor `file`).
64
+ let fnFile = |items: &[Item], rec: &str| -> String {
65
+ items
66
+ .iter()
67
+ .find_map(|i| match i {
68
+ Item::Fn(f) if f.type_param.as_deref() == Some(rec) => Some(f.file.clone()),
69
+ _ => None,
70
+ })
71
+ .unwrap_or_else(|| source.file.clone())
72
+ };
73
+
74
+ for ii in 0..source.items.len() {
75
+ let (rname, isRecord, isValue, embeds) = match &source.items[ii] {
76
+ Item::Enum(e) if !e.embeds.is_empty() => {
77
+ (e.name.clone(), e.isRecord, e.isValue, e.embeds.clone())
78
+ }
79
+ _ => continue,
80
+ };
81
+ let kind = if isValue { "struct" } else { "record" };
82
+ let before = errors.len();
83
+ for em in &embeds {
84
+ if !traits.contains_key(em) {
85
+ errors.push(format!("{} embeds {}, which is not a trait", rname, em));
86
+ } else if isValue || !isRecord {
87
+ errors.push(format!(
88
+ "{} {} cannot embed trait {}; only records embed traits",
89
+ kind, rname, em
90
+ ));
91
+ }
92
+ }
93
+ if errors.len() > before {
94
+ if let Item::Enum(e) = &mut source.items[ii] {
95
+ e.embeds.clear();
96
+ }
97
+ continue;
98
+ }
99
+ let file = fnFile(&source.items, &rname);
100
+ let Item::Enum(e) = &source.items[ii] else { continue };
101
+ let ownNames: Vec<String> = e.variants[0].field_names.clone();
102
+ let mut fieldOwner: Vec<(String, String)> = Vec::new(); // (field, trait)
103
+ let mut newTypes: Vec<Type> = Vec::new();
104
+ let mut newNames: Vec<String> = Vec::new();
105
+ for em in &embeds {
106
+ for f in &traits[em].fields {
107
+ if let Some((_, other)) = fieldOwner.iter().find(|(n, _)| *n == f.name) {
108
+ errors.push(format!(
109
+ "record {} gets field {} from both {} and {}",
110
+ rname, f.name, other, em
111
+ ));
112
+ } else if ownNames.contains(&f.name) {
113
+ errors.push(format!(
114
+ "record {} field {} is already declared by trait {}",
115
+ rname, f.name, em
116
+ ));
117
+ } else {
118
+ fieldOwner.push((f.name.clone(), em.clone()));
119
+ newTypes.push(f.ty.clone());
120
+ newNames.push(f.name.clone());
121
+ }
122
+ }
123
+ }
124
+ let defined = |name: &str| {
125
+ source.items.iter().any(|i| {
126
+ matches!(i, Item::Fn(f) if f.type_param.as_deref() == Some(&rname) && f.name == name)
127
+ })
128
+ };
129
+ let mut defaultOwner: Vec<(String, String)> = Vec::new();
130
+ let mut copies: Vec<Fn> = Vec::new();
131
+ for em in &embeds {
132
+ let t = &traits[em];
133
+ for r in &t.required {
134
+ if !defined(&r.name) {
135
+ errors.push(format!(
136
+ "record {} embeds {} but does not define required method {}",
137
+ rname, em, r.name
138
+ ));
139
+ }
140
+ }
141
+ for m in &t.methods {
142
+ if defined(&m.name) {
143
+ continue;
144
+ }
145
+ if let Some((_, other)) = defaultOwner.iter().find(|(n, _)| *n == m.name) {
146
+ errors.push(format!(
147
+ "record {} gets default method {} from both {} and {}; define {}.{}",
148
+ rname, m.name, other, em, rname, m.name
149
+ ));
150
+ continue;
151
+ }
152
+ defaultOwner.push((m.name.clone(), em.clone()));
153
+ let mut c = m.clone();
154
+ c.type_param = Some(rname.clone());
155
+ c.file = file.clone();
156
+ copies.push(c);
157
+ }
158
+ embedders.entry(em.clone()).or_default().push(rname.clone());
159
+ }
160
+ for em in &embeds {
161
+ let t = &traits[em];
162
+ for f in &t.fields {
163
+ if !fieldOwner.iter().any(|(n, o)| *n == f.name && o == em) {
164
+ continue;
165
+ }
166
+ copies.push(accessor(
167
+ getter(&f.name),
168
+ &rname,
169
+ &file,
170
+ t,
171
+ vec![],
172
+ Some(f.ty.clone()),
173
+ FnBody::Expr(selfField(&f.name)),
174
+ ));
175
+ copies.push(accessor(
176
+ setter(&f.name),
177
+ &rname,
178
+ &file,
179
+ t,
180
+ vec![Param {
181
+ name: "v".to_string(),
182
+ ty: ParamType::Type(f.ty.clone()),
183
+ default: None,
184
+ }],
185
+ None,
186
+ FnBody::Block(Block {
187
+ stmts: vec![Stmt::Assign(Assign {
188
+ targets: vec![AssignTarget::Field(
189
+ Box::new(Expr::Self_),
190
+ f.name.clone(),
191
+ )],
192
+ values: vec![Expr::Var("v".to_string())],
193
+ declare: false,
194
+ ty: None,
195
+ })],
196
+ }),
197
+ ));
198
+ }
199
+ }
200
+ if let Item::Enum(e) = &mut source.items[ii] {
201
+ e.embeds.clear();
202
+ if errors.len() == before {
203
+ let v = &mut e.variants[0];
204
+ let mut tys = newTypes;
205
+ tys.extend(std::mem::take(&mut v.fields));
206
+ let mut names = newNames;
207
+ names.extend(std::mem::take(&mut v.field_names));
208
+ v.fields = tys;
209
+ v.field_names = names;
210
+ }
211
+ }
212
+ if errors.len() == before {
213
+ newFns.extend(copies.into_iter().map(Item::Fn));
214
+ }
215
+ }
216
+
217
+ for item in source.items.iter_mut() {
218
+ let Item::Trait(t) = item else { continue };
219
+ let variants = embedders
220
+ .get(&t.name)
221
+ .cloned()
222
+ .unwrap_or_default()
223
+ .into_iter()
224
+ .map(|r| EnumVariant {
225
+ name: r.clone(),
226
+ member: Some(Type {
227
+ name: r,
228
+ generics: vec![],
229
+ }),
230
+ fields: vec![],
231
+ field_names: vec![],
232
+ values: vec![],
233
+ })
234
+ .collect();
235
+ let e = Enum {
236
+ name: t.name.clone(),
237
+ line: t.line,
238
+ column: t.column,
239
+ generics: vec![],
240
+ params: vec![],
241
+ variants,
242
+ isValue: false,
243
+ isRecord: false,
244
+ embeds: vec![],
245
+ isTrait: true,
246
+ };
247
+ source.traits.push(t.clone());
248
+ *item = Item::Enum(e);
249
+ }
250
+ source.items.extend(newFns);
251
+ errors
252
+ }
plum-core/tests/traits_test.rs ADDED
@@ -0,0 +1,184 @@
1
+ #![allow(non_snake_case)]
2
+ use plum_core::ast::*;
3
+ use plum_core::traits::{getter, lowerTraits, setter};
4
+ use plum_core::unions::resolveMembers;
5
+ use plum_core::AstParser;
6
+
7
+ fn parseRaw(src: &str) -> Source {
8
+ let mut parser = tree_sitter::Parser::new();
9
+ parser
10
+ .set_language(&tree_sitter_plum::LANGUAGE.into())
11
+ .unwrap();
12
+ let tree = parser.parse(src, None).unwrap();
13
+ assert!(!tree.root_node().has_error(), "{}", tree.root_node().to_sexp());
14
+ AstParser::new(src).parseSourceRaw(tree.root_node())
15
+ }
16
+
17
+ fn enumOf<'a>(s: &'a Source, name: &str) -> &'a Enum {
18
+ s.items
19
+ .iter()
20
+ .find_map(|i| match i {
21
+ Item::Enum(e) if e.name == name => Some(e),
22
+ _ => None,
23
+ })
24
+ .unwrap()
25
+ }
26
+
27
+ fn fnOf<'a>(s: &'a Source, rec: &str, name: &str) -> Option<&'a Fn> {
28
+ s.items.iter().find_map(|i| match i {
29
+ Item::Fn(f) if f.type_param.as_deref() == Some(rec) && f.name == name => Some(f),
30
+ _ => None,
31
+ })
32
+ }
33
+
34
+ const BASIC: &str = "trait Ent(z: Int) =\n fun up(self)\n fun gz(self) -> Int = self.z\n\nrecord Hut(Ent, k: Int) =\n fun up(self) -> Int = 1\n";
35
+
36
+ #[test]
37
+ fn accessor_names() {
38
+ assert_eq!(getter("pos"), "__get_pos");
39
+ assert_eq!(setter("pos"), "__set_pos");
40
+ }
41
+
42
+ #[test]
43
+ fn lowers_basic() {
44
+ let mut s = parseRaw(BASIC);
45
+ assert_eq!(lowerTraits(&mut s), Vec::<String>::new());
46
+ assert!(!s.items.iter().any(|i| matches!(i, Item::Trait(_))));
47
+ assert_eq!(s.traits.len(), 1);
48
+ let u = enumOf(&s, "Ent");
49
+ assert!(u.isTrait);
50
+ assert_eq!(u.variants.len(), 1);
51
+ assert_eq!(u.variants[0].member.as_ref().unwrap().name, "Hut");
52
+ let h = enumOf(&s, "Hut");
53
+ assert_eq!(h.variants[0].field_names, vec!["z", "k"]);
54
+ assert!(fnOf(&s, "Hut", "__get_z").is_some());
55
+ let set = fnOf(&s, "Hut", "__set_z").unwrap();
56
+ assert!(set.has_self && set.params.len() == 1);
57
+ let get = fnOf(&s, "Hut", "__get_z").unwrap();
58
+ assert_eq!(get.returns.as_ref().unwrap().name, "Int");
59
+ assert!(fnOf(&s, "Hut", "gz").is_some());
60
+ // required methods are never copied
61
+ assert_eq!(
62
+ s.items.iter().filter(|i| matches!(i, Item::Fn(f) if f.name == "up")).count(),
63
+ 1
64
+ );
65
+ assert!(resolveMembers(&mut s).is_empty());
66
+ assert_eq!(enumOf(&s, "Ent").variants[0].name, "Ent__Hut");
67
+ }
68
+
69
+ #[test]
70
+ fn accessor_bodies_match_parsed_code() {
71
+ let mut s = parseRaw(BASIC);
72
+ lowerTraits(&mut s);
73
+ let r = parseRaw("record Tmp(z: Int) =\n fun getz(self) -> Int = self.z\n fun setz(self, v: Int) =\n self.z = v\n");
74
+ let want = |n: &str| match r.items.iter().find_map(|i| match i {
75
+ Item::Fn(f) if f.name == n => Some(f.body.clone()),
76
+ _ => None,
77
+ }) {
78
+ Some(b) => b,
79
+ None => panic!(),
80
+ };
81
+ assert_eq!(fnOf(&s, "Hut", "__get_z").unwrap().body, want("getz"));
82
+ assert_eq!(fnOf(&s, "Hut", "__set_z").unwrap().body, want("setz"));
83
+ }
84
+
85
+ #[test]
86
+ fn idempotent() {
87
+ let mut s = parseRaw(BASIC);
88
+ lowerTraits(&mut s);
89
+ let snap = s.clone();
90
+ assert!(lowerTraits(&mut s).is_empty());
91
+ assert_eq!(s, snap);
92
+ }
93
+
94
+ #[test]
95
+ fn missing_required() {
96
+ let mut s = parseRaw("trait Ent(z: Int) =\n fun up(self)\n\nrecord Hut(Ent)\n");
97
+ assert_eq!(
98
+ lowerTraits(&mut s),
99
+ vec!["record Hut embeds Ent but does not define required method up"]
100
+ );
101
+ }
102
+
103
+ #[test]
104
+ fn field_clash() {
105
+ let mut s = parseRaw("trait Aa(x: Int)\ntrait Bb(x: Int)\nrecord Rec(Aa, Bb)\n");
106
+ assert_eq!(lowerTraits(&mut s), vec!["record Rec gets field x from both Aa and Bb"]);
107
+ let mut s = parseRaw("trait Aa(x: Int)\nrecord Rec(Aa, x: Int)\n");
108
+ assert_eq!(
109
+ lowerTraits(&mut s),
110
+ vec!["record Rec field x is already declared by trait Aa"]
111
+ );
112
+ }
113
+
114
+ #[test]
115
+ fn default_clash() {
116
+ let src = "trait Aa =\n fun f(self) -> Int = 1\ntrait Bb =\n fun f(self) -> Int = 1\nrecord Rec(Aa, Bb)\n";
117
+ let mut s = parseRaw(src);
118
+ assert_eq!(
119
+ lowerTraits(&mut s),
120
+ vec!["record Rec gets default method f from both Aa and Bb; define Rec.f"]
121
+ );
122
+ let mut s = parseRaw(&format!("{}\n fun f(self) -> Int = 1\n", src.trim_end().to_string() + " ="));
123
+ assert!(lowerTraits(&mut s).is_empty());
124
+ }
125
+
126
+ #[test]
127
+ fn embed_not_trait() {
128
+ let mut s = parseRaw("struct Vec2(x: Float)\nrecord Rec(Vec2)\n");
129
+ assert_eq!(lowerTraits(&mut s), vec!["Rec embeds Vec2, which is not a trait"]);
130
+ }
131
+
132
+ #[test]
133
+ fn struct_embed() {
134
+ let mut s = parseRaw("trait Ent(z: Int)\nstruct Stu(Ent, x: Int)\n");
135
+ assert_eq!(
136
+ lowerTraits(&mut s),
137
+ vec!["struct Stu cannot embed trait Ent; only records embed traits"]
138
+ );
139
+ }
140
+
141
+ #[test]
142
+ fn self_recursive_field() {
143
+ let mut s = parseRaw("trait Act(children: List[Act])\nrecord Nod(Act)\n");
144
+ assert!(lowerTraits(&mut s).is_empty());
145
+ let t = &enumOf(&s, "Nod").variants[0].fields[0];
146
+ assert_eq!(t.name, "List");
147
+ assert_eq!(t.generics[0].name, "Act");
148
+ }
149
+
150
+ #[test]
151
+ fn own_default_wins() {
152
+ let mut s = parseRaw(BASIC.replace("fun up(self) -> Int = 1", "fun up(self) -> Int = 1\n fun gz(self) -> Int = 7").as_str());
153
+ assert!(lowerTraits(&mut s).is_empty());
154
+ assert_eq!(
155
+ s.items.iter().filter(|i| matches!(i, Item::Fn(f) if f.name == "gz")).count(),
156
+ 1
157
+ );
158
+ }
159
+
160
+ #[test]
161
+ fn no_embedders_gives_empty_union() {
162
+ let mut s = parseRaw("trait Lone(z: Int)\n");
163
+ assert!(lowerTraits(&mut s).is_empty());
164
+ let u = enumOf(&s, "Lone");
165
+ assert!(u.isTrait && u.variants.is_empty());
166
+ }
167
+
168
+ #[test]
169
+ fn loader_runs_lowerTraits() {
170
+ let dir = std::env::temp_dir().join(format!("plum_traits_{}", std::process::id()));
171
+ std::fs::create_dir_all(dir.join("plum-std")).unwrap();
172
+ std::fs::write(dir.join("ent.plum"), "module ent\n\ntrait Ent(nxt: Ent) =\n fun up(self)\n").unwrap();
173
+ std::fs::write(
174
+ dir.join("main.plum"),
175
+ "import ent\n\nrecord Hut(Ent) =\n fun up(self) =\n return\n",
176
+ )
177
+ .unwrap();
178
+ let r = plum_core::loadAndMerge(&dir.join("main.plum"), &dir);
179
+ let s = r.unwrap_or_else(|e| panic!("{}", e));
180
+ assert!(enumOf(&s, "Ent").isTrait);
181
+ assert_eq!(enumOf(&s, "Ent").variants[0].name, "Ent__Hut");
182
+ assert_eq!(enumOf(&s, "Hut").variants[0].field_names, vec!["nxt"]);
183
+ let _ = std::fs::remove_dir_all(&dir);
184
+ }
plum-wasm-codegen/src/lib.rs CHANGED
@@ -1692,6 +1692,7 @@ fn desugarTestsToFns(source: &ast::Source) -> (ast::Source, Vec<(String, String)
1692
1692
  stars: source.stars.clone(),
1693
1693
  formLoops: source.formLoops,
1694
1694
  primitives: source.primitives.clone(),
1695
+ traits: source.traits.clone(),
1695
1696
  },
1696
1697
  extra_exports,
1697
1698
  )