Equational reasoning meets Induction (part 1)
I’ve been studying Formal Methods again since the beggining of this year, and now I’m looking at algebraic specifications and I found they pretty elegant. To be able to pratice with it I vibecoded an algebraic specification plugin for Claude, and named it algae.
Since I more interested into understanding the proofs I’m focusing on a syntax that make everything explicit, instead of focusing on soundness and the checker now.
I came across Algebraic Specifications while going through this book:

At chapter 13 the author show the OBJ3 language. So I decided to write one of my own so I can experiment, or even better, let Claude write on of my own. As I said I’m more interested into learning the equational reasoning than having a working solution, so my focus was more into making it very explicit than very pratical for use.
I spend some weeks play with proofs until I arrived to this
# Stack with a partial assert that narrows pop's fallible result, plus
# lemmas with proof sketches. Lemmas and proofs are parsed but not checked.
sort Stack, Elem;
sort Error = {empty_error};
op empty : → Stack;
op push : Stack × Elem → Stack;
op pop : Stack → Stack × Elem | Error;
# A partial operation (⇸, ASCII -/->) narrows a sum to one of its branches.
op assert : Stack × Elem | Error ⇸ Stack × Elem;
op fst : (Stack × Elem) → Stack;
op snd : (Stack × Elem) → Elem;
var s : Stack;
var e : Elem;
axiom push_pop
s.push(e).pop = (s, e);
axiom empty_pop
empty().pop = empty_error;
axiom assert_elim
(s, e).assert = (s, e);
axiom fst_pair
(s, e).fst = s;
axiom snd_pair
(s, e).snd = e;
lemma pop_top
s.push(e).pop.assert.snd = e;
proof
s.push(e).pop.assert.snd;
= (s, e).assert.snd by push_pop;
= (s, e).snd by assert_elim;
= e by snd_pair;
qed;
lemma pop_stack
s.push(e).pop.assert.fst = s;
proof
s.push(e).pop.assert.fst;
= (s, e).assert.fst by push_pop;
= (s, e).fst by assert_elim;
= s by fst_pair;
qed;
At this point I was able to prove little things about stacks. I also had a parsing and typechecking working. There was no checker but all the steps are sound. The proofs go by equational reasoning, for example
This is what I want to prove: s.push(e).pop.assert.fst = s,
- by
axiom push_pop s.push(e).pop = (s, e);I can replaces.push(e).pop→(s, e)getting(s, e).assert.fst - Now you may wonder, why assert is necessary? The problem is that
fstacceptsStack × Elemwhile pop returnsStack × Elem | Error, these are not the same type. Theassertspecify the runtime error checking behavior, this can be.is_okin Rust oris not Nonein Python or type narrowing in typescript. The only axiom we have to eliminateassertisaxiom assert_elim (s, e).assert = (s, e), i.e. we can only allow us to eliminate.assertcall if its argument is an pair. - After that we have
(s, e).fstand thefst_pairaxiom allow us to eliminate the pair to thes. - Since we’re trying to prove
... = sand we get toswe finished our proof.
We can gatter two lessons from this.
-
First, how equational reasoning works: You want to prove something like
A = D, and you have a bunch of equationsA = B,B = C,C = D, you use the equations to replace portions of the goal, step by step, usually getting a simpler goal at each step, until you get toD = Dwhen you can finish the proof (by reflection). -
How you can use equational reasoning to emulate computational reasoning. When we do
const fst = ([a, b]) => a;in javascript and laterfst([1, 2])we’re computing1from[1,2]. The equationaxiom fst_pair (a, b) = aemulate the same behavior. The difference between “computational” and “equational” and more profoundly the difference between computation and logic is that logic is birectional, you can rewrite in both directions, it is a equantion, not a computer procedure, you cannot put a value into the return address and unwind a function to get its inputs, computers do not work that way, logic does! The big advantage and power of computers is that you can put the inputs in place and hit COMPUTE and let it do the hard work for you. The big drawback is that once you have ONE undefined term, it cannot compute anymore. You cannot say “computef(x)” without definingfandx. If you look at our Stack spec,StackandElemare completely abstract. They are not defined at all, this allow us to keep things abstract, to implement this in a computer we need define Stack, probably with an array, which is defined by the underlying language, which is later interpreted or compiled to another language, until finally it is in a form which which your x86_64, or ARM CPU can understand, you can’t skip a step, every bit, every data type, every function, class, everything has to be defined up to the baremetal level, but once it is one, you have that beatiful COMPUTE red buttom that allow you to be very fast. Okay back to the proofs.
After that I want to prove things about queues. Stacks are simple because they
only reason about the last elements, so they do not much state. I mean given an
arbitrary stack s you can relate push and pop with s.push(e).pop = (s,
e) what about queues, how to relate enqueue and dequeue (put and get
below)? put talks about the tail of the queue, get talks about the head of
the queue. Here is the spec:
sort Queue, Elem;
op new : → Queue;
op put : Queue × Elem → Queue;
# Returns (remaining_queue, returned_element)
op get : Queue × Elem → Queue × Elem;
var q : Queue;
var e, f, default : Elem;
# Empty queue
axiom new_get new().get(default)
=
(new(), default);
axiom new_put_get new().put(e).get(default)
=
(new(), e);
axiom fifo_law q.put(e).put(f).get(default)
=
let (rest, x) = q.put(e).get(default)
in (rest.put(f), x);
lemma get_three
new().put(a).put(b).put(c).get(default) =
(new().put(b).put(c), a);
proof
new().put(a).put(b).put(c).get(default);
= let (rest, x) = new().put(a).put(b).get(default) in
(rest.put(c), x) by get_fifo;
= let (rest, x) = (new().put(b), a) in
(rest.put(c), x) by get_two;
= (new().put(b).put(c), a) by let_pair;
qed;
I could prove things about fixed length queues by applying the same axiom again and again and again … but I want to prove things about arbitrary long queues, and it was clear (after GPT told me) that I need induction. I didn’t know how to use induction with equational reasoning so I started to research.
continue …