Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions manual/src/reference/types/function.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
52 changes: 45 additions & 7 deletions ndc_analyser/src/analyser/function_values.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -35,13 +35,27 @@ 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
}

pub(super) fn mark_in_literal(&mut self) {
self.in_literal = true;
}

/// The functions the name can mean, grouped by scope, nearest first.
pub(super) fn groups(&self) -> &[Vec<SymbolId>] {
&self.groups
}
}

/// Which overload a function value means, given what's known of the type
Expand Down Expand Up @@ -250,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<SymbolId> {
Binding::Dynamic(
value
Comment on lines +272 to +273

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Support hashing deferred function values before memoized calls

When a shared generic body passes a deferred overload value to a pure fn, memoization hashes that value before executing the callback, but Object::hash still panics for every OverloadSet. For example, pure fn invoke(g, x) => g(x); fn apply(x) => invoke(abs, x); let h: Fn(Int) -> Int = apply; print(h(1)); now passes analysis but reaches the Rust panic OverloadSet cannot be used as a map key in Vm::dispatch_call, rather than producing 1. Direct specialized calls avoid this representation, so they do not cover the failure. Give deferred function values a supported hash/identity representation, or resolve them before the memoization cache hashes arguments.

Useful? React with 👍 / 👎.

.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 {
Expand Down Expand Up @@ -325,9 +354,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
Expand All @@ -336,8 +376,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);
}
}
76 changes: 50 additions & 26 deletions ndc_analyser/src/analyser/generics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,18 @@ impl Analyser {
})
.collect::<Vec<_>>();
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() => {
let binding = self.every_overload(value);
self.picks.bindings.insert(value.id(), binding);
Comment on lines +136 to +138

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Support deferred overload sets in native callback arguments

When a generic function is used as a value, its shared body now passes an Object::OverloadSet wherever an overloaded callback remains deferred, but native higher-order functions cannot consume that representation: NdcType::MutVmCallable in ndc_macros/src/vm_convert.rs accepts only Object::Function. For example, fn signs(xs) => xs.map(signum); let f: Fn(List<Int>) -> List<Int> = signs; print(f([1, -2])); passes analysis but fails at runtime instead of returning [1,-1]. Direct calls use specialized bodies, so the added signs test misses this path. Make deferred function values usable by native callbacks before storing them as dynamic bindings here.

Useful? React with 👍 / 👎.

Comment on lines +136 to +138

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Capture local overload candidates when creating function values

When a deferred function value contains locally declared overloads and escapes into another function, the emitted OverloadSet retains frame-relative variable locations rather than the functions themselves. For example, 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()); uses the shared generic body and captures this set in h. However, find_scalar_overload resolves its candidates relative to the currently executing closure's frame, not make's frame, so it reads unrelated slots or runs out of bounds. Materialize or capture the candidate functions when constructing an escaping overload value.

Useful? React with 👍 / 👎.

}
_ => {}
}
}
// A value of type `Any` that the body passes where a type parameter
Expand Down Expand Up @@ -209,15 +217,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<TypeVar> {
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::<Vec<_>>();
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
Expand All @@ -232,8 +260,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<TypeVar>)> {
let serials = self.generic_constraints();
let mut decided = parameters
Expand All @@ -245,11 +273,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;
}
Expand All @@ -265,14 +292,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::<Vec<_>>();
(!ambiguous.is_empty()).then_some((serial, ambiguous))
})
.collect()
Expand Down
8 changes: 6 additions & 2 deletions ndc_analyser/src/analyser/solver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1776,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,
}
})
Expand Down
16 changes: 9 additions & 7 deletions ndc_analyser/src/analyser/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion ndc_analyser/src/overloads.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SymbolId>) -> Vec<SymbolId> {
pub(crate) fn most_specific_first(self, mut symbols: Vec<SymbolId>) -> Vec<SymbolId> {
let mut ordered = Vec::with_capacity(symbols.len());
while !symbols.is_empty() {
let next = symbols
Expand Down
12 changes: 12 additions & 0 deletions ndc_analyser/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>` is the sequence it holds,
// as `["a"]` passed to `len` is a `List<String>`: 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;
Comment on lines +827 to +828

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Defer iterable validation when a literal has no default yet

When the argument's literal variable holds another unsettled literal, literal_defaults returns None, so this branch accepts the argument without establishing any relationship to the iterable parameter. For example, in len([1][0]), the indexed element initially holds the unsettled numeric literal and is accepted here, even though it subsequently defaults to Int. iterations_waited_for in solver.rs explicitly excludes literal variables, so no deferred iteration constraint validates it later; debug builds instead reach the early-overload consistency assertion, while release builds leave the invalid call to fail at runtime. Keep the iterable requirement pending until the held literal's type is known rather than returning unconditional success.

Useful? React with 👍 / 👎.

};
self.settle_literal(*var, &held) && self.bind_argument(&parameter, &held)
}
(StaticType::Var(_), _) | (_, StaticType::Var(_)) => self.unify(&parameter, &argument),
(_, StaticType::Any) => true,
(
Expand Down
84 changes: 73 additions & 11 deletions ndc_stdlib/src/sequence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -997,22 +997,84 @@ pub mod extra {
use std::rc::Rc;

pub fn register(env: &mut FunctionRegistry<Rc<NativeFunction>>) {
for signature in [
// Two sequences zip into pairs of their elements, three into
// triples, and four into quadruples.
"<A, B>(Iterable<A>, Iterable<B>) -> List<(A, B)>",
"<A, B, C>(Iterable<A>, Iterable<B>, Iterable<C>) -> List<(A, B, C)>",
"<A, B, C, D>(Iterable<A>, Iterable<B>, Iterable<C>, Iterable<D>) -> List<(A, B, C, D)>",
// More make tuples as long as the number of arguments.
"(*) -> List<Any>",
] {
// A tuple's elements have their own types, and a signature can't say
// "a tuple of any length", so each length up to twelve, as far as
// Rust's standard library goes for tuples, gets one that says which.
const LONGEST: usize = 12;
let names = |count: usize| {
["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"][..count].to_vec()
};
for length in 1..=LONGEST {
let names = names(length);
let parameters = names.join(", ");
let tuple = if length == 1 {
"(A,)".to_string()
} else {
format!("({parameters})")
};
for (name, returns, documentation) in [
("first", names[0], "Returns the first element of a tuple."),
(
"last",
names[length - 1],
"Returns the last element of a tuple.",
),
] {
env.declare_global_fn(Rc::new(NativeFunction {
name: name.to_string(),
documentation: Some(documentation.to_string()),
static_type: StaticType::signature(&format!(
"<{parameters}>({tuple}) -> {returns}"
))
.expect("valid signature"),
func: NativeFunc::Simple(Box::new(move |args| {
tuple_end(args, name == "first")
})),
}));
}
}
// Two or more sequences zip into tuples of their elements.
for length in 2..=LONGEST {
let names = names(length);
let sequences = names
.iter()
.map(|name| format!("Iterable<{name}>"))
.collect::<Vec<_>>()
.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<Any>").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<Value, VmError> {
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<Value, VmError> {
Expand Down
Loading
Loading