plum
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 +20 -5
- plum-checker/src/unions.rs +105 -1
- plum-checker/tests/unions_dispatch_test.rs +108 -0
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, ¶ms, 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)) =
|
|
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
|
+
}
|