Proofs Using C++ Compilation

About a month ago, I realized I could use the Curry-Howard Correspondence to formulate a simple proof assistant using C++’s type system. Of course, the C++ type system is pretty rudimentary, so proving things using it is an uphill battle, but I was able to formulate propositional logic. One major shortcoming is that C++ can’t reason about inductive types, so we have to add axioms to properly reason about logical connectives.

Here is logic.h:

#include <functional>

// Ignore return types so we can form axioms
#pragma GCC diagnostic ignored "-Wreturn-type"

// False is a proposition with no proof,
// so it has no constructors
class False { False () = delete; };

// Modus ponens is just function application
template<typename P, typename Q>
using Implies = std::function<Q(P)>;

// explosion: False -> P
template<typename P>
P explosion (False f) {}

template<typename P>
using Not = Implies<P, False>;

// left: P /\ Q -> P
// right: P /\ Q -> Q
// conj: P -> Q -> P /\ Q
template<typename P_, typename Q_>
class And {
  P_ p;
  Q_ q;
  And<P_, Q_> (P_ p, Q_ q) : p(p), q(q) {}
public:
  template<typename P, typename Q>
  friend P left (And<P, Q> a);
  template<typename P, typename Q>
  friend Q right (And<P, Q> a);
  template<typename P, typename Q>
  friend Implies<Q, And<P, Q>> conj (P p);
};

template<typename P, typename Q>
P left (And<P, Q> a) { return a.p; }
template<typename P, typename Q>
Q right (And<P, Q> a) { return a.q; }
template<typename P, typename Q>
Implies<Q, And<P, Q>> conj (P p) {
    return [p] (Q q) {
        return And<P, Q>(p, q);
    };
}

// inl: P -> P \/ Q
// inr: Q -> P \/ Q
// confluence: P \/ Q -> (P -> R) -> (Q -> R) -> R
template<typename P_, typename Q_>
class Or {
  Or<P_, Q_> () {};
public:
  template<typename P, typename Q>
  friend Or<P, Q> inl (P p);
  template<typename P, typename Q>
  friend Or<P, Q> inr (Q q);
};
template<typename P, typename Q>
Or<P, Q> inl (P p) {}
template<typename P, typename Q>
Or<P, Q> inr (Q q) {}

template<typename P, typename Q, typename R>
Implies<Implies<P, R>,
Implies<Implies<Q, R>,
        R>> confluence (Or<P, Q> pq) {}

#pragma GCC diagnostic error "-Wreturn-type"

And here is a demonstration of two proofs:

#include "logic.h"

class A { A () = delete; };
class B { B () = delete; };
class C { C () = delete; };

template<typename P, typename Q>
auto constrapositive =
  [] (Implies<P, Q> pq) {
    return [pq] (Not<Q> nq) {
      return [pq, nq] (P p) {
        return nq(pq(p));
      };
    };
  };

Implies<Implies<A, B>, Implies<Not<B>, Not<A>>>
contrapositive_check = constrapositive<A, B>;

template<typename P, typename Q, typename R>
auto or_assoc =
  [] (Or<P, Or<Q, R>> pqr) {
    return confluence<P, Or<Q, R>, Or<Or<P, Q>, R>>
      (pqr)
      ([] (P p) { return inl<Or<P, Q>, R>(inl<P, Q>(p)); })
      ([] (Or<Q, R> qr) {
        return confluence<Q, R, Or<Or<P, Q>, R>>
          (qr)
          ([] (Q q) { return inl<Or<P, Q>, R>(inr<P, Q>(q)); })
          ([] (R r) { return inr<Or<P, Q>, R>(r); });
      });
  };

// This is actually only one direction of Or associativity
Implies<Or<A, Or<B, C>>, Or<Or<A, B>, C>>
or_assoc_check = or_assoc<A, B, C>;

int main () {}

The proofs are checked at compile time by way of type checking.

Some things to notice: First, template definitions are not fully type-checked, so we have to use instantiations like or_assoc_check to force type-checking using “concrete placeholders.” Second, C++’s type inference is not strong enough to deduce all intermediate types, so we often have to specify them ourselves (e.g. writing inr<P, Q>(p) instead of inr(p)).

I’m not sure if first order logic can be formulated with C++ in this way. Note that the templating system is Turing-complete, so it is possible to write a full proof assistant in it, but I’m only curious about reusing g++’s type checking system to also check proofs.


15 December 2022