plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
fe4959d
— Peter John
2026-11-02T19:47:30+05:30
T4: checker trait rules
- plum-checker/src/lib.rs +78 -19
- plum-checker/src/traits.rs +58 -0
- plum-checker/tests/trait_fields_tests.rs +0 -1
- plum-checker/tests/traits_tests.rs +115 -0
plum-checker/src/lib.rs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// naming convention), not Rust's idiomatic snake_case — silence the resulting lint.
|
|
3
3
|
#![allow(non_snake_case)]
|
|
4
4
|
|
|
5
|
+
pub mod traits;
|
|
5
6
|
pub mod types;
|
|
6
7
|
pub mod unions;
|
|
7
8
|
|
|
@@ -1032,6 +1033,7 @@ pub struct CheckCtx<'a> {
|
|
|
1032
1033
|
/// `plum-wasm-codegen`, which needs the same tables to resolve `self`, field access,
|
|
1033
1034
|
/// and method dispatch during code generation.
|
|
1034
1035
|
pub fn buildGlobalTables(source: &ast::Source) -> (TypeEnv, MethodEnv, EnumVariants, EnumParams) {
|
|
1036
|
+
traits::register(source);
|
|
1035
1037
|
let conv = plumTypeFromAstG;
|
|
1036
1038
|
let mut global_env: TypeEnv = TypeEnv::new();
|
|
1037
1039
|
let mut methods: MethodEnv = BTreeMap::new();
|
|
@@ -1209,10 +1211,7 @@ fn checkUnions(
|
|
|
1209
1211
|
plum_core::unions::beginPlan();
|
|
1210
1212
|
let (result, info) = check(©);
|
|
1211
1213
|
let plan = plum_core::unions::takePlan();
|
|
1212
|
-
let empty = plan.
|
|
1214
|
+
let empty = plan.isEmpty();
|
|
1213
|
-
&& plan.patterns.is_empty()
|
|
1214
|
-
&& plan.narrowings.is_empty()
|
|
1215
|
-
&& plan.dispatches.is_empty();
|
|
1216
1215
|
let out = if result.is_err() {
|
|
1217
1216
|
// Report errors against the original, whose addresses codegen never sees.
|
|
1218
1217
|
(result, info)
|
|
@@ -2781,12 +2780,31 @@ fn checkStmtInner(
|
|
|
2781
2780
|
));
|
|
2782
2781
|
continue;
|
|
2783
2782
|
}
|
|
2783
|
+
let stmtAddr = stmt as *const ast::Stmt as usize;
|
|
2784
|
-
let
|
|
2784
|
+
let objTy = inferExpr(object, env, ctx).ok();
|
|
2785
|
+
if let Some(msg) = objTy
|
|
2785
|
-
.
|
|
2786
|
+
.as_ref()
|
|
2787
|
+
.and_then(|o| unions::nestedTraitWrite(object, o, ctx))
|
|
2788
|
+
{
|
|
2789
|
+
errors.push(CheckError::at(
|
|
2790
|
+
fn_pos.0,
|
|
2791
|
+
fn_pos.1,
|
|
2792
|
+
format!("fn '{}': assign '{}': {}", fn_name, label, msg),
|
|
2793
|
+
));
|
|
2794
|
+
continue;
|
|
2795
|
+
}
|
|
2796
|
+
let traitTy = objTy
|
|
2797
|
+
.as_ref()
|
|
2798
|
+
.and_then(|o| unions::traitField(o, field_name, stmtAddr, true, ctx));
|
|
2799
|
+
let field_expected = match &traitTy {
|
|
2800
|
+
Some(r) => r.clone().ok(),
|
|
2801
|
+
None => objTy
|
|
2802
|
+
.as_ref()
|
|
2786
|
-
|
|
2803
|
+
.and_then(|o| fieldTypeOf(o, field_name, ctx).ok()),
|
|
2804
|
+
};
|
|
2787
2805
|
match (inferExpr(object, env, ctx), inferExprWith(value, env, ctx, field_expected.as_ref())) {
|
|
2788
2806
|
(Ok(obj_ty @ (PlumType::TNamed(_) | PlumType::TApp(..))), Ok(value_ty)) => {
|
|
2789
|
-
match fieldTypeOf(&obj_ty, field_name, ctx) {
|
|
2807
|
+
match traitTy.unwrap_or_else(|| fieldTypeOf(&obj_ty, field_name, ctx)) {
|
|
2790
2808
|
Ok(field_ty) => {
|
|
2791
2809
|
if let Err(msg) = unifyArg(&field_ty, &value_ty, value, ctx) {
|
|
2792
2810
|
errors.push(CheckError::at(fn_pos.0, fn_pos.1, format!("fn '{}': assign '{}': {}", fn_name, label, msg)));
|
|
@@ -3255,6 +3273,31 @@ fn checkMatch(
|
|
|
3255
3273
|
// Exhaustiveness: enum / Bool / error-name subjects must cover every
|
|
3256
3274
|
// constructor (recursively through variant payloads) unless a catch-all
|
|
3257
3275
|
// arm exists. Literal (Int/Str/Float) subjects are left alone.
|
|
3276
|
+
if let [PlumType::TNamed(tn)] = subject_types.as_slice() {
|
|
3277
|
+
if traits::isTrait(tn, ctx) {
|
|
3278
|
+
let hasWildcard = m.cases.iter().any(|c| {
|
|
3279
|
+
c.guard.is_none()
|
|
3280
|
+
&& c.patterns.len() == 1
|
|
3281
|
+
&& matches!(c.patterns[0], ast::CasePattern::Wildcard)
|
|
3282
|
+
});
|
|
3283
|
+
// Lowered dispatch matches name hidden variants (`Entity__House`) and stay exhaustive.
|
|
3284
|
+
let lowered = m.cases.iter().any(|c| {
|
|
3285
|
+
c.patterns.iter().any(|p| match p {
|
|
3286
|
+
ast::CasePattern::Class { name, .. } | ast::CasePattern::Name(name) => {
|
|
3287
|
+
name.contains("__")
|
|
3288
|
+
}
|
|
3289
|
+
_ => false,
|
|
3290
|
+
})
|
|
3291
|
+
});
|
|
3292
|
+
if !hasWildcard && !lowered {
|
|
3293
|
+
errors.push(CheckError::at(
|
|
3294
|
+
fn_pos.0,
|
|
3295
|
+
fn_pos.1,
|
|
3296
|
+
format!("fn '{}': match on trait {} needs a `_` case", fn_name, tn),
|
|
3297
|
+
));
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3258
3301
|
if let [subject_ty] = subject_types.as_slice() {
|
|
3259
3302
|
let rows: Vec<Vec<&ast::CasePattern>> = m
|
|
3260
3303
|
.cases
|
|
@@ -3262,19 +3305,23 @@ fn checkMatch(
|
|
|
3262
3305
|
.filter(|c| c.guard.is_none() && c.patterns.len() == 1)
|
|
3263
3306
|
.map(|c| vec![&c.patterns[0]])
|
|
3264
3307
|
.collect();
|
|
3308
|
+
let isTraitSubject =
|
|
3309
|
+
matches!(subject_ty, PlumType::TNamed(tn) if traits::isTrait(tn, ctx));
|
|
3265
3310
|
if let Err(CoverFail::Reported(label, missing)) =
|
|
3266
3311
|
coverRows(rows, std::slice::from_ref(subject_ty), ctx)
|
|
3267
3312
|
{
|
|
3313
|
+
if !isTraitSubject {
|
|
3268
|
-
|
|
3314
|
+
errors.push(CheckError::at(
|
|
3269
|
-
|
|
3315
|
+
fn_pos.0,
|
|
3270
|
-
|
|
3316
|
+
fn_pos.1,
|
|
3271
|
-
|
|
3317
|
+
format!(
|
|
3272
|
-
|
|
3318
|
+
"fn '{}': non-exhaustive match on {}: missing {}",
|
|
3273
|
-
|
|
3319
|
+
fn_name,
|
|
3274
|
-
|
|
3320
|
+
label,
|
|
3275
|
-
|
|
3321
|
+
missing.join(", ")
|
|
3276
|
-
|
|
3322
|
+
),
|
|
3277
|
-
|
|
3323
|
+
));
|
|
3324
|
+
}
|
|
3278
3325
|
}
|
|
3279
3326
|
}
|
|
3280
3327
|
|
|
@@ -4730,7 +4777,16 @@ fn inferExprInner(
|
|
|
4730
4777
|
// `Str` is an ordinary class with a real field; a generic
|
|
4731
4778
|
// class substitutes its receiver's type arguments.
|
|
4732
4779
|
PlumType::TNamed(_) | PlumType::TApp(..) => {
|
|
4780
|
+
match unions::traitField(
|
|
4781
|
+
&obj_ty,
|
|
4782
|
+
field_name,
|
|
4783
|
+
expr as *const ast::Expr as usize,
|
|
4784
|
+
false,
|
|
4785
|
+
ctx,
|
|
4786
|
+
) {
|
|
4787
|
+
Some(r) => r,
|
|
4733
|
-
|
|
4788
|
+
None => fieldTypeOf(&obj_ty, field_name, ctx),
|
|
4789
|
+
}
|
|
4734
4790
|
}
|
|
4735
4791
|
// An unresolved generic return reaches here as a plain `TVar`;
|
|
4736
4792
|
// field access on it is unmodeled — allow, codegen will catch.
|
|
@@ -4742,6 +4798,9 @@ fn inferExprInner(
|
|
|
4742
4798
|
},
|
|
4743
4799
|
ast::AttrKind::Method(call) => {
|
|
4744
4800
|
checkStaticUse(&attr.object, &obj_ty, &call.name, ctx)?;
|
|
4801
|
+
if let Some(msg) = traits::methodError(&obj_ty, &call.name, ctx) {
|
|
4802
|
+
return Err(msg);
|
|
4803
|
+
}
|
|
4745
4804
|
// A member value calling a method only its enum defines.
|
|
4746
4805
|
if let Some(r) = unions::memberFallback(&obj_ty, &call.name, ctx) {
|
|
4747
4806
|
let (en, variant) = r?;
|
plum-checker/src/traits.rs
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
//! Trait rules in the checker. A trait is a union enum (`Enum.isTrait`) whose
|
|
2
|
+
//! members are the embedding records; the declared methods come from `Source.traits`.
|
|
3
|
+
use crate::CheckCtx;
|
|
4
|
+
use plum_core::ast;
|
|
5
|
+
use std::cell::RefCell;
|
|
6
|
+
use std::collections::BTreeMap;
|
|
7
|
+
|
|
8
|
+
thread_local! {
|
|
9
|
+
static TRAITS: RefCell<BTreeMap<String, Vec<String>>> = const { RefCell::new(BTreeMap::new()) };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/// Records the traits of `source` (called while the global tables are built).
|
|
13
|
+
pub fn register(source: &ast::Source) {
|
|
14
|
+
let mut out: BTreeMap<String, Vec<String>> = BTreeMap::new();
|
|
15
|
+
for item in &source.items {
|
|
16
|
+
if let ast::Item::Enum(e) = item {
|
|
17
|
+
if e.isTrait {
|
|
18
|
+
out.insert(e.name.clone(), Vec::new());
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
for t in &source.traits {
|
|
23
|
+
let names = t
|
|
24
|
+
.required
|
|
25
|
+
.iter()
|
|
26
|
+
.chain(t.methods.iter())
|
|
27
|
+
.map(|f| f.name.clone())
|
|
28
|
+
.collect();
|
|
29
|
+
out.insert(t.name.clone(), names);
|
|
30
|
+
}
|
|
31
|
+
TRAITS.with(|c| *c.borrow_mut() = out);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
pub fn isTrait(name: &str, _ctx: &CheckCtx) -> bool {
|
|
35
|
+
TRAITS.with(|c| c.borrow().contains_key(name))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// The trait's declared methods: required first, then defaults, declaration order.
|
|
39
|
+
pub fn traitMethods(name: &str, _ctx: &CheckCtx) -> Vec<String> {
|
|
40
|
+
TRAITS.with(|c| c.borrow().get(name).cloned().unwrap_or_default())
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// The error for calling `method` on a value of trait type `ty`, if it is not declared.
|
|
44
|
+
pub fn methodError(ty: &crate::types::PlumType, method: &str, ctx: &CheckCtx) -> Option<String> {
|
|
45
|
+
let name = match ty {
|
|
46
|
+
crate::types::PlumType::TNamed(n) => n,
|
|
47
|
+
_ => return None,
|
|
48
|
+
};
|
|
49
|
+
if !isTrait(name, ctx)
|
|
50
|
+
|| method.starts_with("__get_")
|
|
51
|
+
|| method.starts_with("__set_")
|
|
52
|
+
|| traitMethods(name, ctx).iter().any(|m| m == method)
|
|
53
|
+
|| ctx.methods.contains_key(&(name.clone(), method.to_string()))
|
|
54
|
+
{
|
|
55
|
+
return None;
|
|
56
|
+
}
|
|
57
|
+
Some(format!("trait {name} has no method {method}"))
|
|
58
|
+
}
|
plum-checker/tests/trait_fields_tests.rs
CHANGED
|
@@ -208,7 +208,6 @@ fn plain_union_unaffected() {
|
|
|
208
208
|
/// Through the checker proper (needs the `traitField` call sites in lib.rs and
|
|
209
209
|
/// `UnionPlan::isEmpty` in `checkUnions`).
|
|
210
210
|
#[test]
|
|
211
|
-
#[ignore = "needs T4 call sites"]
|
|
212
211
|
fn typed_source_lowers_field_access() {
|
|
213
212
|
let s = lowered(
|
|
214
213
|
"fun bump(e: Ent) -> Float =\n e.z = e.z + 1.0\n e.z\n\nfun nest(e: Ent) =\n e.pos.x = 1.0\n",
|
plum-checker/tests/traits_tests.rs
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#![allow(non_snake_case)]
|
|
2
|
+
//! Checker rules for traits: only declared methods on a trait value, `_` required in a
|
|
3
|
+
//! match, conversion of embedding records, default methods calling the record's own.
|
|
4
|
+
use plum_checker::checkSource;
|
|
5
|
+
use plum_core::traits::lowerTraits;
|
|
6
|
+
use plum_core::unions::resolveMembers;
|
|
7
|
+
use plum_core::AstParser;
|
|
8
|
+
|
|
9
|
+
const DEFS: &str = "\
|
|
10
|
+
record Vec2(x: Float, y: Float)
|
|
11
|
+
|
|
12
|
+
trait Entity(pos: Vec2, z: Int) =
|
|
13
|
+
fun update(self, dt: Float)
|
|
14
|
+
fun draw(self) -> Int
|
|
15
|
+
fun moveBy(self, d: Vec2) =
|
|
16
|
+
_ := self.draw()
|
|
17
|
+
self.z = self.z + 1
|
|
18
|
+
|
|
19
|
+
record House(Entity, kind: Int) =
|
|
20
|
+
fun update(self, dt: Float) =
|
|
21
|
+
_ := self.draw()
|
|
22
|
+
fun draw(self) -> Int = 1
|
|
23
|
+
fun onClick(self) -> Int = 2
|
|
24
|
+
|
|
25
|
+
record Soldier(Entity) =
|
|
26
|
+
fun update(self, dt: Float) =
|
|
27
|
+
_ := self.draw()
|
|
28
|
+
fun draw(self) -> Int = 3
|
|
29
|
+
";
|
|
30
|
+
|
|
31
|
+
fn run(defs: &str, body: &str) -> Result<(), String> {
|
|
32
|
+
let src = format!("{defs}\n{body}\n");
|
|
33
|
+
let mut parser = tree_sitter::Parser::new();
|
|
34
|
+
parser
|
|
35
|
+
.set_language(&tree_sitter_plum::LANGUAGE.into())
|
|
36
|
+
.unwrap();
|
|
37
|
+
let tree = parser.parse(&src, None).unwrap();
|
|
38
|
+
assert!(!tree.root_node().has_error(), "parse errors:\n{src}");
|
|
39
|
+
let mut source = AstParser::new(&src).parseSourceRaw(tree.root_node());
|
|
40
|
+
let errs = lowerTraits(&mut source);
|
|
41
|
+
assert!(errs.is_empty(), "{errs:?}");
|
|
42
|
+
let errs = resolveMembers(&mut source);
|
|
43
|
+
assert!(errs.is_empty(), "{errs:?}");
|
|
44
|
+
checkSource(&source).map_err(|e| e.into_iter().map(|x| x.message).collect::<Vec<_>>().join(" | "))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#[test]
|
|
48
|
+
fn only_trait_methods() {
|
|
49
|
+
let e = run(DEFS, "fun f(e: Entity) =\n e.onClick()").unwrap_err();
|
|
50
|
+
assert!(e.contains("trait Entity has no method onClick"), "{e}");
|
|
51
|
+
run(DEFS, "fun f(e: Entity) =\n _ := e.draw()").unwrap();
|
|
52
|
+
run(DEFS, "fun f(e: Entity) =\n e.moveBy(Vec2(x: 1.0, y: 1.0))").unwrap();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#[test]
|
|
56
|
+
fn match_trait_needs_wildcard() {
|
|
57
|
+
let e = run(DEFS, "fun f(e: Entity) -> Int =\n match e\n House(a, b, c) => 1").unwrap_err();
|
|
58
|
+
assert!(e.contains("match on trait Entity needs a `_` case"), "{e}");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#[test]
|
|
62
|
+
fn match_trait_wildcard_narrows() {
|
|
63
|
+
run(
|
|
64
|
+
DEFS,
|
|
65
|
+
"fun f(e: Entity) -> Int =\n match e\n House(p, z, k) => k\n _ => 0",
|
|
66
|
+
)
|
|
67
|
+
.unwrap();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#[test]
|
|
71
|
+
fn union_enum_keeps_exhaustive_and_shared_dispatch() {
|
|
72
|
+
let defs = "\
|
|
73
|
+
record Circle(r: Float) =
|
|
74
|
+
fun area(self) -> Float = self.r
|
|
75
|
+
record Square(s: Float) =
|
|
76
|
+
fun area(self) -> Float = self.s
|
|
77
|
+
enum Shape = Circle | Square
|
|
78
|
+
";
|
|
79
|
+
run(defs, "fun f(s: Shape) -> Float =\n s.area()").unwrap();
|
|
80
|
+
run(
|
|
81
|
+
defs,
|
|
82
|
+
"fun f(s: Shape) -> Int =\n match s\n Circle(r) => 1\n Square(q) => 2",
|
|
83
|
+
)
|
|
84
|
+
.unwrap();
|
|
85
|
+
let e = run(defs, "fun f(s: Shape) -> Int =\n match s\n Circle(r) => 1").unwrap_err();
|
|
86
|
+
assert!(e.contains("non-exhaustive"), "{e}");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#[test]
|
|
90
|
+
fn record_in_trait_and_union_enum() {
|
|
91
|
+
let defs = format!("{DEFS}\nenum Thing = House | Soldier\n");
|
|
92
|
+
run(&defs, "fun f(t: Thing) -> Int =\n 1\n\nfun g() -> Int =\n f(Soldier(pos: Vec2(x: 1.0, y: 1.0), z: 0))").unwrap();
|
|
93
|
+
run(&defs, "fun f(e: Entity) -> Int =\n e.draw()\n\nfun g() -> Int =\n f(Soldier(pos: Vec2(x: 1.0, y: 1.0), z: 0))").unwrap();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#[test]
|
|
97
|
+
fn trait_param_accepts_record() {
|
|
98
|
+
run(
|
|
99
|
+
DEFS,
|
|
100
|
+
"fun f(e: Entity) -> Int =\n e.draw()\n\nfun g() -> Int =\n f(House(pos: Vec2(x: 1.0, y: 1.0), z: 0, kind: 1))",
|
|
101
|
+
)
|
|
102
|
+
.unwrap();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#[test]
|
|
106
|
+
fn default_self_call_checks() {
|
|
107
|
+
run(DEFS, "fun f(h: House) =\n h.moveBy(Vec2(x: 1.0, y: 1.0))").unwrap();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#[test]
|
|
111
|
+
fn trait_helpers() {
|
|
112
|
+
// traitMethods order is covered through the rule: declared methods pass, others fail.
|
|
113
|
+
let e = run(DEFS, "fun f(e: Entity) =\n e.nope()").unwrap_err();
|
|
114
|
+
assert!(e.contains("trait Entity has no method nope"), "{e}");
|
|
115
|
+
}
|