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 |