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 byInteractiveUtils.print_native
) - LLVM IR (output of
@code_llvm
, highlighted byInteractiveUtils.print_llvm
) - Typed Julia IR (output of
@code_typed
, highlighted through theBase.show
method ofCore.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_diff
— Macro@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
.
option
s 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()
CodeDiffs.@code_for
— Macro@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).
option
s 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()
CodeDiffs.code_diff
— Functioncode_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
CodeDiffs.code_for_diff
— Functioncode_for_diff(f, types::Type{<:Tuple}; type=:native, color=true, cleanup=true, kwargs...)
code_for_diff(expr::Expr; type=:ast, color=true, kwargs...)
Fetches the code of f
with get_code(Val(type), f, types; kwargs...)
, cleans it up with cleanup_code(Val(type), code)
and highlights it using the appropriate code_highlighter(Val(type))
. The result is two String
s: one without and the other with highlighting.
CodeDiffs.CodeDiff
— TypeCodeDiff(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
.
Code fetching
CodeDiffs.code_native
— Functioncode_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
.
CodeDiffs.code_llvm
— Functioncode_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
.
CodeDiffs.code_typed
— Functioncode_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.
CodeDiffs.code_ast
— Functioncode_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 LineNumberNode
s and alias == true
will replace mangled names (or gensym
s) by more readable names.
CodeDiffs.get_code
— Functionget_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
:llvm
:code_llvm
:typed
:code_typed
:ast
:code_ast
Highlighting
CodeDiffs.code_highlighter
— Functioncode_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.
Cleanup
CodeDiffs.cleanup_code
— Functioncleanup_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
is applied to :native
and :llvm
code.
CodeDiffs.replace_llvm_module_name
— Functionreplace_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
replace_llvm_module_name(code::AbstractString, function_name)
Replace only LLVM module names for function_name
.
CodeDiffs.function_unique_gen_name_regex
— Functionfunction_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:
from
get_function_name
julia_<function_name>_<unique_num>
japi3_<function_name>_<unique_num>
japi1_<function_name>_<unique_num>
j_<function_name>_<unique_num>
j1_<function_name>_<unique_num>
jlcapi_<function_name>_<unique_num>
jfptr_<function_name>_<unique_num>
tojlinvoke<unique_num>
CodeDiffs.global_var_unique_gen_name_regex
— Functionglobal_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.
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.
Diff display
CodeDiffs.optimize_line_changes!
— Functionoptimize_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 tol
erance of the normalized string dist
ance.
This does not aim to produce an optimal CodeDiff
, but simply improve its display.
CodeDiffs.side_by_side_diff
— Functionside_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
.