Repository: timholy/Rebugger.jl Branch: master Commit: 3312b63210e8 Files: 38 Total size: 145.7 KB Directory structure: gitextract_4j1i8l0j/ ├── .github/ │ └── workflows/ │ ├── Documenter.yml │ ├── TagBot.yml │ └── ci.yml ├── .gitignore ├── LICENSE.md ├── Project.toml ├── README.md ├── docs/ │ ├── Project.toml │ ├── make.jl │ └── src/ │ ├── config.md │ ├── index.md │ ├── internals.md │ ├── limitations.md │ ├── reference.md │ └── usage.md ├── src/ │ ├── Rebugger.jl │ ├── debug.jl │ ├── deepcopy.jl │ ├── precompile.jl │ ├── printing.jl │ └── ui.jl └── test/ ├── edit.jl ├── interpret.jl ├── interpret_script.jl ├── interpret_ui.jl ├── my_gcd.jl ├── runtests.jl ├── testmodule.jl └── ui/ ├── v1.0/ │ ├── gcd.multiout │ └── gcdsc.multiout ├── v1.1/ │ ├── gcd.multiout │ └── gcdsc.multiout ├── v1.2/ │ ├── gcd.multiout │ └── gcdsc.multiout ├── v1.3/ │ ├── gcd.multiout │ └── gcdsc.multiout └── v1.5/ ├── gcd.multiout └── gcdsc.multiout ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/Documenter.yml ================================================ name: Documenter on: push: branches: [master] tags: [v*] pull_request: jobs: Documenter: name: Documentation runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: julia-actions/julia-buildpkg@latest - uses: julia-actions/julia-docdeploy@latest env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} ================================================ FILE: .github/workflows/TagBot.yml ================================================ name: TagBot on: issue_comment: types: - created workflow_dispatch: jobs: TagBot: if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' runs-on: ubuntu-latest steps: - uses: JuliaRegistries/TagBot@v1 with: token: ${{ secrets.GITHUB_TOKEN }} ssh: ${{ secrets.DOCUMENTER_KEY }} ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: pull_request: branches: - master push: branches: - master tags: '*' jobs: test: name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: version: - '1.0' - '1' - 'nightly' os: - ubuntu-latest arch: - x64 steps: - uses: actions/checkout@v2 - uses: julia-actions/setup-julia@v1 with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - uses: actions/cache@v1 env: cache-name: cache-artifacts with: path: ~/.julia/artifacts key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} restore-keys: | ${{ runner.os }}-test-${{ env.cache-name }}- ${{ runner.os }}-test- ${{ runner.os }}- - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v1 with: file: lcov.info ================================================ FILE: .gitignore ================================================ docs/build/ docs/site/ test/expected.out test/failed.out ================================================ FILE: LICENSE.md ================================================ The Rebugger.jl package is licensed under the MIT "Expat" License: > Copyright (c) 2018: Tim Holy. > > Permission is hereby granted, free of charge, to any person obtaining a copy > of this software and associated documentation files (the "Software"), to deal > in the Software without restriction, including without limitation the rights > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell > copies of the Software, and to permit persons to whom the Software is > furnished to do so, subject to the following conditions: > > The above copyright notice and this permission notice shall be included in all > copies or substantial portions of the Software. > > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE > SOFTWARE. > ================================================ FILE: Project.toml ================================================ name = "Rebugger" uuid = "ee283ea6-eecd-56e3-beb3-83eb4d3c31e9" version = "0.3.3" [deps] CodeTracking = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2" HeaderREPLs = "54d51984-71c9-52bd-8df9-6718e63e4153" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" JuliaInterpreter = "aa1ae85d-cabe-5617-a682-6adf51b2e16a" REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" [compat] CodeTracking = "0.5" HeaderREPLs = "0.3" JuliaInterpreter = "0.7" Revise = "2.1.10" julia = "1" [extras] Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" TerminalRegressionTests = "98bfdc55-cc95-5876-a49a-74609291cbe0" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] test = ["Test", "Colors", "Pkg", "TerminalRegressionTests"] ================================================ FILE: README.md ================================================ # Rebugger [![Build Status](https://travis-ci.org/timholy/Rebugger.jl.svg?branch=master)](https://travis-ci.org/timholy/Rebugger.jl) [![Build status](https://ci.appveyor.com/api/projects/status/e9t1wlyy995whchc?svg=true)](https://ci.appveyor.com/project/timholy/Rebugger-jl/branch/master) [![codecov.io](http://codecov.io/github/timholy/Rebugger.jl/coverage.svg?branch=master)](http://codecov.io/github/timholy/Rebugger.jl?branch=master) [![PkgEval][pkgeval-img]][pkgeval-url] Rebugger is an expression-level debugger for Julia. It has no ability to interact with or manipulate call stacks (see [Debugger](https://github.com/JuliaDebug/Debugger.jl) or the debugger built into vscode), but it can trace execution via the manipulation of Julia expressions. The name "Rebugger" has 3 meanings: - it is a REPL-based debugger (more on that in the documentation) - it is the [Revise](https://github.com/timholy/Revise.jl)-based debugger - it supports repeated-execution debugging ### Current status Currently broken and unmaintained due to the author having too many other packages to maintain. However, the functionality and general concept is still quite attractive. For anyone interested in taking over maintenance, see [issue #90](https://github.com/timholy/Rebugger.jl/issues/90) for more information. ### JuliaCon 2018 Talk While it's somewhat dated, you can learn about the "edit" interface in the following video: [![](https://img.youtube.com/vi/KuM0AGaN09s/0.jpg)](https://youtu.be/KuM0AGaN09s?t=515) However, the "interpret" interface is recommended for most users. ## Installation and usage **See the documentation**: [![](https://img.shields.io/badge/docs-stable-blue.svg)](https://timholy.github.io/Rebugger.jl/stable) [![](https://img.shields.io/badge/docs-latest-blue.svg)](https://timholy.github.io/Rebugger.jl/dev) Note that Rebugger may benefit from custom configuration, as described in the documentation. In terms of usage, very briefly - for "interpret" mode, type your command and hit Meta-i (which stands for "interpret") - for "edit" mode, "step in" is achieved by positioning your cursor in your input line to the beginning of the call expression you wish to descend into. Then hit Meta-e ("enter"). - also for "edit" mode, for an expression that generates an error, hit Meta-s ("stacktrace") to capture the stacktrace and populate your REPL history with a sequence of expressions that contain the method bodies of the calls in the stacktrace. Meta means Esc or, if your system is configured appropriately, Alt (Linux/Windows) or Option (Macs). More information and complete examples are provided in the documentation. If your operating system assigns these keybindings to something else, you can [configure them to keys of your own choosing](https://timholy.github.io/Rebugger.jl/stable/config/#Customize-keybindings-1). ## Status Rebugger is in early stages of development, and users should currently expect bugs (please do [report](https://github.com/timholy/Rebugger.jl/issues) them). Neverthess it may be of net benefit for some users. [pkgeval-img]: https://juliaci.github.io/NanosoldierReports/pkgeval_badges/R/Rebugger.svg [pkgeval-url]: https://juliaci.github.io/NanosoldierReports/pkgeval_badges/report.html ================================================ FILE: docs/Project.toml ================================================ [deps] Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" [compat] Documenter = "~0.21" ================================================ FILE: docs/make.jl ================================================ using Documenter, Rebugger makedocs( modules = [Rebugger], clean = false, format = Documenter.HTML(prettyurls = get(ENV, "CI", nothing) == "true"), sitename = "Rebugger.jl", authors = "Tim Holy", linkcheck = !("skiplinks" in ARGS), pages = [ "Home" => "index.md", "usage.md", "config.md", "limitations.md", "internals.md", "reference.md", ], ) deploydocs( repo = "github.com/timholy/Rebugger.jl.git", ) ================================================ FILE: docs/src/config.md ================================================ # Configuration ## Run on REPL startup If you decide you like Rebugger, you can add lines such as the following to your `~/.julia/config/startup.jl` file: ```julia atreplinit() do repl try @eval using Revise @async Revise.wait_steal_repl_backend() catch @warn "Could not load Revise." end try @eval using Rebugger catch @warn "Could not load Rebugger." end end ``` ## Customize keybindings As described in [Keyboard shortcuts](@ref), it's possible that Rebugger's default keybindings don't work for you. You can work around problems by changing them to keys of your own choosing. To add your own keybindings, use `Rebugger.add_keybindings(action=keybinding, ...)`. This can be done during a running Rebugger session. Here is an example that maps the "step in" action to the key "F6" and "capture stacktrace" to "F7" ```julia julia> Rebugger.add_keybindings(stepin="\e[17~", stacktrace="\e[18~") ``` To make your keybindings permanent, change the "Rebugger" section of your `startup.jl` file to something like: ```julia atreplinit() do repl ... try @eval using Rebugger # Activate Rebugger's key bindings Rebugger.keybindings[:stepin] = "\e[17~" # Add the keybinding F6 to step into a function. Rebugger.keybindings[:stacktrace] = "\e[18~" # Add the keybinding F7 to capture a stacktrace. catch @warn "Could not load Rebugger." end end ``` !!! note Besides the obvious, one reason to insert the keybindings into the `startup.jl`, has to do with the order in which keybindings are added to the REPL and whether any "stale" bindings that might have side effects are still present. Doing it before `atreplinit` means that there won't be any stale bindings. But how to find out the cryptic string that corresponds to the keybinding you want? Use Julia's `read()` function: ```julia julia> str = read(stdin, String) ^[[17~"\e[17~" # Press F6, followed by Ctrl+D, Ctrl+D julia> str "\e[17~" ``` After calling `read()`, press the keybinding that you want. Then, press `Ctrl+D` twice to terminate the input. The value of `str` is the cryptic string you are looking for. If you want to know whether your key binding is already taken, the [REPL documentation](https://docs.julialang.org/en/latest/stdlib/REPL/#Key-bindings-1) as well as any documentation on your operating system, desktop environment, and/or terminal program can be useful references. ================================================ FILE: docs/src/index.md ================================================ # Introduction to Rebugger Rebugger is an expression-level debugger for Julia. It has two modes of action: - an "interpret" mode that lets you step through code, set breakpoints, and other manipulations common to "typical" debuggers; - an "edit" mode that presents method bodies as objects for manipulation, allowing you to interactively play with the code at different stages of execution. The name "Rebugger" has 3 meanings: - it is a [REPL](https://docs.julialang.org/en/latest/stdlib/REPL/)-based debugger (more on that below) - it is the [Revise](https://github.com/timholy/Revise.jl)-based debugger - it supports repeated-execution debugging ## Installation Begin with ```julia (v1.0) pkg> add Rebugger ``` You can experiment with Rebugger with just ```julia julia> using Rebugger ``` If you eventually decide you like Rebugger, you can optionally configure it so that it is always available (see [Configuration](@ref)). ## Keyboard shortcuts Most of Rebugger's functionality gets triggered by special keyboard shortcuts added to Julia's REPL. Unfortunately, different operating systems and desktop environments vary considerably in their key bindings, and it is possible that the default choices in Rebugger are already assigned other meanings on your platform. There does not appear to be any one set of choices that works on all platforms. The best strategy is to try the demos in [Usage](@ref); if the default shortcuts are already taken on your platform, then you can easily configure Rebugger to use different bindings (see [Configuration](@ref)). Some platforms are known to require or benefit from special attention: #### macOS If you're on macOS, you may want to enable "[Use `option` as the Meta key](https://github.com/timholy/Rebugger.jl/issues/28#issuecomment-414852133)" in your Terminal settings to avoid the need to press Esc before each Rebugger command. #### Ubuntu The default meta key on some Ubuntu versions is left Alt, which is equivalent to Esc Alt on the default Gnome terminal emulator. However, even with this tip you may encounter problems because Rebugger's default key bindings may be assigned to activate menu options within the terminal window, and [this appears not to be configurable]( https://bugs.launchpad.net/ubuntu/+source/nautilus/+bug/1113420). Affected users may wish to [Customize keybindings](@ref). ================================================ FILE: docs/src/internals.md ================================================ # How Rebugger works Rebugger traces execution through use of expression-rewriting and Julia's ordinary `try/catch` control-flow. It maintains internal storage that allows other methods to "deposit" their arguments (a *store*) or temporarily *stash* the function and arguments of a call. ## Implementation of "step in" Rebugger makes use of the buffer containing user input: not just its contents, but also the position of "point" (the seek position) to indicate the specific call expression targeted for stepping in. For example, if a buffer has contents ```julia # if x > 0.5 ^fcomplex(x, 2; kw1=1.1) # ``` where in the above `^` indicates "point," Rebugger uses a multi-stage process to enter `fcomplex` with appropriate arguments: - First, it carries out *caller capture* to determine which function is being called at point, and with which arguments. The main goal here is to be able to then use `which` to determine the specific method. - Once armed with the specific method, it then carries out *callee capture* to obtain all the inputs to the method. For simple methods this may be redundant with *caller capture*, but *callee capture* can also obtain the values of default arguments, keyword arguments, and type parameters. - Finally, Rebugger rewrites the REPL command-line buffer with a suitably-modified version of the body of the called method, so that the user can inspect, run, and manipulate it. ### Caller capture The original expression above is rewritten as ```julia # if x > 0.5 Main.Rebugger.stashed[] = (fcomplex, (x, 2), (kw1=1.1,)) throw(Rebugger.StopException()) # ``` Note that the full context of the original expression is preserved, thereby ensuring that we do not have to be concerned about not having the appropriate local scope for the arguments to the call of `fcomplex`. However, rather than actually calling `fcomplex`, this expression "stashes" the arguments and function in a temporary store internal to Rebugger. It then throws an exception type specifically crafted to signal that the expression executed and exited as expected. This expression is then evaluated inside a block ```julia try Core.eval(Main, caller_capture_expression) throw(StashingFailed()) catch err err isa StashingFailed && rethrow(err) if !(err isa StopException) throw(EvalException(content(buffer), err)) end end ``` Note that this looks for the `StopException`; this is considered the normal execution path. If the `StopException` is never hit, it means evaluation never reached the expression marked by "point" and thus leads to a `StashingFailed` exception. Any other error results in an `EvalException`, usually triggered by other errors in the block of code. Assuming the `StopException` is hit, we then proceed to callee capture. ### Callee capture Rebugger removes the function and arguments from `Rebugger.stashed[]` and then uses `which` to determine the specific method called. It then asks [Revise](https://timholy.github.io/Revise.jl/stable/) for the expression that defines the method. It then analyzes the signature to determine the full complement of inputs and creates a new method that stores them. For example, if the applicable method of `fcomplex` is given by ```julia function fcomplex(x::A, y=1, z=""; kw1=3.2) where A<:AbstractArray{T} where T # end ``` then Rebugger generates a new method ```julia function hidden_fcomplex(x::A, y=1, z=""; kw1=3.2) where A<:AbstractArray{T} where T Main.Rebugger.stored[uuid] = Main.Rebugger.Stored(fcomplex, (:x, :y, :z, :kw1, :A, :T), deepcopy((x, y, z, kw1, A, T))) throw(StopException()) end ``` This method is then called from inside another `try/catch` block that again checks for a `StopException`. This results in the complete set of inputs being *stored*, a more "permanent" form of preservation than *stashing*, which only lasts for the gap between caller and callee capture. If one has the appropriate `uuid`, one can then extract these values at will from storage using [`Rebugger.getstored`](@ref). ### Generating the new buffer contents (the `let` expression) Once callee capture is complete, the user can re-execute any components of the called method as desired. To make this easier, Rebugger replaces the contents of the buffer with a line that looks like this: ```julia @eval let (x, y, z, kw1, A, T) = Main.Rebugger.getstored("0123abc...") # end ``` The `@eval` makes sure that the block will be executed within the module in which `fcomplex` is defined; as a consequence it will have access to all the unexported methods, etc., that `fcomplex` itself has. The `let` block ensures that these variables do not conflict with other objects that may be defined in `ModuleOf_fcomplex`. The values are unloaded from the store (making copies, in case `fcomplex` modifies its inputs) and then execution proceeds into `body`. The user can then edit the buffer at will. ## Implementation of "catch stacktrace" In contrast with "step in," when catching a stacktrace Rebugger does not know the specific methods that will be used in advance of making the call. Consequently, Rebugger has to execute the call twice: - the first call is used to obtain a stacktrace - The trace is analyzed to obtain the specific methods, which are then replaced with versions that place inputs in storage; see [Callee capture](@ref), with the differences + the original method is (temporarily) overwritten by one that executes the store + this "storing" method also includes the full method body These two changes ensure that the "call chain" is not broken. - a second call (recreating the same error, for functions that have deterministic execution) is then made to store all the arguments at each captured stage of the stacktrace. - finally, the original methods are restored. ================================================ FILE: docs/src/limitations.md ================================================ # Limitations Rebugger is in the early stages of development, and users should currently expect bugs (please do [report](https://github.com/timholy/Rebugger.jl/issues) them). Nevertheless it may be of net benefit for some users. Here are some known shortcomings: - Rebugger only has access to code tracked by Revise. To ensure that scripts are tracked, use `includet(filename)` to include-and-track. (See [Revise's documentation](https://timholy.github.io/Revise.jl/stable/user_reference.html).) For stepping into Julia's stdlibs, currently you need a source-build of Julia. - You cannot step into methods defined at the REPL. - For now you can't step into constructors (it tries to step into `(::Type{T})`) - There are occasional glitches in the display. (For brave souls who want to help fix these, see [HeaderREPLs.jl](https://github.com/timholy/HeaderREPLs.jl)) - Rebugger runs best in Julia 1.0. While it should run on Julia 0.7, a local-scope deprecation can cause some problems. If you want 0.7 because of its deprecation warnings and are comfortable building Julia, consider building it at commit f08f3b668d222042425ce20a894801b385c2b1e2, which removed the local-scope deprecation but leaves most of the other deprecation warnings from 0.7 still in place. - If you start `dev`ing a package that you had already loaded, you need to [restart your session](https://github.com/timholy/Revise.jl/issues/146) Another important point (not particularly specific to Rebugger) is that repeatedly executing code that modifies some global state can lead to unexpected side effects. Rebugger works best on methods whose behavior is determined solely by their input arguments. ================================================ FILE: docs/src/reference.md ================================================ # Developer reference ## Capturing arguments ```@docs Rebugger.stepin Rebugger.prepare_caller_capture! Rebugger.method_capture_from_callee Rebugger.signature_names! ``` ## Capturing stacktrace ```@docs Rebugger.capture_stacktrace Rebugger.pregenerated_stacktrace Rebugger.linerange ``` ## Utilities ```@docs Rebugger.clear Rebugger.getstored ``` ================================================ FILE: docs/src/usage.md ================================================ # Usage Rebugger works from Julia's native REPL prompt. Currently there are exactly three keybindings, which here will be described as: - Meta-i, which maps to "interpret" - Meta-e, which maps to "enter" or "step in" - Meta-s, which maps to "stacktrace" (for commands that throw an error) Meta often maps to `Esc`, and if using `Esc` you should hit the two keys in sequence rather than simultaneously. For many users `Alt` (sometimes specifically `Left-Alt`, or `Option` on macs) may be more convenient, as it can be pressed simultaneously with the key. Of course, you may have configured Rebugger to use different key bindings (see [Customize keybindings](@ref)). ## Interpret mode Interpret mode simulates an IDE debugger at the REPL: rather than entering your commands into a special prompt, you use single keystrokes to quickly advance through the code. Let's start with an example: ```julia julia> using Rebugger julia> a = [4, 1, 3, 2]; ``` Now we're going to call `sort`, but don't hit enter: ``` julia> sort(a) ``` Instead, hit Meta-i (Esc-i, Alt-i, or option-i): ``` interpret> sort(a)[ Info: tracking Base sort(v::AbstractArray{T,1} where T) in Base.Sort at sort.jl:742 v = [4, 1, 3, 2] 742 sort(v::AbstractVector; kws...) = begin 742 sort!(copymutable(v); kws...) end ``` The message informs you that Revise (which is used by Rebugger) is now examining the code in Base to extract the definition of `sort`. There's a considerable pause the first time you do this, but later it should generally be faster. After the "Info" line, you can see the method you called printed on top. After that are the local variables of `sort`, which here is just the array you supplied. (You can see some screenshots below in the "edit mode" section that show these in color. The meaning is the same here.) The "742" indicates the line number of "sort.jl", where the `sort` method you're calling is defined. Finally, you'll see a representation of the definition itself. Rebugger typically shows you expressions rather than verbatim text; unlike the text in the original file, this works equally well for `@eval`ed functions and generated functions. The current line number is printed in yellow; here, that's both lines, since the original definition was written on a single line. We can learn about the possibilities by typing `?`: ``` Commands: space: next line enter: continue to next breakpoint or completion →: step in to next call ←: finish frame and return to caller ↑: display the caller frame ↓: display the callee frame b: insert breakpoint at current line c: insert conditional breakpoint at current line r: remove breakpoint at current line d: disable breakpoint at current line e: enable breakpoint at current line q: abort (returns nothing) sort(v::AbstractArray{T,1} where T) in Base.Sort at sort.jl:742 v = [4, 1, 3, 2] 742 sort(v::AbstractVector; kws...) = begin 742 sort!(copymutable(v); kws...) end ``` Let's try stepping in to the call: hit the right arrow, at which point you should see ``` sort(v::AbstractArray{T,1} where T) in Base.Sort at sort.jl:742 #sort#8(kws, ::Any, v::AbstractArray{T,1} where T) in Base.Sort at sort.jl:742 #sort#8 = Base.Sort.#sort#8 kws = Base.Iterators.Pairs{Union{},Union{},Tuple{},NamedTuple{(),Tuple{}}}() @_3 = sort v = [4, 1, 3, 2] 742 sort(v::AbstractVector; kws...) = begin 742 sort!(copymutable(v); kws...) end ``` We're now in a "hidden" method `#sort#8`, generated automatically by Julia to handle keyword and/or optional arguments. This is what actually contains the main body of `sort`. You'll note the source expression hasn't changed, because it's generated from the same definition, but that some additional arguments (`kws` and the "nameless argument" `@_3`) have appeared. If we hit the right arrow again, we enter `copymutable`. Our interest is in stepping further into `sort`, so we're not going to bother walking through `copymutable`; hit left arrow, which finishes the current frame and returns to the caller. This should return you to `#sort#8`. Then hit the right arrow again and you should be here: ``` sort(v::AbstractArray{T,1} where T) in Base.Sort at sort.jl:742 #sort#8(kws, ::Any, v::AbstractArray{T,1} where T) in Base.Sort at sort.jl:742 sort!(v::AbstractArray{T,1} where T) in Base.Sort at sort.jl:682 #sort!#7(alg::Base.Sort.Algorithm, lt, by, rev::Union{Nothing, Bool}, order::Base.Order.Ordering, ::Any, v::AbstractArray{T,1} where T) in Base.Sort at sort.jl:682 #sort!#7 = Base.Sort.#sort!#7 alg = Base.Sort.QuickSortAlg() lt = isless by = identity rev = nothing order = Base.Order.ForwardOrdering() @_7 = sort! v = [4, 1, 3, 2] 681 function sort!(v::AbstractVector; alg::Algorithm=defalg(v), lt=isless, by=identity, re… 682 ordr = ord(lt, by, rev, order) 683 if ordr === Forward && (v isa Vector && eltype(v) <: Integer) 684 n = length(v) ``` Now you can see many more arguments. To understand everything you're seeing, sometimes it may help to open the source file in an editor (hit 'o' for open) for comparison. Note that long function bodies are truncated; you only see a few lines around the current execution point. Line 682 should be highlighted. Hit the space bar and you should advance to 683: ``` sort(v::AbstractArray{T,1} where T) in Base.Sort at sort.jl:742 #sort#8(kws, ::Any, v::AbstractArray{T,1} where T) in Base.Sort at sort.jl:742 sort!(v::AbstractArray{T,1} where T) in Base.Sort at sort.jl:682 #sort!#7(alg::Base.Sort.Algorithm, lt, by, rev::Union{Nothing, Bool}, order::Base.Order.Ordering, ::Any, v::AbstractArray{T,1} where T) in Base.Sort at sort.jl:682 #sort!#7 = Base.Sort.#sort!#7 alg = Base.Sort.QuickSortAlg() lt = isless by = identity rev = nothing order = Base.Order.ForwardOrdering() @_7 = sort! v = [4, 1, 3, 2] ordr = Base.Order.ForwardOrdering() 681 function sort!(v::AbstractVector; alg::Algorithm=defalg(v), lt=isless, by=identity, re… 682 ordr = ord(lt, by, rev, order) 683 if ordr === Forward && (v isa Vector && eltype(v) <: Integer) 684 n = length(v) 685 if n > 1 ``` You can see that the code display also advanced by one line. Let's go forward one more line (hit space) and then hit `b` to insert a breakpoint: ``` sort(v::AbstractArray{T,1} where T) in Base.Sort at sort.jl:742 #sort#8(kws, ::Any, v::AbstractArray{T,1} where T) in Base.Sort at sort.jl:742 sort!(v::AbstractArray{T,1} where T) in Base.Sort at sort.jl:682 #sort!#7(alg::Base.Sort.Algorithm, lt, by, rev::Union{Nothing, Bool}, order::Base.Order.Ordering, ::Any, v::AbstractArray{T,1} where T) in Base.Sort at sort.jl:682 #sort!#7 = Base.Sort.#sort!#7 alg = Base.Sort.QuickSortAlg() lt = isless by = identity rev = nothing order = Base.Order.ForwardOrdering() @_7 = sort! v = [4, 1, 3, 2] ordr = Base.Order.ForwardOrdering() #temp# = true 682 ordr = ord(lt, by, rev, order) 683 if ordr === Forward && (v isa Vector && eltype(v) <: Integer) b684 n = length(v) 685 if n > 1 686 (min, max) = extrema(v) ``` The `b` in the left column indicates an unconditional breakpoint; a `c` would indicate a conditional breakpoint. At this point, hit Enter to finish the entire command (you should see the result printed at the REPL). Now let's run it again, by going back in the REPL history (hit the up arrow) and then hitting Meta-i again: ``` interpret> sort(a) sort(v::AbstractArray{T,1} where T) in Base.Sort at sort.jl:742 v = [4, 1, 3, 2] 742 sort(v::AbstractVector; kws...) = begin 742 sort!(copymutable(v); kws...) end ``` We may be back at the beginning, but remember: we set a breakpoint. Hit Enter to let execution move forward: ``` interpret> sort(a) sort(v::AbstractArray{T,1} where T) in Base.Sort at sort.jl:742 #sort#8(kws, ::Any, v::AbstractArray{T,1} where T) in Base.Sort at sort.jl:742 sort!(v::AbstractArray{T,1} where T) in Base.Sort at sort.jl:682 #sort!#7(alg::Base.Sort.Algorithm, lt, by, rev::Union{Nothing, Bool}, order::Base.Order.Ordering, ::Any, v::AbstractArray{T,1} where T) in Base.Sort at sort.jl:682 #sort!#7 = Base.Sort.#sort!#7 alg = Base.Sort.QuickSortAlg() lt = isless by = identity rev = nothing order = Base.Order.ForwardOrdering() @_7 = sort! v = [4, 1, 3, 2] ordr = Base.Order.ForwardOrdering() #temp# = true 682 ordr = ord(lt, by, rev, order) 683 if ordr === Forward && (v isa Vector && eltype(v) <: Integer) b684 n = length(v) 685 if n > 1 686 (min, max) = extrema(v) ``` We're right back at that breakpoint again. Let's illustrate another example, this time in the context of errors: ``` julia> convert(UInt, -8) ERROR: InexactError: check_top_bit(Int64, -8) Stacktrace: [1] throw_inexacterror(::Symbol, ::Any, ::Int64) at ./boot.jl:583 [2] check_top_bit at ./boot.jl:597 [inlined] [3] toUInt64 at ./boot.jl:708 [inlined] [4] Type at ./boot.jl:738 [inlined] [5] convert(::Type{UInt64}, ::Int64) at ./number.jl:7 [6] top-level scope at none:0 ``` Rebugger re-exports [JuliaInterpreter's breakpoint manipulation utilities](https://juliadebug.github.io/JuliaInterpreter.jl/stable/). Let's turn on breakpoints any time an (uncaught) exception is thrown: ``` julia> break_on(:error) ``` Now repeat that `convert` line but hit Meta-i instead of Enter: ``` interpret> convert(UInt, -8) convert(::Type{T}, x::Number) where T<:Number in Base at number.jl:7 #unused# = UInt64 x = -8 T = UInt64 7 (convert(::Type{T}, x::Number) where T <: Number) = begin 7 T(x) end ``` Now if you hit Enter, you'll be at the place where the error was thrown: ``` interpret> convert(UInt, -8) convert(::Type{T}, x::Number) where T<:Number in Base at number.jl:7 UInt64(x::Union{Bool, Int32, Int64, UInt32, UInt64, UInt8, Int128, Int16, Int8, UInt128, UInt16}) in Core at boot.jl:738 toUInt64(x::Int64) in Core at boot.jl:708 check_top_bit(x) in Core at boot.jl:596 throw_inexacterror(f::Symbol, T, val) in Core at boot.jl:583 f = check_top_bit T = Int64 val = -8 583 throw_inexacterror(f::Symbol, @nospecialize(T), val) = (@_noinline_meta; throw(Inexact… ``` Try using the up and down arrows to navigate up and down the call stack. This doesn't change the notion of current execution point (that's still at that `throw_inexacterror` call above), but it does let you see where you came from. You can turn this off with `break_off` and clear all manually-set breakpoints with `remove()`. ### A few important details There are some calls that you can't step into: most of these are the "builtins," "intrinsics," and "ccalls" that lie at Julia's lowest level. Here's an example from hitting Meta-i on `show`: ``` interpret> show([1,2,4]) show(x) in Base at show.jl:313 x = [1, 2, 4] 313 show(x) = begin 313 show(stdout::IO, x) end ``` That looks like a call you'd want to step into. But if you hit the right arrow, apparently nothing happens. That's because the next statement is actually that type-assertion `stdout::IO`. `typeassert` is a builtin, and consequently not a call you can step into. When in doubt, just repeat the same keystroke; here, the second press of the right arrow takes you to the two-argument `show` method that you probably thought you were descending into. ## Edit mode ### Stepping in Select the expression you want to step into by positioning "point" (your cursor) at the desired location in the command line: ```@raw html ``` It's essential that point is at the very first character of the expression, in this case on the `s` in `show`. !!! note Don't confuse the REPL's cursor with your mouse pointer. Your mouse is essentially irrelevant on the REPL; use arrow keys or the other [navigation features of Julia's REPL](https://docs.julialang.org/en/latest/stdlib/REPL/). Now if you hit Meta-e, you should see something like this: ```@raw html ``` (If not, check [Keyboard shortcuts](@ref) and [Customize keybindings](@ref).) The cyan "Info" line is an indication that the method you're stepping into is a function in Julia's Base module; this is shown by Revise (not Rebugger), and only happens once per session. The remaining lines correspond to the Rebugger header and user input. The magenta line tells you which method you are stepping into. Indented blue line(s) show the value(s) of any input arguments or type parameters. If you're following along, move your cursor to the next `show` call as illustrated above. Hit Meta-e again. You should see a new `show` method, this time with two input arguments. Now let's demonstrate another important display item: position point at the beginning of the `_show_empty` call and hit Meta-e. The display should now look like this: ```@raw html ``` This time, note the yellow/orange line: this is a warning message, and you should pay attention to these. (You might also see red lines, which are generally more serious "errors.") In this case execution never reached `_show_empty`, because it enters `show_vector` instead; if you moved your cursor there, you could trace execution more completely. You can edit these expressions to insert code to display variables or test changes to the code. As an experiment, try stepping into the `show_vector` call from the example above and adding `@show limited` to display a local variable's value: ```@raw html ``` !!! note When editing expressions, you can insert a blank line with Meta-Enter (i.e., Esc-Enter, Alt-Enter, or Option-Enter). See the many [advanced features of Julia's REPL](https://docs.julialang.org/en/latest/stdlib/REPL/#Key-bindings-1) that allow you to efficiently edit these `let`-blocks. Having illustrated the importance of "point" and the various colors used for messages from Rebugger, to ensure readability the remaining examples will be rendered as text. ### Capturing stacktraces in edit mode For a quick demo, we'll use the `Colors` package (`add` it if you don't have it) and deliberately choose a method that will end in an error: we'll try to parse a string as a Hue, Saturation, Lightness (HSL) color, except we'll "forget" that hue cannot be expressed as a percentage and deliberately trigger an error: ```julia julia> using Colors julia> colorant"hsl(80%, 20%, 15%)" ERROR: LoadError: hue cannot end in % Stacktrace: [1] error(::String) at ./error.jl:33 [2] parse_hsl_hue(::SubString{String}) at /home/tim/.julia/dev/Colors/src/parse.jl:26 [3] _parse_colorant(::String) at /home/tim/.julia/dev/Colors/src/parse.jl:75 [4] _parse_colorant at /home/tim/.julia/dev/Colors/src/parse.jl:112 [inlined] [5] parse(::Type{Colorant}, ::String) at /home/tim/.julia/dev/Colors/src/parse.jl:140 [6] @colorant_str(::LineNumberNode, ::Module, ::Any) at /home/tim/.julia/dev/Colors/src/parse.jl:147 in expression starting at REPL[3]:1 ``` To capture the stacktrace, type the last line again or hit the up arrow, but instead of pressing Enter, type Meta-s. After a short delay, you should see something like this: ```julia julia> colorant"hsl(80%, 20%, 15%)" ┌ Warning: Tuple{getfield(Colors, Symbol("#@colorant_str")),LineNumberNode,Module,Any} was not found, perhaps it was generated by code └ @ Revise ~/.julia/dev/Revise/src/Revise.jl:659 Captured elements of stacktrace: [1] parse_hsl_hue(num::AbstractString) in Colors at /home/tim/.julia/packages/Colors/4hvzi/src/parse.jl:25 [2] _parse_colorant(desc::AbstractString) in Colors at /home/tim/.julia/packages/Colors/4hvzi/src/parse.jl:51 [3] _parse_colorant(::Type{C}, ::Type{SUP}, desc::AbstractString) where {C<:Colorant, SUP} in Colors at /home/tim/.julia/packages/Colors/4hvzi/src/parse.jl:112 [4] parse(::Type{C}, desc::AbstractString) where C<:Colorant in Colors at /home/tim/.julia/packages/Colors/4hvzi/src/parse.jl:140 parse_hsl_hue(num::AbstractString) in Colors at /home/tim/.julia/packages/Colors/4hvzi/src/parse.jl:25 num = 80% rebug> @eval Colors let (num,) = Main.Rebugger.getstored("57dbc76a-0def-11e9-1dbf-ef97d29d2e25") begin if num[end] == '%' error("hue cannot end in %") else return parse(Int, num, base=10) end end end ``` (Again, if this doesn't happen check [Keyboard shortcuts](@ref) and [Customize keybindings](@ref).) You are in the method corresponding to `[1]` in the stacktrace. Now you can navigate with your up and down arrows to browse the captured stacktrace. For example, if you hit the up arrow twice, you will be in the method corresponding to `[3]`: ```julia julia> colorant"hsl(80%, 20%, 15%)" ┌ Warning: Tuple{getfield(Colors, Symbol("#@colorant_str")),LineNumberNode,Module,Any} was not found, perhaps it was generated by code └ @ Revise ~/.julia/dev/Revise/src/Revise.jl:659 Captured elements of stacktrace: [1] parse_hsl_hue(num::AbstractString) in Colors at /home/tim/.julia/packages/Colors/4hvzi/src/parse.jl:25 [2] _parse_colorant(desc::AbstractString) in Colors at /home/tim/.julia/packages/Colors/4hvzi/src/parse.jl:51 [3] _parse_colorant(::Type{C}, ::Type{SUP}, desc::AbstractString) where {C<:Colorant, SUP} in Colors at /home/tim/.julia/packages/Colors/4hvzi/src/parse.jl:112 [4] parse(::Type{C}, desc::AbstractString) where C<:Colorant in Colors at /home/tim/.julia/packages/Colors/4hvzi/src/parse.jl:140 [5] @colorant_str(__source__::LineNumberNode, __module__::Module, ex) in Colors at /home/tim/.julia/packages/Colors/4hvzi/src/parse.jl:146 _parse_colorant(::Type{C}, ::Type{SUP}, desc::AbstractString) where {C<:Colorant, SUP} in Colors at /home/tim/.julia/packages/Colors/4hvzi/src/parse.jl:112 C = Colorant SUP = Any desc = hsl(80%, 20%, 15%) rebug> @eval Colors let (C, SUP, desc) = Main.Rebugger.getstored("57d9ebc0-0def-11e9-2ab0-e5d1e4c6e82d") begin _parse_colorant(desc) end end ``` You can hit the down arrow and go back to earlier entries in the trace. Alternatively, you can pick any of these expressions to execute (hit Enter) or edit before execution. You can use the REPL history to test the results of many different changes to the same "method"; the "method" will be run with the same inputs each time. !!! note When point is at the end of the input, the up and down arrows step through the history. But if you move point into the method body (e.g., by using left-arrow), the up and down arrows move within the method body. If you've entered edit mode, you can go back to history mode using PgUp and PgDn. ### Important notes #### "Missing" methods from stacktraces Sometimes, there's a large difference between the "real" stacktrace and the "captured" stacktrace: ```julia julia> using Pkg julia> Pkg.add("NoPkg") Updating registry at `~/.julia/registries/General` Updating git-repo `https://github.com/JuliaRegistries/General.git` ERROR: The following package names could not be resolved: * NoPkg (not found in project, manifest or registry) Please specify by known `name=uuid`. Stacktrace: [1] pkgerror(::String) at /home/tim/src/julia-1.0/usr/share/julia/stdlib/v1.0/Pkg/src/Types.jl:120 [2] #ensure_resolved#42(::Bool, ::Function, ::Pkg.Types.EnvCache, ::Array{Pkg.Types.PackageSpec,1}) at /home/tim/src/julia-1.0/usr/share/julia/stdlib/v1.0/Pkg/src/Types.jl:890 [3] #ensure_resolved at ./none:0 [inlined] [4] #add_or_develop#13(::Symbol, ::Bool, ::Base.Iterators.Pairs{Union{},Union{},Tuple{},NamedTuple{(),Tuple{}}}, ::Function, ::Pkg.Types.Context, ::Array{Pkg.Types.PackageSpec,1}) at /home/tim/src/julia-1.0/usr/share/julia/stdlib/v1.0/Pkg/src/API.jl:59 [5] #add_or_develop at ./none:0 [inlined] [6] #add_or_develop#12 at /home/tim/src/julia-1.0/usr/share/julia/stdlib/v1.0/Pkg/src/API.jl:29 [inlined] [7] #add_or_develop at ./none:0 [inlined] [8] #add_or_develop#11(::Base.Iterators.Pairs{Symbol,Symbol,Tuple{Symbol},NamedTuple{(:mode,),Tuple{Symbol}}}, ::Function, ::Array{String,1}) at /home/tim/src/julia-1.0/usr/share/julia/stdlib/v1.0/Pkg/src/API.jl:28 [9] #add_or_develop at ./none:0 [inlined] [10] #add_or_develop#10 at /home/tim/src/julia-1.0/usr/share/julia/stdlib/v1.0/Pkg/src/API.jl:27 [inlined] [11] #add_or_develop at ./none:0 [inlined] [12] #add#18 at /home/tim/src/julia-1.0/usr/share/julia/stdlib/v1.0/Pkg/src/API.jl:69 [inlined] [13] add(::String) at /home/tim/src/julia-1.0/usr/share/julia/stdlib/v1.0/Pkg/src/API.jl:69 [14] top-level scope at none:0 julia> Pkg.add("NoPkg") # hit Meta-s here [1] pkgerror(msg::String...) in Pkg.Types at /home/tim/src/julia-1/usr/share/julia/stdlib/v1.1/Pkg/src/Types.jl:120 [2] #ensure_resolved#72(registry::Bool, ::Any, env::Pkg.Types.EnvCache, pkgs::AbstractArray{Pkg.Types.PackageSpec,1}) in Pkg.Types at /home/tim/src/julia-1/usr/share/julia/stdlib/v1.1/Pkg/src/Types.jl:981 [3] #add_or_develop#15(mode::Symbol, shared::Bool, kwargs, ::Any, ctx::Pkg.Types.Context, pkgs::Array{Pkg.Types.PackageSpec,1}) in Pkg.API at /home/tim/src/julia-1/usr/share/julia/stdlib/v1.1/Pkg/src/API.jl:34 [4] #add_or_develop#12(kwargs, ::Any, pkg::Union{AbstractString, PackageSpec}) in Pkg.API at /home/tim/src/julia-1/usr/share/julia/stdlib/v1.1/Pkg/src/API.jl:28 [5] add(args...) in Pkg.API at /home/tim/src/julia-1/usr/share/julia/stdlib/v1.1/Pkg/src/API.jl:59 pkgerror(msg::String...) in Pkg.Types at /home/tim/src/julia-1/usr/share/julia/stdlib/v1.1/Pkg/src/Types.jl:120 msg = ("The following package names could not be resolved:\n * NoPkg (not found in project, manifest or registry)\nPlease specify by known `name=uuid`.",) rebug> @eval Pkg.Types let (msg,) = Main.Rebugger.getstored("1ac42628-4b15-11e9-28e7-33f71870bf31") begin throw(PkgError(join(msg))) end end ``` Note that only five methods got captured but the stacktrace is much longer. Most of these methods, however, say "inlined" with line number 0. Rebugger has no way of finding such methods. However, you can enter (i.e., Meta-e) such methods from one that is higher in the stack trace. #### Modified "signatures" Some "methods" you see in the `let` block on the command line will have their "signatures" slightly modified. For example: ```julia julia> dest = zeros(3); julia> copyto!(dest, 1:4) ERROR: BoundsError: attempt to access 3-element Array{Float64,1} at index [1, 2, 3, 4] Stacktrace: [1] copyto!(::IndexLinear, ::Array{Float64,1}, ::IndexLinear, ::UnitRange{Int64}) at ./abstractarray.jl:728 [2] copyto!(::Array{Float64,1}, ::UnitRange{Int64}) at ./abstractarray.jl:723 [3] top-level scope at none:0 julia> copyto!(dest, 1:4) # hit Meta-s here Captured elements of stacktrace: [1] copyto!(::IndexStyle, dest::AbstractArray, ::IndexStyle, src::AbstractArray) in Base at abstractarray.jl:727 [2] copyto!(dest::AbstractArray, src::AbstractArray) in Base at abstractarray.jl:723 copyto!(::IndexStyle, dest::AbstractArray, ::IndexStyle, src::AbstractArray) in Base at abstractarray.jl:727 __IndexStyle_1 = IndexLinear() dest = [0.0, 0.0, 0.0] __IndexStyle_2 = IndexLinear() src = 1:4 rebug> @eval Base let (__IndexStyle_1, dest, __IndexStyle_2, src) = Main.Rebugger.getstored("21a8ab94-a228-11e8-0563-256e39b3996e") begin (destinds, srcinds) = (LinearIndices(dest), LinearIndices(src)) isempty(srcinds) || (checkbounds(Bool, destinds, first(srcinds)) && checkbounds(Bool, destinds, last(srcinds)) || throw(BoundsError(dest, srcinds))) @inbounds for i = srcinds dest[i] = src[i] end return dest end end ``` Note that this `copyto!` method contains two anonymous arguments annotated `::IndexStyle`. Rebugger will make up names for these arguments (here `__IndexStyle_1` and `__IndexStyle_2`). While these will be distinct from one another, Rebugger does not check whether they conflict with any internal names. !!! note This example illustrates a second important point: you may have noticed that this one was considerably slower to print. That's because capturing stacktraces overwrites the methods involved. Since `copyto!` is widely used, this forces recompilation of a lot of methods in Base. In contrast with capturing stacktraces, stepping in (Meta-e) does not overwrite methods, so is sometimes preferred. And of course, interpret mode (Meta-i) also doesn't overwrite methods. ================================================ FILE: src/Rebugger.jl ================================================ module Rebugger using UUIDs, InteractiveUtils using REPL import REPL.LineEdit, REPL.Terminals using REPL.LineEdit: buffer, bufend, content, edit_splice! using REPL.LineEdit: transition, terminal, mode, state using CodeTracking, Revise, JuliaInterpreter, HeaderREPLs using Revise: RelocatableExpr, striplines!, printf_maxsize, whichtt, hasfile, unwrap using JuliaInterpreter: FrameCode, scopeof using Base.Meta: isexpr using Core: CodeInfo # Reexports export @breakpoint, breakpoint, enable, disable, remove, break_on, break_off const msgs = [] # for debugging. The REPL-magic can sometimes overprint error messages include("debug.jl") include("ui.jl") include("printing.jl") include("deepcopy.jl") # Set up keys that enter rebug mode from the regular Julia REPL # This should be called from your ~/.julia/config/startup.jl file function repl_init(repl) repl.interface = REPL.setup_interface(repl; extra_repl_keymap = get_rebugger_modeswitch_dict()) end function rebugrepl_init() # Set up the Rebugger REPL mode with all of its key bindings repl_inited = isdefined(Base, :active_repl) while !isdefined(Base, :active_repl) sleep(0.05) end sleep(0.1) # for extra safety # Set up the custom "rebug" REPL iprompt, eprompt = rebugrepl_init(Base.active_repl, repl_inited) interpret_prompt_ref[] = iprompt rebug_prompt_ref[] = eprompt return nothing end function rebugrepl_init(main_repl, repl_inited) irepl = HeaderREPL(main_repl, InterpretHeader()) interface = REPL.setup_interface(irepl; extra_repl_keymap=Dict[]) iprompt = interface.modes[end] erepl = HeaderREPL(main_repl, RebugHeader()) interface = REPL.setup_interface(erepl; extra_repl_keymap=[get_rebugger_modeswitch_dict(), rebugger_keys]) eprompt = interface.modes[end] add_keybindings(main_repl; override=repl_inited, keybindings...) return iprompt, eprompt end function __init__() # schedule(Task(rebugrepl_init)) task = Task() do try rebugrepl_init() catch exception @error "Rebugger initialization failed" exception=(exception, catch_backtrace()) end end schedule(task) end include("precompile.jl") _precompile_() end # module ================================================ FILE: src/debug.jl ================================================ # Core debugging logic. # Hopefully someday much of this will be replaced by Gallium. const VarnameType = Tuple{Vararg{Union{Symbol,Expr}}} # Expr covers `foo(x, (a,b), y)` destructured-tuple signatures struct Stored method::Method varnames::VarnameType varvals function Stored(m, names, vals) new(m, names, safe_deepcopy(vals...)) end end const stashed = Ref{Any}(nothing) const stored = Dict{UUID,Stored}() # UUID => store data const storefunc = Dict{UUID,Function}() # UUID => function that puts inputs into `stored` const storemap = Dict{Tuple{Method,Bool},UUID}() # (method, overwrite) => UUID struct StopException <: Exception end struct StashingFailed <: Exception end # stashing saves the function and arguments from the caller (transient) struct StorageFailed <: Exception end # storage puts callee values into `stored` (lasts until `stored` is cleared) struct DefMissing <: Exception method::Method exception end struct SignatureError <: Exception method::Method end struct StepException <: Exception msg::String end struct EvalException <: Exception exprstring exception end const base_prefix = '.' * Base.Filesystem.path_separator """ Rebugger.clear() Clear internal data. This deletes storage associated with stored variables, but also forces regeneration of capture methods, which can be handy while debugging Rebugger itself. """ function clear() stashed[] = nothing empty!(stored) empty!(storefunc) empty!(storemap) nothing end ### Stacktraces """ r = linerange(expr, offset=0) Compute the range of lines occupied by `expr`. Returns `nothing` if no line statements can be found. """ function linerange(def::Expr) start = findline(def) stop = findline(def, Iterators.reverse) start !== nothing && stop !== nothing && return start.line:stop.line return nothing end function findline(ex, order) isa(ex, Expr) || return nothing for a in order(ex.args) a isa LineNumberNode && return a if a isa Expr ln = findline(a, order) ln !== nothing && return ln end end return nothing end findline(ex) = findline(ex, identity) """ usrtrace, defs = pregenerated_stacktrace(trace, topname=:capture_stacktrace) Generate a list of methods `usrtrace` and their corresponding definition-expressions `defs` from a stacktrace. Not all methods can be looked up, but this attempts to resolve, e.g., keyword-handling methods and so on. """ function pregenerated_stacktrace(trace; topname = :capture_stacktrace) usrtrace, defs = Method[], Expr[] methodsused = Set{Method}() # When the method can't be found directly in the tables, # look it up by fie and line number function add_by_file_line(defmap, line) for (rdef, sigts) in defmap def = rdef.ex (sigts === nothing || isempty(sigts)) && continue r = linerange(def) # FIXME offset if line ∈ r m = whichtt(last(sigts)) if m ∉ methodsused push!(defs, def) push!(usrtrace, m) push!(methodsused, m) return true end end end return false end function add_by_file_line(pkgdata, file, line) fi = Revise.maybe_parse_from_cache!(pkgdata, relpath(file, pkgdata)) for (mod, exsigs) in fi.modexsigs add_by_file_line(exsigs, line) && return true end return false end for (i, sf) in enumerate(trace) (sf.func == topname || sf.func == Symbol("top-level scope")) && break # truncate at the chosen spot sf.func ∈ notrace && continue mi = sf.linfo file = String(sf.file) if mi isa Core.MethodInstance method = mi.def def = definition(method) if def === nothing # This may be a generated method, perhaps it's a keyword function handler # Look for it by line number local id try id = Revise.get_tracked_id(method.module) catch # Methods from Core.Compiler cause errors on Julia binaries continue end id === nothing && continue pkgdata = Revise.pkgdatas[id] cfile = get(Revise.src_file_key, file, file) rpath = relpath(cfile, pkgdata) hasfile(pkgdata, rpath) || continue fi = Revise.maybe_parse_from_cache!(pkgdata, rpath) # fi = get(pkgdata.fileinfos, rpath, nothing) # if fi !== nothing # add_by_file_line(fi.fm[method.module].defmap, sf) # end else method ∈ methodsused && continue def isa Expr || continue push!(defs, def) push!(usrtrace, method) end else # This method was inlined and hence linfo was not available # Try to find it if startswith(file, base_prefix) # This is a file in Base or Core file = relpath(file, base_prefix) id = Revise.get_tracked_id(Base) pkgdata = Revise.pkgdatas[id] if hasfile(pkgdata, file) add_by_file_line(pkgdata, file, sf.line) && continue elseif startswith(file, "compiler") try id = Revise.get_tracked_id(Core.Compiler) catch # On Julia binaries Core.Compiler is not available continue end pkgdata = Revise.pkgdatas[id] add_by_file_line(pkgdata, relpath(file, pkgdata), sf.line) && continue end end # Try all loaded packages for (id, pkgdata) in Revise.pkgdatas if hasfile(pkgdata, file) add_by_file_line(pkgdata, relpath(file, pkgdata), sf.line) && break end end end end return usrtrace, defs end """ uuids = capture_stacktrace(mod, command) Execute `command` in module `mod`. `command` must throw an error. Then instrument the methods in the stacktrace so that their input variables are stored in `Rebugger.stored`. After storing the inputs, restore the original methods. Since this requires two `eval`s of `command`, usage should be limited to deterministic expressions that always result in the same call chain. """ function capture_stacktrace(mod::Module, command::Expr) errored = true trace = try Core.eval(mod, command) errored = false catch stacktrace(catch_backtrace()) end errored || error("$command did not throw an error") usrtrace, defs = pregenerated_stacktrace(trace) # Eliminate duplicates. Keyword-funcs and default-arg funcs can return the same def i = 1 while i < length(defs) if defs[i] == defs[i+1] deleteat!(defs, i) deleteat!(usrtrace, i) else i += 1 end end isempty(usrtrace) && error("failed to capture any elements of the stacktrace") println(stderr, "Captured elements of stacktrace:") show(stderr, MIME("text/plain"), usrtrace) length(unique(usrtrace)) == length(usrtrace) || @error "the same method appeared twice, not supported. Try stepping into the command." uuids = UUID[] capture_stacktrace!(uuids, usrtrace, defs) do Core.eval(mod, command) end uuids end capture_stacktrace(command::Expr) = capture_stacktrace(Main, command) function capture_stacktrace!(f::Function, uuids::Vector, usrtrace, defs) if isempty(usrtrace) # We've finished modifying all the methods, time to run the command try f() @warn "traced method did not throw an error" catch end return end method, def = usrtrace[end], defs[end] if def !== nothing uuid = method_capture_from_callee(method, def; overwrite=true) push!(uuids, uuid) end # Recurse up the stack until we get to the top... pop!(usrtrace) pop!(defs) capture_stacktrace!(f, uuids, usrtrace, defs) # ...after which it will call the erroring function and come back here. # Having come back, restore the original definition if def != nothing eval_noinfo(method.module, def) # unfortunately this doesn't restore the original method as a viable key to storemap end return uuids end ### Stepping function stepin(io) @assert Rebugger.stashed[] === nothing # Step 1: rewrite the command to stash the call function and its arguments. prepare_caller_capture!(io) capexpr, stop = Meta.parse(content(io), 1) try Core.eval(Main, capexpr) throw(StashingFailed()) catch err err isa StashingFailed && rethrow(err) if !(err isa StopException) throw(EvalException(content(io), err)) end end f, args, kwargs = Rebugger.stashed[] Rebugger.stashed[] = nothing # Step 2: determine which method is called, and if need be create a function # that captures all of the callee's inputs. (This allows us to capture default arguments, # keyword arguments, and type parameters.) method = which(f, Base.typesof(args...)) uuid = get(storemap, (method, false), nothing) if uuid === nothing uuid = method_capture_from_callee(method; overwrite=false) end # Step 3: execute the command to store the inputs. fcapture = storefunc[uuid] tv, decls = Base.arg_decl_parts(method) if !isempty(decls[1][1]) # This is a call-overloaded method, prepend the calling object args = (f, args...) end try Base.invokelatest(fcapture, args...; kwargs...) throw(StorageFailed()) # this should never happen catch err err isa StopException || rethrow(err) end return uuid, generate_let_command(method, uuid) end """ callexpr = prepare_caller_capture!(io) Given a buffer `io` representing a string and "point" (the seek position) set at a call expression, replace the call with one that stashes the function and arguments of the call. For example, if `io` has contents if x > 0.5 ^fcomplex(x, 2; kw1=1.1) where in the above `^` indicates `position(s)` ("point"), rewrite this as if x > 0.5 Main.Rebugger.stashed[] = (fcomplex, (x, 2), (kw1=1.1,)) throw(Rebugger.StopException()) (Keyword arguments do not affect dispatch and hence are not stashed.) Consequently, if this is `eval`ed and execution reaches "^", it causes the arguments of the call to be placed in `Rebugger.stashed`. `callexpr` is the original (unmodified) expression specifying the call, i.e., `fcomplex(x, 2; kw1=1.1)` in this case. This does the buffer-preparation for *caller* capture. For *callee* capture, see [`method_capture_from_callee`](@ref), and [`stepin`](@ref) which puts these two together. """ function prepare_caller_capture!(io) # for testing, needs to work on a normal IO object start = position(io) callstring, _ = stripsc(content(io, start=>bufend(io))) callexpr, len = Meta.parse(callstring, 1; raise=false) callexpr == nothing && throw(StepException("Got empty expression from $callstring")) isa(callexpr, Expr) || throw(StepException("Rebugger can only step into expressions, got $callexpr")) if callexpr.head == :error iend = len for i = 1:2 iend = prevind(callstring, iend) end callstring = callstring[1:iend] callexpr, len = Meta.parse(callstring, 1) end if callexpr.head == :tuple && !(startswith(callstring, "tuple") || startswith(callstring, "(")) # An expression like foo(bar(x)..., 1) where point is positioned at bar callexpr = callexpr.args[1] end if callexpr.head == :ref callexpr = Expr(:call, :getindex, callexpr.args...) elseif callexpr.head == :(=) && isa(callexpr.args[1], Expr) && callexpr.args[1].head == :ref ref, val = callexpr.args callexpr = Expr(:call, :setindex!, ref.args[1], val, ref.args[2:end]...) elseif (callexpr.head == :&& || callexpr.head == :||) && isa(callexpr.args[1], Expr) callexpr = callexpr.args[1] elseif callexpr.head == :... callexpr = callexpr.args[1] elseif callexpr.head == :do callexpr = Expr( :call, callexpr.args[1].args[1], # function name callexpr.args[2], # do block (anonymous function) callexpr.args[1].args[2:end]...) # other arguments end # Must be a call or broadcast ((callexpr.head == :call) | (callexpr.head == :.)) || throw(Meta.ParseError("point must be at a call expression, got $callexpr")) if callexpr.head == :call fname, args = callexpr.args[1], callexpr.args[2:end] else fname, args = :broadcast, [callexpr.args[1], callexpr.args[2].args...] end # In the edited callstring separate any kwargs now. They don't affect dispatch. kwargs = [] if length(args) >= 1 && isa(args[1], Expr) && args[1].head == :parameters # foo(x; kw1=1, ...) syntax (with the semicolon) append!(kwargs, popfirst!(args).args) end while !isempty(args) && isa(args[end], Expr) && args[end].head == :kw # foo(x, kw1=1, ...) syntax (with a comma, no semicolon) push!(kwargs, pop!(args)) end captureexpr = quote Main.Rebugger.stashed[] = ($fname, (($(args...)),), Main.Rebugger.kwstasher(; $(kwargs...))) throw(StopException()) end # Now insert this in place of the marked call # Unfortunately we have to convert to a string and there are scoping issues capturestr = string(captureexpr, '\n') regexunscoped = r"(?(s->"Rebugger."*s)) regexscoped = r"(?(s->"Main."*s)) edit_splice!(io, start=>start+len-1, capturestr) return callexpr end """ uuid = method_capture_from_callee(method; overwrite::Bool=false) Create a version of `method` that stores its inputs in `Main.Rebugger.stored`. For a method function fcomplex(x::A, y=1, z=""; kw1=3.2) where A<:AbstractArray{T} where T end if `overwrite=false`, this generates a new method function hidden_fcomplex(x::A, y=1, z=""; kw1=3.2) where A<:AbstractArray{T} where T Main.Rebugger.stored[uuid] = Main.Rebugger.Stored(fcomplex, (:x, :y, :z, :kw1, :A, :T), deepcopy((x, y, z, kw1, A, T))) throw(StopException()) end (If a `uuid` already exists for `method` from a previous call to `method_capture_from_callee`, it will simply be returned.) With `overwrite=true`, there are two differences: - it replaces `fcomplex` rather than defining `hidden_fcomplex` - rather than throwing `StopException`, it re-inserts `` after the line performing storage The returned `uuid` can be used for accessing the stored data. """ function method_capture_from_callee(method, def; overwrite::Bool=false) uuid = get(storemap, (method, overwrite), nothing) uuid != nothing && return uuid def = pop_annotations(def) sigr, body = def.args[1], def.args[2] sigr == nothing && throw(SignatureError(method)) sigex = convert(Expr, sigr) if sigex.head == :(::) sigex = sigex.args[1] # return type declaration end methname, argnames, kwnames, paramnames = signature_names!(sigex) # Check for call-overloading method, e.g., (obj::ObjType)(x, y...) = callerobj = nothing if methname isa Expr && methname.head == :(::) @assert length(methname.args) == 2 callerobj = methname argnames = (methname.args[1], argnames...) methname = methname.args[2] if methname isa Expr if methname.head == :curly methname = methname.args[1] else dump(methname) error("unexpected call-overloading type") end end end allnames = (argnames..., kwnames..., paramnames...) qallnames = QuoteNode.(allnames) uuid = uuid1() uuidstr = string(uuid) makepair = Pair # Needed for debugging into Core.Compiler, which has its own Pair storeexpr = :(Main.Rebugger.setstored!($makepair($uuidstr, Main.Rebugger.Stored($method, ($(qallnames...),), ($(allnames...),)) ) )) capture_body = overwrite ? quote $storeexpr $body end : quote $storeexpr throw(Main.Rebugger.StopException()) end capture_name = try _gensym(methname) catch nothing end capture_name == nothing && (dump(methname); dump(sigex); error("couldn't gensym")) # capture_name = _gensym(methname) mod = method.module head = def.head == :(=) ? :function : def.head # allows us to intercept macros capture_function = Expr(head, overwrite ? sigex : rename_method(sigex, capture_name, callerobj), capture_body) result = Core.eval(mod, capture_function) if !overwrite storefunc[uuid] = result end storemap[(method, overwrite)] = uuid return uuid end function method_capture_from_callee(method; kwargs...) Revise.get_def(method; modified_files=typeof(Revise.revision_queue)()) # update while ignoring mtime issues here def = definition(method) def == nothing && throw(DefMissing(method, nothing)) method_capture_from_callee(method, def; kwargs...) end function generate_let_command(method::Method, uuid::UUID) s = stored[uuid] @assert method == s.method argstring = '(' * join(s.varnames, ", ") * (length(s.varnames)==1 ? ",)" : ')') Revise.get_def(method; modified_files=String[]) # to avoid mtime updates body = convert(Expr, striplines!(unwrap(definition(method)).args[end])) return """ @eval $(method.module) let $argstring = Main.Rebugger.getstored(\"$uuid\") $body end""" end function generate_let_command(uuid::UUID) s = stored[uuid] generate_let_command(s.method, uuid) end """ args_and_types = Rebugger.getstored(uuid) Retrieve the values of stored arguments and type-parameters from the store specified `uuid`. This makes a copy of values, so as to be safe for repeated execution of methods that modify their inputs. """ getstored(uuidstr::AbstractString) = safe_deepcopy(Main.Rebugger.stored[UUID(uuidstr)].varvals...) function setstored!(p::Pair{S,Stored}) where S<:AbstractString uuidstr, val = p.first, p.second Main.Rebugger.stored[UUID(uuidstr)] = val end kwstasher(; kwargs...) = kwargs ### Utilities """ fname, argnames, kwnames, parameternames = signature_names!(sigex::Expr) Return the function name `fname` and names given to its arguments, keyword arguments, and parameters, as specified by the method signature-expression `sigex`. `sigex` will be modified if some of the arguments are unnamed. # Examples ```jldoctest; setup=:(using Rebugger) julia> Rebugger.signature_names!(:(complexargs(w::Ref{A}, @nospecialize(x::Integer), y, z::String=""; kwarg::Bool=false, kw2::String="", kwargs...) where A <: AbstractArray{T,N} where {T,N})) (:complexargs, (:w, :x, :y, :z), (:kwarg, :kw2, :kwargs), (:A, :T, :N)) julia> ex = :(myzero(::Float64)); # unnamed argument julia> Rebugger.signature_names!(ex) (:myzero, (:__Float64_1,), (), ()) julia> ex :(myzero(__Float64_1::Float64)) ``` """ function signature_names!(sigex::Expr) # TODO: add parameter names argname(s::Symbol) = s function argname(ex::Expr) if ex.head == :(...) && length(ex.args) == 1 # varargs function ex = ex.args[1] ex isa Symbol && return ex end (ex.head == :macrocall || ex.head == :meta) && return argname(ex.args[end]) # @nospecialize ex.head == :kw && return argname(ex.args[1]) # default arguments ex.head == :tuple && return ex # tuple-destructuring argument ex.head == :(::) || throw(ArgumentError(string("expected :(::) expression, got ", ex))) arg = ex.args[1] if length(ex.args) == 1 && (arg isa Symbol) # This argument has a type but no name return arg, true end if isa(arg, Expr) && arg.head == :curly if arg.args[1] == :Type # Argument of the form ::Type{T} return arg.args[2], false elseif arg.args[1] == :NamedTuple return :NamedTuple, true, arg end end return arg end paramname(s::Symbol) = s function paramname(ex::Expr) ex.head == :(<:) && return paramname(ex.args[1]) throw(ArgumentError(string("expected parameter expression, got ", ex))) end kwnames, parameternames = (), [] while sigex.head == :where parameternames = [paramname.(sigex.args[2:end])..., parameternames...] sigex = sigex.args[1] end name = sigex.args[1] offset = 1 if length(sigex.args) > 1 && isa(sigex.args[2], Expr) && sigex.args[2].head == :parameters # keyword arguments kwnames = tuple(argname.(sigex.args[2].args)...) offset += 1 end # Argnames. For any unnamed arguments we have to generate a name. empty!(usdict) argnames = Union{Symbol,Expr}[] for i = offset+1:length(sigex.args) arg = sigex.args[i] retname = argname(arg) if retname isa Tuple should_gen = retname[2] if should_gen # This argument is missing a real name argt = length(retname) == 3 ? retname[3] : retname[1] name = genunderscored(retname[1]) sigex.args[i] = :($name::$argt) else # This is a ::Type{T} argument. We should remove this from the list of parameters name = retname[1] parameternames = filter(!isequal(name), parameternames) end retname = name end push!(argnames, retname) end return sigex.args[1], tuple(argnames...), kwnames, tuple(parameternames...) end function rename_method!(sig::Expr, name::Symbol, callerobj) ex = sig while isa(sig, Expr) && sig.head == :where sig = sig.args[1] end sig.head == :call || (dump(sig); throw(ArgumentError(string("expected call expression, got ", sig)))) sig.args[1] = name if callerobj != nothing # Call overloading, add an argument sig.args = [sig.args[1]; callerobj; sig.args[2:end]] end return ex end rename_method(sig::Expr, name::Symbol, callerobj) = rename_method!(copy(sig), name, callerobj) function stripsc(str) str = chomp(str) display_result = false if endswith(str, ';') str = str[1:end-1] display_result = true end return str, display_result end const poppable_macro = (Symbol("@inline"), Symbol("@noinline"), Symbol("@propagate_inbounds"), Symbol("@eval"), Symbol("@pure")) is_poppable_macro(ex) = ex ∈ poppable_macro || (ex isa Expr && ex.head == :. && ex.args[1] == :Base && ex.args[2].value ∈ poppable_macro) function pop_annotations(def::Expr) def = unwrap(def) while def isa Expr && def.head == :macrocall && is_poppable_macro(def.args[1]) def = def.args[end] def = unwrap(def) end def end # Use to re-evaluate an expression without leaving "breadcrumbs" about where # the eval is coming from. This is used below to prevent the re-evaluaton of an # original method from being attributed to Rebugger itself in future backtraces. eval_noinfo(mod::Module, ex::Expr) = ccall(:jl_toplevel_eval, Any, (Any, Any), mod, ex) eval_noinfo(mod::Module, rex::RelocatableExpr) = eval_noinfo(mod, convert(Expr, rex)) function unquote(ex::Expr) if ex.head == :quote return Expr(:block, ex.args...) end ex end unquote(rex::RelocatableExpr) = unquote(convert(Expr, rex)) _gensym(sym::Symbol) = gensym(sym) _gensym(q::QuoteNode) = _gensym(q.value) _gensym(ex::Expr) = (@assert ex.head == :. && length(ex.args) == 2; _gensym(ex.args[2])) const usdict = Dict{Symbol,Int}() function genunderscored(sym::Symbol) n = get(usdict, sym, 0) + 1 usdict[sym] = n return Symbol("__"*String(sym)*'_'*string(n)) end const notrace = (:error, :throw) ================================================ FILE: src/deepcopy.jl ================================================ # Because `deepcopy(mod::Module)` throws an error, we need a safe approach. # Strategy: wrap the IdDict so that our methods get called rather than Base's. # It's not guaranteed to work for user types that specialize `deepcopy_internal`, # but hopefully that's rare. struct WrappedIdDict dict::IdDict end Base.getindex(w::WrappedIdDict, key) = w.dict[key] Base.setindex!(w::WrappedIdDict, val, key) = w.dict[key] = val Base.haskey(w::WrappedIdDict, k) = haskey(w.dict, k) function safe_deepcopy(a, args...) stackdict = WrappedIdDict(IdDict()) _safe_deepcopy(stackdict, a, args...) end safe_deepcopy() = () _safe_deepcopy(stackdict, a, args...) = (Base.deepcopy_internal(a, stackdict), _safe_deepcopy(stackdict, args...)...) _safe_deepcopy(stackdict) = () # This is the one method we want to override Base.deepcopy_internal(x::Module, stackdict::WrappedIdDict) = x # But the rest are necessary to make it work. This is just a direct copy from Base. Base.deepcopy_internal(x::Union{Symbol,Core.MethodInstance,Method,GlobalRef,DataType,Union,Task}, stackdict::WrappedIdDict) = x Base.deepcopy_internal(x::Tuple, stackdict::WrappedIdDict) = ntuple(i->Base.deepcopy_internal(x[i], stackdict), length(x)) function Base.deepcopy_internal(x::Base.SimpleVector, stackdict::WrappedIdDict) if haskey(stackdict, x) return stackdict[x] end y = Core.svec(Any[Base.deepcopy_internal(x[i], stackdict) for i = 1:length(x)]...) stackdict[x] = y return y end function Base.deepcopy_internal(x::String, stackdict::WrappedIdDict) if haskey(stackdict, x) return stackdict[x] end y = GC.@preserve x unsafe_string(pointer(x), sizeof(x)) stackdict[x] = y return y end function Base.deepcopy_internal(@nospecialize(x), stackdict::WrappedIdDict) T = typeof(x)::DataType nf = nfields(x) (isbitstype(T) || nf == 0) && return x if haskey(stackdict, x) return stackdict[x] end if T.mutable y = ccall(:jl_new_struct_uninit, Any, (Any,), T) stackdict[x] = y for i in 1:nf if isdefined(x,i) ccall(:jl_set_nth_field, Cvoid, (Any, Csize_t, Any), y, i-1, Base.deepcopy_internal(getfield(x,i), stackdict)) end end else flds = Vector{Any}(undef, nf) for i in 1:nf if isdefined(x, i) xi = getfield(x, i) xi = Base.deepcopy_internal(xi, stackdict)::typeof(xi) flds[i] = xi else nf = i - 1 # rest of tail must be undefined values break end end y = ccall(:jl_new_structv, Any, (Any, Ptr{Any}, UInt32), T, flds, nf) end return y::T end function Base.deepcopy_internal(x::Array, stackdict::WrappedIdDict) if haskey(stackdict, x) return stackdict[x] end _deepcopy_array_t(x, eltype(x), stackdict) end function _deepcopy_array_t(@nospecialize(x), T, stackdict::WrappedIdDict) if isbitstype(T) return (stackdict[x]=copy(x)) end dest = similar(x) stackdict[x] = dest for i = 1:(length(x)::Int) if ccall(:jl_array_isassigned, Cint, (Any, Csize_t), x, i-1) != 0 xi = ccall(:jl_arrayref, Any, (Any, Csize_t), x, i-1) if !isbits(xi) xi = Base.deepcopy_internal(xi, stackdict) end ccall(:jl_arrayset, Cvoid, (Any, Any, Csize_t), dest, xi, i-1) end end return dest end function Base.deepcopy_internal(x::Union{Dict,IdDict}, stackdict::WrappedIdDict) if haskey(stackdict, x) return stackdict[x]::typeof(x) end if isbitstype(eltype(x)) return (stackdict[x] = copy(x)) end dest = empty(x) stackdict[x] = dest for (k, v) in x dest[Base.deepcopy_internal(k, stackdict)] = Base.deepcopy_internal(v, stackdict) end dest end ================================================ FILE: src/precompile.jl ================================================ function _precompile_() ccall(:jl_generating_output, Cint, ()) == 1 || return nothing precompile(Tuple{typeof(get_rebugger_modeswitch_dict)}) precompile(Tuple{typeof(rebugrepl_init)}) end ================================================ FILE: src/printing.jl ================================================ # A type for keeping track of the current line number when printing Exprs struct LineNumberIO <: IO io::IO linenos::Vector{Union{Missing,Int}} # source line number for each printed line of the Expr file::Symbol # used to avoid confusion when we are in expanded macros end LineNumberIO(io::IO, line::Integer, file::Symbol) = LineNumberIO(io, Union{Missing,Int}[line], file) LineNumberIO(io::IO, line::Integer, file::AbstractString) = LineNumberIO(io, line, Symbol(file)) const LNIO = Union{LineNumberIO, IOContext{LineNumberIO}} # Instead of printing the source line number to `io.io`, associate it with the # corresponding line of the printout function Base.show_linenumber(io::LineNumberIO, line, file) if file == io.file # Count how many newlines we've encountered data = io.io.data nlines = count(isequal(UInt8('\n')), data) + 1 # TODO: O(N^2), optimize? See below # If there have been more printed lines than assigned line numbers, fill # with `missing` while nlines > length(io.linenos) push!(io.linenos, missing) end # Record this line number io.linenos[nlines] = line end return nothing end Base.show_linenumber(io::LNIO, line, file) = Base.show_linenumber(io.io, line, file) Base.show_linenumber(io::LNIO, line, ::Nothing) = nothing Base.show_linenumber(io::LNIO, line) = nothing # TODO? intercept `\n` here and break the result up into lines at writing time? Base.write(io::LineNumberIO, x::UInt8) = write(io.io, x) # See docstring below function expression_lines(method::Method) def = definition(method) if def === nothing # If the expression is not available, use the source text. This happens for methods in e.g., boot.jl. src, line1 = definition(String, method) methstrings = split(chomp(src), '\n') return Vector(range(Int(line1), length=length(methstrings))), line1, methstrings end def = unwrap(def) # We'll use the file in LineNumberNodes to make sure line numbers refer to the "outer" # method (and does not get confused by macros etc). Because of symlinks and non-normalized paths, # it's more reliable to grab the first LNN for the template filename than to use method.file. lnn = findline(def) mfile = lnn === nothing ? method.file : lnn.file buf = IOBuffer() io = LineNumberIO(buf, method.line, mfile) # deliberately using the in-method numbering print(io, def) seek(buf, 0) methstrings = readlines(buf) linenos = io.linenos while length(linenos) < length(methstrings) push!(linenos, missing) end if startswith(methstrings[1], ":(") # Chop off the Expr-quotes from the printing methstrings[1] = methstrings[1][3:end] methstrings[end] = methstrings[end][1:end-1] end # If it prints with `function`, adjust numbering for the signature line # Note that this works independently of whether it's written as an `=` method # in the source code (the contents of that line may not be the same but that's # true quite generally) startswith(methstrings[1], "function") && (linenos[1] -= 1) @assert issorted(skipmissing(linenos)) # Strip out blank lines from the printed expression # These arise from the fact that we intercepted the printing of LineNumberNodes keeplinenos, keepstrings = Union{Missing,Int}[], String[] for (i, line) in enumerate(methstrings) if !all(isspace, line) push!(keepstrings, line) ln = linenos[i] if ismissing(ln) && i > 1 # Line numbers get associated with the missing LineNumberNodes rather than # the succeeding expression line. Thus we look to the previous entry. # Deliberately go back only one entry. ln = linenos[i-1] end push!(keeplinenos, ln) end end linenos, methstrings = keeplinenos, keepstrings # Fill in missing lines where possible. There is no line info for things like # `end`, `catch` and similar; these are subsumed by the block structure. # So we try to assign line numbers to these missing elements. # However, source text like # for i = 1:5 s += i end # and # for i = 1:5 s += i # end # and # for i = 1:5 # s +=i # end # all get printed in the latter format---so the matching will work only under # certain conditions. lastknown = 1 for i = 2:length(linenos)-1 if ismissing(linenos[i]) if !ismissing(linenos[i+1]) # Process the previous block of missing statements # If the printout has advanced by the same number of lines as the source, # we know what the answer must be. Δidx = i+1 - lastknown Δsrc = linenos[i+1] - linenos[lastknown] if Δsrc == Δidx for j = lastknown+1:i linenos[j] = linenos[j-1] + 1 end end end else lastknown = i end end _, line1 = whereis(method) return linenos, line1, keepstrings end """ linenos, line1, methstrings = expression_lines(frame) Compute the source lines associated with each printed line of the expression associated with the method executed in `frame`. `methstrings` is a vector of strings, one per line of the expression. `linenos` is a vector in 1-to-1 correspondence with `methstrings`, containing either the *compiled* line number or `missing` if the line number is not available. `line1` contains the *actual* (current) line number of the first line of the method body. """ function expression_lines(frame::Frame) m = scopeof(frame) mlinenos, line1, msrc = expression_lines(m) isdefined(m, :generator) || return mlinenos, line1, msrc # The rest of this is specific to generated functions. # Call the generator to get the expression. First we have to build up the arguments. g = m.generator gg = g.gen vars = JuliaInterpreter.locals(frame) ggargs = [] # Static parameters come first for v in vars v.isparam || continue push!(ggargs, v.value) end # Slots are next. Naturally, the generator takes only their types, not their values. for v in vars v.isparam && continue push!(ggargs, JuliaInterpreter._Typeof(v.value)) end # Call the generator def = gg(ggargs...) # `def` contains just the body. Wrap in a `function` to ensure proper indentation, # and then print it. def = Expr(:function, :(generatedtmp()), def) buf = IOBuffer() print(buf, def) # there are no linenos, so no need for LineNumberIO seek(buf, 0) glines = readlines(buf) # Extract the signature line from `msrc`, and paste the body in gsrc = [msrc[1]; glines[2:end]] # Assign line numbers. Other than the first line, there *aren't* any, so use `missing` linenos = [mlinenos[1]; fill(missing, length(gsrc)-1)] return linenos, line1, gsrc end function show_code(term, frame, deflines, nlines) width = displaysize(term)[2] method = scopeof(frame) linenos, line1, showlines = deflines # linenos is in "compiled" numbering, line1 in "current" numbering offset = line1 - method.line # compiled + offset -> current known_linenos = skipmissing(linenos) nd = isempty(known_linenos) ? 0 : ndigits(offset + maximum(known_linenos)) line = linenumber_unexpanded(frame) # this is in "compiled" numbering # lineidx = searchsortedfirst(linenos, line) # Can't use searchsortedfirst with missing lineidx = 0 for (i, l) in enumerate(linenos) if !ismissing(l) && l >= line lineidx = i break end end idxrange = max(1, lineidx-2):min(length(linenos), lineidx+2) iochar = IOBuffer() for idx in idxrange thisline, codestr = linenos[idx], showlines[idx] print(term, breakpoint_style(frame.framecode, thisline)) if ismissing(thisline) print(term, " "^nd) else linestr = lpad(thisline + offset, nd) printstyled(term, linestr; color = thisline==line ? Base.warn_color() : :normal) end linestr = linetrunc(iochar, codestr, width-nd-3) print(term, " ", linestr, '\n') end return length(idxrange) end # Limit output to a single line function linetrunc(iochar::IO, linestr, width) nchars = 0 for c in linestr if nchars == width-2 print(iochar, '…') break else print(iochar, c) end nchars += 1 end return String(take!(iochar)) end linetrunc(linestr, width) = linetrunc(IOBuffer(), linestr, width) function breakpoint_style(framecode, thisline) rng = coderange(framecode, thisline) style = ' ' breakpoints = framecode.breakpoints for i in rng if isassigned(breakpoints, i) bps = breakpoints[i] if !bps.isactive if bps.condition === JuliaInterpreter.falsecondition # removed else style = style == ' ' ? 'd' : 'm' # disabled end else if bps.condition === JuliaInterpreter.truecondition style = style == ' ' ? 'b' : 'm' # unconditional else style = style == ' ' ? 'c' : 'm' # conditional end end end end return style end breakpoint_style(framecode, ::Missing) = ' ' ### Header display function HeaderREPLs.print_header(io::IO, header::RebugHeader) if header.nlines != 0 HeaderREPLs.clear_header_area(io, header) end iocount = IOBuffer() # for counting lines for s in (io, iocount) if !isempty(header.warnmsg) printstyled(s, header.warnmsg, '\n'; color=Base.warn_color()) end if !isempty(header.errmsg) printstyled(s, header.errmsg, '\n'; color=Base.error_color()) end if header.current_method != dummymethod printstyled(s, header.current_method, '\n'; color=:light_magenta) end if header.uuid != dummyuuid data = stored[header.uuid] ds = displaysize(io) printer(args...) = printstyled(args..., '\n'; color=:light_blue) for (name, val) in zip(data.varnames, data.varvals) # Make sure each only spans one line if val === nothing val = "nothing" end try printf_maxsize(printer, s, " ", name, " = ", val; maxlines=1, maxchars=ds[2]-1) catch # don't error just because a print method is borked printstyled(s, " ", name, " errors in its show method"; color=:red) end end end end header.nlines = count_display_lines(iocount, displaysize(io)) header.warnmsg = "" header.errmsg = "" return nothing end function HeaderREPLs.print_header(io::IO, header::InterpretHeader) if header.nlines != 0 HeaderREPLs.clear_header_area(io, header) end header.frame == nothing && return nothing frame, Δ = frameoffset(header.frame, header.leveloffset) header.leveloffset -= Δ frame === nothing && return nothing iocount = IOBuffer() # for counting lines for s in (io, iocount) ds = displaysize(io) printer(args...) = printstyled(args..., '\n'; color=:light_blue) if !isempty(header.warnmsg) printstyled(s, header.warnmsg, '\n'; color=Base.warn_color()) end if !isempty(header.errmsg) printstyled(s, header.errmsg, '\n'; color=Base.error_color()) end indent = "" f = root(frame) while f !== nothing scope = scopeof(f) if f === frame printstyled(s, indent, scope, '\n'; color=:light_magenta, bold=true) else printstyled(s, indent, scope, '\n'; color=:light_magenta) end indent *= ' ' f = f.callee end for (i, var) in enumerate(JuliaInterpreter.locals(frame)) name, val = var.name, var.value name == Symbol("#self#") && (isa(val, Type) || sizeof(val) == 0) && continue name == Symbol("") && (name = "@_" * string(i)) if val === nothing val = "nothing" end try printf_maxsize(printer, s, " ", name, " = ", val; maxlines=1, maxchars=ds[2]-1) catch # don't error just because a print method is borked printstyled(s, " ", name, " errors in its show method"; color=:red) end end end header.nlines = count_display_lines(iocount, displaysize(io)) header.warnmsg = "" header.errmsg = "" return nothing end function frameoffset(frame, offset) while offset > 0 cframe = frame.caller cframe === nothing && break frame = cframe offset -= 1 end return frame, offset end function linenumber_unexpanded(frame) framecode, pc = frame.framecode, frame.pc scope = framecode.scope::Method codeloc = JuliaInterpreter.codelocation(framecode.src, pc) codeloc == 0 && return nothing lineinfo = framecode.src.linetable[codeloc] while lineinfo.file != scope.file && codeloc > 0 codeloc -= 1 lineinfo = framecode.src.linetable[codeloc] end return JuliaInterpreter.getline(lineinfo) end ================================================ FILE: src/ui.jl ================================================ const rebug_prompt_string = "rebug> " const rebug_prompt_ref = Ref{Union{LineEdit.Prompt,Nothing}}(nothing) # set by __init__ const interpret_prompt_ref = Ref{Union{LineEdit.Prompt,Nothing}}(nothing) # set by __init__ # For debugging function logaction(msg) open("/tmp/rebugger.log", "a") do io println(io, "* ", msg) flush(io) end nothing end dummy() = nothing const dummymethod = first(methods(dummy)) const dummyuuid = UUID(UInt128(0)) uuidextractor(str) = match(r"getstored\(\"([a-z0-9\-]+)\"\)", str) mutable struct RebugHeader <: AbstractHeader warnmsg::String errmsg::String uuid::UUID current_method::Method nlines::Int # size of the printed header end RebugHeader() = RebugHeader("", "", dummyuuid, dummymethod, 0) mutable struct InterpretHeader <: AbstractHeader frame::Union{Nothing,Frame} leveloffset::Int val bt warnmsg::String errmsg::String nlines::Int # size of the printed header end InterpretHeader() = InterpretHeader(nothing, 0, nothing, nothing, "", "", 0) struct DummyAST end # fictive input for the put!/take! evaluation by the InterpretREPL backend header(s::LineEdit.MIState) = header(mode(s).repl) header(repl::HeaderREPL) = repl.header header(::LineEditREPL) = header(rebug_prompt_ref[].repl) # default to the Rebug header # Custom methods set_method!(header::RebugHeader, method::Method) = header.current_method = method function set_uuid!(header::RebugHeader, uuid::UUID) if haskey(stored, uuid) header.uuid = uuid header.current_method = stored[uuid].method else header.uuid = dummyuuid header.current_method = dummymethod end uuid end function set_uuid!(header::RebugHeader, str::AbstractString) m = uuidextractor(str) uuid = if m isa RegexMatch && length(m.captures) == 1 UUID(m.captures[1]) else dummyuuid end set_uuid!(header, uuid) end struct FakePrompt{Buf<:IO} input_buffer::Buf end LineEdit.mode(p::FakePrompt) = rebug_prompt_ref[] """ stepin(s) Given a buffer `s` representing a string and "point" (the seek position) set at a call expression, replace the contents of the buffer with a `let` expression that wraps the *body* of the callee. For example, if `s` has contents if x > 0.5 ^fcomplex(x) where in the above `^` indicates `position(s)` ("point"), and if the definition of `fcomplex` is function fcomplex(x::A, y=1, z=""; kw1=3.2) where A<:AbstractArray{T} where T end rewrite `s` so that its contents are @eval ModuleOf_fcomplex let (x, y, z, kw1, A, T) = Main.Rebugger.getstored(id) end where `Rebugger.getstored` returns has been pre-loaded with the values that would have been set when you called `fcomplex(x)` in `s` above. This line can be edited and `eval`ed at the REPL to analyze or improve `fcomplex`, or can be used for further `stepin` calls. """ function stepin(s::LineEdit.MIState) # Add the command we're tracing to the history. That way we can go "up the call stack". pos = position(s) cmd = String(take!(copy(LineEdit.buffer(s)))) add_history(s, cmd) # Analyze the command string and step in local uuid, letcmd try uuid, letcmd = stepin(LineEdit.buffer(s)) catch err repl = rebug_prompt_ref[].repl handled = false if err isa StashingFailed repl.header.warnmsg = "Execution did not reach point" handled = true elseif err isa Meta.ParseError || err isa StepException repl.header.warnmsg = "Expression at point is not a call expression" handled = true elseif err isa EvalException io = IOBuffer() showerror(io, err.exception) errstr = String(take!(io)) repl.header.errmsg = "$errstr while evaluating $(err.exprstring)" handled = true elseif err isa DefMissing repl.header.errmsg = "The expression for method $(err.method) was unavailable. Perhaps it was untracked or generated by code." handled = true end if handled buf = LineEdit.buffer(s) LineEdit.edit_clear(buf) write(buf, cmd) seek(buf, pos) return nothing end rethrow(err) end set_uuid!(header(s), uuid) LineEdit.edit_clear(s) LineEdit.edit_insert(s, letcmd) return nothing end function capture_stacktrace(s) if mode(s) isa LineEdit.PrefixHistoryPrompt # history search, re-enter with the corresponding mode LineEdit.accept_result(s, mode(s)) return capture_stacktrace(s) end cmdstring = LineEdit.content(s) add_history(s, cmdstring) print(REPL.terminal(s), '\n') expr = Meta.parse(cmdstring) local uuids try uuids = capture_stacktrace(expr) catch err print(stderr, err) return nothing end io = IOBuffer() buf = FakePrompt(io) hp = mode(s).hist for uuid in uuids println(io, generate_let_command(uuid)) REPL.add_history(hp, buf) take!(io) end hp.cur_idx = length(hp.history) + 1 if !isempty(uuids) set_uuid!(header(s), uuids[end]) print(REPL.terminal(s), '\n') LineEdit.edit_clear(s) LineEdit.enter_prefix_search(s, find_prompt(s, LineEdit.PrefixHistoryPrompt), true) end return nothing end function add_history(s, str::AbstractString) io = IOBuffer() buf = FakePrompt(io) hp = mode(s).hist println(io, str) REPL.add_history(hp, buf) end function interpret(s) hdr = header(s) hdr.bt = nothing term = REPL.terminal(s) cmdstring = LineEdit.content(s) isempty(cmdstring) && return :done add_history(s, cmdstring) assigns, expr, display_result = simplecall(cmdstring) tupleexpr = JuliaInterpreter.extract_args(Main, expr) callargs = Core.eval(Main, tupleexpr) # get the elements of the call callexpr = Expr(:call, callargs...) frame = JuliaInterpreter.enter_call_expr(callexpr) if frame === nothing local val try hdr.val = Core.eval(Main, callexpr) catch err hdr.val = err hdr.bt = catch_backtrace() end else # frame = JuliaInterpreter.maybe_step_through_wrapper!(frame) hdr.frame = frame deflines = expression_lines(frame) print(term, '\n') # to advance beyond the user's input line nlines = 0 try while true HeaderREPLs.clear_nlines(term, nlines) print_header(term, hdr) if hdr.leveloffset == 0 nlines = show_code(term, frame, deflines, nlines) else f, Δ = frameoffset(frame, hdr.leveloffset) hdr.leveloffset -= Δ nlines = show_code(term, f, expression_lines(f), nlines) end cmd = read(term, Char) if cmd == '?' hdr.warnmsg = """ Commands: space: next line enter: continue to next breakpoint or completion →: step in to next call ←: finish frame and return to caller ↑: display the caller frame ↓: display the callee frame b: insert breakpoint at current line c: insert conditional breakpoint at current line r: remove breakpoint at current line d: disable breakpoint at current line e: enable breakpoint at current line o: open current position in editor q: abort (returns nothing)""" elseif cmd == ' ' hdr.leveloffset = 0 ret = debug_command(frame, :n) if ret === nothing hdr.val = JuliaInterpreter.get_return(root(frame)) break end frame, deflines = refresh(frame, ret, deflines) elseif cmd == '\n' || cmd == '\r' hdr.leveloffset = 0 ret = debug_command(frame, :c) if ret === nothing hdr.val = JuliaInterpreter.get_return(root(frame)) break end frame, deflines = refresh(frame, ret, deflines) elseif cmd == 'q' hdr.val = nothing break elseif cmd == 'o' f, Δ = frameoffset(frame, hdr.leveloffset) file, line = whereis(f) edit(file, line) elseif cmd == '\e' # escape codes nxtcmd = read(term, Char) nxtcmd == "O" && (nxtcmd = '[') # normalize escape code cmd *= nxtcmd while nxtcmd == '[' nxtcmd = read(term, Char) cmd *= nxtcmd end if cmd == "\e[C" # right arrow hdr.leveloffset = 0 ret = debug_command(frame, :s) if ret === nothing # Trying to "step in" at program exit hdr.val = JuliaInterpreter.get_return(frame) break end frame, deflines = refresh(frame, ret, deflines) elseif cmd == "\e[D" # left arrow hdr.leveloffset = 0 ret = debug_command(frame, :finish) if ret === nothing hdr.val = JuliaInterpreter.get_return(root(frame)) break end frame, deflines = refresh(frame, ret, deflines) elseif cmd == "\e[A" # up arrow hdr.leveloffset += 1 elseif cmd == "\e[B" # down arrow hdr.leveloffset = max(0, hdr.leveloffset - 1) end elseif cmd == 'b' || cmd == 'c' if cmd == 'b' cond = nothing else print(term, "enter condition: ") condstr = "" c = read(term, Char) while c != '\n' && c != '\r' print(term, c) condstr *= c c = read(term, Char) end condex = Base.parse_input_line(condstr; filename="condition") cond = (JuliaInterpreter.moduleof(frame), condex) end ret = JuliaInterpreter.whereis(frame) if ret === nothing @warn "Failed to find location info at current statement" else file, line = ret JuliaInterpreter.breakpoint(file, line, cond) end elseif cmd == 'r' breakpoint_action(remove, frame) elseif cmd == 'd' breakpoint_action(disable, frame) elseif cmd == 'e' breakpoint_action(enable, frame) else push!(msgs, cmd) end hdr.frame = frame end catch err hdr.val = err hdr.bt = catch_backtrace() end end # Store the result repl = mode(s).repl if isdefined(repl, :backendref) response = (display_result ? hdr.val : nothing, VERSION >= v"1.2.0-DEV.249" ? hdr.bt !== nothing : hdr.bt) put!(repl.backendref.response_channel, response) end hdr.frame = nothing # Do this last in case of errors if assigns !== nothing && hdr.bt === nothing Core.eval(Main, Expr(:(=), assigns, hdr.val)) end return :done end function refresh(frame, ret, deflines) @assert ret !== nothing cframe, _ = ret if cframe === frame return frame, deflines end return cframe, expression_lines(cframe) end # Find the range of statement indexes that preceed a line # `thisline` should be in "compiled" numbering function coderange(src::CodeInfo, thisline) codeloc = searchsortedfirst(src.linetable, LineNumberNode(thisline, ""); by=x->x.line) if codeloc == 1 return 1:1 end idxline = searchsortedfirst(src.codelocs, codeloc) idxprev = searchsortedfirst(src.codelocs, codeloc-1) + 1 return idxprev:idxline end coderange(framecode::FrameCode, thisline) = coderange(framecode.src, thisline) function breakpoint_action(f, frame) ret = CodeTracking.whereis(frame) if ret === nothing @warn "Failed to find location info at current statement" else file, line = ret for bp in JuliaInterpreter.breakpoints() if bp isa JuliaInterpreter.BreakpointFileLocation && bp.path == file && bp.line == line f(bp) end end end return nothing end function simplecall(cmdstring) cmdstring = chomp(cmdstring) display_result = !endswith(cmdstring, ';') if !display_result cmdstring = cmdstring[1:end-1] end expr = Meta.parse(cmdstring) if expr.head == :(=) assigns = expr.args[1] expr = expr.args[2] else assigns = nothing end return assigns, expr, display_result end ### REPL modes function HeaderREPLs.setup_prompt(repl::HeaderREPL{RebugHeader}, hascolor::Bool) julia_prompt = find_prompt(repl.interface, "julia") prompt = REPL.LineEdit.Prompt( rebug_prompt_string; prompt_prefix = hascolor ? repl.prompt_color : "", prompt_suffix = hascolor ? (repl.envcolors ? Base.input_color : repl.input_color) : "", complete = julia_prompt.complete, on_enter = REPL.return_callback) prompt.on_done = HeaderREPLs.respond(repl, julia_prompt) do str Base.parse_input_line(str; filename="REBUG") end # hist will be handled automatically if repl.history_file is true # keymap_dict is separate return prompt, :rebug end function HeaderREPLs.append_keymaps!(keymaps, repl::HeaderREPL{RebugHeader}) julia_prompt = find_prompt(repl.interface, "julia") kms = [ trigger_search_keymap(repl), mode_termination_keymap(repl, julia_prompt), trigger_prefix_keymap(repl), REPL.LineEdit.history_keymap, REPL.LineEdit.default_keymap, REPL.LineEdit.escape_defaults, ] append!(keymaps, kms) end # To get it to parse the UUID whenever we move through the history, we have to specialize # this method function HeaderREPLs.activate_header(header::RebugHeader, p, s, termbuf, term) str = String(take!(copy(LineEdit.buffer(s)))) set_uuid!(header, str) end ## Interpret also requires a separate repl function HeaderREPLs.setup_prompt(repl::HeaderREPL{InterpretHeader}, hascolor::Bool) julia_prompt = find_prompt(repl.interface, "julia") prompt = REPL.LineEdit.Prompt( "interpret> "; # should never be shown prompt_prefix = hascolor ? repl.prompt_color : "", prompt_suffix = hascolor ? (repl.envcolors ? Base.input_color : repl.input_color) : "", complete = julia_prompt.complete, on_enter = REPL.return_callback) prompt.on_done = HeaderREPLs.respond(repl, julia_prompt) do str return DummyAST() end return prompt, :interpret end function HeaderREPLs.append_keymaps!(keymaps, repl::HeaderREPL{InterpretHeader}) # No keymaps return keymaps end function HeaderREPLs.activate_header(header::InterpretHeader, p, s, termbuf, term) end function Base.put!(c::Channel, input::Tuple{DummyAST,Int}) return nothing end const keybindings = Dict{Symbol,String}( :stacktrace => "\es", # Alt-s ("[s]tacktrace") :stepin => "\ee", # Alt-e ("[e]nter") :interpret => "\ei", # Alt-i ("[i]nterpret") ) const modeswitches = Dict{Any,Any}( :stacktrace => (s, o...) -> capture_stacktrace(s), :stepin => (s, o...) -> (stepin(s); enter_rebug(s)), :interpret => (s, o...) -> (enter_interpret(s); interpret(s)), ) function get_rebugger_modeswitch_dict() rebugger_modeswitch = Dict() for (action, keybinding) in keybindings rebugger_modeswitch[keybinding] = modeswitches[action] end rebugger_modeswitch end function add_keybindings(main_repl; override::Bool=false, kwargs...) history_prompt = find_prompt(main_repl.interface, LineEdit.PrefixHistoryPrompt) julia_prompt = find_prompt(main_repl.interface, "julia") rebug_prompt = find_prompt(main_repl.interface, "rebug") for (action, keybinding) in kwargs if !(action in keys(keybindings)) error("$action is not a supported action.") end if !(keybinding isa Union{String,Vector{String}}) error("Expected the value for $action to be a String or Vector{String}, got $keybinding instead") end if haskey(keybindings, action) keybindings[action] = keybinding end # PackageCompiler can cause these keys to be added twice (once during compilation and once by __init__), # so put these in a try/catch (issue #62) if action == :interpret try LineEdit.add_nested_key!(julia_prompt.keymap_dict, keybinding, modeswitches[action], override=override) catch end else # We need Any here because "cannot convert REPL.LineEdit.PrefixHistoryPrompt to an object of type REPL.LineEdit.Prompt" prompts = Any[julia_prompt, rebug_prompt] if action == :stacktrace push!(prompts, history_prompt) end for prompt in prompts if keybinding isa Vector for kb in keybinding try LineEdit.add_nested_key!(prompt.keymap_dict, kb, modeswitches[action], override=override) catch end end else try LineEdit.add_nested_key!(prompt.keymap_dict, keybinding, modeswitches[action], override=override) catch end end end end end end # These work only at the `rebug>` prompt const rebugger_keys = Dict{Any,Any}( ) ## REPL commands TODO?: ## "\em" (meta-m): create REPL line that populates Main with arguments to current method ## "\eS" (meta-S): save version at REPL to file? (a little dangerous, perhaps make it configurable as to whether this is on) ## F1 is "^[OP" (showvalues?), F4 is "^[OS" (showinputs?) enter_rebug(s) = mode_switch(s, rebug_prompt_ref[]) enter_interpret(s) = mode_switch(s, interpret_prompt_ref[]) function mode_switch(s, other_prompt) buf = copy(LineEdit.buffer(s)) LineEdit.edit_clear(s) LineEdit.transition(s, other_prompt) do LineEdit.state(s, other_prompt).input_buffer = buf end end # julia_prompt = find_prompt(main_repl.interface, "julia") # julia_prompt.keymap_dict['|'] = (s, o...) -> enter_count(s) ================================================ FILE: test/edit.jl ================================================ using Rebugger using Rebugger: StopException using Test, UUIDs, InteractiveUtils, REPL, Pkg, HeaderREPLs using REPL.LineEdit using Revise, Colors if !isdefined(Main, :RebuggerTesting) includet("testmodule.jl") # so the source code here gets loaded end const empty_kwvarargs = Rebugger.kwstasher() uuidextractor(str) = UUID(match(r"getstored\(\"([a-z0-9\-]+)\"\)", str).captures[1]) struct ErrorsOnShow end Base.show(io::IO, ::ErrorsOnShow) = throw(ArgumentError("no show")) const SPACE = VERSION < v"1.5.0-DEV.0" ? "" : " " # maybe 1.4.0-DEV.537? most likely 1.4.0-DEV.604 @testset "Rebugger" begin id = uuid1() @test uuidextractor("vars = getstored(\"$id\") and more stuff") == id @testset "Debug core" begin @testset "Deepcopy" begin args = (3.2, rand(3,3), Rebugger, [Rebugger], "hello", sum, (2,3)) argc = Rebugger.safe_deepcopy(args...) @test argc == args end @testset "Signatures" begin @test Rebugger.signature_names!(:(f(x::Int, @nospecialize(y::String)))) == (:f, (:x, :y), (), ()) @test Rebugger.signature_names!(:(f(x::Int, $(Expr(:meta, :nospecialize, :(y::String)))))) == (:f, (:x, :y), (), ()) ex = :(f(::Type{T}, ::IndexStyle, x::Int, ::IndexStyle) where T) @test Rebugger.signature_names!(ex) == (:f, (:T, :__IndexStyle_1, :x, :__IndexStyle_2), (), ()) @test ex == :(f(::Type{T}, __IndexStyle_1::IndexStyle, x::Int, __IndexStyle_2::IndexStyle) where T) ex = :(f(Tuseless::Type{T}, ::IndexStyle, x::Int) where T) @test Rebugger.signature_names!(ex) == (:f, (:Tuseless, :__IndexStyle_1, :x), (), (:T,)) @test ex == :(f(Tuseless::Type{T}, __IndexStyle_1::IndexStyle, x::Int) where T) # issue #34 ex = :(_mapreduce_dim(f, op, ::NamedTuple{()}, A::AbstractArray, ::Colon)) @test Rebugger.signature_names!(ex) == (:_mapreduce_dim, (:f, :op, :__NamedTuple_1, :A, :__Colon_1), (), ()) @test ex == :(_mapreduce_dim(f, op, __NamedTuple_1::NamedTuple{()}, A::AbstractArray, __Colon_1::Colon)) end @testset "Caller buffer capture and insertion" begin function run_insertion(str, atstr) RebuggerTesting.cbdata1[] = RebuggerTesting.cbdata2[] = Rebugger.stashed[] = nothing io = IOBuffer() idx = findfirst(atstr, str) print(io, str) seek(io, first(idx)-1) callexpr = Rebugger.prepare_caller_capture!(io) capstring = String(take!(io)) capexpr = Meta.parse(capstring) try Core.eval(RebuggerTesting, capexpr) catch err isa(err, StopException) || rethrow(err) end end str = """ for i = 1:5 cbdata1[] = i foo(12, 13; akw="modified") cbdata2[] = i end """ @test run_insertion(str, "foo") @test RebuggerTesting.cbdata1[] == 1 @test RebuggerTesting.cbdata2[] == nothing @test Rebugger.stashed[] == (RebuggerTesting.foo, (12, 13), Rebugger.kwstasher(;akw="modified")) str = """ for i = 1:5 error("not caught") foo(12, 13; akw="modified") end """ @test_throws ErrorException("not caught") run_insertion(str, "foo") @test_throws Rebugger.StepException("Rebugger can only step into expressions, got 77") run_insertion("x = 77", "77") # Module-scoped calls io = IOBuffer() cmdstr = "Scope.func(x, y, z)" print(io, cmdstr) seek(io, 0) callexpr = Rebugger.prepare_caller_capture!(io) @test callexpr == :(Scope.func(x, y, z)) take!(io) # getindex and setindex! expressions cmdstr = "x = a[2,3]" print(io, cmdstr) seek(io, first(findfirst("a", cmdstr))-1) callexpr = Rebugger.prepare_caller_capture!(io) @test callexpr == :(getindex(a, 2, 3)) take!(io) cmdstr = "a[2,3] = x" print(io, cmdstr) seek(io, 0) callexpr = Rebugger.prepare_caller_capture!(io) @test callexpr == :(setindex!(a, x, 2, 3)) take!(io) # Expressions that go beyond "user intention". # More generally we should support marking, but in the case of && and || it's # handled by lowering, so there is nothing to step into anyway. for cmdstr in ("f1(x) && f2(z)", "f1(x) || f2(z)") print(io, cmdstr) seek(io, 0) callexpr = Rebugger.prepare_caller_capture!(io) @test callexpr == :(f1(x)) take!(io) end # issue #5 cmdstr = "abs(abs(x))" print(io, cmdstr) seek(io, 4) callexpr = Rebugger.prepare_caller_capture!(io) @test callexpr == :(abs(x)) take!(io) # splat expressions cmdstr = "foo(bar(x)..., 1)" print(io, cmdstr) idx = findfirst("bar", cmdstr) seek(io, first(idx)-1) callexpr = Rebugger.prepare_caller_capture!(io) @test callexpr == :(bar(x)) end @testset "Callee variable capture" begin def = quote function complexargs(x::A, y=1, str="1.0"; kw1=Float64, kw2=7, kwargs...) where A<:AbstractArray{T} where T return (x .+ y, parse(kw1, str), kw2) end end f = Core.eval(RebuggerTesting, def) @test f([8,9]) == ([9,10], 1.0, 7) m = collect(methods(f))[end] uuid = Rebugger.method_capture_from_callee(m, def) @test Rebugger.method_capture_from_callee(m, def) == uuid # calling twice returns the previously-defined objects fc = Rebugger.storefunc[uuid] @test_throws StopException fc([8,9], 2, "13"; kw1=Int, kw2=0) @test Rebugger.stored[uuid].varnames == (:x, :y, :str, :kw1, :kw2, :kwargs, :A, :T) @test Rebugger.stored[uuid].varvals == ([8,9], 2, "13", Int, 0, empty_kwvarargs, Vector{Int}, Int) @test_throws StopException fc([8,9]; otherkw=77) @test Rebugger.stored[uuid].varnames == (:x, :y, :str, :kw1, :kw2, :kwargs, :A, :T) @test Rebugger.stored[uuid].varvals == ([8,9], 1, "1.0", Float64, 7, pairs((otherkw=77,)), Vector{Int}, Int) uuid2 = Rebugger.method_capture_from_callee(m, def; overwrite=true) @test uuid2 != uuid # note overwriting methods are not stored in storefunc, but our old `f` will call the new method @test f([8,9], 2, "13"; kw1=Int, kw2=0) == ([10,11], 13, 0) Core.eval(RebuggerTesting, def) @test Rebugger.stored[uuid2].varnames == (:x, :y, :str, :kw1, :kw2, :kwargs, :A, :T) @test Rebugger.stored[uuid2].varvals == ([8,9], 2, "13", Int, 0, empty_kwvarargs, Vector{Int}, Int) def = quote @inline modifies!(x) = (x[1] += 1; x) end f = Core.eval(RebuggerTesting, def) @test f([8,9]) == [9,9] m = collect(methods(f))[end] uuid = Rebugger.method_capture_from_callee(m, def) fc = Rebugger.storefunc[uuid] @test_throws StopException fc([8,9]) @test Rebugger.stored[uuid].varnames == (:x,) @test Rebugger.stored[uuid].varvals == ([8,9],) # Extensions of functions from other modules m = @which RebuggerTesting.foo() uuid = Rebugger.method_capture_from_callee(m) fc = Rebugger.storefunc[uuid] @test_throws StopException fc() @test Rebugger.stored[uuid].varnames == Rebugger.stored[uuid].varvals == () end @testset "Step in" begin function run_stepin(str, atstr) io = IOBuffer() idx = findfirst(atstr, str) @test !isempty(idx) print(io, str) seek(io, first(idx)-1) Rebugger.stepin(io) end str = "RebuggerTesting.snoop0()" uuidref, cmd = run_stepin(str, str) uuid1 = uuidextractor(cmd) @test uuid1 == uuidref @test cmd == """ @eval Main.RebuggerTesting let () = Main.Rebugger.getstored("$uuid1") begin snoop1("Spy") end end""" _, cmd = run_stepin(cmd, "snoop1") uuid2 = uuidextractor(cmd) @test cmd == """ @eval Main.RebuggerTesting let (word,) = Main.Rebugger.getstored("$uuid2") begin snoop2(word, "on") end end""" _, cmd = run_stepin(cmd, "snoop2") uuid3 = uuidextractor(cmd) @test cmd == """ @eval Main.RebuggerTesting let (word1, word2) = Main.Rebugger.getstored("$uuid3") begin snoop3(word1, word2, "arguments") end end""" @test Rebugger.getstored(string(uuid1)) == () @test Rebugger.getstored(string(uuid2)) == ("Spy",) @test Rebugger.getstored(string(uuid3)) == ("Spy", "on") str = "RebuggerTesting.kwvarargs(1)" _, cmd = run_stepin(str, str) uuid = uuidextractor(cmd) @test cmd == """ @eval Main.RebuggerTesting let (x, kw1, kwargs) = Main.Rebugger.getstored("$uuid") begin kwvarargs2(x; kw1$(SPACE)=$(SPACE)kw1, kwargs...) end end""" @test Rebugger.getstored(string(uuid)) == (1, 1, empty_kwvarargs) cmd = run_stepin(cmd, "kwvarargs2") str = "RebuggerTesting.kwvarargs(1; passthrough=false)" _, cmd = run_stepin(str, str) uuid = uuidextractor(cmd) @test cmd == """ @eval Main.RebuggerTesting let (x, kw1, kwargs) = Main.Rebugger.getstored("$uuid") begin kwvarargs2(x; kw1$(SPACE)=$(SPACE)kw1, kwargs...) end end""" @test Rebugger.getstored(string(uuid)) == (1, 1, pairs((passthrough=false,))) _, cmd = run_stepin(cmd, "kwvarargs2") # Step in to call-overloading methods str = "RebuggerTesting.hv_test(\"hi\")" _, cmd = run_stepin(str, str) uuid = uuidextractor(cmd) @test cmd == """ @eval Main.RebuggerTesting let (hv, str) = Main.Rebugger.getstored("$uuid") begin hv.x end end""" @test Rebugger.getstored(string(uuid)) == (RebuggerTesting.hv_test, "hi") # Step in to methods that do tuple-destructuring of arguments str = "RebuggerTesting.destruct(1, (2,3), 4)" @test eval(Meta.parse(str)) == 2 _, cmd = run_stepin(str, str) uuid = uuidextractor(cmd) @test cmd == """ @eval Main.RebuggerTesting let (x, (a, b), y) = Main.Rebugger.getstored("$uuid") begin a end end""" @test Rebugger.getstored(string(uuid)) == (1, (2,3), 4) # Step in to a broadcast call str = "sum.([[1,2], (3,5)])" uuid, cmd = run_stepin(str, str) s = Rebugger.stored[uuid] @test s.method.name == :broadcast @test cmd == """ @eval Base.Broadcast let (f, As, Tf) = Main.Rebugger.getstored("$uuid") begin materialize(broadcasted(f, As...)) end end""" @test Rebugger.getstored(string(uuid)) == (sum, (Any[[1,2], (3,5)],), typeof(sum)) Core.eval(Main, Meta.parse(cmd)) == [3,8] str = "max.([1,5], [2,-3])" uuid, cmd = run_stepin(str, str) s = Rebugger.stored[uuid] @test s.method.name == :broadcast @test cmd == """ @eval Base.Broadcast let (f, As, Tf) = Main.Rebugger.getstored("$uuid") begin materialize(broadcasted(f, As...)) end end""" @test Rebugger.getstored(string(uuid)) == (max, ([1,5], [2,-3]), typeof(max)) Core.eval(Main, Meta.parse(cmd)) == [2,5] # Step in to a do block str = "RebuggerTesting.calldo()" uuidref, cmd = run_stepin(str, str) uuid1 = uuidextractor(cmd) @test uuid1 == uuidref @test cmd == """ @eval Main.RebuggerTesting let () = Main.Rebugger.getstored("$uuid1") begin apply(2, 3, 4) do x, y, z snoop3(x, y, z) end end end""" uuidref, cmd = run_stepin(cmd, "apply") uuid1 = uuidextractor(cmd) @test uuid1 == uuidref @test cmd == """ @eval Main.RebuggerTesting let (f, args) = Main.Rebugger.getstored("$uuid1") begin kwvarargs(f) f(args...) end end""" end @testset "Capture stacktrace" begin uuids = nothing mktemp() do path, iostacktrace redirect_stderr(iostacktrace) do uuids = Rebugger.capture_stacktrace(RebuggerTesting, :(snoop0())) end flush(iostacktrace) str = read(path, String) @test occursin("snoop3", str) end @test Rebugger.stored[uuids[1]].varvals == () @test Rebugger.stored[uuids[2]].varvals == ("Spy",) @test Rebugger.stored[uuids[3]].varvals == ("Spy", "on") @test Rebugger.stored[uuids[4]].varvals == ("Spy", "on", "arguments", "simply", empty_kwvarargs, String) @test_throws ErrorException("oops") RebuggerTesting.snoop0() st = try RebuggerTesting.kwfunctop(3) catch; stacktrace(catch_backtrace()) end usrtrace, defs = Rebugger.pregenerated_stacktrace(st; topname=Symbol("macro expansion")) @test length(unique(usrtrace)) == length(usrtrace) m = @which RebuggerTesting.kwfuncmiddle(1,1) @test m ∈ usrtrace # A case that tests inlining and several other aspects of argument capture ex = :([1, 2, 3] .* [1, 2]) # Capture the actual stack trace, trimming it to avoid # anything involving the `eval` itself trace = try Core.eval(Main, ex) catch stacktrace(catch_backtrace()) end i = 1 while i <= length(trace) t = trace[i] if t.func == Symbol("top-level scope") deleteat!(trace, i:length(trace)) end i += 1 end # Get the capture from Rebugger uuids = mktemp() do path, iostacktrace redirect_stderr(iostacktrace) do Rebugger.capture_stacktrace(Main, ex) end end @test length(uuids) == length(trace) for (uuid, t) in zip(reverse(uuids), trace) @test Rebugger.stored[uuid].method.name == t.func end # Try capturing a method from Core. On binaries this would throw # if we didn't catch it. # Because the first entry is "top-level scope", and that terminates # processing in Rebugger.pregenerated_stacktrace, we have to intervene a bit. mod, ex = Main, :(Core.throw(ArgumentError("oops"))) trace = try Core.eval(mod, command) catch err stacktrace(catch_backtrace()) end usrtrace, defs = Rebugger.pregenerated_stacktrace(trace[2:3]) @test usrtrace isa Vector end end @testset "User interface" begin @testset "Printing header" begin h = Rebugger.RebugHeader() h.uuid = uuid = uuid1() meth = @which RebuggerTesting.foo(1,2) h.current_method = meth Rebugger.stored[uuid] = Rebugger.Stored(meth, (:x, :y), (1, ErrorsOnShow())) h.warnmsg = "This is a warning" h.errmsg = "You will not have a second chance" io = IOBuffer() Rebugger.print_header(io, h) str = String(take!(io)) @test startswith(str, """ This is a warning You will not have a second chance foo(x, y) in Main.RebuggerTesting at """) # skip the "upper" part of the file location @test endswith(str, "testmodule.jl:7\n x = 1\n y errors in its show method") end @testset "Demos" begin function prepare_step_command(cmd, atstr) LineEdit.edit_clear(mistate) idx = findfirst(atstr, cmd) @test !isempty(idx) LineEdit.replace_line(mistate, cmd) buf = LineEdit.buffer(mistate) seek(buf, first(idx)-1) return mistate end function do_capture_stacktrace(cmd) l = length(hist.history) LineEdit.replace_line(mistate, cmd) Rebugger.capture_stacktrace(mistate) LineEdit.transition(mistate, julia_prompt) return l+1:length(hist.history) end if isdefined(Base, :active_repl) repl = Base.active_repl mistate = repl.mistate julia_prompt = find_prompt(mistate, "julia") LineEdit.transition(mistate, julia_prompt) hist = julia_prompt.hist header = Rebugger.rebug_prompt_ref[].repl.header histdel = 0 @testset "show demo" begin # this is a demo that appears in the documentation cmd1 = "show([1,2,4])" s = prepare_step_command(cmd1, cmd1) Rebugger.stepin(s) histdel += 1 uuid = header.uuid @test Rebugger.getstored(string(uuid)) == ([1,2,4],) cmd2 = LineEdit.content(s) s = prepare_step_command(cmd2, "show(stdout::IO, x)") Rebugger.stepin(s) histdel += 1 uuid = header.uuid @test Rebugger.getstored(string(uuid))[2] == [1,2,4] cmd3 = LineEdit.content(s) s = prepare_step_command(cmd3, "_show_empty") Rebugger.stepin(s) histdel += 1 @test header.warnmsg == "Execution did not reach point" end @testset "Colors demo" begin # another demo that appears in the documentation desc = "hsl(80%, 20%, 15%)" cmd = "colorant\"hsl(80%, 20%, 15%)\"" local idx mktemp() do path, io redirect_stderr(io) do logs, _ = Test.collect_test_logs() do idx = do_capture_stacktrace(cmd) end end flush(io) seek(io, 0) @test countlines(io) >= 4 end histdel += length(idx) @test length(idx) >= 5 @test hist.history[idx[1]] == cmd @test occursin("error", hist.history[idx[end]]) end @testset "Pkg demo" begin updated = Pkg.UPDATED_REGISTRY_THIS_SESSION[] Pkg.UPDATED_REGISTRY_THIS_SESSION[] = true uuids = Rebugger.capture_stacktrace(Pkg, :(add("NoPkg"))) @test length(uuids) >= 2 Pkg.UPDATED_REGISTRY_THIS_SESSION[] = updated end @testset "Empty stacktraces" begin cmd = "ccall(:jl_throw, Nothing, (Any,), ArgumentError(\"oops\"))" mktemp() do path, io redirect_stderr(io) do LineEdit.replace_line(mistate, cmd) @test Rebugger.capture_stacktrace(mistate) === nothing LineEdit.transition(mistate, julia_prompt) end flush(io) str = read(path, String) @test occursin("failed to capture", str) end end LineEdit.edit_clear(mistate) l = length(hist.history) deleteat!(hist.history, l-histdel+1:l) deleteat!(hist.modes, l-histdel+1:l) hist.cur_idx = length(hist.history)+1 end end end end ================================================ FILE: test/interpret.jl ================================================ using Rebugger, JuliaInterpreter using CodeTracking, Revise, Test, InteractiveUtils if !isdefined(Main, :fixable1) includet("interpret_script.jl") end @testset "Expression-printing and line numbers" begin for m in (@which(Tuple((1,2))), which(Base.show_vector, Tuple{IO,Any}), which(Rebugger.interpret, Tuple{Any}), ) def = definition(m) linenos, line1, methlines = Rebugger.expression_lines(m) @test length(linenos) == length(methlines) @test issorted(skipmissing(linenos)) @test maximum(skipmissing(linenos)) >= maximum(CodeTracking.linerange(def)) end # Line number fill-in for f in (fixable1, fixable2, fixable3) m = first(methods(f)) linenos, line1, methlines = Rebugger.expression_lines(m) @test count(ismissing, linenos) == 1 # only the final end is ambiguous end linenos, line1, methlines = Rebugger.expression_lines(first(methods(unfixable1))) @test count(ismissing, linenos) == 3 # Generated functions for ndims = 2:3 frame = JuliaInterpreter.enter_call(call_generated1, ndims) pc, n = frame.pc, JuliaInterpreter.nstatements(frame.framecode) while pc < n-1 frame, pc = debug_command(frame, :se) end frame, pc = debug_command(frame, :si) linenos, line1, methlines = Rebugger.expression_lines(frame) @test length(methlines) == 3 && strip(methlines[2]) == string(Expr(:tuple, ntuple(i->:val, ndims)...)) end # Unparsed methods frame = JuliaInterpreter.enter_call(getline, LineNumberNode(0, Symbol("fake.jl"))) frame, pc = debug_command(frame, :si) m = JuliaInterpreter.scopeof(frame) if m.file == Symbol("sysimg.jl") # sysimg.jl is excluded from Revise tracking linenos, line1, methlines = Rebugger.expression_lines(frame) @test linenos == [m.line] end # Internal macros (issue #63) frame = JuliaInterpreter.enter_call(f63) deflines = Rebugger.expression_lines(frame) frame, pc = debug_command(frame, :n) io = IOBuffer() Rebugger.show_code(io, frame, deflines, 0) str = String(take!(io)) @test occursin("y = 7", str) end ================================================ FILE: test/interpret_script.jl ================================================ # Test functions for parsing function fixable1(x) return x end fixable2(x) = x function fixable3(A) s = 0 fi = firstindex(A) for i in eachindex(A) for j in fi:i-1 s += A[j] end end return s end function unfixable1(A) s = 0 fi = firstindex(A) for i in eachindex(A) for j in fi:i-1 s += A[j] end end return s end # Generated functions @generated function generated1(A::AbstractArray{T,N}, val) where {T,N} ex = Expr(:tuple) for i = 1:N push!(ex.args, :val) end return ex end call_generated1(ndims) = generated1(fill(0, ntuple(d->1, ndims)...), 7) # getproperty is defined in sysimg.jl getline(lnn) = lnn.line function f63() x = 1 + 1 @info "hello" y = 7 end ================================================ FILE: test/interpret_ui.jl ================================================ # This was copied from Debugger.jl and then modified using TerminalRegressionTests, Rebugger, Revise, CodeTracking using HeaderREPLs, REPL using Test includet("my_gcd.jl") function run_terminal_test(cmd, validation, commands) function compare_replace(em, target; replace=nothing) # Compare two buffer, skipping over the equivalent of key=>rep replacement # However, because of potential differences in wrapping we don't explicitly # perform the replacement; instead, we make the comparison tolerant of difference # `\n`. buf = IOBuffer() decoratorbuf = IOBuffer() TerminalRegressionTests.VT100.dump(buf, decoratorbuf, em) outbuf = take!(buf) success = true if replace !== nothing output = String(outbuf) key, rep = replace idxkey = findfirst(key, target) iout, itgt = firstindex(output), firstindex(target) outlast, tgtlast = lastindex(output), lastindex(target) lrep = length(rep) while success && iout <= outlast && itgt <= tgtlast if itgt == first(idxkey) itgt += length(key) for c in rep cout = output[iout] while c != cout && cout == '\n' iout = nextind(output, iout) cout = output[iout] end if c != cout success = false break end iout = nextind(output, iout) end else cout, ctgt = output[iout], target[itgt] success = cout == ctgt iout, itgt = nextind(output, iout), nextind(target, itgt) end end success && iout > outlast && itgt > tgtlast && return true end outbuf == codeunits(target) && return true open("failed.out","w") do f write(f, output) end open("expected.out","w") do f write(f, target) end error("Test failed. Expected result written to expected.out, actual result written to failed.out") end dirpath = joinpath(@__DIR__, "ui", "v$(VERSION.major).$(VERSION.minor)") isdir(dirpath) || mkpath(dirpath) filepath = joinpath(dirpath, validation) # Fix the path of gcd to match the current running version of Julia gcdfile, gcdline = whereis(@which my_gcd(10, 20)) cmp(a, b, decorator) = compare_replace(a, b; replace="****" => gcdfile*':'*string(gcdline)) TerminalRegressionTests.automated_test(cmp, filepath, commands) do emuterm # TerminalRegressionTests.create_automated_test(filepath, commands) do emuterm main_repl = REPL.LineEditREPL(emuterm, true) main_repl.interface = REPL.setup_interface(main_repl) main_repl.specialdisplay = REPL.REPLDisplay(main_repl) main_repl.mistate = REPL.LineEdit.init_state(REPL.terminal(main_repl), main_repl.interface) iprompt, eprompt = Rebugger.rebugrepl_init(main_repl, true) repl = iprompt.repl s = repl.mistate s.current_mode = iprompt repl.t = emuterm REPL.LineEdit.edit_clear(s) REPL.LineEdit.edit_insert(s, cmd) Rebugger.interpret(s) end end CTRL_C = "\x3" EOT = "\x4" UP_ARROW = "\e[A" run_terminal_test("my_gcd(10, 20)", "gcd.multiout", ['\n']) run_terminal_test("__gcdval__ = my_gcd(10, 20);", "gcdsc.multiout", ['\n']) @test __gcdval__ == gcd(10, 20) ================================================ FILE: test/my_gcd.jl ================================================ # From base, but copied here to make sure we don't fail bacause base changed function my_gcd(a::T, b::T) where T<:Union{Int8,UInt8,Int16,UInt16,Int32,UInt32, Int64,UInt64,Int128,UInt128} a == 0 && return abs(b) b == 0 && return abs(a) za = trailing_zeros(a) zb = trailing_zeros(b) k = min(za, zb) u = unsigned(abs(a >> za)) v = unsigned(abs(b >> zb)) while u != v if u > v u, v = v, u end v -= u v >>= trailing_zeros(v) end r = u << k # T(r) would throw InexactError; we want OverflowError instead r > typemax(T) && throw(OverflowError("gcd($a, $b) overflows")) r % T end ================================================ FILE: test/runtests.jl ================================================ using Rebugger, Test @info "These tests manipulate the console. Wait until you see \"Done\"" include("edit.jl") include("interpret.jl") if Sys.isunix() && VERSION >= v"1.1.0" include("interpret_ui.jl") else @warn "Skipping UI tests" end println("Done") ================================================ FILE: test/testmodule.jl ================================================ module RebuggerTesting const cbdata1 = Ref{Any}(nothing) const cbdata2 = Ref{Any}(nothing) # Do not alter the line number at which `foo` occurs foo(x, y) = nothing snoop0() = snoop1("Spy") snoop1(word) = snoop2(word, "on") snoop2(word1, word2) = snoop3(word1, word2, "arguments") snoop3(word1, word2, word3::T; adv="simply", morekws...) where T = error("oops") kwvarargs(x; kw1=1, kwargs...) = kwvarargs2(x; kw1=kw1, kwargs...) kwvarargs2(x; kw1=0, passthrough=true) = (x, kw1, passthrough) destruct(x, (a, b), y) = a struct HasValue x::Float64 end const hv_test = HasValue(11.1) (hv::HasValue)(str::String) = hv.x @inline kwfuncerr(y) = error("stop") @noinline kwfuncmiddle(x::T, y::Integer=1; kw1="hello", kwargs...) where T = kwfuncerr(y) @inline kwfunctop(x; kwargs...) = kwfuncmiddle(x, 2; kwargs...) function apply(f, args...) kwvarargs(f) f(args...) end calldo() = apply(2, 3, 4) do x, y, z snoop3(x, y, z) end end module RBT2 using ..RebuggerTesting bar(::Int) = 5 RebuggerTesting.foo() = bar(1) end ================================================ FILE: test/ui/v1.0/gcd.multiout ================================================ ++++++++++++++++++++++++++++++++++++++++++++++++++ |gcd(10, 20) |gcd(a::T, b::T) where T<:Union{Int128, Int16, Int32, Int64, Int8, UInt128, UInt1 |6, UInt32, UInt64, UInt8} in Base at intfuncs.jl:31 | a = 10 | b = 20 | T = Int64 | 30 function gcd(a::T, b::T) where T <: Union{Int8, UInt8, Int16, UInt16, Int… | 31 a == 0 && return abs(b) | 32 b == 0 && return abs(a) | 33 za = trailing_zeros(a) | -------------------------------------------------- |AAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAA |AAAAAAAA |AAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA | ================================================ FILE: test/ui/v1.0/gcdsc.multiout ================================================ ++++++++++++++++++++++++++++++++++++++++++++++++++ |__gcdval__ = gcd(10, 20); |gcd(a::T, b::T) where T<:Union{Int128, Int16, Int32, Int64, Int8, UInt128, UInt1 |6, UInt32, UInt64, UInt8} in Base at intfuncs.jl:31 | a = 10 | b = 20 | T = Int64 | 30 function gcd(a::T, b::T) where T <: Union{Int8, UInt8, Int16, UInt16, Int… | 31 a == 0 && return abs(b) | 32 b == 0 && return abs(a) | 33 za = trailing_zeros(a) | -------------------------------------------------- |AAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAA |AAAAAAAA |AAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA | ================================================ FILE: test/ui/v1.1/gcd.multiout ================================================ ++++++++++++++++++++++++++++++++++++++++++++++++++ |gcd(10, 20) |gcd(a::T, b::T) where T<:Union{Int128, Int16, Int32, Int64, Int8, UInt128, UInt1 |6, UInt32, UInt64, UInt8} in Base at intfuncs.jl:31 | a = 10 | b = 20 | T = Int64 | 30 function gcd(a::T, b::T) where T <: Union{Int8, UInt8, Int16, UInt16, Int… | 31 a == 0 && return abs(b) | 32 b == 0 && return abs(a) | 33 za = trailing_zeros(a) | -------------------------------------------------- |AAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAA |AAAAAAAA |AAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA | ================================================ FILE: test/ui/v1.1/gcdsc.multiout ================================================ ++++++++++++++++++++++++++++++++++++++++++++++++++ |__gcdval__ = gcd(10, 20); |gcd(a::T, b::T) where T<:Union{Int128, Int16, Int32, Int64, Int8, UInt128, UInt1 |6, UInt32, UInt64, UInt8} in Base at intfuncs.jl:31 | a = 10 | b = 20 | T = Int64 | 30 function gcd(a::T, b::T) where T <: Union{Int8, UInt8, Int16, UInt16, Int… | 31 a == 0 && return abs(b) | 32 b == 0 && return abs(a) | 33 za = trailing_zeros(a) | -------------------------------------------------- |AAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAA |AAAAAAAA |AAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA | ================================================ FILE: test/ui/v1.2/gcd.multiout ================================================ ++++++++++++++++++++++++++++++++++++++++++++++++++ |gcd(10, 20) |gcd(a::T, b::T) where T<:Union{Int128, Int16, Int32, Int64, Int8, UInt128, UInt1 |6, UInt32, UInt64, UInt8} in Base at **** | a = 10 | b = 20 | T = Int64 | 30 function gcd(a::T, b::T) where T <: Union{Int8, UInt8, Int16, UInt16, Int… | 31 a == 0 && return abs(b) | 32 b == 0 && return abs(a) | 33 za = trailing_zeros(a) | -------------------------------------------------- |AAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAA |AAAAAAAA |AAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA | ================================================ FILE: test/ui/v1.2/gcdsc.multiout ================================================ ++++++++++++++++++++++++++++++++++++++++++++++++++ |__gcdval__ = gcd(10, 20); |gcd(a::T, b::T) where T<:Union{Int128, Int16, Int32, Int64, Int8, UInt128, UInt1 |6, UInt32, UInt64, UInt8} in Base at **** | a = 10 | b = 20 | T = Int64 | 30 function gcd(a::T, b::T) where T <: Union{Int8, UInt8, Int16, UInt16, Int… | 31 a == 0 && return abs(b) | 32 b == 0 && return abs(a) | 33 za = trailing_zeros(a) | -------------------------------------------------- |AAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAA |AAAAAAAA |AAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA | ================================================ FILE: test/ui/v1.3/gcd.multiout ================================================ ++++++++++++++++++++++++++++++++++++++++++++++++++ |my_gcd(10, 20) |my_gcd(a::T, b::T) where T<:Union{Int128, Int16, Int32, Int64, Int8, UInt128, UI |nt16, UInt32, UInt64, UInt8} in Main at **** | a = 10 | b = 20 | T = Int64 | 3 function my_gcd(a::T, b::T) where T <: Union{Int8, UInt8, Int16, UInt16, … | 4 a == 0 && return abs(b) | 5 b == 0 && return abs(a) | 6 za = trailing_zeros(a) | -------------------------------------------------- |AAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAA |AAAAAAAA |AAAAAAAA |AAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA | ================================================ FILE: test/ui/v1.3/gcdsc.multiout ================================================ ++++++++++++++++++++++++++++++++++++++++++++++++++ |__gcdval__ = my_gcd(10, 20); |my_gcd(a::T, b::T) where T<:Union{Int128, Int16, Int32, Int64, Int8, UInt128, UI |nt16, UInt32, UInt64, UInt8} in Main at **** | a = 10 | b = 20 | T = Int64 | 3 function my_gcd(a::T, b::T) where T <: Union{Int8, UInt8, Int16, UInt16, … | 4 a == 0 && return abs(b) | 5 b == 0 && return abs(a) | 6 za = trailing_zeros(a) | -------------------------------------------------- |AAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAA |AAAAAAAA |AAAAAAAA |AAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA | ================================================ FILE: test/ui/v1.5/gcd.multiout ================================================ ++++++++++++++++++++++++++++++++++++++++++++++++++ |my_gcd(10, 20) |my_gcd(a::T, b::T) where T<:Union{Int128, Int16, Int32, Int64, Int8, UInt128, UI |nt16, UInt32, UInt64, UInt8} in Main at **** | a = 10 | b = 20 | T = Int64 | 3 function my_gcd(a::T, b::T) where T <: Union{Int8, UInt8, Int16, UInt16, … | 4 a == 0 && return abs(b) | 5 b == 0 && return abs(a) | 6 za = trailing_zeros(a) | -------------------------------------------------- |AAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAA |AAAAAAAA |AAAAAAAA |AAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA | ================================================ FILE: test/ui/v1.5/gcdsc.multiout ================================================ ++++++++++++++++++++++++++++++++++++++++++++++++++ |__gcdval__ = my_gcd(10, 20); |my_gcd(a::T, b::T) where T<:Union{Int128, Int16, Int32, Int64, Int8, UInt128, UI |nt16, UInt32, UInt64, UInt8} in Main at **** | a = 10 | b = 20 | T = Int64 | 3 function my_gcd(a::T, b::T) where T <: Union{Int8, UInt8, Int16, UInt16, … | 4 a == 0 && return abs(b) | 5 b == 0 && return abs(a) | 6 za = trailing_zeros(a) | -------------------------------------------------- |AAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAA |AAAAAAAA |AAAAAAAA |AAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |