diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index b3ba031e..e2e67765 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -14,15 +14,19 @@ jobs: matrix: version: - '1' - - '1.9.0' + - '1.10.0' + - '1.11-nightly' - 'nightly' - - '1.8.5' os: - ubuntu-latest - macOS-latest - windows-latest arch: - x64 + - x86 + exclude: + - os: macOS-latest + arch: x86 steps: - uses: actions/checkout@v2 - uses: julia-actions/setup-julia@v1 @@ -39,17 +43,14 @@ jobs: ${{ runner.os }}-test-${{ env.cache-name }}- ${{ runner.os }}-test- ${{ runner.os }}- - - name: dev TypedSyntax - shell: julia --color=yes --project=. {0} # this is necessary for the next command to work on Windows + - name: dev TypedSyntax # dev TypedSyntax (a subdir package) since the latest version may not be registered yet + shell: julia --color=yes --project=. {0} # this is necessary for the next command to work on Windows run: 'using Pkg; Pkg.develop(path=joinpath(pwd(), "TypedSyntax"))' - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 with: check_bounds: 'auto' coverage: 'false' - - name: TypedSyntax # run the tests of TypedSyntax (a subdir package) - if: ${{ matrix.os == 'ubuntu-latest' }} - run: julia --project=TypedSyntax -e 'using Pkg; Pkg.test(coverage=true)' # - name: Coverage off # `empty_func` test doesn't work as intended with `coverage=true` # if: ${{ matrix.os == 'ubuntu-latest' }} # run: julia --project -e 'using Pkg; Pkg.test("Cthulhu"; coverage=false)' diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml index f49313b6..54d19dd7 100644 --- a/.github/workflows/TagBot.yml +++ b/.github/workflows/TagBot.yml @@ -4,6 +4,22 @@ on: types: - created workflow_dispatch: + inputs: + lookback: + default: 100 +permissions: + actions: read + checks: read + contents: write + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + security-events: read + statuses: read jobs: TagBot: if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' diff --git a/.github/workflows/TypedSyntaxCI.yml b/.github/workflows/TypedSyntaxCI.yml index 59a351a3..21f88bb4 100644 --- a/.github/workflows/TypedSyntaxCI.yml +++ b/.github/workflows/TypedSyntaxCI.yml @@ -13,11 +13,15 @@ jobs: fail-fast: false matrix: version: - - '1.6' # test LTS versions that are not supported by Cthulhu + - '1' + - '1.10.0' + - '1.11-nightly' + - 'nightly' os: - ubuntu-latest arch: - x64 + - x86 steps: - uses: actions/checkout@v2 - uses: julia-actions/setup-julia@v1 diff --git a/.gitignore b/.gitignore index 9e1c0653..44790698 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ Manifest.toml +Manifest-*.toml *.jl.cov *.jl.*.cov *.jl.mem diff --git a/Project.toml b/Project.toml index 3e71217c..b21ad157 100644 --- a/Project.toml +++ b/Project.toml @@ -1,16 +1,16 @@ name = "Cthulhu" uuid = "f68482b8-f384-11e8-15f7-abe071a5a75f" authors = ["Valentin Churavy and contributors"] -version = "2.9.1" +version = "2.14.0" [deps] CodeTracking = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2" FoldingTrees = "1eca21be-9b9b-4ed8-839a-6d8ae26b1781" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" JuliaSyntax = "70703baa-626e-46a2-a12c-08ffd08c73b4" +PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" Preferences = "21216c6a-2e73-6563-6e65-726566657250" REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" -PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" TypedSyntax = "d265eb64-f81a-44ad-a842-4247ee1503de" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" Unicode = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" @@ -19,12 +19,16 @@ WidthLimitedIO = "b8c1c048-cf81-46c6-9da0-18c1d99e41f2" [compat] CodeTracking = "0.5, 1" FoldingTrees = "1" +InteractiveUtils = "1.9" JuliaSyntax = "0.4" -Preferences = "1" PrecompileTools = "1" -TypedSyntax = "1.1.7" +Preferences = "1" +REPL = "1.9" +TypedSyntax = "1.3.0" +UUIDs = "1.9" +Unicode = "1.9" WidthLimitedIO = "1" -julia = "1.8.5" +julia = "1.10.0" [extras] DeepDiffs = "ab62b9b5-e342-54a8-a765-a90f495de1a6" diff --git a/README.md b/README.md index 3725b0a1..1283c3fc 100644 --- a/README.md +++ b/README.md @@ -274,8 +274,6 @@ Enter `@descend bar(x, y, z)` you can see that, for `foo(4)`, the types within ` ## Viewing the internal representation of Julia code -Anyone using Cthulhu to investigate the behavior of Julia's compiler will -prefer to examine the While Cthulhu tries to place type-annotations on the source code, this obscures detail and can occassionally go awry (see details [here](TypedSyntax/README.md)). For anyone who needs more direct insight, it can be better to look directly at Julia's diff --git a/TypedSyntax/Project.toml b/TypedSyntax/Project.toml index 534e80db..d41a4b42 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.2.1" +version = "1.4.0" [deps] CodeTracking = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2" @@ -10,7 +10,7 @@ JuliaSyntax = "70703baa-626e-46a2-a12c-08ffd08c73b4" [compat] CodeTracking = "1.3" JuliaSyntax = "0.4" -julia = "1.6" +julia = "1.10.0" [extras] Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" diff --git a/TypedSyntax/src/TypedSyntax.jl b/TypedSyntax/src/TypedSyntax.jl index b9f8e4ed..0841deb0 100644 --- a/TypedSyntax/src/TypedSyntax.jl +++ b/TypedSyntax/src/TypedSyntax.jl @@ -1,7 +1,6 @@ module TypedSyntax using Core: CodeInfo, MethodInstance, SlotNumber, SSAValue -using Core.Compiler: TypedSlot using JuliaSyntax: JuliaSyntax, AbstractSyntaxData, SyntaxData, SyntaxNode, GreenNode, AbstractSyntaxNode, SyntaxHead, SourceFile, head, kind, child, children, haschildren, untokenize, first_byte, last_byte, source_line, source_location, sourcetext, @K_str, @KSet_str, is_infix_op_call, is_prefix_op_call, is_prec_assignment, is_operator, is_literal @@ -11,6 +10,7 @@ using CodeTracking export TypedSyntaxNode include("node.jl") +include("vscode.jl") include("show.jl") end diff --git a/TypedSyntax/src/node.jl b/TypedSyntax/src/node.jl index f843825a..c83f07e0 100644 --- a/TypedSyntax/src/node.jl +++ b/TypedSyntax/src/node.jl @@ -1,4 +1,3 @@ - mutable struct TypedSyntaxData <: AbstractSyntaxData source::SourceFile typedsource::CodeInfo @@ -13,29 +12,37 @@ TypedSyntaxData(sd::SyntaxData, src::CodeInfo, typ=nothing) = TypedSyntaxData(sd const TypedSyntaxNode = JuliaSyntax.TreeNode{TypedSyntaxData} const MaybeTypedSyntaxNode = Union{SyntaxNode,TypedSyntaxNode} +@static if VERSION ≥ v"1.11.0-DEV.337" + const SlotType = Core.SlotNumber +else + const SlotType = Union{Core.SlotNumber, Core.Compiler.TypedSlot} +end + struct NoDefaultValue end const no_default_value = NoDefaultValue() # These are TypedSyntaxNode constructor helpers # Call these directly if you want both the TypedSyntaxNode and the `mappings` list, # where `mappings[i]` corresponds to the list of nodes matching `(src::CodeInfo).code[i]`. -function tsn_and_mappings(@nospecialize(f), @nospecialize(t); kwargs...) - m = which(f, t) - src, rt = getsrc(f, t) - tsn_and_mappings(m, src, rt; kwargs...) +function tsn_and_mappings(@nospecialize(f), @nospecialize(tt=Base.default_tt(f)); kwargs...) + inferred_result = get_inferred_result(f, tt) + return tsn_and_mappings(inferred_result.mi, inferred_result.src, inferred_result.rt; kwargs...) end -function tsn_and_mappings(m::Method, src::CodeInfo, @nospecialize(rt); warn::Bool=true, strip_macros::Bool=false, kwargs...) +function tsn_and_mappings(mi::MethodInstance, src::CodeInfo, @nospecialize(rt); warn::Bool=true, strip_macros::Bool=false, kwargs...) + m = mi.def::Method def = definition(String, m) if isnothing(def) warn && @warn "couldn't retrieve source of $m" return nothing, nothing end - return tsn_and_mappings(m, src, rt, def...; warn, strip_macros, kwargs...) + return tsn_and_mappings(mi, src, rt, def...; warn, strip_macros, kwargs...) end -function tsn_and_mappings(m::Method, src::CodeInfo, @nospecialize(rt), sourcetext::AbstractString, lineno::Integer; warn::Bool=true, strip_macros::Bool=false, kwargs...) - rootnode = JuliaSyntax.parsestmt(SyntaxNode, sourcetext; filename=string(m.file), first_line=lineno, kwargs...) +function tsn_and_mappings(mi::MethodInstance, src::CodeInfo, @nospecialize(rt), sourcetext::AbstractString, lineno::Integer; warn::Bool=true, strip_macros::Bool=false, kwargs...) + m = mi.def::Method + filename = isnothing(functionloc(m)[1]) ? string(m.file) : functionloc(m)[1] + rootnode = JuliaSyntax.parsestmt(SyntaxNode, sourcetext; filename=filename, first_line=lineno, kwargs...) if strip_macros rootnode = get_function_def(rootnode) if !is_function_def(rootnode) @@ -44,28 +51,33 @@ function tsn_and_mappings(m::Method, src::CodeInfo, @nospecialize(rt), sourcetex end end Δline = lineno - m.line # offset from original line number (Revise) - mappings, symtyps = map_ssas_to_source(src, rootnode, Δline) + mappings, symtyps = map_ssas_to_source(src, mi, rootnode, Δline) node = TypedSyntaxNode(rootnode, src, mappings, symtyps) node.typ = rt return node, mappings end -TypedSyntaxNode(@nospecialize(f), @nospecialize(t); kwargs...) = tsn_and_mappings(f, t; kwargs...)[1] +TypedSyntaxNode(@nospecialize(f), @nospecialize(tt=Base.default_tt(f)); kwargs...) = tsn_and_mappings(f, tt; kwargs...)[1] function TypedSyntaxNode(mi::MethodInstance; kwargs...) - m = mi.def::Method - src, rt = getsrc(mi) - tsn_and_mappings(m, src, rt; kwargs...)[1] + src, rt = code_typed1_tsn(mi) + tsn_and_mappings(mi, src, rt; kwargs...)[1] +end + +function TypedSyntaxNode(rootnode::SyntaxNode, @nospecialize(f), @nospecialize(tt=Base.default_tt(f)); kwargs...) + inferred_result = get_inferred_result(f, tt) + TypedSyntaxNode(rootnode, inferred_result.src, inferred_result.mi; kwargs...) end -TypedSyntaxNode(rootnode::SyntaxNode, src::CodeInfo, Δline::Integer=0) = - TypedSyntaxNode(rootnode, src, map_ssas_to_source(src, rootnode, Δline)...) +TypedSyntaxNode(rootnode::SyntaxNode, src::CodeInfo, mi::MethodInstance, Δline::Integer=0) = + TypedSyntaxNode(rootnode, src, map_ssas_to_source(src, mi, rootnode, Δline)...) function TypedSyntaxNode(rootnode::SyntaxNode, src::CodeInfo, mappings, symtyps) # There may be ambiguous assignments back to the source; preserve just the unambiguous ones node2ssa = IdDict{SyntaxNode,Int}(only(list) => i for (i, list) in pairs(mappings) if length(list) == 1) # Copy `rootnode`, adding type annotations - trootnode = TypedSyntaxNode(nothing, nothing, TypedSyntaxData(rootnode.data::SyntaxData, src, gettyp(node2ssa, rootnode, src))) + typ = gettyp(node2ssa, rootnode, src) + trootnode = TypedSyntaxNode(nothing, nothing, TypedSyntaxData(rootnode.data::SyntaxData, src, typ)) addchildren!(trootnode, rootnode, src, node2ssa, symtyps, mappings) # Add argtyps to signature fnode = get_function_def(trootnode) @@ -179,12 +191,12 @@ function map_signature!(sig::TypedSyntaxNode, slotnames::Vector{Symbol}, slottyp kwdivider = 1 if havekws && slotnames[1] !== Symbol("#self#") kwdivider = findfirst(1:length(slotnames)) do i - slotnames[i] == Symbol("") && unwrapinternal(slottypes[i]) <: Function # this should be the parent function as an argument + slotnames[i] == Symbol("") && isa(unwrapinternal(slottypes[i]), Function) # this should be the parent function as an argument end if kwdivider === nothing kwdivider = 1 end - if length(slottypes) >= 2 && slotnames[2] == Symbol("") && (nt = unwrapinternal(slottypes[2])) <: NamedTuple + if length(slottypes) >= 2 && slotnames[2] == Symbol("") && (nt = unwrapinternal(slottypes[2]); isa(nt, Type)) && nt <: NamedTuple # Match kwargs argcontainer = children(last(children(sig))) offset = length(children(sig)) - 1 @@ -232,7 +244,7 @@ function map_signature!(sig::TypedSyntaxNode, slotnames::Vector{Symbol}, slottyp if kind(arg) == K"::" && length(children(arg)) == 2 arg = child(arg, 1) end - arg.typ = unwrapinternal(slottypes[idx]) + arg.typ = slottypes[idx] end # It's annoying to print the signature as `foo::typeof(foo)(a::Int)` @@ -264,7 +276,7 @@ function striparg(arg) end function unwrapinternal(@nospecialize(T)) - isa(T, Core.Const) && return Core.Typeof(T.val) + isa(T, Core.Const) && return T.val isa(T, Core.PartialStruct) && return T.typ return T end @@ -275,10 +287,10 @@ function gettyp(node2ssa, node, src) ssavaluetypes = src.ssavaluetypes::Vector{Any} if isa(stmt, Core.ReturnNode) arg = stmt.val - isa(arg, SSAValue) && return unwrapinternal(ssavaluetypes[arg.id]) - is_slot(arg) && return unwrapinternal((src.slottypes::Vector{Any})[arg.id]) + isa(arg, SSAValue) && return ssavaluetypes[arg.id] + is_slot(arg) && return (src.slottypes::Vector{Any})[arg.id] end - return unwrapinternal(ssavaluetypes[i]) + return ssavaluetypes[i] end Base.copy(tsd::TypedSyntaxData) = TypedSyntaxData(tsd.source, tsd.typedsource, tsd.raw, tsd.position, tsd.val, tsd.typ, tsd.runtime) @@ -298,17 +310,57 @@ function sparam_name(mi::MethodInstance, i::Int) return sig.var.name end -function getsrc(@nospecialize(f), @nospecialize(t)) - srcrts = code_typed(f, t; debuginfo=:source, optimize=false) - return only(srcrts) +@static if isdefined(Base, :method_instances) +using Base: method_instances +else +function method_instances(@nospecialize(f), @nospecialize(t), world::UInt) + tt = Base.signature_type(f, t) + results = Core.MethodInstance[] + # this make a better error message than the typeassert that follows + world == typemax(UInt) && error("code reflection cannot be used from generated functions") + for match in Base._methods_by_ftype(tt, -1, world)::Vector + instance = Core.Compiler.specialize_method(match) + push!(results, instance) + end + return results +end end -function getsrc(mi::MethodInstance) - cis = Base.code_typed_by_type(mi.specTypes; debuginfo=:source, optimize=false) - isempty(cis) && error("no applicable type-inferred code found for ", mi) - length(cis) == 1 || error("got $(length(cis)) possible type-inferred results for ", mi, - ", you may need a more specialized signature") - return cis[1]::Pair{CodeInfo} +struct InferredResult + mi::MethodInstance + src::CodeInfo + rt + InferredResult(mi::MethodInstance, src::CodeInfo, @nospecialize(rt)) = new(mi, src, rt) +end +function get_inferred_result(@nospecialize(f), @nospecialize(tt=Base.default_tt(f)), + world::UInt=Base.get_world_counter()) + mis = method_instances(f, tt, world) + if isempty(mis) + sig = sprint(Base.show_tuple_as_call, Symbol(""), Base.signature_type(f, tt)) + error("no applicable type-inferred code found for ", sig) + elseif length(mis) ≠ 1 + sig = sprint(Base.show_tuple_as_call, Symbol(""), Base.signature_type(f, tt)) + error("got $(length(mis)) possible type-inferred results for ", sig, + ", you may need a more specialized signature") + end + mi = only(mis) + return InferredResult(mi, code_typed1_tsn(mi)...) +end + +code_typed1_tsn(mi::MethodInstance) = code_typed1_by_method_instance(mi; optimize=false, debuginfo=:source) + +function code_typed1_by_method_instance(mi::MethodInstance; + optimize::Bool=true, + debuginfo::Symbol=:default, + world::UInt=Base.get_world_counter(), + interp::Core.Compiler.AbstractInterpreter=Core.Compiler.NativeInterpreter(world)) + (ccall(:jl_is_in_pure_context, Bool, ()) || world == typemax(UInt)) && + error("code reflection should not be used from generated functions") + debuginfo = Base.IRShow.debuginfo(debuginfo) + code, rt = Core.Compiler.typeinf_code(interp, mi.def::Method, mi.specTypes, mi.sparam_vals, optimize) + code isa CodeInfo || error("no code is available for ", mi) + debuginfo === :none && Base.remove_linenums!(code) + return Pair{CodeInfo,Any}(code, rt) end function is_function_def(node) # this is not `Base.is_function_def` @@ -388,11 +440,80 @@ function collect_symbol_nodes!(symlocs::AbstractDict, node) return symlocs end +## utility function to extract the line number at a particular program counter (ignoring inlining). +## return <= 0 if there is no line number change caused by this statement +@static if VERSION ≥ v"1.12.0-DEV.173" +function getline(lt::Core.DebugInfo, i::Int) + while true + codeloc = Base.IRShow.getdebugidx(lt, i) + line::Int = codeloc[1] + line < 0 && return 0 # broken or disabled debug info? + line == 0 && return 0 # no line number update (though maybe inlining changed) + ltnext = lt.linetable + if ltnext === nothing + return line + end + i = line + lt = ltnext + end +end +function getnextline(lt::Core.DebugInfo, i::Int, Δline) + while true + codeloc = Base.IRShow.getdebugidx(lt, i) + line::Int = codeloc[1] + line < 0 && return typemax(Int) # broken or disabled debug info? + if line == 0 + i += 1 + continue + end + ltnext = lt.linetable + if ltnext === nothing + break + end + i = line + lt = ltnext + end + # now that we have line i and a list of all lines with code on them lt + # find the next largest line number in this list greater than i, or return typemax(Int) + j = i+1 + currline = Base.IRShow.getdebugidx(lt, i)[1] + while j ≤ typemax(Int) + codeloc = Base.IRShow.getdebugidx(lt, j) + line::Int = codeloc[1] + line < 0 && break + if line == 0 || currline == line + j += 1 + else + return line + Δline + end + end + return typemax(Int) +end + +else # VERSION < v"1.12.0-DEV.173" +function getline(lt, j) + linfo = (j == 0 ? first(lt) : lt[j])::Core.LineInfoNode + linfo.inlined_at == 0 && return linfo.line + @assert linfo.method === Symbol("macro expansion") + linfo = lt[linfo.inlined_at]::Core.LineInfoNode + return linfo.line +end +function getnextline(lt, j, Δline) + j == 0 && return typemax(Int) + j += 1 + while j <= length(lt) + linfo = lt[j]::Core.LineInfoNode + linfo.inlined_at == 0 && return linfo.line + Δline + j += 1 + end + return typemax(Int) +end +end # @static if + # Main logic for mapping `src.code[i]` to node(s) in the SyntaxNode tree # Success: when we map it to a unique node # Δline is the (Revise) offset of the line number -function map_ssas_to_source(src::CodeInfo, rootnode::SyntaxNode, Δline::Int) - mi = src.parent::MethodInstance +function map_ssas_to_source(src::CodeInfo, mi::MethodInstance, rootnode::SyntaxNode, Δline::Int) slottypes = src.slottypes::Union{Nothing, Vector{Any}} have_slottypes = slottypes !== nothing ssavaluetypes = src.ssavaluetypes::Vector{Any} @@ -421,8 +542,13 @@ function map_ssas_to_source(src::CodeInfo, rootnode::SyntaxNode, Δline::Int) # Append (to `mapped`) all nodes in `targets` that are consistent with the line number of the `i`th stmt # (Essentially `copy!(mapped, filter(predicate, targets))`) function append_targets_for_line!(mapped#=::Vector{nodes}=#, i::Int, targets#=::Vector{nodes}=#) - j = src.codelocs[i] - lt = src.linetable::Vector{Any} + @static if VERSION ≥ v"1.12.0-DEV.173" + j = i + lt = src.debuginfo + else + j = src.codelocs[i] + lt = src.linetable::Vector + end start = getline(lt, j) + Δline stop = getnextline(lt, j, Δline) - 1 linerange = start : stop @@ -442,7 +568,7 @@ function map_ssas_to_source(src::CodeInfo, rootnode::SyntaxNode, Δline::Int) function get_targets(@nospecialize(arg)) return if is_slot(arg) # If `arg` is a variable, e.g., the `x` in `f(x)` - name = src.slotnames[(arg::Union{SlotNumber,TypedSlot}).id] + name = src.slotnames[(arg::SlotType).id] is_gensym(name) ? nothing : get(symlocs, symloc_key(name), nothing) # get(symlocs, src.slotnames[arg.id], nothing) # find all places this variable is used elseif isa(arg, GlobalRef) @@ -482,12 +608,15 @@ function map_ssas_to_source(src::CodeInfo, rootnode::SyntaxNode, Δline::Int) argmapping = typeof(rootnode)[] # temporary storage for (i, mapped, stmt) in zip(eachindex(mappings), mappings, src.code) empty!(argmapping) - if is_slot(stmt) || isa(stmt, SSAValue) + if is_slot(stmt) || isa(stmt, SSAValue) || isa(stmt, GlobalRef) append_targets_for_arg!(mapped, i, stmt) elseif isa(stmt, Core.ReturnNode) append_targets_for_line!(mapped, i, append_targets_for_arg!(argmapping, i, stmt.val)) elseif isa(stmt, Expr) - if stmt.head == :(=) && is_slot(stmt.args[1]) + targets = get_targets(stmt) + if targets !== nothing + append_targets_for_line!(mapped, i, targets) + elseif stmt.head == :(=) && is_slot(stmt.args[1]) # We defer setting up `symtyps` for the LHS because processing the RHS first might eliminate ambiguities # # Update `symtyps` for this assignment lhs = stmt.args[1] @@ -497,16 +626,14 @@ function map_ssas_to_source(src::CodeInfo, rootnode::SyntaxNode, Δline::Int) append_targets_for_arg!(mapped, i, stmt) filter_assignment_targets!(mapped, true) # match the RHS of assignments if length(mapped) == 1 - symtyps[only(mapped)] = unwrapinternal( - (is_slot(stmt) & have_slottypes) ? slottypes[(stmt::Union{SlotNumber,TypedSlot}).id] : + symtyps[only(mapped)] = (is_slot(stmt) & have_slottypes) ? slottypes[(stmt::SlotType).id] : isa(stmt, SSAValue) ? ssavaluetypes[stmt.id] : #=literal=#typeof(stmt) - ) end # Now try to assign types to the LHS of the assignment append_targets_for_arg!(argmapping, i, lhs) filter_assignment_targets!(argmapping, false) # match the LHS of assignments if length(argmapping) == 1 - T = unwrapinternal(ssavaluetypes[i]) + T = ssavaluetypes[i] symtyps[only(argmapping)] = T end empty!(argmapping) @@ -609,7 +736,7 @@ function map_ssas_to_source(src::CodeInfo, rootnode::SyntaxNode, Δline::Int) if isexpr(nextstmt, :call) f = nextstmt.args[1] if isa(f, GlobalRef) && f.mod == Base && f.name == :broadcasted - empty!(mapped) + # empty!(mapped) break elseif isa(f, GlobalRef) && f.mod == Base && f.name == :materialize && nextstmt.args[2] === SSAValue(i) push!(mappings[inext], node) @@ -625,7 +752,7 @@ function map_ssas_to_source(src::CodeInfo, rootnode::SyntaxNode, Δline::Int) # `node` or, for the LHS of a `slot = callexpr` statement, one that shares a parent with `node`. if stmt.head == :(=) && is_slot(stmt.args[1]) # Tag the LHS of this expression - arg = stmt.args[1]::Union{SlotNumber, TypedSlot} + arg = stmt.args[1]::SlotType sym = src.slotnames[arg.id] if !is_gensym(sym) lhsnode = node @@ -656,7 +783,7 @@ function map_ssas_to_source(src::CodeInfo, rootnode::SyntaxNode, Δline::Int) end for (arg, j) in argjs if is_slot(arg) - arg = arg::Union{SlotNumber, TypedSlot} + arg = arg::SlotType sym = src.slotnames[arg.id] itr = get(symlocs, symloc_key(sym), nothing) itr === nothing && continue @@ -664,14 +791,14 @@ function map_ssas_to_source(src::CodeInfo, rootnode::SyntaxNode, Δline::Int) haskey(symtyps, t) && continue if skipped_parent(t) == node is_prec_assignment(node) && t == child(node, 1) && continue - symtyps[t] = unwrapinternal(if j > 0 + symtyps[t] = if j > 0 ssavaluetypes[j] elseif have_slottypes # We failed to find it as an SSAValue, it must have type assigned at function entry slottypes[arg.id] else nothing - end) + end break end end @@ -730,7 +857,7 @@ function map_ssas_to_source(src::CodeInfo, rootnode::SyntaxNode, Δline::Int) end return mappings, symtyps end -map_ssas_to_source(src::CodeInfo, rootnode::SyntaxNode, Δline::Integer) = map_ssas_to_source(src, rootnode, Int(Δline)) +map_ssas_to_source(src::CodeInfo, mi::MethodInstance, rootnode::SyntaxNode, Δline::Integer) = map_ssas_to_source(src, mi, rootnode, Int(Δline)) function follow_back(src, arg) # Follow SSAValue backward to see if it maps back to a slot @@ -758,7 +885,7 @@ function is_indexed_iterate(arg) return arg.name == :indexed_iterate end -is_slot(@nospecialize(arg)) = isa(arg, SlotNumber) || isa(arg, TypedSlot) +is_slot(@nospecialize(arg)) = isa(arg, SlotType) is_src_literal(x) = isa(x, Integer) || isa(x, AbstractFloat) || isa(x, String) || isa(x, Char) || isa(x, Symbol) @@ -775,6 +902,9 @@ function skipped_parent(node::SyntaxNode) pnode === nothing && return node ppnode = pnode.parent if ppnode !== nothing && kind(pnode) ∈ KSet"... quote" # might need to add more things here + if kind(node) == K"Identifier" && kind(pnode) == K"quote" && kind(ppnode) == K"." && sourcetext(node) == "materialize" + return ppnode.parent + end return ppnode end return pnode @@ -803,25 +933,6 @@ function symloc_key(sym::Symbol) return sym end -function getline(lt, j) - linfo = (j == 0 ? first(lt) : lt[j])::Core.LineInfoNode - linfo.inlined_at == 0 && return linfo.line - @assert linfo.method === Symbol("macro expansion") - linfo = lt[linfo.inlined_at]::Core.LineInfoNode - return linfo.line -end - -function getnextline(lt, j, Δline) - j == 0 && return typemax(Int) - j += 1 - while j <= length(lt) - linfo = lt[j]::Core.LineInfoNode - linfo.inlined_at == 0 && return linfo.line + Δline - j += 1 - end - return typemax(Int) -end - function find_identifier_or_tuplechild(node::AbstractSyntaxNode, sym) kind(node) == K"Identifier" && node.val === sym && return node, true if kind(node) == K"tuple" # tuple destructuring diff --git a/TypedSyntax/src/show.jl b/TypedSyntax/src/show.jl index 1a5c52fb..b4197b3e 100644 --- a/TypedSyntax/src/show.jl +++ b/TypedSyntax/src/show.jl @@ -44,7 +44,7 @@ function Base.printstyled(io::IO, rootnode::MaybeTypedSyntaxNode; sig, body = children(rootnode) type_annotate, pre, pre2, post = type_annotation_mode(sig, rt; type_annotations, hide_type_stable) position = show_src_expr(io, sig, position, pre, pre2; type_annotations, iswarn, hide_type_stable, nd) - type_annotate && show_annotation(io, rt, post; iswarn) + type_annotate && show_annotation(io, rt, post, rootnode.source, position; iswarn) rootnode = body end position = show_src_expr(io, rootnode, position, "", ""; type_annotations, iswarn, hide_type_stable, nd) @@ -55,6 +55,14 @@ Base.printstyled(rootnode::MaybeTypedSyntaxNode; kwargs...) = printstyled(stdout ndigits_linenumbers(node::AbstractSyntaxNode, idxend = last_byte(node)) = ndigits(node.source.first_line + nlines(node.source, idxend) - 1) +function _print(io::IO, x, node, position) + print(io, x) + + if !isempty(x) + add_hint!(get(io, :inlay_hints, nothing), x, node, position+1) + end +end + function show_src_expr(io::IO, node::MaybeTypedSyntaxNode, position::Int, pre::String, pre2::String; type_annotations::Bool=true, iswarn::Bool=false, hide_type_stable::Bool=false, nd::Int) _lastidx = last_byte(node) position = catchup(io, node, position, nd) @@ -64,33 +72,72 @@ function show_src_expr(io::IO, node::MaybeTypedSyntaxNode, position::Int, pre::S position = catchup(io, first(children(node)), position, nd) end end - print(io, pre) + _print(io, pre, node.source, position) for (i, child) in enumerate(children(node)) - i == 2 && print(io, pre2) + i == 2 && _print(io, pre2, node.source, position) cT = gettyp(child) ctype_annotate, cpre, cpre2, cpost = type_annotation_mode(child, cT; type_annotations, hide_type_stable) position = show_src_expr(io, child, position, cpre, cpre2; type_annotations, iswarn, hide_type_stable, nd) - ctype_annotate && show_annotation(io, cT, cpost; iswarn) + ctype_annotate && show_annotation(io, cT, cpost, node.source, position; iswarn) end - return catchup(io, node, position, nd, _lastidx+1) + return Int(catchup(io, node, position, nd, _lastidx+1)) end # should we print a type-annotation? function is_show_annotation(@nospecialize(T); type_annotations::Bool, hide_type_stable::Bool) type_annotations || return false if isa(T, Core.Const) - T = typeof(T.val) + isa(T.val, Module) && return false + T = Core.Typeof(T.val) end isa(T, Type) || return false hide_type_stable || return true return isa(T, Type) && is_type_unstable(T) end +# Is the type equivalent to the source-text? +# We use `endswith` to handle module qualification +is_type_transparent(node, @nospecialize(T)) = endswith(replace(sprint(show, T), r"\s" => ""), replace(sourcetext(node), r"\s" => "")) + +function is_callfunc(node::MaybeTypedSyntaxNode, @nospecialize(T)) + thisnode = node + pnode = node.parent + while pnode !== nothing && kind(pnode) ∈ KSet"quote ." && pnode.parent !== nothing + thisnode = pnode + pnode = pnode.parent + end + if pnode !== nothing && kind(pnode) ∈ (K"call", K"curly") && ((is_infix_op_call(pnode) && is_operator(thisnode)) || thisnode === pnode.children[1]) + if isa(T, Core.Const) + T = T.val + end + if isa(T, Type) || isa(T, Function) + T === Colon() && sourcetext(node) == ":" && return true + return is_type_transparent(node, T) + end + end + return false +end + function type_annotation_mode(node, @nospecialize(T); type_annotations::Bool, hide_type_stable::Bool) kind(node) == K"return" && return false, "", "", "" + is_callfunc(node, T) && return false, "", "", "" type_annotate = is_show_annotation(T; type_annotations, hide_type_stable) pre = pre2 = post = "" if type_annotate + # Try stripping Core.Const and Type{T} wrappers to check if we need to avoid `String::Type{String}` + # or `String::Core.Const(String)` annotations + S = nothing + if isa(T, Core.Const) + val = T.val + if isa(val, DataType) + S = val + end + elseif isa(T, DataType) && T <: Type && isassigned(T.parameters, 1) + S = T.parameters[1] + end + if S !== nothing && is_type_transparent(node, S) + return false, pre, pre2, post + end if kind(node) ∈ KSet":: where" || is_infix_op_call(node) || (is_prec_assignment(node) && kind(node) != K"=") pre, post = "(", ")" elseif is_prefix_op_call(node) # insert parens after prefix op and before type-annotating @@ -100,19 +147,28 @@ function type_annotation_mode(node, @nospecialize(T); type_annotations::Bool, hi return type_annotate, pre, pre2, post end -function show_annotation(io, @nospecialize(T), post=""; iswarn::Bool) +function show_annotation(io, @nospecialize(T), post, node, position; iswarn::Bool) + diagnostics = get(io, :diagnostics, nothing) + inlay_hints = get(io, :inlay_hints, nothing) + print(io, post) - if iswarn - color = !is_type_unstable(T) ? :cyan : - is_small_union_or_tunion(T) ? :yellow : :red - printstyled(io, "::", T; color) + if isa(T, Core.Const) && isa(T.val, Type) + T = Type{T.val} + end + T_str = string(T) + if iswarn && is_type_unstable(T) + color = is_small_union_or_tunion(T) ? :yellow : :red + printstyled(io, "::", T_str; color) + add_diagnostic!(diagnostics, node, position+1, is_small_union_or_tunion(T) ? DiagnosticKinds.Information : DiagnosticKinds.Warning) + add_hint!(inlay_hints, string(post, "::", T_str), node, position+1; kind=InlayHintKinds.Nothing) else - printstyled(io, "::", T; color=:cyan) + printstyled(io, "::", T_str; color=:cyan) + add_hint!(inlay_hints, string(post, "::", T_str), node, position+1; kind=InlayHintKinds.Type) end end print_linenumber(io::IO, node::MaybeTypedSyntaxNode, position::Int, nd::Int) = - print_linenumber(io, source_line(node.source, position), nd) + print_linenumber(io, source_line(node.source, position+1), nd) print_linenumber(io::IO, ln::Int, nd::Int) = printstyled(io, lpad(ln, nd), " "; color=:light_black) # Do any "overdue" printing, generating a line number if needed. Mostly, this catches whitespace. diff --git a/TypedSyntax/src/vscode.jl b/TypedSyntax/src/vscode.jl new file mode 100644 index 00000000..43ed73e8 --- /dev/null +++ b/TypedSyntax/src/vscode.jl @@ -0,0 +1,91 @@ +diagnostics_available_vscode() = isdefined(Main, :VSCodeServer) && Main.VSCodeServer isa Module && isdefined(Main.VSCodeServer, :DIAGNOSTICS_ENABLED) && Main.VSCodeServer.DIAGNOSTICS_ENABLED[] +inlay_hints_available_vscode() = isdefined(Main, :VSCodeServer) && Main.VSCodeServer isa Module && isdefined(Main.VSCodeServer, :INLAY_HINTS_ENABLED) && Main.VSCodeServer.INLAY_HINTS_ENABLED[] + +module DiagnosticKinds + @enum T Error=0 Warning=1 Information=2 Hint=3 +end + +struct Diagnostic + path::String + line::Int # 1-based indexing + severity::DiagnosticKinds.T + msg::String +end + +to_vscode_type(x::Diagnostic) = (msg=x.msg, path = x.path, line = x.line, severity = Int(x.severity)) +function Base.show(io::IO, ::MIME"application/vnd.julia-vscode.diagnostics", diagnostics::AbstractVector{Diagnostic}) + return ( + source = "Cthulhu", + items = to_vscode_type.(diagnostics), + ) +end + +add_diagnostic!(::Nothing, node, position, severity) = nothing +function add_diagnostic!(diagnostics, node, position, severity) + file_path = node.filename + line = source_line(node, position) + push!(diagnostics, Diagnostic(file_path, line, severity, "Unstable Type")) +end + +function clear_diagnostics_vscode() + if diagnostics_available_vscode() + display(Main.VSCodeServer.InlineDisplay(false), TypedSyntax.Diagnostic[]) + end +end + +function display_diagnostics_vscode(diagnostics) + if diagnostics_available_vscode() && !isnothing(diagnostics) + # InlineDisplay(false) means we don't print to REPL + display(Main.VSCodeServer.InlineDisplay(false), diagnostics) + end +end +display_diagnostics_vscode(io::IO) = display_diagnostics_vscode(get(io, :diagnostics, nothing)) + +const InlayHintKinds = (Type=1, Parameter=2, Nothing=nothing) + +struct InlayHint + line::Int # 1-based indexing + column::Int # 1-based indexing + label::String + kind::Union{Nothing, Int} +end + +to_vscode_type(x::InlayHint) = (position=(x.line, x.column), label=x.label, kind=x.kind) +function Base.show(io::IO, ::MIME"application/vnd.julia-vscode.inlayHints", inlay_hints_by_file::Dict{T, Vector{InlayHint}}) where T + if inlay_hints_available_vscode() + return Dict{T, Vector{NamedTuple{(:position, :label, :kind), Tuple{Tuple{Int, Int}, String, Union{Nothing, Int}}}}}( + filepath => to_vscode_type.(inlay_hints) for (filepath, inlay_hints) in inlay_hints_by_file + ) + end + return nothing +end + +add_hint!(::Nothing, message, node, position; kind=InlayHintKinds.Type) = nothing +function add_hint!(inlay_hints, message, node, position; kind=InlayHintKinds.Type) + filepath = node.filename + line, column = source_location(node, position) + + if filepath ∉ keys(inlay_hints) + inlay_hints[filepath] = InlayHint[] + end + push!(inlay_hints[filepath], InlayHint(line-1, column-1, message, kind)) +end + +function clear_inlay_hints_vscode() + if inlay_hints_available_vscode() + display(Main.VSCodeServer.InlineDisplay(false), Dict{String, Vector{TypedSyntax.InlayHint}}()) + end +end + +function display_inlay_hints_vscode(inlay_hints) + if inlay_hints_available_vscode() && !isnothing(inlay_hints) + # InlineDisplay(false) means we don't print to REPL + display(Main.VSCodeServer.InlineDisplay(false), inlay_hints) + end +end +display_inlay_hints_vscode(io::IO) = display_inlay_hints_vscode(get(io, :inlay_hints, nothing)) + +function clear_all_vscode() + clear_diagnostics_vscode() + clear_inlay_hints_vscode() +end \ No newline at end of file diff --git a/TypedSyntax/test/exhaustive.jl b/TypedSyntax/test/exhaustive.jl index f81c8c3d..dfbc7a6e 100644 --- a/TypedSyntax/test/exhaustive.jl +++ b/TypedSyntax/test/exhaustive.jl @@ -26,7 +26,7 @@ const goodmis = Core.MethodInstance[] continue end try - tsn, _ = TypedSyntax.tsn_and_mappings(m, src, rt, ret...; warn=false) + tsn, _ = TypedSyntax.tsn_and_mappings(mi, src, rt, ret...; warn=false) @test isa(tsn, TypedSyntaxNode) push!(goodmis, mi) catch diff --git a/TypedSyntax/test/runtests.jl b/TypedSyntax/test/runtests.jl index 96f0ab1b..b1677ea9 100644 --- a/TypedSyntax/test/runtests.jl +++ b/TypedSyntax/test/runtests.jl @@ -1,7 +1,8 @@ using JuliaSyntax: JuliaSyntax, SyntaxNode, children, child, sourcetext, kind, @K_str -using TypedSyntax: TypedSyntax, TypedSyntaxNode, getsrc +using TypedSyntax: TypedSyntax, TypedSyntaxNode using Dates, InteractiveUtils, Test +has_name_typ(node, name::Symbol, @nospecialize(Ts::Tuple)) = kind(node) == K"Identifier" && node.val === name && node.typ in Ts has_name_typ(node, name::Symbol, @nospecialize(T)) = kind(node) == K"Identifier" && node.val === name && node.typ === T has_name_notyp(node, name::Symbol) = has_name_typ(node, name, nothing) @@ -15,8 +16,7 @@ include("test_module.jl") """ rootnode = JuliaSyntax.parsestmt(SyntaxNode, st; filename="TSN1.jl") TSN.eval(Expr(rootnode)) - src, _ = getsrc(TSN.f, (Float32, Int, Float64)) - tsn = TypedSyntaxNode(rootnode, src) + tsn = TypedSyntaxNode(rootnode, TSN.f, (Float32, Int, Float64)) sig, body = children(tsn) @test children(sig)[2].typ === Float32 @test children(sig)[3].typ === Int @@ -33,8 +33,7 @@ include("test_module.jl") """ rootnode = JuliaSyntax.parsestmt(SyntaxNode, st; filename="TSN2.jl") TSN.eval(Expr(rootnode)) - src, _ = getsrc(TSN.g, (Int16, Int16, Int32)) - tsn = TypedSyntaxNode(rootnode, src) + tsn = TypedSyntaxNode(rootnode, TSN.g, (Int16, Int16, Int32)) sig, body = children(tsn) @test length(children(sig)) == 4 @test children(body)[2].typ === Int32 @@ -46,13 +45,12 @@ include("test_module.jl") st = "math(x) = x + sin(x + π / 4)" rootnode = JuliaSyntax.parsestmt(SyntaxNode, st; filename="TSN2.jl") TSN.eval(Expr(rootnode)) - src, _ = getsrc(TSN.math, (Int,)) - tsn = TypedSyntaxNode(rootnode, src) + tsn = TypedSyntaxNode(rootnode, TSN.math, (Int,)) sig, body = children(tsn) @test has_name_typ(child(body, 1), :x, Int) @test has_name_typ(child(body, 3, 2, 1), :x, Int) pi4 = child(body, 3, 2, 3) - @test kind(pi4) == K"call" && pi4.typ == typeof(π / 4) + @test kind(pi4) == K"call" && pi4.typ === Core.Const(π / 4) tsn = TypedSyntaxNode(TSN.has2xa, (Real,)) @test tsn.typ === Any sig, body = children(tsn) @@ -70,8 +68,7 @@ include("test_module.jl") st = "math2(x) = sin(x) + sin(x)" rootnode = JuliaSyntax.parsestmt(SyntaxNode, st; filename="TSN2.jl") TSN.eval(Expr(rootnode)) - src, _ = getsrc(TSN.math2, (Int,)) - tsn = TypedSyntaxNode(rootnode, src) + tsn = TypedSyntaxNode(rootnode, TSN.math2, (Int,)) sig, body = children(tsn) @test body.typ === Float64 @test_broken child(body, 1).typ === Float64 @@ -91,8 +88,7 @@ include("test_module.jl") ) rootnode = JuliaSyntax.parsestmt(SyntaxNode, st; filename="TSN3.jl") TSN.eval(Expr(rootnode)) - src, _ = getsrc(TSN.firstfirst, (Vector{Vector{Real}},)) - tsn = TypedSyntaxNode(rootnode, src) + tsn = TypedSyntaxNode(rootnode, TSN.firstfirst, (Vector{Vector{Real}},)) sig, body = children(tsn) @test child(body, idxsinner...).typ === nothing @test child(body, idxsouter...).typ === Vector{Real} @@ -150,8 +146,7 @@ include("test_module.jl") """ rootnode = JuliaSyntax.parsestmt(SyntaxNode, st; filename="TSN4.jl") TSN.eval(Expr(rootnode)) - src, rt = getsrc(TSN.setlist!, (Vector{Vector{Float32}}, Vector{Vector{UInt8}}, Int, Int)) - tsn = TypedSyntaxNode(rootnode, src) + tsn = TypedSyntaxNode(rootnode, TSN.setlist!, (Vector{Vector{Float32}}, Vector{Vector{UInt8}}, Int, Int)) sig, body = children(tsn) nodelist = child(body, 1, 2, 1, 1) # `listget` @test sourcetext(nodelist) == "listget" && nodelist.typ === Vector{Vector{UInt8}} @@ -175,8 +170,7 @@ include("test_module.jl") """ rootnode = JuliaSyntax.parsestmt(SyntaxNode, st; filename="TSN5.jl") TSN.eval(Expr(rootnode)) - src, rt = getsrc(TSN.callfindmin, (Vector{Float64},)) - tsn = TypedSyntaxNode(rootnode, src) + tsn = TypedSyntaxNode(rootnode, TSN.callfindmin, (Vector{Float64},)) sig, body = children(tsn) t = child(body, 1, 1) @test kind(t) == K"tuple" @@ -220,18 +214,18 @@ include("test_module.jl") tsn = TypedSyntaxNode(TSN.nestedgenerators, (Int, Int)) sig, body = children(tsn) @test kind(body) == K"generator" - @test body.typ <: Base.Iterators.Flatten + @test TypedSyntax.unwrapinternal(body.typ) <: Base.Iterators.Flatten tsn = TypedSyntaxNode(TSN.nestedgenerators, (Int,)) sig, body = children(tsn) @test kind(body) == K"generator" - @test body.typ <: Base.Iterators.Flatten + @test TypedSyntax.unwrapinternal(body.typ) <: Base.Iterators.Flatten tsn = TypedSyntaxNode(TSN.nestedexplicit, (Int,)) sig, body = children(tsn) @test kind(body) == K"comprehension" @test body.typ <: Vector node = child(body, 1) @test kind(node) == K"generator" - @test node.typ <: Base.Generator + @test TypedSyntax.unwrapinternal(node.typ) <: Base.Generator # Broadcasting tsn = TypedSyntaxNode(TSN.fbroadcast, (Vector{Int},)) @@ -244,9 +238,9 @@ include("test_module.jl") sig, body = children(tsn) @test body.typ === Float64 cnode = child(body, 2) + @test cnode.typ === Vector{Float64} cnodef = child(cnode, 1, 2, 1) @test kind(cnodef) == K"Identifier" && cnodef.val == :materialize - @test cnode.typ === Vector{Float64} cnode = child(body, 2, 2) cnodef = child(cnode, 1, 2, 1) @test kind(cnodef) == K"Identifier" && cnodef.val == :broadcasted @@ -255,12 +249,7 @@ include("test_module.jl") sig, body = children(tsn) node = child(body, 2) src = tsn.typedsource - if isa(src.code[1], GlobalRef) - @test kind(node) == K"dotcall" && node.typ === Vector{String} - else - # We aren't quite handling this properly yet - @test_broken kind(node) == K"dotcall" && node.typ === Vector{String} - end + @test kind(node) == K"dotcall" && node.typ === Vector{String} tsn = TypedSyntaxNode(TSN.bcast415, (TSN.B415, Float64)) sig, body = children(tsn) @test child(body, 1).typ === Float64 @@ -280,23 +269,23 @@ include("test_module.jl") """ rootnode = JuliaSyntax.parsestmt(SyntaxNode, st; filename="TSN6.jl") TSN.eval(Expr(rootnode)) - src, rt = getsrc(TSN.avoidzero, (Int,)) + inferred_result = TypedSyntax.get_inferred_result(TSN.avoidzero, (Int,)) + src, rt, mi = inferred_result.src, inferred_result.rt, inferred_result.mi # src looks like this: # %1 = Main.TSN.:(var"#avoidzero#6")(true, #self#, x)::Float64 # return %1 # Consequently there is nothing to match, but at least we shouldn't error - tsn = TypedSyntaxNode(rootnode, src) + tsn = TypedSyntaxNode(rootnode, src, mi) @test isa(tsn, TypedSyntaxNode) @test rt === Float64 # Try the kwbodyfunc m = which(TSN.avoidzero, (Int,)) - src, rt = getsrc(Base.bodyfunction(m), (Bool, typeof(TSN.avoidzero), Int,)) - tsn = TypedSyntaxNode(rootnode, src) + tsn = TypedSyntaxNode(rootnode, Base.bodyfunction(m), (Bool, typeof(TSN.avoidzero), Int,)) sig, body = children(tsn) isz = child(body, 2, 1, 1) @test kind(isz) == K"call" && child(isz, 1).val == :iszero @test isz.typ === Bool - @test child(body, 2, 1, 2).typ == Float64 + @test child(body, 2, 1, 2).typ === Core.Const(NaN) # default positional arguments tsn = TypedSyntaxNode(TSN.defaultarg, (Float32,)) @@ -314,7 +303,7 @@ include("test_module.jl") tsn = TypedSyntaxNode(TSN.hasdefaulttypearg, (Type{Float32},)) sig, body = children(tsn) arg = child(sig, 1, 2, 1) - @test kind(arg) == K"::" && arg.typ === Type{Float32} + @test kind(arg) == K"::" && arg.typ === Core.Const(Float32) tsn = TypedSyntaxNode(TSN.hasdefaulttypearg, ()) sig, body = children(tsn) arg = child(sig, 1, 2, 1) @@ -339,25 +328,26 @@ include("test_module.jl") @test tsn.typ == Union{Int,Float64} sig, body = children(tsn) @test has_name_typ(child(sig, 2), :list, Vector{Float64}) - @test has_name_typ(child(body, 1, 1), :s, Int) + @test has_name_typ(child(body, 1, 1), :s, Core.Const(0)) @test has_name_typ(child(body, 2, 1, 1), :x, Float64) node = child(body, 2, 2, 1) @test kind(node) == K"+=" - @test has_name_typ(child(node, 1), :s, Float64) # if this line runs, the LHS now has type `Float64` + @test has_name_typ(child(node, 1), :s, Float64) || # if this line runs, the LHS now has type `Float64` + has_name_typ(child(node, 1), :s, Union{Float64, Int}) # but Julia 1.11 infers this still as the Union @test has_name_typ(child(node, 2), :x, Float64) @test has_name_typ(child(body, 3, 1), :s, Union{Float64, Int}) tsn = TypedSyntaxNode(TSN.summer_iterate, (Vector{Float64},)) @test tsn.typ == Union{Int,Float64} sig, body = children(tsn) - @test has_name_typ(child(body, 2, 1), :ret, Union{Nothing, Tuple{Float64, Int64}}) + @test has_name_typ(child(body, 2, 1), :ret, Union{Nothing, Tuple{Float64, Int}}) @test has_name_typ(child(body, 3, 2, 1, 1, 1), :x, Float64) # `where`, unnamed arguments, and types-as-arguments tsn = TypedSyntaxNode(TSN.zerowhere, (Vector{Int16},)) sig, body = children(tsn) @test child(sig, 1, 2).typ === Vector{Int16} - @test body.typ === Int16 - @test has_name_typ(child(body, 2), :T, Type{Int16}) + @test body.typ === Core.Const(Int16(0)) + @test has_name_typ(child(body, 2), :T, (Core.Const(Int16), Type{Int16})) # tsn = TypedSyntaxNode(TSN.vaparam, (Matrix{Float32}, (String, Bool))) # fails on `which` m = @which TSN.vaparam(rand(3,3), ("hello", false)) mi = first(specializations(m)) @@ -377,10 +367,10 @@ include("test_module.jl") @test has_name_typ(child(body, 2), :Bool, Type{Bool}) tsn = TypedSyntaxNode(TSN.unnamedargs, (Type{Matrix{Float32}}, Type{Int})) sig, body = children(tsn) + @test child(sig, 1, 2).typ === Core.Const(Matrix{Float32}) + @test child(sig, 1, 3).typ === Core.Const(Int) m = @which TSN.unnamedargs(Matrix{Float32}, Int, Int) fbody = Base.bodyfunction(m) - @test child(sig, 1, 2).typ === Type{Matrix{Float32}} - @test child(sig, 1, 3).typ === Type{Int} m = @which TSN.unnamedargs(Matrix{Float32}, Int; a="hello") mi = nothing for _mi in specializations(m) @@ -394,8 +384,8 @@ include("test_module.jl") end tsn = TypedSyntaxNode(mi) sig, body = children(tsn) - @test child(sig, 1, 2).typ === Type{Matrix{Float32}} - @test child(sig, 1, 3).typ === Type{Int} + @test child(sig, 1, 2).typ === Core.Const(Matrix{Float32}) + @test child(sig, 1, 3).typ === Core.Const(Int) @test has_name_notyp(child(sig, 1, 4, 1), :c) @test has_name_typ(child(sig, 1, 5, 1, 1), :a, String) m = @which TSN.unnamedargs(Matrix{Float32}, Int, :c; a="hello") @@ -409,8 +399,8 @@ include("test_module.jl") end tsn = TypedSyntaxNode(mi) sig, body = children(tsn) - @test child(sig, 1, 2).typ === Type{Matrix{Float32}} - @test child(sig, 1, 3).typ === Type{Int} + @test child(sig, 1, 2).typ === Core.Const(Matrix{Float32}) + @test child(sig, 1, 3).typ === Core.Const(Int) @test child(sig, 1, 4, 1).typ === Symbol @test child(sig, 1, 5, 1, 1).typ === String mbody = only(methods(fbody)) @@ -424,8 +414,8 @@ include("test_module.jl") end tsn = TypedSyntaxNode(mi) sig, body = children(tsn) - @test child(sig, 1, 2).typ === Type{Matrix{Float32}} - @test child(sig, 1, 3).typ === Type{Int} + @test child(sig, 1, 2).typ === Core.Const(Matrix{Float32}) + @test child(sig, 1, 3).typ === Core.Const(Int) @test child(sig, 1, 4, 1).typ === Symbol @test child(sig, 1, 5, 1, 1).typ === String tsn = TypedSyntaxNode(TSN.unnamedargs2, (Type{Matrix}, Symbol)) @@ -464,7 +454,7 @@ include("test_module.jl") src = tsn.typedsource @test Symbol("kwargs...") ∈ src.slotnames sig, body = children(tsn) - @test child(body, 2, 1).typ <: Base.Iterators.Pairs + @test TypedSyntax.unwrapinternal(child(body, 2, 1).typ) <: Base.Iterators.Pairs # quoted symbols that could be confused for function definition tsn = TypedSyntaxNode(TSN.isexpreq, (Expr,)) @@ -477,10 +467,10 @@ include("test_module.jl") sig, body = children(tsn) errnode = child(body, 1, 2) errf = child(errnode, 1) - @test errnode.typ === nothing && errf.typ === typeof(Base.throw_boundserror) + @test errnode.typ === nothing && errf.typ === Core.Const(Base.throw_boundserror) retnode = child(body, 2) @test kind(retnode) == K"return" - @test retnode.typ === nothing || retnode.typ === Nothing + @test retnode.typ === Core.Const(nothing) || retnode.typ === nothing # julia 1.10 doesn't assign a type to the Core.ReturnNode # Globals & scoped assignment tsn = TypedSyntaxNode(TSN.setglobal, (Char,)) @@ -492,7 +482,7 @@ include("test_module.jl") tsn = TypedSyntaxNode(TSN.myoftype, (Float64, Int)) sig, body = children(tsn) node = child(body, 1) - @test node.typ === Type{Float64} + @test node.typ === Core.Const(Float64) tsn = TypedSyntaxNode(TSN.DefaultArray{Float32}, (Vector{Int}, Int)) sig, body = children(tsn) @test kind(sig) == K"where" @@ -520,18 +510,15 @@ include("test_module.jl") @test_broken body.typ == Int # Construction from MethodInstance - src, rt = TypedSyntax.getsrc(TSN.myoftype, (Float64, Int)) - tsn = TypedSyntaxNode(src.parent) + tsn = TypedSyntaxNode(TSN.myoftype, (Float64, Int)) sig, body = children(tsn) node = child(body, 1) - @test node.typ === Type{Float64} + @test node.typ === Core.Const(Float64) # UnionAll in signature (issue #409) - @static if VERSION ≥ v"1.9-" - tsn = TypedSyntaxNode(Core.kwcall, (NamedTuple, typeof(issorted), Vector{Int})) - sig, body = children(tsn) - @test has_name_typ(child(sig, 2), :itr, Vector{Int}) - end + tsn = TypedSyntaxNode(Core.kwcall, (NamedTuple, typeof(issorted), Vector{Int})) + sig, body = children(tsn) + @test has_name_typ(child(sig, 2), :itr, Vector{Int}) # Empty `return` (issue #458) tsn = TypedSyntaxNode(TSN.f458, ()) @@ -552,7 +539,7 @@ include("test_module.jl") @test TypedSyntax.num_positional_args(tsn) == 2 # Display - tsn = TypedSyntaxNode(TSN.mysin, (Int,)) + tsn = TypedSyntaxNode(TSN.mysin, (Int64,)) str = sprint(tsn; context=:color=>false) do io, obj printstyled(io, obj; hide_type_stable=false) end @@ -580,9 +567,9 @@ include("test_module.jl") str = sprint(tsn; context=:color=>false) do io, obj printstyled(io, obj; hide_type_stable=false) end - @test occursin("s::$Int = 0::$Int", str) + @test occursin("s::$Int = 0::$Int", str) || occursin("s::Core.Const(0) = 0::Core.Const(0)", str) @test !occursin("(s::$Int = 0::$Int)", str) - @test occursin("(s::Float64 += x::Float64)::Float64", str) + @test occursin("(s::Float64 += x::Float64)::Float64", str) || occursin("(s::Union{Float64, $Int} += x::Float64)::Float64", str) tsn = TypedSyntaxNode(TSN.zerowhere, (Vector{Int16},)) str = sprint(tsn; context=:color=>true) do io, obj printstyled(io, obj; iswarn=true, hide_type_stable=false) @@ -602,7 +589,7 @@ include("test_module.jl") end @test occursin("[1]\e[36m::Float32\e[39m", str) @test occursin("[2]\e[36m::Float32\e[39m", str) - tsn = TypedSyntaxNode(TSN.simplef, (Int, Float64)) + tsn = TypedSyntaxNode(TSN.simplef, (Int64, Float64)) str = sprint(tsn; context=:color=>false) do io, obj printstyled(io, obj; hide_type_stable=false) end @@ -616,6 +603,44 @@ include("test_module.jl") printstyled(io, obj; hide_type_stable=false) end @test occursin("-(x::Float64)::Float64", str) + # Issue #482 + arg = ["key"=>7] + tsn = TypedSyntaxNode(TSN.f482a, (typeof(arg),)) + str = sprint(tsn; context=:color=>false) do io, obj + printstyled(io, obj; hide_type_stable=false) + end + @test occursin("::Type{Dict{String, Any}}", str) + tsn = TypedSyntaxNode(TSN.f482b, (typeof(arg),)) + str = sprint(tsn; context=:color=>false) do io, obj + printstyled(io, obj; hide_type_stable=false) + end + @test !occursin("::Type{Dict{String, Any}}", str) + tsn = TypedSyntaxNode(TSN.obfuscated, (Float64,)) + str = sprint(tsn; context=:color=>false) do io, obj + printstyled(io, obj; hide_type_stable=false) + end + @test occursin("::Core.Const(sin)", str) || occursin("::typeof(sin)", str) + tsn = TypedSyntaxNode(TSN.calls_helper, (Float32,)) + str = sprint(tsn; context=:color=>false) do io, obj + printstyled(io, obj; hide_type_stable=false) + end + @test !occursin("Core.Const", str) + tsn = TypedSyntaxNode(TSN.calls_helper1, (Float32,)) + str = sprint(tsn; context=:color=>false) do io, obj + printstyled(io, obj; hide_type_stable=false) + end + @test !occursin("Core.Const", str) + tsn = TypedSyntaxNode(TSN.calls_helper2, (Float32,)) + str = sprint(tsn; context=:color=>false) do io, obj + printstyled(io, obj; hide_type_stable=false) + end + @test !occursin("Core.Const", str) + tsn = TypedSyntaxNode(TSN.allbutfirst, (Vector{Bool},)) + str = sprint(tsn; context=:color=>false) do io, obj + printstyled(io, obj; hide_type_stable=false) + end + @test occursin("2:end", str) + # issue #413 @test TypedSyntax.is_small_union_or_tunion(Union{}) @@ -629,8 +654,86 @@ include("test_module.jl") # issue #435 tsnc = copy(tsn) @test isa(tsnc, TypedSyntaxNode) + + # issue 487 + inferred_result = TypedSyntax.get_inferred_result(TSN.f487, (Int,)) + src, mi = inferred_result.src, inferred_result.mi + rt = Core.Const(1) + tsn, _ = TypedSyntax.tsn_and_mappings(mi, src, rt) + @test_nowarn str = sprint(tsn; context=:color=>false) do io, obj + printstyled(io, obj; hide_type_stable=false) + end + + # issue 491 + tsn = TypedSyntaxNode(+, (Int, Int)) # need a node, not important what it is + @test_nowarn TypedSyntax.type_annotation_mode(tsn, Union{}; type_annotations=true, hide_type_stable=false) + + # issue 492 + @static if isdefined(Base, :_tuple_unique_fieldtypes) + tsn = TypedSyntaxNode(Base._tuple_unique_fieldtypes, (Any,)) + @test_nowarn str = sprint(tsn; context=:color=>false) do io, obj + printstyled(io, obj; hide_type_stable=false) + end + end + + # issue 493 + tsn = TypedSyntaxNode(TSN.f493, ()) + @test_nowarn str = sprint(tsn; context=:color=>false) do io, obj + printstyled(io, obj; hide_type_stable=false) + end + + # issue 506 + tsn, _ = TypedSyntax.tsn_and_mappings(collect, (typeof(Base.Generator(+, 1:2)),)) + @test_nowarn str = sprint(tsn; context=:color=>false) do io, obj + printstyled(io, obj; hide_type_stable=false) + end end if parse(Bool, get(ENV, "CI", "false")) include("exhaustive.jl") end + +module VSCodeServer + struct InlineDisplay + is_repl::Bool + end + const INLAY_HINTS_ENABLED = Ref(true) + const DIAGNOSTICS_ENABLED = Ref(true) + + function Base.display(d::InlineDisplay, x) + return nothing + end +end +using TypedSyntax: InlayHint, Diagnostic, InlayHintKinds + +@testset "test_vscode.jl" begin + # VSCode + tsn = TypedSyntaxNode(TSN.fVSCode, (Int,)) + + io = IOContext(devnull, :inlay_hints=>Dict{String, Vector{InlayHint}}(), :diagnostics=>Diagnostic[]) + printstyled(io, tsn) + @test getproperty.(first(values(io[:inlay_hints])), :kind) == [InlayHintKinds.Nothing, InlayHintKinds.Type, InlayHintKinds.Nothing] && getproperty.(first(values(io[:inlay_hints])), :label) == ["::Union{Float64, $Int}", "(", ")::Union{Float64, $Int}"] + @test length(io[:diagnostics]) == 2 + + io = IOContext(devnull, :inlay_hints=>Dict{String, Vector{InlayHint}}(), :diagnostics=>Diagnostic[]) + printstyled(io, tsn; hide_type_stable=false) + @test getproperty.(first(values(io[:inlay_hints])), :kind) == vcat(InlayHintKinds.Type, InlayHintKinds.Nothing, repeat([InlayHintKinds.Type], 15), InlayHintKinds.Nothing) && getproperty.(first(values(io[:inlay_hints])), :label) == ["::$Int" + "::Union{Float64, $Int}" + "::$Int" + "(" + "::$Int" + ")::$Int" + "::$Int" + "(" + "::$Int" + ")::$Int" + "(" + "::$Int" + "(" + "::$Int" + ")::Bool" + "::Core.Const(-1)" + "::Core.Const(1.0)" + ")::Union{Float64, $Int}"] + @test length(io[:diagnostics]) == 2 +end diff --git a/TypedSyntax/test/test_module.jl b/TypedSyntax/test/test_module.jl index 0e530cdc..64fe596a 100644 --- a/TypedSyntax/test/test_module.jl +++ b/TypedSyntax/test/test_module.jl @@ -50,10 +50,10 @@ zerowhere(::AbstractArray{T}) where T<:Real = zero(T) vaparam(a::AbstractArray{T,N}, I::NTuple{N,Any}) where {T,N} = N @inline function myplustv(x::T, y::Integer) where {T<:AbstractChar} # vendored copy of +(::T, ::Integer) where T<:AbstractChar if x isa Char - u = Int32((bitcast(UInt32, x) >> 24) % Int8) + u = Int32((Base.bitcast(UInt32, x) >> 24) % Int8) if u >= 0 # inline the runtime fast path z = u + y - return 0 <= z < 0x80 ? bitcast(Char, (z % UInt32) << 24) : Char(UInt32(z)) + return 0 <= z < 0x80 ? Base.bitcast(Char, (z % UInt32) << 24) : Char(UInt32(z)) end end return T(Int32(x) + Int32(y)) @@ -223,4 +223,42 @@ const T426 = Dict{Type{<:Dates.Period}, Bool}( # Issue #458 f458() = return +function fVSCode(x) + z = x + 1 + y = 2 * z + return y + (x > 0 ? -1 : 1.0) +end + +# Issue #482 & #465 +MyDict{T} = Dict{T,Any} +f482a(x) = MyDict{String}(x) +f482b(x) = Dict{String,Any}(x) + +# Issue 487 +f487(x) = 1 + +function f493() + T = rand() > 0.5 ? Int64 : Float64 + sum(rand(T, 100)) +end + +function obfuscated(x) + f = sin + return f(x) +end + +module Internal +export helper +helper(x) = x+1 +module MoreInternal +helper2(x) = x+2 +end +end +using .Internal +calls_helper(x) = helper(x) +calls_helper1(x) = Internal.helper(x) +calls_helper2(x) = Internal.MoreInternal.helper2(x) + +allbutfirst(list) = list[2:end] + end diff --git a/src/Cthulhu.jl b/src/Cthulhu.jl index 4fade1a0..814ff2ac 100644 --- a/src/Cthulhu.jl +++ b/src/Cthulhu.jl @@ -2,7 +2,7 @@ module Cthulhu Base.Experimental.@compiler_options compile=min optimize=1 -using CodeTracking: definition, whereis +using CodeTracking: CodeTracking, definition, whereis, maybe_fix_path using InteractiveUtils using UUIDs using REPL: REPL, AbstractTerminal @@ -20,6 +20,13 @@ const mapany = Base.mapany const ArgTypes = Vector{Any} +@static if VERSION ≥ v"1.11.0-DEV.1498" + import .CC: get_inference_world + using Base: get_world_counter +else + import .CC: get_world_counter, get_world_counter as get_inference_world +end + Base.@kwdef mutable struct CthulhuConfig enable_highlighter::Bool = false highlighter::Cmd = `pygmentize -l` @@ -33,9 +40,13 @@ Base.@kwdef mutable struct CthulhuConfig 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 """ @@ -60,9 +71,13 @@ end - `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`. """ const CONFIG = CthulhuConfig() @@ -91,7 +106,17 @@ export ascend @descend_code_typed Evaluates the arguments to the function or macro call, determines their -types, and calls `code_typed` on the resulting expression. +types, and calls [`descend_code_typed`](@ref) on the resulting expression. +See [`Cthulhu.CONFIG`](@ref) for options and their defaults. + +# Examples +```julia +julia> @descend_code_typed sin(1) +[...] + +julia> @descend_code_typed optimize=false sin(1) +[...] +``` """ macro descend_code_typed(ex0...) InteractiveUtils.gen_call_with_extracted_types_and_kwargs(__module__, :descend_code_typed, ex0) @@ -110,7 +135,23 @@ end @descend_code_warntype Evaluates the arguments to the function or macro call, determines their -types, and calls `code_warntype` on the resulting expression. +types, and calls [`descend_code_warntype`](@ref) on the resulting expression. +See [`Cthulhu.CONFIG`](@ref) for options and their defaults. + +# Examples +```julia +julia> function foo() + T = rand() > 0.5 ? Int64 : Float64 + sin(rand(T)) + end +foo (generic function with 1 method) + +julia> @descend_code_warntype foo() +[...] + +julia> @descend_code_warntype hide_type_stable=true foo() +[...] +``` """ macro descend_code_warntype(ex0...) InteractiveUtils.gen_call_with_extracted_types_and_kwargs(__module__, :descend_code_warntype, ex0) @@ -119,9 +160,22 @@ end """ @descend -Shortcut for [`@descend_code_typed`](@ref). +Evaluates the arguments to the function or macro call, determines their +types, and calls [`descend`](@ref) on the resulting expression. +See [`Cthulhu.CONFIG`](@ref) for options and their defaults. + +# Examples +```julia +julia> @descend sin(1) +[...] + +julia> @descend iswarn=false foo() +[...] +``` """ -const var"@descend" = var"@descend_code_typed" +macro descend(ex0...) + InteractiveUtils.gen_call_with_extracted_types_and_kwargs(__module__, :descend, ex0) +end """ descend_code_typed(f, argtypes=Tuple{...}; kwargs...) @@ -132,6 +186,7 @@ const var"@descend" = var"@descend_code_typed" Given a function and a tuple-type, interactively explore the output of `code_typed` 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. +See [`Cthulhu.CONFIG`](@ref) for `kwargs` and their defaults. # Usage: ```julia @@ -146,13 +201,13 @@ julia> descend_code_typed(Tuple{typeof(sin), Int}) julia> descend_code_typed() do T = rand() > 0.5 ? Int64 : Float64 - sin(rand(T) + sin(rand(T)) end [...] ``` """ descend_code_typed(@nospecialize(args...); kwargs...) = - _descend_with_error_handling(args...; iswarn=false, kwargs...) + _descend_with_error_handling(args...; annotate_source=false, iswarn=false, kwargs...) """ descend_code_warntype(f, argtypes=Tuple{...}; kwargs...) @@ -163,6 +218,7 @@ descend_code_typed(@nospecialize(args...); kwargs...) = Given a function and a tuple-type, interactively explore the output of `code_warntype` 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. +See [`Cthulhu.CONFIG`](@ref) for `kwargs` and their defaults. # Usage: ```julia @@ -177,13 +233,13 @@ julia> descend_code_warntype(Tuple{typeof(sin), Int}) julia> descend_code_warntype() do T = rand() > 0.5 ? Int64 : Float64 - sin(rand(T) + sin(rand(T)) end [...] ``` """ descend_code_warntype(@nospecialize(args...); kwargs...) = - _descend_with_error_handling(args...; iswarn=true, 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) @@ -206,6 +262,7 @@ function __descend_with_error_handling(args...; terminal=default_terminal(), kwa try _descend(terminal, args...; kwargs...) catch x + TypedSyntax.clear_all_vscode() if x isa InterruptException return nothing else @@ -218,46 +275,71 @@ end default_terminal() = REPL.LineEdit.terminal(Base.active_repl) """ - descend + descend(f, argtypes=Tuple{...}; kwargs...) + descend(tt::Type{<:Tuple}; kwargs...) + descend(Cthulhu.BOOKMARKS[i]) + descend(mi::MethodInstance; kwargs...) -Shortcut for [`descend_code_typed`](@ref). -""" -const descend = descend_code_typed +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. +See [`Cthulhu.CONFIG`](@ref) for `kwargs` and their defaults. + +# Usage: +```julia +julia> descend(sin, (Int,)) +[...] -descend_code_typed(interp::AbstractInterpreter, mi::MethodInstance; kwargs...) = - _descend_with_error_handling(interp, mi; iswarn=false, kwargs...) +julia> descend(sin, Tuple{Int}) +[...] + +julia> descend(Tuple{typeof(sin), Int}) +[...] -function codeinst_rt(code::CodeInstance) +julia> descend() do + T = rand() > 0.5 ? Int64 : Float64 + sin(rand(T)) + end +[...] +``` +""" +descend(@nospecialize(args...); kwargs...) = + _descend_with_error_handling(args...; iswarn=true, kwargs...) + +@static if VERSION ≥ v"1.11.0-DEV.207" + using .CC: cached_return_type +else +function cached_return_type(code::CodeInstance) rettype = code.rettype - if isdefined(code, :rettype_const) - rettype_const = code.rettype_const - if isa(rettype_const, Vector{Any}) && !(Vector{Any} <: rettype) - return Core.PartialStruct(rettype, rettype_const) - elseif isa(rettype_const, Core.PartialOpaque) && rettype <: Core.OpaqueClosure - return rettype_const - elseif isa(rettype_const, CC.InterConditional) && !(CC.InterConditional <: rettype) - return rettype_const - else - return Const(rettype_const) - end + isdefined(code, :rettype_const) || return rettype + rettype_const = code.rettype_const + if isa(rettype_const, Vector{Any}) && !(Vector{Any} <: rettype) + return Core.PartialStruct(rettype, rettype_const) + elseif isa(rettype_const, Core.PartialOpaque) && rettype <: Core.OpaqueClosure + return rettype_const + elseif isa(rettype_const, CC.InterConditional) && !(CC.InterConditional <: rettype) + return rettype_const else - return rettype + return Const(rettype_const) + end +end +end + +function cached_exception_type(code::CodeInstance) + @static if VERSION ≥ v"1.11.0-DEV.945" + return code.exctype + else + return nothing end end get_effects(codeinst::CodeInstance) = CC.decode_effects(codeinst.ipo_purity_bits) get_effects(codeinst::CodeInfo) = CC.decode_effects(codeinst.purity) get_effects(src::InferredSource) = src.effects -get_effects(unopt::Dict{Union{MethodInstance, InferenceResult}, InferredSource}, mi::MethodInstance) = - haskey(unopt, mi) ? get_effects(unopt[mi]) : Effects() get_effects(result::InferenceResult) = result.ipo_effects -@static if VERSION ≥ v"1.9-" - get_effects(result::CC.ConstPropResult) = get_effects(result.result) - get_effects(result::CC.ConcreteResult) = result.effects - get_effects(result::CC.SemiConcreteResult) = result.effects -else - get_effects(result::CC.ConstResult) = result.effects -end +get_effects(result::CC.ConstPropResult) = get_effects(result.result) +get_effects(result::CC.ConcreteResult) = result.effects +get_effects(result::CC.SemiConcreteResult) = result.effects # `@constprop :aggressive` here in order to make sure the constant propagation of `allow_no_src` @constprop :aggressive function lookup(interp::CthulhuInterpreter, mi::MethodInstance, optimize::Bool; allow_no_src::Bool=false) @@ -270,7 +352,8 @@ end function lookup_optimized(interp::CthulhuInterpreter, mi::MethodInstance, allow_no_src::Bool=false) codeinst = interp.opt[mi] - rt = codeinst_rt(codeinst) + rt = cached_return_type(codeinst) + exct = cached_exception_type(codeinst) opt = codeinst.inferred if opt !== nothing opt = opt::OptimizedSource @@ -292,19 +375,19 @@ function lookup_optimized(interp::CthulhuInterpreter, mi::MethodInstance, allow_ error("couldn't find the source; inspect `Main.interp` and `Main.mi`") end effects = get_effects(codeinst) - return (; src, rt, infos, slottypes, effects, codeinf) + return (; src, rt, exct, infos, slottypes, effects, codeinf) end function lookup_unoptimized(interp::CthulhuInterpreter, mi::MethodInstance) codeinf = src = copy(interp.unopt[mi].src) - rt = interp.unopt[mi].rt + (; rt, exct) = interp.unopt[mi] infos = interp.unopt[mi].stmt_info effects = get_effects(interp.unopt[mi]) slottypes = src.slottypes if isnothing(slottypes) slottypes = Any[ Any for i = 1:length(src.slotflags) ] end - return (; src, rt, infos, slottypes, effects, codeinf) + return (; src, rt, exct, infos, slottypes, effects, codeinf) end function lookup_constproped(interp::CthulhuInterpreter, override::InferenceResult, optimize::Bool) @@ -322,11 +405,12 @@ function lookup_constproped_optimized(interp::CthulhuInterpreter, override::Infe # e.g. when we switch from constant-prop' unoptimized source src = CC.copy(opt.ir) rt = override.result + exct = @static hasfield(InferenceResult, :exc_result) ? override.exc_result : nothing infos = src.stmts.info slottypes = src.argtypes codeinf = opt.src effects = opt.effects - return (; src, rt, infos, slottypes, effects, codeinf) + return (; src, rt, exct, infos, slottypes, effects, codeinf) else # the source might be unavailable at this point, # when a result is fully constant-folded etc. @@ -340,24 +424,25 @@ function lookup_constproped_unoptimized(interp::CthulhuInterpreter, override::In unopt = interp.unopt[override.linfo] end codeinf = src = copy(unopt.src) - rt = unopt.rt + (; 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 (; src, rt, infos, slottypes, effects, codeinf) + return (; src, rt, exct, infos, slottypes, effects, codeinf) end function lookup_semiconcrete(interp::CthulhuInterpreter, 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) (; codeinf) = lookup(interp, get_mi(override), optimize) - return (; src, rt, infos, slottypes, effects, codeinf) + return (; src, rt, exct, infos, slottypes, effects, codeinf) end function get_override(@nospecialize(info)) @@ -374,17 +459,21 @@ end ## 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 - 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 + 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 ) if isnothing(hide_type_stable) @@ -406,7 +495,9 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs do_typeinf!(new_interp, new_mi) _descend(term, new_interp, new_mi; debuginfo, optimize, interruptexc, iswarn, hide_type_stable, remarks, - with_effects, inline_cost, type_annotations, annotate_source) + with_effects, exception_type, + inline_cost, type_annotations, annotate_source, + inlay_types_vscode, diagnostics_vscode) end custom_toggles = Cthulhu.custom_toggles(interp) if !(custom_toggles isa Vector{CustomToggle}) @@ -417,9 +508,9 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs end while true if isa(override, InferenceResult) - (; src, rt, infos, slottypes, codeinf, effects) = lookup_constproped(interp, curs, override, optimize & !annotate_source) + (; src, rt, exct, infos, slottypes, codeinf, effects) = lookup_constproped(interp, curs, override, optimize & !annotate_source) elseif isa(override, SemiConcreteCallInfo) - (; src, rt, infos, slottypes, codeinf, effects) = lookup_semiconcrete(interp, curs, override, optimize & !annotate_source) + (; src, rt, exct, infos, slottypes, codeinf, effects) = lookup_semiconcrete(interp, curs, override, optimize & !annotate_source) else if optimize && !annotate_source codeinst = get_optimized_codeinst(interp, curs) @@ -429,7 +520,8 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs # but make something up that looks plausible. callsites = Callsite[] if display_CI - callsite = Callsite(-1, MICallInfo(codeinst.def, codeinst.rettype, get_effects(codeinst)), :invoke) + exct = @static VERSION ≥ v"1.11.0-DEV.945" ? codeinst.exct : nothing + callsite = Callsite(-1, MICallInfo(codeinst.def, codeinst.rettype, get_effects(codeinst), exct), :invoke) println(iostream) println(iostream, "│ ─ $callsite") println(iostream, "│ return ", Const(codeinst.rettype_const)) @@ -446,7 +538,7 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs end end end - (; src, rt, infos, slottypes, effects, codeinf) = lookup(interp, curs, optimize & !annotate_source) + (; src, rt, exct, infos, slottypes, effects, codeinf) = lookup(interp, curs, optimize & !annotate_source) end mi = get_mi(curs) src = preprocess_ci!(src, mi, optimize & !annotate_source, CONFIG) @@ -455,11 +547,28 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs else @assert length(src.code) == length(infos) end - callsites, sourcenodes = find_callsites(interp, src, infos, mi, slottypes, optimize & !annotate_source, annotate_source) + infkey = override isa InferenceResult ? override : mi + @static if VERSION ≥ v"1.11.0-DEV.1127" + pc2excts = exception_type ? get_excts(interp, infkey) : nothing + else + if exception_type + @warn "Statement-wise and call-wise exception type information is available only on v\"1.11.0-DEV.1127\" and later" + end + pc2excts = nothing + end + callsites, sourcenodes = find_callsites(interp, src, infos, mi, 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_remarks(interp, override !== nothing ? override : mi) : nothing - pc2effects = with_effects ? get_effects(interp, override !== nothing ? override : mi) : nothing + pc2remarks = remarks ? get_remarks(interp, infkey) : nothing + pc2effects = with_effects ? get_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, mi=mi, @@ -469,13 +578,14 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs :color => true, :displaysize => displaysize(iostream), # displaysize doesn't propagate otherwise :SOURCE_SLOTNAMES => codeinf === nothing ? false : Base.sourceinfo_slotnames(codeinf), - :with_effects => with_effects) + :with_effects => with_effects, + :exception_type => exception_type) stringify(ioctx) do lambda_io - cthulhu_typed(lambda_io, debuginfo, annotate_source ? codeinf : src, rt, effects, mi; - iswarn, hide_type_stable, - pc2remarks, pc2effects, - inline_cost, type_annotations, annotate_source, - interp) + cthulhu_typed(lambda_io, debuginfo, annotate_source ? codeinf : src, rt, exct, effects, mi; + 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) @@ -487,12 +597,13 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs else lambda_io = IOContext(iostream, :SOURCE_SLOTNAMES => Base.sourceinfo_slotnames(codeinf), - :with_effects => with_effects) - cthulhu_typed(lambda_io, debuginfo, src, rt, effects, mi; - iswarn, hide_type_stable, - pc2remarks, pc2effects, - inline_cost, type_annotations, annotate_source, - interp) + :with_effects => with_effects, + :exception_type => exception_type) + cthulhu_typed(lambda_io, debuginfo, src, rt, exct, effects, mi; + 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 @@ -502,8 +613,14 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs @label show_menu shown_callsites = annotate_source ? sourcenodes : callsites - menu = CthulhuMenu(shown_callsites, with_effects, 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, inline_cost, type_annotations, CONFIG.enable_highlighter, custom_toggles) + 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 @@ -547,7 +664,9 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs end end end - menu = CthulhuMenu(show_sub_callsites, with_effects, optimize & !annotate_source, false, false, custom_toggles; sub_menu=true, menu_options...) + 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 @@ -592,14 +711,25 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs override = get_override(info), debuginfo, optimize, interruptexc, iswarn, hide_type_stable, - remarks, with_effects, inline_cost, type_annotations, annotate_source) + 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 !is_cached(get_mi(curs)) @@ -656,6 +786,8 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs 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 @@ -666,8 +798,9 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs display_CI = true elseif toggle === :ast || toggle === :llvm || toggle === :native view_cmd = CODEVIEWS[toggle] + world = get_inference_world(interp) println(iostream) - view_cmd(iostream, mi, optimize, debuginfo, interp, CONFIG) + view_cmd(iostream, mi, optimize, debuginfo, world, CONFIG) display_CI = false else local i = findfirst(ct->ct.toggle === toggle, custom_toggles) @@ -689,6 +822,8 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs end println(iostream) end + + TypedSyntax.clear_all_vscode() end function do_typeinf!(interp::AbstractInterpreter, mi::MethodInstance) @@ -701,15 +836,10 @@ function do_typeinf!(interp::AbstractInterpreter, mi::MethodInstance) return nothing end -function get_specialization(@nospecialize(TT)) - match = Base._which(TT) - mi = specialize_method(match) - return mi -end +get_specialization(@nospecialize tt::Type{<:Tuple}) = specialize_method(Base._which(tt)) -function get_specialization(@nospecialize(F), @nospecialize(TT)) - return get_specialization(Base.signature_type(F, TT)) -end +get_specialization(@nospecialize(f), @nospecialize(tt=default_tt(f))) = + get_specialization(Base.signature_type(f, tt)) function mkinterp(interp::AbstractInterpreter, @nospecialize(args...)) interp′ = CthulhuInterpreter(interp) @@ -765,6 +895,7 @@ function ascend(term, mi; interp::AbstractInterpreter=NativeInterpreter(), kwarg miparent = 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") @@ -779,8 +910,8 @@ function ascend(term, mi; interp::AbstractInterpreter=NativeInterpreter(), kwarg end choice2 = TerminalMenus.request(term, promptstr, linemenu; cursor=choice2) if 0 < choice2 < length(strlocs) - loc = ulocs[choice2] - edit(String(loc[1][2]), first(loc[2])) + loc, lines = ulocs[choice2] + edit(loc[2], first(lines)) elseif choice2 == length(strlocs) browsecodetyped = true break @@ -788,6 +919,9 @@ function ascend(term, mi; interp::AbstractInterpreter=NativeInterpreter(), kwarg 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. interp′ = CthulhuInterpreter(interp) @@ -809,13 +943,17 @@ ascend using PrecompileTools @setup_workload begin - input = Base.link_pipe!(Pipe(), reader_supports_async=true, writer_supports_async=true) - term = REPL.Terminals.TTYTerminal("dumb", input.out, devnull, devnull) - write(input.in, 'q') - @compile_workload begin - descend(gcd, (Int, Int); terminal=term) - # declare we are done with streams - close(input.in) + try + input = Base.link_pipe!(Pipe(), reader_supports_async=true, writer_supports_async=true) + term = REPL.Terminals.TTYTerminal("dumb", input.out, devnull, devnull) + write(input.in, 'q') + @compile_workload begin + descend(gcd, (Int, Int); terminal=term) + # declare we are done with streams + close(input.in) + 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 diff --git a/src/backedges.jl b/src/backedges.jl index 99b6ce93..b33cec35 100644 --- a/src/backedges.jl +++ b/src/backedges.jl @@ -88,13 +88,67 @@ function buildframes(st::Vector{StackTraces.StackFrame}) return ipframes end -# Extension API +## Extension API + +# With hindsight, this API was poorly designed and would be worth fixing in the next breaking release. +# See issue #582 + +""" + edges = backedges(obj) + +Return an iterable of children (callers) of `obj`. An `edge` should support [`Cthulhu.method`](@ref) +and [`Cthulhu.specTypes`](@ref), as well as [`Cthulhu.nextnode`](@ref) to get the next node object. + +Part of the `ascend` API. +""" +function backedges end + +""" + inst = instance(obj) + +Return the current node data corresponding to `obj`. `inst` should support [`Cthulhu.method`](@ref) +and [`Cthulhu.specTypes`](@ref). + +Part of the `ascend` API. +""" +function instance end + +""" + m = method(edge) + +Return the method corresponding to `edge`. + +Part of the `ascend` API. +""" +function method end + +""" + sig = specTypes(edge) + +Return the signature of `edge`. + +Part of the `ascend` API. +""" +function specTypes end + +""" + childobj = nextnode(obj, edge) + +Return the next node object from `obj` indicated by `edge`. + +Part of the `ascend` API. +""" +function nextnode end + + backedges(mi::MethodInstance) = isdefined(mi, :backedges) ? mi.backedges : _emptybackedges method(mi::MethodInstance) = mi.def specTypes(mi::MethodInstance) = mi.specTypes instance(mi::MethodInstance) = mi nextnode(mi, edge) = edge +instance(@nospecialize(tt::Type)) = tt + instance(sfs::Vector{StackTraces.StackFrame}) = isempty(sfs) ? CC.Timings.ROOTmi : sfs[end].linfo::MethodInstance # we checked this type condition within `buildframes` method(sfs::Vector{StackTraces.StackFrame}) = method(instance(sfs)) backedges(sframes::Vector{StackTraces.StackFrame}) = (ret = sframes[2:end]; isempty(ret) ? () : (ret,)) @@ -102,9 +156,9 @@ backedges(sframes::Vector{StackTraces.StackFrame}) = (ret = sframes[2:end]; isem instance(ipframes::Vector{IPFrames}) = isempty(ipframes) ? CC.Timings.ROOTmi : instance(ipframes[1].sfs) backedges(ipframes::Vector{IPFrames}) = (ret = ipframes[2:end]; isempty(ret) ? () : (ret,)) -function callstring(io::IO, obj) - name = isa(method(obj), Method) ? method(obj).name : :toplevel - show_tuple_as_call(nonconcrete_red, IOContext(io, :color=>true), name, specTypes(obj)) +function callstring(io::IO, edge) + name = isa(method(edge), Method) ? method(edge).name : :toplevel + show_tuple_as_call(nonconcrete_red, IOContext(io, :color=>true), name, specTypes(edge)) return String(take!(io)) end function callstring(io::IO, sfs::Vector{StackTraces.StackFrame}) @@ -125,16 +179,18 @@ end function treelist(mi) io = IOBuffer() - str = callstring(io, mi) - treelist!(Node(Data(str, instance(mi))), io, mi, "", Base.IdSet{typeof(instance(mi))}()) + imi = instance(mi) + str = callstring(io, imi) + treelist!(Node(Data(str, imi)), io, mi, "", Base.IdSet{typeof(imi)}()) end function treelist!(parent::Node, io::IO, mi, indent::AbstractString, visited::Base.IdSet) - mi ∈ visited && return parent - push!(visited, instance(mi)) + imi = instance(mi) + imi ∈ visited && return parent + push!(visited, imi) indent *= " " for edge in backedges(mi) str = indent * callstring(io, edge) - child = Node(Data(str, instance(edge)), parent) + child = Node(typeof(parent.data)(str, instance(edge)), parent) treelist!(child, io, nextnode(mi, edge), indent, visited) end return parent diff --git a/src/callsite.jl b/src/callsite.jl index 5410c32f..a8959a83 100644 --- a/src/callsite.jl +++ b/src/callsite.jl @@ -7,17 +7,19 @@ struct MICallInfo <: CallInfo mi::MethodInstance rt effects::Effects - function MICallInfo(mi::MethodInstance, @nospecialize(rt), effects) + exct + function MICallInfo(mi::MethodInstance, @nospecialize(rt), effects, @nospecialize(exct=nothing)) if isa(rt, LimitedAccuracy) - return LimitedCallInfo(new(mi, ignorelimited(rt), effects)) + return LimitedCallInfo(new(mi, ignorelimited(rt), effects, exct)) else - return new(mi, rt, effects) + return new(mi, rt, effects, exct) end end end get_mi(ci::MICallInfo) = ci.mi -get_rt(ci::CallInfo) = ci.rt +get_rt(ci::MICallInfo) = ci.rt get_effects(ci::MICallInfo) = ci.effects +get_exct(ci::MICallInfo) = ci.exct abstract type WrappedCallInfo <: CallInfo end @@ -27,6 +29,7 @@ ignorewrappers(ci::WrappedCallInfo) = ignorewrappers(get_wrapped(ci)) get_mi(ci::WrappedCallInfo) = get_mi(ignorewrappers(ci)) get_rt(ci::WrappedCallInfo) = get_rt(ignorewrappers(ci)) get_effects(ci::WrappedCallInfo) = get_effects(ignorewrappers(ci)) +get_exct(ci::WrappedCallInfo) = get_exct(ignorewrappers(ci)) # only appears when inspecting pre-optimization states struct LimitedCallInfo <: WrappedCallInfo @@ -38,9 +41,12 @@ struct RTCallInfo <: CallInfo f argtyps rt + exct end +get_rt(ci::RTCallInfo) = ci.rt get_mi(ci::RTCallInfo) = nothing get_effects(ci::RTCallInfo) = Effects() +get_exct(ci::RTCallInfo) = ci.exct # uncached callsite, we can't recurse into this call struct UncachedCallInfo <: WrappedCallInfo @@ -56,6 +62,7 @@ end get_mi(::PureCallInfo) = nothing get_rt(pci::PureCallInfo) = pci.rt get_effects(::PureCallInfo) = EFFECTS_TOTAL +get_exct(::PureCallInfo) = Union{} # Failed struct FailedCallInfo <: CallInfo @@ -64,7 +71,8 @@ struct FailedCallInfo <: CallInfo end get_mi(ci::FailedCallInfo) = fail(ci) get_rt(ci::FailedCallInfo) = fail(ci) -get_effects(ci::FailedCallInfo) = Effects() +get_effects(ci::FailedCallInfo) = fail(ci) +get_exct(ci::FailedCallInfo) = fail(ci) function fail(ci::FailedCallInfo) @warn "MethodInstance extraction failed." ci.sig ci.rt return nothing @@ -77,7 +85,8 @@ struct GeneratedCallInfo <: CallInfo end get_mi(genci::GeneratedCallInfo) = fail(genci) get_rt(genci::GeneratedCallInfo) = fail(genci) -get_effects(genci::GeneratedCallInfo) = Effects() +get_effects(genci::GeneratedCallInfo) = fail(genci) +get_exct(genci::GeneratedCallInfo) = fail(genci) function fail(genci::GeneratedCallInfo) @warn "Can't extract MethodInstance from call to generated functions." genci.sig genci.rt return nothing @@ -86,13 +95,16 @@ end struct MultiCallInfo <: CallInfo sig rt + exct callinfos::Vector{CallInfo} + MultiCallInfo(@nospecialize(sig), @nospecialize(rt), callinfos::Vector{CallInfo}, + @nospecialize(exct=nothing)) = + new(sig, rt, exct, callinfos) end -# actual code-error get_mi(ci::MultiCallInfo) = error("Can't extract MethodInstance from multiple call informations") -get_effects(mci::MultiCallInfo) = @static VERSION ≥ v"1.9-" ? - mapreduce(get_effects, CC.merge_effects, mci.callinfos) : - mapreduce(get_effects, CC.tristate_merge, mci.callinfos) +get_rt(ci::MultiCallInfo) = ci.rt +get_effects(mci::MultiCallInfo) = mapreduce(get_effects, CC.merge_effects, mci.callinfos) +get_exct(ci::MultiCallInfo) = ci.exct struct TaskCallInfo <: CallInfo ci::CallInfo @@ -100,6 +112,7 @@ end get_mi(tci::TaskCallInfo) = get_mi(tci.ci) get_rt(tci::TaskCallInfo) = get_rt(tci.ci) get_effects(tci::TaskCallInfo) = get_effects(tci.ci) +get_exct(tci::TaskCallInfo) = get_exct(tci.ci) struct InvokeCallInfo <: CallInfo ci::CallInfo @@ -108,6 +121,7 @@ end get_mi(ici::InvokeCallInfo) = get_mi(ici.ci) get_rt(ici::InvokeCallInfo) = get_rt(ici.ci) get_effects(ici::InvokeCallInfo) = get_effects(ici.ci) +get_exct(ici::InvokeCallInfo) = get_exct(ici.ci) # OpaqueClosure CallInfo struct OCCallInfo <: CallInfo @@ -117,6 +131,7 @@ end get_mi(occi::OCCallInfo) = get_mi(occi.ci) get_rt(occi::OCCallInfo) = get_rt(occi.ci) get_effects(occi::OCCallInfo) = get_effects(occi.ci) +get_exct(occi::OCCallInfo) = get_exct(occi.ci) # Special handling for ReturnTypeCall struct ReturnTypeCallInfo <: CallInfo @@ -125,6 +140,7 @@ end get_mi((; vmi)::ReturnTypeCallInfo) = isa(vmi, FailedCallInfo) ? nothing : get_mi(vmi) get_rt((; vmi)::ReturnTypeCallInfo) = Type{isa(vmi, FailedCallInfo) ? Union{} : widenconst(get_rt(vmi))} get_effects(::ReturnTypeCallInfo) = EFFECTS_TOTAL +get_exct(::ReturnTypeCallInfo) = Union{} # FIXME struct ConstPropCallInfo <: CallInfo mi::CallInfo @@ -133,6 +149,7 @@ end get_mi(cpci::ConstPropCallInfo) = cpci.result.linfo get_rt(cpci::ConstPropCallInfo) = get_rt(cpci.mi) get_effects(cpci::ConstPropCallInfo) = get_effects(cpci.result) +get_exct(cpci::ConstPropCallInfo) = get_exct(cpci.mi) struct ConcreteCallInfo <: CallInfo mi::CallInfo @@ -141,6 +158,7 @@ end get_mi(ceci::ConcreteCallInfo) = get_mi(ceci.mi) get_rt(ceci::ConcreteCallInfo) = get_rt(ceci.mi) get_effects(ceci::ConcreteCallInfo) = get_effects(ceci.mi) +get_exct(cici::ConcreteCallInfo) = get_exct(ceci.mi) struct SemiConcreteCallInfo <: CallInfo mi::CallInfo @@ -149,6 +167,7 @@ end get_mi(scci::SemiConcreteCallInfo) = get_mi(scci.mi) get_rt(scci::SemiConcreteCallInfo) = get_rt(scci.mi) get_effects(scci::SemiConcreteCallInfo) = get_effects(scci.mi) +get_exct(scci::SemiConcreteCallInfo) = get_exct(scci.mi) # CUDA callsite struct CuCallInfo <: CallInfo @@ -189,22 +208,22 @@ function headstring(@nospecialize(T)) end end -function __show_limited(limiter, name, tt, @nospecialize(rt), effects) +function __show_limited(limiter, name, tt, @nospecialize(rt), effects, @nospecialize(exct=nothing)) vastring(@nospecialize(T)) = (isvarargtype(T) ? headstring(T)*"..." : string(T)::String) # 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 - if with_effects - limiter.width += textwidth(repr(effects)) + 1 - end - + with_effects && (limiter.width += textwidth(repr(effects)) + 1) + exception_type && (limiter.width += textwidth(string(exct)) + 1) if !has_space(limiter, name) print(limiter, '…') @goto print_effects end + print(limiter, string(name)) pstrings = String[vastring(T) for T in tt] headstrings = String[ @@ -236,15 +255,28 @@ function __show_limited(limiter, name, tt, @nospecialize(rt), effects) print(limiter, "::…") end -@label print_effects + @label print_effects if with_effects # Print effects unlimited print(limiter.io, " ", effects) end + if exception_type + print(limiter.io, ' ', ExctWrapper(exct)) + end return nothing end +struct ExctWrapper + exct + ExctWrapper(@nospecialize exct) = new(exct) +end + +function Base.show(io::IO, (;exct)::ExctWrapper) + color = exct === Union{} ? :green : :yellow + printstyled(io, "(↑::", exct, ")"; color) +end + function show_callinfo(limiter, mici::MICallInfo) mi = mici.mi tt = (Base.unwrap_unionall(mi.specTypes)::DataType).parameters[2:end] @@ -254,7 +286,8 @@ function show_callinfo(limiter, mici::MICallInfo) name = mi.def.name end rt = get_rt(mici) - __show_limited(limiter, name, tt, rt, get_effects(mici)) + exct = get_exct(mici) + __show_limited(limiter, name, tt, rt, get_effects(mici), exct) end function show_callinfo(limiter, ci::Union{MultiCallInfo, FailedCallInfo, GeneratedCallInfo}) @@ -380,8 +413,7 @@ function print_callsite_info(limiter::IO, info::OCCallInfo) show_callinfo(limiter, info.ci) end -const is_expected_union = @static VERSION ≥ v"1.9-" ? - InteractiveUtils.is_expected_union : Base.is_expected_union +const is_expected_union = InteractiveUtils.is_expected_union function Base.show(io::IO, c::Callsite) limit = get(io, :limit, false)::Bool @@ -474,6 +506,7 @@ end # maybe_callsite for higher-level inputs maybe_callsite(cs::Callsite, callee::MethodInstance) = maybe_callsite(cs.info, callee) +maybe_callsite(cs::Callsite, @nospecialize(tt::Type)) = maybe_callsite(cs.info, tt) maybe_callsite(info::CallInfo, callee::MethodInstance) = maybe_callsite(get_mi(info), callee) # Special CallInfo cases: function maybe_callsite(info::MultiCallInfo, callee::MethodInstance) @@ -485,4 +518,19 @@ end maybe_callsite(info::PureCallInfo, mi::MethodInstance) = mi.specTypes <: Tuple{mapany(Core.Typeof ∘ unwrapconst, info.argtypes)...} maybe_callsite(info::RTCallInfo, mi::MethodInstance) = false +function maybe_callsite(info::RTCallInfo, @nospecialize(tt::Type)) + isa(tt, Union) && return maybe_callsite(info, tt.a) || maybe_callsite(info, tt.b) + isa(tt, DataType) || return false + typeof(info.f) === tt.parameters[1] || return false + for (a, b) in zip(info.argtyps, tt.parameters[2:end]) + a === b || return false + end + return true +end +function maybe_callsite(info::MICallInfo, @nospecialize(tt::Type)) + return tt <: info.mi.specTypes +end + +maybe_callsite(info::CallInfo, @nospecialize(tt::Type)) = false + unwrapconst(@nospecialize(arg)) = arg isa Core.Const ? arg.val : arg diff --git a/src/codeview.jl b/src/codeview.jl index 8678490e..bc55aa42 100644 --- a/src/codeview.jl +++ b/src/codeview.jl @@ -20,44 +20,28 @@ function highlight(io, x, lexer, config::CthulhuConfig) end end -function cthulhu_llvm(io::IO, mi, optimize, debuginfo, interp::CthulhuInterpreter, +function cthulhu_llvm(io::IO, mi, optimize::Bool, debuginfo, world::UInt, config::CthulhuConfig, dump_module::Bool=false, raw::Bool=false) - dump = @static if VERSION ≥ v"1.10.0-DEV.1386" - InteractiveUtils._dump_function_llvm( - mi, get_world_counter(interp), - #=wrapper=# false, !raw, - dump_module, optimize, debuginfo != DInfo.none ? :source : :none, - Base.CodegenParams()) - else - InteractiveUtils._dump_function_linfo_llvm( - mi, get_world_counter(interp), - #=wrapper=# false, #=strip_ir_metadata=# true, - dump_module, optimize, debuginfo != DInfo.none ? :source : :none, - Base.CodegenParams()) - end + dump = InteractiveUtils._dump_function_llvm( + mi, world, + #=wrapper=# false, !raw, + dump_module, optimize, debuginfo != DInfo.none ? :source : :none, + Base.CodegenParams()) highlight(io, dump, "llvm", config) end -function cthulhu_native(io::IO, mi, optimize, debuginfo, interp::CthulhuInterpreter, +function cthulhu_native(io::IO, mi, ::Bool, debuginfo, world::UInt, config::CthulhuConfig, dump_module::Bool=false, raw::Bool=false) - dump = @static if VERSION ≥ v"1.10.0-DEV.1386" - if dump_module - InteractiveUtils._dump_function_native_assembly( - mi, get_world_counter(interp), - #=wrapper=# false, #=syntax=# config.asm_syntax, - debuginfo != DInfo.none ? :source : :none, - #=binary=# false, raw, - Base.CodegenParams()) - else - InteractiveUtils._dump_function_native_disassembly( - mi, get_world_counter(interp), - #=wrapper=# false, #=syntax=# config.asm_syntax, - debuginfo != DInfo.none ? :source : :none, - #=binary=# false) - end + if dump_module + dump = InteractiveUtils._dump_function_native_assembly( + mi, world, + #=wrapper=# false, #=syntax=# config.asm_syntax, + debuginfo != DInfo.none ? :source : :none, + #=binary=# false, raw, + Base.CodegenParams()) else - InteractiveUtils._dump_function_linfo_native( - mi, get_world_counter(interp), + dump = InteractiveUtils._dump_function_native_disassembly( + mi, world, #=wrapper=# false, #=syntax=# config.asm_syntax, debuginfo != DInfo.none ? :source : :none, #=binary=# false) @@ -65,7 +49,7 @@ function cthulhu_native(io::IO, mi, optimize, debuginfo, interp::CthulhuInterpre highlight(io, dump, "asm", config) end -function cthulhu_ast(io::IO, mi, optimize, debuginfo, ::CthulhuInterpreter, config::CthulhuConfig) +function cthulhu_ast(io::IO, mi, ::Bool, debuginfo, ::UInt, config::CthulhuConfig) meth = mi.def::Method ast = definition(Expr, meth) if ast!==nothing @@ -101,11 +85,12 @@ is_type_unstable(@nospecialize(type)) = type isa Type && (!Base.isdispatchelem(t 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, mi::Union{Nothing,MethodInstance}=nothing; - hide_type_stable::Bool=false, inline_cost::Bool=false, interp::CthulhuInterpreter=CthulhuInterpreter()) + 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, effects, mi; iswarn=true, hide_type_stable, inline_cost, interp) + cthulhu_typed(io, debuginfo, src, rt, nothing, effects, mi; iswarn=true, optimize, hide_type_stable, inline_cost, interp) return nothing end @@ -120,10 +105,14 @@ 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), effects::Effects, mi::Union{Nothing,MethodInstance}; - iswarn::Bool=false, hide_type_stable::Bool=false, - pc2remarks::Union{Nothing,PC2Remarks}=nothing, pc2effects::Union{Nothing,PC2Effects}=nothing, + src::Union{CodeInfo,IRCode}, @nospecialize(rt), @nospecialize(exct), + effects::Effects, mi::Union{Nothing,MethodInstance}; + 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()) debuginfo = IRShow.debuginfo(debuginfo) @@ -138,14 +127,49 @@ function cthulhu_typed(io::IO, debuginfo::Symbol, # We empty the body when filling kwargs istruncated = isempty(children(body)) idxend = istruncated ? JuliaSyntax.last_byte(sig) : lastindex(tsn.source) - if any(iszero, src.codelocs) - @warn "Some line information is missing, type-assignment may be incomplete" - end if src.slottypes === nothing @warn "Inference terminated in an incomplete state due to argument-type changes during recursion" end - printstyled(lambda_io, tsn; type_annotations, iswarn, hide_type_stable, idxend) - println(lambda_io) + + 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 + ) + + 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 + + callsite_diagnostics = TypedSyntax.Diagnostic[] + if (diagnostics_vscode || inlay_types_vscode) + vscode_io = IOContext(devnull, :inlay_hints=>vscode_io[:inlay_hints], :diagnostics=>vscode_io[:diagnostics]) + callsite_mis = Dict() # type annotation is a bit long so I skipped it, doesn't seem to affect performance + visited_mis = Set{MethodInstance}((mi,)) + add_callsites!(callsite_mis, visited_mis, callsite_diagnostics, mi; optimize, annotate_source, interp) + for callsite in values(callsite_mis) + if !isnothing(callsite) + descend_into_callsite!(vscode_io, callsite.tsn; iswarn, hide_type_stable, type_annotations) + end + end + end + + !isnothing(vscode_io[:diagnostics]) && append!(callsite_diagnostics, vscode_io[:diagnostics]) + 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 end @@ -155,7 +179,6 @@ function cthulhu_typed(io::IO, debuginfo::Symbol, # we're working on pre-optimization state, need to ignore `LimitedAccuracy` src = copy(src) src.ssavaluetypes = mapany(ignorelimited, src.ssavaluetypes::Vector{Any}) - src.rettype = ignorelimited(src.rettype) if src.slotnames !== nothing slotnames = Base.sourceinfo_slotnames(src) @@ -167,10 +190,23 @@ function cthulhu_typed(io::IO, debuginfo::Symbol, # preprinter configuration preprinter = if src isa IRCode && inline_cost isa(mi, MethodInstance) || throw("`mi::MethodInstance` is required") - code = src isa IRCode ? src.stmts.inst : src.code + if isa(src, IRCode) + @static if VERSION < v"1.11.0-DEV.258" + code = src.stmts.inst + else + code = src.stmts.stmt + end + else + code = src.code + end cst = Vector{Int}(undef, length(code)) params = CC.OptimizationParams(interp) - CC.statement_costs!(cst, code, src, sptypes(mi.sparam_vals), false, params) + sparams = CC.VarState[CC.VarState(sparam, false) for sparam in mi.sparam_vals] + @static if VERSION ≥ v"1.11.0-DEV.32" + CC.statement_costs!(cst, code, src, sparams, params) + else + CC.statement_costs!(cst, code, src, sparams, false, params) + end total_cost = sum(cst) nd = ndigits(total_cost) _lineprinter = lineprinter(src) @@ -196,28 +232,27 @@ function cthulhu_typed(io::IO, debuginfo::Symbol, end end # postprinter configuration - __postprinter = if type_annotations + ___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) - @static if VERSION ≥ v"1.9-" - 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 - function (io::IO, @nospecialize(typ), used::Bool) - __postprinter(io, typ, used) - haskey(io, :idx) || return - idx = io[:idx]::Int - local effects = get(pc2effects, idx, nothing) - effects === nothing && return - print(io, ' ', effects) - end + __postprinter = if isa(src, CodeInfo) && !isnothing(pc2effects) + 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) + 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 @@ -225,21 +260,10 @@ function cthulhu_typed(io::IO, debuginfo::Symbol, postprinter = if isa(src, CodeInfo) && !isnothing(pc2remarks) sort!(pc2remarks) unique!(pc2remarks) # abstract interpretation may have visited a same statement multiple times - @static if VERSION ≥ v"1.9-" - function (io::IO; idx::Int, @nospecialize(kws...)) - _postprinter(io; idx, kws...) - for i = searchsorted(pc2remarks, idx=>"", by=((idx,msg),)->idx) - printstyled(io, ' ', pc2remarks[i].second; color=:light_black) - end - end - else - function (io::IO, @nospecialize(typ), used::Bool) - _postprinter(io, typ, used) - haskey(io, :idx) || return - idx = io[:idx]::Int - for i = searchsorted(pc2remarks, idx=>"", by=((idx,msg),)->idx) - printstyled(io, ' ', pc2remarks[i].second; color=:light_black) - end + function (io::IO; idx::Int, @nospecialize(kws...)) + _postprinter(io; idx, kws...) + for i = searchsorted(pc2remarks, idx=>"", by=((idx,msg),)->idx) + printstyled(io, ' ', pc2remarks[i].second; color=:light_black) end end else @@ -253,12 +277,7 @@ function cthulhu_typed(io::IO, debuginfo::Symbol, if iswarn print(lambda_io, "Body") - @static if VERSION ≥ v"1.9-" - # https://github.com/JuliaLang/julia/pull/46574 - InteractiveUtils.warntype_type_printer(lambda_io; type=rettype, used=true) - else - InteractiveUtils.warntype_type_printer(lambda_io, rettype, true) - end + InteractiveUtils.warntype_type_printer(lambda_io; type=rettype, used=true) if get(lambda_io, :with_effects, false)::Bool print(lambda_io, ' ', effects) end @@ -268,7 +287,7 @@ function cthulhu_typed(io::IO, debuginfo::Symbol, cfg = src isa IRCode ? src.cfg : Core.Compiler.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, MICallInfo(mi, rettype, effects), :invoke) + callsite = Callsite(0, MICallInfo(mi, rettype, effects, exct), :invoke) println(lambda_io, "∘ ", "─"^(max_bb_idx_size), str, " ", callsite) end @@ -276,11 +295,76 @@ function cthulhu_typed(io::IO, debuginfo::Symbol, return nothing end -@static if VERSION >= v"1.10.0-DEV.552" - using Core.Compiler: VarState - sptypes(sparams) = VarState[VarState.(sparams, false)...] -else - sptypes(sparams) = Any[sparams...] +function descend_into_callsite!(io::IO, tsn::TypedSyntaxNode; + iswarn::Bool, hide_type_stable::Bool, type_annotations::Bool) + sig, body = children(tsn) + # We empty the body when filling kwargs + istruncated = isempty(children(body)) + idxend = istruncated ? JuliaSyntax.last_byte(sig) : lastindex(tsn.source) + if !istruncated # If method only fills in default arguments + printstyled(io, tsn; type_annotations, iswarn, hide_type_stable, idxend) + end +end + +function add_callsites!(d::AbstractDict, visited_mis::AbstractSet, diagnostics::AbstractVector, + mi::MethodInstance, source_mi::MethodInstance=mi; + optimize::Bool=true, annotate_source::Bool=false, + interp::AbstractInterpreter=CthulhuInterpreter()) + + callsites, src, rt = try + (; src, rt, infos, slottypes, effects, codeinf) = lookup(interp, mi, optimize & !annotate_source) + + src = preprocess_ci!(src, mi, optimize & !annotate_source, CONFIG) + if (optimize & !annotate_source) || isa(src, IRCode) # optimization might have deleted some statements + infos = src.stmts.info + else + @assert length(src.code) == length(infos) + end + + # 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, mi, slottypes, optimize & !annotate_source, false) + callsites, src, rt + catch + return nothing + end + + for callsite in callsites + callsite_mi = callsite.info isa MultiCallInfo ? nothing : get_mi(callsite) + + if !isnothing(callsite_mi) && callsite_mi ∉ visited_mis + push!(visited_mis, callsite_mi) + add_callsites!(d, visited_mis, diagnostics, callsite_mi, source_mi; optimize, annotate_source, interp) + end + end + + # Check if callsite is not just filling in default arguments and defined in same file as source_mi + if mi == source_mi || mi.def.file != source_mi.def.file + return nothing + end + tsn, _ = get_typed_sourcetext(mi, src, rt; warn=false) + isnothing(tsn) && return nothing + sig, body = children(tsn) + # We empty the body when filling kwargs + istruncated = isempty(children(body)) + istruncated && return nothing + # We add new callsites unless we would have multiple callsites for the same source definition, + # e.g. if f(x) = x is called with different types we print nothing. + key = (mi.def.file, mi.def.line) + if haskey(d, key) + if !isnothing(d[key]) && mi != d[key].mi + d[key] = nothing + push!(diagnostics, + TypedSyntax.Diagnostic( + isnothing(functionloc(mi)[1]) ? string(mi.file) : functionloc(mi)[1], mi.def.line, + TypedSyntax.DiagnosticKinds.Information, + "Cthulhu disabled: This function was called multiple times with different argument types" + ) + ) + end + else + d[key] = (mi=mi, tsn=tsn) + end end function show_variables(io, src, slotnames) @@ -289,12 +373,7 @@ function show_variables(io, src, slotnames) for i = 1:length(slotnames) print(io, " ", slotnames[i]) if isa(slottypes, Vector{Any}) - @static if VERSION ≥ v"1.9-" - # https://github.com/JuliaLang/julia/pull/46574 - InteractiveUtils.warntype_type_printer(io; type=slottypes[i], used=true) - else - InteractiveUtils.warntype_type_printer(io, slottypes[i], true) - end + InteractiveUtils.warntype_type_printer(io; type=slottypes[i], used=true) end println(io) end @@ -342,7 +421,7 @@ const BOOKMARKS = Bookmark[] 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_world_counter(b.interp) + world = get_inference_world(b.interp) CI, rt = InteractiveUtils.code_typed(b; optimize) (; interp, mi) = b (; effects) = lookup(interp, mi, optimize) @@ -352,7 +431,7 @@ function Base.show( return end println(io, "Cthulhu.Bookmark (world: ", world, ")") - cthulhu_typed(io, debuginfo, CI, rt, effects, b.mi; iswarn, hide_type_stable, b.interp) + cthulhu_typed(io, debuginfo, CI, rt, nothing, effects, b.mi; iswarn, optimize, hide_type_stable, b.interp) end function InteractiveUtils.code_typed(b::Bookmark; optimize::Bool=true) @@ -360,12 +439,7 @@ function InteractiveUtils.code_typed(b::Bookmark; optimize::Bool=true) (; src, rt, codeinf) = lookup(interp, mi, optimize) src = preprocess_ci!(src, mi, optimize, CONFIG) if src isa IRCode - @static if VERSION ≥ v"1.10.0-DEV.870" - CC.replace_code_newstyle!(codeinf, src) - else - nargs = Int((mi.def::Method).nargs) - 1 - CC.replace_code_newstyle!(codeinf, src, nargs) - end + CC.replace_code_newstyle!(codeinf, src) end return codeinf => rt end @@ -383,10 +457,10 @@ function InteractiveUtils.code_warntype( CI, rt = InteractiveUtils.code_typed(b; kw...) (; interp, mi) = b (; effects) = lookup(interp, mi, optimize) - cthulhu_warntype(io, debuginfo, CI, rt, effects, b.mi; hide_type_stable, b.interp) + cthulhu_warntype(io, debuginfo, CI, rt, effects, b.mi; optimize, hide_type_stable, b.interp) end -InteractiveUtils.code_llvm(b::Bookmark) = InteractiveUtils.code_llvm(stdout::IO, b) +InteractiveUtils.code_llvm(b::Bookmark; kw...) = InteractiveUtils.code_llvm(stdout::IO, b; kw...) InteractiveUtils.code_llvm( io::IO, b::Bookmark; @@ -400,7 +474,7 @@ InteractiveUtils.code_llvm( b.mi, optimize, debuginfo === :source, - b.interp, + get_inference_world(b.interp), config, dump_module, raw, @@ -421,7 +495,7 @@ InteractiveUtils.code_native( b.mi, optimize, debuginfo === :source, - b.interp, + get_inference_world(b.interp), config, dump_module, raw, diff --git a/src/interface.jl b/src/interface.jl index c7e5e04c..9c9428b5 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -68,20 +68,23 @@ missing `$AbstractCursor` API: """) navigate(curs::CthulhuCursor, callsite::Callsite) = CthulhuCursor(get_mi(callsite)) -get_remarks(::AbstractInterpreter, ::Union{MethodInstance,InferenceResult}) = nothing -get_remarks(interp::CthulhuInterpreter, key::Union{MethodInstance,InferenceResult}) = get(interp.remarks, key, nothing) -get_remarks(::AbstractInterpreter, ::SemiConcreteCallInfo) = PC2Remarks() +get_remarks(::AbstractInterpreter, ::InferenceKey) = nothing +get_remarks(interp::CthulhuInterpreter, key::InferenceKey) = get(interp.remarks, key, nothing) -get_effects(::AbstractInterpreter, ::Union{MethodInstance,InferenceResult}) = nothing -get_effects(interp::CthulhuInterpreter, key::Union{MethodInstance,InferenceResult}) = get(interp.effects, key, nothing) -get_effects(::AbstractInterpreter, ::SemiConcreteCallInfo) = PC2Effects() +get_effects(::AbstractInterpreter, ::InferenceKey) = nothing +get_effects(interp::CthulhuInterpreter, key::InferenceKey) = get(interp.effects, key, nothing) + +get_excts(::AbstractInterpreter, ::InferenceKey) = nothing +get_excts(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, mi::MethodInstance) = CthulhuCursor(mi) -get_effects(interp::CthulhuInterpreter, mi::MethodInstance, opt::Bool) = - get_effects(opt ? interp.opt : interp.unopt, mi) +function get_effects(interp::CthulhuInterpreter, mi::MethodInstance, opt::Bool) + effects = opt ? interp.opt : interp.unopt + return haskey(effects, mi) ? get_effects(effects[mi]) : Effects() +end mutable struct CustomToggle onoff::Bool diff --git a/src/interpreter.jl b/src/interpreter.jl index a2968acb..d25f9285 100644 --- a/src/interpreter.jl +++ b/src/interpreter.jl @@ -1,21 +1,16 @@ -using .CC: AbstractInterpreter, NativeInterpreter, InferenceState, OptimizationState, - CodeInfo, CodeInstance, InferenceResult, WorldRange, WorldView, IRCode, SSAValue - -@static if VERSION ≥ v"1.9-" - const CCCallInfo = CC.CallInfo - const NoCallInfo = CC.NoCallInfo -else - const CCCallInfo = Any - const NoCallInfo = Nothing -end +using .CC: AbstractInterpreter, CallInfo as CCCallInfo, CodeInfo, CodeInstance, + InferenceParams, InferenceResult, InferenceState, IRCode, NativeInterpreter, + NoCallInfo, OptimizationParams, OptimizationState, SSAValue, WorldRange, WorldView struct InferredSource src::CodeInfo stmt_info::Vector{CCCallInfo} effects::Effects rt::Any - InferredSource(src::CodeInfo, stmt_info::Vector{CCCallInfo}, effects, @nospecialize(rt)) = - new(src, stmt_info, effects, rt) + exct::Any + InferredSource(src::CodeInfo, stmt_info::Vector{CCCallInfo}, effects, @nospecialize(rt), + @nospecialize(exct)) = + new(src, stmt_info, effects, rt, exct) end struct OptimizedSource @@ -25,187 +20,205 @@ struct OptimizedSource effects::Effects end +const InferenceKey = Union{MethodInstance,InferenceResult} +const InferenceDict{T} = IdDict{InferenceKey, T} +const OptimizationDict = IdDict{MethodInstance, CodeInstance} const PC2Remarks = Vector{Pair{Int, String}} const PC2Effects = Dict{Int, Effects} +const PC2Excts = Dict{Int, Any} struct CthulhuInterpreter <: AbstractInterpreter native::AbstractInterpreter - unopt::Dict{Union{MethodInstance,InferenceResult}, InferredSource} - opt::Dict{MethodInstance, CodeInstance} + unopt::InferenceDict{InferredSource} + opt::OptimizationDict - remarks::Dict{Union{MethodInstance,InferenceResult}, PC2Remarks} - effects::Dict{Union{MethodInstance,InferenceResult}, PC2Effects} + remarks::InferenceDict{PC2Remarks} + effects::InferenceDict{PC2Effects} + exception_types::InferenceDict{PC2Excts} end function CthulhuInterpreter(interp::AbstractInterpreter=NativeInterpreter()) return CthulhuInterpreter( interp, - Dict{Union{MethodInstance,InferenceResult}, InferredSource}(), - Dict{MethodInstance, CodeInstance}(), - Dict{Union{MethodInstance,InferenceResult}, PC2Remarks}(), - Dict{Union{MethodInstance,InferenceResult}, PC2Effects}()) + InferenceDict{InferredSource}(), + OptimizationDict(), + InferenceDict{PC2Remarks}(), + InferenceDict{PC2Effects}(), + InferenceDict{PC2Excts}()) end -import .CC: InferenceParams, OptimizationParams, get_world_counter, - get_inference_cache, code_cache, lock_mi_inference, unlock_mi_inference, method_table, - inlining_policy -using Base: @invoke - CC.InferenceParams(interp::CthulhuInterpreter) = InferenceParams(interp.native) +@static if VERSION ≥ v"1.11.0-DEV.851" +CC.OptimizationParams(interp::CthulhuInterpreter) = + OptimizationParams(OptimizationParams(interp.native); preserve_local_sources=true) +else CC.OptimizationParams(interp::CthulhuInterpreter) = OptimizationParams(interp.native) -CC.get_world_counter(interp::CthulhuInterpreter) = get_world_counter(interp.native) -CC.get_inference_cache(interp::CthulhuInterpreter) = get_inference_cache(interp.native) - -# No need to do any locking since we're not putting our results into the runtime cache -CC.lock_mi_inference(interp::CthulhuInterpreter, mi::MethodInstance) = nothing -CC.unlock_mi_inference(interp::CthulhuInterpreter, mi::MethodInstance) = nothing -CC.method_table(interp::CthulhuInterpreter) = method_table(interp.native) -struct CthulhuCache - cache::Dict{MethodInstance, CodeInstance} end - -CC.code_cache(interp::CthulhuInterpreter) = WorldView(CthulhuCache(interp.opt), WorldRange(get_world_counter(interp))) -CC.get(wvc::WorldView{CthulhuCache}, mi::MethodInstance, default) = get(wvc.cache.cache, mi, default) -CC.haskey(wvc::WorldView{CthulhuCache}, mi::MethodInstance) = haskey(wvc.cache.cache, mi) -CC.setindex!(wvc::WorldView{CthulhuCache}, ci::CodeInstance, mi::MethodInstance) = setindex!(wvc.cache.cache, ci, mi) +#=CC.=#get_inference_world(interp::CthulhuInterpreter) = get_inference_world(interp.native) +CC.get_inference_cache(interp::CthulhuInterpreter) = CC.get_inference_cache(interp.native) CC.may_optimize(interp::CthulhuInterpreter) = true CC.may_compress(interp::CthulhuInterpreter) = false CC.may_discard_trees(interp::CthulhuInterpreter) = false CC.verbose_stmt_info(interp::CthulhuInterpreter) = true +CC.method_table(interp::CthulhuInterpreter) = CC.method_table(interp.native) + +@static if VERSION ≥ v"1.11.0-DEV.1552" +# Since the cache for `CthulhuInterpreter` is volatile and does not involve with the +# internal code cache, technically, there's no requirement to supply `cache_owner` as an +# identifier for the internal code cache. However, the definition of `cache_owner` is +# necessary for utilizing the default `CodeInstance` constructor, define the overload here. +struct CthulhuCacheToken + token +end +CC.cache_owner(interp::CthulhuInterpreter) = CthulhuCacheToken(CC.cache_owner(interp.native)) +end + +struct CthulhuCache + cache::OptimizationDict +end +CC.code_cache(interp::CthulhuInterpreter) = WorldView(CthulhuCache(interp.opt), WorldRange(get_inference_world(interp))) +CC.get(wvc::WorldView{CthulhuCache}, mi::MethodInstance, default) = get(wvc.cache.cache, mi, default) +CC.haskey(wvc::WorldView{CthulhuCache}, mi::MethodInstance) = haskey(wvc.cache.cache, mi) +CC.setindex!(wvc::WorldView{CthulhuCache}, ci::CodeInstance, mi::MethodInstance) = setindex!(wvc.cache.cache, ci, mi) + function CC.add_remark!(interp::CthulhuInterpreter, sv::InferenceState, msg) - key = CC.any(sv.result.overridden_by_const) ? sv.result : sv.linfo + key = (@static VERSION ≥ v"1.12.0-DEV.317" ? CC.is_constproped(sv) : CC.any(sv.result.overridden_by_const)) ? sv.result : sv.linfo push!(get!(PC2Remarks, interp.remarks, key), sv.currpc=>msg) end -@static if VERSION ≥ v"1.9-" function CC.merge_effects!(interp::CthulhuInterpreter, sv::InferenceState, effects::Effects) - key = CC.any(sv.result.overridden_by_const) ? sv.result : sv.linfo + key = (@static VERSION ≥ v"1.12.0-DEV.317" ? CC.is_constproped(sv) : CC.any(sv.result.overridden_by_const)) ? sv.result : sv.linfo pc2effects = get!(interp.effects, key, PC2Effects()) pc2effects[sv.currpc] = CC.merge_effects(get!(pc2effects, sv.currpc, EFFECTS_TOTAL), effects) @invoke CC.merge_effects!(interp::AbstractInterpreter, sv::InferenceState, effects::Effects) end -end - -@static if VERSION ≤ v"1.10.0-DEV.221" -function CC.type_annotate!(interp::CthulhuInterpreter, sv::InferenceState, run_optimizer::Bool) - changemap = @invoke CC.type_annotate!(interp::AbstractInterpreter, sv::InferenceState, run_optimizer::Bool) - changemap === nothing && return nothing - key = CC.any(sv.result.overridden_by_const) ? sv.result : sv.linfo - pc2remarks = get(interp.remarks, key, nothing) - if pc2remarks !== nothing - sort!(pc2remarks) - unique!(pc2remarks) - for (idx, v) in enumerate(changemap) - if v == typemin(Int) - for i = searchsorted(pc2remarks, idx=>"", by=((idx,msg),)->idx) - @assert false "remarks found in unreached region" - end - end - end - for (idx, v) in enumerate(changemap) - if v < 0 - for i = searchsorted(pc2remarks, idx=>"", by=((idx,msg),)->idx) - pc2remarks[i] = pc2remarks[i].first+v => pc2remarks[i].second - end - end - end - end - pc2effects = get(interp.effects, key, nothing) - if pc2effects !== nothing - for (idx, v) in enumerate(changemap) - if v == typemin(Int) - delete!(pc2effects, idx) - end - end - for (idx, v) in enumerate(changemap) - if v < 0 - haskey(pc2effects, idx) || continue - pc2effects[idx+v] = pc2effects[idx] - delete!(pc2effects, idx) - end - end - end - return changemap -end -end function InferredSource(state::InferenceState) unoptsrc = copy(state.src) - @static if VERSION < v"1.10.0-DEV.1033" - # xref: https://github.com/JuliaLang/julia/pull/49378 - unoptsrc.slottypes = let slottypes = unoptsrc.slottypes - slottypes === nothing ? nothing : copy(slottypes) - end - end + exct = @static VERSION ≥ v"1.11.0-DEV.207" ? state.result.exc_result : nothing return InferredSource( unoptsrc, copy(state.stmt_info), - isdefined(CC, :Effects) ? state.ipo_effects : nothing, - state.result.result) + state.ipo_effects, + state.result.result, + exct) end -function CC.finish(state::InferenceState, interp::CthulhuInterpreter) - res = @invoke CC.finish(state::InferenceState, interp::AbstractInterpreter) - key = CC.any(state.result.overridden_by_const) ? state.result : state.linfo +function cthulhu_finish(@specialize(finishfunc), state::InferenceState, interp::CthulhuInterpreter) + res = @invoke finishfunc(state::InferenceState, interp::AbstractInterpreter) + key = (@static VERSION ≥ v"1.12.0-DEV.317" ? CC.is_constproped(state) : CC.any(state.result.overridden_by_const)) ? state.result : state.linfo interp.unopt[key] = InferredSource(state) return res end -function create_cthulhu_source(@nospecialize(x), effects::Effects) - isa(x, OptimizationState) || return x - ir = x.ir::IRCode - return OptimizedSource(ir, x.src, x.src.inlineable, effects) +function create_cthulhu_source(@nospecialize(opt), effects::Effects) + isa(opt, OptimizationState) || return opt + @static if VERSION ≥ v"1.11-" + # 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))) + else + # TODO do the round-trip here? + ir = CC.copy(opt.ir::IRCode) + end + return OptimizedSource(ir, opt.src, opt.src.inlineable, effects) +end + +@static if VERSION ≥ v"1.12.0-DEV.734" +CC.finishinfer!(state::InferenceState, interp::CthulhuInterpreter) = cthulhu_finish(CC.finishinfer!, state, interp) +function CC.finish!(interp::CthulhuInterpreter, caller::InferenceState; + can_discard_trees::Bool=false) + result = caller.result + result.src = create_cthulhu_source(result.src, result.ipo_effects) + return @invoke CC.finish!(interp::AbstractInterpreter, caller::InferenceState; + can_discard_trees) +end + +elseif VERSION ≥ v"1.11.0-DEV.737" +CC.finish(state::InferenceState, interp::CthulhuInterpreter) = cthulhu_finish(CC.finish, state, interp) +function CC.finish!(interp::CthulhuInterpreter, caller::InferenceState) + result = caller.result + opt = result.src + result.src = create_cthulhu_source(opt, result.ipo_effects) + if opt isa CC.OptimizationState + CC.ir_to_codeinf!(opt) + end + return nothing +end +function CC.transform_result_for_cache(::CthulhuInterpreter, ::MethodInstance, ::WorldRange, + result::InferenceResult) + return result.src end -@static if VERSION ≥ v"1.9-" -function CC.transform_result_for_cache(interp::CthulhuInterpreter, - linfo::MethodInstance, valid_worlds::WorldRange, result::InferenceResult) +else # VERSION < v"1.11.0-DEV.737" +CC.finish(state::InferenceState, interp::CthulhuInterpreter) = cthulhu_finish(CC.finish, state, interp) +function CC.transform_result_for_cache(::CthulhuInterpreter, ::MethodInstance, ::WorldRange, + result::InferenceResult) return create_cthulhu_source(result.src, result.ipo_effects) end -else -function CC.transform_result_for_cache(interp::CthulhuInterpreter, - linfo::MethodInstance, valid_worlds::WorldRange, @nospecialize(inferred_result), - ipo_effects::CC.Effects) - return create_cthulhu_source(inferred_result, ipo_effects) -end +function CC.finish!(interp::CthulhuInterpreter, caller::InferenceResult) + caller.src = create_cthulhu_source(caller.src, caller.ipo_effects) end -@static if VERSION ≥ v"1.9-" +end # @static if + +@static if VERSION ≥ v"1.12.0-DEV.45" +function CC.src_inlining_policy(interp::CthulhuInterpreter, + @nospecialize(src), @nospecialize(info::CCCallInfo), stmt_flag::UInt32) + if isa(src, OptimizedSource) + if CC.is_stmt_inline(stmt_flag) || src.isinlineable + return true + end + return false + else + @assert src isa CC.IRCode || src === nothing "invalid Cthulhu code cache" + # the default inlining policy may try additional effor to find the source in a local cache + return @invoke CC.src_inlining_policy(interp::AbstractInterpreter, + src::Any, info::CCCallInfo, stmt_flag::UInt32) + end +end +CC.retrieve_ir_for_inlining(cached_result::CodeInstance, src::OptimizedSource) = + CC.retrieve_ir_for_inlining(cached_result.def, src.ir::IRCode, true) +CC.retrieve_ir_for_inlining(mi::MethodInstance, src::OptimizedSource, preserve_local_sources::Bool) = + CC.retrieve_ir_for_inlining(mi, src.ir, preserve_local_sources) +elseif VERSION ≥ v"1.11.0-DEV.879" function CC.inlining_policy(interp::CthulhuInterpreter, - @nospecialize(src), @nospecialize(info::CCCallInfo), stmt_flag::UInt8, mi::MethodInstance, argtypes::Vector{Any}) + @nospecialize(src), @nospecialize(info::CCCallInfo), stmt_flag::UInt32) if isa(src, OptimizedSource) if CC.is_stmt_inline(stmt_flag) || src.isinlineable return src.ir end else - @assert src isa CC.SemiConcreteResult || src === nothing "invalid Cthulhu code cache" + @assert src isa CC.IRCode || src === nothing "invalid Cthulhu code cache" # the default inlining policy may try additional effor to find the source in a local cache return @invoke CC.inlining_policy(interp::AbstractInterpreter, - src::Any, info::CCCallInfo, stmt_flag::UInt8, mi::MethodInstance, argtypes::Vector{Any}) + src::Any, info::CCCallInfo, stmt_flag::UInt32) end return nothing end else function CC.inlining_policy(interp::CthulhuInterpreter, - @nospecialize(src), stmt_flag::UInt8, mi::MethodInstance, argtypes::Vector{Any}) - @assert isa(src, OptimizedSource) || isnothing(src) + @nospecialize(src), @nospecialize(info::CCCallInfo), + stmt_flag::(@static VERSION ≥ v"1.11.0-DEV.377" ? UInt32 : UInt8), + mi::MethodInstance, argtypes::Vector{Any}) if isa(src, OptimizedSource) if CC.is_stmt_inline(stmt_flag) || src.isinlineable return src.ir end else + @assert src isa CC.SemiConcreteResult || src === nothing "invalid Cthulhu code cache" # the default inlining policy may try additional effor to find the source in a local cache return @invoke CC.inlining_policy(interp::AbstractInterpreter, - nothing, stmt_flag::UInt8, mi::MethodInstance, argtypes::Vector{Any}) + src::Any, info::CCCallInfo, + stmt_flag::(@static VERSION ≥ v"1.11.0-DEV.377" ? UInt32 : UInt8), + mi::MethodInstance, argtypes::Vector{Any}) end return nothing end end -@static if isdefined(CC, :AbsIntState) function CC.IRInterpretationState(interp::CthulhuInterpreter, code::CodeInstance, mi::MethodInstance, argtypes::Vector{Any}, world::UInt) inferred = @atomic :monotonic code.inferred @@ -214,18 +227,22 @@ function CC.IRInterpretationState(interp::CthulhuInterpreter, ir = CC.copy(inferred.ir) src = inferred.src method_info = CC.MethodInfo(src) + if isdefined(Base, :__has_internal_change) && Base.__has_internal_change(v"1.12-alpha", :codeinfonargs) + argtypes = CC.va_process_argtypes(CC.optimizer_lattice(interp), argtypes, src.nargs, src.isva) + elseif VERSION >= v"1.12.0-DEV.341" + argtypes = CC.va_process_argtypes(CC.optimizer_lattice(interp), argtypes, mi) + end return CC.IRInterpretationState(interp, method_info, ir, mi, argtypes, world, - src.min_world, src.max_world) -end -elseif VERSION ≥ v"1.9-" -function CC.codeinst_to_ir(interp::CthulhuInterpreter, code::CodeInstance) - inferred = @atomic :monotonic code.inferred - inferred === nothing && return nothing - inferred = inferred::OptimizedSource - return CC.copy(inferred.ir) -end + code.min_world, code.max_world) end -function CC.finish!(interp::CthulhuInterpreter, caller::InferenceResult) - caller.src = create_cthulhu_source(caller.src, caller.ipo_effects) +@static if VERSION ≥ v"1.11.0-DEV.1127" +function CC.update_exc_bestguess!(interp::CthulhuInterpreter, @nospecialize(exct), + frame::InferenceState) + key = (@static VERSION ≥ v"1.12.0-DEV.317" ? CC.is_constproped(frame) : CC.any(frame.result.overridden_by_const)) ? frame.result : frame.linfo + pc2excts = get!(PC2Excts, interp.exception_types, key) + pc2excts[frame.currpc] = CC.tmerge(CC.typeinf_lattice(interp), exct, get(pc2excts, frame.currpc, Union{})) + return @invoke CC.update_exc_bestguess!(interp::AbstractInterpreter, exct::Any, + frame::InferenceState) +end end diff --git a/src/preferences.jl b/src/preferences.jl index d7bb895e..d8b8241f 100644 --- a/src/preferences.jl +++ b/src/preferences.jl @@ -1,4 +1,3 @@ - """ ```julia save_config!(config::CthulhuConfig=CONFIG) @@ -36,6 +35,9 @@ function save_config!(config::CthulhuConfig=CONFIG) "inline_cost" => config.inline_cost, "type_annotations" => config.type_annotations, "annotate_source" => config.annotate_source, + "inlay_types_vscode" => config.inlay_types_vscode, + "diagnostics_vscode" => config.diagnostics_vscode, + "jump_always" => config.jump_always, ) end @@ -53,4 +55,7 @@ function read_config!(config::CthulhuConfig) config.inline_cost = @load_preference("inline_cost", config.inline_cost) config.type_annotations = @load_preference("type_annotations", config.type_annotations) config.annotate_source = @load_preference("annotate_source", config.annotate_source) + config.inlay_types_vscode = @load_preference("inlay_types_vscode", config.inlay_types_vscode) + config.diagnostics_vscode = @load_preference("diagnostics_vscode", config.diagnostics_vscode) + config.jump_always = @load_preference("jump_always", config.jump_always) end diff --git a/src/reflection.jl b/src/reflection.jl index d0dc39dd..0df53c6d 100644 --- a/src/reflection.jl +++ b/src/reflection.jl @@ -20,10 +20,19 @@ end function find_callsites(interp::AbstractInterpreter, CI::Union{Core.CodeInfo, IRCode}, stmt_infos::Union{Vector{CCCallInfo}, Nothing}, mi::Core.MethodInstance, - slottypes::Vector{Any}, optimize::Bool=true, annotate_source::Bool=false) + slottypes::Vector{Any}, optimize::Bool=true, annotate_source::Bool=false, + pc2excts::Union{Nothing,PC2Excts}=nothing) sptypes = sptypes_from_meth_instance(mi) callsites, sourcenodes = Callsite[], Union{TypedSyntax.MaybeTypedSyntaxNode,Callsite}[] - stmts = isa(CI, IRCode) ? CI.stmts.inst : CI.code + if isa(CI, IRCode) + @static if VERSION < v"1.11.0-DEV.258" + stmts = CI.stmts.inst + else + stmts = CI.stmts.stmt + end + else + stmts = CI.code + end nstmts = length(stmts) _, mappings = annotate_source ? get_typed_sourcetext(mi, CI, nothing; warn=false) : (nothing, nothing) @@ -45,7 +54,8 @@ function find_callsites(interp::AbstractInterpreter, CI::Union{Core.CodeInfo, IR t = argextype(arg, CI, sptypes, slottypes) return ignorelimited(t) end, args) - callinfos = process_info(interp, info, argtypes, rt, optimize) + exct = isnothing(pc2excts) ? nothing : get(pc2excts, id, nothing) + callinfos = process_info(interp, info, argtypes, rt, optimize, exct) isempty(callinfos) && continue callsite = let if length(callinfos) == 1 @@ -77,7 +87,7 @@ function find_callsites(interp::AbstractInterpreter, CI::Union{Core.CodeInfo, IR func = args[6] ftype = widenconst(argextype(func, CI, sptypes, slottypes)) sig = Tuple{ftype} - callsite = Callsite(id, TaskCallInfo(callinfo(sig, nothing; world=get_world_counter(interp))), head) + callsite = Callsite(id, TaskCallInfo(callinfo(sig, nothing; world=get_inference_world(interp))), head) end end end @@ -89,7 +99,7 @@ function find_callsites(interp::AbstractInterpreter, CI::Union{Core.CodeInfo, IR mi = get_mi(info) meth = mi.def if isa(meth, Method) && nameof(meth.module) === :CUDAnative && meth.name === :cufunction - callsite = transform(Val(:CuFunction), callsite, c, CI, mi, slottypes; world=get_world_counter(interp)) + callsite = transform(Val(:CuFunction), callsite, c, CI, mi, slottypes; world=get_inference_world(interp)) end end @@ -108,68 +118,70 @@ function find_callsites(interp::AbstractInterpreter, CI::Union{Core.CodeInfo, IR end function process_const_info(interp::AbstractInterpreter, @nospecialize(thisinfo), - argtypes::ArgTypes, @nospecialize(rt), @nospecialize(result), optimize::Bool) + argtypes::ArgTypes, @nospecialize(rt), @nospecialize(result), optimize::Bool, + @nospecialize(exct)) is_cached(@nospecialize(key)) = can_descend(interp, key, optimize) if isnothing(result) return thisinfo - elseif (@static VERSION ≥ v"1.9-" && true) && isa(result, CC.ConcreteResult) + elseif (@static VERSION ≥ v"1.11.0-DEV.851" && true) && result isa CC.VolatileInferenceResult + # NOTE we would not hit this case since `finish!(::CthulhuInterpreter, frame::InferenceState)` + # will always transform `frame.result.src` to `OptimizedSource` when frame is inferred + return thisinfo + elseif isa(result, CC.ConcreteResult) linfo = result.mi effects = get_effects(result) - mici = MICallInfo(linfo, rt, effects) + mici = MICallInfo(linfo, rt, effects, exct) return ConcreteCallInfo(mici, argtypes) - elseif (@static VERSION ≥ v"1.9-" && true) && isa(result, CC.ConstPropResult) + elseif isa(result, CC.ConstPropResult) result = result.result linfo = result.linfo effects = get_effects(result) - mici = MICallInfo(linfo, rt, effects) + mici = MICallInfo(linfo, rt, effects, exct) return ConstPropCallInfo(is_cached(optimize ? linfo : result) ? mici : UncachedCallInfo(mici), result) - elseif (@static VERSION ≥ v"1.9-" && true) && isa(result, CC.SemiConcreteResult) + elseif isa(result, CC.SemiConcreteResult) linfo = result.mi effects = get_effects(result) - mici = MICallInfo(linfo, rt, effects) + mici = MICallInfo(linfo, rt, effects, exct) return SemiConcreteCallInfo(mici, result.ir) - elseif (@static !(VERSION ≥ v"1.9-") && true) && isa(result, CC.ConstResult) - linfo = result.mi - effects = get_effects(result) - mici = MICallInfo(linfo, rt, effects) - return ConcreteCallInfo(mici, argtypes) else @assert isa(result, CC.InferenceResult) linfo = result.linfo effects = get_effects(result) - mici = MICallInfo(linfo, rt, effects) + mici = MICallInfo(linfo, rt, effects, exct) return ConstPropCallInfo(is_cached(optimize ? linfo : result) ? mici : UncachedCallInfo(mici), result) end end -function process_info(interp::AbstractInterpreter, @nospecialize(info::CCCallInfo), argtypes::ArgTypes, @nospecialize(rt), optimize::Bool) +function process_info(interp::AbstractInterpreter, @nospecialize(info::CCCallInfo), + argtypes::ArgTypes, @nospecialize(rt), optimize::Bool, + @nospecialize(exct)) is_cached(@nospecialize(key)) = can_descend(interp, key, optimize) - process_recursive(@nospecialize(newinfo)) = process_info(interp, newinfo, argtypes, rt, optimize) + process_recursive(@nospecialize(newinfo)) = process_info(interp, newinfo, argtypes, rt, optimize, exct) if isa(info, MethodResultPure) if isa(info.info, CC.ReturnTypeCallInfo) # xref: https://github.com/JuliaLang/julia/pull/45299#discussion_r871939049 info = info.info # cascade to the special handling below else - return Any[PureCallInfo(argtypes, rt)] + return CallInfo[PureCallInfo(argtypes, rt)] end end if isa(info, MethodMatchInfo) if info.results === missing - return [] + return CallInfo[] end matches = info.results.matches - return mapany(matches) do match::Core.MethodMatch + return CallInfo[let mi = specialize_method(match) effects = get_effects(interp, mi, optimize) - mici = MICallInfo(mi, rt, effects) - return is_cached(mi) ? mici : UncachedCallInfo(mici) - end + mici = MICallInfo(mi, rt, effects, exct) + is_cached(mi) ? mici : UncachedCallInfo(mici) + end for match::Core.MethodMatch in matches] elseif isa(info, UnionSplitInfo) - return mapreduce(process_recursive, vcat, info.matches; init=[])::Vector{Any} + return mapreduce(process_recursive, vcat, info.matches; init=CallInfo[])::Vector{CallInfo} elseif isa(info, UnionSplitApplyCallInfo) - return mapreduce(process_recursive, vcat, info.infos; init=[])::Vector{Any} + return mapreduce(process_recursive, vcat, info.infos; init=CallInfo[])::Vector{CallInfo} elseif isa(info, ApplyCallInfo) # XXX: This could probably use its own info. For now, # we ignore any implicit iterate calls. @@ -177,32 +189,32 @@ function process_info(interp::AbstractInterpreter, @nospecialize(info::CCCallInf elseif isa(info, ConstCallInfo) infos = process_recursive(info.call) @assert length(infos) == length(info.results) - return mapany(enumerate(info.results)) do (i, result) - process_const_info(interp, infos[i], argtypes, rt, result, optimize) - end + return CallInfo[let + process_const_info(interp, infos[i], argtypes, rt, result, optimize, exct) + end for (i, result) in enumerate(info.results)] elseif isa(info, CC.InvokeCallInfo) mi = specialize_method(info.match; preexisting=true) effects = get_effects(interp, mi, false) thisinfo = MICallInfo(mi, rt, effects) - innerinfo = process_const_info(interp, thisinfo, argtypes, rt, info.result, optimize) + innerinfo = process_const_info(interp, thisinfo, argtypes, rt, info.result, optimize, exct) info = InvokeCallInfo(innerinfo) - return Any[info] + return CallInfo[info] elseif isa(info, CC.OpaqueClosureCallInfo) mi = specialize_method(info.match; preexisting=true) effects = get_effects(interp, mi, false) thisinfo = MICallInfo(mi, rt, effects) - innerinfo = process_const_info(interp, thisinfo, argtypes, rt, info.result, optimize) + innerinfo = process_const_info(interp, thisinfo, argtypes, rt, info.result, optimize, exct) info = OCCallInfo(innerinfo) - return Any[info] + return CallInfo[info] elseif isa(info, CC.OpaqueClosureCreateInfo) # TODO: Add ability to descend into OCs at creation site - return [] - elseif (@static VERSION ≥ v"1.9-" && true) && isa(info, CC.FinalizerInfo) + return CallInfo[] + elseif isa(info, CC.FinalizerInfo) # TODO: Add ability to descend into finalizers at creation site - return [] + return CallInfo[] elseif isa(info, CC.ReturnTypeCallInfo) newargtypes = argtypes[2:end] - callinfos = process_info(interp, info.info, newargtypes, unwrapType(widenconst(rt)), optimize) + callinfos = process_info(interp, info.info, newargtypes, unwrapType(widenconst(rt)), optimize, exct) if length(callinfos) == 1 vmi = only(callinfos) else @@ -211,13 +223,13 @@ function process_info(interp::AbstractInterpreter, @nospecialize(info::CCCallInf sig = Tuple{widenconst(newargtypes[1]), argt.parameters...} vmi = FailedCallInfo(sig, Union{}) end - return Any[ReturnTypeCallInfo(vmi)] + return CallInfo[ReturnTypeCallInfo(vmi)] elseif info == NoCallInfo() f = unwrapconst(argtypes[1]) - isa(f, Core.Builtin) && return [] - return [RTCallInfo(f, argtypes[2:end], rt)] + isa(f, Core.Builtin) && return CallInfo[] + return CallInfo[RTCallInfo(f, argtypes[2:end], rt, exct)] elseif info === false - return [] + return CallInfo[] else @eval Main begin interp = $interp @@ -249,11 +261,7 @@ function preprocess_ci!(ci::CodeInfo, mi::MethodInstance, optimize, config::Cthu argtypes = CC.matching_cache_argtypes(mi, nothing, false)[1] ir = CC.inflate_ir(ci, sptypes_from_meth_instance(mi), argtypes) ir = dce!(ir) - @static if VERSION ≥ v"1.10.0-DEV.870" - ci = CC.replace_code_newstyle!(ci, ir) - else - ci = CC.replace_code_newstyle!(ci, ir, length(argtypes)-1) - end + ci = CC.replace_code_newstyle!(ci, ir) end return ci end @@ -291,7 +299,7 @@ function callinfo(sig, rt, max_methods=-1; world=get_world_counter()) return MultiCallInfo(sig, rt, callinfos) end -function find_caller_of(interp::AbstractInterpreter, callee::MethodInstance, caller::MethodInstance; allow_unspecialized::Bool=false) +function find_caller_of(interp::AbstractInterpreter, callee::Union{MethodInstance,Type}, caller::MethodInstance; allow_unspecialized::Bool=false) interp′ = CthulhuInterpreter(interp) do_typeinf!(interp′, caller) locs = Tuple{Core.LineInfoNode,Int}[] @@ -301,7 +309,7 @@ function find_caller_of(interp::AbstractInterpreter, callee::MethodInstance, cal callsites, _ = find_callsites(interp′, src, infos, caller, 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), 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}() @@ -327,29 +335,35 @@ function find_caller_of(interp::AbstractInterpreter, callee::MethodInstance, cal return ulocs end -function add_sourceline!(locs, CI, stmtidx::Int) - if isa(CI, IRCode) - stack = Base.IRShow.compute_loc_stack(CI.linetable, CI.stmts.line[stmtidx]) +function add_sourceline!(locs::Vector{Tuple{Core.LineInfoNode,Int}}, src::Union{CodeInfo,IRCode}, stmtidx::Int, caller::MethodInstance) + @static if VERSION ≥ v"1.12.0-DEV.173" + stack = Base.IRShow.buildLineInfoNode(src.debuginfo, caller, stmtidx) + for (i, di) in enumerate(stack) + loc = Core.LineInfoNode(Main, di.method, di.file, di.line, zero(Int32)) + push!(locs, (loc, i-1)) + end + else # VERSION < v"1.12.0-DEV.173" + if isa(src, IRCode) + stack = Base.IRShow.compute_loc_stack(src.linetable, src.stmts.line[stmtidx]) for (i, idx) in enumerate(stack) - line = CI.linetable[idx] + line = src.linetable[idx] line.line == 0 && continue - push!(locs, (CI.linetable[idx], i-1)) + push!(locs, (src.linetable[idx], i-1)) end else - push!(locs, (CI.linetable[CI.codelocs[stmtidx]], 0)) + push!(locs, (src.linetable[src.codelocs[stmtidx]], 0)) end + end # @static if return locs end function get_typed_sourcetext(mi::MethodInstance, src::CodeInfo, @nospecialize(rt); warn::Bool=true) - meth = mi.def::Method - tsn, mappings = TypedSyntax.tsn_and_mappings(meth, src, rt; warn, strip_macros=true) - return truncate_if_defaultargs!(tsn, mappings, meth) - return tsn, mappings + tsn, mappings = TypedSyntax.tsn_and_mappings(mi, src, rt; warn, strip_macros=true) + return truncate_if_defaultargs!(tsn, mappings, mi.def::Method) end function get_typed_sourcetext(mi::MethodInstance, ::IRCode, @nospecialize(rt); kwargs...) - src, rt = TypedSyntax.getsrc(mi) + src, rt = TypedSyntax.code_typed1_tsn(mi) return get_typed_sourcetext(mi, src, rt; kwargs...) end @@ -373,11 +387,7 @@ function truncate_if_defaultargs!(tsn, mappings, meth) return tsn, mappings end -if isdefined(Core, :kwcall) - is_kw_dispatch(meth::Method) = meth.name == :kwcall || Base.unwrap_unionall(meth.sig).parameters[1] === typeof(Core.kwcall) || !isempty(Base.kwarg_decl(meth)) -else - is_kw_dispatch(meth::Method) = endswith(string(meth.name), "##kw") || !isempty(Base.kwarg_decl(meth)) -end +is_kw_dispatch(meth::Method) = meth.name == :kwcall || Base.unwrap_unionall(meth.sig).parameters[1] === typeof(Core.kwcall) || !isempty(Base.kwarg_decl(meth)) function tag_runtime(node::TypedSyntaxNode, info) node.runtime = isa(info, RTCallInfo) diff --git a/src/ui.jl b/src/ui.jl index 46419545..18ff7ee9 100644 --- a/src/ui.jl +++ b/src/ui.jl @@ -13,7 +13,7 @@ mutable struct CthulhuMenu <: TerminalMenus.ConfiguredMenu{TerminalMenus.Config} custom_toggles::Vector{CustomToggle} end -function show_as_line(callsite::Callsite, with_effects::Bool, optimize::Bool, iswarn::Bool) +function show_as_line(callsite::Callsite, with_effects::Bool, exception_type::Bool, optimize::Bool, iswarn::Bool) reduced_displaysize = displaysize(stdout)::Tuple{Int,Int} .- (0, 3) sprint() do io show(IOContext(io, @@ -21,15 +21,17 @@ function show_as_line(callsite::Callsite, with_effects::Bool, optimize::Bool, is :displaysize => reduced_displaysize, :optimize => optimize, :iswarn => iswarn, - :color => iswarn | with_effects, - :with_effects => with_effects), + :color => iswarn | with_effects | exception_type, + :with_effects => with_effects, + :exception_type => exception_type), callsite) end end -function CthulhuMenu(callsites, with_effects::Bool, optimize::Bool, iswarn::Bool, hide_type_stable::Bool, +function CthulhuMenu(callsites, with_effects::Bool, exception_type::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, optimize, iswarn, hide_type_stable) + options = build_options(callsites, with_effects, exception_type, optimize, iswarn, hide_type_stable) length(options) < 1 && error("CthulhuMenu must have at least one option") # if pagesize is -1, use automatic paging @@ -47,15 +49,15 @@ function CthulhuMenu(callsites, with_effects::Bool, optimize::Bool, iswarn::Bool return CthulhuMenu(options, pagesize, pageoffset, selected, nothing, sub_menu, config, custom_toggles) end -build_options(callsites::Vector{Callsite}, with_effects::Bool, optimize::Bool, iswarn::Bool, ::Bool) = - vcat(map(callsite->show_as_line(callsite, with_effects, optimize, iswarn), callsites), ["↩"]) -function build_options(callsites, with_effects::Bool, optimize::Bool, iswarn::Bool, hide_type_stable::Bool) +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) 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, optimize, iswarn) + show_as_line(node, with_effects, exception_type, optimize, iswarn) else if nd == -1 nd = TypedSyntax.ndigits_linenumbers(node) @@ -92,8 +94,10 @@ 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, inline_cost, type_annotations, highlight, - custom_toggles::Vector{CustomToggle}) +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(use_color::Bool, c::Char) = stringify() do io use_color ? printstyled(io, c; color=:cyan) : print(io, c) end @@ -106,7 +110,16 @@ function usage(@nospecialize(view_cmd), annotate_source, optimize, iswarn, hide_ 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(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, [", @@ -114,7 +127,8 @@ function usage(@nospecialize(view_cmd), annotate_source, optimize, iswarn, hide_ printstyled(io, 'd'; color=debugcolors[Int(debuginfo)+1]) end, "]ebuginfo, [", colorize(remarks, 'r'), "]emarks, [", - colorize(with_effects, 'e'), "]ffects, [", + 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) @@ -148,6 +162,7 @@ const TOGGLES = Dict( 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, @@ -160,6 +175,9 @@ const TOGGLES = Dict( 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) diff --git a/test/generate_irshow.jl b/test/generate_irshow.jl index 0e2ddc1d..2a95722e 100644 --- a/test/generate_irshow.jl +++ b/test/generate_irshow.jl @@ -11,7 +11,7 @@ function generate_test_cases(f, tt, fname=string(nameof(f))) outputs = Dict() tf = (true, false) for optimize in tf - (; src, infos, mi, rt, effects, slottypes) = cthulhu_info(f, tt; optimize); + (; src, infos, mi, 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, ) @@ -20,7 +20,7 @@ function generate_test_cases(f, tt, fname=string(nameof(f))) s = sprint(; context=:color=>true) do io Cthulhu.cthulhu_typed(io, debuginfo, - src, rt, effects, mi; + src, rt, exct, effects, mi; iswarn, hide_type_stable, inline_cost, type_annotations) end s = strip_base_linenums(s) diff --git a/test/irutils.jl b/test/irutils.jl index 43871774..e20fd8da 100644 --- a/test/irutils.jl +++ b/test/irutils.jl @@ -1,14 +1,10 @@ using Core: CodeInfo, ReturnNode, MethodInstance -using Core.Compiler: IRCode, IncrementalCompact, singleton_type, VarState +using Core.Compiler: IRCode, IncrementalCompact, singleton_type using Base.Meta: isexpr using InteractiveUtils: gen_call_with_extracted_types_and_kwargs argextype(@nospecialize args...) = Core.Compiler.argextype(args...) -@static if VERSION >= v"1.10.0-DEV.556" - argextype(@nospecialize(x), src::CodeInfo) = argextype(x, src, VarState[]) -else - argextype(@nospecialize(x), src::CodeInfo) = argextype(x, src, Any[]) -end +argextype(@nospecialize(x), src::CodeInfo) = argextype(x, src, Core.Compiler.VarState[]) code_typed1(args...; kwargs...) = first(only(code_typed(args...; kwargs...)))::CodeInfo macro code_typed1(ex0...) return gen_call_with_extracted_types_and_kwargs(__module__, :code_typed1, ex0) diff --git a/test/runtests.jl b/test/runtests.jl index 019f0623..f956e3ef 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -9,7 +9,7 @@ using Test, PerformanceTestTools include("test_codeview.jl") end - # TODO enable this test on nightly + # TODO enable these tests if false @testset "test_irshow.jl" begin include("test_irshow.jl") @@ -17,12 +17,77 @@ using Test, PerformanceTestTools else @info "skipped test_irshow.jl" end - - @testset "test_terminal.jl" begin - include("test_terminal.jl") + if false + @testset "test_terminal.jl" begin + include("test_terminal.jl") + end + else + @info "skipped test_terminal.jl" end @testset "test_AbstractInterpreter.jl" begin include("test_AbstractInterpreter.jl") end end + +# TODO enable the VSCode-related tests + +# module VSCodeServer +# using TypedSyntax + +# struct InlineDisplay +# is_repl::Bool +# end +# const INLAY_HINTS_ENABLED = Ref(true) +# const DIAGNOSTICS_ENABLED = Ref(true) + +# inlay_hints = [] +# diagnostics = [] + +# function Base.display(d::InlineDisplay, x) +# if x isa Dict{String, Vector{TypedSyntax.InlayHint}} +# push!(inlay_hints, x) +# elseif eltype(x) == TypedSyntax.Diagnostic +# push!(diagnostics, x) +# end +# return nothing +# end + +# function reset_test_containers() +# empty!(inlay_hints) +# empty!(diagnostics) +# end +# end +# module TestVSCodeExt # stops modules defined in test files from overwriting stuff from previous test +# using Test, PerformanceTestTools, ..VSCodeServer +# @testset "runtests.jl VSCodeExt" begin +# @testset "test_Cthulhu.jl" begin +# include("test_Cthulhu.jl") +# end + +# @testset "test_codeview.jl" begin +# include("test_codeview.jl") +# include("test_codeview_vscode.jl") +# end + +# # TODO enable these tests +# if false +# @testset "test_irshow.jl" begin +# include("test_irshow.jl") +# end +# else +# @info "skipped test_irshow.jl" +# end +# if false +# @testset "test_terminal.jl" begin +# include("test_terminal.jl") +# end +# else +# @info "skipped test_terminal.jl" +# end + +# @testset "test_AbstractInterpreter.jl" begin +# include("test_AbstractInterpreter.jl") +# end +# end +# end diff --git a/test/setup.jl b/test/setup.jl index 8b421d24..a2a80aeb 100644 --- a/test/setup.jl +++ b/test/setup.jl @@ -1,12 +1,18 @@ using Test, Cthulhu, InteractiveUtils +if isdefined(parentmodule(@__MODULE__), :VSCodeServer) + using ..VSCodeServer +end -function cthulhu_info(@nospecialize(f), @nospecialize(TT=()); optimize=true) - (interp, mi) = Cthulhu.mkinterp(Core.Compiler.NativeInterpreter(), f, TT) - (; src, rt, infos, slottypes, effects) = Cthulhu.lookup(interp, mi, optimize; allow_no_src=true) +function cthulhu_info(@nospecialize(f), @nospecialize(tt=()); + optimize=true, interp=Core.Compiler.NativeInterpreter()) + (interp, mi) = Cthulhu.mkinterp(f, tt; interp) + (; src, rt, exct, infos, slottypes, effects) = + Cthulhu.lookup(interp, mi, optimize; allow_no_src=true) if src !== nothing - src = Cthulhu.preprocess_ci!(src, mi, optimize, Cthulhu.CthulhuConfig(dead_code_elimination=true)) + config = Cthulhu.CthulhuConfig(; dead_code_elimination=true) + src = Cthulhu.preprocess_ci!(src, mi, optimize, config) end - (; interp, src, infos, mi, rt, slottypes, effects) + return (; interp, src, infos, mi, rt, exct, slottypes, effects) end function find_callsites_by_ftt(@nospecialize(f), @nospecialize(TT=Tuple{}); optimize=true) @@ -20,9 +26,3 @@ end macro find_callsites_by_ftt(ex0...) return InteractiveUtils.gen_call_with_extracted_types_and_kwargs(__module__, :find_callsites_by_ftt, ex0) end - -@static if isdefined(Core.Compiler, :is_foldable_nothrow) - using Core.Compiler: is_foldable_nothrow -else - const is_foldable_nothrow = Core.Compiler.is_total -end diff --git a/test/test_AbstractInterpreter.jl b/test/test_AbstractInterpreter.jl index 2b281812..ece9669c 100644 --- a/test/test_AbstractInterpreter.jl +++ b/test/test_AbstractInterpreter.jl @@ -1,47 +1,99 @@ module test_AbstractInterpreter using Test, Cthulhu +if isdefined(parentmodule(@__MODULE__), :VSCodeServer) + using ..VSCodeServer +end const CC = Core.Compiler -import Core: MethodInstance, CodeInstance -import .CC: WorldRange, WorldView, NativeInterpreter - -# define new `AbstractInterpreter` that satisfies the minimum interface requirements -# while managing its cache independently -macro newinterp(name) - cachename = gensym(string(name, "Cache")) - name = esc(name) + +@static if VERSION ≥ v"1.11.0-DEV.1552" +macro newinterp(InterpName) + InterpCacheName = QuoteNode(Symbol(string(InterpName, "Cache"))) + InterpName = esc(InterpName) + C = Core + CC = Core.Compiler quote - struct $cachename - dict::IdDict{MethodInstance,CodeInstance} + struct $InterpName <: $CC.AbstractInterpreter + meta # additional information + world::UInt + inf_params::$CC.InferenceParams + opt_params::$CC.OptimizationParams + inf_cache::Vector{$CC.InferenceResult} + function $InterpName(meta = nothing; + world::UInt = Base.get_world_counter(), + inf_params::$CC.InferenceParams = $CC.InferenceParams(), + opt_params::$CC.OptimizationParams = $CC.OptimizationParams(), + inf_cache::Vector{$CC.InferenceResult} = $CC.InferenceResult[]) + return new(meta, world, inf_params, opt_params, inf_cache) + end end - struct $name <: CC.AbstractInterpreter - interp::CC.NativeInterpreter - cache::$cachename - $name(world = Base.get_world_counter(); - interp = CC.NativeInterpreter(world), - cache = $cachename(IdDict{MethodInstance,CodeInstance}()) - ) = new(interp, cache) + $CC.InferenceParams(interp::$InterpName) = interp.inf_params + $CC.OptimizationParams(interp::$InterpName) = interp.opt_params + $CC.get_inference_world(interp::$InterpName) = interp.world + $CC.get_inference_cache(interp::$InterpName) = interp.inf_cache + $CC.cache_owner(::$InterpName) = $InterpCacheName + end +end +else +macro newinterp(InterpName) + InterpCacheName = esc(Symbol(string(InterpName, "Cache"))) + InterpName = esc(InterpName) + C = Core + CC = Core.Compiler + quote + struct $InterpCacheName + dict::IdDict{$C.MethodInstance,$C.CodeInstance} end - CC.InferenceParams(interp::$name) = CC.InferenceParams(interp.interp) - CC.OptimizationParams(interp::$name) = CC.OptimizationParams(interp.interp) - CC.get_world_counter(interp::$name) = CC.get_world_counter(interp.interp) - CC.get_inference_cache(interp::$name) = CC.get_inference_cache(interp.interp) - CC.code_cache(interp::$name) = WorldView(interp.cache, WorldRange(CC.get_world_counter(interp))) - CC.get(wvc::WorldView{<:$cachename}, mi::MethodInstance, default) = get(wvc.cache.dict, mi, default) - CC.getindex(wvc::WorldView{<:$cachename}, mi::MethodInstance) = getindex(wvc.cache.dict, mi) - CC.haskey(wvc::WorldView{<:$cachename}, mi::MethodInstance) = haskey(wvc.cache.dict, mi) - CC.setindex!(wvc::WorldView{<:$cachename}, ci::CodeInstance, mi::MethodInstance) = setindex!(wvc.cache.dict, ci, mi) + $InterpCacheName() = $InterpCacheName(IdDict{$C.MethodInstance,$C.CodeInstance}()) + struct $InterpName <: $CC.AbstractInterpreter + meta # additional information + world::UInt + inf_params::$CC.InferenceParams + opt_params::$CC.OptimizationParams + inf_cache::Vector{$CC.InferenceResult} + code_cache::$InterpCacheName + function $InterpName(meta = nothing; + world::UInt = Base.get_world_counter(), + inf_params::$CC.InferenceParams = $CC.InferenceParams(), + opt_params::$CC.OptimizationParams = $CC.OptimizationParams(), + inf_cache::Vector{$CC.InferenceResult} = $CC.InferenceResult[], + code_cache::$InterpCacheName = $InterpCacheName()) + return new(meta, world, inf_params, opt_params, inf_cache, code_cache) + end + end + $CC.InferenceParams(interp::$InterpName) = interp.inf_params + $CC.OptimizationParams(interp::$InterpName) = interp.opt_params + @static if VERSION ≥ v"1.11.0-DEV.1498" + CC.get_inference_world(interp::$InterpName) = interp.world + else + CC.get_world_counter(interp::$InterpName) = interp.world + end + $CC.get_inference_cache(interp::$InterpName) = interp.inf_cache + $CC.code_cache(interp::$InterpName) = $CC.WorldView(interp.code_cache, $CC.WorldRange(interp.world)) + $CC.get(wvc::$CC.WorldView{$InterpCacheName}, mi::$C.MethodInstance, default) = get(wvc.cache.dict, mi, default) + $CC.getindex(wvc::$CC.WorldView{$InterpCacheName}, mi::$C.MethodInstance) = getindex(wvc.cache.dict, mi) + $CC.haskey(wvc::$CC.WorldView{$InterpCacheName}, mi::$C.MethodInstance) = haskey(wvc.cache.dict, mi) + $CC.setindex!(wvc::$CC.WorldView{$InterpCacheName}, ci::$C.CodeInstance, mi::$C.MethodInstance) = setindex!(wvc.cache.dict, ci, mi) end end +end # if VERSION ≥ v"1.11.0-DEV.1552" + +@doc """ +@newinterp NewInterpreter + +Defines new `NewInterpreter <: AbstractInterpreter` whose cache is separated +from the native code cache, satisfying the minimum interface requirements. +""" var"@newinterp" # `OverlayMethodTable` # -------------------- import Base.Experimental: @MethodTable, @overlay @newinterp MTOverlayInterp -@MethodTable(OverlayedMT) -CC.method_table(interp::MTOverlayInterp) = CC.OverlayMethodTable(CC.get_world_counter(interp), OverlayedMT) +@MethodTable OverlayedMT +CC.method_table(interp::MTOverlayInterp) = + CC.OverlayMethodTable(Cthulhu.get_inference_world(interp), OverlayedMT) @overlay OverlayedMT sin(x::Float64) = 1 @testset "OverlayMethodTable integration" begin diff --git a/test/test_Cthulhu.jl b/test/test_Cthulhu.jl index 65615d41..f0b52db6 100644 --- a/test/test_Cthulhu.jl +++ b/test/test_Cthulhu.jl @@ -52,7 +52,7 @@ end # Callsite handling in source-view mode: for kwarg functions, strip the body, and use "typed" callsites for m in (@which(anykwargs("animals")), @which(anykwargs("animals"; cat=1, dog=2))) mi = first_specialization(m) - src, rt = Cthulhu.TypedSyntax.getsrc(mi) + src, rt = Cthulhu.TypedSyntax.code_typed1_tsn(mi) tsn, mappings = Cthulhu.get_typed_sourcetext(mi, src, rt; warn=false) str = sprint(printstyled, tsn) @test occursin("anykwargs", str) && occursin("kwargs...", str) && !occursin("println", str) @@ -61,7 +61,7 @@ end # Likewise for methods that fill in default positional arguments m = @which hasdefaultargs(1) mi = first_specialization(m) - src, rt = Cthulhu.TypedSyntax.getsrc(mi) + src, rt = Cthulhu.TypedSyntax.code_typed1_tsn(mi) tsn, mappings = Cthulhu.get_typed_sourcetext(mi, src, rt; warn=false) str = sprint(printstyled, tsn) @test occursin("hasdefaultargs(a, b=2)", str) @@ -69,7 +69,7 @@ end @test isempty(mappings) m = @which hasdefaultargs(1, 5) mi = first_specialization(m) - src, rt = Cthulhu.TypedSyntax.getsrc(mi) + src, rt = Cthulhu.TypedSyntax.code_typed1_tsn(mi) tsn, mappings = Cthulhu.get_typed_sourcetext(mi, src, rt; warn=false) str = sprint(printstyled, tsn) @test occursin("hasdefaultargs(a, b=2)", str) @@ -83,16 +83,46 @@ end calltwice(c) = twice(c[1]) callsites = find_callsites_by_ftt(calltwice, Tuple{Vector{Float64}}) - @test length(callsites) == 1 && callsites[1].head === :invoke - io = IOBuffer() - print(io, callsites[1]) - @test occursin("invoke twice(::Float64)::Float64", String(take!(io))) + @static if VERSION ≥ v"1.11.0-DEV.753" + @test any(callsites) do callsite + callsite.head === :invoke || return false + io = IOBuffer() + print(io, callsite) + return occursin("invoke twice(::Float64)::Float64", String(take!(io))) + end + @test any(callsites) do callsite + callsite.head === :invoke || return false + io = IOBuffer() + print(io, callsite) + return occursin("invoke throw_boundserror", String(take!(io))) + end + else + @test length(callsites) == 1 && callsites[1].head === :invoke + io = IOBuffer() + print(io, callsites[1]) + @test occursin("invoke twice(::Float64)::Float64", String(take!(io))) + end callsites = find_callsites_by_ftt(calltwice, Tuple{Vector{AbstractFloat}}) - @test length(callsites) == 1 && callsites[1].head === :call - io = IOBuffer() - print(io, callsites[1]) - @test occursin("call twice(::AbstractFloat)", String(take!(io))) + @static if VERSION ≥ v"1.11.0-DEV.753" + @test any(callsites) do callsite + callsite.head === :call || return false + io = IOBuffer() + print(io, callsite) + return occursin("call twice(::AbstractFloat)", String(take!(io))) + end + @test any(callsites) do callsite + callsite.head === :invoke || return false + io = IOBuffer() + print(io, callsite) + return occursin("invoke throw_boundserror", String(take!(io))) + end + else + @test length(callsites) == 1 && callsites[1].head === :call + io = IOBuffer() + print(io, callsites[1]) + @test occursin("call twice(::AbstractFloat)", String(take!(io))) + end # Note the failure of `callinfo` to properly handle specialization @test_broken Cthulhu.callinfo(Tuple{typeof(twice), AbstractFloat}, AbstractFloat) isa Cthulhu.MultiCallInfo @@ -130,26 +160,33 @@ let callsites = find_callsites_by_ftt(call_by_apply, Tuple{Tuple{Int}}; optimize @test length(callsites) == 1 end +Base.@propagate_inbounds _boundscheck_dce(x) = @boundscheck error() +boundscheck_dce_inbounds(x) = @inbounds _boundscheck_dce(x) +boundscheck_dce(x) = _boundscheck_dce(x) + @testset "DCE & boundscheck" begin - M = Module() - @eval M begin - Base.@propagate_inbounds function f(x) - @boundscheck error() + @static if VERSION ≥ v"1.11.0-DEV.377" + # 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 end - g(x) = @inbounds f(x) - h(x) = f(x) - end - - let (; src) = cthulhu_info(M.g, Tuple{Vector{Float64}}) - @test all(src.stmts.inst) do stmt - isa(stmt, Core.GotoNode) || (isa(stmt, Core.ReturnNode) && isdefined(stmt, :val)) + else + let (; src) = cthulhu_info(boundscheck_dce_inbounds, Tuple{Vector{Float64}}) + stmts = @static VERSION < v"1.11.0-DEV.258" ? src.stmts.inst : src.stmts.stmt + @test all(stmts) do stmt + isa(stmt, Core.GotoNode) || (isa(stmt, Core.ReturnNode) && isdefined(stmt, :val)) + end + end + let (; src) = cthulhu_info(boundscheck_dce, Tuple{Vector{Float64}}) + stmts = @static VERSION < v"1.11.0-DEV.258" ? src.stmts.inst : src.stmts.stmt + @test count(!isnothing, stmts) == 2 + stmt = stmts[end] + @test isa(stmt, Core.ReturnNode) && !isdefined(stmt, :val) end - end - - let (; src) = cthulhu_info(M.h, Tuple{Vector{Float64}}) - @test count(!isnothing, src.stmts.inst) == 2 - stmt = src.stmts.inst[end] - @test isa(stmt, Core.ReturnNode) && !isdefined(stmt, :val) end end @@ -167,32 +204,30 @@ let callsites = find_callsites_by_ftt(f_matches, Tuple{Any, Any}; optimize=false @test occursin(r"→ g_matches\(::Any, ?::Any\)::Union{Float64, ?Int\d+}", String(take!(io))) end -@testset "wrapped callinfo" begin - let - m = Module() - @eval m begin - # mutually recursive functions - f(a) = g(a) - g(a) = somecode::Bool ? h(a) : a - h(a) = f(Type{a}) - end +uncached_call1(a) = uncached_call2(a) +uncached_call2(a) = somecode::Bool ? uncached_call3(a) : a +uncached_call3(a) = uncached_call1(Type{a}) - # make sure we form `UncachedCallInfo` so that we won't try to retrieve non-existing cache - callsites = @find_callsites_by_ftt m.f(Int) - @test length(callsites) == 1 - ci = first(callsites).info - @test isa(ci, Cthulhu.UncachedCallInfo) - effects = Cthulhu.get_effects(ci) - @test !Core.Compiler.is_consistent(effects) +@testset "wrapped callinfo" begin + # make sure we form `UncachedCallInfo` so that we won't try to retrieve non-existing cache + callsites = @find_callsites_by_ftt uncached_call1(Int) + @test length(callsites) == 1 + ci = first(callsites).info + @test isa(ci, Cthulhu.UncachedCallInfo) + effects = Cthulhu.get_effects(ci) + @test !Core.Compiler.is_consistent(effects) + @static if VERSION ≥ v"1.11.0-DEV.392" + @test !Core.Compiler.is_effect_free(effects) + else @test Core.Compiler.is_effect_free(effects) - @test !Core.Compiler.is_nothrow(effects) - @test !Core.Compiler.is_terminates(effects) - @test Cthulhu.is_callsite(ci, ci.wrapped.mi) - io = IOBuffer() - show(io, first(callsites)) - @test occursin("< uncached >", String(take!(io))) - # TODO do some test with `LimitedCallInfo`, but they happen at deeper callsites end + @test !Core.Compiler.is_nothrow(effects) + @test !Core.Compiler.is_terminates(effects) + @test Cthulhu.is_callsite(ci, ci.wrapped.mi) + io = IOBuffer() + show(io, first(callsites)) + @test occursin("< uncached >", String(take!(io))) + # TODO do some test with `LimitedCallInfo`, but they happen at deeper callsites end @testset "ConstPropCallInfo" begin @@ -233,12 +268,12 @@ end end t[1] end - @test length(callsites) == 1 # getindex(::Union{Vector{Any}, Const(tuple(1,nothing))}, ::Const(1)) + @test length(callsites) == 1 # getindex(::Union{Vector{Any}, Const(tuple(1,nothing))}, ::Const(1)) callinfo = callsites[1].info @test isa(callinfo, Cthulhu.MultiCallInfo) callinfos = callinfo.callinfos @test length(callinfos) == 2 - @test count(ci->isa(ci, Cthulhu.MICallInfo), callinfos) == 1 # getindex(::Vector{Any}, ::Const(1)) + @test count(ci->isa(ci, Cthulhu.MICallInfo), callinfos) == 1 # getindex(::Vector{Any}, ::Const(1)) @test count(ci->isa(ci, Cthulhu.ConstPropCallInfo) || isa(ci, Cthulhu.SemiConcreteCallInfo), callinfos) == 1 # getindex(::Const(tuple(1,nothing)), ::Const(1)) end @@ -298,7 +333,7 @@ let # check the performance benefit of semi concrete evaluation out end end -@static VERSION ≥ v"1.9-" && @testset "SemiConcreteResult" begin +@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) @@ -317,16 +352,12 @@ function bar346(x::ComplexF64) x = ComplexF64(x.re, 1.0) return sin(x.im) end -@static VERSION >= v"1.10-" && @testset "issue #346" begin +@testset "issue #346" begin let (; interp, src, infos, mi, slottypes) = cthulhu_info(bar346, Tuple{ComplexF64}; optimize=false) callsites, _ = Cthulhu.find_callsites(interp, src, infos, mi, slottypes, false) @test isa(callsites[1].info, Cthulhu.SemiConcreteCallInfo) @test occursin("= < semi-concrete eval > getproperty(::ComplexF64,::Core.Const(:re))::Float64", string(callsites[1])) - @test Cthulhu.get_rt(callsites[end].info) == Core.Const(sin(1.0)) - - @test Cthulhu.get_remarks(interp, callsites[1].info) == Cthulhu.PC2Remarks() - @test Cthulhu.get_effects(interp, callsites[1].info) == Cthulhu.PC2Effects() end end @@ -342,8 +373,8 @@ struct SingletonPureCallable{N} end @test occursin("SingletonPureCallable{1}()(::Float64)::Float64", s) - @test Cthulhu.get_effects(c1) |> is_foldable_nothrow - @test Cthulhu.get_effects(c2) |> is_foldable_nothrow + @test Cthulhu.get_effects(c1) |> Core.Compiler.is_foldable_nothrow + @test Cthulhu.get_effects(c2) |> Core.Compiler.is_foldable_nothrow end @testset "ReturnTypeCallInfo" begin @@ -375,8 +406,8 @@ end print(io, callsites[2]) @test occursin("return_type < only_ints(::Float64)::Union{} >", String(take!(io))) - @test Cthulhu.get_effects(callinfo1) |> is_foldable_nothrow - @test Cthulhu.get_effects(callinfo2) |> is_foldable_nothrow + @test Cthulhu.get_effects(callinfo1) |> Core.Compiler.is_foldable_nothrow + @test Cthulhu.get_effects(callinfo2) |> Core.Compiler.is_foldable_nothrow end @testset "OCCallInfo" begin @@ -387,7 +418,7 @@ end @test length(callsites) == 1 callinfo = only(callsites).info @test callinfo isa Cthulhu.OCCallInfo - @test Cthulhu.get_effects(callinfo) |> !is_foldable_nothrow + @test Cthulhu.get_effects(callinfo) |> !Core.Compiler.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) @@ -445,7 +476,7 @@ invoke_constcall(a::Number, c::Bool) = c ? Number : :number callsite = only(callsites) info = callsite.info @test isa(info, Cthulhu.InvokeCallInfo) - @test Cthulhu.get_effects(info) |> is_foldable_nothrow + @test Cthulhu.get_effects(info) |> Core.Compiler.is_foldable_nothrow rt = Core.Compiler.Const(:Integer) @test info.ci.rt === rt buf = IOBuffer() @@ -458,7 +489,7 @@ invoke_constcall(a::Number, c::Bool) = c ? Number : :number callsite = only(callsites) info = callsite.info @test isa(info, Cthulhu.InvokeCallInfo) - @test Cthulhu.get_effects(info) |> is_foldable_nothrow + @test Cthulhu.get_effects(info) |> Core.Compiler.is_foldable_nothrow @test info.ci.rt === Core.Compiler.Const(:Int) end @@ -469,13 +500,13 @@ invoke_constcall(a::Number, c::Bool) = c ? Number : :number callsite = only(callsites) info = callsite.info @test isa(info, Cthulhu.InvokeCallInfo) - @test Cthulhu.get_effects(info) |> is_foldable_nothrow + @test Cthulhu.get_effects(info) |> Core.Compiler.is_foldable_nothrow inner = info.ci rt = Core.Compiler.Const(Any) @test Cthulhu.get_rt(info) === rt buf = IOBuffer() show(buf, callsite) - @static VERSION ≥ v"1.9-" && @test isa(inner, Cthulhu.SemiConcreteCallInfo) + @test isa(inner, Cthulhu.SemiConcreteCallInfo) @test occursin("= invoke < invoke_constcall(::Any,::$(Core.Compiler.Const(true)))::$rt", String(take!(buf))) end end @@ -642,9 +673,9 @@ end end end function doprint(f) - (; src, mi, rt, effects) = cthulhu_info(f) + (; src, mi, rt, exct, effects) = cthulhu_info(f) io = IOBuffer() - Cthulhu.cthulhu_typed(io, :none, src, rt, effects, mi; iswarn=false) + Cthulhu.cthulhu_typed(io, :none, src, rt, exct, effects, mi; iswarn=false) return String(take!(io)) end @test occursin("invoke f1()::…\n", doprint(getfield(m, :f1))) @@ -674,55 +705,55 @@ end @test isa(mi, Core.MethodInstance) end -## Functions for "backedges & treelist" -# The printing changes when the functions are defined inside the testset -fbackedge1() = 1 -fbackedge2(x) = x > 0 ? fbackedge1() : -fbackedge1() -fst1(x) = backtrace() -@inline fst2(x) = fst1(x) -@noinline fst3(x) = fst2(x) -@inline fst4(x) = fst3(x) -fst5(x) = fst4(x) - -@testset "backedges and treelist" begin - @test fbackedge2(0.2) == 1 - @test fbackedge2(-0.2) == -1 - mi = first_specialization(@which(fbackedge1())) - root = Cthulhu.treelist(mi) - @test Cthulhu.count_open_leaves(root) == 2 - @test root.data.callstr == "fbackedge1()" - @test root.children[1].data.callstr == " fbackedge2(::Float64)" - - # issue #114 - unspecva(@nospecialize(i::Int...)) = 1 - @test unspecva(1, 2) == 1 - mi = first_specialization(only(methods(unspecva))) - root = Cthulhu.treelist(mi) - @test occursin("Vararg", root.data.callstr) - - # Test highlighting and other printing - mi = Cthulhu.get_specialization(:, Tuple{T, T} where T<:Integer) - root = Cthulhu.treelist(mi) - @test occursin("\e[31m::T\e[39m", root.data.callstr) - mi = Cthulhu.get_specialization(Vector{Int}, Tuple{typeof(undef), Int}) - io = IOBuffer() - @test Cthulhu.callstring(io, mi) == "Vector{$Int}(::UndefInitializer, ::$Int)" - mi = Cthulhu.get_specialization(similar, Tuple{Type{Vector{T}}, Dims{1}} where T) - @test occursin(r"31m::Type", Cthulhu.callstring(io, mi)) - - # treelist for stacktraces - tree = Cthulhu.treelist(fst5(1.0)) - @test match(r"fst1 at .*:\d+ => fst2 at .*:\d+ => fst3\(::Float64\) at .*:\d+", tree.data.callstr) !== nothing - @test length(tree.children) == 1 - child = tree.children[1] - @test match(r" fst4 at .*:\d+ => fst5\(::Float64\) at .*:\d+", child.data.callstr) !== nothing - - # issue #184 - tree = Cthulhu.treelist(similar(fst5(1.0), 0)) - @test isempty(tree.data.callstr) - @test isempty(Cthulhu.callstring(io, similar(stacktrace(fst5(1.0)), 0))) - @test Cthulhu.instance(similar(stacktrace(fst5(1.0)), 0)) === Core.Compiler.Timings.ROOTmi -end +# ## Functions for "backedges & treelist" +# # The printing changes when the functions are defined inside the testset +# fbackedge1() = 1 +# fbackedge2(x) = x > 0 ? fbackedge1() : -fbackedge1() +# fst1(x) = backtrace() +# @inline fst2(x) = fst1(x) +# @noinline fst3(x) = fst2(x) +# @inline fst4(x) = fst3(x) +# fst5(x) = fst4(x) + +# @testset "backedges and treelist" begin +# @test fbackedge2(0.2) == 1 +# @test fbackedge2(-0.2) == -1 +# mi = first_specialization(@which(fbackedge1())) +# root = Cthulhu.treelist(mi) +# @test Cthulhu.count_open_leaves(root) == 2 +# @test root.data.callstr == "fbackedge1()" +# @test root.children[1].data.callstr == " fbackedge2(::Float64)" + +# # issue #114 +# unspecva(@nospecialize(i::Int...)) = 1 +# @test unspecva(1, 2) == 1 +# mi = first_specialization(only(methods(unspecva))) +# root = Cthulhu.treelist(mi) +# @test occursin("Vararg", root.data.callstr) + +# # Test highlighting and other printing +# mi = Cthulhu.get_specialization(:, Tuple{T, T} where T<:Integer) +# root = Cthulhu.treelist(mi) +# @test occursin("\e[31m::T\e[39m", root.data.callstr) +# mi = Cthulhu.get_specialization(Vector{Int}, Tuple{typeof(undef), Int}) +# io = IOBuffer() +# @test Cthulhu.callstring(io, mi) == "Vector{$Int}(::UndefInitializer, ::$Int)" +# mi = Cthulhu.get_specialization(similar, Tuple{Type{Vector{T}}, Dims{1}} where T) +# @test occursin(r"31m::Type", Cthulhu.callstring(io, mi)) + +# # treelist for stacktraces +# tree = Cthulhu.treelist(fst5(1.0)) +# @test match(r"fst1 at .*:\d+ => fst2 at .*:\d+ => fst3\(::Float64\) at .*:\d+", tree.data.callstr) !== nothing +# @test length(tree.children) == 1 +# child = tree.children[1] +# @test match(r" fst4 at .*:\d+ => fst5\(::Float64\) at .*:\d+", child.data.callstr) !== nothing + +# # issue #184 +# tree = Cthulhu.treelist(similar(fst5(1.0), 0)) +# @test isempty(tree.data.callstr) +# @test isempty(Cthulhu.callstring(io, similar(stacktrace(fst5(1.0)), 0))) +# @test Cthulhu.instance(similar(stacktrace(fst5(1.0)), 0)) === Core.Compiler.Timings.ROOTmi +# end @testset "ascend" begin # This tests only the non-interactive "look up the caller" portion @@ -783,7 +814,6 @@ end @test lines == [line2] end - @testset "ascend interface" begin m = Module() @eval m begin @@ -926,7 +956,7 @@ end j = only(findall(iscall((src, sin_noconstprop)), src.code)) @test i < j pc2remarks = interp.remarks[mi] - Base.VERSION >= v"1.8" && @test any(pc2remarks) do (pc, msg) + @test any(pc2remarks) do (pc, msg) pc == j && occursin("Disabled by method parameter", msg) end end @@ -938,10 +968,10 @@ function effects_dced(x) a = Any[] end push!(a, x) - n = Core.arraysize(a) + n = Core.arraysize(a, 1) return a, n end -@static VERSION ≥ v"1.9-" && @testset "per-statement effects" begin +@testset "per-statement effects" begin interp, mi = Cthulhu.mkinterp(effects_dced, (Int,)); src = interp.unopt[mi].src i1 = only(findall(iscall((src, isa)), src.code)) @@ -956,7 +986,7 @@ end @test haskey(pc2effects, i4) end -@static VERSION ≥ v"1.9-" && @testset "Bare-bones MIs" begin +@testset "Bare-bones MIs" begin # Get IR for a function, wrap it in a minimal methodinstance (ir, rt) = only(Base.code_ircode(sqrt, (Float64,))) mi = ccall(:jl_new_method_instance_uninit, Ref{Core.MethodInstance}, ()); @@ -980,4 +1010,14 @@ end @test String(take!(io)) == ":toplevel(::Float64)::Float64" end +@inline countvars50037(bitflags::Int, var::Int) = bitflags >> 0 +let (interp, mi) = Cthulhu.mkinterp((Int,)) do var::Int + countvars50037(1, var) + end + key = only(Base.specializations(only(methods(countvars50037)))) + codeinst = interp.opt[key] + inferred = @atomic :monotonic codeinst.inferred + @test length(inferred.ir.cfg.blocks) == 1 +end + end # module test_Cthulhu diff --git a/test/test_codeview.jl b/test/test_codeview.jl index 09957287..1c95b63d 100644 --- a/test/test_codeview.jl +++ b/test/test_codeview.jl @@ -10,7 +10,7 @@ using .TestCodeViewSandbox Revise.track(TestCodeViewSandbox, normpath(@__DIR__, "TestCodeViewSandbox.jl")) @testset "printer test" begin - (; interp, src, infos, mi, rt, effects, slottypes) = cthulhu_info(testf_revise); + (; interp, src, infos, mi, rt, exct, effects, slottypes) = cthulhu_info(testf_revise); tf = (true, false) @testset "codeview: $codeview" for codeview in Cthulhu.CODEVIEWS @@ -22,7 +22,7 @@ Revise.track(TestCodeViewSandbox, normpath(@__DIR__, "TestCodeViewSandbox.jl")) config = Cthulhu.CONFIG io = IOBuffer() - codeview(io, mi, optimize, debuginfo, interp, config) + codeview(io, mi, optimize, debuginfo, Cthulhu.get_inference_world(interp), config) @test !isempty(String(take!(io))) # just check it works end end @@ -35,7 +35,7 @@ Revise.track(TestCodeViewSandbox, normpath(@__DIR__, "TestCodeViewSandbox.jl")) @testset "type_annotations: $type_annotations" for type_annotations in tf io = IOBuffer() Cthulhu.cthulhu_typed(io, debuginfo, - src, rt, effects, mi; + src, rt, exct, effects, mi; iswarn, hide_type_stable, inline_cost, type_annotations) @test !isempty(String(take!(io))) # just check it works end @@ -47,7 +47,7 @@ end @testset "hide type-stable statements" begin let # optimize code - (; src, infos, mi, rt, effects, slottypes) = @eval Module() begin + (; src, infos, mi, rt, exct, effects, slottypes) = @eval Module() begin const globalvar = Ref(42) $cthulhu_info() do a = sin(globalvar[]) @@ -57,7 +57,7 @@ end end function prints(; kwargs...) io = IOBuffer() - Cthulhu.cthulhu_typed(io, :none, src, rt, effects, mi; kwargs...) + Cthulhu.cthulhu_typed(io, :none, src, rt, exct, effects, mi; kwargs...) return String(take!(io)) end @@ -74,7 +74,7 @@ end end let # unoptimize code - (; src, infos, mi, rt, effects, slottypes) = @eval Module() begin + (; src, infos, mi, rt, exct, effects, slottypes) = @eval Module() begin const globalvar = Ref(42) $cthulhu_info(; optimize=false) do a = sin(globalvar[]) @@ -84,7 +84,7 @@ end end function prints(; kwargs...) io = IOBuffer() - Cthulhu.cthulhu_typed(io, :none, src, rt, effects, mi; kwargs...) + Cthulhu.cthulhu_typed(io, :none, src, rt, exct, effects, mi; kwargs...) return String(take!(io)) end diff --git a/test/test_codeview_vscode.jl b/test/test_codeview_vscode.jl new file mode 100644 index 00000000..8ef87974 --- /dev/null +++ b/test/test_codeview_vscode.jl @@ -0,0 +1,305 @@ +module test_codeview_vscode + +using Cthulhu, Test, Revise, REPL, ..VSCodeServer, TypedSyntax +import TypedSyntax: InlayHintKinds + +include("test_vscode_example_functions.jl") + +@testset "VSCode descend test" begin + if !isdefined(@__MODULE__, :fake_terminal) + @eval (@__MODULE__) begin + includet(@__MODULE__, normpath(@__DIR__, "FakeTerminals.jl")) # FIXME change to include + using .FakeTerminals + end + end + + function equal_upto_ordering(x, y) + if length(x) != length(y) + return false + end + + while length(x) > 0 + element = pop!(x) + found_idx = findfirst(x->x==element, y) + if !isnothing(found_idx) + deleteat!(y, found_idx) + else + return false + end + end + return true + end + + for inlay_types_vscode in (true, false), diagnostics_vscode in (true, false), iswarn in (true, false), hide_type_stable in (true, false) + @testset "fib inlay_types_vscode=$inlay_types_vscode, diagnostics_vscode=$diagnostics_vscode, iswarn=$iswarn, hide_type_stable=$hide_type_stable" begin + VSCodeServer.reset_test_containers() + + fake_terminal() do term, in, out, _ + t = @async begin + @test_nowarn descend(fib, (Int,); terminal=term, iswarn, hide_type_stable, inlay_types_vscode, diagnostics_vscode) + end + write(in, 'q') + wait(t) + end + + if inlay_types_vscode + @test length(VSCodeServer.inlay_hints) == 2 + @test isempty(VSCodeServer.inlay_hints[2]) + else + @test length(VSCodeServer.inlay_hints) == 1 + @test isempty(VSCodeServer.inlay_hints[1]) + end + + if diagnostics_vscode + @test length(VSCodeServer.diagnostics) == 2 + @test isempty(VSCodeServer.diagnostics[2]) + if !iswarn + @test isempty(VSCodeServer.diagnostics[1]) + end + end + + if !hide_type_stable && inlay_types_vscode + @test equal_upto_ordering(first(values(VSCodeServer.inlay_hints[1])), [ + TypedSyntax.InlayHint(1, 14, "::$Int", 1) + TypedSyntax.InlayHint(1, 15, "::$Int", 1) + TypedSyntax.InlayHint(3, 11, "(", 1) + TypedSyntax.InlayHint(3, 15, "(", 1) + TypedSyntax.InlayHint(3, 16, "::$Int", 1) + TypedSyntax.InlayHint(3, 20, ")::$Int", 1) + TypedSyntax.InlayHint(3, 21, "::$Int", 1) + TypedSyntax.InlayHint(3, 28, "(", 1) + TypedSyntax.InlayHint(3, 29, "::$Int", 1) + TypedSyntax.InlayHint(3, 33, ")::$Int", 1) + TypedSyntax.InlayHint(3, 34, "::$Int", 1) + TypedSyntax.InlayHint(3, 34, ")::$Int", 1) + ]) + elseif hide_type_stable && inlay_types_vscode + @test isempty(VSCodeServer.inlay_hints[1]) + end + end + end + + for inlay_types_vscode in (true, false), diagnostics_vscode in (true, false), iswarn in (true, false), hide_type_stable in (true, false) + @testset "fVSCode inlay_types_vscode=$inlay_types_vscode, diagnostics_vscode=$diagnostics_vscode, iswarn=$iswarn, hide_type_stable=$hide_type_stable" begin + VSCodeServer.reset_test_containers() + + fake_terminal() do term, in, out, _ + t = @async begin + @test_nowarn descend(fVSCode, (Int,); terminal=term, iswarn, hide_type_stable, inlay_types_vscode, diagnostics_vscode) + end + write(in, 'q') + wait(t) + end + + if inlay_types_vscode + @test length(VSCodeServer.inlay_hints) == 2 + @test isempty(VSCodeServer.inlay_hints[2]) + else + @test length(VSCodeServer.inlay_hints) == 1 + @test isempty(VSCodeServer.inlay_hints[1]) + end + + if diagnostics_vscode + @test length(VSCodeServer.diagnostics) == 2 + @test isempty(VSCodeServer.diagnostics[2]) + if iswarn + @test equal_upto_ordering(getproperty.(VSCodeServer.diagnostics[1], :line), [8, 11]) + @test getproperty.(VSCodeServer.diagnostics[1], :severity) == [TypedSyntax.DiagnosticKinds.Information, TypedSyntax.DiagnosticKinds.Information] + @test getproperty.(VSCodeServer.diagnostics[1], :msg) == ["Unstable Type", "Unstable Type"] + else + @test isempty(VSCodeServer.diagnostics[1]) + end + end + + if !hide_type_stable && inlay_types_vscode + if iswarn + @test equal_upto_ordering(first(values(VSCodeServer.inlay_hints[1])), [ + TypedSyntax.InlayHint(7, 18, "::$Int", 1) + TypedSyntax.InlayHint(7, 19, "::Union{Float64, $Int}", nothing) + TypedSyntax.InlayHint(8, 5, "::$Int", 1) + TypedSyntax.InlayHint(8, 8, "(", 1) + TypedSyntax.InlayHint(8, 9, "::$Int", 1) + TypedSyntax.InlayHint(8, 13, ")::$Int", 1) + TypedSyntax.InlayHint(9, 5, "::$Int", 1) + TypedSyntax.InlayHint(9, 8, "(", 1) + TypedSyntax.InlayHint(9, 13, "::$Int", 1) + TypedSyntax.InlayHint(9, 13, ")::$Int", 1) + TypedSyntax.InlayHint(10, 11, "(", 1) + TypedSyntax.InlayHint(10, 12, "::$Int", 1) + TypedSyntax.InlayHint(10, 16, "(", 1) + TypedSyntax.InlayHint(10, 17, "::$Int", 1) + TypedSyntax.InlayHint(10, 21, ")::Bool", 1) + TypedSyntax.InlayHint(10, 26, "::$Int", 1) + TypedSyntax.InlayHint(10, 32, "::Float64", 1) + TypedSyntax.InlayHint(10, 33, ")::Union{Float64, $Int}", nothing) + ]) + + else + @test equal_upto_ordering(first(values(VSCodeServer.inlay_hints[1])), [ + TypedSyntax.InlayHint(7, 18, "::$Int", 1) + TypedSyntax.InlayHint(7, 19, "::Union{Float64, $Int}", 1) + TypedSyntax.InlayHint(8, 5, "::$Int", 1) + TypedSyntax.InlayHint(8, 8, "(", 1) + TypedSyntax.InlayHint(8, 9, "::$Int", 1) + TypedSyntax.InlayHint(8, 13, ")::$Int", 1) + TypedSyntax.InlayHint(9, 5, "::$Int", 1) + TypedSyntax.InlayHint(9, 8, "(", 1) + TypedSyntax.InlayHint(9, 13, "::$Int", 1) + TypedSyntax.InlayHint(9, 13, ")::$Int", 1) + TypedSyntax.InlayHint(10, 11, "(", 1) + TypedSyntax.InlayHint(10, 12, "::$Int", 1) + TypedSyntax.InlayHint(10, 16, "(", 1) + TypedSyntax.InlayHint(10, 17, "::$Int", 1) + TypedSyntax.InlayHint(10, 21, ")::Bool", 1) + TypedSyntax.InlayHint(10, 26, "::$Int", 1) + TypedSyntax.InlayHint(10, 32, "::Float64", 1) + TypedSyntax.InlayHint(10, 33, ")::Union{Float64, $Int}", 1) + ]) + end + elseif hide_type_stable && inlay_types_vscode + if iswarn + @test equal_upto_ordering(first(values(VSCodeServer.inlay_hints[1])), [ + TypedSyntax.InlayHint(7, 19, "::Union{Float64, $Int}", nothing) + TypedSyntax.InlayHint(10, 11, "(", 1) + TypedSyntax.InlayHint(10, 33, ")::Union{Float64, $Int}", nothing) + ]) + else + @test equal_upto_ordering(first(values(VSCodeServer.inlay_hints[1])), [ + TypedSyntax.InlayHint(7, 19, "::Union{Float64, $Int}", 1) + TypedSyntax.InlayHint(10, 11, "(", 1) + TypedSyntax.InlayHint(10, 33, ")::Union{Float64, $Int}", 1) + ]) + end + end + end + end + + for inlay_types_vscode in (true, false), diagnostics_vscode in (true, false), iswarn in (true, false), hide_type_stable in (true, false) + @testset "fibcall Float64 inlay_types_vscode=$inlay_types_vscode, diagnostics_vscode=$diagnostics_vscode, iswarn=$iswarn, hide_type_stable=$hide_type_stable" begin + VSCodeServer.reset_test_containers() + + fake_terminal() do term, in, out, _ + t = @async begin + @test_nowarn descend(fibcall, (Float64,); terminal=term, iswarn, hide_type_stable, inlay_types_vscode, diagnostics_vscode) + end + write(in, 'q') + wait(t) + end + + if inlay_types_vscode + @test length(VSCodeServer.inlay_hints) == 2 + @test isempty(VSCodeServer.inlay_hints[2]) + else + @test length(VSCodeServer.inlay_hints) == 1 + @test isempty(VSCodeServer.inlay_hints[1]) + end + + if inlay_types_vscode || (diagnostics_vscode && iswarn) + @test length(VSCodeServer.diagnostics) == 2 + @test isempty(VSCodeServer.diagnostics[2]) + @test length(VSCodeServer.diagnostics[1]) == 1 + @test VSCodeServer.diagnostics[1][1].severity == TypedSyntax.DiagnosticKinds.Information + @test VSCodeServer.diagnostics[1][1].line == 2 + @test VSCodeServer.diagnostics[1][1].msg == "Cthulhu disabled: This function was called multiple times with different argument types" + end + + if !hide_type_stable && inlay_types_vscode + @test equal_upto_ordering(first(values(VSCodeServer.inlay_hints[1])), [ + TypedSyntax.InlayHint(14, 18, "::Float64", 1) + TypedSyntax.InlayHint(14, 19, "::$Int", 1) + TypedSyntax.InlayHint(15, 11, "::Type{$Int}", 1) + TypedSyntax.InlayHint(15, 13, "::Float64", 1) + TypedSyntax.InlayHint(15, 14, "::$Int", 1) + TypedSyntax.InlayHint(16, 9, "::Float64", 1) + TypedSyntax.InlayHint(16, 10, "::$Int", 1) + ]) || equal_upto_ordering(first(values(VSCodeServer.inlay_hints[1])), [ + TypedSyntax.InlayHint(14, 18, "::Float64", 1) + TypedSyntax.InlayHint(14, 19, "::$Int", 1) + TypedSyntax.InlayHint(15, 13, "::Float64", 1) + TypedSyntax.InlayHint(15, 14, "::$Int", 1) + TypedSyntax.InlayHint(16, 9, "::Float64", 1) + TypedSyntax.InlayHint(16, 10, "::$Int", 1) + ]) # Workaround for #530 + elseif hide_type_stable && inlay_types_vscode + @test isempty(VSCodeServer.inlay_hints[1]) + end + end + end + + for inlay_types_vscode in (true, false), diagnostics_vscode in (true, false), iswarn in (true, false), hide_type_stable in (true, false) + @testset "fibcall Int inlay_types_vscode=$inlay_types_vscode, diagnostics_vscode=$diagnostics_vscode, iswarn=$iswarn, hide_type_stable=$hide_type_stable" begin + VSCodeServer.reset_test_containers() + + fake_terminal() do term, in, out, _ + t = @async begin + @test_nowarn descend(fibcall, (Int,); terminal=term, iswarn, hide_type_stable, inlay_types_vscode, diagnostics_vscode) + end + write(in, 'q') + wait(t) + end + + if inlay_types_vscode + @test length(VSCodeServer.inlay_hints) == 2 + @test isempty(VSCodeServer.inlay_hints[2]) + else + @test length(VSCodeServer.inlay_hints) == 1 + @test isempty(VSCodeServer.inlay_hints[1]) + end + + if diagnostics_vscode + @test length(VSCodeServer.diagnostics) == 2 + @test isempty(VSCodeServer.diagnostics[2]) + if !iswarn + @test isempty(VSCodeServer.diagnostics[1]) + end + end + + if !hide_type_stable && inlay_types_vscode + @test equal_upto_ordering(first(values(VSCodeServer.inlay_hints[1])), [ + TypedSyntax.InlayHint(14, 18, "::$Int", 1) + TypedSyntax.InlayHint(14, 19, "::$Int", 1) + TypedSyntax.InlayHint(15, 11, "::Type{$Int}", 1) + TypedSyntax.InlayHint(15, 13, "::$Int", 1) + TypedSyntax.InlayHint(15, 14, "::$Int", 1) + TypedSyntax.InlayHint(16, 9, "::$Int", 1) + TypedSyntax.InlayHint(16, 10, "::$Int", 1) + TypedSyntax.InlayHint(1, 14, "::$Int", 1) + TypedSyntax.InlayHint(1, 15, "::$Int", 1) + TypedSyntax.InlayHint(3, 11, "(", 1) + TypedSyntax.InlayHint(3, 15, "(", 1) + TypedSyntax.InlayHint(3, 16, "::$Int", 1) + TypedSyntax.InlayHint(3, 20, ")::$Int", 1) + TypedSyntax.InlayHint(3, 21, "::$Int", 1) + TypedSyntax.InlayHint(3, 28, "(", 1) + TypedSyntax.InlayHint(3, 29, "::$Int", 1) + TypedSyntax.InlayHint(3, 33, ")::$Int", 1) + TypedSyntax.InlayHint(3, 34, "::$Int", 1) + TypedSyntax.InlayHint(3, 34, ")::$Int", 1) + ]) || equal_upto_ordering(first(values(VSCodeServer.inlay_hints[1])), [ + TypedSyntax.InlayHint(14, 18, "::$Int", 1) + TypedSyntax.InlayHint(14, 19, "::$Int", 1) + TypedSyntax.InlayHint(15, 13, "::$Int", 1) + TypedSyntax.InlayHint(15, 14, "::$Int", 1) + TypedSyntax.InlayHint(16, 9, "::$Int", 1) + TypedSyntax.InlayHint(16, 10, "::$Int", 1) + TypedSyntax.InlayHint(1, 14, "::$Int", 1) + TypedSyntax.InlayHint(1, 15, "::$Int", 1) + TypedSyntax.InlayHint(3, 11, "(", 1) + TypedSyntax.InlayHint(3, 15, "(", 1) + TypedSyntax.InlayHint(3, 16, "::$Int", 1) + TypedSyntax.InlayHint(3, 20, ")::$Int", 1) + TypedSyntax.InlayHint(3, 21, "::$Int", 1) + TypedSyntax.InlayHint(3, 28, "(", 1) + TypedSyntax.InlayHint(3, 29, "::$Int", 1) + TypedSyntax.InlayHint(3, 33, ")::$Int", 1) + TypedSyntax.InlayHint(3, 34, "::$Int", 1) + TypedSyntax.InlayHint(3, 34, ")::$Int", 1) + ]) # Workaround for #530 + elseif hide_type_stable && inlay_types_vscode + @test isempty(VSCodeServer.inlay_hints[1]) + end + end + end +end + +end diff --git a/test/test_irshow.jl b/test/test_irshow.jl index 22e2870a..b9418a10 100644 --- a/test/test_irshow.jl +++ b/test/test_irshow.jl @@ -10,7 +10,7 @@ include("IRShowSandbox.jl") tf = (true, false) @testset "optimize: $optimize" for optimize in tf - (; src, infos, mi, rt, effects, slottypes) = cthulhu_info(IRShowSandbox.foo, (Int, Int); optimize); + (; src, infos, mi, rt, exct, effects, slottypes) = cthulhu_info(IRShowSandbox.foo, (Int, Int); optimize); @testset "debuginfo: $debuginfo" for debuginfo in instances(Cthulhu.DInfo.DebugInfo) @testset "iswarn: $iswarn" for iswarn in tf @@ -22,7 +22,7 @@ include("IRShowSandbox.jl") s = sprint(; context=:color=>true) do io Cthulhu.cthulhu_typed(io, debuginfo, - src, rt, effects, mi; + src, rt, exct, effects, mi; iswarn, hide_type_stable, inline_cost, type_annotations) end s = strip_base_linenums(s) diff --git a/test/test_terminal.jl b/test/test_terminal.jl index b4b67091..954e041a 100644 --- a/test/test_terminal.jl +++ b/test/test_terminal.jl @@ -1,6 +1,9 @@ module test_terminal using Test, REPL, Cthulhu, Revise +if isdefined(parentmodule(@__MODULE__), :VSCodeServer) + using ..VSCodeServer +end if !isdefined(@__MODULE__, :fake_terminal) @eval (@__MODULE__) begin @@ -204,14 +207,9 @@ end Base.text_colors[Base.error_color()] end @test occursin("$(warncolor)%\e[39m2 = call → fmulti(::Any)::Union{Float32, Int64}", lines) - if isdefined(Core.Compiler, :NoCallInfo) - write(in, keydict[:down]) - write(in, keydict[:enter]) - lines = cread(out) - else - write(in, keydict[:enter]) - lines = cread1(out) - end + write(in, keydict[:down]) + write(in, keydict[:enter]) + lines = cread(out) @test occursin("%2 = fmulti(::Int32)::Union{Float32, $Int}", lines) @test occursin("%2 = fmulti(::Float32)::Union{Float32, $Int}", lines) @test occursin("%2 = fmulti(::Char)::Union{Float32, $Int}", lines) diff --git a/test/test_vscode_example_functions.jl b/test/test_vscode_example_functions.jl new file mode 100644 index 00000000..8728ed8a --- /dev/null +++ b/test/test_vscode_example_functions.jl @@ -0,0 +1,18 @@ +# exact location in file matters +function fib(n) + if n <= 1 return 1 end + return fib(n - 1) + fib(n - 2) +end + +# exact location in file matters +function fVSCode(x) + z = x + 1 + y = 2 * z + return y + (x > 0 ? -1 : 1.0) +end + +# exact location in file matters +function fibcall(n) + fib(Int(n)) + fib(n) +end \ No newline at end of file