From 97b369c4cd68bfe5af4cd2b9fbe62a776725902d Mon Sep 17 00:00:00 2001 From: Tim Fennis Date: Sat, 14 Nov 2026 10:24:05 +0100 Subject: [PATCH] =?UTF-8?q?refactor(analyser):=20delete=20the=20runtime=20?= =?UTF-8?q?checks=20Any=20values=20no=20longer=20need=20=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With explicit Any, a value of type Any only goes where Any is taken, so the implicit checks the analyser recorded for Any values never fire for a program without errors. Delete the ExpectType opcode, the type_checks tables, the checks the analyser and instances collected for them, the argument nodes calls carried to place them, the string branches of the generic prefixes and suffixes, and the Any relaxations for function values in overload picking. Co-Authored-By: Claude Opus 5.5 --- ndc_analyser/src/analyser/assignments.rs | 60 ++------ ndc_analyser/src/analyser/calls.rs | 14 +- ndc_analyser/src/analyser/declarations.rs | 14 +- ndc_analyser/src/analyser/function_values.rs | 17 +-- ndc_analyser/src/analyser/generics.rs | 23 +-- ndc_analyser/src/analyser/instances.rs | 28 +--- ndc_analyser/src/analyser/mod.rs | 82 +++------- ndc_analyser/src/analyser/solver.rs | 152 +++++-------------- ndc_analyser/src/types.rs | 4 +- ndc_analysis/src/lib.rs | 8 - ndc_core/src/static_type.rs | 12 +- ndc_layout/src/lib.rs | 4 - ndc_stdlib/src/sequence.rs | 36 +---- ndc_vm/src/chunk.rs | 7 +- ndc_vm/src/compiler.rs | 30 +--- ndc_vm/src/vm.rs | 16 +- tests/compiler/tests/compiler.rs | 6 +- 17 files changed, 103 insertions(+), 410 deletions(-) diff --git a/ndc_analyser/src/analyser/assignments.rs b/ndc_analyser/src/analyser/assignments.rs index 96a8a029..d5657188 100644 --- a/ndc_analyser/src/analyser/assignments.rs +++ b/ndc_analyser/src/analyser/assignments.rs @@ -1,5 +1,5 @@ use super::Analyser; -use super::solver::{CallNodes, CallSite, Origin}; +use super::solver::{CallSite, Origin}; use crate::error::AnalysisError; use crate::overloads::{CallKind, ResolvedCall}; use itertools::izip; @@ -38,12 +38,6 @@ impl Analyser { let result = self.constrain_call( CallSite::OpAssignment(id), vec![left_type.clone(), right_type], - // What the operator returns is checked where it's written - // back, when the program picks the operator as it runs. - CallNodes { - arguments: vec![None, Some(r_value.id)], - result: Some(l_value.id), - }, CallKind::Operator, operation, span, @@ -131,10 +125,6 @@ impl Analyser { Some(_) => self.constrain_call( CallSite::Getter(lvalue.id), vec![container.clone(), index_type.clone()], - CallNodes { - arguments: vec![Some(value.id), Some(index.id)], - result: None, - }, CallKind::Regular, "[]", span, @@ -146,10 +136,6 @@ impl Analyser { self.constrain_call( CallSite::Setter(lvalue.id), vec![container, index_type, stored.clone()], - CallNodes { - arguments: vec![Some(value.id), Some(index.id), Some(lvalue.id)], - result: None, - }, CallKind::Regular, "[]=", span, @@ -173,10 +159,6 @@ impl Analyser { let stored = self.constrain_call( CallSite::Getter(lvalue.id), vec![receiver_type.clone()], - CallNodes { - arguments: vec![Some(receiver.id)], - result: None, - }, CallKind::Regular, member, *member_span, @@ -184,10 +166,6 @@ impl Analyser { self.constrain_call( CallSite::FieldSetter(lvalue.id), vec![receiver_type, stored.clone()], - CallNodes { - arguments: vec![Some(receiver.id), Some(lvalue.id)], - result: None, - }, CallKind::Regular, &format!("{member}="), *member_span, @@ -235,7 +213,6 @@ impl Analyser { &mut self, stored_type: &StaticType, value_type: &StaticType, - node: Option, ) -> bool { // A write passes the value where a value of the stored type is // expected, as a call passes an argument: `let xs: Iterable = @@ -245,11 +222,6 @@ impl Analyser { self.vars.rollback(snapshot); return false; } - if let Some(node) = node - && self.vars.resolve(value_type) == StaticType::Any - { - self.expect(node, stored_type.clone()); - } true } @@ -262,24 +234,22 @@ impl Analyser { stored_type: &StaticType, value_type: &StaticType, span: Span, - node: Option, ) { - if !self.accepts_write(stored_type, value_type, node) { - self.emit_rejected_write(stored_type, value_type, span, node); + if !self.accepts_write(stored_type, value_type) { + self.emit_rejected_write(stored_type, value_type, span); } } /// Reports that a value of `value_type` can't be stored where values of /// `stored_type` are kept. Values that share no type, as the branches - /// of `if c { 1 } else { "a" }`, may still become `Any`s, which are - /// checked where they're stored, at `node`, so their write is checked - /// again once they're settled. + /// of `if c { 1 } else { "a" }`, are reported as such once they're + /// settled, unless a later use makes them `Any`s, as `let v: Any = …` + /// does, so their write is checked again then. pub(super) fn emit_rejected_write( &mut self, stored_type: &StaticType, value_type: &StaticType, span: Span, - node: Option, ) { if let StaticType::Var(var) = self.vars.resolve(value_type) && self.vars.is_literal(var) @@ -289,7 +259,6 @@ impl Analyser { stored_type: stored_type.clone(), value_type: value_type.clone(), span, - node, recording: self.recording, copied_through: self.copied_through.clone(), }); @@ -304,8 +273,9 @@ impl Analyser { } /// Checks again each write [`Self::emit_rejected_write`] put off whose - /// value is settled now: as `Any`, the write is checked as the program - /// runs, and as values that share no type, it was reported as such. + /// value is settled now: as `Any`, the write is reported unless `Any` is + /// what's stored there, and as values that share no type, it was + /// reported as such. pub(super) fn recheck_deferred_writes(&mut self) { if self.deferred_writes.is_empty() { return; @@ -322,12 +292,7 @@ impl Analyser { } let recording = std::mem::replace(&mut self.recording, write.recording); let copied_through = std::mem::replace(&mut self.copied_through, write.copied_through); - self.check_write( - &write.stored_type, - &write.value_type, - write.span, - write.node, - ); + self.check_write(&write.stored_type, &write.value_type, write.span); self.recording = recording; self.copied_through = copied_through; } @@ -346,10 +311,10 @@ impl Analyser { AssignmentTarget::Identifier { .. } if self.picks.variables.contains_key(&lvalue.id) => { - self.check_write(stored_type, value_type, span, Some(lvalue.id)); + self.check_write(stored_type, value_type, span); } AssignmentTarget::Member { .. } | AssignmentTarget::Index { .. } => { - self.check_write(stored_type, value_type, span, Some(lvalue.id)); + self.check_write(stored_type, value_type, span); } // Each target gets the element unpacked for it, as in a `let`. AssignmentTarget::Sequence(targets) => { @@ -385,7 +350,6 @@ pub(super) struct DeferredWrite { stored_type: StaticType, value_type: StaticType, span: Span, - node: Option, recording: Option, copied_through: Option, } diff --git a/ndc_analyser/src/analyser/calls.rs b/ndc_analyser/src/analyser/calls.rs index 83c8a265..3966ee8f 100644 --- a/ndc_analyser/src/analyser/calls.rs +++ b/ndc_analyser/src/analyser/calls.rs @@ -1,5 +1,5 @@ use super::Analyser; -use super::solver::{CallNodes, CallSite}; +use super::solver::CallSite; use crate::error::AnalysisError; use crate::overloads::{CallKind, Overloads}; use itertools::Itertools; @@ -80,10 +80,6 @@ impl Analyser { self.constrain_call( CallSite::Getter(id), vec![receiver_type], - CallNodes { - arguments: vec![Some(receiver.id)], - result: Some(id), - }, CallKind::Regular, member, member_span, @@ -94,7 +90,6 @@ impl Analyser { /// Only operator-form calls are eligible for vec dispatch. pub(super) fn analyse_call( &mut self, - id: NodeId, function: &ExpressionLocation, arguments: &[ExpressionLocation], kind: CallKind, @@ -104,7 +99,6 @@ impl Analyser { // function position, so the callee is analysed as a value. The callee // comes before the arguments, in source order like the resolver, so // declarations in it get their slots first. - let nodes = arguments.iter().map(|arg| Some(arg.id)).collect::>(); let Expression::Identifier { name, .. } = &function.expression else { let callee_type = self.analyse_expr(function); let arguments = arguments @@ -126,8 +120,6 @@ impl Analyser { .. } => { if let Some((typ, parameters)) = self.bind_signature(&callee, &arguments) { - let checks = self.expected_of_any(¶meters, &arguments, &nodes); - self.pending.checks.extend(checks); let iterations = self.iterations_waited_for(¶meters, &arguments); self.pending.iterations.extend(iterations); self.flush_copies(None, span); @@ -180,10 +172,6 @@ impl Analyser { self.constrain_index_call( CallSite::Call(function.id), arguments, - CallNodes { - arguments: nodes, - result: Some(id), - }, kind, name, span, diff --git a/ndc_analyser/src/analyser/declarations.rs b/ndc_analyser/src/analyser/declarations.rs index 611c1817..b2935206 100644 --- a/ndc_analyser/src/analyser/declarations.rs +++ b/ndc_analyser/src/analyser/declarations.rs @@ -52,7 +52,6 @@ impl Analyser { self.declaring = declared.map(|(_, symbol)| symbol); } let first_use = self.uses.len(); - let first_check = self.checks.len(); let annotations = parameters .iter() .map(|param| self.lower_annotation(param.annotation.as_ref())) @@ -138,7 +137,7 @@ impl Analyser { if matches!(self.vars.resolve(returns), StaticType::Var(_)) { self.vars.unify(returns, &StaticType::Any); } else { - self.emit_rejected_write(returns, &inferred_return, span, None); + self.emit_rejected_write(returns, &inferred_return, span); } } @@ -155,13 +154,8 @@ impl Analyser { self.vars.leave_level(); self.declaring = None; self.declared_in_batch.insert(symbol); - let typ = self.generalize( - symbol, - &signature, - id, - pure, - [first_use, first_check, first_function], - ); + let typ = + self.generalize(symbol, &signature, id, pure, [first_use, first_function]); self.within_size_limit(symbol, name, typ, span) } _ => signature, @@ -287,7 +281,7 @@ impl Analyser { // The value is checked like an assignment to the annotated // variable, so `let o: Option = None` settles `None`. if let Some(expected_type) = &expected_type { - self.check_write(expected_type, &found_type, *span, Some(lvalue.id)); + self.check_write(expected_type, &found_type, *span); } // An annotation decides the type, or else the value does. diff --git a/ndc_analyser/src/analyser/function_values.rs b/ndc_analyser/src/analyser/function_values.rs index e05c6f8e..f1e54b78 100644 --- a/ndc_analyser/src/analyser/function_values.rs +++ b/ndc_analyser/src/analyser/function_values.rs @@ -169,20 +169,7 @@ impl Analyser { { Binding::Resolved(Candidate::Scalar(symbol)) => Some(symbol), // The program would pick one as it runs, but a function - // value is one function. When the expected type's `Any` - // could be what's not known yet, as in a generic - // function's own body, more than one might be meant. - Binding::Dynamic(candidates) => { - let might = candidates.into_iter().any(|candidate| match candidate { - Candidate::Scalar(symbol) => self.might_be_expected(symbol, expected), - Candidate::Vec(_) => false, - }); - return if might { - Choice::Undecided - } else { - Choice::None - }; - } + // value is one function. _ => None, } }; @@ -247,7 +234,7 @@ impl Analyser { /// Whether a value of `symbol`'s type might be passed where `expected` /// is, once what's not known about either is: an `Fn(Int) -> Int` might - /// be the `Fn(Any) -> Any` a generic function's body expects, but an + /// be an `Fn(A) -> B` whose `A` and `B` aren't known yet, but an /// `Fn(Int, Int) -> Int` isn't. fn might_be_expected(&self, symbol: SymbolId, expected: &StaticType) -> bool { self.vars diff --git a/ndc_analyser/src/analyser/generics.rs b/ndc_analyser/src/analyser/generics.rs index e9452e8a..55ebf725 100644 --- a/ndc_analyser/src/analyser/generics.rs +++ b/ndc_analyser/src/analyser/generics.rs @@ -41,7 +41,7 @@ impl Analyser { signature: &StaticType, declaration: NodeId, pure: bool, - [first_use, first_check, first_function]: [usize; 3], + [first_use, first_function]: [usize; 2], ) -> StaticType { self.solve_waiting(); self.default_literals(TypeVars::is_generalizable); @@ -141,23 +141,9 @@ impl Analyser { _ => {} } } - // A value of type `Any` that the body passes where a type parameter - // is expected, as `a` in `fn f(x) { let a: Any = 5; [x].push(a) }`, - // is checked against what each call makes it. - let checks = self.checks[first_check..] - .iter() - .filter(|(instance, ..)| instance.is_none()) - .map(|(_, node, typ, expected)| { - (*node, self.vars.resolve(typ), self.vars.resolve(expected)) - }) - .filter(|(_, typ, expected)| { - *typ == StaticType::Any - && expected.contains(&|part| matches!(part, StaticType::Generic(_))) - }) - .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, and the checks. + // picks for. // A `pure fn` caches its results by its arguments' values, which can // be equal across types, as `1` and `1.0` are, so each set of types // gets an instance, and a cache, of its own. @@ -173,7 +159,6 @@ impl Analyser { .iter() .flat_map(|waiting| waiting.constraint.inputs()) .chain(uses.iter().flat_map(|generic_use| &generic_use.key)) - .chain(checks.iter().map(|(_, _, expected)| expected)) .chain(¶meters) { for name in typ.type_parameters() { @@ -190,7 +175,6 @@ impl Analyser { uses: uses.into(), declaration, recursive_calls: std::mem::take(&mut self.recursive_calls).into(), - checks: checks.into(), functions: self.declared_functions[first_function..].into(), }, ); @@ -425,8 +409,7 @@ impl Analyser { /// can take the arguments, picked as the program runs. Its type /// parameters, and the `Any` that a call picked as it runs gives, stand /// for any type there: an overload that takes an Int may get one. Each - /// call of the function checks its body with the types it's given, so - /// an `Any` the program wrote was checked there. + /// call of the function checks its body with the types it's given. fn pick_for_any_arguments(&mut self, call: &CallConstraint) { let arguments = call .arguments() diff --git a/ndc_analyser/src/analyser/instances.rs b/ndc_analyser/src/analyser/instances.rs index 54f9c8f9..fda46d1b 100644 --- a/ndc_analyser/src/analyser/instances.rs +++ b/ndc_analyser/src/analyser/instances.rs @@ -33,10 +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]>, - /// 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. - pub(super) checks: Rc<[(NodeId, StaticType, StaticType)]>, /// The functions its body declares, whose types an instance makes what /// its own types make them. pub(super) functions: Rc<[NodeId]>, @@ -59,8 +55,7 @@ pub(super) struct GenericUse { } /// What a call that's about to commit copied from the generic function it -/// commits to, and the checks its arguments need, until it's known whether -/// the call commits. +/// commits to, until it's known whether the call commits. #[derive(Debug, Default)] pub(super) struct Pending { /// Its constraints, with the function they're copied from and the calls @@ -68,8 +63,6 @@ pub(super) struct Pending { pub(super) copies: Vec<(SymbolId, Constraint, Option)>, /// The function and the types the call gives its key. pub(super) uses: Vec<(SymbolId, Vec)>, - /// Where an argument of type `Any` is checked, and against what. - pub(super) checks: Vec<(NodeId, StaticType)>, /// The arguments not known yet that were passed for an `Iterable`, /// with the `T` their elements settle once they're known. pub(super) iterations: Vec<(StaticType, StaticType)>, @@ -77,24 +70,18 @@ pub(super) struct Pending { impl Pending { /// How much is pending, to [`Self::truncate`] to later. - pub(super) fn len(&self) -> [usize; 4] { - [ - self.copies.len(), - self.uses.len(), - self.checks.len(), - self.iterations.len(), - ] + pub(super) fn len(&self) -> [usize; 3] { + [self.copies.len(), self.uses.len(), self.iterations.len()] } - pub(super) fn truncate(&mut self, [copies, uses, checks, iterations]: [usize; 4]) { + pub(super) fn truncate(&mut self, [copies, uses, iterations]: [usize; 3]) { self.copies.truncate(copies); self.uses.truncate(uses); - self.checks.truncate(checks); self.iterations.truncate(iterations); } pub(super) fn clear(&mut self) { - self.truncate([0; 4]); + self.truncate([0; 3]); } } @@ -295,11 +282,6 @@ impl Analyser { picks.function_types.insert(node, typ); } } - for (node, typ, expected) in scheme.checks.iter() { - let typ = self.vars.instantiate_with(typ, &mut made); - let expected = self.vars.instantiate_with(expected, &mut made); - self.expect_if_any(*node, typ, expected); - } for &call in scheme.recursive_calls.iter() { self.pick_tables(Some(instance)).insert( CallSite::Call(call), diff --git a/ndc_analyser/src/analyser/mod.rs b/ndc_analyser/src/analyser/mod.rs index 30358485..9a896228 100644 --- a/ndc_analyser/src/analyser/mod.rs +++ b/ndc_analyser/src/analyser/mod.rs @@ -96,11 +96,6 @@ pub struct Analyser { /// The writes of values that share no type, which are checked again /// once those are settled. deferred_writes: Vec, - /// Where a value is checked as the program runs if it's of type `Any`, - /// with its type and the type it's checked against, which may both - /// still have variables in them, by the instance whose body checks it, - /// or `None` for code compiled once. - checks: Vec<(Option, NodeId, StaticType, StaticType)>, /// The number the next waiting constraint gets. next_serial: u64, /// The waiting constraints that each unsettled variable would let make @@ -176,7 +171,6 @@ impl Analyser { copied_through: None, literal_origins: AHashMap::new(), deferred_writes: Vec::new(), - checks: Vec::new(), next_serial: 0, watchers: AHashMap::new(), watched_settlings: 0, @@ -226,24 +220,6 @@ impl Analyser { } } self.types.resolve_all(&self.vars); - for (instance, node, typ, expected) in std::mem::take(&mut self.checks) { - if self.vars.resolve(&typ) != StaticType::Any { - continue; - } - let expected = self - .vars - .finish(&expected) - .replace(&|part| matches!(part, StaticType::Generic(_)).then_some(StaticType::Any)); - if expected == StaticType::Any { - continue; - } - let checks = match instance.and_then(|instance| self.picks.instances.get_mut(&instance)) - { - Some(picks) => &mut picks.type_checks, - None => &mut self.result.analysis.type_checks, - }; - checks.insert(node, expected); - } let analysis = &mut self.result.analysis; for table in [ &mut analysis.expr_types, @@ -270,24 +246,6 @@ impl Analyser { result } - /// Records that the value of type `Any` at `node` is checked to be an - /// `expected` as the program runs, in the instance whose constraints are - /// being solved, if any. A function's own body checks its type - /// parameters as `Any`. A copy of a constraint checks nothing, as its - /// node is in the body of the function it was copied from, which the - /// instance for the call checks. - pub(super) fn expect(&mut self, node: NodeId, expected: StaticType) { - self.expect_if_any(node, StaticType::Any, expected); - } - - /// As [`Self::expect`], for a value of type `typ`, which is checked if - /// it turns out to be `Any`. - pub(super) fn expect_if_any(&mut self, node: NodeId, typ: StaticType, expected: StaticType) { - if self.copied_through.is_none() { - self.checks.push((self.recording, node, typ, expected)); - } - } - /// Records an analysis error. The analyser continues with a fallback type /// (usually [`StaticType::Error`], which nothing reports again) so that /// the rest of the program is still checked. An error in @@ -624,11 +582,11 @@ impl Analyser { | Expression::MethodCall { function, arguments, - } => self.analyse_call(*id, function, arguments, CallKind::Regular, *span), + } => self.analyse_call(function, arguments, CallKind::Regular, *span), Expression::OperatorCall { function, arguments, - } => self.analyse_call(*id, function, arguments, CallKind::Operator, *span), + } => self.analyse_call(function, arguments, CallKind::Operator, *span), Expression::MemberAccess { receiver, member, @@ -679,10 +637,9 @@ impl Analyser { } // Reads can produce the default value, so its type is part of - // the map's value type. A default that's a function, as an - // `Any` one may be as the program runs, is called for each - // missing key, so what it makes isn't the default's own - // value, and has no node to be checked at. + // the map's value type. A default that's a function is + // called for each missing key, so what it makes isn't the + // default's own value. if let Some(default) = default { let default_type = self.analyse_expr(default); let default_type = self.constrain_default_value(default_type, default.span); @@ -724,7 +681,7 @@ impl Analyser { // `n` is one, whatever else it's used for. for bound in [start, end].into_iter().flatten() { let typ = self.analyse_expr(bound); - self.check_write(&StaticType::Int, &typ, bound.span, Some(bound.id)); + self.check_write(&StaticType::Int, &typ, bound.span); } StaticType::Iterator(Box::new(StaticType::Int)) @@ -795,9 +752,7 @@ impl Analyser { /// The type of the elements of the literal at `span` holding `values`, /// each with where it is and its type, which takes the type its uses - /// settle: in `let xs: List = [1, 2]`, `Any`. A value of type - /// `Any` among them is checked against it as the program runs, at its - /// node if it has one. + /// settle: in `let xs: List = [1, 2]`, `Any`. fn holding( &mut self, values: Vec<(Option, Span, StaticType)>, @@ -810,20 +765,17 @@ impl Analyser { .collect(), span, ); - for (node, _, typ) in values { - if let Some(node) = node { - // An overloaded function value waiting for its type, as - // `abs` in `[len, abs]`, is one of the literal's values. - if self.waiting_function_values.contains(&node) { - for waiting in self.waiting.values_mut() { - if let Constraint::FunctionValue(value) = &mut waiting.constraint - && value.id() == node - { - value.mark_in_literal(); - } + // An overloaded function value waiting for its type, as `abs` in + // `[len, abs]`, is one of the literal's values. + for node in values.into_iter().filter_map(|(node, _, _)| node) { + if self.waiting_function_values.contains(&node) { + for waiting in self.waiting.values_mut() { + if let Constraint::FunctionValue(value) = &mut waiting.constraint + && value.id() == node + { + value.mark_in_literal(); } } - self.expect_if_any(node, typ, element.clone()); } } element @@ -883,7 +835,7 @@ impl Analyser { /// returns the type its values join as, as with [`Self::join`]. fn add_return(&mut self, returns: &mut Returns, value: &ExpressionLocation, typ: StaticType) { if let Some(annotated) = &returns.annotated { - self.check_write(annotated, &typ, value.span, Some(value.id)); + self.check_write(annotated, &typ, value.span); return; } returns.joined = Some(match returns.joined.take() { diff --git a/ndc_analyser/src/analyser/solver.rs b/ndc_analyser/src/analyser/solver.rs index 32d457b5..2708b889 100644 --- a/ndc_analyser/src/analyser/solver.rs +++ b/ndc_analyser/src/analyser/solver.rs @@ -194,26 +194,11 @@ pub(super) enum EarlyPick { }, } -/// The expressions a call's arguments and value come from, where a value -/// of type `Any` is checked against the type the call expects of it as the -/// program runs: in `xs.push(a)` on a `List`, `a` is checked to be an -/// Int. The value an assignment stores is checked at the assignment's -/// target. -#[derive(Debug, Clone, Default)] -pub(super) struct CallNodes { - /// Where each argument comes from. - pub(super) arguments: Vec>, - /// The call itself, whose value is checked when it's `Any` but code - /// meanwhile settled the type it has. - pub(super) result: Option, -} - #[derive(Debug, Clone)] pub(super) struct CallConstraint { site: CallSite, candidates: Candidates, arguments: Vec, - nodes: CallNodes, /// The call's type, which the picked overload's return type settles. result: StaticType, kind: CallKind, @@ -269,12 +254,11 @@ impl Analyser { &mut self, site: CallSite, arguments: Vec, - nodes: CallNodes, kind: CallKind, name: &str, span: Span, ) -> StaticType { - self.constrain_index_call(site, arguments, nodes, kind, name, span, None) + self.constrain_index_call(site, arguments, kind, name, span, None) } /// [`Self::constrain_call`], for `[]` with the index `tuple_index` when @@ -284,7 +268,6 @@ impl Analyser { &mut self, site: CallSite, arguments: Vec, - nodes: CallNodes, kind: CallKind, name: &str, span: Span, @@ -294,7 +277,6 @@ impl Analyser { candidates: self.site_candidates(site).clone(), site, arguments, - nodes, result: StaticType::Never, kind, name: name.to_string(), @@ -401,10 +383,7 @@ impl Analyser { /// Commits `call` to `picked` while its arguments still have unknown /// parts, which settles them from `picked`'s parameters, and returns the - /// call's type, or `None` when they don't fit after all. The program - /// calls `picked` directly only where it would once the arguments are - /// known: an `Any` argument, as `xs` in `fn big(xs) { xs.filter(…) }`, is - /// checked as the program runs. + /// call's type, or `None` when they don't fit after all. fn commit_early( &mut self, call: &CallConstraint, @@ -414,7 +393,6 @@ impl Analyser { let Some(typ) = self.commit_call( &Binding::Resolved(Candidate::Scalar(picked)), arguments, - &call.nodes.arguments, StaticType::Any, ) else { self.pending.clear(); @@ -424,14 +402,7 @@ impl Analyser { .iter() .map(|argument| self.vars.resolve(argument)) .collect::>(); - // An argument of type `Any` is checked where it's passed, so the - // program can call `picked` directly, unless one has nowhere to be - // checked. - let binding = if Self::checks_every_any(&arguments, &call.nodes.arguments) { - Binding::Resolved(Candidate::Scalar(picked)) - } else { - Binding::Dynamic(vec![Candidate::Scalar(picked)]) - }; + let binding = Binding::Resolved(Candidate::Scalar(picked)); if cfg!(debug_assertions) { self.early_picks.push(EarlyPick::Call { candidates: call.candidates.clone(), @@ -471,23 +442,10 @@ impl Analyser { if arguments.iter().any(StaticType::has_errors) { continue; } - // A pick left to the program among one overload is a - // call of it, with its `Any` arguments checked. - let binding = match self + let binding = self .resolve_overloads(&candidates, &arguments, kind) - .binding - { - Binding::Dynamic(only) - if let [Candidate::Scalar(only)] = only[..] - && let StaticType::Function { - parameters: Some(_), - .. - } = self.vars.resolve(self.types.get_type(only)) => - { - Binding::Resolved(Candidate::Scalar(only)) - } - binding => binding, - }; + .binding; + let binding = self.call_of_the_only_one(binding); // A call that nothing takes any more is one an error // was reported for, as an argument whose overload // couldn't be told then got the fallback `Any`, and a @@ -1098,15 +1056,13 @@ impl Analyser { // the read said what the field holds, so it's the value // that doesn't fit. The write fails the same way and // isn't reported again. - CallSite::Getter(id) - if !self.accepts_write(&result, &typ, call.nodes.result) => - { - self.emit_rejected_write(&typ, &result, call.span, None); + CallSite::Getter(id) if !self.accepts_write(&result, &typ) => { + self.emit_rejected_write(&typ, &result, call.span); self.failed_reads.insert(id); } CallSite::Getter(_) => {} _ => { - self.check_write(&result, &typ, call.span, call.nodes.result); + self.check_write(&result, &typ, call.span); } } true @@ -1123,7 +1079,7 @@ impl Analyser { } let (element, span) = (element.clone(), *span); let found = self.iteration_gives(&sequence, span); - self.check_write(&element, &found, span, None); + self.check_write(&element, &found, span); true } Constraint::Unpack { value, parts, span } => { @@ -1152,7 +1108,7 @@ impl Analyser { }; let found = found.unwrap_or_else(|| vec![StaticType::Error; parts.len()]); for (part, found) in parts.iter().zip(found) { - self.check_write(part, &found, *span, None); + self.check_write(part, &found, *span); } true } @@ -1166,7 +1122,7 @@ impl Analyser { return false; } let value = value.clone(); - self.check_write(&value, &Self::default_value_type(default), *span, None); + self.check_write(&value, &Self::default_value_type(default), *span); true } Constraint::Join { @@ -1196,7 +1152,7 @@ impl Analyser { (StaticType::Never, other) | (other, StaticType::Never) => other.clone(), _ => self.join_known(&a, &b, *span), }; - self.check_write(&result, &joined, *span, None); + self.check_write(&result, &joined, *span); true } } @@ -1649,13 +1605,24 @@ impl Analyser { merged } - /// Whether every argument of type `Any` has a node in `nodes` to be checked - /// at where it's passed. - fn checks_every_any(arguments: &[StaticType], nodes: &[Option]) -> bool { - arguments - .iter() - .zip(nodes) - .all(|(argument, node)| node.is_some() || *argument != StaticType::Any) + /// `binding`, except that a pick left to the program among one function + /// whose parameters are known is a call of that function. A pick among + /// one candidate whose parameters aren't known, as a variadic function + /// or a variable that may not hold a function at all, stays the + /// program's. + fn call_of_the_only_one(&self, binding: Binding) -> Binding { + match binding { + Binding::Dynamic(only) + if let [Candidate::Scalar(only)] = only[..] + && let StaticType::Function { + parameters: Some(_), + .. + } = self.vars.resolve(self.types.get_type(only)) => + { + Binding::Resolved(Candidate::Scalar(only)) + } + binding => binding, + } } /// [`Overloads::resolve_call`] over the types as they're known now. @@ -1675,24 +1642,7 @@ impl Analyser { binding, return_type, } = self.resolve_overloads(&call.candidates, arguments, call.kind); - // A pick left to the program among one function with known - // parameters is a call of it, whose `Any` arguments are checked where - // they're passed. A variable of type `Any` may hold any function, and - // an `Any` without a node to check it at, as the target of `a op= b`, - // isn't checked where it's passed, so the program checks those itself. - let binding = match binding { - Binding::Dynamic(only) - if let [Candidate::Scalar(only)] = only[..] - && Self::checks_every_any(arguments, &call.nodes.arguments) - && let StaticType::Function { - parameters: Some(_), - .. - } = self.vars.resolve(self.types.get_type(only)) => - { - Binding::Resolved(Candidate::Scalar(only)) - } - binding => binding, - }; + let binding = self.call_of_the_only_one(binding); // `a op= b` with no operator that takes the operands calls an in-place // overload if one does, as a set has `-=` but no `-`, which keeps `a`. if matches!(binding, Binding::None) @@ -1712,7 +1662,7 @@ impl Analyser { self.record_pick(call.site, binding, arguments); return StaticType::Error; } - let typ = self.commit_call(&binding, arguments, &call.nodes.arguments, return_type); + let typ = self.commit_call(&binding, arguments, return_type); self.record_pick(call.site, binding, arguments); if let Some(typ) = typ { self.flush_copies(Some(call.site), call.span); @@ -1732,13 +1682,11 @@ impl Analyser { /// When the pick is left to the program as it runs, any function may get /// the arguments, so what's unknown in them becomes `Any`. Returns `None` /// when an argument doesn't fit the picked overload after all, which - /// settles nothing. An argument of type `Any` is checked at its node in - /// `nodes` as the program runs, once the call commits. + /// settles nothing. fn commit_call( &mut self, binding: &Binding, arguments: &[StaticType], - nodes: &[Option], return_type: StaticType, ) -> Option { let picked = match binding { @@ -1762,7 +1710,7 @@ impl Analyser { scalar => scalar.clone(), }) .collect::>(); - returns.push(self.commit_call(&scalar, &arguments, &[], StaticType::Any)?); + returns.push(self.commit_call(&scalar, &arguments, StaticType::Any)?); } return Some(StaticType::Tuple(returns)); } @@ -1800,8 +1748,6 @@ impl Analyser { self.pending.truncate(pending); return None; }; - let checks = self.expected_of_any(¶meters, arguments, nodes); - self.pending.checks.extend(checks); let iterations = self.iterations_waited_for(¶meters, arguments); self.pending.iterations.extend(iterations); Some(typ) @@ -1860,9 +1806,6 @@ impl Analyser { Some(site) => self.record_uses(site, call), None => self.pending.uses.clear(), } - for (node, expected) in std::mem::take(&mut self.pending.checks) { - self.expect(node, expected); - } for (sequence, element) in std::mem::take(&mut self.pending.iterations) { self.wait(Constraint::Iterate { sequence, @@ -1957,25 +1900,6 @@ impl Analyser { .collect() } - /// Where each argument of type `Any` that `nodes` has a node for is - /// checked against the parameter it was passed for. - pub(super) fn expected_of_any( - &self, - parameters: &[StaticType], - arguments: &[StaticType], - nodes: &[Option], - ) -> Vec<(NodeId, StaticType)> { - parameters - .iter() - .zip(arguments) - .zip(nodes) - .filter_map(|((parameter, argument), node)| { - let node = (*node)?; - (self.vars.resolve(argument) == StaticType::Any).then(|| (node, parameter.clone())) - }) - .collect() - } - /// The type of calling a value whose type isn't known yet, like `g` in /// `fn(g) => g(1)`: the value is a function taking these arguments. A /// value that can't be one, like `g` in `g(g)`, which would have to take @@ -2005,9 +1929,7 @@ impl Analyser { return_type, } = self.resolve_overloads(&call.candidates, arguments, call.kind); let fits = !matches!(binding, Binding::None) - && self - .commit_call(&binding, arguments, &call.nodes.arguments, return_type) - .is_some(); + && self.commit_call(&binding, arguments, return_type).is_some(); self.vars.rollback(snapshot); self.pending.clear(); fits @@ -2074,7 +1996,7 @@ impl Analyser { let committed = self.commit_early(&call, &arguments, narrowest); if let Some(typ) = &committed { let result = call.result.clone(); - self.check_write(&result, typ, call.span, call.nodes.result); + self.check_write(&result, typ, call.span); } self.copied_through = outer; if committed.is_none() { diff --git a/ndc_analyser/src/types.rs b/ndc_analyser/src/types.rs index 3bcdac80..08c9ca2a 100644 --- a/ndc_analyser/src/types.rs +++ b/ndc_analyser/src/types.rs @@ -391,8 +391,8 @@ impl TypeVars { } } for (literal, held) in literals { - // A value of type `Any` is checked as whatever the others are, so - // it shares their type. + // A literal holding a value of type `Any` holds `Any`s, which + // every value is. if self.plainly_shared(&held) || held .iter() diff --git a/ndc_analysis/src/lib.rs b/ndc_analysis/src/lib.rs index fa9dda7f..3d8644f1 100644 --- a/ndc_analysis/src/lib.rs +++ b/ndc_analysis/src/lib.rs @@ -52,12 +52,6 @@ pub struct Analysis { /// the cast. `None` when the value is already known to have the type, as /// in `1 as Int`, so there is nothing to check. pub cast_checks: AHashMap>, - /// The type a value of type `Any` is checked against as the program - /// runs, where code that expects that type gets it, keyed by where it's - /// checked: an expression, checked once it's computed, or an assignment - /// target or a name a pattern declares, checked before the value is - /// stored. In `let n: Int = a` with `a: Any`, `n` checks for an Int. - pub type_checks: AHashMap, /// How many local slots each function's own variables use, parameters /// first, keyed by its declaration. The compiler places its temporaries /// after them. @@ -91,8 +85,6 @@ pub struct Instance { pub setters: AHashMap, /// As [`Analysis::augmented_assignments`], for the instance's body. pub augmented_assignments: AHashMap, - /// As [`Analysis::type_checks`], for the instance's body. - pub type_checks: AHashMap, /// As [`Analysis::function_types`], for the functions the instance's /// body declares, with the instance's types. pub function_types: AHashMap, diff --git a/ndc_core/src/static_type.rs b/ndc_core/src/static_type.rs index d1fc294a..af407e0a 100644 --- a/ndc_core/src/static_type.rs +++ b/ndc_core/src/static_type.rs @@ -346,24 +346,16 @@ impl StaticType { return_type: pr, }, ) => { - // What a function value takes and returns isn't checked as - // the program runs, so whether it might fit only depends on - // what both types know: `fn(x) => x > 1`, typed `Fn(Any) -> - // Any` where nothing says what `x` is, might be a predicate. - let part = |a: &Self, p: &Self| { - matches(a, p) - || (unknowns_match && (matches!(a, Self::Any) || matches!(p, Self::Any))) - }; let parameters = match (a, p) { // A function value whose parameters aren't known fits // a function type that says nothing about them either. (_, None) => true, (None, Some(_)) => unknowns_match, (Some(a), Some(p)) => { - a.len() == p.len() && a.iter().zip(p).all(|(a, p)| part(a, p)) + a.len() == p.len() && a.iter().zip(p).all(|(a, p)| matches(a, p)) } }; - parameters && part(ar, pr) + parameters && matches(ar, pr) } _ => self == parameter, } diff --git a/ndc_layout/src/lib.rs b/ndc_layout/src/lib.rs index 4c666a0e..d279393a 100644 --- a/ndc_layout/src/lib.rs +++ b/ndc_layout/src/lib.rs @@ -49,8 +49,6 @@ pub struct InstancePicks { pub getters: AHashMap>, pub setters: AHashMap>, pub augmented_assignments: AHashMap>, - /// As [`Analysis::type_checks`], for the instance's body. - pub type_checks: AHashMap, /// As [`Analysis::function_types`], for the functions the instance's /// body declares, with the instance's types. pub function_types: AHashMap, @@ -64,7 +62,6 @@ impl InstancePicks { getters: AHashMap::new(), setters: AHashMap::new(), augmented_assignments: AHashMap::new(), - type_checks: AHashMap::new(), function_types: AHashMap::new(), } } @@ -165,7 +162,6 @@ impl<'a> Layout<'a> { .into_iter() .map(locate_binding) .collect(), - type_checks: picks.type_checks, function_types: picks.function_types, }; analysis diff --git a/ndc_stdlib/src/sequence.rs b/ndc_stdlib/src/sequence.rs index 6d5c74aa..2b617db6 100644 --- a/ndc_stdlib/src/sequence.rs +++ b/ndc_stdlib/src/sequence.rs @@ -630,7 +630,9 @@ mod inner { )) } - fn string_prefixes(text: &str) -> Value { + /// Returns all prefixes of the string as a list of strings. + #[function(name = "prefixes", return_type = Vec)] + pub fn prefixes_string(text: &str) -> Value { Value::list( text.chars() .scan(String::new(), |acc, c| { @@ -641,21 +643,9 @@ mod inner { ) } - /// Returns all prefixes of the string as a list of strings. - #[function(name = "prefixes", return_type = Vec)] - pub fn prefixes_string(text: &str) -> Value { - string_prefixes(text) - } - - /// Returns all prefixes of a sequence, each as a list; for strings, returns string prefixes. + /// Returns all prefixes of a sequence, each as a list. #[function(signature = "(Iterable) -> List>")] pub fn prefixes(seq: SeqValue) -> anyhow::Result { - // A sequence-typed caller can still supply a string. - if let Value::Object(ref obj) = seq - && let Object::String(s) = obj.as_ref() - { - return Ok(string_prefixes(&s.borrow())); - } Ok(Value::list( seq.try_into_iter() .ok_or_else(|| anyhow!("prefixes requires a sequence"))? @@ -667,7 +657,9 @@ mod inner { )) } - fn string_suffixes(text: &str) -> Value { + /// Returns all suffixes of the string as a list of strings. + #[function(name = "suffixes", return_type = Vec)] + pub fn suffixes_string(text: &str) -> Value { Value::list( text.char_indices() .map(|(i, _)| Value::string(text[i..].to_string())) @@ -675,21 +667,9 @@ mod inner { ) } - /// Returns all suffixes of the string as a list of strings. - #[function(name = "suffixes", return_type = Vec)] - pub fn suffixes_string(text: &str) -> Value { - string_suffixes(text) - } - - /// Returns all suffixes of a sequence, each as a list; for strings, returns all trailing substrings. + /// Returns all suffixes of a sequence, each as a list. #[function(signature = "(Iterable) -> List>")] pub fn suffixes(seq: SeqValue) -> anyhow::Result { - // A sequence-typed caller can still supply a string. - if let Value::Object(ref obj) = seq - && let Object::String(s) = obj.as_ref() - { - return Ok(string_suffixes(&s.borrow())); - } let out: Vec = seq .try_into_iter() .ok_or_else(|| anyhow!("suffixes requires a sequence"))? diff --git a/ndc_vm/src/chunk.rs b/ndc_vm/src/chunk.rs index c93e80e8..6e1dd678 100644 --- a/ndc_vm/src/chunk.rs +++ b/ndc_vm/src/chunk.rs @@ -157,9 +157,6 @@ pub enum OpCode { /// Pops a value, pushes `Some(value)` if it conforms to the type and /// `None` if it doesn't. `[… value → … option]` TryCast(Rc), - /// Peeks top, a value of type `Any` where code expects the type; errors - /// if it isn't one. `[… value → … value]` - ExpectType(Rc), } impl OpCode { @@ -198,7 +195,6 @@ impl OpCode { Self::Memoize => "Memoize", Self::CheckType(_) => "CheckType", Self::TryCast(_) => "TryCast", - Self::ExpectType(_) => "ExpectType", } } @@ -254,7 +250,7 @@ impl OpCode { Self::NewCell(_) => 0, Self::Memoize => 0, // Peeks the value without popping it. - Self::CheckType(_) | Self::ExpectType(_) => 0, + Self::CheckType(_) => 0, // Replaces the value with the option holding it. Self::TryCast(_) => 0, } @@ -312,7 +308,6 @@ impl std::fmt::Debug for OpCode { Self::Memoize => write!(f, "Memoize"), Self::CheckType(typ) => write!(f, "CheckType({typ})"), Self::TryCast(typ) => write!(f, "TryCast({typ})"), - Self::ExpectType(typ) => write!(f, "ExpectType({typ})"), } } } diff --git a/ndc_vm/src/compiler.rs b/ndc_vm/src/compiler.rs index 92865ae0..8eb54e03 100644 --- a/ndc_vm/src/compiler.rs +++ b/ndc_vm/src/compiler.rs @@ -317,12 +317,10 @@ impl Compiler { self.compile_expr(*container)?; self.compile_expr(*index)?; self.compile_expr(*value)?; - self.expect_type(l_value.id, span); self.ir.write(OpCode::Call(3), span); } AssignmentTarget::Identifier { span: lv_span, .. } => { self.compile_expr(*value)?; - self.expect_type(l_value.id, lv_span); self.emit_set_var(self.assigned_variable(l_value.id), lv_span); self.emit_unit(); } @@ -342,14 +340,12 @@ impl Compiler { self.compile_binding(&self.setter(l_value.id), member_span)?; self.compile_expr(*receiver)?; self.compile_expr(*value)?; - self.expect_type(l_value.id, member_span); self.ir.write(OpCode::Call(2), member_span); } }, Expression::OpAssignment { l_value, r_value, .. } => { - let target_id = l_value.id; let target = PreparedAssignmentTarget::prepare(self, l_value)?; let Some(binding) = self.augmented_assignment(id) else { return Err(CompileError::unresolved_binding(span)); @@ -360,7 +356,6 @@ impl Compiler { target.emit_read(self)?; self.compile_expr(*r_value)?; self.ir.write(opcode, span); - self.expect_type(target_id, span); target.emit_store(self)?; self.emit_unit(); @@ -557,27 +552,12 @@ impl Compiler { } } - if leaves_value { - self.expect_type(id, span); - } // Every expression except a statement leaves exactly one value. self.ir .set_stack_depth(entry_depth + isize::from(leaves_value)); Ok(()) } - /// Checks the value on top of the stack, which comes from `Any`, where - /// the analyser says code at `at` expects a type. - fn expect_type(&mut self, at: NodeId, span: Span) { - if let Some(expected) = self - .instance_pick(|instance| instance.type_checks.get(&at)) - .or_else(|| self.analysis.type_checks.get(&at)) - { - let expected = Rc::new(expected.clone()); - self.ir.write(OpCode::ExpectType(expected), span); - } - } - fn compile_assignment_target( &mut self, l_value: AssignmentTargetLocation, @@ -585,7 +565,6 @@ impl Compiler { ) -> Result<(), CompileError> { match l_value.target { AssignmentTarget::Identifier { span: lv_span, .. } => { - self.expect_type(l_value.id, lv_span); self.emit_set_var(self.assigned_variable(l_value.id), lv_span); } AssignmentTarget::Index { value, index } => { @@ -596,7 +575,6 @@ impl Compiler { // 4. Call []= function // 5. Pop the return value - self.expect_type(l_value.id, span); let tmp_value = self.allocate_temp(); self.write_temp(tmp_value, span, OpCode::SetLocal); @@ -631,7 +609,6 @@ impl Compiler { let variable = self.analysis.variable(pattern.id).expect( "internal error: every binding pattern is analysed before it's compiled", ); - self.expect_type(pattern.id, span); self.emit_declare(variable, span); } BindingPattern::Sequence(seq) => { @@ -992,10 +969,9 @@ impl Compiler { // instance's picks. A generic function is declared at the top level, // so it captures nothing and is a global, and its instances are // stored in globals of their own. A `pure fn`'s instance has a cache - // of its own, as instances may check values of type `Any` against - // their own types: `pure fn has(xs, v) { xs.contains(v) }` checks `v` - // for an Int in its instance for `List`, but not for - // `List`, and both may be called with an empty list. + // of its own, as arguments of different types can be equal: `f(1)` + // and `f(1.0)` call different instances, which may return different + // values. for (instance, instance_body) in instances.iter().zip(instance_bodies) { let idx = self .ir diff --git a/ndc_vm/src/vm.rs b/ndc_vm/src/vm.rs index 732d5c1d..fc1ef509 100644 --- a/ndc_vm/src/vm.rs +++ b/ndc_vm/src/vm.rs @@ -240,15 +240,6 @@ impl Vm { } } OpCode::TryCast(target) => try_cast(&mut self.stack, target), - OpCode::ExpectType(target) => { - if !convert_top(&mut self.stack, target) { - let value = self.stack.last().expect("stack underflow"); - return Err(VmError::new( - format!("expected {target} but found {}", value.diagnostic_type()), - span, - )); - } - } OpCode::GetLocal(slot) => { let slot = *slot; self.stack.push(self.stack[frame.slot(slot)].clone()); @@ -976,9 +967,10 @@ impl Vm { /// code stays at the same shape and footprint. A parameter like /// `List` isn't checked element by element on that path, so when no /// candidate accepts the arguments that way, they're checked again - /// element by element before the call fails, if `look_inside`: a struct - /// with a `List` field can be made through `Any` with a list of - /// Ints. + /// element by element before the call fails, if `look_inside`: in + /// `fn h(xs) => g(xs)` passed as a value, as in `[[1]].map(h)`, the body + /// compiled for arguments of any type calls `g(List)` with a list + /// of Ints. fn find_scalar_overload( &self, scalars: &[ResolvedVar], diff --git a/tests/compiler/tests/compiler.rs b/tests/compiler/tests/compiler.rs index f287e71b..6eaea4d0 100644 --- a/tests/compiler/tests/compiler.rs +++ b/tests/compiler/tests/compiler.rs @@ -644,9 +644,7 @@ fn a_proven_optional_cast_is_always_some() { fn an_any_cast_for_a_typed_variable_is_checked_once() { let ops = compile_with_analysis("let a: Any = 1; let n: Int = a as Int;"); assert_eq!( - ops.iter() - .filter(|op| matches!(op, CheckType(_) | ExpectType(_))) - .count(), + ops.iter().filter(|op| matches!(op, CheckType(_))).count(), 1, "expected a single CheckType(Int) guard, got: {ops:?}", ); @@ -659,7 +657,7 @@ fn an_any_cast_for_a_typed_variable_is_checked_once() { fn a_typed_value_stored_in_a_typed_variable_is_not_checked() { let ops = compile_with_analysis("let n: Int = 1; let m: Int = n;"); assert!( - ops.iter().all(|op| !matches!(op, ExpectType(_))), + ops.iter().all(|op| !matches!(op, CheckType(_))), "a value of a known type must not be checked, got: {ops:?}", ); }