From 096dab5ff026a4311cb0b5349f92f403d4acdb23 Mon Sep 17 00:00:00 2001 From: Tim Fennis Date: Fri, 13 Nov 2026 19:02:06 +0100 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20bugs=20found=20analysing=20the=20adv?= =?UTF-8?q?ent-of-code-ndc=20solutions=20=F0=9F=8E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- manual/src/reference/types/function.md | 7 ++ ndc_analyser/src/analyser/function_values.rs | 44 +++++++++-- ndc_analyser/src/analyser/generics.rs | 77 ++++++++++++------- ndc_analyser/src/analyser/solver.rs | 4 +- ndc_analyser/src/analyser/tests.rs | 16 ++-- ndc_analyser/src/types.rs | 12 +++ ndc_stdlib/src/sequence.rs | 48 ++++++++++++ ...neric_function_is_picked_for_each_call.ndc | 8 ++ ...r_an_iterable_is_the_sequence_it_holds.ndc | 7 ++ .../028_first_and_last_take_a_tuple.ndc | 4 + 10 files changed, 186 insertions(+), 41 deletions(-) create mode 100644 tests/functional/programs/005_functions/099_an_overloaded_function_value_in_a_generic_function_is_picked_for_each_call.ndc create mode 100644 tests/functional/programs/019_iterable/017_a_literal_passed_for_an_iterable_is_the_sequence_it_holds.ndc create mode 100644 tests/functional/programs/603_stdlib_seq/028_first_and_last_take_a_tuple.ndc diff --git a/manual/src/reference/types/function.md b/manual/src/reference/types/function.md index c2e8004b..b5c10a48 100644 --- a/manual/src/reference/types/function.md +++ b/manual/src/reference/types/function.md @@ -374,6 +374,13 @@ let g = split; // error: 'split' has several overloads, and nothing here sa Like a call, a value uses the nearest scope with an overload that fits. +In a generic function, each call's types pick the overload, as they pick the calls in its body: + +```ndc +fn signs(xs) => xs.map(signum); +print(signs([3, -2]), signs([0.5, -1.5])); // [1,-1] [1.0,-1.0] +``` + ## How a call finds its function A call such as `foo(x)` looks for `foo` in the scope it's written in first, then in each enclosing scope in turn, and finally among the built-in functions. diff --git a/ndc_analyser/src/analyser/function_values.rs b/ndc_analyser/src/analyser/function_values.rs index 5fede382..a1a9b4fb 100644 --- a/ndc_analyser/src/analyser/function_values.rs +++ b/ndc_analyser/src/analyser/function_values.rs @@ -4,7 +4,7 @@ //! `split(String, String)`. use super::Analyser; -use super::solver::{Constraint, EarlyOverload}; +use super::solver::{CallSite, Constraint, EarlyOverload}; use crate::error::AnalysisError; use crate::overloads::CallKind; use ndc_analysis::{Binding, Candidate}; @@ -35,6 +35,15 @@ impl FunctionValue { &self.expected } + /// This value with `expected` as the type expected of it, as a copy of + /// a generic function's constraints gets the types of its call. + pub(super) fn with_expected(&self, expected: StaticType) -> Self { + Self { + expected, + ..self.clone() + } + } + pub(super) fn id(&self) -> NodeId { self.id } @@ -42,6 +51,18 @@ impl FunctionValue { pub(super) fn mark_in_literal(&mut self) { self.in_literal = true; } + + /// Every overload the name can mean, for the program to pick from as it + /// calls the value. + pub(super) fn every_overload(&self) -> Binding { + Binding::Dynamic( + self.groups + .iter() + .flatten() + .map(|&symbol| Candidate::Scalar(symbol)) + .collect(), + ) + } } /// Which overload a function value means, given what's known of the type @@ -325,9 +346,20 @@ impl Analyser { if early && cfg!(debug_assertions) { self.remember_early_function_value(value, symbol); } - self.picks - .bindings - .insert(value.id, Binding::Resolved(Candidate::Scalar(symbol))); + self.record_function_value(value, symbol); + } + + /// Records that `value` means `symbol`, in the instance whose + /// constraints are being solved, if any. A copy of a generic function's + /// constraint records nothing, as the value is in that function's body, + /// whose instance for the call records it. + fn record_function_value(&mut self, value: &FunctionValue, symbol: SymbolId) { + if self.copied_through.is_none() { + self.pick_tables(self.recording).insert( + CallSite::Call(value.id), + Binding::Resolved(Candidate::Scalar(symbol)), + ); + } } /// Picks the nearest overload after an error, so analysis carries on @@ -336,8 +368,6 @@ impl Analyser { let symbol = self.resolver.innermost_reference(value.id); let typ = self.instantiated_type(symbol); self.vars.try_unify(&value.expected, &typ); - self.picks - .bindings - .insert(value.id, Binding::Resolved(Candidate::Scalar(symbol))); + self.record_function_value(value, symbol); } } diff --git a/ndc_analyser/src/analyser/generics.rs b/ndc_analyser/src/analyser/generics.rs index 5782a8fd..f53c7cfc 100644 --- a/ndc_analyser/src/analyser/generics.rs +++ b/ndc_analyser/src/analyser/generics.rs @@ -126,10 +126,19 @@ impl Analyser { }) .collect::>(); for waiting in &scheme { - if waiting.origin.is_none() - && let Constraint::Call(call) = &waiting.constraint - { - self.pick_for_any_arguments(call); + match &waiting.constraint { + Constraint::Call(call) if waiting.origin.is_none() => { + self.pick_for_any_arguments(call); + } + // The body compiled for arguments of type `Any` can't tell + // which overload a function value means, so the program + // picks as it calls it. + Constraint::FunctionValue(value) if waiting.origin.is_none() => { + self.picks + .bindings + .insert(value.id(), value.every_overload()); + } + _ => {} } } // A value of type `Any` that the body passes where a type parameter @@ -209,15 +218,35 @@ impl Analyser { /// The generic function's variables that `constraint` settles once it's /// solved. A function passed to a call gets the types the call passes /// it, as `fn(a, x) => a + x` in `xs.fold(0, fn(a, x) => a + x)`, so - /// they count as settled by the call too. + /// they count as settled by the call too, and so does an overloaded + /// function's name passed to one, as `signum` in `xs.map(signum)`, + /// whose type is what the call expects of it. fn generic_outputs(&self, constraint: &Constraint) -> Vec { let passed_functions = match constraint { - Constraint::Call(call) => call - .arguments() - .iter() - .map(|argument| self.vars.resolve(argument)) - .filter(|argument| matches!(argument, StaticType::Function { .. })) - .collect(), + Constraint::Call(call) => { + let values = self + .waiting + .values() + .filter_map(|waiting| match &waiting.constraint { + Constraint::FunctionValue(value) => { + Some(self.vars.resolve(value.expected())) + } + _ => None, + }) + .collect::>(); + call.arguments() + .iter() + .map(|argument| self.vars.resolve(argument)) + .filter(|argument| { + matches!(argument, StaticType::Function { .. }) || values.contains(argument) + }) + .collect() + } + // Picking the overload settles what the value returns. + Constraint::FunctionValue(value) => match self.vars.resolve(value.expected()) { + StaticType::Function { return_type, .. } => vec![*return_type], + _ => Vec::new(), + }, _ => Vec::new(), }; constraint @@ -232,8 +261,8 @@ impl Analyser { /// The generic function's variables that its constraints wait on but /// that its parameters don't decide, by constraint, oldest first. What /// a constraint settles is decided when what it waits on is. An - /// overloaded function's name used as a value has to be decided in the - /// body, which is compiled once, so what it waits on counts too. + /// overloaded function's name used as a value is picked like a call: + /// for each instance, from the types its parameters decide. fn ambiguous_vars(&self, parameters: &[StaticType]) -> Vec<(u64, Vec)> { let serials = self.generic_constraints(); let mut decided = parameters @@ -245,11 +274,10 @@ impl Analyser { let mut changed = false; for serial in &serials { let constraint = &self.waiting[serial].constraint; - if matches!(constraint, Constraint::FunctionValue(_)) - || !self - .generic_inputs(constraint) - .iter() - .all(|var| decided.contains(var)) + if !self + .generic_inputs(constraint) + .iter() + .all(|var| decided.contains(var)) { continue; } @@ -265,14 +293,11 @@ impl Analyser { .into_iter() .filter_map(|serial| { let constraint = &self.waiting[&serial].constraint; - let ambiguous = match constraint { - Constraint::FunctionValue(_) => self.generic_inputs(constraint), - _ => self - .generic_inputs(constraint) - .into_iter() - .filter(|var| !decided.contains(var)) - .collect(), - }; + let ambiguous = self + .generic_inputs(constraint) + .into_iter() + .filter(|var| !decided.contains(var)) + .collect::>(); (!ambiguous.is_empty()).then_some((serial, ambiguous)) }) .collect() diff --git a/ndc_analyser/src/analyser/solver.rs b/ndc_analyser/src/analyser/solver.rs index fa6d63b5..0734a798 100644 --- a/ndc_analyser/src/analyser/solver.rs +++ b/ndc_analyser/src/analyser/solver.rs @@ -118,7 +118,9 @@ impl Constraint { result: replace(&call.result), ..call.clone() }), - Self::FunctionValue(value) => Self::FunctionValue(value.clone()), + Self::FunctionValue(value) => { + Self::FunctionValue(value.with_expected(replace(value.expected()))) + } Self::Iterate { sequence, element, diff --git a/ndc_analyser/src/analyser/tests.rs b/ndc_analyser/src/analyser/tests.rs index b75ae679..d3183046 100644 --- a/ndc_analyser/src/analyser/tests.rs +++ b/ndc_analyser/src/analyser/tests.rs @@ -1557,15 +1557,17 @@ fn a_lambda_a_generic_function_passes_on_takes_what_each_call_gives_it() { } #[test] -fn an_overloaded_function_value_in_a_generic_function_is_decided_by_its_body() { - // The body is compiled once, so `g` has to be one `abs`, whatever `f` - // is called with. +fn an_overloaded_function_value_in_a_generic_function_is_picked_for_each_call() { + // Each instance of `f` picks the `abs` its argument's type calls for. let globals = natives(&[("abs", "(Int) -> Int"), ("abs", "(Float) -> Float")]); + let source = "fn f(x) { let g = abs; g(x) }"; assert_eq!( - errors("fn f(x) { let g = abs; g(x) } f(1);", globals.clone()), - vec![ - "'abs' has several overloads that are a Fn(Any) -> Any, and nothing here says which one is meant" - ] + analyse_last_type_with_globals(&format!("{source} f(1)"), globals.clone()), + StaticType::Int + ); + assert_eq!( + analyse_last_type_with_globals(&format!("{source} f(2.5)"), globals.clone()), + StaticType::Float ); assert_eq!( errors( diff --git a/ndc_analyser/src/types.rs b/ndc_analyser/src/types.rs index f0d64b39..e49612ce 100644 --- a/ndc_analyser/src/types.rs +++ b/ndc_analyser/src/types.rs @@ -817,6 +817,18 @@ impl TypeVars { // n = a` is still an Int. (StaticType::Var(var), StaticType::Any) if self.is_literal(*var) => true, (StaticType::Iterable(_), StaticType::Var(var)) if !self.is_literal(*var) => true, + // A literal passed for an `Iterable` is the sequence it holds, + // as `["a"]` passed to `len` is a `List`: only a + // parameter is ever an `Iterable` itself. + (StaticType::Iterable(_), StaticType::Var(var)) => { + let Some(held) = self + .literal_defaults(*var) + .and_then(|defaults| defaults.into_iter().next()) + else { + return true; + }; + self.settle_literal(*var, &held) && self.bind_argument(¶meter, &held) + } (StaticType::Var(_), _) | (_, StaticType::Var(_)) => self.unify(¶meter, &argument), (_, StaticType::Any) => true, ( diff --git a/ndc_stdlib/src/sequence.rs b/ndc_stdlib/src/sequence.rs index 76e09ead..187bcf61 100644 --- a/ndc_stdlib/src/sequence.rs +++ b/ndc_stdlib/src/sequence.rs @@ -997,6 +997,41 @@ pub mod extra { use std::rc::Rc; pub fn register(env: &mut FunctionRegistry>) { + // A tuple's first and last elements have their own types, so each + // length up to four gets a signature that says which. + for (name, signatures, documentation) in [ + ( + "first", + [ + "((A,)) -> A", + "((A, B)) -> A", + "((A, B, C)) -> A", + "((A, B, C, D)) -> A", + ], + "Returns the first element of a tuple.", + ), + ( + "last", + [ + "((A,)) -> A", + "((A, B)) -> B", + "((A, B, C)) -> C", + "((A, B, C, D)) -> D", + ], + "Returns the last element of a tuple.", + ), + ] { + for signature in signatures { + env.declare_global_fn(Rc::new(NativeFunction { + name: name.to_string(), + documentation: Some(documentation.to_string()), + static_type: StaticType::signature(signature).expect("valid signature"), + func: NativeFunc::Simple(Box::new(move |args| { + tuple_end(args, name == "first") + })), + })); + } + } for signature in [ // Two sequences zip into pairs of their elements, three into // triples, and four into quadruples. @@ -1015,6 +1050,19 @@ pub mod extra { } } + /// The first or last element of the tuple in `args`. + fn tuple_end(args: &[Value], first: bool) -> Result { + let [Value::Object(object)] = args else { + return Err(VmError::native("expected one tuple".to_string())); + }; + let Object::Tuple(values) = object.as_ref() else { + return Err(VmError::native("expected a tuple".to_string())); + }; + let end = if first { values.first() } else { values.last() }; + end.cloned() + .ok_or_else(|| VmError::native("the tuple is empty".to_string())) + } + fn zip(args: &[Value]) -> Result { if args.len() < 2 { return Err(VmError::native( diff --git a/tests/functional/programs/005_functions/099_an_overloaded_function_value_in_a_generic_function_is_picked_for_each_call.ndc b/tests/functional/programs/005_functions/099_an_overloaded_function_value_in_a_generic_function_is_picked_for_each_call.ndc new file mode 100644 index 00000000..f502cfd1 --- /dev/null +++ b/tests/functional/programs/005_functions/099_an_overloaded_function_value_in_a_generic_function_is_picked_for_each_call.ndc @@ -0,0 +1,8 @@ +// Each call of a generic function picks the overload a function value in its +// body means, from that call's types. +// expect-output: [1,-1] [1.0,-1.0] +fn signs(ns) { + let diffs = ns.pairwise(fn(a, b) { b - a }); + diffs.map(signum) +} +print(signs([1, 3, 2]), signs([1.0, 2.5, 0.5])); diff --git a/tests/functional/programs/019_iterable/017_a_literal_passed_for_an_iterable_is_the_sequence_it_holds.ndc b/tests/functional/programs/019_iterable/017_a_literal_passed_for_an_iterable_is_the_sequence_it_holds.ndc new file mode 100644 index 00000000..ab4e08fb --- /dev/null +++ b/tests/functional/programs/019_iterable/017_a_literal_passed_for_an_iterable_is_the_sequence_it_holds.ndc @@ -0,0 +1,7 @@ +// A list literal passed where any sequence is expected is still a list, so +// `++` takes it afterwards. +// expect-output: ["A","A"] +let t = ("A", ["A"]); +let cur, path = t; +let n = path.len; +print(path ++ [cur]); diff --git a/tests/functional/programs/603_stdlib_seq/028_first_and_last_take_a_tuple.ndc b/tests/functional/programs/603_stdlib_seq/028_first_and_last_take_a_tuple.ndc new file mode 100644 index 00000000..e4b348cc --- /dev/null +++ b/tests/functional/programs/603_stdlib_seq/028_first_and_last_take_a_tuple.ndc @@ -0,0 +1,4 @@ +// `first` and `last` take a tuple, and give its first or last element's type. +// expect-output: 2 3.0 [(-1,0)] +let t = (1, "a", 2.5); +print(t.first + 1, t.last + 0.5, [((-1, 0), true), ((1, 0), false)].filter(fn(x) => x[1]).map(first)); From 9ed947139d9da7814898ff2bd5cf408767284d7f Mon Sep 17 00:00:00 2001 From: Tim Fennis Date: Fri, 13 Nov 2026 19:07:20 +0100 Subject: [PATCH 2/5] =?UTF-8?q?fix(analyser):=20order=20a=20function=20val?= =?UTF-8?q?ue's=20runtime=20overloads=20by=20specificity=20=F0=9F=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- ndc_analyser/src/analyser/function_values.rs | 28 ++++++++++++------- ndc_analyser/src/analyser/generics.rs | 5 ++-- ndc_analyser/src/overloads.rs | 2 +- ...ogram_takes_the_most_specific_overload.ndc | 9 ++++++ 4 files changed, 30 insertions(+), 14 deletions(-) create mode 100644 tests/functional/programs/005_functions/100_a_function_value_left_to_the_program_takes_the_most_specific_overload.ndc diff --git a/ndc_analyser/src/analyser/function_values.rs b/ndc_analyser/src/analyser/function_values.rs index a1a9b4fb..5254a646 100644 --- a/ndc_analyser/src/analyser/function_values.rs +++ b/ndc_analyser/src/analyser/function_values.rs @@ -52,16 +52,9 @@ impl FunctionValue { self.in_literal = true; } - /// Every overload the name can mean, for the program to pick from as it - /// calls the value. - pub(super) fn every_overload(&self) -> Binding { - Binding::Dynamic( - self.groups - .iter() - .flatten() - .map(|&symbol| Candidate::Scalar(symbol)) - .collect(), - ) + /// The functions the name can mean, grouped by scope, nearest first. + pub(super) fn groups(&self) -> &[Vec] { + &self.groups } } @@ -271,6 +264,21 @@ impl Analyser { fits } + /// Every overload `value` can mean, for the program to pick from as it + /// calls it: nearest scope first, and in each the most specific first, + /// as for a call the program picks for, since it takes the first that + /// accepts the arguments. + pub(super) fn every_overload(&self, value: &FunctionValue) -> Binding { + Binding::Dynamic( + value + .groups() + .iter() + .flat_map(|group| self.overloads().most_specific_first(group.clone())) + .map(Candidate::Scalar) + .collect(), + ) + } + /// Picks the overload `value` means if what's known of the type expected /// of it decides it, and returns whether it did. pub(super) fn try_function_value(&mut self, value: &FunctionValue) -> bool { diff --git a/ndc_analyser/src/analyser/generics.rs b/ndc_analyser/src/analyser/generics.rs index f53c7cfc..a8b50549 100644 --- a/ndc_analyser/src/analyser/generics.rs +++ b/ndc_analyser/src/analyser/generics.rs @@ -134,9 +134,8 @@ impl Analyser { // which overload a function value means, so the program // picks as it calls it. Constraint::FunctionValue(value) if waiting.origin.is_none() => { - self.picks - .bindings - .insert(value.id(), value.every_overload()); + let binding = self.every_overload(value); + self.picks.bindings.insert(value.id(), binding); } _ => {} } diff --git a/ndc_analyser/src/overloads.rs b/ndc_analyser/src/overloads.rs index 6be43783..dbb5acea 100644 --- a/ndc_analyser/src/overloads.rs +++ b/ndc_analyser/src/overloads.rs @@ -533,7 +533,7 @@ impl<'a> Overloads<'a> { /// accepts the arguments. Overloads in one scope don't tie, so of those /// that accept them, one is more specific than the others, and it's the /// first. - fn most_specific_first(self, mut symbols: Vec) -> Vec { + pub(crate) fn most_specific_first(self, mut symbols: Vec) -> Vec { let mut ordered = Vec::with_capacity(symbols.len()); while !symbols.is_empty() { let next = symbols diff --git a/tests/functional/programs/005_functions/100_a_function_value_left_to_the_program_takes_the_most_specific_overload.ndc b/tests/functional/programs/005_functions/100_a_function_value_left_to_the_program_takes_the_most_specific_overload.ndc new file mode 100644 index 00000000..a13d6245 --- /dev/null +++ b/tests/functional/programs/005_functions/100_a_function_value_left_to_the_program_takes_the_most_specific_overload.ndc @@ -0,0 +1,9 @@ +// A generic function used as a value runs its body compiled for any +// arguments, which leaves the function value in it to the program: it takes +// the most specific overload that accepts the argument, as a call would. +// expect-output: int +fn choose(x: Int) => "int"; +fn choose(x: Any) => "any"; +fn apply(x) { let g = choose; g(x) } +let h: Fn(Int) -> String = apply; +print(h(1)); From 16839d7927318c97e39ecf9025b4e5cf2b246de0 Mon Sep 17 00:00:00 2001 From: Tim Fennis Date: Fri, 13 Nov 2026 19:10:50 +0100 Subject: [PATCH 3/5] =?UTF-8?q?feat(stdlib):=20type=20first,=20last=20and?= =?UTF-8?q?=20zip=20for=20tuples=20of=20up=20to=20twelve=20elements=20?= =?UTF-8?q?=F0=9F=93=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- ndc_stdlib/src/sequence.rs | 88 +++++++++++-------- ...tuples_up_to_twelve_elements_are_typed.ndc | 5 ++ 2 files changed, 56 insertions(+), 37 deletions(-) create mode 100644 tests/functional/programs/603_stdlib_seq/029_tuples_up_to_twelve_elements_are_typed.ndc diff --git a/ndc_stdlib/src/sequence.rs b/ndc_stdlib/src/sequence.rs index 187bcf61..c6210fe7 100644 --- a/ndc_stdlib/src/sequence.rs +++ b/ndc_stdlib/src/sequence.rs @@ -997,59 +997,73 @@ pub mod extra { use std::rc::Rc; pub fn register(env: &mut FunctionRegistry>) { - // A tuple's first and last elements have their own types, so each - // length up to four gets a signature that says which. - for (name, signatures, documentation) in [ - ( - "first", - [ - "((A,)) -> A", - "((A, B)) -> A", - "((A, B, C)) -> A", - "((A, B, C, D)) -> A", - ], - "Returns the first element of a tuple.", - ), - ( - "last", - [ - "((A,)) -> A", - "((A, B)) -> B", - "((A, B, C)) -> C", - "((A, B, C, D)) -> D", - ], - "Returns the last element of a tuple.", - ), - ] { - for signature in signatures { + // A tuple's elements have their own types, and a signature can't say + // "a tuple of any length", so each length up to twelve, as far as + // Rust's standard library goes for tuples, gets one that says which. + const LONGEST: usize = 12; + let names = |count: usize| { + ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"][..count].to_vec() + }; + for length in 1..=LONGEST { + let names = names(length); + let parameters = names.join(", "); + let tuple = if length == 1 { + "(A,)".to_string() + } else { + format!("({parameters})") + }; + for (name, returns, documentation) in [ + ("first", names[0], "Returns the first element of a tuple."), + ( + "last", + names[length - 1], + "Returns the last element of a tuple.", + ), + ] { env.declare_global_fn(Rc::new(NativeFunction { name: name.to_string(), documentation: Some(documentation.to_string()), - static_type: StaticType::signature(signature).expect("valid signature"), + static_type: StaticType::signature(&format!( + "<{parameters}>({tuple}) -> {returns}" + )) + .expect("valid signature"), func: NativeFunc::Simple(Box::new(move |args| { tuple_end(args, name == "first") })), })); } } - for signature in [ - // Two sequences zip into pairs of their elements, three into - // triples, and four into quadruples. - "(Iterable, Iterable) -> List<(A, B)>", - "(Iterable, Iterable, Iterable) -> List<(A, B, C)>", - "(Iterable, Iterable, Iterable, Iterable) -> List<(A, B, C, D)>", - // More make tuples as long as the number of arguments. - "(*) -> List", - ] { + // Two or more sequences zip into tuples of their elements. + for length in 2..=LONGEST { + let names = names(length); + let sequences = names + .iter() + .map(|name| format!("Iterable<{name}>")) + .collect::>() + .join(", "); env.declare_global_fn(Rc::new(NativeFunction { name: "zip".to_string(), - documentation: Some("Zips two or more sequences together into a list of tuples. The result length is the minimum length of the inputs.".to_string()), - static_type: StaticType::signature(signature).expect("valid signature"), + documentation: Some(ZIP_DOCUMENTATION.to_string()), + static_type: StaticType::signature(&format!( + "<{}>({sequences}) -> List<({})>", + names.join(", "), + names.join(", ") + )) + .expect("valid signature"), func: NativeFunc::Simple(Box::new(zip)), })); } + // More make tuples as long as the number of arguments. + env.declare_global_fn(Rc::new(NativeFunction { + name: "zip".to_string(), + documentation: Some(ZIP_DOCUMENTATION.to_string()), + static_type: StaticType::signature("(*) -> List").expect("valid signature"), + func: NativeFunc::Simple(Box::new(zip)), + })); } + const ZIP_DOCUMENTATION: &str = "Zips two or more sequences together into a list of tuples. The result length is the minimum length of the inputs."; + /// The first or last element of the tuple in `args`. fn tuple_end(args: &[Value], first: bool) -> Result { let [Value::Object(object)] = args else { diff --git a/tests/functional/programs/603_stdlib_seq/029_tuples_up_to_twelve_elements_are_typed.ndc b/tests/functional/programs/603_stdlib_seq/029_tuples_up_to_twelve_elements_are_typed.ndc new file mode 100644 index 00000000..73625026 --- /dev/null +++ b/tests/functional/programs/603_stdlib_seq/029_tuples_up_to_twelve_elements_are_typed.ndc @@ -0,0 +1,5 @@ +// `first`, `last` and `zip` are typed for tuples of up to twelve elements. +// expect-output: 2 z! [(1,2,3,4,5)] +let t = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, "z"); +let zipped: List<(Int, Int, Int, Int, Int)> = zip([1], [2], [3], [4], [5]); +print(t.first + 1, t.last ++ "!", zipped); From 7d53248764fea42eab7449e1227e5a6b5fefc560 Mon Sep 17 00:00:00 2001 From: Tim Fennis Date: Fri, 13 Nov 2026 19:16:29 +0100 Subject: [PATCH 4/5] =?UTF-8?q?fix(vm):=20let=20a=20native=20call=20back?= =?UTF-8?q?=20the=20overloads=20a=20function=20value=20leaves=20to=20the?= =?UTF-8?q?=20program=20=F0=9F=94=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- ndc_analyser/src/analyser/solver.rs | 4 +- ndc_macros/src/vm_convert.rs | 6 ++- ndc_vm/src/value/mod.rs | 9 +++- ndc_vm/src/vm.rs | 49 ++++++++++++++++++- ...eft_to_the_program_works_as_a_callback.ndc | 7 +++ 5 files changed, 69 insertions(+), 6 deletions(-) create mode 100644 tests/functional/programs/005_functions/101_a_function_value_left_to_the_program_works_as_a_callback.ndc diff --git a/ndc_analyser/src/analyser/solver.rs b/ndc_analyser/src/analyser/solver.rs index 0734a798..99ac6708 100644 --- a/ndc_analyser/src/analyser/solver.rs +++ b/ndc_analyser/src/analyser/solver.rs @@ -1778,7 +1778,9 @@ impl Analyser { _ => return None, }; match sequence { - StaticType::Var(var) if !self.vars.is_literal(var) => Some((sequence, element)), + // A literal whose type isn't known yet either, as the + // element of `[1][0]` is, waits like any other value. + StaticType::Var(_) => Some((sequence, element)), _ => None, } }) diff --git a/ndc_macros/src/vm_convert.rs b/ndc_macros/src/vm_convert.rs index 3aee245d..254005f8 100644 --- a/ndc_macros/src/vm_convert.rs +++ b/ndc_macros/src/vm_convert.rs @@ -408,7 +408,11 @@ pub fn try_vm_input(ty: &syn::Type, position: usize) -> Option { let mut #temp = match #raw { ndc_vm::value::Value::Object(_obj) => match _obj.as_ref() { ndc_vm::value::Object::Function(f) => ndc_vm::VmCallable { - function: f.clone(), + function: ndc_vm::Callback::Function(f.clone()), + vm: _vm, + }, + ndc_vm::value::Object::OverloadSet { .. } => ndc_vm::VmCallable { + function: ndc_vm::Callback::Overloads(_obj.clone()), vm: _vm, }, _ => return Err(#err), diff --git a/ndc_vm/src/value/mod.rs b/ndc_vm/src/value/mod.rs index 479d70f3..cadbf654 100644 --- a/ndc_vm/src/value/mod.rs +++ b/ndc_vm/src/value/mod.rs @@ -404,9 +404,14 @@ impl Value { 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 object.as_ref() { + Object::Function(function) => return function.fit(target) == FunctionFit::Proven, + // The overloads of a function the analyser couldn't pick + // from, of which the program picks one for each call. + Object::OverloadSet { .. } => return true, + _ => {} + } } match (target, self.static_type()) { (StaticType::Iterator(_), StaticType::Iterator(_)) => true, diff --git a/ndc_vm/src/vm.rs b/ndc_vm/src/vm.rs index 51b5b0b8..43a9a04f 100644 --- a/ndc_vm/src/vm.rs +++ b/ndc_vm/src/vm.rs @@ -807,6 +807,36 @@ impl Vm { } } + /// Calls the overload of `overloads` that accepts `args`, inline on this + /// VM, as a callback from a native function. + fn call_overloads_back( + &mut self, + overloads: Rc, + args: &[Value], + ) -> Result { + let n = args.len(); + self.stack.push(Value::Object(overloads)); + self.stack.extend(args.iter().cloned()); + let picked = self.resolve_callee(n).map_err(VmError::native)?; + let Some(function) = picked else { + let types = args + .iter() + .map(|arg| arg.static_type().to_string()) + .collect::>() + .join(", "); + let name = self.callee_name(n).unwrap_or_else(|| "?".to_string()); + self.stack.truncate(self.stack.len() - n - 1); + return Err(VmError::native(format!( + "no function called '{name}' found matches the arguments: ({types})" + ))); + }; + let depth = self.frames.len(); + if self.dispatch_call(function, n)? == Dispatch::FramePushed { + self.run_to_depth(depth)?; + } + Ok(self.stack.pop().expect("callback must produce a value")) + } + /// Returns the expression result left on top of the stack after `run()`. /// If the last top-level expression was a statement (semicolon-terminated) /// its value was already popped, so the stack is exactly `num_locals` deep @@ -1205,15 +1235,30 @@ impl CallFrame { /// Calling `call()` runs the function inline on the parent VM via /// `Vm::call_callback` — no child VM is allocated. pub struct VmCallable<'a> { - pub function: Function, + pub function: Callback, pub vm: &'a mut Vm, } +/// What a native function calls back. +pub enum Callback { + /// A function. + Function(Function), + /// The overloads of a function, one of which the program picks for each + /// call's arguments: an overloaded function's name used as a value where + /// the analyser couldn't tell which one is meant. + Overloads(Rc), +} + impl VmCallable<'_> { /// Call this function with the given arguments, running inline on the /// parent VM. pub fn call(&mut self, args: &[Value]) -> Result { - self.vm.call_callback(self.function.clone(), args) + match &self.function { + Callback::Function(function) => self.vm.call_callback(function.clone(), args), + Callback::Overloads(overloads) => { + self.vm.call_overloads_back(Rc::clone(overloads), args) + } + } } } diff --git a/tests/functional/programs/005_functions/101_a_function_value_left_to_the_program_works_as_a_callback.ndc b/tests/functional/programs/005_functions/101_a_function_value_left_to_the_program_works_as_a_callback.ndc new file mode 100644 index 00000000..c3dd7d5e --- /dev/null +++ b/tests/functional/programs/005_functions/101_a_function_value_left_to_the_program_works_as_a_callback.ndc @@ -0,0 +1,7 @@ +// A generic function used as a value runs its body compiled for any +// arguments, where `signum` is left to the program to pick for each element +// `map` passes it. +// expect-output: [1,-1] +fn signs(xs) => xs.map(signum); +let f: Fn(List) -> List = signs; +print(f([1, -2])); From 849bf4e177faf0cf3d4cd8edb3347baafd4f21c8 Mon Sep 17 00:00:00 2001 From: Tim Fennis Date: Fri, 13 Nov 2026 19:29:59 +0100 Subject: [PATCH 5/5] =?UTF-8?q?fix(vm):=20make=20a=20function=20value=20th?= =?UTF-8?q?e=20program=20picks=20the=20overload=20of=20an=20ordinary=20fun?= =?UTF-8?q?ction=20=F0=9F=A7=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- ndc_macros/src/vm_convert.rs | 6 +- ndc_vm/src/chunk.rs | 9 +- ndc_vm/src/compiler.rs | 24 +++- ndc_vm/src/value/function.rs | 41 ++++++- ndc_vm/src/value/mod.rs | 14 +-- ndc_vm/src/vm.rs | 110 ++++++++++-------- ..._the_program_keeps_its_local_overloads.ndc | 12 ++ ..._program_is_checked_as_a_function_type.ndc | 12 ++ ...rogram_can_be_a_pure_function_argument.ndc | 7 ++ 9 files changed, 169 insertions(+), 66 deletions(-) create mode 100644 tests/functional/programs/005_functions/102_a_function_value_left_to_the_program_keeps_its_local_overloads.ndc create mode 100644 tests/functional/programs/005_functions/103_a_function_value_left_to_the_program_is_checked_as_a_function_type.ndc create mode 100644 tests/functional/programs/005_functions/104_a_function_value_left_to_the_program_can_be_a_pure_function_argument.ndc diff --git a/ndc_macros/src/vm_convert.rs b/ndc_macros/src/vm_convert.rs index 254005f8..3aee245d 100644 --- a/ndc_macros/src/vm_convert.rs +++ b/ndc_macros/src/vm_convert.rs @@ -408,11 +408,7 @@ pub fn try_vm_input(ty: &syn::Type, position: usize) -> Option { let mut #temp = match #raw { ndc_vm::value::Value::Object(_obj) => match _obj.as_ref() { ndc_vm::value::Object::Function(f) => ndc_vm::VmCallable { - function: ndc_vm::Callback::Function(f.clone()), - vm: _vm, - }, - ndc_vm::value::Object::OverloadSet { .. } => ndc_vm::VmCallable { - function: ndc_vm::Callback::Overloads(_obj.clone()), + function: f.clone(), vm: _vm, }, _ => return Err(#err), diff --git a/ndc_vm/src/chunk.rs b/ndc_vm/src/chunk.rs index e01e00b2..97878caf 100644 --- a/ndc_vm/src/chunk.rs +++ b/ndc_vm/src/chunk.rs @@ -64,6 +64,7 @@ impl std::fmt::Debug for JumpTarget { /// | `JumpIfFalse` | `[… bool → … bool]` | 0 (peeks, jumps if false) | /// | `MakeList` | `[… v1…vn → … list]` | −(n−1) | /// | `MakeTuple` | `[… v1…vn → … tuple]` | −(n−1) | +/// | `MakeOverloads` | `[… f1…fn → … function]` | −(n−1) | /// | `MakeMap` | `[… k1 v1…kn vn (default?) → … map]` | −(2n−1) or −2n if `has_default` | /// | `MakeRange` | `[… start (end?) → … iter]` | −1 if bounded, 0 if unbounded | /// | `Closure` | `[… → … closure]` | +1 | @@ -119,6 +120,10 @@ pub enum OpCode { MakeList(usize), /// Pops `n` values, pushes a tuple. `[… v1…vn → … tuple]` MakeTuple(usize), + /// Pops `n` functions, most specific first, and pushes a function that + /// calls the first of them that accepts each call's arguments. + /// `[… f1…fn → … function]` + MakeOverloads(usize), /// Pops `2n` values (+ optional default), pushes a map. MakeMap { pairs: usize, has_default: bool }, /// Pushes a closure, capturing values from locals/upvalues. `[… → … closure]` @@ -175,6 +180,7 @@ impl OpCode { Self::SetGlobal(_) => "SetGlobal", Self::MakeList(_) => "MakeList", Self::MakeTuple(_) => "MakeTuple", + Self::MakeOverloads(_) => "MakeOverloads", Self::MakeMap { .. } => "MakeMap", Self::Closure { .. } => "Closure", Self::GetIterator => "GetIterator", @@ -216,7 +222,7 @@ impl OpCode { | Self::GetGlobal(_) => 1, Self::SetLocal(_) | Self::SetCell(_) | Self::SetUpvalue(_) | Self::SetGlobal(_) => -1, // Pops `n` elements, pushes the collection: -n+1. - Self::MakeList(n) | Self::MakeTuple(n) => 1 - *n as isize, + Self::MakeList(n) | Self::MakeTuple(n) | Self::MakeOverloads(n) => 1 - *n as isize, // Pops a key and a value per pair plus the optional default, // pushes the map: -(2*pairs + has_default) + 1. Self::MakeMap { pairs, has_default } => { @@ -270,6 +276,7 @@ impl std::fmt::Debug for OpCode { Self::SetUpvalue(n) => write!(f, "SetUpvalue({n})"), Self::MakeList(n) => write!(f, "MakeList({n})"), Self::MakeTuple(n) => write!(f, "MakeTuple({n})"), + Self::MakeOverloads(n) => write!(f, "MakeOverloads({n})"), Self::MakeMap { pairs, has_default } => { write!(f, "MakeMap({pairs}, default={has_default})") } diff --git a/ndc_vm/src/compiler.rs b/ndc_vm/src/compiler.rs index cdf272f6..de8ad145 100644 --- a/ndc_vm/src/compiler.rs +++ b/ndc_vm/src/compiler.rs @@ -234,9 +234,18 @@ impl Compiler { let idx = self.ir.add_constant(value); self.ir.write(OpCode::Constant(idx), span); } - Expression::Identifier { .. } => { - self.compile_binding(&self.binding(id), span)?; - } + Expression::Identifier { .. } => match self.binding(id) { + // A function's name used as a value whose overload is picked + // as it's called: each overload is looked up now, where the + // value is made, so the value works wherever it's called. + Binding::Dynamic(candidates) => { + for candidate in &candidates { + self.emit_get_var(candidate.var(), span); + } + self.ir.write(OpCode::MakeOverloads(candidates.len()), span); + } + binding => self.compile_binding(&binding, span)?, + }, Expression::Cast { value, .. } => { self.compile_expr(*value)?; match self.analysis.cast_checks.get(&id) { @@ -433,7 +442,14 @@ impl Compiler { } _ => OpCode::Call(arguments.len()), }; - self.compile_expr(*function)?; + // A named callee is looked up as a call picks it, not as a + // value: the overloads the program picks from stay an + // overload set, which a call resolves from its frame. + if let Expression::Identifier { .. } = function.expression { + self.compile_binding(&self.binding(function.id), function_span)?; + } else { + self.compile_expr(*function)?; + } for argument in arguments { self.compile_expr(argument)?; diff --git a/ndc_vm/src/value/function.rs b/ndc_vm/src/value/function.rs index a6eea7c2..21919eb5 100644 --- a/ndc_vm/src/value/function.rs +++ b/ndc_vm/src/value/function.rs @@ -32,8 +32,23 @@ pub enum Function { /// every function value bigger. typ: Box, }, + + /// The overloads of a function, most specific first, of which each call + /// calls the first that accepts its arguments: an overloaded function's + /// name used as a value where the analyser couldn't tell which one is + /// meant, as `signum` in the body of `fn signs(xs) => xs.map(signum)` + /// compiled for arguments of any type. + Overloads(Rc<[Self]>), } +/// The type of a [`Function::Overloads`], which takes what one of its +/// overloads takes and returns what it returns. +static ANY_FUNCTION: std::sync::LazyLock = + std::sync::LazyLock::new(|| StaticType::Function { + parameters: None, + return_type: Box::new(StaticType::Any), + }); + /// How a function fits a function type, from the function's own type, /// which leaves out what isn't known as the program runs. #[derive(Debug, PartialEq, Eq)] @@ -208,7 +223,7 @@ impl Function { Self::Memoized { function, .. } | Self::Checked { function, .. } => { function.prototype() } - Self::Native(_) => None, + Self::Native(_) | Self::Overloads(_) => None, } } @@ -228,6 +243,7 @@ impl Function { Self::Native(f) => Some(&f.name), Self::Closure(c) => c.prototype.name.as_deref(), Self::Memoized { function, .. } | Self::Checked { function, .. } => function.name(), + Self::Overloads(candidates) => candidates.first().and_then(Self::name), } } @@ -244,6 +260,7 @@ impl Function { Self::Closure(c) => &c.prototype.static_type, Self::Memoized { function, .. } => function.signature(), Self::Checked { typ, .. } => typ, + Self::Overloads(_) => &ANY_FUNCTION, } } @@ -315,6 +332,11 @@ 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 { + if let Self::Overloads(candidates) = self { + return candidates + .iter() + .any(|candidate| candidate.conforms_value_args(args)); + } match self.signature() { StaticType::Function { parameters: Some(params), @@ -336,6 +358,11 @@ 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 { + if let Self::Overloads(candidates) = self { + return candidates + .iter() + .any(|candidate| candidate.matches_value_args(args)); + } let StaticType::Function { parameters, .. } = self.signature() else { return false; }; @@ -360,6 +387,7 @@ impl fmt::Debug for Function { Self::Closure(closure) => write!(f, "", closure.prototype.name), Self::Memoized { function, .. } => write!(f, "", function), Self::Checked { function, typ } => write!(f, "<{function:?} checked as {typ}>"), + Self::Overloads(candidates) => write!(f, ""), } } } @@ -375,6 +403,10 @@ impl fmt::Display for Function { Self::Closure(closure) => write!(f, "", closure.prototype.name), Self::Memoized { function, .. } => write!(f, ""), Self::Checked { function, .. } => write!(f, "{function}"), + Self::Overloads(candidates) => match candidates.first().and_then(Self::name) { + Some(name) => write!(f, ""), + None => write!(f, ""), + }, } } } @@ -444,7 +476,12 @@ fn could_be(found: &StaticType, expected: &StaticType) -> bool { /// 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)) + // A type parameter of a built-in's signature, as `U` in `map`'s + // `Fn(T) -> U`, is whatever the function gives it. + matches!( + expected, + StaticType::Any | StaticType::Generic(_) | StaticType::Var(_) + ) || (!has_unknowns(found) && found.fits(expected)) } /// Whether `typ` leaves something unknown: `Any` or a type parameter. diff --git a/ndc_vm/src/value/mod.rs b/ndc_vm/src/value/mod.rs index cadbf654..57089961 100644 --- a/ndc_vm/src/value/mod.rs +++ b/ndc_vm/src/value/mod.rs @@ -404,14 +404,9 @@ impl Value { fn is_a(&self, target: &StaticType) -> bool { if let StaticType::Function { .. } = target && let Self::Object(object) = self + && let Object::Function(function) = object.as_ref() { - match object.as_ref() { - Object::Function(function) => return function.fit(target) == FunctionFit::Proven, - // The overloads of a function the analyser couldn't pick - // from, of which the program picks one for each call. - Object::OverloadSet { .. } => return true, - _ => {} - } + return function.fit(target) == FunctionFit::Proven; } match (target, self.static_type()) { (StaticType::Iterator(_), StaticType::Iterator(_)) => true, @@ -1131,6 +1126,7 @@ impl PartialEq for Object { (Function::Memoized { cache: a, .. }, Function::Memoized { cache: b, .. }) => { Rc::ptr_eq(a, b) } + (Function::Overloads(a), Function::Overloads(b)) => Rc::ptr_eq(a, b), _ => false, } } @@ -1225,6 +1221,10 @@ impl Hash for Object { Rc::as_ptr(cache).hash(state); } Function::Checked { .. } => unreachable!("`unchecked` strips the checks"), + Function::Overloads(candidates) => { + state.write_u8(5); + Rc::as_ptr(candidates).cast::<()>().hash(state); + } } } Self::OverloadSet { .. } => { diff --git a/ndc_vm/src/vm.rs b/ndc_vm/src/vm.rs index 43a9a04f..f5675b50 100644 --- a/ndc_vm/src/vm.rs +++ b/ndc_vm/src/vm.rs @@ -426,6 +426,10 @@ impl Vm { let data = self.stack.split_off(self.stack.len() - size); self.stack.push(Value::Object(Rc::new(Object::Tuple(data)))); } + OpCode::MakeOverloads(size) => { + let size = *size; + self.make_overloads(size, span)?; + } OpCode::MakeMap { pairs, has_default } => { let (pairs, has_default) = (*pairs, *has_default); let default = if has_default { @@ -726,6 +730,62 @@ impl Vm { } } + /// Replaces the `size` functions on top of the stack with a + /// [`Function::Overloads`] of them. + #[cold] + fn make_overloads(&mut self, size: usize, span: Span) -> Result<(), VmError> { + let candidates = self + .stack + .split_off(self.stack.len() - size) + .into_iter() + .map(|candidate| match candidate { + Value::Object(object) => match object.as_ref() { + Object::Function(function) => Ok(function.clone()), + other => Err(other.static_type()), + }, + other => Err(other.static_type()), + }) + .collect::, _>>() + .map_err(|found| { + VmError::new(format!("Unable to invoke {found} as a function."), span) + })?; + self.stack + .push(Value::function(Function::Overloads(candidates))); + Ok(()) + } + + /// Calls the first of `candidates`, a [`Function::Overloads`], that + /// accepts the arguments on the stack. + #[cold] + fn call_overloads( + &mut self, + candidates: &[Function], + args: usize, + ) -> Result { + let arguments = &self.stack[self.stack.len() - args..]; + let picked = candidates + .iter() + .find(|candidate| candidate.matches_value_args(arguments)) + .or_else(|| { + candidates + .iter() + .find(|candidate| candidate.conforms_value_args(arguments)) + }) + .cloned(); + let Some(picked) = picked else { + let types = arguments + .iter() + .map(|argument| argument.static_type().to_string()) + .collect::>() + .join(", "); + let name = candidates.first().and_then(Function::name).unwrap_or("?"); + return Err(VmError::native(format!( + "no function called '{name}' found matches the arguments: ({types})" + ))); + }; + self.dispatch_call(picked, args) + } + /// Inner helper that pushes a new call frame. `memo` is `Some` when the /// call originates from a memoized function and the return value should be /// stored in the cache. @@ -765,6 +825,7 @@ impl Vm { upvalues: Vec::new(), }, Function::Checked { function, typ } => return self.call_checked(function, *typ, args), + Function::Overloads(candidates) => return self.call_overloads(&candidates, args), Function::Memoized { .. } => { // Already unwrapped in dispatch_call; this branch is unreachable. unreachable!( @@ -807,36 +868,6 @@ impl Vm { } } - /// Calls the overload of `overloads` that accepts `args`, inline on this - /// VM, as a callback from a native function. - fn call_overloads_back( - &mut self, - overloads: Rc, - args: &[Value], - ) -> Result { - let n = args.len(); - self.stack.push(Value::Object(overloads)); - self.stack.extend(args.iter().cloned()); - let picked = self.resolve_callee(n).map_err(VmError::native)?; - let Some(function) = picked else { - let types = args - .iter() - .map(|arg| arg.static_type().to_string()) - .collect::>() - .join(", "); - let name = self.callee_name(n).unwrap_or_else(|| "?".to_string()); - self.stack.truncate(self.stack.len() - n - 1); - return Err(VmError::native(format!( - "no function called '{name}' found matches the arguments: ({types})" - ))); - }; - let depth = self.frames.len(); - if self.dispatch_call(function, n)? == Dispatch::FramePushed { - self.run_to_depth(depth)?; - } - Ok(self.stack.pop().expect("callback must produce a value")) - } - /// Returns the expression result left on top of the stack after `run()`. /// If the last top-level expression was a statement (semicolon-terminated) /// its value was already popped, so the stack is exactly `num_locals` deep @@ -1235,30 +1266,15 @@ impl CallFrame { /// Calling `call()` runs the function inline on the parent VM via /// `Vm::call_callback` — no child VM is allocated. pub struct VmCallable<'a> { - pub function: Callback, + pub function: Function, pub vm: &'a mut Vm, } -/// What a native function calls back. -pub enum Callback { - /// A function. - Function(Function), - /// The overloads of a function, one of which the program picks for each - /// call's arguments: an overloaded function's name used as a value where - /// the analyser couldn't tell which one is meant. - Overloads(Rc), -} - impl VmCallable<'_> { /// Call this function with the given arguments, running inline on the /// parent VM. pub fn call(&mut self, args: &[Value]) -> Result { - match &self.function { - Callback::Function(function) => self.vm.call_callback(function.clone(), args), - Callback::Overloads(overloads) => { - self.vm.call_overloads_back(Rc::clone(overloads), args) - } - } + self.vm.call_callback(self.function.clone(), args) } } diff --git a/tests/functional/programs/005_functions/102_a_function_value_left_to_the_program_keeps_its_local_overloads.ndc b/tests/functional/programs/005_functions/102_a_function_value_left_to_the_program_keeps_its_local_overloads.ndc new file mode 100644 index 00000000..826db386 --- /dev/null +++ b/tests/functional/programs/005_functions/102_a_function_value_left_to_the_program_keeps_its_local_overloads.ndc @@ -0,0 +1,12 @@ +// A function value whose overload the program picks holds the overloads it +// was made from, so it works where it's called, outside their scope. +// expect-output: int +fn make(x) { + fn choose(y: Int) => "int"; + fn choose(y: Float) => "float"; + let g = choose; + fn() => g(x) +} +let m: Fn(Int) -> Fn() -> String = make; +let h = m(1); +print(h()); diff --git a/tests/functional/programs/005_functions/103_a_function_value_left_to_the_program_is_checked_as_a_function_type.ndc b/tests/functional/programs/005_functions/103_a_function_value_left_to_the_program_is_checked_as_a_function_type.ndc new file mode 100644 index 00000000..cb958cbc --- /dev/null +++ b/tests/functional/programs/005_functions/103_a_function_value_left_to_the_program_is_checked_as_a_function_type.ndc @@ -0,0 +1,12 @@ +// A function value whose overload the program picks, used as a function +// type through `Any`, gets its calls checked like any function. +// expect-error: expected the function to return String but it returned Int +fn bad(x) { + let g = abs; + g(x); + let a: Any = g; + let b: Fn(Int) -> String = a; + b(1) +} +let h: Fn(Int) -> String = bad; +print(h(1)); diff --git a/tests/functional/programs/005_functions/104_a_function_value_left_to_the_program_can_be_a_pure_function_argument.ndc b/tests/functional/programs/005_functions/104_a_function_value_left_to_the_program_can_be_a_pure_function_argument.ndc new file mode 100644 index 00000000..8884fc78 --- /dev/null +++ b/tests/functional/programs/005_functions/104_a_function_value_left_to_the_program_can_be_a_pure_function_argument.ndc @@ -0,0 +1,7 @@ +// A `pure fn` caches by its arguments, which can be a function value whose +// overload the program picks. +// expect-output: 1 +pure fn invoke(g, x) => g(x); +fn apply(x) => invoke(abs, x); +let h: Fn(Int) -> Int = apply; +print(h(1));