plum

#treesitter#compiler#wasm

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 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, plus `ty`'s own enum.
107
+ /// Enums that `ty` is a member of.
108
- fn candidates(ty: &PlumType, ctx: &CheckCtx) -> BTreeSet<String> {
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 measureOf(Circle(Point(x: 3, y: 4))) == 7
232
+ assert Circle(Point(x: 3, y: 4)).measure() == 7
239
- assert measureOf(Square(Point(x: 3, y: 4))) == 12
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 areaOf(Ring(radius: 5)) == 25
236
+ assert Ring(radius: 5).area() == 25
243
- assert areaOf(Rect(w: 3, h: 4)) == 12
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 areaOf(Ring(5)) == 25
240
+ assert Ring(5).area() == 25
247
- assert areaOf(Rect(3, 4)) == 12
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 areaOf(Circle(r: 1.5)) == 6.75
25
+ assert Circle(r: 1.5).area() == 6.75
32
- assert areaOf(Rect(w: 2.0, h: 3.0)) == 6.0
26
+ assert Rect(w: 2.0, h: 3.0).area() == 6.0
33
27
 
34
28
  test "name dispatches on the variant"
35
- assert nameOf(Circle(r: 1.0)) == "circle"
29
+ assert Circle(r: 1.0).name() == "circle"
36
- assert nameOf(Rect(w: 1.0, h: 1.0)) == "rect"
30
+ assert Rect(w: 1.0, h: 1.0).name() == "rect"