Skip to content
 
 

Repository files navigation

Reach

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+.

Quick start

Add Reach to your dependencies:

def deps do
  [{:reach, "~> 1.2"}]
end

Build 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)

Interactive visualization

mix reach lib/my_app/accounts.ex lib/my_app/auth.ex

Generates 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"}

CLI tools

Eight mix tasks for code analysis. All support --format text (default, colored), json, and oneline.

Codebase overview

# Module inventory sorted by complexity
mix reach.modules --sort complexity

# Dead code detection
mix reach.dead_code

Function analysis

# 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

Data flow

# 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

OTP and performance

# GenServer state machines, ETS coupling, missing handlers
mix reach.otp

# Cross-function redundant computations, suboptimal Enum patterns
mix reach.smell

Terminal graphs

With 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 --graph

Requires {:boxart, "~> 0.3"} in your deps.

Core workflows

Slicing

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 B

Taint analysis

Reach.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]
)

Independence and reordering

# 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)

Dead code

for node <- Reach.dead_code(graph) do
  IO.warn("#{node.source_span.start_line}: unused #{node.type}")
end

Building a graph

# 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)

JavaScript / TypeScript support

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.

Gleam support

mix reach src/app.gleam

Requires the glance parser:

git clone https://github.com/lpil/glance /tmp/glance
cd /tmp/glance && gleam build --target erlang

Multi-file project analysis

project = 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]
)

What makes Reach different

  • Four frontends — Elixir source, Erlang source, Gleam source, BEAM bytecode. The BEAM frontend sees use GenServer callbacks, 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 edgesProcess.monitor:DOWN handlers, trap_exit:EXIT handlers, Task.asyncTask.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.

Nodes and edges

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

Architecture

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"]
Loading

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.

Performance

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.

References

  • 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)

License

MIT

About

Program dependence graph and architecture/code-flow analysis for BEAM projects

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages