plum

#treesitter#compiler#wasm

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

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


ef6dc10 — Peter John 2026-11-01T20:34:39+05:30
T5 coerce test + T6 review fixes: narrowing pending scoped per && operand and per item, for-clause bind shadows
plum-checker/tests/unions_coerce_test.rs CHANGED
@@ -97,7 +97,6 @@ fn member_alone_is_its_own_type() {
97
97
  }
98
98
 
99
99
  #[test]
100
- #[ignore = "review: unify nested arm in lib.rs:834 accepts member<:enum inside TApp/TFun args; need depth guard so wrapVariantFor applies only at top level"]
101
100
  fn invariant_list_rejected() {
102
101
  let (r, _) = run("fun take(xs: List[Shape]) -> Int =\n 1\n\nfun go(cs: List[Circle]) -> Int =\n take(cs)");
103
102
  assert!(r.is_err(), "List[Circle] must not pass as List[Shape]");
plum-core/src/union_lower.rs CHANGED
@@ -10,6 +10,7 @@ use std::collections::{HashMap, HashSet};
10
10
  pub fn applyPlan(source: &mut Source, plan: &UnionPlan) {
11
11
  let mut lw = Lowerer { plan, pending: Vec::new(), needs: Vec::new() };
12
12
  for item in &mut source.items {
13
+ lw.pending.clear();
13
14
  match item {
14
15
  Item::Fn(f) => match &mut f.body {
15
16
  FnBody::Expr(e) => lw.expr(e),
@@ -186,11 +187,10 @@ impl<'a> Lowerer<'a> {
186
187
  }
187
188
  Expr::Unary(u) => self.expr(&mut u.operand),
188
189
  Expr::Bool(b) => {
189
- self.expr(&mut b.left);
190
190
  let mark = self.pending.len();
191
- let _ = mark;
191
+ self.expr(&mut b.left);
192
192
  // Narrowings found on the left are visible on the right of `&&`.
193
- let leftPairs: Vec<(String, String)> = self.pending.clone();
193
+ let leftPairs: Vec<(String, String)> = self.pending[mark..].to_vec();
194
194
  self.expr(&mut b.right);
195
195
  if b.op == BoolOp::And && !leftPairs.is_empty() {
196
196
  renameExpr(&mut b.right, &leftPairs);
@@ -349,7 +349,9 @@ fn rnStmt(s: &mut Stmt, from: &str, to: &str) {
349
349
  ForClause::If(e) | ForClause::Bind(_, e) => rnExpr(e, from, to),
350
350
  }
351
351
  }
352
- if !f.binders.iter().any(|b| b == from) {
352
+ let shadowed = f.binders.iter().any(|b| b == from)
353
+ || f.clauses.iter().any(|c| matches!(c, ForClause::Bind(n, _) if n == from));
354
+ if !shadowed {
353
355
  rnBlock(&mut f.body, from, to);
354
356
  }
355
357
  }
plum-core/tests/union_lower_test.rs CHANGED
@@ -327,3 +327,34 @@ fn dispatch_substitutes_member_generics() {
327
327
  assert_eq!(f.params[0].ty, ParamType::Type(int.clone()));
328
328
  assert_eq!(f.returns, Some(int));
329
329
  }
330
+
331
+ #[test]
332
+ fn narrow_does_not_leak_into_later_items() {
333
+ let mut s = parse(
334
+ "fun a(s: Shape) =\n if Circle := s\n 1\nfun b(s: Shape, t: Bool) =\n if t && s.w\n s.w\n",
335
+ );
336
+ // move the bind condition into an expression-bodied fn
337
+ let Item::Fn(fa) = &mut s.items[0] else { panic!() };
338
+ let FnBody::Block(blk) = &mut fa.body else { panic!() };
339
+ let Stmt::If(i) = blk.stmts.remove(0) else { panic!() };
340
+ fa.body = FnBody::Expr(i.condition);
341
+ let FnBody::Expr(Expr::Bind(bd)) = &fa.body else { panic!("bind") };
342
+ let mut plan = UnionPlan::default();
343
+ plan.narrowings.insert(addr(&bd.case), ("s".into(), "Shape__Circle".into(), "s__n1".into()));
344
+ applyPlan(&mut s, &plan);
345
+ let Stmt::If(i) = &stmts(&s, "b")[0] else { panic!() };
346
+ let Expr::Bool(bo) = &i.condition else { panic!() };
347
+ assert_eq!(fieldObj(&bo.right), "s", "pending narrowing leaked across items");
348
+ }
349
+
350
+ #[test]
351
+ fn narrow_for_clause_bind_shadows() {
352
+ let mut s = parse(
353
+ "fun f(s: Shape, xs: List[Int]) =\n match s\n Circle =>\n for s := range xs\n s.r\n",
354
+ );
355
+ let plan = narrowPlan(&s, "f", "s", "Shape__Circle", "s__n1");
356
+ applyPlan(&mut s, &plan);
357
+ let m = matchOf(&stmts(&s, "f")[0]);
358
+ let Stmt::ForLoop(fl) = &m.cases[0].body.stmts[0] else { panic!() };
359
+ assert_eq!(fieldObj(exprOf(&fl.body.stmts[0])), "s");
360
+ }