[ANN] Revise.jl v3.13: Struct revision support

Happy New Year, everyone!:tada::pine_decoration:

A new year’s gift for you all: Revise.jl v3.13 with struct revision support.
Here’s what’s new in this release.

Struct Revision (Julia 1.12+)

Revise can now handle changes to struct definitions! When you modify a struct, Revise automatically re-evaluates the struct definition along with any methods or types that depend on it.

For example, if you have:

struct Inner
    value::Int
end

struct Outer
    inner::Inner
end

print_value(o::Outer) = println(o.inner.value)

And change Inner to:

struct Inner
    value::Float64
    name::String
end

Revise will redefine Inner, and also re-evaluate Outer (which uses Inner as a field type) and print_value (which references Outer in its signature).

See PR #894 for implementation details.

Limitations: 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:

MyVecType{T} = Vector{T}  # changing this to AbstractVector{T} won't update MyVec
struct MyVec{T}
    v::MyVecType{T}
end

If you change MyVecType{T} from Vector{T} to AbstractVector{T}, the struct MyVec will not be automatically re-evaluated because Revise does not track the dependency edge from MyVecType to MyVec. The same applies to const bindings and other global bindings that are referenced in type definitions.

This limitation actually also affects macros and generated functions—if you change a macro definition, the changes won’t propagate to code that has already expanded that macro.

Workaround: You can manually call revise(MyModule) to force re-evaluation of all definitions in MyModule, which will pick up the new bindings.

Supporting general binding revision would require tracking implicit binding edges across all top-level code, which involves significant interpreter enhancements. This is deferred to future work.


Other Changes

Added

  • Revise.dont_watch(pkg) and Revise.allow_watch(pkg) functions with Preferences.jl persistence. Direct modification of dont_watch_pkgs set is deprecated and will be removed in a future major version. (#976)

Changed

  • Revise.silence() now uses Preferences.jl for persistent storage instead of file-based persistence. (#976)
  • Removed unused Requires and Unicode dependencies. (#977)
  • Internal code quality improvements (#979, #980, #981, #982)

Fixed


For more details, see the documentation.

120 Likes

A big thank you to everyone who contributed to this! I know it was a major effort that involved a lot of work on the internals of Julia.

:heart:

(From the person who opened the original issue in 2017 :wink:)

39 Likes

This sounds great! But how to make use of it? On a new REPL after upgrading to Revise.jl v3.13.0 I get

julia> using Revise

julia> struct Inner
           value::Int
       end

julia> struct Outer
           inner::Inner
       end

julia> Outer |> sizeof
8

julia> Base.get_world_counter() |> Int
38691

julia> struct Inner
           value::Float64
           name::String
       end

julia> Outer |> sizeof
8

julia> using About

julia> Outer |> about
Concrete DataType defined in Main, 8B
  Outer <: Any

Struct with 1 fields:
• inner  @world(Inner, 38685:38691)

 ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
                                   8B

(@v1.12) pkg> status Revise
Status `~/.julia/environments/v1.12/Project.toml`
  [295af30f] Revise v3.13.0

i.e. 8 byte where I would have expected 16 bytes.

Note that you’re not actually using Revise there. This behavior is base Julia’s and is discussed in Inconsistencies in the meaning of code, post binding-partitioning · JuliaLang/julia · Discussion #58337 · GitHub.

You can see that — without a subsequent redefinition of Outer itself — the struct still references the old Inner:

julia> struct Inner
           value::Int
       end

julia> struct Outer
           inner::Inner
       end

julia> dump(Outer)
struct Outer <: Any
  inner::Inner

julia> struct Inner
           value::Float64
           name::String
       end

julia> dump(Outer)
struct Outer <: Any
  inner::@world(Inner, 38662:38668)

This is the reason d’être for using Revise.jl. If this is in a tracked file, then it’s Revise that finds that connection and automatically updates it:

julia> using Revise

julia> write("code.jl", """
       struct Inner
           value::Int
       end
       struct Outer
           inner::Inner
       end
       """)

julia> includet("code.jl")

julia> sizeof(Outer)
8

julia> ### Now edit code.jl to use the new Inner definition...

julia> sizeof(Outer)
16
4 Likes

Thanks! Your post and the linked discussion are both very informative. So in the end it’s a mix of two design decisions I was not aware of:

  • The different handling of “method bodies” versus “type bodies” regarding binding-partitioning
  • Revise.jl not tracking Main

Again, it’s not that revise was neglecting something, its that you weren’t using or interacting with Revise at all. Revise.jl has never watched your REPL and re-evaluated code based on things you did in your REPL. Revise doesnt truly watch modules, it’s all just about watching and analyzing files

8 Likes