Revise.jl
repository·master·Indexed 23 days ago
https://github.com/timholy/revise.jlA tool for Julia developers that allows modifying code and immediately using those changes in an active REPL session without restarting Julia. It supports updating packages, switching git branches, and editing source code, eliminating the overhead of restarting and JIT compilation. Features include automatic startup configuration, support for various Julia release cycles, integration with IJulia, and the ability to track individual files via `includet` or packages via `Pkg.dev`.
What's inside Revise.jl
- Revise.jl allows you to modify code and use the changes without restarting your Julia session. It enables a workflow where you can update packages, switch git branches, or edit source code in your editor, and the changes are typically incorporated into the very next command you issue from the REPL. This eliminates the overhead of restarting Julia, reloading packages, and waiting for JIT compilation.
Core Revise functions for manual use
masterMost users interact with Revise through a small set of primary functions used to load code, track changes, or manage the state of the REPL session. Key functions include:
revise: The primary entry point for managing code revisions.includetor@includet: Used to include a file and track it for changes.Revise.track: Manually tells Revise to track a specific module or file.entr: Used for handling entry points or specific file entries.Revise.retry: Attempts to re-run a failed command.Revise.errors: Accesses or manages error states.Revise.duplicate_methods: Identifies or manages duplicate method definitions caused by reloading.Revise.stale_load: Handles loading of stale modules.
Toplevel binding changes do not propagate
masterRevise does not track implicit dependencies between top-level bindings. If a struct or function depends on a top-level binding (like a
constor a global variable), changing that binding will not trigger a re-evaluation of the dependent code.Example of failure:
MyVecType{T} = Vector{T} struct MyVec{T} v::MyVecType{T} endChanging
MyVecType{T}toAbstractVector{T}will not updateMyVecautomatically.Workaround: Manually call
Revise.revise(ModuleName)to force a re-evaluation of all definitions within that module.How struct revision works in Revise.jl
masterStarting with Julia 1.12, Revise supports changes to
structdefinitions. When you modify a struct, Revise automatically re-evaluates the struct definition and any dependent methods or types.Example of automatic propagation: If you have a struct
Innerused as a field inOuter, and a functionprint_value(o::Outer), changing the fields ofInnerwill trigger a re-evaluation ofOuterandprint_valueautomatically.Limitation: On Julia versions older than 1.12, struct revisions are not supported and require a session restart.
How Revise computes method signatures using lowered code
masterSince version 2.0, Revise uses lowered-code representations to compute method signatures. This approach is more robust than using standard Julia code because:
- Keyword Arguments: It correctly handles the expansion of keyword-argument methods into multiple specialized methods.
- Generated Methods: It can reliably compute signatures for methods generated by code (e.g., inside a loop using
@eval) by stepping through the code usingJuliaInterpreter.
Revise steps through the lowered code and looks for
:methodexpressions. When it finds one, it extracts the signature (the arguments and types) without evaluating the entire expression, which avoids unnecessary recompilation.Note on Code Execution: To avoid running dangerous or side-effect-heavy code (like
init_c_library()) during signature computation, Revise uses backedges to analyze dependencies. It only interprets code blocks that are strictly necessary to reach a:method,:struct_type, or:evalexpression. However, code inside an@evalblock may still be executed because Revise cannot reliably predict its expansion.What Revise can track
masterRevise can track changes in several types of code, including:
- Any package loaded with
importorusing. - Any script loaded with
includet(note: default restrictions may apply). BaseJulia itself (requiresRevise.track(Base)).- Julia standard libraries (e.g.,
Revise.track(Unicode)). Core.Compiler(requires building Julia from source).
To track
Base, use:Revise.track(Base)- Any package loaded with
Understand 'skipping git tests' warning
masterThe warning
skipping git tests because Revise is not under developmentis harmless.This occurs because Revise uses its own git repository for testing its git-based source extraction functionality. These tests only run if you have checked out Revise for development using
pkg> dev Reviseor if you are running on a CI server.How Revise works
masterRevise enables live code updates in a running Julia session by evaluating only the specific changes (diffs) made to your code, rather than reloading entire modules. This approach minimizes re-JITting and allows for changes to be detected independently of line numbers (e.g., moving code within a file does not trigger a full re-evaluation).
Conceptually, Revise implements a
diffandpatchmechanism for the running session. Its core logic follows this pattern:- Identify expressions (
def) present in the old state but missing in the new state, and delete the corresponding methods. - Identify expressions (
def) present in the new state but missing in the old state, and evaluate them usingCore.evalin the appropriate module.
Revise achieves this through several automated steps:
- Callbacks: Hooks into
Baseto detect when new packages are loaded or files areincluded. - Source-code Caching: Maintains a cache for every file to detect changes. For precompiled packages, it uses the source in
*.jifiles; for non-precompiled packages, it parses the source immediately upon inclusion. - File Monitoring: Watches the file system for changes to dependent files.
- REPL Interception: Intercepts the REPL backend to trigger the revision process after every command execution.
- Revision Loop: Re-parses changed files, creates a diff against the cache,
evals the diff, and updates the cache.
- Identify expressions (
Configure the `__revise_mode__` for modules
masterYou can control how Revise tracks changes within a specific module by defining a
__revise_mode__variable inside that module. This variable must be aSymbol.Available modes:
:eval: Evaluate everything (default for packages).:evalmeth: Evaluate changes to method definitions only (default forincludet).:evalassign: Evaluate method definitions and top-level assignment statements (e.g.,a = Int[]).::sigs`: Only scan method signatures to update their locations; no changes are implemented.
How Revise manages code updates (Forward and Backward Workflows)
masterRevise operates using two complementary workflows to bridge the gap between source text and compiled machine code:
- Forward Workflow: Converts source code into running code. It starts with strings from text files, parses them into Julia expressions, evaluates them into Julia objects, and compiles them into machine code.
- Backward Workflow: Converts compiled code back to source information. This is used to provide accurate stack traces and debugging information. Because Julia's internal 'breadcrumbs' (filename/line number) are static and don't update when files change, Revise corrects these line numbers so debuggers and error messages point to the correct location in your modified source files.
Revise maintains internal data structures that parallel Julia's own processing to ensure these workflows stay synchronized.
Track package development with `dev` instead of `add`
masterTo ensure
Revisetracks your changes, you must use thedevcommand in the Julia package manager (Pkg) rather thanadd.When you use
add, Julia installs a registered version of the package into your.julia/packagesdirectory, which is often read-only and not tracked byRevise. Usingdevtells the package manager to treat the package as being under active development, typically placing it in your~/.julia/devfolder whereRevisecan monitor file changes.Understand Revise terminology: definition, signature-expression, and signature-type
masterRevise uses specific terms to describe the components of a method:
- Definition: The full expression that defines a method (e.g., the entire
function ... endblock). - Signature-expression: The part of the definition specifying the name, argument names, types, and type-parameters (e.g.,
print_item(io::IO, item, ntimes::Integer=1, pre::String="")). - Signature-type: The specific types generated from a signature-expression that represent valid ways to call the method. For example, a method with two default arguments will generate three distinct signature-types, each corresponding to a different combination of arguments.
In internal Revise code, a definition is often referred to as
def, a signature-type assigt, and a method table/signature-type pair asmt_sigt.- Definition: The full expression that defines a method (e.g., the entire