Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
2.2.0 - 2026-09-04
Added
analyse,analyse_module,analyse_with_cacheandanalyse_package: theannotate*results plus, for every field access and every bare name in call position, what it resolved to. EachResolvedReferencegives the spans of the access, its label and the accessed value, and aResolution: a record field and the record’s type, a module function, constant or constructor under the module’s canonical path (not the import alias), a local variable, orUnresolved. The variants are named after the compiler’sValueConstructorVariant.annotate*keep their signatures — they are these functions with the resolutions dropped.
Fixed
- A piped call’s callee is no longer annotated twice.
left |> f(args)infersfonce to measure its arity and once as part of the call; the arity probe now runs on a state that is thrown away, soexpressionshas a single entry for the callee’s span, holding the type the call gave it. - Variant narrowing now follows the value, not the name it was bound under.
After
let assert Loud(..) = l,let io = lstill readsio.printlnas the field, and so do a tuple pattern, a closure’s return, a record update, ausevalue and a generic constant. It is dropped where the compiler drops it: through a type variable, out of acaseresult, into a bare constructor, and at a top-level definition’s generalized type. - Calling a field on a value a pattern has narrowed —
Loud(..) as io -> io.println("hi")— now calls the field even when a module namediois in scope. Previously the module function won, unlike the compiler. Reading and calling a field now resolve the same way. Loud(..) as io | Quiet(..) as iono longer narrowsioto either variant, matching the compiler. Acasesubject follows the compiler’s other rule: only the first alternative may narrow it, socase io { Loud(..) | _ -> ... }keeps the narrowing and_ | Loud(..)does not.- A record type imported under a discarded alias (
import kinds.{Box} as _k) keeps its field accessors, in the importing module and downstream. Close(..) as io | kinds.Near(..) as io, withNearimported asClose, keeps its narrowing: the two spellings are recognised as one constructor.- A field is only accessible on the whole type when every variant declares it
at the same position with the same type.
yinA(x: Int, y: String)/B(y: String)is no longer an accessor, matching the compiler. - Passing a labelled argument to a function stored in a record field is now an error, as in the compiler. Previously the labels of an unrelated function with the same name were used to reorder the arguments.
- A local binding now shadows a callable’s labels as well as its type. After
let greet = fn(who: String) -> String { who }, callinggreet(name: "hi")is an error rather than being reordered by the top-levelgreet’s labels, as in the compiler. A parameter and a shadowed unqualified import behave the same way. - A top-level definition replaces the labels of the unqualified import it
shadows.
import imported.{greet}followed by an unlabelledpub fn greet(who: String)no longer letsgreet(name: "hi")borrow the import’sname:; a constant and a record constructor clear the shadowed name’s labels the same way. - Expressions inside a
panic as/todo asmessage are now annotated. Previously nothing in the message got a type —nameinpanic as { "no such user: " <> name }had no annotation at all. The message is now inferred like any other expression and checked againstString, so an ill-typed message is reported instead of ignored.panicandtodostill unify with anything, and the message-less forms are unchanged.
2.1.1 - 2026-08-24
Fixed
- The shipped
.gradedspec is updated to the current spec format.
2.1.0 - 2026-08-05
Changed
- Building a disk resolver or default options no longer touches the filesystem;
I/O happens on first resolution.
disk_resolver()anddefault_options()are now pure: they scanbuild/packagesand read module sources only when the resolver is actually invoked during annotation, rather than eagerly at construction. Behaviour is otherwise unchanged, including that a missingbuild/packagesor an unreadable source surfaces asError(Nil)— now at resolution time instead of construction.
2.0.0 - 2026-07-16
Changed
-
Consolidated the public API into a single
girardmodule. TheType,Scheme, andErrortypes and their constructors, previously exposed fromgirard/types, now live ingirard, and thegirard/typesmodule has been removed. This is a breaking change to the import path; update imports:// Before import girard import girard/types.{type Type, Fn, Named} // After import girard.{type Type, Fn, Named} -
Updated
glanceto 7.0.0, which parses arithmetic in bit-array pattern segment sizes (e.g.<<_:size(n - 1)-bytes, tail:bytes>>). Modules using that form now annotate instead of failing to parse, values referenced in segment sizes are typed asInt, and a top-level constant used only as a segment size now counts as a dependency of the definition using it. Because the public API accepts and annotatesglanceASTs, this is a breaking dependency bump: callers passing pre-parsed modules must move to glance 7.
1.1.1 - 2026-06-20
Changed
- Updated
glanceto 6.1.0, which parses string-prefix patterns that discard the rest (e.g."a" <> _). Modules using that form now annotate instead of failing to parse.
1.1.0 - 2026-06-07
Added
- A reusable interface cache. Annotate a module or walk a package while reusing the work done for shared imports across calls, then drop a single module’s cached interface when its source changes.
Changed
- Faster annotation, most noticeably when typing many modules or re-annotating the same module as it changes.
- Now requires Gleam 1.15.0 or newer.
1.0.0 - 2026-06-02
Added
- Annotate a single Gleam module or a whole package, reporting the inferred type of every expression — keyed by its source span — and the signature of every top-level function and constant.
- Accepts either source text or a
glanceAST you have already parsed. - Choose the build target (Erlang or JavaScript) and supply your own resolver for imported modules, with a default resolver that reads them from disk.
- Best-effort package annotation: definitions that cannot be typed are reported individually, and every other definition in the module is still annotated.
- Render any inferred type to Gleam syntax, produce a human-readable report, and describe why a module could not be typed.
- A command-line interface that annotates a file or standard input.