plum
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 +1 -0
- plum-core/src/loader.rs +4 -0
- plum-core/src/unions.rs +192 -0
- plum-core/tests/unions_test.rs +127 -0
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
|
+
}
|