plum

#treesitter#compiler#wasm

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

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


plum-checker/tests/checker_tests.rs
3d6f280 1
#![allow(non_snake_case)]
03e5a61 2
use plum_checker::types::*;
3d6f280 3
use plum_checker::{checkSource, plumTypeFromAst, unify};
30f1008 4
use plum_core::ast::Type as AstType;
729d7cb 5
use plum_core::{ast::*, AstParser};
729d7cb 6
729d7cb 7
fn parse(src: &str) -> Source {
729d7cb 8
    let mut parser = tree_sitter::Parser::new();
729d7cb 9
    parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap();
729d7cb 10
    let tree = parser.parse(src, None).unwrap();
729d7cb 11
    let ap = AstParser::new(src);
3d6f280 12
    ap.parseSource(tree.root_node())
729d7cb 13
}
30f1008 14
30f1008 15
#[test]
3d6f280 16
fn astTypeIntMapsToTint() {
30f1008 17
    let ast_ty = AstType { name: "Int".to_string(), generics: vec![] };
3d6f280 18
    assert_eq!(plumTypeFromAst(&ast_ty), PlumType::TInt);
30f1008 19
}
30f1008 20
30f1008 21
#[test]
3d6f280 22
fn astTypeUnknownMapsToNamed() {
30f1008 23
    let ast_ty = AstType { name: "MyClass".to_string(), generics: vec![] };
3d6f280 24
    assert_eq!(plumTypeFromAst(&ast_ty), PlumType::TNamed("MyClass".to_string()));
30f1008 25
}
30f1008 26
30f1008 27
#[test]
3d6f280 28
fn unifySameTypesOk() {
30f1008 29
    assert!(unify(&PlumType::TInt, &PlumType::TInt).is_ok());
30f1008 30
    assert!(unify(&PlumType::TFloat, &PlumType::TFloat).is_ok());
30f1008 31
}
30f1008 32
30f1008 33
#[test]
3d6f280 34
fn unifyDifferentTypesErr() {
30f1008 35
    assert!(unify(&PlumType::TInt, &PlumType::TFloat).is_err());
30f1008 36
}
03e5a61 37
03e5a61 38
#[test]
3d6f280 39
fn freshVarsAreUnique() {
03e5a61 40
    let mut state = InferState::new();
3d6f280 41
    let a = state.freshVar();
3d6f280 42
    let b = state.freshVar();
03e5a61 43
    assert_ne!(a, b);
03e5a61 44
    assert_eq!(a, "a0");
03e5a61 45
    assert_eq!(b, "a1");
03e5a61 46
}
03e5a61 47
03e5a61 48
#[test]
3d6f280 49
fn monoScheme() {
03e5a61 50
    let scheme = TypeScheme::mono(PlumType::TInt);
03e5a61 51
    assert!(scheme.vars.is_empty());
03e5a61 52
    assert_eq!(*scheme.body, PlumType::TInt);
03e5a61 53
}
729d7cb 54
729d7cb 55
#[test]
3d6f280 56
fn validAddFnPasses() {
b48d3a3 57
    let src = "fun add(a: Int, b: Int) -> Int =\n  a + b\n";
729d7cb 58
    let source = parse(src);
3d6f280 59
    assert!(checkSource(&source).is_ok(), "expected Ok");
729d7cb 60
}
729d7cb 61
729d7cb 62
#[test]
3d6f280 63
fn wrongReturnTypeIsError() {
b48d3a3 64
    let src = "fun bad() -> Int =\n  True\n";
729d7cb 65
    let source = parse(src);
3d6f280 66
    let result = checkSource(&source);
729d7cb 67
    assert!(result.is_err());
729d7cb 68
    let errs = result.unwrap_err();
729d7cb 69
    assert!(errs[0].message.contains("return type mismatch"), "got: {}", errs[0].message);
729d7cb 70
}
729d7cb 71
729d7cb 72
#[test]
3d6f280 73
fn undeclaredVarIsError() {
b48d3a3 74
    let src = "fun bad() -> Int =\n  x\n";
729d7cb 75
    let source = parse(src);
3d6f280 76
    let result = checkSource(&source);
729d7cb 77
    assert!(result.is_err());
729d7cb 78
}
729d7cb 79
729d7cb 80
#[test]
3d6f280 81
fn typeMismatchInBinaryOpIsError() {
b48d3a3 82
    let src = "fun bad() -> Int =\n  1 + 2.0\n";
729d7cb 83
    let source = parse(src);
3d6f280 84
    let result = checkSource(&source);
729d7cb 85
    assert!(result.is_err());
729d7cb 86
}
d1a4183 87
d1a4183 88
#[test]
3d6f280 89
fn boolLiteralTrueFalseAreBool() {
b48d3a3 90
    let src = "fun isTrue() -> Bool =\n  True\n";
d1a4183 91
    let source = parse(src);
3d6f280 92
    assert!(checkSource(&source).is_ok(), "expected Ok, got {:?}", checkSource(&source).err());
d1a4183 93
}
d1a4183 94
d1a4183 95
#[test]
3d6f280 96
fn boolLiteralWrongReturnTypeIsError() {
b48d3a3 97
    let src = "fun bad() -> Int =\n  False\n";
d1a4183 98
    let source = parse(src);
3d6f280 99
    let result = checkSource(&source);
d1a4183 100
    assert!(result.is_err());
d1a4183 101
}
d1a4183 102
d1a4183 103
#[test]
3d6f280 104
fn methodSelfFieldAccessPasses() {
805d96d 105
    let src = "type Cat =\n  name: Str\n  age: Int\n\n  fun getName() -> Str =\n    self.name\n";
d1a4183 106
    let source = parse(src);
3d6f280 107
    let result = checkSource(&source);
d1a4183 108
    assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
d1a4183 109
}
d1a4183 110
d1a4183 111
#[test]
3d6f280 112
fn methodSelfUnknownFieldIsError() {
7056de2 113
    let src = "type Cat =\n  name: Str\n\n  fun getAge() -> Int =\n    self.age\n";
d1a4183 114
    let source = parse(src);
3d6f280 115
    let result = checkSource(&source);
d1a4183 116
    assert!(result.is_err());
d1a4183 117
}
d1a4183 118
d2640d2 119
#[test]
3d6f280 120
fn nestedMethodTypeChecks() {
805d96d 121
    let src = "type Cat =\n  name: Str\n  age: Int\n\n  fun getName(self) -> Str =\n    self.name\n";
3d6f280 122
    let result = checkSource(&parse(src));
805d96d 123
    assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
d2640d2 124
}
d2640d2 125
d2640d2 126
#[test]
3d6f280 127
fn nestedMethodUnknownFieldIsError() {
805d96d 128
    let src = "type Cat =\n  name: Str\n\n  fun getAge(self) -> Int =\n    self.age\n";
3d6f280 129
    assert!(checkSource(&parse(src)).is_err());
d2640d2 130
}
d2640d2 131
d1a4183 132
#[test]
3d6f280 133
fn selfOutsideMethodIsError() {
b48d3a3 134
    let src = "fun bad() -> Int =\n  self\n";
d1a4183 135
    let source = parse(src);
3d6f280 136
    let result = checkSource(&source);
d1a4183 137
    assert!(result.is_err());
d1a4183 138
}
d1a4183 139
d1a4183 140
#[test]
3d6f280 141
fn classCallChecksFieldTypes() {
b48d3a3 142
    let src = "type Cat =\n  name: Str\n  age: Int\n\nfun makeCat() -> Cat =\n  Cat(name: \"x\", age: 1)\n";
d1a4183 143
    let source = parse(src);
3d6f280 144
    let result = checkSource(&source);
d1a4183 145
    assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
d1a4183 146
}
d1a4183 147
d1a4183 148
#[test]
3d6f280 149
fn classCallWrongFieldTypeIsError() {
b48d3a3 150
    let src = "type Cat =\n  name: Str\n  age: Int\n\nfun makeCat() -> Cat =\n  Cat(name: \"x\", age: \"y\")\n";
d1a4183 151
    let source = parse(src);
3d6f280 152
    let result = checkSource(&source);
d1a4183 153
    assert!(result.is_err());
d1a4183 154
}
d1a4183 155
d1a4183 156
#[test]
3d6f280 157
fn classCallUnknownFieldIsError() {
b48d3a3 158
    let src = "type Cat =\n  name: Str\n\nfun makeCat() -> Cat =\n  Cat(name: \"x\", age: 1)\n";
d1a4183 159
    let source = parse(src);
3d6f280 160
    let result = checkSource(&source);
d1a4183 161
    assert!(result.is_err());
d1a4183 162
}
d1a4183 163
d1a4183 164
#[test]
3d6f280 165
fn methodCallViaAttributeTypeChecksArgs() {
7056de2 166
    let src = "type Cat =\n  name: Str\n\n  fun rename(n: Str) -> Str =\n    n\n\nfun use(c: Cat) -> Str =\n  c.rename(\"x\")\n";
d1a4183 167
    let source = parse(src);
3d6f280 168
    let result = checkSource(&source);
d1a4183 169
    assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
d1a4183 170
}
d1a4183 171
d1a4183 172
#[test]
3d6f280 173
fn matchBindsNamePatternToSubjectType() {
b48d3a3 174
    let src = "fun main(a: Int) -> Int =\n  match a\n    x =>\n      x\n    _ =>\n      0\n";
d1a4183 175
    let source = parse(src);
3d6f280 176
    let result = checkSource(&source);
d1a4183 177
    assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
d1a4183 178
}
d1a4183 179
660674c 180
#[test]
3d6f280 181
fn matchInlineCaseBodyTypeChecks() {
660674c 182
    // Case bodies can be a single inline expression, not just an indented block.
b48d3a3 183
    let src = "fun main(a: Int) -> Int =\n  match a\n    1 => 10\n    _ => 0\n";
660674c 184
    let source = parse(src);
3d6f280 185
    let result = checkSource(&source);
660674c 186
    assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
660674c 187
}
660674c 188
d1a4183 189
#[test]
3d6f280 190
fn matchTrueFalseAreVariantPatternsNotBindingsWithoutEnumDecl() {
d1a4183 191
    // True/False are built-in Bool variants — they must be recognized as tag
d1a4183 192
    // comparisons even when the source doesn't redeclare `enum Bool`, so a
d1a4183 193
    // later `_` wildcard arm remains reachable (each pattern binds/compares,
d1a4183 194
    // it doesn't just re-bind the subject under the name "True").
b48d3a3 195
    let src = "fun pick(a: Bool) -> Int =\n  match a\n    True =>\n      1\n    False =>\n      0\n";
d1a4183 196
    let source = parse(src);
3d6f280 197
    let result = checkSource(&source);
d1a4183 198
    assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
d1a4183 199
}
d1a4183 200
d1a4183 201
#[test]
3d6f280 202
fn matchIntPatternAgainstStrSubjectIsError() {
b48d3a3 203
    let src = "fun main(a: Str) -> Int =\n  match a\n    1 =>\n      1\n    _ =>\n      0\n";
d1a4183 204
    let source = parse(src);
3d6f280 205
    let result = checkSource(&source);
d1a4183 206
    assert!(result.is_err());
d1a4183 207
}
7ac1d37 208
7ac1d37 209
#[test]
3d6f280 210
fn bareEnumTagUnifiesWithOwningEnumType() {
7ac1d37 211
    // Regression: a bare non-Bool tag like `None` used to type as `TNamed("None")`
7ac1d37 212
    // (itself, not its enum), so comparing it against an `Option` value would wrongly
7ac1d37 213
    // fail with a type mismatch.
7ac1d37 214
    let src = "\
7ac1d37 215
enum Option =
9d5550e 216
  | Some[Int]
7ac1d37 217
  | None
7ac1d37 218
b48d3a3 219
fun isNone(o: Option) -> Bool = o == None
7ac1d37 220
";
7ac1d37 221
    let source = parse(src);
3d6f280 222
    let result = checkSource(&source);
7ac1d37 223
    assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
7ac1d37 224
}
7ac1d37 225
7ac1d37 226
#[test]
3d6f280 227
fn variantConstructionChecksArgCountAndTypes() {
7ac1d37 228
    let src = "\
7ac1d37 229
enum Option =
30ae945 230
  | Some[Int]
7ac1d37 231
  | None
7ac1d37 232
b48d3a3 233
fun makeSome(v: Int) -> Option =
7ac1d37 234
  Some(v)
7ac1d37 235
";
7ac1d37 236
    let source = parse(src);
3d6f280 237
    let result = checkSource(&source);
7ac1d37 238
    assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
7ac1d37 239
}
7ac1d37 240
7ac1d37 241
#[test]
3d6f280 242
fn variantConstructionWrongArgTypeIsError() {
7ac1d37 243
    let src = "\
7ac1d37 244
enum Option =
7ac1d37 245
  | Some(Int)
7ac1d37 246
  | None
7ac1d37 247
b48d3a3 248
fun bad() -> Option =
7ac1d37 249
  Some(\"x\")
7ac1d37 250
";
7ac1d37 251
    let source = parse(src);
3d6f280 252
    let result = checkSource(&source);
7ac1d37 253
    assert!(result.is_err());
7ac1d37 254
}
7ac1d37 255
7ac1d37 256
#[test]
3d6f280 257
fn variantConstructionWrongArgCountIsError() {
7ac1d37 258
    let src = "\
7ac1d37 259
enum Shape =
30ae945 260
  | Rect[Float, Float]
30ae945 261
  | Circle[Float]
7ac1d37 262
b48d3a3 263
fun bad() -> Shape =
7ac1d37 264
  Rect(1.0)
7ac1d37 265
";
7ac1d37 266
    let source = parse(src);
3d6f280 267
    let result = checkSource(&source);
7ac1d37 268
    assert!(result.is_err());
7ac1d37 269
}
7ac1d37 270
4fda634 271
#[test]
3d6f280 272
fn enumDiscriminantValuesTypeCheckWithNoErrors() {
4fda634 273
    let src = "\
4fda634 274
enum Step(n: Int) =
4fda634 275
  | ReadMin(0)
4fda634 276
  | ReadMax(1)
4fda634 277
";
4fda634 278
    let source = parse(src);
3d6f280 279
    let result = checkSource(&source);
4fda634 280
    assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
4fda634 281
}
4fda634 282
4fda634 283
#[test]
3d6f280 284
fn enumDiscriminantWrongValueCountIsError() {
4fda634 285
    let src = "\
4fda634 286
enum Step(n: Int) =
4fda634 287
  | ReadMin(0)
4fda634 288
  | ReadMax
4fda634 289
";
4fda634 290
    let source = parse(src);
3d6f280 291
    let result = checkSource(&source);
4fda634 292
    assert!(result.is_err());
4fda634 293
}
4fda634 294
4fda634 295
#[test]
3d6f280 296
fn enumDiscriminantWronglyTypedValueIsError() {
4fda634 297
    let src = "\
4fda634 298
enum Step(n: Int) =
4fda634 299
  | ReadMin(0)
4fda634 300
  | ReadMax(\"x\")
4fda634 301
";
4fda634 302
    let source = parse(src);
3d6f280 303
    let result = checkSource(&source);
4fda634 304
    assert!(result.is_err());
4fda634 305
}
4fda634 306
4fda634 307
#[test]
3d6f280 308
fn enumDiscriminantValueOnParamLessEnumIsError() {
4fda634 309
    let src = "\
4fda634 310
enum Option =
4fda634 311
  | Some(5)
4fda634 312
  | None
4fda634 313
";
4fda634 314
    let source = parse(src);
3d6f280 315
    let result = checkSource(&source);
4fda634 316
    assert!(result.is_err());
4fda634 317
}
4fda634 318
4fda634 319
#[test]
3d6f280 320
fn fieldAccessOnDiscriminantEnumReceiverTypeChecks() {
4fda634 321
    let src = "\
4fda634 322
enum Step(n: Int) =
4fda634 323
  | ReadMin(0)
4fda634 324
  | ReadMax(1)
4fda634 325
4fda634 326
  fun toNumber(self) -> Int =
4fda634 327
    self.n
4fda634 328
";
4fda634 329
    let source = parse(src);
3d6f280 330
    let result = checkSource(&source);
4fda634 331
    assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
4fda634 332
}
4fda634 333
4fda634 334
#[test]
3d6f280 335
fn fieldAccessOnOrdinaryEnumReceiverIsUnaffectedByEnumParams() {
4fda634 336
    // An ordinary enum (no discriminant `params`) isn't in `ctx.classes` OR
4fda634 337
    // `ctx.enum_params`, so it falls through to the same permissive "unmodeled
4fda634 338
    // type" escape hatch every other type not in `ctx.classes` gets (codegen,
4fda634 339
    // not the checker, is what would catch a genuinely bad field access here) —
4fda634 340
    // exactly as it did before discriminant enums existed.
4fda634 341
    let src = "\
4fda634 342
enum Option =
4fda634 343
  | Some[Int]
4fda634 344
  | None
4fda634 345
4fda634 346
  fun bad(self) -> Int =
4fda634 347
    self.n
4fda634 348
";
4fda634 349
    let source = parse(src);
3d6f280 350
    let result = checkSource(&source);
4fda634 351
    assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
4fda634 352
}
4fda634 353
7ac1d37 354
#[test]
3d6f280 355
fn constructorPatternBindsFieldsToDeclaredTypes() {
7ac1d37 356
    let src = "\
7ac1d37 357
enum Shape =
30ae945 358
  | Rect[Float, Float]
30ae945 359
  | Circle[Float]
7ac1d37 360
b48d3a3 361
fun area(s: Shape) -> Float =
7ac1d37 362
  match s
7ac1d37 363
    Rect(w, h) =>
7ac1d37 364
      w * h
7ac1d37 365
    Circle(r) =>
7ac1d37 366
      r * r
7ac1d37 367
";
7ac1d37 368
    let source = parse(src);
3d6f280 369
    let result = checkSource(&source);
7ac1d37 370
    assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
7ac1d37 371
}
7ac1d37 372
7ac1d37 373
#[test]
3d6f280 374
fn constructorPatternWrongFieldCountIsError() {
7ac1d37 375
    let src = "\
7ac1d37 376
enum Shape =
30ae945 377
  | Rect[Float, Float]
30ae945 378
  | Circle[Float]
7ac1d37 379
b48d3a3 380
fun bad(s: Shape) -> Float =
7ac1d37 381
  match s
7ac1d37 382
    Rect(w) =>
7ac1d37 383
      w
7ac1d37 384
    _ =>
7ac1d37 385
      0.0
7ac1d37 386
";
7ac1d37 387
    let source = parse(src);
3d6f280 388
    let result = checkSource(&source);
7ac1d37 389
    assert!(result.is_err());
7ac1d37 390
}
42d88a3 391
42d88a3 392
#[test]
3d6f280 393
fn classNameCollidingWithEnumVariantIsAClearError() {
42d88a3 394
    // `Cat(...)` is ambiguous when `Cat` is both a class and an enum variant:
42d88a3 395
    // downstream code consults `enum_variants` first, so the class
42d88a3 396
    // constructor would otherwise be silently shadowed with no diagnostic.
42d88a3 397
    let src = "\
42d88a3 398
type Cat =
42d88a3 399
  name: Str
42d88a3 400
42d88a3 401
enum Animal =
42d88a3 402
  | Cat
42d88a3 403
  | Dog
42d88a3 404
";
42d88a3 405
    let source = parse(src);
3d6f280 406
    let result = checkSource(&source);
42d88a3 407
    assert!(result.is_err(), "expected Err");
42d88a3 408
    let errs = result.unwrap_err();
42d88a3 409
    assert!(
42d88a3 410
        errs.iter().any(|e| e.message.contains("is declared as both a class and an enum variant")),
42d88a3 411
        "got: {:?}",
42d88a3 412
        errs
42d88a3 413
    );
42d88a3 414
}
22140cf 415
22140cf 416
#[test]
3d6f280 417
fn genericClassInstantiatedAtTwoConcreteTypesTypeChecks() {
22140cf 418
    let src = "\
30ae945 419
type Box[T] =
30ae945 420
  value: T
22140cf 421
b48d3a3 422
fun makeIntBox() -> Box =
22140cf 423
  Box(value: 5)
22140cf 424
b48d3a3 425
fun makeStrBox() -> Box =
22140cf 426
  Box(value: \"x\")
22140cf 427
";
22140cf 428
    let source = parse(src);
3d6f280 429
    let result = checkSource(&source);
22140cf 430
    assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
22140cf 431
}
22140cf 432
22140cf 433
#[test]
3d6f280 434
fn genericFunctionCalledWithDifferentConcreteTypesPerSiteTypeChecks() {
22140cf 435
    let src = "\
b48d3a3 436
fun wrap(value: T) -> Bool =
22140cf 437
  True
22140cf 438
b48d3a3 439
fun useInt() -> Bool =
22140cf 440
  wrap(5)
22140cf 441
b48d3a3 442
fun useStr() -> Bool =
22140cf 443
  wrap(\"x\")
22140cf 444
";
22140cf 445
    let source = parse(src);
3d6f280 446
    let result = checkSource(&source);
22140cf 447
    assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
22140cf 448
}
22140cf 449
22140cf 450
#[test]
3d6f280 451
fn genericFunctionWithTwoIndependentTypeParamsTypeChecks() {
22140cf 452
    let src = "\
b48d3a3 453
fun pair(first: T, second: U) -> Bool =
22140cf 454
  True
22140cf 455
b48d3a3 456
fun use() -> Bool =
22140cf 457
  pair(1, \"x\")
22140cf 458
";
22140cf 459
    let source = parse(src);
3d6f280 460
    let result = checkSource(&source);
22140cf 461
    assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
22140cf 462
}
22140cf 463
c273ea5 464
#[test]
3d6f280 465
fn genericFunctionWithInternallyInconsistentBodyIsRejectedAfterSpecialization() {
c273ea5 466
    // A generic function whose body is genuinely inconsistent with its concrete,
c273ea5 467
    // correct declared return type must still be REJECTED after specialization.
c273ea5 468
    // The `-> Str` on `useIt` means the buggy (unconditional return-overwrite)
c273ea5 469
    // behavior — rewriting `wrong$Str`'s `-> Int` to `-> Str` — would make the
c273ea5 470
    // whole program type-check, masking the real `expected Int, found Str` error.
c273ea5 471
    let src = "\
b48d3a3 472
fun wrong(x: T) -> Int =
c273ea5 473
  \"hello\"
c273ea5 474
b48d3a3 475
fun useIt() -> Str =
c273ea5 476
  wrong(\"s\")
c273ea5 477
";
c273ea5 478
    let source = parse(src);
3d6f280 479
    let result = checkSource(&source);
c273ea5 480
    assert!(
c273ea5 481
        result.is_err(),
c273ea5 482
        "expected the internally-inconsistent generic function to be rejected, got Ok"
c273ea5 483
    );
c273ea5 484
}
c273ea5 485
c273ea5 486
#[test]
3d6f280 487
fn genericEnumSingleInstantiationTypeChecks() {
c273ea5 488
    // A generic Option-shaped enum, constructed at one concrete type (`Some(5)`),
3d6f280 489
    // matched, must resolve end-to-end via checkSource.
c273ea5 490
    let src = "\
c273ea5 491
enum Option =
30ae945 492
  | Some[T]
c273ea5 493
  | None
c273ea5 494
b48d3a3 495
fun get() -> Int =
c273ea5 496
  o = Some(5)
c273ea5 497
  match o
c273ea5 498
    Some(v) =>
c273ea5 499
      v
c273ea5 500
    None =>
c273ea5 501
      0
c273ea5 502
";
c273ea5 503
    let source = parse(src);
3d6f280 504
    let result = checkSource(&source);
c273ea5 505
    assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
c273ea5 506
c273ea5 507
    // Directly prove resolution happened: the monomorphized output must contain a
2216237 508
    // concrete `Option$Int` enum whose `Some$Int` variant carries an `Int` field
2216237 509
    // (not the generic `a`), and must NOT retain the generic `Option` template.
2216237 510
    // The variant name is ALSO mangled (`Some` -> `Some$Int`), the same suffix as
2216237 511
    // the enum's own name.
3d6f280 512
    let mono = plum_checker::monomorphize::monomorphizeSource(&source)
c273ea5 513
        .expect("monomorphize should succeed");
c273ea5 514
    let opt = mono.items.iter().find_map(|it| match it {
c273ea5 515
        Item::Enum(e) if e.name == "Option$Int" => Some(e),
c273ea5 516
        _ => None,
c273ea5 517
    });
c273ea5 518
    let opt = opt.expect("expected a specialized `Option$Int` enum in the output");
2216237 519
    let some = opt.variants.iter().find(|v| v.name == "Some$Int")
2216237 520
        .expect("expected `Some$Int` (mangled) variant on `Option$Int`");
c273ea5 521
    assert_eq!(some.fields, vec!["Int".to_string()], "Some's field should be concrete Int");
c273ea5 522
    assert!(
c273ea5 523
        !mono.items.iter().any(|it| matches!(it, Item::Enum(e) if e.name == "Option")),
c273ea5 524
        "the generic `Option` template must be dropped from the output"
c273ea5 525
    );
c273ea5 526
}
c273ea5 527
c273ea5 528
#[test]
3d6f280 529
fn genericEnumMultipleInstantiationsCoexistAndTypeCheck() {
c273ea5 530
    // The SAME generic enum instantiated at two different concrete types in one
2216237 531
    // program must now type-check correctly for BOTH instantiations — this is the
2216237 532
    // behavior this task adds (previously this was a documented, rejected limitation).
c273ea5 533
    let src = "\
c273ea5 534
enum Option =
30ae945 535
  | Some[T]
c273ea5 536
  | None
c273ea5 537
b48d3a3 538
fun useInt() -> Int =
c273ea5 539
  o = Some(5)
c273ea5 540
  match o
c273ea5 541
    Some(v) =>
c273ea5 542
      v
c273ea5 543
    None =>
c273ea5 544
      0
c273ea5 545
b48d3a3 546
fun useStr() -> Str =
c273ea5 547
  o = Some(\"x\")
c273ea5 548
  match o
c273ea5 549
    Some(v) =>
c273ea5 550
      v
c273ea5 551
    None =>
c273ea5 552
      \"z\"
c273ea5 553
";
c273ea5 554
    let source = parse(src);
3d6f280 555
    let result = checkSource(&source);
2216237 556
    assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
2216237 557
2216237 558
    // Directly prove both specializations exist independently, with distinct
2216237 559
    // mangled variant names, so neither collides with the other.
3d6f280 560
    let mono = plum_checker::monomorphize::monomorphizeSource(&source)
2216237 561
        .expect("monomorphize should succeed");
2216237 562
    let has_enum_with_variant = |enum_name: &str, variant_name: &str| {
2216237 563
        mono.items.iter().any(|it| matches!(it, Item::Enum(e) if e.name == enum_name
2216237 564
            && e.variants.iter().any(|v| v.name == variant_name)))
2216237 565
    };
2216237 566
    assert!(has_enum_with_variant("Option$Int", "Some$Int"), "expected Option$Int with Some$Int");
2216237 567
    assert!(has_enum_with_variant("Option$Str", "Some$Str"), "expected Option$Str with Some$Str");
c273ea5 568
}
c273ea5 569
22140cf 570
#[test]
3d6f280 571
fn genericMethodOnGenericClassTypeChecks() {
22140cf 572
    let src = "\
30ae945 573
type Box[T] =
30ae945 574
  value: T
22140cf 575
7056de2 576
  fun getValue() -> T =
7056de2 577
    self.value
22140cf 578
b48d3a3 579
fun use() -> Int =
22140cf 580
  b = Box(value: 5)
22140cf 581
  b.getValue()
22140cf 582
";
22140cf 583
    let source = parse(src);
3d6f280 584
    let result = checkSource(&source);
22140cf 585
    assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
22140cf 586
}
2e28ecc 587
2e28ecc 588
#[test]
6613e6d 589
#[ignore = "slow (~100s) — exercises the 10,000-specialization runaway-recursion guard; run explicitly with --ignored when touching monomorphize.rs's guard logic"]
3d6f280 590
fn unboundedRecursiveGenericInstantiationIsAClearError() {
2e28ecc 591
    // `recurse` re-wraps its argument in a `Box` on every recursive call, so each
2e28ecc 592
    // specialization's own body demands a specialization of `recurse` at a STRICTLY
2e28ecc 593
    // bigger type (`recurse$Int`, then `recurse$Box$Int`, then `recurse$Box$Box$Int`,
2e28ecc 594
    // ...), forever. This genuinely grows the worklist without bound (unlike a
2e28ecc 595
    // generic class field merely NAMING a recursive generic type in its own
2e28ecc 596
    // declaration, which is never itself a call site and so never reaches the
2e28ecc 597
    // worklist at all) and must fail with a clear, bounded error rather than hang.
2e28ecc 598
    let src = "\
30ae945 599
type Box[T] =
30ae945 600
  value: T
2e28ecc 601
b48d3a3 602
fun recurse(v: T) -> Int =
2e28ecc 603
  b = Box(value: v)
2e28ecc 604
  recurse(b)
2e28ecc 605
b48d3a3 606
fun use() -> Int =
2e28ecc 607
  recurse(5)
2e28ecc 608
";
2e28ecc 609
    let source = parse(src);
3d6f280 610
    let result = checkSource(&source);
2e28ecc 611
    assert!(result.is_err());
2e28ecc 612
    let errs = result.unwrap_err();
2e28ecc 613
    assert!(errs[0].message.contains("monomorphize"), "got: {:?}", errs);
2e28ecc 614
}
2216237 615
2216237 616
2216237 617
2216237 618
2216237 619
#[test]
3d6f280 620
fn ordinaryFunctionWithBareGenericEnumParamTypeChecks() {
2216237 621
    // The shape that broke the pre-existing codegen test: an otherwise-ordinary
2216237 622
    // function taking a bare generic-enum-typed parameter.
2216237 623
    let src = "\
2216237 624
enum Option =
30ae945 625
  | Some[T]
2216237 626
  | None
2216237 627
b48d3a3 628
fun unwrapOr(o: Option, default: Int) -> Int =
2216237 629
  match o
2216237 630
    Some(v) =>
2216237 631
      v
2216237 632
    None =>
2216237 633
      default
2216237 634
b48d3a3 635
fun use() -> Int =
2216237 636
  unwrapOr(Some(5), 0)
2216237 637
";
2216237 638
    let source = parse(src);
3d6f280 639
    let result = checkSource(&source);
2216237 640
    assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
2216237 641
2216237 642
    // Directly prove `unwrapOr` itself got specialized (not left bare/unresolved).
3d6f280 643
    let mono = plum_checker::monomorphize::monomorphizeSource(&source)
2216237 644
        .expect("monomorphize should succeed");
2216237 645
    let has_specialized_unwrap_or = mono.items.iter().any(|it| matches!(it, Item::Fn(f)
2216237 646
        if f.name.starts_with("unwrapOr$") && f.type_param.is_none()));
2216237 647
    assert!(has_specialized_unwrap_or, "expected a specialized `unwrapOr$...` function in the output");
2216237 648
}
2216237 649
2216237 650
#[test]
3d6f280 651
fn ordinaryFunctionWithBareGenericClassParamTypeChecks() {
2216237 652
    // The same shape, for a generic CLASS param instead of an enum — untested until
2216237 653
    // now, but the identical root cause: `Box` is dropped from the monomorphized
2216237 654
    // output, so a bare `Box`-typed param would otherwise reference nothing.
2216237 655
    let src = "\
30ae945 656
type Box[T] =
30ae945 657
  value: T
2216237 658
7056de2 659
  fun getBoxValue() -> T =
7056de2 660
    self.value
2216237 661
b48d3a3 662
fun sumBox(b: Box) -> Int =
2216237 663
  b.getBoxValue()
2216237 664
b48d3a3 665
fun use() -> Int =
2216237 666
  sumBox(Box(value: 5))
2216237 667
";
2216237 668
    let source = parse(src);
3d6f280 669
    let result = checkSource(&source);
2216237 670
    assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
2216237 671
}
d1ea2ff 672
d1ea2ff 673
#[test]
3d6f280 674
fn closureLiteralInfersAsAFunctionType() {
d1ea2ff 675
    let src = "\
b48d3a3 676
fun useClosure() -> Bool =
d1ea2ff 677
  cb = |v|
d1ea2ff 678
    True
d1ea2ff 679
  cb(5)
d1ea2ff 680
";
d1ea2ff 681
    let source = parse(src);
3d6f280 682
    let result = checkSource(&source);
d1ea2ff 683
    assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
d1ea2ff 684
}
d1ea2ff 685
d1ea2ff 686
#[test]
3d6f280 687
fn fnValueTypedParamCanBeCalled() {
d1ea2ff 688
    let src = "\
b48d3a3 689
fun each(cb: fn(Int) -> Bool) -> Bool =
d1ea2ff 690
  cb(5)
d1ea2ff 691
";
d1ea2ff 692
    let source = parse(src);
3d6f280 693
    let result = checkSource(&source);
d1ea2ff 694
    assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
d1ea2ff 695
}
d1ea2ff 696
47abc49 697
#[test]
3d6f280 698
fn fieldAssignmentTargetWithMatchingTypePasses() {
47abc49 699
    let src = "\
47abc49 700
type Cat =
47abc49 701
  name: Str
47abc49 702
  age: Int
47abc49 703
805d96d 704
  fun haveBirthday() =
805d96d 705
    self.age = self.age + 1
47abc49 706
";
47abc49 707
    let source = parse(src);
3d6f280 708
    assert!(checkSource(&source).is_ok(), "expected Ok");
47abc49 709
}
47abc49 710
47abc49 711
#[test]
3d6f280 712
fn fieldAssignmentTargetWithMismatchedTypeIsError() {
47abc49 713
    let src = "\
47abc49 714
type Cat =
47abc49 715
  name: Str
47abc49 716
  age: Int
47abc49 717
7056de2 718
  fun breakCat() =
7056de2 719
    self.age = \"oops\"
47abc49 720
";
47abc49 721
    let source = parse(src);
3d6f280 722
    let result = checkSource(&source);
47abc49 723
    assert!(result.is_err());
47abc49 724
}
47abc49 725
47abc49 726
#[test]
3d6f280 727
fn fieldAssignmentTargetUnknownFieldIsError() {
47abc49 728
    let src = "\
47abc49 729
type Cat =
47abc49 730
  name: Str
47abc49 731
  age: Int
47abc49 732
7056de2 733
  fun breakCat() =
7056de2 734
    self.nope = 1
47abc49 735
";
47abc49 736
    let source = parse(src);
3d6f280 737
    let result = checkSource(&source);
47abc49 738
    assert!(result.is_err());
47abc49 739
}
47abc49 740
d1ea2ff 741
#[test]
3d6f280 742
fn closurePassedToFnValueTypedParamTypeChecks() {
d1ea2ff 743
    let src = "\
b48d3a3 744
fun each(cb: fn(Int) -> Bool) -> Bool =
d1ea2ff 745
  cb(5)
d1ea2ff 746
b48d3a3 747
fun use() -> Bool =
d1ea2ff 748
  each(|v|
d1ea2ff 749
    True)
d1ea2ff 750
";
d1ea2ff 751
    let source = parse(src);
3d6f280 752
    let result = checkSource(&source);
d1ea2ff 753
    assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
d1ea2ff 754
}
4a494a8 755
4a494a8 756
#[test]
3d6f280 757
fn variadicCallWithZeroTrailingArgsPasses() {
4a494a8 758
    let src = "\
b48d3a3 759
fun sumAll(nums: ...Int) -> Int =
4a494a8 760
  0
4a494a8 761
b48d3a3 762
fun useSumAll() -> Int =
4a494a8 763
  sumAll()
4a494a8 764
";
4a494a8 765
    let source = parse(src);
3d6f280 766
    assert!(checkSource(&source).is_ok(), "expected Ok");
4a494a8 767
}
4a494a8 768
4a494a8 769
#[test]
3d6f280 770
fn variadicCallWithSeveralTrailingArgsPasses() {
4a494a8 771
    let src = "\
b48d3a3 772
fun sumAll(nums: ...Int) -> Int =
4a494a8 773
  0
4a494a8 774
b48d3a3 775
fun useSumAll() -> Int =
4a494a8 776
  sumAll(1, 2, 3)
4a494a8 777
";
4a494a8 778
    let source = parse(src);
3d6f280 779
    assert!(checkSource(&source).is_ok(), "expected Ok");
4a494a8 780
}
4a494a8 781
4a494a8 782
#[test]
3d6f280 783
fn variadicCallWithMismatchedTrailingArgTypeIsError() {
4a494a8 784
    let src = "\
b48d3a3 785
fun sumAll(nums: ...Int) -> Int =
4a494a8 786
  0
4a494a8 787
b48d3a3 788
fun useSumAll() -> Int =
4a494a8 789
  sumAll(1, \"two\")
4a494a8 790
";
4a494a8 791
    let source = parse(src);
3d6f280 792
    assert!(checkSource(&source).is_err());
4a494a8 793
}
4a494a8 794
4a494a8 795
#[test]
3d6f280 796
fn variadicCallWithFixedPrefixPasses() {
4a494a8 797
    let src = "\
b48d3a3 798
fun combine(prefix: Int, rest: ...Int) -> Int =
4a494a8 799
  prefix
4a494a8 800
b48d3a3 801
fun useCombine() -> Int =
4a494a8 802
  combine(1, 2, 3)
4a494a8 803
";
4a494a8 804
    let source = parse(src);
3d6f280 805
    assert!(checkSource(&source).is_ok(), "expected Ok");
4a494a8 806
}
4a494a8 807
4a494a8 808
#[test]
3d6f280 809
fn twoVariadicParamsIsError() {
4a494a8 810
    let src = "\
b48d3a3 811
fun bad(a: ...Int, b: ...Int) -> Int =
4a494a8 812
  0
4a494a8 813
";
4a494a8 814
    let source = parse(src);
3d6f280 815
    assert!(checkSource(&source).is_err());
4a494a8 816
}
4a494a8 817
4a494a8 818
#[test]
3d6f280 819
fn variadicParamNotLastIsError() {
4a494a8 820
    let src = "\
b48d3a3 821
fun bad(a: ...Int, b: Int) -> Int =
4a494a8 822
  0
4a494a8 823
";
4a494a8 824
    let source = parse(src);
3d6f280 825
    assert!(checkSource(&source).is_err());
4a494a8 826
}
4a494a8 827
4a494a8 828
#[test]
3d6f280 829
fn forLoopOverVariadicBindsElementType() {
4a494a8 830
    let src = "\
b48d3a3 831
fun sumAll(nums: ...Int) -> Int =
4a494a8 832
  total = 0
0000000 833
  for v := range nums
4a494a8 834
    total = total + v
4a494a8 835
  total
4a494a8 836
";
4a494a8 837
    let source = parse(src);
3d6f280 838
    assert!(checkSource(&source).is_ok(), "expected Ok");
4a494a8 839
}
4a494a8 840
4a494a8 841
#[test]
3d6f280 842
fn forLoopOverVariadicWithTwoVarsIsError() {
4a494a8 843
    let src = "\
b48d3a3 844
fun bad(nums: ...Int) -> Int =
0000000 845
  for v, i := range nums
4a494a8 846
    v
4a494a8 847
  0
4a494a8 848
";
4a494a8 849
    let source = parse(src);
3d6f280 850
    assert!(checkSource(&source).is_err());
4a494a8 851
}