Skip to content

Prove termination of diff algorithm implementation - #37

Draft
ninioArtillero wants to merge 9 commits into
seereason:masterfrom
tweag:xg/ses-termination
Draft

Prove termination of diff algorithm implementation#37
ninioArtillero wants to merge 9 commits into
seereason:masterfrom
tweag:xg/ses-termination

Conversation

@ninioArtillero

Copy link
Copy Markdown
Collaborator

All commits build independenlty and can be read in sequence.

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.
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.
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%
Comment on lines +272 to +281
-- 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

@facundominguez facundominguez Aug 1, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding vStep prev in the middle of the wavefront is rather arbitrary.

I'm wondering if we could reformulate the wavefront invariant from

prev and next are two diagonals apart

to

prev and next are two diagonals apart if poi next < lena

This should suffice to ensure that furthestReaching is always called with nodes in the same diagonal, and it is the reason that motivates the wavefront invariant.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and it is the reason that motivates the wavefront invariant.

Maybe this should be said at the place where the Wavefront type alias is defined.

Comment thread src/Data/Algorithm/Diff.hs Outdated
Comment on lines +199 to +200
-- calls to other lifted functions inside a reflected body prevent PLE
-- from unfolding this function's defining equations.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think LH is designed to work like that. Maybe there is a bug when mixing inlined and reflected functions?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One workaround to try is to make all functions reflected.

Comment on lines +226 to +229
-- 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is still unclear how this relates to the previous proof.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In any case, it may be worth reviewing the need for the lemma before polishing this.

Comment on lines +233 to +234
-> {_wfDistanceToGoal lena lenb xs == lena + lenb + 1
|| _wfDistanceToGoal lena lenb xs >= lena - lenb - k}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This disjunction is not exactly what has been discussed before.

Comment on lines +95 to +100
-- | 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not, but so far the lemmas seem to be necessary for the proof to proceed, and in stepAndMerge passing it in a let results in three leves of nested expressions, which felt a little awkward. I thought about importing the homonymous function from ProofCombinators (or even ?), but preffered defining it here rather than adding another dependency (which would need to be added to both Diff and `Diff-liquidhaskell) just for a single combinator.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

withProof seems to be duplicating ? from liquid-prelude, while ? produces less work for LH, and should allow reflecting functions that use lemmas.

Comment thread src/Data/Algorithm/Diff.hs Outdated
Comment thread src/Data/Algorithm/Diff.hs Outdated
Comment thread src/Data/Algorithm/Diff.hs Outdated
&& (poj prev < lenb <=> len v > 0)
&& (len v > 0 =>
_kdiag (head v) == _kdiag prev - 1)
&& (poj prev < lenb =>

@facundominguez facundominguez Aug 1, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is unclear why this condition is necessary. It might depend on what the _wfDistanceToGoal is defined to be for the empty list.

-- 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

@facundominguez facundominguez Aug 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This lemma looks unnecessary. This is my reasoning skipping the prevalent lena and lenb parameters:

By preconditions we know that distanceToGoal (next:rest) > 0.
Then, by definition of distanceToGoal we know that distanceToGoal rest > 0, which is the precondition of the recursive call stepAndMerge next rest.

Next we can prove that stepAndMerge decreases distanceToGoal as follows:

    distanceToGoal (stepAndMerge prev (next:rest)) < distanceToGoal (next:rest)

==  (by definition of stepAndMerge and distanceToGoal and IF guard "poi next < lena")

    ( manhattanDistance
        (addsnake cd (furthestReaching (vStep prev) (hStep next))
      `min`
      distanceToGoal (stepAndMerge next rest)
    )
    <
    (manhattanDistance next `min` distanceToGoal rest)

<==  (by definition of min)

    H1 && HI

where
HI: distanceToGoal (stepAndMerge next rest) < distanceToGoal rest
H1: manhattanDistance
        (addsnake cd (furthestReaching (vStep prev) (hStep next))
    < manhattanDistance next

HI holds because it is the postcondition of the recursive call.

Now we prove H1

    manhattanDistance
        (addsnake cd (furthestReaching (vStep prev) (hStep next))

<=  (by postcondition of addsnake)

    manhattanDistance (furthestReaching (vStep prev) (hStep next))

==  (by definition of furthestReaching)

    min (manhattanDistance (vStep prev)) (manhattanDistance (hStep next))

<=  (by definition of min)

    manhattanDistance (hStep next)

<  (by definition of hStep and IF guard "poi next < lena")

    manhattanDistance next

QED

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't taking HI (postcondition) as given assuming what we want to prove?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. It is the inductive hypothesis. We assume the property holds for rest, and we want to prove it for next:rest.

@facundominguez

Copy link
Copy Markdown
Contributor

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


{-@ reflect _manhattanDistance @-}
{-@ _manhattanDistance :: lena : Nat -> lenb : Nat -> {i : Nat | lena >= i} -> { j : Nat | lenb >= j} -> Nat @-}
_manhattanDistance :: Int -> Int -> Int -> Int -> Int

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this could be changed or augmented with another function with type:

_manhattanDistanceDL :: Int -> Int -> DL -> Int

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