diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..d7d9cc8b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog for Cthulhu + +## `v3.0.0` + +### Improvements + +- The way that packages integrate with Cthulhu to customize the behavior of code introspection has been redesigned (see https://github.com/JuliaDebug/Cthulhu.jl/pull/662 for more details). +- A new UI command mapped to `DEL` (backspace, `'\x7f'`) now allows to go back (ascend) with a single key press. + +### Breaking changes + +- The `AbstractCursor` interface was removed, deleting or changing the signature of most associated methods (`Cthulhu.AbstractCursor`, `Cthulhu.lookup`, `Cthulhu.lookup_constproped`, `Cthulhu.get_ci`, `Cthulhu.update_cursor`, `Cthulhu.navigate`) +- The `Cthulhu.custom_toggles` interface was removed, along with `Cthulhu.CustomToggle`, replaced by `Cthulhu.menu_commands` and `Cthulhu.Command`, respectively. +- `CthulhuConfig` is no longer mutable (but `CthulhuState`, the new state structure holding it, is mutable). `CONFIG` was therefore changed to a global typed but non-const variable, which may be reassigned by users (instead of mutating its fields) with `Cthulhu.CONFIG = my_new_config`. See [`set_config`] and [`set_config!`] to modify an existing configuration, and [`save_config!`] to persist a configuration with Preferences.jl. +- The `annotate_source` configuration option was removed in favor of a `view::Symbol` configuration option, with the `:source|:ast|:typed|:llvm|:native` available views by default (more may be defined by providers). +- The `exception_type` configuration option was renamed to `exception_types` for consistency with `remarks`. +- The `with_effects` configuration option was renamed to `effects` for consistency with `remarks` and `exception_types`. +- The `inline_cost` configuration option was renamed to `inlining_costs`, also for consistency reasons. +- The `interruptexc` configuration option was removed. It used to control whether `q` exited (by throwing an `InterruptException`) or ascended, but now that backspace was added as a shortcut to ascend, we can now unconditionally exit with `q` (which actually matches its action description). + diff --git a/Project.toml b/Project.toml index d7acc960..7b468550 100644 --- a/Project.toml +++ b/Project.toml @@ -1,24 +1,10 @@ -authors = ["Valentin Churavy and contributors"] name = "Cthulhu" uuid = "f68482b8-f384-11e8-15f7-abe071a5a75f" -version = "2.17.10" - -[compat] -CodeTracking = "0.5, 1, 2" -Compiler = "0.1" -FoldingTrees = "1" -InteractiveUtils = "1.9" -JuliaSyntax = "1" -PrecompileTools = "1" -Preferences = "1" -REPL = "1.9" -TypedSyntax = "1.3.0" -UUIDs = "1.9" -Unicode = "1.9" -WidthLimitedIO = "1" -julia = "1.12" +version = "3.0.0" +authors = ["Valentin Churavy and contributors"] [deps] +Accessors = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697" CodeTracking = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2" FoldingTrees = "1eca21be-9b9b-4ed8-839a-6d8ae26b1781" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" @@ -37,9 +23,27 @@ Compiler = "807dbc54-b67e-4c79-8afb-eafe4df6f2e1" [extensions] CthulhuCompilerExt = "Compiler" +[compat] +Accessors = "0.1.42" +CodeTracking = "0.5, 1, 2" +Compiler = "0.1" +FoldingTrees = "1" +InteractiveUtils = "1.9" +JuliaSyntax = "1" +PrecompileTools = "1" +Preferences = "1" +REPL = "1.9" +TypedSyntax = "1.3.0" +UUIDs = "1.9" +Unicode = "1.9" +WidthLimitedIO = "1" +julia = "1.12" + [extras] CthulhuCompilerExt = "a0401a94-d28a-5d0d-bd4d-a83640b62d95" DeepDiffs = "ab62b9b5-e342-54a8-a765-a90f495de1a6" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" PerformanceTestTools = "dc46b164-d16f-48ec-a853-60448fc869fe" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" @@ -47,4 +51,4 @@ StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Test", "DeepDiffs", "PerformanceTestTools", "Random", "Revise", "StaticArrays"] +test = ["Test", "Compiler", "DeepDiffs", "LinearAlgebra", "Logging", "PerformanceTestTools", "Random", "Revise", "StaticArrays"] diff --git a/README.md b/README.md index 4e1da9a6..04ee0b19 100644 --- a/README.md +++ b/README.md @@ -49,13 +49,18 @@ these interactive features; you can also see Cthulhu v2.8 in action in ## Usage: descend ```julia -function foo() +function foo(x) T = rand() > 0.5 ? Int64 : Float64 - sum(rand(T, 100)) + x + sum(rand(T, 100)) end -descend(foo, Tuple{}) # option 1: specify by function name and argument types -@descend foo() # option 2: apply `@descend` to a working execution of the function +# option 1: use the function form to specify the signature +descend(foo, (Int,)) +descend(foo, Tuple{Type{Int}}) +descend(Tuple{typeof(foo), Type{Int}}) +# option 2: use `@descend` with valid syntax to automatically extract the signature +@descend foo(1) +@descend foo(::Int) # 1.13+ only ``` If you do this, you'll see quite a bit of text output. Let's break it down and @@ -329,3 +334,8 @@ true julia> Cthulhu.save_config!(Cthulhu.CONFIG) # Will be automatically read next time you `using Cthulhu` ``` + +## Development + +If you intend to Revise changes made to Cthulhu, it is strongly advised to disable the Compiler extension by uncommenting it from the `Project.toml` while iterating. +Otherwise, the Compiler extension will be unconditionally loaded, and Revise seems to fail to revise the changes into `CthulhuCompilerExt`. diff --git a/TypedSyntax/Project.toml b/TypedSyntax/Project.toml index ee132289..5da1ea8a 100644 --- a/TypedSyntax/Project.toml +++ b/TypedSyntax/Project.toml @@ -1,7 +1,7 @@ name = "TypedSyntax" uuid = "d265eb64-f81a-44ad-a842-4247ee1503de" authors = ["Tim Holy and contributors"] -version = "1.5.2" +version = "1.5.3" [deps] CodeTracking = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2" diff --git a/TypedSyntax/src/node.jl b/TypedSyntax/src/node.jl index ebcc9971..f60283b5 100644 --- a/TypedSyntax/src/node.jl +++ b/TypedSyntax/src/node.jl @@ -495,7 +495,7 @@ function map_ssas_to_source(src::CodeInfo, mi::MethodInstance, rootnode::SyntaxN used = BitSet() for (i, stmt) in enumerate(src.code) Core.Compiler.scan_ssa_use!(push!, used, stmt) - if isa(stmt, Core.ReturnNode) + if isa(stmt, Core.ReturnNode) && isdefined(stmt, :val) val = stmt.val if isa(val, SSAValue) push!(used, val.id) @@ -573,7 +573,7 @@ function map_ssas_to_source(src::CodeInfo, mi::MethodInstance, rootnode::SyntaxN empty!(argmapping) if is_slot(stmt) || isa(stmt, SSAValue) || isa(stmt, GlobalRef) append_targets_for_arg!(mapped, i, stmt) - elseif isa(stmt, Core.ReturnNode) + elseif isa(stmt, Core.ReturnNode) && isdefined(stmt, :val) append_targets_for_line!(mapped, i, append_targets_for_arg!(argmapping, i, stmt.val)) elseif isa(stmt, Expr) targets = get_targets(stmt) diff --git a/ext/CthulhuCompilerExt.jl b/ext/CthulhuCompilerExt.jl index 2247bd8b..6b2659ee 100644 --- a/ext/CthulhuCompilerExt.jl +++ b/ext/CthulhuCompilerExt.jl @@ -6,7 +6,7 @@ using Cthulhu: Cthulhu function __init__() Cthulhu.CTHULHU_MODULE[] = @__MODULE__ - read_config!(CONFIG) + read_config!() end include("../src/CthulhuBase.jl") diff --git a/src/Cthulhu.jl b/src/Cthulhu.jl index 542bb598..8391e51e 100644 --- a/src/Cthulhu.jl +++ b/src/Cthulhu.jl @@ -1,14 +1,33 @@ module Cthulhu export @descend, @descend_code_typed, @descend_code_warntype, - descend, descend_code_typed, descend_code_warntype, ascend + descend, descend_code_typed, descend_code_warntype, ascend, + AbstractProvider const CC = Base.Compiler const IRShow = Base.IRShow const CTHULHU_MODULE = Ref{Module}(@__MODULE__) -__init__() = read_config!(CONFIG) +function is_compiler_loaded() + pkgid = Base.PkgId(Base.UUID("807dbc54-b67e-4c79-8afb-eafe4df6f2e1"), "Compiler") + return haskey(Base.loaded_modules, pkgid) +end +is_compiler_extension_loaded() = CTHULHU_MODULE[] !== @__MODULE__ + +function resolve_module(Compiler::Module) + Compiler === Base.Compiler && return @__MODULE__ + Compiler === CTHULHU_MODULE[].Compiler && return CTHULHU_MODULE[] + return resolve_module() +end +resolve_module() = CTHULHU_MODULE[] +function resolve_module(@nospecialize(::T)) where {T} + mod = parentmodule(T) + nameof(mod) === :Compiler && return resolve_module(mod) + return resolve_module() +end + +__init__() = read_config!() include("CthulhuBase.jl") include("backedges.jl") @@ -26,7 +45,16 @@ See [`Cthulhu.CONFIG`](@ref) for options and their defaults. julia> @descend sin(1) [...] -julia> @descend iswarn=false foo() +julia> @descend view=:typed iswarn=true optimize=false foo() # equivalent to `@descend_warntype` +[...] + +julia> @descend view=:typed iswarn=false foo() # equivalent to `@descend_code_typed` +[...] + +julia> @descend interp=SomeInterpreter() foo() # use a custom `Compiler.AbstractInterpreter` +[...] + +julia> @descend provider=SomeProvider() foo() # use a custom `AbstractProvider`, see the docs for more details [...] ``` """ @@ -41,6 +69,10 @@ Evaluates the arguments to the function or macro call, determines their types, and calls [`descend_code_typed`](@ref) on the resulting expression. See [`Cthulhu.CONFIG`](@ref) for options and their defaults. +This macro is equivalent to `@descend` with the following options set (unless provided): +- `view = :typed` +- `iswarn = false` + # Examples ```julia julia> @descend_code_typed sin(1) @@ -61,6 +93,11 @@ Evaluates the arguments to the function or macro call, determines their types, and calls [`descend_code_warntype`](@ref) on the resulting expression. See [`Cthulhu.CONFIG`](@ref) for options and their defaults. +This macro is equivalent to `@descend` with the following options set (unless provided): +- `view = :typed` +- `iswarn = true` +- `optimize = false` + # Examples ```julia julia> function foo() @@ -83,12 +120,12 @@ end """ descend(f, argtypes=Tuple{...}; kwargs...) descend(tt::Type{<:Tuple}; kwargs...) - descend(Cthulhu.BOOKMARKS[i]) + descend(Cthulhu.BOOKMARKS[i]; kwargs...) descend(mi::MethodInstance; kwargs...) Given a function and a tuple-type, interactively explore the source code of functions -annotated with inferred types by descending into `invoke` statements. Type enter to select an -`invoke` to descend into, select `↩` to ascend, and press `q` or `control-c` to quit. +annotated with inferred types by descending into `invoke` statements. Type enter to select a callsite +to descend into, select `↩` or press backspace to ascend, and press `q` or `ctrl-c` to quit. See [`Cthulhu.CONFIG`](@ref) for `kwargs` and their defaults. # Usage: @@ -109,8 +146,20 @@ julia> descend() do [...] ``` """ -function descend(@nospecialize(args...); @nospecialize(kwargs...)) - CTHULHU_MODULE[].descend_impl(args...; kwargs...) +function descend(@nospecialize(args...); interp=nothing, + provider=nothing, + @nospecialize(kwargs...)) + if provider !== nothing + mod = resolve_module(provider) + mod.descend_with_error_handling(args...; provider, kwargs...) + elseif interp !== nothing + mod = resolve_module(interp) + mod.descend_with_error_handling(args...; interp, kwargs...) + else + mod = resolve_module() + mod.descend_with_error_handling(args...; kwargs...) + end + return nothing end """ @@ -142,9 +191,8 @@ julia> descend_code_typed() do [...] ``` """ -function descend_code_typed(@nospecialize(args...); kwargs...) - CTHULHU_MODULE[].descend_code_typed_impl(args...; kwargs...) -end +descend_code_typed(@nospecialize(args...); view = :typed, iswarn = false, kwargs...) = + descend(args...; view, iswarn, kwargs...) """ descend_code_warntype(f, argtypes=Tuple{...}; kwargs...) @@ -175,9 +223,8 @@ julia> descend_code_warntype() do [...] ``` """ -function descend_code_warntype(@nospecialize(args...); kwargs...) - CTHULHU_MODULE[].descend_code_warntype_impl(args...; kwargs...) -end +descend_code_warntype(@nospecialize(args...); view = :typed, iswarn = true, optimize = false, kwargs...) = + descend(args...; view, iswarn, optimize, kwargs...) """ ascend(mi::MethodInstance; kwargs...) @@ -197,15 +244,19 @@ using PrecompileTools @setup_workload begin try @compile_workload begin - terminal = Testing.FakeTerminal() + terminal = Testing.VirtualTerminal() + harness = Testing.@run terminal @descend terminal=terminal gcd(1, 2) task = @async @descend terminal=terminal.tty gcd(1, 2) - write(terminal, 'q') - wait(task) - finalize(terminal) + @assert Testing.end_terminal_session(harness) end catch err @error "Errorred while running the precompile workload, the package may or may not work but latency will be long" exeption=(err,catch_backtrace()) end end +get_specialization(@nospecialize(f), @nospecialize(tt=default_tt(f))) = + get_specialization(Base.signature_type(f, tt)) +get_specialization(@nospecialize tt::Type{<:Tuple}) = + specialize_method(Base._which(tt)) + end # module Cthulhu diff --git a/src/CthulhuBase.jl b/src/CthulhuBase.jl index 8f05c952..6f72c1c7 100644 --- a/src/CthulhuBase.jl +++ b/src/CthulhuBase.jl @@ -1,5 +1,6 @@ Base.Experimental.@compiler_options compile=min optimize=1 +using Accessors using CodeTracking: CodeTracking, definition, whereis, maybe_fix_path using InteractiveUtils using UUIDs @@ -17,7 +18,7 @@ using .CC: AbstractInterpreter, CallMeta, ApplyCallInfo, CallInfo as CCCallInfo, NativeInterpreter, NoCallInfo, OptimizationParams, OptimizationState, UnionSplitApplyCallInfo, UnionSplitInfo, WorldRange, WorldView, get_inference_world, argextype, argtypes_to_type, compileable_specialization, ignorelimited, singleton_type, - specialize_method, sptypes_from_meth_instance, widenconst + specialize_method, sptypes_from_meth_instance, widenconst, method_table, findsup using Base: @constprop, default_tt, isvarargtype, unwrapva, unwrap_unionall, rewrap_unionall const mapany = Base.mapany @@ -28,129 +29,24 @@ using Base: get_world_counter get_mi(ci::CodeInstance) = CC.get_ci_mi(ci) get_mi(mi::MethodInstance) = mi -Base.@kwdef mutable struct CthulhuConfig - enable_highlighter::Bool = false - highlighter::Cmd = `pygmentize -l` - asm_syntax::Symbol = :att - pretty_ast::Bool = false - interruptexc::Bool = true - debuginfo::Symbol = :compact - optimize::Bool = true - iswarn::Bool = false - hide_type_stable::Bool = false - remarks::Bool = false - with_effects::Bool = false - exception_type::Bool = false - inline_cost::Bool = false - type_annotations::Bool = true - annotate_source::Bool = true # overrides optimize, although the current setting is preserved - inlay_types_vscode::Bool = true - diagnostics_vscode::Bool = true - jump_always::Bool = false -end - -""" - Cthulhu.CONFIG - -# Options -- `enable_highlighter::Bool`: Use command line `highlighter` to syntax highlight - Julia, LLVM and native code. -- `highlighter::Cmd`: A command line program that receives "julia" as an argument and julia - code as stdin. Defaults to `$(CthulhuConfig().highlighter)`. -- `asm_syntax::Symbol`: Set the syntax of assembly code being used. - Defaults to `$(CthulhuConfig().asm_syntax)`. -- `pretty_ast::Bool`: Use a pretty printer for the ast dump. Defaults to `false`. -- `interruptexc::Bool`: Use -key to quit or ascend. Defaults to `false`. -- `debuginfo::Symbol`: Initial state of "debuginfo" toggle. Defaults to `:compact`. - Options:. `:none`, `:compact`, `:source` -- `optimize::Bool`: Initial state of "optimize" toggle. Defaults to `true`. -- `hide_type_stable::Bool`: Initial state of "hide_type_stable" toggle. Defaults to `false`. -- `iswarn::Bool`: Initial state of "warn" toggle. Defaults to `false`. -- `remarks::Bool` Initial state of "remarks" toggle. Defaults to `false`. -- `with_effects::Bool` Intial state of "effects" toggle. Defaults to `false`. -- `exception_type::Bool` `Intial state of "exception type" toggle. Defaults to `false`. -- `inline_cost::Bool` Initial state of "inlining costs" toggle. Defaults to `false`. -- `type_annotations::Bool` Initial state of "type annnotations" toggle. Defaults to `true`. -- `annotate_source::Bool` Initial state of "Source". Defaults to `true`. -- `inlay_types_vscode::Bool` Initial state of "vscode: inlay types" toggle. Defaults to `true` -- `diagnostics_vscode::Bool` Initial state of "Vscode: diagnostics" toggle. Defaults to `true` -- `jump_always::Bool` Initial state of "jump to source always" toggle. Defaults to `false`. - -Other keyword arguments are passed to [`Cthulhu.CthulhuMenu`](@ref) and/or -[`REPL.TerminalMenus`](https://docs.julialang.org/en/v1/stdlib/REPL/#Customization-/-Configuration). -""" -const CONFIG = CthulhuConfig() - using Preferences +include("config.jl") include("preferences.jl") -module DInfo - @enum DebugInfo none compact source -end -using .DInfo: DebugInfo -const AnyDebugInfo = Union{DebugInfo,Symbol} - -include("interpreter.jl") -include("callsite.jl") include("interface.jl") +include("callsite.jl") +include("compiler.jl") +include("state.jl") +include("interpreter.jl") +include("provider.jl") include("reflection.jl") include("ui.jl") include("codeview.jl") +include("bookmark.jl") +include("descend.jl") +include("ascend.jl") -""" - @interp - -For debugging. Returns a CthulhuInterpreter from the appropriate entrypoint. -""" -macro interp(ex0...) - InteractiveUtils.gen_call_with_extracted_types_and_kwargs(__module__, :mkinterp, ex0) -end - -descend_code_typed_impl(@nospecialize(args...); kwargs...) = - _descend_with_error_handling(args...; annotate_source=false, iswarn=false, kwargs...) - -descend_code_warntype_impl(@nospecialize(args...); kwargs...) = - _descend_with_error_handling(args...; annotate_source=false, iswarn=true, optimize=false, kwargs...) - -function _descend_with_error_handling(@nospecialize(f), @nospecialize(argtypes = default_tt(f)); kwargs...) - ft = Core.Typeof(f) - if isa(argtypes, Type) - u = unwrap_unionall(argtypes) - tt = rewrap_unionall(Tuple{ft, u.parameters...}, argtypes) - else - tt = Tuple{ft, argtypes...} - end - __descend_with_error_handling(tt; kwargs...) -end -_descend_with_error_handling(mi::MethodInstance; kwargs...) = - __descend_with_error_handling(mi; kwargs...) -_descend_with_error_handling(@nospecialize(tt::Type{<:Tuple}); kwargs...) = - __descend_with_error_handling(tt; kwargs...) -_descend_with_error_handling(interp::AbstractInterpreter, mi::MethodInstance; kwargs...) = - __descend_with_error_handling(interp, mi; kwargs...) -function __descend_with_error_handling(args...; terminal=default_terminal(), kwargs...) - @nospecialize - try - _descend(terminal, args...; kwargs...) - catch x - TypedSyntax.clear_all_vscode() - if x isa InterruptException - return nothing - else - rethrow(x) - end - end - return nothing -end - -function default_terminal() - term_env = get(ENV, "TERM", @static Sys.iswindows() ? "" : "dumb") - term = REPL.Terminals.TTYTerminal(term_env, stdin, stdout, stderr) - return term -end - -descend_impl(@nospecialize(args...); kwargs...) = - _descend_with_error_handling(args...; iswarn=true, kwargs...) +resolve_module(::AbstractProvider) = @__MODULE__ using .CC: cached_return_type @@ -163,585 +59,3 @@ get_effects(source::InferredSource) = source.effects get_effects(result::CC.ConstPropResult) = get_effects(result.result) get_effects(result::CC.ConcreteResult) = result.effects get_effects(result::CC.SemiConcreteResult) = result.effects - -struct LookupResult - src::Union{CodeInfo,IRCode,Nothing} - rt - exct - infos::Vector{CCCallInfo} - slottypes::Vector{Any} - effects::Effects - codeinf::Union{Nothing,CodeInfo} - function LookupResult(src::Union{CodeInfo,IRCode,Nothing}, @nospecialize(rt), @nospecialize(exct), - infos::Vector{CCCallInfo}, slottypes::Vector{Any}, - effects::Effects, codeinf::Union{Nothing,CodeInfo}) - return new(src, rt, exct, infos, slottypes, effects, codeinf) - end -end - -# `@constprop :aggressive` here in order to make sure the constant propagation of `allow_no_src` -@constprop :aggressive function lookup(interp::CthulhuInterpreter, ci::CodeInstance, optimize::Bool; allow_no_src::Bool=false) - if optimize - return lookup_optimized(interp, ci, allow_no_src) - else - return lookup_unoptimized(interp, ci) - end -end - -function lookup_optimized(interp::CthulhuInterpreter, ci::CodeInstance, allow_no_src::Bool=false) - rt = cached_return_type(ci) - exct = cached_exception_type(ci) - opt = ci.inferred - if opt !== nothing - opt = opt::OptimizedSource - src = CC.copy(opt.ir) - codeinf = opt.src - infos = src.stmts.info - slottypes = src.argtypes - elseif allow_no_src - # This doesn't showed up as covered, but it is (see the CI test with `coverage=false`). - # But with coverage on, the empty function body isn't empty due to :code_coverage_effect expressions. - codeinf = src = nothing - infos = CCCallInfo[] - slottypes = Any[Base.unwrap_unionall(ci.def.specTypes).parameters...] - else - Core.eval(Main, quote - interp = $interp - ci = $ci - end) - error("couldn't find the source; inspect `Main.interp` and `Main.mi`") - end - effects = get_effects(ci) - return LookupResult(src, rt, exct, infos, slottypes, effects, codeinf) -end - -function lookup_unoptimized(interp::CthulhuInterpreter, ci::CodeInstance) - unopt = interp.unopt[ci] - codeinf = src = copy(unopt.src) - (; rt, exct) = unopt - infos = unopt.stmt_info - effects = unopt.effects - slottypes = src.slottypes - if isnothing(slottypes) - slottypes = Any[ Any for i = 1:length(src.slotflags) ] - end - return LookupResult(src, rt, exct, infos, slottypes, effects, codeinf) -end - -function lookup_constproped(interp::CthulhuInterpreter, override::InferenceResult, optimize::Bool) - if optimize - return lookup_constproped_optimized(interp, override) - else - return lookup_constproped_unoptimized(interp, override) - end -end - -function lookup_constproped_optimized(interp::CthulhuInterpreter, override::InferenceResult) - opt = override.src - isa(opt, OptimizedSource) || error("couldn't find the source") - # `(override::InferenceResult).src` might has been transformed to OptimizedSource already, - # e.g. when we switch from constant-prop' unoptimized source - src = CC.copy(opt.ir) - rt = override.result - exct = override.exc_result - infos = src.stmts.info - slottypes = src.argtypes - codeinf = opt.src - effects = opt.effects - return LookupResult(src, rt, exct, infos, slottypes, effects, codeinf) -end - -function lookup_constproped_unoptimized(interp::CthulhuInterpreter, override::InferenceResult) - unopt = interp.unopt[override] - codeinf = src = copy(unopt.src) - (; rt, exct) = unopt - infos = unopt.stmt_info - effects = get_effects(unopt) - slottypes = src.slottypes - if isnothing(slottypes) - slottypes = Any[ Any for i = 1:length(src.slotflags) ] - end - return LookupResult(src, rt, exct, infos, slottypes, effects, codeinf) -end - -function lookup_semiconcrete(interp::CthulhuInterpreter, ci::CodeInstance, override::SemiConcreteCallInfo, optimize::Bool) - src = CC.copy(override.ir) - rt = get_rt(override) - exct = Any # TODO - infos = src.stmts.info - slottypes = src.argtypes - effects = get_effects(override) - (; exct, codeinf) = lookup(interp, ci, optimize) - return LookupResult(src, rt, exct, infos, slottypes, effects, codeinf) -end - -function get_override(@nospecialize(info)) - isa(info, ConstPropCallInfo) && return info.result - isa(info, SemiConcreteCallInfo) && return info - isa(info, OCCallInfo) && return get_override(info.ci) - return nothing -end - -## -# _descend is the main driver function. -# src/reflection.jl has the tools to discover methods -# src/ui.jl provides the user facing interface to which _descend responds -## -function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::AbstractCursor; - override::Union{Nothing,InferenceResult,SemiConcreteCallInfo} = nothing, - debuginfo::Union{Symbol,DebugInfo} = CONFIG.debuginfo, # default is compact debuginfo - optimize::Bool = CONFIG.optimize, # default is true - interruptexc::Bool = CONFIG.interruptexc, - iswarn::Bool = CONFIG.iswarn, # default is false - hide_type_stable::Union{Nothing,Bool} = CONFIG.hide_type_stable, - verbose::Union{Nothing,Bool} = nothing, - remarks::Bool = CONFIG.remarks&!CONFIG.optimize, # default is false - with_effects::Bool = CONFIG.with_effects, # default is false - exception_type::Bool = CONFIG.exception_type, # default is false - inline_cost::Bool = CONFIG.inline_cost&CONFIG.optimize, # default is false - type_annotations::Bool = CONFIG.type_annotations, # default is true - annotate_source::Bool = CONFIG.annotate_source, # default is true - inlay_types_vscode::Bool = CONFIG.inlay_types_vscode, # default is true - diagnostics_vscode::Bool = CONFIG.diagnostics_vscode, # default is true - jump_always::Bool = CONFIG.jump_always, # default is false - kwargs..., # additional kwargs passed to `menu_options` - ) - - if isnothing(hide_type_stable) - hide_type_stable = something(verbose, false) - end - isnothing(verbose) || Base.depwarn("The `verbose` keyword argument to `Cthulhu.descend` is deprecated. Use `hide_type_stable` instead.") - if isa(debuginfo, Symbol) - debuginfo = getfield(DInfo, debuginfo)::DebugInfo - end - - menu_options = (; cursor = '•', scroll_wrap = true, kwargs...) - display_CI = true - view_cmd = cthulhu_typed - iostream = term.out_stream::IO - function additional_descend(new_ci::CodeInstance) - new_interp = CthulhuInterpreter(interp) - _descend(term, new_interp, new_ci; - debuginfo, optimize, interruptexc, iswarn, hide_type_stable, remarks, - with_effects, exception_type, - inline_cost, type_annotations, annotate_source, - inlay_types_vscode, diagnostics_vscode) - end - custom_toggles = (@__MODULE__).custom_toggles(interp) - if !(custom_toggles isa Vector{CustomToggle}) - error(lazy""" - invalid `$AbstractInterpreter` API: - `$(Cthulhu.custom_toggles)(interp::$(typeof(interp))` is expected to return `Vector{CustomToggle}` object. - """) - end - while true - if isa(override, InferenceResult) - (; src, rt, exct, infos, slottypes, codeinf, effects) = lookup_constproped(interp, curs, override, optimize & !annotate_source) - elseif isa(override, SemiConcreteCallInfo) - (; src, rt, exct, infos, slottypes, codeinf, effects) = lookup_semiconcrete(interp, curs, override, optimize & !annotate_source) - else - if optimize && !annotate_source - codeinst = get_ci(curs) - if codeinst.inferred === nothing - if isdefined(codeinst, :rettype_const) - # TODO use `codeinfo_for_const` - # This was inferred to a pure constant - we have no code to show, - # but make something up that looks plausible. - callsites = Callsite[] - if display_CI - exct = cached_exception_type(codeinst) - callsite = Callsite(-1, EdgeCallInfo(codeinst, codeinst.rettype, get_effects(codeinst), exct), :invoke) - println(iostream) - println(iostream, "│ ─ $callsite") - println(iostream, "│ return ", Const(codeinst.rettype_const)) - println(iostream) - end - mi = get_mi(codeinst) - @goto show_menu - else - @info """ - Inference discarded the source for this call because of recursion: - Cthulhu nevertheless is trying to retrieve the source for further inspection. - """ - ci = get_ci(curs) - if !haskey(interp.unopt, ci) - additional_descend(ci) - break - else - (; src, rt, exct, infos, slottypes, effects, codeinf) = lookup_unoptimized(interp, ci) - optimize = false - @goto lookup_complete - end - end - end - end - (; src, rt, exct, infos, slottypes, effects, codeinf) = lookup(interp, curs, optimize & !annotate_source) - end - @label lookup_complete - ci = get_ci(curs) - mi = get_mi(ci) - infkey = override isa InferenceResult ? override : ci - pc2excts = exception_type ? get_pc_exct(interp, infkey) : nothing - callsites, sourcenodes = find_callsites(interp, src, infos, ci, slottypes, optimize & !annotate_source, annotate_source, pc2excts) - - if jump_always - if isdefined(Main, :VSCodeServer) && Main.VSCodeServer isa Module && isdefined(Main.VSCodeServer, :openfile) - Main.VSCodeServer.openfile(whereis(mi.def::Method)...; preserve_focus=true) - else - edit(whereis(mi.def::Method)...) - end - end - - if display_CI - pc2remarks = remarks ? get_pc_remarks(interp, infkey) : nothing - pc2effects = with_effects ? get_pc_effects(interp, infkey) : nothing - printstyled(IOContext(iostream, :limit=>true), mi.def, '\n'; bold=true) - if debuginfo == DInfo.compact - str = let debuginfo=debuginfo, src=src, codeinf=codeinf, rt=rt, - iswarn=iswarn, hide_type_stable=hide_type_stable, - pc2remarks=pc2remarks, pc2effects=pc2effects, inline_cost=inline_cost, type_annotations=type_annotations - ioctx = IOContext(iostream, - :color => true, - :displaysize => displaysize(iostream), # displaysize doesn't propagate otherwise - :SOURCE_SLOTNAMES => codeinf === nothing ? false : Base.sourceinfo_slotnames(codeinf), - :with_effects => with_effects, - :exception_type => exception_type) - stringify(ioctx) do lambda_io - cthulhu_typed(lambda_io, debuginfo, annotate_source ? codeinf : src, rt, exct, effects, ci; - iswarn, optimize, hide_type_stable, - pc2remarks, pc2effects, pc2excts, - inline_cost, type_annotations, annotate_source, inlay_types_vscode, diagnostics_vscode, - jump_always, interp) - end - end - # eliminate trailing indentation (see first item in bullet list in PR #189) - rmatch = findfirst(r"\u001B\[90m\u001B\[(\d+)G( *)\u001B\[1G\u001B\[39m\u001B\[90m( *)\u001B\[39m$", str) - if rmatch !== nothing - str = str[begin:prevind(str, first(rmatch))] - end - print(iostream, str) - else - lambda_io = IOContext(iostream, - :SOURCE_SLOTNAMES => codeinf === nothing ? false : Base.sourceinfo_slotnames(codeinf), - :with_effects => with_effects, - :exception_type => exception_type) - cthulhu_typed(lambda_io, debuginfo, src, rt, exct, effects, ci; - iswarn, optimize, hide_type_stable, - pc2remarks, pc2effects, pc2excts, - inline_cost, type_annotations, annotate_source, inlay_types_vscode, diagnostics_vscode, - jump_always, interp) - end - view_cmd = cthulhu_typed - else - display_CI = true - end - - @label show_menu - - shown_callsites = annotate_source ? sourcenodes : callsites - menu = CthulhuMenu(shown_callsites, with_effects, exception_type, - optimize & !annotate_source, - iswarn&get(iostream, :color, false)::Bool, - hide_type_stable, custom_toggles; menu_options...) - usg = usage(view_cmd, annotate_source, optimize, iswarn, hide_type_stable, - debuginfo, remarks, with_effects, exception_type, inline_cost, - type_annotations, CONFIG.enable_highlighter, inlay_types_vscode, - diagnostics_vscode, jump_always, custom_toggles) - cid = request(term, usg, menu) - toggle = menu.toggle - - if toggle === nothing - if cid == length(callsites) + 1 - break - end - if cid == -1 - interruptexc ? throw(InterruptException()) : break - end - callsite = callsites[cid] - sourcenode = !isempty(sourcenodes) ? sourcenodes[cid] : nothing - - info = callsite.info - if info isa MultiCallInfo - show_sub_callsites = sub_callsites = let callsite=callsite - map(ci->Callsite(callsite.id, ci, callsite.head), info.callinfos) - end - if isempty(sub_callsites) - Core.eval(Main, quote - interp = $interp - mi = $mi - info = $info - end) - @error "Expected multiple callsites, but found none. Please fill an issue with a reproducing example." - continue - end - if sourcenode !== nothing - show_sub_callsites = let callsite=callsite - map(info.callinfos) do ci - p = Base.unwrap_unionall(get_ci(ci).def.specTypes).parameters - if isa(sourcenode, TypedSyntax.MaybeTypedSyntaxNode) && length(p) == length(children(sourcenode)) + 1 - newnode = copy(sourcenode) - for (i, child) in enumerate(children(newnode)) - child.typ = p[i+1] - end - newnode - else - Callsite(callsite.id, ci, callsite.head) - end - end - end - end - menu = CthulhuMenu(show_sub_callsites, with_effects, exception_type, - optimize & !annotate_source, false, false, custom_toggles; - sub_menu=true, menu_options...) - cid = request(term, "", menu) - if cid == length(sub_callsites) + 1 - continue - end - if cid == -1 - interruptexc ? throw(InterruptException()) : break - end - - callsite = sub_callsites[cid] - info = callsite.info - end - - # forcibly enter and inspect the frame, although the native interpreter gave up - if info isa TaskCallInfo - @info """ - Inference didn't analyze this call because it is a dynamic call: - Cthulhu nevertheless is trying to descend into it for further inspection. - """ - additional_descend(get_ci(info)::CodeInstance) - continue - elseif info isa RTCallInfo - @info """ - This is a runtime call. You cannot descend into it. - """ - @goto show_menu - end - - # recurse - next_cursor = navigate(curs, callsite)::Union{AbstractCursor,Nothing} - if next_cursor === nothing - continue - end - - _descend(term, interp, next_cursor; - override = get_override(info), debuginfo, - optimize, interruptexc, - iswarn, hide_type_stable, - remarks, with_effects, exception_type, inline_cost, - type_annotations, annotate_source, - inlay_types_vscode, diagnostics_vscode, - jump_always) - - elseif toggle === :warn - iswarn ⊻= true - elseif toggle === :with_effects - with_effects ⊻= true - elseif toggle === :exception_type - exception_type ⊻= true - elseif toggle === :hide_type_stable - hide_type_stable ⊻= true - elseif toggle === :inlay_types_vscode - inlay_types_vscode ⊻= true - TypedSyntax.clear_inlay_hints_vscode() - elseif toggle === :diagnostics_vscode - diagnostics_vscode ⊻= true - TypedSyntax.clear_diagnostics_vscode() - elseif toggle === :optimize - optimize ⊻= true - if remarks && optimize - @warn "Disable optimization to see the inference remarks." - end - elseif toggle === :debuginfo - debuginfo = DebugInfo((Int(debuginfo) + 1) % 3) - elseif toggle === :remarks - remarks ⊻= true - if remarks && optimize - @warn "Disable optimization to see the inference remarks." - end - elseif toggle === :inline_cost - inline_cost ⊻= true - if inline_cost && !optimize - @warn "Enable optimization to see the inlining costs." - end - elseif toggle === :type_annotations - type_annotations ⊻= true - elseif toggle === :highlighter - CONFIG.enable_highlighter ⊻= true - if CONFIG.enable_highlighter - @info "Using syntax highlighter $(CONFIG.highlighter)." - else - @info "Turned off syntax highlighter for Julia, LLVM and native code." - end - display_CI = false - elseif toggle === :dump_params - @info "Dumping inference cache." - Core.show(mapany(((i, x),) -> (i, x.result, x.linfo), enumerate(CC.get_inference_cache(interp)))) - Core.println() - display_CI = false - elseif toggle === :bookmark - push!(BOOKMARKS, Bookmark(mi, interp)) - @info "The method is pushed at the end of `Cthulhu.BOOKMARKS`." - display_CI = false - elseif toggle === :revise - # Call Revise.revise() without introducing a dependency on Revise - id = Base.PkgId(UUID("295af30f-e4ad-537b-8983-00126c2a3abe"), "Revise") - mod = get(Base.loaded_modules, id, nothing) - if mod !== nothing - revise = getfield(mod, :revise)::Function - revise() - ci = do_typeinf!(interp, mi) - curs = update_cursor(curs, ci) - else - @warn "Failed to load Revise." - end - elseif toggle === :edit - edit(whereis(mi.def::Method)...) - display_CI = false - elseif toggle === :jump_always - jump_always ⊻= true - elseif toggle === :typed - view_cmd = cthulhu_typed - annotate_source = false - display_CI = true - elseif toggle === :source - view_cmd = cthulhu_typed - annotate_source = true - display_CI = true - elseif toggle === :ast || toggle === :llvm || toggle === :native - view_cmd = CODEVIEWS[toggle] - world = get_inference_world(interp) - println(iostream) - src = CC.typeinf_code(interp, mi, true) - view_cmd(iostream, mi, src, optimize, debuginfo, world, CONFIG) - display_CI = false - else - local i = findfirst(ct->ct.toggle === toggle, custom_toggles) - @assert i !== nothing - ct = custom_toggles[i] - onoff = ct.onoff ⊻= true - if onoff - curs = ct.callback_on(curs) - else - curs = ct.callback_off(curs) - end - if !(curs isa AbstractCursor) - local f = onoff ? "callback_on" : "callback_off" - error(lazy""" - invalid `$AbstractInterpreter` API: - `f` callback is expected to return `AbstractCursor` object. - """) - end - end - println(iostream) - end - - TypedSyntax.clear_all_vscode() -end - -function do_typeinf!(interp::AbstractInterpreter, mi::MethodInstance) - result = InferenceResult(mi) - ci = CC.engine_reserve(interp, mi) - result.ci = ci - # we may want to handle the case when `InferenceState(...)` returns `nothing`, - # which indicates code generation of a `@generated` has been failed, - # and show it in the UI in some way? - frame = InferenceState(result, #=cache_mode=#:global, interp)::InferenceState - CC.typeinf(interp, frame) - return ci -end - -get_specialization(@nospecialize(f), @nospecialize(tt=default_tt(f))) = - get_specialization(Base.signature_type(f, tt)) -get_specialization(@nospecialize tt::Type{<:Tuple}) = - specialize_method(Base._which(tt)) - -function mkinterp(interp::AbstractInterpreter, @nospecialize(args...)) - interp′ = CthulhuInterpreter(interp) - mi = get_specialization(args...) - ci = do_typeinf!(interp′, mi) - return interp′, ci -end -mkinterp(@nospecialize(args...); interp::AbstractInterpreter=NativeInterpreter()) = mkinterp(interp, args...) - -function _descend(@nospecialize(args...); - interp::AbstractInterpreter=NativeInterpreter(), kwargs...) - (interp′, ci) = mkinterp(interp, args...) - _descend(interp′, ci; kwargs...) -end -_descend(interp::AbstractInterpreter, ci::CodeInstance; terminal=default_terminal(), kwargs...) = - _descend(terminal, interp, ci; kwargs...) -_descend(term::AbstractTerminal, interp::AbstractInterpreter, ci::CodeInstance; kwargs...) = - _descend(term, interp, AbstractCursor(interp, ci); kwargs...) -function _descend(term::AbstractTerminal, mi::MethodInstance; - interp::AbstractInterpreter=NativeInterpreter(), kwargs...) - interp′ = CthulhuInterpreter(interp) - ci = do_typeinf!(interp′, mi) - _descend(term, interp′, ci; kwargs...) -end -function _descend(term::AbstractTerminal, @nospecialize(args...); - interp::AbstractInterpreter=NativeInterpreter(), kwargs...) - (interp′, ci) = mkinterp(interp, args...) - _descend(term, interp′, ci; kwargs...) -end -descend_code_typed_impl(b::Bookmark; kw...) = - _descend_with_error_handling(b.interp, b.ci.def; iswarn=false, kw...) -descend_code_warntype_impl(b::Bookmark; kw...) = - _descend_with_error_handling(b.interp, b.ci.def; iswarn=true, kw...) - -function ascend_impl( - term, mi; interp::AbstractInterpreter=NativeInterpreter(), - pagesize::Int=10, dynamic::Bool=false, maxsize::Int=pagesize, kwargs... - ) - root = Cthulhu.treelist(mi) - root === nothing && return - menu = TreeMenu(root; pagesize, dynamic, maxsize) - choice = menu.current - while choice !== nothing - menu.chosen = false - choice = TerminalMenus.request(term, "Choose a call for analysis (q to quit):", menu; cursor=menu.currentidx) - browsecodetyped = true - if choice !== nothing - node = menu.current - mi = Cthulhu.instance(node.data.nd) - if !isroot(node) - # Help user find the sites calling the parent - miparent = Cthulhu.instance(node.parent.data.nd) - ulocs = find_caller_of(interp, miparent, mi; allow_unspecialized=true) - if !isempty(ulocs) - ulocs = [(k[1], maybe_fix_path(String(k[2])), k[3]) => v for (k, v) in ulocs] - strlocs = [string(" "^k[3] * '"', k[2], "\", ", k[1], ": lines ", v) for (k, v) in ulocs] - explain_inlining = length(ulocs) > 1 ? "(including inlined callers represented by indentation) " : "" - push!(strlocs, "Browse typed code") - linemenu = TerminalMenus.RadioMenu(strlocs; charset=:ascii) - browsecodetyped = false - choice2 = 1 - while choice2 != -1 - promptstr = sprint(miparent, explain_inlining; context=:color=>get(term, :color, false)) do iobuf, mip, exi - printstyled(iobuf, "\nOpen an editor at a possible caller of\n "; color=:light_cyan) - print(iobuf, miparent) - printstyled(iobuf, "\n$(explain_inlining)or browse typed code:"; color=:light_cyan) - end - choice2 = TerminalMenus.request(term, promptstr, linemenu; cursor=choice2) - if 0 < choice2 < length(strlocs) - loc, lines = ulocs[choice2] - edit(loc[2], first(lines)) - elseif choice2 == length(strlocs) - browsecodetyped = true - break - end - end - end - end - if !isa(mi, MethodInstance) - error("You can only descend into known calls. If you tried to descend into a runtime-dispatched signature, try its caller instead.") - end - # The main application of `ascend` is finding cases of non-inferrability, so the - # warn highlighting is useful. - browsecodetyped && _descend(term, mi; interp, annotate_source=true, iswarn=true, optimize=false, interruptexc=false, pagesize, kwargs...) - end - end -end -ascend_impl(mi; kwargs...) = ascend_impl(default_terminal(), mi; kwargs...) diff --git a/src/ascend.jl b/src/ascend.jl new file mode 100644 index 00000000..35c8bf56 --- /dev/null +++ b/src/ascend.jl @@ -0,0 +1,56 @@ +function ascend_impl( + term, mi; + interp::AbstractInterpreter=NativeInterpreter(), + provider::AbstractProvider=AbstractProvider(interp), + pagesize::Int=10, dynamic::Bool=false, maxsize::Int=pagesize, + menu_options=(; pagesize), kwargs...) + root = Cthulhu.treelist(mi) + root === nothing && return + menu = TreeMenu(root; pagesize, dynamic, maxsize) + choice = menu.current + while choice !== nothing + menu.chosen = false + choice = TerminalMenus.request(term, "Choose a call for analysis (q to quit):", menu; cursor=menu.currentidx) + browsecodetyped = true + if choice !== nothing + node = menu.current + mi = Cthulhu.instance(node.data.nd) + if !isroot(node) + # Help user find the sites calling the parent + parent = Cthulhu.instance(node.parent.data.nd) + ulocs = find_caller_of(provider, parent, mi; allow_unspecialized=true) + if !isempty(ulocs) + ulocs = [(k[1], maybe_fix_path(String(k[2])), k[3]) => v for (k, v) in ulocs] + strlocs = [string(" "^k[3] * '"', k[2], "\", ", k[1], ": lines ", v) for (k, v) in ulocs] + explain_inlining = length(ulocs) > 1 ? "(including inlined callers represented by indentation) " : "" + push!(strlocs, "Browse typed code") + linemenu = TerminalMenus.RadioMenu(strlocs; charset=:ascii) + browsecodetyped = false + choice2 = 1 + while choice2 != -1 + promptstr = sprint(parent, explain_inlining; context=:color=>get(term, :color, false)) do iobuf, mip, exi + printstyled(iobuf, "\nOpen an editor at a possible caller of\n "; color=:light_cyan) + print(iobuf, parent) + printstyled(iobuf, "\n$(explain_inlining)or browse typed code:"; color=:light_cyan) + end + choice2 = TerminalMenus.request(term, promptstr, linemenu; cursor=choice2) + if 0 < choice2 < length(strlocs) + loc, lines = ulocs[choice2] + edit(loc[2], first(lines)) + elseif choice2 == length(strlocs) + browsecodetyped = true + break + end + end + end + end + if !isa(mi, MethodInstance) + error("You can only descend into known calls. If you tried to descend into a runtime-dispatched signature, try its caller instead.") + end + # The main application of `ascend` is finding cases of non-inferrability, so the + # warn highlighting is useful. + browsecodetyped && _descend(term, mi; provider, view=:source, iswarn=true, optimize=false, menu_options, kwargs...) + end + end +end +ascend_impl(mi; kwargs...) = ascend_impl(default_terminal(), mi; kwargs...) diff --git a/src/bookmark.jl b/src/bookmark.jl new file mode 100644 index 00000000..0a3ed108 --- /dev/null +++ b/src/bookmark.jl @@ -0,0 +1,86 @@ +""" + Cthulhu.Bookmark + +A `Cthulhu.Bookmark` remembers a method marked by `b` key during a descent. +It can be used with the following functions: + +* `descend(::Bookmark)`, `descend_code_typed(::Bookmark)`, + `descend_code_warntype(::Bookmark)`: continue the descent. +* `code_typed(::Bookmark)`, `code_warntype([::IO,] ::Bookmark)`: show typed IR +* `code_llvm([::IO,] ::Bookmark)`: show LLVM IR +* `code_native([::IO,] ::Bookmark)`: show native code +""" +struct Bookmark + provider::AbstractProvider + config::CthulhuConfig + ci::CodeInstance +end +Bookmark(provider::AbstractProvider, ci::CodeInstance; config::CthulhuConfig = CONFIG) = + Bookmark(provider, config, ci) + +function CthulhuState(bookmark::Bookmark; terminal=default_terminal(), kwargs...) + config = set_config(bookmark.config; kwargs...) + state = CthulhuState(bookmark.provider; terminal, config, bookmark.ci) + return state +end + +""" + Cthulhu.BOOKMARKS :: Vector{Bookmark} + +During a descent, state can be "bookmarked" by pressing `b`, which pushes a [`Cthulhu.Bookmark`](@ref) into `Cthulhu.BOOKMARKS`. This can be used to, e.g., continue descending with `descend(Cthulhu.BOOKMARKS[end])`. + +See [`Cthulhu.Bookmark`](@ref) for other uses. +""" +const BOOKMARKS = Bookmark[] + +function Base.show(io::IO, ::MIME"text/plain", bookmark::Bookmark; kwargs...) + (; provider, ci) = bookmark + state = CthulhuState(bookmark; kwargs...) + result = LookupResult(provider, ci, state.config.optimize) + world = get_inference_world(provider) + if get(io, :typeinfo, Any) === Bookmark # a hack to check if in Vector etc. + info = EdgeCallInfo(ci, result.rt, Effects()) + callsite = Callsite(-1, info, :invoke) + print(io, callsite) + print(io, " (world: ", world, ")") + return + end + println(io, Bookmark, " (world: $world):") + view_function(state)(io, provider, state, result) +end + +function Base.code_typed(bookmark::Bookmark; kwargs...) + (; provider, ci) = bookmark + state = CthulhuState(bookmark; kwargs...) + result = LookupResult(provider, ci, state.config.optimize) + src = something(result.src, result.ir)::Union{CodeInfo, IRCode} + return src => result.rt +end + +InteractiveUtils.code_warntype(bookmark::Bookmark; kwargs...) = + InteractiveUtils.code_warntype(stdout::IO, bookmark; kwargs...) +InteractiveUtils.code_llvm(bookmark::Bookmark; kwargs...) = + InteractiveUtils.code_llvm(stdout::IO, bookmark; kwargs...) +InteractiveUtils.code_native(bookmark::Bookmark; kwargs...) = + InteractiveUtils.code_native(stdout::IO, bookmark; kwargs...) + +function InteractiveUtils.code_warntype(io::IO, bookmark::Bookmark; kwargs...) + (; provider, ci) = bookmark + state = CthulhuState(bookmark; kwargs...) + result = LookupResult(provider, ci, state.config.optimize) + cthulhu_warntype(io, provider, state, result) +end + +function InteractiveUtils.code_llvm(io::IO, bookmark::Bookmark; dump_module = false, raw = false, kwargs...) + (; provider, ci) = bookmark + state = CthulhuState(bookmark; kwargs...) + result = LookupResult(provider, ci, state.config.optimize) + cthulhu_llvm(io, provider, state, result; dump_module, raw) +end + +function InteractiveUtils.code_native(io::IO, bookmark::Bookmark; dump_module = false, raw = false, kwargs...) + (; provider, ci) = bookmark + state = CthulhuState(bookmark; kwargs...) + result = LookupResult(provider, ci, state.config.optimize) + cthulhu_native(io, provider, state, result; dump_module, raw) +end diff --git a/src/callsite.jl b/src/callsite.jl index 996f2188..81d34005 100644 --- a/src/callsite.jl +++ b/src/callsite.jl @@ -217,12 +217,12 @@ function __show_limited(limiter, name, tt, @nospecialize(rt), effects, @nospecia # If effects are explicitly turned on, make sure to print them, even # if there otherwise isn't space for them, since the effects are the # most important piece of information if turned on. - with_effects = get(limiter, :with_effects, false)::Bool - exception_type = get(limiter, :exception_type, false)::Bool && exct !== nothing + show_effects = get(limiter, :effects, false)::Bool + exception_types = get(limiter, :exception_types, false)::Bool && exct !== nothing if isa(limiter, TextWidthLimiter) - with_effects && (limiter.width += textwidth(repr(effects)) + 1) - exception_type && (limiter.width += textwidth(string(exct)) + 1) + show_effects && (limiter.width += textwidth(repr(effects)) + 1) + exception_types && (limiter.width += textwidth(string(exct)) + 1) limiter.limit = max(limiter.width, limiter.limit) end @@ -263,11 +263,11 @@ function __show_limited(limiter, name, tt, @nospecialize(rt), effects, @nospecia end @label print_effects - if with_effects + if show_effects # Print effects unlimited print(limiter.io, " ", effects) end - if exception_type + if exception_types print(limiter.io, ' ', ExctWrapper(exct)) end @@ -429,24 +429,25 @@ function Base.show(io::IO, c::Callsite) iswarn = get(io, :iswarn, false)::Bool info = c.info rt = get_rt(info) - if iswarn && is_type_unstable(rt) - color = if rt isa Union && is_expected_union(rt) - Base.warn_color() + limiter = TextWidthLimiter(io, cols) + if c.id != -1 + if iswarn && is_type_unstable(rt) + color = if rt isa Union && is_expected_union(rt) + Base.warn_color() + else + Base.error_color() + end + printstyled(io, '%'; color) else - Base.error_color() + print(io, '%') end - printstyled(io, '%'; color) - else - print(io, '%') + limiter.width += 1 # for the '%' character + print(limiter, c.id, " = ") end - limiter = TextWidthLimiter(io, cols) - limiter.width += 1 # for the '%' character - print(limiter, string(c.id)) if isa(info, EdgeCallInfo) - print(limiter, optimize ? string(" = ", c.head, ' ') : " = ") + optimize && print(limiter, c.head, ' ') show_callinfo(limiter, info) else - print(limiter, " = ") print_callsite_info(limiter, info) end return nothing diff --git a/src/codeview.jl b/src/codeview.jl index 87f3e3b9..1c13fcc8 100644 --- a/src/codeview.jl +++ b/src/codeview.jl @@ -20,52 +20,52 @@ function highlight(io, x, lexer, config::CthulhuConfig) end end -function cthulhu_llvm(io::IO, mi, src::CodeInfo, optimize::Bool, debuginfo, world::UInt, - config::CthulhuConfig, dump_module::Bool=false, raw::Bool=false) +function cthulhu_llvm(io::IO, provider::AbstractProvider, state::CthulhuState, result::LookupResult; dump_module::Bool=false, raw::Bool=false) + result.src === nothing && error("`result.src` must be a `CodeInfo` to use this function") + (; config) = state + (; optimize, debuginfo) = config + world = get_inference_world(provider) dump = InteractiveUtils._dump_function_llvm( - mi, src, + state.mi, result.src, #=wrapper=# false, !raw, - dump_module, optimize, debuginfo != DInfo.none ? :source : :none, + dump_module, optimize, debuginfo !== :none ? :source : :none, Base.CodegenParams()) highlight(io, dump, "llvm", config) end -function cthulhu_native(io::IO, mi, src::CodeInfo, ::Bool, debuginfo, world::UInt, - config::CthulhuConfig, dump_module::Bool=false, raw::Bool=false) +function cthulhu_native(io::IO, provider::AbstractProvider, state::CthulhuState, result::LookupResult; dump_module::Bool=false, raw::Bool=false) + result.src === nothing && error("`result.src` must be a `CodeInfo` to use this function") + (; config) = state + (; debuginfo, asm_syntax) = config + world = get_inference_world(provider) if dump_module dump = InteractiveUtils._dump_function_native_assembly( - mi, src, + state.mi, result.src, #=wrapper=# false, #=syntax=# config.asm_syntax, - debuginfo != DInfo.none ? :source : :none, + debuginfo != debuginfo !== :none ? :source : :none, #=binary=# false, raw, Base.CodegenParams()) else dump = InteractiveUtils._dump_function_native_disassembly( - mi, world, + state.mi, world, #=wrapper=# false, #=syntax=# config.asm_syntax, - debuginfo != DInfo.none ? :source : :none, + debuginfo != debuginfo !== :none ? :source : :none, #=binary=# false) end highlight(io, dump, "asm", config) end -function cthulhu_ast(io::IO, mi, ::CodeInfo, optimize::Bool, debuginfo, world::UInt, config::CthulhuConfig) - return cthulhu_ast(io, mi, optimize, debuginfo, world, config) -end - -function cthulhu_ast(io::IO, mi, ::Bool, debuginfo, ::UInt, config::CthulhuConfig) - method = mi.def::Method - ast = definition(Expr, method) - if ast !== nothing - if !config.pretty_ast - dump(io, ast; maxdepth=typemax(Int)) - else - show(io, ast) - # Meta.show_sexpr(io, ast) - # Could even highlight the above as some kind-of LISP - end +function cthulhu_ast(io::IO, provider::AbstractProvider, state::CthulhuState, result::LookupResult) + def = state.mi.def + !isa(def, Method) && @warn "Can't show the AST because the definition is not a method." + ast = definition(Expr, def) + ast === nothing && return @warn "Could not retrieve AST of $def. AST display requires Revise.jl to be loaded." + if !state.config.pretty_ast + dump(io, ast; maxdepth=typemax(Int)) else - @warn "Could not retrieve AST of $method. AST display requires Revise.jl to be loaded." + show(io, ast) + # Meta.show_sexpr(io, ast) + # Could even highlight the above as some kind-of LISP end end @@ -84,81 +84,56 @@ function is_type_unstable(code::Union{IRCode, CodeInfo}, idx::Int, used::BitSet) end is_type_unstable(@nospecialize(type)) = type isa Type && (!Base.isdispatchelem(type) || type == Core.Box) -cthulhu_warntype(args...; kwargs...) = cthulhu_warntype(stdout::IO, args...; kwargs...) -function cthulhu_warntype(io::IO, debuginfo::AnyDebugInfo, - src::Union{CodeInfo,IRCode}, @nospecialize(rt), effects::Effects, codeinst::Union{Nothing,CodeInstance}=nothing; - hide_type_stable::Bool=false, inline_cost::Bool=false, optimize::Bool=false, - interp::CthulhuInterpreter=CthulhuInterpreter()) - if inline_cost - isa(mi, MethodInstance) || error("Need a MethodInstance to show inlining costs. Call `cthulhu_typed` directly instead.") - end - cthulhu_typed(io, debuginfo, src, rt, nothing, effects, codeinst; iswarn=true, optimize, hide_type_stable, inline_cost, interp) - return nothing +function cthulhu_warntype(io::IO, provider::AbstractProvider, state::CthulhuState, result::LookupResult) + @reset state.config.iswarn = true + return cthulhu_typed(io, provider, state, result) end -cthulhu_typed(io::IO, debuginfo::DebugInfo, args...; kwargs...) = - cthulhu_typed(io, Symbol(debuginfo), args...; kwargs...) -function cthulhu_typed(io::IO, debuginfo::Symbol, - src::Union{CodeInfo,IRCode}, @nospecialize(rt), @nospecialize(exct), - effects::Effects, codeinst::Union{Nothing,CodeInstance}; - iswarn::Bool=false, hide_type_stable::Bool=false, optimize::Bool=true, - pc2remarks::Union{Nothing,PC2Remarks}=nothing, - pc2effects::Union{Nothing,PC2Effects}=nothing, - pc2excts::Union{Nothing,PC2Excts}=nothing, - inline_cost::Bool=false, type_annotations::Bool=true, annotate_source::Bool=false, - inlay_types_vscode::Bool=false, diagnostics_vscode::Bool=false, jump_always::Bool=false, - interp::AbstractInterpreter=CthulhuInterpreter()) +function cthulhu_source(io::IO, provider::AbstractProvider, state::CthulhuState, result::LookupResult) + return cthulhu_typed(io, provider, state, result) +end - mi = codeinst === nothing ? nothing : codeinst.def +function cthulhu_typed(io::IO, provider::AbstractProvider, state::CthulhuState, result::LookupResult) + (; mi, ci, config) = state + src = something(result.ir, result.src)::Union{IRCode, CodeInfo} - debuginfo = IRShow.debuginfo(debuginfo) + pc2remarks = !result.optimized & config.remarks ? get_pc_remarks(provider, ci) : nothing + pc2effects = config.effects ? get_pc_effects(provider, ci) : nothing + pc2excts = config.exception_types ? get_pc_excts(provider, ci) : nothing + costs = result.optimized & config.inlining_costs ? get_inlining_costs(provider, mi, src) : nothing + + debuginfo = IRShow.debuginfo(config.debuginfo) lineprinter = __debuginfo[debuginfo] - rettype = ignorelimited(rt) + rettype = ignorelimited(result.rt) lambda_io = IOContext(io, :limit=>true) - if annotate_source && isa(src, CodeInfo) - tsn, _ = get_typed_sourcetext(mi, src, rt) + if isa(result.src, CodeInfo) + tsn, _ = get_typed_sourcetext(mi, result.src, result.rt) if tsn !== nothing sig, body = children(tsn) # We empty the body when filling kwargs istruncated = is_leaf(body) idxend = istruncated ? JuliaSyntax.last_byte(sig) : lastindex(tsn.source) - if src.slottypes === nothing - @warn "Inference terminated in an incomplete state due to argument-type changes during recursion" - end - - diagnostics_vscode &= iswarn # If warnings are off then no diagnostics are shown - # Check if diagnostics are avaiable and if mi is defined in a file - if !TypedSyntax.diagnostics_available_vscode() || isnothing(functionloc(mi)[1]) - diagnostics_vscode = false - end - if !TypedSyntax.inlay_hints_available_vscode() || isnothing(functionloc(mi)[1]) - inlay_types_vscode = false - end vscode_io = IOContext( - jump_always && inlay_types_vscode ? devnull : lambda_io, - :inlay_hints => inlay_types_vscode ? Dict{String,Vector{TypedSyntax.InlayHint}}() : nothing , - :diagnostics => diagnostics_vscode ? TypedSyntax.Diagnostic[] : nothing + config.view !== :source || config.jump_always && config.inlay_types_vscode ? devnull : lambda_io, + :inlay_hints => config.inlay_types_vscode ? Dict{String,Vector{TypedSyntax.InlayHint}}() : nothing , + :diagnostics => config.diagnostics_vscode ? TypedSyntax.Diagnostic[] : nothing ) - if istruncated - printstyled(lambda_io, tsn; type_annotations, iswarn, hide_type_stable, idxend) - else - printstyled(vscode_io, tsn; type_annotations, iswarn, hide_type_stable, idxend) - end + source_io = ifelse(istruncated, lambda_io, vscode_io) + printstyled(source_io, tsn; config.type_annotations, config.iswarn, config.hide_type_stable, idxend) + println(source_io) callsite_diagnostics = TypedSyntax.Diagnostic[] - if (diagnostics_vscode || inlay_types_vscode) - vscode_io = IOContext(devnull, :inlay_hints=>vscode_io[:inlay_hints], :diagnostics=>vscode_io[:diagnostics]) - if haskey(interp.unopt, codeinst) # don't process const-proped results - callsite_cis = Dict() # type annotation is a bit long so I skipped it, doesn't seem to affect performance - visited_cis = Set{CodeInstance}((codeinst,)) - add_callsites!(callsite_cis, visited_cis, callsite_diagnostics, codeinst; optimize, annotate_source, interp) - for callsite in values(callsite_cis) - if !isnothing(callsite) - descend_into_callsite!(vscode_io, callsite.tsn; iswarn, hide_type_stable, type_annotations) - end + if (config.diagnostics_vscode || config.inlay_types_vscode) + vscode_io = IOContext(devnull, :inlay_hints => vscode_io[:inlay_hints], :diagnostics => vscode_io[:diagnostics]) + callsite_cis = Dict() # type annotation is a bit long so I skipped it, doesn't seem to affect performance + visited_cis = Set{CodeInstance}((ci,)) + add_callsites!(callsite_cis, visited_cis, callsite_diagnostics, provider, ci, result) + for callsite in values(callsite_cis) + if !isnothing(callsite) + descend_into_callsite!(vscode_io, callsite.tsn; config.type_annotations, config.iswarn, config.hide_type_stable) end end end @@ -167,9 +142,8 @@ function cthulhu_typed(io::IO, debuginfo::Symbol, TypedSyntax.display_diagnostics_vscode(callsite_diagnostics) TypedSyntax.display_inlay_hints_vscode(vscode_io) - (jump_always && inlay_types_vscode) || println(lambda_io) istruncated && @info "This method only fills in default arguments; descend into the body method to see the full source." - return nothing + config.view === :source && return # nothing more to show end end @@ -186,18 +160,12 @@ function cthulhu_typed(io::IO, debuginfo::Symbol, end # preprinter configuration - preprinter = if inline_cost & optimize - isa(mi, MethodInstance) || throw("`mi::MethodInstance` is required") - code = isa(src, IRCode) ? src.stmts.stmt : src.code - cst = Vector{Int}(undef, length(code)) - params = CC.OptimizationParams(interp) - sparams = CC.VarState[CC.VarState(sparam, false) for sparam in mi.sparam_vals] - CC.statement_costs!(cst, code, src, sparams, params) - total_cost = sum(cst) + preprinter = if costs !== nothing + total_cost = sum(costs) nd = ndigits(total_cost) _lineprinter = lineprinter(src) function (io, linestart, idx) - str = idx > 0 ? lpad(cst[idx], nd+1) : + str = idx > 0 ? lpad(costs[idx], nd+1) : idx == -1 ? lpad(total_cost, nd+1) : " "^(nd+1) str = sprint(; context=:color=>true) do @nospecialize io @@ -218,32 +186,24 @@ function cthulhu_typed(io::IO, debuginfo::Symbol, end end # postprinter configuration - ___postprinter = if type_annotations - iswarn ? InteractiveUtils.warntype_type_printer : IRShow.default_expr_type_printer - else - Returns(nothing) - end - __postprinter = if isa(src, CodeInfo) && !isnothing(pc2effects) + ___postprinter = !config.type_annotations ? Returns(nothing) : + config.iswarn ? InteractiveUtils.warntype_type_printer : + IRShow.default_expr_type_printer + __postprinter = pc2effects === nothing ? ___postprinter : function (io::IO; idx::Int, @nospecialize(kws...)) ___postprinter(io; idx, kws...) local effects = get(pc2effects, idx, nothing) effects === nothing && return print(io, ' ', effects) end - else - ___postprinter - end - _postprinter = if isa(src, CodeInfo) && !isnothing(pc2excts) + _postprinter = pc2excts === nothing ? __postprinter : function (io::IO; idx::Int, @nospecialize(kws...)) __postprinter(io; idx, kws...) local exct = get(pc2excts, idx, nothing) exct === nothing && return print(io, ' ', ExctWrapper(exct)) end - else - __postprinter - end - postprinter = if isa(src, CodeInfo) && !isnothing(pc2remarks) + postprinter = pc2remarks === nothing ? _postprinter : begin sort!(pc2remarks) unique!(pc2remarks) # abstract interpretation may have visited a same statement multiple times function (io::IO; idx::Int, @nospecialize(kws...)) @@ -252,28 +212,25 @@ function cthulhu_typed(io::IO, debuginfo::Symbol, printstyled(io, ' ', pc2remarks[i].second; color=:light_black) end end - else - _postprinter end - should_print_stmt = hide_type_stable ? is_type_unstable : Returns(true) + should_print_stmt = config.hide_type_stable ? is_type_unstable : Returns(true) bb_color = (src isa IRCode && debuginfo === :compact) ? :normal : :light_black irshow_config = IRShow.IRShowConfig(preprinter, postprinter; should_print_stmt, bb_color) - if !inline_cost && iswarn + if !config.inlining_costs && config.iswarn print(lambda_io, "Body") InteractiveUtils.warntype_type_printer(lambda_io; type=rettype, used=true) - if get(lambda_io, :with_effects, false)::Bool - print(lambda_io, ' ', effects) + if get(lambda_io, :effects, false)::Bool + print(lambda_io, ' ', result.effects) end println(lambda_io) else - isa(codeinst, CodeInstance) || throw("`codeinst::CodeInstance` is required") cfg = src isa IRCode ? src.cfg : CC.compute_basic_blocks(src.code) max_bb_idx_size = length(string(length(cfg.blocks))) str = irshow_config.line_info_preprinter(lambda_io, " "^(max_bb_idx_size + 2), -1) - callsite = Callsite(0, EdgeCallInfo(codeinst, rettype, effects, exct), :invoke) + callsite = Callsite(0, EdgeCallInfo(ci, rettype, result.effects, result.exct), :invoke) println(lambda_io, "∘ ", "─"^(max_bb_idx_size), str, " ", callsite) end @@ -293,21 +250,11 @@ function descend_into_callsite!(io::IO, tsn::TypedSyntaxNode; end function add_callsites!(d::AbstractDict, visited_cis::AbstractSet, diagnostics::AbstractVector, - ci::CodeInstance, source_ci::CodeInstance=ci; - optimize::Bool=true, annotate_source::Bool=false, - interp::AbstractInterpreter=CthulhuInterpreter()) - mi = ci.def - - callsites, src, rt = try - (; src, rt, infos, slottypes, effects, codeinf) = lookup(interp, ci, optimize & !annotate_source) - - # We pass false as it doesn't affect callsites and skips fetching the method definition - # using CodeTracking which is slow - callsites, _ = find_callsites(interp, src, infos, ci, slottypes, optimize & !annotate_source, false) - callsites, src, rt - catch - return nothing - end + provider::AbstractProvider, ci::CodeInstance, result::LookupResult, + source_ci::CodeInstance=ci) + mi = get_mi(ci) + (; ir, src, rt, infos, slottypes, effects, optimized) = result + callsites, _ = find_callsites(provider, result, ci) for callsite in callsites info = callsite.info @@ -320,14 +267,15 @@ function add_callsites!(d::AbstractDict, visited_cis::AbstractSet, diagnostics:: in(callsite_ci, visited_cis) && continue push!(visited_cis, callsite_ci) - add_callsites!(d, visited_cis, diagnostics, callsite_ci, source_ci; optimize, annotate_source, interp) + result = LookupResult(provider, callsite_ci, optimized)::LookupResult + add_callsites!(d, visited_cis, diagnostics, provider, callsite_ci, result, source_ci) end # Check if callsite is not just filling in default arguments and defined in same file as source_ci if ci == source_ci || ci.def.def.file != source_ci.def.def.file return nothing end - tsn, _ = get_typed_sourcetext(mi, src, rt; warn=false) + tsn, _ = get_typed_sourcetext(mi, something(src, ir)::Union{CodeInfo, IRCode}, rt; warn=false) isnothing(tsn) && return nothing sig, body = children(tsn) # We empty the body when filling kwargs @@ -364,129 +312,3 @@ function show_variables(io, src, slotnames) end println(io) end - -# These are standard code views that don't need any special handling, -# This namedtuple maps toggle::Symbol to function -const CODEVIEWS = (; - # typed=cthulhu_typed, - llvm=cthulhu_llvm, - native=cthulhu_native, - ast=cthulhu_ast, -) - -""" - Cthulhu.Bookmark - -A `Cthulhu.Bookmark` remembers a method marked by `b` key during a descent. -It can be used with the following functions: - -* `descend(::Bookmark)`, `descend_code_typed(::Bookmark)`, - `descend_code_warntype(::Bookmark)`: continue the descent. -* `code_typed(::Bookmark)`, `code_warntype([::IO,] ::Bookmark)`: show typed IR -* `code_llvm([::IO,] ::Bookmark)`: pretty-print LLVM IR -* `code_native([::IO,] ::Bookmark)`: pretty-print native code -""" -struct Bookmark - ci::CodeInstance - interp::AbstractInterpreter -end - -""" - Cthulhu.BOOKMARKS :: Vector{Bookmark} - -During a descent, methods can be "bookmarked" by pressing `b` key. It -pushes a [`Cthulhu.Bookmark`](@ref) into `Cthulhu.BOOKMARKS`. This can be -used to, e.g., continue descending by `descend(Cthulhu.BOOKMARKS[end])`. -See [`Cthulhu.Bookmark`](@ref) for other usages. -""" -const BOOKMARKS = Bookmark[] - -# Turn off `optimize` and `debuginfo` for default `show` so that the -# output is smaller. -function Base.show( - io::IO, ::MIME"text/plain", b::Bookmark; - optimize::Bool=false, debuginfo::AnyDebugInfo=:none, iswarn::Bool=false, hide_type_stable::Bool=false) - world = get_inference_world(b.interp) - CI, rt = InteractiveUtils.code_typed(b; optimize) - (; interp, ci) = b - (; effects) = lookup(interp, ci, optimize) - if get(io, :typeinfo, Any) === Bookmark # a hack to check if in Vector etc. - print(io, Callsite(-1, EdgeCallInfo(b.ci, rt, Effects()), :invoke)) - print(io, " (world: ", world, ")") - return - end - println(io, "Cthulhu.Bookmark (world: ", world, ")") - cthulhu_typed(io, debuginfo, CI, rt, nothing, effects, b.ci; iswarn, optimize, hide_type_stable, b.interp) -end - -function InteractiveUtils.code_typed(b::Bookmark; optimize::Bool=true) - (; interp, ci) = b - (; src, rt, codeinf) = lookup(interp, ci, optimize) - return codeinf => rt -end - -InteractiveUtils.code_warntype(b::Bookmark; kw...) = - InteractiveUtils.code_warntype(stdout::IO, b; kw...) -function InteractiveUtils.code_warntype( - io::IO, - b::Bookmark; - optimize::Bool=false, - debuginfo::AnyDebugInfo = :source, - hide_type_stable::Bool = true, - kw..., -) - CI, rt = InteractiveUtils.code_typed(b; kw...) - (; interp, ci) = b - (; effects) = lookup(interp, ci, optimize) - cthulhu_warntype(io, debuginfo, CI, rt, effects, b.ci; optimize, hide_type_stable, b.interp) -end - -InteractiveUtils.code_llvm(b::Bookmark; kw...) = InteractiveUtils.code_llvm(stdout::IO, b; kw...) -InteractiveUtils.code_native(b::Bookmark; kw...) = - InteractiveUtils.code_native(stdout::IO, b; kw...) - -function InteractiveUtils.code_llvm( - io::IO, - b::Bookmark; - optimize = true, - debuginfo = :source, - dump_module = false, - raw = false, - config = CONFIG, -) - src = CC.typeinf_code(b.interp, b.ci.def, true) - return cthulhu_llvm( - io, - b.ci.def, - src, - optimize, - debuginfo === :source, - get_inference_world(b.interp), - config, - dump_module, - raw, - ) -end - -function InteractiveUtils.code_native( - io::IO, - b::Bookmark; - optimize = true, - debuginfo = :source, - dump_module = false, - raw = false, - config = CONFIG, -) - src = CC.typeinf_code(b.interp, b.ci.def, true) - return cthulhu_native( - io, - b.ci.def, - src, - optimize, - debuginfo === :source, - get_inference_world(b.interp), - config, - dump_module, - raw, - ) -end diff --git a/src/compiler.jl b/src/compiler.jl new file mode 100644 index 00000000..40e975dc --- /dev/null +++ b/src/compiler.jl @@ -0,0 +1,204 @@ +AbstractProvider(interp::NativeInterpreter) = DefaultProvider(interp) + +function AbstractProvider(interp::AbstractInterpreter) + error(lazy"""missing `$AbstractInterpreter` API: + `$(typeof(interp))` is required to implement `$AbstractProvider(interp::$(typeof(interp)))) -> AbstractProvider`. + """) +end + +function find_method_instance(provider::AbstractProvider, interp::AbstractInterpreter, @nospecialize(tt::Type{<:Tuple}), world::UInt) + mt = method_table(interp) + match, valid_worlds = findsup(tt, mt) + match === nothing && return nothing + mi = specialize_method(match) + return mi +end + +function generate_code_instance(provider::AbstractProvider, interp::AbstractInterpreter, mi::MethodInstance) + ci = run_type_inference(provider, interp, mi) + return ci +end + +function find_caller_of(provider::AbstractProvider, interp::AbstractInterpreter, callee::Union{MethodInstance,Type}, mi::MethodInstance, allow_unspecialized::Bool) + ci = generate_code_instance(provider, interp, mi) + @assert get_mi(ci) === mi + locs = Tuple{Core.LineInfoNode,Int}[] + for optimize in (true, false) + result = LookupResult(provider, interp, ci, optimize)::LookupResult + callsites, _ = find_callsites(provider, result, ci) + callsites = allow_unspecialized ? filter(cs -> maybe_callsite(cs, callee), callsites) : + filter(cs -> is_callsite(cs, callee), callsites) + foreach(cs -> add_sourceline!(locs, result.src, cs.id, mi), callsites) + end + # Consolidate by method, but preserve the order + prlookup = Dict{Tuple{Symbol,Symbol},Int}() + ulocs = Pair{Tuple{Symbol,Symbol,Int},Vector{Int}}[] + if !isempty(locs) + for (loc, depth) in locs + locname = loc.method + if isa(locname, MethodInstance) + locname = locname.def.name + end + idx = get(prlookup, (locname, loc.file), nothing) + if idx === nothing + push!(ulocs, (locname, loc.file, depth) => Int[]) + prlookup[(locname, loc.file)] = idx = length(ulocs) + end + lines = ulocs[idx][2] + line = loc.line + if line ∉ lines + push!(lines, line) + end + end + end + return ulocs +end + +function get_inlining_costs(provider::AbstractProvider, interp::AbstractInterpreter, mi::MethodInstance, src::Union{CodeInfo, IRCode}) + code = isa(src, IRCode) ? src.stmts.stmt : src.code + costs = zeros(Int, length(code)) + params = CC.OptimizationParams(interp) + sparams = CC.VarState[CC.VarState(sparam, false) for sparam in mi.sparam_vals] + CC.statement_costs!(costs, code, src, sparams, params) + return costs +end + +show_parameters(io::IO, provider::AbstractProvider, interp::AbstractInterpreter) = show_inference_cache(io, interp) + +function show_inference_cache(io::IO, interp::AbstractInterpreter) + @info "Dumping inference cache." + cache = CC.get_inference_cache(interp) + for (i, (; linfo, result)) in enumerate(cache) + println(io, i, ": ", linfo, "::", result) + end +end + +function LookupResult(provider::AbstractProvider, interp::AbstractInterpreter, ci::CodeInstance, optimize::Bool) + optimize && return lookup_optimized(provider, interp, ci) + return lookup_unoptimized(provider, interp, ci) +end + +function LookupResult(provider::AbstractProvider, interp::AbstractInterpreter, result::InferenceResult, optimize::Bool) + optimize && return lookup_constproped_optimized(provider, interp, result) + return lookup_constproped_unoptimized(provider, interp, result) +end + +function LookupResult(provider::AbstractProvider, interp::AbstractInterpreter, call::SemiConcreteCallInfo, optimize::Bool) + return lookup_semiconcrete(provider, interp, call) +end + +struct InferredSource + src::CodeInfo + stmt_info::Vector{Any} + effects::Effects + rt::Any + exct::Any + function InferredSource(src, stmt_info, effects, @nospecialize(rt), @nospecialize(exct)) + return new(src, stmt_info, effects, rt, exct) + end +end + +struct OptimizedSource + ir::IRCode + src::CodeInfo + isinlineable::Bool + effects::Effects +end + +const InferenceKey = Union{CodeInstance,InferenceResult} # TODO make this `CodeInstance` fully +const InferenceDict{InferenceValue} = IdDict{InferenceKey, InferenceValue} +const PC2Remarks = Vector{Pair{Int, String}} +const PC2CallMeta = Dict{Int, CallMeta} +const PC2Effects = Dict{Int, Effects} +const PC2Excts = Dict{Int, Any} + +function lookup_optimized(provider::AbstractProvider, interp::AbstractInterpreter, ci::CodeInstance) + mi = get_mi(ci) + rt = cached_return_type(ci) + exct = cached_exception_type(ci) + effects = get_effects(ci) + if ci.inferred === nothing + if CC.use_const_api(ci) + @assert isdefined(ci, :rettype_const) + @static if VERSION > v"1.13-" + range = CC.WorldRange(1, typemax(UInt)) + src = CC.codeinfo_for_const(interp, get_mi(ci), range, Core.svec(), ci.rettype_const) + else + src = CC.codeinfo_for_const(interp, get_mi(ci), ci.rettype_const) + end + src.ssavaluetypes = Any[Any] + infos = Any[CC.NoCallInfo()] + slottypes = Any[] + return LookupResult(nothing, src, rt, exct, infos, slottypes, effects, true) + else + @warn "Inference decided not to cache optimized code for $mi; unoptimized code will be returned instead." + return lookup_unoptimized(provider, interp, ci) + end + end + opt = OptimizedSource(provider, interp, ci) + ir = copy(opt.ir) + infos = collect(Any, ir.stmts.info) + slottypes = ir.argtypes + return LookupResult(ir, opt.src, rt, exct, infos, slottypes, effects, true) +end + +function lookup_unoptimized(provider::AbstractProvider, interp::AbstractInterpreter, ci::CodeInstance) + unopt = InferredSource(provider, interp, ci) + src = copy(unopt.src) + (; rt, exct) = unopt + infos = unopt.stmt_info + slottypes = @something(src.slottypes, Any[Any for _ in 1:length(src.slotflags)]) + return LookupResult(nothing, src, rt, exct, infos, slottypes, unopt.effects, false) +end + +function lookup_constproped_optimized(provider::AbstractProvider, interp::AbstractInterpreter, override::InferenceResult) + opt = OptimizedSource(provider, interp, override) + ir = copy(opt.ir) + src = ir_to_src(ir) + rt = override.result + exct = override.exc_result + infos = ir.stmts.info + slottypes = ir.argtypes + return LookupResult(ir, src, rt, exct, infos, slottypes, opt.effects, true) +end + +function lookup_constproped_unoptimized(provider::AbstractProvider, interp::AbstractInterpreter, override::InferenceResult) + unopt = InferredSource(provider, interp, override) + src = copy(unopt.src) + (; rt, exct) = unopt + infos = unopt.stmt_info + effects = get_effects(unopt) + slottypes = @something(src.slottypes, Any[Any for _ in 1:length(src.slotflags)]) + return LookupResult(nothing, src, rt, exct, infos, slottypes, effects, false) +end + +function lookup_semiconcrete(provider::AbstractProvider, interp::AbstractInterpreter, override::SemiConcreteCallInfo) + ir = copy(override.ir) + src = ir_to_src(ir) + rt = get_rt(override) + exct = Any # TODO + infos = ir.stmts.info + effects = get_effects(override) + return LookupResult(ir, src, rt, exct, infos, src.slottypes, effects, true) +end + +function ir_to_src(ir::IRCode; slotnames = nothing) + nargs = length(ir.argtypes) + src = ccall(:jl_new_code_info_uninit, Ref{CodeInfo}, ()) + slotnames = @something(slotnames, [Symbol(:_, i) for i in 1:nargs]) + length(slotnames) == length(ir.argtypes) || error("mismatched `argtypes` and `slotnames`") + + src.nargs = nargs + src.isva = false + src.slotnames = slotnames + src.slotflags = fill(zero(UInt8), nargs) + src.slottypes = copy(ir.argtypes) + CC.replace_code_newstyle!(src, ir) + CC.widen_all_consts!(src) + return src +end + +function run_type_inference(provider::AbstractProvider, interp::AbstractInterpreter, mi::MethodInstance) + ci = CC.typeinf_ext(interp, mi, CC.SOURCE_MODE_GET_SOURCE) + return ci::CodeInstance +end diff --git a/src/config.jl b/src/config.jl new file mode 100644 index 00000000..68b43e02 --- /dev/null +++ b/src/config.jl @@ -0,0 +1,58 @@ +Base.@kwdef struct CthulhuConfig + enable_highlighter::Bool = false + highlighter::Cmd = `pygmentize -l` + asm_syntax::Symbol = :att + pretty_ast::Bool = false + debuginfo::Symbol = :compact + optimize::Bool = true + iswarn::Bool = false + hide_type_stable::Bool = false + remarks::Bool = false + effects::Bool = false + exception_types::Bool = false + inlining_costs::Bool = false + type_annotations::Bool = true + inlay_types_vscode::Bool = true + diagnostics_vscode::Bool = true + jump_always::Bool = false + view::Symbol = :source + menu_options::NamedTuple = () + function CthulhuConfig(enable_highlighter, highlighter, asm_syntax, pretty_ast, debuginfo, optimize, iswarn, hide_type_stable, remarks, effects, exception_types, inlining_costs, type_annotations, inlay_types_vscode, diagnostics_vscode, jump_always, view, menu_options) + diagnostics_vscode &= iswarn # if warnings are off, then no diagnostics are shown + diagnostics_vscode &= TypedSyntax.diagnostics_available_vscode() + inlay_types_vscode &= TypedSyntax.inlay_hints_available_vscode() + optimize &= view !== :source + return new(enable_highlighter, highlighter, asm_syntax, pretty_ast, debuginfo, optimize, iswarn, hide_type_stable, remarks, effects, exception_types, inlining_costs, type_annotations, inlay_types_vscode, diagnostics_vscode, jump_always, view, menu_options) + end +end + +""" + Cthulhu.CONFIG + +# Options +- `enable_highlighter::Bool`: Use command line `highlighter` to syntax highlight + Julia, LLVM and native code. +- `highlighter::Cmd`: A command line program that receives "julia" as an argument and julia + code as stdin. Defaults to `$(CthulhuConfig().highlighter)`. +- `asm_syntax::Symbol`: Set the syntax of assembly code being used. + Defaults to `$(CthulhuConfig().asm_syntax)`. +- `pretty_ast::Bool`: Use a pretty printer for the ast dump. Defaults to `false`. +- `debuginfo::Symbol`: Initial state of "debuginfo" toggle. Defaults to `:compact`. + Options:. `:none`, `:compact`, `:source` +- `optimize::Bool`: Initial state of "optimize" toggle. Defaults to `true`. +- `hide_type_stable::Bool`: Initial state of "hide_type_stable" toggle. Defaults to `false`. +- `iswarn::Bool`: Initial state of "warn" toggle. Defaults to `false`. +- `remarks::Bool` Initial state of "remarks" toggle. Defaults to `false`. +- `effects::Bool` Intial state of "effects" toggle. Defaults to `false`. +- `exception_types::Bool` `Intial state of "exception types" toggle. Defaults to `false`. +- `inlining_costs::Bool` Initial state of "inlining costs" toggle. Defaults to `false`. +- `type_annotations::Bool` Initial state of "type annnotations" toggle. Defaults to `true`. +- `view::Symbol` Initial state of the view. Defaults to `:source`. Can be either of `:source`, `:ast`, `:typed`, `:llvm` and `:native`. Non-default `AbstractProvider`s may further customize available views. +- `inlay_types_vscode::Bool` Initial state of "vscode: inlay types" toggle. Defaults to `true` +- `diagnostics_vscode::Bool` Initial state of "Vscode: diagnostics" toggle. Defaults to `true` +- `jump_always::Bool` Initial state of "jump to source always" toggle. Defaults to `false`. + +Other keyword arguments are passed to [`Cthulhu.CthulhuMenu`](@ref) and/or +[`REPL.TerminalMenus`](https://docs.julialang.org/en/v1/stdlib/REPL/#Customization-/-Configuration). +""" +global CONFIG::CthulhuConfig = CthulhuConfig() diff --git a/src/descend.jl b/src/descend.jl new file mode 100644 index 00000000..dff996da --- /dev/null +++ b/src/descend.jl @@ -0,0 +1,212 @@ +function descend_with_error_handling(args...; kwargs...) + @nospecialize + try + return _descend(args...; kwargs...) + catch x + isa(x, InterruptException) && return :interrupted + rethrow(x) + finally + TypedSyntax.clear_all_vscode() + end + return nothing +end + +function _descend(terminal::AbstractTerminal, provider::AbstractProvider, mi::MethodInstance; kwargs...) + ci = generate_code_instance(provider, mi) + config = set_config(CONFIG; kwargs...) + state = CthulhuState(provider; terminal, config, mi, ci) + descend!(state) +end + +function _descend(terminal::AbstractTerminal, provider::AbstractProvider, @nospecialize(args...); world = Base.tls_world_age(), kwargs...) + mi = find_method_instance(provider, args..., world) + isa(mi, MethodInstance) || error("No method instance found for $(join(args, ", "))") + _descend(terminal, provider, mi; kwargs...) +end + +function _descend(terminal::AbstractTerminal, @nospecialize(args...); interp=NativeInterpreter(), provider=AbstractProvider(interp), kwargs...) + _descend(terminal, provider, args...; kwargs...) +end + +_descend(@nospecialize(args...); terminal::AbstractTerminal=default_terminal(), kwargs...) = + _descend(terminal, args...; kwargs...) + +function _descend(bookmark::Bookmark; terminal::AbstractTerminal=default_terminal(), kwargs...) + state = CthulhuState(bookmark; terminal, kwargs...) + descend!(state) +end + +## +# descend! is the main driver function. +# src/reflection.jl has the tools to discover methods. +# src/state.jl provides the state that is mutated by `descend!`. +# src/ui.jl provides the user facing interface to which `descend!` responds. +## +function descend!(state::CthulhuState) + (; provider) = state + commands = menu_commands(provider) + status = nothing + if !isa(commands, Vector{Command}) + error(lazy""" + invalid `$AbstractProvider` API: + `$(Cthulhu.commands)(provider::$(typeof(provider))` is expected to have `Vector{Command}` as return type. + """) + end + while true + (; config, mi, ci) = state + iostream = state.terminal.out_stream::IO + menu_options = (; cursor = '•', scroll_wrap = true, config.menu_options...) + + mi::MethodInstance, ci::CodeInstance + + src = something(state.override, ci) + result = LookupResult(provider, src, config.optimize)::LookupResult + + if config.jump_always + def = state.mi.def + if !isa(def, Method) + @warn "Can't jump to source location because the definition is not a method." + else + if isdefined(Main, :VSCodeServer) && Main.VSCodeServer isa Module && isdefined(Main.VSCodeServer, :openfile) + Main.VSCodeServer.openfile(whereis(def)...; preserve_focus=true) + else + edit(whereis(def)...) + end + end + end + + if state.display_code + printstyled(IOContext(iostream, :limit => true), mi.def, '\n'; bold=true) + ioctx = IOContext(iostream, + :color => true, + :displaysize => displaysize(iostream), # displaysize doesn't propagate otherwise + :SOURCE_SLOTNAMES => source_slotnames(result), + :effects => config.effects, + :exception_types => config.exception_types) + str = stringify(ioctx) do io + view_function(state)(io, provider, state, result) + end + # eliminate trailing indentation (see first item in bullet list in PR #189) + rmatch = findfirst(r"\u001B\[90m\u001B\[(\d+)G( *)\u001B\[1G\u001B\[39m\u001B\[90m( *)\u001B\[39m$", str) + if rmatch !== nothing + str = str[begin:prevind(str, first(rmatch))] + end + print(iostream, str) + end + + callsites, source_nodes = find_callsites(provider, result, ci, config.view === :source) + + @label show_menu + + shown_callsites = config.view === :source ? source_nodes : callsites + + state.display_code = false + menu = CthulhuMenu(state, shown_callsites, config.effects, config.exception_types, config.optimize, + config.iswarn & get(iostream, :color, false)::Bool, + config.hide_type_stable, commands; menu_options...) + usg = usage(provider, state, commands) + cid = request(state.terminal, usg, menu) + toggle = menu.toggle + toggle === :ascend && break + + if toggle === nothing + callsite = select_callsite(state, callsites, source_nodes, cid, menu_options, commands)::Union{Symbol, Callsite} + callsite === :ascend && (status = :ascended; break) + callsite === :exit && (status = :exited; break) + callsite === :continue && continue + (; info) = callsite + + # forcibly enter and inspect the frame, although the native interpreter gave up + if info isa TaskCallInfo + @info """ + Inference didn't analyze this call because it is a dynamic call: + Cthulhu nevertheless is trying to descend into it for further inspection. + """ + state.ci = get_ci(info)::CodeInstance + descend!(state) + continue + elseif info isa RTCallInfo + @info """ + This is a runtime call. You cannot descend into it. + """ + @goto show_menu + end + + ci = get_ci(callsite) + override = get_override(provider, info) + ci === nothing && override === nothing && continue + + # Recurse into the callsite. + + prev = save_descend_state(state) + state.mi = get_mi(ci) + state.ci = ci + state.override = override + state.display_code = true + status = descend!(state) + status === :exited && break + restore_descend_state!(state, prev) + state.display_code = true + + println(iostream) + end + end + + TypedSyntax.clear_all_vscode() + return status +end + +function select_callsite(state::CthulhuState, callsites::Vector{Callsite}, source_nodes, i::Int, menu_options::NamedTuple, commands::Vector{Command}) + (; config) = state + i === length(callsites) + 1 && return :ascend + i === -1 && return :exit + + callsite = callsites[i] + !isa(callsite.info, MultiCallInfo) && return callsite + + (; info) = callsite + source_node = !isempty(source_nodes) ? source_nodes[i] : nothing + sub_callsites = map(ci -> Callsite(callsite.id, ci, callsite.head), info.callinfos) + if isempty(sub_callsites) + @eval Main begin + state = $state + callsite = $callsite + end + error("Expected multiple callsites, but found none. Please fill an issue with a reproducing example.") + end + menu_callsites = source_node === nothing ? sub_callsites : menu_callsites_from_source_node(callsite, source_node) + io = state.terminal.out_stream::IO + printstyled(io, "\nChoose which of the possible calls to descend into:"; color=:blue) + menu = CthulhuMenu(state, menu_callsites, config.effects, config.exception_types, config.optimize, + false, false, commands; + sub_menu=true, menu_options...) + i = request(state.terminal, "", menu) + i === length(sub_callsites) + 1 && return :continue + i === -1 && return :exit + + return sub_callsites[i] +end + +function menu_callsites_from_source_node(callsite::Callsite, source_node) + callsites = Union{Callsite,TypedSyntax.MaybeTypedSyntaxNode}[] + for info in callsite.info.callinfos + ci = get_ci(info) + mi = get_mi(ci) + p = Base.unwrap_unionall(mi.specTypes).parameters + if isa(source_node, TypedSyntax.MaybeTypedSyntaxNode) && length(p) == length(children(source_node)) + 1 + new_node = copy(source_node) + for (i, child) in enumerate(children(new_node)) + child.typ = p[i+1] + end + push!(callsites, new_node) + else + push!(callsites, Callsite(callsite.id, info, callsite.head)) + end + end + return callsites +end + +function source_slotnames(result::LookupResult) + result.src === nothing && return false + return Base.sourceinfo_slotnames(result.src) +end diff --git a/src/interface.jl b/src/interface.jl index b7e73ec8..09c163af 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -1,97 +1,212 @@ """ - AbstractCursor - -Required overloads: -- `Cthulhu.lookup(interp::AbstractInterpreter, curs::AbstractCursor, optimize::Bool)` -- `Cthulhu.lookup_constproped(interp::AbstractInterpreter, curs::AbstractCursor, override::InferenceResult, optimize::Bool)` -- `Cthulhu.get_ci(curs::AbstractCursor) -> CodeInstance` -- `Cthulhu.update_cursor(curs::AbstractCursor, mi::MethodInstance)` -- `Cthulhu.navigate(curs::AbstractCursor, callsite::Callsite) -> AbstractCursor` + abstract type AbstractProvider end + +`AbstractProvider` drives the high-level interface for Cthulhu, +allowing it to *provide* all the data Cthulhu needs for its +display and callsite introspection features. + +!!! warning + This interface is still considered experimental at this time; future changes are to + be expected, in which case we will do our best to communicate them in CHANGELOG.md. + +Cthulhu relies mainly on the following types from Base: +- `MethodInstance`, representing a specialization of a method. +- `CodeInstance`, the high-level result of the Julia compilation pipeline. +- `CodeInfo`, representing Julia code, as output e.g. by `@code_typed`. + +And a few from the compiler: +- `Compiler.IRCode`, somewhat similar to `CodeInfo`, but in a different representation. +- `Compiler.Effects`, code metadata used to model part of its semantics and behavior. +- `Compiler.InferenceResult`, holding the results of inference. + +Cthulhu is integrated with the Julia compiler, and allows reuse +of its mechanisms to generate these data structures. In particular, +default methods are defined for `AbstractProvider`s that are associated with +a `Compiler.AbstractInterpreter` (the interface type for Compiler, analogous +to `AbstractProvider` for Cthulhu). This allows to reuse some of the commonly +used patterns that are expected with the majority of Compiler integrations. +The association is made by returning an `AbstractInterpreter` with +`Cthulhu.get_abstract_interpreter(provider::SomeProvider)`, which by default +returns `nothing` (no association). + +The reverse is implemented as well: sometimes, an `AbstractInterpreter` implemented +by a package is central enough that it is part of its API. In this case, this package +may define a constructor `Cthulhu.AbstractProvider(interp::SomeInterpreter)` to automatically +pick the correct provider just by doing `@descend interp=SomeInterpreter() f(x)`. + +The interface for `AbstractProvider` requires a few methods to be defined. If it is associated +with an `AbstractInterpreter` (see two paragraphs above), then only the following is required: +- `run_type_inference(provider::SomeProvider, interp::SomeInterpreter, mi::MethodInstance)` to emit a `CodeInstance` + by invoking regular type inference, typically calling `typeinf_` methods from Compiler. +- `OptimizedSource(provider::SomeProvider, interp::SomeInterpreter, ci::CodeInstance)` +- `OptimizedSource(provider::SomeProvider, interp::SomeInterpreter, result::InferenceResult)` + (for uncached inference results, e.g. when looking up callsites emanating from concrete evaluation) +- `InferredSource(provider::SomeProvider, interp::SomeInterpreter, ci::CodeInstance)` + +By default, an `AbstractProvider` is not associated with any particular `Compiler.AbstractInterpreter`, +with `Cthulhu.get_abstract_interpreter(provider::SomeProvider)` returning `nothing`. In this case, +the following methods are required: +- `get_inference_world(provider::SomeProvider)`, returning the world in which the provider operates. Methods and bindings (including e.g. types and globals) defined after this world age should not be taken into account when providing the data Cthulhu needs. +- `find_method_instance(provider::SomeProvider, tt::Type{<:Tuple}, world::UInt)`, to return the specialization of the matching method for `tt`, returning a `MethodInstance`. +- `generate_code_instance(provider::SomeProvider, mi::MethodInstance)` to emit the `CodeInstance` + that holds compilation results to be later retrieved with `LookupResult`. +- `LookupResult(provider::SomeProvider, src, optimize::Bool)` to assemble most of the information + that Cthulhu needs to display code and to provide callsites to select for further descent. `src` + is the generated `CodeInstance` or any override emanating from `get_override(provider::SomeProvider, info::Cthulhu.CallInfo)` (more on that a few paragraphs below). + which by default may return `InferenceResult` (constant propagation) or `SemiConcreteCallInfo` (semi-concrete evaluation). This "override" replaces a `CodeInstance` for callsites + that are semantically associated with one but which have further information available, or for which the compiler decides to not cache one. +- `find_caller_of(provider::SomeProvider, callee::Union{MethodInstance,Type}, mi::MethodInstance; allow_unspecialized::Bool=false)` to find occurrences of calls to `callee` in the provided method instance. This is the backbone of [`ascend`](@ref), and is not used for `descend`. + +Callsites are retrieved by inspecting `Compiler.CallInfo` data structures (which are currently distinct from `Cthulhu.CallInfo`, see the warning below), which are metadata +semantically attached to expressions, relating one to the process that produced them. +In most cases, expressions semantically related to a call to another function will have a `Compiler.CallInfo` attached +that maps to the relevant `CodeInstance`. However, it may be useful to note that there may be cases where a `CodeInstance` is not readily available: that is the case for +constant propagation (the original `CodeInstance` is not kept if the result is +inferred to a constant), and for semi-concrete evaluation which produces temporary refined IR +that contains more accurate information than the `CodeInstance` it originates from. + +When selecting a callsite, a `LookupResult` is constructed from the data that originates from the corresponding `Cthulhu.CallInfo` structure (which is derived from the `Compiler.CallInfo` metadata). +Generally, this will be a `CodeInstance`, but it may be overriden with another source, depending on the implementation of +`get_override(provider::SomeProvider, info::Cthulhu.CallInfo)` (which may be extended for custom providers). +By default, the override will be either a `Compiler.SemiConcreteCallInfo` for semi-concrete +evaluation, or a `Compiler.InferenceResult` for the results of constant propagation. + +!!! warn + `Compiler.CallInfo` and `Cthulhu.CallInfo` are currently distinct abstract types. + This part of Cthulhu will likely be subject to change, and is not yet publicly + available for customization by `AbstractProvider`s beyond its downstream use via `get_override`. + If feeling adventurous, you may extend the `Cthulhu.find_callsites` internal function, + however you are encouraged to file an issue and/or contribute to making this part more + accessible by breaking it into well-interfaced parts instead of duplicating its implementation. + +Optionally, `AbstractProvider`s may modify default behavior by extending the following: +- `get_override(provider::SomeProvider, info::Cthulhu.CallInfo)`: return a data structure that is + more accurate than the `CodeInstance` associated with a given `Cthulhu.CallInfo`. + +If any of the relevant `CthulhuConfig` fields may be set, these should be implemented as well +or will give a warning by default when called: +- `get_pc_remarks(provider::SomeProvider, ci::CodeInstance)` (required for the 'remarks' toggle) +- `get_pc_effects(provider::SomeProvider, ci::CodeInstance)` (required for the 'effects' toggle) +- `get_pc_excts(provider::SomeProvider, ci::CodeInstance)` (required for the 'exception types' toggle) +- `get_inlining_costs(provider::SomeProvider, mi::MethodInstance, src::Union{CodeInfo, IRCode})` + (required for the 'inlining costs' toggle) + +`AbstractProvider`s may additionally extend the following UI-related methods: +- `menu_commands(provider::SomeProvider)` to provide commands for the UI. You will most likely + want to add or remove commands to `default_menu_commands(provider)` to keep all or part of the + built-in commands. +- `is_command_enabled(provider::SomeProvider, state::CthulhuState, command::Command)` to + dynamically enable/disable commands based on e.g. which view we are in. You will most likely want to fall back to + `Cthulhu.is_default_command_enabled(provider, state, command)` to keep the behavior for default commands. +- `show_command(io::IO, provider::SomeProvider, state::CthulhuState, command::Command)` to customize + the display of commands in the terminal UI. The defaults should be good enough for most uses, however. + You may take a look at the source code to know how it interacts with other customizable entry points, + such as `style_for_command_key` and `value_for_command`. + +If you do not intend to provide a method that would be required for a specific UI component (for example, +you do not want to support displaying remarks, effects or exception types), you should extend +`Cthulhu.menu_commands(provider::SomeProvider)` and return a list of commands that excludes the +corresponding command. To that effect, you can use `Cthulhu.default_menu_commands(provider)` +and simply filter out the relevant command. """ -abstract type AbstractCursor; end -struct CthulhuCursor <: AbstractCursor - ci::CodeInstance +abstract type AbstractProvider end + +get_abstract_interpreter(provider::AbstractProvider) = nothing + +function CC.get_inference_world(provider::AbstractProvider) + interp = get_abstract_interpreter(provider) + interp !== nothing && return get_inference_world(interp) + error(lazy"Not implemented for $provider") end -lookup(interp::AbstractInterpreter, curs::AbstractCursor, optimize::Bool) = error(lazy""" -missing `$AbstractCursor` API: -`$(typeof(curs))` is required to implement the `$lookup(interp::$(typeof(interp)), curs::$(typeof(curs)), optimize::Bool)` interface. -""") -lookup(interp::CthulhuInterpreter, curs::CthulhuCursor, optimize::Bool) = - lookup(interp, get_ci(curs), optimize) - -lookup_constproped(interp::AbstractInterpreter, curs::AbstractCursor, override::InferenceResult, optimize::Bool) = error(lazy""" -missing `$AbstractCursor` API: -`$(typeof(curs))` is required to implement the `$lookup_constproped(interp::$(typeof(interp)), curs::$(typeof(curs)), override::InferenceResult, optimize::Bool)` interface. -""") -lookup_constproped(interp::CthulhuInterpreter, ::CthulhuCursor, override::InferenceResult, optimize::Bool) = - lookup_constproped(interp, override, optimize) - -lookup_semiconcrete(interp::AbstractInterpreter, curs::AbstractCursor, override::SemiConcreteCallInfo, optimize::Bool) = error(lazy""" -missing `$AbstractCursor` API: -`$(typeof(curs))` is required to implement the `$lookup_semicocnrete(interp::$(typeof(interp)), curs::$(typeof(curs)), override::SemiConcreteCallInfo, optimize::Bool)` interface. -""") -lookup_semiconcrete(interp::CthulhuInterpreter, curs::CthulhuCursor, override::SemiConcreteCallInfo, optimize::Bool) = - lookup_semiconcrete(interp, get_ci(curs), override, optimize) - -get_ci(curs::AbstractCursor) = error(lazy""" -missing `$AbstractCursor` API: -`$(typeof(curs))` is required to implement the `$get_ci(curs::$(typeof(curs))) -> CodeInstance` interface. -""") -get_ci(curs::CthulhuCursor) = curs.ci - -update_cursor(curs::AbstractCursor, ::CodeInstance) = error(lazy""" -missing `$AbstractCursor` API: -`$(typeof(curs))` is required to implement the `$update_cursor(curs::$(typeof(curs)), mi::MethodInstance) -> $(typeof(curs))` interface. -""") -update_cursor(curs::CthulhuCursor, ci::CodeInstance) = CthulhuCursor(ci) - -# TODO: This interface is incomplete, should probably also take a current cursor, -# or maybe be `CallSite based` -can_descend(interp::AbstractInterpreter, @nospecialize(key), optimize::Bool) = error(lazy""" -missing `$AbstractInterpreter` API: -`$(typeof(interp))` is required to implement the `$can_descend(interp::$(typeof(interp)), @nospecialize(key), optimize::Bool) -> Bool` interface. -""") -can_descend(interp::CthulhuInterpreter, @nospecialize(key), optimize::Bool) = - haskey(optimize ? key isa CodeInstance : interp.unopt, key) - -navigate(curs::AbstractCursor, callsite::Callsite) = error(lazy""" -missing `$AbstractCursor` API: -`$(typeof(curs))` is required to implement the `$navigate(curs::$(typeof(curs)), callsite::Callsite) -> AbstractCursor` interface. -""") -navigate(curs::CthulhuCursor, callsite::Callsite) = CthulhuCursor(get_ci(callsite)) - -get_pc_remarks(::AbstractInterpreter, ::InferenceKey) = nothing -get_pc_remarks(interp::CthulhuInterpreter, key::InferenceKey) = get(interp.remarks, key, nothing) - -get_pc_effects(::AbstractInterpreter, ::InferenceKey) = nothing -get_pc_effects(interp::CthulhuInterpreter, key::InferenceKey) = get(interp.effects, key, nothing) - -get_pc_exct(::AbstractInterpreter, ::InferenceKey) = nothing -get_pc_exct(interp::CthulhuInterpreter, key::InferenceKey) = get(interp.exception_types, key, nothing) - -# This method is optional, but should be implemented if there is -# a sensible default cursor for a MethodInstance -AbstractCursor(interp::AbstractInterpreter, ci::CodeInstance) = CthulhuCursor(ci) - -mutable struct CustomToggle - onoff::Bool - key::UInt32 - toggle::Symbol - description::String - callback_on - callback_off - function CustomToggle(onoff::Bool, key, description, - @nospecialize(callback_on), @nospecialize(callback_off)) - key = convert(UInt32, key) - desc = convert(String, description) - toggle = Symbol(desc) - if haskey(TOGGLES, key) - error(lazy"invalid Cthulhu API: key `$key` is already used.") - elseif toggle in values(TOGGLES) - error(lazy"invalid Cthulhu API: toggle `$toggle` is already used.") +find_method_instance(provider::AbstractProvider, @nospecialize(f), world::UInt = Base.tls_world_age()) = + find_method_instance(provider, f, Base.default_tt(f), world) + +function find_method_instance(provider::AbstractProvider, @nospecialize(f), @nospecialize(argtypes), world::UInt = Base.tls_world_age()) + # TODO: handle opaque closures + tt = Base.signature_type(f, argtypes) + return find_method_instance(provider, tt, world) +end + +function find_method_instance(provider::AbstractProvider, @nospecialize(tt::Type{<:Tuple}), world::UInt) + interp = get_abstract_interpreter(provider) + interp !== nothing && return find_method_instance(provider, interp, tt, world) + error(lazy"Not implemented for $provider") +end + +function generate_code_instance(provider::AbstractProvider, mi::MethodInstance) + interp = get_abstract_interpreter(provider) + interp !== nothing && return generate_code_instance(provider, interp, mi) + error(lazy"Not implemented for $provider") +end + +get_pc_remarks(provider::AbstractProvider, ci::CodeInstance) = + @warn "Remarks could not be retrieved for $ci with provider type $(typeof(provider))" +get_pc_effects(provider::AbstractProvider, ci::CodeInstance) = + @warn "Effects could not be retrieved for $ci with provider type $(typeof(provider))" +get_pc_excts(provider::AbstractProvider, ci::CodeInstance) = + @warn "Exception types could not be retrieved for $ci with provider type $(typeof(provider))" + +function find_caller_of(provider::AbstractProvider, callee::Union{MethodInstance,Type}, mi::MethodInstance; allow_unspecialized::Bool=false) + interp = get_abstract_interpreter(provider) + interp !== nothing && return find_caller_of(provider, interp, callee, mi, allow_unspecialized) + error(lazy"Not implemented for $provider") +end + +function get_inlining_costs(provider::AbstractProvider, mi::MethodInstance, src::Union{CodeInfo, IRCode}) + interp = get_abstract_interpreter(provider) + interp !== nothing && return get_inlining_costs(provider, interp, mi, src) + return nothing +end + +function show_parameters(io::IO, provider::AbstractProvider) + interp = get_abstract_interpreter(provider) + interp !== nothing && return show_parameters(io, provider, interp) + @warn "Not implemented for provider type $(typeof(provider))" +end + +function get_override(provider::AbstractProvider, @nospecialize(info)) + isa(info, ConstPropCallInfo) && return info.result + isa(info, SemiConcreteCallInfo) && return info + isa(info, OCCallInfo) && return get_override(provider, info.ci) + return nothing +end + +struct LookupResult + ir::Union{IRCode, Nothing} # used over `src` for callsite detection and printing + src::Union{CodeInfo, Nothing} # may be required (e.g. for LLVM and native module dumps) + rt + exct + infos::Vector{Any} + slottypes::Vector{Any} + effects::Effects + optimized::Bool + function LookupResult(ir, src, @nospecialize(rt), @nospecialize(exct), + infos, slottypes, effects, optimized) + ninfos = length(infos) + if src === nothing && ir === nothing + throw(ArgumentError("At least one of `src` or `ir` must have a value")) end - return new(onoff, key, toggle, desc, callback_on, callback_off) + if isa(ir, IRCode) && ninfos ≠ length(ir.stmts) + throw(ArgumentError("`ir` and `infos` are inconsistent with $(length(ir.stmts)) IR statements and $ninfos call infos")) + end + if isa(src, CodeInfo) + if !isa(src.ssavaluetypes, Vector{Any}) + throw(ArgumentError("`src.ssavaluetypes::$(typeof(src.ssavaluetypes))` must be a Vector{Any}")) + end + if !isa(ir, IRCode) && ninfos ≠ length(src.code) + throw(ArgumentError("`src` and `infos` are inconsistent with $(length(src.code)) code statements and $ninfos call infos")) + end + end + return new(ir, src, rt, exct, infos, slottypes, effects, optimized) end end -custom_toggles(interp::AbstractInterpreter) = CustomToggle[] + +function LookupResult(provider::AbstractProvider, src, optimize::Bool) + interp = get_abstract_interpreter(provider) + interp !== nothing && return LookupResult(provider, interp, src, optimize) + error(lazy""" + missing `$AbstractProvider` API: + `$(typeof(provider))` is required to implement the `$LookupResult(provider::$(typeof(provider)), src, optimize::Bool)` interface. + """) +end diff --git a/src/interpreter.jl b/src/interpreter.jl index 4e3c77df..e1c69c3c 100644 --- a/src/interpreter.jl +++ b/src/interpreter.jl @@ -1,28 +1,3 @@ -struct InferredSource - src::CodeInfo - stmt_info::Vector{CCCallInfo} - effects::Effects - rt::Any - exct::Any - InferredSource(src::CodeInfo, stmt_info::Vector{CCCallInfo}, effects, @nospecialize(rt), - @nospecialize(exct)) = - new(src, stmt_info, effects, rt, exct) -end - -struct OptimizedSource - ir::IRCode - src::CodeInfo - isinlineable::Bool - effects::Effects -end - -const InferenceKey = Union{CodeInstance,InferenceResult} # TODO make this `CodeInstance` fully -const InferenceDict{InferenceValue} = IdDict{InferenceKey, InferenceValue} -const PC2Remarks = Vector{Pair{Int, String}} -const PC2CallMeta = Dict{Int, CallMeta} -const PC2Effects = Dict{Int, Effects} -const PC2Excts = Dict{Int, Any} - mutable struct CthulhuCacheToken end struct CthulhuInterpreter <: AbstractInterpreter @@ -46,7 +21,7 @@ function CthulhuInterpreter(interp::AbstractInterpreter=NativeInterpreter()) InferenceDict{PC2Excts}()) end -Base.show(io::IO, interp::CthulhuInterpreter) = print(io, typeof(interp), "(...)") +Base.show(io::IO, interp::CthulhuInterpreter) = print(io, typeof(interp), '(', interp.native, ')') CC.InferenceParams(interp::CthulhuInterpreter) = InferenceParams(interp.native) CC.OptimizationParams(interp::CthulhuInterpreter) = @@ -65,6 +40,27 @@ CC.method_table(interp::CthulhuInterpreter) = CC.method_table(interp.native) # necessary for utilizing the default `CodeInstance` constructor, define the overload here. CC.cache_owner(interp::CthulhuInterpreter) = interp.cache_token +function OptimizedSource(provider::AbstractProvider, interp::CthulhuInterpreter, override::InferenceResult) + isa(override.src, OptimizedSource) || error("couldn't find the source") + return override.src +end + +function OptimizedSource(provider::AbstractProvider, interp::CthulhuInterpreter, ci::CodeInstance) + opt = ci.inferred + isa(opt, OptimizedSource) && return opt + @eval Main begin + interp = $interp + ci = $ci + opt = $opt + end + error("couldn't find the source; you may inspect `Main.interp|ci|opt`") +end + +function InferredSource(provider::AbstractProvider, interp::CthulhuInterpreter, src::Union{CodeInstance,InferenceResult}) + unopt = interp.unopt[src] + return unopt +end + function get_inference_key(state::InferenceState) result = state.result CC.is_constproped(state) && return result # TODO result.ci_as_edge? @@ -119,7 +115,7 @@ function InferredSource(state::InferenceState) exct = state.result.exc_result return InferredSource( unoptsrc, - copy(state.stmt_info), + collect(Any, state.stmt_info), state.ipo_effects, state.result.result, exct) @@ -197,7 +193,8 @@ function create_cthulhu_source(result::InferenceResult, effects::Effects) # get the (theoretically) same effect as the jl_compress_ir -> jl_uncompress_ir -> inflate_ir round-trip ir = CC.compact!(CC.cfg_simplify!(CC.copy(opt.ir::IRCode))) end - return OptimizedSource(ir, opt.src, opt.src.inlineable, effects) + src = CC.ir_to_codeinf!(opt) + return OptimizedSource(ir, src, src.inlineable, effects) end function set_cthulhu_source!(result::InferenceResult) diff --git a/src/preferences.jl b/src/preferences.jl index b922e11a..114b9835 100644 --- a/src/preferences.jl +++ b/src/preferences.jl @@ -5,7 +5,9 @@ save_config!(config::CthulhuConfig=CONFIG) Save a Cthulhu.jl configuration `config` (by default, `Cthulhu.CONFIG`) to your `LocalPreferences.toml` file using Preferences.jl. -The saved preferences will be automatically loaded next time you `using Cthulhu` +The saved preferences will be automatically loaded next time you `using Cthulhu`. + +See also: [`set_config`](@ref), [`set_config!`](@ref) ## Examples ```julia @@ -30,29 +32,53 @@ function save_config!(config::CthulhuConfig=CONFIG) "optimize" => config.optimize, "iswarn" => config.iswarn, "remarks" => config.remarks, - "with_effects" => config.with_effects, - "inline_cost" => config.inline_cost, + "effects" => config.effects, + "inlining_costs" => config.inlining_costs, "type_annotations" => config.type_annotations, - "annotate_source" => config.annotate_source, + "view" => String(config.view), "inlay_types_vscode" => config.inlay_types_vscode, "diagnostics_vscode" => config.diagnostics_vscode, "jump_always" => config.jump_always) end -function read_config!(config::CthulhuConfig) - config.enable_highlighter = load_preference(Cthulhu, "enable_highlighter", config.enable_highlighter) - config.highlighter = Cmd(load_preference(Cthulhu, "highlighter", config.highlighter)) - config.asm_syntax = Symbol(load_preference(Cthulhu, "asm_syntax", config.asm_syntax)) - config.pretty_ast = load_preference(Cthulhu, "pretty_ast", config.pretty_ast) - config.debuginfo = Symbol(load_preference(Cthulhu, "debuginfo", config.debuginfo)) - config.optimize = load_preference(Cthulhu, "optimize", config.optimize) - config.iswarn = load_preference(Cthulhu, "iswarn", config.iswarn) - config.remarks = load_preference(Cthulhu, "remarks", config.remarks) - config.with_effects = load_preference(Cthulhu, "with_effects", config.with_effects) - config.inline_cost = load_preference(Cthulhu, "inline_cost", config.inline_cost) - config.type_annotations = load_preference(Cthulhu, "type_annotations", config.type_annotations) - config.annotate_source = load_preference(Cthulhu, "annotate_source", config.annotate_source) - config.inlay_types_vscode = load_preference(Cthulhu, "inlay_types_vscode", config.inlay_types_vscode) - config.diagnostics_vscode = load_preference(Cthulhu, "diagnostics_vscode", config.diagnostics_vscode) - config.jump_always = load_preference(Cthulhu, "jump_always", config.jump_always) +function read_config!() + global CONFIG + @reset CONFIG.enable_highlighter = load_preference(Cthulhu, "enable_highlighter", CONFIG.enable_highlighter) + @reset CONFIG.highlighter = Cmd(load_preference(Cthulhu, "highlighter", CONFIG.highlighter)) + @reset CONFIG.asm_syntax = Symbol(load_preference(Cthulhu, "asm_syntax", CONFIG.asm_syntax)) + @reset CONFIG.pretty_ast = load_preference(Cthulhu, "pretty_ast", CONFIG.pretty_ast) + @reset CONFIG.debuginfo = Symbol(load_preference(Cthulhu, "debuginfo", CONFIG.debuginfo)) + @reset CONFIG.optimize = load_preference(Cthulhu, "optimize", CONFIG.optimize) + @reset CONFIG.iswarn = load_preference(Cthulhu, "iswarn", CONFIG.iswarn) + @reset CONFIG.remarks = load_preference(Cthulhu, "remarks", CONFIG.remarks) + @reset CONFIG.effects = load_preference(Cthulhu, "effects", CONFIG.effects) + @reset CONFIG.inlining_costs = load_preference(Cthulhu, "inlining_costs", CONFIG.inlining_costs) + @reset CONFIG.type_annotations = load_preference(Cthulhu, "type_annotations", CONFIG.type_annotations) + @reset CONFIG.view = Symbol(load_preference(Cthulhu, "view", CONFIG.view)) + @reset CONFIG.inlay_types_vscode = load_preference(Cthulhu, "inlay_types_vscode", CONFIG.inlay_types_vscode) + @reset CONFIG.diagnostics_vscode = load_preference(Cthulhu, "diagnostics_vscode", CONFIG.diagnostics_vscode) + @reset CONFIG.jump_always = load_preference(Cthulhu, "jump_always", CONFIG.jump_always) + return CONFIG +end + +""" + set_config(config::CthulhuConfig = CONFIG; parameters...) + set_config(config::CthulhuConfig, parameters::NamedTuple) + +Create a new `CthulhuConfig` from the parameters provided as keyword arguments, +with all other parameters identical to those of `config`. +""" +function set_config end + +set_config(config::CthulhuConfig = CONFIG; parameters...) = set_config(config, NamedTuple(parameters)) +set_config(config::CthulhuConfig, parameters::NamedTuple) = setproperties(config, parameters) + +""" + set_config!(; kwargs...) + +Create a new `CthulhuConfig` with [`set_config`](@ref), then update the binding `Cthulhu.CONFIG` +to now refer to that object. +""" +function set_config!(; kwargs...) + global CONFIG = set_config(CONFIG; kwargs...) end diff --git a/src/provider.jl b/src/provider.jl new file mode 100644 index 00000000..a847add2 --- /dev/null +++ b/src/provider.jl @@ -0,0 +1,11 @@ +struct DefaultProvider <: AbstractProvider + interp::CthulhuInterpreter +end +DefaultProvider(interp::AbstractInterpreter = NativeInterpreter()) = DefaultProvider(CthulhuInterpreter(interp)) + +get_abstract_interpreter(provider::DefaultProvider) = provider.interp +AbstractProvider(interp::CthulhuInterpreter) = DefaultProvider(interp) + +get_pc_remarks(provider::DefaultProvider, ci::CodeInstance) = get(provider.interp.remarks, ci, nothing) +get_pc_effects(provider::DefaultProvider, ci::CodeInstance) = get(provider.interp.effects, ci, nothing) +get_pc_excts(provider::DefaultProvider, ci::CodeInstance) = get(provider.interp.exception_types, ci, nothing) diff --git a/src/reflection.jl b/src/reflection.jl index 3356368a..014f30e8 100644 --- a/src/reflection.jl +++ b/src/reflection.jl @@ -6,32 +6,30 @@ using Base.Meta using Base: may_invoke_generator transform(::Val, callsite) = callsite -function transform(::Val{:CuFunction}, interp, callsite, callexpr, CI, mi, slottypes; world=get_world_counter()) +function transform(::Val{:CuFunction}, provider, callsite, callexpr, src, mi, slottypes; world=get_world_counter()) sptypes = sptypes_from_meth_instance(mi) - tt = argextype(callexpr.args[4], CI, sptypes, slottypes) - ft = argextype(callexpr.args[3], CI, sptypes, slottypes) + tt = argextype(callexpr.args[4], src, sptypes, slottypes) + ft = argextype(callexpr.args[3], src, sptypes, slottypes) isa(tt, Const) || return callsite sig = Tuple{widenconst(ft), tt.val.parameters...} - return Callsite(callsite.id, CuCallInfo(callinfo(interp, sig, Nothing; world)), callsite.head) + return Callsite(callsite.id, CuCallInfo(callinfo(provider, sig, Nothing; world)), callsite.head) end -function find_callsites(interp::AbstractInterpreter, CI::Union{CodeInfo,IRCode}, - stmt_infos::Union{Vector{CCCallInfo}, Nothing}, ci::CodeInstance, - slottypes::Vector{Any}, optimize::Bool=true, annotate_source::Bool=false, - pc2excts::Union{Nothing,PC2Excts}=nothing) - mi = ci.def +function find_callsites(provider::AbstractProvider, result::LookupResult, ci::CodeInstance, annotate_source::Bool=false, pc2excts::Union{Nothing,PC2Excts}=nothing) + mi = get_mi(ci) sptypes = sptypes_from_meth_instance(mi) callsites, sourcenodes = Callsite[], Union{TypedSyntax.MaybeTypedSyntaxNode,Callsite}[] - stmts = isa(CI, IRCode) ? CI.stmts.stmt : CI.code + src = something(result.ir, result.src)::Union{IRCode, CodeInfo} + stmts = isa(src, IRCode) ? src.stmts.stmt : src.code nstmts = length(stmts) - _, mappings = annotate_source ? get_typed_sourcetext(mi, CI, nothing; warn=false) : (nothing, nothing) + _, mappings = annotate_source ? get_typed_sourcetext(mi, src, nothing; warn=false) : (nothing, nothing) for id = 1:nstmts stmt = stmts[id] isa(stmt, Expr) || continue callsite = nothing - if stmt_infos !== nothing && is_call_expr(stmt, optimize) - info = stmt_infos[id] + if is_call_expr(stmt, result.optimized) + info = result.infos[id] if info !== nothing if isa(info, CC.UnionSplitApplyCallInfo) info = something(unpack_cthulhuinfo_from_unionsplit(info), info) @@ -41,27 +39,27 @@ function find_callsites(interp::AbstractInterpreter, CI::Union{CodeInfo,IRCode}, (; info, rt, exct, effects) = info.meta @assert !isa(info, CthulhuCallInfo) else - rt = ignorelimited(argextype(SSAValue(id), CI, sptypes, slottypes)) + rt = ignorelimited(argextype(SSAValue(id), src, sptypes, result.slottypes)) exct = isnothing(pc2excts) ? nothing : get(pc2excts, id, nothing) effects = nothing end # in unoptimized IR, there may be `slot = rhs` expressions, which `argextype` doesn't handle # so extract rhs for such an case local args = stmt.args - if !optimize + if !result.optimized args = (ignorelhs(stmt)::Expr).args end argtypes = Vector{Any}(undef, length(args)) - ft = ignorelimited(argextype(args[1], CI, sptypes, slottypes)) + ft = ignorelimited(argextype(args[1], src, sptypes, result.slottypes)) f = CC.singleton_type(ft) f === Core.Intrinsics.llvmcall && continue f === Core.Intrinsics.cglobal && continue argtypes[1] = ft for i = 2:length(args) - t = argextype(args[i], CI, sptypes, slottypes) + t = argextype(args[i], src, sptypes, result.slottypes) argtypes[i] = ignorelimited(t) end - callinfos = process_info(interp, info, argtypes, rt, optimize, exct, effects) + callinfos = process_info(provider, result, info, argtypes, rt, exct, effects) isempty(callinfos) && continue callsite = let if length(callinfos) == 1 @@ -79,7 +77,7 @@ function find_callsites(interp::AbstractInterpreter, CI::Union{CodeInfo,IRCode}, (stmt isa Expr) || continue (; head, args) = stmt if head === :invoke - rt = argextype(SSAValue(id), CI, sptypes, slottypes) + rt = argextype(SSAValue(id), src, sptypes, result.slottypes) arg1 = args[1]::CodeInstance effects = get_effects(arg1) # TODO callsite = Callsite(id, EdgeCallInfo(arg1, rt, effects), head) @@ -87,13 +85,14 @@ function find_callsites(interp::AbstractInterpreter, CI::Union{CodeInfo,IRCode}, # special handling of jl_new_task length(args) > 0 || continue arg1 = args[1] + isexpr(arg1, :tuple) && (arg1 = arg1.args[1]) if arg1 isa QuoteNode cfunc = arg1.value if cfunc === :jl_new_task func = args[6] - ftype = widenconst(argextype(func, CI, sptypes, slottypes)) + ftype = widenconst(argextype(func, src, sptypes, result.slottypes)) sig = Tuple{ftype} - callsite = Callsite(id, TaskCallInfo(callinfo(interp, sig, nothing; world=get_inference_world(interp))), head) + callsite = Callsite(id, TaskCallInfo(callinfo(provider, sig, nothing; world=get_inference_world(provider))), head) end end end @@ -105,7 +104,7 @@ function find_callsites(interp::AbstractInterpreter, CI::Union{CodeInfo,IRCode}, ci = get_ci(info) meth = ci.def.def if isa(meth, Method) && nameof(meth.module) === :CUDAnative && meth.name === :cufunction - callsite = transform(Val(:CuFunction), interp, callsite, c, CI, ci.def, slottypes; world=get_inference_world(interp)) + callsite = transform(Val(:CuFunction), provider, callsite, c, src, ci.def, result.slottypes; world=get_inference_world(provider)) end end @@ -123,8 +122,8 @@ function find_callsites(interp::AbstractInterpreter, CI::Union{CodeInfo,IRCode}, return callsites, sourcenodes end -function process_const_info(interp::AbstractInterpreter, @nospecialize(thisinfo), - argtypes::ArgTypes, @nospecialize(rt), @nospecialize(result), optimize::Bool, +function process_const_info(provider::AbstractProvider, ::LookupResult, @nospecialize(thisinfo), + argtypes::ArgTypes, @nospecialize(rt), @nospecialize(result), @nospecialize(exct)) if isnothing(result) return thisinfo @@ -154,10 +153,15 @@ function process_const_info(interp::AbstractInterpreter, @nospecialize(thisinfo) end end -function process_info(interp::AbstractInterpreter, @nospecialize(info::CCCallInfo), - argtypes::ArgTypes, @nospecialize(rt), optimize::Bool, +function process_info(provider::AbstractProvider, result::LookupResult, @nospecialize(info::CCCallInfo), + argtypes::ArgTypes, @nospecialize(rt), @nospecialize(exct), effects::Union{Effects, Nothing}) - process_recursive(@nospecialize(newinfo)) = process_info(interp, newinfo, argtypes, rt, optimize, exct, effects) + process_recursive(@nospecialize(newinfo)) = process_info(provider, result, newinfo, argtypes, rt, exct, effects) + + if isa(info, CC.ModifyOpInfo) + # Just ignore it. + info = info.info + end if isa(info, MethodResultPure) if isa(info.info, CC.ReturnTypeCallInfo) @@ -188,14 +192,14 @@ function process_info(interp::AbstractInterpreter, @nospecialize(info::CCCallInf infos = process_recursive(info.call) @assert length(infos) == length(info.results) return CallInfo[let - process_const_info(interp, infos[i], argtypes, rt, result, optimize, exct) - end for (i, result) in enumerate(info.results)] + process_const_info(provider, result, infos[i], argtypes, rt, value, exct) + end for (i, value) in enumerate(info.results)] elseif isa(info, CC.InvokeCallInfo) edge = info.edge if edge !== nothing effects = @something(effects, get_effects(edge)) thisinfo = EdgeCallInfo(edge, rt, effects) - innerinfo = process_const_info(interp, thisinfo, argtypes, rt, info.result, optimize, exct) + innerinfo = process_const_info(provider, result, thisinfo, argtypes, rt, info.result, exct) else innerinfo = RTCallInfo(unwrapconst(argtypes[1]), argtypes[2:end], rt, exct) end @@ -206,7 +210,7 @@ function process_info(interp::AbstractInterpreter, @nospecialize(info::CCCallInf if edge !== nothing effects = @something(effects, get_effects(edge)) thisinfo = EdgeCallInfo(edge, rt, effects) - innerinfo = process_const_info(interp, thisinfo, argtypes, rt, info.result, optimize, exct) + innerinfo = process_const_info(provider, result, thisinfo, argtypes, rt, info.result, exct) else innerinfo = RTCallInfo(unwrapconst(argtypes[1]), argtypes[2:end], rt, exct) end @@ -222,7 +226,7 @@ function process_info(interp::AbstractInterpreter, @nospecialize(info::CCCallInf return CallInfo[] elseif isa(info, CC.ReturnTypeCallInfo) newargtypes = argtypes[2:end] - callinfos = process_info(interp, info.info, newargtypes, unwrapType(widenconst(rt)), optimize, exct, effects) + callinfos = process_info(provider, result, info.info, newargtypes, unwrapType(widenconst(rt)), exct, effects) if length(callinfos) == 1 vmi = only(callinfos) else @@ -240,13 +244,13 @@ function process_info(interp::AbstractInterpreter, @nospecialize(info::CCCallInf return CallInfo[] else @eval Main begin - interp = $interp + provider = $provider + result = $result info = $info argtypes = $argtypes rt = $rt - optimize = $optimize end - error("inspect `Main.interp|info|argtypes|rt|optimize`") + error("unhandled `Compiler.CallInfo` of type $(typeof(info)); you may inspect `Main.provider|result|info|argtypes|rt`") end end @@ -272,7 +276,7 @@ function callinfo(interp, sig, rt, max_methods=-1; world=get_world_counter()) else mi = specialize_method(meth, atypes, sparams) if mi !== nothing - edge = do_typeinf!(interp, mi) + edge = generate_code_instance(interp, mi) push!(callinfos, EdgeCallInfo(edge, rt, Effects())) else push!(callinfos, FailedCallInfo(sig, rt)) @@ -285,42 +289,6 @@ function callinfo(interp, sig, rt, max_methods=-1; world=get_world_counter()) return MultiCallInfo(sig, rt, callinfos) end -function find_caller_of(interp::AbstractInterpreter, callee::Union{MethodInstance,Type}, caller::MethodInstance; allow_unspecialized::Bool=false) - interp′ = CthulhuInterpreter(interp) - codeinst = do_typeinf!(interp′, caller) - @assert codeinst.def === caller - locs = Tuple{Core.LineInfoNode,Int}[] - for optimize in (true, false) - (; src, rt, infos, slottypes) = lookup(interp′, codeinst, optimize) - callsites, _ = find_callsites(interp′, src, infos, codeinst, slottypes, optimize) - callsites = allow_unspecialized ? filter(cs->maybe_callsite(cs, callee), callsites) : - filter(cs->is_callsite(cs, callee), callsites) - foreach(cs -> add_sourceline!(locs, src, cs.id, caller), callsites) - end - # Consolidate by method, but preserve the order - prlookup = Dict{Tuple{Symbol,Symbol},Int}() - ulocs = Pair{Tuple{Symbol,Symbol,Int},Vector{Int}}[] - if !isempty(locs) - for (loc, depth) in locs - locname = loc.method - if isa(locname, MethodInstance) - locname = locname.def.name - end - idx = get(prlookup, (locname, loc.file), nothing) - if idx === nothing - push!(ulocs, (locname, loc.file, depth) => Int[]) - prlookup[(locname, loc.file)] = idx = length(ulocs) - end - lines = ulocs[idx][2] - line = loc.line - if line ∉ lines - push!(lines, line) - end - end - end - return ulocs -end - function add_sourceline!(locs::Vector{Tuple{Core.LineInfoNode,Int}}, src::Union{CodeInfo,IRCode}, stmtidx::Int, caller::MethodInstance) stack = IRShow.buildLineInfoNode(src.debuginfo, caller, stmtidx) for (i, di) in enumerate(stack) diff --git a/src/state.jl b/src/state.jl new file mode 100644 index 00000000..1dcd2f5d --- /dev/null +++ b/src/state.jl @@ -0,0 +1,258 @@ +mutable struct CthulhuState + terminal::AbstractTerminal + provider::AbstractProvider + config::CthulhuConfig + mi::Union{Nothing, MethodInstance} + ci::Union{Nothing, CodeInstance} + override::Any + display_code::Bool +end +function CthulhuState(provider::AbstractProvider; terminal = default_terminal(), config = CONFIG, + ci = nothing, mi = ci === nothing ? nothing : get_mi(ci), override = nothing) + return CthulhuState(terminal, provider, config, mi, ci, override, true) +end + +function default_terminal() + term_env = get(ENV, "TERM", @static Sys.iswindows() ? "" : "dumb") + term = REPL.Terminals.TTYTerminal(term_env, stdin, stdout, stderr) + return term +end + +view_function(state::CthulhuState) = view_function(state.provider, state.config.view) +function view_function(provider::AbstractProvider, view::Symbol) + view === :source && return cthulhu_source + view === :typed && return cthulhu_typed + view === :ast && return cthulhu_ast + view === :llvm && return cthulhu_llvm + view === :native && return cthulhu_native + throw_view_is_not_supported(provider, view) +end + +throw_view_is_not_supported(provider, view) = throw(ArgumentError("View `$view` is not supported for provider $(typeof(provider))")) + +mutable struct Command + f::Any + key::UInt32 + name::Symbol + description::String + category::Symbol + function Command(@nospecialize(f), key, name, description, category) + key = convert(UInt32, key) + description = convert(String, description) + return new(f, key, name, description, category) + end +end + +function default_menu_commands(provider::AbstractProvider) + commands = Command[ + set_option('w', :iswarn, :toggles, "warn"), + set_option('h', :hide_type_stable, :toggles, "hide type-stable statements"), + set_option('o', :optimize, :toggles), + set_debuginfo('d'), + set_option('r', :remarks, :toggles), + set_option('e', :effects, :toggles, "effects"), + set_option('x', :exception_types, :toggles), + set_option('i', :inlining_costs, :toggles, "inlining costs"), + set_option('t', :type_annotations, :toggles), + set_option('s', :enable_highlighter, :toggles, "syntax highlight for Source/LLVM/Native"), + set_option('v', :inlay_types_vscode, :toggles, "vscode: inlay types"), + set_option('V', :diagnostics_vscode, :toggles, "vscode: diagnostics"), + set_option('j', :jump_always, :toggles, "jump to source always"; redisplay = false), + set_view('S', :source, :show), + set_view('A', :ast, :show, "AST"), + set_view('T', :typed, :show), + set_view('L', :llvm, :show, "LLVM"), + set_view('N', :native, :show), + perform_action(_ -> nothing, 'q', :quit, :actions), # built-in from TerminalMenus + perform_action(_ -> nothing, '⟵', :ascend, :actions), + perform_action(bookmark_method, 'b', :bookmark, :actions), + perform_action(edit_source_code, 'E', :edit, :actions, "Edit source code"), + perform_action(revise_and_redisplay!, 'R', :revise, :actions, "Revise and redisplay"), + perform_action(dump_parameters, 'P', :dump_params, :actions, "dump params cache"), + ] + return commands +end + +function perform_action(f, key::Char, name::Symbol, category, description = string(name)) + return Command(f, key, name, description, category) +end + +function set_debuginfo(key::Char) + return Command(key, :debuginfo, "debuginfo", :toggles) do state + values = (:none, :compact, :source) + previous = state.config.debuginfo + i = findfirst(==(previous), values) + next = i === nothing ? :none : values[mod1(i + 1, 3)] + return set_option!(state, :debuginfo, next; redisplay = true) + end +end + +function set_option(key::Char, option::Symbol, category, description = string(option); redisplay = true) + return Command(state -> toggle_option!(state, option; redisplay), key, option, description, category) +end + +function set_view(key::Char, option::Symbol, category, description = string(option)) + return Command(state -> set_option!(state, :view, option; redisplay = state.config.view !== option), key, option, description, category) +end + +function toggle_option(state::CthulhuState, option::Symbol) + (; config) = state + hasproperty(config, option) || throw(ArgumentError("Unknown configuration option `$option`")) + previous = getproperty(config, option) + return !previous +end + +function toggle_option!(state::CthulhuState, option::Symbol; redisplay = false) + value = toggle_option(state, option) + set_option!(state, option, value; redisplay) +end + +function set_option!(state::CthulhuState, option::Symbol, value; redisplay = false) + (; config) = state + hasproperty(config, option) || throw(ArgumentError("Unknown configuration option `$option`")) + + if value === true + if option === :remarks && config.optimize || option === :optimize && config.remarks + @warn "Disable optimization to see the inference remarks." + end + option === :inlining_costs && !config.optimize && @warn "Enable optimization to see the inlining costs." + end + + if option === :enable_highlighter + value === true && @info "Using syntax highlighter $(CONFIG.highlighter)." + value === false && @info "Turned off syntax highlighter for Julia, LLVM and native code." + end + + if value === false + option === :optimize && config.inlining_costs && @warn "Enable optimization to see the inlining costs." + option === :inlay_types_vscode && TypedSyntax.clear_inlay_hints_vscode() + option === :diagnostics_vscode && TypedSyntax.clear_diagnostics_vscode() + end + + redisplay && (state.display_code = true) + state.config = set_config(config, NamedTuple((option => value,))) +end + +function edit_source_code(state::CthulhuState) + def = state.mi.def + isa(def, Method) || return @warn "Can't go to the source location because the definition is not a method." + edit(whereis(def)...) +end + +function revise_and_redisplay!(state::CthulhuState) + # Call Revise.revise() without introducing a dependency on Revise + id = Base.PkgId(UUID("295af30f-e4ad-537b-8983-00126c2a3abe"), "Revise") + mod = get(Base.loaded_modules, id, nothing) + mod === nothing && return @warn "Revise must be loaded to revise changes" + revise = getfield(mod, :revise)::Function + revise() + world = Base.get_world_counter() + state.mi = find_method_instance(state.provider, state.mi.specTypes, world) + state.ci = generate_code_instance(state.provider, state.mi) + state.display_code = true +end + +function bookmark_method(state::CthulhuState) + push!(BOOKMARKS, Bookmark(state.provider, state.ci; state.config)) + mod = resolve_module(state.provider) + @info "The current `descend` state was saved for later use. You may access it with `$mod.BOOKMARKS[end]`." + if nameof(mod) === :CthulhuCompilerExt + @info "You can get the `CthulhuCompilerExt` module with `Base.get_extension(Cthulhu, :CthulhuCompilerExt)`" + end +end + +function dump_parameters(state::CthulhuState) + io = state.terminal.out_stream::IO + show_parameters(io, state.provider) +end + +function split_by_category(commands::Vector{Command}) + categories = Pair{Symbol,Vector{Command}}[] + for toggle in commands + i = findfirst(==(toggle.category) ∘ first, categories) + if i === nothing + push!(categories, toggle.category => [toggle]) + else + _, list = categories[i] + push!(list, toggle) + end + end + return categories +end + +save_descend_state(state::CthulhuState) = (; state.mi, state.ci, state.override) + +function restore_descend_state!(state::CthulhuState, (; mi, ci, override)::NamedTuple) + state.mi = mi + state.ci = ci + state.override = override + return state +end + +# AbstractProvider interface + +menu_commands(provider::AbstractProvider) = default_menu_commands(provider) + +is_command_enabled(provider::AbstractProvider, state::CthulhuState, command::Command) = + is_default_command_enabled(provider, state, command) + +function is_default_command_enabled(provider::AbstractProvider, state::CthulhuState, command::Command) + command.name === :inlay_types_vscode && return TypedSyntax.inlay_hints_available_vscode() + command.name === :diagnostics_vscode && return TypedSyntax.diagnostics_available_vscode() + if state.config.view === :source + disabled_commands = (:optimize, :debuginfo, :remarks, :effects, :exception_types, :inlining_costs) + return !in(command.name, disabled_commands) + end + return true +end + +function show_command(io::IO, provider::AbstractProvider, state::CthulhuState, command::Command) + (; description) = command + key = Char(command.key) + style = style_for_command_key(provider, state, command) + i = findfirst(x -> lowercase(x) == lowercase(key), description) + if i === nothing + i = 1 + printstyled(io, key; style...) + print(io, ' ', description) + return + end + print(io, description[1:prevind(description, i)]) + print(io, '[') + printstyled(io, key; style...) + print(io, ']') + print(io, description[nextind(description, i):end]) +end + +style_for_command_key(provider::AbstractProvider, state::CthulhuState, command::Command) = + default_style_for_command_key(provider, state, command) + +function default_style_for_command_key(provider::AbstractProvider, state::CthulhuState, command::Command) + value = value_for_command(provider, state, command) + isa(value, Symbol) && command.name === :debuginfo && return debuginfo_style(value) + return default_style(value) +end + +value_for_command(provider::AbstractProvider, state::CthulhuState, command::Command) = + value_for_default_command(provider, state, command) + +function value_for_default_command(provider::AbstractProvider, state::CthulhuState, command::Command) + command.category === :show && return state.config.view === command.name + command.category !== :toggles && return nothing + !hasproperty(state.config, command.name) && return nothing + return getproperty(state.config, command.name) +end + +default_style(@nospecialize(value)) = NamedTuple() +default_style(value::Nothing) = (; color=:cyan, bold=false) +function default_style(value::Bool) + color = ifelse(value, :green, :red) + return (; color, bold = value) +end + +function debuginfo_style(value::Symbol) + i = findfirst(==(value), (:none, :compact, :source)) + i === nothing && return NamedTuple() + color = (:red, :light_black, :green)[i] + return (; color, bold = color === :green) +end diff --git a/src/testing.jl b/src/testing.jl index 0252cf61..fc0ac017 100644 --- a/src/testing.jl +++ b/src/testing.jl @@ -2,10 +2,12 @@ module Testing using REPL.Terminals: TTYTerminal, UnixTerminal using REPL: TerminalMenus -import Base: read, write -mutable struct FakeTerminal <: UnixTerminal - # Use pipes so we can easily do blocking reads. +""" +Terminal implementation that connects virtual input/output/error pipes +to be interacted with asynchronously with a program. +""" +mutable struct VirtualTerminal <: UnixTerminal input::Pipe output::Pipe error::Pipe @@ -13,7 +15,7 @@ mutable struct FakeTerminal <: UnixTerminal in_stream::IO out_stream::IO err_stream::IO - function FakeTerminal() + function VirtualTerminal(; context = [:color => get(stdout, :color, false)]) input = Pipe() output = Pipe() error = Pipe() @@ -21,23 +23,201 @@ mutable struct FakeTerminal <: UnixTerminal Base.link_pipe!(output, reader_supports_async=true, writer_supports_async=true) Base.link_pipe!(error, reader_supports_async=true, writer_supports_async=true) term_env = get(ENV, "TERM", @static Sys.iswindows() ? "" : "dumb") - tty = TTYTerminal(term_env, input.out, IOContext(output.in, :color => get(stdout, :color, false)), error.in) - terminal = new(input, output, error, tty, tty.in_stream, tty.out_stream, tty.err_stream) + in_stream = input.out + out_stream = IOContext(output.in, context...) + err_stream = error.in + tty = TTYTerminal(term_env, in_stream, out_stream, err_stream) + terminal = new(input, output, error, tty, in_stream, out_stream, err_stream) return finalizer(terminal) do x @async begin - close(x.input.in) - close(x.output.in) - close(x.error.in) + close(x.input) + close(x.output) + close(x.error) end end end end -Base.write(terminal::FakeTerminal, char::Char) = write(terminal.input, char) -Base.write(terminal::FakeTerminal, input::String) = write(terminal.input, input) -Base.read(terminal::FakeTerminal, args...; error = false) = read(ifelse(error, terminal.error, terminal.output), args...) -TerminalMenus.request(terminal::FakeTerminal, args...; kwargs...) = TerminalMenus.request(terminal.tty, args...; kwargs...) +Base.write(terminal::VirtualTerminal, key::Symbol) = write(terminal, KEYS[key]) +Base.write(terminal::VirtualTerminal, char::Char) = write(terminal.input, char) +Base.write(terminal::VirtualTerminal, input::String) = write(terminal.input, input) +Base.read(terminal::VirtualTerminal, args...; error = false) = read(ifelse(error, terminal.error, terminal.output), args...) +TerminalMenus.request(terminal::VirtualTerminal, args...; kwargs...) = TerminalMenus.request(terminal.tty, args...; kwargs...) -export FakeTerminal +""" +IO subtype that continuously reads another `IO` object in a non-blocking way +using an asynchronously-running task and puts the results into a byte buffer. + +The intended use case is to ensure writes to the wrapped `IO` are always read +to reliably avoid blocking future writes past a certain amount. +""" +struct AsyncIO <: IO + io::IO + buffer::Vector{UInt8} + lock::ReentrantLock + task::Task +end + +function AsyncIO(io::IO) + buffer = UInt8[] + lock = ReentrantLock() + task = @async while !eof(io) + available = readavailable(io) + while bytesavailable(io) > 0 + append!(available, readavailable(io)) + end + @lock lock append!(buffer, available) + end + return AsyncIO(io, buffer, lock, task) +end +AsyncIO(terminal::VirtualTerminal) = AsyncIO(terminal.output) + +function Base.peek(io::AsyncIO, ::Type{UInt8}) + while bytesavailable(io) < 1 yield() end + return @lock io.lock io.buffer[1] +end +function Base.read(io::AsyncIO, ::Type{UInt8}) + ref = Ref(0x00) + GC.@preserve ref begin + ptr = Base.unsafe_convert(Ptr{UInt8}, ref) + unsafe_read(io, ptr, 1) + end + return ref[] +end +Base.bytesavailable(io::AsyncIO) = @lock io.lock length(io.buffer) +Base.eof(io::AsyncIO) = istaskdone(io.task) && @lock io.lock isempty(io.buffer) + +function Base.readavailable(io::AsyncIO) + bytes = UInt8[] + @lock io.lock begin + append!(bytes, io.buffer) + empty!(io.buffer) + end + return bytes +end + +function Base.unsafe_read(io::AsyncIO, to::Ptr{UInt8}, nb::UInt) + written = 0 + while written < nb + bytesavailable(io) > 0 || (yield(); continue) + @lock io.lock begin + (; buffer) = io + n = min(length(buffer), nb) + GC.@preserve buffer begin + unsafe_copyto!(to, pointer(buffer), n) + end + splice!(buffer, 1:n) + written += n + end + end +end + +# We use the '↩' character as a delimiter for UI displays (this is +# specific to Cthulhu, the last character we emit is that one). +# Because we print it twice with Cthulhu (once for menu options, once +# at the end for callsite selection), we effectively use twice-'↩' as a delimiter. +cread1(io::AsyncIO) = readuntil(io, '↩'; keep=true) +cread(io::AsyncIO) = cread1(io) * cread1(io) +strip_ansi_escape_sequences(str) = replace(str, r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])" => "") +function read_next(io::AsyncIO) + displayed = cread(io) + text = strip_ansi_escape_sequences(displayed) + return (displayed, text) +end + +""" +A testing utility that correctly wraps a [`VirtualTerminal`](@ref) +with an asynchronous IO reader to avoid blocking while asynchronously +executing a task that interacts with the terminal. + +This harness: +- Protects the terminal's input from a blocking congestion (caused by waiting for a read on its output). +- Logs any task errors to `stderr` to avoid silent failures. + +In an ideal world, we wouldn't need a utility like that, but it so happens that +emulated asynchronous terminal interactions are tricky especially when we rely on +testing unstructured IO output to be semantically delimited into regions. +""" +mutable struct TestHarness + terminal::VirtualTerminal + io::AsyncIO + task::Task + function TestHarness(terminal::VirtualTerminal) + io = AsyncIO(terminal) + harness = new(terminal, io) + end +end + +macro display_errors(expr) + quote + try + $(esc(expr)) + catch err + bt = catch_backtrace() + # print all at once, to avoid interleaving output with other prints + output = sprint(Base.display_error, err, bt; context = :color => true) + println(output) + end + end +end + +function Base.run(harness::TestHarness, task::Task) + !isdefined(harness, :task) || error("A task was already executed with this harness") + harness.task = task +end + +macro run(terminal, ex) + quote + terminal = $(esc(terminal)) + harness = TestHarness(terminal) + task = @async @display_errors $(esc(ex)) + run(harness, task) + harness + end +end + +function read_next(harness::TestHarness) + istaskdone(harness.task) && error("The task is not running") + return read_next(harness.io) +end + +function skip_delimiter(harness::TestHarness) + cread1(harness.io) + return nothing +end + +function wait_for(task::Task, timeout = 10.0) + t0 = time() + @goto body + while time() - t0 < timeout + @label body + istaskfailed(task) && return wait(task) + istaskdone(task) && return true + yield() + end + return false +end + +end_terminal_session(harness::TestHarness) = + end_terminal_session(harness.terminal, harness.task, harness.io) + +function end_terminal_session(terminal::VirtualTerminal, task::Task, io::AsyncIO) + wait_for(task, 0.0) && @goto finished + write(terminal, 'q') + wait_for(task, 1.0) && @goto finished + write(terminal, 'q') + wait_for(task, 1.0) && @goto finished + @assert wait_for(task) + @label finished + finalize(terminal) + @assert wait_for(io.task, 1.0) + return istaskdone(task) && istaskdone(io.task) +end + +const KEYS = Dict(:up => "\e[A", + :down => "\e[B", + :enter => '\r') + +export VirtualTerminal, TestHarness, @run, read_next, end_terminal_session, skip_delimiter end # module Testing diff --git a/src/ui.jl b/src/ui.jl index e22165ac..566ffdb9 100644 --- a/src/ui.jl +++ b/src/ui.jl @@ -10,10 +10,11 @@ mutable struct CthulhuMenu <: TerminalMenus.ConfiguredMenu{TerminalMenus.Config} toggle::Union{Nothing, Symbol} sub_menu::Bool config::TerminalMenus.Config - custom_toggles::Vector{CustomToggle} + commands::Vector{Command} + state::CthulhuState end -function show_as_line(callsite::Callsite, with_effects::Bool, exception_type::Bool, optimize::Bool, iswarn::Bool) +function show_as_line(callsite::Callsite, effects::Bool, exception_types::Bool, optimize::Bool, iswarn::Bool) reduced_displaysize = displaysize(stdout)::Tuple{Int,Int} .- (0, 3) sprint() do io show(IOContext(io, @@ -21,28 +22,26 @@ function show_as_line(callsite::Callsite, with_effects::Bool, exception_type::Bo :displaysize => reduced_displaysize, :optimize => optimize, :iswarn => iswarn, - :color => iswarn | with_effects | exception_type, - :with_effects => with_effects, - :exception_type => exception_type), + :color => iswarn | effects | exception_types, + :effects => effects, + :exception_types => exception_types), callsite) end end """ CthulhuMenu(args...; pagesize::Int=10, sub_menu = false, kwargs...) - Set up the callsite menu with the given arguments. This is largely internal, but the main keywords to control the menu are: - `pagesize::Int` (default 10) Number of callsites to show at a time (without scrolling). - `sub_menu::Bool` (default false) if true, user can only pick a callsite, not change options. - Others are passed to [`REPL.TerminalMenus.Config`](https://docs.julialang.org/en/v1/stdlib/REPL/#REPL.TerminalMenus.Config). """ -function CthulhuMenu(callsites, with_effects::Bool, exception_type::Bool, +function CthulhuMenu(state::CthulhuState, callsites, effects::Bool, exception_types::Bool, optimize::Bool, iswarn::Bool, hide_type_stable::Bool, - custom_toggles::Vector{CustomToggle}; pagesize::Int=10, sub_menu = false, kwargs...) - options = build_options(callsites, with_effects, exception_type, optimize, iswarn, hide_type_stable) + commands::Vector{Command}; pagesize::Int=10, sub_menu = false, kwargs...) + options = build_options(callsites, effects, exception_types, optimize, iswarn, hide_type_stable) length(options) < 1 && error("CthulhuMenu must have at least one option") # if pagesize is -1, use automatic paging @@ -57,18 +56,18 @@ function CthulhuMenu(callsites, with_effects::Bool, exception_type::Bool, config = TerminalMenus.Config(; kwargs...) - return CthulhuMenu(options, pagesize, pageoffset, selected, nothing, sub_menu, config, custom_toggles) + return CthulhuMenu(options, pagesize, pageoffset, selected, nothing, sub_menu, config, commands, state) end -build_options(callsites::Vector{Callsite}, with_effects::Bool, exception_type::Bool, optimize::Bool, iswarn::Bool, ::Bool) = - vcat(map(callsite->show_as_line(callsite, with_effects, exception_type, optimize, iswarn), callsites), ["↩"]) -function build_options(callsites, with_effects::Bool, exception_type::Bool, optimize::Bool, iswarn::Bool, hide_type_stable::Bool) +build_options(callsites::Vector{Callsite}, effects::Bool, exception_types::Bool, optimize::Bool, iswarn::Bool, ::Bool) = + vcat(map(callsite->show_as_line(callsite, effects, exception_types, optimize, iswarn), callsites), ["↩"]) +function build_options(callsites, effects::Bool, exception_types::Bool, optimize::Bool, iswarn::Bool, hide_type_stable::Bool) reduced_displaysize::Int = (displaysize(stdout)::Tuple{Int,Int})[2] - 3 nd::Int = -1 shown_callsites = map(callsites) do node if isa(node, Callsite) - show_as_line(node, with_effects, exception_type, optimize, iswarn) + show_as_line(node, effects, exception_types, optimize, iswarn) else if nd == -1 nd = TypedSyntax.ndigits_linenumbers(node) @@ -105,119 +104,51 @@ function stringify(@nospecialize(f), context::IOContext) end const debugcolors = (:nothing, :light_black, :yellow) -function usage(@nospecialize(view_cmd), annotate_source, optimize, iswarn, hide_type_stable, - debuginfo, remarks, with_effects, exception_type, inline_cost, - type_annotations, highlight, inlay_types_vscode, diagnostics_vscode, - jump_always, custom_toggles::Vector{CustomToggle}) - colorize(active_option::Bool, c::Char) = stringify() do io - active_option ? printstyled(io, c; bold=true, color=:green) : printstyled(io, c; color=:red) - end - - colorize(s::AbstractString; color::Symbol = :cyan) = stringify() do io - printstyled(io, s; color) - end - - io = IOBuffer() - ioctx = IOContext(io, :color=>true) - - println(ioctx, - colorize("Select a call to descend into or ↩ to ascend. [q]uit. [b]ookmark."; color=:blue)) - print(ioctx, - colorize("Toggles"), ": [", - colorize(iswarn, 'w'), "]arn, [", - colorize(hide_type_stable, 'h'), "]ide type-stable statements, [", - colorize(type_annotations, 't'), "]ype annotations, [", - colorize(highlight, 's'), "]yntax highlight for Source/LLVM/Native, [", - colorize(jump_always, 'j'), "]ump to source always"), - if TypedSyntax.inlay_hints_available_vscode() - print(ioctx, ", [", - colorize(inlay_types_vscode, 'v'), "]scode: inlay types") - end - if TypedSyntax.diagnostics_available_vscode() - print(ioctx, ", [", - colorize(diagnostics_vscode, 'V'), "]scode: diagnostics") - end - if !annotate_source - print(ioctx, ", [", - colorize(optimize, 'o'), "]ptimize, [", - stringify() do io - printstyled(io, 'd'; color=debugcolors[Int(debuginfo)+1]) - end, "]ebuginfo, [", - colorize(remarks, 'r'), "]emarks, [", - colorize(with_effects, 'e'), "]ffects, ", - "e[", colorize(exception_type, 'x'), "]ception types, [", - colorize(inline_cost, 'i'), "]nlining costs") - end - for i = 1:length(custom_toggles) - ct = custom_toggles[i] - print(ioctx, ", [", colorize(ct.onoff, Char(ct.key)), ']', ct.description) - i ≠ length(custom_toggles) && print(ioctx, ", ") - end - print(ioctx, '.') - println(ioctx) - println(ioctx, - colorize("Show"), ": [", - colorize(annotate_source, 'S'), "]ource code, [", - colorize(view_cmd === cthulhu_ast, 'A'), "]ST, [", - colorize(!annotate_source && view_cmd === cthulhu_typed, 'T'), "]yped code, [", - colorize(view_cmd === cthulhu_llvm, 'L'), "]LVM IR, [", - colorize(view_cmd === cthulhu_native, 'N'), "]ative code") - print(ioctx, - colorize("Actions"), - ": [E]dit source code, [R]evise and redisplay") - if !annotate_source - print(ioctx, - "\n", - colorize("Advanced"), - ": dump [P]arams cache.") +function usage(provider::AbstractProvider, state::CthulhuState, commands::Vector{Command}) + buffer = IOBuffer() + io = IOContext(buffer, :color => true) + + printstyled(io, "\nSelect a call to descend into or ↩ to ascend.\n"; color = :blue) + categories = split_by_category(commands) + for (category, list) in categories + did_print = false + for command in list + is_command_enabled(provider, state, command) || continue + if !did_print + printstyled(io, '\n', uppercasefirst(string(category)); color=:cyan) + print(io, ": ") + did_print = true + else + print(io, ", ") + end + show_command(io, provider, state, command) + end + did_print && print(io, '.') end - return String(take!(io)) + println(io) + return String(take!(buffer)) end -const TOGGLES = Dict( - UInt32('w') => :warn, - UInt32('h') => :hide_type_stable, - UInt32('o') => :optimize, - UInt32('d') => :debuginfo, - UInt32('r') => :remarks, - UInt32('e') => :with_effects, - UInt32('x') => :exception_type, - UInt32('i') => :inline_cost, - UInt32('t') => :type_annotations, - UInt32('s') => :highlighter, - UInt32('S') => :source, - UInt32('A') => :ast, - UInt32('T') => :typed, - UInt32('L') => :llvm, - UInt32('N') => :native, - UInt32('P') => :dump_params, - UInt32('b') => :bookmark, - UInt32('R') => :revise, - UInt32('E') => :edit, - UInt32('v') => :inlay_types_vscode, - UInt32('V') => :diagnostics_vscode, - UInt32('j') => :jump_always -) - -function TerminalMenus.keypress(m::CthulhuMenu, key::UInt32) - m.sub_menu && return false - toggle = get(TOGGLES, key, nothing) - if !isnothing(toggle) - m.toggle = toggle - return true - end - local i = findfirst(ct->ct.key == key, m.custom_toggles) - isnothing(i) && return false - m.toggle = m.custom_toggles[i].toggle +function TerminalMenus.keypress(menu::CthulhuMenu, key::UInt32) + menu.sub_menu && return false + (; state) = menu + (; provider) = state + key === UInt32('\x7f' #= backspace =#) && (key = UInt32('⟵')) + i = findfirst(x -> x.key == key && is_command_enabled(provider, state, x), menu.commands) + i === nothing && return false + println(state.terminal.out_stream::IO) + command = menu.commands[i] + command.f(menu.state) + menu.toggle = command.name return true end function TerminalMenus.pick(menu::CthulhuMenu, cursor::Int) menu.selected = cursor - return true #break out of the menu + return true # break out of the menu end -function TerminalMenus.writeline(buf::IOBuffer, menu::CthulhuMenu, idx::Int, iscursor::Bool) +function TerminalMenus.writeline(buffer::IOBuffer, menu::CthulhuMenu, idx::Int, iscursor::Bool) line = replace(menu.options[idx], "\n" => "\\n") - print(buf, line) + print(buffer, line) end diff --git a/test/TestCodeViewSandbox.jl b/test/TestCodeViewSandbox.jl index a2dd16c8..e69de29b 100644 --- a/test/TestCodeViewSandbox.jl +++ b/test/TestCodeViewSandbox.jl @@ -1,10 +0,0 @@ -module TestCodeViewSandbox - -function testf_revise() - T = rand() > 0.5 ? Int64 : Float64 - sum(rand(T, 100)) -end - -export testf_revise - -end diff --git a/test/generate_irshow.jl b/test/generate_irshow.jl index 941ff7b0..01cc5535 100644 --- a/test/generate_irshow.jl +++ b/test/generate_irshow.jl @@ -12,27 +12,27 @@ function generate_test_cases(f, tt, fname=string(nameof(f))) tf = (true, false) for optimize in tf (; src, infos, codeinst, rt, exct, effects, slottypes) = cthulhu_info(f, tt; optimize); - for (debuginfo, iswarn, hide_type_stable, inline_cost, type_annotations) in Iterators.product( - instances(Cthulhu.DInfo.DebugInfo), tf, tf, tf, tf, + for (debuginfo, iswarn, hide_type_stable, inlining_costs, type_annotations) in Iterators.product( + (:none, :source, :compact), tf, tf, tf, tf, ) - !optimize && debuginfo === Cthulhu.DInfo.compact && continue - !optimize && inline_cost && continue + !optimize && debuginfo === :compact && continue + !optimize && inlining_costs && continue s = sprint(; context=:color=>true) do io Cthulhu.cthulhu_typed(io, debuginfo, src, rt, exct, effects, codeinst; - iswarn, hide_type_stable, inline_cost, type_annotations) + iswarn, hide_type_stable, inlining_costs, type_annotations) end s = strip_base_linenums(s) - bname = irshow_filename(fname, optimize, debuginfo, iswarn, hide_type_stable, inline_cost, type_annotations) + bname = irshow_filename(fname, optimize, debuginfo, iswarn, hide_type_stable, inlining_costs, type_annotations) fpath = normpath(@__DIR__, bname) write(fpath, s) - in(s, values(outputs)) && (@show((optimize, debuginfo, iswarn, hide_type_stable, inline_cost, type_annotations)); @show(findfirst(==(s), pairs(outputs)))) + in(s, values(outputs)) && (@show((optimize, debuginfo, iswarn, hide_type_stable, inlining_costs, type_annotations)); @show(findfirst(==(s), pairs(outputs)))) @assert !in(s, values(outputs)) - outputs[(optimize, debuginfo, iswarn, hide_type_stable, inline_cost, type_annotations)] = s + outputs[(optimize, debuginfo, iswarn, hide_type_stable, inlining_costs, type_annotations)] = s end end end diff --git a/test/irshowutils.jl b/test/irshowutils.jl index 395220b4..5fb10887 100644 --- a/test/irshowutils.jl +++ b/test/irshowutils.jl @@ -1,6 +1,6 @@ -function irshow_filename(fname, optimize, debuginfo, iswarn, hide_type_stable, inline_cost, type_annotations) +function irshow_filename(fname, optimize, debuginfo, iswarn, hide_type_stable, inlining_costs, type_annotations) return "test_irshow-ground_truth/$fname-$(optimize ? :opt : :unopt)-$(debuginfo)-$(iswarn ? :warn : :nowarn)\ - -$(hide_type_stable ? :hide_type_stable : :show_type_stable)-$(inline_cost ? :inline_cost : :nocost)\ + -$(hide_type_stable ? :hide_type_stable : :show_type_stable)-$(inlining_costs ? :inlining_costs : :nocost)\ -$(type_annotations ? :types : :notypes)" end diff --git a/test/irutils.jl b/test/irutils.jl index 8e68137d..4a650e73 100644 --- a/test/irutils.jl +++ b/test/irutils.jl @@ -1,5 +1,4 @@ using Core.IR -using Cthulhu: Cthulhu using Base.Meta: isexpr using InteractiveUtils: gen_call_with_extracted_types_and_kwargs diff --git a/test/provider_utils.jl b/test/provider_utils.jl new file mode 100644 index 00000000..afe0ad16 --- /dev/null +++ b/test/provider_utils.jl @@ -0,0 +1,89 @@ +using Core.IR +using Test +using .Cthulhu: CC, DefaultProvider, get_inference_world, find_method_instance, generate_code_instance, get_ci, get_override, LookupResult, find_callsites, Command, menu_commands, is_command_enabled, show_command, CthulhuState, PC2Effects, get_pc_effects, PC2Remarks, get_pc_remarks, PC2Excts, get_pc_excts, get_inlining_costs, show_parameters +using Cthulhu.Testing: VirtualTerminal, TestHarness, @run, wait_for, read_next, end_terminal_session, KEYS +using Logging: with_logger, NullLogger + +function test_provider_api(provider, args...) + world = get_inference_world(provider) + @test isa(world, UInt) + mi = find_method_instance(provider, args..., world) + @test isa(mi, MethodInstance) + ci = generate_code_instance(provider, mi) + @test isa(ci, CodeInstance) + result = LookupResult(provider, ci, false) + @test isa(result, LookupResult) + result = LookupResult(provider, ci, true) + @test isa(result, LookupResult) + + commands = menu_commands(provider) + @test isa(commands, Vector{Command}) + state = CthulhuState(provider; ci, mi) + @test any(command -> is_command_enabled(provider, state, command) === true, commands) + for command in commands + output = sprint(show_command, provider, state, command; context = :color => true) + @test isa(output, String) + @test contains(output, Char(command.key)) + if command.name === :dump_params + with_logger(NullLogger()) do + output = sprint(show_parameters, provider) + @test isa(output, String) + end + end + end + + with_logger(NullLogger()) do + effects = get_pc_effects(provider, ci) + @test effects === nothing || isa(effects, PC2Effects) + remarks = get_pc_remarks(provider, ci) + @test remarks === nothing || isa(remarks, PC2Remarks) + excts = get_pc_excts(provider, ci) + @test excts === nothing || isa(excts, PC2Excts) + inlining_costs = get_inlining_costs(provider, mi, something(result.src, result.ir)) + @test inlining_costs === nothing || isa(inlining_costs, Vector{Int}) + end + + result = LookupResult(provider, ci, false) + callsites, _ = find_callsites(provider, result, ci) + @test length(callsites) ≥ ifelse(result.optimized, 1, 5) + for callsite in callsites + ci = get_ci(callsite) + ci === nothing && continue + override = get_override(provider, callsite.info) + src = something(override, ci) + result = LookupResult(provider, src, false) + @test isa(result, LookupResult) + result = LookupResult(provider, src, true) + @test isa(result, LookupResult) + end +end + +function test_descend_for_provider(provider, args...; show = false) + terminal = VirtualTerminal() + harness = @run terminal descend(args...; terminal, provider) + write(terminal, 'T') + write(terminal, 'o') # optimize: on + write(terminal, 'L') + write(terminal, 'd') # debuginfo: :source + write(terminal, 'd') # debuginfo: :compact + write(terminal, 'T') + write(terminal, 'd') # debuginfo: :none + write(terminal, 'S') # optimize: off + write(terminal, 'T') + write(terminal, 'r') # remarks: on + write(terminal, 'e') # effects: on + write(terminal, 'o') # optimize: on + write(terminal, 'i') # inlining costs: on + write(terminal, 'o') # optimize: off + write(terminal, KEYS[:enter]) + write(terminal, 'S') + write(terminal, KEYS[:up]) + write(terminal, KEYS[:enter]) + write(terminal, 'q') + if show + wait_for(harness.task) + displayed = String(readavailable(harness.io)) + println(displayed) + end + @test end_terminal_session(harness) +end diff --git a/test/providers/CountingProviderModule.jl b/test/providers/CountingProviderModule.jl new file mode 100644 index 00000000..1e437528 --- /dev/null +++ b/test/providers/CountingProviderModule.jl @@ -0,0 +1,46 @@ +module CountingProviderModule + +using Core.IR +import ..Cthulhu +using ..Cthulhu: CC, AbstractProvider, DefaultProvider, CthulhuInterpreter, generate_code_instance, Command, default_menu_commands, OptimizedSource, InferredSource, run_type_inference +using .CC: InferenceResult + +mutable struct CountingProvider <: AbstractProvider + default::DefaultProvider + count::Int + CountingProvider() = new(DefaultProvider(), 0) +end + +Cthulhu.get_abstract_interpreter(provider::CountingProvider) = + Cthulhu.get_abstract_interpreter(provider.default) + +function Cthulhu.generate_code_instance(provider::CountingProvider, mi::MethodInstance) + provider.count += 1 + return generate_code_instance(provider.default, mi) +end + +function Cthulhu.menu_commands(provider::CountingProvider) + commands = default_menu_commands(provider) + append!(commands, [ + modify_count('+', :increment, 1), + modify_count('-', :decrement, -1), + ]) + return commands +end + +function modify_count(key, name, value) + Command(state -> state.provider.count += value, key, name, string(name), :actions) +end + +Cthulhu.run_type_inference(provider::CountingProvider, interp::CthulhuInterpreter, mi::MethodInstance) = + run_type_inference(provider.default, interp, mi) +Cthulhu.OptimizedSource(provider::CountingProvider, interp::CthulhuInterpreter, ci::CodeInstance) = + OptimizedSource(provider.default, interp, ci) +Cthulhu.OptimizedSource(provider::CountingProvider, interp::CthulhuInterpreter, result::InferenceResult) = + OptimizedSource(provider.default, interp, result) +Cthulhu.InferredSource(provider::CountingProvider, interp::CthulhuInterpreter, ci::CodeInstance) = + InferredSource(provider.default, interp, ci) + +export CountingProvider + +end diff --git a/test/providers/ExternalProviderModule.jl b/test/providers/ExternalProviderModule.jl new file mode 100644 index 00000000..91cec392 --- /dev/null +++ b/test/providers/ExternalProviderModule.jl @@ -0,0 +1,112 @@ +module ExternalProviderModule + +using Core.IR +import ..Cthulhu +using ..Cthulhu: CC, AbstractProvider, generate_code_instance, Command, default_menu_commands, OptimizedSource, InferredSource, run_type_inference, LookupResult +using .CC: InferenceResult, AbstractInterpreter, NativeInterpreter + +mutable struct ExternalOwner end + +struct InferredIR + ir::CC.IRCode + slotnames::Vector{Symbol} + inlining_cost::CC.InlineCostType + nargs::UInt + isva::Bool +end + +struct ExternalInterpreter <: AbstractInterpreter + owner::ExternalOwner + native::NativeInterpreter + ExternalInterpreter() = new(ExternalOwner(), NativeInterpreter()) +end + +Base.show(io::IO, interp::ExternalInterpreter) = print(io, typeof(interp), "()") + +CC.InferenceParams(interp::ExternalInterpreter) = CC.InferenceParams(interp.native) +CC.OptimizationParams(interp::ExternalInterpreter) = CC.OptimizationParams(interp.native) +CC.get_inference_world(interp::ExternalInterpreter) = CC.get_inference_world(interp.native) +CC.get_inference_cache(interp::ExternalInterpreter) = CC.get_inference_cache(interp.native) + +CC.may_optimize(::ExternalInterpreter) = true +CC.may_compress(::ExternalInterpreter) = false +CC.may_discard_trees(::ExternalInterpreter) = false +CC.method_table(interp::ExternalInterpreter) = CC.method_table(interp.native) +CC.cache_owner(interp::ExternalInterpreter) = interp.owner + +function get_slotnames(def::Method) + names = split(def.slot_syms, '\0') + return map(Symbol, names) +end + +function CC.src_inlining_policy(interp::ExternalInterpreter, @nospecialize(src), @nospecialize(info::CC.CallInfo), stmt_flag::UInt32) + isa(src, InferredIR) && return src.inlining_cost !== CC.MAX_INLINE_COST + return @invoke CC.src_inlining_policy(interp::AbstractInterpreter, src, info::CC.CallInfo, stmt_flag::UInt32) +end + +function CC.transform_result_for_cache(interp::ExternalInterpreter, result::InferenceResult) + (; ir) = result.src.optresult + (; src) = result.src + slotnames = get_slotnames(result.linfo.def) + inlining_cost = CC.compute_inlining_cost(interp, result) + return InferredIR(ir, slotnames, inlining_cost, src.nargs, src.isva) +end + +CC.transform_result_for_cache(interp::ExternalInterpreter, result::InferenceResult, edges::CC.SimpleVector) = + CC.transform_result_for_cache(interp, result) + +function CC.transform_result_for_local_cache(interp::ExternalInterpreter, result::InferenceResult) + CC.result_is_constabi(interp, result) && return nothing + return CC.transform_result_for_cache(interp, result) +end + +function CC.retrieve_ir_for_inlining(ci::CodeInstance, result::InferredIR) + mi = CC.get_ci_mi(ci) + return CC.retrieve_ir_for_inlining(mi, result.ir, true) +end + +function CC.retrieve_ir_for_inlining(mi::MethodInstance, result::InferredIR, preserve_local_sources::Bool) + return CC.retrieve_ir_for_inlining(mi, result.ir, true) +end + +mutable struct ExternalProvider <: AbstractProvider + interp::ExternalInterpreter + ExternalProvider() = new(ExternalInterpreter()) +end + +CC.get_inference_world(provider::ExternalProvider) = CC.get_inference_world(provider.interp) + +Cthulhu.find_method_instance(provider::ExternalProvider, @nospecialize(tt::Type{<:Tuple}), world::UInt) = + Cthulhu.find_method_instance(provider, provider.interp, tt, world) + +function Cthulhu.generate_code_instance(provider::ExternalProvider, mi::MethodInstance) + ci = CC.typeinf_ext(provider.interp, mi, CC.SOURCE_MODE_GET_SOURCE) + @assert isa(ci.inferred, InferredIR) + return ci +end + +function Cthulhu.LookupResult(provider::ExternalProvider, ci::CodeInstance, optimize::Bool #= ignored =#) + @assert isa(ci.inferred, InferredIR) + ir = copy(ci.inferred.ir) + src = Cthulhu.ir_to_src(ir) + src.ssavaluetypes = copy(ir.stmts.type) + src.min_world = @atomic ci.min_world + src.max_world = @atomic ci.max_world + rt = Cthulhu.cached_return_type(ci) + exct = Cthulhu.cached_exception_type(ci) + effects = Cthulhu.get_effects(ci) + infos = copy(ir.stmts.info) + return LookupResult(ir, src, rt, exct, infos, src.slottypes, effects, true #= optimized =#) +end + +Cthulhu.get_override(provider::ExternalProvider, @nospecialize(info)) = nothing + +function Cthulhu.menu_commands(provider::ExternalProvider) + commands = default_menu_commands(provider) + filter!(x -> !in(x.name, (:optimize, :dump_params)), commands) + return commands +end + +export ExternalProvider + +end diff --git a/test/providers/OverlayProviderModule.jl b/test/providers/OverlayProviderModule.jl new file mode 100644 index 00000000..09cf4138 --- /dev/null +++ b/test/providers/OverlayProviderModule.jl @@ -0,0 +1,81 @@ +module OverlayProviderModule # inspired by the Cthulhu integration at serenity4/SPIRV.jl/ext/SPIRVCthulhuExt.jl + +using Core.IR +import ..Cthulhu +using ..Cthulhu: CC, AbstractProvider, CthulhuInterpreter, generate_code_instance, Command, default_menu_commands, OptimizedSource, InferredSource, run_type_inference +using .CC: OverlayMethodTable, AbstractInterpreter, InferenceResult, InferenceParams, OptimizationParams + +### Interpreter + +Base.Experimental.@MethodTable METHOD_TABLE + +macro overlay(ex) + esc(:(Base.Experimental.@overlay $METHOD_TABLE $ex)) +end + +@overlay exp(x::Float64) = 42.0 + +mutable struct OverlayToken end + +struct OverlayInterpreter <: AbstractInterpreter + token::OverlayToken + method_table::OverlayMethodTable + local_cache::Vector{InferenceResult} + world::UInt + inference_parameters::InferenceParams + optimization_parameters::OptimizationParams +end + +function OverlayInterpreter(world::UInt = Base.get_world_counter(); + inference_parameters = InferenceParams(), + optimization_parameters = OptimizationParams()) + return OverlayInterpreter( + OverlayToken(), + OverlayMethodTable(world, METHOD_TABLE), + InferenceResult[], + world, + inference_parameters, + optimization_parameters, + ) +end + +CC.InferenceParams(interp::OverlayInterpreter) = interp.inference_parameters +CC.OptimizationParams(interp::OverlayInterpreter) = interp.optimization_parameters +CC.get_world_counter(interp::OverlayInterpreter) = interp.world +CC.get_inference_cache(interp::OverlayInterpreter) = interp.local_cache +CC.get_inference_world(interp::OverlayInterpreter) = interp.world +CC.cache_owner(interp::OverlayInterpreter) = interp.token +CC.method_table(interp::OverlayInterpreter) = interp.method_table + +function Base.show(io::IO, interp::OverlayInterpreter) + print(io, typeof(interp), "(...)") +end + +### Provider + +mutable struct OverlayProvider <: AbstractProvider + interp::OverlayInterpreter + cthulhu::CthulhuInterpreter + function OverlayProvider() + interp = OverlayInterpreter() + cthulhu = CthulhuInterpreter(interp) + return new(interp, cthulhu) + end +end + +Cthulhu.get_abstract_interpreter(provider::OverlayProvider) = provider.interp + +# Let Cthulhu manage storage of all sources. +# This only works so long as we don't rely on overriding compilation methods for `OverlayInterpreter` +# (besides those from the `AbstractInterpreter` interface that are forwarded by `CthulhuInterpreter`). + +Cthulhu.run_type_inference(provider::OverlayProvider, interp::OverlayInterpreter, mi::MethodInstance) = + run_type_inference(provider, provider.cthulhu, mi) +Cthulhu.OptimizedSource(provider::OverlayProvider, interp::OverlayInterpreter, ci::CodeInstance) = + OptimizedSource(provider, provider.cthulhu, ci) +Cthulhu.OptimizedSource(provider::OverlayProvider, interp::OverlayInterpreter, result::InferenceResult) = + OptimizedSource(provider, provider.cthulhu, result) +Cthulhu.InferredSource(provider::OverlayProvider, interp::OverlayInterpreter, ci::CodeInstance) = + InferredSource(provider, provider.cthulhu, ci) + +end diff --git a/test/runtests.jl b/test/runtests.jl index 93aa2e5d..c30c984b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,11 +1,33 @@ using Test, PerformanceTestTools using Core: Const # allows correct printing as `Const` instead of `Core.Const` +using Cthulhu: is_compiler_loaded, is_compiler_extension_loaded -@testset "runtests.jl" begin +if is_compiler_loaded() # don't load it otherwise, as that would load the Cthulhu extension + using Revise + Revise.track(Base) # get the `@info` log now, to avoid polluting test outputs later +end + +@testset "Cthulhu.jl" begin + before = is_compiler_extension_loaded() @testset "Core functionality" include("test_Cthulhu.jl") @testset "Code view" include("test_codeview.jl") + @testset "Provider functionality" include("test_provider.jl") @testset "Terminal tests" include("test_terminal.jl") @testset "AbstractInterpreter" include("test_AbstractInterpreter.jl") + after = is_compiler_extension_loaded() + @assert before === after # make sure we don't mess up the test setup by loading Compiler durin tests + if !is_compiler_extension_loaded() + @eval import Compiler # trigger the extension + if is_compiler_extension_loaded() # allow extension to be disabled locally during development + @testset "Tests with Compiler extension loaded" begin + @testset "Core functionality" include("test_Cthulhu.jl") + @testset "Code view" include("test_codeview.jl") + @testset "Provider functionality" include("test_provider.jl") + @testset "Terminal tests" include("test_terminal.jl") + @testset "AbstractInterpreter" include("test_AbstractInterpreter.jl") + end + end + end # TODO enable these tests false || return @info "skipped test_irshow.jl" @testset "IRShow display tests" include("test_irshow.jl") diff --git a/test/setup.jl b/test/setup.jl index 54e0a36c..119711f0 100644 --- a/test/setup.jl +++ b/test/setup.jl @@ -1,4 +1,5 @@ -using Test, Cthulhu, InteractiveUtils +using Test, InteractiveUtils +using .Cthulhu: AbstractProvider, CthulhuConfig, CthulhuState, find_method_instance, generate_code_instance, LookupResult if isdefined(parentmodule(@__MODULE__), :VSCodeServer) using ..VSCodeServer end @@ -7,16 +8,16 @@ end function cthulhu_info(@nospecialize(f), @nospecialize(tt=()); optimize=true, interp=Cthulhu.CC.NativeInterpreter()) - (interp, codeinst) = Cthulhu.mkinterp(f, tt; interp) - (; src, rt, exct, infos, slottypes, effects) = - Cthulhu.lookup(interp, codeinst, optimize; allow_no_src=true) - return (; interp, src, infos, codeinst, rt, exct, slottypes, effects) + provider = AbstractProvider(interp) + mi = find_method_instance(provider, f, tt) + ci = generate_code_instance(provider, mi) + result = LookupResult(provider, ci, optimize) + return provider, mi, ci, result end function find_callsites_by_ftt(@nospecialize(f), @nospecialize(TT=Tuple{}); optimize=true) - (; interp, src, infos, codeinst, slottypes) = cthulhu_info(f, TT; optimize) - src === nothing && return Cthulhu.Callsite[] - callsites, _ = Cthulhu.find_callsites(interp, src, infos, codeinst, slottypes, optimize) + provider, mi, ci, result = cthulhu_info(f, TT; optimize) + callsites, _ = Cthulhu.find_callsites(provider, result, ci) @test all(c -> Cthulhu.get_effects(c) isa Cthulhu.Effects, callsites) return callsites end diff --git a/test/test_AbstractInterpreter.jl b/test/test_AbstractInterpreter.jl index 8286c4f6..ec928aa3 100644 --- a/test/test_AbstractInterpreter.jl +++ b/test/test_AbstractInterpreter.jl @@ -1,6 +1,7 @@ module test_AbstractInterpreter using Test, Cthulhu +using Cthulhu: DefaultProvider, find_method_instance, generate_code_instance if isdefined(parentmodule(@__MODULE__), :VSCodeServer) using ..VSCodeServer end @@ -79,11 +80,12 @@ CC.method_table(interp::MTOverlayInterp) = @overlay OverlayedMT sin(x::Float64) = 1 @testset "OverlayMethodTable integration" begin - interp, mi = Cthulhu.mkinterp((Int,); interp=MTOverlayInterp()) do x - sin(x) - end - inferred = interp.unopt[mi] - @test inferred.rt === Core.Const(1) + f = x -> sin(x) + interp = MTOverlayInterp() + provider = DefaultProvider(interp) + mi = find_method_instance(provider, f, (Int,)) + ci = generate_code_instance(provider, mi) + @test ci.rettype_const === 1 end end # module test_AbstractInterpreter diff --git a/test/test_Cthulhu.jl b/test/test_Cthulhu.jl index 3b57286f..daba71a6 100644 --- a/test/test_Cthulhu.jl +++ b/test/test_Cthulhu.jl @@ -1,8 +1,11 @@ module test_Cthulhu -using Test, Cthulhu, StaticArrays, Random +using Test, StaticArrays, Random using Core: Const -const CC = Cthulhu.CTHULHU_MODULE[].CC + +using Cthulhu: Cthulhu as _Cthulhu, callstring +const Cthulhu = _Cthulhu.CTHULHU_MODULE[] +using .Cthulhu: CC, DefaultProvider, CthulhuConfig, CONFIG, set_config, set_config! include("setup.jl") include("irutils.jl") @@ -162,11 +165,8 @@ boundscheck_dce(x) = _boundscheck_dce(x) @testset "DCE & boundscheck" begin # no boundscheck elimination on Julia-level compilation for f in (boundscheck_dce_inbounds, boundscheck_dce) - let (; src) = cthulhu_info(f, Tuple{Vector{Float64}}) - @test count(src.stmts.stmt) do stmt - isexpr(stmt, :boundscheck) - end == 1 - end + (_, _, _, result) = cthulhu_info(f, Tuple{Vector{Float64}}) + @test count(stmt -> isexpr(stmt, :boundscheck), result.ir.stmts.stmt) == 1 end end @@ -263,17 +263,14 @@ Base.@assume_effects :terminates_locally function issue41694(x) end @testset "ConstResult" begin # constant prop' on all the splits - let callsites = find_callsites_by_ftt(; optimize = false) do - issue41694(12) - end - callinfo = only(callsites).info - @test isa(callinfo, Cthulhu.ConcreteCallInfo) - @test Cthulhu.get_rt(callinfo) == Const(factorial(12)) - @test Cthulhu.get_effects(callinfo) |> CC.is_foldable - io = IOBuffer() - print(io, only(callsites)) - @test occursin("= < concrete eval > issue41694(::Const(12))", String(take!(io))) - end + callsites = find_callsites_by_ftt(() -> issue41694(12); optimize = false) + callinfo = only(callsites).info + @test isa(callinfo, Cthulhu.ConcreteCallInfo) + @test Cthulhu.get_rt(callinfo) == Const(factorial(12)) + @test Cthulhu.get_effects(callinfo) |> CC.is_foldable + io = IOBuffer() + print(io, only(callsites)) + @test occursin("= < concrete eval > issue41694(::Const(12))", String(take!(io))) end let # check the performance benefit of semi concrete evaluation @@ -293,17 +290,14 @@ let # check the performance benefit of semi concrete evaluation end @testset "SemiConcreteResult" begin # constant prop' on all the splits - let callsites = find_callsites_by_ftt((Int,); optimize = false) do x - semi_concrete_eval(42, x) - end - callinfo = only(callsites).info - @test isa(callinfo, Cthulhu.SemiConcreteCallInfo) - @test Cthulhu.get_rt(callinfo) == Const(semi_concrete_eval(42, 0)) - # @test Cthulhu.get_effects(callinfo) |> CC.is_semiconcrete_eligible - io = IOBuffer() - print(io, only(callsites)) - @test occursin("= < semi-concrete eval > semi_concrete_eval(::Const(42),::$Int)", String(take!(io))) - end + callsites = find_callsites_by_ftt(x -> semi_concrete_eval(42, x), (Int,); optimize = false) + callinfo = only(callsites).info + @test isa(callinfo, Cthulhu.SemiConcreteCallInfo) + @test Cthulhu.get_rt(callinfo) == Const(semi_concrete_eval(42, 0)) + # @test Cthulhu.get_effects(callinfo) |> CC.is_semiconcrete_eligible + io = IOBuffer() + print(io, only(callsites)) + @test occursin("= < semi-concrete eval > semi_concrete_eval(::Const(42),::$Int)", String(take!(io))) end function bar346(x::ComplexF64) @@ -311,12 +305,10 @@ function bar346(x::ComplexF64) return sin(x.im) end @testset "issue #346" begin - let (; interp, src, infos, codeinst, slottypes) = cthulhu_info(bar346, Tuple{ComplexF64}; optimize=false) - callsites, _ = Cthulhu.find_callsites(interp, src, infos, codeinst, slottypes, false) - @test isa(callsites[1].info, Cthulhu.SemiConcreteCallInfo) - @test occursin("= < semi-concrete eval > getproperty(::ComplexF64,::Const(:re))::Float64", string(callsites[1])) - @test Cthulhu.get_rt(callsites[end].info) == Const(sin(1.0)) - end + callsites = find_callsites_by_ftt(bar346, Tuple{ComplexF64}; optimize=false) + @test isa(callsites[1].info, Cthulhu.SemiConcreteCallInfo) + @test occursin("= < semi-concrete eval > getproperty(::ComplexF64,::Const(:re))::Float64", string(callsites[1])) + @test Cthulhu.get_rt(callsites[end].info) == Const(sin(1.0)) end struct SingletonPureCallable{N} end @@ -339,10 +331,10 @@ end only_ints(::Integer) = 1 callsites = find_callsites_by_ftt(; optimize=false) do - t1 = CC.return_type(only_ints, Tuple{Int}) # successful `return_type` - t2 = CC.return_type(only_ints, Tuple{Float64}) # failed `return_type` - t1, t2 - end + t1 = CC.return_type(only_ints, Tuple{Int}) # successful `return_type` + t2 = CC.return_type(only_ints, Tuple{Float64}) # failed `return_type` + t1, t2 + end # We have the function resolved as `getproperty(Compiler, :return_type)` first. @test length(callsites) == 4 extract_callsite(i) = callsites[2i] @@ -371,46 +363,44 @@ end end @testset "OCCallInfo" begin - let callsites = find_callsites_by_ftt((Int,Int,); optimize=false) do a, b - oc = Base.Experimental.@opaque b -> sin(a) + cos(b) - oc(b) - end - @test length(callsites) == 1 - callinfo = only(callsites).info - @test callinfo isa Cthulhu.OCCallInfo - @test Cthulhu.get_effects(callinfo) |> !CC.is_foldable_nothrow - # TODO not sure what these effects are (and neither is Base.infer_effects yet) - @test callinfo.ci.rt === Base.return_types((Int,Int)) do a, b - sin(a) + cos(b) - end |> only === Float64 - - buf = IOBuffer() - Cthulhu.show_callinfo(buf, callinfo.ci) - s = "opaque closure(::$Int)::$Float64" - @test String(take!(buf)) == s - print(buf, only(callsites)) - @test occursin("< opaque closure call > $s", String(take!(buf))) + callsites = find_callsites_by_ftt((Int,Int,); optimize=false) do a, b + oc = Base.Experimental.@opaque b -> sin(a) + cos(b) + oc(b) end + @test length(callsites) == 1 + callinfo = only(callsites).info + @test callinfo isa Cthulhu.OCCallInfo + @test Cthulhu.get_effects(callinfo) |> !CC.is_foldable_nothrow + # TODO not sure what these effects are (and neither is Base.infer_effects yet) + @test callinfo.ci.rt === Base.return_types((Int,Int)) do a, b + sin(a) + cos(b) + end |> only === Float64 + + buf = IOBuffer() + Cthulhu.show_callinfo(buf, callinfo.ci) + s = "opaque closure(::$Int)::$Float64" + @test String(take!(buf)) == s + print(buf, only(callsites)) + @test occursin("< opaque closure call > $s", String(take!(buf))) # const-prop'ed OC callsite - let callsites = find_callsites_by_ftt((Int,); optimize=false) do a - oc = Base.Experimental.@opaque Base.@constprop :aggressive b -> sin(b) - oc(42) - end + callsites = find_callsites_by_ftt((Int,); optimize=false) do a + oc = Base.Experimental.@opaque Base.@constprop :aggressive b -> sin(b) + oc(42) + end - @test length(callsites) == 1 - callinfo = only(callsites).info - @test callinfo isa Cthulhu.OCCallInfo - inner = callinfo.ci - @test inner isa Cthulhu.ConstPropCallInfo || inner isa Cthulhu.SemiConcreteCallInfo + @test length(callsites) == 1 + callinfo = only(callsites).info + @test callinfo isa Cthulhu.OCCallInfo + inner = callinfo.ci + @test inner isa Cthulhu.ConstPropCallInfo || inner isa Cthulhu.SemiConcreteCallInfo - buf = IOBuffer() - Cthulhu.show_callinfo(buf, callinfo.ci) - s = "opaque closure(::$(Const(42)))::$(Const(sin(42)))" - @test String(take!(buf)) == s - print(buf, only(callsites)) - @test occursin("< opaque closure call > $s", String(take!(buf))) - end + buf = IOBuffer() + Cthulhu.show_callinfo(buf, callinfo.ci) + s = "opaque closure(::$(Const(42)))::$(Const(sin(42)))" + @test String(take!(buf)) == s + print(buf, only(callsites)) + @test occursin("< opaque closure call > $s", String(take!(buf))) end # tasks @@ -430,45 +420,43 @@ invoke_constcall(a::Any, c::Bool) = c ? Any : :any invoke_constcall(a::Number, c::Bool) = c ? Number : :number @testset "InvokeCallInfo" begin - let callsites = find_callsites_by_ftt((Int,); optimize=false) do n - Base.@invoke invoke_call(n::Integer) - end - callsite = only(callsites) - info = callsite.info - @test isa(info, Cthulhu.InvokeCallInfo) - @test Cthulhu.get_effects(info) |> CC.is_foldable_nothrow - rt = CC.Const(:Integer) - @test info.ci.rt === rt - buf = IOBuffer() - show(buf, callsite) - @test occursin("= invoke < invoke_call(::$Int)::$rt >", String(take!(buf))) - end - let callsites = find_callsites_by_ftt((Int,); optimize=false) do n - Base.@invoke invoke_call(n::Int) - end - callsite = only(callsites) - info = callsite.info - @test isa(info, Cthulhu.InvokeCallInfo) - @test Cthulhu.get_effects(info) |> CC.is_foldable_nothrow - @test info.ci.rt === CC.Const(:Int) - end + callsites = find_callsites_by_ftt((Int,); optimize=false) do n + Base.@invoke invoke_call(n::Integer) + end + callsite = only(callsites) + info = callsite.info + @test isa(info, Cthulhu.InvokeCallInfo) + @test Cthulhu.get_effects(info) |> CC.is_foldable_nothrow + rt = CC.Const(:Integer) + @test info.ci.rt === rt + buf = IOBuffer() + show(buf, callsite) + @test occursin("= invoke < invoke_call(::$Int)::$rt >", String(take!(buf))) + + callsites = find_callsites_by_ftt((Int,); optimize=false) do n + Base.@invoke invoke_call(n::Int) + end + callsite = only(callsites) + info = callsite.info + @test isa(info, Cthulhu.InvokeCallInfo) + @test Cthulhu.get_effects(info) |> CC.is_foldable_nothrow + @test info.ci.rt === CC.Const(:Int) # const prop' / semi-concrete callsite - let callsites = find_callsites_by_ftt((Any,); optimize=false) do a - Base.@invoke invoke_constcall(a::Any, true::Bool) - end - callsite = only(callsites) - info = callsite.info - @test isa(info, Cthulhu.InvokeCallInfo) - @test Cthulhu.get_effects(info) |> CC.is_foldable_nothrow - inner = info.ci - rt = Const(Any) - @test Cthulhu.get_rt(info) === rt - buf = IOBuffer() - show(buf, callsite) - @test isa(inner, Cthulhu.SemiConcreteCallInfo) - @test occursin("= invoke < invoke_constcall(::Any,::$(Const(true)))::$rt", String(take!(buf))) - end + callsites = find_callsites_by_ftt((Any,); optimize=false) do a + Base.@invoke invoke_constcall(a::Any, true::Bool) + end + callsite = only(callsites) + info = callsite.info + @test isa(info, Cthulhu.InvokeCallInfo) + @test Cthulhu.get_effects(info) |> CC.is_foldable_nothrow + inner = info.ci + rt = Const(Any) + @test Cthulhu.get_rt(info) === rt + buf = IOBuffer() + show(buf, callsite) + @test isa(inner, Cthulhu.SemiConcreteCallInfo) + @test occursin("= invoke < invoke_constcall(::Any,::$(Const(true)))::$rt", String(take!(buf))) end ## @@ -613,13 +601,13 @@ end end @testset "warntype variables" begin - src, rettype = code_typed(identity, (Any,); optimize=false)[1] - effects = Base.infer_effects(identity, (Any,)) - io = IOBuffer() - ioctx = IOContext(io, :color=>true) - Cthulhu.cthulhu_warntype(ioctx, :none, src, rettype, effects, nothing) - str = String(take!(io)) - @test occursin("x\e[91m\e[1m::Any\e[22m\e[39m", str) + provider, mi, ci, result = cthulhu_info(identity, (Any,); optimize = false) + state = CthulhuState(provider; mi, ci) + buffer = IOBuffer() + io = IOContext(buffer, :color => true) + Cthulhu.cthulhu_warntype(io, provider, state, result) + str = String(take!(buffer)) + @test occursin("x\e[31m::Any\e[39m", str) end @testset "Limit printing (issue #94)" begin @@ -633,36 +621,23 @@ end end end function doprint(f) - (; src, codeinst, rt, exct, effects) = cthulhu_info(f; optimize=false) + provider, mi, ci, result = cthulhu_info(f; optimize = false) + config = set_config(CONFIG; view = :typed) + state = CthulhuState(provider; mi, ci, config) io = IOBuffer() - Cthulhu.cthulhu_typed(io, :none, src, rt, exct, effects, codeinst; iswarn=false) + Cthulhu.cthulhu_typed(io, provider, state, result) return String(take!(io)) end - @test occursin("invoke f1()::…\n", doprint(getfield(m, :f1))) - str = doprint(getfield(m, :f2)) + @test occursin("invoke f1()::…\n", doprint(m.f1)) + str = doprint(m.f2) @test occursin("y::Const([1, 2, 3", str) @test !occursin("500,", str) end @testset "Issue #132" begin f132(w, dim) = [i == dim ? w[i]/2 : w[i]/1 for i in eachindex(w)] - interp, mi = Cthulhu.mkinterp(f132, (Vector{Int}, Int)) - @test isa(mi, Core.CodeInstance) # just check that the above succeeded -end - -@testset "@interp" begin - finterp1(x) = 2 - (interp, mi) = Cthulhu.@interp finterp1(5) - @test isa(mi, Core.CodeInstance) - - finterp2(x, y) = string(x, y) - (interp, mi) = Cthulhu.@interp finterp2("hi", " there") - @test isa(mi, Core.CodeInstance) - - finterp3(x, y, z) = (x + y) / z - tt = Tuple{typeof(finterp3), Int64, Int64, Float64} - (interp, mi) = Cthulhu.mkinterp(tt) - @test isa(mi, Core.CodeInstance) + callsites = find_callsites_by_ftt(f132, (Vector{Int}, Int)) + @test !isempty(callsites) # just check that the above succeeded end # ## Functions for "backedges & treelist" @@ -728,12 +703,13 @@ end return val, line1, line2, line3 end _, line1, line2, line3 = caller(7) - micaller = Cthulhu.get_specialization(caller, Tuple{Int}) - micallee_Int = Cthulhu.get_specialization(callee, Tuple{Int}) - micallee_Float64 = Cthulhu.get_specialization(callee, Tuple{Float64}) - info, lines = only(Cthulhu.find_caller_of(CC.NativeInterpreter(), micallee_Int, micaller)) + provider = DefaultProvider() + micaller = find_method_instance(provider, caller, (Int,)) + micallee_Int = find_method_instance(provider, callee, (Int,)) + micallee_Float64 = find_method_instance(provider, callee, (Float64,)) + info, lines = only(Cthulhu.find_caller_of(provider, micallee_Int, micaller)) @test info == (:caller, Symbol(@__FILE__), 0) && lines == [line1, line3] - info, lines = only(Cthulhu.find_caller_of(CC.NativeInterpreter(), micallee_Float64, micaller)) + info, lines = only(Cthulhu.find_caller_of(provider, micallee_Float64, micaller)) @test info == (:caller, Symbol(@__FILE__), 0) && lines == [line2] M = Module() @@ -743,11 +719,11 @@ end g(c) = f(c...); const gline = @__LINE__ end @test M.g(Any["cat", "dog"]) == "cat dog" - - mif = Cthulhu.get_specialization(M.f, Tuple{String, Vararg{String}}) - mig = Cthulhu.get_specialization(M.g, Tuple{Vector{Any}}) - @test isempty(Cthulhu.find_caller_of(Cthulhu.CthulhuInterpreter(), mif, mig)) - candidate, lines = only(Cthulhu.find_caller_of(Cthulhu.CthulhuInterpreter(), mif, mig; allow_unspecialized=true)) + provider = DefaultProvider() + mif = find_method_instance(provider, M.f, (String, Vararg{String})) + mig = find_method_instance(provider, M.g, (Vector{Any},)) + @test isempty(Cthulhu.find_caller_of(provider, mif, mig)) + candidate, lines = only(Cthulhu.find_caller_of(provider, mif, mig; allow_unspecialized=true)) @test candidate[1] === nameof(M.g) @test candidate[2] === Symbol(@__FILE__) @test candidate[3] == 0 # depth @@ -762,9 +738,10 @@ end return val, line1, line2 end _, line1, line2 = outercaller(7) - micaller = Cthulhu.get_specialization(outercaller, Tuple{Int}) - micallee = Cthulhu.get_specialization(nicallee, Tuple{Int}) - callerinfo = Cthulhu.find_caller_of(CC.NativeInterpreter(), micallee, micaller) + provider = DefaultProvider() + micaller = find_method_instance(provider, outercaller, (Int,)) + micallee = find_method_instance(provider, nicallee, (Int,)) + callerinfo = Cthulhu.find_caller_of(provider, micallee, micaller) @test length(callerinfo) == 2 info, lines = callerinfo[1] @test info == (:outercaller, Symbol(@__FILE__), 0) @@ -788,7 +765,7 @@ end end io = IOBuffer() - @test Cthulhu.callstring(io, m.FunnyMI()) == "funny(::Char)" + @test callstring(io, m.FunnyMI()) == "funny(::Char)" end ## @@ -848,8 +825,9 @@ let callsites = find_callsites_by_ftt(issue152_another, (Tuple{Float64,Vararg{Fl end @testset "Bookmarks" begin - (interp, ci) = Cthulhu.mkinterp(sqrt, Tuple{Float64}) - b = Cthulhu.Bookmark(ci, interp) + provider, mi, ci, result = cthulhu_info(sqrt, (Float64,)) + config = set_config(CONFIG; view = :typed, optimize = true) + b = Cthulhu.Bookmark(provider, config, ci) @testset "code_typed(bookmark)" begin ci, rt = code_typed(b) @@ -865,14 +843,14 @@ end @testset "show(io, bookmark)" begin str = sprint(io -> show(io, "text/plain", b)) - @test occursin("Cthulhu.Bookmark (world: ", str) + @test occursin("$Cthulhu.Bookmark (world: ", str) end @testset "show(io, [bookmark])" begin # Test that it does not print the full IR: str = sprint(io -> show(io, "text/plain", [b])) - @test occursin("world:", str) - @test !occursin("Cthulhu.Bookmark (world: ", str) + @test contains(str, "\n invoke sqrt(::Float64)::Float64 (world:") + @test !occursin("$Cthulhu.Bookmark (world: ", str) end @testset "Smoke tests" begin @@ -886,18 +864,15 @@ end @testset "preferences" begin # Test that load and save are able to set state - Cthulhu.CONFIG.enable_highlighter = true - Cthulhu.CONFIG.debuginfo = :none - Cthulhu.save_config!(Cthulhu.CONFIG) - - Cthulhu.CONFIG.enable_highlighter = false - Cthulhu.CONFIG.debuginfo = :compact + set_config!(; enable_highlighter = true, debuginfo = :none) + Cthulhu.save_config!() + set_config!(; enable_highlighter = false, debuginfo = :compact) + @test Cthulhu.CONFIG.enable_highlighter === false @test Cthulhu.CONFIG.debuginfo === :compact - @test !Cthulhu.CONFIG.enable_highlighter - Cthulhu.read_config!(Cthulhu.CONFIG) + Cthulhu.read_config!() + @test Cthulhu.CONFIG.enable_highlighter === true @test Cthulhu.CONFIG.debuginfo === :none - @test Cthulhu.CONFIG.enable_highlighter end Base.@constprop :none sin_noconstprop(x) = sin(x) @@ -910,12 +885,12 @@ function remarks_dced(x) return v end @testset "per-statement remarks" begin - interp, mi = Cthulhu.mkinterp(remarks_dced, (Float64,)); - src = interp.unopt[mi].src + provider, mi, ci, result = cthulhu_info(remarks_dced, (Float64,)) + src = provider.interp.unopt[ci].src i = only(findall(iscall((src, sin)), src.code)) j = only(findall(iscall((src, sin_noconstprop)), src.code)) @test i < j - pc2remarks = interp.remarks[mi] + pc2remarks = provider.interp.remarks[ci] @test any(pc2remarks) do (pc, msg) pc == j && occursin("Disabled by method parameter", msg) end @@ -932,14 +907,14 @@ function effects_dced(x) return a, n end @testset "per-statement effects" begin - interp, mi = Cthulhu.mkinterp(effects_dced, (Int,)); - src = interp.unopt[mi].src + provider, mi, ci, result = cthulhu_info(effects_dced, (Int,)) + src = provider.interp.unopt[ci].src i1 = only(findall(iscall((src, isa)), src.code)) i2 = only(findall(iscall((src, getindex)), src.code)) i3 = only(findall(iscall((src, push!)), src.code)) i4 = only(findall(iscall((src, Core.arraysize)), src.code)) @test i1 < i2 < i3 < i4 - pc2effects = interp.effects[mi] + pc2effects = provider.interp.effects[ci] @test haskey(pc2effects, i1) @test haskey(pc2effects, i2) @test haskey(pc2effects, i3) @@ -947,21 +922,19 @@ end end @inline countvars50037(bitflags::Int, var::Int) = bitflags >> 0 -let (interp, codeinst) = Cthulhu.mkinterp((Int,)) do var::Int +let (_, _, ci, _) = cthulhu_info((Int,)) do var::Int # Make sure that code is cached by ensuring a non-const return type. x = Base.inferencebarrier(1)::Int countvars50037(x, var) end - inferred = @atomic :monotonic codeinst.inferred + inferred = @atomic :monotonic ci.inferred @test length(inferred.ir.cfg.blocks) == 1 end f515() = cglobal((:foo, bar)) @testset "issue #515" begin - let (; interp, src, infos, codeinst, slottypes) = cthulhu_info(f515) - callsites, _ = Cthulhu.find_callsites(interp, src, infos, codeinst, slottypes) - @test isempty(callsites) - end + callsites = find_callsites_by_ftt(f515) + @test isempty(callsites) end end # module test_Cthulhu diff --git a/test/test_codeview.jl b/test/test_codeview.jl index 52b7fad7..9a025fd4 100644 --- a/test/test_codeview.jl +++ b/test/test_codeview.jl @@ -1,43 +1,67 @@ module test_codeview -using Cthulhu, Test, Revise +using Test +using Logging: NullLogger, with_logger + +using Cthulhu: Cthulhu as _Cthulhu, is_compiler_loaded +const Cthulhu = _Cthulhu.CTHULHU_MODULE[] +using .Cthulhu: CthulhuState, view_function, CONFIG, set_config, cthulhu_typed include("setup.jl") -# NOTE setup for `cthulhu_ast` -include("TestCodeViewSandbox.jl") -using .TestCodeViewSandbox -Revise.track(TestCodeViewSandbox, normpath(@__DIR__, "TestCodeViewSandbox.jl")) +if is_compiler_loaded() + @eval using Revise + Revise.track(Base) # get the `@info` log now, to avoid polluting test outputs later + file = tempname() * ".jl" + open(file, "w+") do io + println(io, """ + module Sandbox + + function testf_revise() + T = rand() > 0.5 ? Int64 : Float64 + sum(rand(T, 100)) + end -@testset "printer test" begin - (; interp, src, infos, codeinst, rt, exct, effects, slottypes) = cthulhu_info(testf_revise); - tf = (true, false) - mi = codeinst.def - @testset "codeview: $codeview" for codeview in Cthulhu.CODEVIEWS - if !isdefined(@__MODULE__(), :Revise) - codeview == Cthulhu.cthulhu_ast && continue end - @testset "optimize: $optimize" for optimize in tf - @testset "debuginfo: $debuginfo" for debuginfo in instances(Cthulhu.DInfo.DebugInfo) - config = Cthulhu.CONFIG + """) + end + include(file) + (; testf_revise) = Sandbox + Revise.track(Sandbox, file) +else + function testf_revise() + T = rand() > 0.5 ? Int64 : Float64 + sum(rand(T, 100)) + end +end +@testset "printer test" begin + tf = (true, false) + @testset "optimize: $optimize" for optimize in tf + provider, mi, ci, result = cthulhu_info(testf_revise; optimize) + @testset "view: $view" for view in (:source, :ast, :typed, :llvm, :native) + view === :ast && !isdefined(@__MODULE__(), :Revise) && continue + @testset "debuginfo: $debuginfo" for debuginfo in (:none, :source, :compact) + config = set_config(CONFIG; view, debuginfo) + state = CthulhuState(provider; config, ci, mi) io = IOBuffer() - src = Cthulhu.CC.typeinf_code(interp, mi, true) - codeview(io, mi, src, optimize, debuginfo, Cthulhu.get_inference_world(interp), config) - @test !isempty(String(take!(io))) # just check it works + view_function(state)(io, provider, state, result) + output = String(take!(io)) + @test !isempty(output) # just check it works end end end - @testset "debuginfo: $debuginfo" for debuginfo in instances(Cthulhu.DInfo.DebugInfo) + provider, mi, ci, result = cthulhu_info(testf_revise; optimize = true) + @testset "debuginfo: $debuginfo" for debuginfo in (:none, :source, :compact) @testset "iswarn: $iswarn" for iswarn in tf @testset "hide_type_stable: $hide_type_stable" for hide_type_stable in tf - @testset "inline_cost: $inline_cost" for inline_cost in tf + @testset "inlining_costs: $inlining_costs" for inlining_costs in tf @testset "type_annotations: $type_annotations" for type_annotations in tf + config = set_config(CONFIG; view = :typed, debuginfo, iswarn, hide_type_stable, inlining_costs, type_annotations) + state = CthulhuState(provider; config, ci, mi) io = IOBuffer() - Cthulhu.cthulhu_typed(io, debuginfo, - src, rt, exct, effects, codeinst; - iswarn, hide_type_stable, inline_cost, type_annotations) + view_function(state)(io, provider, state, result) @test !isempty(String(take!(io))) # just check it works end end @@ -47,8 +71,20 @@ Revise.track(TestCodeViewSandbox, normpath(@__DIR__, "TestCodeViewSandbox.jl")) end @testset "hide type-stable statements" begin - let # optimize code - (; src, infos, codeinst, rt, exct, effects, slottypes) = @eval Module() begin + function printer(provider, mi, ci, result) + return function prints(; kwargs...) + io = IOBuffer() + config = set_config(CONFIG; debuginfo = :none, kwargs...) + state = CthulhuState(provider; config, ci, mi) + with_logger(NullLogger()) do + cthulhu_typed(io, provider, state, result) + end + return String(take!(io)) + end + end + + @testset "optimized" begin + provider, mi, ci, result = @eval Module() begin const globalvar = Ref(42) $cthulhu_info() do a = sin(globalvar[]) @@ -56,26 +92,21 @@ end return (a, b) end end - function prints(; kwargs...) - io = IOBuffer() - Cthulhu.cthulhu_typed(io, :none, src, rt, exct, effects, codeinst; kwargs...) - return String(take!(io)) - end + prints = printer(provider, mi, ci, result) - let # by default, should print every statement - s = prints() - @test occursin("globalvar", s) - @test occursin("undefvar", s) - end - let # should omit type stable statements - s = prints(; hide_type_stable=true) - @test !occursin("globalvar", s) - @test occursin("undefvar", s) - end + # by default, should print every statement + s = prints() + @test occursin("globalvar", s) + @test occursin("undefvar", s) + + # should omit type stable statements + s = prints(; hide_type_stable=true) + @test !occursin("globalvar", s) + @test occursin("undefvar", s) end - let # unoptimize code - (; src, infos, codeinst, rt, exct, effects, slottypes) = @eval Module() begin + @testset "unoptimized" begin + provider, mi, ci, result = @eval Module() begin const globalvar = Ref(42) $cthulhu_info(; optimize=false) do a = sin(globalvar[]) @@ -83,41 +114,37 @@ end return (a, b) end end - function prints(; kwargs...) - io = IOBuffer() - Cthulhu.cthulhu_typed(io, :none, src, rt, exct, effects, codeinst; kwargs...) - return String(take!(io)) - end + prints = printer(provider, mi, ci, result) - let # by default, should print every statement - s = prints() - @test occursin("globalvar", s) - @test occursin("undefvar", s) - end - let # should omit type stable statements - s = prints(; hide_type_stable=true) - @test !occursin("globalvar", s) - @test occursin("undefvar", s) - end + # by default, should print every statement + s = prints() + @test occursin("globalvar", s) + @test occursin("undefvar", s) + + # should omit type stable statements + s = prints(; hide_type_stable=true) + @test !occursin("globalvar", s) + @test occursin("undefvar", s) # should work for warn mode - let - s = prints(; iswarn=true) - @test occursin("globalvar", s) - @test occursin("undefvar", s) - end - let - s = prints(; iswarn=true, hide_type_stable=true) - @test !occursin("globalvar", s) - @test occursin("undefvar", s) - end + s = prints(; iswarn=true) + @test occursin("globalvar", s) + @test occursin("undefvar", s) + s = prints(; iswarn=true, hide_type_stable=true) + @test !occursin("globalvar", s) + @test occursin("undefvar", s) end -end +end; @testset "Regressions" begin # Issue #675 - (; src, infos, codeinst, rt, exct, effects, slottypes) = cthulhu_info(NamedTuple; optimize=false) - Cthulhu.cthulhu_typed(IOBuffer(), :none, src, rt, exct, effects, codeinst; annotate_source=true) + provider, mi, ci, result = cthulhu_info(testf_revise; optimize=false) + config = set_config(; view = :source, debuginfo = :none) + state = CthulhuState(provider; config, ci, mi) + io = IOBuffer() + view_function(state)(io, provider, state, result) + output = String(take!(io)) + @test isa(output, String) end end # module test_codeview diff --git a/test/test_irshow.jl b/test/test_irshow.jl index c7009dfe..f753246a 100644 --- a/test/test_irshow.jl +++ b/test/test_irshow.jl @@ -12,22 +12,22 @@ include("IRShowSandbox.jl") @testset "optimize: $optimize" for optimize in tf (; src, infos, codeinst, rt, exct, effects, slottypes) = cthulhu_info(IRShowSandbox.foo, (Int, Int); optimize); - @testset "debuginfo: $debuginfo" for debuginfo in instances(Cthulhu.DInfo.DebugInfo) + @testset "debuginfo: $debuginfo" for debuginfo in (:none, :source, :compact) @testset "iswarn: $iswarn" for iswarn in tf @testset "hide_type_stable: $hide_type_stable" for hide_type_stable in tf - @testset "inline_cost: $inline_cost" for inline_cost in tf + @testset "inlining_costs: $inlining_costs" for inlining_costs in tf @testset "type_annotations: $type_annotations" for type_annotations in tf - !optimize && debuginfo === Cthulhu.DInfo.compact && continue - !optimize && inline_cost && continue + !optimize && debuginfo === :compact && continue + !optimize && inlining_costs && continue s = sprint(; context=:color=>true) do io Cthulhu.cthulhu_typed(io, debuginfo, src, rt, exct, effects, codeinst; - iswarn, hide_type_stable, inline_cost, type_annotations) + iswarn, hide_type_stable, inlining_costs, type_annotations) end s = strip_base_linenums(s) - bname = irshow_filename("foo", optimize, debuginfo, iswarn, hide_type_stable, inline_cost, type_annotations) + bname = irshow_filename("foo", optimize, debuginfo, iswarn, hide_type_stable, inlining_costs, type_annotations) fpath = normpath(@__DIR__, bname) ground_truth = read(fpath, String) diff --git a/test/test_provider.jl b/test/test_provider.jl new file mode 100644 index 00000000..181b0ae4 --- /dev/null +++ b/test/test_provider.jl @@ -0,0 +1,73 @@ +module test_provider + +using Test +using Core.IR +using Cthulhu: CTHULHU_MODULE + +@eval module Impl +const Cthulhu = $(CTHULHU_MODULE[]) +using Cthulhu: descend +using .Cthulhu: DefaultProvider +include("provider_utils.jl") +include("providers/CountingProviderModule.jl") +using .CountingProviderModule: CountingProvider +include("providers/OverlayProviderModule.jl") +using .OverlayProviderModule: OverlayProvider +@static if VERSION > v"1.13-" + include("providers/ExternalProviderModule.jl") + using .ExternalProviderModule: ExternalProvider +end +end + +logs(warnings) = tuple.(:warn, warnings) + +normal_warnings = logs([ + "Disable optimization to see the inference remarks.", + "Enable optimization to see the inlining costs.", +]) + +impl_warnings = logs([ + r"Remarks could not be retrieved", + r"Remarks could not be retrieved", + r"Effects could not be retrieved", + normal_warnings[1][end], + r"Effects could not be retrieved", + r"Effects could not be retrieved", + normal_warnings[2][end], + r"Remarks could not be retrieved", + r"Effects could not be retrieved", + r"Remarks could not be retrieved", + r"Effects could not be retrieved", + r"Remarks could not be retrieved", + r"Effects could not be retrieved", + r"Remarks could not be retrieved", + r"Effects could not be retrieved", +]) + +impl_warnings_noopt = impl_warnings[[3, 7, 9, 11, 13, 15]] + +function test_function(x) + y = 1 + 1 # constant propgation (concrete evaluation) + z, state = iterate((2, x)) # semi-concrete evaluation + return exp(x + z) # a normal call +end + +@testset "Example providers" begin + args = (test_function, (Int,)) + + @testset "Provider API" begin + Impl.test_provider_api(Impl.DefaultProvider(), args...) + Impl.test_provider_api(Impl.CountingProvider(), args...) + Impl.test_provider_api(Impl.OverlayProvider(), args...) + VERSION > v"1.13-" && Impl.test_provider_api(Impl.ExternalProvider(), args...) + end + + @testset "`descend`" begin + @test_logs normal_warnings... Impl.test_descend_for_provider(Impl.DefaultProvider(), args...) + @test_logs impl_warnings... Impl.test_descend_for_provider(Impl.CountingProvider(), args...) + @test_logs impl_warnings... Impl.test_descend_for_provider(Impl.OverlayProvider(), args...) + VERSION > v"1.13-" && @test_logs impl_warnings_noopt... Impl.test_descend_for_provider(Impl.ExternalProvider(), args...) + end +end; + +end # module test_provider diff --git a/test/test_terminal.jl b/test/test_terminal.jl index b440b2bd..2f41e3a7 100644 --- a/test/test_terminal.jl +++ b/test/test_terminal.jl @@ -1,201 +1,185 @@ module test_terminal using Core: Const -using Test, REPL, Cthulhu, Revise -using Cthulhu.Testing: FakeTerminal -_Cthulhu::Module = Cthulhu.CTHULHU_MODULE[] +using Test, REPL, LinearAlgebra +using Cthulhu.Testing +using Cthulhu.Testing: @run +using Cthulhu: Cthulhu as _Cthulhu, is_compiler_loaded, descend, descend_code_warntype, @descend, ascend +const Cthulhu = _Cthulhu.CTHULHU_MODULE[] if isdefined(parentmodule(@__MODULE__), :VSCodeServer) using ..VSCodeServer end -cread1(terminal) = readuntil(terminal.output, '↩'; keep=true) -cread(terminal) = cread1(terminal) * cread1(terminal) -strip_ansi_escape_sequences(str) = replace(str, r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])" => "") -function read_from(terminal) - displayed = cread(terminal) - text = strip_ansi_escape_sequences(displayed) - return (displayed, text) -end - # For multi-call sites -module MultiCall +module Definitions fmulti(::Int32) = 1 fmulti(::Float32) = rand(Float32) fmulti(::Char) = 3 callfmulti(c) = fmulti(c[]) -end -const keys = Dict(:up => "\e[A", - :down => "\e[B", - :enter => '\r') - -macro with_try_stderr(out, expr) - quote - try - $(esc(expr)) - catch err - bt = catch_backtrace() - Base.display_error(stderr, err, bt) - #close($(esc(out))) - end +reduce_tuple(@nospecialize(ixs)) = ixs +function reduce_tuple(ixs::Tuple) + values = ntuple(length(ixs)) do i + @inline + reduce_tuple(ixs[i]) end + return prod(values) end - -function wait_for(task::Task, timeout = 10.0) - t0 = time() - while time() - t0 < timeout - istaskfailed(task) && return wait(task) - istaskdone(task) && return true - yield() - end - return false end -@testset "Terminal" begin - @test _Cthulhu.default_terminal() isa REPL.Terminals.TTYTerminal - colorize(active_option::Bool, c::Char) = _Cthulhu.stringify() do io - active_option ? printstyled(io, c; bold=true, color=:green) : printstyled(io, c; color=:red) - end - - colorize(s::AbstractString; color::Symbol = :cyan) = _Cthulhu.stringify() do io - printstyled(io, s; color) - end - # Write a file that we track with Revise. Creating it programmatically allows us to rewrite it with - # different content - revisedfile = tempname() - open(revisedfile, "w") do io - println(io, - """ +if is_compiler_loaded() + @eval using Revise + Revise.track(Base) # get the `@info` log now, to avoid polluting test outputs later + revised_file = tempname() * ".jl" + open(revised_file, "w+") do io + println(io, """ + module Sandbox function simplef(a, b) z = a * a return z + b end + end # module """) end - includet(@__MODULE__, revisedfile) + include(revised_file) + (; simplef) = Sandbox + Revise.track(Sandbox, revised_file) +else + @eval function simplef(a, b) + z = a * a + return z + b + end +end - # Copy the user's current settings and set up the defaults - CONFIG = deepcopy(_Cthulhu.CONFIG) - config = _Cthulhu.CthulhuConfig() - for fn in fieldnames(_Cthulhu.CthulhuConfig) - setfield!(_Cthulhu.CONFIG, fn, getfield(config, fn)) +@testset "Terminal" begin + @test Cthulhu.default_terminal() isa REPL.Terminals.TTYTerminal + colorize(active_option::Bool, c::Char) = Cthulhu.stringify() do io + active_option ? printstyled(io, c; bold=true, color=:green) : printstyled(io, c; color=:red) + end + + colorize(s::AbstractString; color::Symbol = :cyan) = Cthulhu.stringify() do io + printstyled(io, s; color) end - try - terminal = FakeTerminal() - task = @async @with_try_stderr terminal.output descend(simplef, Tuple{Float32, Int32}; annotate_source=false, interruptexc=false, iswarn=false, terminal) - - displayed, text = read_from(terminal) - @test occursin("simplef(a, b)", text) - @test occursin(r"Base\.mul_float\(.*, .*\)::Float32", text) - @test occursin('[' * colorize(true, 'o') * "]ptimize", displayed) - @test occursin('[' * colorize(true, 'T') * "]yped", displayed) - @test occursin('\n' * colorize("Select a call to descend into or ↩ to ascend. [q]uit. [b]ookmark."; color = :blue), displayed) # beginning of the line - @test occursin('•', text) - write(terminal, 'o') # switch to unoptimized - displayed, text = read_from(terminal) - @test occursin("simplef(a, b)", text) + Cthulhu.CONFIG = Cthulhu.CthulhuConfig(; menu_options = (; pagesize = 10000)) + + terminal = VirtualTerminal() + harness = @run terminal descend(simplef, Tuple{Float32, Int32}; view=:typed, optimize=true, iswarn=false, terminal) + + displayed, text = read_next(harness) + @test occursin("simplef(a, b)", text) + @test occursin(r"Base\.mul_float\(.*, .*\)::Float32", text) + @test occursin('[' * colorize(true, 'o') * "]ptimize", displayed) + @test occursin('[' * colorize(true, 'T') * "]yped", displayed) + @test occursin('\n' * colorize("Select a call to descend into or ↩ to ascend."; color = :blue), displayed) # beginning of the line + @test occursin('•', text) + write(terminal, 'o') # switch to unoptimized + displayed, text = read_next(harness) + @test occursin("simplef(a, b)", text) + if @isdefined(Revise) @test occursin("::Const(*)", text) - @test occursin("(z = (%1)(a, a))", text) - @test occursin('[' * colorize(false, 'o') * "]ptimize", displayed) - @test occursin('\n' * colorize("Select a call to descend into or ↩ to ascend. [q]uit. [b]ookmark."; color = :blue), displayed) # beginning of the line - @test occursin("• %2 = *(::Float32,::Float32)::Float32", text) - - # Call selection - write(terminal, keys[:down]) - write(terminal, keys[:enter]) - cread1(terminal) - displayed, text = read_from(terminal) - @test occursin(r"• %\d = promote\(::Float32,::Int32\)::Tuple{Float32, Float32}", text) - write(terminal, keys[:up]) - write(terminal, keys[:enter]) - cread1(terminal) - displayed, text = read_from(terminal) - @test occursin("• %2 = *(::Float32,::Float32)::Float32", text) # back to where we started - write(terminal, 'o') # back to optimized - displayed, text = read_from(terminal) - @test !occursin("Variables", text) - @test occursin(r"Base\.mul_float\(.*, .*\)::Float32", text) - write(terminal, 'i') # show inline costs - displayed, text = read_from(terminal) - @test occursin(r"\d %\d = intrinsic Base\.mul_float", text) - @test occursin('[' * colorize(true, 'i') * "]nlining costs", displayed) - write(terminal, 'i') # hide inline costs - displayed, text = read_from(terminal) - write(terminal, 'o') - displayed, text = read_from(terminal) - @test occursin("Variables", text) - write(terminal, 'w') # unoptimized + warntype - displayed, text = read_from(terminal) - @test occursin("Variables", text) - @test occursin(r"z.*::Float32", text) - @test occursin("Body", text) - @test occursin('[' * colorize(true, 'w') * "]arn", displayed) - @test occursin('\n' * colorize("Select a call to descend into or ↩ to ascend. [q]uit. [b]ookmark."; color = :blue), displayed) - @test occursin("• %2 = *(::Float32,::Float32)::Float32", text) - - # Source view - write(terminal, 'S') - displayed, text = read_from(terminal) - @test occursin("z\e[36m::Float32\e[39m = (a\e[36m::Float32\e[39m * a\e[36m::Float32\e[39m)\e[36m::Float32\e[39m", displayed) - @test occursin('[' * colorize(true, 'S') * "]ource", displayed) - write(terminal, 's'); cread(terminal) # turn on syntax highlighting - write(terminal, 'S') # refresh source code - displayed, text = read_from(terminal) - @test occursin("simplef", text) - @test occursin("\u001B", first(split(displayed, '\n'))) - @test occursin('[' * colorize(true, 's') * "]yntax", displayed) - write(terminal, 's'); cread(terminal) # turn off syntax highlighting - - # Back to typed code - write(terminal, 'T'); cread(terminal) - write(terminal, 'o') - displayed, text = read_from(terminal) - @test occursin('[' * colorize(true, 'T') * "]yped", displayed) - - # AST view - write(terminal, 'A') - displayed, text = read_from(terminal) + end + @test occursin("(z = (%1)(a, a))", text) + @test occursin('[' * colorize(false, 'o') * "]ptimize", displayed) + @test occursin('\n' * colorize("Select a call to descend into or ↩ to ascend."; color = :blue), displayed) # beginning of the line + @test occursin("• %2 = *(::Float32,::Float32)::Float32", text) + + # Call selection + write(terminal, :down) + skip_delimiter(harness) + write(terminal, :enter) + displayed, text = read_next(harness) + @test occursin(r"• %\d = promote\(::Float32,::Int32\)::Tuple{Float32, Float32}", text) + write(terminal, :up) + skip_delimiter(harness) + write(terminal, :enter) + displayed, text = read_next(harness) + @test occursin("• %2 = *(::Float32,::Float32)::Float32", text) # back to where we started + write(terminal, 'o') # back to optimized + displayed, text = read_next(harness) + @test !occursin("Variables", text) + @test occursin(r"Base\.mul_float\(.*, .*\)::Float32", text) + write(terminal, 'i') # show inline costs + displayed, text = read_next(harness) + @test occursin(r"\d %\d = intrinsic Base\.mul_float", text) + @test occursin('[' * colorize(true, 'i') * "]nlining costs", displayed) + write(terminal, 'i') # hide inline costs + displayed, text = read_next(harness) + write(terminal, 'o') + displayed, text = read_next(harness) + @test occursin("Variables", text) + write(terminal, 'w') # unoptimized + warntype + displayed, text = read_next(harness) + @test occursin("Variables", text) + @test occursin(r"z.*::Float32", text) + @test occursin("Body", text) + @test occursin('[' * colorize(true, 'w') * "]arn", displayed) + @test occursin('\n' * colorize("Select a call to descend into or ↩ to ascend."; color = :blue), displayed) + @test occursin("• %2 = *(::Float32,::Float32)::Float32", text) + + # Source view + write(terminal, 'S') + displayed, text = read_next(harness) + # @test occursin("z\e[36m::Float32\e[39m = (a\e[36m::Float32\e[39m * a\e[36m::Float32\e[39m)\e[36m::Float32\e[39m", displayed) + @test occursin('[' * colorize(true, 'S') * "]ource", displayed) + write(terminal, 's') # turn on syntax highlighting + displayed, text = read_next(harness) + @test occursin("simplef", text) + @test occursin("\u001B", first(split(displayed, '\n'))) + @test occursin('[' * colorize(true, 's') * "]yntax", displayed) + write(terminal, 's'); read_next(harness) # turn off syntax highlighting + + # Back to typed code + write(terminal, 'T'); read_next(harness) + write(terminal, 'o') + displayed, text = read_next(harness) + @test occursin('[' * colorize(true, 'T') * "]yped", displayed) + + # AST view + write(terminal, 'A') + displayed, text = read_next(harness) + if @isdefined(Revise) @test occursin("Symbol simplef", text) @test occursin("Symbol call", text) - @test occursin('[' * colorize(true, 'A') * "]ST", displayed) - - # LLVM view - write(terminal, 'L') - displayed, text = read_from(terminal) - @test occursin(r"sitofp i(64|32)", text) - @test occursin("fadd float", text) - @test occursin("┌ @ promotion.jl", text) # debug info on by default - @test occursin('[' * colorize(true, 'L') * "]LVM", displayed) - # turn off debug info - write(terminal, 'd'); cread(terminal) - write(terminal, 'd'); cread(terminal) - write(terminal, 'L') - displayed, text = read_from(terminal) - @test occursin("[d]ebuginfo", displayed) - @test !occursin("┌ @ promotion.jl", text) - - # Native code view - write(terminal, 'N') - displayed, text = read_from(terminal) - @test occursin("retq", text) - @test occursin('[' * colorize(true, 'N') * "]ative", displayed) - # Typed view (by selector) - write(terminal, 'T') - displayed, text = read_from(terminal) - @test occursin("Base.mul_float(a, a)::Float32", text) - @test occursin('[' * colorize(true, 'T') * "]yped", displayed) - - # Parameter dumping - write(terminal, 'P') - displayed, text = read_from(terminal) - - # Revise + end + @test occursin('[' * colorize(true, 'A') * "]ST", displayed) + + # LLVM view + write(terminal, 'L') + displayed, text = read_next(harness) + @test occursin(r"sitofp i(64|32)", text) + @test occursin("fadd float", text) + @test occursin("┌ @ promotion.jl", text) # debug info on by default + @test occursin('[' * colorize(true, 'L') * "]LVM", displayed) + # turn off debug info + write(terminal, 'd'); read_next(harness) + write(terminal, 'd'); read_next(harness) + write(terminal, 'L') + displayed, text = read_next(harness) + @test occursin('[' * colorize(false, 'd') * "]ebuginfo", displayed) + @test !occursin("┌ @ promotion.jl", text) + + # Native code view + write(terminal, 'N') + displayed, text = read_next(harness) + @test occursin("retq", text) + @test occursin('[' * colorize(true, 'N') * "]ative", displayed) + # Typed view (by selector) + write(terminal, 'T') + displayed, text = read_next(harness) + @test occursin("Base.mul_float(a, a)::Float32", text) + @test occursin('[' * colorize(true, 'T') * "]yped", displayed) + + # Parameter dumping + write(terminal, 'P') + displayed, text = read_next(harness) + + if @isdefined(Revise) # Use delays to ensure unambiguous differences in time stamps # (macOS is particularly sensitive) and execution of @async processes sleep(0.1) - open(revisedfile, "w") do io + open(revised_file, "w") do io println(io, """ function simplef(a, b) @@ -206,166 +190,213 @@ end end sleep(0.1) write(terminal, 'R') - sleep(0.1) - write(terminal, 'T'); cread(terminal) - displayed, text = read_from(terminal) - @test_broken occursin("z = a * b", displayed) - write(terminal, 'q') - readavailable(terminal.output) - @assert wait_for(task) - finalize(terminal) - - # Multicall & iswarn=true - terminal = FakeTerminal() - task = @async @with_try_stderr output descend_code_warntype(MultiCall.callfmulti, Tuple{Any}; annotate_source=false, interruptexc=false, optimize=false, terminal) - - displayed, text = read_from(terminal) - @test occursin("\nBody", text) - @test occursin("\e[1m::Union{Float32, $Int}\e[22m\e[39m", displayed) - @test occursin("Base.getindex(c)\e[91m\e[1m::Any\e[22m\e[39m", displayed) - warncolor = if _Cthulhu.is_expected_union(Union{Float32, Int64}) - Base.text_colors[Base.warn_color()] - else - Base.text_colors[Base.error_color()] - end - @test occursin("$(warncolor)%\e[39m3 = call → fmulti(::Any)::Union{Float32, Int64}", displayed) - write(terminal, keys[:down]) - write(terminal, keys[:enter]) - displayed, text = read_from(terminal) - @test occursin("%3 = fmulti(::Int32)::Union{Float32, $Int}", displayed) - @test occursin("%3 = fmulti(::Float32)::Union{Float32, $Int}", displayed) - @test occursin("%3 = fmulti(::Char)::Union{Float32, $Int}", displayed) - write(terminal, 'q') - @assert wait_for(task) - finalize(terminal) - - # Tasks (see the special handling in `_descend`) - task_function() = @sync @async show(io, "Hello") - terminal = FakeTerminal() - task = @async @with_try_stderr output @descend terminal=terminal annotate_source=false task_function() - - displayed, text = read_from(terminal) - @test occursin(r"• %\d\d = task", text) - write(terminal, keys[:enter]) - displayed, text = read_from(terminal) - @test occursin("call show(::IO,::String)", text) - write(terminal, 'q') - @assert wait_for(task) - finalize(terminal) - - # descend with MethodInstances - mi = _Cthulhu.get_specialization(MultiCall.callfmulti, Tuple{typeof(Ref{Any}(1))}) - terminal = FakeTerminal() - task = @async @with_try_stderr output descend(mi; annotate_source=false, optimize=false, terminal) - - displayed, text = read_from(terminal) - @test occursin("fmulti(::Any)", text) - write(terminal, 'q') - @assert wait_for(task) - finalize(terminal) - - terminal = FakeTerminal() - task = @async @with_try_stderr output descend_code_warntype(mi; annotate_source=false, interruptexc=false, optimize=false, terminal) - - displayed, text = read_from(terminal) - @test occursin("Base.getindex(c)\e[91m\e[1m::Any\e[22m\e[39m", displayed) - write(terminal, 'q') - @assert wait_for(task) - finalize(terminal) - - # Fallback to typed code - terminal = FakeTerminal() - task = @async @with_try_stderr output redirect_stderr(terminal.error) do - descend(x -> [x], (Int,); annotate_source=true, interruptexc=false, optimize=false, terminal) - end + displayed, text = read_next(harness) + # FIXME: Sources are revised, but Cthulhu still displays the unrevised code. + @test_broken occursin("Base.mul_float(a, b)::Float32", text) + write(terminal, 'S') + displayed, text = read_next(harness) + @test_broken occursin("z::Float32 = (a::Float32 * b::Int32)", text) + # FIXME: Replace this test by the one marked as broken just above when fixed. + @test occursin("z::Float32 = (a::Float32 * b)", text) + @test end_terminal_session(harness) + end - displayed, text = read_from(terminal) - warnings = String(readavailable(terminal.error)) - @test occursin("couldn't retrieve source", warnings) + # Multicall & iswarn=true + terminal = VirtualTerminal() + harness = @run terminal descend_code_warntype(Definitions.callfmulti, Tuple{Any}; view=:typed, optimize=false, terminal) + + displayed, text = read_next(harness) + @test occursin("\nBody", text) + @test occursin("\e[1m::Union{Float32, $Int}\e[22m\e[39m", displayed) + @test occursin("Base.getindex(c)\e[91m\e[1m::Any\e[22m\e[39m", displayed) + warncolor = if Cthulhu.is_expected_union(Union{Float32, Int64}) + Base.text_colors[Base.warn_color()] + else + Base.text_colors[Base.error_color()] + end + @test occursin("$(warncolor)%\e[39m3 = call → fmulti(::Any)::Union{Float32, Int64}", displayed) + write(terminal, :down) + write(terminal, :enter) + displayed, text = read_next(harness) + @test occursin("%3 = fmulti(::Int32)::Union{Float32, $Int}", displayed) + @test occursin("%3 = fmulti(::Float32)::Union{Float32, $Int}", displayed) + @test occursin("%3 = fmulti(::Char)::Union{Float32, $Int}", displayed) + @test end_terminal_session(harness) + + # Tasks (see the special handling in `_descend`) + task_function() = @sync @async show(io, "Hello") + terminal = VirtualTerminal() + harness = @run terminal @descend terminal=terminal view=:typed optimize=true task_function() + + displayed, text = read_next(harness) + @test occursin(r"• %\d\d = task", text) + write(terminal, :enter) + displayed, text = read_next(harness) + @test occursin("call show(::IO,::String)", text) + @test end_terminal_session(harness) + + # descend with MethodInstances + mi = _Cthulhu.get_specialization(Definitions.callfmulti, Tuple{typeof(Ref{Any}(1))}) + terminal = VirtualTerminal() + harness = @run terminal descend(mi; view=:typed, optimize=false, terminal) + + displayed, text = read_next(harness) + @test occursin("fmulti(::Any)", text) + @test end_terminal_session(harness) + + terminal = VirtualTerminal() + harness = @run terminal descend_code_warntype(mi; view=:typed, optimize=false, terminal) + + displayed, text = read_next(harness) + @test occursin("Base.getindex(c)\e[91m\e[1m::Any\e[22m\e[39m", displayed) + @test end_terminal_session(harness) + + + # Fallback to typed code + @test_logs (:warn, r"couldn't retrieve source") match_mode=:any begin + terminal = VirtualTerminal() + harness = @run terminal descend(x -> [x], (Int,); view=:source, optimize=false, terminal) + + displayed, text = read_next(harness) @test occursin("dynamic Base.vect(x)", text) @test occursin("(::$Int)::Vector{$Int}", text) - write(terminal, 'q') - @assert wait_for(task) - finalize(terminal) - - # `ascend` - @noinline inner3(x) = 3x - @inline inner2(x) = 2*inner3(x) - inner1(x) = -1*inner2(x) - inner1(0x0123) - mi = _Cthulhu.get_specialization(inner3, Tuple{UInt16}) - terminal = FakeTerminal() - task = @async @with_try_stderr output redirect_stderr(terminal.error) do - ascend(terminal, mi; pagesize=11) + @test end_terminal_session(harness) + end + + @testset "Source discarded because of LimitedAccuracy (#642)" begin + @test_logs (:warn, r"Inference decided not to cache") match_mode=:any begin + terminal = VirtualTerminal() + harness = @run terminal descend(Definitions.reduce_tuple, (typeof(((1, (1, 1)), 1)),); terminal) + displayed, text = read_next(harness) + write(terminal, 'T') + displayed, text = read_next(harness) + write(terminal, :down) + skip_delimiter(harness) + write(terminal, :down) + skip_delimiter(harness) + write(terminal, :enter) + displayed, text = read_next(harness) + write(terminal, 'o') + @test end_terminal_session(harness) end + end + + @testset "Code instances from [semi-]concrete evaluation (#609, #610)" begin + U = UpperTriangular{Int64, Matrix{Int64}} + S = Symmetric{Int64, Matrix{Int64}} + terminal = VirtualTerminal() + harness = @run terminal descend(*, (U, S); terminal) + displayed, text = read_next(harness) + write(terminal, :enter) + displayed, text = read_next(harness) + write(terminal, :up) + skip_delimiter(harness) + write(terminal, :up) + skip_delimiter(harness) + write(terminal, :enter) + displayed, text = read_next(harness) + write(terminal, :enter) + displayed, text = read_next(harness) + write(terminal, :enter) + displayed, text = read_next(harness) + VERSION < v"1.13-" && @test contains(text, "_trimul!") + @test end_terminal_session(harness) + end - write(terminal, keys[:down]) - write(terminal, keys[:enter]) - write(terminal, keys[:down]) - write(terminal, keys[:enter]) - displayed, text = read_from(terminal) - lines = split(text, '\n') - i = first(findfirst("inner3", lines[2])) - @test first(findfirst("inner2", lines[3])) == i + 2 - from = findfirst(==("Open an editor at a possible caller of"), lines) - @test isa(from, Int) - @test occursin("inner2", lines[from + 3]) - write(terminal, 'q') - write(terminal, 'q') - @assert wait_for(task) - finalize(terminal) - - # With backtraces - bt = try sum([]); catch; catch_backtrace(); end - terminal = FakeTerminal() - task = @async @with_try_stderr output ascend(terminal, bt) - write(terminal, keys[:enter]) - displayed, text = read_from(terminal) - @test occursin("Choose a call for analysis (q to quit):", text) - @test occursin("mapreduce_empty", text) - @test occursin("reduce_empty(op::Function, T::Type)", text) || occursin("reduce_empty(::typeof(+), ::Type{Any})", text) - @test occursin("Select a call to descend into or ↩ to ascend.", text) - write(terminal, 'q') - write(terminal, 'q') - @assert wait_for(task) - finalize(terminal) - - # With stacktraces - st = try; sum([]); catch; stacktrace(catch_backtrace()); end - terminal = FakeTerminal() - task = @async @with_try_stderr output ascend(terminal, st) - write(terminal, keys[:enter]) - displayed, text = read_from(terminal) - @test occursin("Choose a call for analysis (q to quit):", text) - @test occursin("mapreduce_empty", text) - @test occursin("reduce_empty(op::Function, T::Type)", text) || occursin("reduce_empty(::typeof(+), ::Type{Any})", text) - @test occursin("Select a call to descend into or ↩ to ascend.", text) - write(terminal, 'q') - write(terminal, 'q') - @assert wait_for(task) - finalize(terminal) - - # With ExceptionStack (e.g., REPL's global `err` variable) - exception_stack = try; sum([]); catch e; Base.ExceptionStack([(exception=e, backtrace=stacktrace(catch_backtrace()))]); end - terminal = FakeTerminal() - task = @async @with_try_stderr output ascend(terminal, exception_stack) - write(terminal, keys[:enter]) - displayed, text = read_from(terminal) - @test occursin("Choose a call for analysis (q to quit):", text) - @test occursin("mapreduce_empty", text) - @test occursin("reduce_empty(op::Function, T::Type)", text) || occursin("reduce_empty(::typeof(+), ::Type{Any})", text) - @test occursin("Select a call to descend into or ↩ to ascend.", text) - write(terminal, 'q') - write(terminal, 'q') - @assert wait_for(task) - finalize(terminal) - finally - # Restore the previous settings - for fn in fieldnames(_Cthulhu.CthulhuConfig) - setfield!(_Cthulhu.CONFIG, fn, getfield(CONFIG, fn)) + # `ascend` + @noinline inner3(x) = 3x + @inline inner2(x) = 2*inner3(x) + inner1(x) = -1*inner2(x) + inner1(0x0123) + mi = _Cthulhu.get_specialization(inner3, Tuple{UInt16}) + terminal = VirtualTerminal() + harness = @run terminal ascend(terminal, mi; pagesize=11) + + write(terminal, :down) + write(terminal, :enter) + write(terminal, :down) + write(terminal, :enter) + displayed, text = read_next(harness) + lines = split(text, '\n') + i = first(findfirst("inner3", lines[2])) + @test first(findfirst("inner2", lines[3])) == i + 2 + from = findfirst(==("Open an editor at a possible caller of"), lines) + @test isa(from, Int) + @test occursin("inner2", lines[from + 3]) + write(terminal, 'q') + @test end_terminal_session(harness) + + # With backtraces + bt = try sum([]); catch; catch_backtrace(); end + terminal = VirtualTerminal() + harness = @run terminal ascend(terminal, bt) + write(terminal, :enter) + displayed, text = read_next(harness) + @test occursin("Choose a call for analysis (q to quit):", text) + @test occursin("mapreduce_empty", text) + @test occursin("reduce_empty(op::Function, T::Type)", text) || occursin("reduce_empty(::typeof(+), ::Type{Any})", text) + @test occursin("Select a call to descend into or ↩ to ascend.", text) + write(terminal, 'q') + @test end_terminal_session(harness) + + # With stacktraces + st = try; sum([]); catch; stacktrace(catch_backtrace()); end + terminal = VirtualTerminal() + harness = @run terminal ascend(terminal, st) + write(terminal, :enter) + displayed, text = read_next(harness) + @test occursin("Choose a call for analysis (q to quit):", text) + @test occursin("mapreduce_empty", text) + @test occursin("reduce_empty(op::Function, T::Type)", text) || occursin("reduce_empty(::typeof(+), ::Type{Any})", text) + @test occursin("Select a call to descend into or ↩ to ascend.", text) + write(terminal, 'q') + @test end_terminal_session(harness) + + # With ExceptionStack (e.g., REPL's global `err` variable) + exception_stack = try; sum([]); catch e; Base.ExceptionStack([(exception=e, backtrace=stacktrace(catch_backtrace()))]); end + terminal = VirtualTerminal() + harness = @run terminal ascend(terminal, exception_stack) + write(terminal, :enter) + displayed, text = read_next(harness) + @test occursin("Choose a call for analysis (q to quit):", text) + @test occursin("mapreduce_empty", text) + @test occursin("reduce_empty(op::Function, T::Type)", text) || occursin("reduce_empty(::typeof(+), ::Type{Any})", text) + @test occursin("Select a call to descend into or ↩ to ascend.", text) + write(terminal, 'q') + @test end_terminal_session(harness) + + @testset "Bookmarks" begin + bookmarks = Cthulhu.BOOKMARKS + n = length(bookmarks) + + @test_logs (:info, r"`descend` state was saved for later use") match_mode=:any begin + terminal = VirtualTerminal() + harness = @run terminal descend(exp, (Int,); view=:typed, optimize=true, terminal) + write(terminal, :enter) + write(terminal, 'b') # should push a bookmark to `BOOKMARKS` + write(terminal, 'q') + @test end_terminal_session(harness) end - rm(revisedfile) + + @test length(bookmarks) == n + 1 + bookmark = pop!(bookmarks) + mi = Cthulhu.get_mi(bookmark.ci) + method = mi.def + @test method === which(exp, (Float64,)) + @test bookmark.config.view === :typed + @test bookmark.config.optimize === true + + terminal = VirtualTerminal() + @test_logs (:info, r"`descend` state was saved for later use") match_mode=:any begin + harness = @run terminal Cthulhu.descend_with_error_handling(bookmark; view=:llvm, terminal) + write(terminal, 'b') # should push a bookmark to `BOOKMARKS` + write(terminal, 'q') + @test end_terminal_session(harness) + end + + bookmark = pop!(bookmarks) + @test bookmark.config.view === :llvm end -end +end; end # module test_terminal