From cd605366716c459dcd78a520cb96b41d45d41610 Mon Sep 17 00:00:00 2001 From: Tim Fennis Date: Fri, 13 Nov 2026 11:06:36 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20address=20small=20findings=20from=20the?= =?UTF-8?q?=20type=20checker=20review=20=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015rQP9SKVmpvME4F2LXeWmG --- manual/src/reference/types/number.md | 10 ++++++ ndc_resolver/src/name_resolution.rs | 4 +-- ndc_stdlib/src/sequence.rs | 36 +++++++++---------- ndc_vm/src/compiler.rs | 30 +++++++++++----- .../024_zip_takes_any_kind_of_sequence.ndc | 4 +++ 5 files changed, 54 insertions(+), 30 deletions(-) create mode 100644 tests/functional/programs/603_stdlib_seq/024_zip_takes_any_kind_of_sequence.ndc diff --git a/manual/src/reference/types/number.md b/manual/src/reference/types/number.md index afe4c9bf..df2a998c 100644 --- a/manual/src/reference/types/number.md +++ b/manual/src/reference/types/number.md @@ -53,6 +53,16 @@ print([1.5, 2.25].fold(0, fn(acc, x) => acc + x)); // 3.75 A value that's already an `Int`, like the result of `len`, stays one. +A variable has one type, which all its uses decide, including the ones after it's read. So a later use can make a literal a `Float`, and `/` and `\` then divide `Float`s rather than truncate: + +```ndc +let x = 7; +print(x / 2); // 3.5 +x = 2.5; // x is a Float, so the 7 is too +``` + +Write `7.0` or annotate `let x: Int = 7` to say which one is meant. + Arbitrary-radix literals such as `16r2a` remain `Int` literals and do not accept `n`. The `i` and `j` suffixes create complex `Number` values: ```ndc diff --git a/ndc_resolver/src/name_resolution.rs b/ndc_resolver/src/name_resolution.rs index bbdab23b..5af00797 100644 --- a/ndc_resolver/src/name_resolution.rs +++ b/ndc_resolver/src/name_resolution.rs @@ -475,8 +475,6 @@ impl NameResolver { natives.into_iter().map(|(&symbol, _)| symbol).collect() } - /// The symbol a resolved declaration introduced: a binding pattern identifier's - /// or a named function's, keyed by its node. /// Declares a top-level value named `name` that no code refers to by /// name, as no identifier can be spelled like it: an instance of a generic /// function, which the analyser picks for the calls that use it. Being @@ -488,6 +486,8 @@ impl NameResolver { symbol } + /// The symbol a resolved declaration introduced: a binding pattern identifier's + /// or a named function's, keyed by its node. pub fn declared_symbol(&self, node: NodeId) -> SymbolId { self.declarations[&node] } diff --git a/ndc_stdlib/src/sequence.rs b/ndc_stdlib/src/sequence.rs index d916e3c9..56a3c0cd 100644 --- a/ndc_stdlib/src/sequence.rs +++ b/ndc_stdlib/src/sequence.rs @@ -985,26 +985,24 @@ pub mod extra { "zip must be called with 2 or more arguments".to_string(), )); } - let iters: Option>> = args + // The iterators are stepped together, so an endless one like `1..` + // stops with the others. + let mut iters = args .iter() - .map(|arg| match arg { - Value::Object(obj) => match obj.as_ref() { - Object::List(l) => Some(l.borrow().clone()), - Object::Tuple(t) => Some(t.clone()), - _ => None, - }, - _ => None, - }) - .collect(); - let iters = iters.ok_or_else(|| VmError::native("zip requires sequences".to_string()))?; - let min_len = iters.iter().map(|v| v.len()).min().unwrap_or(0); - let result: Vec = (0..min_len) - .map(|i| { - Value::Object(Rc::new(Object::Tuple( - iters.iter().map(|v| v[i].clone()).collect(), - ))) - }) - .collect(); + .map(|arg| arg.clone().try_into_iter()) + .collect::>>() + .ok_or_else(|| VmError::native("zip requires sequences".to_string()))?; + let mut result = Vec::new(); + 'zipping: loop { + let mut tuple = Vec::with_capacity(iters.len()); + for iter in &mut iters { + let Some(value) = iter.next() else { + break 'zipping; + }; + tuple.push(value); + } + result.push(Value::Object(Rc::new(Object::Tuple(tuple)))); + } Ok(Value::Object(Rc::new(Object::list(result)))) } } diff --git a/ndc_vm/src/compiler.rs b/ndc_vm/src/compiler.rs index 200a51ba..cdf272f6 100644 --- a/ndc_vm/src/compiler.rs +++ b/ndc_vm/src/compiler.rs @@ -13,6 +13,14 @@ use ndc_parser::{ use std::cell::{Cell, RefCell}; use std::rc::Rc; +/// Where an instance of a generic function is in [`Analysis::instances`]: +/// the declaration it's an instance of, and which of its instances it is. +#[derive(Clone, Copy)] +struct InstanceAt { + declaration: NodeId, + index: usize, +} + pub struct Compiler { ir: OptimizerIr, source_locals: usize, @@ -25,7 +33,7 @@ pub struct Compiler { analysis: Rc, /// The instance of a generic function whose body is being compiled, /// whose picks take the place of the function's own. - instance: Option>, + instance: Option, /// Whether the code compiled so far used a pick of `instance` that /// differs from the function's own, so it differs from what the /// function's own body compiles to. @@ -633,7 +641,10 @@ impl Compiler { &'a self, pick: impl FnOnce(&'a Instance) -> Option<&'a T>, ) -> Option<&'a T> { - let found = self.instance.as_deref().and_then(pick); + let found = self + .instance + .map(|at| &self.analysis.instances[&at.declaration][at.index]) + .and_then(pick); if found.is_some() { self.used_instance.set(true); } @@ -879,9 +890,12 @@ impl Compiler { (self.prototype(id, name, body)?, Vec::new()) } else { let compiled = self.prototype(id, name.clone(), body.clone())?; - let instance_bodies = instances - .iter() - .map(|instance| { + let instance_bodies = (0..instances.len()) + .map(|index| { + let instance = InstanceAt { + declaration: id, + index, + }; self.compile_function_body(id, name.clone(), body.clone(), Some(instance)) .map(|(compiled, _)| compiled) }) @@ -991,7 +1005,7 @@ impl Compiler { id: NodeId, name: Option, body: ExpressionLocation, - instance: Option<&Instance>, + instance: Option, ) -> Result<(CompiledFunction, bool), CompileError> { let static_type = self.analysis.function_type(id).clone(); let mut fn_compiler = Self { @@ -1003,9 +1017,7 @@ impl Compiler { allow_return: true, optimize: self.optimize, analysis: Rc::clone(&self.analysis), - instance: instance - .map(|instance| Rc::new(instance.clone())) - .or_else(|| self.instance.clone()), + instance: instance.or(self.instance), used_instance: Cell::new(false), prototypes: Rc::clone(&self.prototypes), }; diff --git a/tests/functional/programs/603_stdlib_seq/024_zip_takes_any_kind_of_sequence.ndc b/tests/functional/programs/603_stdlib_seq/024_zip_takes_any_kind_of_sequence.ndc new file mode 100644 index 00000000..f921aac6 --- /dev/null +++ b/tests/functional/programs/603_stdlib_seq/024_zip_takes_any_kind_of_sequence.ndc @@ -0,0 +1,4 @@ +// `zip` takes any sequence, and stops at the shortest, so an endless range +// can be one of them. +// expect-output: [("a","c"),("b","d")] [(1,4),(2,5)] [(1,3,"x"),(2,4,"y")] +print(zip("ab", "cd"), zip(1..3, 4..), zip([1, 2], (3, 4), "xy"));