plum

#treesitter#compiler#wasm

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

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


1ecfcf4 — Peter John 2026-11-01T18:24:09+05:30
T2: resolve + plan recorder
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 unions;
11
12
 
12
13
  pub use formatter::{
13
14
  formatSource, formatSourceWithOpts, inlineSingleStatementCaseBodies, FormatterError,
plum-core/src/loader.rs CHANGED
@@ -72,6 +72,10 @@ pub fn loadAndMergeWithOverrides(
72
72
  formLoops: false,
73
73
  primitives: prims,
74
74
  };
75
+ let memberErrors = crate::unions::resolveMembers(&mut merged);
76
+ if !memberErrors.is_empty() {
77
+ return Err(memberErrors.join("\n"));
78
+ }
75
79
  crate::forms::prepareForms(&mut merged);
76
80
  Ok(merged)
77
81
  }
plum-core/src/unions.rs ADDED
@@ -0,0 +1,192 @@
1
+ //! Enums as unions of member types: member resolution and the plan recorder.
2
+ //!
3
+ //! `resolveMembers` turns each member variant of `enum Shape = Circle | Red`
4
+ //! into a hidden positional variant `Shape__Circle(Circle)`; tags stay as they
5
+ //! are. The checker records where unions are used (`UnionPlan`) and a later
6
+ //! pass rewrites the program from that plan.
7
+ use crate::ast::{Enum, EnumVariant, Item, Source, Type};
8
+ use std::cell::{Cell, RefCell};
9
+ use std::collections::HashMap;
10
+
11
+ /// Types the compiler provides without an item in the source.
12
+ const BUILTIN_MEMBERS: &[&str] = &["Int", "Float", "Byte"];
13
+
14
+ /// `Shape__Circle`: the hidden variant that wraps member `Circle` of `Shape`.
15
+ pub fn hiddenVariant(enumName: &str, member: &str) -> String {
16
+ format!("{enumName}__{member}")
17
+ }
18
+
19
+ fn isKnownType(source: &Source, name: &str) -> bool {
20
+ BUILTIN_MEMBERS.contains(&name)
21
+ || source.primitives.iter().any(|p| p.name == name)
22
+ || source.items.iter().any(|i| match i {
23
+ Item::Enum(e) => e.name == name,
24
+ Item::ErrorSet(s) => s.name == name,
25
+ _ => false,
26
+ })
27
+ }
28
+
29
+ /// Resolves every member variant of every enum in place. Returns one error
30
+ /// message per problem. Safe to run twice: already hidden variants are skipped.
31
+ pub fn resolveMembers(source: &mut Source) -> Vec<String> {
32
+ let mut errors = Vec::new();
33
+ // Decide per (enum, variant) first, then mutate: `isKnownType` borrows `source`.
34
+ let mut decisions: Vec<(usize, usize, bool)> = Vec::new();
35
+ for (ii, item) in source.items.iter().enumerate() {
36
+ let Item::Enum(e) = item else { continue };
37
+ if e.isValue || e.isRecord {
38
+ continue;
39
+ }
40
+ let pending: Vec<usize> = e
41
+ .variants
42
+ .iter()
43
+ .enumerate()
44
+ .filter(|(_, v)| match &v.member {
45
+ Some(m) => v.name != hiddenVariant(&e.name, &m.name),
46
+ None => false,
47
+ })
48
+ .map(|(vi, _)| vi)
49
+ .collect();
50
+ // Duplicate base names among members (resolved or not).
51
+ let mut bases: Vec<&str> = Vec::new();
52
+ let mut dup = false;
53
+ for v in &e.variants {
54
+ if let Some(m) = &v.member {
55
+ if bases.contains(&m.name.as_str()) && isKnownOrGeneric(source, e, m) {
56
+ errors.push(format!(
57
+ "enum {} has two {} members; a pattern '{}' would be ambiguous",
58
+ e.name, m.name, m.name
59
+ ));
60
+ dup = true;
61
+ break;
62
+ }
63
+ bases.push(&m.name);
64
+ }
65
+ }
66
+ if dup {
67
+ continue;
68
+ }
69
+ for vi in pending {
70
+ let v = &e.variants[vi];
71
+ let m = v.member.as_ref().unwrap();
72
+ // A variant named like its own enum is never a member.
73
+ let isMember = m.name != e.name
74
+ && (isKnownType(source, &m.name) || !m.generics.is_empty());
75
+ if isMember && !isKnownType(source, &m.name) {
76
+ errors.push(format!("unknown member type {}", m.name));
77
+ continue;
78
+ }
79
+ decisions.push((ii, vi, isMember));
80
+ }
81
+ }
82
+ for (ii, vi, isMember) in decisions {
83
+ let Item::Enum(e) = &mut source.items[ii] else { continue };
84
+ let enumName = e.name.clone();
85
+ let v: &mut EnumVariant = &mut e.variants[vi];
86
+ if isMember {
87
+ let m = v.member.clone().unwrap();
88
+ v.name = hiddenVariant(&enumName, &m.name);
89
+ v.fields = vec![m];
90
+ v.field_names = Vec::new();
91
+ } else {
92
+ v.member = None;
93
+ }
94
+ }
95
+ errors
96
+ }
97
+
98
+ /// A repeated name counts as a duplicate member only if it would resolve to a
99
+ /// member (a repeated tag is a different, older problem).
100
+ fn isKnownOrGeneric(source: &Source, e: &Enum, m: &Type) -> bool {
101
+ m.name != e.name && (isKnownType(source, &m.name) || !m.generics.is_empty())
102
+ }
103
+
104
+ /// Errors for the removed forms: inline payloads and a lone self-named variant.
105
+ /// Written here, switched on by the migration task.
106
+ pub fn legacyErrors(source: &Source) -> Vec<String> {
107
+ let mut errors = Vec::new();
108
+ for item in &source.items {
109
+ let Item::Enum(e) = item else { continue };
110
+ if e.isValue || e.isRecord {
111
+ continue;
112
+ }
113
+ for v in &e.variants {
114
+ if v.name == e.name {
115
+ errors.push(format!(
116
+ "enum {0} has a single variant named {0} — write record {0}(...)",
117
+ e.name
118
+ ));
119
+ } else if v.member.is_none() && v.values.is_empty() && !v.fields.is_empty() {
120
+ errors.push(format!(
121
+ "inline variant payloads are removed: declare record {0}(...) and list {0} in enum {1}",
122
+ v.name, e.name
123
+ ));
124
+ }
125
+ }
126
+ }
127
+ errors
128
+ }
129
+
130
+ /// The hidden variant of `enumName` that wraps member `member` (a base name).
131
+ pub fn memberOf(source: &Source, enumName: &str, member: &str) -> Option<String> {
132
+ source.items.iter().find_map(|i| match i {
133
+ Item::Enum(e) if e.name == enumName => e.variants.iter().find_map(|v| {
134
+ let m = v.member.as_ref()?;
135
+ (m.name == member && v.name == hiddenVariant(enumName, member)).then(|| v.name.clone())
136
+ }),
137
+ _ => None,
138
+ })
139
+ }
140
+
141
+ // ---- plan recorder ----------------------------------------------------
142
+
143
+ /// Where the checker found union uses, keyed by AST node address.
144
+ #[derive(Debug, Clone, Default, PartialEq)]
145
+ pub struct UnionPlan {
146
+ /// expression address -> hidden variant to wrap it in
147
+ pub wraps: HashMap<usize, String>,
148
+ /// pattern address -> hidden variant it matches
149
+ pub patterns: HashMap<usize, String>,
150
+ /// case address -> (subject, hidden variant, fresh binding name)
151
+ pub narrowings: HashMap<usize, (String, String, String)>,
152
+ /// call address -> hidden variants to dispatch over
153
+ pub dispatches: HashMap<usize, Vec<String>>,
154
+ }
155
+
156
+ thread_local! {
157
+ static PLAN: RefCell<UnionPlan> = RefCell::new(UnionPlan::default());
158
+ static FRESH: Cell<usize> = const { Cell::new(0) };
159
+ }
160
+
161
+ /// Starts a new plan: clears the recorder and the fresh-name counter.
162
+ pub fn beginPlan() {
163
+ PLAN.with(|p| *p.borrow_mut() = UnionPlan::default());
164
+ FRESH.with(|c| c.set(0));
165
+ }
166
+
167
+ pub fn recordWrap(exprAddr: usize, variant: String) {
168
+ PLAN.with(|p| p.borrow_mut().wraps.insert(exprAddr, variant));
169
+ }
170
+
171
+ pub fn recordPattern(patAddr: usize, variant: String) {
172
+ PLAN.with(|p| p.borrow_mut().patterns.insert(patAddr, variant));
173
+ }
174
+
175
+ /// Records a narrowing; the binding gets the fresh name `<subject>__n<k>`.
176
+ pub fn recordNarrow(caseAddr: usize, subject: String, variant: String) {
177
+ let k = FRESH.with(|c| {
178
+ c.set(c.get() + 1);
179
+ c.get()
180
+ });
181
+ let fresh = format!("{subject}__n{k}");
182
+ PLAN.with(|p| p.borrow_mut().narrowings.insert(caseAddr, (subject, variant, fresh)));
183
+ }
184
+
185
+ pub fn recordDispatch(callAddr: usize, variants: Vec<String>) {
186
+ PLAN.with(|p| p.borrow_mut().dispatches.insert(callAddr, variants));
187
+ }
188
+
189
+ /// Returns the recorded plan and clears the recorder.
190
+ pub fn takePlan() -> UnionPlan {
191
+ PLAN.with(|p| std::mem::take(&mut *p.borrow_mut()))
192
+ }
plum-core/tests/unions_test.rs ADDED
@@ -0,0 +1,127 @@
1
+ #![allow(non_snake_case)]
2
+ use plum_core::ast::*;
3
+ use plum_core::unions::*;
4
+ use plum_core::AstParser;
5
+
6
+ fn parse(src: &str) -> Source {
7
+ let mut parser = tree_sitter::Parser::new();
8
+ parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap();
9
+ let tree = parser.parse(src, None).unwrap();
10
+ assert!(!tree.root_node().has_error(), "{}", tree.root_node().to_sexp());
11
+ AstParser::new(src).parseSource(tree.root_node())
12
+ }
13
+
14
+ fn enumOf<'a>(s: &'a Source, name: &str) -> &'a Enum {
15
+ s.items
16
+ .iter()
17
+ .find_map(|i| match i {
18
+ Item::Enum(e) if e.name == name => Some(e),
19
+ _ => None,
20
+ })
21
+ .unwrap()
22
+ }
23
+
24
+ fn ty(n: &str) -> Type {
25
+ Type { name: n.to_string(), generics: vec![] }
26
+ }
27
+
28
+ #[test]
29
+ fn hiddenVariantName() {
30
+ assert_eq!(hiddenVariant("Shape", "Circle"), "Shape__Circle");
31
+ }
32
+
33
+ #[test]
34
+ fn resolvesMemberAndTag() {
35
+ let mut s = parse("struct Circle(r: Float)\nenum Shape = Circle | Red");
36
+ assert!(resolveMembers(&mut s).is_empty());
37
+ let e = enumOf(&s, "Shape");
38
+ assert_eq!(e.variants[0].name, "Shape__Circle");
39
+ assert_eq!(e.variants[0].fields, vec![ty("Circle")]);
40
+ assert_eq!(e.variants[0].member, Some(ty("Circle")));
41
+ assert_eq!(e.variants[1].name, "Red");
42
+ assert!(e.variants[1].fields.is_empty());
43
+ assert_eq!(e.variants[1].member, None);
44
+ }
45
+
46
+ #[test]
47
+ fn narrowFreshNames() {
48
+ beginPlan();
49
+ recordNarrow(0x10, "s".into(), "Shape__Circle".into());
50
+ recordNarrow(0x20, "s".into(), "Shape__Square".into());
51
+ let p = takePlan();
52
+ assert_eq!(p.narrowings[&0x10], ("s".into(), "Shape__Circle".into(), "s__n1".into()));
53
+ assert_eq!(p.narrowings[&0x20].2, "s__n2");
54
+ assert!(takePlan().narrowings.is_empty());
55
+ beginPlan();
56
+ recordNarrow(0x10, "s".into(), "V".into());
57
+ assert_eq!(takePlan().narrowings[&0x10].2, "s__n1");
58
+ }
59
+
60
+ #[test]
61
+ fn recordsOtherKinds() {
62
+ beginPlan();
63
+ recordWrap(1, "A__B".into());
64
+ recordPattern(2, "A__B".into());
65
+ recordDispatch(3, vec!["A__B".into()]);
66
+ let p = takePlan();
67
+ assert_eq!(p.wraps[&1], "A__B");
68
+ assert_eq!(p.patterns[&2], "A__B");
69
+ assert_eq!(p.dispatches[&3], vec!["A__B".to_string()]);
70
+ }
71
+
72
+ #[test]
73
+ fn dup_base_name() {
74
+ let mut s = parse("struct List(n: Int)\nenum Js = List[Int] | List[Str]");
75
+ assert_eq!(
76
+ resolveMembers(&mut s),
77
+ vec!["enum Js has two List members; a pattern 'List' would be ambiguous"]
78
+ );
79
+ }
80
+
81
+ #[test]
82
+ fn member_in_two_enums() {
83
+ let mut s = parse("struct Circle(r: Float)\nenum Shape = Circle | Red\nenum Round = Circle | Blue");
84
+ assert!(resolveMembers(&mut s).is_empty());
85
+ assert_eq!(enumOf(&s, "Shape").variants[0].name, "Shape__Circle");
86
+ assert_eq!(enumOf(&s, "Round").variants[0].name, "Round__Circle");
87
+ }
88
+
89
+ #[test]
90
+ fn idempotent() {
91
+ let mut s = parse("struct Circle(r: Float)\nenum Shape = Circle | Red");
92
+ resolveMembers(&mut s);
93
+ let once = s.clone();
94
+ assert!(resolveMembers(&mut s).is_empty());
95
+ assert_eq!(s, once);
96
+ }
97
+
98
+ #[test]
99
+ fn memberOfLookup() {
100
+ let mut s = parse("struct Circle(r: Float)\nenum Shape = Circle | Red");
101
+ resolveMembers(&mut s);
102
+ assert_eq!(memberOf(&s, "Shape", "Circle"), Some("Shape__Circle".to_string()));
103
+ assert_eq!(memberOf(&s, "Shape", "Oval"), None);
104
+ assert_eq!(memberOf(&s, "Shape", "Red"), None);
105
+ }
106
+
107
+ #[test]
108
+ fn unknownGenericMember() {
109
+ let mut s = parse("enum Js = Nope[Int]");
110
+ assert_eq!(resolveMembers(&mut s), vec!["unknown member type Nope"]);
111
+ }
112
+
113
+ #[test]
114
+ fn builtinMember() {
115
+ let mut s = parse("enum Num = Int | Float");
116
+ assert!(resolveMembers(&mut s).is_empty());
117
+ assert_eq!(enumOf(&s, "Num").variants[0].name, "Num__Int");
118
+ }
119
+
120
+ #[test]
121
+ fn legacyErrorsReported() {
122
+ let s = parse("enum Sh =\n | Circle(r: Float)\nenum User =\n | User(n: Int)\n");
123
+ let errs = legacyErrors(&s);
124
+ assert_eq!(errs.len(), 2);
125
+ assert!(errs[0].starts_with("inline variant payloads are removed: declare record Circle(...)"));
126
+ assert!(errs[1].contains("enum User has a single variant named User"));
127
+ }