Inventory Invoice

Adding up a shopping bill, and being honest about what “all the lines” means.

inventory.py · output · proof · check · try it in the playground


The question

You buy three kinds of fruit:

All prices are in the same currency. Each line of the invoice is quantity times price, and the invoice adds up all the lines. Easy for a person; the interesting part is how a computer shows its work, including the step “I added up all the lines”.

What is the total on the invoice?


What we tell eyekind

# item(Name, Quantity, UnitPrice)
fact(item('apple', 3, 2))
fact(item('pear', 4, 3))
fact(item('plum', 2, 5))
implies([item(Name, Quantity, Price), is_(Total, mul(Quantity, Price))], line_total(Name, Total))
# sum(Numbers, Total): Total is the sum of the list Numbers.
fact(sum([], 0))
implied_by(sum([X, *Xs], Total), [sum(Xs, Rest), is_(Total, add(X, Rest))])
implies([findall(Amount, line_total(_, Amount), Amounts), sum(Amounts, Total)], invoice(Total))
query(invoice(Total))

The collection must see every line total, so eyekind runs the findall rule only after all line totals have been derived: it puts that rule in a later stratum, a later stage of reasoning.


What eyekind concludes

// Answers: each instance of a query() that holds.
invoice(28)

The line totals are 3 × 2 = 6, 4 × 3 = 12 and 2 × 5 = 10, and 6 + 12 + 10 = 28.


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.
invoice(28)

// Clauses: the program clauses the steps cite, by clause number.
clause(5, fact(sum([], 0)))
clause(6, implied_by(sum([X, ...Xs], Total), [sum(Xs, Rest), is(Total, add(X, Rest))]))
clause(7, implies([findall(Amount, line_total(_0, Amount), Amounts), sum(Amounts, Total)], invoice(Total)))

// 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(invoice(28), clause(7), {Amount: A, _0: B, Amounts: [6, 12, 10], Total: 28}, [findall(A, line_total(B, A), [6, 12, 10]), sum([6, 12, 10], 28)])
step(findall(A, line_total(B, A), [6, 12, 10]), 'collected', {}, [])
step(sum([6, 12, 10], 28), clause(6), {X: 6, Xs: [12, 10], Total: 28, Rest: 22}, [sum([12, 10], 22), is(28, add(6, 22))])
step(sum([12, 10], 22), clause(6), {X: 12, Xs: [10], Total: 22, Rest: 10}, [sum([10], 10), is(22, add(12, 10))])
step(sum([10], 10), clause(6), {X: 10, Xs: [], Total: 10, Rest: 0}, [sum([], 0), is(10, add(10, 0))])
step(sum([], 0), clause(5), {}, [])
step(is(10, add(10, 0)), 'builtin', {}, [])
step(is(22, add(12, 10)), 'builtin', {}, [])
step(is(28, add(6, 22)), 'builtin', {}, [])

It has three parts:

Read the steps as sentences:

  1. The invoice is 28 — by rule 7, because the collected line totals are [6, 12, 10] and their sum is 28.
  2. The line totals are exactly [6, 12, 10] — collected: eyekind gathered every line total it had derived. This step is taken on trust (see below).
  3. The sum of [6, 12, 10] is 28 — by rule 6, with X = 6 and the rest [12, 10] adding up to 22, because 6 + 22 = 28.
  4. The sum of [12, 10] is 22 — by rule 6, because the sum of [10] is 10 and 12 + 10 = 22.
  5. The sum of [10] is 10 — by rule 6, because the sum of [] is 0 and 10 + 0 = 10.
  6. The sum of [] is 0 — fact 5.
  7. 10 + 0 = 10 — builtin: a calculation, which the checker redoes itself rather than trusting.
  8. 12 + 10 = 22 — builtin.
  9. 28 = 6 + 22 — builtin.

Two kinds of justification appear besides program lines. A 'builtin' step is a calculation; anyone can redo it. A 'collected' step is different: it says a list is complete. Each number in it is a line total, but “there are no others” is something eyekind knows only because it searched everything it had derived. A proof cannot point at a missing line, so this step is taken on trust.


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.
invoice(28)

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

// Obligations: absence and collection completeness are taken on trust.
obligation('collected', 'theory_scoped', findall(A, line_total(B, A), [6, 12, 10]))

// Proof statistics: recorded steps and how they were checked.
steps(9)
verified(5)
recomputed(3)
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 statistics: 9 steps, 5 verified against the program, 3 recomputed, 1 trusted, 1 claim.

The obligation line names what was taken on trust: the list [6, 12, 10] is all the line totals. 'theory_scoped' means “complete with respect to this program”: add another item and the list would change. The verdict is checked with obligations: the invoice of 28 follows from the program, provided that list is complete. The verdict does not say the program has no other conclusions.


Try it

eyekind examples/inventory.py            # the answers
eyekind --proof examples/inventory.py    # answers with their proof
eyekind --goal 'line_total(Name, Total)' examples/inventory.py

The last command asks for the line totals, which the query does not show: line_total('apple', 6), line_total('pear', 12) and line_total('plum', 10).

Or open it in the playground. Add fact(item('fig', 5, 1)) and run again: the invoice becomes invoice(33), because the collection now gathers four line totals.


Takeaway

A total is only right if no line was left out. eyekind does not hide that assumption: the report names it, so you know exactly what you are trusting.