plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
4659312
— Peter John
2026-11-01T21:08:17+05:30
fix: member values fall back to their enum's methods
- README.md +2 -0
- plum-checker/src/lib.rs +6 -0
- plum-checker/src/unions.rs +63 -4
- plum-checker/tests/unions_dispatch_test.rs +42 -0
- plum-examples/match.plum +6 -12
- plum-examples/shapes.plum +4 -10
README.md
CHANGED
|
@@ -479,6 +479,8 @@ enum Round = Circle | Oval
|
|
|
479
479
|
record Oval(a: Float)
|
|
480
480
|
```
|
|
481
481
|
|
|
482
|
+
A method call on a member value falls back to the enum when the member has no such method and exactly one enum it belongs to defines it (`Circle(r: 1.5).area()` calls `Shape.area`); if two enums define it, the call is an error until you convert the value first.
|
|
483
|
+
|
|
482
484
|
A discriminant enum gives every tag a shared number:
|
|
483
485
|
|
|
484
486
|
```plum
|
plum-checker/src/lib.rs
CHANGED
|
@@ -4192,6 +4192,12 @@ fn inferExprInner(
|
|
|
4192
4192
|
)),
|
|
4193
4193
|
},
|
|
4194
4194
|
ast::AttrKind::Method(call) => {
|
|
4195
|
+
// A member value calling a method only its enum defines.
|
|
4196
|
+
if let Some(r) = unions::memberFallback(&obj_ty, &call.name, ctx) {
|
|
4197
|
+
let (en, variant) = r?;
|
|
4198
|
+
plum_core::unions::recordWrap(&attr.object as *const ast::Expr as usize, variant);
|
|
4199
|
+
return inferMethodCall(&en, &call.name, &call.args, env, ctx, expected);
|
|
4200
|
+
}
|
|
4195
4201
|
inferMethodCall(&obj_ty, &call.name, &call.args, env, ctx, expected)
|
|
4196
4202
|
}
|
|
4197
4203
|
}
|
plum-checker/src/unions.rs
CHANGED
|
@@ -104,8 +104,8 @@ pub fn recordMemberWrap(expr: &ast::Expr, actual: &PlumType, expected: Option<&P
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
/// Enums that `ty` is a member of
|
|
107
|
+
/// Enums that `ty` is a member of.
|
|
108
|
-
fn
|
|
108
|
+
fn memberOfEnums(ty: &PlumType, ctx: &CheckCtx) -> BTreeSet<String> {
|
|
109
109
|
let mut out = BTreeSet::new();
|
|
110
110
|
for (vn, info) in ctx.enum_variants.iter() {
|
|
111
111
|
if info.field_types.len() == 1
|
|
@@ -121,6 +121,12 @@ fn candidates(ty: &PlumType, ctx: &CheckCtx) -> BTreeSet<String> {
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
|
+
out
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Enums that `ty` is a member of, plus `ty`'s own enum.
|
|
128
|
+
fn candidates(ty: &PlumType, ctx: &CheckCtx) -> BTreeSet<String> {
|
|
129
|
+
let mut out = memberOfEnums(ty, ctx);
|
|
124
130
|
if let Some((n, _)) = enumOf(ty) {
|
|
125
131
|
if !members(n, &[], ctx).is_empty() {
|
|
126
132
|
out.insert(n.to_string());
|
|
@@ -129,6 +135,48 @@ fn candidates(ty: &PlumType, ctx: &CheckCtx) -> BTreeSet<String> {
|
|
|
129
135
|
out
|
|
130
136
|
}
|
|
131
137
|
|
|
138
|
+
/// For `recv.name(..)` where `recv` is a member value whose type has no method
|
|
139
|
+
/// `name`: the one enum it is a member of that defines `name` (itself or by
|
|
140
|
+
/// auto-dispatch) and the hidden variant that wraps the receiver. `None` when
|
|
141
|
+
/// the ordinary rules apply; an error when two or more enums qualify.
|
|
142
|
+
pub fn memberFallback(
|
|
143
|
+
obj: &PlumType,
|
|
144
|
+
name: &str,
|
|
145
|
+
ctx: &CheckCtx,
|
|
146
|
+
) -> Option<Result<(PlumType, String), String>> {
|
|
147
|
+
let recv = crate::methodReceiverName(obj)?;
|
|
148
|
+
if ctx.methods.contains_key(&(recv, name.to_string())) {
|
|
149
|
+
return None;
|
|
150
|
+
}
|
|
151
|
+
if let Ok(PlumType::TFun(..)) = crate::fieldTypeOf(obj, name, ctx) {
|
|
152
|
+
return None;
|
|
153
|
+
}
|
|
154
|
+
let qualifying: Vec<String> = memberOfEnums(obj, ctx)
|
|
155
|
+
.into_iter()
|
|
156
|
+
.filter(|e| {
|
|
157
|
+
let generic = ctx.enum_variants.values().any(|i| &i.enum_name == e && !i.generics.is_empty());
|
|
158
|
+
!generic
|
|
159
|
+
&& (ctx.methods.contains_key(&(e.clone(), name.to_string()))
|
|
160
|
+
|| matches!(dispatchPlan(&PlumType::TNamed(e.clone()), name, ctx), Some(Ok(_))))
|
|
161
|
+
})
|
|
162
|
+
.collect();
|
|
163
|
+
match qualifying.len() {
|
|
164
|
+
0 => None,
|
|
165
|
+
1 => {
|
|
166
|
+
let en = PlumType::TNamed(qualifying[0].clone());
|
|
167
|
+
let v = wrapVariantFor(&en, obj, ctx)?;
|
|
168
|
+
Some(Ok((en, v)))
|
|
169
|
+
}
|
|
170
|
+
_ => {
|
|
171
|
+
let (last, rest) = qualifying.split_last().unwrap();
|
|
172
|
+
Some(Err(format!(
|
|
173
|
+
"{obj} has no method '{name}' (it is a member of {} and {last}, which both define it — convert first)",
|
|
174
|
+
rest.join(", ")
|
|
175
|
+
)))
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
132
180
|
/// Joins two branch types through their one common union enum. `None` when
|
|
133
181
|
/// neither branch involves unions (the ordinary rules apply).
|
|
134
182
|
pub fn joinBranches(
|
|
@@ -347,6 +395,18 @@ pub fn dispatchSig(
|
|
|
347
395
|
callAddr: usize,
|
|
348
396
|
ctx: &CheckCtx,
|
|
349
397
|
) -> Option<Result<(Vec<PlumType>, PlumType), String>> {
|
|
398
|
+
Some(dispatchPlan(obj, name, ctx)?.map(|(variants, params, ret)| {
|
|
399
|
+
plan::recordDispatch(callAddr, variants);
|
|
400
|
+
(params, ret)
|
|
401
|
+
}))
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/// `dispatchSig` without recording: `(variants, params, return type)`.
|
|
405
|
+
fn dispatchPlan(
|
|
406
|
+
obj: &PlumType,
|
|
407
|
+
name: &str,
|
|
408
|
+
ctx: &CheckCtx,
|
|
409
|
+
) -> Option<Result<(Vec<String>, Vec<PlumType>, PlumType), String>> {
|
|
350
410
|
let (en, args) = enumOf(obj)?;
|
|
351
411
|
if ctx.methods.contains_key(&(en.to_string(), name.to_string())) || !isUnion(en, ctx) {
|
|
352
412
|
return None;
|
|
@@ -381,9 +441,8 @@ pub fn dispatchSig(
|
|
|
381
441
|
variants.push(v);
|
|
382
442
|
}
|
|
383
443
|
let (_, sig) = first?;
|
|
384
|
-
plan::recordDispatch(callAddr, variants);
|
|
385
444
|
let PlumType::TFun(params, ret) = sig else { return None };
|
|
386
|
-
Some(Ok((params, *ret)))
|
|
445
|
+
Some(Ok((variants, params, *ret)))
|
|
387
446
|
}
|
|
388
447
|
|
|
389
448
|
/// `"{e}"` on a union enum: records a `toStr` dispatch when every member has `toStr`.
|
plum-checker/tests/unions_dispatch_test.rs
CHANGED
|
@@ -28,6 +28,25 @@ enum WithTag = Circle | Plain
|
|
|
28
28
|
enum Missing = Circle | NoArea
|
|
29
29
|
enum Clash = Circle | Odd
|
|
30
30
|
|
|
31
|
+
record Bare(x: Float)
|
|
32
|
+
|
|
33
|
+
enum Boxed =
|
|
34
|
+
| Bare
|
|
35
|
+
| Circle
|
|
36
|
+
|
|
37
|
+
fun area(self) -> Float = 1.0
|
|
38
|
+
|
|
39
|
+
enum Round =
|
|
40
|
+
| Bare
|
|
41
|
+
| Rectangle
|
|
42
|
+
|
|
43
|
+
fun area(self) -> Float = 2.0
|
|
44
|
+
|
|
45
|
+
enum Lone =
|
|
46
|
+
| Bare
|
|
47
|
+
|
|
48
|
+
fun size(self) -> Float = 3.0
|
|
49
|
+
|
|
31
50
|
record Wrap[T](v: T) =
|
|
32
51
|
fun toStr(self) -> Str = \"w\"
|
|
33
52
|
|
|
@@ -106,3 +125,26 @@ fn generic_member_dispatch() {
|
|
|
106
125
|
r.unwrap();
|
|
107
126
|
assert_eq!(p.dispatches.values().next().unwrap().len(), 2);
|
|
108
127
|
}
|
|
128
|
+
|
|
129
|
+
#[test]
|
|
130
|
+
fn member_falls_back_to_its_enum() {
|
|
131
|
+
let (r, p) = run("fun g() -> Float =\n Bare(x: 1.0).size()");
|
|
132
|
+
r.unwrap();
|
|
133
|
+
assert_eq!(p.wraps.len(), 1, "{:?}", p.wraps);
|
|
134
|
+
assert_eq!(p.wraps.values().next().unwrap(), "Lone__Bare");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#[test]
|
|
138
|
+
fn member_fallback_ambiguous_error() {
|
|
139
|
+
let (r, _) = run("fun f() -> Float =\n Bare(x: 1.0).area()");
|
|
140
|
+
assert!(
|
|
141
|
+
errs(r).ends_with("Bare has no method 'area' (it is a member of Boxed and Round, which both define it — convert first)")
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
#[test]
|
|
146
|
+
fn member_own_method_wins() {
|
|
147
|
+
let (r, p) = run("fun f() -> Float =\n Circle(r: 1.5).area()");
|
|
148
|
+
r.unwrap();
|
|
149
|
+
assert!(p.wraps.is_empty() && p.dispatches.is_empty());
|
|
150
|
+
}
|
plum-examples/match.plum
CHANGED
|
@@ -175,12 +175,6 @@ enum Figure =
|
|
|
175
175
|
record Ring(radius: Int)
|
|
176
176
|
record Rect(w: Int, h: Int)
|
|
177
177
|
|
|
178
|
-
fun measureOf(s: Shape) -> Int =
|
|
179
|
-
s.measure()
|
|
180
|
-
|
|
181
|
-
fun areaOf(f: Figure) -> Int =
|
|
182
|
-
f.area()
|
|
183
|
-
|
|
184
178
|
test "match example computes correctly"
|
|
185
179
|
assert main() == 5
|
|
186
180
|
|
|
@@ -235,16 +229,16 @@ test "match on a plain class supports literal/wildcard sub-patterns and case fal
|
|
|
235
229
|
assert Point(x: 5, y: 5).classify() == "elsewhere"
|
|
236
230
|
|
|
237
231
|
test "a plain class nested inside an enum variant pattern destructures correctly"
|
|
238
|
-
assert
|
|
232
|
+
assert Circle(Point(x: 3, y: 4)).measure() == 7
|
|
239
|
-
assert
|
|
233
|
+
assert Square(Point(x: 3, y: 4)).measure() == 12
|
|
240
234
|
|
|
241
235
|
test "named-payload enum variant constructs via named args and destructures by position"
|
|
242
|
-
assert
|
|
236
|
+
assert Ring(radius: 5).area() == 25
|
|
243
|
-
assert
|
|
237
|
+
assert Rect(w: 3, h: 4).area() == 12
|
|
244
238
|
|
|
245
239
|
test "named-payload enum variant still supports positional construction too"
|
|
246
|
-
assert
|
|
240
|
+
assert Ring(5).area() == 25
|
|
247
|
-
assert
|
|
241
|
+
assert Rect(3, 4).area() == 12
|
|
248
242
|
|
|
249
243
|
test "a pattern name reused with different types in one fn"
|
|
250
244
|
r := Sample(1)
|
plum-examples/shapes.plum
CHANGED
|
@@ -21,16 +21,10 @@ enum Shape =
|
|
|
21
21
|
record Circle(r: Float)
|
|
22
22
|
record Rect(w: Float, h: Float)
|
|
23
23
|
|
|
24
|
-
fun areaOf(s: Shape) -> Float =
|
|
25
|
-
s.area()
|
|
26
|
-
|
|
27
|
-
fun nameOf(s: Shape) -> Str =
|
|
28
|
-
s.name()
|
|
29
|
-
|
|
30
24
|
test "area dispatches on the variant"
|
|
31
|
-
assert
|
|
25
|
+
assert Circle(r: 1.5).area() == 6.75
|
|
32
|
-
assert
|
|
26
|
+
assert Rect(w: 2.0, h: 3.0).area() == 6.0
|
|
33
27
|
|
|
34
28
|
test "name dispatches on the variant"
|
|
35
|
-
assert
|
|
29
|
+
assert Circle(r: 1.0).name() == "circle"
|
|
36
|
-
assert
|
|
30
|
+
assert Rect(w: 1.0, h: 1.0).name() == "rect"
|