Repository navigation
Conversation
A variable now keeps the type its first value or its annotation gave it. This is step 1 of the type system RFC (#281). Before, an assignment of another type widened the variable to a join of the two types, but calls already resolved against the narrower type kept their pick (#279). Behaviour changes: - Assigning a value that doesn't fit is an error: `let x = 1; x = 2.5` reports "mismatched types: found Float but expected Int". The same goes for augmented assignment (`x += 0.5`), loop variables, destructuring assignment, and index writes, which no longer widen their container (`let xs = [1]; xs[0] = "a"`). - What an empty `[]` or `%{}`, or a `None`, will hold is settled by the first write: after `let m = %{}; m["a"] = 1`, `m` is a `Map<String, Int>`, and `m["b"] = "two"` is an error. When nothing settles it, it keeps the type it had before (`List<Any>`, `Map<Any, ()>`, `Option<Any>`). In-place operations settle them too: after `let xs = []; xs ++= [1, 2]`, `xs` is a `List<Int>`. Only writes and in-place operations settle these types for now; `xs.push(1)` doesn't, because `push` isn't generic yet. - An annotated `let` is checked the same way, so `let o: Option<Int> = None` and `let p: (Int, Option<Int>) = (1, None)` are accepted. The annotation check used to reject both. - A value of type `Any` can be stored anywhere, since `Any` is the dynamic type. That keeps `sum += value` working while iterating a map whose types aren't known. - `let pos = (); pos = (1, 3)` is now an error. A variable that gets its value later holds an `Option`: `let pos = None; pos = Some((1, 3))`. Internals: - `StaticType::Var` is a type variable. Only the analyser creates them, and every method in `ndc_core` treats one as `Any`. The analyser settles its variables with unification (`TypeVars` in `types.rs`). Variables made in a function body fall back to their default when the body ends, so a function's type never carries one. Every remaining variable falls back at the end of each batch, before the compiler or the language server see the analysis; a debug assertion checks that none is left. - `TypeBinding` is gone. Inferred and annotated types behave the same now, so the type table just maps symbols to types. Programs and tests written for widening now state the new rules. The manual's sections on reassignment, augmented assignment and casts describe them, and every example in those sections was run through the interpreter. Known gap until step 2: a call doesn't settle the types of its arguments, because built-in signatures aren't generic yet. In `let xs = []; xs.push("a"); xs[0] = 1`, the push leaves `xs`'s element type unsettled, and the write then settles it as `Int`. Step 2 unifies every argument with the picked overload's signature, which closes this. Fixes #279. Closes #143: the reassignment in its example is now reported where it happens. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_015rQP9SKVmpvME4F2LXeWmG --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Built-in functions can now declare generic signatures, and a call works out what their type parameters stand for from its arguments. This is step 2a of the type system RFC (#281). It also closes the gap #286 left: a call now settles the types of its arguments. Declaring a signature: - `#[function(signature = "<T>(List<T>, T)")]` replaces the types read off the Rust parameters. The macro parses the signature with `ndc_core`'s parser while it expands, so a malformed signature or a wrong number of parameters is a compile error at the attribute. Hand-registered natives use `StaticType::signature("…")`. - The syntax is the language's own: `<K, V>(Map<K, V>, K) -> V`, tuples `(A, B)` and `(A,)`, `Fn(T) -> U`, and `(*)` for any arguments (not allowed in the macro, whose functions take a fixed number). Without `-> …` a function returns `()`. How calls use it: - Overload resolution still picks the overload as before, treating a type parameter like `Any`. The picked signature then gets fresh type variables for its parameters, and every argument is bound to its parameter. `xs.push(1)` settles an empty `xs` as `List<Int>`, and the call's type is the instantiated return type (`xs.first()` is an `Int`). - An argument that doesn't fit is an error: `[1].push(1.0)` reports "No function called 'push' found that matches the arguments 'List<Int>, Float'" (#129). - A parameter of type `Any`, or a call whose overload is only picked at runtime, settles what's unknown in its arguments as `Any`, because the function may store anything in them. A call picked at runtime has `Any` in place of its candidates' type parameters. - Index writes are calls too: `xs[i] = v` commits the `[]=` signature, with the container's element type standing in for the value, and `m[k] += v` also commits `[]` to read the old value. A failed read is reported once. - Calling a function value, as in `fs[0](x)`, binds its arguments to the function's parameter types, like a named call. A generic built-in used as a value (`let p = push`) gets one instantiated type there. - At runtime, a type parameter matches any value, like `Any`. Converted so far: the list module, and the `[]`, `[]=`, `++` and `++=` operators for lists and maps. A list is indexed by an `Int` (giving a `T`), by a range (giving a `List<T>`), or by an index of unknown type (giving `Any`), so an untyped index keeps the allocation-free path. The rest of the stdlib follows in 2b. Behaviour changes: - `++` and `++=` on lists keep one element type, so `[1] ++ ["a"]` is an error. A `List<Any>` accepts both. - A map is read and written with keys of its key type: after `let m = %{}; m[1] = 2`, `m["a"] = 3` is an error. - A map whose default is a function, as in `%{:fn() => []}`, has the function's return type as its value type, because the function is called to make each missing value. - Calling a function value with arguments it doesn't accept is an error: "A Function(Int) -> Int doesn't accept the arguments 'String'". - `pop`, `pop_left`, `first` and `last` are typed to return `T`. On an empty list `pop` and `pop_left` return `()` at runtime, as before. - `xs.remove(xs.len())` reports "index … is out of bounds" instead of panicking. Still to come: - In-place operators keep the rule from #286 that settles their target from the right operand, because they don't commit a signature yet. That comes with constraint solving in step 3, which also lets overload matching itself see through type variables. - Until 2b converts them, built-ins that take `Sequence<Any>` or `Any` settle a not-yet-known list as `List<Any>`, so `let xs = []; xs.len; xs.push(1); xs.push("a")` is accepted. - An annotated map with a function default, `let m: Map<String, List<Int>> = %{:fn() => []}`, is rejected, because the closure doesn't see the expected type. Expected function types come in 2c. - Binding a type parameter is order-dependent where subtyping still applies; that goes with subtyping in step 4. Tests and programs that relied on the old types were updated: cyclic lists are annotated `List<Any>`, maps mixing numeric key types are annotated `Map<Any, String>`, and two map tests use keys of one type. The manual's section on reassignment describes how calls settle types. While checking the manual I found two examples that don't run on `feat/type-checker`, before this change too: tuple concatenation with `++=` in `tuple.md`, and the closure example in `function.md`, whose `x.sum()` finds no overload for a `List<Any>`. Closes #129. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_015rQP9SKVmpvME4F2LXeWmG --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The rest of the standard library now declares generic signatures, so element, key and value types flow through calls the way they do for lists since #287. This is step 2b of the type system RFC (#281). A parameter that accepts any value and only reads it now has its own type parameter instead of `Any`, so passing a value whose type isn't settled yet no longer settles it as `Any`. For example `==` and `<` are `<A, B>(A, B) -> Bool`, and `len` is `<T>(Sequence<T>) -> Int`. So `let xs = []; print(xs == [], xs.len); xs.push(1); xs.push("a")` now rejects the second push. Functions whose result really is unknown, like `json_decode`, still return `Any`. What changed, by area: - Sequences: `map` returns `List<U>` of what its function returns. `filter`, `sorted`, `take` and `find` keep the element type. `fold` and `reduce` return the accumulator type, `enumerate` gives `(Int, T)`, and `max`/`min` give `T`. `zip` of two sequences gives `List<(A, B)>`; zipping three or more lists, which was wrongly typed as pairs, is `List<Any>`. `in`, `max` and `min` gained map overloads, because at runtime they look at a map's keys. - Containers: deque, heap and map functions are generic. `keys` is `List<K>`, `values` is `List<V>`, and a deque is indexed and sliced like a list. `set` makes a `Map<T, ()>`. `union`/`|` and `~` keep one map type, while `&`, `intersection` and `-=` ignore the right operand's values. - Values and strings: `Some` is `<T>(T) -> Option<T>`, `unwrap` is `<T>(Option<T>) -> T`, and `unwrap_or` takes a default of the option's type. `clone` and `deepcopy` keep their argument's type, `frequencies` makes a `Map<T, Int>`, and comparisons, `string`, `<>`, `assert_eq` and `json_encode` take their own type parameters. `captures` was declared `List<String>` but returns one list per match, `List<List<String>>`. Behaviour changes: - Mixing types where a signature keeps one is an error: pushing a `String` onto a deque of `Int`, `Some(1).unwrap_or("x")`, `"a" in (1..10)`, or `|` between a map and a set. Programs that mix types on purpose annotate the container, e.g. `MinHeap<Any>`. - `split_once`, `strip_prefix`, `strip_suffix` and `strip_circumfix` returned `()` when the pattern didn't match, although they were typed to return a string. They now report the mismatch as an error (`"hello" doesn't start with "x"`), and the new `split_once?`, `strip_prefix?`, `strip_suffix?` and `strip_circumfix?` return an `Option`. The manual's string page describes both. - `max(a, b)` and `min(a, b)` on two values still return `Any`, because `<T>(T, T) -> T` would reject `max(1, 1.5)`. Kept less precise than the RFC wants, because of what the analyser and VM can do today: - Function parameters are `Fn(*) -> U`, not `Fn(T) -> Bool`. Overload matching treats function parameters contravariantly, so `Fn(T)` rejects `fn(x: Int) => …`. And runtime dispatch can't match a lambda that returns `Any` against a concrete return type. This needs expected function types (2c) and the call constraints of step 3. - Inserting into a set, `insert(set, key)`, takes `Map<K, Any>` rather than `Map<K, ()>`, and `transposed`, `multi_cartesian_product` and `flat_map` return lists of `Any`. Runtime dispatch never matches a container parameter whose element type is known. - `combinations` and `permutations` yield tuples whose length is a runtime value, so their elements stay `Any`. - `-=` and `&=` with a set on the right of a map are still rejected by the in-place eligibility check, which is deferred to the work on `op=`. Programs that mixed types on purpose were annotated (`Map<Any, ()>`, `MinHeap<Any>`, `MaxHeap<Any>`), and the range test binds its wrongly typed values to `Any` so it still checks the runtime error. The cast examples in the manual and two cast tests no longer use `keys` as their source of a `List<Any>`, since `keys` is now precise. Apart from the string examples changed above, all manual examples print the same output as before, and every benchmark program runs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_015rQP9SKVmpvME4F2LXeWmG --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Function types can now be written in annotations, and a function passed as an argument is checked against the function type it's passed for. This is the first part of step 2c of the type system RFC (#281). Writing function types: - `Fn(A, B) -> C` is a function taking an `A` and a `B` and returning a `C`; without `-> …` it returns `()`. It works in every annotation: `let f: Fn(Int) -> Int = …`, parameters, return types (`fn make() -> Fn(Int) -> Int`) and nested function types. - Types are shown the same way in messages: `Fn(Int) -> Int` instead of `Function(Int) -> Int`. Passing functions: - A function passed for a function parameter has to take what the parameter passes and return what it returns, where both types know it. An unannotated lambda, `Fn(Any) -> Any`, fits any `Fn(Int) -> Int` and is checked as the program runs. `fn(x: String) => 1`, `fn(x) => "a"` and `fn(a, b) => 1` are rejected where an `Fn(Int) -> Int` is expected. - A type parameter in an expected function type, like `T` in `Fn(T) -> U`, fits a typed lambda. The lambda's parameter types then settle it, so `xs.map(fn(x: Int) => x)` makes an empty `xs` a `List<Int>`, and `[1].map(fn(x: String) => x)` is an error. - Calling a function value through a variable or an expression checks its arguments as before, now with the function types an annotation gives. - Runtime dispatch checks only that a function argument takes the right number of arguments, because a function's types can't be checked while the program runs. The stdlib's higher-order functions now declare their real function types, instead of `Fn(*)`, which #288 had to use: - `map<T, U>(Sequence<T>, Fn(T) -> U) -> List<U>` and `flat_map<T, U>(Sequence<T>, Fn(T) -> Sequence<U>) -> List<U>`. - `filter`, `count`, `find`, `locate`, `any`, `all` and `none` take an `Fn(T) -> Bool`, which they already required at runtime. So `[1, 2].filter(fn(x) => 1)` is now an error. - `fold<T, A>(Sequence<T>, A, Fn(A, T) -> A) -> A`, `reduce<T>(Sequence<T>, Fn(T, T) -> T) -> T`, and comparators like `max_by` and `sort_by` take an `Fn(T, T) -> U`. Overload matching only lets a signature's type parameters fit any function parameter. An `Any` argument still means the overload is picked at runtime. A first version that let every unknown type fit made `Any` arguments match overloads exactly, and failed 10 tests. A test map whose keys are two getters of different types now declares `Map<Any, String>`. The join of two function types picks the first one; the RFC turns such mixed literals into `Any` in step 4. The manual's function page has a section on function types. Comparing all manual examples against `feat/type-checker`, the only differences are the new examples, plus a map loop whose iteration order changes between runs on both builds. Next in 2c: lambdas take their parameter types from the function type they're passed for, so an unannotated `fn(x) => x + 1` in `xs.map(…)` gets the element type. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_015rQP9SKVmpvME4F2LXeWmG --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A lambda now takes its parameter types from a function type that's expected where it's written, when that type is known before the lambda is analysed. This is part of step 2c of the type system RFC (#281). Where a lambda gets an expected function type: - As the value of an annotated variable or of an assignment: in `let f: Fn(Int) -> Int = fn(x) => x + 1`, `x` is an `Int`. - As an argument to a function value, `fs[0](fn(x) => …)`, from the callee's type, which is known before its arguments are analysed. - As a map's default function, which is expected to return the map's value type. So `let m: Map<String, List<Int>> = %{:fn() => []}` is accepted now; the #287 review found it was rejected. With an expected type, an unannotated parameter takes the expected parameter's type, unless that type is `Any`. The body is checked against the expected return type, so an empty literal returned from it adopts that type. Not included: a lambda passed to a named function, as in `xs.map(fn(x) => x + 1)`, keeps `Any` parameters. A first version gave it the parameter types of the overload its call picks. That needed a stand-in for the lambda during overload matching, and a rule for when the stand-in can't tell overloads apart, which a review then had to patch. Step 3's call constraints solve this without either, so that part was taken out again. Behaviour changes: a lambda body with an expected type is checked with real parameter types, so `let f: Fn(Int) -> Int = fn(x) => x ++ "!"` is reported before the program runs. Internals: `analyse_function_declaration` takes the declaration's parts as a `FunctionDeclaration` struct, plus an optional expected type, and `analyse_with_expected` has an arm for lambdas. The manual's function types section describes where lambdas get their types, and that lambdas passed to named functions don't yet. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_015rQP9SKVmpvME4F2LXeWmG --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: aae3152384
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… ⏳ (#292) Step 3a of RFC #281 ("Step 3 in detail"): the constraint solver, without generic functions yet. Until now each call picked its overload when the analyser reached it, using the argument types known at that moment. Information that arrived later was handled with special cases: the expected type passed into lambdas, the stand-in element type for index writes, and the literal arms of `analyse_with_expected`. Now a call whose arguments still contain type variables waits: - Elimination: the overloads that can't take what's known are crossed off. When one is left, the call commits to it, which settles its arguments from that overload's parameters. It doesn't commit to an overload that fits only because an `Any` parameter takes an argument of another type, since `Any` only loses to an exact match once the arguments are known: `xs[0] = []` on a `List<Int>` is still an error. - Forward: once the arguments are known, the call is resolved as before. - A waiting call is retried when a variable it waits on is settled. When a named function or the batch ends, defaults decide what they can first: a call still waiting takes its narrowest overload if every overload that fits is in one scope and none of its arguments is wholly unknown (`m["k"]` on an empty map picks `[](Map<K, V>, K)` over `[](Any, Any)`), and `a op= b` keeps the type of `b`. Then what nothing settled gets its fallback (`Any`) and the rest is resolved as before, with runtime dispatch where an argument is `Any`. Behaviour changes: - A lambda's unannotated parameters are type variables instead of `Any`, settled by its body, by what it's passed to, and by how it's called. `xs.map(fn(x) => x + 1)` is a `List<Int>`, and `ps.sort_by(fn(a, b) => a.score - b.score)` reads `P`'s field. - A lambda stored in a `let` has one type: `let f = fn(x) => x * 2; f(3); f(2.5)` is now an error. A named `fn` still takes `Any`. - Calling a variable whose type isn't known yet, like `g` in `fn(g, v) => g(v)`, makes it a function taking those arguments, and later calls of `g` are checked against that type. - `for x in s` and `let (a, b) = v` wait until the kind of `s` or `v` is known instead of giving `Any`. - An index write `xs[i] = v` is the call `[]=(xs, i, v)` with the real value type. It waits for the value, so a wrong value is reported as `mismatched types` against the element type, or against a list of them for a slice. - `if` branches, list and map literal elements, and `return`s are joined part by part where they share a shape, and a part that isn't known yet waits until it is before joining at the least upper bound. So `fn(x) => if c { x } else { 0 }` doesn't fix `x` as an `Int`, and `if c { [] } else { [] }` is a list that `push` can settle. - `a op= b` with operands that aren't fully known is the waiting call `a = op(a, b)`, and the in-place overload is picked once it resolves. - Field writes (`p.x = v`) settle their receiver like field reads do. A write that no overload takes waits until its target's type is known, and a value that doesn't fit the field is reported once, against the field. - A write (an assignment or an annotated `let`) passes its value like an argument, so `let xs: Sequence<Int> = []` settles the list's elements as Ints. - An operator doesn't commit early while an operand is still wholly unknown, as that operand could be a tuple it applies to element-wise: `fn(x) => x ++ "!"` can take `("a", "b")`. - A map default whose type isn't known yet waits until it's known whether it's a function that makes the values. - Calling a function value with the wrong number of arguments is an analysis error instead of a runtime one. - A cast settles what isn't known about its value: `(fn(x) => x) as Fn(Int) -> Int` makes `x` an `Int` and needs no runtime check. - A waiting call, iteration or unpacking whose result was already used at another type is an error once it resolves. - Error messages show parts that aren't known yet as `Any`. Committing a waiting call early is only right if it gets the overload it would get once its arguments are known. Debug builds check this: each early commit is recorded, and at the end of an error-free batch the call is resolved again with the final types and the two must agree. Where a committed overload doesn't take the arguments as they are, such as an `Any` argument, the call keeps runtime dispatch, as it would with the final types. Removed: `analyse_with_expected` (literal and lambda arms), the index-write stand-in, and the analysis-time `commit_call` error path, which now reports through the solver. Not in this PR: with fully known operands `op=` still goes through the old path, including the in-place eligibility gate. Named functions keep `Any` parameters until 3c. Known tradeoff: an annotation no longer reaches a lambda before its body is checked, so `let double: Fn(Int) -> Int = fn(x) => x ++ "!"` reports the mismatch on `double` rather than on `x ++ "!"`. No benchmark got slower: closures and hof_pipeline are 7–8% faster, the others within noise. Analysis stays linear in the number of waiting calls (4000 never-called lambdas: 82 ms, 61 ms before). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_015rQP9SKVmpvME4F2LXeWmG Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015rQP9SKVmpvME4F2LXeWmG
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dc58b621cc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…d as a value 🎯 (#300) Step 3b of RFC #281: an overloaded function used as a value is the overload the type expected of it picks. This is what #246 and #217 asked for. Until now a function's name used as a value was its newest overload, whatever the code around it expected. `["a b"].map(split)` passed `split(String, String)` and failed, and `[-1.5, 2.5].map(abs)` passed `abs(Int)`. Now the name waits, like a call, until the type expected of it is known well enough: - When one overload fits what's known, it's the one meant. `map` passes each element, so `split` is `split(String)`, and over Floats `abs` is `abs(Float)`. - A function type annotation picks one too, as do a variable's uses: after `let f = split; f("a b")`, `f` is `split(String)`. - Once the expected parameter types are known, the value is the overload a call with arguments of those types would pick, so the nearest scope, the most specific overload and `Any` losing to an exact type work as for calls; it must also return what's expected. `fn f(x: Int)` next to `fn f(x)` gives `f(Int)` for an `Fn(Int) -> String`. While the parameter types aren't fully known, it commits by the same rule a waiting call does (one function shared by both), so the return type never picks an overload early. - The type expected of an `if` or a list is expected of each branch or element, so `let f: Fn(Int) -> Int = if c { abs } else { floor }` works. - A return annotation settles what the body left unknown before the body's waiting constraints are settled, so `fn f() -> Fn(String, String) -> List<String> { split }` picks the pattern overload. - When nothing says which overload is meant, it's an error that asks for a function type: `'split' has several overloads, and nothing here says which one is meant`, with an example annotation of an overload that fits, type parameters shown as `Any`. When no overload fits, it's `No overload of 'split' is a Fn(Int) -> Int`. - Overloads that redefine each other (same parameters) count as one, the newest, since the redefinition is already reported. Behaviour changes: - A position that expects `Any` doesn't pick an overload, so `print(abs)` is now an error, and so is passing an overloaded function to a named function's parameter, as in `fn apply(g, x) => g(x); apply(abs, -1)`. Named functions' parameters stop being `Any` in 3c, after which that call picks `abs(Int)` from the `-1`. - `015_struct/006` now annotates the getter it means once two structs have an `x` field. The early-commit check from #292 covers function values too: one chosen before its expected type is fully known must be the one chosen once it is. The manual's function page has a new section on overloaded functions as values. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_015rQP9SKVmpvME4F2LXeWmG Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015rQP9SKVmpvME4F2LXeWmG
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 87ade7c9aa
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Adds a fuzz harness for the analyser's overload choices, and fixes the first bug it found. The analyser commits a call or a function value to an overload before the types involved are fully known, so that information can flow into lambdas and empty literals. Debug builds check that each such early commit is the overload it would get once the types are known (#292). Codex found four cases of disagreement in review, one at a time. This harness looks for them systematically. `tests/proptest/tests/overloads.rs` generates well-formed programs from the pieces that make the analyser commit early: - overloaded functions taking `Int`, `Float`, `String`, lists or anything - two structs sharing a field name - empty literals settled by later uses - lambdas, function values, `map`, `fold` and `if` - index and field writes, `op=`, casts and annotations Each program is analysed in a debug build, and a panic is a failure; the minimal failing program is printed as source. About three quarters of the generated programs make at least one early commit. It runs like the token fuzzer: seeds in `overloads.regressions` replay in every `cargo test`, and fresh fuzzing is opt-in with `--ignored`. CONTRIBUTING.md describes it. The early-commit check now also runs on programs with type errors. Those are most of what the generator produces, and the whole test suite, error tests included, passes with it on. Bug fix: an operator with an `Any` operand committed early, although the operand could be a tuple the operator applies to element-wise. With `fn(a: Any) => a ++ []`, a tuple of lists failed at runtime instead of being concatenated element-wise as before #292, and debug builds panicked. An `Any` operand now holds an operator back like an unknown one does, both while waiting and in the default at the end of a function. After the fix, 50,000 generated programs ran without a failure. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_015rQP9SKVmpvME4F2LXeWmG Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e56251032b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Step 3c of the type system RFC (#281): a `fn` declared in the program is generic. Its unannotated parameters are type variables, the calls its body can't decide yet become its constraints, and each call checks them with its own argument types: ```ndc fn inc(x) => x + 1; let a: Int = inc(1); let b: Float = inc(2.5); fn apply(f, x) => f(x); apply(abs, -1); // was an error since 3b: abs(Int) is the one that takes -1 fn fib(n) => if n < 2 { n } else { fib(n - 1) + fib(n - 2) }; let i: Int = fib(10); // was Any ``` Language changes: - A call whose arguments the body can't use is an error at the call, with notes pointing into the body through each generic call it went through. Before, such a call failed as the program ran. - A `fn` declared in a function or block isn't generic. Its parameters are typed like a lambda's, so its first call decides them. Before, they were `Any`. - A variable from outside keeps one type: in `let seen = []; fn remember(n) { seen.push(n) }`, the first call decides what `remember` takes. - Overloads in one scope may not tie: fit some call with neither more specific. `fn f(x: Int, y)` next to `fn f(x, y: Int)` is an error. `fn show(x)` next to `fn show(x: Int)` is fine, as a typed fallback. A stdlib test checks that no built-ins tie, and none do. The RFC's "Overloads may not overlap" section is updated to match. How it works: - Variables have levels (Rémy). The body of a top-level `fn` is level 1, and settling a level-0 variable lowers the variables in its new type. When the body ends, each unsettled level-1 variable in the signature or in a waiting constraint is settled as a named type parameter (`A`, `B`, …). Hover shows these names. Everything else the body made gets its fallback. - Ambiguous variables: a variable that a waiting constraint depends on, but that the parameters don't decide, is settled in the body first, as at the end of the program. This covers default rules, then fallbacks. Two rules are new: - A function passed to a call counts as decided by the call, as `fn(a, x) => a + x` is by `fold`. - An `if` branch that is only ambiguous takes the other branch's type. This gives `fib` its type `fib(A) -> A`. - An overloaded function value in a generic body must be decided there, since the body is compiled once. - The scheme is the constraints still waiting on type parameters. A call that commits to the function copies them with fresh variables and tries them at once. Copies made through a generic call become the caller's constraints. Copies of the same call with the same arguments are merged, so a function that calls another twice doesn't double its constraints. - Each copy carries the chain of calls it came through. Its errors are reported at the outermost call, with secondary labels for each step: "`twice` calls `inc` here" and "`inc` needs this". The CLI renders them. The LSP shows only the primary span. - A generic body is still compiled once. Its waiting calls are picked as for `Any` arguments, which means at runtime, and copies record no picks. Callers still get precise types. Instances are 3d. Fixes found on the way: - A call left to the program as it runs lists its candidates most specific first, and the VM takes the first that accepts the arguments. Because overloads in one scope can't tie, that's the most specific one. An `Any` or type-parameter argument no longer counts as an exact match for a fallback while a more specific overload in the same scope could take it, and that scope keeps the call. Without this, `fn via(v) => show(v)` called `show(x)` for an Int. The cost: such calls, like `a[r][k]` on an untyped `a`, are decided at runtime instead of binding to a built-in's `Any` fallback. That makes `matrix_mul` about 30% slower, until 3d compiles generic bodies per instance. - An early commit through a `Sequence<T>` parameter no longer settles a bare variable as `Sequence`. Example: `fn mut_map(list, f) { list.len; list[i] = … }` would otherwise make `list` a `Sequence` and reject the index write. - A waiting `a op= b` whose operands become known is decided by the same rule as one known at the start, including the in-place overload. Before, `remove_keys(a, b) { a -= b }` on maps found no `-`, and an `Any` `xs ++= ys` lost the in-place append. - A function that returns itself (`fn f() { f }`) returns `Any` instead of failing the occurs check. Tests: - Updated tests that relied on unannotated parameters being `Any`. Each now writes `: Any` where it tests runtime behaviour: 004/066, 016/017, bug0009, bug0011. One unit test expected an `Any` argument to pick `pick(Any)` statically; it now dispatches at runtime. - Added functional tests 005/071–080 and unit tests covering each rule above. Mutation testing breaks each rule in turn and checks that a test fails. - The benchmark programs run unchanged. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_015rQP9SKVmpvME4F2LXeWmG --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 107fe8cebd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… its calls give it 🧬 (#306) Step 3d of the type system RFC (#281): a generic function's body is compiled once for each set of types its calls give it, and each copy picks its calls' overloads from those types. Before, it was compiled once and decided those calls as the program ran. ```ndc fn inc(x) => x + 1; print(inc(1), inc(2.5)); ``` ```text inc(1) calls inc<Int>, whose + is +(Int, Int), called directly inc(2.5) calls inc<Float>, whose + is +(Float, Int) ``` How it works: - Key: an instance is for the types of the function's key. The key is the type parameters that its waiting constraints, and the generic functions its body calls, depend on. `fn wrap(x) => [x]` has an empty key, so its one body serves every call, as before. A call whose key types are all `Any` uses the function's own body, which still decides its calls at runtime. So does runtime dispatch. A function's name used as a value is always the function itself, so two values of it stay equal and work as the same map key. - Uses: when a call commits to a generic function, the analyser records the types it gives the key. A use inside a generic body whose types depend on that body's type parameters becomes part of the body's scheme. Uses made while solving a copy of a constraint aren't recorded, as their picks belong to the function they were copied from. - Instance pass: at the end of a batch, each use gets the instance for its now-known types. A new instance is a hidden top-level symbol named like `inc<Float>`, which no identifier can spell, so the layout gives it the next global slot. The pass solves the function's constraints again for the instance's types and records the picks in that instance's own tables. It also follows the generic calls inside, until no new instances appear. Errors are suppressed during the pass, because they were reported at the calls. - `pure fn`: a generic `pure fn`'s instances share its memo cache, through a new `MemoizeLike` opcode, so a call through a value of the function finds what a direct call to an instance computed. - Recursion: a call in a generic body to the function itself has the body's own types, since recursion is monomorphic. So in an instance, it calls that instance. - Compiler: each instance is compiled at its function's declaration. Its picks are overlaid on the function's own, and nested functions inherit them. It's stored in its own global. A top-level function captures nothing, so an instance is a plain constant. Calls point at the instance's global. - Functions declared inside: a function or lambda declared in a generic body is compiled with each instance. Where that uses none of the instance's picks, it reuses the prototype from the function's own body, so it stays the same function: `make(1) == make(1.5)` when `make` returns one. - REPL: a call on a later line, to a function an earlier line declared, uses the function's own body. The earlier line's code isn't kept, so no new instance can be compiled for it. That's a performance limit, not a correctness one, and keeping generic bodies to compile later instances is follow-up work. Performance against master: - `matrix_mul`: 29% faster (3c had made it about 30% slower) - `fibonacci`: 10% faster - `string_concat` and `closures`: about 10% faster - `perlin`, `ackermann` and `quicksort`: 4–5% faster - the others: unchanged within noise Tests: - Analyser unit tests cover: one instance per set of key types; no instance for an empty key or `Any` arguments; an instance calling the instances its types need. - Functional tests: 005/083 covers an instance calling another generic function; 005/084 covers a generic function used as a value staying equal to itself. - A unit test covers an instance calling itself where its body recurses. 010_memoization/003 covers the shared cache, and 005/085 plus two compiler tests cover the nested functions. - REPL tests cover instances within one line, and a later line calling an earlier function with new types. - The overload fuzzer now generates generic functions that call each other. Debug builds assert that no use outside a generic body has a type parameter in its key. With that, the fuzzer found a bug where a body's uses were sorted after its type parameters were named; its seed is kept in `overloads.regressions`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_015rQP9SKVmpvME4F2LXeWmG --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…type 🛂 (#308) Step 4a, part 1, of the type system RFC (#281): `Any` becomes the dynamic type. A value of type `Any` goes where code expects another type, and is checked to have that type when the program gets there. ```ndc let a: Any = "five"; let n: Int = a; // error: expected Int but found String ``` Before, an `Any` value went into a typed variable unchecked, and a function with an annotated return type couldn't return one at all. Where a value of type `Any` is checked: - Storing it in an annotated variable, or a name a pattern declares. - Assigning it to a variable, an element or a field, including through `op=` and destructuring. - Passing it for a parameter whose type the call knows. `xs.push(a)` on a `List<Int>` checks for an Int. - Returning it from a function with an annotated return type. Each value returned is now checked against the annotation on its own, so a mismatch is reported at that value, not at the function. - A waiting call that turns out to return `Any` after other code had already settled its type. How it works: - The analyser records each check in a new `Analysis::type_checks` table, keyed by where it happens: an expression, checked once it's computed, or an assignment target or a declared name, checked before the value is stored. - The compiler emits a new `ExpectType` opcode there. It works like `CheckType` (containers are scanned, as `as` scans them), but its error reads `expected Int but found String`. - Checks in a generic function's body depend on the types each instance is for. In `fn put(xs, v) { xs.push(v) }`, the instance for `List<Int>` checks `v` for an Int and the one for `List<String>` for a String. So each instance keeps its own checks next to its picks, and the function's own body checks its type parameters as `Any`. A copy of a constraint in a caller records nothing, because the instance for that call does the check. - Each instance of a generic `pure fn` now has a memo cache of its own, and the `MemoizeLike` opcode from #306, which made them share one, is gone. Instances can fail where the others don't for the same arguments, directly or through what they call: two empty lists of different element types look the same as the program runs. So a call through a value of the function, which runs its own body, no longer finds what a direct call to an instance computed. None of the benchmarks gets a check, so their performance is unchanged. Not in this PR: containers are still covariant, so `[a]` with `a: Any` still passes for a `List<Int>` without a check. Invariance is the next part of 4a. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_015rQP9SKVmpvME4F2LXeWmG --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d14b1ac60c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…iant 🧩 (#310) Steps 4c and 4a-2 of the type system RFC (#281). Containers and function types become invariant, and literals take the type their use needs. Invariance alone would reject `let xs: List<Any> = [1, 2]`, so the two land together. Behaviour changes: - An integer literal is an `Int` unless its use says otherwise: `let f: Float = 7` holds `7.0`, and `let total = 0; total += 2.5` makes `total` a Float. - List, map and tuple literals take their element types from their use, as in `let xs: List<Any> = [1, 2]`. Otherwise a literal is the type its values share, so `[1.5, 1]` is a `List<Float>`, or `Any` if they share none, as in `[1, "a"]`. - Two literals of different types are an error at the value: after `let v = ["s"]`, `v = [1]` reports the `1`. - The first use whose overload pick depends on a literal decides its type. A use that picks the same overload whatever the literal is, like `xs.push(1)`, leaves it open. - A `List<Int>` is not a `List<Any>`, and the same goes for maps, tuples, `Option` and function types. Converting goes through `Any` or `as`. - Overloads are ranked by "is an instance of", so `f(List<Int>)` and `f(List<Any>)` are unrelated. - In the standard library, slicing takes an `Iterator<Int>` instead of an `Iterator<Any>`, and adding to a set is `insert(Map<K, ()>, K)`. How it works: - A literal's type is a type variable with bounds that settling it checks: an integer literal's may be `Int`, `Float`, `Number` or `Any`, and a collection literal's element type any type each of its values fits. Each bound remembers the literal it came from, so mixed values of one literal make it `Any`, while values of several literals that don't fit are reported. - A literal nothing settles takes the first default with which every call waiting on it still fits, before any other default rule applies. In `total += 2.5`, an Int `total` couldn't hold the Float result, so `0` is a Float. - `StaticType::fits` and `may_fit` replace subtyping in overload matching and ranking, and argument binding unifies inside containers. The VM keeps `is_subtype` for its runtime checks, and joins keep `lub` until 4a-3. Programs compile to the same bytecode as before, so they run as fast. Analysis is slower where calls wait for literals: about 1 ms more on the benchmarks. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_015rQP9SKVmpvME4F2LXeWmG --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…311) Step 4a-3 of the type system RFC (#281): the last of subtyping goes. Values join by unification instead of a least upper bound, and `is_subtype`, `lub` and `glb` are deleted from `StaticType`. Behaviour changes: - Two branches of an `if`, or the values a function returns, are one type when they unify, with literals taking the other side's type, and `Any` when they don't: `if c { 1 } else { 2.5 }` is a Float, `if c { 1 } else { "a" }` is `Any`, and `if c { [] } else { [1] }` is a `List<Int>`. - Containers of different types join as `Any`, not as a container of `Any`: a `List<Int>` and a `List<Float>` join as `Any`, as do two tuples of different types. The same goes for what a call the program decides as it runs returns. - A cast to a function type checks only how many arguments the function takes, as runtime dispatch already did. How it works: - A join waits only while a whole branch is unknown, as `x` in `fn(x) => if c { x } else { 0 }`, so a lambda's parameter isn't fixed by the branch next to it. A branch that nothing else settles takes the other branch's type at the end of a batch too, not only in a generic function. - `StaticType::join` is the type both values have, or `Any`. It replaces `lub` for the results of calls decided at runtime, for op=, and for the element types the VM reports for values. - The VM checks a value against a scalar, struct, function or iterator type with `Value::is_a` instead of comparing types with `is_subtype`, and an `Option` parameter by its value. - `overlaps`, which rejects impossible casts, compares types structurally. Bug fixes from #310: - When a call's pick depended on a literal, the literal's type was decided but the pick made before was kept. If the literal became `Any`, another overload could take the call as the program runs, so the pick is now made again once the literal is decided. - Literals whose values share no type, like `[1, "a"]`, are `Any` lists on their own, but two of them made one type were rejected as literals of different types. Now any literal can be one type with such a literal: `if c { [[1, "a"]] } else { [[2, "b"]] }` is a `List<List<Any>>`, and after `let v = [1, "a"]`, `v = ["b"]` holds `Any`s. Literals of one type each, like `["s"]` and `[1]`, still differ. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_015rQP9SKVmpvME4F2LXeWmG --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…313) Step 4b of the type system RFC (#281): `Sequence<T>` becomes `Iterable<T>`, which is no longer a type a value has, but a parameter type that takes any kind of sequence of `T`s. Behaviour changes: - `Sequence<T>` is renamed `Iterable<T>`, so annotations that name `Sequence` no longer compile. - `Iterable<T>` can only be a parameter's type, or what a function passed for one returns, as in `fn apply(f: Fn(Int) -> Iterable<Int>)` or `flat_map`'s callback. `let xs: Iterable<Int> = [1]`, `x as Iterable<Int>` and a return type of `Iterable<Int>` are errors that ask for the kind of sequence. - `fn total(xs: Iterable<Int>)` takes a list, a tuple, a range or any other sequence of Ints, and is compiled once for each kind it's called with. A call with anything else is reported as one with no overload that fits, and `total` and `total(x: Any)` are two overloads, with the first more specific. - Iterating over something that doesn't iterate, like `for x in true` or `for i in 1`, is reported before the program runs, and so is a callback for an `Iterable<T>` that returns one, as in `fn flatten(xs) => [0].flat_map(fn(_) => xs)` called with `true`. - When the program picks the overload as it runs, as for an `Any` argument, an `Iterable<Int>` parameter takes no sequence, as a `List<Int>` one doesn't, since what it holds isn't scanned. - A tuple iterates as the one type its elements are once made the same, so `(1, 2.5)` iterates as Floats. A tuple whose elements share no type iterates as `Any`, as a literal of such values does. - `prefixes` and `suffixes` return a `List<List<T>>`; strings keep their own overloads. How it works: - A call whose argument for an `Iterable<T>` isn't known yet waits for it, as `for x in xs` does, since the argument could be any kind of sequence. That replaces the special case that kept such a call from committing early, which made the argument a `Sequence` instead of what it was. - An annotated `Iterable<T>` parameter is an unannotated one with an `Iterate` constraint that its elements are `T`s. A generic function carries the constraint to its calls, which is what makes it generic over the kind of sequence. The type table also keeps the parameter's shape, `Iterable<T>`, which picking an overload and checking overloads against each other use, while a call binds the declared type. - A tuple whose elements aren't known yet is iterated once they are, so iterating `(a, b)` doesn't make `a` and `b` one type. - A function passed where a function type is expected binds its `Iterable` parameters to what the type passes, so `[[1], [2, 3]].map(len)` takes `len(Iterable<T>)`. - A standard library test checks that no native signature has `Iterable` elsewhere. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_015rQP9SKVmpvME4F2LXeWmG --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…#315) Step 4d of the type system RFC (#281), for issue #206: struct fields reached through `Any`. The type checks #206 asked for are already in place after the earlier steps. A constructor called through `Any` is rejected by runtime dispatch's parameter check, a field written through `Any` gets the runtime check from #308, and the issue's own example is now an analysis error. What remained was a false rejection: runtime dispatch never lets a value through for a parameter like `List<Int>`, because it doesn't scan containers, so a struct with a `List<Int>` field couldn't be made through `Any` even with a list of Ints, and `process(List<Int>)` and `process(List<String>)` never dispatched at all. Behaviour change: - When no overload takes the arguments without looking at their elements, runtime dispatch checks the elements before the call fails, as `as` does. `let make: Any = Q; make([1, 2])` now makes a `Q` with a `List<Int>` field, and `make(["s"])` is still rejected. Calls that dispatch today pick the same overload as before, at the same cost, and only a call that would otherwise fail scans its arguments. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_015rQP9SKVmpvME4F2LXeWmG Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…s 🩹 (#316) Fixes the review findings still open on the type checker PR (#291): places where a type the analyser promised didn't match the value the program gets, so a later call could pick the wrong overload. Behaviour changes: - `pop` and `pop_left` on an empty list are an error, as `pop` on a deque or heap already is, since their return type promises an element. `pop?` and `pop_left?` return `None` instead. - A list write with an index of type `Any` checks the value: `xs[i] = "bad"` on a `List<Int>` is an analysis error, and `x["foo"] = 69` is one too, where it failed at runtime with "Invalid list index" before. An `Any` index still picks between the element and the range overload as the program runs. Bug fixes: - A map comprehension's default is one of the map's values: `%{:"missing", x: x for x in xs}` holds Strings as well as Ints. - A map literal key without a value holds `()`: `%{"a", "b": 1}` holds `()` as well as Ints. - The catch-all `[]=(List<T>, Any, Any)` overload is gone. Calls the program picks the overload of as it runs keep only the overloads that take the arguments with each type parameter one type, as early picks already did, so `[]=(List<T>, Int, T)` no longer looks like it takes a String for a `List<Int>`. Operators, which may apply to each element of a tuple, and function arguments, whose types aren't checked as the program runs, are left out of that check. - Runtime dispatch matches `()` as cheaply as a scalar, and any iterator conforms to an `Iterator<T>`, since its elements can't be checked without consuming it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_015rQP9SKVmpvME4F2LXeWmG Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts in the manual were resolved by turning the type checker's examples into master's runnable form and correcting the ones that no longer held: number literals adapt, so reassignment examples use a non-literal Int, and an `Any` operand needs an `Any` parameter now that generic functions keep their argument's type. Tests expecting `Any` in a tuple `[]=` error and whole Floats printed without `.0` were updated to master's output. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015rQP9SKVmpvME4F2LXeWmG
A list literal's numbers took the type their uses needed when the list
itself was passed (`n.push(0.5)` makes `[1]` a list of Floats), but not
when one of its elements was used. These were all errors:
- `let n = [1]; n[0] += 0.5` and `n[0] = n[0] + 0.5`
- `let x = n[0]; x = 0.5`
- `let y = n.first() + 0.5; n.push(y)`
- `m["a"] += 0.5` on `%{"a": 1}`
They now run, with Float elements. A list whose elements are real Ints,
such as `["a".len()]`, still rejects `+= 0.5`.
When a literal tries one of its defaults (Int, Float, Number), the
analyser checks that the calls waiting on it still fit. That check was
too loose in three ways, and it now:
- Counts only the overload that resolution would pick once the arguments
are known, scope by scope as a call does. Before, any overload that took
them counted, so `[](Any, Any) -> Any` let `n[0]` fit whatever it had to
give back.
- Checks the waiting calls together. A call whose overload is known
keeps what it binds, such as its result, and the calls waiting on that
are checked too.
- Also checks the literal's holder, here the list's element type. A
holder used to wait until its numbers were settled, so its calls were
never checked when the numbers took their defaults. Now it is settled in
the same trial.
In the same trial, the other numbers in that list take the same default.
The values of `[1, 2]` are Ints or Floats, never one of each, so `[1,
2].push("a")` stays an error rather than turning the list into a
`List<Any>`. This also keeps a large list literal linear: one trial
settles every number in it.
Literals that another literal holds now get their defaults after the
ones that aren't held. For example, the index `0` in `n[0]` is settled
as an Int before the list's numbers try theirs.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_015rQP9SKVmpvME4F2LXeWmG
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Small fixes from the review of the type checker (#291): - `zip` takes any sequence, as its `Iterable` signature says. It used to accept only lists and tuples, so `zip("ab", "cd")` or `zip` over a range passed analysis and failed at runtime. It now steps the arguments' iterators together and stops at the shortest, so an endless range works as one of them. - The compiler no longer copies an instance's pick tables for each instance it compiles. It refers to the instance by its declaration and index in `Analysis::instances`. - The manual's number page says that a later use can make a `let`-bound literal a Float, which changes what `/` and `\` do. - `declared_symbol`'s doc comment is back on `declared_symbol`; it had ended up on `declare_instance`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_015rQP9SKVmpvME4F2LXeWmG Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The literals in a generic function's body were all settled when the body
ended, before its type parameters were named. That pinned the function
to the literals' default types, so these were rejected:
- `fn fact(n) => if n < 2 { 1 } else { n * fact(n - 1) }; fact(5.0)`
- `fn k(x) => [x, "b"]; k(1)`
- `fn g(x) => [x, 1]; g(2.5)`
- `fn f(xs) { xs.push(1); xs }; f([2.5])`
These now work. A literal that the parameters' types reach, through a
waiting constraint or through what a literal holds, becomes a type
parameter and keeps its literal bounds. Each call gets a fresh literal
with those bounds, which the call's uses settle as they would a literal
written at the call. Literals the parameters don't reach are still
settled in the body. The integer literals one literal holds share one
type parameter, as the values of `[x, 1, 2]` have one type.
Each instance records the types of its integer literals, and the
compiler emits those values, so `fact`'s instance for Floats returns
`1.0`. A call whose instance key is all `Any`, apart from
integer-literal parameters that are Int, uses the function itself, which
compiles its literals as Ints.
Only a call in the batch that declares the function gets an instance. A
function used as a value, as in `let f = pair`, and a call of a function
declared on an earlier REPL line both run the function's own body, so
their integer-literal parameters are pinned to Int. `fact(1.0)` on a
later REPL line is still an analysis error, as before, and `let f:
Fn(Float) -> List<Float> = pair` is an error. Without the pin, either
would return an Int typed as a Float.
When merging the copies of a callee's constraints, copies of one literal
from two calls with the same arguments now count as the same call.
Unifying two literal types also skips the bounds the target already has:
same position, same held type. Without these, `fn f1(x) => f0(x) +
f0(x)` and so on grew exponentially.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_015rQP9SKVmpvME4F2LXeWmG
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ts 💬 (#321) Clearer errors for two programs the type checker rejects on purpose. A generic function that calls itself with an argument built from one of its parameters, as `nest([x], n - 1)` does in `fn nest(x, n)`, would need a version of itself for each deeper type. Binding `[x]` to `x` made the parameter a list holding itself, so the error came at the outer call: "No function called 'nest' found that matches the arguments 'Int, Int'". The error is now at the recursive call. It says that each call would need a version for a deeper type, and suggests annotating the parameter `Any`, which already works. When another overload might take the call, the body can't tell. Instead the check happens when instances are made: a use inside an instance of the same function, whose key holds that instance's key in a bigger type, gets the same error. `fn f(xs: List<Any>) => 42; fn f(x) => f([x]); f(1)` used to hang analysis, since `[x]` is a `List<Int>`, not a `List<Any>`. An overloaded function in a list literal, such as `abs` in `[len, abs]`, was expected to have the type of the list's other values, and failed with "No overload of 'abs' is a Fn(Iterable<Any>) -> Int". The other values of a literal don't say which overload is meant, so it's now the usual "'abs' has several overloads, and nothing here says which one is meant" error, with the function-type hint. A sibling that fits still picks the overload: `[fn(x: Int) => x, abs]` works. Also adds a test that a literal map key takes the map's key type (`%{1: "a", 2.5: "b"}[1]`). #317 and #320 already fixed that. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_015rQP9SKVmpvME4F2LXeWmG --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Fixes found by running a set of Advent of Code 2025 solutions on the
type checker and on master.
Bug fixes:
- A generic function's own body, compiled for arguments of any type,
failed to compile with "encountered unresolved binding" when a call in
it fit no overload once the type parameters were `Any`, as `sum` fits no
`List<Any>`: `fn f(s) { [i for (i, c) in s.enumerate()].sum() }`. That
body now leaves such a call to the program as it runs.
- An integer literal could become a Float to reach an overload that
takes it only through `Any`: in `let e: String = cs[0]` on a
`List<Int>`, the index became `0.0` to pick `[](List<T>, Any) -> Any`,
so analysis passed and the program failed with "List indices must be Int
values". An `Any` parameter is now a fallback that a literal doesn't
change its type to reach when another overload in the same scope takes a
number type or a type parameter there, so this is reported as the
mismatch it is. Where nothing in that scope takes a number, as with
`print`, a literal still adapts to its other uses.
- An operator on tuples committed at once, element by element, when an
operand or a tuple element wasn't known yet, giving `Any` there for
good: `(cs[0] - (1, 1)).product()` on a list built by a comprehension
failed with "No function called 'product' found that matches the
arguments 'Tuple<Any, Any>'". It now waits for operands and elements
nothing is known about, while a partly known one, like `[]`, is still
settled by the overload each position picks.
Performance:
- An operator on tuples through runtime dispatch, as `(l - r) * (l - r)`
with `l` from `combinations`, first tried every scalar overload again,
element by element, before applying itself to each element. Applying to
each element now comes first, and the element-by-element check stays the
last thing tried before a call fails. `conforms_value_args` no longer
clones the function's type, and conformance is only memoized for
container targets.
- Marking an overloaded function value as one of a list literal's values
scanned every waiting constraint for each value of unknown type, which
made large list literals quadratic. Only nodes that waited as function
values are looked up now.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…pe 🎯 (#323) Indexing a tuple gave `Any`, as signatures can't say that the result's type depends on the index's value. That `Any` spread through code that uses tuples as points or pairs: an Advent of Code solution needed `as List<Tuple<Int, Any>>` to type-check, and the calls that took the `Any` were dispatched as the program ran. An index written as an integer in the code, as in `t[1]` or `t[-1]`, now gives that element's type: - `(1, "a")[1]` is a `String`, and `(1, "a")[-2]` an `Int`. - It works per instance in generic functions: `fn first(t) => t[0]` returns a `String` for `first(("x", 2))`. - The `[]` call is picked when the program is compiled. - An index past either end is an analysis error: "Index 3 is out of bounds for a tuple of 3 elements". - An index computed as the program runs (`t[i]`) still gives `Any`. The `[]` call's constraint records the literal index (`CallConstraint::tuple_index`). Once the container is known to be a tuple, the solver gives the element's type and settles the index as an Int. Copies of `t[0]` and `t[1]` have the same argument types, so the index is part of what makes two copies the same call. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Two-argument `min` and `max` were `<A, B>(A, B) -> Any`. So `min(a, b)` was `Any`, and every call that used the result was dispatched as the program ran, as with the corners `(min(...), min(...))` of a rectangle. They're now `<T>(T, T) -> T`. A literal takes the other argument's type, so `min(2.5, 1)` is the Float `1.0`. Behaviour change: an Int and a Float together, as in `min(n, 2.5)` with `n` an Int variable, is now an analysis error: "No function called 'min' found that matches the arguments 'Int, Float'". Convert one of them, as with `float(n)`. The manual's `lowest min= g(x)` example starts `lowest` at `Inf`, a Float, so it now prints `0.0 3481.0`. An `(Any, Any) -> Any` overload for mixed arguments can't sit next to `<T>(T, T) -> T`. Overload resolution treats an unannotated type parameter and `Any` as equally specific fallbacks, so whichever is declared later wins. That sends either the mixed arguments or the same-typed ones to the wrong overload. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#325) An anonymous function passed to a call used to be analysed before the analyser looked at the function it's passed to, so its parameters got their types from its body. In `players.map(fn(p) => p.size)`, `Team` is the only struct with a `size` field, so `p` became a `Team`, and the error was that `map` doesn't take `List<Player>, Fn(Team) -> Int`, pointing at the whole call. Now a call analyses its other arguments first. An anonymous function with unannotated parameters then gets the parameter types that every overload that takes those arguments agrees on, before its body is checked, as Rust, TypeScript, Kotlin and C# do. The example now reports `No function called 'size' found that matches the arguments 'Player'` at `p.size`. Where the overloads disagree, or a type isn't known yet, the parameter works out its type as before. - Error messages about calls with a lambda show the parameter types it got: `filter`'s test now expects `Fn(Int) -> Int` where it said `Fn(Any) -> Int`. - `merges_literals` counted a literal that appears in two arguments twice, as the element of `[0]` does in `[0].flat_map(fn(_) => xs)` once `_` has its type. That made the call wait, and `flatten(true)` reported a failed `flat_map` instead of `Bool is not iterable`. It now counts each literal once. The manual's lambda section gets the example. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Cleanup found by a review of the type-checker branch. No language behaviour changes; −387 lines. Analyser: - One `instantiate_signature` and `bind_arguments` replace six copies of "give a signature fresh variables and pass the arguments" (`could_accept`, `overload_fits`, `merges_literals`, `bind_signature`, `commit_call`, the lambda parameter pass). - Function declarations build their parameter types directly instead of going through `TypeSignature`, so `declare_parameters`, `TypeSignature::from_annotated_bindings` and `TypeSignature::types` go. - Duplicate overload helpers merged (`parameters_of`, `could_pass`, `is_compatible`/`could_accept_call`), as are the five "check a write, report it if it doesn't fit" sites, the two "settle if it fits, else roll back" loops in `TypeVars`, the two type-parameter collectors (now `StaticType::type_parameters`), and the two copies of the in-place-then-operator candidate merge for `op=`. `merge_copies` is one flat loop. - A call that commits early to one overload is now a direct call. Its `Any` arguments were already checked where they're passed, so the single-candidate `Dynamic` binding only made the VM search a set of one. Leftovers from before the type checker: - The VM's element-wise tuple path for a picked function had code for choosing between several functions, which its only caller never passes. Both element-wise paths now share one loop. - `[](List<T>, Any)` and `[](Deque<T>, Any)` are gone: indices are inferred now, and an `Any` index still goes to `[](Any, Any)`. - `ndc_analyser/RETURN_TYPE_INFERENCE.md` described the removed subtyping design. The troubleshooting page on overload dispatch claimed dispatch never scans and that user functions can't take typed containers; it now describes calls with `Any` values as they work. The compiled output of the analysis benchmarks is unchanged by the analyser refactor, and their analysis time is the same. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…rameters 🔍 (#328) A value of type `Any` that a generic function's body passed where one of its type parameters was expected went unchecked. A call could then return a value whose static type was wrong: ```ndc fn id2(x) { let a: Any = 5; let b = [x]; b.push(a); b[1] } let r = id2("s"); // typed String, held the Int 5 ``` The check was recorded once, in terms of the type parameter, and an instance's key only had the type parameters its waiting constraints, generic calls and literals depend on, so `id2` had no instances and its own body checked `a` against `Any`. The checks the body makes of `Any` values against a type with a type parameter in it are now part of the scheme: they add their type parameters to the key, and each instance checks them against its own types. `id2("s")` now fails at `b.push(a)` with "expected String but found Int". 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…results 📐 (#330) Some built-ins still returned `Any` in their element types, so everything computed from their results was dispatched as the program ran. - `transposed` and `multi_cartesian_product` take a sequence of lists and return a `List<List<T>>` of the lists' element type. Rows of different types (`[[1, 2], ["a"]]`) still give a `List<List<Any>>`, and `transposed` of strings gives lists of strings. - `zip` of three or four sequences returns a list of typed tuples (`List<(A, B, C)>`, `List<(A, B, C, D)>`); five or more still give a `List<Any>`. The rows are lists rather than any sequence because a signature can't nest `Iterable<T>` inside another type. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…d as 🔒 (#329) A value of type `Any` used as a function type was only checked for how many arguments the function takes, so a call through it could return anything: ```ndc let a: Any = fn(x) => "s"; let f: Fn(Int) -> Int = a; let n: Int = f(1); // n held "s" ``` A function is now checked against what its own type says it takes and returns, and when what it returns isn't known, its calls are checked. - A function whose type rules out the function type isn't one. The example now fails at `let f` with "expected Fn(Int) -> Int but found Fn(Any) -> String", as does a lambda that takes a String. An `Any` in the function's own type, as an unannotated parameter has, fits anything. - A lambda's value now records the return type the analyser inferred, rather than `Any`, so most lambdas are decided without checking their calls. - A function that may fit, such as a named function's value, whose return type isn't recorded, becomes a new `Function::Checked` when it's cast or passed where the function type is expected. Each call through it runs the function and checks its result: "expected the function to return Int but it returned String". It equals the function it checks. - Inside a container, and for an overload the program picks as it runs, a function can't be replaced by a checked one, so only a function whose own type shows it fits does. Cast the value first to have its calls checked. Before, both accepted any function with the right number of parameters. - A call the analyser leaves to the program among one overload with known parameters is now a direct call, with its `Any` arguments checked where they're passed. That's what gives a function argument passed to such a call its checks. A variable of type `Any` being called, and a function with an `Iterable<T>` parameter, whose kind isn't in its declared type, still have the program check the arguments. A checked call records the return type it expects on the callee's VM frame, which checks the result when it returns, so recursion through a checked function goes as deep as any other. A function it returns may get its own calls checked, and a function returning `Never` fits any return type. The new variant's type is boxed so that function values stay the same size; a call-heavy benchmark (`fib(27)`) runs in the same time as before (1.3% more instructions). The manual's casts page describes the checks. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…a call like any other ⚖️ (#332) Picking an overload matched each argument against its parameter on its own, so a type parameter could stand for a different type at each position: `max(T, T) -> T` looked like it took a Float and an Int. The solver's unification got it right for ordinary calls, but three places worked around the picker instead, and `a op= b` went through the picker alone. So `best max= x`, with `best` a Float and `x` an Int, passed analysis and failed when it ran, while `best = max(best, x)` was rejected (found in an Advent of Code solution). - The picker now makes each type parameter one type across the call. Inside a container a type parameter is the type there (a `List<Any>` makes `T` `Any`), and a value passed for `T` itself has to fit it, so `push(List<Any>, String)` still fits `push(List<T>, T)`. An argument of type `Any` as a whole agrees with anything, as it's checked where it's passed. - `a op= b` is now a call constraint like any other, as `a = op(a, b)`: one path instead of two (the separate one that picked overloads for known operand types is gone), and an in-place overload such as `++=` is used where it takes the operands exactly. An operator with no plain counterpart, like a set's `-=`, is called when no plain operator takes the operands. - The workarounds go: the filter on runtime picks in `resolve_overloads`, and the eligibility gate for exact in-place picks. Behaviour changes: - `best max= x` with a Float and an Int is an analysis error, as `max(best, x)` is. - `a op= b` with a right operand of type `Any` is no longer an error. It works like `a = a op b`: the operator makes a new value, which is checked where it's written back, so a value that doesn't fit never reaches the target. As nothing says the operand fits, it doesn't change the target in place; a cast to the target's type does. - `values ++= ["two"]` on a `List<Int>` now reports that no `++` takes `List<Int>, List<String>` instead of a type mismatch. The manual's augmented-assignment and casts pages describe the new rules. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A parameter annotated `Iterable<T>` took any kind of sequence of `T`s, but an unannotated parameter already takes any sequence its body can iterate, and only the feature's own tests used it. Supporting it split each function's type in two (its declared type and the "shape" calls saw), with a separate iteration constraint per such parameter. A review of the type checker flagged it as costing far more code than it gave users. `Iterable<T>` can no longer be written in a type annotation, for a parameter as for a value: "`Iterable<T>` can't be written in a type annotation", with help to leave the parameter unannotated or name the kind of sequence. Built-in signatures keep it. This deletes the shape table and `callable_type` (every caller now reads the declared type), the parameter-annotation lowering that split off the element type, and the per-parameter iteration constraints and runtime types for such parameters. The `Iterable` tests that only covered the annotation go; the others now use unannotated parameters. The manual's section on it now describes unannotated parameters. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…#334) Each generic function's type can be twice the size of the type of the one it calls, so a chain of them grows exponentially, the known worst case of Hindley–Milner inference. With `fn f0(x) => (x, x)` and `fn fi(x) => f{i-1}(f{i-1}(x))`, analysis ran out of memory at `f6`, even with the call behind `if false`. A generic function whose type has more than 10,000 parts is now reported ("The type of `f4` has more than 10000 parts, too many to work with", with help to annotate a parameter or the return type with `Any`), like rustc's type length limit, and is analysed from there on as taking and returning `Any`, so the functions that call it don't grow it further. The size is counted once per generic function, stopping at the limit. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… 💬 (#335) A call that no overload takes only said which arguments it was given. When a function's parameter type comes from something other than its annotations, that left the reader guessing. In `let r = []; fn put(x) { r.push(fn() => x); 0 } put(1); put("a");`, the first call makes `r` hold functions returning Ints, so `put` takes an Int from then on, and `put("a")` reported only "No function called 'put' found that matches the arguments 'String'". The error now has help saying what the overloads it could reach take, when there are at most three: "`put` takes (Int)", or "The overloads of `f` take (Int), (String)". Larger sets, like the built-in operators, are left out. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A `while` loop always had the unit type, even `while true` with no
`break`, which only ends by returning. A function whose body ends in one
therefore returned the join of its `return`s and `()`, which is `Any`,
and everything computed from its result was `Any` too. Found in an
Advent of Code solution: `fn merge_ranges(ranges) { … while true { … if
done { return rc; } … } }` made `rr` `Any`, so `[(hi - lo + 1) for (lo,
hi) in rr].sum` failed to find a `sum` for `List<Any>`.
A `while true` loop that no `break` leaves now has type `Never`, like a
`return` or Rust's `loop` without a `break`, so a function ending in one
returns what its `return`s do. A `break` inside a nested loop leaves
that loop, not this one. To tell, the analyser's loop depth became a
stack of loops that records whether a `break` leaves each.
The manual's while-loop page describes it.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…lone ✂️ (#340) A number literal in a generic function's body that the function's parameters reached became an extra type parameter, which each call settled: `fn pair(x) => [x, 1]` made `pair(2.5)` `[2.5, 1.0]`. That needed its own machinery: literal type parameters with bounds in each scheme, per-instance literal types read by the compiler, and special Int rules for calls from an earlier REPL line and for function values. As a result the same code typed differently in a file and in the REPL. A review of the type checker found that no realistic program used it. A generic body's literals are now decided by the body alone, before the function is generalised, with the same look-ahead as literals anywhere else. They're never type parameters, so a file and the REPL type a program the same way. Behaviour changes: - `fn fact(n) => if n < 2 { 1 } else { n * fact(n - 1) }` called as `fact(5.0)` is an analysis error pointing into the body. Writing `1.0` makes it work for Ints and Floats alike. - `fn pair(x) => [x, 1]` only takes Ints, `fn k(x) => [x, "b"]` only Strings, and `fn f(xs) { xs.push(1); xs }` only `List<Int>`. - `fn inc(x) => x + 1` still takes Floats, since `+(Float, Int)` exists, and `fib(10.0)` still works. A literal at the call still adapts: `let r: Float = g(3)` makes the `3` a Float. - Next to `fn f(xs: List<Any>)`, `fn f(x) => f([x])` now makes `x` an `Any`, so it calls the `List<Any>` overload instead of reporting growing recursion. Removed: `Scheme.literals` and `literal_nodes`, `LiteralBounds`, `instantiate_literals`, `resolve_bounds`, `with_literals_named`, `literals_tied_to_parameters`, `number_literals`, the Int rules for earlier batches and function values, the per-instance `literal_types` in the analysis, layout and compiler, and the bound de-duplication only copied literals needed (−353 lines of Rust). A second commit sorts the unsettled literals by a key computed once, which was quadratic for a list of 20,000 values. The manual's function and augmented-assignment pages are updated. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Analysing every program in the advent-of-code-ndc repository turned up
three things the type checker got wrong. Five programs that failed
analysis now pass, including one master rejected too.
- **A literal passed for an `Iterable<T>` became an `Iterable` itself.**
Binding a literal's still-open type to an `Iterable<T>` parameter
unified the two, so the value's type became `Iterable<String>`, which
only a parameter may be. In `let cur, path = ("A", ["A"]); path.len;
path ++ [cur]` the `++` then found no overload. Such a literal is now
settled as the sequence it holds (a `List<String>` here), and its
elements are bound to `T`.
- **An overloaded function used as a value in a generic function was
decided once for the body.** `fn signs(xs) => xs.map(signum)` reported
that nothing says which `signum` is meant, though each call's types do.
A function value now waits like a call: it's part of the function's
scheme, each instance picks the overload its types call for, and the
body compiled for `Any` arguments leaves the pick to the program. There
the value is a new kind of function, `Function::Overloads`, made by a
new `MakeOverloads` instruction from the overloads looked up where the
value is made, most specific first; each call calls the first that
accepts its arguments. Being an ordinary function, it can be passed to
built-ins like `map`, captured, cached by a `pure fn`, and checked
against a function type. Copying the scheme to a call now gives the
value the call's types, and picking it counts as settling what it
returns.
- **`first` and `last` only took lists.** Their implementation takes any
slice, and programs call them on pairs: `.map(first)` over `(point,
flag)` tuples. They now also take tuples of one to twelve elements,
typed by the element they return, and `zip` gives typed tuples for up to
twelve sequences (it went up to four). A signature can't say "a tuple of
any length", so each length gets its own overload; twelve is where
Rust's standard library stops for tuples.
The manual's section on overloaded function values covers generic
functions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
) A review of the type-checker branch (PR #291) found these, all independent of how `Any` works (which is being redesigned separately, #331). Bug fixes: - A `pure fn` shared one cache between `1` and `1.0`, which are equal, so `same(1.0)` returned the cached Int: each set of argument types of a `pure fn` now gets an instance, and a cache, of its own. - A tuple whose elements still waited on a literal iterated as `Any`, so `(x - 1, y + 1).map(abs)` was rejected while the list form worked. A single-overload call now waits while a sequence argument waits to iterate. - A later call could take a nested function away from an earlier one: in `fn run2() { fn d(x) => "inner"; let r = d(1); d(2.5); r }`, `d(1)` ended up calling an outer `fn d(x: Int)`. When the nearest scope that takes a call has one overload whose parameters take whatever its literals become, the call commits to it. - A range's bounds weren't required to be Ints, so `let xs = [0.5, n]` could turn `n` into a Float and `0..n` failed as the program ran. The bounds are now Ints, and a Float or Number bound is an analysis error. - `xs[1..3] = 5..7` passed analysis and failed as the program ran: a list slice now takes any sequence, as its signature says. Simplifications (from the review, −84 lines): delete the unused `StaticType::with_element_type` and a slice branch of `validate_assignment_write` that could never run, replace `TypeVars::try_settle` with `try_unify`, define `held_literal` as the first of `held_literals`, and give a value callee the same checks path as other calls. Analysis of the large benchmark files is 2–3% slower, from the tuple fix's extra check per sequence parameter; the VM is unchanged. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…_subsequence 📐 (#346) Groundwork for making `Any` explicit (#331): these built-ins gave `Any` that nobody wrote, which would need a cast at every use once `Any` only comes out through `as`. - `panic(message)` stops the program with the message and has type `Never`, so it can end a branch whose other branches give a value: `if c { 1 } else { panic("no such thing") }` is an `Int`. `assert(false)` can't do this, as `assert` returns when its condition holds. - `combinations(xs, k)` and `permutations(xs, k)` now give lists rather than tuples, as `k` is only known when the program runs: `Iterator<List<T>>` and `List<List<T>>` instead of `Any`. Destructuring them works as before; they print as `[1,2]` rather than `(1,2)`. On the advent-of-brian day 8 solution, nothing in the pair loop is picked as the program runs any more, which made part 2 about a third faster. - `find_subsequence` returned the index or `()`, typed `Any`. It now returns an `Int` and fails when the subsequence isn't there, and the new `find_subsequence?` returns an `Option<Int>`, like `first`/`first?`. An empty subsequence is found at 0 instead of panicking. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Making `Any` explicit (#331) leaves a cast as the only way to get a value out of `Any`, and there was no way to test a value's type: `x as T` stops the program when `x` isn't a `T`. `x as? T` is that test: `Some(x)`, as a `T`, when the value is a `T`, and `None` when it isn't. It has type `Option<T>`. ```ndc let v: Any = 3; if (v as? Int).is_some() { print("an int"); } print([1, "a"] as? List<Int>); // None ``` - Lexer, parser, formatter, highlighter and LSP completion know `as?`, which has the precedence of `as` (so `(x as? Int).is_some()` needs its parentheses) and needs a space after it, as `?` can be part of a name. - The analyser checks it as it checks `as`: a cast that can never succeed is an error (it would always be `None`), and one it can prove needs no check. Unlike `as`, it doesn't settle what isn't known about the value: testing `xs as? List<Int>` leaves an empty list free to hold Strings. A number written there still takes the type: `1 as? Float` is `Some(1.0)`. - A new `TryCast` instruction converts the value as `as` does, functions included (they get their calls checked), and gives `None` instead of failing. Its body is out of the VM's main loop, so calls run exactly as before (`fib(27)` executes the same number of instructions). The manual's casts page has a section on it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…348) Closes #326. When a check failed, the analyser gave the failed expression the type `Any`, and everything built from it could fail again, so one mistake gave several errors whose help pointed the wrong way: ```ndc struct Player { name: String, score: Int } let players = [Player("ann", 3), Player("bob", 5)]; print(players.map(fn(p) => p.len).sum()); ``` reported the missing `len` and then "No function called 'sum' found that matches the arguments 'List<Any>'", with a hint to cast. It now reports only the first. This is also groundwork for making `Any` explicit (#331), where an error-recovery `Any` would cause even more such errors. - A failed check gives the new `StaticType::Error`, which fits anything both ways, joins with anything as itself, and iterates and unpacks as itself. It's only used by the analyser: the types it hands to the compiler, VM and LSP turn it into `Any`, which is what hover and inlay hints showed before. - A call whose arguments (or callee) contain it is skipped without a report, and returns it; the unknowns it would have settled become it too, so a lambda passed to it stays quiet. The places that reported a failure and returned `Any` (no overload, a committed overload that didn't fit, tuple index out of bounds, growing recursion, not iterable, a failed unpack, bad annotations, a type that grows too large, names in a statement with name errors) now return it. - A literal keeps its own type when an error is written into it, and the debug-build check of early picks skips picks whose arguments now contain it. As with rustc and TypeScript, a call passed a failed value reports nothing about its other arguments either. Errors in a value's declared type are still reported: after `let s: String = missing();`, `let n: Int = s;` is still an error. Every existing test program reports the same errors as before; analysis time is unchanged. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
No description provided.