diff --git a/manual/src/features/casts.md b/manual/src/features/casts.md index ec093004..c6c1f2e5 100644 --- a/manual/src/features/casts.md +++ b/manual/src/features/casts.md @@ -37,6 +37,24 @@ missing-key lookup inserts it. A default *function*'s results can't be verified without calling it, so such a map only conforms when the value type is `Any`. +A function is checked against what its own type says: one that takes a +`String` can't be a `Fn(Int) -> Int`. What a function returns often isn't +known until it runs, so a function that may fit a function type returns a +function that checks each call's result: + +```ndc,fails +fn text(x) => "s"; // its value takes and returns anything +let value: Any = text; +let f = value as Fn(Int) -> Int; +f(1) +// error[vm]: expected the function to return Int but it returned String +``` + +Inside a container, where the function can't be replaced by one that checks +its calls, only a function whose own type shows it fits does. So does an +overload picked as the program runs from a value of type `Any`: cast the +value to the function type first to have its calls checked. + The analyser rejects casts between types that cannot share a value: ```ndc,fails diff --git a/ndc_analyser/src/analyser/declarations.rs b/ndc_analyser/src/analyser/declarations.rs index 4a83550b..bb73070b 100644 --- a/ndc_analyser/src/analyser/declarations.rs +++ b/ndc_analyser/src/analyser/declarations.rs @@ -211,6 +211,14 @@ impl Analyser { .analysis .inferred_return_types .insert(id, inferred_return.clone()); + // A lambda's value says what it returns, which tells whether it's + // a function type an `Any` is used as without checking its calls. + if declared.is_none() + && let Some(StaticType::Function { return_type, .. }) = + self.result.analysis.function_types.get_mut(&id) + { + **return_type = inferred_return.clone(); + } } // A named function's value is checked as the program runs against diff --git a/ndc_analyser/src/analyser/solver.rs b/ndc_analyser/src/analyser/solver.rs index 6a5ae395..4e77c00a 100644 --- a/ndc_analyser/src/analyser/solver.rs +++ b/ndc_analyser/src/analyser/solver.rs @@ -460,7 +460,16 @@ impl Analyser { .resolve_overloads(&candidates, &arguments, kind) .binding { - Binding::Dynamic(only) if only.len() == 1 => Binding::Resolved(only[0]), + Binding::Dynamic(only) + if let [Candidate::Scalar(only)] = only[..] + && !self.types.has_shape(only) + && let StaticType::Function { + parameters: Some(_), + .. + } = self.vars.resolve(&self.types.callable_type(only)) => + { + Binding::Resolved(Candidate::Scalar(only)) + } binding => binding, }; assert!( @@ -1537,6 +1546,24 @@ impl Analyser { binding, return_type, } = self.resolve_overloads(&call.candidates, arguments, call.kind); + // A pick left to the program among one function with known + // parameters is a call of it, whose `Any` arguments are checked where + // they're passed. A variable of type `Any` may hold any function, and + // an `Iterable` parameter's kind isn't in its declared type, so the + // program checks those arguments itself. + let binding = match binding { + Binding::Dynamic(only) + if let [Candidate::Scalar(only)] = only[..] + && !self.types.has_shape(only) + && let StaticType::Function { + parameters: Some(_), + .. + } = self.vars.resolve(&self.types.callable_type(only)) => + { + Binding::Resolved(Candidate::Scalar(only)) + } + binding => binding, + }; if matches!(binding, Binding::None) { self.report_failed_call(call, arguments); self.record_pick(call.site, binding, arguments); diff --git a/ndc_analyser/src/types.rs b/ndc_analyser/src/types.rs index 203c3d13..881d3afb 100644 --- a/ndc_analyser/src/types.rs +++ b/ndc_analyser/src/types.rs @@ -38,6 +38,13 @@ impl TypeTable { /// The type of `symbol` as a call sees it, which picks the overload a /// call means: `fn total(xs: Iterable)` takes an `Iterable`, /// although its declared type takes a type parameter each call settles. + /// Whether a parameter of `symbol` takes any sequence of a type, as + /// `xs: Iterable` does, which its declared type leaves to an + /// iteration constraint. + pub(crate) fn has_shape(&self, symbol: SymbolId) -> bool { + self.shapes.contains_key(&symbol) + } + pub(crate) fn callable_type(&self, symbol: SymbolId) -> std::borrow::Cow<'_, StaticType> { let typ = self.get_type(symbol); let ( diff --git a/ndc_vm/src/value/function.rs b/ndc_vm/src/value/function.rs index 53540d9f..a6eea7c2 100644 --- a/ndc_vm/src/value/function.rs +++ b/ndc_vm/src/value/function.rs @@ -22,6 +22,29 @@ pub enum Function { cache: Rc>>, function: Rc, }, + + /// A function passed as a function type it may not have: a value of + /// type `Any` used as a `Fn(Int) -> Int` that holds a function whose + /// return type isn't known. Each call checks what it returns. + Checked { + function: Rc, + /// The function type it's used as, boxed so that it doesn't make + /// every function value bigger. + typ: Box, + }, +} + +/// How a function fits a function type, from the function's own type, +/// which leaves out what isn't known as the program runs. +#[derive(Debug, PartialEq, Eq)] +pub enum FunctionFit { + /// It takes what the type passes and returns what it returns. + Proven, + /// It takes what the type passes, but what it returns has to be checked + /// at each call. + IfChecked, + /// It can't be a function of that type. + No, } pub struct NativeFunction { @@ -182,7 +205,9 @@ impl Function { match self { Self::Closure(c) => Some(&c.prototype), Self::Compiled(f) => Some(f), - Self::Memoized { function, .. } => function.prototype(), + Self::Memoized { function, .. } | Self::Checked { function, .. } => { + function.prototype() + } Self::Native(_) => None, } } @@ -190,7 +215,9 @@ impl Function { pub fn documentation(&self) -> Option<&str> { match self { Self::Native(f) => f.documentation.as_deref(), - Self::Memoized { function, .. } => function.documentation(), + Self::Memoized { function, .. } | Self::Checked { function, .. } => { + function.documentation() + } _ => None, } } @@ -200,16 +227,87 @@ impl Function { Self::Compiled(f) => f.name.as_deref(), Self::Native(f) => Some(&f.name), Self::Closure(c) => c.prototype.name.as_deref(), - Self::Memoized { function, .. } => function.name(), + Self::Memoized { function, .. } | Self::Checked { function, .. } => function.name(), } } pub fn static_type(&self) -> StaticType { + self.signature().clone() + } + + /// The function's type: what it takes and returns, as far as that's + /// known as the program runs. + fn signature(&self) -> &StaticType { match self { - Self::Compiled(f) => f.static_type.clone(), - Self::Native(f) => f.static_type.clone(), - Self::Closure(c) => c.prototype.static_type.clone(), - Self::Memoized { function, .. } => function.static_type(), + Self::Compiled(f) => &f.static_type, + Self::Native(f) => &f.static_type, + Self::Closure(c) => &c.prototype.static_type, + Self::Memoized { function, .. } => function.signature(), + Self::Checked { typ, .. } => typ, + } + } + + /// The function a [`Self::Checked`] checks, or this one: checking its + /// calls doesn't make it another function, as far as `==` goes. + pub fn unchecked(&self) -> &Self { + match self { + Self::Checked { function, .. } => function.unchecked(), + function => function, + } + } + + /// The types of the parameters, when it takes a known number of them. + pub fn parameters(&self) -> Option<&[StaticType]> { + match self.signature() { + StaticType::Function { + parameters: Some(parameters), + .. + } => Some(parameters), + _ => None, + } + } + + /// How this function fits `expected`, a function type. A parameter or + /// return type that its own type leaves unknown, as `Any`, fits + /// anything, but an unknown return type has to be checked at each call: + /// a lambda that returns a String doesn't fit `Fn(Int) -> Int`, and one + /// whose return type isn't known fits it if checked. + pub fn fit(&self, expected: &StaticType) -> FunctionFit { + let StaticType::Function { + parameters: expected_parameters, + return_type: expected_return, + } = expected + else { + return FunctionFit::No; + }; + let StaticType::Function { + parameters, + return_type, + } = self.signature() + else { + return FunctionFit::No; + }; + let (takes, takes_proven) = match (expected_parameters, parameters) { + (Some(expected), Some(parameters)) => ( + expected.len() == parameters.len() + && expected + .iter() + .zip(parameters) + .all(|(expected, parameter)| could_be(expected, parameter)), + expected + .iter() + .zip(parameters) + .all(|(expected, parameter)| proves(expected, parameter)), + ), + (_, None) => (true, true), + (None, Some(_)) => (true, false), + }; + if !takes || !could_be(return_type, expected_return) { + FunctionFit::No + } else if takes_proven && proves(return_type, expected_return) { + FunctionFit::Proven + } else { + FunctionFit::IfChecked } } @@ -217,13 +315,7 @@ impl Function { /// element of a container argument against a parameter like /// `List`, which [`Self::matches_value_args`] doesn't. pub fn conforms_value_args(&self, args: &[Value]) -> bool { - let st = match self { - Self::Native(f) => &f.static_type, - Self::Compiled(f) => &f.static_type, - Self::Closure(c) => &c.prototype.static_type, - Self::Memoized { function, .. } => return function.conforms_value_args(args), - }; - match st { + match self.signature() { StaticType::Function { parameters: Some(params), .. @@ -244,13 +336,7 @@ impl Function { /// Returns true if this function accepts the given arguments, without /// allocating a `StaticType` for each of them. pub fn matches_value_args(&self, args: &[Value]) -> bool { - let st = match self { - Self::Native(f) => &f.static_type, - Self::Compiled(f) => &f.static_type, - Self::Closure(c) => &c.prototype.static_type, - Self::Memoized { function, .. } => return function.matches_value_args(args), - }; - let StaticType::Function { parameters, .. } = st else { + let StaticType::Function { parameters, .. } = self.signature() else { return false; }; match parameters { @@ -273,6 +359,7 @@ impl fmt::Debug for Function { Self::Native(native) => write!(f, "{native:?}"), Self::Closure(closure) => write!(f, "", closure.prototype.name), Self::Memoized { function, .. } => write!(f, "", function), + Self::Checked { function, typ } => write!(f, "<{function:?} checked as {typ}>"), } } } @@ -287,6 +374,7 @@ impl fmt::Display for Function { Self::Native(native) => write!(f, "", native.name), Self::Closure(closure) => write!(f, "", closure.prototype.name), Self::Memoized { function, .. } => write!(f, ""), + Self::Checked { function, .. } => write!(f, "{function}"), } } } @@ -295,3 +383,76 @@ impl fmt::Debug for NativeFunction { write!(f, "", self.name, self.static_type) } } + +/// Whether a value of type `found` could be an `expected`, where what isn't +/// known in either, `Any` or a type parameter, could be anything. +fn could_be(found: &StaticType, expected: &StaticType) -> bool { + let unknown = |typ: &StaticType| { + matches!( + typ, + StaticType::Any | StaticType::Generic(_) | StaticType::Var(_) + ) + }; + // A function that never returns returns whatever is expected. + if unknown(found) || unknown(expected) || *found == StaticType::Never { + return true; + } + match (found, expected) { + (StaticType::List(a), StaticType::List(b)) + | (StaticType::Deque(a), StaticType::Deque(b)) + | (StaticType::Option(a), StaticType::Option(b)) + | (StaticType::Iterator(a), StaticType::Iterator(b)) + | (StaticType::Iterable(a), StaticType::Iterable(b)) + | (StaticType::MinHeap(a), StaticType::MinHeap(b)) + | (StaticType::MaxHeap(a), StaticType::MaxHeap(b)) => could_be(a, b), + (found, StaticType::Iterable(element)) => found + .sequence_element_type() + .is_some_and(|found| could_be(&found, element)), + (StaticType::Tuple(a), StaticType::Tuple(b)) => { + a.len() == b.len() && a.iter().zip(b).all(|(a, b)| could_be(a, b)) + } + ( + StaticType::Map { key, value }, + StaticType::Map { + key: expected_key, + value: expected_value, + }, + ) => could_be(key, expected_key) && could_be(value, expected_value), + ( + StaticType::Function { + parameters, + return_type, + }, + StaticType::Function { + parameters: expected_parameters, + return_type: expected_return, + }, + ) => { + let takes = match (parameters, expected_parameters) { + (Some(a), Some(b)) => { + a.len() == b.len() && a.iter().zip(b).all(|(a, b)| could_be(b, a)) + } + _ => true, + }; + takes && could_be(return_type, expected_return) + } + (found, expected) => found == expected, + } +} + +/// Whether every value of type `found` is an `expected`: a function that +/// takes `Any` takes anything, and one that returns a `List` returns an +/// `Iterable`. +fn proves(found: &StaticType, expected: &StaticType) -> bool { + *expected == StaticType::Any || (!has_unknowns(found) && found.fits(expected)) +} + +/// Whether `typ` leaves something unknown: `Any` or a type parameter. +fn has_unknowns(typ: &StaticType) -> bool { + typ.contains(&|part| { + matches!( + part, + StaticType::Any | StaticType::Generic(_) | StaticType::Var(_) + ) + }) +} diff --git a/ndc_vm/src/value/mod.rs b/ndc_vm/src/value/mod.rs index 1443fdb8..479d70f3 100644 --- a/ndc_vm/src/value/mod.rs +++ b/ndc_vm/src/value/mod.rs @@ -397,28 +397,43 @@ impl Value { /// Whether this value is a `target`, a type that holds no other values /// as a container does: a scalar, a struct, a function or an iterator. - /// What a function takes and returns can't be checked as the program - /// runs, only how many arguments it takes, and what an iterator makes - /// can't be either without making it. + /// A function is one if its own type says so (see [`Function::fit`]): + /// only [`Self::converted_to`] can make one that may be one a function + /// whose calls are checked. What an iterator makes can't be checked + /// without making it. fn is_a(&self, target: &StaticType) -> bool { + if let StaticType::Function { .. } = target + && let Self::Object(object) = self + && let Object::Function(function) = object.as_ref() + { + return function.fit(target) == FunctionFit::Proven; + } match (target, self.static_type()) { - ( - StaticType::Function { - parameters: expected, - .. - }, - StaticType::Function { - parameters: takes, .. - }, - ) => match (expected, takes) { - (Some(expected), Some(takes)) => expected.len() == takes.len(), - _ => true, - }, (StaticType::Iterator(_), StaticType::Iterator(_)) => true, (target, found) => found == *target, } } + /// This value as a `target`, if it is one, checking every element of a + /// container: a function that may be a function `target` is wrapped so + /// that each call checks what it returns. + pub fn converted_to(&self, target: &StaticType) -> Option { + if let StaticType::Function { .. } = target + && let Self::Object(object) = self + && let Object::Function(function) = object.as_ref() + { + return match function.fit(target) { + FunctionFit::Proven => Some(self.clone()), + FunctionFit::IfChecked => Some(Self::function(Function::Checked { + function: Rc::new(function.unchecked().clone()), + typ: Box::new(target.clone()), + })), + FunctionFit::No => None, + }; + } + self.conforms_to(target).then(|| self.clone()) + } + /// Check whether this value conforms to `target`, scanning container /// elements recursively. Empty containers conform to any element type. /// @@ -1098,7 +1113,7 @@ impl PartialEq for Object { ) => a_entries.borrow().eq(&*b_entries.borrow()), (Self::Function(a), Self::Function(b)) => { // Compare function values by identity (pointer equality) - match (a, b) { + match (a.unchecked(), b.unchecked()) { (Function::Compiled(a), Function::Compiled(b)) => { std::ptr::addr_eq(Rc::as_ptr(a), Rc::as_ptr(b)) } @@ -1187,7 +1202,7 @@ impl Hash for Object { } Self::Function(f) => { state.write_u8(9); - match f { + match f.unchecked() { Function::Compiled(func) => { state.write_u8(1); Rc::as_ptr(func).hash(state); @@ -1204,6 +1219,7 @@ impl Hash for Object { state.write_u8(4); Rc::as_ptr(cache).hash(state); } + Function::Checked { .. } => unreachable!("`unchecked` strips the checks"), } } Self::OverloadSet { .. } => { diff --git a/ndc_vm/src/vm.rs b/ndc_vm/src/vm.rs index 16091494..51b5b0b8 100644 --- a/ndc_vm/src/vm.rs +++ b/ndc_vm/src/vm.rs @@ -8,6 +8,7 @@ use crate::iterator::{ use crate::value::{CompiledFunction, Function, NativeFunc, NativeFunction}; use crate::{ClosureFunction, Object, Value}; use ndc_analysis::{CaptureSource, ResolvedVar}; +use ndc_core::StaticType; use ndc_lexer::Span; use std::cell::RefCell; use std::hash::{Hash, Hasher}; @@ -74,6 +75,9 @@ pub struct CallFrame { /// cache to write into and the hash key when the frame returns. `None` /// for ordinary (non-memoized) calls. memo: Option, + /// What the frame's call has to return, when it's a call of a + /// [`Function::Checked`]. Boxed to keep frames small. + returns: Option>, } /// The VM's globals for `natives`: native `i` is global slot `i`. The analyser @@ -108,6 +112,7 @@ impl Vm { ip: 0, frame_pointer: 0, memo: None, + returns: None, }], output: OutputSink::Stdout, #[cfg(feature = "trace")] @@ -195,19 +200,27 @@ impl Vm { } OpCode::Return => { let ret = self.stack.pop().expect("stack underflow"); - let frame = self.frames.last().expect("no frame"); + let frame = self.frames.last_mut().expect("no frame"); let frame_pointer = frame.frame_pointer; // If this was a memoized call, cache the return value now, // before the frame is torn down. if let Some((cache, key)) = &frame.memo { cache.borrow_mut().insert(*key, ret.clone()); } + let returns = frame.returns.take(); if frame_pointer == 0 { return Err(VmError::new("Return at top level", span)); } self.stack.truncate(frame_pointer - 1); self.frames.pop().expect("no frame to pop"); self.stack.push(ret); + // A checked call's result is checked where it was called. + if let Some(returns) = returns { + let caller = self.frames.last().expect("a call has a caller"); + let call = caller.closure.prototype.body.span(caller.ip - 1); + convert_result(&mut self.stack, &returns) + .map_err(|message| VmError::new(message, call))?; + } if self.frames.len() == target_depth { break Ok(()); } @@ -218,8 +231,8 @@ impl Vm { .push(frame.closure.prototype.body.constant(idx).clone()); } OpCode::CheckType(target) => { - let value = self.stack.last().expect("stack underflow"); - if !value.conforms_to(target) { + if !convert_top(&mut self.stack, target) { + let value = self.stack.last().expect("stack underflow"); return Err(VmError::new( format!("cannot cast {} to {target}", value.diagnostic_type()), span, @@ -227,8 +240,8 @@ impl Vm { } } OpCode::ExpectType(target) => { - let value = self.stack.last().expect("stack underflow"); - if !value.conforms_to(target) { + if !convert_top(&mut self.stack, target) { + let value = self.stack.last().expect("stack underflow"); return Err(VmError::new( format!("expected {target} but found {}", value.diagnostic_type()), span, @@ -649,6 +662,14 @@ impl Vm { let mut hasher = DefaultHasher::default(); for arg in &self.stack[start..] { arg.hash(&mut hasher); + // A function checked as another function type is the same + // function, but a call may return it, so the cache tells + // them apart. + if let Value::Object(object) = arg + && let Object::Function(Function::Checked { typ, .. }) = object.as_ref() + { + typ.hash(&mut hasher); + } } let key = hasher.finish(); @@ -667,6 +688,44 @@ impl Vm { } } + /// Calls the function a [`Function::Checked`] checks, and checks that + /// what it returns is what the function type it's used as says: right + /// away when the call completes here, or when its frame returns. + #[cold] + fn call_checked( + &mut self, + function: Rc, + typ: StaticType, + args: usize, + ) -> Result { + let StaticType::Function { return_type, .. } = typ else { + unreachable!("a checked function is used as a function type"); + }; + // The function type it's used as may take more than the function + // does, as `Fn(Any) -> Int` would of a function taking an Int. + let arguments = &self.stack[self.stack.len() - args..]; + if !function.conforms_value_args(arguments) { + return Err(VmError::native(format!( + "the function doesn't take the arguments ({})", + arguments + .iter() + .map(|argument| argument.diagnostic_type().to_string()) + .collect::>() + .join(", ") + ))); + } + match self.dispatch_call(Rc::unwrap_or_clone(function), args)? { + Dispatch::FramePushed => { + self.frames.last_mut().expect("a frame was pushed").returns = Some(return_type); + Ok(Dispatch::FramePushed) + } + Dispatch::Completed => { + convert_result(&mut self.stack, &return_type).map_err(VmError::native)?; + Ok(Dispatch::Completed) + } + } + } + /// Inner helper that pushes a new call frame. `memo` is `Some` when the /// call originates from a memoized function and the return value should be /// stored in the cache. @@ -705,6 +764,7 @@ impl Vm { prototype: f, upvalues: Vec::new(), }, + Function::Checked { function, typ } => return self.call_checked(function, *typ, args), Function::Memoized { .. } => { // Already unwrapped in dispatch_call; this branch is unreachable. unreachable!( @@ -718,6 +778,7 @@ impl Vm { ip: 0, frame_pointer: self.stack.len() - args, memo, + returns: None, }); for _ in args..num_locals { self.stack.push(Value::unit()); @@ -1169,3 +1230,34 @@ fn bad_range_bound(value: &Value) -> String { } format!("range bounds must be Int, got {}", value.diagnostic_type()) } + +/// Checks that the value on top of `stack` is a `target`, and makes it one: +/// a function that may be a function `target` gets its calls checked (see +/// [`Value::converted_to`]). +fn convert_top(stack: &mut [Value], target: &StaticType) -> bool { + let value = stack.last_mut().expect("stack underflow"); + if !matches!(target, StaticType::Function { .. }) { + return value.conforms_to(target); + } + match value.converted_to(target) { + Some(converted) => { + *value = converted; + true + } + None => false, + } +} + +/// Checks that the result of a call of a [`Function::Checked`], on top of +/// `stack`, is a `returns`, and makes it one, as [`convert_top`] does: a +/// function it returns may get its own calls checked. +fn convert_result(stack: &mut [Value], returns: &StaticType) -> Result<(), String> { + if convert_top(stack, returns) { + return Ok(()); + } + let returned = stack.last().expect("a call leaves its result"); + Err(format!( + "expected the function to return {returns} but it returned {}", + returned.diagnostic_type() + )) +} diff --git a/tests/functional/programs/017_dynamic_any/012_a_function_from_any_checks_what_it_returns.ndc b/tests/functional/programs/017_dynamic_any/012_a_function_from_any_checks_what_it_returns.ndc new file mode 100644 index 00000000..7189fe54 --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/012_a_function_from_any_checks_what_it_returns.ndc @@ -0,0 +1,11 @@ +// A function of type `Any` used as a `Fn(Int) -> Int` checks what each call +// returns when its own type doesn't say: `text`'s value takes and returns +// anything. +// expect-error: expected the function to return Int but it returned String +fn show(x: Int) { "int" } +fn show(x: String) { "string" } +fn text(x) => "s"; +let a: Any = text; +let f: Fn(Int) -> Int = a; +let n: Int = f(1); +print(show(n), n); diff --git a/tests/functional/programs/017_dynamic_any/013_a_function_from_any_must_return_what_its_type_says.ndc b/tests/functional/programs/017_dynamic_any/013_a_function_from_any_must_return_what_its_type_says.ndc new file mode 100644 index 00000000..397f9e9f --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/013_a_function_from_any_must_return_what_its_type_says.ndc @@ -0,0 +1,4 @@ +// A lambda whose own type says it returns a String isn't a `Fn(Int) -> Int`. +// expect-error: expected Fn(Int) -> Int but found Fn(Any) -> String +let a: Any = fn(x) => "s"; +let f: Fn(Int) -> Int = a; diff --git a/tests/functional/programs/017_dynamic_any/014_a_checked_function_is_the_same_function.ndc b/tests/functional/programs/017_dynamic_any/014_a_checked_function_is_the_same_function.ndc new file mode 100644 index 00000000..d5203a43 --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/014_a_checked_function_is_the_same_function.ndc @@ -0,0 +1,7 @@ +// A function of type `Any` used as a function type is the same function: it +// equals itself, and a call that returns what's expected works. +// expect-output: 3 true true +fn inc(x) => x + 1; +let a: Any = inc; +let f: Fn(Int) -> Int = a; +print(f(2), f == inc, a == f); diff --git a/tests/functional/programs/017_dynamic_any/015_an_overload_picked_as_the_program_runs_takes_a_function_that_fits.ndc b/tests/functional/programs/017_dynamic_any/015_an_overload_picked_as_the_program_runs_takes_a_function_that_fits.ndc new file mode 100644 index 00000000..ddd84ef6 --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/015_an_overload_picked_as_the_program_runs_takes_a_function_that_fits.ndc @@ -0,0 +1,9 @@ +// An overload picked as the program runs takes a function of type `Any` only +// if its own type shows it fits: `text`'s value takes and returns anything, +// so nothing says it's a `Fn(Int) -> Int`. Casting it first checks its calls. +// expect-output: 2 +fn h(f: Fn(Int) -> Int) => f(1) + 1; +fn h(x: Int) => x; +fn one(x) => x; +let a: Any = one; +print(h(a as Fn(Int) -> Int)); diff --git a/tests/functional/programs/017_dynamic_any/016_a_function_from_any_must_take_what_its_type_passes.ndc b/tests/functional/programs/017_dynamic_any/016_a_function_from_any_must_take_what_its_type_passes.ndc new file mode 100644 index 00000000..8e84ae13 --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/016_a_function_from_any_must_take_what_its_type_passes.ndc @@ -0,0 +1,4 @@ +// A lambda that takes a String isn't a `Fn(Int) -> Int`. +// expect-error: expected Fn(Int) -> Int but found Fn(String) -> Int +let a: Any = fn(s: String) => 1; +let f: Fn(Int) -> Int = a; diff --git a/tests/functional/programs/017_dynamic_any/017_an_overload_picked_as_the_program_runs_rejects_an_unchecked_function.ndc b/tests/functional/programs/017_dynamic_any/017_an_overload_picked_as_the_program_runs_rejects_an_unchecked_function.ndc new file mode 100644 index 00000000..8fd7d398 --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/017_an_overload_picked_as_the_program_runs_rejects_an_unchecked_function.ndc @@ -0,0 +1,8 @@ +// An overload picked as the program runs doesn't take a function of type +// `Any` whose own type doesn't show it fits the parameter. +// expect-error: no function called 'h' found matches the arguments +fn h(f: Fn(Int) -> Int) => f(1) + 1; +fn h(x: Int) => x; +fn text(x) => "s"; +let a: Any = text; +print(h(a)); diff --git a/tests/functional/programs/017_dynamic_any/018_a_checked_function_returning_a_function_checks_that_one_too.ndc b/tests/functional/programs/017_dynamic_any/018_a_checked_function_returning_a_function_checks_that_one_too.ndc new file mode 100644 index 00000000..1b386aee --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/018_a_checked_function_returning_a_function_checks_that_one_too.ndc @@ -0,0 +1,8 @@ +// A checked function that returns a function makes that one checked too, when +// its own type doesn't show it fits. +// expect-output: 1 +fn id(x) => x; +fn make() => id; +let a: Any = make; +let f = a as Fn() -> Fn(Int) -> Int; +print(f()(1)); diff --git a/tests/functional/programs/017_dynamic_any/019_a_function_that_never_returns_fits_any_return_type.ndc b/tests/functional/programs/017_dynamic_any/019_a_function_that_never_returns_fits_any_return_type.ndc new file mode 100644 index 00000000..816d6da1 --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/019_a_function_that_never_returns_fits_any_return_type.ndc @@ -0,0 +1,6 @@ +// A function that never returns can be used as one returning anything. +// expect-output: ok +fn forever() -> Never { forever() } +let a: Any = forever; +let f = a as Fn() -> Int; +print("ok"); diff --git a/tests/functional/programs/017_dynamic_any/020_recursion_through_a_checked_function_runs_deep.ndc b/tests/functional/programs/017_dynamic_any/020_recursion_through_a_checked_function_runs_deep.ndc new file mode 100644 index 00000000..3fb5b0bd --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/020_recursion_through_a_checked_function_runs_deep.ndc @@ -0,0 +1,8 @@ +// A checked function's calls run on the VM's own stack, so recursion through +// one goes as deep as any other. +// expect-output: 0 +let f: Fn(Int) -> Int = fn(x: Int) => 0; +fn step(x: Int) { if x == 0 { 0 } else { f(x - 1) } } +let a: Any = step; +f = a; +print(f(10000)); diff --git a/tests/functional/programs/017_dynamic_any/021_an_iterable_parameter_checks_an_any_argument.ndc b/tests/functional/programs/017_dynamic_any/021_an_iterable_parameter_checks_an_any_argument.ndc new file mode 100644 index 00000000..646ea57b --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/021_an_iterable_parameter_checks_an_any_argument.ndc @@ -0,0 +1,5 @@ +// An `Any` passed for an `Iterable` parameter is checked to be a sequence. +// expect-error: no function called 'consume' found matches the arguments: (Bool) +fn consume(xs: Iterable) => 0; +let a: Any = true; +print(consume(a)); diff --git a/tests/functional/programs/017_dynamic_any/022_a_function_returning_a_list_fits_one_returning_an_iterable.ndc b/tests/functional/programs/017_dynamic_any/022_a_function_returning_a_list_fits_one_returning_an_iterable.ndc new file mode 100644 index 00000000..a37cdb65 --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/022_a_function_returning_a_list_fits_one_returning_an_iterable.ndc @@ -0,0 +1,7 @@ +// A function whose own type says it returns a list fits a function type that +// returns a sequence, without checking its calls. +// expect-output: 1 +fn h(f: Fn(Int) -> Iterable) => 1; +fn h(x: Int) => 0; +let a: Any = fn(x: Int) => [x]; +print(h(a)); diff --git a/tests/functional/programs/017_dynamic_any/023_a_checked_function_checks_its_arguments.ndc b/tests/functional/programs/017_dynamic_any/023_a_checked_function_checks_its_arguments.ndc new file mode 100644 index 00000000..90bdfc7f --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/023_a_checked_function_checks_its_arguments.ndc @@ -0,0 +1,9 @@ +// A function used as a function type that takes more than it does checks +// each call's arguments: `store` takes an Int, not anything. +// expect-error: the function doesn't take the arguments (Bool) +let n: Int = 0; +fn store(x: Int) { n = x; 0 } +let a: Any = store; +let g: Any = a as Fn(Any) -> Int; +g(true); +print(n); diff --git a/tests/functional/programs/017_dynamic_any/024_a_pure_function_tells_checked_functions_apart.ndc b/tests/functional/programs/017_dynamic_any/024_a_pure_function_tells_checked_functions_apart.ndc new file mode 100644 index 00000000..7f4429b1 --- /dev/null +++ b/tests/functional/programs/017_dynamic_any/024_a_pure_function_tells_checked_functions_apart.ndc @@ -0,0 +1,11 @@ +// A function checked as two function types gives two arguments a `pure fn` +// caches separately. +// expect-output: ok +pure fn cached(f: Any) => f; +fn id(x) => x; +let a: Any = id; +let ints = a as Fn(Int) -> Int; +let strs = a as Fn(String) -> String; +cached(ints); +let s: Fn(String) -> String = cached(strs); +print(s("ok"));