plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


a3ab768 — Peter John 2026-10-25T16:25:50+05:30
fix(monomorphize): resolve a closure's own generic from cb's declared return
plum-checker/src/monomorphize.rs CHANGED
@@ -643,6 +643,14 @@ struct Monomorphizer<'a> {
643
643
  /// the top of every `rewriteFnBody` call; never needs saving/restoring since
644
644
  /// closures aren't recursed into by this pass (see `rewriteExpr`'s `Closure` arm).
645
645
  current_return_type: Option<ast::Type>,
646
+ /// True while `current_return_type` holds a closure argument's declared
647
+ /// return (see `rewriteMethodClosureArgs`), which may still contain the
648
+ /// method's own unresolved generic letters.
649
+ return_type_is_closure_expected: bool,
650
+ /// The enclosing real function's return type while
651
+ /// `return_type_is_closure_expected` is set (fallback for letters the
652
+ /// closure's expected return leaves unresolved).
653
+ closure_outer_return_type: Option<ast::Type>,
646
654
  /// (receiver name or `None` for a free function, function/method name) ->
647
655
  /// (the generic enum its declared return type names, the concrete type args
648
656
  /// it names them with) — for every function/method whose OWN declared return
@@ -772,6 +780,7 @@ impl<'a> Monomorphizer<'a> {
772
780
  /// the body's construction site has just been specialized to a mangled name.
773
781
  fn rewriteFnBody(&mut self, f: &mut ast::Fn, resolve_return: bool) -> Result<(), String> {
774
782
  self.current_return_type = f.returns.clone();
783
+ self.return_type_is_closure_expected = false;
775
784
  self.local_types_by_name.clear();
776
785
  let mut env = self.global_env.clone();
777
786
  if let Some(recv) = &f.type_param {
@@ -1329,6 +1338,23 @@ impl<'a> Monomorphizer<'a> {
1329
1338
  },
1330
1339
  None => Vec::new(),
1331
1340
  };
1341
+ // The closure's own declared return, with the receiver's bindings
1342
+ // substituted (`andThen`'s `cb: fn(T) -> Try[U, E]` on a
1343
+ // `Try[Int, [Bad]]` receiver -> `Try[U, [Bad]]`). A bare `Ok(n * 2)`
1344
+ // in the closure body can only pin `T`, never `E`; without this it
1345
+ // fell back to the ENCLOSING function's return type (wrong scope)
1346
+ // and stayed unmangled, so `resolveMethodOwnGenerics` couldn't
1347
+ // recover `U`. Only a non-bare-letter return is used (a bare
1348
+ // `fn(T) -> U` says nothing more than the outer fallback did).
1349
+ let closure_expected_ret: Option<ast::Type> = match &method_and_bindings {
1350
+ Some((method, class_bindings)) => match method.params.get(i).map(|p| &p.ty) {
1351
+ Some(ast::ParamType::Fn(_, Some(ret))) if !ret.generics.is_empty() => {
1352
+ Some(substituteType(ret, &Substitution(class_bindings.clone())))
1353
+ }
1354
+ _ => None,
1355
+ },
1356
+ None => None,
1357
+ };
1332
1358
  let mut closure_env = env.clone();
1333
1359
  for (p, ty) in cl.params.iter().zip(closure_param_tys.iter()) {
1334
1360
  closure_env.insert(p.clone(), TypeScheme::mono(ty.clone()));
@@ -1338,7 +1364,23 @@ impl<'a> Monomorphizer<'a> {
1338
1364
  .entry(p.clone())
1339
1365
  .or_insert_with(|| TypeScheme::mono(PlumType::TVar("_".to_string())));
1340
1366
  }
1367
+ if let Some(expected) = closure_expected_ret {
1368
+ let saved_ret = self.current_return_type.replace(expected);
1369
+ let saved_flag = std::mem::replace(&mut self.return_type_is_closure_expected, true);
1370
+ // Nested closures keep the outermost real function's return.
1371
+ let saved_outer = if saved_flag {
1372
+ self.closure_outer_return_type.clone()
1373
+ } else {
1374
+ std::mem::replace(&mut self.closure_outer_return_type, saved_ret.clone())
1375
+ };
1376
+ let result = self.rewriteBlock(&mut cl.body, &mut closure_env);
1377
+ self.current_return_type = saved_ret;
1378
+ self.return_type_is_closure_expected = saved_flag;
1379
+ self.closure_outer_return_type = saved_outer;
1380
+ result?;
1381
+ } else {
1341
- self.rewriteBlock(&mut cl.body, &mut closure_env)?;
1382
+ self.rewriteBlock(&mut cl.body, &mut closure_env)?;
1383
+ }
1342
1384
  }
1343
1385
  Ok(())
1344
1386
  }
@@ -2036,6 +2078,30 @@ impl<'a> Monomorphizer<'a> {
2036
2078
  if let Some(rt) = self.current_return_type.clone() {
2037
2079
  if rt.name == e.name && rt.generics.len() == params.len() {
2038
2080
  for (p, gt) in params.iter().zip(rt.generics.iter()) {
2081
+ // A closure's expected return can still name the
2082
+ // method's own not-yet-inferred letter (`U` in
2083
+ // `Try[U, [Bad]]`) — that's what this site is about
2084
+ // to determine, not a binding to copy.
2085
+ // Fall back to the enclosing function's return (the
2086
+ // pre-closure-expected behavior), so `return
2087
+ // x.andThen(|n| Err(Bad))` in a `-> Try[Int, [Bad]]`
2088
+ // function still pins `T`.
2089
+ if self.return_type_is_closure_expected
2090
+ && gt.generics.is_empty()
2091
+ && isGenericParamName(&gt.name)
2092
+ {
2093
+ if bindings.contains_key(p) {
2094
+ continue;
2095
+ }
2096
+ if let Some(outer) = self.closure_outer_return_type.clone() {
2097
+ if outer.name == e.name && outer.generics.len() == params.len() {
2098
+ let og = &outer.generics[params.iter().position(|q| q == p).unwrap()];
2099
+ let resolved = self.resolveFieldType(og);
2100
+ bindings.insert(p.clone(), crate::plumTypeFromAst(&resolved));
2101
+ }
2102
+ }
2103
+ continue;
2104
+ }
2039
2105
  if !bindings.contains_key(p) {
2040
2106
  // See the matching comment in
2041
2107
  // `resolveClassInstantiation` — `gt` needs
@@ -2686,6 +2752,8 @@ pub fn monomorphizeSource(source: &ast::Source) -> Result<ast::Source, String> {
2686
2752
  worklist: Vec::new(),
2687
2753
  produced: Vec::new(),
2688
2754
  current_return_type: None,
2755
+ return_type_is_closure_expected: false,
2756
+ closure_outer_return_type: None,
2689
2757
  fn_return_generic_enum: BTreeMap::new(),
2690
2758
  local_types_by_name: BTreeMap::new(),
2691
2759
  rename_counter: 0,
plum-checker/tests/checker_tests.rs CHANGED
@@ -1680,3 +1680,30 @@ fun f() -> Try[Int, [Red]] =
1680
1680
  );
1681
1681
  assert!(bad.is_err());
1682
1682
  }
1683
+
1684
+ #[test]
1685
+ fn andThenClosureReturningBareOkResolvesOwnGeneric() {
1686
+ // `cb: fn(T) -> Try[U, E]`: a bare `Ok(n * 2)` only pins `T`; `E` must
1687
+ // come from the receiver via the closure's declared return, or `U` is lost.
1688
+ let src = "\
1689
+ enum Try[T, E] =
1690
+ | Ok(T)
1691
+ | Err(E)
1692
+
1693
+ fun andThen(self, cb: fn(T) -> Try[U, E]) -> Try[U, E] =
1694
+ match self
1695
+ Ok(v) =>
1696
+ return cb(v)
1697
+ Err(e) =>
1698
+ return Err(e)
1699
+
1700
+ fun five() -> Try[Int, [Bad]] =
1701
+ return Ok(5)
1702
+
1703
+ fun main() =
1704
+ r := five().andThen(|n| Ok(n * 2))
1705
+ ";
1706
+ let source = parseClean(src);
1707
+ let mono = plum_checker::monomorphize::monomorphizeSource(&source);
1708
+ assert!(mono.is_ok(), "expected Ok, got {:?}", mono.err());
1709
+ }
plum-std/Try.plum CHANGED
@@ -104,6 +104,9 @@ fun mergeForTryTest(s: Str, i: Int) -> Try[Int, [BadNumStr, OutOfBounds]] =
104
104
  x := xs.get(i)?
105
105
  return Ok(n + x)
106
106
 
107
+ fun andThenBareErrForTryTest() -> Try[Int, [Bad]] =
108
+ return okIntForTryTest().andThen(|n| Err(Bad))
109
+
107
110
  fun closureMapForTryTest(s: Str) -> Try[Int, [InvalidFirst]] =
108
111
  n := parseInt(s) ? |e| InvalidFirst
109
112
  return Ok(n)
@@ -121,6 +124,11 @@ test "andThen chains fallible steps"
121
124
  assert a.unwrap() == 1
122
125
  assert errIntForTryTest().andThen(|n| fifthForTryTest(n)).isErr()
123
126
 
127
+ test "andThen with closure returning bare Ok"
128
+ assert okIntForTryTest().andThen(|n| Ok(n * 2)).unwrap() == 10
129
+ assert errIntForTryTest().andThen(|n| Ok(n * 2)).isErr()
130
+ assert andThenBareErrForTryTest().isErr()
131
+
124
132
  test "unwrapOr and unwrapOrElse fall back on Err"
125
133
  assert errIntForTryTest().unwrapOr(9) == 9
126
134
  assert okIntForTryTest().unwrapOr(9) == 5