plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
a9d88ec
— Peter John
2026-11-01T23:13:23+05:30
test: dedupe checker Rust tests (246 -> 61), rejection cases as table tests (146 rows)
- plum-checker/tests/annotated_decl_test.rs +0 -113
- plum-checker/tests/checker_tests.rs +184 -1746
- plum-checker/tests/error_union_tests.rs +45 -382
- plum-checker/tests/examples_test.rs +0 -48
- plum-checker/tests/primitive_generics_test.rs +0 -11
- plum-checker/tests/unions_coerce_test.rs +0 -120
- plum-checker/tests/unions_dispatch_test.rs +0 -150
- plum-checker/tests/unions_pattern_test.rs +0 -138
- plum-checker/tests/unions_tests.rs +330 -0
plum-checker/tests/annotated_decl_test.rs
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
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
|
-
record Circle(r: Float)
|
|
8
|
-
record Square(s: Float)
|
|
9
|
-
record Oval(a: Float)
|
|
10
|
-
enum Shape = Circle | Square
|
|
11
|
-
enum Round = Circle | Oval
|
|
12
|
-
enum Json = Float | Str
|
|
13
|
-
enum Tree[T] = Node[T] | Leaf
|
|
14
|
-
record Node[T](v: T)
|
|
15
|
-
";
|
|
16
|
-
|
|
17
|
-
fn run(body: &str) -> (Result<(), Vec<String>>, UnionPlan) {
|
|
18
|
-
let src = format!("{DEFS}\n{body}\n");
|
|
19
|
-
let mut parser = tree_sitter::Parser::new();
|
|
20
|
-
parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap();
|
|
21
|
-
let tree = parser.parse(&src, None).unwrap();
|
|
22
|
-
assert!(!tree.root_node().has_error(), "parse errors:\n{src}");
|
|
23
|
-
let mut source = AstParser::new(&src).parseSource(tree.root_node());
|
|
24
|
-
let errs = resolveMembers(&mut source);
|
|
25
|
-
assert!(errs.is_empty(), "{errs:?}");
|
|
26
|
-
beginPlan();
|
|
27
|
-
let r = checkSource(&source).map_err(|e| e.into_iter().map(|x| x.message).collect());
|
|
28
|
-
(r, takePlan())
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
fn wraps(p: &UnionPlan) -> Vec<String> {
|
|
32
|
-
let mut v: Vec<String> = p.wraps.values().cloned().collect();
|
|
33
|
-
v.sort();
|
|
34
|
-
v
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
fn errs(body: &str) -> Vec<String> {
|
|
39
|
-
run(body).0.unwrap_err()
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
#[test]
|
|
43
|
-
fn annotatedUnionLocalRecordsTheWrap() {
|
|
44
|
-
let (r, p) = run("fun go() -> Int =\n s: Shape := Circle(r: 2.0)\n 1");
|
|
45
|
-
r.unwrap();
|
|
46
|
-
assert_eq!(wraps(&p), vec!["Shape__Circle"]);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
#[test]
|
|
50
|
-
fn annotatedLocalHasTheAnnotationType() {
|
|
51
|
-
// `s` is a `Shape`, so passing it where a `Shape` is expected needs no wrap.
|
|
52
|
-
let (r, p) = run("fun take(s: Shape) -> Int =\n 1\n\nfun go() -> Int =\n s: Shape := Circle(r: 2.0)\n take(s)");
|
|
53
|
-
r.unwrap();
|
|
54
|
-
assert_eq!(wraps(&p), vec!["Shape__Circle"]);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
#[test]
|
|
58
|
-
fn annotatedReassignmentConverts() {
|
|
59
|
-
let (r, p) = run("fun go() -> Int =\n s: Shape := Circle(r: 2.0)\n s = Square(s: 1.0)\n 1");
|
|
60
|
-
r.unwrap();
|
|
61
|
-
assert_eq!(wraps(&p), vec!["Shape__Circle", "Shape__Square"]);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
#[test]
|
|
65
|
-
fn annotatedOptionalAcceptsValueAndNull() {
|
|
66
|
-
let (r, _) = run("fun go() -> Int =\n a: Int? := 3\n b: Int? := null\n 1");
|
|
67
|
-
r.unwrap();
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
#[test]
|
|
71
|
-
fn annotatedListAndFnTypes() {
|
|
72
|
-
let (r, _) = run("fun go() -> Int =\n xs: List[Int] := List[Int]()\n f: fn(Int) -> Int := |n| n + 1\n f(1)");
|
|
73
|
-
r.unwrap();
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
#[test]
|
|
77
|
-
fn annotatedGenericParamInGenericFn() {
|
|
78
|
-
let (r, _) = run("fun id(x: T) -> T =\n y: T := x\n y");
|
|
79
|
-
r.unwrap();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
#[test]
|
|
83
|
-
fn annotatedMismatchUsesTheNormalText() {
|
|
84
|
-
let e = errs("fun go() -> Int =\n s: Int := \"x\"\n 1");
|
|
85
|
-
assert!(e.iter().any(|m| m.contains("assign 's'") && m.contains("mismatch")), "{e:?}");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
#[test]
|
|
89
|
-
fn annotationWithEqualsIsRejected() {
|
|
90
|
-
let e = errs("fun go() -> Int =\n s: Shape = Circle(r: 1.0)\n 1");
|
|
91
|
-
assert!(e.iter().any(|m| m.contains("annotated declarations use :=")), "{e:?}");
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
#[test]
|
|
95
|
-
fn bareAnnotationIsRejected() {
|
|
96
|
-
let e = errs("fun go() -> Int =\n total: Float\n 1");
|
|
97
|
-
assert!(e.iter().any(|m| m.contains("needs a value")), "{e:?}");
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
#[test]
|
|
101
|
-
fn annotatedRedeclarationIsRejected() {
|
|
102
|
-
let e = errs("fun go() -> Int =\n s := 1\n s: Int := 2\n s");
|
|
103
|
-
assert!(e.iter().any(|m| m.contains("already declared")), "{e:?}");
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
#[test]
|
|
107
|
-
fn annotatedFloatTakesAnIntLikeAParamDoes() {
|
|
108
|
-
let (via_param, _) = run("fun take(x: Float) -> Float =\n x\n\nfun go() -> Float =\n take(3)");
|
|
109
|
-
let (via_decl, _) = run("fun go() -> Float =\n n: Float := 3\n n");
|
|
110
|
-
// Today an Int never converts to a Float param; the annotation adds nothing.
|
|
111
|
-
assert!(via_param.is_err());
|
|
112
|
-
assert!(via_decl.is_err());
|
|
113
|
-
}
|
plum-checker/tests/checker_tests.rs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#![allow(non_snake_case)]
|
|
2
2
|
use plum_checker::types::*;
|
|
3
3
|
use plum_checker::{
|
|
4
|
-
checkSource, plumTypeFromAst,
|
|
4
|
+
checkSource, plumTypeFromAst,
|
|
5
5
|
typedSource, unify, CheckCtx,
|
|
6
6
|
};
|
|
7
7
|
use plum_core::ast::Type as AstType;
|
|
@@ -85,961 +85,25 @@ fn monoScheme() {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
#[test]
|
|
88
|
-
fn
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
#[test]
|
|
95
|
-
fn validTestBlockPasses() {
|
|
96
|
-
let src = "\
|
|
97
|
-
fun add(a: Int, b: Int) -> Int =
|
|
98
|
-
a + b
|
|
99
|
-
|
|
100
|
-
test \"add works\"
|
|
101
|
-
assert add(1, 2) == 3
|
|
102
|
-
assert add(2, 2) == 4
|
|
103
|
-
";
|
|
104
|
-
let source = parse(src);
|
|
105
|
-
assert!(checkSource(&source).is_ok(), "expected Ok");
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
#[test]
|
|
109
|
-
fn nonBoolAssertConditionIsError() {
|
|
110
|
-
let src = "\
|
|
111
|
-
test \"bad assert\"
|
|
112
|
-
assert 1 + 2
|
|
113
|
-
";
|
|
114
|
-
let source = parse(src);
|
|
115
|
-
let result = checkSource(&source);
|
|
116
|
-
assert!(result.is_err());
|
|
117
|
-
let errs = result.unwrap_err();
|
|
118
|
-
assert!(
|
|
119
|
-
errs[0].message.contains("assert must be Bool"),
|
|
120
|
-
"got: {}",
|
|
121
|
-
errs[0].message
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
#[test]
|
|
126
|
-
fn wrongReturnTypeIsError() {
|
|
127
|
-
let src = "fun bad() -> Int =\n true\n";
|
|
128
|
-
let source = parse(src);
|
|
129
|
-
let result = checkSource(&source);
|
|
130
|
-
assert!(result.is_err());
|
|
131
|
-
let errs = result.unwrap_err();
|
|
132
|
-
assert!(
|
|
133
|
-
errs[0].message.contains("return type mismatch"),
|
|
134
|
-
"got: {}",
|
|
135
|
-
errs[0].message
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
#[test]
|
|
140
|
-
fn undeclaredVarIsError() {
|
|
141
|
-
let src = "fun bad() -> Int =\n x\n";
|
|
142
|
-
let source = parse(src);
|
|
143
|
-
let result = checkSource(&source);
|
|
144
|
-
assert!(result.is_err());
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
#[test]
|
|
148
|
-
fn typeMismatchInBinaryOpIsError() {
|
|
149
|
-
let src = "fun bad() -> Int =\n 1 + 2.0\n";
|
|
150
|
-
let source = parse(src);
|
|
151
|
-
let result = checkSource(&source);
|
|
152
|
-
assert!(result.is_err());
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
#[test]
|
|
156
|
-
fn boolLiteralTrueFalseAreBool() {
|
|
157
|
-
let src = "fun isTrue() -> Bool =\n true\n";
|
|
158
|
-
let source = parse(src);
|
|
159
|
-
assert!(
|
|
160
|
-
checkSource(&source).is_ok(),
|
|
161
|
-
"expected Ok, got {:?}",
|
|
162
|
-
checkSource(&source).err()
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
#[test]
|
|
167
|
-
fn boolLiteralWrongReturnTypeIsError() {
|
|
168
|
-
let src = "fun bad() -> Int =\n false\n";
|
|
169
|
-
let source = parse(src);
|
|
170
|
-
let result = checkSource(&source);
|
|
171
|
-
assert!(result.is_err());
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
#[test]
|
|
175
|
-
fn methodSelfFieldAccessPasses() {
|
|
176
|
-
let src =
|
|
177
|
-
"enum Cat =\n | Cat(name: Str, age: Int)\n\n fun getName() -> Str =\n self.name\n";
|
|
178
|
-
let source = parse(src);
|
|
179
|
-
let result = checkSource(&source);
|
|
180
|
-
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
#[test]
|
|
184
|
-
fn methodSelfUnknownFieldIsError() {
|
|
185
|
-
let src = "enum Cat =\n | Cat(name: Str)\n\n fun getAge() -> Int =\n self.age\n";
|
|
186
|
-
let source = parse(src);
|
|
187
|
-
let result = checkSource(&source);
|
|
188
|
-
assert!(result.is_err());
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
#[test]
|
|
192
|
-
fn nestedMethodTypeChecks() {
|
|
193
|
-
let src =
|
|
194
|
-
"enum Cat =\n | Cat(name: Str, age: Int)\n\n fun getName(self) -> Str =\n self.name\n";
|
|
195
|
-
let result = checkSource(&parse(src));
|
|
196
|
-
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
#[test]
|
|
200
|
-
fn nestedMethodUnknownFieldIsError() {
|
|
201
|
-
let src = "enum Cat =\n | Cat(name: Str)\n\n fun getAge(self) -> Int =\n self.age\n";
|
|
202
|
-
assert!(checkSource(&parse(src)).is_err());
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
#[test]
|
|
206
|
-
fn selfOutsideMethodIsError() {
|
|
207
|
-
let src = "fun bad() -> Int =\n self\n";
|
|
208
|
-
let source = parse(src);
|
|
209
|
-
let result = checkSource(&source);
|
|
210
|
-
assert!(result.is_err());
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
#[test]
|
|
214
|
-
fn classCallChecksFieldTypes() {
|
|
215
|
-
let src = "enum Cat =\n | Cat(name: Str, age: Int)\n\nfun makeCat() -> Cat =\n Cat(name: \"x\", age: 1)\n";
|
|
216
|
-
let source = parse(src);
|
|
217
|
-
let result = checkSource(&source);
|
|
218
|
-
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
#[test]
|
|
222
|
-
fn classCallWrongFieldTypeIsError() {
|
|
223
|
-
let src = "enum Cat =\n | Cat(name: Str, age: Int)\n\nfun makeCat() -> Cat =\n Cat(name: \"x\", age: \"y\")\n";
|
|
224
|
-
let source = parse(src);
|
|
225
|
-
let result = checkSource(&source);
|
|
226
|
-
assert!(result.is_err());
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
#[test]
|
|
230
|
-
fn classCallUnknownFieldIsError() {
|
|
231
|
-
let src =
|
|
232
|
-
"enum Cat =\n | Cat(name: Str)\n\nfun makeCat() -> Cat =\n Cat(name: \"x\", age: 1)\n";
|
|
233
|
-
let source = parse(src);
|
|
234
|
-
let result = checkSource(&source);
|
|
235
|
-
assert!(result.is_err());
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
#[test]
|
|
239
|
-
fn methodCallViaAttributeTypeChecksArgs() {
|
|
240
|
-
let src = "enum Cat =\n | Cat(name: Str)\n\n fun rename(n: Str) -> Str =\n n\n\nfun use(c: Cat) -> Str =\n c.rename(\"x\")\n";
|
|
241
|
-
let source = parse(src);
|
|
242
|
-
let result = checkSource(&source);
|
|
243
|
-
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
#[test]
|
|
247
|
-
fn matchBindsNamePatternToSubjectType() {
|
|
248
|
-
let src = "fun main(a: Int) -> Int =\n match a\n x =>\n x\n _ =>\n 0\n";
|
|
249
|
-
let source = parse(src);
|
|
250
|
-
let result = checkSource(&source);
|
|
251
|
-
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
#[test]
|
|
255
|
-
fn matchInlineCaseBodyTypeChecks() {
|
|
256
|
-
// Case bodies can be a single inline expression, not just an indented block.
|
|
257
|
-
let src = "fun main(a: Int) -> Int =\n match a\n 1 => 10\n _ => 0\n";
|
|
258
|
-
let source = parse(src);
|
|
259
|
-
let result = checkSource(&source);
|
|
260
|
-
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
#[test]
|
|
264
|
-
fn matchTrueFalseAreVariantPatternsNotBindingsWithoutEnumDecl() {
|
|
265
|
-
// true/false are built-in Bool variants — they must be recognized as tag
|
|
266
|
-
// comparisons even when the source doesn't redeclare `enum Bool`, so a
|
|
267
|
-
// later `_` wildcard arm remains reachable (each pattern binds/compares,
|
|
268
|
-
// it doesn't just re-bind the subject under the name "true").
|
|
269
|
-
let src =
|
|
270
|
-
"fun pick(a: Bool) -> Int =\n match a\n true =>\n 1\n false =>\n 0\n";
|
|
271
|
-
let source = parse(src);
|
|
272
|
-
let result = checkSource(&source);
|
|
273
|
-
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
#[test]
|
|
277
|
-
fn matchIntPatternAgainstStrSubjectIsError() {
|
|
278
|
-
let src = "fun main(a: Str) -> Int =\n match a\n 1 =>\n 1\n _ =>\n 0\n";
|
|
279
|
-
let source = parse(src);
|
|
280
|
-
let result = checkSource(&source);
|
|
281
|
-
assert!(result.is_err());
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
#[test]
|
|
285
|
-
fn bareEnumTagUnifiesWithOwningEnumType() {
|
|
286
|
-
// Regression: a bare non-Bool tag like `Nothing` used to type as `TNamed("Nothing")`
|
|
287
|
-
// (itself, not its enum), so comparing it against a `Maybe` value would wrongly
|
|
288
|
-
// fail with a type mismatch.
|
|
289
|
-
let src = "\
|
|
290
|
-
enum Maybe =
|
|
291
|
-
| Just(Int)
|
|
292
|
-
| Nothing
|
|
293
|
-
|
|
294
|
-
fun isNone(o: Maybe) -> Bool = o == Nothing
|
|
295
|
-
";
|
|
296
|
-
let source = parse(src);
|
|
297
|
-
let result = checkSource(&source);
|
|
298
|
-
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
#[test]
|
|
302
|
-
fn variantConstructionChecksArgCountAndTypes() {
|
|
303
|
-
let src = "\
|
|
304
|
-
enum Maybe =
|
|
305
|
-
| Just(Int)
|
|
306
|
-
| Nothing
|
|
307
|
-
|
|
308
|
-
fun makeJust(v: Int) -> Maybe =
|
|
309
|
-
Just(v)
|
|
310
|
-
";
|
|
311
|
-
let source = parse(src);
|
|
312
|
-
let result = checkSource(&source);
|
|
313
|
-
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
#[test]
|
|
317
|
-
fn variantConstructionWrongArgTypeIsError() {
|
|
318
|
-
let src = "\
|
|
319
|
-
enum Maybe =
|
|
320
|
-
| Just(Int)
|
|
321
|
-
| Nothing
|
|
322
|
-
|
|
323
|
-
fun bad() -> Maybe =
|
|
324
|
-
Just(\"x\")
|
|
325
|
-
";
|
|
326
|
-
let source = parse(src);
|
|
327
|
-
let result = checkSource(&source);
|
|
328
|
-
assert!(result.is_err());
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
#[test]
|
|
332
|
-
fn variantConstructionWrongArgCountIsError() {
|
|
333
|
-
let src = "\
|
|
334
|
-
enum Shape =
|
|
335
|
-
| Rect(Float, Float)
|
|
336
|
-
| Circle(Float)
|
|
337
|
-
|
|
338
|
-
fun bad() -> Shape =
|
|
339
|
-
Rect(1.0)
|
|
340
|
-
";
|
|
341
|
-
let source = parse(src);
|
|
342
|
-
let result = checkSource(&source);
|
|
343
|
-
assert!(result.is_err());
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
#[test]
|
|
347
|
-
fn enumDiscriminantValuesTypeCheckWithNoErrors() {
|
|
348
|
-
let src = "\
|
|
349
|
-
enum Step(n: Int) =
|
|
350
|
-
| ReadMin(0)
|
|
351
|
-
| ReadMax(1)
|
|
352
|
-
";
|
|
353
|
-
let source = parse(src);
|
|
354
|
-
let result = checkSource(&source);
|
|
355
|
-
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
#[test]
|
|
359
|
-
fn enumDiscriminantWrongValueCountIsError() {
|
|
360
|
-
let src = "\
|
|
361
|
-
enum Step(n: Int) =
|
|
362
|
-
| ReadMin(0)
|
|
363
|
-
| ReadMax
|
|
364
|
-
";
|
|
365
|
-
let source = parse(src);
|
|
366
|
-
let result = checkSource(&source);
|
|
367
|
-
assert!(result.is_err());
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
#[test]
|
|
371
|
-
fn enumDiscriminantWronglyTypedValueIsError() {
|
|
372
|
-
let src = "\
|
|
373
|
-
enum Step(n: Int) =
|
|
374
|
-
| ReadMin(0)
|
|
375
|
-
| ReadMax(\"x\")
|
|
376
|
-
";
|
|
377
|
-
let source = parse(src);
|
|
378
|
-
let result = checkSource(&source);
|
|
379
|
-
assert!(result.is_err());
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
#[test]
|
|
383
|
-
fn enumDiscriminantValueOnParamLessEnumIsError() {
|
|
384
|
-
let src = "\
|
|
385
|
-
enum Maybe =
|
|
386
|
-
| Just(5)
|
|
387
|
-
| Nothing
|
|
388
|
-
";
|
|
389
|
-
let source = parse(src);
|
|
390
|
-
let result = checkSource(&source);
|
|
391
|
-
assert!(result.is_err());
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
#[test]
|
|
395
|
-
fn fieldAccessOnDiscriminantEnumReceiverTypeChecks() {
|
|
396
|
-
let src = "\
|
|
397
|
-
enum Step(n: Int) =
|
|
398
|
-
| ReadMin(0)
|
|
399
|
-
| ReadMax(1)
|
|
400
|
-
|
|
401
|
-
fun toNumber(self) -> Int =
|
|
402
|
-
self.n
|
|
403
|
-
";
|
|
404
|
-
let source = parse(src);
|
|
405
|
-
let result = checkSource(&source);
|
|
406
|
-
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
#[test]
|
|
410
|
-
fn fieldAccessOnOrdinaryEnumReceiverIsUnaffectedByEnumParams() {
|
|
411
|
-
// An ordinary enum (no discriminant `params`) isn't in `ctx.classes` OR
|
|
412
|
-
// `ctx.enum_params`, so it falls through to the same permissive "unmodeled
|
|
413
|
-
// type" escape hatch every other type not in `ctx.classes` gets (codegen,
|
|
414
|
-
// not the checker, is what would catch a genuinely bad field access here) —
|
|
415
|
-
// exactly as it did before discriminant enums existed.
|
|
416
|
-
let src = "\
|
|
417
|
-
enum Maybe =
|
|
418
|
-
| Just(Int)
|
|
419
|
-
| Nothing
|
|
420
|
-
|
|
421
|
-
fun bad(self) -> Int =
|
|
422
|
-
self.n
|
|
423
|
-
";
|
|
424
|
-
let source = parse(src);
|
|
425
|
-
let result = checkSource(&source);
|
|
426
|
-
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
#[test]
|
|
430
|
-
fn constructorPatternBindsFieldsToDeclaredTypes() {
|
|
431
|
-
let src = "\
|
|
432
|
-
enum Shape =
|
|
433
|
-
| Rect(Float, Float)
|
|
434
|
-
| Circle(Float)
|
|
435
|
-
|
|
436
|
-
fun area(s: Shape) -> Float =
|
|
437
|
-
match s
|
|
438
|
-
Rect(w, h) =>
|
|
439
|
-
w * h
|
|
440
|
-
Circle(r) =>
|
|
441
|
-
r * r
|
|
442
|
-
";
|
|
443
|
-
let source = parse(src);
|
|
444
|
-
let result = checkSource(&source);
|
|
445
|
-
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
#[test]
|
|
449
|
-
fn constructorPatternWrongFieldCountIsError() {
|
|
450
|
-
let src = "\
|
|
451
|
-
enum Shape =
|
|
452
|
-
| Rect(Float, Float)
|
|
453
|
-
| Circle(Float)
|
|
454
|
-
|
|
455
|
-
fun bad(s: Shape) -> Float =
|
|
456
|
-
match s
|
|
457
|
-
Rect(w) =>
|
|
458
|
-
w
|
|
459
|
-
_ =>
|
|
460
|
-
0.0
|
|
461
|
-
";
|
|
462
|
-
let source = parse(src);
|
|
463
|
-
let result = checkSource(&source);
|
|
464
|
-
assert!(result.is_err());
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
#[test]
|
|
468
|
-
fn bareTypeEnumVariantSugarWrapsTheSameNamedRecordShapedEnum() {
|
|
469
|
-
// `| Cat` (bare, no `[...]`) naming an already-declared record-shaped enum
|
|
470
|
-
// (the `type Cat = ...` replacement) is sugar for wrapping that whole
|
|
471
|
-
// record as the variant's single payload field — shorthand for `| Cat[Cat]`
|
|
472
|
-
// under a separate tag name. This is the DOP-friendly form: a sealed union
|
|
473
|
-
// of plain records, matching Java's "sealed interface implemented directly
|
|
474
|
-
// by records" shape.
|
|
475
|
-
let src = "\
|
|
476
|
-
record Cat(name: Str)
|
|
477
|
-
|
|
478
|
-
enum Animal = Cat | Dog
|
|
479
|
-
|
|
480
|
-
fun catName(a: Animal) -> Str =
|
|
481
|
-
match a
|
|
482
|
-
Cat(n) =>
|
|
483
|
-
n
|
|
484
|
-
Dog =>
|
|
485
|
-
\"unknown\"
|
|
486
|
-
";
|
|
487
|
-
let source = parse(src);
|
|
488
|
-
let result = checkSource(&source);
|
|
489
|
-
assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
#[test]
|
|
493
|
-
fn genericClassInstantiatedAtTwoConcreteTypesTypeChecks() {
|
|
494
|
-
let src = "\
|
|
495
|
-
enum Box[T] =
|
|
496
|
-
| Box(value: T)
|
|
497
|
-
|
|
498
|
-
fun makeIntBox() -> Box[Int] =
|
|
499
|
-
Box(value: 5)
|
|
500
|
-
|
|
501
|
-
fun makeStrBox() -> Box[Str] =
|
|
502
|
-
Box(value: \"x\")
|
|
503
|
-
";
|
|
504
|
-
let source = parse(src);
|
|
505
|
-
let result = checkSource(&source);
|
|
506
|
-
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
#[test]
|
|
510
|
-
fn genericFunctionCalledWithDifferentConcreteTypesPerSiteTypeChecks() {
|
|
511
|
-
let src = "\
|
|
512
|
-
fun wrap(value: T) -> Bool =
|
|
513
|
-
true
|
|
514
|
-
|
|
515
|
-
fun useInt() -> Bool =
|
|
516
|
-
wrap(5)
|
|
517
|
-
|
|
518
|
-
fun useStr() -> Bool =
|
|
519
|
-
wrap(\"x\")
|
|
520
|
-
";
|
|
521
|
-
let source = parse(src);
|
|
522
|
-
let result = checkSource(&source);
|
|
523
|
-
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
#[test]
|
|
527
|
-
fn genericFunctionWithTwoIndependentTypeParamsTypeChecks() {
|
|
528
|
-
let src = "\
|
|
529
|
-
fun pair(first: T, second: U) -> Bool =
|
|
530
|
-
true
|
|
531
|
-
|
|
532
|
-
fun use() -> Bool =
|
|
533
|
-
pair(1, \"x\")
|
|
534
|
-
";
|
|
535
|
-
let source = parse(src);
|
|
536
|
-
let result = checkSource(&source);
|
|
537
|
-
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
#[test]
|
|
541
|
-
fn genericFunctionWithInternallyInconsistentBodyIsRejectedAfterSpecialization() {
|
|
542
|
-
// A generic function whose body is genuinely inconsistent with its concrete,
|
|
543
|
-
// correct declared return type must still be REJECTED after specialization.
|
|
544
|
-
// The `-> Str` on `useIt` means the buggy (unconditional return-overwrite)
|
|
545
|
-
// behavior — rewriting `wrong$Str`'s `-> Int` to `-> Str` — would make the
|
|
546
|
-
// whole program type-check, masking the real `expected Int, found Str` error.
|
|
547
|
-
let src = "\
|
|
548
|
-
fun wrong(x: T) -> Int =
|
|
549
|
-
\"hello\"
|
|
550
|
-
|
|
551
|
-
fun useIt() -> Str =
|
|
552
|
-
wrong(\"s\")
|
|
553
|
-
";
|
|
554
|
-
let source = parse(src);
|
|
555
|
-
let result = checkSource(&source);
|
|
556
|
-
assert!(
|
|
557
|
-
result.is_err(),
|
|
558
|
-
"expected the internally-inconsistent generic function to be rejected, got Ok"
|
|
559
|
-
);
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
#[test]
|
|
563
|
-
fn genericEnumSingleInstantiationTypeChecks() {
|
|
564
|
-
// A generic Maybe-shaped enum, constructed at one concrete type (`Just(5)`),
|
|
565
|
-
// matched, must resolve end-to-end via checkSource.
|
|
566
|
-
let src = "\
|
|
567
|
-
enum Maybe[T] =
|
|
568
|
-
| Just(T)
|
|
569
|
-
| Nothing
|
|
570
|
-
|
|
571
|
-
fun get() -> Int =
|
|
572
|
-
o = Just(5)
|
|
573
|
-
match o
|
|
574
|
-
Just(v) =>
|
|
575
|
-
v
|
|
576
|
-
Nothing =>
|
|
577
|
-
0
|
|
578
|
-
";
|
|
579
|
-
let source = parse(src);
|
|
580
|
-
let result = checkSource(&source);
|
|
581
|
-
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
582
|
-
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
#[test]
|
|
586
|
-
fn genericEnumMultipleInstantiationsCoexistAndTypeCheck() {
|
|
587
|
-
// The SAME generic enum instantiated at two different concrete types in one
|
|
588
|
-
// program must now type-check correctly for BOTH instantiations — this is the
|
|
589
|
-
// behavior this task adds (previously this was a documented, rejected limitation).
|
|
590
|
-
let src = "\
|
|
591
|
-
enum Maybe[T] =
|
|
592
|
-
| Just(T)
|
|
593
|
-
| Nothing
|
|
594
|
-
|
|
595
|
-
fun useInt() -> Int =
|
|
596
|
-
o = Just(5)
|
|
597
|
-
match o
|
|
598
|
-
Just(v) =>
|
|
599
|
-
v
|
|
600
|
-
Nothing =>
|
|
601
|
-
0
|
|
602
|
-
|
|
603
|
-
fun useStr() -> Str =
|
|
604
|
-
o = Just(\"x\")
|
|
605
|
-
match o
|
|
606
|
-
Just(v) =>
|
|
607
|
-
v
|
|
608
|
-
Nothing =>
|
|
609
|
-
\"z\"
|
|
610
|
-
";
|
|
611
|
-
let source = parse(src);
|
|
612
|
-
let result = checkSource(&source);
|
|
613
|
-
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
614
|
-
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
#[test]
|
|
618
|
-
fn genericMethodOnGenericClassTypeChecks() {
|
|
619
|
-
let src = "\
|
|
620
|
-
enum Box[T] =
|
|
621
|
-
| Box(value: T)
|
|
622
|
-
|
|
623
|
-
fun getValue() -> T =
|
|
624
|
-
self.value
|
|
625
|
-
|
|
626
|
-
fun use() -> Int =
|
|
627
|
-
b = Box(value: 5)
|
|
628
|
-
b.getValue()
|
|
629
|
-
";
|
|
630
|
-
let source = parse(src);
|
|
631
|
-
let result = checkSource(&source);
|
|
632
|
-
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
#[test]
|
|
636
|
-
fn ordinaryFunctionWithBareGenericEnumParamTypeChecks() {
|
|
637
|
-
// The shape that broke the pre-existing codegen test: an otherwise-ordinary
|
|
638
|
-
// function taking a bare generic-enum-typed parameter.
|
|
639
|
-
let src = "\
|
|
640
|
-
enum Maybe[T] =
|
|
641
|
-
| Just(T)
|
|
642
|
-
| Nothing
|
|
643
|
-
|
|
644
|
-
fun unwrapOr(o: Maybe[Int], default: Int) -> Int =
|
|
645
|
-
match o
|
|
646
|
-
Just(v) =>
|
|
647
|
-
v
|
|
648
|
-
Nothing =>
|
|
649
|
-
default
|
|
650
|
-
|
|
651
|
-
fun use() -> Int =
|
|
652
|
-
unwrapOr(Just(5), 0)
|
|
653
|
-
";
|
|
654
|
-
let source = parse(src);
|
|
655
|
-
let result = checkSource(&source);
|
|
656
|
-
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
657
|
-
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
#[test]
|
|
661
|
-
fn ordinaryFunctionWithBareGenericClassParamTypeChecks() {
|
|
662
|
-
// The same shape, for a generic CLASS param instead of an enum — untested until
|
|
663
|
-
// now, but the identical root cause: a generic class param must carry its type
|
|
664
|
-
// arguments.
|
|
665
|
-
let src = "\
|
|
666
|
-
enum Box[T] =
|
|
667
|
-
| Box(value: T)
|
|
668
|
-
|
|
669
|
-
fun getBoxValue() -> T =
|
|
670
|
-
self.value
|
|
671
|
-
|
|
672
|
-
fun sumBox(b: Box[Int]) -> Int =
|
|
673
|
-
b.getBoxValue()
|
|
674
|
-
|
|
675
|
-
fun use() -> Int =
|
|
676
|
-
sumBox(Box(value: 5))
|
|
677
|
-
";
|
|
678
|
-
let source = parse(src);
|
|
679
|
-
let result = checkSource(&source);
|
|
680
|
-
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
#[test]
|
|
684
|
-
fn closureLiteralInfersAsAFunctionType() {
|
|
685
|
-
let src = "\
|
|
686
|
-
fun useClosure() -> Bool =
|
|
687
|
-
cb = |v|
|
|
688
|
-
true
|
|
689
|
-
cb(5)
|
|
690
|
-
";
|
|
691
|
-
let source = parse(src);
|
|
692
|
-
let result = checkSource(&source);
|
|
693
|
-
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
#[test]
|
|
697
|
-
fn fnValueTypedParamCanBeCalled() {
|
|
698
|
-
let src = "\
|
|
699
|
-
fun each(cb: fn(Int) -> Bool) -> Bool =
|
|
700
|
-
cb(5)
|
|
701
|
-
";
|
|
702
|
-
let source = parse(src);
|
|
703
|
-
let result = checkSource(&source);
|
|
704
|
-
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
#[test]
|
|
708
|
-
fn fieldAssignmentTargetWithMatchingTypePasses() {
|
|
709
|
-
let src = "\
|
|
710
|
-
enum Cat =
|
|
711
|
-
| Cat(name: Str, age: Int)
|
|
712
|
-
|
|
713
|
-
fun haveBirthday() =
|
|
714
|
-
self.age = self.age + 1
|
|
715
|
-
";
|
|
716
|
-
let source = parse(src);
|
|
717
|
-
assert!(checkSource(&source).is_ok(), "expected Ok");
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
#[test]
|
|
721
|
-
fn fieldAssignmentTargetWithMismatchedTypeIsError() {
|
|
722
|
-
let src = "\
|
|
723
|
-
enum Cat =
|
|
724
|
-
| Cat(name: Str, age: Int)
|
|
725
|
-
|
|
726
|
-
fun breakCat() =
|
|
727
|
-
self.age = \"oops\"
|
|
728
|
-
";
|
|
729
|
-
let source = parse(src);
|
|
730
|
-
let result = checkSource(&source);
|
|
731
|
-
assert!(result.is_err());
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
#[test]
|
|
735
|
-
fn fieldAssignmentTargetUnknownFieldIsError() {
|
|
736
|
-
let src = "\
|
|
737
|
-
enum Cat =
|
|
738
|
-
| Cat(name: Str, age: Int)
|
|
739
|
-
|
|
740
|
-
fun breakCat() =
|
|
741
|
-
self.nope = 1
|
|
742
|
-
";
|
|
743
|
-
let source = parse(src);
|
|
744
|
-
let result = checkSource(&source);
|
|
745
|
-
assert!(result.is_err());
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
#[test]
|
|
749
|
-
fn closurePassedToFnValueTypedParamTypeChecks() {
|
|
750
|
-
let src = "\
|
|
751
|
-
fun each(cb: fn(Int) -> Bool) -> Bool =
|
|
752
|
-
cb(5)
|
|
753
|
-
|
|
754
|
-
fun use() -> Bool =
|
|
755
|
-
each(|v|
|
|
756
|
-
true)
|
|
757
|
-
";
|
|
758
|
-
let source = parse(src);
|
|
759
|
-
let result = checkSource(&source);
|
|
760
|
-
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
#[test]
|
|
764
|
-
fn variadicCallWithZeroTrailingArgsPasses() {
|
|
765
|
-
let src = "\
|
|
766
|
-
fun sumAll(nums: ...Int) -> Int =
|
|
767
|
-
0
|
|
768
|
-
|
|
769
|
-
fun useSumAll() -> Int =
|
|
770
|
-
sumAll()
|
|
771
|
-
";
|
|
772
|
-
let source = parse(src);
|
|
773
|
-
assert!(checkSource(&source).is_ok(), "expected Ok");
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
#[test]
|
|
777
|
-
fn variadicCallWithSeveralTrailingArgsPasses() {
|
|
778
|
-
let src = "\
|
|
779
|
-
fun sumAll(nums: ...Int) -> Int =
|
|
780
|
-
0
|
|
781
|
-
|
|
782
|
-
fun useSumAll() -> Int =
|
|
783
|
-
sumAll(1, 2, 3)
|
|
784
|
-
";
|
|
785
|
-
let source = parse(src);
|
|
786
|
-
assert!(checkSource(&source).is_ok(), "expected Ok");
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
#[test]
|
|
790
|
-
fn enumVariantAsParamTypeAcceptsThatVariant() {
|
|
791
|
-
let src = "\
|
|
792
|
-
enum Color =
|
|
793
|
-
| Red
|
|
794
|
-
| Blue
|
|
795
|
-
|
|
796
|
-
fun stringifyColor(v: Red) -> Int =
|
|
797
|
-
0
|
|
798
|
-
|
|
799
|
-
fun useIt() -> Int =
|
|
800
|
-
stringifyColor(Red)
|
|
801
|
-
";
|
|
802
|
-
let source = parse(src);
|
|
803
|
-
assert!(checkSource(&source).is_ok(), "expected Ok");
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
#[test]
|
|
807
|
-
fn enumVariantAsParamTypeRejectsADifferentVariant() {
|
|
808
|
-
let src = "\
|
|
809
|
-
enum Color =
|
|
810
|
-
| Red
|
|
811
|
-
| Blue
|
|
812
|
-
|
|
813
|
-
fun stringifyColor(v: Red) -> Int =
|
|
814
|
-
0
|
|
815
|
-
|
|
816
|
-
fun useIt() -> Int =
|
|
817
|
-
stringifyColor(Blue)
|
|
818
|
-
";
|
|
819
|
-
let source = parse(src);
|
|
820
|
-
assert!(checkSource(&source).is_err());
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
#[test]
|
|
824
|
-
fn variadicCallWithMismatchedTrailingArgTypeIsError() {
|
|
825
|
-
let src = "\
|
|
826
|
-
fun sumAll(nums: ...Int) -> Int =
|
|
827
|
-
0
|
|
828
|
-
|
|
829
|
-
fun useSumAll() -> Int =
|
|
830
|
-
sumAll(1, \"two\")
|
|
831
|
-
";
|
|
832
|
-
let source = parse(src);
|
|
833
|
-
assert!(checkSource(&source).is_err());
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
#[test]
|
|
837
|
-
fn variadicCallWithFixedPrefixPasses() {
|
|
838
|
-
let src = "\
|
|
839
|
-
fun combine(prefix: Int, rest: ...Int) -> Int =
|
|
840
|
-
prefix
|
|
841
|
-
|
|
842
|
-
fun useCombine() -> Int =
|
|
843
|
-
combine(1, 2, 3)
|
|
844
|
-
";
|
|
845
|
-
let source = parse(src);
|
|
846
|
-
assert!(checkSource(&source).is_ok(), "expected Ok");
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
#[test]
|
|
850
|
-
fn twoVariadicParamsIsError() {
|
|
851
|
-
let src = "\
|
|
852
|
-
fun bad(a: ...Int, b: ...Int) -> Int =
|
|
853
|
-
0
|
|
854
|
-
";
|
|
855
|
-
let source = parse(src);
|
|
856
|
-
assert!(checkSource(&source).is_err());
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
#[test]
|
|
860
|
-
fn variadicParamNotLastIsError() {
|
|
861
|
-
let src = "\
|
|
862
|
-
fun bad(a: ...Int, b: Int) -> Int =
|
|
863
|
-
0
|
|
864
|
-
";
|
|
865
|
-
let source = parse(src);
|
|
866
|
-
assert!(checkSource(&source).is_err());
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
#[test]
|
|
870
|
-
fn forLoopOverVariadicBindsElementType() {
|
|
871
|
-
let src = "\
|
|
872
|
-
fun sumAll(nums: ...Int) -> Int =
|
|
873
|
-
total = 0
|
|
874
|
-
for v := range nums
|
|
875
|
-
total = total + v
|
|
876
|
-
total
|
|
877
|
-
";
|
|
878
|
-
let source = parse(src);
|
|
879
|
-
assert!(checkSource(&source).is_ok(), "expected Ok");
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
#[test]
|
|
883
|
-
fn forLoopOverVariadicWithTwoVarsBindsIndexAndElement() {
|
|
884
|
-
let src = "\
|
|
885
|
-
fun f(nums: ...Int) -> Int =
|
|
886
|
-
total := 0
|
|
887
|
-
for i, v := range nums
|
|
888
|
-
total = total + i * v
|
|
889
|
-
total
|
|
890
|
-
";
|
|
891
|
-
let source = parse(src);
|
|
892
|
-
assert!(checkSource(&source).is_ok(), "expected Ok");
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
#[test]
|
|
896
|
-
fn matchDestructuresPlainClassAndBindsFieldTypes() {
|
|
897
|
-
let src = "\
|
|
898
|
-
enum Point =
|
|
899
|
-
| Point(x: Int, y: Int)
|
|
900
|
-
|
|
901
|
-
fun sumPoint(p: Point) -> Int =
|
|
902
|
-
match p
|
|
903
|
-
Point(x, y) => x + y
|
|
904
|
-
";
|
|
905
|
-
let source = parse(src);
|
|
906
|
-
assert!(
|
|
907
|
-
checkSource(&source).is_ok(),
|
|
908
|
-
"expected Ok, got: {:?}",
|
|
909
|
-
checkSource(&source)
|
|
910
|
-
);
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
#[test]
|
|
914
|
-
fn matchClassPatternWrongFieldCountIsError() {
|
|
915
|
-
let src = "\
|
|
916
|
-
enum Point =
|
|
917
|
-
| Point(x: Int, y: Int)
|
|
918
|
-
|
|
919
|
-
fun sumPoint(p: Point) -> Int =
|
|
920
|
-
match p
|
|
921
|
-
Point(x) => x
|
|
922
|
-
";
|
|
923
|
-
let source = parse(src);
|
|
924
|
-
let result = checkSource(&source);
|
|
925
|
-
assert!(result.is_err());
|
|
926
|
-
let errs = result.unwrap_err();
|
|
927
|
-
assert!(
|
|
928
|
-
errs.iter()
|
|
929
|
-
.any(|e| e.message.contains("expects 2 field(s), got 1")),
|
|
930
|
-
"got: {:?}",
|
|
931
|
-
errs
|
|
932
|
-
);
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
#[test]
|
|
936
|
-
fn namedPayloadEnumVariantConstructsWithNamedArgs() {
|
|
937
|
-
let src = "\
|
|
938
|
-
enum Shape =
|
|
939
|
-
| Circle(radius: Int)
|
|
940
|
-
| Square(x: Int, y: Int)
|
|
941
|
-
|
|
942
|
-
fun area(self) -> Int =
|
|
943
|
-
match self
|
|
944
|
-
Circle(radius) => radius * radius
|
|
945
|
-
Square(x, y) => x * y
|
|
946
|
-
|
|
947
|
-
fun makeCircle() -> Shape =
|
|
948
|
-
Circle(radius: 5)
|
|
949
|
-
";
|
|
950
|
-
let source = parse(src);
|
|
951
|
-
assert!(
|
|
952
|
-
checkSource(&source).is_ok(),
|
|
953
|
-
"expected Ok, got: {:?}",
|
|
954
|
-
checkSource(&source)
|
|
955
|
-
);
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
#[test]
|
|
959
|
-
fn namedPayloadEnumVariantMissingFieldIsError() {
|
|
960
|
-
let src = "\
|
|
961
|
-
enum Shape =
|
|
962
|
-
| Square(x: Int, y: Int)
|
|
963
|
-
|
|
964
|
-
fun bad() -> Shape =
|
|
965
|
-
Square(x: 1)
|
|
966
|
-
";
|
|
967
|
-
let source = parse(src);
|
|
968
|
-
let result = checkSource(&source);
|
|
969
|
-
assert!(result.is_err());
|
|
970
|
-
let errs = result.unwrap_err();
|
|
971
|
-
assert!(
|
|
972
|
-
errs.iter()
|
|
973
|
-
.any(|e| e.message.contains("variant 'Square' missing field 'y'")),
|
|
974
|
-
"got: {:?}",
|
|
975
|
-
errs
|
|
976
|
-
);
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
#[test]
|
|
980
|
-
fn namedPayloadEnumVariantUnknownFieldIsError() {
|
|
981
|
-
let src = "\
|
|
982
|
-
enum Shape =
|
|
983
|
-
| Square(x: Int, y: Int)
|
|
984
|
-
|
|
985
|
-
fun bad() -> Shape =
|
|
986
|
-
Square(x: 1, y: 2, z: 3)
|
|
987
|
-
";
|
|
988
|
-
let source = parse(src);
|
|
989
|
-
let result = checkSource(&source);
|
|
990
|
-
assert!(result.is_err());
|
|
991
|
-
let errs = result.unwrap_err();
|
|
992
|
-
assert!(
|
|
993
|
-
errs.iter()
|
|
994
|
-
.any(|e| e.message.contains("unknown field 'z' on variant 'Square'")),
|
|
995
|
-
"got: {:?}",
|
|
996
|
-
errs
|
|
997
|
-
);
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
#[test]
|
|
1001
|
-
fn fieldAccessOnMultiVariantEnumValueResolvesViaTheUniqueOwningVariant() {
|
|
1002
|
-
// `radius` belongs to exactly one of `Shape`'s variants, so `.radius` on a
|
|
1003
|
-
// `Shape`-typed value type-checks even though `Shape` itself has more than
|
|
1004
|
-
// one variant — codegen compiles it as a checked downcast to `Circle`.
|
|
88
|
+
fn fieldAccessOnOrdinaryEnumReceiverIsUnaffectedByEnumParams() {
|
|
89
|
+
// An ordinary enum (no discriminant `params`) isn't in `ctx.classes` OR
|
|
90
|
+
// `ctx.enum_params`, so it falls through to the same permissive "unmodeled
|
|
91
|
+
// type" escape hatch every other type not in `ctx.classes` gets (codegen,
|
|
92
|
+
// not the checker, is what would catch a genuinely bad field access here) —
|
|
93
|
+
// exactly as it did before discriminant enums existed.
|
|
1005
94
|
let src = "\
|
|
1006
|
-
enum
|
|
95
|
+
enum Maybe =
|
|
1007
|
-
| Circle(radius: Int)
|
|
1008
|
-
|
|
|
96
|
+
| Just(Int)
|
|
97
|
+
| Nothing
|
|
1009
98
|
|
|
1010
|
-
fun
|
|
99
|
+
fun bad(self) -> Int =
|
|
1011
|
-
r := Circle(radius: 5)
|
|
1012
|
-
|
|
100
|
+
self.n
|
|
1013
101
|
";
|
|
1014
102
|
let source = parse(src);
|
|
1015
103
|
let result = checkSource(&source);
|
|
1016
104
|
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
1017
105
|
}
|
|
1018
106
|
|
|
1019
|
-
#[test]
|
|
1020
|
-
fn fieldAccessOnMultiVariantEnumValueIsAmbiguousAcrossSameNamedFields() {
|
|
1021
|
-
let src = "\
|
|
1022
|
-
enum Shape =
|
|
1023
|
-
| Circle(radius: Int)
|
|
1024
|
-
| Square(radius: Int)
|
|
1025
|
-
|
|
1026
|
-
fun bad() -> Int =
|
|
1027
|
-
r := Circle(radius: 5)
|
|
1028
|
-
return r.radius
|
|
1029
|
-
";
|
|
1030
|
-
let source = parse(src);
|
|
1031
|
-
let result = checkSource(&source);
|
|
1032
|
-
assert!(result.is_err());
|
|
1033
|
-
let errs = result.unwrap_err();
|
|
1034
|
-
assert!(
|
|
1035
|
-
errs.iter().any(|e| e
|
|
1036
|
-
.message
|
|
1037
|
-
.contains("field 'radius' is ambiguous across multiple variants of enum 'Shape'")),
|
|
1038
|
-
"got: {:?}",
|
|
1039
|
-
errs
|
|
1040
|
-
);
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
107
|
// ---------------------------------------------------------------------------
|
|
1044
108
|
// Roc-style tag unions + `Try` (T2). Sources define a local `Try` inline.
|
|
1045
109
|
// ---------------------------------------------------------------------------
|
|
@@ -1071,142 +135,6 @@ fn errText(r: Result<(), Vec<CheckError>>) -> String {
|
|
|
1071
135
|
.join(" | ")
|
|
1072
136
|
}
|
|
1073
137
|
|
|
1074
|
-
|
|
1075
|
-
// ---------- enum variant exhaustiveness ----------
|
|
1076
|
-
|
|
1077
|
-
fn enumMatch(prelude: &str, arms: &str) -> Result<(), Vec<CheckError>> {
|
|
1078
|
-
checkSource(&parseClean(&format!("{}{}", prelude, arms)))
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
const COLOR_DEF: &str = "\
|
|
1082
|
-
enum Color =
|
|
1083
|
-
| Red
|
|
1084
|
-
| Green
|
|
1085
|
-
| Blue
|
|
1086
|
-
|
|
1087
|
-
fun pick() -> Color =
|
|
1088
|
-
Red
|
|
1089
|
-
|
|
1090
|
-
fun name() -> Str =
|
|
1091
|
-
color := pick()
|
|
1092
|
-
match color
|
|
1093
|
-
";
|
|
1094
|
-
|
|
1095
|
-
#[test]
|
|
1096
|
-
fn missing_enum_variant_is_rejected() {
|
|
1097
|
-
let r = errText(enumMatch(COLOR_DEF, " Red => \"r\"\n Green => \"g\"\n"));
|
|
1098
|
-
assert!(r.contains("non-exhaustive match on Color: missing Blue"), "got: {}", r);
|
|
1099
|
-
let ok = enumMatch(COLOR_DEF, " Red => \"r\"\n Green => \"g\"\n Blue => \"b\"\n");
|
|
1100
|
-
assert!(ok.is_ok(), "got {:?}", ok.err());
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
#[test]
|
|
1104
|
-
fn wildcard_and_binding_arms_make_enum_match_exhaustive() {
|
|
1105
|
-
let w = enumMatch(COLOR_DEF, " Red => \"r\"\n _ => \"x\"\n");
|
|
1106
|
-
assert!(w.is_ok(), "got {:?}", w.err());
|
|
1107
|
-
let b = enumMatch(COLOR_DEF, " Red => \"r\"\n other => \"x\"\n");
|
|
1108
|
-
assert!(b.is_ok(), "got {:?}", b.err());
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
const WRAP_DEF: &str = "\
|
|
1112
|
-
enum Inner =
|
|
1113
|
-
| Present(Int)
|
|
1114
|
-
| Absent
|
|
1115
|
-
|
|
1116
|
-
enum Outer =
|
|
1117
|
-
| Wrap(Inner)
|
|
1118
|
-
| Empty
|
|
1119
|
-
|
|
1120
|
-
fun mk() -> Outer =
|
|
1121
|
-
Empty
|
|
1122
|
-
|
|
1123
|
-
fun name() -> Str =
|
|
1124
|
-
o := mk()
|
|
1125
|
-
match o
|
|
1126
|
-
";
|
|
1127
|
-
|
|
1128
|
-
#[test]
|
|
1129
|
-
fn nested_enum_payload_coverage() {
|
|
1130
|
-
let ok = enumMatch(
|
|
1131
|
-
WRAP_DEF,
|
|
1132
|
-
" Wrap(Present(v)) => \"p\"\n Wrap(Absent) => \"a\"\n Empty => \"e\"\n",
|
|
1133
|
-
);
|
|
1134
|
-
assert!(ok.is_ok(), "got {:?}", ok.err());
|
|
1135
|
-
let r = errText(enumMatch(
|
|
1136
|
-
WRAP_DEF,
|
|
1137
|
-
" Wrap(Present(v)) => \"p\"\n Empty => \"e\"\n",
|
|
1138
|
-
));
|
|
1139
|
-
assert!(r.contains("non-exhaustive match on Inner: missing Absent"), "got: {}", r);
|
|
1140
|
-
let lit = errText(enumMatch(
|
|
1141
|
-
WRAP_DEF,
|
|
1142
|
-
" Wrap(Present(0)) => \"p\"\n Wrap(Absent) => \"a\"\n Empty => \"e\"\n",
|
|
1143
|
-
));
|
|
1144
|
-
assert!(lit.contains("non-exhaustive match on Inner: missing Present"), "got: {}", lit);
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
#[test]
|
|
1148
|
-
fn bool_match_exhaustiveness() {
|
|
1149
|
-
let def = "\
|
|
1150
|
-
fun f(b: Bool) -> Int =
|
|
1151
|
-
match b
|
|
1152
|
-
";
|
|
1153
|
-
let ok = enumMatch(def, " true => 1\n false => 0\n");
|
|
1154
|
-
assert!(ok.is_ok(), "got {:?}", ok.err());
|
|
1155
|
-
let r = errText(enumMatch(def, " true => 1\n"));
|
|
1156
|
-
assert!(r.contains("non-exhaustive match on Bool: missing false"), "got: {}", r);
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
#[test]
|
|
1160
|
-
fn discriminant_enum_match_exhaustiveness() {
|
|
1161
|
-
let def = "\
|
|
1162
|
-
enum Step(n: Int) =
|
|
1163
|
-
| ReadMin(10)
|
|
1164
|
-
| ReadMax(20)
|
|
1165
|
-
|
|
1166
|
-
fun f(s: Step) -> Int =
|
|
1167
|
-
match s
|
|
1168
|
-
";
|
|
1169
|
-
let ok = enumMatch(def, " ReadMin => 1\n ReadMax => 2\n");
|
|
1170
|
-
assert!(ok.is_ok(), "got {:?}", ok.err());
|
|
1171
|
-
let r = errText(enumMatch(def, " ReadMin => 1\n"));
|
|
1172
|
-
assert!(r.contains("non-exhaustive match on Step: missing ReadMax"), "got: {}", r);
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
fn repoRoot() -> std::path::PathBuf {
|
|
1176
|
-
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
/// Acceptance: every std and example file checks with zero errors on the
|
|
1180
|
-
/// source as written (generics are erased, never specialized).
|
|
1181
|
-
#[test]
|
|
1182
|
-
fn everyStdAndExampleChecksStructurally() {
|
|
1183
|
-
let mut failures: Vec<String> = Vec::new();
|
|
1184
|
-
for dir in ["plum-std", "plum-examples"] {
|
|
1185
|
-
let mut files: Vec<_> = std::fs::read_dir(repoRoot().join(dir))
|
|
1186
|
-
.unwrap()
|
|
1187
|
-
.filter_map(|e| e.ok())
|
|
1188
|
-
.map(|e| e.path())
|
|
1189
|
-
.filter(|p| p.extension().and_then(|e| e.to_str()) == Some("plum"))
|
|
1190
|
-
.collect();
|
|
1191
|
-
files.sort();
|
|
1192
|
-
for path in files {
|
|
1193
|
-
let source = plum_core::loadAndMerge(&path, &repoRoot())
|
|
1194
|
-
.unwrap_or_else(|e| panic!("failed to load {}: {}", path.display(), e));
|
|
1195
|
-
if let Err(errs) = checkSource(&source) {
|
|
1196
|
-
for e in errs {
|
|
1197
|
-
failures.push(format!(
|
|
1198
|
-
"{}: {}",
|
|
1199
|
-
path.file_name().unwrap().to_string_lossy(),
|
|
1200
|
-
e.message
|
|
1201
|
-
));
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
assert!(failures.is_empty(), "structural check errors:\n{}", failures.join("\n"));
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
138
|
// ---- structural generics (T2) ----
|
|
1211
139
|
|
|
1212
140
|
const GENERIC_DEFS: &str = "\
|
|
@@ -1282,13 +210,6 @@ fn method_own_generic_instantiated_per_call() {
|
|
|
1282
210
|
assert!(errText(bad).contains("type mismatch"));
|
|
1283
211
|
}
|
|
1284
212
|
|
|
1285
|
-
#[test]
|
|
1286
|
-
fn unbounded_tparam_method_now_ok() {
|
|
1287
|
-
// Bounds are ignored: a method on a bare `T` is checked per instantiation.
|
|
1288
|
-
let r = checkSource(&parseClean("fun f(x: T) -> Int =\n x.hash()\n"));
|
|
1289
|
-
assert!(r.is_ok(), "uninstantiated unbounded T must not error: {:?}", r.err());
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
213
|
const DUCK_DEFS: &str = "\
|
|
1293
214
|
enum Num =
|
|
1294
215
|
| Num(v: Int)
|
|
@@ -1304,33 +225,16 @@ fun maxOf(a: T, b: T) -> T =
|
|
|
1304
225
|
|
|
1305
226
|
";
|
|
1306
227
|
|
|
1307
|
-
#[test]
|
|
1308
|
-
fn duck_ok() {
|
|
1309
|
-
let src = format!(
|
|
1310
|
-
"{}fun main() -> Num =\n maxOf(Num(v: 1), Num(v: 2))\n",
|
|
1311
|
-
DUCK_DEFS
|
|
1312
|
-
);
|
|
1313
|
-
let r = checkSource(&parseClean(&src));
|
|
1314
|
-
assert!(r.is_ok(), "{:?}", r.err());
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
#[test]
|
|
1318
|
-
fn duck_missing_method_chain() {
|
|
1319
|
-
let src = format!(
|
|
1320
|
-
|
|
228
|
+
/// A generic body is only checked per instantiation: an uninstantiated one is never type-errored.
|
|
1321
|
-
DUCK_DEFS
|
|
1322
|
-
);
|
|
1323
|
-
let msg = errText(checkSource(&parseClean(&src)));
|
|
1324
|
-
assert!(msg.contains("called at"), "{}", msg);
|
|
1325
|
-
assert!(msg.contains("in maxOf[Plain]"), "{}", msg);
|
|
1326
|
-
assert!(msg.contains("Plain has no method 'lt'"), "{}", msg);
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
229
|
#[test]
|
|
1330
230
|
fn uninstantiated_generic_not_type_errored() {
|
|
1331
231
|
let src = format!("{}fun unused(a: T) -> Int =\n a.nothing() + \"s\"\n", DUCK_DEFS);
|
|
1332
232
|
let r = checkSource(&parseClean(&src));
|
|
1333
233
|
assert!(r.is_ok(), "{:?}", r.err());
|
|
234
|
+
for src in ["fun f(x: T) -> Int =\n x.hash()\n", "fun f(x: T) -> Int =\n x\n"] {
|
|
235
|
+
let r = checkSource(&parseClean(src));
|
|
236
|
+
assert!(r.is_ok(), "{src}: {:?}", r.err());
|
|
237
|
+
}
|
|
1334
238
|
}
|
|
1335
239
|
|
|
1336
240
|
#[test]
|
|
@@ -1347,18 +251,6 @@ fn nested_instantiation_chain() {
|
|
|
1347
251
|
assert!(msg.contains("Plain has no method 'lt'"), "{}", msg);
|
|
1348
252
|
}
|
|
1349
253
|
|
|
1350
|
-
#[test]
|
|
1351
|
-
fn operator_missing_method_error() {
|
|
1352
|
-
let src = format!(
|
|
1353
|
-
"{}fun twice(a: T) -> T =\n a + a\n\nfun main() -> Plain =\n twice(Plain(v: 1))\n",
|
|
1354
|
-
DUCK_DEFS
|
|
1355
|
-
);
|
|
1356
|
-
let msg = errText(checkSource(&parseClean(&src)));
|
|
1357
|
-
assert!(msg.contains("in twice[Plain]"), "{}", msg);
|
|
1358
|
-
assert!(msg.contains("called at"), "{}", msg);
|
|
1359
|
-
assert!(msg.contains("Plain has no operator '+' (define add)"), "{}", msg);
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
254
|
#[test]
|
|
1363
255
|
fn instantiation_set_listed() {
|
|
1364
256
|
let src = format!(
|
|
@@ -1378,49 +270,6 @@ fn instantiation_set_listed() {
|
|
|
1378
270
|
assert!(!info.inst_expr_types.is_empty());
|
|
1379
271
|
}
|
|
1380
272
|
|
|
1381
|
-
#[test]
|
|
1382
|
-
fn duck_typed_generic_method_ok() {
|
|
1383
|
-
let src = "\
|
|
1384
|
-
enum Keyed[T] =
|
|
1385
|
-
| Keyed(value: T)
|
|
1386
|
-
|
|
1387
|
-
fun h(self) -> Int =
|
|
1388
|
-
self.value.hash()
|
|
1389
|
-
";
|
|
1390
|
-
let r = checkSource(&parseClean(src));
|
|
1391
|
-
assert!(r.is_ok(), "got {:?}", r.err());
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
#[test]
|
|
1395
|
-
fn generic_free_fn_instantiated_per_call() {
|
|
1396
|
-
let defs = "\
|
|
1397
|
-
fun identity(value: T) -> T =
|
|
1398
|
-
value
|
|
1399
|
-
|
|
1400
|
-
fun a() -> Int =
|
|
1401
|
-
identity(5)
|
|
1402
|
-
|
|
1403
|
-
fun b() -> Str =
|
|
1404
|
-
identity(\"x\")
|
|
1405
|
-
";
|
|
1406
|
-
assert!(checkSource(&parseClean(&format!("{}{}", GENERIC_DEFS, defs))).is_ok());
|
|
1407
|
-
let bad = "fun identity(value: T) -> T =\n value\n\nfun a() -> Str =\n identity(5)\n";
|
|
1408
|
-
assert!(checkSource(&parseClean(bad)).is_err());
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
#[test]
|
|
1412
|
-
fn generic_return_of_param_rejected_for_concrete_declared_return() {
|
|
1413
|
-
// Duck typing: only an instantiation where `T` is not `Int` is rejected.
|
|
1414
|
-
assert!(checkSource(&parseClean("fun f(x: T) -> Int =\n x\n")).is_ok());
|
|
1415
|
-
assert!(checkSource(&parseClean(
|
|
1416
|
-
"fun f(x: T) -> Int =\n x\n\nfun main() -> Int =\n f(\"s\")\n"
|
|
1417
|
-
))
|
|
1418
|
-
.is_err());
|
|
1419
|
-
let method = "enum Box[T] =\n | Box(value: T)\n\n fun get() -> Int =\n self.value\n\nfun main() -> Int =\n Box(value: \"s\").get()\n";
|
|
1420
|
-
assert!(checkSource(&parseClean(method)).is_err());
|
|
1421
|
-
assert!(checkSource(&parseClean("fun f(x: T) -> T =\n x\n")).is_ok());
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
273
|
fn hasInferVar(t: &PlumType) -> bool {
|
|
1425
274
|
match t {
|
|
1426
275
|
PlumType::TVar(_) => true,
|
|
@@ -1460,96 +309,8 @@ fn typed_source_has_no_unresolved_infer_vars() {
|
|
|
1460
309
|
assert!(bad.is_empty(), "{:#?}", bad);
|
|
1461
310
|
}
|
|
1462
311
|
|
|
1463
|
-
#[test]
|
|
1464
|
-
fn probe_nested_generic_substitution() {
|
|
1465
|
-
let ok = "fun f() -> List[Box[Int]] =\n List(items: Box(value: 1))\n";
|
|
1466
|
-
assert!(checkSource(&parseClean(&format!("{}{}", GENERIC_DEFS, ok))).is_ok());
|
|
1467
|
-
let bad = "fun f() -> List[Box[Str]] =\n List(items: Box(value: 1))\n";
|
|
1468
|
-
assert!(checkSource(&parseClean(&format!("{}{}", GENERIC_DEFS, bad))).is_err());
|
|
1469
|
-
let m = "fun f() -> List[Str] =\n List(items: 1).map(|x| x + 1)\n";
|
|
1470
|
-
assert!(checkSource(&parseClean(&format!("{}{}", GENERIC_DEFS, m))).is_err());
|
|
1471
|
-
let m2 = "fun f() -> List[Int] =\n List(items: 1).map(|x| x + 1)\n";
|
|
1472
|
-
assert!(checkSource(&parseClean(&format!("{}{}", GENERIC_DEFS, m2))).is_ok());
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
#[test]
|
|
1476
|
-
fn bare_generic_type_in_signature_is_rejected() {
|
|
1477
|
-
let src = "\
|
|
1478
|
-
enum Parcel[T] =
|
|
1479
|
-
| Delivered(T)
|
|
1480
|
-
| Lost
|
|
1481
|
-
|
|
1482
|
-
fun contentsOr(p: Parcel, default: Int) -> Int =
|
|
1483
|
-
match p
|
|
1484
|
-
Delivered(v) => v
|
|
1485
|
-
Lost => default
|
|
1486
|
-
";
|
|
1487
|
-
let errs = checkSource(&parse(src)).unwrap_err();
|
|
1488
|
-
assert!(
|
|
1489
|
-
errs.iter().any(|e| e
|
|
1490
|
-
.message
|
|
1491
|
-
.contains("generic type 'Parcel' needs type arguments, e.g. Parcel[T]")),
|
|
1492
|
-
"got: {:?}",
|
|
1493
|
-
errs
|
|
1494
|
-
);
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
#[test]
|
|
1498
|
-
fn bare_generic_type_allowed_inside_its_own_methods() {
|
|
1499
|
-
let src = "\
|
|
1500
|
-
enum Box[T] =
|
|
1501
|
-
| Box(value: T)
|
|
1502
|
-
|
|
1503
|
-
fun same(self) -> Box =
|
|
1504
|
-
self
|
|
1505
|
-
";
|
|
1506
|
-
let result = checkSource(&parse(src));
|
|
1507
|
-
assert!(result.is_ok(), "got {:?}", result.err());
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
312
|
// ---------- built-in Bool (no `import std/Bool`, no `enum Bool`) ----------
|
|
1511
313
|
|
|
1512
|
-
#[test]
|
|
1513
|
-
fn builtin_bool_needs_no_import() {
|
|
1514
|
-
let src = "\
|
|
1515
|
-
fun both(a: Bool, b: Bool) -> Bool =
|
|
1516
|
-
a && !b || false
|
|
1517
|
-
|
|
1518
|
-
fun main() -> Bool =
|
|
1519
|
-
x := true
|
|
1520
|
-
x == both(true, false)
|
|
1521
|
-
";
|
|
1522
|
-
let r = checkSource(&parseClean(src));
|
|
1523
|
-
assert!(r.is_ok(), "got {:?}", r.err());
|
|
1524
|
-
}
|
|
1525
|
-
|
|
1526
|
-
#[test]
|
|
1527
|
-
fn builtin_bool_match_missing_false_is_rejected() {
|
|
1528
|
-
let r = errText(checkSource(&parseClean(
|
|
1529
|
-
"fun name(b: Bool) -> Str =\n match b\n true => \"y\"\n",
|
|
1530
|
-
)));
|
|
1531
|
-
assert!(r.contains("non-exhaustive match on Bool: missing false"), "got: {}", r);
|
|
1532
|
-
let ok = checkSource(&parseClean(
|
|
1533
|
-
"fun name(b: Bool) -> Str =\n match b\n true => \"y\"\n false => \"n\"\n",
|
|
1534
|
-
));
|
|
1535
|
-
assert!(ok.is_ok(), "got {:?}", ok.err());
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
#[test]
|
|
1539
|
-
fn true_false_are_never_tags() {
|
|
1540
|
-
let src = "fun f() -> Int =\n x := true\n y := false\n if x == y then 1 else 0\n";
|
|
1541
|
-
let r = checkSource(&parseClean(src));
|
|
1542
|
-
assert!(r.is_ok(), "got {:?}", r.err());
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
#[test]
|
|
1546
|
-
fn builtin_missing_method_strict() {
|
|
1547
|
-
let src = "fun maxOf(a: T, b: T) -> T =\n if a.lt(b) then b else a\n\nfun main() -> Int =\n maxOf(1, 2)\n";
|
|
1548
|
-
let msg = errText(checkSource(&parseClean(src)));
|
|
1549
|
-
assert!(msg.contains("in maxOf[Int] (called at "), "{}", msg);
|
|
1550
|
-
assert!(msg.contains("Int has no method 'lt'"), "{}", msg);
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
314
|
#[test]
|
|
1554
315
|
fn called_at_exact_line_col() {
|
|
1555
316
|
// `maxOf` is declared on line 4 but called on line 7, col 3.
|
|
@@ -1570,48 +331,10 @@ fn nested_chain_exact_sites() {
|
|
|
1570
331
|
assert!(msg.contains("Plain has no method 'nope'"), "{}", msg);
|
|
1571
332
|
}
|
|
1572
333
|
|
|
1573
|
-
fn valueErrors(src: &str) -> Vec<String> {
|
|
1574
|
-
match checkSource(&parse(src)) {
|
|
1575
|
-
Ok(()) => vec![],
|
|
1576
|
-
Err(es) => es.into_iter().map(|e| e.message).collect(),
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
#[test]
|
|
1581
|
-
fn value_enum_str_field_rejected() {
|
|
1582
|
-
let errs = valueErrors("struct Named(label: Str, n: Int)\n");
|
|
1583
|
-
assert!(
|
|
1584
|
-
errs.iter().any(|e| e.contains("struct field 'label' must be Int, Float, Bool, Byte or a struct")),
|
|
1585
|
-
"{errs:?}"
|
|
1586
|
-
);
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
#[test]
|
|
1590
|
-
fn value_enum_recursion_rejected() {
|
|
1591
|
-
let errs = valueErrors("struct Ping(b: Pong)\nstruct Pong(a: Ping)\n");
|
|
1592
|
-
assert!(errs.iter().any(|e| e.contains("cannot contain itself")), "{errs:?}");
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
#[test]
|
|
1596
|
-
fn value_enum_valid_accepted() {
|
|
1597
|
-
let errs = valueErrors("struct Vec2(x: Float, y: Float)\nstruct Rect(pos: Vec2, size: Vec2)\n");
|
|
1598
|
-
assert!(errs.is_empty(), "{errs:?}");
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
|
-
#[test]
|
|
1602
|
-
fn not_iterable_error() {
|
|
1603
|
-
let src = "fun main() -> Int =\n total := 0\n for x := range 5.5f\n total = total + 1\n total\n";
|
|
1604
|
-
let msg = errText(checkSource(&parseClean(src)));
|
|
1605
|
-
assert!(msg.contains("'5.5f' is not a for-form or iterable"), "{}", msg);
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
334
|
// ---------------------------------------------------------------------------
|
|
1609
335
|
// for-forms: inline funs expanded at `for x := range form(..)` sites (plum-core forms.rs)
|
|
1610
336
|
// ---------------------------------------------------------------------------
|
|
1611
337
|
|
|
1612
|
-
const UPTO: &str = "fun* upTo(start: Int, end: Int) *Int =\n i := start\n while i < end\n cur := i\n i = i + 1\n yield cur\n\n";
|
|
1613
|
-
const TWICE: &str = "fun* twice(n: Int) *Int =\n for i := range upTo(0, n)\n yield i\n\n";
|
|
1614
|
-
|
|
1615
338
|
fn twice() -> String {
|
|
1616
339
|
format!("{UPTO}{TWICE}")
|
|
1617
340
|
}
|
|
@@ -1620,22 +343,8 @@ fn formErrors(src: &str) -> String {
|
|
|
1620
343
|
errText(checkSource(&parseClean(src)))
|
|
1621
344
|
}
|
|
1622
345
|
|
|
1623
|
-
#[test]
|
|
1624
|
-
fn form_expands_and_checks() {
|
|
1625
|
-
let src = format!(
|
|
1626
|
-
|
|
346
|
+
const UPTO: &str = "fun* upTo(start: Int, end: Int) *Int =\n i := start\n while i < end\n cur := i\n i = i + 1\n yield cur\n\n";
|
|
1627
|
-
twice()
|
|
1628
|
-
);
|
|
1629
|
-
assert!(checkSource(&parseClean(&src)).is_ok());
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
#[test]
|
|
1633
|
-
fn fun_star_range() {
|
|
1634
|
-
let src = format!(
|
|
1635
|
-
|
|
347
|
+
const TWICE: &str = "fun* twice(n: Int) *Int =\n for i := range upTo(0, n)\n yield i\n\n";
|
|
1636
|
-
);
|
|
1637
|
-
assert!(checkSource(&parseClean(&src)).is_ok());
|
|
1638
|
-
}
|
|
1639
348
|
|
|
1640
349
|
#[test]
|
|
1641
350
|
fn clause_if_keeps_when_true() {
|
|
@@ -1650,75 +359,6 @@ fn clause_if_keeps_when_true() {
|
|
|
1650
359
|
assert!(dbg.contains("Not("), "guard should be `if !cond {{ continue }}`: {}", dbg);
|
|
1651
360
|
}
|
|
1652
361
|
|
|
1653
|
-
#[test]
|
|
1654
|
-
fn misplaced_yield_rejected() {
|
|
1655
|
-
let src = "fun* leaky(n: Int) *Int =\n x := 1 + yield n\n x\n\nfun main() -> Int =\n for i := range leaky(3)\n i\n 0\n";
|
|
1656
|
-
let parsed = parseClean(src);
|
|
1657
|
-
let msg = errText(checkSource(&parsed));
|
|
1658
|
-
assert!(msg.contains("yield"), "{msg}");
|
|
1659
|
-
}
|
|
1660
|
-
|
|
1661
|
-
#[test]
|
|
1662
|
-
fn inline_recursion_rejected() {
|
|
1663
|
-
let direct = "fun* loopy(n: Int) *Int =\n for i := range loopy(n)\n yield i\n\nfun main() -> Int =\n for i := range loopy(3)\n i\n 0\n";
|
|
1664
|
-
assert!(formErrors(direct).contains("fun* 'loopy' can't be recursive"));
|
|
1665
|
-
let mutual = "fun* pingF(n: Int) *Int =\n for i := range pongF(n)\n yield i\n\nfun* pongF(n: Int) *Int =\n for i := range pingF(n)\n yield i\n\nfun main() -> Int =\n for i := range pingF(3)\n i\n 0\n";
|
|
1666
|
-
assert!(formErrors(mutual).contains("can't be recursive"));
|
|
1667
|
-
}
|
|
1668
|
-
|
|
1669
|
-
#[test]
|
|
1670
|
-
fn fun_star_only_as_for_source() {
|
|
1671
|
-
let src = format!("{}fun main() -> Int =\n x := twice(3)\n 0\n", twice());
|
|
1672
|
-
assert!(formErrors(&src).contains("fun* 'twice' can only be used as a for source"));
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
#[test]
|
|
1676
|
-
fn not_a_form_error() {
|
|
1677
|
-
let src = "fun five() -> Float =\n 5.0f\n\nfun main() -> Int =\n for x := range five()\n x\n 0\n";
|
|
1678
|
-
assert!(formErrors(src).contains("'five()' is not a for-form or iterable"));
|
|
1679
|
-
}
|
|
1680
|
-
|
|
1681
|
-
#[test]
|
|
1682
|
-
fn binder_arity_mismatch() {
|
|
1683
|
-
let src = format!("{}fun main() -> Int =\n for a, b := range twice(3)\n a\n 0\n", twice());
|
|
1684
|
-
assert!(formErrors(&src).contains("binds 2 names but yields 1"));
|
|
1685
|
-
let two = "fun* pairs(n: Int) *(Int, Int) =\n yield n, n\n\nfun main() -> Int =\n for a := range pairs(3)\n a\n 0\n";
|
|
1686
|
-
assert!(formErrors(two).contains("binds 1 names but yields 2"), "{}", formErrors(two));
|
|
1687
|
-
}
|
|
1688
|
-
|
|
1689
|
-
#[test]
|
|
1690
|
-
fn star_type_only_on_fun_star() {
|
|
1691
|
-
let src = "fun f(n: Int) *Int =\n n\n\nfun main() -> Int =\n 0\n";
|
|
1692
|
-
assert!(formErrors(src).contains("*T is only allowed on fun*"), "{}", formErrors(src));
|
|
1693
|
-
let seq = "fun g(n: Int, src: *Int) -> Int =\n n\n\nfun main() -> Int =\n 0\n";
|
|
1694
|
-
assert!(formErrors(seq).contains("*T is only allowed on fun*"));
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
#[test]
|
|
1698
|
-
fn fun_star_requires_star_type() {
|
|
1699
|
-
let src = "fun* f(n: Int) -> Int =\n n\n\nfun main() -> Int =\n 0\n";
|
|
1700
|
-
assert!(formErrors(src).contains("fun* 'f' must declare its yield type (*T)"), "{}", formErrors(src));
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
#[test]
|
|
1704
|
-
fn fun_star_return_rejected() {
|
|
1705
|
-
let src = "fun* f(n: Int) *Int =\n return 1\n\nfun main() -> Int =\n 0\n";
|
|
1706
|
-
assert!(formErrors(src).contains("'return' is not allowed"), "{}", formErrors(src));
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
#[test]
|
|
1710
|
-
fn yield_outside_fun_star_rejected() {
|
|
1711
|
-
let src = "fun main() -> Int =\n yield 1\n 0\n";
|
|
1712
|
-
assert!(formErrors(src).contains("yield is only allowed inside a fun*"), "{}", formErrors(src));
|
|
1713
|
-
}
|
|
1714
|
-
|
|
1715
|
-
#[test]
|
|
1716
|
-
fn unused_fun_star_is_checked_at_definition() {
|
|
1717
|
-
let src = "fun* broken(n: Int) *Int =\n yield n + nope\n\nfun main() -> Int =\n 0\n";
|
|
1718
|
-
let msg = formErrors(src);
|
|
1719
|
-
assert!(msg.contains("fun* 'broken'") && msg.contains("nope"), "{msg}");
|
|
1720
|
-
}
|
|
1721
|
-
|
|
1722
362
|
#[test]
|
|
1723
363
|
fn fun_star_type_error_at_definition_names_the_fun_star() {
|
|
1724
364
|
let src = "fun* bad(n: Int) *Int -> Int =\n total := 0\n total = total + \"s\"\n total\n\nfun main() -> Int =\n 0\n";
|
|
@@ -1726,89 +366,6 @@ fn fun_star_type_error_at_definition_names_the_fun_star() {
|
|
|
1726
366
|
assert!(msg.contains("fun* 'bad'") && !msg.contains("__star_"), "{msg}");
|
|
1727
367
|
}
|
|
1728
368
|
|
|
1729
|
-
#[test]
|
|
1730
|
-
fn expansion_error_names_form_and_use_site() {
|
|
1731
|
-
// the user's body is the broken part: the error names the form and the `for` position
|
|
1732
|
-
let src = format!("{}fun main() -> Int =\n for i := range twice(4)\n undefinedName\n 0\n", twice());
|
|
1733
|
-
let msg = formErrors(&src);
|
|
1734
|
-
assert!(msg.contains("undefinedName"), "{msg}");
|
|
1735
|
-
let bad = format!("{}fun main() -> Int =\n for a, b := range twice(3)\n a\n 0\n", twice());
|
|
1736
|
-
let msg = formErrors(&bad);
|
|
1737
|
-
assert!(msg.contains("in for-form 'twice' (used at"), "{msg}");
|
|
1738
|
-
}
|
|
1739
|
-
|
|
1740
|
-
#[test]
|
|
1741
|
-
fn nested_fun_star_forms() {
|
|
1742
|
-
let src = format!(
|
|
1743
|
-
"{}fun* outer(n: Int) *Int =\n for i := range twice(n)\n yield i + 1\n\nfun main() -> Int =\n total := 0\n for j := range outer(3)\n total = total + j\n total\n",
|
|
1744
|
-
twice()
|
|
1745
|
-
);
|
|
1746
|
-
assert!(checkSource(&parseClean(&src)).is_ok(), "{}", formErrors(&src));
|
|
1747
|
-
}
|
|
1748
|
-
|
|
1749
|
-
#[test]
|
|
1750
|
-
fn seq_value_cannot_be_stored() {
|
|
1751
|
-
for body in [" saved := src\n for x := range saved\n yield x\n", " for x := range src\n yield x\n src\n"] {
|
|
1752
|
-
let src = format!(
|
|
1753
|
-
"fun* leak(src: *T) *T =\n{body}\nfun main() -> Int =\n for i := range leak(List(1))\n i\n 0\n"
|
|
1754
|
-
);
|
|
1755
|
-
assert!(
|
|
1756
|
-
formErrors(&src).contains("sequence parameter 'src' can only be used as a for source"),
|
|
1757
|
-
"{}",
|
|
1758
|
-
formErrors(&src)
|
|
1759
|
-
);
|
|
1760
|
-
}
|
|
1761
|
-
}
|
|
1762
|
-
|
|
1763
|
-
#[test]
|
|
1764
|
-
fn indexed_removed() {
|
|
1765
|
-
// `indexed` is gone from std/Seq: two binders over a list give (position, element).
|
|
1766
|
-
let iter = std::fs::read_to_string(
|
|
1767
|
-
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../plum-std/Seq.plum"),
|
|
1768
|
-
)
|
|
1769
|
-
.unwrap();
|
|
1770
|
-
assert!(!iter.contains("fun* indexed"), "indexed must be removed");
|
|
1771
|
-
assert!(!iter.contains("fun* entries"), "entries moved to Map");
|
|
1772
|
-
}
|
|
1773
|
-
|
|
1774
|
-
#[test]
|
|
1775
|
-
fn star_method_binder_arity_mismatch() {
|
|
1776
|
-
let src = "enum Bag =\n | Bag(n: Int)\n\n fun* pairs(self) *(Int, Int) =\n yield self.n, self.n\n\nfun main() -> Int =\n b := Bag(n: 1)\n for d := range b.pairs()\n d\n 0\n";
|
|
1777
|
-
assert!(formErrors(src).contains("binds 1 names but yields 2"), "{}", formErrors(src));
|
|
1778
|
-
}
|
|
1779
|
-
|
|
1780
|
-
fn checkErr(src: &str) -> String {
|
|
1781
|
-
let source = parse(src);
|
|
1782
|
-
match checkSource(&source) {
|
|
1783
|
-
Ok(_) => String::new(),
|
|
1784
|
-
Err(errs) => errs.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join("\n"),
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
#[test]
|
|
1789
|
-
fn bool_literals_lowercase() {
|
|
1790
|
-
let ok = "fun f() -> Bool =\n x := true\n y := false\n x && !y\n";
|
|
1791
|
-
assert!(checkSource(&parse(ok)).is_ok());
|
|
1792
|
-
let bad = "fun f() -> Int =\n true\n";
|
|
1793
|
-
assert!(checkErr(bad).contains("return type mismatch"), "{}", checkErr(bad));
|
|
1794
|
-
}
|
|
1795
|
-
|
|
1796
|
-
#[test]
|
|
1797
|
-
fn bool_match_exhaustive() {
|
|
1798
|
-
let ok = "fun f(b: Bool) -> Int =\n match b\n true => 1\n false => 0\n";
|
|
1799
|
-
assert!(checkSource(&parse(ok)).is_ok());
|
|
1800
|
-
let bad = "fun f(b: Bool) -> Int =\n match b\n true => 1\n";
|
|
1801
|
-
assert!(checkErr(bad).contains("non-exhaustive match on Bool: missing false"), "{}", checkErr(bad));
|
|
1802
|
-
}
|
|
1803
|
-
|
|
1804
|
-
#[test]
|
|
1805
|
-
fn capital_True_is_plain_identifier() {
|
|
1806
|
-
let r = checkErr("fun f() -> Bool =\n True\n");
|
|
1807
|
-
assert!(!r.is_empty(), "`True` must no longer be a Bool literal");
|
|
1808
|
-
let r = checkErr("fun f(b: Bool) -> Int =\n match b\n True => 1\n false => 0\n");
|
|
1809
|
-
assert!(!r.is_empty(), "`True` must not be a Bool pattern");
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
369
|
// ---- error unions in the type checker (the legacy Try/tag-union tests are gone) ----
|
|
1813
370
|
|
|
1814
371
|
#[test]
|
|
@@ -1819,28 +376,6 @@ fn nested_generic_field() {
|
|
|
1819
376
|
);
|
|
1820
377
|
}
|
|
1821
378
|
|
|
1822
|
-
#[test]
|
|
1823
|
-
fn error_sets_are_covariant_in_generic_arguments() {
|
|
1824
|
-
let defs = "\
|
|
1825
|
-
fun wide(b: Box[error{A, B}!Int]) -> Int =
|
|
1826
|
-
1
|
|
1827
|
-
|
|
1828
|
-
fun narrow(b: Box[error{A}!Int]) -> Int =
|
|
1829
|
-
wide(b)
|
|
1830
|
-
";
|
|
1831
|
-
let r = checkSource(&parseClean(&format!("{}{}", GENERIC_DEFS, defs)));
|
|
1832
|
-
assert!(r.is_ok(), "got {:?}", r.err());
|
|
1833
|
-
let rev = "\
|
|
1834
|
-
fun narrow(b: Box[error{A}!Int]) -> Int =
|
|
1835
|
-
1
|
|
1836
|
-
|
|
1837
|
-
fun bad(b: Box[error{A, B}!Int]) -> Int =
|
|
1838
|
-
narrow(b)
|
|
1839
|
-
";
|
|
1840
|
-
let r = checkSource(&parseClean(&format!("{}{}", GENERIC_DEFS, rev)));
|
|
1841
|
-
assert!(r.is_err());
|
|
1842
|
-
}
|
|
1843
|
-
|
|
1844
379
|
#[test]
|
|
1845
380
|
fn typed_source_records_error_union_return() {
|
|
1846
381
|
let source = parseClean(&format!(
|
|
@@ -1852,194 +387,12 @@ fn typed_source_records_error_union_return() {
|
|
|
1852
387
|
assert_eq!(lastFnBodyType("fun f() -> error{A}!Int =\n 1\n"), PlumType::TInt);
|
|
1853
388
|
}
|
|
1854
389
|
|
|
1855
|
-
#[test]
|
|
1856
|
-
fn match_exhaustive_over_error_names() {
|
|
1857
|
-
let base = "\
|
|
1858
|
-
fun g(n: Int) -> !Int =
|
|
1859
|
-
if n == 0
|
|
1860
|
-
return error.A
|
|
1861
|
-
if n == 1
|
|
1862
|
-
return error.B
|
|
1863
|
-
n
|
|
1864
|
-
|
|
1865
|
-
fun f(n: Int) -> Int =
|
|
1866
|
-
g(n) catch e
|
|
1867
|
-
match e
|
|
1868
|
-
{}";
|
|
1869
|
-
let ok = checkSrc(&base.replace("{}", " A => 1\n B => 2\n"));
|
|
1870
|
-
assert!(ok.is_ok(), "got {:?}", ok.err());
|
|
1871
|
-
let wild = checkSrc(&base.replace("{}", " A => 1\n _ => 2\n"));
|
|
1872
|
-
assert!(wild.is_ok(), "got {:?}", wild.err());
|
|
1873
|
-
let missing = errText(checkSrc(&base.replace("{}", " A => 1\n")));
|
|
1874
|
-
assert!(missing.contains("non-exhaustive match on error{A, B}: missing B"), "got: {}", missing);
|
|
1875
|
-
let unknown = checkSrc(&base.replace("{}", " A => 1\n C => 3\n _ => 2\n"));
|
|
1876
|
-
assert!(unknown.is_err());
|
|
1877
|
-
}
|
|
1878
|
-
|
|
1879
|
-
#[test]
|
|
1880
|
-
fn unknown_lowercase_still_error() {
|
|
1881
|
-
let r = checkSrc("fun f() -> Int =\n nope\n");
|
|
1882
|
-
assert!(r.is_err());
|
|
1883
|
-
assert!(errText(r).contains("undefined name 'nope'"));
|
|
1884
|
-
}
|
|
1885
|
-
|
|
1886
|
-
#[test]
|
|
1887
|
-
fn crash_requires_str() {
|
|
1888
|
-
assert!(checkSrc("fun f() -> Int =\n crash \"boom\"\n").is_ok());
|
|
1889
|
-
let bad = errText(checkSrc("fun f() -> Int =\n crash 5\n"));
|
|
1890
|
-
assert!(bad.contains("Str"), "got: {}", bad);
|
|
1891
|
-
}
|
|
1892
|
-
|
|
1893
390
|
const PERSON: &str = "\
|
|
1894
391
|
enum Person =
|
|
1895
392
|
| Person(name: Str, age: Int)
|
|
1896
393
|
|
|
1897
394
|
";
|
|
1898
395
|
|
|
1899
|
-
#[test]
|
|
1900
|
-
fn nullable_is_a_distinct_optional_type() {
|
|
1901
|
-
// An optional is not an error union (and vice versa).
|
|
1902
|
-
let r = checkSrc(&format!(
|
|
1903
|
-
"{}fun a(p: Person?) -> error{{Bad}}!Person =\n p\n",
|
|
1904
|
-
PERSON
|
|
1905
|
-
));
|
|
1906
|
-
assert!(r.is_err());
|
|
1907
|
-
let r = checkSrc(&format!(
|
|
1908
|
-
"{}fun b(p: error{{Bad}}!Person) -> Person? =\n p\n",
|
|
1909
|
-
PERSON
|
|
1910
|
-
));
|
|
1911
|
-
assert!(r.is_err());
|
|
1912
|
-
}
|
|
1913
|
-
|
|
1914
|
-
#[test]
|
|
1915
|
-
fn null_return_absent() {
|
|
1916
|
-
let r = checkSrc(&format!(
|
|
1917
|
-
"{}fun find(n: Int) -> Person? =\n if n > 0\n return Person(name: \"a\", age: n)\n return null\n",
|
|
1918
|
-
PERSON
|
|
1919
|
-
));
|
|
1920
|
-
assert!(r.is_ok(), "got {:?}", r.err());
|
|
1921
|
-
// `Missing` is an ordinary name: it is not a value.
|
|
1922
|
-
let bad = checkSrc(&format!("{}fun find() -> Person? =\n return Missing\n", PERSON));
|
|
1923
|
-
assert!(bad.is_err());
|
|
1924
|
-
// null needs an optional slot.
|
|
1925
|
-
assert!(checkSrc("fun f() -> Int =\n return null\n").is_err());
|
|
1926
|
-
assert!(checkSrc("fun f() -> !Int =\n return null\n").is_err());
|
|
1927
|
-
assert!(checkSrc("fun f() -> error{Bad}!Int =\n return null\n").is_err());
|
|
1928
|
-
}
|
|
1929
|
-
|
|
1930
|
-
#[test]
|
|
1931
|
-
fn null_without_context_error() {
|
|
1932
|
-
let msg = errText(checkSource(&parseClean("fun f() -> Int =\n x := null\n 1\n")));
|
|
1933
|
-
assert!(msg.contains("cannot infer the type of null"), "{msg}");
|
|
1934
|
-
}
|
|
1935
|
-
|
|
1936
|
-
#[test]
|
|
1937
|
-
fn catch_on_optional_rejected() {
|
|
1938
|
-
let msg = errText(checkSource(&parseClean("fun f(a: Int?) -> Int =\n a catch 0\n")));
|
|
1939
|
-
assert!(msg.contains("catch is for errors; use ?:"), "{msg}");
|
|
1940
|
-
}
|
|
1941
|
-
|
|
1942
|
-
#[test]
|
|
1943
|
-
fn elvis_on_error_rejected() {
|
|
1944
|
-
let msg = errText(checkSource(&parseClean("fun g() -> error{Bad}!Int = 1\n\nfun f() -> Int =\n g() ?: 0\n")));
|
|
1945
|
-
assert!(msg.contains("?: is for optionals; use catch"), "{msg}");
|
|
1946
|
-
}
|
|
1947
|
-
|
|
1948
|
-
#[test]
|
|
1949
|
-
fn try_on_optional_rejected() {
|
|
1950
|
-
let msg = errText(checkSource(&parseClean("fun g() -> Int? = 1\n\nfun f() -> !Int =\n try g()\n")));
|
|
1951
|
-
assert!(msg.contains("try is for errors; use ?: return null"), "{msg}");
|
|
1952
|
-
}
|
|
1953
|
-
|
|
1954
|
-
#[test]
|
|
1955
|
-
fn elvis_and_bindings_typecheck() {
|
|
1956
|
-
let r = checkSrc(
|
|
1957
|
-
"\
|
|
1958
|
-
fun f(a: Int?, b: Int?) -> Int =
|
|
1959
|
-
x := a ?: b ?: 3
|
|
1960
|
-
if y := a
|
|
1961
|
-
return x + y
|
|
1962
|
-
while z := b
|
|
1963
|
-
return z
|
|
1964
|
-
return if w := a then w else 0
|
|
1965
|
-
",
|
|
1966
|
-
);
|
|
1967
|
-
assert!(r.is_ok(), "got {:?}", r.err());
|
|
1968
|
-
// The binding needs an optional.
|
|
1969
|
-
assert!(checkSrc("fun f(a: Int) -> Int =\n if y := a\n return y\n 0\n").is_err());
|
|
1970
|
-
// It is not visible outside its body.
|
|
1971
|
-
assert!(checkSrc("fun f(a: Int?) -> Int =\n if y := a\n return y\n y\n").is_err());
|
|
1972
|
-
}
|
|
1973
|
-
|
|
1974
|
-
#[test]
|
|
1975
|
-
fn error_union_of_optional_nests() {
|
|
1976
|
-
let r = checkSrc(
|
|
1977
|
-
"\
|
|
1978
|
-
fun g(n: Int) -> error{Bad}!Int? =
|
|
1979
|
-
if n < 0
|
|
1980
|
-
return error.Bad
|
|
1981
|
-
if n == 0
|
|
1982
|
-
return null
|
|
1983
|
-
n
|
|
1984
|
-
|
|
1985
|
-
fun f(n: Int) -> error{Bad}!Int =
|
|
1986
|
-
v := try g(n)
|
|
1987
|
-
v ?: 0
|
|
1988
|
-
",
|
|
1989
|
-
);
|
|
1990
|
-
assert!(r.is_ok(), "got {:?}", r.err());
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
|
-
#[test]
|
|
1994
|
-
fn plain_value_is_the_success_value() {
|
|
1995
|
-
let r = checkSrc("fun f(n: Int) -> error{Neg}!Int =\n if n < 0\n return error.Neg\n return n\n\nfun g(n: Int) -> error{Neg}!Int =\n n\n");
|
|
1996
|
-
assert!(r.is_ok(), "got {:?}", r.err());
|
|
1997
|
-
assert!(checkSrc("fun g() -> error{Neg}!Int =\n \"x\"\n").is_err());
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
|
-
#[test]
|
|
2001
|
-
fn safe_nav_chain() {
|
|
2002
|
-
let r = checkSrc(
|
|
2003
|
-
"\
|
|
2004
|
-
enum Target =
|
|
2005
|
-
| Target(name: Str)
|
|
2006
|
-
|
|
2007
|
-
enum User =
|
|
2008
|
-
| User(target: Target?)
|
|
2009
|
-
|
|
2010
|
-
fun f(u: User?) -> Str? =
|
|
2011
|
-
u?.target?.name
|
|
2012
|
-
",
|
|
2013
|
-
);
|
|
2014
|
-
assert!(r.is_ok(), "got {:?}", r.err());
|
|
2015
|
-
}
|
|
2016
|
-
|
|
2017
|
-
#[test]
|
|
2018
|
-
fn try_merges_error_sets() {
|
|
2019
|
-
let r = checkSrc(
|
|
2020
|
-
"\
|
|
2021
|
-
fun a() -> error{Neg}!Int =
|
|
2022
|
-
1
|
|
2023
|
-
|
|
2024
|
-
fun b() -> error{BadNumStr}!Int =
|
|
2025
|
-
1
|
|
2026
|
-
|
|
2027
|
-
fun f() -> error{Neg, BadNumStr}!Int =
|
|
2028
|
-
x := try a()
|
|
2029
|
-
y := try b()
|
|
2030
|
-
x + y
|
|
2031
|
-
",
|
|
2032
|
-
);
|
|
2033
|
-
assert!(r.is_ok(), "got {:?}", r.err());
|
|
2034
|
-
}
|
|
2035
|
-
|
|
2036
|
-
#[test]
|
|
2037
|
-
fn nested_error_union_is_not_wrapped_twice() {
|
|
2038
|
-
// a plain value is the success value exactly once
|
|
2039
|
-
assert!(checkSrc("fun f(i: Int) -> error{Bad}!Int =\n return i\n").is_ok());
|
|
2040
|
-
assert!(checkSrc("fun f() -> error{Bad}!Int =\n return Missing\n").is_err());
|
|
2041
|
-
}
|
|
2042
|
-
|
|
2043
396
|
#[test]
|
|
2044
397
|
fn tag_unions_and_try_are_gone() {
|
|
2045
398
|
// Tag-union types and `T ! [..]` do not parse.
|
|
@@ -2050,64 +403,6 @@ fn tag_unions_and_try_are_gone() {
|
|
|
2050
403
|
}
|
|
2051
404
|
}
|
|
2052
405
|
|
|
2053
|
-
#[test]
|
|
2054
|
-
fn inline_if_branch_type_mismatch() {
|
|
2055
|
-
let src = "fun f(c: Bool) -> Int =\n if c then 1 else \"no\"\n";
|
|
2056
|
-
let msg = errText(checkSource(&parseClean(src)));
|
|
2057
|
-
assert!(msg.contains("inline if branches"), "{}", msg);
|
|
2058
|
-
}
|
|
2059
|
-
|
|
2060
|
-
#[test]
|
|
2061
|
-
fn try_precedence_unary() {
|
|
2062
|
-
let src = "fun f() -> !Int = 2\n\nfun g(n: Int) -> !Int =\n n * try f()\n";
|
|
2063
|
-
let r = checkSource(&parseClean(src));
|
|
2064
|
-
assert!(r.is_ok(), "got {:?}", r.err());
|
|
2065
|
-
}
|
|
2066
|
-
|
|
2067
|
-
const PATTERN_IF_ENUM: &str = "enum Shape =\n | Circle(radius: Int)\n | Rect(w: Int, h: Int)\n | Dot\n\n";
|
|
2068
|
-
|
|
2069
|
-
#[test]
|
|
2070
|
-
fn pattern_bindings_scoped_to_branch() {
|
|
2071
|
-
let ok = format!("{}fun f(s: Shape) -> Int =\n if Circle(r) := s\n return r\n 0\n", PATTERN_IF_ENUM);
|
|
2072
|
-
assert!(checkSource(&parseClean(&ok)).is_ok());
|
|
2073
|
-
let bad = format!("{}fun f(s: Shape) -> Int =\n if Circle(r) := s\n return r\n r\n", PATTERN_IF_ENUM);
|
|
2074
|
-
let msg = errText(checkSource(&parseClean(&bad)));
|
|
2075
|
-
assert!(msg.contains("undefined name"), "{}", msg);
|
|
2076
|
-
let other = format!("{}fun f(s: Shape) -> Int =\n if Circle(r) := s\n return r\n else\n return r\n 0\n", PATTERN_IF_ENUM);
|
|
2077
|
-
assert!(errText(checkSource(&parseClean(&other))).contains("undefined name"));
|
|
2078
|
-
}
|
|
2079
|
-
|
|
2080
|
-
#[test]
|
|
2081
|
-
fn pattern_if_guard_sees_bindings_and_must_be_bool() {
|
|
2082
|
-
let ok = format!("{}fun f(s: Shape) -> Int =\n if Rect(w, h) := s, w > h\n return w\n 0\n", PATTERN_IF_ENUM);
|
|
2083
|
-
assert!(checkSource(&parseClean(&ok)).is_ok());
|
|
2084
|
-
let bad = format!("{}fun f(s: Shape) -> Int =\n if Rect(w, h) := s, w\n return w\n 0\n", PATTERN_IF_ENUM);
|
|
2085
|
-
assert!(checkSource(&parseClean(&bad)).is_err());
|
|
2086
|
-
}
|
|
2087
|
-
|
|
2088
|
-
#[test]
|
|
2089
|
-
fn pattern_if_wrong_arity_and_non_optional_name_are_errors() {
|
|
2090
|
-
let arity = format!("{}fun f(s: Shape) -> Int =\n if Rect(w) := s\n return w\n 0\n", PATTERN_IF_ENUM);
|
|
2091
|
-
assert!(checkSource(&parseClean(&arity)).is_err());
|
|
2092
|
-
let plain = "fun f(n: Int) -> Int =\n if x := n\n return x\n 0\n";
|
|
2093
|
-
assert!(checkSource(&parseClean(plain)).is_err());
|
|
2094
|
-
}
|
|
2095
|
-
|
|
2096
|
-
#[test]
|
|
2097
|
-
fn pattern_if_needs_no_exhaustiveness() {
|
|
2098
|
-
let src = format!("{}fun f(s: Shape) -> Int =\n if Dot := s\n return 1\n 0\n", PATTERN_IF_ENUM);
|
|
2099
|
-
assert!(checkSource(&parseClean(&src)).is_ok());
|
|
2100
|
-
}
|
|
2101
|
-
|
|
2102
|
-
#[test]
|
|
2103
|
-
fn comparing_an_optional_or_error_union_with_a_value_is_rejected() {
|
|
2104
|
-
let msg = errText(checkSource(&parseClean("fun f(a: Int?) -> Bool =\n a == 3\n")));
|
|
2105
|
-
assert!(msg.contains("cannot compare"), "{msg}");
|
|
2106
|
-
let msg = errText(checkSource(&parseClean("fun f(a: error{Bad}!Int) -> Bool =\n a == 3\n")));
|
|
2107
|
-
assert!(msg.contains("cannot compare"), "{msg}");
|
|
2108
|
-
assert!(checkSource(&parseClean("fun f(a: Int?) -> Bool =\n a == null\n")).is_ok());
|
|
2109
|
-
}
|
|
2110
|
-
|
|
2111
406
|
#[test]
|
|
2112
407
|
fn unknown_method_on_heap_const_is_checker_error() {
|
|
2113
408
|
let src = |call: &str| {
|
|
@@ -2134,31 +429,174 @@ fn recovered_call_failure_does_not_reposition_genuine_error() {
|
|
|
2134
429
|
assert_eq!((e.line, e.column), (9, 1), "{:?}", errs);
|
|
2135
430
|
}
|
|
2136
431
|
|
|
432
|
+
|
|
2137
|
-
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
2138
|
-
|
|
434
|
+
// Rejected programs, table-driven by area. Each row is (source, needles): the
|
|
2139
|
-
for src in [
|
|
2140
|
-
|
|
435
|
+
// checker must reject the source and every needle must appear in its messages.
|
|
436
|
+
// Valid programs are covered by the Plum `test` blocks in plum-std/plum-examples.
|
|
437
|
+
// ---------------------------------------------------------------------------
|
|
438
|
+
|
|
2141
|
-
|
|
439
|
+
type Row = (String, &'static [&'static str]);
|
|
2142
|
-
|
|
440
|
+
|
|
2143
|
-
let msg = errText(checkSrc(src));
|
|
2144
|
-
|
|
441
|
+
fn r(src: &str, needles: &'static [&'static str]) -> Row {
|
|
2145
|
-
|
|
442
|
+
(src.to_string(), needles)
|
|
2146
443
|
}
|
|
2147
444
|
|
|
2148
|
-
#[test]
|
|
2149
|
-
fn digit_variant_pattern_still_works() {
|
|
2150
|
-
let src = "enum V2 =\n | A1(n: Int)\n | B2\n\nfun f(w: V2) -> Int =\n match w\n A1(n) => n\n B2 => 0\n\nfun g(w: V2) -> Int =\n if B2 := w\n return 1\n 0\n";
|
|
2151
|
-
|
|
445
|
+
fn assertRejected(rows: Vec<Row>) {
|
|
446
|
+
for (src, needles) in rows {
|
|
447
|
+
let msg = match checkSource(&parseClean(&src)) {
|
|
448
|
+
Ok(()) => panic!("expected the checker to reject:\n{src}"),
|
|
449
|
+
Err(es) => es.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join(" | "),
|
|
450
|
+
};
|
|
451
|
+
for n in needles {
|
|
452
|
+
assert!(msg.contains(n), "{src}\nexpected {n:?} in: {msg}");
|
|
453
|
+
}
|
|
454
|
+
}
|
|
2152
455
|
}
|
|
2153
456
|
|
|
2154
457
|
#[test]
|
|
2155
|
-
fn
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
("
|
|
2159
|
-
("fun
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
458
|
+
fn rejects_basic_expressions_and_calls() {
|
|
459
|
+
let cat = "enum Cat =\n | Cat(name: Str, age: Int)\n\n";
|
|
460
|
+
assertRejected(vec![
|
|
461
|
+
r("test \"bad assert\"\n assert 1 + 2\n", &["assert must be Bool"]),
|
|
462
|
+
r("fun bad() -> Int =\n true\n", &["return type mismatch"]),
|
|
463
|
+
r("fun bad() -> Int =\n false\n", &["return type mismatch"]),
|
|
464
|
+
r("fun bad() -> Int =\n x\n", &["undefined name 'x'"]),
|
|
465
|
+
r("fun bad() -> Int =\n 1 + 2.0\n", &["binary op: type mismatch"]),
|
|
466
|
+
r("fun bad() -> Int =\n self\n", &["undefined name 'self'"]),
|
|
467
|
+
r("fun f() -> Int =\n nope\n", &["undefined name 'nope'"]),
|
|
468
|
+
r("fun f() -> Int =\n crash 5\n", &["Str"]),
|
|
469
|
+
r("fun f(c: Bool) -> Int =\n if c then 1 else \"no\"\n", &["inline if branches"]),
|
|
470
|
+
r("fun f() -> Bool =\n True\n", &["found True"]),
|
|
471
|
+
r("fun f(b: Bool) -> Int =\n match b\n True => 1\n false => 0\n", &["not a variant of Bool"]),
|
|
472
|
+
r("fun main() -> Int =\n total := 0\n for x := range 5.5f\n total = total + 1\n total\n", &["'5.5f' is not a for-form or iterable"]),
|
|
473
|
+
r("fun five() -> Float =\n 5.0f\n\nfun main() -> Int =\n for x := range five()\n x\n 0\n", &["'five()' is not a for-form or iterable"]),
|
|
474
|
+
r("fun a(p: Int) -> Int =\n p\n\nfun main() =\n x := a(\"a\" => 1)\n", &["import std/Map"]),
|
|
475
|
+
r("fun f() -> Bool =\n a := |x| x\n b := |x| x\n a == b\n", &["can't compare functions"]),
|
|
476
|
+
r("fun f(a: fn(Int) -> Int, b: fn(Int) -> Int) -> Bool =\n a == b\n", &["can't compare functions"]),
|
|
477
|
+
r("fun u() =\n return\n\nfun f() -> Bool =\n u() == u()\n", &["can't compare Unit values"]),
|
|
478
|
+
r("fun f(a: Int?) -> Bool =\n a == 3\n", &["cannot compare"]),
|
|
479
|
+
r("fun f(a: error{Bad}!Int) -> Bool =\n a == 3\n", &["cannot compare"]),
|
|
480
|
+
// enum/class fields and constructors
|
|
481
|
+
r(&format!("{cat}fun getAge() -> Int =\n self.age\n"), &["undefined name 'self'"]),
|
|
482
|
+
r("enum Cat =\n | Cat(name: Str)\n\n fun getAge() -> Int =\n self.age\n", &["no field 'age' on type 'Cat'"]),
|
|
483
|
+
r("enum Cat =\n | Cat(name: Str)\n\n fun getAge(self) -> Int =\n self.age\n", &["no field 'age' on type 'Cat'"]),
|
|
484
|
+
r(&format!("{cat}fun makeCat() -> Cat =\n Cat(name: \"x\", age: \"y\")\n"), &["variant 'Cat' field 'age': type mismatch"]),
|
|
485
|
+
r("enum Cat =\n | Cat(name: Str)\n\nfun makeCat() -> Cat =\n Cat(name: \"x\", age: 1)\n", &["unknown field 'age' on variant 'Cat'"]),
|
|
486
|
+
r(&format!("{cat} fun breakCat() =\n self.age = \"oops\"\n"), &["assign 'self.age': type mismatch"]),
|
|
487
|
+
r(&format!("{cat} fun breakCat() =\n self.nope = 1\n"), &["no field 'nope' on type 'Cat'"]),
|
|
488
|
+
r("enum Shape =\n | Square(x: Int, y: Int)\n\nfun bad() -> Shape =\n Square(x: 1)\n", &["variant 'Square' missing field 'y'"]),
|
|
489
|
+
r("enum Shape =\n | Square(x: Int, y: Int)\n\nfun bad() -> Shape =\n Square(x: 1, y: 2, z: 3)\n", &["unknown field 'z' on variant 'Square'"]),
|
|
490
|
+
r("enum Shape =\n | Circle(radius: Int)\n | Square(radius: Int)\n\nfun bad() -> Int =\n r := Circle(radius: 5)\n return r.radius\n", &["field 'radius' is ambiguous across multiple variants of enum 'Shape'"]),
|
|
491
|
+
r("enum Maybe =\n | Just(Int)\n | Nothing\n\nfun bad() -> Maybe =\n Just(\"x\")\n", &["variant 'Just' arg 0: type mismatch"]),
|
|
492
|
+
r("enum Shape =\n | Rect(Float, Float)\n | Circle(Float)\n\nfun bad() -> Shape =\n Rect(1.0)\n", &["variant 'Rect': expected 2 arg(s), got 1"]),
|
|
493
|
+
// enum discriminants
|
|
494
|
+
r("enum Step(n: Int) =\n | ReadMin(0)\n | ReadMax\n", &["expected 1 discriminant value(s), got 0"]),
|
|
495
|
+
r("enum Step(n: Int) =\n | ReadMin(0)\n | ReadMax(\"x\")\n", &["param 'n': type mismatch: expected Int, found Str"]),
|
|
496
|
+
r("enum Maybe =\n | Just(5)\n | Nothing\n", &["declares no params"]),
|
|
497
|
+
// variant as a parameter type rejects a different variant
|
|
498
|
+
r("enum Color =\n | Red\n | Blue\n\nfun stringifyColor(v: Red) -> Int =\n 0\n\nfun useIt() -> Int =\n stringifyColor(Blue)\n", &["expected variant 'Red'"]),
|
|
499
|
+
// variadics
|
|
500
|
+
r("fun sumAll(nums: ...Int) -> Int =\n 0\n\nfun useSumAll() -> Int =\n sumAll(1, \"two\")\n", &["variadic arg 1: type mismatch"]),
|
|
501
|
+
r("fun bad(a: ...Int, b: ...Int) -> Int =\n 0\n", &["at most one variadic parameter is allowed"]),
|
|
502
|
+
r("fun bad(a: ...Int, b: Int) -> Int =\n 0\n", &["a variadic parameter must be last"]),
|
|
503
|
+
// value structs
|
|
504
|
+
r("struct Named(label: Str, n: Int)\n", &["struct field 'label' must be Int, Float, Bool, Byte or a struct"]),
|
|
505
|
+
r("struct Ping(b: Pong)\nstruct Pong(a: Ping)\n", &["cannot contain itself"]),
|
|
506
|
+
]);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
#[test]
|
|
510
|
+
fn rejects_bad_patterns_and_non_exhaustive_matches() {
|
|
511
|
+
let color = "enum Color =\n | Red\n | Green\n | Blue\n\nfun pick() -> Color =\n Red\n\nfun name() -> Str =\n color := pick()\n match color\n";
|
|
512
|
+
let wrap = "enum Inner =\n | Present(Int)\n | Absent\n\nenum Outer =\n | Wrap(Inner)\n | Empty\n\nfun mk() -> Outer =\n Empty\n\nfun name() -> Str =\n o := mk()\n match o\n";
|
|
513
|
+
let shape = "enum Shape =\n | Circle(radius: Int)\n | Rect(w: Int, h: Int)\n | Dot\n\n";
|
|
514
|
+
assertRejected(vec![
|
|
515
|
+
r(&format!("{color} Red => \"r\"\n Green => \"g\"\n"), &["non-exhaustive match on Color: missing Blue"]),
|
|
516
|
+
r(&format!("{wrap} Wrap(Present(v)) => \"p\"\n Empty => \"e\"\n"), &["non-exhaustive match on Inner: missing Absent"]),
|
|
517
|
+
r(&format!("{wrap} Wrap(Present(0)) => \"p\"\n Wrap(Absent) => \"a\"\n Empty => \"e\"\n"), &["non-exhaustive match on Inner: missing Present"]),
|
|
518
|
+
r("fun f(b: Bool) -> Int =\n match b\n true => 1\n", &["non-exhaustive match on Bool: missing false"]),
|
|
519
|
+
r("enum Step(n: Int) =\n | ReadMin(10)\n | ReadMax(20)\n\nfun f(s: Step) -> Int =\n match s\n ReadMin => 1\n", &["non-exhaustive match on Step: missing ReadMax"]),
|
|
520
|
+
r("fun main(a: Str) -> Int =\n match a\n 1 =>\n 1\n _ =>\n 0\n", &["match case: type mismatch: expected Str, found Int"]),
|
|
521
|
+
r("enum Shape =\n | Rect(Float, Float)\n | Circle(Float)\n\nfun bad(s: Shape) -> Float =\n match s\n Rect(w) =>\n w\n _ =>\n 0.0\n", &["expects 2 field(s), got 1"]),
|
|
522
|
+
r("enum Point =\n | Point(x: Int, y: Int)\n\nfun sumPoint(p: Point) -> Int =\n match p\n Point(x) => x\n", &["expects 2 field(s), got 1"]),
|
|
523
|
+
r("fun f(n: Int) -> Int =\n match n\n MAX => 1\n _ => 0\n", &["'MAX' is not a variant of Int"]),
|
|
524
|
+
r("fun f(n: Int) -> Int =\n if MAX := n\n return 1\n 0\n", &["'MAX' is not a variant of Int"]),
|
|
525
|
+
// pattern-if
|
|
526
|
+
r(&format!("{shape}fun f(s: Shape) -> Int =\n if Circle(r) := s\n return r\n r\n"), &["undefined name"]),
|
|
527
|
+
r(&format!("{shape}fun f(s: Shape) -> Int =\n if Circle(r) := s\n return r\n else\n return r\n 0\n"), &["undefined name"]),
|
|
528
|
+
r(&format!("{shape}fun f(s: Shape) -> Int =\n if Rect(w, h) := s, w\n return w\n 0\n"), &["condition after ',' must be Bool"]),
|
|
529
|
+
r(&format!("{shape}fun f(s: Shape) -> Int =\n if Rect(w) := s\n return w\n 0\n"), &["expects 2 field(s), got 1"]),
|
|
530
|
+
r("fun f(n: Int) -> Int =\n if x := n\n return x\n 0\n", &["needs an optional (T?)"]),
|
|
531
|
+
r("fun f(a: Int) -> Int =\n if y := a\n return y\n 0\n", &["needs an optional (T?)"]),
|
|
532
|
+
r("fun f(a: Int?) -> Int =\n if y := a\n return y\n y\n", &["undefined name 'y'"]),
|
|
533
|
+
]);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
#[test]
|
|
537
|
+
fn rejects_bad_generics_and_duck_typed_calls() {
|
|
538
|
+
let duck_missing = format!("{DUCK_DEFS}fun main() -> Plain =\n maxOf(Plain(v: 1), Plain(v: 2))\n");
|
|
539
|
+
let op_missing = format!("{DUCK_DEFS}fun twice(a: T) -> T =\n a + a\n\nfun main() -> Plain =\n twice(Plain(v: 1))\n");
|
|
540
|
+
let generic = |tail: &str| format!("{GENERIC_DEFS}{tail}");
|
|
541
|
+
assertRejected(vec![
|
|
542
|
+
r(&duck_missing, &["called at", "in maxOf[Plain]", "Plain has no method 'lt'"]),
|
|
543
|
+
r(&op_missing, &["in twice[Plain]", "called at", "Plain has no operator '+' (define add)"]),
|
|
544
|
+
r("fun maxOf(a: T, b: T) -> T =\n if a.lt(b) then b else a\n\nfun main() -> Int =\n maxOf(1, 2)\n", &["in maxOf[Int] (called at ", "Int has no method 'lt'"]),
|
|
545
|
+
// a generic body inconsistent with its declared return is rejected once instantiated
|
|
546
|
+
r("fun wrong(x: T) -> Int =\n \"hello\"\n\nfun useIt() -> Str =\n wrong(\"s\")\n", &["in wrong[Str]"]),
|
|
547
|
+
r("fun identity(value: T) -> T =\n value\n\nfun a() -> Str =\n identity(5)\n", &["expected Str, found Int"]),
|
|
548
|
+
r("fun f(x: T) -> Int =\n x\n\nfun main() -> Int =\n f(\"s\")\n", &["in f[Str]"]),
|
|
549
|
+
r("enum Box[T] =\n | Box(value: T)\n\n fun get() -> Int =\n self.value\n\nfun main() -> Int =\n Box(value: \"s\").get()\n", &["in Box.get[Str]"]),
|
|
550
|
+
r(&generic("fun f() -> List[Box[Str]] =\n List(items: Box(value: 1))\n"), &["variant 'Box' field 'value': type mismatch"]),
|
|
551
|
+
r(&generic("fun f() -> List[Str] =\n List(items: 1).map(|x| x + 1)\n"), &["method 'List.map' arg 0: type mismatch"]),
|
|
552
|
+
r(&generic("fun f(xs: List[Int]) -> List[Int] =\n xs.map(|n| \"{n}\")\n"), &["type mismatch"]),
|
|
553
|
+
r("enum Parcel[T] =\n | Delivered(T)\n | Lost\n\nfun contentsOr(p: Parcel, default: Int) -> Int =\n match p\n Delivered(v) => v\n Lost => default\n", &["generic type 'Parcel' needs type arguments, e.g. Parcel[T]"]),
|
|
554
|
+
// error sets are covariant in generic arguments, not contravariant
|
|
555
|
+
r(&generic("fun narrow(b: Box[error{A}!Int]) -> Int =\n 1\n\nfun bad(b: Box[error{A, B}!Int]) -> Int =\n narrow(b)\n"), &["is not in the pinned error set error{A}"]),
|
|
556
|
+
]);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
#[test]
|
|
560
|
+
fn rejects_bad_for_forms_and_fun_stars() {
|
|
561
|
+
let tw = twice();
|
|
562
|
+
assertRejected(vec![
|
|
563
|
+
r("fun* leaky(n: Int) *Int =\n x := 1 + yield n\n x\n\nfun main() -> Int =\n for i := range leaky(3)\n i\n 0\n", &["yield"]),
|
|
564
|
+
r("fun* loopy(n: Int) *Int =\n for i := range loopy(n)\n yield i\n\nfun main() -> Int =\n for i := range loopy(3)\n i\n 0\n", &["fun* 'loopy' can't be recursive"]),
|
|
565
|
+
r("fun* pingF(n: Int) *Int =\n for i := range pongF(n)\n yield i\n\nfun* pongF(n: Int) *Int =\n for i := range pingF(n)\n yield i\n\nfun main() -> Int =\n for i := range pingF(3)\n i\n 0\n", &["can't be recursive"]),
|
|
566
|
+
r(&format!("{tw}fun main() -> Int =\n x := twice(3)\n 0\n"), &["fun* 'twice' can only be used as a for source"]),
|
|
567
|
+
r(&format!("{tw}fun main() -> Int =\n for a, b := range twice(3)\n a\n 0\n"), &["binds 2 names but yields 1", "in for-form 'twice' (used at"]),
|
|
568
|
+
r("fun* pairs(n: Int) *(Int, Int) =\n yield n, n\n\nfun main() -> Int =\n for a := range pairs(3)\n a\n 0\n", &["binds 1 names but yields 2"]),
|
|
569
|
+
r("enum Bag =\n | Bag(n: Int)\n\n fun* pairs(self) *(Int, Int) =\n yield self.n, self.n\n\nfun main() -> Int =\n b := Bag(n: 1)\n for d := range b.pairs()\n d\n 0\n", &["binds 1 names but yields 2"]),
|
|
570
|
+
r("fun f(n: Int) *Int =\n n\n\nfun main() -> Int =\n 0\n", &["*T is only allowed on fun*"]),
|
|
571
|
+
r("fun g(n: Int, src: *Int) -> Int =\n n\n\nfun main() -> Int =\n 0\n", &["*T is only allowed on fun*"]),
|
|
572
|
+
r("fun* f(n: Int) -> Int =\n n\n\nfun main() -> Int =\n 0\n", &["fun* 'f' must declare its yield type (*T)"]),
|
|
573
|
+
r("fun* f(n: Int) *Int =\n return 1\n\nfun main() -> Int =\n 0\n", &["'return' is not allowed"]),
|
|
574
|
+
r("fun main() -> Int =\n yield 1\n 0\n", &["yield is only allowed inside a fun*"]),
|
|
575
|
+
r("fun* broken(n: Int) *Int =\n yield n + nope\n\nfun main() -> Int =\n 0\n", &["fun* 'broken'", "nope"]),
|
|
576
|
+
r(&format!("{tw}fun main() -> Int =\n for i := range twice(4)\n undefinedName\n 0\n"), &["undefinedName"]),
|
|
577
|
+
r("fun* leak(src: *T) *T =\n saved := src\n for x := range saved\n yield x\n\nfun main() -> Int =\n for i := range leak(List(1))\n i\n 0\n", &["sequence parameter 'src' can only be used as a for source"]),
|
|
578
|
+
r("fun* leak(src: *T) *T =\n for x := range src\n yield x\n src\n\nfun main() -> Int =\n for i := range leak(List(1))\n i\n 0\n", &["sequence parameter 'src' can only be used as a for source"]),
|
|
579
|
+
]);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
#[test]
|
|
583
|
+
fn rejects_bad_optionals_and_error_unions() {
|
|
584
|
+
let person = PERSON;
|
|
585
|
+
let base = "fun g(n: Int) -> !Int =\n if n == 0\n return error.A\n if n == 1\n return error.B\n n\n\nfun f(n: Int) -> Int =\n g(n) catch e\n match e\n";
|
|
586
|
+
assertRejected(vec![
|
|
587
|
+
r(&format!("{base} A => 1\n"), &["non-exhaustive match on error{A, B}: missing B"]),
|
|
588
|
+
r(&format!("{base} A => 1\n C => 3\n _ => 2\n"), &["'C' is not in the error set [A, B]"]),
|
|
589
|
+
r("fun f() -> Int =\n x := null\n 1\n", &["cannot infer the type of null"]),
|
|
590
|
+
r("fun f() -> Int =\n return null\n", &["found error{null}"]),
|
|
591
|
+
r("fun f() -> !Int =\n return null\n", &["only an optional (T?) can be null"]),
|
|
592
|
+
r("fun f() -> error{Bad}!Int =\n return null\n", &["only an optional (T?) can be null"]),
|
|
593
|
+
r("fun f(a: Int?) -> Int =\n a catch 0\n", &["catch is for errors; use ?:"]),
|
|
594
|
+
r("fun g() -> error{Bad}!Int = 1\n\nfun f() -> Int =\n g() ?: 0\n", &["?: is for optionals; use catch"]),
|
|
595
|
+
r("fun g() -> Int? = 1\n\nfun f() -> !Int =\n try g()\n", &["try is for errors; use ?: return null"]),
|
|
596
|
+
r(&format!("{person}fun a(p: Person?) -> error{{Bad}}!Person =\n p\n"), &["an optional is handled with"]),
|
|
597
|
+
r(&format!("{person}fun b(p: error{{Bad}}!Person) -> Person? =\n p\n"), &["an error union is handled with"]),
|
|
598
|
+
r(&format!("{person}fun find() -> Person? =\n return Missing\n"), &["expected Person?, found Missing"]),
|
|
599
|
+
r("fun f() -> error{Bad}!Int =\n return Missing\n", &["expected error{Bad}!Int, found Missing"]),
|
|
600
|
+
r("fun g() -> error{Neg}!Int =\n \"x\"\n", &["expected error{Neg}!Int, found Str"]),
|
|
601
|
+
]);
|
|
2164
602
|
}
|
plum-checker/tests/error_union_tests.rs
CHANGED
|
@@ -68,147 +68,6 @@ fun low(n: Int) -> !Int =
|
|
|
68
68
|
assert_eq!(setOf(src, "top"), vec!["Deep"]);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
#[test]
|
|
72
|
-
fn pinned_set_rejects_extra() {
|
|
73
|
-
let src = "\
|
|
74
|
-
fun a(n: Int) -> !Int =
|
|
75
|
-
return error.Negative
|
|
76
|
-
|
|
77
|
-
fun b(n: Int) -> error{Other}!Int =
|
|
78
|
-
try a(n)
|
|
79
|
-
";
|
|
80
|
-
let msg = check(src).unwrap_err();
|
|
81
|
-
assert!(msg.contains("'Negative' is not in the pinned error set error{Other}"), "{}", msg);
|
|
82
|
-
let ok = "\
|
|
83
|
-
fun a(n: Int) -> error{Negative}!Int =
|
|
84
|
-
return error.Negative
|
|
85
|
-
|
|
86
|
-
fun b(n: Int) -> error{Negative, Other}!Int =
|
|
87
|
-
try a(n)
|
|
88
|
-
";
|
|
89
|
-
assert!(check(ok).is_ok(), "{:?}", check(ok));
|
|
90
|
-
assert!(check("fun c() -> error{A}!Int =\n return error.B\n").unwrap_err().contains("'B' is not in the pinned"));
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
#[test]
|
|
94
|
-
fn recursive_needs_pinned() {
|
|
95
|
-
let src = "\
|
|
96
|
-
fun fact(n: Int) -> !Int =
|
|
97
|
-
if n < 0
|
|
98
|
-
return error.Negative
|
|
99
|
-
if n == 0
|
|
100
|
-
return 1
|
|
101
|
-
n * {try fact(n - 1)}
|
|
102
|
-
";
|
|
103
|
-
let msg = check(src).unwrap_err();
|
|
104
|
-
assert!(msg.contains("fn 'fact' is recursive: pin its error set (-> error{..}!T)"), "{}", msg);
|
|
105
|
-
let pinned = src.replace("-> !Int", "-> error{Negative}!Int");
|
|
106
|
-
assert!(check(&pinned).is_ok(), "{:?}", check(&pinned));
|
|
107
|
-
// Mutual recursion.
|
|
108
|
-
let mutual = "\
|
|
109
|
-
fun even(n: Int) -> !Bool =
|
|
110
|
-
if n == 0
|
|
111
|
-
return true
|
|
112
|
-
try odd(n - 1)
|
|
113
|
-
|
|
114
|
-
fun odd(n: Int) -> !Bool =
|
|
115
|
-
if n == 0
|
|
116
|
-
return false
|
|
117
|
-
try even(n - 1)
|
|
118
|
-
";
|
|
119
|
-
let msg = check(mutual).unwrap_err();
|
|
120
|
-
assert!(msg.contains("fn 'even' is recursive") && msg.contains("fn 'odd' is recursive"), "{}", msg);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
#[test]
|
|
124
|
-
fn extern_needs_pinned() {
|
|
125
|
-
let msg = check("extern fun host(n: Int) -> !Int\n").unwrap_err();
|
|
126
|
-
assert!(msg.contains("extern fn 'host' must pin its error set"), "{}", msg);
|
|
127
|
-
assert!(check("extern fun host(n: Int) -> error{HostFailed}!Int\n").is_ok());
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
#[test]
|
|
131
|
-
fn catch_default_type() {
|
|
132
|
-
let ok = "\
|
|
133
|
-
fun f() -> !Int =
|
|
134
|
-
return error.A
|
|
135
|
-
|
|
136
|
-
fun g() -> Int =
|
|
137
|
-
f() catch 0
|
|
138
|
-
";
|
|
139
|
-
assert!(check(ok).is_ok(), "{:?}", check(ok));
|
|
140
|
-
let bad = "\
|
|
141
|
-
fun f() -> !Int =
|
|
142
|
-
return error.A
|
|
143
|
-
|
|
144
|
-
fun g() -> Int =
|
|
145
|
-
f() catch \"zero\"
|
|
146
|
-
";
|
|
147
|
-
let msg = check(bad).unwrap_err();
|
|
148
|
-
assert!(msg.contains("catch default") && msg.contains("mismatch"), "{}", msg);
|
|
149
|
-
// Not an error union.
|
|
150
|
-
let msg = check("fun g(n: Int) -> Int =\n n catch 0\n").unwrap_err();
|
|
151
|
-
assert!(msg.contains("'catch' requires an error union"), "{}", msg);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
#[test]
|
|
155
|
-
fn unhandled_union_is_a_mismatch() {
|
|
156
|
-
let src = "\
|
|
157
|
-
fun f() -> !Int =
|
|
158
|
-
return error.A
|
|
159
|
-
|
|
160
|
-
fun g() -> Int =
|
|
161
|
-
f()
|
|
162
|
-
";
|
|
163
|
-
let msg = check(src).unwrap_err();
|
|
164
|
-
assert!(msg.contains("handle it with `try`, `!`, `catch` or `?:`"), "{}", msg);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
#[test]
|
|
168
|
-
fn catch_block_exhaustive_match() {
|
|
169
|
-
let base = "\
|
|
170
|
-
fun f(n: Int) -> !Int =
|
|
171
|
-
if n < 0
|
|
172
|
-
return error.Negative
|
|
173
|
-
if n > 9
|
|
174
|
-
return error.TooBig
|
|
175
|
-
n
|
|
176
|
-
|
|
177
|
-
";
|
|
178
|
-
let ok = format!("{}{}", base, "\
|
|
179
|
-
fun g(n: Int) -> Int =
|
|
180
|
-
f(n) catch e
|
|
181
|
-
match e
|
|
182
|
-
Negative => 0
|
|
183
|
-
TooBig => 9
|
|
184
|
-
");
|
|
185
|
-
assert!(check(&ok).is_ok(), "{:?}", check(&ok));
|
|
186
|
-
let missing = format!("{}{}", base, "\
|
|
187
|
-
fun g(n: Int) -> Int =
|
|
188
|
-
f(n) catch e
|
|
189
|
-
match e
|
|
190
|
-
Negative => 0
|
|
191
|
-
");
|
|
192
|
-
let msg = check(&missing).unwrap_err();
|
|
193
|
-
assert!(msg.contains("non-exhaustive") && msg.contains("TooBig"), "{}", msg);
|
|
194
|
-
// A diverging block, and a non-diverging one that yields no value.
|
|
195
|
-
let div = format!("{}{}", base, "\
|
|
196
|
-
fun g(n: Int) -> Int =
|
|
197
|
-
x := f(n) catch e
|
|
198
|
-
return 0
|
|
199
|
-
x
|
|
200
|
-
");
|
|
201
|
-
assert!(check(&div).is_ok(), "{:?}", check(&div));
|
|
202
|
-
let noval = format!("{}{}", base, "\
|
|
203
|
-
fun g(n: Int) -> Int =
|
|
204
|
-
x := f(n) catch e
|
|
205
|
-
y := 1
|
|
206
|
-
x
|
|
207
|
-
");
|
|
208
|
-
let msg = check(&noval).unwrap_err();
|
|
209
|
-
assert!(msg.contains("catch block must end with a value"), "{}", msg);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
71
|
#[test]
|
|
213
72
|
fn catch_return_error_maps() {
|
|
214
73
|
let src = "\
|
|
@@ -225,35 +84,6 @@ fun f(s: Str) -> !Int =
|
|
|
225
84
|
assert!(msg.contains("Int"), "{}", msg);
|
|
226
85
|
}
|
|
227
86
|
|
|
228
|
-
#[test]
|
|
229
|
-
fn catch_bare_error_value_is_type_error() {
|
|
230
|
-
// A bare `error.X` default is an ordinary value: it must have the success type.
|
|
231
|
-
let src = "\
|
|
232
|
-
fun parse(s: Str) -> !Int =
|
|
233
|
-
return error.BadNum
|
|
234
|
-
|
|
235
|
-
fun f(s: Str) -> !Int =
|
|
236
|
-
x := parse(s) catch error.InvalidFirst
|
|
237
|
-
x
|
|
238
|
-
";
|
|
239
|
-
let msg = check(src).unwrap_err();
|
|
240
|
-
assert!(msg.contains("catch default"), "{}", msg);
|
|
241
|
-
assert!(msg.contains("Int"), "{}", msg);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
#[test]
|
|
245
|
-
fn catch_crash_default() {
|
|
246
|
-
let src = "\
|
|
247
|
-
fun parse(s: Str) -> !Int =
|
|
248
|
-
return error.BadNum
|
|
249
|
-
|
|
250
|
-
fun f(s: Str) -> Int =
|
|
251
|
-
x := parse(s) catch crash \"no\"
|
|
252
|
-
x + 1
|
|
253
|
-
";
|
|
254
|
-
check(src).unwrap();
|
|
255
|
-
}
|
|
256
|
-
|
|
257
87
|
#[test]
|
|
258
88
|
fn bang_not_in_error_set() {
|
|
259
89
|
let src = "\
|
|
@@ -284,85 +114,6 @@ fn no_payload_errors() {
|
|
|
284
114
|
assert!(rejected, "a payload on an error value must be rejected");
|
|
285
115
|
}
|
|
286
116
|
|
|
287
|
-
#[test]
|
|
288
|
-
fn catch_e_name() {
|
|
289
|
-
let src = "\
|
|
290
|
-
fun f() -> !Int =
|
|
291
|
-
return error.NotFound
|
|
292
|
-
|
|
293
|
-
fun g() -> Str =
|
|
294
|
-
f() catch e
|
|
295
|
-
return e.name()
|
|
296
|
-
\"ok\"
|
|
297
|
-
";
|
|
298
|
-
assert!(check(src).is_ok(), "{:?}", check(src));
|
|
299
|
-
let src2 = "\
|
|
300
|
-
fun f() -> !Int =
|
|
301
|
-
return error.NotFound
|
|
302
|
-
|
|
303
|
-
fun g() -> Int =
|
|
304
|
-
f() catch e
|
|
305
|
-
e.name()
|
|
306
|
-
";
|
|
307
|
-
let msg = check(src2).unwrap_err();
|
|
308
|
-
assert!(msg.contains("mismatch"), "{}", msg);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
#[test]
|
|
312
|
-
fn optional_elvis_and_bang() {
|
|
313
|
-
let src = String::from("\
|
|
314
|
-
fun find(n: Int) -> Str? =
|
|
315
|
-
if n > 0
|
|
316
|
-
return \"found\"
|
|
317
|
-
return null
|
|
318
|
-
|
|
319
|
-
fun a(n: Int) -> Str =
|
|
320
|
-
find(n) ?: \"none\"
|
|
321
|
-
|
|
322
|
-
fun b(n: Int) -> Str =
|
|
323
|
-
find(n)!
|
|
324
|
-
|
|
325
|
-
fun c(n: Int) -> Str? =
|
|
326
|
-
s := find(n) ?: return null
|
|
327
|
-
s
|
|
328
|
-
");
|
|
329
|
-
assert!(check(&src).is_ok(), "{:?}", check(&src));
|
|
330
|
-
// `try` on an optional is a type error.
|
|
331
|
-
let src = String::from("\
|
|
332
|
-
fun find(n: Int) -> Str? =
|
|
333
|
-
return null
|
|
334
|
-
|
|
335
|
-
fun d(n: Int) -> !Str =
|
|
336
|
-
try find(n)
|
|
337
|
-
");
|
|
338
|
-
assert!(check(&src).unwrap_err().contains("try is for errors"));
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
#[test]
|
|
342
|
-
fn safe_nav_on_error_union() {
|
|
343
|
-
let src = "\
|
|
344
|
-
record User(name: Str)
|
|
345
|
-
|
|
346
|
-
fun load(n: Int) -> !User =
|
|
347
|
-
return error.NoUser
|
|
348
|
-
|
|
349
|
-
fun name(n: Int) -> !Str =
|
|
350
|
-
load(n)?.name
|
|
351
|
-
";
|
|
352
|
-
// Same set; `?.` flattens.
|
|
353
|
-
assert!(check(src).is_ok(), "{:?}", check(src));
|
|
354
|
-
let src2 = "\
|
|
355
|
-
record User(name: Str)
|
|
356
|
-
|
|
357
|
-
fun load(n: Int) -> !User =
|
|
358
|
-
return error.NoUser
|
|
359
|
-
|
|
360
|
-
fun name(n: Int) -> Str =
|
|
361
|
-
load(n)?.name catch \"anon\"
|
|
362
|
-
";
|
|
363
|
-
assert!(check(src2).is_ok(), "{:?}", check(src2));
|
|
364
|
-
}
|
|
365
|
-
|
|
366
117
|
#[test]
|
|
367
118
|
fn pinned_set_vs_generic_unambiguous() {
|
|
368
119
|
// Generics next to a pinned set: the set comes first, in front of the `!`.
|
|
@@ -404,27 +155,6 @@ fun f() -> !Int =
|
|
|
404
155
|
}
|
|
405
156
|
}
|
|
406
157
|
|
|
407
|
-
#[test]
|
|
408
|
-
fn null_is_absent_anywhere() {
|
|
409
|
-
// Constructor argument of type `Int?`: not an early return, and legal in a
|
|
410
|
-
// fn that returns a plain value.
|
|
411
|
-
let src = "\
|
|
412
|
-
record Cfg(port: Int?)
|
|
413
|
-
|
|
414
|
-
fun mk() -> Cfg =
|
|
415
|
-
Cfg(port: null)
|
|
416
|
-
";
|
|
417
|
-
assert_eq!(check(src), Ok(()));
|
|
418
|
-
// A bare variable holds no type for null.
|
|
419
|
-
assert!(check("fun f() -> Int =\n x := null\n 1\n").unwrap_err().contains("cannot infer the type of null"));
|
|
420
|
-
// But it does not fit an `Int` slot.
|
|
421
|
-
let bad = "fun f() -> Int =\n return null\n";
|
|
422
|
-
assert!(check(bad).unwrap_err().contains("type mismatch"));
|
|
423
|
-
// Only `null` fits a `T?`.
|
|
424
|
-
assert!(check("fun f() -> Int? =\n return error.Other\n").is_err());
|
|
425
|
-
assert_eq!(check("fun f() -> Int? =\n return null\n"), Ok(()));
|
|
426
|
-
}
|
|
427
|
-
|
|
428
158
|
#[test]
|
|
429
159
|
fn error_value_returns_failure() {
|
|
430
160
|
let src = "\
|
|
@@ -445,25 +175,6 @@ fun g() -> !Int =
|
|
|
445
175
|
assert_eq!(check(via), Ok(()));
|
|
446
176
|
}
|
|
447
177
|
|
|
448
|
-
#[test]
|
|
449
|
-
fn fail_removed() {
|
|
450
|
-
let msg = check("fun f() -> !Int =\n fail Bad\n 1\n").unwrap_err();
|
|
451
|
-
assert!(msg.contains("undefined name 'fail'"), "{}", msg);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
#[test]
|
|
455
|
-
fn old_bracket_pin_removed() {
|
|
456
|
-
// `!Int [A]` is now just `Int[A]` generics, not a pinned set.
|
|
457
|
-
assert!(check("fun f() -> !Int [A] =\n return error.A\n").is_err());
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
#[test]
|
|
461
|
-
fn inline_set_pins() {
|
|
462
|
-
assert_eq!(check("fun f() -> error{A, B}!Int =\n return error.A\n"), Ok(()));
|
|
463
|
-
let msg = check("fun f() -> error{A}!Int =\n return error.B\n").unwrap_err();
|
|
464
|
-
assert!(msg.contains("'B' is not in the pinned error set error{A}"), "{}", msg);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
178
|
#[test]
|
|
468
179
|
fn named_set_pins() {
|
|
469
180
|
let src = "\
|
|
@@ -506,89 +217,6 @@ fun g() -> (A1 || error{Dd})!Int =
|
|
|
506
217
|
assert_eq!(setOf(src, "g"), vec!["Aa", "Bb", "Dd"]);
|
|
507
218
|
}
|
|
508
219
|
|
|
509
|
-
#[test]
|
|
510
|
-
fn pinned_rejects_extra() {
|
|
511
|
-
let src = "\
|
|
512
|
-
error Small = Aa
|
|
513
|
-
|
|
514
|
-
fun a() -> error{Aa, Bb}!Int =
|
|
515
|
-
return error.Aa
|
|
516
|
-
|
|
517
|
-
fun b() -> Small!Int =
|
|
518
|
-
try a()
|
|
519
|
-
";
|
|
520
|
-
let msg = check(src).unwrap_err();
|
|
521
|
-
assert!(msg.contains("'Bb' is not in the pinned error set"), "{}", msg);
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
#[test]
|
|
525
|
-
fn match_bare_names() {
|
|
526
|
-
let base = "\
|
|
527
|
-
error Two = Aa | Bb
|
|
528
|
-
|
|
529
|
-
fun f(e: Two) -> Int =
|
|
530
|
-
match e
|
|
531
|
-
{}";
|
|
532
|
-
assert_eq!(check(&base.replace("{}", " Aa => 1\n Bb => 2\n")), Ok(()));
|
|
533
|
-
let msg = check(&base.replace("{}", " Aa => 1\n")).unwrap_err();
|
|
534
|
-
assert!(msg.contains("missing Bb"), "{}", msg);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
#[test]
|
|
538
|
-
fn match_error_prefix_accepted() {
|
|
539
|
-
let base = "\
|
|
540
|
-
error Two = Aa | Bb
|
|
541
|
-
|
|
542
|
-
fun f(e: Two) -> Int =
|
|
543
|
-
match e
|
|
544
|
-
{}";
|
|
545
|
-
assert_eq!(check(&base.replace("{}", " error.Aa => 1\n Bb => 2\n")), Ok(()));
|
|
546
|
-
let msg = check(&base.replace("{}", " error.Aa => 1\n error.Zz => 2\n _ => 3\n")).unwrap_err();
|
|
547
|
-
assert!(msg.contains("'Zz' is not in the error set"), "{}", msg);
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
#[test]
|
|
551
|
-
fn duplicate_error_set_name() {
|
|
552
|
-
let msg = check("error Dup = Aa\nerror Dup = Bb\n\nfun f() -> Int =\n 1\n").unwrap_err();
|
|
553
|
-
assert!(msg.contains("duplicate error set 'Dup'"), "{}", msg);
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
#[test]
|
|
557
|
-
fn match_arm_names_must_be_in_the_error_set() {
|
|
558
|
-
let base = "\
|
|
559
|
-
fun f(n: Int) -> !Int =
|
|
560
|
-
if n < 0
|
|
561
|
-
return error.Negative
|
|
562
|
-
if n > 9
|
|
563
|
-
return error.TooBig
|
|
564
|
-
n
|
|
565
|
-
|
|
566
|
-
";
|
|
567
|
-
let bad = format!("{}{}", base, "\
|
|
568
|
-
fun g(n: Int) -> Int =
|
|
569
|
-
f(n) catch e
|
|
570
|
-
match e
|
|
571
|
-
Negative => 0
|
|
572
|
-
TooBig => 9
|
|
573
|
-
Bogus => 1
|
|
574
|
-
");
|
|
575
|
-
let msg = check(&bad).unwrap_err();
|
|
576
|
-
assert!(msg.contains("'Bogus' is not in the error set [Negative, TooBig]"), "{}", msg);
|
|
577
|
-
// An empty set (a union that can't fail) accepts no arm names either.
|
|
578
|
-
let empty = "\
|
|
579
|
-
fun safe(n: Int) -> !Int =
|
|
580
|
-
n
|
|
581
|
-
|
|
582
|
-
fun g(n: Int) -> Int =
|
|
583
|
-
safe(n) catch e
|
|
584
|
-
match e
|
|
585
|
-
Anything => 0
|
|
586
|
-
_ => 1
|
|
587
|
-
";
|
|
588
|
-
let msg = check(empty).unwrap_err();
|
|
589
|
-
assert!(msg.contains("'Anything' is not in the error set []"), "{}", msg);
|
|
590
|
-
}
|
|
591
|
-
|
|
592
220
|
#[test]
|
|
593
221
|
fn try_inside_star_fn_has_a_specific_message() {
|
|
594
222
|
let src = "\
|
|
@@ -610,15 +238,50 @@ fun* gen(n: Int) *Int =
|
|
|
610
238
|
assert!(!msg.contains("declare `-> !T`"), "{}", msg);
|
|
611
239
|
}
|
|
612
240
|
|
|
613
|
-
#[test]
|
|
614
|
-
fn undefined_pair_hint() {
|
|
615
|
-
let src = "\
|
|
616
|
-
fun f(a: Int) -> Int =
|
|
617
|
-
a
|
|
618
241
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
242
|
+
/// Rejected programs: every row must fail the checker with each needle in its messages.
|
|
243
|
+
/// (Valid error-union programs are covered by the Plum tests in plum-std.)
|
|
244
|
+
#[test]
|
|
245
|
+
fn rejects_bad_error_unions() {
|
|
246
|
+
let fneg = "fun f(n: Int) -> !Int =\n if n < 0\n return error.Negative\n if n > 9\n return error.TooBig\n n\n\n";
|
|
247
|
+
let two = "error Two = Aa | Bb\n\nfun f(e: Two) -> Int =\n match e\n";
|
|
248
|
+
let rows: Vec<(String, &[&str])> = vec![
|
|
249
|
+
// pinned sets
|
|
250
|
+
("fun a(n: Int) -> !Int =\n return error.Negative\n\nfun b(n: Int) -> error{Other}!Int =\n try a(n)\n".into(), &["'Negative' is not in the pinned error set error{Other}"]),
|
|
251
|
+
("fun c() -> error{A}!Int =\n return error.B\n".into(), &["'B' is not in the pinned"]),
|
|
252
|
+
("error Small = Aa\n\nfun a() -> error{Aa, Bb}!Int =\n return error.Aa\n\nfun b() -> Small!Int =\n try a()\n".into(), &["'Bb' is not in the pinned error set"]),
|
|
253
|
+
("fun f() -> Nope!Int =\n 1\n".into(), &["unknown error set 'Nope'"]),
|
|
254
|
+
("error Dup = Aa\nerror Dup = Bb\n\nfun f() -> Int =\n 1\n".into(), &["duplicate error set 'Dup'"]),
|
|
255
|
+
("extern fun host(n: Int) -> !Int\n".into(), &["extern fn 'host' must pin its error set"]),
|
|
256
|
+
("fun fact(n: Int) -> !Int =\n if n < 0\n return error.Negative\n if n == 0\n return 1\n n * {try fact(n - 1)}\n".into(), &["fn 'fact' is recursive: pin its error set (-> error{..}!T)"]),
|
|
257
|
+
("fun even(n: Int) -> !Bool =\n if n == 0\n return true\n try odd(n - 1)\n\nfun odd(n: Int) -> !Bool =\n if n == 0\n return false\n try even(n - 1)\n".into(), &["fn 'even' is recursive", "fn 'odd' is recursive"]),
|
|
258
|
+
// catch / try / !
|
|
259
|
+
("fun f() -> !Int =\n return error.A\n\nfun g() -> Int =\n f() catch \"zero\"\n".into(), &["catch default", "mismatch"]),
|
|
260
|
+
("fun g(n: Int) -> Int =\n n catch 0\n".into(), &["'catch' requires an error union"]),
|
|
261
|
+
("fun parse(s: Str) -> !Int =\n return error.BadNum\n\nfun f(s: Str) -> !Int =\n x := parse(s) catch error.InvalidFirst\n x\n".into(), &["catch default", "Int"]),
|
|
262
|
+
("fun f() -> !Int =\n return error.A\n\nfun g() -> Int =\n f()\n".into(), &["handle it with `try`, `!`, `catch` or `?:`"]),
|
|
263
|
+
(format!("{fneg}fun g(n: Int) -> Int =\n f(n) catch e\n match e\n Negative => 0\n"), &["non-exhaustive", "TooBig"]),
|
|
264
|
+
(format!("{fneg}fun g(n: Int) -> Int =\n x := f(n) catch e\n y := 1\n x\n"), &["catch block must end with a value"]),
|
|
265
|
+
(format!("{fneg}fun g(n: Int) -> Int =\n f(n) catch e\n match e\n Negative => 0\n TooBig => 9\n Bogus => 1\n"), &["'Bogus' is not in the error set [Negative, TooBig]"]),
|
|
266
|
+
("fun safe(n: Int) -> !Int =\n n\n\nfun g(n: Int) -> Int =\n safe(n) catch e\n match e\n Anything => 0\n _ => 1\n".into(), &["'Anything' is not in the error set []"]),
|
|
267
|
+
("fun f() -> !Int =\n return error.NotFound\n\nfun g() -> Int =\n f() catch e\n e.name()\n".into(), &["mismatch"]),
|
|
268
|
+
("fun g(n: Int) -> Int =\n n!\n".into(), &["'!' requires an error union"]),
|
|
269
|
+
// error values and optionals
|
|
270
|
+
("fun f() -> Int =\n return error.X\n".into(), &["found error{X}"]),
|
|
271
|
+
("fun f() -> Int =\n return null\n".into(), &["type mismatch"]),
|
|
272
|
+
("fun f() -> Int? =\n return error.Other\n".into(), &["expected Int?, found error{Other}"]),
|
|
273
|
+
("fun f() -> !Int =\n fail Bad\n 1\n".into(), &["undefined name 'fail'"]),
|
|
274
|
+
("fun f() -> !Int [A] =\n return error.A\n".into(), &["pin an error set in front of the `!`"]),
|
|
275
|
+
// matching on a named error set
|
|
276
|
+
(format!("{two} Aa => 1\n"), &["missing Bb"]),
|
|
277
|
+
(format!("{two} error.Aa => 1\n error.Zz => 2\n _ => 3\n"), &["'Zz' is not in the error set"]),
|
|
278
|
+
// misc hints
|
|
279
|
+
("fun f(a: Int) -> Int =\n a\n\nfun main() =\n x := f(\"a\" => 1)\n".into(), &["import std/Map"]),
|
|
280
|
+
];
|
|
281
|
+
for (src, needles) in rows {
|
|
282
|
+
let msg = check(&src).expect_err(&format!("expected the checker to reject:\n{src}"));
|
|
283
|
+
for n in needles {
|
|
284
|
+
assert!(msg.contains(n), "{src}\nexpected {n:?} in: {msg}");
|
|
285
|
+
}
|
|
286
|
+
}
|
|
624
287
|
}
|
plum-checker/tests/examples_test.rs
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
#![allow(non_snake_case)]
|
|
2
|
-
use plum_checker::checkSource;
|
|
3
|
-
|
|
4
|
-
fn examplesDir() -> std::path::PathBuf {
|
|
5
|
-
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../plum-examples")
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
fn libPath() -> std::path::PathBuf {
|
|
9
|
-
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
fn exampleFiles() -> Vec<std::path::PathBuf> {
|
|
13
|
-
let mut files: Vec<_> = std::fs::read_dir(examplesDir())
|
|
14
|
-
.expect("plum-examples/ directory should exist")
|
|
15
|
-
.filter_map(|e| e.ok())
|
|
16
|
-
.map(|e| e.path())
|
|
17
|
-
.filter(|p| p.extension().and_then(|e| e.to_str()) == Some("plum"))
|
|
18
|
-
.collect();
|
|
19
|
-
files.sort();
|
|
20
|
-
assert!(
|
|
21
|
-
!files.is_empty(),
|
|
22
|
-
"plum-examples/ should contain at least one .plum file"
|
|
23
|
-
);
|
|
24
|
-
files
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/// Every example must load (resolving its own `import`s against the repo root,
|
|
28
|
-
/// the real compilation path — several examples now genuinely `import` real
|
|
29
|
-
/// stdlib types like `std/Bool`/`std/Str`, so a bare single-file parse would
|
|
30
|
-
/// leave those names undeclared) and pass `checkSource`. These files exist
|
|
31
|
-
/// specifically to prove each piece of the currently-supported grammar
|
|
32
|
-
/// surface actually works end to end, not just in isolated unit tests.
|
|
33
|
-
#[test]
|
|
34
|
-
fn everyExampleParsesAndTypeChecks() {
|
|
35
|
-
for path in exampleFiles() {
|
|
36
|
-
let source = plum_core::loadAndMerge(&path, &libPath())
|
|
37
|
-
.unwrap_or_else(|e| panic!("failed to load {}: {}", path.display(), e));
|
|
38
|
-
let result = checkSource(&source);
|
|
39
|
-
assert!(
|
|
40
|
-
result.is_ok(),
|
|
41
|
-
"{} failed type checking: {:?}",
|
|
42
|
-
path.display(),
|
|
43
|
-
result
|
|
44
|
-
.err()
|
|
45
|
-
.map(|errs| errs.into_iter().map(|e| e.message).collect::<Vec<_>>())
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
}
|
plum-checker/tests/primitive_generics_test.rs
CHANGED
|
@@ -22,17 +22,6 @@ fn primitiveGenericsAreNotPseudoVariants() {
|
|
|
22
22
|
assert!(bad.is_empty(), "pseudo-entries in enum_variants: {bad:?}");
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
#[test]
|
|
26
|
-
fn arrayIntMethodsStillTypeCheck() {
|
|
27
|
-
let source = load(
|
|
28
|
-
"b",
|
|
29
|
-
"import std/Array\nimport std/Int\n\nfun main() -> Int =\n a := Array[Int]().push(3).push(4)\n return a.get(1) + a.length()\n",
|
|
30
|
-
);
|
|
31
|
-
if let Err(errs) = checkSource(&source) {
|
|
32
|
-
panic!("{:?}", errs.iter().map(|e| e.message.clone()).collect::<Vec<_>>());
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
25
|
#[test]
|
|
37
26
|
fn arrayElementTypeIsEnforced() {
|
|
38
27
|
let source = load(
|
plum-checker/tests/unions_coerce_test.rs
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
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
|
-
record Circle(r: Float)
|
|
8
|
-
record Square(s: Float)
|
|
9
|
-
record Oval(a: Float)
|
|
10
|
-
enum Shape = Circle | Square
|
|
11
|
-
enum Round = Circle | Oval
|
|
12
|
-
enum Json = Float | Str
|
|
13
|
-
enum Tree[T] = Node[T] | Leaf
|
|
14
|
-
record Node[T](v: T)
|
|
15
|
-
";
|
|
16
|
-
|
|
17
|
-
fn run(body: &str) -> (Result<(), Vec<String>>, UnionPlan) {
|
|
18
|
-
let src = format!("{DEFS}\n{body}\n");
|
|
19
|
-
let mut parser = tree_sitter::Parser::new();
|
|
20
|
-
parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap();
|
|
21
|
-
let tree = parser.parse(&src, None).unwrap();
|
|
22
|
-
assert!(!tree.root_node().has_error(), "parse errors:\n{src}");
|
|
23
|
-
let mut source = AstParser::new(&src).parseSource(tree.root_node());
|
|
24
|
-
let errs = resolveMembers(&mut source);
|
|
25
|
-
assert!(errs.is_empty(), "{errs:?}");
|
|
26
|
-
beginPlan();
|
|
27
|
-
let r = checkSource(&source).map_err(|e| e.into_iter().map(|x| x.message).collect());
|
|
28
|
-
(r, takePlan())
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
fn wraps(p: &UnionPlan) -> Vec<String> {
|
|
32
|
-
let mut v: Vec<String> = p.wraps.values().cloned().collect();
|
|
33
|
-
v.sort();
|
|
34
|
-
v
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
#[test]
|
|
38
|
-
fn literal_into_json() {
|
|
39
|
-
let (r, p) = run("fun f() -> Json =\n 3.0");
|
|
40
|
-
r.unwrap();
|
|
41
|
-
assert_eq!(wraps(&p), vec!["Json__Float"]);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
#[test]
|
|
45
|
-
fn wrap_at_arg_return_push() {
|
|
46
|
-
let (r, p) = run("fun take(s: Shape) -> Float =\n 1.0\n\nfun go() -> Float =\n take(Circle(r: 1.0))");
|
|
47
|
-
r.unwrap();
|
|
48
|
-
assert_eq!(wraps(&p), vec!["Shape__Circle"]);
|
|
49
|
-
let (r, p) = run("fun mk() -> Shape =\n Square(s: 2.0)");
|
|
50
|
-
r.unwrap();
|
|
51
|
-
assert_eq!(wraps(&p), vec!["Shape__Square"]);
|
|
52
|
-
let (r, p) = run("fun many(xs: ...Shape) -> Int =\n 1\n\nfun go() -> Int =\n many(Circle(r: 1.0), Square(s: 1.0))");
|
|
53
|
-
r.unwrap();
|
|
54
|
-
assert_eq!(wraps(&p), vec!["Shape__Circle", "Shape__Square"]);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
#[test]
|
|
58
|
-
fn wrap_reassign_and_generic() {
|
|
59
|
-
let (r, p) = run("fun pick() -> Shape =\n Circle(r: 1.0)\n\nfun go() -> Int =\n s := pick()\n s = Square(s: 1.0)\n 1");
|
|
60
|
-
r.unwrap();
|
|
61
|
-
assert_eq!(wraps(&p), vec!["Shape__Circle", "Shape__Square"]);
|
|
62
|
-
let (r, p) = run("fun mk() -> Tree[Int] =\n Node(v: 3)");
|
|
63
|
-
r.unwrap();
|
|
64
|
-
assert_eq!(wraps(&p), vec!["Tree__Node"]);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
#[test]
|
|
68
|
-
fn join_if_branches() {
|
|
69
|
-
let (r, p) = run("fun go(c: Bool) -> Int =\n x := if c then Circle(r: 1.0) else Square(s: 1.0)\n 1");
|
|
70
|
-
r.unwrap();
|
|
71
|
-
assert_eq!(wraps(&p), vec!["Shape__Circle", "Shape__Square"]);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
#[test]
|
|
75
|
-
fn join_ambiguous() {
|
|
76
|
-
let (r, _) = run("fun go(c: Bool) -> Int =\n x := if c then Square(s: 1.0) else Oval(a: 1.0)\n 1");
|
|
77
|
-
assert!(r.unwrap_err().join("|").contains("cannot join Square and Oval: no single enum has both members"));
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
#[test]
|
|
81
|
-
fn no_wrap_same_type() {
|
|
82
|
-
let (r, p) = run("fun take(c: Circle) -> Int =\n 1\n\nfun go() -> Int =\n take(Circle(r: 1.0))");
|
|
83
|
-
r.unwrap();
|
|
84
|
-
assert!(p.wraps.is_empty());
|
|
85
|
-
let (r, p) = run("fun mk() -> Circle =\n Circle(r: 1.0)");
|
|
86
|
-
r.unwrap();
|
|
87
|
-
assert!(p.wraps.is_empty());
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
#[test]
|
|
91
|
-
fn member_alone_is_its_own_type() {
|
|
92
|
-
let (r, p) = run("fun mk() -> Circle =\n Circle(r: 1.0)");
|
|
93
|
-
r.unwrap();
|
|
94
|
-
assert!(p.wraps.is_empty());
|
|
95
|
-
let (r, _) = run("fun mk() -> Shape =\n Oval(a: 1.0)");
|
|
96
|
-
assert!(r.is_err());
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
#[test]
|
|
100
|
-
fn invariant_list_rejected() {
|
|
101
|
-
let (r, _) = run("fun take(xs: List[Shape]) -> Int =\n 1\n\nfun go(cs: List[Circle]) -> Int =\n take(cs)");
|
|
102
|
-
assert!(r.is_err(), "List[Circle] must not pass as List[Shape]");
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
#[test]
|
|
106
|
-
fn nonmember_and_other_enum_rejected() {
|
|
107
|
-
let (r, _) = run("fun take(s: Shape) -> Int =\n 1\n\nfun go() -> Int =\n take(Oval(a: 1.0))");
|
|
108
|
-
assert!(r.is_err());
|
|
109
|
-
let (r, _) = run("fun take(s: Shape) -> Int =\n 1\n\nfun go() -> Int =\n take(Round(Oval(a: 1.0)))");
|
|
110
|
-
let _ = r;
|
|
111
|
-
let (r, _) = run("fun take(s: Shape) -> Int =\n 1\n\nfun go() -> Int =\n take(\"x\")");
|
|
112
|
-
assert!(r.is_err());
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
#[test]
|
|
116
|
-
fn method_arg_nested_single_wrap() {
|
|
117
|
-
let (r, p) = run("fun take(s: Shape) -> Shape =\n s\n\nfun go() -> Shape =\n take(take(Circle(r: 1.0)))");
|
|
118
|
-
r.unwrap();
|
|
119
|
-
assert_eq!(wraps(&p), vec!["Shape__Circle"]);
|
|
120
|
-
}
|
plum-checker/tests/unions_dispatch_test.rs
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
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
|
-
record Circle(r: Float) =
|
|
8
|
-
fun area(self) -> Float = self.r
|
|
9
|
-
fun toStr(self) -> Str = \"c\"
|
|
10
|
-
|
|
11
|
-
record Rectangle(w: Float) =
|
|
12
|
-
fun area(self) -> Float = self.w
|
|
13
|
-
fun toStr(self) -> Str = \"r\"
|
|
14
|
-
|
|
15
|
-
record Odd(w: Float) =
|
|
16
|
-
fun area(self) -> Int = 1
|
|
17
|
-
|
|
18
|
-
record NoArea(w: Float)
|
|
19
|
-
|
|
20
|
-
enum Shape = Circle | Rectangle
|
|
21
|
-
enum Own =
|
|
22
|
-
| Circle
|
|
23
|
-
| Rectangle
|
|
24
|
-
|
|
25
|
-
fun area(self) -> Float = 0.0
|
|
26
|
-
|
|
27
|
-
enum WithTag = Circle | Plain
|
|
28
|
-
enum Missing = Circle | NoArea
|
|
29
|
-
enum Clash = Circle | Odd
|
|
30
|
-
|
|
31
|
-
record Bare(x: Float)
|
|
32
|
-
|
|
33
|
-
enum Boxed =
|
|
34
|
-
| Bare
|
|
35
|
-
| Circle
|
|
36
|
-
|
|
37
|
-
fun area(self) -> Float = 1.0
|
|
38
|
-
|
|
39
|
-
enum Round =
|
|
40
|
-
| Bare
|
|
41
|
-
| Rectangle
|
|
42
|
-
|
|
43
|
-
fun area(self) -> Float = 2.0
|
|
44
|
-
|
|
45
|
-
enum Lone =
|
|
46
|
-
| Bare
|
|
47
|
-
|
|
48
|
-
fun size(self) -> Float = 3.0
|
|
49
|
-
|
|
50
|
-
record Wrap[T](v: T) =
|
|
51
|
-
fun toStr(self) -> Str = \"w\"
|
|
52
|
-
|
|
53
|
-
enum Js = Circle | Wrap[Js]
|
|
54
|
-
";
|
|
55
|
-
|
|
56
|
-
fn run(body: &str) -> (Result<(), Vec<String>>, UnionPlan) {
|
|
57
|
-
let src = format!("{DEFS}\n{body}\n");
|
|
58
|
-
let mut parser = tree_sitter::Parser::new();
|
|
59
|
-
parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap();
|
|
60
|
-
let tree = parser.parse(&src, None).unwrap();
|
|
61
|
-
assert!(!tree.root_node().has_error(), "parse errors:\n{src}");
|
|
62
|
-
let mut source = AstParser::new(&src).parseSource(tree.root_node());
|
|
63
|
-
let errs = resolveMembers(&mut source);
|
|
64
|
-
assert!(errs.is_empty(), "{errs:?}");
|
|
65
|
-
beginPlan();
|
|
66
|
-
let r = checkSource(&source).map_err(|e| e.into_iter().map(|x| x.message).collect());
|
|
67
|
-
(r, takePlan())
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
fn errs(r: Result<(), Vec<String>>) -> String {
|
|
71
|
-
r.unwrap_err().join(" | ")
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
#[test]
|
|
75
|
-
fn dispatch_ok() {
|
|
76
|
-
let (r, p) = run("fun f(s: Shape) -> Float =\n s.area()");
|
|
77
|
-
r.unwrap();
|
|
78
|
-
assert_eq!(p.dispatches.len(), 1);
|
|
79
|
-
let v = p.dispatches.values().next().unwrap();
|
|
80
|
-
assert_eq!(v, &vec!["Shape__Circle".to_string(), "Shape__Rectangle".to_string()]);
|
|
81
|
-
let (r, _) = run("fun f(s: Shape) -> Int =\n x := s.area()\n take(x)\n\nfun take(i: Int) -> Int =\n i");
|
|
82
|
-
assert!(r.is_err(), "area() is Float, not Int");
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
#[test]
|
|
86
|
-
fn own_method_wins() {
|
|
87
|
-
let (r, p) = run("fun f(s: Own) -> Float =\n s.area()");
|
|
88
|
-
r.unwrap();
|
|
89
|
-
assert!(p.dispatches.is_empty());
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
#[test]
|
|
93
|
-
fn tag_member_error() {
|
|
94
|
-
let (r, _) = run("fun f(s: WithTag) -> Float =\n s.area()");
|
|
95
|
-
assert!(errs(r).contains("WithTag has no method 'area': member Plain is a tag"));
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
#[test]
|
|
99
|
-
fn missing_method_error() {
|
|
100
|
-
let (r, _) = run("fun f(s: Missing) -> Float =\n s.area()");
|
|
101
|
-
assert!(errs(r).contains("Missing has no method 'area': member NoArea doesn't define it"));
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
#[test]
|
|
105
|
-
fn signature_mismatch_error() {
|
|
106
|
-
let (r, _) = run("fun f(s: Clash) -> Float =\n s.area()");
|
|
107
|
-
let e = errs(r);
|
|
108
|
-
assert!(e.contains("Clash.area: members disagree: Circle.area is "), "{e}");
|
|
109
|
-
assert!(e.contains(", Odd.area is "), "{e}");
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
#[test]
|
|
113
|
-
fn interpolation_toStr() {
|
|
114
|
-
let (r, p) = run("fun f(s: Shape) -> Str =\n \"{s}\"");
|
|
115
|
-
r.unwrap();
|
|
116
|
-
assert_eq!(p.dispatches.len(), 1);
|
|
117
|
-
let (r, p) = run("fun f(s: Missing) -> Str =\n \"{s}\"");
|
|
118
|
-
let _ = r;
|
|
119
|
-
assert!(p.dispatches.is_empty());
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
#[test]
|
|
123
|
-
fn generic_member_dispatch() {
|
|
124
|
-
let (r, p) = run("fun f(j: Js) -> Str =\n j.toStr()");
|
|
125
|
-
r.unwrap();
|
|
126
|
-
assert_eq!(p.dispatches.values().next().unwrap().len(), 2);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
#[test]
|
|
130
|
-
fn member_falls_back_to_its_enum() {
|
|
131
|
-
let (r, p) = run("fun g() -> Float =\n Bare(x: 1.0).size()");
|
|
132
|
-
r.unwrap();
|
|
133
|
-
assert_eq!(p.wraps.len(), 1, "{:?}", p.wraps);
|
|
134
|
-
assert_eq!(p.wraps.values().next().unwrap(), "Lone__Bare");
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
#[test]
|
|
138
|
-
fn member_fallback_ambiguous_error() {
|
|
139
|
-
let (r, _) = run("fun f() -> Float =\n Bare(x: 1.0).area()");
|
|
140
|
-
assert!(
|
|
141
|
-
errs(r).ends_with("Bare has no method 'area' (it is a member of Boxed and Round, which both define it — convert first)")
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
#[test]
|
|
146
|
-
fn member_own_method_wins() {
|
|
147
|
-
let (r, p) = run("fun f() -> Float =\n Circle(r: 1.5).area()");
|
|
148
|
-
r.unwrap();
|
|
149
|
-
assert!(p.wraps.is_empty() && p.dispatches.is_empty());
|
|
150
|
-
}
|
plum-checker/tests/unions_pattern_test.rs
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
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
|
-
}
|
|
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-checker/tests/unions_tests.rs
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
#![allow(non_snake_case)]
|
|
2
|
+
//! Union plans recorded by the checker (wraps, narrowings, pattern members, dispatches)
|
|
3
|
+
//! plus the rejected programs of the union features: member coercion, narrowing by
|
|
4
|
+
//! `match` / pattern-if, member-method dispatch and annotated declarations.
|
|
5
|
+
//! Valid-program behavior at runtime is covered by the Plum tests.
|
|
6
|
+
use plum_checker::checkSource;
|
|
7
|
+
use plum_core::unions::{beginPlan, resolveMembers, takePlan, UnionPlan};
|
|
8
|
+
use plum_core::AstParser;
|
|
9
|
+
|
|
10
|
+
const COERCE: &str = "\
|
|
11
|
+
record Circle(r: Float)
|
|
12
|
+
record Square(s: Float)
|
|
13
|
+
record Oval(a: Float)
|
|
14
|
+
enum Shape = Circle | Square
|
|
15
|
+
enum Round = Circle | Oval
|
|
16
|
+
enum Json = Float | Str
|
|
17
|
+
enum Tree[T] = Node[T] | Leaf
|
|
18
|
+
record Node[T](v: T)
|
|
19
|
+
";
|
|
20
|
+
|
|
21
|
+
const PATTERN: &str = "\
|
|
22
|
+
struct Circle(r: Float)
|
|
23
|
+
struct Rectangle(w: Float, h: Float)
|
|
24
|
+
record Pill(len: Float)
|
|
25
|
+
enum Shape = Circle | Rectangle
|
|
26
|
+
enum Cap = Pill | Circle
|
|
27
|
+
enum Jv = JvNull | Float | Int
|
|
28
|
+
";
|
|
29
|
+
|
|
30
|
+
const NEST: &str = "\
|
|
31
|
+
enum IntOpt = Present | Absent
|
|
32
|
+
record Present(v: Int)
|
|
33
|
+
enum Nested = Wrap | Empty
|
|
34
|
+
record Wrap(v: IntOpt)
|
|
35
|
+
";
|
|
36
|
+
|
|
37
|
+
const DISPATCH: &str = "\
|
|
38
|
+
record Circle(r: Float) =
|
|
39
|
+
fun area(self) -> Float = self.r
|
|
40
|
+
fun toStr(self) -> Str = \"c\"
|
|
41
|
+
|
|
42
|
+
record Rectangle(w: Float) =
|
|
43
|
+
fun area(self) -> Float = self.w
|
|
44
|
+
fun toStr(self) -> Str = \"r\"
|
|
45
|
+
|
|
46
|
+
record Odd(w: Float) =
|
|
47
|
+
fun area(self) -> Int = 1
|
|
48
|
+
|
|
49
|
+
record NoArea(w: Float)
|
|
50
|
+
|
|
51
|
+
enum Shape = Circle | Rectangle
|
|
52
|
+
enum Own =
|
|
53
|
+
| Circle
|
|
54
|
+
| Rectangle
|
|
55
|
+
|
|
56
|
+
fun area(self) -> Float = 0.0
|
|
57
|
+
|
|
58
|
+
enum WithTag = Circle | Plain
|
|
59
|
+
enum Missing = Circle | NoArea
|
|
60
|
+
enum Clash = Circle | Odd
|
|
61
|
+
|
|
62
|
+
record Bare(x: Float)
|
|
63
|
+
|
|
64
|
+
enum Boxed =
|
|
65
|
+
| Bare
|
|
66
|
+
| Circle
|
|
67
|
+
|
|
68
|
+
fun area(self) -> Float = 1.0
|
|
69
|
+
|
|
70
|
+
enum Round =
|
|
71
|
+
| Bare
|
|
72
|
+
| Rectangle
|
|
73
|
+
|
|
74
|
+
fun area(self) -> Float = 2.0
|
|
75
|
+
|
|
76
|
+
enum Lone =
|
|
77
|
+
| Bare
|
|
78
|
+
|
|
79
|
+
fun size(self) -> Float = 3.0
|
|
80
|
+
|
|
81
|
+
record Wrap[T](v: T) =
|
|
82
|
+
fun toStr(self) -> Str = \"w\"
|
|
83
|
+
|
|
84
|
+
enum Js = Circle | Wrap[Js]
|
|
85
|
+
";
|
|
86
|
+
|
|
87
|
+
fn run(defs: &str, body: &str) -> (Result<(), Vec<String>>, UnionPlan) {
|
|
88
|
+
let src = format!("{defs}\n{body}\n");
|
|
89
|
+
let mut parser = tree_sitter::Parser::new();
|
|
90
|
+
parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap();
|
|
91
|
+
let tree = parser.parse(&src, None).unwrap();
|
|
92
|
+
assert!(!tree.root_node().has_error(), "parse errors:\n{src}");
|
|
93
|
+
let mut source = AstParser::new(&src).parseSource(tree.root_node());
|
|
94
|
+
let errs = resolveMembers(&mut source);
|
|
95
|
+
assert!(errs.is_empty(), "{errs:?}");
|
|
96
|
+
beginPlan();
|
|
97
|
+
let r = checkSource(&source).map_err(|e| e.into_iter().map(|x| x.message).collect());
|
|
98
|
+
(r, takePlan())
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
fn wraps(p: &UnionPlan) -> Vec<String> {
|
|
102
|
+
let mut v: Vec<String> = p.wraps.values().cloned().collect();
|
|
103
|
+
v.sort();
|
|
104
|
+
v
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// Each row (defs, body, needles) must be rejected, with every needle in the messages.
|
|
108
|
+
fn assertRejected(rows: &[(&str, &str, &[&str])]) {
|
|
109
|
+
for (defs, body, needles) in rows {
|
|
110
|
+
let (r, _) = run(defs, body);
|
|
111
|
+
let msg = r.expect_err(&format!("expected the checker to reject:\n{body}")).join(" | ");
|
|
112
|
+
for n in *needles {
|
|
113
|
+
assert!(msg.contains(*n), "{body}\nexpected {n:?} in: {msg}");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---- member coercion: where the checker records a wrap ----
|
|
119
|
+
|
|
120
|
+
#[test]
|
|
121
|
+
fn literal_into_json() {
|
|
122
|
+
let (r, p) = run(COERCE, "fun f() -> Json =\n 3.0");
|
|
123
|
+
r.unwrap();
|
|
124
|
+
assert_eq!(wraps(&p), vec!["Json__Float"]);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#[test]
|
|
128
|
+
fn wrap_at_arg_return_push() {
|
|
129
|
+
let (r, p) = run(COERCE, "fun take(s: Shape) -> Float =\n 1.0\n\nfun go() -> Float =\n take(Circle(r: 1.0))");
|
|
130
|
+
r.unwrap();
|
|
131
|
+
assert_eq!(wraps(&p), vec!["Shape__Circle"]);
|
|
132
|
+
let (r, p) = run(COERCE, "fun mk() -> Shape =\n Square(s: 2.0)");
|
|
133
|
+
r.unwrap();
|
|
134
|
+
assert_eq!(wraps(&p), vec!["Shape__Square"]);
|
|
135
|
+
let (r, p) = run(COERCE, "fun many(xs: ...Shape) -> Int =\n 1\n\nfun go() -> Int =\n many(Circle(r: 1.0), Square(s: 1.0))");
|
|
136
|
+
r.unwrap();
|
|
137
|
+
assert_eq!(wraps(&p), vec!["Shape__Circle", "Shape__Square"]);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#[test]
|
|
141
|
+
fn wrap_reassign_and_generic() {
|
|
142
|
+
let (r, p) = run(COERCE, "fun pick() -> Shape =\n Circle(r: 1.0)\n\nfun go() -> Int =\n s := pick()\n s = Square(s: 1.0)\n 1");
|
|
143
|
+
r.unwrap();
|
|
144
|
+
assert_eq!(wraps(&p), vec!["Shape__Circle", "Shape__Square"]);
|
|
145
|
+
let (r, p) = run(COERCE, "fun mk() -> Tree[Int] =\n Node(v: 3)");
|
|
146
|
+
r.unwrap();
|
|
147
|
+
assert_eq!(wraps(&p), vec!["Tree__Node"]);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#[test]
|
|
151
|
+
fn join_if_branches() {
|
|
152
|
+
let (r, p) = run(COERCE, "fun go(c: Bool) -> Int =\n x := if c then Circle(r: 1.0) else Square(s: 1.0)\n 1");
|
|
153
|
+
r.unwrap();
|
|
154
|
+
assert_eq!(wraps(&p), vec!["Shape__Circle", "Shape__Square"]);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#[test]
|
|
158
|
+
fn no_wrap_when_the_type_already_matches() {
|
|
159
|
+
let (r, p) = run(COERCE, "fun take(c: Circle) -> Int =\n 1\n\nfun go() -> Int =\n take(Circle(r: 1.0))");
|
|
160
|
+
r.unwrap();
|
|
161
|
+
assert!(p.wraps.is_empty());
|
|
162
|
+
let (r, p) = run(COERCE, "fun mk() -> Circle =\n Circle(r: 1.0)");
|
|
163
|
+
r.unwrap();
|
|
164
|
+
assert!(p.wraps.is_empty());
|
|
165
|
+
// a nested call wraps once, not twice
|
|
166
|
+
let (r, p) = run(COERCE, "fun take(s: Shape) -> Shape =\n s\n\nfun go() -> Shape =\n take(take(Circle(r: 1.0)))");
|
|
167
|
+
r.unwrap();
|
|
168
|
+
assert_eq!(wraps(&p), vec!["Shape__Circle"]);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ---- annotated declarations ----
|
|
172
|
+
|
|
173
|
+
#[test]
|
|
174
|
+
fn annotated_local_has_the_annotation_type() {
|
|
175
|
+
// `s` is a `Shape`, so passing it where a `Shape` is expected needs no second wrap.
|
|
176
|
+
let (r, p) = run(COERCE, "fun take(s: Shape) -> Int =\n 1\n\nfun go() -> Int =\n s: Shape := Circle(r: 2.0)\n take(s)");
|
|
177
|
+
r.unwrap();
|
|
178
|
+
assert_eq!(wraps(&p), vec!["Shape__Circle"]);
|
|
179
|
+
let (r, p) = run(COERCE, "fun go() -> Int =\n s: Shape := Circle(r: 2.0)\n s = Square(s: 1.0)\n 1");
|
|
180
|
+
r.unwrap();
|
|
181
|
+
assert_eq!(wraps(&p), vec!["Shape__Circle", "Shape__Square"]);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---- narrowing and patterns ----
|
|
185
|
+
|
|
186
|
+
#[test]
|
|
187
|
+
fn destructure_member() {
|
|
188
|
+
let (r, p) = run(PATTERN, "fun f(s: Shape) -> Float =\n match s\n Circle(r) => r\n Rectangle(w, h) => w * h");
|
|
189
|
+
r.unwrap();
|
|
190
|
+
let mut v: Vec<_> = p.patterns.values().cloned().collect();
|
|
191
|
+
v.sort();
|
|
192
|
+
assert_eq!(v, vec!["Shape__Circle", "Shape__Rectangle"]);
|
|
193
|
+
assert!(p.narrowings.is_empty());
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
#[test]
|
|
197
|
+
fn narrow_match() {
|
|
198
|
+
let (r, p) = run(PATTERN, "fun f(s: Shape) -> Float =\n match s\n Circle => s.r\n Rectangle => s.w");
|
|
199
|
+
r.unwrap();
|
|
200
|
+
assert_eq!(p.narrowings.len(), 2);
|
|
201
|
+
let mut v: Vec<_> = p.narrowings.values().map(|x| x.1.clone()).collect();
|
|
202
|
+
v.sort();
|
|
203
|
+
assert_eq!(v, vec!["Shape__Circle", "Shape__Rectangle"]);
|
|
204
|
+
assert!(p.narrowings.values().all(|x| x.0 == "s"));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
#[test]
|
|
208
|
+
fn expr_subject_no_narrow() {
|
|
209
|
+
let (r, p) = run(PATTERN, "fun g() -> Shape =\n Circle(r: 1.0)\n\nfun f() -> Int =\n match g()\n Circle => 1\n Rectangle => 2");
|
|
210
|
+
r.unwrap();
|
|
211
|
+
assert!(p.narrowings.is_empty());
|
|
212
|
+
assert_eq!(p.patterns.len(), 2);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#[test]
|
|
216
|
+
fn pattern_if_and_primitive_members_narrow() {
|
|
217
|
+
let (r, p) = run(PATTERN, "fun f(s: Shape) -> Float =\n if Circle := s\n return s.r\n 0.0");
|
|
218
|
+
r.unwrap();
|
|
219
|
+
assert_eq!(p.narrowings.len(), 1);
|
|
220
|
+
let (r, p) = run(PATTERN, "fun f(j: Jv) -> Float =\n match j\n Float => j + 1.0\n Int => 0.0\n JvNull => 0.0");
|
|
221
|
+
r.unwrap();
|
|
222
|
+
assert_eq!(p.narrowings.len(), 2);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
#[test]
|
|
226
|
+
fn narrowed_record_member_can_be_written_but_a_struct_copy_cannot() {
|
|
227
|
+
let (r, _) = run(PATTERN, "fun f(s: Cap) -> Int =\n match s\n Pill =>\n s.len = 1.0\n Circle => 0\n 0");
|
|
228
|
+
r.unwrap();
|
|
229
|
+
let (r, _) = run(PATTERN, "fun f(s: Shape) -> Int =\n match s\n Circle =>\n s.r = 1.0\n Rectangle => 0\n 0");
|
|
230
|
+
assert!(r
|
|
231
|
+
.unwrap_err()
|
|
232
|
+
.join("|")
|
|
233
|
+
.contains("s is a copy of the Circle inside Shape; assign it to a local to change it"));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ---- member-method dispatch ----
|
|
237
|
+
|
|
238
|
+
#[test]
|
|
239
|
+
fn dispatch_over_members() {
|
|
240
|
+
let (r, p) = run(DISPATCH, "fun f(s: Shape) -> Float =\n s.area()");
|
|
241
|
+
r.unwrap();
|
|
242
|
+
assert_eq!(p.dispatches.len(), 1);
|
|
243
|
+
let v = p.dispatches.values().next().unwrap();
|
|
244
|
+
assert_eq!(v, &vec!["Shape__Circle".to_string(), "Shape__Rectangle".to_string()]);
|
|
245
|
+
let (r, p) = run(DISPATCH, "fun f(j: Js) -> Str =\n j.toStr()");
|
|
246
|
+
r.unwrap();
|
|
247
|
+
assert_eq!(p.dispatches.values().next().unwrap().len(), 2);
|
|
248
|
+
let (r, p) = run(DISPATCH, "fun f(s: Shape) -> Str =\n \"{s}\"");
|
|
249
|
+
r.unwrap();
|
|
250
|
+
assert_eq!(p.dispatches.len(), 1);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
#[test]
|
|
254
|
+
fn own_method_wins_over_member_dispatch() {
|
|
255
|
+
let (r, p) = run(DISPATCH, "fun f(s: Own) -> Float =\n s.area()");
|
|
256
|
+
r.unwrap();
|
|
257
|
+
assert!(p.dispatches.is_empty());
|
|
258
|
+
let (r, p) = run(DISPATCH, "fun f() -> Float =\n Circle(r: 1.5).area()");
|
|
259
|
+
r.unwrap();
|
|
260
|
+
assert!(p.wraps.is_empty() && p.dispatches.is_empty());
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
#[test]
|
|
264
|
+
fn member_falls_back_to_its_enum() {
|
|
265
|
+
let (r, p) = run(DISPATCH, "fun g() -> Float =\n Bare(x: 1.0).size()");
|
|
266
|
+
r.unwrap();
|
|
267
|
+
assert_eq!(p.wraps.len(), 1, "{:?}", p.wraps);
|
|
268
|
+
assert_eq!(p.wraps.values().next().unwrap(), "Lone__Bare");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
#[test]
|
|
272
|
+
fn no_dispatch_recorded_when_a_member_lacks_toStr() {
|
|
273
|
+
let (_, p) = run(DISPATCH, "fun f(s: Missing) -> Str =\n \"{s}\"");
|
|
274
|
+
assert!(p.dispatches.is_empty());
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ---- rejected programs ----
|
|
278
|
+
|
|
279
|
+
#[test]
|
|
280
|
+
fn rejects_bad_member_coercions() {
|
|
281
|
+
assertRejected(&[
|
|
282
|
+
(COERCE, "fun go(c: Bool) -> Int =\n x := if c then Square(s: 1.0) else Oval(a: 1.0)\n 1", &["cannot join Square and Oval: no single enum has both members"]),
|
|
283
|
+
(COERCE, "fun mk() -> Shape =\n Oval(a: 1.0)", &["expected Shape, found Oval"]),
|
|
284
|
+
(COERCE, "fun take(s: Shape) -> Int =\n 1\n\nfun go() -> Int =\n take(Oval(a: 1.0))", &["expected Shape, found Oval"]),
|
|
285
|
+
(COERCE, "fun take(s: Shape) -> Int =\n 1\n\nfun go() -> Int =\n take(\"x\")", &["expected Shape, found Str"]),
|
|
286
|
+
(COERCE, "fun take(xs: List[Shape]) -> Int =\n 1\n\nfun go(cs: List[Circle]) -> Int =\n take(cs)", &["expected List[Shape], found List[Circle]"]),
|
|
287
|
+
// annotated declarations
|
|
288
|
+
(COERCE, "fun go() -> Int =\n s: Int := \"x\"\n 1", &["assign 's'", "mismatch"]),
|
|
289
|
+
(COERCE, "fun go() -> Int =\n s: Shape = Circle(r: 1.0)\n 1", &["annotated declarations use :="]),
|
|
290
|
+
(COERCE, "fun go() -> Int =\n total: Float\n 1", &["needs a value"]),
|
|
291
|
+
(COERCE, "fun go() -> Int =\n s := 1\n s: Int := 2\n s", &["already declared"]),
|
|
292
|
+
// an Int does not convert to a Float, as a param or as an annotated declaration
|
|
293
|
+
(COERCE, "fun take(x: Float) -> Float =\n x\n\nfun go() -> Float =\n take(3)", &["expected Float, found Int"]),
|
|
294
|
+
(COERCE, "fun go() -> Float =\n n: Float := 3\n n", &["expected Float, found Int"]),
|
|
295
|
+
]);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
#[test]
|
|
299
|
+
fn rejects_bad_narrowing_and_non_exhaustive_member_matches() {
|
|
300
|
+
assertRejected(&[
|
|
301
|
+
(PATTERN, "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", &["expected Int, found Float"]),
|
|
302
|
+
// after the match, and outside its arm, `s` is the whole Shape again
|
|
303
|
+
(PATTERN, "fun f(s: Shape) -> Float =\n match s\n Circle => 1.0\n Rectangle => 2.0\n s.r", &["no field 'r' on type 'Shape'"]),
|
|
304
|
+
(PATTERN, "fun f(s: Shape) -> Float =\n match s\n Circle => s.w\n Rectangle => 2.0", &["no field 'w' on type 'Circle'"]),
|
|
305
|
+
(PATTERN, "fun f(s: Shape) -> Float =\n if Circle := s\n return 1.0\n s.r", &["no field 'r' on type 'Shape'"]),
|
|
306
|
+
(PATTERN, "fun f(s: Shape) -> Float =\n match s\n Circle => 1.0", &["non-exhaustive match on Shape: missing Rectangle"]),
|
|
307
|
+
]);
|
|
308
|
+
for (body, needles) in [
|
|
309
|
+
("fun f(n: Nested) -> Int =\n match n\n Wrap(Present(v)) => v\n Empty => 0", &["non-exhaustive match", "Absent"][..]),
|
|
310
|
+
("fun f(n: Nested) -> Int =\n match n\n Wrap(Present(v)) => v\n Wrap(Absent) => -1", &["missing Empty"][..]),
|
|
311
|
+
] {
|
|
312
|
+
let (r, _) = run(NEST, body);
|
|
313
|
+
let msg = r.unwrap_err().join(" | ");
|
|
314
|
+
for n in needles {
|
|
315
|
+
assert!(msg.contains(n), "{body}\nexpected {n:?} in: {msg}");
|
|
316
|
+
}
|
|
317
|
+
assert!(!msg.contains("__"), "internal member names must not leak: {msg}");
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
#[test]
|
|
322
|
+
fn rejects_bad_member_dispatch() {
|
|
323
|
+
assertRejected(&[
|
|
324
|
+
(DISPATCH, "fun f(s: WithTag) -> Float =\n s.area()", &["WithTag has no method 'area': member Plain is a tag"]),
|
|
325
|
+
(DISPATCH, "fun f(s: Missing) -> Float =\n s.area()", &["Missing has no method 'area': member NoArea doesn't define it"]),
|
|
326
|
+
(DISPATCH, "fun f(s: Clash) -> Float =\n s.area()", &["Clash.area: members disagree: Circle.area is ", ", Odd.area is "]),
|
|
327
|
+
(DISPATCH, "fun f(s: Shape) -> Int =\n x := s.area()\n take(x)\n\nfun take(i: Int) -> Int =\n i", &["expected Int, found Float"]),
|
|
328
|
+
(DISPATCH, "fun f() -> Float =\n Bare(x: 1.0).area()", &["Bare has no method 'area' (it is a member of Boxed and Round, which both define it — convert first)"]),
|
|
329
|
+
]);
|
|
330
|
+
}
|