diff --git a/docs/src/index.md b/docs/src/index.md index c88fa54..35fe131 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -11,9 +11,7 @@ Supports: - native CPU assembly (output of `@code_native`, highlighted by `InteractiveUtils.print_native`) - LLVM IR (output of `@code_llvm`, highlighted by `InteractiveUtils.print_llvm`) - Typed Julia IR (output of `@code_typed`, highlighted through the `Base.show` method of `Core.CodeInfo`) - - Julia AST (an `Expr`), highlighting is done with: - - OhMyREPL.jl's Julia syntax highlighting in Markdown code blocks - - (Julia ≥ v1.11) [JuliaSyntaxHighlighting.jl](https://github.com/JuliaLang/JuliaSyntaxHighlighting.jl) + - Julia AST (an `Expr`), highlighting is done with OhMyREPL.jl's Julia syntax highlighting in Markdown code blocks The [`@code_diff`](@ref) macro is the main entry point. If possible, the code type will be detected automatically, otherwise add e.g. `type=:llvm` for LLVM IR comparison: @@ -56,29 +54,42 @@ julia> @code_diff type=:llvm debuginfo=:none color=false f1(1) f2(1) 5 } ┃ } 5 ``` -# Main functions +# Comparison entry points ```@docs -CodeDiff -compare_code_native -compare_code_llvm -compare_code_typed -compare_ast -code_diff(::Any, ::Any) -code_diff(::Val{:ast}, ::Any, ::Any) @code_diff +code_diff +code_for_diff +CodeDiff ``` -# Display functions +# Code fetching ```@docs -optimize_line_changes! -replace_llvm_module_name -side_by_side_diff +code_native +code_llvm +code_typed +code_ast +get_code +``` + +# Highlighting + +```@docs +code_highlighter ``` -# Internals +# Cleanup ```@docs +cleanup_code +replace_llvm_module_name LLVM_MODULE_NAME_REGEX ``` + +# Diff display + +```@docs +optimize_line_changes! +side_by_side_diff +``` diff --git a/src/CodeDiffs.jl b/src/CodeDiffs.jl index ce89157..dd55c85 100644 --- a/src/CodeDiffs.jl +++ b/src/CodeDiffs.jl @@ -11,13 +11,16 @@ using Markdown using StringDistances using WidthLimitedIO -export @code_diff +export @code_diff, 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("get_code.jl") +include("highlighting.jl") +include("cleanup.jl") include("compare.jl") include("display.jl") diff --git a/src/cleanup.jl b/src/cleanup.jl new file mode 100644 index 0000000..6a6d0d8 --- /dev/null +++ b/src/cleanup.jl @@ -0,0 +1,78 @@ + +""" + LLVM_MODULE_NAME_REGEX + +Should match the LLVM module of any function which does not have any of `'",;-` or spaces +in it. + +It is `'get_function_name'`, in `'julia/src/codegen.cpp'` which builds the function name +for the LLVM module used to get the function code. The regex is built to match any output +from that function. +Since the `'globalUniqueGeneratedNames'` counter (the number at the end of the module name) +is incremented at each call to `'get_function_name'`, and since `code_llvm` or `code_native` +forces a compilation, it should be guaranteed that the match with the highest number at +the end is the name of our function in `code`. +""" +const LLVM_MODULE_NAME_REGEX = r"(?>julia|japi3|japi1)_([^\"\s,;\-']*)_(\d+)" + + +""" + replace_llvm_module_name(code::AbstractString) + +Remove in `code` the trailing numbers in the LLVM module names, e.g. `"julia_f_2007" => "f"`. +This allows to remove false differences when comparing raw code, since each call to +`code_native` (or `code_llvm`) triggers a new compilation using an unique LLVM module name, +therefore each consecutive call is different even though the actual code does not +change. + +```jldoctest; setup = :(using InteractiveUtils; import CodeDiffs: replace_llvm_module_name) +julia> f() = 1 +f (generic function with 1 method) + +julia> buf = IOBuffer(); + +julia> code_native(buf, f, Tuple{}) # Equivalent to `@code_native f()` + +julia> code₁ = String(take!(buf)); + +julia> code_native(buf, f, Tuple{}) + +julia> code₂ = String(take!(buf)); + +julia> code₁ == code₂ # Different LLVM module names... +false + +julia> replace_llvm_module_name(code₁) == replace_llvm_module_name(code₂) # ...but same code +true +``` +""" +replace_llvm_module_name(code::AbstractString) = replace(code, LLVM_MODULE_NAME_REGEX => s"\1") + + +""" + replace_llvm_module_name(code::AbstractString, function_name) + +Replace only LLVM module names for `function_name`. +""" +function replace_llvm_module_name(code::AbstractString, function_name) + function_name = string(function_name) + if Sys.islinux() && startswith(function_name, '@') + # See 'get_function_name' in 'julia/src/codegen.cpp' + function_name = function_name[2:end] + end + func_re = Regex("(?>julia|japi3|japi1)_\\Q$(function_name)\\E_(\\d+)") + return replace(code, func_re => function_name) +end + + +""" + cleanup_code(::Val{code_type}, code) + +Perform minor changes to `code` to improve readability and the quality of the differences. + +Currently only [`replace_llvm_module_name`](@ref) is applied to `:native` and `:llvm` code. +""" +cleanup_code(_, c) = c + +cleanup_code(::Val{:native}, c) = replace_llvm_module_name(c) +cleanup_code(::Val{:llvm}, c) = replace_llvm_module_name(c) diff --git a/src/compare.jl b/src/compare.jl index ad237e9..7d8d3d5 100644 --- a/src/compare.jl +++ b/src/compare.jl @@ -1,689 +1,172 @@ -""" - LLVM_MODULE_NAME_REGEX - -Should match the LLVM module of any function which does not have any of `'",;-` or spaces -in it. - -It is `'get_function_name'`, in `'julia/src/codegen.cpp'` which builds the function name -for the LLVM module used to get the function code. The regex is built to match any output -from that function. -Since the `'globalUniqueGeneratedNames'` counter (the number at the end of the module name) -is incremented at each call to `'get_function_name'`, and since `code_llvm` or `code_native` -forces a compilation, it should be guaranteed that the match with the highest number at -the end is the name of our function in `code`. -""" -const LLVM_MODULE_NAME_REGEX = r"(?>julia|japi3|japi1)_([^\"\s,;\-']*)_(\d+)" - - -""" - replace_llvm_module_name(code::AbstractString) - -Remove in `code` the trailing numbers in the LLVM module names, e.g. `"julia_f_2007" => "f"`. -This allows to remove false differences when comparing raw code, since each call to -`code_native` (or `code_llvm`) triggers a new compilation using an unique LLVM module name, -therefore each consecutive call is different even though the actual code does not -change. - -```jldoctest; setup = :(using InteractiveUtils; import CodeDiffs: replace_llvm_module_name) -julia> f() = 1 -f (generic function with 1 method) - -julia> buf = IOBuffer(); - -julia> code_native(buf, f, Tuple{}) # Equivalent to `@code_native f()` - -julia> code₁ = String(take!(buf)); - -julia> code_native(buf, f, Tuple{}) - -julia> code₂ = String(take!(buf)); - -julia> code₁ == code₂ # Different LLVM module names... -false - -julia> replace_llvm_module_name(code₁) == replace_llvm_module_name(code₂) # ...but same code -true -``` -""" -replace_llvm_module_name(code::AbstractString) = replace(code, LLVM_MODULE_NAME_REGEX => s"\1") - - -""" - replace_llvm_module_name(code::AbstractString, function_name) - -Replace only LLVM module names for `function_name`. -""" -function replace_llvm_module_name(code::AbstractString, function_name) - function_name = string(function_name) - if Sys.islinux() && startswith(function_name, '@') - # See 'get_function_name' in 'julia/src/codegen.cpp' - function_name = function_name[2:end] - end - func_re = Regex("(?>julia|japi3|japi1)_\\Q$(function_name)\\E_(\\d+)") - return replace(code, func_re => function_name) +function should_strip_last_newline(str) + # Strip the last newline only if there is more than one + f_pos = findfirst('\n', str) + isnothing(f_pos) && return false + nl_end = endswith(str, '\n') + return nl_end && f_pos < length(str) end -compare_code(code₁::AbstractString, code₂::AbstractString, highlight_func; kwargs...) = - compare_code(String(code₁), String(code₂), highlight_func; kwargs...) - -function compare_code(code₁::String, code₂::String, highlight_func; color=true) - io_buf = IOBuffer() - highlight_ctx = IOContext(io_buf, :color => true) - - code₁ = replace_llvm_module_name(code₁) - if color - highlight_func(highlight_ctx, code₁) - code₁_colored = String(take!(io_buf)) - else - code₁_colored = code₁ - end - - code₂ = replace_llvm_module_name(code₂) - if color - highlight_func(highlight_ctx, code₂) - code₂_colored = String(take!(io_buf)) - else - code₂_colored = code₂ - end - - if endswith(code₁, '\n') && endswith(code₂, '\n') +function code_diff(code₁::String, code₂::String, code₁_colored::String, code₂_colored::String) + if should_strip_last_newline(code₁) && should_strip_last_newline(code₂) code₁ = rstrip(==('\n'), code₁) - code₁_colored = rstrip(==('\n'), code₁_colored) code₂ = rstrip(==('\n'), code₂) + code₁_colored = rstrip(==('\n'), code₁_colored) code₂_colored = rstrip(==('\n'), code₂_colored) - end - - diff = CodeDiff(code₁, code₂, code₁_colored, code₂_colored) - optimize_line_changes!(diff) - return diff -end - - -function show_as_string(a, color::Bool) - io_buf = IOBuffer() - - Base.show(IOContext(io_buf, :color => false), MIME"text/plain"(), a) - a_str = String(take!(io_buf)) - - if color - Base.show(IOContext(io_buf, :color => true), MIME"text/plain"(), a) - a_colored = String(take!(io_buf)) else - a_colored = a_str - end - - return a_str, a_colored -end - - -function show_as_string(s::AbstractString, color::Bool) - io_buf = IOBuffer() - - # For `AnnotatedString` => get the unannotated string - a_str = String(s) - - if color - # Use `print` instead of `Base.show(io, MIME"text/plain", s)` to avoid quotes and - # escape sequences. - print(IOContext(io_buf, :color => true), s) - a_colored = String(take!(io_buf)) - else - a_colored = a_str - end - - return a_str, a_colored -end - - -function compare_show(code₁, code₂; color=true, force_no_ansi=false) - code₁_str, code₁_colored = show_as_string(code₁, color) - code₂_str, code₂_colored = show_as_string(code₂, color) - - if force_no_ansi - code₁_str = replace(code₁_str, ANSI_REGEX => "") - code₂_str = replace(code₂_str, ANSI_REGEX => "") - end + # Hack to make sure `deepdiff` creates a `StringLineDiff` + if !occursin('\n', code₁) + code₁ *= '\n' + code₁_colored *= '\n' + end - # Hack to make sure `deepdiff` creates a `StringLineDiff` - if !occursin('\n', code₁_str) - code₁_str *= '\n' - code₁_colored *= '\n' - end - if !occursin('\n', code₂_str) - code₂_str *= '\n' - code₂_colored *= '\n' + if !occursin('\n', code₂) + code₂ *= '\n' + code₂_colored *= '\n' + end end - if endswith(code₁_str, '\n') && endswith(code₂_str, '\n') && - count(==('\n'), code₁_str) > 1 && count(==('\n'), code₂_str) > 1 - # Strip the last newline only if there is more than one - code₁_str = rstrip(==('\n'), code₁_str) - code₂_str = rstrip(==('\n'), code₂_str) - code₁_colored = rstrip(==('\n'), code₁_colored) - code₂_colored = rstrip(==('\n'), code₂_colored) - end + diff = CodeDiff(code₁, code₂, code₁_colored, code₂_colored) - diff = CodeDiff(code₁_str, code₂_str, code₁_colored, code₂_colored) optimize_line_changes!(diff) return diff end """ - compare_code_native(code₁, code₂; color=true) + code_diff(args₁::Tuple, args₂::Tuple; extra_1=(;), extra_2=(;), kwargs...) -Return a [`CodeDiff`](@ref) between `code₁` and `code₂`. -Codes are cleaned-up with [`replace_llvm_module_name`](@ref) beforehand. +Function equivalent to [`@code_diff`](@ref)`(extra_1, extra_2, kwargs..., args₁, args₂)`. +`kwargs` are common to both sides, while `extra_1` and `extra_2` are passed to +[`code_for_diff`](@ref) only with `args₁` and `args₂` respectively. -If `color == true`, then both codes are highlighted using `InteractiveUtils.print_native`. -""" -function compare_code_native(code₁::AbstractString, code₂::AbstractString; color=true) - return compare_code(code₁, code₂, InteractiveUtils.print_native; color) -end +```jldoctest; setup=(f() = 1; g() = 2) +julia> diff_1 = @code_diff debuginfo_1=:none f() g(); +julia> diff_2 = code_diff((f, Tuple{}), (g, Tuple{}); extra_1=(; debuginfo=:none)); +julia> diff_1 == diff_2 +true +``` """ - compare_code_native( - f₁::Base.Callable, types₁::Type{<:Tuple}, - f₂::Base.Callable, types₂::Type{<:Tuple}; - color=true, kwargs... - ) - -Call `InteractiveUtils.code_native(f₁, types₁)` and `InteractiveUtils.code_native(f₂, types₂)` -and return their [`CodeDiff`](@ref). `kwargs` are passed to `code_native`. -""" -function compare_code_native( - f₁::Base.Callable, types₁::Type{<:Tuple}, - f₂::Base.Callable, types₂::Type{<:Tuple}; - color=true, kwargs... -) - @nospecialize(f₁, types₁, f₂, types₂) - - io_buf = IOBuffer() - io_ctx = IOContext(io_buf, :color => false) - - InteractiveUtils.code_native(io_ctx, f₁, types₁; kwargs...) - code₁ = String(take!(io_buf)) - - InteractiveUtils.code_native(io_buf, f₂, types₂; kwargs...) - code₂ = String(take!(io_buf)) - - return compare_code_native(code₁, code₂; color) -end - - -function method_instance(sig, world) - @nospecialize(sig) - @static if VERSION < v"1.10" - mth_match = Base._which(sig, world) - else - mth_match = Base._which(sig; world) - end - return Core.Compiler.specialize_method(mth_match) -end - - -function method_instance(f, types, world) - @nospecialize(f, types) - return method_instance(Base.signature_type(f, types), world) +function code_diff(args₁::Tuple, args₂::Tuple; extra_1=(;), extra_2=(;), kwargs...) + code₁, hl_code₁ = code_for_diff(args₁...; kwargs..., extra_1...) + code₂, hl_code₂ = code_for_diff(args₂...; kwargs..., extra_2...) + return code_diff(code₁, code₂, hl_code₁, hl_code₂) end """ - compare_code_native( - f::Base.Callable, types::Type{<:Tuple}, world₁, world₂; - color=true, kwargs... - ) + code_for_diff(f::Base.Callable, types::Type{<:Tuple}; type=:native, color=true, kwargs...) + code_for_diff(expr::Expr; type=:ast, color=true, kwargs...) -Similar to [`compare_code_native(f₁, types₁, f₂, types₂)`](@ref), but as a difference -between `f` in world ages `world₁` and `world₂`. +Fetches the code of `f` with [`get_code(Val(type), f, types; kwargs...)`](@ref), cleans it +up with [`cleanup_code(Val(type), code)`](@ref) and highlights it using the appropriate +[`code_highlighter(Val(type))`](@ref). +The result is two `String`s: one without and the other with highlighting. """ -function compare_code_native( - f::Base.Callable, types::Type{<:Tuple}, world₁::Integer, world₂::Integer; - color=true, dump_module=true, syntax=:intel, raw=false, debuginfo=:default, binary=false -) +function code_for_diff(f::Base.Callable, types::Type{<:Tuple}; type=:native, color=true, kwargs...) @nospecialize(f, types) - - sig = Base.signature_type(f, types) - @static if VERSION < v"1.10" - params = Base.CodegenParams(debug_info_kind=Cint(0)) - else - params = Base.CodegenParams(debug_info_kind=Cint(0), safepoint_on_entry=raw, gcstack_arg=raw) - end - - if debuginfo === :default - debuginfo = :source - elseif debuginfo !== :source && debuginfo !== :none - throw(ArgumentError("'debuginfo' must be either :source or :none")) - end - - # See `InteractiveUtils._dump_function` - mi_f₁ = method_instance(sig, world₁) - @static if VERSION < v"1.10" - f₁_str = InteractiveUtils._dump_function_linfo_native(mi_f₁, world₁, false, syntax, debuginfo, binary) - else - if dump_module - f₁_str = InteractiveUtils._dump_function_native_assembly(mi_f₁, world₁, false, syntax, debuginfo, binary, raw, params) - else - f₁_str = InteractiveUtils._dump_function_native_disassembly(mi_f₁, world₁, false, syntax, debuginfo, binary) - end - end - - mi_f₂ = method_instance(sig, world₂) - @static if VERSION < v"1.10" - f₂_str = InteractiveUtils._dump_function_linfo_native(mi_f₂, world₂, false, syntax, debuginfo, binary) - else - if dump_module - f₂_str = InteractiveUtils._dump_function_native_assembly(mi_f₂, world₂, false, syntax, debuginfo, binary, raw, params) - else - f₂_str = InteractiveUtils._dump_function_native_disassembly(mi_f₂, world₂, false, syntax, debuginfo, binary) - end - end - - return compare_code_native(f₁_str, f₂_str; color) -end - - -""" - compare_code_llvm(code₁, code₂; color=true) - -Return a [`CodeDiff`](@ref) between `code₁` and `code₂`. -Codes are cleaned-up with [`replace_llvm_module_name`](@ref) beforehand. - -If `color == true`, then both codes are highlighted using `InteractiveUtils.print_llvm`. -""" -function compare_code_llvm(code₁::AbstractString, code₂::AbstractString; color=true) - return compare_code(code₁, code₂, InteractiveUtils.print_llvm; color) -end - - -""" - compare_code_llvm( - f₁::Base.Callable, types₁::Type{<:Tuple}, - f₂::Base.Callable, types₂::Type{<:Tuple}; - color=true, kwargs... - ) - -Call `InteractiveUtils.code_llvm(f₁, types₁)` and `InteractiveUtils.code_llvm(f₂, types₂)` -and return their [`CodeDiff`](@ref). `kwargs` are passed to `code_llvm`. -""" -function compare_code_llvm( - f₁::Base.Callable, types₁::Type{<:Tuple}, - f₂::Base.Callable, types₂::Type{<:Tuple}; - color=true, kwargs... -) - @nospecialize(f₁, types₁, f₂, types₂) - - io_buf = IOBuffer() - io_ctx = IOContext(io_buf, :color => false) - - InteractiveUtils.code_llvm(io_ctx, f₁, types₁; kwargs...) - code₁ = String(take!(io_buf)) - - InteractiveUtils.code_llvm(io_buf, f₂, types₂; kwargs...) - code₂ = String(take!(io_buf)) - - return compare_code_llvm(code₁, code₂; color) + code = get_code(Val(type), f, types; kwargs...) + return code_for_diff(code, Val(type), color) end - -""" - compare_code_llvm( - f::Base.Callable, types::Type{<:Tuple}, world₁, world₂; - color=true, kwargs... - ) - -Similar to [`compare_code_llvm(f₁, types₁, f₂, types₂)`](@ref), but as a difference -between `f` in world ages `world₁` and `world₂`. -""" -function compare_code_llvm( - f::Base.Callable, types::Type{<:Tuple}, world₁::Integer, world₂::Integer; - color=true, raw=false, dump_module=false, optimize=true, debuginfo=:default -) - @nospecialize(f, types) - - sig = Base.signature_type(f, types) - @static if VERSION < v"1.10" - params = Base.CodegenParams(debug_info_kind=Cint(0)) - else - params = Base.CodegenParams(debug_info_kind=Cint(0), safepoint_on_entry=raw, gcstack_arg=raw) - end - - if debuginfo === :default - debuginfo = :source - elseif debuginfo !== :source && debuginfo !== :none - throw(ArgumentError("'debuginfo' must be either :source or :none")) - end - - # See `InteractiveUtils._dump_function` - mi_f₁ = method_instance(sig, world₁) - @static if VERSION < v"1.10" - f₁_str = InteractiveUtils._dump_function_linfo_llvm( - mi_f₁, world₁, false, !raw, dump_module, optimize, debuginfo, params - ) - else - f₁_str = InteractiveUtils._dump_function_llvm( - mi_f₁, world₁, false, !raw, dump_module, optimize, debuginfo, params - ) - end - - mi_f₂ = method_instance(sig, world₂) - @static if VERSION < v"1.10" - f₂_str = InteractiveUtils._dump_function_linfo_llvm( - mi_f₂, world₂, false, !raw, dump_module, optimize, debuginfo, params - ) - else - f₂_str = InteractiveUtils._dump_function_llvm( - mi_f₂, world₂, false, !raw, dump_module, optimize, debuginfo, params - ) +function code_for_diff(expr::Union{Expr, QuoteNode}; type=:ast, color=true, kwargs...) + if type !== :ast + throw(ArgumentError("wrong type for `$(typeof(expr))`: `$type`, expected `:ast`")) end - - return compare_code_llvm(f₁_str, f₂_str; color) + code = code_ast(expr; kwargs...) + return code_for_diff(code, Val(type), color) end +function code_for_diff(code, type::Val, color) + code = cleanup_code(type, code) -""" - compare_code_typed(code_info₁::Pair, code_info₂::Pair; color=true) - compare_code_typed(code_info₁::Core.CodeInfo, code_info₂::Core.CodeInfo; color=true) - -Return a [`CodeDiff`](@ref) between `code_info₁` and `code_info₂`. - -If `color == true`, then both codes are highlighted. -""" -function compare_code_typed( - code_info₁::CI, code_info₂::CI; color=true -) where {CI <: Union{Core.CodeInfo, Pair{Core.CodeInfo, <:Type}}} - return compare_show(code_info₁, code_info₂; color) -end - - -""" - compare_code_typed( - f₁::Base.Callable, types₁::Type{<:Tuple}, - f₂::Base.Callable, types₂::Type{<:Tuple}; - color=true, kwargs... - ) - -Call `Base.code_typed(f₁, types₁)` and `Base.code_typed(f₂, types₂)` and return their -[`CodeDiff`](@ref). `kwargs` are passed to `code_typed`. - -Both function calls should only match a single method. -""" -function compare_code_typed( - f₁::Base.Callable, types₁::Type{<:Tuple}, - f₂::Base.Callable, types₂::Type{<:Tuple}; - color=true, kwargs... -) - @nospecialize(f₁, types₁, f₂, types₂) - - code_info₁ = Base.code_typed(f₁, types₁; kwargs...) - code_info₁ = only(code_info₁) - - code_info₂ = Base.code_typed(f₂, types₂; kwargs...) - code_info₂ = only(code_info₂) - - return compare_code_typed(code_info₁, code_info₂; color) -end - - -""" - compare_code_typed( - f::Base.Callable, types::Type{<:Tuple}, world₁, world₂; - color=true, kwargs... - ) - -Similar to [`compare_code_typed(f₁, types₁, f₂, types₂)`](@ref), but as a difference -between `f` in world ages `world₁` and `world₂`. -""" -function compare_code_typed( - f::Base.Callable, types::Type{<:Tuple}, world₁::Integer, world₂::Integer; - color=true, kwargs... -) - @nospecialize(f, types) - - code_info₁ = Base.code_typed(f, types; world=world₁, kwargs...) - code_info₁ = only(code_info₁) - - code_info₂ = Base.code_typed(f, types; world=world₂, kwargs...) - code_info₂ = only(code_info₂) - - return compare_code_typed(code_info₁, code_info₂; color) -end - - -""" - compare_ast(code₁::Expr, code₂::Expr; color=true, prettify=true, lines=false, alias=false) - -A [`CodeDiff`](@ref) between `code₁` and `code₂`, relying on the native display of Julia AST. - -If `prettify == true`, then -[`MacroTools.prettify(code; lines, alias)`](https://fluxml.ai/MacroTools.jl/stable/utilities/#MacroTools.prettify) -is used to cleanup the AST. `lines == true` will keep the `LineNumberNode`s and `alias == true` -will replace mangled names (or `gensym`s) by dummy names. - -`color == true` is special, see [`compare_ast(code₁::AbstractString, code₂::AbstractString)`](@ref). -""" -function compare_ast(code₁::Expr, code₂::Expr; color=true, prettify=true, lines=false, alias=false) - if prettify - code₁ = MacroTools.prettify(code₁; lines, alias) - code₂ = MacroTools.prettify(code₂; lines, alias) - end - - # Placing the `Expr`s in blocks is required to have a multiline display - code₁ = MacroTools.block(code₁) - code₂ = MacroTools.block(code₂) + code_str = sprint(code_highlighter(type), code; context=(:color => false,)) + code_str = replace(code_str, ANSI_REGEX => "") # Make sure there is no decorations if color - io_buf = IOBuffer() - - print(io_buf, code₁) - code_str₁ = String(take!(io_buf)) - - print(io_buf, code₂) - code_str₂ = String(take!(io_buf)) - - return compare_ast(code_str₁, code_str₂) + code_highlighted = sprint(code_highlighter(type), code; context=(:color => true,)) else - return compare_show(code₁, code₂; color=false) - end -end - - -""" - compare_ast(code₁::AbstractString, code₂::AbstractString; color=true) - compare_ast(code₁::Markdown.MD, code₂::Markdown.MD; color=true) - -[`CodeDiff`](@ref) between Julia code string, in the form of Markdown code blocks. - -Relies on the Markdown code highlighting from [`OhMyREPL.jl`](https://github.com/KristofferC/OhMyREPL.jl) -to colorize Julia code. -""" -function compare_ast(code₁::Markdown.MD, code₂::Markdown.MD; color=true) - if !haskey(Base.loaded_modules, OhMYREPL_PKG_ID) - @warn "OhMyREPL.jl is not loaded, AST highlighting will not work" maxlog=1 + code_highlighted = code_str end - 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₂)) - return compare_ast(code_md₁, code_md₂; color) -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) + return code_str, code_highlighted 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. + @code_diff [type=:native] [color=true] [option=value...] f₁(...) f₂(...) + @code_diff [option=value...] :(expr₁) :(expr₂) -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 +Compare the methods called by the `f₁(...)` and `f₂(...)` or the expressions `expr₁` and +`expr₂`, then return a [`CodeDiff`](@ref). +`option`s are passed to [`get_code`](@ref). Option names ending with `_1` or `_2` are passed +to the call of `get_code` for `f₁` and `f₂` respectively. They can also be packed into +`extra_1` and `extra_2`. -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 +To compare `Expr` in variables, use `@code_diff :(\$a) :(\$b)`. +```julia +# Default comparison +@code_diff type=:native f() g() -""" - code_diff(::Val{:ast}, code₁, code₂; kwargs...) +# No debuginfo for `f()` and `g()` +@code_diff type=:native debuginfo=:none f() g() -Compare AST in `code₁` and `code₂`, which can be `Expr` or any `AbstractString`. -""" -code_diff(::Val{:ast}, code₁, code₂; kwargs...) = compare_ast(code₁, code₂; kwargs...) +# No debuginfo for `f()` +@code_diff type=:native debuginfo_1=:none f() g() -code_diff(::Val{:native}, code₁::AbstractString, code₂::AbstractString; kwargs...) = - compare_code(code₁, code₂, InteractiveUtils.print_native; kwargs...) -code_diff(::Val{:llvm}, code₁::AbstractString, code₂::AbstractString; kwargs...) = - compare_code(code₁, code₂, InteractiveUtils.print_llvm; kwargs...) -code_diff(::Val{:typed}, code₁::AbstractString, code₂::AbstractString; kwargs...) = - compare_code(code₁, code₂, identity; kwargs...) +# No debuginfo for `g()` +@code_diff type=:native debuginfo_2=:none f() g() -""" - code_diff(::Val{type}, f₁, types₁, f₂, types₂; kwargs...) - code_diff(::Val{type}, code₁, code₂; kwargs...) - code_diff(args...; type=:native, kwargs...) +# Options can be passed from variables with `extra_1` and `extra_2` +opts = (; debuginfo=:none, world=Base.get_world_counter()) +@code_diff type=:native extra_1=opts extra_2=opts f() g() -Dispatch to [`compare_code_native`](@ref), [`compare_code_llvm`](@ref), -[`compare_code_typed`](@ref) or [`compare_ast`](@ref) depending on `type`. +# `type` and `color` can also be made different in each side +@code_diff type_1=:native type_2=:llvm f() f() +``` """ -code_diff(code₁, code₂; type::Symbol=:native, kwargs...) = - code_diff(Val(type), code₁, code₂; kwargs...) - -@nospecialize - -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...) - -@specialize - - -function replace_locals_by_gensyms(expr) - Base.isexpr(expr, :call) && expr.args[1] === :error && return expr, Expr(:block) - Base.isexpr(expr, :call) && expr.args[1] === :code_diff && return Expr(:block), expr +macro code_diff(args...) + length(args) < 2 && return :(throw(ArgumentError("@code_diff takes at least 2 arguments"))) + options = args[1:end-2] + code₁, code₂ = args[end-1:end] - call_expr = pop!(expr.args) - if !(Base.isexpr(call_expr, :call) && call_expr.args[1] === :code_diff) - error("expected call to `code_diff`, got: $call_expr") - end + # 2 ways to pass kwargs to a specific side: `extra_1=(; kwargs...)` or `_1=val`/`_2=val` + # otherwise it is an option common to both sides. + options₁ = Expr[] + options₂ = Expr[] + for option in options + if !(Base.isexpr(option, :(=), 2) && option.args[1] isa Symbol) + opt_error = "options must be in the form `key=value`, got: `$option`" + return :(throw(ArgumentError($opt_error))) + end - locals = Symbol[] - locals = Dict{Symbol, Symbol}() - # Replace `a` or `(a, b)` in `local a = 1`. We suppose `expr` is the result of - # `InteractiveUtils.gen_call_with_extracted_types`, therefore there is less edge cases - # to care about. - expr = MacroTools.postwalk(expr) do e - !Base.isexpr(e, :local) && return e - assign = e.args[1] - !Base.isexpr(assign, :(=)) && error("unexpected `local` expression: $e") - if Base.isexpr(assign.args[1], :tuple) - l_vars = assign.args[1] - map!(l_vars.args, l_vars.args) do l - locals[l] = gensym(l) + opt_name = option.args[1]::Symbol + if opt_name in (:extra_1, :extra_2) + opt_splat = Expr(:..., esc(option.args[2])) + if opt_name === :extra_1 + push!(options₁, opt_splat) + else + push!(options₂, opt_splat) end - elseif Base.isexpr(assign.args[1], :call) - # alias for dot function, already a gensym else - l = assign.args[1] - assign.args[1] = locals[l] = gensym(l) + opt_name_str = String(opt_name) + has_suffix = endswith(opt_name_str, r"_[12]") + opt_name = has_suffix ? Symbol(opt_name_str[1:end-2]) : opt_name + kw_option = Expr(:kw, opt_name, esc(option.args[2])) + if endswith(opt_name_str, "_1") + push!(options₁, kw_option) + elseif endswith(opt_name_str, "_2") + push!(options₂, kw_option) + else + push!(options₁, kw_option) + push!(options₂, kw_option) + end end - return Expr(:local, assign) - end - - # We assume that all `locals` defined in `expr` are only used in the last expression, - # the `call(:code_diff, ...)`, which is true since `code_diff` starts with `code_`. - call_expr = MacroTools.postwalk(call_expr) do e - !(e isa Symbol) && return e - return get(locals, e, e) - end - - if Base.isexpr(call_expr.args[2], :parameters) - !isempty(call_expr.args[2].args) && error("unexpected kwargs: $call_expr") - popat!(call_expr.args, 2) # Remove the kwargs - end - - return expr, call_expr -end - - -""" - @code_diff [type=:native] [option=value...] f₁(...) f₂(...) - @code_diff [option=value...] :(expr₁) :(expr₂) - -Compare the methods called by the `f₁(...)` and `f₂(...)` expressions, and return a -[`CodeDiff`](@ref). - -`option`s are passed as key-word arguments to [`code_diff`](@ref) and then to the -`compare_code_*` function for the given code `type`. - -In the other form of `@code_diff`, compare the `Expr`essions `expr₁` and `expr₂`, the `type` -is inferred as `:ast`. -To compare `Expr` in variables, use `@code_diff :(\$a) :(\$b)`, or call -[`compare_ast`](@ref) directly. -""" -macro code_diff(args...) - length(args) < 2 && throw(ArgumentError("@code_diff takes at least 2 arguments")) - options = args[1:end-2] - code₁, code₂ = args[end-1:end] - - options = map(options) do option - !(option isa Expr && option.head === :(=)) && - throw(ArgumentError("options must be in the form `key=value`, got: $option")) - return Expr(:kw, esc(option.args[1]), esc(option.args[2])) end # Simple values such as `:(1)` are stored in a `QuoteNode` @@ -693,30 +176,34 @@ macro code_diff(args...) if Base.isexpr(code₁, :quote) && Base.isexpr(code₂, :quote) code₁ = esc(code₁) code₂ = esc(code₂) - call_code_diff = :($code_diff($code₁, $code₂; type=:ast)) - append!(call_code_diff.args[2].args, options) - return call_code_diff + code_for_diff₁ = :($code_for_diff($code₁; type=:ast, $(options₁...))) + code_for_diff₂ = :($code_for_diff($code₂; type=:ast, $(options₂...))) else - expr₁ = InteractiveUtils.gen_call_with_extracted_types(__module__, :code_diff, code₁) - expr₂ = InteractiveUtils.gen_call_with_extracted_types(__module__, :code_diff, code₂) - - # `gen_call_with_extracted_types` will, to handle some specific calls, store some - # values in local variables with a fixed name. This is a problem as we might use - # the same variable name twice since we want to fuse `expr₁` and `expr₂`. - expr₁, call_expr₁ = replace_locals_by_gensyms(expr₁) - expr₂, call_expr₂ = replace_locals_by_gensyms(expr₂) - - if Base.isexpr(call_expr₁, :call) && Base.isexpr(call_expr₂, :call) - # Fuse both calls to `code_diff`, into a single kwcall - call_code_diff = :($code_diff( - ($(call_expr₁.args[2:3]...),), - ($(call_expr₂.args[2:3]...),); - )) - append!(call_code_diff.args[2].args, options) - else - call_code_diff = call_expr₁ # `expr₁` or `expr₂` has an error expression + # `code_for_diff`'s name must start with `code` in order to replicate the behavior + # of the other `@code_*` macros. + code_for_diff₁ = InteractiveUtils.gen_call_with_extracted_types(__module__, :code_for_diff, code₁) + code_for_diff₂ = InteractiveUtils.gen_call_with_extracted_types(__module__, :code_for_diff, code₂) + + if Base.isexpr(code_for_diff₁, :call) && code_for_diff₁.args[1] === :error + return code_for_diff₁ + elseif Base.isexpr(code_for_diff₂, :call) && code_for_diff₂.args[1] === :error + return code_for_diff₂ end - return MacroTools.flatten(Expr(:block, expr₁, expr₂, call_code_diff)) + # `gen_call_with_extracted_types` adds kwargs inconsistently so we do it ourselves + args₁ = Base.isexpr(code_for_diff₁, :call) ? code_for_diff₁.args : code_for_diff₁.args[end].args + args₂ = Base.isexpr(code_for_diff₂, :call) ? code_for_diff₂.args : code_for_diff₂.args[end].args + !Base.isexpr(args₁[2], :parameters) && insert!(args₁, 2, Expr(:parameters)) + !Base.isexpr(args₂[2], :parameters) && insert!(args₂, 2, Expr(:parameters)) + append!(args₁[2].args, options₁) + append!(args₂[2].args, options₂) + end + + return quote + let + local code₁, hl_code₁ = $code_for_diff₁ + local code₂, hl_code₂ = $code_for_diff₂ + code_diff(code₁, code₂, hl_code₁, hl_code₂) + end end end diff --git a/src/get_code.jl b/src/get_code.jl new file mode 100644 index 0000000..842a20d --- /dev/null +++ b/src/get_code.jl @@ -0,0 +1,224 @@ + +function method_instance(sig, world) + @nospecialize(sig) + world = UInt64(world) + @static if VERSION < v"1.10" + mth_match = Base._which(sig, world) + else + mth_match = Base._which(sig; world) + end + return Core.Compiler.specialize_method(mth_match) +end + + +function method_instance(f::Base.Callable, types::Type{<:Tuple}, world=Base.get_world_counter()) + @nospecialize(f, types) + return method_instance(Base.signature_type(f, types), world) +end + + +function code_native(f::Base.Callable, types::Type{<:Tuple}, ::Nothing; kwargs...) + @nospecialize(f, types) + io_buf = IOBuffer() + io_ctx = IOContext(io_buf, :color => false) + InteractiveUtils.code_native(io_ctx, f, types; kwargs...) + return String(take!(io_buf)) +end + + +function code_native( + f::Base.Callable, types::Type{<:Tuple}, world::Integer; + dump_module=true, syntax=:intel, raw=false, debuginfo=:default, binary=false +) + @nospecialize(f, types) + mi = method_instance(f, types, world) + + @static if VERSION < v"1.10" + params = Base.CodegenParams(debug_info_kind=Cint(0)) + else + params = Base.CodegenParams(debug_info_kind=Cint(0), safepoint_on_entry=raw, gcstack_arg=raw) + end + + if debuginfo === :default + debuginfo = :source + elseif debuginfo !== :source && debuginfo !== :none + throw(ArgumentError("'debuginfo' must be either :source or :none")) + end + + # See `InteractiveUtils._dump_function` + @static if VERSION < v"1.10" + f_str = InteractiveUtils._dump_function_linfo_native(mi, world, false, syntax, debuginfo, binary) + else + if dump_module + f_str = InteractiveUtils._dump_function_native_assembly(mi, world, false, syntax, debuginfo, binary, raw, params) + else + f_str = InteractiveUtils._dump_function_native_disassembly(mi, world, false, syntax, debuginfo, binary) + end + end + + return f_str +end + + +""" + code_native(f, types; world=nothing, kwargs...) + +The native code of the method of `f` called with `types` (a `Tuple` type), as a string. +`world` defaults to the current world age. +`kwargs` are forwarded to `InteractiveUtils.code_native`. +""" +code_native(@nospecialize(f), @nospecialize(types); world=nothing, kwargs...) = + code_native(f, types, world; kwargs...) + + +function code_llvm(f::Base.Callable, types::Type{<:Tuple}, ::Nothing; kwargs...) + @nospecialize(f, types) + io_buf = IOBuffer() + io_ctx = IOContext(io_buf, :color => false) + InteractiveUtils.code_llvm(io_ctx, f, types; kwargs...) + return String(take!(io_buf)) +end + + +function code_llvm( + f::Base.Callable, types::Type{<:Tuple}, world::Integer; + raw=false, dump_module=false, optimize=true, debuginfo=:default +) + @nospecialize(f, types) + mi = method_instance(f, types, world) + + @static if VERSION < v"1.10" + params = Base.CodegenParams(debug_info_kind=Cint(0)) + else + params = Base.CodegenParams(debug_info_kind=Cint(0), safepoint_on_entry=raw, gcstack_arg=raw) + end + + if debuginfo === :default + debuginfo = :source + elseif debuginfo !== :source && debuginfo !== :none + throw(ArgumentError("'debuginfo' must be either :source or :none")) + end + + # See `InteractiveUtils._dump_function` + @static if VERSION < v"1.10" + f_str = InteractiveUtils._dump_function_linfo_llvm( + mi, world, false, !raw, dump_module, optimize, debuginfo, params + ) + else + f_str = InteractiveUtils._dump_function_llvm( + mi, world, false, !raw, dump_module, optimize, debuginfo, params + ) + end + + return f_str +end + + +""" + code_llvm(f, types; world=nothing, kwargs...) + +The LLVM-IR code of the method of `f` called with `types` (a `Tuple` type), as a string. +`world` defaults to the current world age. +`kwargs` are forwarded to `InteractiveUtils.code_native`. +""" +code_llvm(@nospecialize(f), @nospecialize(types); world=nothing, kwargs...) = + code_llvm(f, types, world; kwargs...) + + +""" + code_typed(f, types; world=nothing, kwargs...) + +The Julia-IR code (aka 'typed code') of the method of `f` called with `types` +(a `Tuple` type), as a `Core.CodeInfo`. +`world` defaults to the current world age. +`kwargs` are forwarded to `Base.code_typed`. + +The function call should only match a single method. +""" +function code_typed(f, types; world=nothing, kwargs...) + @nospecialize(f, types) + if isnothing(world) + code_info = Base.code_typed(f, types; kwargs...) + else + code_info = Base.code_typed(f, types; world, kwargs...) + end + return only(code_info) +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=nothing) + @nospecialize(f, types) + if !isnothing(world) + error("Revise.jl does not keep track of previous definitions: \ + cannot get the AST from a previous world age") + end + mi = method_instance(f, types) + return method_to_ast(mi) +end + + +""" + code_ast(f, types; world=nothing, prettify=true, lines=false, alias=false) + +The Julia AST of the method of `f` called with `types` (a `Tuple` type), as a `Expr`. +[`Revise.jl`](https://github.com/timholy/Revise.jl) is used to get those definitions, and +it must be loaded **before** the definition of `f`'s method to get the AST for. + +`world` defaults to the current world age. Since `Revise.jl` does not keep track of all +definitions in all world ages, it is very likely that the only retrievable definition is +the most recent one. + +If `prettify == true`, then [`MacroTools.prettify(code; lines, alias)`](https://fluxml.ai/MacroTools.jl/stable/utilities/#MacroTools.prettify) +is used to cleanup the AST. `lines == true` will keep the `LineNumberNode`s and `alias == true` +will replace mangled names (or `gensym`s) by more readable names. +""" +function code_ast(f::Base.Callable, types::Type{<:Tuple}; prettify=true, lines=false, alias=false, kwargs...) + @nospecialize(f, types) + code = method_to_ast(f, types; kwargs...) + return code_ast(code; prettify, lines, alias) +end + +function code_ast(code::QuoteNode; kwargs...) + return code_ast(Expr(:quote, Expr(:block, code.value)); kwargs...) +end + +function code_ast(code::Expr; prettify=true, lines=false, alias=false) + if prettify + code = MacroTools.prettify(code; lines, alias) + end + # Placing the `Expr`s in blocks is required to have a multiline display + return MacroTools.block(code) +end + + +@nospecialize + +""" + get_code(::Val{code_type}, f, types; world=nothing, kwargs...) + +The code object of `code_type` for `f`. Dispatch depends on `code_type`: + - `:native`: [`code_native`](@ref) + - `:llvm`: [`code_llvm`](@ref) + - `:typed`: [`code_typed`](@ref) + - `:ast`: [`code_ast`](@ref) +""" +get_code(::Val{:native}, f, types; kwargs...) = code_native(f, types; kwargs...) +get_code(::Val{:llvm}, f, types; kwargs...) = code_llvm(f, types; kwargs...) +get_code(::Val{:typed}, f, types; kwargs...) = code_typed(f, types; kwargs...) +get_code(::Val{:ast}, f, types; kwargs...) = code_ast(f, types; kwargs...) + +@specialize diff --git a/src/highlighting.jl b/src/highlighting.jl new file mode 100644 index 0000000..2b0c7d9 --- /dev/null +++ b/src/highlighting.jl @@ -0,0 +1,39 @@ + +# Use `print` by default instead of `Base.show(io, MIME"text/plain", s)` to avoid quotes +# and escape sequences for strings +no_highlighting(io::IO, s::AbstractString) = print(io, s) +no_highlighting(io::IO, c) = Base.show(io, MIME"text/plain"(), c) + +""" + code_highlighter(::Val{code_type}) where {code_type} + +Return a function of signature `(io::IO, code_obj)` which prints `code_obj` to `io` with +highlighting/decorations. By default `print(io, code_obj)` is used for `AbstractString`s +and `Base.show(io, MIME"text/plain"(), code_obj)` otherwise. + +The highlighting function is called twice: once for color-less text and again with color. +""" +code_highlighter(_) = no_highlighting + +code_highlighter(::Val{:native}) = InteractiveUtils.print_native +code_highlighter(::Val{:llvm}) = InteractiveUtils.print_llvm + + +highlight_ast(io::IO, ast::Expr) = highlight_ast(io, sprint(Base.show, ast)) + +function highlight_ast(io::IO, ast::AbstractString) + if !haskey(Base.loaded_modules, OhMYREPL_PKG_ID) + @warn "OhMyREPL.jl is not loaded, AST highlighting will not work" maxlog=1 + end + ast_md = Markdown.MD(Markdown.julia, Markdown.Code("julia", ast)) + + ast_md_str = sprint(Base.show, MIME"text/plain"(), ast_md; context=IOContext(io)) + if startswith(ast_md_str, " ") + # Markdown adds two spaces in front of every line + ast_md_str = replace(ast_md_str[3:end], "\n " => '\n') + end + + print(io, ast_md_str) +end + +code_highlighter(::Val{:ast}) = highlight_ast diff --git a/test/references/a_vs_b_COLOR.jl_ast b/test/references/a_vs_b_COLOR.jl_ast index 12e92ab..f185d0e 100644 --- a/test/references/a_vs_b_COLOR.jl_ast +++ b/test/references/a_vs_b_COLOR.jl_ast @@ -1,8 +1,8 @@ - begin  ┃  begin -  ┣⟫ println("B") - 1 + 2 ⟪╋⟫ 1 + 3 - f(a, b) ⟪╋⟫ f(a, d) - g(c, d) ⟪╋⟫ g(c, b) -  ┣⟫ h(x, y) - "test" ⟪╋⟫ "test2" - end  ┃  end \ No newline at end of file +quote  ┃ quote +  ┣⟫ println("B") + 1 + 2 ⟪╋⟫ 1 + 3 + f(a, b) ⟪╋⟫ f(a, d) + g(c, d) ⟪╋⟫ g(c, b) +  ┣⟫ h(x, y) + "test" ⟪╋⟫ "test2" +end  ┃ end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index d4752d3..910fe3a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -114,11 +114,11 @@ end end @testset "AST" begin - diff = CodeDiffs.compare_ast(:(1+2), :(1+2); color=false, prettify=false, lines=false, alias=false) + diff = CodeDiffs.code_diff((:(1+2),), (:(1+2),); type=:ast, color=false, prettify=false, lines=false, alias=false) @test CodeDiffs.issame(diff) @test diff.before == diff.highlighted_before == "quote\n 1 + 2\nend" - diff = CodeDiffs.compare_ast(:(1+2), :(1+3); color=false, prettify=false, lines=false, alias=false) + diff = CodeDiffs.code_diff((:(1+2),), (:(1+3),); type=:ast, color=false, prettify=false, lines=false, alias=false) @test !CodeDiffs.issame(diff) @test length(DeepDiffs.added(diff)) == length(DeepDiffs.removed(diff)) == 1 @@ -126,10 +126,10 @@ end $(LineNumberNode(42, :file)) 1+2 end - diff = CodeDiffs.compare_ast(e, :(1+2); color=false, prettify=false, lines=true, alias=false) + diff = CodeDiffs.code_diff((e,), (:(1+2),); type=:ast, color=false, prettify=false, lines=true, alias=false) @test !CodeDiffs.issame(diff) @test occursin("#= file:42 =#", diff.before) - diff = CodeDiffs.compare_ast(e, :(1+2); color=false, prettify=true, lines=false, alias=false) + diff = CodeDiffs.code_diff((e,), (:(1+2),); type=:ast, color=false, prettify=true, lines=false, alias=false) @test CodeDiffs.issame(diff) @test diff == (@code_diff color=false :($e) :(1+2)) end @@ -141,40 +141,40 @@ end """) @testset "Typed" begin - diff = CodeDiffs.compare_code_typed(f1, Tuple{}, f1, Tuple{}; color=false) + diff = CodeDiffs.code_diff((f1, Tuple{}), (f1, Tuple{}); type=:typed, color=false) @test CodeDiffs.issame(diff) - - diff = CodeDiffs.compare_code_typed(f1, Tuple{}, f2, Tuple{}; color=false) + + diff = CodeDiffs.code_diff((f1, Tuple{}), (f2, Tuple{}); type=:typed, color=false) @test !CodeDiffs.issame(diff) @test length(DeepDiffs.added(diff)) == length(DeepDiffs.removed(diff)) == 1 @test diff == (@code_diff type=:typed color=false f1() f2()) end @testset "LLVM" begin - diff = CodeDiffs.compare_code_llvm(f1, Tuple{}, f1, Tuple{}; color=false) + diff = CodeDiffs.code_diff((f1, Tuple{}), (f1, Tuple{}); type=:llvm, color=false) @test CodeDiffs.issame(diff) @test !occursin(r"julia_f1", diff.before) # LLVM module names should have been cleaned up - - diff = CodeDiffs.compare_code_llvm(f1, Tuple{}, f2, Tuple{}; color=false) + + diff = CodeDiffs.code_diff((f1, Tuple{}), (f2, Tuple{}); type=:llvm, color=false) @test !CodeDiffs.issame(diff) @test diff == (@code_diff type=:llvm color=false f1() f2()) end @testset "Native" begin - diff = CodeDiffs.compare_code_native(f1, Tuple{}, f1, Tuple{}; color=false) + diff = CodeDiffs.code_diff((f1, Tuple{}), (f1, Tuple{}); type=:native, color=false) @test CodeDiffs.issame(diff) @test !occursin(r"julia_f1", diff.before) # LLVM module names should have been cleaned up - diff = CodeDiffs.compare_code_native(f1, Tuple{}, f2, Tuple{}; color=false) + diff = CodeDiffs.code_diff((f1, Tuple{}), (f2, Tuple{}); type=:native, color=false) @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) + diff = CodeDiffs.code_diff((f1, Tuple{}), (f1, Tuple{}); type=:ast, color=false) @test CodeDiffs.issame(diff) - diff = CodeDiffs.compare_ast(f1, Tuple{}, f2, Tuple{}; color=false) + diff = CodeDiffs.code_diff((f1, Tuple{}), (f2, Tuple{}); type=:ast, color=false) @test !CodeDiffs.issame(diff) @test diff == (@code_diff type=:ast color=false f1() f2()) end @@ -199,7 +199,7 @@ end "test2" end - diff = CodeDiffs.compare_ast(A, B; color=false) + diff = CodeDiffs.code_diff((A,), (B,); type=:ast, color=false) @test !CodeDiffs.issame(diff) @test length(DeepDiffs.added(diff)) == 8 @test length(DeepDiffs.changed(diff)) == 4 @@ -221,16 +221,16 @@ end @testset "Display" begin function test_cmp_display(f₁, args₁, f₂, args₂) @testset "Typed" begin - diff = CodeDiffs.compare_code_typed(f₁, args₁, f₂, args₂; color=true) + diff = CodeDiffs.code_diff((f₁, args₁), (f₂, args₂); type=:typed, color=true) @test findfirst(CodeDiffs.ANSI_REGEX, diff.before) === nothing @test !endswith(diff.before, '\n') && !endswith(diff.after, '\n') println(TEST_IO, "\nTyped: $(nameof(f₁)) vs. $(nameof(f₂))") printstyled(TEST_IO, display_str(diff; columns=120)) println(TEST_IO) end - + @testset "LLVM" begin - diff = CodeDiffs.compare_code_llvm(f₁, args₁, f₂, args₂; color=true, debuginfo=:none) + diff = CodeDiffs.code_diff((f₁, args₁), (f₂, args₂); type=:llvm, color=true, debuginfo=:none) @test findfirst(CodeDiffs.ANSI_REGEX, diff.before) === nothing @test !endswith(diff.before, '\n') && !endswith(diff.after, '\n') @test rstrip(@io2str InteractiveUtils.print_llvm(IOContext(::IO, :color => true), diff.before)) == diff.highlighted_before @@ -240,7 +240,7 @@ end end @testset "Native" begin - diff = CodeDiffs.compare_code_native(f₁, args₁, f₂, args₂; color=true, debuginfo=:none) + diff = CodeDiffs.code_diff((f₁, args₁), (f₂, args₂); type=:native, color=true, debuginfo=:none) @test findfirst(CodeDiffs.ANSI_REGEX, diff.before) === nothing @test !endswith(diff.before, '\n') && !endswith(diff.after, '\n') @test rstrip(@io2str InteractiveUtils.print_native(IOContext(::IO, :color => true), diff.before)) == diff.highlighted_before @@ -250,7 +250,7 @@ end end @testset "AST" begin - diff = CodeDiffs.compare_ast(f₁, args₁, f₂, args₂; color=true) + diff = CodeDiffs.code_diff((f₁, args₁), (f₂, args₂); type=:ast, 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₂))") @@ -259,7 +259,7 @@ end end @testset "Line numbers" begin - diff = CodeDiffs.compare_code_typed(f₁, args₁, f₂, args₂; color=false) + diff = CodeDiffs.code_diff((f₁, args₁), (f₂, args₂); type=:typed, color=false) @test findfirst(CodeDiffs.ANSI_REGEX, diff.before) === nothing withenv("CODE_DIFFS_LINE_NUMBERS" => true) do println(TEST_IO, "\nTyped + line numbers: $(nameof(f₁)) vs. $(nameof(f₂))") @@ -312,7 +312,7 @@ end "test2" end - diff = CodeDiffs.compare_ast(A, B; color=false) + diff = CodeDiffs.code_diff((A,), (B,); type=:ast, color=false) check_diff_display_order(diff, [ "quote" => "quote", @@ -332,11 +332,13 @@ end @test_reference "references/a_vs_b_LINES.jl_ast" display_str(diff; color=false, columns=120) end - diff = CodeDiffs.compare_ast(A, B; color=true) + diff = CodeDiffs.code_diff((A,), (B,); type=:ast, color=true) @test_reference "references/a_vs_b_COLOR.jl_ast" display_str(diff; color=true, columns=120) # Single line code should not cause any issues with DeepDiffs.jl - diff = CodeDiffs.code_diff(Val(:ast), "1 + 2", "1 + 3"; color=false) + a = "1 + 2" + b = "1 + 3" + diff = CodeDiffs.code_diff(a, b, a, b) @test length(CodeDiffs.added(diff)) == length(CodeDiffs.removed(diff)) == 1 end end @@ -355,42 +357,44 @@ end @testset "World age" begin @no_overwrite_warning begin file_name = eval_for_revise("f() = 1") - w₁ = Base.get_world_counter() + world_1 = Base.get_world_counter() eval_for_revise("f() = 2", file_name, false) - w₂ = Base.get_world_counter() + world_2 = Base.get_world_counter() end + extra_1 = (; world=world_1) + extra_2 = (; world=world_2) + @testset "Typed" begin - diff = CodeDiffs.compare_code_typed(f, Tuple{}, w₁, w₁; color=false, debuginfo=:none) + diff = CodeDiffs.code_diff((f, Tuple{}), (f, Tuple{}); type=:typed, color=false, debuginfo=:none, extra_1, extra_2=extra_1) @test CodeDiffs.issame(diff) - diff = CodeDiffs.compare_code_typed(f, Tuple{}, w₁, w₂; color=false, debuginfo=:none) + diff = CodeDiffs.code_diff((f, Tuple{}), (f, Tuple{}); type=:typed, color=false, debuginfo=:none, extra_1, extra_2) @test !CodeDiffs.issame(diff) @test occursin("1", diff.before) @test occursin("2", diff.after) + @test diff == (@code_diff type=:typed color=false debuginfo=:none world_1=world_1 world_2=world_2 f() f()) end @testset "LLVM" begin - diff = CodeDiffs.compare_code_llvm(f, Tuple{}, w₁, w₁; color=false, debuginfo=:none) + diff = CodeDiffs.code_diff((f, Tuple{}), (f, Tuple{}); type=:llvm, color=false, debuginfo=:none, extra_1, extra_2=extra_1) @test CodeDiffs.issame(diff) - diff = CodeDiffs.compare_code_llvm(f, Tuple{}, w₁, w₂; color=false, debuginfo=:none) + diff = CodeDiffs.code_diff((f, Tuple{}), (f, Tuple{}); type=:llvm, color=false, debuginfo=:none, extra_1, extra_2) @test !CodeDiffs.issame(diff) @test occursin("1", diff.before) @test occursin("2", diff.after) + @test diff == (@code_diff type=:llvm color=false debuginfo=:none world_1=world_1 world_2=world_2 f() f()) end @testset "Native" begin - diff = CodeDiffs.compare_code_native(f, Tuple{}, w₁, w₁; color=false, debuginfo=:none) + diff = CodeDiffs.code_diff((f, Tuple{}), (f, Tuple{}); type=:native, color=false, debuginfo=:none, extra_1, extra_2=extra_1) @test CodeDiffs.issame(diff) - diff = CodeDiffs.compare_code_native(f, Tuple{}, w₁, w₂; color=false, debuginfo=:none) + diff = CodeDiffs.code_diff((f, Tuple{}), (f, Tuple{}); type=:native, color=false, debuginfo=:none, extra_1, extra_2) @test !CodeDiffs.issame(diff) - end - - @testset "AST" begin - @test_throws ErrorException CodeDiffs.compare_ast(f, Tuple{}, w₁, w₁; color=false) + @test diff == (@code_diff type=:native color=false debuginfo=:none world_1=world_1 world_2=world_2 f() f()) end end @@ -409,7 +413,9 @@ end end # Lines with changes use another code path for tabs alignment - diff = CodeDiffs.compare_show("\tabc\t123\n\tabc\t456", "\tabc\t126\n\tabc\t456") + a = "\tabc\t123\n\tabc\t456" + b = "\tabc\t126\n\tabc\t456" + diff = CodeDiffs.code_diff(a, b, a, b) diff_str = split(display_str(diff; color=false), '\n') @test startswith(diff_str[1], " abc 1") @test startswith(diff_str[2], " abc 4") @@ -417,14 +423,15 @@ end @testset "@code_diff" begin @static VERSION ≥ v"1.8-" && @testset "error" begin - @test_throws "not a function call" eval(:(@code_diff "f()" g())) - @test_throws "not a function call" eval(:(@code_diff f() "g()")) - @test_throws "not a function call" eval(:(@code_diff "f()" "g()")) - @test_throws "not a function call" eval(:(@code_diff a b)) - @test_throws "`key=value`, got: a" eval(:(@code_diff a b c)) + @test_throws "not a function call" @code_diff "f()" g() + @test_throws "not a function call" @code_diff f() "g()" + @test_throws "not a function call" @code_diff "f()" "g()" + @test_throws "not a function call" @code_diff a b + @test_throws "`key=value`, got: `a`" @code_diff a b c + @test_throws "world age" @code_diff type=:ast world_1=1 f() f() end - @testset "Keywords" begin + @testset "Kwargs" begin @testset "type=$t" for t in (:native, :llvm, :typed) # `type` can be a variable @code_diff type=t +(1, 2) +(2, 3) @@ -463,5 +470,11 @@ end a = :(1+2) @test !CodeDiffs.issame(@code_diff :($a) :(2+2)) end + + @testset "Extra" begin + d1 = @code_diff extra_1=(; type=:native, debuginfo=:none) extra_2=(; type=:llvm, color=false) f() f() + d2 = @code_diff type_1=:native debuginfo_1=:none type_2=:llvm color_2=false f() f() + @test d1 == d2 + end end end