From be93b5f0aad9388d102910252fcf2d93f55fe386 Mon Sep 17 00:00:00 2001 From: Luc Briand <34173752+Keluaa@users.noreply.github.com> Date: Fri, 19 Apr 2024 19:40:37 +0200 Subject: [PATCH] Compare from functions signatures --- Project.toml | 5 +++- src/CodeDiffs.jl | 4 +-- src/compare.jl | 68 ++++++++++++++++++++++++++++++++++++++++++++++++ test/runtests.jl | 57 ++++++++++++++++++++++++++++++++++++---- 4 files changed, 126 insertions(+), 8 deletions(-) diff --git a/Project.toml b/Project.toml index b1d1904..3c47f37 100644 --- a/Project.toml +++ b/Project.toml @@ -4,6 +4,7 @@ authors = ["Luc Briand <34173752+Keluaa@users.noreply.github.com> and contributo version = "1.0.0-DEV" [deps] +CodeTracking = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2" DeepDiffs = "ab62b9b5-e342-54a8-a765-a90f495de1a6" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" @@ -13,6 +14,7 @@ WidthLimitedIO = "b8c1c048-cf81-46c6-9da0-18c1d99e41f2" [compat] Aqua = "0.7" +CodeTracking = "1" DeepDiffs = "1" InteractiveUtils = "1" MacroTools = "0.5" @@ -29,7 +31,8 @@ DeepDiffs = "ab62b9b5-e342-54a8-a765-a90f495de1a6" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" OhMyREPL = "5fb14364-9ced-5910-84b2-373655c76a03" ReferenceTests = "324d217c-45ce-50fc-942e-d289b448e8cf" +Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Aqua", "InteractiveUtils", "OhMyREPL", "ReferenceTests", "Test"] +test = ["Aqua", "InteractiveUtils", "OhMyREPL", "ReferenceTests", "Revise", "Test"] diff --git a/src/CodeDiffs.jl b/src/CodeDiffs.jl index 5bd833f..ce89157 100644 --- a/src/CodeDiffs.jl +++ b/src/CodeDiffs.jl @@ -1,10 +1,9 @@ module CodeDiffs # TODO: option to ignore differences in code comments (such as when comparing methods in different worlds) -# TODO: add `using CodeTracking: definition`, then do like `Cthuhlu.jl` to retrive the function def from its call: https://github.com/JuliaDebug/Cthulhu.jl/blob/9ba8bfc53efed453cb150c9f3e4c279521c5cb17/src/codeview.jl#L54C9-L54C33 # TODO: GPU assembly / LLVM IR support -# TODO: explain in the docs how to interface with this package +using CodeTracking using DeepDiffs using InteractiveUtils using MacroTools @@ -16,6 +15,7 @@ export @code_diff const ANSI_REGEX = r"(?>\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]))+" const OhMYREPL_PKG_ID = Base.PkgId(Base.UUID("5fb14364-9ced-5910-84b2-373655c76a03"), "OhMyREPL") +const Revise_PKG_ID = Base.PkgId(Base.UUID("295af30f-e4ad-537b-8983-00126c2a3abe"), "Revise") include("CodeDiff.jl") include("compare.jl") diff --git a/src/compare.jl b/src/compare.jl index 0231b3d..9a85d49 100644 --- a/src/compare.jl +++ b/src/compare.jl @@ -215,6 +215,7 @@ end function method_instance(sig, world) + @nospecialize(sig) @static if VERSION < v"1.10" mth_match = Base._which(sig, world) else @@ -224,6 +225,12 @@ function method_instance(sig, world) end +function method_instance(f, types, world) + @nospecialize(f, types) + return method_instance(Base.signature_type(f, types), world) +end + + """ compare_code_native( f::Base.Callable, types::Type{<:Tuple}, world₁, world₂; @@ -500,6 +507,7 @@ function compare_ast(code₁::Markdown.MD, code₂::Markdown.MD; color=true) return compare_show(code₁, code₂; color, force_no_ansi=true) end + function compare_ast(code₁::AbstractString, code₂::AbstractString; color=true) code_md₁ = Markdown.MD(Markdown.julia, Markdown.Code("julia", code₁)) code_md₂ = Markdown.MD(Markdown.julia, Markdown.Code("julia", code₂)) @@ -507,6 +515,65 @@ function compare_ast(code₁::AbstractString, code₂::AbstractString; color=tru end +function method_to_ast(method::Method) + ast = CodeTracking.definition(Expr, method) + if isnothing(ast) + if !haskey(Base.loaded_modules, Revise_PKG_ID) + error("cannot retrieve the AST definition of `$(method.name)` as Revise.jl is not loaded") + else + error("could not retrieve the AST definition of `$(method.sig)` at world age $(method.primary_world)") + end + end + return ast +end + +method_to_ast(mi::Core.MethodInstance) = method_to_ast(mi.def) + +function method_to_ast(f::Base.Callable, types::Type{<:Tuple}, world=Base.get_world_counter()) + @nospecialize(f, types) + sig = Base.signature_type(f, types) + mi = method_instance(sig, world) + return method_to_ast(mi) +end + + +""" + compare_ast( + f₁::Base.Callable, types₁::Type{<:Tuple}, + f₂::Base.Callable, types₂::Type{<:Tuple}; + color=true, kwargs... + ) + +Retrieve the AST for the definitions of the methods matching the calls to `f₁` and `f₂` +using [`CodeTracking.jl`](https://github.com/timholy/CodeTracking.jl), then compare them. + +For `CodeTracking.jl` to work, [`Revise.jl`](https://github.com/timholy/Revise.jl) must be +loaded. +""" +function compare_ast( + f₁::Base.Callable, types₁::Type{<:Tuple}, + f₂::Base.Callable, types₂::Type{<:Tuple}; + kwargs... +) + @nospecialize(f₁, types₁, f₂, types₂) + code₁ = method_to_ast(f₁, types₁) + code₂ = method_to_ast(f₂, types₂) + return compare_ast(code₁, code₂; kwargs...) +end + + +function compare_ast( + f::Base.Callable, types::Type{<:Tuple}, world₁::Integer, world₂::Integer; + kwargs... +) + @nospecialize(f, types) + # While this does work if both versions of `f` are defined in the REPL at different + # lines, this isn't testable. + # This is here solely to have a homogenous interface. + error("Revise.jl does not keep track of previous definitions: cannot compare") +end + + """ code_diff(::Val{:ast}, code₁, code₂; kwargs...) @@ -537,6 +604,7 @@ code_diff(code₁, code₂; type::Symbol=:native, kwargs...) = code_diff(::Val{:native}, f₁, types₁, f₂, types₂; kwargs...) = compare_code_native(f₁, types₁, f₂, types₂; kwargs...) code_diff(::Val{:llvm}, f₁, types₁, f₂, types₂; kwargs...) = compare_code_llvm(f₁, types₁, f₂, types₂; kwargs...) code_diff(::Val{:typed}, f₁, types₁, f₂, types₂; kwargs...) = compare_code_typed(f₁, types₁, f₂, types₂; kwargs...) +code_diff(::Val{:ast}, f₁, types₁, f₂, types₂; kwargs...) = compare_ast(f₁, types₁, f₂, types₂; kwargs...) code_diff(code₁::Tuple, code₂::Tuple; type::Symbol=:native, kwargs...) = code_diff(Val(type), code₁..., code₂...; kwargs...) diff --git a/test/runtests.jl b/test/runtests.jl index d29fc03..760664f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,8 +1,10 @@ + using Aqua using CodeDiffs using DeepDiffs using InteractiveUtils using ReferenceTests +using Revise using Test @@ -39,6 +41,22 @@ macro no_overwrite_warning(expr) end +function eval_for_revise(str, path=tempname(), init=true) + open(path, "w") do file + println(file, str) + end + + if init + Revise.includet(path) + else + main_pkg_data = Revise.pkgdatas[Base.PkgId(nothing, "Main")] + Revise.revise_file_now(main_pkg_data, path) + end + + return path +end + + # OhMyREPL is quite reluctant from loading its Markdown highlighting overload in a testing # environment. See https://github.com/KristofferC/OhMyREPL.jl/blob/b0071f5ee785a81ca1e69a561586ff270b4dc2bb/src/OhMyREPL.jl#L106 prev = jl_options_overload(:isinteractive, Int8(1)) @@ -46,8 +64,8 @@ prev = jl_options_overload(:isinteractive, Int8(1)) jl_options_overload(:isinteractive, prev) -# Disable printing diffs to stdout by setting `ENV["TEST_PRINT_DIFFS"] = false` -const TEST_PRINT_DIFFS = parse(Bool, get(ENV, "TEST_PRINT_DIFFS", "true")) +# Enable printing diffs to stdout only in CI by default +const TEST_PRINT_DIFFS = parse(Bool, get(ENV, "TEST_PRINT_DIFFS", get(ENV, "CI", "false"))) const TEST_IO = TEST_PRINT_DIFFS ? stdout : IOContext(IOBuffer(), stdout) @@ -117,8 +135,10 @@ end end @testset "Basic function" begin + eval_for_revise(""" f1() = 1 f2() = 2 + """) @testset "Typed" begin diff = CodeDiffs.compare_code_typed(f1, Tuple{}, f1, Tuple{}; color=false) @@ -149,6 +169,15 @@ end @test !CodeDiffs.issame(diff) @test diff == (@code_diff type=:native color=false f1() f2()) end + + @testset "AST" begin + diff = CodeDiffs.compare_ast(f1, Tuple{}, f1, Tuple{}; color=false) + @test CodeDiffs.issame(diff) + + diff = CodeDiffs.compare_ast(f1, Tuple{}, f2, Tuple{}; color=false) + @test !CodeDiffs.issame(diff) + @test diff == (@code_diff type=:ast color=false f1() f2()) + end end @testset "Changes" begin @@ -220,6 +249,15 @@ end println(TEST_IO) end + @testset "AST" begin + diff = CodeDiffs.compare_ast(f₁, args₁, f₂, args₂; color=true) + @test findfirst(CodeDiffs.ANSI_REGEX, diff.before) === nothing + @test !endswith(diff.before, '\n') && !endswith(diff.after, '\n') + println(TEST_IO, "\nAST: $(nameof(f₁)) vs. $(nameof(f₂))") + printstyled(TEST_IO, display_str(diff; columns=120)) + println(TEST_IO) + end + @testset "Line numbers" begin diff = CodeDiffs.compare_code_typed(f₁, args₁, f₂, args₂; color=false) @test findfirst(CodeDiffs.ANSI_REGEX, diff.before) === nothing @@ -232,11 +270,14 @@ end end @testset "f1" begin + eval_for_revise(""" f() = 1 + """) test_cmp_display(f, Tuple{}, f, Tuple{}) end @testset "saxpy" begin + eval_for_revise(""" function saxpy(r, a, x, y) for i in eachindex(r) r[i] = a * x[i] + y[i] @@ -248,6 +289,7 @@ end r[i] = a * x[i] + y[i] end end + """) saxpy_args = Tuple{Vector{Int}, Int, Vector{Int}, Vector{Int}} test_cmp_display(saxpy, saxpy_args, saxpy_simd, saxpy_args) @@ -311,10 +353,11 @@ end end @testset "World age" begin - @no_overwrite_warning @eval begin - f() = 1 + @no_overwrite_warning begin + file_name = eval_for_revise("f() = 1") w₁ = Base.get_world_counter() - f() = 2 + + eval_for_revise("f() = 2", file_name, false) w₂ = Base.get_world_counter() end @@ -345,6 +388,10 @@ end diff = CodeDiffs.compare_code_native(f, Tuple{}, w₁, w₂; color=false, debuginfo=:none) @test !CodeDiffs.issame(diff) end + + @testset "AST" begin + @test_throws ErrorException CodeDiffs.compare_ast(f, Tuple{}, w₁, w₁; color=false) + end end @testset "Tabs" begin