feat: type annotations 📚#131
Conversation
c2ca703 to
234f27a
Compare
Co-authored-by: Claude <noreply@anthropic.com>
The lexer greedily tokenises `>>`, `>=`, and `>>=` as single tokens, which breaks nested generic type annotations like `List<List<Int>>`. The parser now splits these compound tokens when closing angle brackets in type parameter lists. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduces TypeBinding enum (Inferred/Annotated) to track whether a variable was declared with an explicit type annotation. Annotated bindings refuse type widening on reassignment, emitting a type mismatch error instead. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extends the annotated binding check to OpAssignment (+=, /=, etc.). Also fixes a bug where destructured `let v, n = 100, 100` incorrectly marked sub-bindings as annotated. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds `named_parameter` parser for function param lists, separate from `named_binding` (used by `let` destructuring). Params now accept optional type annotations (e.g. `fn foo(x: Int)`). Also adds `TypeSignature::from_annotated_bindings` constructor. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Parse `-> Type` return type annotations on function declarations - Analyser validates inferred return type against annotation - Parameter type annotations now feed into analysis (no longer ignored) - Register Int-specific overloads for +, -, *, % with fast i64 path - Register Float-specific overloads for +, -, *, /, % - Widen container element type on index op-assignment (e.g. x[0] /= 3) - Add StaticType::with_element_type helper Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…holders 🔄 When pre-registering a recursive function, use the declared return type annotation instead of Any so that recursive calls resolve correctly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduce FunctionParameter to unify parameter representation in the AST, replacing TypeSignature on FunctionDeclaration. Move inferred return types to an AnalysisResult side table so the LSP can distinguish annotated vs inferred return types. Walk parameter lvalues in the visitor so parameter type hints are emitted. Also compute the LUB of return types across all overload candidates in dynamic bindings, improving type inference for overloaded functions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
234f27a to
8b928a8
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8b928a8510
ℹ️ 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".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9794545e4d
ℹ️ 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".
## Summary Fixes #139. Regression from #131 (the type-annotation work): vectorized tuple arithmetic crashed at runtime whenever the result of one tuple op was fed into another, e.g. `let diff = a - b; diff * diff` or `(a - b) * (a - b)`. ## Root cause In `resolve_function_with_argument_types`, the `Binding::Dynamic` arm inferred the call's result as the LUB of every candidate's declared return type. That LUB is sound only when one of those candidates is statically guaranteed to fire. With dynamic dispatch the value-level dispatcher can fall through to elementwise / vectorized handling at runtime and produce a value no declared overload returns. So `let diff = a - b` over tuples was inferred as `diff: Number` (the LUB of the numeric overloads), and the follow-up `diff * diff` then exact-matched `(Number, Number) -> Number`, emitted a direct `Call` instead of `OverloadSet` dispatch, and bypassed dynamic dispatch entirely — surfacing as `expected number, got Tuple<Int, Int, Int>` at runtime. The same hole shows up whenever a `Tuple` flows through dynamic dispatch — e.g. `(id(1), id(2)) - (id(3), id(4))` where `id` returns `Any` — because the analyser still gets back a confident-but-wrong `Number`. ## Fix Widen the `Binding::Dynamic` arm's result to `StaticType::Any`. Runtime-dispatched calls don't have a sound static bound on their result, so the analyser stops pretending otherwise. Every cascade rung now stays on dynamic dispatch, and the VM's existing value-level dispatcher decides at runtime. ## Changes - `ndc_analyser/src/analyser.rs` — `Binding::Dynamic` arm returns `Any` instead of LUB-of-candidate-returns. No more special-cased vectorization detection. - `tests/functional/programs/900_bugs/bug0021_chained_vectorized_tuple_arith.ndc` — regression test covering the original repro from #139 plus a `Tuple<Any, …>` variant (`fn id(x) -> Any => x` to keep the type-erasure independent of stdlib evolution). ## Performance Acceptable regression — within noise on this machine. `hyperfine --warmup 3 --runs 30`: | program | before (ms) | after (ms) | ratio | |---|---|---|---| | sieve | 112.8 ± 5.7 | 109.1 ± 4.1 | 1.03× faster after | | matrix_mul | 58.9 ± 4.4 | 58.1 ± 3.8 | 1.01× faster after | | fibonacci_typed | 57.4 ± 4.5 | 59.1 ± 4.2 | 1.03× slower after | A smaller 5-run sweep over `fibonacci`, `quicksort`, `hof_pipeline`, `ackermann`, `pi_approx` all landed at 1.00×–1.06× in either direction with σ bigger than the delta. ## Trade-off worth flagging `Binding::Dynamic` sites lose their LUB-derived inlay hints — e.g. a user-defined function with `(Int) -> Int` and `(Float) -> Float` overloads called via dynamic dispatch used to surface `Number` on the LHS of a `let`, and will now surface `Any` (filtered from inlay hints). The LUB was always a heuristic that happened to look right; the soundness fix removes it. Happy to revisit if the LSP UX hit feels too steep. 🤖 PR description by Claude. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This PR adds support for type annotations in a few positions.
It notably does not support: