A gradual type checker and abstract interpreter for Elixir.
This is a proof of concept exploring a practical, structural type analysis for the Elixir language. Manthan is inspired by Pydantic and Typescript and follows the pragmatism and gradual-ness of these tools. The main goal is accurate type hints for every expression, and crashing early to avoid bugs introduced during refactors.
Manthan is not just a static analysis tool; it is an inference engine that understands your code. By combining Abstract Interpretation with a Gradual Type System, Manthan allows you to mix strongly typed contracts with dynamic Elixir idioms, providing safety where you need it and flexibility where you want it.
- Gradual Typing: Incrementally adopt types. Use
deftypedfor strict contracts and standarddeffor dynamic code. Manthan validates the boundary between them. - Deep Inference: supports
- return type propogation
- recursion
- Generic Support: Full support for parametric polymorphism in Lists (
[integer]) and Structs/Maps (MapSet[string]). - Use
@specinfo: Manthan will use any existing @spec definitions from libraries. - Shim/External signatures: Easily type 3rd-party libraries or standard library modules using
declaretypewithout modifying their source code. - Abstract interpretation: Manthan parses BEAM debug info and runs an abstract "Eval-Apply" loop, effectively "running" your code in the abstract type domain to find errors logic checkers might miss.
- Editor Integration:
- Fast Feedback: Checks individual files in milliseconds using
--file. - LSP-like Experience: JSON output format powers Neovim diagnostics and Inlay Hints. See Neovim section below.
- Fast Feedback: Checks individual files in milliseconds using
Add Manthan to your mix.exs dependencies (the package is not registered yet so you'll need to clone and refer to it):
def deps do
[
{:manthan, path: "path/to/manthan"}
]
endTo start using Manthan, simply use Manthan in your module. This enables the deftyped, deftypedp, typed_struct, and declaretype macros.
Use deftyped to define public functions with signatures. Manthan will verify the body against the signature and ensure callers respect the contract.
defmodule MyApp.Math do
use Manthan
# Enforce strict integer arithmetic
deftyped add(a: integer, b: integer) :: integer do
a + b
end
# Private functions can be typed explicitely...
deftypedp double(x: integer) :: integer do
x * 2
end
# ...or left untyped! Manthan will infer the type of `infer_me`
# by analyzing the body (even if recursive!). This only
# works if all function calls can be inferred.
defp infer_me(x) do
if x > 0, do: x - 1, else: 0.0
end
endDefine structs with type information that Manthan can track.
defmodule MyApp.User do
use Manthan
typed_struct do
field :name, string
field :age, integer
field :tags, [string] # List of strings
end
endElixir has a vast ecosystem. You can "teach" Manthan about external libraries (like Jason or specific std_lib modules) without waiting for them to adopt Manthan.
defmodule MyApp.Glue do
use Manthan
# Tell Manthan that Jason.encode! takes any term and returns a string
declaretype Jason.encode!(term: any) :: string
# Use generics!
# e.g., MapSet.new takes a list of T and returns a MapSet of T
declaretype MapSet.new(list: [T]) :: MapSet[T] when type_var(T)
deftyped serialize_ids(ids: [integer]) :: string do
# Manthan knows `ids` is [integer], so MapSet.new returns MapSet[integer]
set = MapSet.new(ids)
Jason.encode!(set)
end
end| Type | Example | Description |
|---|---|---|
| Primitives | integer, float, string, boolean, atom |
Basic Erlang types. |
| Any | any |
The dynamic type. Compatible with everything (gradual typing). |
| Lists | [integer], [any] |
Homogeneous lists. |
| Tuples | {float, float} |
Fixed-size tuples with specific elements. |
| Unions | integer | float |
Value can be one of multiple types. |
| Structs | User, MyApp.User |
module references defined via typed_struct. |
| Generics | MapSet[string], Box[T] |
Parametric types. |
Check your entire project:
mix manthanCheck a specific file (fast mode):
mix manthan --file lib/my_app/worker.exOutput JSON for tooling:
mix manthan --format jsonManthan includes a Lua script for seamless Neovim integration, providing Diagnostics (error underlines) and Inlay Hints (seeing inferred types inline).
- Copy
manthan_nvim.luato your project root (or configure your editor to source it). - Source it in Neovim:
:luafile manthan_nvim.lua. - Open any
.exfile. Manthan runs on save.
Features:
- Diagnostics: Highlights type mismatches (e.g., passing
floatto a function expectinginteger). - Inlay Hints: Shows the inferred type of variables at the end of the line (e.g.,
precision: integer).
Manthan operates on the Expanded AST provided by BEAM debug info.
- Load: It loads
attributesfrom compiled modules to build a global type environment (Manthan.StdLib). - Parse: It parses your code into an internal AST.
- Eval: It traverses the AST, maintaining an environment of variable bindings.
- Apply:
- For Typed Calls, it unifies arguments against the signature.
- For Untyped/Private Calls, it performs Abstract Interpretation, effectively "running" the function in the type domain to determine its result.
Built with ❤️ using Elixir metaprogramming.