plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
8a21edd
— Peter John
2026-11-01T20:31:34+05:30
T4: checker patterns + narrowing
- plum-checker/src/lib.rs +43 -9
- plum-checker/src/unions.rs +148 -0
- plum-checker/tests/unions_pattern_test.rs +109 -0
plum-checker/src/lib.rs
CHANGED
|
@@ -629,7 +629,7 @@ const RETURN_KEY: &str = "$return";
|
|
|
629
629
|
const STAR_KEY: &str = "$star";
|
|
630
630
|
/// `TypeEnv` key prefix marking a name as a top-level function (a local of the
|
|
631
631
|
/// same name shadows it instead of being a redeclaration).
|
|
632
|
-
const GLOBAL_KEY: &str = "$global:";
|
|
632
|
+
pub(crate) const GLOBAL_KEY: &str = "$global:";
|
|
633
633
|
pub fn plumTypeFromAst(ty: &ast::Type) -> PlumType {
|
|
634
634
|
if ty.name == ast::ERROR_UNION_TYPE_NAME && !ty.generics.is_empty() {
|
|
635
635
|
return errUnionFromAst(ty, plumTypeFromAst);
|
|
@@ -910,6 +910,8 @@ pub struct EnumVariantInfo {
|
|
|
910
910
|
/// The owning enum's generic parameter names (`[K, V]` for `Map`) —
|
|
911
911
|
/// empty for a non-generic enum and for non-generic uses.
|
|
912
912
|
pub generics: Vec<String>,
|
|
913
|
+
/// The owning declaration is a `struct` (copy semantics).
|
|
914
|
+
pub is_value: bool,
|
|
913
915
|
}
|
|
914
916
|
/// Enum variant name -> its info, e.g. `"Red" -> { enum_name: "Color", tag: 1, field_types: [] }`.
|
|
915
917
|
pub type EnumVariants = BTreeMap<String, EnumVariantInfo>;
|
|
@@ -1002,6 +1004,7 @@ pub fn buildGlobalTables(source: &ast::Source) -> (TypeEnv, MethodEnv, EnumVaria
|
|
|
1002
1004
|
field_names,
|
|
1003
1005
|
values: v.values.clone(),
|
|
1004
1006
|
generics: enumGenericParams(e),
|
|
1007
|
+
is_value: e.isValue,
|
|
1005
1008
|
},
|
|
1006
1009
|
);
|
|
1007
1010
|
}
|
|
@@ -2243,6 +2246,10 @@ fn checkStmtInner(
|
|
|
2243
2246
|
}
|
|
2244
2247
|
ast::AssignTarget::Field(object, field_name) => {
|
|
2245
2248
|
let label = format!("{}.{}", describeTargetObject(object), field_name);
|
|
2249
|
+
if let Some(msg) = unions::copyWriteError(object, env) {
|
|
2250
|
+
errors.push(CheckError::at(fn_pos.0, fn_pos.1, format!("fn '{}': assign '{}': {}", fn_name, label, msg)));
|
|
2251
|
+
continue;
|
|
2252
|
+
}
|
|
2246
2253
|
let field_expected = inferExpr(object, env, ctx)
|
|
2247
2254
|
.ok()
|
|
2248
2255
|
.and_then(|o| fieldTypeOf(&o, field_name, ctx).ok());
|
|
@@ -2451,6 +2458,8 @@ enum CoverFail {
|
|
|
2451
2458
|
struct CoverCtor {
|
|
2452
2459
|
name: String,
|
|
2453
2460
|
fields: Vec<PlumType>,
|
|
2461
|
+
/// A union member: covered by `Name` or `Name(..)` with irrefutable sub-patterns.
|
|
2462
|
+
member: bool,
|
|
2454
2463
|
}
|
|
2455
2464
|
|
|
2456
2465
|
static COVER_WILDCARD: ast::CasePattern = ast::CasePattern::Wildcard;
|
|
@@ -2486,6 +2495,7 @@ fn coverCtors(ty: &PlumType, ctx: &CheckCtx) -> Option<(String, Vec<CoverCtor>)>
|
|
|
2486
2495
|
.map(|n| CoverCtor {
|
|
2487
2496
|
name: n.to_string(),
|
|
2488
2497
|
fields: Vec::new(),
|
|
2498
|
+
member: false,
|
|
2489
2499
|
})
|
|
2490
2500
|
.collect(),
|
|
2491
2501
|
)),
|
|
@@ -2496,6 +2506,7 @@ fn coverCtors(ty: &PlumType, ctx: &CheckCtx) -> Option<(String, Vec<CoverCtor>)>
|
|
|
2496
2506
|
.map(|n| CoverCtor {
|
|
2497
2507
|
name: n.clone(),
|
|
2498
2508
|
fields: Vec::new(),
|
|
2509
|
+
member: false,
|
|
2499
2510
|
})
|
|
2500
2511
|
.collect(),
|
|
2501
2512
|
)),
|
|
@@ -2512,7 +2523,10 @@ fn coverCtors(ty: &PlumType, ctx: &CheckCtx) -> Option<(String, Vec<CoverCtor>)>
|
|
|
2512
2523
|
Some((
|
|
2513
2524
|
enum_name.to_string(),
|
|
2514
2525
|
vs.into_iter()
|
|
2526
|
+
.map(|(k, i)| match unions::memberBase(k, i) {
|
|
2527
|
+
Some(base) => CoverCtor { name: base, fields: Vec::new(), member: true },
|
|
2515
|
-
|
|
2528
|
+
None => CoverCtor {
|
|
2529
|
+
member: false,
|
|
2516
2530
|
name: k.to_string(),
|
|
2517
2531
|
// A discriminant enum's field types are its shared
|
|
2518
2532
|
// params, which patterns never destructure.
|
|
@@ -2521,6 +2535,7 @@ fn coverCtors(ty: &PlumType, ctx: &CheckCtx) -> Option<(String, Vec<CoverCtor>)>
|
|
|
2521
2535
|
} else {
|
|
2522
2536
|
variantFields(i, ty)
|
|
2523
2537
|
},
|
|
2538
|
+
},
|
|
2524
2539
|
})
|
|
2525
2540
|
.collect(),
|
|
2526
2541
|
))
|
|
@@ -2572,7 +2587,11 @@ fn coverRows<'a>(
|
|
|
2572
2587
|
if n != c.name {
|
|
2573
2588
|
continue;
|
|
2574
2589
|
}
|
|
2590
|
+
if c.member {
|
|
2591
|
+
if !fs.iter().all(patCatchAll) {
|
|
2592
|
+
continue;
|
|
2593
|
+
}
|
|
2575
|
-
if let ast::CasePattern::Name(_) = r[0] {
|
|
2594
|
+
} else if let ast::CasePattern::Name(_) = r[0] {
|
|
2576
2595
|
// Bare `Circle` against a payload variant: tag check only.
|
|
2577
2596
|
row.extend(std::iter::repeat(&COVER_WILDCARD).take(c.fields.len()));
|
|
2578
2597
|
} else if fs.len() == c.fields.len() {
|
|
@@ -2661,12 +2680,19 @@ fn checkMatch(
|
|
|
2661
2680
|
let mut case_env = env.clone();
|
|
2662
2681
|
if case.patterns.len() == subject_types.len() {
|
|
2663
2682
|
for (pat, sty) in case.patterns.iter().zip(subject_types.iter()) {
|
|
2683
|
+
let narrow = if case.patterns.len() == 1 {
|
|
2684
|
+
unions::narrowTarget(&m.subjects[0], pat, sty, env, ctx)
|
|
2685
|
+
} else {
|
|
2686
|
+
None
|
|
2687
|
+
};
|
|
2664
2688
|
if let Err(msg) = checkPattern(pat, sty, &mut case_env, ctx) {
|
|
2665
2689
|
errors.push(CheckError::at(
|
|
2666
2690
|
fn_pos.0,
|
|
2667
2691
|
fn_pos.1,
|
|
2668
2692
|
format!("fn '{}': match case: {}", fn_name, msg),
|
|
2669
2693
|
));
|
|
2694
|
+
} else {
|
|
2695
|
+
unions::applyNarrow(narrow, case, &mut case_env, ctx);
|
|
2670
2696
|
}
|
|
2671
2697
|
}
|
|
2672
2698
|
} else {
|
|
@@ -2878,7 +2904,7 @@ fn inferClassCallRaw(
|
|
|
2878
2904
|
/// Checks a single case pattern against the type of the subject it matches, binding any
|
|
2879
2905
|
/// new names it introduces into `env`. Constructor-payload sub-patterns (`Next(x)`) bind
|
|
2880
2906
|
/// against that variant's declared field types (see `EnumVariantInfo::field_types`).
|
|
2881
|
-
fn checkPattern(
|
|
2907
|
+
pub(crate) fn checkPattern(
|
|
2882
2908
|
pat: &ast::CasePattern,
|
|
2883
2909
|
subject_ty: &PlumType,
|
|
2884
2910
|
env: &mut TypeEnv,
|
|
@@ -2897,6 +2923,9 @@ fn checkPattern(
|
|
|
2897
2923
|
}
|
|
2898
2924
|
}
|
|
2899
2925
|
}
|
|
2926
|
+
if let Some(r) = unions::checkMemberPattern(pat, subject_ty, env, ctx) {
|
|
2927
|
+
return r;
|
|
2928
|
+
}
|
|
2900
2929
|
match pat {
|
|
2901
2930
|
ast::CasePattern::Wildcard => Ok(()),
|
|
2902
2931
|
ast::CasePattern::Int(_) => unify(subject_ty, &PlumType::TInt, ctx),
|
|
@@ -3088,10 +3117,11 @@ pub(crate) fn lookupFieldType(
|
|
|
3088
3117
|
// codegen will catch a genuine mismatch, exactly as it always has).
|
|
3089
3118
|
// Distinguish by whether any variant belonging to this enum name
|
|
3090
3119
|
// declares named fields at all.
|
|
3120
|
+
[] if unions::isUnion(class_name, ctx)
|
|
3091
|
-
|
|
3121
|
+
|| ctx
|
|
3092
|
-
|
|
3122
|
+
.enum_variants
|
|
3093
|
-
|
|
3123
|
+
.values()
|
|
3094
|
-
|
|
3124
|
+
.any(|info| info.enum_name == class_name && !info.field_names.is_empty()) =>
|
|
3095
3125
|
{
|
|
3096
3126
|
Err(format!(
|
|
3097
3127
|
"no field '{}' on type '{}'",
|
|
@@ -3510,7 +3540,11 @@ fn bindCondEnv(cond: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<TypeEn
|
|
|
3510
3540
|
};
|
|
3511
3541
|
benv.insert(n.clone(), TypeScheme::mono(t));
|
|
3512
3542
|
}
|
|
3543
|
+
pat => {
|
|
3544
|
+
let narrow = unions::narrowTarget(&b.value, pat, &ty, env, ctx);
|
|
3513
|
-
|
|
3545
|
+
checkPattern(pat, &ty, &mut benv, ctx)?;
|
|
3546
|
+
unions::applyNarrow(narrow, &b.case, &mut benv, ctx);
|
|
3547
|
+
}
|
|
3514
3548
|
}
|
|
3515
3549
|
if let Some(g) = &b.case.guard {
|
|
3516
3550
|
let gt = inferExpr(g, &benv, ctx)?;
|
plum-checker/src/unions.rs
CHANGED
|
@@ -46,6 +46,11 @@ fn members(enumName: &str, args: &[PlumType], ctx: &CheckCtx) -> Vec<(String, Pl
|
|
|
46
46
|
.collect()
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/// Whether `enumName` has any type members.
|
|
50
|
+
pub fn isUnion(enumName: &str, ctx: &CheckCtx) -> bool {
|
|
51
|
+
!members(enumName, &[], ctx).is_empty()
|
|
52
|
+
}
|
|
53
|
+
|
|
49
54
|
/// The hidden variant that converts `actual` into the union enum `expected`.
|
|
50
55
|
pub fn wrapVariantFor(expected: &PlumType, actual: &PlumType, ctx: &CheckCtx) -> Option<String> {
|
|
51
56
|
if IN_WRAP.with(|c| c.get()) {
|
|
@@ -130,3 +135,146 @@ pub fn joinBranches(
|
|
|
130
135
|
}
|
|
131
136
|
Some(Ok(e))
|
|
132
137
|
}
|
|
138
|
+
|
|
139
|
+
// ---- patterns and narrowing ------------------------------------------------
|
|
140
|
+
|
|
141
|
+
use crate::types::{TypeEnv, TypeScheme};
|
|
142
|
+
use crate::{checkPattern, EnumVariantInfo};
|
|
143
|
+
|
|
144
|
+
thread_local! {
|
|
145
|
+
/// Address of a top-level pattern that narrows (it records a narrowing, not a pattern).
|
|
146
|
+
static NARROW_TOP: Cell<usize> = const { Cell::new(0) };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const COPY_KEY: &str = "$union-copy:";
|
|
150
|
+
|
|
151
|
+
/// The member base name of a hidden variant (`Shape__Circle` -> `Circle`).
|
|
152
|
+
pub fn memberBase(variant: &str, info: &EnumVariantInfo) -> Option<String> {
|
|
153
|
+
if info.field_types.len() != 1 || !info.field_names.is_empty() {
|
|
154
|
+
return None;
|
|
155
|
+
}
|
|
156
|
+
variant
|
|
157
|
+
.strip_prefix(&format!("{}__", info.enum_name))
|
|
158
|
+
.map(|b| b.to_string())
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// `(hidden variant, member type)` of the member of `subject`'s enum named `name`.
|
|
162
|
+
fn memberNamed(subject: &PlumType, name: &str, ctx: &CheckCtx) -> Option<(String, PlumType)> {
|
|
163
|
+
let (en, args) = enumOf(subject)?;
|
|
164
|
+
let prefix = format!("{en}__");
|
|
165
|
+
members(en, args, ctx)
|
|
166
|
+
.into_iter()
|
|
167
|
+
.find(|(v, _)| v.strip_prefix(&prefix) == Some(name))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/// Checks a pattern that names a member of the subject's union enum. `None`
|
|
171
|
+
/// when it does not (the ordinary rules apply).
|
|
172
|
+
pub fn checkMemberPattern(
|
|
173
|
+
pat: &ast::CasePattern,
|
|
174
|
+
subject: &PlumType,
|
|
175
|
+
env: &mut TypeEnv,
|
|
176
|
+
ctx: &CheckCtx,
|
|
177
|
+
) -> Option<Result<(), String>> {
|
|
178
|
+
let addr = pat as *const ast::CasePattern as usize;
|
|
179
|
+
match pat {
|
|
180
|
+
ast::CasePattern::Name(n) if n.chars().next().is_some_and(|c| c.is_uppercase()) => {
|
|
181
|
+
let (v, _) = memberNamed(subject, n, ctx)?;
|
|
182
|
+
if NARROW_TOP.with(|c| c.get()) != addr {
|
|
183
|
+
plan::recordPattern(addr, v);
|
|
184
|
+
}
|
|
185
|
+
Some(Ok(()))
|
|
186
|
+
}
|
|
187
|
+
ast::CasePattern::Class { name, fields } => {
|
|
188
|
+
let (v, mt) = memberNamed(subject, name, ctx)?;
|
|
189
|
+
plan::recordPattern(addr, v);
|
|
190
|
+
let tys: Vec<PlumType> = match enumOf(&mt) {
|
|
191
|
+
Some((m, _)) => match ctx.enum_variants.get(m) {
|
|
192
|
+
Some(info) if info.enum_name == m => crate::variantFields(info, &mt),
|
|
193
|
+
_ => vec![mt.clone()],
|
|
194
|
+
},
|
|
195
|
+
None => vec![mt.clone()],
|
|
196
|
+
};
|
|
197
|
+
if fields.len() != tys.len() {
|
|
198
|
+
return Some(Err(format!(
|
|
199
|
+
"constructor pattern '{}' expects {} field(s), got {}",
|
|
200
|
+
name,
|
|
201
|
+
tys.len(),
|
|
202
|
+
fields.len()
|
|
203
|
+
)));
|
|
204
|
+
}
|
|
205
|
+
for (f, t) in fields.iter().zip(tys.iter()) {
|
|
206
|
+
if let Err(e) = checkPattern(f, t, env, ctx) {
|
|
207
|
+
return Some(Err(e));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
Some(Ok(()))
|
|
211
|
+
}
|
|
212
|
+
_ => None,
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
pub struct Narrow {
|
|
217
|
+
subject: String,
|
|
218
|
+
variant: String,
|
|
219
|
+
ty: PlumType,
|
|
220
|
+
enumName: String,
|
|
221
|
+
pat: usize,
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/// A bare member pattern over a local/param/`self` subject narrows it. Marks
|
|
225
|
+
/// the pattern so `checkMemberPattern` does not also record it.
|
|
226
|
+
pub fn narrowTarget(
|
|
227
|
+
subject: &ast::Expr,
|
|
228
|
+
pat: &ast::CasePattern,
|
|
229
|
+
subject_ty: &PlumType,
|
|
230
|
+
env: &TypeEnv,
|
|
231
|
+
ctx: &CheckCtx,
|
|
232
|
+
) -> Option<Narrow> {
|
|
233
|
+
let ast::CasePattern::Name(n) = pat else { return None };
|
|
234
|
+
let name = match subject {
|
|
235
|
+
ast::Expr::Var(v) if env.contains_key(v) && !env.contains_key(&format!("{}{}", crate::GLOBAL_KEY, v)) => v.clone(),
|
|
236
|
+
ast::Expr::Self_ => "self".to_string(),
|
|
237
|
+
_ => return None,
|
|
238
|
+
};
|
|
239
|
+
let (variant, ty) = memberNamed(subject_ty, n, ctx)?;
|
|
240
|
+
let addr = pat as *const ast::CasePattern as usize;
|
|
241
|
+
NARROW_TOP.with(|c| c.set(addr));
|
|
242
|
+
Some(Narrow { subject: name, variant, ty, enumName: enumOf(subject_ty)?.0.to_string(), pat: addr })
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/// After a pattern check: records the narrowing and retypes the subject.
|
|
246
|
+
pub fn applyNarrow(n: Option<Narrow>, case: &ast::Case, env: &mut TypeEnv, ctx: &CheckCtx) {
|
|
247
|
+
let Some(n) = n else { return };
|
|
248
|
+
NARROW_TOP.with(|c| {
|
|
249
|
+
if c.get() == n.pat {
|
|
250
|
+
c.set(0)
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
plan::recordNarrow(case as *const ast::Case as usize, n.subject.clone(), n.variant.clone());
|
|
254
|
+
let isStruct = enumOf(&n.ty)
|
|
255
|
+
.and_then(|(m, _)| ctx.enum_variants.get(m))
|
|
256
|
+
.is_some_and(|i| i.is_value);
|
|
257
|
+
env.insert(n.subject.clone(), TypeScheme::mono(n.ty.clone()));
|
|
258
|
+
let key = format!("{COPY_KEY}{}", n.subject);
|
|
259
|
+
if isStruct {
|
|
260
|
+
let member = enumOf(&n.ty).map(|x| x.0).unwrap_or("");
|
|
261
|
+
env.insert(key, TypeScheme::mono(PlumType::TNamed(format!("{member}|{}", n.enumName))));
|
|
262
|
+
} else {
|
|
263
|
+
env.remove(&key);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/// The error for `subject.field = ..` where `subject` is a narrowed struct copy.
|
|
268
|
+
pub fn copyWriteError(object: &ast::Expr, env: &TypeEnv) -> Option<String> {
|
|
269
|
+
let name = match object {
|
|
270
|
+
ast::Expr::Var(v) => v.as_str(),
|
|
271
|
+
ast::Expr::Self_ => "self",
|
|
272
|
+
_ => return None,
|
|
273
|
+
};
|
|
274
|
+
let s = env.get(&format!("{COPY_KEY}{name}"))?;
|
|
275
|
+
let PlumType::TNamed(t) = &*s.body else { return None };
|
|
276
|
+
let (member, en) = t.split_once('|')?;
|
|
277
|
+
Some(format!(
|
|
278
|
+
"{name} is a copy of the {member} inside {en}; assign it to a local to change it"
|
|
279
|
+
))
|
|
280
|
+
}
|
plum-checker/tests/unions_pattern_test.rs
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#![allow(non_snake_case)]
|
|
2
|
+
use plum_checker::checkSource;
|
|
3
|
+
use plum_core::unions::{beginPlan, resolveMembers, takePlan, UnionPlan};
|
|
4
|
+
use plum_core::AstParser;
|
|
5
|
+
|
|
6
|
+
const DEFS: &str = "\
|
|
7
|
+
struct Circle(r: Float)
|
|
8
|
+
struct Rectangle(w: Float, h: Float)
|
|
9
|
+
record Pill(len: Float)
|
|
10
|
+
enum Shape = Circle | Rectangle
|
|
11
|
+
enum Cap = Pill | Circle
|
|
12
|
+
enum Jv = JvNull | Float | Int
|
|
13
|
+
";
|
|
14
|
+
|
|
15
|
+
fn run(body: &str) -> (Result<(), Vec<String>>, UnionPlan) {
|
|
16
|
+
let src = format!("{DEFS}\n{body}\n");
|
|
17
|
+
let mut parser = tree_sitter::Parser::new();
|
|
18
|
+
parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap();
|
|
19
|
+
let tree = parser.parse(&src, None).unwrap();
|
|
20
|
+
assert!(!tree.root_node().has_error(), "parse errors:\n{src}");
|
|
21
|
+
let mut source = AstParser::new(&src).parseSource(tree.root_node());
|
|
22
|
+
let errs = resolveMembers(&mut source);
|
|
23
|
+
assert!(errs.is_empty(), "{errs:?}");
|
|
24
|
+
beginPlan();
|
|
25
|
+
let r = checkSource(&source).map_err(|e| e.into_iter().map(|x| x.message).collect());
|
|
26
|
+
(r, takePlan())
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fn errs(r: Result<(), Vec<String>>) -> String {
|
|
30
|
+
r.unwrap_err().join(" | ")
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#[test]
|
|
34
|
+
fn destructure_member() {
|
|
35
|
+
let (r, p) = run("fun f(s: Shape) -> Float =\n match s\n Circle(r) => r\n Rectangle(w, h) => w * h");
|
|
36
|
+
r.unwrap();
|
|
37
|
+
let mut v: Vec<_> = p.patterns.values().cloned().collect();
|
|
38
|
+
v.sort();
|
|
39
|
+
assert_eq!(v, vec!["Shape__Circle", "Shape__Rectangle"]);
|
|
40
|
+
assert!(p.narrowings.is_empty());
|
|
41
|
+
let (r, _) = run("fun take(i: Int) -> Int =\n i\n\nfun f(s: Shape) -> Int =\n match s\n Circle(r) => take(r)\n Rectangle(w, h) => 1");
|
|
42
|
+
assert!(r.is_err(), "r is a Float, not an Int");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#[test]
|
|
46
|
+
fn narrow_match() {
|
|
47
|
+
let (r, p) = run("fun f(s: Shape) -> Float =\n match s\n Circle => s.r\n Rectangle => s.w");
|
|
48
|
+
r.unwrap();
|
|
49
|
+
assert_eq!(p.narrowings.len(), 2);
|
|
50
|
+
let mut v: Vec<_> = p.narrowings.values().map(|x| x.1.clone()).collect();
|
|
51
|
+
v.sort();
|
|
52
|
+
assert_eq!(v, vec!["Shape__Circle", "Shape__Rectangle"]);
|
|
53
|
+
assert!(p.narrowings.values().all(|x| x.0 == "s"));
|
|
54
|
+
// after the match, s is a Shape again
|
|
55
|
+
let (r, _) = run("fun f(s: Shape) -> Float =\n match s\n Circle => 1.0\n Rectangle => 2.0\n s.r");
|
|
56
|
+
assert!(r.is_err());
|
|
57
|
+
// outside its arm s has no w
|
|
58
|
+
let (r, _) = run("fun f(s: Shape) -> Float =\n match s\n Circle => s.w\n Rectangle => 2.0");
|
|
59
|
+
assert!(r.is_err());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#[test]
|
|
63
|
+
fn narrow_struct_write_error() {
|
|
64
|
+
let (r, _) = run("fun f(s: Shape) -> Int =\n match s\n Circle =>\n s.r = 1.0\n Rectangle => 0\n 0");
|
|
65
|
+
assert!(
|
|
66
|
+
errs(r).contains("s is a copy of the Circle inside Shape; assign it to a local to change it")
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#[test]
|
|
71
|
+
fn narrow_record_write_ok() {
|
|
72
|
+
let (r, _) = run("fun f(s: Cap) -> Int =\n match s\n Pill =>\n s.len = 1.0\n Circle => 0\n 0");
|
|
73
|
+
r.unwrap();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#[test]
|
|
77
|
+
fn exhaustive_members() {
|
|
78
|
+
let (r, _) = run("fun f(s: Shape) -> Float =\n match s\n Circle => 1.0\n Rectangle => 2.0");
|
|
79
|
+
r.unwrap();
|
|
80
|
+
let (r, _) = run("fun f(s: Shape) -> Float =\n match s\n Circle => 1.0");
|
|
81
|
+
let e = errs(r);
|
|
82
|
+
assert!(e.contains("non-exhaustive match on Shape: missing Rectangle"), "{e}");
|
|
83
|
+
let (r, _) = run("fun f(s: Jv) -> Int =\n match s\n JvNull => 0\n Float => 1\n Int => 2");
|
|
84
|
+
r.unwrap();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#[test]
|
|
88
|
+
fn expr_subject_no_narrow() {
|
|
89
|
+
let (r, p) = run("fun g() -> Shape =\n Circle(r: 1.0)\n\nfun f() -> Int =\n match g()\n Circle => 1\n Rectangle => 2");
|
|
90
|
+
r.unwrap();
|
|
91
|
+
assert!(p.narrowings.is_empty());
|
|
92
|
+
assert_eq!(p.patterns.len(), 2);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#[test]
|
|
96
|
+
fn pattern_if() {
|
|
97
|
+
let (r, p) = run("fun f(s: Shape) -> Float =\n if Circle := s\n return s.r\n 0.0");
|
|
98
|
+
r.unwrap();
|
|
99
|
+
assert_eq!(p.narrowings.len(), 1);
|
|
100
|
+
let (r, _) = run("fun f(s: Shape) -> Float =\n if Circle := s\n return 1.0\n s.r");
|
|
101
|
+
assert!(r.is_err());
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#[test]
|
|
105
|
+
fn primitive_member_narrows() {
|
|
106
|
+
let (r, p) = run("fun f(j: Jv) -> Float =\n match j\n Float => j + 1.0\n Int => 0.0\n JvNull => 0.0");
|
|
107
|
+
r.unwrap();
|
|
108
|
+
assert_eq!(p.narrowings.len(), 2);
|
|
109
|
+
}
|