plum

#treesitter#compiler#wasm

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

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


e9976f3 — Peter John 2026-11-01T20:34:14+05:30
T5: checker auto-dispatch
plum-checker/src/lib.rs CHANGED
@@ -750,7 +750,7 @@ fn unifyErr(t1: &PlumType, t2: &PlumType, ctx: &CheckCtx) -> Result<(), String>
750
750
  pub fn unify(t1: &PlumType, t2: &PlumType, ctx: &CheckCtx) -> Result<(), String> {
751
751
  match (t1, t2) {
752
752
  (PlumType::TVar(_), _) | (_, PlumType::TVar(_)) => Ok(()),
753
- (PlumType::TErr(..), _) | (_, PlumType::TErr(..)) => unifyErr(t1, t2, ctx),
753
+ (PlumType::TErr(..), _) | (_, PlumType::TErr(..)) => unions::nested(|| unifyErr(t1, t2, ctx)),
754
754
  (PlumType::TInt, PlumType::TInt) => Ok(()),
755
755
  (PlumType::TFloat, PlumType::TFloat) => Ok(()),
756
756
  (PlumType::TBool, PlumType::TBool) => Ok(()),
@@ -774,7 +774,7 @@ pub fn unify(t1: &PlumType, t2: &PlumType, ctx: &CheckCtx) -> Result<(), String>
774
774
  // Structural generics: same base, arguments unify pairwise.
775
775
  (PlumType::TApp(a, xs), PlumType::TApp(b, ys)) if a == b && xs.len() == ys.len() => {
776
776
  for (x, y) in xs.iter().zip(ys.iter()) {
777
- unify(x, y, ctx).map_err(|e| {
777
+ unions::nested(|| unify(x, y, ctx)).map_err(|e| {
778
778
  format!("type mismatch: expected {}, found {} ({})", t1, t2, e)
779
779
  })?;
780
780
  }
@@ -827,9 +827,9 @@ pub fn unify(t1: &PlumType, t2: &PlumType, ctx: &CheckCtx) -> Result<(), String>
827
827
  }
828
828
  (PlumType::TFun(ps1, r1), PlumType::TFun(ps2, r2)) if ps1.len() == ps2.len() => {
829
829
  for (p1, p2) in ps1.iter().zip(ps2.iter()) {
830
- unify(p1, p2, ctx)?;
830
+ unions::nested(|| unify(p1, p2, ctx))?;
831
831
  }
832
- unify(r1, r2, ctx)
832
+ unions::nested(|| unify(r1, r2, ctx))
833
833
  }
834
834
  // A member value converts to its union enum (`Circle` where `Shape` is expected).
835
835
  _ if unions::wrapVariantFor(t1, t2, ctx).is_some() => Ok(()),
@@ -3245,6 +3245,7 @@ fn inferMethodCall(
3245
3245
  ctx: &CheckCtx,
3246
3246
  expected: Option<&PlumType>,
3247
3247
  ) -> Result<PlumType, String> {
3248
+ let call_addr = CUR_EXPR.with(|c| c.get());
3248
3249
  // `e.name()` on an error value (`catch e`) is its text.
3249
3250
  if matches!(obj_ty, PlumType::TErrName(_)) && name == "name" && args.is_empty() {
3250
3251
  return Ok(PlumType::TStr);
@@ -3310,6 +3311,16 @@ fn inferMethodCall(
3310
3311
  }
3311
3312
  return Ok(PlumType::TVar("_".to_string()));
3312
3313
  };
3314
+ if let Some(r) = unions::dispatchSig(obj_ty, name, call_addr, ctx) {
3315
+ let (params, ret) = r?;
3316
+ let mut binds = Binds::new();
3317
+ if let Some(exp) = expected {
3318
+ bindMatch(&ret, exp, &mut binds);
3319
+ }
3320
+ let label = format!("method '{}.{}'", class_name, name);
3321
+ checkCallArgs(&label, &params, args, params.len(), env, ctx, &mut binds)?;
3322
+ return Ok(substVars(&ret, &binds));
3323
+ }
3313
3324
  let Some(sig) = ctx.methods.get(&(class_name.to_string(), name.to_string())) else {
3314
3325
  match plum_core::forms::seqMethodKind(name) {
3315
3326
  Some(false) => {
@@ -3679,7 +3690,11 @@ fn inferExprInner(
3679
3690
  // were never part of this check.
3680
3691
  for part in &s.parts {
3681
3692
  if let ast::StringPart::Interp(e) = part {
3693
+ let ity = inferExprWith(e, env, ctx, None);
3694
+ if let Ok(t) = &ity {
3695
+ unions::recordInterpolation(e, t, ctx);
3696
+ }
3682
- if let Ok(PlumType::TApp(n, targs)) = inferExprWith(e, env, ctx, None) {
3697
+ if let Ok(PlumType::TApp(n, targs)) = ity {
3683
3698
  let gens = enumGenericsOf(&n, ctx);
3684
3699
  if gens.len() == targs.len()
3685
3700
  && ctx.methods.contains_key(&(n.clone(), "toStr".to_string()))
plum-checker/src/unions.rs CHANGED
@@ -10,6 +10,16 @@ use std::collections::{BTreeMap, BTreeSet};
10
10
  thread_local! {
11
11
  /// Set while a member field is being unified, so conversions do not nest.
12
12
  static IN_WRAP: Cell<bool> = const { Cell::new(false) };
13
+ /// Nesting depth inside type arguments / fn types: conversion applies only at depth 0.
14
+ static DEPTH: Cell<usize> = const { Cell::new(0) };
15
+ }
16
+
17
+ /// Runs `f` (a unify of a nested type position), where no member conversion applies.
18
+ pub fn nested<R>(f: impl FnOnce() -> R) -> R {
19
+ DEPTH.with(|d| d.set(d.get() + 1));
20
+ let r = f();
21
+ DEPTH.with(|d| d.set(d.get() - 1));
22
+ r
13
23
  }
14
24
 
15
25
  fn enumOf(ty: &PlumType) -> Option<(&str, &[PlumType])> {
@@ -53,7 +63,7 @@ pub fn isUnion(enumName: &str, ctx: &CheckCtx) -> bool {
53
63
 
54
64
  /// The hidden variant that converts `actual` into the union enum `expected`.
55
65
  pub fn wrapVariantFor(expected: &PlumType, actual: &PlumType, ctx: &CheckCtx) -> Option<String> {
56
- if IN_WRAP.with(|c| c.get()) {
66
+ if IN_WRAP.with(|c| c.get()) || DEPTH.with(|d| d.get()) > 0 {
57
67
  return None;
58
68
  }
59
69
  if matches!(actual, PlumType::TVar(_) | PlumType::TParam(_) | PlumType::TErr(..)) {
@@ -278,3 +288,97 @@ pub fn copyWriteError(object: &ast::Expr, env: &TypeEnv) -> Option<String> {
278
288
  "{name} is a copy of the {member} inside {en}; assign it to a local to change it"
279
289
  ))
280
290
  }
291
+
292
+ // ---- auto-dispatch -----------------------------------------------------------
293
+
294
+ /// Every variant of `enumName` in tag order: `(variant name, member type or None for a tag)`.
295
+ fn allVariants(enumName: &str, args: &[PlumType], ctx: &CheckCtx) -> Vec<(String, Option<PlumType>)> {
296
+ let prefix = format!("{enumName}__");
297
+ let mut vs: Vec<(&String, &EnumVariantInfo)> =
298
+ ctx.enum_variants.iter().filter(|(_, i)| i.enum_name == enumName).collect();
299
+ vs.sort_by_key(|(_, i)| i.tag);
300
+ let ms = members(enumName, args, ctx);
301
+ vs.into_iter()
302
+ .map(|(n, _)| {
303
+ let m = if n.starts_with(&prefix) {
304
+ ms.iter().find(|(v, _)| v == n).map(|(_, t)| t.clone())
305
+ } else {
306
+ None
307
+ };
308
+ (n.clone(), m)
309
+ })
310
+ .collect()
311
+ }
312
+
313
+ /// One member's instantiated method signature, if its type defines the method.
314
+ fn memberSig(mt: &PlumType, name: &str, ctx: &CheckCtx) -> Option<PlumType> {
315
+ let recv = crate::methodReceiverName(mt)?;
316
+ let sig = ctx.methods.get(&(recv.clone(), name.to_string()))?;
317
+ let owner = match mt {
318
+ PlumType::TApp(n, args) if *n == recv => crate::appSubst(crate::enumGenericsOf(n, ctx), args),
319
+ _ => BTreeMap::new(),
320
+ };
321
+ Some(crate::instSigMap(sig, &owner).0)
322
+ }
323
+
324
+ fn memberLabel(variant: &str, enumName: &str) -> String {
325
+ variant.strip_prefix(&format!("{enumName}__")).unwrap_or(variant).to_string()
326
+ }
327
+
328
+ /// For `obj.name(..)` on a union enum with no method of its own: the common
329
+ /// signature of the members' methods, and the dispatch is recorded. `None`
330
+ /// when the receiver is not a union or defines `name` itself.
331
+ pub fn dispatchSig(
332
+ obj: &PlumType,
333
+ name: &str,
334
+ callAddr: usize,
335
+ ctx: &CheckCtx,
336
+ ) -> Option<Result<(Vec<PlumType>, PlumType), String>> {
337
+ let (en, args) = enumOf(obj)?;
338
+ if ctx.methods.contains_key(&(en.to_string(), name.to_string())) || !isUnion(en, ctx) {
339
+ return None;
340
+ }
341
+ let mut first: Option<(String, PlumType)> = None;
342
+ let mut variants = Vec::new();
343
+ for (v, m) in allVariants(en, args, ctx) {
344
+ let label = memberLabel(&v, en);
345
+ let Some(mt) = m else {
346
+ return Some(Err(format!("{en} has no method '{name}': member {label} is a tag")));
347
+ };
348
+ let Some(sig) = memberSig(&mt, name, ctx) else {
349
+ return Some(Err(format!("{en} has no method '{name}': member {label} doesn't define it")));
350
+ };
351
+ match &first {
352
+ None => first = Some((label, sig)),
353
+ Some((l0, s0)) => {
354
+ if unify(s0, &sig, ctx).is_err() {
355
+ return Some(Err(format!(
356
+ "{en}.{name}: members disagree: {l0}.{name} is {s0}, {label}.{name} is {sig}"
357
+ )));
358
+ }
359
+ }
360
+ }
361
+ variants.push(v);
362
+ }
363
+ let (_, sig) = first?;
364
+ plan::recordDispatch(callAddr, variants);
365
+ let PlumType::TFun(params, ret) = sig else { return None };
366
+ Some(Ok((params, *ret)))
367
+ }
368
+
369
+ /// `"{e}"` on a union enum: records a `toStr` dispatch when every member has `toStr`.
370
+ pub fn recordInterpolation(e: &ast::Expr, ty: &PlumType, ctx: &CheckCtx) {
371
+ let Some((en, args)) = enumOf(ty) else { return };
372
+ if ctx.methods.contains_key(&(en.to_string(), "toStr".to_string())) || !isUnion(en, ctx) {
373
+ return;
374
+ }
375
+ let vs = allVariants(en, args, ctx);
376
+ let mut out = Vec::new();
377
+ for (v, m) in vs {
378
+ match m {
379
+ Some(mt) if memberSig(&mt, "toStr", ctx).is_some() => out.push(v),
380
+ _ => return,
381
+ }
382
+ }
383
+ plan::recordDispatch(e as *const ast::Expr as usize, out);
384
+ }
plum-checker/tests/unions_dispatch_test.rs ADDED
@@ -0,0 +1,108 @@
1
+ #![allow(non_snake_case)]
2
+ use plum_checker::checkSource;
3
+ use plum_core::unions::{beginPlan, resolveMembers, takePlan, UnionPlan};
4
+ use plum_core::AstParser;
5
+
6
+ const DEFS: &str = "\
7
+ record Circle(r: Float) =
8
+ fun area(self) -> Float = self.r
9
+ fun toStr(self) -> Str = \"c\"
10
+
11
+ record Rectangle(w: Float) =
12
+ fun area(self) -> Float = self.w
13
+ fun toStr(self) -> Str = \"r\"
14
+
15
+ record Odd(w: Float) =
16
+ fun area(self) -> Int = 1
17
+
18
+ record NoArea(w: Float)
19
+
20
+ enum Shape = Circle | Rectangle
21
+ enum Own =
22
+ | Circle
23
+ | Rectangle
24
+
25
+ fun area(self) -> Float = 0.0
26
+
27
+ enum WithTag = Circle | Plain
28
+ enum Missing = Circle | NoArea
29
+ enum Clash = Circle | Odd
30
+
31
+ record Wrap[T](v: T) =
32
+ fun toStr(self) -> Str = \"w\"
33
+
34
+ enum Js = Circle | Wrap[Js]
35
+ ";
36
+
37
+ fn run(body: &str) -> (Result<(), Vec<String>>, UnionPlan) {
38
+ let src = format!("{DEFS}\n{body}\n");
39
+ let mut parser = tree_sitter::Parser::new();
40
+ parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap();
41
+ let tree = parser.parse(&src, None).unwrap();
42
+ assert!(!tree.root_node().has_error(), "parse errors:\n{src}");
43
+ let mut source = AstParser::new(&src).parseSource(tree.root_node());
44
+ let errs = resolveMembers(&mut source);
45
+ assert!(errs.is_empty(), "{errs:?}");
46
+ beginPlan();
47
+ let r = checkSource(&source).map_err(|e| e.into_iter().map(|x| x.message).collect());
48
+ (r, takePlan())
49
+ }
50
+
51
+ fn errs(r: Result<(), Vec<String>>) -> String {
52
+ r.unwrap_err().join(" | ")
53
+ }
54
+
55
+ #[test]
56
+ fn dispatch_ok() {
57
+ let (r, p) = run("fun f(s: Shape) -> Float =\n s.area()");
58
+ r.unwrap();
59
+ assert_eq!(p.dispatches.len(), 1);
60
+ let v = p.dispatches.values().next().unwrap();
61
+ assert_eq!(v, &vec!["Shape__Circle".to_string(), "Shape__Rectangle".to_string()]);
62
+ let (r, _) = run("fun f(s: Shape) -> Int =\n x := s.area()\n take(x)\n\nfun take(i: Int) -> Int =\n i");
63
+ assert!(r.is_err(), "area() is Float, not Int");
64
+ }
65
+
66
+ #[test]
67
+ fn own_method_wins() {
68
+ let (r, p) = run("fun f(s: Own) -> Float =\n s.area()");
69
+ r.unwrap();
70
+ assert!(p.dispatches.is_empty());
71
+ }
72
+
73
+ #[test]
74
+ fn tag_member_error() {
75
+ let (r, _) = run("fun f(s: WithTag) -> Float =\n s.area()");
76
+ assert!(errs(r).contains("WithTag has no method 'area': member Plain is a tag"));
77
+ }
78
+
79
+ #[test]
80
+ fn missing_method_error() {
81
+ let (r, _) = run("fun f(s: Missing) -> Float =\n s.area()");
82
+ assert!(errs(r).contains("Missing has no method 'area': member NoArea doesn't define it"));
83
+ }
84
+
85
+ #[test]
86
+ fn signature_mismatch_error() {
87
+ let (r, _) = run("fun f(s: Clash) -> Float =\n s.area()");
88
+ let e = errs(r);
89
+ assert!(e.contains("Clash.area: members disagree: Circle.area is "), "{e}");
90
+ assert!(e.contains(", Odd.area is "), "{e}");
91
+ }
92
+
93
+ #[test]
94
+ fn interpolation_toStr() {
95
+ let (r, p) = run("fun f(s: Shape) -> Str =\n \"{s}\"");
96
+ r.unwrap();
97
+ assert_eq!(p.dispatches.len(), 1);
98
+ let (r, p) = run("fun f(s: Missing) -> Str =\n \"{s}\"");
99
+ let _ = r;
100
+ assert!(p.dispatches.is_empty());
101
+ }
102
+
103
+ #[test]
104
+ fn generic_member_dispatch() {
105
+ let (r, p) = run("fun f(j: Js) -> Str =\n j.toStr()");
106
+ r.unwrap();
107
+ assert_eq!(p.dispatches.values().next().unwrap().len(), 2);
108
+ }