diff --git a/manual/src/features/augmented-assignment.md b/manual/src/features/augmented-assignment.md index d2c6d1b0..16c85646 100644 --- a/manual/src/features/augmented-assignment.md +++ b/manual/src/features/augmented-assignment.md @@ -39,49 +39,53 @@ source order. The augmented assignment expression itself evaluates to `()`. ## Type checking -An augmented assignment with an in-place operator such as `++=` never changes -the type of its target. If the right-hand side would force a type change the -program is rejected: +`a op= b` is checked like `a = a op b`: the operator has to take both operands, +and what it returns has to fit `a`. An in-place operator such as `++=` is used +where it does the same thing, so it never changes the type of its target. A +right-hand side whose element type differs is rejected: ```ndc,fails let values = [1]; -values ++= ["two"]; // error: mismatched types: found List but expected List +values ++= ["two"]; // error: No function called '++' found that matches the arguments 'List, List' ``` -The right-hand side has to *provably* fit. A wider element type is not enough, -because the operator copies the values across without checking them: +A wider element type is not enough either, as `++` keeps one element type: ```ndc,fails let values: List = [1]; let rhs: List = [0.5n]; -values ++= rhs; // error: mismatched types: found List but expected List +values ++= rhs; // error: No function called '++' found that matches the arguments 'List, List' ``` -The same goes for an operand whose type is unknown. `Any` says nothing about -what the value holds, and no later step re-checks it, so it is rejected too: +An operand of type `Any` is checked as the program runs, as in `a = a op b`. +What the operator makes is checked where it's written back, so a value that +doesn't fit fails there and leaves the target as it was. As nothing says the +operand fits before it's checked, the operator makes a new value rather than +changing the target in place: -```ndc,fails +```ndc fn opaque(x: Any) => x; let values = [1]; -values ++= opaque([2, 3]); // error: mismatched types: found Any but expected List +let same = values; +values ++= opaque([2, 3]); +print(values, same); // [1,2,3] [1] ``` -[Cast](casts.md) the operand to say what it holds. The cast checks the value, so -the assignment gets its guarantee from the cast site: - -```ndc +```ndc,fails fn opaque(x: Any) => x; let values = [1]; -values ++= opaque([2, 3]) as List; -print(values); // [1,2,3] +values ++= opaque([0.5]); // error: expected List but found List ``` -A cast that does not hold fails there rather than corrupting the target: +[Cast](casts.md) the operand to say what it holds, which checks it at the cast +and lets `++=` change the target in place: -```ndc,fails +```ndc fn opaque(x: Any) => x; let values = [1]; -values ++= opaque([0.5]) as List; // error: cannot cast List to List +let same = values; +values ++= opaque([2, 3]) as List; +print(values, same); // [1,2,3] [1,2,3] ``` Annotate the target with `Any` to opt into heterogeneous contents: diff --git a/manual/src/features/casts.md b/manual/src/features/casts.md index c6c1f2e5..2293ab35 100644 --- a/manual/src/features/casts.md +++ b/manual/src/features/casts.md @@ -92,10 +92,10 @@ let values: List = [1, 2, 3]; print((values as List).sum()); // 6 ``` -A specialized `op=` mutates its target in place and keeps its type, so it only -accepts a right operand that provably fits. A value of unknown type does not, -and a cast gets the elements checked instead of letting them slip into the -target unverified: +A specialized `op=` such as `++=` changes its target in place, so it's only +used with a right operand that provably fits. With a value of unknown type, +`a op= b` makes a new value like `a = a op b` does; a cast checks the elements +first, so the target can be changed in place: ```ndc fn opaque(x) => x; diff --git a/ndc_analyser/src/analyser/assignments.rs b/ndc_analyser/src/analyser/assignments.rs index c3e307d6..a2f2b0a1 100644 --- a/ndc_analyser/src/analyser/assignments.rs +++ b/ndc_analyser/src/analyser/assignments.rs @@ -23,25 +23,26 @@ impl Analyser { ) -> StaticType { let left_type = self.analyse_assignment_target(l_value, span); let right_type = self.analyse_expr(r_value); - // `a op= b` is `a = op(a, b)`. Where the operands aren't fully known - // yet, the operator waits for them like any call, and the in-place - // overload is picked along with it. + // `a op= b` is `a = op(a, b)`: the operator is a call like any other, + // and the in-place overload is picked along with it where it does the + // same (see `record_pick`). let (left_type, right_type) = ( self.vars.resolve(&left_type), self.vars.resolve(&right_type), ); - if (left_type.has_vars() || right_type.has_vars()) - && !self - .overloads() - .reachable_symbols(&self.resolver.augmented_candidates(id).operator) - .is_empty() + if !self + .overloads() + .reachable_symbols(&self.resolver.augmented_candidates(id).operator) + .is_empty() { let result = self.constrain_call( CallSite::OpAssignment(id), vec![left_type.clone(), right_type], + // What the operator returns is checked where it's written + // back, when the program picks the operator as it runs. CallNodes { arguments: vec![None, Some(r_value.id)], - result: None, + result: Some(l_value.id), }, CallKind::Operator, operation, @@ -50,16 +51,15 @@ impl Analyser { self.validate_assignment_write(l_value, &left_type, &result, span); return StaticType::unit(); } - let written = self.op_assignment_now(id, &left_type, &right_type, operation, span); + let written = self.in_place_only(id, &left_type, &right_type, operation, span); self.validate_assignment_write(l_value, &left_type, &written, span); StaticType::unit() } - /// Picks what `a op= b` with operands of these types does and returns - /// the type of the value written back to `a`. An in-place overload - /// such as `++=` is used where it keeps `a`'s type, and otherwise `op` - /// makes a new value. - pub(super) fn op_assignment_now( + /// Picks the in-place overload `a op= b` calls when there's no operator + /// `op` to fall back on, and returns the type of the value written back + /// to `a`, which an in-place overload doesn't change. + fn in_place_only( &mut self, id: NodeId, left_type: &StaticType, @@ -67,63 +67,20 @@ impl Analyser { operation: &str, span: Span, ) -> StaticType { - let arg_types = vec![left_type.clone(), right_type.clone()]; - - let ResolvedCall { - binding: assign_binding, - return_type: assign_return, - } = self.overloads().resolve_call( + let arguments = [left_type.clone(), right_type.clone()]; + let ResolvedCall { binding, .. } = self.overloads().resolve_call( &self.resolver.augmented_candidates(id).in_place, - &arg_types, - CallKind::Operator, - ); - let ResolvedCall { - binding: op_binding, - return_type: op_return, - } = self.overloads().resolve_call( - &self.resolver.augmented_candidates(id).operator, - &arg_types, + &arguments, CallKind::Operator, ); - let has_op_binding = !matches!(op_binding, Binding::None); - let assign_is_eligible = Self::augmented_rhs_is_compatible(left_type, right_type); - - let writeback_type = match assign_binding { - Binding::Resolved(candidate) if assign_is_eligible => { - self.record_augmented_pick(id, Binding::Resolved(candidate)); - None - } - Binding::Dynamic(assign_candidates) if assign_is_eligible => { - let binding = Self::in_place_then_operators(assign_candidates, op_binding.clone()); - let result_type = if has_op_binding { - assign_return.join(&op_return) - } else { - assign_return - }; - self.record_augmented_pick(id, binding); - Some(result_type) - } - Binding::Resolved(_) | Binding::Dynamic(_) => { - // A specialized mutation exists, but using it would - // change the concrete left type. Reject it here rather - // than falling through to an ordinary operation whose - // erased return type could widen the same target. - self.emit(AnalysisError::mismatched_types(right_type, left_type, span)); - Some(StaticType::Any) - } - Binding::None if has_op_binding => { - self.record_augmented_pick(id, op_binding); - Some(op_return) - } - Binding::None => { - self.emit(AnalysisError::function_not_found( - operation, &arg_types, span, - )); - Some(StaticType::Any) - } - }; - - writeback_type.unwrap_or_else(|| left_type.clone()) + if matches!(binding, Binding::None) { + self.emit(AnalysisError::function_not_found( + operation, &arguments, span, + )); + return StaticType::Any; + } + self.record_augmented_pick(id, binding); + left_type.clone() } /// What `a op= b` does when the program picks as it runs: the in-place diff --git a/ndc_analyser/src/analyser/solver.rs b/ndc_analyser/src/analyser/solver.rs index 4e77c00a..564417be 100644 --- a/ndc_analyser/src/analyser/solver.rs +++ b/ndc_analyser/src/analyser/solver.rs @@ -418,8 +418,13 @@ impl Analyser { .map(|argument| self.vars.resolve(argument)) .collect::>(); // An argument of type `Any` is checked where it's passed, so the - // program can call `picked` directly. - let binding = Binding::Resolved(Candidate::Scalar(picked)); + // program can call `picked` directly, unless one has nowhere to be + // checked. + let binding = if Self::checks_every_any(&arguments, &call.nodes.arguments) { + Binding::Resolved(Candidate::Scalar(picked)) + } else { + Binding::Dynamic(vec![Candidate::Scalar(picked)]) + }; if cfg!(debug_assertions) { self.early_picks.push(EarlyPick::Call { candidates: call.candidates.clone(), @@ -1271,9 +1276,6 @@ impl Analyser { } if arguments.iter().all(|argument| !argument.has_vars()) { - if let (CallSite::OpAssignment(id), [left, right]) = (call.site, &arguments[..]) { - return Some(self.op_assignment_now(id, left, right, &call.name, call.span)); - } return Some(self.resolve_call_now(call, &arguments)); } @@ -1492,51 +1494,23 @@ impl Analyser { merged } - /// [`Overloads::resolve_call`], with the overloads it leaves the program - /// to pick from as it runs cut to the ones that take the arguments with - /// each type parameter one type. Picking looks at each argument on its - /// own, so `[]=(List, Int, T)` looks like it takes a String for a - /// `List` when the index is `Any`, but it can't. + /// Whether every argument of type `Any` has a node in `nodes` to be checked + /// at where it's passed. + fn checks_every_any(arguments: &[StaticType], nodes: &[Option]) -> bool { + arguments + .iter() + .zip(nodes) + .all(|(argument, node)| node.is_some() || *argument != StaticType::Any) + } + + /// [`Overloads::resolve_call`] over the types as they're known now. pub(super) fn resolve_overloads( &mut self, candidates: &Candidates, arguments: &[StaticType], kind: CallKind, ) -> ResolvedCall { - let mut resolved = self.overloads().resolve_call(candidates, arguments, kind); - // An operator may apply an overload to each element of a tuple - // instead, which takes what the elements are. - if kind == CallKind::Operator { - return resolved; - } - if let Binding::Dynamic(picked) = &resolved.binding { - let picked = picked.clone(); - // What a function takes and returns isn't checked as the program - // runs, so it's left out: a lambda that returns `Any` may still be - // what's expected. - let snapshot = self.vars.snapshot(); - let arguments = arguments - .iter() - .map(|argument| match self.vars.resolve(argument) { - StaticType::Function { .. } => self.vars.fresh(StaticType::Any), - argument => argument, - }) - .collect::>(); - let kept = picked - .into_iter() - .filter(|candidate| match candidate { - Candidate::Scalar(symbol) => self.could_accept(*symbol, &arguments), - Candidate::Vec(_) => true, - }) - .collect::>(); - self.vars.rollback(snapshot); - resolved.binding = if kept.is_empty() { - Binding::None - } else { - Binding::Dynamic(kept) - }; - } - resolved + self.overloads().resolve_call(candidates, arguments, kind) } /// Resolves `call` with the arguments as they are, reporting it when no @@ -1548,13 +1522,15 @@ impl Analyser { } = self.resolve_overloads(&call.candidates, arguments, call.kind); // A pick left to the program among one function with known // parameters is a call of it, whose `Any` arguments are checked where - // they're passed. A variable of type `Any` may hold any function, and - // an `Iterable` parameter's kind isn't in its declared type, so the - // program checks those arguments itself. + // they're passed. A variable of type `Any` may hold any function, an + // `Iterable` parameter's kind isn't in its declared type, and an + // `Any` without a node to check it at, as the target of `a op= b`, + // isn't checked where it's passed, so the program checks those itself. let binding = match binding { Binding::Dynamic(only) if let [Candidate::Scalar(only)] = only[..] && !self.types.has_shape(only) + && Self::checks_every_any(arguments, &call.nodes.arguments) && let StaticType::Function { parameters: Some(_), .. @@ -1564,6 +1540,20 @@ impl Analyser { } binding => binding, }; + // `a op= b` with no operator that takes the operands calls an in-place + // overload if one does, as a set has `-=` but no `-`, which keeps `a`. + if matches!(binding, Binding::None) + && let CallSite::OpAssignment(id) = call.site + { + let in_place = self.resolver.augmented_candidates(id).in_place.clone(); + let in_place = self + .resolve_overloads(&in_place, arguments, call.kind) + .binding; + if !matches!(in_place, Binding::None) { + self.record_augmented_pick(id, in_place); + return arguments[0].clone(); + } + } if matches!(binding, Binding::None) { self.report_failed_call(call, arguments); self.record_pick(call.site, binding, arguments); @@ -2054,7 +2044,7 @@ impl Analyser { ); let compatible = Self::augmented_rhs_is_compatible(&arguments[0], &arguments[1]); let binding = match (in_place.binding, binding) { - (Binding::Resolved(candidate), _) if compatible => Binding::Resolved(candidate), + (Binding::Resolved(candidate), _) => Binding::Resolved(candidate), // Which one applies is left to the program as it runs, // which mutates in place where it can. (Binding::Dynamic(in_place), operators) if compatible => { diff --git a/ndc_analyser/src/analyser/tests.rs b/ndc_analyser/src/analyser/tests.rs index 9cdde341..191ee237 100644 --- a/ndc_analyser/src/analyser/tests.rs +++ b/ndc_analyser/src/analyser/tests.rs @@ -223,7 +223,7 @@ fn an_in_place_operator_assignment_cannot_change_the_target_type() { ("++=".to_string(), concat.clone()), ("++".to_string(), concat.clone()), ], - "mismatched types: found List but expected List", + "No function called '++' found that matches the arguments 'List, List'", ); assert_analysis_error( "let values: List = [1]; values ++= [\"two\"];", @@ -231,7 +231,7 @@ fn an_in_place_operator_assignment_cannot_change_the_target_type() { ("++=".to_string(), concat.clone()), ("++".to_string(), concat), ], - "mismatched types: found List but expected List", + "No function called '++' found that matches the arguments 'List, List'", ); } @@ -1055,16 +1055,22 @@ fn a_cast_settles_what_isnt_known_about_its_value() { #[test] fn a_waiting_call_does_not_commit_to_an_overload_taking_any_instead() { - // Only the catch-all `[]=` takes a list of unknown elements as an Int - // list's element, but once the elements are known the element-wise - // `[]=` is the one that fits, and it doesn't take a list. + // While the list's elements aren't known, both `[]=` might take the + // write, so it waits. Once they're Ints, only the catch-all takes a list + // as an element: the element-wise `[]=` would need its `T` to be both an + // Int and a list. let globals = [ natives(&[("[]=", "(List, Any, Any)")]), index_natives(), ] .concat(); assert_eq!( - errors("let xs = [1]; xs[0] = [];", globals), + errors("let xs = [1]; xs[0] = [];", globals.clone()), + Vec::::new() + ); + // Without the catch-all, nothing takes it. + assert_eq!( + errors("let xs = [1]; xs[0] = [];", index_natives()), vec!["mismatched types: found List but expected Int"] ); } diff --git a/ndc_analyser/src/overloads.rs b/ndc_analyser/src/overloads.rs index 3d4b17de..24522744 100644 --- a/ndc_analyser/src/overloads.rs +++ b/ndc_analyser/src/overloads.rs @@ -66,6 +66,7 @@ fn could_take(typ: &StaticType, sig: &[StaticType]) -> bool { .iter() .zip(sig) .all(|(param, arg)| arg.may_fit(param)) + && StaticType::type_parameters_agree(parameters, sig) } StaticType::Function { parameters: None, .. diff --git a/ndc_core/src/static_type.rs b/ndc_core/src/static_type.rs index 17bbd617..4b7dbd8a 100644 --- a/ndc_core/src/static_type.rs +++ b/ndc_core/src/static_type.rs @@ -620,6 +620,121 @@ impl StaticType { .iter() .zip(parameters) .all(|(argument, parameter)| argument.fits(parameter)) + && Self::type_parameters_agree(parameters, types) + } + + /// Whether each type parameter of `parameters` stands for one type in + /// `arguments`: `(Int, String)` doesn't fit `(T, T)`, as `T` would have to + /// be both. Inside a container a type parameter is the type there, as a + /// `List` makes `T` `Any`, and a value passed for `T` itself only has + /// to fit it: `push(List, String)` fits `push(List, T)`. What isn't + /// known yet, and an argument that's `Any` as a whole, which is checked as + /// the program runs, agree with anything. + pub fn type_parameters_agree(parameters: &[Self], arguments: &[Self]) -> bool { + let mut bound = Vec::new(); + for (parameter, argument) in parameters.iter().zip(arguments) { + argument.bind_type_parameters(parameter, false, &mut bound); + } + bound.iter().enumerate().all(|(i, (name, typ, inside))| { + let exact = bound + .iter() + .find(|(other, _, other_inside)| other == name && *other_inside); + match exact { + // What a container says it is decides, and a value passed + // for it fits it. + Some((_, exact, _)) if *inside => exact.agrees_with(typ), + Some((_, exact, _)) => { + *typ == Self::Any || typ.fits(exact) || typ.agrees_with(exact) + } + // Values passed for it are what it is, so they agree. + None => bound[..i] + .iter() + .filter(|(earlier, _, _)| earlier == name) + .all(|(_, earlier, _)| { + *typ == Self::Any || *earlier == Self::Any || earlier.agrees_with(typ) + }), + } + }) + } + + /// Adds to `bound` the part of this type that each type parameter in + /// `parameter` is matched against, as [`Self::matches`] matches them, + /// with whether it's inside a container, where it has to be that type. + fn bind_type_parameters<'p>( + &self, + parameter: &'p Self, + inside: bool, + bound: &mut Vec<(&'p str, Self, bool)>, + ) { + match (self, parameter) { + (_, Self::Generic(name)) => bound.push((name, self.clone(), inside)), + // A sequence passed for an `Iterable` holds values passed for `T`. + (Self::Iterable(a), Self::Iterable(p)) => a.bind_type_parameters(p, inside, bound), + (argument, Self::Iterable(p)) if Self::is_sequence(argument) => { + if let Some(element) = argument.sequence_element_type() { + element.bind_type_parameters(p, inside, bound); + } + } + (Self::Option(a), Self::Option(p)) + | (Self::List(a), Self::List(p)) + | (Self::Iterator(a), Self::Iterator(p)) + | (Self::MinHeap(a), Self::MinHeap(p)) + | (Self::MaxHeap(a), Self::MaxHeap(p)) + | (Self::Deque(a), Self::Deque(p)) => a.bind_type_parameters(p, true, bound), + (Self::Tuple(a), Self::Tuple(p)) if a.len() == p.len() => { + for (a, p) in a.iter().zip(p) { + a.bind_type_parameters(p, true, bound); + } + } + (Self::Map { key: ak, value: av }, Self::Map { key: pk, value: pv }) => { + ak.bind_type_parameters(pk, true, bound); + av.bind_type_parameters(pv, true, bound); + } + _ => {} + } + } + + /// Whether this type and `other` could be one type, where what isn't + /// known yet in either could be anything. + fn agrees_with(&self, other: &Self) -> bool { + let unknown = |typ: &Self| matches!(typ, Self::Var(_) | Self::Generic(_) | Self::Never); + if unknown(self) || unknown(other) { + return true; + } + match (self, other) { + (Self::Option(a), Self::Option(b)) + | (Self::List(a), Self::List(b)) + | (Self::Iterator(a), Self::Iterator(b)) + | (Self::Iterable(a), Self::Iterable(b)) + | (Self::MinHeap(a), Self::MinHeap(b)) + | (Self::MaxHeap(a), Self::MaxHeap(b)) + | (Self::Deque(a), Self::Deque(b)) => a.agrees_with(b), + (Self::Tuple(a), Self::Tuple(b)) => { + a.len() == b.len() && a.iter().zip(b).all(|(a, b)| a.agrees_with(b)) + } + (Self::Map { key: ak, value: av }, Self::Map { key: bk, value: bv }) => { + ak.agrees_with(bk) && av.agrees_with(bv) + } + ( + Self::Function { + parameters: ap, + return_type: ar, + }, + Self::Function { + parameters: bp, + return_type: br, + }, + ) => { + let parameters = match (ap, bp) { + (Some(a), Some(b)) => { + a.len() == b.len() && a.iter().zip(b).all(|(a, b)| a.agrees_with(b)) + } + _ => true, + }; + parameters && ar.agrees_with(br) + } + (a, b) => a == b, + } } pub fn unpack(&self) -> Option + '_>> { diff --git a/ndc_stdlib/src/sequence.rs b/ndc_stdlib/src/sequence.rs index 2ecc80e0..76e09ead 100644 --- a/ndc_stdlib/src/sequence.rs +++ b/ndc_stdlib/src/sequence.rs @@ -708,7 +708,10 @@ mod inner { } /// Transposes a sequence of strings, turning rows of characters into columns. - #[function(name = "transposed", signature = "(Iterable) -> List>")] + #[function( + name = "transposed", + signature = "(Iterable) -> List>" + )] pub fn transposed_strings(seq: SeqValue) -> anyhow::Result { transpose(seq) } diff --git a/tests/functional/programs/004_basic/065_incompatible_specialized_op_assignment.ndc b/tests/functional/programs/004_basic/065_incompatible_specialized_op_assignment.ndc index cd551592..45ba0b26 100644 --- a/tests/functional/programs/004_basic/065_incompatible_specialized_op_assignment.ndc +++ b/tests/functional/programs/004_basic/065_incompatible_specialized_op_assignment.ndc @@ -1,3 +1,3 @@ -// expect-error: mismatched types: found List but expected List +// expect-error: No function called '++' found that matches the arguments 'List, List' let values = [1]; values ++= ["two"]; diff --git a/tests/functional/programs/004_basic/066_op_assignment_any_rhs.ndc b/tests/functional/programs/004_basic/066_op_assignment_any_rhs.ndc index c19f68a6..e521249d 100644 --- a/tests/functional/programs/004_basic/066_op_assignment_any_rhs.ndc +++ b/tests/functional/programs/004_basic/066_op_assignment_any_rhs.ndc @@ -1,8 +1,7 @@ -// A specialized `op=` copies values into the left operand without inspecting -// them, so an `Any` right operand is rejected rather than silently accepted: -// nothing downstream would check the elements. Cast the operand to state what -// it holds. -// expect-error: mismatched types: found Any but expected List +// `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]); diff --git a/tests/functional/programs/004_basic/067_op_assignment_supertype_rhs.ndc b/tests/functional/programs/004_basic/067_op_assignment_supertype_rhs.ndc index 69667af4..964ace69 100644 --- a/tests/functional/programs/004_basic/067_op_assignment_supertype_rhs.ndc +++ b/tests/functional/programs/004_basic/067_op_assignment_supertype_rhs.ndc @@ -1,8 +1,8 @@ // A specialized `op=` mutates the left operand in place and keeps its type, so // a right operand whose element type is merely a *supertype* is not enough: // appending a Float here would leave a List holding a non-Int. -// expect-error: mismatched types: found List but expected List +// expect-error: No function called '++' found that matches the arguments 'List, List' let values: List = [1]; -let rhs: List = [0.5]; +let rhs: List = []; values ++= rhs; print(values) diff --git a/tests/functional/programs/004_basic/075_op_assignment_takes_one_type_for_a_type_parameter.ndc b/tests/functional/programs/004_basic/075_op_assignment_takes_one_type_for_a_type_parameter.ndc new file mode 100644 index 00000000..82c4da8f --- /dev/null +++ b/tests/functional/programs/004_basic/075_op_assignment_takes_one_type_for_a_type_parameter.ndc @@ -0,0 +1,6 @@ +// `max` of two values takes two of one type, in `a max= b` as in +// `max(a, b)`: a Float and an Int aren't one type. +// expect-error: No function called 'max' found that matches the arguments 'Float, Int' +let best = -Inf; +let x: Int = 3; +best max= x; diff --git a/tests/functional/programs/004_basic/076_op_assignment_checks_what_an_any_operand_makes.ndc b/tests/functional/programs/004_basic/076_op_assignment_checks_what_an_any_operand_makes.ndc new file mode 100644 index 00000000..1ccc82a9 --- /dev/null +++ b/tests/functional/programs/004_basic/076_op_assignment_checks_what_an_any_operand_makes.ndc @@ -0,0 +1,6 @@ +// What `a op= b` makes from an `Any` operand is checked where it's written +// back, so a value that doesn't fit never reaches the target. +// expect-error: expected List but found List +fn opaque(x) -> Any => x; +let values = [1]; +values ++= opaque([0.5]); diff --git a/tests/functional/programs/004_basic/077_op_assignment_checks_an_any_target_before_the_call.ndc b/tests/functional/programs/004_basic/077_op_assignment_checks_an_any_target_before_the_call.ndc new file mode 100644 index 00000000..dd0609b7 --- /dev/null +++ b/tests/functional/programs/004_basic/077_op_assignment_checks_an_any_target_before_the_call.ndc @@ -0,0 +1,7 @@ +// The target of `a op= b` is passed to the operator, so a target of type +// `Any` is checked against what the operator takes before it runs. +// expect-error: no function called 'choose' found matches the arguments: (String, Int) +fn choose(a: Int, b: Int) => a; +let x: Any = "wrong"; +x choose= 1; +print(x);