Skip to content

feat: type annotations 📚#131

Merged
timfennis merged 14 commits into
masterfrom
feature/type-annotations
May 7, 2026
Merged

feat: type annotations 📚#131
timfennis merged 14 commits into
masterfrom
feature/type-annotations

Conversation

@timfennis
Copy link
Copy Markdown
Owner

@timfennis timfennis commented Apr 18, 2026

This PR adds support for type annotations in a few positions.

let a: Int = 3;
fn bar(b: Int) -> Int { b + 1 }

It notably does not support:

  • Generic type annotations
  • Type annotations in for loops
for a, b: (Int, Int) in test {

}

@timfennis timfennis force-pushed the feature/type-annotations branch from c2ca703 to 234f27a Compare May 7, 2026 08:03
timfennis and others added 13 commits May 7, 2026 10:15
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>
@timfennis timfennis force-pushed the feature/type-annotations branch from 234f27a to 8b928a8 Compare May 7, 2026 08:17
@timfennis timfennis marked this pull request as ready for review May 7, 2026 08:25
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread ndc_analyser/src/analyser.rs
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread ndc_analyser/src/analyser.rs
@timfennis timfennis merged commit 4d272e3 into master May 7, 2026
1 check passed
@timfennis timfennis deleted the feature/type-annotations branch May 7, 2026 11:59
timfennis added a commit that referenced this pull request May 20, 2026
## 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant