Static Type Analysis
Calcit includes a built-in static type analysis system that performs compile-time checks to catch common errors before runtime. This system operates during the preprocessing phase and provides warnings for type mismatches and other potential issues.
Overview
The static analysis system provides:
- Type inference - Automatically infers types from literals and expressions
- Type annotations - Optional type hints for function parameters and return values
- Compile-time warnings - Catches errors before code execution
- Zero runtime overhead - All checks happen during preprocessing
Type Annotations
Function Parameter Types
Annotate function parameters using assert-type within the function body:
defn calculate-total (items)
assert-type items :list
reduce items 0
fn (acc item) (+ acc item)
Return Type Annotations
There are two ways to specify return types:
1. Formal Hint (hint-fn)
Use hint-fn with return-type at the start of the function body:
defn get-name (user)
hint-fn $ return-type :string
|demo
2. Compact Hint (Trailing Label)
For defn and fn, you can place a type label immediately after the parameters:
defn add (a b) :number
+ a b
let
f $ fn (x y) :number $ + x y
f 10 20
Multiple Annotations
defn add (a b) :number
assert-type a :number
assert-type b :number
+ a b
Supported Types
The following type tags are supported:
| Tag | Calcit Type |
|---|---|
:nil | Nil |
:bool | Boolean |
:number | Number |
:string | String |
:symbol | Symbol |
:tag | Tag (Keyword) |
:list | List |
:map | Hash Map |
:set | Set |
:tuple | Tuple (general) |
:fn | Function |
:ref | Atom / Ref |
:any / :dynamic | Any type (wildcard) |
Complex Types
Optional Types
Represent values that can be nil. Use the :: :optional <type> syntax:
defn greet (name)
assert-type name $ :: :optional :string
str "|Hello " (or name "|Guest")
Variadic Types
Represent variable arguments in & parameters:
defn sum (& xs)
assert-type xs $ :: :& :number
reduce xs 0 &+
Record and Enum Types
Use the name defined by defrecord or defenum:
defrecord User :name
defn get-name (u)
assert-type u User
.-name u
Built-in Type Checks
Function Arity Checking
The system validates that function calls have the correct number of arguments:
defn greet (name age)
str "|Hello " name "|, you are " age
; Error: expects 2 args but got 1
; greet |Alice
Record Field Access
Validates that record fields exist:
defrecord User :name :age
defn get-user-email (user)
.-email user
; Warning: field 'email' not found in record User
; Available fields: name, age
Tuple Index Bounds
Checks tuple index access at compile time:
let
point (%:: :Point 10 20 30)
&tuple:nth point 5 ; Warning: index 5 out of bounds, tuple has 4 elements
Enum Variant Validation
Validates enum construction and pattern matching:
defenum Result
:Ok :any
:Error :string
; Warning: variant 'Failure' not found in enum Result
%:: Result :Failure "|something went wrong"
; Available variants: Ok, Error
; Warning: variant 'Ok' expects 1 payload but got 2
%:: Result :Ok 42 |extra
Method Call Validation
Checks that methods exist for the receiver type:
defn process-list (xs)
; .unknown-method xs
println "|demo code"
; "Warning: unknown method .unknown-method for :list"
; Available methods: .map, .filter, .count, ...
Recur Arity Checking
Validates that recur calls have the correct number of arguments:
defn factorial (n acc)
if (<= n 1) acc
recur (dec n) (* n acc)
; Warning: recur expects 2 args but got 3
; recur (dec n) (* n acc) 999
Note: Recur arity checking automatically skips:
- Functions with variadic parameters (
&rest args) - Functions with optional parameters (
?markers) - Macro-generated functions (e.g., from
loopmacro) calcit.corenamespace functions
Type Inference
The system infers types from various sources:
Literal Types
let
x 42 ; inferred as :number
y |hello ; inferred as :string
z true ; inferred as :bool
w nil ; inferred as :nil
println "|demo code"
Function Return Types
let
numbers (range 10) ; inferred as :list
first-num (&list:first numbers) ; inferred as :number
println "|demo code"
Record and Struct Types
defstruct Point :x :y
let
p (%:: Point :x 10 :y 20) ; inferred as Point record
x-val (.:x p) ; inferred from field type
println "|demo code"
Type Assertions
Use assert-type to explicitly check types during preprocessing:
defn process-data (data)
assert-type data :list
&list:map data transform-fn
Note: assert-type is evaluated during preprocessing and removed at runtime, so there's no performance penalty.
Type Inspection Tool
Use &inspect-type to debug type inference. Pass a symbol name and the inferred type is printed to stderr during preprocessing:
defn demo ()
let
x 10
nums $ [] 1 2 3
assert-type nums :list
&inspect-type x ; Prints: [&inspect-type] x => number type
&inspect-type nums ; Prints: [&inspect-type] nums => list type
let
first $ &list:nth nums 0
&inspect-type first ; Prints: [&inspect-type] first => dynamic type
assert-type first :number
&inspect-type first ; Prints: [&inspect-type] first => number type
Note: This is a development tool - remove it in production code. Returns nil at runtime.
Optional Types
Calcit supports optional type annotations for nullable values:
defn find-user (id)
hint-fn $ return-type $ :: :optional :record
; May return nil if user not found
println "|demo code"
Variadic Types
Functions with rest parameters use variadic type annotations:
defn sum (& numbers)
hint-fn $ return-type :number
assert-type numbers $ :: :& :number
reduce numbers 0 +
Function Types
Functions can be typed as :fn. You can also assert input types:
defn apply-twice (f x)
assert-type f :fn
assert-type x :number
f (f x)
Disabling Checks
Per-Function
Skip checks for specific functions by naming them with special markers:
- Functions with
%in the name (macro-generated) - Functions with
$in the name (special markers) - Functions starting with
__(internal functions)
Per-Namespace
Checks are automatically skipped for:
calcit.corenamespace (external library)- Functions with variadic or optional parameters (complex arity rules)
Best Practices
1. Use Type Annotations for Public APIs
defn public-api-function (input)
hint-fn $ return-type :string
assert-type input :map
process-input input
2. Leverage Type Inference
Let the system infer types from literals and function calls:
defn calculate-area (width height)
; Types inferred from arithmetic operations
* width height
3. Add Assertions for Critical Code
defn critical-operation (data)
assert-type data :list
; Ensure data is a list before processing
dangerous-operation data
4. Document Complex Types
; Function that takes a map with specific keys
defn process-user (user-map)
assert-type user-map :map
; Expected keys: :name :email :age
println "|demo code"
Limitations
- Dynamic Code: Type checks don't apply to dynamically generated code
- JavaScript Interop: JS function calls are not type-checked
- Macro Expansion: Some macros may generate code that bypasses checks
- Runtime Polymorphism: Type checks are conservative with polymorphic code
Error Messages
Type check warnings include:
- Location information: namespace, function, and code location
- Expected vs actual types: clear description of the mismatch
- Available options: list of valid fields/methods/variants
Example warning:
[Warn] Tuple index out of bounds: tuple has 3 element(s), but trying to access index 5, at my-app.core/process-point
Advanced Topics
Custom Type Predicates
While Calcit doesn't support custom type predicates in the static analysis system yet, you can use runtime checks:
defn is-positive? (n)
and (number? n) (> n 0)
Type-Driven Development
- Write function signatures with type annotations
- Let the compiler guide implementation
- Use warnings to catch edge cases
- Add assertions for invariants
Performance
Static type analysis:
- Runs during preprocessing phase
- Zero runtime overhead
- Only checks functions that are actually called
- Cached between hot reloads (incremental)
See Also
- Polymorphism - Object-oriented programming patterns
- Macros - Metaprogramming and code generation
- Data - Data types and structures