shashi/manthan

a practical type checker for Elixir inspired by Pydantic

★ 0Forks 0ElixirGitHub ↗Compare

README

Manthan

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.

Key Features

  • Gradual Typing: Incrementally adopt types. Use deftyped for strict contracts and standard def for 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 @spec info: Manthan will use any existing @spec definitions from libraries.
  • Shim/External signatures: Easily type 3rd-party libraries or standard library modules using declaretype without 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.

Quick Start

1. Installation

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"}
  ]
end

2. Usage

To start using Manthan, simply use Manthan in your module. This enables the deftyped, deftypedp, typed_struct, and declaretype macros.

Defining Typed Functions

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
end

Typed Structs

Define 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
end

External Types (declaretype)

Elixir 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

The Type System

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.

CLI & Editor Integration

Run from Terminal

Check your entire project:

mix manthan

Check a specific file (fast mode):

mix manthan --file lib/my_app/worker.ex

Output JSON for tooling:

mix manthan --format json

Neovim Integration

Manthan includes a Lua script for seamless Neovim integration, providing Diagnostics (error underlines) and Inlay Hints (seeing inferred types inline).

  1. Copy manthan_nvim.lua to your project root (or configure your editor to source it).
  2. Source it in Neovim: :luafile manthan_nvim.lua.
  3. Open any .ex file. Manthan runs on save.

Features:

  • Diagnostics: Highlights type mismatches (e.g., passing float to a function expecting integer).
  • Inlay Hints: Shows the inferred type of variables at the end of the line (e.g., precision: integer).

How it uorks

Manthan operates on the Expanded AST provided by BEAM debug info.

  1. Load: It loads attributes from compiled modules to build a global type environment (Manthan.StdLib).
  2. Parse: It parses your code into an internal AST.
  3. Eval: It traverses the AST, maintaining an environment of variable bindings.
  4. 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.

Contributors

shashi

Issues