From a050c91902261f2784a51187ad8b70bb15101248 Mon Sep 17 00:00:00 2001 From: Tim Fennis Date: Fri, 13 Nov 2026 17:19:40 +0100 Subject: [PATCH 1/2] =?UTF-8?q?refactor(analyser):=20type=20the=20literals?= =?UTF-8?q?=20in=20a=20generic=20body=20by=20the=20body=20alone=20?= =?UTF-8?q?=E2=9C=82=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- manual/src/features/augmented-assignment.md | 2 +- manual/src/reference/types/function.md | 15 +- ndc_analyser/src/analyser/declarations.rs | 8 +- ndc_analyser/src/analyser/function_values.rs | 4 +- ndc_analyser/src/analyser/generics.rs | 171 +++--------------- ndc_analyser/src/analyser/instances.rs | 35 +--- ndc_analyser/src/analyser/mod.rs | 11 +- ndc_analyser/src/analyser/solver.rs | 29 +-- ndc_analyser/src/types.rs | 158 +--------------- ndc_analysis/src/lib.rs | 5 - ndc_layout/src/lib.rs | 5 - ndc_vm/src/compiler.rs | 5 +- ...in_a_generic_body_is_typed_by_the_body.ndc | 7 + ...n_a_generic_body_takes_the_call_s_type.ndc | 6 - ...ter_and_a_literal_is_typed_by_the_body.ndc | 5 + ...ding_a_parameter_takes_the_call_s_type.ndc | 7 - ...c_function_as_a_value_has_int_literals.ndc | 5 - ...tion_makes_a_list_of_what_it_is_given.ndc} | 0 ...ll_through_another_overload_can_t_grow.ndc | 4 +- ...st_of_a_parameter_is_typed_by_the_body.ndc | 7 + ...n_takes_what_its_body_s_literals_allow.ndc | 8 + 21 files changed, 79 insertions(+), 418 deletions(-) create mode 100644 tests/functional/programs/005_functions/087_a_literal_in_a_generic_body_is_typed_by_the_body.ndc delete mode 100644 tests/functional/programs/005_functions/087_a_literal_in_a_generic_body_takes_the_call_s_type.ndc create mode 100644 tests/functional/programs/005_functions/088_a_list_of_a_parameter_and_a_literal_is_typed_by_the_body.ndc delete mode 100644 tests/functional/programs/005_functions/088_a_literal_holding_a_parameter_takes_the_call_s_type.ndc delete mode 100644 tests/functional/programs/005_functions/089_a_generic_function_as_a_value_has_int_literals.ndc rename tests/functional/programs/005_functions/{090_copies_of_a_literal_keep_what_each_holds.ndc => 090_each_call_of_a_generic_function_makes_a_list_of_what_it_is_given.ndc} (100%) create mode 100644 tests/functional/programs/005_functions/097_a_list_of_a_parameter_is_typed_by_the_body.ndc create mode 100644 tests/functional/programs/005_functions/098_a_generic_function_takes_what_its_body_s_literals_allow.ndc diff --git a/manual/src/features/augmented-assignment.md b/manual/src/features/augmented-assignment.md index 16c85646..c9d269eb 100644 --- a/manual/src/features/augmented-assignment.md +++ b/manual/src/features/augmented-assignment.md @@ -132,7 +132,7 @@ print(x); // 8 One common use case is tracking the highest or lowest value in a loop: ```ndc -fn g(x) => (x - 40) ^ 2; +fn g(x) => (x - 40.0) ^ 2; let lowest, highest = Inf, -Inf; for x in 1..100 { diff --git a/manual/src/reference/types/function.md b/manual/src/reference/types/function.md index 9e3fdf07..c2e8004b 100644 --- a/manual/src/reference/types/function.md +++ b/manual/src/reference/types/function.md @@ -242,13 +242,18 @@ print(a, b, i); // 2 3.5 55 The body is compiled for each set of types it's called with, so the calls in it are picked when the program is compiled, not as it runs. Called with a value of type `Any`, the function picks them as it runs. -A number literal in the body that meets a parameter takes its type from each call, as a literal written at the call would: +A number literal in the body is typed by the body alone, as one anywhere else is, and never by a call: the `1` in `inc` is an `Int` whatever `inc` is given. So a literal that a result is made of fixes that result's type. `fact` returns the `1` or a product, which can only be one type, an `Int`, so it doesn't take a `Float`: -```ndc +```ndc,fails fn fact(n) => if n < 2 { 1 } else { n * fact(n - 1) }; -fn pair(x) => [x, 1]; -print(fact(5), fact(5.0)); // 120 120.0 -print(pair(2), pair(2.5)); // [2,1] [2.5,1.0] +let r = fact(5.0); // error: mismatched types: found Float but expected Int +``` + +Write `1.0` for a `fact` of `Float`s, which works for `Int`s too, as a `Float` times an `Int` is a `Float`: + +```ndc +fn fact(n) => if n < 2 { 1.0 } else { n * fact(n - 1) }; +print(fact(5), fact(5.0)); // 120.0 120.0 ``` A call with an argument the body can't use is an error at the call, with a note on the line in the body that can't take it: diff --git a/ndc_analyser/src/analyser/declarations.rs b/ndc_analyser/src/analyser/declarations.rs index f37144b6..295c3a24 100644 --- a/ndc_analyser/src/analyser/declarations.rs +++ b/ndc_analyser/src/analyser/declarations.rs @@ -49,7 +49,6 @@ impl Analyser { self.declaring = declared.map(|(_, symbol)| symbol); } let first_use = self.uses.len(); - let first_literal = self.number_literals.len(); let first_check = self.checks.len(); let annotations = parameters .iter() @@ -165,12 +164,7 @@ impl Analyser { self.vars.leave_level(); self.declaring = None; self.declared_in_batch.insert(symbol); - let typ = self.generalize( - symbol, - &signature, - id, - [first_use, first_literal, first_check], - ); + let typ = self.generalize(symbol, &signature, id, [first_use, first_check]); self.within_size_limit(symbol, name, typ, span) } _ => signature, diff --git a/ndc_analyser/src/analyser/function_values.rs b/ndc_analyser/src/analyser/function_values.rs index f28e3084..5fede382 100644 --- a/ndc_analyser/src/analyser/function_values.rs +++ b/ndc_analyser/src/analyser/function_values.rs @@ -89,7 +89,7 @@ impl Analyser { .insert(id, Binding::Resolved(Candidate::Scalar(symbol))); // A generic function used as a value, as in `let p = push`, has one // type there, which its uses settle. - let typ = self.instantiate_function(symbol, false); + let typ = self.instantiate_function(symbol); self.flush_copies(None, span); typ } @@ -319,7 +319,7 @@ impl Analyser { } fn commit_function_value(&mut self, value: &FunctionValue, symbol: SymbolId, early: bool) { - let typ = self.instantiate_function(symbol, false); + let typ = self.instantiate_function(symbol); self.vars.bind_argument(&value.expected, &typ); self.flush_copies(None, value.span); if early && cfg!(debug_assertions) { diff --git a/ndc_analyser/src/analyser/generics.rs b/ndc_analyser/src/analyser/generics.rs index 577f3a63..5782a8fd 100644 --- a/ndc_analyser/src/analyser/generics.rs +++ b/ndc_analyser/src/analyser/generics.rs @@ -10,8 +10,8 @@ use super::instances::Scheme; use super::solver::{CallConstraint, Constraint, Waiting}; use crate::error::AnalysisError; use crate::overloads::is_strictly_more_specific; -use crate::types::{LiteralBounds, TypeVars}; -use ahash::{AHashMap, AHashSet}; +use crate::types::TypeVars; +use ahash::AHashSet; use ndc_analysis::{Binding, Candidate}; use ndc_core::{NodeId, StaticType, TypeVar}; use ndc_lexer::Span; @@ -29,24 +29,22 @@ impl Analyser { /// `signature`, and returns its type with its type parameters: each /// variable its body made that nothing outside it refers to becomes /// one, and the constraints still waiting on them become the function's. - /// A variable those constraints wait on but that the parameters don't - /// decide, like the element type of `[]` in `fn f(x) { [].push(x) }`, - /// can't be decided by a call either, so it's settled here first, as at - /// the end of the program. + /// The body's literals are typed by the body alone, as at the end of + /// the program: the `1` in `fn inc(x) => x + 1` is an Int, whatever + /// `x` is. A variable the constraints wait on but that the parameters + /// don't decide, like the element type of `[]` in `fn f(x) { + /// [].push(x) }`, can't be decided by a call either, so it's settled + /// here too. pub(super) fn generalize( &mut self, symbol: SymbolId, signature: &StaticType, declaration: NodeId, - [first_use, first_literal, first_check]: [usize; 3], + [first_use, first_check]: [usize; 2], ) -> StaticType { self.solve_waiting(); - // A literal in the body that its type parameters don't reach has - // one type, which a call can't change. The others are type - // parameters, which each call settles. - let tied = self.literals_tied_to_parameters(signature); - self.default_literals(|vars, var| vars.is_generalizable(var) && !tied.contains(&var)); - self.settle_ambiguous(signature, &tied); + self.default_literals(TypeVars::is_generalizable); + self.settle_ambiguous(signature); self.merge_copies(); let scheme = self @@ -92,68 +90,12 @@ impl Analyser { } } let mut named = 0; - let mut literals: Vec<(Box, LiteralBounds)> = Vec::new(); - let mut seen = parameters.iter().copied().collect::>(); - // The integer literals one literal holds have one type, `[x, 1, 2]` - // holds Ints or Floats, so they're one type parameter, named for - // the first of them. - let mut first_number = AHashMap::::new(); - let mut names = AHashMap::>::new(); - let mut next = 0; - while let Some(&var) = parameters.get(next) { - next += 1; - // What a literal holds is a type parameter too: the `1` in - // `fn g(x) => [x, 1]` is a Float when `x` is. - let mut first = None; - for held in self.vars.held_vars(var) { - if seen.insert(held) { - parameters.push(held); - } - if self.vars.is_literal(held) && self.vars.is_number(held) { - match first { - None => first = Some(held), - Some(first) if first != held => { - first_number.entry(held).or_insert(first); - } - Some(_) => {} - } - } - } - if !self.vars.is_generalizable(var) { - continue; + for var in parameters { + if self.vars.is_generalizable(var) { + self.vars.generalize(var, &type_parameter_name(named)); + named += 1; } - if let Some(name) = first_number.get(&var).and_then(|first| names.get(first)) { - let name = name.clone(); - if let Some(bounds) = self.vars.generalize(var, &name) - && let Some((_, first_bounds)) = - literals.iter_mut().find(|(literal, _)| *literal == name) - { - first_bounds.extend(bounds); - } - continue; - } - let name: Box = type_parameter_name(named).into(); - if let Some(bounds) = self.vars.generalize(var, &name) { - literals.push((name.clone(), bounds)); - } - names.insert(var, name); - named += 1; } - let literals = literals - .into_iter() - .map(|(name, bounds)| (name, self.vars.resolve_bounds(&bounds))) - .collect::>(); - // A literal that's tied to a type parameter but that nothing outside - // the body sees, like the list in `fn f(x) { let xs = [x]; 0 }`, - // takes its default. - self.default_literals(TypeVars::is_generalizable); - let literal_nodes = self - .number_literals - .split_off(first_literal) - .into_iter() - .map(|(node, typ)| (node, self.vars.resolve(&typ))) - .filter(|(_, typ)| matches!(typ, StaticType::Generic(_))) - .collect::>(); // A type only the body makes, as of a list nothing else uses, gets // its fallback. @@ -206,13 +148,12 @@ impl Analyser { .collect::>(); // An instance is for the types of the type parameters that what it // compiles differently depends on: the constraints and uses it - // picks for, the literals typed by a type parameter, and the checks. + // picks for, and the checks. let mut key: Vec> = Vec::new(); for typ in scheme .iter() .flat_map(|waiting| waiting.constraint.inputs()) .chain(uses.iter().flat_map(|generic_use| &generic_use.key)) - .chain(literal_nodes.iter().map(|(_, typ)| typ)) .chain(checks.iter().map(|(_, _, expected)| expected)) { for name in typ.type_parameters() { @@ -229,77 +170,12 @@ impl Analyser { uses: uses.into(), declaration, recursive_calls: std::mem::take(&mut self.recursive_calls).into(), - literals: literals.into(), - literal_nodes: literal_nodes.into(), checks: checks.into(), }, ); self.vars.resolve(signature) } - /// The literals in the body of the generic function whose type is - /// `signature` that its parameters' types reach, through a waiting - /// constraint or what a literal holds: the `1` in `fn fact(n) => if n < - /// 2 { 1 } else { n * fact(n - 1) }` is what `fact` returns, like `n * - /// fact(n - 1)`, and the list in `fn k(x) => [x, "b"]` holds `x`. - fn literals_tied_to_parameters(&self, signature: &StaticType) -> AHashSet { - let open = |var: &TypeVar| self.vars.is_generalizable(*var); - let literals = self - .vars - .unsettled_literals(TypeVars::is_generalizable) - .into_iter() - .collect::>(); - // The variables that are the same or decide each other: those of one - // constraint, and a literal with what it holds. - let mut groups = self - .generic_constraints() - .into_iter() - .map(|serial| { - let constraint = &self.waiting[&serial].constraint; - constraint - .inputs() - .iter() - .chain(constraint.outputs()) - .flat_map(|typ| self.vars.unsettled(typ)) - .filter(open) - .collect::>() - }) - .collect::>(); - for &literal in &literals { - let mut group = self.vars.held_vars(literal); - group.retain(open); - group.push(literal); - groups.push(group); - } - let parameters = match signature { - StaticType::Function { - parameters: Some(parameters), - .. - } => parameters.as_slice(), - _ => &[], - }; - let mut reached = parameters - .iter() - .flat_map(|typ| self.vars.unsettled(typ)) - .filter(open) - .collect::>(); - loop { - let mut changed = false; - for group in &groups { - if group.iter().any(|var| reached.contains(var)) { - for &var in group { - changed |= reached.insert(var); - } - } - } - if !changed { - break; - } - } - reached.retain(|var| literals.contains(var)); - reached - } - /// The waiting constraints that wait on or settle a variable of the /// generic function whose body just ended, oldest first. fn generic_constraints(&self) -> Vec { @@ -358,17 +234,12 @@ impl Analyser { /// 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. - fn ambiguous_vars( - &self, - parameters: &[StaticType], - tied: &AHashSet, - ) -> Vec<(u64, Vec)> { + fn ambiguous_vars(&self, parameters: &[StaticType]) -> Vec<(u64, Vec)> { let serials = self.generic_constraints(); let mut decided = parameters .iter() .flat_map(|typ| self.vars.unsettled(typ)) .filter(|&var| self.vars.is_generalizable(var)) - .chain(tied.iter().copied()) .collect::>(); loop { let mut changed = false; @@ -411,7 +282,7 @@ impl Analyser { /// that its parameters don't decide, like the end of the program /// settles its own: a call that a default rule decides is decided by it, /// and what's left gets its fallbacks, oldest constraint first. - fn settle_ambiguous(&mut self, signature: &StaticType, tied: &AHashSet) { + fn settle_ambiguous(&mut self, signature: &StaticType) { let parameters = match signature { StaticType::Function { parameters: Some(parameters), @@ -420,7 +291,7 @@ impl Analyser { _ => Vec::new(), }; loop { - let ambiguous = self.ambiguous_vars(¶meters, tied); + let ambiguous = self.ambiguous_vars(¶meters); let Some((_, vars)) = ambiguous.first().cloned() else { return; }; @@ -475,8 +346,8 @@ impl Analyser { fn merge_copies(&mut self) { let serials = self.generic_constraints(); let mut kept: Vec = Vec::new(); - // A call's arguments and result, as one type, so copies of one - // literal in them become one, as they're settled the same way. + // A call's arguments and result, as one type, so the two calls' + // results become one. let call_types = |call: &CallConstraint| { StaticType::Tuple( call.arguments() diff --git a/ndc_analyser/src/analyser/instances.rs b/ndc_analyser/src/analyser/instances.rs index ffc46f32..2035d790 100644 --- a/ndc_analyser/src/analyser/instances.rs +++ b/ndc_analyser/src/analyser/instances.rs @@ -9,7 +9,6 @@ use super::Analyser; use super::solver::{CallSite, Constraint, Origin, Waiting}; use crate::error::AnalysisError; -use crate::types::LiteralBounds; use ahash::AHashMap; use ndc_analysis::{Binding, Candidate}; use ndc_core::{NodeId, StaticType}; @@ -34,15 +33,6 @@ pub(super) struct Scheme { pub(super) declaration: NodeId, /// The calls its body makes of itself, which have its own types. pub(super) recursive_calls: Rc<[NodeId]>, - /// Its type parameters that are the types of literals in its body, - /// with their bounds, which each call settles as a literal there would - /// be: the `1` in `fn fact(n) => if n < 2 { 1 } else { n * fact(n - 1) - /// }` is a Float for `fact(5.0)`. - pub(super) literals: Rc<[(Box, LiteralBounds)]>, - /// The integer literals in its body whose type is one of its type - /// parameters, with that type, which an instance records its own types - /// for. - pub(super) literal_nodes: Rc<[(NodeId, StaticType)]>, /// The values its body checks as the program runs against a type that /// has its type parameters in it, with their types and that type, which /// an instance checks against what its types make it. @@ -237,23 +227,14 @@ impl Analyser { /// The instance of `function` for these types of its key: the function /// itself when they're all `Any`, as its own body picks its calls as the /// program runs, or when it was declared by an earlier batch, which - /// compiled it already. Its own body's integer literals are Ints, so an - /// integer literal's type parameter may be one. + /// compiled it already. fn instance_for( &mut self, function: SymbolId, key: Vec, new: &mut Vec<(SymbolId, SymbolId, Vec)>, ) -> SymbolId { - let scheme = &self.schemes[&function]; - let is_own = scheme.key.iter().zip(&key).all(|(name, typ)| { - *typ == StaticType::Any - || (*typ == StaticType::Int - && scheme - .literals - .iter() - .any(|(literal, bounds)| literal == name && bounds.is_number())) - }); + let is_own = key.iter().all(|typ| *typ == StaticType::Any); if is_own || !self.declared_in_batch.contains(&function) { return function; } @@ -292,7 +273,6 @@ impl Analyser { .collect::>(); let outer = self.recording.replace(instance); let first = self.vars.count(); - self.vars.instantiate_literals(&scheme.literals, &mut made); for waiting in scheme.constraints.iter() { let mut copy = Waiting { constraint: waiting @@ -330,17 +310,6 @@ impl Analyser { }); } self.settle_waiting(first); - let literal_types = scheme - .literal_nodes - .iter() - .map(|(node, typ)| { - let typ = self.vars.instantiate_with(typ, &mut made); - (*node, self.vars.resolve(&typ)) - }) - .collect::>(); - if let Some(picks) = self.picks.instances.get_mut(&instance) { - picks.literal_types.extend(literal_types); - } self.recording = outer; } } diff --git a/ndc_analyser/src/analyser/mod.rs b/ndc_analyser/src/analyser/mod.rs index ddb59a70..b708765c 100644 --- a/ndc_analyser/src/analyser/mod.rs +++ b/ndc_analyser/src/analyser/mod.rs @@ -75,9 +75,6 @@ pub struct Analyser { declaring: Option, /// The calls its body makes of itself. recursive_calls: Vec, - /// The integer literals in the batch, with their types, which a - /// generic function's instances may give types of their own. - number_literals: Vec<(NodeId, StaticType)>, /// The nodes of overloaded function values that waited for the type /// expected of them, as `abs` in `[len, abs]` does. waiting_function_values: AHashSet, @@ -162,7 +159,6 @@ impl Analyser { declared_in_batch: AHashSet::new(), declaring: None, recursive_calls: Vec::new(), - number_literals: Vec::new(), waiting_function_values: AHashSet::new(), expected_parameters: AHashMap::new(), copied_through: None, @@ -198,7 +194,6 @@ impl Analyser { self.check_early_picks(); } self.failed_reads.clear(); - self.number_literals.clear(); self.waiting_function_values.clear(); self.types.resolve_all(&self.vars); for (instance, node, typ, expected) in std::mem::take(&mut self.checks) { @@ -401,11 +396,7 @@ impl Analyser { StaticType::Option(Box::new(self.vars.fresh(StaticType::Any))) } Expression::StringLiteral(_) => StaticType::String, - Expression::NumericLiteral(NumericLiteral::Int64(_)) => { - let typ = self.vars.fresh_number(*span); - self.number_literals.push((*id, typ.clone())); - typ - } + Expression::NumericLiteral(NumericLiteral::Int64(_)) => self.vars.fresh_number(*span), Expression::NumericLiteral(NumericLiteral::Float64(_)) => StaticType::Float, Expression::NumericLiteral( NumericLiteral::NumberInt(_) diff --git a/ndc_analyser/src/analyser/solver.rs b/ndc_analyser/src/analyser/solver.rs index d339669b..fa6d63b5 100644 --- a/ndc_analyser/src/analyser/solver.rs +++ b/ndc_analyser/src/analyser/solver.rs @@ -244,9 +244,7 @@ impl CallConstraint { } /// Whether `other` calls what this calls, the same way, with arguments - /// of the same types, so it returns the same type. Copies of one - /// literal count as the same type, as the same call settles them the - /// same way. + /// of the same types, so it returns the same type. pub(super) fn is_same_call(&self, other: &Self, vars: &TypeVars) -> bool { std::mem::discriminant(&self.site) == std::mem::discriminant(&other.site) && self.kind == other.kind @@ -257,7 +255,7 @@ impl CallConstraint { .arguments .iter() .zip(&other.arguments) - .all(|(a, b)| vars.with_literals_named(a) == vars.with_literals_named(b)) + .all(|(a, b)| vars.resolve(a) == vars.resolve(b)) } } @@ -1613,7 +1611,7 @@ impl Analyser { Binding::Dynamic(_) | Binding::None => None, }; let pending = self.pending.len(); - let signature = picked.map(|symbol| self.instantiate_function(symbol, true)); + let signature = picked.map(|symbol| self.instantiate_function(symbol)); let Some( signature @ StaticType::Function { parameters: Some(_), @@ -1640,27 +1638,10 @@ impl Analyser { /// `symbol`'s type with new variables for its type parameters. For a /// generic function declared in the program, its constraints get the /// same variables, and wait in [`Self::pending`] until - /// [`Self::flush_copies`] knows which call they're for. `call` says - /// whether it's for a call, which may get an instance of its own. - pub(super) fn instantiate_function(&mut self, symbol: SymbolId, call: bool) -> StaticType { + /// [`Self::flush_copies`] knows which call they're for. + pub(super) fn instantiate_function(&mut self, symbol: SymbolId) -> StaticType { let declared = self.vars.resolve(self.types.get_type(symbol)); let mut made = Vec::new(); - if let Some(scheme) = self.schemes.get(&symbol) { - let literals = Rc::clone(&scheme.literals); - // Only a call in the batch that declares the function gets an - // instance. A value, as in `let f = pair`, or a call of a - // function an earlier batch declared, runs the function's own - // body, whose integer literals are Ints. - if !call || !self.declared_in_batch.contains(&symbol) { - made.extend( - literals - .iter() - .filter(|(_, bounds)| bounds.is_number()) - .map(|(name, _)| (name.clone(), StaticType::Int)), - ); - } - self.vars.instantiate_literals(&literals, &mut made); - } let typ = self.vars.instantiate_with(&declared, &mut made); // A copy of a function's own constraint that calls the function // again, as a recursive call its body couldn't decide does, is diff --git a/ndc_analyser/src/types.rs b/ndc_analyser/src/types.rs index f283faa0..19fdebae 100644 --- a/ndc_analyser/src/types.rs +++ b/ndc_analyser/src/types.rs @@ -84,52 +84,6 @@ enum Bound { Holds(StaticType, TypeVar, Span), } -/// The bounds of a literal's type that became a type parameter of a generic -/// function, which each call's copy of the parameter gets: the `1` in -/// `fn fact(n) => if n < 2 { 1 } else { n * fact(n - 1) }` is an Int or a -/// Float, whichever the call needs. -#[derive(Debug, Clone)] -pub(crate) struct LiteralBounds(Vec); - -impl Bound { - /// The type of the value it holds, if it holds one. - fn held(&self) -> Option<&StaticType> { - match self { - Self::Holds(held, _, _) => Some(held), - Self::Number(..) => None, - } - } - - /// Which bound of which literal it is: the value's kind and where it is - /// in the source. - fn position(&self) -> (bool, usize, usize, usize) { - let (is_number, span) = match self { - Self::Number(_, span) => (true, span), - Self::Holds(_, _, span) => (false, span), - }; - ( - is_number, - span.source_id().index(), - span.offset(), - span.end(), - ) - } -} - -impl LiteralBounds { - /// Adds the bounds of another literal of the same type. - pub(crate) fn extend(&mut self, other: Self) { - self.0.extend(other.0); - } - - /// Whether they're an integer literal's. - pub(crate) fn is_number(&self) -> bool { - self.0 - .iter() - .any(|bound| matches!(bound, Bound::Number(..))) - } -} - /// The state of the variables at one point, which [`TypeVars::rollback`] /// returns to: overload resolution tries each candidate this way. #[derive(Debug, Clone, Copy)] @@ -499,93 +453,13 @@ impl TypeVars { } /// Settles `var` as the type parameter `name` of the generic function - /// whose body made it, and returns its bounds if it's a literal's type, - /// which the parameter keeps. - pub(crate) fn generalize(&mut self, var: TypeVar, name: &str) -> Option { - let bounds = std::mem::take(&mut self.bounds[var.0 as usize]); + /// whose body made it. + pub(crate) fn generalize(&mut self, var: TypeVar, name: &str) { + debug_assert!( + !self.is_literal(var), + "a literal's type is a type parameter" + ); self.settle(var, StaticType::Generic(name.into())); - (!bounds.is_empty()).then_some(LiteralBounds(bounds)) - } - - /// `bounds` with the types the values held have now, which are type - /// parameters once the generic function's body has ended. - pub(crate) fn resolve_bounds(&self, bounds: &LiteralBounds) -> LiteralBounds { - LiteralBounds( - bounds - .0 - .iter() - .map(|bound| match bound { - Bound::Holds(held, literal, span) => { - Bound::Holds(self.resolve(held), *literal, *span) - } - number @ Bound::Number(..) => number.clone(), - }) - .collect(), - ) - } - - /// Adds a new literal type to `made` for each of a generic function's - /// literal type parameters `literals` it doesn't have yet, with the - /// parameter's bounds, so each call settles its own: `fact(5.0)` makes - /// the `1` in `fact` a Float. - pub(crate) fn instantiate_literals( - &mut self, - literals: &[(Box, LiteralBounds)], - made: &mut Vec<(Box, StaticType)>, - ) { - let mut new = Vec::new(); - for (name, bounds) in literals { - if made.iter().any(|(generic, _)| generic == name) { - continue; - } - let typ = self.fresh(if bounds.is_number() { - StaticType::Int - } else { - StaticType::Any - }); - let StaticType::Var(var) = typ else { - unreachable!("a fresh type is a variable"); - }; - made.push((name.clone(), typ)); - new.push((var, bounds)); - } - for (var, bounds) in new { - let bounds = bounds - .0 - .iter() - .map(|bound| match bound { - Bound::Number(_, span) => Bound::Number(var, *span), - Bound::Holds(held, _, span) => { - Bound::Holds(self.instantiate_with(held, made), var, *span) - } - }) - .collect(); - self.bounds[var.0 as usize] = bounds; - self.literals.push(var); - } - } - - /// `typ` with each unsettled literal type in it replaced by a type that - /// says which literals it's the type of, so two copies of one generic - /// function's literal type parameter look the same: each call of `f0` - /// in `fn f1(x) => f0(x) + f0(x)` copies the `1` of `fn f0(x) => x + 1`. - pub(crate) fn with_literals_named(&self, typ: &StaticType) -> StaticType { - self.resolve(typ).replace(&|part| match part { - StaticType::Var(var) if self.is_literal(*var) => { - let mut spans = self.bounds[var.0 as usize] - .iter() - .map(|bound| match bound { - Bound::Number(_, span) | Bound::Holds(_, _, span) => { - (span.source_id().index(), span.offset(), span.end()) - } - }) - .collect::>(); - spans.sort_unstable(); - spans.dedup(); - Some(StaticType::Generic(format!("literal{spans:?}").into())) - } - _ => None, - }) } /// Whether `typ` is made from the unsettled variable `var`, directly or @@ -650,18 +524,6 @@ impl TypeVars { } match shallow { StaticType::Var(other) => { - // Copies of one literal, as two calls of a generic function - // make, have the same bounds, which it needs once. Copies - // may hold values of different types, as `wrap(1)` and - // `wrap("s")` of `fn wrap(x) => [x]` do, which it needs both. - let mut known = AHashMap::<_, Vec>::new(); - for bound in &self.bounds[other.0 as usize] { - known.entry(bound.position()).or_default().push( - bound - .held() - .map_or(StaticType::Int, |held| self.resolve(held)), - ); - } for bound in bounds { // A literal holding a value of the type the other // variable already is says nothing more. @@ -670,14 +532,6 @@ impl TypeVars { { continue; } - let held = bound - .held() - .map_or(StaticType::Int, |held| self.resolve(held)); - let same = known.entry(bound.position()).or_default(); - if same.contains(&held) { - continue; - } - same.push(held); self.bounds[other.0 as usize].push(bound); self.bounded.push(other); self.literals.push(other); diff --git a/ndc_analysis/src/lib.rs b/ndc_analysis/src/lib.rs index b468e078..4dbff040 100644 --- a/ndc_analysis/src/lib.rs +++ b/ndc_analysis/src/lib.rs @@ -93,11 +93,6 @@ pub struct Instance { pub augmented_assignments: AHashMap, /// As [`Analysis::type_checks`], for the instance's body. pub type_checks: AHashMap, - /// The types of the integer literals in the instance's body whose type - /// is one of the function's type parameters, in place of the ones in - /// [`Analysis::expr_types`]: the `1` in `fn fact(n) => if n < 2 { 1 } - /// else { n * fact(n - 1) }` is a Float in the instance for Floats. - pub literal_types: AHashMap, } impl Analysis { diff --git a/ndc_layout/src/lib.rs b/ndc_layout/src/lib.rs index 9659bd2f..d0794941 100644 --- a/ndc_layout/src/lib.rs +++ b/ndc_layout/src/lib.rs @@ -51,9 +51,6 @@ pub struct InstancePicks { pub augmented_assignments: AHashMap>, /// As [`Analysis::type_checks`], for the instance's body. pub type_checks: AHashMap, - /// The types of the integer literals in the instance's body whose type - /// is one of the function's type parameters. - pub literal_types: AHashMap, } impl InstancePicks { @@ -65,7 +62,6 @@ impl InstancePicks { setters: AHashMap::new(), augmented_assignments: AHashMap::new(), type_checks: AHashMap::new(), - literal_types: AHashMap::new(), } } } @@ -166,7 +162,6 @@ impl<'a> Layout<'a> { .map(locate_binding) .collect(), type_checks: picks.type_checks, - literal_types: picks.literal_types, }; analysis .instances diff --git a/ndc_vm/src/compiler.rs b/ndc_vm/src/compiler.rs index 310b6e35..cdf272f6 100644 --- a/ndc_vm/src/compiler.rs +++ b/ndc_vm/src/compiler.rs @@ -215,10 +215,7 @@ impl Compiler { let value = match literal { // An integer literal is what its use made it: `1` in // `[1.5, 1]` is a Float. - NumericLiteral::Int64(i) => match self - .instance_pick(|instance| instance.literal_types.get(&id)) - .or_else(|| self.analysis.expr_types.get(&id)) - { + NumericLiteral::Int64(i) => match self.analysis.expr_types.get(&id) { Some(StaticType::Float) => Value::float(i as f64), Some(StaticType::Number) => { Value::number(crate::value::AdvancedNumber::Int(i.into())) diff --git a/tests/functional/programs/005_functions/087_a_literal_in_a_generic_body_is_typed_by_the_body.ndc b/tests/functional/programs/005_functions/087_a_literal_in_a_generic_body_is_typed_by_the_body.ndc new file mode 100644 index 00000000..d9eb286a --- /dev/null +++ b/tests/functional/programs/005_functions/087_a_literal_in_a_generic_body_is_typed_by_the_body.ndc @@ -0,0 +1,7 @@ +// Nothing in `fact`'s body needs the `1` to be anything else, so it's an +// Int, which `fact` returns. Called with a Float, `n * fact(n - 1)` is a +// Float, and the two branches can't both be what `fact` returns: write +// `1.0` for a `fact` of Floats. +// expect-error: mismatched types: found Float but expected Int +fn fact(n) => if n < 2 { 1 } else { n * fact(n - 1) }; +print(fact(5.0)); diff --git a/tests/functional/programs/005_functions/087_a_literal_in_a_generic_body_takes_the_call_s_type.ndc b/tests/functional/programs/005_functions/087_a_literal_in_a_generic_body_takes_the_call_s_type.ndc deleted file mode 100644 index ad76228e..00000000 --- a/tests/functional/programs/005_functions/087_a_literal_in_a_generic_body_takes_the_call_s_type.ndc +++ /dev/null @@ -1,6 +0,0 @@ -// The `1` in `fact` is what `fact` returns, so it's a Float when `fact` is -// called with one: each call settles the literal as a literal there would be. -// expect-output: 120 120.0 1.0 55.0 -fn fact(n) => if n < 2 { 1 } else { n * fact(n - 1) }; -fn fib(n) => if n < 2 { n } else { fib(n - 1) + fib(n - 2) }; -print(fact(5), fact(5.0), fact(1.0), fib(10.0)); diff --git a/tests/functional/programs/005_functions/088_a_list_of_a_parameter_and_a_literal_is_typed_by_the_body.ndc b/tests/functional/programs/005_functions/088_a_list_of_a_parameter_and_a_literal_is_typed_by_the_body.ndc new file mode 100644 index 00000000..1a9b56e1 --- /dev/null +++ b/tests/functional/programs/005_functions/088_a_list_of_a_parameter_and_a_literal_is_typed_by_the_body.ndc @@ -0,0 +1,5 @@ +// The `1` in `[x, 1]` is an Int, so the list holds Ints and `x` is one: +// `g` doesn't take a Float. +// expect-error: No function called 'g' found that matches the arguments 'Float' +fn g(x) => [x, 1]; +print(g(2), g(2.5)); diff --git a/tests/functional/programs/005_functions/088_a_literal_holding_a_parameter_takes_the_call_s_type.ndc b/tests/functional/programs/005_functions/088_a_literal_holding_a_parameter_takes_the_call_s_type.ndc deleted file mode 100644 index 4a9ec6ad..00000000 --- a/tests/functional/programs/005_functions/088_a_literal_holding_a_parameter_takes_the_call_s_type.ndc +++ /dev/null @@ -1,7 +0,0 @@ -// A literal that holds a parameter, or is passed with one, is as generic -// as the parameter: `[x, 1]` is a list of Floats when `x` is a Float. -// expect-output: [2.5,1.0] [2,1] [1,"b"] [2.5,1.0] -fn g(x) => [x, 1]; -fn k(x) => [x, "b"]; -fn f(xs) { xs.push(1); xs } -print(g(2.5), g(2), k(1), f([2.5])); diff --git a/tests/functional/programs/005_functions/089_a_generic_function_as_a_value_has_int_literals.ndc b/tests/functional/programs/005_functions/089_a_generic_function_as_a_value_has_int_literals.ndc deleted file mode 100644 index f60c6ab9..00000000 --- a/tests/functional/programs/005_functions/089_a_generic_function_as_a_value_has_int_literals.ndc +++ /dev/null @@ -1,5 +0,0 @@ -// A generic function used as a value runs its own body, whose integer -// literals are Ints, so `pair` can't be a function returning Floats. -// expect-error: mismatched types -fn pair(x) => [x, 1]; -let f: Fn(Float) -> List = pair; diff --git a/tests/functional/programs/005_functions/090_copies_of_a_literal_keep_what_each_holds.ndc b/tests/functional/programs/005_functions/090_each_call_of_a_generic_function_makes_a_list_of_what_it_is_given.ndc similarity index 100% rename from tests/functional/programs/005_functions/090_copies_of_a_literal_keep_what_each_holds.ndc rename to tests/functional/programs/005_functions/090_each_call_of_a_generic_function_makes_a_list_of_what_it_is_given.ndc diff --git a/tests/functional/programs/005_functions/094_a_recursive_call_through_another_overload_can_t_grow.ndc b/tests/functional/programs/005_functions/094_a_recursive_call_through_another_overload_can_t_grow.ndc index 03472edf..272018bc 100644 --- a/tests/functional/programs/005_functions/094_a_recursive_call_through_another_overload_can_t_grow.ndc +++ b/tests/functional/programs/005_functions/094_a_recursive_call_through_another_overload_can_t_grow.ndc @@ -1,8 +1,8 @@ -// `[x]` with an Int `x` is a `List`, which isn't the `List` the +// `[-x]` with an Int `x` is a `List`, which isn't the `List` the // first `f` takes, so `f(1)` would need `f` for `List`, then for // `List>`, without end. Another overload taking the call only // keeps the error from being reported in the body. // expect-error: `f` calls itself with an argument built from what it was given fn f(xs: List) => 42; -fn f(x) => f([x]); +fn f(x) => f([-x]); print(f(1)); diff --git a/tests/functional/programs/005_functions/097_a_list_of_a_parameter_is_typed_by_the_body.ndc b/tests/functional/programs/005_functions/097_a_list_of_a_parameter_is_typed_by_the_body.ndc new file mode 100644 index 00000000..7215c286 --- /dev/null +++ b/tests/functional/programs/005_functions/097_a_list_of_a_parameter_is_typed_by_the_body.ndc @@ -0,0 +1,7 @@ +// `[x]` is a list of what `x` is, and `f` can't take a list of its own +// parameter's type, so only the first `f` takes it, which makes `x` an +// `Any`: `f(1)` calls `f([1])` with a `List`. +// expect-output: 42 +fn f(xs: List) => 42; +fn f(x) => f([x]); +print(f(1)); diff --git a/tests/functional/programs/005_functions/098_a_generic_function_takes_what_its_body_s_literals_allow.ndc b/tests/functional/programs/005_functions/098_a_generic_function_takes_what_its_body_s_literals_allow.ndc new file mode 100644 index 00000000..7e7fc927 --- /dev/null +++ b/tests/functional/programs/005_functions/098_a_generic_function_takes_what_its_body_s_literals_allow.ndc @@ -0,0 +1,8 @@ +// The literals in a generic function's body are Ints, and an Int works +// with a Float in `+`, `-` and `<`, so these take Floats too. `fact` with a +// `1.0` returns what it's given multiplied. +// expect-output: 3.5 3 55.0 120.0 120.0 +fn inc(x) => x + 1; +fn fib(n) => if n < 2 { n } else { fib(n - 1) + fib(n - 2) }; +fn fact(n) => if n < 2 { 1.0 } else { n * fact(n - 1) }; +print(inc(2.5), inc(2), fib(10.0), fact(5.0), fact(5)); From a9aa78ab881e63e1b45d3ae05136e7f2f5fc4905 Mon Sep 17 00:00:00 2001 From: Tim Fennis Date: Fri, 13 Nov 2026 18:19:24 +0100 Subject: [PATCH 2/2] =?UTF-8?q?perf(analyser):=20sort=20the=20unsettled=20?= =?UTF-8?q?literals=20by=20a=20key=20computed=20once=20=E2=9A=A1?= 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/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ndc_analyser/src/types.rs b/ndc_analyser/src/types.rs index 19fdebae..f0d64b39 100644 --- a/ndc_analyser/src/types.rs +++ b/ndc_analyser/src/types.rs @@ -253,7 +253,7 @@ impl TypeVars { Bound::Number(..) => None, }) .collect::>(); - literals.sort_by_key(|&var| (self.is_number(var), held.contains(&var))); + literals.sort_by_cached_key(|&var| (self.is_number(var), held.contains(&var))); literals }