graded

Effect checker for Gleam via sidecar .graded annotation files.

graded verifies that your Gleam functions respect their declared effect budgets. Annotations live in .graded sidecar files alongside your source — your Gleam code stays clean.

Usage

gleam run -m graded check [directory]         # enforce check annotations (default)
gleam run -m graded infer [directory]         # infer and write effect annotations
gleam run -m graded infer --dry-run [dir]     # preview the spec changes, writing nothing
gleam run -m graded effect <name> [directory] # look up one effect, writing nothing
gleam run -m graded effect <name> --format=graded  # ... as a .graded line
gleam run -m graded why <name> [directory]    # explain a function's effects
gleam run -m graded catalog                   # list the bundled catalog files
gleam run -m graded catalog <package>         # print the catalog file selected for it
gleam run -m graded catalog <package>@<ver>   # print exactly that bundled file
gleam run -m graded format [directory]        # normalize .graded file formatting

Programmatic API

Use run to check a directory and get back one ModuleReport per file, carrying the warning and violation lines graded prints for it. Use run_infer to infer effects and write .graded files, or run_infer_dry_run to get back a diff of what that write would change without performing it. Use run_effect to resolve one function or type-field name and get its .graded line back, run_why to get the effects of one function explained call by call, or catalog_list / catalog_show to read graded’s own bundled catalog — none of them touching anything on disk.

Every type this module’s signatures name is defined here. Nothing under graded/internal is part of the API, and a release may change it freely.

Types

Why graded catalog could not answer.

pub type CatalogProblem {
  NoCatalogEntry(package: String)
  NoBundledVersion(
    package: String,
    requested: String,
    bundled: List(String),
  )
  NotInstalled(package: String, bundled: List(String))
  NoManifest(path: String)
  EmptyCatalog(directory: String)
  NoCatalogDirectory(candidates: List(String))
}

Constructors

  • NoCatalogEntry(package: String)

    No bundled file names this package.

  • NoBundledVersion(
      package: String,
      requested: String,
      bundled: List(String),
    )

    The package is bundled, but not at the version asked for.

  • NotInstalled(package: String, bundled: List(String))

    The package is bundled but not in this project’s manifest.

  • NoManifest(path: String)

    There is no manifest to read at the path the command consulted, or its TOML is malformed, so no package has an installed version to select on.

  • EmptyCatalog(directory: String)

    The catalog directory exists but holds no catalog file.

  • NoCatalogDirectory(candidates: List(String))

    None of the paths graded looks for its bundled catalog under exists.

How run_effect_formatted renders an answer.

pub type Format {
  Graded
  Prose
}

Constructors

  • Graded

    A .graded line, with any provenance as a // comment — the whole output parses as spec syntax.

  • Prose

    Sentences describing the same answer, as graded effect prints them.

Errors that can occur during checking, inference, or formatting.

Every variant is renderable on its own: where the cause is a graded type, the variant carries what graded would print for it rather than the type itself, so naming an error never means importing graded/internal.

pub type GradedError {
  DirectoryReadError(path: String, cause: simplifile.FileError)
  FileReadError(path: String, cause: simplifile.FileError)
  FileWriteError(path: String, cause: simplifile.FileError)
  DirectoryCreateError(path: String, cause: simplifile.FileError)
  GleamParseError(path: String, cause: glance.Error)
  GradedParseError(path: String, message: String)
  InvalidConfig(path: String, message: String)
  FormatCheckFailed(paths: List(String))
  CyclicImports(modules: List(String))
  EffectNotFound(name: String)
  FunctionNotFound(name: String)
  PackError(message: String)
  CatalogError(problem: CatalogProblem)
}

Constructors

  • DirectoryReadError(path: String, cause: simplifile.FileError)

    Could not read the source directory.

  • FileReadError(path: String, cause: simplifile.FileError)

    Could not read a source or annotation file.

  • FileWriteError(path: String, cause: simplifile.FileError)

    Could not write an annotation file.

  • DirectoryCreateError(path: String, cause: simplifile.FileError)

    Could not create the output directory for annotation files.

  • GleamParseError(path: String, cause: glance.Error)

    A .gleam source file could not be parsed.

  • GradedParseError(path: String, message: String)

    A .graded annotation file could not be parsed. message is the description graded itself prints, line number and hint included.

  • InvalidConfig(path: String, message: String)

    gleam.toml was present but malformed, unreadable, or missing its name. A missing gleam.toml is tolerated and does not produce this error. message describes what was wrong with it.

  • FormatCheckFailed(paths: List(String))

    One or more .graded files are not formatted (returned by run_format_check).

  • CyclicImports(modules: List(String))

    The project’s import graph contains a cycle. Gleam disallows circular imports at the language level, so this should be unreachable in practice — if it ever fires it indicates a bug in the dependency edge extraction rather than user code.

  • EffectNotFound(name: String)

    graded effect found no effect for the queried name: it names no public function and no declared type field.

  • FunctionNotFound(name: String)

    graded why found no function to explain: the name isn’t module-qualified, names no module of this project, or names no function of that module.

  • PackError(message: String)

    graded pack could not inject the spec into the hex tarball: the tarball was missing, its identity didn’t match the project, the configured spec_file path was unsafe, or the tarball transform failed.

  • CatalogError(problem: CatalogProblem)

    graded catalog could not answer: see CatalogProblem.

What checking one file found, as the lines graded prints for it.

Both halves, because both are what a caller acts on: the CLI prints the warnings with their count and the violations with theirs, and exits on the violations alone. A count is list.length of the list.

pub type ModuleReport {
  ModuleReport(
    file: String,
    warnings: List(String),
    violations: List(String),
  )
}

Constructors

  • ModuleReport(
      file: String,
      warnings: List(String),
      violations: List(String),
    )

    Arguments

    file

    The source file these results belong to — the spec file, for the warnings a spec line earns on its own.

    warnings

    One rendered line per warning, in the order graded reports them.

    violations

    One rendered line per violation, likewise.

Values

pub fn catalog_list() -> Result(String, GradedError)

List the bundled catalog: one package@version line per bundled file, sorted, with a comment on the line each installed package of the project the process runs in resolves to.

This is graded’s bundled catalog alone: a dependency’s shipped spec, a path dependency’s spec and your own assume all override it, so run_effect is what answers which source wins for a name. Nothing is written to disk.

pub fn catalog_show(
  package: String,
  version: option.Option(String),
  directory: String,
) -> Result(String, GradedError)

Print one bundled catalog file for package, under a // header line saying which file it is and why — so the output is itself a valid .graded file. version names a bundled version exactly; None selects the one directory’s project installs. Nothing is written to disk.

pub fn main() -> Nil
pub fn pack_project(
  project_root: String,
  tarball: option.Option(String),
) -> Result(String, GradedError)

Inject the configured .graded spec into project_root’s hex tarball. tarball overrides the default build/<name>-<version>.tar. Returns a success message (with the publish command), a GradedParseError when the spec does not parse — nothing a consumer would read is ever packed — or a PackError.

pub fn run(
  directory: String,
) -> Result(List(ModuleReport), GradedError)

Run the checker on all .gleam files in a directory.

Reads the project’s single spec file (default <package_name>.graded) to find inferred public-API effects, check invariants, assume declarations, and field annotations, then reports one ModuleReport per source file, plus one for the spec file itself where a spec line is dead.

pub fn run_effect(
  directory: String,
  name: String,
) -> Result(String, GradedError)

Look up one name’s effect in directory’s project and render it as a .graded line.

name is either a module-qualified function (myapp/router.handle) or a type field (myapp/repo.Repo.find). Functions resolve from the spec file, dependencies, the catalog, and an in-memory inference pass, so a public function resolves without a prior graded infer; type fields resolve from declared field assume lines. Any provenance is appended as a // comment line, so the whole output parses as .graded syntax. Nothing is written to disk.

The CLI defaults to --format=prose for the person reading a terminal; this function keeps returning the parseable form, which is what a caller linking against the module wants. run_effect_formatted takes the format.

Returns EffectNotFound when the name is neither a public function nor a declared type field.

pub fn run_effect_formatted(
  directory: String,
  name: String,
  format: Format,
) -> Result(String, GradedError)

Look up one name’s effect and render it in format. Both formats render one structured answer, so they can differ in wording but never in what they report.

pub fn run_format(directory: String) -> Result(Nil, GradedError)

Format the project’s spec file in place. The spec file is the single source of truth for hand-written check/assume lines and the inferred public-API effects.

pub fn run_format_check(
  directory: String,
) -> Result(Nil, GradedError)

Check that the project’s spec file is already formatted. Returns error with the file path if it isn’t. Used by CI as format --check.

pub fn run_format_stdin(
  input: String,
) -> Result(String, GradedError)

Format a .graded spec given as a string, as graded format --stdin does for editor integration: parse the input, then sort and reformat it. Returns a GradedParseError naming <stdin> if the input doesn’t parse.

pub fn run_infer(directory: String) -> Result(Nil, GradedError)

Infer effects for all .gleam files in directory. Writes two outputs:

  1. Per-module cache files under <cache_dir>/<module_path>.graded, containing the inferred effects of every function in the module (public + private). Regenerated freely; not shipped.

  2. One spec file at <spec_file> containing the inferred effects of every public function across all modules, plus any hand-written check, assume, or type annotations the user already had in the spec file (those lines are preserved verbatim).

Walks the project’s import graph in topological order so each module is analysed after every other project module it imports — a single pass resolves transitive chains of any depth.

pub fn run_infer_dry_run(
  directory: String,
) -> Result(String, GradedError)

Preview what run_infer would change: a line diff of the spec file, or a message saying there is nothing to change. Runs the same inference run_infer does and writes nothing — neither the spec file nor the cache.

The diff’s old side is the spec file exactly as it sits on disk, so the re-rendering run_infer applies to every line it writes shows up in the preview too.

pub fn run_why(
  directory: String,
  name: String,
) -> Result(String, GradedError)

Explain where one function’s effects come from, as prose.

name is a module-qualified function in one of this project’s own modules (myapp/router.handle) — why re-walks a body, so there has to be one. Private functions are accepted: the walk is over source this project holds, unlike run_effect, which answers from the public knowledge base.

The output holds one block per check line declared for the function, each with that line’s own bounds fed to the analysis, in spec-file order — two check lines can substitute the same body differently, so neither block speaks for the other. With no check line there is one block, analysed with no bounds. A block states the function’s total effect, the check line it came from (informationally — the subset verdict is graded check’s), and one line per contributing call: what the call is, the effects it contributes, and either why they stayed unresolved or which source resolved them.

Contributors are the calls the checker reaches, not the call sites written in the body: a resolved call to a same-module function is replaced by that function’s own calls, so those surface instead, at spans inside it.

Returns FunctionNotFound when the name isn’t a function of a project module. Nothing is written to disk.

Search Document