Shortest Path

The cheapest way from A to D, and the honest fine print behind “cheapest”.

shortest-path.py · output · proof · check · try it in the playground


The question

A small road map: four places, five one-way roads, each with a cost (think minutes, or kilometres).

a --4--> b --3--> d
a --2--> c --1--> b
         c --8--> d

Finding a route is easy. Saying a route is the cheapest means saying that no cheaper route exists: a claim about every route you didn’t pick.

What is the cheapest way to get from a to d, and how can we trust that nothing cheaper exists?


What we tell eyekind: the map and the routes

# edge(From, To, Cost)
fact(edge('a', 'b', 4))
fact(edge('a', 'c', 2))
fact(edge('c', 'b', 1))
fact(edge('b', 'd', 3))
fact(edge('c', 'd', 8))
implies(edge(X, Y, Cost), path(X, Y, Cost))
implies(
  [path(X, Y, Before), edge(Y, Z, Weight), is_(Cost, add(Before, Weight))],
  path(X, Z, Cost),
)

In words: a road is a path; a path followed by one more road is a longer path, and the costs add up. The map has no loops, so this ends with every route and its total cost. Listing all routes before choosing suits a small map; it is not Dijkstra’s algorithm.


What we tell eyekind: “cheapest”

# cheaper(X, Y, Cost): some route from X to Y costs less than Cost.
implied_by(cheaper(X, Y, Cost), [path(X, Y, Other), lt(Other, Cost)])
implies([path(X, Y, Cost), not_(cheaper(X, Y, Cost))], shortest(X, Y, Cost))
query(shortest('a', 'd', Cost))

eyekind first finishes listing all paths, and only then looks for “no cheaper one”. It cuts the program into layers, called strata, so a negation waits for everything it denies; the search for something cheaper is therefore over a complete list.


What eyekind concludes

// Answers: each instance of a query() that holds.
shortest('a', 'd', 6)

The first line is a comment saying what follows: the answers to the query. There are three ways from a to d, costing 7 (via b), 10 (via c) and 6 (via c, then b). The cheapest costs 6.


The proof, line by line

eyekind --proof writes down how it reached the answer. This is the complete proof:

// Answers: each instance of a query() that holds.
shortest('a', 'd', 6)

// Clauses: the program clauses the steps cite, by clause number.
clause(2, fact(edge('a', 'c', 2)))
clause(3, fact(edge('c', 'b', 1)))
clause(4, fact(edge('b', 'd', 3)))
clause(6, implies(edge(X, Y, Cost), path(X, Y, Cost)))
clause(7, implies([path(X, Y, Before), edge(Y, Z, Weight), is(Cost, add(Before, Weight))], path(X, Z, Cost)))
clause(9, implies([path(X, Y, Cost), not(cheaper(X, Y, Cost))], shortest(X, Y, Cost)))

// Steps: each goal, what justifies it (a clause, a builtin, a control, or an
// absence or collection taken on trust), the bindings and the goals it uses.
step(shortest('a', 'd', 6), clause(9), {X: 'a', Y: 'd', Cost: 6}, [path('a', 'd', 6), not(cheaper('a', 'd', 6))])
step(path('a', 'd', 6), clause(7), {X: 'a', Y: 'b', Before: 3, Z: 'd', Weight: 3, Cost: 6}, [path('a', 'b', 3), edge('b', 'd', 3), is(6, add(3, 3))])
step(path('a', 'b', 3), clause(7), {X: 'a', Y: 'c', Before: 2, Z: 'b', Weight: 1, Cost: 3}, [path('a', 'c', 2), edge('c', 'b', 1), is(3, add(2, 1))])
step(path('a', 'c', 2), clause(6), {X: 'a', Y: 'c', Cost: 2}, [edge('a', 'c', 2)])
step(edge('a', 'c', 2), clause(2), {}, [])
step(edge('c', 'b', 1), clause(3), {}, [])
step(is(3, add(2, 1)), 'builtin', {}, [])
step(edge('b', 'd', 3), clause(4), {}, [])
step(is(6, add(3, 3)), 'builtin', {}, [])
step(not(cheaper('a', 'd', 6)), 'absent', {}, [])

It has three parts:

Read the steps as sentences:

  1. The shortest route from a to d costs 6 — by rule 9, because there is a path from a to d costing 6, and nothing cheaper was found.
  2. There is a path from a to d costing 6 — by rule 7, with Y = b, because there is a path from a to b costing 3, the road b → d costs 3, and 6 is 3 + 3.
  3. There is a path from a to b costing 3 — by rule 7, with Y = c, because there is a path from a to c costing 2, the road c → b costs 1, and 3 is 2 + 1.
  4. There is a path from a to c costing 2 — by rule 6, because the road a → c costs 2.
  5. The road a → c costs 2 — fact 2.
  6. The road c → b costs 1 — fact 3.
  7. 3 is 2 + 1 — 'builtin': a calculation, which anyone checking the proof simply redoes.
  8. The road b → d costs 3 — fact 4.
  9. 6 is 3 + 3 — 'builtin'.
  10. No path from a to d is cheaper than 6 — 'absent': eyekind searched everything it had concluded and found none. This is not derived from a rule; it is taken on trust, and the check report lists it as an obligation.

Steps 2 to 9 show the route in full. Step 10 is different: it is a claim that something does not exist, and the other two routes, costing 7 and 10, do not appear in the proof at all.


Checked: what the report says

A proof is only worth something if someone else can check it. eyekind -c generates the proof and checks it against the program; --check-proof checks a saved proof. The checker never reasons by itself: it only confirms each step against the program. This is the complete report:

// Answers checked subject to the obligations below.
shortest('a', 'd', 6)

// C1-C8: resolution, acyclicity, justification, coverage,
// re-decision, boundary consistency, relevance and witness integrity.
condition('C1', 'resolution', 'ok', 7)
condition('C2', 'well_founded', 'ok', 10)
condition('C3', 'justification', 'ok', 10)
condition('C4', 'coverage', 'ok', 10)
condition('C5', 're_decision', 'ok', 2)
condition('C6', 'boundary_consistency', 'ok', 0)
condition('C7', 'relevance', 'ok', 11)
condition('C8', 'witness_integrity', 'ok', 0)

// Obligations: absence and collection completeness are taken on trust.
obligation('absent', 'theory_scoped', not(cheaper('a', 'd', 6)))

// Proof statistics: recorded steps and how they were checked.
steps(10)
verified(7)
recomputed(2)
composed(0)
trusted(1)
claims(1)

// This verdict applies to the claims above; it does not establish completeness.
verdict('checked_with_obligations')

Each condition line names a check, says 'ok', and counts what it looked at:

The one obligation line says, in plain words: no route from a to d costs less than 6. 'theory_scoped' means this holds relative to this program: true of this map, not necessarily of the real roads.

The statistics repeat this: 10 steps, 7 verified against the program, 2 recomputed, 1 taken on trust. The verdict is checked with obligations: the route of cost 6 certainly exists, and it is the cheapest provided the absence holds. A proof can show a path exists; it cannot show that none cheaper does. The verdict also does not establish that there are no other answers.


Strict mode

If you need a proof with nothing taken on trust, ask for it:

eyekind --proof examples/shortest-path.py > /tmp/sp-proof.py
eyekind --strict-proof --check-proof /tmp/sp-proof.py examples/shortest-path.py

The strict check refuses this proof, with verdict failed(1) and exit code 2, because it leans on the trusted absence: the report adds failure('C5', not(cheaper('a', 'd', 6)), 'trusted boundary forbidden: absent'). Nothing is wrong with the answer; strict mode just does not accept “searched and found nothing” as evidence.


Try it

eyekind examples/shortest-path.py            # the answer
eyekind --proof examples/shortest-path.py    # with its proof

Or open it in the playground. Add a direct road fact(edge('a', 'd', 5)) and run again: the answer becomes shortest('a', 'd', 5). Or make the road c → b cost 5 instead of 1: the route via c and b now costs 10, and the cheapest becomes the one via b alone, shortest('a', 'd', 7).


Takeaway

“Best” always hides a “nothing better exists”. eyekind shows the route it found, and tells you plainly which part of the answer is a search that came up empty.