Skip to content

Prove termination of (optimized) diff algorithm implementation - #37

Merged
ninioArtillero merged 8 commits into
seereason:masterfrom
tweag:xg/ses-termination
Aug 12, 2026
Merged

Prove termination of (optimized) diff algorithm implementation#37
ninioArtillero merged 8 commits into
seereason:masterfrom
tweag:xg/ses-termination

Conversation

@ninioArtillero

@ninioArtillero ninioArtillero commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

This PR adds an static check for the ses (smallest edit script) function termination by defining a wave front's distance to the endpoint as decreasing metric. This function is the implementation's entry point to the Myers diff algorithm, whose original specification waives the need for a termination proof by bounding the algorithm's outer loop by the known worst case edit script length (i.e. the sum of both input's lengths). The implicit termination proof contained in this check provides a compeling argument for the correctness of the implementation by stablishing a link with its original specification.

The check depends on:

  • The refactoring of dstep (2825ba1) and ses (572638c ) functions. The former resulting in a 4x performance improvement that required a new benchmark comparing inputs with a bigger size difference (607c8d3) to be noticed.
  • The addition of phantom (i.e. unused in source code) parameters to dstep and addsnake (8eba64a) to thread a notion of the edit grid within the call stack
  • And the inclusion of supplementary functions that are only relevant to the Liquid Haskell specifications and have no impact in the implementation whatsoever.

Refactorings

The "iterate, flatten and find" steps of ses are merged in a single "search" step. Formerly, the search for the endpoint was done using dropWhile (\dl -> poi dl /= lena || poj dl /= lenb) on a single infinite stream containing all nodes, from succesive iterations of the wave front, in order. Now, the seach :: Int -> [DL] -> [DI] does the same thing, inspecting one wave front iteration at a time, allowing Liquid Haskell to reason about the incremental reach of the wave front to the goal. Benchmarks (cabal bench) shows a negligeble performance gain on this change alone when tested on a local branch. This is still the case after the dstep optimization; before and after benchmark results are shown below.

The dstep optimization consist (informally) of narrowing the wave front to in-bound nodes only: checks are placed to drop non-competing and out-of-bound nodes. The performance gain on the existing benchmark is barely noticeable. However, the new benchmark comparing inputs differing noticeably in size shows the gain to be in the order of 4x for this case. Incidentally, this addresses #1 robustly for all disparities between input sizes, which the additional equations to getDiffBy added in #28 solved for the specific case of an input of size 0; these equations are removed to avoid the unnecessary complexity of this ad hoc case.
This refactoring is instrumental to prove dstep reduces wfDistanceToGoal (6590d8e), providing guarantee that the manhattanDistance is non-negative on all wave front nodes. Also, the previously local hStep and vStep functions where moved to the top level in to reflect them (bc324f5), making their unfoldings available in the refinement logic, in order to reduce the specification load: otherwise, they incur in substantive specifications of their own (as found before this change in 6590d8e). For details on how this refactoring is related to the wave front specification see [NOTE: diagonal-invariant].

Benchmarks

Before dstep refactoring (on 607c8d3)

Benchmark simple: RUNNING...
benchmarking diff bool lists/1000 bools
time                 5.747 ms   (5.610 ms .. 5.851 ms)
                     0.998 R�   (0.998 R� .. 0.999 R�)
mean                 5.534 ms   (5.503 ms .. 5.581 ms)
std dev              122.2  s   (89.75  s .. 175.3  s)

benchmarking diff bool lists/1000/500 bools
time                 7.767 ms   (7.691 ms .. 7.842 ms)
                     1.000 R�   (0.999 R� .. 1.000 R�)
mean                 7.736 ms   (7.698 ms .. 7.777 ms)
std dev              119.6  s   (96.57  s .. 148.0  s)

Benchmark simple: FINISH

After dstep refactoring, but before ses refactoring (on 6590d8e)

Benchmark simple: RUNNING...
benchmarking diff bool lists/1000 bools
time                 5.199 ms   (5.158 ms .. 5.263 ms)
                     0.999 R�   (0.999 R� .. 1.000 R�)
mean                 5.179 ms   (5.156 ms .. 5.202 ms)
std dev              68.38  s   (56.26  s .. 86.47  s)

benchmarking diff bool lists/1000/500 bools
time                 1.897 ms   (1.886 ms .. 1.912 ms)
                     1.000 R�   (0.999 R� .. 1.000 R�)
mean                 1.893 ms   (1.887 ms .. 1.899 ms)
std dev              21.91  s   (17.44  s .. 26.36  s)

Benchmark simple: FINISH

After both refactorings (on 52f18d4):

Benchmark simple: RUNNING...
benchmarking diff bool lists/1000 bools
time                 4.604 ms   (4.570 ms .. 4.629 ms)
                     1.000 R�   (1.000 R� .. 1.000 R�)
mean                 4.633 ms   (4.617 ms .. 4.651 ms)
std dev              53.36  s   (44.97  s .. 65.39  s)

benchmarking diff bool lists/1000/500 bools
time                 1.619 ms   (1.609 ms .. 1.630 ms)
                     1.000 R�   (1.000 R� .. 1.000 R�)
mean                 1.629 ms   (1.625 ms .. 1.634 ms)
std dev              16.82  s   (14.07  s .. 20.30  s)

Benchmark simple: FINISH

Phantom parameters and Liquid Haskell supplementary functions.

The new phantom parameters in dstep and addsnake carrying both input lengths are used to pass these values to the supplementary functions defined under the Proving the Algorithm's Termination in LiquidHaskell code section at the bottom of Diff.hs.

LiquidHaskell specific machinery leading to the definition of the WaveFront refinement type alias (introduced in #32) is moved to the new code section LiquidHaskell Wave Front Specification, also near the bottom of the file. The DLN alias is modified to take parameters for the input lengths, and a local invariant for the DL type is added in a using annotation to make the positivity of its coordinates available in contexts where their values where not pattern matched.

Comment thread src/Data/Algorithm/Diff.hs Outdated
Comment thread src/Data/Algorithm/Diff.hs Outdated
Comment thread src/Data/Algorithm/Diff.hs Outdated
Comment thread src/Data/Algorithm/Diff.hs Outdated
Comment thread src/Data/Algorithm/Diff/Refinement.hs Outdated
Comment thread src/Data/Algorithm/Diff.hs Outdated
Comment thread src/Data/Algorithm/Diff.hs Outdated
Comment thread src/Data/Algorithm/Diff.hs Outdated
Comment thread src/Data/Algorithm/Diff.hs Outdated
Comment thread src/Data/Algorithm/Diff.hs Outdated
@facundominguez

Copy link
Copy Markdown
Contributor

This PR should revert 33bf8bc, which is made redundant by a0f181c.

Comment thread src/Data/Algorithm/Diff.hs Outdated
Comment thread src/Data/Algorithm/Diff.hs Outdated
Comment thread src/Data/Algorithm/Diff.hs Outdated
@ninioArtillero
ninioArtillero force-pushed the xg/ses-termination branch 2 times, most recently from e5cb8c3 to 34f28ed Compare August 6, 2026 21:48
Comment thread bench/bench.hs Outdated
Comment thread src/Data/Algorithm/Diff.hs Outdated
Comment thread src/Data/Algorithm/Diff.hs Outdated
Comment thread src/Data/Algorithm/Diff.hs Outdated
@ninioArtillero
ninioArtillero force-pushed the xg/ses-termination branch 3 times, most recently from 5b0b7b7 to cd35bf8 Compare August 10, 2026 19:38
@ninioArtillero
ninioArtillero marked this pull request as ready for review August 10, 2026 19:38
@facundominguez

Copy link
Copy Markdown
Contributor

Very nice \o/

@ninioArtillero ninioArtillero changed the title Prove termination of diff algorithm implementation Prove termination of diff algorithm implementation with optimization Aug 12, 2026
@ninioArtillero ninioArtillero changed the title Prove termination of diff algorithm implementation with optimization Prove termination of (optimized) diff algorithm implementation Aug 12, 2026
@facundominguez
facundominguez force-pushed the xg/ses-termination branch 2 times, most recently from 4acf270 to fb216f2 Compare August 12, 2026 12:13
ninioArtillero and others added 4 commits August 12, 2026 09:35
The manhattan distance from a node towards the algorithm's end point (lena, lenb)
is used as termination metric for `addsnake`. Phantom parameters for both
input lengths are introduced to provide them as arguments to the metric.

The diagonal predicate `DiagPred` is a refinement type alias encoding the
condition of an equality predicate (such as `canDiag`) to enter the recursive call
inside `addsnake`. This allows Liquid Haskell to know that both coordinates
are smaller than its corresponding input length, those fulfilling
`manhattanDistance` preconditions.

`dstep` is extended to provide the required phantom parameters and is
temporarily ignored because proving node coordinates in a wave front are
within bounds (`manhatanDistance` preconditions) requires discarting
out-of-bounds nodes. This is implemented as an optimization in a following commit.

Co-authored-by: Facundo Domínguez <facundominguez@gmail.com>
Co-authored-by: Facundo Domínguez <facundominguez@gmail.com>
`dstep` is optimized by adding checks that prevent the production
of out-of-bound nodes (those that would exit the edit grid).
This solves the problem `getDiffBy` empty list equations
where intended to address in
seereason@33bf8bc
which are removed to avoid unnecessary complexity.

Adds a source note describing the design choice to leave aside a (small) possible
optimization in favor of reduced specification complexity.

Co-authored-by: Facundo Domínguez <facundominguez@gmail.com>
A `_wfDistanceToGoal` function is defined to be used as termination metric
for `ses`. In this commit, `dstep` is specified and checked to reduce it
from input to output.

The wave front diagonal condition is changed to look at the head
of the node list instead of the diagonal edit distance parameter,
and its nodes are specified to be within bounds.
Indeed, now that wave fronts are trimmed down to be within bounds,
we can no longer guarantee that the first node's diagonal matches
the edit distance.

The `stepAndMerge` specification is strengthen to preserve this new variant of
wave front diagonal invariant: With the current optimization of `dstep`,
wave fronts don't necessarily grow, but the 2-step specing is preserved.
Its postconditions stating that it decreases the measure if the result is
non-empty are split--(A -> B) and (A -> C) instead of (A -> B && C)-- for
better error reporting from LH.

`furthestReaching` is now reflected to simplify its postconditions, and
a precondition to check that edit trace of the compared nodes match is added.

Co-authored-by: Facundo Domínguez <facundominguez@gmail.com>
ninioArtillero and others added 4 commits August 12, 2026 09:43
The implementation of `ses` changes from a `dropWhile` driven search for
the algorithm's end point in a lazy stream composed of all wave front nodes,
to the explicit recursion of a wave-front wise search for such end point.

This change was designed to allow a termination proof using Liquid Haskell:
by inspecting each wave front separately, instead of all concatenated together in an
infinite stream, we can define a wave front metric as its minimum distance to
the endpoint and show it is reduced by the recursive calls (to `dstep`).

Performance-wise, we get a small optimization of the benchmark of ~12%
Co-authored-by: Facundo Domínguez <facundominguez@gmail.com>
`_kdiag` is reflected too, so that it can be unfolded by PLE
when used in definition of `_wfDiags`.
@ninioArtillero
ninioArtillero merged commit 777d81a into seereason:master Aug 12, 2026
13 checks passed
@ninioArtillero
ninioArtillero deleted the xg/ses-termination branch August 12, 2026 17:32
@ninioArtillero ninioArtillero mentioned this pull request Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants