Program dependence graph for Elixir, Erlang, and Gleam.
Reach builds a graph of what depends on what in your code — data flow, control flow, and side effects. Trace any value back to its origin, find tainted paths from user input to dangerous sinks, or check whether two statements can be safely reordered.
Works on Elixir source, Erlang source, Gleam source, and compiled BEAM bytecode. Elixir 1.18+ / OTP 27+.
Add Reach to your dependencies:
def deps do
[{:reach, "~> 1.2"}]
endBuild a graph from any Elixir code and ask questions about it:
graph = Reach.string_to_graph!("""
def run(input) do
command = String.trim(input)
System.cmd("sh", ["-c", command])
end
""")Find what affects a specific call — trace the System.cmd back to its origins:
[cmd_call] = Reach.nodes(graph, type: :call, module: System, function: :cmd)
Reach.backward_slice(graph, cmd_call.id)Check whether user input reaches a dangerous sink without sanitization:
Reach.taint_analysis(graph,
sources: [type: :call, function: :params],
sinks: [type: :call, module: System, function: :cmd],
sanitizers: [type: :call, function: :sanitize]
)Or check whether two statements can be safely reordered:
Reach.independent?(graph, node_a.id, node_b.id)mix reach lib/my_app/accounts.ex lib/my_app/auth.exGenerates a self-contained HTML report with three tabs:
- Control Flow — expression-level graph with branch/converge edges, syntax-highlighted source, if/case/unless detection
- Call Graph — function calls grouped by module
- Data Flow — variable def→use chains within functions
Fully offline — everything embedded in a single HTML file.
Requires optional deps:
{:jason, "~> 1.0"},
{:makeup, "~> 1.0"}, # syntax highlighting
{:makeup_elixir, "~> 1.0"}Eight mix tasks for code analysis. All support --format text (default,
colored), json, and oneline.
# Module inventory sorted by complexity
mix reach.modules --sort complexity
# Dead code detection
mix reach.dead_code# What calls this and what does it call?
mix reach.deps MyApp.Accounts.register/2
# What breaks if I change this function?
mix reach.impact MyApp.Accounts.register/2# Does user input reach the database?
mix reach.flow --from conn.params --to Repo
# Where is this variable defined and used?
mix reach.flow --variable user
# What code affects this line?
mix reach.slice lib/my_app/accounts.ex:45
# Where does this value flow to?
mix reach.slice --forward lib/my_app/accounts.ex:45# GenServer state machines, ETS coupling, missing handlers
mix reach.otp
# Cross-function redundant computations, suboptimal Enum patterns
mix reach.smellWith the optional boxart dependency, render graphs directly in the terminal:
# Control flow graph with source code
mix reach.graph MyApp.Server.handle_call/3
# Call graph as a tree
mix reach.graph MyApp.Server.handle_call/3 --call-graph
# Any command with --graph
mix reach.deps MyApp.Accounts.register/2 --graph
mix reach.impact MyApp.Accounts.register/2 --graph
mix reach.modules --graph
mix reach.otp --graphRequires {:boxart, "~> 0.3"} in your deps.
graph = Reach.file_to_graph!("lib/accounts.ex")
Reach.backward_slice(graph, node_id) # what affects this expression?
Reach.forward_slice(graph, node_id) # what does this expression affect?
Reach.chop(graph, source_id, sink_id) # all paths from A to BReach.taint_analysis(graph,
sources: [type: :call, function: :params],
sinks: [type: :call, module: Repo, function: :query],
sanitizers: [type: :call, function: :sanitize]
)
#=> [%{source: node, sink: node, path: [node_id, ...], sanitized: boolean}]
# Predicates work too
Reach.taint_analysis(graph,
sources: &(&1.meta[:function] in [:params, :body_params]),
sinks: [type: :call, module: System, function: :cmd]
)# Safe to reorder?
Reach.independent?(graph, id_a, id_b)
# Data flow between expressions
Reach.data_flows?(graph, source_id, sink_id)
Reach.depends?(graph, id_a, id_b)for node <- Reach.dead_code(graph) do
IO.warn("#{node.source_span.start_line}: unused #{node.type}")
end# Elixir source
graph = Reach.file_to_graph!("lib/my_module.ex")
{:ok, graph} = Reach.string_to_graph("def foo(x), do: x + 1")
# Erlang source (auto-detected by extension)
{:ok, graph} = Reach.file_to_graph("src/my_module.erl")
# Pre-parsed AST (for Credo/ExDNA integration)
{:ok, graph} = Reach.ast_to_graph(ast)
# Gleam source (requires glance parser)
{:ok, graph} = Reach.file_to_graph("src/app.gleam")
# Compiled BEAM bytecode — sees macro-expanded code
{:ok, graph} = Reach.module_to_graph(MyApp.Accounts)Reach can analyze JavaScript and TypeScript files, including cross-language data flow for projects using QuickBEAM.
# Parse JS/TS files
{:ok, nodes} = Reach.Frontend.JavaScript.parse_file("assets/app.ts")
# Cross-language analysis with the QuickBEAM plugin
graph = Reach.string_to_graph!(elixir_source, plugins: [Reach.Plugins.QuickBEAM])The QuickBEAM plugin automatically detects embedded JS in
QuickBEAM.eval calls and creates cross-language edges:
| Edge | Meaning |
|---|---|
:js_eval |
Elixir eval call → JS function definitions |
{:js_call, name} |
QuickBEAM.call(rt, name) → JS named function |
{:beam_call, name} |
JS Beam.callSync(name) → Elixir handler |
Requires {:quickbeam, "~> 0.10", optional: true} in your deps.
TypeScript is stripped via OXC, ES module syntax handled automatically.
mix reach src/app.gleamRequires the glance parser:
git clone https://github.com/lpil/glance /tmp/glance
cd /tmp/glance && gleam build --target erlangproject = Reach.Project.from_mix_project()
# or: Reach.Project.from_glob("lib/**/*.ex")
# Cross-module taint analysis
Reach.Project.taint_analysis(project,
sources: [type: :call, function: :params],
sinks: [type: :call, module: System, function: :cmd]
)- Four frontends — Elixir source, Erlang source, Gleam source, BEAM
bytecode. The BEAM frontend sees
use GenServercallbacks, macro-expanded code, and generated functions invisible to source analysis. - OTP-aware — GenServer state threading, message content flow, ETS dependencies, process dictionary tracking, call/reply pairing.
- Concurrency edges —
Process.monitor→:DOWNhandlers,trap_exit→:EXIThandlers,Task.async→Task.await, supervisor child startup ordering. - Interprocedural — context-sensitive slicing (Horwitz-Reps-Binkley), cross-module call resolution, dependency summaries for external packages.
- Effect classification — knows which functions are pure, which do I/O, and which send messages. Covers Enum, Map, String, and 30+ more modules out of the box.
Every expression in the analyzed code becomes a node:
Reach.nodes(graph)
Reach.nodes(graph, type: :call, module: Repo, function: :insert, arity: 1)
node.type #=> :call
node.meta #=> %{module: Repo, function: :insert, arity: 1}
node.source_span #=> %{file: "lib/accounts.ex", start_line: 12, ...}Edges capture dependencies:
| Label | Meaning |
|---|---|
{:data, var} |
Data flows through variable |
:containment |
Parent depends on child sub-expression |
{:control, label} |
Controlled by branch condition |
:call |
Call site to callee |
:parameter_in / :parameter_out |
Argument/return flow |
:summary |
Parameter flows to return value |
:state_read / :state_pass |
GenServer state flow |
{:ets_dep, table} |
ETS write → read |
:monitor_down / :trap_exit / :link_exit |
Crash propagation |
:task_result |
Task.async → Task.await |
{:message_content, tag} |
Message payload flow |
:js_eval |
Elixir eval → JS function |
{:js_call, name} |
Elixir call → JS named function |
{:beam_call, name} |
JS Beam.callSync → Elixir handler |
graph TD
Source["Source (.ex / .erl / .gleam / .js / .beam)"] --> Frontend
Frontend["Frontend → IR Nodes"] --> CFG
CFG["Control Flow Graph"] --> Dom
Dom["Dominators"] --> CD
CD["Control Dependence"] --> PDG
CFG --> DD
DD["Data Dependence"] --> PDG
PDG["Program Dependence Graph"] --> SDG
SDG["System Dependence Graph + OTP"] --> Query
Plugins["Plugins (Phoenix, Ecto, QuickBEAM...)"] --> SDG
Query["Slicing · Taint · Independence"]
Reach builds a program dependence graph (PDG) — a directed graph where nodes are expressions and edges are data/control dependencies. Multiple function PDGs are connected into a system dependence graph (SDG) via call/summary edges for interprocedural analysis.
Single-threaded, Apple M1 Pro:
| Project | Files | Time |
|---|---|---|
| Livebook | 72 | 160ms |
| Oban | 64 | 195ms |
| Keila | 190 | 282ms |
| Phoenix | 74 | 333ms |
| Absinthe | 282 | 375ms |
740 files, zero crashes.
- Ferrante, Ottenstein, Warren — The Program Dependence Graph and Its Use in Optimization (1987)
- Horwitz, Reps, Binkley — Interprocedural Slicing Using Dependence Graphs (1990)
- Silva, Tamarit, Tomás — System Dependence Graphs for Erlang Programs (2012)
- Cooper, Harvey, Kennedy — A Simple, Fast Dominance Algorithm (2001)