# graded
[](https://hex.pm/packages/graded)
[](https://hexdocs.pm/graded/)
[](https://github.com/alvivi/graded/actions/workflows/ci.yml)
> Effect checking for Gleam via sidecar annotations.
**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.
## Quick start
```sh
gleam add --dev graded
```
Infer effects for your project:
```sh
gleam run -m graded infer
```
This scans `src/`, analyzes every public function, and writes effect annotations to `priv/graded/`.
### Lustre example
In a [Lustre](https://hexdocs.pm/lustre/) app, `view` must be pure — it builds HTML from the model without side effects. Enforce this with graded:
```gleam
// src/app.gleam
import gleam/io
import lustre/element.{type Element}
import lustre/element/html
pub fn view(model: Model) -> Element(Msg) {
io.println("rendering") // oops — side effect in view!
html.div([], [html.text(model.name)])
}
```
```
// priv/graded/app.graded
check view : []
```
```sh
$ gleam run -m graded check
src/app.gleam: view calls gleam/io.println with effects [Stdout] but declared []
graded: 1 violation(s) found
```
Remove the `io.println` and the check passes. Lustre's `init` and `update` functions are also pure — they return `#(Model, Effect(Msg))` where `Effect` is a data description, not an executed side effect.
### General example
Constrain any function's effect budget:
```
// priv/graded/api.graded
check handle_request : [Http, Stdout]
```
If `handle_request` does something outside its budget (like writing to a database), graded reports the violation with the call site.
## How it works
Four annotation types in `.graded` files:
- **`effects fn : [...]`** — inferred cache, auto-generated by `graded infer`, not enforced
- **`check fn : [...]`** — invariant, enforced by `graded check`, violations break the build
- **`type Type.field : [...]`** — declares effects for function-typed fields on custom types
- **`external effects module.fn : [...]`** — declares effects for external/third-party functions
The checker walks the Gleam AST (via [glance](https://hexdocs.pm/glance/)), resolves imports, follows local calls transitively, and unions the effect sets. If the actual effects aren't a subset of the declared budget, it's a violation.
Effect knowledge is resolved in priority order:
1. **Your `.graded` files** — `external effects` declarations you write for your project
2. **Dependency `.graded` files** — shipped by libraries in `build/packages/*/priv/graded/`
3. **Path dependencies** — local deps declared with `path = "..."` in `gleam.toml` are inferred from source automatically
4. **Bundled catalog** — versioned catalog files shipped with graded (see below)
5. **Conservative default** — unknown functions get `[Unknown]`
## Higher-order functions
Functions that accept callbacks can declare parameter effect bounds:
```
// f must be pure — safe_map inherits no effects from its callback
check safe_map(f: []) : []
// apply passes through f's effects
effects apply(f: [Stdout]) : [Stdout]
```
When checking, calls to bounded parameters (like `f(x)` inside `apply`) use the declared bound instead of `[Unknown]`.
## Type field effects
For types with function-typed fields, declare their effects at the type level:
```
type Handler.on_click : [Dom]
type Request.send : [Http]
```
When checking `handler.on_click(event)`, graded looks up the parameter's type annotation to resolve the field's effects. Parameters must be explicitly typed in the Gleam source for this to work.
## External declarations
Annotate third-party library functions without modifying the library:
```
external effects gleam/httpc.send : [Http]
external effects simplifile.read : [FileSystem]
external effects gleam/otp/actor.start : [Process]
```
Externals are merged into the knowledge base before both `infer` and `check`, so calls to these functions resolve with the declared effects instead of `[Unknown]`.
This is also the right mechanism for **FFI functions**. If your project has Erlang or JavaScript FFI implementations, the checker can't analyze the foreign code and will flag those calls as `[Unknown]`. Declare their effects with `external effects` in your `.graded` file:
```
// priv/graded/my_app.graded
external effects my_app/native.hash_password : [Crypto]
external effects my_app/native.read_env : [System]
external effects my_app/native.pure_helper : []
```
Since externals are loaded before inference, `graded infer` will propagate the declared effects through callers correctly — no `[Unknown]` noise.
## .graded file format
```
// Comments use Gleam-style //
type Handler.on_click : [Dom]
check render_page : []
check handle_request : [Http, Stdout]
check safe_map(f: []) : []
effects render_page : []
effects handle_request : [Http, Stdout]
effects apply(f: [Stdout]) : [Stdout]
```
- `[]` means pure — no effects allowed
- `[Http, Dom]` — these specific effects are permitted
## Effect labels
Effect labels are plain strings — you can use any name. The bundled catalog uses these conventions:
| Label | Meaning | Example functions |
|---|---|---|
| `Stdout` | Writes to standard output | `gleam/io.println`, `gleam/io.debug` |
| `Stderr` | Writes to standard error | `gleam/io.print_error` |
| `Stdin` | Reads from standard input | `gleam/erlang.get_line` |
| `Process` | Spawns, sends to, or manages BEAM processes | `gleam/erlang/process.send`, `gleam/otp/actor.start` |
| `Http` | Network HTTP requests | `gleam/httpc.send`, `lustre_http.get` |
| `FileSystem` | Reads or writes the filesystem | `simplifile.read`, `simplifile.write` |
| `Dom` | Browser DOM manipulation | `lustre.start`, `lustre.register` |
| `Time` | Reads system clock or timezone | `gleam/time/timestamp.system_time`, `gleam/time/calendar.local_offset` |
You can define your own labels for project-specific effects:
```
external effects my_app/email.send : [Email]
external effects my_app/metrics.record : [Telemetry]
check handle_request : [Http, Email]
```
- `graded infer` regenerates `effects` lines while preserving `check` lines, `type` lines, comments, and blank lines
- `graded format` normalizes spacing and sorting
## Effect catalog
graded ships with versioned catalog files for common Gleam packages, so you get effect knowledge out of the box without writing `external effects` declarations 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 version.
For example, if you have `gleam_stdlib@0.71.0` installed and the catalog has `gleam_stdlib@0.70.0.graded`, that file is used — effects don't change between patch versions. A new catalog file is only needed when a library adds modules or changes effect semantics.
### Covered packages
| Package | Effects | Labels |
|---|---|---|
| **gleam_stdlib** | `gleam/io.*` | `Stdout`, `Stderr` |
| **gleam_erlang** | `gleam/erlang/process.*` | `Process`, `Stdin`, `FileSystem` |
| **gleam_otp** | `gleam/otp/actor.*`, `gleam/otp/supervisor.*` | `Process` |
| **gleam_http** | (pure) | — |
| **gleam_httpc** | `gleam/httpc.send` | `Http` |
| **gleam_json** | (pure) | — |
| **lustre** | `lustre.start`, `lustre.send`, `lustre/server_component.*` | `Process`, `Dom` |
| **lustre_http** | `lustre_http.*` | `Http` |
| **simplifile** | `simplifile.*` | `FileSystem` |
| **filepath** | (pure) | — |
| **gleam_regexp** | (pure) | — |
| **gleam_yielder** | (pure) | — |
| **gleam_crypto** | (pure) | — |
| **gleam_time** | `gleam/time/timestamp.system_time`, `gleam/time/calendar.local_offset`, `.utc_offset` | `Time` |
| **houdini** | (pure) | — |
| **tom** | (pure) | — |
Module-level declarations like `external effects gleam/list : []` mark an entire module as pure — any function from that module resolves as effect-free.
### Adding your own catalog entries
For packages not in the catalog, use `external effects` declarations in your project's `.graded` files:
```
// priv/graded/app.graded
external effects some_package/module.function : [Http]
external effects some_package/pure_module : []
```
### Contributing catalog files
To add a new package to the bundled catalog, create `priv/catalog/{package}@{version}.graded` with `external effects` declarations for its modules and functions. Only one version file per package is needed — the version resolution algorithm handles older installations.
## Commands
```sh
gleam run -m graded check [directory] # enforce check annotations (default)
gleam run -m graded infer [directory] # infer and write effects annotations
gleam run -m graded format [directory] # normalize .graded file formatting
gleam run -m graded format --check [directory] # verify formatting (CI mode)
gleam run -m graded format --stdin # format from stdin (editor integration)
```
## Current scope
The effect checker handles:
- Qualified and unqualified calls
- Pipe chains
- Closures and nested functions
- Case branches and guards
- Transitive local call analysis with cycle detection
- Parameter effect bounds for higher-order functions
- Type field effect annotations with type-aware resolution
- Dependency effect loading from `priv/graded/`
- Automatic inference for path dependencies in monorepo setups
- Record constructor detection (uppercase-initial names are always pure)
- Structure-preserving `.graded` file merging
## Limitations
graded performs **syntax-level analysis** using [glance](https://hexdocs.pm/glance/) — it walks the AST without type information. This keeps the tool simple and avoids depending on compiler internals, but comes with trade-offs:
- **Function references passed as values are not tracked.** If you write `list.map(items, io.println)`, graded sees `list.map` (pure) but doesn't recognize that `io.println` carries `[Stdout]` — it's passed as a value, not called. The effect is lost. Inline anonymous functions (`list.map(items, fn(x) { io.println(x) })`) work correctly because graded sees the `io.println` call directly in the function body.
- **No effect polymorphism.** You can't express "this function has whatever effects its argument has." Each `check` annotation declares a specific combination of parameter bounds. There's no way to write a generic `map(f: [e]) : [e]` — you'd need separate annotations for each concrete effect set.
- **Local transitive analysis is same-module only.** If function `a` calls `b` which calls `c`, and all are in the same module, effects are resolved transitively. But if `b` is in another module and has no `.graded` annotation, it resolves as `[Unknown]`.
- **External code is opaque.** Erlang/JavaScript FFI implementations, pre-compiled dependencies without `.graded` files, and dynamically dispatched calls cannot be analyzed. Use `external effects` declarations to annotate these manually.
In practice, the common patterns (inline callbacks, direct calls, pipe chains) are handled correctly. The main gap is function references passed to higher-order functions — a pattern that can be worked around by inlining the callback or adding explicit annotations.
## Future work
### Effect polymorphism
The current parameter bounds system requires concrete effect sets. A polymorphic system would allow effect variables:
```
effects map(f: [e]) : [e]
```
This would let one signature express that `map` propagates whatever effects its callback has, eliminating the need for per-use-case annotations. The checker would need effect unification — at each call site, bind `e` to the concrete effects of the argument and propagate upward.
### Typed AST integration
The root cause of most limitations is the lack of type information. If the Gleam compiler exposed typed AST metadata (expression types, resolved function references), graded could:
- Track effects of function references passed as values (knowing that `io.println` in `list.map(items, io.println)` is a function with `[Stdout]`)
- Resolve field calls without requiring explicit type annotations on parameters
- Eliminate false `[Unknown]` results from indirect calls
These two features — effect polymorphism and typed AST access — together would close the remaining soundness gaps and bring graded from syntax-level approximation to a complete effect system.
### Privacy and information flow checking
The next major feature is **lattice-based privacy tracking** — preventing sensitive data (PII, credentials) from flowing into logs, error messages, or third-party services.
Both checkers share the same theoretical foundation: graded modal type theory (see [THEORY.md](./THEORY.md)). Effects use sets with union; privacy uses lattices with join.
## License
Apache-2.0