Aether Type Inference Guide
How Type Inference Works
Aether implements a type inference system that automatically deduces types from context, reducing the need for explicit type annotations in most cases.
Basic Inference
From Literals
x = 42 // inferred: int
pi = 3.14 // inferred: float
name = "Alice" // inferred: string
flag = true // inferred: bool
p = null // inferred: ptr
A bare integer literal always infers as int there is no long literal or long <value> prefix. To get a long (64-bit) binding, annotate the declaration explicitly:
big = 0 // inferred: int
long wide = 0 // explicit: long (64-bit)
From Expressions
a = 10
b = 20
sum = a + b // inferred: int (both operands are int)
x = 3.14
y = 2.0
result = x * y // inferred: float
From Functions
// Return type inferred from return statement
add(a, b) {
return a + b // monomorphizes to one signature from the first
// inferred call site (not per-call polymorphism)
}
// Parameters inferred from usage
multiply(x, y) {
return x * y
}
main() {
n = add(10, 20) // add inferred as: int -> int -> int
f = multiply(3.14, 2.0) // multiply inferred as: float -> float -> float
}
Advanced Inference
Struct Fields
struct Point {
x, // Type inferred from initialization
y
}
struct Player {
name,
health,
score
}
main() {
p = Point{ x: 10, y: 20 } // x, y inferred as int
player = Player{
name: "Alice", // string
health: 100, // int
score: 0 // int
}
}
Struct fields infer their types from the initialisation site. A struct declared with bare field names (no : type) and never initialised keeps the fields at the parser's fallback type. Always initialise via a struct literal at least once, or annotate the field explicitly.
Array Elements
nums = [1, 2, 3, 4, 5] // inferred: int[]
names = ["Alice", "Bob"] // inferred: string[]
mixed_ok = [1.0, 2.0, 3.0] // inferred: float[]
Through Assignments
x = 42 // x: int
y = x // y: int (inferred from x)
z = y + 10 // z: int (inferred from y + int)
When to Use Explicit Types
While inference handles most cases, explicit types are useful for:
1. Function Signatures (Documentation)
// Explicit types make intent clear
calculate_damage(base: int, defense: int): int {
return base - defense
}
// vs inferred (less clear to reader)
calculate_damage(base, defense) {
return base - defense
}
2. Complex Scenarios
// When inference might be ambiguous
process_data(input: float[]) {
// Explicit type ensures correct interpretation
}
3. Public APIs
// Export with explicit types for clarity
export calculate_score(kills: int, deaths: int, assists: int): float {
return (kills + assists / 2.0) / (deaths + 1)
}
Mixed Explicit/Inferred
You can mix explicit and inferred types:
// Some parameters explicit, some inferred
process(data, threshold: int) {
return data > threshold // data type inferred from usage
}
// Explicit return, inferred parameters
format_name(first, last): string {
return "${first} ${last}" // parameters inferred as string
}
How Inference Works Internally
Phase 1: Constraint Collection
The compiler walks the AST and collects type constraints:
x = 42
// Constraint: x must be int (from literal 42)
y = x + 10
// Constraint: y must be int (from x:int + 10:int)
Phase 2: Constraint Solving
Constraints are propagated iteratively until all types are known:
Iteration 1: x = int (from literal)
Iteration 2: y = int (from x + int)
Done: All types resolved
Phase 3: Validation
Once inferred, types are validated for consistency:
x = 42
x = "hello" // rejected at the C-compile stage (-Wint-conversion),
// not by an Aether type-inference diagnostic
Null and Pointer Inference
The null keyword is typed as ptr:
conn = null // inferred: ptr
conn = tcp_connect_raw(...) // still ptr, type is consistent
Integer 0 interoperates with ptr only in limited cases. Assigning 0 to a variable that is already ptr-typed is fine (a null slot). But 0-first-then-ptr widening is not a general rule: the pass in compiler/analysis/type_inference.c (widen_ptr_assigned_locals) only retypes a 0-initialized local as ptr when that local lives inside a ptr-returning function. The same pattern in a plain int-returning main() keeps the local as int and fails to compile with incompatible pointer to integer conversion:
listen(): ptr {
server = 0 // widened to ptr: this function returns ptr
server = tcp_listen_raw(80) // OK
return server
}
Constants
Top-level const declarations infer their type from the value:
const MAX = 100 // inferred: int
const NAME = "hello" // inferred: string
Edge Cases
Ambiguous Types
A local declaration uses C-style prefix syntax (type name = value), not the postfix name: type form. A bare x: int local does not parse; it reports error[E0100]: Expected statement in block. Postfix name: type is valid only for function parameters and struct fields.
// Give the local an explicit type up front
int x = get_value() // OK - x is int
Generic Functions
// Currently not supported - specify explicit types
identity(x: int): int {
return x
}
// Future: Generic type parameters
// identity<T>(x: T): T {
// return x
// }
Performance Impact
Type inference happens at compile-time only:
- Zero runtime overhead
- Same generated C code as explicit types
- Full type safety maintained
- No performance difference
Best Practices
Do:
- Let inference handle obvious cases (literals, simple expressions)
- Use explicit types for public APIs and exports
- Use explicit types when it improves readability
- Mix explicit/inferred for balance
Don't:
- Over-annotate everything (defeats the purpose)
- Rely on inference for complex logic (document with types)
- Leave ambiguous types un-annotated
Examples
Good: Minimal but Clear
struct Player {
name: string, // Explicit for documentation
health, // Inferred from init
score // Inferred from init
}
calculate_total(base, bonus) { // Inferred from usage
return base + bonus
}
main() {
p = Player{ name: "Alice", health: 100, score: 0 }
total = calculate_total(p.score, 10)
}
Better: Strategic Explicit Types
struct Player {
name: string,
health: int, // Explicit: important field
score: int // Explicit: important field
}
// Explicit return: public function
calculate_total(base, bonus): int {
return base + bonus
}
main() {
p = Player{ name: "Alice", health: 100, score: 0 }
total = calculate_total(p.score, 10)
}
Error Messages
Compiler diagnostics are E-coded (for example E0100, E0301) and print with a source span and a help: line. There is no free-form "Type inference failed" message. For instance, a call to a name the compiler cannot resolve reports:
error[E0301]: Undefined function 'unknown_value'
When you do need to pin a local's type, use the C-style prefix form int x = ...; the postfix x: int = ... form does not parse.
Summary
- Inference covers literals, simple expressions, and single-call-site functions.
- Explicit types double as documentation; use them for public APIs and exports.
- Inference is compile-time only, with no runtime cost.
- Inferred types carry the same guarantees as explicit ones.
Type inference removes most annotations while keeping the generated C strictly typed.