Argus.Extractors.ErrorHandling.CatchClauses (Panoptes v0.13.0)

Copy Markdown View Source

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

summary()

@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

analyse(instrs, label)

@spec analyse([tuple()], non_neg_integer()) :: summary()

Summarise the handler at label of the function instrs.