plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
a039616
— Peter John
2026-11-02T10:41:03+05:30
feat: a fun without self is a static; Type.f() and x.f() are checked
- README.md +4 -1
- plum-checker/src/lib.rs +61 -3
- plum-checker/tests/checker_tests.rs +11 -6
- plum-examples/structs_records.plum +3 -3
- plum-lsp/src/completion.rs +4 -0
- plum-lsp/src/main.rs +2 -0
- plum-wasm-codegen/src/lib.rs +1 -0
README.md
CHANGED
|
@@ -507,7 +507,7 @@ enum Step(n: Int) =
|
|
|
507
507
|
|
|
508
508
|
### Methods, `self` and field access
|
|
509
509
|
|
|
510
|
-
A `fun` declared indented directly inside an `enum`, `struct` or `record` body is a method
|
|
510
|
+
A `fun` declared indented directly inside an `enum`, `struct` or `record` body whose first parameter is `self` is a method, called on a value (`c.getAge()`). One without `self` is a static function, called on the type (`Cat.named("Tom")`), and can't use `self`. Calling a static on a value, or a method on the type, is an error:
|
|
511
511
|
|
|
512
512
|
```plum
|
|
513
513
|
record Cat(name: Str, age: Int) =
|
|
@@ -517,6 +517,9 @@ record Cat(name: Str, age: Int) =
|
|
|
517
517
|
fun birthday(self) -> Int =
|
|
518
518
|
self.age + 1
|
|
519
519
|
|
|
520
|
+
fun named(name: Str) -> Cat = # static: Cat.named("Tom")
|
|
521
|
+
Cat(name: name, age: 0)
|
|
522
|
+
|
|
520
523
|
record Wrapper(inner: Cat, tag: Int)
|
|
521
524
|
|
|
522
525
|
fun main() -> Int =
|
plum-checker/src/lib.rs
CHANGED
|
@@ -943,6 +943,23 @@ pub fn buildPrimitiveGenerics(source: &ast::Source) -> PrimitiveGenerics {
|
|
|
943
943
|
.collect()
|
|
944
944
|
}
|
|
945
945
|
|
|
946
|
+
/// `(type, name)` of every function declared in a type body without `self`: a
|
|
947
|
+
/// static, called as `Type.name(args)`, never on a value.
|
|
948
|
+
pub type StaticMethods = BTreeSet<(String, String)>;
|
|
949
|
+
|
|
950
|
+
pub fn buildStaticMethods(source: &ast::Source) -> StaticMethods {
|
|
951
|
+
source
|
|
952
|
+
.items
|
|
953
|
+
.iter()
|
|
954
|
+
.filter_map(|item| match item {
|
|
955
|
+
ast::Item::Fn(f) if !f.has_self => {
|
|
956
|
+
f.type_param.as_ref().map(|t| (t.clone(), f.name.clone()))
|
|
957
|
+
}
|
|
958
|
+
_ => None,
|
|
959
|
+
})
|
|
960
|
+
.collect()
|
|
961
|
+
}
|
|
962
|
+
|
|
946
963
|
pub fn buildMinRequiredArgs(source: &ast::Source) -> MinRequiredArgs {
|
|
947
964
|
let mut result = MinRequiredArgs::new();
|
|
948
965
|
for item in &source.items {
|
|
@@ -969,6 +986,9 @@ pub struct CheckCtx<'a> {
|
|
|
969
986
|
pub enum_params: &'a EnumParams,
|
|
970
987
|
pub min_required: &'a MinRequiredArgs,
|
|
971
988
|
pub primitive_generics: &'a PrimitiveGenerics,
|
|
989
|
+
/// `None` skips the static-vs-method call check: codegen re-infers source
|
|
990
|
+
/// the checker already accepted.
|
|
991
|
+
pub statics: Option<&'a StaticMethods>,
|
|
972
992
|
}
|
|
973
993
|
|
|
974
994
|
/// Builds the global lookup tables (function/const signatures, class fields, method
|
|
@@ -1616,12 +1636,14 @@ fn checkSourceInner(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
|
|
|
1616
1636
|
let (mut global_env, methods, enum_variants, enum_params) = buildGlobalTables(source);
|
|
1617
1637
|
let min_required = buildMinRequiredArgs(source);
|
|
1618
1638
|
let primitive_generics = buildPrimitiveGenerics(source);
|
|
1639
|
+
let statics = buildStaticMethods(source);
|
|
1619
1640
|
let ctx = CheckCtx {
|
|
1620
1641
|
methods: &methods,
|
|
1621
1642
|
enum_variants: &enum_variants,
|
|
1622
1643
|
enum_params: &enum_params,
|
|
1623
1644
|
min_required: &min_required,
|
|
1624
1645
|
primitive_generics: &primitive_generics,
|
|
1646
|
+
statics: Some(&statics),
|
|
1625
1647
|
};
|
|
1626
1648
|
// Constants are inlined at their use sites, so their type is whatever their
|
|
1627
1649
|
// initializer infers to (`FLAG = true`, `N = LIMIT * 2 + 1`, `T = List(1, 2)`).
|
|
@@ -2105,8 +2127,9 @@ fn checkFn(
|
|
|
2105
2127
|
}
|
|
2106
2128
|
}
|
|
2107
2129
|
|
|
2130
|
+
// Methods (a `fun` in a type body whose first parameter is `self`) get a
|
|
2108
|
-
//
|
|
2131
|
+
// `self: Receiver` binding; a `fun` without `self` is a static.
|
|
2109
|
-
if let Some(recv) = &f.type_param {
|
|
2132
|
+
if let (Some(recv), true) = (&f.type_param, f.has_self) {
|
|
2110
2133
|
let generics = enumGenericsOf(recv, ctx);
|
|
2111
2134
|
let self_ty = if generics.is_empty() {
|
|
2112
2135
|
plumTypeFromName(recv)
|
|
@@ -3905,7 +3928,9 @@ fn inferExprInner(
|
|
|
3905
3928
|
Some(s) if !s.vars.is_empty() => Ok(instSig(&s.body, &BTreeMap::new())),
|
|
3906
3929
|
_ => lookup(env, name),
|
|
3907
3930
|
},
|
|
3908
|
-
ast::Expr::Self_ => lookup(env, "self")
|
|
3931
|
+
ast::Expr::Self_ => lookup(env, "self").map_err(|_| {
|
|
3932
|
+
"`self` is only available in a method, a `fun` in a type body whose first parameter is `self` (one without `self` is a static, called as `Type.f()`)".to_string()
|
|
3933
|
+
}),
|
|
3909
3934
|
// A bare `NAME` lexes as a type_identifier whenever it starts with an
|
|
3910
3935
|
// uppercase letter — which includes every SCREAMING_CASE top-level
|
|
3911
3936
|
// const (`PI`, `MAX_VALUE`, ...), since there's no separate "constant"
|
|
@@ -4309,6 +4334,7 @@ fn inferExprInner(
|
|
|
4309
4334
|
)),
|
|
4310
4335
|
},
|
|
4311
4336
|
ast::AttrKind::Method(call) => {
|
|
4337
|
+
checkStaticUse(&attr.object, &obj_ty, &call.name, ctx)?;
|
|
4312
4338
|
// A member value calling a method only its enum defines.
|
|
4313
4339
|
if let Some(r) = unions::memberFallback(&obj_ty, &call.name, ctx) {
|
|
4314
4340
|
let (en, variant) = r?;
|
|
@@ -4322,6 +4348,38 @@ fn inferExprInner(
|
|
|
4322
4348
|
}
|
|
4323
4349
|
}
|
|
4324
4350
|
|
|
4351
|
+
/// `Type.f()` must name a static (a `fun` without `self`) and `x.f()` a
|
|
4352
|
+
/// method: a static has no value to run on, and a method needs one.
|
|
4353
|
+
fn checkStaticUse(
|
|
4354
|
+
object: &ast::Expr,
|
|
4355
|
+
obj_ty: &PlumType,
|
|
4356
|
+
name: &str,
|
|
4357
|
+
ctx: &CheckCtx,
|
|
4358
|
+
) -> Result<(), String> {
|
|
4359
|
+
let (Some(statics), Some(class)) = (ctx.statics, methodReceiverName(obj_ty)) else {
|
|
4360
|
+
return Ok(());
|
|
4361
|
+
};
|
|
4362
|
+
let key = (class.clone(), name.to_string());
|
|
4363
|
+
if !ctx.methods.contains_key(&key) {
|
|
4364
|
+
return Ok(());
|
|
4365
|
+
}
|
|
4366
|
+
let on_type = matches!(object, ast::Expr::TypeName(n) if *n == class);
|
|
4367
|
+
let is_static = statics.contains(&key);
|
|
4368
|
+
if on_type && !is_static {
|
|
4369
|
+
return Err(format!(
|
|
4370
|
+
"'{}' is a method of {} (it takes `self`): call it on a value, `x.{}(...)`",
|
|
4371
|
+
name, class, name
|
|
4372
|
+
));
|
|
4373
|
+
}
|
|
4374
|
+
if !on_type && is_static {
|
|
4375
|
+
return Err(format!(
|
|
4376
|
+
"'{}' is a static function of {} (no `self`): call it as `{}.{}(...)`",
|
|
4377
|
+
name, class, class, name
|
|
4378
|
+
));
|
|
4379
|
+
}
|
|
4380
|
+
Ok(())
|
|
4381
|
+
}
|
|
4382
|
+
|
|
4325
4383
|
/// Makes errors from expanded for-forms and from `fun*` definitions
|
|
4326
4384
|
/// readable: the checker only sees the expanded code (hygienic locals named
|
|
4327
4385
|
/// `__f<id>_x`) and ordinary-fn copies named `__star_<name>`.
|
plum-checker/tests/checker_tests.rs
CHANGED
|
@@ -14,12 +14,14 @@ fn emptyCtx() -> CheckCtx<'static> {
|
|
|
14
14
|
static ENUM_PARAMS: OnceLock<plum_checker::EnumParams> = OnceLock::new();
|
|
15
15
|
static MIN_REQUIRED: OnceLock<plum_checker::MinRequiredArgs> = OnceLock::new();
|
|
16
16
|
static PRIM_GENERICS: OnceLock<plum_checker::PrimitiveGenerics> = OnceLock::new();
|
|
17
|
+
static STATICS: OnceLock<plum_checker::StaticMethods> = OnceLock::new();
|
|
17
18
|
CheckCtx {
|
|
18
19
|
methods: METHODS.get_or_init(Default::default),
|
|
19
20
|
enum_variants: ENUM_VARIANTS.get_or_init(Default::default),
|
|
20
21
|
enum_params: ENUM_PARAMS.get_or_init(Default::default),
|
|
21
22
|
min_required: MIN_REQUIRED.get_or_init(Default::default),
|
|
22
23
|
primitive_generics: PRIM_GENERICS.get_or_init(Default::default),
|
|
24
|
+
statics: Some(STATICS.get_or_init(Default::default)),
|
|
23
25
|
}
|
|
24
26
|
}
|
|
25
27
|
|
|
@@ -463,7 +465,7 @@ fn rejects_basic_expressions_and_calls() {
|
|
|
463
465
|
r("fun bad() -> Int =\n false\n", &["return type mismatch"]),
|
|
464
466
|
r("fun bad() -> Int =\n x\n", &["undefined name 'x'"]),
|
|
465
467
|
r("fun bad() -> Int =\n 1 + 2.0\n", &["binary op: type mismatch"]),
|
|
466
|
-
r("fun bad() -> Int =\n self\n", &["
|
|
468
|
+
r("fun bad() -> Int =\n self\n", &["`self` is only available in a method"]),
|
|
467
469
|
r("fun f() -> Int =\n nope\n", &["undefined name 'nope'"]),
|
|
468
470
|
r("fun f() -> Int =\n crash 5\n", &["Str"]),
|
|
469
471
|
r("fun f(c: Bool) -> Int =\n if c then 1 else \"no\"\n", &["inline if branches"]),
|
|
@@ -478,13 +480,16 @@ fn rejects_basic_expressions_and_calls() {
|
|
|
478
480
|
r("fun f(a: Int?) -> Bool =\n a == 3\n", &["cannot compare"]),
|
|
479
481
|
r("fun f(a: error{Bad}!Int) -> Bool =\n a == 3\n", &["cannot compare"]),
|
|
480
482
|
// enum/class fields and constructors
|
|
481
|
-
r(&format!("{cat}fun getAge() -> Int =\n self.age\n"), &["
|
|
483
|
+
r(&format!("{cat}fun getAge() -> Int =\n self.age\n"), &["`self` is only available in a method"]),
|
|
484
|
+
// a `fun` without `self` is a static: called on the type, never on a value
|
|
482
|
-
r("
|
|
485
|
+
r("record Cat(age: Int) =\n fun getAge() -> Int =\n self.age\n", &["`self` is only available in a method"]),
|
|
486
|
+
r("record Cat(age: Int) =\n fun make() -> Cat =\n Cat(age: 1)\n\nfun f() -> Cat =\n Cat(age: 2).make()\n", &["'make' is a static function of Cat", "Cat.make(...)"]),
|
|
487
|
+
r("record Cat(age: Int) =\n fun getAge(self) -> Int =\n self.age\n\nfun f() -> Int =\n Cat.getAge()\n", &["'getAge' is a method of Cat", "call it on a value"]),
|
|
483
488
|
r("enum Cat =\n | Cat(name: Str)\n\n fun getAge(self) -> Int =\n self.age\n", &["no field 'age' on type 'Cat'"]),
|
|
484
489
|
r(&format!("{cat}fun makeCat() -> Cat =\n Cat(name: \"x\", age: \"y\")\n"), &["variant 'Cat' field 'age': type mismatch"]),
|
|
485
490
|
r("enum Cat =\n | Cat(name: Str)\n\nfun makeCat() -> Cat =\n Cat(name: \"x\", age: 1)\n", &["unknown field 'age' on variant 'Cat'"]),
|
|
486
|
-
r(&format!("{cat} fun breakCat() =\n self.age = \"oops\"\n"), &["assign 'self.age': type mismatch"]),
|
|
491
|
+
r(&format!("{cat} fun breakCat(self) =\n self.age = \"oops\"\n"), &["assign 'self.age': type mismatch"]),
|
|
487
|
-
r(&format!("{cat} fun breakCat() =\n self.nope = 1\n"), &["no field 'nope' on type 'Cat'"]),
|
|
492
|
+
r(&format!("{cat} fun breakCat(self) =\n self.nope = 1\n"), &["no field 'nope' on type 'Cat'"]),
|
|
488
493
|
r("enum Shape =\n | Square(x: Int, y: Int)\n\nfun bad() -> Shape =\n Square(x: 1)\n", &["variant 'Square' missing field 'y'"]),
|
|
489
494
|
r("enum Shape =\n | Square(x: Int, y: Int)\n\nfun bad() -> Shape =\n Square(x: 1, y: 2, z: 3)\n", &["unknown field 'z' on variant 'Square'"]),
|
|
490
495
|
r("enum Shape =\n | Circle(radius: Int)\n | Square(radius: Int)\n\nfun bad() -> Int =\n r := Circle(radius: 5)\n return r.radius\n", &["field 'radius' is ambiguous across multiple variants of enum 'Shape'"]),
|
|
@@ -546,7 +551,7 @@ fn rejects_bad_generics_and_duck_typed_calls() {
|
|
|
546
551
|
r("fun wrong(x: T) -> Int =\n \"hello\"\n\nfun useIt() -> Str =\n wrong(\"s\")\n", &["in wrong[Str]"]),
|
|
547
552
|
r("fun identity(value: T) -> T =\n value\n\nfun a() -> Str =\n identity(5)\n", &["expected Str, found Int"]),
|
|
548
553
|
r("fun f(x: T) -> Int =\n x\n\nfun main() -> Int =\n f(\"s\")\n", &["in f[Str]"]),
|
|
549
|
-
r("enum Box[T] =\n | Box(value: T)\n\n fun get() -> Int =\n self.value\n\nfun main() -> Int =\n Box(value: \"s\").get()\n", &["in Box.get[Str]"]),
|
|
554
|
+
r("enum Box[T] =\n | Box(value: T)\n\n fun get(self) -> Int =\n self.value\n\nfun main() -> Int =\n Box(value: \"s\").get()\n", &["in Box.get[Str]"]),
|
|
550
555
|
r(&generic("fun f() -> List[Box[Str]] =\n List(items: Box(value: 1))\n"), &["variant 'Box' field 'value': type mismatch"]),
|
|
551
556
|
r(&generic("fun f() -> List[Str] =\n List(items: 1).map(|x| x + 1)\n"), &["method 'List.map' arg 0: type mismatch"]),
|
|
552
557
|
r(&generic("fun f(xs: List[Int]) -> List[Int] =\n xs.map(|n| \"{n}\")\n"), &["type mismatch"]),
|
plum-examples/structs_records.plum
CHANGED
|
@@ -83,10 +83,10 @@ fun bump(v: Vec2) -> Float =
|
|
|
83
83
|
|
|
84
84
|
record Point(x: Int, y: Int)
|
|
85
85
|
|
|
86
|
-
# `self`
|
|
86
|
+
# A method takes `self` first; a `fun` without `self` is a static (`Vec2.zero()`).
|
|
87
87
|
record Cat(name: Str, age: Int) =
|
|
88
88
|
|
|
89
|
-
fun getAge() -> Int =
|
|
89
|
+
fun getAge(self) -> Int =
|
|
90
90
|
self.age
|
|
91
91
|
|
|
92
92
|
fun birthday(self) -> Int =
|
|
@@ -225,7 +225,7 @@ test "record construction is named or positional"
|
|
|
225
225
|
assert Point(x: 3, y: 4).y == 4
|
|
226
226
|
assert Point(3, 4).y == 4
|
|
227
227
|
|
|
228
|
-
test "record methods
|
|
228
|
+
test "record methods"
|
|
229
229
|
c := Cat(name: "x", age: 7)
|
|
230
230
|
assert c.getAge() == 7
|
|
231
231
|
assert c.birthday() == 8
|
plum-lsp/src/completion.rs
CHANGED
|
@@ -326,6 +326,10 @@ pub fn memberItems(
|
|
|
326
326
|
if !owners.contains(r) || (r != &recv_name && items.iter().any(|i| i.label == *name)) {
|
|
327
327
|
continue;
|
|
328
328
|
}
|
|
329
|
+
// A static (no `self`) is reached as `Type.f()`, not on a value.
|
|
330
|
+
if ctx.statics.is_some_and(|st| st.contains(&(r.clone(), name.clone()))) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
329
333
|
let detail = match fn_ty {
|
|
330
334
|
PlumType::TFun(params, ret) => Some(format!(
|
|
331
335
|
"({}) -> {:?}",
|
plum-lsp/src/main.rs
CHANGED
|
@@ -330,12 +330,14 @@ fn memberCompletionFor(
|
|
|
330
330
|
plum_checker::buildGlobalTables(&merged);
|
|
331
331
|
let min_required = plum_checker::buildMinRequiredArgs(&merged);
|
|
332
332
|
let primitive_generics = plum_checker::buildPrimitiveGenerics(&merged);
|
|
333
|
+
let statics = plum_checker::buildStaticMethods(&merged);
|
|
333
334
|
let ctx = plum_checker::CheckCtx {
|
|
334
335
|
methods: &methods,
|
|
335
336
|
enum_variants: &enum_variants,
|
|
336
337
|
enum_params: &enum_params,
|
|
337
338
|
min_required: &min_required,
|
|
338
339
|
primitive_generics: &primitive_generics,
|
|
340
|
+
statics: Some(&statics),
|
|
339
341
|
};
|
|
340
342
|
let cursor_fn = completion::enclosingFn(&own_source, cursor_line);
|
|
341
343
|
completion::memberItems(cursor_fn, receiver, &global_env, &ctx, &merged.stars)
|
plum-wasm-codegen/src/lib.rs
CHANGED
|
@@ -1370,6 +1370,7 @@ fn checkCtxOf<'a>(
|
|
|
1370
1370
|
enum_params: ctx_enum_params,
|
|
1371
1371
|
min_required: ctx_min_required,
|
|
1372
1372
|
primitive_generics: ctx_primitive_generics,
|
|
1373
|
+
statics: None,
|
|
1373
1374
|
}
|
|
1374
1375
|
}
|
|
1375
1376
|
|