plum
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 +17 -3
- plum-checker/tests/unions_pattern_test.rs +29 -0
- plum-examples/match.plum +0 -3
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
|
|
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
|
-
|
|
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)
|