Capabilities
Qualitative overview of flowR's support for R.
Please keep in mind that the capabilities are a qualitative measure.
Statements like "flowR can fully handle 50/80 capabilities" are discouraged as some capabilities have a vastly different granularity (and different levels of real-world presence).
Missing something? Suggest a capability!
Nothing matches that.
Names and Identifiers
Every name an R program writes down, the Form it is written in, and the binding it resolves to.
21 fully8 partially1 notExpressions
The calls, Operators, control flow, and function definitions an R program is built from.
CallsIndex AccessOperatorsControl-FlowFunction DefinitionsImportant Built-InsLiteral Values
52 fully22 partially1 notNon-Standard Evaluations/Semantics
The places where R does not evaluate an expression the way it is written, so a name in it may mean something other than the binding it seems to read.
1 fully5 partially1 notObject-Oriented Programming
R's object systems, the class an object carries, and the dispatch that decides which function body a generic call runs.
S3S4Class-Based Dependency Attribution
2 fully10 partially0 notR File Structure
The lexical shape of a source file, down to its line endings and encoding.
5 fully4 partially0 notProject
The files around the R code, from the package metadata and the packages a version manager pins to what runs before a script does.
Package MetadataDependency ManagersStartup and DiscoveryPre-Processors/external Tooling
10 fully5 partially0 notSystem, I/O, FFI, and Other Files
Everything a program reaches for beyond its own code, from files it sources to calls it makes out of R.
0 fully10 partially0 notTypes
What a value is, how we infer it from the code, and the coercions R applies between types.
0 fully1 partially3 notNames and Identifiers
21 fully8 partially1 not
Every name an R program writes down, the Form it is written in, and the binding it resolves to.
Consider the following R code:
"f" <- function(x) { get("x") }
`y x` <- 2
print(`y x` + f(3))Identifiers of interest are:
- The symbols
x(Normal),f(Quoted), and`y x`(Escaped). - The function calls
<-,function,{,get,+, andprint(Calls, all given with Normal). Especially{is identified as a Grouping of the Function Definitions' body. - The quoted name created by a function call
get(Created).
Besides the parameter x, which is resolved in its Lexicographic Scope, the other identifiers are resolved in the Global Scope.
flowchart LR
10["`**function** (L. 1)
*RFunctionDefinition*`"]
subgraph "flow-10" ["function(x) #123; get(#34;x#34;) #125; (L. 1)"]
1["`**x** (L. 1)
*RSymbol*`"]
8-get-name(["`**#34;x#34;** (L. 1)
*RSymbol*`"])
8[["`base#58;#58;**get** (L. 1)
*RFunctionCall*`"]]
built-in:get["`Built-In:
get`"]
style built-in:get stroke:gray,fill:gray,stroke-width:2px,opacity:.8;
9[["`base#58;#58;**#123;**
*RExpressionList*`"]]
built-in:_123_["`Built-In:
#123;`"]
style built-in:_123_ stroke:gray,fill:gray,stroke-width:2px,opacity:.8;
end
0["`**#34;f#34;** (L. 1)
*RString*`"]
11[["`base#58;#58;**#60;#45;** (L. 1)
*RBinaryOp*`"]]
built-in:_60_-["`Built-In:
#60;#45;`"]
style built-in:_60_- stroke:gray,fill:gray,stroke-width:2px,opacity:.8;
13{{"`**2** (L. 2)
*RNumber*`"}}
12["`**#96;y x#96;** (L. 2)
*RSymbol*`"]
14[["`base#58;#58;**#60;#45;** (L. 2)
*RBinaryOp*`"]]
16(["`**#96;y x#96;** (L. 3)
*RSymbol*`"])
18{{"`**3** (L. 3)
*RNumber*`"}}
20[["`**f** (L. 3)
*RFunctionCall*`"]]
21[["`base#58;#58;**#43;** (L. 3)
*RBinaryOp*`"]]
built-in:_43_["`Built-In:
#43;`"]
style built-in:_43_ stroke:gray,fill:gray,stroke-width:2px,opacity:.8;
23[["`base#58;#58;**print** (L. 3)
*RFunctionCall*`"]]
style 23 stroke:red,stroke-width:5px;
built-in:print["`Built-In:
print`"]
style built-in:print stroke:gray,fill:gray,stroke-width:2px,opacity:.8;
1 -->|"def-by-on-call"| 18
8-get-name -->|"reads"| 1
8 -->|"reads, returns, arg"| 8-get-name
8 -.->|"reads, calls"| built-in:get
linkStyle 3 stroke:gray;
9 -->|"returns, arg"| 8
9 -.->|"reads, calls"| built-in:_123_
linkStyle 5 stroke:gray;
10 -.-|function| flow-10
0 -->|"defined-by, flow"| 11
0 -->|"defined-by"| 10
11 -->|"reads, arg"| 10
11 -->|"returns, arg"| 0
11 -.->|"reads, calls"| built-in:_60_-
linkStyle 11 stroke:gray;
12 -->|"defined-by, flow"| 14
12 -->|"defined-by"| 13
14 -->|"reads, arg"| 13
14 -->|"returns, arg"| 12
14 -.->|"reads, calls"| built-in:_60_-
linkStyle 16 stroke:gray;
16 -->|"reads"| 12
18 -->|"def-on-call"| 1
20 -->|"reads, arg"| 18
20 -->|"reads"| 0
20 -->|"returns"| 8
20 -->|"calls"| 10
21 -->|"reads, arg"| 16
21 -->|"reads, arg"| 20
21 -.->|"reads, calls"| built-in:_43_
linkStyle 25 stroke:gray;
23 -->|"reads, returns, arg"| 21
23 -.->|"reads, calls"| built-in:print
linkStyle 27 stroke:gray;R Code of the (simplified) Dataflow Graph
The analysis ran (parse and normalize with the tree-sitter engine) in the generation environment. No signature database is mounted, so library() attaches no exports (base-R names are still qualified, e.g., stats::acf). We encountered unknown side effects (with ids: 23 (linked)) during the analysis.
"f" <- function(x) { get("x") }
`y x` <- 2
print(`y x` + f(3))A name is written plainly, as a string, in backticks, or as the argument of a call that reads it as one.
5 children500
A name written as R's grammar allows it, like
aorplot. Every other form ends up as one of these, and a lookup links it to the definitions that may reach it.A name given as a string, like
"a"or'plot'. Such a name may hold spaces, but R accepts the quotes only where it is defined, so reading it back needs backticks or a name-creating call.A name escaped in backticks, like
`a`or`my var`. It binds and resolves like a normal name, the syntax only lifts the grammar's restriction on the characters it may hold.A call that reads a string argument as the name it spells, such as
get,get0,mget,exists,match.fun, andassign. The string counts as a use of that name, or forassignas a definition. The string may also reach the call through variables and the parameters of the functions it is in (f <- function(n) get(n)(); f("h")), and the value of such a lookup is what it finds for the call it runs in, sof("a"); f("b")tells both apart. Without an environment, the name is looked up in the frame of the function and those of the functions it is defined in, then in the global names at the call. A name only known at runtime (get(Sys.getenv("V"))) is not linked.A created name whose string argument is not a literal but folds to one. Covers string literals, variables holding one, and
paste0/paste/file.pathover constants, each followed like a written-out name.
Which definitions a name may reach, through the scopes around it, the environments a program builds, and the packages below them.
25 children1681
Resolve a name against the bindings of the global environment, whenever no enclosing function definition binds it. Every top-level definition that may still be active is linked, so a use after an
ifreaches both branches' definitions.Resolve a name to the innermost enclosing definition, and only to the global scope when no enclosing one binds it. Where a function is written decides what its body sees, not where it is called.
Two closures from the same factory keep independent state in R, as every call of the factory makes an environment of its own. We keep one environment per definition instead, so a
<<-in one closure is read as reaching the binding the other captured.A function captures the environment it was defined in, not the values that were in it. So it sees a write made to that environment after its definition, a sibling closure's
<<-included. A closure a function returns without calling it reads its free variables where it is called, so the call making it does not read them.An environment a program builds and names itself, rather than one a definition opens. Covers
new.env,assign/get/localwithenvir=,e$x,attach,with, and aliasing such an environment through a variable. A write from within a function reaches the environment it names, a read of the whole environment (as.list(e)) sees every field written, and copies of an environment variable (g <- e) share its fields.8 children440
Track environment assignments and reads across branches and loop bodies, a key built at run time such as
paste0("k", i)included.Specifying a parent for a newly-created environment from a dynamic or unknown expression (
new.env(parent = f())). Such a parent falls back to the default (parent.frame()) instead of resolving.Specifying a parent for a newly-created environment from a tracked environment variable or a constant (
new.env(parent = e),new.env(parent = emptyenv())).Aliasing a tracked environment variable (
alias <- e), which binds a second name to the same environment rather than a copy. A write to the original made after the alias is not reflected through it, unlike a read.Reading through an aliased tracked environment variable (
alias <- e). Every assign made up to the alias is visible through it.Evaluating an expression inside a named environment with
with(data, expr). Reads of names the tracked env defines resolve, including through a computeddataargument or a nested call.Reading via
parent.frame()$name, and a write escaping througheval.parent(quote(name <- value)). Works only one call away, and storing the frame first drops the binding.Support for
rm(list=..., envir=sys.frame(N))removing variables from a specific call frame. Negative and zero offsets are handled from within depth-1 functions. A removal from the global environment inside a function, withenvir = .GlobalEnv,envir = globalenv(), orpos = 1, is seen by the callers, like a<<-would be. A removal from the environment of a closure,rm("v", envir = environment(g)), is seen by the calls of that closure, which then read what the environments around it bind.
Handling side-effects through environments, which act as reference types and are not copied when modified. A write through a parameter (
env$f <- v,assign("f", v, envir = env)) is replayed at each call that hands over a known environment, a list is copied by R so its write stays in the function, and any other value keeps the write as an unknown side effect of the call (see Environment Alias and Side-Effects in Function Call). A call that only writes into the environment it hands over reads nothing of it, so storing a function there does not call what an earlier call stored. The value of a field (e$x,e[["x"]]) is what the environment binds it to, as long as nothing but tracked lookups, writes, and functions of the program get the environment; a read in a loop, a write into part of a field, and anything unknown writing leave it unknown.Resolve a name in call position only against function definitions, a name in value position against every binding. This is why the
c <- 1below does not shadow the call toc.Resolve a name against the packages the search path holds, whenever no scope in the script binds it. Attached packages sit below
.GlobalEnv, so a global binding shadows an export.Programmatically inspecting or mutating the search path with
search(),searchpaths(), or detaching by position.search()andsearchpaths()read everylibrary,attach, anddetachbefore them, anddetach/unloadNamespaceremove the entry they name, butsearch()itself is not computed as a value.Separate what a package's namespace holds from what attaching it puts on the search path. The imports environment a package carries for itself is not modelled.
Resolve a name written
pkg::nameto the export it names, no matter what the script binds. This depends on what the signature database knows, and an unresolved name stays unresolved rather than reported.Resolve a name written
pkg:::name, which reaches what the namespace keeps to itself. Whether the name is genuinely internal rather than exported is not checked (see Namespace Exports).Know which names a package's namespace declares exported versus keeps internal. The
namespace-accessrule checks a::/:::choice against what the signature database records, which omits most internal names to stay small.Attach a package named by
library,require,attachNamespace, ... to the search path. From that point on an unbound name may resolve to one of its exports, while a binding in the script still shadows it.Undo a library attach with
detach,unloadNamespace, ... Neither is modelled, so a name used afterdetachstill resolves to the package's export.Open a scope of its own with a plain
localblock. A name it binds is invisible outside, while a super assignment from within reaches the enclosing scope.Send
local's body to a specific environment. Its writes land in that environment, also inside a function that binds the same name itself.Resolve a call that does not name the function it runs, as
Recalldoes. It is linked to the enclosing function definition, the same way a call through the function's own name is.
Expressions
52 fully22 partially1 not
The calls, Operators, control flow, and function definitions an R program is built from.
- #Calls929 tests486 dataflow, 464 slice, 103 output and 3 more
signature tests
higher-order sum origin, a plot after the call, a loop body still calls
Everything that is a call in R, from
f(x)over an operator to a group, with the arguments it binds and the side effects it may have.16 children1150
Read
(and{as the calls they are, mapped to their primitive implementations. A group hands on the value of its last expression, sox <- { 1; 2 }bindsxto2, and redefining`{`replaces that meaning.Link a call like
f(x)orfoo::bar(x, y)to the definitions its name may resolve to, and its arguments to the formals they bind. Every definition that may be active is linked, which over-approximates rather than picking one.7 children520
An argument given without a name, like
f(3), which binds to a formal by position. It is matched after every named argument has taken its formal.- #Empty Arguments99 tests29 slice, 27 output, 2 dataflow and 1 more
signature tests
switch falls through an empty case, Not giving first parameter, get in function
An argument left out entirely, as the second one in
foo::bar(3, ,42). It still takes a position, which is what makesm[1, ]an index access on one dimension. An argument given with a name, like
f(x = 3), which binds to the formal of that name. Inside a call=names an argument rather than binding a name.An argument whose name is written as a string, like
f('x' = 3). It binds the same formal a named argument would, the way a quoted name binds an ordinary one.Match every argument of a call to the formal it binds, exact names first, then unique prefixes (
pmatch), then position. A formal behind...matches only exactly, and an ambiguous prefix binds nothing.An argument that binds a name while it is evaluated, as
f(x <- 3)does. Whether the argument is ever forced is not modelled (see promises), sof <- function(a) 1; f(x <- 3)still believesxis 3. A call in the argument depends on the call it is handed to, sog(FALSE, f(4))keepsg.- #Side-Effects in Function Call150 tests141 slice, 118 dataflow, 24 output
signature tests
a plot after the call, a nested call defined later, a closure handed down twice
A call that binds a name outside itself, as
setXTo(3)does with a super assignment. Such a write reaches the caller through several call levels, but one through a shared environment is missed.
A call to
finside the definition off. The name resolves to the definition it sits in, so the call links back to it rather than staying unresolved.A call whose target is an expression rather than a name, like
(function(x) x)(3)orfactory(0)(). It links to the definition that expression yields, without a name being involved.A function handled as a value: handed to another function, like the callback of
lapply,Reduce, orpurrr::map, or picked at runtime withswitch,match.fun, ordo.call. A function value is followed through the dataflow wherever it goes, also as a string that names it, so the call that runs it is linked to its definition and sees its reads and side effects.Reducehands its running result to the callback and returns what the callback returns. A function whose name is only known at runtime, likeget(paste0("f", i)), is not linked.An operator written between its arguments, like
x + yorx %>% f(y). It is the same call as`+`(x, y), which is why redefining the operator changes what it does.A name that R defines being bound in the program, as in
print <- function(x) xor`for` <- function(a,b,c) a. The redefinition wins wherever the built-in would have been used, respecting scope and order. Only::/:::are exempt.A call that changes state the rest of the program reads without naming it, as
setwdchanges where a path points. The working directory, the options, and the graphics state are tracked, so a read links to the calls that set what it reads. Other ambient state, like environment variables or the random seed, stays an unknown side effect. A call inside a function that acts on the global environment, likesource,library, orrm(envir = .GlobalEnv), affects the callers of that function as well.- #Working Directory1067 tests301 output, 301 slice, 15 dataflow and 1 more
signature tests
setwd after point, called function propagates, getwd reads what setwd set
Track the effective working directory across
setwd, control-flow- and location-sensitive. Interprocedural, sourced, and loop cases are treated as unbounded.
The bracket, double-bracket, dollar, and slot forms for picking an element out of a container, with names, empty positions, and multiple indices.
7 children700
The
[call, as inx[i],x[i, ,b], orx[3][y]. The container is read as a whole without separating the cell an index picks, and writing through it is a replacement call.The
[[call, as inx[[i]]orx[[i, b]]. Read like single-bracket access by the dataflow. The value solver picks the element a constant name (matched exactly) or position names out of a list it knows, andNULLfor a name the list does not have.The
$call, as inx$y,x$"y", orx$y$z, whose right side is a name rather than a value. On a list$matches that name partially, sol$alreaches an element namedalpha. The value solver knows the listslist(...)builds and the replacements change, with the same matching, while a field of an environment stays unknown to it.The
@call, which reads a slot of an S4 object. The slot name is read like any other access, without checking it against the classsetClassdeclared.An index given with a name, as in
x[i = 3]orx[[i=]]. It is matched like any other named argument of the underlying[call.An index left out, as in
x[]orx[2,,42]. The gap is an empty argument and still counts as a position.An index that is a vector or a condition, as in
x[i > 3]orx[c(1,3)]. The index expression is read, but which elements it selects stays unknown, as vectorized operations are not evaluated.
R's unary, binary, and special operators, the model formula, and every way a name can be bound to a value.
15 children1131
An operator written before its single argument, like
+3or-3. It is a call of the operator's name with one argument.An operator written between its two arguments, like
3 + 4or3 * 4. It is a call of the operator's name with both arguments, in the order R gives them.13 children931
An operator whose name is spelled between percent signs, like
3 %in% 4or3 %*% 4. The name binds like any other, so a program may define%between%itself and the infix call resolves to it.A formula written with
~, likey ~ xory ~ x + z. Its operands are non-standard evaluation: a barey ~ xnames columns rather than variables, so neither name is read.Every way to bind a name to a value, with the local and super operators, the calls doing the same, or a write through a replacement function.
11 children821
Bind a name in the current scope with
<-, as inx <- 3. A target that is an access (x$y <- 3) rebinds the whole container through a replacement function.Bind a name with
->, as in3 -> x. Identical to left assignment with the sides swapped.Bind a name with
=, as inx = 3. This holds only at the start of an expression, as inside a call the same token gives a named argument (f(a = 3)).Bind a column in place with data.table's
:=, as inx[,a:=3,]. It is read as a write to the table, not to a name, and which columns exist stays unknown (see data masking).Bind a name in an enclosing scope with
<<-, as inx <<- 42. The write leaves the function it sits in, which is what makes it a side effect of the call.- #Super Right Assignment6 tests4 dataflow, 2 desugar
signature tests
global define with ->> in function, read after, Manual Max Function
Bind a name in an enclosing scope with
->>, as in42 ->> x. Identical to super left assignment with the sides swapped. An assignment is itself an expression that hands on the value it bound. This is what makes
x <- y <- 3bind both names andprint(x <- 4)print4.Bind a name given as a string, with
assign,delayedAssign, ... The name has to be a resolvable string. A delayed expression runs where the name is first read, so it sees the bindings at that point and its own writes happen there, in theassign.envgiven.assign("x", 3) delayedAssign("y", x * 2) x <- 5 yWrite to a range of a container, as in
x[1:3] <- 3. Like every replacement call it rebinds the whole container, as which cells the range covers is not tracked.Read a write to a call, like
x[i] <- 3orx$y <- 3, as the`[<-`(x, 3)R runs for it. The name in front is both read and rebound, under the conditions the write stands under, so a later read reaches the old binding only through the replacement. The value solver applies$<-and[[<-with a constant name or position to a list it knows (l$a$b <- 1included,NULLremoving an element), any other replacement leaves the value unknown. A named argument in such a call (g(v, k = 2) <- 3) leaves its argument edge dangling.A binding
lockBinding(orlockEnvironment) makes constant. Neither is recognized as a built-in, so the assignment R would have rejected is read as an ordinary rebinding.
Conditionals, the three loop forms, their jumps, and how an error leaves a computation.
9 children810
The conditional
if (x) y else z, andif (x) ywithout an alternative. Both branches stay possible, so a name bound in either reaches a use below, and a missingelseyieldsNULL.- #for loop926 tests291 slice, 266 output, 65 dataflow and 1 more
signature tests
a loop body still calls, a while loop counting down, the counter of a while loop
The loop
for (i in 1:3) print(i), which binds its variable in the surrounding scope. The body is analyzed once for any number of iterations, so a name it binds also reaches its own next read and the reads of the functions the body calls, unless the body surely binds it anew before the call, andistays bound after the loop. Value resolution tells the runs over a known sequence apart. The loop
while (x) b, whose condition is read before every iteration. Whether the body ever runs is unknown to the dataflow graph, so a name it binds and the one bound before it both reach a use below. Value resolution counts the runs when it can tell the condition of each one (i <- 0; while(i < 3) i <- i + 1) and resolves the value of the last one.The loop
repeat {b; if (x) break}, which only abreakor areturnleaves. Like every loop the body is analyzed once, for any number of iterations. Value resolution counts the runs of a body that ends with its onlyif (x) breakwhen it can tellxin each one.Leave the innermost enclosing loop with
break(break()included). The rest of the body becomes unreachable on that path, so what it would have bound does not reach a use after the loop.Start the next iteration of the innermost enclosing loop with
next(next()included). The rest of the body is skipped on that path, while the loop itself continues.Pick a branch by name or position with
switch. A branch left empty falls through to the next one, and every branch stays possible, as the selector does not narrow which of them runs. The call returns the branch it picks, so a function chosen byswitchis linked to the call that runs it.- #return152 tests66 slice, 30 output, 5 dataflow
signature tests
return parameter named, read of parameter in return, an early return
Leave the enclosing function with
return(3). Its argument becomes a value of the call, next to the implicit return of whatever path does not return early. A condition raised with
stop/warningand caught withtry/tryCatch/withCallingHandlers. The path on which a call throws before a write stays open, so aftertryCatch({ risky(); x <- 2 }, ...)the variablexmay still hold its earlier value. A handler only runs when the block may signal a condition of its class. Anything that may signal counts (an operator, a call of a built-in or of a function we cannot resolve, a variable that may force a promise), onlystop,warning, andmessagehanded constants are matched by the class they signal, sotryCatch(message("m"), warning = h)never runsh. A calling handler is linked to everywarning,message, orsignalConditionthat may reach it, also in the functions the block calls. This holds for a handler written inline as well as for one given as a value (a variable, a list element, the result of a factory, or a parameter), which is resolved like any function value.
The parameters a function binds, their defaults,
..., promises, and the value the call hands back.7 children610
- #Normal4573 tests1362 slice, 1267 output, 41 dataflow and 1 more
signature tests
call on.exit at the end, the shape of a data frame, with add, on.exit's append
A function written with
function(x) x. It opens a scope of its own, so its body resolves names in its lexicographic scope rather than where the call happens. The parameters a definition declares, with their names, defaults,
..., and the promise each is bound to.4 children310
A parameter written as a name, as
xinfunction(x) x. It binds that name in the body, where it shadows an enclosing binding of the same name.A parameter with a default, as in
function(x = 3) x. The default is evaluated in the function's own scope, so it may read another parameter (function(x, y = x * 2)).The variadic parameter
..., which collects every argument no other formal took. Passing it on keeps those arguments together, and a call handing it on binds them, with their names, as each known call of the function around hands them over...2and...elt(2)read only the second of them, and an argument no read sees once handed on is never forced, while a formal behind it is only matched by its exact name.An argument is a promise, evaluated where it was written but only when the body first reads it. When that happens is not modelled, nor are the writes that forcing it performs, so
function(x = y) { y <- 3; x }does not tell us whereyis read.
A function hands back the value of the last expression it evaluates, without a
return. Every path's last expression is such a value of the call.The short form
\(x) xof a function definition. It is purely syntax and binds its parameters and scopes its body exactly like the long form.
The base-R functions we give a meaning of their own rather than treating as opaque calls, the ones that compute on the language included.
14 children2120
The operators
&&and||, which evaluate their right side only if the left one does not decide the result. The right side is read as a path that may not run, unlike the vectorized&/|.The native pipe
|>, which is syntax rather than a call: the parser rewrites it. The left-hand side becomes the first argument or fills the_placeholder.The experimental pipe-bind
=>, which names what the pipe hands on and which R only enables under_R_USE_PIPEBIND_. It is off by default and needsengine.r-shell.pipeBind. Tree-sitter's grammar has no production for it.Give
:,seq, ... the sequence they produce, gathered by abstract interpretation. That value lets us reason about a loop bound or an index, butseq,seq_len,seq_along, andrepare not folded even for literal arguments.The
.Internaland.Primitivecalls a base function reaches its C implementation through. The call is kept and its arguments read, but the name.Primitive("sum")spells is not resolved the way a created name would be.The global options
optionssets andgetOption,.Options, printing, and formatting read. Each option is tracked by name, sogetOption("digits")reaches the lastoptions(digits = 3),printandcatreach the printing options, and an unrelated option usually stays out of a slice.withr::with_optionsandlocal_optionsscope a change, andevalof code that sets options counts as setting them. Where the lastoptions(...)surely setting an option names it as an argument,getOptionhands back that value, sooptions(cb = function() 4); getOption("cb")()calls the function. Other values are not interpreted, anoptions(op)restoring an unknown list may set any option, and options that change every conversion (scipen,OutDec) stay an unknown side effect. A function flowR cannot see into may read any option.The graphics parameters
parandlayoutset for the active device, thepaletteall devices share, and the devices opened and closed. A plot reads the state of the device it draws on, so it links to theparcalls before it, a new device starts afresh, anddev.off()returns to the state of the device that becomes active.withr::with_parand thewith_<device>functions scope a change. Within a function the open devices are unknown, so a device call only adds to what may hold, and grid-based plots like ggplot2 ignore the parameters.The help calls
?,??,help, ...?and??are recognized, but their topic is read as an ordinary name where R only looks it up, andhelp/help.searchare not known at all.Code that reads or builds code, by quoting an expression, evaluating it elsewhere, parsing it from a string, or rewriting a function.
6 children060
Read a part of a function with
body,formals,args, orenvironment. What comes back is opaque, and the whole function is read, so asking only forformals(f)keeps all off's body in a slice.Rewrite a part of a function with
body<-,formals<-, orenvironment<-. Like every replacement function it rebinds the name, and a later call reaches the original function through that rebinding.Take an expression as a value with
quote,substitute,bquote, ... A quoted argument is non-standard evaluation: the names in it are not read, andsubstitutedoes not reach the caller's expression.Run an expression that is a value with
eval,evalq,eval.parent, ... The captured code is linked where it is evaluated, also through a name bound once toquote,as.name,str2lang, orparse, buteval(expr, envir)with an environment we do not know stays an unknown side effect. A constructed call, likeeval(call("f", x))oreval(as.call(list(as.name("f"), x))), runs like ado.callof that function, also when the call is bound to a name first.A string that reads the names inside it, as
glue::glue("{x}"),cli::cli_alert_info("{.val {x}}"), orstringr::str_gluedo. Those names resolve in the scope of the template, and one aimed at another scope (.envir,.con,glue_data) becomes an unknown side effect.x <- 2 glue::glue("x plus one is {x + 1}")- #Parsing81 tests24 output, 24 slice, 2 dataflow
signature tests
eval of parsed text, simple eval use - from 2 variables, simple eval use
Turn text into an expression with
parseorstr2lang, and an expression back into text withdeparse. Whatparseproduces is only reached through evaluation, anddeparseis not modelled beyond reading its argument.
The values written into the source itself, which is what a resolved name and an S3 class are read from.
7 children700
Recognize numbers like
3,3.14, the integer3L, hexadecimals such as0xFFand0x1p3, as well as the typed missingsNA_integer_/NA_real_, ...A string literal like
"a"or'b'. Its value is kept, which is what lets it stand for a name or fold into a resolved one.1 child100
A raw string like
r"(a)", in which a backslash is no escape. Its value is the same string the quoted form would give.
Recognize the logicals
TRUEandFALSE, ... Their short formsTandFare ordinary bindings and can be reassigned, whileTRUEandFALSEare reserved.The
NULLobject, R's empty value. It is what anifwithout an else branch yields, and assigning it to a list element removes that element.The constants
InfandNaN, which arithmetic produces rather than errors on. They are ordinary values here, and the predicates asking for them are not evaluated, as no type is inferred.
Non-Standard Evaluations/Semantics
1 fully5 partially1 not
The places where R does not evaluate an expression the way it is written, so a name in it may mean something other than the binding it seems to read.
A call that evaluates its arguments against the columns of a data frame first, as
subset(d, col > 1), the dplyr verbs,ggplot2::aes, and data.table's:=do. Which columns exist is unknown to us, so a column shadowing a variable still resolves to the variable.A shorter operand is repeated to the length of the longer one. This is not modelled, so the operands are read but the length of what an operator produces is not derived from them.
An operation applied to every element of a vector at once. Comparisons, the reducing functions, and
ifelsewiden to the unknown value, so an index built from one selects an unknown set of elements.Code registered to run at a point the program does not write out, with
on.exitoruserhooks. Anon.exitbody runs when the function it sits in ends, in the orderaddandaftergive, and a hook registered in a branch or loop only maybe.setHookis not modelled.Which operator binds tighter, as the documentation lays it out. It decides the shape of the tree the parser hands us, so
-2^2is-(2^2)before anything else looks at it.The data an object carries beside its value, which is where its S3 class and its shape live.
2 children020
An attribute a program sets itself, with
attrorattributes. Which attributes an object carries is not part of the value we track, so only the rebinding of the object itself is seen.An attribute R gives a meaning of its own, such as
dim,names, or theclassthat dispatch reads.dim<-,names<-, andclass<-track shape only on a data frame.
Object-Oriented Programming
2 fully10 partially0 not
R's object systems, the class an object carries, and the dispatch that decides which function body a generic call runs.
Classes and methods built on the
classattribute andUseMethoddispatch. A class is a string an object carries rather than a declaration, so what a call runs follows from the namesgeneric.classin scope.3 children120
Give an object its class with
structure(..., class =),class<-, oroldClass<-. A class written as a string literal is tracked and reaches the dispatch that follows it.Route a generic call to the method that runs.
UseMethodand a built-in generic likelengthortoStringlink to the definitions namedgeneric.classin scope for the class the dispatched object may have, thengeneric.default. The class is followed through the value flow to where it is set: astructure(..., class =), aclass<-oroldClass<-, a constructor function returning such an object, the implicit class of a constant, and the arguments bound to the generic at every call. A dispatch on what the calls of its function hand it is decided per call as well, through up to three calls (a generic called by a function called by another), so a slice for one call keeps only the methods that call may run, as long as each function is only ever called by name.UseMethod("f", y)dispatches ony, a replacementlab(a) <- von the classahad before, both through alab<-of the program and through a built-in likenames<-or[<-, and printing a visible top-level value calls only theprintmethods of the classes it may have, so a slice for that value keeps what they print. A replacement within a function is decided per call of that function as well. Where any class is not known, everygeneric.classmethod stays a target. A group generic likeOps.foo,Math.foo, orSummary.foois reached from the operators and functions of its group, soa == bdispatches toOps.foofor an object of classfoo, narrowed by the classes of both operands.Walk the class vector with
NextMethod. Where the class of the dispatched object is known, it reaches the methods of the classes after that of its own method in the class vector, then the default, and otherwise every method of the generic. A method run for several class vectors decides itsNextMethodper call of the generic like the dispatch itself. It hands on the current values of the formals. The generic and objectNextMethod("f", x)names change nothing within a dispatch, while in a method called directly it dispatches the generic it names on the class of the first argument.
Formal classes and methods declared with
setClass,setGeneric, andsetMethod. Unlike S3 the class is declared, with named slots read through@.3 children120
Declare a class with
setClassand build one withnew. Thenewcall is linked to thesetClassthat declared the class and calls the validity functions of the class and its superclasses and theinitializemethods R selects for a new object of the class (with those they reach throughcallNextMethod), and a slot is read through@like any other access.Route a generic call to the method
setMethod(orsetReplaceMethod, for the<-generic) registered.standardGenericevaluates the arguments it dispatches on, and the call reaches the methods R may select for their classes: the classnewcreates or the implicit class of a constant, followed through the value flow like in S3 dispatch. Selection follows R's distance throughcontains, class unions andsetIs, withANYfarthest andmissingfor an argument not given, over every argument of the signature, and keeps the methods tied for the closest. A method registered after a top-level call is not reached by it, and asetMethoddepends only on thesetGenericand on an earlier method of the same signature it may replace, not on the methods of other signatures. A method set again for the same signature, unconditionally and before every run of the call, is replaced and not reached. A call within a function is decided per call of that function as well, through up to three calls. An S3 object dispatches on its first class ifsetOldClassregistered that class (each class of the vector extending the next) once before the methods were set, as R keeps the method it picked for a class until a method is set. Where a class is not known (another S3 object, a computed signature), every method stays a target, as no type is inferred beyond that.Reach a parent method with
callNextMethod, and inherit throughcontains.callNextMethodreaches the method R would select for the signature the calling method is defined for if that method did not exist, which follows the superclasses incontainsnearest first. A chain ofcallNextMethodcalls is followed method by method, whatever the class of the object, and only a computed signature falls back to every method of the generic.
Reference classes made with
setRefClass, whose objects are mutable and thus behave like the shared environments they are built on. A method call on an instance is linked to the method it runs, and.selfis bound to that instance, so a method changing a field changes the object it was called on. The instance is found like for R6, and.selfnever resolves to a variable of that name outside the object. Typing is not supported.Classes made with
R6::R6Class, read as one unit rather than as the environment behind them. Like RC an R6 object is mutable. A method call is linked to the method it runs, also on a fresh$new()and through method chains, andselfandprivateare bound to the instance the method runs on, so a method changingself$vchanges that object. The instance is found through the value flow of the receiver: a variable and every alias of it, a parameter bound to the instance at each call, what a called function returns, and a list element or field of another object holding it. As R encloses the methods by the object,selfandprivatenever resolve to variables of those names outside it. Typing, inheritance, active bindings, and handling objects fully "as units" are not supported.Classes made with
S7::new_class, read as one unit, dispatch and inheritance included. Typing is not supported, nor are objects handled fully "as units."Attribute the use of a class to the package that owns it, so a class use implies a dependency for library detection and version guessing. It is the class counterpart of resolving a name to its package.
2 children020
Attribute an S3 class to the package that owns it. Only a class written as a string literal is attributed, not one from a variable or a
c(...)vector.Attribute an S4 class to the package that owns it. S4 ownership is not in the signature database, so a bare class use is not attributed.
R File Structure
5 fully4 partially0 not
The lexical shape of a source file, down to its line endings and encoding.
- #Comments14 tests8 desugar, 6 slice, 1 output
signature tests
for-loop with comment, With comments in parameter, simple line comment
A comment like
# this is a comment, a shebang line included. It is never read as code, so no name in it resolves. Recognize
#line n "file"as its own node (r-shell only). It is parsed but never retargets a location, and tree-sitter reads it as a comment.A semicolon separating two expressions on one line, as in
a; b; c. It says the same as a newline: the expressions run in the order they are written.Recognize and resolve newlines like
a\nb\nc, where a newline ends an expression unless it is still incomplete. A trailing operator or an unclosed bracket continues on the next line.Recognize
\n(Unix),\r\n(Windows), and a lone\r(classic Mac). Normalized at the r-bridge boundary, with both engines.UTF-8 beyond ASCII in string literals, in a comment, and in names (plain as well as backtick-escaped). Such names bind and resolve like any other, with both engines.
Recognize a UTF-8 byte-order mark at the start of a file. The tree-sitter engine reads past it, the r-shell engine rejects the same input as unparsable.
Handle source that does not parse. The
syntactically-validrule locates the region and offers a fix. The strict parser rejects the file, while tree-sitter's lax mode (off by default) drops the region.Reject a syntactic keyword like
`if`or`function`where R's grammar requires an expression. The r-shell engine rejectsif <- 5, while tree-sitter's grammar parses it as an ordinary assignment.
Project
10 fully5 partially0 not
The files around the R code, from the package metadata and the packages a version manager pins to what runs before a script does.
The files that describe the package itself, what it needs, and what it offers.
5 children410
Read a package's
DESCRIPTION. Its DCF records give the package name, version, R version, dependency fields, andCollateorder.Read a package's
NAMESPACE. Acts onimport/importFrom,importClassesFrom/importMethodsFromfor S4, andexport/S3method, which is what says whether a name is exported or internal.Read the
.Rdpages underman/, their macros, and the indices beside them. A documented name is tied back to the page that documents it.Read a package's
NEWS/NEWS.md. We parse the versions it announces and what each changed, which is what a version guess is checked against.Read the
R/sysdata.rdaa package keeps its internal data in, and thedata/files it exports. What those bindings hold is not reconstructed.
The lockfiles and manifests a version manager pins a project's packages with.
7 children520
Read the configuration of renv, the most widespread R project-library manager. The library it points at is neither installed nor restored.
Read the configuration of packrat, the predecessor of renv. The
packrat/liblibrary beside it is not loaded.Read the configuration of rv, a declarative project manager in the style of cargo. Parses
rproject.tomland the resolvedrv.lock.Read the configuration of uvr, an R project manager modelled on uv. Parses
uvr.tomland theuvr.lockbeside it.Read the configuration of rig projects and scripts. Parses
rproj.toml, the inline# /// scriptblock a script declares its dependencies in, and the resolvedrproj.lockor<script>.R.lock.Read the package library a project installs into (
renv/library,packrat/lib, or the platform library). Their code is not read.Fetch the source of a package no signature database knows when the code uses it, from CRAN, Bioconductor, or GitHub (
solver.sigdb.onDemand). The repository comes from aninstall_github-style call, theRemotesof theDESCRIPTION, or the configuration. The exports come from theNAMESPACE(anexportPatternmatched against the names the code defines) and the formals from the functions theR/files define at the top level, cached per package so a later analysis works offline. A fetched package counts as untrusted: its code is parsed, never run, onlyhttpsto public hosts (or the configured mirrors) is used, what it states is cut down to names, formals, and a version, and it cannot take over a function that matters to security (system,eval, file and network calls), so those keep being found. Only a trusted configuration can switch this on. Function bodies are not analyzed, and a package loaded under a name only known at run time is not fetched.
What runs before a script does and what counts as part of the project at all.
2 children110
Read the files R runs or reads before a script (
.Rprofile,Rprofile.site,.Renviron,Renviron.site). An.Rprofileis read as R code whose bindings precede the script, but variables set by an environment file are not interpreted.Read the
.gitignoreand.Rbuildignorethat say which files are not part of the project. Follows gitignore globs and the regular expressionsR CMD builduses.
The tooling around R code rather than the R in it, such as roxygen2 blocks and woven documents.
1 child010
Handle the roxygen2 blocks that precede a definition. They are read as a comment and what a tag states does not reach how a name resolves, so an
importFromtag leaves the name below it unqualified.
System, I/O, FFI, and Other Files
0 fully10 partially0 not
Everything a program reaches for beyond its own code, from files it sources to calls it makes out of R.
Pull another file's code into the analysis with
source,sys.source, ... A sourced file's top-level bindings become visible to the code below the call. Inside a function,sourceevaluates the file in the global environment unless it is called withlocal = TRUE, as R does, andsys.source(which defaults to the base environment) is approximated the same way. Only a path that resolves is followed.source("helpers.R") sys.source("setup.R", envir = environment())Files a program writes objects to and reads them back from, with
save,load,readRDS, ... Aloadbinds names we cannot know, so neither the names nor the values behind them are reconstructed.Reading and writing data files with
read.csv,write.csv, ... What a file contains does not enter the analysis, which is also why the columns of a data frame stay unknown.A file read after a call that wrote it, also when the call copies, moves, packs, or unpacks it. A read of a file links to the earlier writes of a file of the same name, and calls like
file.copy(from, to)orunzip(zipfile, exdir)read the files of some arguments and write those of others, soread.csv("data/b.csv")afterunzip("a.zip", exdir = "data")keeps theunzipand whatever wrotea.zip. What an archive holds is unknown, so unpacking one counts as writing any file, and a read of a file nothing named before reads every such write. Paths are compared by name, not against the working directory.download.file("https://example.org/a.zip", "a.zip") unzip("a.zip", exdir = "data") d <- read.csv("data/b.csv")Calling out to compiled code with
.C,.Call,.External,.Fortran, ... The call carries an unknown side effect, like a write through a shared environment, and the foreign code itself is not analyzed.Handle
system,system.*, ... An injectable command built from user input is flagged by theproblematic-inputsandunescaped-argumentsrules.Support R-Markdown files as R sources. Code chunks are extracted, but not inline
r expror theparamsof the YAML front matter.Support Jupyter Notebooks as R sources. Cells are read in document order, not execution order, and the kernel is not checked.
Support Quarto files as R sources. Code chunks are extracted, but not inline
r expror theparamsof the YAML front matter.Support for Sweave files as R sources. Code chunks are extracted,
\Sexpr{}inline expressions are not.
Types
0 fully1 partially3 not
What a value is, how we infer it from the code, and the coercions R applies between types.
The atomic types
numeric,character,logical, ...typeof,class, andmodeare not evaluated and resolve to the unknown top value, so not even a literal yields its type.The composite types a list, a data frame, or an object has. None of them is tracked, so
class(list(1, 2))resolves to the unknown top value.Derive the type of an expression from the code that produces it. A type predicate never narrows a branch, so
if(is.numeric(1)) y <- 1 else y <- "a"still leavesyas both alternatives.The type R silently converts a value to, as
c(1, "a")orTRUE + 1do. A vector is not unified, soc(1, "a")keeps a number beside a string, and theas.*converters are not evaluated.