CodeDiffs

Compare different types of code and display it in the terminal. For cleaner results, syntax highlighting is separated from the difference calculation.

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

The @code_diff 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:

julia> f1(a) = a + 1
f1 (generic function with 1 method)

julia> @code_diff type=:llvm debuginfo=:none color=false f1(Int64(1)) f1(Int8(1))
define i64 @f1(i64 signext %0) #0 {   ⟪╋⟫define i64 @f1(i8 signext %0) #0 {
top:                                   ┃ top:
                                       ┣⟫  %1 = sext i8 %0 to i64
  %1 = add i64 %0, 1                  ⟪╋⟫  %2 = add nsw i64 %1, 1
  ret i64 %1                          ⟪╋⟫  ret i64 %2
}                                      ┃ }

julia> f2(a) = a - 1
f2 (generic function with 1 method)

julia> @code_diff type=:llvm debuginfo=:none color=false f1(1) f2(1)
define i64 @f1(i64 signext %0) #0 {   ⟪╋⟫define i64 @f2(i64 signext %0) #0 {
top:                                   ┃ top:
  %1 = add i64 %0, 1                  ⟪╋⟫  %1 = add i64 %0, -1
  ret i64 %1                           ┃   ret i64 %1
}                                      ┃ }

Setting the environment variable "CODE_DIFFS_LINE_NUMBERS" to true will display line numbers on each side:

julia> ENV["CODE_DIFFS_LINE_NUMBERS"] = true
true

julia> @code_diff type=:llvm debuginfo=:none color=false f1(1) f2(1)
1 define i64 @f1(i64 signext %0) #0 { ⟪╋⟫define i64 @f2(i64 signext %0) #0 { 1
2 top:                                 ┃ top:                                2
3   %1 = add i64 %0, 1                ⟪╋⟫  %1 = add i64 %0, -1               3
4   ret i64 %1                         ┃   ret i64 %1                        4
5 }                                    ┃ }                                   5

Comparison entry points

CodeDiffs.@code_diffMacro
@code_diff [type=:native] [color=true] [cleanup=true] [option=value...] f₁(...) f₂(...)
@code_diff [option=value...] :(expr₁) :(expr₂)

Compare the methods called by the f₁(...) and f₂(...) or the expressions expr₁ and expr₂, then return a CodeDiff.

options are passed to get_code. 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.

To compare Expr in variables, use @code_diff :($a) :($b).

cleanup == true will use cleanup_code to make the codes more prone to comparisons (e.g. by renaming variables with names which change every time).

# Default comparison
@code_diff type=:native f() g()

# No debuginfo for `f()` and `g()`
@code_diff type=:native debuginfo=:none f() g()

# No debuginfo for `f()`
@code_diff type=:native debuginfo_1=:none f() g()

# No debuginfo for `g()`
@code_diff type=:native debuginfo_2=:none f() g()

# 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()

# `type` and `color` can also be made different in each side
@code_diff type_1=:native type_2=:llvm f() f()
source
CodeDiffs.@code_forMacro
@code_for [type=:native] [color=true] [io=stdout] [cleanup=true] [option=value...] f(...)
@code_for [option=value...] :(expr)

Display the code of f(...) to io for the given code type, or the expression expr (AST only).

options are passed to get_code. To display an Expr in a variable, use @code_for :($expr).

io defaults to stdout. If io == String, then the code is not printed and is simply returned.

If the type option is the first option, "type" can be omitted: i.e. @code_for :llvm f() is valid.

cleanup == true will use cleanup_code on the code.

# Default comparison
@code_for type=:native f()

# Without debuginfo
@code_for type=:llvm debuginfo=:none f()

# Options sets can be passed from variables with the `extra` options
opts = (; debuginfo=:none, world=Base.get_world_counter())
@code_for type=:typed extra=opts f()

# Same as above, but shorter since we can omit "type"
@code_for :typed extra=opts f()
source
CodeDiffs.code_diffFunction
code_diff(args₁::Tuple, args₂::Tuple; extra_1=(;), extra_2=(;), kwargs...)

Function equivalent to @code_diff(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 only with args₁ and args₂ respectively.

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
source
CodeDiffs.CodeDiffType
CodeDiff(code₁, code₂)
CodeDiff(code₁, code₂, highlighted₁, highlighted₂)

A difference between code₁ and code₂.

code₁ and code₂ should have no highlighting. Only highlighted₁ and highlighted₂ should have syntax highlighting. When showing the differences, their formatting will be re-applied.

For cleaner differences, use replace_llvm_module_name on all codes.

Use optimize_line_changes! to improve the difference.

Fancy REPL output is done with side_by_side_diff.

source

Code fetching

CodeDiffs.code_nativeFunction
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.

source
CodeDiffs.code_llvmFunction
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.

source
CodeDiffs.code_typedFunction
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.

source
CodeDiffs.code_astFunction
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 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) is used to cleanup the AST. lines == true will keep the LineNumberNodes and alias == true will replace mangled names (or gensyms) by more readable names.

source

Highlighting

CodeDiffs.code_highlighterFunction
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 AbstractStrings 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.

source

Cleanup

CodeDiffs.replace_llvm_module_nameFunction
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.

In Julia 1.11+, global variables names are also replaced with global_var_unique_gen_name_regex.

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
source
replace_llvm_module_name(code::AbstractString, function_name)

Replace only LLVM module names for function_name.

source
CodeDiffs.function_unique_gen_name_regexFunction
function_unique_gen_name_regex()
function_unique_gen_name_regex(function_name)

Regex matching all LLVM function names which might change from one compilation to another. As an example, in the outputs of @code_llvm below:

julia> f() = 1
f (generic function with 1 method)

julia> @code_llvm f()
...
define i64 @julia_f_855() #0 {
...

julia> @code_llvm f()
...
define i64 @julia_f_857() #0 {
...

the regex will match julia_f_855 and julia_f_857.

function_unique_gen_name_regex() should work for any function which does not have any characters in '",;- or spaces in its name. The function name is either in the capture group 1 or 2.

function_unique_gen_name_regex(function_name) should work with any generated name for the given function name.

It is 'globalUniqueGeneratedNames' in 'julia/src/codegen.cpp' which gives the unique number on the generated code. The regex matches most usages of this counter:

source
CodeDiffs.global_var_unique_gen_name_regexFunction
global_var_unique_gen_name_regex()
global_var_unique_gen_name_regex(global_name)

Regex matching all global variable names which might change from one compilation to another.

Julia 1.11

Those global variables names only appear starting from Julia 1.11.

In LLVM IR, those variables are mentioned as such: @"+Core.GenericMemory#14067.jit". In native code, they look like this: ".L+Core.GenericMemory#13985.jit", with maybe some .set and .size sections at the end of the code (in x86 ASM).

global_var_unique_gen_name_regex() should work for any variable which does not have any characters in '",;- or spaces in its name.

global_var_unique_gen_name_regex(global_name) should work with any generated name for the given variable name.

It is 'globalUniqueGeneratedNames' in 'julia/src/codegen.cpp' which gives the unique number on the generated code. The regex matches only a single usage of this counter: in julia_pgv(ctx, cname, addr) at 'src/cgutils.cpp#L358' which is then added a ".jit" suffix in 'src/aotcompile.cpp#L2064' when doing code introspection.

source

Diff display

CodeDiffs.optimize_line_changes!Function
optimize_line_changes!(diff::CodeDiff; dist=Levenshtein(), tol=0.7)

Merges consecutive line removals+additions into single line changes in diff, when they are within the tolerance of the normalized string distance.

This does not aim to produce an optimal CodeDiff, but simply improve its display.

source
CodeDiffs.side_by_side_diffFunction
side_by_side_diff([io::IO,] diff::CodeDiff; tab_width=4, width=nothing, line_numbers=nothing)

Side by side display of a CodeDiff to io (defaults to stdout).

width defaults to the width of the terminal. It is 80 by default for non-terminal io.

tab_width is the number of spaces tabs are replaced with.

line_numbers=true will add line numbers on each side of the columns. It defaults to the environment variable "CODE_DIFFS_LINE_NUMBERS", which itself defaults to false.

source