diff --git a/CHANGELOG.md b/CHANGELOG.md index c94a97c..dbbc8a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,33 @@ ## Version 0.9.0 (unreleased) -This release overhauls the terminal UI. The most visible changes: the debugger -now runs full screen by default, `bp`/`f`/`w` open interactive menus, and a new -`debug>` REPL mode is entered with `)`. +This release adds mixed-mode stepping (the new default) and overhauls the +terminal UI. The most visible changes: stepping over Base/dependency code runs +at native speed, the debugger runs full screen by default, `bp`/`f`/`w`/`focus` +open interactive menus, and a new `debug>` REPL mode is entered with `)`. + +### Mixed-mode stepping — the new default + +Instead of interpreting everything (slow) or stepping over compiled code +blindly (breakpoints missed), the debugger now interprets only calls that can +reach the "focus set" — `Main`, packages loaded from dev checkouts, the package +you `@enter`ed, and anything you add — and runs everything else natively. +Callbacks and user types passed into Base or dependency code keep the carrying +call chain interpreted, so `map(f, xs)`, `sort(xs; by = f)`, broadcasts and +do-blocks remain fully debuggable while e.g. `sort(::Vector{Int})` runs at +compiled speed. Breakpoints anywhere outside the focus set automatically fall +back to full interpretation, with a message. + +- The prompt shows the mode: `1|debug>` (mixed, the default), + `1|interpreted>`, `1|compiled>` — each with its own color. +- `M` toggles mixed ↔ interpreted; `C` still toggles compiled mode. +- `mode [interpreted|mixed|compiled]` shows/sets the mode; + `Debugger.set_default_mode!(:interpreted)` (e.g. in `startup.jl`) restores + the previous default. +- `focus` shows and manages the focus set; stepping into a dependency package + adds it to the set for the session (a message is printed). +- API: `Debugger.interpret_module!`, `Debugger.compile_module!`, + `Debugger.reset_module!`, `Debugger.interpreted_modules`. ### Keys at the `debug>` prompt @@ -14,6 +38,7 @@ Pressed at the beginning of an empty prompt line: |:----|:-------| | `` ` `` | enter evaluation mode (as before) | | `C` | toggle compiled mode (as before) | +| `M` | **new** — toggle mixed mode | | `L` | toggle lowered code (as before) | | `T` | **new** — cycle how variable types are shown: compact → none → types only → full | | `S` | **new** — toggle "sticky" (full-screen) mode | @@ -36,6 +61,8 @@ scrolling transcript back. are toggleable rows in the same menu. - `f` (no arguments) opens a frame picker for the call stack. - `w` (no arguments) opens the watch list (**`d`** deletes an entry). +- `focus` (no arguments) opens the mixed-mode focus set: **space/enter** + toggles whether a module is interpreted, **`a`** adds one by name. All existing subcommands (`bp rm 2`, `bp add foo:12`, `f 3`, `w rm 1`, ...) still work, and non-interactive terminals automatically get the old text diff --git a/Project.toml b/Project.toml index 958bf2b..ed53c2f 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "Debugger" uuid = "31a5f54b-26ea-5ae9-a837-f05ce5417438" -version = "0.8.1" +version = "0.9.0" [deps] CodeTracking = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2" @@ -18,7 +18,7 @@ TerminalRegressionTests = {url = "https://github.com/JuliaDebug/TerminalRegressi [compat] CodeTracking = "2, 3" Highlights = "0.6" -JuliaInterpreter = "0.10, 0.11" +JuliaInterpreter = "0.11" PrecompileTools = "1" TerminalRegressionTests = "0.3" julia = "1.10" diff --git a/README.md b/README.md index 8e5372b..b42b823 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,8 @@ Misc: - `o`: open the current line in an editor - `q`: quit the debugger, returning `nothing` - `C`: toggle compiled mode +- `M`: toggle mixed mode (see "Stepping modes" below) +- `mode [interpreted|mixed|compiled]`: show or set the stepping mode - `L`: toggle showing lowered code instead of source code - `T`: cycle how variable types are shown: compact (long type parameters elided), no types, types only, full types - `S`: toggle "sticky" (full-screen) mode, on by default: the debugger runs on the terminal's alternate screen (restored when quitting, like `less` or `vim`) and redraws the status in place instead of scrolling @@ -232,28 +234,74 @@ Hit breakpoint: >[1] f(x) at REPL[6]:3 ``` -### Compiled mode +### Stepping modes: interpreted, mixed and compiled -In order to fully support breakpoints, the debugger interprets all code, even code that is stepped over. -Currently, there are cases where the interpreter is too slow for this to be feasible. -A workaround is to use "compiled mode" which is toggled by pressing `C` in the debug REPL mode (note the change of prompt color). -When using compiled mode, code that is stepped over will be executed -by the normal julia compiler and run just as fast as normally. -The drawback is that breakpoints in compiled code that is stepped over are missed. +In order to fully support breakpoints, the debugger can interpret all code, even code that is stepped +over. Currently, there are cases where the interpreter is too slow for this to be feasible. The +debugger therefore supports three stepping modes: -To split the difference between these two extremes, one can fine tune which modules are compiled and which are not. -For example, to compile all code in Base, even when not in `C` mode, and hence break on all specified points in user code, -issue the following commands on the REPL *before* `@enter`: +- **mixed** (the default): calls into modules outside the "focus set" — `Core`, `Base`, the standard + libraries *and* dependency packages — run natively, at full compiled speed, *unless* the call could + reach focus code. A call is interpreted whenever the callee or any argument carries a function or + type from a focus module, also inside wrappers: `map(f, xs)`, `sort(xs; by = f)`, `f.(xs)`, + do-blocks, `Vector{MyType}` arguments and so on all remain fully debuggable, while e.g. + `sort(::Vector{Int})` or a `DataFrames` call operating purely on its own types runs at native + speed. As the default, it keeps the plain `1|debug>` prompt. +- **interpreted**: everything is interpreted; breakpoints always work, including the corner cases + mixed mode cannot catch (see below). Toggled with the `M` key (note the `1|interpreted>` prompt + and its color). +- **compiled**: everything that is stepped over runs natively. Fastest, but breakpoints in code that + is stepped over are missed. Toggled with the `C` key (note the `1|compiled>` prompt and its color). + +The mode can also be shown/set with the `mode [interpreted|mixed|compiled]` command in the debug REPL, +and the mode new sessions start in can be set (e.g. in `startup.jl`) with: ```jl -using JuliaInterpreter, MethodAnalysis -union!(JuliaInterpreter.compiled_modules, child_modules(Base)) +Debugger.set_default_mode!(:interpreted) ``` -Additional imported modules can also always be compiled with: +The focus set — the modules mixed mode interprets — is seeded automatically at each `@enter`/`@run`: + +- `Main` (functions defined at the REPL or in scripts); +- every loaded package whose source is a *dev checkout* (a `pkgdir` outside the read-only + `~/.julia/packages` store), i.e. the packages you are plausibly developing; +- the package of the function you entered (so `@enter DataFrames.groupby(df, :a)` makes `DataFrames` + debuggable even though it is a registered install) — except `Base`/stdlibs, which would de-facto + disable the fast path; +- stepping into (`s`) a frame of a dependency package adds it to the focus set for the session (an + info message is printed). Stepping into `Base`/a stdlib does *not* — a hint is printed instead. + +Inside the debugger, `focus` opens an interactive menu over the current set (space/enter toggles a +module, `a` adds one by name); `focus add SomeMod` / `focus rm SomeMod` (or `focus rm i` by number) +do the same non-interactively. These adjust the session; the permanent equivalents, e.g. for +`startup.jl`: ```jl -union!(JuliaInterpreter.compiled_modules, SomePackage) +Debugger.interpret_module!(SparseArrays) # always interpret (debug into) this module +Debugger.compile_module!(MyBigHelperPkg) # never interpret this module (also skips its `@bp` markers) +Debugger.interpreted_modules() # show the current focus set +``` + +For soundness, mixed mode automatically falls back to full interpretation while any breakpoint is set +outside the focus set (including file breakpoints that could match such a module), while a breakpoint +is set in a focus package that loaded non-focus packages depend on (native code in the latter could +otherwise run past it), or while `break_on(:error)` or `break_on(:throw)` is active — a message is +printed when this happens. Known limitations of mixed mode (switch to interpreted mode if they +matter): focus-module callbacks hidden in type-erased containers (e.g. `Vector{Any}`, fields typed +`::Function`) are not detected; neither are focus-module methods whose signatures only mention +non-focus types (type piracy), nor focus code reached without any focus-typed argument at the call +site (callbacks stored in global registries, `Task`s, finalizers). `@bp` markers inside non-focus +modules are skipped, and file breakpoints on source that was `eval`'d or `include`d from outside a +package directory may not trigger the fallback. + +The [`JuliaInterpreter.compiled_methods`/`compiled_modules`](https://github.com/JuliaDebug/JuliaInterpreter.jl) +mechanisms keep working in every mode and take precedence over mixed mode's decisions +(as does `JuliaInterpreter.interpreted_methods`, in the other direction). For example, to compile all +code in Base even in interpreted mode: + +```jl +using JuliaInterpreter, MethodAnalysis +union!(JuliaInterpreter.compiled_modules, child_modules(Base)) ``` @@ -270,7 +318,7 @@ shows the current settings. The available options are: - `max_vars::Int`: maximum number of variables shown in the automatic status display (default: `15`) - `sticky::Bool`: "full screen" mode — the debugger runs on the terminal's alternate screen and redraws the status in place instead of scrolling (default: `true`); can also be toggled with the `S` key - `charset::Symbol`: `:unicode` or `:ascii` (default: `:unicode`) -- `menus::Bool`: use the interactive menus for `bp`, `f` and `w` (default: `true`) +- `menus::Bool`: use the interactive menus for `bp`, `f`, `w` and `focus` (default: `true`) The older entry points `Debugger.set_theme(theme)` and `Debugger.set_highlight(false)` still work. diff --git a/src/Debugger.jl b/src/Debugger.jl index 2e385b8..15e88c8 100644 --- a/src/Debugger.jl +++ b/src/Debugger.jl @@ -21,6 +21,7 @@ const SEARCH_PATH = [] function __init__() append!(SEARCH_PATH,[joinpath(Sys.BINDIR,"../share/julia/base/"), joinpath(Sys.BINDIR,"../include/")]) + init_mixed_mode() auto_install_repl_mode() return nothing end @@ -38,13 +39,17 @@ using .LineNumbers: SourceFile, compute_line # watch expressions between invocations of the debugger interface const WATCH_LIST = [] +include("mixed_mode.jl") + Base.@kwdef mutable struct DebuggerState frame::Union{Nothing, Frame} level::Int = 1 broke_on_error::Bool = false watch_list::Vector = WATCH_LIST lowered_status::Bool = false - interp::Interpreter = RecursiveInterpreter() + interp::Interpreter = interp_for_mode(DEFAULT_MODE[]) + # the mode to come back to when the `C` (compiled mode) toggle is switched off + noncompiled_interp::Interpreter = RecursiveInterpreter() repl = nothing terminal = nothing main_mode = nothing @@ -65,10 +70,44 @@ function output_stream(state::DebuggerState) return IOContext(io, :color => have_color) end +# `C` key: binary toggle between compiled mode and the previous non-compiled mode function toggle_mode(state) - state.interp = (state.interp === RecursiveInterpreter() ? (state.interp = NonRecursiveInterpreter()) : (state.interp = RecursiveInterpreter())) + if state.interp isa NonRecursiveInterpreter + state.interp = state.noncompiled_interp + else + state.noncompiled_interp = state.interp + state.interp = NonRecursiveInterpreter() + end +end + +# `M` key: binary toggle between mixed and interpreted mode +function toggle_mixed(state) + if state.interp isa MixedInterpreter + state.interp = RecursiveInterpreter() + else + state.interp = MixedInterpreter() + end + state.noncompiled_interp = state.interp end +function set_session_mode!(state, mode::Symbol) + new_interp = interp_for_mode(mode) + if new_interp isa NonRecursiveInterpreter + # remember the current mode so the `C` toggle returns to it + if !(state.interp isa NonRecursiveInterpreter) + state.noncompiled_interp = state.interp + end + else + state.noncompiled_interp = new_interp + end + state.interp = new_interp + return state.interp +end + +mode_description(::RecursiveInterpreter) = "interpreted (all code is interpreted, breakpoints always work)" +mode_description(::MixedInterpreter) = "mixed (code outside the focus modules runs natively unless it can reach them)" +mode_description(::NonRecursiveInterpreter) = "compiled (stepped-over code runs natively, breakpoints in it are missed)" + toggle_lowered(state) = state.lowered_status = !state.lowered_status function active_frame(state) diff --git a/src/commands.jl b/src/commands.jl index f25861b..dc28376 100644 --- a/src/commands.jl +++ b/src/commands.jl @@ -43,6 +43,7 @@ function execute_command(state::DebuggerState, v::Union{Val{:c},Val{:nc},Val{:n} assert_allow_step(state) || return false cmd == "so" && (cmd = "finish") cmd == "u" && (cmd = "until") + maybe_warn_native_suppressed(state) ret = debug_command(state.interp, state.frame, Symbol(cmd); kwargs...) if ret === nothing state.overall_result = get_return(root(state.frame)) @@ -50,6 +51,10 @@ function execute_command(state::DebuggerState, v::Union{Val{:c},Val{:nc},Val{:n} return false else state.frame, pc = ret + # an explicit step-in may land in a module outside the focus set + if v isa Union{Val{:s},Val{:si},Val{:sg},Val{:sl}} + maybe_grow_focus!(state) + end if pc isa BreakpointRef if pc.stmtidx != 0 # This is the dummy breakpoint to stop just after entering a call if state.terminal !== nothing # fix this, it happens when a test hits this and hasn't set a terminal @@ -282,6 +287,98 @@ function execute_command(state::DebuggerState, v::Union{Val{:bp}}, cmd::Abstract end +function execute_command(state::DebuggerState, ::Val{:mode}, cmd::AbstractString) + cmds = split(cmd, r" +") + io = output_stream(state) + if length(cmds) == 1 + println(io, "Stepping mode: ", mode_description(state.interp)) + if state.interp isa MixedInterpreter + reason = native_suppressed_reason() + if reason === nothing + println(io, "Focus (interpreted) modules: ", + join(nameof.(interpreted_modules()), ", "), " (manage with `focus`)") + else + printstyled(io, "Fast path suspended, running fully interpreted ($reason)\n"; color=:yellow) + end + end + return false + elseif length(cmds) == 2 + mode = Symbol(cmds[2]) + if mode in (:interpreted, :mixed, :compiled) + set_session_mode!(state, mode) + println(io, "Stepping mode: ", mode_description(state.interp)) + return false + end + end + return invalid_command(state, cmd) +end + +function _resolve_module(state, name::AbstractString) + ex = try + Meta.parse(name) + catch + return nothing + end + for m in (state.frame === nothing ? (Main,) : (moduleof(active_frame(state)), Main)) + val = try + Core.eval(m, ex) + catch + continue + end + val isa Module && return val + end + # fall back to loaded packages without a binding in the above modules + matches = Set{Module}() + for (_, m) in Base.loaded_modules + string(nameof(m)) == name && push!(matches, m) + end + length(matches) == 1 && return first(matches) + if length(matches) > 1 + printstyled(stderr, "Multiple loaded modules named $name\n"; color=Base.error_color()) + end + return nothing +end + +function execute_command(state::DebuggerState, ::Val{:focus}, cmd::AbstractString) + cmds = split(cmd, r" +") + io = output_stream(state) + function print_focus() + println(io, "Focus set (modules that are interpreted; everything else runs natively when possible):") + for (i, m) in enumerate(interpreted_modules()) + println(io, " [$i] $m") + end + end + if length(cmds) == 1 + if menus_available(state) + focus_menu(state) + else + print_focus() + end + return false + elseif length(cmds) == 3 && cmds[2] in ("add", "rm") + local mod + i = cmds[2] == "rm" ? tryparse(Int, cmds[3]) : nothing + if i !== nothing + mods = interpreted_modules() + if !(1 <= i <= length(mods)) + printstyled(stderr, "No module numbered $i in the focus set\n"; color=Base.error_color()) + return false + end + mod = mods[i] + else + mod = _resolve_module(state, cmds[3]) + if mod === nothing + printstyled(stderr, "Could not resolve $(repr(cmds[3])) as a loaded module\n"; color=Base.error_color()) + return false + end + end + cmds[2] == "add" ? focus_module!(mod) : unfocus_module!(mod) + print_focus() + return false + end + return invalid_command(state, cmd) +end + function execute_command(state::DebuggerState, _, cmd) display(Markdown.parse("""Unknown command `$cmd`. Run `?` to obtain help.""")) return false @@ -297,6 +394,10 @@ function execute_command(state::DebuggerState, ::Union{Val{:help}, Val{:?}}, cmd - `o`: open the current line in an editor\\ - `q`: quit the debugger, returning `nothing`\\ - `C`: toggle compiled mode\\ + - `M`: toggle mixed mode (code outside the focus modules runs natively unless it can reach them)\\ + - `mode [interpreted|mixed|compiled]`: show or set the stepping mode\\ + - `focus`: show the focus set (the modules mixed mode interprets) as an interactive menu\\ + - `focus add Mod`/`focus rm Mod|i`: add/remove a module (by name or number) from the focus set\\ - `L`: toggle showing lowered code instead of source code\\ - `T`: cycle how variable types are shown (compact, none, types only, full)\\ - `S`: toggle "sticky" (full-screen) mode, on by default: the debugger runs on the terminal's alternate screen and redraws the status in place\\ diff --git a/src/config.jl b/src/config.jl index 18e9077..3802897 100644 --- a/src/config.jl +++ b/src/config.jl @@ -23,7 +23,7 @@ const STICKY = Ref(true) const CHARSET = Ref{Symbol}(:unicode) -# Interactive TerminalMenus-based menus for `bp`, `f` and `w` +# Interactive TerminalMenus-based menus for `bp`, `f`, `w` and `focus` const INTERACTIVE_MENUS = Ref(true) set_theme(theme::String) = _current_theme[] = theme @@ -53,7 +53,7 @@ settings as a `NamedTuple`; keywords set the corresponding option: screen (restored on quit) and redraws the status in place instead of scrolling (default: `true`). Can be toggled with the `S` key in the debugger. - `charset::Symbol`: `:unicode` or `:ascii` (default: `:unicode`) -- `menus::Bool`: use interactive menus for `bp`, `f` and `w` (default: `true`) +- `menus::Bool`: use interactive menus for `bp`, `f`, `w` and `focus` (default: `true`) """ function config(; theme::Union{Nothing,String} = nothing, highlight::Union{Nothing,Bool} = nothing, diff --git a/src/menus.jl b/src/menus.jl index a39cffe..dcc8bf8 100644 --- a/src/menus.jl +++ b/src/menus.jl @@ -1,7 +1,7 @@ -# Interactive menus (REPL.TerminalMenus) for breakpoints, frames and watch -# expressions. All of them are driven by `ActionMenu`, a generic menu where -# every row is an arbitrary object rendered by a callback and key presses map -# to actions. +# Interactive menus (REPL.TerminalMenus) for breakpoints, frames, watch +# expressions and the mixed-mode focus set. All of them are driven by +# `ActionMenu`, a generic menu where every row is an arbitrary object rendered +# by a callback and key presses map to actions. function menus_available(state::DebuggerState) INTERACTIVE_MENUS[] || return false @@ -275,3 +275,85 @@ function watch_menu(state::DebuggerState) run_menu(menu, state) return nothing end + +# --- focus menu -------------------------------------------------------------- + +# Modules worth showing: the current focus set plus everything toggled off (so a +# toggle is reversible without retyping the name), the permanent overrides, and +# the Base/stdlib modules the user entered or stepped into (not auto-focused, +# but one toggle away). +function focus_menu_rows() + mods = Set{Module}(focus_modules()) + union!(mods, SESSION_UNFOCUSED) + union!(mods, INTERPRET_OVERRIDES) + union!(mods, COMPILE_OVERRIDES) + union!(mods, _stdlib_focus_hinted) + return sort!(collect(mods); by = m -> string(nameof(m))) +end + +function focus_menu_writerow(io::IO, mod::Module, idx::Int) + infocus = is_focus_module(mod) + printstyled(io, infocus ? char_bp_enabled() : char_bp_disabled(); + color = infocus ? :green : :light_black) + print(io, " ", idx, "] ") + printstyled(io, string(mod); color = infocus ? :normal : :light_black) + if mod ∈ INTERPRET_OVERRIDES || mod ∈ COMPILE_OVERRIDES + printstyled(io, " (pinned)"; color = :light_black) + elseif mod === SESSION_ENTRY_ROOT[] + printstyled(io, " (entered)"; color = :light_black) + end +end + +focus_menu_toggle(mod::Module) = + is_focus_module(mod) ? unfocus_module!(mod) : focus_module!(mod) + +function focus_menu(state::DebuggerState) + add_requested = Ref(false) + cursor = 1 + + onkey = function (m, key, idx) + if key == ' ' || key == 't' + focus_menu_toggle(m.rows[idx]::Module) + elseif key == 'a' + # adding needs a free-form module name; leave the menu, prompt for + # one line and reopen + add_requested[] = true + return true + end + return false + end + onpick = function (m, idx) + focus_menu_toggle(m.rows[idx]::Module) + return false + end + + while true + rows = focus_menu_rows() + add_requested[] = false + + menu = ActionMenu(rows; writerow = focus_menu_writerow, onkey = onkey, onpick = onpick, + help = "[space/enter] toggle [a] add [q] quit", + menu_settings(state)...) + run_menu(menu, state; cursor = cursor) + + add_requested[] || break + io = output_stream(state) + printstyled(io, "focus add> "; color = :light_black) + input = try + strip(readline(state.terminal.in_stream)) + catch + "" + end + if !isempty(input) + mod = _resolve_module(state, String(input)) + if mod === nothing + printstyled(stderr, "Could not resolve $(repr(input)) as a loaded module\n"; color=Base.error_color()) + else + focus_module!(mod) + # reopen with the cursor on the newly added module + cursor = something(findfirst(==(Base.moduleroot(mod)), focus_menu_rows()), 1) + end + end + end + return nothing +end diff --git a/src/mixed_mode.jl b/src/mixed_mode.jl new file mode 100644 index 0000000..a45ffe8 --- /dev/null +++ b/src/mixed_mode.jl @@ -0,0 +1,681 @@ +# Mixed-mode execution: run "boring" calls (Base/stdlib code operating purely on +# Base/stdlib data) natively, while interpreting every call that could lead to user +# code — user functions passed as arguments (also inside wrappers like `Generator`, +# `Broadcasted` or keyword-call `NamedTuple`s) and calls that may dispatch on user +# types. +# +# All policy state below is process-global and unsynchronized, like the rest of +# Debugger/JuliaInterpreter (breakpoints, WATCH_LIST, compiled_methods): one debug +# session at a time is assumed; nested or concurrent sessions share and reseed it. + +""" + MixedInterpreter <: JuliaInterpreter.Interpreter + +An interpreter that recurses (like `RecursiveInterpreter`) into any call that can +reach user code, but executes calls natively (like `NonRecursiveInterpreter`) when +both the callee and all arguments belong to "compiled" modules (by default `Core`, +`Base` and the standard libraries). See `Debugger.compile_module!`, +`Debugger.interpret_module!` and the `mode` debugger command. +""" +struct MixedInterpreter <: Interpreter end + +# ------------------------------------------------------------------ +# Module classification: the focus set +# ------------------------------------------------------------------ + +# Mixed mode interprets the modules being debugged — the "focus set" — and runs +# everything else (Base, stdlibs *and* dependency packages) natively when possible. +# The focus set is seeded per debug session: `Main`, packages loaded from dev +# checkouts (a `pkgdir` outside every depot's read-only `packages/` store), the +# root module of the entered method, and the permanent overrides below. + +const STDLIB_NAMES = Set{Symbol}() +const COMPILE_OVERRIDES = Set{Module}() # roots forced out of focus via compile_module! +const INTERPRET_OVERRIDES = Set{Module}() # roots forced into focus via interpret_module! +const FOCUS = Ref{Union{Nothing,Set{Module}}}(nothing) # nothing = seed on first use +const _dev_package_cache = Dict{Module,Bool}() + +# julia's compiler is a separate root module on ≥1.12; matched by identity so a +# user package that happens to be called `Compiler` is not misclassified +const _compiler_root = Ref{Union{Nothing,Module}}(nothing) + +is_stdlib_root(root::Module) = + root === Base || root === Core || nameof(root) ∈ STDLIB_NAMES || + root === _compiler_root[] + +_maybe_realpath(path::String) = try realpath(path) catch; normpath(path) end + +# Component-aware containment: `packages-dev` is not inside `packages`. +_path_within(path::String, dir::String) = path == dir || startswith(path, joinpath(dir, "")) + +_pkg_devdir() = _maybe_realpath(get(ENV, "JULIA_PKG_DEVDIR") do + joinpath(isempty(DEPOT_PATH) ? joinpath(homedir(), ".julia") : DEPOT_PATH[1], "dev") +end) + +# "Dev checkout" — source the user can plausibly be editing/debugging. Registered +# installs live in a depot's content-addressed (read-only) `packages/` store. +function is_dev_package(root::Module) + get!(_dev_package_cache, root) do + root === Main && return true + is_stdlib_root(root) && return false + dir = pkgdir(root) + dir === nothing && return true # non-package root, e.g. eval'd module + rdir = _maybe_realpath(dir) + _path_within(rdir, _pkg_devdir()) && return true # explicit devdir wins + for depot in DEPOT_PATH + pkgs = joinpath(depot, "packages") + isdir(pkgs) || continue + _path_within(rdir, _maybe_realpath(pkgs)) && return false + end + return !_path_within(rdir, _maybe_realpath(Sys.STDLIB)) + end +end + +function default_focus() + focus = Set{Module}((Main,)) + for (_, m) in Base.loaded_modules + root = Base.moduleroot(m) + is_dev_package(root) && push!(focus, root) + end + union!(focus, INTERPRET_OVERRIDES) + setdiff!(focus, COMPILE_OVERRIDES) + setdiff!(focus, SESSION_UNFOCUSED) # respected on mid-session (lazy) recomputation + return focus +end + +function focus_modules() + focus = FOCUS[] + focus === nothing || return focus + return FOCUS[] = default_focus() +end + +is_focus_module(root::Module) = root ∈ focus_modules() +is_compiled_module(root::Module) = !is_focus_module(root) + +# Reseed the focus set at the start of a debug session. The entered method's root +# module joins the focus so that `@enter SomePkg.f(...)` makes `SomePkg` debuggable +# even when it is a registered install — except for Base/stdlibs, where focusing +# would de-facto disable the fast path (the entered frame itself is always +# interpreted anyway, so stepping inside it works regardless). +const SESSION_ENTRY_ROOT = Ref{Union{Nothing,Module}}(nothing) +const _pending_entry_hint = Ref{Union{Nothing,Module}}(nothing) + +# Printed once per entered stdlib module, after the first status draw of the +# session (a message printed earlier is erased by entering the alternate screen +# in sticky mode). +function maybe_print_entry_hint(state) + mod = _pending_entry_hint[] + mod === nothing && return nothing + _pending_entry_hint[] = nothing + state.interp isa MixedInterpreter || return nothing + printstyled(output_stream(state), + "$mod is not added to the focus set when entered; calls to it from " * + "other frames still run natively (`focus add $(nameof(mod))` to " * + "interpret it everywhere)\n"; color=:light_black) + return nothing +end + +function seed_focus!(frame) + empty!(SESSION_UNFOCUSED) # session choices do not survive into a new session + SESSION_ENTRY_ROOT[] = nothing + focus = default_focus() + scope = frame === nothing ? nothing : JuliaInterpreter.scopeof(root(frame)) + mod = scope isa Method ? scope.module : scope + if mod isa Module + entry = Base.moduleroot(mod) + if !is_stdlib_root(entry) + # recorded even when overridden, so reset_module! can restore it + SESSION_ENTRY_ROOT[] = entry + entry ∉ COMPILE_OVERRIDES && push!(focus, entry) + elseif entry ∉ INTERPRET_OVERRIDES && entry ∉ _stdlib_focus_hinted + # entering Base/a stdlib does not focus it (that would de-facto + # disable the fast path); note it once, and the `focus` menu shows + # it as a toggleable row. The message itself is printed by + # `maybe_print_entry_hint` after the first status draw — printing + # here would be swallowed by the switch to the alternate screen. + push!(_stdlib_focus_hinted, entry) + _pending_entry_hint[] = entry + end + end + _loaded_fingerprint[] = length(Base.loaded_modules) + if FOCUS[] != focus + FOCUS[] = focus + invalidate_policy_cache!() + end + return focus +end + +# Packages can be loaded mid-session (e.g. from the evaluation prompt); newly +# loaded dev packages join the focus set and the source-path caches used for +# file-breakpoint suppression must be rebuilt. Checked once per debugger command. +const _loaded_fingerprint = Ref(-1) + +function refresh_loaded_modules!() + n = length(Base.loaded_modules) + n == _loaded_fingerprint[] && return nothing + _loaded_fingerprint[] = n + if FOCUS[] !== nothing + for (_, m) in Base.loaded_modules + root = Base.moduleroot(m) + if is_dev_package(root) && root ∉ SESSION_UNFOCUSED && root ∉ COMPILE_OVERRIDES + push!(FOCUS[], root) + end + end + end + invalidate_policy_cache!() + return nothing +end + +# Session-scoped focus manipulation (the `focus` debugger command). Roots removed +# with `focus rm` are remembered so automatic focus growth does not re-add them. +const SESSION_UNFOCUSED = Set{Module}() + +function focus_module!(mod::Module) + root = Base.moduleroot(mod) + delete!(SESSION_UNFOCUSED, root) + added = root ∉ focus_modules() + added && (push!(focus_modules(), root); invalidate_policy_cache!()) + return added +end + +function unfocus_module!(mod::Module) + root = Base.moduleroot(mod) + push!(SESSION_UNFOCUSED, root) + removed = root ∈ focus_modules() + removed && (delete!(focus_modules(), root); invalidate_policy_cache!()) + return removed +end + +""" + Debugger.compile_module!(mod::Module) + +Permanently force `mod` (more precisely, its root module) out of mixed mode's focus +set: calls into it run natively when no argument carries focus code. Note that this +also skips any inline `@bp` markers inside `mod`. For the current session only, use +the `focus rm` debugger command instead. +""" +function compile_module!(mod::Module) + root = Base.moduleroot(mod) + delete!(INTERPRET_OVERRIDES, root) + push!(COMPILE_OVERRIDES, root) + FOCUS[] === nothing || delete!(FOCUS[], root) + invalidate_policy_cache!() + return nothing +end + +""" + Debugger.interpret_module!(mod::Module) + +Permanently add `mod` (more precisely, its root module) to mixed mode's focus set: +calls into it, and calls whose arguments carry types from it, are always +interpreted. Use this to debug into a standard library or a registered dependency. +For the current session only, use the `focus add` debugger command instead. +""" +function interpret_module!(mod::Module) + root = Base.moduleroot(mod) + delete!(COMPILE_OVERRIDES, root) + push!(INTERPRET_OVERRIDES, root) + FOCUS[] === nothing || push!(FOCUS[], root) + invalidate_policy_cache!() + return nothing +end + +""" + Debugger.reset_module!(mod::Module) + +Remove any permanent [`interpret_module!`](@ref)/[`compile_module!`](@ref) override +for `mod`, returning it to automatic focus classification. +""" +function reset_module!(mod::Module) + root = Base.moduleroot(mod) + delete!(COMPILE_OVERRIDES, root) + delete!(INTERPRET_OVERRIDES, root) + # reclassify only this root; other session focus choices are kept + if FOCUS[] !== nothing + automatic = is_dev_package(root) || root === SESSION_ENTRY_ROOT[] + if automatic && root ∉ SESSION_UNFOCUSED + push!(FOCUS[], root) + else + delete!(FOCUS[], root) + end + end + invalidate_policy_cache!() + return nothing +end + +""" + Debugger.interpreted_modules() -> Vector{Module} + +Return a snapshot of the current focus set: the root modules mixed mode interprets. +Everything else runs natively when no argument carries focus code. See +[`interpret_module!`](@ref), [`compile_module!`](@ref) and the `focus` debugger +command (mutating the returned vector has no effect). +""" +interpreted_modules() = sort!(collect(focus_modules()); by = m -> string(nameof(m))) + +# ------------------------------------------------------------------ +# "Does this value carry focus code?" — classification of runtime values +# ------------------------------------------------------------------ + +# Constructors must be classified by the type they construct, not by +# `typeof(MyT) === DataType` (which would classify them as Core). +classify(@nospecialize x) = x isa Type ? x : typeof(x) + +const _usercode_cache = IdDict{Any,Bool}() + +function invalidate_policy_cache!() + empty!(_usercode_cache) + empty!(_revdep_cache) + _bp_suppression_computed[] = false + _suppression_warned[] = false + _compiled_path_prefixes[] = nothing + _compiled_source_basenames[] = nothing + return nothing +end + +""" + carries_focus_code(x) -> Bool + +`true` if the runtime value `x` is, or (recursively through its type parameters) +mentions, a function or type from a module in the focus set. Focus `Module`s and +`GlobalRef`s into them also count: their types belong to `Core`, but a callee can +resolve and call focus code through them. +""" +function carries_focus_code(@nospecialize(x)) + x isa Module && return is_focus_module(Base.moduleroot(x)) + x isa GlobalRef && return is_focus_module(Base.moduleroot(x.mod)) + x isa TypeVar && return _carries_user_code(x) + x isa Core.TypeofVararg && return _carries_user_code(x) # `classify` would erase it + return _carries_user_code(classify(x)) +end + +function _carries_user_code(@nospecialize(t)) + r = get(_usercode_cache, t, nothing) + r !== nothing && return r::Bool + sawcycle = Ref(false) + result = _usercode_walk(t, Base.IdSet{Any}(), sawcycle) + # A result computed under an "assume boring" cycle assumption is not safe to memoize. + sawcycle[] || (_usercode_cache[t] = result) + return result +end + +function _usercode_walk(@nospecialize(t), inprogress, sawcycle) + if t isa TypeVar + return _usercode_walk(t.ub, inprogress, sawcycle) || + _usercode_walk(t.lb, inprogress, sawcycle) + elseif t isa Union + return _usercode_walk(t.a, inprogress, sawcycle) || + _usercode_walk(t.b, inprogress, sawcycle) + elseif t isa UnionAll + return _usercode_walk(t.var, inprogress, sawcycle) || + _usercode_walk(t.body, inprogress, sawcycle) + elseif t isa Core.TypeofVararg + return (isdefined(t, :T) && _usercode_walk(t.T, inprogress, sawcycle)) || + (isdefined(t, :N) && _usercode_walk_param(t.N, inprogress, sawcycle)) + elseif t isa DataType + if t ∈ inprogress + sawcycle[] = true + return false + end + r = get(_usercode_cache, t, nothing) + r !== nothing && return r::Bool + is_compiled_module(Base.moduleroot(t.name.module)) || return true + push!(inprogress, t) + try + for p in t.parameters + _usercode_walk_param(p, inprogress, sawcycle) && return true + end + finally + delete!(inprogress, t) + end + return false + end + return false # Union{}, Module, etc. +end + +# Type parameters can be types or values; values can be types/modules/tuples that +# carry focus identity their `typeof` would erase (e.g. `Val{(MyT,)}` has the value +# parameter `(MyT,)` whose type is just `Tuple{DataType}`). +function _usercode_walk_param(@nospecialize(p), inprogress, sawcycle) + if p isa Type || p isa TypeVar || p isa Core.TypeofVararg + return _usercode_walk(p, inprogress, sawcycle) + elseif p isa Module + return !is_compiled_module(Base.moduleroot(p)) + elseif p isa Tuple + return any(x -> _usercode_walk_param(x, inprogress, sawcycle), p) + else + return _usercode_walk(typeof(p), inprogress, sawcycle) + end +end + +# ------------------------------------------------------------------ +# Breakpoint-based suppression of the fast path +# ------------------------------------------------------------------ + +# Native calls create no frames, so a breakpoint inside compiled territory could be +# reached from *any* native call (e.g. LinearAlgebra natively calling into Base). +# The only sound cheap answer is a global one: if any enabled breakpoint could live +# in a compiled module, mixed mode suppresses native execution entirely. +# +# `nothing` means "needs recomputation"; invalidated by the breakpoint-update hook. +const _bp_suppression = Ref{Union{Nothing,String}}(nothing) +const _bp_suppression_computed = Ref(false) +const _suppression_warned = Ref(false) + +function _breakpoints_updated_hook(f, bp) + _bp_suppression[] = nothing + _bp_suppression_computed[] = false + _suppression_warned[] = false + return nothing +end + +_scope_root(scope::Method) = Base.moduleroot(scope.module) +_scope_root(scope::Module) = Base.moduleroot(scope) + +function _target_root(@nospecialize(f)) + f isa Method && return Base.moduleroot(f.module) + t = Base.unwrap_unionall(classify(f)) + t isa DataType && return Base.moduleroot(t.name.module) + return nothing +end + +# A breakpoint in a *focus* package is still not safe if a loaded non-focus package +# (transitively) depends on it: a native call into the latter can reach the focus +# package without any focus-typed argument (e.g. registered JuliaInterpreter calling +# a dev'ed CodeTracking). `identify_package(where, name)` answers "is `name` a +# declared dependency of `where`" from the loaded manifest, so walking dependents +# recursively covers chains like non-focus A → focus B → focus M. +const _revdep_cache = Dict{Module,Bool}() + +_has_nonfocus_reverse_dep(M::Module) = _revdep_risk(M, Set{Module}()) + +function _revdep_risk(M::Module, inprogress::Set{Module}) + r = get(_revdep_cache, M, nothing) + r !== nothing && return r::Bool + M ∈ inprogress && return false # package graphs are acyclic; guard regardless + M === Main && return false + pkgdir(M) === nothing && return false + target = Base.PkgId(M) + target.uuid === nothing && return false + name = String(nameof(M)) + push!(inprogress, M) + result = false + seen = Set{Module}() + for (_, m) in Base.loaded_modules + R = Base.moduleroot(m) + (R ∈ seen || R === M || R === Main) && continue + push!(seen, R) + pkgid = Base.PkgId(R) + pkgid.uuid === nothing && continue + Base.identify_package(pkgid, name) == target || continue + # R depends on M: native code can reach M if R itself is native (non-focus) + # or if native code can reach R + if !is_focus_module(R) || _revdep_risk(R, inprogress) + result = true + break + end + end + delete!(inprogress, M) + return _revdep_cache[M] = result +end + +# Source locations of the non-focus ("compiled") modules, used to decide whether a +# file breakpoint could match compiled code: Julia's base and stdlib sources plus +# the package directories of every loaded non-focus root. +const _compiled_path_prefixes = Ref{Union{Nothing,Vector{String}}}(nothing) +const _compiled_source_basenames = Ref{Union{Nothing,Set{String}}}(nothing) + +function _compiled_source_dirs() + dirs = String[normpath(joinpath(Sys.BINDIR, Base.DATAROOTDIR, "julia")), Sys.STDLIB] + seen = Set{Module}() + for (_, m) in Base.loaded_modules + root = Base.moduleroot(m) + root ∈ seen && continue + push!(seen, root) + # focus packages are also risky when a loaded non-focus package depends on them + is_focus_module(root) && !_has_nonfocus_reverse_dep(root) && continue + d = pkgdir(root) + d === nothing || push!(dirs, d) + end + return unique!(String[_maybe_realpath(d) for d in dirs]) +end + +function compiled_path_prefixes() + v = _compiled_path_prefixes[] + v === nothing || return v + return _compiled_path_prefixes[] = _compiled_source_dirs() +end + +function compiled_source_basenames() + v = _compiled_source_basenames[] + v === nothing || return v + names = Set{String}() + for dir in compiled_path_prefixes() + isdir(dir) || continue + for (_, _, files) in walkdir(dir; onerror = _ -> nothing) + for f in files + endswith(f, ".jl") && push!(names, f) + end + end + end + return _compiled_source_basenames[] = names +end + +# Relative file breakpoints match by path suffix against *any* source file (e.g. +# `"sort.jl"` also matches `base/sort.jl`), so only clearly-user paths avoid +# suppressing the fast path. +function _filebp_could_target_compiled(bp) + if isabspath(bp.path) + return any(pre -> _path_within(bp.abspath, pre), compiled_path_prefixes()) + else + return basename(bp.path) ∈ compiled_source_basenames() + end +end + +""" + native_suppressed_reason() -> Union{Nothing,String} + +Return the reason mixed mode is currently running fully interpreted, or `nothing` +if the fast path is active. +""" +function native_suppressed_reason() + # code may load packages mid-command (e.g. `using` from the evaluation prompt + # or inside interpreted code); the length check is a cheap no-op otherwise + refresh_loaded_modules!() + if JuliaInterpreter.break_on_error[] || JuliaInterpreter.break_on_throw[] + return "break on error/throw is enabled" + end + if !_bp_suppression_computed[] + _bp_suppression[] = _compute_bp_suppression() + _bp_suppression_computed[] = true + end + return _bp_suppression[] +end + +function _bp_target_suppression(root::Union{Module,Nothing}, bp) + root === nothing && return "breakpoint with unrecognized target: $bp" + if !is_focus_module(root) + return "breakpoint set outside the focus set: $bp" + elseif _has_nonfocus_reverse_dep(root) + return "breakpoint in $root, which loaded non-focus packages depend on: $bp" + end + return nothing +end + +function _compute_bp_suppression() + for bp in JuliaInterpreter.breakpoints() + bp.enabled[] || continue + for inst in bp.instances + reason = _bp_target_suppression(_scope_root(inst.framecode.scope), bp) + reason === nothing || return reason + end + if bp isa JuliaInterpreter.BreakpointSignature + reason = _bp_target_suppression(_target_root(bp.f), bp) + reason === nothing || return reason + elseif bp isa JuliaInterpreter.BreakpointFileLocation + # Instances only show where the breakpoint has matched *so far*; the + # path decides whether it could still match compiled code. + if _filebp_could_target_compiled(bp) + return "file breakpoint may match outside the focus set: $bp" + end + else + return "unrecognized breakpoint type: $(typeof(bp))" + end + end + return nothing +end + +# Focus growth on explicit step-in: landing in a dependency package means the user +# wants to debug it, so it joins the session focus set (announced). Base/stdlib +# frames are routinely passed through on the way into a user callback (e.g. the +# kwarg-body method of `sort(v; by = f)`), so those only get a once-per-module hint +# instead of silently disabling the fast path. +const _stdlib_focus_hinted = Set{Module}() + +function maybe_grow_focus!(state) + state.interp isa MixedInterpreter || return nothing + frame = state.frame + frame === nothing && return nothing + scope = JuliaInterpreter.scopeof(leaf(frame)) + mod = scope isa Method ? scope.module : scope + mod isa Module || return nothing + root = Base.moduleroot(mod) + is_focus_module(root) && return nothing + if is_stdlib_root(root) || root ∈ COMPILE_OVERRIDES || root ∈ SESSION_UNFOCUSED + # explicit exclusions win over automatic growth; stdlibs are only hinted + # because focusing e.g. Base would de-facto disable the fast path + if root ∉ _stdlib_focus_hinted + push!(_stdlib_focus_hinted, root) + printstyled(stderr, "stepped into $root (not added to the focus set; " * + "`focus add $(nameof(root))` to interpret it everywhere)\n"; color=:light_black) + end + else + focus_module!(root) + printstyled(stderr, "added $root to the focus set (it will now be interpreted); " * + "`focus rm $(nameof(root))` to undo\n"; color=:light_black) + end + return nothing +end + +# Called at the start of each stepping command. Recomputes the suppression state +# (the breakpoint-update hook does not fire for e.g. `JuliaInterpreter.remove()`) +# and prints, once per suppression period, why mixed mode is not fast right now. +function maybe_warn_native_suppressed(state) + state.interp isa MixedInterpreter || return nothing + _bp_suppression_computed[] = false + reason = native_suppressed_reason() + if reason === nothing + _suppression_warned[] = false + elseif !_suppression_warned[] + _suppression_warned[] = true + printstyled(stderr, "mixed mode running fully interpreted ($reason)\n"; color=:yellow) + end + return nothing +end + +# ------------------------------------------------------------------ +# The policy and the interpreter hook +# ------------------------------------------------------------------ + +function run_native(fargs::Vector{Any}, world::UInt=Base.get_world_counter()) + native_suppressed_reason() === nothing || return false + for x in fargs + carries_focus_code(x) && return false + end + # `JuliaInterpreter.interpreted_methods` has documented precedence over any + # compiled-mode mechanism. It is empty by default, so the method lookup below + # is not paid in the common case. + if !isempty(JuliaInterpreter.interpreted_methods) + sig = Tuple{Base.mapany(JuliaInterpreter._Typeof, fargs)...} + method = try + Base.invoke_in_world(world, which, sig) + catch + nothing + end + method !== nothing && method ∈ JuliaInterpreter.interpreted_methods && return false + end + return true +end + +function JuliaInterpreter.evaluate_call!(interp::MixedInterpreter, frame::Frame, + fargs::Vector{Any}, enter_generated::Bool) + f = fargs[1] + # `Core.eval`, `rethrow` and `Core.invoke` have dedicated handling in the generic + # method that must not be bypassed by a native call. + if f === Core.eval || f === Base.rethrow || f === Core.invoke || !run_native(fargs, frame.world) + return @invoke JuliaInterpreter.evaluate_call!(interp::Interpreter, frame::Frame, + fargs::Vector{Any}, enter_generated::Bool) + end + return JuliaInterpreter.native_call(fargs, frame) +end + +# ------------------------------------------------------------------ +# Modes +# ------------------------------------------------------------------ + +const DEFAULT_MODE = Ref{Symbol}(:mixed) + +function interp_for_mode(mode::Symbol) + mode === :interpreted && return RecursiveInterpreter() + mode === :mixed && return MixedInterpreter() + mode === :compiled && return NonRecursiveInterpreter() + throw(ArgumentError("unknown stepping mode :$mode (expected :interpreted, :mixed or :compiled)")) +end + +mode_of_interp(::RecursiveInterpreter) = :interpreted +mode_of_interp(::MixedInterpreter) = :mixed +mode_of_interp(::NonRecursiveInterpreter) = :compiled + +""" + Debugger.set_default_mode!(mode::Symbol) + +Set the stepping mode future debug sessions start in (suitable for `startup.jl`). +`mode` is one of: + +- `:mixed` (default): run Base/stdlib code natively unless the call can reach user + code (user functions or types among the arguments). Much faster than interpreting + everything; see the manual for the cases it cannot catch. +- `:interpreted`: interpret everything; breakpoints always work. +- `:compiled`: run everything below the current frame natively (like the `C` toggle). + +The mode of a running session is changed with the `mode` debugger command, or the +`M` (mixed) and `C` (compiled) keys. +""" +function set_default_mode!(mode::Symbol) + interp_for_mode(mode) # validate + DEFAULT_MODE[] = mode + return mode +end + +""" + Debugger.default_mode() -> Symbol + +The stepping mode new debug sessions start in. See [`set_default_mode!`](@ref). +""" +default_mode() = DEFAULT_MODE[] + +# Drop all session/policy state so it is recomputed from scratch. Used by the +# precompile workload (no policy state may be serialized into the pkgimage) and +# defensively at `__init__`. +function reset_mixed_mode_state!() + FOCUS[] = nothing + SESSION_ENTRY_ROOT[] = nothing + _pending_entry_hint[] = nothing + empty!(SESSION_UNFOCUSED) + empty!(_dev_package_cache) + empty!(_stdlib_focus_hinted) + _loaded_fingerprint[] = -1 + invalidate_policy_cache!() + return nothing +end + +function init_mixed_mode() + empty!(STDLIB_NAMES) + for name in readdir(Sys.STDLIB) + isdir(joinpath(Sys.STDLIB, name)) && push!(STDLIB_NAMES, Symbol(name)) + end + isdefined(Base, :Compiler) && (_compiler_root[] = Base.moduleroot(Base.Compiler::Module)) + reset_mixed_mode_state!() + JuliaInterpreter.on_breakpoints_updated(_breakpoints_updated_hook) + return nothing +end diff --git a/src/precompile.jl b/src/precompile.jl index 08c62ce..188be23 100644 --- a/src/precompile.jl +++ b/src/precompile.jl @@ -49,4 +49,8 @@ end empty!(WATCH_LIST) JuliaInterpreter.remove() _ALT_SCREEN[] = false + # The session ran under the mixed-mode default and populated the focus set + # and policy caches with references to the precompile process' modules; + # reset so nothing stale is serialized (reclassified lazily at runtime). + reset_mixed_mode_state!() end diff --git a/src/repl.jl b/src/repl.jl index 4e3c5ab..7b3c46f 100644 --- a/src/repl.jl +++ b/src/repl.jl @@ -1,6 +1,11 @@ promptname(level, name) = "$level|$name> " +# Mixed mode is the default, so it keeps the plain debug prompt (in the +# traditional orange); the other modes are called out by name and color. +mode_prompt_name(state) = state.interp isa NonRecursiveInterpreter ? "compiled" : + state.interp isa RecursiveInterpreter ? "interpreted" : "debug" + function write_prompt(terminal, mode) LineEdit.write_prompt(terminal, mode, LineEdit.hascolor(terminal)) end @@ -77,12 +82,16 @@ function RunDebugger(frame, repl = nothing, terminal = nothing; initial_continue terminal = Base.active_repl.t end state = DebuggerState(; frame=frame, repl=repl, terminal=terminal) + seed_focus!(frame) # Setup debug panel normal_prefix = Sys.iswindows() ? "\e[33m" : "\e[38;5;166m" compiled_prefix = "\e[96m" - panel = LineEdit.Prompt(promptname(state.level, "debug"); - prompt_prefix = () -> state.interp == NonRecursiveInterpreter() ? compiled_prefix : normal_prefix, + # bright magenta; green would be confusable with the julia> prompt + interpreted_prefix = "\e[95m" + panel = LineEdit.Prompt(promptname(state.level, mode_prompt_name(state)); + prompt_prefix = () -> state.interp isa NonRecursiveInterpreter ? compiled_prefix : + state.interp isa RecursiveInterpreter ? interpreted_prefix : normal_prefix, prompt_suffix = Base.text_colors[:normal], on_enter = s->true) @@ -98,7 +107,6 @@ function RunDebugger(frame, repl = nothing, terminal = nothing; initial_continue panel.on_done = (s,buf,ok)->begin line = String(take!(buf)) - old_level = state.level if !ok || strip(line) == "q" LineEdit.transition(s, :abort) LineEdit.reset_state(s) @@ -136,9 +144,8 @@ function RunDebugger(frame, repl = nothing, terminal = nothing; initial_continue LineEdit.reset_state(s) return false end - if old_level != state.level - panel.prompt = promptname(state.level, "debug") - end + # both the level and the stepping mode are part of the prompt + panel.prompt = promptname(state.level, mode_prompt_name(state)) LineEdit.reset_state(s) if state.frame === nothing LineEdit.transition(s, :abort) @@ -166,12 +173,29 @@ function RunDebugger(frame, repl = nothing, terminal = nothing; initial_continue 'C' => function (s, args...) if isempty(s) || position(LineEdit.buffer(s)) == 0 toggle_mode(state) - write(state.terminal, '\r') + panel.prompt = promptname(state.level, mode_prompt_name(state)) + io = output_stream(state) + println(io) + show_status(state) + printstyled(io, "Stepping mode: ", mode_description(state.interp), "\n"; color=:light_black) write_prompt(state.terminal, panel) else LineEdit.edit_insert(s, "C") end end, + 'M' => function (s, args...) + if isempty(s) || position(LineEdit.buffer(s)) == 0 + toggle_mixed(state) + panel.prompt = promptname(state.level, mode_prompt_name(state)) + io = output_stream(state) + println(io) + show_status(state) + printstyled(io, "Stepping mode: ", mode_description(state.interp), "\n"; color=:light_black) + write_prompt(state.terminal, panel) + else + LineEdit.edit_insert(s, "M") + end + end, 'L' => function (s, args...) if isempty(s) || position(LineEdit.buffer(s)) == 0 toggle_lowered(state) @@ -265,6 +289,9 @@ function RunDebugger(frame, repl = nothing, terminal = nothing; initial_continue JuliaInterpreter.maybe_next_call!(state.frame) end show_status(state) + # transient notices must come after the status draw, which erases + # everything below itself in sticky mode + maybe_print_entry_hint(state) prompts = LineEdit.TextInterface[panel] diff --git a/test/menus.jl b/test/menus.jl index 745d701..89a25c6 100644 --- a/test/menus.jl +++ b/test/menus.jl @@ -255,6 +255,53 @@ end end end +@testset "focus menu" begin + Debugger.FOCUS[] = nothing + Debugger.invalidate_policy_cache!() + try + # toggle Main off with space (row order depends on the environment, so + # navigate to it), back on with enter; the row stays for re-toggling + rows = Debugger.focus_menu_rows() + idx = findfirst(==(Main), rows) + state = menu_state(nothing, fill(:down, idx - 1)..., ' ', 'q') + quietly(() -> Debugger.focus_menu(state)) + @test Main ∉ Debugger.interpreted_modules() + rows = Debugger.focus_menu_rows() + idx = findfirst(==(Main), rows) + @test idx !== nothing # still shown, just toggled off + state = menu_state(nothing, fill(:down, idx - 1)..., :enter, 'q') + quietly(() -> Debugger.focus_menu(state)) + @test Main ∈ Debugger.interpreted_modules() + + # add a module from the menu: `a` prompts for a name and reopens + state = menu_state(nothing, 'a', "JuliaInterpreter\n", 'q') + quietly(() -> Debugger.focus_menu(state)) + @test JuliaInterpreter ∈ Debugger.interpreted_modules() + + # row rendering + buf = IOBuffer() + Debugger.focus_menu_writerow(IOContext(buf, :displaysize => (24, 80)), Main, 1) + @test occursin("Main", String(take!(buf))) + finally + Debugger.FOCUS[] = nothing + empty!(Debugger.SESSION_UNFOCUSED) + Debugger.invalidate_policy_cache!() + end +end + +@testset "focus command falls back to a text list without menus" begin + Debugger.config(menus = false) + try + state = menu_state(nothing) + execute_command(state, Val{:focus}(), "focus") + out = String(take!(state.terminal.out_stream)) + @test occursin("Focus set", out) + @test occursin("Main", out) + finally + Debugger.config(menus = true) + end +end + @testset "frameless bp commands (debug> mode)" begin JuliaInterpreter.remove() try diff --git a/test/mixed_mode.jl b/test/mixed_mode.jl new file mode 100644 index 0000000..4343bac --- /dev/null +++ b/test/mixed_mode.jl @@ -0,0 +1,380 @@ +module MixedModeTests + +using Debugger: Debugger, MixedInterpreter, run_native, carries_focus_code, + native_suppressed_reason, compile_module!, interpret_module!, + interpreted_modules, interp_for_mode, set_default_mode!, default_mode, + focus_module!, unfocus_module!, seed_focus!, is_focus_module +using JuliaInterpreter +using JuliaInterpreter: debug_command, BreakpointRef, @bp +import CodeTracking +import REPL +using Test + +struct MyT + x::Int +end +struct MyCallable end +(::MyCallable)(x) = x + 1 +user_f(x) = 2x + +inner(x) = x * 2 +map_entry(xs) = sum(map(inner, xs)) + length(xs) +sortby_entry(xs) = sort(xs; by = inner) +bcast_entry(xs) = inner.(xs) +doblock_entry() = sprint(io -> print(io, inner(3))) +splat_entry(xs) = inner(xs...) +gcd_entry() = gcd(10, 20) + 0 +sum_entry(x) = sum(abs2, 1:x) + 1 +catcher() = try + sqrt(-1.0) +catch + 42 +end +bp_cb(x) = (@bp; x + 1) +bp_entry(xs) = sum(map(bp_cb, xs)) +const ji_breakpoints = JuliaInterpreter.breakpoints +ji_entry() = ji_breakpoints() isa Vector +gcd_step_entry() = gcd(4, 6) > 0 + +mkstate(frame) = Debugger.DebuggerState(frame = frame) + +fresh_suppression!() = (Debugger._bp_suppression_computed[] = false) + +@testset "policy: carries_focus_code" begin + @test !carries_focus_code(sin) + @test !carries_focus_code([1, 2, 3]) + @test !carries_focus_code("abc") + @test !carries_focus_code(Union{Int, Missing}[]) + @test !carries_focus_code(AbstractVector) + @test carries_focus_code(user_f) + @test carries_focus_code(MyT) # constructors are not DataType/Core + @test carries_focus_code(MyCallable()) # callable struct + @test carries_focus_code(MyT[]) # user type as type parameter + @test carries_focus_code(AbstractVector{MyT}) + @test carries_focus_code(Base.Generator(user_f, [1])) + @test carries_focus_code((by = user_f,)) # keyword-call NamedTuple + @test carries_focus_code(Val(user_f)) # value type parameter + @test carries_focus_code(Val((user_f,))) # focus function inside a tuple value parameter + @test !carries_focus_code(Val((1, 2))) + @test carries_focus_code(TypeVar(:T, Union{}, MyT)) # TypeVar value with focus bound + @test carries_focus_code(Vararg{MyT}) # top-level Vararg value + @test !carries_focus_code(Vararg{Int}) + # Registered dependencies are not in focus; a devved JuliaInterpreter is. + ji_focused = is_focus_module(Base.moduleroot(JuliaInterpreter)) + @test carries_focus_code(JuliaInterpreter.breakpoints) == ji_focused + @test carries_focus_code(JuliaInterpreter.BreakpointRef[]) == ji_focused + # modules and GlobalRefs can smuggle focus code without focus-typed args + @test carries_focus_code(Main) + @test carries_focus_code(@__MODULE__) + @test !carries_focus_code(Base) + @test carries_focus_code(GlobalRef(Main, :x)) + @test !carries_focus_code(GlobalRef(Base, :sin)) +end + +@testset "path containment" begin + @test Debugger._path_within("/a/packages/X", "/a/packages") + @test Debugger._path_within("/a/packages", "/a/packages") + @test !Debugger._path_within("/a/packages-dev/X", "/a/packages") +end + +@testset "policy: run_native" begin + @test run_native(Any[sort, [3, 1, 2]]) + @test !run_native(Any[map, user_f, [1, 2]]) + @test !run_native(Any[MyT, 1]) + @test !run_native(Any[Core.kwcall, (by = user_f,), sort, [3, 1, 2]]) + # Registered dependencies run natively; devved dependencies stay interpretable. + ji_focused = is_focus_module(Base.moduleroot(JuliaInterpreter)) + @test run_native(Any[JuliaInterpreter.breakpoints]) == !ji_focused + @test run_native(Any[map, JuliaInterpreter.leaf, JuliaInterpreter.Frame[]]) == !ji_focused + # documented limitation: type-erased containers hide user code + @test run_native(Any[foreach, print, Any[user_f]]) +end + +@testset "focus set" begin + mods = interpreted_modules() + @test Main ∈ mods # test module's root is Main + @test Base ∉ mods && Core ∉ mods + ji_is_dev = Debugger.is_dev_package(JuliaInterpreter) + @test (JuliaInterpreter ∈ mods) == ji_is_dev + # under PkgEval Debugger itself is a registered install, so compare rather than assert + @test (Debugger ∈ mods) == Debugger.is_dev_package(Debugger) + + # session-scoped add/remove + unfocus_module!(JuliaInterpreter) + @test focus_module!(JuliaInterpreter) + @test JuliaInterpreter ∈ interpreted_modules() + @test !run_native(Any[JuliaInterpreter.breakpoints]) + @test unfocus_module!(JuliaInterpreter) + @test run_native(Any[JuliaInterpreter.breakpoints]) + + # permanent overrides + try + compile_module!(@__MODULE__) # root is Main + @test run_native(Any[user_f, 1]) + interpret_module!(@__MODULE__) + @test !run_native(Any[user_f, 1]) + finally + delete!(Debugger.COMPILE_OVERRIDES, Base.moduleroot(@__MODULE__)) + delete!(Debugger.INTERPRET_OVERRIDES, Base.moduleroot(@__MODULE__)) + Debugger.FOCUS[] = nothing + Debugger.invalidate_policy_cache!() + end + @test !run_native(Any[user_f, 1]) +end + +@testset "focus seeding" begin + # entering a dependency function focuses its package + frame = JuliaInterpreter.enter_call(CodeTracking.maybe_fix_path, "/no/such/path.jl") + seed_focus!(frame) + @test CodeTracking ∈ interpreted_modules() + # entering a Base function does not drag Base into focus, but it is + # remembered (hint printed once) and offered as a row in the focus menu. + # The hint is once-per-process; earlier tests may have stepped into Base + delete!(Debugger._stdlib_focus_hinted, Base) + Debugger._pending_entry_hint[] = nothing + frame = JuliaInterpreter.enter_call(gcd, 10, 20) + seed_focus!(frame) + @test Base ∉ interpreted_modules() + @test CodeTracking ∉ interpreted_modules() # reseed dropped the previous entry root + @test Main ∈ interpreted_modules() + @test Base ∈ Debugger._stdlib_focus_hinted + @test Base ∈ Debugger.focus_menu_rows() + # the hint is stashed at seed time (printing there would be swallowed by + # the alternate-screen switch) and printed after the first status draw + @test Debugger._pending_entry_hint[] === Base + term = REPL.Terminals.TTYTerminal("xterm", IOBuffer(), IOBuffer(), IOBuffer()) + st = Debugger.DebuggerState(frame = nothing, terminal = term) + Debugger.maybe_print_entry_hint(st) + @test occursin("focus add Base", String(take!(term.out_stream))) + @test Debugger._pending_entry_hint[] === nothing +end + +@testset "JuliaInterpreter.interpreted_methods precedence" begin + m = which(sort, (Vector{Int},)) + push!(JuliaInterpreter.interpreted_methods, m) + try + @test !run_native(Any[sort, [3, 1, 2]]) + finally + delete!(JuliaInterpreter.interpreted_methods, m) + end + @test run_native(Any[sort, [3, 1, 2]]) +end + +@testset "breakpoint suppression" begin + JuliaInterpreter.remove(); fresh_suppression!() + @test native_suppressed_reason() === nothing + + # breakpoints in user code do not suppress the fast path + bp = JuliaInterpreter.breakpoint(inner); fresh_suppression!() + @test native_suppressed_reason() === nothing + JuliaInterpreter.remove(bp) + bp = JuliaInterpreter.breakpoint(MyT); fresh_suppression!() + @test native_suppressed_reason() === nothing + JuliaInterpreter.remove(bp) + + # breakpoints in compiled modules suppress it + bp = JuliaInterpreter.breakpoint(sort); fresh_suppression!() + @test native_suppressed_reason() !== nothing + @test !run_native(Any[sort, [3, 1, 2]]) + JuliaInterpreter.remove(bp); fresh_suppression!() + @test native_suppressed_reason() === nothing + + # disabled breakpoints do not suppress + bp = JuliaInterpreter.breakpoint(sort) + JuliaInterpreter.disable(bp); fresh_suppression!() + @test native_suppressed_reason() === nothing + JuliaInterpreter.remove(bp) + + # break_on suppresses + JuliaInterpreter.break_on(:error) + @test native_suppressed_reason() !== nothing + JuliaInterpreter.break_off(:error); fresh_suppression!() + @test native_suppressed_reason() === nothing + + # a breakpoint in a focus package that loaded non-focus packages depend on + # also suppresses (native JuliaInterpreter code can reach CodeTracking) + @test Debugger._has_nonfocus_reverse_dep(CodeTracking) + @test !Debugger._has_nonfocus_reverse_dep(Debugger) + try + focus_module!(CodeTracking) + bp = JuliaInterpreter.breakpoint(CodeTracking.maybe_fix_path); fresh_suppression!() + @test occursin("depend on", something(native_suppressed_reason(), "")) + JuliaInterpreter.remove(bp) + finally + unfocus_module!(CodeTracking) + empty!(Debugger.SESSION_UNFOCUSED) + fresh_suppression!() + end + @test native_suppressed_reason() === nothing +end + +@testset "reset_module!" begin + try + compile_module!(@__MODULE__) # root is Main + @test Main ∉ interpreted_modules() + focus_module!(JuliaInterpreter) # unrelated session choice + Debugger.reset_module!(@__MODULE__) + @test Main ∈ interpreted_modules() + @test JuliaInterpreter ∈ interpreted_modules() # survived the reset + finally + delete!(Debugger.COMPILE_OVERRIDES, Main) + Debugger.FOCUS[] = nothing + empty!(Debugger.SESSION_UNFOCUSED) + Debugger.invalidate_policy_cache!() + end +end + +@testset "julia internals are not auto-focused" begin + if isdefined(Base, :Compiler) + @test Debugger.is_stdlib_root(Base.moduleroot(Base.Compiler)) + end + # ...but a user module that happens to be named Compiler is + @test !Debugger.is_stdlib_root(Module(:Compiler)) +end + +@testset "reset_module! keeps the entered package focused" begin + try + frame = JuliaInterpreter.enter_call(CodeTracking.maybe_fix_path, "/no/such/path.jl") + seed_focus!(frame) + @test CodeTracking ∈ interpreted_modules() # focused as the entry root + compile_module!(CodeTracking) + @test CodeTracking ∉ interpreted_modules() + Debugger.reset_module!(CodeTracking) + @test CodeTracking ∈ interpreted_modules() # entry-root focus restored + + # same but with the override in place before the session starts + compile_module!(CodeTracking) + frame = JuliaInterpreter.enter_call(CodeTracking.maybe_fix_path, "/no/such/path.jl") + seed_focus!(frame) + @test CodeTracking ∉ interpreted_modules() # override wins at seed time + Debugger.reset_module!(CodeTracking) + @test CodeTracking ∈ interpreted_modules() # entry root remembered anyway + finally + delete!(Debugger.COMPILE_OVERRIDES, CodeTracking) + Debugger.SESSION_ENTRY_ROOT[] = nothing + Debugger.FOCUS[] = nothing + Debugger.invalidate_policy_cache!() + end +end + +@testset "file breakpoint path classification" begin + mkbp(path) = JuliaInterpreter.BreakpointFileLocation( + path, CodeTracking.maybe_fix_path(abspath(path)), 1, nothing, Ref(true), BreakpointRef[]) + # relative paths match by suffix against any file, including Base sources + @test Debugger._filebp_could_target_compiled(mkbp("sort.jl")) + @test !Debugger._filebp_could_target_compiled(mkbp("surely_not_a_base_file.jl")) + base_sort = joinpath(Sys.BINDIR, Base.DATAROOTDIR, "julia", "base", "sort.jl") + @test Debugger._filebp_could_target_compiled(mkbp(base_sort)) + userfile = joinpath(mktempdir(), "userscript.jl") + write(userfile, "f() = 1\n") + @test !Debugger._filebp_could_target_compiled(mkbp(userfile)) + + bp = JuliaInterpreter.breakpoint(userfile, 1); fresh_suppression!() + @test native_suppressed_reason() === nothing + JuliaInterpreter.remove(bp) +end + +function break_leaf_scope(entry, args...) + frame = JuliaInterpreter.enter_call(entry, args...) + ret = debug_command(MixedInterpreter(), frame, :c) + ret === nothing && return nothing + frame2, pc = ret + pc isa BreakpointRef || return nothing + return JuliaInterpreter.scopeof(JuliaInterpreter.leaf(frame2)).name +end + +@testset "breakpoints in user callbacks fire through Base higher-order functions" begin + JuliaInterpreter.breakpoint(inner) + try + @test break_leaf_scope(map_entry, [1, 2, 3]) === :inner + @test break_leaf_scope(sortby_entry, [3, 1, 2]) === :inner + @test break_leaf_scope(bcast_entry, [1, 2, 3]) === :inner + @test break_leaf_scope(doblock_entry) === :inner + @test break_leaf_scope(splat_entry, (3,)) === :inner + finally + JuliaInterpreter.remove() + fresh_suppression!() + end +end + +@testset "breakpoint in Base fires because suppression interprets everything" begin + JuliaInterpreter.breakpoint(gcd) + try + @test break_leaf_scope(gcd_entry) === :gcd + finally + JuliaInterpreter.remove() + fresh_suppression!() + end +end + +@testset "native execution is correct" begin + frame = JuliaInterpreter.enter_call(sum_entry, 10) + @test debug_command(MixedInterpreter(), frame, :finish) === nothing + @test JuliaInterpreter.get_return(frame) == sum(abs2, 1:10) + 1 + + # an error thrown inside a native call is caught by the interpreted caller + frame = JuliaInterpreter.enter_call(catcher) + @test debug_command(MixedInterpreter(), frame, :finish) === nothing + @test JuliaInterpreter.get_return(frame) == 42 +end + +@testset "@bp fires through Base higher-order functions" begin + frame = JuliaInterpreter.enter_call(bp_entry, [1, 2]) + ret = debug_command(MixedInterpreter(), frame, :c) + @test ret !== nothing && ret[2] isa BreakpointRef + @test JuliaInterpreter.scopeof(JuliaInterpreter.leaf(ret[1])).name === :bp_cb +end + +@testset "focus grows on step-in" begin + Debugger.FOCUS[] = nothing + Debugger.invalidate_policy_cache!() + try + # stepping into a dependency package adds it to the session focus set + state = mkstate(JuliaInterpreter.enter_call(ji_entry)) + @test state.interp isa MixedInterpreter + Debugger.execute_command(state, Val{:s}(), "s") + @test JuliaInterpreter ∈ interpreted_modules() + # stepping into Base does not + state = mkstate(JuliaInterpreter.enter_call(gcd_step_entry)) + Debugger.execute_command(state, Val{:s}(), "s") + @test Base ∉ interpreted_modules() + finally + Debugger.FOCUS[] = nothing + Debugger.invalidate_policy_cache!() + end +end + +@testset "modes" begin + @test default_mode() === :mixed + @test interp_for_mode(:interpreted) isa JuliaInterpreter.RecursiveInterpreter + @test interp_for_mode(:mixed) isa MixedInterpreter + @test interp_for_mode(:compiled) isa JuliaInterpreter.NonRecursiveInterpreter + @test_throws ArgumentError interp_for_mode(:bogus) + @test_throws ArgumentError set_default_mode!(:bogus) + state = Debugger.DebuggerState(frame = nothing) + @test state.interp isa MixedInterpreter + try + set_default_mode!(:interpreted) + state = Debugger.DebuggerState(frame = nothing) + @test state.interp isa JuliaInterpreter.RecursiveInterpreter + finally + set_default_mode!(:mixed) + end + # M toggles interpreted and back; C toggles compiled and back + state = Debugger.DebuggerState(frame = nothing) + Debugger.toggle_mixed(state) + @test state.interp isa JuliaInterpreter.RecursiveInterpreter + Debugger.toggle_mixed(state) + @test state.interp isa MixedInterpreter + Debugger.toggle_mode(state) + @test state.interp isa JuliaInterpreter.NonRecursiveInterpreter + Debugger.toggle_mode(state) + @test state.interp isa MixedInterpreter + # `mode compiled` followed by `C` returns to the previous mode too + Debugger.set_session_mode!(state, :compiled) + @test state.interp isa JuliaInterpreter.NonRecursiveInterpreter + Debugger.toggle_mode(state) + @test state.interp isa MixedInterpreter +end + +end # module MixedModeTests diff --git a/test/runtests.jl b/test/runtests.jl index 34d9988..1dc88c2 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -20,5 +20,6 @@ Debugger.set_highlight(false) include("stepping.jl") include("interpret.jl") include("menus.jl") + include("mixed_mode.jl") include("ui.jl") #end diff --git a/test/ui.jl b/test/ui.jl index f52d0e8..546f6a1 100644 --- a/test/ui.jl +++ b/test/ui.jl @@ -105,7 +105,11 @@ end print_full_path = Debugger._print_full_path[] source_lines = Debugger.NUM_SOURCE_LINES_UP_DOWN[] watch_list = copy(Debugger.WATCH_LIST) + saved_mode = Debugger.default_mode() Debugger._print_full_path[] = false + # the recorded terminal sessions were captured in mixed mode (the default); + # pin it in case the process default was changed + Debugger.set_default_mode!(:mixed) try using TerminalRegressionTests @@ -232,6 +236,7 @@ end Debugger.NUM_SOURCE_LINES_UP_DOWN[] = source_lines empty!(Debugger.WATCH_LIST) append!(Debugger.WATCH_LIST, watch_list) + Debugger.set_default_mode!(saved_mode) end else @warn "Skipping UI tests" diff --git a/test/ui/history_gcd.multiout b/test/ui/history_gcd.multiout index e93b45b..813ca80 100644 --- a/test/ui/history_gcd.multiout +++ b/test/ui/history_gcd.multiout @@ -2843,6 +2843,20 @@ | |→ (===)(20, 0) |1|debug> +|[1/2] ==(x, y) at promotion.jl:637 +|>637 (==)(x::T, y::T) where {T<:Number} = x === y +| +| x::Int64 = 20 (arg) +| y::Int64 = 0 (arg) +| T = Int64 (param) +| +| w1] sin(a): UndefVarError: `a` not defined in `Base` +| w2] b: UndefVarError: `b` not defined in `Base` +| +|→ (===)(20, 0) +|Stepping mode: compiled (stepped-over code runs natively, breakpoints in it are +|missed) +|1|compiled> -------------------------------------------------- |AAAAAABBBBBBBBBBBBCCCCCCCCCCC |CCCCBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB @@ -2975,7 +2989,21 @@ |CCCCCCBBBHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH | |EEBBBBBBBBBBBB -|IIIIIIIII +|FFFFFFFFF +|AAAAAABBBBBBBBCCCCCCCCCCCCCCCCCCCC +|DDDDDDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +| +|BBBBBBBBBBBBBBBCCCCCCC +|BBBBBBBBBBBBBBCCCCCCC +|BBBBBBBBBBBBBBBBBBCCCCCCCCC +| +|CCCCCCBBBBBBBBHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH +|CCCCCCBBBHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH +| +|EEBBBBBBBBBBBB +|CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC +|CCCCCCC +|IIIIIIIIIIII ++++++++++++++++++++++++++++++++++++++++++++++++++ |[1/1] my_gcd(a, b) at ui.jl:5 | 4 function my_gcd(a::T, b::T) where T<:Union{Int8,UInt8,Int16,UInt16,Int32,UIn @@ -3108,7 +3136,21 @@ | w2] b: UndefVarError: `b` not defined in `Base` | |→ (===)(20, 0) -|1|debug> c +|1|debug> +|[1/2] ==(x, y) at promotion.jl:637 +|>637 (==)(x::T, y::T) where {T<:Number} = x === y +| +| x::Int64 = 20 (arg) +| y::Int64 = 0 (arg) +| T = Int64 (param) +| +| w1] sin(a): UndefVarError: `a` not defined in `Base` +| w2] b: UndefVarError: `b` not defined in `Base` +| +|→ (===)(20, 0) +|Stepping mode: compiled (stepped-over code runs natively, breakpoints in it are +|missed) +|1|compiled> c |Hit breakpoint: |[1/1] my_gcd(a, b) at ui.jl:13 | 9 k = min(za, zb) @@ -3134,7 +3176,7 @@ | w2] b: 20 | |→ (!=)(0x0000000000000005, 0x0000000000000005) -|1|debug> +|1|compiled> -------------------------------------------------- |AAAAAABBBBBBBBBBBBCCCCCCCCCCC |CCCCBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB @@ -3267,7 +3309,21 @@ |CCCCCCBBBHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH | |EEBBBBBBBBBBBB -|IIIIIIIIIB +|FFFFFFFFF +|AAAAAABBBBBBBBCCCCCCCCCCCCCCCCCCCC +|DDDDDDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +| +|BBBBBBBBBBBBBBBCCCCCCC +|BBBBBBBBBBBBBBCCCCCCC +|BBBBBBBBBBBBBBBBBBCCCCCCCCC +| +|CCCCCCBBBBBBBBHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH +|CCCCCCBBBHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH +| +|EEBBBBBBBBBBBB +|CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC +|CCCCCCC +|IIIIIIIIIIIIB |BBBBBBBBBBBBBBB |AAAAAABBBBBBBBBBBBCCCCCCCCCCCC |CCCCCBBBBBBBBBBBBBBB @@ -3293,7 +3349,7 @@ |CCCCCCBBBBB | |EEBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB -|IIIIIIIII +|IIIIIIIIIIII ++++++++++++++++++++++++++++++++++++++++++++++++++ |[1/1] my_gcd(a, b) at ui.jl:5 | 4 function my_gcd(a::T, b::T) where T<:Union{Int8,UInt8,Int16,UInt16,Int32,UIn @@ -3426,7 +3482,21 @@ | w2] b: UndefVarError: `b` not defined in `Base` | |→ (===)(20, 0) -|1|debug> c +|1|debug> +|[1/2] ==(x, y) at promotion.jl:637 +|>637 (==)(x::T, y::T) where {T<:Number} = x === y +| +| x::Int64 = 20 (arg) +| y::Int64 = 0 (arg) +| T = Int64 (param) +| +| w1] sin(a): UndefVarError: `a` not defined in `Base` +| w2] b: UndefVarError: `b` not defined in `Base` +| +|→ (===)(20, 0) +|Stepping mode: compiled (stepped-over code runs natively, breakpoints in it are +|missed) +|1|compiled> c |Hit breakpoint: |[1/1] my_gcd(a, b) at ui.jl:13 | 9 k = min(za, zb) @@ -3452,6 +3522,33 @@ | w2] b: 20 | |→ (!=)(0x0000000000000005, 0x0000000000000005) +|1|compiled> +|[1/1] my_gcd(a, b) at ui.jl:13 +| 9 k = min(za, zb) +| 10 u = unsigned(abs(a >> za)) +| 11 v = unsigned(abs(b >> zb)) +|●12 Debugger.@bp +|>13 while u != v +| 14 if u > v +| 15 u, v = v, u +| 16 end +| 17 v -= u +| +| a::Int64 = 10 (arg) +| b::Int64 = 20 (arg) +| T = Int64 (param) +| v::UInt64 = 0x0000000000000005 +| u::UInt64 = 0x0000000000000005 +| k::Int64 = 1 +| zb::Int64 = 2 +| za::Int64 = 1 +| +| w1] sin(a): -0.5440211108893698 +| w2] b: 20 +| +|→ (!=)(0x0000000000000005, 0x0000000000000005) +|Stepping mode: mixed (code outside the focus modules runs natively unless it can +| reach them) |1|debug> -------------------------------------------------- |AAAAAABBBBBBBBBBBBCCCCCCCCCCC @@ -3585,8 +3682,47 @@ |CCCCCCBBBHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH | |EEBBBBBBBBBBBB -|IIIIIIIIIB +|FFFFFFFFF +|AAAAAABBBBBBBBCCCCCCCCCCCCCCCCCCCC +|DDDDDDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +| +|BBBBBBBBBBBBBBBCCCCCCC +|BBBBBBBBBBBBBBCCCCCCC +|BBBBBBBBBBBBBBBBBBCCCCCCCCC +| +|CCCCCCBBBBBBBBHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH +|CCCCCCBBBHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH +| +|EEBBBBBBBBBBBB +|CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC +|CCCCCCC +|IIIIIIIIIIIIB +|BBBBBBBBBBBBBBB +|AAAAAABBBBBBBBBBBBCCCCCCCCCCCC +|CCCCCBBBBBBBBBBBBBBB +|CCCCCBBBBBBBBBBBBBBBBBBBBBBBBBB +|CCCCCBBBBBBBBBBBBBBBBBBBBBBBBBB +|HHHHHBBBBBBBBBBBB +|DDDDDAAAAAAAAAAAA +|CCCCCBBBBBBBBBBBB +|CCCCCBBBBBBBBBBBBBBBBBBB +|CCCCCBBBBBBB +|CCCCCBBBBBBBBBB +| +|BBBBBBBBBBBBBBBBCCCCCCC +|BBBBBBBBBBBBBBBBCCCCCCC +|BBBBBBBBBBBBBBBBBBBCCCCCCCCC +|BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB +|BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB |BBBBBBBBBBBBBBB +|BBBBBBBBBBBBBBB +|BBBBBBBBBBBBBBB +| +|CCCCCCBBBBBBBBBBBBBBBBBBBBBBBBBBB +|CCCCCCBBBBB +| +|EEBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB +|IIIIIIIIIIII |AAAAAABBBBBBBBBBBBCCCCCCCCCCCC |CCCCCBBBBBBBBBBBBBBB |CCCCCBBBBBBBBBBBBBBBBBBBBBBBBBB @@ -3611,4 +3747,6 @@ |CCCCCCBBBBB | |EEBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB +|CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC +|CCCCCCCCCCCC |FFFFFFFFF \ No newline at end of file