diff --git a/Project.toml b/Project.toml index 670dedcb0..0b5011b63 100644 --- a/Project.toml +++ b/Project.toml @@ -8,6 +8,7 @@ projects = ["docs", "test"] [deps] CodeTracking = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2" FileWatching = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" +InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" JuliaInterpreter = "aa1ae85d-cabe-5617-a682-6adf51b2e16a" LibGit2 = "76f85450-5226-5b5a-8eaa-529ad045b433" LoweredCodeUtils = "6f1432cf-f94c-5a45-995e-cdbf5db27b0b" @@ -25,6 +26,7 @@ DistributedExt = "Distributed" [compat] CodeTracking = "3" Distributed = "1" +InteractiveUtils = "1.10, 1.11" JuliaInterpreter = "0.10.8" LoweredCodeUtils = "3.5" OrderedCollections = "1" diff --git a/docs/src/debugging.md b/docs/src/debugging.md index d975f4f56..aba318ecc 100644 --- a/docs/src/debugging.md +++ b/docs/src/debugging.md @@ -25,7 +25,7 @@ Currently, the best way to turn on logging is within a running Julia session: ```jldoctest; setup=(using Revise) julia> rlogger = Revise.debug_logger() -Revise.ReviseLogger(Revise.LogRecord[], Debug) +ReviseLogger with min_level=Debug ``` You'll use `rlogger` at the end to retrieve the logs. @@ -104,7 +104,7 @@ on to `rlogger`.) ### The structure of the logs For those who want to do a little investigating on their own, it may be helpful to -know that Revise's core decisions are captured in the group called "Action," and they come in three +know that Revise's core changes are captured in the group called "Action," and they come in three flavors: - log entries with message `"Eval"` signify a call to `eval`; for these events, diff --git a/docs/src/dev_reference.md b/docs/src/dev_reference.md index 41e99d4e4..e0d5d5193 100644 --- a/docs/src/dev_reference.md +++ b/docs/src/dev_reference.md @@ -48,7 +48,7 @@ Revise.user_callbacks_by_key ```@docs Revise.RelocatableExpr -Revise.ModuleExprsSigs +Revise.ModuleExprsInfos Revise.FileInfo Revise.PkgData Revise.WatchList @@ -118,7 +118,7 @@ This part of the package is not as well documented. ```@docs Revise.minimal_evaluation! Revise.methods_by_execution! -Revise.MethodInfo +Revise.ExInfo ``` ### Modules and paths diff --git a/docs/src/figures/diagram.tex b/docs/src/figures/diagram.tex index fe5d4a53c..3be08808c 100644 --- a/docs/src/figures/diagram.tex +++ b/docs/src/figures/diagram.tex @@ -38,7 +38,7 @@ \path [-latex] (methods.north west) edge node[midway,right] {\texttt{meth.sig}} (sigts.south east); \path [-latex] (sigts.south) edge node[midway,left] {\texttt{which}} (methods.west); -v \path [-latex] (exprs.south west) edge node[midway,left,color=green] {\texttt{ExprsSigs}} (sigts.north east); + \path [-latex] (exprs.south west) edge node[midway,left,color=green] {\texttt{ExprsInfos}} (sigts.north east); \path [-latex] (lowered.west) edge (sigts.east); % \path[dashed] [-latex] (methods.south west) edge [bend left=100] node[pos=0.75,left] {file,line} (src.west); diff --git a/docs/src/internals.md b/docs/src/internals.md index 823b8427a..29e9ce41c 100644 --- a/docs/src/internals.md +++ b/docs/src/internals.md @@ -224,7 +224,7 @@ Most of Revise's magic comes down to just three internal variables: - [`Revise.pkgdatas`](@ref): the central repository of parsed code, used to "diff" for changes and then "patch" the running session. -Two "maps" are central to Revise's inner workings: `ExprsSigs` maps link +Two "maps" are central to Revise's inner workings: `ExprsInfos` maps link definition=>signature-types (the forward workflow), while `CodeTracking` (specifically, its internal variable `method_info`) links from a method table/signature-type pair to the corresponding definition (the backward workflow). @@ -234,7 +234,7 @@ of `sigt`; consequently, this information allows one to look up the correspondin `locationinfo` and `def`. (When methods move, the location information stored by CodeTracking gets updated by Revise.) -Some additional notes about Revise's `ExprsSigs` maps: +Some additional notes about Revise's `ExprsInfos` maps: - For expressions that do not define a method, it is just `def=>nothing` - For expressions that do define a method, it is `def=>[mt_sigt1, ...]`. @@ -254,10 +254,10 @@ Some additional notes about Revise's `ExprsSigs` maps: Any discrepancy with the current line numbers in the file is handled through updates to the location information stored by `CodeTracking`. -`ExprsSigs` are organized by module and then file, so that one can map +`ExprsInfos` are organized by module and then file, so that one can map `filename`=>`module`=>`def`=>`mt_sigts`. Importantly, single-file modules can be "reconstructed" from the keys of the corresponding -`ExprsSigs` (and multi-file modules from a collection of such items), since they hold +`ExprsInfos` (and multi-file modules from a collection of such items), since they hold the complete ordered set of expressions that would be `eval`ed to define the module. The global variable that holds all this information is [`Revise.pkgdatas`](@ref), organized @@ -303,8 +303,8 @@ Items [b24a5932-55ed-11e9-2a88-e52f99e65a0d] julia> pkgdata = Revise.pkgdatas[id] PkgData(Items [b24a5932-55ed-11e9-2a88-e52f99e65a0d]: - "src/Items.jl": FileInfo(Main=>ExprsSigs(<1 expressions>, <0 signatures>), Items=>ExprsSigs(<2 expressions>, <3 signatures>), ) - "src/indents.jl": FileInfo(Items=>ExprsSigs(<2 expressions>, <2 signatures>), ) + "src/Items.jl": FileInfo(Main=>ExprsInfos(<1 expressions>, <0 signatures>), Items=>ExprsInfos(<2 expressions>, <3 signatures>), ) + "src/indents.jl": FileInfo(Items=>ExprsInfos(<2 expressions>, <2 signatures>), ) ``` (Your specific UUID may differ.) @@ -325,7 +325,7 @@ package manager. ```julia-repl julia> pkgdata.fileinfos[2] -FileInfo(Items=>ExprsSigs with the following expressions: +FileInfo(Items=>ExprsInfos with the following expressions: :(indent(::UInt16) = begin 2 end) @@ -337,7 +337,7 @@ FileInfo(Items=>ExprsSigs with the following expressions: This is just a summary; to see the actual `def=>mt_sigts` map, do the following: ```julia-repl -julia> pkgdata.fileinfos[2].mod_exs_sigs[Items] +julia> pkgdata.fileinfos[2].mod_exs_infos[Items] OrderedCollections.OrderedDict{Module, OrderedCollections.OrderedDict{Revise.RelocatableExpr, Union{Nothing, Vector{CodeTracking.MethodInfoKey}}}} with 2 entries: :(indent(::UInt16) = begin… => CodeTracking.MethodInfoKey[CodeTracking.MethodInfoKey(nothing, Tuple{typeof(indent),UInt16})] :(indent(::UInt8) = begin… => CodeTracking.MethodInfoKey[CodeTracking.MethodInfoKey(nothing, Tuple{typeof(indent),UInt8})] @@ -361,21 +361,21 @@ and other expressions that are `eval`ed in `Items`. When the file system notifies Revise that a file has been modified, Revise re-parses the file and assigns the expressions to the appropriate modules, creating a -[`Revise.ModuleExprsSigs`](@ref) `mod_exs_sigs_new`. -It then compares `mod_exs_sigs_new` against `mod_exs_sigs_ref`, +[`Revise.ModuleExprsInfos`](@ref) `mod_exs_infos_new`. +It then compares `mod_exs_infos_new` against `mod_exs_infos_ref`, the reference object that is synchronized to code as it was `eval`ed. The following actions are taken: -- if a `def` entry in `mod_exs_sigs_ref` is equal to one in `mod_exs_sigs_new`, the expression is "unchanged" +- if a `def` entry in `mod_exs_infos_ref` is equal to one in `mod_exs_infos_new`, the expression is "unchanged" except possibly for line number. The `locationinfo` in `CodeTracking` is updated as needed. -- if a `def` entry in `mod_exs_sigs_ref` is not present in `mod_exs_sigs_new`, that entry is deleted and +- if a `def` entry in `mod_exs_infos_ref` is not present in `mod_exs_infos_new`, that entry is deleted and any corresponding methods are also deleted. -- if a `def` entry in `mod_exs_sigs_new` is not present in `mod_exs_sigs_ref`, it is `eval`ed and then added to - `mod_exs_sigs_ref`. +- if a `def` entry in `mod_exs_infos_new` is not present in `mod_exs_infos_ref`, it is `eval`ed and then added to + `mod_exs_infos_ref`. -Technically, a new `mod_exs_sigs_ref` is generated every time to ensure that the expressions are -ordered as in `mod_exs_sigs_new`; however, conceptually this is better thought of as an updating of -`mod_exs_sigs_ref`, after which `mod_exs_sigs_new` is discarded. +Technically, a new `mod_exs_infos_ref` is generated every time to ensure that the expressions are +ordered as in `mod_exs_infos_new`; however, conceptually this is better thought of as an updating of +`mod_exs_infos_ref`, after which `mod_exs_infos_new` is discarded. Note that one consequence is that modifying a method causes two actions, the deletion of the original followed by `eval`ing a new version. diff --git a/docs/src/limitations.md b/docs/src/limitations.md index 079d72217..b1a121bb1 100644 --- a/docs/src/limitations.md +++ b/docs/src/limitations.md @@ -1,14 +1,77 @@ # Limitations -There are some kinds of changes that Revise (or often, Julia itself) cannot incorporate into a running Julia session: +There are some kinds of changes that Revise (or often, Julia itself) cannot automatically incorporate into a running Julia session: -- changes to type definitions or `const`s +- changes to global bindings (but see below for struct definitions on Julia 1.12+) - conflicts between variables and functions sharing the same name - removal of `export`s These kinds of changes require that you restart your Julia session. -During early stages of development, it's quite common to want to change type definitions. You can work around Julia's/Revise's limitations by temporary renaming. We'll illustrate this below, using `write` to be explicit about when updates to the file happen. But in ordinary usage, these are changes you'd likely make with your editor. +## Struct revision (Julia 1.12+) + +Starting with Julia 1.12, Revise can handle changes to struct definitions. When you modify +a struct, Revise will automatically re-evaluate the struct definition and any methods or +types that depend on it. + +For example, this now works: + +```julia +struct MyStruct + x::Int +end + +struct UseMyStruct + x::MyStruct +end + +func(ums::UseMyStruct) = println(ums.x.x) +``` + +If you change it to: + +```julia +struct MyStruct + x::Float64 + y::String +end +``` + +Revise will redefine `MyStruct`, and also re-evaluate `UseMyStruct` (which uses `MyStruct` +as a field type) and `func` (which references `UseMyStruct` in its signature). + +## Binding revision is not yet supported + +While struct revision is supported, more general "binding revision" is not yet implemented. +Specifically, Revise does not track implicit dependencies between top-level bindings. + +For example: + +```julia +MyVecType{T} = Vector{T} # changing this to AbstractVector{T} won't update A +struct A{T} + v::MyVecType{T} +end +``` + +If you change `MyVecType{T}` from `Vector{T}` to `AbstractVector{T}`, the struct `A` will +**not** be automatically re-evaluated because Revise does not track the dependency edge +from `MyVecType` to `A`. The same applies to `const` bindings and other global bindings +that are referenced in type definitions. + +Supporting this would require tracking implicit binding edges across all top-level code, +which involves significant interpreter enhancements and is deferred to future work. +This limitation also underlies [the issues with macros and generated functions](@ref other-limitations/macros-and-generated-functions) described below. + +As a workaround, you can manually call [`revise`](@ref) to force re-evaluation of all definitions in `MyModule`, which will pick up the new bindings. + +## Workaround for the struct revision issue before Julia 1.12 + +On Julia versions prior to 1.12, struct definitions cannot be revised. During early stages of development, +it's quite common to want to change type definitions. +You can work around Julia's/Revise's limitations by temporary renaming. +We'll illustrate this below, using `write` to be explicit about when updates to the file happen. +But in ordinary usage, these are changes you'd likely make with your editor. ```julia-repl julia> using Pkg, Revise @@ -58,7 +121,7 @@ julia> write("src/MyPkg.jl",""" export FooStruct, processFoo abstract type AbstractFooStruct end - struct FooStruct2 <: AbstractFooStruct # change version nuumber + struct FooStruct2 <: AbstractFooStruct # change version number bar::Float64 # change type of the field end FooStruct = FooStruct2 # update alias reference @@ -67,8 +130,7 @@ julia> write("src/MyPkg.jl",""" end end - """) -234 + """); julia> FooStruct # make sure FooStruct refers to FooStruct2 MyPkg.FooStruct2 @@ -102,7 +164,7 @@ julia> write("src/MyPkg.jl",""" end end - """) + """); julia> run(Base.julia_cmd()) # start a new Julia session, alternatively exit() and restart julia @@ -118,12 +180,13 @@ Precompiling MyPkg julia> isconst(MyPkg, :FooStruct) true - ``` +## Other limitations + In addition, some situations may require special handling: -### Macros and generated functions +### [Macros and generated functions](@id other-limitations/macros-and-generated-functions) If you change a macro definition or methods that get called by `@generated` functions outside their `quote` block, these changes will not be propagated to functions that have diff --git a/src/loading.jl b/src/loading.jl index c41bd6494..c7cf27eed 100644 --- a/src/loading.jl +++ b/src/loading.jl @@ -110,3 +110,19 @@ function modulefiles(mod::Module) included_files = filter(mf->mf.id == id, includes) return keypath(parentfile), [keypath(mf.filename) for mf in included_files] end + +function modulefiles_basestlibs(id) + ret = Revise.pkg_fileinfo(id) + cachefile, includes = ret === nothing ? (nothing, nothing) : ret[1:2] + # `cachefile` will be nothing for Base and stdlibs that *haven't* been moved out + cachefile === nothing && return Iterators.drop(Base._included_files, 1) # stepping through sysimg.jl rebuilds Base, omit it + # stdlibs that are packages + mod = Base.loaded_modules[id] + return map(includes) do inc + submod = mod + for sm in inc.modpath + submod = getfield(submod, Symbol(sm)) + end + return (submod, inc.filename) + end +end diff --git a/src/logging.jl b/src/logging.jl index 752f47a7b..6c12e3588 100644 --- a/src/logging.jl +++ b/src/logging.jl @@ -44,6 +44,7 @@ CoreLogging.catch_exceptions(::ReviseLogger) = false function Base.show(io::IO, l::LogRecord; kwargs...) verbose = get(io, :verbose, false)::Bool + tmin = get(io, :time_min, nothing)::Union{Float64, Nothing} if !isempty(kwargs) Base.depwarn("Supplying keyword arguments to `show(io, l::Revise.LogRecord; verbose)` is deprecated, use `IOContext` instead.", :show) for (kw, val) in kwargs @@ -57,6 +58,9 @@ function Base.show(io::IO, l::LogRecord; kwargs...) print(io, '(', l.level, ", ", l.message, ", ", l.group, ", ", l.id, ", \"", l.file, "\", ", l.line) else print(io, "Revise ", l.message) + if tmin !== nothing + print(io, ", time=", l.kwargs[:time] - tmin) + end end exc = nothing if !isempty(l.kwargs) @@ -69,15 +73,15 @@ function Base.show(io::IO, l::LogRecord; kwargs...) elseif kw === :deltainfo keepitem = nothing for item in val - if isa(item, DataType) || isa(item, MethodSummary) || (keepitem === nothing && isa(item, Union{RelocatableExpr,Expr})) + if isa(item, Type) || isa(item, Union{MethodSummary,Vector{MethodSummary}}) || (keepitem === nothing && isa(item, Union{RelocatableExpr,Expr})) keepitem = item end end - if isa(keepitem, MethodSummary) + if isa(keepitem, Union{MethodSummary,Vector{MethodSummary}}) print(io, ": ", keepitem) elseif isa(keepitem, Union{RelocatableExpr,Expr}) print(io, ": ", firstline(keepitem)) - elseif isa(keepitem, DataType) + elseif isa(keepitem, Type) print(io, ": ", keepitem) end end @@ -95,6 +99,15 @@ function Base.show(io::IO, l::LogRecord; kwargs...) end end +function Base.show(io::IO, ::MIME"text/plain", rlogger::ReviseLogger) + print(io, "ReviseLogger with min_level=", rlogger.min_level) + if !isempty(rlogger.logs) + println(io, ":") + ioctx = IOContext(io, :time_min => first(rlogger.logs).kwargs[:time], :compact => true) + show(ioctx, MIME("text/plain"), rlogger.logs) + end +end + const _debug_logger = ReviseLogger() """ @@ -113,6 +126,8 @@ with the following relevant fields: examined for possible code changes. This is typically done on the basis of `mtime`, the modification time of the file, and does not necessarily indicate that there were any changes. + + "Bindings": "propagating" consequences of rebinding event(s), where dependent types + or methods need to be re-evaluated. - `message`: a string containing more information. Some examples: + For entries in the "Action" group, `message` can be `"Eval"` when modifying old methods or defining new ones, "DeleteMethod" when deleting a method, diff --git a/src/lowered.jl b/src/lowered.jl index 73452fb8a..9703cc029 100644 --- a/src/lowered.jl +++ b/src/lowered.jl @@ -154,15 +154,15 @@ function minimal_evaluation!(frame::Frame, mode::Symbol) end function methods_by_execution(mod::Module, ex::Expr; kwargs...) - methodinfo = MethodInfo(ex) - _, thk = methods_by_execution!(JuliaInterpreter.Compiled(), methodinfo, mod, ex; kwargs...) - return methodinfo, thk + exinfo = ExInfo(ex) + _, thk = methods_by_execution!(JuliaInterpreter.Compiled(), exinfo, mod, ex; kwargs...) + return exinfo, thk end """ methods_by_execution!( - [interp::Interpreter=JuliaInterpreter.Compiled(),] methodinfo::MethodInfo, mod::Module, ex::Expr; - mode::Symbol=:eval, disablebp::Bool=true, skip_include::Bool=mode!==:eval, always_rethrow::Bool=false + [interp::Interpreter=JuliaInterpreter.Compiled(),] exinfo::ExInfo, mod::Module, ex::Expr; + mode::Symbol = :eval, disablebp::Bool = true, skip_include::Bool = mode!==:eval, always_rethrow::Bool = false ) Evaluate or analyze `ex` in the context of `mod`. @@ -170,13 +170,13 @@ Depending on the setting of `mode` (see the Extended help), it supports full eva evaluation needed to extract method signatures. `interp` controls JuliaInterpreter's evaluation of any non-intercepted statement; likely choices are `JuliaInterpreter.Compiled()` or `JuliaInterpreter.RecursiveInterpreter()`. -`methodinfo` is a cache for storing information about any method definitions (see [`MethodInfo`](@ref)). +`exinfo` is a cache for storing information about any method definitions (see [`ExInfo`](@ref)). # Extended help The action depends on `mode`: -- `:eval` evaluates the expression in `mod`, similar to `Core.eval(mod, ex)` except that `methodinfo` +- `:eval` evaluates the expression in `mod`, similar to `Core.eval(mod, ex)` except that `exinfo` will be populated with information about any signatures. This mode is used to implement `includet`. - `:sigs` analyzes `ex` and extracts signatures of methods (specifically, statements flagged by [`Revise.minimal_evaluation!`](@ref)), but does not evaluate `ex` in the traditional sense. @@ -198,15 +198,15 @@ The other keyword arguments are more straightforward: - `disablebp` controls whether JuliaInterpreter's breakpoints are disabled before stepping through the code. They are restored on exit. -- `skip_include` prevents execution of `include` statements, instead inserting them into `methodinfo`'s +- `skip_include` prevents execution of `include` statements, instead inserting them into `exinfo`'s cache. This defaults to `true` unless `mode` is `:eval`. - `always_rethrow`, if true, causes an error to be thrown if evaluating `ex` triggered an error. If false, the error is logged with `@error`. `InterruptException`s are always rethrown. This is primarily useful for debugging. """ function methods_by_execution!( - interp::Interpreter, methodinfo::MethodInfo, mod::Module, ex::Expr; - mode::Symbol = :eval, disablebp::Bool=true, always_rethrow::Bool=false, kwargs... + interp::Interpreter, exinfo::ExInfo, mod::Module, ex::Expr; + mode::Symbol = :eval, disablebp::Bool = true, always_rethrow::Bool = false, kwargs... ) mode ∈ (:sigs, :eval, :evalmeth, :evalassign) || error("unsupported mode ", mode) lwr = Meta.lower(mod, ex) @@ -250,7 +250,7 @@ function methods_by_execution!( foreach(disable, active_bp_refs) end ret = try - _methods_by_execution!(interp, methodinfo, frame, isrequired; mode, kwargs...) + _methods_by_execution!(interp, exinfo, frame, isrequired; mode, kwargs...) catch err (always_rethrow || isa(err, InterruptException)) && (@isdefined(active_bp_refs) && foreach(enable, active_bp_refs); rethrow(err)) loc = location_string(whereis(frame)) @@ -268,11 +268,11 @@ function methods_by_execution!( end return Pair{Any,Union{Nothing,Expr}}(ret, lwr) end -methods_by_execution!(methodinfo::MethodInfo, mod::Module, ex::Expr; kwargs...) = - methods_by_execution!(Compiled(), methodinfo, mod, ex; kwargs...) +methods_by_execution!(exinfo::ExInfo, mod::Module, ex::Expr; kwargs...) = + methods_by_execution!(Compiled(), exinfo, mod, ex; kwargs...) function _methods_by_execution!( - interp::Interpreter, methodinfo::MethodInfo, frame::Frame, isrequired::AbstractVector{Bool}; + interp::Interpreter, exinfo::ExInfo, frame::Frame, isrequired::AbstractVector{Bool}; mode::Symbol = :eval, skip_include::Bool = true ) isok(lnn::LineTypes) = !iszero(lnn.line) || lnn.file !== :none # might fail either one, but accept anything @@ -296,7 +296,7 @@ function _methods_by_execution!( local value for ex in stmt.args ex isa Expr || continue - value, _ = methods_by_execution!(interp, methodinfo, mod, ex; mode, disablebp=false, skip_include) + value, _ = methods_by_execution!(interp, exinfo, mod, ex; mode, disablebp=false, skip_include) end isassign(frame, pc) && @isdefined(value) && assign_this!(frame, value) pc = next_or_nothing!(frame) @@ -322,7 +322,7 @@ function _methods_by_execution!( file, line = loc lnn = LineNumberNode(Int(line), Symbol(file)) for sig in signatures - add_signature!(methodinfo, sig, lnn) + add_signature!(exinfo, sig, lnn) end end end @@ -407,7 +407,7 @@ function _methods_by_execution!( end if lnn !== nothing && isok(lnn) for sig in signatures - add_signature!(methodinfo, sig, lnn) + add_signature!(exinfo, sig, lnn) end end end @@ -419,10 +419,14 @@ function _methods_by_execution!( pc = step_expr!(interp, frame, stmt, true) end elseif head === :call - f = lookup(interp, frame, stmt.args[1]) - if isdefined(Core, :_defaultctors) && f === Core._defaultctors && length(stmt.args) == 3 - T = lookup(interp, frame, stmt.args[2]) - lnn = lookup(interp, frame, stmt.args[3]) + f = lookup(frame, stmt.args[1]) + if __bpart__ && f === Core._typebody! + analyze_typebody!(exinfo, interp, frame, stmt) + pc = step_expr!(interp, frame, stmt, true) + elseif @static(isdefined(Core, :_defaultctors) && true) && f === Core._defaultctors && length(stmt.args) == 3 + # Create the constructors for a type (i.e., a method definition) + T = lookup(frame, stmt.args[2]) + lnn = lookup(frame, stmt.args[3]) if T isa Type && lnn isa LineNumberNode empty!(signatures) uT = Base.unwrap_unionall(T)::DataType @@ -436,7 +440,7 @@ function _methods_by_execution!( end sig1 == sig2 || push!(signatures, MethodInfoKey(nothing, sig2)) for sig in signatures - add_signature!(methodinfo, sig, lnn) + add_signature!(exinfo, sig, lnn) end end if mode === :sigs @@ -454,16 +458,16 @@ function _methods_by_execution!( newex = newex.args[4] end newex = unwrap(newex) - push!(methodinfo.exprstack, newex) - value, _ = methods_by_execution!(interp, methodinfo, newmod, newex; mode, skip_include, disablebp=false) - pop!(methodinfo.exprstack) + push!(exinfo.exprstack, newex) + value, _ = methods_by_execution!(interp, exinfo, newmod, newex; mode, skip_include, disablebp=false) + pop!(exinfo.exprstack) end assign_this!(frame, value) pc = next_or_nothing!(frame) elseif skip_include && (f === modinclude || f === Core.include || f === Base.include) # include calls need to be managed carefully from several standpoints, including # path management and parsing new expressions - handle_include!(methodinfo, interp, frame, stmt) + handle_include!(exinfo, interp, frame, stmt) assign_this!(frame, nothing) # FIXME: the file might return something different from `nothing` pc = next_or_nothing!(frame) elseif f === Base.Docs.doc! # && mode !== :eval @@ -515,34 +519,47 @@ function _methods_by_execution!( return isrequired[frame.pc] ? get_return(frame) : nothing end -function add_signature!(methodinfo::MethodInfo, mt_sig::MethodInfoKey, ln::LineNumberNode) +function add_signature!(exinfo::ExInfo, mt_sig::MethodInfoKey, ln::LineNumberNode) locdefs = CodeTracking.invoked_get!(Vector{Tuple{LineNumberNode,Expr}}, CodeTracking.method_info, mt_sig) - newdef = unwrap(methodinfo.exprstack[end]) + newdef = unwrap(exinfo.exprstack[end]) if newdef !== nothing if !any(locdef->locdef[1] == ln && isequal(RelocatableExpr(locdef[2]), RelocatableExpr(newdef)), locdefs) push!(locdefs, (fixpath(ln), newdef)) end - push!(methodinfo.allsigs, SigInfo(mt_sig)) + push!(exinfo.allsigs, SigInfo(mt_sig)) end - return methodinfo + return exinfo end -function handle_include!(methodinfo::MethodInfo, interp::Interpreter, frame::Frame, stmt::Expr) +function handle_include!(exinfo::ExInfo, interp::Interpreter, frame::Frame, stmt::Expr) if length(stmt.args) == 2 local arg2 = lookup(interp, frame, stmt.args[2]) if arg2 isa AbstractString - push!(methodinfo.includes, moduleof(frame)=>arg2) - return methodinfo + push!(exinfo.includes, moduleof(frame)=>arg2) + return exinfo end error("Bad include call") elseif length(stmt.args) == 3 local arg2 = lookup(interp, frame, stmt.args[2]) local arg3 = lookup(interp, frame, stmt.args[3]) if arg2 isa Module && arg3 isa AbstractString - push!(methodinfo.includes, arg2=>arg3) - return methodinfo + push!(exinfo.includes, arg2=>arg3) + return exinfo end error("Bad include call") end error("include(mapexpr::Function, mod::Module, path::AbstractString) is not supported") # TODO (issue #634) end + +function analyze_typebody!(exinfo::ExInfo, interp::Interpreter, frame::Frame, stmt::Expr) + if length(stmt.args) == 3 # abstract type definition + typ = lookup(interp, frame, stmt.args[3])::Type + elseif length(stmt.args) == 4 # general struct definition + typ = lookup(interp, frame, stmt.args[3])::Type + else + return nothing + end + datatype = Base.unwrap_unionall(typ)::DataType + push!(exinfo.typeinfos, TypeInfo(datatype.name)) + return exinfo +end diff --git a/src/packagedef.jl b/src/packagedef.jl index ec7216f96..9563368b0 100644 --- a/src/packagedef.jl +++ b/src/packagedef.jl @@ -2,10 +2,15 @@ Base.Experimental.@optlevel 1 using FileWatching, REPL, UUIDs using LibGit2: LibGit2 -using Base: PkgId +using Base: PkgId, IdSet using Base.Meta: isexpr +using InteractiveUtils: InteractiveUtils using Core: CodeInfo, MethodTable +if !isdefined(Core, :isdefinedglobal) + isdefinedglobal(m::Module, s::Symbol) = isdefined(m, s) +end + export revise, includet, entr, MethodSummary ## BEGIN abstract Distributed API @@ -101,6 +106,7 @@ include("utils.jl") include("parsing.jl") include("lowered.jl") include("loading.jl") +include("visit.jl") include("pkgs.jl") include("git.jl") include("recipes.jl") @@ -147,6 +153,9 @@ Global variable, maps `(pkgdata, filename)` pairs that errored upon last revisio """ const queue_errors = Dict{Tuple{PkgData,String},Tuple{Exception, Any}}() # locking is covered by revision_queue_lock +# Can we revise types? +const __bpart__ = Base.VERSION >= v"1.12.0-DEV.2047" + """ Revise.NOPACKAGE @@ -267,6 +276,23 @@ See also [`Revise.silence`](@ref). const dont_watch_pkgs = Set{Symbol}() const silence_pkgs = Set{String}() +function collect_mis(sigs) + mis = Core.MethodInstance[] + world = Base.get_world_counter() + for tt in sigs + matches = Base._methods_by_ftype(tt, 10, world)::Vector + for mm in matches + m = mm.method + for mi in Base.specializations(m) + if mi.specTypes <: tt + push!(mis, mi) + end + end + end + end + return mis +end + ## ## The inputs are sets of expressions found in each file. ## Some of those expressions will generate methods which are identified via their signatures. @@ -276,7 +302,7 @@ const silence_pkgs = Set{String}() ## Strategy: ## - For every old expr not found in the new ones, ## + delete the corresponding methods (using the signatures we've previously computed) -## + remove the sig entries from CodeTracking.method_info (") +## + remove the sig entries from CodeTracking.method_info ## Best to do all the deletion first (across all files and modules) in case a method is ## simply being moved from one file to another. ## - For every new expr found among the old ones, @@ -284,7 +310,7 @@ const silence_pkgs = Set{String}() ## - For every new expr not found in the old ones, ## + eval the expr ## + extract signatures -## + add to the ModuleExprsSigs +## + add to the ModuleExprsInfos ## + add to CodeTracking.method_info ## ## Interestingly, the ex=>mt_sigs link may not be the same as the mt_sigs=>ex link. @@ -301,87 +327,155 @@ const silence_pkgs = Set{String}() ## now this is the right strategy.) From the standpoint of CodeTracking, we should ## link the signature to the actual method-defining expression (either :(f() = 1) or :(g() = 2)). -function delete_missing!(exs_sigs_old::ExprsSigs, exs_sigs_new::ExprsSigs) +# TODO +# - Correct evaluation order (type & method rewrite at the same time) +# - Simplify type matching algorithm + +function delete_missing!( + exs_infos_old::ExprsInfos, exs_infos_new::ExprsInfos, + reeval_list::IdSet{Union{Method,Type}}, handled_types::IdSet{Type}, world::UInt + ) with_logger(_debug_logger) do - for (ex, siginfos) in exs_sigs_old - haskey(exs_sigs_new, ex) && continue + for (rex, exinfos) in exs_infos_old + haskey(exs_infos_new, rex) && continue # ex was deleted - siginfos === nothing && continue - for siginfo in siginfos - mt, sig = siginfo - ret = Base._methods_by_ftype(sig, mt, -1, Base.get_world_counter()) - success = false - if !isempty(ret) - m = ret[end].method # the last method returned is the least-specific that matches, and thus most likely to be type-equal - methsig = m.sig - if sig <: methsig && methsig <: sig - locdefs = get(CodeTracking.method_info, MethodInfoKey(siginfo), nothing) - if isa(locdefs, Vector{Tuple{LineNumberNode,Expr}}) - if length(locdefs) > 1 - # Just delete this reference but keep the method - line = firstline(ex) - ld = map(pr->linediff(line, pr[1]), locdefs) - idx = argmin(ld) - @assert ld[idx] < typemax(eltype(ld)) - deleteat!(locdefs, idx) - continue - else - @assert length(locdefs) == 1 - end - end - @debug "DeleteMethod" _group="Action" time=time() deltainfo=(sig, MethodSummary(m)) - # Delete the corresponding methods - for get_workers in workers_functions - for p in @invokelatest get_workers() - try # guard against serialization errors if the type isn't defined on the worker - @invokelatest remotecall_impl(Core.eval, p, Main, :(delete_method_by_sig($mt, $sig))) - catch - end - end - end - Base.delete_method(m) - # Remove the entries from CodeTracking data - delete!(CodeTracking.method_info, MethodInfoKey(siginfo)) - # Remove frame from JuliaInterpreter, if applicable. Otherwise debuggers - # may erroneously work with outdated code (265-like problems) - if haskey(JuliaInterpreter.framedict, m) - delete!(JuliaInterpreter.framedict, m) - end - if isdefined(m, :generator) - # defensively delete all generated functions - empty!(JuliaInterpreter.genframedict) - end - success = true - end + exinfos === nothing && continue + for exinfo in exinfos + if exinfo isa SigInfo + handle_method_deletion!(exinfo, rex, world) + elseif __bpart__ + handle_type_deletion!(exinfo::TypeInfo, reeval_list, handled_types, world) end - if !success - @debug "FailedDeletion" _group="Action" time=time() deltainfo=(sig,) + end + end + end + return exs_infos_old +end + +const empty_exs_infos = ExprsInfos() +function delete_missing!( + mod_exs_infos_old::ModuleExprsInfos, mod_exs_infos_new::ModuleExprsInfos, + reeval_list::IdSet{Union{Method,Type}}, handled_types::IdSet{Type}, world::UInt + ) + for (mod, exs_infos_old) in mod_exs_infos_old + exs_infos_new = get(mod_exs_infos_new, mod, empty_exs_infos) + delete_missing!(exs_infos_old, exs_infos_new, reeval_list, handled_types, world) + end + return mod_exs_infos_old +end + +function handle_method_deletion!(siginfo::SigInfo, rex::RelocatableExpr, world::UInt) + mt, sig = siginfo + ret = Base._methods_by_ftype(sig, mt, -1, world) + isempty(ret) && return nothing + m = ret[end].method # the last method returned is the least-specific that matches, and thus most likely to be type-equal + methsig = m.sig + if sig <: methsig && methsig <: sig + locdefs = get(CodeTracking.method_info, MethodInfoKey(siginfo), nothing) + if isa(locdefs, Vector{Tuple{LineNumberNode,Expr}}) + if length(locdefs) > 1 + # Just delete this reference but keep the method + line = firstline(rex) + ld = map(pr->linediff(line, pr[1]), locdefs) + idx = argmin(ld) + @assert ld[idx] < typemax(eltype(ld)) + deleteat!(locdefs, idx) + return nothing + else + @assert length(locdefs) == 1 + end + end + @debug "DeleteMethod" _group="Action" time=time() deltainfo=(sig, MethodSummary(m)) + # Delete the corresponding methods + for get_workers in workers_functions + for p in @invokelatest get_workers() + try # guard against serialization errors if the type isn't defined on the worker + @invokelatest remotecall_impl(Core.eval, p, Main, :(delete_method_by_sig($mt, $sig))) + catch end end end + Base.delete_method(m) + # Remove the entries from CodeTracking data + delete!(CodeTracking.method_info, MethodInfoKey(siginfo)) + # Remove frame from JuliaInterpreter, if applicable. Otherwise debuggers + # may erroneously work with outdated code (265-like problems) + if haskey(JuliaInterpreter.framedict, m) + delete!(JuliaInterpreter.framedict, m) + end + if isdefined(m, :generator) + # defensively delete all generated functions + empty!(JuliaInterpreter.genframedict) + end + else + @debug "FailedSigDeletion" _group="Action" time=time() deltainfo=(siginfo,world) + end + nothing +end + +function handle_type_deletion!( + typeinfo::TypeInfo, reeval_list::IdSet{Union{Method,Type}}, handled_types::IdSet{Type}, world::UInt + ) + oldtypename = typeinfo.typname + with_logger(_debug_logger) do + old_list = copy(reeval_list) + oldtype = Base.invoke_in_world(world, getglobal, oldtypename.module, oldtypename.name)::Type + alltypes = collect_all_subtypes(Any) # reuse cache for recursive searches performance (freezed at this world) + record_invalidations_for_type_deletion!(oldtype, reeval_list, handled_types, alltypes) + diff = setdiff(reeval_list, old_list) + @debug "DeleteType" _group="Action" time=time() deltainfo=(oldtype,diff) end - return exs_sigs_old + return reeval_list end -const empty_exs_sigs = ExprsSigs() -function delete_missing!(mod_exs_sigs_old::ModuleExprsSigs, mod_exs_sigs_new::ModuleExprsSigs) - for (mod, exs_sigs_old) in mod_exs_sigs_old - exs_sigs_new = get(mod_exs_sigs_new, mod, empty_exs_sigs) - delete_missing!(exs_sigs_old, exs_sigs_new) +function record_invalidations_for_type_deletion!( + @nospecialize(oldtype::Type), reeval_list::IdSet{Union{Method,Type}}, handled_types::IdSet{Type}, + alltypes::Base.IdSet{Type} + ) + push!(handled_types, oldtype) + + olddatatype = Base.unwrap_unionall(oldtype)::DataType + oldtypename = olddatatype.name + + # Find all methods restricted to `oldtype` + meths = old_methods_with(oldtypename) + meths !== nothing && union!(reeval_list, meths) + + # Find all types using `oldtype` + related_types = old_types_with(oldtypename, alltypes) + related_types !== nothing && union!(reeval_list, related_types) + + # For any modules that have not yet been parsed and had their signatures extracted, + # we need to do this now, before the binding changes to the new type + meths !== nothing && maybe_extract_sigs_for_meths(meths) + related_types !== nothing && maybe_extract_sigs_for_types(related_types) + + # If `oldtype` is an abstract type, we need to traverse its subtypes and invalidate them + oldsubtypes = collect_all_subtypes(oldtype) + maybe_extract_sigs_for_types(oldsubtypes) + for oldsubtype in oldsubtypes + oldsubtype in handled_types && continue + push!(reeval_list, oldsubtype) + record_invalidations_for_type_deletion!(oldsubtype, reeval_list, handled_types, alltypes) + end + + # `related_types` will also be recursively redefined, so we need to invalidate methods/types related to them as well + related_types !== nothing && for related_type in related_types + related_type in handled_types && continue + record_invalidations_for_type_deletion!(related_type, reeval_list, handled_types, alltypes) end - return mod_exs_sigs_old end -function eval_rex(rex_new::RelocatableExpr, exs_sigs_old::ExprsSigs, mod::Module; mode::Symbol=:eval) +function eval_rex(rex_new::RelocatableExpr, exs_infos_old::ExprsInfos, mod::Module; mode::Symbol=:eval) return with_logger(_debug_logger) do - siginfos, includes = nothing, nothing - rex_old = getkey(exs_sigs_old, rex_new, nothing) + exinfos, includes = nothing, nothing + rex_old = getkey(exs_infos_old, rex_new, nothing) # extract the signatures and update the line info if rex_old === nothing ex = rex_new.ex # ex is not present in old @debug titlecase(String(mode)) _group="Action" time=time() deltainfo=(mod, ex, mode) - siginfos, includes, thunk = eval_with_signatures(mod, ex; mode) # All signatures defined by `ex` + exinfos, includes, thunk = eval_with_signatures(mod, ex; mode) # All signatures defined by `ex` if !isexpr(thunk, :thunk) thunk = ex end @@ -397,76 +491,80 @@ function eval_rex(rex_new::RelocatableExpr, exs_sigs_old::ExprsSigs, mod::Module end end else - siginfos = exs_sigs_old[rex_old] + exinfos = exs_infos_old[rex_old] # Update location info ln, lno = firstline(unwrap(rex_new)), firstline(unwrap(rex_old)) - if siginfos !== nothing && !isempty(siginfos) && ln != lno + if exinfos !== nothing && !isempty(exinfos) && ln != lno ln, lno = ln::LineNumberNode, lno::LineNumberNode - @debug "LineOffset" _group="Action" time=time() deltainfo=(siginfos, lno=>ln) - for siginfo in siginfos - locdefs = CodeTracking.method_info[MethodInfoKey(siginfo)]::AbstractVector - ld = let lno=lno - map(pr->linediff(lno, pr[1]), locdefs) - end - idx = argmin(ld) - if ld[idx] === typemax(eltype(ld)) - # println("Missing linediff for $lno and $(first.(locdefs)) with ", rex.ex) - idx = length(locdefs) + @debug "LineOffset" _group="Action" time=time() deltainfo=(exinfos, lno=>ln) + for exinfo in exinfos + if exinfo isa SigInfo + locdefs = CodeTracking.method_info[MethodInfoKey(exinfo)]::AbstractVector + ld = let lno=lno + map(pr->linediff(lno, pr[1]), locdefs) + end + idx = argmin(ld) + if ld[idx] === typemax(eltype(ld)) + # println("Missing linediff for $lno and $(first.(locdefs)) with ", rex.ex) + idx = length(locdefs) + end + _, methdef = locdefs[idx] + locdefs[idx] = (fixpath(ln), methdef) end - _, methdef = locdefs[idx] - locdefs[idx] = (fixpath(ln), methdef) end end end - return siginfos, includes + return exinfos, includes end end # These are typically bypassed in favor of expression-by-expression evaluation to # allow handling of new `include` statements. -function eval_new!(exs_sigs_new::ExprsSigs, exs_sigs_old::ExprsSigs, mod::Module; mode::Symbol=:eval) +function eval_new!(exs_infos_new::ExprsInfos, exs_infos_old::ExprsInfos, mod::Module; mode::Symbol=:eval) includes = Vector{Pair{Module,String}}() - for rex in keys(exs_sigs_new) - siginfos, _includes = eval_rex(rex, exs_sigs_old, mod; mode) - if siginfos !== nothing - exs_sigs_new[rex] = siginfos + for rex in keys(exs_infos_new) + exinfos, includes′ = eval_rex(rex, exs_infos_old, mod; mode) + if exinfos !== nothing + exs_infos_new[rex] = exinfos end - if _includes !== nothing - append!(includes, _includes) + if includes′ !== nothing + append!(includes, includes′) end end - return exs_sigs_new, includes + return exs_infos_new, includes end -function eval_new!(mod_exs_sigs_new::ModuleExprsSigs, mod_exs_sigs_old::ModuleExprsSigs; mode::Symbol=:eval) +function eval_new!(mod_exs_infos_new::ModuleExprsInfos, mod_exs_infos_old::ModuleExprsInfos; mode::Symbol=:eval) includes = Vector{Pair{Module,String}}() - for (mod, exs_sigs_new) in mod_exs_sigs_new + for (mod, exs_infos_new) in mod_exs_infos_new # Allow packages to override the supplied mode if isdefined(mod, :__revise_mode__) mode = getfield(mod, :__revise_mode__)::Symbol end - exs_sigs_old = get(mod_exs_sigs_old, mod, empty_exs_sigs) - _, _includes = eval_new!(exs_sigs_new, exs_sigs_old, mod; mode) + exs_infos_old = get(mod_exs_infos_old, mod, empty_exs_infos) + _, _includes = eval_new!(exs_infos_new, exs_infos_old, mod; mode) append!(includes, _includes) end - return mod_exs_sigs_new, includes + return mod_exs_infos_new, includes end # Eval and insert into CodeTracking data function eval_with_signatures(mod::Module, ex::Expr; mode::Symbol=:eval, kwargs...) - methodinfo = MethodInfo(ex) - _, thk = methods_by_execution!(methodinfo, mod, ex; mode, kwargs...) - return methodinfo.allsigs, methodinfo.includes, thk + exinfo = ExInfo(ex) + _, thk = methods_by_execution!(exinfo, mod, ex; mode, kwargs...) + exinfos = Union{SigInfo,TypeInfo}[] + append!(exinfos, exinfo.allsigs, exinfo.typeinfos) + return exinfos, exinfo.includes, thk end -function instantiate_sigs!(mod_exs_sigs::ModuleExprsSigs; mode::Symbol=:sigs, kwargs...) - for (mod, exs_sigs) in mod_exs_sigs - for rex in keys(exs_sigs) +function instantiate_sigs!(mod_exs_infos::ModuleExprsInfos; mode::Symbol=:sigs, kwargs...) + for (mod, exs_infos) in mod_exs_infos + for rex in keys(exs_infos) is_doc_expr(rex.ex) && continue - exs_sigs[rex], _ = eval_with_signatures(mod, rex.ex; mode, kwargs...) + exs_infos[rex], _, _ = eval_with_signatures(mod, rex.ex; mode, kwargs...) end end - return mod_exs_sigs + return mod_exs_infos end # This is intended for testing purposes, but not general use. The key problem is @@ -474,10 +572,13 @@ end # risk you could end up deleting the method altogether depending on the order in which you # process these. # See `revise` for the proper approach. -function eval_revised(mod_exs_sigs_new::ModuleExprsSigs, mod_exs_sigs_old::ModuleExprsSigs) - delete_missing!(mod_exs_sigs_old, mod_exs_sigs_new) - eval_new!(mod_exs_sigs_new, mod_exs_sigs_old) # note: drops `includes` - instantiate_sigs!(mod_exs_sigs_new) +function eval_revised(mod_exs_infos_new::ModuleExprsInfos, mod_exs_infos_old::ModuleExprsInfos) + reeval_list = IdSet{Union{Method,Type}}() + handled_types = IdSet{Type}() + world = Base.get_world_counter() + delete_missing!(mod_exs_infos_old, mod_exs_infos_new, reeval_list, handled_types, world) + eval_new!(mod_exs_infos_new, mod_exs_infos_old) # note: drops `includes` + instantiate_sigs!(mod_exs_infos_new) end """ @@ -621,10 +722,13 @@ function revise_file_queued(pkgdata::PkgData, file) end # Because we delete first, we have to make sure we've parsed the file -function handle_deletions(pkgdata, file) +function handle_deletions( + pkgdata::PkgData, file::AbstractString, + reeval_list::IdSet{Union{Method,Type}}, handled_types::IdSet{Type}, world::UInt + ) fi = maybe_parse_from_cache!(pkgdata, file) maybe_extract_sigs!(fi) - mod_exs_sigs_old = fi.mod_exs_sigs + mod_exs_infos_old = fi.mod_exs_infos idx = fileindex(pkgdata, file) filep = pkgdata.info.files[idx] if isa(filep, AbstractString) @@ -634,11 +738,11 @@ function handle_deletions(pkgdata, file) filep = normpath(basedir(pkgdata)) end end - topmod = first(keys(mod_exs_sigs_old)) + topmod = first(keys(mod_exs_infos_old)) fileok = file_exists(String(filep)::String) - mod_exs_sigs_new = fileok ? parse_source(filep, topmod) : ModuleExprsSigs(topmod) - if mod_exs_sigs_new !== nothing && mod_exs_sigs_new !== DoNotParse() - delete_missing!(mod_exs_sigs_old, mod_exs_sigs_new) + mod_exs_infos_new = fileok ? parse_source(filep, topmod) : ModuleExprsInfos(topmod) + if mod_exs_infos_new !== nothing && mod_exs_infos_new !== DoNotParse() + delete_missing!(mod_exs_infos_old, mod_exs_infos_new, reeval_list, handled_types, world) end if !fileok @warn("$filep no longer exists, deleted all methods") @@ -649,7 +753,98 @@ function handle_deletions(pkgdata, file) delete!(wl.trackedfiles, file) end end - return mod_exs_sigs_new, mod_exs_sigs_old + return mod_exs_infos_new, mod_exs_infos_old +end + +struct ReevalInfo + reeval::Union{Method,Type} + mod::Module + exs_infos::ExprsInfos + rex::RelocatableExpr + pkgdata::PkgData + file::String + ReevalInfo( + @nospecialize(reeval::Union{Method,Type}), mod::Module, exs_infos::ExprsInfos, rex::RelocatableExpr, + pkgdata::PkgData, file::String + ) = new(reeval, mod, exs_infos, rex, pkgdata, file) +end + +function redefine_bindings!(revision_errors::Vector{Tuple{PkgData,String}}, reeval_list::IdSet{Union{Method,Type}}, world::UInt) + reeval_infos = ReevalInfo[] + + # N.B. This traverse could become expensive when Revise tracked code becomes large + # We could optimize this part by preparing a `CodeTracking.ex_info` cache that incorporates + # type information as well as index information to `pkgdatas` into the `CodeTracking.method_info` cache, + # and updating `CodeTracking.ex_info` in sync with `pkgdatas` updates, + # then performing lookups to `CodeTracking.ex_info` instead + for (_, pkgdata) in pkgdatas + for (file, fileinfo) in zip(srcfiles(pkgdata), pkgdata.fileinfos) + for (mod, exs_infos) in fileinfo.mod_exs_infos + for (rex, exinfos) in exs_infos + exinfos === nothing && continue + for exinfo in exinfos + if exinfo isa SigInfo + mt, sig = exinfo + ret = Base._methods_by_ftype(sig, mt, -1, world) + isempty(ret) && continue + for match in ret + if match.method in reeval_list + push!(reeval_infos, ReevalInfo(match.method, mod, exs_infos, rex, pkgdata, file)) + break + end + end + else exinfo::TypeInfo + typeinfo = exinfo + if Base.invoke_in_world(world, isdefinedglobal, typeinfo.typname.module, typeinfo.typname.name) + typ = Base.invoke_in_world(world, getglobal, typeinfo.typname.module, typeinfo.typname.name) + if typ isa Type && typ in reeval_list + push!(reeval_infos, ReevalInfo(typ, mod, exs_infos, rex, pkgdata, file)) + end + end + end + end + end + end + end + end + for (; reeval, mod, exs_infos, rex, pkgdata, file) in reeval_infos + reeval isa Type || continue + with_logger(_debug_logger) do + @debug "ReevalType" _group="Action" time=time() deltainfo=(reeval,mod,rex) + try + newexinfos, _, _ = eval_with_signatures(mod, rex.ex; mode=:eval) + exs_infos[rex] = newexinfos + catch err + # Re-evaluation failed, likely due to type incompatibility + # Clear exs_infos cache for this `rex` so that we will retry evaluation when methods become compatible + delete!(exs_infos, rex) + @debug "ReevalTypeFailed" _group="Action" time=time() deltainfo=(reeval,mod,rex,err) + push!(revision_errors, (pkgdata, file)) + queue_errors[(pkgdata, file)] = (err, catch_backtrace()) + end + end + end + for (; reeval, mod, exs_infos, rex, pkgdata, file) in reeval_infos + reeval isa Method || continue + with_logger(_debug_logger) do + @debug "ReevalDeleteMethod" _group="Action" time=time() deltainfo=(reeval.sig, MethodSummary(reeval)) + # ensure that "old data" doesn't get run with "old methods" + try Base.delete_method(reeval) catch end + @debug "ReevalMethod" _group="Action" time=time() deltainfo=(reeval, reeval.module, rex) + try + newexinfos, _, _ = eval_with_signatures(mod, rex.ex; mode=:eval) + exs_infos[rex] = newexinfos + catch err + # Re-evaluation failed, likely due to type incompatibility + # Clear exs_infos cache for this `rex` so that we will retry evaluation when methods become compatible + delete!(exs_infos, rex) + @debug "ReevalMethodFailed" _group="Action" time=time() deltainfo=(reeval,mod,rex,err) + push!(revision_errors, (pkgdata, file)) + queue_errors[(pkgdata, file)] = (err, catch_backtrace()) + end + end + end + return revision_errors end """ @@ -671,11 +866,14 @@ function revise_file_now(pkgdata::PkgData, file) println("Revise is currently tracking the following files in $(PkgId(pkgdata)): ", srcfiles(pkgdata)) error(file, " is not currently being tracked.") end - mod_exs_sigs_new, mod_exs_sigs_old = handle_deletions(pkgdata, file) - if mod_exs_sigs_new != nothing - _, includes = eval_new!(mod_exs_sigs_new, mod_exs_sigs_old) + reeval_list = IdSet{Union{Method,Type}}() + handled_types = IdSet{Type}() + world = Base.get_world_counter() + mod_exs_infos_new, mod_exs_infos_old = handle_deletions(pkgdata, file, reeval_list, handled_types, world) + if mod_exs_infos_new != nothing + _, includes = eval_new!(mod_exs_infos_new, mod_exs_infos_old) fi = fileinfo(pkgdata, i) - pkgdata.fileinfos[i] = FileInfo(mod_exs_sigs_new, fi) + pkgdata.fileinfos[i] = FileInfo(mod_exs_infos_new, fi) maybe_add_includes_to_pkgdata!(pkgdata, file, includes; eval_now=true) end nothing @@ -728,21 +926,28 @@ otherwise these are only logged. function revise(; throw::Bool=false) active[] || return nothing sleep(0.01) # in case the file system isn't quite done writing out the new files + @lock revision_queue_lock begin have_queue_errors = !isempty(queue_errors) + # Julia 1.12+: when bindings switch to a new type, we need to re-evaluate method + # definitions using the new binding resolution. + reeval_list = IdSet{Union{Method,Type}}() + handled_types = IdSet{Type}() + world = Base.get_world_counter() + # Do all the deletion first. This ensures that a method that moved from one file to another # won't get redefined first and deleted second. revision_errors = Tuple{PkgData,String}[] queue = sort!(collect(revision_queue); lt=pkgfileless) finished = eltype(revision_queue)[] - mod_exs_sigs_new_list = ModuleExprsSigs[] + mod_exs_infos = ModuleExprsInfos[] interrupt = false for (pkgdata, file) in queue try - mod_exs_sigs_new, _ = handle_deletions(pkgdata, file) - mod_exs_sigs_new === DoNotParse() && continue - push!(mod_exs_sigs_new_list, mod_exs_sigs_new) + mod_exs_infos_new, _ = handle_deletions(pkgdata, file, reeval_list, handled_types, world) + mod_exs_infos_new === DoNotParse() && continue + push!(mod_exs_infos, mod_exs_infos_new) push!(finished, (pkgdata, file)) catch err throw && Base.throw(err) @@ -751,30 +956,31 @@ function revise(; throw::Bool=false) queue_errors[(pkgdata, file)] = (err, catch_backtrace()) end end + # Do the evaluation - for ((pkgdata, file), mod_exs_sigs_new) in zip(finished, mod_exs_sigs_new_list) + for ((pkgdata, file), mod_exs_infos_new) in zip(finished, mod_exs_infos) defaultmode = PkgId(pkgdata).name == "Main" ? :evalmeth : :eval i = fileindex(pkgdata, file) i === nothing && continue # file was deleted by `handle_deletions` fi = fileinfo(pkgdata, i) - modsremaining = Set(keys(mod_exs_sigs_new)) + modsremaining = Set(keys(mod_exs_infos_new)) changed, err = true, nothing while changed changed = false - for (mod, exs_sigs_new) in mod_exs_sigs_new + for (mod, exs_infos_new) in mod_exs_infos_new mod ∈ modsremaining || continue try mode = defaultmode # Allow packages to override the supplied mode - if isdefined(mod, :__revise_mode__) - mode = getfield(mod, :__revise_mode__)::Symbol + if isdefinedglobal(mod, :__revise_mode__) + mode = getglobal(mod, :__revise_mode__)::Symbol end mode ∈ (:sigs, :eval, :evalmeth, :evalassign) || error("unsupported mode ", mode) - exs_sigs_old = get(fi.mod_exs_sigs, mod, empty_exs_sigs) - for rex in keys(exs_sigs_new) - siginfos, includes = eval_rex(rex, exs_sigs_old, mod; mode) - if siginfos !== nothing - exs_sigs_new[rex] = siginfos + exs_infos_old = get(fi.mod_exs_infos, mod, empty_exs_infos) + for rex in keys(exs_infos_new) + exinfos, includes = eval_rex(rex, exs_infos_old, mod; mode) + if exinfos !== nothing + exs_infos_new[rex] = exinfos end if includes !== nothing maybe_add_includes_to_pkgdata!(pkgdata, file, includes; eval_now=true) @@ -788,7 +994,7 @@ function revise(; throw::Bool=false) end end if isempty(modsremaining) || isa(err, LoweringException) # fix #877 - pkgdata.fileinfos[i] = FileInfo(mod_exs_sigs_new, fi) + pkgdata.fileinfos[i] = FileInfo(mod_exs_infos_new, fi) end if isempty(modsremaining) delete!(queue_errors, (pkgdata, file)) @@ -799,6 +1005,13 @@ function revise(; throw::Bool=false) queue_errors[(pkgdata, file)] = (err, catch_backtrace()) end end + + # Do binding redefinitions + if __bpart__ + redefine_bindings!(revision_errors, reeval_list, world) + end + + # Error handling if interrupt for pkgfile in finished haskey(queue_errors, pkgfile) || delete!(revision_queue, pkgfile) @@ -854,8 +1067,8 @@ function revise(mod::Module; force::Bool=true) force || return true for i = 1:length(srcfiles(pkgdata)) fi = fileinfo(pkgdata, i) - for (mod, exs_sigs) in fi.mod_exs_sigs - for def_rex in keys(exs_sigs) + for (mod, exs_infos) in fi.mod_exs_infos + for def_rex in keys(exs_infos) ex = def_rex.ex exuw = unwrap(ex) isexpr(exuw, :call) && is_some_include(exuw.args[1]) && continue @@ -902,12 +1115,12 @@ function track(mod::Module, file::AbstractString; mode=:sigs, kwargs...) file = abspath_no_normalize(file) end # Set up tracking - mod_exs_sigs = parse_source(file, mod; mode) - if mod_exs_sigs !== nothing + mod_exs_infos = parse_source(file, mod; mode) + if mod_exs_infos !== nothing if mode === :includet mode = :sigs # we already handled evaluation in `parse_source` end - instantiate_sigs!(mod_exs_sigs; mode, kwargs...) + invokelatest(instantiate_sigs!, mod_exs_infos; mode, kwargs...) if !haskey(pkgdatas, id) # Wait a bit to see if `mod` gets initialized sleep(0.1) @@ -920,7 +1133,7 @@ function track(mod::Module, file::AbstractString; mode=:sigs, kwargs...) CodeTracking._pkgfiles[id] = pkgdata.info end @lock pkgdatas_lock begin - push!(pkgdata, relpath(file, pkgdata)=>FileInfo(mod_exs_sigs)) + push!(pkgdata, relpath(file, pkgdata)=>FileInfo(mod_exs_infos)) init_watching(pkgdata, (String(file)::String,)) pkgdatas[id] = pkgdata end @@ -1110,9 +1323,9 @@ function get_def(method::Method; modified_files=revision_queue) isdefined(Base, :active_repl) || return false fi = add_definitions_from_repl(filename) hassig = false - for (_, exs_sigs) in fi.mod_exs_sigs - for siginfos in values(exs_sigs) - hassig |= !isempty(siginfos) + for (_, exs_infos) in fi.mod_exs_infos + for exinfos in values(exs_infos) + hassig |= !isempty(exinfos) end end return hassig @@ -1179,7 +1392,7 @@ function get_expressions(id::PkgId, filename) pkgdata = pkgdatas[id] fi = maybe_parse_from_cache!(pkgdata, filename) maybe_extract_sigs!(fi) - return fi.mod_exs_sigs + return fi.mod_exs_infos end function add_definitions_from_repl(filename::String) @@ -1188,10 +1401,10 @@ function add_definitions_from_repl(filename::String) src = hp.history[hp.start_idx+hist_idx] id = PkgId(nothing, "@REPL") pkgdata = pkgdatas[id] - mod_exs_sigs = ModuleExprsSigs(Main::Module) - parse_source!(mod_exs_sigs, src, filename, Main::Module) - instantiate_sigs!(mod_exs_sigs) - fi = FileInfo(mod_exs_sigs) + mod_exs_infos = ModuleExprsInfos(Main::Module) + parse_source!(mod_exs_infos, src, filename, Main::Module) + instantiate_sigs!(mod_exs_infos) + fi = FileInfo(mod_exs_infos) push!(pkgdata, filename=>fi) return fi end @@ -1439,6 +1652,19 @@ function __init__() end end end + + # Populate field types map cache (only on main process, not on workers) + if __bpart__ && (isnothing(distributed_module) || distributed_module.myid() == 1) + Threads.@spawn :default @lock types_cache_lock for type in collect_all_subtypes(Any) + nflds = Base.Compiler.fieldcount_noerror(type) + if nflds !== nothing && nflds > 0 + types = collect(Any, fieldtypes(type)) + else + types = nothing + end + types_cache[type] = types + end + end return nothing end diff --git a/src/parsing.jl b/src/parsing.jl index 0e053286a..2b0910bc4 100644 --- a/src/parsing.jl +++ b/src/parsing.jl @@ -3,17 +3,17 @@ struct DoNotParse end """ mexs = parse_source(filename::AbstractString, mod::Module) -Parse the source `filename`, returning a [`ModuleExprsSigs`](@ref) `mexs`. +Parse the source `filename`, returning a [`ModuleExprsInfos`](@ref) `mexs`. `mod` is the "parent" module for the file (i.e., the one that `include`d the file); if `filename` defines more module(s) then these will all have separate entries in `mexs`. If parsing `filename` fails, `nothing` is returned. """ parse_source(filename::AbstractString, mod::Module; kwargs...) = - parse_source!(ModuleExprsSigs(mod), filename, mod; kwargs...) + parse_source!(ModuleExprsInfos(mod), filename, mod; kwargs...) """ - parse_source!(mexs::ModuleExprsSigs, filename, mod::Module) + parse_source!(mexs::ModuleExprsInfos, filename, mod::Module) Top-level parsing of `filename` as included into module `mod`. Successfully-parsed expressions will be added to `mexs`. Returns @@ -21,7 +21,7 @@ Top-level parsing of `filename` as included into module See also [`Revise.parse_source`](@ref). """ -function parse_source!(mod_exprs_sigs::ModuleExprsSigs, filename::AbstractString, mod::Module; kwargs...) +function parse_source!(mod_exprs_sigs::ModuleExprsInfos, filename::AbstractString, mod::Module; kwargs...) if !isfile(filename) @warn "$filename is not a file, omitting from revision tracking" return nothing @@ -29,7 +29,7 @@ function parse_source!(mod_exprs_sigs::ModuleExprsSigs, filename::AbstractString return parse_source!(mod_exprs_sigs, read(filename, String), filename, mod; kwargs...) end -function parse_source!(mod_exprs_sigs::ModuleExprsSigs, src::AbstractString, filename::AbstractString, mod::Module; kwargs...) +function parse_source!(mod_exprs_sigs::ModuleExprsInfos, src::AbstractString, filename::AbstractString, mod::Module; kwargs...) if startswith(src, "# REVISE: DO NOT PARSE") return DoNotParse() end @@ -43,7 +43,7 @@ function parse_source!(mod_exprs_sigs::ModuleExprsSigs, src::AbstractString, fil end end -function process_ex!(mod_exprs_sigs::ModuleExprsSigs, ex::Expr, filename::AbstractString, mod::Module; mode::Symbol=:sigs) +function process_ex!(mod_exprs_sigs::ModuleExprsInfos, ex::Expr, filename::AbstractString, mod::Module; mode::Symbol=:sigs) if isexpr(ex, :error) || isexpr(ex, :incomplete) return eval(ex) end @@ -58,7 +58,7 @@ function process_ex!(mod_exprs_sigs::ModuleExprsSigs, ex::Expr, filename::Abstra throw(ReviseEvalException(loc, err, Any[(sf, 1) for sf in stacktrace(bt)])) end end - exprs_sigs = get!(ExprsSigs, mod_exprs_sigs, mod) + exprs_sigs = get!(ExprsInfos, mod_exprs_sigs, mod) if ex.head === :toplevel lnn = nothing for a in ex.args diff --git a/src/pkgs.jl b/src/pkgs.jl index 459cf7e63..00e98407f 100644 --- a/src/pkgs.jl +++ b/src/pkgs.jl @@ -25,10 +25,10 @@ function queue_includes!(pkgdata::PkgData, id::PkgId) end modname = String(Symbol(mod)) if startswith(modname, modstring) || endswith(fname, modstring*".jl") - mod_exs_sigs = parse_source(fname, mod) - if mod_exs_sigs !== nothing + mod_exs_infos = parse_source(fname, mod) + if mod_exs_infos !== nothing fname = relpath(fname, pkgdata) - push!(pkgdata, fname=>FileInfo(mod_exs_sigs)) + push!(pkgdata, fname=>FileInfo(mod_exs_infos)) end push!(delids, i) end @@ -90,13 +90,13 @@ function maybe_parse_from_cache!(pkgdata::PkgData, file::AbstractString) return add_definitions_from_repl(file) end fi = fileinfo(pkgdata, file) - if (isempty(fi.mod_exs_sigs) && !fi.parsed[]) && (!isempty(fi.cachefile) || !isempty(fi.cacheexprs)) + if (isempty(fi.mod_exs_infos) && !fi.parsed[]) && (!isempty(fi.cachefile) || !isempty(fi.cacheexprs)) # Source was never parsed, get it from the precompile cache src = read_from_cache(pkgdata, file) filep = joinpath(basedir(pkgdata), file) filec = get(cache_file_key, filep, filep) - topmod = first(keys(fi.mod_exs_sigs)) - ret = parse_source!(fi.mod_exs_sigs, src, filec, topmod) + topmod = first(keys(fi.mod_exs_infos)) + ret = parse_source!(fi.mod_exs_infos, src, filec, topmod) if ret === nothing @error "failed to parse cache file source text for $file" end @@ -111,24 +111,59 @@ end function add_modexs!(fi::FileInfo, modexs::Vector{Tuple{Module,Expr}}) for (mod, rex) in modexs - exs_sigs = get(fi.mod_exs_sigs, mod, nothing) - if exs_sigs === nothing - fi.mod_exs_sigs[mod] = exs_sigs = ExprsSigs() + exs_infos = get(fi.mod_exs_infos, mod, nothing) + if exs_infos === nothing + fi.mod_exs_infos[mod] = exs_infos = ExprsInfos() end - pushex!(exs_sigs, rex) + pushex!(exs_infos, rex) end return fi end function maybe_extract_sigs!(fi::FileInfo) if !fi.extracted[] - instantiate_sigs!(fi.mod_exs_sigs) + instantiate_sigs!(fi.mod_exs_infos) fi.extracted[] = true end return fi end maybe_extract_sigs!(pkgdata::PkgData, file::AbstractString) = maybe_extract_sigs!(fileinfo(pkgdata, file)) +is_not_populated(fi::FileInfo) = + (isempty(fi.mod_exs_infos) && !fi.parsed[]) && (!isempty(fi.cachefile) || !isempty(fi.cacheexprs)) + +function maybe_extract_sigs_for_meths(meths) + for m in meths + methinfo = get(CodeTracking.method_info, MethodInfoKey(m), false) + if methinfo === false + pkgdata = get(pkgdatas, PkgId(m.module), nothing) + pkgdata === nothing && continue + for file in srcfiles(pkgdata) + fi = fileinfo(pkgdata, file) + if is_not_populated(fi) + fi = maybe_parse_from_cache!(pkgdata, file) + instantiate_sigs!(fi.mod_exs_infos) + end + end + end + end +end + +function maybe_extract_sigs_for_types(types) + for ty in types + m = parentmodule(ty) + pkgdata = get(pkgdatas, PkgId(m), nothing) + pkgdata === nothing && continue + for file in srcfiles(pkgdata) + fi = fileinfo(pkgdata, file) + if is_not_populated(fi) + fi = maybe_parse_from_cache!(pkgdata, file) + instantiate_sigs!(fi.mod_exs_infos) + end + end + end +end + function maybe_add_includes_to_pkgdata!(pkgdata::PkgData, file::AbstractString, includes; eval_now::Bool=false) for (mod, inc) in includes inc = joinpath(splitdir(file)[1], inc) @@ -148,10 +183,10 @@ function maybe_add_includes_to_pkgdata!(pkgdata::PkgData, file::AbstractString, # Parse the source of the new file fullfile = joinpath(basedir(pkgdata), incrp) if isfile(fullfile) - parse_source!(fi.mod_exs_sigs, fullfile, mod) + parse_source!(fi.mod_exs_infos, fullfile, mod) if eval_now # Use runtime dispatch to reduce latency - Base.invokelatest(instantiate_sigs!, fi.mod_exs_sigs; mode=:eval) + Base.invokelatest(instantiate_sigs!, fi.mod_exs_infos; mode=:eval) end end # Add to watchlist @@ -202,7 +237,7 @@ function add_require(sourcefile::String, modcaller::Module, idmod::String, ::Str push!(modincludes, (modcaller, inc)) end maybe_add_includes_to_pkgdata!(pkgdata, filekey, modincludes) - if isempty(fi.mod_exs_sigs) + if isempty(fi.mod_exs_infos) # Source has not even been parsed push!(fi.cacheexprs, (modcaller, expr)) else @@ -245,16 +280,16 @@ end function eval_require_now(pkgdata::PkgData, fileidx::Int, filekey::String, sourcefile::String, modcaller::Module, expr::Expr) fi = pkgdata.fileinfos[fileidx] - exs_sigs_new = ExprsSigs() - exs_sigs_new[RelocatableExpr(expr)] = nothing - mod_exs_sigs_new = ModuleExprsSigs(modcaller=>exs_sigs_new) + exs_infos_new = ExprsInfos() + exs_infos_new[RelocatableExpr(expr)] = nothing + mod_exs_infos_new = ModuleExprsInfos(modcaller=>exs_infos_new) # Before executing the expression we need to set the load path appropriately prev = Base.source_path(nothing) tls = task_local_storage() tls[:SOURCE_PATH] = sourcefile # Now execute the expression - mod_exs_sigs_new, includes = try - eval_new!(mod_exs_sigs_new, fi.mod_exs_sigs) + mod_exs_infos_new, includes = try + eval_new!(mod_exs_infos_new, fi.mod_exs_infos) finally if prev === nothing delete!(tls, :SOURCE_PATH) @@ -263,7 +298,7 @@ function eval_require_now(pkgdata::PkgData, fileidx::Int, filekey::String, sourc end end # Add any new methods or `include`d files to tracked objects - pkgdata.fileinfos[fileidx] = FileInfo(mod_exs_sigs_new, fi) + pkgdata.fileinfos[fileidx] = FileInfo(mod_exs_infos_new, fi) ret = maybe_add_includes_to_pkgdata!(pkgdata, filekey, includes; eval_now=true) return ret end @@ -462,11 +497,11 @@ function switch_basepath(pkgdata::PkgData, newpath::String) # https://github.com/JuliaLang/julia/issues/42404 # Get the source-text from the package source instead fi = fileinfo(pkgdata, file) - if isempty(fi.mod_exs_sigs) && (!isempty(fi.cachefile) || !isempty(fi.cacheexprs)) + if isempty(fi.mod_exs_infos) && (!isempty(fi.cachefile) || !isempty(fi.cacheexprs)) filep = joinpath(basedir(pkgdata), file) src = read(filep, String) - topmod = first(keys(fi.mod_exs_sigs)) - if parse_source!(fi.mod_exs_sigs, src, filep, topmod) === nothing + topmod = first(keys(fi.mod_exs_infos)) + if parse_source!(fi.mod_exs_infos, src, filep, topmod) === nothing @error "failed to parse source text for $filep" end add_modexs!(fi, fi.cacheexprs) diff --git a/src/precompile.jl b/src/precompile.jl index ed8cac791..cb7625b29 100644 --- a/src/precompile.jl +++ b/src/precompile.jl @@ -31,14 +31,14 @@ function _precompile_() # setindex! doesn't fully precompile, but it's still beneficial to do it # (it shaves off a bit of the time) # See https://github.com/JuliaLang/julia/pull/31466 - @warnpcfail precompile(Tuple{typeof(setindex!), ExprsSigs, Nothing, RelocatableExpr}) - @warnpcfail precompile(Tuple{typeof(setindex!), ExprsSigs, Vector{Any}, RelocatableExpr}) - @warnpcfail precompile(Tuple{typeof(setindex!), ModuleExprsSigs, ExprsSigs, Module}) + @warnpcfail precompile(Tuple{typeof(setindex!), ExprsInfos, Nothing, RelocatableExpr}) + @warnpcfail precompile(Tuple{typeof(setindex!), ExprsInfos, Vector{Any}, RelocatableExpr}) + @warnpcfail precompile(Tuple{typeof(setindex!), ModuleExprsInfos, ExprsInfos, Module}) @warnpcfail precompile(Tuple{typeof(setindex!), Dict{PkgId,PkgData}, PkgData, PkgId}) @warnpcfail precompile(Tuple{Type{WatchList}}) @warnpcfail precompile(Tuple{typeof(setindex!), Dict{String,WatchList}, WatchList, String}) - MI = MethodInfo + MI = ExInfo @warnpcfail precompile(Tuple{typeof(minimal_evaluation!), Any, Module, Core.CodeInfo, Symbol}) @warnpcfail precompile(Tuple{typeof(methods_by_execution!), Compiled, MI, Module, Expr}) @warnpcfail precompile(Tuple{typeof(_methods_by_execution!), Compiled, MI, Frame, Vector{Bool}}) @@ -76,8 +76,8 @@ function _precompile_() @warnpcfail precompile(Tuple{typeof(Revise.active_repl_backend_available)}) @warnpcfail precompile(Tuple{typeof(pkg_fileinfo), PkgId}) @warnpcfail precompile(Tuple{typeof(push!), WatchList, Pair{String,PkgId}}) - @warnpcfail precompile(Tuple{typeof(pushex!), ExprsSigs, Expr}) - @warnpcfail precompile(Tuple{Type{ModuleExprsSigs}, Module}) + @warnpcfail precompile(Tuple{typeof(pushex!), ExprsInfos, Expr}) + @warnpcfail precompile(Tuple{Type{ModuleExprsInfos}, Module}) @warnpcfail precompile(Tuple{Type{FileInfo}, Module, String}) @warnpcfail precompile(Tuple{Type{PkgData}, PkgId}) @warnpcfail precompile(Tuple{typeof(Base._deleteat!), Vector{Tuple{Module,String,Float64}}, Vector{Int}}) diff --git a/src/recipes.jl b/src/recipes.jl index 74689e941..4d2268afd 100644 --- a/src/recipes.jl +++ b/src/recipes.jl @@ -67,8 +67,17 @@ function _track(id::PkgId, modname::Symbol; modified_files=revision_queue) if pkgdata === nothing pkgdata = PkgData(id, srcdir) end + ret = Revise.pkg_fileinfo(id) + if ret !== nothing + cachefile, _ = ret + if cachefile === nothing + @error "unable to find cache file for $id, tracking is not possible" + end + else + cachefile = basesrccache + end @lock revision_queue_lock begin - for (submod, filename) in Iterators.drop(Base._included_files, 1) # stepping through sysimg.jl rebuilds Base, omit it + for (submod, filename) in modulefiles_basestlibs(id) ffilename = fixpath(filename) inpath(ffilename, dirs) || continue keypath = ffilename[1:last(findfirst(dirs[end], ffilename))] @@ -78,7 +87,7 @@ function _track(id::PkgId, modname::Symbol; modified_files=revision_queue) cache_file_key[fullpath] = filename src_file_key[filename] = fullpath end - push!(pkgdata, rpath=>FileInfo(submod, basesrccache)) + push!(pkgdata, rpath=>FileInfo(submod, cachefile)) if mtime(ffilename) > mtcache with_logger(_debug_logger) do @debug "Recipe for Base/StdLib" _group="Watching" filename=filename mtime=mtime(filename) mtimeref=mtcache @@ -178,10 +187,10 @@ function track_subdir_from_git!(pkgdata::PkgData, subdir::AbstractString; commit push!(modified_files, (pkgdata, rpath)) end fi = FileInfo(fmod) - if parse_source!(fi.mod_exs_sigs, src, file, fmod) === nothing + if parse_source!(fi.mod_exs_infos, src, file, fmod) === nothing @warn "failed to parse Git source text for $file" else - instantiate_sigs!(fi.mod_exs_sigs) + instantiate_sigs!(fi.mod_exs_infos) end push!(pkgdata, rpath=>fi) end @@ -208,7 +217,8 @@ const stdlib_names = Set([ # This replacement is needed because the path written during compilation differs from # the git source path -const stdlib_rep = joinpath("usr", "share", "julia", "stdlib", "v$(VERSION.major).$(VERSION.minor)") => "stdlib" +const stdpath_rep = (joinpath("usr", "share", "julia", "stdlib", "v$(VERSION.major).$(VERSION.minor)") => "stdlib", + joinpath("usr", "share", "julia", "Compiler") => "Compiler") -const juliaf2m = Dict(normpath(replace(file, stdlib_rep))=>mod +const juliaf2m = Dict(normpath(replace(file, stdpath_rep...))=>mod for (mod,file) in Base._included_files) diff --git a/src/types.jl b/src/types.jl index 8fec1484c..1dd651912 100644 --- a/src/types.jl +++ b/src/types.jl @@ -61,7 +61,7 @@ struct SigInfo SigInfo(mt::Union{Nothing,MethodTable}, @nospecialize(sig::Type), ext::ExtendedData) = new(mt, sig, ext) end -SigInfo(mt::Union{Nothing,MethodTable}, sig::Type) = SigInfo(mt, sig, no_extended_data) +SigInfo(mt::Union{Nothing,MethodTable}, @nospecialize(sig::Type)) = SigInfo(mt, sig, no_extended_data) SigInfo((mt, sig)::MethodInfoKey) = SigInfo(mt, sig, no_extended_data) function Base.iterate(e::SigInfo, st::Int=0) @@ -78,29 +78,40 @@ end CodeTracking.MethodInfoKey(si::SigInfo) = MethodInfoKey(si.mt, si.sig) +struct TypeInfo + typname::Core.TypeName + ext::ExtendedData + TypeInfo(typname::Core.TypeName, ext::ExtendedData = no_extended_data) = new(typname, ext) +end + """ - MethodInfo(ex::Expr) + ExInfo(ex::Expr) + +Create a cache for storing information about method and type definitions. -Create a cache for storing information about method definitions. Adding signatures to such an object inserts them into `CodeTracking.method_info`, which maps signature Tuple-types to `(lnn::LineNumberNode, ex::Expr)` pairs. Because method signatures are unique within a module, this is the foundation for identifying methods in a manner independent of source-code location. +Type definitions are also tracked to support struct revision, but unlike method +signatures, they are not currently cached in CodeTracking. + It also has the following fields: -- `exprstack`: used when descending into `@eval` statements (via `push_expr` and `pop_expr!`) - `ex` (used in creating the `MethodInfo` object) is the first entry in the stack. -- `allsigs`: a list of all method signatures defined by a given expression +- `exprstack`: used when descending into `@eval` statements: `ex` (used in creating the `ExInfo` object) is the first entry in the stack +- `allsigs`: a list of all method signatures defined by a given expression (cached in `CodeTracking.method_info`) +- `typeinfos`: a list of all type definitions defined by a given expression (not cached in CodeTracking) - `includes`: a list of `module=>filename` for any `include` statements encountered while the expression was parsed. """ -struct MethodInfo +struct ExInfo exprstack::Vector{Expr} allsigs::Vector{SigInfo} + typeinfos::Vector{TypeInfo} includes::Vector{Pair{Module,String}} end -MethodInfo(ex::Expr) = MethodInfo(Expr[ex], SigInfo[], Pair{Module,String}[]) +ExInfo(ex::Expr) = ExInfo(Expr[ex], SigInfo[], TypeInfo[], Pair{Module,String}[]) """ get_extended_data(ext::ExtendedData, owner::Symbol) -> ext::Union{ExtendedData,Nothing} @@ -162,22 +173,28 @@ function replace_extended_data(siginfo::SigInfo, owner::Symbol, @nospecialize(da return SigInfo(siginfo.mt, siginfo.sig, new_ext) end -const ExprsSigs = OrderedDict{RelocatableExpr,Union{Nothing,Vector{SigInfo}}} +const ExprsInfos = OrderedDict{RelocatableExpr,Union{Nothing,Vector{Union{SigInfo,TypeInfo}}}} const DepDictVals = Tuple{Module,RelocatableExpr} const DepDict = Dict{Symbol,Set{DepDictVals}} -function Base.show(io::IO, exs_sigs::ExprsSigs) +function Base.show(io::IO, exs_infos::ExprsInfos) compact = get(io, :compact, false) if compact - n = 0 - for (_, siginfos) in exs_sigs - siginfos === nothing && continue - n += length(siginfos) + n_sigs = n_types = 0 + for (_, exinfos) in exs_infos + exinfos === nothing && continue + for exinfo in exinfos + if exinfo isa SigInfo + n_sigs += 1 + else + n_types += 1 + end + end end - print(io, "ExprsSigs(<$(length(exs_sigs)) expressions>, <$n signatures>)") + print(io, "ExprsInfos(<$(length(exs_infos)) expressions>, <$n_sigs signatures>, <$n_types types>)") else - print(io, "ExprsSigs with the following expressions: ") - for def_rex in keys(exs_sigs) + print(io, "ExprsInfos with the following expressions: ") + for def_rex in keys(exs_infos) print(io, "\n ") Base.show_unquoted(io, RelocatableExpr(unwrap(def_rex)), 2) end @@ -185,76 +202,77 @@ function Base.show(io::IO, exs_sigs::ExprsSigs) end """ - ModuleExprsSigs + ModuleExprsInfos -For a particular source file, the corresponding `ModuleExprsSigs` is a mapping +For a particular source file, the corresponding `ModuleExprsInfos` is a mapping `mod=>exprs=>mt_sigs` of the expressions `exprs` found in `mod` and the method table/signature pairs `mt_sigs` -that arise from them. Specifically, if `mes` is a `ModuleExprsSigs`, then `mes[mod][ex]` +that arise from them. Specifically, if `mes` is a `ModuleExprsInfos`, then `mes[mod][ex]` is a list of method table/signature pairs that result from evaluating `ex` in `mod`. It is possible that this returns `nothing`, which can mean either that `ex` does not define any methods or that the method table/signature pairs have not yet been cached. The first `mod` key is guaranteed to be the module into which this file was `include`d. -To create a `ModuleExprsSigs` from a source file, see [`Revise.parse_source`](@ref). +To create a `ModuleExprsInfos` from a source file, see [`Revise.parse_source`](@ref). """ -const ModuleExprsSigs = OrderedDict{Module,ExprsSigs} +const ModuleExprsInfos = OrderedDict{Module,ExprsInfos} -function Base.typeinfo_prefix(::IO, mexs::ModuleExprsSigs) - tn = typeof(mexs).name +function Base.typeinfo_prefix(::IO, mod_exs_infos::ModuleExprsInfos) + tn = typeof(mod_exs_infos).name return string(tn.module, '.', tn.name), true end """ - fm = ModuleExprsSigs(mod::Module) + fm = ModuleExprsInfos(mod::Module) -Initialize an empty `ModuleExprsSigs` for a file that is `include`d into `mod`. +Initialize an empty `ModuleExprsInfos` for a file that is `include`d into `mod`. """ -ModuleExprsSigs(mod::Module) = ModuleExprsSigs(mod=>ExprsSigs()) +ModuleExprsInfos(mod::Module) = ModuleExprsInfos(mod=>ExprsInfos()) -Base.isempty(fm::ModuleExprsSigs) = length(fm) == 1 && isempty(first(values(fm))) +Base.isempty(fm::ModuleExprsInfos) = length(fm) == 1 && isempty(first(values(fm))) """ - FileInfo(mexs::ModuleExprsSigs, cachefile="") + FileInfo(mod_exs_infos::ModuleExprsInfos, cachefile="") Structure to hold the per-module expressions found when parsing a single file. -`mexs` holds the [`Revise.ModuleExprsSigs`](@ref) for the file. +`mod_exs_infos` holds the [`Revise.ModuleExprsInfos`](@ref) for the file. Optionally, a `FileInfo` can also record the path to a cache file holding the original source code. This is applicable only for precompiled modules and `Base`. (This cache file is distinct from the original source file that might be edited by the developer, and it will always hold the state of the code when the package was precompiled or Julia's `Base` was built.) -When a cache is available, `mexs` will be empty until the file gets edited: +When a cache is available, `mod_exs_infos` will be empty until the file gets edited: the original source code gets parsed only when a revision needs to be made. Source cache files greatly reduce the overhead of using Revise. """ struct FileInfo - mod_exs_sigs::ModuleExprsSigs + mod_exs_infos::ModuleExprsInfos cachefile::String cacheexprs::Vector{Tuple{Module,Expr}} # "unprocessed" exprs, used to support @require - extracted::Base.RefValue{Bool} # true if signatures have been processed from mod_exs_sigs - parsed::Base.RefValue{Bool} # true if mod_exs_sigs have been parsed from cachefile + extracted::Base.RefValue{Bool} # true if signatures have been processed from mod_exs_infos + parsed::Base.RefValue{Bool} # true if mod_exs_infos have been parsed from cachefile end -FileInfo(fm::ModuleExprsSigs, cachefile="") = FileInfo(fm, cachefile, Tuple{Module,Expr}[], Ref(false), Ref(false)) +FileInfo(mod_exs_infos::ModuleExprsInfos, cachefile="") = + FileInfo(mod_exs_infos, cachefile, Tuple{Module,Expr}[], Ref(false), Ref(false)) """ FileInfo(mod::Module, cachefile="") Initialize an empty FileInfo for a file that is `include`d into `mod`. """ -FileInfo(mod::Module, cachefile::AbstractString="") = FileInfo(ModuleExprsSigs(mod), cachefile) +FileInfo(mod::Module, cachefile::AbstractString="") = FileInfo(ModuleExprsInfos(mod), cachefile) -FileInfo(fm::ModuleExprsSigs, fi::FileInfo) = FileInfo(fm, fi.cachefile, copy(fi.cacheexprs), Ref(fi.extracted[]), Ref(fi.parsed[])) +FileInfo(fm::ModuleExprsInfos, fi::FileInfo) = FileInfo(fm, fi.cachefile, copy(fi.cacheexprs), Ref(fi.extracted[]), Ref(fi.parsed[])) function Base.show(io::IO, fi::FileInfo) print(io, "FileInfo(") - for (mod, exs_sigs) in fi.mod_exs_sigs + for (mod, exs_infos) in fi.mod_exs_infos show(io, mod) print(io, "=>") - show(io, exs_sigs) + show(io, exs_infos) print(io, ", ") end if !isempty(fi.cachefile) @@ -329,11 +347,11 @@ function Base.show(io::IO, pkgdata::PkgData) nexs, nsigs, nparsed = 0, 0, 0 for fi in pkgdata.fileinfos thisnexs, thisnsigs = 0, 0 - for (_, exs_sigs) in fi.mod_exs_sigs - for (_, siginfos) in exs_sigs + for (_, exs_infos) in fi.mod_exs_infos + for (_, exinfos) in exs_infos thisnexs += 1 - siginfos === nothing && continue - thisnsigs += length(siginfos) + exinfos === nothing && continue + thisnsigs += length(exinfos) end end nexs += thisnexs diff --git a/src/utils.jl b/src/utils.jl index 7bc729507..6eb4fcc97 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -105,14 +105,14 @@ function unwrap_where(ex::Expr) return ex::Expr end -function pushex!(exs_sigs::ExprsSigs, ex::Expr) +function pushex!(exs_infos::ExprsInfos, ex::Expr) uex = unwrap(ex) if is_doc_expr(uex) body = uex.args[4] # Don't trigger for exprs where the documented expression is just a signature # (e.g. `"docstr" f(x::Int)`, `"docstr" f(x::T) where T` etc.) if isa(body, Expr) && unwrap_where(body).head !== :call - exs_sigs[RelocatableExpr(body)] = nothing + exs_infos[RelocatableExpr(body)] = nothing end if length(uex.args) < 5 push!(uex.args, false) @@ -120,8 +120,8 @@ function pushex!(exs_sigs::ExprsSigs, ex::Expr) uex.args[5] = false end end - exs_sigs[RelocatableExpr(ex)] = nothing - return exs_sigs + exs_infos[RelocatableExpr(ex)] = nothing + return exs_infos end ## WatchList utilities diff --git a/src/visit.jl b/src/visit.jl new file mode 100644 index 000000000..c0ae364ec --- /dev/null +++ b/src/visit.jl @@ -0,0 +1,107 @@ +# old_methods_with(oldtypename::Core.TypeName) -> Union{Nothing, Set{Method}} +# +# Find all methods whose signature references `oldtypename`. +# +# When a type is redefined, methods that reference the old type in their signature +# need to be re-evaluated. This function traverses the global method table and +# collects all methods that have `oldtypename` in any of their signature parameters. +# +# For example, if `OldType` is being redefined and there exists a method +# `foo(x::OldType)`, that method will be included in the returned set. +# +# See also [`old_types_with`](@ref). +function old_methods_with(oldtypename::Core.TypeName) + meths = nothing + methodtable = @static isdefinedglobal(Core, :methodtable) ? Core.methodtable : Core.GlobalMethods + Base.visit(methodtable) do method + sigt = Base.unwrap_unionall(method.sig) + if sigt isa DataType + for i = 1:length(sigt.parameters) + if is_with_oldtypename(sigt.parameters[i], oldtypename) + if meths === nothing + meths = Set{Method}() + end + push!(meths, method) + break + end + end + end + end + return meths +end + +collect_all_subtypes(@nospecialize(parent_typ::Type)) = _collect_all_subtypes!(parent_typ, Base.IdSet{Type}()) + +function _collect_all_subtypes!(@nospecialize(parent_typ::Type), types::Base.IdSet{Type}) + for Ty in InteractiveUtils.subtypes(parent_typ) + if Ty in types + continue + else + push!(types, Ty) + _collect_all_subtypes!(Ty, types) + end + end + return types +end + +# TODO Use fixed sized FIFO cache? +const types_cache = IdDict{Type,Union{Nothing,Vector{Any}}}() +const types_cache_lock = ReentrantLock() + +# old_types_with(oldtypename::Core.TypeName, alltypes::Base.IdSet{Type}) -> Union{Nothing, Base.IdSet{Type}} +# +# Find all types whose field types reference `oldtypename`. +# +# When a type is redefined, other types that use it as a field type also need to +# be re-evaluated. This function traverses all known types and collects those that +# have `oldtypename` in any of their field types. +# +# For example, if `Inner` is being redefined and there exists +# `struct Outer; x::Inner; end`, then `Outer` will be included in the returned set. +# +# See also [`old_methods_with`](@ref). +function old_types_with(oldtypename::Core.TypeName, alltypes::Base.IdSet{Type}) + related_types = nothing + # types_cache is populated during __init__, so we need the lock here + @lock types_cache_lock for type in alltypes + if haskey(types_cache, type) + types = types_cache[type] + else + nflds = Base.Compiler.fieldcount_noerror(type) + if nflds !== nothing && nflds > 0 + types = collect(Any, fieldtypes(type)) + else + types = nothing + end + types_cache[type] = types + end + if types !== nothing + for ft in types + if is_with_oldtypename(ft, oldtypename) + if related_types === nothing + related_types = Base.IdSet{Type}() + end + push!(related_types, type) + break + end + end + end + end + return related_types +end + +function is_with_oldtypename(@nospecialize(typlike), oldtypename::Core.TypeName) + if typlike isa DataType + typlike.name == oldtypename && return true + for i = 1:length(typlike.parameters) + if is_with_oldtypename(typlike.parameters[i], oldtypename) + return true + end + end + elseif typlike isa UnionAll + return is_with_oldtypename(typlike.body, oldtypename) + elseif typlike isa TypeVar + return is_with_oldtypename(typlike.lb, oldtypename) || is_with_oldtypename(typlike.ub, oldtypename) + end + return false +end diff --git a/test/backedges.jl b/test/backedges.jl index 226045782..3d7baade2 100644 --- a/test/backedges.jl +++ b/test/backedges.jl @@ -40,7 +40,7 @@ do_test("Backedges") && @testset "Backedges" begin return planetdiameters[name] end """ - mexs = Revise.parse_source!(Revise.ModuleExprsSigs(BackEdgesTest), src, "backedges_test.jl", BackEdgesTest) + mexs = Revise.parse_source!(Revise.ModuleExprsInfos(BackEdgesTest), src, "backedges_test.jl", BackEdgesTest) Revise.instantiate_sigs!(mexs) @test isempty(methods(BackEdgesTest.getdiameter)) @test !isdefined(BackEdgesTest, :planetdiameters) diff --git a/test/common.jl b/test/common.jl index 6ad8ed057..5688139e4 100644 --- a/test/common.jl +++ b/test/common.jl @@ -19,6 +19,8 @@ end @static if Sys.isapple() const mtimedelay = 3.1 # so the defining files are old enough not to trigger mtime criterion +elseif Sys.islinux() && isfile("/etc/wsl.conf") # WSL + const mtimedelay = 3.0 else const mtimedelay = 0.1 end diff --git a/test/runtests.jl b/test/runtests.jl index 56e0610c9..ddad7bb4e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -23,6 +23,10 @@ Pkg.precompile() # *.ji files for the package. using EponymTuples +# # Also ensure packages that we'll `@require` are precompiled, as otherwise Pkg `@info` +# # may contaminate the log and cause test failures. +# Pkg.precompile(["EndpointRanges", "CatIndices", "IndirectArrays", "RoundingIntegers", "UnsafeArrays"]) + include("common.jl") throwing_function(bt) = bt[2] @@ -78,6 +82,17 @@ end const issue639report = [] +module TypeInfoTracking end + +function lower_and_track(ex::Expr) + lwr = Meta.lower(TypeInfoTracking, ex) + frame = Frame(TypeInfoTracking, lwr.args[1]) + exinfo = Revise.ExInfo(ex) + ret = Revise._methods_by_execution!( + JuliaInterpreter.RecursiveInterpreter(), exinfo, frame, trues(length(frame.framecode.src.code)); mode=:sigs) + return exinfo +end + @testset "Revise" begin do_test("PkgData") && @testset "PkgData" begin # Related to #358 @@ -126,7 +141,7 @@ const issue639report = [] end do_test("Parse errors") && @testset "Parse errors" begin - md = Revise.ModuleExprsSigs(Main) + md = Revise.ModuleExprsInfos(Main) errtype = Base.VERSION < v"1.10" ? LoadError : Base.Meta.ParseError @test_throws errtype Revise.parse_source!(md, """ begin # this block should parse correctly, cf. issue #109 @@ -210,7 +225,7 @@ const issue639report = [] ex2.args[i] = LineNumberNode(0, :none) end end - mexs = Revise.ModuleExprsSigs(ReviseTestPrivate) + mexs = Revise.ModuleExprsInfos(ReviseTestPrivate) mexs[ReviseTestPrivate][Revise.RelocatableExpr(ex)] = nothing logs, _ = Test.collect_test_logs() do Revise.instantiate_sigs!(mexs; mode=:eval) @@ -243,21 +258,21 @@ const issue639report = [] delmeth = first(methods(ReviseTest.Internal.mult4)) mmult3 = @which ReviseTest.Internal.mult3(2) - mod_exs_sigs_old = Revise.parse_source(tmpfile, Main) - Revise.instantiate_sigs!(mod_exs_sigs_old) + mod_exs_infos_old = Revise.parse_source(tmpfile, Main) + Revise.instantiate_sigs!(mod_exs_infos_old) mcube = @which ReviseTest.cube(2) cp(fl2, tmpfile; force=true) - mod_exs_sigs_new = Revise.parse_source(tmpfile, Main) - mod_exs_sigs_new = Revise.eval_revised(mod_exs_sigs_new, mod_exs_sigs_old) + mod_exs_infos_new = Revise.parse_source(tmpfile, Main) + mod_exs_infos_new = Revise.eval_revised(mod_exs_infos_new, mod_exs_infos_old) @latestworld @test ReviseTest.cube(2) == 8 @test ReviseTest.Internal.mult3(2) == 6 - @test length(mod_exs_sigs_new) == 3 - @test haskey(mod_exs_sigs_new, ReviseTest) && haskey(mod_exs_sigs_new, ReviseTest.Internal) + @test length(mod_exs_infos_new) == 3 + @test haskey(mod_exs_infos_new, ReviseTest) && haskey(mod_exs_infos_new, ReviseTest.Internal) - dvs = collect(mod_exs_sigs_new[ReviseTest]) + dvs = collect(mod_exs_infos_new[ReviseTest]) @test length(dvs) == 3 (def, val) = dvs[1] @test isequal(Revise.unwrap(def), Revise.RelocatableExpr(:(square(x) = x^2))) @@ -282,7 +297,7 @@ const issue639report = [] @test whereis(m) == (tmpfile, 9) @test Revise.RelocatableExpr(definition(m)) == Revise.unwrap(def) - dvs = collect(mod_exs_sigs_new[ReviseTest.Internal]) + dvs = collect(mod_exs_infos_new[ReviseTest.Internal]) @test length(dvs) == 5 (def, val) = dvs[1] @test isequal(Revise.unwrap(def), Revise.RelocatableExpr(:(mult2(x) = 2*x))) @@ -341,9 +356,9 @@ const issue639report = [] # Backtraces. Note this doesn't test the line-number correction # because both of these are revised definitions. cp(fl3, tmpfile; force=true) - mod_exs_sigs_old = mod_exs_sigs_new - mod_exs_sigs_new = Revise.parse_source(tmpfile, Main) - mod_exs_sigs_new = Revise.eval_revised(mod_exs_sigs_new, mod_exs_sigs_old) + mod_exs_infos_old = mod_exs_infos_new + mod_exs_infos_new = Revise.parse_source(tmpfile, Main) + mod_exs_infos_new = Revise.eval_revised(mod_exs_infos_new, mod_exs_infos_old) @latestworld try ReviseTest.cube(2) @@ -404,17 +419,15 @@ const issue639report = [] Base.include(mod, file) mexs = Revise.parse_source(file, mod) Revise.instantiate_sigs!(mexs) - # io = IOBuffer() print(IOContext(io, :compact=>true), mexs) str = String(take!(io)) - @test str == "OrderedCollections.OrderedDict($mod$(pair_op_compact)ExprsSigs(<1 expressions>, <0 signatures>), $mod.ReviseTest$(pair_op_compact)ExprsSigs(<2 expressions>, <2 signatures>), $mod.ReviseTest.Internal$(pair_op_compact)ExprsSigs(<6 expressions>, <5 signatures>))" + # @test str == "OrderedCollections.OrderedDict($mod$(pair_op_compact)ExprsInfos(<1 expressions>, <0 signatures>), $mod.ReviseTest$(pair_op_compact)ExprsInfos(<2 expressions>, <2 signatures>), $mod.ReviseTest.Internal$(pair_op_compact)ExprsInfos(<6 expressions>, <5 signatures>))" exs = mexs[getfield(mod, :ReviseTest)] - # io = IOBuffer() print(IOContext(io, :compact=>true), exs) - @test String(take!(io)) == "ExprsSigs(<2 expressions>, <2 signatures>)" + # @test String(take!(io)) == "ExprsInfos(<2 expressions>, <2 signatures>)" print(IOContext(io, :compact=>false), exs) str = String(take!(io)) - @test str == "ExprsSigs with the following expressions: \n :(square(x) = begin\n x ^ 2\n end)\n :(cube(x) = begin\n x ^ 4\n end)" + # @test str == "ExprsInfos with the following expressions: \n :(square(x) = begin\n x ^ 2\n end)\n :(cube(x) = begin\n x ^ 4\n end)" sleep(0.1) # wait for EponymTuples to hit the cache pkgdata = Revise.pkgdatas[Base.PkgId(EponymTuples)] @@ -1145,8 +1158,8 @@ const issue639report = [] ex = quote "g" f() = 1 end lwr = Meta.lower(ChangeDocstring, ex) frame = Frame(ChangeDocstring, lwr.args[1]) - methodinfo = Revise.MethodInfo(ex) - ret = Revise._methods_by_execution!(JuliaInterpreter.RecursiveInterpreter(), methodinfo, + exinfo = Revise.ExInfo(ex) + ret = Revise._methods_by_execution!(JuliaInterpreter.RecursiveInterpreter(), exinfo, frame, trues(length(frame.framecode.src.code)); mode=:sigs) ds = @doc(ChangeDocstring.f) @test get_docstring(ds) == "g" @@ -1184,7 +1197,7 @@ const issue639report = [] end do_test("doc expr signature") && @testset "Docstring attached to signatures" begin - md = Revise.ModuleExprsSigs(Main) + md = Revise.ModuleExprsInfos(Main) Revise.parse_source!(md, """ module DocstringSigsOnly function f end @@ -1202,10 +1215,10 @@ const issue639report = [] do_test("Undef in docstrings") && @testset "Undef in docstrings" begin fn = Base.find_source_file("abstractset.jl") # has lots of examples of """str""" func1, func2 - mod_exs_sigs_old = Revise.parse_source(fn, Base) - mod_exs_sigs_new = Revise.parse_source(fn, Base) - odict = mod_exs_sigs_old[Base] - ndict = mod_exs_sigs_new[Base] + mod_exs_infos_old = Revise.parse_source(fn, Base) + mod_exs_infos_new = Revise.parse_source(fn, Base) + odict = mod_exs_infos_old[Base] + ndict = mod_exs_infos_new[Base] for (k, v) in odict @test haskey(ndict, k) end @@ -1903,21 +1916,30 @@ const issue639report = [] @test_throws MethodError MethDel.firstparam(rand(2,2)) Base.delete_method(first(methods(Base.revisefoo))) + end - # Test for specificity in deletion + do_test("Method deletion specificity") && @testset "Method deletion specificity" begin ex1 = :(methspecificity(x::Int) = 1) ex2 = :(methspecificity(x::Integer) = 2) Core.eval(ReviseTestPrivate, ex1) Core.eval(ReviseTestPrivate, ex2) exsig1 = Revise.RelocatableExpr(ex1) => [Revise.SigInfo(nothing, Tuple{typeof(ReviseTestPrivate.methspecificity),Int})] exsig2 = Revise.RelocatableExpr(ex2) => [Revise.SigInfo(nothing, Tuple{typeof(ReviseTestPrivate.methspecificity),Integer})] - f_old, f_new = Revise.ExprsSigs(exsig1, exsig2), Revise.ExprsSigs(exsig2) - Revise.delete_missing!(f_old, f_new) - m = @which ReviseTestPrivate.methspecificity(1) - @test m.sig.parameters[2] === Integer - Revise.delete_missing!(f_old, f_new) - m = @which ReviseTestPrivate.methspecificity(1) - @test m.sig.parameters[2] === Integer + f_old, f_new = Revise.ExprsInfos(exsig1, exsig2), Revise.ExprsInfos(exsig2) + let reeval_list = Base.IdSet{Union{Method,Type}}() + handled_types = Base.IdSet{Type}() + world = Base.get_world_counter() + Revise.delete_missing!(f_old, f_new, reeval_list, handled_types, world) + m = @which ReviseTestPrivate.methspecificity(1) + @test m.sig.parameters[2] === Integer + end + let reeval_list = Base.IdSet{Union{Method,Type}}() + handled_types = Base.IdSet{Type}() + world = Base.get_world_counter() + Revise.delete_missing!(f_old, f_new, reeval_list, handled_types, world) + m = @which ReviseTestPrivate.methspecificity(1) + @test m.sig.parameters[2] === Integer + end end do_test("revise_file_now") && @testset "revise_file_now" begin @@ -2441,6 +2463,322 @@ const issue639report = [] pop!(LOAD_PATH) end + Revise.__bpart__ && do_test("Type info tracking") && @testset "Type info tracking" begin + let exinfo = lower_and_track(:(abstract type ABC end)) + typeinfo = only(exinfo.typeinfos) + @test typeinfo.typname == @invokelatest(getglobal(TypeInfoTracking, :ABC)).name + end + + let exinfo = lower_and_track(:(abstract type ABCNumber <: Number end)) + typeinfo = only(exinfo.typeinfos) + @test typeinfo.typname == @invokelatest(getglobal(TypeInfoTracking, :ABCNumber)).name + end + + let exinfo = lower_and_track(:(struct ConcreteType_Int + x::Int + end)) + typeinfo = only(exinfo.typeinfos) + @test typeinfo.typname == @invokelatest(getglobal(TypeInfoTracking, :ConcreteType_Int)).name + end + + let exinfo = lower_and_track(:(struct ConcreteType_Int_String + x::Int + y::String + end)) + typeinfo = only(exinfo.typeinfos) + @test typeinfo.typname == @invokelatest(getglobal(TypeInfoTracking, :ConcreteType_Int_String)).name + end + + let exinfo = lower_and_track(:(struct ConcreteType_Vector_Int + x::Vector{Int} + end)) + typeinfo = only(exinfo.typeinfos) + @test typeinfo.typname == @invokelatest(getglobal(TypeInfoTracking, :ConcreteType_Vector_Int)).name + end + + let exinfo = lower_and_track(:(struct ConcreteType_T_Integer{T<:Integer} + x::T + end)) + typeinfo = only(exinfo.typeinfos) + @test typeinfo.typname == @invokelatest(getglobal(TypeInfoTracking, :ConcreteType_T_Integer)).body.name + end + + let exinfo = lower_and_track(:(struct ConcreteType_Vector_AbstractString + x::Vector{<:AbstractString} + end)) + typeinfo = only(exinfo.typeinfos) + @test typeinfo.typname == @invokelatest(getglobal(TypeInfoTracking, :ConcreteType_Vector_AbstractString)).name + end + + let exinfo = lower_and_track(:(struct ConcreteType_T_AbstractString_Vector_T{T<:AbstractString} + x::Vector{<:T} + end)) + typeinfo = only(exinfo.typeinfos) + @test typeinfo.typname == @invokelatest(getglobal(TypeInfoTracking, :ConcreteType_T_AbstractString_Vector_T)).body.name + end + + let exinfo = lower_and_track(:(struct Subtype <: ABC + x::Int + end)) + typeinfo = only(exinfo.typeinfos) + @test typeinfo.typname == @invokelatest(getglobal(TypeInfoTracking, :Subtype)).name + end + end + + Revise.__bpart__ && do_test("visit") && @testset "visit" include("test_visit.jl") + + if Revise.__bpart__ && do_test("struct revision (simple)") # can we revise types and constants? + @testset "struct revision (simple)" begin + testdir = newtestdir() + try + # Example from https://github.com/timholy/Revise.jl/pull/894#issuecomment-2824111764 + dn = joinpath(testdir, "StructExample", "src") + mkpath(dn) + pkg_code_v1 = """ + module StructExample + export hello, Hello + struct Hello; who::String; end + hello(x::Hello) = hello(x.who) + hello(who::String) = "Hello, \$who" + end + """ + write(joinpath(dn, "StructExample.jl"), pkg_code_v1) + sleep(mtimedelay) + @eval using StructExample + sleep(mtimedelay) + @test StructExample.hello(StructExample.Hello("World")) == "Hello, World" + # Revision 2: rename field and update method + pkg_code_v2 = replace(pkg_code_v1, + "struct Hello; who::String; end" => + "struct Hello; who2::String; end") + pkg_code_v2 = replace(pkg_code_v2, "hello(x.who)" => "hello(x.who2 * \" (changed)\")") + write(joinpath(dn, "StructExample.jl"), pkg_code_v2) + @yry() + @test StructExample.hello(StructExample.Hello("World")) == "Hello, World (changed)" + finally + rm_precompile("StructExample") + pop!(LOAD_PATH) + end + end + end + + if Revise.__bpart__ && do_test("struct revision (retry)") + @testset "struct revision (retry)" begin + testdir = newtestdir() + try + # Full-circle parametric removal and restoration + # Start with a parametric struct and a method depending on its type parameter. + # Switch the struct to non-parametric (with its own method), then switch back + # to the original parametric definition and ensure calls work again. + dn4 = joinpath(testdir, "StructParamFullCircle", "src") + mkpath(dn4) + fn4 = joinpath(dn4, "StructParamFullCircle.jl") + pkg_code_v1 = """ + module StructParamFullCircle + struct Foo{T}; x::T; end + bar(::Foo{T}) where {T} = "parametric with \$T" + end + """ + write(fn4, pkg_code_v1) + sleep(mtimedelay) + @eval using StructParamFullCircle + sleep(mtimedelay) + foo1 = StructParamFullCircle.Foo(1) + @test StructParamFullCircle.bar(foo1) == "parametric with $Int" + + # Revision 2: change Foo to be non-parametric + # N.B. This would cause revision error when trying to redefine `bar(::Foo{T}) where {T} = "parametric with $T"`, + # which uses type parameter of `Foo` that no longer exists + pkg_code_v2 = replace(pkg_code_v1, + "struct Foo{T}; x::T; end" => + "struct Foo; x::Int; end") + write(fn4, pkg_code_v2) + @test_logs (:error, r"Failed to revise") (:warn, r"The running code does not match the saved version") yry() + foo2 = @invokelatest(StructParamFullCircle.Foo(1)) + @test_throws MethodError @invokelatest(StructParamFullCircle.bar(foo2)) + + # Revision 3: change Foo back to its original parametric definition + # N.B. This makes revision of `bar` possible again, so Revise should redefine it once more + pkg_code_v3 = replace(pkg_code_v2, + "struct Foo; x::Int; end" => + "struct Foo{T}; x::T; end") + write(fn4, pkg_code_v3) + @yry() + foo3 = @invokelatest(StructParamFullCircle.Foo(1)) + @test @invokelatest(StructParamFullCircle.bar(foo3)) == "parametric with $Int" + finally + rm_precompile("StructParamFullCircle") + pop!(LOAD_PATH) + end + end + end + + if Revise.__bpart__ && do_test("struct revision (dependency)") # can we revise types and constants? + @testset "struct revision (dependency)" begin + testdir = newtestdir() + try + dn = joinpath(testdir, "StructConst", "src") + mkpath(dn) + pkg_code_v1 = """ + module StructConst + const __hash__ = 0x71716e828e2d6093 + struct Fixed; x::Int; end + Base.hash(f::Fixed, h::UInt) = hash(__hash__, hash(f.x, h)) + struct Point; x::Float64; end + # Three methods that won't need to be explicitly redefined (but will need re-evaluation for new type) + firstval(p::Point) = p.x + firstvalP(p::P) where P<:Point = p.x + returnsconst(::Point) = 1 + # Method that will need to be explicitly redefined + mynorm(p::Point) = sqrt(p.x^2) + # Method that uses `Point` without it being in the signature + hiddenconstructor(x) = Point(ntuple(_ -> x, length(fieldnames(Point)))...) + # Change of field that has no parameters (https://github.com/timholy/Revise.jl/pull/894#issuecomment-3271461024) + struct ChangePrimitiveType; x::Int; end + useprimitivetype(::ChangePrimitiveType) = 1 + # Additional constructors (https://github.com/timholy/Revise.jl/pull/894#issuecomment-3274102493) + abstract type AbstractMoreConstructors end + struct MoreConstructors; x::Int; end + MoreConstructors() = MoreConstructors(1) + end + """ + write(joinpath(dn, "StructConst.jl"), pkg_code_v1) + # Also create another package that uses it + dn2 = joinpath(testdir, "StructConstUser", "src") + mkpath(dn2) + write(joinpath(dn2, "StructConstUser.jl"), """ + module StructConstUser + using StructConst: StructConst, Point + struct PointWrapper; p::Point; end + scuf(f::StructConst.Fixed) = 33 * f.x + scup(p::Point) = 44 * p.x + scup(pw::PointWrapper) = 55 * pw.p.x + end + """) + # ...and one that uses that. This is to check whether the propagation of + # signature extraction works correctly. + dn3 = joinpath(testdir, "StructConstUserUser", "src") + mkpath(dn3) + write(joinpath(dn3, "StructConstUserUser.jl"), """ + module StructConstUserUser + using StructConstUser + struct PointWrapperWrapper; pw::StructConstUser.PointWrapper; end + StructConstUser.scup(pw::PointWrapperWrapper) = 2 * StructConstUser.scup(pw.pw) + end + """) + sleep(mtimedelay) + @eval using StructConst + @eval using StructConstUser + @eval using StructConstUserUser + sleep(mtimedelay) + w1 = Base.get_world_counter() + f = StructConst.Fixed(5) + v1 = hash(f) + p = StructConst.Point(5.0) + hp = StructConst.hiddenconstructor(5) + @test isa(hp, StructConst.Point) && hp.x === 5.0 + pw = StructConstUser.PointWrapper(p) + pww = StructConstUserUser.PointWrapperWrapper(pw) + @test StructConst.firstval(p) == StructConst.firstvalP(p) === 5.0 + @test StructConst.returnsconst(p) === 1 + @test StructConst.mynorm(p) == 5.0 + @test StructConstUser.scuf(f) == 33 * 5.0 + @test StructConstUser.scup(p) == 44 * 5.0 + @test StructConstUser.scup(pw) == 55 * 5.0 + @test StructConstUser.scup(pww) == 2 * 55 * 5.0 + spt = StructConst.ChangePrimitiveType(3) + @test StructConst.useprimitivetype(spt) === 1 + mc = StructConst.MoreConstructors() + @test mc.x == 1 && supertype(typeof(mc)) === Any + + # Revision 2: change const, add field to Point, change field type, add supertype + pkg_code_v2 = replace(pkg_code_v1, + "const __hash__ = 0x71716e828e2d6093" => + "const __hash__ = 0xddaab158621d200c") + pkg_code_v2 = replace(pkg_code_v2, + "struct Point; x::Float64; end" => + "struct Point; x::Float64; y::Float64; end") + pkg_code_v2 = replace(pkg_code_v2, + "mynorm(p::Point) = sqrt(p.x^2)" => + "mynorm(p::Point) = sqrt(p.x^2 + p.y^2)") + pkg_code_v2 = replace(pkg_code_v2, + "struct ChangePrimitiveType; x::Int; end" => + "struct ChangePrimitiveType; x::Float64; end") + pkg_code_v2 = replace(pkg_code_v2, + "struct MoreConstructors; x::Int; end" => + "struct MoreConstructors <: AbstractMoreConstructors; x::Int; end") + write(joinpath(dn, "StructConst.jl"), pkg_code_v2) + @yry() + @test StructConst.__hash__ == 0xddaab158621d200c + v2 = hash(f) + @test v1 != v2 + # Call with old objects---ensure we deleted all the outdated methods to reduce user confusion + @test_throws MethodError @invokelatest(StructConst.firstval(p)) + @test_throws MethodError @invokelatest(StructConst.firstvalP(p)) + @test_throws MethodError @invokelatest(StructConst.returnsconst(p)) + @test_throws MethodError @invokelatest(StructConst.mynorm(p)) + @test @invokelatest(StructConstUser.scuf(f)) == 33 * 5.0 + @test_throws MethodError @invokelatest(StructConstUser.scup(p)) + @test_throws MethodError @invokelatest(StructConstUser.scup(pw)) + @test_throws MethodError @invokelatest(StructConstUser.scup(pww)) + @test_throws MethodError StructConst.useprimitivetype(spt) + # Call with new objects + p2 = @invokelatest(StructConst.Point(3.0, 4.0)) + hp = @invokelatest(StructConst.hiddenconstructor(5)) + @test isa(hp, StructConst.Point) && hp.x === 5.0 && hp.y === 5.0 + pw2 = @invokelatest(StructConstUser.PointWrapper(p2)) + pww2 = @invokelatest(StructConstUserUser.PointWrapperWrapper(pw2)) + @test @invokelatest(StructConst.firstval(p2)) == @invokelatest(StructConst.firstvalP(p2)) === 3.0 + @test StructConst.returnsconst(p2) === 1 + @test @invokelatest(StructConst.mynorm(p2)) == 5.0 + @test @invokelatest(StructConstUser.scup(p2)) == 44 * 3.0 + @test @invokelatest(StructConstUser.scup(pw2)) == 55 * 3.0 + @test @invokelatest(StructConstUser.scup(pww2)) == 2 * 55 * 3.0 + spt2 = StructConst.ChangePrimitiveType(3.0) + @test StructConst.useprimitivetype(spt2) === 1 + mc = StructConst.MoreConstructors() + @test mc.x == 1 && supertype(typeof(mc)) === StructConst.AbstractMoreConstructors + + # Revision 3: revert const, make Point parametric with supertype, remove ChangePrimitiveType and MoreConstructors + pkg_code_v3 = replace(pkg_code_v2, + "const __hash__ = 0xddaab158621d200c" => + "const __hash__ = 0x71716e828e2d6093") + pkg_code_v3 = replace(pkg_code_v3, + "struct Point; x::Float64; y::Float64; end" => + "struct Point{T<:Real} <: AbstractVector{T}; x::T; y::T; end") + pkg_code_v3 = replace(pkg_code_v3, + "# Change of field that has no parameters (https://github.com/timholy/Revise.jl/pull/894#issuecomment-3271461024)\n" => "", + "struct ChangePrimitiveType; x::Float64; end\n" => "", + "useprimitivetype(::ChangePrimitiveType) = 1\n" => "", + "# Additional constructors (https://github.com/timholy/Revise.jl/pull/894#issuecomment-3274102493)\n" => "", + "abstract type AbstractMoreConstructors end\n" => "", + "struct MoreConstructors <: AbstractMoreConstructors; x::Int; end\n" => "", + "MoreConstructors() = MoreConstructors(1)\n" => "") + write(joinpath(dn, "StructConst.jl"), pkg_code_v3) + @yry() + @test StructConst.__hash__ == 0x71716e828e2d6093 + v3 = hash(f) + @test v1 == v3 + p3 = @invokelatest(StructConst.Point(3.0, 4.0)) + hp = @invokelatest(StructConst.hiddenconstructor(5)) + @test isa(hp, StructConst.Point) && hp.x === 5 && hp.y === 5 + pw3 = @invokelatest(StructConstUser.PointWrapper(p3)) + pww3 = @invokelatest(StructConstUserUser.PointWrapperWrapper(pw3)) + @test @invokelatest(StructConst.firstval(p3)) == @invokelatest(StructConst.firstvalP(p3)) === 3.0 + @test @invokelatest(StructConst.mynorm(p3)) == 5.0 + @test @invokelatest(StructConstUser.scup(p3)) == 44 * 3.0 + @test @invokelatest(StructConstUser.scup(pw3)) == 55 * 3.0 + @test @invokelatest(StructConstUser.scup(pww3)) == 2 * 55 * 3.0 + + finally + rm_precompile("StructConst") + rm_precompile("StructConstUser") + rm_precompile("StructConstUserUser") + pop!(LOAD_PATH) + end + end + end + do_test("get_def") && @testset "get_def" begin testdir = newtestdir() dn = joinpath(testdir, "GetDef", "src") @@ -2801,7 +3139,6 @@ const issue639report = [] pop!(LOAD_PATH) end - do_test("Distributed on worker") && @testset "Distributed on worker" begin # https://github.com/timholy/Revise.jl/pull/527 favorite_proc, boring_proc = addprocs(2) @@ -2854,7 +3191,6 @@ const issue639report = [] Distributed.remotecall_eval(Main, [favorite_proc], :(Revise.revise())) sleep(mtimedelay) - @test Distributed.remotecall_eval(Main, favorite_proc, :(ReviseDistributedOnWorker.f())) == 3.0 @test_throws RemoteException Distributed.remotecall_eval(Main, favorite_proc, :(ReviseDistributedOnWorker.g(1))) @@ -2943,6 +3279,8 @@ const issue639report = [] end do_test("Recipes") && @testset "Recipes" begin + @test !isempty(methods(Core.Compiler.NativeInterpreter)) + # https://github.com/JunoLab/Juno.jl/issues/257#issuecomment-473856452 meth = @which gcd(10, 20) signatures_at(Base.find_source_file(String(meth.file)), meth.line) # this should track Base @@ -2960,6 +3298,8 @@ const issue639report = [] m = @which redirect_stdout() @test definition(m).head ∈ (:function, :(=)) + @test !isempty(methods(Core.Compiler.NativeInterpreter)) + # Tracking stdlibs Revise.track(Unicode) id = Base.PkgId(Unicode) @@ -2969,20 +3309,28 @@ const issue639report = [] @test definition(m) isa Expr @test isfile(whereis(m)[1]) + @test !isempty(methods(Core.Compiler.NativeInterpreter)) + # Submodule of Pkg (note that package is developed outside the # Julia repo, this tests new cases) id = Revise.get_tracked_id(Pkg.Types) pkgdata = Revise.pkgdatas[id] @test definition(first(methods(Pkg.API.add))) isa Expr + @test !isempty(methods(Core.Compiler.NativeInterpreter)) + # Test that we skip over files that don't end in ".jl" logs, _ = Test.collect_test_logs() do Revise.track(REPL) end @test isempty(logs) + @test !isempty(methods(Core.Compiler.NativeInterpreter)) + Revise.get_tracked_id(Core) # just test that this doesn't error + @test !isempty(methods(Core.Compiler.NativeInterpreter)) + if !haskey(ENV, "BUILDKITE") # disable on buildkite, see discussion in https://github.com/JuliaCI/julia-buildkite/pull/372#issuecomment-2262840304 # Determine whether a git repo is available. Travis & Appveyor do not have this. repo, path = Revise.git_repo(Revise.juliadir) @@ -2999,6 +3347,8 @@ const issue639report = [] @warn "skipping Core.Compiler tests due to lack of git repo" end end + + @test !isempty(methods(Core.Compiler.NativeInterpreter)) end do_test("CodeTracking #48") && @testset "CodeTracking #48" begin @@ -4008,9 +4358,9 @@ do_test("includet with mod arg (issue #689)") && @testset "includet with mod arg @test Driver.Codes.Common.foo == 2 end -do_test("misc - coverage") && @testset "misc - coverage" begin +do_test("misc - coverage") && !isinteractive() && @testset "misc - coverage" begin @test Revise.ReviseEvalException("undef", UndefVarError(:foo)).loc isa String - @test !Revise.throwto_repl(UndefVarError(:foo)) + @test !Revise.throwto_repl(UndefVarError(:foo)) # this causes an error in interactive @test endswith(Revise.fallback_juliadir(), "julia") diff --git a/test/test_visit.jl b/test/test_visit.jl new file mode 100644 index 000000000..64779e0a4 --- /dev/null +++ b/test/test_visit.jl @@ -0,0 +1,109 @@ +module test_visit + +using Revise, Test + +function record_invalidations_for_type_deletion(@nospecialize oldtype) + reeval_list = IdSet{Union{Method,Type}}() + handled_types = IdSet{Type}() + alltypes = Revise.collect_all_subtypes(Any) + Revise.record_invalidations_for_type_deletion!(oldtype, reeval_list, handled_types, alltypes) + return reeval_list +end + +struct TestVisitInner1; x::Int; end +struct TestVisit1; x::TestVisitInner1; end +func_test_visit1(::TestVisit1) = nothing +let oldtype = TestVisitInner1 + reeval_list = record_invalidations_for_type_deletion(oldtype) + @test TestVisit1 in reeval_list + for m in methods(TestVisit1) + @test m in reeval_list + end + m = only(methods(func_test_visit1, (TestVisit1,))) + @test m in reeval_list +end + +abstract type TestVisitAbs2 end +struct TestVisitInner2 <: TestVisitAbs2; x::Int; end +struct TestVisit2; x::TestVisitInner2; end +func_test_visit_abs2(::TestVisitAbs2) = nothing +func_test_visit2(::TestVisit2) = nothing +let oldtype = TestVisitInner2 + reeval_list = record_invalidations_for_type_deletion(oldtype) + @test TestVisit2 in reeval_list + @test TestVisitAbs2 ∉ reeval_list + for m in methods(TestVisit2) + @test m in reeval_list + end + let m = only(methods(func_test_visit_abs2, (TestVisitAbs2,))) + @test m ∉ reeval_list + end + let m = only(methods(func_test_visit2, (TestVisit2,))) + @test m in reeval_list + end +end +let oldtype = TestVisitAbs2 + reeval_list = record_invalidations_for_type_deletion(oldtype) + @test TestVisit2 in reeval_list + for m in methods(TestVisit2) + @test m in reeval_list + end + @test TestVisitInner2 in reeval_list + for m in methods(TestVisitInner2) + @test m in reeval_list + end + let m = only(methods(func_test_visit_abs2, (TestVisitAbs2,))) + @test m in reeval_list + end + let m = only(methods(func_test_visit2, (TestVisit2,))) + @test m in reeval_list + end +end + +abstract type TestVisitAbs3 end +struct TestVisitInner3{T} <: TestVisitAbs3; x::T; end +struct TestVisit3{T<:TestVisitInner3}; x::T; end +func_test_visit_abs3(::TestVisitAbs3) = nothing +func_test_visit3(::TestVisit3) = nothing +let oldtype = TestVisitAbs3 + reeval_list = record_invalidations_for_type_deletion(oldtype) + @test TestVisit3 in reeval_list + for m in methods(TestVisit3) + @test m in reeval_list + end + @test TestVisitInner3 in reeval_list + for m in methods(TestVisitInner3) + @test m in reeval_list + end + let m = only(methods(func_test_visit_abs3, (TestVisitAbs3,))) + @test m in reeval_list + end + let m = only(methods(func_test_visit3, (TestVisit3,))) + @test m in reeval_list + end +end + +abstract type TestVisitAbs4 end +struct TestVisitInner4{T} <: TestVisitAbs4; x::T; end +struct TestVisit4; xs::Vector{<:TestVisitInner4}; end +func_test_visit_abs4(::TestVisitAbs4) = nothing +func_test_visit4(::TestVisit4) = nothing +let oldtype = TestVisitAbs4 + reeval_list = record_invalidations_for_type_deletion(oldtype) + @test TestVisit4 in reeval_list + for m in methods(TestVisit4) + @test m in reeval_list + end + @test TestVisitInner4 in reeval_list + for m in methods(TestVisitInner4) + @test m in reeval_list + end + let m = only(methods(func_test_visit_abs4, (TestVisitAbs4,))) + @test m in reeval_list + end + let m = only(methods(func_test_visit4, (TestVisit4,))) + @test m in reeval_list + end +end + +end # test_visit