Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions docs/src/debugging.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions docs/src/dev_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Revise.user_callbacks_by_key

```@docs
Revise.RelocatableExpr
Revise.ModuleExprsSigs
Revise.ModuleExprsInfos
Revise.FileInfo
Revise.PkgData
Revise.WatchList
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/src/figures/diagram.tex
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
34 changes: 17 additions & 17 deletions docs/src/internals.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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, ...]`.
Expand All @@ -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
Expand Down Expand Up @@ -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.)
Expand All @@ -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)
Expand All @@ -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})]
Expand All @@ -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.
Expand Down
81 changes: 72 additions & 9 deletions docs/src/limitations.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -67,8 +130,7 @@ julia> write("src/MyPkg.jl","""
end

end
""")
234
""");

julia> FooStruct # make sure FooStruct refers to FooStruct2
MyPkg.FooStruct2
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down
16 changes: 16 additions & 0 deletions src/loading.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
21 changes: 18 additions & 3 deletions src/logging.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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()

"""
Expand All @@ -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,
Expand Down
Loading
Loading