Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"**/crs-config.json": "jsonc"
},
"cSpell.words": [
"Algorithmica",
"alist",
"autofmt",
"Barbin",
Expand Down Expand Up @@ -42,6 +43,7 @@
"Fpath",
"fprintf",
"Fsegment",
"gazagnaire",
"getcwd",
"gpatch",
"groupi",
Expand All @@ -57,6 +59,7 @@
"lsplit",
"lstrip",
"mapi",
"monopampam",
"Mathieu",
"mbarbin",
"MELPA",
Expand Down Expand Up @@ -99,6 +102,7 @@
"Streeters",
"stringable",
"stringp",
"tangled",
"textf",
"textutils",
"Uchar",
Expand Down
21 changes: 19 additions & 2 deletions NOTICE.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,23 @@ This project has a test dependency to the opam packages [printbox](https://opam.

A copy of the license file for the printbox project is located under `third-party-licenses/`.

## Windtrap.Myers
## Gazagnaire ocaml-merge3 (Myers diff)

`Myers` is vendored from the [windtrap](https://github.com/invariant-hq/windtrap) test framework by Thibaut Mattio (released under `ISC`) plus minor modifications documented in the file.
Myers is vendored from by Thomas Gazagnaire (released under `ISC`)
[ocaml-merge3](https://tangled.org/gazagnaire.org/ocaml-merge3). Only the pure
diff computation is vendored; the parts unused by this project are not included.
The exact provenance and list of changes are documented at the top of
`src/myers/merge3.ml` and in `src/myers/vendor.json`.

A copy of the license file for ocaml-merge3 is located under
`third-party-license/gazagnaire/ocaml-merge3/LICENSE`.

## Windtrap (unified-diff renderer)

The unified-diff renderer in `src/myers/myers.ml` is vendored from
[windtrap](https://github.com/invariant-hq/windtrap) by Invariant Systems
(released under `ISC`). The exact provenance and list of changes are documented
at the top of `src/myers/myers.ml` and in `src/myers/vendor.json`.

A copy of the license file for windtrap is located under
`third-party-license/invariant-hq/windtrap/LICENSE`.
1 change: 0 additions & 1 deletion crs-tests.opam
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ depends: [
"volgo-git-unix" {>= "0.0.21"}
"volgo-hg-unix" {>= "0.0.21"}
"volgo-vcs" {>= "0.0.21"}
"windtrap" {with-test & >= "0.1.0"}
"yojson" {>= "2.2.2"}
"odoc" {with-doc}
]
Expand Down
9 changes: 0 additions & 9 deletions dune-project
Original file line number Diff line number Diff line change
Expand Up @@ -131,15 +131,6 @@
(>= 0.0.21))
(volgo-vcs
(>= 0.0.21))
(windtrap
;; For the scope of the project the [:with-test] annotation below does not
;; match our conventions, as this is test-only package. But without it,
;; [opam-dune-lint] complains (it is only used by a dune [test] stanza atm.)
;; We wish to continue relying on a clean exit code for [opam-dune-lint] at
;; this time, so we added the annotation.
(and
:with-test
(>= 0.1.0)))
(yojson
(>= 2.2.2))))

Expand Down
7 changes: 1 addition & 6 deletions src/myers/COPYING.HEADER
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
crs-myers - Vendoring windtrap.myers with minor changes
crs-myers - Myers diff computation and unified-diff printing
Copyright (C) 2026 Mathieu Barbin <mathieu.barbin@gmail.com>
SPDX-License-Identifier: ISC

The code was vendored from [https://github.com/invariant-hq/windtrap].

Copyright (c) 2026 Invariant Systems. All rights reserved.
SPDX-License-Identifier: ISC
1 change: 1 addition & 0 deletions src/myers/dune
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
(library
(name crs_myers)
(package crs-tests)
(private_modules merge3)
(flags :standard -w +a-4-40-41-42-44-45-48-66 -warn-error +a)
(instrumentation
(backend bisect_ppx))
Expand Down
146 changes: 146 additions & 0 deletions src/myers/merge3.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
(**************************************************************************)
(* crs-myers - Myers diff computation and unified-diff printing *)
(* Copyright (C) 2026 Mathieu Barbin <mathieu.barbin@gmail.com> *)
(* SPDX-License-Identifier: ISC *)
(**************************************************************************)

(* Copyright (c) 2024-2026 Thomas Gazagnaire <thomas@gazagnaire.org> *)
(* SPDX-License-Identifier: ISC *)

(* Notice: This file was vendored from gazagnaire/ocaml-merge3 (the [Merge3]
module, [lib/merge3.ml]) as documented in [vendor.json] and the project's
root [NOTICE.md].

List of changes:

- Applied local project ocamlformat (janestreet profile).
- Removed the parts unused by this project.

- Replace use of globally-visible [Stdlib.Exit] exception by a custom one.
An [eq] that itself raised [Exit] would be silently swallowed and yield
a wrong diff. *)

(** {1 Myers' O(ND) Diff Algorithm}

E. W. Myers, "An O(ND) Difference Algorithm and Its Variations",
Algorithmica 1(2), 1986, pp. 251–266.

The algorithm finds the shortest edit script (SES) between two sequences. It
works by computing the furthest-reaching D-paths for increasing edit
distances D = 0, 1, 2, ... The key insight is that diagonal k = x - y
represents a state where x characters from [a] and y from [b] have been
consumed, and only even/odd diagonals are reachable at each step.

Time: O(ND) where N = |a| + |b| and D = edit distance. Space: O(D²) for the
trace (one V-array per step). *)

type 'a edit =
| Keep of 'a
| Delete of 'a
| Insert of 'a

(** Compute the furthest-reaching D-paths.

Records snapshots of the active V range [-d..d] (size 2d+1) at each step
instead of the full V array (size 2*max_d+1). This is the standard Myers
space optimisation: at step d only diagonals -d..d are reachable, so the
rest of V is unused. The trace becomes O(D²) instead of O(D*N), which is a
substantial win when D ≪ N (typical for incremental edits).

Returns [(D, trace)] where [trace.(d)] is an array of length [2*d+1] indexed
by [k+d] (so trace.(d).(0) holds V[-d], trace.(d).(2*d) holds V[d]). *)

exception Myers_done

let myers_forward ~eq ~off a b ~max_d =
let n = Array.length a
and m = Array.length b in
let vlen = (2 * max_d) + 1 in
let v = Array.make vlen 0 in
v.(off + 1) <- 0;
let trace = Array.make (max_d + 1) [||] in
let final_d = ref 0 in
(try
for d = 0 to max_d do
(* Snapshot only the active range used at step d (diagonals -d..d). *)
trace.(d) <- Array.sub v (off - d) ((2 * d) + 1);
for k0 = 0 to d do
let k = -d + (2 * k0) in
let x0 =
if k = -d || (k <> d && v.(off + k - 1) < v.(off + k + 1))
then v.(off + k + 1)
else v.(off + k - 1) + 1
in
let x = ref x0
and y = ref (x0 - k) in
while !x < n && !y < m && eq a.(!x) b.(!y) do
incr x;
incr y
done;
v.(off + k) <- !x;
if !x >= n && !y >= m
then (
final_d := d;
raise_notrace Myers_done)
done
done
with
| Myers_done -> ());
!final_d, trace
;;

(** Backtrack one step in the Myers trace, emitting the snake's [Keep]
operations and the single non-diagonal edit. Returns the previous [(x, y)]
position.

[vv] is the snapshot at step [dd]: an array of length [2*dd+1] where
[vv.(k+dd)] holds the V value for diagonal [k]. *)
let backtrack_step ~vv ~dd ~x ~y a b edits =
let k = x - y in
(* The previous snapshot only has diagonals -(dd-1)..(dd-1), but we read
V[k-1] and V[k+1] from the current step's snapshot — those are guaranteed
to be in range because k ∈ [-dd, dd] and k±1 ∈ [-(dd+1), dd+1], but
critically when we make the choice we look at V[k-1] and V[k+1] from
the SAME snapshot (saved at the start of step dd, which is the V state
after step dd-1), so they're both in [-(dd-1), dd-1] ⊆ [-dd, dd]. *)
let v_at i = vv.(i + dd) in
let is_insert = k = -dd || (k <> dd && v_at (k - 1) < v_at (k + 1)) in
let snake_x = if is_insert then v_at (k + 1) else v_at (k - 1) + 1 in
for i = x - 1 downto snake_x do
edits := Keep a.(i) :: !edits
done;
if is_insert
then edits := Insert b.(snake_x - k - 1) :: !edits
else edits := Delete a.(snake_x - 1) :: !edits;
let prev_k = if is_insert then k + 1 else k - 1 in
let prev_x = v_at prev_k in
prev_x, prev_x - prev_k
;;

let diff ~eq (a : 'a array) (b : 'a array) : 'a edit list =
let n = Array.length a
and m = Array.length b in
if n = 0 && m = 0
then []
else if n = 0
then Array.to_list b |> List.map (fun x -> Insert x)
else if m = 0
then Array.to_list a |> List.map (fun x -> Delete x)
else (
let max_d = n + m in
let off = max_d in
let d, trace = myers_forward ~eq ~off a b ~max_d in
let edits = ref [] in
let x = ref n
and y = ref m in
for step = 0 to d - 1 do
let dd = d - step in
let nx, ny = backtrack_step ~vv:trace.(dd) ~dd ~x:!x ~y:!y a b edits in
x := nx;
y := ny
done;
for i = !x - 1 downto 0 do
edits := Keep a.(i) :: !edits
done;
!edits)
;;
31 changes: 31 additions & 0 deletions src/myers/merge3.mli
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
(*_*************************************************************************)
(*_ crs-myers - Myers diff computation and unified-diff printing *)
(*_ Copyright (C) 2026 Mathieu Barbin <mathieu.barbin@gmail.com> *)
(*_ SPDX-License-Identifier: ISC *)
(*_*************************************************************************)

(*_ Copyright (c) 2024-2026 Thomas Gazagnaire <thomas@gazagnaire.org> *)
(*_ SPDX-License-Identifier: ISC *)

(** Myers' O(ND) shortest-edit-script, vendored from gazagnaire/ocaml-merge3.

Only the pure diff computation is vendored; see [merge3.ml] and the root
[NOTICE.md] for the list of (non-algorithmic) parts removed. *)

(** An edit operation in the shortest edit script. *)
type 'a edit =
| Keep of 'a (** Line present in both sequences. *)
| Delete of 'a (** Line present in old, absent in new. *)
| Insert of 'a (** Line absent in old, present in new. *)

(** [diff ~eq a b] computes the shortest edit script from [a] to [b] using
Myers' O(ND) algorithm. [eq] is the equality predicate.

The result is a list of edits that transforms [a] into [b]:
- [Keep x]: line [x] is present in both
- [Delete x]: line [x] from [a] is removed
- [Insert x]: line [x] from [b] is added

Time: O(ND) where N = |a| + |b| and D = edit distance. Space: O(D²) for the
trace. *)
val diff : eq:('a -> 'a -> bool) -> 'a array -> 'a array -> 'a edit list
Loading