plum

#treesitter#compiler#wasm

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 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, plumTypeFromName,
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 validAddFnPasses() {
89
- let src = "fun add(a: Int, b: Int) -> Int =\n a + b\n";
90
- let source = parse(src);
91
- assert!(checkSource(&source).is_ok(), "expected Ok");
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 Shape =
95
+ enum Maybe =
1007
- | Circle(radius: Int)
1008
- | Square(side: Int)
96
+ | Just(Int)
97
+ | Nothing
1009
98
 
1010
- fun getRadius(s: Shape) -> Int =
99
+ fun bad(self) -> Int =
1011
- r := Circle(radius: 5)
1012
- return r.radius
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
- "{}fun main() -> Plain =\n maxOf(Plain(v: 1), Plain(v: 2))\n",
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
- "{}fun main() -> Int =\n total := 0\n for i := range twice(4), if i > 1\n total = total + i\n total\n",
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
- "{UPTO}fun main() -> Int =\n total := 0\n for i := range upTo(2, 5)\n total = total + i\n total\n"
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
- #[test]
433
+ // ---------------------------------------------------------------------------
2138
- fn const_shaped_name_pattern_on_int_is_rejected() {
434
+ // Rejected programs, table-driven by area. Each row is (source, needles): the
2139
- for src in [
2140
- "fun f(n: Int) -> Int =\n match n\n MAX => 1\n _ => 0\n",
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
- "fun f(n: Int) -> Int =\n if MAX := n\n return 1\n 0\n",
439
+ type Row = (String, &'static [&'static str]);
2142
- ] {
440
+
2143
- let msg = errText(checkSrc(src));
2144
- assert!(msg.contains("'MAX' is not a variant of Int"), "{msg}");
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
- assert!(checkSrc(src).is_ok());
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 eq_on_functions_and_unit_is_rejected() {
2156
- for (src, want) in [
2157
- ("fun f() -> Bool =\n a := |x| x\n b := |x| x\n a == b\n", "can't compare functions"),
2158
- ("fun f(a: fn(Int) -> Int, b: fn(Int) -> Int) -> Bool =\n a == b\n", "can't compare functions"),
2159
- ("fun u() =\n return\n\nfun f() -> Bool =\n u() == u()\n", "can't compare Unit values"),
2160
- ] {
2161
- let msg = errText(checkSrc(src));
2162
- assert!(msg.contains(want), "{src}: {msg}");
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
- fun main() =
620
- x := f(\"a\" => 1)
621
- ";
622
- let msg = check(src).unwrap_err();
623
- assert!(msg.contains("import std/Map"), "{}", msg);
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
+ }