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 format [directory] # normalize .graded file formatting
Programmatic API
Use run to check a directory and get back a list of CheckResult values,
each containing any violations found per file. 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, or run_why to get the effects of one function
explained call by call — both touching nothing on disk.
Types
Errors that can occur during checking, inference, or formatting.
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, cause: @internal ParseError)
InvalidConfig(path: String, cause: @internal ConfigError)
FormatCheckFailed(paths: List(String))
CyclicImports(modules: List(String))
EffectNotFound(name: String)
FunctionNotFound(name: String)
PackError(message: String)
}
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, cause: @internal ParseError)A
.gradedannotation file could not be parsed. -
InvalidConfig(path: String, cause: @internal ConfigError)gleam.tomlwas present but malformed, or missing itsname. A missinggleam.tomlis tolerated and does not produce this error. -
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.
Values
pub fn infer_path_dep(
dep_path: String,
base_kb: @internal KnowledgeBase,
consumer_modules: set.Set(String),
package_targets: @internal PackageTargets,
) -> Result(
#(
dict.Dict(@internal QualifiedName, @internal EffectTerm),
dict.Dict(@internal QualifiedName, List(@internal ParamBound)),
dict.Dict(@internal QualifiedName, @internal EffectTerm),
dict.Dict(@internal QualifiedName, @internal ReturnProvenance),
),
Nil,
)
Build the dependency-graph index for a single path dep, topo-sort it,
then infer every module in dependency order. Returns the union of all
inferred effects, polymorphic param bounds, returned-operator signatures,
and return-value provenance keyed by QualifiedName so the caller can fold
them into the global knowledge base — the provenance lets a consumer resolve
a computed-receiver call into the dep (dep.inner(dep.factory(x))) that a
committed dep spec, which does not serialize provenance, cannot. Errors are
swallowed (returned as Error(Nil)) to
preserve the existing tolerance: a malformed dep shouldn’t break the whole
project.
Exposed (pub) primarily so tests can exercise the topological-order path
inference on a temporary directory tree without going through
gleam.toml resolution. Production callers go through
enrich_with_path_deps which reads gleam.toml to discover dep paths.
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) or a PackError.
pub fn run(
directory: String,
) -> Result(List(@internal CheckResult), 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, external
hints, and type field annotations, then reports violations per source
file.
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 type 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: @internal Format,
) -> Result(String, GradedError)
Look up one name’s effect and render it in format: answer.Graded for
the .graded line above, answer.Prose for sentences describing the same
answer. Both render one structured answer, so they can differ in wording but
never in what they report.
pub fn run_effect_from_project(
directory: String,
name: String,
) -> Result(String, GradedError)
Look up name the long way: assemble the whole project context — every
module parsed, dependency sources scanned, girard run package-wide — and
answer from its knowledge base, skipping the spec-only fast path
run_effect tries first.
Exposed (pub) primarily so a test can assert the two paths agree. A fast-path answer is only correct if it is what the full context would have said, byte for byte; nothing else about the two is allowed to differ.
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/external/type 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, @internal ParseError)
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
the input’s parse error if it 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,external effects, 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.