What a try handler catches: the classes it tests for, the reason tags
it discriminates on, and whether some path catches a class outright.
Reading the bytecode
{:try, reg, {:f, L}} installs a handler; the block at L begins with
{:try_case, reg}, after which {x,0} holds the class (:error,
:exit, :throw), {x,1} the reason and {x,2} the stacktrace. The
clauses of an Elixir rescue/catch compile to tests on those
registers: a class test is is_eq_exact {x,0} :exit; a reason pattern
such as {:noproc, _} is is_tagged_tuple {x,1} 2 :noproc, or an
is_eq_exact on an element pulled out with get_tuple_element. A
clause with no pattern on the reason (catch :exit, reason ->,
rescue e ->) tests the class and runs its body. A clause that no
clause matches falls to a block that re-raises (bif raise).
The walk follows every path from try_case — the pass and fail edges of
each test, every select_val arm, jumps and fallthroughs — carrying the
class the path has established and whether it has tested the reason
({x,1} or a register copied or projected from it). A path that ends in
a normal return without having tested the reason catches its class
outright (total); a class established on any such path is a class the
handler catches. Paths that end in a raise are not catches. Reason tags
are the atoms compared along a catching path, recorded against that
path's class — so catch :exit, {:noproc, _} yields the pair
{:exit, :noproc} and nothing for :error. Within one clause the
atoms its body compares count too (a case in the body), the same
over-approximation callback_tag makes: a rule asks whether a tag is
NOT handled, so seeing too many suppresses rather than invents.
An Elixir rescue X tests the reason's __struct__ (a map_get
before Exception.normalize/3); the register that read holds is
treated as a projection of the reason, so the clause counts as tested
and X is its tag.
Summary
Types
What one handler catches. classes and totals are among :error,
:exit, :throw and :* (no class test on the path); tags are
{class, atom} pairs, the atoms compared along a path that catches
that class; falls_through are the tags compared on
a path that reaches a case with no clause for its value — the
compiler emits such a case of its own for e.field access, so the
tags say which case it was.
Functions
Summarise the handler at label of the function instrs.
Types
@type summary() :: %{ classes: [atom()], totals: [atom()], tags: [{atom(), atom()}], falls_through: [atom()] }
What one handler catches. classes and totals are among :error,
:exit, :throw and :* (no class test on the path); tags are
{class, atom} pairs, the atoms compared along a path that catches
that class; falls_through are the tags compared on
a path that reaches a case with no clause for its value — the
compiler emits such a case of its own for e.field access, so the
tags say which case it was.
Functions
@spec analyse([tuple()], non_neg_integer()) :: summary()
Summarise the handler at label of the function instrs.