Skip to content

Commit 4d272e3

Browse files
timfennisclaude
andauthored
feat: type annotations 📚 (#131)
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 { } ``` --------- Co-authored-by: Claude <[email protected]>
1 parent 59b0fd9 commit 4d272e3

29 files changed

Lines changed: 1184 additions & 216 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
fn fib(n: Int) -> Int {
2+
if n <= 1 { 1 } else { fib(n - 2) + fib(n - 1) }
3+
}
4+
5+
fib(26);
Lines changed: 40 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,42 @@
11
# Types
22

3-
Andy C++ is currently a dynamically typed language, that means that type checks are performed at runtime. Although
4-
you currently can't annotate your variables using type names they do have types at runtime.
5-
6-
The type system is hierarchical with the root type being `Any`:
7-
8-
* Any
9-
* [Option](./types/option.md)
10-
* [Boolean](./types/boolean.md)
11-
* [Number](./types/number.md)
12-
* Integer
13-
* Int64 (64bit signed)
14-
* Bigint (unlimited size)
15-
* Float
16-
* Complex
17-
* Rational
18-
* Sequence
19-
* [String](./types/string.md): A mutable list of characters
20-
* [List](./types/list.md): A mutable list
21-
* [Tuple](./types/tuple.md): An immutable list
22-
* [Unit](./types/unit.md)
23-
* [Map](./types/map-and-set.md): A hashmap that associates keys with values
24-
* [Deque](./types/deque.md): A double ended queue
25-
* [MinHeap & MaxHeap](./types/min-max-heap.md): Min/max Heap
26-
* Iterator: A type that can be consumed and produces values (Currently only used for range expressions like `5..100`)
27-
* [Function](./types/function.md)
28-
29-
> **Note:** `Any` is the base type for all other types. When you declare a function, its arguments default to type `Any`.
30-
> Currently, the `Any` type is implicit and does not appear explicitly in the language.
3+
Andy C++ runs as a dynamically typed language — values carry their types at runtime and most checking happens then. You can also attach type annotations to variables, function parameters, and return values, and the analyser will use them to flag obvious mismatches before the program runs.
4+
5+
The type system is hierarchical with `Any` at the root:
6+
7+
* `Any`
8+
* [`Option<T>`](./types/option.md)
9+
* [`Bool`](./types/boolean.md)
10+
* [`Number`](./types/number.md)
11+
* `Int` — machine `i64` or arbitrary-precision `BigInt`, picked automatically
12+
* `Float`
13+
* `Complex`
14+
* `Rational`
15+
* `Sequence<T>`
16+
* [`String`](./types/string.md): a mutable list of characters
17+
* [`List<T>`](./types/list.md): a mutable list
18+
* [`Tuple<T, ...>`](./types/tuple.md): an immutable list
19+
* [`()`](./types/unit.md): unit, the empty tuple
20+
* [`Map<K, V>`](./types/map-and-set.md): a hashmap that associates keys with values
21+
* [`Deque<T>`](./types/deque.md): a double-ended queue
22+
* [`MinHeap<T>` / `MaxHeap<T>`](./types/min-max-heap.md): min/max heap
23+
* `Iterator<T>`: produces values when consumed (currently only from range expressions like `5..100`)
24+
* [`Function`](./types/function.md)
25+
26+
These are also the names you write in annotations. Generic types take their parameters in angle brackets:
27+
28+
```ndc
29+
let xs: List<Int> = [1, 2, 3];
30+
let table: Map<String, Int> = %{"a": 1, "b": 2};
31+
let maybe: Option<Any> = Some("hi");
32+
let pair: Tuple<Int, String> = (1, "hi");
33+
let pair2: (Int, String) = (1, "hi"); // tuple shorthand
34+
```
35+
36+
Nested generics work too — the parser handles the `>>` ambiguity for you:
37+
38+
```ndc
39+
let grid: List<List<Int>> = [[1, 2], [3, 4]];
40+
```
41+
42+
> **Note:** `Any` is the base type for every other type, so an `Any`-annotated binding will accept anything. When a parameter or value has no annotation and the analyser can't infer a type, it falls back to `Any`. There is also a `Never` type used internally for things like `break` that don't produce a value — you'll rarely need to write it by hand.

‎manual/src/reference/types/function.md‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,33 @@ let x = fn(y) => y, 3;
8989
let x = fn(y) => (y, 3);
9090
```
9191

92+
## Type annotations
93+
94+
Parameters and return values can carry type annotations, just like `let` bindings:
95+
96+
```ndc
97+
fn greet(name: String) -> String => "hello " <> name;
98+
99+
fn add(x: Int, y: Int) -> Int {
100+
x + y
101+
}
102+
```
103+
104+
Annotations are optional — leave them off and the parameter is treated as `Any`. Mix and match as you like:
105+
106+
```ndc
107+
fn first(xs: List<Int>) => xs[0]; // params annotated, return inferred
108+
fn count(xs) -> Int => len(xs); // return annotated, params inferred
109+
```
110+
111+
If the body produces a value that doesn't fit the declared return type, the analyser flags it:
112+
113+
```ndc
114+
fn bad() -> Int { "hello" } // ERROR: mismatched types
115+
```
116+
117+
A return-type annotation also helps the analyser understand recursive calls — without it, a recursive call resolves against an unknown return type and you can lose precision.
118+
92119
## Function overloading
93120

94121
You can overload functions by declaring multiple `fn` definitions with the same name and different parameter counts.
@@ -108,7 +135,7 @@ fn foo(a) { a + 1 }
108135
fn foo(a) { a + 2 } // ERROR: redefinition of 'foo' with 1 parameter
109136
```
110137

111-
> **Note:** The engine can also overload functions by argument type, and the standard library uses that support in a few places. You cannot write those overloads in user code yet because the language does not let you declare argument types in function signatures.
138+
> **Note:** The engine can also dispatch by argument type, and the standard library uses that to register specialised overloads (for example, an `Int`-only fast path for `+`). User code can't declare two overloads with the same name and arity yet, even when the parameter types differ — the resolver only distinguishes overloads by parameter count.
112139
113140
## Function shadowing
114141

‎manual/src/reference/variables-and-scopes.md‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,43 @@ pos = ("a", "b"); // type is still Sequence<Any>
5858

5959
> **Tip:** For the best type inference, initialize variables with a value that matches the intended type. For example, use `let pos = (0, 0);` instead of `let pos = ();` if you intend to store a 2-tuple of numbers.
6060
61+
## Type annotations
62+
63+
You can pin a variable's type by adding `: Type` after the name. The initialiser still has to fit, the analyser just checks it for you up front.
64+
65+
```ndc
66+
let count: Int = 0;
67+
let name: String = "world";
68+
let xs: List<Int> = [1, 2, 3];
69+
```
70+
71+
A subtype is fine — `Int` fits where `Number` is asked for, and so on:
72+
73+
```ndc
74+
let n: Number = 3; // OK: Int is a Number
75+
let x: Any = "anything"; // OK: everything is Any
76+
```
77+
78+
A mismatch is rejected with a `mismatched types` error:
79+
80+
```ndc
81+
let x: Int = "hello"; // ERROR: mismatched types: found String but expected Int
82+
```
83+
84+
Once a binding has an annotation, it stays locked to that type. Reassignment and augmented assignment can't widen it the way they widen an inferred binding:
85+
86+
```ndc
87+
let x: Int = 5;
88+
x = "test"; // ERROR: mismatched types
89+
x /= 2; // ERROR: division can produce a Rational, which doesn't fit in Int
90+
```
91+
92+
If you want a binding that widens freely, just leave the annotation off. Annotations are opt-in.
93+
94+
The same syntax shows up on function parameters and return types — see the [Function](./types/function.md) page.
95+
96+
See [Types](./types.md) for the full list of names you can use, including generics like `List<T>`, `Map<K, V>`, and tuple shorthand `(Int, String)`.
97+
6198
## Destructuring
6299

63100
Destructuring works more like Python than Rust. Commas matter more than the delimiters, so `[]` and `()` both work.

0 commit comments

Comments
 (0)