girard

Package Version Hex Docs CI

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.

The project is stable: its inferred types are validated differentially against the real compiler across the hex ecosystem (see PACKAGES.md).

Usage

Add the package to your Gleam project:

gleam add girard

Then annotate some source:

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:

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/types values — pattern-match on Named/Fn/Var/Tuple, or render one with girard.type_to_string.

Command line

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, 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.

import girard
import girard/types.{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:

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, …).

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 == [].

Limitations

Contributing

See CONTRIBUTING.md for the development workflow, differential testing against the real compiler, and an overview of the architecture.

API documentation is available at https://hexdocs.pm/girard.

Search Document