moodymudskipper / boomer Goto Github PK
View Code? Open in Web Editor NEWDebugging Tools to Inspect the Intermediate Steps of a Call
Home Page: https://moodymudskipper.github.io/boomer/
Debugging Tools to Inspect the Intermediate Steps of a Call
Home Page: https://moodymudskipper.github.io/boomer/
Found out about this package today, very cool concept. I saw in the readme that it includes an addin, so you might want to add it to https://github.com/daattali/addinslist . Feel free to close the issue if not!
so things like pipe chains and {{ arg }}
are displayed nicely
From: #40
We might signal it when we override a variable accessible in a parent env, i.e. signal if exists("var") && !exists("var", exec_env)
atm we wait for the result to be computed and print both, in case of long calls it would be nice to see which one is being executed.
Using View is a hack but would still be helpful,
We'd call boom(subset(head(mtcars, 1 + 1), qsec > 17), view = TRUE)
and have this happen, simulated here by :
boomed <- list("subset(head(mtcars, 1 + 1), qsec > 17)" = list(
" " = subset(head(mtcars, 1 + 1), qsec > 17),
"head(mtcars, 1 + 1)" = list(
" " = head(mtcars, 1 + 1),
"1 + 1" = 1 + 1
),
"qsec > 17" = c(FALSE, TRUE)
))
nm <- names(boomed)
assign(nm, boomed[[1]])
eval(substitute(View(OBJ), list(OBJ = as.symbol(nm))))
This is a bit akward, but technically we can have a nice webpage or shiny app that would do the same.
Instead (or on top) of printing, we'd store results in a nested list, and offer a nice interface to explore it.
I can imagine something with tabs as in {covr}, so if we rig_in_namespace
several functions each call would get a tab, so we'd explore nesting by expanding the tree, and recursive rigged functions by clicking on calls that would open the relevant tab, intermediate results would be easy to print or fetch.
With a minor tweak implemented in https://github.com/moodymudskipper/boomer/tree/xml-experiment we can alter the output so it can be easily collapsed/expanded in an xml editor :
boom(subset(head(mtcars, 2), qsec > 17))
#> <subset CALL=subset(head(mtcars, 2), qsec GT 17)>
#> <head CALL=head(mtcars, 2)>
#> mpg cyl disp hp drat wt qsec vs am gear carb
#> Mazda RX4 21 6 160 110 3.9 2.620 16.46 0 1 4 4
#> Mazda RX4 Wag 21 6 160 110 3.9 2.875 17.02 0 1 4 4
#> </head>
#> <GT CALL=qsec GT 17>
#> [1] FALSE TRUE
#> </GT>
#> mpg cyl disp hp drat wt qsec vs am gear carb
#> Mazda RX4 Wag 21 6 160 110 3.9 2.875 17.02 0 1 4 4
#> </subset>
my_print <- function(x) {
writeLines(paste0("my: ", x))
invisible(x)
}
boom(my_print(1 + 2))
#> <my_print CALL=my_print(1 + 2)>
#> <+ CALL=1 + 2>
#> [1] 3
#> </+>
#> my: 3
#> [1] 3
#> </my_print>
It comes with its problem too (readability, multiline, reserved characters), but the idea might be used for something nicer.
The above looks ok in notepad++
worked for me for a failing tidy evaluation pipe, but not for a failing SE pipe. Should we tryCatch()
the evaluation?
library(tidyverse)
1 %>%
identity() %>%
I() %>%
boomer::boom()
#> identity(.)
#> [1] 1
#> I(.)
#> [1] 1
#> 1 %>% identity() %>% I()
#> [1] 1
1 %>%
identity() %>%
missing_function() %>%
I() %>%
boomer::boom()
#> Error in missing_function(.): could not find function "missing_function"
failing_function <- function(x) {
stop("oops")
}
1 %>%
identity() %>%
failing_function() %>%
I() %>%
boomer::boom()
#> Error in (function (x) : oops
Created on 2021-04-29 by the reprex package (v2.0.0)
to avoid repetition of output.
library(boomer)
library(tidyverse)
test <- boomer::rigger() + function() {
a <- 1 + 2 * 3
}
test()
#> 2 * 3
#> [1] 6
#> 1 + 2 * 3
#> [1] 7
#> a <- 1 + 2 * 3
#> [1] 7
#> {
#> a <- 1 + 2 * 3
#> }
#> [1] 7
#> [1] 7
Created on 2021-04-26 by the reprex package (v2.0.0)
Should we print first, then execute?
my_print <- function(x) {
writeLines(paste0("my: ", x))
invisible(x)
}
boomer::boom(my_print(1 + 2))
#> · 1 + 2
#> [1] 3
#> my: 3
#> my_print(1 + 2)
#> [1] 3
Created on 2021-06-07 by the reprex package (v0.3.0)
I believe my: 3
should come below the my_print()
call, this has confused me a few times in the past.
fun <- function(x) {
x
}
boomer::boom(fun(1:3))
#> Error in asNamespace(ns): not a namespace
Created on 2021-04-15 by the reprex package (v1.0.0)
Hey @moodymudskipper Thanks for your work. boom
doesn't seem to work with %>%
for me.
library(boomer)
library(magrittr)
mtcars %>%
head(2) %>%
subset(qsec > 17) %>%
boom()
Session Details:
> sessionInfo()
R version 4.0.0 (2020-04-24)
Platform: x86_64-apple-darwin17.0 (64-bit)
Running under: macOS Catalina 10.15.7
Matrix products: default
BLAS: /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBLAS.dylib
LAPACK: /Library/Frameworks/R.framework/Versions/4.0/Resources/lib/libRlapack.dylib
locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
attached base packages:
[1] stats graphics grDevices
[4] utils datasets methods
[7] base
other attached packages:
[1] magrittr_1.5
[2] boomer_0.0.0.9000
loaded via a namespace (and not attached):
[1] ps_1.3.2
[2] digest_0.6.25
[3] R6_2.4.1
[4] reprex_0.3.0
[5] evaluate_0.14
[6] rlang_0.4.10
[7] rstudioapi_0.11
[8] fs_1.4.1
[9] callr_3.4.3
[10] whisker_0.4
[11] rmarkdown_2.6
[12] tools_4.0.0
[13] xfun_0.20
[14] compiler_4.0.0
[15] processx_3.4.2
[16] clipr_0.7.0
[17] htmltools_0.5.1.1
[18] knitr_1.28````
from #16
Have you considered explicitly wrapping, instead of shimming functions? Are there downsides? I think this would also solve #10.
boom(1 + 2 * 3)
could create boomer:::wrapx(1 + boomer:::wrapx(2 * 3))
, with wrapx()
along the lines of:
wrapx <- function(code) {
source <- deparse1(unwrapx(substitute(code))) # or we could use the original srcref?
out <- force(code)
# side effects
out
}
unwrapx <- function(expr) {
# Walk AST to remove boomer:::wrapx() calls for pretty printing
}
The advantage is that we could also boom all inputs. This isn't too important for constants but perhaps much more for variables:
boomer:::wrapx(boomer:::wrapx(1) + boomer:::wrapx(boomer:::wrapx(2) * boomer:::wrapx(3)))
It might simplify the internal code too.
I feel that having many arguments to boom and rig is distracting, maybe some or most would be better only set through options.
I think clock
and print
might stay, because we'd use them to have a specific behavior in some circumstances.
ignore
, visible_only
and print_args
are more about general preferences, and defaults like ignore = getOption("boom.ignore")
don't look good.
max_indent
would go there too.
It'd be clearer to document them separately, and then we can have as many options as we want and keep simple usages and doc of boom/rig friendly.
cli::symbol$ellipsis
#> [1] "…"
boomer::boom(cli::symbol$ellipsis)
#> cli::symbol$ellipsis
#> Error: simpleError/error/condition
#> Error in .Primitive("$")(cli::symbol, ellipsis): object of type 'closure' is not subsettable
Created on 2021-06-09 by the reprex package (v2.0.0)
When using rig_in_namespace
, each function is first rigged in place, then wrappers of all those are set in each enclosing environments.
In doing so we don't provide a mask where to look for the flags for first call or evaluated args.
As a consequence, in the example below we don't signal that we enter add1_wrapper
:
fake_package("fake", list(
add1 = function(x) {
x + 1
},
add1_wrapper = function(x) {
add1(x)
},
rec_factorial = function(x) {
if(x == 1) return(1)
x * rec_factorial(x-1)
}
))
rig_in_namespace(add1, add1_wrapper, print_args = TRUE)
add1_wrapper(1)
fun <- function(x) {
xx <- rlang::ensym(x)
xx
}
fun(xx)
#> xx
boomer::rig(fun)(xx)
#> rlang::ensym(x)
#> xx
#> Error in eval(expr, p): object 'xx' not found
Created on 2021-06-03 by the reprex package (v2.0.0)
Are we unquoting implicitly somewhere? How to work around/fix?
Copy/pastes were fine when code was simpler to avoid adding abstraction layers, but now it's starting to be hard to maintain.
for CI/CD?
A fix would be to use standard symbols when interactive()
is TRUE
, we could enter and exit a rigged function with ">>" and "<<", and arm and detonate the bomb with "O" and "X"
Might be relevant : https://stackoverflow.com/questions/44153072/unicode-with-knitr-and-rmarkdown
related to #37
FALSE by default, set to TRUE to have readable output whether the system is UTF8-enabled or not.
Set to TRUE
before tests so snapshot tests can work again.
just a global option, no need to make this an argument.
Now that we have successful R CMD CHECK and tests, let's keep it this way and work towards 100% coverage.
boomer::boom(for (i in 1:10) i)
#> Error in exists(fun_chr, pf): invalid first argument
Created on 2021-05-25 by the reprex package (v2.0.0)
fun <- function(x) {
pillar::pillar_shaft(x)
}
fun <- boomer::rig(fun)
fun(1:3)
#> Error in eval(e[[2L]], where): argument "print_fun" is missing, with no default
Created on 2021-04-15 by the reprex package (v1.0.0)
There are a few ways to show that something changed (e.g. when we are exploding the calls of a rigged function called by another rigged function) :
Right now I tend to think that nested calls should be indented, and that a prefix "my_rigged_function_name>"
before printing any call would make it easy to follow along when using rig_in_namespace
on several functions that call each other.
I think announcing explicitly just once "booming my_rigged_function_name"
is not possible without altering the function's body, which I feel might come back to bite us in some hard to debug corner cases, and bugs in debugging tools are not good for sanity.
Because of the way we store ..FIRST_CALL..
and ..EVALED_ARGS..
in the mask.
fake_package("fake", list(
add2 = function(a, b) {
a + b
},
add4 = function(a, b, c, d) {
add2(a, b) + add2(c, d)
},
rec_factorial = function(x) {
if(x == 1) return(1)
x * rec_factorial(x-1)
}
))
rig_in_namespace(add2, add4, rec_factorial, print_args = TRUE)
# works
add4(1, 2, 3, 4)
# rigged functions calling themselves are not opened/closed right
add2(add2(1, 2), add2(3, 4))
# recursive calls of rigged functions are not opened/closed right
rec_factorial(2)
works :
doesn't :
doesn't either:
I believe the mask might contain a variable ..STATE..
that would be a list, the first call of a rigged function would add an item to that list, and set up that it would be peeled off on exit (with withr::defer_parent
as done atm). then wrap()
ped functions would look at the last item.
from #16 :
Right now we need to restart the session (or devtools::load_all() if only rigging in developped package).
Still not sure what would be the best way, maybe we'd need unrig_in_namespace()
, that unrigs eveything if no arg is given (we'd need to keep an index of rigged functions), and have rig_in_namespace()
add as it does now, but make sure we cannot rig a function 2 times.
Or we might have rig_in_namespace()
work a bit like dplyr::select()
rigged functions have the same code as the original function, however this is not the case of wrapper functions.
So if wrapper functions are returned, or if the rigged function does anything with their body, the result will be unexpected :
I'm not sure if it can be improved, it's also hard to document.
If the mask contained active bindings rather than wrapper functions, we might make the active binding check how it's used, and if it's not used as a calling function, return the original function, but I haven't figured out how to make the active binding know how it's used.
We would have a function similar to rig but :
This way no need for debugonce hell, moreover the case when the error is in a function that was built at runtime (that we can't debugonce from the outside) is especially painful to debug but with this trick would be much less painful.
Looks not easy but useful.
from : #40
c <- function(x) {
if (x > 0) wat
}
b <- function(x) c(x)
b <- boomer::rig(b, print_args = TRUE)
a <- function(x) b(3)
a(5)
#> 👇 b
#> 💣 c
#> Registered S3 method overwritten by 'pryr':
#> method from
#> print.bytes Rcpp
#> x :
#> [1] 3
#> 💥 c(x)
#> Error: simpleError/error/condition
#> Error in (function (x) : object 'wat' not found
#> 👆 b
We might find a way to trigger this at a more appropriate time.
On the other hand, we use only function promise_evaled
, which has nothing to do with print.bytes()
, is unexported, and has really simple code, so it might be cleaner just to have it in the package.
Its cpp code is :
#include <Rcpp.h>
using namespace Rcpp;
// [[Rcpp::export]]`
bool promise_evaled(Symbol name, Environment env) {
SEXP object = Rf_findVar(name, env); return PRVALUE(object) != R_UnboundValue;
}
The R code is :
promise_evaled <- function(name, env) {
.Call('pryr_promise_evaled', PACKAGE = 'pryr', name, env)
}
I find in the repo the old C code, so we can avoid a Rcpp dependency :
SEXP promise_evaled(SEXP name, SEXP env) {
SEXP object = findVar(name, env);
return(ScalarLogical(PRVALUE(object) != R_UnboundValue));
}
and the R code would be
.Call("promise_evaled", name, env)
remotes::install_github("moodymudskipper/boomer") does not work on windows, R 4.0.4.
I get: Error: Failed to install 'unknown package' from GitHub:
cannot open the connection.
Backtrace:
library(magrittr)
piped <- function(x) {
x %>%
identity() %>%
identity() %>%
identity()
}
piped <- boomer::rig(piped)
piped(3)
#> 👇 piped
#> 💣 %>%
#> · 💣 identity
#> · · 💣 identity
#> · · · 💣 identity
#> · · · 💥 identity(.)
#> · · · [1] 3
#> · · ·
#> · · 💥 identity(.)
#> · · [1] 3
#> · ·
#> · 💥 identity(.)
#> · [1] 3
#> ·
#> 💥 x %>% identity() %>% identity() %>% identity()
#> [1] 3
#>
#> 👆 piped
#> [1] 3
Created on 2021-06-17 by the reprex package (v2.0.0)
Is there a way to flatten these? Maybe after reaching a certain depth we could switch to two-digit numbers (and add an extra level only if another function is called)?
Looks like we can get the srcref from sys.call()
?
a <- function() {
# dummy comment
b() # located on the third line of the source file
}
b <- function() {
call <- sys.call()
file <- getSrcFilename(call)
line <- unclass(getSrcref(call))[[1]]
paste0(file, ":", line)
}
a()
#> [1] "<text>:3"
Created on 2021-06-14 by the reprex package (v2.0.0)
The correct file name is returned when I run locally from a proper file.
b <- function(x) x
b <- boomer::rig(b)
a <- function(x) b(3)
a <- boomer::rig(a)
a(5)
#> · 👇 b
#> · 💣 crayon::yellow
#> · 💥 crayon::yellow(strrep("· ", globals$n_indent))
#> · [1] "· "
#> ·
#> · 💣 crayon::yellow
#> · 💥 crayon::yellow("a")
#> · [1] "a"
#> ·
#> · 👇 a
#> · 💣 withr::defer_parent
#> · 💥 withr::defer_parent({
#> cat(dots, "👆 ", crayon::yellow("a"), "\n", sep = "")
#> mask$..FIRST_CURLY.. <- TRUE
#> })
#> · $expr
#> · {
#> · cat(dots, "👆 ", crayon::yellow("a"), "\n", sep = "")
#> · mask$..FIRST_CURLY.. <- TRUE
#> · }
#> ·
#> · $envir
#> · <environment: 0x5634b3576f28>
#> · attr(,"handlers")
#> · attr(,"handlers")[[1]]
#> · attr(,"handlers")[[1]]$expr
#> · {
#> · cat(dots, "👆 ", crayon::yellow("b"), "\n", sep = "")
#> · mask$..FIRST_CURLY.. <- TRUE
#> · }
#> ·
#> · attr(,"handlers")[[1]]$envir
#> · <environment: 0x5634b3639fd8>
#> ·
#> ·
#> ·
#> ·
#> · 💣 crayon::cyan
#> · 💥 crayon::cyan(deparse1(sc_bkp[[1]]))
#> · [1] "b"
#> ·
#> · 💣 b
#> · 💣 rlang::eval_bare
#> · · 💣 function (...) { globals <- getFromNamespace("globals", "boomer") globals$n_indent <- globals$n_indent + 1 dots <- crayon::yellow(strrep("· ", globals$n_indent)) on.exit({ globals$n_indent <- globals$n_indent - 1 }) sc <- sys.call() sc_bkp <- sc sc[[1]] <- function (x) x cat(dots, "💣 ", crayon::cyan(deparse1(sc_bkp[[1]])), "\n", sep = "") success <- FALSE error <- tryCatch({ res <- withVisible(rlang::eval_bare(sc, parent.frame())) success <- TRUE }, error = identity) if (success && !res$visible && FALSE) return(invisible(res$value)) cat(dots, "💥 ", crayon::cyan(deparse1(sc_bkp, collapse = paste0("\n", strrep(" ", globals$n_indent + 3)))), "\n", sep = "") if (!success) { writeLines(crayon::magenta("Error:", paste0(class(error), collapse = "/"))) stop(error) } res <- res$value print_fun <- getFromNamespace("fetch_print_fun", "boomer")(function (x, ...) UseMethod("print"), res) writeLines(c(paste0(dots, capture.output(print_fun(res))), dots)) res }
#> · · 💥 (function (...)
#> {
#> globals <- getFromNamespace("globals", "boomer")
#> globals$n_indent <- globals$n_indent + 1
#> dots <- crayon::yellow(strrep("· ", globals$n_indent))
#> on.exit({
#> globals$n_indent <- globals$n_indent - 1
#> })
#> sc <- sys.call()
#> sc_bkp <- sc
#> sc[[1]] <- function (x)
#> x
#> cat(dots, "💣 ", crayon::cyan(deparse1(sc_bkp[[1]])), "\n", sep = "")
#> success <- FALSE
#> error <- tryCatch({
#> res <- withVisible(rlang::eval_bare(sc, parent.frame()))
#> success <- TRUE
#> }, error = identity)
#> if (success && !res$visible && FALSE)
#> return(invisible(res$value))
#> cat(dots, "💥 ", crayon::cyan(deparse1(sc_bkp, collapse = paste0("\n", strrep(" ", globals$n_indent + 3)))), "\n", sep = "")
#> if (!success) {
#> writeLines(crayon::magenta("Error:", paste0(class(error), collapse = "/")))
#> stop(error)
#> }
#> res <- res$value
#> print_fun <- getFromNamespace("fetch_print_fun", "boomer")(function (x, ...)
#> UseMethod("print"), res)
#> writeLines(c(paste0(dots, capture.output(print_fun(res))), dots))
#> res
#> })(3)
#> · · [1] 3
#> · ·
#> · 💥 rlang::eval_bare(sc, parent.frame())
#> · [1] 3
#> ·
#> · 💣 crayon::cyan
#> · 💥 crayon::cyan(deparse1(sc_bkp, collapse = paste0("\n", strrep(" ", globals$n_indent + 3))))
#> · [1] "b(3)"
#> ·
#> · 💥 b(3)
#> · · 💣 print_fun
#> · [1] 3
#> · · 💥 print_fun(res)
#> · · [1] 3
#> · ·
#> · [1] 3
#> ·
#> 💣 crayon::yellow
#> 💥 crayon::yellow("a")
#> [1] "a"
#>
#> · 👆 a
#> [1] 3
Created on 2021-06-11 by the reprex package (v2.0.0)
that call each other.
I'd like to propose a wire()
function:
wire <- function(..., ignore = , ) {
names <- c(...)
# rigs all functions
# ensures that the function environments are updated with the newly rigged functions
}
And also ignite()
, to be called from .onLoad()
:
ignite <- function() {
config <- Sys.getenv("R_BOOMER")
...
wire(...)
}
To be used like this:
.onLoad <- function(...) {
if (requireNamespace("boomer", quietly = TRUE)) {
boomer::ignite()
}
}
c("boomer_rigged", "function")
So we can recognized them easily.
rig_in_namespace
will use it to make sure all function environments are synced, it will also make it easy to unrig, just set back the environment to initial value.
because base
and head
really shouldn't be part of this:
all.names(quote(base::head(3)))
#> [1] "::" "base" "head"
Created on 2021-05-02 by the reprex package (v2.0.0)
Not sure if this counts as documented behavior of all.names()
.
I wonder if we should memoise double_colon()
and triple_colon()
, because wrapping occurs at run time here?
and improve the doc
I've hit a limitation of boomer today: in a sequence of functions that forward their arguments, the argument values are very hard to trace.
Motivating example:
c <- function(x) wat
b <- function(x) c(x)
b <- boomer::rig(b)
a <- function(x) b(3)
a(5)
#> 👇 b
#> 💣 c
#> 💥 c(x)
#> Error: simpleError/error/condition
#> Error in (function (x) : object 'wat' not found
#> 👆 b
Created on 2021-06-11 by the reprex package (v2.0.0)
How can we enhance the output so that we learn from it that x
is 3 in the c(x)
call? Can we enhance rig()
in a way that it (optionally) prints its arguments? Maybe we can print only those arguments that were actually forced by the function call, to avoid NSE issues?
just like the addin for devtools::test()
prints to the "build" tab.
It would make it easier to browse long input and not "pollute" the console.
Not sure if RStudio offers this flexibility, we might request if it doesn't.
Long form:
fun <- function(x) {
...
}
# Repetition
fun <- boom::rig(fun)
Wrapping:
# Closing parenthesis is far away
fun <- boom::rig(function(x) {
...
})
Reverse pipe:
# Needs %<% in global namespace
fun <- boom::rig() %<% function(x) {
...
}
Maybe:
fun <- function(x) {
...
}
# with a suitable implementation
boom::xxx(fun)
Or:
# with a suitable implementation
fun <- boom::rigger + function(x) {
...
}
from #16 :
rig_in_namespace(pkg::foo, pkg:::bar)
from #33
There might be other potential problems that the magrittr exception that we hacked a bit brutally.
We could choose to print the top call, then have a way to check if a wrap()ped function was called during execution, then if it wasn't not print the bottom call, but :
"..."
Maybe there's a way
must be done in .onLoad()
.
Consider a package (such as dm) that imports dplyr::filter()
. When using boomer::rigger()
to rig a function that uses filter()
(such as dm_insert_zoomed_outgoing_fks()
in cynkra/dm@ee564d1), that function environment's filter
shim points to stats::filter()
.
The problem is circumvented by adding the following to .onLoad()
:
dm_insert_zoomed_outgoing_fks <<- boomer::rig(dm_insert_zoomed_outgoing_fks)
I'm sorry, it seems this makes rigger()
less useful than anticipated.
Should we use withVisible()
and optionally hide invisible output (via an argument to rig()
)? This solves part of #12.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.