diff --git a/.github/workflows/Downstream.yml b/.github/workflows/Downstream.yml new file mode 100644 index 0000000..fb84981 --- /dev/null +++ b/.github/workflows/Downstream.yml @@ -0,0 +1,48 @@ +name: IntegrationTest +on: + push: + branches: [master] + tags: [v*] + pull_request: + +jobs: + test: + name: ${{ matrix.package.repo }}/${{ matrix.julia-version }} + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + julia-version: [1] + os: [ubuntu-latest] + package: + - {user: timholy, repo: Revise.jl} + + steps: + - uses: actions/checkout@v6 + - uses: julia-actions/setup-julia@v3 + with: + version: ${{ matrix.julia-version }} + - uses: julia-actions/julia-buildpkg@v1 + - name: Clone Downstream + uses: actions/checkout@v6 + with: + repository: ${{ matrix.package.user }}/${{ matrix.package.repo }} + path: downstream + - name: Load this and run the downstream tests + shell: julia --color=yes --project=downstream {0} + run: | + using Pkg + try + # force it to use this PR's version of the package + Pkg.develop(PackageSpec(path=".")) # resolver may fail with main deps + Pkg.update() + Pkg.test() # resolver may fail with test time deps + catch err + err isa Pkg.Resolve.ResolverError || rethrow() + # If we can't resolve that means this is incompatible by SemVer and this is fine + # It means we marked this as a breaking change, so we don't need to worry about + # Mistakenly introducing a breaking change, as we have intentionally made one + @info "Not compatible with this release. No problem." exception=err + exit(0) # Exit immediately, as a success + end diff --git a/docs/Manifest.toml b/docs/Manifest.toml index e99feee..293840e 100644 --- a/docs/Manifest.toml +++ b/docs/Manifest.toml @@ -1,6 +1,6 @@ # This file is machine-generated - editing it directly is not advised -julia_version = "1.12.0-beta1" +julia_version = "1.12.6" manifest_format = "2.0" project_hash = "9333156eea8af2fffc3b9e8bb1bfc75d6bb99ef7" @@ -27,10 +27,10 @@ uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" version = "1.11.0" [[deps.CodeTracking]] -deps = ["InteractiveUtils", "UUIDs"] -git-tree-sha1 = "062c5e1a5bf6ada13db96a4ae4749a4c2234f521" +deps = ["InteractiveUtils", "REPL", "UUIDs"] +git-tree-sha1 = "cfb7a2e89e245a9d5016b70323db412b3a7438d5" uuid = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2" -version = "1.3.9" +version = "3.0.2" [[deps.CodecZlib]] deps = ["TranscodingStreams", "Zlib_jll"] @@ -38,54 +38,70 @@ git-tree-sha1 = "962834c22b66e32aa10f7611c08c8ca4e20749a9" uuid = "944b1d66-785c-5afd-91f1-9de20f533193" version = "0.7.8" +[[deps.Compiler]] +git-tree-sha1 = "382d79bfe72a406294faca39ef0c3cef6e6ce1f1" +uuid = "807dbc54-b67e-4c79-8afb-eafe4df6f2e1" +version = "0.1.1" + +[[deps.CompilerSupportLibraries_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" +version = "1.3.0+1" + [[deps.Dates]] deps = ["Printf"] uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" version = "1.11.0" [[deps.DocStringExtensions]] -git-tree-sha1 = "e7b7e6f178525d17c720ab9c081e4ef04429f860" +git-tree-sha1 = "7442a5dfe1ebb773c29cc2962a8980f47221d76c" uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" -version = "0.9.4" +version = "0.9.5" [[deps.Documenter]] -deps = ["ANSIColoredPrinters", "AbstractTrees", "Base64", "CodecZlib", "Dates", "DocStringExtensions", "Downloads", "Git", "IOCapture", "InteractiveUtils", "JSON", "LibGit2", "Logging", "Markdown", "MarkdownAST", "Pkg", "PrecompileTools", "REPL", "RegistryInstances", "SHA", "TOML", "Test", "Unicode"] -git-tree-sha1 = "9d733459cea04dcf1c41522ec25c31576387be8a" +deps = ["ANSIColoredPrinters", "AbstractTrees", "Base64", "CodecZlib", "Dates", "DocStringExtensions", "Downloads", "Git", "IOCapture", "InteractiveUtils", "JSON", "Logging", "Markdown", "MarkdownAST", "Pkg", "PrecompileTools", "REPL", "RegistryInstances", "SHA", "TOML", "Test", "Unicode"] +git-tree-sha1 = "56e9c37b5e7c3b4f080ab1da18d72d5c290e184a" uuid = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -version = "1.10.1" +version = "1.17.0" [[deps.Downloads]] deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" -version = "1.6.0" +version = "1.7.0" [[deps.Expat_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "d55dffd9ae73ff72f1c0482454dcf2ec6c6c4a63" +git-tree-sha1 = "e6c4a6407a949e79a9d3f249bf49e6987c80e01f" uuid = "2e619515-83b5-522b-bb60-26c02a35a201" -version = "2.6.5+0" +version = "2.8.2+0" [[deps.FileWatching]] uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" version = "1.11.0" [[deps.Git]] -deps = ["Git_jll"] -git-tree-sha1 = "04eff47b1354d702c3a85e8ab23d539bb7d5957e" +deps = ["Git_LFS_jll", "Git_jll", "JLLWrappers", "OpenSSH_jll"] +git-tree-sha1 = "824a1890086880696fc908fe12a17bcf61738bd8" uuid = "d7ba0133-e1db-5d97-8f8c-041e4b3a1eb2" -version = "1.3.1" +version = "1.5.0" + +[[deps.Git_LFS_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "8c66e385d631bb934ff05e76d4a566c640c8df69" +uuid = "020c3dae-16b3-5ae5-87b3-4cb189e250b2" +version = "3.7.1+0" [[deps.Git_jll]] deps = ["Artifacts", "Expat_jll", "JLLWrappers", "LibCURL_jll", "Libdl", "Libiconv_jll", "OpenSSL_jll", "PCRE2_jll", "Zlib_jll"] -git-tree-sha1 = "2f6d6f7e6d6de361865d4394b802c02fc944fc7c" +git-tree-sha1 = "0dd4cfb426924210c8f42742751cbde74b27bfa3" uuid = "f8c6e375-362e-5223-8a59-34ff63f689eb" -version = "2.49.0+0" +version = "2.54.0+0" [[deps.IOCapture]] deps = ["Logging", "Random"] -git-tree-sha1 = "b6d6bfdd7ce25b0f9b2f6b3dd56b2673a66c8770" +git-tree-sha1 = "0ee181ec08df7d7c911901ea38baf16f755114dc" uuid = "b5f81e59-6552-4d32-b1f0-c071b021bf89" -version = "0.2.5" +version = "1.0.0" [[deps.InteractiveUtils]] deps = ["Markdown"] @@ -94,21 +110,27 @@ version = "1.11.0" [[deps.JLLWrappers]] deps = ["Artifacts", "Preferences"] -git-tree-sha1 = "a007feb38b422fbdab534406aeca1b86823cb4d6" +git-tree-sha1 = "7204148362dafe5fe6a273f855b8ccbe4df8173e" uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" -version = "1.7.0" +version = "1.8.0" [[deps.JSON]] -deps = ["Dates", "Mmap", "Parsers", "Unicode"] -git-tree-sha1 = "31e996f0a15c7b280ba9f76636b3ff9e2ae58c9a" +deps = ["Dates", "Logging", "Parsers", "PrecompileTools", "StructUtils", "UUIDs", "Unicode"] +git-tree-sha1 = "c89d196f5ffb64bfbf80985b699ea913b0d2c211" uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -version = "0.21.4" +version = "1.6.1" + + [deps.JSON.extensions] + JSONArrowExt = ["ArrowTypes"] + + [deps.JSON.weakdeps] + ArrowTypes = "31f734f8-188a-4ce0-8406-c8a06bd891cd" [[deps.JuliaInterpreter]] deps = ["CodeTracking", "InteractiveUtils", "Random", "UUIDs"] -git-tree-sha1 = "40237fa9a763c6e9e2465191bb09f7dab4b17593" +git-tree-sha1 = "cac745d4e5b7980d1121a549bc8aeb44b47cbd09" uuid = "aa1ae85d-cabe-5617-a682-6adf51b2e16a" -version = "0.10.0" +version = "0.11.3" [[deps.JuliaSyntaxHighlighting]] deps = ["StyledStrings"] @@ -128,7 +150,7 @@ version = "0.6.4" [[deps.LibCURL_jll]] deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll", "Zlib_jll", "nghttp2_jll"] uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" -version = "8.11.1+1" +version = "8.15.0+0" [[deps.LibGit2]] deps = ["LibGit2_jll", "NetworkOptions", "Printf", "SHA"] @@ -160,10 +182,10 @@ uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" version = "1.11.0" [[deps.LoweredCodeUtils]] -deps = ["JuliaInterpreter"] +deps = ["CodeTracking", "Compiler", "JuliaInterpreter"] path = ".." uuid = "6f1432cf-f94c-5a45-995e-cdbf5db27b0b" -version = "3.3.0" +version = "3.7.2" [[deps.Markdown]] deps = ["Base64", "JuliaSyntaxHighlighting", "StyledStrings"] @@ -172,26 +194,28 @@ version = "1.11.0" [[deps.MarkdownAST]] deps = ["AbstractTrees", "Markdown"] -git-tree-sha1 = "465a70f0fc7d443a00dcdc3267a497397b8a3899" +git-tree-sha1 = "93c718d892e73931841089cdc0e982d6dd9cc87b" uuid = "d0879d2d-cac2-40c8-9cee-1863dc0c7391" -version = "0.1.2" - -[[deps.Mmap]] -uuid = "a63ad114-7e13-5084-954f-fe012c677804" -version = "1.11.0" +version = "0.1.3" [[deps.MozillaCACerts_jll]] uuid = "14a3606d-f60d-562e-9121-12d972cd8159" -version = "2024.12.31" +version = "2025.11.4" [[deps.NetworkOptions]] uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" version = "1.3.0" +[[deps.OpenSSH_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "OpenSSL_jll", "Zlib_jll"] +git-tree-sha1 = "b862f484cf659efabffd601c5c920e981c942b63" +uuid = "9bd350c2-7e96-507f-8002-3f2e150b4e1b" +version = "10.4.1+0" + [[deps.OpenSSL_jll]] deps = ["Artifacts", "Libdl"] uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" -version = "3.0.16+0" +version = "3.5.4+0" [[deps.PCRE2_jll]] deps = ["Artifacts", "Libdl"] @@ -200,14 +224,14 @@ version = "10.44.0+1" [[deps.Parsers]] deps = ["Dates", "PrecompileTools", "UUIDs"] -git-tree-sha1 = "44f6c1f38f77cafef9450ff93946c53bd9ca16ff" +git-tree-sha1 = "32a4e09c5f29402573d673901778a0e03b0807b9" uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" -version = "2.8.2" +version = "2.8.6" [[deps.Pkg]] deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "p7zip_jll"] uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -version = "1.12.0" +version = "1.12.1" weakdeps = ["REPL"] [deps.Pkg.extensions] @@ -215,15 +239,15 @@ weakdeps = ["REPL"] [[deps.PrecompileTools]] deps = ["Preferences"] -git-tree-sha1 = "516f18f048a195409d6e072acf879a9f017d3900" +git-tree-sha1 = "edbeefc7a4889f528644251bdb5fc9ab5348bc2c" uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a" -version = "1.3.2" +version = "1.3.4" [[deps.Preferences]] deps = ["TOML"] -git-tree-sha1 = "9306f6085165d270f7e3db02af26a400d580f5c6" +git-tree-sha1 = "8b770b60760d4451834fe79dd483e318eee709c4" uuid = "21216c6a-2e73-6563-6e65-726566657250" -version = "1.4.3" +version = "1.5.2" [[deps.Printf]] deps = ["Unicode"] @@ -258,6 +282,22 @@ version = "1.11.0" uuid = "6462fe0b-24de-5631-8697-dd941f90decc" version = "1.11.0" +[[deps.StructUtils]] +deps = ["Dates", "UUIDs"] +git-tree-sha1 = "82bee338d650aa515f31866c460cb7e3bcef90b8" +uuid = "ec057cc2-7a8d-4b58-b3b3-92acb9f63b42" +version = "2.8.2" + + [deps.StructUtils.extensions] + StructUtilsMeasurementsExt = ["Measurements"] + StructUtilsStaticArraysCoreExt = ["StaticArraysCore"] + StructUtilsTablesExt = ["Tables"] + + [deps.StructUtils.weakdeps] + Measurements = "eff96d63-e80a-5855-80a2-b1b0885c5ab7" + StaticArraysCore = "1e83bf80-4336-4d27-bf5d-d5a4f845583c" + Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" + [[deps.StyledStrings]] uuid = "f489334b-da3d-4c2e-b8f0-e476e12c162b" version = "1.11.0" @@ -302,6 +342,6 @@ uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" version = "1.64.0+1" [[deps.p7zip_jll]] -deps = ["Artifacts", "Libdl"] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" -version = "17.5.0+2" +version = "17.7.0+0" diff --git a/docs/src/api.md b/docs/src/api.md index ea5a0bf..bd5976d 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -5,6 +5,7 @@ ```@docs signature methoddef! +methoddefs! rename_framemethods! bodymethod ``` diff --git a/docs/src/edges.md b/docs/src/edges.md index 2bab98f..aa24d36 100644 --- a/docs/src/edges.md +++ b/docs/src/edges.md @@ -79,7 +79,7 @@ julia> lwr = Meta.lower(Main, ex) and then extract the edges: ```julia -julia> edges = CodeEdges(lwr.args[1]) +julia> edges = CodeEdges(Main, lwr.args[1]) CodeEdges: s: assigned on [1, 16], depends on [15], and used by [12, 15] k: assigned on [2, 18], depends on [17], and used by [11, 17] @@ -188,7 +188,7 @@ Suppose we want to evaluate just the lines needed to compute `s`. We can find out which lines these are with ```julia -julia> isrequired = lines_required(:s, lwr.args[1], edges) +julia> isrequired = lines_required(GlobalRef(Main, :s), lwr.args[1], edges) 24-element BitArray{1}: 1 0 diff --git a/docs/src/signatures.md b/docs/src/signatures.md index d385f6d..1dfb3c4 100644 --- a/docs/src/signatures.md +++ b/docs/src/signatures.md @@ -90,8 +90,8 @@ end))) This reveals the *three* methods actually got defined: - one method of `f` with a single positional argument (this is the second 3-argument `:method` expression) -- a keyword-handling method that checks the names of supplied keyword arguments and fills in defaults (this is the third 3-argument `:method` expression). This method can be obtained from `Core.kwfunc(f)`, which returns a function named `f##kw`. -- a "keyword-body" method that actually does the work specifies by our function definition. This method gets called by the other two. (This is the first 3-argument `:method` expression.) +- a keyword-handling method that checks the names of supplied keyword arguments and fills in defaults (this is the third 3-argument `:method` expression). Its keyword-dispatch function can be obtained from `Core.kwfunc(f)` (on supported Julia versions this is `Core.kwcall`). +- a "keyword-body" method that actually does the work specified by our function definition. This method gets called by the other two. (This is the first 3-argument `:method` expression.) From examining the lowered code we might guess that this function is called `#f#2`. What happens if we try to get it? diff --git a/src/codeedges.jl b/src/codeedges.jl index 7d75804..a9926ca 100644 --- a/src/codeedges.jl +++ b/src/codeedges.jl @@ -190,7 +190,7 @@ end """ controller::SelectiveEvalController -When this object is passed as the `recurse` argument of `selective_eval!`, +When this object is passed as the `controller` argument of `selective_eval!`, the selective execution is adjusted as follows: - **Termination point**: In Julia's IR representation (`CodeInfo`), a terminal @@ -222,6 +222,7 @@ SelectiveEvalController() = SelectiveEvalController(BitSet(), CFGShortCut[]) inner::S isrequired::T controller::SelectiveEvalController + last_executed::Base.RefValue{Int} end An `JuliaInterpreter.Interpreter` that executes only the statements marked `true` in `isrequired`. @@ -234,7 +235,10 @@ struct SelectiveInterpreter{S<:Interpreter,T<:AbstractVector{Bool}} <: Interpret inner::S isrequired::T controller::SelectiveEvalController + last_executed::Base.RefValue{Int} end +SelectiveInterpreter(inner::S, isrequired::T, controller::SelectiveEvalController) where {S<:Interpreter,T<:AbstractVector{Bool}} = + SelectiveInterpreter(inner, isrequired, controller, Ref(0)) function namedkeys(cl::CodeLinks) ukeys = Set{GlobalRef}() @@ -495,7 +499,7 @@ function Base.show(io::IO, edges::CodeEdges) end """ - edges = CodeEdges(src::CodeInfo) + edges = CodeEdges(mod::Module, src::CodeInfo) Analyze `src` and determine the chain of dependencies. @@ -520,7 +524,7 @@ function CodeEdges(src::CodeInfo, cl::CodeLinks) emptylink = Links() emptylist = Int[] for (i, stmt) in enumerate(src.code) - # Identify line predecents for slots and named variables + # Identify line predecessors for slots and named variables if (lhs_rhs = get_lhs_rhs(stmt); lhs_rhs !== nothing) stmt = stmt::Expr lhs, _ = lhs_rhs @@ -731,6 +735,11 @@ end function lines_required!(isrequired::AbstractVector{Bool}, objs, src::CodeInfo, edges::CodeEdges, controller::SelectiveEvalController=SelectiveEvalController(); norequire = ()) + # A controller describes one particular slice. Recompute it from scratch so + # callers can safely reuse the same object for another slice. + empty!(controller.termination_points) + empty!(controller.shortcuts) + # Mark any requested objects (their lines of assignment) objs = add_requests!(isrequired, objs, edges, norequire) @@ -999,7 +1008,7 @@ function record_termination_points!(controller::SelectiveEvalController, isrequi nothing end -# Do a traveral of "numbered" predecessors and find statement ranges and names of type definitions +# Do a traversal of "numbered" predecessors and find statement ranges and names of type definitions function find_typedefs(src::CodeInfo) typedef_blocks, typedef_names = UnitRange{Int}[], Symbol[] i = 1 @@ -1054,21 +1063,42 @@ function add_typedefs!(isrequired, src::CodeInfo, edges::CodeEdges, (typedef_blo while idx < length(stmts) stmt = stmts[idx] isrequired[idx] || (idx += 1; continue) + intypedef = false for (typedefr, typedefn) in zip(typedef_blocks, typedef_names) if idx ∈ typedefr ireq = view(isrequired, typedefr) if !all(ireq) changed = true ireq .= true - # Also mark any by-type constructor(s) associated with this typedef - var = get(edges.byname, typedefn, nothing) - if var !== nothing - for s in var.succs - s ∈ norequire && continue - stmt2 = stmts[s] - if ismethod(stmt2) && (fname = method_name(stmt2::Expr); fname === false || fname === nothing) - isrequired[s] = true - end + end + # Also mark any by-type constructor(s) associated with this typedef. + # Julia 1.12+ emits default constructors as a `_defaultctors` call + # that directly consumes the type-body result. + for s in edges.succs[last(typedefr)] + s ∈ norequire && continue + if is_defaultctors_call(stmts[s]) && !isrequired[s] + isrequired[s] = true + changed = true + end + end + # Older lowerings emit anonymous `:method` expressions associated + # with the type's global binding. + typedefoffset = findfirst(i -> istypedef(getrhs(stmts[i])), typedefr) + typedefidx = typedefoffset === nothing ? nothing : first(typedefr) + typedefoffset - 1 + typedefstmt = typedefidx === nothing ? nothing : getrhs(stmts[typedefidx]) + typedefmod = isexpr(typedefstmt, :call) && typedefstmt.args[2] isa Module ? + typedefstmt.args[2] : nothing + var = typedefmod === nothing ? nothing : + get(edges.byname, GlobalRef(typedefmod, typedefn), nothing) + if var !== nothing + for s in var.succs + s ∈ norequire && continue + stmt2 = stmts[s] + if (ismethod(stmt2) && + (fname = method_name(stmt2::Expr); fname === false || fname === nothing) && + !isrequired[s]) + isrequired[s] = true + changed = true end end end @@ -1079,9 +1109,11 @@ function add_typedefs!(isrequired, src::CodeInfo, edges::CodeEdges, (typedef_blo isrequired[ctor] = true end idx = last(typedefr) + 1 - continue + intypedef = true + break end end + intypedef && continue # Anonymous functions may not yet include the method definition if isanonymous_typedef(stmt) i = idx + 1 @@ -1160,6 +1192,7 @@ function JuliaInterpreter.step_expr!(interp::SelectiveInterpreter, frame::Frame, end end if interp.isrequired[pc] + interp.last_executed[] = pc step_expr!(interp.inner, frame, istoplevel) else next_or_nothing!(interp, frame) @@ -1172,10 +1205,10 @@ function JuliaInterpreter.get_return(interp::SelectiveInterpreter, frame::Frame) if interp.isrequired[pc] return lookup_return(interp.inner, frame, node) end - else - if isassigned(frame.framedata.ssavalues, pc) - return frame.framedata.ssavalues[pcexec] - end + end + pcexec = interp.last_executed[] + if pcexec != 0 && isassigned(frame.framedata.ssavalues, pcexec) + return frame.framedata.ssavalues[pcexec] end return nothing end @@ -1207,7 +1240,7 @@ Note that the interpreter does not recurse into callees, so there is currently n interprocedural selective evaluation. This will return either a `BreakpointRef`, the value obtained from the last executed statement -(if stored to `frame.framedata.ssavlues`), or `nothing`. +(if stored to `frame.framedata.ssavalues`), or `nothing`. Typically, assignment to a variable binding does not result in an ssa store by JuliaInterpreter. """ function selective_eval!( diff --git a/src/signatures.jl b/src/signatures.jl index efe123c..409f08c 100644 --- a/src/signatures.jl +++ b/src/signatures.jl @@ -344,9 +344,7 @@ Rename the gensymmed methods in `frame` to match those that are currently active The issues are described in https://github.com/JuliaLang/julia/issues/30908. `frame` will be modified in-place as needed. -Returns a vector of `name=>start:stop` pairs specifying the range of lines in `frame` -at which method definitions occur. In some cases there may be more than one method with -the same name in the `start:stop` range. +Returns a dictionary mapping each method's lowered `GlobalRef` to its [`MethodInfo`](@ref). """ function rename_framemethods! end @@ -389,7 +387,7 @@ rename_framemethods!(frame::Frame) = rename_framemethods!(RecursiveInterpreter() Scans forward from `pc` in `frame` until a method is found that calls `name`. `pctop` points to the beginning of that method's signature. -`isgen` is true if `name` corresponds to sa GeneratedFunctionStub. +`isgen` is true if `name` corresponds to a generated-function stub. Alternatively, this returns `nothing` if `pc` does not appear to point to either a keyword or generated method. @@ -690,14 +688,24 @@ methoddef!(signatures::Vector{MethodInfoKey}, frame::Frame, pc::Int; define::Boo methoddef!(signatures::Vector{MethodInfoKey}, frame::Frame; define::Bool=true) = methoddef!(RecursiveInterpreter(), signatures, frame; define) +""" + ret = methoddefs!([interp::Interpreter=RecursiveInterpreter()], signatures, frame; define=true) + +Process all method definitions at or after `frame.pc`, appending each method-table/signature +pair to `signatures`. As with [`methoddef!`](@ref), set `define=false` to extract signatures +without evaluating methods that are already defined. Returns the next program counter, normally +`nothing` after reaching the end of the frame. +""" function methoddefs!(interp::Interpreter, signatures::Vector{MethodInfoKey}, frame::Frame, pc::Int; define::Bool=true) ret = methoddef!(interp, signatures, frame, pc; define) - pc = ret === nothing ? ret : ret[1] + ret === nothing && return nothing + pc = ret[1] return _methoddefs!(interp, signatures, frame, pc; define) end function methoddefs!(interp::Interpreter, signatures::Vector{MethodInfoKey}, frame::Frame; define::Bool=true) ret = methoddef!(interp, signatures, frame; define) - pc = ret === nothing ? ret : ret[1] + ret === nothing && return nothing + pc = ret[1] return _methoddefs!(interp, signatures, frame, pc; define) end methoddefs!(signatures::Vector{MethodInfoKey}, frame::Frame, pc::Int; define::Bool=true) = @@ -705,7 +713,7 @@ methoddefs!(signatures::Vector{MethodInfoKey}, frame::Frame, pc::Int; define::Bo methoddefs!(signatures::Vector{MethodInfoKey}, frame::Frame; define::Bool=true) = methoddefs!(RecursiveInterpreter(), signatures, frame; define) -function _methoddefs!(interp::Interpreter, signatures::Vector{MethodInfoKey}, frame::Frame, pc::Int; define::Bool=define) +function _methoddefs!(interp::Interpreter, signatures::Vector{MethodInfoKey}, frame::Frame, pc::Int; define::Bool=true) while pc !== nothing stmt = pc_expr(frame, pc) if !ismethod(stmt) diff --git a/src/utils.jl b/src/utils.jl index e521d60..6907ece 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -16,7 +16,7 @@ end """ iscallto(stmt, name, src) -Returns `true` is `stmt` is a call expression to `name`. +Returns `true` if `stmt` is a call expression to `name`. """ function iscallto(@nospecialize(stmt), mod::Module, name::GlobalRef, src) if isa(stmt, Expr) @@ -168,8 +168,10 @@ function ismethod_with_name(src, stmt, target::AbstractString; reentrant::Bool=f isdone = true end end - # On Julia 1.6 we have to add escaping (CBinding makes function names like "(S)") - target = escape_string(target, "()") + # Escape all regular-expression metacharacters. Function names can themselves + # be operators (for example, `+`) or contain punctuation (for example, CBinding's + # names like `(S)`). + target = escape_string(target, "\\.^\$|?*+()[]{}") return match(Regex("(^|#)$target(\$|#)"), isa(name, GlobalRef) ? string(name.name) : string(name)) !== nothing end @@ -346,7 +348,7 @@ end showempty(list) = isempty(list) ? '∅' : list -# Smooth the transition between CC and Base (not requried for v1.12 and above) +# Smooth the transition between CC and Base (not required for v1.12 and above) rng(bb::BasicBlock) = (r = bb.stmts; return CC.first(r):CC.last(r)) function pushall!(dest, src) diff --git a/test/codeedges.jl b/test/codeedges.jl index bb2b7df..fe4ce02 100644 --- a/test/codeedges.jl +++ b/test/codeedges.jl @@ -3,7 +3,7 @@ module codeedges using LoweredCodeUtils using LoweredCodeUtils.JuliaInterpreter using LoweredCodeUtils: CC -using LoweredCodeUtils: callee_matches, istypedef, exclude_named_typedefs, is_defaultctors_call +using LoweredCodeUtils: callee_matches, exclude_named_typedefs, is_defaultctors_call, istypedef using JuliaInterpreter: is_global_ref, is_quotenode using Test @@ -67,7 +67,7 @@ module ModSelective end # Check that the result of direct evaluation agrees with selective evaluation Core.eval(ModEval, ex) isrequired = lines_required(GlobalRef(ModSelective, :x), src, edges) - # theere is too much diversity in lowering across Julia versions to make it useful to test `sum(isrequired)` + # there is too much diversity in lowering across Julia versions to make it useful to test `sum(isrequired)` selective_eval_fromstart!(frame, isrequired, #=istoplevel=#true) @test ModSelective.x === ModEval.x @test allmissing(ModSelective, (:y, :z, :a, :b, :k)) @@ -255,6 +255,30 @@ module ModSelective end @test ModSelective.x == 5 @test !isdefined(ModSelective, :yy) + # A controller can be reused for a different slice of the same source. + # Its shortcuts and termination points must describe only the latest slice. + let mod = Module(:ModReusedController) + ex = quote + flag = true + if flag + x = 1 + else + x = 2 + end + y = 3 + end + src = Meta.lower(mod, ex).args[1] + edges = CodeEdges(mod, src) + controller = SelectiveEvalController() + lines_required(GlobalRef(mod, :y), src, edges, controller) + isrequired = lines_required(GlobalRef(mod, :x), src, edges, controller) + selective_eval_fromstart!( + LoweredCodeUtils.RecursiveInterpreter(), Frame(mod, src), + isrequired, controller, true) + @test @invokelatest(mod.x) == 1 + @test !@invokelatest(isdefined(mod, :y)) + end + # Inactive statements at the beginning of a terminal block shouldn't terminate # selective evaluation before later required statements in the same block. let mod = Module(:ModTerminationPointAfterRequired) @@ -280,6 +304,78 @@ module ModSelective end @test @invokelatest(mod.hits) == [2] end + # A reused frame may contain an SSA value at an inactive termination point. + # Return the value from this selective run, not that stale value. + let mod = Module(:ModGetReturnAtTerminationPoint) + ex = quote + a = 1 + sin(0.3) + end + frame = Frame(mod, ex) + JuliaInterpreter.finish!(frame, #=istoplevel=#true) # first run: all ssavalues get stored + src = frame.framecode.src + edges = CodeEdges(mod, src) + controller = SelectiveEvalController() + isrequired = lines_required(GlobalRef(mod, :a), src, edges, controller) + ret = selective_eval_fromstart!( + LoweredCodeUtils.RecursiveInterpreter(), frame, isrequired, controller, true) + @test frame.pc ∈ controller.termination_points + @test isassigned(frame.framedata.ssavalues, frame.pc) + @test ret === nothing + end + + # When the final executed statement does store an SSA value, return that value + # even if execution subsequently skips to an inactive return statement. + let mod = Module(:ModGetLastExecutedValue) + src = Meta.lower(mod, quote + Base.identity(42) + Base.identity(99) + end).args[1] + isrequired = falses(length(src.code)) + isrequired[1] = true + frame = Frame(mod, src) + ret = selective_eval_fromstart!(frame, isrequired, #=istoplevel=#true) + @test isassigned(frame.framedata.ssavalues, 1) + @test ret === frame.framedata.ssavalues[1] + end + + # Requiring one type definition must not mark an unrelated one that follows it. + # (On Julia ≤1.11 the lowered blocks of two consecutive `abstract type`s are + # adjacent, and a loop-control bug in `add_typedefs!` marked the second block + # in full whenever the first one was required.) + let mod = Module(:ModConsecutiveTypedefs) + lwr = Meta.lower(mod, quote + abstract type AbsA end + abstract type AbsB end + end) + src = lwr.args[1] + blocks, names = LoweredCodeUtils.find_typedefs(src) + @test names == [:AbsA, :AbsB] + edges = CodeEdges(mod, src) + isrequired = lines_required(first(blocks[1]) + 1, src, edges) + @test !any(isrequired[blocks[2]]) + selective_eval_fromstart!(Frame(mod, src), isrequired, #=istoplevel=#true) + @test @invokelatest(isdefined(mod, :AbsA)) + @test !@invokelatest(isdefined(mod, :AbsB)) + end + + # Julia 1.12+ lowers default constructors to a single `_defaultctors` call. + # Requiring the type definition must require that call too. + let mod = Module(:ModRequiredDefaultConstructors) + src = Meta.lower(mod, :(struct WithDefaultConstructor; value; end)).args[1] + edges = CodeEdges(mod, src) + isrequired = lines_required!(istypedef.(src.code), src, edges) + ctorpc = findfirst(is_defaultctors_call, src.code) + @static if VERSION >= v"1.12-" + @test ctorpc !== nothing + @test isrequired[ctorpc] + end + selective_eval_fromstart!(Frame(mod, src), isrequired, #=istoplevel=#true) + T = @invokelatest(mod.WithDefaultConstructor) + value = Base.invokelatest(T, 7) + @test value.value == 7 + end + # Control-flow in an abstract type definition ex = :(abstract type StructParent{T, N} <: AbstractArray{T, N} end) frame = Frame(ModSelective, ex) diff --git a/test/signatures.jl b/test/signatures.jl index 34c51ea..ed41389 100644 --- a/test/signatures.jl +++ b/test/signatures.jl @@ -183,6 +183,18 @@ bodymethtest5(x, y=Dict(1=>2)) = 5 ret = methoddef!(empty!(signatures), frame; define=true) @test isempty(signatures) @test ret === nothing + + # `methoddefs!` must also stop cleanly at a bare forward declaration. + frame = Frame(Lowering, :(function another_nomethod end)) + ret = methoddefs!(empty!(signatures), frame; define=true) + @test isempty(signatures) + @test ret === nothing + + # Operator names contain regular-expression metacharacters. + let mod = Module(:OperatorMethodName) + src = Frame(mod, :(+(x, y) = x)).framecode.src + @test any(stmt -> LoweredCodeUtils.ismethod_with_name(src, stmt, "+"), src.code) + end frame = Frame(Lowering, :(function amethod() nothing end)) ret = methoddef!(empty!(signatures), frame; define=true) @test !isempty(signatures)