| Line | Exclusive | Inclusive | Code |
|---|---|---|---|
| 1 | module Revise | ||
| 2 | |||
| 3 | using FileWatching, REPL, Distributed, UUIDs | ||
| 4 | import LibGit2 | ||
| 5 | using Base: PkgId | ||
| 6 | using Base.Meta: isexpr | ||
| 7 | using Core: CodeInfo | ||
| 8 | |||
| 9 | using OrderedCollections, CodeTracking, JuliaInterpreter, LoweredCodeUtils | ||
| 10 | using CodeTracking: PkgFiles, basedir, srcfiles | ||
| 11 | using JuliaInterpreter: whichtt, is_doc_expr, step_expr!, finish_and_return!, get_return | ||
| 12 | using JuliaInterpreter: @lookup, moduleof, scopeof, pc_expr, prepare_thunk, split_expressions | ||
| 13 | using LoweredCodeUtils: next_or_nothing!, isanonymous_typedef, define_anonymous | ||
| 14 | |||
| 15 | export revise, includet, entr, MethodSummary | ||
| 16 | |||
| 17 | """ | ||
| 18 | Revise.watching_files[] | ||
| 19 | |||
| 20 | Returns `true` if we watch files rather than their containing directory. | ||
| 21 | FreeBSD and NFS-mounted systems should watch files, otherwise we prefer to watch | ||
| 22 | directories. | ||
| 23 | """ | ||
| 24 | const watching_files = Ref(Sys.KERNEL == :FreeBSD) | ||
| 25 | |||
| 26 | """ | ||
| 27 | Revise.polling_files[] | ||
| 28 | |||
| 29 | Returns `true` if we should poll the filesystem for changes to the files that define | ||
| 30 | loaded code. It is preferable to avoid polling, instead relying on operating system | ||
| 31 | notifications via `FileWatching.watch_file`. However, NFS-mounted | ||
| 32 | filesystems (and perhaps others) do not support file-watching, so for code stored | ||
| 33 | on such filesystems you should turn polling on. | ||
| 34 | |||
| 35 | See the documentation for the `JULIA_REVISE_POLL` environment variable. | ||
| 36 | """ | ||
| 37 | const polling_files = Ref(false) | ||
| 38 | function wait_changed(file) | ||
| 39 | try | ||
| 40 | polling_files[] ? poll_file(file) : watch_file(file) | ||
| 41 | catch err | ||
| 42 | if Sys.islinux() && err isa Base.IOError && err.code == -28 # ENOSPC | ||
| 43 | @warn """Your operating system has run out of inotify capacity. | ||
| 44 | Check the current value with `cat /proc/sys/fs/inotify/max_user_watches`. | ||
| 45 | Set it to a higher level with, e.g., `echo 65536 | sudo tee -a /proc/sys/fs/inotify/max_user_watches`. | ||
| 46 | This requires having administrative privileges on your machine (or talk to your sysadmin). | ||
| 47 | See https://github.com/timholy/Revise.jl/issues/26 for more information.""" | ||
| 48 | end | ||
| 49 | rethrow(err) | ||
| 50 | end | ||
| 51 | return nothing | ||
| 52 | end | ||
| 53 | |||
| 54 | """ | ||
| 55 | Revise.tracking_Main_includes[] | ||
| 56 | |||
| 57 | Returns `true` if files directly included from the REPL should be tracked. | ||
| 58 | The default is `false`. See the documentation regarding the `JULIA_REVISE_INCLUDE` | ||
| 59 | environment variable to customize it. | ||
| 60 | """ | ||
| 61 | const tracking_Main_includes = Ref(false) | ||
| 62 | |||
| 63 | include("relocatable_exprs.jl") | ||
| 64 | include("types.jl") | ||
| 65 | include("utils.jl") | ||
| 66 | include("parsing.jl") | ||
| 67 | include("backedges.jl") | ||
| 68 | include("lowered.jl") | ||
| 69 | include("pkgs.jl") | ||
| 70 | include("git.jl") | ||
| 71 | include("recipes.jl") | ||
| 72 | include("logging.jl") | ||
| 73 | include("deprecations.jl") | ||
| 74 | |||
| 75 | ### Globals to keep track of state | ||
| 76 | |||
| 77 | """ | ||
| 78 | Revise.watched_files | ||
| 79 | |||
| 80 | Global variable, `watched_files[dirname]` returns the collection of files in `dirname` | ||
| 81 | that we're monitoring for changes. The returned value has type [`Revise.WatchList`](@ref). | ||
| 82 | |||
| 83 | This variable allows us to watch directories rather than files, reducing the burden on | ||
| 84 | the OS. | ||
| 85 | """ | ||
| 86 | const watched_files = Dict{String,WatchList}() | ||
| 87 | |||
| 88 | """ | ||
| 89 | Revise.revision_queue | ||
| 90 | |||
| 91 | Global variable, `revision_queue` holds `(pkgdata,filename)` pairs that we need to revise, meaning | ||
| 92 | that these files have changed since we last processed a revision. | ||
| 93 | This list gets populated by callbacks that watch directories for updates. | ||
| 94 | """ | ||
| 95 | const revision_queue = Set{Tuple{PkgData,String}}() | ||
| 96 | |||
| 97 | """ | ||
| 98 | Revise.queue_errors | ||
| 99 | |||
| 100 | Global variable, maps `(pkgdata, filename)` pairs that errored upon last revision to | ||
| 101 | `(exception, backtrace)`. | ||
| 102 | """ | ||
| 103 | const queue_errors = Dict{Tuple{PkgData,String},Tuple{Exception, Any}}() | ||
| 104 | |||
| 105 | """ | ||
| 106 | Revise.pkgdatas | ||
| 107 | |||
| 108 | `pkgdatas` is the core information that tracks the relationship between source code | ||
| 109 | and julia objects, and allows re-evaluation of code in the proper module scope. | ||
| 110 | It is a dictionary indexed by PkgId: | ||
| 111 | `pkgdatas[id]` returns a value of type [`Revise.PkgData`](@ref). | ||
| 112 | """ | ||
| 113 | const pkgdatas = Dict{PkgId,PkgData}() | ||
| 114 | |||
| 115 | const moduledeps = Dict{Module,DepDict}() | ||
| 116 | function get_depdict(mod::Module) | ||
| 117 | if !haskey(moduledeps, mod) | ||
| 118 | moduledeps[mod] = DepDict() | ||
| 119 | end | ||
| 120 | return moduledeps[mod] | ||
| 121 | end | ||
| 122 | |||
| 123 | """ | ||
| 124 | Revise.included_files | ||
| 125 | |||
| 126 | Global variable, `included_files` gets populated by callbacks we register with `include`. | ||
| 127 | It's used to track non-precompiled packages and, optionally, user scripts (see docs on | ||
| 128 | `JULIA_REVISE_INCLUDE`). | ||
| 129 | """ | ||
| 130 | const included_files = Tuple{Module,String}[] # (module, filename) | ||
| 131 | |||
| 132 | """ | ||
| 133 | Revise.basesrccache | ||
| 134 | |||
| 135 | Full path to the running Julia's cache of source code defining `Base`. | ||
| 136 | """ | ||
| 137 | const basesrccache = normpath(joinpath(Sys.BINDIR, Base.DATAROOTDIR, "julia", "base.cache")) | ||
| 138 | |||
| 139 | """ | ||
| 140 | Revise.basebuilddir | ||
| 141 | |||
| 142 | Julia's top-level directory when Julia was built, as recorded by the entries in | ||
| 143 | `Base._included_files`. | ||
| 144 | """ | ||
| 145 | const basebuilddir = begin | ||
| 146 | sysimg = filter(x->endswith(x[2], "sysimg.jl"), Base._included_files)[1][2] | ||
| 147 | dirname(dirname(sysimg)) | ||
| 148 | end | ||
| 149 | |||
| 150 | """ | ||
| 151 | Revise.juliadir | ||
| 152 | |||
| 153 | Constant specifying full path to julia top-level source directory. | ||
| 154 | This should be reliable even for local builds, cross-builds, and binary installs. | ||
| 155 | """ | ||
| 156 | const juliadir = begin | ||
| 157 | local jldir = basebuilddir | ||
| 158 | try | ||
| 159 | isdir(joinpath(jldir, "base")) || throw(ErrorException("$(jldir) does not have \"base\"")) | ||
| 160 | catch | ||
| 161 | # Binaries probably end up here. We fall back on Sys.BINDIR | ||
| 162 | jldir = joinpath(Sys.BINDIR, Base.DATAROOTDIR, "julia") | ||
| 163 | if !isdir(joinpath(jldir, "base")) | ||
| 164 | while true | ||
| 165 | trydir = joinpath(jldir, "base") | ||
| 166 | isdir(trydir) && break | ||
| 167 | trydir = joinpath(jldir, "share", "julia", "base") | ||
| 168 | if isdir(trydir) | ||
| 169 | jldir = joinpath(jldir, "share", "julia") | ||
| 170 | break | ||
| 171 | end | ||
| 172 | jldirnext = dirname(jldir) | ||
| 173 | jldirnext == jldir && break | ||
| 174 | jldir = jldirnext | ||
| 175 | end | ||
| 176 | end | ||
| 177 | end | ||
| 178 | normpath(jldir) | ||
| 179 | end | ||
| 180 | const cache_file_key = Dict{String,String}() # corrected=>uncorrected filenames | ||
| 181 | const src_file_key = Dict{String,String}() # uncorrected=>corrected filenames | ||
| 182 | |||
| 183 | """ | ||
| 184 | Revise.dont_watch_pkgs | ||
| 185 | |||
| 186 | Global variable, use `push!(Revise.dont_watch_pkgs, :MyPackage)` to prevent Revise | ||
| 187 | from tracking changes to `MyPackage`. You can do this from the REPL or from your | ||
| 188 | `.julia/config/startup.jl` file. | ||
| 189 | |||
| 190 | See also [`Revise.silence`](@ref). | ||
| 191 | """ | ||
| 192 | const dont_watch_pkgs = Set{Symbol}() | ||
| 193 | const silence_pkgs = Set{Symbol}() | ||
| 194 | const depsdir = joinpath(dirname(@__DIR__), "deps") | ||
| 195 | const silencefile = Ref(joinpath(depsdir, "silence.txt")) # Ref so that tests don't clobber | ||
| 196 | |||
| 197 | ## | ||
| 198 | ## The inputs are sets of expressions found in each file. | ||
| 199 | ## Some of those expressions will generate methods which are identified via their signatures. | ||
| 200 | ## From "old" expressions we know their corresponding signatures, but from "new" | ||
| 201 | ## expressions we have not yet computed them. This makes old and new asymmetric. | ||
| 202 | ## | ||
| 203 | ## Strategy: | ||
| 204 | ## - For every old expr not found in the new ones, | ||
| 205 | ## + delete the corresponding methods (using the signatures we've previously computed) | ||
| 206 | ## + remove the sig entries from CodeTracking.method_info (") | ||
| 207 | ## Best to do all the deletion first (across all files and modules) in case a method is | ||
| 208 | ## simply being moved from one file to another. | ||
| 209 | ## - For every new expr found among the old ones, | ||
| 210 | ## + update the location info in CodeTracking.method_info | ||
| 211 | ## - For every new expr not found in the old ones, | ||
| 212 | ## + eval the expr | ||
| 213 | ## + extract signatures | ||
| 214 | ## + add to the ModuleExprsSigs | ||
| 215 | ## + add to CodeTracking.method_info | ||
| 216 | ## | ||
| 217 | ## Interestingly, the ex=>sigs link may not be the same as the sigs=>ex link. | ||
| 218 | ## Consider a conditional block, | ||
| 219 | ## if Sys.islinux() | ||
| 220 | ## f() = 1 | ||
| 221 | ## g() = 2 | ||
| 222 | ## else | ||
| 223 | ## g() = 3 | ||
| 224 | ## end | ||
| 225 | ## From the standpoint of Revise's diff-and-patch functionality, we should look for | ||
| 226 | ## diffs in this entire block. (Really good backedge support---or a variant of `lower` that | ||
| 227 | ## links back to the specific expression---might change this, but for | ||
| 228 | ## now this is the right strategy.) From the standpoint of CodeTracking, we should | ||
| 229 | ## link the signature to the actual method-defining expression (either :(f() = 1) or :(g() = 2)). | ||
| 230 | |||
| 231 | function delete_missing!(exs_sigs_old::ExprsSigs, exs_sigs_new) | ||
| 232 | with_logger(_debug_logger) do | ||
| 233 | for (ex, sigs) in exs_sigs_old | ||
| 234 | haskey(exs_sigs_new, ex) && continue | ||
| 235 | # ex was deleted | ||
| 236 | sigs === nothing && continue | ||
| 237 | for sig in sigs | ||
| 238 | ret = Base._methods_by_ftype(sig, -1, typemax(UInt)) | ||
| 239 | success = false | ||
| 240 | if !isempty(ret) | ||
| 241 | m = ret[end][3]::Method # the last method returned is the least-specific that matches, and thus most likely to be type-equal | ||
| 242 | methsig = m.sig | ||
| 243 | if sig <: methsig && methsig <: sig | ||
| 244 | @debug "DeleteMethod" _group="Action" time=time() deltainfo=(sig, MethodSummary(m)) | ||
| 245 | # Delete the corresponding methods | ||
| 246 | for p in workers() | ||
| 247 | try # guard against serialization errors if the type isn't defined on the worker | ||
| 248 | remotecall(Core.eval, p, Main, :(delete_method_by_sig($sig))) | ||
| 249 | catch | ||
| 250 | end | ||
| 251 | end | ||
| 252 | Base.delete_method(m) | ||
| 253 | # Remove the entries from CodeTracking data | ||
| 254 | delete!(CodeTracking.method_info, sig) | ||
| 255 | # Remove frame from JuliaInterpreter, if applicable. Otherwise debuggers | ||
| 256 | # may erroneously work with outdated code (265-like problems) | ||
| 257 | if haskey(JuliaInterpreter.framedict, m) | ||
| 258 | delete!(JuliaInterpreter.framedict, m) | ||
| 259 | end | ||
| 260 | if isdefined(m, :generator) | ||
| 261 | # defensively delete all generated functions | ||
| 262 | empty!(JuliaInterpreter.genframedict) | ||
| 263 | end | ||
| 264 | success = true | ||
| 265 | end | ||
| 266 | end | ||
| 267 | if !success | ||
| 268 | @debug "FailedDeletion" _group="Action" time=time() deltainfo=(sig,) | ||
| 269 | end | ||
| 270 | end | ||
| 271 | end | ||
| 272 | end | ||
| 273 | return exs_sigs_old | ||
| 274 | end | ||
| 275 | |||
| 276 | const empty_exs_sigs = ExprsSigs() | ||
| 277 | function delete_missing!(mod_exs_sigs_old::ModuleExprsSigs, mod_exs_sigs_new) | ||
| 278 | for (mod, exs_sigs_old) in mod_exs_sigs_old | ||
| 279 | exs_sigs_new = get(mod_exs_sigs_new, mod, empty_exs_sigs) | ||
| 280 | delete_missing!(exs_sigs_old, exs_sigs_new) | ||
| 281 | end | ||
| 282 | return mod_exs_sigs_old | ||
| 283 | end | ||
| 284 | |||
| 285 | function eval_new!(exs_sigs_new::ExprsSigs, exs_sigs_old, mod::Module) | ||
| 286 | includes = Vector{String}() | ||
| 287 | with_logger(_debug_logger) do | ||
| 288 | for rex in keys(exs_sigs_new) | ||
| 289 | rexo = getkey(exs_sigs_old, rex, nothing) | ||
| 290 | # extract the signatures and update the line info | ||
| 291 | local sigs | ||
| 292 | if rexo === nothing | ||
| 293 | ex = rex.ex | ||
| 294 | # ex is not present in old | ||
| 295 | @debug "Eval" _group="Action" time=time() deltainfo=(mod, ex) | ||
| 296 | # try | ||
| 297 | sigs, deps, _includes, thunk = eval_with_signatures(mod, ex) # All signatures defined by `ex` | ||
| 298 | append!(includes, _includes) | ||
| 299 | if VERSION < v"1.3.0" || !isexpr(thunk, :thunk) | ||
| 300 | thunk = ex | ||
| 301 | end | ||
| 302 | for p in workers() | ||
| 303 | p == myid() && continue | ||
| 304 | try # don't error if `mod` isn't defined on the worker | ||
| 305 | remotecall(Core.eval, p, mod, thunk) | ||
| 306 | catch | ||
| 307 | end | ||
| 308 | end | ||
| 309 | storedeps(deps, rex, mod) | ||
| 310 | # catch err | ||
| 311 | # @error "failure to evaluate changes in $mod" | ||
| 312 | # showerror(stderr, err) | ||
| 313 | # println_maxsize(stderr, "\n", ex; maxlines=20) | ||
| 314 | # end | ||
| 315 | else | ||
| 316 | sigs = exs_sigs_old[rexo] | ||
| 317 | # Update location info | ||
| 318 | ln, lno = firstline(unwrap(rex)), firstline(unwrap(rexo)) | ||
| 319 | if sigs !== nothing && !isempty(sigs) && ln != lno | ||
| 320 | @debug "LineOffset" _group="Action" time=time() deltainfo=(sigs, lno=>ln) | ||
| 321 | for sig in sigs | ||
| 322 | local methloc, methdef | ||
| 323 | # try | ||
| 324 | methloc, methdef = CodeTracking.method_info[sig] | ||
| 325 | # catch err | ||
| 326 | # @show sig sigs | ||
| 327 | # @show CodeTracking.method_info | ||
| 328 | # rethrow(err) | ||
| 329 | # end | ||
| 330 | CodeTracking.method_info[sig] = (newloc(methloc, ln, lno), methdef) | ||
| 331 | end | ||
| 332 | end | ||
| 333 | end | ||
| 334 | # @show rex rexo sigs | ||
| 335 | exs_sigs_new[rex] = sigs | ||
| 336 | end | ||
| 337 | end | ||
| 338 | return exs_sigs_new, includes | ||
| 339 | end | ||
| 340 | |||
| 341 | function eval_new!(mod_exs_sigs_new::ModuleExprsSigs, mod_exs_sigs_old) | ||
| 342 | includes = Vector{Pair{Module,Vector{String}}}() | ||
| 343 | for (mod, exs_sigs_new) in mod_exs_sigs_new | ||
| 344 | exs_sigs_old = get(mod_exs_sigs_old, mod, empty_exs_sigs) | ||
| 345 | _, _includes = eval_new!(exs_sigs_new, exs_sigs_old, mod) | ||
| 346 | push!(includes, mod=>_includes) | ||
| 347 | end | ||
| 348 | return mod_exs_sigs_new, includes | ||
| 349 | end | ||
| 350 | |||
| 351 | struct CodeTrackingMethodInfo | ||
| 352 | exprstack::Vector{Expr} | ||
| 353 | allsigs::Vector{Any} | ||
| 354 | deps::Set{Union{GlobalRef,Symbol}} | ||
| 355 | includes::Vector{String} | ||
| 356 | end | ||
| 357 | CodeTrackingMethodInfo(ex::Expr) = CodeTrackingMethodInfo([ex], Any[], Set{Union{GlobalRef,Symbol}}(), String[]) | ||
| 358 | CodeTrackingMethodInfo(rex::RelocatableExpr) = CodeTrackingMethodInfo(rex.ex) | ||
| 359 | |||
| 360 | function add_signature!(methodinfo::CodeTrackingMethodInfo, @nospecialize(sig), ln) | ||
| 361 | CodeTracking.method_info[sig] = (fixpath(ln), methodinfo.exprstack[end]) | ||
| 362 | push!(methodinfo.allsigs, sig) | ||
| 363 | return methodinfo | ||
| 364 | end | ||
| 365 | push_expr!(methodinfo::CodeTrackingMethodInfo, mod::Module, ex::Expr) = (push!(methodinfo.exprstack, ex); methodinfo) | ||
| 366 | pop_expr!(methodinfo::CodeTrackingMethodInfo) = (pop!(methodinfo.exprstack); methodinfo) | ||
| 367 | function add_dependencies!(methodinfo::CodeTrackingMethodInfo, be::BackEdges, src, chunks) | ||
| 368 | isempty(src.code) && return methodinfo | ||
| 369 | stmt1 = first(src.code) | ||
| 370 | if isexpr(stmt1, :gotoifnot) && isa(stmt1.args[1], Union{GlobalRef,Symbol}) | ||
| 371 | if any(chunk->hastrackedexpr(src, chunk), chunks) | ||
| 372 | push!(methodinfo.deps, stmt1.args[1]) | ||
| 373 | end | ||
| 374 | end | ||
| 375 | # for (dep, lines) in be.byname | ||
| 376 | # for ln in lines | ||
| 377 | # stmt = src.code[ln] | ||
| 378 | # if isexpr(stmt, :(=)) && stmt.args[1] == dep | ||
| 379 | # continue | ||
| 380 | # else | ||
| 381 | # push!(methodinfo.deps, dep) | ||
| 382 | # end | ||
| 383 | # end | ||
| 384 | # end | ||
| 385 | return methodinfo | ||
| 386 | end | ||
| 387 | function add_includes!(methodinfo::CodeTrackingMethodInfo, filename) | ||
| 388 | push!(methodinfo.includes, filename) | ||
| 389 | return methodinfo | ||
| 390 | end | ||
| 391 | |||
| 392 | # Eval and insert into CodeTracking data | ||
| 393 | function eval_with_signatures(mod, ex::Expr; define=true, kwargs...) | ||
| 394 | methodinfo = CodeTrackingMethodInfo(ex) | ||
| 395 | docexprs = Dict{Module,Vector{Expr}}() | ||
| 396 | _, frame = methods_by_execution!(finish_and_return!, methodinfo, docexprs, mod, ex; define=define, kwargs...) | ||
| 397 | return methodinfo.allsigs, methodinfo.deps, methodinfo.includes, frame | ||
| 398 | end | ||
| 399 | |||
| 400 | function instantiate_sigs!(modexsigs::ModuleExprsSigs; define=false, kwargs...) | ||
| 401 | for (mod, exsigs) in modexsigs | ||
| 402 | for rex in keys(exsigs) | ||
| 403 | is_doc_expr(rex.ex) && continue | ||
| 404 | sigs, deps, _ = eval_with_signatures(mod, rex.ex; define=define, kwargs...) | ||
| 405 | exsigs[rex.ex] = sigs | ||
| 406 | storedeps(deps, rex, mod) | ||
| 407 | end | ||
| 408 | end | ||
| 409 | return modexsigs | ||
| 410 | end | ||
| 411 | |||
| 412 | function storedeps(deps, rex, mod) | ||
| 413 | for dep in deps | ||
| 414 | if isa(dep, GlobalRef) | ||
| 415 | haskey(moduledeps, dep.mod) || continue | ||
| 416 | ddict, sym = get_depdict(dep.mod), dep.name | ||
| 417 | else | ||
| 418 | ddict, sym = get_depdict(mod), dep | ||
| 419 | end | ||
| 420 | if !haskey(ddict, sym) | ||
| 421 | ddict[sym] = Set{DepDictVals}() | ||
| 422 | end | ||
| 423 | push!(ddict[sym], (mod, rex)) | ||
| 424 | end | ||
| 425 | return rex | ||
| 426 | end | ||
| 427 | |||
| 428 | # This is intended for testing purposes, but not general use. The key problem is | ||
| 429 | # that it doesn't properly handle methods that move from one file to another; there is the | ||
| 430 | # risk you could end up deleting the method altogether depending on the order in which you | ||
| 431 | # process these. | ||
| 432 | # See `revise` for the proper approach. | ||
| 433 | function eval_revised(mod_exs_sigs_new, mod_exs_sigs_old) | ||
| 434 | delete_missing!(mod_exs_sigs_old, mod_exs_sigs_new) | ||
| 435 | eval_new!(mod_exs_sigs_new, mod_exs_sigs_old) # note: drops `includes` | ||
| 436 | instantiate_sigs!(mod_exs_sigs_new) | ||
| 437 | end | ||
| 438 | |||
| 439 | """ | ||
| 440 | Revise.init_watching(files) | ||
| 441 | Revise.init_watching(pkgdata::PkgData, files) | ||
| 442 | |||
| 443 | For every filename in `files`, monitor the filesystem for updates. When the file is | ||
| 444 | updated, either [`Revise.revise_dir_queued`](@ref) or [`Revise.revise_file_queued`](@ref) will | ||
| 445 | be called. | ||
| 446 | |||
| 447 | Use the `pkgdata` version if the files are supplied using relative paths. | ||
| 448 | """ | ||
| 449 | function init_watching(pkgdata::PkgData, files) | ||
| 450 | udirs = Set{String}() | ||
| 451 | for file in files | ||
| 452 | dir, basename = splitdir(file) | ||
| 453 | dirfull = joinpath(basedir(pkgdata), dir) | ||
| 454 | already_watching = haskey(watched_files, dirfull) | ||
| 455 | already_watching || (watched_files[dirfull] = WatchList()) | ||
| 456 | push!(watched_files[dirfull], basename=>pkgdata) | ||
| 457 | if watching_files[] | ||
| 458 | fwatcher = Rescheduler(revise_file_queued, (pkgdata, file)) | ||
| 459 | schedule(Task(fwatcher)) | ||
| 460 | else | ||
| 461 | already_watching || push!(udirs, dir) | ||
| 462 | end | ||
| 463 | end | ||
| 464 | for dir in udirs | ||
| 465 | dirfull = joinpath(basedir(pkgdata), dir) | ||
| 466 | updatetime!(watched_files[dirfull]) | ||
| 467 | if !watching_files[] | ||
| 468 | dwatcher = Rescheduler(revise_dir_queued, (dirfull,)) | ||
| 469 | schedule(Task(dwatcher)) | ||
| 470 | end | ||
| 471 | end | ||
| 472 | return nothing | ||
| 473 | end | ||
| 474 | init_watching(files) = init_watching(PkgId(Main), files) | ||
| 475 | |||
| 476 | """ | ||
| 477 | revise_dir_queued(dirname) | ||
| 478 | |||
| 479 | Wait for one or more of the files registered in `Revise.watched_files[dirname]` to be | ||
| 480 | modified, and then queue the corresponding files on [`Revise.revision_queue`](@ref). | ||
| 481 | This is generally called via a [`Revise.Rescheduler`](@ref). | ||
| 482 | """ | ||
| 483 | @noinline function revise_dir_queued(dirname) | ||
| 484 | @assert isabspath(dirname) | ||
| 485 | if !isdir(dirname) | ||
| 486 | sleep(0.1) # in case git has done a delete/replace cycle | ||
| 487 | if !isdir(dirname) | ||
| 488 | with_logger(SimpleLogger(stderr)) do | ||
| 489 | @warn "$dirname is not an existing directory, Revise is not watching" | ||
| 490 | end | ||
| 491 | return false | ||
| 492 | end | ||
| 493 | end | ||
| 494 | latestfiles, stillwatching = watch_files_via_dir(dirname) # will block here until file(s) change | ||
| 495 | for (file, id) in latestfiles | ||
| 496 | key = joinpath(dirname, file) | ||
| 497 | pkgdata = pkgdatas[id] | ||
| 498 | if hasfile(pkgdata, key) # issue #228 | ||
| 499 | push!(revision_queue, (pkgdata, relpath(key, pkgdata))) | ||
| 500 | end | ||
| 501 | end | ||
| 502 | return stillwatching | ||
| 503 | end | ||
| 504 | |||
| 505 | # See #66. | ||
| 506 | """ | ||
| 507 | revise_file_queued(pkgdata::PkgData, filename) | ||
| 508 | |||
| 509 | Wait for modifications to `filename`, and then queue the corresponding files on [`Revise.revision_queue`](@ref). | ||
| 510 | This is generally called via a [`Revise.Rescheduler`](@ref). | ||
| 511 | |||
| 512 | This is used only on platforms (like BSD) which cannot use [`Revise.revise_dir_queued`](@ref). | ||
| 513 | """ | ||
| 514 | function revise_file_queued(pkgdata::PkgData, file) | ||
| 515 | file0 = file | ||
| 516 | if !isabspath(file) | ||
| 517 | file = joinpath(basedir(pkgdata), file) | ||
| 518 | end | ||
| 519 | if !file_exists(file) | ||
| 520 | sleep(0.1) # in case git has done a delete/replace cycle | ||
| 521 | if !file_exists(file) | ||
| 522 | push!(revision_queue, (pkgdata, file0)) # process file deletions | ||
| 523 | return false | ||
| 524 | end | ||
| 525 | end | ||
| 526 | |||
| 527 | wait_changed(file) # will block here until the file changes | ||
| 528 | # Check to see if we're still watching this file | ||
| 529 | dirfull, basename = splitdir(file) | ||
| 530 | if haskey(watched_files, dirfull) | ||
| 531 | push!(revision_queue, (pkgdata, file0)) | ||
| 532 | return true | ||
| 533 | end | ||
| 534 | return false | ||
| 535 | end | ||
| 536 | |||
| 537 | # Because we delete first, we have to make sure we've parsed the file | ||
| 538 | function handle_deletions(pkgdata, file) | ||
| 539 | fi = maybe_parse_from_cache!(pkgdata, file) | ||
| 540 | mexsold = fi.modexsigs | ||
| 541 | filep = normpath(joinpath(basedir(pkgdata), file)) | ||
| 542 | topmod = first(keys(mexsold)) | ||
| 543 | mexsnew = file_exists(filep) ? parse_source(filep, topmod) : | ||
| 544 | (@warn("$filep no longer exists, deleting all methods"); ModuleExprsSigs(topmod)) | ||
| 545 | if mexsnew !== nothing | ||
| 546 | delete_missing!(mexsold, mexsnew) | ||
| 547 | end | ||
| 548 | return mexsnew, mexsold | ||
| 549 | end | ||
| 550 | |||
| 551 | """ | ||
| 552 | Revise.revise_file_now(pkgdata::PkgData, file) | ||
| 553 | |||
| 554 | Process revisions to `file`. This parses `file` and computes an expression-level diff | ||
| 555 | between the current state of the file and its most recently evaluated state. | ||
| 556 | It then deletes any removed methods and re-evaluates any changed expressions. | ||
| 557 | Note that generally it is better to use [`revise`](@ref) as it properly handles methods | ||
| 558 | that move from one file to another. | ||
| 559 | |||
| 560 | `id` must be a key in [`Revise.pkgdatas`](@ref), and `file` a key in | ||
| 561 | `Revise.pkgdatas[id].fileinfos`. | ||
| 562 | """ | ||
| 563 | function revise_file_now(pkgdata::PkgData, file) | ||
| 564 | i = fileindex(pkgdata, file) | ||
| 565 | if i === nothing | ||
| 566 | println("Revise is currently tracking the following files in $(pkgdata.id): ", keys(pkgdict)) | ||
| 567 | error(file, " is not currently being tracked.") | ||
| 568 | end | ||
| 569 | mexsnew, mexsold = handle_deletions(pkgdata, file) | ||
| 570 | if mexsnew != nothing | ||
| 571 | _, includes = eval_new!(mexsnew, mexsold) | ||
| 572 | fi = fileinfo(pkgdata, i) | ||
| 573 | pkgdata.fileinfos[i] = FileInfo(mexsnew, fi) | ||
| 574 | maybe_add_includes_to_pkgdata!(pkgdata, file, includes) | ||
| 575 | end | ||
| 576 | nothing | ||
| 577 | end | ||
| 578 | |||
| 579 | """ | ||
| 580 | Revise.errors() | ||
| 581 | |||
| 582 | Report the errors represented in [`Revise.queue_errors`](@ref). | ||
| 583 | Errors are automatically reported the first time they are encountered, but this function | ||
| 584 | can be used to report errors again. | ||
| 585 | """ | ||
| 586 | function errors(revision_errors=keys(queue_errors)) | ||
| 587 | for (pkgdata, file) in revision_errors | ||
| 588 | (err, bt) = queue_errors[(pkgdata, file)] | ||
| 589 | fullpath = joinpath(basedir(pkgdata), file) | ||
| 590 | @error "Failed to revise $fullpath" exception=(err, trim_toplevel!(bt)) | ||
| 591 | end | ||
| 592 | end | ||
| 593 | |||
| 594 | """ | ||
| 595 | revise() | ||
| 596 | |||
| 597 | `eval` any changes in the revision queue. See [`Revise.revision_queue`](@ref). | ||
| 598 | """ | ||
| 599 | function revise() | ||
| 600 | sleep(0.01) # in case the file system isn't quite done writing out the new files | ||
| 601 | |||
| 602 | # Do all the deletion first. This ensures that a method that moved from one file to another | ||
| 603 | # won't get redefined first and deleted second. | ||
| 604 | revision_errors = [] | ||
| 605 | finished = eltype(revision_queue)[] | ||
| 606 | mexsnews = ModuleExprsSigs[] | ||
| 607 | interrupt = false | ||
| 608 | for (pkgdata, file) in revision_queue | ||
| 609 | try | ||
| 610 | push!(mexsnews, handle_deletions(pkgdata, file)[1]) | ||
| 611 | push!(finished, (pkgdata, file)) | ||
| 612 | catch err | ||
| 613 | interrupt |= isa(err, InterruptException) | ||
| 614 | push!(revision_errors, (pkgdata, file)) | ||
| 615 | queue_errors[(pkgdata, file)] = (err, catch_backtrace()) | ||
| 616 | end | ||
| 617 | end | ||
| 618 | # Do the evaluation | ||
| 619 | for ((pkgdata, file), mexsnew) in zip(finished, mexsnews) | ||
| 620 | i = fileindex(pkgdata, file) | ||
| 621 | fi = fileinfo(pkgdata, i) | ||
| 622 | try | ||
| 623 | _, includes = eval_new!(mexsnew, fi.modexsigs) | ||
| 624 | pkgdata.fileinfos[i] = FileInfo(mexsnew, fi) | ||
| 625 | delete!(queue_errors, (pkgdata, file)) | ||
| 626 | maybe_add_includes_to_pkgdata!(pkgdata, file, includes) | ||
| 627 | catch err | ||
| 628 | interrupt |= isa(err, InterruptException) | ||
| 629 | push!(revision_errors, (pkgdata, file)) | ||
| 630 | queue_errors[(pkgdata, file)] = (err, catch_backtrace()) | ||
| 631 | end | ||
| 632 | end | ||
| 633 | if interrupt | ||
| 634 | for pkgfile in finished | ||
| 635 | haskey(queue_errors, pkgfile) || delete!(revision_queue, pkgfile) | ||
| 636 | end | ||
| 637 | else | ||
| 638 | empty!(revision_queue) | ||
| 639 | end | ||
| 640 | errors(revision_errors) | ||
| 641 | if !isempty(queue_errors) | ||
| 642 | io = IOBuffer() | ||
| 643 | println(io, "\n") # better here than in the triple-quoted literal, see https://github.com/JuliaLang/julia/issues/34105 | ||
| 644 | for (pkgdata, file) in keys(queue_errors) | ||
| 645 | println(io, " ", joinpath(basedir(pkgdata), file)) | ||
| 646 | end | ||
| 647 | str = String(take!(io)) | ||
| 648 | @warn """Due to a previously reported error, the running code does not match saved version for the following files:$str | ||
| 649 | Use Revise.errors() to report errors again.""" | ||
| 650 | end | ||
| 651 | tracking_Main_includes[] && queue_includes(Main) | ||
| 652 | nothing | ||
| 653 | end | ||
| 654 | revise(backend::REPL.REPLBackend) = revise() | ||
| 655 | |||
| 656 | """ | ||
| 657 | revise(mod::Module) | ||
| 658 | |||
| 659 | Reevaluate every definition in `mod`, whether it was changed or not. This is useful | ||
| 660 | to propagate an updated macro definition, or to force recompiling generated functions. | ||
| 661 | """ | ||
| 662 | function revise(mod::Module) | ||
| 663 | mod == Main && error("cannot revise(Main)") | ||
| 664 | id = PkgId(mod) | ||
| 665 | pkgdata = pkgdatas[id] | ||
| 666 | for (i, file) in enumerate(srcfiles(pkgdata)) | ||
| 667 | fi = fileinfo(pkgdata, i) | ||
| 668 | for (mod, exsigs) in fi.modexsigs | ||
| 669 | for def in keys(exsigs) | ||
| 670 | ex = def.ex | ||
| 671 | exuw = unwrap(ex) | ||
| 672 | isexpr(exuw, :call) && exuw.args[1] == :include && continue | ||
| 673 | try | ||
| 674 | Core.eval(mod, ex) | ||
| 675 | catch err | ||
| 676 | @show mod | ||
| 677 | display(ex) | ||
| 678 | rethrow(err) | ||
| 679 | end | ||
| 680 | end | ||
| 681 | end | ||
| 682 | end | ||
| 683 | return true # fixme try/catch? | ||
| 684 | end | ||
| 685 | |||
| 686 | """ | ||
| 687 | Revise.track(mod::Module, file::AbstractString) | ||
| 688 | Revise.track(file::AbstractString) | ||
| 689 | |||
| 690 | Watch `file` for updates and [`revise`](@ref) loaded code with any | ||
| 691 | changes. `mod` is the module into which `file` is evaluated; if omitted, | ||
| 692 | it defaults to `Main`. | ||
| 693 | |||
| 694 | If this produces many errors, check that you specified `mod` correctly. | ||
| 695 | """ | ||
| 696 | function track(mod::Module, file::AbstractString; kwargs...) | ||
| 697 | isfile(file) || error(file, " is not a file") | ||
| 698 | # Determine whether we're already tracking this file | ||
| 699 | id = PkgId(mod) | ||
| 700 | file = normpath(abspath(file)) | ||
| 701 | haskey(pkgdatas, id) && hasfile(pkgdatas[id], file) && return nothing | ||
| 702 | # Set up tracking | ||
| 703 | fm = parse_source(file, mod) | ||
| 704 | if fm !== nothing | ||
| 705 | instantiate_sigs!(fm; kwargs...) | ||
| 706 | if !haskey(pkgdatas, id) | ||
| 707 | # Wait a bit to see if `mod` gets initialized | ||
| 708 | # This can happen if the module's __init__ function | ||
| 709 | # calls `track`, e.g., via a @require. Ref issue #403. | ||
| 710 | sleep(0.1) | ||
| 711 | end | ||
| 712 | if !haskey(pkgdatas, id) | ||
| 713 | pkgdatas[id] = PkgData(id, pathof(mod)) | ||
| 714 | end | ||
| 715 | pkgdata = pkgdatas[id] | ||
| 716 | if !haskey(CodeTracking._pkgfiles, id) | ||
| 717 | CodeTracking._pkgfiles[id] = pkgdata.info | ||
| 718 | end | ||
| 719 | push!(pkgdata, relpath(file, pkgdata)=>FileInfo(fm)) | ||
| 720 | init_watching(pkgdata, (file,)) | ||
| 721 | end | ||
| 722 | return nothing | ||
| 723 | end | ||
| 724 | |||
| 725 | function track(file::AbstractString; kwargs...) | ||
| 726 | startswith(file, juliadir) && error("use Revise.track(Base) or Revise.track(<stdlib module>)") | ||
| 727 | track(Main, file; kwargs...) | ||
| 728 | end | ||
| 729 | |||
| 730 | """ | ||
| 731 | includet(filename) | ||
| 732 | |||
| 733 | Load `filename` and track any future changes to it. `includet` is essentially shorthand for | ||
| 734 | |||
| 735 | Revise.track(Main, filename; define=true, skip_include=false) | ||
| 736 | |||
| 737 | `includet` is intended for "user scripts," e.g., a file you use locally for a specific | ||
| 738 | purpose such as loading a specific data set or performing a particular analysis. | ||
| 739 | Do *not* use `includet` for packages, as those should be handled by `using` or `import`. | ||
| 740 | (If you're working with code in Base or one of Julia's standard libraries, use | ||
| 741 | `Revise.track(mod)` instead, where `mod` is the module.) | ||
| 742 | If `using` and `import` aren't working, you may have packages in a non-standard location; | ||
| 743 | try fixing it with something like `push!(LOAD_PATH, "/path/to/my/private/repos")`. | ||
| 744 | |||
| 745 | `includet` is deliberately non-recursive, so if `filename` loads any other files, | ||
| 746 | they will not be automatically tracked. | ||
| 747 | (See [`Revise.track`](@ref) to set it up manually.) | ||
| 748 | """ | ||
| 749 | function includet(mod::Module, file::AbstractString) | ||
| 750 | prev = Base.source_path(nothing) | ||
| 751 | if prev === nothing | ||
| 752 | file = abspath(file) | ||
| 753 | else | ||
| 754 | file = normpath(joinpath(dirname(prev), file)) | ||
| 755 | end | ||
| 756 | tls = task_local_storage() | ||
| 757 | tls[:SOURCE_PATH] = file | ||
| 758 | try | ||
| 759 | track(mod, file; define=true, skip_include=false) | ||
| 760 | finally | ||
| 761 | if prev === nothing | ||
| 762 | delete!(tls, :SOURCE_PATH) | ||
| 763 | else | ||
| 764 | tls[:SOURCE_PATH] = prev | ||
| 765 | end | ||
| 766 | end | ||
| 767 | return nothing | ||
| 768 | end | ||
| 769 | includet(file::AbstractString) = includet(Main, file) | ||
| 770 | |||
| 771 | """ | ||
| 772 | entr(f, files; postpone=false, pause=0.02) | ||
| 773 | entr(f, files, modules; postpone=false, pause=0.02) | ||
| 774 | |||
| 775 | Execute `f()` whenever files listed in `files`, or code in `modules`, updates. | ||
| 776 | `entr` will process updates (and block your command line) until you press Ctrl-C. | ||
| 777 | Unless `postpone` is `true`, `f()` will be executed also when calling `entr`, | ||
| 778 | regardless of file changes. The `pause` is the period (in seconds) that `entr` | ||
| 779 | will wait between being triggered and actually calling `f()`, to handle | ||
| 780 | clusters of modifications, such as those produced by saving files in certain | ||
| 781 | text editors. | ||
| 782 | |||
| 783 | # Example | ||
| 784 | |||
| 785 | ```julia | ||
| 786 | entr(["/tmp/watched.txt"], [Pkg1, Pkg2]) do | ||
| 787 | println("update") | ||
| 788 | end | ||
| 789 | ``` | ||
| 790 | This will print "update" every time `"/tmp/watched.txt"` or any of the code defining | ||
| 791 | `Pkg1` or `Pkg2` gets updated. | ||
| 792 | """ | ||
| 793 | function entr(f::Function, files, modules=nothing; postpone=false, pause=0.02) | ||
| 794 | yield() | ||
| 795 | files = collect(files) # because we may add to this list | ||
| 796 | if modules !== nothing | ||
| 797 | for mod in modules | ||
| 798 | id = PkgId(mod) | ||
| 799 | pkgdata = pkgdatas[id] | ||
| 800 | for file in srcfiles(pkgdata) | ||
| 801 | push!(files, joinpath(basedir(pkgdata), file)) | ||
| 802 | end | ||
| 803 | end | ||
| 804 | end | ||
| 805 | active = true | ||
| 806 | try | ||
| 807 | @sync begin | ||
| 808 | postpone || f() | ||
| 809 | for file in files | ||
| 810 | waitfor = isdir(file) ? watch_folder : watch_file | ||
| 811 | @async while active | ||
| 812 | ret = waitfor(file, 1) | ||
| 813 | if active && (ret.changed || ret.renamed) | ||
| 814 | sleep(pause) | ||
| 815 | revise() | ||
| 816 | Base.invokelatest(f) | ||
| 817 | end | ||
| 818 | end | ||
| 819 | end | ||
| 820 | end | ||
| 821 | catch err | ||
| 822 | if isa(err, InterruptException) | ||
| 823 | active = false | ||
| 824 | else | ||
| 825 | rethrow(err) | ||
| 826 | end | ||
| 827 | end | ||
| 828 | end | ||
| 829 | |||
| 830 | """ | ||
| 831 | Revise.silence(pkg) | ||
| 832 | |||
| 833 | Silence warnings about not tracking changes to package `pkg`. | ||
| 834 | """ | ||
| 835 | function silence(pkg::Symbol) | ||
| 836 | push!(silence_pkgs, pkg) | ||
| 837 | if !isdir(depsdir) | ||
| 838 | mkpath(depsdir) | ||
| 839 | end | ||
| 840 | open(silencefile[], "w") do io | ||
| 841 | for p in silence_pkgs | ||
| 842 | println(io, p) | ||
| 843 | end | ||
| 844 | end | ||
| 845 | nothing | ||
| 846 | end | ||
| 847 | silence(pkg::AbstractString) = silence(Symbol(pkg)) | ||
| 848 | |||
| 849 | ## Utilities | ||
| 850 | |||
| 851 | """ | ||
| 852 | method = get_method(sigt) | ||
| 853 | |||
| 854 | Get the method `method` with signature-type `sigt`. This is used to provide | ||
| 855 | the method to `Base.delete_method`. | ||
| 856 | |||
| 857 | If `sigt` does not correspond to a method, returns `nothing`. | ||
| 858 | |||
| 859 | # Examples | ||
| 860 | |||
| 861 | ```jldoctest; setup = :(using Revise), filter = r"in Main at.*" | ||
| 862 | julia> mymethod(::Int) = 1 | ||
| 863 | mymethod (generic function with 1 method) | ||
| 864 | |||
| 865 | julia> mymethod(::AbstractFloat) = 2 | ||
| 866 | mymethod (generic function with 2 methods) | ||
| 867 | |||
| 868 | julia> Revise.get_method(Tuple{typeof(mymethod), Int}) | ||
| 869 | mymethod(::Int64) in Main at REPL[0]:1 | ||
| 870 | |||
| 871 | julia> Revise.get_method(Tuple{typeof(mymethod), Float64}) | ||
| 872 | mymethod(::AbstractFloat) in Main at REPL[1]:1 | ||
| 873 | |||
| 874 | julia> Revise.get_method(Tuple{typeof(mymethod), Number}) | ||
| 875 | |||
| 876 | ``` | ||
| 877 | """ | ||
| 878 | function get_method(@nospecialize(sigt)) | ||
| 879 | mths = Base._methods_by_ftype(sigt, -1, typemax(UInt)) | ||
| 880 | length(mths) == 1 && return mths[1][3] | ||
| 881 | if !isempty(mths) | ||
| 882 | # There might be many methods, but the one that should match should be the | ||
| 883 | # last one, since methods are ordered by specificity | ||
| 884 | i = lastindex(mths) | ||
| 885 | while i > 0 | ||
| 886 | m = mths[i][3] | ||
| 887 | m.sig == sigt && return m | ||
| 888 | i -= 1 | ||
| 889 | end | ||
| 890 | end | ||
| 891 | return nothing | ||
| 892 | end | ||
| 893 | |||
| 894 | """ | ||
| 895 | success = get_def(method::Method) | ||
| 896 | |||
| 897 | As needed, load the source file necessary for extracting the code defining `method`. | ||
| 898 | The source-file defining `method` must be tracked. | ||
| 899 | If it is in Base, this will execute `track(Base)` if necessary. | ||
| 900 | |||
| 901 | This is a callback function used by `CodeTracking.jl`'s `definition`. | ||
| 902 | """ | ||
| 903 | function get_def(method::Method; modified_files=revision_queue) | ||
| 904 | yield() # magic bug fix for the OSX test failures. TODO: figure out why this works (prob. Julia bug) | ||
| 905 | if method.file == :none && String(method.name)[1] == '#' | ||
| 906 | # This is likely to be a kwarg method, try to find something with location info | ||
| 907 | method = bodymethod(method) | ||
| 908 | end | ||
| 909 | filename = fixpath(String(method.file)) | ||
| 910 | if startswith(filename, "REPL[") | ||
| 911 | isdefined(Base, :active_repl) || return false | ||
| 912 | fi = add_definitions_from_repl(filename) | ||
| 913 | hassig = false | ||
| 914 | for (mod, exs) in fi.modexsigs | ||
| 915 | for sigs in values(exs) | ||
| 916 | hassig |= !isempty(sigs) | ||
| 917 | end | ||
| 918 | end | ||
| 919 | return hassig | ||
| 920 | end | ||
| 921 | id = get_tracked_id(method.module; modified_files=modified_files) | ||
| 922 | id === nothing && return false | ||
| 923 | pkgdata = pkgdatas[id] | ||
| 924 | filename = relpath(filename, pkgdata) | ||
| 925 | if hasfile(pkgdata, filename) | ||
| 926 | def = get_def(method, pkgdata, filename) | ||
| 927 | def !== nothing && return true | ||
| 928 | end | ||
| 929 | # Lookup can fail for macro-defined methods, see https://github.com/JuliaLang/julia/issues/31197 | ||
| 930 | # We need to find the right file. | ||
| 931 | if method.module == Base || method.module == Core || method.module == Core.Compiler | ||
| 932 | @warn "skipping $method to avoid parsing too much code" | ||
| 933 | CodeTracking.method_info[method.sig] = missing | ||
| 934 | return false | ||
| 935 | end | ||
| 936 | parentfile, included_files = modulefiles(method.module) | ||
| 937 | if parentfile !== nothing | ||
| 938 | def = get_def(method, pkgdata, relpath(parentfile, pkgdata)) | ||
| 939 | def !== nothing && return true | ||
| 940 | for modulefile in included_files | ||
| 941 | def = get_def(method, pkgdata, relpath(modulefile, pkgdata)) | ||
| 942 | def !== nothing && return true | ||
| 943 | end | ||
| 944 | end | ||
| 945 | # As a last resort, try every file in the package | ||
| 946 | for file in srcfiles(pkgdata) | ||
| 947 | def = get_def(method, pkgdata, file) | ||
| 948 | def !== nothing && return true | ||
| 949 | end | ||
| 950 | @warn "$(method.sig) was not found" | ||
| 951 | # So that we don't call it again, store missingness info in CodeTracking | ||
| 952 | CodeTracking.method_info[method.sig] = missing | ||
| 953 | return false | ||
| 954 | end | ||
| 955 | |||
| 956 | function get_def(method, pkgdata, filename) | ||
| 957 | maybe_parse_from_cache!(pkgdata, filename) | ||
| 958 | return get(CodeTracking.method_info, method.sig, nothing) | ||
| 959 | end | ||
| 960 | |||
| 961 | function get_tracked_id(id::PkgId; modified_files=revision_queue) | ||
| 962 | # Methods from Base or the stdlibs may require that we start tracking | ||
| 963 | if !haskey(pkgdatas, id) | ||
| 964 | recipe = id.name === "Compiler" ? :Compiler : Symbol(id.name) | ||
| 965 | recipe == :Core && return nothing | ||
| 966 | _track(id, recipe; modified_files=modified_files) | ||
| 967 | @info "tracking $recipe" | ||
| 968 | if !haskey(pkgdatas, id) | ||
| 969 | @warn "despite tracking $recipe, $id was not found" | ||
| 970 | return nothing | ||
| 971 | end | ||
| 972 | end | ||
| 973 | return id | ||
| 974 | end | ||
| 975 | get_tracked_id(mod::Module; modified_files=revision_queue) = | ||
| 976 | get_tracked_id(PkgId(mod); modified_files=modified_files) | ||
| 977 | |||
| 978 | function get_expressions(id::PkgId, filename) | ||
| 979 | get_tracked_id(id) | ||
| 980 | pkgdata = pkgdatas[id] | ||
| 981 | maybe_parse_from_cache!(pkgdata, filename) | ||
| 982 | fi = fileinfo(pkgdata, filename) | ||
| 983 | return fi.modexsigs | ||
| 984 | end | ||
| 985 | |||
| 986 | function add_definitions_from_repl(filename) | ||
| 987 | hist_idx = parse(Int, filename[6:end-1]) | ||
| 988 | hp = Base.active_repl.interface.modes[1].hist | ||
| 989 | src = hp.history[hp.start_idx+hist_idx] | ||
| 990 | id = PkgId(nothing, "@REPL") | ||
| 991 | pkgdata = pkgdatas[id] | ||
| 992 | mexs = ModuleExprsSigs(Main) | ||
| 993 | parse_source!(mexs, src, filename, Main) | ||
| 994 | instantiate_sigs!(mexs) | ||
| 995 | fi = FileInfo(mexs) | ||
| 996 | push!(pkgdata, filename=>fi) | ||
| 997 | return fi | ||
| 998 | end | ||
| 999 | |||
| 1000 | function fix_line_statements!(ex::Expr, file::Symbol, line_offset::Int=0) | ||
| 1001 | if ex.head == :line | ||
| 1002 | ex.args[1] += line_offset | ||
| 1003 | ex.args[2] = file | ||
| 1004 | else | ||
| 1005 | for (i, a) in enumerate(ex.args) | ||
| 1006 | if isa(a, Expr) | ||
| 1007 | fix_line_statements!(a::Expr, file, line_offset) | ||
| 1008 | elseif isa(a, LineNumberNode) | ||
| 1009 | ex.args[i] = file_line_statement(a::LineNumberNode, file, line_offset) | ||
| 1010 | end | ||
| 1011 | end | ||
| 1012 | end | ||
| 1013 | ex | ||
| 1014 | end | ||
| 1015 | |||
| 1016 | file_line_statement(lnn::LineNumberNode, file::Symbol, line_offset) = | ||
| 1017 | LineNumberNode(lnn.line + line_offset, file) | ||
| 1018 | |||
| 1019 | function update_stacktrace_lineno!(trace) | ||
| 1020 | local nrep | ||
| 1021 | for i = 1:length(trace) | ||
| 1022 | t = trace[i] | ||
| 1023 | has_nrep = !isa(t, StackTraces.StackFrame) | ||
| 1024 | if has_nrep | ||
| 1025 | t, nrep = t | ||
| 1026 | end | ||
| 1027 | t = t::StackTraces.StackFrame | ||
| 1028 | if t.linfo isa Core.MethodInstance | ||
| 1029 | m = t.linfo.def | ||
| 1030 | sigt = m.sig | ||
| 1031 | # Why not just call `whereis`? Because that forces tracking. This is being | ||
| 1032 | # clever by recognizing that these entries exist only if there have been updates. | ||
| 1033 | updated = get(CodeTracking.method_info, sigt, nothing) | ||
| 1034 | if updated !== nothing | ||
| 1035 | lnn = updated[1] | ||
| 1036 | lineoffset = lnn.line - m.line | ||
| 1037 | t = StackTraces.StackFrame(t.func, lnn.file, t.line+lineoffset, t.linfo, t.from_c, t.inlined, t.pointer) | ||
| 1038 | trace[i] = has_nrep ? (t, nrep) : t | ||
| 1039 | end | ||
| 1040 | end | ||
| 1041 | end | ||
| 1042 | return trace | ||
| 1043 | end | ||
| 1044 | |||
| 1045 | function method_location(method::Method) | ||
| 1046 | # Why not just call `whereis`? Because that forces tracking. This is being | ||
| 1047 | # clever by recognizing that these entries exist only if there have been updates. | ||
| 1048 | updated = get(CodeTracking.method_info, method.sig, nothing) | ||
| 1049 | if updated !== nothing | ||
| 1050 | lnn = updated[1] | ||
| 1051 | return lnn.file, lnn.line | ||
| 1052 | end | ||
| 1053 | return method.file, method.line | ||
| 1054 | end | ||
| 1055 | |||
| 1056 | @noinline function run_backend(backend) | ||
| 1057 | while true | ||
| 1058 | tls = task_local_storage() | ||
| 1059 | tls[:SOURCE_PATH] = nothing | ||
| 1060 | ast, show_value = take!(backend.repl_channel) | ||
| 1061 | if show_value == -1 | ||
| 1062 | # exit flag | ||
| 1063 | break | ||
| 1064 | end | ||
| 1065 | # Process revisions, skipping `exit()` (issue #327) | ||
| 1066 | if !isa(ast, Expr) || length(ast.args) < 2 || (ex = ast.args[2]; !isexpr(ex, :call)) || length(ex.args) != 1 || ex.args[1] != :exit | ||
| 1067 | revise(backend) | ||
| 1068 | end | ||
| 1069 | # Now eval the input | ||
| 1070 | 833 (100 %) |
833 (100 %)
samples spent calling
eval_user_input
REPL.eval_user_input(ast, backend)
|
|
| 1071 | end | ||
| 1072 | nothing | ||
| 1073 | end | ||
| 1074 | |||
| 1075 | """ | ||
| 1076 | steal_repl_backend(backend = Base.active_repl_backend) | ||
| 1077 | |||
| 1078 | Replace the REPL's normal backend with one that calls [`revise`](@ref) before executing | ||
| 1079 | any REPL input. | ||
| 1080 | """ | ||
| 1081 | function steal_repl_backend(backend = Base.active_repl_backend) | ||
| 1082 | @async begin | ||
| 1083 | # terminate the current backend | ||
| 1084 | put!(backend.repl_channel, (nothing, -1)) | ||
| 1085 | fetch(backend.backend_task) | ||
| 1086 | # restart a new backend that differs only by processing the | ||
| 1087 | # revision queue before evaluating each user input | ||
| 1088 | backend.backend_task = @async run_backend(backend) | ||
| 1089 | end | ||
| 1090 | nothing | ||
| 1091 | end | ||
| 1092 | |||
| 1093 | function wait_steal_repl_backend() | ||
| 1094 | iter = 0 | ||
| 1095 | # wait for active_repl_backend to exist | ||
| 1096 | while !isdefined(Base, :active_repl_backend) && iter < 20 | ||
| 1097 | sleep(0.05) | ||
| 1098 | iter += 1 | ||
| 1099 | end | ||
| 1100 | if isdefined(Base, :active_repl_backend) | ||
| 1101 | steal_repl_backend(Base.active_repl_backend) | ||
| 1102 | else | ||
| 1103 | @warn "REPL initialization failed, Revise is not in automatic mode. Call `revise()` manually." | ||
| 1104 | end | ||
| 1105 | end | ||
| 1106 | |||
| 1107 | """ | ||
| 1108 | Revise.async_steal_repl_backend() | ||
| 1109 | |||
| 1110 | Wait for the REPL to complete its initialization, and then call [`Revise.steal_repl_backend`](@ref). | ||
| 1111 | This is necessary because code registered with `atreplinit` runs before the REPL is | ||
| 1112 | initialized, and there is no corresponding way to register code to run after it is complete. | ||
| 1113 | """ | ||
| 1114 | function async_steal_repl_backend() | ||
| 1115 | mode = get(ENV, "JULIA_REVISE", "auto") | ||
| 1116 | if mode == "auto" | ||
| 1117 | atreplinit() do repl | ||
| 1118 | @async wait_steal_repl_backend() | ||
| 1119 | end | ||
| 1120 | end | ||
| 1121 | return nothing | ||
| 1122 | end | ||
| 1123 | |||
| 1124 | """ | ||
| 1125 | Revise.init_worker(p) | ||
| 1126 | |||
| 1127 | Define methods on worker `p` that Revise needs in order to perform revisions on `p`. | ||
| 1128 | Revise itself does not need to be running on `p`. | ||
| 1129 | """ | ||
| 1130 | function init_worker(p) | ||
| 1131 | remotecall(Core.eval, p, Main, quote | ||
| 1132 | function whichtt(sig) | ||
| 1133 | ret = Base._methods_by_ftype(sig, -1, typemax(UInt)) | ||
| 1134 | isempty(ret) && return nothing | ||
| 1135 | m = ret[end][3]::Method # the last method returned is the least-specific that matches, and thus most likely to be type-equal | ||
| 1136 | methsig = m.sig | ||
| 1137 | (sig <: methsig && methsig <: sig) || return nothing | ||
| 1138 | return m | ||
| 1139 | end | ||
| 1140 | function delete_method_by_sig(sig) | ||
| 1141 | m = whichtt(sig) | ||
| 1142 | isa(m, Method) && Base.delete_method(m) | ||
| 1143 | end | ||
| 1144 | end) | ||
| 1145 | end | ||
| 1146 | |||
| 1147 | function __init__() | ||
| 1148 | myid() == 1 || return nothing | ||
| 1149 | if isfile(silencefile[]) | ||
| 1150 | pkgs = readlines(silencefile[]) | ||
| 1151 | for pkg in pkgs | ||
| 1152 | push!(silence_pkgs, Symbol(pkg)) | ||
| 1153 | end | ||
| 1154 | end | ||
| 1155 | push!(Base.package_callbacks, watch_package) | ||
| 1156 | push!(Base.include_callbacks, | ||
| 1157 | (mod::Module, fn::AbstractString) -> push!(included_files, (mod, normpath(abspath(fn))))) | ||
| 1158 | mode = get(ENV, "JULIA_REVISE", "auto") | ||
| 1159 | if mode == "auto" | ||
| 1160 | if isdefined(Base, :active_repl_backend) | ||
| 1161 | steal_repl_backend(Base.active_repl_backend::REPL.REPLBackend) | ||
| 1162 | elseif isdefined(Main, :IJulia) | ||
| 1163 | Main.IJulia.push_preexecute_hook(revise) | ||
| 1164 | end | ||
| 1165 | if isdefined(Main, :Atom) | ||
| 1166 | setup_atom(getfield(Main, :Atom)::Module) | ||
| 1167 | end | ||
| 1168 | end | ||
| 1169 | polling = get(ENV, "JULIA_REVISE_POLL", "0") | ||
| 1170 | if polling == "1" | ||
| 1171 | polling_files[] = watching_files[] = true | ||
| 1172 | end | ||
| 1173 | rev_include = get(ENV, "JULIA_REVISE_INCLUDE", "0") | ||
| 1174 | if rev_include == "1" | ||
| 1175 | tracking_Main_includes[] = true | ||
| 1176 | end | ||
| 1177 | # Correct line numbers for code moving around | ||
| 1178 | Base.update_stackframes_callback[] = update_stacktrace_lineno! | ||
| 1179 | if isdefined(Base, :methodloc_callback) | ||
| 1180 | Base.methodloc_callback[] = method_location | ||
| 1181 | end | ||
| 1182 | # Populate CodeTracking data for dependencies and initialize watching | ||
| 1183 | for mod in (CodeTracking, OrderedCollections, JuliaInterpreter, LoweredCodeUtils) | ||
| 1184 | id = PkgId(mod) | ||
| 1185 | parse_pkg_files(id) | ||
| 1186 | pkgdata = pkgdatas[id] | ||
| 1187 | init_watching(pkgdata, srcfiles(pkgdata)) | ||
| 1188 | end | ||
| 1189 | # Add `includet` to the compiled_modules (fixes #302) | ||
| 1190 | for m in methods(includet) | ||
| 1191 | push!(JuliaInterpreter.compiled_methods, m) | ||
| 1192 | end | ||
| 1193 | # Set up a repository for methods defined at the REPL | ||
| 1194 | id = PkgId(nothing, "@REPL") | ||
| 1195 | pkgdatas[id] = pkgdata = PkgData(id, nothing) | ||
| 1196 | # Set the lookup callbacks | ||
| 1197 | CodeTracking.method_lookup_callback[] = get_def | ||
| 1198 | CodeTracking.expressions_callback[] = get_expressions | ||
| 1199 | |||
| 1200 | # Watch the manifest file for changes | ||
| 1201 | mfile = manifest_file() | ||
| 1202 | if mfile === nothing | ||
| 1203 | @warn "no Manifest.toml file found, static paths used" | ||
| 1204 | else | ||
| 1205 | wmthunk = Rescheduler(watch_manifest, (mfile,)) | ||
| 1206 | schedule(Task(wmthunk)) | ||
| 1207 | end | ||
| 1208 | return nothing | ||
| 1209 | end | ||
| 1210 | |||
| 1211 | function setup_atom(atommod::Module)::Nothing | ||
| 1212 | handlers = getfield(atommod, :handlers) | ||
| 1213 | for x in ["eval", "evalall", "evalshow", "evalrepl"] | ||
| 1214 | if haskey(handlers, x) | ||
| 1215 | old = handlers[x] | ||
| 1216 | Main.Atom.handle(x) do data | ||
| 1217 | revise() | ||
| 1218 | old(data) | ||
| 1219 | end | ||
| 1220 | end | ||
| 1221 | end | ||
| 1222 | return nothing | ||
| 1223 | end | ||
| 1224 | |||
| 1225 | include("precompile.jl") | ||
| 1226 | _precompile_() | ||
| 1227 | |||
| 1228 | end # module |