Skip to content

alvivi/girard

Repository files navigation

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

  • Parsing is bounded by glance. girard does not parse Gleam itself, so source that 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 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.

About

A Gleam type annotator that mirrors the compiler's type inference

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors