Current section
Files
Jump to
Current section
Files
README.md
# girard
[](https://hex.pm/packages/girard)
[](https://hexdocs.pm/girard/)
[](https://github.com/alvivi/girard/actions/workflows/test.yml)
A Gleam source type annotator, in Gleam!
Runs type inference over Gleam source — replicating the real Gleam compiler — and
reports the inferred type of every expression (by source span) together with each
top-level definition's signature. Parsing is delegated to
[`glance`](https://hexdocs.pm/glance/).
The project is stable: its inferred types are validated differentially against
the real compiler across the hex ecosystem (see [`PACKAGES.md`](PACKAGES.md)).
## Why?
The Gleam compiler infers a type for every expression, but it does not expose
that information as a library: there is no API a tool can call to ask "what is
the type of the expression at this span?". girard exists to answer exactly
that question.
That makes it a building block for language tooling written in Gleam:
- **Editor tooling and language servers** — code actions, hovers, and
completions that need the type of the expression under the cursor. For
example, a "wrap this element" refactor only makes sense when it knows the
expression already has the element type it is wrapping.
- **Linters and analyzers** — rules that depend on types rather than syntax
alone, without reimplementing inference.
- **Code generation and refactoring tools** — codemods that must know a
binding's signature to rewrite call sites safely.
Because girard consumes [`glance`](https://hexdocs.pm/glance/) ASTs and keys
its annotations by source span, a tool that already parses with glance can join
inferred types directly back onto its own AST — no compiler invocation, no AST
fork, no parsing twice.
## Usage
Add the package to your Gleam project:
```sh
gleam add girard
```
Then annotate some source:
```gleam
import girard
import gleam/io
const code = "pub fn double(x) { x + x }"
pub fn main() {
io.println(girard.report(code))
}
```
This program outputs the following to the console:
```text
double: fn(Int) -> Int
19-20: Int
19-24: Int
23-24: Int
```
`report` is the quick, human-readable rendering. For programmatic use,
`girard.annotate(code, girard.default_options())` returns a structured
`AnnotatedModule`: each top-level definition's `Scheme` (in `functions` /
`constants`) and every expression's `Type` keyed by its source span (in
`expressions`). These are structured [`girard`](src/girard.gleam) values —
pattern-match on `Named`/`Fn`/`Var`/`Tuple`, or render one with
`girard.type_to_string`.
### Command line
```sh
gleam run -- path/to/file.gleam # annotate a file
gleam run -- - # annotate stdin
cat file.gleam | gleam run # annotate stdin
gleam run -- --help # usage
```
Imports are resolved from `src/` and `build/packages` (so `import gleam/list`
works); ill-typed input prints a single `// error: …` line.
### Annotating a `glance` AST you already parsed
If you have already parsed the source with
[`glance`](https://hexdocs.pm/glance/), hand the `glance.Module` to
`girard.annotate_module` instead of a source string, so the source is parsed
once, not twice. Each expression `Annotation` carries a `glance.Span` — the same
span glance puts on every AST node — so you join the inferred types onto your own
tree by span, and inspect them as structured values.
```gleam
import girard.{type Type, Fn, Named}
import glance
import gleam/dict.{type Dict}
import gleam/list
/// Parse once with glance, then annotate that AST. Returns each expression's
/// inferred type keyed by its glance span, to join onto your own AST nodes.
pub fn types_by_span(source: String) -> Dict(#(Int, Int), Type) {
let assert Ok(module) = glance.module(source)
let assert Ok(annotated) =
girard.annotate_module(module, girard.default_options())
list.fold(annotated.expressions, dict.new(), fn(acc, a) {
dict.insert(acc, #(a.span.start, a.span.end), a.type_)
})
}
/// A definition's generalized signature is a structured `Scheme` (`.type_` is
/// the type, `.vars` are its quantified type-variable ids) you can pattern-match.
pub fn return_kind(source: String, name: String) -> String {
let assert Ok(module) = glance.module(source)
let assert Ok(annotated) =
girard.annotate_module(module, girard.default_options())
case list.key_find(annotated.functions, name) {
Ok(scheme) ->
case scheme.type_ {
Fn(_args, Named("gleam", "Int", [])) -> "returns Int"
Fn(_args, Named("gleam", "List", [_])) -> "returns a List"
Fn(_args, other) -> girard.type_to_string(other)
other -> girard.type_to_string(other)
}
Error(_) -> "no such function"
}
}
```
(Imported modules are still parsed internally, via the resolver — only the
module you pass is taken pre-parsed.)
### Options: resolver and target
`annotate`, `annotate_module`, and `annotate_package` all take an `Options`
value. Build it from `girard.default_options()` (disk resolver, `Erlang` target)
and customize it with the `with_*` setters:
```gleam
girard.default_options()
|> girard.with_target(girard.JavaScript) // type for the JS target
|> girard.with_resolver(fn(_) { Error(Nil) }) // resolve no imports
```
The resolver is `fn(module_path) -> Result(source, Nil)`; inject your own to
resolve imports from anywhere (an in-memory map, a build tree, …).
### Reusing imported interfaces
An editor or package-walking tool can carry a `Cache` between annotations so
shared imports are parsed and inferred once:
```gleam
let options = girard.default_options()
let cache = girard.new_cache()
let #(first_result, cache) =
girard.annotate_with_cache(first_source, options, cache)
let #(second_result, cache) =
girard.annotate_with_cache(second_source, options, cache)
```
A cache assumes the same resolver and target for its whole lifetime. When an
imported module changes, invalidate its module path before the next call:
```gleam
let cache = girard.invalidate(cache, "my_app/shared")
```
`invalidate` removes only that module. If its public interface changed, also
invalidate cached importers, or start again from `new_cache()`.
### Annotating a whole package
`girard.annotate_package(modules, options)` annotates many modules in one pass,
inferring a shared import only once across the whole run. `modules` is a list of
`#(module_path, glance.Module)`; the result maps each path to a `ModuleResult`
(`.annotated` plus `.skipped`).
Unlike `annotate`/`annotate_module`, it is **best-effort per definition**: a
top-level function or constant that does not type — along with anything that
depends on it — is listed in that module's `.skipped` (with the error that
declined it) rather than failing the module, and every other definition is still
annotated. A strict check is just `result.skipped == []`.
The resolver must be able to load package-local imports as well as external
dependencies. Supplying a module in `modules` gives girard its AST to annotate;
it does not implicitly add that source to the resolver. An in-memory package can
provide both views from one source table:
```gleam
import girard
import glance
import gleam/dict
import gleam/list
let sources =
dict.from_list([
#("my_app/a", "pub fn answer() { 42 }"),
#(
"my_app/b",
"import my_app/a\npub fn answer() { a.answer() }",
),
])
let resolver = fn(path) { dict.get(sources, path) }
let modules =
sources
|> dict.to_list
|> list.map(fn(entry) {
let #(path, source) = entry
let assert Ok(module) = glance.module(source)
#(path, module)
})
let options =
girard.default_options()
|> girard.with_resolver(resolver)
let results = girard.annotate_package(modules, options)
```
## Limitations
- **Parsing is bounded by `glance`.** girard does not parse Gleam itself, so
source that [`glance`](https://hexdocs.pm/glance/) cannot parse, girard cannot
annotate. Since imports are resolved by parsing, an unparseable module also
makes its dependents fail with `unbound variable`. The gaps the sweep surfaces
are all in bit-array syntax — chiefly arithmetic in a bit-array *pattern*
segment size, e.g. `<<value:size(len - 1)>>` (the construction side parses, the
pattern side does not). These are `glance` limitations, not girard inference
errors.
- **Inferred types, not diagnostics.** girard reproduces the types the compiler
infers, but it is not a full type checker: when a module cannot be typed it
returns a single `Error` for the first problem found, not the compiler's full
set of diagnostics.
- **Scoped to compilable code.** Inference is validated against programs the real
compiler accepts; packages that do not compile with current tooling are out of
scope, since the compiler cannot type them either.
## Contributing
See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the development workflow,
differential testing, and code and commit conventions. See
[`AGENTS.md`](AGENTS.md) for the architecture, inference pipeline, state model,
and design decisions.
API documentation is available at <https://hexdocs.pm/girard>.