plum

#treesitter#compiler#wasm

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

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


acc2f1f — Peter John 2026-11-01T20:55:22+05:30
fix: exhaustiveness through union member sub-patterns
plum-checker/src/lib.rs CHANGED
@@ -2654,10 +2654,24 @@ fn coverRows<'a>(
2654
2654
  continue;
2655
2655
  }
2656
2656
  if c.member {
2657
- if !fs.iter().all(patCatchAll) {
2657
+ if fs.iter().all(patCatchAll) {
2658
+ row.extend(std::iter::repeat(&COVER_WILDCARD).take(c.fields.len()));
2659
+ } else {
2660
+ // Refutable sub-patterns: the hidden variant's one field is
2661
+ // the member type, matched by the user's pattern itself (a
2662
+ // record destructures its own fields) or, for a non-record
2663
+ // member, by its single sub-pattern.
2664
+ let is_record = c.fields.first().and_then(nominalName).is_some_and(|m| {
2665
+ ctx.enum_variants.get(m).is_some_and(|i| i.enum_name == m)
2666
+ });
2667
+ if is_record {
2668
+ row.push(r[0]);
2669
+ } else if fs.len() == 1 {
2670
+ row.push(&fs[0]);
2671
+ } else {
2658
- continue;
2672
+ continue;
2673
+ }
2659
2674
  }
2660
- row.extend(std::iter::repeat(&COVER_WILDCARD).take(c.fields.len()));
2661
2675
  } else if let ast::CasePattern::Name(_) = r[0] {
2662
2676
  // Bare `Circle` against a payload variant: tag check only.
2663
2677
  row.extend(std::iter::repeat(&COVER_WILDCARD).take(c.fields.len()));
plum-checker/tests/unions_pattern_test.rs CHANGED
@@ -107,3 +107,32 @@ fn primitive_member_narrows() {
107
107
  r.unwrap();
108
108
  assert_eq!(p.narrowings.len(), 2);
109
109
  }
110
+
111
+ const NEST: &str = "\
112
+ enum IntOpt = Present | Absent
113
+ record Present(v: Int)
114
+ enum Nested = Wrap | Empty
115
+ record Wrap(v: IntOpt)
116
+ record Point(x: Int, y: Int)
117
+ enum Geo = Located | Nowhere
118
+ record Located(p: Point)
119
+ ";
120
+
121
+ #[test]
122
+ fn refutable_member_subpatterns_exhaustive() {
123
+ let (r, _) = run(&format!("{NEST}\nfun f(n: Nested) -> Int =\n match n\n Wrap(Present(v)) => v\n Wrap(Absent) => -1\n Empty => 0"));
124
+ r.unwrap();
125
+ let (r, _) = run(&format!("{NEST}\nfun f(g: Geo) -> Int =\n match g\n Located(Point(x, y)) => x + y\n Nowhere => 0"));
126
+ r.unwrap();
127
+ }
128
+
129
+ #[test]
130
+ fn refutable_member_subpatterns_missing_case() {
131
+ let (r, _) = run(&format!("{NEST}\nfun f(n: Nested) -> Int =\n match n\n Wrap(Present(v)) => v\n Empty => 0"));
132
+ let e = errs(r);
133
+ assert!(e.contains("non-exhaustive match") && e.contains("Absent"), "{e}");
134
+ assert!(!e.contains("__"), "{e}");
135
+ let (r, _) = run(&format!("{NEST}\nfun f(n: Nested) -> Int =\n match n\n Wrap(Present(v)) => v\n Wrap(Absent) => -1"));
136
+ let e = errs(r);
137
+ assert!(e.contains("missing Empty") && !e.contains("__"), "{e}");
138
+ }
plum-examples/match.plum CHANGED
@@ -54,7 +54,6 @@ fun unwrapNested(n: Nested) -> Int =
54
54
  match n
55
55
  Wrap(Present(v)) => v
56
56
  Wrap(Absent) => -1
57
- Wrap(_) => -2
58
57
  Empty => 0
59
58
 
60
59
  enum Reading[T] = Sample[T] | NoSignal
@@ -72,7 +71,6 @@ fun unwrapGenericBox(b: GenericBox[Reading[Int]]) -> Int =
72
71
  match b
73
72
  GFull(Sample(v)) => v
74
73
  GFull(NoSignal) => -1
75
- GFull(_) => -2
76
74
  GEmpty => 0
77
75
 
78
76
  enum RecOption = RSome | RNone
@@ -156,7 +154,6 @@ enum Shape =
156
154
  match self
157
155
  Circle(Point(x, y)) => x + y
158
156
  Square(Point(x, y)) => x * y
159
- _ => 0
160
157
 
161
158
  record Circle(v: Point)
162
159
  record Square(v: Point)