From b1c1d0187b0a7c2d5a42f8b6e1089d8a32315e3e Mon Sep 17 00:00:00 2001 From: Tim Fennis Date: Sat, 14 Nov 2026 09:48:57 +0100 Subject: [PATCH 1/2] =?UTF-8?q?feat(analyser):=20make=20Any=20a=20top=20ty?= =?UTF-8?q?pe=20that=20takes=20a=20cast=20to=20get=20a=20value=20out=20?= =?UTF-8?q?=F0=9F=93=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- manual/src/features/augmented-assignment.md | 16 +++ manual/src/features/casts.md | 16 +++ manual/src/reference/memory-management.md | 2 +- manual/src/reference/types.md | 43 +++++-- manual/src/reference/types/function.md | 20 ++- manual/src/reference/types/struct.md | 4 +- manual/src/reference/types/tuple.md | 2 +- manual/src/reference/variables-and-scopes.md | 2 +- .../overload-dispatch-collections.md | 45 ++++--- ndc_analyser/src/analyser/assignments.rs | 4 +- ndc_analyser/src/analyser/calls.rs | 2 + ndc_analyser/src/analyser/generics.rs | 8 +- ndc_analyser/src/analyser/solver.rs | 44 +++++-- ndc_analyser/src/analyser/tests.rs | 120 ++++++++++-------- ndc_analyser/src/error.rs | 41 ++++-- ndc_analyser/src/overloads.rs | 84 +++++------- ndc_analyser/src/types.rs | 40 ++++-- ndc_core/src/static_type.rs | 39 +++--- tests/compiler/tests/compiler.rs | 17 ++- .../004_basic/055_unified_op_assign.ndc | 7 +- .../004_basic/066_op_assignment_any_rhs.ndc | 8 -- ...nt_takes_an_any_operand_through_a_cast.ndc | 7 + ...nment_checks_what_an_any_operand_makes.ndc | 6 - ...076_op_assignment_takes_no_any_operand.ndc | 5 + ...t_checks_an_any_target_before_the_call.ndc | 7 - .../077_op_assignment_takes_no_any_target.ndc | 7 + .../033_let_binding_shadows_let_binding.ndc | 4 +- ...y_types_is_checked_as_the_program_runs.ndc | 8 -- ...ment_picks_the_overload_that_takes_any.ndc | 13 ++ ...st_to_pick_an_overload_by_its_elements.ndc | 8 ++ ...checks_elements_when_nothing_else_fits.ndc | 8 -- ...led_with_an_any_has_its_calls_take_one.ndc | 5 + ...body_s_own_calls_are_picked_as_it_runs.ndc | 7 - ...passed_for_a_type_parameter_is_checked.ndc | 12 -- ...shed_into_a_literal_makes_it_hold_anys.ndc | 12 ++ ...rom_any_is_checked_as_a_function_type.ndc} | 6 +- ..._a_value_applies_an_operator_to_tuples.ndc | 10 ++ ..._parameter_keeps_the_parameter_generic.ndc | 10 ++ .../057_a_write_takes_no_any_index.ndc | 5 + ...ite_with_an_any_index_checks_the_value.ndc | 5 - ..._an_any_index_cast_picks_by_the_index.ndc} | 4 +- .../033_a_key_without_a_value_holds_unit.ndc | 17 ++- ...rehension_default_is_one_of_the_values.ndc | 16 ++- .../008_iterators/002_range_contains.ndc | 12 -- ...10_a_range_is_searched_only_for_an_int.ndc | 5 + ...n_instance_takes_no_any_its_types_dont.ndc | 9 ++ ...ance_that_checks_any_has_its_own_cache.ndc | 10 -- ...se_callee_checks_any_has_its_own_cache.ndc | 10 -- ...e_whose_callee_takes_no_any_takes_none.ndc | 10 ++ .../009_vector_exact_match_precision.ndc | 10 +- .../027_call_function_stored_in_field.ndc | 2 +- ...r_called_through_any_checks_its_fields.ndc | 4 - ..._through_any_takes_a_cast_to_be_called.ndc | 4 + ...ed_through_any_takes_a_typed_container.ndc | 5 - ..._cast_from_any_takes_a_typed_container.ndc | 5 + ...a_field_written_through_any_is_checked.ndc | 5 - ...ritten_with_a_cast_from_any_is_checked.ndc | 6 + ...er_field_written_with_a_cast_from_any.ndc} | 2 +- .../016_casts/012_cyclic_cast_error.ndc | 2 +- ...annotated_let_takes_an_any_of_its_type.ndc | 6 - ...otated_let_takes_an_any_through_a_cast.ndc | 6 + .../002_an_annotated_let_checks_an_any.ndc | 4 - .../002_an_annotated_let_takes_no_any.ndc | 5 + .../003_an_assignment_checks_an_any.ndc | 5 - .../003_an_assignment_takes_no_any.ndc | 6 + ...c => 004_a_destructuring_takes_no_any.ndc} | 2 +- ...rn_annotation_takes_an_any_of_its_type.ndc | 5 - ...annotation_takes_an_any_through_a_cast.ndc | 5 + .../006_a_cast_checks_an_any.ndc | 9 ++ .../006_a_return_annotation_checks_an_any.ndc | 8 -- .../007_a_generic_argument_checks_an_any.ndc | 7 - .../007_a_generic_argument_takes_no_any.ndc | 7 + .../008_an_index_write_checks_an_any.ndc | 5 - .../008_an_index_write_takes_no_any.ndc | 5 + .../009_a_field_write_checks_an_any.ndc | 6 - .../009_a_field_write_takes_no_any.ndc | 6 + .../010_a_cast_to_a_container_is_scanned.ndc | 4 + .../010_a_container_from_any_is_scanned.ndc | 4 - ...ment_makes_a_parameter_any_in_the_body.ndc | 10 ++ ...stance_checks_an_any_against_its_types.ndc | 11 -- ..._cast_from_any_checks_what_it_returns.ndc} | 4 +- ...om_any_must_return_what_its_type_says.ndc} | 4 +- ..._checked_function_is_the_same_function.ndc | 6 +- ...rom_any_must_take_what_its_type_passes.ndc | 4 + ...rom_any_must_take_what_its_type_passes.ndc | 4 - ...ram_runs_rejects_an_unchecked_function.ndc | 8 -- ...overload_takes_no_function_of_type_any.ndc | 8 ++ ...n_through_a_checked_function_runs_deep.ndc | 2 +- ...om_any_whose_type_fits_is_not_checked.ndc} | 2 +- ..._checked_function_checks_its_arguments.ndc | 4 +- ...function_tells_checked_functions_apart.ndc | 2 +- ...ny_takes_only_what_it_was_compiled_for.ndc | 6 +- ...es_only_what_the_instance_made_it_take.ndc | 7 +- ...through_any_to_change_its_element_type.ndc | 2 +- ...snt_depend_on_a_literal_leaves_it_open.ndc | 10 ++ ...e_pick_depends_on_a_literal_decides_it.ndc | 9 -- ...lt_function_is_not_checked_as_a_value.ndc} | 2 +- ...an_any_value_makes_a_literal_hold_anys.ndc | 8 ++ ...ares_the_type_of_the_values_next_to_it.ndc | 9 -- ...e_no_type_is_checked_once_they_are_any.ndc | 7 - ...e_no_type_stored_as_an_any_take_a_cast.ndc | 7 + ...on_returning_any_gives_no_typed_values.ndc | 6 + .../016_sequence_prefixes_suffixes.ndc | 11 +- .../bug0009_upvalue_in_overload_set.ndc | 12 +- ...bug0021_chained_vectorized_tuple_arith.ndc | 10 +- tests/functional/tests/repl.rs | 2 +- 106 files changed, 657 insertions(+), 472 deletions(-) delete mode 100644 tests/functional/programs/004_basic/066_op_assignment_any_rhs.ndc create mode 100644 tests/functional/programs/004_basic/066_op_assignment_takes_an_any_operand_through_a_cast.ndc delete mode 100644 tests/functional/programs/004_basic/076_op_assignment_checks_what_an_any_operand_makes.ndc create mode 100644 tests/functional/programs/004_basic/076_op_assignment_takes_no_any_operand.ndc delete mode 100644 tests/functional/programs/004_basic/077_op_assignment_checks_an_any_target_before_the_call.ndc create mode 100644 tests/functional/programs/004_basic/077_op_assignment_takes_no_any_target.ndc delete mode 100644 tests/functional/programs/005_functions/081_a_parameter_the_body_types_is_checked_as_the_program_runs.ndc create mode 100644 tests/functional/programs/005_functions/081_an_any_argument_picks_the_overload_that_takes_any.ndc create mode 100644 tests/functional/programs/005_functions/086_an_any_takes_a_cast_to_pick_an_overload_by_its_elements.ndc delete mode 100644 tests/functional/programs/005_functions/086_runtime_dispatch_checks_elements_when_nothing_else_fits.ndc create mode 100644 tests/functional/programs/005_functions/095_a_generic_body_called_with_an_any_has_its_calls_take_one.ndc delete mode 100644 tests/functional/programs/005_functions/095_a_generic_body_s_own_calls_are_picked_as_it_runs.ndc delete mode 100644 tests/functional/programs/005_functions/096_an_any_value_passed_for_a_type_parameter_is_checked.ndc create mode 100644 tests/functional/programs/005_functions/096_an_any_value_pushed_into_a_literal_makes_it_hold_anys.ndc rename tests/functional/programs/005_functions/{103_a_function_value_left_to_the_program_is_checked_as_a_function_type.ndc => 103_a_function_value_cast_from_any_is_checked_as_a_function_type.ndc} (52%) create mode 100644 tests/functional/programs/005_functions/109_a_generic_function_used_as_a_value_applies_an_operator_to_tuples.ndc create mode 100644 tests/functional/programs/005_functions/110_a_list_of_a_parameter_keeps_the_parameter_generic.ndc create mode 100644 tests/functional/programs/006_lists/057_a_write_takes_no_any_index.ndc delete mode 100644 tests/functional/programs/006_lists/057_a_write_with_an_any_index_checks_the_value.ndc rename tests/functional/programs/006_lists/{058_a_write_with_an_any_index_picks_by_the_index.ndc => 058_a_write_with_an_any_index_cast_picks_by_the_index.ndc} (69%) create mode 100644 tests/functional/programs/008_iterators/010_a_range_is_searched_only_for_an_int.ndc create mode 100644 tests/functional/programs/010_memoization/004_an_instance_takes_no_any_its_types_dont.ndc delete mode 100644 tests/functional/programs/010_memoization/004_an_instance_that_checks_any_has_its_own_cache.ndc delete mode 100644 tests/functional/programs/010_memoization/005_an_instance_whose_callee_checks_any_has_its_own_cache.ndc create mode 100644 tests/functional/programs/010_memoization/005_an_instance_whose_callee_takes_no_any_takes_none.ndc delete mode 100644 tests/functional/programs/015_struct/033_a_constructor_called_through_any_checks_its_fields.ndc create mode 100644 tests/functional/programs/015_struct/033_a_constructor_through_any_takes_a_cast_to_be_called.ndc delete mode 100644 tests/functional/programs/015_struct/034_a_constructor_called_through_any_takes_a_typed_container.ndc create mode 100644 tests/functional/programs/015_struct/034_a_constructor_cast_from_any_takes_a_typed_container.ndc delete mode 100644 tests/functional/programs/015_struct/035_a_field_written_through_any_is_checked.ndc create mode 100644 tests/functional/programs/015_struct/035_a_field_written_with_a_cast_from_any_is_checked.ndc rename tests/functional/programs/015_struct/{036_a_container_field_written_through_any_is_checked.ndc => 036_a_container_field_written_with_a_cast_from_any.ndc} (80%) delete mode 100644 tests/functional/programs/017_dynamic_any/001_an_annotated_let_takes_an_any_of_its_type.ndc create mode 100644 tests/functional/programs/017_dynamic_any/001_an_annotated_let_takes_an_any_through_a_cast.ndc delete mode 100644 tests/functional/programs/017_dynamic_any/002_an_annotated_let_checks_an_any.ndc create mode 100644 tests/functional/programs/017_dynamic_any/002_an_annotated_let_takes_no_any.ndc delete mode 100644 tests/functional/programs/017_dynamic_any/003_an_assignment_checks_an_any.ndc create mode 100644 tests/functional/programs/017_dynamic_any/003_an_assignment_takes_no_any.ndc rename tests/functional/programs/017_dynamic_any/{004_a_destructuring_checks_each_any.ndc => 004_a_destructuring_takes_no_any.ndc} (53%) delete mode 100644 tests/functional/programs/017_dynamic_any/005_a_return_annotation_takes_an_any_of_its_type.ndc create mode 100644 tests/functional/programs/017_dynamic_any/005_a_return_annotation_takes_an_any_through_a_cast.ndc create mode 100644 tests/functional/programs/017_dynamic_any/006_a_cast_checks_an_any.ndc delete mode 100644 tests/functional/programs/017_dynamic_any/006_a_return_annotation_checks_an_any.ndc delete mode 100644 tests/functional/programs/017_dynamic_any/007_a_generic_argument_checks_an_any.ndc create mode 100644 tests/functional/programs/017_dynamic_any/007_a_generic_argument_takes_no_any.ndc delete mode 100644 tests/functional/programs/017_dynamic_any/008_an_index_write_checks_an_any.ndc create mode 100644 tests/functional/programs/017_dynamic_any/008_an_index_write_takes_no_any.ndc delete mode 100644 tests/functional/programs/017_dynamic_any/009_a_field_write_checks_an_any.ndc create mode 100644 tests/functional/programs/017_dynamic_any/009_a_field_write_takes_no_any.ndc create mode 100644 tests/functional/programs/017_dynamic_any/010_a_cast_to_a_container_is_scanned.ndc delete mode 100644 tests/functional/programs/017_dynamic_any/010_a_container_from_any_is_scanned.ndc create mode 100644 tests/functional/programs/017_dynamic_any/011_an_any_argument_makes_a_parameter_any_in_the_body.ndc delete mode 100644 tests/functional/programs/017_dynamic_any/011_each_instance_checks_an_any_against_its_types.ndc rename tests/functional/programs/017_dynamic_any/{012_a_function_from_any_checks_what_it_returns.ndc => 012_a_function_cast_from_any_checks_what_it_returns.ndc} (76%) rename tests/functional/programs/017_dynamic_any/{013_a_function_from_any_must_return_what_its_type_says.ndc => 013_a_function_cast_from_any_must_return_what_its_type_says.ndc} (52%) create mode 100644 tests/functional/programs/017_dynamic_any/016_a_function_cast_from_any_must_take_what_its_type_passes.ndc delete mode 100644 tests/functional/programs/017_dynamic_any/016_a_function_from_any_must_take_what_its_type_passes.ndc delete mode 100644 tests/functional/programs/017_dynamic_any/017_an_overload_picked_as_the_program_runs_rejects_an_unchecked_function.ndc create mode 100644 tests/functional/programs/017_dynamic_any/017_an_overload_takes_no_function_of_type_any.ndc rename tests/functional/programs/017_dynamic_any/{022_a_function_returning_a_list_fits_one_returning_an_iterable.ndc => 022_a_function_cast_from_any_whose_type_fits_is_not_checked.ndc} (86%) create mode 100644 tests/functional/programs/018_literals/015_a_use_whose_pick_doesnt_depend_on_a_literal_leaves_it_open.ndc delete mode 100644 tests/functional/programs/018_literals/015_the_first_use_whose_pick_depends_on_a_literal_decides_it.ndc rename tests/functional/programs/018_literals/{017_a_map_default_function_returning_any_is_not_checked_as_a_value.ndc => 017_a_map_default_function_is_not_checked_as_a_value.ndc} (86%) create mode 100644 tests/functional/programs/018_literals/029_an_any_value_makes_a_literal_hold_anys.ndc delete mode 100644 tests/functional/programs/018_literals/029_an_any_value_shares_the_type_of_the_values_next_to_it.ndc delete mode 100644 tests/functional/programs/018_literals/031_a_write_of_values_that_share_no_type_is_checked_once_they_are_any.ndc create mode 100644 tests/functional/programs/018_literals/031_values_that_share_no_type_stored_as_an_any_take_a_cast.ndc create mode 100644 tests/functional/programs/018_literals/032_a_map_default_function_returning_any_gives_no_typed_values.ndc diff --git a/manual/src/features/augmented-assignment.md b/manual/src/features/augmented-assignment.md index 39469556..a6b24a16 100644 --- a/manual/src/features/augmented-assignment.md +++ b/manual/src/features/augmented-assignment.md @@ -55,6 +55,22 @@ mixed ++= ["two"]; print(mixed); // [1,"two"] ``` +An operand of type `Any` takes a [cast](./casts.md) like anywhere else, as +the operator has to take it: + +```ndc,fails +fn opaque(x) -> Any => x; +let values = [1]; +values ++= opaque([2]); // error: No function called '++' found that matches the arguments 'List, Any' +``` + +```ndc +fn opaque(x) -> Any => x; +let values = [1]; +values ++= opaque([2]) as List; +print(values); // [1,2] +``` + ## Optimization You might expect `list ++= [1,2,3]` to desugar to `list = list ++ [1,2,3]`, but that would waste work. Andy C++ handles some augmented assignments directly. In this case, it appends `[1,2,3]` without creating an intermediate list. diff --git a/manual/src/features/casts.md b/manual/src/features/casts.md index 568ef80c..e9d86801 100644 --- a/manual/src/features/casts.md +++ b/manual/src/features/casts.md @@ -11,6 +11,22 @@ let ints = values as List; print(ints.sum()); // 3 ``` +A cast is the way a value comes out of an `Any`. Anything goes into an +`Any`, but an `Any` doesn't go where another type is expected, and no +operator or function takes it unless it takes an `Any` (see [Getting a value +out of an `Any`](../reference/types.md#getting-a-value-out-of-an-any)): + +```ndc,fails +let value: Any = 5; +value + 1 +// error[analysis]: No function called '+' found that matches the arguments 'Any, Int' +``` + +```ndc +let value: Any = 5; +print((value as Int) + 1); // 6 +``` + 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` diff --git a/manual/src/reference/memory-management.md b/manual/src/reference/memory-management.md index 59fb7a27..b2dade9c 100644 --- a/manual/src/reference/memory-management.md +++ b/manual/src/reference/memory-management.md @@ -28,7 +28,7 @@ A closure that captures a variable which holds a reference back to itself create ```ndc { - let f: Any = (); + let f: Fn(Int) -> Int = fn(x) => 0; f = fn(x) { if x > 0 { x + f(x - 1) } else { 0 } }; print(f(3)); // 6 } diff --git a/manual/src/reference/types.md b/manual/src/reference/types.md index 2b216307..f69a151b 100644 --- a/manual/src/reference/types.md +++ b/manual/src/reference/types.md @@ -117,13 +117,13 @@ print(total([1, 2]), total((3, 4)), total(1..4)); // 3 7 6 ### 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`: +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` and a cast: ```ndc,fails -let words = "a b".split(" "); // List +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 +let anything = any as List; // fine: checked as the program runs +let wrong: List = words; // error: found List but expected List ``` ### Values of different types @@ -169,21 +169,38 @@ 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:** 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`, but values of different types are an error (see [Values of different types](#values-of-different-types)). 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. +> **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`, which then takes a cast to be used as anything else (see below), and values of different types are an error (see [Values of different types](#values-of-different-types)). 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 +### Getting a value out of an `Any` -You can use an `Any` value where a specific type is expected. The program checks the value when it gets there: +Anything goes into an `Any`, but getting a value back out takes a [cast](../features/casts.md): `value as Int` checks as the program runs that the value is an Int, and `value as? Int` gives an `Option`. An `Any` doesn't go where another type is expected without one: ```ndc,fails let a: Any = 5; -let n: Int = a; // fine: a holds an Int -print(n + 1); // 6 +let n: Int = a; // error: mismatched types: found Any but expected Int +``` + +```ndc +let a: Any = 5; +let n: Int = a as Int; // checked: a holds an Int +print(n + 1); // 6 +print(a as? String); // None +``` + +The same goes for assigning it, passing it to a function, returning it from one, iterating over it, calling it, and using an operator on it: each needs something that takes an `Any`, or a cast. -let b: Any = "five"; -let m: Int = b; // error: expected Int but found String +```ndc,fails +let a: Any = 5; +a + 1 // error: No function called '+' found that matches the arguments 'Any, Int' ``` -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. +A call with an `Any` argument only uses an overload that takes an `Any` there: one whose parameter is `Any`, or has no annotation, which is then an `Any` in its body. A cast picks another: + +```ndc +fn describe(x: Int) => "an int"; +fn describe(x: Any) => "something"; +let a: Any = 5; +print(describe(a), describe(a as Int)); // something an int +``` -You can also check it yourself with a [cast](../features/casts.md): `value as List`. +Functions like `print`, `string` and `==` take a value of any type as it is, so they take an `Any` too. diff --git a/manual/src/reference/types/function.md b/manual/src/reference/types/function.md index 4174027d..cf4dcc20 100644 --- a/manual/src/reference/types/function.md +++ b/manual/src/reference/types/function.md @@ -226,7 +226,7 @@ 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 unlike a function declared with `fn` it can't take an `Int` in one call and a `Float` in the next. A lambda parameter that nothing gives a type is `Any`, and is checked when the program runs. +A lambda stored in a variable has one type, like any variable, so unlike a function declared with `fn` it can't take an `Int` in one call and a `Float` in the next. A lambda parameter that nothing gives a type is `Any`, which its body then has to work with: in `let f = fn(x) => x + 1;` with nothing calling `f`, no `+` takes `x`, so annotate it as `fn(x: Int)`. ## Generic functions @@ -258,7 +258,13 @@ shout("a"); // error: No function called '+' found that matches the arguments // `shout` calls `inc` here, and `inc` needs `x + 1` ``` -To take any value whatever its type, annotate the parameter with `Any`: the body then works with it as the program runs. +To take any value whatever its type, annotate the parameter with `Any`: the body then works with an `Any`, which takes a [cast](../../features/casts.md) to be used as anything else. A call with an `Any` argument has an unannotated parameter be an `Any` too, so its body has to work with one: + +```ndc,fails +fn inc(x) => x + 1; +let a: Any = 1; +inc(a); // error: No function called '+' found that matches the arguments 'Any, Int' +``` ## Function overloading @@ -284,6 +290,16 @@ print(describe("a")); // string print(describe([1])); // something else ``` +An argument of type `Any` only goes to an overload that takes an `Any`, whatever it holds as the program runs, and a cast picks another: + +```ndc +fn describe(x: Int) { "int" } +fn describe(x: Any) { "something else" } + +let a: Any = 1; +print(describe(a), describe(a as Int)); // something else int +``` + Two overloads in the same scope can't have the same parameter types. Parameter names don't count: ```ndc,fails diff --git a/manual/src/reference/types/struct.md b/manual/src/reference/types/struct.md index 2389be8a..b2f6c850 100644 --- a/manual/src/reference/types/struct.md +++ b/manual/src/reference/types/struct.md @@ -144,14 +144,14 @@ needs parentheses around the member access: `(s.f)()` first evaluates `s.f` (the getter) and then calls its result. ```ndc -struct Callback { f: Any } +struct Callback { f: Fn(Int) -> Int } let cb = Callback(fn (a) => a * 2); print((cb.f)(21)); // 42 ``` ```ndc,fails -struct Callback { f: Any } +struct Callback { f: Fn(Int) -> Int } let cb = Callback(fn (a) => a * 2); cb.f(21); // ERROR: this is method-call syntax for `f(cb, 21)` diff --git a/manual/src/reference/types/tuple.md b/manual/src/reference/types/tuple.md index 9aba9359..af9152a8 100644 --- a/manual/src/reference/types/tuple.md +++ b/manual/src/reference/types/tuple.md @@ -35,7 +35,7 @@ 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`. +An index computed as the program runs, like `t[i]`, could pick any element, so it gives `Any`, which takes a [cast](../../features/casts.md) to say what it is: `t[i] as Int`. Create a 1-element tuple by adding a trailing comma: diff --git a/manual/src/reference/variables-and-scopes.md b/manual/src/reference/variables-and-scopes.md index 54cfbc31..97af3be9 100644 --- a/manual/src/reference/variables-and-scopes.md +++ b/manual/src/reference/variables-and-scopes.md @@ -81,7 +81,7 @@ 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`. +When nothing settles them, they stay `Any`, which takes a [cast](../features/casts.md) to be used as anything else. A variable that only gets its real value later holds an `Option`: diff --git a/manual/src/troubleshooting/overload-dispatch-collections.md b/manual/src/troubleshooting/overload-dispatch-collections.md index d918dcd2..9e04c2e4 100644 --- a/manual/src/troubleshooting/overload-dispatch-collections.md +++ b/manual/src/troubleshooting/overload-dispatch-collections.md @@ -2,30 +2,35 @@ ## Background -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. +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. When every argument type is known and no overload can accept them, the call could only ever fail, so it is rejected before the program runs 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. +## An `Any` takes a cast -## Element types cost a scan +An argument of type `Any`, because it was annotated as `Any` or comes from a function that returns `Any`, only goes to an overload that takes an `Any`. Two overloads like these take none: -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? | -| `Iterable` | Is the value any sequence type? | -| `String`, `Int`, … | Exact kind check | +```ndc,fails +fn process(xs: List) => "ints"; +fn process(xs: List) => "strings"; +let value: Any = [1]; +process(value) +// error[analysis]: No function called 'process' found that matches the arguments 'Any' +``` -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`: +A [cast](../features/casts.md) says which one is meant. It checks the value as the program runs, and for a list that scans each element, but the cost lands at a site you wrote: +```ndc +fn process(xs: List) => "ints"; +fn process(xs: List) => "strings"; +let value: Any = [1]; +print(process(value as List)); // ints ``` -fn process(xs: List) -fn process(xs: List) -``` -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. +To find out which one it is, test it with `as?`, which gives `None` when the value isn't one: + +```ndc +let value: Any = ["a"]; +print(value as? List, value as? List); // None Some(["a"]) +``` ## A `List` fits no typed overload @@ -37,7 +42,7 @@ fn sum(Iterable) -> Float fn sum(Iterable) -> Number ``` -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: +A value known only as a `List` fits none of them, as its elements could be anything, so the call is rejected before it runs: ```ndc,fails let values: List = [1, 2, 3]; @@ -49,7 +54,7 @@ values.sum() ## 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 call: +State the element type with a [cast](../features/casts.md): ```ndc let values: List = [1, 2, 3]; @@ -58,4 +63,4 @@ let values: List = [1, 2, 3]; print((values as List).sum()); // 6 ``` -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. +Better still, leave out the `Any`: a value keeps the type it was created with unless it's stored as an `Any`, and a parameter without an annotation takes the type of each call's argument. diff --git a/ndc_analyser/src/analyser/assignments.rs b/ndc_analyser/src/analyser/assignments.rs index 596d1b3d..96a8a029 100644 --- a/ndc_analyser/src/analyser/assignments.rs +++ b/ndc_analyser/src/analyser/assignments.rs @@ -229,8 +229,8 @@ impl Analyser { /// `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. + /// value only fits where an `Any` is kept: it takes a cast to be stored + /// as anything else. pub(super) fn accepts_write( &mut self, stored_type: &StaticType, diff --git a/ndc_analyser/src/analyser/calls.rs b/ndc_analyser/src/analyser/calls.rs index fef46dc0..83c8a265 100644 --- a/ndc_analyser/src/analyser/calls.rs +++ b/ndc_analyser/src/analyser/calls.rs @@ -29,6 +29,8 @@ impl Analyser { hides_functions, span, ) + } else if types.contains(&StaticType::Any) { + AnalysisError::function_not_found_for_any(name, types, span) } else if overloads.a_cast_could_resolve(candidates, types) { AnalysisError::function_not_found_suggest_cast(name, types, span) } else { diff --git a/ndc_analyser/src/analyser/generics.rs b/ndc_analyser/src/analyser/generics.rs index 0a95da4d..e9452e8a 100644 --- a/ndc_analyser/src/analyser/generics.rs +++ b/ndc_analyser/src/analyser/generics.rs @@ -422,14 +422,18 @@ impl Analyser { /// 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. + /// can take the arguments, picked as the program runs. Its type + /// parameters, and the `Any` that a call picked as it runs gives, stand + /// for any type there: an overload that takes an Int may get one. Each + /// call of the function checks its body with the types it's given, so + /// an `Any` the program wrote was checked there. 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) + matches!(part, StaticType::Any).then(|| StaticType::Generic("T".into())) }) }) .collect::>(); diff --git a/ndc_analyser/src/analyser/solver.rs b/ndc_analyser/src/analyser/solver.rs index 1bfb7982..32d457b5 100644 --- a/ndc_analyser/src/analyser/solver.rs +++ b/ndc_analyser/src/analyser/solver.rs @@ -328,6 +328,11 @@ impl Analyser { /// 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 self.vars.resolve(sequence) == StaticType::Any { + let error = AnalysisError::not_iterable(&StaticType::Any, span); + self.emit(error); + return StaticType::Error; + } if let Some(element) = self.vars.element_type(sequence) { return element; } @@ -483,6 +488,13 @@ impl Analyser { } binding => binding, }; + // A call that nothing takes any more is one an error + // was reported for, as an argument whose overload + // couldn't be told then got the fallback `Any`, and a + // program with errors doesn't run. + if binding == Binding::None && !self.result.errors.is_empty() { + continue; + } assert!( binding == picked, "internal error: `{name}` committed to {picked:?} before its arguments \ @@ -500,6 +512,11 @@ impl Analyser { continue; } let choice = self.choose_function_value(&value, &expected); + // As for a call: a value nothing fits any more got the + // fallback `Any` from a failed use, which was reported. + if choice == Choice::None && !self.result.errors.is_empty() { + continue; + } assert!( choice == Choice::One(picked), "internal error: the function value {value:?} committed to {picked:?} \ @@ -752,6 +769,11 @@ impl Analyser { Some(held) if self.vars.is_literal(var) => { self.vars.settle_literal(var, &StaticType::Var(held)); } + // A literal that holds only values of its own type, + // as `[x]` does once a parameter `x` is its type, is + // what they are, which isn't known yet: settling it + // as `Any` would make `x` one. + _ if self.vars.settle_literal_as_its_values(var) => {} _ => self.vars.settle_with_fallbacks(&[var]), } } @@ -1387,12 +1409,13 @@ impl Analyser { } /// 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. + /// operand that isn't known yet can be one, but an `Any` takes a cast + /// to 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)) + .any(|argument| matches!(argument, StaticType::Var(_))) } /// The overload a call of `candidates` with arguments of these types @@ -1754,10 +1777,10 @@ impl Analyser { }, ) = signature else { - // What's passed to a function that takes anything, as `print` - // does, is an `Any`, and so are the values of a literal among - // them that share no type. What's passed to an overload the - // program picks isn't known, but a literal says what it is. + // A function that takes anything, as `print` does, takes each + // value as the type it is, as a type parameter would, so it + // settles nothing. What's passed to an overload the program + // picks isn't known, but a literal says what it is. let variadic = matches!( signature, Some(StaticType::Function { @@ -1765,12 +1788,11 @@ impl Analyser { .. }) ); - for argument in arguments { - if variadic { - self.vars.bind_argument(&StaticType::Any, argument); + if !variadic { + for argument in arguments { + self.vars + .settle_unknowns_but_literals(argument, &StaticType::Any); } - self.vars - .settle_unknowns_but_literals(argument, &StaticType::Any); } return Some(return_type); }; diff --git a/ndc_analyser/src/analyser/tests.rs b/ndc_analyser/src/analyser/tests.rs index fc6b348d..6c3ed843 100644 --- a/ndc_analyser/src/analyser/tests.rs +++ b/ndc_analyser/src/analyser/tests.rs @@ -259,20 +259,16 @@ fn annotated_declaration_rejects_a_mismatched_container_literal() { ); } -// `f` might hold any function, so which one runs is only known at runtime. +// An `Any` takes a cast to be called, which says what function it holds. #[test] -fn a_call_of_a_value_of_unknown_type_dispatches_at_runtime() { - let (_, result) = analyse_with_globals("fn call(f: Any) { f(\"x\") }", vec![]); - assert!(result.errors.is_empty(), "errors: {:?}", result.errors); - let callee = result - .analysis - .bindings - .values() - .exactly_one() - .expect("`f` is the only identifier"); - assert!( - matches!(callee, Binding::Dynamic(candidates) if candidates.len() == 1), - "{callee:?}" +fn calling_an_any_takes_a_cast() { + assert_eq!( + errors("fn call(f: Any) { f(\"x\") }", vec![]), + vec!["The variable 'f' is Any, which can't be called"] + ); + assert_eq!( + analyse_last_type("fn call(f: Any) { (f as Fn(String) -> Int)(\"x\") } call"), + function(vec![StaticType::Any], StaticType::Int) ); } @@ -445,7 +441,7 @@ fn a_variable_keeps_its_inferred_type() { } #[test] -fn an_any_value_can_be_stored_anywhere() { +fn an_any_value_takes_a_cast_to_be_stored_in_a_typed_variable() { let any_value = vec![( "anything".to_string(), StaticType::Function { @@ -454,9 +450,18 @@ fn an_any_value_can_be_stored_anywhere() { }, )]; assert_eq!( - analyse_last_type_with_globals("let x = 3; x = anything(); x", any_value), + errors("let x = 3; x = anything(); x", any_value.clone()), + vec!["mismatched types: found Any but expected Int"] + ); + assert_eq!( + analyse_last_type_with_globals("let x = 3; x = anything() as Int; x", any_value.clone()), StaticType::Int ); + // Anything goes into an `Any` without one. + assert_eq!( + analyse_last_type_with_globals("let x: Any = anything(); x = 3; x", any_value), + StaticType::Any + ); } #[test] @@ -535,16 +540,20 @@ fn each_call_instantiates_the_signature_afresh() { } #[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. +fn an_any_passed_for_a_list_takes_a_cast_that_says_its_elements() { + // `xs` could hold anything, so `reversed` doesn't take it, but a cast + // says what it holds, which settles `T`. 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::Any]), - return_type: Box::new(StaticType::List(Box::new(StaticType::Any))), - } + errors("fn f(xs: Any) { reversed(xs) }", globals.clone()), + vec!["No function called 'reversed' found that matches the arguments 'Any'"] + ); + assert_eq!( + analyse_last_type_with_globals("fn f(xs: Any) { reversed(xs as List) } f", globals), + function( + vec![StaticType::Any], + StaticType::List(Box::new(StaticType::Int)) + ) ); } @@ -960,13 +969,13 @@ fn a_call_nothing_else_settles_takes_no_narrowest_overload_when_unsure() { 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. + // Nothing is known about `x`, so it's `Any`, which 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" ), - function(StaticType::Any, StaticType::Any) + function(StaticType::Any, StaticType::String) ); // Overloads in two scopes fit, so none settles `x`: it's `Any`, and // `[x]` is a `List`, which neither takes. @@ -1085,17 +1094,24 @@ fn an_operator_waits_while_an_operand_could_still_be_a_tuple() { } #[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. +fn an_operator_takes_no_any_operand_that_no_overload_takes() { + // `a` could be a tuple that `++` applies to element-wise, or a list, + // but it takes a cast to be either. let globals = natives(&[("++", "(List, List) -> List")]); - let (_, result) = analyse_with_globals("let g = fn(a: Any) => a ++ [];", globals); + assert_eq!( + errors("let g = fn(a: Any) => a ++ [];", globals.clone()), + vec!["No function called '++' found that matches the arguments 'Any, List'"] + ); + let (_, result) = + analyse_with_globals("let g = fn(a: Any) => (a as List) ++ [];", 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(_))) - )); + assert!( + result + .analysis + .bindings + .values() + .all(|binding| !matches!(binding, Binding::Dynamic(_))) + ); } #[test] @@ -1211,17 +1227,6 @@ fn the_help_for_an_ambiguous_function_value_names_an_overload_that_fits() { .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] @@ -1316,10 +1321,12 @@ fn a_join_waits_until_both_branches_are_known() { } #[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)); +fn a_parameter_nothing_settles_is_an_any_that_no_overload_takes() { + // Nothing says what `x` is, so it's `Any`, which neither `+` takes. + assert_eq!( + errors("let f = fn(x) => x + 1; f", ambiguous_plus()), + vec!["No function called '+' found that matches the arguments 'Any, Int'"] + ); } /// `len`, a native taking a `String`, for the test below. @@ -1656,12 +1663,21 @@ fn a_function_whose_calls_dont_depend_on_its_types_has_no_instances() { } #[test] -fn a_call_with_any_arguments_uses_the_function_itself() { +fn a_call_with_an_any_argument_checks_the_body_with_any() { + // `x` is `Any` in this call, which no `+` takes. assert_eq!( - instance_count( + errors( "fn inc(x) => x + 1; let a: Any = 1; inc(a);", ambiguous_plus() ), + vec!["No function called '+' found that matches the arguments 'Any, Int'"] + ); + // A body that works with `Any` takes it. + assert_eq!( + instance_count( + "fn wrap(x) => [x]; let a: Any = 1; wrap(a);", + ambiguous_plus() + ), 0 ); } diff --git a/ndc_analyser/src/error.rs b/ndc_analyser/src/error.rs index 3b872166..75440d68 100644 --- a/ndc_analyser/src/error.rs +++ b/ndc_analyser/src/error.rs @@ -109,7 +109,7 @@ impl AnalysisError { Self { text: format!("mismatched types: found {found} but expected {expected}"), span, - help_text: None, + help_text: (*found == StaticType::Any).then(|| cast_out_of_any(&expected.to_string())), notes: Vec::new(), } } @@ -133,7 +133,7 @@ impl AnalysisError { Self { text: format!("{typ} is not iterable"), span, - help_text: None, + help_text: (*typ == StaticType::Any).then(|| cast_out_of_any("List")), notes: Vec::new(), } } @@ -270,14 +270,17 @@ impl AnalysisError { ), other => format!("The variable '{name}' is {other}, which can't be called"), }; + let help_text = if hides_functions { + Some(format!( + "A variable hides the functions named '{name}' further out: it doesn't join their overloads" + )) + } else { + (*variable_type == StaticType::Any).then(|| cast_out_of_any("Fn(Int) -> Int")) + }; Self { text, span, - help_text: hides_functions.then(|| { - format!( - "A variable hides the functions named '{name}' further out: it doesn't join their overloads" - ) - }), + help_text, notes: Vec::new(), } } @@ -308,6 +311,20 @@ impl AnalysisError { } } + /// Same as [`Self::function_not_found`], for a call with an argument of + /// type `Any` that only overloads taking something else could take: + /// `a + 1` with `a: Any`. + pub(crate) fn function_not_found_for_any( + ident: &str, + types: &[StaticType], + span: Span, + ) -> Self { + Self { + help_text: Some(cast_out_of_any("Int")), + ..Self::function_not_found(ident, types, span) + } + } + /// Same as [`Self::function_not_found`], for a call an overload would /// accept if the arguments were known more precisely. pub(crate) fn function_not_found_suggest_cast( @@ -408,8 +425,16 @@ impl AnalysisError { Self { text: format!("Unable to invoke {typ} as a function."), span, - help_text: None, + help_text: (*typ == StaticType::Any).then(|| cast_out_of_any("Fn(Int) -> Int")), notes: Vec::new(), } } } + +/// Help for a value of type `Any` used where `example`, or another type, +/// is expected. +fn cast_out_of_any(example: &str) -> String { + format!( + "A value of type `Any` only goes where `Any` is taken. Cast it to say what it holds, as in `value as {example}`, or `value as? {example}` to get an Option" + ) +} diff --git a/ndc_analyser/src/overloads.rs b/ndc_analyser/src/overloads.rs index dbb5acea..21d51e4b 100644 --- a/ndc_analyser/src/overloads.rs +++ b/ndc_analyser/src/overloads.rs @@ -50,10 +50,10 @@ struct ScalarWalk { } /// 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 +/// signature at runtime: its type isn't known yet, or it's a variadic +/// function, or a function whose parameters don't provably conflict with +/// `sig`, as the parts of either type not known yet could be anything. An +/// `Any` is not one of them: it only goes where an `Any` is taken. 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 { @@ -72,7 +72,10 @@ fn could_take(typ: &StaticType, sig: &[StaticType]) -> bool { parameters: None, .. } => true, // A value whose type isn't known may hold a function. - unknown => unknown.is_unknown(), + unknown => matches!( + unknown, + StaticType::Var(_) | StaticType::Generic(_) | StaticType::Error + ), } } @@ -568,8 +571,10 @@ impl<'a> Overloads<'a> { /// * `Static` — at least one arg is statically a non-empty tuple and /// every tuple-shaped arg shares that length. Positions *may* contain /// empty candidate lists; the caller surfaces that as a call error. - /// * `AnyFallback` — no static tuple shape but at least one arg is `Any`, - /// so vec might still apply at runtime; the runtime narrows per pair. + /// * `AnyFallback` — no static tuple shape but at least one arg isn't + /// known, as in a generic function's own body, so vec might still apply + /// at runtime; the runtime narrows per pair. An `Any` isn't one: it + /// takes a cast to be a tuple. /// * `None` — no possible vec interpretation. fn resolve_vec(self, candidates: &Candidates, sig: &[StaticType]) -> Option { if let Some((axis_len, positions)) = self.resolve_vec_static(candidates, sig) { @@ -578,10 +583,17 @@ impl<'a> Overloads<'a> { positions, }); } - if !sig.iter().any(StaticType::is_unknown) { + let unknown = |typ: &StaticType| { + matches!( + typ, + StaticType::Var(_) | StaticType::Generic(_) | StaticType::Error + ) + }; + if !sig.iter().any(unknown) { return None; } - let permissive: Vec = vec![StaticType::Any; sig.len()]; + // Each argument could be anything, as a type parameter could. + let permissive: Vec = vec![StaticType::Generic("T".into()); sig.len()]; let symbols = self.candidates_for_sig(candidates, &permissive); if symbols.is_empty() { None @@ -865,10 +877,10 @@ mod tests { ); } - // An `Any` argument might be an `Int` or a `String`, so both overloads stay - // candidates, and the call returns what either of them could. + // An `Any` argument goes only where `Any` is taken: it takes a cast + // to be an `Int` or a `String`. #[test] - fn overloads_that_might_accept_the_call_are_picked_at_runtime() { + fn an_any_argument_takes_no_overload_of_another_type() { let list_of = |typ| StaticType::List(Box::new(typ)); let (types, foos) = table(vec![ fn_type(vec![StaticType::Int], list_of(StaticType::Int)), @@ -880,66 +892,36 @@ mod tests { &[StaticType::Any], CallKind::Regular, ); - let Binding::Dynamic(picked) = resolved.binding else { - panic!("expected a dynamic binding, got {:?}", resolved.binding); - }; - assert_eq!(picked.len(), 2); - assert!( - foos.iter() - .all(|&foo| picked.contains(&Candidate::Scalar(foo))) - ); - // A List and a List have no type in common but `Any`. - assert_eq!(resolved.return_type, StaticType::Any); + assert_eq!(resolved.binding, Binding::None); } #[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. + fn an_any_argument_takes_the_overload_that_takes_any() { + // `show(Int)` doesn't take an `Any`, whatever it holds as the + // program runs, so `show(Any)` takes the call, 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:?}" - ); + assert_resolves_to(&types, &candidates(&[&group]), &[StaticType::Any], shows[1]); } } #[test] - fn a_fallback_left_to_the_program_still_hides_the_overloads_further_out() { + fn an_any_argument_takes_the_nearest_overload_that_takes_any() { // 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( + assert_resolves_to( &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]) - ]), + shows[2], ); } diff --git a/ndc_analyser/src/types.rs b/ndc_analyser/src/types.rs index dca9d97d..3bcdac80 100644 --- a/ndc_analyser/src/types.rs +++ b/ndc_analyser/src/types.rs @@ -556,6 +556,25 @@ impl TypeVars { self.unify(&StaticType::Var(var), typ) } + /// Settles the literal type `var` as a new variable of its level, when + /// the values it holds are all of its own type, and returns whether it + /// did. In `fn f(x) { [x] ++ [x] }`, the lists' elements are what `x` + /// is, which their uses made the literals' type: what isn't known about + /// them is what isn't known about `x`, which can still be any type. + pub(crate) fn settle_literal_as_its_values(&mut self, var: TypeVar) -> bool { + let own = StaticType::Var(var); + let held = self.held(var); + if held.is_empty() || held.iter().any(|(typ, _)| self.resolve(typ) != own) { + return false; + } + let plain = self.next_var(); + self.solutions.push(None); + self.fallbacks.push(StaticType::Any); + self.levels.push(self.levels[var.0 as usize]); + self.bounds.push(Vec::new()); + self.settle_literal(var, &StaticType::Var(plain)) + } + /// Starts the body of a generic function: the variables made until /// [`Self::leave_level`] belong to it. pub(crate) fn enter_level(&mut self) { @@ -949,8 +968,9 @@ impl TypeVars { /// 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 + /// that a parameter of type `Any` takes anything. An argument of type + /// `Any` only goes where an `Any` is taken: it takes a cast to be passed + /// as anything else. 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 @@ -967,14 +987,12 @@ impl TypeVars { self.settle_literal(*var, &StaticType::Any) } (_, 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 - } + // A value of type `Any` doesn't make a literal's type `Any`, so it + // doesn't go where the literal's values are: `n` in `let n = 1; + // n = a` is an Int, which `a` takes a cast to be. A value whose + // check failed leaves the literal's type what the literal says. + (StaticType::Var(var), StaticType::Error) if self.is_literal(*var) => true, + (StaticType::Var(var), StaticType::Any) if self.is_literal(*var) => false, (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 @@ -989,7 +1007,7 @@ impl TypeVars { 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::Error) | (StaticType::Error, _) => true, ( StaticType::Function { parameters: expected, diff --git a/ndc_core/src/static_type.rs b/ndc_core/src/static_type.rs index a7211aee..d1fc294a 100644 --- a/ndc_core/src/static_type.rs +++ b/ndc_core/src/static_type.rs @@ -295,12 +295,11 @@ impl StaticType { /// 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`. + /// except that the parts of this type not known yet stand for any type + /// too. `Any` is not one of them: it goes only where `Any` is taken, + /// and takes a cast to go anywhere else. pub fn may_fit(&self, parameter: &Self) -> bool { - matches!(self, Self::Any) || self.fits(parameter) || self.matches(parameter, true) + self.fits(parameter) || self.matches(parameter, true) } /// Whether this type is `parameter`, where what [`Self::fits`] and @@ -608,11 +607,14 @@ impl StaticType { self.sequence_element_type() } - /// Returns `true` if this type could be a callable function at runtime. - /// This is true for `Function` types and for `Any` (which might be a - /// function at runtime). Concrete non-function types return `false`. + /// Whether a value of this type can be called: a function, or a value + /// whose type isn't known yet or whose check failed. An `Any` takes a + /// cast to a function type to be called, like any other use. pub fn could_be_callable(&self) -> bool { - matches!(self, Self::Function { .. }) || self.is_unknown() + matches!( + self, + Self::Function { .. } | Self::Var(_) | Self::Generic(_) | Self::Error + ) } pub fn is_fn_and_matches(&self, types: &[Self]) -> bool { @@ -638,9 +640,10 @@ impl StaticType { /// `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. + /// to fit it: `push(List, String)` fits `push(List, T)`, but + /// `push(List, Any)` doesn't. What isn't known yet agrees with + /// anything, but `Any` is a type like any other: `(Int, Any)` doesn't fit + /// `(T, T)`. pub fn type_parameters_agree(parameters: &[Self], arguments: &[Self]) -> bool { let mut bound = Vec::new(); for (parameter, argument) in parameters.iter().zip(arguments) { @@ -654,16 +657,12 @@ impl StaticType { // 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) - } + Some((_, exact, _)) => 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) - }), + .all(|(_, earlier, _)| earlier.agrees_with(typ)), } }) } @@ -849,8 +848,8 @@ mod test { 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)); + // An `Any` goes only where `Any` is taken, whatever it holds. + assert!(!StaticType::Any.may_fit(&list_of_two_tuple_int)); } #[test] diff --git a/tests/compiler/tests/compiler.rs b/tests/compiler/tests/compiler.rs index 1b3ed8ef..f287e71b 100644 --- a/tests/compiler/tests/compiler.rs +++ b/tests/compiler/tests/compiler.rs @@ -636,16 +636,19 @@ fn a_proven_optional_cast_is_always_some() { ); } -// let a: Any = 1; let n: Int = a; +// let a: Any = 1; let n: Int = a as Int; // -// An `Any` stored where an Int is expected is checked before it's stored. +// An `Any` stored where an Int is expected takes a cast, which is the only +// check: storing what the cast gives needs none. #[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!( +fn an_any_cast_for_a_typed_variable_is_checked_once() { + let ops = compile_with_analysis("let a: Any = 1; let n: Int = a as Int;"); + assert_eq!( ops.iter() - .any(|op| matches!(op, ExpectType(t) if **t == ndc_core::StaticType::Int)), - "expected an ExpectType(Int) guard, got: {ops:?}", + .filter(|op| matches!(op, CheckType(_) | ExpectType(_))) + .count(), + 1, + "expected a single CheckType(Int) guard, got: {ops:?}", ); } diff --git a/tests/functional/programs/004_basic/055_unified_op_assign.ndc b/tests/functional/programs/004_basic/055_unified_op_assign.ndc index e82fbdcd..9156c560 100644 --- a/tests/functional/programs/004_basic/055_unified_op_assign.ndc +++ b/tests/functional/programs/004_basic/055_unified_op_assign.ndc @@ -13,11 +13,12 @@ sets[0] -= %{2}; assert_eq(set, %{1, 3}); assert_eq(sets[0], %{1, 3}); -// An Any-typed indexed value resolves dynamically. op= candidates have -// priority over ordinary op candidates, and their returned lhs is written back. +// An Any-typed indexed value takes a cast to be a list. The cast gives the +// same list, which the op= candidate updates in place. let dynamic_alias = [10]; let dynamic_values: List = [dynamic_alias]; -dynamic_values[0] ++= [20]; +let dynamic_element = dynamic_values[0] as List; +dynamic_element ++= [20]; assert_eq(dynamic_alias, [10, 20]); assert_eq(dynamic_values[0], [10, 20]); 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 deleted file mode 100644 index e521249d..00000000 --- a/tests/functional/programs/004_basic/066_op_assignment_any_rhs.ndc +++ /dev/null @@ -1,8 +0,0 @@ -// `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/066_op_assignment_takes_an_any_operand_through_a_cast.ndc b/tests/functional/programs/004_basic/066_op_assignment_takes_an_any_operand_through_a_cast.ndc new file mode 100644 index 00000000..d607b5f9 --- /dev/null +++ b/tests/functional/programs/004_basic/066_op_assignment_takes_an_any_operand_through_a_cast.ndc @@ -0,0 +1,7 @@ +// `a op= b` with an `Any` right operand takes a cast that says what it is, +// which is checked as the program runs. +// expect-output: [1,2,3] +fn opaque(x) -> Any => x; +let values = [1]; +values ++= opaque([2, 3]) as List; +print(values) 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 deleted file mode 100644 index 1ccc82a9..00000000 --- a/tests/functional/programs/004_basic/076_op_assignment_checks_what_an_any_operand_makes.ndc +++ /dev/null @@ -1,6 +0,0 @@ -// 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/076_op_assignment_takes_no_any_operand.ndc b/tests/functional/programs/004_basic/076_op_assignment_takes_no_any_operand.ndc new file mode 100644 index 00000000..3b11351e --- /dev/null +++ b/tests/functional/programs/004_basic/076_op_assignment_takes_no_any_operand.ndc @@ -0,0 +1,5 @@ +// No `++` takes a `List` and an `Any`, so `a op= b` doesn't either. +// expect-error: No function called '++' found that matches the arguments 'List, Any' +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 deleted file mode 100644 index dd0609b7..00000000 --- a/tests/functional/programs/004_basic/077_op_assignment_checks_an_any_target_before_the_call.ndc +++ /dev/null @@ -1,7 +0,0 @@ -// 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/077_op_assignment_takes_no_any_target.ndc b/tests/functional/programs/004_basic/077_op_assignment_takes_no_any_target.ndc new file mode 100644 index 00000000..3dadbc52 --- /dev/null +++ b/tests/functional/programs/004_basic/077_op_assignment_takes_no_any_target.ndc @@ -0,0 +1,7 @@ +// The target of `a op= b` is passed to the operator, which takes an Int, so +// a target of type `Any` doesn't fit. +// expect-error: No function called 'choose' found that matches the arguments 'Any, Int' +fn choose(a: Int, b: Int) => a; +let x: Any = "wrong"; +x choose= 1; +print(x); diff --git a/tests/functional/programs/005_functions/033_let_binding_shadows_let_binding.ndc b/tests/functional/programs/005_functions/033_let_binding_shadows_let_binding.ndc index 92944ea1..71c08422 100644 --- a/tests/functional/programs/005_functions/033_let_binding_shadows_let_binding.ndc +++ b/tests/functional/programs/005_functions/033_let_binding_shadows_let_binding.ndc @@ -1,6 +1,6 @@ // A second let binding fully shadows the first, no overloading. -let foo = fn(a) => "first:" ++ a; -let foo = fn(a, b) => "second:" ++ a ++ "," ++ b; +let foo = fn(a: String) => "first:" ++ a; +let foo = fn(a: String, b: String) => "second:" ++ a ++ "," ++ b; // Only the 2-arg version exists now. assert_eq(foo("x", "y"), "second:x,y"); 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 deleted file mode 100644 index cebae648..00000000 --- a/tests/functional/programs/005_functions/081_a_parameter_the_body_types_is_checked_as_the_program_runs.ndc +++ /dev/null @@ -1,8 +0,0 @@ -// `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/081_an_any_argument_picks_the_overload_that_takes_any.ndc b/tests/functional/programs/005_functions/081_an_any_argument_picks_the_overload_that_takes_any.ndc new file mode 100644 index 00000000..057f5f39 --- /dev/null +++ b/tests/functional/programs/005_functions/081_an_any_argument_picks_the_overload_that_takes_any.ndc @@ -0,0 +1,13 @@ +// `pick`'s body makes `x` an Int, so a value of type `Any` goes to the `Any` +// overload, whatever it holds. A type test tells an Int apart, which then +// goes to the other one. +// expect-output: fallback fallback 1 1 fallback +fn need_int(x: Int) => x; +fn pick(x) => need_int(x); +fn pick(x: Any) => "fallback"; +fn via(x: Any) => pick(x); +fn tested(x: Any) -> Any { + let n = x as? Int; + if n.is_some() { pick(n.unwrap()) } else { pick(x) } +} +print(via(1), via("a"), pick(1), tested(1), tested("a")); diff --git a/tests/functional/programs/005_functions/086_an_any_takes_a_cast_to_pick_an_overload_by_its_elements.ndc b/tests/functional/programs/005_functions/086_an_any_takes_a_cast_to_pick_an_overload_by_its_elements.ndc new file mode 100644 index 00000000..cf1a8b15 --- /dev/null +++ b/tests/functional/programs/005_functions/086_an_any_takes_a_cast_to_pick_an_overload_by_its_elements.ndc @@ -0,0 +1,8 @@ +// Neither overload takes a value of type `Any`, so a cast says which list it +// is, which checks the elements as the program runs. +// 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 as List), process(b as List)); 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 deleted file mode 100644 index aafe0268..00000000 --- a/tests/functional/programs/005_functions/086_runtime_dispatch_checks_elements_when_nothing_else_fits.ndc +++ /dev/null @@ -1,8 +0,0 @@ -// 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/095_a_generic_body_called_with_an_any_has_its_calls_take_one.ndc b/tests/functional/programs/005_functions/095_a_generic_body_called_with_an_any_has_its_calls_take_one.ndc new file mode 100644 index 00000000..7cd246b2 --- /dev/null +++ b/tests/functional/programs/005_functions/095_a_generic_body_called_with_an_any_has_its_calls_take_one.ndc @@ -0,0 +1,5 @@ +// `f` called with an `Any` has `s` be one, which `enumerate` doesn't take. +// expect-error: No function called 'enumerate' found that matches the arguments 'Any' +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/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 deleted file mode 100644 index 5dfc0082..00000000 --- a/tests/functional/programs/005_functions/095_a_generic_body_s_own_calls_are_picked_as_it_runs.ndc +++ /dev/null @@ -1,7 +0,0 @@ -// 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 deleted file mode 100644 index 570c4a52..00000000 --- a/tests/functional/programs/005_functions/096_an_any_value_passed_for_a_type_parameter_is_checked.ndc +++ /dev/null @@ -1,12 +0,0 @@ -// 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/096_an_any_value_pushed_into_a_literal_makes_it_hold_anys.ndc b/tests/functional/programs/005_functions/096_an_any_value_pushed_into_a_literal_makes_it_hold_anys.ndc new file mode 100644 index 00000000..30923215 --- /dev/null +++ b/tests/functional/programs/005_functions/096_an_any_value_pushed_into_a_literal_makes_it_hold_anys.ndc @@ -0,0 +1,12 @@ +// A value of type `Any` pushed into a list the function makes has that list +// hold `Any`s, whatever each call passes for `x`: `with_five("s")` is a +// `List`, not a `List`. +// expect-output: [1,5] ["s",5] +fn with_five(x) { + let five: Any = 5; + let xs = [x]; + xs.push(five); + xs +} +let anything: List = with_five("s"); +print(with_five(1), anything); 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_cast_from_any_is_checked_as_a_function_type.ndc similarity index 52% rename from tests/functional/programs/005_functions/103_a_function_value_left_to_the_program_is_checked_as_a_function_type.ndc rename to tests/functional/programs/005_functions/103_a_function_value_cast_from_any_is_checked_as_a_function_type.ndc index cb958cbc..664533a5 100644 --- 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_cast_from_any_is_checked_as_a_function_type.ndc @@ -1,11 +1,11 @@ -// A function value whose overload the program picks, used as a function -// type through `Any`, gets its calls checked like any function. +// A function value whose overload the program picks, cast to a function type +// from `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; + let b = a as Fn(Int) -> String; b(1) } let h: Fn(Int) -> String = bad; diff --git a/tests/functional/programs/005_functions/109_a_generic_function_used_as_a_value_applies_an_operator_to_tuples.ndc b/tests/functional/programs/005_functions/109_a_generic_function_used_as_a_value_applies_an_operator_to_tuples.ndc new file mode 100644 index 00000000..ba02aff6 --- /dev/null +++ b/tests/functional/programs/005_functions/109_a_generic_function_used_as_a_value_applies_an_operator_to_tuples.ndc @@ -0,0 +1,10 @@ +// A generic function used as a value runs its body compiled for any +// arguments, where `+` may get tuples, which it adds element by element, +// and so may what a call picked as the program runs gives. +// expect-output: (4,6) 3 (2,3) +fn add(a, b) => a + b; +let pairs: Fn((Int, Int), (Int, Int)) -> (Int, Int) = add; +let ints: Fn(Int, Int) -> Int = add; +fn bump(xs) => xs.first() + (1, 1); +let bumped: Fn(List<(Int, Int)>) -> (Int, Int) = bump; +print(pairs((1, 2), (3, 4)), ints(1, 2), bumped([(1, 2)])); diff --git a/tests/functional/programs/005_functions/110_a_list_of_a_parameter_keeps_the_parameter_generic.ndc b/tests/functional/programs/005_functions/110_a_list_of_a_parameter_keeps_the_parameter_generic.ndc new file mode 100644 index 00000000..d552f34a --- /dev/null +++ b/tests/functional/programs/005_functions/110_a_list_of_a_parameter_keeps_the_parameter_generic.ndc @@ -0,0 +1,10 @@ +// `[cur] ++ [cur]` makes the lists' elements what `cur` is, which each call +// says, so `cur` stays a type parameter rather than becoming an `Any` that +// `+` doesn't take. +// expect-output: (2,3) [(1,2),(1,2)] +fn f(cur) { + let next = cur + (1, 1); + let both = [cur] ++ [cur]; + print(next, both); +} +f((1, 2)); diff --git a/tests/functional/programs/006_lists/057_a_write_takes_no_any_index.ndc b/tests/functional/programs/006_lists/057_a_write_takes_no_any_index.ndc new file mode 100644 index 00000000..460db7cd --- /dev/null +++ b/tests/functional/programs/006_lists/057_a_write_takes_no_any_index.ndc @@ -0,0 +1,5 @@ +// No `[]=` for a list takes an index of type `Any`. +// expect-error: No function called '[]=' found that matches the arguments 'List, Any, Int' +let xs = ["a".len()]; +let i: Any = 0; +xs[i] = 1; 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 deleted file mode 100644 index b6c0ce31..00000000 --- a/tests/functional/programs/006_lists/057_a_write_with_an_any_index_checks_the_value.ndc +++ /dev/null @@ -1,5 +0,0 @@ -// 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_cast_picks_by_the_index.ndc similarity index 69% rename from tests/functional/programs/006_lists/058_a_write_with_an_any_index_picks_by_the_index.ndc rename to tests/functional/programs/006_lists/058_a_write_with_an_any_index_cast_picks_by_the_index.ndc index 5db14f8b..90e70f19 100644 --- 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_cast_picks_by_the_index.ndc @@ -1,8 +1,8 @@ // expect-output: [5] [7,8,3] let xs = ["a".len()]; let i: Any = 0; -xs[i] = 5; +xs[i as Int] = 5; let ys = [1, 2, 3]; let range: Any = 0..2; -ys[range] = [7, 8]; +ys[range as Iterator] = [7, 8]; print(xs, ys); 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 index 59a574b4..d7897f74 100644 --- 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 @@ -1,8 +1,17 @@ -// expect-output: unit int +// expect-output: any unit int fn g(x: Int) => "int"; fn g(x: ()) => "unit"; fn g(x: Any) => "any"; -// The values are `()` and an Int, so they are `Any`s, and each read -// picks its overload as the program runs. +// The values are `()` and an Int, so they are `Any`s, which go to `g(Any)`. +// A type test tells what each one holds. +fn what(x: Any) { + if (x as? Int).is_some() { + g(x as Int) + } else if (x as? ()).is_some() { + g(x as ()) + } else { + g(x) + } +} let m: Map = %{"a", "b": 1}; -print(g(m["a"]), g(m["b"])); +print(g(m["a"]), what(m["a"]), what(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 index ca8154e4..1f37de82 100644 --- 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 @@ -1,8 +1,18 @@ // The default is a String, so a read may give one, not just the Ints: the -// values are `Any`s, which pick their overload as the program runs. -// expect-output: str int +// values are `Any`s, which go to `g(Any)`. A type test tells what each one +// holds. +// expect-output: any str int fn g(x: Int) => "int"; fn g(x: String) => "str"; fn g(x: Any) => "any"; +fn what(x: Any) { + if (x as? Int).is_some() { + g(x as Int) + } else if (x as? String).is_some() { + g(x as String) + } else { + g(x) + } +} let m: Map = %{:"missing", x: x for x in 1..3}; -print(g(m[99]), g(m[1])); +print(g(m[1]), what(m[99]), what(m[1])); diff --git a/tests/functional/programs/008_iterators/002_range_contains.ndc b/tests/functional/programs/008_iterators/002_range_contains.ndc index 4cde5b6e..1ab6b321 100644 --- a/tests/functional/programs/008_iterators/002_range_contains.ndc +++ b/tests/functional/programs/008_iterators/002_range_contains.ndc @@ -24,18 +24,6 @@ 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, 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 assert_eq(1 in (1..1), false); // Single-value exclusive range diff --git a/tests/functional/programs/008_iterators/010_a_range_is_searched_only_for_an_int.ndc b/tests/functional/programs/008_iterators/010_a_range_is_searched_only_for_an_int.ndc new file mode 100644 index 00000000..91f9c592 --- /dev/null +++ b/tests/functional/programs/008_iterators/010_a_range_is_searched_only_for_an_int.ndc @@ -0,0 +1,5 @@ +// `in` takes a value of the type the range holds, which an `Any` takes a +// cast to be. +// expect-error: No function called 'in' found that matches the arguments 'Any, Iterator' +let string: Any = "string"; +print(string in (1..10)); diff --git a/tests/functional/programs/010_memoization/004_an_instance_takes_no_any_its_types_dont.ndc b/tests/functional/programs/010_memoization/004_an_instance_takes_no_any_its_types_dont.ndc new file mode 100644 index 00000000..da495b6c --- /dev/null +++ b/tests/functional/programs/010_memoization/004_an_instance_takes_no_any_its_types_dont.ndc @@ -0,0 +1,9 @@ +// The instance of `has` for a `List` takes an `Any` for `v`, which the +// one for a `List` doesn't. +// expect-error: No function called 'contains' found that matches the arguments 'List, Any' +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/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 deleted file mode 100644 index 96f49143..00000000 --- a/tests/functional/programs/010_memoization/004_an_instance_that_checks_any_has_its_own_cache.ndc +++ /dev/null @@ -1,10 +0,0 @@ -// 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 deleted file mode 100644 index 9ab35501..00000000 --- a/tests/functional/programs/010_memoization/005_an_instance_whose_callee_checks_any_has_its_own_cache.ndc +++ /dev/null @@ -1,10 +0,0 @@ -// `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/005_an_instance_whose_callee_takes_no_any_takes_none.ndc b/tests/functional/programs/010_memoization/005_an_instance_whose_callee_takes_no_any_takes_none.ndc new file mode 100644 index 00000000..199d3b69 --- /dev/null +++ b/tests/functional/programs/010_memoization/005_an_instance_whose_callee_takes_no_any_takes_none.ndc @@ -0,0 +1,10 @@ +// `outer` doesn't call anything with `v` itself, but its instance for a +// `List` calls the instance of `has` that doesn't take an `Any`. +// expect-error: No function called 'contains' found that matches the arguments 'List, Any' +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/013_vector_math/009_vector_exact_match_precision.ndc b/tests/functional/programs/013_vector_math/009_vector_exact_match_precision.ndc index d0cdd9c1..33928cae 100644 --- a/tests/functional/programs/013_vector_math/009_vector_exact_match_precision.ndc +++ b/tests/functional/programs/013_vector_math/009_vector_exact_match_precision.ndc @@ -13,13 +13,11 @@ let e: Tuple = a + b; let f: Tuple = e + (10, 20); assert_eq(f, (14, 26)); -// Any args fall to LUB (no scalar `-(Any, Any)` exists, so vec dispatch -// can't pin a single overload at compile time): `Tuple - -// Tuple` remains `Tuple` because Int, Float, and Number -// are siblings and their least upper bound is Any. +// No scalar `-(Any, Any)` exists, so a tuple of `Any`s takes a cast to the +// tuple of numbers it holds, which then keeps its precise element types. let l: List = [1, 2]; let p: Tuple = (l.first, l.first); let q: Tuple = (l.last, l.last); -let r: Tuple = p - q; -let s: Tuple = r * r; +let r: Tuple = (p as (Int, Int)) - (q as (Int, Int)); +let s: Tuple = r * r; assert_eq(s, (1, 1)); diff --git a/tests/functional/programs/015_struct/027_call_function_stored_in_field.ndc b/tests/functional/programs/015_struct/027_call_function_stored_in_field.ndc index f339b2af..0664b00e 100644 --- a/tests/functional/programs/015_struct/027_call_function_stored_in_field.ndc +++ b/tests/functional/programs/015_struct/027_call_function_stored_in_field.ndc @@ -1,7 +1,7 @@ // `s.f()` is method-call syntax (it lowers to `f(s)`), so calling a function // stored in a field requires parentheses: `(s.f)()` evaluates the getter and // calls its result. This used to silently lower to `f(s)` as well. -struct S { f: Any, n: Int } +struct S { f: Fn(Int) -> Int, n: Int } let s = S(fn (a) => a * 2, 5); assert_eq((s.f)(21), 42); 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 deleted file mode 100644 index a9953f25..00000000 --- a/tests/functional/programs/015_struct/033_a_constructor_called_through_any_checks_its_fields.ndc +++ /dev/null @@ -1,4 +0,0 @@ -// 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/033_a_constructor_through_any_takes_a_cast_to_be_called.ndc b/tests/functional/programs/015_struct/033_a_constructor_through_any_takes_a_cast_to_be_called.ndc new file mode 100644 index 00000000..e40e36f9 --- /dev/null +++ b/tests/functional/programs/015_struct/033_a_constructor_through_any_takes_a_cast_to_be_called.ndc @@ -0,0 +1,4 @@ +// expect-error: The variable 'make' is Any, which can't be called +struct P { x: Int } +let make: Any = P; +make(1); 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 deleted file mode 100644 index 620932cb..00000000 --- a/tests/functional/programs/015_struct/034_a_constructor_called_through_any_takes_a_typed_container.ndc +++ /dev/null @@ -1,5 +0,0 @@ -// 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/034_a_constructor_cast_from_any_takes_a_typed_container.ndc b/tests/functional/programs/015_struct/034_a_constructor_cast_from_any_takes_a_typed_container.ndc new file mode 100644 index 00000000..1033840d --- /dev/null +++ b/tests/functional/programs/015_struct/034_a_constructor_cast_from_any_takes_a_typed_container.ndc @@ -0,0 +1,5 @@ +// expect-output: [1,2] +struct Q { xs: List } +let make: Any = Q; +let q = make as Fn(List) -> Q; +print(q([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 deleted file mode 100644 index 2d55d6bf..00000000 --- a/tests/functional/programs/015_struct/035_a_field_written_through_any_is_checked.ndc +++ /dev/null @@ -1,5 +0,0 @@ -// 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/035_a_field_written_with_a_cast_from_any_is_checked.ndc b/tests/functional/programs/015_struct/035_a_field_written_with_a_cast_from_any_is_checked.ndc new file mode 100644 index 00000000..bc50ed2c --- /dev/null +++ b/tests/functional/programs/015_struct/035_a_field_written_with_a_cast_from_any_is_checked.ndc @@ -0,0 +1,6 @@ +// expect-error: cannot cast String to Int +struct P { x: Int } +let p: Any = P(1); +let v: Any = "s"; +let q = p as P; +q.x = v as Int; 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_with_a_cast_from_any.ndc similarity index 80% rename from tests/functional/programs/015_struct/036_a_container_field_written_through_any_is_checked.ndc rename to tests/functional/programs/015_struct/036_a_container_field_written_with_a_cast_from_any.ndc index 3475d7c4..f998aa7e 100644 --- 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_with_a_cast_from_any.ndc @@ -2,5 +2,5 @@ struct Q { xs: List } let q = Q([1]); let v: Any = [2]; -q.xs = v; +q.xs = v as List; print(q.xs); diff --git a/tests/functional/programs/016_casts/012_cyclic_cast_error.ndc b/tests/functional/programs/016_casts/012_cyclic_cast_error.ndc index 91160bfd..e8409936 100644 --- a/tests/functional/programs/016_casts/012_cyclic_cast_error.ndc +++ b/tests/functional/programs/016_casts/012_cyclic_cast_error.ndc @@ -1,6 +1,6 @@ // A self-referential container fails a typed cast instead of overflowing // the stack: the check recurses on the target type, which is finite. // expect-error: cannot cast -let x: Any = []; +let x: List = []; x.push(x); print(x as List); 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 deleted file mode 100644 index bda05a7a..00000000 --- a/tests/functional/programs/017_dynamic_any/001_an_annotated_let_takes_an_any_of_its_type.ndc +++ /dev/null @@ -1,6 +0,0 @@ -// 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/001_an_annotated_let_takes_an_any_through_a_cast.ndc b/tests/functional/programs/017_dynamic_any/001_an_annotated_let_takes_an_any_through_a_cast.ndc new file mode 100644 index 00000000..7d04529c --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/001_an_annotated_let_takes_an_any_through_a_cast.ndc @@ -0,0 +1,6 @@ +// A value of type `Any` takes a cast to be stored where an Int is expected, +// which checks that it is one as the program runs. +// expect-output: 6 +let a: Any = 5; +let n: Int = a as Int; +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 deleted file mode 100644 index 9bb57b52..00000000 --- a/tests/functional/programs/017_dynamic_any/002_an_annotated_let_checks_an_any.ndc +++ /dev/null @@ -1,4 +0,0 @@ -// 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/002_an_annotated_let_takes_no_any.ndc b/tests/functional/programs/017_dynamic_any/002_an_annotated_let_takes_no_any.ndc new file mode 100644 index 00000000..1bf4bb09 --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/002_an_annotated_let_takes_no_any.ndc @@ -0,0 +1,5 @@ +// A value of type `Any` doesn't go where an Int is expected without a cast. +// expect-error: mismatched types: found Any but expected Int +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 deleted file mode 100644 index e443bd6c..00000000 --- a/tests/functional/programs/017_dynamic_any/003_an_assignment_checks_an_any.ndc +++ /dev/null @@ -1,5 +0,0 @@ -// 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/003_an_assignment_takes_no_any.ndc b/tests/functional/programs/017_dynamic_any/003_an_assignment_takes_no_any.ndc new file mode 100644 index 00000000..60823c38 --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/003_an_assignment_takes_no_any.ndc @@ -0,0 +1,6 @@ +// `n` is an Int, which a value of type `Any` takes a cast to be. +// expect-error: mismatched types: found Any but expected Int +let a: Any = 5; +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_takes_no_any.ndc similarity index 53% rename from tests/functional/programs/017_dynamic_any/004_a_destructuring_checks_each_any.ndc rename to tests/functional/programs/017_dynamic_any/004_a_destructuring_takes_no_any.ndc index 3ba08af0..bc9d565c 100644 --- a/tests/functional/programs/017_dynamic_any/004_a_destructuring_checks_each_any.ndc +++ b/tests/functional/programs/017_dynamic_any/004_a_destructuring_takes_no_any.ndc @@ -1,4 +1,4 @@ -// expect-error: expected Int but found String +// expect-error: mismatched types: found Any but expected String let a: Any = "five"; let n = 0; let 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 deleted file mode 100644 index 0597ed09..00000000 --- a/tests/functional/programs/017_dynamic_any/005_a_return_annotation_takes_an_any_of_its_type.ndc +++ /dev/null @@ -1,5 +0,0 @@ -// 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/005_a_return_annotation_takes_an_any_through_a_cast.ndc b/tests/functional/programs/017_dynamic_any/005_a_return_annotation_takes_an_any_through_a_cast.ndc new file mode 100644 index 00000000..b8c37f50 --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/005_a_return_annotation_takes_an_any_through_a_cast.ndc @@ -0,0 +1,5 @@ +// A function annotated to return an Int returns a value of type `Any` cast +// to one, which is checked as the program runs. +// expect-output: 3 +fn first(xs: Any) -> Int { xs[0] as Int } +print(first([3])); diff --git a/tests/functional/programs/017_dynamic_any/006_a_cast_checks_an_any.ndc b/tests/functional/programs/017_dynamic_any/006_a_cast_checks_an_any.ndc new file mode 100644 index 00000000..e56db13f --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/006_a_cast_checks_an_any.ndc @@ -0,0 +1,9 @@ +// expect-error: cannot cast List to List +fn first(xs: Any) -> Int { + let ints = xs as List; + if ints.len() == 0 { + return 0; + } + ints[0] +} +print(first(["three"])); 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 deleted file mode 100644 index 9613cb48..00000000 --- a/tests/functional/programs/017_dynamic_any/006_a_return_annotation_checks_an_any.ndc +++ /dev/null @@ -1,8 +0,0 @@ -// 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 deleted file mode 100644 index 8fb8a955..00000000 --- a/tests/functional/programs/017_dynamic_any/007_a_generic_argument_checks_an_any.ndc +++ /dev/null @@ -1,7 +0,0 @@ -// `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/007_a_generic_argument_takes_no_any.ndc b/tests/functional/programs/017_dynamic_any/007_a_generic_argument_takes_no_any.ndc new file mode 100644 index 00000000..db8218cf --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/007_a_generic_argument_takes_no_any.ndc @@ -0,0 +1,7 @@ +// `push(List, T)` takes an Int for a `List`, which an argument of +// type `Any` takes a cast to be. +// expect-error: No function called 'push' found that matches the arguments 'List, Any' +let xs = [1, 2]; +let a: Any = 3; +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 deleted file mode 100644 index 692d3e4f..00000000 --- a/tests/functional/programs/017_dynamic_any/008_an_index_write_checks_an_any.ndc +++ /dev/null @@ -1,5 +0,0 @@ -// 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/008_an_index_write_takes_no_any.ndc b/tests/functional/programs/017_dynamic_any/008_an_index_write_takes_no_any.ndc new file mode 100644 index 00000000..eac3b0f4 --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/008_an_index_write_takes_no_any.ndc @@ -0,0 +1,5 @@ +// expect-error: mismatched types: found Any but expected Int +let xs = [1, 2]; +let a: Any = 3; +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 deleted file mode 100644 index f46e83ee..00000000 --- a/tests/functional/programs/017_dynamic_any/009_a_field_write_checks_an_any.ndc +++ /dev/null @@ -1,6 +0,0 @@ -// 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/009_a_field_write_takes_no_any.ndc b/tests/functional/programs/017_dynamic_any/009_a_field_write_takes_no_any.ndc new file mode 100644 index 00000000..7a5517ef --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/009_a_field_write_takes_no_any.ndc @@ -0,0 +1,6 @@ +// expect-error: mismatched types: found Any but expected Int +struct Point { x: Int, y: Int } +let p = Point(1, 2); +let a: Any = 3; +p.x = a; +print(p); diff --git a/tests/functional/programs/017_dynamic_any/010_a_cast_to_a_container_is_scanned.ndc b/tests/functional/programs/017_dynamic_any/010_a_cast_to_a_container_is_scanned.ndc new file mode 100644 index 00000000..a37f2768 --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/010_a_cast_to_a_container_is_scanned.ndc @@ -0,0 +1,4 @@ +// expect-error: cannot cast List to List +let a: Any = ["one"]; +let xs: List = a as List; +print(xs); 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 deleted file mode 100644 index c89eba8f..00000000 --- a/tests/functional/programs/017_dynamic_any/010_a_container_from_any_is_scanned.ndc +++ /dev/null @@ -1,4 +0,0 @@ -// 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_an_any_argument_makes_a_parameter_any_in_the_body.ndc b/tests/functional/programs/017_dynamic_any/011_an_any_argument_makes_a_parameter_any_in_the_body.ndc new file mode 100644 index 00000000..66ff2241 --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/011_an_any_argument_makes_a_parameter_any_in_the_body.ndc @@ -0,0 +1,10 @@ +// `put` called with an `Any` has `v` be one, which `push` takes for a list +// of `Any`s but not for a list of Ints. +// expect-error: No function called 'push' found that matches the arguments 'List, Any' +fn put(xs, v) { xs.push(v) } +let a: Any = "s"; +let anything: List = ["t"]; +put(anything, a); +print(anything); +let numbers = [1]; +put(numbers, a); 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 deleted file mode 100644 index 0adfcd33..00000000 --- a/tests/functional/programs/017_dynamic_any/011_each_instance_checks_an_any_against_its_types.ndc +++ /dev/null @@ -1,11 +0,0 @@ -// `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_cast_from_any_checks_what_it_returns.ndc similarity index 76% rename from tests/functional/programs/017_dynamic_any/012_a_function_from_any_checks_what_it_returns.ndc rename to tests/functional/programs/017_dynamic_any/012_a_function_cast_from_any_checks_what_it_returns.ndc index 7189fe54..d6b1257f 100644 --- 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_cast_from_any_checks_what_it_returns.ndc @@ -1,4 +1,4 @@ -// A function of type `Any` used as a `Fn(Int) -> Int` checks what each call +// A function of type `Any` cast to 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 @@ -6,6 +6,6 @@ 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 f = a as Fn(Int) -> Int; 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_cast_from_any_must_return_what_its_type_says.ndc similarity index 52% rename from tests/functional/programs/017_dynamic_any/013_a_function_from_any_must_return_what_its_type_says.ndc rename to tests/functional/programs/017_dynamic_any/013_a_function_cast_from_any_must_return_what_its_type_says.ndc index 397f9e9f..b3e80aec 100644 --- 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_cast_from_any_must_return_what_its_type_says.ndc @@ -1,4 +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 +// expect-error: cannot cast Fn(Any) -> String to Fn(Int) -> Int let a: Any = fn(x) => "s"; -let f: Fn(Int) -> Int = a; +let f = a as Fn(Int) -> Int; 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 index d5203a43..adce5f73 100644 --- 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 @@ -1,7 +1,7 @@ -// A function of type `Any` used as a function type is the same function: it +// A function of type `Any` cast to 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; +fn inc(x: Int) => x + 1; let a: Any = inc; -let f: Fn(Int) -> Int = a; +let f = a as Fn(Int) -> Int; print(f(2), f == inc, a == f); diff --git a/tests/functional/programs/017_dynamic_any/016_a_function_cast_from_any_must_take_what_its_type_passes.ndc b/tests/functional/programs/017_dynamic_any/016_a_function_cast_from_any_must_take_what_its_type_passes.ndc new file mode 100644 index 00000000..c421dcd3 --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/016_a_function_cast_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: cannot cast Fn(String) -> Int to Fn(Int) -> Int +let a: Any = fn(s: String) => 1; +let f = 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 deleted file mode 100644 index 8e84ae13..00000000 --- a/tests/functional/programs/017_dynamic_any/016_a_function_from_any_must_take_what_its_type_passes.ndc +++ /dev/null @@ -1,4 +0,0 @@ -// 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 deleted file mode 100644 index 8fd7d398..00000000 --- a/tests/functional/programs/017_dynamic_any/017_an_overload_picked_as_the_program_runs_rejects_an_unchecked_function.ndc +++ /dev/null @@ -1,8 +0,0 @@ -// 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/017_an_overload_takes_no_function_of_type_any.ndc b/tests/functional/programs/017_dynamic_any/017_an_overload_takes_no_function_of_type_any.ndc new file mode 100644 index 00000000..2b57015c --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/017_an_overload_takes_no_function_of_type_any.ndc @@ -0,0 +1,8 @@ +// No overload of `h` takes a value of type `Any`, even one holding a +// function: a cast says which function type it is. +// expect-error: No function called 'h' found that matches the arguments 'Any' +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)); 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 index 3fb5b0bd..e624b2e9 100644 --- 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 @@ -4,5 +4,5 @@ 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; +f = a as Fn(Int) -> Int; 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_cast_from_any_whose_type_fits_is_not_checked.ndc similarity index 86% rename from tests/functional/programs/017_dynamic_any/022_a_function_returning_a_list_fits_one_returning_an_iterable.ndc rename to tests/functional/programs/017_dynamic_any/022_a_function_cast_from_any_whose_type_fits_is_not_checked.ndc index e81571e4..a7cf4eb8 100644 --- 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_cast_from_any_whose_type_fits_is_not_checked.ndc @@ -4,4 +4,4 @@ fn h(f: Fn(Int) -> List) => 1; fn h(x: Int) => 0; let a: Any = fn(x: Int) => [x]; -print(h(a)); +print(h(a as Fn(Int) -> List)); 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 index 90bdfc7f..b1980863 100644 --- 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 @@ -1,9 +1,9 @@ -// A function used as a function type that takes more than it does checks +// A function cast to 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; +let g = 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 index 7f4429b1..dad4416f 100644 --- 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 @@ -7,5 +7,5 @@ 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); +let s = cached(strs) as Fn(String) -> String; print(s("ok")); diff --git a/tests/functional/programs/017_dynamic_any/025_a_nested_function_from_any_takes_only_what_it_was_compiled_for.ndc b/tests/functional/programs/017_dynamic_any/025_a_nested_function_from_any_takes_only_what_it_was_compiled_for.ndc index 94d41593..e8dab29f 100644 --- a/tests/functional/programs/017_dynamic_any/025_a_nested_function_from_any_takes_only_what_it_was_compiled_for.ndc +++ b/tests/functional/programs/017_dynamic_any/025_a_nested_function_from_any_takes_only_what_it_was_compiled_for.ndc @@ -1,12 +1,12 @@ // A nested function's value takes what its first call made its parameter, -// so through `Any` it doesn't take a String its body wasn't compiled for. -// expect-error: no function called 'inner' found matches the arguments: (String) +// so through `Any` it isn't a function that takes a String. +// expect-error: cannot cast Fn(Int) -> Any to Fn(String) -> String fn show(x: Int) => "int"; fn show(x) => "other"; fn outer() { fn inner(v) => show(v); inner(1); let a: Any = inner; - a("s") + (a as Fn(String) -> String)("s") } print(outer()); diff --git a/tests/functional/programs/017_dynamic_any/026_a_lambda_from_an_instance_takes_only_what_the_instance_made_it_take.ndc b/tests/functional/programs/017_dynamic_any/026_a_lambda_from_an_instance_takes_only_what_the_instance_made_it_take.ndc index 121b03b0..bd94ef77 100644 --- a/tests/functional/programs/017_dynamic_any/026_a_lambda_from_an_instance_takes_only_what_the_instance_made_it_take.ndc +++ b/tests/functional/programs/017_dynamic_any/026_a_lambda_from_an_instance_takes_only_what_the_instance_made_it_take.ndc @@ -1,9 +1,10 @@ // A lambda that an instance of a generic function makes takes what that -// instance's types make it take, so through `Any` it doesn't take a String. -// expect-error: found matches the arguments: (String) +// instance's types make it take, so through `Any` it isn't a function that +// takes a String. +// expect-error: cannot cast Fn(Int) -> String to Fn(String) -> String fn show(x: Int) => "int"; fn show(x) => "other"; fn mk(x) { fn(y) { let l = [x]; l.push(y); show(l[1]) } } print(mk(1)(2)); let a: Any = mk(1); -print(a("s")); +print((a as Fn(String) -> String)("s")); 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 index 9343a5ce..56c91746 100644 --- 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 @@ -1,6 +1,6 @@ // expect-output: ["a","b",1] let words = "a b".split(" "); let any: Any = words; -let anything: List = any; +let anything = any as List; anything.push(1); print(anything); diff --git a/tests/functional/programs/018_literals/015_a_use_whose_pick_doesnt_depend_on_a_literal_leaves_it_open.ndc b/tests/functional/programs/018_literals/015_a_use_whose_pick_doesnt_depend_on_a_literal_leaves_it_open.ndc new file mode 100644 index 00000000..0d62b73b --- /dev/null +++ b/tests/functional/programs/018_literals/015_a_use_whose_pick_doesnt_depend_on_a_literal_leaves_it_open.ndc @@ -0,0 +1,10 @@ +// `map` takes `describe(x)` whether the list holds Strings or `Any`s, as +// `describe(x: Int)` takes neither, so its pick doesn't decide what the list +// holds, and the annotation after it makes it a List. +// expect-output: ["other"] ["other"] +fn describe(x: Int) => "int"; +fn describe(x) => "other"; +let words = ["a"]; +let described = words.map(describe); +let anything: List = words; +print(described, anything.map(describe)); 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 deleted file mode 100644 index feb122ca..00000000 --- a/tests/functional/programs/018_literals/015_the_first_use_whose_pick_depends_on_a_literal_decides_it.ndc +++ /dev/null @@ -1,9 +0,0 @@ -// `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/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_is_not_checked_as_a_value.ndc similarity index 86% rename from tests/functional/programs/018_literals/017_a_map_default_function_returning_any_is_not_checked_as_a_value.ndc rename to tests/functional/programs/018_literals/017_a_map_default_function_is_not_checked_as_a_value.ndc index f0832557..f05e5335 100644 --- 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_is_not_checked_as_a_value.ndc @@ -1,6 +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; +fn fallback() -> Int => 1; let m: Map = %{:fallback}; print(m["a"]); diff --git a/tests/functional/programs/018_literals/029_an_any_value_makes_a_literal_hold_anys.ndc b/tests/functional/programs/018_literals/029_an_any_value_makes_a_literal_hold_anys.ndc new file mode 100644 index 00000000..ff799bea --- /dev/null +++ b/tests/functional/programs/018_literals/029_an_any_value_makes_a_literal_hold_anys.ndc @@ -0,0 +1,8 @@ +// An Int goes into an `Any`, so a literal holding an `Any` is a List, +// and a cast makes it a List. +// expect-output: any any int +fn f(xs: List) => "int"; +fn f(xs: List) => "any"; +let n: Int = 1; +let a: Any = 2; +print(f([n, a]), f([3, a]), f([n, a as Int])); diff --git a/tests/functional/programs/018_literals/029_an_any_value_shares_the_type_of_the_values_next_to_it.ndc b/tests/functional/programs/018_literals/029_an_any_value_shares_the_type_of_the_values_next_to_it.ndc deleted file mode 100644 index aa3866df..00000000 --- a/tests/functional/programs/018_literals/029_an_any_value_shares_the_type_of_the_values_next_to_it.ndc +++ /dev/null @@ -1,9 +0,0 @@ -// An `Any` among a literal's values is checked as whatever the others are, -// so `[n, a]` is a List when `n` is an Int, and a list of an untyped -// number and an `Any` is a List. -// expect-output: int any -fn f(xs: List) => "int"; -fn f(xs: List) => "any"; -let n: Int = 1; -let a: Any = 2; -print(f([n, a]), f([3, a])); diff --git a/tests/functional/programs/018_literals/031_a_write_of_values_that_share_no_type_is_checked_once_they_are_any.ndc b/tests/functional/programs/018_literals/031_a_write_of_values_that_share_no_type_is_checked_once_they_are_any.ndc deleted file mode 100644 index c982c903..00000000 --- a/tests/functional/programs/018_literals/031_a_write_of_values_that_share_no_type_is_checked_once_they_are_any.ndc +++ /dev/null @@ -1,7 +0,0 @@ -// `v` becomes an `Any` where it's stored as one, so storing it in an Int is -// checked as the program runs. -// expect-error: expected Int but found String -let v = if false { 1 } else { "a" }; -let n: Int = v; -let a: Any = v; -print(n); diff --git a/tests/functional/programs/018_literals/031_values_that_share_no_type_stored_as_an_any_take_a_cast.ndc b/tests/functional/programs/018_literals/031_values_that_share_no_type_stored_as_an_any_take_a_cast.ndc new file mode 100644 index 00000000..26415c98 --- /dev/null +++ b/tests/functional/programs/018_literals/031_values_that_share_no_type_stored_as_an_any_take_a_cast.ndc @@ -0,0 +1,7 @@ +// `v` becomes an `Any` where it's stored as one, and an `Any` takes a cast to +// be stored in an Int. +// expect-error: mismatched types: found Any but expected Int +let v = if false { 1 } else { "a" }; +let n: Int = v; +let a: Any = v; +print(n); diff --git a/tests/functional/programs/018_literals/032_a_map_default_function_returning_any_gives_no_typed_values.ndc b/tests/functional/programs/018_literals/032_a_map_default_function_returning_any_gives_no_typed_values.ndc new file mode 100644 index 00000000..1861c8a3 --- /dev/null +++ b/tests/functional/programs/018_literals/032_a_map_default_function_returning_any_gives_no_typed_values.ndc @@ -0,0 +1,6 @@ +// A map of Ints calls `fallback` for a missing key, which gives an `Any`, +// not an Int. +// expect-error: mismatched types +fn fallback() -> Any => 1; +let m: Map = %{:fallback}; +print(m["a"]); 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 6e0cde1b..d278d3be 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,12 +1,13 @@ -// The general overload must retain string behavior when the concrete kind is hidden. +// A string's prefixes and suffixes are strings, even when it comes out of an +// `Any`. let text: Any = "aé🦀"; -assert_eq(text.prefixes(), ["a", "aé", "aé🦀"]); -assert_eq(text.suffixes(), ["aé🦀", "é🦀", "🦀"]); -let empty_text: Any = ""; +assert_eq((text as String).prefixes(), ["a", "aé", "aé🦀"]); +assert_eq((text as String).suffixes(), ["aé🦀", "é🦀", "🦀"]); +let empty_text = ""; assert_eq(empty_text.prefixes(), []); assert_eq(empty_text.suffixes(), []); -// Untyped parameters exercise runtime overload dispatch for both kinds. +// Untyped parameters pick the overload for each kind. fn prefixes_of(value) => value.prefixes(); fn suffixes_of(value) => value.suffixes(); assert_eq(prefixes_of("ab"), ["a", "ab"]); 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 70c2f88a..75bb7675 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 @@ -1,13 +1,15 @@ -// A call dispatched at run time among overloads that the calling function -// captured. `inner` calls `describe` with an argument of unknown type, so the -// call goes through an overload set whose candidates are upvalues, passed -// through `middle`. Resolving such a candidate used to panic with "upvalue +// A call among overloads that the calling function captured. `inner` calls +// `describe`, whose overloads are upvalues, passed through `middle`. +// Resolving such a candidate as the program ran used to panic with "upvalue // resolution in overload sets". fn outer() { fn describe(x: Int) { "int" } fn describe(x: String) { "string" } fn middle() { - fn inner(v: Any) { describe(v) } + fn inner(v: Any) { + let n = v as? Int; + if n.is_some() { describe(n.unwrap()) } else { describe(v as String) } + } inner(1) ++ " " ++ inner("a") } middle() 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 91c7a2b3..2ab2619f 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,12 +16,12 @@ 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` 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. +// Same hazard but the tuple elements come out of `Any`s: `id` returns +// `Any`, so the tuples below are `(Any, Any, Any)` until a cast says they +// hold Ints, and chained ops on what the cast gives are picked statically. fn id(x) -> Any => x; -let av = (id(1), id(2), id(3)); -let bv = (id(4), id(5), id(6)); +let av = (id(1), id(2), id(3)) as (Int, Int, Int); +let bv = (id(4), id(5), id(6)) as (Int, Int, Int); let cv = av - bv; assert_eq(cv, (-3, -3, -3)); assert_eq(cv * cv, (9, 9, 9)); diff --git a/tests/functional/tests/repl.rs b/tests/functional/tests/repl.rs index b0f9c7b6..56ab24d7 100644 --- a/tests/functional/tests/repl.rs +++ b/tests/functional/tests/repl.rs @@ -201,7 +201,7 @@ fn functions_share_globals_with_later_lines() { #[test] fn closures_keep_the_loop_variables_of_a_finished_line() { let out = repl_output(&[ - "let fs = []; let i = 0;", + "let fs: List Int> = []; let i = 0;", "while i < 3 { let j = i; fs.push(fn() { j }); i += 1; }", "print([f() for f in fs])", ]); From 58993f3d098378f3eb29febb0f99916473de8c5c Mon Sep 17 00:00:00 2001 From: Tim Fennis Date: Sat, 14 Nov 2026 09:57:23 +0100 Subject: [PATCH 2/2] fix(analyser): give a call with Any arguments its own instance A generic function called with an Any argument reused its own body, whose calls dispatch at runtime, so its result could disagree with its type. Co-Authored-By: Claude Opus 5.5 --- ndc_analyser/src/analyser/instances.rs | 11 ++++++----- ...ion_called_with_any_takes_the_overload_for_any.ndc | 9 +++++++++ 2 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 tests/functional/programs/017_dynamic_any/027_a_generic_function_called_with_any_takes_the_overload_for_any.ndc diff --git a/ndc_analyser/src/analyser/instances.rs b/ndc_analyser/src/analyser/instances.rs index d10147a9..54f9c8f9 100644 --- a/ndc_analyser/src/analyser/instances.rs +++ b/ndc_analyser/src/analyser/instances.rs @@ -228,17 +228,18 @@ impl Analyser { } /// 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. + /// itself 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) { + // A call with `Any` arguments gets an instance too: its calls take + // only the overloads that take `Any`, while the function's own body + // leaves its picks to the program, for its value. + if !self.declared_in_batch.contains(&function) { return function; } if let Some(&instance) = self.instances.get(&(function, key.clone())) { diff --git a/tests/functional/programs/017_dynamic_any/027_a_generic_function_called_with_any_takes_the_overload_for_any.ndc b/tests/functional/programs/017_dynamic_any/027_a_generic_function_called_with_any_takes_the_overload_for_any.ndc new file mode 100644 index 00000000..1071f01d --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/027_a_generic_function_called_with_any_takes_the_overload_for_any.ndc @@ -0,0 +1,9 @@ +// A generic function called with an `Any` argument gets its own copy, whose +// calls take only the overloads that take `Any`, as its type says. +// expect-output: fallback 1 +fn pick(x: Int) => 1; +fn pick(x: Any) => "fallback"; +fn wrap(x) => pick(x); +let a: Any = 1; +let s: String = wrap(a); +print(s, wrap(2));