Skip to content

Commit

Permalink
Initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Keluaa committed Mar 31, 2024
1 parent 3939ab6 commit 597f8f7
Show file tree
Hide file tree
Showing 26 changed files with 3,085 additions and 33 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,6 @@ jobs:
shell: julia --project=docs --color=yes {0}
run: |
using Documenter: DocMeta, doctest
using CodeDifferences
DocMeta.setdocmeta!(CodeDifferences, :DocTestSetup, :(using CodeDifferences); recursive=true)
doctest(CodeDifferences)
using CodeDiffs
DocMeta.setdocmeta!(CodeDiffs, :DocTestSetup, :(using CodeDiffs); recursive=true)
doctest(CodeDiffs)
24 changes: 22 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,34 @@
name = "CodeDifferences"
name = "CodeDiffs"
uuid = "0d84036a-ccd8-408b-b2b2-9a2d9429e273"
authors = ["Luc Briand <[email protected]> and contributors"]
version = "1.0.0-DEV"

[deps]
DeepDiffs = "ab62b9b5-e342-54a8-a765-a90f495de1a6"
InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
StringDistances = "88034a9c-02f8-509d-84a9-84ec65e18404"
WidthLimitedIO = "b8c1c048-cf81-46c6-9da0-18c1d99e41f2"

[compat]
Aqua = "0.7"
DeepDiffs = "1"
InteractiveUtils = "1"
MacroTools = "0.5"
Markdown = "1"
ReferenceTests = "0.10"
StringDistances = "0.11"
Test = "1"
WidthLimitedIO = "1"
julia = "1.6"

[extras]
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
DeepDiffs = "ab62b9b5-e342-54a8-a765-a90f495de1a6"
InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
ReferenceTests = "324d217c-45ce-50fc-942e-d289b448e8cf"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Aqua", "Test"]
test = ["Aqua", "InteractiveUtils", "ReferenceTests", "Test"]
49 changes: 44 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,46 @@
# CodeDifferences
# CodeDiffs

[![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://Keluaa.github.io/CodeDifferences.jl/stable/)
[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://Keluaa.github.io/CodeDifferences.jl/dev/)
[![Build Status](https://github.com/Keluaa/CodeDifferences.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/Keluaa/CodeDifferences.jl/actions/workflows/CI.yml?query=branch%3Amain)
[![Coverage](https://codecov.io/gh/Keluaa/CodeDifferences.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/Keluaa/CodeDifferences.jl)
[![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://Keluaa.github.io/CodeDiffs.jl/stable/)
[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://Keluaa.github.io/CodeDiffs.jl/dev/)
[![Build Status](https://github.com/Keluaa/CodeDiffs.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/Keluaa/CodeDiffs.jl/actions/workflows/CI.yml?query=branch%3Amain)
[![Coverage](https://codecov.io/gh/Keluaa/CodeDiffs.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/Keluaa/CodeDiffs.jl)
[![Aqua](https://raw.githubusercontent.com/JuliaTesting/Aqua.jl/master/badge.svg)](https://github.com/JuliaTesting/Aqua.jl)

Compare code and display the difference in the terminal side-by-side.
Supports syntax highlighting.

The [`@code_diff`](@ref) macro is the main entry point. If possible, the code type will be
detected automatically, otherwise add e.g. `type=:native` for native assembly comparison:
```julia
julia> f1(a) = a + 1
f1 (generic function with 1 method)

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

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

julia> @code_diff type=:llvm debuginfo=:none f1(1) f2(1)
; Function Attrs: uwtable ┃ ; Function Attrs: uwtable
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
} ┃ }
```

## Supported languages

- native CPU assembly (output of `@code_native`)
- LLVM IR (output of `@code_llvm`)
- Typed Julia IR (output of `@code_typed`)
- Julia AST (any `Expr`)
5 changes: 4 additions & 1 deletion docs/Project.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[deps]
CodeDifferences = "0d84036a-ccd8-408b-b2b2-9a2d9429e273"
CodeDiffs = "0d84036a-ccd8-408b-b2b2-9a2d9429e273"
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"

[compat]
Documenter = "1"
12 changes: 6 additions & 6 deletions docs/make.jl
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
using CodeDifferences
using CodeDiffs
using Documenter

DocMeta.setdocmeta!(CodeDifferences, :DocTestSetup, :(using CodeDifferences); recursive=true)
DocMeta.setdocmeta!(CodeDiffs, :DocTestSetup, :(using CodeDiffs); recursive=true)

makedocs(;
modules=[CodeDifferences],
modules=[CodeDiffs],
authors="Luc Briand <[email protected]> and contributors",
sitename="CodeDifferences.jl",
sitename="CodeDiffs.jl",
format=Documenter.HTML(;
canonical="https://Keluaa.github.io/CodeDifferences.jl",
canonical="https://Keluaa.github.io/CodeDiffs.jl",
edit_link="main",
assets=String[],
),
Expand All @@ -18,6 +18,6 @@ makedocs(;
)

deploydocs(;
repo="github.com/Keluaa/CodeDifferences.jl",
repo="github.com/Keluaa/CodeDiffs.jl",
devbranch="main",
)
74 changes: 68 additions & 6 deletions docs/src/index.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,76 @@
```@meta
CurrentModule = CodeDifferences
CurrentModule = CodeDiffs
```

# CodeDifferences
# CodeDiffs

Documentation for [CodeDifferences](https://github.com/Keluaa/CodeDifferences.jl).
Compare different types of code and display it in the terminal.
For cleaner results, syntax highlighting is separated from the difference calculation.

```@index
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)

The [`@code_diff`](@ref) macro is the main entry point. If possible, the code type will be
detected automatically, otherwise add e.g. `type=:native` for native assembly comparison:

```jldoctest; setup=:(using CodeDiffs)

Check failure on line 21 in docs/src/index.md

View workflow job for this annotation

GitHub Actions / Documentation

doctest failure in src/index.md:21-46 ```jldoctest; setup=:(using CodeDiffs) 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)) ; Function Attrs: uwtable ┃ ; Function Attrs: uwtable define i64 @F1(i64 signext %0) #0 { ⟪╋⟫define i64 @F1(i8 signext %0) #0 { top: ┃ top: %1 = add i64 %0, 1 ⟪╋⟫ %2 = add nsw i64 %1, 1 ret i64 %1 ⟪╋⟫ ret i64 %2 ┣⟫ %1 = sext i8 %0 to i64 } ┃ } ┃ julia> f2(a) = a - 1 f2 (generic function with 1 method) julia> @code_diff type=:llvm debuginfo=:none color=false f1(1) f2(1) ; Function Attrs: uwtable ┃ ; Function Attrs: uwtable 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 } ┃ } ┃ ``` Subexpression: @code_diff type=:llvm debuginfo=:none color=false f1(Int64(1)) f1(Int8(1)) Evaluated output: define i64 @F1(i64 signext %0) #0 { ⟪╋⟫define i64 @F1(i8 signext %0) #0 { top: ┃ top: %1 = add i64 %0, 1 ⟪╋⟫ %2 = add nsw i64 %1, 1 ret i64 %1 ⟪╋⟫ ret i64 %2 ┣⟫ %1 = sext i8 %0 to i64 } ┃ } ┃ Expected output: ; Function Attrs: uwtable ┃ ; Function Attrs: uwtable define i64 @F1(i64 signext %0) #0 { ⟪╋⟫define i64 @F1(i8 signext %0) #0 { top: ┃ top: %1 = add i64 %0, 1 ⟪╋⟫ %2 = add nsw i64 %1, 1 ret i64 %1 ⟪╋⟫ ret i64 %2 ┣⟫ %1 = sext i8 %0 to i64 } ┃ } ┃ diff = Warning: Diff output requires color. ; Function Attrs: uwtable ┃ ; Function Attrs: uwtable define i64 @F1(i64 signext %0) #0 { ⟪╋⟫define i64 @F1(i8 signext %0) #0 { top: ┃ top: %1 = add i64 %0, 1 ⟪╋⟫ %2 = add nsw i64 %1, 1 ret i64 %1 ⟪╋⟫ ret i64 %2 ┣⟫ %1 = sext i8 %0 to i64 } ┃ } ┃

Check failure on line 21 in docs/src/index.md

View workflow job for this annotation

GitHub Actions / Documentation

doctest failure in src/index.md:21-46 ```jldoctest; setup=:(using CodeDiffs) 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)) ; Function Attrs: uwtable ┃ ; Function Attrs: uwtable define i64 @F1(i64 signext %0) #0 { ⟪╋⟫define i64 @F1(i8 signext %0) #0 { top: ┃ top: %1 = add i64 %0, 1 ⟪╋⟫ %2 = add nsw i64 %1, 1 ret i64 %1 ⟪╋⟫ ret i64 %2 ┣⟫ %1 = sext i8 %0 to i64 } ┃ } ┃ julia> f2(a) = a - 1 f2 (generic function with 1 method) julia> @code_diff type=:llvm debuginfo=:none color=false f1(1) f2(1) ; Function Attrs: uwtable ┃ ; Function Attrs: uwtable 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 } ┃ } ┃ ``` Subexpression: @code_diff type=:llvm debuginfo=:none color=false f1(1) f2(1) Evaluated output: 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 } ┃ } ┃ Expected output: ; Function Attrs: uwtable ┃ ; Function Attrs: uwtable 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 } ┃ } ┃ diff = Warning: Diff output requires color. ; Function Attrs: uwtable ┃ ; Function Attrs: uwtable 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 } ┃ } ┃
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))
; Function Attrs: uwtable ┃ ; Function Attrs: uwtable
define i64 @f1(i64 signext %0) #0 { ⟪╋⟫define i64 @f1(i8 signext %0) #0 {
top: ┃ top:
%1 = add i64 %0, 1 ⟪╋⟫ %2 = add nsw i64 %1, 1
ret i64 %1 ⟪╋⟫ ret i64 %2
┣⟫ %1 = sext i8 %0 to i64
} ┃ }
julia> f2(a) = a - 1
f2 (generic function with 1 method)
julia> @code_diff type=:llvm debuginfo=:none color=false f1(1) f2(1)
; Function Attrs: uwtable ┃ ; Function Attrs: uwtable
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
} ┃ }
```

```@autodocs
Modules = [CodeDifferences]
Setting the environment variable `"CODE_DIFFS_LINE_NUMBERS"` to `true` will display line
numbers on each side.

# Main functions

```@docs
CodeDiff
compare_code_native
compare_code_llvm
compare_code_typed
compare_ast
code_diff(::AbstractString, ::AbstractString)
code_diff(::Markdown.MD, ::Markdown.MD)
@code_diff
```

# Display functions

```@docs
optimize_line_changes!
replace_llvm_module_name
side_by_side_diff
```

# Internals

```@docs
LLVM_MODULE_NAME_REGEX
```
114 changes: 114 additions & 0 deletions src/CodeDiff.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@

"""
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`](@ref) on all codes.
Use [`optimize_line_changes!`](@ref) to improve the difference.
Fancy REPL output is done with [`side_by_side_diff`](@ref).
"""
struct CodeDiff <: DeepDiffs.DeepDiff
before::String
after::String
changed::Dict{Int, DeepDiffs.StringDiff}
ignore_added::Set{Int}
diff::DeepDiffs.VectorDiff
highlighted_before::String
highlighted_after::String
end


function CodeDiff(
diff::DeepDiffs.StringLineDiff,
highlighted_before::AbstractString, highlighted_after::AbstractString
)
return CodeDiff(
diff.before, diff.after, Dict(), Set(), diff.diff,
highlighted_before, highlighted_after
)
end

function CodeDiff(X, Y, highlighted_X, highlighted_Y)
return CodeDiff(DeepDiffs.deepdiff(X, Y), highlighted_X, highlighted_Y)
end

CodeDiff(X, Y) = CodeDiff(X, Y, X, Y)


DeepDiffs.before(diff::CodeDiff) = diff.before
DeepDiffs.after(diff::CodeDiff) = diff.after
DeepDiffs.added(diff::CodeDiff) = DeepDiffs.added(diff.diff)
DeepDiffs.removed(diff::CodeDiff) = DeepDiffs.removed(diff.diff)
DeepDiffs.changed(diff::CodeDiff) = diff.changed

issame(diff::CodeDiff) = isempty(DeepDiffs.added(diff)) && isempty(DeepDiffs.removed(diff))

Base.:(==)(d1::CodeDiff, d2::CodeDiff) = DeepDiffs.fieldequal(d1, d2)

Base.show(io::IO, ::MIME"text/plain", diff::CodeDiff) = side_by_side_diff(io, diff)

function Base.show(io::IO, diff::CodeDiff)
xlines = split(diff.before, '\n')
ylines = split(diff.after, '\n')
DeepDiffs.visitall(diff.diff) do idx, state, last
if state == :removed
printstyled(io, "- ", xlines[idx], color=:red)
elseif state == :added
printstyled(io, "+ ", ylines[idx], color=:green)
else
print(io, " ", xlines[idx])
end
!last && println(io)
end
end


"""
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 `tol`erance of the normalized string `dist`ance.
This does not aim to produce an optimal `CodeDiff`, but simply improve its display.
"""
function optimize_line_changes!(diff::CodeDiff; dist=StringDistances.Levenshtein(), tol=0.7)
xlines = split(diff.before, '\n')
ylines = split(diff.after, '\n')

empty!(diff.changed)
empty!(diff.ignore_added)
previously_removed = Vector{Int}()
removed_start = 1
iadded = 1

DeepDiffs.visitall(diff.diff) do idx, state, _
if state == :removed
# Removed lines are always iterated first, so they are compared against added lines
push!(previously_removed, idx)
elseif state == :added
iadded += 1
for (li, removed_line) in enumerate(previously_removed[removed_start:end])
if StringDistances.compare(xlines[removed_line], ylines[idx], dist) tol
diff.changed[removed_line] = DeepDiffs.deepdiff(xlines[removed_line], ylines[idx])
push!(diff.ignore_added, idx)
removed_start += li # The next added lines will start from the next removed line
break
end
end
else
# Treat conserved lines as a "reset" point
empty!(previously_removed)
removed_start = 1
end
end

return diff
end
5 changes: 0 additions & 5 deletions src/CodeDifferences.jl

This file was deleted.

24 changes: 24 additions & 0 deletions src/CodeDiffs.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
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 DeepDiffs
using InteractiveUtils
using MacroTools
using Markdown
using StringDistances
using WidthLimitedIO

export @code_diff

const ANSI_REGEX = r"(?>\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]))+"
const OhMYREPL_PKG_ID = Base.PkgId(Base.UUID("5fb14364-9ced-5910-84b2-373655c76a03"), "OhMyREPL")

include("CodeDiff.jl")
include("compare.jl")
include("display.jl")

end
Loading

0 comments on commit 597f8f7

Please sign in to comment.