diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5a4d35c2..aff3985b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,7 +25,7 @@ The workspace has three test crates, each living under `tests/`: |---|---|---| | `functional_tests` | `tests/functional/` | End-to-end `.ndc` programs and REPL integration tests. The crate's `build.rs` auto-generates one Rust `#[test]` per `.ndc` file under `tests/functional/programs/`. | | `compiler_tests` | `tests/compiler/` | Asserts exact bytecode `OpCode` sequences for given source. | -| `proptest_tests` | `tests/proptest/` | Property-based fuzz harness that drives random token streams through the parser → analyser → compiler → VM and looks for panics. | +| `proptest_tests` | `tests/proptest/` | Property-based fuzz harnesses: random token streams through the parser → analyser → compiler → VM, and generated programs that stress the analyser's overload choices. Both look for panics. | ### Run everything @@ -79,6 +79,27 @@ the seed to `tests/proptest/tests/panic.regressions`. **Commit the updated `.regressions` file** — every subsequent `cargo test` will guard against that input. +### Run the overload fuzz harness + +`tests/proptest/tests/overloads.rs` generates well-formed programs built +from what makes the analyser commit to an overload before the types are +fully known: overloaded functions, empty literals, lambdas, function +values, `map`, `if`, writes and annotations. In debug builds the analyser +checks that each such early commit is the overload the call or value gets +once the types are known, so any disagreement panics. The minimal +failing program is printed as source. + +```bash +# Fuzz only (1024 cases by default) +cargo test -p proptest_tests --test overloads -- --ignored + +# Long fuzz run +PROPTEST_CASES=100000 cargo test -p proptest_tests --test overloads -- --ignored +``` + +Its seeds go to `tests/proptest/tests/overloads.regressions`, which +`known_overload_regressions_do_not_panic` replays in every `cargo test`. + ## Writing new tests ### Functional `.ndc` tests diff --git a/Cargo.lock b/Cargo.lock index 94aac531..05061bf0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1288,6 +1288,7 @@ dependencies = [ name = "ndc_macros" version = "0.1.0" dependencies = [ + "ndc_core", "proc-macro2", "quote", "syn", @@ -1323,6 +1324,7 @@ dependencies = [ "factorial", "itertools 0.15.0", "md5", + "ndc_analyser", "ndc_core", "ndc_macros", "ndc_vm", diff --git a/manual/src/features/augmented-assignment.md b/manual/src/features/augmented-assignment.md index 580d4096..c9d269eb 100644 --- a/manual/src/features/augmented-assignment.md +++ b/manual/src/features/augmented-assignment.md @@ -39,49 +39,53 @@ source order. The augmented assignment expression itself evaluates to `()`. ## Type checking -An augmented assignment with an in-place operator such as `++=` never changes -the type of its target. If the right-hand side would force a type change the -program is rejected: +`a op= b` is checked like `a = a op b`: the operator has to take both operands, +and what it returns has to fit `a`. An in-place operator such as `++=` is used +where it does the same thing, so it never changes the type of its target. A +right-hand side whose element type differs is rejected: ```ndc,fails let values = [1]; -values ++= ["two"]; // error: mismatched types: found List but expected List +values ++= ["two"]; // error: No function called '++' found that matches the arguments 'List, List' ``` -The right-hand side has to *provably* fit. A wider element type is not enough, -because the operator copies the values across without checking them: +A wider element type is not enough either, as `++` keeps one element type: ```ndc,fails let values: List = [1]; let rhs: List = [0.5n]; -values ++= rhs; // error: mismatched types: found List but expected List +values ++= rhs; // error: No function called '++' found that matches the arguments 'List, List' ``` -The same goes for an operand whose type is unknown. `Any` says nothing about -what the value holds, and no later step re-checks it, so it is rejected too: +An operand of type `Any` is checked as the program runs, as in `a = a op b`. +What the operator makes is checked where it's written back, so a value that +doesn't fit fails there and leaves the target as it was. As nothing says the +operand fits before it's checked, the operator makes a new value rather than +changing the target in place: -```ndc,fails -fn opaque(x) => x; +```ndc +fn opaque(x: Any) => x; let values = [1]; -values ++= opaque([2, 3]); // error: mismatched types: found Any but expected List +let same = values; +values ++= opaque([2, 3]); +print(values, same); // [1,2,3] [1] ``` -[Cast](casts.md) the operand to say what it holds. The cast checks the value, so -the assignment gets its guarantee from the cast site: - -```ndc -fn opaque(x) => x; +```ndc,fails +fn opaque(x: Any) => x; let values = [1]; -values ++= opaque([2, 3]) as List; -print(values); // [1,2,3] +values ++= opaque([0.5]); // error: expected List but found List ``` -A cast that does not hold fails there rather than corrupting the target: +[Cast](casts.md) the operand to say what it holds, which checks it at the cast +and lets `++=` change the target in place: -```ndc,fails -fn opaque(x) => x; +```ndc +fn opaque(x: Any) => x; let values = [1]; -values ++= opaque([0.5]) as List; // error: cannot cast List to List +let same = values; +values ++= opaque([2, 3]) as List; +print(values, same); // [1,2,3] [1,2,3] ``` Annotate the target with `Any` to opt into heterogeneous contents: @@ -92,13 +96,20 @@ mixed ++= ["two"]; print(mixed); // [1,"two"] ``` -Operators without an in-place implementation behave like `target = target op value` -and may widen the inferred type of the target, just like a regular assignment: +Operators without an in-place implementation behave like `target = target op value`, +so the result has to fit the target's type, just like in a regular assignment: + +```ndc,fails +let numbers = ["a".len()]; // a List +numbers[0] += 0.5; // error: Int + Float is a Float, which doesn't fit in List +``` + +A list of number literals takes the type its uses need, so there the numbers are Floats: ```ndc -let numbers = [1]; -numbers[0] += 0.5; // fine: the element type widens from Int to Any -print(numbers); // [1.5] +let numbers = [1, 2]; +numbers[0] += 0.5; +print(numbers); // [1.5,2.0] ``` ## Optimization @@ -121,7 +132,7 @@ print(x); // 8 One common use case is tracking the highest or lowest value in a loop: ```ndc -fn g(x) => (x - 40) ^ 2; +fn g(x) => (x - 40.0) ^ 2; let lowest, highest = Inf, -Inf; for x in 1..100 { @@ -129,5 +140,5 @@ for x in 1..100 { highest max= g(x); } -print(lowest, highest); // 0 3481 +print(lowest, highest); // 0.0 3481.0 ``` diff --git a/manual/src/features/casts.md b/manual/src/features/casts.md index b5220417..3d7391dd 100644 --- a/manual/src/features/casts.md +++ b/manual/src/features/casts.md @@ -3,19 +3,18 @@ The `as` operator asserts that a value has the given type: ```ndc -let values = %{}; -values.insert(1); -values.insert(2); +let values: List = [1, 2]; -// keys returns List; the cast recovers the element type so the +// values is a List; the cast recovers the element type so the // result works with functions that expect a List. -let keys = values.keys as List; -print(keys.max()); // 2 +let ints = values as List; +print(ints.sum()); // 3 ``` -A cast checks the value without converting it. `5 as Float` is an error -because `5` is an `Int`; use conversion functions like `float(5)` to change -a value's type. +A cast checks the value without converting it. `n as Float` is an error +when `n` is an `Int`; use conversion functions like `float(n)` to change a +value's type. A number literal takes the type the cast says, so `5 as Float` +is the Float `5.0`. ## Checking @@ -38,6 +37,24 @@ missing-key lookup inserts it. A default *function*'s results can't be verified without calling it, so such a map only conforms when the value type is `Any`. +A function is checked against what its own type says: one that takes a +`String` can't be a `Fn(Int) -> Int`. What a function returns often isn't +known until it runs, so a function that may fit a function type returns a +function that checks each call's result: + +```ndc,fails +fn text(x) => "s"; // its value takes and returns anything +let value: Any = text; +let f = value as Fn(Int) -> Int; +f(1) +// error[vm]: expected the function to return Int but it returned String +``` + +Inside a container, where the function can't be replaced by one that checks +its calls, only a function whose own type shows it fits does. So does an +overload picked as the program runs from a value of type `Any`: cast the +value to the function type first to have its calls checked. + The analyser rejects casts between types that cannot share a value: ```ndc,fails @@ -45,12 +62,66 @@ The analyser rejects casts between types that cannot share a value: // error[analysis]: invalid cast: String can never be Int ``` +## Testing a value's type with `as?` + +`value as? T` is the cast that doesn't fail: it is `Some(value)`, as a `T`, +when the value is a `T`, and `None` when it isn't. Its type is `Option`. + +```ndc +let value: Any = 5; +print(value as? Int); // Some(5) +print(value as? String); // None +``` + +It checks a value the way `as` does, so a container is checked element by +element, and a function that may fit a function type comes out checking its +calls: + +```ndc +let mixed: Any = [1, "a"]; +print(mixed as? List); // None +print(mixed as? List); // Some([1,"a"]) + +fn inc(x: Int) => x + 1; +let f: Any = inc; +print((f as? Fn(Int) -> Int).unwrap()(2)); // 3 +print((f as? Fn(String) -> Int).is_some()); // false +``` + +That makes it the way to ask what a value is: + +```ndc +fn describe(value: Any) { + if (value as? Int).is_some() { + "a number" + } else if (value as? String).is_some() { + "some text" + } else { + "something else" + } +} +print(describe(1)); // a number +print(describe("a")); // some text +print(describe([1])); // something else +``` + +An `as?` that can never succeed is rejected like the `as` it stands for, +since it would always be `None`: + +```ndc,fails +"foo" as? Int +// error[analysis]: invalid cast: String can never be Int +``` + ## Precedence -`as` binds tighter than binary operators and looser than unary operators +`as` and `as?` bind tighter than binary operators and looser than unary operators and calls, so `a + b as Int` means `a + (b as Int)`. Method calls bind tighter than the cast: recovering an element type before a method call -needs parentheses, as in `(values.keys as List).max()`. +needs parentheses, as in `(values.keys as List).max()`. An `as?` cast +needs them too before a method call on the option: `(value as? Int).is_some()`. +A name can contain `?`, so `as?` needs a space after it: `x as?Int` reads as +`x` followed by a name `as?Int`. A `<` after the cast type is read as a type argument list when the tokens that follow form one, and as a less-than comparison otherwise, so @@ -61,27 +132,24 @@ that follow form one, and as a less-than comparison otherwise, so The analyser only accepts what it can prove. Where it cannot, a cast states the value's type and checks it at the cast site. -Reassigning a variable widens its type, which can lose the detail a function -needs: +A wider annotation than the value needs can lose the detail a function needs: ```ndc,fails -let line = "a b c"; -line = line.split(" "); // line is now Sequence, not List -line.remove(0); // error: no `remove` matches Sequence, Int +let values: List = [1, 2, 3]; +values.sum(); // error: no `sum` matches List ``` -A cast states that it is still a list: +A cast states what the list holds: ```ndc -let line = "a b c"; -line = line.split(" "); -print((line as List).remove(0)); // a +let values: List = [1, 2, 3]; +print((values as List).sum()); // 6 ``` -A specialized `op=` mutates its target in place and keeps its type, so it only -accepts a right operand that provably fits. A value of unknown type does not, -and a cast gets the elements checked instead of letting them slip into the -target unverified: +A specialized `op=` such as `++=` changes its target in place, so it's only +used with a right operand that provably fits. With a value of unknown type, +`a op= b` makes a new value like `a = a op b` does; a cast checks the elements +first, so the target can be changed in place: ```ndc fn opaque(x) => x; diff --git a/manual/src/reference/memory-management.md b/manual/src/reference/memory-management.md index 217825f4..59fb7a27 100644 --- a/manual/src/reference/memory-management.md +++ b/manual/src/reference/memory-management.md @@ -12,8 +12,8 @@ There are two common ways to create cycles: ```ndc { - let m = %{}; - let n = %{}; + let m: Map = %{}; + let n: Map = %{}; m["n"] = n; n["m"] = m; print(m["n"]["m"] == m); // true diff --git a/manual/src/reference/types.md b/manual/src/reference/types.md index ea21d94e..7b6b43fb 100644 --- a/manual/src/reference/types.md +++ b/manual/src/reference/types.md @@ -2,7 +2,7 @@ Andy C++ runs as a dynamically typed language — values carry their types at runtime and most checking happens then. You can also attach type annotations to variables, function parameters, and return values, and the analyser will use them to flag obvious mismatches before the program runs. -The type system is hierarchical with `Any` at the root: +Every value is an `Any`. The other types are: * `Any` * [`Option`](./types/option.md) @@ -10,7 +10,7 @@ The type system is hierarchical with `Any` at the root: * [`Int`](./types/number.md) — checked signed 64-bit integers * [`Float`](./types/number.md) — IEEE 754 `f64` values * [`Number`](./types/number.md) — arbitrary-size, rational, floating-point, and complex values - * `Sequence` + * Sequences, which a `for` loop iterates over: * [`String`](./types/string.md): a mutable list of characters * [`List`](./types/list.md): a mutable list * [`Tuple`](./types/tuple.md): an immutable list @@ -19,7 +19,7 @@ The type system is hierarchical with `Any` at the root: * [`Deque`](./types/deque.md): a double-ended queue * [`MinHeap` / `MaxHeap`](./types/min-max-heap.md): min/max heap * `Iterator`: produces values when consumed (currently only from range expressions like `5..100`) - * [`Function`](./types/function.md) + * [`Fn(A, B) -> C`](./types/function.md): a function taking an `A` and a `B` and returning a `C` * [Structs](./types/struct.md): user-defined record types, one per `struct` declaration These are also the names you write in annotations. Generic types take their parameters in angle brackets: @@ -27,7 +27,7 @@ These are also the names you write in annotations. Generic types take their para ```ndc let xs: List = [1, 2, 3]; let table: Map = %{"a": 1, "b": 2}; -let maybe: Option = Some("hi"); +let maybe: Option = Some("hi"); let pair: Tuple = (1, "hi"); let pair2: (Int, String) = (1, "hi"); // tuple shorthand @@ -41,6 +41,52 @@ let grid: List> = [[1, 2], [3, 4]]; print(grid); // [[1,2],[3,4]] ``` +### Any kind of sequence + +A parameter without an annotation takes any kind of sequence its body can iterate. The function is compiled once for each kind it's called with: + +```ndc +fn total(xs) { + let sum = 0; + for x in xs { + sum += x; + } + sum +} +print(total([1, 2]), total((3, 4)), total(1..4)); // 3 7 6 +``` + +Built-in functions say this with `Iterable` in their signatures, as `sum(Iterable)` does, but an annotation can't: a value has a kind of sequence, which its annotation names, as in `List`. A tuple whose elements have different types iterates as `Any`. + +### Element types are exact + +A `List` is not a `List`: a `List` could be given a String, which the list of Ints would then hold. The same goes for maps, tuples, options, the other containers, and function types. To change a container's element type, go through `Any`: + +```ndc,fails +let words = "a b".split(" "); // List +let any: Any = words; +let anything: List = any; // fine: checked as the program runs +let wrong: List = words; // error: found List but expected List +``` + +A literal takes the type its use needs, so an annotation can widen it directly. A literal whose values share no type is a container of `Any`: + +```ndc +let xs: List = [1, 2]; +xs.push("three"); +let mixed = [1, "a"]; // List +print(xs, mixed); // [1,2,"three"] [1,"a"] +``` + +The branches of an `if` work the same way. They're one type when they can be, with literals taking the other branch's type, and `Any` when they can't: + +```ndc +let a = if true { 1 } else { 2.5 }; // Float +let b = if true { 1 } else { "a" }; // Any +let c = if true { [] } else { [1] }; // List +print(a, b, c); // 1.0 1 [] +``` + [Struct](./types/struct.md) names are type names like any other, including inside generics: ```ndc @@ -56,6 +102,21 @@ used before (or inside) its own declaration, and it goes out of scope together w block or function that declared it. See [Struct](./types/struct.md) for the scoping rules. -> **Note:** `Any` is the base type for every other type, so an `Any`-annotated binding will accept anything. When a parameter or value has no annotation and the analyser can't infer a type, it falls back to `Any`. There is also a `Never` type used internally for things like `break` that don't produce a value — you'll rarely need to write it by hand. +> **Note:** every value fits `Any`, so an `Any`-annotated binding will accept anything. When a value has no annotation and the analyser can't infer a type, it falls back to `Any`. A parameter of a function declared in the program gets the type of each call's argument instead (see [Generic functions](./types/function.md#generic-functions)). There is also a `Never` type used internally for things like `break` that don't produce a value — you'll rarely need to write it by hand. + +### Using an `Any` value where a type is expected + +You can use an `Any` value where a specific type is expected. The program checks the value when it gets there: + +```ndc,fails +let a: Any = 5; +let n: Int = a; // fine: a holds an Int +print(n + 1); // 6 + +let b: Any = "five"; +let m: Int = b; // error: expected Int but found String +``` + +The same goes for assigning it, passing it to a function, or returning it from one. For a list or a map, every element is checked. -To go the other way — recover a precise type from an `Any`-typed value — use a [cast](../features/casts.md): `value as List`. +You can also check it yourself with a [cast](../features/casts.md): `value as List`. diff --git a/manual/src/reference/types/function.md b/manual/src/reference/types/function.md index 843acf7f..b5c10a48 100644 --- a/manual/src/reference/types/function.md +++ b/manual/src/reference/types/function.md @@ -148,7 +148,7 @@ print(greet("world")); // hello world print(add(1, 2)); // 3 ``` -Annotations are optional — leave them off and the parameter is treated as `Any`. Mix and match as you like: +Annotations are optional. A parameter without one gets its type from how the function is used: each call of a function declared in the program gives it the types of that call's arguments (see [Generic functions](#generic-functions)), and a lambda gets the types of how it's used (see below). Mix and match as you like: ```ndc fn first(xs: List) => xs[0]; // params annotated, return inferred @@ -161,10 +161,143 @@ print(count([1, 2, 3])); // 3 If the body produces a value that doesn't fit the declared return type, the analyser flags it: ```ndc,fails -fn bad() -> Int { "hello" } // ERROR: mismatched types +fn bad() -> Int { "hello" } // error: mismatched types ``` -A return-type annotation also helps the analyser understand recursive calls — without it, a recursive call resolves against an unknown return type and you can lose precision. +A recursive call returns what the function returns, so `fib` below returns an `Int` when it's called with one, without an annotation. + +### Function types + +The type of a function is written `Fn(A, B) -> C`: it takes an `A` and a `B` and returns a `C`. Without `-> …` the function returns `()`: + +```ndc +let twice: Fn(Int) -> Int = fn(x: Int) => x * 2; +let log: Fn(String) = fn(s: String) { print(s); }; + +fn apply(f: Fn(Int) -> Int, x: Int) -> Int { f(x) } +fn make_adder(n: Int) -> Fn(Int) -> Int { fn(x: Int) => x + n } + +print(apply(twice, 4)); // 8 +print(apply(make_adder(10), 1)); // 11 +``` + +A function passed for a function parameter has to take what the parameter passes and return what it returns: + +```ndc,fails +fn apply(f: Fn(Int) -> Int, x: Int) -> Int { f(x) } +apply(fn(s: String) => 1, 4); // error: the function takes a String, but apply passes an Int +``` + +A lambda's unannotated parameters get their types from how the lambda is used: the function type it's stored as, what the function it's passed to gives it, and the calls in its body. Its body is checked with those types: + +```ndc +let shout: Fn(String) -> String = fn(s) => s ++ "!"; // s is a String +print(shout("hi")); // hi! + +let tens = [3, 1, 2].map(fn(x) => x * 10); // x is an Int, so tens is a List +print(tens); // [30,10,20] +``` + +```ndc,fails +let double: Fn(Int) -> Int = fn(x) => x ++ "!"; // error: `x ++ "!"` makes x a String, so the lambda is a Fn(String) -> String +``` + +A lambda passed to a function gets its parameter types from that function and its other arguments before its body is checked, so a mistake in the body is reported where it is: + +```ndc,fails +struct Player { name: String, score: Int } +struct Team { name: String, size: Int } +let players = [Player("ann", 3), Player("bob", 5)]; +players.map(fn(p) => p.size); // error: p is a Player, which has no size +``` + +A lambda stored in a variable has one type, like any variable, so it can't take an `Int` in one call and a `Float` in the next: + +```ndc,fails +let twice = fn(x) => x * 2; +twice("abc".len()); +twice(2.5); // error: twice takes an Int +``` + +A function declared with `fn` in the program, such as `fn twice(x) => x * 2`, can be called with both (see [Generic functions](#generic-functions)). A lambda parameter that nothing gives a type is `Any`, and is checked when the program runs. + +## Generic functions + +A function declared with `fn` in the program takes any argument its body can work with. Each call gets the types its body makes of that call's arguments: + +```ndc +fn inc(x) => x + 1; + +let a: Int = inc(1); // + on an Int and an Int is an Int +let b: Float = inc(2.5); // + on a Float and an Int is a Float + +fn apply(f, x) => f(x); +print(apply(abs, -1)); // 1 +print(apply(fn(s) => s ++ "!", "hi")); // hi! + +fn fib(n) => if n < 2 { n } else { fib(n - 1) + fib(n - 2) }; +let i: Int = fib(10); +print(a, b, i); // 2 3.5 55 +``` + +The body is compiled for each set of types it's called with, so the calls in it are picked when the program is compiled, not as it runs. Called with a value of type `Any`, the function picks them as it runs. + +A number literal in the body is typed by the body alone, as one anywhere else is, and never by a call: the `1` in `inc` is an `Int` whatever `inc` is given. So a literal that a result is made of fixes that result's type. `fact` returns the `1` or a product, which can only be one type, an `Int`, so it doesn't take a `Float`: + +```ndc,fails +fn fact(n) => if n < 2 { 1 } else { n * fact(n - 1) }; +let r = fact(5.0); // error: mismatched types: found Float but expected Int +``` + +Write `1.0` for a `fact` of `Float`s, which works for `Int`s too, as a `Float` times an `Int` is a `Float`: + +```ndc +fn fact(n) => if n < 2 { 1.0 } else { n * fact(n - 1) }; +print(fact(5), fact(5.0)); // 120.0 120.0 +``` + +A call with an argument the body can't use is an error at the call, with a note on the line in the body that can't take it: + +```ndc,fails +fn inc(x) => x + 1; +fn shout(s) => inc(s) ++ "!"; +shout("a"); // error: No function called '+' found that matches the arguments 'String, Int' + // `shout` calls `inc` here, and `inc` needs `x + 1` +``` + +What the body can do with a parameter can make its type known right away: `xs.push(1)` only works on a list of Ints, so `fn f(xs) { xs.push(1) }` takes a `List`. + +A recursive call has to pass the types the function was called with. One that passes something built from a parameter would need a version of the function for every deeper type, so it's an error. Annotate the parameter with `Any` to let it change as the program runs: + +```ndc,fails +fn nest(x, n) { if n == 0 { x } else { nest([x], n - 1) } } // error: `nest` calls itself with an argument built from what it was given +``` + +```ndc +fn nest(x: Any, n) { if n == 0 { x } else { nest([x], n - 1) } } +print(nest(1, 3)); // [[[1]]] +``` + +Only a function declared in the program itself is generic. One declared in another function or in a block has one type, like a lambda in a variable, and its first call decides it: + +```ndc,fails +fn outer() { + fn same(v) => v; + same(1); + same("a"); // error: same takes an Int +} +``` + +A variable declared outside a function has one type too, even when the function is generic, so the first call decides what it holds: + +```ndc,fails +let seen = []; +fn remember(n) { seen.push(n); n } +remember(1); +remember("a"); // error: seen holds Ints, so remember takes an Int +``` + +To take any value whatever its type, annotate the parameter with `Any`: the body then works with it as the program runs. ## Function overloading @@ -190,11 +323,62 @@ print(describe("a")); // string print(describe([1])); // something else ``` -Two overloads in the same scope can't have the same parameter types. Parameter names don't count, and a parameter without a type annotation has type `Any`: +Two overloads in the same scope can't have the same parameter types. Parameter names don't count: ```ndc,fails fn foo(a) { a + 1 } -fn foo(b) { b + 2 } // ERROR: Illegal redefinition of function 'foo' with parameters (Any) +fn foo(b) { b + 2 } // error: Illegal redefinition of function 'foo' with parameters (Any) +``` + +A parameter without a type annotation takes any argument the body can work with, so an overload with one is a fallback, like one taking `Any`, but with a typed body. A call uses the most specific overload that fits it: + +```ndc +fn show(x: Int) { "int" } +fn show(x) { "other" } + +print(show(1)); // int +print(show("a")); // other +``` + +Two overloads in one scope can't both fit a call with neither more specific than the other, as nothing would say which one the call means: + +```ndc,fails +fn f(x: Int, y) { "first" } +fn f(x, y: Int) { "second" } // error: A call could fit both f(_, Int) and f(Int, _), declared earlier in the same scope, and neither is more specific +``` + +## An overloaded function as a value + +A function's name can be used as a value, as in `xs.map(abs)`. When the name has several overloads, the type expected where it's used picks one. `map` passes each element, so over a list of Floats `abs` is `abs(Float)`, and over strings `split` is `split(String)` rather than `split(String, String)`. A function type annotation picks one too, and so do a variable's uses: + +```ndc +print([-1.5, 2.5].map(abs)); // [1.5,2.5] +print(["a b"].map(split)); // [["a","b"]] + +let by_pattern: Fn(String, String) -> List = split; +print(by_pattern("a:b", ":")); // ["a","b"] + +let f = split; +print(f("a b")); // ["a","b"] +``` + +Here `f` is `split(String)`, as it's called with one String. + +When nothing says which overload is meant, it's an error that asks for a function type: + +```ndc,fails +let g = split; // error: 'split' has several overloads, and nothing here says which one is meant +``` + +`print(abs)` is the same error, as `print` takes any value, and so is `[len, abs]`: the other values of a list don't say which overload is meant. + +Like a call, a value uses the nearest scope with an overload that fits. + +In a generic function, each call's types pick the overload, as they pick the calls in its body: + +```ndc +fn signs(xs) => xs.map(signum); +print(signs([3, -2]), signs([0.5, -1.5])); // [1,-1] [1.0,-1.0] ``` ## How a call finds its function diff --git a/manual/src/reference/types/number.md b/manual/src/reference/types/number.md index 82bbc779..df2a998c 100644 --- a/manual/src/reference/types/number.md +++ b/manual/src/reference/types/number.md @@ -8,7 +8,7 @@ Andy C++ exposes three sibling numeric types: * `Float` stores an IEEE 754 `f64`. * `Number` supports arbitrary-size integers, exact rational values, floats, and complex values. -`Int`, `Float`, and `Number` share `Any` as their nearest common supertype. An `Int` does not satisfy a `Number` annotation. Use an `n` literal or the `Number` constructor when you need the advanced mode: +`Int`, `Float`, and `Number` are separate types, which have only `Any` in common. An `Int` value does not satisfy a `Number` annotation. Use an `n` literal or the `Number` constructor when you need the advanced mode: ```ndc let count: Int = 42; @@ -37,6 +37,32 @@ print(binary, octal, hexadecimal); // 42 42 42 An integer literal without `n` must fit in `i64`. The lexer reports an error and suggests the suffixed form when it does not fit. +An integer literal without a suffix is an `Int`, unless how it's used says otherwise. Then it's a `Float` or a `Number`: + +```ndc +print(7 / 2); // 3 +let f: Float = 7; +print(f / 2); // 3.5 + +let total = 0; +total += 2.5; // total is a Float +print(total); // 2.5 + +print([1.5, 2.25].fold(0, fn(acc, x) => acc + x)); // 3.75 +``` + +A value that's already an `Int`, like the result of `len`, stays one. + +A variable has one type, which all its uses decide, including the ones after it's read. So a later use can make a literal a `Float`, and `/` and `\` then divide `Float`s rather than truncate: + +```ndc +let x = 7; +print(x / 2); // 3.5 +x = 2.5; // x is a Float, so the 7 is too +``` + +Write `7.0` or annotate `let x: Int = 7` to say which one is meant. + Arbitrary-radix literals such as `16r2a` remain `Int` literals and do not accept `n`. The `i` and `j` suffixes create complex `Number` values: ```ndc diff --git a/manual/src/reference/types/string.md b/manual/src/reference/types/string.md index a441e832..f716bbaa 100644 --- a/manual/src/reference/types/string.md +++ b/manual/src/reference/types/string.md @@ -39,18 +39,43 @@ print(string); // raw string with r#"stop doing this"# `strip_prefix(string, prefix)` and `strip_suffix(string, suffix)` remove one matching prefix or suffix. `strip_circumfix(string, prefix, suffix)` removes both only when -both match without overlapping. Each function returns a new string on success or -`()` on mismatch, leaving the original string unchanged. Matches are exact and -case-sensitive; empty patterns match without removing any text. +both match without overlapping. Each function returns a new string, leaving the +original string unchanged, and it's an error when the pattern doesn't match. Matches +are exact and case-sensitive; empty patterns match without removing any text. ```ndc -print("foobar".strip_prefix("foo")); // bar -print("foobar".strip_suffix("bar")); // foo -print("[hello]".strip_circumfix("[", "]")); // hello -print("foofoo".strip_prefix("foo")); // foo -print("hello".strip_prefix("x") == ()); // true -print("aaa".strip_circumfix("aa", "aa") == ()); // true -print("[]".strip_circumfix("[", "]") == ""); // true +print("foobar".strip_prefix("foo")); // bar +print("foobar".strip_suffix("bar")); // foo +print("[hello]".strip_circumfix("[", "]")); // hello +print("foofoo".strip_prefix("foo")); // foo +print("[]".strip_circumfix("[", "]") == ""); // true +``` + +A mismatch is an error: + +```ndc,fails +"hello".strip_prefix("x"); // error: "hello" doesn't start with "x" +``` + +When a mismatch isn't an error, the `?` variants return an `Option` instead: + +```ndc +print("foobar".strip_prefix?("foo")); // Some("bar") +print("hello".strip_prefix?("x")); // None +print("aaa".strip_circumfix?("aa", "aa")); // None +``` + +## Splitting once + +`split_once(string, pattern)` splits a string at the first occurrence of `pattern` into +the parts before and after it. It's an error when `pattern` doesn't occur, and +`split_once?` returns `None` instead: + +```ndc +let (key, value) = "port=80".split_once("="); +print(key); // port +print("a=b=c".split_once("=")); // ("a","b=c") +print("abc".split_once?("=")); // None ``` ## Operators diff --git a/manual/src/reference/types/tuple.md b/manual/src/reference/types/tuple.md index 4eabb5ac..9aba9359 100644 --- a/manual/src/reference/types/tuple.md +++ b/manual/src/reference/types/tuple.md @@ -21,6 +21,22 @@ let my_tuple = (1,2,3); my_tuple[0] = 99; // ERROR ``` +An index written as a number, like `t[1]` or `t[-1]`, picks one element, and has that element's type. One past the end is an error before the program runs: + +```ndc +let point = ("origin", 0, 0); +let name: String = point[0]; +let y: Int = point[-1]; +print(name, y); // origin 0 +``` + +```ndc,fails +let point = ("origin", 0, 0); +print(point[3]); // error: Index 3 is out of bounds for a tuple of 3 elements +``` + +An index computed as the program runs, like `t[i]`, could pick any element, so it gives `Any`. + Create a 1-element tuple by adding a trailing comma: ```ndc diff --git a/manual/src/reference/variables-and-scopes.md b/manual/src/reference/variables-and-scopes.md index 0700b035..54cfbc31 100644 --- a/manual/src/reference/variables-and-scopes.md +++ b/manual/src/reference/variables-and-scopes.md @@ -42,31 +42,62 @@ print(x); // 3 ## Reassignment -The `=` operator can be used to reassign a value to an existing variable. When you reassign a variable to a value of a different type, the variable's type is widened to the least upper bound (LUB) of the old and new types. +The `=` operator assigns a new value to an existing variable. A variable keeps the type its first value gave it, so a new value has to fit that type: -```ndc -let x = 1; // the initializer and declaration have type Int -x = 2; // subsequent reads still have type Int -x = 3.14; // subsequent reads have type Any (Int and Float are siblings) -print(x); // 3.14 +```ndc,fails +let n = "abc".len(); // n is Int +n = 2; // fine +n = 3.14; // error: mismatched types: found Float but expected Int ``` -Augmented assignment widens an inferred binding from the operation's result in the same way: +A number literal takes the type its uses need, so a variable that starts at `1` and is later given `3.14` is a `Float` from the start: ```ndc -let total = 3; -total += 0.5; // Int + Float returns Float, so subsequent reads have type Any -print(total); // 3.5 +let x = 1; +x = 3.14; +print(x); // 3.14 +``` + +Augmented assignment is checked the same way, against the result of the operation: + +```ndc,fails +let total = "abc".len(); +total += 0.5; // error: Int + Float is a Float, which doesn't fit in Int +``` + +What an empty list or map holds, or what `None` will hold, isn't known yet. The first assignment that stores something settles it, and later ones have to fit: + +```ndc,fails +let counts = %{}; // key and value types not known yet +counts["a"] = 1; // counts is Map from here on +counts["b"] = "two"; // error: mismatched types: found String but expected Int ``` +Calls settle them too, through the types the called function takes. `push` takes a `List` and a `T`, so pushing an `Int` makes the list a `List`, and later pushes have to fit: + +```ndc,fails +let xs = []; +xs.push(1); // xs is List from here on +xs.push("two"); // error: No function called 'push' found that matches the arguments 'List, String' +``` + +When nothing settles them, they stay `Any`. + +A variable that only gets its real value later holds an `Option`: + ```ndc -let pos = (); // type is () -pos = (1, 2); // type widens to Sequence -pos = ("a", "b"); // type is still Sequence -print(pos); // ("a","b") +let pos = None; +pos = Some((1, 2)); +print(pos); // Some((1,2)) ``` -> **Tip:** For the best type inference, initialize variables with a value that matches the intended type. For example, use `let pos = (0, 0);` instead of `let pos = ();` if you intend to store a 2-tuple of numbers. +To store values of different types in one variable, annotate it with `Any`: + +```ndc +let x: Any = 1; +x = "text"; // fine +print(x); // text +``` ## Type annotations @@ -79,7 +110,7 @@ let xs: List = [1, 2, 3]; print(count, name, xs); // 0 world [1,2,3] ``` -A subtype is fine. All concrete types fit where `Any` is requested: +Every value fits where `Any` is requested: ```ndc let n: Number = 3n; @@ -103,7 +134,7 @@ let nested: List> = [[]]; print(xs, m, nested); // [] %{} [[]] ``` -Once a binding has an annotation, it stays locked to that type. Reassignment and augmented assignment can't widen it the way they widen an inferred binding: +An annotated variable keeps its annotated type, just as an inferred one keeps the type of its first value: ```ndc,fails let x: Int = 5; @@ -115,8 +146,6 @@ let x: Int = 5; x += 0.5; // ERROR: the Float result doesn't fit in Int ``` -If you want a binding that widens freely, just leave the annotation off. Annotations are opt-in. - The same syntax shows up on function parameters and return types — see the [Function](./types/function.md) page. See [Types](./types.md) for the full list of names you can use, including generics like `List`, `Map`, and tuple shorthand `(Int, String)`. diff --git a/manual/src/reference/while-loop.md b/manual/src/reference/while-loop.md index 0984c591..16a6a0aa 100644 --- a/manual/src/reference/while-loop.md +++ b/manual/src/reference/while-loop.md @@ -20,3 +20,18 @@ while n <= 100 { n += 1; } ``` + +A `while true` loop that no `break` leaves never ends, so it has no value at all, like a `return`. A function can end in one and leave it with `return`, and it returns what its `return`s do: + +```ndc +fn first_square_above(n) { + let i = 1; + while true { + if i * i > n { + return i * i; + } + i += 1; + } +} +print(first_square_above(50) + 1); // 65 +``` diff --git a/manual/src/troubleshooting/overload-dispatch-collections.md b/manual/src/troubleshooting/overload-dispatch-collections.md index 894a493f..d918dcd2 100644 --- a/manual/src/troubleshooting/overload-dispatch-collections.md +++ b/manual/src/troubleshooting/overload-dispatch-collections.md @@ -2,56 +2,42 @@ ## Background -When Andy C++ can determine at compile time which function overload to call, it does so — the -call is free of any type-checking overhead at runtime. When it cannot (because an argument was -inferred as `Any`), the VM performs **dynamic dispatch**: it tests each candidate overload at -runtime to find the best match. +Andy C++ picks the overload a call uses before the program runs, from the types of its arguments, so the call costs nothing extra at runtime. Only when an argument has type `Any`, because it was annotated as `Any` or comes from a function that returns `Any`, does the VM pick as the program runs, by testing each candidate overload against the values. -The reverse also holds. Sometimes every argument type is known at compile time, but no -overload can accept them: each candidate is fully annotated, and none matches the call's -arity and argument types. Such a call could only ever fail at runtime. Instead of waiting -for that, it is rejected at compile time with a -`No function called '…' found that matches the arguments` error. +The reverse also holds. When every argument type is known and no overload can accept them, the call could only ever fail at runtime, so it is rejected before the program runs with a `No function called '…' found that matches the arguments` error. -## O(1) dispatch guarantee +## Element types cost a scan -For dynamic dispatch the VM checks whether a value *conforms to* the parameter type without -iterating the container contents. Specifically: +To pick an overload for a value of type `Any`, the VM first looks only at what kind of value it is, which doesn't depend on its size: | Parameter type | Check performed | |---|---| | `List` | Is the value a list? | | `Map` | Is the value a map? | | `Deque` | Is the value a deque? | -| `Sequence` | Is the value any sequence type? | +| `Iterable` | Is the value any sequence type? | | `String`, `Int`, … | Exact kind check | -This means that dispatch is **O(1)** regardless of how many elements are in the collection. - -## Limitation: element types are not checked at runtime - -Because the element-type check is skipped, the VM **cannot distinguish** overloads that differ -only in their container element types via dynamic dispatch. For example, two hypothetical overloads: +A parameter like `List` doesn't take a list in that first pass. When no overload takes the arguments without looking at their elements, the VM checks the elements before the call fails, scanning each container. So two overloads like these still work for a value of type `Any`: ``` -fn process(List) -fn process(List) +fn process(xs: List) +fn process(xs: List) ``` -would both fail to match under dynamic dispatch if the list type cannot be resolved at compile -time, because verifying element types would require scanning the entire container. +Each call through an `Any` scans the list, though, and an overload that takes the value without looking at its elements, like `process(x: Any)`, is picked before either of them. -Standard library overloads do differ by element type. Numeric sequence functions preserve the -concrete numeric type, so `sum` has three typed overloads: +## A `List` fits no typed overload + +Standard library overloads also differ by element type. Numeric sequence functions keep the numeric type, so `sum` has three typed overloads: ``` -fn sum(Sequence) -> Int -fn sum(Sequence) -> Float -fn sum(Sequence) -> Number +fn sum(Iterable) -> Int +fn sum(Iterable) -> Float +fn sum(Iterable) -> Number ``` -A value the analyser only knows as `List` matches none of them. Rather than dispatch on -it and scan, the call is rejected at compile time: +A value known only as a `List` fits none of them, as its elements could be anything. Rather than pick as the program runs and scan, the call is rejected before it runs: ```ndc,fails let values: List = [1, 2, 3]; @@ -61,35 +47,15 @@ values.sum() // as in `value as List`. ``` -Overloads that differ only by container *kind* (e.g. `pop(List)` vs `pop(MinHeap)`) -are still distinguished by the kind check alone. User-defined functions cannot yet declare -typed container parameters (the syntax is not implemented), so user overloads always use -`Any`. - ## Workaround -State the element type with a [cast](../features/casts.md). A cast scans the value, but the -cost lands at a site you wrote rather than inside every dispatch: +State the element type with a [cast](../features/casts.md). A cast scans the value, but the cost lands at a site you wrote rather than inside every call: ```ndc -let values = %{}; -values.insert(1); -values.insert(2); +let values: List = [1, 2, 3]; -// keys is typed List; the cast recovers Int so sum can resolve. -print((values.keys as List).sum()); // 3 +// values is typed List; the cast recovers Int so sum can resolve. +print((values as List).sum()); // 6 ``` -Otherwise, move the call to a location where Andy C++ can infer the argument types -statically — for example, directly at the call site rather than through an intermediate -untyped function parameter: - -```ndc,norun -// The type of `data` is Any here — dynamic dispatch used -fn handle(data) { - process(data) -} - -// Preferred: call process() directly where the type is known -process(my_list) -``` +Better still, leave out the `Any`: a value keeps the type it was created with unless it passes through something typed `Any`, and a parameter without an annotation takes the type of each call's argument. diff --git a/ndc_analyser/RETURN_TYPE_INFERENCE.md b/ndc_analyser/RETURN_TYPE_INFERENCE.md deleted file mode 100644 index 507c5491..00000000 --- a/ndc_analyser/RETURN_TYPE_INFERENCE.md +++ /dev/null @@ -1,98 +0,0 @@ -# Return type inference problem and solution - -## The problem - -The analyser infers function return types from the body expression's type. The body is usually a `Block`, and a block's type is the type of its last expression. - -The issue: when a function uses an explicit `return` statement with a semicolon (which is common), the parser wraps it as `Statement(Return { value })`. The `Statement` handler discards the inner type and returns `unit`: - -```rust -Expression::Statement(inner) => { - self.analyse(inner)?; - Ok(StaticType::unit()) // ← return type lost -} -``` - -This means a function like: - -```ndc -fn make_fn() { - fn inner() { 42 } - return inner; // ← semicolon makes this a Statement(Return) -} -``` - -...has its return type inferred as `Tuple([])` (unit) instead of `Function([] -> Int)`. - -### Why this matters now - -Previously this was masked because the function resolution fallback (`find_all_slots_by_name`) included ALL bindings regardless of type. A variable like `let f = make_fn()` with type `unit` would still be included in the dynamic overload set, and the VM would resolve it correctly at runtime. - -When we changed function resolution to skip known non-callable types (so that `let foo = 300; foo(42)` produces a compile error instead of a confusing runtime error), the mis-typed `unit` bindings got filtered out, breaking legitimate code. - -## The solution: `Never` bottom type + `return_type_stack` - -Two complementary changes: - -### 1. `StaticType::Never` — bottom type for diverging expressions - -`return`, `break`, and `continue` now produce `StaticType::Never` instead of the value's type or unit. `Never` is a subtype of every type, so `lub(T, Never) = T`. The `Statement` handler propagates `Never` instead of converting it to unit: - -```rust -Expression::Statement(inner) => { - let typ = self.analyse(inner)?; - if typ == StaticType::Never { - Ok(StaticType::Never) // diverging — don't convert to unit - } else { - Ok(StaticType::unit()) - } -} -``` - -This means `return 1;` (a `Statement(Return)`) has type `Never`, not unit. When the block computes its implicit return type, `lub(previous_type, Never) = previous_type` — the dead code after a return doesn't pollute the type. - -Without this, `return 1;` would contribute both `Int` (via the return stack) and `unit` (via the block's implicit type), and `lub(Int, unit) = Any` — making every function with explicit returns infer `Any`. - -### 2. `return_type_stack` — tracking explicit return types - -Added a `return_type_stack: Vec>` to the `Analyser` struct. It tracks explicit `return` types for each nested function scope: - -1. **On function entry**: push `None` onto the stack -2. **On `return expr`**: analyse `expr`, `fold_lub` its type into the top of the stack, return `Never` -3. **On function exit**: pop the stack, combine with the block's implicit return type: - -```rust -let implicit_return = self.analyse(body)?; -let explicit_return = self.return_type_stack.pop().unwrap(); - -let return_type = match explicit_return { - Some(ret) => ret.lub(&implicit_return), - None => implicit_return, -}; -``` - -### How they work together - -For `fn foo() { return 1; }`: -- `return 1` → pushes `Int` to stack, returns `Never` -- `Statement(Return)` → propagates `Never` -- Block implicit type: `Never` -- Stack: `Some(Int)` -- Combined: `lub(Int, Never) = Int` ✓ - -For `fn foo(x) { if x > 0 { return x; } "default" }`: -- `return x` → pushes `Int` to stack, returns `Never` -- `Statement(Return)` → `Never` -- If-true branch: `Never`, if-false (implicit): unit -- `lub(Never, unit) = unit` → if expression type is unit -- Statement wrapping if → unit -- `"default"` → `String` -- Block implicit type: `String` -- Stack: `Some(Int)` -- Combined: `lub(Int, String) = Any` ✓ - -For `fn foo() { 1 }`: -- No explicit return -- Block implicit type: `Int` -- Stack: `None` -- Combined: `Int` ✓ diff --git a/ndc_analyser/src/analyser/assignments.rs b/ndc_analyser/src/analyser/assignments.rs index b9078e5b..c87bfaad 100644 --- a/ndc_analyser/src/analyser/assignments.rs +++ b/ndc_analyser/src/analyser/assignments.rs @@ -1,4 +1,5 @@ use super::Analyser; +use super::solver::{CallNodes, CallSite}; use crate::error::AnalysisError; use crate::overloads::{CallKind, ResolvedCall}; use itertools::izip; @@ -6,6 +7,7 @@ use ndc_analysis::{Binding, Candidate}; use ndc_core::{NodeId, StaticType}; use ndc_lexer::Span; use ndc_parser::{AssignmentTarget, AssignmentTargetLocation, ExpressionLocation}; +use ndc_resolver::SymbolId; impl Analyser { /// Analyses `l_value op= r_value`: picks the in-place overload (`+=`) if one @@ -21,90 +23,88 @@ impl Analyser { ) -> StaticType { let left_type = self.analyse_assignment_target(l_value, span); let right_type = self.analyse_expr(r_value); - let arg_types = vec![left_type.clone(), right_type.clone()]; + // `a op= b` is `a = op(a, b)`: the operator is a call like any other, + // and the in-place overload is picked along with it where it does the + // same (see `record_pick`). + let (left_type, right_type) = ( + self.vars.resolve(&left_type), + self.vars.resolve(&right_type), + ); + if !self + .overloads() + .reachable_symbols(&self.resolver.augmented_candidates(id).operator) + .is_empty() + { + let result = self.constrain_call( + CallSite::OpAssignment(id), + vec![left_type.clone(), right_type], + // What the operator returns is checked where it's written + // back, when the program picks the operator as it runs. + CallNodes { + arguments: vec![None, Some(r_value.id)], + result: Some(l_value.id), + }, + CallKind::Operator, + operation, + span, + ); + self.validate_assignment_write(l_value, &left_type, &result, span); + return StaticType::unit(); + } + let written = self.in_place_only(id, &left_type, &right_type, operation, span); + self.validate_assignment_write(l_value, &left_type, &written, span); + StaticType::unit() + } - let ResolvedCall { - binding: assign_binding, - return_type: assign_return, - } = self.overloads().resolve_call( + /// Picks the in-place overload `a op= b` calls when there's no operator + /// `op` to fall back on, and returns the type of the value written back + /// to `a`, which an in-place overload doesn't change. + fn in_place_only( + &mut self, + id: NodeId, + left_type: &StaticType, + right_type: &StaticType, + operation: &str, + span: Span, + ) -> StaticType { + let arguments = [left_type.clone(), right_type.clone()]; + // An operand whose check failed was reported there. + if arguments.iter().any(StaticType::has_errors) { + return StaticType::Error; + } + let ResolvedCall { binding, .. } = self.overloads().resolve_call( &self.resolver.augmented_candidates(id).in_place, - &arg_types, - CallKind::Operator, - ); - let ResolvedCall { - binding: op_binding, - return_type: op_return, - } = self.overloads().resolve_call( - &self.resolver.augmented_candidates(id).operator, - &arg_types, + &arguments, CallKind::Operator, ); - let has_op_binding = !matches!(op_binding, Binding::None); - let assign_is_eligible = Self::augmented_rhs_is_compatible(&left_type, &right_type); - - let writeback_type = match assign_binding { - Binding::Resolved(candidate) if assign_is_eligible => { - self.picks - .augmented_assignments - .insert(id, Binding::Resolved(candidate)); - None - } - Binding::Dynamic(mut assign_candidates) if assign_is_eligible => { - match op_binding.clone() { - Binding::Resolved(candidate) => { - if !assign_candidates.contains(&candidate) { - assign_candidates.push(candidate); - } - } - Binding::Dynamic(op_candidates) => { - for candidate in op_candidates { - if !assign_candidates.contains(&candidate) { - assign_candidates.push(candidate); - } - } - } - Binding::None => {} - } + if matches!(binding, Binding::None) { + self.emit(AnalysisError::function_not_found( + operation, &arguments, span, + )); + return StaticType::Error; + } + self.record_augmented_pick(id, binding); + left_type.clone() + } - let result_type = if has_op_binding { - assign_return.lub(&op_return) - } else { - assign_return - }; - self.picks - .augmented_assignments - .insert(id, Binding::Dynamic(assign_candidates)); - Some(result_type) - } - Binding::Resolved(_) | Binding::Dynamic(_) => { - // A specialized mutation exists, but using it would - // change the concrete left type. Reject it here rather - // than falling through to an ordinary operation whose - // erased return type could widen the same target. - self.emit(AnalysisError::mismatched_types( - &right_type, - &left_type, - span, - )); - None - } - Binding::None if has_op_binding => { - self.picks.augmented_assignments.insert(id, op_binding); - Some(op_return) - } - Binding::None => { - self.emit(AnalysisError::function_not_found( - operation, &arg_types, span, - )); - None - } + /// What `a op= b` does when the program picks as it runs: the in-place + /// overloads `in_place` first, so it mutates in place where it can, and + /// then the operators `operators` picks that aren't among them. + pub(super) fn in_place_then_operators( + mut in_place: Vec>, + operators: Binding, + ) -> Binding { + let operators = match operators { + Binding::Resolved(operator) => vec![operator], + Binding::Dynamic(operators) => operators, + Binding::None => Vec::new(), }; - - if let Some(result_type) = writeback_type { - self.validate_assignment_write(l_value, &left_type, &result_type, span); + for operator in operators { + if !in_place.contains(&operator) { + in_place.push(operator); + } } - - StaticType::unit() + Binding::Dynamic(in_place) } /// Records where an assignment stores its value, and returns the type of @@ -118,77 +118,43 @@ impl Analyser { AssignmentTarget::Identifier { .. } => { let symbol = self.resolver.assigned_symbol(lvalue.id); self.picks.variables.insert(lvalue.id, symbol); - self.types.get_type(symbol).clone() + self.vars.resolve(self.types.get_type(symbol)) } AssignmentTarget::Index { index, value } => { // The container before the index, in source order like the resolver. - let type_of_index_target = self.analyse_expr(value); + let container = self.analyse_expr(value); let index_type = self.analyse_expr(index); - - let get_args = [type_of_index_target.clone(), index_type.clone()]; - let set_args = [ - type_of_index_target.clone(), - index_type.clone(), - StaticType::Any, - ]; - - // Indexing isn't operator-form for vec purposes: there's no - // natural broadcast story for `(list_a, list_b)[i]`. Only an - // augmented assignment reads the old value through `[]`. - let mut getter_missing = false; - if let Some(getters) = self.resolver.getter_candidates(lvalue.id) { - let getter = self - .overloads() - .resolve_call(getters, &get_args, CallKind::Regular) - .binding; - getter_missing = matches!(getter, Binding::None); - self.picks.getters.insert(lvalue.id, getter); - } - let setter = self - .overloads() - .resolve_call( - self.resolver.setter_candidates(lvalue.id), - &set_args, + // `xs[i] op= v` reads the old value with `[]`, so the value + // written has the type that reading gives. A plain write + // stores a value whose type the write settles. + let stored = match self.resolver.getter_candidates(lvalue.id) { + Some(_) => self.constrain_call( + CallSite::Getter(lvalue.id), + vec![container.clone(), index_type.clone()], + CallNodes { + arguments: vec![Some(value.id), Some(index.id)], + result: None, + }, CallKind::Regular, - ) - .binding; - let setter_missing = matches!(setter, Binding::None); - self.picks.setters.insert(lvalue.id, setter); - - let Some(element_type) = type_of_index_target.index_element_type() else { - self.emit(AnalysisError::unable_to_index_into( - &type_of_index_target, + "[]", span, - )); - return StaticType::Any; + ), + None => self.vars.fresh(StaticType::Any), }; - - // A tuple has `[]` but no `[]=`: it can be indexed but not - // changed in place. One error says so; the value isn't also - // checked against a slot that can't be written. - if getter_missing { - self.emit(AnalysisError::function_not_found( - "[]", - &get_args, - lvalue.span, - )); - return StaticType::Any; - } - if setter_missing { - self.emit(AnalysisError::function_not_found( - "[]=", - &set_args, - lvalue.span, - )); - return StaticType::Any; - } - - if Self::index_type_is_slice(&index_type) - && let StaticType::List(element) = &type_of_index_target - { - return StaticType::List(element.clone()); - } - element_type + // `xs[i] = v` is the call `[]=(xs, i, v)`, which waits for + // the value's type when nothing else picks the overload. + self.constrain_call( + CallSite::Setter(lvalue.id), + vec![container, index_type, stored.clone()], + CallNodes { + arguments: vec![Some(value.id), Some(index.id), Some(lvalue.id)], + result: None, + }, + CallKind::Regular, + "[]=", + span, + ); + stored } // What `(a, b) = …` can store is a tuple of what each target can. AssignmentTarget::Sequence(seq) => StaticType::Tuple( @@ -201,51 +167,40 @@ impl Analyser { member, member_span, } => { + // `p.x = v` stores through `x=(p, v)`, and what it stores + // has the type `x(p)` reads. let receiver_type = self.analyse_expr(receiver); - let getter_args = [receiver_type.clone()]; - let setter_args = [receiver_type, StaticType::Any]; - let getters = self - .resolver - .getter_candidates(lvalue.id) - .expect("internal error: member target without getter candidates"); - let getter = - self.overloads() - .resolve_call(getters, &getter_args, CallKind::Regular); - let setter_name = format!("{member}="); - let setter = self.overloads().resolve_call( - self.resolver.setter_candidates(lvalue.id), - &setter_args, + let stored = self.constrain_call( + CallSite::Getter(lvalue.id), + vec![receiver_type.clone()], + CallNodes { + arguments: vec![Some(receiver.id)], + result: None, + }, CallKind::Regular, + member, + *member_span, ); - let getter_missing = matches!(getter.binding, Binding::None); - let setter_missing = matches!(setter.binding, Binding::None); - - self.picks.getters.insert(lvalue.id, getter.binding); - self.picks.setters.insert(lvalue.id, setter.binding); - - if getter_missing { - self.emit(AnalysisError::function_not_found( - member, - &getter_args, - *member_span, - )); - } else if setter_missing { - self.emit(AnalysisError::function_not_found( - &setter_name, - &setter_args, - *member_span, - )); - } - - getter.return_type + self.constrain_call( + CallSite::FieldSetter(lvalue.id), + vec![receiver_type, stored.clone()], + CallNodes { + arguments: vec![Some(receiver.id), Some(lvalue.id)], + result: None, + }, + CallKind::Regular, + &format!("{member}="), + *member_span, + ); + stored } } } /// An index expression typed as an integer sequence (e.g. `0..2`) selects /// a slice of the container rather than a single element. - fn index_type_is_slice(index_type: &StaticType) -> bool { - index_type.is_subtype(&StaticType::Iterator(Box::new(StaticType::Int))) + pub(super) fn index_type_is_slice(index_type: &StaticType) -> bool { + index_type.fits(&StaticType::Iterator(Box::new(StaticType::Int))) } /// Specialized `op=` implementations mutate the left operand in place and @@ -256,22 +211,82 @@ impl Analyser { /// /// A tuple left-hand side represents vector dispatch, so a scalar right /// operand must be compatible with every concrete tuple element. - fn augmented_rhs_is_compatible(left_type: &StaticType, right_type: &StaticType) -> bool { + pub(super) fn augmented_rhs_is_compatible( + left_type: &StaticType, + right_type: &StaticType, + ) -> bool { match (left_type, right_type) { (StaticType::Tuple(left), StaticType::Tuple(right)) => { left.len() == right.len() - && right - .iter() - .zip(left) - .all(|(right, left)| right.is_subtype(left)) + && right.iter().zip(left).all(|(right, left)| right.fits(left)) } - (StaticType::Tuple(left), right) => left.iter().all(|left| right.is_subtype(left)), - (left, right) => right.is_subtype(left), + (StaticType::Tuple(left), right) => left.iter().all(|left| right.fits(left)), + (left, right) => right.fits(left), } } - /// Validate a value that will be stored through an lvalue, widening an - /// inferred binding when the location has a stable variable to update. + /// Whether a value of `value_type` can be stored where values of + /// `stored_type` are kept, settling type variables on the way: in + /// `let xs = []; xs[0] = 1`, the write settles the element type of `xs` + /// as `Int`. The stored type never widens to fit the value. An `Any` + /// value fits anywhere, as `Any` is the dynamic type, and is checked at + /// `node` as the program runs. + pub(super) fn accepts_write( + &mut self, + stored_type: &StaticType, + value_type: &StaticType, + node: Option, + ) -> bool { + // A write passes the value where a value of the stored type is + // expected, as a call passes an argument: `let xs: Iterable = + // []` settles the list's elements as Ints. + let snapshot = self.vars.snapshot(); + if !self.vars.bind_argument(stored_type, value_type) { + self.vars.rollback(snapshot); + return false; + } + if let Some(node) = node + && self.vars.resolve(value_type) == StaticType::Any + { + self.expect(node, stored_type.clone()); + } + true + } + + /// Checks that a value of `value_type` can be stored where values of + /// `stored_type` are kept, as [`Self::accepts_write`], and reports it + /// where it can't. A constraint that waited settles the type it gave + /// out this way, which code analysed meanwhile may have settled already. + pub(super) fn check_write( + &mut self, + stored_type: &StaticType, + value_type: &StaticType, + span: Span, + node: Option, + ) { + if !self.accepts_write(stored_type, value_type, node) { + self.emit_rejected_write(stored_type, value_type, span); + } + } + + /// Reports that a value of `value_type` can't be stored where values of + /// `stored_type` are kept. + pub(super) fn emit_rejected_write( + &mut self, + stored_type: &StaticType, + value_type: &StaticType, + span: Span, + ) { + let error = AnalysisError::mismatched_types( + &self.vars.shown(value_type), + &self.vars.shown(stored_type), + span, + ); + self.emit(error); + } + + /// Checks a value that will be stored through an lvalue against the type + /// stored there. pub(super) fn validate_assignment_write( &mut self, lvalue: &AssignmentTargetLocation, @@ -283,95 +298,10 @@ impl Analyser { AssignmentTarget::Identifier { .. } if self.picks.variables.contains_key(&lvalue.id) => { - let target = self.resolver.assigned_symbol(lvalue.id); - let widened = stored_type.lub(value_type); - if widened != *stored_type - && let Err(annotated_type) = self.types.update_binding_type(target, widened) - && !value_type.is_subtype(&annotated_type) - { - self.emit(AnalysisError::mismatched_types( - value_type, - &annotated_type, - span, - )); - } + self.check_write(stored_type, value_type, span, Some(lvalue.id)); } - AssignmentTarget::Member { .. } => { - if !value_type.is_subtype(stored_type) { - self.emit(AnalysisError::mismatched_types( - value_type, - stored_type, - span, - )); - } - } - AssignmentTarget::Index { value, index, .. } => { - if value_type.is_subtype(stored_type) { - return; - } - - let is_slice = self - .result - .analysis - .expr_types - .get(&index.id) - .is_some_and(Self::index_type_is_slice); - let (stored_element_type, value_element_type) = if is_slice { - let Some(stored_element_type) = stored_type.index_element_type() else { - self.emit(AnalysisError::mismatched_types( - value_type, - stored_type, - span, - )); - return; - }; - let Some(value_element_type) = value_type.sequence_element_type() else { - self.emit(AnalysisError::mismatched_types( - value_type, - stored_type, - span, - )); - return; - }; - (stored_element_type, value_element_type) - } else { - (stored_type.clone(), value_type.clone()) - }; - - if matches!( - self.picks.bindings.get(&value.id), - Some(Binding::Resolved(Candidate::Scalar(_))) - ) { - let target = self.resolver.innermost_reference(value.id); - let container_type = self.types.get_type(target).clone(); - // An earlier target of the same destructuring may already - // have widened the container, as in `l[0], l[1] = …`, so - // check against its current type rather than `stored_type`. - if container_type - .index_element_type() - .is_some_and(|element| value_element_type.is_subtype(&element)) - { - return; - } - let widened_container = container_type.lub( - &container_type - .with_element_type(stored_element_type.lub(&value_element_type)), - ); - if widened_container != container_type - && self - .types - .update_binding_type(target, widened_container) - .is_ok() - { - return; - } - } - - self.emit(AnalysisError::mismatched_types( - value_type, - stored_type, - span, - )); + AssignmentTarget::Member { .. } | AssignmentTarget::Index { .. } => { + self.check_write(stored_type, value_type, span, Some(lvalue.id)); } // Each target gets the element unpacked for it, as in a `let`. AssignmentTarget::Sequence(targets) => { diff --git a/ndc_analyser/src/analyser/calls.rs b/ndc_analyser/src/analyser/calls.rs index 80bb16ee..fef46dc0 100644 --- a/ndc_analyser/src/analyser/calls.rs +++ b/ndc_analyser/src/analyser/calls.rs @@ -1,17 +1,18 @@ use super::Analyser; +use super::solver::{CallNodes, CallSite}; use crate::error::AnalysisError; -use crate::overloads::{CallKind, Overloads, ResolvedCall}; -use ndc_analysis::Binding; +use crate::overloads::{CallKind, Overloads}; +use itertools::Itertools; use ndc_core::{NodeId, StaticType}; -use ndc_lexer::Span; +use ndc_lexer::{NumericLiteral, Span}; use ndc_parser::{Expression, ExpressionLocation}; -use ndc_resolver::Candidates; +use ndc_resolver::{Candidates, SymbolId}; impl Analyser { /// Why no candidate accepts a call of `name` with arguments of these types: /// a variable ended the search, a cast would make an overload fit, or /// nothing fits. - fn no_match( + pub(super) fn no_match( &self, candidates: &Candidates, name: &str, @@ -23,7 +24,7 @@ impl Analyser { let hides_functions = overloads.hides_functions(candidates); AnalysisError::variable_rejects_arguments( name, - self.types.get_type(variable), + &self.vars.shown(self.types.get_type(variable)), types, hides_functions, span, @@ -32,12 +33,36 @@ impl Analyser { AnalysisError::function_not_found_suggest_cast(name, types, span) } else { AnalysisError::function_not_found(name, types, span) + .with_overloads(name, &self.what_the_overloads_take(candidates)) } } + /// What each overload `candidates` can reach takes, as `(Int, String)`, + /// when there are few enough to list: why `put("a")` doesn't fit a `put` + /// whose body made its parameter an Int. + fn what_the_overloads_take(&self, candidates: &Candidates) -> Vec { + const MOST: usize = 3; + let symbols = self.overloads().reachable_symbols(candidates); + if symbols.len() > MOST { + return Vec::new(); + } + symbols + .into_iter() + .filter_map( + |symbol| match self.vars.shown(self.types.get_type(symbol)) { + StaticType::Function { + parameters: Some(parameters), + .. + } => Some(format!("({})", parameters.iter().join(", "))), + _ => None, + }, + ) + .collect() + } + /// Overload resolution over the types declared so far. pub(super) fn overloads(&self) -> Overloads<'_> { - Overloads::new(&self.types, self.resolver.symbols()) + Overloads::new(&self.types, &self.vars, self.resolver.symbols()) } /// Analyses `receiver.member`, the call `member(receiver)`. @@ -50,82 +75,287 @@ impl Analyser { ) -> StaticType { // `xs.len` is the call `len(xs)`, among functions only. let receiver_type = self.analyse_expr(receiver); - let getters = self - .resolver - .getter_candidates(id) - .expect("internal error: member access without getter candidates"); - let ResolvedCall { - binding, - return_type, - } = self.overloads().resolve_call( - getters, - std::slice::from_ref(&receiver_type), + self.constrain_call( + CallSite::Getter(id), + vec![receiver_type], + CallNodes { + arguments: vec![Some(receiver.id)], + result: Some(id), + }, CallKind::Regular, - ); - - if matches!(binding, Binding::None) { - let error = self.no_match(getters, member, &[receiver_type], member_span); - self.emit(error); - } - - self.picks.getters.insert(id, binding); - return_type + member, + member_span, + ) } /// Resolves a call (regular or operator-form) and returns its result type. /// Only operator-form calls are eligible for vec dispatch. pub(super) fn analyse_call( &mut self, + id: NodeId, function: &ExpressionLocation, arguments: &[ExpressionLocation], kind: CallKind, span: Span, ) -> StaticType { // Higher-order call shapes like `get_function()()` have a non-identifier - // function position; in that case we just analyse the callee as a value - // and trust the runtime to dispatch. The callee comes before the - // arguments, in source order like the resolver, so declarations in it - // get their slots first. + // function position, so the callee is analysed as a value. The callee + // comes before the arguments, in source order like the resolver, so + // declarations in it get their slots first. + let nodes = arguments.iter().map(|arg| Some(arg.id)).collect::>(); let Expression::Identifier { name, .. } = &function.expression else { let callee_type = self.analyse_expr(function); - for arg in arguments { - self.analyse_expr(arg); + let arguments = arguments + .iter() + .map(|arg| self.analyse_expr(arg)) + .collect::>(); + // A call needs to know what it calls now, so what a literal + // holds, like the functions in `[f, g][0](1)`, is settled. + if let StaticType::Var(var) = self.vars.resolve(&callee_type) + && self.vars.is_literal(var) + { + self.vars.settle_with_fallbacks(&[var]); + self.solve_waiting(); } - return match callee_type { - StaticType::Function { return_type, .. } => *return_type, - StaticType::Any => StaticType::Any, + return match self.vars.resolve(&callee_type) { + // The callee's type says what it takes, like a picked overload's. + callee @ StaticType::Function { + parameters: Some(_), + .. + } => { + if let Some((typ, parameters)) = self.bind_signature(&callee, &arguments) { + let checks = self.expected_of_any(¶meters, &arguments, &nodes); + self.pending.checks.extend(checks); + let iterations = self.iterations_waited_for(¶meters, &arguments); + self.pending.iterations.extend(iterations); + self.flush_copies(None, span); + typ + } else { + let arguments = arguments + .iter() + .map(|argument| self.vars.shown(argument)) + .collect::>(); + self.emit(AnalysisError::callee_rejects_arguments( + &self.vars.shown(&callee), + &arguments, + span, + )); + StaticType::Error + } + } + callee @ StaticType::Var(_) => self.call_unknown_function(&callee, arguments), + // The callee's check failed, which was reported there. + StaticType::Error => { + for argument in &arguments { + self.vars.settle_unknowns_as_errors(argument); + } + StaticType::Error + } + // Nothing tells what the callee does with its arguments. + callee if callee.could_be_callable() => { + for argument in &arguments { + self.vars.settle_unknowns(argument, &StaticType::Any); + } + match callee { + StaticType::Function { return_type, .. } => *return_type, + _ => StaticType::Any, + } + } other => { - self.emit(AnalysisError::not_callable(&other, span)); - StaticType::Any + self.emit(AnalysisError::not_callable(&self.vars.shown(&other), span)); + StaticType::Error } }; }; // A named callee declares nothing, so its overload can be picked after // the arguments' types are known. - let mut type_sig = Vec::with_capacity(arguments.len()); - for arg in arguments.iter() { - type_sig.push(self.analyse_expr(arg)); + let tuple_index = match arguments { + [_, index] if name == "[]" => literal_index(index), + _ => None, + }; + let arguments = self.analyse_arguments(function.id, arguments); + self.constrain_index_call( + CallSite::Call(function.id), + arguments, + CallNodes { + arguments: nodes, + result: Some(id), + }, + kind, + name, + span, + tuple_index, + ) + } + + /// Analyses the arguments of a call of the function named at `callee` + /// and returns their types. An anonymous function among them is + /// analysed after the others, with the parameter types the overloads + /// that take those agree on, so its body is checked against what it + /// will be passed: in `players.map(fn(p) => p.size)`, `p` is a + /// `Player`, which has no `size`. + fn analyse_arguments( + &mut self, + callee: NodeId, + arguments: &[ExpressionLocation], + ) -> Vec { + let mut types = arguments + .iter() + .map(|argument| { + (!takes_parameter_types_from_the_call(argument)) + .then(|| self.analyse_expr(argument)) + }) + .collect::>(); + for (position, argument) in arguments.iter().enumerate() { + if types[position].is_some() { + continue; + } + if let Some(expected) = self.parameters_the_overloads_agree_on(callee, &types, position) + && expected.len() == parameter_count(argument) + { + self.expected_parameters.insert(argument.id, expected); + } + types[position] = Some(self.analyse_expr(argument)); } + types.into_iter().flatten().collect() + } - let ResolvedCall { - binding, - return_type, - } = self.overloads().resolve_call( - self.resolver.reference_candidates(function.id), - &type_sig, - kind, - ); + /// The types of the parameters of the function passed at `position` + /// that every overload at `callee` that takes the `known` arguments + /// agrees on, each `None` where they don't, or where it isn't known + /// yet. `None` when no overload takes them, or one takes a function of + /// any type there. + fn parameters_the_overloads_agree_on( + &mut self, + callee: NodeId, + known: &[Option], + position: usize, + ) -> Option>> { + let candidates = self.resolver.reference_candidates(callee).clone(); + let mut agreed: Option>> = None; + for symbol in self.overloads().reachable_symbols(&candidates) { + let snapshot = self.vars.snapshot(); + let expected = self.parameters_one_overload_expects(symbol, known, position); + self.vars.rollback(snapshot); + let expected = match expected { + Expects::Parameters(expected) => expected, + Expects::Nothing => continue, + Expects::Anything => return None, + }; + agreed = Some(match agreed { + None => expected, + Some(agreed) => agreed + .into_iter() + .zip(expected) + .map(|(agreed, expected)| { + agreed.filter(|agreed| Some(agreed) == expected.as_ref()) + }) + .collect(), + }); + } + agreed + } - if matches!(binding, Binding::None) { - let candidates = self.resolver.reference_candidates(function.id); - let error = self.no_match(candidates, name, &type_sig, span); - self.emit(error); - self.picks.bindings.insert(function.id, binding); - return StaticType::Any; + /// The parameter types `symbol` expects of the function passed at + /// `position` if it takes the `known` arguments. Settles variables, so + /// the caller rolls them back. + fn parameters_one_overload_expects( + &mut self, + symbol: SymbolId, + known: &[Option], + position: usize, + ) -> Expects { + let function = self.vars.resolve(self.types.get_type(symbol)); + let first = self.vars.count(); + let Some((parameters, _)) = self.vars.instantiate_signature(&function) else { + return Expects::Anything; + }; + let takes_known = parameters.len() == known.len() + && parameters.iter().zip(known).all(|(parameter, argument)| { + argument + .as_ref() + .is_none_or(|argument| self.vars.bind_argument(parameter, argument)) + }); + if !takes_known { + return Expects::Nothing; + } + match self.vars.resolve(¶meters[position]) { + StaticType::Function { + parameters: Some(expected), + .. + } => Expects::Parameters( + expected + .iter() + .map(|typ| { + Some(self.vars.resolve(typ)).filter(|typ| is_known_before(typ, first)) + }) + .collect(), + ), + _ => Expects::Anything, } + } +} + +/// What an overload expects of a function passed to it. +enum Expects { + /// It doesn't take the call's other arguments. + Nothing, + /// A function whose parameters have these types, where they're known. + Parameters(Vec>), + /// It takes a function of any type, or anything at all. + Anything, +} + +/// Whether `argument` is an anonymous function with a parameter whose type +/// isn't written, which the call it's passed to can give: `fn(p) => +/// p.score`, but not `fn(p: Player) => p.score`. +fn takes_parameter_types_from_the_call(argument: &ExpressionLocation) -> bool { + matches!( + &argument.expression, + Expression::FunctionDeclaration { name: None, parameters, .. } + if parameters.iter().any(|parameter| parameter.annotation.is_none()) + ) +} + +/// How many parameters the function declared by `argument` has. +fn parameter_count(argument: &ExpressionLocation) -> usize { + match &argument.expression { + Expression::FunctionDeclaration { parameters, .. } => parameters.len(), + _ => 0, + } +} + +/// Whether `typ` is known as far as the code around the call knows it: +/// nothing in it is only known as the program runs, and its variables are +/// from before `first`, not ones an overload's signature made. A list +/// literal's elements are a variable until its uses settle them. +fn is_known_before(typ: &StaticType, first: usize) -> bool { + !typ.contains(&|part| match part { + StaticType::Var(var) => var.0 as usize >= first, + StaticType::Any | StaticType::Generic(_) => true, + _ => false, + }) +} - self.picks.bindings.insert(function.id, binding); - return_type +/// The value of an index that's an integer written in the code, as in +/// `t[1]` or `t[-1]`. +fn literal_index(index: &ExpressionLocation) -> Option { + match &index.expression { + Expression::NumericLiteral(NumericLiteral::Int64(value)) => Some(*value), + Expression::Call { + function, + arguments, + } + | Expression::OperatorCall { + function, + arguments, + } => match (&function.expression, &arguments[..]) { + (Expression::Identifier { name, .. }, [operand]) if name == "-" => { + literal_index(operand).and_then(i64::checked_neg) + } + _ => None, + }, + _ => None, } } diff --git a/ndc_analyser/src/analyser/declarations.rs b/ndc_analyser/src/analyser/declarations.rs index 07104843..a5fe79ce 100644 --- a/ndc_analyser/src/analyser/declarations.rs +++ b/ndc_analyser/src/analyser/declarations.rs @@ -1,9 +1,8 @@ -use super::Analyser; +use super::{Analyser, Returns}; use crate::error::AnalysisError; -use crate::types::TypeBinding; -use itertools::{Itertools, izip}; +use itertools::izip; use ndc_core::r#struct::StructInfo; -use ndc_core::{NodeId, StaticType, TypeSignature}; +use ndc_core::{NodeId, StaticType}; use ndc_lexer::Span; use ndc_parser::{ BindingPattern, BindingPatternLocation, ExpressionLocation, FunctionParameter, StructField, @@ -12,67 +11,116 @@ use ndc_parser::{ use ndc_resolver::SymbolId; use std::rc::Rc; +/// What a function declaration consists of, named like `fn f(x) { … }` or +/// anonymous like `fn(x) => …`. +#[derive(Clone, Copy)] +pub(super) struct FunctionDeclaration<'a> { + pub id: NodeId, + pub name: Option<&'a str>, + pub parameters: &'a [FunctionParameter], + pub body: &'a ExpressionLocation, + pub return_annotation: Option<&'a TypeExpr>, + /// Whether it's a `pure fn`, whose results are cached. + pub pure: bool, + pub span: Span, +} + impl Analyser { - /// Analyses a function declaration and returns its type. + /// Analyses a function declaration and returns its type. Its + /// unannotated parameters are type variables that what the body does + /// with them constrains. A lambda's, and a nested `fn`'s, are settled by + /// how the code around it uses it: in `xs.map(fn(x) => x + 1)`, `x` gets + /// the type of `xs`'s elements. A `fn` in the program scope is generic: + /// each call settles them anew (see [`Self::generalize`]). pub(super) fn analyse_function_declaration( &mut self, - id: NodeId, - name: Option<&str>, - parameters: &[FunctionParameter], - body: &ExpressionLocation, - return_annotation: Option<&TypeExpr>, - span: Span, + function: FunctionDeclaration<'_>, ) -> StaticType { - let type_signature = TypeSignature::from_annotated_bindings( - parameters - .iter() - .map(|param| { - let annotation = self.lower_annotation(param.annotation.as_ref()); - (param.name().to_string(), annotation) - }) - .collect(), - ); + let FunctionDeclaration { + id, + name, + parameters, + body, + return_annotation, + pure, + span, + } = function; + let declared = name.map(|name| (name, self.resolver.declared_symbol(id))); + let generic = declared.is_some_and(|(_, symbol)| self.is_generic_function(symbol)); + if generic { + self.vars.enter_level(); + self.declaring = declared.map(|(_, symbol)| symbol); + } + let first_use = self.uses.len(); + let first_check = self.checks.len(); + let annotations = parameters + .iter() + .map(|param| self.lower_annotation(param.annotation.as_ref())) + .collect::>(); + let expected = self.expected_parameters.remove(&id).unwrap_or_default(); + let param_types = annotations + .iter() + .enumerate() + .map(|(position, annotation)| { + annotation + .clone() + .or_else(|| expected.get(position).cloned().flatten()) + .unwrap_or_else(|| self.vars.fresh(StaticType::Any)) + }) + .collect::>(); let annotated_return = self.lower_annotation(return_annotation); + // Until the body says more, a named function's value takes what its + // annotations say, as its body is compiled once for every type it's + // called with. + let runtime_parameters = match declared { + Some(_) => Some( + annotations + .iter() + .map(|annotation| annotation.clone().unwrap_or(StaticType::Any)) + .collect(), + ), + None => Some(param_types.clone()), + }; self.result.analysis.function_types.insert( id, StaticType::Function { - parameters: type_signature.types(), + parameters: runtime_parameters, return_type: Box::new(annotated_return.clone().unwrap_or_default()), }, ); - // Pre-register the function before analyzing its body so recursive calls can - // resolve the name. The return type is unknown at this point so we use Any. - // A redefinition is reported but still bound, since the resolver declared it. - let declared = if let Some(name) = name { - let symbol = self.resolver.declared_symbol(id); - let parameters = type_signature.types(); - if self.redefines_function(symbol, parameters.as_deref()) { - self.emit(AnalysisError::function_redefinition( - name, - parameters.as_deref(), - span, - )); - } + // The function is declared before its body is analysed, so a + // recursive call can call it. Its return type is what the body + // returns, which the recursive calls share. A redefinition is + // reported but still bound, since the resolver declared it. + let recursive_return = match (&declared, &annotated_return) { + (Some(_), None) => Some(self.vars.fresh(StaticType::Any)), + _ => None, + }; + if let Some((name, symbol)) = declared { + self.check_overlap(symbol, name, Some(¶m_types), span); let placeholder = StaticType::Function { - parameters: type_signature.types(), - return_type: Box::new(annotated_return.clone().unwrap_or_default()), + parameters: Some(param_types.clone()), + return_type: Box::new( + annotated_return + .clone() + .or_else(|| recursive_return.clone()) + .unwrap_or_default(), + ), }; - self.types - .declare(symbol, TypeBinding::Inferred(placeholder)); - Some(symbol) - } else { - None - }; + self.types.declare(symbol, placeholder); + } - self.return_type_stack.push(None); + self.return_type_stack.push(Returns { + annotated: annotated_return.clone(), + joined: None, + }); // A function body starts outside of any loop: `break` can't // leave the function to end a loop around its declaration. - let outer_loop_depth = std::mem::take(&mut self.loop_depth); - let parameter_nodes = parameters.iter().map(|p| p.lvalue.id).collect_vec(); - let param_types = self.declare_parameters(&type_signature, ¶meter_nodes); - + let outer_loops = std::mem::take(&mut self.loops); for (param, typ) in parameters.iter().zip(¶m_types) { + let symbol = self.resolver.declared_symbol(param.lvalue.id); + self.types.declare(symbol, typ.clone()); self.result .analysis .variable_types @@ -80,44 +128,94 @@ impl Analyser { } let implicit_return = self.analyse_expr(body); - let explicit_return = self.return_type_stack.pop().unwrap(); - self.loop_depth = outer_loop_depth; + let mut returns = self.return_type_stack.pop().unwrap(); + self.loops = outer_loops; - // Combine explicit `return` types with the block's implicit return type. - let inferred_return = match explicit_return { - Some(ret) => ret.lub(&implicit_return), - None => implicit_return, - }; + // The body's value is returned like the values of `return`. An + // annotated return type is what each of them returns, which can + // settle what the body left waiting: in `fn f() -> Fn(String) -> + // List { split }`, it picks `split(String)`. + self.add_return(&mut returns, body, implicit_return); + let inferred_return = returns.joined.unwrap_or(StaticType::Never); - // If there is an annotated return type, validate it; - // otherwise record the inferred type in the analysis. - if let Some(annotated) = &annotated_return { - if !inferred_return.is_subtype(annotated) { + if let Some(returns) = &recursive_return + && !self.vars.try_unify(returns, &inferred_return) + { + // A function that returns itself, like `fn f() { f }`, would + // have a type that contains itself, so it returns `Any`. + if matches!(self.vars.resolve(returns), StaticType::Var(_)) { + self.vars.unify(returns, &StaticType::Any); + } else { self.emit(AnalysisError::mismatched_types( - &inferred_return, - annotated, + &self.vars.shown(&inferred_return), + &self.vars.shown(returns), span, )); } - } else { + } + + let signature = StaticType::Function { + parameters: Some(param_types.clone()), + return_type: Box::new( + annotated_return + .clone() + .unwrap_or_else(|| inferred_return.clone()), + ), + }; + let function_type = match declared { + Some((_, symbol)) if generic => { + self.vars.leave_level(); + self.declaring = None; + self.declared_in_batch.insert(symbol); + let typ = self.generalize(symbol, &signature, id, pure, [first_use, first_check]); + self.within_size_limit(symbol, name, typ, span) + } + _ => signature, + }; + let inferred_return = self.vars.resolve(&inferred_return); + + if annotated_return.is_none() { self.result .analysis .inferred_return_types .insert(id, inferred_return.clone()); + // A lambda's value says what it returns, which tells whether it's + // a function type an `Any` is used as without checking its calls. + if declared.is_none() + && let Some(StaticType::Function { return_type, .. }) = + self.result.analysis.function_types.get_mut(&id) + { + **return_type = inferred_return.clone(); + } } - let effective_return = annotated_return.unwrap_or(inferred_return); - - let function_type = StaticType::Function { - parameters: Some(param_types.clone()), - return_type: Box::new(effective_return), - }; + // A named function's value is checked as the program runs against + // what it takes, as far as that can be checked: its body may have + // made an unannotated parameter an Int. + if declared.is_some() + && let StaticType::Function { + parameters: Some(parameters), + .. + } = self.vars.resolve(&function_type) + && let Some(StaticType::Function { + parameters: Some(runtime), + .. + }) = self.result.analysis.function_types.get_mut(&id) + { + for ((runtime, parameter), annotation) in + runtime.iter_mut().zip(parameters).zip(&annotations) + { + if annotation.is_none() { + *runtime = checkable_as_the_program_runs(¶meter); + } + } + } // A named declaration stores the function under its name and, like // `let`, evaluates to unit. An anonymous one evaluates to the function. match declared { - Some(symbol) => { - let _ = self.types.update_binding_type(symbol, function_type); + Some((_, symbol)) => { + self.types.declare(symbol, function_type); self.picks.variables.insert(id, symbol); StaticType::unit() } @@ -125,6 +223,43 @@ impl Analyser { } } + /// `typ`, the type of the generic function `symbol`, unless it has more + /// parts than the analyser can work with, as when each function in a + /// chain doubles what the previous one returns: `fn f1(x) => f0(f0(x))` + /// with `fn f0(x) => (x, x)`. Then it's reported, and what the function + /// takes and returns are failed checks, so what calls it isn't built + /// from its type, and isn't reported again. + fn within_size_limit( + &mut self, + symbol: SymbolId, + name: Option<&str>, + typ: StaticType, + span: Span, + ) -> StaticType { + const LIMIT: usize = 10_000; + let parts = std::cell::Cell::new(0_usize); + let too_large = typ.contains(&|_| { + parts.set(parts.get() + 1); + parts.get() > LIMIT + }); + if !too_large { + return typ; + } + self.emit(AnalysisError::type_too_large( + name.unwrap_or("?"), + LIMIT, + span, + )); + self.schemes.remove(&symbol); + let StaticType::Function { parameters, .. } = typ else { + return StaticType::Error; + }; + StaticType::Function { + parameters: parameters.map(|parameters| vec![StaticType::Error; parameters.len()]), + return_type: Box::new(StaticType::Error), + } + } + /// Declares a struct's type, constructor and field accessors. pub(super) fn analyse_struct_declaration( &mut self, @@ -134,7 +269,7 @@ impl Analyser { ) -> StaticType { let field_types: Vec = fields .iter() - .map(|f| self.lower_type_expr(&f.annotation)) + .map(|f| self.lower_value_type(&f.annotation)) .collect(); let struct_id = self.struct_registry.register( @@ -154,69 +289,18 @@ impl Analyser { let symbols = self.resolver.struct_symbols(id).clone(); self.struct_types .insert(symbols.type_symbol, Rc::clone(&info)); - self.types.declare( - symbols.constructor, - TypeBinding::Annotated(info.constructor_type()), - ); + self.types + .declare(symbols.constructor, info.constructor_type()); self.picks.variables.insert(id, symbols.constructor); for (index, &(getter, setter)) in symbols.accessors.iter().enumerate() { - self.types - .declare(getter, TypeBinding::Annotated(info.getter_type(index))); - self.types - .declare(setter, TypeBinding::Annotated(info.setter_type(index))); + self.types.declare(getter, info.getter_type(index)); + self.types.declare(setter, info.setter_type(index)); } self.picks.structs.insert(id, StructInfo::clone(&info)); StaticType::unit() } - /// Whether a function declared earlier in the same scope as `symbol` has - /// its name and the same parameter types, which makes `symbol` an illegal - /// redefinition. `fn f(x: Int)` and `fn f(x: String)` are two overloads, - /// but `fn f(x)` and `fn f(y)` both take `Any`. `None` is variadic. - fn redefines_function(&self, symbol: SymbolId, parameters: Option<&[StaticType]>) -> bool { - let name = self.resolver.symbol_name(symbol); - let scope = self.resolver.scope(self.resolver.scope_of(symbol)); - scope.bindings()[..self.resolver.binding_position(symbol)] - .iter() - .filter(|&&earlier| self.resolver.symbol_name(earlier) == name) - .any(|&earlier| match self.types.get_type(earlier) { - StaticType::Function { - parameters: earlier, - .. - } => earlier.as_deref() == parameters, - _ => false, - }) - } - - /// Declares a function's parameters and returns their types. - fn declare_parameters( - &mut self, - type_signature: &TypeSignature, - parameter_nodes: &[NodeId], - ) -> Vec { - let TypeSignature::Exact(parameters) = type_signature else { - return vec![]; - }; - - for (param, &node) in parameters.iter().zip(parameter_nodes) { - let has_annotation = param.type_name != StaticType::Any; - let binding = if has_annotation { - TypeBinding::Annotated(param.type_name.clone()) - } else { - TypeBinding::Inferred(StaticType::Any) - }; - - let symbol = self.resolver.declared_symbol(node); - self.types.declare(symbol, binding); - } - - parameters - .iter() - .map(|param| param.type_name.clone()) - .collect() - } - /// Declares each name in a `let` or `for` pattern, checking /// `found_type` against the annotation and the pattern's shape. pub(super) fn declare_binding_pattern( @@ -228,33 +312,32 @@ impl Analyser { ) { match &lvalue.pattern { BindingPattern::Identifier { span, .. } => { - // If there is a type annotation and the given type is not a subtype of the annotated type we emit an error - if let Some(expected_type) = &expected_type - && !found_type.is_subtype(expected_type) - { - self.emit(AnalysisError::mismatched_types( - &found_type, - expected_type, - *span, - )); + // The value is checked like an assignment to the annotated + // variable, so `let o: Option = None` settles `None`. + if let Some(expected_type) = &expected_type { + self.check_write(expected_type, &found_type, *span, Some(lvalue.id)); } - let type_binding = match expected_type { - Some(annotated) => TypeBinding::Annotated(annotated), - None => TypeBinding::Inferred(found_type), - }; - + // An annotation decides the type, or else the value does. + let typ = expected_type.unwrap_or(found_type); let symbol = self.resolver.declared_symbol(lvalue.id); - self.types.declare(symbol, type_binding.clone()); + self.types.declare(symbol, typ.clone()); self.picks.variables.insert(lvalue.id, symbol); - self.result - .analysis - .variable_types - .insert(lvalue.id, type_binding.typ().clone()); + self.result.analysis.variable_types.insert(lvalue.id, typ); } BindingPattern::Sequence(seq) => { + // What a value whose type isn't known yet unpacks into is + // settled once its type is. + let found_type = self.vars.resolve(&found_type); + if expected_type.is_none() && matches!(found_type, StaticType::Var(_)) { + let parts = self.constrain_unpacking(found_type, seq.len(), span); + for (sub_lvalue, part) in seq.iter().zip(parts) { + self.declare_binding_pattern(sub_lvalue, None, part, span); + } + return; + } // If the type is a fixed-length Tuple whose arity doesn't match - // the number of bindings, fall back to Any for each element. This + // the number of bindings, bind each name as a failed check. This // can happen when a variable is declared with one type (e.g. ()) // and later reassigned to a tuple of a different arity — the // analyser doesn't track reassignment types. @@ -269,7 +352,7 @@ impl Analyser { elems.len(), span, )); - self.bind_patterns_as_any(seq, span); + self.bind_patterns_as_errors(seq, span); return; } else { Box::new(elems.iter()) @@ -278,7 +361,7 @@ impl Analyser { iter } else { self.emit(AnalysisError::unable_to_unpack_type(&resolved_type, span)); - self.bind_patterns_as_any(seq, span); + self.bind_patterns_as_errors(seq, span); return; }; @@ -308,18 +391,46 @@ impl Analyser { if desired_length != actual_len { self.emit(AnalysisError::unable_to_unpack_type(&found_type, span)); - self.bind_patterns_as_any(&seq[actual_len..], span); + self.bind_patterns_as_errors(&seq[actual_len..], span); } } } } - /// Binds every name in `patterns` as `Any`, so a pattern that failed to - /// type-check still declares its names. In `let (a, b, c) = (1, 2)`, `c` + /// Binds every name in `patterns` as a failed check, so a pattern that + /// failed to type-check still declares its names. In `let (a, b, c) = (1, 2)`, `c` /// is bound and a later `print(c)` reports nothing further. - fn bind_patterns_as_any(&mut self, patterns: &[BindingPatternLocation], span: Span) { + fn bind_patterns_as_errors(&mut self, patterns: &[BindingPatternLocation], span: Span) { for pattern in patterns { - self.declare_binding_pattern(pattern, None, StaticType::Any, span); + self.declare_binding_pattern(pattern, None, StaticType::Error, span); } } } + +/// What the program can check about a value of type `typ` as it runs: the +/// kind of value, but not what a container holds, and nothing about a type +/// parameter, which takes anything. `List` is checked as a list. +fn checkable_as_the_program_runs(typ: &StaticType) -> StaticType { + let any = || Box::new(StaticType::Any); + match typ { + StaticType::Var(_) | StaticType::Generic(_) | StaticType::Tuple(_) => StaticType::Any, + StaticType::List(_) => StaticType::List(any()), + StaticType::Deque(_) => StaticType::Deque(any()), + StaticType::Iterable(_) => StaticType::Iterable(any()), + StaticType::Iterator(_) => StaticType::Iterator(any()), + StaticType::MinHeap(_) => StaticType::MinHeap(any()), + StaticType::MaxHeap(_) => StaticType::MaxHeap(any()), + StaticType::Option(_) => StaticType::Option(any()), + StaticType::Map { .. } => StaticType::Map { + key: any(), + value: any(), + }, + StaticType::Function { parameters, .. } => StaticType::Function { + parameters: parameters + .as_ref() + .map(|parameters| vec![StaticType::Any; parameters.len()]), + return_type: any(), + }, + other => other.clone(), + } +} diff --git a/ndc_analyser/src/analyser/function_values.rs b/ndc_analyser/src/analyser/function_values.rs new file mode 100644 index 00000000..e05c6f8e --- /dev/null +++ b/ndc_analyser/src/analyser/function_values.rs @@ -0,0 +1,387 @@ +//! A function's name used as a value, as `split` in `xs.map(split)`. When +//! the name has several overloads, the type expected where it's used picks +//! one: `map` passes one `String`, so `split(String)` is meant, not +//! `split(String, String)`. + +use super::Analyser; +use super::solver::{CallSite, Constraint, EarlyOverload}; +use crate::error::AnalysisError; +use crate::overloads::CallKind; +use ndc_analysis::{Binding, Candidate}; +use ndc_core::{NodeId, StaticType}; +use ndc_lexer::Span; +use ndc_resolver::{BindingKind, Candidates, SymbolId}; + +/// An overloaded function's name used as a value, waiting for the type +/// expected of it to say which overload is meant. +#[derive(Debug, Clone)] +pub(super) struct FunctionValue { + id: NodeId, + name: String, + /// What the name can mean, as for a call of it. + candidates: Candidates, + /// The functions among them, grouped by scope, nearest first. + groups: Vec>, + /// The type expected of the value, which its uses settle. + expected: StaticType, + /// Whether it's a value of a literal, as `abs` is in `[len, abs]`, whose + /// other values say what's expected of it, but not which overload it is. + in_literal: bool, + span: Span, +} + +impl FunctionValue { + pub(super) fn expected(&self) -> &StaticType { + &self.expected + } + + /// This value with `expected` as the type expected of it, as a copy of + /// a generic function's constraints gets the types of its call. + pub(super) fn with_expected(&self, expected: StaticType) -> Self { + Self { + expected, + ..self.clone() + } + } + + pub(super) fn id(&self) -> NodeId { + self.id + } + + pub(super) fn mark_in_literal(&mut self) { + self.in_literal = true; + } + + /// The functions the name can mean, grouped by scope, nearest first. + pub(super) fn groups(&self) -> &[Vec] { + &self.groups + } +} + +/// Which overload a function value means, given what's known of the type +/// expected of it. +#[derive(Debug, PartialEq)] +pub(super) enum Choice { + /// This overload. + One(SymbolId), + /// Several fit what's known so far. + Undecided, + /// None fits. + None, +} + +impl Analyser { + /// Analyses an identifier read as a value and returns its type. A name + /// with several overloads waits until the type expected of it picks one. + pub(super) fn analyse_value_reference( + &mut self, + id: NodeId, + name: &str, + span: Span, + ) -> StaticType { + let symbol = self.resolver.innermost_reference(id); + let groups = self.overloaded_function_groups(id, symbol); + if groups.iter().map(Vec::len).sum::() > 1 { + let expected = self.vars.fresh(StaticType::Any); + let value = FunctionValue { + id, + name: name.to_string(), + candidates: self.resolver.reference_candidates(id).clone(), + groups, + expected: expected.clone(), + in_literal: false, + span, + }; + if !self.try_function_value(&value) { + self.waiting_function_values.insert(id); + self.wait(Constraint::FunctionValue(value)); + } + return self.vars.resolve(&expected); + } + self.picks + .bindings + .insert(id, Binding::Resolved(Candidate::Scalar(symbol))); + // A generic function used as a value, as in `let p = push`, has one + // type there, which its uses settle. + let typ = self.instantiate_function(symbol); + self.flush_copies(None, span); + typ + } + + /// The functions the name at `id` can mean, grouped by scope, nearest + /// first, if the nearest thing it can mean, `innermost`, is a function. + fn overloaded_function_groups(&self, id: NodeId, innermost: SymbolId) -> Vec> { + let symbols = self.resolver.symbols(); + if symbols.kind(innermost) != BindingKind::Function { + return Vec::new(); + } + self.overloads() + .reachable_groups(self.resolver.reference_candidates(id)) + .into_iter() + .map(|group| { + group + .into_iter() + .filter(|&symbol| symbols.kind(symbol) == BindingKind::Function) + .collect::>() + }) + .filter(|group| !group.is_empty()) + .collect() + } + + /// `symbol`'s type with fresh variables for its type parameters. + fn instantiated_type(&mut self, symbol: SymbolId) -> StaticType { + let typ = self.vars.resolve(self.types.get_type(symbol)); + self.vars.instantiate(&typ) + } + + /// Which overload `value` means where a value of type `expected` is + /// expected. Once that's a function type, the value is called with + /// arguments of its parameter types, so it means the overload a call + /// with those arguments means, decided by the same rules: while they + /// aren't fully known, only an overload that's the only one able to take + /// them, and not through an `Any` parameter, can be committed to; once + /// they are, the nearest scope, the most specific overload and `Any` + /// losing to an exact type pick it. The return type doesn't pick, as it + /// doesn't for a call: the overload must return what's expected, or none + /// fits. Before the expected type is a function type, an overload is + /// only chosen when it's the only one there is to choose. + pub(super) fn choose_function_value( + &mut self, + value: &FunctionValue, + expected: &StaticType, + ) -> Choice { + if let StaticType::Function { + parameters: Some(parameters), + .. + } = expected + { + let picked = if parameters.iter().any(StaticType::has_vars) { + match self.early_overload(&value.candidates, parameters, CallKind::Regular) { + EarlyOverload::Only(only) => Some(only), + EarlyOverload::NoneFits => return Choice::None, + EarlyOverload::Undecided => return Choice::Undecided, + } + } else { + match self + .overloads() + .resolve_call(&value.candidates, parameters, CallKind::Regular) + .binding + { + Binding::Resolved(Candidate::Scalar(symbol)) => Some(symbol), + // The program would pick one as it runs, but a function + // value is one function. When the expected type's `Any` + // could be what's not known yet, as in a generic + // function's own body, more than one might be meant. + Binding::Dynamic(candidates) => { + let might = candidates.into_iter().any(|candidate| match candidate { + Candidate::Scalar(symbol) => self.might_be_expected(symbol, expected), + Candidate::Vec(_) => false, + }); + return if might { + Choice::Undecided + } else { + Choice::None + }; + } + _ => None, + } + }; + return match picked { + Some(symbol) if self.fits_expected(symbol, expected) => Choice::One(symbol), + _ => Choice::None, + }; + } + let fitting = self.fitting_groups(value, expected); + let nearest = if expected.has_vars() { + match &fitting[..] { + [only] => only.clone(), + [] => return Choice::None, + _ => return Choice::Undecided, + } + } else { + match fitting.first() { + Some(nearest) => nearest.clone(), + None => return Choice::None, + } + }; + match nearest[..] { + [only] => Choice::One(only), + // Overloads with the same parameters are a redefinition, which + // is reported where it's declared. The newest one counts. + [newest, ..] if self.all_take_the_same(&nearest) => Choice::One(newest), + _ => Choice::Undecided, + } + } + + /// The overloads of `value` that a value of type `expected` can be, + /// grouped by scope, nearest first, leaving out scopes with none. + fn fitting_groups( + &mut self, + value: &FunctionValue, + expected: &StaticType, + ) -> Vec> { + value + .groups + .iter() + .map(|group| { + group + .iter() + .copied() + .filter(|&symbol| self.fits_expected(symbol, expected)) + .collect::>() + }) + .filter(|group| !group.is_empty()) + .collect() + } + + /// Whether the functions `symbols` all take the same parameter types. + fn all_take_the_same(&self, symbols: &[SymbolId]) -> bool { + let parameters = |symbol: SymbolId| match self.types.get_type(symbol) { + StaticType::Function { parameters, .. } => parameters.clone(), + _ => None, + }; + symbols + .iter() + .all(|&symbol| parameters(symbol) == parameters(symbols[0])) + } + + /// Whether a value of `symbol`'s type might be passed where `expected` + /// is, once what's not known about either is: an `Fn(Int) -> Int` might + /// be the `Fn(Any) -> Any` a generic function's body expects, but an + /// `Fn(Int, Int) -> Int` isn't. + fn might_be_expected(&self, symbol: SymbolId, expected: &StaticType) -> bool { + self.vars + .resolve(self.types.get_type(symbol)) + .may_fit(&self.vars.resolve(expected)) + } + + /// Whether a value of `symbol`'s type can be passed where `expected` is. + fn fits_expected(&mut self, symbol: SymbolId, expected: &StaticType) -> bool { + let snapshot = self.vars.snapshot(); + let typ = self.instantiated_type(symbol); + let fits = self.vars.bind_argument(expected, &typ); + self.vars.rollback(snapshot); + fits + } + + /// Every overload `value` can mean, for the program to pick from as it + /// calls it: nearest scope first, and in each the most specific first, + /// as for a call the program picks for, since it takes the first that + /// accepts the arguments. + pub(super) fn every_overload(&self, value: &FunctionValue) -> Binding { + Binding::Dynamic( + value + .groups() + .iter() + .flat_map(|group| self.overloads().most_specific_first(group.clone())) + .map(Candidate::Scalar) + .collect(), + ) + } + + /// Picks the overload `value` means if what's known of the type expected + /// of it decides it, and returns whether it did. + pub(super) fn try_function_value(&mut self, value: &FunctionValue) -> bool { + let expected = self.vars.resolve(&value.expected); + // What's expected of it comes from a check that failed, as in + // `missing().map(abs)`, which was reported there. + if expected.has_errors() { + self.give_up_on_function_value(value); + return true; + } + match self.choose_function_value(value, &expected) { + Choice::One(symbol) => { + self.commit_function_value(value, symbol, expected.has_vars()); + true + } + Choice::Undecided if expected.has_vars() => false, + Choice::Undecided => { + let example = self.overload_example(value, &expected); + self.emit(AnalysisError::ambiguous_function_value( + &value.name, + &self.vars.shown(&expected), + &example, + value.span, + )); + self.give_up_on_function_value(value); + true + } + // The other values of a literal can't say which overload is + // meant, as the literal could hold `Any`s: `[len, abs]`. + Choice::None if value.in_literal => { + let example = self.overload_example(value, &StaticType::Any); + self.emit(AnalysisError::ambiguous_function_value( + &value.name, + &StaticType::Any, + &example, + value.span, + )); + self.give_up_on_function_value(value); + true + } + Choice::None => { + self.emit(AnalysisError::no_overload_fits( + &value.name, + &self.vars.shown(&expected), + value.span, + )); + self.give_up_on_function_value(value); + true + } + } + } + + /// The type of an overload that fits `expected`, nearest first, as an + /// example of an annotation that picks one. A type parameter can't be + /// written in an annotation, so it's shown as `Any`. + fn overload_example(&mut self, value: &FunctionValue, expected: &StaticType) -> StaticType { + let symbol = self + .fitting_groups(value, expected) + .first() + .and_then(|group| group.first().copied()) + .or_else(|| { + value + .groups + .iter() + .flatten() + .copied() + .find(|&symbol| self.might_be_expected(symbol, expected)) + }) + .unwrap_or_else(|| self.resolver.innermost_reference(value.id)); + self.vars + .shown(self.types.get_type(symbol)) + .replace(&|part| matches!(part, StaticType::Generic(_)).then_some(StaticType::Any)) + } + + fn commit_function_value(&mut self, value: &FunctionValue, symbol: SymbolId, early: bool) { + let typ = self.instantiate_function(symbol); + self.vars.bind_argument(&value.expected, &typ); + self.flush_copies(None, value.span); + if early && cfg!(debug_assertions) { + self.remember_early_function_value(value, symbol); + } + self.record_function_value(value, symbol); + } + + /// Records that `value` means `symbol`, in the instance whose + /// constraints are being solved, if any. A copy of a generic function's + /// constraint records nothing, as the value is in that function's body, + /// whose instance for the call records it. + fn record_function_value(&mut self, value: &FunctionValue, symbol: SymbolId) { + if self.copied_through.is_none() { + self.pick_tables(self.recording).insert( + CallSite::Call(value.id), + Binding::Resolved(Candidate::Scalar(symbol)), + ); + } + } + + /// Picks the nearest overload after an error, so analysis carries on + /// without reporting the same value again. + fn give_up_on_function_value(&mut self, value: &FunctionValue) { + let symbol = self.resolver.innermost_reference(value.id); + let typ = self.instantiated_type(symbol); + self.vars.try_unify(&value.expected, &typ); + self.record_function_value(value, symbol); + } +} diff --git a/ndc_analyser/src/analyser/generics.rs b/ndc_analyser/src/analyser/generics.rs new file mode 100644 index 00000000..638ced00 --- /dev/null +++ b/ndc_analyser/src/analyser/generics.rs @@ -0,0 +1,625 @@ +//! Generic functions. A `fn` declared in the program scope takes any +//! arguments its body can work with: its unannotated parameters are type +//! variables, and the calls its body makes on them wait as its constraints. +//! `fn inc(x) => x + 1` is `inc(A) -> B where +(A, Int) -> B`, and a call +//! `inc(2.5)` copies the constraint as `+(Float, Int) -> ?`, which settles +//! the call's type as `Float`. + +use super::Analyser; +use super::instances::Scheme; +use super::solver::{CallConstraint, Constraint, Waiting}; +use crate::error::AnalysisError; +use crate::overloads::is_strictly_more_specific; +use crate::types::TypeVars; +use ahash::AHashSet; +use ndc_analysis::{Binding, Candidate}; +use ndc_core::{NodeId, StaticType, TypeVar}; +use ndc_lexer::Span; +use ndc_resolver::{ScopeKind, SymbolId}; + +impl Analyser { + /// Whether the function declared as `symbol` is generic, which a `fn` + /// declared in the program scope is. One declared in a function or a + /// block has one type, like a lambda. + pub(super) fn is_generic_function(&self, symbol: SymbolId) -> bool { + self.resolver.scope(self.resolver.scope_of(symbol)).kind() == ScopeKind::Program + } + + /// Ends the body of the generic function `symbol`, whose type is + /// `signature`, and returns its type with its type parameters: each + /// variable its body made that nothing outside it refers to becomes + /// one, and the constraints still waiting on them become the function's. + /// The body's literals are typed by the body alone, as at the end of + /// the program: the `1` in `fn inc(x) => x + 1` is an Int, whatever + /// `x` is. A variable the constraints wait on but that the parameters + /// don't decide, like the element type of `[]` in `fn f(x) { + /// [].push(x) }`, can't be decided by a call either, so it's settled + /// here too. + pub(super) fn generalize( + &mut self, + symbol: SymbolId, + signature: &StaticType, + declaration: NodeId, + pure: bool, + [first_use, first_check]: [usize; 2], + ) -> StaticType { + self.solve_waiting(); + self.default_literals(TypeVars::is_generalizable); + self.settle_ambiguous(signature); + self.merge_copies(); + + let scheme = self + .generic_constraints() + .into_iter() + .map(|serial| { + self.waiting + .remove(&serial) + .expect("the constraint is waiting") + }) + .collect::>(); + + // The generic functions the body calls with types that depend on + // its type parameters are the instance's to pick. + let (uses, outside): (Vec<_>, Vec<_>) = self + .uses + .split_off(first_use) + .into_iter() + .partition(|generic_use| { + generic_use.key.iter().any(|typ| { + self.vars + .unsettled(typ) + .into_iter() + .any(|var| self.vars.is_generalizable(var)) + }) + }); + self.uses.extend(outside); + + // Name the type parameters in the order they appear. + let mut parameters = self.vars.unsettled(signature); + for waiting in &scheme { + for typ in waiting + .constraint + .inputs() + .iter() + .chain(waiting.constraint.outputs()) + { + for var in self.vars.unsettled(typ) { + if !parameters.contains(&var) { + parameters.push(var); + } + } + } + } + let mut named = 0; + for var in parameters { + if self.vars.is_generalizable(var) { + self.vars.generalize(var, &type_parameter_name(named)); + named += 1; + } + } + + // A type only the body makes, as of a list nothing else uses, gets + // its fallback. + let uses = uses + .into_iter() + .map(|mut generic_use| { + for typ in &generic_use.key { + let unnamed = self.vars.unsettled(typ); + self.vars.settle_with_fallbacks(&unnamed); + } + generic_use.key = generic_use + .key + .iter() + .map(|typ| self.vars.resolve(typ)) + .collect(); + generic_use + }) + .collect::>(); + self.watched_settlings = self.vars.settled_count(); + + let scheme = scheme + .into_iter() + .map(|waiting| Waiting { + constraint: waiting + .constraint + .map_types(&mut |typ| self.vars.resolve(typ)), + origin: waiting.origin, + }) + .collect::>(); + for waiting in &scheme { + match &waiting.constraint { + Constraint::Call(call) if waiting.origin.is_none() => { + self.pick_for_any_arguments(call); + } + // The body compiled for arguments of type `Any` can't tell + // which overload a function value means, so the program + // picks as it calls it. + Constraint::FunctionValue(value) if waiting.origin.is_none() => { + let binding = self.every_overload(value); + self.picks.bindings.insert(value.id(), binding); + } + _ => {} + } + } + // A value of type `Any` that the body passes where a type parameter + // is expected, as `a` in `fn f(x) { let a: Any = 5; [x].push(a) }`, + // is checked against what each call makes it. + let checks = self.checks[first_check..] + .iter() + .filter(|(instance, ..)| instance.is_none()) + .map(|(_, node, typ, expected)| { + (*node, self.vars.resolve(typ), self.vars.resolve(expected)) + }) + .filter(|(_, typ, expected)| { + *typ == StaticType::Any + && expected.contains(&|part| matches!(part, StaticType::Generic(_))) + }) + .collect::>(); + // An instance is for the types of the type parameters that what it + // compiles differently depends on: the constraints and uses it + // picks for, and the checks. + // A `pure fn` caches its results by its arguments' values, which can + // be equal across types, as `1` and `1.0` are, so each set of types + // gets an instance, and a cache, of its own. + let parameters = match self.vars.resolve(signature) { + StaticType::Function { + parameters: Some(parameters), + .. + } if pure => parameters, + _ => Vec::new(), + }; + let mut key: Vec> = Vec::new(); + for typ in scheme + .iter() + .flat_map(|waiting| waiting.constraint.inputs()) + .chain(uses.iter().flat_map(|generic_use| &generic_use.key)) + .chain(checks.iter().map(|(_, _, expected)| expected)) + .chain(¶meters) + { + for name in typ.type_parameters() { + if !key.contains(&name) { + key.push(name); + } + } + } + self.schemes.insert( + symbol, + Scheme { + constraints: scheme.into(), + key: key.into(), + uses: uses.into(), + declaration, + recursive_calls: std::mem::take(&mut self.recursive_calls).into(), + checks: checks.into(), + }, + ); + self.vars.resolve(signature) + } + + /// The waiting constraints that wait on or settle a variable of the + /// generic function whose body just ended, oldest first. + fn generic_constraints(&self) -> Vec { + self.waiting + .iter() + .filter(|(_, waiting)| { + waiting + .constraint + .inputs() + .iter() + .chain(waiting.constraint.outputs()) + .flat_map(|typ| self.vars.unsettled(typ)) + .any(|var| self.vars.is_generalizable(var)) + }) + .map(|(&serial, _)| serial) + .collect() + } + + /// The generic function's variables that `constraint` waits on, leaving + /// out what it settles itself. + fn generic_inputs(&self, constraint: &Constraint) -> Vec { + let outputs = self.generic_outputs(constraint); + constraint + .inputs() + .iter() + .flat_map(|typ| self.vars.unsettled(typ)) + .filter(|&var| self.vars.is_generalizable(var) && !outputs.contains(&var)) + .collect() + } + + /// The generic function's variables that `constraint` settles once it's + /// solved. A function passed to a call gets the types the call passes + /// it, as `fn(a, x) => a + x` in `xs.fold(0, fn(a, x) => a + x)`, so + /// they count as settled by the call too, and so does an overloaded + /// function's name passed to one, as `signum` in `xs.map(signum)`, + /// whose type is what the call expects of it. + fn generic_outputs(&self, constraint: &Constraint) -> Vec { + let passed_functions = match constraint { + Constraint::Call(call) => { + let values = self + .waiting + .values() + .filter_map(|waiting| match &waiting.constraint { + Constraint::FunctionValue(value) => { + Some(self.vars.resolve(value.expected())) + } + _ => None, + }) + .collect::>(); + call.arguments() + .iter() + .map(|argument| self.vars.resolve(argument)) + .filter(|argument| { + matches!(argument, StaticType::Function { .. }) || values.contains(argument) + }) + .collect() + } + // Picking the overload settles what the value returns. + Constraint::FunctionValue(value) => match self.vars.resolve(value.expected()) { + StaticType::Function { return_type, .. } => vec![*return_type], + _ => Vec::new(), + }, + _ => Vec::new(), + }; + constraint + .outputs() + .iter() + .chain(&passed_functions) + .flat_map(|typ| self.vars.unsettled(typ)) + .filter(|&var| self.vars.is_generalizable(var)) + .collect() + } + + /// The generic function's variables that its constraints wait on but + /// that its parameters don't decide, by constraint, oldest first. What + /// a constraint settles is decided when what it waits on is. An + /// overloaded function's name used as a value is picked like a call: + /// for each instance, from the types its parameters decide. + fn ambiguous_vars(&self, parameters: &[StaticType]) -> Vec<(u64, Vec)> { + let serials = self.generic_constraints(); + let mut decided = parameters + .iter() + .flat_map(|typ| self.vars.unsettled(typ)) + .filter(|&var| self.vars.is_generalizable(var)) + .collect::>(); + loop { + let mut changed = false; + for serial in &serials { + let constraint = &self.waiting[serial].constraint; + if !self + .generic_inputs(constraint) + .iter() + .all(|var| decided.contains(var)) + { + continue; + } + for var in self.generic_outputs(constraint) { + changed |= decided.insert(var); + } + } + if !changed { + break; + } + } + serials + .into_iter() + .filter_map(|serial| { + let constraint = &self.waiting[&serial].constraint; + let ambiguous = self + .generic_inputs(constraint) + .into_iter() + .filter(|var| !decided.contains(var)) + .collect::>(); + (!ambiguous.is_empty()).then_some((serial, ambiguous)) + }) + .collect() + } + + /// Settles the variables of the generic function with type `signature` + /// that its parameters don't decide, like the end of the program + /// settles its own: a call that a default rule decides is decided by it, + /// and what's left gets its fallbacks, oldest constraint first. + fn settle_ambiguous(&mut self, signature: &StaticType) { + let parameters = match signature { + StaticType::Function { + parameters: Some(parameters), + .. + } => parameters.clone(), + _ => Vec::new(), + }; + loop { + let ambiguous = self.ambiguous_vars(¶meters); + let Some((_, vars)) = ambiguous.first().cloned() else { + return; + }; + let decided_by_default = ambiguous.iter().any(|(serial, vars)| { + let only_ambiguous = self.waiting.get(serial).is_some_and(|waiting| { + self.generic_inputs(&waiting.constraint) + .iter() + .all(|var| vars.contains(var)) + }); + (only_ambiguous + && (self.default_to_narrowest(*serial) || self.default_to_operand(*serial))) + || self.default_to_branch(*serial, vars) + }); + if !decided_by_default { + self.vars.settle_with_fallbacks(&vars); + } + self.solve_waiting(); + } + } + + /// Settles a branch of the waiting `if` `serial` whose type is one of + /// `ambiguous` as the other branch's type, and returns whether it did. + /// In `fn fib(n) => if n < 2 { n } else { fib(n - 1) + fib(n - 2) }`, + /// what `+` returns is what `fib` returns, which is what the `if` + /// returns: nothing but the other branch can say what it is. + pub(super) fn default_to_branch(&mut self, serial: u64, ambiguous: &[TypeVar]) -> bool { + let Some(Waiting { + constraint: Constraint::Join { + types: [a, b, _], .. + }, + .. + }) = self.waiting.get(&serial) + else { + return false; + }; + let (a, b) = (self.vars.resolve(a), self.vars.resolve(b)); + for (branch, other) in [(&a, &b), (&b, &a)] { + if let StaticType::Var(var) = branch + && ambiguous.contains(var) + && self.vars.try_unify(branch, other) + { + return true; + } + } + false + } + + /// Merges each copy of a generic function's constraint into an earlier + /// constraint that's the same call with the same arguments, which + /// returns the same type. In `fn g(x) => h(x) + h(x)`, both calls of + /// `h` copy `h`'s constraints, and `g` keeps one of each. + fn merge_copies(&mut self) { + let serials = self.generic_constraints(); + let mut kept: Vec = Vec::new(); + // A call's arguments and result, as one type, so the two calls' + // results become one. + let call_types = |call: &CallConstraint| { + StaticType::Tuple( + call.arguments() + .iter() + .chain([call.result()]) + .cloned() + .collect(), + ) + }; + for serial in serials { + let waiting = &self.waiting[&serial]; + let Constraint::Call(call) = &waiting.constraint else { + continue; + }; + // Only a copy can be the same as an earlier call. + let earlier = waiting.origin.as_ref().and_then(|_| { + kept.iter() + .find_map(|earlier| match &self.waiting[earlier].constraint { + Constraint::Call(earlier) if earlier.is_same_call(call, &self.vars) => { + Some(earlier) + } + _ => None, + }) + }); + if let Some(earlier) = earlier + && self.vars.try_unify(&call_types(earlier), &call_types(call)) + { + self.waiting.remove(&serial); + } else { + kept.push(serial); + } + } + self.solve_waiting(); + } + + /// Records what the body of a generic function calls at `call`, which + /// is compiled once for every type it's called with: the overloads that + /// can take the arguments, picked as the program runs. + fn pick_for_any_arguments(&mut self, call: &CallConstraint) { + let arguments = call + .arguments() + .iter() + .map(|argument| { + argument.replace(&|part| { + matches!(part, StaticType::Generic(_)).then_some(StaticType::Any) + }) + }) + .collect::>(); + let binding = match self + .resolve_overloads(call.candidates(), &arguments, call.kind()) + .binding + { + // An overload may take the values the body gets as the program + // runs although none takes their types with `Any` in them, as + // `sum` takes no `List`. The program picks among them as it + // runs, which looks at a container's elements. + Binding::None => { + let candidates = self + .overloads() + .reachable_symbols(call.candidates()) + .into_iter() + .filter( + |&symbol| match self.vars.resolve(self.types.get_type(symbol)) { + StaticType::Function { + parameters: Some(parameters), + .. + } => parameters.len() == arguments.len(), + StaticType::Function { + parameters: None, .. + } => true, + _ => false, + }, + ) + .map(Candidate::Scalar) + .collect::>(); + if candidates.is_empty() { + Binding::None + } else { + Binding::Dynamic(candidates) + } + } + binding => binding, + }; + self.record_pick(call.site(), binding, &arguments); + } + + /// Reports the function `symbol`, about to be declared with parameters + /// of these types, if an earlier function with its name in its scope + /// takes the same parameters, or if a call could fit both and neither + /// is more specific, so nothing says which one it means. A parameter + /// without an annotation takes any argument the body can work with, so + /// `fn f(x: Int, y)` and `fn f(x, y: Int)` both fit `f(1, 2)`. But + /// `fn show(x)` next to `fn show(x: Int)` is a fallback that a call + /// with an Int doesn't pick, like `fn show(x: Any)`. + pub(super) fn check_overlap( + &mut self, + symbol: SymbolId, + name: &str, + parameters: Option<&[StaticType]>, + span: Span, + ) { + let scope = self.resolver.scope(self.resolver.scope_of(symbol)); + let earlier = scope.bindings()[..self.resolver.binding_position(symbol)] + .iter() + .filter(|&&earlier| self.resolver.symbol_name(earlier) == name) + .filter_map( + |&earlier| match self.vars.resolve(self.types.get_type(earlier)) { + StaticType::Function { parameters, .. } => Some(parameters), + _ => None, + }, + ) + .collect::>(); + let shown = |parameters: Option<&[StaticType]>| { + parameters.map(|parameters| { + parameters + .iter() + .map(|parameter| { + parameter.replace(&|part| { + matches!(part, StaticType::Var(_) | StaticType::Generic(_)) + .then_some(StaticType::Any) + }) + }) + .collect::>() + }) + }; + for earlier in earlier { + if shown(earlier.as_deref()) == shown(parameters) { + self.emit(AnalysisError::function_redefinition( + name, + shown(parameters).as_deref(), + span, + )); + return; + } + if let (Some(earlier), Some(parameters)) = (&earlier, parameters) + && is_a_tie(earlier, parameters) + { + self.emit(AnalysisError::tied_overloads( + name, + &shown_with_blanks(parameters), + &shown_with_blanks(earlier), + span, + )); + return; + } + } + } +} + +/// The name of a generic function's `index`th type parameter: `A`, `B`, …, +/// `Z`, `A1`, …. +fn type_parameter_name(index: usize) -> String { + let letter = char::from(b'A' + u8::try_from(index % 26).expect("less than 26")); + match index / 26 { + 0 => letter.to_string(), + round => format!("{letter}{round}"), + } +} + +/// Parameter types as an error shows them, with `_` for a parameter whose +/// type the body decides. +fn shown_with_blanks(parameters: &[StaticType]) -> Vec { + parameters + .iter() + .map(|parameter| match parameter { + StaticType::Var(_) | StaticType::Generic(_) => "_".to_string(), + other => other.to_string(), + }) + .collect() +} + +/// Whether some call could fit overloads taking `a` and `b`, but neither +/// is more specific than the other, so nothing says which one it means. +fn is_a_tie(a: &[StaticType], b: &[StaticType]) -> bool { + a.len() == b.len() + && a.iter().zip(b).all(|(a, b)| may_take_the_same(a, b)) + && !is_strictly_more_specific(a, b) + && !is_strictly_more_specific(b, a) +} + +/// Whether some argument could be passed both where `a` and where `b` is +/// expected and not lose to either: a type parameter takes anything, and +/// `Any` only another `Any` or a type parameter, as it loses to every +/// other type. +fn may_take_the_same(a: &StaticType, b: &StaticType) -> bool { + let wildcard = |typ: &StaticType| matches!(typ, StaticType::Var(_) | StaticType::Generic(_)); + match (a, b) { + _ if wildcard(a) || wildcard(b) => true, + (StaticType::Option(x), StaticType::Option(y)) + | (StaticType::Iterable(x), StaticType::Iterable(y)) + | (StaticType::List(x), StaticType::List(y)) + | (StaticType::Iterator(x), StaticType::Iterator(y)) + | (StaticType::MinHeap(x), StaticType::MinHeap(y)) + | (StaticType::MaxHeap(x), StaticType::MaxHeap(y)) + | (StaticType::Deque(x), StaticType::Deque(y)) => may_take_the_same(x, y), + (StaticType::Tuple(xs), StaticType::Tuple(ys)) => { + xs.len() == ys.len() && xs.iter().zip(ys).all(|(x, y)| may_take_the_same(x, y)) + } + (StaticType::Map { key: k1, value: v1 }, StaticType::Map { key: k2, value: v2 }) => { + may_take_the_same(k1, k2) && may_take_the_same(v1, v2) + } + // `Any` takes only what another `Any` does. + _ => a == b, + } +} + +/// The pairs of `natives` with one name that a call could fit with +/// neither more specific than the other, which overloads in one scope may +/// not be, each described as `name(A) and name(B)`. +pub fn tied_natives(natives: &[(String, StaticType)]) -> Vec { + let mut tied = Vec::new(); + for (i, (name, a)) in natives.iter().enumerate() { + for (other, b) in &natives[i + 1..] { + if let ( + true, + StaticType::Function { + parameters: Some(a), + .. + }, + StaticType::Function { + parameters: Some(b), + .. + }, + ) = (name == other, a, b) + && is_a_tie(a, b) + { + let shown = |parameters: &[StaticType]| { + parameters + .iter() + .map(ToString::to_string) + .collect::>() + .join(", ") + }; + tied.push(format!("{name}({}) and {name}({})", shown(a), shown(b))); + } + } + } + tied +} diff --git a/ndc_analyser/src/analyser/instances.rs b/ndc_analyser/src/analyser/instances.rs new file mode 100644 index 00000000..2035d790 --- /dev/null +++ b/ndc_analyser/src/analyser/instances.rs @@ -0,0 +1,346 @@ +//! Instances of generic functions. A generic function's body is compiled +//! once for each set of types its calls give the type parameters its +//! constraints depend on, its *key*, and each copy picks its calls' overloads +//! from those types. `fn inc(x) => x + 1` has key `A`: `inc(1)` calls the +//! instance for `Int`, whose `+` is `+(Int, Int)`, and `inc(2.5)` the one +//! for `Float`. `fn wrap(x) => [x]` has an empty key, so its own body serves +//! every call. + +use super::Analyser; +use super::solver::{CallSite, Constraint, Origin, Waiting}; +use crate::error::AnalysisError; +use ahash::AHashMap; +use ndc_analysis::{Binding, Candidate}; +use ndc_core::{NodeId, StaticType}; +use ndc_layout::InstancePicks; +use ndc_lexer::Span; +use ndc_resolver::SymbolId; +use std::rc::Rc; + +/// A generic function's type beyond its signature: what its body still +/// waits on, and which types an instance is for. +#[derive(Debug, Clone)] +pub(super) struct Scheme { + /// The constraints its body left waiting on its type parameters. + pub(super) constraints: Rc<[Waiting]>, + /// The type parameters the constraints and `uses` depend on, which an + /// instance is for. + pub(super) key: Rc<[Box]>, + /// The generic functions its body calls with types that depend on its + /// type parameters. + pub(super) uses: Rc<[GenericUse]>, + /// The function's declaration. + pub(super) declaration: NodeId, + /// The calls its body makes of itself, which have its own types. + pub(super) recursive_calls: Rc<[NodeId]>, + /// The values its body checks as the program runs against a type that + /// has its type parameters in it, with their types and that type, which + /// an instance checks against what its types make it. + pub(super) checks: Rc<[(NodeId, StaticType, StaticType)]>, +} + +/// A call of a generic function, or its name used as a value, which needs +/// the instance for the types it gives the function's key. +#[derive(Debug, Clone)] +pub(super) struct GenericUse { + /// Where the use's pick is recorded. + pub(super) site: CallSite, + pub(super) function: SymbolId, + /// The types it gives the function's key, in the key's order. + pub(super) key: Vec, + /// The instance whose body the use is in, `None` for code that's compiled + /// once, like the program's top level. + pub(super) instance: Option, + /// Where the use is. + pub(super) span: Span, +} + +/// What a call that's about to commit copied from the generic function it +/// commits to, and the checks its arguments need, until it's known whether +/// the call commits. +#[derive(Debug, Default)] +pub(super) struct Pending { + /// Its constraints, with the function they're copied from and the calls + /// they were copied through before. + pub(super) copies: Vec<(SymbolId, Constraint, Option)>, + /// The function and the types the call gives its key. + pub(super) uses: Vec<(SymbolId, Vec)>, + /// Where an argument of type `Any` is checked, and against what. + pub(super) checks: Vec<(NodeId, StaticType)>, + /// The arguments not known yet that were passed for an `Iterable`, + /// with the `T` their elements settle once they're known. + pub(super) iterations: Vec<(StaticType, StaticType)>, +} + +impl Pending { + /// How much is pending, to [`Self::truncate`] to later. + pub(super) fn len(&self) -> [usize; 4] { + [ + self.copies.len(), + self.uses.len(), + self.checks.len(), + self.iterations.len(), + ] + } + + pub(super) fn truncate(&mut self, [copies, uses, checks, iterations]: [usize; 4]) { + self.copies.truncate(copies); + self.uses.truncate(uses); + self.checks.truncate(checks); + self.iterations.truncate(iterations); + } + + pub(super) fn clear(&mut self) { + self.truncate([0; 4]); + } +} + +impl Analyser { + /// Records that the call at `site` committed to the generic functions + /// [`Pending::uses`] holds, unless it's in a copy of a constraint, whose + /// pick belongs to the function it was copied from. A tuple operator's + /// call commits to one overload per element, which needs no instance. + pub(super) fn record_uses(&mut self, site: CallSite, span: Span) { + let uses = std::mem::take(&mut self.pending.uses); + if self.copied_through.is_some() { + return; + } + if let [(function, key)] = &uses[..] { + self.uses.push(GenericUse { + site, + function: *function, + key: key.clone(), + instance: self.recording, + span, + }); + } + } + + /// Finds the instance each use of a generic function in the batch needs, + /// and points the use at it. A new instance's constraints are solved for + /// its types, which records its picks and the instances its own uses + /// need, until there are no new ones. Every error was reported where the + /// constraints were copied to the calls, so solving them again reports + /// nothing. + pub(super) fn instantiate_uses(&mut self) { + let mut new = Vec::new(); + loop { + let uses = std::mem::take(&mut self.uses); + for generic_use in uses { + self.point_at_instance(&generic_use, &mut new); + } + let Some((instance, function, key)) = new.pop() else { + break; + }; + self.solve_instance(instance, function, &key); + } + } + + /// Points `generic_use` at the instance it needs, adding it to `new` if + /// it's new. A use that left the pick to the program as it runs keeps + /// it. + fn point_at_instance( + &mut self, + generic_use: &GenericUse, + new: &mut Vec<(SymbolId, SymbolId, Vec)>, + ) { + let key = generic_use + .key + .iter() + .map(|typ| self.vars.shown(typ)) + .collect::>(); + debug_assert!( + !key.iter() + .any(|typ| typ.contains(&|part| matches!(part, StaticType::Generic(_)))), + "internal error: a use outside of generic bodies has a type parameter in {key:?}" + ); + // A function whose instance calls it with types built from its own, + // as `fn f(x) => f([x])` does, would need instances without end. + // The call is left to the program as it runs. + if self.grows_an_instance(generic_use, &key) { + let name = self.resolver.symbol_name(generic_use.function).to_string(); + self.emit(AnalysisError::recursion_changes_types( + &name, + generic_use.span, + )); + return; + } + let instance = self.instance_for(generic_use.function, key, new); + if instance == generic_use.function { + return; + } + // An instance's body picks what the function's own body does, + // except where its types say otherwise. + let function = Binding::Resolved(Candidate::Scalar(generic_use.function)); + let picked = generic_use + .instance + .and_then(|of| self.pick_tables(Some(of)).at(generic_use.site).cloned()) + .or_else(|| self.pick_tables(None).at(generic_use.site).cloned()); + if picked != Some(function) { + return; + } + self.pick_tables(generic_use.instance).insert( + generic_use.site, + Binding::Resolved(Candidate::Scalar(instance)), + ); + } + + /// Whether `generic_use`, in the body of an instance of the function it + /// uses, needs an instance whose key holds that instance's key inside a + /// bigger type: `f>` from `f`. + fn grows_an_instance(&self, generic_use: &GenericUse, key: &[StaticType]) -> bool { + let Some(outer) = generic_use.instance else { + return false; + }; + self.instances + .iter() + .any(|((function, outer_key), &instance)| { + instance == outer + && *function == generic_use.function + && key + .iter() + .zip(outer_key) + .any(|(new, old)| new != old && new.contains(&|part| part == old)) + }) + } + + /// The tables a pick in the body of `instance` goes in, or the shared + /// ones for `None`. + pub(super) fn pick_tables(&mut self, instance: Option) -> PickTables<'_> { + let picks = &mut self.picks; + match instance.and_then(|instance| picks.instances.get_mut(&instance)) { + Some(picks) => PickTables { + bindings: &mut picks.bindings, + getters: &mut picks.getters, + setters: &mut picks.setters, + augmented_assignments: &mut picks.augmented_assignments, + }, + None => PickTables { + bindings: &mut picks.bindings, + getters: &mut picks.getters, + setters: &mut picks.setters, + augmented_assignments: &mut picks.augmented_assignments, + }, + } + } + + /// The instance of `function` for these types of its key: the function + /// itself when they're all `Any`, as its own body picks its calls as the + /// program runs, or when it was declared by an earlier batch, which + /// compiled it already. + fn instance_for( + &mut self, + function: SymbolId, + key: Vec, + new: &mut Vec<(SymbolId, SymbolId, Vec)>, + ) -> SymbolId { + let is_own = key.iter().all(|typ| *typ == StaticType::Any); + if is_own || !self.declared_in_batch.contains(&function) { + return function; + } + if let Some(&instance) = self.instances.get(&(function, key.clone())) { + return instance; + } + let name = format!( + "{}<{}>", + self.resolver.symbol_name(function), + key.iter() + .map(ToString::to_string) + .collect::>() + .join(", ") + ); + let instance = self.resolver.declare_instance(name); + self.types + .declare(instance, self.types.get_type(function).clone()); + let declaration = self.schemes[&function].declaration; + self.picks + .instances + .insert(instance, InstancePicks::new(declaration)); + self.instances.insert((function, key.clone()), instance); + new.push((instance, function, key)); + instance + } + + /// Solves the constraints of `function` for the instance whose key has + /// these types, recording the picks they make in the instance's picks. + fn solve_instance(&mut self, instance: SymbolId, function: SymbolId, key: &[StaticType]) { + let scheme = self.schemes[&function].clone(); + let mut made = scheme + .key + .iter() + .cloned() + .zip(key.iter().cloned()) + .collect::>(); + let outer = self.recording.replace(instance); + let first = self.vars.count(); + for waiting in scheme.constraints.iter() { + let mut copy = Waiting { + constraint: waiting + .constraint + .map_types(&mut |typ| self.vars.instantiate_with(typ, &mut made)), + origin: waiting.origin.clone(), + }; + if !self.try_waiting(&mut copy) { + self.wait_as(copy); + } + } + for (node, typ, expected) in scheme.checks.iter() { + let typ = self.vars.instantiate_with(typ, &mut made); + let expected = self.vars.instantiate_with(expected, &mut made); + self.expect_if_any(*node, typ, expected); + } + for &call in scheme.recursive_calls.iter() { + self.pick_tables(Some(instance)).insert( + CallSite::Call(call), + Binding::Resolved(Candidate::Scalar(instance)), + ); + } + for generic_use in scheme.uses.iter() { + let key = generic_use + .key + .iter() + .map(|typ| self.vars.instantiate_with(typ, &mut made)) + .collect(); + self.uses.push(GenericUse { + site: generic_use.site, + function: generic_use.function, + key, + instance: Some(instance), + span: generic_use.span, + }); + } + self.settle_waiting(first); + self.recording = outer; + } +} + +/// The tables of one body's picks, by the kind of site. +pub(super) struct PickTables<'a> { + bindings: &'a mut AHashMap>, + getters: &'a mut AHashMap>, + setters: &'a mut AHashMap>, + augmented_assignments: &'a mut AHashMap>, +} + +impl<'a> PickTables<'a> { + fn table(self, site: CallSite) -> (&'a mut AHashMap>, NodeId) { + match site { + CallSite::Call(id) => (self.bindings, id), + CallSite::Getter(id) => (self.getters, id), + CallSite::Setter(id) | CallSite::FieldSetter(id) => (self.setters, id), + CallSite::OpAssignment(id) => (self.augmented_assignments, id), + } + } + + /// The pick at `site`. + pub(super) fn at(self, site: CallSite) -> Option<&'a Binding> { + let (table, id) = self.table(site); + table.get(&id) + } + + /// Records `binding` as the pick at `site`. + pub(super) fn insert(self, site: CallSite, binding: Binding) { + let (table, id) = self.table(site); + table.insert(id, binding); + } +} diff --git a/ndc_analyser/src/analyser/mod.rs b/ndc_analyser/src/analyser/mod.rs index b34fb0d8..9464dfe5 100644 --- a/ndc_analyser/src/analyser/mod.rs +++ b/ndc_analyser/src/analyser/mod.rs @@ -1,20 +1,30 @@ use crate::error::AnalysisError; use crate::overloads::CallKind; -use crate::types::{TypeBinding, TypeTable}; -use ahash::AHashMap; -use ndc_analysis::{Analysis, Binding, Candidate}; -use ndc_core::StaticType; +use crate::types::{TypeTable, TypeVars}; +use ahash::{AHashMap, AHashSet}; +use ndc_analysis::Analysis; use ndc_core::r#struct::{StructInfo, StructRegistry}; +use ndc_core::{NodeId, StaticType, TypeVar}; use ndc_layout::{Layout, Picks}; -use ndc_lexer::NumericLiteral; +use ndc_lexer::{NumericLiteral, Span}; use ndc_parser::{Expression, ExpressionLocation, ForBody, ForIteration, TypeExpr}; use ndc_resolver::{NameResolver, SymbolId}; +use std::collections::BTreeMap; use std::fmt::Debug; use std::rc::Rc; mod assignments; mod calls; mod declarations; +mod function_values; +pub(crate) mod generics; +mod instances; +mod solver; + +use declarations::FunctionDeclaration; +pub use generics::tied_natives; +use instances::{GenericUse, Pending, Scheme}; +use solver::{Constraint, EarlyPick, Origin, Waiting}; #[cfg(test)] mod tests; @@ -37,16 +47,74 @@ pub struct Analyser { struct_registry: StructRegistry, /// The type of every value declared so far. types: TypeTable, + /// What the type variables in those types stand for, once known. + vars: TypeVars, + /// The calls and other uses that wait for types that aren't known yet, + /// by the order they started waiting in. + waiting: BTreeMap, + /// The constraints of each generic function declared in the program, + /// such as `+(A, Int) -> B` for `fn inc(x) => x + 1`, which a call of it + /// copies with the types it's called with. + schemes: AHashMap, + /// What a call just copied from a generic function, until the call is + /// known to commit to it. + pending: Pending, + /// The uses of generic functions in the batch whose instances aren't + /// known yet. + uses: Vec, + /// The instance of each generic function for each set of types of its + /// key that a call gave it. + instances: AHashMap<(SymbolId, Vec), SymbolId>, + /// The instance whose constraints are being solved, which gets the + /// picks they make. + recording: Option, + /// The generic functions the batch declares, which the batch compiles + /// instances of. + declared_in_batch: AHashSet, + /// The generic function whose body is being analysed. + declaring: Option, + /// The calls its body makes of itself. + recursive_calls: Vec, + /// The nodes of overloaded function values that waited for the type + /// expected of them, as `abs` in `[len, abs]` does. + waiting_function_values: AHashSet, + /// The types an anonymous function's unannotated parameters get from + /// the call it's passed to, by the function's node: in + /// `players.map(fn(p) => p.score)`, `p` is a `Player`. + expected_parameters: AHashMap>>, + /// The calls the constraint being solved was copied through, when it's + /// a copy of a generic function's constraint. + copied_through: Option, + /// Where a value is checked as the program runs if it's of type `Any`, + /// with its type and the type it's checked against, which may both + /// still have variables in them, by the instance whose body checks it, + /// or `None` for code compiled once. + checks: Vec<(Option, NodeId, StaticType, StaticType)>, + /// The number the next waiting constraint gets. + next_serial: u64, + /// The waiting constraints that each unsettled variable would let make + /// progress once settled. + watchers: AHashMap>, + /// [`TypeVars::settled_count`] when the waiting constraints were last + /// woken, so the ones watching a variable settled since are woken next. + watched_settlings: usize, + /// The calls that committed to an overload before their arguments were + /// known, which debug builds check at the end of the batch. + early_picks: Vec, + /// The nodes of `xs[i] op= v` whose read of `xs[i]` failed, so the + /// write, which fails the same way, isn't reported again. + failed_reads: AHashSet, /// What each struct's type symbol stands for, so `p: Point` lowers to /// `Point`'s struct type. Every other type name is built in. struct_types: AHashMap>, - /// Stack of explicit `return` types for each enclosing function scope. - /// Pushed on function entry, popped on exit. The value accumulates the - /// lub of all `return ` types seen so far. - return_type_stack: Vec>, - /// How many loops enclose the expression being analysed in the current - /// function, so `break` and `continue` outside of one are reported. - loop_depth: usize, + /// What each enclosing function returns with `return`, innermost last. + /// Pushed on function entry, popped on exit. + return_type_stack: Vec, + /// The loops that enclose the expression being analysed in the current + /// function, innermost last, each with whether a `break` leaves it: a + /// `while true` that none leaves never ends. `break` and `continue` + /// outside of one are reported. + loops: Vec, /// What the current batch's analysis has found so far. result: AnalysisResult, /// The symbols the current batch's uses pick, which the layout turns into @@ -57,6 +125,15 @@ pub struct Analyser { laid_out_scopes: usize, } +/// What a function returns. +#[derive(Debug)] +pub(super) struct Returns { + /// Its annotated return type. + annotated: Option, + /// The type its values have in common, without an annotation. + joined: Option, +} + impl Analyser { /// An analyser for programs that can call `natives`, given as name and /// type in registry order: the first is the native in global slot 0. @@ -67,15 +144,34 @@ impl Analyser { ); let mut types = TypeTable::default(); for ((_, typ), symbol) in natives.into_iter().zip(resolver.native_symbols()) { - types.declare(symbol, TypeBinding::Inferred(typ)); + types.declare(symbol, typ); } Self { resolver, types, + vars: TypeVars::default(), + waiting: BTreeMap::new(), + schemes: AHashMap::new(), + pending: Pending::default(), + uses: Vec::new(), + instances: AHashMap::new(), + recording: None, + declared_in_batch: AHashSet::new(), + declaring: None, + recursive_calls: Vec::new(), + waiting_function_values: AHashSet::new(), + expected_parameters: AHashMap::new(), + copied_through: None, + checks: Vec::new(), + next_serial: 0, + watchers: AHashMap::new(), + watched_settlings: 0, + failed_reads: AHashSet::new(), + early_picks: Vec::new(), struct_registry: StructRegistry::default(), struct_types: AHashMap::new(), return_type_stack: Vec::new(), - loop_depth: 0, + loops: Vec::new(), result: AnalysisResult::default(), picks: Picks::default(), laid_out_scopes: 0, @@ -85,6 +181,55 @@ impl Analyser { /// Take the accumulated analysis result (including any errors), /// resetting it for the next analysis. pub fn take_result(&mut self) -> AnalysisResult { + // Nothing after this batch can settle its type variables any more, + // so the ones left unsettled get their fallbacks, what waited on them + // is solved, and no variable leaves the analyser. + self.settle_waiting(0); + self.vars.forget_literals(); + self.instantiate_uses(); + self.declared_in_batch.clear(); + debug_assert!(self.waiting.is_empty(), "unsolved: {:?}", self.waiting); + self.watchers.clear(); + if cfg!(debug_assertions) { + self.check_early_picks(); + } + self.failed_reads.clear(); + self.waiting_function_values.clear(); + self.types.resolve_all(&self.vars); + for (instance, node, typ, expected) in std::mem::take(&mut self.checks) { + if self.vars.resolve(&typ) != StaticType::Any { + continue; + } + let expected = self + .vars + .finish(&expected) + .replace(&|part| matches!(part, StaticType::Generic(_)).then_some(StaticType::Any)); + if expected == StaticType::Any { + continue; + } + let checks = match instance.and_then(|instance| self.picks.instances.get_mut(&instance)) + { + Some(picks) => &mut picks.type_checks, + None => &mut self.result.analysis.type_checks, + }; + checks.insert(node, expected); + } + let analysis = &mut self.result.analysis; + for table in [ + &mut analysis.expr_types, + &mut analysis.variable_types, + &mut analysis.inferred_return_types, + &mut analysis.function_types, + ] { + for typ in table.values_mut() { + *typ = self.vars.finish(typ); + debug_assert!(!typ.has_vars(), "unsettled type variable in {typ}"); + } + } + for typ in analysis.cast_checks.values_mut().flatten() { + *typ = self.vars.finish(typ); + } + let mut result = std::mem::take(&mut self.result); Layout::new(&self.resolver).lay_out( std::mem::take(&mut self.picks), @@ -95,9 +240,41 @@ impl Analyser { result } + /// Records that the value of type `Any` at `node` is checked to be an + /// `expected` as the program runs, in the instance whose constraints are + /// being solved, if any. A function's own body checks its type + /// parameters as `Any`. A copy of a constraint checks nothing, as its + /// node is in the body of the function it was copied from, which the + /// instance for the call checks. + pub(super) fn expect(&mut self, node: NodeId, expected: StaticType) { + self.expect_if_any(node, StaticType::Any, expected); + } + + /// As [`Self::expect`], for a value of type `typ`, which is checked if + /// it turns out to be `Any`. + pub(super) fn expect_if_any(&mut self, node: NodeId, typ: StaticType, expected: StaticType) { + if self.copied_through.is_none() { + self.checks.push((self.recording, node, typ, expected)); + } + } + /// Records an analysis error. The analyser continues with a fallback type - /// (usually `Any`) so that subsequent code is still checked. + /// (usually [`StaticType::Error`], which nothing reports again) so that + /// the rest of the program is still checked. An error in + /// a copy of a generic function's constraint is the call's: it's + /// reported there, with notes pointing into the function. fn emit(&mut self, err: AnalysisError) { + // An instance's constraints were checked where they were copied to + // its calls. + if self.recording.is_some() { + return; + } + let err = match &self.copied_through { + Some(frames) => { + err.through_calls(frames.iter().map(|frame| (frame.name.as_str(), frame.call))) + } + None => err, + }; self.result.errors.push(err); } @@ -106,13 +283,25 @@ impl Analyser { /// and a struct name becomes that struct's `StaticType::Struct`. The /// resolver has already matched each name to its type symbol. A wrong /// generic-argument count is reported as an analysis error at the - /// annotation's span, and the annotation falls back to `Any` so the rest + /// annotation's span, and the annotation is a failed check so the rest /// of the program is still checked. fn lower_type_expr(&mut self, expr: &TypeExpr) -> StaticType { match expr { TypeExpr::Tuple { elements, .. } => { StaticType::Tuple(elements.iter().map(|e| self.lower_type_expr(e)).collect()) } + TypeExpr::Function { + parameters, + return_type, + .. + } => StaticType::Function { + parameters: Some(parameters.iter().map(|p| self.lower_type_expr(p)).collect()), + return_type: Box::new( + return_type + .as_ref() + .map_or_else(StaticType::unit, |r| self.lower_type_expr(r)), + ), + }, TypeExpr::Name { name, args, @@ -125,7 +314,7 @@ impl Analyser { match self.struct_types.get(&symbol) { Some(_) if has_args => { self.emit(AnalysisError::type_does_not_take_generic_args(name, *span)); - StaticType::Any + StaticType::Error } Some(info) => info.static_type(), // A built-in type: `from_name_and_args` checks its generic arity. @@ -133,7 +322,7 @@ impl Analyser { Ok(typ) => typ, Err(err) => { self.emit(AnalysisError::invalid_type_annotation(&err, *span)); - StaticType::Any + StaticType::Error } }, } @@ -141,13 +330,34 @@ impl Analyser { } } + /// Lowers an annotation of something other than a parameter, which an + /// `Iterable` can't be: `let xs: Iterable` doesn't say what + /// kind of sequence `xs` is. fn lower_annotation(&mut self, annotation: Option<&TypeExpr>) -> Option { - annotation.map(|expr| self.lower_type_expr(expr)) + annotation.map(|expr| self.lower_value_type(expr)) + } + + /// [`Self::lower_type_expr`] for the type of a value, which an + /// `Iterable` is never part of: only a built-in function takes any + /// kind of sequence of a type. + fn lower_value_type(&mut self, expr: &TypeExpr) -> StaticType { + let typ = self.lower_type_expr(expr); + self.without_iterables(typ, expr.span()) + } + + /// `typ`, reporting each `Iterable` in it, which becomes the type of + /// a failed check. + fn without_iterables(&mut self, typ: StaticType, span: Span) -> StaticType { + if !typ.contains(&|part| matches!(part, StaticType::Iterable(_))) { + return typ; + } + self.emit(AnalysisError::iterable_in_an_annotation(span)); + typ.replace(&|part| matches!(part, StaticType::Iterable(_)).then_some(StaticType::Error)) } /// Analyses a top-level statement. Names resolve first; a statement with /// unresolved names isn't analysed further, which would only repeat its errors. - /// The names it still declared are bound as `Any`, so in + /// The names it still declared are bound as failed checks, so in /// `fn f() { missing } let g = f`, only `missing` is reported. pub fn analyse(&mut self, expr_loc: &ExpressionLocation) -> StaticType { let bound_before = self.resolver.current_bindings().len(); @@ -159,93 +369,16 @@ impl Analyser { .extend(name_errors.into_iter().map(AnalysisError::from)); let declared = self.resolver.current_bindings()[bound_before..].to_vec(); for symbol in declared { - self.types - .declare(symbol, TypeBinding::Inferred(StaticType::Any)); + self.types.declare(symbol, StaticType::Error); } - return StaticType::Any; + return StaticType::Error; } self.analyse_expr(expr_loc) } fn analyse_expr(&mut self, expr_loc: &ExpressionLocation) -> StaticType { let typ = self.analyse_inner(expr_loc); - self.result - .analysis - .expr_types - .insert(expr_loc.id, typ.clone()); - typ - } - - /// Like [`Self::analyse_expr`], but with an expected type from an annotation - /// or an annotated target: `let x: List = []` types the literal as - /// `List` instead of `List`. A container literal with no - /// elements has nothing to infer from, so it adopts the expected element - /// types; non-empty literals recurse so nested empties (`[(1, [])]`) - /// adopt too. Everything else falls back to plain analysis, and the - /// caller still subtype-checks the result against the expectation. - /// - /// This is a temporary measure until the type system supports type - /// parameters and proper unification, at which point empty literals can - /// be typed with fresh type variables instead of adopting the annotation. - fn analyse_with_expected( - &mut self, - expr_loc: &ExpressionLocation, - expected: &StaticType, - ) -> StaticType { - let typ = match (&expr_loc.expression, expected) { - ( - Expression::List { values }, - StaticType::List(element) | StaticType::Sequence(element), - ) => { - let mut element_type: Option = None; - for value in values { - let found = self.analyse_with_expected(value, element); - Self::fold_lub(&mut element_type, found); - } - StaticType::List(Box::new( - element_type.unwrap_or_else(|| element.as_ref().clone()), - )) - } - (Expression::Map { values, default }, StaticType::Map { key, value }) => { - let is_empty = values.is_empty(); - let mut key_type: Option = None; - let mut value_type: Option = None; - for (entry_key, entry_value) in values { - let found = self.analyse_with_expected(entry_key, key); - Self::fold_lub(&mut key_type, found); - if let Some(entry_value) = entry_value { - let found = self.analyse_with_expected(entry_value, value); - Self::fold_lub(&mut value_type, found); - } - } - if let Some(default) = default { - let found = self.analyse_with_expected(default, value); - Self::fold_lub(&mut value_type, found); - } - StaticType::Map { - key: Box::new(key_type.unwrap_or_else(|| key.as_ref().clone())), - value: Box::new(value_type.unwrap_or_else(|| { - if is_empty { - value.as_ref().clone() - } else { - // A set literal like `%{1, 2}` has unit values. - StaticType::unit() - } - })), - } - } - (Expression::Tuple { values }, StaticType::Tuple(elements)) - if values.len() == elements.len() => - { - let types = values - .iter() - .zip(elements) - .map(|(value, element)| self.analyse_with_expected(value, element)) - .collect(); - StaticType::Tuple(types) - } - _ => return self.analyse_expr(expr_loc), - }; + self.solve_waiting(); self.result .analysis .expr_types @@ -263,9 +396,12 @@ impl Analyser { ) -> StaticType { match expression { Expression::BoolLiteral(_) => StaticType::Bool, - Expression::NoneLiteral => StaticType::Option(Box::new(StaticType::Any)), + // What `None`, `[]` and `%{}` hold is only known from later uses. + Expression::NoneLiteral => { + StaticType::Option(Box::new(self.vars.fresh(StaticType::Any))) + } Expression::StringLiteral(_) => StaticType::String, - Expression::NumericLiteral(NumericLiteral::Int64(_)) => StaticType::Int, + Expression::NumericLiteral(NumericLiteral::Int64(_)) => self.vars.fresh_number(*span), Expression::NumericLiteral(NumericLiteral::Float64(_)) => StaticType::Float, Expression::NumericLiteral( NumericLiteral::NumberInt(_) @@ -273,23 +409,19 @@ impl Analyser { | NumericLiteral::Complex(_), ) => StaticType::Number, Expression::Break => { - if self.loop_depth == 0 { - self.emit(AnalysisError::break_outside_loop(*span)); + match self.loops.last_mut() { + Some(broken) => *broken = true, + None => self.emit(AnalysisError::break_outside_loop(*span)), } StaticType::Never } Expression::Continue => { - if self.loop_depth == 0 { + if self.loops.is_empty() { self.emit(AnalysisError::continue_outside_loop(*span)); } StaticType::Never } - Expression::Identifier { .. } => { - let symbol = self.resolver.innermost_reference(*id); - let binding = Binding::Resolved(Candidate::Scalar(symbol)); - self.picks.bindings.insert(*id, binding); - self.types.get_type(symbol).clone() - } + Expression::Identifier { name, .. } => self.analyse_value_reference(*id, name, *span), Expression::Statement(inner) => { let typ = self.analyse_expr(inner); // Diverging statements (return/break/continue) propagate Never @@ -306,15 +438,43 @@ impl Analyser { StaticType::Bool } Expression::Grouping(expr) => self.analyse_expr(expr), - Expression::Cast { value, annotation } => { - let target = self.lower_type_expr(annotation); - let found = self.analyse_with_expected(value, &target); + Expression::Cast { + value, + annotation, + optional, + } => { + let target = self.lower_value_type(annotation); + let found = self.analyse_expr(value); + // What isn't known about the value yet is what the cast says + // it is: `(fn(x) => x) as Fn(Int) -> Int` takes an Int, and + // needs no check as the program runs, and `1 as Float` is a + // Float. Known parts are checked, and so is a value nothing + // is known about, like what a call that waits returns. + // `as?` only tests the value, which stays what it is: in `fn + // f(xs) { (xs as? List).is_some() }`, `xs` can still be a + // list of Strings. A number written there is what it's cast + // to, as with `as`: `1 as? Float` is `Some(1.0)`. + let found = self.vars.resolve(&found); + let unknown = matches!(found, StaticType::Var(var) if !self.vars.is_literal(var)); + let literal = matches!(found, StaticType::Var(var) if self.vars.is_literal(var)); + if found.has_vars() && !unknown && (!*optional || literal) { + self.vars.try_unify(&found, &target); + } + let found = self.vars.resolve(&found); if !found.overlaps(&target) { + let found = self.vars.shown(&found); self.emit(AnalysisError::impossible_cast(&found, &target, *span)); } - let check = (!found.is_subtype(&target)).then(|| target.clone()); + let check = (!found.fits(&target)).then(|| target.clone()); self.result.analysis.cast_checks.insert(*id, check); - target + // `as?` is the same cast, with `None` where `as` would fail. + // One that can never succeed is an error like `as`: it would + // always be `None`. + if *optional { + StaticType::Option(Box::new(target)) + } else { + target + } } Expression::VariableDeclaration { l_value, @@ -323,10 +483,7 @@ impl Analyser { } => { let annotated_type = self.lower_annotation(annotated_type.as_ref()); let value_span = value.span; - let found_type = match &annotated_type { - Some(expected) => self.analyse_with_expected(value, expected), - None => self.analyse_expr(value), - }; + let found_type = self.analyse_expr(value); self.declare_binding_pattern( l_value, @@ -338,7 +495,7 @@ impl Analyser { } Expression::Assignment { l_value, r_value } => { let old_type = self.analyse_assignment_target(l_value, *span); - let new_type = self.analyse_with_expected(r_value, &old_type); + let new_type = self.analyse_expr(r_value); self.validate_assignment_write(l_value, &old_type, &new_type, *span); StaticType::unit() @@ -353,15 +510,17 @@ impl Analyser { parameters, body, return_annotation, + pure, .. - } => self.analyse_function_declaration( - *id, - name.as_deref(), + } => self.analyse_function_declaration(FunctionDeclaration { + id: *id, + name: name.as_deref(), parameters, body, - return_annotation.as_ref(), - *span, - ), + return_annotation: return_annotation.as_ref(), + pure: *pure, + span: *span, + }), Expression::Block { statements } => { let mut last = None; for s in statements { @@ -383,7 +542,7 @@ impl Analyser { StaticType::unit() }; - true_type.lub(&false_type) + self.join(&true_type, &false_type, *span) } Expression::While { expression, @@ -391,23 +550,42 @@ impl Analyser { } => { // The condition is part of the loop too, as it runs on every // iteration. - self.loop_depth += 1; + self.loops.push(false); self.analyse_expr(expression); self.analyse_expr(loop_body); - self.loop_depth -= 1; - StaticType::unit() + let broken = self.loops.pop().expect("the loop was pushed"); + // A `while true` that no `break` leaves only ends by + // returning, so it has no value, like a `return`: a function + // whose body ends in one returns what its `return`s do. + if !broken && matches!(expression.expression, Expression::BoolLiteral(true)) { + StaticType::Never + } else { + StaticType::unit() + } } Expression::For { iterations, body } => { // A map comprehension's default is evaluated once, before the // loop starts. - if let ForBody::Map { - default: Some(default), - .. - } = body.as_ref() - { - self.analyse_expr(default); + let default = match body.as_ref() { + ForBody::Map { + default: Some(default), + .. + } => { + let typ = self.analyse_expr(default); + Some(self.constrain_default_value(typ, default.span)) + } + _ => None, + }; + match (self.analyse_for_iterations(iterations, body), default) { + // Reads can produce the default value, so it's one of the + // map's values: `%{:"none", x: x for x in xs}` holds + // Strings as well as what `xs` does. + (StaticType::Map { key, value }, Some(default)) => StaticType::Map { + key, + value: Box::new(self.join(&value, &default, *span)), + }, + (typ, _) => typ, } - self.analyse_for_iterations(iterations, body) } Expression::Call { function, @@ -416,66 +594,107 @@ impl Analyser { | Expression::MethodCall { function, arguments, - } => self.analyse_call(function, arguments, CallKind::Regular, *span), + } => self.analyse_call(*id, function, arguments, CallKind::Regular, *span), Expression::OperatorCall { function, arguments, - } => self.analyse_call(function, arguments, CallKind::Operator, *span), + } => self.analyse_call(*id, function, arguments, CallKind::Operator, *span), Expression::MemberAccess { receiver, member, member_span, } => self.analyse_member_access(*id, receiver, member, *member_span), + // Each element of a tuple literal takes the type its uses settle, + // as a list literal's elements do: `let t: (Any, Int) = ("a", 1)`. + // An element whose type isn't known yet already does. Expression::Tuple { values } => { let mut types = Vec::with_capacity(values.len()); - for v in values { - types.push(self.analyse_expr(v)); + for value in values { + let typ = self.analyse_expr(value); + types.push(match self.vars.resolve(&typ) { + StaticType::Var(_) => typ, + _ => self.holding(vec![(Some(value.id), value.span, typ)]), + }); } StaticType::Tuple(types) } Expression::List { values } => { - let element_type = self.analyse_multiple_expression_with_same_type(values); - - StaticType::List(Box::new(element_type.unwrap_or(StaticType::Any))) + let element_type = if values.is_empty() { + self.vars.fresh(StaticType::Any) + } else { + let values = values + .iter() + .map(|value| (Some(value.id), value.span, self.analyse_expr(value))) + .collect(); + self.holding(values) + }; + StaticType::List(Box::new(element_type)) } Expression::Map { values, default } => { - let mut key_type: Option = None; - let mut value_type: Option = None; + let mut keys = Vec::new(); + let mut held = Vec::new(); for (key, value) in values { - Self::fold_lub(&mut key_type, self.analyse_expr(key)); + keys.push((Some(key.id), key.span, self.analyse_expr(key))); if let Some(value) = value { - Self::fold_lub(&mut value_type, self.analyse_expr(value)); + held.push((Some(value.id), value.span, self.analyse_expr(value))); + } + } + // A key without a value holds `()`, which in `%{"a", "b": 1}` + // is one of the map's values too. + if !held.is_empty() { + for (key, _) in values.iter().filter(|(_, value)| value.is_none()) { + held.push((None, key.span, StaticType::unit())); } } // Reads can produce the default value, so its type is part of - // the map's value type. + // the map's value type. A default that's a function, as an + // `Any` one may be as the program runs, is called for each + // missing key, so what it makes isn't the default's own + // value, and has no node to be checked at. if let Some(default) = default { let default_type = self.analyse_expr(default); - Self::fold_lub(&mut value_type, default_type); + let default_type = self.constrain_default_value(default_type, default.span); + held.push((None, default.span, default_type)); } + let key_type = if keys.is_empty() { + self.vars.fresh(StaticType::Any) + } else { + self.holding(keys) + }; + // A literal with keys but no values, like `%{1, 2}`, is a set. + let value_type = if !held.is_empty() { + self.holding(held) + } else if values.is_empty() { + self.vars.fresh(StaticType::unit()) + } else { + StaticType::unit() + }; StaticType::Map { - key: Box::new(key_type.unwrap_or(StaticType::Any)), - value: Box::new(value_type.unwrap_or_else(StaticType::unit)), + key: Box::new(key_type), + value: Box::new(value_type), } } Expression::Return { value } => { let typ = self.analyse_expr(value); - match self.return_type_stack.last_mut() { - Some(slot) => Self::fold_lub(slot, typ), + match self.return_type_stack.pop() { + Some(mut returns) => { + self.add_return(&mut returns, value, typ); + self.return_type_stack.push(returns); + } None => self.emit(AnalysisError::return_outside_function(*span)), } StaticType::Never } Expression::RangeInclusive { start, end } | Expression::RangeExclusive { start, end } => { - if let Some(start) = start { - self.analyse_expr(start); - } - if let Some(end) = end { - self.analyse_expr(end); + // A range counts in Ints, so its bounds are Ints: in `0..n`, + // `n` is one, whatever else it's used for. + for bound in [start, end].into_iter().flatten() { + let typ = self.analyse_expr(bound); + self.check_write(&StaticType::Int, &typ, bound.span, Some(bound.id)); } StaticType::Iterator(Box::new(StaticType::Int)) @@ -503,11 +722,9 @@ impl Analyser { let sequence_span = sequence.span; let sequence_type = self.analyse_expr(sequence); - let found_type = sequence_type - .sequence_element_type() - .unwrap_or(StaticType::Any); + let found_type = self.constrain_iteration(sequence_type, sequence_span); - self.loop_depth += 1; + self.loops.push(false); // A `for` binder has no type annotation. self.declare_binding_pattern(l_value, None, found_type, sequence_span); } @@ -541,27 +758,64 @@ impl Analyser { } }; if matches!(iteration, ForIteration::Iteration { .. }) { - self.loop_depth -= 1; + self.loops.pop(); } typ } - fn analyse_multiple_expression_with_same_type( - &mut self, - expressions: &Vec, - ) -> Option { - let mut element_type: Option = None; - for expression in expressions { - Self::fold_lub(&mut element_type, self.analyse_expr(expression)); + /// The type of the elements of a literal holding `values`, each with + /// where it is and its type, which takes the type its uses settle: in + /// `let xs: List = [1, 2]`, `Any`. A value of type `Any` among + /// them is checked against it as the program runs, at its node if it + /// has one. + fn holding(&mut self, values: Vec<(Option, Span, StaticType)>) -> StaticType { + let element = self.vars.fresh_holding( + values + .iter() + .map(|(_, span, typ)| (typ.clone(), *span)) + .collect(), + ); + for (node, _, typ) in values { + if let Some(node) = node { + // An overloaded function value waiting for its type, as + // `abs` in `[len, abs]`, is one of the literal's values. + if self.waiting_function_values.contains(&node) { + for waiting in self.waiting.values_mut() { + if let Constraint::FunctionValue(value) = &mut waiting.constraint + && value.id() == node + { + value.mark_in_literal(); + } + } + } + self.expect_if_any(node, typ, element.clone()); + } + } + element + } + + /// The type of the values a map default of type `default` produces. A + /// function default, as in `%{:fn() => []}`, is called for each missing + /// key, so the values are what it returns. + pub(super) fn default_value_type(default: StaticType) -> StaticType { + match default { + StaticType::Function { return_type, .. } => *return_type, + other => other, } - element_type } - /// Fold a new type into an accumulator via least-upper-bound. - fn fold_lub(acc: &mut Option, new_type: StaticType) { - match acc { - Some(prev) => *prev = prev.lub(&new_type), - None => *acc = Some(new_type), + /// Adds the value `value` of type `typ` that a function returns to + /// `returns`. A function with an annotated return type returns that + /// type, and each value it returns has to fit it. Without one, it + /// returns the type its values join as, as with [`Self::join`]. + fn add_return(&mut self, returns: &mut Returns, value: &ExpressionLocation, typ: StaticType) { + if let Some(annotated) = &returns.annotated { + self.check_write(annotated, &typ, value.span, Some(value.id)); + return; } + returns.joined = Some(match returns.joined.take() { + Some(joined) => self.join(&joined, &typ, value.span), + None => typ, + }); } } diff --git a/ndc_analyser/src/analyser/solver.rs b/ndc_analyser/src/analyser/solver.rs new file mode 100644 index 00000000..91907052 --- /dev/null +++ b/ndc_analyser/src/analyser/solver.rs @@ -0,0 +1,2174 @@ +//! Calls and other uses whose types aren't known yet wait here until they +//! are. In `xs.map(fn(x) => x + 1)`, the `+` waits for `x`'s type, which +//! picking `map` settles from the type of `xs`. + +use super::Analyser; +use super::function_values::{Choice, FunctionValue}; +use crate::error::AnalysisError; +use crate::overloads::{CallKind, ResolvedCall, is_strictly_more_specific}; +use crate::types::TypeVars; +use ahash::{AHashMap, AHashSet}; +use itertools::Itertools; +use ndc_analysis::{Binding, Candidate}; +use ndc_core::{NodeId, StaticType, TypeVar}; +use ndc_lexer::Span; +use ndc_resolver::{Candidates, SymbolId}; +use std::collections::BTreeSet; +use std::rc::Rc; + +/// Whether a waiting call can still take its arguments. +enum Fit { + /// Its overload is known and takes them. + Picked, + /// Some overload might. + Maybe, + /// None can. + No, +} + +/// What a call can commit to before its arguments are fully known. +pub(super) enum EarlyOverload { + /// This overload, whatever the arguments turn out to be. + Only(SymbolId), + /// More than one could still be picked. + Undecided, + /// None can take the arguments. + NoneFits, +} + +/// Where a call's candidates come from and where its pick goes. +#[derive(Debug, Clone, Copy)] +pub(super) enum CallSite { + /// A call or an operator: the overloads the callee's name can mean. + Call(NodeId), + /// `x.f`, or reading the old value in `xs[i] += 1`. + Getter(NodeId), + /// The write in `xs[i] = v`. + Setter(NodeId), + /// The write in `p.x = v`. + FieldSetter(NodeId), + /// The operator in `a op= b`, which is `a = op(a, b)`. + OpAssignment(NodeId), +} + +/// Something that waits until the types it depends on are known. +#[derive(Debug, Clone)] +pub(super) enum Constraint { + /// A call whose overload can't be picked yet. + Call(CallConstraint), + /// An overloaded function's name used as a value, waiting for the type + /// expected of it to pick an overload. + FunctionValue(FunctionValue), + /// `for x in s`: what iterating over `sequence` gives. + Iterate { + sequence: StaticType, + element: StaticType, + span: Span, + }, + /// `let (a, b) = v`: what each name gets from `value`. + Unpack { + value: StaticType, + parts: Vec, + span: Span, + }, + /// The values a map default makes: what it returns if it's a function + /// that's called for each missing key, and the default itself if not. + MapDefault { + default: StaticType, + value: StaticType, + span: Span, + }, + /// The one type the two branches of an `if` have, once neither is + /// wholly unknown: the branches' types and then the result's. + Join { types: [StaticType; 3], span: Span }, +} + +impl Constraint { + /// The types whose variables the constraint waits on. + pub(super) fn inputs(&self) -> &[StaticType] { + match self { + Self::Call(call) => &call.arguments, + Self::FunctionValue(value) => std::slice::from_ref(value.expected()), + Self::Iterate { sequence, .. } => std::slice::from_ref(sequence), + Self::Unpack { value, .. } => std::slice::from_ref(value), + Self::MapDefault { default, .. } => std::slice::from_ref(default), + // A result known from outside says what each branch is. + Self::Join { types, .. } => types, + } + } + + /// The types the constraint settles once it's solved, which are known + /// once its inputs are. + pub(super) fn outputs(&self) -> &[StaticType] { + match self { + Self::Call(call) => std::slice::from_ref(&call.result), + Self::FunctionValue(_) => &[], + Self::Iterate { element, .. } => std::slice::from_ref(element), + Self::Unpack { parts, .. } => parts, + Self::MapDefault { value, .. } => std::slice::from_ref(value), + Self::Join { types, .. } => &types[2..], + } + } + + /// The constraint with `replace` applied to each of its types. + pub(super) fn map_types(&self, replace: &mut impl FnMut(&StaticType) -> StaticType) -> Self { + match self { + Self::Call(call) => Self::Call(CallConstraint { + arguments: call.arguments.iter().map(&mut *replace).collect(), + result: replace(&call.result), + ..call.clone() + }), + Self::FunctionValue(value) => { + Self::FunctionValue(value.with_expected(replace(value.expected()))) + } + Self::Iterate { + sequence, + element, + span, + } => Self::Iterate { + sequence: replace(sequence), + element: replace(element), + span: *span, + }, + Self::Unpack { value, parts, span } => Self::Unpack { + value: replace(value), + parts: parts.iter().map(&mut *replace).collect(), + span: *span, + }, + Self::MapDefault { + default, + value, + span, + } => Self::MapDefault { + default: replace(default), + value: replace(value), + span: *span, + }, + Self::Join { types, span } => Self::Join { + types: types.clone().map(|typ| replace(&typ)), + span: *span, + }, + } + } +} + +/// A call to a generic function, through which a constraint of the +/// function's body was copied to the code calling it. +#[derive(Debug, Clone)] +pub(super) struct Frame { + /// The generic function called. + pub(super) function: SymbolId, + /// Its name, for errors. + pub(super) name: String, + /// The call. + pub(super) call: Span, +} + +/// The calls a constraint was copied through, outermost first: the call in +/// the code being analysed, then the call in that function's body that +/// copied it there, and so on. Its own span is in the last function's body. +pub(super) type Origin = Rc<[Frame]>; + +/// A constraint that waits, and the calls it was copied through if it's a +/// copy of a generic function's constraint. +#[derive(Debug, Clone)] +pub(super) struct Waiting { + pub(super) constraint: Constraint, + pub(super) origin: Option, +} + +/// An overload a call committed to before its arguments were known, which +/// [`Analyser::check_early_picks`] checks once they are. +#[derive(Debug)] +pub(super) enum EarlyPick { + Call { + candidates: Candidates, + arguments: Vec, + kind: CallKind, + name: String, + binding: Binding, + }, + FunctionValue { + value: FunctionValue, + picked: SymbolId, + }, +} + +/// The expressions a call's arguments and value come from, where a value +/// of type `Any` is checked against the type the call expects of it as the +/// program runs: in `xs.push(a)` on a `List`, `a` is checked to be an +/// Int. The value an assignment stores is checked at the assignment's +/// target. +#[derive(Debug, Clone, Default)] +pub(super) struct CallNodes { + /// Where each argument comes from. + pub(super) arguments: Vec>, + /// The call itself, whose value is checked when it's `Any` but code + /// meanwhile settled the type it has. + pub(super) result: Option, +} + +#[derive(Debug, Clone)] +pub(super) struct CallConstraint { + site: CallSite, + candidates: Candidates, + arguments: Vec, + nodes: CallNodes, + /// The call's type, which the picked overload's return type settles. + result: StaticType, + kind: CallKind, + name: String, + span: Span, + /// The index of `t[1]`, a number written in the code, which picks the + /// element of a tuple `t` and its type. + tuple_index: Option, +} + +impl CallConstraint { + pub(super) fn site(&self) -> CallSite { + self.site + } + + pub(super) fn candidates(&self) -> &Candidates { + &self.candidates + } + + pub(super) fn arguments(&self) -> &[StaticType] { + &self.arguments + } + + pub(super) fn result(&self) -> &StaticType { + &self.result + } + + pub(super) fn kind(&self) -> CallKind { + self.kind + } + + /// Whether `other` calls what this calls, the same way, with arguments + /// of the same types, so it returns the same type. + pub(super) fn is_same_call(&self, other: &Self, vars: &TypeVars) -> bool { + std::mem::discriminant(&self.site) == std::mem::discriminant(&other.site) + && self.kind == other.kind + && self.candidates == other.candidates + && self.tuple_index == other.tuple_index + && self.arguments.len() == other.arguments.len() + && self + .arguments + .iter() + .zip(&other.arguments) + .all(|(a, b)| vars.resolve(a) == vars.resolve(b)) + } +} + +impl Analyser { + /// Picks the overload a call means and returns the call's type. When the + /// arguments aren't known well enough to pick one, the call waits, and + /// its type is a variable that the pick settles later. + pub(super) fn constrain_call( + &mut self, + site: CallSite, + arguments: Vec, + nodes: CallNodes, + kind: CallKind, + name: &str, + span: Span, + ) -> StaticType { + self.constrain_index_call(site, arguments, nodes, kind, name, span, None) + } + + /// [`Self::constrain_call`], for `[]` with the index `tuple_index` when + /// it's a number written in the code, which picks a tuple's element. + #[allow(clippy::too_many_arguments)] + pub(super) fn constrain_index_call( + &mut self, + site: CallSite, + arguments: Vec, + nodes: CallNodes, + kind: CallKind, + name: &str, + span: Span, + tuple_index: Option, + ) -> StaticType { + let mut call = CallConstraint { + candidates: self.site_candidates(site).clone(), + site, + arguments, + nodes, + result: StaticType::Never, + kind, + name: name.to_string(), + span, + tuple_index, + }; + if let Some(typ) = self.try_call(&call) { + return typ; + } + call.result = self.vars.fresh(StaticType::Any); + let result = call.result.clone(); + self.wait(Constraint::Call(call)); + result + } + + /// What iterating over a value of type `sequence` gives, which waits + /// until the kind of sequence is known. + pub(super) fn constrain_iteration(&mut self, sequence: StaticType, span: Span) -> StaticType { + if self.vars.waits_to_iterate(&sequence) { + let element = self.vars.fresh(StaticType::Any); + self.wait(Constraint::Iterate { + sequence, + element: element.clone(), + span, + }); + return element; + } + self.iteration_gives(&sequence, span) + } + + /// What iterating over a value of the known type `sequence` gives, + /// reporting a value that doesn't iterate, like `true`. + fn iteration_gives(&mut self, sequence: &StaticType, span: Span) -> StaticType { + if let Some(element) = self.vars.element_type(sequence) { + return element; + } + let error = AnalysisError::not_iterable(&self.vars.shown(sequence), span); + self.emit(error); + StaticType::Error + } + + /// The types `let (a, b, …) = value` gives each name when the kind of + /// value isn't known yet. They're settled once it is. + pub(super) fn constrain_unpacking( + &mut self, + value: StaticType, + count: usize, + span: Span, + ) -> Vec { + let parts = (0..count) + .map(|_| self.vars.fresh(StaticType::Any)) + .collect::>(); + self.wait(Constraint::Unpack { + value, + parts: parts.clone(), + span, + }); + parts + } + + /// The one type that values of type `a` and `b` both have, as the two + /// branches of an `if`: the type they are once made the same, or `Any` + /// if they can't be. `if c { 1 } else { 2.5 }` is a Float, `if c { [] } + /// else { [1] }` a `List`, and `if c { 1 } else { "a" }` is `Any`. + /// A branch nothing is known about yet waits, so a lambda's parameter + /// isn't fixed by the branch next to it: in `fn(x) => if c { x } else + /// { 0 }`, `x` can still be a `String`. + pub(super) fn join(&mut self, a: &StaticType, b: &StaticType, span: Span) -> StaticType { + let (a, b) = (self.vars.resolve(a), self.vars.resolve(b)); + let unknown = |this: &Self, typ: &StaticType| matches!(typ, StaticType::Var(var) if !this.vars.is_literal(*var)); + match (&a, &b) { + (StaticType::Never, _) => b, + (_, StaticType::Never) => a, + _ if a == b => a, + _ if unknown(self, &a) || unknown(self, &b) => { + let result = self.vars.fresh(StaticType::Any); + self.wait(Constraint::Join { + types: [a, b, result.clone()], + span, + }); + result + } + _ => self.join_known(&a, &b), + } + } + + /// [`Self::join`] of two types that aren't wholly unknown: the type + /// they are once made the same, or `Any`. + fn join_known(&mut self, a: &StaticType, b: &StaticType) -> StaticType { + let snapshot = self.vars.snapshot(); + if self.vars.unify(a, b) { + return self.vars.resolve(a); + } + self.vars.rollback(snapshot); + StaticType::Any + } + + /// Commits `call` to `picked` while its arguments still have unknown + /// parts, which settles them from `picked`'s parameters, and returns the + /// call's type, or `None` when they don't fit after all. The program + /// calls `picked` directly only where it would once the arguments are + /// known: an `Any` argument, as `xs` in `fn big(xs) { xs.filter(…) }`, is + /// checked as the program runs. + fn commit_early( + &mut self, + call: &CallConstraint, + arguments: &[StaticType], + picked: SymbolId, + ) -> Option { + let Some(typ) = self.commit_call( + &Binding::Resolved(Candidate::Scalar(picked)), + arguments, + &call.nodes.arguments, + StaticType::Any, + ) else { + self.pending.clear(); + return None; + }; + let arguments = arguments + .iter() + .map(|argument| self.vars.resolve(argument)) + .collect::>(); + // An argument of type `Any` is checked where it's passed, so the + // program can call `picked` directly, unless one has nowhere to be + // checked. + let binding = if Self::checks_every_any(&arguments, &call.nodes.arguments) { + Binding::Resolved(Candidate::Scalar(picked)) + } else { + Binding::Dynamic(vec![Candidate::Scalar(picked)]) + }; + if cfg!(debug_assertions) { + self.early_picks.push(EarlyPick::Call { + candidates: call.candidates.clone(), + arguments: call.arguments.clone(), + kind: call.kind, + name: call.name.clone(), + binding: binding.clone(), + }); + } + self.record_pick(call.site, binding, &arguments); + self.flush_copies(Some(call.site), call.span); + Some(typ) + } + + /// Checks that each call that committed to an overload before its + /// arguments were known got the overload it would get now that they + /// are. Committing early is only right when nothing learned later could + /// change the pick, which is easy to get wrong, so debug builds check + /// every early commit the tests make. + pub(super) fn check_early_picks(&mut self) { + let early_picks = std::mem::take(&mut self.early_picks); + for pick in early_picks { + match pick { + EarlyPick::Call { + candidates, + arguments, + kind, + name, + binding: picked, + } => { + let arguments = arguments + .iter() + .map(|argument| self.vars.resolve(argument)) + .collect::>(); + // A call passed a value whose check failed later picks + // nothing, and the program doesn't run. + if arguments.iter().any(StaticType::has_errors) { + continue; + } + // A pick left to the program among one overload is a + // call of it, with its `Any` arguments checked. + let binding = match self + .resolve_overloads(&candidates, &arguments, kind) + .binding + { + Binding::Dynamic(only) + if let [Candidate::Scalar(only)] = only[..] + && let StaticType::Function { + parameters: Some(_), + .. + } = self.vars.resolve(self.types.get_type(only)) => + { + Binding::Resolved(Candidate::Scalar(only)) + } + binding => binding, + }; + assert!( + binding == picked, + "internal error: `{name}` committed to {picked:?} before its arguments \ + were known, but with ({}) it means {binding:?}", + arguments + .iter() + .map(ToString::to_string) + .collect::>() + .join(", "), + ); + } + EarlyPick::FunctionValue { value, picked } => { + let expected = self.vars.resolve(value.expected()); + if expected.has_errors() { + continue; + } + let choice = self.choose_function_value(&value, &expected); + assert!( + choice == Choice::One(picked), + "internal error: the function value {value:?} committed to {picked:?} \ + before its type was known, but with {} it means {choice:?}", + self.vars.resolve(value.expected()), + ); + } + } + } + } + + /// Remembers that `value` committed to `picked` before the type expected + /// of it was known, for [`Self::check_early_picks`]. + pub(super) fn remember_early_function_value( + &mut self, + value: &FunctionValue, + picked: SymbolId, + ) { + self.early_picks.push(EarlyPick::FunctionValue { + value: value.clone(), + picked, + }); + } + + /// The values a map default of type `default` makes, which waits until + /// it's known whether the default is a function: in `fn(f) => %{:f}`, it + /// depends on what's passed for `f`. + pub(super) fn constrain_default_value( + &mut self, + default: StaticType, + span: Span, + ) -> StaticType { + match self.vars.resolve(&default) { + StaticType::Var(_) => { + let value = self.vars.fresh(StaticType::Any); + self.wait(Constraint::MapDefault { + default, + value: value.clone(), + span, + }); + value + } + known => Self::default_value_type(known), + } + } + + /// Makes `constraint` wait, in the order it started waiting, until one + /// of the variables it waits on is settled. + pub(super) fn wait(&mut self, constraint: Constraint) { + let origin = self.copied_through.clone(); + self.wait_as(Waiting { constraint, origin }); + } + + /// Makes `waiting` wait, as [`Self::wait`]. + pub(super) fn wait_as(&mut self, waiting: Waiting) { + let serial = self.next_serial; + self.next_serial += 1; + self.wait_again(serial, waiting); + } + + fn wait_again(&mut self, serial: u64, waiting: Waiting) { + for var in waiting + .constraint + .inputs() + .iter() + .flat_map(|input| self.vars.unsettled(input)) + { + self.watchers.entry(var).or_default().push(serial); + } + self.waiting.insert(serial, waiting); + } + + /// Solves `waiting` if what it waits on is known, as + /// [`Self::try_constraint`], and returns whether it did. A copy of a + /// generic function's constraint reports its errors at the call it was + /// copied through, and records no pick: the function's body was + /// compiled with its own. + pub(super) fn try_waiting(&mut self, waiting: &mut Waiting) -> bool { + let outer = std::mem::replace(&mut self.copied_through, waiting.origin.clone()); + let solved = self.try_constraint(&mut waiting.constraint); + self.copied_through = outer; + solved + } + + /// Tries again each waiting constraint that waits on a variable settled + /// since this last ran, oldest first, until that settles nothing more. + pub(super) fn solve_waiting(&mut self) { + self.solve_waiting_and_report(); + } + + /// [`Self::solve_waiting`], returning the constraints it tried that + /// still wait. + fn solve_waiting_and_report(&mut self) -> BTreeSet { + let mut still_waiting = BTreeSet::new(); + loop { + let settled = self.vars.settled_since(self.watched_settlings); + if settled.is_empty() { + break; + } + let woken = settled + .iter() + .filter_map(|var| self.watchers.remove(var)) + .flatten() + .collect::>(); + self.watched_settlings = self.vars.settled_count(); + for serial in woken { + // A constraint woken by an earlier variable may be solved already. + let Some(mut waiting) = self.waiting.remove(&serial) else { + continue; + }; + if self.try_waiting(&mut waiting) { + still_waiting.remove(&serial); + } else { + self.wait_again(serial, waiting); + still_waiting.insert(serial); + } + } + } + self.watched_settlings = self.vars.settled_count(); + still_waiting + } + + /// Solves what waits on the variables made since `first`, which nothing + /// outside the code they were made in can settle. Literals take their + /// defaults first. Then what a default rule can decide is decided by it: + /// a call commits to its narrowest overload, `a op= b` keeps the type of + /// `b`, and a branch of an `if` takes the other branch's type. What's + /// left gets its fallbacks, oldest constraint first. Constraints that wait only on + /// older variables keep waiting. + pub(super) fn settle_waiting(&mut self, first: usize) { + // Each constraint is checked for a default rule once, and again only + // after something it waits on changed. + let mut unchecked = self.waiting.keys().copied().collect::>(); + unchecked.extend(self.solve_waiting_and_report()); + // Fallbacks only ever make more known, so a constraint found to wait + // on no variable made since `first` never needs them. + let mut fallen_back = 0; + loop { + // A literal takes its default before anything else's is applied, + // as a call its type would settle may be waiting for it. + if self.default_literals(|_, var| var.0 as usize >= first) { + unchecked.extend(self.waiting.keys().copied()); + } + while let Some(serial) = unchecked.pop_first() { + let vars = self.waits_on_vars_since(serial, first); + if !vars.is_empty() + && (self.default_to_narrowest(serial) + || self.default_to_operand(serial) + || self.default_to_branch(serial, &vars)) + { + unchecked.extend(self.solve_waiting_and_report()); + } + } + let next = self + .waiting + .range(fallen_back..) + .map(|(&serial, _)| serial) + .find_map(|serial| { + let vars = self.waits_on_vars_since(serial, first); + (!vars.is_empty()).then_some((serial, vars)) + }); + let Some((serial, vars)) = next else { + break; + }; + fallen_back = serial; + self.vars.settle_with_fallbacks(&vars); + unchecked.extend(self.solve_waiting_and_report()); + } + self.vars.settle_from(first); + self.solve_waiting(); + // A call can still wait when its arguments are known: a variable's + // type might have settled since, which nothing watches. + let serials = self.waiting.keys().copied().collect::>(); + for serial in serials { + self.retry(serial); + } + self.solve_waiting(); + } + + /// Settles the types of the literals that `belongs` says to that nothing + /// settled, and returns whether it settled any. Each takes the first of + /// its defaults that every waiting call it's in can still take: in + /// `let total = 0; total += 2.5`, `+` with a Float can't give an Int + /// back to `total`, so `0` is a Float. + pub(super) fn default_literals( + &mut self, + belongs: impl Fn(&TypeVars, TypeVar) -> bool, + ) -> bool { + let mut settled_any = false; + loop { + let mut progress = false; + let holders = self.vars.holders(); + for var in self.vars.unsettled_literals(&belongs) { + if !self.vars.is_literal(var) { + continue; + } + let Some(defaults) = self.vars.literal_defaults(var) else { + continue; + }; + let fits = defaults.iter().position(|default| { + let snapshot = self.vars.snapshot(); + let fits = self.literal_fits_as(var, default, &holders); + self.vars.rollback(snapshot); + fits + }); + // When none fits, the first one says best what's wrong. + let default = &defaults[fits.unwrap_or(0)]; + let holders_of_var = holders + .get(&var) + .into_iter() + .flatten() + .copied() + .filter(|&holder| self.vars.is_literal(holder)) + .collect::>(); + let snapshot = self.vars.snapshot(); + if self + .settle_with_fellows(var, default, &holders_of_var) + .is_none() + { + self.vars.rollback(snapshot); + if self.vars.holds_several_literals(var) { + self.report_literals_that_differ(var, default); + } else { + self.vars.settle_with_fallbacks(&[var]); + } + } + self.solve_waiting(); + progress = true; + settled_any = true; + } + if !progress { + // What's left holds literals that aren't these to settle, + // like `n` in `let n = 1; fn f() { [n] }`, and is what they + // are: a list of whatever `n` is. + let left = self.vars.unsettled_literals(&belongs); + if left.is_empty() { + return settled_any; + } + for var in left { + match self.vars.held_literal(var) { + Some(held) if self.vars.is_literal(var) => { + self.vars.settle_literal(var, &StaticType::Var(held)); + } + _ => self.vars.settle_with_fallbacks(&[var]), + } + } + self.solve_waiting(); + settled_any = true; + } + } + } + + /// Reports the values of the literals whose element type is `var` that + /// don't fit `default`, and settles it as `default`: after `let v = + /// ["s"]; v = [1]`, `1` isn't a String. + fn report_literals_that_differ(&mut self, var: TypeVar, default: &StaticType) { + for (held, span) in self.vars.held(var) { + let snapshot = self.vars.snapshot(); + let fits = self.vars.bind_argument(default, &held); + self.vars.rollback(snapshot); + if !fits { + let error = AnalysisError::mismatched_types(&self.vars.shown(&held), default, span); + self.emit(error); + } + } + self.vars.settle_literal(var, default); + } + + /// Settles the literal `var` as `default`, and returns whether the + /// waiting calls on it still fit, and those on the literals that hold + /// it, `holders` says which, with each settled as one of its own + /// defaults: in `let x = n[0]; x = 0.5` on `n = [1]`, `n`'s element type + /// holds the `1`, and `[]` gives a Float only if it's a Float. The + /// other values those hold take the same type, as the values of a + /// literal have one type when they can: `[1, 2]` holds Ints or Floats, + /// not one of each. + fn literal_fits_as( + &mut self, + var: TypeVar, + default: &StaticType, + holders: &AHashMap>, + ) -> bool { + let holders_of_var = holders + .get(&var) + .into_iter() + .flatten() + .copied() + .filter(|&holder| self.vars.is_literal(holder)) + .collect::>(); + let Some(settled) = self.settle_with_fellows(var, default, &holders_of_var) else { + return false; + }; + if !self.calls_still_fit(&settled) { + return false; + } + holders_of_var.into_iter().all(|holder| { + if !self.vars.is_literal(holder) { + return true; + } + // One that still waits for another literal is checked with it. + let Some(defaults) = self.vars.literal_defaults(holder) else { + return true; + }; + defaults.iter().any(|default| { + let snapshot = self.vars.snapshot(); + let fits = self.literal_fits_as(holder, default, holders); + if !fits { + self.vars.rollback(snapshot); + } + fits + }) + }) + } + + /// Settles the literal `var` as `default`, and the other literals that + /// `holders` hold too where they can take it, as the values of `[1, 2]` + /// have one type. Returns the literals it settled, or `None` if `var` + /// can't be `default`. + fn settle_with_fellows( + &mut self, + var: TypeVar, + default: &StaticType, + holders: &[TypeVar], + ) -> Option> { + if !self.vars.settle_literal(var, default) { + return None; + } + let mut settled = vec![var]; + for &holder in holders { + for other in self.vars.held_literals(holder) { + let snapshot = self.vars.snapshot(); + if self.vars.settle_literal(other, default) { + settled.push(other); + } else { + self.vars.rollback(snapshot); + } + } + } + Some(settled) + } + + /// Whether the waiting calls that wait on `vars` can still take their + /// arguments, and give back what's expected of them, together: in + /// `let x = xs[0] + 0.5; xs.push(x)` on `xs = [1]`, `+` gives a Float + /// when the `1` is an Int, which `push` then can't take. A call whose + /// overload is known says what it gives back, so the calls that wait + /// on that are checked too, and the others again. + fn calls_still_fit(&mut self, vars: &[TypeVar]) -> bool { + let mut serials = Vec::new(); + let mut seen = AHashSet::new(); + for var in vars { + for &watcher in self.watchers.get(var).into_iter().flatten() { + if seen.insert(watcher) { + serials.push(watcher); + } + } + } + let mut picked = AHashSet::new(); + loop { + let mut learned = false; + let mut next = 0; + while let Some(&serial) = serials.get(next) { + next += 1; + if picked.contains(&serial) { + continue; + } + let Some(Waiting { + constraint: Constraint::Call(call), + .. + }) = self.waiting.get(&serial).cloned() + else { + continue; + }; + let reached = std::iter::once(&call.result) + .chain(&call.arguments) + .flat_map(|typ| self.vars.unsettled(typ)) + .collect::>(); + match self.call_still_fits(&call) { + Fit::No => return false, + Fit::Maybe => {} + Fit::Picked => { + picked.insert(serial); + learned = true; + for var in reached { + for &watcher in self.watchers.get(&var).into_iter().flatten() { + if seen.insert(watcher) { + serials.push(watcher); + } + } + } + } + } + } + if !learned { + return true; + } + } + } + + /// Whether some overload of `call` takes its arguments, as far as + /// they're known, and returns what fits its result. Once they're + /// known, the call gets the overload that resolution picks, scope by + /// scope, so only that one counts, and what it binds is kept: + /// `[](Any, Any)` takes `xs[0]` but loses to `[](List, Int) -> T`. + fn call_still_fits(&mut self, call: &CallConstraint) -> Fit { + let arguments = call + .arguments + .iter() + .map(|argument| self.vars.resolve(argument)) + .collect::>(); + if Self::operand_could_be_a_tuple(call.kind, &arguments) { + return Fit::Maybe; + } + if arguments.iter().all(|argument| !argument.has_vars()) + && let Binding::Resolved(Candidate::Scalar(symbol)) = self + .overloads() + .resolve_call(&call.candidates, &arguments, call.kind) + .binding + { + if self.takes_a_number_as_a_fallback(symbol, &call.candidates, &arguments) { + return Fit::No; + } + let snapshot = self.vars.snapshot(); + if self.overload_fits(symbol, &arguments, &call.result) { + return Fit::Picked; + } + self.vars.rollback(snapshot); + return Fit::No; + } + let reachable = self.overloads().reachable_symbols(&call.candidates); + let fits = reachable.into_iter().any(|symbol| { + let snapshot = self.vars.snapshot(); + let fits = self.overload_fits(symbol, &arguments, &call.result); + self.vars.rollback(snapshot); + fits + }); + if fits { Fit::Maybe } else { Fit::No } + } + + /// Whether `symbol` takes a number among `arguments` through an `Any` + /// parameter where another of `candidates` in its scope takes a number + /// type or a type parameter, which makes `symbol` a fallback that a + /// literal doesn't take a type to reach: `[](List, Any)` takes the + /// Float index `0.0`, but `[](List, Int)` is what takes an index. + /// Where nothing else in that scope takes a number, as for `print`, the + /// `Any` says nothing about what the literal is. + fn takes_a_number_as_a_fallback( + &self, + symbol: SymbolId, + candidates: &Candidates, + arguments: &[StaticType], + ) -> bool { + let parameters_of = |symbol: SymbolId| { + self.overloads() + .parameters_of(symbol) + .filter(|parameters| parameters.len() == arguments.len()) + }; + let is_number = |typ: &StaticType| { + matches!( + typ, + StaticType::Int | StaticType::Float | StaticType::Number + ) + }; + let Some(parameters) = parameters_of(symbol) else { + return false; + }; + let fallbacks = parameters + .iter() + .zip(arguments) + .enumerate() + .filter(|(_, (parameter, argument))| { + **parameter == StaticType::Any && is_number(argument) + }) + .map(|(position, _)| position) + .collect::>(); + !fallbacks.is_empty() + && self + .overloads() + .same_scope_symbols(candidates, symbol) + .into_iter() + .filter(|&other| other != symbol) + .filter_map(parameters_of) + .any(|parameters| { + fallbacks.iter().any(|&position| { + is_number(¶meters[position]) + || matches!(parameters[position], StaticType::Generic(_)) + }) + }) + } + + /// Whether `symbol` takes `arguments` and returns what fits `result`, + /// binding the types on the way. One whose parameters aren't known + /// takes anything. + fn overload_fits( + &mut self, + symbol: SymbolId, + arguments: &[StaticType], + result: &StaticType, + ) -> bool { + let function = self.vars.resolve(self.types.get_type(symbol)); + let Some((parameters, return_type)) = self.vars.instantiate_signature(&function) else { + return true; + }; + self.vars.bind_arguments(¶meters, arguments) + && self.vars.bind_argument(result, &return_type) + } + + /// Tries the waiting constraint `serial` again, if it's still waiting. + /// A call can wait although its arguments are known: a variable's type + /// might have settled since, which nothing watches. + pub(super) fn retry(&mut self, serial: u64) { + let Some(mut waiting) = self.waiting.remove(&serial) else { + return; + }; + if !self.try_waiting(&mut waiting) { + self.wait_again(serial, waiting); + } + } + + /// The unsettled variables made since `first` that the waiting + /// constraint `serial` waits on, if it's still waiting. + fn waits_on_vars_since(&self, serial: u64, first: usize) -> Vec { + self.waiting.get(&serial).map_or_else(Vec::new, |waiting| { + waiting + .constraint + .inputs() + .iter() + .flat_map(|input| self.vars.unsettled(input)) + .filter(|var| var.0 as usize >= first) + .collect() + }) + } + + /// What nothing else settled in the waiting `a op= b` keeps the type it + /// already had, so it's taken from `b`: in `m["k"] += 3` on an empty + /// map, the values are Ints. Returns whether that settled anything. + pub(super) fn default_to_operand(&mut self, serial: u64) -> bool { + let Some(Waiting { + constraint: Constraint::Call(call), + .. + }) = self.waiting.get(&serial) + else { + return false; + }; + let (CallSite::OpAssignment(_), [target, operand]) = (call.site, &call.arguments[..]) + else { + return false; + }; + let (target, operand) = (target.clone(), operand.clone()); + let before = self.vars.settled_count(); + self.vars.try_unify(&target, &operand) && self.vars.settled_count() > before + } + + /// Solves `constraint` if what it waits on is known, and returns whether + /// it did. + fn try_constraint(&mut self, constraint: &mut Constraint) -> bool { + match constraint { + Constraint::Call(call) => { + let Some(typ) = self.try_call(call) else { + return false; + }; + let result = call.result.clone(); + match call.site { + // What `p.x = v` writes was settled by the value before + // the read said what the field holds, so it's the value + // that doesn't fit. The write fails the same way and + // isn't reported again. + CallSite::Getter(id) + if !self.accepts_write(&result, &typ, call.nodes.result) => + { + self.emit_rejected_write(&typ, &result, call.span); + self.failed_reads.insert(id); + } + CallSite::Getter(_) => {} + _ => { + self.check_write(&result, &typ, call.span, call.nodes.result); + } + } + true + } + Constraint::FunctionValue(value) => self.try_function_value(value), + Constraint::Iterate { + sequence, + element, + span, + } => { + let sequence = self.vars.resolve(sequence); + if self.vars.waits_to_iterate(&sequence) { + return false; + } + let (element, span) = (element.clone(), *span); + let found = self.iteration_gives(&sequence, span); + self.check_write(&element, &found, span, None); + true + } + Constraint::Unpack { value, parts, span } => { + let value = self.vars.resolve(value); + if matches!(value, StaticType::Var(_)) { + return false; + } + let found = match &value { + StaticType::Tuple(elements) if elements.len() != parts.len() => { + self.emit(AnalysisError::tuple_arity_mismatch( + parts.len(), + elements.len(), + *span, + )); + None + } + value => { + let found = value + .unpack() + .map(|found| found.take(parts.len()).cloned().collect::>()); + if found.is_none() { + self.emit(AnalysisError::unable_to_unpack_type(value, *span)); + } + found + } + }; + let found = found.unwrap_or_else(|| vec![StaticType::Error; parts.len()]); + for (part, found) in parts.iter().zip(found) { + self.check_write(part, &found, *span, None); + } + true + } + Constraint::MapDefault { + default, + value, + span, + } => { + let default = self.vars.resolve(default); + if matches!(default, StaticType::Var(_)) { + return false; + } + let value = value.clone(); + self.check_write(&value, &Self::default_value_type(default), *span, None); + true + } + Constraint::Join { + types: [a, b, result], + span, + } => { + let (a, b, result) = (a.clone(), b.clone(), result.clone()); + // A result known from outside is what's expected of each + // branch: in `let f: Fn(Int) -> Int = if c { abs } else + // { floor }`, it says which `abs` and `floor` are meant. + if !matches!(self.vars.resolve(&result), StaticType::Var(_)) { + for side in [&a, &b] { + if self.vars.resolve(side).has_vars() { + let snapshot = self.vars.snapshot(); + if !self.vars.bind_argument(&result, side) { + self.vars.rollback(snapshot); + } + } + } + } + let (a, b) = (self.vars.resolve(&a), self.vars.resolve(&b)); + let unknown = |typ: &StaticType| matches!(typ, StaticType::Var(var) if !self.vars.is_literal(*var)); + if unknown(&a) || unknown(&b) { + return false; + } + let joined = match (&a, &b) { + (StaticType::Never, other) | (other, StaticType::Never) => other.clone(), + _ => self.join_known(&a, &b), + }; + self.check_write(&result, &joined, *span, None); + true + } + } + } + + /// Picks the overload `call` means, records it, and returns the call's + /// type, or `None` when the arguments aren't known well enough yet. A + /// call whose arguments are known is resolved the usual way. Otherwise + /// the candidates that can't accept what's known are crossed off, and + /// when one is left, the call commits to it, which settles the arguments + /// from its parameters. + fn try_call(&mut self, call: &CallConstraint) -> Option { + let arguments = call + .arguments + .iter() + .map(|argument| self.vars.resolve(argument)) + .collect::>(); + if arguments.iter().any(StaticType::has_errors) { + return Some(self.skip_failed_call(call, &arguments)); + } + // A tuple indexed by a number written in the code gives that + // element, of its own type: `(1, "a")[1]` is a String, and + // `(1, "a")[-2]` an Int. + if let (Some(index), Some(StaticType::Tuple(elements))) = + (call.tuple_index, arguments.first()) + { + let length = i64::try_from(elements.len()).expect("a tuple's length fits an i64"); + let position = if index < 0 { length + index } else { index }; + let element = usize::try_from(position) + .ok() + .and_then(|position| elements.get(position)) + .cloned(); + let indexed = vec![arguments[0].clone(), StaticType::Int]; + if let [_, index_type] = &arguments[..] { + self.vars.bind_argument(&StaticType::Int, index_type); + } + self.resolve_call_now(call, &indexed); + let Some(element) = element else { + self.emit(AnalysisError::tuple_index_out_of_bounds( + index, + elements.len(), + call.span, + )); + return Some(StaticType::Error); + }; + return Some(element); + } + + let reachable = self.overloads().reachable_symbols(&call.candidates); + + // A generic function that calls itself with an argument made from + // the parameter it's passed for, as `nest([x], n - 1)` in `fn + // nest(x, n)`, would need a version of itself for `x`'s type, one + // for lists of it, one for lists of those, without end. + // Another overload may take the call, as `f(xs: List)` does + // `f([x])`, so it's only this function's when no other can. + if let Some(function) = self.declaring + && self.copied_through.is_none() + && self.recording.is_none() + && reachable.contains(&function) + && reachable + .iter() + .all(|&symbol| symbol == function || !self.could_accept(symbol, &arguments)) + && let StaticType::Function { + parameters: Some(parameters), + .. + } = self.vars.resolve(self.types.get_type(function)) + && parameters.len() == arguments.len() + && parameters + .iter() + .zip(&arguments) + .any(|(parameter, argument)| { + matches!(parameter, StaticType::Var(var) + if *argument != *parameter && self.vars.is_made_from(argument, *var)) + }) + { + let name = self.resolver.symbol_name(function).to_string(); + self.emit(AnalysisError::recursion_changes_types(&name, call.span)); + self.record_pick( + call.site, + Binding::Resolved(Candidate::Scalar(function)), + &call.arguments, + ); + return Some(StaticType::Error); + } + + if let [only] = reachable[..] { + match self.vars.resolve(self.types.get_type(only)) { + // A variable whose type isn't known yet, like the parameter + // `f` in `fn(f) => f(1)`, holds a function that takes these + // arguments. + callee @ StaticType::Var(var) if !self.vars.is_literal(var) => { + let typ = self.call_unknown_function(&callee, arguments); + self.record_pick( + call.site, + Binding::Resolved(Candidate::Scalar(only)), + &call.arguments, + ); + return Some(typ); + } + // A variable whose value's check failed, like `f` in `let f + // = missing(); f(1)`, was reported there. + StaticType::Error => return Some(self.skip_failed_call(call, &arguments)), + _ => {} + } + } + + // A variable holding a literal, like `n` in `let n = 1; n(2)`, can't + // be called, which is said best once it's settled. + if reachable.iter().any(|&symbol| { + matches!( + self.vars.resolve(self.types.get_type(symbol)), + StaticType::Var(var) if self.vars.is_literal(var) + ) + }) { + return None; + } + + // A write waits for the value it stores, which is analysed after the + // target: only then can it tell whether the value is what's wrong. + if let (CallSite::Setter(_) | CallSite::FieldSetter(_), Some(StaticType::Var(var))) = + (call.site, arguments.last()) + && !self.vars.is_literal(*var) + { + return None; + } + + if arguments.iter().all(|argument| !argument.has_vars()) { + return Some(self.resolve_call_now(call, &arguments)); + } + + match self.early_overload(&call.candidates, &arguments, call.kind) { + // A write that nothing takes may still fit once its target's + // type is known, which also says better what's wrong. + EarlyOverload::NoneFits + if matches!(call.site, CallSite::Setter(_) | CallSite::FieldSetter(_)) => + { + None + } + // A literal takes the type its uses need, or else its default, + // which says better what's wrong: `f(1)` with `f(String)` gets + // an Int. On tuples, an operator may apply to each element. + EarlyOverload::NoneFits + if arguments + .iter() + .any(|argument| self.vars.has_unsettled_literals(argument)) => + { + None + } + // An operator applies to each element of a tuple, which waits + // for the operands and elements nothing is known about yet: in + // `(xs[0], 1) - (1, 1)`, the first element is whatever `xs` + // holds, and in `p - (1, 1)`, `p` may be a tuple of anything. An + // element partly known, as `[]` is a list, is settled by the + // overload each position picks. + EarlyOverload::NoneFits + if call.kind == CallKind::Operator + && arguments.iter().any(|argument| match argument { + StaticType::Var(_) => true, + StaticType::Tuple(elements) => elements + .iter() + .any(|element| matches!(element, StaticType::Var(_))), + _ => false, + }) => + { + None + } + EarlyOverload::NoneFits => Some(self.resolve_call_now(call, &arguments)), + EarlyOverload::Only(only) => { + let Some(typ) = self.commit_early(call, &arguments, only) else { + self.report_failed_call(call, &arguments); + return Some(StaticType::Error); + }; + Some(typ) + } + EarlyOverload::Undecided => None, + } + } + + /// Skips a call that's passed a value whose check failed, or that calls + /// one, as that was reported where it failed: the call picks nothing, + /// settles what only it could have said about its arguments as failed + /// too, and gives a value whose check failed. In `missing().len()`, only + /// `missing` is reported. + fn skip_failed_call(&mut self, call: &CallConstraint, arguments: &[StaticType]) -> StaticType { + for argument in arguments { + self.vars.settle_unknowns_as_errors(argument); + } + self.record_pick(call.site, Binding::None, arguments); + StaticType::Error + } + + /// Whether an operator might still apply to each element of a tuple: an + /// operand that isn't known yet, or is `Any`, can be one. + fn operand_could_be_a_tuple(kind: CallKind, arguments: &[StaticType]) -> bool { + kind == CallKind::Operator + && arguments + .iter() + .any(|argument| matches!(argument, StaticType::Var(_) | StaticType::Any)) + } + + /// The overload a call of `candidates` with arguments of these types + /// commits to while the arguments aren't fully known: the only one that + /// can take them, once nothing learned later could make another one the + /// pick. One that takes them only through an `Any` parameter would lose + /// to an exact type once they're known, and an operator may also apply + /// to each element of a tuple, which an unknown or `Any` operand can be: + /// `fn(x) => x ++ "!"` can take `("a", "b")`. + /// Function values are picked by the same rule, as calls with the + /// expected parameter types. + pub(super) fn early_overload( + &mut self, + candidates: &Candidates, + arguments: &[StaticType], + kind: CallKind, + ) -> EarlyOverload { + let reachable = self.overloads().reachable_symbols(candidates); + let literals = arguments + .iter() + .flat_map(|argument| self.vars.unsettled(argument)) + .filter(|&var| self.vars.is_literal(var)) + .collect::>(); + // The nearest scope with an overload that takes the call hides those + // further out. When that scope has one overload, whose parameters + // take whatever the literals turn out to be, as a nested `fn d(x)` + // does, the call is its, though a literal isn't decided yet. + if !literals.is_empty() + && !Self::operand_could_be_a_tuple(kind, arguments) + && let Some(nearest) = self + .overloads() + .reachable_groups(candidates) + .into_iter() + .find(|group| { + group + .iter() + .any(|&symbol| self.could_accept(symbol, arguments)) + }) + && let [only] = nearest[..] + && self.takes_any_literal(only, arguments) + { + return EarlyOverload::Only(only); + } + let viable = reachable + .iter() + .copied() + .filter(|&symbol| self.could_accept(symbol, arguments)) + .collect::>(); + match viable[..] { + // A literal may still become `Any`, which an overload that can't + // take it now might take: `[1, "s"]` holds `Any`s, which + // `p(x: String)` may get. + [] if !literals.is_empty() && self.fits_once_any(&reachable, arguments, None) => { + EarlyOverload::Undecided + } + [] => EarlyOverload::NoneFits, + [only] + if self.fits_without_any(only, arguments) + && !Self::operand_could_be_a_tuple(kind, arguments) + && !self.merges_literals(only, arguments) => + { + // When the pick depends on what a literal is now, as + // `p(x)` over `p(x: Int)` does for the Strings in + // `["s"].map(p)`, the literal is what it is from here on: + // the first use that depends on it decides it. + // What they're decided as may still leave the pick open, + // as `Any` would, so it's made again with them settled. + if !literals.is_empty() && self.fits_once_any(&reachable, arguments, Some(only)) { + if !self.decide_literals(&literals, only, arguments) { + return EarlyOverload::Undecided; + } + let arguments = arguments + .iter() + .map(|argument| self.vars.resolve(argument)) + .collect::>(); + return self.early_overload(candidates, &arguments, kind); + } + EarlyOverload::Only(only) + } + _ => EarlyOverload::Undecided, + } + } + + /// Whether `symbol`'s parameter for each argument with a literal in it + /// takes any type, as an unannotated one does, and the other arguments + /// are known, so nothing still to come can change whether `symbol` takes + /// the call. + fn takes_any_literal(&self, symbol: SymbolId, arguments: &[StaticType]) -> bool { + let Some(parameters) = self.overloads().parameters_of(symbol) else { + return false; + }; + parameters.len() == arguments.len() + && parameters + .iter() + .zip(arguments) + .all(|(parameter, argument)| { + let unsettled = self.vars.unsettled(argument); + if unsettled.iter().all(|&var| !self.vars.is_literal(var)) { + return unsettled.is_empty(); + } + match self.vars.resolve(parameter) { + StaticType::Var(var) => !self.vars.is_literal(var), + StaticType::Generic(_) => true, + _ => false, + } + }) + } + + /// Whether one of `reachable` other than `except` takes `arguments` + /// once each literal among them is `Any`. + fn fits_once_any( + &mut self, + reachable: &[SymbolId], + arguments: &[StaticType], + except: Option, + ) -> bool { + let as_any = arguments + .iter() + .map(|argument| { + self.vars.resolve(argument).replace(&|part| match part { + StaticType::Var(var) if self.vars.is_literal(*var) => Some(StaticType::Any), + _ => None, + }) + }) + .collect::>(); + reachable + .iter() + .filter(|&&symbol| Some(symbol) != except) + .any(|&symbol| self.could_accept(symbol, &as_any)) + } + + /// Settles each of `literals` as the first of its defaults with which + /// `only` still takes `arguments`, and returns whether it could. + fn decide_literals( + &mut self, + literals: &[TypeVar], + only: SymbolId, + arguments: &[StaticType], + ) -> bool { + for &var in literals { + // A list of numbers is what the numbers are. + while let Some(held) = self.vars.held_literal(var) { + if !self.decide_literals(&[held], only, arguments) { + return false; + } + } + if !self.vars.is_literal(var) { + continue; + } + let Some(defaults) = self.vars.literal_defaults(var) else { + return false; + }; + let decided = defaults.iter().any(|default| { + let snapshot = self.vars.snapshot(); + let fits = + self.vars.settle_literal(var, default) && self.could_accept(only, arguments); + if !fits { + self.vars.rollback(snapshot); + } + fits + }); + if !decided { + return false; + } + } + true + } + + /// Whether passing these arguments to `symbol` would make the types of + /// two literals one, as `++(List, List)` does with `[1]` and + /// `["a"]`. Each literal first gets its own type, which says better + /// whether they're the same. + fn merges_literals(&mut self, symbol: SymbolId, arguments: &[StaticType]) -> bool { + // A literal in two arguments, as the element of `[0]` is in + // `[0].flat_map(fn(x) => …)` once `x` has its type, is one literal. + let literals = arguments + .iter() + .flat_map(|argument| self.vars.unsettled(argument)) + .filter(|&var| self.vars.is_literal(var)) + .unique() + .collect::>(); + if literals.len() < 2 { + return false; + } + let function = self.vars.resolve(self.types.get_type(symbol)); + let snapshot = self.vars.snapshot(); + let Some((parameters, _)) = self.vars.instantiate_signature(&function) else { + return false; + }; + for (parameter, argument) in parameters.iter().zip(arguments) { + self.vars.bind_argument(parameter, argument); + } + let mut became = Vec::new(); + let merged = literals.iter().any(|&var| { + let StaticType::Var(now) = self.vars.resolve(&StaticType::Var(var)) else { + return false; + }; + let merged = became.contains(&now); + became.push(now); + merged + }); + self.vars.rollback(snapshot); + merged + } + + /// Whether every argument of type `Any` has a node in `nodes` to be checked + /// at where it's passed. + fn checks_every_any(arguments: &[StaticType], nodes: &[Option]) -> bool { + arguments + .iter() + .zip(nodes) + .all(|(argument, node)| node.is_some() || *argument != StaticType::Any) + } + + /// [`Overloads::resolve_call`] over the types as they're known now. + pub(super) fn resolve_overloads( + &mut self, + candidates: &Candidates, + arguments: &[StaticType], + kind: CallKind, + ) -> ResolvedCall { + self.overloads().resolve_call(candidates, arguments, kind) + } + + /// Resolves `call` with the arguments as they are, reporting it when no + /// overload accepts them. + fn resolve_call_now(&mut self, call: &CallConstraint, arguments: &[StaticType]) -> StaticType { + let ResolvedCall { + binding, + return_type, + } = self.resolve_overloads(&call.candidates, arguments, call.kind); + // A pick left to the program among one function with known + // parameters is a call of it, whose `Any` arguments are checked where + // they're passed. A variable of type `Any` may hold any function, and + // an `Any` without a node to check it at, as the target of `a op= b`, + // isn't checked where it's passed, so the program checks those itself. + let binding = match binding { + Binding::Dynamic(only) + if let [Candidate::Scalar(only)] = only[..] + && Self::checks_every_any(arguments, &call.nodes.arguments) + && let StaticType::Function { + parameters: Some(_), + .. + } = self.vars.resolve(self.types.get_type(only)) => + { + Binding::Resolved(Candidate::Scalar(only)) + } + binding => binding, + }; + // `a op= b` with no operator that takes the operands calls an in-place + // overload if one does, as a set has `-=` but no `-`, which keeps `a`. + if matches!(binding, Binding::None) + && let CallSite::OpAssignment(id) = call.site + { + let in_place = self.resolver.augmented_candidates(id).in_place.clone(); + let in_place = self + .resolve_overloads(&in_place, arguments, call.kind) + .binding; + if !matches!(in_place, Binding::None) { + self.record_augmented_pick(id, in_place); + return arguments[0].clone(); + } + } + if matches!(binding, Binding::None) { + self.report_failed_call(call, arguments); + self.record_pick(call.site, binding, arguments); + return StaticType::Error; + } + let typ = self.commit_call(&binding, arguments, &call.nodes.arguments, return_type); + self.record_pick(call.site, binding, arguments); + if let Some(typ) = typ { + self.flush_copies(Some(call.site), call.span); + typ + } else { + self.pending.clear(); + self.report_failed_call(call, arguments); + StaticType::Error + } + } + + /// Settles the types a call's arguments leave unknown from the overload + /// it picked, and returns the call's type. The picked signature gets new + /// variables for its type parameters, and each argument is passed where + /// its parameter is expected: `push(xs, 1)` with `push(List, T)` + /// settles `xs` as a `List`, and `push(xs, "a")` then doesn't fit. + /// When the pick is left to the program as it runs, any function may get + /// the arguments, so what's unknown in them becomes `Any`. Returns `None` + /// when an argument doesn't fit the picked overload after all, which + /// settles nothing. An argument of type `Any` is checked at its node in + /// `nodes` as the program runs, once the call commits. + fn commit_call( + &mut self, + binding: &Binding, + arguments: &[StaticType], + nodes: &[Option], + return_type: StaticType, + ) -> Option { + let picked = match binding { + Binding::Resolved(Candidate::Scalar(symbol)) => Some(*symbol), + // An operator on tuples applies one overload to each position, + // with a scalar operand used at every position. + Binding::Resolved(Candidate::Vec(symbol)) => { + let Some(length) = arguments.iter().find_map(|argument| match argument { + StaticType::Tuple(elements) => Some(elements.len()), + _ => None, + }) else { + return Some(return_type); + }; + let scalar = Binding::Resolved(Candidate::Scalar(*symbol)); + let mut returns = Vec::with_capacity(length); + for position in 0..length { + let arguments = arguments + .iter() + .map(|argument| match argument { + StaticType::Tuple(elements) => elements[position].clone(), + scalar => scalar.clone(), + }) + .collect::>(); + returns.push(self.commit_call(&scalar, &arguments, &[], StaticType::Any)?); + } + return Some(StaticType::Tuple(returns)); + } + Binding::Dynamic(_) | Binding::None => None, + }; + let pending = self.pending.len(); + let signature = picked.map(|symbol| self.instantiate_function(symbol)); + let Some( + signature @ StaticType::Function { + parameters: Some(_), + .. + }, + ) = signature + else { + for argument in arguments { + self.vars.settle_unknowns(argument, &StaticType::Any); + } + return Some(return_type); + }; + let Some((typ, parameters)) = self.bind_signature(&signature, arguments) else { + self.pending.truncate(pending); + return None; + }; + let checks = self.expected_of_any(¶meters, arguments, nodes); + self.pending.checks.extend(checks); + let iterations = self.iterations_waited_for(¶meters, arguments); + self.pending.iterations.extend(iterations); + Some(typ) + } + + /// `symbol`'s type with new variables for its type parameters. For a + /// generic function declared in the program, its constraints get the + /// same variables, and wait in [`Self::pending`] until + /// [`Self::flush_copies`] knows which call they're for. + pub(super) fn instantiate_function(&mut self, symbol: SymbolId) -> StaticType { + let declared = self.vars.resolve(self.types.get_type(symbol)); + let mut made = Vec::new(); + let typ = self.vars.instantiate_with(&declared, &mut made); + // A copy of a function's own constraint that calls the function + // again, as a recursive call its body couldn't decide does, is + // checked by the copies the outer call already made: recursion + // doesn't change the types. + let recursive = self + .copied_through + .iter() + .flat_map(|frames| frames.iter()) + .any(|frame| frame.function == symbol); + let Some(scheme) = self.schemes.get(&symbol).cloned() else { + return typ; + }; + if !recursive { + for waiting in scheme.constraints.iter() { + let constraint = waiting + .constraint + .map_types(&mut |typ| self.vars.instantiate_with(typ, &mut made)); + self.pending + .copies + .push((symbol, constraint, waiting.origin.clone())); + } + } + if !scheme.key.is_empty() { + let key = scheme + .key + .iter() + .map(|name| { + self.vars + .instantiate_with(&StaticType::Generic(name.clone()), &mut made) + }) + .collect(); + self.pending.uses.push((symbol, key)); + } + typ + } + + /// Adds the constraints [`Self::instantiate_function`] copied for a call + /// at `call`, and solves the ones it can. A call at `site` gets the + /// instance its types need; a function's name used as a value, with no + /// `site`, stays the function itself, so two values of it are equal. + pub(super) fn flush_copies(&mut self, site: Option, call: Span) { + match site { + Some(site) => self.record_uses(site, call), + None => self.pending.uses.clear(), + } + for (node, expected) in std::mem::take(&mut self.pending.checks) { + self.expect(node, expected); + } + for (sequence, element) in std::mem::take(&mut self.pending.iterations) { + self.wait(Constraint::Iterate { + sequence, + element, + span: call, + }); + } + let copies = std::mem::take(&mut self.pending.copies); + for (function, constraint, origin) in copies { + let name = self.resolver.symbol_name(function).to_string(); + let frames = self + .copied_through + .iter() + .flat_map(|outer| outer.iter().cloned()) + .chain(std::iter::once(Frame { + function, + name, + call, + })) + .chain(origin.iter().flat_map(|inner| inner.iter().cloned())) + .collect::>(); + let mut waiting = Waiting { + constraint, + origin: Some(frames.into()), + }; + if !self.try_waiting(&mut waiting) { + self.wait_as(waiting); + } + } + } + + /// Passes each argument where `signature` expects its parameter, which + /// settles what's unknown in either, and returns what the signature + /// returns, and its parameters. Returns `None` and settles nothing when + /// an argument doesn't fit, or the number of arguments is wrong. + pub(super) fn bind_signature( + &mut self, + signature: &StaticType, + arguments: &[StaticType], + ) -> Option<(StaticType, Vec)> { + let StaticType::Function { + parameters: Some(parameters), + return_type, + } = signature + else { + unreachable!("only a function with known parameters has a signature to bind"); + }; + let snapshot = self.vars.snapshot(); + if !self.vars.bind_arguments(parameters, arguments) { + self.vars.rollback(snapshot); + return None; + } + Some((self.vars.resolve(return_type), parameters.clone())) + } + + /// Each argument nothing is known about yet that was passed for an + /// `Iterable`, or that a function passed for one returns, with the + /// `T` that iterating over it gives once it's known: in `fn total(xs) + /// => xs.sum()`, what `xs` is waits for a call, and so does `xs` in + /// `fn flatten(xs) => [0].flat_map(fn(_) => xs)`. + pub(super) fn iterations_waited_for( + &self, + parameters: &[StaticType], + arguments: &[StaticType], + ) -> Vec<(StaticType, StaticType)> { + parameters + .iter() + .zip(arguments) + .filter_map(|(parameter, argument)| { + let (element, sequence) = + match (self.vars.resolve(parameter), self.vars.resolve(argument)) { + (StaticType::Iterable(element), sequence) => (*element, sequence), + ( + StaticType::Function { return_type, .. }, + StaticType::Function { + return_type: returns, + .. + }, + ) => match *return_type { + StaticType::Iterable(element) => (*element, *returns), + _ => return None, + }, + _ => return None, + }; + match sequence { + // A literal whose type isn't known yet either, as the + // element of `[1][0]` is, waits like any other value. + StaticType::Var(_) => Some((sequence, element)), + _ => None, + } + }) + .collect() + } + + /// Where each argument of type `Any` that `nodes` has a node for is + /// checked against the parameter it was passed for. + pub(super) fn expected_of_any( + &self, + parameters: &[StaticType], + arguments: &[StaticType], + nodes: &[Option], + ) -> Vec<(NodeId, StaticType)> { + parameters + .iter() + .zip(arguments) + .zip(nodes) + .filter_map(|((parameter, argument), node)| { + let node = (*node)?; + (self.vars.resolve(argument) == StaticType::Any).then(|| (node, parameter.clone())) + }) + .collect() + } + + /// The type of calling a value whose type isn't known yet, like `g` in + /// `fn(g) => g(1)`: the value is a function taking these arguments. A + /// value that can't be one, like `g` in `g(g)`, which would have to take + /// itself, stays unknown, and the call is left to the program as it runs. + pub(super) fn call_unknown_function( + &mut self, + callee: &StaticType, + arguments: Vec, + ) -> StaticType { + let returns = self.vars.fresh(StaticType::Any); + let function = StaticType::Function { + parameters: Some(arguments), + return_type: Box::new(returns.clone()), + }; + if self.vars.try_unify(callee, &function) { + self.vars.resolve(&returns) + } else { + StaticType::Any + } + } + + /// Whether some overload of `call` accepts arguments of these types. + fn call_fits(&mut self, call: &CallConstraint, arguments: &[StaticType]) -> bool { + let snapshot = self.vars.snapshot(); + let ResolvedCall { + binding, + return_type, + } = self.resolve_overloads(&call.candidates, arguments, call.kind); + let fits = !matches!(binding, Binding::None) + && self + .commit_call(&binding, arguments, &call.nodes.arguments, return_type) + .is_some(); + self.vars.rollback(snapshot); + self.pending.clear(); + fits + } + + /// Commits the waiting call `serial` to the narrowest overload that + /// fits it, once nothing else can settle its arguments, and returns + /// whether it did. That's the overload the call would pick if the + /// arguments' unknown parts were whatever that overload needs: in + /// `m["k"]` on an empty map, `[](Map, K)` is narrower than + /// `[](Any, Any)`, so the map's keys are Strings. It only commits when + /// every overload that fits is in the same scope, as a nearer one would + /// otherwise win, and when each unknown is inside an argument whose kind + /// is known: an argument nothing at all is known about is `Any`, which + /// the overloads taking `Any` take. + pub(super) fn default_to_narrowest(&mut self, serial: u64) -> bool { + let Some(Waiting { + constraint: Constraint::Call(call), + .. + }) = self.waiting.get(&serial) + else { + return false; + }; + let arguments = call + .arguments + .iter() + .map(|argument| self.vars.resolve(argument)) + .collect::>(); + if arguments + .iter() + .any(|argument| matches!(argument, StaticType::Var(_))) + || Self::operand_could_be_a_tuple(call.kind, &arguments) + { + return false; + } + let groups = self.overloads().reachable_groups(&call.candidates); + let mut fitting = Vec::new(); + for group in groups { + let fits = group + .into_iter() + .filter(|&symbol| self.could_accept(symbol, &arguments)) + .collect::>(); + if !fits.is_empty() { + fitting.push(fits); + } + } + let [group] = &fitting[..] else { + return false; + }; + let Some(narrowest) = self + .most_specific(group) + .filter(|&narrowest| self.fits_without_any(narrowest, &arguments)) + else { + return false; + }; + let Some(Waiting { + constraint: Constraint::Call(call), + origin, + }) = self.waiting.remove(&serial) + else { + unreachable!("the call was waiting a moment ago"); + }; + let outer = std::mem::replace(&mut self.copied_through, origin.clone()); + let committed = self.commit_early(&call, &arguments, narrowest); + if let Some(typ) = &committed { + let result = call.result.clone(); + self.check_write(&result, typ, call.span, call.nodes.result); + } + self.copied_through = outer; + if committed.is_none() { + self.wait_again( + serial, + Waiting { + constraint: Constraint::Call(call), + origin, + }, + ); + return false; + } + true + } + + /// The one of `symbols` whose parameters are narrower than every other + /// one's, as `[](Map, K)` is than `[](Any, Any)`, if there is one. + /// A single function is the narrowest of one. + fn most_specific(&self, symbols: &[SymbolId]) -> Option { + let parameters = symbols + .iter() + .map(|&symbol| self.overloads().parameters_of(symbol)) + .collect::>>()?; + let (narrowest, _) = parameters.iter().enumerate().find(|&(i, narrower)| { + parameters + .iter() + .enumerate() + .all(|(j, wider)| i == j || is_strictly_more_specific(narrower, wider)) + })?; + Some(symbols[narrowest]) + } + + /// Whether `symbol` is a function whose parameters take these arguments + /// as the types they are. An `Any` parameter standing in for an argument + /// of another type is a fallback that only wins over the overloads + /// taking exact types once the arguments are known, so a waiting call + /// doesn't commit to it: `xs[0] = []` on a `List` leaves only + /// `[]=(List, Any, Any)` taking the empty list, but it's still an + /// error once the list's type is known. An `Iterable` parameter takes + /// any kind of sequence, so it can't tell what an argument nothing is + /// known about is, and the call waits for it as `for x in xs` does. + fn fits_without_any(&self, symbol: SymbolId, arguments: &[StaticType]) -> bool { + self.overloads() + .parameters_of(symbol) + .is_some_and(|parameters| { + parameters + .iter() + .zip(arguments) + .all(|(parameter, argument)| match (parameter, argument) { + (StaticType::Any, argument) => *argument == StaticType::Any, + (StaticType::Iterable(_), argument) => { + !self.vars.waits_to_iterate(argument) + } + _ => true, + }) + }) + } + + /// Whether `symbol` can accept arguments of these types, where each part + /// not known yet fits anything. + fn could_accept(&mut self, symbol: SymbolId, arguments: &[StaticType]) -> bool { + let function = self.vars.resolve(self.types.get_type(symbol)); + let snapshot = self.vars.snapshot(); + let Some((parameters, _)) = self.vars.instantiate_signature(&function) else { + return matches!(function, StaticType::Function { .. }) || function.is_unknown(); + }; + let fits = self.vars.bind_arguments(¶meters, arguments); + self.vars.rollback(snapshot); + fits + } + + fn site_candidates(&self, site: CallSite) -> &Candidates { + match site { + CallSite::Call(id) => self.resolver.reference_candidates(id), + CallSite::Getter(id) => self + .resolver + .getter_candidates(id) + .expect("internal error: a getter call without getter candidates"), + CallSite::Setter(id) | CallSite::FieldSetter(id) => self.resolver.setter_candidates(id), + CallSite::OpAssignment(id) => &self.resolver.augmented_candidates(id).operator, + } + } + + pub(super) fn record_pick( + &mut self, + site: CallSite, + binding: Binding, + arguments: &[StaticType], + ) { + if self.copied_through.is_some() { + return; + } + // A generic function's body calls itself with its own types, so an + // instance's recursive call is a call of the instance. + if let (CallSite::Call(id), Binding::Resolved(Candidate::Scalar(callee)), None) = + (site, &binding, self.recording) + && self.declaring == Some(*callee) + { + self.recursive_calls.push(id); + } + match site { + CallSite::Call(_) + | CallSite::Getter(_) + | CallSite::Setter(_) + | CallSite::FieldSetter(_) => { + self.pick_tables(self.recording).insert(site, binding); + } + // An in-place overload such as `++=` does the same thing faster, + // so it's used where one takes the operands as they are. + CallSite::OpAssignment(id) => { + let arguments = arguments + .iter() + .map(|argument| self.vars.resolve(argument)) + .collect::>(); + let in_place = self.overloads().resolve_call( + &self.resolver.augmented_candidates(id).in_place, + &arguments, + CallKind::Operator, + ); + let compatible = Self::augmented_rhs_is_compatible(&arguments[0], &arguments[1]); + let binding = match (in_place.binding, binding) { + (Binding::Resolved(candidate), _) => Binding::Resolved(candidate), + // Which one applies is left to the program as it runs, + // which mutates in place where it can. + (Binding::Dynamic(in_place), operators) if compatible => { + Self::in_place_then_operators(in_place, operators) + } + (_, binding) => binding, + }; + self.record_augmented_pick(id, binding); + } + }; + } + + /// Records what `a op= b` at `id` does, as [`Self::record_pick`]. + pub(super) fn record_augmented_pick(&mut self, id: NodeId, binding: Binding) { + if self.copied_through.is_none() { + self.pick_tables(self.recording) + .insert(CallSite::OpAssignment(id), binding); + } + } + + /// Reports that no overload accepts `call`. Writing `xs[i] += 1` reads + /// and writes `xs[i]`, and when the read fails, the write fails the same + /// way, which is reported once. + fn report_failed_call(&mut self, call: &CallConstraint, arguments: &[StaticType]) { + let arguments = &arguments + .iter() + .map(|argument| self.vars.shown(argument)) + .collect::>(); + let error = match call.site { + CallSite::Call(_) | CallSite::Getter(_) | CallSite::OpAssignment(_) => { + if let CallSite::Getter(id) = call.site { + self.failed_reads.insert(id); + } + self.no_match(&call.candidates, &call.name, arguments, call.span) + } + CallSite::Setter(id) | CallSite::FieldSetter(id) if self.failed_reads.contains(&id) => { + return; + } + CallSite::FieldSetter(_) => { + AnalysisError::function_not_found(&call.name, arguments, call.span) + } + CallSite::Setter(_) => match arguments.as_slice() { + [container, ..] + if container.index_element_type().is_none() && !container.is_unknown() => + { + AnalysisError::unable_to_index_into(container, call.span) + } + // The container and index fit, but the value doesn't fit + // what the container holds there: an element, or a list of + // them for a slice like `xs[0..2]`. + [container, index, value] + if let Some(element) = container.index_element_type() + && let expected = if Self::index_type_is_slice(index) { + StaticType::List(Box::new(element)) + } else { + element + } + && self.call_fits( + call, + &[container.clone(), index.clone(), expected.clone()], + ) => + { + AnalysisError::mismatched_types(value, &expected, call.span) + } + _ => AnalysisError::function_not_found(&call.name, arguments, call.span), + }, + }; + self.emit(error); + } +} diff --git a/ndc_analyser/src/analyser/tests.rs b/ndc_analyser/src/analyser/tests.rs index 1f681389..f7d95e92 100644 --- a/ndc_analyser/src/analyser/tests.rs +++ b/ndc_analyser/src/analyser/tests.rs @@ -1,5 +1,6 @@ use super::*; use itertools::Itertools; +use ndc_analysis::{Binding, Candidate}; use ndc_lexer::{Lexer, SourceId}; use ndc_parser::Parser; @@ -12,11 +13,15 @@ fn analyse_with_globals( .expect("lex failed"); let expressions = Parser::from_tokens(tokens).parse().expect("parse failed"); let mut analyser = Analyser::new(globals); - let mut last_type = StaticType::unit(); for expression in &expressions { - last_type = analyser.analyse(expression); + analyser.analyse(expression); } - (last_type, analyser.take_result()) + let result = analyser.take_result(); + // The last statement's type as the whole program settled it. + let last_type = expressions.last().map_or_else(StaticType::unit, |last| { + result.analysis.expr_types[&last.id].clone() + }); + (last_type, result) } fn analyse_last_type_with_globals(source: &str, globals: Vec<(String, StaticType)>) -> StaticType { @@ -36,14 +41,22 @@ fn analyse_last_type(source: &str) -> StaticType { /// `[]` and `[]=`, which index reads and writes go through. Name resolution /// rejects index access without them, as it would any other unknown function. fn index_natives() -> Vec<(String, StaticType)> { - let any_function = StaticType::Function { - parameters: None, - return_type: Box::new(StaticType::Any), - }; - vec![ - ("[]".to_string(), any_function.clone()), - ("[]=".to_string(), any_function), + // The stdlib's list and map overloads. + [ + ("[]", "(List, Int) -> T"), + ("[]", "(List, Iterator) -> List"), + ("[]", "(Map, K) -> V"), + ("[]=", "(List, Int, T)"), + ("[]=", "(List, Iterator, Iterable)"), + ("[]=", "(Map, K, V)"), + ("[]=", "(String, Any, String)"), ] + .into_iter() + .map(|(name, signature)| { + let typ = StaticType::signature(signature).expect("valid signature"); + (name.to_string(), typ) + }) + .collect() } /// Every error analysing `source` reports, in order. @@ -122,12 +135,6 @@ fn break_and_continue_need_a_loop_in_their_own_function() { } } -#[test] -fn destructuring_assignment_widens_each_target() { - let typ = analyse_last_type("let a = 1; let b = 2; (a, b) = (\"s\", 2); a"); - assert_eq!(typ, StaticType::Int.lub(&StaticType::String)); -} - #[test] fn break_and_continue_inside_a_loop_are_accepted() { for source in [ @@ -165,6 +172,7 @@ fn a_function_returns_the_join_of_its_body_and_every_return() { }", vec![], ); + // Tuples of different types have no type in common but `Any`. assert!(result.errors.is_empty(), "errors: {:?}", result.errors); assert_eq!( result @@ -173,72 +181,7 @@ fn a_function_returns_the_join_of_its_body_and_every_return() { .values() .exactly_one() .ok(), - Some(&StaticType::Tuple(vec![StaticType::Any; 3])), - ); -} - -#[test] -fn inferred_list_index_assignment_widens_element_type() { - assert_eq!( - analyse_last_type_with_globals( - "let values = [1]; values[0] = \"two\"; values", - index_natives() - ), - StaticType::List(Box::new(StaticType::Any)), - ); -} - -#[test] -fn inferred_list_slice_assignment_widens_element_type() { - assert_eq!( - analyse_last_type_with_globals( - "let values = [1]; values[0..1] = [\"two\"]; values", - index_natives() - ), - StaticType::List(Box::new(StaticType::Any)), - ); -} - -#[test] -fn inferred_map_index_assignment_widens_value_and_preserves_key_type() { - assert_eq!( - analyse_last_type_with_globals( - "let values = %{\"one\": 1}; values[\"two\"] = \"two\"; values", - index_natives() - ), - StaticType::Map { - key: Box::new(StaticType::String), - value: Box::new(StaticType::Any), - }, - ); -} - -#[test] -fn inferred_index_augmented_assignment_widens_element_type() { - let add = StaticType::Function { - parameters: Some(vec![StaticType::Int, StaticType::Float]), - return_type: Box::new(StaticType::Number), - }; - assert_eq!( - analyse_last_type_with_globals( - "let values = [1]; values[0] += 0.5; values", - [index_natives(), vec![("+".to_string(), add)]].concat(), - ), - StaticType::List(Box::new(StaticType::Any)), - ); -} - -#[test] -fn inferred_identifier_assignments_widen_subsequent_reads() { - assert_eq!(analyse_last_type("let x = 3; x = 0.5; x"), StaticType::Any,); - - let add = StaticType::Function { - parameters: Some(vec![StaticType::Int, StaticType::Float]), - return_type: Box::new(StaticType::Float), - }; - assert_eq!( - analyse_last_type_with_globals("let x = 3; x += 0.5; x", vec![("+".to_string(), add)],), - StaticType::Any, + Some(&StaticType::Any), ); } @@ -257,11 +200,8 @@ fn annotated_identifier_augmented_assignment_rejects_widening() { #[test] fn an_in_place_operator_assignment_keeps_the_target_type() { - let list_any = StaticType::List(Box::new(StaticType::Any)); - let append = StaticType::Function { - parameters: Some(vec![list_any.clone(), list_any.clone()]), - return_type: Box::new(list_any), - }; + let append = + StaticType::signature("(List, List) -> List").expect("valid signature"); assert_eq!( analyse_last_type_with_globals( @@ -274,11 +214,8 @@ fn an_in_place_operator_assignment_keeps_the_target_type() { #[test] fn an_in_place_operator_assignment_cannot_change_the_target_type() { - let list_any = StaticType::List(Box::new(StaticType::Any)); - let concat = StaticType::Function { - parameters: Some(vec![list_any.clone(), list_any.clone()]), - return_type: Box::new(list_any), - }; + let concat = + StaticType::signature("(List, List) -> List").expect("valid signature"); assert_analysis_error( "let values = [1]; values ++= [\"two\"];", @@ -286,7 +223,7 @@ fn an_in_place_operator_assignment_cannot_change_the_target_type() { ("++=".to_string(), concat.clone()), ("++".to_string(), concat.clone()), ], - "mismatched types: found List but expected List", + "No function called '++' found that matches the arguments 'List, List'", ); assert_analysis_error( "let values: List = [1]; values ++= [\"two\"];", @@ -294,7 +231,7 @@ fn an_in_place_operator_assignment_cannot_change_the_target_type() { ("++=".to_string(), concat.clone()), ("++".to_string(), concat), ], - "mismatched types: found List but expected List", + "No function called '++' found that matches the arguments 'List, List'", ); } @@ -329,7 +266,7 @@ fn annotated_declaration_rejects_a_mismatched_container_literal() { // `f` might hold any function, so which one runs is only known at runtime. #[test] fn a_call_of_a_value_of_unknown_type_dispatches_at_runtime() { - let (_, result) = analyse_with_globals("fn call(f) { f(\"x\") }", vec![]); + let (_, result) = analyse_with_globals("fn call(f: Any) { f(\"x\") }", vec![]); assert!(result.errors.is_empty(), "errors: {:?}", result.errors); let callee = result .analysis @@ -475,66 +412,1416 @@ fn same_arity_overloads_with_different_parameter_types_are_allowed() { ); } -/// `len`, a native taking a `String`, for the test below. -fn len_native() -> Vec<(String, StaticType)> { - vec![( - "len".to_string(), +#[test] +fn a_variable_keeps_its_inferred_type() { + let add = StaticType::Function { + parameters: Some(vec![StaticType::Int, StaticType::Float]), + return_type: Box::new(StaticType::Float), + }; + // `one()` is an Int that isn't a literal, which would take the type + // its uses need. + let globals = [ + index_natives(), + vec![("+".to_string(), add)], + natives(&[("one", "() -> Int")]), + ] + .concat(); + let scalar = "mismatched types: found Float but expected Int"; + let list = "mismatched types: found List but expected List"; + for (source, expected) in [ + ("let x = one(); x = 0.5;", scalar), + ("let x = one(); x += 0.5;", scalar), + ( + "let a = one(); let b = one(); (a, b) = (0.5, one());", + scalar, + ), + ("for x in [one()] { x = 0.5; }", scalar), + ("let values = [one()]; values[0] = 0.5;", scalar), + ("let values = [one()]; values[0..1] = [0.5];", list), + ("let values = [one()]; values[0] += 0.5;", scalar), + ( + "let values = %{\"one\": one()}; values[\"two\"] = 0.5;", + scalar, + ), + ] { + assert_eq!(errors(source, globals.clone()), vec![expected], "{source}"); + } +} + +#[test] +fn an_any_value_can_be_stored_anywhere() { + let any_value = vec![( + "anything".to_string(), StaticType::Function { - parameters: Some(vec![StaticType::String]), - return_type: Box::new(StaticType::Int), + parameters: Some(vec![]), + return_type: Box::new(StaticType::Any), }, - )] + )]; + assert_eq!( + analyse_last_type_with_globals("let x = 3; x = anything(); x", any_value), + StaticType::Int + ); } #[test] -fn a_call_uses_a_variable_standing_nearer_than_a_function() { +fn the_first_write_settles_what_an_empty_literal_holds() { + let list_of = |element| StaticType::List(Box::new(element)); assert_eq!( - errors("fn foo(x) { 1 } fn test(foo) { foo(2) }", vec![]), - Vec::::new(), + analyse_last_type_with_globals("let xs = []; xs[0] = 1; xs", index_natives()), + list_of(StaticType::Int) ); assert_eq!( - errors("fn test(len) { len(\"abc\") }", len_native()), - Vec::::new(), + analyse_last_type_with_globals("let xs = []; xs[0] = [1.5]; xs", index_natives()), + list_of(list_of(StaticType::Float)) + ); + assert_eq!( + analyse_last_type_with_globals("let m = %{}; m[\"a\"] = 1; m", index_natives()), + StaticType::Map { + key: Box::new(StaticType::String), + value: Box::new(StaticType::Int), + } + ); + // Reads before the write see the settled type too, once the batch ends. + assert_eq!( + analyse_last_type_with_globals("let xs = []; let ys = xs; xs[0] = 1; ys", index_natives()), + list_of(StaticType::Int) ); } #[test] -fn a_call_of_a_variable_that_cant_be_called_does_not_reach_the_function() { +fn an_annotation_settles_what_its_value_holds() { + let option_of = |inner| StaticType::Option(Box::new(inner)); assert_eq!( - errors("fn test(len: Int) { len(\"abc\") }", len_native()), - vec!["The variable 'len' is Int, which can't be called"], + analyse_last_type("let o: Option = None; o"), + option_of(StaticType::Int) + ); + assert_eq!( + analyse_last_type("let p: (Int, Option) = (1, None); p"), + StaticType::Tuple(vec![StaticType::Int, option_of(StaticType::Int)]) ); } -/// `+`, which augmented assignment needs, for the tests below. -fn plus_native() -> Vec<(String, StaticType)> { - vec![( - "+".to_string(), +/// Natives with these signatures, as a stdlib would declare them. +fn natives(signatures: &[(&str, &str)]) -> Vec<(String, StaticType)> { + signatures + .iter() + .map(|(name, signature)| { + let typ = StaticType::signature(signature).expect("valid signature"); + ((*name).to_string(), typ) + }) + .collect() +} + +#[test] +fn a_call_settles_its_arguments_through_the_picked_signature() { + let globals = natives(&[("push", "(List, T)")]); + assert_eq!( + analyse_last_type_with_globals("let xs = []; push(xs, 1); xs", globals.clone()), + StaticType::List(Box::new(StaticType::Int)) + ); + assert_eq!( + errors("let xs = [1]; push(xs, \"a\");", globals), + vec!["No function called 'push' found that matches the arguments 'List, String'"] + ); +} + +#[test] +fn each_call_instantiates_the_signature_afresh() { + let globals = natives(&[("first", "(List) -> T")]); + assert_eq!( + analyse_last_type_with_globals("first([1.5])", globals.clone()), + StaticType::Float + ); + assert_eq!( + analyse_last_type_with_globals("first([1]); first([\"a\"])", globals), + StaticType::String + ); +} + +#[test] +fn a_call_picked_at_runtime_has_no_type_parameters_in_its_type() { + // `xs` could be anything, so `reversed` is picked when the program runs + // and its `T` is never known. + let globals = natives(&[("reversed", "(List) -> List")]); + assert_eq!( + analyse_last_type_with_globals("fn f(xs: Any) { reversed(xs) } f", globals), StaticType::Function { - parameters: Some(vec![StaticType::Int, StaticType::Int]), - return_type: Box::new(StaticType::Int), - }, - )] + parameters: Some(vec![StaticType::Any]), + return_type: Box::new(StaticType::List(Box::new(StaticType::Any))), + } + ); } #[test] -fn field_reads_and_writes_skip_a_value_named_like_the_field() { - for source in [ - "struct P { x: Int } fn f(p, x) { p.x }", - "struct P { x: Int } fn f(p, x) { p.x += 1 }", +fn a_generic_built_in_used_as_a_value_has_one_type() { + let globals = natives(&[("push", "(List, T)")]); + assert_eq!( + analyse_last_type_with_globals("let p = push; let xs = [1]; p(xs, 2); p", globals), + StaticType::Function { + parameters: Some(vec![ + StaticType::List(Box::new(StaticType::Int)), + StaticType::Int + ]), + return_type: Box::new(StaticType::unit()), + } + ); +} + +#[test] +fn calling_a_function_value_checks_its_arguments() { + assert_eq!( + errors( + "let f = fn(x: Int) { x }; let g = [f]; g[0](\"a\");", + index_natives() + ), + vec!["A function of type Fn(Int) -> Int doesn't accept the arguments 'String'"] + ); +} + +#[test] +fn an_index_write_checks_the_index_against_the_container() { + let plus = natives(&[("+", "(Int, Int) -> Int")]); + let globals = [index_natives(), plus].concat(); + for (source, expected) in [ + ( + "let m = %{1: 2}; m[\"a\"] = 3;", + "No function called '[]=' found that matches the arguments 'Map, String, Int'", + ), + // Reading the old value fails first, and is the one error reported. + ( + "let m = %{\"a\": 1}; m[0] += 3;", + "No function called '[]' found that matches the arguments 'Map, Int'", + ), + ("let a = 5; a[0] = 1;", "Unable to index into Int"), + ] { + assert_eq!(errors(source, globals.clone()), vec![expected], "{source}"); + } +} + +#[test] +fn a_function_annotation_types_a_variable() { + let int_to_int = StaticType::Function { + parameters: Some(vec![StaticType::Int]), + return_type: Box::new(StaticType::Int), + }; + assert_eq!( + analyse_last_type("let f: Fn(Int) -> Int = fn(x: Int) => x; f"), + int_to_int + ); + assert_eq!( + errors("let f: Fn(Int) -> Int = fn(x: String) => x;", vec![]), + vec!["mismatched types: found Fn(String) -> String but expected Fn(Int) -> Int"] + ); +} + +#[test] +fn a_function_argument_gets_what_the_function_parameter_passes() { + let globals = natives(&[("map", "(Iterable, Fn(T) -> U) -> List")]); + // A typed lambda fits a type parameter, and settles it. + assert_eq!( + analyse_last_type_with_globals( + "let xs = []; map(xs, fn(x: Int) => x); xs", + globals.clone() + ), + StaticType::List(Box::new(StaticType::Int)) + ); + assert_eq!( + analyse_last_type_with_globals("map([1], fn(x: Int) => \"a\")", globals.clone()), + StaticType::List(Box::new(StaticType::String)) + ); + // It has to take what the list holds. + assert_eq!( + errors("map([1], fn(x: String) => x);", globals), + vec![ + "No function called 'map' found that matches the arguments 'List, Fn(String) -> String'" + ] + ); +} + +#[test] +fn a_function_argument_fits_a_function_parameter_where_their_known_parts_agree() { + let apply = "fn apply(f: Fn(Int) -> Int) { f(1) } "; + // An untyped lambda's parameters take the types `apply` passes. + assert_eq!( + errors(&format!("{apply}apply(fn(x) => x);"), vec![]), + Vec::::new() + ); + for (lambda, shown) in [ + ("fn(x: String) => 1", "Fn(String) -> Int"), + ("fn(x) => \"a\"", "Fn(Int) -> String"), + ("fn(a, b) => 1", "Fn(Any, Any) -> Int"), ] { assert_eq!( - errors(source, plus_native()), - Vec::::new(), - "{source}" + errors(&format!("{apply}apply({lambda});"), vec![]), + vec![format!( + "No function called 'apply' found that matches the arguments '{shown}'" + )], + "{lambda}" ); } } #[test] -fn plain_assignment_to_a_field_is_not_ambiguous_with_a_value_named_like_the_field() { +fn a_function_value_rejects_a_callback_taking_another_number_of_arguments() { + // Called through an expression, so no overload is matched first. assert_eq!( - errors("struct P { x: Int } fn f(p, x) { p.x = 1 }", vec![]), - Vec::::new() + errors( + "fn apply(f: Fn(Int) -> Int) { f(1) } let fs = [apply]; fs[0](fn(a, b) => 1);", + index_natives() + ), + vec![ + "A function of type Fn(Fn(Int) -> Int) -> Int doesn't accept the arguments 'Fn(Any, Any) -> Int'" + ] + ); +} + +#[test] +fn a_lambdas_type_picks_between_overloads_taking_functions() { + let choose = "fn choose(f: Fn() -> Int) { 1 } fn choose(f: Fn() -> String) { \"s\" } "; + assert_eq!( + analyse_last_type(&format!("{choose}choose(fn() => 1)")), + StaticType::Int + ); + assert_eq!( + analyse_last_type(&format!("{choose}choose(fn() => \"a\")")), + StaticType::String + ); +} + +#[test] +fn a_lambda_takes_the_function_type_expected_where_it_is_written() { + let int_to_int = StaticType::Function { + parameters: Some(vec![StaticType::Int]), + return_type: Box::new(StaticType::Int), + }; + // An annotated variable. + assert_eq!( + analyse_last_type("let f: Fn(Int) -> Int = fn(x) => x; f"), + int_to_int + ); + // A function value's parameter. Either `+` could take `x` until the + // call says it's a `String`. + let globals = natives(&[("+", "(Int, Int) -> Int"), ("+", "(Float, Int) -> Float")]); + assert_eq!( + errors( + "let apply = fn(g: Fn(String) -> Int) { g(\"a\") }; let fs = [apply]; fs[0](fn(x) => x + 1);", + [index_natives(), globals].concat() + ), + vec!["No function called '+' found that matches the arguments 'String, Int'"] + ); + // A map's default function returns the map's values. + assert_eq!( + analyse_last_type("let m: Map> = %{:fn() => []}; m"), + StaticType::Map { + key: Box::new(StaticType::String), + value: Box::new(StaticType::List(Box::new(StaticType::Int))), + } + ); +} + +#[test] +fn an_any_parameter_settles_what_its_argument_leaves_unknown_as_any() { + // `keep` may store anything in the list, so it can't become a List. + let globals = [natives(&[("keep", "(List)")]), index_natives()].concat(); + assert_eq!( + analyse_last_type_with_globals("let xs = []; keep(xs); xs[0] = 1; xs", globals), + StaticType::List(Box::new(StaticType::Any)) + ); +} + +#[test] +fn a_default_function_gives_a_map_the_values_it_returns() { + assert_eq!( + analyse_last_type("%{:fn() => 1}"), + StaticType::Map { + key: Box::new(StaticType::Any), + value: Box::new(StaticType::Int), + } + ); +} + +#[test] +fn what_nothing_settles_falls_back_to_any() { + assert_eq!( + analyse_last_type("let xs = []; xs"), + StaticType::List(Box::new(StaticType::Any)) + ); + assert_eq!( + analyse_last_type("None"), + StaticType::Option(Box::new(StaticType::Any)) + ); + // An empty map with nothing written to it is an empty set. + assert_eq!( + analyse_last_type("%{}"), + StaticType::Map { + key: Box::new(StaticType::Any), + value: Box::new(StaticType::unit()), + } + ); +} + +#[test] +fn a_function_settles_its_own_type_variables_when_its_body_ends() { + // `f` returns a list of unknown elements: they fall back to `Any` when + // `f` ends, so a caller's write can't settle them for every caller. + assert_eq!( + analyse_last_type_with_globals( + "fn f() { [] } let xs = f(); xs[0] = \"a\"; f()", + index_natives() + ), + StaticType::List(Box::new(StaticType::Any)) + ); +} + +/// `+` with two overloads that both take an `Int` on the right, so `x + 1` +/// can't be picked until `x`'s type is known. +fn ambiguous_plus() -> Vec<(String, StaticType)> { + natives(&[("+", "(Int, Int) -> Int"), ("+", "(Float, Int) -> Float")]) +} + +fn function(parameters: Vec, return_type: StaticType) -> StaticType { + StaticType::Function { + parameters: Some(parameters), + return_type: Box::new(return_type), + } +} + +#[test] +fn a_call_waits_until_its_arguments_pick_an_overload() { + // Either `+` could take `x` until `f` is called with a Float. + assert_eq!( + analyse_last_type_with_globals("let f = fn(x) => x + 1; f(2.5); f", ambiguous_plus()), + function(vec![StaticType::Float], StaticType::Float) + ); +} + +#[test] +fn a_call_commits_to_the_only_overload_that_can_take_its_arguments() { + let globals = natives(&[ + ("plus", "(Int, Int) -> Int"), + ("plus", "(String, String) -> String"), + ]); + assert_eq!( + analyse_last_type_with_globals("let f = fn(x) => plus(x, 1); f", globals), + function(vec![StaticType::Int], StaticType::Int) + ); +} + +#[test] +fn a_lambda_passed_to_a_function_takes_its_parameter_types_from_the_call() { + let globals = [ + natives(&[("map", "(List, Fn(T) -> U) -> List")]), + ambiguous_plus(), + ] + .concat(); + assert_eq!( + analyse_last_type_with_globals("[2.5].map(fn(x) => x + 1)", globals), + StaticType::List(Box::new(StaticType::Float)) + ); +} + +#[test] +fn a_lambda_in_a_variable_has_one_type() { + assert_eq!( + errors( + "let f = fn(x) => x + 1; f(one()); f(2.5);", + [ambiguous_plus(), natives(&[("one", "() -> Int")])].concat() + ), + vec!["The variable 'f' holds a function that doesn't accept the arguments 'Float'"] + ); +} + +#[test] +fn a_variable_holding_an_unknown_function_takes_what_it_is_called_with() { + assert_eq!( + analyse_last_type_with_globals( + "let apply = fn(g, v) => g(v); apply(fn(z) => z + 1, 2.5)", + ambiguous_plus() + ), + StaticType::Float + ); + // Each later call goes through the type the first one settled. + assert_eq!( + analyse_last_type_with_globals( + "let apply = fn(g, v) { g(v); g(v) }; apply(fn(z) => z + 1, 2.5)", + ambiguous_plus() + ), + StaticType::Float + ); + assert_eq!( + errors("let apply = fn(g) { g(1); g(\"s\") };", ambiguous_plus()), + vec!["The variable 'g' holds a function that doesn't accept the arguments 'String'"] + ); + // A function that would have to take itself stays unknown, and the + // call is checked as the program runs. + assert_eq!( + errors("let f = fn(g) => g(g);", vec![]), + Vec::::new() + ); +} + +#[test] +fn iterating_waits_until_the_kind_of_sequence_is_known() { + let globals = [natives(&[("push", "(List, T)")]), ambiguous_plus()].concat(); + assert_eq!( + analyse_last_type_with_globals( + "let g = fn(rows) { let out = []; for r in rows { out.push(r + 1) }; out }; g([2.5])", + globals + ), + StaticType::List(Box::new(StaticType::Float)) + ); +} + +#[test] +fn unpacking_waits_until_the_kind_of_value_is_known() { + assert_eq!( + analyse_last_type("let swap = fn(p) { let (a, b) = p; (b, a) }; swap((1, \"s\"))"), + StaticType::Tuple(vec![StaticType::String, StaticType::Int]) + ); + assert_eq!( + errors("let f = fn(p) { let (a, b) = p; a }; f((1, 2, 3));", vec![]), + vec!["mismatched tuple arity: found a len=2 identifier and a len=3 annotation."] + ); +} + +#[test] +fn what_iterating_or_unpacking_gives_must_fit_how_it_was_used_meanwhile() { + // `plus(r, 1)` and `plus(a, 1)` make `r` and `a` Ints before the call + // settles what they are. + let globals = natives(&[ + ("plus", "(Int, Int) -> Int"), + ("plus", "(String, String) -> String"), + ]); + assert_eq!( + errors( + "let g = fn(rows) { for r in rows { plus(r, 1); } }; g([\"s\"]);", + globals.clone() + ), + vec!["mismatched types: found String but expected Int"] + ); + assert_eq!( + errors( + "let f = fn(p) { let (a, b) = p; plus(a, 1) }; f((\"s\", 2));", + globals + ), + vec!["mismatched types: found String but expected Int"] + ); +} + +#[test] +fn an_op_assignment_waits_for_its_operands_like_any_call() { + // `t += 1` doesn't fix `t` as an `Int` before the call says what `x` is. + assert_eq!( + analyse_last_type_with_globals( + "let f = fn(x) { let t = x; t += 1; t }; f(2.5)", + ambiguous_plus() + ), + StaticType::Float + ); + // The in-place overload is picked along with the operator once they're + // known, as `++=` is here: the natives' global slots are in this order. + let globals = natives(&[ + ("++", "(List, List) -> List"), + ("++", "(String, String) -> String"), + ("++=", "(List, List) -> List"), + ]); + let (_, result) = analyse_with_globals("let f = fn(xs) { xs ++= [1]; }; f([2]);", globals); + assert!(result.errors.is_empty(), "{:?}", result.errors); + assert_eq!( + result + .analysis + .augmented_assignments + .values() + .collect::>(), + vec![&Binding::Resolved(Candidate::Scalar( + ndc_analysis::ResolvedVar::Global { slot: 2 } + ))] + ); + // A target nothing else settles keeps the type of what's added to it. + assert_eq!( + analyse_last_type_with_globals("let f = fn(x) { x += 1; x }; f", ambiguous_plus()), + function(vec![StaticType::Int], StaticType::Int) + ); +} + +#[test] +fn a_call_nothing_else_settles_takes_its_narrowest_overload() { + // `[](Any, Any)` would take the map too, but loses to the map's own `[]`, + // which settles the key type. + let globals = [natives(&[("[]", "(Any, Any) -> Any")]), index_natives()].concat(); + assert_eq!( + analyse_last_type_with_globals("let m = %{}; m[\"k\"]; m", globals), + StaticType::Map { + key: Box::new(StaticType::String), + value: Box::new(StaticType::unit()), + } + ); +} + +#[test] +fn a_call_nothing_else_settles_takes_no_narrowest_overload_when_unsure() { + let function = |parameter, returns| StaticType::Function { + parameters: Some(vec![parameter]), + return_type: Box::new(returns), + }; + // Nothing is known about `x`, so it's `Any`, and the program picks + // `pick(Int)` or `pick(Any)` as it runs. + assert_eq!( + analyse_last_type( + "fn pick(x: Int) => 1; fn pick(x: Any) => \"any\"; let f = fn(x) => pick(x); f" + ), + function(StaticType::Any, StaticType::Any) + ); + // Overloads in two scopes fit, so none settles `x`: it's `Any`, and + // `[x]` is a `List`, which neither takes. + assert_eq!( + errors( + "fn pick(xs: List) => \"outer\"; { fn pick(xs: List) => 1; let f = fn(x) => pick([x]); f }", + vec![] + ), + vec!["No function called 'pick' found that matches the arguments 'List'"] + ); +} + +#[test] +fn a_waiting_call_does_not_commit_to_a_narrower_overload_too_early() { + // `pick(x)` waits: `x` may still turn out to be what only `pick(Any)` takes. + assert_eq!( + analyse_last_type( + "fn pick(x: Int) => 1; fn pick(x: Any) => \"any\"; let f = fn(x) => pick(x); f(\"s\")" + ), + StaticType::String + ); + // The nearest scope's `pick` takes the call, however narrow the outer one is. + assert_eq!( + analyse_last_type( + "fn pick(x: Int) => 1; { fn pick(x: Any) => \"inner\"; let f = fn(x) => pick(x); f(1) }" + ), + StaticType::String + ); +} + +#[test] +fn a_failed_op_assignment_is_reported_once() { + let globals = [ + natives(&[("++", "(String, String) -> String")]), + index_natives(), + ] + .concat(); + assert_eq!( + errors("let g = fn(xs) { xs[0] ++= \"a\"; }; g([1]);", globals), + vec!["No function called '++' found that matches the arguments 'Int, String'"] + ); +} + +#[test] +fn a_field_write_settles_the_receiver_like_a_field_read() { + let structs = "struct P { score: Int } struct Q { score: String } "; + assert_eq!( + errors( + &format!("{structs}let f = fn(p) {{ p.score = 3; p }}; f(Q(\"a\"));"), + vec![] + ), + vec!["The variable 'f' holds a function that doesn't accept the arguments 'Q'"] + ); + // A value that doesn't fit the field is reported once, against the field. + assert_eq!( + errors( + &format!("{structs}let f = fn(p) {{ p.score = 1.5; }}; f(P(1));"), + vec![] + ), + vec!["mismatched types: found Float but expected Int"] + ); +} + +#[test] +fn an_error_shows_what_isnt_known_yet_as_any() { + assert_eq!( + errors("let f = fn(x) -> Int { [x] };", vec![]), + vec!["mismatched types: found List but expected Int"] + ); +} + +#[test] +fn a_cast_settles_what_isnt_known_about_its_value() { + let (_, result) = analyse_with_globals("(fn(x) => x) as Fn(Int) -> Int", vec![]); + assert!(result.errors.is_empty(), "{:?}", result.errors); + // `x` is an Int, so the cast holds without a check when the program runs. + assert_eq!( + result.analysis.cast_checks.values().collect::>(), + vec![&None] + ); +} + +#[test] +fn a_waiting_call_does_not_commit_to_an_overload_taking_any_instead() { + // While the list's elements aren't known, both `[]=` might take the + // write, so it waits. Once they're Ints, only the catch-all takes a list + // as an element: the element-wise `[]=` would need its `T` to be both an + // Int and a list. + let globals = [ + natives(&[("[]=", "(List, Any, Any)")]), + index_natives(), + ] + .concat(); + assert_eq!( + errors("let xs = [1]; xs[0] = [];", globals.clone()), + Vec::::new() + ); + // Without the catch-all, nothing takes it. + assert_eq!( + errors("let xs = [1]; xs[0] = [];", index_natives()), + vec!["mismatched types: found List but expected Int"] + ); +} + +#[test] +fn an_operator_waits_while_an_operand_could_still_be_a_tuple() { + // `x ++ "!"` could also apply to each string of a tuple. + let globals = natives(&[("++", "(String, String) -> String")]); + assert_eq!( + analyse_last_type_with_globals( + "let suffix = fn(x) => x ++ \"!\"; suffix((\"a\", \"b\"))", + globals + ), + StaticType::Tuple(vec![StaticType::String, StaticType::String]) + ); +} + +#[test] +fn an_operator_doesnt_commit_early_while_an_operand_is_any() { + // `a` is `Any`, so it may be a tuple that `++` applies to element-wise: + // the call is picked when the program runs, among all the overloads + // that could do that, as it would be with the types as they end up. + let globals = natives(&[("++", "(List, List) -> List")]); + let (_, result) = analyse_with_globals("let g = fn(a: Any) => a ++ [];", globals); + assert!(result.errors.is_empty(), "{:?}", result.errors); + assert!(matches!( + result.analysis.bindings.values().find(|binding| matches!(binding, Binding::Dynamic(_))), + Some(Binding::Dynamic(candidates)) if candidates.iter().any(|c| matches!(c, Candidate::Vec(_))) + )); +} + +#[test] +fn a_write_passes_its_value_like_an_argument() { + // Any sequence goes where an `Iterable` is expected, and settles its + // elements as Ints. + assert_eq!( + analyse_last_type_with_globals( + "let ys = []; total(ys); ys", + natives(&[("total", "(Iterable) -> Int")]) + ), + StaticType::List(Box::new(StaticType::Int)) + ); +} + +#[test] +fn a_map_default_waits_until_it_is_known_whether_it_makes_the_values() { + let globals = [natives(&[("+", "(Int, Int) -> Int")]), index_natives()].concat(); + assert_eq!( + analyse_last_type_with_globals( + "let make = fn(factory) => %{:factory}; let m = make(fn() => 1); m[\"x\"] + 1", + globals + ), + StaticType::Int + ); +} + +/// `split` as the stdlib has it: one overload splits on whitespace, the +/// other on a pattern. +fn split_natives() -> Vec<(String, StaticType)> { + natives(&[ + ("split", "(String) -> List"), + ("split", "(String, String) -> List"), + ("map", "(List, Fn(T) -> U) -> List"), + ]) +} + +#[test] +fn an_overloaded_function_value_is_the_overload_its_use_expects() { + let list_of_lists = StaticType::List(Box::new(StaticType::List(Box::new(StaticType::String)))); + assert_eq!( + analyse_last_type_with_globals("[\"a b\"].map(split)", split_natives()), + list_of_lists + ); + // A variable's uses say what it is. + assert_eq!( + analyse_last_type_with_globals("let f = split; f(\"a b\"); f", split_natives()), + StaticType::Function { + parameters: Some(vec![StaticType::String]), + return_type: Box::new(StaticType::List(Box::new(StaticType::String))), + } + ); +} + +#[test] +fn a_return_annotation_picks_the_overloaded_function_value_returned() { + assert_eq!( + errors( + "fn by_pattern() -> Fn(String, String) -> List { split }", + split_natives() + ), + Vec::::new() + ); +} + +#[test] +fn a_return_annotation_settles_what_the_body_left_unknown() { + assert_eq!( + errors("fn f() -> List { let xs = []; xs }", vec![]), + Vec::::new() + ); + // Each value returned has to fit the annotation. + assert_eq!( + errors("fn f(x) -> Int { if x { return \"s\"; } 1 }", vec![]), + vec!["mismatched types: found String but expected Int"] + ); +} + +#[test] +fn the_help_for_an_ambiguous_function_value_names_an_overload_that_fits() { + let help = |source: &str, globals| { + let (_, result) = analyse_with_globals(source, globals); + result.errors[0].help_text().map(str::to_string) + }; + // A type parameter can't be written in an annotation. + assert_eq!( + help( + "let f = push;", + natives(&[("push", "(List, T)"), ("push", "(MinHeap, T)")]) + ), + Some( + "Say which one with a function type, as in `let f: Fn(MinHeap, Any) -> () = push`" + .to_string() + ) + ); + // The inner `pick` takes two arguments, so the example is an outer one. + assert_eq!( + help( + "fn pick(x: Int) => 1; fn pick(x: Float) => 1.0; { fn pick(a, b) => 0; let f: Fn(Any) -> Any = pick; }", + vec![] + ), + Some( + "Say which one with a function type, as in `let f: Fn(Float) -> Float = pick`" + .to_string() + ) + ); +} + +#[test] +fn a_function_value_is_not_picked_by_its_return_type_before_its_parameters_are_known() { + // Only `pick(Any)` returns an Int, but once `xs` holds Ints a call with + // an Int means `pick(Int)`, so that's the one meant, and it doesn't fit. + let globals = natives(&[ + ("map", "(List, Fn(T) -> U) -> List"), + ("push", "(List, T)"), + ]); + assert_eq!( + errors( + "fn pick(x) => 0; fn pick(x: Int) => \"s\"; let xs = []; let ys: List = xs.map(pick); xs.push(1);", + globals + ), + vec!["No overload of 'pick' is a Fn(Int) -> Int"] + ); +} + +#[test] +fn a_function_value_commits_when_only_one_overload_can_take_the_arguments() { + // `map` calls `split` with one argument, which only `split(String)` + // takes, so it's meant even though nothing says what `xs` holds. + assert_eq!( + errors("let xs = []; xs.map(split);", split_natives()), + Vec::::new() + ); +} + +#[test] +fn an_overloaded_function_value_nothing_picks_is_an_error() { + assert_eq!( + errors("let f = split;", split_natives()), + vec!["'split' has several overloads, and nothing here says which one is meant"] + ); + assert_eq!( + errors("let f: Fn(Int) -> Int = split;", split_natives()), + vec!["No overload of 'split' is a Fn(Int) -> Int"] + ); + // `split(String)` takes the String, but doesn't return an Int. + assert_eq!( + errors("let f: Fn(String) -> Int = split;", split_natives()), + vec!["No overload of 'split' is a Fn(String) -> Int"] + ); +} + +#[test] +fn an_overloaded_function_value_uses_the_nearest_scope_with_an_overload_that_fits() { + // The inner `pick` doesn't take an Int, so the outer one is meant. + assert_eq!( + analyse_last_type( + "fn pick(x: Int) => 1; { fn pick(x: String) => \"s\"; let f: Fn(Int) -> Int = pick; f(1) }" + ), + StaticType::Int + ); + // When both fit, the nearer one is meant, which + // 005_functions/067 shows by what the program prints. +} + +#[test] +fn a_join_waits_until_both_branches_are_known() { + // `x` isn't fixed as an `Int` by the branch next to it: the branches join + // once the call says what `x` is. + assert_eq!( + analyse_last_type("let f = fn(x) => if true { x } else { 1 }; f(2)"), + StaticType::Int + ); + // The literal `1` becomes a Float like `x`, but a String and an Int join + // as `Any`. + assert_eq!( + analyse_last_type("let f = fn(x) => if true { x } else { 1 }; f(2.5)"), + StaticType::Float + ); + assert_eq!( + analyse_last_type("let f = fn(x) => if true { x } else { 1 }; f(\"a\")"), + StaticType::Any + ); + // What both branches share is known meanwhile: two lists join as a list + // that `push` can take. + assert_eq!( + analyse_last_type_with_globals( + "let xs = if true { [] } else { [] }; xs.push(1); xs", + natives(&[("push", "(List, T)"), ("push", "(MinHeap, T)")]) + ), + StaticType::List(Box::new(StaticType::Int)) + ); +} + +#[test] +fn a_call_nothing_picks_falls_back_to_picking_at_runtime() { + let (last, result) = analyse_with_globals("let f = fn(x) => x + 1; f", ambiguous_plus()); + assert!(result.errors.is_empty(), "{:?}", result.errors); + assert_eq!(last, function(vec![StaticType::Any], StaticType::Any)); +} + +/// `len`, a native taking a `String`, for the test below. +fn len_native() -> Vec<(String, StaticType)> { + vec![( + "len".to_string(), + StaticType::Function { + parameters: Some(vec![StaticType::String]), + return_type: Box::new(StaticType::Int), + }, + )] +} + +#[test] +fn a_call_uses_a_variable_standing_nearer_than_a_function() { + assert_eq!( + errors("fn foo(x) { 1 } fn test(foo) { foo(2) }", vec![]), + Vec::::new(), + ); + assert_eq!( + errors("fn test(len) { len(\"abc\") }", len_native()), + Vec::::new(), + ); +} + +#[test] +fn a_call_of_a_variable_that_cant_be_called_does_not_reach_the_function() { + assert_eq!( + errors("fn test(len: Int) { len(\"abc\") }", len_native()), + vec!["The variable 'len' is Int, which can't be called"], + ); +} + +/// `+`, which augmented assignment needs, for the tests below. +fn plus_native() -> Vec<(String, StaticType)> { + vec![( + "+".to_string(), + StaticType::Function { + parameters: Some(vec![StaticType::Int, StaticType::Int]), + return_type: Box::new(StaticType::Int), + }, + )] +} + +#[test] +fn field_reads_and_writes_skip_a_value_named_like_the_field() { + for source in [ + "struct P { x: Int } fn f(p, x) { p.x }", + "struct P { x: Int } fn f(p, x) { p.x += 1 }", + ] { + assert_eq!( + errors(source, plus_native()), + Vec::::new(), + "{source}" + ); + } +} + +#[test] +fn plain_assignment_to_a_field_is_not_ambiguous_with_a_value_named_like_the_field() { + assert_eq!( + errors("struct P { x: Int } fn f(p, x) { p.x = 1 }", vec![]), + Vec::::new() + ); +} + +#[test] +fn a_generic_function_has_the_type_its_body_makes_of_each_call() { + for (call, typ) in [("inc(1)", StaticType::Int), ("inc(2.5)", StaticType::Float)] { + assert_eq!( + analyse_last_type_with_globals( + &format!("fn inc(x) => x + 1; inc(1); inc(2.5); {call}"), + ambiguous_plus() + ), + typ, + "{call}" + ); + } +} + +#[test] +fn a_generic_function_calls_what_its_body_calls_as_the_program_runs() { + // `inc`'s body is compiled once, for Ints and Floats alike. + let (_, result) = analyse_with_globals("fn inc(x) => x + 1; inc(1);", ambiguous_plus()); + assert!(result.errors.is_empty(), "{:?}", result.errors); + assert!( + result + .analysis + .bindings + .values() + .any(|binding| matches!(binding, Binding::Dynamic(_))), + "{:?}", + result.analysis.bindings + ); +} + +#[test] +fn a_generic_function_calling_one_has_its_constraints_too() { + assert_eq!( + analyse_last_type_with_globals( + "fn inc(x) => x + 1; fn twice(x) => inc(inc(x)); twice(2.5)", + ambiguous_plus() + ), + StaticType::Float + ); +} + +#[test] +fn an_error_in_a_generic_body_is_the_outermost_calls() { + let source = "fn inc(x) => x + 1; fn twice(x) => inc(inc(x)); twice(\"a\");"; + let (_, result) = analyse_with_globals(source, ambiguous_plus()); + let [error] = &result.errors[..] else { + panic!("one error expected: {:?}", result.errors); + }; + let text = |span: Span| &source[span.range()]; + assert_eq!(text(error.span()), "twice(\"a\")"); + assert_eq!( + error + .notes() + .iter() + .map(|(span, note)| (text(*span), note.as_str())) + .collect::>(), + vec![ + ("inc(x)", "`twice` calls `inc` here"), + ("x + 1", "`inc` needs this"), + ] + ); +} + +#[test] +fn a_generic_function_keeps_one_copy_of_each_call_it_copies() { + // Each function calls the one before twice. With a copy of each call's + // constraints, the last would have 2^40. + let mut source = "fn f0(x) => x + 1;".to_string(); + for i in 1..40 { + source.push_str(&format!("fn f{i}(x) => f{}(x) + f{}(x);", i - 1, i - 1)); + } + source.push_str("f39(2.5)"); + assert_eq!( + analyse_last_type_with_globals( + &source, + natives(&[ + ("+", "(Int, Int) -> Int"), + ("+", "(Float, Int) -> Float"), + ("+", "(Float, Float) -> Float"), + ]) + ), + StaticType::Float + ); +} + +#[test] +fn a_variable_from_outside_has_one_type_in_a_generic_function() { + // `n` is stored in `xs`, which the first call settles. + let globals = natives(&[("push", "(List, T)")]); + assert_eq!( + analyse_last_type_with_globals( + "let xs = []; fn f(n) { xs.push(n) } f(1); xs", + globals.clone() + ), + StaticType::List(Box::new(StaticType::Int)) + ); + assert_eq!( + errors( + "let xs = []; fn f(n) { xs.push(n) } f(1); f(\"a\");", + globals + ), + vec!["No function called 'f' found that matches the arguments 'String'"] + ); +} + +#[test] +fn what_no_call_can_decide_is_decided_in_the_body() { + // The map's types can't depend on how `count` is called, so they're + // settled where it's declared, as at the end of the program. + let mut globals = index_natives(); + globals.extend(natives(&[("+", "(Int, Int) -> Int")])); + assert_eq!( + analyse_last_type_with_globals( + "fn count() { let m = %{}; m[\"k\"] = 1; m } count()", + globals + ), + StaticType::Map { + key: Box::new(StaticType::String), + value: Box::new(StaticType::Int), + } + ); +} + +#[test] +fn a_recursive_generic_function_returns_what_its_branches_join_to() { + let globals = natives(&[ + ("<", "(Int, Int) -> Bool"), + ("<", "(Float, Int) -> Bool"), + ("-", "(Int, Int) -> Int"), + ("-", "(Float, Int) -> Float"), + ("+", "(Int, Int) -> Int"), + ("+", "(Float, Float) -> Float"), + ]); + for (argument, typ) in [("10", StaticType::Int), ("10.0", StaticType::Float)] { + assert_eq!( + analyse_last_type_with_globals( + &format!( + "fn fib(n) => if n < 2 {{ n }} else {{ fib(n - 1) + fib(n - 2) }}; fib({argument})" + ), + globals.clone() + ), + typ, + ); + } +} + +#[test] +fn overloads_tie_when_a_call_could_fit_both_and_neither_is_more_specific() { + for (source, ties) in [ + ("fn f(x: Int, y) {} fn f(x, y: Int) {}", true), + ("fn f(x: Int, y: Any) {} fn f(x: Any, y: Int) {}", false), + ("fn f(x: Int) {} fn f(x) {}", false), + ("fn f(x: List) {} fn f(x) {}", false), + ("fn f(x: Int) {} fn f(x: Any) {}", false), + ("fn f(x: List) {} fn f(x: List) {}", false), + ("fn f(x) {} fn f(x, y) {}", false), + ] { + let errors = errors(source, vec![]); + assert_eq!( + errors + .iter() + .any(|error| error.contains("neither is more specific")), + ties, + "{source}: {errors:?}" + ); + } + // Two unannotated parameters are a redefinition, as two `Any`s are. + assert_eq!( + errors("fn f(x) {} fn f(y) {}", vec![]), + vec!["Illegal redefinition of function 'f' with parameters (Any) in the same scope"] + ); +} + +#[test] +fn a_lambda_a_generic_function_passes_on_takes_what_each_call_gives_it() { + // `map` waits for what `xs` is, so its lambda's types are only known + // once `incs` is called. + let mut globals = natives(&[ + ("map", "(List, Fn(T) -> U) -> List"), + ("map", "(Deque, Fn(T) -> U) -> List"), + ]); + globals.extend(ambiguous_plus()); + assert_eq!( + analyse_last_type_with_globals( + "fn incs(xs) => xs.map(fn(x) => x + 1); incs([1]); incs([2.5])", + globals + ), + StaticType::List(Box::new(StaticType::Float)) + ); +} + +#[test] +fn an_overloaded_function_value_in_a_generic_function_is_picked_for_each_call() { + // Each instance of `f` picks the `abs` its argument's type calls for. + let globals = natives(&[("abs", "(Int) -> Int"), ("abs", "(Float) -> Float")]); + let source = "fn f(x) { let g = abs; g(x) }"; + assert_eq!( + analyse_last_type_with_globals(&format!("{source} f(1)"), globals.clone()), + StaticType::Int + ); + assert_eq!( + analyse_last_type_with_globals(&format!("{source} f(2.5)"), globals.clone()), + StaticType::Float + ); + assert_eq!( + errors( + "fn f(x) { let g: Fn(Int) -> Int = abs; g(x) } f(1);", + globals + ), + Vec::::new() + ); +} + +#[test] +fn a_generic_function_stored_in_a_variable_keeps_its_constraints() { + assert_eq!( + analyse_last_type_with_globals("fn inc(x) => x + 1; let h = inc; h(2.5)", ambiguous_plus()), + StaticType::Float + ); + assert_eq!( + errors( + "fn inc(x) => x + 1; let h = inc; h(\"a\");", + ambiguous_plus() + ), + vec!["No function called '+' found that matches the arguments 'String, Int'"] + ); +} + +/// How many instances of generic functions analysing `source` makes. +fn instance_count(source: &str, globals: Vec<(String, StaticType)>) -> usize { + let (_, result) = analyse_with_globals(source, globals); + assert!(result.errors.is_empty(), "{:?}", result.errors); + result.analysis.instances.values().map(Vec::len).sum() +} + +#[test] +fn each_set_of_key_types_gets_one_instance() { + let (_, result) = analyse_with_globals( + "fn inc(x) => x + 1; inc(1); inc(2.5); inc(3);", + ambiguous_plus(), + ); + assert!(result.errors.is_empty(), "{:?}", result.errors); + let instances = result + .analysis + .instances + .values() + .flatten() + .collect::>(); + assert_eq!(instances.len(), 2); + // Each picks its `+` from its types. + for instance in instances { + assert!( + instance + .bindings + .values() + .any(|binding| matches!(binding, Binding::Resolved(_))), + "{instance:?}" + ); + } +} + +#[test] +fn a_function_whose_calls_dont_depend_on_its_types_has_no_instances() { + assert_eq!( + instance_count("fn wrap(x) => [x]; wrap(1); wrap(\"a\");", vec![]), + 0 + ); +} + +#[test] +fn a_call_with_any_arguments_uses_the_function_itself() { + assert_eq!( + instance_count( + "fn inc(x) => x + 1; let a: Any = 1; inc(a);", + ambiguous_plus() + ), + 0 + ); +} + +#[test] +fn an_instance_calls_the_instances_its_types_need() { + // `twice(2.5)` needs `twice` for Floats, which needs `inc` for Floats. + let (_, result) = analyse_with_globals( + "fn inc(x) => x + 1; fn twice(x) => inc(inc(x)); twice(2.5);", + ambiguous_plus(), + ); + assert!(result.errors.is_empty(), "{:?}", result.errors); + assert_eq!( + result.analysis.instances.len(), + 2, + "{:?}", + result.analysis.instances + ); + let twice = result + .analysis + .instances + .values() + .flatten() + .find(|instance| instance.bindings.len() == 2) + .expect("twice's instance picks its two calls of inc"); + let inc = result + .analysis + .instances + .values() + .flatten() + .find(|instance| instance.bindings.len() == 1) + .expect("inc's instance picks its +"); + assert!( + twice + .bindings + .values() + .all(|binding| *binding == Binding::Resolved(Candidate::Scalar(inc.variable))), + "{twice:?}" + ); +} + +#[test] +fn an_instance_calls_itself_where_its_body_recurses() { + let globals = natives(&[ + ("<", "(Int, Int) -> Bool"), + ("-", "(Int, Int) -> Int"), + ("+", "(Int, Int) -> Int"), + ]); + let (_, result) = analyse_with_globals( + "fn fib(n) => if n < 2 { n } else { fib(n - 1) + fib(n - 2) }; fib(10);", + globals, + ); + assert!(result.errors.is_empty(), "{:?}", result.errors); + let [instance] = &result + .analysis + .instances + .values() + .flatten() + .collect::>()[..] + else { + panic!("one instance expected: {:?}", result.analysis.instances); + }; + let calls_itself = instance + .bindings + .values() + .filter(|binding| **binding == Binding::Resolved(Candidate::Scalar(instance.variable))) + .count(); + assert_eq!(calls_itself, 2, "{instance:?}"); +} + +#[test] +fn a_list_literal_holding_the_list_it_is_pushed_to_is_analysed() { + // `[xs]` holds a list of what it is itself, which used to recurse + // forever while its type was settled. + for source in [ + "let xs = []; xs.push([xs]);", + "let xs = [\"s\"]; xs = [xs];", + ] { + let (_, result) = analyse_with_globals(source, natives(&[("push", "(List, T)")])); + assert!(result.errors.len() <= 1, "{source}: {:?}", result.errors); + } +} + +#[test] +fn a_call_no_overload_takes_says_what_the_overloads_take() { + // `put`'s body puts `x` in `r`, which the first call settles as holding + // Ints, so `put` takes an Int from then on. + let (_, result) = analyse_with_globals( + "let r = []; fn put(x) { r.push(x); 0 } put(1); put(\"a\");", + natives(&[("push", "(List, T)")]), + ); + let help = result + .errors + .iter() + .find_map(|error| error.help_text().map(str::to_string)); + assert_eq!( + help.as_deref(), + Some("`put` takes (Int)"), + "{:?}", + result.errors + ); +} + +/// Natives for the tests of what's built from a failed check. +fn failed_check_natives() -> Vec<(String, StaticType)> { + let mut globals = natives(&[ + ("len", "(Iterable) -> Int"), + ("map", "(Iterable, Fn(T) -> U) -> List"), + ("sum", "(Iterable) -> Int"), + ("sum", "(Iterable) -> Float"), + ("abs", "(Int) -> Int"), + ("abs", "(Float) -> Float"), + ("++", "(List, List) -> List"), + ("++", "(String, String) -> String"), + ]); + globals.extend(index_natives()); + globals +} + +#[test] +fn what_is_built_from_a_failed_call_is_not_reported_again() { + let globals = failed_check_natives(); + assert_eq!( + errors( + "struct Player { name: String, score: Int } + let players = [Player(\"ann\", 3), Player(\"bob\", 5)]; + players.map(fn(p) => p.len).sum();", + globals, + ), + vec!["No function called 'len' found that matches the arguments 'Player'"] + ); +} + +#[test] +fn a_failed_call_is_reported_once_wherever_its_value_goes() { + let globals = failed_check_natives(); + let failed = "No function called 'len' found that matches the arguments 'Bool'"; + for source in [ + // Indexed. + "let x = len(true)[0]; [x].sum();", + // Iterated. + "for x in len(true) { [x].sum(); }", + "let xs: List = [x for x in len(true)];", + // Passed on. + "[len(true)].sum();", + "len(true).map(abs);", + "len(true).map(fn(x) => [x]).sum();", + "abs(len(true)) ++ \"a\";", + // Assigned to a typed variable. + "let xs: List = [len(true)];", + "let s: String = len(true);", + // Called. + "let f = len(true); [f(1)].sum();", + // Unpacked. + "let (a, b) = len(true); [a, b].sum();", + ] { + assert_eq!(errors(source, globals.clone()), vec![failed], "{source}"); + } +} + +#[test] +fn an_error_after_a_failed_call_is_still_reported() { + let globals = failed_check_natives(); + assert_eq!( + errors( + "let n = len(true); let s: String = 1; [\"a\"].sum();", + globals, + ), + vec![ + "No function called 'len' found that matches the arguments 'Bool'", + "mismatched types: found Int but expected String", + "No function called 'sum' found that matches the arguments 'List'", + ] + ); +} + +#[test] +fn a_failed_annotation_is_not_reported_again() { + let globals = failed_check_natives(); + assert_eq!( + errors("let x: Int = 1; [x].sum();", globals), + vec!["type `Int` does not take generic arguments"] ); } diff --git a/ndc_analyser/src/error.rs b/ndc_analyser/src/error.rs index 156ff329..43390f4a 100644 --- a/ndc_analyser/src/error.rs +++ b/ndc_analyser/src/error.rs @@ -10,6 +10,8 @@ pub struct AnalysisError { text: String, span: Span, help_text: Option, + /// Other places that explain the error, each with what it says there. + notes: Vec<(Span, String)>, } impl From for AnalysisError { @@ -18,6 +20,7 @@ impl From for AnalysisError { text: error.to_string(), span: error.span(), help_text: error.help_text(), + notes: Vec::new(), } } } @@ -33,11 +36,47 @@ impl AnalysisError { self.help_text.as_deref() } + /// Other places that explain the error, each with what it says there. + pub fn notes(&self) -> &[(Span, String)] { + &self.notes + } + + /// This error, found in a generic function's body while checking a call + /// of it, as the call's error. `calls` are the calls it came through, + /// outermost first, each with the function it calls: in + /// `fn yy(l) => l.len(); yy(5)`, the call `yy(5)` gets the error that + /// there's no `len(Int)`, with a note at `l.len()`. + pub(crate) fn through_calls<'a>( + mut self, + calls: impl IntoIterator, + ) -> Self { + let calls = calls.into_iter().collect::>(); + let Some(&(_, outermost)) = calls.first() else { + return self; + }; + let mut notes = calls + .windows(2) + .map(|pair| { + let [(caller, _), (callee, call)] = pair else { + unreachable!("windows of two"); + }; + (*call, format!("`{caller}` calls `{callee}` here")) + }) + .collect::>(); + let (innermost, _) = calls[calls.len() - 1]; + notes.push((self.span, format!("`{innermost}` needs this"))); + notes.append(&mut self.notes); + self.notes = notes; + self.span = outermost; + self + } + pub(crate) fn invalid_type_annotation(err: &StaticTypeConstructionError, span: Span) -> Self { Self { text: err.to_string(), span, help_text: Some(err.help_text().to_string()), + notes: Vec::new(), } } @@ -46,6 +85,7 @@ impl AnalysisError { text: format!("type `{name}` does not take generic arguments"), span, help_text: None, + notes: Vec::new(), } } @@ -60,6 +100,7 @@ impl AnalysisError { ), span, help_text: None, + notes: Vec::new(), } } @@ -68,6 +109,65 @@ impl AnalysisError { text: format!("mismatched types: found {found} but expected {expected}"), span, help_text: None, + notes: Vec::new(), + } + } + + pub(crate) fn not_iterable(typ: &StaticType, span: Span) -> Self { + Self { + text: format!("{typ} is not iterable"), + span, + help_text: None, + notes: Vec::new(), + } + } + + pub(crate) fn recursion_changes_types(function: &str, span: Span) -> Self { + Self { + text: format!( + "`{function}` calls itself with an argument built from what it was given, so each call would need a version of `{function}` for a deeper type" + ), + span, + help_text: Some( + "A recursive call has to pass the types the function was called with. Annotate the parameter with `Any` to let them change as the program runs" + .to_string(), + ), + notes: Vec::new(), + } + } + + pub(crate) fn type_too_large(function: &str, limit: usize, span: Span) -> Self { + Self { + text: format!( + "The type of `{function}` has more than {limit} parts, too many to work with" + ), + span, + help_text: Some( + "Each call through it makes a bigger type. Annotate a parameter or the return type with `Any` to stop it growing" + .to_string(), + ), + notes: Vec::new(), + } + } + + pub(crate) fn tuple_index_out_of_bounds(index: i64, length: usize, span: Span) -> Self { + Self { + text: format!("Index {index} is out of bounds for a tuple of {length} elements"), + span, + help_text: None, + notes: Vec::new(), + } + } + + pub(crate) fn iterable_in_an_annotation(span: Span) -> Self { + Self { + text: "`Iterable` can't be written in a type annotation".to_string(), + span, + help_text: Some( + "A parameter without an annotation takes any kind of sequence. Say which kind a value is, as in `List`" + .to_string(), + ), + notes: Vec::new(), } } @@ -76,6 +176,7 @@ impl AnalysisError { text: format!("invalid cast: {found} can never be {target}"), span, help_text: None, + notes: Vec::new(), } } @@ -96,6 +197,27 @@ impl AnalysisError { help_text: Some( "Overloads in one scope need different parameter types, so annotate a parameter's type to tell them apart".to_string(), ), + notes: Vec::new(), + } + } + + pub(crate) fn tied_overloads( + name: &str, + parameters: &[String], + earlier: &[String], + span: Span, + ) -> Self { + Self { + text: format!( + "A call could fit both {name}({}) and {name}({}), declared earlier in the same scope, and neither is more specific", + parameters.join(", "), + earlier.join(", "), + ), + span, + help_text: Some( + "A parameter without a type annotation (`_` here) takes any argument the body can work with. Annotate the parameters so that one overload is more specific, or so that they take different arguments".to_string(), + ), + notes: Vec::new(), } } @@ -104,6 +226,7 @@ impl AnalysisError { text: format!("Unable to index into {typ}"), span, help_text: None, + notes: Vec::new(), } } pub(crate) fn unable_to_unpack_type(typ: &StaticType, span: Span) -> Self { @@ -111,6 +234,7 @@ impl AnalysisError { text: format!("Invalid unpacking of {typ}"), span, help_text: None, + notes: Vec::new(), } } @@ -138,6 +262,7 @@ impl AnalysisError { "A variable hides the functions named '{name}' further out: it doesn't join their overloads" ) }), + notes: Vec::new(), } } @@ -149,6 +274,21 @@ impl AnalysisError { ), span, help_text: None, + notes: Vec::new(), + } + } + + /// This error with help saying what the overloads of `name` take, given + /// as `(Int, String)` each, if any. + pub(crate) fn with_overloads(self, name: &str, overloads: &[String]) -> Self { + let help_text = match overloads { + [] => return self, + [only] => format!("`{name}` takes {only}"), + several => format!("The overloads of `{name}` take {}", several.join(", ")), + }; + Self { + help_text: Some(help_text), + ..self } } @@ -171,11 +311,46 @@ impl AnalysisError { } } + /// `example` is one of the overloads' types, to show how to say which + /// one is meant. + pub(crate) fn ambiguous_function_value( + name: &str, + expected: &StaticType, + example: &StaticType, + span: Span, + ) -> Self { + let text = if *expected == StaticType::Any { + format!("'{name}' has several overloads, and nothing here says which one is meant") + } else { + format!( + "'{name}' has several overloads that are a {expected}, and nothing here says which one is meant" + ) + }; + Self { + text, + span, + help_text: Some(format!( + "Say which one with a function type, as in `let f: {example} = {name}`" + )), + notes: Vec::new(), + } + } + + pub(crate) fn no_overload_fits(name: &str, expected: &StaticType, span: Span) -> Self { + Self { + text: format!("No overload of '{name}' is a {expected}"), + span, + help_text: None, + notes: Vec::new(), + } + } + pub(crate) fn break_outside_loop(span: Span) -> Self { Self { text: "unexpected break statement outside of loop".to_string(), span, help_text: None, + notes: Vec::new(), } } @@ -184,6 +359,7 @@ impl AnalysisError { text: "unexpected continue statement outside of loop".to_string(), span, help_text: None, + notes: Vec::new(), } } @@ -192,6 +368,23 @@ impl AnalysisError { text: "unexpected return statement outside of function body".to_string(), span, help_text: None, + notes: Vec::new(), + } + } + + pub(crate) fn callee_rejects_arguments( + callee: &StaticType, + types: &[StaticType], + span: Span, + ) -> Self { + Self { + text: format!( + "A function of type {callee} doesn't accept the arguments '{}'", + types.iter().join(", ") + ), + span, + help_text: None, + notes: Vec::new(), } } @@ -200,6 +393,7 @@ impl AnalysisError { text: format!("Unable to invoke {typ} as a function."), span, help_text: None, + notes: Vec::new(), } } } diff --git a/ndc_analyser/src/lib.rs b/ndc_analyser/src/lib.rs index 50a1051f..f9f6ff9b 100644 --- a/ndc_analyser/src/lib.rs +++ b/ndc_analyser/src/lib.rs @@ -3,5 +3,5 @@ mod error; mod overloads; mod types; -pub use analyser::{Analyser, AnalysisResult}; +pub use analyser::{Analyser, AnalysisResult, tied_natives}; pub use error::AnalysisError; diff --git a/ndc_analyser/src/overloads.rs b/ndc_analyser/src/overloads.rs index 2e24977c..dbb5acea 100644 --- a/ndc_analyser/src/overloads.rs +++ b/ndc_analyser/src/overloads.rs @@ -1,7 +1,8 @@ -use crate::types::TypeTable; +use crate::types::{TypeTable, TypeVars}; use ndc_analysis::{Binding, Candidate}; use ndc_core::StaticType; use ndc_resolver::{BindingKind, Candidates, SymbolId, SymbolTable}; +use std::borrow::Cow; /// Whether a call site can fall back to element-wise tuple broadcast when no /// scalar overload matches the argument types directly. Set by the parser: @@ -39,7 +40,7 @@ pub(crate) enum VecResolution { /// Output of a single scalar overload walk across the scope chain. struct ScalarWalk { - /// First exact-subtype match found; short-circuits the walk. + /// First exact match found; short-circuits the walk. exact: Option, /// First-scope-wins loose-compatibility candidates. loose: Option>, @@ -48,21 +49,51 @@ struct ScalarWalk { all_by_name: Vec, } -/// Returns `true` iff `more` is a strict subtype of `less` pointwise — i.e. -/// every position satisfies `more[i] <: less[i]` and at least one position is -/// not bidirectionally equivalent. Used by [`Overloads::most_specific_match`] to order -/// matching overloads by specificity: an overload with `more` parameters -/// accepts strictly fewer call signatures than one with `less` parameters. -fn is_strictly_more_specific(more: &[StaticType], less: &[StaticType]) -> bool { +/// Whether a value of type `typ` could still accept a call with this +/// signature at runtime: its type is `Any`, a variadic function, or a function whose +/// parameters don't provably conflict with `sig`: an `Any` might turn +/// out to be anything, and the parts of either type not known yet could +/// be anything. A lambda `fn(x) => x * 2` can be passed for an +/// `Fn(Int) -> Int`, but `fn(x: String) => 1` can't. +fn could_take(typ: &StaticType, sig: &[StaticType]) -> bool { + match typ { + StaticType::Function { + parameters: Some(parameters), + .. + } => { + parameters.len() == sig.len() + && parameters + .iter() + .zip(sig) + .all(|(param, arg)| arg.may_fit(param)) + && StaticType::type_parameters_agree(parameters, sig) + } + StaticType::Function { + parameters: None, .. + } => true, + // A value whose type isn't known may hold a function. + unknown => unknown.is_unknown(), + } +} + +/// Returns `true` iff `more` is strictly more specific than `less`: each of +/// its parameter types is the other's, or an instance of it, and at least +/// one isn't the other's. An instance of a type parameter is any type, and +/// every type is an instance of `Any`: `f(Int)` is more specific than +/// `f(x)` and `f(Any)`, but `f(List)` and `f(List)` are +/// unrelated. Used by [`Overloads::most_specific_match`] to order matching +/// overloads by specificity: an overload with `more` parameters accepts +/// strictly fewer call signatures than one with `less` parameters. +pub(crate) fn is_strictly_more_specific(more: &[StaticType], less: &[StaticType]) -> bool { if more.len() != less.len() { return false; } let mut any_strict = false; for (m, l) in more.iter().zip(less) { - if !m.is_subtype(l) { + if !m.fits(l) { return false; } - if !l.is_subtype(m) { + if !l.fits(m) { any_strict = true; } } @@ -97,13 +128,20 @@ fn push_unique(out: &mut Vec, item: T) { #[derive(Clone, Copy)] pub(crate) struct Overloads<'a> { types: &'a TypeTable, + /// What the type variables in those types stand for so far. A function + /// that returns a captured `let xs = []` returns what `xs` holds. + vars: &'a TypeVars, /// Tells a `fn` from a plain binding like a `let`, which doesn't join overloads. symbols: &'a SymbolTable, } impl<'a> Overloads<'a> { - pub(crate) fn new(types: &'a TypeTable, symbols: &'a SymbolTable) -> Self { - Self { types, symbols } + pub(crate) fn new(types: &'a TypeTable, vars: &'a TypeVars, symbols: &'a SymbolTable) -> Self { + Self { + types, + vars, + symbols, + } } /// The candidate groups a call can reach, nearest scope first. A plain @@ -129,6 +167,41 @@ impl<'a> Overloads<'a> { }) } + /// The type of `symbol` as far as it's known: a parameter `g` whose type + /// a call such as `g(1)` settled is a function from then on. + fn type_of(self, symbol: SymbolId) -> Cow<'a, StaticType> { + match self.types.get_type(symbol) { + typ if typ.has_vars() => Cow::Owned(self.vars.resolve(typ)), + typ => Cow::Borrowed(typ), + } + } + + /// The candidates a call can reach, grouped by scope, nearest first. + pub(crate) fn reachable_groups(self, candidates: &Candidates) -> Vec> { + self.reachable(candidates) + .map(<[SymbolId]>::to_vec) + .collect() + } + + /// Every candidate a call can reach, nearest scope first. + pub(crate) fn reachable_symbols(self, candidates: &Candidates) -> Vec { + self.reachable(candidates).flatten().copied().collect() + } + + /// The reachable candidates in the scope `symbol` is declared in, which + /// are the ones a call that picks `symbol` could pick instead: one + /// further out loses to any that takes the call. + pub(crate) fn same_scope_symbols( + self, + candidates: &Candidates, + symbol: SymbolId, + ) -> Vec { + self.reachable(candidates) + .find(|group| group.contains(&symbol)) + .map(<[SymbolId]>::to_vec) + .unwrap_or_default() + } + /// The variable that ends the search, if one does: in /// `fn foo(a) { … } let foo = fn(a, b) { … };`, a call of `foo` ends at the `let`. pub(crate) fn variable_ending_search(self, candidates: &Candidates) -> Option { @@ -163,10 +236,11 @@ impl<'a> Overloads<'a> { /// misleading advice. pub(crate) fn a_cast_could_resolve(self, candidates: &Candidates, sig: &[StaticType]) -> bool { self.reachable(candidates).flatten().any(|&symbol| { + let typ = self.type_of(symbol); let StaticType::Function { parameters: Some(parameters), .. - } = self.types.get_type(symbol) + } = typ.as_ref() else { return false; }; @@ -178,7 +252,7 @@ impl<'a> Overloads<'a> { && parameters .iter() .zip(sig) - .any(|(param, arg)| !arg.is_subtype(param)) + .any(|(param, arg)| !arg.fits(param)) }) } @@ -201,9 +275,10 @@ impl<'a> Overloads<'a> { /// position and no looser scalar competes. Returns /// `Binding::Resolved(Vec)`, return type = `Tuple`. /// 3. **Loose scalar or vec candidates** — returns - /// `Binding::Dynamic(candidates)`, return type = LUB of contributions - /// (pure-scalar LUBs declared returns; pure-vec LUBs per position and - /// wraps in `Tuple<…>`; mixed scalar+vec collapses to `Any`). + /// `Binding::Dynamic(candidates)`, return type = the join of the + /// contributions (pure-scalar joins the declared returns; pure-vec + /// joins per position and wraps in `Tuple<…>`; mixed scalar+vec + /// collapses to `Any`). /// 4. **Same-name callables, no signature match** — last-resort /// fallback so `Any`-typed callees still get a candidate list. /// Returns `Binding::Dynamic`, return type = `Any`. @@ -299,7 +374,7 @@ impl<'a> Overloads<'a> { if walk .all_by_name .iter() - .any(|&symbol| self.could_accept_call(symbol, sig)) + .any(|&symbol| could_take(&self.type_of(symbol), sig)) { let candidates = walk .all_by_name @@ -328,20 +403,32 @@ impl<'a> Overloads<'a> { let mut all_by_name = Vec::new(); for group in self.reachable(candidates) { + let compatible = || { + let compatible = group + .iter() + .copied() + .filter(|&symbol| self.is_compatible(symbol, sig)) + .collect::>(); + self.most_specific_first(compatible) + }; if let Some(exact) = self.most_specific_match(group, sig) { + // An overload in this scope takes the call, so it's this + // scope's, but a more specific one here may take it as the + // program runs, which then picks among them. + let (exact, loose) = if self.a_more_specific_one_could_fit(group, exact, sig) { + (None, Some(compatible())) + } else { + (Some(exact), None) + }; return ScalarWalk { - exact: Some(exact), - loose: None, + exact, + loose, all_by_name: Vec::new(), }; } if loose.is_none() { - let compatible = group - .iter() - .copied() - .filter(|&symbol| self.is_compatible(symbol, sig)) - .collect::>(); + let compatible = compatible(); if !compatible.is_empty() { loose = Some(compatible); } @@ -353,7 +440,7 @@ impl<'a> Overloads<'a> { .iter() .rev() .copied() - .filter(|&symbol| self.types.get_type(symbol).could_be_callable()), + .filter(|&symbol| self.type_of(symbol).could_be_callable()), ); } @@ -369,11 +456,10 @@ impl<'a> Overloads<'a> { /// /// Resolution proceeds in two stages: /// 1. Filter to overloads whose declared signature accepts `sig` - /// (the usual subtype check via [`StaticType::is_fn_and_matches`]). + /// (each argument [`StaticType::fits`] its parameter). /// 2. Among the survivors, pick the one whose parameter signature is /// most specific — i.e. not strictly dominated by any other match's - /// parameter signature in the subtype partial order. A signature `b` - /// strictly dominates `a` when `b <: a` and `a` is not `<: b`. + /// parameter signature (see [`is_strictly_more_specific`]). /// /// Variadic overloads (`parameters: None`) match anything but are treated /// as the least specific shape: any concrete overload that also matches @@ -386,55 +472,95 @@ impl<'a> Overloads<'a> { let matches = group .iter() .copied() - .filter(|&symbol| self.types.get_type(symbol).is_fn_and_matches(sig)) + .filter(|&symbol| self.type_of(symbol).is_fn_and_matches(sig)) .collect::>(); - let params_of = |symbol: SymbolId| -> Option<&[StaticType]> { - match self.types.get_type(symbol) { - StaticType::Function { - parameters: Some(p), - .. - } => Some(p.as_slice()), - // Variadic / non-function bindings have no concrete shape. - _ => None, - } - }; - // `group` is newest first, so ties resolve to the most-recently registered overload. matches.iter().copied().find(|&i| { - let Some(pi) = params_of(i) else { + let Some(pi) = self.parameters_of(i) else { // Variadic only wins if every other match is also variadic. - return matches.iter().all(|&j| params_of(j).is_none()); + return matches.iter().all(|&j| self.parameters_of(j).is_none()); }; matches.iter().all(|&j| { if i == j { return true; } - let Some(pj) = params_of(j) else { + let Some(pj) = self.parameters_of(j) else { // Variadic never dominates a concrete overload. return true; }; - !is_strictly_more_specific(pj, pi) + !is_strictly_more_specific(&pj, &pi) }) }) } - /// Whether `symbol` is a function whose parameters don't provably conflict - /// with `sig`, so it might accept the call at runtime. - fn is_compatible(self, symbol: SymbolId, sig: &[StaticType]) -> bool { - let StaticType::Function { parameters, .. } = self.types.get_type(symbol) else { + /// The parameter types of `symbol`, if it's a function that isn't + /// variadic. + pub(crate) fn parameters_of(self, symbol: SymbolId) -> Option> { + match self.type_of(symbol).into_owned() { + StaticType::Function { + parameters: Some(parameters), + .. + } => Some(parameters), + _ => None, + } + } + + /// Whether an overload in `group` more specific than `exact`, which + /// accepts `sig`, might accept the call when the program runs. An + /// argument of type `Any` fits `show(x: Any)`, but if it's an Int when + /// the program runs, `show(x: Int)` is the overload that takes it. + fn a_more_specific_one_could_fit( + self, + group: &[SymbolId], + exact: SymbolId, + sig: &[StaticType], + ) -> bool { + let Some(exact) = self.parameters_of(exact) else { return false; }; - let Some(parameters) = parameters else { - unreachable!( - "a variadic function always matches exactly before compatibility is checked" - ); - }; - parameters.len() == sig.len() - && parameters + group.iter().any(|&other| { + self.parameters_of(other).is_some_and(|other_parameters| { + is_strictly_more_specific(&other_parameters, &exact) + && self.is_compatible(other, sig) + }) + }) + } + + /// `symbols` with each one before every one it's more specific than, + /// and otherwise in the order given. The program tries the overloads it + /// picks from as it runs in this order, and takes the first that + /// accepts the arguments. Overloads in one scope don't tie, so of those + /// that accept them, one is more specific than the others, and it's the + /// first. + pub(crate) fn most_specific_first(self, mut symbols: Vec) -> Vec { + let mut ordered = Vec::with_capacity(symbols.len()); + while !symbols.is_empty() { + let next = symbols .iter() - .zip(sig) - .all(|(param, arg)| !param.is_incompatible_with(arg)) + .position(|&symbol| { + let Some(parameters) = self.parameters_of(symbol) else { + // A variadic function is the least specific. + return symbols + .iter() + .all(|&other| self.parameters_of(other).is_none()); + }; + symbols.iter().all(|&other| { + self.parameters_of(other) + .is_none_or(|other| !is_strictly_more_specific(&other, ¶meters)) + }) + }) + .unwrap_or(0); + ordered.push(symbols.remove(next)); + } + ordered + } + + /// Whether `symbol` is a function whose parameters don't provably conflict + /// with `sig`, so it might accept the call at runtime. + fn is_compatible(self, symbol: SymbolId, sig: &[StaticType]) -> bool { + let typ = self.type_of(symbol); + matches!(typ.as_ref(), StaticType::Function { .. }) && could_take(&typ, sig) } /// Try to resolve a vec candidate set for `sig`. @@ -452,7 +578,7 @@ impl<'a> Overloads<'a> { positions, }); } - if !sig.iter().any(|t| matches!(t, StaticType::Any)) { + if !sig.iter().any(StaticType::is_unknown) { return None; } let permissive: Vec = vec![StaticType::Any; sig.len()]; @@ -511,46 +637,30 @@ impl<'a> Overloads<'a> { out } - /// Whether a candidate could still accept a call with this signature at - /// runtime: its type is `Any`, a variadic function, or a function whose - /// parameters don't provably conflict with `sig`. - fn could_accept_call(self, symbol: SymbolId, sig: &[StaticType]) -> bool { - match self.types.get_type(symbol) { - StaticType::Function { - parameters: Some(parameters), - .. - } => { - parameters.len() == sig.len() - && parameters - .iter() - .zip(sig) - .all(|(param, arg)| !param.is_incompatible_with(arg)) - } - StaticType::Function { - parameters: None, .. - } - | StaticType::Any => true, - _ => false, - } - } - /// Return type of a scalar candidate. Falls back to `Any` if the binding /// turned out not to carry a function type (e.g. `Any`-typed callee). + /// + /// A type parameter such as `T` in `first(List) -> T` is `Any` + /// here: only a call that commits to the overload knows what it stands + /// for, and one picked at runtime never does. fn scalar_return_type(self, symbol: SymbolId) -> StaticType { - match self.types.get_type(symbol) { - StaticType::Function { return_type, .. } => return_type.as_ref().clone(), + match self.type_of(symbol).as_ref() { + StaticType::Function { return_type, .. } => self + .vars + .resolve(return_type) + .replace(&|part| matches!(part, StaticType::Generic(_)).then_some(StaticType::Any)), _ => StaticType::Any, } } - /// LUB the return types of a list of scalar candidates. Used when every + /// Join the return types of a list of scalar candidates. Used when every /// candidate in a Dynamic binding came from the compat-filtered walk and /// is therefore guaranteed to be a function. - fn lub_scalar_returns(self, symbols: &[SymbolId]) -> StaticType { + fn join_scalar_returns(self, symbols: &[SymbolId]) -> StaticType { symbols .iter() .map(|&symbol| self.scalar_return_type(symbol)) - .reduce(|a, b| a.lub(&b)) + .reduce(|a, b| a.join(&b)) .unwrap_or(StaticType::Any) } @@ -563,14 +673,14 @@ impl<'a> Overloads<'a> { ) -> StaticType { let has_scalar = !scalar_loose.is_empty(); - // Pure scalar: LUB the declared returns. This is the precision-recovery - // case PR #140 had to widen to `Any` for soundness; routing it through - // the LUB now is safe because we know none of the candidates can vec. + // Pure scalar: join the declared returns. This is the precision-recovery + // case PR #140 had to widen to `Any` for soundness; joining them now is + // safe because we know none of the candidates can vec. if has_scalar && vec_resolution.is_none() { - return self.lub_scalar_returns(scalar_loose); + return self.join_scalar_returns(scalar_loose); } - // Pure static vec: per-position LUB → Tuple<…> of element returns. + // Pure static vec: per-position join → Tuple<…> of element returns. if !has_scalar && let Some(VecResolution::Static { axis_len, @@ -579,16 +689,16 @@ impl<'a> Overloads<'a> { { let elements: Vec = positions .iter() - .map(|pos| self.lub_scalar_returns(pos)) + .map(|pos| self.join_scalar_returns(pos)) .collect(); debug_assert_eq!(elements.len(), *axis_len); return StaticType::Tuple(elements); } // Mixed scalar+vec, Any-fallback vec, or empty (defensive): the - // result has no useful upper bound — widen to `Any`. `LUB(scalar, - // Tuple<…>)` collapses to `Any` in our lattice anyway, so there's - // nothing more precise to compute for the mixed case. + // result has no type in common — widen to `Any`. A scalar and a + // `Tuple<…>` join as `Any` anyway, so there's nothing more precise to + // compute for the mixed case. StaticType::Any } } @@ -596,7 +706,6 @@ impl<'a> Overloads<'a> { #[cfg(test)] mod tests { use super::*; - use crate::types::TypeBinding; use ndc_resolver::{BindingKind, SymbolTable}; use std::cell::RefCell; @@ -631,7 +740,7 @@ mod tests { .into_iter() .map(|typ| { let symbol = symbol(BindingKind::Function); - table.declare(symbol, TypeBinding::Inferred(typ)); + table.declare(symbol, typ); symbol }) .collect(); @@ -655,8 +764,9 @@ mod tests { sig: &[StaticType], kind: CallKind, ) -> ResolvedCall { - SYMBOLS - .with_borrow(|table| Overloads::new(types, table).resolve_call(candidates, sig, kind)) + SYMBOLS.with_borrow(|table| { + Overloads::new(types, &TypeVars::default(), table).resolve_call(candidates, sig, kind) + }) } /// Asserts the call picks `expected` as its single overload. @@ -679,8 +789,8 @@ mod tests { // version must win even though the element version was registered later. #[test] fn more_specific_overload_wins_in_same_scope() { - let seq_any = StaticType::Sequence(Box::new(StaticType::Any)); - let fn_any = variadic_fn(StaticType::Any); + let seq_any = StaticType::Iterable(Box::new(StaticType::Any)); + let fn_any = variadic_fn(StaticType::Generic("U".into())); let (types, locates) = table(vec![ fn_type(vec![seq_any.clone(), fn_any], StaticType::Any), fn_type(vec![seq_any, StaticType::Any], StaticType::Any), @@ -778,7 +888,59 @@ mod tests { foos.iter() .all(|&foo| picked.contains(&Candidate::Scalar(foo))) ); - assert_eq!(resolved.return_type, list_of(StaticType::Any)); + // A List and a List have no type in common but `Any`. + assert_eq!(resolved.return_type, StaticType::Any); + } + + #[test] + fn an_unknown_argument_leaves_a_fallback_to_the_program_with_the_more_specific_first() { + // An `Any` argument fits `show(Any)`, but when it's an Int as the + // program runs, `show(Int)` takes it. The program takes the first + // overload that accepts the arguments, so that one comes first, + // whichever was declared first. + let (types, shows) = table(vec![ + fn_type(vec![StaticType::Int], StaticType::String), + fn_type(vec![StaticType::Any], StaticType::String), + ]); + for group in [[shows[0], shows[1]], [shows[1], shows[0]]] { + let resolved = resolve( + &types, + &candidates(&[&group]), + &[StaticType::Any], + CallKind::Regular, + ); + assert_eq!( + resolved.binding, + Binding::Dynamic(vec![ + Candidate::Scalar(shows[0]), + Candidate::Scalar(shows[1]) + ]), + "declared in the order {group:?}" + ); + } + } + + #[test] + fn a_fallback_left_to_the_program_still_hides_the_overloads_further_out() { + // The inner `show(Any)` takes every call, so the outer one never does. + let (types, shows) = table(vec![ + fn_type(vec![StaticType::Any], StaticType::String), + fn_type(vec![StaticType::Int], StaticType::String), + fn_type(vec![StaticType::Any], StaticType::String), + ]); + let resolved = resolve( + &types, + &candidates(&[&shows[1..], &shows[..1]]), + &[StaticType::Any], + CallKind::Regular, + ); + assert_eq!( + resolved.binding, + Binding::Dynamic(vec![ + Candidate::Scalar(shows[1]), + Candidate::Scalar(shows[2]) + ]), + ); } #[test] @@ -824,14 +986,12 @@ mod tests { fn a_variable_holding_a_function_ends_the_search() { let (mut types, fns) = table(vec![fn_type(vec![StaticType::Int], StaticType::Int)]); let variable = symbol(BindingKind::Plain); - types.declare( - variable, - TypeBinding::Inferred(fn_type(vec![StaticType::String], StaticType::Int)), - ); + types.declare(variable, fn_type(vec![StaticType::String], StaticType::Int)); let candidates = candidates(&[&[fns[0], variable]]); SYMBOLS.with_borrow(|table| { - let overloads = Overloads::new(&types, table); + let vars = TypeVars::default(); + let overloads = Overloads::new(&types, &vars, table); let resolved = overloads.resolve_call(&candidates, &[StaticType::Int], CallKind::Regular); assert_eq!(resolved.binding, Binding::None); @@ -846,13 +1006,11 @@ mod tests { fn a_variable_ending_the_search_hides_only_the_functions_further_out() { let (mut types, fns) = table(vec![fn_type(vec![StaticType::Int], StaticType::Int)]); let variable = symbol(BindingKind::Plain); - types.declare( - variable, - TypeBinding::Inferred(fn_type(vec![StaticType::String], StaticType::Int)), - ); + types.declare(variable, fn_type(vec![StaticType::String], StaticType::Int)); SYMBOLS.with_borrow(|table| { - let overloads = Overloads::new(&types, table); + let vars = TypeVars::default(); + let overloads = Overloads::new(&types, &vars, table); assert!(overloads.hides_functions(&candidates(&[&[variable], &[fns[0]]]))); assert!(!overloads.hides_functions(&candidates(&[&[variable]]))); }); @@ -865,13 +1023,14 @@ mod tests { fn a_variable_ends_the_search_whatever_its_type() { let (mut types, fns) = table(vec![fn_type(vec![StaticType::Int], StaticType::Int)]); let variable = symbol(BindingKind::Plain); - types.declare(variable, TypeBinding::Inferred(StaticType::Int)); + types.declare(variable, StaticType::Int); let candidates = candidates(&[&[variable], &[fns[0]]]); let resolved = resolve(&types, &candidates, &[StaticType::Int], CallKind::Regular); assert_eq!(resolved.binding, Binding::None); SYMBOLS.with_borrow(|table| { - let overloads = Overloads::new(&types, table); + let vars = TypeVars::default(); + let overloads = Overloads::new(&types, &vars, table); assert_eq!( overloads.variable_ending_search(&candidates), Some(variable) diff --git a/ndc_analyser/src/types.rs b/ndc_analyser/src/types.rs index b4c742c7..a7e68ed7 100644 --- a/ndc_analyser/src/types.rs +++ b/ndc_analyser/src/types.rs @@ -1,88 +1,1035 @@ -use ahash::AHashMap; -use ndc_core::StaticType; +use ahash::{AHashMap, AHashSet}; +use ndc_core::{StaticType, TypeVar}; +use ndc_lexer::Span; use ndc_resolver::SymbolId; -#[derive(Debug, Clone)] -pub(crate) enum TypeBinding { - Inferred(StaticType), - Annotated(StaticType), -} - -impl TypeBinding { - pub fn typ(&self) -> &StaticType { - match self { - Self::Inferred(t) | Self::Annotated(t) => t, - } - } - - pub fn is_annotated(&self) -> bool { - matches!(self, Self::Annotated(_)) - } -} - /// The type of every value the name resolver declared, natives included, -/// keyed by its symbol. An assignment can widen an inferred type, but an -/// annotated one is fixed. +/// keyed by its symbol. A value keeps its type: an assignment must fit it. #[derive(Debug, Default)] pub(crate) struct TypeTable { - types: AHashMap, + types: AHashMap, } impl TypeTable { - /// Records the type of a value the resolver declared. - pub(crate) fn declare(&mut self, symbol: SymbolId, binding: TypeBinding) { - self.types.insert(symbol, binding); + /// Records the type of a value the resolver declared. A function's + /// placeholder type is replaced this way once its body is analysed. + pub(crate) fn declare(&mut self, symbol: SymbolId, typ: StaticType) { + self.types.insert(symbol, typ); } pub(crate) fn get_type(&self, symbol: SymbolId) -> &StaticType { self.types .get(&symbol) .expect("internal error: a value is analysed after its declaration") - .typ() } - /// Try to update a value's type. Returns `Err` with the annotated type if - /// the value has an explicit type annotation and cannot be widened. - pub(crate) fn update_binding_type( + /// Replaces the settled type variables in every value's type with what + /// they stand for. + pub(crate) fn resolve_all(&mut self, vars: &TypeVars) { + for typ in self.types.values_mut() { + *typ = vars.resolve(typ); + debug_assert!(!typ.has_vars(), "unsettled type variable in {typ}"); + } + } +} + +/// What each type variable of the analysis stands for once something settled +/// it. In `let xs = []; xs[0] = 1;`, the element type of `xs` is a variable +/// that the assignment settles as `Int`. +#[derive(Debug, Default)] +pub(crate) struct TypeVars { + /// What each variable stands for, once settled. + solutions: Vec>, + /// What each variable becomes if nothing settles it: `Any` for the + /// elements of `[]`, which is what `[]` was typed as before. + fallbacks: Vec, + /// The variables in the order they were settled, so a [`Snapshot`] can + /// unsettle the ones settled after it, and the solver can tell which + /// ones were settled since it last looked. + trail: Vec, + /// How deep in generic functions each variable belongs: 0 for the + /// program's top level, 1 for the body of a top-level `fn`. A variable + /// that a function's body made but that something outside it now refers + /// to, like `n` in `let xs = []; fn f(n) { xs.push(n) }`, belongs outside. + levels: Vec, + /// The level new variables get. + level: u32, + /// The levels lowered so far, with what each was before, so a + /// [`Snapshot`] can raise them again. + lowered: Vec<(TypeVar, u32)>, + /// What each variable may stand for beyond what unifying says, which + /// settling it checks: the type of the literal `1` is an `Int`, a + /// `Float` or a `Number`. + bounds: Vec>, + /// The variables a bound was added to after they were made, in order, + /// so a [`Snapshot`] can take the bounds off again. + bounded: Vec, + /// The variables that got a bound, in the order they got it, which are + /// the types of literals. + literals: Vec, +} + +/// What a variable may stand for, beyond what unifying says. +#[derive(Debug, Clone)] +enum Bound { + /// It's the type of an integer literal, like `1`: an `Int`, a `Float` + /// or a `Number`, or `Any`, which takes anything. It's an `Int` unless + /// something says otherwise. With the literal's type as it was made, + /// and where the literal is. + Number(TypeVar, Span), + /// It's the type of the elements of a literal that holds a value of + /// this type, like `[1, 2.5]`, so the value has to fit it. With the + /// literal, as the type of the elements it was made for, and where the + /// value is. + Holds(StaticType, TypeVar, Span), +} + +/// The state of the variables at one point, which [`TypeVars::rollback`] +/// returns to: overload resolution tries each candidate this way. +#[derive(Debug, Clone, Copy)] +pub(crate) struct Snapshot { + vars: usize, + trail: usize, + lowered: usize, + bounded: usize, +} + +impl TypeVars { + /// A new variable, which becomes `fallback` if nothing settles it. + pub(crate) fn fresh(&mut self, fallback: StaticType) -> StaticType { + let var = self.next_var(); + self.solutions.push(None); + self.fallbacks.push(fallback); + self.levels.push(self.level); + self.bounds.push(Vec::new()); + StaticType::Var(var) + } + + /// The type of an integer literal, like `1`, which its uses settle as an + /// `Int`, a `Float` or a `Number`: `1` in `[1.5, 1]` is a Float. It's an + /// `Int` unless something says otherwise. + pub(crate) fn fresh_number(&mut self, span: Span) -> StaticType { + let literal = self.next_var(); + self.fresh_bounded(StaticType::Int, vec![Bound::Number(literal, span)]) + } + + /// The variable [`Self::fresh`] makes next. + fn next_var(&self) -> TypeVar { + TypeVar(u32::try_from(self.solutions.len()).expect("fewer than 2^32 type variables")) + } + + /// The type of the elements of a literal holding values of the types + /// `held`, which its uses settle as any type each value fits: in + /// `let xs: List = [1, 2]`, `Any`. Unless something says otherwise + /// it's the type the values share, or `Any` if they share none. + pub(crate) fn fresh_holding(&mut self, held: Vec<(StaticType, Span)>) -> StaticType { + let literal = self.next_var(); + self.fresh_bounded( + StaticType::Any, + held.into_iter() + .map(|(typ, span)| Bound::Holds(typ, literal, span)) + .collect(), + ) + } + + /// A literal whose type the literal type `var` holds values of and that + /// isn't settled yet, as `1`'s is in `[1]`. + pub(crate) fn held_literal(&self, var: TypeVar) -> Option { + self.held_literals(var).into_iter().next() + } + + /// The unsettled literal types that each unsettled literal type is + /// held by: the element type of `[1]` holds the `1`'s. + pub(crate) fn holders(&self) -> AHashMap> { + let mut holders = AHashMap::>::new(); + let mut seen = AHashSet::new(); + for &holder in &self.literals { + if (holder.0 as usize) >= self.solutions.len() + || !self.is_literal(holder) + || !seen.insert(holder) + { + continue; + } + for held in self.held_literals(holder) { + let held_by = holders.entry(held).or_default(); + if !held_by.contains(&holder) { + held_by.push(holder); + } + } + } + holders + } + + /// The unsettled literal types whose values the literal type `var` + /// holds: the `1`'s and the `2`'s in `[1, 2]`. + pub(crate) fn held_literals(&self, var: TypeVar) -> Vec { + let mut held_literals = Vec::new(); + let mut seen = AHashSet::new(); + for bound in &self.bounds[var.0 as usize] { + if let Bound::Holds(held, _, _) = bound + && let StaticType::Var(held) = self.resolve(held) + && held != var + && self.is_literal(held) + && seen.insert(held) + { + held_literals.push(held); + } + } + held_literals + } + + /// The values the literal type `var` holds, with where each is. + pub(crate) fn held(&self, var: TypeVar) -> Vec<(StaticType, Span)> { + self.bounds[var.0 as usize] + .iter() + .map(|bound| match bound { + Bound::Holds(typ, _, span) => (typ.clone(), *span), + Bound::Number(_, span) => (StaticType::Int, *span), + }) + .collect() + } + + /// Whether the literal type `var` holds the values of more than one + /// literal, as it does once `v = [1]` makes it the type of `v`'s + /// `["s"]` too. Values of one literal that share no type are held as + /// `Any`, but two literals of different types are an error. + pub(crate) fn holds_several_literals(&self, var: TypeVar) -> bool { + let mut literals = self.bounds[var.0 as usize].iter().map(|bound| match bound { + Bound::Holds(_, literal, _) | Bound::Number(literal, _) => *literal, + }); + literals + .next() + .is_some_and(|first| literals.any(|literal| literal != first)) + } + + fn fresh_bounded(&mut self, fallback: StaticType, bounds: Vec) -> StaticType { + let typ = self.fresh(fallback); + let StaticType::Var(var) = typ else { + unreachable!("a fresh type is a variable"); + }; + self.bounds[var.0 as usize] = bounds; + self.literals.push(var); + typ + } + + /// The unsettled types of literals that `belongs` says to settle, the + /// ones holding values before the numbers, so a value can still say + /// what a number is: `1` in `[1, 2.5]` is a Float. + pub(crate) fn unsettled_literals( + &self, + belongs: impl Fn(&Self, TypeVar) -> bool, + ) -> Vec { + let mut literals = Vec::new(); + let mut seen = AHashSet::new(); + for &var in &self.literals { + if (var.0 as usize) < self.solutions.len() + && self.solution(var).is_none() + && !self.bounds[var.0 as usize].is_empty() + && belongs(self, var) + && seen.insert(var) + { + literals.push(var); + } + } + // What the literals a literal holds take decides whether its uses + // fit, so they're settled last: in `n[0]` on `n = [1]`, whether `[]` + // gives the `1` back depends on the index `0` being an Int. + let held = literals + .iter() + .flat_map(|&var| self.bounds[var.0 as usize].iter()) + .filter_map(|bound| match bound { + Bound::Holds(held, _, _) => match self.resolve(held) { + StaticType::Var(held) => Some(held), + _ => None, + }, + Bound::Number(..) => None, + }) + .collect::>(); + literals.sort_by_cached_key(|&var| (self.is_number(var), held.contains(&var))); + literals + } + + /// Forgets the types of literals, once they're all settled at the end + /// of a batch. + pub(crate) fn forget_literals(&mut self) { + debug_assert!( + self.literals + .iter() + .all(|&var| var.0 as usize >= self.solutions.len() || !self.is_literal(var)), + "a literal's type is left unsettled" + ); + self.literals.clear(); + } + + /// Whether `var` is the type of an integer literal. + pub(crate) fn is_number(&self, var: TypeVar) -> bool { + self.bounds[var.0 as usize] + .iter() + .any(|bound| matches!(bound, Bound::Number(..))) + } + + /// What the unsettled literal type `var` could be when nothing says + /// otherwise, best first. `None` if that isn't known yet: a list of + /// numbers is a list of whatever the numbers are, which is up to them. + pub(crate) fn literal_defaults(&mut self, var: TypeVar) -> Option> { + let mut defaults = Vec::new(); + let mut waits = false; + let mut unknown = None; + // The literal made first says first what the type is: after `let v + // = ["s"]; v = [1]`, it's what `v` was. + let mut bounds = self.bounds[var.0 as usize].iter().collect::>(); + bounds.sort_by_key(|bound| match bound { + Bound::Holds(_, literal, _) | Bound::Number(literal, _) => literal.0, + }); + for bound in bounds { + let held = match bound { + Bound::Number(..) => { + for number in [StaticType::Int, StaticType::Float, StaticType::Number] { + if !defaults.contains(&number) { + defaults.push(number); + } + } + continue; + } + Bound::Holds(held, _, _) => held, + }; + match self.resolve(held) { + StaticType::Var(held) if self.is_literal(held) => waits = true, + // A value whose type isn't known holds what its type is: + // `fn wrap(x) => [x]` returns a list of what `x` is. + StaticType::Var(held) => { + unknown.get_or_insert(StaticType::Var(held)); + } + // A value that never arrives says nothing about the type. + StaticType::Never => {} + held => { + if !defaults.contains(&held) { + defaults.push(held); + } + } + } + } + if defaults.is_empty() && waits { + return None; + } + if defaults.is_empty() { + // Values that share no type are held as `Any`, which settling + // the literal's type with a default it doesn't take falls back + // to. + defaults.push(unknown.unwrap_or(StaticType::Any)); + } else if !defaults.contains(&StaticType::Any) && self.a_literal_holds_any(var) { + // A literal whose values share no type is held as `Any`, which + // any literal can be too: after `let v = [1, "a"]`, `v = ["b"]` + // holds `Any`s like `v` does. + defaults.push(StaticType::Any); + } + Some(defaults) + } + + /// Whether a literal whose values `var` holds holds values that share no + /// type, as `[1, "a"]` does, and is held as `Any` on its own. + fn a_literal_holds_any(&mut self, var: TypeVar) -> bool { + let mut literals = Vec::new(); + for bound in &self.bounds[var.0 as usize] { + match bound { + Bound::Holds(held, literal, _) => match literals + .iter_mut() + .find(|(found, _): &&mut (TypeVar, Vec)| found == literal) + { + Some((_, held_by_it)) => held_by_it.push(held.clone()), + None => literals.push((*literal, vec![held.clone()])), + }, + // A number literal is a number. + Bound::Number(..) => {} + } + } + literals.into_iter().any(|(_, held)| { + let snapshot = self.snapshot(); + let shared = held[1..].iter().all(|other| self.unify(&held[0], other)); + self.rollback(snapshot); + !shared + }) + } + + /// Whether the unsettled literal type `var` could still be settled as + /// one of its defaults, or waits for the literals it holds to say. + fn some_default_fits(&mut self, var: TypeVar) -> bool { + let Some(defaults) = self.literal_defaults(var) else { + return true; + }; + let snapshot = self.snapshot(); + let fits = defaults + .iter() + .any(|default| self.try_unify(&StaticType::Var(var), default)); + self.rollback(snapshot); + fits + } + + /// What the unsettled literal type `var` is shown as: what it would be + /// if nothing said otherwise, as far as that's known. + pub(crate) fn preview(&self, var: TypeVar) -> StaticType { + if self.is_number(var) { + return StaticType::Int; + } + let mut held = Vec::new(); + for bound in &self.bounds[var.0 as usize] { + // A literal that holds itself, like `[xs]` pushed to `xs`, says + // nothing more about what it is. + if let Bound::Holds(typ, _, _) = bound + && !self + .resolve(typ) + .contains(&|part| *part == StaticType::Var(var)) + { + let shown = self.shown(typ); + if !held.contains(&shown) { + held.push(shown); + } + } + } + match &held[..] { + [only] => only.clone(), + _ => StaticType::Any, + } + } + + /// Whether `var` is unsettled and has bounds, as a literal's type does. + pub(crate) fn is_literal(&self, var: TypeVar) -> bool { + self.solution(var).is_none() && !self.bounds[var.0 as usize].is_empty() + } + + /// Whether `typ` has a part that's a literal's type not settled yet. + pub(crate) fn has_unsettled_literals(&self, typ: &StaticType) -> bool { + self.unsettled(typ) + .into_iter() + .any(|var| self.is_literal(var)) + } + + /// Settles `var` as `typ`, and returns whether `typ` is within its + /// bounds, as [`Self::unify`] does. + pub(crate) fn settle_literal(&mut self, var: TypeVar, typ: &StaticType) -> bool { + self.unify(&StaticType::Var(var), typ) + } + + /// Starts the body of a generic function: the variables made until + /// [`Self::leave_level`] belong to it. + pub(crate) fn enter_level(&mut self) { + self.level += 1; + } + + /// Ends the body [`Self::enter_level`] started. The variables it made + /// that nothing outside refers to can then be generalized. + pub(crate) fn leave_level(&mut self) { + self.level -= 1; + } + + /// Whether `var` is unsettled and belongs to a generic function's body + /// that has ended, so it can become one of the function's type + /// parameters. + pub(crate) fn is_generalizable(&self, var: TypeVar) -> bool { + self.solution(var).is_none() && self.levels[var.0 as usize] > self.level + } + + /// Settles `var` as the type parameter `name` of the generic function + /// whose body made it. + pub(crate) fn generalize(&mut self, var: TypeVar, name: &str) { + debug_assert!( + !self.is_literal(var), + "a literal's type is a type parameter" + ); + self.settle(var, StaticType::Generic(name.into())); + } + + /// Whether `typ` is made from the unsettled variable `var`, directly or + /// through what a literal in it holds: `[x]` is made from `x`'s type. + pub(crate) fn is_made_from(&self, typ: &StaticType, var: TypeVar) -> bool { + let mut seen = AHashSet::new(); + let mut todo = self.unsettled(typ); + while let Some(next) = todo.pop() { + if next == var { + return true; + } + if seen.insert(next) && self.is_literal(next) { + todo.extend(self.held_vars(next)); + } + } + false + } + + /// The variables in what the literal type `var` holds: `x`'s in `[x, 1]`. + pub(crate) fn held_vars(&self, var: TypeVar) -> Vec { + self.bounds[var.0 as usize] + .iter() + .flat_map(|bound| match bound { + Bound::Holds(held, _, _) => self.unsettled(held), + Bound::Number(..) => Vec::new(), + }) + .collect() + } + + /// How many variables there are, so [`Self::settle_from`] can settle + /// the ones made after this point. + pub(crate) fn count(&self) -> usize { + self.solutions.len() + } + + fn solution(&self, var: TypeVar) -> Option<&StaticType> { + self.solutions[var.0 as usize].as_ref() + } + + /// Settles `var` as `typ`, and returns whether `typ` is within the + /// bounds of `var`. When `typ` is another variable, it gets them. On a + /// mismatch, `var` and the variables settled meanwhile stay settled. + fn settle(&mut self, var: TypeVar, typ: StaticType) -> bool { + debug_assert!(self.solution(var).is_none(), "{var:?} is settled twice"); + // What `var` stands for now belongs where `var` does. + let level = self.levels[var.0 as usize]; + if level < self.level { + for inner in self.unsettled(&typ) { + let inner_level = self.levels[inner.0 as usize]; + if inner_level > level { + self.lowered.push((inner, inner_level)); + self.levels[inner.0 as usize] = level; + } + } + } + let shallow = self.shallow(&typ); + self.solutions[var.0 as usize] = Some(typ); + self.trail.push(var); + let bounds = self.bounds[var.0 as usize].clone(); + if bounds.is_empty() { + return true; + } + match shallow { + StaticType::Var(other) => { + for bound in bounds { + // A literal holding a value of the type the other + // variable already is says nothing more. + if let Bound::Holds(held, _, _) = &bound + && self.shallow(held) == StaticType::Var(other) + { + continue; + } + self.bounds[other.0 as usize].push(bound); + self.bounded.push(other); + self.literals.push(other); + } + // The values of two literals have to have a type in common: + // `["s"]` and `[1]` can't be one literal's type. + !self.holds_several_literals(other) || self.some_default_fits(other) + } + typ => bounds.iter().all(|bound| match bound { + Bound::Number(..) => matches!( + typ, + StaticType::Int + | StaticType::Float + | StaticType::Number + | StaticType::Any + | StaticType::Error + ), + Bound::Holds(held, _, _) => self.bind_argument(&typ, held), + }), + } + } + + /// How many settlings there have been, to pass to + /// [`Self::settled_since`] later. + pub(crate) fn settled_count(&self) -> usize { + self.trail.len() + } + + /// The variables settled since there were `count` settlings. A rollback + /// only undoes settlings made after the snapshot it returns to, so + /// `count` from before a snapshot stays valid. + pub(crate) fn settled_since(&self, count: usize) -> &[TypeVar] { + &self.trail[count.min(self.trail.len())..] + } + + /// The current state, to [`Self::rollback`] to later. + pub(crate) fn snapshot(&self) -> Snapshot { + Snapshot { + vars: self.solutions.len(), + trail: self.trail.len(), + lowered: self.lowered.len(), + bounded: self.bounded.len(), + } + } + + /// Unsettles what was settled since `snapshot`, and forgets the variables + /// made since. + pub(crate) fn rollback(&mut self, snapshot: Snapshot) { + for var in self.trail.drain(snapshot.trail..) { + self.solutions[var.0 as usize] = None; + } + for (var, level) in self.lowered.drain(snapshot.lowered..).rev() { + self.levels[var.0 as usize] = level; + } + for var in self.bounded.drain(snapshot.bounded..).rev() { + if (var.0 as usize) < snapshot.vars { + self.bounds[var.0 as usize].pop(); + } + } + self.solutions.truncate(snapshot.vars); + self.fallbacks.truncate(snapshot.vars); + self.levels.truncate(snapshot.vars); + self.bounds.truncate(snapshot.vars); + } + + /// Unifies `a` and `b` if they can be made the same type, and otherwise + /// leaves every variable as it was. + pub(crate) fn try_unify(&mut self, a: &StaticType, b: &StaticType) -> bool { + let snapshot = self.snapshot(); + let unified = self.unify(a, b); + if !unified { + self.rollback(snapshot); + } + unified + } + + /// The unsettled variables in `typ`, each once, outermost first. + pub(crate) fn unsettled(&self, typ: &StaticType) -> Vec { + let unsettled = std::cell::RefCell::new(Vec::new()); + self.resolve(typ).contains(&|part| { + if let StaticType::Var(var) = part + && !unsettled.borrow().contains(var) + { + unsettled.borrow_mut().push(*var); + } + false + }); + unsettled.into_inner() + } + + /// `typ` as an error shows it: the parts not known yet could still be + /// anything, so they're shown as `Any`, except a literal's, which is + /// shown as what it is unless something says otherwise: `[1]` as a + /// `List`. + pub(crate) fn shown(&self, typ: &StaticType) -> StaticType { + self.resolve(typ).replace(&|part| match part { + StaticType::Var(var) if self.is_literal(*var) => Some(self.preview(*var)), + StaticType::Var(_) => Some(StaticType::Any), + _ => None, + }) + } + + /// Settles each of `vars` that is still unsettled with its fallback. A + /// literal's type gets the first of its defaults within its bounds. + pub(crate) fn settle_with_fallbacks(&mut self, vars: &[TypeVar]) { + for &var in vars { + if self.solution(var).is_some() { + continue; + } + let defaults = match self.literal_defaults(var) { + Some(defaults) if !self.bounds[var.0 as usize].is_empty() => defaults, + _ => vec![self.fallbacks[var.0 as usize].clone()], + }; + if !defaults + .iter() + .any(|default| self.try_unify(&StaticType::Var(var), default)) + { + // `Any` takes anything a literal holds. + self.settle(var, StaticType::Any); + } + } + } + + /// `typ` with every settled variable replaced by what it stands for. + pub(crate) fn resolve(&self, typ: &StaticType) -> StaticType { + typ.replace(&|part| match part { + StaticType::Var(var) => self.solution(*var).map(|solution| self.resolve(solution)), + _ => None, + }) + } + + /// `typ` as the analysis hands it on, once every variable in it is + /// settled: [`Self::resolve`]d, and with each failed check's type shown + /// as `Any`, which is what's known about it. + pub(crate) fn finish(&self, typ: &StaticType) -> StaticType { + typ.replace(&|part| match part { + StaticType::Var(var) => self.solution(*var).map(|solution| self.finish(solution)), + StaticType::Error => Some(StaticType::Any), + _ => None, + }) + } + + /// `typ`, or what it stands for if it's a settled variable. + fn shallow(&self, typ: &StaticType) -> StaticType { + match typ { + StaticType::Var(var) => self + .solution(*var) + .map_or_else(|| typ.clone(), |solution| self.shallow(solution)), + _ => typ.clone(), + } + } + + /// Makes `a` and `b` the same type by settling the unsettled variables in + /// either, and returns whether they are the same now. `List` and + /// `List` are, once `?0` is `Int`. `Never` fits any variable without + /// settling it, since a value of that type never arrives. On a mismatch, + /// the variables settled before it stay settled. + pub(crate) fn unify(&mut self, a: &StaticType, b: &StaticType) -> bool { + let (a, b) = (self.shallow(a), self.shallow(b)); + match (&a, &b) { + (StaticType::Var(x), StaticType::Var(y)) if x == y => true, + (StaticType::Var(_), StaticType::Never) | (StaticType::Never, StaticType::Var(_)) => { + true + } + (StaticType::Var(var), other) | (other, StaticType::Var(var)) => { + // A variable can't stand for a type that contains it. + if self + .resolve(other) + .contains(&|part| *part == StaticType::Var(*var)) + { + return false; + } + self.settle(*var, other.clone()) + } + // A failed check's type is every type, so nothing built from it + // fails again. + (StaticType::Error, _) | (_, StaticType::Error) => true, + (StaticType::Option(x), StaticType::Option(y)) + | (StaticType::Iterable(x), StaticType::Iterable(y)) + | (StaticType::List(x), StaticType::List(y)) + | (StaticType::Iterator(x), StaticType::Iterator(y)) + | (StaticType::MinHeap(x), StaticType::MinHeap(y)) + | (StaticType::MaxHeap(x), StaticType::MaxHeap(y)) + | (StaticType::Deque(x), StaticType::Deque(y)) => self.unify(x, y), + (StaticType::Tuple(xs), StaticType::Tuple(ys)) => { + xs.len() == ys.len() && xs.iter().zip(ys).all(|(x, y)| self.unify(x, y)) + } + (StaticType::Map { key: k1, value: v1 }, StaticType::Map { key: k2, value: v2 }) => { + let keys = self.unify(k1, k2); + let values = self.unify(v1, v2); + keys && values + } + ( + StaticType::Function { + parameters: p1, + return_type: r1, + }, + StaticType::Function { + parameters: p2, + return_type: r2, + }, + ) => { + let parameters = match (p1, p2) { + (Some(p1), Some(p2)) => { + p1.len() == p2.len() && p1.iter().zip(p2).all(|(x, y)| self.unify(x, y)) + } + (None, None) => true, + _ => false, + }; + let returns = self.unify(r1, r2); + parameters && returns + } + _ => a == b, + } + } + + /// `typ` with each type parameter, such as `T` in `(List, T)`, + /// replaced by a new variable of its own. A call does this to the + /// signature of the overload it picked, so its arguments settle what `T` + /// stands for at that call. + pub(crate) fn instantiate(&mut self, typ: &StaticType) -> StaticType { + self.instantiate_with(typ, &mut Vec::new()) + } + + /// The parameters and return type of the function type `typ`, with new + /// variables for its type parameters, or `None` when `typ` isn't a + /// function whose parameters are known. + pub(crate) fn instantiate_signature( &mut self, - symbol: SymbolId, - new_type: StaticType, - ) -> Result<(), StaticType> { - let binding = self - .types - .get_mut(&symbol) - .expect("internal error: a value is analysed after its declaration"); - if binding.is_annotated() { - return Err(binding.typ().clone()); - } - *binding = TypeBinding::Inferred(new_type); - Ok(()) + typ: &StaticType, + ) -> Option<(Vec, StaticType)> { + let StaticType::Function { + parameters: Some(_), + .. + } = typ + else { + return None; + }; + let StaticType::Function { + parameters: Some(parameters), + return_type, + } = self.instantiate(typ) + else { + unreachable!("instantiating a function type gives a function type"); + }; + Some((parameters, *return_type)) + } + + /// Passes each of `arguments` where its parameter is expected, as + /// [`Self::bind_argument`], and returns whether there's one argument + /// for each parameter and each fits. + pub(crate) fn bind_arguments( + &mut self, + parameters: &[StaticType], + arguments: &[StaticType], + ) -> bool { + parameters.len() == arguments.len() + && parameters + .iter() + .zip(arguments) + .all(|(parameter, argument)| self.bind_argument(parameter, argument)) + } + + /// [`Self::instantiate`], with the variables already made for some type + /// parameters in `made`, which gets the ones made for the others. A + /// generic function's constraints are instantiated with its signature. + pub(crate) fn instantiate_with( + &mut self, + typ: &StaticType, + made: &mut Vec<(Box, StaticType)>, + ) -> StaticType { + for name in typ.type_parameters() { + if !made.iter().any(|(generic, _)| *generic == name) { + let var = self.fresh(StaticType::Any); + made.push((name, var)); + } + } + typ.replace(&|part| match part { + StaticType::Generic(name) => made + .iter() + .find(|(generic, _)| generic == name) + .map(|(_, var)| var.clone()), + _ => None, + }) + } + + /// Settles the parts of `argument` that aren't known yet by passing it + /// where a `parameter` is expected, and returns whether it fits there. In + /// `push(xs, 1)` with `push(List, ?0)` and `xs: List`, both + /// variables become `Int`. Passing makes the two types the same, except + /// that a parameter of type `Any` takes anything, and an argument of + /// type `Any` goes anywhere, as it's checked as the program runs. Inside + /// a container, a function type, an `Option` or a tuple, `Any` is just + /// another type: a `List` doesn't fit a `List`. An + /// `Iterable` takes any kind of sequence, and settles `T` from its + /// elements. An argument nothing is known about yet stays what it is, + /// as the call that takes it waits for it to iterate. + pub(crate) fn bind_argument(&mut self, parameter: &StaticType, argument: &StaticType) -> bool { + let (parameter, argument) = (self.shallow(parameter), self.shallow(argument)); + match (¶meter, &argument) { + (_, StaticType::Never) | (StaticType::Any, _) => true, + // A value of type `Any` is checked where it goes, so it leaves a + // literal's type what the literal says: `n` in `let n = 1; + // n = a` is still an Int. So does a value whose check failed. + (StaticType::Var(var), StaticType::Any | StaticType::Error) + if self.is_literal(*var) => + { + true + } + (StaticType::Iterable(_), StaticType::Var(var)) if !self.is_literal(*var) => true, + // A literal passed for an `Iterable` is the sequence it holds, + // as `["a"]` passed to `len` is a `List`: only a + // parameter is ever an `Iterable` itself. + (StaticType::Iterable(_), StaticType::Var(var)) => { + let Some(held) = self + .literal_defaults(*var) + .and_then(|defaults| defaults.into_iter().next()) + else { + return true; + }; + self.settle_literal(*var, &held) && self.bind_argument(¶meter, &held) + } + (StaticType::Var(_), _) | (_, StaticType::Var(_)) => self.unify(¶meter, &argument), + (_, StaticType::Any | StaticType::Error) | (StaticType::Error, _) => true, + ( + StaticType::Function { + parameters: expected, + return_type: p, + }, + StaticType::Function { + parameters: takes, + return_type: a, + }, + ) => { + // A function type that says nothing about the parameters + // takes any function. + let parameters = match (expected, takes) { + (None, _) => true, + (Some(expected), Some(takes)) => { + expected.len() == takes.len() + && expected.iter().zip(takes).all(|(e, t)| self.passes(e, t)) + } + (Some(_), None) => false, + }; + // A function returning an `Iterable` may return any kind of + // sequence of `U`s, as `flat_map`'s callback does. + parameters + && match self.shallow(p) { + StaticType::Iterable(_) => self.bind_argument(p, a), + _ => self.unify(p, a), + } + } + (StaticType::Iterable(p), _) => match self.element_type(&argument) { + Some(element) => self.bind_argument(p, &element), + None => false, + }, + _ => self.unify(¶meter, &argument), + } + } + + /// Whether what a function type passes as an `expected` parameter is + /// what a function that `takes` it takes: the same type, or any kind of + /// sequence for an `Iterable`, as `len` takes the lists that + /// `[[1], [2]].map(len)` passes. + fn passes(&mut self, expected: &StaticType, takes: &StaticType) -> bool { + match self.shallow(takes) { + StaticType::Iterable(_) => self.bind_argument(takes, expected), + _ => self.unify(expected, takes), + } + } + + /// Whether what iterating over a value of type `typ` gives isn't known + /// yet: it's not known what `typ` is, or it's a tuple whose elements + /// aren't all known. + pub(crate) fn waits_to_iterate(&self, typ: &StaticType) -> bool { + match self.resolve(typ) { + StaticType::Var(var) => !self.is_literal(var), + StaticType::Tuple(elements) => elements.iter().any(|e| self.has_unknowns(e)), + _ => false, + } + } + + /// Whether `typ` has a part not known yet that isn't a literal's type. + fn has_unknowns(&self, typ: &StaticType) -> bool { + self.unsettled(typ) + .into_iter() + .any(|var| !self.is_literal(var)) + } + + /// What iterating over a value of type `typ` gives, or `None` if it + /// doesn't iterate. A tuple iterates as the one type its elements are + /// once made the same, or as `Any` if they can't be: `(1, 2.5)` iterates + /// as Floats, and `(1, "a")` as `Any`. + pub(crate) fn element_type(&mut self, typ: &StaticType) -> Option { + match self.resolve(typ) { + // A number doesn't iterate, whatever kind of number it becomes. + StaticType::Var(var) if self.is_number(var) => None, + // What its elements are isn't known yet, and they don't have to + // be one type, so they're not made one: `(a, b)` iterates as + // `Any` until `a` and `b` are known. + StaticType::Tuple(elements) if elements.iter().any(|e| self.has_unknowns(e)) => { + Some(StaticType::Any) + } + StaticType::Tuple(elements) if !elements.is_empty() => { + let snapshot = self.snapshot(); + if elements[1..] + .iter() + .all(|element| self.unify(&elements[0], element)) + { + Some(self.resolve(&elements[0])) + } else { + self.rollback(snapshot); + Some(StaticType::Any) + } + } + other => other.sequence_element_type(), + } + } + + /// Settles every unsettled variable in `typ` as `to`. + pub(crate) fn settle_unknowns(&mut self, typ: &StaticType, to: &StaticType) { + for var in self.unsettled(typ) { + if self.solution(var).is_none() { + self.settle(var, to.clone()); + } + } + } + + /// Settles every unsettled variable in `typ` but a literal's as the + /// type of a failed check: in `missing(xs).map(fn(x) => x.size)`, the + /// call of `map` says nothing about `x`, which then isn't checked. + /// Literals take their defaults as usual. + pub(crate) fn settle_unknowns_as_errors(&mut self, typ: &StaticType) { + for var in self.unsettled(typ) { + if self.solution(var).is_none() && !self.is_literal(var) { + self.settle(var, StaticType::Error); + } + } + } + + /// Settles every variable made since `first` that nothing settled with + /// its fallback. + pub(crate) fn settle_from(&mut self, first: usize) { + for index in first..self.solutions.len() { + let var = TypeVar(u32::try_from(index).expect("fewer than 2^32 type variables")); + self.settle_with_fallbacks(&[var]); + } } } #[cfg(test)] mod tests { - use super::*; - use ndc_resolver::{BindingKind, SymbolTable}; + use super::TypeVars; + use ndc_core::StaticType; + use ndc_lexer::Span; + + fn span() -> Span { + Span::synthetic() + } #[test] - fn inferred_types_can_change_but_annotated_types_are_fixed() { - let mut symbols = SymbolTable::default(); - let inferred = symbols.declare("a".to_string(), BindingKind::Plain); - let annotated = symbols.declare("b".to_string(), BindingKind::Plain); - let mut table = TypeTable::default(); - table.declare(inferred, TypeBinding::Inferred(StaticType::Int)); - table.declare(annotated, TypeBinding::Annotated(StaticType::Int)); + fn a_number_literal_is_a_number_or_any() { + let mut vars = TypeVars::default(); + for typ in [ + StaticType::Int, + StaticType::Float, + StaticType::Number, + StaticType::Any, + ] { + let literal = vars.fresh_number(span()); + assert!(vars.try_unify(&literal, &typ), "{typ}"); + } + let literal = vars.fresh_number(span()); + assert!(!vars.try_unify(&literal, &StaticType::String)); + } + #[test] + fn a_literal_holds_what_each_value_fits() { + let mut vars = TypeVars::default(); + let number = vars.fresh_number(span()); + let element = + vars.fresh_holding(vec![(number.clone(), span()), (StaticType::Float, span())]); + assert!(!vars.try_unify(&element, &StaticType::String)); + assert!(vars.try_unify(&element, &StaticType::Float)); + // Holding a Float made the number a Float too. + assert_eq!(vars.resolve(&number), StaticType::Float); + } + + #[test] + fn a_rollback_takes_back_the_bounds_another_variable_got() { + let mut vars = TypeVars::default(); + let literal = vars.fresh_number(span()); + let other = vars.fresh(StaticType::Any); + let snapshot = vars.snapshot(); + assert!(vars.unify(&literal, &other)); + // `other` is the literal now, so it can't be a String. + assert!(!vars.try_unify(&other, &StaticType::String)); + vars.rollback(snapshot); + assert!(vars.try_unify(&other, &StaticType::String)); + } + + #[test] + fn a_literal_defaults_to_what_its_values_share() { + let mut vars = TypeVars::default(); + let number = vars.fresh_number(span()); + let StaticType::Var(element) = + vars.fresh_holding(vec![(number, span()), (StaticType::Float, span())]) + else { + unreachable!("a literal's type is a variable"); + }; assert_eq!( - table.update_binding_type(inferred, StaticType::Number), - Ok(()) - ); - assert_eq!(table.get_type(inferred), &StaticType::Number); - assert_eq!( - table.update_binding_type(annotated, StaticType::Number), - Err(StaticType::Int) + vars.literal_defaults(element), + Some(vec![StaticType::Float]) ); - assert_eq!(table.get_type(annotated), &StaticType::Int); + let StaticType::Var(mixed) = vars.fresh_holding(vec![ + (StaticType::String, span()), + (StaticType::Int, span()), + ]) else { + unreachable!("a literal's type is a variable"); + }; + vars.settle_with_fallbacks(&[mixed]); + assert_eq!(vars.resolve(&StaticType::Var(mixed)), StaticType::Any); } } diff --git a/ndc_analysis/src/lib.rs b/ndc_analysis/src/lib.rs index 91942184..4dbff040 100644 --- a/ndc_analysis/src/lib.rs +++ b/ndc_analysis/src/lib.rs @@ -52,6 +52,12 @@ pub struct Analysis { /// the cast. `None` when the value is already known to have the type, as /// in `1 as Int`, so there is nothing to check. pub cast_checks: AHashMap>, + /// The type a value of type `Any` is checked against as the program + /// runs, where code that expects that type gets it, keyed by where it's + /// checked: an expression, checked once it's computed, or an assignment + /// target or a name a pattern declares, checked before the value is + /// stored. In `let n: Int = a` with `a: Any`, `n` checks for an Int. + pub type_checks: AHashMap, /// How many local slots each function's own variables use, parameters /// first, keyed by its declaration. The compiler places its temporaries /// after them. @@ -62,6 +68,31 @@ pub struct Analysis { /// How many global slots the program needs: the natives and every /// top-level declaration, counting earlier REPL lines. pub global_count: usize, + /// The instances of each generic function the batch declares, keyed by + /// the declaration. An instance is the function's body for the types one + /// set of calls gives it, which picks the overloads its calls use from + /// those types: `fn inc(x) => x + 1` has one for Ints, whose `+` is + /// `+(Int, Int)`. The declaration itself is the instance for arguments + /// of any type, whose calls are picked as the program runs. + pub instances: AHashMap>, +} + +/// One instance of a generic function: where it's stored, and the picks in +/// its body that differ from the function's own. +#[derive(Debug, Clone)] +pub struct Instance { + /// The global the instance is stored in. + pub variable: ResolvedVar, + /// As [`Analysis::bindings`], for the instance's body. + pub bindings: AHashMap, + /// As [`Analysis::getters`], for the instance's body. + pub getters: AHashMap, + /// As [`Analysis::setters`], for the instance's body. + pub setters: AHashMap, + /// As [`Analysis::augmented_assignments`], for the instance's body. + pub augmented_assignments: AHashMap, + /// As [`Analysis::type_checks`], for the instance's body. + pub type_checks: AHashMap, } impl Analysis { diff --git a/ndc_core/src/lib.rs b/ndc_core/src/lib.rs index 9438362b..c8521294 100644 --- a/ndc_core/src/lib.rs +++ b/ndc_core/src/lib.rs @@ -1,9 +1,10 @@ mod node_id; +mod signature; pub mod static_type; pub mod r#struct; pub use node_id::NodeId; -pub use static_type::{Parameter, StaticType, TypeSignature}; +pub use static_type::{Parameter, StaticType, TypeSignature, TypeVar}; use std::slice::Iter; pub struct FunctionRegistry { diff --git a/ndc_core/src/signature.rs b/ndc_core/src/signature.rs new file mode 100644 index 00000000..64b48f93 --- /dev/null +++ b/ndc_core/src/signature.rs @@ -0,0 +1,268 @@ +//! Built-in function signatures written in the language's own type syntax, +//! such as `(List, T) -> Option`. + +use crate::StaticType; + +impl StaticType { + /// The function type a built-in's signature describes: + /// `(List, T) -> Option` declares the type parameter `T`, two + /// parameters and a return type. Without `-> …` the function returns + /// `()`. `(*)` takes any number of arguments of any type, and + /// `Fn(Int) -> Int` is a function type. + pub fn signature(text: &str) -> Result { + let mut parser = SignatureParser { + tokens: tokenize(text)?, + position: 0, + generics: Vec::new(), + }; + let signature = parser.signature()?; + match parser.peek() { + None => Ok(signature), + Some(token) => Err(format!("unexpected '{token}' after the signature")), + } + } +} + +fn tokenize(text: &str) -> Result, String> { + let mut tokens = Vec::new(); + let mut chars = text.char_indices().peekable(); + while let Some((start, c)) = chars.next() { + match c { + c if c.is_whitespace() => {} + '<' | '>' | '(' | ')' | ',' | '*' => tokens.push(c.to_string()), + '-' if chars.next_if(|&(_, next)| next == '>').is_some() => { + tokens.push("->".to_string()); + } + c if c.is_alphabetic() || c == '_' => { + let mut end = start + c.len_utf8(); + while let Some((index, next)) = + chars.next_if(|&(_, next)| next.is_alphanumeric() || next == '_') + { + end = index + next.len_utf8(); + } + tokens.push(text[start..end].to_string()); + } + other => return Err(format!("unexpected '{other}' in signature '{text}'")), + } + } + Ok(tokens) +} + +struct SignatureParser { + tokens: Vec, + position: usize, + /// The type parameters the signature declared, such as `T` in ``. + generics: Vec, +} + +impl SignatureParser { + fn peek(&self) -> Option<&str> { + self.tokens.get(self.position).map(String::as_str) + } + + fn next(&mut self) -> Result { + let token = self + .tokens + .get(self.position) + .cloned() + .ok_or_else(|| "the signature ends too early".to_string())?; + self.position += 1; + Ok(token) + } + + fn eat(&mut self, token: &str) -> bool { + let found = self.peek() == Some(token); + if found { + self.position += 1; + } + found + } + + fn expect(&mut self, token: &str) -> Result<(), String> { + let found = self.next()?; + if found == token { + Ok(()) + } else { + Err(format!("expected '{token}' but found '{found}'")) + } + } + + /// `(…) -> R`, where the type parameters and the return type are + /// optional. + fn signature(&mut self) -> Result { + if self.eat("<") { + loop { + let name = self.next()?; + if self.generics.contains(&name) { + return Err(format!("type parameter '{name}' is declared twice")); + } + self.generics.push(name); + if !self.eat(",") { + break; + } + } + self.expect(">")?; + } + self.function() + } + + /// `(…) -> R` after a function's name or after `Fn`. + fn function(&mut self) -> Result { + self.expect("(")?; + let parameters = if self.eat("*") { + self.expect(")")?; + None + } else { + Some(self.list(")")?) + }; + let return_type = if self.eat("->") { + self.typ()? + } else { + StaticType::unit() + }; + Ok(StaticType::Function { + parameters, + return_type: Box::new(return_type), + }) + } + + /// Types separated by commas up to `close`, which is consumed. + fn list(&mut self, close: &str) -> Result, String> { + let mut types = Vec::new(); + while !self.eat(close) { + types.push(self.typ()?); + if !self.eat(",") { + self.expect(close)?; + break; + } + } + Ok(types) + } + + fn typ(&mut self) -> Result { + let token = self.next()?; + match token.as_str() { + // `()` is unit, `(A, B)` and `(A,)` are tuples, and `(A)` is just `A`. + "(" => { + let start = self.position; + let mut elements = self.list(")")?; + let trailing_comma = self.tokens[start..self.position] + .ends_with(&[",".to_string(), ")".to_string()]); + if elements.len() == 1 && !trailing_comma { + Ok(elements.remove(0)) + } else { + Ok(StaticType::Tuple(elements)) + } + } + "Fn" => self.function(), + name if self.generics.iter().any(|generic| generic == name) => { + Ok(StaticType::Generic(name.into())) + } + name => { + let arguments = if self.eat("<") { + self.list(">")? + } else { + Vec::new() + }; + StaticType::from_name_and_args(name, arguments).map_err(|error| error.to_string()) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn function(parameters: Option>, return_type: StaticType) -> StaticType { + StaticType::Function { + parameters, + return_type: Box::new(return_type), + } + } + + fn generic(name: &str) -> StaticType { + StaticType::Generic(name.into()) + } + + #[test] + fn a_signature_declares_its_parameters_and_return_type() { + assert_eq!( + StaticType::signature("(List, T) -> Option"), + Ok(function( + Some(vec![StaticType::List(Box::new(generic("T"))), generic("T")]), + StaticType::Option(Box::new(generic("T"))), + )) + ); + assert_eq!( + StaticType::signature("(String, Int) -> Bool"), + Ok(function( + Some(vec![StaticType::String, StaticType::Int]), + StaticType::Bool + )) + ); + } + + #[test] + fn a_signature_without_a_return_type_returns_unit() { + assert_eq!( + StaticType::signature("()"), + Ok(function(Some(vec![]), StaticType::unit())) + ); + } + + #[test] + fn tuples_function_types_and_variadics_parse() { + assert_eq!( + StaticType::signature("(Map) -> List<(K, V)>"), + Ok(function( + Some(vec![StaticType::Map { + key: Box::new(generic("K")), + value: Box::new(generic("V")), + }]), + StaticType::List(Box::new(StaticType::Tuple(vec![ + generic("K"), + generic("V") + ]))), + )) + ); + assert_eq!( + StaticType::signature("(List, Fn(T) -> U) -> List"), + Ok(function( + Some(vec![ + StaticType::List(Box::new(generic("T"))), + function(Some(vec![generic("T")]), generic("U")), + ]), + StaticType::List(Box::new(generic("U"))), + )) + ); + assert_eq!( + StaticType::signature("(*) -> Any"), + Ok(function(None, StaticType::Any)) + ); + assert_eq!( + StaticType::signature("((Int,), (Int))"), + Ok(function( + Some(vec![ + StaticType::Tuple(vec![StaticType::Int]), + StaticType::Int + ]), + StaticType::unit() + )) + ); + } + + #[test] + fn a_malformed_signature_is_rejected() { + for text in [ + "(List)", + "(T)", + "(Int", + "(Int) ->", + "(Int) -> Int extra", + "(Int; Int)", + ] { + assert!(StaticType::signature(text).is_err(), "{text}"); + } + } +} diff --git a/ndc_core/src/static_type.rs b/ndc_core/src/static_type.rs index 4ebd7d46..a7211aee 100644 --- a/ndc_core/src/static_type.rs +++ b/ndc_core/src/static_type.rs @@ -14,26 +14,6 @@ impl Default for TypeSignature { } } -impl TypeSignature { - pub fn from_annotated_bindings(bindings: Vec<(String, Option)>) -> Self { - Self::Exact( - bindings - .into_iter() - .map(|(name, annotation)| { - Parameter::new(name, annotation.unwrap_or(StaticType::Any)) - }) - .collect(), - ) - } - - pub fn types(&self) -> Option> { - match self { - Self::Variadic => None, - Self::Exact(v) => Some(v.iter().map(|p| p.type_name.clone()).collect()), - } - } -} - #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct Parameter { pub name: String, @@ -49,12 +29,33 @@ impl Parameter { } } +/// A type the analyser hasn't worked out yet. See [`StaticType::Var`]. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub struct TypeVar(pub u32); + #[derive(Debug, Clone, Eq, PartialEq, Hash, Default)] pub enum StaticType { #[default] Any, + /// A type the analyser hasn't worked out yet, such as the element type + /// of `[]` before anything is stored in it. Only the analyser creates + /// these, and it replaces every one before handing its results on. Until + /// then, every method here treats one as `Any`. + Var(TypeVar), + /// A type parameter of a generic built-in's signature, such as `T` in + /// `push(List, T)`. Each call stands for its own type in place of + /// it, which the analyser works out from the arguments. Every method + /// here treats one as `Any`. + Generic(Box), + /// The type of an expression whose check failed, which was reported. + /// Nothing built from it is reported again: it fits anything, anything + /// fits it, and a call it's passed to fails silently. In `[p].map(fn(p) + /// => p.size).sum()`, only the missing `size` is reported, not `sum` of + /// a list of what it gave. Only the analyser makes it, and it shows as + /// `Any`. + Error, /// Bottom type for diverging expressions (`return`, `break`, `continue`). - /// `Never` is a subtype of every type: `lub(T, Never) = T`. + /// A value of it fits anywhere, and it joins with any type as that type. Never, Bool, Function { @@ -72,8 +73,8 @@ pub enum StaticType { Float, Int, - // Sequences List -> Sequence - Sequence(Box), + // What a value of a kind of sequence iterates as, which only a parameter is: `List` passes for an `Iterable`. + Iterable(Box), List(Box), String, Tuple(Vec), @@ -117,7 +118,7 @@ impl StaticType { /// Must stay in sync with its match arms; the unit test /// `builtin_type_names_are_constructible` guards this. pub const BUILTIN_TYPE_NAMES: &'static [&'static str] = &[ - "Any", "Never", "Bool", "Number", "Float", "Int", "String", "Option", "Sequence", "List", + "Any", "Never", "Bool", "Number", "Float", "Int", "String", "Option", "Iterable", "List", "Iterator", "MinHeap", "MaxHeap", "Deque", "Tuple", "Map", ]; @@ -136,8 +137,8 @@ impl StaticType { "Option" => { Self::require_exactly_one_arg(name, args).map(|elem| Self::Option(Box::new(elem))) } - "Sequence" => { - Self::require_exactly_one_arg(name, args).map(|elem| Self::Sequence(Box::new(elem))) + "Iterable" => { + Self::require_exactly_one_arg(name, args).map(|elem| Self::Iterable(Box::new(elem))) } "List" => { Self::require_exactly_one_arg(name, args).map(|elem| Self::List(Box::new(elem))) @@ -214,277 +215,35 @@ impl StaticType { } } - /// Checks if `self` is a subtype of `other`. - /// - /// A type S is a subtype of T (S <: T) if a value of type S can be safely - /// used wherever a value of type T is expected. - /// - /// # Key Rules - /// - `Any` is the top type (supertype of all types) - /// - `Int`, `Float`, and `Number` are sibling types - /// - `Sequence` > sequence types with element type T - /// - Generic types are covariant in their type parameters - /// - Function parameters are **contravariant**, returns are **covariant** - /// - /// Note: this function probably doesn't handle variadic functions correctly - pub fn is_subtype(&self, other: &Self) -> bool { - // Never is the bottom type: subtype of everything - if matches!(self, Self::Never) { - return true; - } - - // Any is the universal supertype - if matches!(other, Self::Any) { - return true; - } - - // Only Any satisfies the above; all other types fail here - if matches!(self, Self::Any) { - return false; - } - - #[allow(clippy::match_same_arms)] - #[allow(clippy::unnested_or_patterns)] - match (self, other) { - // Reflexivity: every type is a subtype of itself - _ if self == other => true, - - // Sequence hierarchy: specific sequences are subtypes of Sequence - // where T is their element type (covariant) - (Self::List(s), Self::Sequence(t)) - | (Self::Iterator(s), Self::Sequence(t)) - | (Self::MinHeap(s), Self::Sequence(t)) - | (Self::MaxHeap(s), Self::Sequence(t)) - | (Self::Deque(s), Self::Sequence(t)) => s.is_subtype(t), - - // String is Sequence - (Self::String, Self::Sequence(t)) => Self::String.is_subtype(t.as_ref()), - - // Tuple is Sequence - (Self::Tuple(elems), Self::Sequence(t)) => { - Self::compute_tuple_element_type(elems).is_subtype(t) - } - - // Map is Sequence> - (Self::Map { key, value }, Self::Sequence(t)) => { - Self::Tuple(vec![key.as_ref().clone(), value.as_ref().clone()]) - .is_subtype(t.as_ref()) - } - - // Generic types are covariant: Container <: Container if S <: T - (Self::Option(s), Self::Option(t)) - | (Self::List(s), Self::List(t)) - | (Self::Iterator(s), Self::Iterator(t)) - | (Self::MinHeap(s), Self::MinHeap(t)) - | (Self::MaxHeap(s), Self::MaxHeap(t)) - | (Self::Deque(s), Self::Deque(t)) - | (Self::Sequence(s), Self::Sequence(t)) => s.is_subtype(t), - - // Tuples are covariant pointwise and must have the same arity - (Self::Tuple(s_elems), Self::Tuple(t_elems)) => { - s_elems.len() == t_elems.len() - && s_elems.iter().zip(t_elems).all(|(s, t)| s.is_subtype(t)) - } - - // Maps are covariant in both key and value type parameters - (Self::Map { key: k1, value: v1 }, Self::Map { key: k2, value: v2 }) => { - k1.is_subtype(k2) && v1.is_subtype(v2) - } - - // Functions: contravariant in parameters, covariant in return type - // F1 <: F2 iff params(F2) <: params(F1) AND return(F1) <: return(F2) - ( - Self::Function { - parameters: p1, - return_type: r1, - }, - Self::Function { - parameters: p2, - return_type: r2, - }, - ) => { - // Return type is covariant: must satisfy r1 <: r2 - let return_ok = r1.is_subtype(r2); - - // Parameters are contravariant: must satisfy p2 <: p1 (reversed!) - let params_ok = match (p1, p2) { - (None, None) => true, - // A function with specific params is a subtype of one with generic params - (Some(_), None) => true, - // Cannot substitute generic params with specific ones - (None, Some(_)) => false, - (Some(ps1), Some(ps2)) => { - ps1.len() == ps2.len() && - // Note the reversal: p2 must be subtype of p1! - ps1.iter() - .zip(ps2) - .all(|(p1, p2)| p2.is_subtype(p1)) - } - }; - - return_ok && params_ok - } - - _ => false, - } - } - - // - /// Computes the Least Upper Bound (join) of two types. - /// - /// The LUB is the most specific type that is a supertype of both inputs. - /// - /// # Examples - /// - `lub(Int, Float) = Any` - /// - `lub(List, List) = List` - /// - `lub(List, Iterator) = Sequence` - /// - `lub(Int, String) = Any` - pub fn lub(&self, other: &Self) -> Self { - // Never is the bottom type: lub(T, Never) = T - if matches!(self, Self::Never) { - return other.clone(); - } - if matches!(other, Self::Never) { - return self.clone(); - } - - // Any is the top type - if matches!(self, Self::Any) || matches!(other, Self::Any) { - return Self::Any; - } - - // Reflexivity: lub(T, T) = T - if self == other { - return self.clone(); - } - - // If one is a subtype of the other, return the supertype - if self.is_subtype(other) { - return other.clone(); - } - if other.is_subtype(self) { - return self.clone(); - } - - match (self, other) { - // Covariant generic types: compute LUB pointwise - (Self::Option(s), Self::Option(t)) => Self::Option(Box::new(s.lub(t))), - (Self::List(s), Self::List(t)) => Self::List(Box::new(s.lub(t))), - (Self::Iterator(s), Self::Iterator(t)) => Self::Iterator(Box::new(s.lub(t))), - (Self::MinHeap(s), Self::MinHeap(t)) => Self::MinHeap(Box::new(s.lub(t))), - (Self::MaxHeap(s), Self::MaxHeap(t)) => Self::MaxHeap(Box::new(s.lub(t))), - (Self::Deque(s), Self::Deque(t)) => Self::Deque(Box::new(s.lub(t))), - (Self::Sequence(s), Self::Sequence(t)) => Self::Sequence(Box::new(s.lub(t))), - - // Maps are covariant in both parameters - (Self::Map { key: k1, value: v1 }, Self::Map { key: k2, value: v2 }) => Self::Map { - key: Box::new(k1.lub(k2)), - value: Box::new(v1.lub(v2)), - }, - - // Tuples of same arity: compute LUB pointwise - (Self::Tuple(e1), Self::Tuple(e2)) if e1.len() == e2.len() => { - Self::Tuple(e1.iter().zip(e2).map(|(a, b)| a.lub(b)).collect()) - } - - // Different sequence types: generalize to Sequence - _ if Self::is_sequence(self) && Self::is_sequence(other) => { - let elem1 = self.sequence_element_type().expect("must be seq"); - let elem2 = other.sequence_element_type().expect("must be seq"); - Self::Sequence(Box::new(elem1.lub(&elem2))) - } - - // Functions: covariant in return, contravariant in parameters - ( - Self::Function { - parameters: p1, - return_type: r1, - }, - Self::Function { - parameters: p2, - return_type: r2, - }, - ) => { - // Return type: covariant, so take LUB - let return_type = Box::new(r1.lub(r2)); - - // Parameters: contravariant, so we need GLB (greatest lower bound) - let parameters = match (p1, p2) { - (Some(ps1), Some(ps2)) if ps1.len() == ps2.len() => { - Some(ps1.iter().zip(ps2).map(|(a, b)| a.glb(b)).collect()) - } - // Incompatible or unknown parameter lists - _ => None, - }; - - Self::Function { - parameters, - return_type, - } - } - - // No common supertype found: default to Any - _ => Self::Any, - } - } - - /// Computes the Greatest Lower Bound (meet) of two types. - /// - /// The GLB is the most general type that is a subtype of both inputs. - /// This is required for contravariant positions (e.g., function parameters). - /// - /// Note: Not all type pairs have a representable GLB in our type system. - /// In such cases, we return a conservative approximation. - fn glb(&self, other: &Self) -> Self { - // Any is the top type: glb(Any, T) = T + /// The type a value of either type has: the type itself when both are + /// the same, and otherwise `Any`. `Never` has no values, so it joins as + /// the other type. `List` and `List` join as `Any`, not as + /// `List`, which neither list is. + #[must_use] + pub fn join(&self, other: &Self) -> Self { match (self, other) { - (Self::Any, t) | (t, Self::Any) => t.clone(), + (Self::Never, other) | (other, Self::Never) => other.clone(), + (Self::Error, _) | (_, Self::Error) => Self::Error, _ if self == other => self.clone(), - _ if self.is_subtype(other) => self.clone(), - _ if other.is_subtype(self) => other.clone(), - - // Covariant types: compute GLB pointwise - (Self::Option(s), Self::Option(t)) => Self::Option(Box::new(s.glb(t))), - (Self::List(s), Self::List(t)) => Self::List(Box::new(s.glb(t))), - (Self::Iterator(s), Self::Iterator(t)) => Self::Iterator(Box::new(s.glb(t))), - (Self::MinHeap(s), Self::MinHeap(t)) => Self::MinHeap(Box::new(s.glb(t))), - (Self::MaxHeap(s), Self::MaxHeap(t)) => Self::MaxHeap(Box::new(s.glb(t))), - (Self::Deque(s), Self::Deque(t)) => Self::Deque(Box::new(s.glb(t))), - (Self::Sequence(s), Self::Sequence(t)) => Self::Sequence(Box::new(s.glb(t))), - - (Self::Map { key: k1, value: v1 }, Self::Map { key: k2, value: v2 }) => Self::Map { - key: Box::new(k1.glb(k2)), - value: Box::new(v1.glb(v2)), - }, - - (Self::Tuple(e1), Self::Tuple(e2)) if e1.len() == e2.len() => { - Self::Tuple(e1.iter().zip(e2).map(|(a, b)| a.glb(b)).collect()) - } - - // No representable GLB: conservatively return first type - // A complete type system would have a Bottom type (⊥) here - _ => self.clone(), + _ => Self::Any, } } - /// Computes the element type of a tuple when viewed as a sequence. - /// Returns the LUB of all tuple elements. + /// Computes the element type of a tuple when viewed as a sequence: the + /// type its elements join as. fn compute_tuple_element_type(elems: &[Self]) -> Self { - if elems.is_empty() { - return Self::Any; - } - elems .iter() - .skip(1) - .fold(elems[0].clone(), |acc, elem| acc.lub(elem)) + .cloned() + .reduce(|acc, elem| acc.join(&elem)) + .unwrap_or(Self::Any) } /// Checks if a type is a sequence-like type. fn is_sequence(ty: &Self) -> bool { matches!( ty, - Self::Sequence(_) + Self::Iterable(_) | Self::List(_) | Self::String | Self::Tuple(_) @@ -500,11 +259,11 @@ impl StaticType { /// /// - `List`, `Iterator`, etc. → `T` /// - `String` → `String` - /// - `Tuple` → `lub(T1, ..., Tn)` + /// - `Tuple` → the join of `T1`, ..., `Tn` /// - `Map` → `Tuple` pub fn sequence_element_type(&self) -> Option { match self { - Self::Sequence(t) + Self::Iterable(t) | Self::List(t) | Self::Iterator(t) | Self::MinHeap(t) @@ -516,11 +275,228 @@ impl StaticType { key.as_ref().clone(), value.as_ref().clone(), ])), - Self::Any => Some(Self::Any), + Self::Error => Some(Self::Error), + unknown if unknown.is_unknown() => Some(Self::Any), _ => None, } } + /// Whether a value of this type can be passed where `parameter` is + /// expected as it is, with nothing to check as the program runs. The + /// two types are the same, except that `parameter` can be `Any`, which + /// takes anything, and that its type parameters and the parts of it not + /// known yet stand for any type. Inside a container, a function type, + /// an `Option` or a tuple, `Any` is just another type: a `List` + /// doesn't fit a `List`, as the list could then get a String. A + /// `Iterable` takes any kind of sequence of what fits `T`. + pub fn fits(&self, parameter: &Self) -> bool { + matches!(parameter, Self::Any) || self.matches(parameter, false) + } + + /// Whether a value of this type might be passed where `parameter` is + /// expected, once what isn't known about it is: as [`Self::fits`], + /// except that the parts of this type not known yet, and the whole of + /// it if it's `Any`, stand for any type too. `Any` checked as the + /// program runs can turn out to be an Int, but a `List` is never a + /// `List`. + pub fn may_fit(&self, parameter: &Self) -> bool { + matches!(self, Self::Any) || self.fits(parameter) || self.matches(parameter, true) + } + + /// Whether this type is `parameter`, where what [`Self::fits`] and + /// [`Self::may_fit`] let stand for any type does, and the variables of + /// this type too if `unknowns_match`. + fn matches(&self, parameter: &Self, unknowns_match: bool) -> bool { + let wildcard = |typ: &Self| matches!(typ, Self::Var(_) | Self::Generic(_)); + if wildcard(parameter) + || (unknowns_match && wildcard(self)) + || self.is_error() + || parameter.is_error() + { + return true; + } + let matches = |a: &Self, p: &Self| a.matches(p, unknowns_match); + match (self, parameter) { + (Self::Never, _) => true, + (Self::Iterable(a), Self::Iterable(p)) => a.fits(p) || matches(a, p), + (argument, Self::Iterable(p)) if Self::is_sequence(argument) => { + let element = argument + .sequence_element_type() + .expect("a sequence has elements"); + element.fits(p) || matches(&element, p) + } + (Self::Option(a), Self::Option(p)) + | (Self::List(a), Self::List(p)) + | (Self::Iterator(a), Self::Iterator(p)) + | (Self::MinHeap(a), Self::MinHeap(p)) + | (Self::MaxHeap(a), Self::MaxHeap(p)) + | (Self::Deque(a), Self::Deque(p)) => matches(a, p), + (Self::Tuple(a), Self::Tuple(p)) => { + a.len() == p.len() && a.iter().zip(p).all(|(a, p)| matches(a, p)) + } + (Self::Map { key: ak, value: av }, Self::Map { key: pk, value: pv }) => { + matches(ak, pk) && matches(av, pv) + } + ( + Self::Function { + parameters: a, + return_type: ar, + }, + Self::Function { + parameters: p, + return_type: pr, + }, + ) => { + // What a function value takes and returns isn't checked as + // the program runs, so whether it might fit only depends on + // what both types know: `fn(x) => x > 1`, typed `Fn(Any) -> + // Any` where nothing says what `x` is, might be a predicate. + let part = |a: &Self, p: &Self| { + matches(a, p) + || (unknowns_match && (matches!(a, Self::Any) || matches!(p, Self::Any))) + }; + let parameters = match (a, p) { + // A function value whose parameters aren't known fits + // a function type that says nothing about them either. + (_, None) => true, + (None, Some(_)) => unknowns_match, + (Some(a), Some(p)) => { + a.len() == p.len() && a.iter().zip(p).all(|(a, p)| part(a, p)) + } + }; + parameters && part(ar, pr) + } + _ => self == parameter, + } + } + + /// Whether this type stands for every type: `Any`, a type variable, a + /// type parameter or a failed check's type, which are all `Any` to the + /// methods here. + pub fn is_unknown(&self) -> bool { + matches!( + self, + Self::Any | Self::Var(_) | Self::Generic(_) | Self::Error + ) + } + + /// Whether this is the type of an expression whose check failed. + pub fn is_error(&self) -> bool { + matches!(self, Self::Error) + } + + /// Whether a failed check's type is part of this type: `List` + /// is the type of a list holding a value whose check failed. + pub fn has_errors(&self) -> bool { + self.contains(&Self::is_error) + } + + /// This type with each part that `replace` gives a type for replaced by + /// that type, looking at the outer parts first: `List` becomes + /// `List` when `replace` maps `?0` to `Int`. + #[must_use] + pub fn replace(&self, replace: &impl Fn(&Self) -> Option) -> Self { + if let Some(replaced) = replace(self) { + return replaced; + } + let map = |typ: &Self| Box::new(typ.replace(replace)); + match self { + Self::Function { + parameters, + return_type, + } => Self::Function { + parameters: parameters + .as_ref() + .map(|parameters| parameters.iter().map(|p| p.replace(replace)).collect()), + return_type: map(return_type), + }, + Self::Option(t) => Self::Option(map(t)), + Self::Iterable(t) => Self::Iterable(map(t)), + Self::List(t) => Self::List(map(t)), + Self::Iterator(t) => Self::Iterator(map(t)), + Self::MinHeap(t) => Self::MinHeap(map(t)), + Self::MaxHeap(t) => Self::MaxHeap(map(t)), + Self::Deque(t) => Self::Deque(map(t)), + Self::Tuple(elements) => { + Self::Tuple(elements.iter().map(|e| e.replace(replace)).collect()) + } + Self::Map { key, value } => Self::Map { + key: map(key), + value: map(value), + }, + Self::Any + | Self::Var(_) + | Self::Generic(_) + | Self::Error + | Self::Never + | Self::Bool + | Self::Struct { .. } + | Self::Number + | Self::Float + | Self::Int + | Self::String => self.clone(), + } + } + + /// Whether this type or any of its parts satisfies `test`. + pub fn contains(&self, test: &impl Fn(&Self) -> bool) -> bool { + if test(self) { + return true; + } + match self { + Self::Function { + parameters, + return_type, + } => { + return_type.contains(test) + || parameters + .iter() + .flatten() + .any(|parameter| parameter.contains(test)) + } + Self::Option(t) + | Self::Iterable(t) + | Self::List(t) + | Self::Iterator(t) + | Self::MinHeap(t) + | Self::MaxHeap(t) + | Self::Deque(t) => t.contains(test), + Self::Tuple(elements) => elements.iter().any(|e| e.contains(test)), + Self::Map { key, value } => key.contains(test) || value.contains(test), + Self::Any + | Self::Var(_) + | Self::Generic(_) + | Self::Error + | Self::Never + | Self::Bool + | Self::Struct { .. } + | Self::Number + | Self::Float + | Self::Int + | Self::String => false, + } + } + + /// The type parameters in this type, each once, outermost first: `T` + /// and `U` in `Fn(T) -> List`. + pub fn type_parameters(&self) -> Vec> { + let names = std::cell::RefCell::new(Vec::>::new()); + self.contains(&|part| { + if let Self::Generic(name) = part + && !names.borrow().contains(name) + { + names.borrow_mut().push(name.clone()); + } + false + }); + names.into_inner() + } + + /// Whether any type variable is left in this type. + pub fn has_vars(&self) -> bool { + self.contains(&|t| matches!(t, Self::Var(_))) + } + #[must_use] pub fn unit() -> Self { Self::Tuple(vec![]) @@ -533,8 +509,8 @@ impl StaticType { /// Checks whether some runtime value could satisfy both `self` and `other`. /// /// # Examples - /// - `Sequence` overlaps `List`: a `List` inhabits - /// both, even though neither type is a subtype of the other + /// - `Iterable` overlaps `List`: a list of Strings is + /// both /// - `Any` overlaps every type /// - `List` does not overlap `List` /// - `Int` does not overlap `String` @@ -553,30 +529,28 @@ impl StaticType { /// `Never` overlaps nothing, since no value inhabits it. pub fn overlaps(&self, other: &Self) -> bool { // Never has no inhabitants, so nothing can satisfy it — not even - // another Never. This has to come first: Never is a subtype of every - // type, so the shortcut below would otherwise report overlap. + // another Never. if matches!(self, Self::Never) || matches!(other, Self::Never) { return false; } - // A subtype relation in either direction implies a common inhabitant. - // This also handles Any, the supertype of everything. - if self.is_subtype(other) || other.is_subtype(self) { + // `Any` and what isn't known yet could be anything, and a type has + // the values it has. + if self.is_unknown() || other.is_unknown() || self == other { return true; } match (self, other) { - // Sequence overlaps every sequence-family type whose element - // type overlaps T, even when the subtype check fails because the - // relationship points in different directions per layer. + // Iterable overlaps every sequence-family type whose element + // type overlaps T. ( - Self::Sequence(t), + Self::Iterable(t), Self::List(u) | Self::Iterator(u) | Self::MinHeap(u) | Self::MaxHeap(u) | Self::Deque(u) - | Self::Sequence(u), + | Self::Iterable(u), ) | ( Self::List(u) @@ -584,16 +558,16 @@ impl StaticType { | Self::MinHeap(u) | Self::MaxHeap(u) | Self::Deque(u), - Self::Sequence(t), + Self::Iterable(t), ) => t.overlaps(u), - (Self::Sequence(t), Self::String) | (Self::String, Self::Sequence(t)) => { + (Self::Iterable(t), Self::String) | (Self::String, Self::Iterable(t)) => { Self::String.overlaps(t) } - (Self::Sequence(t), Self::Tuple(elems)) | (Self::Tuple(elems), Self::Sequence(t)) => { + (Self::Iterable(t), Self::Tuple(elems)) | (Self::Tuple(elems), Self::Iterable(t)) => { elems.iter().all(|elem| elem.overlaps(t)) } - (Self::Sequence(t), Self::Map { key, value }) - | (Self::Map { key, value }, Self::Sequence(t)) => { + (Self::Iterable(t), Self::Map { key, value }) + | (Self::Map { key, value }, Self::Iterable(t)) => { Self::Tuple(vec![key.as_ref().clone(), value.as_ref().clone()]).overlaps(t) } @@ -626,30 +600,6 @@ impl StaticType { } } - // BRUH - pub fn is_incompatible_with(&self, other: &Self) -> bool { - !self.is_subtype(other) && !other.is_subtype(self) - } - - /// Returns a new type with the element type replaced. For container types - /// like `List`, this returns `List`. Returns `None` if the - /// type does not have a replaceable element type. - pub fn with_element_type(&self, new_elem: Self) -> Self { - match self { - Self::List(_) => Self::List(Box::new(new_elem)), - Self::Sequence(_) => Self::Sequence(Box::new(new_elem)), - Self::Iterator(_) => Self::Iterator(Box::new(new_elem)), - Self::MinHeap(_) => Self::MinHeap(Box::new(new_elem)), - Self::MaxHeap(_) => Self::MaxHeap(Box::new(new_elem)), - Self::Deque(_) => Self::Deque(Box::new(new_elem)), - Self::Map { key, .. } => Self::Map { - key: key.clone(), - value: Box::new(new_elem), - }, - _ => self.clone(), - } - } - pub fn index_element_type(&self) -> Option { if let Self::Map { value, .. } = self { return Some(value.as_ref().clone()); @@ -662,7 +612,7 @@ impl StaticType { /// This is true for `Function` types and for `Any` (which might be a /// function at runtime). Concrete non-function types return `false`. pub fn could_be_callable(&self) -> bool { - matches!(self, Self::Function { .. } | Self::Any) + matches!(self, Self::Function { .. }) || self.is_unknown() } pub fn is_fn_and_matches(&self, types: &[Self]) -> bool { @@ -671,23 +621,148 @@ impl StaticType { return false; }; - let Some(_) = parameters else { + let Some(parameters) = parameters else { // If this branch happens then the function we're matching against is variadic meaning it's always a match return true; }; - self.is_subtype(&Self::Function { - parameters: Some(types.to_vec()), - return_type: Box::new(Self::Any), + parameters.len() == types.len() + && types + .iter() + .zip(parameters) + .all(|(argument, parameter)| argument.fits(parameter)) + && Self::type_parameters_agree(parameters, types) + } + + /// Whether each type parameter of `parameters` stands for one type in + /// `arguments`: `(Int, String)` doesn't fit `(T, T)`, as `T` would have to + /// be both. Inside a container a type parameter is the type there, as a + /// `List` makes `T` `Any`, and a value passed for `T` itself only has + /// to fit it: `push(List, String)` fits `push(List, T)`. What isn't + /// known yet, and an argument that's `Any` as a whole, which is checked as + /// the program runs, agree with anything. + pub fn type_parameters_agree(parameters: &[Self], arguments: &[Self]) -> bool { + let mut bound = Vec::new(); + for (parameter, argument) in parameters.iter().zip(arguments) { + argument.bind_type_parameters(parameter, false, &mut bound); + } + bound.iter().enumerate().all(|(i, (name, typ, inside))| { + let exact = bound + .iter() + .find(|(other, _, other_inside)| other == name && *other_inside); + match exact { + // What a container says it is decides, and a value passed + // for it fits it. + Some((_, exact, _)) if *inside => exact.agrees_with(typ), + Some((_, exact, _)) => { + *typ == Self::Any || typ.fits(exact) || typ.agrees_with(exact) + } + // Values passed for it are what it is, so they agree. + None => bound[..i] + .iter() + .filter(|(earlier, _, _)| earlier == name) + .all(|(_, earlier, _)| { + *typ == Self::Any || *earlier == Self::Any || earlier.agrees_with(typ) + }), + } }) } + /// Adds to `bound` the part of this type that each type parameter in + /// `parameter` is matched against, as [`Self::matches`] matches them, + /// with whether it's inside a container, where it has to be that type. + fn bind_type_parameters<'p>( + &self, + parameter: &'p Self, + inside: bool, + bound: &mut Vec<(&'p str, Self, bool)>, + ) { + match (self, parameter) { + (_, Self::Generic(name)) => bound.push((name, self.clone(), inside)), + // A sequence passed for an `Iterable` holds values passed for `T`. + (Self::Iterable(a), Self::Iterable(p)) => a.bind_type_parameters(p, inside, bound), + (argument, Self::Iterable(p)) if Self::is_sequence(argument) => { + if let Some(element) = argument.sequence_element_type() { + element.bind_type_parameters(p, inside, bound); + } + } + (Self::Option(a), Self::Option(p)) + | (Self::List(a), Self::List(p)) + | (Self::Iterator(a), Self::Iterator(p)) + | (Self::MinHeap(a), Self::MinHeap(p)) + | (Self::MaxHeap(a), Self::MaxHeap(p)) + | (Self::Deque(a), Self::Deque(p)) => a.bind_type_parameters(p, true, bound), + (Self::Tuple(a), Self::Tuple(p)) if a.len() == p.len() => { + for (a, p) in a.iter().zip(p) { + a.bind_type_parameters(p, true, bound); + } + } + (Self::Map { key: ak, value: av }, Self::Map { key: pk, value: pv }) => { + ak.bind_type_parameters(pk, true, bound); + av.bind_type_parameters(pv, true, bound); + } + _ => {} + } + } + + /// Whether this type and `other` could be one type, where what isn't + /// known yet in either could be anything. + fn agrees_with(&self, other: &Self) -> bool { + let unknown = |typ: &Self| { + matches!( + typ, + Self::Var(_) | Self::Generic(_) | Self::Never | Self::Error + ) + }; + if unknown(self) || unknown(other) { + return true; + } + match (self, other) { + (Self::Option(a), Self::Option(b)) + | (Self::List(a), Self::List(b)) + | (Self::Iterator(a), Self::Iterator(b)) + | (Self::Iterable(a), Self::Iterable(b)) + | (Self::MinHeap(a), Self::MinHeap(b)) + | (Self::MaxHeap(a), Self::MaxHeap(b)) + | (Self::Deque(a), Self::Deque(b)) => a.agrees_with(b), + (Self::Tuple(a), Self::Tuple(b)) => { + a.len() == b.len() && a.iter().zip(b).all(|(a, b)| a.agrees_with(b)) + } + (Self::Map { key: ak, value: av }, Self::Map { key: bk, value: bv }) => { + ak.agrees_with(bk) && av.agrees_with(bv) + } + ( + Self::Function { + parameters: ap, + return_type: ar, + }, + Self::Function { + parameters: bp, + return_type: br, + }, + ) => { + let parameters = match (ap, bp) { + (Some(a), Some(b)) => { + a.len() == b.len() && a.iter().zip(b).all(|(a, b)| a.agrees_with(b)) + } + _ => true, + }; + parameters && ar.agrees_with(br) + } + (a, b) => a == b, + } + } + pub fn unpack(&self) -> Option + '_>> { match self { // Any just unpacks to an infinite list of Any - Self::Any => Some(Box::new(std::iter::repeat(&Self::Any))), + // The same types as `is_unknown`, spelled out for this exhaustive match. + Self::Any | Self::Var(_) | Self::Generic(_) => { + Some(Box::new(std::iter::repeat(&Self::Any))) + } + Self::Error => Some(Box::new(std::iter::repeat(&Self::Error))), Self::List(elem) - | Self::Sequence(elem) + | Self::Iterable(elem) | Self::Iterator(elem) | Self::MinHeap(elem) | Self::MaxHeap(elem) @@ -710,7 +785,9 @@ impl StaticType { impl fmt::Display for StaticType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Any => write!(f, "Any"), + // A failed check's type shows as what it was before there was one. + Self::Any | Self::Error => write!(f, "Any"), + Self::Var(_) => write!(f, "_"), Self::Never => write!(f, "Never"), Self::Bool => write!(f, "Bool"), Self::Function { @@ -718,17 +795,17 @@ impl fmt::Display for StaticType { return_type, } => write!( f, - "Function({}) -> {return_type}", + "Fn({}) -> {return_type}", parameters .as_deref() .map_or(String::from("*"), |p| p.iter().join(", ")) ), - Self::Struct { name, .. } => write!(f, "{name}"), + Self::Generic(name) | Self::Struct { name, .. } => write!(f, "{name}"), Self::Option(elem) => write!(f, "Option<{elem}>"), Self::Number => write!(f, "Number"), Self::Float => write!(f, "Float"), Self::Int => write!(f, "Int"), - Self::Sequence(elem) => write!(f, "Sequence<{elem}>"), + Self::Iterable(elem) => write!(f, "Iterable<{elem}>"), Self::List(elem) => write!(f, "List<{elem}>"), Self::String => write!(f, "String"), Self::Tuple(tup) if tup.is_empty() => write!(f, "()"), @@ -763,13 +840,17 @@ mod test { use super::*; #[test] - fn test_list_of_type_compatibility() { + fn a_list_fits_only_a_list_of_its_own_element_type() { let list_of_two_tuple_int = StaticType::List(Box::new(StaticType::Tuple(vec![ StaticType::Int, StaticType::Int, ]))); let list_of_any = StaticType::List(Box::new(StaticType::Any)); - assert!(list_of_two_tuple_int.is_subtype(&list_of_any)); + assert!(!list_of_two_tuple_int.fits(&list_of_any)); + assert!(list_of_two_tuple_int.fits(&list_of_two_tuple_int)); + assert!(list_of_two_tuple_int.fits(&StaticType::Any)); + // ...but might once an `Any` it's passed for turns out to hold one. + assert!(StaticType::Any.may_fit(&list_of_two_tuple_int)); } #[test] @@ -787,13 +868,16 @@ mod test { StaticType::Int, ]))); - assert!(fun.is_fn_and_matches(&[list_of_two_tuple_int, StaticType::Int])); + // A list of tuples isn't a list of `Any`, which could get a String. + assert!(!fun.is_fn_and_matches(&[list_of_two_tuple_int.clone(), StaticType::Int])); + let generic = StaticType::signature("(List, Int) -> T").expect("valid signature"); + assert!(generic.is_fn_and_matches(&[list_of_two_tuple_int, StaticType::Int])); } #[test] fn test_overlaps() { let list_of = |elem: StaticType| StaticType::List(Box::new(elem)); - let seq_of = |elem: StaticType| StaticType::Sequence(Box::new(elem)); + let seq_of = |elem: StaticType| StaticType::Iterable(Box::new(elem)); // Mixed-direction relationships overlap: a List inhabits both. assert!(seq_of(StaticType::String).overlaps(&list_of(StaticType::Any))); @@ -835,10 +919,10 @@ mod test { #[test] fn public_numeric_types_are_siblings() { - assert!(!StaticType::Int.is_subtype(&StaticType::Number)); - assert!(!StaticType::Float.is_subtype(&StaticType::Number)); - assert_eq!(StaticType::Int.lub(&StaticType::Float), StaticType::Any); - assert_eq!(StaticType::Int.lub(&StaticType::Number), StaticType::Any); + assert!(!StaticType::Int.fits(&StaticType::Number)); + assert!(!StaticType::Float.fits(&StaticType::Number)); + assert_eq!(StaticType::Int.join(&StaticType::Float), StaticType::Any); + assert_eq!(StaticType::Int.join(&StaticType::Number), StaticType::Any); assert!(StaticType::from_name_and_args("Rational", vec![]).is_err()); assert!(StaticType::from_name_and_args("Complex", vec![]).is_err()); } diff --git a/ndc_diagnostics/src/lib.rs b/ndc_diagnostics/src/lib.rs index 064c1c31..794f7f2e 100644 --- a/ndc_diagnostics/src/lib.rs +++ b/ndc_diagnostics/src/lib.rs @@ -86,13 +86,16 @@ fn into_diagnostics(err: InterpreterError) -> Vec> { .iter() .map(|cause| { let span = cause.span(); + let mut labels = vec![ + Label::primary(span.source_id(), span.range()).with_message("related to this"), + ]; + labels.extend(cause.notes().iter().map(|(span, note)| { + Label::secondary(span.source_id(), span.range()).with_message(note) + })); let mut d = Diagnostic::error() .with_code("analysis") .with_message(cause.to_string()) - .with_labels(vec![ - Label::primary(span.source_id(), span.range()) - .with_message("related to this"), - ]); + .with_labels(labels); if let Some(help) = cause.help_text() { d = d.with_notes(vec![help.to_owned()]); } diff --git a/ndc_fmt/src/format.rs b/ndc_fmt/src/format.rs index 014483c7..7c13d076 100644 --- a/ndc_fmt/src/format.rs +++ b/ndc_fmt/src/format.rs @@ -208,9 +208,15 @@ impl<'a> Formatter<'a> { } Expression::Logical { .. } => self.operator_chain(expression), Expression::Grouping(inner) => concat([text("("), self.expr(inner), text(")")]), - Expression::Cast { value, annotation } => { - concat([self.expr(value), text(" as "), type_expr(annotation)]) - } + Expression::Cast { + value, + annotation, + optional, + } => concat([ + self.expr(value), + text(if *optional { " as? " } else { " as " }), + type_expr(annotation), + ]), Expression::VariableDeclaration { l_value, annotated_type, diff --git a/ndc_fmt/tests/style.rs b/ndc_fmt/tests/style.rs index 03dab4f2..3aefcdc4 100644 --- a/ndc_fmt/tests/style.rs +++ b/ndc_fmt/tests/style.rs @@ -24,6 +24,14 @@ fn spaces_go_around_operators_and_after_commas() { ); } +#[test] +fn a_cast_keeps_its_operator() { + assert_formats( + "let a = x as Int;let b = x as? List;", + "let a = x as Int;\nlet b = x as? List;\n", + ); +} + #[test] fn statements_are_indented_by_four_spaces() { assert_formats( diff --git a/ndc_highlight/src/lib.rs b/ndc_highlight/src/lib.rs index 9609b229..9385c3a2 100644 --- a/ndc_highlight/src/lib.rs +++ b/ndc_highlight/src/lib.rs @@ -151,6 +151,7 @@ fn kind( | Token::While | Token::Pure | Token::As + | Token::AsOptional | Token::LogicAnd | Token::LogicOr | Token::LogicNot => Kind::Keyword, diff --git a/ndc_layout/src/lib.rs b/ndc_layout/src/lib.rs index b91eb915..d0794941 100644 --- a/ndc_layout/src/lib.rs +++ b/ndc_layout/src/lib.rs @@ -8,9 +8,11 @@ //! them lives, once per batch. use ahash::AHashMap; -use ndc_analysis::{Analysis, Binding, CaptureSource, DeclaredStruct, FieldAccessors, ResolvedVar}; -use ndc_core::NodeId; +use ndc_analysis::{ + Analysis, Binding, CaptureSource, DeclaredStruct, FieldAccessors, Instance, ResolvedVar, +}; use ndc_core::r#struct::StructInfo; +use ndc_core::{NodeId, StaticType}; use ndc_resolver::{NameResolver, ScopeId, ScopeKind, SymbolId}; #[cfg(test)] @@ -33,6 +35,35 @@ pub struct Picks { /// Each struct declaration's struct, for [`Analysis::structs`], which also /// says where its field accessors live. pub structs: AHashMap, + /// For [`Analysis::instances`], by the symbol each instance is stored as. + pub instances: AHashMap, +} + +/// What one instance of a generic function picks in its body, where that +/// differs from the function's own picks. +#[derive(Debug)] +pub struct InstancePicks { + /// The generic function's declaration. + pub declaration: NodeId, + pub bindings: AHashMap>, + pub getters: AHashMap>, + pub setters: AHashMap>, + pub augmented_assignments: AHashMap>, + /// As [`Analysis::type_checks`], for the instance's body. + pub type_checks: AHashMap, +} + +impl InstancePicks { + pub fn new(declaration: NodeId) -> Self { + Self { + declaration, + bindings: AHashMap::new(), + getters: AHashMap::new(), + setters: AHashMap::new(), + augmented_assignments: AHashMap::new(), + type_checks: AHashMap::new(), + } + } } /// Where the resolver's symbols live at runtime. @@ -119,6 +150,25 @@ impl<'a> Layout<'a> { let locate_binding = |(node, binding): (NodeId, Binding)| { (node, binding.map(|symbol| locate_use(node, symbol))) }; + for (symbol, picks) in picks.instances { + let instance = Instance { + variable: self.location(symbol, None), + bindings: picks.bindings.into_iter().map(locate_binding).collect(), + getters: picks.getters.into_iter().map(locate_binding).collect(), + setters: picks.setters.into_iter().map(locate_binding).collect(), + augmented_assignments: picks + .augmented_assignments + .into_iter() + .map(locate_binding) + .collect(), + type_checks: picks.type_checks, + }; + analysis + .instances + .entry(picks.declaration) + .or_default() + .push(instance); + } analysis .bindings .extend(picks.bindings.into_iter().map(locate_binding)); diff --git a/ndc_lexer/src/token.rs b/ndc_lexer/src/token.rs index 116b779e..439554e1 100644 --- a/ndc_lexer/src/token.rs +++ b/ndc_lexer/src/token.rs @@ -83,6 +83,7 @@ pub enum Token { FatArrow, // => // Keywords As, + AsOptional, // as? Break, Continue, Else, @@ -155,6 +156,7 @@ impl fmt::Display for Token { Self::GreaterGreater => ">>", Self::Bang => "!", Self::As => "as", + Self::AsOptional => "as?", Self::Break => "break", Self::Continue => "continue", Self::Else => "else", @@ -332,6 +334,7 @@ impl From for Token { // Normal keywords "and" => Self::LogicAnd, "as" => Self::As, + "as?" => Self::AsOptional, "break" => Self::Break, "continue" => Self::Continue, "else" => Self::Else, diff --git a/ndc_lsp/src/features/completion.rs b/ndc_lsp/src/features/completion.rs index f38024de..8d005f9a 100644 --- a/ndc_lsp/src/features/completion.rs +++ b/ndc_lsp/src/features/completion.rs @@ -157,7 +157,7 @@ fn group_overloads<'a>( continue; }; if let (Some(recv), Some(params)) = (receiver_type, parameters) - && (params.is_empty() || !recv.is_subtype(¶ms[0])) + && (params.is_empty() || !recv.may_fit(¶ms[0])) { continue; } @@ -193,15 +193,15 @@ fn group_overloads<'a>( groups } -/// `[Int]` -> `Int`; `[List, Sequence]` -> `Sequence` (a +/// `[Int]` -> `Int`; `[List, Iterable]` -> `Iterable` (a /// candidate subsuming all others wins); `[Int, Float, Number]` -> /// `Int | Float | Number` (no candidate subsumes the others). fn merged_type_display(candidates: &[&StaticType]) -> String { - if let Some(supertype) = candidates + if let Some(widest) = candidates .iter() - .find(|t| candidates.iter().all(|c| c.is_subtype(t))) + .find(|t| candidates.iter().all(|c| c.fits(t))) { - return supertype.to_string(); + return widest.to_string(); } candidates .iter() @@ -212,8 +212,8 @@ fn merged_type_display(candidates: &[&StaticType]) -> String { /// Language keywords offered in general (non-dot) completion. const KEYWORDS: &[&str] = &[ - "let", "fn", "pure", "struct", "if", "else", "while", "for", "in", "as", "return", "break", - "continue", "and", "or", "not", "true", "false", "None", + "let", "fn", "pure", "struct", "if", "else", "while", "for", "in", "as", "as?", "return", + "break", "continue", "and", "or", "not", "true", "false", "None", ]; fn keyword_completions() -> impl Iterator { @@ -498,7 +498,7 @@ mod tests { #[test] fn merged_type_display_shows_a_common_supertype_or_a_union() { let list = StaticType::List(Box::new(StaticType::Int)); - let seq = StaticType::Sequence(Box::new(StaticType::Int)); + let seq = StaticType::Iterable(Box::new(StaticType::Int)); assert_eq!(merged_type_display(&[&list, &seq]), seq.to_string()); assert_eq!( merged_type_display(&[&StaticType::Int, &StaticType::Float]), diff --git a/ndc_lsp/src/features/inlay_hints.rs b/ndc_lsp/src/features/inlay_hints.rs index 83bbd3e1..c252d209 100644 --- a/ndc_lsp/src/features/inlay_hints.rs +++ b/ndc_lsp/src/features/inlay_hints.rs @@ -110,6 +110,14 @@ mod tests { assert!(has_label(&hints, |l| l == ": Int")); } + #[test] + fn a_failed_check_is_shown_as_any() { + // `len(true)` fails, and what it gives is shown as `Any`. + let hints = collect_hints("let n = len(true);\nlet m = [n, n];"); + assert!(has_label(&hints, |l| l == ": Any"), "{hints:?}"); + assert!(has_label(&hints, |l| l == ": List"), "{hints:?}"); + } + #[test] fn annotated_let_binding_skips_type_inlay() { let hints = collect_hints("let value: Int = 1;"); @@ -142,8 +150,11 @@ mod tests { #[test] fn unannotated_param_gets_inlay() { + // `foo` takes any argument, so its parameter's type is `foo`'s type + // parameter, which it returns. let hints = collect_hints("fn foo(x) { x }"); - assert!(has_label(&hints, |l| l == ": Any")); + assert!(has_label(&hints, |l| l == ": A"), "{hints:?}"); + assert!(has_label(&hints, |l| l == " -> A"), "{hints:?}"); } #[test] diff --git a/ndc_macros/Cargo.toml b/ndc_macros/Cargo.toml index 327f95f8..9a62eb55 100644 --- a/ndc_macros/Cargo.toml +++ b/ndc_macros/Cargo.toml @@ -9,6 +9,8 @@ license = "MIT" proc-macro = true [dependencies] +# Parses `signature = "…"` while the macro expands, so a bad one is a compile error. +ndc_core.workspace = true proc-macro2 = "1.0.106" quote = "1.0.46" syn = { version = "2.0.118", features = ["full", "extra-traits"] } diff --git a/ndc_macros/src/function.rs b/ndc_macros/src/function.rs index 6f174047..f99c1230 100644 --- a/ndc_macros/src/function.rs +++ b/ndc_macros/src/function.rs @@ -21,6 +21,7 @@ pub fn wrap_function(function: &syn::ItemFn) -> syn::Result )]; let mut return_type = None; + let mut signature: Option = None; let mut documentation_buffer = String::new(); for attr in &function.attrs { @@ -37,6 +38,10 @@ pub fn wrap_function(function: &syn::ItemFn) -> syn::Result let value: syn::Type = meta.value()?.parse()?; return_type = Some(map_type(&value)?); Ok(()) + } else if meta.path.is_ident("signature") { + // #[function(signature = "(List, T) -> Option")] + signature = Some(meta.value()?.parse()?); + Ok(()) } else { Err(meta.error("unsupported property on function")) } @@ -51,6 +56,16 @@ pub fn wrap_function(function: &syn::ItemFn) -> syn::Result } } + if let Some(signature) = &signature { + if return_type.is_some() { + return Err(syn::Error::new_spanned( + signature, + "a signature already has a return type: drop `return_type`", + )); + } + check_signature(signature, function.sig.inputs.len())?; + } + let return_type = match return_type { Some(t) => t, None => map_return_type(&function.sig.output)?, @@ -73,6 +88,7 @@ pub fn wrap_function(function: &syn::ItemFn) -> syn::Result &ident, function_name, &return_type, + signature.as_ref(), &documentation_buffer, ) }) @@ -278,11 +294,40 @@ fn map_type_path(p: &syn::TypePath) -> syn::Result { } } +/// Checks that a signature such as `(List, T)` parses, and that it +/// declares as many parameters as the Rust function takes. +fn check_signature(signature: &syn::LitStr, expected: usize) -> syn::Result<()> { + let parsed = ndc_core::StaticType::signature(&signature.value()) + .map_err(|error| syn::Error::new_spanned(signature, error))?; + let ndc_core::StaticType::Function { + parameters: Some(parameters), + .. + } = parsed + else { + return Err(syn::Error::new_spanned( + signature, + "a native function takes a fixed number of arguments, so `(*)` doesn't fit", + )); + }; + if parameters.len() == expected { + Ok(()) + } else { + Err(syn::Error::new_spanned( + signature, + format!( + "the signature declares {} parameters but the function takes {expected}", + parameters.len() + ), + )) + } +} + fn wrap_single( function: &syn::ItemFn, identifier: &syn::Ident, register_as_function_name: &proc_macro2::Literal, return_type: &TokenStream, + signature: Option<&syn::LitStr>, docs: &str, ) -> syn::Result { let inner_ident = format_ident!("{}_inner", identifier); @@ -300,6 +345,7 @@ fn wrap_single( register_as_function_name, docs, return_type, + signature, ) .ok_or_else(|| { syn::Error::new_spanned( @@ -350,6 +396,7 @@ fn try_generate_vm_native( fn_name: &proc_macro2::Literal, docs: &str, return_type_override: &TokenStream, + signature: Option<&syn::LitStr>, ) -> Option { use crate::vm_convert::{try_vm_input, try_vm_return}; @@ -415,15 +462,27 @@ fn try_generate_vm_native( } else { quote! { Some(String::from(#docs)) } }; + // A signature replaces the types read off the Rust parameters. + let static_type = if let Some(signature) = signature { + quote! { + ndc_core::StaticType::signature(#signature).unwrap_or_else(|error| { + panic!("invalid signature of {}: {error}", #fn_name) + }) + } + } else { + quote! { + ndc_core::StaticType::Function { + parameters: Some(vec![#(#param_types.clone()),*]), + return_type: Box::new(#return_static_type), + } + } + }; let native_let = quote! { let native: std::rc::Rc = std::rc::Rc::new(ndc_vm::value::NativeFunction { name: String::from(#fn_name), documentation: #documentation_tokens, - static_type: ndc_core::StaticType::Function { - parameters: Some(vec![#(#param_types.clone()),*]), - return_type: Box::new(#return_static_type), - }, + static_type: #static_type, func: #func_variant, }); }; diff --git a/ndc_macros/src/vm_convert.rs b/ndc_macros/src/vm_convert.rs index 41ba731e..3aee245d 100644 --- a/ndc_macros/src/vm_convert.rs +++ b/ndc_macros/src/vm_convert.rs @@ -366,7 +366,7 @@ pub fn try_vm_input(ty: &syn::Type, position: usize) -> Option { NdcType::SeqValue => VmInputArg { extract: quote! {}, pass: quote! { #raw.clone() }, - static_type: quote! { ndc_core::StaticType::Sequence(Box::new(ndc_core::StaticType::Any)) }, + static_type: quote! { ndc_core::StaticType::Iterable(Box::new(ndc_core::StaticType::Any)) }, }, NdcType::MapValue => VmInputArg { diff --git a/ndc_parser/src/expression.rs b/ndc_parser/src/expression.rs index fa24d990..56147bec 100644 --- a/ndc_parser/src/expression.rs +++ b/ndc_parser/src/expression.rs @@ -35,9 +35,12 @@ pub enum Expression { }, Grouping(Box), /// `value as Type` — asserts the value has the target type; never converts. + /// `value as? Type` (`optional`) — `Some(value)` when the value has the + /// target type, `None` when it doesn't. Cast { value: Box, annotation: TypeExpr, + optional: bool, }, VariableDeclaration { l_value: BindingPatternLocation, diff --git a/ndc_parser/src/parser.rs b/ndc_parser/src/parser.rs index 49cc45a9..0775d419 100644 --- a/ndc_parser/src/parser.rs +++ b/ndc_parser/src/parser.rs @@ -650,12 +650,13 @@ impl Parser { fn cast(&mut self) -> Result { let mut expression = self.tight_unary()?; - while self.consume_token_if(&[Token::As]).is_some() { + while let Some(operator) = self.consume_token_if(&[Token::As, Token::AsOptional]) { let annotation = self.cast_type_annotation()?; let span = expression.span.merge(annotation.span()); expression = Expression::Cast { value: Box::new(expression), annotation, + optional: operator.token == Token::AsOptional, } .to_location(span); } @@ -1428,6 +1429,11 @@ impl Parser { }; match token { + Token::Identifier(name) + if name == "Fn" && self.peek_next_token() == Some(&Token::LeftParentheses) => + { + self.function_type() + } Token::Identifier(_) => self.named_or_generic_type(), Token::LeftParentheses => self.tuple_type(), _ => Err(Error::with_help( @@ -1617,6 +1623,29 @@ impl Parser { Ok(greater_span) } + /// Parses `Fn(A, B) -> C`, where `-> C` is optional. + fn function_type(&mut self) -> Result { + let start = self.require_current_token()?.span; + let (parameters, parameters_span) = self.delimited_comma_separated( + &Token::LeftParentheses, + &Token::RightParentheses, + Self::type_annotation, + true, + )?; + let (return_type, span) = if self.consume_token_if(&[Token::RightArrow]).is_some() { + let return_type = self.type_annotation()?; + let span = start.merge(return_type.span()); + (Some(Box::new(return_type)), span) + } else { + (None, start.merge(parameters_span)) + }; + Ok(TypeExpr::Function { + parameters, + return_type, + span, + }) + } + pub fn tuple_type(&mut self) -> Result { let (elements, span) = self.delimited_comma_separated( &Token::LeftParentheses, diff --git a/ndc_parser/src/type_expr.rs b/ndc_parser/src/type_expr.rs index fc11594b..b07c6a33 100644 --- a/ndc_parser/src/type_expr.rs +++ b/ndc_parser/src/type_expr.rs @@ -18,13 +18,22 @@ pub enum TypeExpr { }, /// A tuple type: `(Int, String)`. The unit type `()` is the empty tuple. Tuple { elements: Vec, span: Span }, + /// A function type: `Fn(Int, Int) -> Int`. Without `-> …` the function + /// returns `()`. + Function { + parameters: Vec, + return_type: Option>, + span: Span, + }, } impl TypeExpr { #[must_use] pub fn span(&self) -> Span { match self { - Self::Name { span, .. } | Self::Tuple { span, .. } => *span, + Self::Name { span, .. } | Self::Tuple { span, .. } | Self::Function { span, .. } => { + *span + } } } } @@ -56,6 +65,19 @@ impl fmt::Display for TypeExpr { join(f, elements)?; write!(f, ")") } + Self::Function { + parameters, + return_type, + .. + } => { + write!(f, "Fn(")?; + join(f, parameters)?; + write!(f, ")")?; + match return_type { + Some(return_type) => write!(f, " -> {return_type}"), + None => Ok(()), + } + } } } } diff --git a/ndc_parser/tests/type_annotations.rs b/ndc_parser/tests/type_annotations.rs index 9455a2c1..5886d7a6 100644 --- a/ndc_parser/tests/type_annotations.rs +++ b/ndc_parser/tests/type_annotations.rs @@ -29,6 +29,18 @@ fn names(annotation: &TypeExpr, found: &mut Vec<(String, NodeId)>) { names(element, found); } } + TypeExpr::Function { + parameters, + return_type, + .. + } => { + for parameter in parameters { + names(parameter, found); + } + if let Some(return_type) = return_type { + names(return_type, found); + } + } } } @@ -89,3 +101,36 @@ fn a_cast_target_before_a_comparison_gets_its_own_id() { }); assert_distinct(ids); } + +#[test] +fn a_function_type_has_parameters_and_an_optional_return_type() { + for (source, shown) in [ + ( + "let f: Fn(Int, String) -> Bool = g", + "Fn(Int, String) -> Bool", + ), + ("let f: Fn() = g", "Fn()"), + ( + "let f: Fn(Fn(Int) -> Int) -> List = g", + "Fn(Fn(Int) -> Int) -> List", + ), + ( + "fn h(f: Fn(Int) -> Int, x: Int) -> Fn(Int) -> Int { f }", + "Fn(Int) -> Int", + ), + ] { + let expression = parse_one(source); + let annotation = match expression.expression { + Expression::VariableDeclaration { + annotated_type: Some(annotation), + .. + } => annotation, + Expression::FunctionDeclaration { parameters, .. } => parameters[0] + .annotation + .clone() + .expect("the first parameter is annotated"), + other => panic!("unexpected expression {other:?}"), + }; + assert_eq!(annotation.to_string(), shown, "{source}"); + } +} diff --git a/ndc_resolver/src/name_resolution.rs b/ndc_resolver/src/name_resolution.rs index 00fb7c32..5af00797 100644 --- a/ndc_resolver/src/name_resolution.rs +++ b/ndc_resolver/src/name_resolution.rs @@ -151,7 +151,7 @@ impl NameResolutionError { /// within a scope. For `x` in `let x = 1; { let x = 2; x }`: `[[x2], [x1]]`. /// Identifiers always have at least one candidate, augmented operators may /// not. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Candidates(Vec>); /// The two operations an augmented assignment can use. For `xs ++= ys`: @@ -263,9 +263,24 @@ impl ScopeTree { } pub(crate) fn bind(&mut self, symbol_id: SymbolId) { - let position = self.scopes[self.current.0].bindings.len(); - self.scopes[self.current.0].bind(symbol_id); - self.bound_in.insert(symbol_id, (self.current, position)); + self.bind_in(self.current, symbol_id); + } + + /// Binds a value in `scope`, which may not be the current one. + fn bind_in(&mut self, scope: ScopeId, symbol_id: SymbolId) { + let position = self.scopes[scope.0].bindings.len(); + self.scopes[scope.0].bind(symbol_id); + self.bound_in.insert(symbol_id, (scope, position)); + } + + /// The program's top-level scope. + fn program(&self) -> ScopeId { + let position = self + .scopes + .iter() + .position(|scope| scope.kind == ScopeKind::Program) + .expect("internal error: the resolver starts in the program scope"); + ScopeId(position) } /// Binds a type name in the current scope. Types aren't runtime values, so @@ -460,6 +475,17 @@ impl NameResolver { natives.into_iter().map(|(&symbol, _)| symbol).collect() } + /// Declares a top-level value named `name` that no code refers to by + /// name, as no identifier can be spelled like it: an instance of a generic + /// function, which the analyser picks for the calls that use it. Being + /// top-level, it's a global. + pub fn declare_instance(&mut self, name: String) -> SymbolId { + let symbol = self.symbols.declare(name, BindingKind::Function); + let program = self.scopes.program(); + self.scopes.bind_in(program, symbol); + symbol + } + /// The symbol a resolved declaration introduced: a binding pattern identifier's /// or a named function's, keyed by its node. pub fn declared_symbol(&self, node: NodeId) -> SymbolId { @@ -1000,6 +1026,18 @@ impl NameResolver { self.resolve_type(element)?; } } + TypeExpr::Function { + parameters, + return_type, + .. + } => { + for parameter in parameters { + self.resolve_type(parameter)?; + } + if let Some(return_type) = return_type { + self.resolve_type(return_type)?; + } + } } Ok(()) } diff --git a/ndc_stdlib/Cargo.toml b/ndc_stdlib/Cargo.toml index 12cbf99d..f95d0aed 100644 --- a/ndc_stdlib/Cargo.toml +++ b/ndc_stdlib/Cargo.toml @@ -23,6 +23,9 @@ serde_json = { workspace = true, optional = true } md5 = { version = "0.8.0", optional = true } sha1 = { version = "0.11.0", optional = true } +[dev-dependencies] +ndc_analyser.workspace = true + [features] default = ["crypto", "rand", "regex", "serde"] crypto = ["dep:md5", "dep:sha1"] diff --git a/ndc_stdlib/src/aoc.rs b/ndc_stdlib/src/aoc.rs index a35ffb66..16e9ae16 100644 --- a/ndc_stdlib/src/aoc.rs +++ b/ndc_stdlib/src/aoc.rs @@ -7,6 +7,7 @@ mod inner { use std::rc::Rc; /// Counts the occurrences of each item in a sequence and returns a map with the frequencies. + #[function(signature = "(Iterable) -> Map")] pub fn frequencies(seq: SeqValue) -> anyhow::Result { let mut counts: HashMap = HashMap::new(); diff --git a/ndc_stdlib/src/cmp.rs b/ndc_stdlib/src/cmp.rs index 32e0244d..8d1e2c86 100644 --- a/ndc_stdlib/src/cmp.rs +++ b/ndc_stdlib/src/cmp.rs @@ -16,7 +16,15 @@ mod inner { } } + /// Stops the program with the error `message`. It never returns, so it + /// can end a branch whose other branches give a value. + #[function(signature = "(String) -> Never")] + pub fn panic(message: &str) -> anyhow::Result { + Err(anyhow!(message.to_string())) + } + /// Produces an error if the arguments aren't equal to each other. + #[function(signature = "(A, B)")] pub fn assert_eq(left: Value, right: Value) -> anyhow::Result<()> { if left == right { Ok(()) @@ -28,6 +36,7 @@ mod inner { } /// Produces an error if the arguments are equal to each other. + #[function(signature = "(A, B)")] pub fn assert_ne(left: Value, right: Value) -> anyhow::Result<()> { if left == right { Err(anyhow!(format!( @@ -49,28 +58,38 @@ mod inner { } /// Returns the larger of `left` and `right`, preferring `left` if they are equal. + #[function(signature = "(T, T) -> T")] pub fn max(left: Value, right: Value) -> anyhow::Result { + larger(left, right) + } + + /// Returns the smaller of `left` and `right`, preferring `left` if they are equal. + #[function(signature = "(T, T) -> T")] + pub fn min(left: Value, right: Value) -> anyhow::Result { + smaller(left, right) + } + + fn larger(left: Value, right: Value) -> anyhow::Result { match left.partial_cmp(&right) { Some(Ordering::Equal | Ordering::Greater) => Ok(left), Some(Ordering::Less) => Ok(right), - None => Err(anyhow!( - "cannot compare {} and {}", - left.static_type(), - right.static_type() - )), + None => Err(incomparable(&left, &right)), } } - /// Returns the smaller of `left` and `right`, preferring `left` if they are equal. - pub fn min(left: Value, right: Value) -> anyhow::Result { + fn smaller(left: Value, right: Value) -> anyhow::Result { match left.partial_cmp(&right) { Some(Ordering::Equal | Ordering::Less) => Ok(left), Some(Ordering::Greater) => Ok(right), - None => Err(anyhow!( - "cannot compare {} and {}", - left.static_type(), - right.static_type() - )), + None => Err(incomparable(&left, &right)), } } + + fn incomparable(left: &Value, right: &Value) -> anyhow::Error { + anyhow!( + "cannot compare {} and {}", + left.static_type(), + right.static_type() + ) + } } diff --git a/ndc_stdlib/src/deque.rs b/ndc_stdlib/src/deque.rs index e22f0c78..79376778 100644 --- a/ndc_stdlib/src/deque.rs +++ b/ndc_stdlib/src/deque.rs @@ -8,7 +8,7 @@ mod inner { /// Creates a new `Deque` type. /// /// The `Deque` type allows the user to quickly append and remove elements from both the start and the end of the list. - #[function(name = "Deque", return_type = VecDeque)] + #[function(name = "Deque", signature = "() -> Deque")] pub fn create_deque() -> Value { Value::Object(std::rc::Rc::new(Object::Deque(std::cell::RefCell::new( VecDeque::new(), @@ -16,16 +16,19 @@ mod inner { } /// Pushes the `value` to the front of the `deque`. + #[function(signature = "(Deque, T)")] pub fn push_front(deque: &mut VecDeque, value: Value) { deque.push_front(value); } /// Appends the `value` to the end of the `deque`. + #[function(signature = "(Deque, T)")] pub fn push_back(deque: &mut VecDeque, value: Value) { deque.push_back(value); } /// Removes and returns the first element of the `deque`. + #[function(signature = "(Deque) -> T")] pub fn pop_front(deque: &mut VecDeque) -> anyhow::Result { deque .pop_front() @@ -33,7 +36,7 @@ mod inner { } /// Removes and returns the first element of the `deque` as an `Option` returning `None` if the queue is empty. - #[function(name = "pop_front?", return_type = Option)] + #[function(name = "pop_front?", signature = "(Deque) -> Option")] pub fn maybe_pop_front(deque: &mut VecDeque) -> Value { match deque.pop_front() { None => Value::None, @@ -42,6 +45,7 @@ mod inner { } /// Removes and returns the last element of the `deque`. + #[function(signature = "(Deque) -> T")] pub fn pop_back(deque: &mut VecDeque) -> anyhow::Result { deque .pop_back() @@ -49,7 +53,7 @@ mod inner { } /// Removes and returns the last element of the `deque` as an `Option` returning `None` if the queue is empty. - #[function(name = "pop_back?", return_type = Option)] + #[function(name = "pop_back?", signature = "(Deque) -> Option")] pub fn maybe_pop_back(deque: &mut VecDeque) -> Value { match deque.pop_back() { None => Value::None, @@ -58,6 +62,7 @@ mod inner { } /// Returns (but does not remove) the first element of the `deque`. + #[function(signature = "(Deque) -> T")] pub fn front(deque: &VecDeque) -> anyhow::Result { deque .front() @@ -66,7 +71,7 @@ mod inner { } /// Returns (but does not remove) the first element of the `deque` as an `Option` returning `None` if the queue is empty. - #[function(name = "front?", return_type = Option)] + #[function(name = "front?", signature = "(Deque) -> Option")] pub fn maybe_front(deque: &VecDeque) -> Value { match deque.front().cloned() { None => Value::None, @@ -75,6 +80,7 @@ mod inner { } /// Returns (but does not remove) the last element of the `deque`. + #[function(signature = "(Deque) -> T")] pub fn back(deque: &VecDeque) -> anyhow::Result { deque .back() @@ -83,7 +89,7 @@ mod inner { } /// Returns (but does not remove) the last element of the `deque` as an `Option` returning `None` if the queue is empty. - #[function(name = "back?", return_type = Option)] + #[function(name = "back?", signature = "(Deque) -> Option")] pub fn maybe_back(deque: &VecDeque) -> Value { match deque.back().cloned() { None => Value::None, @@ -92,16 +98,19 @@ mod inner { } /// Returns `true` if the `deque` contains the `value` or `false` otherwise. + #[function(signature = "(Deque, T) -> Bool")] pub fn contains(deque: &VecDeque, value: Value) -> bool { deque.contains(&value) } /// Returns `true` if the `deque` is empty and `false` otherwise. + #[function(signature = "(Deque) -> Bool")] pub fn is_empty(deque: &VecDeque) -> bool { deque.is_empty() } /// Removes all elements from the `deque` + #[function(signature = "(Deque)")] pub fn clear(deque: &mut VecDeque) { deque.clear(); } diff --git a/ndc_stdlib/src/hash_map.rs b/ndc_stdlib/src/hash_map.rs index e085fbb9..1cbd48c3 100644 --- a/ndc_stdlib/src/hash_map.rs +++ b/ndc_stdlib/src/hash_map.rs @@ -9,7 +9,7 @@ mod inner { /// Returns a list of all the keys in the map or set. /// /// Note that for a set this will return the values in the set. - #[function(return_type = Vec<_>)] + #[function(signature = "(Map) -> List")] pub fn keys(map: &mut HashMap) -> Value { Value::Object(Rc::new(Object::list( map.keys().cloned().collect::>(), @@ -19,7 +19,7 @@ mod inner { /// Returns a list of all the values in the map. /// /// Note that for sets this will return a list of unit types, you should use keys if you want the values in the set. - #[function(return_type = Vec<_>)] + #[function(signature = "(Map) -> List")] pub fn values(map: &mut HashMap) -> Value { Value::Object(Rc::new(Object::list( map.values().cloned().collect::>(), @@ -27,12 +27,13 @@ mod inner { } /// Removes a key from the map or a value from a set. + #[function(signature = "(Map, K)")] pub fn remove(map: &mut HashMap, key: Value) { map.remove(&key); } /// Removes all keys from the `left` map/set that are present in the `right` map/set. - #[function(name = "remove")] + #[function(name = "remove", signature = "(Map, Map)")] pub fn remove_map(left: &mut HashMap, right: &HashMap) { for (key, _) in right { left.remove(key); @@ -40,24 +41,28 @@ mod inner { } /// Insert a value into a map. - #[function(name = "insert")] + #[function(name = "insert", signature = "(Map, K, V)")] pub fn insert_map(map: &mut HashMap, key: Value, value: Value) { map.insert(key, value); } /// Inserts a value into a set. - #[function(name = "insert")] + #[function(name = "insert", signature = "(Map, K)")] pub fn insert_set(map: &mut HashMap, key: Value) { map.insert(key, Value::unit()); } /// Returns true if the map or set contains no elements. + #[function(signature = "(Map) -> Bool")] pub fn is_empty(map: &HashMap) -> bool { map.is_empty() } /// Intersection-assign: retains only elements present in both maps or sets. - #[function(name = "&=")] + #[function( + name = "&=", + signature = "(Map, Map) -> Map" + )] pub fn intersect_assign( lhs: MapValue, rhs: &HashMap, @@ -75,7 +80,7 @@ mod inner { } /// Union-assign: adds all elements from the right map or set into the left. - #[function(name = "|=")] + #[function(name = "|=", signature = "(Map, Map) -> Map")] pub fn union_assign(lhs: MapValue, rhs: &HashMap) -> anyhow::Result { { let Value::Object(ref obj) = lhs else { @@ -93,7 +98,10 @@ mod inner { } /// Difference-assign: removes all elements from the left map or set that are present in the right. - #[function(name = "-=")] + #[function( + name = "-=", + signature = "(Map, Map) -> Map" + )] pub fn difference_assign( lhs: MapValue, rhs: &HashMap, @@ -111,7 +119,7 @@ mod inner { } /// Symmetric-difference-assign: retains only elements present in exactly one of the two maps or sets. - #[function(name = "~=")] + #[function(name = "~=", signature = "(Map, Map) -> Map")] pub fn symmetric_difference_assign( lhs: MapValue, rhs: &HashMap, @@ -132,7 +140,7 @@ mod inner { /// Returns the union (elements that are in either `left` or `right`) of two maps or sets. /// /// This is the same as evaluating the expression `left | right`. - #[function(alias = "|")] + #[function(alias = "|", signature = "(Map, Map) -> Map")] pub fn union(left: MapValue, right: &HashMap) -> anyhow::Result { let Value::Object(ref obj) = left else { anyhow::bail!("| requires a map on the left side"); @@ -154,7 +162,10 @@ mod inner { /// Returns the intersection (elements that are in both `left` and `right`) of two maps or sets. /// /// This is the same as evaluating the expression `left & right`. - #[function(alias = "&")] + #[function( + alias = "&", + signature = "(Map, Map) -> Map" + )] pub fn intersection(left: MapValue, right: &HashMap) -> anyhow::Result { let Value::Object(ref obj) = left else { anyhow::bail!("& requires a map on the left side"); @@ -176,7 +187,7 @@ mod inner { /// Returns the symmetric difference (elements that are either in `left` or `right` but not both) of two maps or sets. /// /// This is the same as evaluating the expression `left ~ right`. - #[function(alias = "~")] + #[function(alias = "~", signature = "(Map, Map) -> Map")] pub fn symmetric_difference( left: MapValue, right: &HashMap, @@ -199,7 +210,7 @@ mod inner { } /// Converts the given sequence to set. - #[function(return_type = Map<_, ()>)] + #[function(signature = "(Iterable) -> Map")] pub fn set(seq: SeqValue) -> anyhow::Result { use ndc_vm::hash_map::HashMap; diff --git a/ndc_stdlib/src/heap.rs b/ndc_stdlib/src/heap.rs index a49a99f4..58373ba1 100644 --- a/ndc_stdlib/src/heap.rs +++ b/ndc_stdlib/src/heap.rs @@ -7,7 +7,7 @@ mod inner { use std::collections::BinaryHeap; /// Creates a new empty min-heap (priority queue where the smallest element is popped first). - #[function(name = "MinHeap", return_type = MinHeap<_>)] + #[function(name = "MinHeap", signature = "() -> MinHeap")] pub fn create_min_heap() -> Value { Value::Object(std::rc::Rc::new(Object::MinHeap(std::cell::RefCell::new( BinaryHeap::new(), @@ -15,7 +15,7 @@ mod inner { } /// Creates a new empty max-heap (priority queue where the largest element is popped first). - #[function(name = "MaxHeap", return_type = MaxHeap<_>)] + #[function(name = "MaxHeap", signature = "() -> MaxHeap")] pub fn create_max_heap() -> Value { Value::Object(std::rc::Rc::new(Object::MaxHeap(std::cell::RefCell::new( BinaryHeap::new(), @@ -23,7 +23,7 @@ mod inner { } /// Removes and returns the smallest element from the min-heap, or `None` if empty. - #[function(name = "pop?", return_type = Option)] + #[function(name = "pop?", signature = "(MinHeap) -> Option")] pub fn maybe_min_pop(heap: &mut BinaryHeap>) -> Value { match heap.pop() { None => Value::None, @@ -32,7 +32,7 @@ mod inner { } /// Removes and returns the largest element from the max-heap, or `None` if empty. - #[function(name = "pop?", return_type = Option)] + #[function(name = "pop?", signature = "(MaxHeap) -> Option")] pub fn maybe_max_pop(heap: &mut BinaryHeap) -> Value { match heap.pop() { None => Value::None, @@ -41,7 +41,7 @@ mod inner { } /// Removes and returns the smallest element from the min-heap, or errors if empty. - #[function(name = "pop")] + #[function(name = "pop", signature = "(MinHeap) -> T")] pub fn min_pop(heap: &mut BinaryHeap>) -> anyhow::Result { heap.pop() .map(|Reverse(OrdValue(v))| v) @@ -49,7 +49,7 @@ mod inner { } /// Removes and returns the largest element from the max-heap, or errors if empty. - #[function(name = "pop")] + #[function(name = "pop", signature = "(MaxHeap) -> T")] pub fn max_pop(heap: &mut BinaryHeap) -> anyhow::Result { heap.pop() .map(|OrdValue(v)| v) @@ -57,13 +57,13 @@ mod inner { } /// Pushes a value onto the min-heap. - #[function(name = "push")] + #[function(name = "push", signature = "(MinHeap, T)")] pub fn min_push(heap: &mut BinaryHeap>, value: Value) { heap.push(Reverse(OrdValue(value))); } /// Pushes a value onto the max-heap. - #[function(name = "push")] + #[function(name = "push", signature = "(MaxHeap, T)")] pub fn max_push(heap: &mut BinaryHeap, value: Value) { heap.push(OrdValue(value)); } diff --git a/ndc_stdlib/src/index.rs b/ndc_stdlib/src/index.rs index e9a58e0f..98b29ae6 100644 --- a/ndc_stdlib/src/index.rs +++ b/ndc_stdlib/src/index.rs @@ -55,19 +55,18 @@ fn register_get(env: &mut FunctionRegistry>) { let doc = "Retrieves an element by index. Supports negative indices and range slicing.".to_string(); - // [] -> Any - env.declare_global_fn(Rc::new(NativeFunction { - name: "[]".to_string(), - documentation: Some(doc.clone()), - static_type: StaticType::Function { - parameters: Some(vec![ - StaticType::List(Box::new(StaticType::Any)), - StaticType::Any, - ]), - return_type: Box::new(StaticType::Any), - }, - func: make_get_func_simple(), - })); + // An element by its index, or a slice by a range of indices. + for signature in [ + "(List, Int) -> T", + "(List, Iterator) -> List", + ] { + env.declare_global_fn(Rc::new(NativeFunction { + name: "[]".to_string(), + documentation: Some(doc.clone()), + static_type: StaticType::signature(signature).expect("valid signature"), + func: make_get_func_simple(), + })); + } // [] -> String env.declare_global_fn(Rc::new(NativeFunction { @@ -93,34 +92,23 @@ fn register_get(env: &mut FunctionRegistry>) { func: make_get_func(), })); - // [] -> Any - env.declare_global_fn(Rc::new(NativeFunction { - name: "[]".to_string(), - documentation: Some(doc.clone()), - static_type: StaticType::Function { - parameters: Some(vec![ - StaticType::Deque(Box::new(StaticType::Any)), - StaticType::Any, - ]), - return_type: Box::new(StaticType::Any), - }, - func: make_get_func_simple(), - })); + // A deque is indexed like a list, and a range of it is a list too. + for signature in [ + "(Deque, Int) -> T", + "(Deque, Iterator) -> List", + ] { + env.declare_global_fn(Rc::new(NativeFunction { + name: "[]".to_string(), + documentation: Some(doc.clone()), + static_type: StaticType::signature(signature).expect("valid signature"), + func: make_get_func_simple(), + })); + } - // [] -> Any env.declare_global_fn(Rc::new(NativeFunction { name: "[]".to_string(), documentation: Some(doc), - static_type: StaticType::Function { - parameters: Some(vec![ - StaticType::Map { - key: Box::new(StaticType::Any), - value: Box::new(StaticType::Any), - }, - StaticType::Any, - ]), - return_type: Box::new(StaticType::Any), - }, + static_type: StaticType::signature("(Map, K) -> V").expect("valid signature"), func: make_get_func(), })); } @@ -128,20 +116,18 @@ fn register_get(env: &mut FunctionRegistry>) { fn register_set(env: &mut FunctionRegistry>) { let doc = "Sets an element by index. Supports negative indices and range slicing for lists and strings.".to_string(); - // []= -> () - env.declare_global_fn(Rc::new(NativeFunction { - name: "[]=".to_string(), - documentation: Some(doc.clone()), - static_type: StaticType::Function { - parameters: Some(vec![ - StaticType::List(Box::new(StaticType::Any)), - StaticType::Any, - StaticType::Any, - ]), - return_type: Box::new(StaticType::unit()), - }, - func: make_set_func(), - })); + // An element by its index, or a slice by a range of indices. + for signature in [ + "(List, Int, T)", + "(List, Iterator, Iterable)", + ] { + env.declare_global_fn(Rc::new(NativeFunction { + name: "[]=".to_string(), + documentation: Some(doc.clone()), + static_type: StaticType::signature(signature).expect("valid signature"), + func: make_set_func(), + })); + } // []= -> () env.declare_global_fn(Rc::new(NativeFunction { @@ -158,21 +144,10 @@ fn register_set(env: &mut FunctionRegistry>) { func: make_set_func(), })); - // []= -> () env.declare_global_fn(Rc::new(NativeFunction { name: "[]=".to_string(), documentation: Some(doc), - static_type: StaticType::Function { - parameters: Some(vec![ - StaticType::Map { - key: Box::new(StaticType::Any), - value: Box::new(StaticType::Any), - }, - StaticType::Any, - StaticType::Any, - ]), - return_type: Box::new(StaticType::unit()), - }, + static_type: StaticType::signature("(Map, K, V)").expect("valid signature"), func: make_set_func(), })); } @@ -480,21 +455,22 @@ fn vm_set_at_index(container: &Value, index_value: &Value, rhs: Value) -> Result list[idx] = rhs; } VmOffset::Range(from, to) => { - let rhs_vec = match rhs { - Value::Object(o) => match o.as_ref() { - Object::List(l) => l.borrow().clone(), - Object::Tuple(t) => t.clone(), - _ => { - return Err(VmError::native( - "cannot assign non-list to list slice", - )); - } - }, - _ => { - return Err(VmError::native( - "cannot assign non-list to list slice", - )); + // Any sequence goes in a slice: a list, a tuple, a + // range or another iterator. + let rhs_vec = match &rhs { + Value::Object(o) if matches!(o.as_ref(), Object::List(_)) => { + let Object::List(l) = o.as_ref() else { + unreachable!("matched a list"); + }; + l.borrow().clone() } + _ => rhs + .clone() + .try_into_iter() + .ok_or_else(|| { + VmError::native("cannot assign non-sequence to list slice") + })? + .collect(), }; let tail: Vec = list.drain(from..).collect(); list.extend(rhs_vec); diff --git a/ndc_stdlib/src/lib.rs b/ndc_stdlib/src/lib.rs index ef1f503a..399e4c09 100644 --- a/ndc_stdlib/src/lib.rs +++ b/ndc_stdlib/src/lib.rs @@ -49,3 +49,67 @@ pub fn register(env: &mut FunctionRegistry>) { string::register(env); value::register(env); } + +#[cfg(test)] +mod tests { + use super::*; + use ndc_core::StaticType; + + /// A call that fits two overloads in one scope must find one of them + /// more specific, so it never has to choose between generic overloads. + #[test] + fn no_two_overloads_tie() { + let mut registry = FunctionRegistry::default(); + register(&mut registry); + let natives = registry + .iter() + .map(|function| (function.name.clone(), function.static_type.clone())) + .collect::>(); + assert_eq!(ndc_analyser::tied_natives(&natives), Vec::::new()); + } + + /// `Iterable` takes any kind of sequence, which only a parameter can + /// do, or what a function passed for one returns, as `flat_map`'s + /// callback does. What a function returns is one kind of sequence. + #[test] + fn an_iterable_is_only_a_parameter() { + let iterable = + |typ: &StaticType| typ.contains(&|part| matches!(part, StaticType::Iterable(_))); + let mut registry = FunctionRegistry::default(); + register(&mut registry); + for function in registry.iter() { + let StaticType::Function { + parameters, + return_type, + } = &function.static_type + else { + continue; + }; + assert!( + !iterable(return_type), + "{} returns an Iterable", + function.name + ); + for parameter in parameters.iter().flatten() { + let nested = match parameter { + StaticType::Iterable(element) => iterable(element), + StaticType::Function { + parameters, + return_type, + } => { + parameters.iter().flatten().any(iterable) + || matches!(&**return_type, StaticType::Iterable(element) if iterable(element)) + || (!matches!(&**return_type, StaticType::Iterable(_)) + && iterable(return_type)) + } + other => iterable(other), + }; + assert!( + !nested, + "{} takes an Iterable inside another type", + function.name + ); + } + } + } +} diff --git a/ndc_stdlib/src/list.rs b/ndc_stdlib/src/list.rs index f4be2b66..eff81f22 100644 --- a/ndc_stdlib/src/list.rs +++ b/ndc_stdlib/src/list.rs @@ -9,7 +9,7 @@ mod inner { use num::{BigInt, ToPrimitive}; /// Converts any sequence into a list - #[function(return_type = Vec<_>)] + #[function(signature = "(Iterable) -> List")] pub fn list(seq: SeqValue) -> anyhow::Result { Ok(Value::list( seq.try_into_iter() @@ -19,30 +19,39 @@ mod inner { } /// Returns `true` if the list contains the given element. + #[function(signature = "(List, T) -> Bool")] pub fn contains(list: &[Value], elem: Value) -> bool { list.contains(&elem) } /// Returns `true` if the list contains the given subsequence. + #[function(signature = "(List, List) -> Bool")] pub fn contains_subsequence(list: &[Value], subsequence: &[Value]) -> bool { list.windows(subsequence.len()).contains(&subsequence) } - /// Returns the starting index of the first occurrence of `subsequence` in `list`, or unit if not found. - pub fn find_subsequence(list: &[Value], subsequence: &[Value]) -> Value { - let result = list - .windows(subsequence.len()) - .enumerate() - .find(|(_, seq)| *seq == subsequence) - .map(|(idx, _)| idx); - if let Some(result) = result { - Value::Int(result as i64) - } else { - Value::unit() + /// Returns the starting index of the first occurrence of `subsequence` in `list`, or produces an error if there is none. + #[function(signature = "(List, List) -> Int")] + pub fn find_subsequence(list: &[Value], subsequence: &[Value]) -> anyhow::Result { + subsequence_start(list, subsequence) + .map(|idx| Value::Int(idx as i64)) + .ok_or_else(|| anyhow!("the list doesn't contain the subsequence")) + } + + /// Returns the starting index of the first occurrence of `subsequence` in `list`, or `None` if there is none. + #[function( + name = "find_subsequence?", + signature = "(List, List) -> Option" + )] + pub fn maybe_find_subsequence(list: &[Value], subsequence: &[Value]) -> Value { + match subsequence_start(list, subsequence) { + None => Value::None, + Some(idx) => Value::Object(Rc::new(Object::Some(Value::Int(idx as i64)))), } } /// Inserts an element at the given index, shifting all elements after it to the right. + #[function(signature = "(List, Int, T)")] pub fn insert(list: &mut Vec, index: usize, elem: Value) -> anyhow::Result<()> { if index > list.len() { return Err(anyhow!("index {index} is out of bounds")); @@ -52,8 +61,9 @@ mod inner { } /// Removes and returns the element at position `index` within the list, shifting all elements after it to the left. + #[function(signature = "(List, Int) -> T")] pub fn remove(list: &mut Vec, index: usize) -> anyhow::Result { - if index > list.len() { + if index >= list.len() { return Err(anyhow!("index {index} is out of bounds")); } @@ -61,27 +71,31 @@ mod inner { } /// Removes all instances of `element` from `list` + #[function(signature = "(List, T)")] pub fn remove_element(list: &mut Vec, element: Value) { list.retain(|cur| cur != &element); } /// Appends `elem` to the back of `list` + #[function(signature = "(List, T)")] pub fn push(list: &mut Vec, elem: Value) { list.push(elem); } /// Moves elements from `other` to `list`, leaving `other` empty. + #[function(signature = "(List, List)")] pub fn append(list: &mut Vec, other: &mut Vec) { list.append(other); } /// Copies elements from `other` to `list`, not touching `other`. + #[function(signature = "(List, List)")] pub fn extend(list: &mut Vec, other: &[Value]) { list.extend_from_slice(other); } /// Removes and returns the last element from the list, or `None` if empty. - #[function(name = "pop?", return_type = Option<_>)] + #[function(name = "pop?", signature = "(List) -> Option")] pub fn maybe_pop(list: &mut Vec) -> Value { match list.pop() { None => Value::None, @@ -89,13 +103,16 @@ mod inner { } } - /// Removes and returns the last element from the list, or unit if empty. - pub fn pop(list: &mut Vec) -> Value { - list.pop().unwrap_or_else(Value::unit) + /// Removes and returns the last element from the list. Use `pop?` to get + /// `None` from an empty list instead of an error. + #[function(signature = "(List) -> T")] + pub fn pop(list: &mut Vec) -> anyhow::Result { + list.pop() + .ok_or_else(|| anyhow::anyhow!("the list is empty")) } /// Removes and returns the first element from the list, or `None` if empty. - #[function(name = "pop_left?", return_type = Option<_>)] + #[function(name = "pop_left?", signature = "(List) -> Option")] pub fn maybe_pop_left(list: &mut Vec) -> Value { if list.is_empty() { return Value::None; @@ -103,12 +120,14 @@ mod inner { Value::Object(Rc::new(Object::Some(list.remove(0)))) } - /// Removes and returns the first element from the list, or unit if empty. - pub fn pop_left(list: &mut Vec) -> Value { + /// Removes and returns the first element from the list. Use `pop_left?` + /// to get `None` from an empty list instead of an error. + #[function(signature = "(List) -> T")] + pub fn pop_left(list: &mut Vec) -> anyhow::Result { if list.is_empty() { - return Value::unit(); + return Err(anyhow::anyhow!("the list is empty")); } - list.remove(0) + Ok(list.remove(0)) } /// Resolves a possibly-negative index against `len`, wrapping negative @@ -128,6 +147,7 @@ mod inner { /// Returns a copy of the element at `index`, or an error if the index is out /// of bounds. The index must be non-negative; use `index` for indexing from /// the end. + #[function(signature = "(List, Int) -> T")] pub fn get(list: &[Value], index: usize) -> anyhow::Result { list.get(index) .cloned() @@ -137,7 +157,7 @@ mod inner { /// Returns a copy of the element at `index`, or `None` if `index` is past /// the end of the list. A negative or oversized index is a conversion error, /// not `None`; use `index?` for indexing from the end. - #[function(name = "get?", return_type = Option<_>)] + #[function(name = "get?", signature = "(List, Int) -> Option")] pub fn maybe_get(list: &[Value], index: usize) -> Value { match list.get(index) { None => Value::None, @@ -148,6 +168,7 @@ mod inner { /// Returns a copy of the element at `i`, wrapping negative indices from the /// end of the list; errors if the index is out of bounds. The named form of /// the `[]` operator. + #[function(signature = "(List, Int) -> T")] pub fn index(list: &[Value], i: &BigInt) -> anyhow::Result { wrap_index(i, list.len()) .map(|idx| list[idx].clone()) @@ -157,7 +178,7 @@ mod inner { /// Returns a copy of the element at `i`, wrapping negative indices from the /// end of the list; returns `None` if the index is out of bounds. The /// non-throwing twin of the `[]` operator. - #[function(name = "index?", return_type = Option<_>)] + #[function(name = "index?", signature = "(List, Int) -> Option")] pub fn maybe_index(list: &[Value], i: &BigInt) -> Value { match wrap_index(i, list.len()) { Some(idx) => Value::Object(Rc::new(Object::Some(list[idx].clone()))), @@ -166,22 +187,25 @@ mod inner { } /// Creates a copy of the list with its elements in reverse order - #[function(return_type = Vec<_>)] + #[function(signature = "(List) -> List")] pub fn reversed(list: &[Value]) -> Value { Value::list(list.iter().rev().cloned().collect()) } /// Removes all values from the list + #[function(signature = "(List)")] pub fn clear(list: &mut Vec) { list.clear(); } /// Returns `true` if the list contains no elements + #[function(signature = "(List) -> Bool")] pub fn is_empty(list: &[Value]) -> bool { list.is_empty() } /// Splits the collection into two at the given index. + #[function(signature = "(List, Int) -> List")] pub fn split_off(list: &mut Vec, index: usize) -> anyhow::Result { if index > list.len() { return Err(anyhow!("index {index} is out of bounds")); @@ -192,11 +216,13 @@ mod inner { /// Shortens the list, keeping the first `len` elements and dropping the rest. /// If `len` is greater or equal to the length of the list, nothing happens. + #[function(signature = "(List, Int)")] pub fn truncate(list: &mut Vec, len: usize) { list.truncate(len); } /// Returns a copy of the first element of the list or results in an error if the list is empty. + #[function(signature = "(List) -> T")] pub fn first(list: &[Value]) -> anyhow::Result { list.first() .cloned() @@ -204,7 +230,7 @@ mod inner { } /// Returns a copy of the first element, or `None` if the list is empty. - #[function(name = "first?", return_type = Option<_>)] + #[function(name = "first?", signature = "(List) -> Option")] pub fn maybe_first(list: &[Value]) -> Value { match list.first() { None => Value::None, @@ -213,6 +239,7 @@ mod inner { } /// Returns a copy of the last element of the list or results in an error if the list is empty. + #[function(signature = "(List) -> T")] pub fn last(list: &[Value]) -> anyhow::Result { list.last() .cloned() @@ -220,7 +247,7 @@ mod inner { } /// Returns a copy of the last element, or `None` if the list is empty. - #[function(name = "last?", return_type = Option<_>)] + #[function(name = "last?", signature = "(List) -> Option")] pub fn maybe_last(list: &[Value]) -> Value { match list.last() { None => Value::None, @@ -229,7 +256,7 @@ mod inner { } /// Returns the Cartesian product of two lists as a list of tuples. - #[function(return_type = Vec<_>)] + #[function(signature = "(List, List) -> List<(T, U)>")] pub fn cartesian_product(list_a: &[Value], list_b: &[Value]) -> Value { Value::list( list_a @@ -256,13 +283,8 @@ pub mod ops { let native = Rc::new(NativeFunction { name: "++".to_string(), documentation: Some("Concatenates two lists into a new list.".to_string()), - static_type: StaticType::Function { - parameters: Some(vec![ - StaticType::List(Box::new(StaticType::Any)), - StaticType::List(Box::new(StaticType::Any)), - ]), - return_type: Box::new(StaticType::List(Box::new(StaticType::Any))), - }, + static_type: StaticType::signature("(List, List) -> List") + .expect("valid signature"), func: NativeFunc::Simple(Box::new(|args| { let [left, right] = args else { return Err(VmError::native(format!( @@ -320,13 +342,8 @@ pub mod ops { documentation: Some( "Appends all elements from the right list to the left list in place.".to_string(), ), - static_type: StaticType::Function { - parameters: Some(vec![ - StaticType::List(Box::new(StaticType::Any)), - StaticType::List(Box::new(StaticType::Any)), - ]), - return_type: Box::new(StaticType::List(Box::new(StaticType::Any))), - }, + static_type: StaticType::signature("(List, List) -> List") + .expect("valid signature"), func: NativeFunc::Simple(Box::new(|args| { let [left, right] = args else { return Err(VmError::native(format!( @@ -378,3 +395,13 @@ pub mod ops { env.declare_global_fn(native); } } + +/// Where `subsequence` first starts in `list`, if it's in it. +fn subsequence_start(list: &[Value], subsequence: &[Value]) -> Option { + // An empty subsequence starts anywhere, and `windows` can't be empty. + if subsequence.is_empty() { + return Some(0); + } + list.windows(subsequence.len()) + .position(|window| window == subsequence) +} diff --git a/ndc_stdlib/src/math.rs b/ndc_stdlib/src/math.rs index fd92ce7c..82a0a4fe 100644 --- a/ndc_stdlib/src/math.rs +++ b/ndc_stdlib/src/math.rs @@ -83,6 +83,23 @@ fn declare( })); } +/// Declares a native whose type is a generic signature such as +/// `(A, B) -> Bool`, see [`StaticType::signature`]. +fn declare_signature( + env: &mut FunctionRegistry>, + name: &str, + signature: &str, + documentation: impl Into, + func: impl Fn(&[Value]) -> Result + 'static, +) { + env.declare_global_fn(Rc::new(NativeFunction { + name: name.to_string(), + documentation: Some(documentation.into()), + static_type: StaticType::signature(signature).expect("valid signature"), + func: NativeFunc::Simple(Box::new(func)), + })); +} + /// Declares a native whose operands and result have fixed types. /// /// A type name is written once and used twice: as the operand's declared @@ -527,32 +544,25 @@ fn register_comparisons(env: &mut FunctionRegistry>) { "Returns whether the left value is less than or equal to the right.", ), ] { - declare( - env, - name, - vec![StaticType::Any, StaticType::Any], - StaticType::Bool, - docs, - move |args| Ok(Value::Bool(predicate(compare_operands(args)?))), - ); + declare_signature(env, name, "(A, B) -> Bool", docs, move |args| { + Ok(Value::Bool(predicate(compare_operands(args)?))) + }); } - declare( + declare_signature( env, "==", - vec![StaticType::Any, StaticType::Any], - StaticType::Bool, + "(A, B) -> Bool", "Returns whether two values are equal.", |args| { arity(args, 2)?; Ok(Value::Bool(args[0] == args[1])) }, ); - declare( + declare_signature( env, "!=", - vec![StaticType::Any, StaticType::Any], - StaticType::Bool, + "(A, B) -> Bool", "Returns whether two values are not equal.", |args| { arity(args, 2)?; @@ -564,21 +574,14 @@ fn register_comparisons(env: &mut FunctionRegistry>) { ("<=>", false, "Performs a three-way comparison."), (">=<", true, "Performs a reverse three-way comparison."), ] { - declare( - env, - name, - vec![StaticType::Any, StaticType::Any], - StaticType::Int, - docs, - move |args| { - let result = match compare_operands(args)? { - Ordering::Less => -1, - Ordering::Equal => 0, - Ordering::Greater => 1, - }; - Ok(Value::Int(if reverse { -result } else { result })) - }, - ); + declare_signature(env, name, "(A, B) -> Int", docs, move |args| { + let result = match compare_operands(args)? { + Ordering::Less => -1, + Ordering::Equal => 0, + Ordering::Greater => 1, + }; + Ok(Value::Int(if reverse { -result } else { result })) + }); } } @@ -709,7 +712,7 @@ fn register_aggregates(env: &mut FunctionRegistry>) { declare( env, name, - vec![StaticType::Sequence(Box::new(mode.static_type()))], + vec![StaticType::Iterable(Box::new(mode.static_type()))], mode.static_type(), if product { "Returns the product of a numeric sequence." @@ -1078,22 +1081,20 @@ fn bigint_number(value: BigInt) -> Value { } fn register_conversions(env: &mut FunctionRegistry>) { - declare( + declare_signature( env, "int", - vec![StaticType::Any], - StaticType::Int, + "(A) -> Int", "Converts a value to a checked i64 Int.", |args| { arity(args, 1)?; convert_to_int(&args[0]).map(Value::Int) }, ); - declare( + declare_signature( env, "float", - vec![StaticType::Any], - StaticType::Float, + "(A) -> Float", "Converts a value to a Float.", |args| { arity(args, 1)?; diff --git a/ndc_stdlib/src/rand.rs b/ndc_stdlib/src/rand.rs index f6eea107..a8fd797d 100644 --- a/ndc_stdlib/src/rand.rs +++ b/ndc_stdlib/src/rand.rs @@ -25,6 +25,7 @@ mod inner { use ndc_vm::value::AdvancedNumber; /// Randomly shuffles the elements of the list in place. + #[function(signature = "(List)")] pub fn shuffle(list: &mut [Value]) { list.shuffle(&mut rand::rng()); } @@ -32,7 +33,7 @@ mod inner { /// Returns a copy of the input sequence converted to a list with the elements shuffled in random order. /// /// Note: this currently does consume iterators - #[function(return_type = Vec<_>)] + #[function(signature = "(Iterable) -> List")] pub fn shuffled(list: SeqValue) -> anyhow::Result { Ok(Value::list( list.try_into_iter() diff --git a/ndc_stdlib/src/regex.rs b/ndc_stdlib/src/regex.rs index d5d14059..6bc04caf 100644 --- a/ndc_stdlib/src/regex.rs +++ b/ndc_stdlib/src/regex.rs @@ -43,7 +43,7 @@ mod inner { } /// Returns all capture groups from the first match of the regular expression. - #[function(return_type = Vec)] + #[function(signature = "(String, String) -> List>")] pub fn captures(haystack: &str, regex: &str) -> Result { let r = Regex::new(regex)?; diff --git a/ndc_stdlib/src/sequence.rs b/ndc_stdlib/src/sequence.rs index 8a1c9af9..6d5c74aa 100644 --- a/ndc_stdlib/src/sequence.rs +++ b/ndc_stdlib/src/sequence.rs @@ -1,6 +1,7 @@ #![allow(clippy::ptr_arg)] use anyhow::anyhow; +use itertools::Itertools; use ndc_macros::export_module; use ndc_vm::VmCallable; use ndc_vm::value::{Object, SeqValue, Value}; @@ -60,11 +61,15 @@ mod inner { use std::rc::Rc; /// Returns `true` if the element is contained in the sequence. - #[function(name = "in")] + #[function(name = "in", signature = "(T, Iterable) -> Bool")] pub fn op_contains(elem: Value, seq: SeqValue) -> bool { - match &seq { + contains(&elem, &seq) + } + + fn contains(elem: &Value, seq: &Value) -> bool { + match seq { Value::Object(obj) => match obj.as_ref() { - Object::String(s) => match &elem { + Object::String(s) => match elem { Value::Object(e) => match e.as_ref() { Object::String(needle) => { if Rc::ptr_eq(s, needle) { @@ -76,17 +81,17 @@ mod inner { }, _ => false, }, - Object::List(l) => l.borrow().contains(&elem), - Object::Tuple(t) => t.contains(&elem), - Object::Map { entries, .. } => entries.borrow().contains_key(&elem), - Object::Deque(d) => d.borrow().contains(&elem), - Object::MinHeap(h) => h.borrow().iter().any(|v| v.0.0 == elem), - Object::MaxHeap(h) => h.borrow().iter().any(|v| v.0 == elem), + Object::List(l) => l.borrow().contains(elem), + Object::Tuple(t) => t.contains(elem), + Object::Map { entries, .. } => entries.borrow().contains_key(elem), + Object::Deque(d) => d.borrow().contains(elem), + Object::MinHeap(h) => h.borrow().iter().any(|v| v.0.0 == *elem), + Object::MaxHeap(h) => h.borrow().iter().any(|v| v.0 == *elem), Object::Iterator(i) => { { let iter = i.borrow(); if let Some((start, end, inclusive)) = iter.range_bounds() { - let Value::Int(n) = elem else { return false }; + let &Value::Int(n) = elem else { return false }; return if inclusive { n >= start && n <= end } else { @@ -94,14 +99,14 @@ mod inner { }; } if let Some(start) = iter.unbounded_range_start() { - let Value::Int(n) = elem else { return false }; + let &Value::Int(n) = elem else { return false }; return n >= start; } } // Finite non-range iterator — linear scan loop { match i.borrow_mut().next() { - Some(v) if v == elem => return true, + Some(v) if v == *elem => return true, Some(_) => {} None => return false, } @@ -113,8 +118,31 @@ mod inner { } } + /// Returns `true` if the map contains the given key. + #[function(name = "in", signature = "(K, Map) -> Bool")] + pub fn op_contains_key(key: Value, map: SeqValue) -> bool { + contains(&key, &map) + } + + /// Returns the highest key in the map. + #[function(name = "max", signature = "(Map) -> K")] + pub fn max_key(map: SeqValue) -> anyhow::Result { + highest(map) + } + + /// Returns the lowest key in the map. + #[function(name = "min", signature = "(Map) -> K")] + pub fn min_key(map: SeqValue) -> anyhow::Result { + lowest(map) + } + /// Returns the highest element in the sequence. + #[function(signature = "(Iterable) -> T")] pub fn max(seq: SeqValue) -> anyhow::Result { + highest(seq) + } + + fn highest(seq: Value) -> anyhow::Result { match seq { Value::Object(obj) => match Rc::unwrap_or_clone(obj) { Object::String(s) => { @@ -143,11 +171,13 @@ mod inner { } /// Returns the element for which the key function returns the highest value. + #[function(signature = "(Iterable, Fn(T) -> U) -> T")] pub fn max_by_key(seq: SeqValue, func: &mut VmCallable<'_>) -> anyhow::Result { by_key(seq, func, Ordering::Greater) } /// Returns the element for which the key function returns the lowest value. + #[function(signature = "(Iterable, Fn(T) -> U) -> T")] pub fn min_by_key(seq: SeqValue, func: &mut VmCallable<'_>) -> anyhow::Result { by_key(seq, func, Ordering::Less) } @@ -157,6 +187,7 @@ mod inner { /// The comparator function takes two elements and returns a number. A positive result means the /// first argument is greater than the second, a negative result means the first argument is /// less than the second, and zero means they are equal. + #[function(signature = "(Iterable, Fn(T, T) -> U) -> T")] pub fn max_by(seq: SeqValue, comp: &mut VmCallable<'_>) -> anyhow::Result { by_comp(seq, comp, Ordering::Greater) } @@ -166,12 +197,18 @@ mod inner { /// The comparator function takes two elements and returns a number. A positive result means the /// first argument is greater than the second, a negative result means the first argument is /// less than the second, and zero means they are equal. + #[function(signature = "(Iterable, Fn(T, T) -> U) -> T")] pub fn min_by(seq: SeqValue, comp: &mut VmCallable<'_>) -> anyhow::Result { by_comp(seq, comp, Ordering::Less) } /// Returns the lowest element in the sequence. + #[function(signature = "(Iterable) -> T")] pub fn min(seq: SeqValue) -> anyhow::Result { + lowest(seq) + } + + fn lowest(seq: Value) -> anyhow::Result { match seq { Value::Object(obj) => match Rc::unwrap_or_clone(obj) { Object::String(s) => { @@ -200,7 +237,7 @@ mod inner { } /// Sorts the list in place. - #[function(return_type = ())] + #[function(signature = "(List)")] pub fn sort(list: &mut Vec) -> anyhow::Result<()> { let mut err: Option = None; list.sort_by(|a, b| { @@ -229,7 +266,7 @@ mod inner { /// - for values equal to `0` the first argument is equal to the second argument /// /// This function only works for strings and lists and will throw errors otherwise. - #[function(return_type = ())] + #[function(signature = "(List, Fn(T, T) -> U)")] pub fn sort_by(list: &mut Vec, comp: &mut VmCallable<'_>) -> anyhow::Result<()> { let mut err: Option = None; list.sort_by(|left, right| { @@ -257,7 +294,7 @@ mod inner { } /// Returns a sorted copy of the input sequence as a list. - #[function(return_type = Vec<_>)] + #[function(signature = "(Iterable) -> List")] pub fn sorted(seq: SeqValue) -> anyhow::Result { let mut list: Vec = seq .try_into_iter() @@ -275,7 +312,7 @@ mod inner { /// - for values lower than `0` the first argument is smaller than the second argument /// - for values higher than `0` the first argument is greater than the second argument /// - for values equal to `0` the first argument is equal to the second argument - #[function(return_type = Vec<_>)] + #[function(signature = "(Iterable, Fn(T, T) -> U) -> List")] pub fn sorted_by(seq: SeqValue, comp: &mut VmCallable<'_>) -> anyhow::Result { let mut list: Vec = seq .try_into_iter() @@ -312,6 +349,7 @@ mod inner { } /// Returns the length of the sequence, for strings this returns the number of UTF-8 characters. + #[function(signature = "(Iterable) -> Int")] pub fn len(seq: SeqValue) -> anyhow::Result { match &seq { Value::Object(obj) => match obj.as_ref() { @@ -333,7 +371,7 @@ mod inner { } /// Returns a lazy iterator yielding `(index, value)` tuples for each element of `seq`. - #[function(return_type = Iterator)] + #[function(signature = "(Iterable) -> Iterator<(Int, T)>")] pub fn enumerate(seq: SeqValue) -> anyhow::Result { let iter = EnumerateIter::new(seq).ok_or_else(|| anyhow!("enumerate requires a sequence"))?; @@ -341,6 +379,7 @@ mod inner { } /// Reduces/folds the given sequence using the given combining function and a custom initial value. + #[function(signature = "(Iterable, A, Fn(A, T) -> A) -> A")] pub fn fold( seq: SeqValue, initial: Value, @@ -355,6 +394,7 @@ mod inner { } /// Reduces/folds the given sequence using the given combining function. + #[function(signature = "(Iterable, Fn(T, T) -> T) -> T")] pub fn reduce(seq: SeqValue, function: &mut VmCallable<'_>) -> anyhow::Result { let mut iterator = seq .try_into_iter() @@ -366,7 +406,7 @@ mod inner { } /// Filters the given sequence using the `predicate`. - #[function(return_type = Vec<_>)] + #[function(signature = "(Iterable, Fn(T) -> Bool) -> List")] pub fn filter(seq: SeqValue, predicate: &mut VmCallable<'_>) -> anyhow::Result { let mut out = Vec::new(); for element in seq @@ -386,6 +426,7 @@ mod inner { } /// Returns the number of elements in the input sequence for which the given `predicate` returns `true`. + #[function(signature = "(Iterable, Fn(T) -> Bool) -> Int")] pub fn count(seq: SeqValue, predicate: &mut VmCallable<'_>) -> anyhow::Result { let mut out = 0i64; for element in seq @@ -402,6 +443,7 @@ mod inner { } /// Returns the value of the first element for which the `predicate` is true for the given input sequence. + #[function(signature = "(Iterable, Fn(T) -> Bool) -> T")] pub fn find(seq: SeqValue, predicate: &mut VmCallable<'_>) -> anyhow::Result { for element in seq .try_into_iter() @@ -420,6 +462,7 @@ mod inner { } /// Returns the first index of the element for which the `predicate` is true in the input sequence. + #[function(signature = "(Iterable, Fn(T) -> Bool) -> Int")] pub fn locate(seq: SeqValue, predicate: &mut VmCallable<'_>) -> anyhow::Result { for (idx, element) in seq .try_into_iter() @@ -436,7 +479,7 @@ mod inner { } /// Returns the first index of the element or produces an error - #[function(name = "locate")] + #[function(name = "locate", signature = "(Iterable, T) -> Int")] pub fn locate_element(seq: SeqValue, element: Value) -> anyhow::Result { for (idx, el) in seq .try_into_iter() @@ -451,6 +494,7 @@ mod inner { } /// Returns `true` if the `predicate` is true for none of the elements in `seq`. + #[function(signature = "(Iterable, Fn(T) -> Bool) -> Bool")] pub fn none(seq: SeqValue, function: &mut VmCallable<'_>) -> anyhow::Result { for item in seq .try_into_iter() @@ -471,7 +515,7 @@ mod inner { } /// Returns `true` if the `predicate` is true for all the elements in `seq`. - #[function(return_type = bool)] + #[function(signature = "(Iterable, Fn(T) -> Bool) -> Bool")] pub fn all(seq: SeqValue, function: &mut VmCallable<'_>) -> anyhow::Result { for item in seq .try_into_iter() @@ -492,6 +536,7 @@ mod inner { } /// Returns `true` if the `predicate` is true for any of the elements in `seq`. + #[function(signature = "(Iterable, Fn(T) -> Bool) -> Bool")] pub fn any(seq: SeqValue, predicate: &mut VmCallable<'_>) -> anyhow::Result { for item in seq .try_into_iter() @@ -512,7 +557,7 @@ mod inner { } /// Applies the function to each element in a sequence returning the result as a list. - #[function(return_type = Vec<_>)] + #[function(signature = "(Iterable, Fn(T) -> U) -> List")] pub fn map(seq: SeqValue, function: &mut VmCallable<'_>) -> anyhow::Result { let mut out = Vec::new(); for item in seq @@ -525,7 +570,7 @@ mod inner { } /// Applies a function to each item in a sequence, flattens the resulting sequences, and returns a single combined sequence. - #[function(return_type = Vec<_>)] + #[function(signature = "(Iterable, Fn(T) -> Iterable) -> List")] pub fn flat_map(seq: SeqValue, function: &mut VmCallable<'_>) -> anyhow::Result { let mut out = Vec::new(); for item in seq @@ -542,6 +587,7 @@ mod inner { } /// Returns the first element of the sequence or the `default` value otherwise. + #[function(signature = "(Iterable, T) -> T")] pub fn first_or(seq: SeqValue, default: Value) -> Value { seq.try_into_iter() .and_then(|mut i| i.next()) @@ -549,6 +595,7 @@ mod inner { } /// Returns the first element of the sequence or the return value of the given function. + #[function(signature = "(Iterable, Fn() -> T) -> T")] pub fn first_or_else(seq: SeqValue, default: &mut VmCallable<'_>) -> anyhow::Result { if let Some(item) = seq.try_into_iter().and_then(|mut i| i.next()) { return Ok(item); @@ -556,8 +603,8 @@ mod inner { default.call(&[]).map_err(|e| anyhow!(e)) } - /// Returns the `k` sized combinations of the given sequence `seq` as a lazy iterator of tuples. - #[function(return_type = Iterator)] + /// Returns the `k` sized combinations of the given sequence `seq` as a lazy iterator of lists. + #[function(signature = "(Iterable, Int) -> Iterator>")] pub fn combinations(seq: SeqValue, k: usize) -> anyhow::Result { let iter = CombinationsIter::new(seq, k) .ok_or_else(|| anyhow!("combinations requires a sequence"))?; @@ -565,20 +612,20 @@ mod inner { } /// Returns a lazy iterator yielding the first `n` elements of `seq`. - #[function(return_type = Iterator)] + #[function(signature = "(Iterable, Int) -> Iterator")] pub fn take(seq: SeqValue, n: usize) -> anyhow::Result { let iter = TakeIter::new(seq, n).ok_or_else(|| anyhow!("take requires a sequence"))?; Ok(Value::iterator(iter.into_shared())) } - /// Returns the `k` sized permutations of the given sequence `seq` as a list of tuples. - #[function(return_type = Vec<_>)] + /// Returns the `k` sized permutations of the given sequence `seq` as a list of lists. + #[function(signature = "(Iterable, Int) -> List>")] pub fn permutations(seq: SeqValue, k: usize) -> anyhow::Result { Ok(Value::list( seq.try_into_iter() .ok_or_else(|| anyhow!("permutations requires a sequence"))? .permutations(k) - .map(Value::tuple) + .map(Value::list) .collect::>(), )) } @@ -601,7 +648,7 @@ mod inner { } /// Returns all prefixes of a sequence, each as a list; for strings, returns string prefixes. - #[function(return_type = Vec<_>)] + #[function(signature = "(Iterable) -> List>")] pub fn prefixes(seq: SeqValue) -> anyhow::Result { // A sequence-typed caller can still supply a string. if let Value::Object(ref obj) = seq @@ -635,7 +682,7 @@ mod inner { } /// Returns all suffixes of a sequence, each as a list; for strings, returns all trailing substrings. - #[function(return_type = Vec<_>)] + #[function(signature = "(Iterable) -> List>")] pub fn suffixes(seq: SeqValue) -> anyhow::Result { // A sequence-typed caller can still supply a string. if let Value::Object(ref obj) = seq @@ -655,30 +702,28 @@ mod inner { } /// Transposes a sequence of sequences, turning rows into columns, and returns the result as a list of lists. - #[function(return_type = Vec<_>)] + #[function(signature = "(Iterable>) -> List>")] pub fn transposed(seq: SeqValue) -> anyhow::Result { - let main: Vec = seq - .try_into_iter() - .ok_or_else(|| anyhow!("transposed requires a sequence"))? - .collect(); - let mut iterators: Vec>> = Vec::new(); - for elem in main { - iterators.push(Box::new(elem.try_into_iter().ok_or_else(|| { - anyhow!("elements of transposed sequence must be iterable") - })?)); - } - let mut out = Vec::new(); - loop { - let row: Vec = iterators.iter_mut().filter_map(|i| i.next()).collect(); - if row.is_empty() { - return Ok(Value::list(out)); - } - out.push(Value::list(row)); - } + transpose(seq) + } + + /// Transposes a sequence of strings, turning rows of characters into columns. + #[function( + name = "transposed", + signature = "(Iterable) -> List>" + )] + pub fn transposed_strings(seq: SeqValue) -> anyhow::Result { + transpose(seq) + } + + /// Transposes a sequence of sequences whose elements have different types. + #[function(name = "transposed", signature = "(Iterable) -> List>")] + pub fn transposed_any(seq: SeqValue) -> anyhow::Result { + transpose(seq) } /// Return a list of all windows, wrapping back to the first elements when the window would otherwise exceed the length of source list, producing tuples of size 2. - #[function(return_type = Vec<_>)] + #[function(signature = "(Iterable) -> List<(T, T)>")] pub fn circular_tuple_windows(seq: SeqValue) -> anyhow::Result { Ok(Value::list( seq.try_into_iter() @@ -692,7 +737,7 @@ mod inner { } /// Returns a list of all size-2 windows in `seq`. - #[function(return_type = Vec<_>)] + #[function(signature = "(Iterable) -> List>")] pub fn pairwise(seq: SeqValue) -> anyhow::Result { Ok(Value::list( seq.try_into_iter() @@ -705,7 +750,10 @@ mod inner { } /// Applies a function to each pair of consecutive elements in a sequence and returns the results as a list. - #[function(name = "pairwise", return_type = Vec<_>)] + #[function( + name = "pairwise", + signature = "(Iterable, Fn(T, T) -> U) -> List" + )] pub fn pairwise_map(seq: SeqValue, function: &mut VmCallable<'_>) -> anyhow::Result { let main: Vec = seq .try_into_iter() @@ -719,7 +767,7 @@ mod inner { } /// Returns a list of all contiguous windows of `length` size. The windows overlap. If the `seq` is shorter than size, the iterator returns no values. - #[function(return_type = Vec<_>)] + #[function(signature = "(Iterable, Int) -> List>")] pub fn windows(seq: SeqValue, length: usize) -> anyhow::Result { if length == 0 { return Err(anyhow!("window size must be non-zero")); @@ -738,7 +786,7 @@ mod inner { /// /// The powerset of a set contains all subsets including the empty set and the full input set. A powerset has length `2^n` where `n` is the length of the input set. /// Each list produced by this function represents a subset of the elements in the source sequence. - #[function(return_type = Vec<_>)] + #[function(signature = "(Iterable) -> List>")] pub fn subsequences(seq: SeqValue) -> anyhow::Result { Ok(Value::list( seq.try_into_iter() @@ -750,7 +798,10 @@ mod inner { } /// Return a list that represents the powerset of the elements of `seq` that are exactly `length` long. - #[function(name = "subsequences", return_type = Vec<_>)] + #[function( + name = "subsequences", + signature = "(Iterable, Int) -> List>" + )] pub fn subsequences_len(seq: SeqValue, length: usize) -> anyhow::Result { Ok(Value::list( seq.try_into_iter() @@ -788,31 +839,23 @@ mod inner { /// [3,"c",false] /// ] /// ``` - #[function(return_type = Vec<_>)] + #[function(signature = "(Iterable>) -> List>")] pub fn multi_cartesian_product(seq: SeqValue) -> anyhow::Result { - let mut iterators = Vec::new(); - for elem in seq - .try_into_iter() - .ok_or_else(|| anyhow!("multi_cartesian_product requires a sequence"))? - { - let inner: Vec = elem - .try_into_iter() - .ok_or_else(|| anyhow!("elements of sequence must be iterable"))? - .collect_vec(); - iterators.push(inner.into_iter()); - } - Ok(Value::list( - iterators - .into_iter() - .multi_cartesian_product() - .map(Value::list) - .collect_vec(), - )) + cartesian_product(seq) + } + + /// The cartesian product of sequences whose elements have different types. + #[function( + name = "multi_cartesian_product", + signature = "(Iterable) -> List>" + )] + pub fn multi_cartesian_product_any(seq: SeqValue) -> anyhow::Result { + cartesian_product(seq) } /// Split the input sequence into evenly sized chunks. If the input length of the sequence /// is not dividable by the chunk_size the last chunk will contain fewer elements. - #[function(return_type = Vec<_>)] + #[function(signature = "(Iterable, Int) -> List>")] pub fn chunks(seq: SeqValue, chunk_size: usize) -> anyhow::Result { if chunk_size == 0 { return Err(anyhow!("chunk size must be non-zero")); @@ -828,7 +871,7 @@ mod inner { } /// Returns an infinite iterator that repeats the given value. - #[function(return_type = Iterator)] + #[function(signature = "(T) -> Iterator")] pub fn repeat(value: Value) -> Value { Value::iterator(Rc::new(std::cell::RefCell::new(ndc_vm::RepeatIter::new( value, @@ -836,7 +879,7 @@ mod inner { } /// Returns an iterator that repeats the given value `times` times. - #[function(name = "repeat", return_type = Iterator)] + #[function(name = "repeat", signature = "(T, Int) -> Iterator")] pub fn repeat_times(value: Value, times: usize) -> Value { Value::iterator(Rc::new(std::cell::RefCell::new( ndc_vm::RepeatIter::new_limited(value, times), @@ -844,6 +887,48 @@ mod inner { } } +fn transpose(seq: SeqValue) -> anyhow::Result { + let main: Vec = seq + .try_into_iter() + .ok_or_else(|| anyhow!("transposed requires a sequence"))? + .collect(); + let mut iterators: Vec>> = Vec::new(); + for elem in main { + iterators.push(Box::new(elem.try_into_iter().ok_or_else(|| { + anyhow!("elements of transposed sequence must be iterable") + })?)); + } + let mut out = Vec::new(); + loop { + let row: Vec = iterators.iter_mut().filter_map(|i| i.next()).collect(); + if row.is_empty() { + return Ok(Value::list(out)); + } + out.push(Value::list(row)); + } +} + +fn cartesian_product(seq: SeqValue) -> anyhow::Result { + let mut iterators = Vec::new(); + for elem in seq + .try_into_iter() + .ok_or_else(|| anyhow!("multi_cartesian_product requires a sequence"))? + { + let inner: Vec = elem + .try_into_iter() + .ok_or_else(|| anyhow!("elements of sequence must be iterable"))? + .collect_vec(); + iterators.push(inner.into_iter()); + } + Ok(Value::list( + iterators + .into_iter() + .multi_cartesian_product() + .map(Value::list) + .collect_vec(), + )) +} + fn by_key(seq: Value, func: &mut VmCallable<'_>, better: Ordering) -> anyhow::Result { let mut best_value: Option = None; let mut best_key: Option = None; @@ -912,45 +997,110 @@ pub mod extra { use std::rc::Rc; pub fn register(env: &mut FunctionRegistry>) { + // A tuple's elements have their own types, and a signature can't say + // "a tuple of any length", so each length up to twelve, as far as + // Rust's standard library goes for tuples, gets one that says which. + const LONGEST: usize = 12; + let names = |count: usize| { + ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"][..count].to_vec() + }; + for length in 1..=LONGEST { + let names = names(length); + let parameters = names.join(", "); + let tuple = if length == 1 { + "(A,)".to_string() + } else { + format!("({parameters})") + }; + for (name, returns, documentation) in [ + ("first", names[0], "Returns the first element of a tuple."), + ( + "last", + names[length - 1], + "Returns the last element of a tuple.", + ), + ] { + env.declare_global_fn(Rc::new(NativeFunction { + name: name.to_string(), + documentation: Some(documentation.to_string()), + static_type: StaticType::signature(&format!( + "<{parameters}>({tuple}) -> {returns}" + )) + .expect("valid signature"), + func: NativeFunc::Simple(Box::new(move |args| { + tuple_end(args, name == "first") + })), + })); + } + } + // Two or more sequences zip into tuples of their elements. + for length in 2..=LONGEST { + let names = names(length); + let sequences = names + .iter() + .map(|name| format!("Iterable<{name}>")) + .collect::>() + .join(", "); + env.declare_global_fn(Rc::new(NativeFunction { + name: "zip".to_string(), + documentation: Some(ZIP_DOCUMENTATION.to_string()), + static_type: StaticType::signature(&format!( + "<{}>({sequences}) -> List<({})>", + names.join(", "), + names.join(", ") + )) + .expect("valid signature"), + func: NativeFunc::Simple(Box::new(zip)), + })); + } + // More make tuples as long as the number of arguments. env.declare_global_fn(Rc::new(NativeFunction { name: "zip".to_string(), - documentation: Some("Zips two or more sequences together into a list of tuples. The result length is the minimum length of the inputs.".to_string()), - static_type: StaticType::Function { - parameters: None, - return_type: Box::new(StaticType::List(Box::new(StaticType::Tuple(vec![ - StaticType::Any, - StaticType::Any, - ])))), - }, - func: NativeFunc::Simple(Box::new(|args| { - if args.len() < 2 { - return Err(VmError::native( - "zip must be called with 2 or more arguments".to_string(), - )); - } - let iters: Option>> = args - .iter() - .map(|arg| match arg { - Value::Object(obj) => match obj.as_ref() { - Object::List(l) => Some(l.borrow().clone()), - Object::Tuple(t) => Some(t.clone()), - _ => None, - }, - _ => None, - }) - .collect(); - let iters = - iters.ok_or_else(|| VmError::native("zip requires sequences".to_string()))?; - let min_len = iters.iter().map(|v| v.len()).min().unwrap_or(0); - let result: Vec = (0..min_len) - .map(|i| { - Value::Object(Rc::new(Object::Tuple( - iters.iter().map(|v| v[i].clone()).collect(), - ))) - }) - .collect(); - Ok(Value::Object(Rc::new(Object::list(result)))) - })), + documentation: Some(ZIP_DOCUMENTATION.to_string()), + static_type: StaticType::signature("(*) -> List").expect("valid signature"), + func: NativeFunc::Simple(Box::new(zip)), })); } + + const ZIP_DOCUMENTATION: &str = "Zips two or more sequences together into a list of tuples. The result length is the minimum length of the inputs."; + + /// The first or last element of the tuple in `args`. + fn tuple_end(args: &[Value], first: bool) -> Result { + let [Value::Object(object)] = args else { + return Err(VmError::native("expected one tuple".to_string())); + }; + let Object::Tuple(values) = object.as_ref() else { + return Err(VmError::native("expected a tuple".to_string())); + }; + let end = if first { values.first() } else { values.last() }; + end.cloned() + .ok_or_else(|| VmError::native("the tuple is empty".to_string())) + } + + fn zip(args: &[Value]) -> Result { + if args.len() < 2 { + return Err(VmError::native( + "zip must be called with 2 or more arguments".to_string(), + )); + } + // The iterators are stepped together, so an endless one like `1..` + // stops with the others. + let mut iters = args + .iter() + .map(|arg| arg.clone().try_into_iter()) + .collect::>>() + .ok_or_else(|| VmError::native("zip requires sequences".to_string()))?; + let mut result = Vec::new(); + 'zipping: loop { + let mut tuple = Vec::with_capacity(iters.len()); + for iter in &mut iters { + let Some(value) = iter.next() else { + break 'zipping; + }; + tuple.push(value); + } + result.push(Value::Object(Rc::new(Object::Tuple(tuple)))); + } + Ok(Value::Object(Rc::new(Object::list(result)))) + } } diff --git a/ndc_stdlib/src/serde.rs b/ndc_stdlib/src/serde.rs index 0c540a85..3f989f4e 100644 --- a/ndc_stdlib/src/serde.rs +++ b/ndc_stdlib/src/serde.rs @@ -227,6 +227,7 @@ mod inner { /// heaps, structs, functions, maps with non-string keys or a default value, /// and values that contain themselves. Use `json_encode_lossy` to convert /// those anyway. + #[function(signature = "(A) -> String")] pub fn json_encode(input: Value) -> anyhow::Result { let v = value_to_json(&input, false, &mut HashSet::new())?; Ok(v.to_string()) @@ -242,6 +243,7 @@ mod inner { /// `None` and non-finite floats become `null`. There is no lossy /// counterpart for `json_decode` because these conversions cannot be /// reversed. Functions and values that contain themselves are still errors. + #[function(signature = "(A) -> String")] pub fn json_encode_lossy(input: Value) -> anyhow::Result { let v = value_to_json(&input, true, &mut HashSet::new())?; Ok(v.to_string()) diff --git a/ndc_stdlib/src/string.rs b/ndc_stdlib/src/string.rs index a781c45e..58862f62 100644 --- a/ndc_stdlib/src/string.rs +++ b/ndc_stdlib/src/string.rs @@ -1,5 +1,5 @@ use ndc_macros::export_module; -use ndc_vm::value::{SeqValue, Value, VmString}; +use ndc_vm::value::{Object, SeqValue, Value, VmString}; use std::rc::Rc; @@ -25,11 +25,24 @@ pub fn join_to_string(list: SeqValue, sep: &str) -> anyhow::Result { } } +/// `Some(value)`, or `None` when there's no value. +fn option(value: Option) -> Value { + value.map_or(Value::None, |value| { + Value::Object(Rc::new(Object::Some(value))) + }) +} + +/// `string` without `prefix` and `suffix`, when it starts and ends with them +/// without the two overlapping. +fn circumfix<'a>(string: &'a str, prefix: &str, suffix: &str) -> Option<&'a str> { + string.strip_prefix(prefix)?.strip_suffix(suffix) +} + #[export_module] mod inner { /// Concatenates two values into a string. - #[function(name = "<>")] + #[function(name = "<>", signature = "(A, B) -> String")] pub fn op_string_concat(left: Value, right: Value) -> String { format!("{left}{right}") } @@ -47,6 +60,7 @@ mod inner { } /// Returns the provided value as a string + #[function(signature = "(A) -> String")] pub fn string(value: Value) -> String { format!("{value}") } @@ -111,6 +125,7 @@ mod inner { } /// Joins elements of the sequence into a single string using `sep` as the separator. + #[function(signature = "(Iterable, String) -> String")] pub fn join(list: SeqValue, sep: &str) -> anyhow::Result { join_to_string(list, sep) } @@ -122,6 +137,7 @@ mod inner { } /// Joins paragraphs into a single string, inserting blank lines between them. + #[function(signature = "(Iterable) -> String")] pub fn unparagraphs(list: SeqValue) -> anyhow::Result { join_to_string(list, "\n\n") } @@ -133,6 +149,7 @@ mod inner { } /// Joins lines into a single string, inserting newline characters between them. + #[function(signature = "(Iterable) -> String")] pub fn unlines(list: SeqValue) -> anyhow::Result { join_to_string(list, "\n") } @@ -144,6 +161,7 @@ mod inner { } /// Joins words into a single string, separating them with spaces. + #[function(signature = "(Iterable) -> String")] pub fn unwords(list: SeqValue) -> anyhow::Result { join_to_string(list, " ") } @@ -170,39 +188,91 @@ mod inner { Value::list(string.split(pattern).map(Value::string).collect()) } - /// Splits the string at the first occurrence of `pattern`, returning a tuple-like value. - #[function(return_type = (String, String))] - pub fn split_once(string: &str, pattern: &str) -> Value { - match string.split_once(pattern) { - Some((fst, snd)) => Value::tuple(vec![Value::string(fst), Value::string(snd)]), - None => Value::unit(), - } - } - - /// Removes one matching prefix, returning `()` if it does not match. - #[function(return_type = String)] - pub fn strip_prefix(string: &str, prefix: &str) -> Value { + /// Splits the string at the first occurrence of `pattern` into the parts + /// before and after it. It's an error if `pattern` doesn't occur; use + /// `split_once?` to get `None` instead. + #[function(signature = "(String, String) -> (String, String)")] + pub fn split_once(string: &str, pattern: &str) -> anyhow::Result { + let (before, after) = string + .split_once(pattern) + .ok_or_else(|| anyhow!("{string:?} doesn't contain {pattern:?}"))?; + Ok(Value::tuple(vec![ + Value::string(before), + Value::string(after), + ])) + } + + /// Splits the string at the first occurrence of `pattern` into the parts + /// before and after it, or returns `None` if `pattern` doesn't occur. + #[function( + name = "split_once?", + signature = "(String, String) -> Option<(String, String)>" + )] + pub fn maybe_split_once(string: &str, pattern: &str) -> Value { + option( + string.split_once(pattern).map(|(before, after)| { + Value::tuple(vec![Value::string(before), Value::string(after)]) + }), + ) + } + + /// Removes one matching prefix. It's an error if the string doesn't start + /// with `prefix`; use `strip_prefix?` to get `None` instead. + pub fn strip_prefix(string: &str, prefix: &str) -> anyhow::Result { string .strip_prefix(prefix) - .map_or_else(Value::unit, Value::string) + .map(str::to_string) + .ok_or_else(|| anyhow!("{string:?} doesn't start with {prefix:?}")) } - /// Removes one matching suffix, returning `()` if it does not match. - #[function(return_type = String)] - pub fn strip_suffix(string: &str, suffix: &str) -> Value { - string - .strip_suffix(suffix) - .map_or_else(Value::unit, Value::string) + /// Removes one matching prefix, or returns `None` if the string doesn't + /// start with `prefix`. + #[function( + name = "strip_prefix?", + signature = "(String, String) -> Option" + )] + pub fn maybe_strip_prefix(string: &str, prefix: &str) -> Value { + option(string.strip_prefix(prefix).map(Value::string)) } - /// Removes one matching prefix and suffix, returning `()` if either fails to match. - /// The prefix and suffix must not overlap. Empty patterns match without removing text. - #[function(return_type = String)] - pub fn strip_circumfix(string: &str, prefix: &str, suffix: &str) -> Value { + /// Removes one matching suffix. It's an error if the string doesn't end + /// with `suffix`; use `strip_suffix?` to get `None` instead. + pub fn strip_suffix(string: &str, suffix: &str) -> anyhow::Result { string - .strip_prefix(prefix) - .and_then(|rest| rest.strip_suffix(suffix)) - .map_or_else(Value::unit, Value::string) + .strip_suffix(suffix) + .map(str::to_string) + .ok_or_else(|| anyhow!("{string:?} doesn't end with {suffix:?}")) + } + + /// Removes one matching suffix, or returns `None` if the string doesn't + /// end with `suffix`. + #[function( + name = "strip_suffix?", + signature = "(String, String) -> Option" + )] + pub fn maybe_strip_suffix(string: &str, suffix: &str) -> Value { + option(string.strip_suffix(suffix).map(Value::string)) + } + + /// Removes one matching prefix and suffix, which must not overlap. Empty + /// patterns match without removing text. It's an error if they don't both + /// match; use `strip_circumfix?` to get `None` instead. + pub fn strip_circumfix(string: &str, prefix: &str, suffix: &str) -> anyhow::Result { + circumfix(string, prefix, suffix) + .map(str::to_string) + .ok_or_else(|| { + anyhow!("{string:?} doesn't start with {prefix:?} and end with {suffix:?} without overlap") + }) + } + + /// Removes one matching prefix and suffix, which must not overlap, or + /// returns `None` if they don't both match. + #[function( + name = "strip_circumfix?", + signature = "(String, String, String) -> Option" + )] + pub fn maybe_strip_circumfix(string: &str, prefix: &str, suffix: &str) -> Value { + option(circumfix(string, prefix, suffix).map(Value::string)) } /// Removes leading and trailing whitespace from the string. diff --git a/ndc_stdlib/src/value.rs b/ndc_stdlib/src/value.rs index adbd8509..b54b930a 100644 --- a/ndc_stdlib/src/value.rs +++ b/ndc_stdlib/src/value.rs @@ -6,6 +6,7 @@ mod inner { use std::rc::Rc; /// Returns the documentation string for a function, or an empty string if none is available. + #[function(signature = "(Fn(*) -> R) -> String")] pub fn docs(value: Value) -> anyhow::Result { match &value { Value::Object(obj) => match obj.as_ref() { @@ -25,6 +26,7 @@ mod inner { /// Returns the reference count for the value, if the value is not reference counted it will return 0. /// /// Note: this function does increase the ref count by 1 + #[function(signature = "(A) -> Int")] pub fn ref_count(value: Value) -> usize { match value { Value::Object(rc) => Rc::strong_count(&rc), @@ -33,18 +35,19 @@ mod inner { } /// Creates a new instance of `Some` - #[function(name = "Some", return_type = Option)] + #[function(name = "Some", signature = "(T) -> Option")] pub fn some(value: Value) -> Value { Value::Object(Rc::new(Object::Some(value))) } /// Creates a new instance of `None` - #[function(return_type = Option<_>)] + #[function(signature = "() -> Option")] pub fn none() -> Value { Value::None } /// Returns true if the argument is `Some`. + #[function(signature = "(A) -> Bool")] pub fn is_some(value: Value) -> bool { matches!( value, @@ -54,11 +57,13 @@ mod inner { } /// Returns true if the argument is `None`. + #[function(signature = "(A) -> Bool")] pub fn is_none(value: Value) -> bool { matches!(value, Value::None) } /// Extracts the value from an Option or errors if it's either None or a non-Option type + #[function(signature = "(Option) -> T")] pub fn unwrap(value: Value) -> anyhow::Result { match value { Value::Object(obj) => match obj.as_ref() { @@ -71,6 +76,7 @@ mod inner { } /// Extracts the value from an Option, returning `default` when it is `None`. + #[function(signature = "(Option, T) -> T")] pub fn unwrap_or(value: Value, default: Value) -> anyhow::Result { match value { Value::Object(obj) => match obj.as_ref() { @@ -83,11 +89,13 @@ mod inner { } /// Returns a shallow copy of the given value. + #[function(signature = "(T) -> T")] pub fn clone(value: Value) -> Value { value.shallow_clone() } /// Returns a deep copy of the given value, duplicating all nested structures. + #[function(signature = "(T) -> T")] pub fn deepcopy(value: Value) -> Value { value.deep_copy() } diff --git a/ndc_vm/src/chunk.rs b/ndc_vm/src/chunk.rs index 6a2731f0..c93e80e8 100644 --- a/ndc_vm/src/chunk.rs +++ b/ndc_vm/src/chunk.rs @@ -64,6 +64,7 @@ impl std::fmt::Debug for JumpTarget { /// | `JumpIfFalse` | `[… bool → … bool]` | 0 (peeks, jumps if false) | /// | `MakeList` | `[… v1…vn → … list]` | −(n−1) | /// | `MakeTuple` | `[… v1…vn → … tuple]` | −(n−1) | +/// | `MakeOverloads` | `[… f1…fn → … function]` | −(n−1) | /// | `MakeMap` | `[… k1 v1…kn vn (default?) → … map]` | −(2n−1) or −2n if `has_default` | /// | `MakeRange` | `[… start (end?) → … iter]` | −1 if bounded, 0 if unbounded | /// | `Closure` | `[… → … closure]` | +1 | @@ -119,6 +120,10 @@ pub enum OpCode { MakeList(usize), /// Pops `n` values, pushes a tuple. `[… v1…vn → … tuple]` MakeTuple(usize), + /// Pops `n` functions, most specific first, and pushes a function that + /// calls the first of them that accepts each call's arguments. + /// `[… f1…fn → … function]` + MakeOverloads(usize), /// Pops `2n` values (+ optional default), pushes a map. MakeMap { pairs: usize, has_default: bool }, /// Pushes a closure, capturing values from locals/upvalues. `[… → … closure]` @@ -149,6 +154,12 @@ pub enum OpCode { Memoize, /// Peeks top; errors if the value does not conform to the type. `[… value → … value]` CheckType(Rc), + /// Pops a value, pushes `Some(value)` if it conforms to the type and + /// `None` if it doesn't. `[… value → … option]` + TryCast(Rc), + /// Peeks top, a value of type `Any` where code expects the type; errors + /// if it isn't one. `[… value → … value]` + ExpectType(Rc), } impl OpCode { @@ -172,6 +183,7 @@ impl OpCode { Self::SetGlobal(_) => "SetGlobal", Self::MakeList(_) => "MakeList", Self::MakeTuple(_) => "MakeTuple", + Self::MakeOverloads(_) => "MakeOverloads", Self::MakeMap { .. } => "MakeMap", Self::Closure { .. } => "Closure", Self::GetIterator => "GetIterator", @@ -185,6 +197,8 @@ impl OpCode { Self::NewCell(_) => "NewCell", Self::Memoize => "Memoize", Self::CheckType(_) => "CheckType", + Self::TryCast(_) => "TryCast", + Self::ExpectType(_) => "ExpectType", } } @@ -212,7 +226,7 @@ impl OpCode { | Self::GetGlobal(_) => 1, Self::SetLocal(_) | Self::SetCell(_) | Self::SetUpvalue(_) | Self::SetGlobal(_) => -1, // Pops `n` elements, pushes the collection: -n+1. - Self::MakeList(n) | Self::MakeTuple(n) => 1 - *n as isize, + Self::MakeList(n) | Self::MakeTuple(n) | Self::MakeOverloads(n) => 1 - *n as isize, // Pops a key and a value per pair plus the optional default, // pushes the map: -(2*pairs + has_default) + 1. Self::MakeMap { pairs, has_default } => { @@ -240,7 +254,9 @@ impl OpCode { Self::NewCell(_) => 0, Self::Memoize => 0, // Peeks the value without popping it. - Self::CheckType(_) => 0, + Self::CheckType(_) | Self::ExpectType(_) => 0, + // Replaces the value with the option holding it. + Self::TryCast(_) => 0, } } } @@ -266,6 +282,7 @@ impl std::fmt::Debug for OpCode { Self::SetUpvalue(n) => write!(f, "SetUpvalue({n})"), Self::MakeList(n) => write!(f, "MakeList({n})"), Self::MakeTuple(n) => write!(f, "MakeTuple({n})"), + Self::MakeOverloads(n) => write!(f, "MakeOverloads({n})"), Self::MakeMap { pairs, has_default } => { write!(f, "MakeMap({pairs}, default={has_default})") } @@ -294,6 +311,8 @@ impl std::fmt::Debug for OpCode { Self::Unpack(n) => write!(f, "Unpack({n})"), Self::Memoize => write!(f, "Memoize"), Self::CheckType(typ) => write!(f, "CheckType({typ})"), + Self::TryCast(typ) => write!(f, "TryCast({typ})"), + Self::ExpectType(typ) => write!(f, "ExpectType({typ})"), } } } diff --git a/ndc_vm/src/compiler.rs b/ndc_vm/src/compiler.rs index 6d22611f..27db14f8 100644 --- a/ndc_vm/src/compiler.rs +++ b/ndc_vm/src/compiler.rs @@ -1,7 +1,8 @@ use crate::chunk::{JumpTarget, LabelId, OpCode, OptimizerIr}; use crate::value::{CompiledFunction, Function}; use crate::{Object, Value}; -use ndc_analysis::{Analysis, Binding, Candidate, ResolvedVar}; +use ahash::AHashMap; +use ndc_analysis::{Analysis, Binding, Candidate, Instance, ResolvedVar}; use ndc_core::NodeId; use ndc_core::StaticType; use ndc_lexer::{NumericLiteral, Span}; @@ -9,8 +10,17 @@ use ndc_parser::{ AssignmentTarget, AssignmentTargetLocation, BindingPattern, BindingPatternLocation, Expression, ExpressionLocation, ForBody, ForIteration, LogicalOperator, }; +use std::cell::{Cell, RefCell}; use std::rc::Rc; +/// Where an instance of a generic function is in [`Analysis::instances`]: +/// the declaration it's an instance of, and which of its instances it is. +#[derive(Clone, Copy)] +struct InstanceAt { + declaration: NodeId, + index: usize, +} + pub struct Compiler { ir: OptimizerIr, source_locals: usize, @@ -21,6 +31,17 @@ pub struct Compiler { optimize: bool, /// What the analyser worked out about the batch being compiled. analysis: Rc, + /// The instance of a generic function whose body is being compiled, + /// whose picks take the place of the function's own. + instance: Option, + /// Whether the code compiled so far used a pick of `instance` that + /// differs from the function's own, so it differs from what the + /// function's own body compiles to. + used_instance: Cell, + /// The functions compiled outside of any instance, by declaration. A + /// function an instance's body declares is the same function when it + /// compiles the same, so it keeps its identity. + prototypes: Rc>>>, } impl Compiler { @@ -34,6 +55,9 @@ impl Compiler { allow_return: false, optimize: true, analysis: Rc::default(), + instance: None, + used_instance: Cell::new(false), + prototypes: Rc::default(), } } @@ -47,6 +71,9 @@ impl Compiler { allow_return: false, optimize: false, analysis: Rc::default(), + instance: None, + used_instance: Cell::new(false), + prototypes: Rc::default(), } } /// Compiles a program, or one REPL line, into a function the VM runs at @@ -186,7 +213,15 @@ impl Compiler { } Expression::NumericLiteral(literal) => { let value = match literal { - NumericLiteral::Int64(i) => Value::int(i), + // An integer literal is what its use made it: `1` in + // `[1.5, 1]` is a Float. + NumericLiteral::Int64(i) => match self.analysis.expr_types.get(&id) { + Some(StaticType::Float) => Value::float(i as f64), + Some(StaticType::Number) => { + Value::number(crate::value::AdvancedNumber::Int(i.into())) + } + _ => Value::int(i), + }, NumericLiteral::Float64(f) => Value::float(f), NumericLiteral::NumberInt(i) => { Value::number(crate::value::AdvancedNumber::Int(i)) @@ -199,19 +234,40 @@ impl Compiler { let idx = self.ir.add_constant(value); self.ir.write(OpCode::Constant(idx), span); } - Expression::Identifier { .. } => { - let analysis = Rc::clone(&self.analysis); - self.compile_binding(analysis.binding(id), span)?; - } - Expression::Cast { value, .. } => { + Expression::Identifier { .. } => match self.binding(id) { + // A function's name used as a value whose overload is picked + // as it's called: each overload is looked up now, where the + // value is made, so the value works wherever it's called. + Binding::Dynamic(candidates) => { + for candidate in &candidates { + self.emit_get_var(candidate.var(), span); + } + self.ir.write(OpCode::MakeOverloads(candidates.len()), span); + } + binding => self.compile_binding(&binding, span)?, + }, + Expression::Cast { + value, optional, .. + } => { self.compile_expr(*value)?; - match self.analysis.cast_checks.get(&id) { + let check = match self.analysis.cast_checks.get(&id) { None => return Err(CompileError::unresolved_cast(span)), - Some(None) => {} - Some(Some(target)) => { - let target = Rc::new(target.clone()); + Some(check) => check.clone().map(Rc::new), + }; + match (check, optional) { + (None, false) => {} + (Some(target), false) => { self.ir.write(OpCode::CheckType(target), span); } + // A value proven to be the type is always `Some`, as + // every value is an `Any`. + (None, true) => { + self.ir + .write(OpCode::TryCast(Rc::new(StaticType::Any)), span); + } + (Some(target), true) => { + self.ir.write(OpCode::TryCast(target), span); + } } } Expression::Statement(stm) => { @@ -257,15 +313,16 @@ impl Compiler { value: container, index, } => { - let analysis = Rc::clone(&self.analysis); - self.compile_binding(analysis.setter(l_value.id), span)?; + self.compile_binding(&self.setter(l_value.id), span)?; self.compile_expr(*container)?; self.compile_expr(*index)?; self.compile_expr(*value)?; + self.expect_type(l_value.id, span); self.ir.write(OpCode::Call(3), span); } AssignmentTarget::Identifier { span: lv_span, .. } => { self.compile_expr(*value)?; + self.expect_type(l_value.id, lv_span); self.emit_set_var(self.assigned_variable(l_value.id), lv_span); self.emit_unit(); } @@ -282,27 +339,28 @@ impl Compiler { member_span, .. } => { - let analysis = Rc::clone(&self.analysis); - self.compile_binding(analysis.setter(l_value.id), member_span)?; + self.compile_binding(&self.setter(l_value.id), member_span)?; self.compile_expr(*receiver)?; self.compile_expr(*value)?; + self.expect_type(l_value.id, member_span); self.ir.write(OpCode::Call(2), member_span); } }, Expression::OpAssignment { l_value, r_value, .. } => { + let target_id = l_value.id; let target = PreparedAssignmentTarget::prepare(self, l_value)?; - let analysis = Rc::clone(&self.analysis); - let Some(binding) = analysis.augmented_assignments.get(&id) else { + let Some(binding) = self.augmented_assignment(id) else { return Err(CompileError::unresolved_binding(span)); }; - let opcode = Self::call_opcode_for(binding, 2); - self.compile_binding(binding, span)?; + let opcode = Self::call_opcode_for(&binding, 2); + self.compile_binding(&binding, span)?; target.emit_read(self)?; self.compile_expr(*r_value)?; self.ir.write(opcode, span); + self.expect_type(target_id, span); target.emit_store(self)?; self.emit_unit(); @@ -393,11 +451,18 @@ impl Compiler { let function_span = function.span; let opcode = match &function.expression { Expression::Identifier { .. } => { - Self::call_opcode_for(self.analysis.binding(function.id), arguments.len()) + Self::call_opcode_for(&self.binding(function.id), arguments.len()) } _ => OpCode::Call(arguments.len()), }; - self.compile_expr(*function)?; + // A named callee is looked up as a call picks it, not as a + // value: the overloads the program picks from stay an + // overload set, which a call resolves from its frame. + if let Expression::Identifier { .. } = function.expression { + self.compile_binding(&self.binding(function.id), function_span)?; + } else { + self.compile_expr(*function)?; + } for argument in arguments { self.compile_expr(argument)?; @@ -410,11 +475,10 @@ impl Compiler { member_span, .. } => { - let analysis = Rc::clone(&self.analysis); - let getter = analysis + let getter = self .getter(id) .expect("internal error: every member access is analysed before it's compiled"); - self.compile_binding(getter, member_span)?; + self.compile_binding(&getter, member_span)?; self.compile_expr(*receiver)?; self.ir.write(OpCode::Call(1), member_span); } @@ -493,12 +557,27 @@ impl Compiler { } } + if leaves_value { + self.expect_type(id, span); + } // Every expression except a statement leaves exactly one value. self.ir .set_stack_depth(entry_depth + isize::from(leaves_value)); Ok(()) } + /// Checks the value on top of the stack, which comes from `Any`, where + /// the analyser says code at `at` expects a type. + fn expect_type(&mut self, at: NodeId, span: Span) { + if let Some(expected) = self + .instance_pick(|instance| instance.type_checks.get(&at)) + .or_else(|| self.analysis.type_checks.get(&at)) + { + let expected = Rc::new(expected.clone()); + self.ir.write(OpCode::ExpectType(expected), span); + } + } + fn compile_assignment_target( &mut self, l_value: AssignmentTargetLocation, @@ -506,6 +585,7 @@ impl Compiler { ) -> Result<(), CompileError> { match l_value.target { AssignmentTarget::Identifier { span: lv_span, .. } => { + self.expect_type(l_value.id, lv_span); self.emit_set_var(self.assigned_variable(l_value.id), lv_span); } AssignmentTarget::Index { value, index } => { @@ -516,11 +596,11 @@ impl Compiler { // 4. Call []= function // 5. Pop the return value + self.expect_type(l_value.id, span); let tmp_value = self.allocate_temp(); self.write_temp(tmp_value, span, OpCode::SetLocal); - let analysis = Rc::clone(&self.analysis); - self.compile_binding(analysis.setter(l_value.id), span)?; + self.compile_binding(&self.setter(l_value.id), span)?; self.compile_expr(*value)?; self.compile_expr(*index)?; self.write_temp(tmp_value, span, OpCode::GetLocal); @@ -551,6 +631,7 @@ impl Compiler { let variable = self.analysis.variable(pattern.id).expect( "internal error: every binding pattern is analysed before it's compiled", ); + self.expect_type(pattern.id, span); self.emit_declare(variable, span); } BindingPattern::Sequence(seq) => { @@ -572,12 +653,57 @@ impl Compiler { /// The function an augmented assignment reads the target at `target` with. fn target_getter(&self, target: NodeId) -> Binding { - self.analysis - .getter(target) + self.getter(target) .expect("internal error: an augmented assignment's target has a getter") + } + + /// What the identifier `id` refers to, in the instance being compiled. + fn binding(&self, id: NodeId) -> Binding { + self.instance_pick(|instance| instance.bindings.get(&id)) + .unwrap_or_else(|| self.analysis.binding(id)) + .clone() + } + + /// The pick `pick` finds in the instance being compiled, if any, noting + /// that the code depends on the instance. + fn instance_pick<'a, T>( + &'a self, + pick: impl FnOnce(&'a Instance) -> Option<&'a T>, + ) -> Option<&'a T> { + let found = self + .instance + .map(|at| &self.analysis.instances[&at.declaration][at.index]) + .and_then(pick); + if found.is_some() { + self.used_instance.set(true); + } + found + } + + /// The function that reads through the target at `id`, if it reads, in + /// the instance being compiled. + fn getter(&self, id: NodeId) -> Option { + self.instance_pick(|instance| instance.getters.get(&id)) + .or_else(|| self.analysis.getter(id)) + .cloned() + } + + /// The function that writes through the target at `id`, in the instance + /// being compiled. + fn setter(&self, id: NodeId) -> Binding { + self.instance_pick(|instance| instance.setters.get(&id)) + .unwrap_or_else(|| self.analysis.setter(id)) .clone() } + /// The function the augmented assignment `id` calls, in the instance + /// being compiled. + fn augmented_assignment(&self, id: NodeId) -> Option { + self.instance_pick(|instance| instance.augmented_assignments.get(&id)) + .or_else(|| self.analysis.augmented_assignments.get(&id)) + .cloned() + } + /// Emit code that puts the resolved callee on top of the stack. /// /// * `Resolved(Scalar(var))` and `Resolved(Vec(scalar))` both emit a @@ -785,36 +911,29 @@ impl Compiler { span: Span, ) -> Result<(), CompileError> { let name_variable = self.analysis.variable(id); - let static_type = self.analysis.function_type(id).clone(); - let mut fn_compiler = Self { - ir: Default::default(), - source_locals: self.analysis.frame_size(id), - temp_count: 0, - temp_uses: Vec::new(), - loop_stack: vec![], - allow_return: true, - optimize: self.optimize, - analysis: Rc::clone(&self.analysis), - }; - fn_compiler.compile_expr(body)?; - fn_compiler.ir.write(OpCode::Return, Span::synthetic()); - fn_compiler.layout_temporaries(); - - if fn_compiler.optimize { - fn_compiler.ir.peephole(); - } - - let num_locals = fn_compiler.num_locals(); - let compiled = CompiledFunction { - name, - static_type, - body: fn_compiler.ir.into_chunk(), - num_locals, - num_globals: 0, + let analysis = Rc::clone(&self.analysis); + let instances = analysis.instances.get(&id).map_or(&[][..], Vec::as_slice); + // The function's own body comes first, so the functions it declares + // are there for its instances to reuse. + let (compiled, instance_bodies) = if instances.is_empty() { + (self.prototype(id, name, body)?, Vec::new()) + } else { + let compiled = self.prototype(id, name.clone(), body.clone())?; + let instance_bodies = (0..instances.len()) + .map(|index| { + let instance = InstanceAt { + declaration: id, + index, + }; + self.compile_function_body(id, name.clone(), body.clone(), Some(instance)) + .map(|(compiled, _)| compiled) + }) + .collect::, _>>()?; + (compiled, instance_bodies) }; let idx = self .ir - .add_constant(Value::function(Function::Compiled(Rc::new(compiled)))); + .add_constant(Value::function(Function::Compiled(compiled))); // A captured function may capture itself, before its value exists, so // its cell must be in place before the closure is made. @@ -846,11 +965,9 @@ impl Compiler { match name_variable { Some(var @ (ResolvedVar::Local { .. } | ResolvedVar::Global { .. })) => { self.emit_declare(var, span); - self.emit_unit(); } Some(ResolvedVar::Cell { slot }) => { self.ir.write(OpCode::SetCell(slot), span); - self.emit_unit(); } Some(ResolvedVar::Upvalue { .. }) => { unreachable!("the analyser never assigns a declaration to an upvalue") @@ -858,9 +975,103 @@ impl Compiler { None => {} } + // Each instance of a generic function is its body compiled with the + // instance's picks. A generic function is declared at the top level, + // so it captures nothing and is a global, and its instances are + // stored in globals of their own. A `pure fn`'s instance has a cache + // of its own, as instances may check values of type `Any` against + // their own types: `pure fn has(xs, v) { xs.contains(v) }` checks `v` + // for an Int in its instance for `List`, but not for + // `List`, and both may be called with an empty list. + for (instance, instance_body) in instances.iter().zip(instance_bodies) { + let idx = self + .ir + .add_constant(Value::function(Function::Compiled(Rc::new(instance_body)))); + self.ir.write(OpCode::Constant(idx), span); + if pure { + self.ir.write(OpCode::Memoize, span); + } + self.emit_declare(instance.variable, span); + } + + if name_variable.is_some() { + self.emit_unit(); + } + Ok(()) } + /// The compiled function declared at `id`. Inside an instance's body, a + /// function that compiles as it does outside of it is the same one. + fn prototype( + &self, + id: NodeId, + name: Option, + body: ExpressionLocation, + ) -> Result, CompileError> { + let (compiled, used_instance) = self.compile_function_body(id, name, body, None)?; + if self.instance.is_none() { + let compiled = Rc::new(compiled); + self.prototypes + .borrow_mut() + .insert(id, Rc::clone(&compiled)); + return Ok(compiled); + } + if used_instance { + self.used_instance.set(true); + } else if let Some(prototype) = self.prototypes.borrow().get(&id) { + return Ok(Rc::clone(prototype)); + } + Ok(Rc::new(compiled)) + } + + /// Compiles the body of the function declared at `id`, with the picks + /// of `instance` if it's an instance of a generic function. The + /// functions it declares are compiled with them too. Also returns + /// whether the body used a pick of the instance it's compiled in. + fn compile_function_body( + &self, + id: NodeId, + name: Option, + body: ExpressionLocation, + instance: Option, + ) -> Result<(CompiledFunction, bool), CompileError> { + let static_type = self.analysis.function_type(id).clone(); + let mut fn_compiler = Self { + ir: Default::default(), + source_locals: self.analysis.frame_size(id), + temp_count: 0, + temp_uses: Vec::new(), + loop_stack: vec![], + allow_return: true, + optimize: self.optimize, + analysis: Rc::clone(&self.analysis), + instance: instance.or(self.instance), + used_instance: Cell::new(false), + prototypes: Rc::clone(&self.prototypes), + }; + fn_compiler.compile_expr(body)?; + fn_compiler.ir.write(OpCode::Return, Span::synthetic()); + fn_compiler.layout_temporaries(); + + if fn_compiler.optimize { + fn_compiler.ir.peephole(); + } + + let num_locals = fn_compiler.num_locals(); + let used_instance = fn_compiler.used_instance.get(); + Ok(( + CompiledFunction { + name, + static_type, + body: fn_compiler.ir.into_chunk(), + num_locals, + num_globals: 0, + }, + used_instance, + )) + } + fn compile_for( &mut self, iterations: &[ForIteration], @@ -1079,7 +1290,7 @@ impl PreparedAssignmentTarget { container_span, index_span, getter: compiler.target_getter(l_value.id), - setter: compiler.analysis.setter(l_value.id).clone(), + setter: compiler.setter(l_value.id), }) } AssignmentTarget::Member { @@ -1098,7 +1309,7 @@ impl PreparedAssignmentTarget { receiver_span, member_span, getter: compiler.target_getter(l_value.id), - setter: compiler.analysis.setter(l_value.id).clone(), + setter: compiler.setter(l_value.id), }) } AssignmentTarget::Sequence(_) => unreachable!( diff --git a/ndc_vm/src/iterator.rs b/ndc_vm/src/iterator.rs index 5ba66c7c..e2a3e20f 100644 --- a/ndc_vm/src/iterator.rs +++ b/ndc_vm/src/iterator.rs @@ -457,7 +457,7 @@ impl VmIterator for CombinationsIter { if k == 0 { return if self.first { self.first = false; - Some(Value::tuple(vec![])) + Some(Value::list(vec![])) } else { None }; @@ -504,7 +504,7 @@ impl VmIterator for CombinationsIter { } // All indices are in-bounds: direct indexing is safe. - Some(Value::tuple( + Some(Value::list( self.indices .iter() .map(|&i| self.buffer[i].clone()) diff --git a/ndc_vm/src/value/function.rs b/ndc_vm/src/value/function.rs index 7608d437..21919eb5 100644 --- a/ndc_vm/src/value/function.rs +++ b/ndc_vm/src/value/function.rs @@ -22,6 +22,44 @@ pub enum Function { cache: Rc>>, function: Rc, }, + + /// A function passed as a function type it may not have: a value of + /// type `Any` used as a `Fn(Int) -> Int` that holds a function whose + /// return type isn't known. Each call checks what it returns. + Checked { + function: Rc, + /// The function type it's used as, boxed so that it doesn't make + /// every function value bigger. + typ: Box, + }, + + /// The overloads of a function, most specific first, of which each call + /// calls the first that accepts its arguments: an overloaded function's + /// name used as a value where the analyser couldn't tell which one is + /// meant, as `signum` in the body of `fn signs(xs) => xs.map(signum)` + /// compiled for arguments of any type. + Overloads(Rc<[Self]>), +} + +/// The type of a [`Function::Overloads`], which takes what one of its +/// overloads takes and returns what it returns. +static ANY_FUNCTION: std::sync::LazyLock = + std::sync::LazyLock::new(|| StaticType::Function { + parameters: None, + return_type: Box::new(StaticType::Any), + }); + +/// How a function fits a function type, from the function's own type, +/// which leaves out what isn't known as the program runs. +#[derive(Debug, PartialEq, Eq)] +pub enum FunctionFit { + /// It takes what the type passes and returns what it returns. + Proven, + /// It takes what the type passes, but what it returns has to be checked + /// at each call. + IfChecked, + /// It can't be a function of that type. + No, } pub struct NativeFunction { @@ -182,15 +220,19 @@ impl Function { match self { Self::Closure(c) => Some(&c.prototype), Self::Compiled(f) => Some(f), - Self::Memoized { function, .. } => function.prototype(), - Self::Native(_) => None, + Self::Memoized { function, .. } | Self::Checked { function, .. } => { + function.prototype() + } + Self::Native(_) | Self::Overloads(_) => None, } } pub fn documentation(&self) -> Option<&str> { match self { Self::Native(f) => f.documentation.as_deref(), - Self::Memoized { function, .. } => function.documentation(), + Self::Memoized { function, .. } | Self::Checked { function, .. } => { + function.documentation() + } _ => None, } } @@ -200,29 +242,128 @@ impl Function { Self::Compiled(f) => f.name.as_deref(), Self::Native(f) => Some(&f.name), Self::Closure(c) => c.prototype.name.as_deref(), - Self::Memoized { function, .. } => function.name(), + Self::Memoized { function, .. } | Self::Checked { function, .. } => function.name(), + Self::Overloads(candidates) => candidates.first().and_then(Self::name), } } pub fn static_type(&self) -> StaticType { + self.signature().clone() + } + + /// The function's type: what it takes and returns, as far as that's + /// known as the program runs. + fn signature(&self) -> &StaticType { match self { - Self::Compiled(f) => f.static_type.clone(), - Self::Native(f) => f.static_type.clone(), - Self::Closure(c) => c.prototype.static_type.clone(), - Self::Memoized { function, .. } => function.static_type(), + Self::Compiled(f) => &f.static_type, + Self::Native(f) => &f.static_type, + Self::Closure(c) => &c.prototype.static_type, + Self::Memoized { function, .. } => function.signature(), + Self::Checked { typ, .. } => typ, + Self::Overloads(_) => &ANY_FUNCTION, + } + } + + /// The function a [`Self::Checked`] checks, or this one: checking its + /// calls doesn't make it another function, as far as `==` goes. + pub fn unchecked(&self) -> &Self { + match self { + Self::Checked { function, .. } => function.unchecked(), + function => function, + } + } + + /// The types of the parameters, when it takes a known number of them. + pub fn parameters(&self) -> Option<&[StaticType]> { + match self.signature() { + StaticType::Function { + parameters: Some(parameters), + .. + } => Some(parameters), + _ => None, + } + } + + /// How this function fits `expected`, a function type. A parameter or + /// return type that its own type leaves unknown, as `Any`, fits + /// anything, but an unknown return type has to be checked at each call: + /// a lambda that returns a String doesn't fit `Fn(Int) -> Int`, and one + /// whose return type isn't known fits it if checked. + pub fn fit(&self, expected: &StaticType) -> FunctionFit { + let StaticType::Function { + parameters: expected_parameters, + return_type: expected_return, + } = expected + else { + return FunctionFit::No; + }; + let StaticType::Function { + parameters, + return_type, + } = self.signature() + else { + return FunctionFit::No; + }; + let (takes, takes_proven) = match (expected_parameters, parameters) { + (Some(expected), Some(parameters)) => ( + expected.len() == parameters.len() + && expected + .iter() + .zip(parameters) + .all(|(expected, parameter)| could_be(expected, parameter)), + expected + .iter() + .zip(parameters) + .all(|(expected, parameter)| proves(expected, parameter)), + ), + (_, None) => (true, true), + (None, Some(_)) => (true, false), + }; + if !takes || !could_be(return_type, expected_return) { + FunctionFit::No + } else if takes_proven && proves(return_type, expected_return) { + FunctionFit::Proven + } else { + FunctionFit::IfChecked + } + } + + /// Whether this function accepts the given arguments, checking every + /// element of a container argument against a parameter like + /// `List`, which [`Self::matches_value_args`] doesn't. + pub fn conforms_value_args(&self, args: &[Value]) -> bool { + if let Self::Overloads(candidates) = self { + return candidates + .iter() + .any(|candidate| candidate.conforms_value_args(args)); + } + match self.signature() { + StaticType::Function { + parameters: Some(params), + .. + } => { + params.len() == args.len() + && params + .iter() + .zip(args) + .all(|(param, arg)| arg.conforms_to(param)) + } + StaticType::Function { + parameters: None, .. + } => true, + _ => false, } } /// Returns true if this function accepts the given arguments, without /// allocating a `StaticType` for each of them. pub fn matches_value_args(&self, args: &[Value]) -> bool { - let st = match self { - Self::Native(f) => &f.static_type, - Self::Compiled(f) => &f.static_type, - Self::Closure(c) => &c.prototype.static_type, - Self::Memoized { function, .. } => return function.matches_value_args(args), - }; - let StaticType::Function { parameters, .. } = st else { + if let Self::Overloads(candidates) = self { + return candidates + .iter() + .any(|candidate| candidate.matches_value_args(args)); + } + let StaticType::Function { parameters, .. } = self.signature() else { return false; }; match parameters { @@ -245,6 +386,8 @@ impl fmt::Debug for Function { Self::Native(native) => write!(f, "{native:?}"), Self::Closure(closure) => write!(f, "", closure.prototype.name), Self::Memoized { function, .. } => write!(f, "", function), + Self::Checked { function, typ } => write!(f, "<{function:?} checked as {typ}>"), + Self::Overloads(candidates) => write!(f, ""), } } } @@ -259,6 +402,11 @@ impl fmt::Display for Function { Self::Native(native) => write!(f, "", native.name), Self::Closure(closure) => write!(f, "", closure.prototype.name), Self::Memoized { function, .. } => write!(f, ""), + Self::Checked { function, .. } => write!(f, "{function}"), + Self::Overloads(candidates) => match candidates.first().and_then(Self::name) { + Some(name) => write!(f, ""), + None => write!(f, ""), + }, } } } @@ -267,3 +415,81 @@ impl fmt::Debug for NativeFunction { write!(f, "", self.name, self.static_type) } } + +/// Whether a value of type `found` could be an `expected`, where what isn't +/// known in either, `Any` or a type parameter, could be anything. +fn could_be(found: &StaticType, expected: &StaticType) -> bool { + let unknown = |typ: &StaticType| { + matches!( + typ, + StaticType::Any | StaticType::Generic(_) | StaticType::Var(_) + ) + }; + // A function that never returns returns whatever is expected. + if unknown(found) || unknown(expected) || *found == StaticType::Never { + return true; + } + match (found, expected) { + (StaticType::List(a), StaticType::List(b)) + | (StaticType::Deque(a), StaticType::Deque(b)) + | (StaticType::Option(a), StaticType::Option(b)) + | (StaticType::Iterator(a), StaticType::Iterator(b)) + | (StaticType::Iterable(a), StaticType::Iterable(b)) + | (StaticType::MinHeap(a), StaticType::MinHeap(b)) + | (StaticType::MaxHeap(a), StaticType::MaxHeap(b)) => could_be(a, b), + (found, StaticType::Iterable(element)) => found + .sequence_element_type() + .is_some_and(|found| could_be(&found, element)), + (StaticType::Tuple(a), StaticType::Tuple(b)) => { + a.len() == b.len() && a.iter().zip(b).all(|(a, b)| could_be(a, b)) + } + ( + StaticType::Map { key, value }, + StaticType::Map { + key: expected_key, + value: expected_value, + }, + ) => could_be(key, expected_key) && could_be(value, expected_value), + ( + StaticType::Function { + parameters, + return_type, + }, + StaticType::Function { + parameters: expected_parameters, + return_type: expected_return, + }, + ) => { + let takes = match (parameters, expected_parameters) { + (Some(a), Some(b)) => { + a.len() == b.len() && a.iter().zip(b).all(|(a, b)| could_be(b, a)) + } + _ => true, + }; + takes && could_be(return_type, expected_return) + } + (found, expected) => found == expected, + } +} + +/// Whether every value of type `found` is an `expected`: a function that +/// takes `Any` takes anything, and one that returns a `List` returns an +/// `Iterable`. +fn proves(found: &StaticType, expected: &StaticType) -> bool { + // A type parameter of a built-in's signature, as `U` in `map`'s + // `Fn(T) -> U`, is whatever the function gives it. + matches!( + expected, + StaticType::Any | StaticType::Generic(_) | StaticType::Var(_) + ) || (!has_unknowns(found) && found.fits(expected)) +} + +/// Whether `typ` leaves something unknown: `Any` or a type parameter. +fn has_unknowns(typ: &StaticType) -> bool { + typ.contains(&|part| { + matches!( + part, + StaticType::Any | StaticType::Generic(_) | StaticType::Var(_) + ) + }) +} diff --git a/ndc_vm/src/value/mod.rs b/ndc_vm/src/value/mod.rs index 5c8708a6..57089961 100644 --- a/ndc_vm/src/value/mod.rs +++ b/ndc_vm/src/value/mod.rs @@ -44,7 +44,7 @@ fn bounded_element_type<'a>( } let found = value.static_type_with_budget(depth, budget); element_type = Some(match element_type { - Some(previous) => previous.lub(&found), + Some(previous) => previous.join(&found), None => found, }); } @@ -130,11 +130,11 @@ impl Ord for OrdValue { } } -/// Type alias for [`Value`] that signals `StaticType::Sequence` to the `#[export_module]` macro. +/// Type alias for [`Value`] that signals `StaticType::Iterable` to the `#[export_module]` macro. /// /// Use this instead of `ndc_vm::value::Value` in stdlib function signatures when the parameter /// must accept any iterable sequence. The extraction is identical (zero-copy pass-through on the -/// VM path) but the macro emits `StaticType::Sequence(Any)` instead of `StaticType::Any`, which +/// VM path) but the macro emits `StaticType::Iterable(Any)` instead of `StaticType::Any`, which /// preserves the correct type for dispatch and static analysis. pub type SeqValue = Value; @@ -277,7 +277,7 @@ impl Value { /// # Performance /// /// **O(n) for `List`, `Map`, and `Deque`** — these variants iterate all - /// elements to compute the element type via `lub`. Avoid calling this in + /// elements to join their types into the element type. Avoid calling this in /// hot paths on container values. Use the dedicated helpers instead: /// /// - [`Value::is_number`] — O(1) check for numeric types @@ -323,13 +323,14 @@ impl Value { /// avoiding full container iteration for common cases. /// /// When `param` is a container type whose inner types are all `Any` (e.g. - /// `Sequence(Any)`, `Map { Any, Any }`, `List(Any)`), only the outer kind is + /// `Iterable(Any)`, `Map { Any, Any }`, `List(Any)`), only the outer kind is /// checked — no element iteration occurs. Typed container parameters are - /// rejected rather than scanned; the remaining cases fall back to - /// `self.static_type().is_subtype(param)`. + /// rejected rather than scanned; the remaining cases are checked as + /// [`Value::is_a`] does. pub fn matches_param(&self, param: &StaticType) -> bool { match param { - StaticType::Any => true, + // A type parameter like `T` in `push(List, T)` takes any value. + param if param.is_unknown() => true, StaticType::Int => { matches!(self, Self::Int(_)) } @@ -339,21 +340,35 @@ impl Value { StaticType::String => { matches!(self, Self::Object(o) if matches!(o.as_ref(), Object::String(_))) } - StaticType::List(t) if matches!(t.as_ref(), StaticType::Any) => { + StaticType::List(t) if t.is_unknown() => { matches!(self, Self::Object(o) if matches!(o.as_ref(), Object::List(_))) } - StaticType::Deque(t) if matches!(t.as_ref(), StaticType::Any) => { + StaticType::Deque(t) if t.is_unknown() => { matches!(self, Self::Object(o) if matches!(o.as_ref(), Object::Deque(_))) } - StaticType::Map { key, value } - if matches!( - (key.as_ref(), value.as_ref()), - (StaticType::Any, StaticType::Any) - ) => - { + StaticType::Map { key, value } if key.is_unknown() && value.is_unknown() => { matches!(self, Self::Object(o) if matches!(o.as_ref(), Object::Map { .. })) } - StaticType::Sequence(t) if matches!(t.as_ref(), StaticType::Any) => { + StaticType::MinHeap(t) if t.is_unknown() => { + matches!(self, Self::Object(o) if matches!(o.as_ref(), Object::MinHeap(_))) + } + StaticType::MaxHeap(t) if t.is_unknown() => { + matches!(self, Self::Object(o) if matches!(o.as_ref(), Object::MaxHeap(_))) + } + // Unit, the empty tuple, is checked as cheaply as a scalar. + StaticType::Tuple(elements) if elements.is_empty() => { + matches!(self, Self::Object(o) if matches!(o.as_ref(), Object::Tuple(values) if values.is_empty())) + } + // An option holds at most one value, which is cheap to check. + StaticType::Option(t) => match self { + Self::None => true, + Self::Object(o) => match o.as_ref() { + Object::Some(value) => value.matches_param(t), + _ => false, + }, + _ => false, + }, + StaticType::Iterable(t) if t.is_unknown() => { matches!(self, Self::Object(o) if matches!( o.as_ref(), Object::List(_) @@ -371,11 +386,52 @@ impl Value { // compile time; if it falls through to dynamic dispatch, return false. StaticType::List(_) | StaticType::Deque(_) - | StaticType::Sequence(_) + | StaticType::Iterable(_) | StaticType::Map { .. } - | StaticType::Tuple(_) => false, - _ => self.static_type().is_subtype(param), + | StaticType::Tuple(_) + | StaticType::MinHeap(_) + | StaticType::MaxHeap(_) => false, + _ => self.is_a(param), + } + } + + /// Whether this value is a `target`, a type that holds no other values + /// as a container does: a scalar, a struct, a function or an iterator. + /// A function is one if its own type says so (see [`Function::fit`]): + /// only [`Self::converted_to`] can make one that may be one a function + /// whose calls are checked. What an iterator makes can't be checked + /// without making it. + fn is_a(&self, target: &StaticType) -> bool { + if let StaticType::Function { .. } = target + && let Self::Object(object) = self + && let Object::Function(function) = object.as_ref() + { + return function.fit(target) == FunctionFit::Proven; + } + match (target, self.static_type()) { + (StaticType::Iterator(_), StaticType::Iterator(_)) => true, + (target, found) => found == *target, + } + } + + /// This value as a `target`, if it is one, checking every element of a + /// container: a function that may be a function `target` is wrapped so + /// that each call checks what it returns. + pub fn converted_to(&self, target: &StaticType) -> Option { + if let StaticType::Function { .. } = target + && let Self::Object(object) = self + && let Object::Function(function) = object.as_ref() + { + return match function.fit(target) { + FunctionFit::Proven => Some(self.clone()), + FunctionFit::IfChecked => Some(Self::function(Function::Checked { + function: Rc::new(function.unchecked().clone()), + typ: Box::new(target.clone()), + })), + FunctionFit::No => None, + }; } + self.conforms_to(target).then(|| self.clone()) } /// Check whether this value conforms to `target`, scanning container @@ -391,8 +447,23 @@ impl Value { } fn conforms_to_cached(&self, target: &StaticType, cache: &mut ConformanceCache) -> bool { + // Only a container target looks inside the value, which may be shared + // or cyclic; checking against another type takes no time to redo. + let looks_inside = matches!( + target, + StaticType::Option(_) + | StaticType::List(_) + | StaticType::Deque(_) + | StaticType::Tuple(_) + | StaticType::Map { .. } + | StaticType::MinHeap(_) + | StaticType::MaxHeap(_) + | StaticType::Iterable(_) + ); let cache_key = match self { - Self::Object(object) => Some((Rc::as_ptr(object), std::ptr::from_ref(target))), + Self::Object(object) if looks_inside => { + Some((Rc::as_ptr(object), std::ptr::from_ref(target))) + } _ => None, }; if let Some(cache_key) = cache_key @@ -404,7 +475,7 @@ impl Value { // Static types are finite trees, so this pair cannot be re-entered // before its result is stored. let result = match target { - StaticType::Any => true, + target if target.is_unknown() => true, StaticType::Option(inner) => match self { Self::None => true, Self::Object(object) => match object.as_ref() { @@ -465,7 +536,7 @@ impl Value { Self::Object(object) if matches!(object.as_ref(), Object::MaxHeap(values) if values.borrow().iter().all(|OrdValue(value)| value.conforms_to_cached(element, cache))) ), - StaticType::Sequence(element) => match self { + StaticType::Iterable(element) => match self { Self::Object(object) => match object.as_ref() { Object::List(values) => values .borrow() @@ -478,7 +549,7 @@ impl Value { .borrow() .iter() .all(|value| value.conforms_to_cached(element, cache)), - Object::String(_) => StaticType::String.is_subtype(element), + Object::String(_) => StaticType::String.fits(element), Object::Map { entries, .. } => { entries .borrow() @@ -489,7 +560,7 @@ impl Value { key.conforms_to_cached(&elements[0], cache) && value.conforms_to_cached(&elements[1], cache) } - StaticType::Sequence(inner) => { + StaticType::Iterable(inner) => { key.conforms_to_cached(inner, cache) && value.conforms_to_cached(inner, cache) } @@ -527,7 +598,7 @@ impl Value { { false } - _ => self.static_type().is_subtype(target), + _ => self.is_a(target), }, }; @@ -654,8 +725,8 @@ impl Object { /// Returns the best static type descriptor for this runtime value. /// /// `[1, 2]` reports `List` and `%{"a": 1}` reports `Map`: - /// lists and deques join their element types into a least upper bound, while - /// maps do so separately for keys and values. Tuples preserve each position's + /// lists and deques join their element types, the type they share or else + /// `Any`, while maps do so separately for keys and values. Tuples preserve each position's /// type, and `Some` wraps the type of its inner value. Empty lists, deques, and /// maps report `Any` for the types they cannot infer. /// @@ -729,11 +800,11 @@ impl Object { } let found_value = value.static_type_with_budget(depth - 1, budget); key_type = Some(match key_type { - Some(previous) => previous.lub(&found_key), + Some(previous) => previous.join(&found_key), None => found_key, }); value_type = Some(match value_type { - Some(previous) => previous.lub(&found_value), + Some(previous) => previous.join(&found_value), None => found_value, }); } @@ -1042,7 +1113,7 @@ impl PartialEq for Object { ) => a_entries.borrow().eq(&*b_entries.borrow()), (Self::Function(a), Self::Function(b)) => { // Compare function values by identity (pointer equality) - match (a, b) { + match (a.unchecked(), b.unchecked()) { (Function::Compiled(a), Function::Compiled(b)) => { std::ptr::addr_eq(Rc::as_ptr(a), Rc::as_ptr(b)) } @@ -1055,6 +1126,7 @@ impl PartialEq for Object { (Function::Memoized { cache: a, .. }, Function::Memoized { cache: b, .. }) => { Rc::ptr_eq(a, b) } + (Function::Overloads(a), Function::Overloads(b)) => Rc::ptr_eq(a, b), _ => false, } } @@ -1131,7 +1203,7 @@ impl Hash for Object { } Self::Function(f) => { state.write_u8(9); - match f { + match f.unchecked() { Function::Compiled(func) => { state.write_u8(1); Rc::as_ptr(func).hash(state); @@ -1148,6 +1220,11 @@ impl Hash for Object { state.write_u8(4); Rc::as_ptr(cache).hash(state); } + Function::Checked { .. } => unreachable!("`unchecked` strips the checks"), + Function::Overloads(candidates) => { + state.write_u8(5); + Rc::as_ptr(candidates).cast::<()>().hash(state); + } } } Self::OverloadSet { .. } => { @@ -1255,7 +1332,8 @@ mod tests { let mut cache = ConformanceCache::default(); assert!(value.conforms_to_cached(&target, &mut cache)); - assert_eq!(cache.len(), 4); + // One entry per list level; the `Any` inside needs no looking into. + assert_eq!(cache.len(), 3); // Break the reference cycle so the test does not leak its allocations. elements.borrow_mut().clear(); @@ -1273,7 +1351,7 @@ mod tests { // Dynamic dispatch must only inspect the outer container kind. let _borrow = elements.borrow_mut(); assert!(value.matches_param(&StaticType::List(Box::new(StaticType::Any)))); - assert!(value.matches_param(&StaticType::Sequence(Box::new(StaticType::Any,)))); + assert!(value.matches_param(&StaticType::Iterable(Box::new(StaticType::Any,)))); assert!(!value.matches_param(&StaticType::List(Box::new(StaticType::Int,)))); } diff --git a/ndc_vm/src/vm.rs b/ndc_vm/src/vm.rs index f3954904..732d5c1d 100644 --- a/ndc_vm/src/vm.rs +++ b/ndc_vm/src/vm.rs @@ -8,6 +8,7 @@ use crate::iterator::{ use crate::value::{CompiledFunction, Function, NativeFunc, NativeFunction}; use crate::{ClosureFunction, Object, Value}; use ndc_analysis::{CaptureSource, ResolvedVar}; +use ndc_core::StaticType; use ndc_lexer::Span; use std::cell::RefCell; use std::hash::{Hash, Hasher}; @@ -74,6 +75,9 @@ pub struct CallFrame { /// cache to write into and the hash key when the frame returns. `None` /// for ordinary (non-memoized) calls. memo: Option, + /// What the frame's call has to return, when it's a call of a + /// [`Function::Checked`]. Boxed to keep frames small. + returns: Option>, } /// The VM's globals for `natives`: native `i` is global slot `i`. The analyser @@ -108,6 +112,7 @@ impl Vm { ip: 0, frame_pointer: 0, memo: None, + returns: None, }], output: OutputSink::Stdout, #[cfg(feature = "trace")] @@ -195,19 +200,27 @@ impl Vm { } OpCode::Return => { let ret = self.stack.pop().expect("stack underflow"); - let frame = self.frames.last().expect("no frame"); + let frame = self.frames.last_mut().expect("no frame"); let frame_pointer = frame.frame_pointer; // If this was a memoized call, cache the return value now, // before the frame is torn down. if let Some((cache, key)) = &frame.memo { cache.borrow_mut().insert(*key, ret.clone()); } + let returns = frame.returns.take(); if frame_pointer == 0 { return Err(VmError::new("Return at top level", span)); } self.stack.truncate(frame_pointer - 1); self.frames.pop().expect("no frame to pop"); self.stack.push(ret); + // A checked call's result is checked where it was called. + if let Some(returns) = returns { + let caller = self.frames.last().expect("a call has a caller"); + let call = caller.closure.prototype.body.span(caller.ip - 1); + convert_result(&mut self.stack, &returns) + .map_err(|message| VmError::new(message, call))?; + } if self.frames.len() == target_depth { break Ok(()); } @@ -218,14 +231,24 @@ impl Vm { .push(frame.closure.prototype.body.constant(idx).clone()); } OpCode::CheckType(target) => { - let value = self.stack.last().expect("stack underflow"); - if !value.conforms_to(target) { + if !convert_top(&mut self.stack, target) { + let value = self.stack.last().expect("stack underflow"); return Err(VmError::new( format!("cannot cast {} to {target}", value.diagnostic_type()), span, )); } } + OpCode::TryCast(target) => try_cast(&mut self.stack, target), + OpCode::ExpectType(target) => { + if !convert_top(&mut self.stack, target) { + let value = self.stack.last().expect("stack underflow"); + return Err(VmError::new( + format!("expected {target} but found {}", value.diagnostic_type()), + span, + )); + } + } OpCode::GetLocal(slot) => { let slot = *slot; self.stack.push(self.stack[frame.slot(slot)].clone()); @@ -352,10 +375,8 @@ impl Vm { } OpCode::CallVec(args) => { let args = *args; - // Compiler emitted a directly-loaded scalar function — no - // OverloadSet probing needed. We still verify it's callable - // because the analyser may have widened its slot to Any - // somewhere along the way. + // The analyser picked the scalar function, which the + // compiler loaded directly: no overload set to probe. let scalar = { let callee = &self.stack[self.stack.len() - args - 1]; let Value::Object(obj) = callee else { @@ -387,8 +408,11 @@ impl Vm { )); } }; - let scalars = [scalar]; - if let Err(mut e) = self.dispatch_vec_call(&scalars, args, axis_len, span) { + if let Err(mut e) = + self.call_elementwise(args, axis_len, scalar.name(), span, |_, _, _| { + Ok(scalar.clone()) + }) + { e.span.get_or_insert(span); return Err(e); } @@ -403,6 +427,10 @@ impl Vm { let data = self.stack.split_off(self.stack.len() - size); self.stack.push(Value::Object(Rc::new(Object::Tuple(data)))); } + OpCode::MakeOverloads(size) => { + let size = *size; + self.make_overloads(size, span)?; + } OpCode::MakeMap { pairs, has_default } => { let (pairs, has_default) = (*pairs, *has_default); let default = if has_default { @@ -639,6 +667,14 @@ impl Vm { let mut hasher = DefaultHasher::default(); for arg in &self.stack[start..] { arg.hash(&mut hasher); + // A function checked as another function type is the same + // function, but a call may return it, so the cache tells + // them apart. + if let Value::Object(object) = arg + && let Object::Function(Function::Checked { typ, .. }) = object.as_ref() + { + typ.hash(&mut hasher); + } } let key = hasher.finish(); @@ -657,6 +693,100 @@ impl Vm { } } + /// Calls the function a [`Function::Checked`] checks, and checks that + /// what it returns is what the function type it's used as says: right + /// away when the call completes here, or when its frame returns. + #[cold] + fn call_checked( + &mut self, + function: Rc, + typ: StaticType, + args: usize, + ) -> Result { + let StaticType::Function { return_type, .. } = typ else { + unreachable!("a checked function is used as a function type"); + }; + // The function type it's used as may take more than the function + // does, as `Fn(Any) -> Int` would of a function taking an Int. + let arguments = &self.stack[self.stack.len() - args..]; + if !function.conforms_value_args(arguments) { + return Err(VmError::native(format!( + "the function doesn't take the arguments ({})", + arguments + .iter() + .map(|argument| argument.diagnostic_type().to_string()) + .collect::>() + .join(", ") + ))); + } + match self.dispatch_call(Rc::unwrap_or_clone(function), args)? { + Dispatch::FramePushed => { + self.frames.last_mut().expect("a frame was pushed").returns = Some(return_type); + Ok(Dispatch::FramePushed) + } + Dispatch::Completed => { + convert_result(&mut self.stack, &return_type).map_err(VmError::native)?; + Ok(Dispatch::Completed) + } + } + } + + /// Replaces the `size` functions on top of the stack with a + /// [`Function::Overloads`] of them. + #[cold] + fn make_overloads(&mut self, size: usize, span: Span) -> Result<(), VmError> { + let candidates = self + .stack + .split_off(self.stack.len() - size) + .into_iter() + .map(|candidate| match candidate { + Value::Object(object) => match object.as_ref() { + Object::Function(function) => Ok(function.clone()), + other => Err(other.static_type()), + }, + other => Err(other.static_type()), + }) + .collect::, _>>() + .map_err(|found| { + VmError::new(format!("Unable to invoke {found} as a function."), span) + })?; + self.stack + .push(Value::function(Function::Overloads(candidates))); + Ok(()) + } + + /// Calls the first of `candidates`, a [`Function::Overloads`], that + /// accepts the arguments on the stack. + #[cold] + fn call_overloads( + &mut self, + candidates: &[Function], + args: usize, + ) -> Result { + let arguments = &self.stack[self.stack.len() - args..]; + let picked = candidates + .iter() + .find(|candidate| candidate.matches_value_args(arguments)) + .or_else(|| { + candidates + .iter() + .find(|candidate| candidate.conforms_value_args(arguments)) + }) + .cloned(); + let Some(picked) = picked else { + let types = arguments + .iter() + .map(|argument| argument.static_type().to_string()) + .collect::>() + .join(", "); + let name = candidates.first().and_then(Function::name).unwrap_or("?"); + return Err(VmError::native(format!( + "no function called '{name}' found matches the arguments: ({types})" + ))); + }; + self.dispatch_call(picked, args) + } + /// Inner helper that pushes a new call frame. `memo` is `Some` when the /// call originates from a memoized function and the return value should be /// stored in the cache. @@ -695,6 +825,8 @@ impl Vm { prototype: f, upvalues: Vec::new(), }, + Function::Checked { function, typ } => return self.call_checked(function, *typ, args), + Function::Overloads(candidates) => return self.call_overloads(&candidates, args), Function::Memoized { .. } => { // Already unwrapped in dispatch_call; this branch is unreachable. unreachable!( @@ -708,6 +840,7 @@ impl Vm { ip: 0, frame_pointer: self.stack.len() - args, memo, + returns: None, }); for _ in args..num_locals { self.stack.push(Value::unit()); @@ -767,9 +900,17 @@ impl Vm { match &self.stack[self.stack.len() - args - 1] { Value::Object(obj) => match obj.as_ref() { Object::Function(f) => Ok(Some(f.clone())), - Object::OverloadSet { scalars, .. } => { + Object::OverloadSet { + scalars, + vec_candidates, + } => { let start = self.stack.len() - args; - Ok(self.find_scalar_overload(scalars, &self.stack[start..])) + let args = &self.stack[start..]; + // An operator on tuples applies to each element, which + // comes before looking inside containers for a scalar + // overload: that's only before the call fails. + let elementwise = !vec_candidates.is_empty() && vec_axis_len(args).is_some(); + Ok(self.find_scalar_overload(scalars, args, !elementwise)) } other => Err(format!( "Unable to invoke {} as a function.", @@ -832,10 +973,20 @@ impl Vm { /// Walks the `OverloadSet`'s scalar candidates in priority order, /// returning the first whose parameter types accept `args`. Mirrors /// master's `find_overload` body so the hot path on numerics-heavy - /// code stays at the same shape and footprint. - fn find_scalar_overload(&self, scalars: &[ResolvedVar], args: &[Value]) -> Option { + /// code stays at the same shape and footprint. A parameter like + /// `List` isn't checked element by element on that path, so when no + /// candidate accepts the arguments that way, they're checked again + /// element by element before the call fails, if `look_inside`: a struct + /// with a `List` field can be made through `Any` with a list of + /// Ints. + fn find_scalar_overload( + &self, + scalars: &[ResolvedVar], + args: &[Value], + look_inside: bool, + ) -> Option { let frame_pointer = self.frames.last().expect("no frame").frame_pointer; - scalars.iter().find_map(|var| { + let function = |var| { let value = self.resolve_var(var, frame_pointer); let Value::Object(obj) = value else { return None; @@ -843,15 +994,24 @@ impl Vm { let Object::Function(f) = obj.as_ref() else { return None; }; - f.matches_value_args(args).then(|| f.clone()) - }) + Some(f.clone()) + }; + scalars + .iter() + .find_map(|var| function(var).filter(|f| f.matches_value_args(args))) + .or_else(|| { + look_inside.then(|| { + scalars + .iter() + .find_map(|var| function(var).filter(|f| f.conforms_value_args(args))) + })? + }) } - /// Vec dispatch for `Binding::Dynamic` operator calls — the runtime - /// picks the matching scalar overload per element position. Resolves - /// candidates lazily from `vec_candidates` with a last-match cache, so - /// homogeneous tuples (the common case) pay one resolve per outer call - /// instead of N. No upfront `Vec` allocation. + /// Vec dispatch for `Binding::Dynamic` operator calls: the program picks + /// the scalar overload for each element position, trying the one the + /// previous position used first, so a tuple whose elements all have one + /// type picks once. fn dispatch_vec_call_dynamic( &mut self, overload_set: &Rc, @@ -863,11 +1023,6 @@ impl Vm { unreachable!("dispatch_vec_call_dynamic invoked with non-OverloadSet callee"); }; debug_assert!(!vec_candidates.is_empty()); - - let arg_start = self.stack.len() - args; - let arg_values: Vec = self.stack.split_off(arg_start); - self.stack.pop(); // discard the callee slot - let frame_pointer = self.frames.last().expect("no frame").frame_pointer; // Resolve one candidate up front so the error paths can borrow its name @@ -886,136 +1041,62 @@ impl Vm { f.name() }); - let mut elem_args: Vec = Vec::with_capacity(args); - let mut results: Vec = Vec::with_capacity(axis_len); - // Cached last-match Function. Homogeneous tuples reuse this across - // positions; heterogeneous tuples fall through to the candidate walk. let mut last_match: Option = None; - - for i in 0..axis_len { - elem_args.clear(); - for arg in &arg_values { - elem_args.push(vec_element_at(arg, i)); - } - - let scalar: Function = if let Some(f) = &last_match - && f.matches_value_args(&elem_args) + self.call_elementwise(args, axis_len, callee_name, span, |vm, elements, i| { + if let Some(f) = &last_match + && f.matches_value_args(elements) { - f.clone() - } else { - let mut found: Option = None; - for var in vec_candidates { - let value = self.resolve_var(var, frame_pointer); - let Value::Object(obj) = value else { continue }; - let Object::Function(f) = obj.as_ref() else { - continue; - }; - if f.matches_value_args(&elem_args) { - found = Some(f.clone()); - break; - } - } - let Some(f) = found else { - let element_types = elem_args - .iter() - .map(|v| v.static_type().to_string()) - .collect::>() - .join(", "); - let name = callee_name.unwrap_or("?"); - return Err(VmError::new( - format!("no overload of '{name}' accepts element {i}: ({element_types})"), - span, - )); + return Ok(f.clone()); + } + let found = vec_candidates.iter().find_map(|var| { + let Value::Object(obj) = vm.resolve_var(var, frame_pointer) else { + return None; }; - last_match = Some(f.clone()); - f - }; - - let result = self.call_callback(scalar, &elem_args).map_err(|mut e| { - let prefix = match callee_name { - Some(name) => format!("while vectorising '{name}' at index {i}: "), - None => format!("while vectorising at index {i}: "), + let Object::Function(f) = obj.as_ref() else { + return None; }; - e.message = format!("{prefix}{}", e.message); - e.span.get_or_insert(span); - e - })?; - results.push(result); - } - - self.stack - .push(Value::Object(Rc::new(Object::Tuple(results)))); - Ok(()) + f.matches_value_args(elements).then(|| f.clone()) + }); + let Some(f) = found else { + let element_types = elements + .iter() + .map(|v| v.static_type().to_string()) + .collect::>() + .join(", "); + let name = callee_name.unwrap_or("?"); + return Err(VmError::new( + format!("no overload of '{name}' accepts element {i}: ({element_types})"), + span, + )); + }; + last_match = Some(f.clone()); + Ok(f) + }) } - /// Vec dispatch when the scalar set is already resolved — the `CallVec` - /// opcode path. `scalars.len() == 1` is the analyser-pinned fast path - /// that skips the per-element probe entirely. - fn dispatch_vec_call( + /// Calls the function `pick` gives for each element position of the + /// tuple arguments on the stack, and replaces the callee and arguments + /// with the tuple of the results: `(1, 2) + (3, 4)` is `(1 + 3, 2 + 4)`. + fn call_elementwise( &mut self, - scalars: &[Function], args: usize, axis_len: usize, + callee_name: Option<&str>, span: Span, + mut pick: impl FnMut(&mut Self, &[Value], usize) -> Result, ) -> Result<(), VmError> { - let arg_start = self.stack.len() - args; - // Borrow the callee name straight off the resolved scalars — its lifetime - // is tied to the caller-owned slice, not to `self`, so the error paths - // can use it without allocating a `String` on the success path. - let callee_name: Option<&str> = scalars.first().and_then(|f| f.name()); - // Materialise the broadcast arguments up front so the inner // call_callback can hold &mut self without conflicting with stack // borrows. Element values clone Rcs only. - let arg_values: Vec = self.stack.split_off(arg_start); + let arg_values: Vec = self.stack.split_off(self.stack.len() - args); self.stack.pop(); // discard the callee slot - let pinned: Option<&Function> = if scalars.len() == 1 { - Some(&scalars[0]) - } else { - None - }; - let mut elem_args: Vec = Vec::with_capacity(args); let mut results: Vec = Vec::with_capacity(axis_len); - // Cache the last scalar that matched. Homogeneous tuples — the common - // case — all want the same scalar at every position, so probing it - // first short-circuits the candidate walk for positions 1..N. - let mut last_match: Option = None; - for i in 0..axis_len { elem_args.clear(); - for arg in &arg_values { - elem_args.push(vec_element_at(arg, i)); - } - - let scalar: Function = if let Some(f) = pinned { - f.clone() - } else if let Some(idx) = last_match - && scalars[idx].matches_value_args(&elem_args) - { - scalars[idx].clone() - } else { - let Some((idx, found)) = scalars - .iter() - .enumerate() - .find(|(_, f)| f.matches_value_args(&elem_args)) - else { - let element_types = elem_args - .iter() - .map(|v| v.static_type().to_string()) - .collect::>() - .join(", "); - let name = callee_name.unwrap_or("?"); - return Err(VmError::new( - format!("no overload of '{name}' accepts element {i}: ({element_types})"), - span, - )); - }; - last_match = Some(idx); - found.clone() - }; - + elem_args.extend(arg_values.iter().map(|arg| vec_element_at(arg, i))); + let scalar = pick(self, &elem_args, i)?; let result = self.call_callback(scalar, &elem_args).map_err(|mut e| { let prefix = match callee_name { Some(name) => format!("while vectorising '{name}' at index {i}: "), @@ -1211,3 +1292,46 @@ fn bad_range_bound(value: &Value) -> String { } format!("range bounds must be Int, got {}", value.diagnostic_type()) } + +/// Replaces the value on top of `stack` with `Some` of it, made the +/// `target` as [`Value::converted_to`] does, or with `None` when it isn't +/// one. +#[cold] +fn try_cast(stack: &mut [Value], target: &StaticType) { + let value = stack.last_mut().expect("stack underflow"); + *value = match value.converted_to(target) { + Some(converted) => Value::Object(Rc::new(Object::Some(converted))), + None => Value::None, + }; +} + +/// Checks that the value on top of `stack` is a `target`, and makes it one: +/// a function that may be a function `target` gets its calls checked (see +/// [`Value::converted_to`]). +fn convert_top(stack: &mut [Value], target: &StaticType) -> bool { + let value = stack.last_mut().expect("stack underflow"); + if !matches!(target, StaticType::Function { .. }) { + return value.conforms_to(target); + } + match value.converted_to(target) { + Some(converted) => { + *value = converted; + true + } + None => false, + } +} + +/// Checks that the result of a call of a [`Function::Checked`], on top of +/// `stack`, is a `returns`, and makes it one, as [`convert_top`] does: a +/// function it returns may get its own calls checked. +fn convert_result(stack: &mut [Value], returns: &StaticType) -> Result<(), String> { + if convert_top(stack, returns) { + return Ok(()); + } + let returned = stack.last().expect("a call leaves its result"); + Err(format!( + "expected the function to return {returns} but it returned {}", + returned.diagnostic_type() + )) +} diff --git a/tests/compiler/tests/compiler.rs b/tests/compiler/tests/compiler.rs index 600788e6..8969dd50 100644 --- a/tests/compiler/tests/compiler.rs +++ b/tests/compiler/tests/compiler.rs @@ -534,6 +534,62 @@ fn a_proven_cast_needs_no_runtime_check() { ); } +// let x: Any = 1; x as? Int +// +// An optional cast tests the value with a TryCast, which gives an option +// instead of a CheckType guard that fails. +#[test] +fn an_unproven_optional_cast_tries_the_type_at_runtime() { + let ops = compile_with_analysis("let x: Any = 1; x as? Int"); + assert!( + ops.iter() + .any(|op| matches!(op, TryCast(t) if **t == ndc_core::StaticType::Int)), + "expected a TryCast(Int), got: {ops:?}", + ); + assert!( + ops.iter().all(|op| !matches!(op, CheckType(_))), + "an optional cast must not emit CheckType, got: {ops:?}", + ); +} + +// 1 as? Int +// +// A proven optional cast is always `Some`, which a TryCast to `Any` makes. +#[test] +fn a_proven_optional_cast_is_always_some() { + let ops = compile_with_analysis("1 as? Int"); + assert!( + ops.iter() + .any(|op| matches!(op, TryCast(t) if **t == ndc_core::StaticType::Any)), + "expected a TryCast(Any), got: {ops:?}", + ); +} + +// let a: Any = 1; let n: Int = a; +// +// An `Any` stored where an Int is expected is checked before it's stored. +#[test] +fn an_any_stored_in_a_typed_variable_is_checked() { + let ops = compile_with_analysis("let a: Any = 1; let n: Int = a;"); + assert!( + ops.iter() + .any(|op| matches!(op, ExpectType(t) if **t == ndc_core::StaticType::Int)), + "expected an ExpectType(Int) guard, got: {ops:?}", + ); +} + +// let n: Int = 1; let m = n; +// +// A value whose type is known needs no check. +#[test] +fn a_typed_value_stored_in_a_typed_variable_is_not_checked() { + let ops = compile_with_analysis("let n: Int = 1; let m: Int = n;"); + assert!( + ops.iter().all(|op| !matches!(op, ExpectType(_))), + "a value of a known type must not be checked, got: {ops:?}", + ); +} + // fn outer() { // let x = 1; // let f = fn() { x }; @@ -675,3 +731,61 @@ fn temporaries_in_a_function_come_after_its_frames_own_locals() { written.dedup(); assert_eq!(written, [0, 1, 2, 3, 4], "{ops:?}"); } + +/// The functions `function`'s code puts on the stack, in order. +fn nested_functions(function: &CompiledFunction) -> Vec> { + function + .opcodes() + .iter() + .filter_map(|opcode| match opcode { + Constant(index) + | Closure { + constant_idx: index, + .. + } => Some(*index), + _ => None, + }) + .filter_map(|index| match function.body().constant(index) { + ndc_vm::Value::Object(object) => match object.as_ref() { + ndc_vm::Object::Function(ndc_vm::Function::Compiled(function)) => { + Some(Rc::clone(function)) + } + _ => None, + }, + _ => None, + }) + .collect() +} + +/// The compiled top-level code of `input`, with the standard library. +fn compile_program_with_stdlib(input: &str) -> CompiledFunction { + let mut interp = ndc_interpreter::Interpreter::capturing(); + interp.configure(ndc_stdlib::register); + interp + .compile_str_unoptimized(input) + .expect("compile failed") +} + +// A generic function's declaration compiles its own body (constant 0), then +// each instance (constant 1 for `adder` with Ints). A lambda in the body is +// compiled with the instance: its `+` is picked for Ints there, and as the +// program runs in the function's own body, so the two differ. +#[test] +fn a_function_declared_in_an_instance_uses_the_instances_picks() { + let program = + compile_program_with_stdlib("fn adder(x) { let g = fn(y) => y + 1; g(x) } adder(1);"); + let own = nested_functions(&function_constant(&program, 0))[0].clone(); + let instance = nested_functions(&function_constant(&program, 1))[0].clone(); + assert!(!Rc::ptr_eq(&own, &instance)); + assert_ne!(own.opcodes(), instance.opcodes()); +} + +// A function declared in an instance that compiles as it does in the +// function's own body is the same function, so it keeps its identity. +#[test] +fn a_function_declared_in_an_instance_that_compiles_the_same_is_the_same() { + let program = compile_program_with_stdlib("fn make(x) { x + 1; fn f() => 42; f } make(1);"); + let own = nested_functions(&function_constant(&program, 0))[0].clone(); + let instance = nested_functions(&function_constant(&program, 1))[0].clone(); + assert!(Rc::ptr_eq(&own, &instance)); +} diff --git a/tests/functional/programs/001_math/049_numeric_key_identity.ndc b/tests/functional/programs/001_math/049_numeric_key_identity.ndc index 148f3caa..ce7f6606 100644 --- a/tests/functional/programs/001_math/049_numeric_key_identity.ndc +++ b/tests/functional/programs/001_math/049_numeric_key_identity.ndc @@ -1,24 +1,24 @@ // Hashing takes a fast path for values an f64 represents exactly. Equal // values must land in the same bucket whichever mode holds them, and unequal // ones must not be merged by the shortcut. -let dyadic = %{}; +let dyadic: Map = %{}; dyadic[0.5] = "float"; dyadic[1n/2n] = "rational"; assert_eq(dyadic.len(), 1); -let inexact = %{}; +let inexact: Map = %{}; inexact[1n/3n] = "exact"; inexact[1.0/3.0] = "approx"; assert_eq(inexact.len(), 2); -let integers = %{}; +let integers: Map = %{}; integers[5] = "int"; integers[5.0] = "float"; integers[5n] = "number"; assert_eq(integers.len(), 1); // a big integer that is still exactly an f64 agrees with that float -let big = %{}; +let big: Map = %{}; big[2n ^ 100n] = "bigint"; big[Number(2.0 ^ 100)] = "float"; assert_eq(big.len(), 1); diff --git a/tests/functional/programs/004_basic/046_annotated_let_binding.ndc b/tests/functional/programs/004_basic/046_annotated_let_binding.ndc index 9f2901d1..5bc66f51 100644 --- a/tests/functional/programs/004_basic/046_annotated_let_binding.ndc +++ b/tests/functional/programs/004_basic/046_annotated_let_binding.ndc @@ -12,7 +12,6 @@ let number_value: Number = 3n; let string_value: String = "hello"; let option_value: Option = Some(3); -let sequence_value: Sequence = [1, 2, 3]; let list_value: List = [1, 2, 3]; let iterator_value: Iterator = 1..10; let min_heap_value: MinHeap = MinHeap(); diff --git a/tests/functional/programs/004_basic/060_an_index_assignment_keeps_the_element_type.ndc b/tests/functional/programs/004_basic/060_an_index_assignment_keeps_the_element_type.ndc new file mode 100644 index 00000000..85d94186 --- /dev/null +++ b/tests/functional/programs/004_basic/060_an_index_assignment_keeps_the_element_type.ndc @@ -0,0 +1,5 @@ +// A container keeps the element type it was inferred with: storing a String +// in a List is an error rather than widening the list. +// expect-error: mismatched types: found String but expected Int +let values = [1]; +values[0] = "two"; diff --git a/tests/functional/programs/004_basic/060_inferred_index_assignment_widens.ndc b/tests/functional/programs/004_basic/060_inferred_index_assignment_widens.ndc deleted file mode 100644 index 121916e2..00000000 --- a/tests/functional/programs/004_basic/060_inferred_index_assignment_widens.ndc +++ /dev/null @@ -1,11 +0,0 @@ -let values = [1]; -values[0] = "two"; -// The concatenation requires values[0] to have widened from Int to Any so -// that operator dispatch is deferred until runtime. -assert_eq(values[0] ++ "!", "two!"); - -let mapped = %{"one": 1}; -mapped["two"] = "two"; -// Map value inference must widen for the same reason while preserving the -// String key type. -assert_eq(mapped["two"] ++ "!", "two!"); diff --git a/tests/functional/programs/004_basic/065_incompatible_specialized_op_assignment.ndc b/tests/functional/programs/004_basic/065_incompatible_specialized_op_assignment.ndc index cd551592..45ba0b26 100644 --- a/tests/functional/programs/004_basic/065_incompatible_specialized_op_assignment.ndc +++ b/tests/functional/programs/004_basic/065_incompatible_specialized_op_assignment.ndc @@ -1,3 +1,3 @@ -// expect-error: mismatched types: found List but expected List +// expect-error: No function called '++' found that matches the arguments 'List, List' let values = [1]; values ++= ["two"]; diff --git a/tests/functional/programs/004_basic/066_op_assignment_any_rhs.ndc b/tests/functional/programs/004_basic/066_op_assignment_any_rhs.ndc index 2137c005..e521249d 100644 --- a/tests/functional/programs/004_basic/066_op_assignment_any_rhs.ndc +++ b/tests/functional/programs/004_basic/066_op_assignment_any_rhs.ndc @@ -1,9 +1,8 @@ -// A specialized `op=` copies values into the left operand without inspecting -// them, so an `Any` right operand is rejected rather than silently accepted: -// nothing downstream would check the elements. Cast the operand to state what -// it holds. -// expect-error: mismatched types: found Any but expected List -fn opaque(x) => x; +// `a op= b` with an `Any` right operand is `a = a op b`, checked as the +// program runs: nothing says the operand fits, so `++` makes a new list, +// which is checked where it's written back. +// expect-output: [1,2,3] +fn opaque(x) -> Any => x; let values = [1]; values ++= opaque([2, 3]); print(values) diff --git a/tests/functional/programs/004_basic/067_op_assignment_supertype_rhs.ndc b/tests/functional/programs/004_basic/067_op_assignment_supertype_rhs.ndc index 69667af4..964ace69 100644 --- a/tests/functional/programs/004_basic/067_op_assignment_supertype_rhs.ndc +++ b/tests/functional/programs/004_basic/067_op_assignment_supertype_rhs.ndc @@ -1,8 +1,8 @@ // A specialized `op=` mutates the left operand in place and keeps its type, so // a right operand whose element type is merely a *supertype* is not enough: // appending a Float here would leave a List holding a non-Int. -// expect-error: mismatched types: found List but expected List +// expect-error: No function called '++' found that matches the arguments 'List, List' let values: List = [1]; -let rhs: List = [0.5]; +let rhs: List = []; values ++= rhs; print(values) diff --git a/tests/functional/programs/004_basic/070_a_variable_keeps_its_type.ndc b/tests/functional/programs/004_basic/070_a_variable_keeps_its_type.ndc new file mode 100644 index 00000000..d69fddec --- /dev/null +++ b/tests/functional/programs/004_basic/070_a_variable_keeps_its_type.ndc @@ -0,0 +1,6 @@ +// A variable keeps the type its first value gave it, so a call resolved +// against that type, like the `+` in `f`, stays right. +// expect-error: mismatched types: found Float but expected Int +let x = "a".len(); +fn f() { x + 1 } +x = 2.5; diff --git a/tests/functional/programs/004_basic/071_some_has_the_type_of_its_value.ndc b/tests/functional/programs/004_basic/071_some_has_the_type_of_its_value.ndc new file mode 100644 index 00000000..389761e8 --- /dev/null +++ b/tests/functional/programs/004_basic/071_some_has_the_type_of_its_value.ndc @@ -0,0 +1,4 @@ +// `Some(1)` is an Option, so the variable can't later hold an Option. +// expect-error: mismatched types: found Option but expected Option +let o = Some(1); +o = Some("a"); diff --git a/tests/functional/programs/004_basic/072_unwrap_returns_the_type_inside_the_option.ndc b/tests/functional/programs/004_basic/072_unwrap_returns_the_type_inside_the_option.ndc new file mode 100644 index 00000000..c7a124a3 --- /dev/null +++ b/tests/functional/programs/004_basic/072_unwrap_returns_the_type_inside_the_option.ndc @@ -0,0 +1,9 @@ +// `none()` leaves the option's type open until `Some(2)` settles it, and +// `unwrap` returns the type inside: `xs` becomes a List. +let o = none(); +o = Some(2); +let xs = []; +xs.push(o.unwrap()); +xs.push(o.unwrap_or(0) + 1); +xs.push("a"); +// expect-error: No function called 'push' found that matches the arguments 'List, String' diff --git a/tests/functional/programs/004_basic/073_unwrap_or_takes_a_default_of_the_options_type.ndc b/tests/functional/programs/004_basic/073_unwrap_or_takes_a_default_of_the_options_type.ndc new file mode 100644 index 00000000..c32f5c06 --- /dev/null +++ b/tests/functional/programs/004_basic/073_unwrap_or_takes_a_default_of_the_options_type.ndc @@ -0,0 +1,4 @@ +// `unwrap_or` returns the type inside the option, so its default must have that type too. +// expect-error: No function called 'unwrap_or' found that matches the arguments 'Option, String' +let o = Some(1); +print(o.unwrap_or("none")); diff --git a/tests/functional/programs/004_basic/074_an_index_write_of_a_value_not_yet_known_keeps_the_element_type.ndc b/tests/functional/programs/004_basic/074_an_index_write_of_a_value_not_yet_known_keeps_the_element_type.ndc new file mode 100644 index 00000000..1d05f5cf --- /dev/null +++ b/tests/functional/programs/004_basic/074_an_index_write_of_a_value_not_yet_known_keeps_the_element_type.ndc @@ -0,0 +1,5 @@ +// The empty list's elements aren't known when it's written, but it's still +// not an Int, which is what the list holds. +// expect-error: mismatched types: found List but expected Int +let values = [1]; +values[0] = []; diff --git a/tests/functional/programs/004_basic/075_op_assignment_takes_one_type_for_a_type_parameter.ndc b/tests/functional/programs/004_basic/075_op_assignment_takes_one_type_for_a_type_parameter.ndc new file mode 100644 index 00000000..82c4da8f --- /dev/null +++ b/tests/functional/programs/004_basic/075_op_assignment_takes_one_type_for_a_type_parameter.ndc @@ -0,0 +1,6 @@ +// `max` of two values takes two of one type, in `a max= b` as in +// `max(a, b)`: a Float and an Int aren't one type. +// expect-error: No function called 'max' found that matches the arguments 'Float, Int' +let best = -Inf; +let x: Int = 3; +best max= x; diff --git a/tests/functional/programs/004_basic/076_op_assignment_checks_what_an_any_operand_makes.ndc b/tests/functional/programs/004_basic/076_op_assignment_checks_what_an_any_operand_makes.ndc new file mode 100644 index 00000000..1ccc82a9 --- /dev/null +++ b/tests/functional/programs/004_basic/076_op_assignment_checks_what_an_any_operand_makes.ndc @@ -0,0 +1,6 @@ +// What `a op= b` makes from an `Any` operand is checked where it's written +// back, so a value that doesn't fit never reaches the target. +// expect-error: expected List but found List +fn opaque(x) -> Any => x; +let values = [1]; +values ++= opaque([0.5]); diff --git a/tests/functional/programs/004_basic/077_op_assignment_checks_an_any_target_before_the_call.ndc b/tests/functional/programs/004_basic/077_op_assignment_checks_an_any_target_before_the_call.ndc new file mode 100644 index 00000000..dd0609b7 --- /dev/null +++ b/tests/functional/programs/004_basic/077_op_assignment_checks_an_any_target_before_the_call.ndc @@ -0,0 +1,7 @@ +// The target of `a op= b` is passed to the operator, so a target of type +// `Any` is checked against what the operator takes before it runs. +// expect-error: no function called 'choose' found matches the arguments: (String, Int) +fn choose(a: Int, b: Int) => a; +let x: Any = "wrong"; +x choose= 1; +print(x); diff --git a/tests/functional/programs/004_basic/078_an_endless_while_loop_has_no_value.ndc b/tests/functional/programs/004_basic/078_an_endless_while_loop_has_no_value.ndc new file mode 100644 index 00000000..08d76f2e --- /dev/null +++ b/tests/functional/programs/004_basic/078_an_endless_while_loop_has_no_value.ndc @@ -0,0 +1,12 @@ +// A `while true` that no `break` leaves never ends, so a function ending in +// one returns what its `return`s do: a list of pairs, whose sum is typed. +// expect-output: 4 +fn grow(pairs) { + while true { + if pairs.len > 2 { + return pairs; + } + pairs.push((1, 1)); + } +} +print([hi - lo + 1 for (lo, hi) in grow([(1, 2)])].sum); diff --git a/tests/functional/programs/004_basic/079_a_while_true_loop_left_by_break_is_unit.ndc b/tests/functional/programs/004_basic/079_a_while_true_loop_left_by_break_is_unit.ndc new file mode 100644 index 00000000..2211cd3e --- /dev/null +++ b/tests/functional/programs/004_basic/079_a_while_true_loop_left_by_break_is_unit.ndc @@ -0,0 +1,10 @@ +// A `while true` that a `break` leaves ends normally, with the unit value. +// expect-output: true +let i = 0; +let value = while true { + i += 1; + if i > 3 { + break; + } +}; +print(value == ()); diff --git a/tests/functional/programs/005_functions/042_typed_container_param_not_dispatchable.ndc b/tests/functional/programs/005_functions/042_typed_container_param_not_dispatchable.ndc index 208d59ca..db25662f 100644 --- a/tests/functional/programs/005_functions/042_typed_container_param_not_dispatchable.ndc +++ b/tests/functional/programs/005_functions/042_typed_container_param_not_dispatchable.ndc @@ -1,6 +1,6 @@ -// Neither type is a subtype of the other, so the call is rejected at compile -// time. A cast is how you tell the analyser what the value actually holds. -// expect-error: No function called 'consume' found that matches the arguments 'Sequence' +// A List isn't a List, so the call is rejected at compile time. +// A cast is how you tell the analyser what the value actually holds. +// expect-error: No function called 'consume' found that matches the arguments 'List' fn consume(xs: List) => xs; -let xs: Sequence = [1]; +let xs = ["a".len()]; consume(xs) diff --git a/tests/functional/programs/005_functions/044_cast_suggested_when_no_overload_matches.ndc b/tests/functional/programs/005_functions/044_cast_suggested_when_no_overload_matches.ndc index 264e406a..ae52b7ca 100644 --- a/tests/functional/programs/005_functions/044_cast_suggested_when_no_overload_matches.ndc +++ b/tests/functional/programs/005_functions/044_cast_suggested_when_no_overload_matches.ndc @@ -1,6 +1,5 @@ // The call would resolve if the argument type were narrower, so the error // carries help pointing at a cast rather than leaving the user at a dead end. // expect-error: Cast to say what the value holds -let line = "a b c"; -line = line.split(" "); -print(line.remove(0)) +let values: List = [1, 2]; +print(values.sum()) diff --git a/tests/functional/programs/005_functions/059_a_function_returns_what_its_captured_list_holds.ndc b/tests/functional/programs/005_functions/059_a_function_returns_what_its_captured_list_holds.ndc new file mode 100644 index 00000000..55d76bd6 --- /dev/null +++ b/tests/functional/programs/005_functions/059_a_function_returns_what_its_captured_list_holds.ndc @@ -0,0 +1,9 @@ +// `get` returns the captured `xs`, so once `xs` is settled as a List, +// a call of `get` is a List as well, and picks the same overload. +fn f(x: List) { "int" } +fn f(x: List) { "any" } +let xs = []; +fn get() { xs } +xs ++= [1]; +print(f(xs), f(get())); +// expect-output: int int diff --git a/tests/functional/programs/005_functions/060_a_call_picked_at_runtime_returns_any.ndc b/tests/functional/programs/005_functions/060_a_call_picked_at_runtime_returns_any.ndc new file mode 100644 index 00000000..87bcc382 --- /dev/null +++ b/tests/functional/programs/005_functions/060_a_call_picked_at_runtime_returns_any.ndc @@ -0,0 +1,5 @@ +// `xs` could be anything, so `first` is picked when the program runs and +// its `T` isn't known: the call is `Any`, and calling its result is fine. +fn invoke(xs) { xs.first()() } +print(invoke([fn() => 5])); +// expect-output: 5 diff --git a/tests/functional/programs/005_functions/061_function_types_in_annotations.ndc b/tests/functional/programs/005_functions/061_function_types_in_annotations.ndc new file mode 100644 index 00000000..06404d26 --- /dev/null +++ b/tests/functional/programs/005_functions/061_function_types_in_annotations.ndc @@ -0,0 +1,14 @@ +// `Fn(A, B) -> C` is the type of a function taking an A and a B and +// returning a C. Without `-> …` it returns `()`. +let twice: Fn(Int) -> Int = fn(x: Int) => x * 2; +fn apply(f: Fn(Int) -> Int, x: Int) -> Int { f(x) } +fn make_adder(n: Int) -> Fn(Int) -> Int { fn(x: Int) => x + n } +let log: Fn(String) = fn(s: String) { print(s); }; +log("start"); +print(apply(twice, 4)); +print(apply(make_adder(10), 1)); +print(apply(fn(x) => x - 1, 1)); +// expect-output: start +// expect-output: 8 +// expect-output: 11 +// expect-output: 0 diff --git a/tests/functional/programs/005_functions/062_a_function_argument_must_take_what_the_parameter_passes.ndc b/tests/functional/programs/005_functions/062_a_function_argument_must_take_what_the_parameter_passes.ndc new file mode 100644 index 00000000..7133adbe --- /dev/null +++ b/tests/functional/programs/005_functions/062_a_function_argument_must_take_what_the_parameter_passes.ndc @@ -0,0 +1,5 @@ +// `apply` passes its function an Int, so a function taking a String can't +// be given to it. +// expect-error: No function called 'apply' found that matches the arguments 'Fn(String) -> Int, Int' +fn apply(f: Fn(Int) -> Int, x: Int) -> Int { f(x) } +apply(fn(s: String) => 1, 4); diff --git a/tests/functional/programs/005_functions/063_a_lambda_takes_the_expected_function_type.ndc b/tests/functional/programs/005_functions/063_a_lambda_takes_the_expected_function_type.ndc new file mode 100644 index 00000000..de241ce6 --- /dev/null +++ b/tests/functional/programs/005_functions/063_a_lambda_takes_the_expected_function_type.ndc @@ -0,0 +1,14 @@ +// A lambda written where a function type is expected takes its parameter +// types from it: in an annotated variable, an assignment, or an argument +// to a function value. These lambdas need no annotations. +let double: Fn(Int) -> Int = fn(x) => x * 2; +let shout: Fn(String) -> String = fn(s) => s; +shout = fn(s) => s ++ "!"; +let apply = fn(f: Fn(Int) -> Int, x: Int) -> Int { f(x) }; +let calls = [apply]; +print(double(4)); +print(shout("hi")); +print(calls[0](fn(x) => x + 1, 1)); +// expect-output: 8 +// expect-output: hi! +// expect-output: 2 diff --git a/tests/functional/programs/005_functions/064_a_waiting_call_is_picked_as_soon_as_its_arguments_are_known.ndc b/tests/functional/programs/005_functions/064_a_waiting_call_is_picked_as_soon_as_its_arguments_are_known.ndc new file mode 100644 index 00000000..d482215d --- /dev/null +++ b/tests/functional/programs/005_functions/064_a_waiting_call_is_picked_as_soon_as_its_arguments_are_known.ndc @@ -0,0 +1,9 @@ +// `xs[0] + 1` waits until the push settles what `xs` holds, and is picked +// then, so `y` is a Float before `y += 1` is checked. +// expect-output: 4.5 +let xs = []; +let g = fn() => xs[0] + 1; +xs.push(2.5); +let y = g(); +y += 1; +print(y); diff --git a/tests/functional/programs/005_functions/065_a_waiting_call_must_return_what_was_expected_of_it.ndc b/tests/functional/programs/005_functions/065_a_waiting_call_must_return_what_was_expected_of_it.ndc new file mode 100644 index 00000000..be8ab404 --- /dev/null +++ b/tests/functional/programs/005_functions/065_a_waiting_call_must_return_what_was_expected_of_it.ndc @@ -0,0 +1,5 @@ +// `x + 1` waits for `x`, and `r`'s annotation decides its type meanwhile. +// Once the call picks `+(Float, Int)`, its Float result must fit that. +// expect-error: mismatched types: found Float but expected String +let g = fn(x) { let r: String = x + 1; r }; +g(2.5); diff --git a/tests/functional/programs/005_functions/066_a_lambda_in_a_variable_has_one_type.ndc b/tests/functional/programs/005_functions/066_a_lambda_in_a_variable_has_one_type.ndc new file mode 100644 index 00000000..12138a88 --- /dev/null +++ b/tests/functional/programs/005_functions/066_a_lambda_in_a_variable_has_one_type.ndc @@ -0,0 +1,6 @@ +// A lambda's parameters are settled by its uses like any variable's type, +// so it can't be called with an Int and then with a Float. +// expect-error: doesn't accept the arguments 'Float' +let twice = fn(x) => x * 2; +print(twice("abc".len())); +print(twice(2.5)); diff --git a/tests/functional/programs/005_functions/067_an_overloaded_function_value_takes_the_overload_its_use_expects.ndc b/tests/functional/programs/005_functions/067_an_overloaded_function_value_takes_the_overload_its_use_expects.ndc new file mode 100644 index 00000000..6e2f8a48 --- /dev/null +++ b/tests/functional/programs/005_functions/067_an_overloaded_function_value_takes_the_overload_its_use_expects.ndc @@ -0,0 +1,22 @@ +// An overloaded function's name used as a value is the overload the type +// expected there picks: `map` passes each element. +// expect-output: [1.5,2.5] +// expect-output: [1,2] +// expect-output: [["a","b"]] +// expect-output: ["string"] ["int"] +// expect-output: ["a","b"] +// expect-output: inner +fn f(x: Int) { "int" } +fn f(x: String) { "string" } +print([-1.5, 2.5].map(abs)); +print([-1, 2].map(abs)); +print(["a b"].map(split)); +print(["a"].map(f), [1].map(f)); +let g = split; +print(g("a b")); +fn pick(x: Int) { 1 } +{ + fn pick(x: Any) { "inner" } + let h: Fn(Any) -> String = pick; + print(h(1)); +} diff --git a/tests/functional/programs/005_functions/068_an_overloaded_function_value_nothing_picks_is_an_error.ndc b/tests/functional/programs/005_functions/068_an_overloaded_function_value_nothing_picks_is_an_error.ndc new file mode 100644 index 00000000..d9911013 --- /dev/null +++ b/tests/functional/programs/005_functions/068_an_overloaded_function_value_nothing_picks_is_an_error.ndc @@ -0,0 +1,4 @@ +// Nothing says which `split` is meant, so the program has to say it with a +// function type. +// expect-error: 'split' has several overloads, and nothing here says which one is meant +let g = split; diff --git a/tests/functional/programs/005_functions/069_function_values_in_branches_and_lists_take_the_type_expected_of_them.ndc b/tests/functional/programs/005_functions/069_function_values_in_branches_and_lists_take_the_type_expected_of_them.ndc new file mode 100644 index 00000000..b04e7b2a --- /dev/null +++ b/tests/functional/programs/005_functions/069_function_values_in_branches_and_lists_take_the_type_expected_of_them.ndc @@ -0,0 +1,11 @@ +// The type expected of an `if` or a list is expected of each function value +// in it, which picks its overload. +// expect-output: 1 +// expect-output: 2 +// expect-output: 3 +let f: Fn(Int) -> Int = if true { abs } else { floor }; +print(f(-1)); +let fs: List Int> = [abs, floor]; +print(fs[0](-2)); +let x = if true { abs } else { abs }; +print(x(-3)); diff --git a/tests/functional/programs/005_functions/070_a_function_value_picks_the_most_specific_overload_like_a_call.ndc b/tests/functional/programs/005_functions/070_a_function_value_picks_the_most_specific_overload_like_a_call.ndc new file mode 100644 index 00000000..0f1d379e --- /dev/null +++ b/tests/functional/programs/005_functions/070_a_function_value_picks_the_most_specific_overload_like_a_call.ndc @@ -0,0 +1,7 @@ +// With the parameter types known, a function value is the overload a call +// with arguments of those types would pick: `f(Int)` over `f(Any)`. +// expect-output: int ["int"] int +fn f(x: Int) { "int" } +fn f(x) { "any" } +let g: Fn(Int) -> String = f; +print(g(1), [1].map(f), f(1)); diff --git a/tests/functional/programs/005_functions/071_a_generic_function_takes_each_type_its_body_can_work_with.ndc b/tests/functional/programs/005_functions/071_a_generic_function_takes_each_type_its_body_can_work_with.ndc new file mode 100644 index 00000000..796f0f53 --- /dev/null +++ b/tests/functional/programs/005_functions/071_a_generic_function_takes_each_type_its_body_can_work_with.ndc @@ -0,0 +1,10 @@ +// A function declared in the program takes any argument its body can work +// with, and each call gets the type the body makes of its own arguments: +// `inc(1)` is an Int and `inc(2.5)` a Float. +// expect-output: 2 3.5 Int Float +fn inc(x) => x + 1; +let a = inc(1); +let b = inc(2.5); +let c: Int = a; +let d: Float = b; +print(a, b, "Int", "Float"); diff --git a/tests/functional/programs/005_functions/072_a_generic_function_called_with_what_its_body_cannot_use_is_an_error.ndc b/tests/functional/programs/005_functions/072_a_generic_function_called_with_what_its_body_cannot_use_is_an_error.ndc new file mode 100644 index 00000000..9d9389dc --- /dev/null +++ b/tests/functional/programs/005_functions/072_a_generic_function_called_with_what_its_body_cannot_use_is_an_error.ndc @@ -0,0 +1,7 @@ +// A call of a generic function is checked against what its body does with +// the arguments. The error is reported at the call, with a note on the line +// in the body that can't take them. +// expect-error: No function called '+' found that matches the arguments 'String, Int' +fn inc(x) => x + 1; +fn shout(s) => inc(s) ++ "!"; +shout("a"); diff --git a/tests/functional/programs/005_functions/073_a_recursive_generic_function_returns_what_it_is_called_with.ndc b/tests/functional/programs/005_functions/073_a_recursive_generic_function_returns_what_it_is_called_with.ndc new file mode 100644 index 00000000..aed68b58 --- /dev/null +++ b/tests/functional/programs/005_functions/073_a_recursive_generic_function_returns_what_it_is_called_with.ndc @@ -0,0 +1,7 @@ +// A recursive call has the types of the call it's in, so `fib` returns an +// Int for an Int and a Float for a Float, as the annotations check. +// expect-output: 55 55.0 +fn fib(n) => if n < 2 { n } else { fib(n - 1) + fib(n - 2) }; +let i: Int = fib(10); +let f: Float = fib(10.0); +print(i, f); diff --git a/tests/functional/programs/005_functions/074_a_generic_function_passes_its_argument_to_a_function_argument.ndc b/tests/functional/programs/005_functions/074_a_generic_function_passes_its_argument_to_a_function_argument.ndc new file mode 100644 index 00000000..19ff8675 --- /dev/null +++ b/tests/functional/programs/005_functions/074_a_generic_function_passes_its_argument_to_a_function_argument.ndc @@ -0,0 +1,7 @@ +// A parameter called as a function takes what the call passes it, so +// `apply` takes an overloaded function too: `abs(Int)` is the one that +// takes `-1`. +// expect-output: 1 hi! [2,3] +fn apply(f, x) => f(x); +fn inc(x) => x + 1; +print(apply(abs, -1), apply(fn(s) => s ++ "!", "hi"), [1, 2].map(inc)); diff --git a/tests/functional/programs/005_functions/075_a_generic_function_appends_in_place.ndc b/tests/functional/programs/005_functions/075_a_generic_function_appends_in_place.ndc new file mode 100644 index 00000000..37740b00 --- /dev/null +++ b/tests/functional/programs/005_functions/075_a_generic_function_appends_in_place.ndc @@ -0,0 +1,9 @@ +// `xs ++= ys` in a generic function appends to the caller's list, as it +// does where the list's type is known. +// expect-output: [1,2,3] +fn append(xs, ys) { + xs ++= ys; +} +let list = [1]; +append(list, [2, 3]); +print(list); diff --git a/tests/functional/programs/005_functions/076_a_function_declared_in_a_function_has_one_type.ndc b/tests/functional/programs/005_functions/076_a_function_declared_in_a_function_has_one_type.ndc new file mode 100644 index 00000000..7a9eade5 --- /dev/null +++ b/tests/functional/programs/005_functions/076_a_function_declared_in_a_function_has_one_type.ndc @@ -0,0 +1,9 @@ +// Only a function declared in the program is generic. One declared in a +// function or a block has one type, like a lambda in a variable: its first +// call decides it. +// expect-error: No function called 'same' found that matches the arguments 'String' +fn outer() { + fn same(v) => v; + same(1); + same("a"); +} diff --git a/tests/functional/programs/005_functions/077_overloads_a_call_could_fit_must_not_tie.ndc b/tests/functional/programs/005_functions/077_overloads_a_call_could_fit_must_not_tie.ndc new file mode 100644 index 00000000..69855b6a --- /dev/null +++ b/tests/functional/programs/005_functions/077_overloads_a_call_could_fit_must_not_tie.ndc @@ -0,0 +1,5 @@ +// `f(1, 2)` fits both overloads, and neither is more specific, so nothing +// would say which one it means. +// expect-error: A call could fit both f(_, Int) and f(Int, _) +fn f(x: Int, y) { "first" } +fn f(x, y: Int) { "second" } diff --git a/tests/functional/programs/005_functions/078_an_unannotated_parameter_is_a_fallback_next_to_exact_overloads.ndc b/tests/functional/programs/005_functions/078_an_unannotated_parameter_is_a_fallback_next_to_exact_overloads.ndc new file mode 100644 index 00000000..5e393802 --- /dev/null +++ b/tests/functional/programs/005_functions/078_an_unannotated_parameter_is_a_fallback_next_to_exact_overloads.ndc @@ -0,0 +1,8 @@ +// A call uses the most specific overload that fits it, so `show(x)` takes +// what `show(Int)` doesn't, like `show(x: Any)` would, but with a typed body. +// expect-output: int other other +fn show(x: Int) { "int" } +fn show(x) { "other" } +fn show_any(x: Int) { "int" } +fn show_any(x: Any) { "other" } +print(show(1), show("a"), show_any([1])); diff --git a/tests/functional/programs/005_functions/079_a_captured_variable_has_one_type_in_a_generic_function.ndc b/tests/functional/programs/005_functions/079_a_captured_variable_has_one_type_in_a_generic_function.ndc new file mode 100644 index 00000000..2eaceed7 --- /dev/null +++ b/tests/functional/programs/005_functions/079_a_captured_variable_has_one_type_in_a_generic_function.ndc @@ -0,0 +1,11 @@ +// A variable declared outside a function has one type, though the function +// is generic: the first call decides what `seen` holds, and so what +// `remember` takes. +// expect-error: No function called 'remember' found that matches the arguments 'String' +let seen = []; +fn remember(n) { + seen.push(n); + n +} +remember(1); +remember("a"); diff --git a/tests/functional/programs/005_functions/080_a_generic_body_calls_the_most_specific_overload.ndc b/tests/functional/programs/005_functions/080_a_generic_body_calls_the_most_specific_overload.ndc new file mode 100644 index 00000000..110d752a --- /dev/null +++ b/tests/functional/programs/005_functions/080_a_generic_body_calls_the_most_specific_overload.ndc @@ -0,0 +1,8 @@ +// A generic function's body is compiled once, so a call on its parameter +// picks the overload as the program runs: the most specific one that takes +// the argument, `show(Int)` for an Int. +// expect-output: int other +fn show(x: Int) { "int" } +fn show(x) { "other" } +fn via(v) => show(v); +print(via(1), via("a")); diff --git a/tests/functional/programs/005_functions/081_a_parameter_the_body_types_is_checked_as_the_program_runs.ndc b/tests/functional/programs/005_functions/081_a_parameter_the_body_types_is_checked_as_the_program_runs.ndc new file mode 100644 index 00000000..cebae648 --- /dev/null +++ b/tests/functional/programs/005_functions/081_a_parameter_the_body_types_is_checked_as_the_program_runs.ndc @@ -0,0 +1,8 @@ +// `pick`'s body makes `x` an Int, so a call decided as the program runs +// tells it apart from the `Any` fallback, as a call with known types does. +// expect-output: 1 fallback 1 +fn need_int(x: Int) => x; +fn pick(x) => need_int(x); +fn pick(x: Any) => "fallback"; +fn via(x: Any) => pick(x); +print(via(1), via("a"), pick(1)); diff --git a/tests/functional/programs/005_functions/082_a_recursive_call_the_body_cannot_decide_is_checked_once.ndc b/tests/functional/programs/005_functions/082_a_recursive_call_the_body_cannot_decide_is_checked_once.ndc new file mode 100644 index 00000000..b716b60e --- /dev/null +++ b/tests/functional/programs/005_functions/082_a_recursive_call_the_body_cannot_decide_is_checked_once.ndc @@ -0,0 +1,6 @@ +// The recursive call could mean either `f`, so it waits until `f` is +// called. Checking it with the call's types doesn't check `f` again. +// expect-output: a 1 +fn f(x: Int) => x; +fn f(x) => if true { x } else { f(x) }; +print(f("a"), f(1)); diff --git a/tests/functional/programs/005_functions/083_each_instance_calls_the_instances_its_types_need.ndc b/tests/functional/programs/005_functions/083_each_instance_calls_the_instances_its_types_need.ndc new file mode 100644 index 00000000..a8b3886a --- /dev/null +++ b/tests/functional/programs/005_functions/083_each_instance_calls_the_instances_its_types_need.ndc @@ -0,0 +1,10 @@ +// A generic function's body is compiled for each type it's called with, and +// the generic functions it calls are the ones for the types they get there: +// `perlin(0.5)` calls `fade` for Floats. +// expect-output: 0.25 1 0.25 +fn fade(t) => t * t; +fn perlin(x) { + let xf = x - int(x); + fade(xf) +} +print(perlin(1.5), fade(1), fade(0.5)); diff --git a/tests/functional/programs/005_functions/084_a_generic_function_used_as_a_value_is_the_function_itself.ndc b/tests/functional/programs/005_functions/084_a_generic_function_used_as_a_value_is_the_function_itself.ndc new file mode 100644 index 00000000..03130c72 --- /dev/null +++ b/tests/functional/programs/005_functions/084_a_generic_function_used_as_a_value_is_the_function_itself.ndc @@ -0,0 +1,7 @@ +// A generic function's name used as a value is the function, whatever type +// it's used at, so two values of it are equal. +// expect-output: true 2 2.5 +fn inc(x) => x + 1; +let a: Fn(Int) -> Int = inc; +let b: Fn(Float) -> Float = inc; +print(a == b, a(1), b(1.5)); diff --git a/tests/functional/programs/005_functions/085_a_function_declared_in_a_generic_body_keeps_its_identity.ndc b/tests/functional/programs/005_functions/085_a_function_declared_in_a_generic_body_keeps_its_identity.ndc new file mode 100644 index 00000000..c787145e --- /dev/null +++ b/tests/functional/programs/005_functions/085_a_function_declared_in_a_generic_body_keeps_its_identity.ndc @@ -0,0 +1,10 @@ +// A function declared in a generic function's body is compiled again for +// each type the generic function is called with. Where it compiles the same, +// it's the same function, so `make(1)` and `make(1.5)` return equal values. +// expect-output: true 42 +fn make(x) { + x + 1; + fn f() => 42; + f +} +print(make(1) == make(1.5), make(1)()); diff --git a/tests/functional/programs/005_functions/086_runtime_dispatch_checks_elements_when_nothing_else_fits.ndc b/tests/functional/programs/005_functions/086_runtime_dispatch_checks_elements_when_nothing_else_fits.ndc new file mode 100644 index 00000000..aafe0268 --- /dev/null +++ b/tests/functional/programs/005_functions/086_runtime_dispatch_checks_elements_when_nothing_else_fits.ndc @@ -0,0 +1,8 @@ +// Neither overload takes a list without looking at what it holds, so the +// program checks the elements to pick one. +// expect-output: ints strings +fn process(xs: List) => "ints"; +fn process(xs: List) => "strings"; +let a: Any = [1]; +let b: Any = ["s"]; +print(process(a), process(b)); diff --git a/tests/functional/programs/005_functions/087_a_literal_in_a_generic_body_is_typed_by_the_body.ndc b/tests/functional/programs/005_functions/087_a_literal_in_a_generic_body_is_typed_by_the_body.ndc new file mode 100644 index 00000000..d9eb286a --- /dev/null +++ b/tests/functional/programs/005_functions/087_a_literal_in_a_generic_body_is_typed_by_the_body.ndc @@ -0,0 +1,7 @@ +// Nothing in `fact`'s body needs the `1` to be anything else, so it's an +// Int, which `fact` returns. Called with a Float, `n * fact(n - 1)` is a +// Float, and the two branches can't both be what `fact` returns: write +// `1.0` for a `fact` of Floats. +// expect-error: mismatched types: found Float but expected Int +fn fact(n) => if n < 2 { 1 } else { n * fact(n - 1) }; +print(fact(5.0)); diff --git a/tests/functional/programs/005_functions/088_a_list_of_a_parameter_and_a_literal_is_typed_by_the_body.ndc b/tests/functional/programs/005_functions/088_a_list_of_a_parameter_and_a_literal_is_typed_by_the_body.ndc new file mode 100644 index 00000000..1a9b56e1 --- /dev/null +++ b/tests/functional/programs/005_functions/088_a_list_of_a_parameter_and_a_literal_is_typed_by_the_body.ndc @@ -0,0 +1,5 @@ +// The `1` in `[x, 1]` is an Int, so the list holds Ints and `x` is one: +// `g` doesn't take a Float. +// expect-error: No function called 'g' found that matches the arguments 'Float' +fn g(x) => [x, 1]; +print(g(2), g(2.5)); diff --git a/tests/functional/programs/005_functions/090_each_call_of_a_generic_function_makes_a_list_of_what_it_is_given.ndc b/tests/functional/programs/005_functions/090_each_call_of_a_generic_function_makes_a_list_of_what_it_is_given.ndc new file mode 100644 index 00000000..1143122b --- /dev/null +++ b/tests/functional/programs/005_functions/090_each_call_of_a_generic_function_makes_a_list_of_what_it_is_given.ndc @@ -0,0 +1,8 @@ +// Each call of `wrap` makes a list of what it's given, so the two lists +// hold different types and the outer list holds both. +// expect-output: [[1],["s"]] [[1,3],[2]] +fn wrap(x) => [x]; +let xs = [wrap(1), wrap("s")]; +let ys = [wrap(1), wrap(2)]; +ys[0].push(3); +print(xs, ys); diff --git a/tests/functional/programs/005_functions/091_a_recursive_call_can_t_grow_its_argument_types.ndc b/tests/functional/programs/005_functions/091_a_recursive_call_can_t_grow_its_argument_types.ndc new file mode 100644 index 00000000..9958e439 --- /dev/null +++ b/tests/functional/programs/005_functions/091_a_recursive_call_can_t_grow_its_argument_types.ndc @@ -0,0 +1,5 @@ +// `nest` for an Int would need `nest` for a list of Ints, which needs one +// for lists of those, without end, so the recursive call is the error. +// expect-error: `nest` calls itself with an argument built from what it was given +fn nest(x, n) { if n == 0 { x } else { nest([x], n - 1) } } +nest(1, 3); diff --git a/tests/functional/programs/005_functions/092_a_recursive_call_through_any_can_grow.ndc b/tests/functional/programs/005_functions/092_a_recursive_call_through_any_can_grow.ndc new file mode 100644 index 00000000..c657fa9e --- /dev/null +++ b/tests/functional/programs/005_functions/092_a_recursive_call_through_any_can_grow.ndc @@ -0,0 +1,4 @@ +// A parameter of type `Any` takes anything, so the recursive call can wrap it. +// expect-output: [[[1]]] +fn nest(x: Any, n) { if n == 0 { x } else { nest([x], n - 1) } } +print(nest(1, 3)); diff --git a/tests/functional/programs/005_functions/093_an_overloaded_function_in_a_list_needs_a_type.ndc b/tests/functional/programs/005_functions/093_an_overloaded_function_in_a_list_needs_a_type.ndc new file mode 100644 index 00000000..6e98689a --- /dev/null +++ b/tests/functional/programs/005_functions/093_an_overloaded_function_in_a_list_needs_a_type.ndc @@ -0,0 +1,3 @@ +// The other values of a list don't say which `abs` is meant. +// expect-error: 'abs' has several overloads, and nothing here says which one is meant +let fs = [len, abs]; diff --git a/tests/functional/programs/005_functions/094_a_recursive_call_through_another_overload_can_t_grow.ndc b/tests/functional/programs/005_functions/094_a_recursive_call_through_another_overload_can_t_grow.ndc new file mode 100644 index 00000000..272018bc --- /dev/null +++ b/tests/functional/programs/005_functions/094_a_recursive_call_through_another_overload_can_t_grow.ndc @@ -0,0 +1,8 @@ +// `[-x]` with an Int `x` is a `List`, which isn't the `List` the +// first `f` takes, so `f(1)` would need `f` for `List`, then for +// `List>`, without end. Another overload taking the call only +// keeps the error from being reported in the body. +// expect-error: `f` calls itself with an argument built from what it was given +fn f(xs: List) => 42; +fn f(x) => f([-x]); +print(f(1)); diff --git a/tests/functional/programs/005_functions/095_a_generic_body_s_own_calls_are_picked_as_it_runs.ndc b/tests/functional/programs/005_functions/095_a_generic_body_s_own_calls_are_picked_as_it_runs.ndc new file mode 100644 index 00000000..5dfc0082 --- /dev/null +++ b/tests/functional/programs/005_functions/095_a_generic_body_s_own_calls_are_picked_as_it_runs.ndc @@ -0,0 +1,7 @@ +// The function's own body is compiled for arguments of any type, where +// `sum` gets a list of what `s` holds: no overload takes a `List`, so +// the call is picked as the program runs. +// expect-output: 1 3 +fn f(s) { [i for (i, c) in s.enumerate()].sum() } +let a: Any = "abc"; +print(f("ab"), f(a)); diff --git a/tests/functional/programs/005_functions/096_an_any_value_passed_for_a_type_parameter_is_checked.ndc b/tests/functional/programs/005_functions/096_an_any_value_passed_for_a_type_parameter_is_checked.ndc new file mode 100644 index 00000000..570c4a52 --- /dev/null +++ b/tests/functional/programs/005_functions/096_an_any_value_passed_for_a_type_parameter_is_checked.ndc @@ -0,0 +1,12 @@ +// A value of type `Any` passed where a generic function's type parameter is +// expected is checked against what each call makes it: `with_five("s")` +// pushes the Int 5 into a list of Strings. +// expect-error: expected String but found Int +fn with_five(x) { + let five: Any = 5; + let xs = [x]; + xs.push(five); + xs +} +print(with_five(1)); +print(with_five("s")); diff --git a/tests/functional/programs/005_functions/097_a_list_of_a_parameter_is_typed_by_the_body.ndc b/tests/functional/programs/005_functions/097_a_list_of_a_parameter_is_typed_by_the_body.ndc new file mode 100644 index 00000000..7215c286 --- /dev/null +++ b/tests/functional/programs/005_functions/097_a_list_of_a_parameter_is_typed_by_the_body.ndc @@ -0,0 +1,7 @@ +// `[x]` is a list of what `x` is, and `f` can't take a list of its own +// parameter's type, so only the first `f` takes it, which makes `x` an +// `Any`: `f(1)` calls `f([1])` with a `List`. +// expect-output: 42 +fn f(xs: List) => 42; +fn f(x) => f([x]); +print(f(1)); diff --git a/tests/functional/programs/005_functions/097_a_type_that_grows_too_large_is_an_error.ndc b/tests/functional/programs/005_functions/097_a_type_that_grows_too_large_is_an_error.ndc new file mode 100644 index 00000000..334a9872 --- /dev/null +++ b/tests/functional/programs/005_functions/097_a_type_that_grows_too_large_is_an_error.ndc @@ -0,0 +1,12 @@ +// Each function doubles the type the one before it returns, so the types +// grow too large to work with, which is reported instead of running out of +// memory. +// expect-error: The type of `f4` has more than 10000 parts +fn f0(x) => (x, x); +fn f1(x) => f0(f0(x)); +fn f2(x) => f1(f1(x)); +fn f3(x) => f2(f2(x)); +fn f4(x) => f3(f3(x)); +fn f5(x) => f4(f4(x)); +fn f6(x) => f5(f5(x)); +if false { print(f6(1)); } diff --git a/tests/functional/programs/005_functions/098_a_generic_function_takes_what_its_body_s_literals_allow.ndc b/tests/functional/programs/005_functions/098_a_generic_function_takes_what_its_body_s_literals_allow.ndc new file mode 100644 index 00000000..7e7fc927 --- /dev/null +++ b/tests/functional/programs/005_functions/098_a_generic_function_takes_what_its_body_s_literals_allow.ndc @@ -0,0 +1,8 @@ +// The literals in a generic function's body are Ints, and an Int works +// with a Float in `+`, `-` and `<`, so these take Floats too. `fact` with a +// `1.0` returns what it's given multiplied. +// expect-output: 3.5 3 55.0 120.0 120.0 +fn inc(x) => x + 1; +fn fib(n) => if n < 2 { n } else { fib(n - 1) + fib(n - 2) }; +fn fact(n) => if n < 2 { 1.0 } else { n * fact(n - 1) }; +print(inc(2.5), inc(2), fib(10.0), fact(5.0), fact(5)); diff --git a/tests/functional/programs/005_functions/099_an_overloaded_function_value_in_a_generic_function_is_picked_for_each_call.ndc b/tests/functional/programs/005_functions/099_an_overloaded_function_value_in_a_generic_function_is_picked_for_each_call.ndc new file mode 100644 index 00000000..f502cfd1 --- /dev/null +++ b/tests/functional/programs/005_functions/099_an_overloaded_function_value_in_a_generic_function_is_picked_for_each_call.ndc @@ -0,0 +1,8 @@ +// Each call of a generic function picks the overload a function value in its +// body means, from that call's types. +// expect-output: [1,-1] [1.0,-1.0] +fn signs(ns) { + let diffs = ns.pairwise(fn(a, b) { b - a }); + diffs.map(signum) +} +print(signs([1, 3, 2]), signs([1.0, 2.5, 0.5])); diff --git a/tests/functional/programs/005_functions/100_a_function_value_left_to_the_program_takes_the_most_specific_overload.ndc b/tests/functional/programs/005_functions/100_a_function_value_left_to_the_program_takes_the_most_specific_overload.ndc new file mode 100644 index 00000000..a13d6245 --- /dev/null +++ b/tests/functional/programs/005_functions/100_a_function_value_left_to_the_program_takes_the_most_specific_overload.ndc @@ -0,0 +1,9 @@ +// A generic function used as a value runs its body compiled for any +// arguments, which leaves the function value in it to the program: it takes +// the most specific overload that accepts the argument, as a call would. +// expect-output: int +fn choose(x: Int) => "int"; +fn choose(x: Any) => "any"; +fn apply(x) { let g = choose; g(x) } +let h: Fn(Int) -> String = apply; +print(h(1)); diff --git a/tests/functional/programs/005_functions/101_a_function_value_left_to_the_program_works_as_a_callback.ndc b/tests/functional/programs/005_functions/101_a_function_value_left_to_the_program_works_as_a_callback.ndc new file mode 100644 index 00000000..c3dd7d5e --- /dev/null +++ b/tests/functional/programs/005_functions/101_a_function_value_left_to_the_program_works_as_a_callback.ndc @@ -0,0 +1,7 @@ +// A generic function used as a value runs its body compiled for any +// arguments, where `signum` is left to the program to pick for each element +// `map` passes it. +// expect-output: [1,-1] +fn signs(xs) => xs.map(signum); +let f: Fn(List) -> List = signs; +print(f([1, -2])); diff --git a/tests/functional/programs/005_functions/102_a_function_value_left_to_the_program_keeps_its_local_overloads.ndc b/tests/functional/programs/005_functions/102_a_function_value_left_to_the_program_keeps_its_local_overloads.ndc new file mode 100644 index 00000000..826db386 --- /dev/null +++ b/tests/functional/programs/005_functions/102_a_function_value_left_to_the_program_keeps_its_local_overloads.ndc @@ -0,0 +1,12 @@ +// A function value whose overload the program picks holds the overloads it +// was made from, so it works where it's called, outside their scope. +// expect-output: int +fn make(x) { + fn choose(y: Int) => "int"; + fn choose(y: Float) => "float"; + let g = choose; + fn() => g(x) +} +let m: Fn(Int) -> Fn() -> String = make; +let h = m(1); +print(h()); diff --git a/tests/functional/programs/005_functions/103_a_function_value_left_to_the_program_is_checked_as_a_function_type.ndc b/tests/functional/programs/005_functions/103_a_function_value_left_to_the_program_is_checked_as_a_function_type.ndc new file mode 100644 index 00000000..cb958cbc --- /dev/null +++ b/tests/functional/programs/005_functions/103_a_function_value_left_to_the_program_is_checked_as_a_function_type.ndc @@ -0,0 +1,12 @@ +// A function value whose overload the program picks, used as a function +// type through `Any`, gets its calls checked like any function. +// expect-error: expected the function to return String but it returned Int +fn bad(x) { + let g = abs; + g(x); + let a: Any = g; + let b: Fn(Int) -> String = a; + b(1) +} +let h: Fn(Int) -> String = bad; +print(h(1)); diff --git a/tests/functional/programs/005_functions/104_a_function_value_left_to_the_program_can_be_a_pure_function_argument.ndc b/tests/functional/programs/005_functions/104_a_function_value_left_to_the_program_can_be_a_pure_function_argument.ndc new file mode 100644 index 00000000..8884fc78 --- /dev/null +++ b/tests/functional/programs/005_functions/104_a_function_value_left_to_the_program_can_be_a_pure_function_argument.ndc @@ -0,0 +1,7 @@ +// A `pure fn` caches by its arguments, which can be a function value whose +// overload the program picks. +// expect-output: 1 +pure fn invoke(g, x) => g(x); +fn apply(x) => invoke(abs, x); +let h: Fn(Int) -> Int = apply; +print(h(1)); diff --git a/tests/functional/programs/005_functions/105_a_nested_function_takes_a_literal_call_from_one_further_out.ndc b/tests/functional/programs/005_functions/105_a_nested_function_takes_a_literal_call_from_one_further_out.ndc new file mode 100644 index 00000000..7e528d86 --- /dev/null +++ b/tests/functional/programs/005_functions/105_a_nested_function_takes_a_literal_call_from_one_further_out.ndc @@ -0,0 +1,7 @@ +// The nearest scope with a function that takes a call is the call's, so a +// later call with another type can't hand an earlier one to an outer `d`. +// expect-output: inner inner +fn d(x: Int) => "outer"; +fn run() { fn d(x) => "inner"; d(1) } +fn run2() { fn d(x) => "inner"; let r = d(1); d(2.5); r } +print(run(), run2()); diff --git a/tests/functional/programs/005_functions/106_a_nested_function_waits_for_other_arguments_before_taking_a_call.ndc b/tests/functional/programs/005_functions/106_a_nested_function_waits_for_other_arguments_before_taking_a_call.ndc new file mode 100644 index 00000000..66d32244 --- /dev/null +++ b/tests/functional/programs/005_functions/106_a_nested_function_waits_for_other_arguments_before_taking_a_call.ndc @@ -0,0 +1,9 @@ +// A call whose other arguments aren't known yet waits for them before the +// nearest scope takes it: `y` may make the outer `d` the one that fits. +// expect-output: outer +fn d(x: Int, y: String) => "outer"; +fn run(y) { + fn d(x, y: Int) => "inner"; + d(1, y) +} +print(run("s")); diff --git a/tests/functional/programs/005_functions/107_panic_never_returns.ndc b/tests/functional/programs/005_functions/107_panic_never_returns.ndc new file mode 100644 index 00000000..7e895a75 --- /dev/null +++ b/tests/functional/programs/005_functions/107_panic_never_returns.ndc @@ -0,0 +1,8 @@ +// `panic` stops the program and never returns, so it can end a branch whose +// other branch gives a value. +// expect-error: no such thing +fn pick(c) { + if c { 1 } else { panic("no such thing") } +} +let n: Int = pick(true); +print(n + pick(false)); diff --git a/tests/functional/programs/006_lists/015_string_as_list_index_error.ndc b/tests/functional/programs/006_lists/015_string_as_list_index_error.ndc index 6fe80975..ab413124 100644 --- a/tests/functional/programs/006_lists/015_string_as_list_index_error.ndc +++ b/tests/functional/programs/006_lists/015_string_as_list_index_error.ndc @@ -1,3 +1,3 @@ -// expect-error: Invalid list index +// expect-error: No function called '[]=' found that matches the arguments 'List, String let x = [1,2,3]; x["foo"] = 69; // ERROR diff --git a/tests/functional/programs/006_lists/016_string_as_list_index_with_augmented_assignment_error.ndc b/tests/functional/programs/006_lists/016_string_as_list_index_with_augmented_assignment_error.ndc index 152533dd..ba7ebc38 100644 --- a/tests/functional/programs/006_lists/016_string_as_list_index_with_augmented_assignment_error.ndc +++ b/tests/functional/programs/006_lists/016_string_as_list_index_with_augmented_assignment_error.ndc @@ -1,3 +1,3 @@ -// expect-error: Invalid list index +// expect-error: No function called '[]=' found that matches the arguments 'List, String let x = [1,2,3]; x["foo"] += 69; // ERROR diff --git a/tests/functional/programs/006_lists/028_destructure_in_for.ndc b/tests/functional/programs/006_lists/028_destructure_in_for.ndc index 29d17cea..a506d1b7 100644 --- a/tests/functional/programs/006_lists/028_destructure_in_for.ndc +++ b/tests/functional/programs/006_lists/028_destructure_in_for.ndc @@ -5,7 +5,8 @@ let map = %{ 3: "quux", }; -let set = %{}; +// The set holds both the Int keys and the String values. +let set: Map = %{}; for key, value in map { set.insert(key); set.insert(value); diff --git a/tests/functional/programs/006_lists/050_a_loop_variable_keeps_the_element_type.ndc b/tests/functional/programs/006_lists/050_a_loop_variable_keeps_the_element_type.ndc new file mode 100644 index 00000000..f5f9d399 --- /dev/null +++ b/tests/functional/programs/006_lists/050_a_loop_variable_keeps_the_element_type.ndc @@ -0,0 +1,6 @@ +// A loop variable keeps the element type: `line` is a String, so it can't +// hold the list `split` returns. Give the list a name of its own. +// expect-error: mismatched types: found List but expected String +for line in ["a b c"] { + line = line.split(" "); +} diff --git a/tests/functional/programs/006_lists/050_remove_on_widened_sequence.ndc b/tests/functional/programs/006_lists/050_remove_on_widened_sequence.ndc deleted file mode 100644 index 19a1a8a3..00000000 --- a/tests/functional/programs/006_lists/050_remove_on_widened_sequence.ndc +++ /dev/null @@ -1,7 +0,0 @@ -// Reassignment widens `line` to Sequence, which no longer matches the -// List parameter of `remove`. The analyser will not guess that the value is -// really a list — cast to say so. -// expect-output: a ["b","c"] -let line = "a b c"; -line = line.split(" "); -print((line as List).remove(0), line) diff --git a/tests/functional/programs/006_lists/051_an_in_place_append_settles_the_element_type.ndc b/tests/functional/programs/006_lists/051_an_in_place_append_settles_the_element_type.ndc new file mode 100644 index 00000000..2979d89b --- /dev/null +++ b/tests/functional/programs/006_lists/051_an_in_place_append_settles_the_element_type.ndc @@ -0,0 +1,6 @@ +// `++=` appends in place, so the empty list takes the type of what it +// appends, and a later write must fit it. +// expect-error: mismatched types: found String but expected Int +let xs = []; +xs ++= [1, 2]; +xs[0] = "a"; diff --git a/tests/functional/programs/006_lists/052_an_in_place_append_settles_each_list_in_a_tuple.ndc b/tests/functional/programs/006_lists/052_an_in_place_append_settles_each_list_in_a_tuple.ndc new file mode 100644 index 00000000..6c852696 --- /dev/null +++ b/tests/functional/programs/006_lists/052_an_in_place_append_settles_each_list_in_a_tuple.ndc @@ -0,0 +1,8 @@ +// `++=` on a tuple of lists appends to each one, so each empty list takes +// the type of what it appends. +// expect-error: No function called '++' found that matches the arguments 'Tuple, List>, List' +// (`a[0] = "x"` decides first that `a` holds Strings.) +let t = ([], []); +t ++= [1, 2]; +let a, b = t; +a[0] = "x"; diff --git a/tests/functional/programs/006_lists/053_push_checks_the_element_type.ndc b/tests/functional/programs/006_lists/053_push_checks_the_element_type.ndc new file mode 100644 index 00000000..5b138f91 --- /dev/null +++ b/tests/functional/programs/006_lists/053_push_checks_the_element_type.ndc @@ -0,0 +1,4 @@ +// `push` takes a List and a T, so a list of integers only takes integers. +// expect-error: No function called 'push' found that matches the arguments 'List, Float' +let x = ["a".len()]; +x.push(1.0); diff --git a/tests/functional/programs/006_lists/054_remove_past_the_end_is_an_error.ndc b/tests/functional/programs/006_lists/054_remove_past_the_end_is_an_error.ndc new file mode 100644 index 00000000..e69afd42 --- /dev/null +++ b/tests/functional/programs/006_lists/054_remove_past_the_end_is_an_error.ndc @@ -0,0 +1,4 @@ +// Removing at the list's length is out of bounds: an error, not a crash. +// expect-error: index 2 is out of bounds +let xs = [1, 2]; +xs.remove(2); diff --git a/tests/functional/programs/006_lists/055_comparing_or_printing_a_list_leaves_its_element_type_open.ndc b/tests/functional/programs/006_lists/055_comparing_or_printing_a_list_leaves_its_element_type_open.ndc new file mode 100644 index 00000000..43a83001 --- /dev/null +++ b/tests/functional/programs/006_lists/055_comparing_or_printing_a_list_leaves_its_element_type_open.ndc @@ -0,0 +1,7 @@ +// `==` and `string` only read their arguments, so they don't settle the +// element type of an empty list: the first `push` does. +// expect-error: No function called 'push' found that matches the arguments 'List, String' +let xs = []; +print(xs == [], xs.string); +xs.push(1); +xs.push("a"); diff --git a/tests/functional/programs/006_lists/056_pop_on_an_empty_list_is_an_error.ndc b/tests/functional/programs/006_lists/056_pop_on_an_empty_list_is_an_error.ndc new file mode 100644 index 00000000..d1219484 --- /dev/null +++ b/tests/functional/programs/006_lists/056_pop_on_an_empty_list_is_an_error.ndc @@ -0,0 +1,8 @@ +// `pop` returns an element, so an empty list has none to give; `pop?` gives +// `None` instead. +// expect-output: None +// expect-error: the list is empty +let xs = ["a".len()]; +xs.pop(); +print(xs.pop?()); +xs.pop(); diff --git a/tests/functional/programs/006_lists/057_a_write_with_an_any_index_checks_the_value.ndc b/tests/functional/programs/006_lists/057_a_write_with_an_any_index_checks_the_value.ndc new file mode 100644 index 00000000..b6c0ce31 --- /dev/null +++ b/tests/functional/programs/006_lists/057_a_write_with_an_any_index_checks_the_value.ndc @@ -0,0 +1,5 @@ +// Whatever the index turns out to be, a String doesn't go in a list of Ints. +// expect-error: mismatched types: found String but expected Int +let xs = ["a".len()]; +let i: Any = 0; +xs[i] = "bad"; diff --git a/tests/functional/programs/006_lists/058_a_write_with_an_any_index_picks_by_the_index.ndc b/tests/functional/programs/006_lists/058_a_write_with_an_any_index_picks_by_the_index.ndc new file mode 100644 index 00000000..5db14f8b --- /dev/null +++ b/tests/functional/programs/006_lists/058_a_write_with_an_any_index_picks_by_the_index.ndc @@ -0,0 +1,8 @@ +// expect-output: [5] [7,8,3] +let xs = ["a".len()]; +let i: Any = 0; +xs[i] = 5; +let ys = [1, 2, 3]; +let range: Any = 0..2; +ys[range] = [7, 8]; +print(xs, ys); diff --git a/tests/functional/programs/006_lists/059_a_failed_call_in_a_list_is_reported_once.ndc b/tests/functional/programs/006_lists/059_a_failed_call_in_a_list_is_reported_once.ndc new file mode 100644 index 00000000..de8396a6 --- /dev/null +++ b/tests/functional/programs/006_lists/059_a_failed_call_in_a_list_is_reported_once.ndc @@ -0,0 +1,5 @@ +// expect-error: No function called 'len' found that matches the arguments 'Bool' +// The list holds what a failed call gave, so `sum` isn't reported as well. +let x = len(true)[0]; +let xs = [x, x]; +print(xs.sum()); diff --git a/tests/functional/programs/007_map_and_set/019_map_set_comprehensions.ndc b/tests/functional/programs/007_map_and_set/019_map_set_comprehensions.ndc index 096e525f..b56d9cb9 100644 --- a/tests/functional/programs/007_map_and_set/019_map_set_comprehensions.ndc +++ b/tests/functional/programs/007_map_and_set/019_map_set_comprehensions.ndc @@ -10,4 +10,3 @@ assert(def[64]); assert(def[49]); assert(!def[59]); assert(!def[1010101]); -assert(!def["haha"]); diff --git a/tests/functional/programs/007_map_and_set/024_op_assign_default_value.ndc b/tests/functional/programs/007_map_and_set/024_op_assign_default_value.ndc index 43f90d9b..badb92d6 100644 --- a/tests/functional/programs/007_map_and_set/024_op_assign_default_value.ndc +++ b/tests/functional/programs/007_map_and_set/024_op_assign_default_value.ndc @@ -1,7 +1,7 @@ let map = %{:0}; // map with default 0 map["foo"] += 3; -map[0] += 10; -map[0] += 10; +map["bar"] += 10; +map["bar"] += 10; assert_eq(map["foo"], 3); -assert_eq(map[0], 20); \ No newline at end of file +assert_eq(map["bar"], 20); \ No newline at end of file diff --git a/tests/functional/programs/007_map_and_set/027_op_assign_empty_map_error.ndc b/tests/functional/programs/007_map_and_set/027_op_assign_empty_map_error.ndc index 592a4897..60a71193 100644 --- a/tests/functional/programs/007_map_and_set/027_op_assign_empty_map_error.ndc +++ b/tests/functional/programs/007_map_and_set/027_op_assign_empty_map_error.ndc @@ -1,5 +1,5 @@ -// An empty map without a default has value type `()`, so no `+` overload can -// ever apply and the analyser rejects the call. -// expect-error: No function called '+' found that matches the arguments '(), Int' +// `%{}` doesn't say what its values are, so `+=` on one of them is checked +// when the program runs, and fails there because the key is missing. +// expect-error: Key not found in map let map = %{}; map["invalid key"] += 3; diff --git a/tests/functional/programs/007_map_and_set/029_an_empty_map_takes_the_types_written_to_it.ndc b/tests/functional/programs/007_map_and_set/029_an_empty_map_takes_the_types_written_to_it.ndc new file mode 100644 index 00000000..cdd13465 --- /dev/null +++ b/tests/functional/programs/007_map_and_set/029_an_empty_map_takes_the_types_written_to_it.ndc @@ -0,0 +1,6 @@ +// The first write to an empty map settles its key and value types, and later +// writes must fit them. +// expect-error: mismatched types: found String but expected Int +let counts = %{}; +counts["a"] = 1; +counts["b"] = "two"; diff --git a/tests/functional/programs/007_map_and_set/030_a_map_is_written_with_keys_of_its_key_type.ndc b/tests/functional/programs/007_map_and_set/030_a_map_is_written_with_keys_of_its_key_type.ndc new file mode 100644 index 00000000..3f65a21e --- /dev/null +++ b/tests/functional/programs/007_map_and_set/030_a_map_is_written_with_keys_of_its_key_type.ndc @@ -0,0 +1,5 @@ +// The first write settles the map's key type, and later writes must use it. +// expect-error: No function called '[]=' found that matches the arguments 'Map, String, Int' +let m = %{}; +m[1] = 2; +m["a"] = 3; diff --git a/tests/functional/programs/007_map_and_set/031_set_operators_keep_the_left_value_type.ndc b/tests/functional/programs/007_map_and_set/031_set_operators_keep_the_left_value_type.ndc new file mode 100644 index 00000000..8cd44322 --- /dev/null +++ b/tests/functional/programs/007_map_and_set/031_set_operators_keep_the_left_value_type.ndc @@ -0,0 +1,7 @@ +// `&` keeps the left map's values whatever the right one holds, while `|` +// mixes the values of both maps, so they must be of one type. +// expect-error: No function called '|' found that matches the arguments 'Map, Map' +let ages = %{"ann": 31, "bob": 42}; +let kept: Map = ages & set(["ann"]); +assert_eq(kept.values.sum, 31); +ages | set(["cid"]); diff --git a/tests/functional/programs/007_map_and_set/032_a_default_function_returns_the_maps_value_type.ndc b/tests/functional/programs/007_map_and_set/032_a_default_function_returns_the_maps_value_type.ndc new file mode 100644 index 00000000..cd0deed7 --- /dev/null +++ b/tests/functional/programs/007_map_and_set/032_a_default_function_returns_the_maps_value_type.ndc @@ -0,0 +1,6 @@ +// The default function makes each missing value, so it's expected to +// return the map's value type, and the empty list it returns is a List. +// expect-error: No function called 'push' found that matches the arguments 'List, String' +let m: Map> = %{:fn() => []}; +m["a"].push(1); +m["b"].push("x"); diff --git a/tests/functional/programs/007_map_and_set/033_a_key_without_a_value_holds_unit.ndc b/tests/functional/programs/007_map_and_set/033_a_key_without_a_value_holds_unit.ndc new file mode 100644 index 00000000..3c2593bc --- /dev/null +++ b/tests/functional/programs/007_map_and_set/033_a_key_without_a_value_holds_unit.ndc @@ -0,0 +1,6 @@ +// expect-output: unit int +fn g(x: Int) => "int"; +fn g(x: ()) => "unit"; +fn g(x: Any) => "any"; +let m = %{"a", "b": 1}; +print(g(m["a"]), g(m["b"])); diff --git a/tests/functional/programs/007_map_and_set/034_a_comprehension_default_is_one_of_the_values.ndc b/tests/functional/programs/007_map_and_set/034_a_comprehension_default_is_one_of_the_values.ndc new file mode 100644 index 00000000..2accb112 --- /dev/null +++ b/tests/functional/programs/007_map_and_set/034_a_comprehension_default_is_one_of_the_values.ndc @@ -0,0 +1,7 @@ +// The default is a String, so a read may give one, not just the Ints. +// expect-output: str int +fn g(x: Int) => "int"; +fn g(x: String) => "str"; +fn g(x: Any) => "any"; +let m = %{:"missing", x: x for x in 1..3}; +print(g(m[99]), g(m[1])); diff --git a/tests/functional/programs/007_map_and_set/035_a_literal_key_takes_the_map_s_key_type.ndc b/tests/functional/programs/007_map_and_set/035_a_literal_key_takes_the_map_s_key_type.ndc new file mode 100644 index 00000000..05495e45 --- /dev/null +++ b/tests/functional/programs/007_map_and_set/035_a_literal_key_takes_the_map_s_key_type.ndc @@ -0,0 +1,5 @@ +// The `1` is a key of a map of Floats, so it's the Float `1.0`. +// expect-output: a false true +let m = %{1: "a", 2.5: "b"}; +let n = %{1.5: "c"}; +print(m[1], 1 in n, 1.5 in n); diff --git a/tests/functional/programs/008_iterators/002_range_contains.ndc b/tests/functional/programs/008_iterators/002_range_contains.ndc index 74da3c79..4cde5b6e 100644 --- a/tests/functional/programs/008_iterators/002_range_contains.ndc +++ b/tests/functional/programs/008_iterators/002_range_contains.ndc @@ -24,12 +24,17 @@ assert_eq(-1 in (-10..=-1), true); // -1 is at the upper bound assert_eq(-1 in (-10..-1), false); // 0 is outside the negative range assert_eq(0 in (-10..=-1), false); // 0 is outside the negative range -// Invalid types -assert_eq("string" in (1..10), false); // Non-integer type -assert_eq(3.14 in (1..10), false); // Float type -assert_eq(true in (1..10), false); // Boolean type -assert_eq(() in (1..10), false); // Null type -assert_eq([1, 2, 3] in (1..10), false); // Array type +// Invalid types, typed `Any` so that only the range itself can tell +let string: Any = "string"; +let float: Any = 3.14; +let boolean: Any = true; +let unit: Any = (); +let array: Any = [1, 2, 3]; +assert_eq(string in (1..10), false); // Non-integer type +assert_eq(float in (1..10), false); // Float type +assert_eq(boolean in (1..10), false); // Boolean type +assert_eq(unit in (1..10), false); // Null type +assert_eq(array in (1..10), false); // Array type // Boundary cases assert_eq(1 in (1..=1), true); // Single-value inclusive range diff --git a/tests/functional/programs/008_iterators/005_take.ndc b/tests/functional/programs/008_iterators/005_take.ndc index 918946fc..f51ae21c 100644 --- a/tests/functional/programs/008_iterators/005_take.ndc +++ b/tests/functional/programs/008_iterators/005_take.ndc @@ -11,4 +11,4 @@ assert_eq([1, 2].take(10).list(), [1, 2]); assert_eq([1, 2, 3].take(0).list(), []); // Chained: take from a combinations iterator. -assert_eq(combinations([1, 2, 3, 4], 2).take(3).list(), [(1, 2), (1, 3), (1, 4)]); +assert_eq(combinations([1, 2, 3, 4], 2).take(3).list(), [[1, 2], [1, 3], [1, 4]]); diff --git a/tests/functional/programs/008_iterators/007_range_bound_types.ndc b/tests/functional/programs/008_iterators/007_range_bound_types.ndc index 7cf89390..cc74bbb0 100644 --- a/tests/functional/programs/008_iterators/007_range_bound_types.ndc +++ b/tests/functional/programs/008_iterators/007_range_bound_types.ndc @@ -1,4 +1,4 @@ -// Every non-Int bound used to claim the integer was too large. Only an -// integer that genuinely does not fit is a size problem. -// expect-error: range bounds must be Int, got Number +// Every non-Int bound used to claim the integer was too large. A range +// counts in Ints, so a Number bound is reported before the program runs. +// expect-error: mismatched types: found Number but expected Int for i in 0n..3n { print(i); } diff --git a/tests/functional/programs/008_iterators/008_a_range_bound_is_an_int.ndc b/tests/functional/programs/008_iterators/008_a_range_bound_is_an_int.ndc new file mode 100644 index 00000000..e4b80020 --- /dev/null +++ b/tests/functional/programs/008_iterators/008_a_range_bound_is_an_int.ndc @@ -0,0 +1,10 @@ +// A range counts in Ints, so a literal used as its bound is an Int, and a +// list mixing it with a Float holds `Any`s. +// expect-output: 3 [0.5,3] +let n = 3; +let xs = [0.5, n]; +let total = 0; +for i in 0..n { + total += i; +} +print(total, xs); diff --git a/tests/functional/programs/008_iterators/009_iterating_a_failed_call_is_reported_once.ndc b/tests/functional/programs/008_iterators/009_iterating_a_failed_call_is_reported_once.ndc new file mode 100644 index 00000000..4e2bc254 --- /dev/null +++ b/tests/functional/programs/008_iterators/009_iterating_a_failed_call_is_reported_once.ndc @@ -0,0 +1,8 @@ +// expect-error: No function called 'len' found that matches the arguments 'Bool' +// What iterating gives comes from the failed call, so nothing built from it +// is reported. +let n = len(true); +let words: List = [x for x in n]; +for x in n.map(fn(i) => [i]) { + print(x.sum()); +} diff --git a/tests/functional/programs/009_slicing/013_a_list_slice_takes_any_sequence.ndc b/tests/functional/programs/009_slicing/013_a_list_slice_takes_any_sequence.ndc new file mode 100644 index 00000000..98288bb9 --- /dev/null +++ b/tests/functional/programs/009_slicing/013_a_list_slice_takes_any_sequence.ndc @@ -0,0 +1,7 @@ +// A list slice can be assigned any sequence of its element type. +// expect-output: [1,5,6] [8,9,3] +let xs = [1, 2, 3]; +xs[1..3] = 5..7; +let ys = [1, 2, 3]; +ys[0..2] = (8, 9); +print(xs, ys); diff --git a/tests/functional/programs/010_memoization/003_each_copy_of_a_generic_pure_fn_has_its_own_cache.ndc b/tests/functional/programs/010_memoization/003_each_copy_of_a_generic_pure_fn_has_its_own_cache.ndc new file mode 100644 index 00000000..c20fb19c --- /dev/null +++ b/tests/functional/programs/010_memoization/003_each_copy_of_a_generic_pure_fn_has_its_own_cache.ndc @@ -0,0 +1,15 @@ +// A generic `pure fn` is compiled once for each type it's called with, and +// each of those copies has a cache of its own. The second direct call finds +// what the first computed. A call through a value of the function runs the +// function's own body, which computes it again. +// expect-output: 10 10 10 2 +let calls = 0; +pure fn add(x, y) { + calls += 1; + x + y +} +let first = add(5, 5); +let again = add(5, 5); +let f = add; +let through_value = f(5, 5); +print(first, again, through_value, calls); diff --git a/tests/functional/programs/010_memoization/004_an_instance_that_checks_any_has_its_own_cache.ndc b/tests/functional/programs/010_memoization/004_an_instance_that_checks_any_has_its_own_cache.ndc new file mode 100644 index 00000000..96f49143 --- /dev/null +++ b/tests/functional/programs/010_memoization/004_an_instance_that_checks_any_has_its_own_cache.ndc @@ -0,0 +1,10 @@ +// The instance of `has` for a `List` checks that `v` is an Int, which +// the one for a `List` doesn't. Both get an empty list, so a cache +// they shared would hand the second call the first one's result. +// expect-error: expected Int but found String +pure fn has(xs, v) { xs.contains(v) } +let anything: List = []; +let numbers: List = []; +let a: Any = "s"; +print(has(anything, a)); +print(has(numbers, a)); diff --git a/tests/functional/programs/010_memoization/005_an_instance_whose_callee_checks_any_has_its_own_cache.ndc b/tests/functional/programs/010_memoization/005_an_instance_whose_callee_checks_any_has_its_own_cache.ndc new file mode 100644 index 00000000..9ab35501 --- /dev/null +++ b/tests/functional/programs/010_memoization/005_an_instance_whose_callee_checks_any_has_its_own_cache.ndc @@ -0,0 +1,10 @@ +// `outer` doesn't check anything itself, but its instance for a +// `List` calls the instance of `has` that checks `v` for an Int. +// expect-error: expected Int but found String +fn has(xs, v) { xs.contains(v) } +pure fn outer(xs, v) { has(xs, v) } +let anything: List = []; +let numbers: List = []; +let a: Any = "s"; +print(outer(anything, a)); +print(outer(numbers, a)); diff --git a/tests/functional/programs/010_memoization/006_a_pure_fn_caches_each_argument_type_apart.ndc b/tests/functional/programs/010_memoization/006_a_pure_fn_caches_each_argument_type_apart.ndc new file mode 100644 index 00000000..2e0ea672 --- /dev/null +++ b/tests/functional/programs/010_memoization/006_a_pure_fn_caches_each_argument_type_apart.ndc @@ -0,0 +1,7 @@ +// A `pure fn` caches by its arguments, and `1` and `1.0` are equal, so each +// argument type gets a cache of its own. +// expect-output: 1 1.5 +pure fn same(x) => x; +let n = same(1); +let f = same(1.0); +print(n, f + 0.5); diff --git a/tests/functional/programs/011_heap/010_float_ordering.ndc b/tests/functional/programs/011_heap/010_float_ordering.ndc index a2999348..6bc635cc 100644 --- a/tests/functional/programs/011_heap/010_float_ordering.ndc +++ b/tests/functional/programs/011_heap/010_float_ordering.ndc @@ -20,7 +20,7 @@ assert_eq(h.pop, 2.2); assert_eq(h.pop, 1.0); // MinHeap: int vs float with fractional tiebreaker (5 < 5.3, 4.9 < 5) -let h = MinHeap(); +let h: MinHeap = MinHeap(); h.push(5); h.push(5.3); h.push(4.9); @@ -29,7 +29,7 @@ assert_eq(h.pop, 5); assert_eq(h.pop, 5.3); // int equal to float (5 == 5.0) in a MaxHeap -let h = MaxHeap(); +let h: MaxHeap = MaxHeap(); h.push(5); h.push(5.0); h.push(6); diff --git a/tests/functional/programs/011_heap/011_numeric_types_ordering.ndc b/tests/functional/programs/011_heap/011_numeric_types_ordering.ndc index c5c37904..578c1ab1 100644 --- a/tests/functional/programs/011_heap/011_numeric_types_ordering.ndc +++ b/tests/functional/programs/011_heap/011_numeric_types_ordering.ndc @@ -1,8 +1,8 @@ // Tests heap ordering with BigInt, Rational, and Complex values, exercising // the VM's cross-type OrdValue comparator path. -// MinHeap with BigInts (values outside i64 range) -let h = MinHeap(); +// MinHeap with BigInts (values outside i64 range) and an Int +let h: MinHeap = MinHeap(); h.push(9223372036854775808n); // i64::MAX + 1 h.push(9223372036854775809n); h.push(9223372036854775807); // i64::MAX (fits in i64) @@ -20,7 +20,7 @@ assert_eq(h.pop, 5n / 3n); assert_eq(h.pop, 1n / 2n); // MinHeap: mixed int and Rational -let h = MinHeap(); +let h: MinHeap = MinHeap(); h.push(2); h.push(3n / 2n); // 1.5 h.push(5n / 2n); // 2.5 diff --git a/tests/functional/programs/011_heap/012_a_popped_value_keeps_the_element_type.ndc b/tests/functional/programs/011_heap/012_a_popped_value_keeps_the_element_type.ndc new file mode 100644 index 00000000..a1fe961e --- /dev/null +++ b/tests/functional/programs/011_heap/012_a_popped_value_keeps_the_element_type.ndc @@ -0,0 +1,5 @@ +// A MinHeap pops Strings, so adding an Int to a popped value is an error. +// expect-error: No function called '+' found that matches the arguments 'String, Int' +let heap = MinHeap(); +heap.push("a"); +heap.pop() + 1; diff --git a/tests/functional/programs/012_deque/004_push_checks_the_element_type.ndc b/tests/functional/programs/012_deque/004_push_checks_the_element_type.ndc new file mode 100644 index 00000000..60f61287 --- /dev/null +++ b/tests/functional/programs/012_deque/004_push_checks_the_element_type.ndc @@ -0,0 +1,5 @@ +// The first push settles what a deque holds, and later pushes must fit it. +// expect-error: No function called 'push_front' found that matches the arguments 'Deque, String' +let queue = Deque(); +queue.push_back(1); +queue.push_front("a"); diff --git a/tests/functional/programs/012_deque/005_a_deque_element_keeps_its_type.ndc b/tests/functional/programs/012_deque/005_a_deque_element_keeps_its_type.ndc new file mode 100644 index 00000000..c6843920 --- /dev/null +++ b/tests/functional/programs/012_deque/005_a_deque_element_keeps_its_type.ndc @@ -0,0 +1,10 @@ +// Reading from a Deque, by index, slice or pop, gives Ints and Lists of Ints. +// expect-output: 3 +let queue = Deque(); +queue.push_back(1); +queue.push_back(2); +queue.push_back(3); +let first: Int = queue[0]; +let rest: List = queue[1..]; +let last: Int = queue.pop_back(); +print(first + rest.sum - last); diff --git a/tests/functional/programs/013_vector_math/010_vector_op_assignment.ndc b/tests/functional/programs/013_vector_math/010_vector_op_assignment.ndc index 88881cd5..c373ba22 100644 --- a/tests/functional/programs/013_vector_math/010_vector_op_assignment.ndc +++ b/tests/functional/programs/013_vector_math/010_vector_op_assignment.ndc @@ -1,6 +1,6 @@ // Compound assignment must use the vec return type, not the underlying -// scalar's return. `a += (3, 4)` on a `Tuple` lvalue must -// widen-check against `Tuple`, not `Int`. +// scalar's return. `a += (3, 4)` on a `Tuple` lvalue must be +// checked against `Tuple`, not `Int`. let a: Tuple = (1, 2); a += (3, 4); assert_eq(a, (4, 6)); @@ -14,9 +14,8 @@ let c: Tuple = (3, 4); c *= 2; assert_eq(c, (6, 8)); -// Without an annotation the inferred lvalue type must also widen -// correctly. `d` starts as `Tuple` and a follow-up read -// must still see the precise tuple element types. +// Without an annotation the same holds: `d` is `Tuple`, and a +// follow-up read still sees the precise tuple element types. let d = (1, 2); d += (10, 20); let e: Tuple = d + (100, 200); diff --git a/tests/functional/programs/013_vector_math/015_an_operator_on_a_tuple_waits_for_what_it_holds.ndc b/tests/functional/programs/013_vector_math/015_an_operator_on_a_tuple_waits_for_what_it_holds.ndc new file mode 100644 index 00000000..293840d0 --- /dev/null +++ b/tests/functional/programs/013_vector_math/015_an_operator_on_a_tuple_waits_for_what_it_holds.ndc @@ -0,0 +1,5 @@ +// `cs[0]` isn't known when `-` is reached, but it's a tuple of Ints, so +// the result is one too. +// expect-output: 6 +let cs = [(x, y) for (x, y) in [[3, 4]]]; +print((cs[0] - (1, 1)).product()); diff --git a/tests/functional/programs/015_struct/006_accessor_as_map_key.ndc b/tests/functional/programs/015_struct/006_accessor_as_map_key.ndc index b5d8e245..22cbde87 100644 --- a/tests/functional/programs/015_struct/006_accessor_as_map_key.ndc +++ b/tests/functional/programs/015_struct/006_accessor_as_map_key.ndc @@ -19,15 +19,17 @@ assert(x != y); let by_ctor = %{Point: "the Point constructor"}; assert_eq(by_ctor[Point], "the Point constructor"); -// A same-named field on another struct is a distinct getter. +// A same-named field on another struct is a distinct getter. Once both +// exist, `x` used as a value needs a type to say which one it is. let point_x = x; struct Other { x: Int, } -let other_x = x; +let other_x: Fn(Other) -> Int = x; assert(point_x != other_x); -let both = %{point_x: "Point", other_x: "Other"}; +// The two getters have different types, so the keys are typed `Any`. +let both: Map = %{point_x: "Point", other_x: "Other"}; assert_eq(both.len(), 2); assert_eq(both[point_x], "Point"); assert_eq(both[other_x], "Other"); diff --git a/tests/functional/programs/015_struct/033_a_constructor_called_through_any_checks_its_fields.ndc b/tests/functional/programs/015_struct/033_a_constructor_called_through_any_checks_its_fields.ndc new file mode 100644 index 00000000..a9953f25 --- /dev/null +++ b/tests/functional/programs/015_struct/033_a_constructor_called_through_any_checks_its_fields.ndc @@ -0,0 +1,4 @@ +// expect-error: no function called 'P' found matches the arguments: (String) +struct P { x: Int } +let make: Any = P; +make("not an int"); diff --git a/tests/functional/programs/015_struct/034_a_constructor_called_through_any_takes_a_typed_container.ndc b/tests/functional/programs/015_struct/034_a_constructor_called_through_any_takes_a_typed_container.ndc new file mode 100644 index 00000000..620932cb --- /dev/null +++ b/tests/functional/programs/015_struct/034_a_constructor_called_through_any_takes_a_typed_container.ndc @@ -0,0 +1,5 @@ +// The list is checked element by element, as nothing takes it otherwise. +// expect-output: [1,2] +struct Q { xs: List } +let make: Any = Q; +print(make([1, 2]).xs); diff --git a/tests/functional/programs/015_struct/035_a_field_written_through_any_is_checked.ndc b/tests/functional/programs/015_struct/035_a_field_written_through_any_is_checked.ndc new file mode 100644 index 00000000..2d55d6bf --- /dev/null +++ b/tests/functional/programs/015_struct/035_a_field_written_through_any_is_checked.ndc @@ -0,0 +1,5 @@ +// expect-error: expected Int but found String +struct P { x: Int } +let p: Any = P(1); +let v: Any = "s"; +p.x = v; diff --git a/tests/functional/programs/015_struct/036_a_container_field_written_through_any_is_checked.ndc b/tests/functional/programs/015_struct/036_a_container_field_written_through_any_is_checked.ndc new file mode 100644 index 00000000..3475d7c4 --- /dev/null +++ b/tests/functional/programs/015_struct/036_a_container_field_written_through_any_is_checked.ndc @@ -0,0 +1,6 @@ +// expect-output: [2] +struct Q { xs: List } +let q = Q([1]); +let v: Any = [2]; +q.xs = v; +print(q.xs); diff --git a/tests/functional/programs/015_struct/037_a_lambda_passed_to_map_takes_the_element_type.ndc b/tests/functional/programs/015_struct/037_a_lambda_passed_to_map_takes_the_element_type.ndc new file mode 100644 index 00000000..b8189215 --- /dev/null +++ b/tests/functional/programs/015_struct/037_a_lambda_passed_to_map_takes_the_element_type.ndc @@ -0,0 +1,8 @@ +// A lambda passed to `map` gets its parameter's type from the list before +// its body is checked: `p` is a `Player`, which has no `size`, though a +// `Team` does. +// expect-error: No function called 'size' found that matches the arguments 'Player' +struct Player { name: String, score: Int } +struct Team { name: String, size: Int } +let players = [Player("ann", 3), Player("bob", 5)]; +print(players.map(fn(p) => p.size)); diff --git a/tests/functional/programs/015_struct/038_a_lambda_takes_its_parameter_types_from_the_call.ndc b/tests/functional/programs/015_struct/038_a_lambda_takes_its_parameter_types_from_the_call.ndc new file mode 100644 index 00000000..6e53dbc5 --- /dev/null +++ b/tests/functional/programs/015_struct/038_a_lambda_takes_its_parameter_types_from_the_call.ndc @@ -0,0 +1,8 @@ +// Each lambda's parameters take the types the function it's passed to +// gives them, from the arguments before and after it. +// expect-output: 8 ["ann","bob"] ["bob"] +struct Player { name: String, score: Int } +struct Team { name: String, score: Int } +let players = [Player("ann", 3), Player("bob", 5)]; +let total = players.fold(0, fn(sum, p) => sum + p.score); +print(total, players.map(fn(p) => p.name), players.filter(fn(p) => p.score > 4).map(fn(p) => p.name)); diff --git a/tests/functional/programs/015_struct/039_a_failed_field_read_is_reported_once.ndc b/tests/functional/programs/015_struct/039_a_failed_field_read_is_reported_once.ndc new file mode 100644 index 00000000..9848eaf0 --- /dev/null +++ b/tests/functional/programs/015_struct/039_a_failed_field_read_is_reported_once.ndc @@ -0,0 +1,5 @@ +// expect-error: No function called 'len' found that matches the arguments 'Player' +// Only the missing `len` is reported, not `sum` of the list of what it gave. +struct Player { name: String, score: Int } +let players = [Player("ann", 3), Player("bob", 5)]; +print(players.map(fn(p) => p.len).sum()); diff --git a/tests/functional/programs/016_casts/002_list_downcast_from_any.ndc b/tests/functional/programs/016_casts/002_list_downcast_from_any.ndc index 46127c56..ca7ff06d 100644 --- a/tests/functional/programs/016_casts/002_list_downcast_from_any.ndc +++ b/tests/functional/programs/016_casts/002_list_downcast_from_any.ndc @@ -1,6 +1,6 @@ -// `keys` returns List; the cast recovers the element type so calls on -// the result can resolve against Int overloads. -let values = %{}; +// The keys of a `Map` are a List; the cast recovers the element +// type so calls on the result can resolve against Int overloads. +let values: Map = %{}; values.insert(3); values.insert(1); values.insert(2); diff --git a/tests/functional/programs/016_casts/014_overlapping_hierarchy.ndc b/tests/functional/programs/016_casts/014_overlapping_hierarchy.ndc index 70c3f476..75e615be 100644 --- a/tests/functional/programs/016_casts/014_overlapping_hierarchy.ndc +++ b/tests/functional/programs/016_casts/014_overlapping_hierarchy.ndc @@ -1,8 +1,7 @@ -// Neither List nor Sequence is a subtype of the other, but a -// List is a subtype of both, so the cast is possible and gets checked +// A List may hold only Ints, so the cast is possible and gets checked // at runtime rather than rejected outright. let xs: List = [1, 2]; -assert_eq(xs as Sequence, [1, 2]); +assert_eq(xs as List, [1, 2]); let m: Map = %{1: 2}; -assert_eq(m as Sequence<(Int, Int)>, %{1: 2}); +assert_eq(m as Map, %{1: 2}); diff --git a/tests/functional/programs/016_casts/015_overlapping_hierarchy_error.ndc b/tests/functional/programs/016_casts/015_overlapping_hierarchy_error.ndc index b3414ef6..7a22a9c9 100644 --- a/tests/functional/programs/016_casts/015_overlapping_hierarchy_error.ndc +++ b/tests/functional/programs/016_casts/015_overlapping_hierarchy_error.ndc @@ -1,5 +1,5 @@ // The cast is possible for the type, so it reaches the runtime check — which // then rejects this particular value. -// expect-error: cannot cast List to Sequence +// expect-error: cannot cast List to List let xs: List = [1, "two"]; -xs as Sequence +xs as List diff --git a/tests/functional/programs/016_casts/017_cast_op_assignment_error.ndc b/tests/functional/programs/016_casts/017_cast_op_assignment_error.ndc index f065834e..d3772c81 100644 --- a/tests/functional/programs/016_casts/017_cast_op_assignment_error.ndc +++ b/tests/functional/programs/016_casts/017_cast_op_assignment_error.ndc @@ -1,7 +1,7 @@ // The cast checks the elements, so a bad operand fails at the cast site // instead of silently landing in the target. // expect-error: cannot cast List to List -fn opaque(x) => x; +fn opaque(x) -> Any => x; let values = [1]; values ++= opaque([0.5]) as List; print(values) diff --git a/tests/functional/programs/016_casts/023_numeric_sibling_cast_error.ndc b/tests/functional/programs/016_casts/023_numeric_sibling_cast_error.ndc index da4deb7a..b577512a 100644 --- a/tests/functional/programs/016_casts/023_numeric_sibling_cast_error.ndc +++ b/tests/functional/programs/016_casts/023_numeric_sibling_cast_error.ndc @@ -1,4 +1,6 @@ // A cast asserts, it does not convert, and Int is not a Number under the -// sibling numeric model — `Number(1)` is the conversion. +// sibling numeric model — `Number(1)` is the conversion. A literal like `1` +// would take the type the cast says. // expect-error: invalid cast: Int can never be Number -1 as Number +let n = "a".len(); +n as Number diff --git a/tests/functional/programs/016_casts/024_cast_selects_typed_overload.ndc b/tests/functional/programs/016_casts/024_cast_selects_typed_overload.ndc index 48b36fb8..cb27fa3b 100644 --- a/tests/functional/programs/016_casts/024_cast_selects_typed_overload.ndc +++ b/tests/functional/programs/016_casts/024_cast_selects_typed_overload.ndc @@ -2,7 +2,7 @@ // separate Int, Float, and Number overloads, and a value the analyser only // knows as List matches none of them, so each cast below picks a // different overload at compile time. -let values = %{}; +let values: Map = %{}; values.insert(1); values.insert(2); assert_eq((values.keys as List).sum(), 3); diff --git a/tests/functional/programs/016_casts/025_a_cast_gives_a_lambda_its_parameter_types.ndc b/tests/functional/programs/016_casts/025_a_cast_gives_a_lambda_its_parameter_types.ndc new file mode 100644 index 00000000..20807bb1 --- /dev/null +++ b/tests/functional/programs/016_casts/025_a_cast_gives_a_lambda_its_parameter_types.ndc @@ -0,0 +1,5 @@ +// Casting a lambda to a function type settles the parameters nothing else +// settled, so the cast holds when the program runs. +// expect-output: 1 +let f = (fn(x) => x) as Fn(Int) -> Int; +print(f(1)); diff --git a/tests/functional/programs/016_casts/026_optional_cast_some.ndc b/tests/functional/programs/016_casts/026_optional_cast_some.ndc new file mode 100644 index 00000000..2c578e78 --- /dev/null +++ b/tests/functional/programs/016_casts/026_optional_cast_some.ndc @@ -0,0 +1,4 @@ +// `as?` gives Some of the value when it is the type. +// expect-output: Some(5) +let value: Any = 5; +print(value as? Int); diff --git a/tests/functional/programs/016_casts/027_optional_cast_none.ndc b/tests/functional/programs/016_casts/027_optional_cast_none.ndc new file mode 100644 index 00000000..ceeeeb32 --- /dev/null +++ b/tests/functional/programs/016_casts/027_optional_cast_none.ndc @@ -0,0 +1,4 @@ +// `as?` gives None when the value is not the type. +// expect-output: None +let value: Any = "five"; +print(value as? Int); diff --git a/tests/functional/programs/016_casts/028_optional_cast_containers.ndc b/tests/functional/programs/016_casts/028_optional_cast_containers.ndc new file mode 100644 index 00000000..a2cec4c5 --- /dev/null +++ b/tests/functional/programs/016_casts/028_optional_cast_containers.ndc @@ -0,0 +1,5 @@ +// A container is checked element by element, as `as` does. +// expect-output: None Some([1,"a"]) Some([1,2]) +let mixed: Any = [1, "a"]; +let ints: Any = [1, 2]; +print(mixed as? List, mixed as? List, ints as? List); diff --git a/tests/functional/programs/016_casts/029_optional_cast_functions.ndc b/tests/functional/programs/016_casts/029_optional_cast_functions.ndc new file mode 100644 index 00000000..40463844 --- /dev/null +++ b/tests/functional/programs/016_casts/029_optional_cast_functions.ndc @@ -0,0 +1,6 @@ +// A function that may be the function type comes out checking its calls. +// expect-output: 3 false +fn inc(x: Int) => x + 1; +let a: Any = inc; +let f = (a as? Fn(Int) -> Int).unwrap(); +print(f(2), (a as? Fn(String) -> Int).is_some()); diff --git a/tests/functional/programs/016_casts/030_optional_cast_checked_function_error.ndc b/tests/functional/programs/016_casts/030_optional_cast_checked_function_error.ndc new file mode 100644 index 00000000..8ba5a879 --- /dev/null +++ b/tests/functional/programs/016_casts/030_optional_cast_checked_function_error.ndc @@ -0,0 +1,6 @@ +// A function whose result is checked as it is called. +// expect-error: expected the function to return Int but it returned String +fn text(x) => "s"; +let a: Any = text; +let f = (a as? Fn(Int) -> Int).unwrap(); +print(f(1)); diff --git a/tests/functional/programs/016_casts/031_optional_cast_type_test.ndc b/tests/functional/programs/016_casts/031_optional_cast_type_test.ndc new file mode 100644 index 00000000..ae52dfe0 --- /dev/null +++ b/tests/functional/programs/016_casts/031_optional_cast_type_test.ndc @@ -0,0 +1,12 @@ +// `as?` tests the type of a value. +// expect-output: int string other +fn describe(value: Any) { + if (value as? Int).is_some() { + "int" + } else if (value as? String).is_some() { + "string" + } else { + "other" + } +} +print(describe(1), describe("a"), describe([1])); diff --git a/tests/functional/programs/016_casts/032_impossible_optional_cast_error.ndc b/tests/functional/programs/016_casts/032_impossible_optional_cast_error.ndc new file mode 100644 index 00000000..ab9cae4b --- /dev/null +++ b/tests/functional/programs/016_casts/032_impossible_optional_cast_error.ndc @@ -0,0 +1,3 @@ +// An optional cast that can never succeed would always be None. +// expect-error: invalid cast: String can never be Int +print("foo" as? Int); diff --git a/tests/functional/programs/016_casts/033_proven_optional_cast.ndc b/tests/functional/programs/016_casts/033_proven_optional_cast.ndc new file mode 100644 index 00000000..d63a1f90 --- /dev/null +++ b/tests/functional/programs/016_casts/033_proven_optional_cast.ndc @@ -0,0 +1,4 @@ +// A proven optional cast is Some, and its type is an Option of the target. +// expect-output: 3 +let n: Option = 3 as? Int; +print(n.unwrap()); diff --git a/tests/functional/programs/016_casts/034_an_optional_cast_leaves_the_value_s_type_open.ndc b/tests/functional/programs/016_casts/034_an_optional_cast_leaves_the_value_s_type_open.ndc new file mode 100644 index 00000000..3edf1c60 --- /dev/null +++ b/tests/functional/programs/016_casts/034_an_optional_cast_leaves_the_value_s_type_open.ndc @@ -0,0 +1,7 @@ +// `as?` only tests a value: a list whose elements aren't known yet stays +// open, so it can still hold Strings. The empty list is a `List` then. +// expect-output: true ["a"] +let xs = []; +let ints = (xs as? List).is_some(); +xs.push("a"); +print(ints, xs); diff --git a/tests/functional/programs/017_dynamic_any/001_an_annotated_let_takes_an_any_of_its_type.ndc b/tests/functional/programs/017_dynamic_any/001_an_annotated_let_takes_an_any_of_its_type.ndc new file mode 100644 index 00000000..bda05a7a --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/001_an_annotated_let_takes_an_any_of_its_type.ndc @@ -0,0 +1,6 @@ +// A value of type `Any` fits where an Int is expected, and is checked to be +// one as the program runs. +// expect-output: 6 +let a: Any = 5; +let n: Int = a; +print(n + 1); diff --git a/tests/functional/programs/017_dynamic_any/002_an_annotated_let_checks_an_any.ndc b/tests/functional/programs/017_dynamic_any/002_an_annotated_let_checks_an_any.ndc new file mode 100644 index 00000000..9bb57b52 --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/002_an_annotated_let_checks_an_any.ndc @@ -0,0 +1,4 @@ +// expect-error: expected Int but found String +let a: Any = "five"; +let n: Int = a; +print(n); diff --git a/tests/functional/programs/017_dynamic_any/003_an_assignment_checks_an_any.ndc b/tests/functional/programs/017_dynamic_any/003_an_assignment_checks_an_any.ndc new file mode 100644 index 00000000..e443bd6c --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/003_an_assignment_checks_an_any.ndc @@ -0,0 +1,5 @@ +// expect-error: expected Int but found String +let a: Any = "five"; +let n = 1; +n = a; +print(n); diff --git a/tests/functional/programs/017_dynamic_any/004_a_destructuring_checks_each_any.ndc b/tests/functional/programs/017_dynamic_any/004_a_destructuring_checks_each_any.ndc new file mode 100644 index 00000000..3ba08af0 --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/004_a_destructuring_checks_each_any.ndc @@ -0,0 +1,6 @@ +// expect-error: expected Int but found String +let a: Any = "five"; +let n = 0; +let s = ""; +(n, s) = (a, a); +print(n, s); diff --git a/tests/functional/programs/017_dynamic_any/005_a_return_annotation_takes_an_any_of_its_type.ndc b/tests/functional/programs/017_dynamic_any/005_a_return_annotation_takes_an_any_of_its_type.ndc new file mode 100644 index 00000000..0597ed09 --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/005_a_return_annotation_takes_an_any_of_its_type.ndc @@ -0,0 +1,5 @@ +// A function annotated to return an Int can return a value of type `Any`, +// which is checked as the program runs. +// expect-output: 3 +fn first(xs: Any) -> Int { xs[0] } +print(first([3])); diff --git a/tests/functional/programs/017_dynamic_any/006_a_return_annotation_checks_an_any.ndc b/tests/functional/programs/017_dynamic_any/006_a_return_annotation_checks_an_any.ndc new file mode 100644 index 00000000..9613cb48 --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/006_a_return_annotation_checks_an_any.ndc @@ -0,0 +1,8 @@ +// expect-error: expected Int but found String +fn first(xs: Any) -> Int { + if xs.len() == 0 { + return 0; + } + xs[0] +} +print(first(["three"])); diff --git a/tests/functional/programs/017_dynamic_any/007_a_generic_argument_checks_an_any.ndc b/tests/functional/programs/017_dynamic_any/007_a_generic_argument_checks_an_any.ndc new file mode 100644 index 00000000..8fb8a955 --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/007_a_generic_argument_checks_an_any.ndc @@ -0,0 +1,7 @@ +// `push(List, T)` takes an Int for a `List`, so an argument of type +// `Any` is checked to be one. +// expect-error: expected Int but found String +let xs = [1, 2]; +let a: Any = "three"; +xs.push(a); +print(xs); diff --git a/tests/functional/programs/017_dynamic_any/008_an_index_write_checks_an_any.ndc b/tests/functional/programs/017_dynamic_any/008_an_index_write_checks_an_any.ndc new file mode 100644 index 00000000..692d3e4f --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/008_an_index_write_checks_an_any.ndc @@ -0,0 +1,5 @@ +// expect-error: expected Int but found String +let xs = [1, 2]; +let a: Any = "three"; +xs[0] = a; +print(xs); diff --git a/tests/functional/programs/017_dynamic_any/009_a_field_write_checks_an_any.ndc b/tests/functional/programs/017_dynamic_any/009_a_field_write_checks_an_any.ndc new file mode 100644 index 00000000..f46e83ee --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/009_a_field_write_checks_an_any.ndc @@ -0,0 +1,6 @@ +// expect-error: expected Int but found String +struct Point { x: Int, y: Int } +let p = Point(1, 2); +let a: Any = "three"; +p.x = a; +print(p); diff --git a/tests/functional/programs/017_dynamic_any/010_a_container_from_any_is_scanned.ndc b/tests/functional/programs/017_dynamic_any/010_a_container_from_any_is_scanned.ndc new file mode 100644 index 00000000..c89eba8f --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/010_a_container_from_any_is_scanned.ndc @@ -0,0 +1,4 @@ +// expect-error: expected List but found List +let a: Any = ["one"]; +let xs: List = a; +print(xs); diff --git a/tests/functional/programs/017_dynamic_any/011_each_instance_checks_an_any_against_its_types.ndc b/tests/functional/programs/017_dynamic_any/011_each_instance_checks_an_any_against_its_types.ndc new file mode 100644 index 00000000..0adfcd33 --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/011_each_instance_checks_an_any_against_its_types.ndc @@ -0,0 +1,11 @@ +// `put` picks `push` for each set of types it's called with, so each +// instance checks an argument of type `Any` against its own: the String +// goes into the list of Strings, but not into the list of Ints. +// expect-error: expected Int but found String +fn put(xs, v) { xs.push(v) } +let a: Any = "s"; +let words = ["t"]; +put(words, a); +print(words); +let numbers = [1]; +put(numbers, a); diff --git a/tests/functional/programs/017_dynamic_any/012_a_function_from_any_checks_what_it_returns.ndc b/tests/functional/programs/017_dynamic_any/012_a_function_from_any_checks_what_it_returns.ndc new file mode 100644 index 00000000..7189fe54 --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/012_a_function_from_any_checks_what_it_returns.ndc @@ -0,0 +1,11 @@ +// A function of type `Any` used as a `Fn(Int) -> Int` checks what each call +// returns when its own type doesn't say: `text`'s value takes and returns +// anything. +// expect-error: expected the function to return Int but it returned String +fn show(x: Int) { "int" } +fn show(x: String) { "string" } +fn text(x) => "s"; +let a: Any = text; +let f: Fn(Int) -> Int = a; +let n: Int = f(1); +print(show(n), n); diff --git a/tests/functional/programs/017_dynamic_any/013_a_function_from_any_must_return_what_its_type_says.ndc b/tests/functional/programs/017_dynamic_any/013_a_function_from_any_must_return_what_its_type_says.ndc new file mode 100644 index 00000000..397f9e9f --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/013_a_function_from_any_must_return_what_its_type_says.ndc @@ -0,0 +1,4 @@ +// A lambda whose own type says it returns a String isn't a `Fn(Int) -> Int`. +// expect-error: expected Fn(Int) -> Int but found Fn(Any) -> String +let a: Any = fn(x) => "s"; +let f: Fn(Int) -> Int = a; diff --git a/tests/functional/programs/017_dynamic_any/014_a_checked_function_is_the_same_function.ndc b/tests/functional/programs/017_dynamic_any/014_a_checked_function_is_the_same_function.ndc new file mode 100644 index 00000000..d5203a43 --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/014_a_checked_function_is_the_same_function.ndc @@ -0,0 +1,7 @@ +// A function of type `Any` used as a function type is the same function: it +// equals itself, and a call that returns what's expected works. +// expect-output: 3 true true +fn inc(x) => x + 1; +let a: Any = inc; +let f: Fn(Int) -> Int = a; +print(f(2), f == inc, a == f); diff --git a/tests/functional/programs/017_dynamic_any/015_an_overload_picked_as_the_program_runs_takes_a_function_that_fits.ndc b/tests/functional/programs/017_dynamic_any/015_an_overload_picked_as_the_program_runs_takes_a_function_that_fits.ndc new file mode 100644 index 00000000..ddd84ef6 --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/015_an_overload_picked_as_the_program_runs_takes_a_function_that_fits.ndc @@ -0,0 +1,9 @@ +// An overload picked as the program runs takes a function of type `Any` only +// if its own type shows it fits: `text`'s value takes and returns anything, +// so nothing says it's a `Fn(Int) -> Int`. Casting it first checks its calls. +// expect-output: 2 +fn h(f: Fn(Int) -> Int) => f(1) + 1; +fn h(x: Int) => x; +fn one(x) => x; +let a: Any = one; +print(h(a as Fn(Int) -> Int)); diff --git a/tests/functional/programs/017_dynamic_any/016_a_function_from_any_must_take_what_its_type_passes.ndc b/tests/functional/programs/017_dynamic_any/016_a_function_from_any_must_take_what_its_type_passes.ndc new file mode 100644 index 00000000..8e84ae13 --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/016_a_function_from_any_must_take_what_its_type_passes.ndc @@ -0,0 +1,4 @@ +// A lambda that takes a String isn't a `Fn(Int) -> Int`. +// expect-error: expected Fn(Int) -> Int but found Fn(String) -> Int +let a: Any = fn(s: String) => 1; +let f: Fn(Int) -> Int = a; diff --git a/tests/functional/programs/017_dynamic_any/017_an_overload_picked_as_the_program_runs_rejects_an_unchecked_function.ndc b/tests/functional/programs/017_dynamic_any/017_an_overload_picked_as_the_program_runs_rejects_an_unchecked_function.ndc new file mode 100644 index 00000000..8fd7d398 --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/017_an_overload_picked_as_the_program_runs_rejects_an_unchecked_function.ndc @@ -0,0 +1,8 @@ +// An overload picked as the program runs doesn't take a function of type +// `Any` whose own type doesn't show it fits the parameter. +// expect-error: no function called 'h' found matches the arguments +fn h(f: Fn(Int) -> Int) => f(1) + 1; +fn h(x: Int) => x; +fn text(x) => "s"; +let a: Any = text; +print(h(a)); diff --git a/tests/functional/programs/017_dynamic_any/018_a_checked_function_returning_a_function_checks_that_one_too.ndc b/tests/functional/programs/017_dynamic_any/018_a_checked_function_returning_a_function_checks_that_one_too.ndc new file mode 100644 index 00000000..1b386aee --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/018_a_checked_function_returning_a_function_checks_that_one_too.ndc @@ -0,0 +1,8 @@ +// A checked function that returns a function makes that one checked too, when +// its own type doesn't show it fits. +// expect-output: 1 +fn id(x) => x; +fn make() => id; +let a: Any = make; +let f = a as Fn() -> Fn(Int) -> Int; +print(f()(1)); diff --git a/tests/functional/programs/017_dynamic_any/019_a_function_that_never_returns_fits_any_return_type.ndc b/tests/functional/programs/017_dynamic_any/019_a_function_that_never_returns_fits_any_return_type.ndc new file mode 100644 index 00000000..816d6da1 --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/019_a_function_that_never_returns_fits_any_return_type.ndc @@ -0,0 +1,6 @@ +// A function that never returns can be used as one returning anything. +// expect-output: ok +fn forever() -> Never { forever() } +let a: Any = forever; +let f = a as Fn() -> Int; +print("ok"); diff --git a/tests/functional/programs/017_dynamic_any/020_recursion_through_a_checked_function_runs_deep.ndc b/tests/functional/programs/017_dynamic_any/020_recursion_through_a_checked_function_runs_deep.ndc new file mode 100644 index 00000000..3fb5b0bd --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/020_recursion_through_a_checked_function_runs_deep.ndc @@ -0,0 +1,8 @@ +// A checked function's calls run on the VM's own stack, so recursion through +// one goes as deep as any other. +// expect-output: 0 +let f: Fn(Int) -> Int = fn(x: Int) => 0; +fn step(x: Int) { if x == 0 { 0 } else { f(x - 1) } } +let a: Any = step; +f = a; +print(f(10000)); diff --git a/tests/functional/programs/017_dynamic_any/022_a_function_returning_a_list_fits_one_returning_an_iterable.ndc b/tests/functional/programs/017_dynamic_any/022_a_function_returning_a_list_fits_one_returning_an_iterable.ndc new file mode 100644 index 00000000..e81571e4 --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/022_a_function_returning_a_list_fits_one_returning_an_iterable.ndc @@ -0,0 +1,7 @@ +// A function whose own type says it returns a list of Ints fits a function +// type that returns one, without checking its calls. +// expect-output: 1 +fn h(f: Fn(Int) -> List) => 1; +fn h(x: Int) => 0; +let a: Any = fn(x: Int) => [x]; +print(h(a)); diff --git a/tests/functional/programs/017_dynamic_any/023_a_checked_function_checks_its_arguments.ndc b/tests/functional/programs/017_dynamic_any/023_a_checked_function_checks_its_arguments.ndc new file mode 100644 index 00000000..90bdfc7f --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/023_a_checked_function_checks_its_arguments.ndc @@ -0,0 +1,9 @@ +// A function used as a function type that takes more than it does checks +// each call's arguments: `store` takes an Int, not anything. +// expect-error: the function doesn't take the arguments (Bool) +let n: Int = 0; +fn store(x: Int) { n = x; 0 } +let a: Any = store; +let g: Any = a as Fn(Any) -> Int; +g(true); +print(n); diff --git a/tests/functional/programs/017_dynamic_any/024_a_pure_function_tells_checked_functions_apart.ndc b/tests/functional/programs/017_dynamic_any/024_a_pure_function_tells_checked_functions_apart.ndc new file mode 100644 index 00000000..7f4429b1 --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/024_a_pure_function_tells_checked_functions_apart.ndc @@ -0,0 +1,11 @@ +// A function checked as two function types gives two arguments a `pure fn` +// caches separately. +// expect-output: ok +pure fn cached(f: Any) => f; +fn id(x) => x; +let a: Any = id; +let ints = a as Fn(Int) -> Int; +let strs = a as Fn(String) -> String; +cached(ints); +let s: Fn(String) -> String = cached(strs); +print(s("ok")); diff --git a/tests/functional/programs/018_literals/001_an_integer_literal_is_an_int_unless_something_says_otherwise.ndc b/tests/functional/programs/018_literals/001_an_integer_literal_is_an_int_unless_something_says_otherwise.ndc new file mode 100644 index 00000000..80ba1670 --- /dev/null +++ b/tests/functional/programs/018_literals/001_an_integer_literal_is_an_int_unless_something_says_otherwise.ndc @@ -0,0 +1,2 @@ +// expect-output: 3 +print(7 / 2); diff --git a/tests/functional/programs/018_literals/002_an_integer_literal_takes_the_type_a_use_needs.ndc b/tests/functional/programs/018_literals/002_an_integer_literal_takes_the_type_a_use_needs.ndc new file mode 100644 index 00000000..0a15b39a --- /dev/null +++ b/tests/functional/programs/018_literals/002_an_integer_literal_takes_the_type_a_use_needs.ndc @@ -0,0 +1,4 @@ +// The annotation makes `7` a Float, so the division is a Float's. +// expect-output: 3.5 +let f: Float = 7; +print(f / 2); diff --git a/tests/functional/programs/018_literals/003_an_integer_literal_can_be_a_number.ndc b/tests/functional/programs/018_literals/003_an_integer_literal_can_be_a_number.ndc new file mode 100644 index 00000000..94db0d13 --- /dev/null +++ b/tests/functional/programs/018_literals/003_an_integer_literal_can_be_a_number.ndc @@ -0,0 +1,3 @@ +// expect-output: 1/3 +let n: Number = 1; +print(n / 3); diff --git a/tests/functional/programs/018_literals/004_a_variable_holding_a_literal_takes_what_it_is_given.ndc b/tests/functional/programs/018_literals/004_a_variable_holding_a_literal_takes_what_it_is_given.ndc new file mode 100644 index 00000000..93275c28 --- /dev/null +++ b/tests/functional/programs/018_literals/004_a_variable_holding_a_literal_takes_what_it_is_given.ndc @@ -0,0 +1,5 @@ +// `+` with a Float can't give `total` an Int back, so `0` is a Float. +// expect-output: 2.5 +let total = 0; +total += 2.5; +print(total); diff --git a/tests/functional/programs/018_literals/005_an_accumulator_takes_the_type_of_the_elements.ndc b/tests/functional/programs/018_literals/005_an_accumulator_takes_the_type_of_the_elements.ndc new file mode 100644 index 00000000..833fd8cc --- /dev/null +++ b/tests/functional/programs/018_literals/005_an_accumulator_takes_the_type_of_the_elements.ndc @@ -0,0 +1,3 @@ +// `fold` passes its result back to the lambda, so `0` is a Float. +// expect-output: 3.75 +print([1.5, 2.25].fold(0, fn(acc, x) => acc + x)); diff --git a/tests/functional/programs/018_literals/006_a_list_of_numbers_takes_the_type_they_share.ndc b/tests/functional/programs/018_literals/006_a_list_of_numbers_takes_the_type_they_share.ndc new file mode 100644 index 00000000..e0e6d901 --- /dev/null +++ b/tests/functional/programs/018_literals/006_a_list_of_numbers_takes_the_type_they_share.ndc @@ -0,0 +1,4 @@ +// `1` is a Float like `1.5`, so dividing it gives a Float. +// expect-output: 0.5 +let xs = [1.5, 1]; +print(xs[1] / 2); diff --git a/tests/functional/programs/018_literals/007_a_list_literal_takes_its_annotated_type.ndc b/tests/functional/programs/018_literals/007_a_list_literal_takes_its_annotated_type.ndc new file mode 100644 index 00000000..c8039c45 --- /dev/null +++ b/tests/functional/programs/018_literals/007_a_list_literal_takes_its_annotated_type.ndc @@ -0,0 +1,4 @@ +// expect-output: [1,2,"three"] +let xs: List = [1, 2]; +xs.push("three"); +print(xs); diff --git a/tests/functional/programs/018_literals/008_a_literal_of_values_that_share_no_type_holds_any.ndc b/tests/functional/programs/018_literals/008_a_literal_of_values_that_share_no_type_holds_any.ndc new file mode 100644 index 00000000..ad69d45c --- /dev/null +++ b/tests/functional/programs/018_literals/008_a_literal_of_values_that_share_no_type_holds_any.ndc @@ -0,0 +1,4 @@ +// expect-output: [1,"a",2.5] +let xs = [1, "a"]; +xs.push(2.5); +print(xs); diff --git a/tests/functional/programs/018_literals/009_a_map_literal_takes_its_annotated_type.ndc b/tests/functional/programs/018_literals/009_a_map_literal_takes_its_annotated_type.ndc new file mode 100644 index 00000000..a0573c42 --- /dev/null +++ b/tests/functional/programs/018_literals/009_a_map_literal_takes_its_annotated_type.ndc @@ -0,0 +1,4 @@ +// expect-output: 1 two +let m: Map = %{"a": 1}; +m["b"] = "two"; +print(m["a"], m["b"]); diff --git a/tests/functional/programs/018_literals/010_a_tuple_literal_takes_its_annotated_type.ndc b/tests/functional/programs/018_literals/010_a_tuple_literal_takes_its_annotated_type.ndc new file mode 100644 index 00000000..73f8a843 --- /dev/null +++ b/tests/functional/programs/018_literals/010_a_tuple_literal_takes_its_annotated_type.ndc @@ -0,0 +1,3 @@ +// expect-output: ("a",1) +let t: (Any, Int) = ("a", 1); +print(t); diff --git a/tests/functional/programs/018_literals/011_two_literals_of_different_types_are_an_error.ndc b/tests/functional/programs/018_literals/011_two_literals_of_different_types_are_an_error.ndc new file mode 100644 index 00000000..8396a7a4 --- /dev/null +++ b/tests/functional/programs/018_literals/011_two_literals_of_different_types_are_an_error.ndc @@ -0,0 +1,4 @@ +// `v` is a list of Strings, so it can't be given a list of Ints. +// expect-error: mismatched types: found List but expected List +let v = ["s"]; +v = [1]; diff --git a/tests/functional/programs/018_literals/012_a_list_of_ints_is_not_a_list_of_any.ndc b/tests/functional/programs/018_literals/012_a_list_of_ints_is_not_a_list_of_any.ndc new file mode 100644 index 00000000..5a7de16f --- /dev/null +++ b/tests/functional/programs/018_literals/012_a_list_of_ints_is_not_a_list_of_any.ndc @@ -0,0 +1,5 @@ +// A List could get a String, which the List would then hold. +// Converting goes through `Any`. +// expect-error: mismatched types: found List but expected List +let words = "a b".split(" "); +let anything: List = words; diff --git a/tests/functional/programs/018_literals/013_a_list_goes_through_any_to_change_its_element_type.ndc b/tests/functional/programs/018_literals/013_a_list_goes_through_any_to_change_its_element_type.ndc new file mode 100644 index 00000000..9343a5ce --- /dev/null +++ b/tests/functional/programs/018_literals/013_a_list_goes_through_any_to_change_its_element_type.ndc @@ -0,0 +1,6 @@ +// expect-output: ["a","b",1] +let words = "a b".split(" "); +let any: Any = words; +let anything: List = any; +anything.push(1); +print(anything); diff --git a/tests/functional/programs/018_literals/014_a_function_type_is_invariant.ndc b/tests/functional/programs/018_literals/014_a_function_type_is_invariant.ndc new file mode 100644 index 00000000..fe288258 --- /dev/null +++ b/tests/functional/programs/018_literals/014_a_function_type_is_invariant.ndc @@ -0,0 +1,5 @@ +// A function taking Any isn't a function taking an Int: the program +// couldn't check what a call through it returns. +// expect-error: mismatched types: found Fn(Any) -> Any but expected Fn(Int) -> Any +fn f(x: Any) => x; +let g: Fn(Int) -> Any = f; diff --git a/tests/functional/programs/018_literals/015_the_first_use_whose_pick_depends_on_a_literal_decides_it.ndc b/tests/functional/programs/018_literals/015_the_first_use_whose_pick_depends_on_a_literal_decides_it.ndc new file mode 100644 index 00000000..feb122ca --- /dev/null +++ b/tests/functional/programs/018_literals/015_the_first_use_whose_pick_depends_on_a_literal_decides_it.ndc @@ -0,0 +1,9 @@ +// `describe(x)` takes the Strings, but would lose to `describe(x: Int)` if +// the list held `Any`, so `map` decides that the list holds Strings. It +// can't become a List after that. +// expect-error: mismatched types: found List but expected List +fn describe(x: Int) => "int"; +fn describe(x) => "other"; +let words = ["a"]; +print(words.map(describe)); +let anything: List = words; diff --git a/tests/functional/programs/018_literals/016_a_use_that_picks_the_same_whatever_a_literal_is_leaves_it_open.ndc b/tests/functional/programs/018_literals/016_a_use_that_picks_the_same_whatever_a_literal_is_leaves_it_open.ndc new file mode 100644 index 00000000..001f4659 --- /dev/null +++ b/tests/functional/programs/018_literals/016_a_use_that_picks_the_same_whatever_a_literal_is_leaves_it_open.ndc @@ -0,0 +1,6 @@ +// `push` is the same whatever the list holds, so the Float that's pushed +// later decides that `1` is a Float too. +// expect-output: 0.5 +let xs = [1]; +xs.push(1.5); +print(xs[0] / 2); diff --git a/tests/functional/programs/018_literals/017_a_map_default_function_returning_any_is_not_checked_as_a_value.ndc b/tests/functional/programs/018_literals/017_a_map_default_function_returning_any_is_not_checked_as_a_value.ndc new file mode 100644 index 00000000..f0832557 --- /dev/null +++ b/tests/functional/programs/018_literals/017_a_map_default_function_returning_any_is_not_checked_as_a_value.ndc @@ -0,0 +1,6 @@ +// The map calls `fallback` for a missing key, so the function itself isn't +// one of the map's Ints. +// expect-output: 1 +fn fallback() -> Any => 1; +let m: Map = %{:fallback}; +print(m["a"]); diff --git a/tests/functional/programs/018_literals/018_branches_of_different_types_join_as_any.ndc b/tests/functional/programs/018_literals/018_branches_of_different_types_join_as_any.ndc new file mode 100644 index 00000000..3f64e407 --- /dev/null +++ b/tests/functional/programs/018_literals/018_branches_of_different_types_join_as_any.ndc @@ -0,0 +1,6 @@ +// The `1` becomes a Float like the other branch, but a String and an Int +// have no type in common but `Any`. +// expect-output: 1.0 1 +print(if true { 1 } else { 2.5 }, if true { 1 } else { "a" }); +let x = if false { 1 } else { "a" }; +x = 2.5; diff --git a/tests/functional/programs/018_literals/019_branches_of_one_shape_are_made_one_type.ndc b/tests/functional/programs/018_literals/019_branches_of_one_shape_are_made_one_type.ndc new file mode 100644 index 00000000..42fb2566 --- /dev/null +++ b/tests/functional/programs/018_literals/019_branches_of_one_shape_are_made_one_type.ndc @@ -0,0 +1,5 @@ +// The two lists are one type, so `e` holds Ints like `[1]`. +// expect-error: No function called 'push' found that matches the arguments 'List, String' +let e = []; +let r = if true { e } else { [1] }; +e.push("s"); diff --git a/tests/functional/programs/018_literals/020_an_empty_branch_takes_the_other_branch_s_type.ndc b/tests/functional/programs/018_literals/020_an_empty_branch_takes_the_other_branch_s_type.ndc new file mode 100644 index 00000000..642c3ddf --- /dev/null +++ b/tests/functional/programs/018_literals/020_an_empty_branch_takes_the_other_branch_s_type.ndc @@ -0,0 +1,4 @@ +// expect-error: No function called 'push' found that matches the arguments 'List, String' +let xs = ["a".len()]; +let r = if true { xs } else { [] }; +r.push("s"); diff --git a/tests/functional/programs/018_literals/021_branches_of_literals_that_each_hold_any_are_one_type.ndc b/tests/functional/programs/018_literals/021_branches_of_literals_that_each_hold_any_are_one_type.ndc new file mode 100644 index 00000000..0e128256 --- /dev/null +++ b/tests/functional/programs/018_literals/021_branches_of_literals_that_each_hold_any_are_one_type.ndc @@ -0,0 +1,6 @@ +// Each inner list holds values that share no type, so each is a List, +// and the branches are one type: a List>. +// expect-output: nested +fn f(x: List>) => "nested"; +fn f(x: Any) => "any"; +print(f(if true { [[1, "a"]] } else { [[2, "b"]] })); diff --git a/tests/functional/programs/018_literals/022_a_literal_takes_the_any_of_a_variable_holding_any.ndc b/tests/functional/programs/018_literals/022_a_literal_takes_the_any_of_a_variable_holding_any.ndc new file mode 100644 index 00000000..cedeec2f --- /dev/null +++ b/tests/functional/programs/018_literals/022_a_literal_takes_the_any_of_a_variable_holding_any.ndc @@ -0,0 +1,6 @@ +// `v` holds `Any`s, so the list given to it does too. +// expect-output: ["b",2.5] +let v = [1, "a"]; +v = ["b"]; +v.push(2.5); +print(v); diff --git a/tests/functional/programs/018_literals/023_an_index_write_lets_a_list_literal_adapt.ndc b/tests/functional/programs/018_literals/023_an_index_write_lets_a_list_literal_adapt.ndc new file mode 100644 index 00000000..60c159a3 --- /dev/null +++ b/tests/functional/programs/018_literals/023_an_index_write_lets_a_list_literal_adapt.ndc @@ -0,0 +1,10 @@ +// The `1` and `2` are Floats, as adding 0.5 to an element of the list +// gives a Float that's written back into it. +// expect-output: [1.5,2.0] [[1.0,2.5]] %{"a": 1.5} +let xs = [1, 2]; +xs[0] += 0.5; +let grid = [[1, 2]]; +grid[0][1] = grid[0][1] + 0.5; +let m = %{"a": 1}; +m["a"] += 0.5; +print(xs, grid, m); diff --git a/tests/functional/programs/018_literals/024_a_read_element_takes_the_type_its_uses_need.ndc b/tests/functional/programs/018_literals/024_a_read_element_takes_the_type_its_uses_need.ndc new file mode 100644 index 00000000..4a6eda93 --- /dev/null +++ b/tests/functional/programs/018_literals/024_a_read_element_takes_the_type_its_uses_need.ndc @@ -0,0 +1,10 @@ +// What `[]` and `first` give back is what the list holds, so a use of the +// element that needs a Float makes the list's `1` a Float too. +// expect-output: [1.0] 0.5 [1.0,1.5] +let xs = [1]; +let x = xs[0]; +x = 0.5; +let ys = [1]; +let y = ys.first() + 0.5; +ys.push(y); +print(xs, x, ys); diff --git a/tests/functional/programs/018_literals/025_the_values_of_a_literal_take_one_type.ndc b/tests/functional/programs/018_literals/025_the_values_of_a_literal_take_one_type.ndc new file mode 100644 index 00000000..6ed5e1bb --- /dev/null +++ b/tests/functional/programs/018_literals/025_the_values_of_a_literal_take_one_type.ndc @@ -0,0 +1,5 @@ +// The `1` and `2` are of one type, so no type they can both be takes a +// String: the list doesn't become a list of Any. +// expect-error: No function called 'push' found that matches the arguments 'List, String' +let xs = [1, 2]; +xs.push("a"); diff --git a/tests/functional/programs/018_literals/026_a_literal_s_use_picks_its_overload_by_scope.ndc b/tests/functional/programs/018_literals/026_a_literal_s_use_picks_its_overload_by_scope.ndc new file mode 100644 index 00000000..355e9fbe --- /dev/null +++ b/tests/functional/programs/018_literals/026_a_literal_s_use_picks_its_overload_by_scope.ndc @@ -0,0 +1,10 @@ +// The inner `f` takes the call, so it doesn't matter that the outer one +// would give a String back for an Int: the `1` stays an Int. +// expect-output: 1 0.5 +fn f(x: Int) => "outer"; +{ + fn f(x: Any) => 0.5; + let n = 1; + let r: Float = f(n); + print(n, r); +} diff --git a/tests/functional/programs/018_literals/027_a_literal_doesn_t_become_a_float_to_reach_an_any_overload.ndc b/tests/functional/programs/018_literals/027_a_literal_doesn_t_become_a_float_to_reach_an_any_overload.ndc new file mode 100644 index 00000000..a886e8b6 --- /dev/null +++ b/tests/functional/programs/018_literals/027_a_literal_doesn_t_become_a_float_to_reach_an_any_overload.ndc @@ -0,0 +1,7 @@ +// `[](List, Int)` gives an Int, which isn't a String. A Float index would +// reach `[](List, Any)`, but that overload is a fallback, so the `0` +// stays an Int and the mismatch is reported. +// expect-error: mismatched types: found Int but expected String +let ys = ["a".len()]; +let cs = [x for x in ys]; +let e: String = cs[0]; diff --git a/tests/functional/programs/018_literals/028_an_outer_number_overload_doesn_t_hold_a_literal_back.ndc b/tests/functional/programs/018_literals/028_an_outer_number_overload_doesn_t_hold_a_literal_back.ndc new file mode 100644 index 00000000..21a16ac8 --- /dev/null +++ b/tests/functional/programs/018_literals/028_an_outer_number_overload_doesn_t_hold_a_literal_back.ndc @@ -0,0 +1,11 @@ +// The inner `f` takes the call whatever `n` is, so the outer `f(Int)` says +// nothing about `n`, which `n += 0.5` makes a Float. +// expect-output: 0.5 +fn f(x: Int) => "outer"; +{ + fn f(x: Any) => 0.5; + let n = 0; + f(n); + n += 0.5; + print(n); +} diff --git a/tests/functional/programs/019_iterable/001_a_parameter_without_an_annotation_takes_any_kind_of_sequence.ndc b/tests/functional/programs/019_iterable/001_a_parameter_without_an_annotation_takes_any_kind_of_sequence.ndc new file mode 100644 index 00000000..aa5cd486 --- /dev/null +++ b/tests/functional/programs/019_iterable/001_a_parameter_without_an_annotation_takes_any_kind_of_sequence.ndc @@ -0,0 +1,11 @@ +// A parameter without an annotation takes any kind of sequence its body can +// iterate, and each kind gets its own copy. +// expect-output: 3 7 6 +fn total(xs) { + let sum = 0; + for x in xs { + sum += x; + } + sum +} +print(total([1, 2]), total((3, 4)), total(1..4)); diff --git a/tests/functional/programs/019_iterable/002_a_sequence_of_elements_the_body_cannot_use_is_an_error.ndc b/tests/functional/programs/019_iterable/002_a_sequence_of_elements_the_body_cannot_use_is_an_error.ndc new file mode 100644 index 00000000..27d9f0a9 --- /dev/null +++ b/tests/functional/programs/019_iterable/002_a_sequence_of_elements_the_body_cannot_use_is_an_error.ndc @@ -0,0 +1,10 @@ +// What a sequence holds has to work in the body. +// expect-error: No function called '+' found that matches the arguments 'Int, String' +fn total(xs) { + let sum = 0; + for x in xs { + sum += x; + } + sum +} +total(["a"]); diff --git a/tests/functional/programs/019_iterable/003_an_iterable_cannot_be_written_in_an_annotation.ndc b/tests/functional/programs/019_iterable/003_an_iterable_cannot_be_written_in_an_annotation.ndc new file mode 100644 index 00000000..0873088a --- /dev/null +++ b/tests/functional/programs/019_iterable/003_an_iterable_cannot_be_written_in_an_annotation.ndc @@ -0,0 +1,3 @@ +// A value has a kind of sequence, which an annotation has to say. +// expect-error: `Iterable` can't be written in a type annotation +let xs: Iterable = [1]; diff --git a/tests/functional/programs/019_iterable/004_a_cast_cannot_be_to_an_iterable.ndc b/tests/functional/programs/019_iterable/004_a_cast_cannot_be_to_an_iterable.ndc new file mode 100644 index 00000000..0fcbecc6 --- /dev/null +++ b/tests/functional/programs/019_iterable/004_a_cast_cannot_be_to_an_iterable.ndc @@ -0,0 +1,2 @@ +// expect-error: `Iterable` can't be written in a type annotation +print([1] as Iterable); diff --git a/tests/functional/programs/019_iterable/005_a_tuple_of_different_types_iterates_as_any.ndc b/tests/functional/programs/019_iterable/005_a_tuple_of_different_types_iterates_as_any.ndc new file mode 100644 index 00000000..a9094d84 --- /dev/null +++ b/tests/functional/programs/019_iterable/005_a_tuple_of_different_types_iterates_as_any.ndc @@ -0,0 +1,5 @@ +// expect-output: 1 +// expect-output: a +for x in (1, "a") { + print(x); +} diff --git a/tests/functional/programs/019_iterable/006_a_call_waits_for_what_it_iterates.ndc b/tests/functional/programs/019_iterable/006_a_call_waits_for_what_it_iterates.ndc new file mode 100644 index 00000000..98ca98e3 --- /dev/null +++ b/tests/functional/programs/019_iterable/006_a_call_waits_for_what_it_iterates.ndc @@ -0,0 +1,5 @@ +// `len` takes any kind of sequence, so `f` waits for its calls to say what +// `xs` is, and each gets its own copy. +// expect-output: 2 3 +fn f(xs) => xs.len(); +print(f("ab"), f([1, 2, 3])); diff --git a/tests/functional/programs/019_iterable/007_a_callback_may_return_any_kind_of_sequence.ndc b/tests/functional/programs/019_iterable/007_a_callback_may_return_any_kind_of_sequence.ndc new file mode 100644 index 00000000..efa5ae82 --- /dev/null +++ b/tests/functional/programs/019_iterable/007_a_callback_may_return_any_kind_of_sequence.ndc @@ -0,0 +1,2 @@ +// expect-output: [1,1,2,2] ["a","b"] +print([1, 2].flat_map(fn(x) => [x, x]), ["ab"].flat_map(fn(s) => s)); diff --git a/tests/functional/programs/019_iterable/008_a_function_taking_an_iterable_is_a_value.ndc b/tests/functional/programs/019_iterable/008_a_function_taking_an_iterable_is_a_value.ndc new file mode 100644 index 00000000..466be680 --- /dev/null +++ b/tests/functional/programs/019_iterable/008_a_function_taking_an_iterable_is_a_value.ndc @@ -0,0 +1,2 @@ +// expect-output: [1,2] +print([[1], [2, 3]].map(len)); diff --git a/tests/functional/programs/019_iterable/011_a_callback_parameter_may_return_any_kind_of_sequence.ndc b/tests/functional/programs/019_iterable/011_a_callback_parameter_may_return_any_kind_of_sequence.ndc new file mode 100644 index 00000000..0198fe69 --- /dev/null +++ b/tests/functional/programs/019_iterable/011_a_callback_parameter_may_return_any_kind_of_sequence.ndc @@ -0,0 +1,4 @@ +// A callback passed on to `flat_map` may return any kind of sequence. +// expect-output: [1,1,2,2] [1,2] +fn apply(f) => [1, 2].flat_map(f); +print(apply(fn(x) => [x, x]), apply(fn(x) => (x,))); diff --git a/tests/functional/programs/019_iterable/012_iterating_a_tuple_does_not_make_its_elements_one_type.ndc b/tests/functional/programs/019_iterable/012_iterating_a_tuple_does_not_make_its_elements_one_type.ndc new file mode 100644 index 00000000..7570ba8c --- /dev/null +++ b/tests/functional/programs/019_iterable/012_iterating_a_tuple_does_not_make_its_elements_one_type.ndc @@ -0,0 +1,8 @@ +// expect-output: true +// expect-output: a +fn visit(a, b) { + for x in (a, b) { + print(x); + } +} +visit(true, "a"); diff --git a/tests/functional/programs/019_iterable/013_a_bool_is_not_iterable.ndc b/tests/functional/programs/019_iterable/013_a_bool_is_not_iterable.ndc new file mode 100644 index 00000000..0f6a4c80 --- /dev/null +++ b/tests/functional/programs/019_iterable/013_a_bool_is_not_iterable.ndc @@ -0,0 +1,4 @@ +// expect-error: Bool is not iterable +for x in true { + print(x); +} diff --git a/tests/functional/programs/019_iterable/014_what_a_callback_returns_is_checked_to_iterate.ndc b/tests/functional/programs/019_iterable/014_what_a_callback_returns_is_checked_to_iterate.ndc new file mode 100644 index 00000000..ad7e0d8d --- /dev/null +++ b/tests/functional/programs/019_iterable/014_what_a_callback_returns_is_checked_to_iterate.ndc @@ -0,0 +1,4 @@ +// `flat_map` iterates what its callback returns, which is `xs`. +// expect-error: Bool is not iterable +fn flatten(xs) => [0].flat_map(fn(_) => xs); +flatten(true); diff --git a/tests/functional/programs/019_iterable/016_a_parameter_cannot_be_annotated_as_an_iterable.ndc b/tests/functional/programs/019_iterable/016_a_parameter_cannot_be_annotated_as_an_iterable.ndc new file mode 100644 index 00000000..e7b0d454 --- /dev/null +++ b/tests/functional/programs/019_iterable/016_a_parameter_cannot_be_annotated_as_an_iterable.ndc @@ -0,0 +1,3 @@ +// A parameter without an annotation takes any kind of sequence already. +// expect-error: `Iterable` can't be written in a type annotation +fn total(xs: Iterable) => 0; diff --git a/tests/functional/programs/019_iterable/017_a_literal_passed_for_an_iterable_is_the_sequence_it_holds.ndc b/tests/functional/programs/019_iterable/017_a_literal_passed_for_an_iterable_is_the_sequence_it_holds.ndc new file mode 100644 index 00000000..ab4e08fb --- /dev/null +++ b/tests/functional/programs/019_iterable/017_a_literal_passed_for_an_iterable_is_the_sequence_it_holds.ndc @@ -0,0 +1,7 @@ +// A list literal passed where any sequence is expected is still a list, so +// `++` takes it afterwards. +// expect-output: ["A","A"] +let t = ("A", ["A"]); +let cur, path = t; +let n = path.len; +print(path ++ [cur]); diff --git a/tests/functional/programs/020_tuples/001_a_literal_index_gives_the_element_s_type.ndc b/tests/functional/programs/020_tuples/001_a_literal_index_gives_the_element_s_type.ndc new file mode 100644 index 00000000..a0f30b84 --- /dev/null +++ b/tests/functional/programs/020_tuples/001_a_literal_index_gives_the_element_s_type.ndc @@ -0,0 +1,11 @@ +// `t[1]` picks the second element, so it's a String, and `t[-2]` the +// first, an Int. A generic function gets each call's element types. +// expect-output: a 2 x ("x",1) +fn first(t) => t[0]; +fn swap(t) => (t[1], t[0]); +let t = (1, "a"); +let s: String = t[1]; +let n: Int = t[-2] + 1; +let f: String = first(("x", 2)); +let p: (String, Int) = swap((1, "x")); +print(s, n, f, p); diff --git a/tests/functional/programs/020_tuples/002_a_literal_index_past_the_end_is_an_error.ndc b/tests/functional/programs/020_tuples/002_a_literal_index_past_the_end_is_an_error.ndc new file mode 100644 index 00000000..8b05a274 --- /dev/null +++ b/tests/functional/programs/020_tuples/002_a_literal_index_past_the_end_is_an_error.ndc @@ -0,0 +1,3 @@ +// expect-error: Index -3 is out of bounds for a tuple of 2 elements +let t = (1, "a"); +print(t[-3]); diff --git a/tests/functional/programs/020_tuples/003_an_element_has_its_own_type.ndc b/tests/functional/programs/020_tuples/003_an_element_has_its_own_type.ndc new file mode 100644 index 00000000..373f880c --- /dev/null +++ b/tests/functional/programs/020_tuples/003_an_element_has_its_own_type.ndc @@ -0,0 +1,4 @@ +// The first element is an Int, which isn't a String. +// expect-error: mismatched types: found Int but expected String +let t = (1, "a"); +let s: String = t[0]; diff --git a/tests/functional/programs/020_tuples/004_a_tuple_of_values_still_waiting_on_a_literal_iterates_as_its_type.ndc b/tests/functional/programs/020_tuples/004_a_tuple_of_values_still_waiting_on_a_literal_iterates_as_its_type.ndc new file mode 100644 index 00000000..01d972de --- /dev/null +++ b/tests/functional/programs/020_tuples/004_a_tuple_of_values_still_waiting_on_a_literal_iterates_as_its_type.ndc @@ -0,0 +1,6 @@ +// A tuple whose elements wait for a literal's type iterates as that type +// once it's known, as a list does. +// expect-output: [2,5] [2,5] +let x = 3; +let y = 4; +print((x - 1, y + 1).map(abs), [x - 1, y + 1].map(abs)); diff --git a/tests/functional/programs/602_stdlib_string/008_strip_prefix.ndc b/tests/functional/programs/602_stdlib_string/008_strip_prefix.ndc index cb9803a7..9a42959e 100644 --- a/tests/functional/programs/602_stdlib_string/008_strip_prefix.ndc +++ b/tests/functional/programs/602_stdlib_string/008_strip_prefix.ndc @@ -1,12 +1,13 @@ assert_eq(strip_prefix("foobar", "foo"), "bar"); +assert_eq("foobar".strip_prefix?("foo"), Some("bar")); assert_eq("foofoo".strip_prefix("foo"), "foo"); -assert_eq("foobar".strip_prefix("bar"), ()); -assert_eq("foo".strip_prefix("foobar"), ()); -assert_eq("Foo".strip_prefix("foo"), ()); +assert_eq("foobar".strip_prefix?("bar"), None); +assert_eq("foo".strip_prefix?("foobar"), None); +assert_eq("Foo".strip_prefix?("foo"), None); assert_eq("foo".strip_prefix("foo"), ""); assert_eq("foo".strip_prefix(""), "foo"); assert_eq("".strip_prefix(""), ""); -assert_eq("".strip_prefix("foo"), ()); +assert_eq("".strip_prefix?("foo"), None); assert_eq("❤你好".strip_prefix("❤"), "你好"); let original = "foobar"; let stripped = original.strip_prefix("foo"); diff --git a/tests/functional/programs/602_stdlib_string/009_strip_suffix.ndc b/tests/functional/programs/602_stdlib_string/009_strip_suffix.ndc index 730b657a..9d1a4ebc 100644 --- a/tests/functional/programs/602_stdlib_string/009_strip_suffix.ndc +++ b/tests/functional/programs/602_stdlib_string/009_strip_suffix.ndc @@ -1,12 +1,13 @@ assert_eq(strip_suffix("foobar", "bar"), "foo"); +assert_eq("foobar".strip_suffix?("bar"), Some("foo")); assert_eq("barbar".strip_suffix("bar"), "bar"); -assert_eq("foobar".strip_suffix("foo"), ()); -assert_eq("bar".strip_suffix("foobar"), ()); -assert_eq("BAR".strip_suffix("bar"), ()); +assert_eq("foobar".strip_suffix?("foo"), None); +assert_eq("bar".strip_suffix?("foobar"), None); +assert_eq("BAR".strip_suffix?("bar"), None); assert_eq("bar".strip_suffix("bar"), ""); assert_eq("bar".strip_suffix(""), "bar"); assert_eq("".strip_suffix(""), ""); -assert_eq("".strip_suffix("bar"), ()); +assert_eq("".strip_suffix?("bar"), None); assert_eq("你好❤".strip_suffix("❤"), "你好"); let original = "foobar"; let stripped = original.strip_suffix("bar"); diff --git a/tests/functional/programs/602_stdlib_string/010_strip_circumfix.ndc b/tests/functional/programs/602_stdlib_string/010_strip_circumfix.ndc index 2706700e..cd030edd 100644 --- a/tests/functional/programs/602_stdlib_string/010_strip_circumfix.ndc +++ b/tests/functional/programs/602_stdlib_string/010_strip_circumfix.ndc @@ -1,17 +1,18 @@ assert_eq(strip_circumfix("[hello]", "[", "]"), "hello"); +assert_eq("[hello]".strip_circumfix?("[", "]"), Some("hello")); assert_eq("[[hello]]".strip_circumfix("[", "]"), "[hello]"); assert_eq("hello".strip_circumfix("", ""), "hello"); -assert_eq("[hello".strip_circumfix("[", "]"), ()); -assert_eq("hello]".strip_circumfix("[", "]"), ()); -assert_eq("hello".strip_circumfix("[", "]"), ()); -assert_eq("aaa".strip_circumfix("aa", "aa"), ()); -assert_eq("a".strip_circumfix("a", "a"), ()); +assert_eq("[hello".strip_circumfix?("[", "]"), None); +assert_eq("hello]".strip_circumfix?("[", "]"), None); +assert_eq("hello".strip_circumfix?("[", "]"), None); +assert_eq("aaa".strip_circumfix?("aa", "aa"), None); +assert_eq("a".strip_circumfix?("a", "a"), None); assert_eq("[]".strip_circumfix("[", "]"), ""); assert_eq("hello".strip_circumfix("", ""), "hello"); assert_eq("hello]".strip_circumfix("", "]"), "hello"); assert_eq("[hello".strip_circumfix("[", ""), "hello"); assert_eq("".strip_circumfix("", ""), ""); -assert_eq("".strip_circumfix("[", "]"), ()); +assert_eq("".strip_circumfix?("[", "]"), None); assert_eq("「你好❤」".strip_circumfix("「", "」"), "你好❤"); let original = "[hello]"; let stripped = original.strip_circumfix("[", "]"); diff --git a/tests/functional/programs/602_stdlib_string/011_split_once.ndc b/tests/functional/programs/602_stdlib_string/011_split_once.ndc new file mode 100644 index 00000000..8dc8dbd3 --- /dev/null +++ b/tests/functional/programs/602_stdlib_string/011_split_once.ndc @@ -0,0 +1,7 @@ +assert_eq("key=value".split_once("="), ("key", "value")); +assert_eq("a=b=c".split_once("="), ("a", "b=c")); +assert_eq("=b".split_once("="), ("", "b")); +assert_eq("key=value".split_once?("="), Some(("key", "value"))); +assert_eq("keyvalue".split_once?("="), None); +let (key, value) = "port: 80".split_once(": "); +assert_eq(key ++ value, "port80"); diff --git a/tests/functional/programs/602_stdlib_string/012_split_once_without_the_pattern_is_an_error.ndc b/tests/functional/programs/602_stdlib_string/012_split_once_without_the_pattern_is_an_error.ndc new file mode 100644 index 00000000..bc5a2773 --- /dev/null +++ b/tests/functional/programs/602_stdlib_string/012_split_once_without_the_pattern_is_an_error.ndc @@ -0,0 +1,4 @@ +// `split_once?` returns `None` when the pattern doesn't occur; `split_once` +// reports it. +// expect-error: doesn't contain +"keyvalue".split_once("="); diff --git a/tests/functional/programs/602_stdlib_string/013_strip_prefix_without_a_match_is_an_error.ndc b/tests/functional/programs/602_stdlib_string/013_strip_prefix_without_a_match_is_an_error.ndc new file mode 100644 index 00000000..fd4c1a1e --- /dev/null +++ b/tests/functional/programs/602_stdlib_string/013_strip_prefix_without_a_match_is_an_error.ndc @@ -0,0 +1,4 @@ +// `strip_prefix?` returns `None` when the prefix doesn't match; `strip_prefix` +// reports it. +// expect-error: doesn't start with +"hello".strip_prefix("x"); diff --git a/tests/functional/programs/602_stdlib_string/014_strip_suffix_without_a_match_is_an_error.ndc b/tests/functional/programs/602_stdlib_string/014_strip_suffix_without_a_match_is_an_error.ndc new file mode 100644 index 00000000..981cacc6 --- /dev/null +++ b/tests/functional/programs/602_stdlib_string/014_strip_suffix_without_a_match_is_an_error.ndc @@ -0,0 +1,4 @@ +// `strip_suffix?` returns `None` when the suffix doesn't match; `strip_suffix` +// reports it. +// expect-error: doesn't end with +"hello".strip_suffix("x"); diff --git a/tests/functional/programs/602_stdlib_string/015_strip_circumfix_without_a_match_is_an_error.ndc b/tests/functional/programs/602_stdlib_string/015_strip_circumfix_without_a_match_is_an_error.ndc new file mode 100644 index 00000000..ff2361c7 --- /dev/null +++ b/tests/functional/programs/602_stdlib_string/015_strip_circumfix_without_a_match_is_an_error.ndc @@ -0,0 +1,4 @@ +// The prefix and suffix may not overlap, so this doesn't match. +// `strip_circumfix?` would return `None`; `strip_circumfix` reports it. +// expect-error: and end with +"aaa".strip_circumfix("aa", "aa"); diff --git a/tests/functional/programs/603_stdlib_seq/011_combinations.ndc b/tests/functional/programs/603_stdlib_seq/011_combinations.ndc index 8364eb41..63730ab9 100644 --- a/tests/functional/programs/603_stdlib_seq/011_combinations.ndc +++ b/tests/functional/programs/603_stdlib_seq/011_combinations.ndc @@ -1,18 +1,18 @@ // Basic 2-combinations of a list. -assert_eq(combinations([1, 2, 3], 2).list(), [(1, 2), (1, 3), (2, 3)]); +assert_eq(combinations([1, 2, 3], 2).list(), [[1, 2], [1, 3], [2, 3]]); // k=0 yields exactly one empty tuple. -assert_eq(combinations([1, 2], 0).list(), [()]); +assert_eq(combinations([1, 2], 0).list(), [[]]); // k > length yields nothing. assert_eq(combinations([1, 2], 5).list(), []); // Works on a tuple source. -assert_eq(combinations((10, 20, 30), 2).list(), [(10, 20), (10, 30), (20, 30)]); +assert_eq(combinations((10, 20, 30), 2).list(), [[10, 20], [10, 30], [20, 30]]); // Works on a map source (order is unspecified, test length only). assert_eq(combinations(%{"a": 1, "b": 2, "c": 3}, 2).list().len(), 3); // Lazy: take only the first combination from a large range iterator — the pool // is never fully materialised. -assert_eq(combinations(1..100, 2).take(1).list(), [(1, 2)]); +assert_eq(combinations(1..100, 2).take(1).list(), [[1, 2]]); diff --git a/tests/functional/programs/603_stdlib_seq/016_sequence_prefixes_suffixes.ndc b/tests/functional/programs/603_stdlib_seq/016_sequence_prefixes_suffixes.ndc index cbf75fcd..4fbaedb1 100644 --- a/tests/functional/programs/603_stdlib_seq/016_sequence_prefixes_suffixes.ndc +++ b/tests/functional/programs/603_stdlib_seq/016_sequence_prefixes_suffixes.ndc @@ -1,8 +1,8 @@ // The general overload must retain string behavior when the concrete kind is hidden. -let text: Sequence = "aé🦀"; +let text: Any = "aé🦀"; assert_eq(text.prefixes(), ["a", "aé", "aé🦀"]); assert_eq(text.suffixes(), ["aé🦀", "é🦀", "🦀"]); -let empty_text: Sequence = ""; +let empty_text: Any = ""; assert_eq(empty_text.prefixes(), []); assert_eq(empty_text.suffixes(), []); diff --git a/tests/functional/programs/603_stdlib_seq/017_map_returns_a_list_of_what_its_function_returns.ndc b/tests/functional/programs/603_stdlib_seq/017_map_returns_a_list_of_what_its_function_returns.ndc new file mode 100644 index 00000000..9d165691 --- /dev/null +++ b/tests/functional/programs/603_stdlib_seq/017_map_returns_a_list_of_what_its_function_returns.ndc @@ -0,0 +1,5 @@ +// `map` returns a list of what its function returns, so mapping numbers to +// strings gives a List, which only takes strings. +// expect-error: No function called 'push' found that matches the arguments 'List, Int' +let labels = [1, 2, 3].map(fn(n) => "label"); +labels.push(4); diff --git a/tests/functional/programs/603_stdlib_seq/018_filter_keeps_the_element_type.ndc b/tests/functional/programs/603_stdlib_seq/018_filter_keeps_the_element_type.ndc new file mode 100644 index 00000000..fffcd4ef --- /dev/null +++ b/tests/functional/programs/603_stdlib_seq/018_filter_keeps_the_element_type.ndc @@ -0,0 +1,5 @@ +// `filter` returns some of the elements it is given, so filtering a +// List gives a List, which only takes integers. +// expect-error: No function called 'push' found that matches the arguments 'List, String' +let big = [1, 2, 3].filter(fn(n) => n > 1); +big.push("4"); diff --git a/tests/functional/programs/603_stdlib_seq/019_max_min_and_in_look_at_the_keys_of_a_map.ndc b/tests/functional/programs/603_stdlib_seq/019_max_min_and_in_look_at_the_keys_of_a_map.ndc new file mode 100644 index 00000000..b081a0d7 --- /dev/null +++ b/tests/functional/programs/603_stdlib_seq/019_max_min_and_in_look_at_the_keys_of_a_map.ndc @@ -0,0 +1,7 @@ +// A map is a sequence of (key, value) pairs, but `max`, `min` and `in` look +// at its keys, so the max of a Map is a String. +// expect-output: b a true +let m = %{"a": 1, "b": 2}; +let highest: String = m.max(); +let lowest: String = m.min(); +print(highest, lowest, "a" in m); diff --git a/tests/functional/programs/603_stdlib_seq/020_zip_makes_tuples_as_long_as_the_number_of_lists.ndc b/tests/functional/programs/603_stdlib_seq/020_zip_makes_tuples_as_long_as_the_number_of_lists.ndc new file mode 100644 index 00000000..74ec505c --- /dev/null +++ b/tests/functional/programs/603_stdlib_seq/020_zip_makes_tuples_as_long_as_the_number_of_lists.ndc @@ -0,0 +1,11 @@ +// `zip` of two lists gives pairs of their elements, and `zip` of three lists +// gives triples. +// expect-output: a +// expect-output: 9 +// expect-output: 12 +let (number, letter) = zip([1, 2], ["a", "b"])[0]; +let text: String = letter; +print(text); +for (a, b, c) in zip([1, 2], [3, 4], [5, 6]) { + print(a + b + c); +} diff --git a/tests/functional/programs/603_stdlib_seq/021_filter_takes_a_predicate_returning_bool.ndc b/tests/functional/programs/603_stdlib_seq/021_filter_takes_a_predicate_returning_bool.ndc new file mode 100644 index 00000000..57bd9cf3 --- /dev/null +++ b/tests/functional/programs/603_stdlib_seq/021_filter_takes_a_predicate_returning_bool.ndc @@ -0,0 +1,4 @@ +// `filter` keeps the elements its predicate returns `true` for, so a +// predicate returning an Int is an error. +// expect-error: No function called 'filter' found that matches the arguments 'List, Fn(Int) -> Int' +[1, 2, 3].filter(fn(x) => 1); diff --git a/tests/functional/programs/603_stdlib_seq/022_lambdas_are_checked_against_what_a_function_passes_and_returns.ndc b/tests/functional/programs/603_stdlib_seq/022_lambdas_are_checked_against_what_a_function_passes_and_returns.ndc new file mode 100644 index 00000000..31008d11 --- /dev/null +++ b/tests/functional/programs/603_stdlib_seq/022_lambdas_are_checked_against_what_a_function_passes_and_returns.ndc @@ -0,0 +1,10 @@ +// A lambda with a typed parameter takes the list's elements, and an untyped +// one returning a value of unknown type is checked as the program runs, even +// when the list's type isn't known either. +fn big(xs) { xs.filter(fn(x) => x > 1) } +print([1, 2, 3].map(fn(x: Int) => x + 1)); +print(big([1, 2, 3])); +print([[true], [false]].filter(fn(x) => x[0])); +// expect-output: [2,3,4] +// expect-output: [2,3] +// expect-output: [[true]] diff --git a/tests/functional/programs/603_stdlib_seq/023_a_lambda_takes_its_parameter_types_from_the_call.ndc b/tests/functional/programs/603_stdlib_seq/023_a_lambda_takes_its_parameter_types_from_the_call.ndc new file mode 100644 index 00000000..6c593863 --- /dev/null +++ b/tests/functional/programs/603_stdlib_seq/023_a_lambda_takes_its_parameter_types_from_the_call.ndc @@ -0,0 +1,8 @@ +// `sort_by` passes the list's elements to the lambda, so `a.score` reads +// the field of `P`, and `map` makes a List that can't take a String. +// expect-error: No function called 'push' found that matches the arguments 'List, String' +struct P { score: Int } +let ps = [P(3), P(1)]; +ps.sort_by(fn(a, b) => a.score - b.score); +let scores = ps.map(fn(p) => p.score); +scores.push("a"); diff --git a/tests/functional/programs/603_stdlib_seq/024_zip_takes_any_kind_of_sequence.ndc b/tests/functional/programs/603_stdlib_seq/024_zip_takes_any_kind_of_sequence.ndc new file mode 100644 index 00000000..f921aac6 --- /dev/null +++ b/tests/functional/programs/603_stdlib_seq/024_zip_takes_any_kind_of_sequence.ndc @@ -0,0 +1,4 @@ +// `zip` takes any sequence, and stops at the shortest, so an endless range +// can be one of them. +// expect-output: [("a","c"),("b","d")] [(1,4),(2,5)] [(1,3,"x"),(2,4,"y")] +print(zip("ab", "cd"), zip(1..3, 4..), zip([1, 2], (3, 4), "xy")); diff --git a/tests/functional/programs/603_stdlib_seq/026_transposed_and_cartesian_products_keep_the_element_type.ndc b/tests/functional/programs/603_stdlib_seq/026_transposed_and_cartesian_products_keep_the_element_type.ndc new file mode 100644 index 00000000..2e2a3661 --- /dev/null +++ b/tests/functional/programs/603_stdlib_seq/026_transposed_and_cartesian_products_keep_the_element_type.ndc @@ -0,0 +1,6 @@ +// `transposed` and `multi_cartesian_product` give lists of the elements the +// rows hold, and sequences of mixed elements give lists of `Any`. +// expect-output: [[1,3],[2,4]] [[1,3],[2,3]] [[1,"a"],[2,"a"]] +let columns: List> = [[1, 2], [3, 4]].transposed(); +let products: List> = [[1, 2], [3]].multi_cartesian_product(); +print(columns, products, [[1, 2], ["a"]].multi_cartesian_product()); diff --git a/tests/functional/programs/603_stdlib_seq/027_zip_of_three_or_four_sequences_makes_typed_tuples.ndc b/tests/functional/programs/603_stdlib_seq/027_zip_of_three_or_four_sequences_makes_typed_tuples.ndc new file mode 100644 index 00000000..8894b392 --- /dev/null +++ b/tests/functional/programs/603_stdlib_seq/027_zip_of_three_or_four_sequences_makes_typed_tuples.ndc @@ -0,0 +1,5 @@ +// Zipping three or four sequences makes tuples of their element types. +// expect-output: [(1,"a",1.5)] [(1,"a",true,2)] +let triples: List<(Int, String, Float)> = zip([1], ["a"], [1.5]); +let quadruples: List<(Int, String, Bool, Int)> = zip([1], ["a"], [true], [2]); +print(triples, quadruples); diff --git a/tests/functional/programs/603_stdlib_seq/028_first_and_last_take_a_tuple.ndc b/tests/functional/programs/603_stdlib_seq/028_first_and_last_take_a_tuple.ndc new file mode 100644 index 00000000..e4b348cc --- /dev/null +++ b/tests/functional/programs/603_stdlib_seq/028_first_and_last_take_a_tuple.ndc @@ -0,0 +1,4 @@ +// `first` and `last` take a tuple, and give its first or last element's type. +// expect-output: 2 3.0 [(-1,0)] +let t = (1, "a", 2.5); +print(t.first + 1, t.last + 0.5, [((-1, 0), true), ((1, 0), false)].filter(fn(x) => x[1]).map(first)); diff --git a/tests/functional/programs/603_stdlib_seq/029_tuples_up_to_twelve_elements_are_typed.ndc b/tests/functional/programs/603_stdlib_seq/029_tuples_up_to_twelve_elements_are_typed.ndc new file mode 100644 index 00000000..73625026 --- /dev/null +++ b/tests/functional/programs/603_stdlib_seq/029_tuples_up_to_twelve_elements_are_typed.ndc @@ -0,0 +1,5 @@ +// `first`, `last` and `zip` are typed for tuples of up to twelve elements. +// expect-output: 2 z! [(1,2,3,4,5)] +let t = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, "z"); +let zipped: List<(Int, Int, Int, Int, Int)> = zip([1], [2], [3], [4], [5]); +print(t.first + 1, t.last ++ "!", zipped); diff --git a/tests/functional/programs/603_stdlib_seq/030_combinations_and_permutations_are_typed_lists.ndc b/tests/functional/programs/603_stdlib_seq/030_combinations_and_permutations_are_typed_lists.ndc new file mode 100644 index 00000000..fad7ecf3 --- /dev/null +++ b/tests/functional/programs/603_stdlib_seq/030_combinations_and_permutations_are_typed_lists.ndc @@ -0,0 +1,4 @@ +// Combinations and permutations are lists of the elements, so what they hold +// is typed. +// expect-output: [3,4,5] [3,4,3,5,4,5] +print([a + b for [a, b] in [1, 2, 3].combinations(2)], [p.sum() for p in [1, 2, 3].permutations(2)]); diff --git a/tests/functional/programs/603_stdlib_seq/031_find_subsequence_has_an_optional_form.ndc b/tests/functional/programs/603_stdlib_seq/031_find_subsequence_has_an_optional_form.ndc new file mode 100644 index 00000000..9a7f7b4b --- /dev/null +++ b/tests/functional/programs/603_stdlib_seq/031_find_subsequence_has_an_optional_form.ndc @@ -0,0 +1,4 @@ +// `find_subsequence` gives where a subsequence starts, and `find_subsequence?` +// gives `None` when it isn't there. +// expect-output: 2 Some(0) None +print([1, 2, 3, 4].find_subsequence([3, 4]), [1, 2].find_subsequence?([1]), [1, 2].find_subsequence?([5])); diff --git a/tests/functional/programs/604_stdlib_math/003_mixed_sum_error.ndc b/tests/functional/programs/604_stdlib_math/003_mixed_sum_error.ndc index 797c2816..04d35cc6 100644 --- a/tests/functional/programs/604_stdlib_math/003_mixed_sum_error.ndc +++ b/tests/functional/programs/604_stdlib_math/003_mixed_sum_error.ndc @@ -1,3 +1,4 @@ -// Int, Float, and Number are siblings, so a heterogeneous list is List. +// The elements share no type, so the list is a List, which no `sum` +// takes. // expect-error: No function called 'sum' -print([1, 2.0].sum()); +print([1, "two"].sum()); diff --git a/tests/functional/programs/604_stdlib_math/004_min_and_max_give_the_type_of_their_arguments.ndc b/tests/functional/programs/604_stdlib_math/004_min_and_max_give_the_type_of_their_arguments.ndc new file mode 100644 index 00000000..3ff06d3d --- /dev/null +++ b/tests/functional/programs/604_stdlib_math/004_min_and_max_give_the_type_of_their_arguments.ndc @@ -0,0 +1,7 @@ +// `min` and `max` of two values give a value of their type, and a literal +// takes the other argument's type. +// expect-output: 2 1.0 3.0 +let a = "ab".len(); +let m: Int = min(a, 5); +let x = 2.5; +print(m, min(x, 1), max(x, 3)); diff --git a/tests/functional/programs/604_stdlib_math/005_min_takes_two_values_of_one_type.ndc b/tests/functional/programs/604_stdlib_math/005_min_takes_two_values_of_one_type.ndc new file mode 100644 index 00000000..56b36581 --- /dev/null +++ b/tests/functional/programs/604_stdlib_math/005_min_takes_two_values_of_one_type.ndc @@ -0,0 +1,4 @@ +// An Int and a Float have no type in common, so say which one is meant. +// expect-error: No function called 'min' found that matches the arguments 'Int, Float' +let a = "ab".len(); +print(min(a, 2.5)); diff --git a/tests/functional/programs/605_stdlib_serde/017_encode_error_cycle.ndc b/tests/functional/programs/605_stdlib_serde/017_encode_error_cycle.ndc index 6aea15eb..63fa2fcc 100644 --- a/tests/functional/programs/605_stdlib_serde/017_encode_error_cycle.ndc +++ b/tests/functional/programs/605_stdlib_serde/017_encode_error_cycle.ndc @@ -1,4 +1,4 @@ -let l = [1, 2]; +let l: List = [1, 2]; l.push(l); json_encode(l); // expect-error: cannot convert a value that contains itself to JSON diff --git a/tests/functional/programs/605_stdlib_serde/025_lossy_error_cycle.ndc b/tests/functional/programs/605_stdlib_serde/025_lossy_error_cycle.ndc index 88478a7d..d2229e2c 100644 --- a/tests/functional/programs/605_stdlib_serde/025_lossy_error_cycle.ndc +++ b/tests/functional/programs/605_stdlib_serde/025_lossy_error_cycle.ndc @@ -1,4 +1,4 @@ -let l = [1, 2]; +let l: List = [1, 2]; l.push(l); json_encode_lossy(l); // expect-error: cannot convert a value that contains itself to JSON diff --git a/tests/functional/programs/605_stdlib_serde/026_lossy_error_heap_cycle.ndc b/tests/functional/programs/605_stdlib_serde/026_lossy_error_heap_cycle.ndc index 8dd784c2..03f77031 100644 --- a/tests/functional/programs/605_stdlib_serde/026_lossy_error_heap_cycle.ndc +++ b/tests/functional/programs/605_stdlib_serde/026_lossy_error_heap_cycle.ndc @@ -1,4 +1,4 @@ -let h = MaxHeap(); +let h: MaxHeap = MaxHeap(); h.push(h); json_encode_lossy(h); // expect-error: cannot convert a value that contains itself to JSON diff --git a/tests/functional/programs/900_bugs/bug0009_upvalue_in_overload_set.ndc b/tests/functional/programs/900_bugs/bug0009_upvalue_in_overload_set.ndc index 715e022f..70c2f88a 100644 --- a/tests/functional/programs/900_bugs/bug0009_upvalue_in_overload_set.ndc +++ b/tests/functional/programs/900_bugs/bug0009_upvalue_in_overload_set.ndc @@ -7,7 +7,7 @@ fn outer() { fn describe(x: Int) { "int" } fn describe(x: String) { "string" } fn middle() { - fn inner(v) { describe(v) } + fn inner(v: Any) { describe(v) } inner(1) ++ " " ++ inner("a") } middle() diff --git a/tests/functional/programs/900_bugs/bug0011_unpack_non_sequence.ndc b/tests/functional/programs/900_bugs/bug0011_unpack_non_sequence.ndc index b9a342d1..2e5ecb1d 100644 --- a/tests/functional/programs/900_bugs/bug0011_unpack_non_sequence.ndc +++ b/tests/functional/programs/900_bugs/bug0011_unpack_non_sequence.ndc @@ -1,6 +1,6 @@ // B3: exec_unpack panicked on non-sequence values instead of returning VmError. // expect-error: cannot unpack -fn foo(n) { +fn foo(n: Any) { let a, b = n; } diff --git a/tests/functional/programs/900_bugs/bug0015_unpack_mismatched_tuple_arity.ndc b/tests/functional/programs/900_bugs/bug0015_unpack_mismatched_tuple_arity.ndc index cdf6a84f..b97ac27d 100644 --- a/tests/functional/programs/900_bugs/bug0015_unpack_mismatched_tuple_arity.ndc +++ b/tests/functional/programs/900_bugs/bug0015_unpack_mismatched_tuple_arity.ndc @@ -1,9 +1,8 @@ -// Destructuring let where the variable was declared as () but later -// reassigned to a 2-tuple should not cause a false "not declared" error. -// The analyser widens the type via LUB on reassignment. +// Destructuring a captured tuple that was reassigned after the function was +// declared should not cause a false "not declared" error. // expect-output: 4 -let pos = (); +let pos = (0, 0); pos = (1, 3); fn use_pos() { diff --git a/tests/functional/programs/900_bugs/bug0021_chained_vectorized_tuple_arith.ndc b/tests/functional/programs/900_bugs/bug0021_chained_vectorized_tuple_arith.ndc index dc116e84..91c7a2b3 100644 --- a/tests/functional/programs/900_bugs/bug0021_chained_vectorized_tuple_arith.ndc +++ b/tests/functional/programs/900_bugs/bug0021_chained_vectorized_tuple_arith.ndc @@ -16,11 +16,9 @@ assert_eq(diff * diff, (9, 9, 9)); assert_eq((a - b) * (a - b), (9, 9, 9)); -// Same hazard but the tuple elements are statically `Any`. `id` here erases -// type information so the tuples below are `Tuple`; this -// mirrors what happens with stdlib natives whose `Value` return type infers -// to `Any`. The vectorization detection must treat `Any` as a potentially- -// numeric element so chained ops still go through dynamic dispatch. +// Same hazard but the tuple elements are statically `Any`: `id` returns +// `Any`, so the tuples below are `(Any, Any, Any)`. An `Any` element may be +// a number, so chained ops still go through dynamic dispatch. fn id(x) -> Any => x; let av = (id(1), id(2), id(3)); let bv = (id(4), id(5), id(6)); diff --git a/tests/functional/programs/900_bugs/bug0022_combinations_lazy_source.ndc b/tests/functional/programs/900_bugs/bug0022_combinations_lazy_source.ndc index 788304d0..ee98490b 100644 --- a/tests/functional/programs/900_bugs/bug0022_combinations_lazy_source.ndc +++ b/tests/functional/programs/900_bugs/bug0022_combinations_lazy_source.ndc @@ -6,23 +6,23 @@ // some entirely. Fix: pull `indices[k-1] + 1` before computing pivot. // Eager (list) reference — correct for both pre- and post-fix. -assert_eq([1, 2, 3, 4].combinations(2).list(), [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]); +assert_eq([1, 2, 3, 4].combinations(2).list(), [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]); // Lazy (iterator) source — this is the path the bug lived in. assert_eq( [1, 2, 3, 4].enumerate().combinations(2).list(), [ - ((0, 1), (1, 2)), - ((0, 1), (2, 3)), - ((0, 1), (3, 4)), - ((1, 2), (2, 3)), - ((1, 2), (3, 4)), - ((2, 3), (3, 4)) + [(0, 1), (1, 2)], + [(0, 1), (2, 3)], + [(0, 1), (3, 4)], + [(1, 2), (2, 3)], + [(1, 2), (3, 4)], + [(2, 3), (3, 4)] ] ); // Larger k — exercise the multi-pull-per-pivot path. let triples = [10, 20, 30, 40, 50].enumerate().combinations(3).list(); assert_eq(triples.len, 10); -assert_eq(triples[0], ((0, 10), (1, 20), (2, 30))); -assert_eq(triples[-1], ((2, 30), (3, 40), (4, 50))); +assert_eq(triples[0], [(0, 10), (1, 20), (2, 30)]); +assert_eq(triples[-1], [(2, 30), (3, 40), (4, 50)]); diff --git a/tests/functional/programs/900_bugs/bug0027_combinations_huge_k.ndc b/tests/functional/programs/900_bugs/bug0027_combinations_huge_k.ndc index 44fb00d0..778dc9c4 100644 --- a/tests/functional/programs/900_bugs/bug0027_combinations_huge_k.ndc +++ b/tests/functional/programs/900_bugs/bug0027_combinations_huge_k.ndc @@ -20,4 +20,4 @@ assert_eq(lazy.list(), []); assert_eq(lazy.list(), []); // Valid combinations are still produced correctly. -assert_eq([1, 2, 3, 4].combinations(2).list(), [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]); +assert_eq([1, 2, 3, 4].combinations(2).list(), [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]); diff --git a/tests/functional/programs/900_bugs/bug0033_cyclic_container_diagnostic.ndc b/tests/functional/programs/900_bugs/bug0033_cyclic_container_diagnostic.ndc index aa7f0fe2..f83a7c1b 100644 --- a/tests/functional/programs/900_bugs/bug0033_cyclic_container_diagnostic.ndc +++ b/tests/functional/programs/900_bugs/bug0033_cyclic_container_diagnostic.ndc @@ -2,6 +2,6 @@ // forever and aborted the process with a stack overflow. The bounded // description stops at a fixed depth instead. // expect-error: cannot compare -let a = []; +let a: List = []; a.push(a); print(a < 1); diff --git a/tests/functional/programs/900_bugs/bug0037_closures_keep_block_scoped_variables.ndc b/tests/functional/programs/900_bugs/bug0037_closures_keep_block_scoped_variables.ndc index 65dc7e1b..64263e66 100644 --- a/tests/functional/programs/900_bugs/bug0037_closures_keep_block_scoped_variables.ndc +++ b/tests/functional/programs/900_bugs/bug0037_closures_keep_block_scoped_variables.ndc @@ -63,7 +63,7 @@ fn main() { let call = fn() { 0 }; let build = fn() { 0 }; let get = fn() { 0 }; - let set = fn() { 0 }; + let set = fn() { }; { fn helper() { 5 } call = fn() { helper() }; diff --git a/tests/functional/programs/900_bugs/bug0040_assign_to_tuple_element.ndc b/tests/functional/programs/900_bugs/bug0040_assign_to_tuple_element.ndc index f71fbea3..a7bcbf70 100644 --- a/tests/functional/programs/900_bugs/bug0040_assign_to_tuple_element.ndc +++ b/tests/functional/programs/900_bugs/bug0040_assign_to_tuple_element.ndc @@ -1,5 +1,5 @@ // Assigning to an element of a tuple used to crash the compiler with // "encountered unresolved binding during compilation". -// expect-error: No function called '[]=' found that matches the arguments 'Tuple, Int, Any' +// expect-error: No function called '[]=' found that matches the arguments 'Tuple, Int, Int' let t = (1, 2, 3); t[0] = 99; diff --git a/tests/functional/programs/900_bugs/bug0042_assign_to_tuple_slice.ndc b/tests/functional/programs/900_bugs/bug0042_assign_to_tuple_slice.ndc index 4e72b1dd..dab18d00 100644 --- a/tests/functional/programs/900_bugs/bug0042_assign_to_tuple_slice.ndc +++ b/tests/functional/programs/900_bugs/bug0042_assign_to_tuple_slice.ndc @@ -1,5 +1,5 @@ // Assigning to a slice of a tuple reports that tuples have no `[]=`, and only // that: the value isn't also checked against the element type. -// expect-error: No function called '[]=' found that matches the arguments 'Tuple, Iterator, Any' +// expect-error: No function called '[]=' found that matches the arguments 'Tuple, Iterator, Tuple' let t = (1, 2); t[0..1] = (5,); diff --git a/tests/functional/programs/998_not_desired/big_int_ranges.ndc b/tests/functional/programs/998_not_desired/big_int_ranges.ndc index 75c9a00c..f75e7949 100644 --- a/tests/functional/programs/998_not_desired/big_int_ranges.ndc +++ b/tests/functional/programs/998_not_desired/big_int_ranges.ndc @@ -1,4 +1,4 @@ -// expect-error: Integer too large for range bounds +// expect-error: mismatched types: found Number but expected Int // Not sure what to think of this one, but right now you cannot construct ranges with ints outside of the i64 range // it does make type analysis a tiny bit easier though (2n^1024n)..((2n^1024n)+1n) diff --git a/tests/functional/tests/repl.rs b/tests/functional/tests/repl.rs index 595eed4e..b0f9c7b6 100644 --- a/tests/functional/tests/repl.rs +++ b/tests/functional/tests/repl.rs @@ -218,3 +218,21 @@ fn configure_after_a_line_ran_updates_the_globals() { let out = String::from_utf8(interp.get_output().unwrap()).unwrap(); assert_eq!(out.trim(), "1"); } + +#[test] +fn a_generic_function_from_an_earlier_line_takes_a_call_with_new_types() { + // The line that declared `inc` compiled its instance for Ints. A later + // line calls it with a Float, which its own body takes. + let out = repl_output(&[ + "fn inc(x) => x + 1;", + "print(inc(1));", + "print(inc(2.5), inc(1));", + ]); + assert_eq!(out.trim(), "2\n3.5 2"); +} + +#[test] +fn a_generic_function_on_one_line_uses_its_instances() { + let out = repl_output(&["fn inc(x) => x + 1; print(inc(1), inc(2.5));"]); + assert_eq!(out.trim(), "2 3.5"); +} diff --git a/tests/proptest/tests/overloads.regressions b/tests/proptest/tests/overloads.regressions new file mode 100644 index 00000000..28a08757 --- /dev/null +++ b/tests/proptest/tests/overloads.regressions @@ -0,0 +1,12 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc ccf1065cc5b6785de2f3e60fab2f91a07dbf049d640de1fd8e4cd69e87a31628 # shrinks to program = struct S { n: Int } struct T { n: String } fn p(x: List) => 1; fn q(x: List) => 1; fn g(a) => a.len(); fn h(a) => g(a); let v0 = 1; let v1 = 1; let v2 = 1; let v3 = 1; v0[0] += (if true { q(push).fold(0, fn(acc, a) => acc + a) } else { fn(a) => a ++ "!" }); +cc 28b971024cd28c1bd67b10fd47576f0057292aac87059605340f0ab7c835a6de # shrinks to program = struct S { n: Int } struct T { n: String } fn p(x: String) => 1; fn p(x) => 1; fn q(x) => 1; fn g(a) => a; fn h(a) => g(a); let v0 = 1; let v1 = 1; let v2 = 1; let v3 = 1; v0 += [1, "s"].map(fn(a) => p(a)); +cc 4c529d1b26bda369a7a7106e5e40396abfa3a955a96ea68d07756b86ca4f49fa # shrinks to program = struct S { n: Int } struct T { n: String } fn p(x: List) => 1; fn p(x) => 1; fn q(x) => 1; fn g(a) => a; fn h(a) => g(a); let v0 = 1; let v1 = 1; let v2 = 1; let v3 = 1; v0[0] = (1)([fn(a) => p(a), len]); +cc ab11f2151f1074090878c174fcbb8148d9db5640b1b288bb551118afb4a91606 # shrinks to program = struct S { n: Int } struct T { n: String } fn p(x: Int) => 1; fn p(x) => x; fn q(x: List) => 1; fn g(a) => a; fn h(a) => g(a); let v0 = 1; let v1 = 1; let v2 = 1; let v3 = 1; v0 += (q(%{}))(["s"].map(p)); +cc 1a8c5b5e8e0c29aa6847f08a786b42e2c659fcdc43101296a32471c5fad65d8f # shrinks to program = struct S { n: Int } struct T { n: String } fn p(x) => 1; fn q(x: String) => 1; fn g(a) => a; fn h(a) => g(a); let v0 = 1; let v1 = 1; let v2 = 1; let v3 = 1; v0 = ["s"]["k"][0]; +cc 6adf8fd1236fcab1614255c0f54fc8dcba7032bb8692c3e9cad64da57eb4a6fb # shrinks to program = struct S { n: Int } struct T { n: String } fn p(x: List) => 1; fn p(x) => 1; fn q(x: List) => 1; fn g(a) => a; fn h(a) => g(a); let v0 = 2.5; let v1 = 1; let v2 = 1; let v3 = 1; v0.push([(1)(1), 1].map(p)); diff --git a/tests/proptest/tests/overloads.rs b/tests/proptest/tests/overloads.rs new file mode 100644 index 00000000..84000d80 --- /dev/null +++ b/tests/proptest/tests/overloads.rs @@ -0,0 +1,324 @@ +//! Property-based test for the analyser's overload choices. +//! +//! The analyser commits a call or a function value to an overload before +//! the types involved are fully known, so that information can flow into +//! lambdas and empty literals. That's only right if it's the overload the +//! call or value would get once the types are known, and in debug builds +//! the analyser asserts this for every early commit (`check_early_picks`). +//! It also asserts that no type variable is left unsettled. +//! +//! The token fuzzer in `panic.rs` rarely gets past the parser, so this one +//! generates well-formed programs instead, from the pieces that make the +//! analyser commit early: overloaded functions taking `Int`, `Float`, +//! `String`, lists and anything, empty literals settled by later uses, +//! lambdas, function values, `map`, `if`, index writes, `op=`, +//! annotations, and generic functions calling each other, whose instances +//! the analyser works out after the program. Analysing each one must not panic. Type errors are +//! expected and ignored. +//! +//! * `known_overload_regressions_do_not_panic` replays the seeds saved in +//! `overloads.regressions` and runs as part of `cargo test`. +//! * `fuzz_overload_choices` generates fresh programs and is `#[ignore]`d: +//! +//! ```sh +//! cargo test -p proptest_tests --test overloads -- --ignored +//! PROPTEST_CASES=100000 cargo test -p proptest_tests --test overloads -- --ignored +//! ``` + +use ndc_analyser::Analyser; +use ndc_core::FunctionRegistry; +use ndc_lexer::{Lexer, SourceId}; +use ndc_parser::Parser; +use proptest::prelude::*; +use proptest::test_runner::FileFailurePersistence; +use std::fmt; +use std::panic; +use std::sync::Once; +use std::thread; +// The other harnesses in this crate run programs; this one only analyses. +use ndc_vm as _; +use num as _; + +/// A generated program, printed as its source so a failure can be pasted +/// into a file and run. +#[derive(Clone)] +struct Program(String); + +impl fmt::Debug for Program { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "\n{}\n", self.0) + } +} + +/// The parameter types an overload can declare, `""` being none (`Any`). +const PARAMETER_TYPES: [&str; 5] = ["Int", "Float", "String", "List", ""]; + +/// What an overload can return, by the name of its parameter `x`. +const BODIES: [&str; 6] = ["1", "2.5", "\"s\"", "x", "[x]", "true"]; + +/// Overloads of `name` taking one parameter each, of different types so they +/// aren't redefinitions, and maybe one taking two. +fn arb_overloads(name: &'static str) -> impl Strategy { + ( + prop::sample::subsequence(PARAMETER_TYPES.to_vec(), 1..=3), + prop::collection::vec(prop::sample::select(BODIES.to_vec()), 3), + prop::option::of(prop::sample::select(BODIES.to_vec())), + ) + .prop_map(move |(types, bodies, two)| { + let mut source = String::new(); + for (typ, body) in types.iter().zip(bodies) { + let annotation = if typ.is_empty() { + String::new() + } else { + format!(": {typ}") + }; + source.push_str(&format!("fn {name}(x{annotation}) => {body};\n")); + } + if let Some(body) = two { + source.push_str(&format!("fn {name}(x, y: Int) => {body};\n")); + } + source + }) +} + +/// The names a value can be: variables, overloaded functions declared by +/// the program, and overloaded or generic built-ins. +const VALUES: [&str; 17] = [ + "v0", "v1", "v2", "v3", "p", "q", "abs", "split", "push", "len", "max", "first", "sum", "n", + "S", "g", "h", +]; + +const LITERALS: [&str; 13] = [ + "1", + "2.5", + "\"s\"", + "[]", + "[1]", + "[\"s\"]", + "%{}", + "%{\"k\": 1}", + "None", + "(1, 2)", + "S(1)", + "T(\"s\")", + "[S(1)]", +]; + +/// What a function taking `a` can do with it, in ways that need its type. +const BODIES_USING_A: [&str; 15] = [ + "a", + "p(a)", + "q(a)", + "a + 1", + "a ++ \"!\"", + "[a]", + "a.len()", + "if true { a } else { 1 }", + "if true { a } else { [] }", + "p(a, 1)", + "a[0]", + "a.n", + "a.n + 1", + "a as Int", + "a.first()", +]; + +/// A lambda taking `a`, whose body uses `a` in ways that need its type. +fn arb_lambda() -> impl Strategy { + prop::sample::select(BODIES_USING_A.to_vec()).prop_map(|body| format!("fn(a) => {body}")) +} + +/// Two generic functions, `g` and `h`, where `h` calls `g`, so that each +/// call needs instances of both. +fn arb_generic_functions() -> impl Strategy { + ( + prop::sample::select(BODIES_USING_A.to_vec()), + prop::sample::select(vec![ + "g(a)", + "g(a) + 1", + "[g(a)]", + "g([a])", + "p(g(a))", + "if true { g(a) } else { a }", + "g(a).len()", + ]), + ) + .prop_map(|(g, h)| format!("fn g(a) => {g};\nfn h(a) => {h};\n")) +} + +/// An expression, nesting a few levels deep. +fn arb_expression() -> impl Strategy { + let leaf = prop_oneof![ + 3 => prop::sample::select(LITERALS.to_vec()).prop_map(str::to_string), + 3 => prop::sample::select(VALUES.to_vec()).prop_map(str::to_string), + 2 => arb_lambda(), + ]; + leaf.prop_recursive(3, 16, 2, |inner| { + prop_oneof![ + ( + prop::sample::select(vec!["p", "q", "abs", "len", "g", "h"]), + inner.clone() + ) + .prop_map(|(f, e)| format!("{f}({e})")), + (inner.clone(), inner.clone()).prop_map(|(a, b)| format!("p({a}, {b})")), + (inner.clone(), inner.clone()).prop_map(|(a, b)| format!("({a} + {b})")), + (inner.clone(), inner.clone()).prop_map(|(a, b)| format!("[{a}, {b}]")), + (inner.clone(), inner.clone()) + .prop_map(|(a, b)| format!("(if true {{ {a} }} else {{ {b} }})")), + ( + inner.clone(), + prop_oneof![ + arb_lambda(), + prop::sample::select(VALUES.to_vec()).prop_map(str::to_string) + ] + ) + .prop_map(|(xs, f)| format!("{xs}.map({f})")), + inner.clone().prop_map(|e| format!("{e}[0]")), + inner.clone().prop_map(|e| format!("{e}.n")), + inner + .clone() + .prop_map(|e| format!("{e}.fold(0, fn(acc, a) => acc + a)")), + inner.clone().prop_map(|e| format!("({e} as List)")), + inner.clone().prop_map(|e| format!("{e}[\"k\"]")), + (inner.clone(), inner).prop_map(|(f, e)| format!("({f})({e})")), + ] + }) +} + +/// Annotations a `let` can have. +const ANNOTATIONS: [&str; 8] = [ + "Int", + "String", + "List", + "List", + "Fn(Int) -> Int", + "Fn(String) -> List", + "List Int>", + "Any", +]; + +const VARIABLES: [&str; 4] = ["v0", "v1", "v2", "v3"]; + +/// A statement on the variables `v0` to `v3`. +fn arb_statement() -> impl Strategy { + let variable = || prop::sample::select(VARIABLES.to_vec()); + prop_oneof![ + 3 => (variable(), arb_expression()).prop_map(|(v, e)| format!("{v}.push({e});")), + 2 => (variable(), arb_expression()).prop_map(|(v, e)| format!("{v}[0] = {e};")), + 1 => (variable(), arb_expression()).prop_map(|(v, e)| format!("{v}[\"k\"] = {e};")), + 1 => (variable(), arb_expression()).prop_map(|(v, e)| format!("{v}.n = {e};")), + 1 => (variable(), arb_expression()).prop_map(|(v, e)| format!("{v}[0] += {e};")), + 2 => (variable(), arb_expression()).prop_map(|(v, e)| format!("{v} += {e};")), + 2 => (variable(), arb_expression()).prop_map(|(v, e)| format!("{v} = {e};")), + 2 => (prop::sample::select(ANNOTATIONS.to_vec()), arb_expression()) + .prop_map(|(t, e)| format!("let w: {t} = {e};")), + 2 => arb_expression().prop_map(|e| format!("{e};")), + 1 => (variable(), arb_expression()).prop_map(|(v, e)| format!("for e in {v} {{ {e}; }}")), + ] +} + +fn arb_program() -> impl Strategy { + ( + arb_overloads("p"), + arb_overloads("q"), + arb_generic_functions(), + prop::collection::vec(prop::sample::select(LITERALS.to_vec()), 4), + prop::collection::vec(arb_statement(), 1..=6), + ) + .prop_map(|(p, q, generic, initial, statements)| { + // Two structs with a field of the same name, so `x.n` and the + // getter `n` have overloads to choose from. + let mut source = + format!("struct S {{ n: Int }}\nstruct T {{ n: String }}\n{p}{q}{generic}"); + for (variable, value) in VARIABLES.iter().zip(initial) { + source.push_str(&format!("let {variable} = {value};\n")); + } + for statement in statements { + source.push_str(&statement); + source.push('\n'); + } + Program(source) + }) +} + +const WORKER_THREAD_NAME: &str = "proptest-overload-worker"; + +/// Silences panics on the worker thread, so shrinking doesn't print each +/// attempt's panic; the final one is resumed on the test thread. +fn install_quiet_panic_hook() { + static HOOK: Once = Once::new(); + HOOK.call_once(|| { + let default_hook = panic::take_hook(); + panic::set_hook(Box::new(move |info| { + if thread::current().name() != Some(WORKER_THREAD_NAME) { + default_hook(info); + } + })); + }); +} + +/// Analyses `program` on a worker thread, so its panic can be silenced and +/// resumed here for proptest to shrink. +fn analyse_quietly(program: Program) { + install_quiet_panic_hook(); + let result = thread::Builder::new() + .name(WORKER_THREAD_NAME.into()) + .spawn(move || analyse(&program)) + .expect("spawn worker thread") + .join(); + if let Err(payload) = result { + panic::resume_unwind(payload); + } +} + +/// Analyses `program` with the standard library's functions, as the +/// interpreter would. Analysis panics on a failed debug assertion. +fn analyse(program: &Program) { + let Ok(tokens) = Lexer::new(&program.0, SourceId::SYNTHETIC).collect::, _>>() + else { + return; + }; + let Ok(expressions) = Parser::from_tokens(tokens).parse() else { + return; + }; + let mut registry = FunctionRegistry::default(); + ndc_stdlib::register(&mut registry); + let natives = registry + .iter() + .map(|function| (function.name.clone(), function.static_type.clone())) + .collect(); + let mut analyser = Analyser::new(natives); + for expression in &expressions { + analyser.analyse(expression); + } + analyser.take_result(); +} + +fn persistence_config(cases: u32) -> ProptestConfig { + ProptestConfig { + cases, + max_shrink_iters: 4096, + failure_persistence: Some(Box::new(FileFailurePersistence::WithSource("regressions"))), + ..ProptestConfig::default() + } +} + +proptest! { + // cases=0 only replays the seeds in `overloads.regressions`. + #![proptest_config(persistence_config(0))] + + #[test] + fn known_overload_regressions_do_not_panic(program in arb_program()) { + analyse_quietly(program); + } +} + +proptest! { + #![proptest_config(persistence_config(1024))] + + #[test] + #[ignore = "fuzz test; opt-in via `cargo test ... -- --ignored`"] + fn fuzz_overload_choices(program in arb_program()) { + analyse_quietly(program); + } +}