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.
3.0.0 - 2026-09-05
Added
AnnotatedModule.dropped: the name and span of each top-level function or constant@targetleft out of this build. A span with no annotation is now explainable — girard declined the definition (skipped), dropped it for the target (dropped), or never records that position at all.
Changed
annotate,annotate_module,annotate_with_cacheandannotate_packagereturn the resolutions 2.2.0 introduced, on a newAnnotatedModuleresolutionsfield. Their signatures are unchanged, so reading fields off the result still works as it did; constructing or fully destructuringAnnotatedModulemust name the two new fields,resolutionsanddropped.Resolution’sRecordFieldrenames its first fieldrecord→receiver. It holds the type of the value the field was read from — theuserinuser.name— whichrecordwas too easy to read as the whole access. Positional matches (RecordField(type_, label)) are unaffected; only a labelled one needs the new name.
Removed
-
analyse,analyse_module,analyse_with_cacheandanalyse_package, and theAnalysisthey returned. 2.2.0 shipped them besideannotate*so 2.x consumers could stay put; 3.0.0 drops them, with no alias. Call theannotate*of the same name and read one record instead of two:// Before let assert Ok(analysis) = girard.analyse(source, options) analysis.annotated.expressions analysis.resolutions // After let assert Ok(annotated) = girard.annotate(source, options) annotated.expressions annotated.resolutions
Fixed
- A
@targetsibling pair is listed once infunctions/constants, under the active target’s signature. Both used to be listed, and both under the active one’s signature. A definition dropped for the target no longer takes a same-named unqualified import’s signature either. - A definition in
skippedno longer also appears inannotated.functions, asModuleResultpromises. A localpub fn gthat does not type, over animport imported.{g}it shadows, was listed in both — infunctionsunder the import’s signature rather than its own. AnnotatedModule.functionsandconstantsare in the source order their docs have always promised; both were reversed. Reading a definition by name is unaffected, but walking either list in order was walking the module backwards.girard.reportlists definitions in the same new order.- A skipped definition no longer breaks calls to the import it shadows. In
best-effort mode the skipped definition’s argument labels outlived it, so a
later call using the import’s own labels was wrongly rejected with
UnknownLabelorAmbiguousCall. Those calls now type. - A resolution now names the module a call was typed against. Where
a’s owngwas skipped and shadowed animport imported.{g}, an importer callinga.gwas told it had calleda.grather thanimported.g. - A lambda that is piped into, called directly, given as a
usecallback or passed to a capture now sees its parameter types before its body is inferred, as the compiler does. A field access on such a parameter resolves at the access instead of being deferred and published asUnresolved. With a module of the parameter’s name in scope it now reads the field where it previously took the module’s export, which changes the inferred type when the two differ. - A
use’s callback and a capture’s hole are placed by the same reorder as the arguments around them, so a labelled argument taking a later declared slot no longer leaves either in the wrong one, and an argument written out of declared order is checked against the parameter it was placed against. - A lambda checked against a known function type shares the type-variable
names its own annotations write, as one inferred without a known type
already did: in
fn(m: Msg(a)) -> Next(Msg(a))the twoas are one type wherever the lambda is checked.
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.