Introduction to stackcallr
stackcallr.RmdWhy stackcallr
When you’re reading unfamiliar code, one of the first questions is:
what does this function actually call? For a simple function you can
just read the body. For an S3 generic like dplyr::select(),
the body is a single line, UseMethod("select"), and the
real work happens in a method you’d have to go find yourself.
stackcallr answers that question directly. Give
call_stack() the name of a function, and it returns the
tree of every function it calls, the functions those call, and so on,
resolving S3 dispatch along the way.
The basic call
call_stack() takes a single string naming a function,
either bare ("mean") or namespaced
("pkg::fun", "pkg:::internal_fun"):
call_stack("mean.default", max_depth = 1)
#> █─mean.default
#> ├─.Internal
#> ├─anyNA
#> ├─c
#> ├─floor
#> ├─is.complex
#> ├─is.logical
#> ├─is.na
#> ├─is.numeric
#> ├─isTRUE
#> ├─length
#> ├─return
#> ├─sort.int
#> ├─stop
#> ├─unique
#> └─warningThe result is a call_stack object: a tree where each
node has a name and a list of calls (its
children). Printing it draws the tree.
Limiting depth
Some call trees are deep. max_depth caps how many levels
are followed, so max_depth = 1 returns only the functions
called directly by sd():
call_stack("stats::sd", max_depth = 1)
#> █─sd
#> ├─as.double
#> ├─is.factor
#> ├─is.vector
#> ├─sqrt
#> └─varRaising it to 2 expands each of those nodes one level
further:
call_stack("stats::sd", max_depth = 2)
#> █─sd
#> ├─as.double
#> ├─█─is.factor
#> │ └─inherits
#> ├─█─is.vector
#> │ └─.Internal
#> ├─sqrt
#> └─█─var
#> ├─.Call
#> ├─as.matrix
#> ├─c
#> ├─is.atomic
#> ├─is.data.frame
#> ├─is.na
#> ├─is.null
#> ├─missing
#> ├─pmatch
#> ├─stop
#> └─stopifnotThe default, max_depth = Inf, follows every call all the
way down, stopping only at primitives (functions with no R-level body to
inspect) and at calls it has already followed once, so recursive or
mutually recursive functions don’t loop forever.
Following S3 dispatch
Static analysis alone can’t tell you what
UseMethod("select") actually runs, since that depends on
the class of the argument at call time. call_stack()
handles this case specifically: when a function’s body calls
UseMethod(), it looks up every method registered for that
generic and follows each one, in place of a single unhelpful
UseMethod leaf.
call_stack("mean", max_depth = 1)
#> █─mean
#> ├─mean.Date
#> ├─mean.default
#> ├─mean.difftime
#> ├─mean.POSIXct
#> ├─mean.POSIXlt
#> └─mean.quosureEach mean.* entry above is a real method, resolved and
ready to be expanded further with a larger max_depth.
How resolution works
call_stack() never runs the function you point it at. It
works entirely from source:
- The named function is resolved to a closure via
get()/getExportedValue(). - Its body is parsed with
codetools::findGlobals()to list the functions it references, with R’s own syntax ({,if,<-,+, …) filtered out, since those aren’t calls in any meaningful sense. - Each referenced name is looked up in the function’s own lexical scope, the same place R itself would resolve it, so calls to internal, unexported helpers are found correctly.
- If the body calls
UseMethod(), the generic’s registered methods are found and substituted in for that call. - The process repeats for every function found, down to
max_depthor until a cycle is detected.
What it can’t resolve
UseMethod() is the one form of dynamic dispatch
call_stack() follows. Everything else that decides which
function to run at call time stays unresolved, and shows up as a leaf
rather than being expanded:
-
NextMethod()calls, since the next method depends on the class vector in play at the time. - S4 and R6 dispatch.
- Functions named only as data, such as those passed to
do.call(),match.fun(), orRecall().
A leaf in the printed tree, then, means one of three things: a primitive, a cycle already followed once, or a call that static analysis can’t see through.
How this differs from lobstr::cst()
If the printed trees look familiar, that’s because the
block-character style is the one lobstr::ast() uses. The
resemblance stops at the drawing. lobstr::cst() prints the
call stack tree at the moment you call it: it walks
sys.calls(), the frames that are actually executing right
now. You use it from inside running code, usually mid-debug, after
browser() has dropped you at an error, and it shows exactly
one path, the sequence of calls that got you to that point. A real call
stack is linear, so there are no branches to show.
call_stack() (and call_tree_dir() /
call_tree_app()) work from the other direction. They never
run the function. They parse its body and recursively expand everything
it could call, including every branch, so a generic contributes
all of its UseMethod() dispatch targets rather than the one
method a particular run happened to hit. The result is a tree of
possibilities, not a record of an execution.
# mid-debug, "how did execution get here right now"
lobstr::cst()
# no execution at all, "what could this function call, in total"
call_stack("dplyr::select")Reach for cst() when you’re already inside the problem.
Reach for call_stack() when you want to audit a function
you’d rather not execute, or map an unfamiliar codebase before running
any of it.
Learning more
See ?call_stack for the full argument reference, and
?print.call_stack for how the tree is drawn.