From 9f96940cea6d4517b848be5cb6ab3730e354077d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20G=C3=B3ngora?= Date: Wed, 6 May 2026 13:59:46 -0600 Subject: [PATCH 01/19] Ignore `dstep` to prove `addsnake` termination 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. --- src/Data/Algorithm/Diff.hs | 63 ++++++++++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/src/Data/Algorithm/Diff.hs b/src/Data/Algorithm/Diff.hs index f408393..e352abb 100644 --- a/src/Data/Algorithm/Diff.hs +++ b/src/Data/Algorithm/Diff.hs @@ -169,6 +169,19 @@ furthestReaching x y | poi x >= poi y = x | otherwise = y +-- * Proving the algorithm termination in Liquid Haskell +-- +-- The original algorithm is known to terminate because a wave front /eventually/ reaches the 'endPoint'. +-- To prove this, both inputs lengths are threaded within phantom parameters throughout the implementation. +-- In essence, both lengths are used to encode the edit grid and its end point. + +{-@ inline _manhattanDistance @-} +{-@ _manhattanDistance :: lena : Nat -> lenb : Nat -> {i : Nat | lena >= i} -> { j : Nat | lenb >= j} -> Nat @-} +_manhattanDistance :: Int -> Int -> Int -> Int -> Int +_manhattanDistance lena lenb i j = lena - i + lenb - j + +{-@ type DiagPred M N = i : Nat -> j : Nat -> {b : Bool | ((i >= M || j >= N) => not b)} @-} + -- | Build a /diagonal predicate/ — a closure that tests whether position -- @(i, j)@ in the edit graph has a diagonal edge (a /match point/ in Myers' -- terminology). @@ -186,6 +199,7 @@ canDiag eq as bs lena lenb = \ i j -> arAs = listArray (0,lena - 1) as arBs = listArray (0,lenb - 1) bs +{-@ ignore dstep @-} -- | Perform one breadth-first search expansion step, advancing every wave front -- 'DL' node by one 'DI' edit (one non-diagonal edge) and then following -- any available snake. @@ -210,22 +224,28 @@ canDiag eq as bs lena lenb = \ i j -> -- with one more node than the input. {-@ dstep - :: (Nat -> Nat -> Bool) + :: lena : Nat + -> lenb : Nat + -> DiagPred lena lenb -> d : Nat -> {nodes : WaveFront d | len nodes > 0} -> {v : WaveFront (d + 1) | len v = len nodes + 1} @-} dstep - :: (Int -> Int -> Bool) -- ^ Diagonal predicate + :: Int -- ^ First input's length phantom parameter for termination check. + -> Int -- ^ Second input's length phantom parameter for termination check. + -> (Int -> Int -> Bool) -- ^ Diagonal predicate -> Int -- ^ The current D-length; used for the static check of wave front invariant. -> [DL] -- ^ A non-empty wave front of nodes at edit distance D -> [DL] -- ^ A non-empty wave front of nodes at edit distance D+1 --- NOTE: @_d@ is a phantom (apparently unused) parameter required by local LiquidHaskell specifications. --- This parameter sits at the first equation as a workaround --- to GHC removing it when desugaring multi-equation definitions. --- See https://github.com/ucsd-progsys/liquidhaskell/issues/2704 -dstep _ _d [] = error "dstep: Cannot perform expansion on an empty list of nodes" -dstep cd _ (dl:dls) = addsnake cd (hStep dl) : stepAndMerge dl dls +-- FIXME: @lena@, @lenb@ and @_d@ in the first equation are phantom (apparently unused) +-- parameters required by local LiquidHaskell specifications. +-- They sit at the first equation as a workaround to GHC removing them +-- when desugaring multi-equation definitions. +-- All could be replaced by underscores after fixing: +-- https://github.com/ucsd-progsys/liquidhaskell/issues/2704 +dstep lena lenb _ _d [] = error "dstep: Cannot perform expansion on an empty list of nodes" +dstep lena lenb cd _ (dl:dls) = addsnake lena lenb cd (hStep dl) : stepAndMerge dl dls where {-@ hStep :: x : DLN _d -> {v : DLN (_d + 1) | _kdiag v = _kdiag x + 1} @-} hStep node = node {poi = poi node + 1, path = F : path node} @@ -239,11 +259,10 @@ dstep cd _ (dl:dls) = addsnake cd (hStep dl) : stepAndMerge dl dls -> {v : [DLN (_d+1)] | _wfDiags (_kdiag prev - 1) v && len v = len rest + 1} / [len rest] @-} stepAndMerge :: DL -> [DL] -> [DL] - stepAndMerge prev [] = [addsnake cd $ vStep prev] + stepAndMerge prev [] = [addsnake lena lenb cd $ vStep prev] stepAndMerge prev (next:rest) = - addsnake cd (furthestReaching (vStep prev) (hStep next)) : stepAndMerge next rest + addsnake lena lenb cd (furthestReaching (vStep prev) (hStep next)) : stepAndMerge next rest -{-@ lazy addsnake @-} -- | Follow a /snake/ from the current position of a 'DL' node. -- -- A snake is a sequence of diagonal (cost-free) edges in the edit graph, @@ -252,10 +271,22 @@ dstep cd _ (dl:dls) = addsnake cd (hStep dl) : stepAndMerge dl dls -- @(poi dl, poj dl)@, this function advances both 'poi' and 'poj' as long -- as consecutive elements match, leaving 'path' unchanged (diagonal moves -- are not recorded as edit steps). -{-@ addsnake :: (Nat -> Nat -> Bool) -> x : DL -> {v : DL | path v == path x && _kdiag v = _kdiag x} @-} -addsnake :: (Int -> Int -> Bool) -> DL -> DL -addsnake cd dl - | cd pi pj = addsnake cd $ +{-@ +addsnake :: lena : Nat + -> lenb : Nat + -> DiagPred lena lenb + -> dl : DL + -> {v : DL | path v == path dl + && _kdiag v = _kdiag dl} + / [_manhattanDistance lena lenb (poi dl) (poj dl)] +@-} +addsnake :: Int -- ^ First input's length phantom parameter for termination check. + -> Int -- ^ Second input's length phantom parameter for termination check. + -> (Int -> Int -> Bool) -- ^ Diagonal predicate, a.k.a. 'canDiag' + -> DL + -> DL +addsnake lena lenb cd dl + | cd pi pj = addsnake lena lenb cd $ dl {poi = pi + 1, poj = pj + 1, path = path dl} | otherwise = dl where pi = poi dl; pj = poj dl @@ -290,7 +321,7 @@ addsnake cd dl -- unchanged. ses :: (a -> b -> Bool) -> [a] -> [b] -> [DI] ses eq as bs = path . head . dropWhile (\dl -> poi dl /= lena || poj dl /= lenb) . - concat . iterate (uncurry (dstep cd) . withD) . (:[]) . addsnake cd $ + concat . iterate (uncurry (dstep lena lenb cd) . withD) . (:[]) . addsnake lena lenb cd $ DL {poi=0,poj=0,path=[]} where cd = canDiag eq as bs lena lenb lena = length as; lenb = length bs From a0f181c450861893661607a91c74e2d4af4940fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20G=C3=B3ngora?= Date: Wed, 6 May 2026 13:59:46 -0600 Subject: [PATCH 02/19] Refactor/optimize `dstep` by removing out-of-bound nodes --- src/Data/Algorithm/Diff.hs | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/Data/Algorithm/Diff.hs b/src/Data/Algorithm/Diff.hs index e352abb..45e5eb0 100644 --- a/src/Data/Algorithm/Diff.hs +++ b/src/Data/Algorithm/Diff.hs @@ -245,7 +245,9 @@ dstep -- All could be replaced by underscores after fixing: -- https://github.com/ucsd-progsys/liquidhaskell/issues/2704 dstep lena lenb _ _d [] = error "dstep: Cannot perform expansion on an empty list of nodes" -dstep lena lenb cd _ (dl:dls) = addsnake lena lenb cd (hStep dl) : stepAndMerge dl dls +dstep lena lenb cd _ (dl:dls) = + if poi dl >= lena then stepAndMerge dl dls + else addsnake lena lenb cd (hStep dl) : stepAndMerge dl dls where {-@ hStep :: x : DLN _d -> {v : DLN (_d + 1) | _kdiag v = _kdiag x + 1} @-} hStep node = node {poi = poi node + 1, path = F : path node} @@ -258,10 +260,26 @@ dstep lena lenb cd _ (dl:dls) = addsnake lena lenb cd (hStep dl) : stepAndMerge -> {rest : [DLN _d] | _wfDiags (_kdiag prev - 2) rest} -> {v : [DLN (_d+1)] | _wfDiags (_kdiag prev - 1) v && len v = len rest + 1} / [len rest] @-} - stepAndMerge :: DL -> [DL] -> [DL] - stepAndMerge prev [] = [addsnake lena lenb cd $ vStep prev] - stepAndMerge prev (next:rest) = - addsnake lena lenb cd (furthestReaching (vStep prev) (hStep next)) : stepAndMerge next rest + stepAndMerge prev nodes = + -- When a node lying on the bottom boundary is found on the wave front + -- all upcoming nodes are discarted because their in-bound childs would + -- eventually need to cross the former's diagonal (in /more/ steps) + -- to reach the endpoint, and thus are not SES candidates. + if poj prev >= lenb then [] + else case nodes of + [] -> [addsnake lena lenb cd $ vStep prev] + (next:rest) -> + -- HACK: This check saves us from an unneeded call to furthestReaching, + -- as the horizontal child of the next node would be out-of-bounds, + -- but in fact we could drop this child node altogether because + -- the next node being on the right border means all previous nodes + -- would need to cross the next node's diagonal in more steps, + -- and thus cannot compete to the endpoint. + -- However, this would result in a negligible performance gain + -- and the loss of the wave front diagonal invariant, + -- so we keep it for now. + if poi next >= lena then addsnake lena lenb cd (vStep prev) : stepAndMerge next rest + else addsnake lena lenb cd (furthestReaching (vStep prev) (hStep next)) : stepAndMerge next rest -- | Follow a /snake/ from the current position of a 'DL' node. -- From c6c5a2e7027adf1abb6e1073534ad20809278e6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20G=C3=B3ngora?= Date: Wed, 6 May 2026 13:59:46 -0600 Subject: [PATCH 03/19] Prove `dstep` reduces distance to goal 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. --- src/Data/Algorithm/Diff.hs | 137 ++++++++++++++++++++++---- src/Data/Algorithm/Diff/Refinement.hs | 7 ++ 2 files changed, 125 insertions(+), 19 deletions(-) diff --git a/src/Data/Algorithm/Diff.hs b/src/Data/Algorithm/Diff.hs index 45e5eb0..19e832a 100644 --- a/src/Data/Algorithm/Diff.hs +++ b/src/Data/Algorithm/Diff.hs @@ -77,7 +77,8 @@ import Prelude hiding (pi) import Data.Array (listArray, (!)) import Data.Algorithm.Diff.Type import Data.Algorithm.Diff.Refinement (fst3, snd3, thd3, headIsFirst, - headIsSecond, headIsBoth, noStuttering) + headIsSecond, headIsBoth, noStuttering, + withProof) -- | /Diff Instruction/ — an internal enum recording the direction of a single -- non-diagonal edge traversed in the Myers edit graph. Every non-diagonal @@ -124,9 +125,14 @@ data DL = DL -- 'S' steps are stored. } deriving (Show, Eq) --- This refinement type alias represents a 'DL' value with a fixed /D-length/, --- which we call a "D-path location node". -{-@ type DLN D = { x : DL | len (path x) = D } @-} +-- Field refinements are only attached when a 'DL' is destructed; +-- here a local invariant is declared to make the coordinate non-negativity available +-- for opaque values (e.g. list elements reached through PLE unfoldings). +{-@ using (DL) as { dl : DL | poi dl >= 0 && poj dl >= 0 } @-} + +-- A "D-path location node" is a 'DL' value within the edit grid bounds +-- having a fixed /D-length/. +{-@ type DLN M N D = { x : DL | len (path x) = D && _withinBounds M N x} @-} {-@ inline _kdiag @-} -- | Computes the k-diagonal of a node. @@ -145,7 +151,7 @@ _wfDiags k (dl:dls) = poi dl - poj dl == k && _wfDiags (k - 2) dls -- A wave front is a list of 'DL' nodes, all at the same edit distance @D@, -- with k-diagonals @D@, @D−2@, …, @-D+2@, @-D@. -{-@ type WaveFront D = {xs : [DLN D] | _wfDiags D xs} @-} +{-@ type WaveFront M N D = {xs : [DLN M N D] | _wfDiags (_kdiag (head xs)) xs} @-} -- | Select the furthest-reaching candidate of two 'DL' nodes competing for the -- same k-diagonal, as required by the Myers algorithm. @@ -163,7 +169,8 @@ _wfDiags k (dl:dls) = poi dl - poj dl == k && _wfDiags (k - 2) dls -- > length (path x) == length (path y) {-@ furthestReaching :: x : DL -> {y : DL | _kdiag x = _kdiag y} - -> {v : DL | v = x || v = y} @-} + -> {v : DL | (v = x || v = y) + && poi v >= poi x && poi v >= poi y} @-} furthestReaching :: DL -> DL -> DL furthestReaching x y | poi x >= poi y = x @@ -180,6 +187,70 @@ furthestReaching x y _manhattanDistance :: Int -> Int -> Int -> Int -> Int _manhattanDistance lena lenb i j = lena - i + lenb - j +{-@ reflect _wfDistanceToGoal @-} +{-@ _wfDistanceToGoal :: lena : Nat -> lenb : Nat + -> nodes:[{dl:DL | _withinBounds lena lenb dl}] -> Nat / [len nodes] @-} + +-- | The smallest manhattan distance from a wave front node to the goal @(lena, lenb)@. +-- The empty wave front yields @lena + lenb + 1@, a sentinel strictly greater +-- than any in-bounds node's distance, acting as the identity for the minimum. +-- +-- NOTE: the body deliberately avoids helper functions ('min', '_manhattanDistance'): +-- calls to other lifted functions inside a reflected body prevent PLE +-- from unfolding this function's defining equations. +_wfDistanceToGoal :: Int -> Int -> [DL] -> Int +_wfDistanceToGoal lena lenb [] = lena + lenb + 1 +_wfDistanceToGoal lena lenb (dl:dls) = + if lena - poi dl + lenb - poj dl < _wfDistanceToGoal lena lenb dls + then lena - poi dl + lenb - poj dl + else _wfDistanceToGoal lena lenb dls + +-- | A wave front distance lower bound from its diagonal structure: +-- every node of a k-diagonal anchored wave front lies on a diagonal @k' <= k@ +-- and within bounds (@poj <= lenb@), hence the wave front distance to the goal +-- is at least @lena - lenb - k@. We can prove it like so: +-- +-- @ +-- (definition of k-diagonal) +-- k = poi - poj +-- => (algebraic manipulation) +-- poi = k + poj +-- => (algebraic manipulation) +-- lena - poi + lenb - poj = lena - (k + poj) + lenb - poj = lena - lenb - k + 2 * (lenb - poj) +-- => (within bounds: poj <= lenb) +-- lena - poi + lenb - poj >= lena - lenb - k +-- => (definition of manhattan distance) +-- _manhattanDistance lena lenb poi poj >= lena - lenb - k +-- @ +-- +-- In particular, this justifies discarding the nodes after a bottom-boundary node: +-- suppose @(i, lenb)@ is on diagonal @k + 2@, then the following nodes would +-- have distances at least @_manhattanDistance lena lenb i lenb + 2@, thus +-- they cannot beat that node's surviving children in the race to the goal. +{-@ _wfDistanceLowerBound + :: lena : Nat -> lenb : Nat -> k : Int + -> xs : {v : [{dl : DL | _withinBounds lena lenb dl}] | _wfDiags k v} + -> {_wfDistanceToGoal lena lenb xs == lena + lenb + 1 + || _wfDistanceToGoal lena lenb xs >= lena - lenb - k} + / [len xs] @-} +_wfDistanceLowerBound :: Int -> Int -> Int -> [DL] -> () +_wfDistanceLowerBound _ _ _ [] = () +_wfDistanceLowerBound lena lenb k (_:dls) = _wfDistanceLowerBound lena lenb (k - 2) dls + +{-@ inline _reducesDistanceToGoal @-} +{-@ _reducesDistanceToGoal :: lena : Nat -> lenb : Nat -> wf1:[{dl:DL | _withinBounds lena lenb dl}] -> wf2:[{dl:DL | _withinBounds lena lenb dl}] -> Bool @-} +_reducesDistanceToGoal :: Int -> Int -> [DL] -> [DL] -> Bool +_reducesDistanceToGoal lena lenb wf1 wf2 = _wfDistanceToGoal lena lenb wf2 < _wfDistanceToGoal lena lenb wf1 + +{-@ inline _withinBounds @-} +{-@ _withinBounds :: lena : Nat -> lenb : Nat -> dl : DL -> {v:Bool | v <=> (poi dl <= lena && poj dl <= lenb) } @-} +_withinBounds :: Int -> Int -> DL -> Bool +_withinBounds lena lenb dl = poi dl <= lena && poj dl <= lenb + +{-@ inline endPoint @-} +endPoint :: Int -> Int -> DL -> Bool +endPoint lena lenb dl = poi dl == lena && poj dl == lenb + {-@ type DiagPred M N = i : Nat -> j : Nat -> {b : Bool | ((i >= M || j >= N) => not b)} @-} -- | Build a /diagonal predicate/ — a closure that tests whether position @@ -199,7 +270,6 @@ canDiag eq as bs lena lenb = \ i j -> arAs = listArray (0,lena - 1) as arBs = listArray (0,lenb - 1) bs -{-@ ignore dstep @-} -- | Perform one breadth-first search expansion step, advancing every wave front -- 'DL' node by one 'DI' edit (one non-diagonal edge) and then following -- any available snake. @@ -228,8 +298,10 @@ dstep -> lenb : Nat -> DiagPred lena lenb -> d : Nat - -> {nodes : WaveFront d | len nodes > 0} - -> {v : WaveFront (d + 1) | len v = len nodes + 1} + -> {nodes : WaveFront lena lenb d | len nodes > 0 + && not (endPoint lena lenb (head nodes)) + && _wfDistanceToGoal lena lenb nodes > 0} + -> {v : WaveFront lena lenb (d + 1) | len v > 0 && _reducesDistanceToGoal lena lenb nodes v} @-} dstep :: Int -- ^ First input's length phantom parameter for termination check. @@ -247,19 +319,38 @@ dstep dstep lena lenb _ _d [] = error "dstep: Cannot perform expansion on an empty list of nodes" dstep lena lenb cd _ (dl:dls) = if poi dl >= lena then stepAndMerge dl dls - else addsnake lena lenb cd (hStep dl) : stepAndMerge dl dls + else + (addsnake lena lenb cd (hStep dl) : stepAndMerge dl dls) + -- If @dl@ lies on the bottom boundary, @stepAndMerge dl dls@ discards + -- all of @dls@; the lemma shows the discarded nodes are farther from + -- the goal than @dl@'s horizontal child. + `withProof` _wfDistanceLowerBound lena lenb (_kdiag dl - 2) dls where - {-@ hStep :: x : DLN _d -> {v : DLN (_d + 1) | _kdiag v = _kdiag x + 1} @-} + {-@ hStep + :: x : DLN lena lenb _d + -> {v : DL | len (path v) = _d + 1 && poi v = poi x + 1 && poj v = poj x} @-} hStep node = node {poi = poi node + 1, path = F : path node} - {-@ vStep :: x : DLN _d -> {v : DLN (_d + 1) | _kdiag v = _kdiag x - 1} @-} + {-@ vStep + :: x : DLN lena lenb _d + -> {v : DL | len (path v) = _d + 1 && poi v = poi x && poj v = poj x + 1} @-} vStep node = node {poj = poj node + 1, path = S : path node} -- Merge vertical step of previous node with horizontal step of next node, -- selecting the furthest-reaching candidate for each shared k-diagonal, -- and extend it along matching elements. - {-@ stepAndMerge :: prev : DLN _d - -> {rest : [DLN _d] | _wfDiags (_kdiag prev - 2) rest} - -> {v : [DLN (_d+1)] | _wfDiags (_kdiag prev - 1) v && len v = len rest + 1} - / [len rest] @-} + {-@ stepAndMerge + :: prev: DLN lena lenb _d + -> rest : {xs : [DLN lena lenb _d] | _wfDiags (_kdiag prev - 2) xs + && _wfDistanceToGoal lena lenb xs > 0} + -> {v : [DLN lena lenb (_d + 1)] | _wfDiags (_kdiag prev - 1) v + && (poj prev < lenb <=> len v > 0) + && (len v > 0 => + _kdiag (head v) == _kdiag prev - 1) + && (poj prev < lenb => + _wfDistanceToGoal lena lenb v + < _manhattanDistance lena lenb (poi prev) (poj prev) + && _wfDistanceToGoal lena lenb v + < _wfDistanceToGoal lena lenb rest)} + / [len rest] @-} stepAndMerge prev nodes = -- When a node lying on the bottom boundary is found on the wave front -- all upcoming nodes are discarted because their in-bound childs would @@ -279,7 +370,12 @@ dstep lena lenb cd _ (dl:dls) = -- and the loss of the wave front diagonal invariant, -- so we keep it for now. if poi next >= lena then addsnake lena lenb cd (vStep prev) : stepAndMerge next rest - else addsnake lena lenb cd (furthestReaching (vStep prev) (hStep next)) : stepAndMerge next rest + else + (addsnake lena lenb cd (furthestReaching (vStep prev) (hStep next)) : stepAndMerge next rest) + -- If @next@ lies on the bottom boundary, the recursive call + -- discards all of @rest@; the lemma shows the discarded nodes + -- are farther from the goal than the merged child. + `withProof` _wfDistanceLowerBound lena lenb (_kdiag next - 2) rest -- | Follow a /snake/ from the current position of a 'DL' node. -- @@ -293,9 +389,12 @@ dstep lena lenb cd _ (dl:dls) = addsnake :: lena : Nat -> lenb : Nat -> DiagPred lena lenb - -> dl : DL + -> {dl : DL | _withinBounds lena lenb dl} -> {v : DL | path v == path dl - && _kdiag v = _kdiag dl} + && _kdiag v = _kdiag dl + && _withinBounds lena lenb v + && poi v >= poi dl + && poj v >= poj dl} / [_manhattanDistance lena lenb (poi dl) (poj dl)] @-} addsnake :: Int -- ^ First input's length phantom parameter for termination check. diff --git a/src/Data/Algorithm/Diff/Refinement.hs b/src/Data/Algorithm/Diff/Refinement.hs index 8a9f1f8..f7132e8 100644 --- a/src/Data/Algorithm/Diff/Refinement.hs +++ b/src/Data/Algorithm/Diff/Refinement.hs @@ -91,3 +91,10 @@ snd3 :: (a, b, c) -> b snd3 (_, y, _) = y thd3 :: (a, b, c) -> c thd3 (_, _, z) = z + +-- | Attach a proof term (typically a lemma application) to a value. +-- The lemma's postcontition enters the verification context at the +-- application site while the value is returned unchanged. +{-@ withProof :: x:a -> b -> {v:a | v = x} @-} +withProof :: a -> b -> a +withProof x _ = x From 99137046ffa3f00bebc62c396aca649b3e0b41c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20G=C3=B3ngora?= Date: Wed, 6 May 2026 13:59:46 -0600 Subject: [PATCH 04/19] Refactor `ses` to use wave-front wise search for the endpoint. 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% --- src/Data/Algorithm/Diff.hs | 45 +++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/src/Data/Algorithm/Diff.hs b/src/Data/Algorithm/Diff.hs index 19e832a..062bce6 100644 --- a/src/Data/Algorithm/Diff.hs +++ b/src/Data/Algorithm/Diff.hs @@ -79,6 +79,7 @@ import Data.Algorithm.Diff.Type import Data.Algorithm.Diff.Refinement (fst3, snd3, thd3, headIsFirst, headIsSecond, headIsBoth, noStuttering, withProof) +import Data.Foldable (find) -- | /Diff Instruction/ — an internal enum recording the direction of a single -- non-diagonal edge traversed in the Myers edit graph. Every non-diagonal @@ -412,37 +413,37 @@ addsnake lena lenb cd dl -- | Compute shortest edit script (SES), as the minimum sequence of 'DI' edit -- steps that transforms @as@ into @bs@, returned in reverse order. -- --- @ses eq as bs@ runs the Myers O(ND) diff algorithm following --- a five-step pipeline: +-- @ses eq as bs@ runs the Myers O(ND) diff algorithm: -- --- 1. __Seed__: create an initial 0-path wave front @[addsnake cd (DL 0 0 [])]@ +-- 1. __Seed__: create an initial 0-path wave front @[addsnake lena lenb cd (DL 0 0 [])]@ -- having a single node on the tip of the longest origin-sourced snake. --- 2. __Iterate__: apply 'dstep' repeatedly via 'iterate', producing an --- infinite list of wave fronts (one per edit distance D = 0, 1, 2, …). --- 3. __Flatten__: 'concat' all wave fronts into a single stream of 'DL' nodes. --- 4. __Find__: 'dropWhile' skips nodes until one reaches @(lena, lenb)@ — the --- bottom-right corner of the edit graph — which is the terminal node of a --- shortest edit script. --- 5. __Extract__: 'head' returns that node; its 'path' field carries the edit +-- 2. __Search__: for each wave front at edit distance \( D = 0, 1, \ldots \), +-- check whether any node has reached the goal @(lena, lenb)@. If not, +-- apply 'dstep' to advance to edit distance \( D+1 \). +-- 3. __Extract__: the first goal node's 'path' field carries the edit -- trace in reverse order. -- --- This implementation is purely functional: rather than updating a shared --- diagonal frontier array in place, as in the original paper, it builds a new --- list of 'DL' nodes for each value of \( D \) and concatenates them into --- a single lazy stream. This is simpler but carries a larger per-node overhead: --- each 'DL' holds its own edit trace as a @['DI']@ list that structurally --- shares its tail with the parent node's trace (consing one step reuses the --- existing spine), rather than the paper's single-integer-per-diagonal --- representation. The asymptotic time +-- This implementation deviates from the paper in the folowing way: +-- rather than updating a shared diagonal frontier array in place, +-- as in the original paper, it builds a new list of 'DL' nodes +-- for each value of \( D \). This is simpler but carries a +-- larger per-node overhead: each 'DL' holds its own edit trace as a @['DI']@ +-- list that structurally shares its tail with the parent node's trace (consing +-- one step reuses the existing spine), rather than the paper's +-- single-integer-per-diagonal representation. The asymptotic time -- and space complexity — \( O(ND) \) and \( O(D^2) \) respectively — is -- unchanged. ses :: (a -> b -> Bool) -> [a] -> [b] -> [DI] -ses eq as bs = path . head . dropWhile (\dl -> poi dl /= lena || poj dl /= lenb) . - concat . iterate (uncurry (dstep lena lenb cd) . withD) . (:[]) . addsnake lena lenb cd $ - DL {poi=0,poj=0,path=[]} +ses eq as bs = search 0 [addsnake lena lenb cd (DL 0 0 [])] where cd = canDiag eq as bs lena lenb lena = length as; lenb = length bs - withD xs = (length . path . head $ xs, xs) + search :: Int -> [DL] -> [DI] + search _ [] = error "ses: The search must have a seed node" + search d wf = case findEndpoint lena lenb wf of + Just p -> path p + Nothing -> search (d + 1) (dstep lena lenb cd d wf) + findEndpoint :: Int -> Int -> [DL] -> Maybe DL + findEndpoint i j = find (endPoint i j) -- | Takes two lists and returns a list of differences between them. This is -- 'getDiffBy' with '==' used as predicate. From 5eda54d0a4e649c91e0849215be8571e219ffeaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20G=C3=B3ngora?= Date: Wed, 6 May 2026 13:59:46 -0600 Subject: [PATCH 05/19] `ses` termination proof --- src/Data/Algorithm/Diff.hs | 57 ++++++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 9 deletions(-) diff --git a/src/Data/Algorithm/Diff.hs b/src/Data/Algorithm/Diff.hs index 062bce6..02f3a96 100644 --- a/src/Data/Algorithm/Diff.hs +++ b/src/Data/Algorithm/Diff.hs @@ -238,6 +238,20 @@ _wfDistanceLowerBound :: Int -> Int -> Int -> [DL] -> () _wfDistanceLowerBound _ _ _ [] = () _wfDistanceLowerBound lena lenb k (_:dls) = _wfDistanceLowerBound lena lenb (k - 2) dls +-- | The termination metric is non-negative: every in-bounds node has a +-- non-negative manhattan distance to the goal, and the empty wave front +-- yields the positive sentinel. Needed because the @Nat@ result refinement +-- of the reflected '_wfDistanceToGoal' is not instantiated at logic-level +-- applications, while termination metrics must be provably non-negative. +{-@ _minDistanceNonNegative + :: lena : Nat -> lenb : Nat + -> xs : [{dl : DL | _withinBounds lena lenb dl}] + -> {_wfDistanceToGoal lena lenb xs >= 0} + / [len xs] @-} +_minDistanceNonNegative :: Int -> Int -> [DL] -> () +_minDistanceNonNegative _ _ [] = () +_minDistanceNonNegative lena lenb (_:dls) = _minDistanceNonNegative lena lenb dls + {-@ inline _reducesDistanceToGoal @-} {-@ _reducesDistanceToGoal :: lena : Nat -> lenb : Nat -> wf1:[{dl:DL | _withinBounds lena lenb dl}] -> wf2:[{dl:DL | _withinBounds lena lenb dl}] -> Bool @-} _reducesDistanceToGoal :: Int -> Int -> [DL] -> [DL] -> Bool @@ -263,13 +277,26 @@ endPoint lena lenb dl = poi dl == lena && poj dl == lenb -- -- The first two 'Int' parameters stand for the lengths of the input lists, -- which are captured from the outer scope to compute them only once. -canDiag :: (a -> b -> Bool) -> [a] -> [b] -> Int -> Int -> Int -> Int -> Bool -canDiag eq as bs lena lenb = \ i j -> - if i < lena && j < lenb then (arAs ! i) `eq` (arBs ! j) else False - where - -- Lists are converted into arrays to have O(1) lookups. - arAs = listArray (0,lena - 1) as - arBs = listArray (0,lenb - 1) bs +{-@ +canDiag :: (a -> b -> Bool) + -> [a] + -> [b] + -> lena : Int + -> lenb : Int + -> DiagPred lena lenb +@-} +canDiag :: (a -> b -> Bool) -- ^ Custom equality predicate + -> [a] -- ^ First input + -> [b] -- ^ Second input + -> Int -- ^ First input's length + -> Int -- ^ Second input's lenth + -> (Int -> Int -> Bool) -- ^ Diagonal predicate on the edit grid +canDiag eq as bs lena lenb = \i j -> + (i < lena && j < lenb) && ((arAs ! i) `eq` (arBs ! j)) + where + -- Lists are converted into arrays to have O(1) lookups. + arAs = listArray (0,lena - 1) as + arBs = listArray (0,lenb - 1) bs -- | Perform one breadth-first search expansion step, advancing every wave front -- 'DL' node by one 'DI' edit (one non-diagonal edge) and then following @@ -409,7 +436,6 @@ addsnake lena lenb cd dl | otherwise = dl where pi = poi dl; pj = poj dl -{-@ ignore ses @-} -- | Compute shortest edit script (SES), as the minimum sequence of 'DI' edit -- steps that transforms @as@ into @bs@, returned in reverse order. -- @@ -437,11 +463,24 @@ ses :: (a -> b -> Bool) -> [a] -> [b] -> [DI] ses eq as bs = search 0 [addsnake lena lenb cd (DL 0 0 [])] where cd = canDiag eq as bs lena lenb lena = length as; lenb = length bs + {-@ search :: d : Nat + -> {dls : WaveFront lena lenb d | len dls > 0} + -> {v : [DI] | len v >= d} + / [_wfDistanceToGoal lena lenb dls] @-} search :: Int -> [DL] -> [DI] search _ [] = error "ses: The search must have a seed node" search d wf = case findEndpoint lena lenb wf of Just p -> path p - Nothing -> search (d + 1) (dstep lena lenb cd d wf) + Nothing -> let wf' = dstep lena lenb cd d wf + in search (d + 1) + (wf' `withProof` _minDistanceNonNegative lena lenb wf') + -- The abstract refinement @q@ lets 'find' carry the wave + -- front element refinement (notably @len (path dl) == d@) + -- over to the returned endpoint. + {-@ assume findEndpoint :: forall Bool>. + i : Nat -> j : Nat -> xs : [DL] + -> { m : Maybe {dl : DL | endPoint i j dl} + | m == Nothing => _wfDistanceToGoal i j xs > 0} @-} findEndpoint :: Int -> Int -> [DL] -> Maybe DL findEndpoint i j = find (endPoint i j) From d02c7fcfcc41ab9ac930b72982d411a8ac91fcec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20G=C3=B3ngora?= Date: Mon, 3 Aug 2026 09:13:52 -0600 Subject: [PATCH 06/19] Move and reflect step functions in the top level to reduce spec --- src/Data/Algorithm/Diff.hs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Data/Algorithm/Diff.hs b/src/Data/Algorithm/Diff.hs index 02f3a96..ef1b091 100644 --- a/src/Data/Algorithm/Diff.hs +++ b/src/Data/Algorithm/Diff.hs @@ -298,6 +298,14 @@ canDiag eq as bs lena lenb = \i j -> arAs = listArray (0,lena - 1) as arBs = listArray (0,lenb - 1) bs +{-@ reflect hStep @-} +hStep :: DL -> DL +hStep node = node {poi = poi node + 1, path = F : path node} + +{-@ reflect vStep @-} +vStep :: DL -> DL +vStep node = node {poj = poj node + 1, path = S : path node} + -- | Perform one breadth-first search expansion step, advancing every wave front -- 'DL' node by one 'DI' edit (one non-diagonal edge) and then following -- any available snake. @@ -354,14 +362,6 @@ dstep lena lenb cd _ (dl:dls) = -- the goal than @dl@'s horizontal child. `withProof` _wfDistanceLowerBound lena lenb (_kdiag dl - 2) dls where - {-@ hStep - :: x : DLN lena lenb _d - -> {v : DL | len (path v) = _d + 1 && poi v = poi x + 1 && poj v = poj x} @-} - hStep node = node {poi = poi node + 1, path = F : path node} - {-@ vStep - :: x : DLN lena lenb _d - -> {v : DL | len (path v) = _d + 1 && poi v = poi x && poj v = poj x + 1} @-} - vStep node = node {poj = poj node + 1, path = S : path node} -- Merge vertical step of previous node with horizontal step of next node, -- selecting the furthest-reaching candidate for each shared k-diagonal, -- and extend it along matching elements. From d5ce74080b73677844317d4b5f86e9463da11a87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20G=C3=B3ngora?= Date: Mon, 3 Aug 2026 11:20:42 -0600 Subject: [PATCH 07/19] Rename spec variable to match source --- src/Data/Algorithm/Diff.hs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Data/Algorithm/Diff.hs b/src/Data/Algorithm/Diff.hs index ef1b091..ff556c8 100644 --- a/src/Data/Algorithm/Diff.hs +++ b/src/Data/Algorithm/Diff.hs @@ -366,9 +366,9 @@ dstep lena lenb cd _ (dl:dls) = -- selecting the furthest-reaching candidate for each shared k-diagonal, -- and extend it along matching elements. {-@ stepAndMerge - :: prev: DLN lena lenb _d - -> rest : {xs : [DLN lena lenb _d] | _wfDiags (_kdiag prev - 2) xs - && _wfDistanceToGoal lena lenb xs > 0} + :: prev : DLN lena lenb _d + -> nodes : {xs : [DLN lena lenb _d] | _wfDiags (_kdiag prev - 2) xs + && _wfDistanceToGoal lena lenb xs > 0} -> {v : [DLN lena lenb (_d + 1)] | _wfDiags (_kdiag prev - 1) v && (poj prev < lenb <=> len v > 0) && (len v > 0 => @@ -377,8 +377,8 @@ dstep lena lenb cd _ (dl:dls) = _wfDistanceToGoal lena lenb v < _manhattanDistance lena lenb (poi prev) (poj prev) && _wfDistanceToGoal lena lenb v - < _wfDistanceToGoal lena lenb rest)} - / [len rest] @-} + < _wfDistanceToGoal lena lenb nodes)} + / [len nodes] @-} stepAndMerge prev nodes = -- When a node lying on the bottom boundary is found on the wave front -- all upcoming nodes are discarted because their in-bound childs would From 1bf7759449eccf29937f7a17523f325e037c48b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20G=C3=B3ngora?= Date: Mon, 3 Aug 2026 09:13:52 -0600 Subject: [PATCH 08/19] Simplify workaround documentation --- src/Data/Algorithm/Diff.hs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Data/Algorithm/Diff.hs b/src/Data/Algorithm/Diff.hs index ff556c8..b1fe806 100644 --- a/src/Data/Algorithm/Diff.hs +++ b/src/Data/Algorithm/Diff.hs @@ -346,12 +346,8 @@ dstep -> Int -- ^ The current D-length; used for the static check of wave front invariant. -> [DL] -- ^ A non-empty wave front of nodes at edit distance D -> [DL] -- ^ A non-empty wave front of nodes at edit distance D+1 --- FIXME: @lena@, @lenb@ and @_d@ in the first equation are phantom (apparently unused) --- parameters required by local LiquidHaskell specifications. --- They sit at the first equation as a workaround to GHC removing them --- when desugaring multi-equation definitions. --- All could be replaced by underscores after fixing: --- https://github.com/ucsd-progsys/liquidhaskell/issues/2704 +-- @lena@, @lenb@ and @_d@ are named in the first equation as a workaround +-- to https://github.com/ucsd-progsys/liquidhaskell/issues/2704 dstep lena lenb _ _d [] = error "dstep: Cannot perform expansion on an empty list of nodes" dstep lena lenb cd _ (dl:dls) = if poi dl >= lena then stepAndMerge dl dls From 29d584a05b7c5af3fa4f8ddc97b2a95aa2454fb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20G=C3=B3ngora?= Date: Mon, 3 Aug 2026 12:14:16 -0600 Subject: [PATCH 09/19] Reflect `_manhattanDistance` to allow PLE unfolding of `_wfDistanceToGoal` --- src/Data/Algorithm/Diff.hs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Data/Algorithm/Diff.hs b/src/Data/Algorithm/Diff.hs index b1fe806..aaa0f5a 100644 --- a/src/Data/Algorithm/Diff.hs +++ b/src/Data/Algorithm/Diff.hs @@ -183,7 +183,7 @@ furthestReaching x y -- To prove this, both inputs lengths are threaded within phantom parameters throughout the implementation. -- In essence, both lengths are used to encode the edit grid and its end point. -{-@ inline _manhattanDistance @-} +{-@ reflect _manhattanDistance @-} {-@ _manhattanDistance :: lena : Nat -> lenb : Nat -> {i : Nat | lena >= i} -> { j : Nat | lenb >= j} -> Nat @-} _manhattanDistance :: Int -> Int -> Int -> Int -> Int _manhattanDistance lena lenb i j = lena - i + lenb - j @@ -195,15 +195,12 @@ _manhattanDistance lena lenb i j = lena - i + lenb - j -- | The smallest manhattan distance from a wave front node to the goal @(lena, lenb)@. -- The empty wave front yields @lena + lenb + 1@, a sentinel strictly greater -- than any in-bounds node's distance, acting as the identity for the minimum. --- --- NOTE: the body deliberately avoids helper functions ('min', '_manhattanDistance'): --- calls to other lifted functions inside a reflected body prevent PLE --- from unfolding this function's defining equations. _wfDistanceToGoal :: Int -> Int -> [DL] -> Int _wfDistanceToGoal lena lenb [] = lena + lenb + 1 _wfDistanceToGoal lena lenb (dl:dls) = - if lena - poi dl + lenb - poj dl < _wfDistanceToGoal lena lenb dls - then lena - poi dl + lenb - poj dl + -- We avoid using 'min' here so that LH can unfold this definition. + if _manhattanDistance lena lenb (poi dl) (poj dl) < _wfDistanceToGoal lena lenb dls + then _manhattanDistance lena lenb (poi dl) (poj dl) else _wfDistanceToGoal lena lenb dls -- | A wave front distance lower bound from its diagonal structure: From fa737616666606ebdf7996259af4cb609db4cf78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Facundo=20Dom=C3=ADnguez?= Date: Wed, 5 Aug 2026 10:33:02 +0000 Subject: [PATCH 10/19] Make explicit intermediate bound lemma --- src/Data/Algorithm/Diff.hs | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/Data/Algorithm/Diff.hs b/src/Data/Algorithm/Diff.hs index aaa0f5a..a872b1f 100644 --- a/src/Data/Algorithm/Diff.hs +++ b/src/Data/Algorithm/Diff.hs @@ -193,10 +193,10 @@ _manhattanDistance lena lenb i j = lena - i + lenb - j -> nodes:[{dl:DL | _withinBounds lena lenb dl}] -> Nat / [len nodes] @-} -- | The smallest manhattan distance from a wave front node to the goal @(lena, lenb)@. --- The empty wave front yields @lena + lenb + 1@, a sentinel strictly greater +-- The empty wave front yields @lena + lenb + 2@, a sentinel strictly greater -- than any in-bounds node's distance, acting as the identity for the minimum. _wfDistanceToGoal :: Int -> Int -> [DL] -> Int -_wfDistanceToGoal lena lenb [] = lena + lenb + 1 +_wfDistanceToGoal lena lenb [] = lena + lenb + 2 _wfDistanceToGoal lena lenb (dl:dls) = -- We avoid using 'min' here so that LH can unfold this definition. if _manhattanDistance lena lenb (poi dl) (poj dl) < _wfDistanceToGoal lena lenb dls @@ -225,15 +225,27 @@ _wfDistanceToGoal lena lenb (dl:dls) = -- suppose @(i, lenb)@ is on diagonal @k + 2@, then the following nodes would -- have distances at least @_manhattanDistance lena lenb i lenb + 2@, thus -- they cannot beat that node's surviving children in the race to the goal. -{-@ _wfDistanceLowerBound - :: lena : Nat -> lenb : Nat -> k : Int +{-@ _wfDistanceLowerBoundK + :: lena : Nat -> lenb : Nat -> {k : Int | lenb + k + 2 >= 0} -> xs : {v : [{dl : DL | _withinBounds lena lenb dl}] | _wfDiags k v} - -> {_wfDistanceToGoal lena lenb xs == lena + lenb + 1 - || _wfDistanceToGoal lena lenb xs >= lena - lenb - k} + -> {_wfDistanceToGoal lena lenb xs >= lena - lenb - k} / [len xs] @-} -_wfDistanceLowerBound :: Int -> Int -> Int -> [DL] -> () -_wfDistanceLowerBound _ _ _ [] = () -_wfDistanceLowerBound lena lenb k (_:dls) = _wfDistanceLowerBound lena lenb (k - 2) dls +_wfDistanceLowerBoundK :: Int -> Int -> Int -> [DL] -> () +_wfDistanceLowerBoundK _ _ _ [] = () +_wfDistanceLowerBoundK lena lenb k (_:dls) = _wfDistanceLowerBoundK lena lenb (k - 2) dls + +{-@ +_wfDistanceLowerBound + :: lena : Nat -> lenb : Nat -> {prev : DL | _withinBounds lena lenb prev} + -> xs : {v : [{dl : DL | _withinBounds lena lenb dl}] | _wfDiags (_kdiag prev - 2) v} + -> { poj prev >= lenb => _wfDistanceToGoal lena lenb xs > _manhattanDistance lena lenb (poi prev) (poj prev)} + @-} +_wfDistanceLowerBound :: Int -> Int -> DL -> [DL] -> () +_wfDistanceLowerBound lena lenb prev [] = () +_wfDistanceLowerBound lena lenb prev xs@(_:_) = () + where + _lemma = _wfDistanceLowerBoundK lena lenb (_kdiag prev - 2) xs + -- | The termination metric is non-negative: every in-bounds node has a -- non-negative manhattan distance to the goal, and the empty wave front @@ -353,7 +365,7 @@ dstep lena lenb cd _ (dl:dls) = -- If @dl@ lies on the bottom boundary, @stepAndMerge dl dls@ discards -- all of @dls@; the lemma shows the discarded nodes are farther from -- the goal than @dl@'s horizontal child. - `withProof` _wfDistanceLowerBound lena lenb (_kdiag dl - 2) dls + `withProof` _wfDistanceLowerBound lena lenb dl dls where -- Merge vertical step of previous node with horizontal step of next node, -- selecting the furthest-reaching candidate for each shared k-diagonal, @@ -396,7 +408,7 @@ dstep lena lenb cd _ (dl:dls) = -- If @next@ lies on the bottom boundary, the recursive call -- discards all of @rest@; the lemma shows the discarded nodes -- are farther from the goal than the merged child. - `withProof` _wfDistanceLowerBound lena lenb (_kdiag next - 2) rest + `withProof` _wfDistanceLowerBound lena lenb next rest -- | Follow a /snake/ from the current position of a 'DL' node. -- From 1833e3a204931efec5d7ee42410a76c6894cc417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20G=C3=B3ngora?= Date: Wed, 5 Aug 2026 11:18:05 -0600 Subject: [PATCH 11/19] Update documentation of lemmas and complete manual proof --- src/Data/Algorithm/Diff.hs | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/Data/Algorithm/Diff.hs b/src/Data/Algorithm/Diff.hs index a872b1f..ab4be98 100644 --- a/src/Data/Algorithm/Diff.hs +++ b/src/Data/Algorithm/Diff.hs @@ -205,8 +205,10 @@ _wfDistanceToGoal lena lenb (dl:dls) = -- | A wave front distance lower bound from its diagonal structure: -- every node of a k-diagonal anchored wave front lies on a diagonal @k' <= k@ --- and within bounds (@poj <= lenb@), hence the wave front distance to the goal --- is at least @lena - lenb - k@. We can prove it like so: +-- and within bounds, with diagonals ranging from @-lenb@ to @lena@, +-- hence the wave front distance to the goal is at least @lena - lenb - k@. +-- +-- We can prove it manually like so: -- -- @ -- (definition of k-diagonal) @@ -218,13 +220,16 @@ _wfDistanceToGoal lena lenb (dl:dls) = -- => (within bounds: poj <= lenb) -- lena - poi + lenb - poj >= lena - lenb - k -- => (definition of manhattan distance) --- _manhattanDistance lena lenb poi poj >= lena - lenb - k +-- (*) _manhattanDistance lena lenb poi poj >= lena - lenb - k +-- => (other node's lie in lower diagonals: k' <= k) +-- (**) lena - lenb - k' >= lena - lenb - k +-- => ((*) applied to k' and combined with (**)) +-- _manhattanDistance lena lenb poi' poj' >= lena - lenb - k +-- => (_wfDistanceToGoal is the minimum of all node's manhattan distances) +-- _wfDistanceToGoal lena lenb xs >= lena - lenb - k +-- +-- QED -- @ --- --- In particular, this justifies discarding the nodes after a bottom-boundary node: --- suppose @(i, lenb)@ is on diagonal @k + 2@, then the following nodes would --- have distances at least @_manhattanDistance lena lenb i lenb + 2@, thus --- they cannot beat that node's surviving children in the race to the goal. {-@ _wfDistanceLowerBoundK :: lena : Nat -> lenb : Nat -> {k : Int | lenb + k + 2 >= 0} -> xs : {v : [{dl : DL | _withinBounds lena lenb dl}] | _wfDiags k v} @@ -234,6 +239,11 @@ _wfDistanceLowerBoundK :: Int -> Int -> Int -> [DL] -> () _wfDistanceLowerBoundK _ _ _ [] = () _wfDistanceLowerBoundK lena lenb k (_:dls) = _wfDistanceLowerBoundK lena lenb (k - 2) dls +-- | If a wave front's node (@prev@) is on the bottom boundary, then the following +-- nodes lie farther from the goal. Intuitively, the reason is that the following +-- nodes children would need more steps to cross @prev@'s diagonal to reach the goal. +-- This lemma allows LH to reason about the case where nodes are discarded +-- after a bottom-boundary node within 'dstep'. {-@ _wfDistanceLowerBound :: lena : Nat -> lenb : Nat -> {prev : DL | _withinBounds lena lenb prev} From 4da712287537bafa346b2e6651c6b88370fcc67f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20G=C3=B3ngora?= Date: Wed, 5 Aug 2026 11:38:41 -0600 Subject: [PATCH 12/19] Remove unnecessary metric related specs --- src/Data/Algorithm/Diff.hs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Data/Algorithm/Diff.hs b/src/Data/Algorithm/Diff.hs index ab4be98..9daae49 100644 --- a/src/Data/Algorithm/Diff.hs +++ b/src/Data/Algorithm/Diff.hs @@ -184,14 +184,10 @@ furthestReaching x y -- In essence, both lengths are used to encode the edit grid and its end point. {-@ reflect _manhattanDistance @-} -{-@ _manhattanDistance :: lena : Nat -> lenb : Nat -> {i : Nat | lena >= i} -> { j : Nat | lenb >= j} -> Nat @-} _manhattanDistance :: Int -> Int -> Int -> Int -> Int _manhattanDistance lena lenb i j = lena - i + lenb - j {-@ reflect _wfDistanceToGoal @-} -{-@ _wfDistanceToGoal :: lena : Nat -> lenb : Nat - -> nodes:[{dl:DL | _withinBounds lena lenb dl}] -> Nat / [len nodes] @-} - -- | The smallest manhattan distance from a wave front node to the goal @(lena, lenb)@. -- The empty wave front yields @lena + lenb + 2@, a sentinel strictly greater -- than any in-bounds node's distance, acting as the identity for the minimum. @@ -272,7 +268,6 @@ _minDistanceNonNegative _ _ [] = () _minDistanceNonNegative lena lenb (_:dls) = _minDistanceNonNegative lena lenb dls {-@ inline _reducesDistanceToGoal @-} -{-@ _reducesDistanceToGoal :: lena : Nat -> lenb : Nat -> wf1:[{dl:DL | _withinBounds lena lenb dl}] -> wf2:[{dl:DL | _withinBounds lena lenb dl}] -> Bool @-} _reducesDistanceToGoal :: Int -> Int -> [DL] -> [DL] -> Bool _reducesDistanceToGoal lena lenb wf1 wf2 = _wfDistanceToGoal lena lenb wf2 < _wfDistanceToGoal lena lenb wf1 From d6c363fc2817d1e4ec5959a3f9009738d74442ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20G=C3=B3ngora?= Date: Wed, 5 Aug 2026 12:22:14 -0600 Subject: [PATCH 13/19] Remove unnecessary empty list equations from `getDiffBy` (revert #28) The optimization introduced in `dstep` (narrowing the wave front to only within bound nodes) solves the problem this additional equations addressed in https://github.com/seereason/Diff/commit/33bf8bc7f95a825b51f97e53848d39ef957546d9 They are removed to avoid unnecessary complexity. --- src/Data/Algorithm/Diff.hs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Data/Algorithm/Diff.hs b/src/Data/Algorithm/Diff.hs index 9daae49..ad8cb5a 100644 --- a/src/Data/Algorithm/Diff.hs +++ b/src/Data/Algorithm/Diff.hs @@ -517,8 +517,6 @@ getGroupedDiff = getGroupedDiffBy (==) -- | A form of 'getDiff' with no 'Eq' constraint. Instead, an equality predicate -- is taken as the first argument. getDiffBy :: (a -> b -> Bool) -> [a] -> [b] -> [PolyDiff a b] -getDiffBy _ a [] = map First a -getDiffBy _ [] b = map Second b getDiffBy eq a b = markup a b . reverse $ ses eq a b where markup (x:xs) (y:ys) ds | eq x y = Both x y : markup xs ys ds From dcbab9b1dc5cc7fb45a877750fb0e9b5087e046d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20G=C3=B3ngora?= Date: Wed, 5 Aug 2026 12:43:04 -0600 Subject: [PATCH 14/19] Refactor `manhattanDistance` to take a `DL` argument --- src/Data/Algorithm/Diff.hs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Data/Algorithm/Diff.hs b/src/Data/Algorithm/Diff.hs index ad8cb5a..a42ac55 100644 --- a/src/Data/Algorithm/Diff.hs +++ b/src/Data/Algorithm/Diff.hs @@ -184,8 +184,8 @@ furthestReaching x y -- In essence, both lengths are used to encode the edit grid and its end point. {-@ reflect _manhattanDistance @-} -_manhattanDistance :: Int -> Int -> Int -> Int -> Int -_manhattanDistance lena lenb i j = lena - i + lenb - j +_manhattanDistance :: Int -> Int -> DL -> Int +_manhattanDistance lena lenb dl = lena - (poi dl) + lenb - (poj dl) {-@ reflect _wfDistanceToGoal @-} -- | The smallest manhattan distance from a wave front node to the goal @(lena, lenb)@. @@ -195,8 +195,8 @@ _wfDistanceToGoal :: Int -> Int -> [DL] -> Int _wfDistanceToGoal lena lenb [] = lena + lenb + 2 _wfDistanceToGoal lena lenb (dl:dls) = -- We avoid using 'min' here so that LH can unfold this definition. - if _manhattanDistance lena lenb (poi dl) (poj dl) < _wfDistanceToGoal lena lenb dls - then _manhattanDistance lena lenb (poi dl) (poj dl) + if _manhattanDistance lena lenb dl < _wfDistanceToGoal lena lenb dls + then _manhattanDistance lena lenb dl else _wfDistanceToGoal lena lenb dls -- | A wave front distance lower bound from its diagonal structure: @@ -216,11 +216,11 @@ _wfDistanceToGoal lena lenb (dl:dls) = -- => (within bounds: poj <= lenb) -- lena - poi + lenb - poj >= lena - lenb - k -- => (definition of manhattan distance) --- (*) _manhattanDistance lena lenb poi poj >= lena - lenb - k +-- (*) _manhattanDistance lena lenb dl >= lena - lenb - k -- => (other node's lie in lower diagonals: k' <= k) -- (**) lena - lenb - k' >= lena - lenb - k --- => ((*) applied to k' and combined with (**)) --- _manhattanDistance lena lenb poi' poj' >= lena - lenb - k +-- => ((*) applied to dl', for k', and combined with (**)) +-- _manhattanDistance lena lenb dl' >= lena - lenb - k -- => (_wfDistanceToGoal is the minimum of all node's manhattan distances) -- _wfDistanceToGoal lena lenb xs >= lena - lenb - k -- @@ -244,7 +244,7 @@ _wfDistanceLowerBoundK lena lenb k (_:dls) = _wfDistanceLowerBoundK lena lenb (k _wfDistanceLowerBound :: lena : Nat -> lenb : Nat -> {prev : DL | _withinBounds lena lenb prev} -> xs : {v : [{dl : DL | _withinBounds lena lenb dl}] | _wfDiags (_kdiag prev - 2) v} - -> { poj prev >= lenb => _wfDistanceToGoal lena lenb xs > _manhattanDistance lena lenb (poi prev) (poj prev)} + -> { poj prev >= lenb => _wfDistanceToGoal lena lenb xs > _manhattanDistance lena lenb prev} @-} _wfDistanceLowerBound :: Int -> Int -> DL -> [DL] -> () _wfDistanceLowerBound lena lenb prev [] = () @@ -385,7 +385,7 @@ dstep lena lenb cd _ (dl:dls) = _kdiag (head v) == _kdiag prev - 1) && (poj prev < lenb => _wfDistanceToGoal lena lenb v - < _manhattanDistance lena lenb (poi prev) (poj prev) + < _manhattanDistance lena lenb prev && _wfDistanceToGoal lena lenb v < _wfDistanceToGoal lena lenb nodes)} / [len nodes] @-} @@ -433,7 +433,7 @@ addsnake :: lena : Nat && _withinBounds lena lenb v && poi v >= poi dl && poj v >= poj dl} - / [_manhattanDistance lena lenb (poi dl) (poj dl)] + / [_manhattanDistance lena lenb dl] @-} addsnake :: Int -- ^ First input's length phantom parameter for termination check. -> Int -- ^ Second input's length phantom parameter for termination check. From 34f28ed43305351ddbfca1271e54341114771ae2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20G=C3=B3ngora?= Date: Wed, 5 Aug 2026 16:35:38 -0600 Subject: [PATCH 15/19] Refine `_wfDistanceLowerBoundK` lemma documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Facundo Domínguez --- src/Data/Algorithm/Diff.hs | 47 ++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/src/Data/Algorithm/Diff.hs b/src/Data/Algorithm/Diff.hs index a42ac55..3a14f46 100644 --- a/src/Data/Algorithm/Diff.hs +++ b/src/Data/Algorithm/Diff.hs @@ -199,30 +199,37 @@ _wfDistanceToGoal lena lenb (dl:dls) = then _manhattanDistance lena lenb dl else _wfDistanceToGoal lena lenb dls --- | A wave front distance lower bound from its diagonal structure: --- every node of a k-diagonal anchored wave front lies on a diagonal @k' <= k@ --- and within bounds, with diagonals ranging from @-lenb@ to @lena@, --- hence the wave front distance to the goal is at least @lena - lenb - k@. +-- | A lemma that expresses a lower bound of the wavefront distance in terms +-- of the diagonal of the first node: @lena - lenb - k@ +-- +-- We assume all the nodes to be within the grid. +-- +-- @lena - lenb@ is the diagonal of the goal. Informally, the +-- shortest way from a node must necessarily visit all the intermediate +-- diagonals. The minimum amount of diagonals to visit is given by the +-- difference between the diagonal indices of the goal and the first element. -- -- We can prove it manually like so: -- +-- For every node @dl = DL i j p@ we can prove +-- @H(dl) = _manhattanDistance lena lenb dl >= lena - lenb - (i - j)@ +-- -- @ --- (definition of k-diagonal) --- k = poi - poj --- => (algebraic manipulation) --- poi = k + poj --- => (algebraic manipulation) --- lena - poi + lenb - poj = lena - (k + poj) + lenb - poj = lena - lenb - k + 2 * (lenb - poj) --- => (within bounds: poj <= lenb) --- lena - poi + lenb - poj >= lena - lenb - k --- => (definition of manhattan distance) --- (*) _manhattanDistance lena lenb dl >= lena - lenb - k --- => (other node's lie in lower diagonals: k' <= k) --- (**) lena - lenb - k' >= lena - lenb - k --- => ((*) applied to dl', for k', and combined with (**)) --- _manhattanDistance lena lenb dl' >= lena - lenb - k --- => (_wfDistanceToGoal is the minimum of all node's manhattan distances) --- _wfDistanceToGoal lena lenb xs >= lena - lenb - k +-- _manhattanDistance lena lenb dl +-- = +-- lena - (poi dl) + lenb - (poj dl) +-- = +-- lena - i + lenb - j +-- = +-- lena - lenb - (i - j) + 2 * (lenb - j) +-- >= +-- lena - lenb - (i - j) +-- @ +-- +-- Since @H(dl)@ holds for every node in the wave front, it follows +-- that the wave front distance is at least as large as the smallest of +-- these bounds, which is @lena - lenb - k@ for the largest @k = i0 - j0@, +-- which is the diagonal of the first node. -- -- QED -- @ From bad07b2b51976959f8100096396ee2129c314aae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Facundo=20Dom=C3=ADnguez?= Date: Thu, 9 Jul 2026 12:29:53 +0000 Subject: [PATCH 16/19] Add a benchmark with a larger size difference between the inputs The introduced benchmark makes the improvement of the `dstep` refactoring (dropping out-of-bounds nodes) more noticeable. --- bench/bench.hs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/bench/bench.hs b/bench/bench.hs index a24391e..55f1d96 100644 --- a/bench/bench.hs +++ b/bench/bench.hs @@ -24,7 +24,10 @@ doBenchMarks seed = let rbools = randoms (mkStdGen seed) :: [Bool] (s1000_1, rbools1) = splitAt 1000 rbools (s1000_2, rbools2) = splitAt 1000 rbools1 - in s1000_1 `deepseq` s1000_2 `deepseq` defaultMain [ - bgroup "diff bool lists" $ [bench "1000 bools" $ nf (getDiff s1000_1) s1000_2] - ] - + s500_2 = take 500 s1000_2 + in s1000_1 `deepseq` s1000_2 `deepseq` defaultMain + [ bgroup "diff bool lists" + [ bench "1000 bools" $ nf (getDiff s1000_1) s1000_2 + , bench "1000/500 bools" $ nf (getDiff s1000_1) s500_2 + ] + ] From cf592e046b42a4c9c29b0ceb3e33325964de3b46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20G=C3=B3ngora?= Date: Thu, 6 Aug 2026 15:57:21 -0600 Subject: [PATCH 17/19] Drop `withProof` in favor of `const` for lemmas A definition less, plus upstream has an optimization for `const` in https://github.com/ucsd-progsys/liquidhaskell/pull/2732 that could turn out to be useful (currently is makes no difference because lemmas are not being composed). --- src/Data/Algorithm/Diff.hs | 9 ++++----- src/Data/Algorithm/Diff/Refinement.hs | 7 ------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/Data/Algorithm/Diff.hs b/src/Data/Algorithm/Diff.hs index 3a14f46..32f9264 100644 --- a/src/Data/Algorithm/Diff.hs +++ b/src/Data/Algorithm/Diff.hs @@ -77,8 +77,7 @@ import Prelude hiding (pi) import Data.Array (listArray, (!)) import Data.Algorithm.Diff.Type import Data.Algorithm.Diff.Refinement (fst3, snd3, thd3, headIsFirst, - headIsSecond, headIsBoth, noStuttering, - withProof) + headIsSecond, headIsBoth, noStuttering) import Data.Foldable (find) -- | /Diff Instruction/ — an internal enum recording the direction of a single @@ -377,7 +376,7 @@ dstep lena lenb cd _ (dl:dls) = -- If @dl@ lies on the bottom boundary, @stepAndMerge dl dls@ discards -- all of @dls@; the lemma shows the discarded nodes are farther from -- the goal than @dl@'s horizontal child. - `withProof` _wfDistanceLowerBound lena lenb dl dls + `const` _wfDistanceLowerBound lena lenb dl dls where -- Merge vertical step of previous node with horizontal step of next node, -- selecting the furthest-reaching candidate for each shared k-diagonal, @@ -420,7 +419,7 @@ dstep lena lenb cd _ (dl:dls) = -- If @next@ lies on the bottom boundary, the recursive call -- discards all of @rest@; the lemma shows the discarded nodes -- are farther from the goal than the merged child. - `withProof` _wfDistanceLowerBound lena lenb next rest + `const` _wfDistanceLowerBound lena lenb next rest -- | Follow a /snake/ from the current position of a 'DL' node. -- @@ -490,7 +489,7 @@ ses eq as bs = search 0 [addsnake lena lenb cd (DL 0 0 [])] Just p -> path p Nothing -> let wf' = dstep lena lenb cd d wf in search (d + 1) - (wf' `withProof` _minDistanceNonNegative lena lenb wf') + (wf' `const` _minDistanceNonNegative lena lenb wf') -- The abstract refinement @q@ lets 'find' carry the wave -- front element refinement (notably @len (path dl) == d@) -- over to the returned endpoint. diff --git a/src/Data/Algorithm/Diff/Refinement.hs b/src/Data/Algorithm/Diff/Refinement.hs index f7132e8..8a9f1f8 100644 --- a/src/Data/Algorithm/Diff/Refinement.hs +++ b/src/Data/Algorithm/Diff/Refinement.hs @@ -91,10 +91,3 @@ snd3 :: (a, b, c) -> b snd3 (_, y, _) = y thd3 :: (a, b, c) -> c thd3 (_, _, z) = z - --- | Attach a proof term (typically a lemma application) to a value. --- The lemma's postcontition enters the verification context at the --- application site while the value is returned unchanged. -{-@ withProof :: x:a -> b -> {v:a | v = x} @-} -withProof :: a -> b -> a -withProof x _ = x From ba57dffa967199a652ec02d84e93e9a58708810f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20G=C3=B3ngora?= Date: Thu, 6 Aug 2026 17:33:54 -0600 Subject: [PATCH 18/19] Improve documentation related to `_wfDiags` invariant --- src/Data/Algorithm/Diff.hs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/Data/Algorithm/Diff.hs b/src/Data/Algorithm/Diff.hs index 32f9264..c93a9d6 100644 --- a/src/Data/Algorithm/Diff.hs +++ b/src/Data/Algorithm/Diff.hs @@ -151,6 +151,11 @@ _wfDiags k (dl:dls) = poi dl - poj dl == k && _wfDiags (k - 2) dls -- A wave front is a list of 'DL' nodes, all at the same edit distance @D@, -- with k-diagonals @D@, @D−2@, …, @-D+2@, @-D@. +-- Wave fronts establish a connection with the Myers algorithm: +-- @D@ corresponds to the algorithm's current iteration step and the resulting +-- 'ses' length. In Myers the diagonal arrangement is used to optimize space; +-- within 'dstep' it allows us to ensure 'furthestReaching' always compares +-- nodes on the same diagonals. {-@ type WaveFront M N D = {xs : [DLN M N D] | _wfDiags (_kdiag (head xs)) xs} @-} -- | Select the furthest-reaching candidate of two 'DL' nodes competing for the @@ -369,6 +374,8 @@ dstep -- @lena@, @lenb@ and @_d@ are named in the first equation as a workaround -- to https://github.com/ucsd-progsys/liquidhaskell/issues/2704 dstep lena lenb _ _d [] = error "dstep: Cannot perform expansion on an empty list of nodes" +-- This definition branches according to whether a node is on a boundary +-- to avoid constructing out-of-bound nodes and discarding other non-competing nodes. dstep lena lenb cd _ (dl:dls) = if poi dl >= lena then stepAndMerge dl dls else @@ -404,15 +411,11 @@ dstep lena lenb cd _ (dl:dls) = else case nodes of [] -> [addsnake lena lenb cd $ vStep prev] (next:rest) -> - -- HACK: This check saves us from an unneeded call to furthestReaching, - -- as the horizontal child of the next node would be out-of-bounds, - -- but in fact we could drop this child node altogether because - -- the next node being on the right border means all previous nodes - -- would need to cross the next node's diagonal in more steps, - -- and thus cannot compete to the endpoint. - -- However, this would result in a negligible performance gain - -- and the loss of the wave front diagonal invariant, - -- so we keep it for now. + -- The next node being on the right border means all + -- previous nodes cannot compete to the endpoint, because their + -- children require more steps to cross the next node's diagonal. + -- HACK: However, we keep @prev@'s vertical child node to preserve + -- the '_wfDiags' invariant at a negligible performance penalty. if poi next >= lena then addsnake lena lenb cd (vStep prev) : stepAndMerge next rest else (addsnake lena lenb cd (furthestReaching (vStep prev) (hStep next)) : stepAndMerge next rest) From e7d243428d1d42d12fa62fa20ffad1f9dd58bcc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20G=C3=B3ngora?= Date: Thu, 6 Aug 2026 16:47:48 -0600 Subject: [PATCH 19/19] Prove `findEndpoint` (WIP) --- src/Data/Algorithm/Diff.hs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Data/Algorithm/Diff.hs b/src/Data/Algorithm/Diff.hs index c93a9d6..f534e22 100644 --- a/src/Data/Algorithm/Diff.hs +++ b/src/Data/Algorithm/Diff.hs @@ -134,6 +134,9 @@ data DL = DL -- having a fixed /D-length/. {-@ type DLN M N D = { x : DL | len (path x) = D && _withinBounds M N x} @-} +-- The endpoint node: a 'DLN' that has reached the goal position. +{-@ type Endpoint M N D = { x : DLN M N D | endPoint M N x } @-} + {-@ inline _kdiag @-} -- | Computes the k-diagonal of a node. -- Used in LiquidHaskell logic as an expression. @@ -287,7 +290,7 @@ _reducesDistanceToGoal lena lenb wf1 wf2 = _wfDistanceToGoal lena lenb wf2 < _wf _withinBounds :: Int -> Int -> DL -> Bool _withinBounds lena lenb dl = poi dl <= lena && poj dl <= lenb -{-@ inline endPoint @-} +{-@ reflect endPoint @-} endPoint :: Int -> Int -> DL -> Bool endPoint lena lenb dl = poi dl == lena && poj dl == lenb @@ -488,7 +491,7 @@ ses eq as bs = search 0 [addsnake lena lenb cd (DL 0 0 [])] / [_wfDistanceToGoal lena lenb dls] @-} search :: Int -> [DL] -> [DI] search _ [] = error "ses: The search must have a seed node" - search d wf = case findEndpoint lena lenb wf of + search d wf = case findEndpoint lena lenb d wf of Just p -> path p Nothing -> let wf' = dstep lena lenb cd d wf in search (d + 1) @@ -496,12 +499,12 @@ ses eq as bs = search 0 [addsnake lena lenb cd (DL 0 0 [])] -- The abstract refinement @q@ lets 'find' carry the wave -- front element refinement (notably @len (path dl) == d@) -- over to the returned endpoint. - {-@ assume findEndpoint :: forall Bool>. - i : Nat -> j : Nat -> xs : [DL] - -> { m : Maybe {dl : DL | endPoint i j dl} - | m == Nothing => _wfDistanceToGoal i j xs > 0} @-} - findEndpoint :: Int -> Int -> [DL] -> Maybe DL - findEndpoint i j = find (endPoint i j) + {-@ assume findEndpoint :: i : Nat -> j : Nat -> d : Nat + -> xs : [DLN i j d] + -> { m : Maybe (Endpoint i j d) + | m == Nothing => _wfDistanceToGoal i j xs > 0} @-} + findEndpoint :: Int -> Int -> Int -> [DL] -> Maybe DL + findEndpoint i j _d wf = find (endPoint i j) wf -- | Takes two lists and returns a list of differences between them. This is -- 'getDiffBy' with '==' used as predicate.