Skip to content

Commit

Permalink
Merge pull request #9 from Keluaa/no_eval_args
Browse files Browse the repository at this point in the history
Replicate the behaviour of `@code_native`
  • Loading branch information
Keluaa authored Apr 21, 2024
2 parents 2790422 + c1e0d4f commit cb2125a
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 30 deletions.
103 changes: 87 additions & 16 deletions src/compare.jl
Original file line number Diff line number Diff line change
Expand Up @@ -612,18 +612,68 @@ code_diff(code₁::Tuple, code₂::Tuple; type::Symbol=:native, 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

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

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)
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)
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 [type] [option=value...] a b
@code_diff [option=value...] :(expr₁) :(expr₂)
Compare the methods called by the `f₁(...)` and `f₂(...)` expressions, and return a
[`CodeDiff`](@ref).
In the other form of `@code_diff`, `a` and `b` must be either variable names (`Symbol`s)
or quoted expressions (e.g. `@code_diff :(1+2) :(2+3)`): in this case the difference type
might be inferred automatically.
`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"))
Expand All @@ -633,19 +683,40 @@ macro code_diff(args...)
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, option.args[1], option.args[2])
return Expr(:kw, esc(option.args[1]), esc(option.args[2]))
end

code₁, code₂ = map((code₁, code₂)) do code
(!(code isa Expr) || code.head === :quote) && return code
code.head !== :call && throw(ArgumentError("expected call expression, got: $code"))
# `f(a, b)` => `(f, Base.typesof(a, b))`
f = code.args[1]
f_args = :(Base.typesof($(code.args[2:end]...)))
return :($f, $f_args)
end
# Simple values such as `:(1)` are stored in a `QuoteNode`
code₁ isa QuoteNode && (code₁ = Expr(:quote, Expr(:block, code₁.value)))
code₂ isa QuoteNode && (code₂ = Expr(:quote, Expr(:block, code₂.value)))

call_expr = :($code_diff($code₁, $code₂; ))
append!(call_expr.args[2].args, options)
return esc(call_expr)
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
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
end

return MacroTools.flatten(Expr(:block, expr₁, expr₂, call_code_diff))
end
end
30 changes: 23 additions & 7 deletions src/display.jl
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,15 @@ function next_ansi_sequence(str, idx, str_len)
end


const ANSI_DEFAULT_BKG = "\e[49m"
const ANSI_RED_BKG = "\e[41m"
const ANSI_GREEN_BKG = "\e[42m"

const ANSI_DEFAULT_FGR = "\e[39m"
const ANSI_RED_FGR = "\e[31m"
const ANSI_GREEN_FGR = "\e[32m"


function printstyled_code_line_diff(
io::IO, diff::DeepDiffs.StringDiff, highlighted_left, removed_only::Bool,
tab_replacement
Expand All @@ -80,9 +89,9 @@ function printstyled_code_line_diff(
ychars = DeepDiffs.after(diff.diff)

if get(io, :color, false)
default_bkg = "\e[49m" # ANSI for the default background color
removed_bkg_color = removed_only ? "\e[41m" : "" # ANSI for red background
added_bkg_color = removed_only ? "" : "\e[42m" # ANSI for green background
default_bkg = ANSI_DEFAULT_BKG
removed_bkg_color = removed_only ? ANSI_RED_BKG : ""
added_bkg_color = removed_only ? "" : ANSI_GREEN_BKG
else
default_bkg = ""
removed_bkg_color = ""
Expand Down Expand Up @@ -169,10 +178,17 @@ function side_by_side_diff(io::IO, diff::CodeDiff; tab_width=4, width=nothing, l
empty_line_num = ""
end

sep_same = ""
sep_removed = "⟪┫ "
sep_added = " ┣⟫"
sep_changed_to = "⟪╋⟫"
left_removed = ""
right_added = ""
if get(io, :color, false)
left_removed = ANSI_RED_FGR * left_removed * ANSI_DEFAULT_FGR
right_added = ANSI_GREEN_FGR * right_added * ANSI_DEFAULT_FGR
end

sep_same = ""
sep_removed = left_removed * ""
sep_added = "" * right_added
sep_changed_to = left_removed * "" * right_added

column_width = fld(width - length(sep_same), 2)
column_width 5 && error("output terminal width ($width) is too small")
Expand Down
12 changes: 6 additions & 6 deletions test/references/a_vs_b_COLOR.jl_ast
Original file line number Diff line number Diff line change
@@ -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"
  ┣⟫ 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
52 changes: 51 additions & 1 deletion test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ end
@test occursin("#= file:42 =#", diff.before)
diff = CodeDiffs.compare_ast(e, :(1+2); color=false, prettify=true, lines=false, alias=false)
@test CodeDiffs.issame(diff)
@test diff == (@code_diff type=:ast color=false e :(1+2))
@test diff == (@code_diff color=false :($e) :(1+2))
end

@testset "Basic function" begin
Expand Down Expand Up @@ -408,4 +408,54 @@ end
end
end
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))
end

@testset "Keywords" begin
@testset "type=$t" for t in (:native, :llvm, :typed)
# `type` can be a variable
@code_diff type=t +(1, 2) +(2, 3)
end
end

@static VERSION v"1.9-" && @testset "Special calls" begin
# From 1.9 `@code_native` (& others) support dot calls
@test !CodeDiffs.issame(@code_diff identity.(1:3) identity(1:3))
@test !CodeDiffs.issame(@code_diff (x -> x + 1).(1:3) identity(1:3))

sum_args(a, b; c=1, d=2) = a + b + c + d
@test !CodeDiffs.issame(@code_diff sum_args(1, 2) sum_args(1, 2; c=3, d=4))

# Apparently `@code_*` does not support broadcasts with kwargs. This should be
# a common error with both mecros.
expected_error = nothing
try
eval(:(@code_native sum_args.(1:5, 2; c=4)))
catch e
expected_error = sprint(Base.showerror, e)
end

actual_error = nothing
try
eval(:(@code_diff sum_args.(1:5, 2; c=4) sum_args.(1:3, 2:5)))
catch e
actual_error = sprint(Base.showerror, e)
end
@test expected_error == actual_error
end

@testset "AST" begin
@test !CodeDiffs.issame(@code_diff :(1) :(2))
@test !CodeDiffs.issame(@code_diff :(1+2) :(2+2))
a = :(1+2)
@test !CodeDiffs.issame(@code_diff :($a) :(2+2))
end
end
end

0 comments on commit cb2125a

Please sign in to comment.