Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
0.13.0 - 2026-08-20
Added
external returns <module>.<function> : <operator>declares the closure an FFI producer hands back, so calling it resolves instead of costing[Unknown]. The line is hand-written and preserved bygraded infer, and it answers where the declaration stands alone — a call refuses it where the declaration is out of the build’s reach or a Gleam fallback body runs beside it, and says which. Dependencies and path dependencies can ship the line for their own producers.
0.12.1 - 2026-08-19
Fixed
- A catalog entry that both declares a function
external effectsand carries aneffectsline for it now resolves to the declaration, so the function’s effects no longer come from a term whose parameter bounds were dropped. - Two catalog files keying the same function now settle it whole: the file
whose
effectsline wins the term supplies its parameter bounds too. A higher-order entry another package declaresexternal effectsno longer loses its bounds — and with them its callers’ effects — depending on which catalog file was read last. - A stale duplicate copy of a dependency module left under
build/packages— a dependency moved to a path dependency without agleam clean— no longer has a say in anything graded reads from dependency source. The copy the build compiles against alone decides whether anexternal effectsline naming one of its functions is dead, which parameter signatures match at call sites, which update builders resolve, and which of its functions are foreign; a winning copy graded cannot read contributes nothing and still shadows the stale one beside it.
0.12.0 - 2026-08-18
Added
- New
graded why <name>command: explains where a function’s effects come from, one line per effect contributor, naming the source that resolved each one or the reason it stayed[Unknown]. Works for any project function, private ones included, with or without acheckline, and writes nothing. - New
graded effect <name>command: looks up a function or type-field effect and prints it in prose, without writing the spec file or the cache. Pass--format=gradedfor a.gradedline that parses back. graded effectnames the source that answered — your spec, a dependency’s shipped spec, a catalog entry, inference — as asource:line in prose and a// resolved from ...comment in--format=graded.- New
graded infer --dry-runflag: prints a line diff of whatinferwould change in the spec file, and writes nothing. - New
graded packcommand: injects the configured.gradedspec into the hex tarball built bygleam export hex-tarball, so the spec ships with the package and downstream projects read it with no setup. - New
graded --helpandgraded --version. - New
[tools.graded].targetssetting for packages built for more than the one targetgleam.toml’stargetfield can name. - A record field set through a builder (
with_*) record update now resolves to the set value’s precise effect, last-write-wins, instead of[Unknown]— including when the builder lives in a dependency.
Changed
- BREAKING: a per-function
external effectsline naming one of your own Gleam-bodied functions is now ignored, with a warning — the body is what runs, and every path now walks it.graded inferdeletes the line and writes theeffectsline it was suppressing. There is no replacement override: fix the source or widen thecheckbudget. The module-levelexternal effects <module>form is unchanged. - A
checkline on an@externalfunction is now verified against what declares it — anexternal effectsline, a module-level external, or a catalog entry — instead of silently passing. Without a declaration it checks against[Unknown], and a staleeffectsline no longer answers for an external anywhere: not for its callers, not forgraded effect. - Foreign code is now opaque on every channel a value carries its effects
through: a closure returned by an
@external, or a record field wired through one, reads[Unknown]instead of inheriting the Gleam fallback body’s inferred effect.graded inferno longer writes areturnsline for an@external, and a dependency’s shippedeffects/returnsline for a function its source declares@externalis refused. Specs that leaned on fallback-derived results will report new violations; wrap the producer in ordinary Gleam or annotate the field with atypeline. - graded now reads build targets.
gleam.toml’starget(or[tools.graded].targets) decides which@externalimplementations are compiled: a Gleam fallback body that runs on a compiled target is charged to every caller and weighed against the function’s owncheckline, a body no compiled target reaches is dead text and charged nothing, and a declaration covering only targets the build never compiles reads[Unknown]under a message that says why.@targetnarrows a single function the same way. - A package that names no
targetnow reads Gleam fallback bodies on Erlang — the compiler’s own default — so stdlib functions with JavaScript externals (list.append,dict.from_list, …) no longer resolve to[Unknown]in a default project. - Violation messages now say why an effect is unresolved (
whose type could not be resolved,an external with no declared effects, …) and where a resolved one came from ((from gleam_stdlib's catalog entry)). - Violation messages describe call sites in prose —
calls field `resolver` on `config`,calls a computed function value— instead of printing internal sentinels, and say “unresolved effects” only when the reported set carriesUnknown. graded checkflagsexternal effectslines that resolve to nothing — a function or module no dependency, catalog entry, or project module can place — mirroring the existingcheckandtypelints.- The “passed as a value” warning now quotes the declaration callers are
actually charged rather than a stale spec line, and stays silent when the
whole effect set is
[Unknown]. graded effectreads publicity and existence from your source: a private function, a private@external, or a spec line naming a function your source doesn’t define now exits non-zero, and a module that fails to parse is reported as such instead of being answered from the spec.- A directory inside a package’s
src/now narrows what is reported, not what is analysed, so a scopedgraded checkreports exactly what the whole-package run reports for those files. A scopedinferstill writes the whole package’s spec. - Calls into girard resolve from the spec girard now ships in its hex tarball. graded requires girard 2.1.0 or later.
- An unknown command or option now prints a usage error and exits non-zero instead of being treated as a directory to check.
Fixed
- A
checkline admitting[Unknown]no longer fails on an effect variable the caller has no way to bind; such a variable now reads as the[Unknown]it is on every channel. - A call to a function-typed parameter no
checkline names now resolves to that parameter’s effects instead of[Unknown], agreeing with whatgraded effectanswers for the same function. graded checkno longer reports one violating call once per call site that reaches it, and violations print in source order.- A spec file that exists but can’t be read now errors with
Could not read: <path>instead of being treated as absent. - A dependency’s shipped
external effectsdeclarations now resolve for consumers; previously such functions reported[Unknown]. - A path dependency’s committed spec now outranks the bundled catalog for the same function, as documented.
- A committed higher-order
effectsline now applies its own parameter bounds with its own term, instead of pairing the term with bounds re-derived from source. graded inferno longer writes an effect line that fails to parse back; an under-applied effect operator renders as[Unknown].- A record field wired to a function from the module doing the wiring now
resolves to that function’s effect instead of
[Unknown]. - A field a builder overlay does not replace now resolves through a
traceable base instead of
[Unknown]. - A module resolving a field through a builder overlay now reports the same effect to its consumers as it does when checked directly.
- A record field wired to one of the producer’s own functions now resolves in the module that defined it, never against a same-named function of the consumer’s.
- A field call on a receiver graded can’t trace to a construction no longer borrows the effect of an unrelated construction elsewhere in the package; it stays conservative instead of understating.
- A record field wired from a producer whose return type is a module-local
alias to a function type now infers the producer’s real effect instead of
[Unknown]. - A polymorphic producer wired into a field now binds the construction-site argument’s effect instead of dropping it.
- A residual effect variable that collides with a returned closure’s
callback parameter now grounds to
[Unknown]instead of being captured and silently dropped. - README documentation links now point at the published hexdocs pages instead of 404-ing on the package page.
graded check/inferpointed at a package root now scope tosrc/instead of reporting all checks passed while checking nothing.
0.11.0 - 2026-07-16
Changed
- Modules using arithmetic in bit-array pattern segment sizes
(
<<value:size(n * 8)>>) now parse and infer instead of collapsing every function in them to[Unknown], via glance 7.0.0 (through girard 2.0.0).
Fixed
- A computed receiver whose helper returns one of its parameters — as a bare
parameter, a receiver path (
config.options), or a constructor rebuilt from parameter-rooted fields — now forwards field effects instead of collapsing to[Unknown]. Same-module and cross-module helpers both resolve; anything the provenance can’t trace still widens. - A helper returning a
case/ifforwards through every branch when all branches are parameter-rooted; a branch it can’t trace still widens the whole receiver. - A helper that rebuilds a record from a mix of parameters and literal
defaults now forwards the parameter-rooted fields instead of collapsing
the whole build, and field shorthand (
Options(resolver:)) resolves to the parameter it puns. - A labeled computed-receiver call now reorders its arguments into declared parameter order before substituting, forwarding exactly as a positional call does.
- A helper returning a parameter through direct tail recursion now resolves by a bounded fixpoint instead of widening.
- A path dependency inferred from source now propagates its return-value provenance to the consumer, so computed-receiver calls into it forward field effects like same-package modules do.
- Function-typed field effects now forward through more receiver argument
shapes: a parameter passed directly, a field path rooted at a caller
parameter, an inline constructor or factory call wired from a caller
parameter, and a let-bound alias of any of these. A caller bound such as
config.options.resolver: [Stdout]discharges the forwarded effect; left unbound it still collapses to[Unknown]. - Calling a let-bound alias of a parameter (
let f = handler; f(x)) now resolves through the parameter’s bound, shadowing a same-named unqualified import. - A recursive function reached through a higher-order call (
list.flat_map( children, walk)) now resolves to its real effect instead of[Unknown]. - graded now finds its bundled catalog relative to its own install directory instead of the working directory, and warns when none can be found instead of degrading silently.
inferandcheckagainst an out-of-tree source directory now root the spec and cache at that project’s owngleam.toml, not the passed directory.
0.10.1 - 2026-06-26
Fixed
- A function-typed field on a dependency-defined type now resolves to its
declared effect instead of
[Unknown]: the dependency’s source is read to type the receiver, so atype dep/repo.Repo.find : [Storage]line resolves at the call site. - A dependency’s
typefield annotations, and the catalog’s, are now loaded into the knowledge base, so a consumer resolves field calls on dependency-defined records without re-declaring them.
0.10.0 - 2026-06-25
Added
- Lustre 5 catalog entry (
lustre@5.0.0.graded); 4.x projects keeplustre@4.0.0.graded. graded checkwarns on spec lines that match nothing: acheckline naming no project function, or atypeline resolving no callable field.- Function-typed record fields on receivers with no traceable construction
site now resolve polymorphically as field-effect variables, dischargeable
by a field bound or a
typeline; left unbound they concretize to[Unknown], never silently[].
Fixed
- A module-level
external effects <module>declaration now governs a path dependency’s inferred module with its full effect set, instead of being flattened to pure or shadowed by source inference. - A module-level external now governs the consumer’s own project modules
too, at both
checkandinfertime, instead of being shadowed by in-memory inference of the module’s source.
0.9.4 - 2026-06-24
Fixed
- A closure passed to a second-order parameter now keeps the callable
bindings captured at its creation site, so a name bound there
(
let suffix = string.append) resolves precisely on re-analysis instead of[Unknown]. - Expression-valued callees — an immediately invoked closure, an applied
returned function, a
case/ifselecting the function being called — now propagate the callee’s effect instead of being inferred pure; an opaque computed callee resolves to[Unknown]rather than[]. - A parameter that shadows an unqualified import now resolves to the parameter, not the import.
- A let-bound closure called directly by name (
let helper = fn(x) { ... }; helper(1)) now resolves to its body’s effect instead of[Unknown]. - More higher-order closure patterns resolve precisely: callbacks with ordinary value parameters, callbacks that ignore a higher-order parameter, producers whose returned closure captures a first-order callback, and immediately invoked closures with several arguments.
- An immediate application of a returned function (
make(io.println)()) no longer drops the producer’s arguments, and an internal effect variable no call site can bind collapses to[Unknown]instead of leaking.
0.9.3 - 2026-06-23
Fixed
- A same-module (unqualified) call into a bodyless
@externalnow applies itsexternal effectsdeclaration, matching the cross-module path. Undeclared externals still resolve to[Unknown].
0.9.2 - 2026-06-23
Fixed
- Record update expressions (
Rec(..base, field: expr)) now have their updated field values walked, so their effects are counted. - Dependency, catalog, and path-dependency resolution now read from the checked project’s own root instead of the process working directory.
- A higher-order function defined in a path dependency now discharges its
callback parameter’s effect at the call site instead of leaking the
parameter’s effect variable, reaching parity with
build/packagesdependencies. - graded now compiles and runs on the JavaScript target.
0.9.1 - 2026-06-23
Added
- Catalog entries for the pure value libraries
bigi,glearray,iv, andgleam_community_maths.
Fixed
- A higher-order callback passed with a Gleam label (
apply(with: parser)) now binds to its parameter, matching positional arguments.
0.9.0 - 2026-06-22
Added
- Field bounds: a
checkline can bound a function-typed field reached through a parameter (check myapp.view(handler.on_click: [Dom]) : [Dom]), taking priority over receiver-type resolution. - A field bound whose path matches no field call in the body, and a parameter bound naming no declared parameter, now emit warnings.
Fixed
gleam/time/calendar.utc_offsetis now[]instead of[Time]: it is a compile-time constant, not a clock read.- A same-module named function passed to a first-order fn-typed parameter
now resolves to its actual effect instead of
[Unknown].
0.8.1 - 2026-06-22
Changed
- Dropped the
stdinandgleam_yielderdependencies;graded format --stdinnow reads standard input through a small built-in FFI. Thestdinpackage cappedgleam_stdlibbelow1.0.0, making graded uninstallable alongside packages requiringgleam_stdlib >= 1.0.0.
0.8.0 - 2026-06-21
Added
- Catalog entries for 27 more of the most-used Gleam packages: glance,
glexer, justin, snag, ranger, marceau, gleam_community_colour,
gleam_community_ansi, glam, splitter, gleam_bitwise, gleam_javascript,
and gleam_deque (pure); glisten, mist, wisp, pog, gleam_fetch,
gleam_hackney, gleam_cowboy, gleam_elli, shellout, logging, argv,
directories, birl, and youid (effectful). The catalog now covers all of
the core
gleam-langruntime, data, and HTTP packages. - New effect labels
Network,Database,Exec, andRandom.
Fixed
- A function in both an installed dependency’s spec file and the bundled catalog now takes its effects from the dependency’s spec file.
- Effects inside
panic/todo/echomessages and bit-string segments are now counted. graded formatandformat --checknow error on an unparseable spec file instead of succeeding silently.- A malformed
gleam.tomlis now reported as an error instead of being silently ignored.
0.7.0 - 2026-06-19
Added
- Second-order (higher-kinded) effect variables: the effect representation
is now an
EffectTerm(a lambda calculus with union), so an operator parameter (action: fn(fn() -> Nil) -> a) infers a curried application over its callbacks and beta-reduces to concrete effects at each call site. Named refs, closures,case/ifbranches, and returned operators all lift; the.gradedsyntax gained operator applications and bounds (fn(a, b) -> [a, b]), with first-order lines unchanged. See docs/SECOND_ORDER_EFFECTS.md. - More value flow resolves instead of
[Unknown]: blocks resolve to their tail expression; returned operators cross modules and packages viareturnslines; record fields wired to an inline closure infer from the body;checkauto-infers project modules missing from the spec (in memory, nothing written); operator-typed record fields are lifted and applied; and a producer returning or wrapping an operator parameter (a decorator) resolves. Environmenteffect and an envoy catalog entry for process env-var access.
Fixed
@external(FFI) functions are now[Unknown]by default — foreign code is opaque, and a Gleam fallback body only runs on the other compile target. Opt into a precise effect withexternal effectsor the catalog.- Field calls on a record built at several construction sites no longer leak operator bounds; the union of operators is applied to the call’s arguments and distributes.
inferno longer hangs on densely mutually-recursive modules: analysis is memoized per module and SCC-partitioned, with results unchanged.
Notes
- Remaining residuals, all sound and collapsing to
[Unknown]: a parameter selected through a branch, a field wired to a constructor parameter, a function reached through arbitrary computation, ause-tailed return, and external/FFI code. Annotate explicitly where needed.
0.6.0 - 2026-04-21
Added
- Same-function value flow: calls through local
letbindings now resolve — function-ref aliases (let f = io.println; f("hi"), transitively), and record construction (let v = Validator(to_error: io.println); v.to_error("oops")), labelled or positional. Shadowing and Gleam’s block/closure scoping are respected.
Notes
- Cross-function record construction remains opaque and still needs
type-level annotations. Pattern destructuring and
use-bound names are deliberately treated as opaque.
0.5.0 - 2026-04-13
Added
- Effect polymorphism: effect variables let one signature propagate a
callback’s effects (
effects myapp.map_with_log(f: [e]) : [Stdout, e]);graded inferproduces them automatically for fn-typed parameters. - Call-site substitution: at each call, effect variables bind to the concrete effects of the argument passed — labeled or positional, across modules and into dependencies.
- Dependency parameter positions are learned by parsing each dependency’s source, so positional arguments to polymorphic dependency functions resolve without labels.
- Wildcard
[_]documented as the top of the effect lattice — a declared budget of[_]permits any effects.
Changed
- Violation messages now hint at a
checkbound or a concrete argument when the actual effects contain unresolved effect variables.
0.4.2 - 2026-04-12
Fixed
- Added
gleam/dynamic/decodeto thegleam_stdlibcatalog; decoder combinators are pure but resolved as[Unknown]. - Cross-module type constructors (
types.NotFound(id)) now resolve as pure, matching unqualified constructors; effects inside a constructor’s arguments still propagate.
0.4.1 - 2026-04-11
Fixed
graded infernow reads the spec file’sexternal effectsandtypedeclarations into the knowledge base before walking the import graph, so functions calling a module declared pure stop inferring[Unknown].
0.4.0 - 2026-04-10
Added
[tools.graded]config table ingleam.toml, withspec_fileandcache_dirfields.
Changed
- Annotations moved out of
priv/graded/: each package now has a single spec file at the project root (default<package_name>.graded) holding the public-API effects,checkinvariants, externals, andtypelines, while per-module inferred effects live inbuild/.graded/as a regenerable cache. - Spec-file names are now module-qualified (
myapp/router.handle_request,type myapp.Handler.on_click : [Dom]); cache files keep bare names. - Library authors must add their spec file to
included_filesingleam.tomlfor consumers to see their effects. - No automatic migration: move every line from
priv/graded/*.gradedinto the root spec file, qualify the names, rungraded infer, and delete the old directory.
0.3.0 - 2026-04-07
Added
- Cross-module effect propagation: inferred effects from sibling project modules are used when analyzing other modules in the same project.
0.2.0 - 2026-04-07
Added
- Catalog entries for
gleam_time(system_time,local_offset,utc_offsetare[Time], the rest pure) andhoudini(pure). - Automatic effect inference for path dependencies declared in
gleam.toml, two-pass so cross-dependency calls resolve, loading any existing.gradedfiles for parameter bounds.
Fixed
- Record constructors (
Ok,Error,Some, custom types) are no longer inferred as[Unknown]; constructors are always pure.
0.1.0 - 2025-04-04
Added
- Effect checker for Gleam via sidecar
.gradedannotation files. graded checkcommand to enforcecheckannotations.graded infercommand to infer and writeeffectsannotations.graded formatcommand with--checkand--stdinmodes.- Higher-order effect tracking with parameter bounds.
- Field call effect tracking with type-aware resolution.
- External effect declarations for third-party functions.
- Wildcard effect
[_]as the universal top element. - Warnings for function references passed as values with known effects.
- Versioned catalog system resolved against
manifest.toml. - Catalog entries for
gleam_stdlib,gleam_erlang,gleam_otp,gleam_http,gleam_httpc,gleam_json,gleam_regexp,gleam_yielder,gleam_crypto,lustre,lustre_http,simplifile,filepath,tom.