plum

#treesitter#compiler#wasm

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 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) = unifyReturn(&declared_ret, &t, e, ctx) {
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) = unifyReturn(&declared_ret, &t, last_expr, ctx) {
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) = unifyReturn(declared_ret, &t, e, ctx) {
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() -> Int =
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() -> Int =
62
+ fun getValue() -> T =
63
63
  self.value
64
64
 
65
- fun doubled(value: T) -> Int =
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() -> Int =
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 =