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
-
GradedA
.gradedline, with any provenance as a//comment — the whole output parses as spec syntax. -
ProseSentences describing the same answer, as
graded effectprints 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
.gleamsource file could not be parsed. -
GradedParseError(path: String, message: String)A
.gradedannotation file could not be parsed.messageis the description graded itself prints, line number and hint included. -
InvalidConfig(path: String, message: String)gleam.tomlwas present but malformed, unreadable, or missing itsname. A missinggleam.tomlis tolerated and does not produce this error.messagedescribes what was wrong with it. -
FormatCheckFailed(paths: List(String))One or more
.gradedfiles are not formatted (returned byrun_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 effectfound no effect for the queried name: it names no public function and no declared type field. -
FunctionNotFound(name: String)graded whyfound 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 packcould not inject the spec into the hex tarball: the tarball was missing, its identity didn’t match the project, the configuredspec_filepath was unsafe, or the tarball transform failed. -
CatalogError(problem: CatalogProblem)graded catalogcould not answer: seeCatalogProblem.
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 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:
-
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. -
One spec file at
<spec_file>containing the inferred effects of every public function across all modules, plus any hand-writtencheck,assume, ortypeannotations 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.