plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
9fbf75a
— Peter John
2026-10-25T19:14:57+05:30
fix(checker): reject generic T returned as a concrete type; resolve typedSource vars
- plum-checker/src/lib.rs +43 -20
- plum-checker/tests/checker_tests.rs +60 -0
- plum-examples/functions.plum +5 -5
plum-checker/src/lib.rs
CHANGED
|
@@ -14,6 +14,7 @@ thread_local! {
|
|
|
14
14
|
/// Counter for the fresh `_t<N>` inference variables minted per generic call site.
|
|
15
15
|
static FRESH: Cell<u32> = const { Cell::new(0) };
|
|
16
16
|
/// When set, `inferExprWith` records every expression's type here (see `typedSource`).
|
|
17
|
+
static RECORD_BINDS: RefCell<Option<Binds>> = const { RefCell::new(None) };
|
|
17
18
|
static RECORD: RefCell<Option<HashMap<usize, PlumType>>> = const { RefCell::new(None) };
|
|
18
19
|
}
|
|
19
20
|
|
|
@@ -68,6 +69,24 @@ fn substVars(ty: &PlumType, b: &Binds) -> PlumType {
|
|
|
68
69
|
go(ty, b, 0)
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
/// Replaces every remaining unresolved inference variable (`_`, `_tN`,
|
|
73
|
+
/// `_closure_x`) by `Unit`.
|
|
74
|
+
fn defaultVars(ty: &PlumType) -> PlumType {
|
|
75
|
+
use PlumType::*;
|
|
76
|
+
match ty {
|
|
77
|
+
TVar(_) => TUnit,
|
|
78
|
+
TApp(n, args) => TApp(n.clone(), args.iter().map(defaultVars).collect()),
|
|
79
|
+
TFun(ps, r) => TFun(ps.iter().map(defaultVars).collect(), Box::new(defaultVars(r))),
|
|
80
|
+
TVariadic(i) => TVariadic(Box::new(defaultVars(i))),
|
|
81
|
+
TTags(tags) => TTags(
|
|
82
|
+
tags.iter()
|
|
83
|
+
.map(|(k, v)| (k.clone(), v.as_ref().map(|t| Box::new(defaultVars(t)))))
|
|
84
|
+
.collect(),
|
|
85
|
+
),
|
|
86
|
+
_ => ty.clone(),
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
71
90
|
/// Free generic parameter names in `ty`, first-appearance order.
|
|
72
91
|
fn freeTParams(ty: &PlumType, out: &mut Vec<String>) {
|
|
73
92
|
use PlumType::*;
|
|
@@ -117,6 +136,7 @@ fn bindMatch(pattern: &PlumType, actual: &PlumType, b: &mut Binds) {
|
|
|
117
136
|
match b.get(n).cloned() {
|
|
118
137
|
None => {
|
|
119
138
|
b.insert(n.clone(), a.clone());
|
|
139
|
+
recordBind(n, a);
|
|
120
140
|
}
|
|
121
141
|
// A variable standing for a whole tag union widens to cover all
|
|
122
142
|
// the unions seen for it (E is covariant).
|
|
@@ -126,6 +146,7 @@ fn bindMatch(pattern: &PlumType, actual: &PlumType, b: &mut Binds) {
|
|
|
126
146
|
for (k, v) in new {
|
|
127
147
|
merged.entry(k.clone()).or_insert_with(|| v.clone());
|
|
128
148
|
}
|
|
149
|
+
recordBind(n, &TTags(merged.clone()));
|
|
129
150
|
b.insert(n.clone(), TTags(merged));
|
|
130
151
|
}
|
|
131
152
|
Some(bound) => bindMatch(&bound, a, b),
|
|
@@ -248,6 +269,16 @@ fn freshOwner(generics: &[String]) -> BTreeMap<String, PlumType> {
|
|
|
248
269
|
generics.iter().map(|g| (g.clone(), freshVar())).collect()
|
|
249
270
|
}
|
|
250
271
|
|
|
272
|
+
/// Remembers a variable binding made at any call site (variable names are
|
|
273
|
+
/// globally unique) so `typedSource` can resolve recorded types at the end.
|
|
274
|
+
fn recordBind(name: &str, ty: &PlumType) {
|
|
275
|
+
RECORD_BINDS.with(|r| {
|
|
276
|
+
if let Some(m) = r.borrow_mut().as_mut() {
|
|
277
|
+
m.insert(name.to_string(), ty.clone());
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
251
282
|
fn recordType(expr: &ast::Expr, ty: &PlumType) {
|
|
252
283
|
RECORD.with(|r| {
|
|
253
284
|
if let Some(m) = r.borrow_mut().as_mut() {
|
|
@@ -563,22 +594,6 @@ pub fn unify(t1: &PlumType, t2: &PlumType, ctx: &CheckCtx) -> Result<(), String>
|
|
|
563
594
|
}
|
|
564
595
|
}
|
|
565
596
|
|
|
566
|
-
/// Return-position check: a value still typed by a bare generic parameter is
|
|
567
|
-
/// accepted for a concrete declared return (`fun get() -> Int = self.value` in
|
|
568
|
-
/// a `Box[T]` is only ever instantiated at `Int` by existing code), everything
|
|
569
|
-
/// else goes through `unifyArg`.
|
|
570
|
-
fn unifyReturn(
|
|
571
|
-
declared: &PlumType,
|
|
572
|
-
actual: &PlumType,
|
|
573
|
-
actual_expr: &ast::Expr,
|
|
574
|
-
ctx: &CheckCtx,
|
|
575
|
-
) -> Result<(), String> {
|
|
576
|
-
if matches!(actual, PlumType::TParam(_)) && !matches!(declared, PlumType::TParam(_)) {
|
|
577
|
-
return Ok(());
|
|
578
|
-
}
|
|
579
|
-
unifyArg(declared, actual, actual_expr, ctx)
|
|
580
|
-
}
|
|
581
|
-
|
|
582
597
|
/// True if `expr` (looking through parens) is a direct construction/reference
|
|
583
598
|
/// of exactly the enum variant named `variant_name` — a bare payload-free
|
|
584
599
|
/// reference (`Red`), a positional-payload construction (`Circle(1)`), or a
|
|
@@ -1008,8 +1023,16 @@ pub fn checkSource(source: &ast::Source) -> CheckResult<()> {
|
|
|
1008
1023
|
/// instantiated type of every expression (keyed by expression address) for codegen.
|
|
1009
1024
|
pub fn typedSource(source: &ast::Source) -> CheckResult<TypedInfo> {
|
|
1010
1025
|
RECORD.with(|r| *r.borrow_mut() = Some(HashMap::new()));
|
|
1026
|
+
RECORD_BINDS.with(|r| *r.borrow_mut() = Some(Binds::new()));
|
|
1011
1027
|
let result = checkSourceInner(source);
|
|
1012
|
-
let recorded = RECORD.with(|r| r.borrow_mut().take()).unwrap_or_default();
|
|
1028
|
+
let mut recorded = RECORD.with(|r| r.borrow_mut().take()).unwrap_or_default();
|
|
1029
|
+
let binds = RECORD_BINDS.with(|r| r.borrow_mut().take()).unwrap_or_default();
|
|
1030
|
+
// Zonk: apply the final substitution; a variable nothing ever constrained
|
|
1031
|
+
// (an unused `None`, an empty literal, ...) is defaulted to `Unit`, since
|
|
1032
|
+
// no value of it can ever be observed.
|
|
1033
|
+
for t in recorded.values_mut() {
|
|
1034
|
+
*t = defaultVars(&substVars(t, &binds));
|
|
1035
|
+
}
|
|
1013
1036
|
result.map(|()| TypedInfo {
|
|
1014
1037
|
expr_types: recorded,
|
|
1015
1038
|
})
|
|
@@ -1232,7 +1255,7 @@ fn checkFn(
|
|
|
1232
1255
|
match &f.body {
|
|
1233
1256
|
ast::FnBody::Expr(e) => match inferExprWith(e, &env, ctx, Some(&declared_ret)) {
|
|
1234
1257
|
Ok(t) => {
|
|
1235
|
-
if let Err(msg) =
|
|
1258
|
+
if let Err(msg) = unifyArg(&declared_ret, &t, e, ctx) {
|
|
1236
1259
|
errors.push(CheckError::at(
|
|
1237
1260
|
f.line,
|
|
1238
1261
|
f.column,
|
|
@@ -1267,7 +1290,7 @@ fn checkFn(
|
|
|
1267
1290
|
if let Some((last_expr, line, column)) = tail {
|
|
1268
1291
|
match inferExprWith(last_expr, &env, ctx, Some(&declared_ret)) {
|
|
1269
1292
|
Ok(t) => {
|
|
1270
|
-
if let Err(msg) =
|
|
1293
|
+
if let Err(msg) = unifyArg(&declared_ret, &t, last_expr, ctx) {
|
|
1271
1294
|
errors.push(CheckError::at(
|
|
1272
1295
|
line,
|
|
1273
1296
|
column,
|
|
@@ -1406,7 +1429,7 @@ fn checkStmt(
|
|
|
1406
1429
|
}
|
|
1407
1430
|
ast::Stmt::Return(Some(e)) => match inferExprWith(e, env, ctx, Some(declared_ret)) {
|
|
1408
1431
|
Ok(t) => {
|
|
1409
|
-
if let Err(msg) =
|
|
1432
|
+
if let Err(msg) = unifyArg(declared_ret, &t, e, ctx) {
|
|
1410
1433
|
errors.push(CheckError::at(
|
|
1411
1434
|
fn_pos.0,
|
|
1412
1435
|
fn_pos.1,
|
plum-checker/tests/checker_tests.rs
CHANGED
|
@@ -2164,3 +2164,63 @@ fun b() -> Str =
|
|
|
2164
2164
|
let bad = "fun identity(value: T) -> T =\n value\n\nfun a() -> Str =\n identity(5)\n";
|
|
2165
2165
|
assert!(checkSource(&parseClean(bad)).is_err());
|
|
2166
2166
|
}
|
|
2167
|
+
|
|
2168
|
+
#[test]
|
|
2169
|
+
fn generic_return_of_param_rejected_for_concrete_declared_return() {
|
|
2170
|
+
// Under erasure a `T` value must never be accepted where `Int` is declared.
|
|
2171
|
+
assert!(checkSource(&parseClean("fun f(x: T) -> Int =\n x\n")).is_err());
|
|
2172
|
+
let method = "enum Box[T] =\n | Box(value: T)\n\n fun get() -> Int =\n self.value\n";
|
|
2173
|
+
assert!(checkSource(&parseClean(method)).is_err());
|
|
2174
|
+
assert!(checkSource(&parseClean("fun f(x: T) -> T =\n x\n")).is_ok());
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
fn hasInferVar(t: &PlumType) -> bool {
|
|
2178
|
+
match t {
|
|
2179
|
+
PlumType::TVar(_) => true,
|
|
2180
|
+
PlumType::TApp(_, a) => a.iter().any(hasInferVar),
|
|
2181
|
+
PlumType::TFun(ps, r) => ps.iter().any(hasInferVar) || hasInferVar(r),
|
|
2182
|
+
PlumType::TVariadic(i) => hasInferVar(i),
|
|
2183
|
+
PlumType::TTags(m) => m.values().flatten().any(|t| hasInferVar(t)),
|
|
2184
|
+
_ => false,
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
#[test]
|
|
2189
|
+
fn typed_source_has_no_unresolved_infer_vars() {
|
|
2190
|
+
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
|
|
2191
|
+
let lib = root.clone();
|
|
2192
|
+
let mut bad = Vec::new();
|
|
2193
|
+
for dir in ["plum-std", "plum-examples"] {
|
|
2194
|
+
for entry in std::fs::read_dir(root.join(dir)).unwrap() {
|
|
2195
|
+
let path = entry.unwrap().path();
|
|
2196
|
+
if path.extension().and_then(|e| e.to_str()) != Some("plum") {
|
|
2197
|
+
continue;
|
|
2198
|
+
}
|
|
2199
|
+
let ast = plum_core::loadAndMerge(&path, &lib).unwrap();
|
|
2200
|
+
let info = match typedSource(&ast) {
|
|
2201
|
+
Ok(i) => i,
|
|
2202
|
+
Err(e) => {
|
|
2203
|
+
bad.push(format!("{}: check failed: {}", path.display(), e[0].message));
|
|
2204
|
+
continue;
|
|
2205
|
+
}
|
|
2206
|
+
};
|
|
2207
|
+
let n = info.expr_types.values().filter(|t| hasInferVar(t)).count();
|
|
2208
|
+
if n > 0 {
|
|
2209
|
+
bad.push(format!("{}: {} unresolved", path.display(), n));
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
assert!(bad.is_empty(), "{:#?}", bad);
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
#[test]
|
|
2217
|
+
fn probe_nested_generic_substitution() {
|
|
2218
|
+
let ok = "fun f() -> List[Box[Int]] =\n List(items: Box(value: 1))\n";
|
|
2219
|
+
assert!(checkSource(&parseClean(&format!("{}{}", GENERIC_DEFS, ok))).is_ok());
|
|
2220
|
+
let bad = "fun f() -> List[Box[Str]] =\n List(items: Box(value: 1))\n";
|
|
2221
|
+
assert!(checkSource(&parseClean(&format!("{}{}", GENERIC_DEFS, bad))).is_err());
|
|
2222
|
+
let m = "fun f() -> List[Str] =\n List(items: 1).map(|x| x + 1)\n";
|
|
2223
|
+
assert!(checkSource(&parseClean(&format!("{}{}", GENERIC_DEFS, m))).is_err());
|
|
2224
|
+
let m2 = "fun f() -> List[Int] =\n List(items: 1).map(|x| x + 1)\n";
|
|
2225
|
+
assert!(checkSource(&parseClean(&format!("{}{}", GENERIC_DEFS, m2))).is_ok());
|
|
2226
|
+
}
|
plum-examples/functions.plum
CHANGED
|
@@ -46,7 +46,7 @@ fun double(n: Int) -> Int =
|
|
|
46
46
|
enum GenericBox[T] =
|
|
47
47
|
| GenericBox(value: T)
|
|
48
48
|
|
|
49
|
-
fun getIntValue() ->
|
|
49
|
+
fun getIntValue() -> T =
|
|
50
50
|
self.value
|
|
51
51
|
|
|
52
52
|
fun useGenericBox() -> Int =
|
|
@@ -59,10 +59,10 @@ fun identity(value: T) -> T =
|
|
|
59
59
|
enum GenericBox2[T] =
|
|
60
60
|
| GenericBox2(value: T)
|
|
61
61
|
|
|
62
|
-
fun getValue() ->
|
|
62
|
+
fun getValue() -> T =
|
|
63
63
|
self.value
|
|
64
64
|
|
|
65
|
-
fun doubled(value:
|
|
65
|
+
fun doubled(value: Int) -> Int =
|
|
66
66
|
identity(value) + identity(value)
|
|
67
67
|
|
|
68
68
|
enum Parcel[T] =
|
|
@@ -90,10 +90,10 @@ fun strContentsOr(p: Parcel, default: Int) -> Int =
|
|
|
90
90
|
enum GenericBox3[T] =
|
|
91
91
|
| GenericBox3(value: T)
|
|
92
92
|
|
|
93
|
-
fun getBoxValue() ->
|
|
93
|
+
fun getBoxValue() -> T =
|
|
94
94
|
self.value
|
|
95
95
|
|
|
96
|
-
fun sumBox(b: GenericBox3) -> Int =
|
|
96
|
+
fun sumBox(b: GenericBox3[Int]) -> Int =
|
|
97
97
|
b.getBoxValue()
|
|
98
98
|
|
|
99
99
|
fun combineVariadic(prefix: Int, rest: ...Int) -> Int =
|