graded reference

This document is the reference for the .graded spec language. graded resolves each function’s effects — a set of string labels — and checks them against the budgets you declare: an effect set passes when it is a subset of its budget. Below: every annotation kind, the effect-set syntax, the resolution order, the effect-label conventions, and the bundled catalog. For installation, project layout, configuration, and the CLI, see the README; for how the analysis works under the hood, see How analysis works at the end.

The spec file and the cache

graded keeps two kinds of .graded file:

Annotation kinds

Five kinds of line appear in a spec file.

effects — inferred effects

effects myapp.view : []
effects myapp/router.handle_request : [Http, Stdout]

Written by graded infer for every public function. Regenerated on each run — do not edit by hand. (The cache holds the same lines for private functions too.)

check — enforced invariant

check myapp.view : []
check myapp/router.handle_request : [Http, Stdout]

An invariant enforced by graded check. If the function’s actual effects aren’t a subset of the declared budget, the build breaks. This is the line you write to pin a function’s effects down.

The name is module-qualified (myapp/router.handle_request). A check whose name matches no function in any project module — most often a missing module qualifier — never runs against anything and passes silently; graded check warns about it.

type — function-typed field effects

type myapp.Handler.on_click : [Dom]
type myapp/router.Request.send : [Http]

Declares the effect of a function-typed field on a custom type. See Type field effects.

The type is module-qualified by the module that defines it. An unqualified or mis-qualified type line keys nothing, so the field silently resolves to [Unknown]; graded check warns when a type line matches no field of any project type.

external effects — third-party and FFI functions

external effects gleam/httpc.send : [Http]
external effects simplifile.read : [FileSystem]

Declares effects for functions graded can’t analyse — dependencies and FFI. See External declarations.

returns — returned operators and latent effects

// a producer that returns one of its operator parameters (a decorator)
returns myapp.traced : fn(cb) -> [cb]

// a producer that returns a closure with a latent effect
returns myapp.make_logger : [Stdout]

Serialized by graded infer for functions that return a function. It lets the returned function’s effect resolve at the call site (let h = make_logger(); h()) across module and package boundaries, not just within the defining module. Like effects, these lines are regenerated and shouldn’t be hand-edited.

Effect resolution order

When graded needs a function’s effects, it consults these sources in priority order and takes the first hit:

  1. Your spec filecheck, external effects, type, and returns declarations in <package_name>.graded.
  2. Cross-module project effects — effects inferred from sibling modules in the same project, propagated in topological order. A fresh checkout resolves transitive call chains with no prior graded infer; committed effects lines always win, and check writes nothing to disk.
  3. Dependency spec files — shipped by libraries at build/packages/<dep>/<dep_spec_file> (each dep’s spec path comes from its own [tools.graded] config). A dependency’s own spec outranks the bundled catalog.
  4. Path dependencies — local deps declared with path = "..." in gleam.toml. graded reads their spec files; if a path dep ships none, it falls back to inferring from that dep’s source.
  5. Bundled catalog — the versioned catalog files shipped with graded (see Effect catalog).
  6. Conservative default — anything still unresolved gets [Unknown].

Effect set syntax

An effect set appears inside brackets. The shapes:

Higher-order signatures add two more shapes (see Higher-order functions):

Wildcard caveat. Because [_] is lattice top, it absorbs everything in a union. A function whose inferred effects would be [Stdout, e] (polymorphic) but whose declared type is [_] loses the variable — correct, but surprising. If you want polymorphism, don’t declare a wildcard bound.

Higher-order functions

Parameter effect bounds

A function that accepts a callback can bound that parameter’s effects:

// f must be pure — safe_map inherits no effects from its callback
check myapp.safe_map(f: []) : []

// apply passes f's effects straight through
effects myapp.apply(f: [Stdout]) : [Stdout]

A call to a bounded parameter (f(x) inside apply) uses the declared bound instead of [Unknown].

Field bounds

A bound’s name can be a param.field path, declaring the effect of a function-typed field reached through a parameter:

// handler.on_click carries [Dom] inside view
check myapp.view(handler.on_click: [Dom]) : [Dom]

A field call handler.on_click(event) then resolves to [Dom] directly, taking priority over receiver-type resolution. This is the boundary-scoped counterpart to a type line: the type line declares a field’s effect for every receiver of that type package-wide, the field bound for one check’d function. A field bound and an ordinary parameter bound can share one check line.

A field bound declares a concrete effect set: it resolves to exactly the effects written, with no call-site substitution. For an effect-polymorphic field — one whose effect depends on its own arguments — use a type line instead, which substitutes the field call’s arguments into the declared variables.

If a field bound’s param.field path matches no field call in the checked function’s body, graded emits a warning — the bound is dead. When the receiver is a parameter the cause is a typo in the path; when it isn’t, the warning also notes the field call may have resolved through value provenance (a receiver traced to a construction site), which shadows the bound.

Precedence. A field bound only competes with receiver-type (type-line) resolution, and wins it. It does not override value provenance: when the receiver is traced to a construction site — a direct constructor or a factory — and the field resolves through that value, the call is resolved before it is ever treated as a field call, so the bound doesn’t apply. This isn’t a conflict in practice: field bounds exist for receivers graded can’t trace (a parameter, a value threaded through data), which is exactly the case where there’s no provenance to compete with.

Effect polymorphism

When a function’s effects depend on its callback, use lowercase effect variables:

// validate_range's effects are whatever to_error's effects are
effects myapp.validate_range(to_error: [e]) : [e]

// map_with_log carries [Stdout] on top of f's effects
effects myapp.map_with_log(f: [e]) : [Stdout, e]

graded infer writes these automatically when it sees a function calling a parameter that has a fn(...) -> ... type (whether annotated in source or inferred by girard) — the variable is named after the parameter. At each call site, graded binds the variable to the argument’s effects:

An inline closure argument (validate_range(42, fn(m) { io.println(m) })) is analysed directly — its body’s effects are counted in the caller — so it resolves without needing the variable. Both labeled (to_error: OutOfRange) and positional (OutOfRange) arguments resolve. A function value graded can’t trace — pulled from a data structure, say — stays [Unknown]; see LIMITATIONS.md.

Second-order (operator) effects

When a parameter’s own type takes a function (action: fn(fn() -> Nil) -> a), its effect variable is higher-kinded — an operator Eff → Eff rather than a flat Eff. A call action(cb) infers an operator application, and at the call site the operator argument is lifted and the application beta-reduces to the concrete effect. graded models this with a small lambda-calculus-with-union (EffectTerm); the operator-bound and application syntax above is its surface form. Operator arguments resolve from named references, inline and let-bound closures, case/if branches over function-like options, blocks, and functions returned from a call. The full design and the property suite are in docs/SECOND_ORDER_EFFECTS.md.

Type field effects

Custom types can have function-typed fields (a Handler with an on_click, a Validator with a to_error). graded resolves a field call v.on_click(event) in two steps: it asks girard for v’s nominal type — which works for any receiver, a parameter, a returned value, or an alias chain, falling back to a syntactic parameter annotation when girard can’t type the function — and then looks up that type’s field effect.

The field’s effect comes from one of:

Field effects are keyed by the type’s defining module (from girard’s inferred type), so two different types both named Validator never conflate. When a field is wired to a value graded can’t trace — a constructor parameter, or a local that isn’t a traceable function — it falls back to [Unknown]. The escape hatch is a type line, or a field bound when the assertion belongs at a single function boundary; see LIMITATIONS.md.

Dependency-defined types. The receiver type a field call resolves to can belong to a dependency, so a type line may name a dependency module (type dep/repo.Repo.find : [Storage]). This works for both path and published dependencies — girard reads the dependency’s source to type the receiver. A dependency can also ship its own type lines in its committed spec file; a consumer picks them up automatically, the same way it inherits a dependency’s effects and external annotations, so the capability-record pattern needs no per-consumer re-declaration. A consumer’s own type line still wins on a clash.

External declarations and FFI

external effects annotates a function graded can’t see into, without touching the library:

external effects gleam/httpc.send : [Http]
external effects simplifile.read : [FileSystem]
external effects gleam/otp/actor.start : [Process]

These are merged into the knowledge base before both infer and check, so callers resolve them instead of getting [Unknown].

A name with no . is a module-level external: it declares the whole module’s effect at once, so every function in it resolves to that set without a per-function line.

external effects gleam/list : []           // the whole module is pure
external effects some_db/client : [Database] // every client function does Database I/O

Module-level externals work on dependency modules (hex or path) and on your own project modules. For a dependency or project module graded would otherwise infer, the declaration suppresses that inference: every function in the module resolves to the declared set instead of an inferred [Unknown], and graded infer writes no per-function effects lines for it (just as a per-function external suppresses its own line). Use the per-function form when functions in a module differ; use the module-level form when one budget fits the module. A per-function external effects mod.fn or a catalog effects line for the same function takes precedence over a module-level external.

This is also the mechanism for FFI. A bodyless @external function is opaque — graded infers [Unknown], never the [] an empty body would suggest, since the foreign implementation may do anything (this holds even when the @external carries a pure-looking Gleam fallback body). Declare its real effect with an external effects line to make callers propagate correctly.

Effect labels

Effect labels are plain strings — you can use any name. The bundled catalog uses these conventions:

LabelMeaningExample functions
StdoutWrites to standard outputgleam/io.println, logging.log
StderrWrites to standard errorgleam/io.print_error
StdinReads from standard inputgleam/erlang.get_line
ProcessSpawns, sends to, or manages BEAM processesgleam/erlang/process.send, gleam/otp/actor.start
HttpNetwork HTTP requestsgleam/httpc.send, gleam/fetch.send, lustre_http.get
NetworkLower-level socket / server I/Oglisten.start, mist.start
DatabaseDatabase queriespog.query, pog.execute
FileSystemReads or writes the filesystemsimplifile.read, wisp.serve_static
EnvironmentReads env vars or command-line argumentsenvoy.get, argv.load, directories.home_dir
ExecRuns an external programshellout.command, shellout.which
DomBrowser DOM manipulationlustre.start, lustre.register
TimeReads system clock or timezonegleam/time/timestamp.system_time, birl.now
RandomNondeterministic generationyouid/uuid.v4, wisp.random_string

Define your own labels for project-specific effects — they need no registration:

external effects my_app/email.send : [Email]
external effects my_app/metrics.record : [Telemetry]
check my_app/api.handle_request : [Http, Email]

graded infer regenerates the inferred effects and returns lines while preserving your check, type, external, comments, and blank lines. graded format normalizes spacing and sorting.

Effect catalog

graded ships versioned catalog files for common Gleam packages, so you get effect knowledge out of the box without writing external effects for standard libraries.

Catalog files live in priv/catalog/ and are named {package}@{version}.graded. At load time graded reads your project’s manifest.toml to determine installed dependency versions, then selects the highest catalog version that doesn’t exceed the installed one. So gleam_stdlib@0.71.0 installed against a gleam_stdlib@0.70.0.graded catalog file uses that file — effects don’t change between patch versions. A new catalog file is only needed when a library adds modules or changes effect semantics. A dependency that ships its own .graded spec overrides the catalog (resolution order step 3 above).

Browse priv/catalog/ for the exact set of covered packages and the effects each one declares — the files are plain .graded and readable at a glance. It covers the core gleam-lang packages and the most-used community libraries. For a package the catalog doesn’t cover, add an external effects declaration in your spec file.

Declaring uncatalogued dependencies

The bundled catalog is a curated convenience for common packages, not a general-purpose registry that grows on request. To teach graded about a dependency it doesn’t catalog — hex or path — declare its effects yourself with external effects in your spec file:

external effects some_dep/io : [FileSystem]   // module-level: whole-module budget
external effects some_dep/net.fetch : [Http]   // per-function: precision

Use the module-level form when one budget fits the whole module, the per-function form when functions differ. Both forms apply uniformly to hex and path dependencies — a module-level external suppresses path-dep source inference for that module, so it resolves to the declared set rather than an inferred [Unknown]. This keeps your effect knowledge in your own spec file, versioned with your project.

How analysis works

graded parses your Gleam source with glance, resolves imports, follows local calls transitively, and unions the effect sets it finds. Composition is set union; checking is subset inclusion — if a function’s actual effects aren’t a subset of its declared budget, that’s a violation, reported with the call site.

On top of the syntax layer, graded runs girard — a Hindley-Milner type annotator for Gleam — over the whole package to learn the inferred type of every expression. Types are an enhancement layer applied per function: a function girard can’t type falls back to the syntax-level path, so types only ever sharpen a result (resolving a field call’s receiver, for example), never change an already-resolved one. The analysis is sound, not complete: when it can’t statically trace a value it falls back to the [Unknown] effect rather than guess, so effects are never silently understated. The patterns that fall back are catalogued in LIMITATIONS.md.

Search Document