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
[](https://travis-ci.org/timholy/Rebugger.jl)
[](https://ci.appveyor.com/project/timholy/Rebugger-jl/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://youtu.be/KuM0AGaN09s?t=515)
However, the "interpret" interface is recommended for most users.
## Installation and usage
**See the documentation**:
[](https://timholy.github.io/Rebugger.jl/stable)
[](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 <kbd> Meta-i </kbd> (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 <kbd> Meta-e </kbd> ("enter").
- also for "edit" mode, for an expression that generates an error, hit <kbd> Meta-s </kbd> ("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.
<kbd> Meta </kbd> means <kbd> Esc </kbd> or, if your system is configured appropriately,
<kbd> Alt </kbd> (Linux/Windows) or <kbd> Option </kbd> (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
# <some code>
if x > 0.5
^fcomplex(x, 2; kw1=1.1)
# <more code>
```
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
# <some code>
if x > 0.5
Main.Rebugger.stashed[] = (fcomplex, (x, 2), (kw1=1.1,))
throw(Rebugger.StopException())
# <more code>
```
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
# <body>
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 <ModuleOf_fcomplex> let (x, y, z, kw1, A, T) = Main.Rebugger.getstored("0123abc...")
# <body>
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
<img src="../images/stepin1.png" width="160px"/>
```
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
<img src="../images/stepin2.png" width="660px"/>
```
(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
<img src="../images/stepin3.png" width="690px"/>
```
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
<img src="../images/stepin4.png" width="800px"/>
```
!!! 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
<some code>
if x > 0.5
^fcomplex(x, 2; kw1=1.1)
<more code>
where in the above `^` indicates `position(s)` ("point"), rewrite this as
<some code>
if x > 0.5
Main.Rebugger.stashed[] = (fcomplex, (x, 2), (kw1=1.1,))
throw(Rebugger.StopException())
<more code>
(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"(?<!\.)StopException"
capturestr = replace(capturestr, regexunscoped=>(s->"Rebugger."*s))
regexscoped = r"(?<!\.)Rebugger\.StopException"
capturestr = replace(capturestr, regexscoped=>(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
<body>
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 `<body>` 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...) = <body>
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
<some code>
if x > 0.5
^fcomplex(x)
<more code>
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
<body>
end
rewrite `s` so that its contents are
@eval ModuleOf_fcomplex let (x, y, z, kw1, A, T) = Main.Rebugger.getstored(id)
<body>
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
|
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
Condensed preview — 38 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (156K chars).
[
{
"path": ".github/workflows/Documenter.yml",
"chars": 422,
"preview": "name: Documenter\non:\n push:\n branches: [master]\n tags: [v*]\n pull_request:\n\njobs:\n Documenter:\n name: Docume"
},
{
"path": ".github/workflows/TagBot.yml",
"chars": 362,
"preview": "name: TagBot\non:\n issue_comment:\n types:\n - created\n workflow_dispatch:\njobs:\n TagBot:\n if: github.event_n"
},
{
"path": ".github/workflows/ci.yml",
"chars": 1226,
"preview": "name: CI\non:\n pull_request:\n branches:\n - master\n push:\n branches:\n - master\n tags: '*'\njobs:\n tes"
},
{
"path": ".gitignore",
"chars": 57,
"preview": "docs/build/\ndocs/site/\ntest/expected.out\ntest/failed.out\n"
},
{
"path": "LICENSE.md",
"chars": 1159,
"preview": "The Rebugger.jl package is licensed under the MIT \"Expat\" License:\n\n> Copyright (c) 2018: Tim Holy.\n>\n> Permission is he"
},
{
"path": "Project.toml",
"chars": 845,
"preview": "name = \"Rebugger\"\nuuid = \"ee283ea6-eecd-56e3-beb3-83eb4d3c31e9\"\nversion = \"0.3.3\"\n\n[deps]\nCodeTracking = \"da1fd8a2-8d9e-"
},
{
"path": "README.md",
"chars": 3354,
"preview": "# Rebugger\n\n[](https://travis-ci.org/timholy"
},
{
"path": "docs/Project.toml",
"chars": 138,
"preview": "[deps]\nDocumenter = \"e30172f5-a6a5-5a46-863b-614d45cd2de4\"\nRevise = \"295af30f-e4ad-537b-8983-00126c2a3abe\"\n\n[compat]\nDoc"
},
{
"path": "docs/make.jl",
"chars": 489,
"preview": "using Documenter, Rebugger\n\nmakedocs(\n modules = [Rebugger],\n clean = false,\n format = Documenter.HTML(prettyur"
},
{
"path": "docs/src/config.md",
"chars": 2501,
"preview": "# Configuration\n\n## Run on REPL startup\n\nIf you decide you like Rebugger, you can add lines such as the following to you"
},
{
"path": "docs/src/index.md",
"chars": 2375,
"preview": "# Introduction to Rebugger\n\nRebugger is an expression-level debugger for Julia.\nIt has two modes of action:\n\n- an \"inter"
},
{
"path": "docs/src/internals.md",
"chars": 6036,
"preview": "# How Rebugger works\n\nRebugger traces execution through use of expression-rewriting and Julia's ordinary\n`try/catch` con"
},
{
"path": "docs/src/limitations.md",
"chars": 1697,
"preview": "# Limitations\n\nRebugger is in the early stages of development, and users should currently expect bugs (please do [report"
},
{
"path": "docs/src/reference.md",
"chars": 352,
"preview": "# Developer reference\n\n## Capturing arguments\n\n```@docs\nRebugger.stepin\nRebugger.prepare_caller_capture!\nRebugger.method"
},
{
"path": "docs/src/usage.md",
"chars": 24697,
"preview": "# Usage\n\nRebugger works from Julia's native REPL prompt. Currently there are exactly three keybindings,\nwhich here will "
},
{
"path": "src/Rebugger.jl",
"chars": 2244,
"preview": "module Rebugger\n\nusing UUIDs, InteractiveUtils\nusing REPL\nimport REPL.LineEdit, REPL.Terminals\nusing REPL.LineEdit: buff"
},
{
"path": "src/debug.jl",
"chars": 24815,
"preview": "# Core debugging logic.\n# Hopefully someday much of this will be replaced by Gallium.\n\nconst VarnameType = Tuple{Vararg{"
},
{
"path": "src/deepcopy.jl",
"chars": 3949,
"preview": "# Because `deepcopy(mod::Module)` throws an error, we need a safe approach.\n# Strategy: wrap the IdDict so that our meth"
},
{
"path": "src/precompile.jl",
"chars": 200,
"preview": "function _precompile_()\n ccall(:jl_generating_output, Cint, ()) == 1 || return nothing\n precompile(Tuple{typeof(ge"
},
{
"path": "src/printing.jl",
"chars": 13732,
"preview": "# A type for keeping track of the current line number when printing Exprs\nstruct LineNumberIO <: IO\n io::IO\n linen"
},
{
"path": "src/ui.jl",
"chars": 19835,
"preview": "const rebug_prompt_string = \"rebug> \"\nconst rebug_prompt_ref = Ref{Union{LineEdit.Prompt,Nothing}}(nothing) # set "
},
{
"path": "test/edit.jl",
"chars": 21458,
"preview": "using Rebugger\nusing Rebugger: StopException\nusing Test, UUIDs, InteractiveUtils, REPL, Pkg, HeaderREPLs\nusing REPL.Line"
},
{
"path": "test/interpret.jl",
"chars": 2225,
"preview": "using Rebugger, JuliaInterpreter\nusing CodeTracking, Revise, Test, InteractiveUtils\n\nif !isdefined(Main, :fixable1)\n "
},
{
"path": "test/interpret_script.jl",
"chars": 791,
"preview": "# Test functions for parsing\nfunction fixable1(x)\n return x\nend\nfixable2(x) = x\nfunction fixable3(A)\n s = 0\n fi"
},
{
"path": "test/interpret_ui.jl",
"chars": 3736,
"preview": "# This was copied from Debugger.jl and then modified\n\nusing TerminalRegressionTests, Rebugger, Revise, CodeTracking\nusin"
},
{
"path": "test/my_gcd.jl",
"chars": 668,
"preview": "# From base, but copied here to make sure we don't fail bacause base changed\nfunction my_gcd(a::T, b::T) where T<:Union{"
},
{
"path": "test/runtests.jl",
"chars": 262,
"preview": "using Rebugger, Test\n\n@info \"These tests manipulate the console. Wait until you see \\\"Done\\\"\"\ninclude(\"edit.jl\")\ninclude"
},
{
"path": "test/testmodule.jl",
"chars": 1060,
"preview": "module RebuggerTesting\n\nconst cbdata1 = Ref{Any}(nothing)\nconst cbdata2 = Ref{Any}(nothing)\n\n# Do not alter the line num"
},
{
"path": "test/ui/v1.0/gcd.multiout",
"chars": 831,
"preview": "++++++++++++++++++++++++++++++++++++++++++++++++++\n|gcd(10, 20)\n|gcd(a::T, b::T) where T<:Union{Int128, Int16, Int32, In"
},
{
"path": "test/ui/v1.0/gcdsc.multiout",
"chars": 859,
"preview": "++++++++++++++++++++++++++++++++++++++++++++++++++\n|__gcdval__ = gcd(10, 20);\n|gcd(a::T, b::T) where T<:Union{Int128, In"
},
{
"path": "test/ui/v1.1/gcd.multiout",
"chars": 831,
"preview": "++++++++++++++++++++++++++++++++++++++++++++++++++\n|gcd(10, 20)\n|gcd(a::T, b::T) where T<:Union{Int128, Int16, Int32, In"
},
{
"path": "test/ui/v1.1/gcdsc.multiout",
"chars": 859,
"preview": "++++++++++++++++++++++++++++++++++++++++++++++++++\n|__gcdval__ = gcd(10, 20);\n|gcd(a::T, b::T) where T<:Union{Int128, In"
},
{
"path": "test/ui/v1.2/gcd.multiout",
"chars": 809,
"preview": "++++++++++++++++++++++++++++++++++++++++++++++++++\n|gcd(10, 20)\n|gcd(a::T, b::T) where T<:Union{Int128, Int16, Int32, In"
},
{
"path": "test/ui/v1.2/gcdsc.multiout",
"chars": 837,
"preview": "++++++++++++++++++++++++++++++++++++++++++++++++++\n|__gcdval__ = gcd(10, 20);\n|gcd(a::T, b::T) where T<:Union{Int128, In"
},
{
"path": "test/ui/v1.3/gcd.multiout",
"chars": 868,
"preview": "++++++++++++++++++++++++++++++++++++++++++++++++++\n|my_gcd(10, 20)\n|my_gcd(a::T, b::T) where T<:Union{Int128, Int16, Int"
},
{
"path": "test/ui/v1.3/gcdsc.multiout",
"chars": 896,
"preview": "++++++++++++++++++++++++++++++++++++++++++++++++++\n|__gcdval__ = my_gcd(10, 20);\n|my_gcd(a::T, b::T) where T<:Union{Int1"
},
{
"path": "test/ui/v1.5/gcd.multiout",
"chars": 868,
"preview": "++++++++++++++++++++++++++++++++++++++++++++++++++\n|my_gcd(10, 20)\n|my_gcd(a::T, b::T) where T<:Union{Int128, Int16, Int"
},
{
"path": "test/ui/v1.5/gcdsc.multiout",
"chars": 896,
"preview": "++++++++++++++++++++++++++++++++++++++++++++++++++\n|__gcdval__ = my_gcd(10, 20);\n|my_gcd(a::T, b::T) where T<:Union{Int1"
}
]
About this extraction
This page contains the full source code of the timholy/Rebugger.jl GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 38 files (145.7 KB), approximately 40.6k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.