Revise 3.16: fast type-revision, frozen worlds, and complete language support

A month ago I announced Revise 3.15.0 and described a big push through the issue backlog. That push has continued, and I’m pleased to announce Revise 3.16.1 (together with its recent siblings 3.15.1 and 3.16.0). First, the numbers: since June 10, there have been

  • 26 PRs merged across three releases
  • 34 issues closed, all but one of which predated the previous announcement. The oldest was filed in July 2020, and the median had been open for about two and a quarter years.
  • Open issues dropped from 41 to 8.

I’m pretty excited that a package of Revise’s complexity has gotten down to just 8 open issues. Moreover, most of the survivors are design questions or feature requests rather than bugs.

struct revision: headed for on-by-default

The change I suspect will excite users most: struct revision is now fast enough to turn on by default. On Julia 1.12 and later, Revise has supported struct revision since 3.13.0, but so far this has been off by default because the bookkeeping could take a very long time. It’s still off-by-default, but the performance barrier is now gone (#1076). The scan that discovers code affected by a type change used to walk the type tree recursively, rescanning the names of every loaded module at each step; it is now a single sweep over module bindings. In a session with ~29,000 types, the cost dropped from ~5.5 seconds to ~13 millisecond, and sessions with large type trees had been reporting minutes (#988) on workloads that are now ~40ms. The rewrite also eliminated a race that produced spurious world-age warnings (#993, #925).

With the performance barrier gone, I intend to make struct revision the default in Revise 3.17.0. Think of 3.16.x as the release candidate: if you’re on Julia 1.12 or later, please opt in now and report anything that misbehaves. To enable it, add (or edit) a LocalPreferences.toml file next to the Project.toml of your active project or default environment, containing:

[Revise]
revise_structs = true

See the documentation for details.

Revise now runs in a “frozen world”

Another very exciting change is that Revise now runs in a “frozen world” (#1092). Julia’s world age mechanism means that loading new packages, or revising code, can invalidate previously-compiled code. When the invalidated code was Revise’s own machinery, the results ranged from sluggishness (Revise recompiling itself after you load a big package) to outright failure: the notorious “method too new” and “binding accessed prior to its definition world” errors that could appear when Revise ran mid-way through a half-applied update. CUDA and Plots users were hit especially often (#552, #607, #701, #746, #807).

Now, Revise’s internals (signature extraction, method/type deletion and insertion) run pinned to the world age captured when Revise was loaded, so nothing you load or revise afterward can invalidate them or confuse their dispatch. Your code, of course, is still evaluated at the latest world thanks to per-frame world support in JuliaInterpreter 0.11. This eliminates a whole class of failures that have plagued Revise for years. (For the rare soul who deliberately revises Revise itself or one of its dependencies, Revise.advance_world!() re-pins to the current world.)

Nearly all of the serious work behind this feature actually landed upstream, in JuliaInterpreter 0.11.0: a long sequence of PRs finally delivered proper world-age support to JuliaInterpreter. Interpreted code now follows the same world-age semantics as compiled code—each frame executes at a well-defined world—where previously every interpreted call behaved like Base.invokelatest. That’s a long-desired feature in its own right, and its benefits extend to Debugger.jl and the other tools built on JuliaInterpreter.

More faithful revisions

Several fixes close gaps where a revision was silently incomplete:

  • Files included into more than one module. If the same file is included into several modules, editing it now updates all of them; previously only the first was revised (#730, from 2023).
  • include(mapexpr, file). Transforms passed to the two-argument form of include(a feature used by DispatchDoctor among other packages) now re-applied on revision instead of being silently dropped (#634, from 2021; #820). This work required Julia changes (#62176) so will only work on Julia 1.14+.
  • Removing an export. Deleting an export statement now retracts the export (#633, from 2021). This also relies on new Julia support (#62131) and so requires Julia 1.14+.
  • Revised types inside Unions. Methods whose signatures mention a revised struct only through a Union{Foo,Bar} were previously left dispatching on the stale type (#932).
  • Sharper diffing of macro-generated code. Revise’s expression comparator used to consider any generated (#...) name equal to any other, so a change confined to such names was invisible. Names are now matched by base name with consistent pairing (#1086).

The multiple-includes and support for include(mapexpr, file) may be a noteworthy milestone on their own: with those done, Revise may be able to claim that if your package precompiles, Revise supports it. That doesn’t mean that Revise doesn’t have bugs or limitations, but at least it now faithfully represents the relationships between source files and the packages that get built from them.

New capabilities

  • Revise.stale_load("MyPkg"). Suppose you’ve edited a package’s source but you’d rather not wait for re-precompilation: stale_load loads the most recent (stale) precompile cache and then revises just the files that changed. For packages that are expensive to compile, this can be a big win (#738).
  • @includet. The includet function has always evaluated files into Main, because a function cannot know its caller’s module. The new @includet macro evaluates into the calling module, just as include does (#682). Relatedly, includet now returns the value of the file’s last expression, matching include (#783).
  • Early warning for duplicate methods. Defining the same method signature in two places within a package works fine in a live session but breaks the next precompilation. Revise now warns as soon as a revision creates this situation, rather than letting you discover it at your next restart (#889).

Robustness against code generators

Investigating #945 (a code generator that deletes and rewrites entire source directories) uncovered two failure modes that could bite anyone:

  • Files that vanish and reappear. A revise() landing while a file was temporarily deleted used to permanently delete all its methods and drop it from tracking. Now there’s a grace period (Revise.missing_file_grace[], default 5 seconds) during which the file is left alone, and even past the grace period the file stays registered so its return is detected (#1074).
  • Rewrites faster than the filesystem clock. File timestamps advance in coarse ticks (~10ms on Linux, worse under virtualization), so a very fast delete-and-rewrite could produce an identical timestamp and the change was lost. Revise now trusts the filesystem event (which names the changed file) rather than relying on timestamp comparison (#1075). In stress testing with thousands of rapid rewrite cycles, losses went from roughly 1-in-700 to zero.

Latency

  • revise() no longer sleeps before processing changes. The pause was there to let editors finish writing, but with other improvements to synchronization the sleep was pure added latency (#838, from 2024).

Try it, and please report any problems

With the backlog down to single digits, “just works” is closer than it has ever been, and to me Revise now feels basically done. It’s a great time to test it out and report any bugs that harm the user experience, so that we’re well-prepared for an eventual rewrite around JuliaLowering and partial migration to Base.

Absolutely fantastic, what an impressive achievement! I can’t wait to try it out. Fast automatic struct revision is going to be so convenient - and I’ve also been hit my several of the now-closed bugs so the extra stability is going to be a nice QoL improvement.

This is major, thank you very much!