hol

HOL theorem prover in MoonBit

hol
theorem-prover
moonbit
moon add bobzhang/hol@0.1.1
Download zip
Author
Version
0.1.1
License
MIT
Last updated
3 months ago
Downloads
25
README

#A Comprehensive Introduction to HOL

This tutorial is a self-contained introduction to Higher-Order Logic (HOL) as implemented in this MoonBit port of HOL Light. Every code example is a verified mbt check block -- compiled and tested by moon test -- so nothing here is hand-waved.


#1. What is HOL?

Higher-Order Logic is a formal logic powerful enough to express most of mathematics. "Higher-order" means that quantifiers range over functions and predicates, not just individuals. HOL sits at the sweet spot between expressiveness and automation: it is strong enough to formalize real analysis, probability theory, and hardware verification, yet simple enough that its trusted kernel fits in a few hundred lines of code.

#The LCF Architecture

This implementation follows the LCF (Logic for Computable Functions) architecture pioneered by Robin Milner in the 1970s:

  1. There is an abstract type Thm (theorem).
  2. Values of type Thm can only be created by a small set of primitive inference rules in the kernel.
  3. Because MoonBit enforces module boundaries, no outside code can fabricate a Thm -- every theorem is the result of a valid chain of inferences.

This is the fundamental soundness guarantee: if the kernel is correct, every Thm value represents a logically valid statement. Derived rules, tactics, and decision procedures can be arbitrarily complex without compromising soundness, because they must ultimately bottom out in kernel primitives.

#Lineage

This project ports HOL Light, John Harrison's minimalist HOL implementation, to MoonBit. HOL Light traces its lineage through HOL88 back to Mike Gordon's original HOL system.

#Project Structure

The implementation is organized into MoonBit packages, each building on the previous:

PackageKey filesDescription
foundation/lib.mbt, subst.mbtMergesort with deduplication; substitution infrastructure
types/type.mbt, match.mbt, env.mbtHOL types (simply-typed lambda calculus) + type matching
terms/term.mbt, aconv.mbt, match.mbt, subst.mbt, ...HOL terms (locally nameless) + term matching
logic/kernel.mbt, bool_syntax.mbt, equal.mbtLCF kernel, connectives defined from equality, derived rules

We will walk through each package from the bottom up.


#2. Types: The Type Language

HOL is a simply-typed lambda calculus. Every term has a type, and types are built from two constructors:

///|
enum Type {
TyVar(String) // type variable: enables polymorphism
TyApp(String, Array[Type]) // type constructor applied to arguments
}

  • TyVar("'a") is a type variable -- polymorphic constants like equality are parameterized over type variables.
  • TyApp("bool", []) is the Boolean type (propositions).
  • TyApp("fun", [A, B]) is the function type A -> B.
  • TyApp("list", [A]) would be A list (once list is registered).

#Type Registry

The set of valid type constructors and their arities is tracked in a mutable registry (type_table). Initially it contains just two primitives:

  • bool (arity 0) -- the type of propositions
  • ind (arity 0) -- the type of individuals (needed for the axiom of infinity)

The function type constructor fun is built-in (hardcoded arity 2) and does not appear in the registry. mk_type enforces that you use the correct number of arguments:

///|
test "types: creating basic types" {
@hol.Type::reset_table()
@hol.Term::reset_table()
// Primitive types
let bool_ty = @types.bool_ty() // TyApp("bool", [])
let ind_ty = @types.ind_ty() // TyApp("ind", [])
assert_eq(@hol.Type::pprint(bool_ty), "bool")
assert_eq(@hol.Type::pprint(ind_ty), "ind")

// Type variables
let a = @types.mk_var("'a")
let b = @types.mk_var("'b")
assert_eq(@hol.Type::pprint(a), "'a")
assert_true(a.is_var())
assert_false(b.is_type())

// Function types: 'a -> 'b
let fun_ty = @types.mk_fun(a, b)
assert_eq(@hol.Type::pprint(fun_ty), "'a --> 'b")
assert_true(fun_ty.is_fun())
let (dom, rng) = fun_ty.dest_fun()
assert_eq(@hol.Type::pprint(dom), "'a")
assert_eq(@hol.Type::pprint(rng), "'b")
}

#Registering New Type Constructors

You can extend the type universe with new_type. For example, registering a unary list constructor:

///|
test "types: registering list and building list types" {
@hol.Type::reset_table()
@hol.Term::reset_table()
@hol.Type::new_type("list", 1)

let a = @types.mk_var("'a")
let list_a = @hol.Type::mk_type("list", [a])
assert_eq(@hol.Type::pprint(list_a), "'a list")

// Nested: ('a -> bool) list
let pred_list = @hol.Type::mk_type("list", [
@types.mk_fun(a, @types.bool_ty()),
])
assert_eq(@hol.Type::pprint(pred_list), "'a --> bool list")
}

#Type Substitution

Type substitution replaces type variables with concrete types. This is how polymorphic constants get specialized:

///|
test "types: substitution" {
@hol.Type::reset_table()
@hol.Term::reset_table()
@hol.Type::new_type("list", 1)

let a = @types.mk_var("'a")
let b = @types.mk_var("'b")
let list_a = @hol.Type::mk_type("list", [a])

// Substitute 'a := bool
let sigma : @hol.TypeSubst = Subst(pairs=[(a, @types.bool_ty())])
let result = list_a.subst(sigma)
assert_eq(@hol.Type::pprint(result), "bool list")

// Substitute in a function type: ('a -> 'b) becomes (bool -> ind)
let fun_ab = @types.mk_fun(a, b)
let sigma2 : @hol.TypeSubst = Subst(pairs=[
(a, @types.bool_ty()),
(b, @types.ind_ty()),
])
let result2 = fun_ab.subst(sigma2)
assert_eq(@hol.Type::pprint(result2), "bool --> ind")
}

#Type Matching

Type matching finds a substitution S such that subst(S, pattern) == target. This is used internally by the kernel to check that polymorphic constants are instantiated consistently:

///|
test "types: matching" {
@hol.Type::reset_table()
@hol.Term::reset_table()

let a = @types.mk_var("'a")
let bool_ty = @types.bool_ty()

let empty_state : @hol.TypeMatchState = { subst: Subst(), tyvars: [] }

// Match 'a against bool => { 'a := bool }
let s = a.match_type(bool_ty, empty_state).subst
let bound = s.lookup(a)
assert_true(bound == Some(bool_ty))

// Match ('a -> 'a) against (bool -> bool) => { 'a := bool }
let fun_aa = @types.mk_fun(a, a)
let fun_bb = @types.mk_fun(bool_ty, bool_ty)
let s2 = fun_aa.match_type(fun_bb, empty_state).subst
assert_true(s2.lookup(a) == Some(bool_ty))
}


#3. Terms: The Term Language

HOL terms form a simply-typed lambda calculus with constants. This implementation uses a locally nameless representation:

///|
enum Term {
FVar(String, Type) // free (named) variable with its type
BVar(Int) // bound variable (de Bruijn index)
Const(String, Type) // declared constant (e.g., "=")
App(Term, Term) // function application
Abs(Term, Term) // lambda abstraction
}

#Why Locally Nameless?

The key insight: bound variables use de Bruijn indices (BVar(0) = the innermost binder, BVar(1) = the next one out, etc.), while free variables use names (FVar("x", ty)). This means alpha-equivalent terms have identical representations:

// \x. x and \y. y both become Abs(_, BVar(0))

No need for alpha-conversion during comparison -- structural equality suffices.

#Building Terms

///|
test "terms: free variables and constants" {
@hol.Type::reset_table()
@hol.Term::reset_table()
let bool_ty = @types.bool_ty()

// Free variables carry a name and a type
let p = @terms.mk_var("p", bool_ty)
guard p is FVar(name, ty) else { fail("expected FVar") }
assert_eq(name, "p")
assert_true(ty == bool_ty)

// The only primitive constant: polymorphic equality = : 'a -> 'a -> bool
let a = @types.mk_var("'a")
let eq_ty = @types.mk_fun(a, @types.mk_fun(a, bool_ty))
let eq_const = @hol.Term::mk_const("=", eq_ty)
assert_true(eq_const is Const(_, _))
guard eq_const is Const(cname, _) else { fail("expected constant") }
assert_eq(cname, "=")
}

#Lambda Abstraction: mk_abs and dest_abs

mk_abs(x, body) replaces free occurrences of x in body with BVar(0), producing a locally nameless abstraction. dest_abs reverses the process, picking a fresh name to avoid capture:

///|
test "terms: abstraction roundtrip" {
@hol.Type::reset_table()
@hol.Term::reset_table()
let a = @types.mk_var("'a")
let x = @terms.mk_var("x", a)

// Build the identity function: \x. x
let id_fn = @hol.Term::mk_abs(x, x)
assert_true(id_fn is Abs(_, _))

// Destruct it back
let (bv, body) = id_fn.dest_abs()
// The binder variable and body should pretty-print the same (identity)
assert_eq(@hol.Term::pprint(body), @hol.Term::pprint(bv))

// The type of \x. x is 'a -> 'a
let id_ty = id_fn.type_of()
assert_eq(@hol.Type::pprint(id_ty), "'a --> 'a")
}

#Application and Type Inference

///|
test "terms: application and type_of" {
@hol.Type::reset_table()
@hol.Term::reset_table()
let a = @types.mk_var("'a")
let x = @terms.mk_var("x", a)
let y = @terms.mk_var("y", a)

// Build the application (\x. x) y
let id_fn = @hol.Term::mk_abs(x, x)
let app = @terms.mk_app(id_fn, y)
assert_true(app is App(_, _))

// The type of (\x. x) y should be 'a
let app_ty = app.type_of()
assert_true(app_ty == a)

// Build an equation: x = y
let eq = @hol.Term::mk_eq(x, y)
assert_true(@hol.Term::is_eq(eq))
let (lhs, rhs) = @hol.Term::dest_eq(eq)
assert_true(lhs == x)
assert_true(rhs == y)
}

#Alpha-Convertibility

Because bound variables are de Bruijn indices, alpha-equivalent terms are structurally identical. The aconv function checks this:

///|
test "terms: alpha-convertibility" {
@hol.Type::reset_table()
@hol.Term::reset_table()
let a = @types.mk_var("'a")
let x = @terms.mk_var("x", a)
let y = @terms.mk_var("y", a)

// \x. x and \y. y are alpha-convertible
let abs_x = @hol.Term::mk_abs(x, x)
let abs_y = @hol.Term::mk_abs(y, y)
assert_true(abs_x.aconv(abs_y))

// But x and y (as free variables) are NOT alpha-convertible
assert_false(x.aconv(y))
}

#Pretty-Printing

///|
test "terms: pretty-printing" {
@hol.Type::reset_table()
@hol.Term::reset_table()
let bool_ty = @types.bool_ty()
let p = @terms.mk_var("p", bool_ty)
let q = @terms.mk_var("q", bool_ty)

// A simple equation: p = q
let eq = @hol.Term::mk_eq(p, q)
assert_eq(@hol.Term::pprint(eq), "= p q :: bool")

// The identity on booleans: (\p. p)
let id_bool = @hol.Term::mk_abs(p, p)
assert_eq(@hol.Term::pprint(id_bool), "(\\p. p) :: bool --> bool")
}


#4. Substitution and Matching

Substitution is central to HOL. There are three kinds:

  1. Type substitution (Type::subst) -- replaces type variables in types
  2. Type instantiation (Term::inst) -- applies a type substitution to every type annotation in a term
  3. Term substitution (Term::subst) -- replaces free term variables

#Type Instantiation on Terms

This is how polymorphic constants get specialized. For example, the equality constant = : 'a -> 'a -> bool can be instantiated to = : bool -> bool -> bool:

///|
test "subst: type instantiation on terms" {
@hol.Type::reset_table()
@hol.Term::reset_table()
let a = @types.mk_var("'a")
let bool_ty = @types.bool_ty()

// Build polymorphic equality: = : 'a -> 'a -> bool
let eq_ty = @types.mk_fun(a, @types.mk_fun(a, bool_ty))
let eq_poly = @hol.Term::mk_const("=", eq_ty)

// Instantiate 'a := bool
let sigma : @hol.TypeSubst = Subst(pairs=[(a, bool_ty)])
let eq_bool = eq_poly.inst(sigma)

// The resulting type should be bool -> bool -> bool
let result_ty = eq_bool.type_of()
let expected = @types.mk_fun(bool_ty, @types.mk_fun(bool_ty, bool_ty))
assert_true(result_ty == expected)
}

#Term Substitution

Term substitution replaces free variables in a term:

///|
test "subst: term substitution" {
@hol.Type::reset_table()
@hol.Term::reset_table()
let bool_ty = @types.bool_ty()
let p = @terms.mk_var("p", bool_ty)
let q = @terms.mk_var("q", bool_ty)

// Build p = p, then substitute p := q to get q = q
let eq_pp = @hol.Term::mk_eq(p, p)
let sigma : @hol.TermSubst = Subst(pairs=[(p, q)])
let eq_qq = eq_pp.subst(sigma)

let (lhs, rhs) = @hol.Term::dest_eq(eq_qq)
assert_true(lhs == q)
assert_true(rhs == q)
}

#Term Matching

Pattern matching finds substitutions (both term-level and type-level) such that applying them to a pattern yields the observation. This is used by tactics and rewriting:

///|
test "subst: term matching" {
@hol.Type::reset_table()
@hol.Term::reset_table()
let a = @types.mk_var("'a")
let bool_ty = @types.bool_ty()

// Pattern: variable x of type 'a
let x = @terms.mk_var("x", a)
// Observation: variable p of type bool
let p = @terms.mk_var("p", bool_ty)

// match x against p => { x := p } and { 'a := bool }
let (tm_s, ty_s) = x.match_term(p)
assert_eq([..tm_s].length(), 1)
assert_eq([..ty_s].length(), 1)
}


#5. Boolean Syntax: Connectives from Equality

One of HOL Light's most elegant design decisions: all logical connectives and quantifiers are defined in terms of the single primitive constant = : 'a -> 'a -> bool. The definitions are:

T := (\p. p) = (\p. p) /\ := \p q. (\f. f p q) = (\f. f T T) ==> := \p q. (p /\ q) = p forall:= \P. P = (\x. T) exist := \P. forall q. (forall x. P x ==> q) ==> q \/ := \p q. forall r. (p ==> r) ==> (q ==> r) ==> r F := forall p. p ~ := \p. p ==> F ?! := \P. exist P /\ forall x y. P x /\ P y ==> x = y

Each connective has a uniform API of three functions:
  • mk_* (constructor) -- builds the formula
  • dest_* (destructor) -- takes it apart
  • is_* (predicate) -- tests whether a term has that form

#Implication and Negation

///|
test "bool_syntax: implication and negation" {
@hol.Type::reset_table()
@hol.Term::reset_table()
let bool_ty = @types.bool_ty()
let p = @terms.mk_var("p", bool_ty)
let q = @terms.mk_var("q", bool_ty)

// Build p ==> q
let imp = @hol.BoolSyntax::mk_imp(p, q)
assert_true(@hol.BoolSyntax::is_imp(imp))
let (ant, conseq) = @hol.BoolSyntax::dest_imp(imp)
assert_true(ant == p)
assert_true(conseq == q)

// Build ~p
let neg = @hol.BoolSyntax::mk_neg(p)
assert_true(@hol.BoolSyntax::is_neg(neg))
// dest_neg returns the inner term (not the whole negation)
let inner = @hol.BoolSyntax::dest_neg(neg)
assert_true(inner == p)
}

#Quantifiers

Quantifiers are applied to abstractions. forall x. P x is encoded as (forall) (\x. P x) -- the universal constant applied to a lambda:

///|
test "bool_syntax: quantifiers" {
@hol.Type::reset_table()
@hol.Term::reset_table()
let bool_ty = @types.bool_ty()
let x = @terms.mk_var("x", bool_ty)
let p = @terms.mk_var("p", bool_ty)

// Build: forall x. x ==> p
// Internally this is App(forall_inst, Abs(x, x ==> p))
let body = @hol.BoolSyntax::mk_imp(x, p)
let fa = @hol.BoolSyntax::mk_forall(x, body)
assert_true(fa is App(_, _))
// The type of a quantified formula is bool
assert_true(fa.type_of() == bool_ty)

// Build: exist x. x ==> p
let ex = @hol.BoolSyntax::mk_exists(x, body)
assert_true(ex is App(_, _))
assert_true(ex.type_of() == bool_ty)
}

#Conjunction and Disjunction

///|
test "bool_syntax: conjunction and disjunction" {
@hol.Type::reset_table()
@hol.Term::reset_table()
let bool_ty = @types.bool_ty()
let p = @terms.mk_var("p", bool_ty)
let q = @terms.mk_var("q", bool_ty)

// Build p /\ q
let conj = @hol.BoolSyntax::mk_conj(p, q)
assert_true(@hol.BoolSyntax::is_conj(conj))
let (cl, cr) = @hol.BoolSyntax::dest_conj(conj)
assert_true(cl == p)
assert_true(cr == q)

// Build p \/ q
let disj = @hol.BoolSyntax::mk_disj(p, q)
assert_true(@hol.BoolSyntax::is_disj(disj))
let (dl, dr) = @hol.BoolSyntax::dest_disj(disj)
assert_true(dl == p)
assert_true(dr == q)
}


#6. The Kernel: Primitive Inference Rules

The kernel is the trusted core of HOL. It provides the abstract type Thm and exactly 10 primitive inference rules. A theorem { hyps, concl } asserts that under hypotheses hyps, the conclusion concl holds.

#The Rules at a Glance

refl t |- t = t assume phi phi |- phi eqMp (A1 |- phi = psi) (A2 |- phi) A1 U A2 |- psi absThm v (A |- t = u) A |- (\v. t) = (\v. u) appThm (A1 |- f = g) (A2 |- x = y) A1 U A2 |- f x = g y deductAntisym (A1 |- phi) (A2 |- psi) (A1\{psi}) U (A2\{phi}) |- phi = psi termSubst sigma (A |- phi) A[sigma] |- phi[sigma] typeSubst sigma (A |- phi) A[sigma] |- phi[sigma] betaConv (\v. t) u |- (\v. t) u = t[u/v] defineConst c t |- c = t

Let's work through each one with live examples.

#refl: Reflexivity of Equality

refl(t) produces |- t = t with no hypotheses:

///|
test "kernel: refl" {
@hol.Type::reset_table()
@hol.Term::reset_table()
let a = @types.mk_var("'a")
let x = @terms.mk_var("x", a)

// |- x = x
let th = @hol.Kernel::refl(x)
assert_eq(th.hyps.length(), 0) // no hypotheses
let (lhs, rhs) = @hol.Term::dest_eq(th.concl)
assert_true(lhs == x)
assert_true(rhs == x)
}

#assume: Assumption Introduction

assume(phi) produces phi |- phi -- the formula assumed as both hypothesis and conclusion:

///|
test "kernel: assume" {
@hol.Type::reset_table()
@hol.Term::reset_table()
let p = @terms.mk_var("p", @types.bool_ty())

// p |- p
let th = @hol.Kernel::assume_(p)
assert_eq(th.hyps.length(), 1)
assert_true(th.hyps[0] == p)
assert_true(th.concl == p)
}

#eqMp: Modus Ponens for Equality

Given A1 |- phi = psi and A2 |- phi, conclude A1 U A2 |- psi. This is the workhorse rule -- it lets you "rewrite" the truth of phi into the truth of psi via an equation:

///|
test "kernel: eqMp" {
@hol.Type::reset_table()
@hol.Term::reset_table()
let p = @terms.mk_var("p", @types.bool_ty())

// |- p = p (from refl) and p |- p (from assume)
let th_eq = @hol.Kernel::refl(p) // |- p = p
let th_p = @hol.Kernel::assume_(p) // p |- p

// p |- p (by eqMp with trivial equation)
let th = @hol.Kernel::eqMp(th_eq, th_p)
assert_true(th.concl == p)
assert_eq(th.hyps.length(), 1)
}

#appThm: Congruence for Application

Given A1 |- f = g and A2 |- x = y, conclude A1 U A2 |- f x = g y. If equal functions are applied to equal arguments, the results are equal:

///|
test "kernel: appThm" {
@hol.Type::reset_table()
@hol.Term::reset_table()
let a = @types.mk_var("'a")
let x = @terms.mk_var("x", a)
let f = @terms.mk_var("f", @types.mk_fun(a, a))

// |- f = f and |- x = x
let th_f = @hol.Kernel::refl(f)
let th_x = @hol.Kernel::refl(x)

// |- f x = f x
let th = @hol.Kernel::appThm(th_f, th_x)
let (lhs, rhs) = @hol.Term::dest_eq(th.concl)
assert_true(lhs.aconv(rhs))
}

#absThm: Congruence Under Abstraction

Given A |- t = u and a variable v not free in the hypotheses, conclude A |- (\v. t) = (\v. u):

///|
test "kernel: absThm" {
@hol.Type::reset_table()
@hol.Term::reset_table()
let a = @types.mk_var("'a")
let x = @terms.mk_var("x", a)

// |- x = x
let th = @hol.Kernel::refl(x)

// |- (\x. x) = (\x. x)
let abs_th = @hol.Kernel::absThm(x, th)
let (lhs, rhs) = @hol.Term::dest_eq(abs_th.concl)
assert_true(lhs is Abs(_, _))
assert_true(lhs.aconv(rhs))
}

#deductAntisym: The Deduction Theorem

Given A1 |- phi and A2 |- psi, conclude (A1 \ {psi}) U (A2 \ {phi}) |- phi = psi. This is how you prove two formulas equal by assuming each and deriving the other:

///|
test "kernel: deductAntisym" {
@hol.Type::reset_table()
@hol.Term::reset_table()
let p = @terms.mk_var("p", @types.bool_ty())
let q = @terms.mk_var("q", @types.bool_ty())

// p |- p and q |- q
let th_p = @hol.Kernel::assume_(p)
let th_q = @hol.Kernel::assume_(q)

// {p}\{q} U {q}\{p} |- p = q => {p, q} |- p = q
let th = @hol.Kernel::deductAntisym(th_p, th_q)
let (lhs, rhs) = @hol.Term::dest_eq(th.concl)
assert_true(lhs == p)
assert_true(rhs == q)
// Both p and q remain as hypotheses (since p != q)
assert_eq(th.hyps.length(), 2)
}

#betaConv: Beta Reduction

betaConv((\v. t), u) produces |- (\v. t) u = t[u/v]:

///|
test "kernel: betaConv" {
@hol.Type::reset_table()
@hol.Term::reset_table()
let a = @types.mk_var("'a")
let x = @terms.mk_var("x", a)
let y = @terms.mk_var("y", a)

// (\x. x) applied to y => |- (\x. x) y = y
let id_fn = @hol.Term::mk_abs(x, x)
let th = @hol.Kernel::betaConv(id_fn, y)
let (_, rhs) = @hol.Term::dest_eq(th.concl)
assert_true(rhs == y)

// More interesting: (\x. x = x) applied to y => |- (\x. x = x) y = (y = y)
let eq_self = @hol.Term::mk_eq(x, x)
let pred = @hol.Term::mk_abs(x, eq_self)
let th2 = @hol.Kernel::betaConv(pred, y)
let (_, rhs2) = @hol.Term::dest_eq(th2.concl)
// rhs2 should be y = y
let (eq_l, eq_r) = @hol.Term::dest_eq(rhs2)
assert_true(eq_l == y)
assert_true(eq_r == y)
}

#defineConst: Definitional Extension

defineConst("c", t) registers a new constant c with the type of t and returns |- c = t. This is how the logical vocabulary grows while maintaining conservativity:

///|
test "kernel: defineConst" {
@hol.Type::reset_table()
@hol.Term::reset_table()
let a = @types.mk_var("'a")
let x = @terms.mk_var("x", a)

// Define id := \x. x
let id_body = @hol.Term::mk_abs(x, x)
let th = @hol.Kernel::defineConst("tutorial_id", id_body)

// The theorem is |- tutorial_id = (\x. x)
assert_eq(th.hyps.length(), 0)
let (lhs, rhs) = @hol.Term::dest_eq(th.concl)
assert_true(lhs is Const(_, _))
assert_true(rhs.aconv(id_body))
}

#termSubst and typeSubst: Substitution Rules

These rules apply substitutions to both hypotheses and conclusion:

///|
test "kernel: termSubst and typeSubst" {
@hol.Type::reset_table()
@hol.Term::reset_table()
let a = @types.mk_var("'a")
let bool_ty = @types.bool_ty()
let x = @terms.mk_var("x", a)
let y = @terms.mk_var("y", a)

// |- x = x (via refl)
let th = @hol.Kernel::refl(x)

// termSubst {x := y} gives |- y = y
let s : @hol.TermSubst = Subst(pairs=[(x, y)])
let th2 = @hol.Kernel::termSubst(s, th)
let (lhs, rhs) = @hol.Term::dest_eq(th2.concl)
assert_true(lhs == y)
assert_true(rhs == y)

// typeSubst {'a := bool} on |- x = x gives |- x:bool = x:bool
let ts : @hol.TypeSubst = Subst(pairs=[(a, bool_ty)])
let th3 = @hol.Kernel::typeSubst(ts, th)
let result_ty = th3.concl.type_of()
assert_true(result_ty == bool_ty)
}


#7. Derived Rules: Building on the Kernel

logic/equal.mbt builds derived inference rules using only kernel primitives. Because they compose only sound primitives, they are sound by construction -- no additional trust is needed.

#sym: Symmetry of Equality

Given A |- s = t, derive A |- t = s:

///|
test "derived: sym" {
@hol.Type::reset_table()
@hol.Term::reset_table()
let a = @types.mk_var("'a")
let x = @terms.mk_var("x", a)
let y = @terms.mk_var("y", a)

// x = y |- x = y
let eq_xy = @hol.Term::mk_eq(x, y)
let th = @hol.Kernel::assume_(eq_xy)

// x = y |- y = x
let sym_th = @hol.Equal::sym(th)
let (lhs, rhs) = @hol.Term::dest_eq(sym_th.concl)
assert_true(lhs == y)
assert_true(rhs == x)
}

#How does sym work internally?

The derivation is surprisingly clever. Here is the sketch:

// Given: A |- s = t
//
// 1. refl(s) gives |- s = s
// 2. apTerm((= s), th) gives A |- (= s s) = (= s t)
// i.e., A |- (s = s) = (s = t)
// 3. eqMp with the |- s = s from step 1 yields A |- t = s
//
// The actual code uses appThm and the partially-applied equality operator
// to achieve this in a compact way.

#trans: Transitivity of Equality

Given A1 |- s = t and A2 |- t = u, derive A1 U A2 |- s = u:

///|
test "derived: trans" {
@hol.Type::reset_table()
@hol.Term::reset_table()
let a = @types.mk_var("'a")
let x = @terms.mk_var("x", a)
let y = @terms.mk_var("y", a)
let z = @terms.mk_var("z", a)

// x = y |- x = y and y = z |- y = z
let th1 = @hol.Kernel::assume_(@hol.Term::mk_eq(x, y))
let th2 = @hol.Kernel::assume_(@hol.Term::mk_eq(y, z))

// {x = y, y = z} |- x = z
let th = @hol.Equal::trans(th1, th2)
let (lhs, rhs) = @hol.Term::dest_eq(th.concl)
assert_true(lhs == x)
assert_true(rhs == z)
}

#apTerm and apThm: Congruence Helpers

  • apTerm(f, A |- x = y) derives A |- f x = f y (apply the same function to both sides)
  • apThm(x, A |- f = g) derives A |- f x = g x (apply equal functions to the same argument)

///|
test "derived: apTerm and apThm" {
@hol.Type::reset_table()
@hol.Term::reset_table()
let a = @types.mk_var("'a")
let x = @terms.mk_var("x", a)
let y = @terms.mk_var("y", a)
let f = @terms.mk_var("f", @types.mk_fun(a, a))
let g = @terms.mk_var("g", @types.mk_fun(a, a))

// x = y |- x = y
let th_xy = @hol.Kernel::assume_(@hol.Term::mk_eq(x, y))

// apTerm f (x = y |- x = y) => x = y |- f x = f y
let th1 = @hol.Equal::apTerm(f, th_xy)
let (lhs1, rhs1) = @hol.Term::dest_eq(th1.concl)
assert_true(lhs1 is App(_, _))
assert_true(rhs1 is App(_, _))

// f = g |- f = g
let th_fg = @hol.Kernel::assume_(@hol.Term::mk_eq(f, g))

// apThm x (f = g |- f = g) => f = g |- f x = g x
let th2 = @hol.Equal::apThm(x, th_fg)
let (lhs2, rhs2) = @hol.Term::dest_eq(th2.concl)
assert_true(lhs2 is App(_, _))
assert_true(rhs2 is App(_, _))
}

#mkBinop: Binary Operator Congruence

mkBinop(op, lth, rth) combines two equalities under a binary operator: given A1 |- l1 = l2 and A2 |- r1 = r2, derive A1 U A2 |- op l1 r1 = op l2 r2:

///|
test "derived: mkBinop" {
@hol.Type::reset_table()
@hol.Term::reset_table()
let bool_ty = @types.bool_ty()
let p1 = @terms.mk_var("p1", bool_ty)
let p2 = @terms.mk_var("p2", bool_ty)
let q1 = @terms.mk_var("q1", bool_ty)
let q2 = @terms.mk_var("q2", bool_ty)

// p1 = p2 |- p1 = p2 and q1 = q2 |- q1 = q2
let lth = @hol.Kernel::assume_(@hol.Term::mk_eq(p1, p2))
let rth = @hol.Kernel::assume_(@hol.Term::mk_eq(q1, q2))

// Use the implication connective as the binary operator
let op = @hol.BoolSyntax::implication()
let th = @hol.Equal::mkBinop(op, lth, rth)

// Result: |- (==> p1 q1) = (==> p2 q2)
let (lhs, rhs) = @hol.Term::dest_eq(th.concl)
assert_true(@hol.BoolSyntax::is_imp(lhs))
assert_true(@hol.BoolSyntax::is_imp(rhs))
}

#alpha: Alpha-Equivalence as a Theorem

alpha(t1, t2) produces |- t1 = t2 when t1 and t2 are alpha-convertible:

///|
test "derived: alpha" {
@hol.Type::reset_table()
@hol.Term::reset_table()
let a = @types.mk_var("'a")
let x = @terms.mk_var("x", a)
let y = @terms.mk_var("y", a)

// \x. x and \y. y are alpha-equivalent
let t1 = @hol.Term::mk_abs(x, x)
let t2 = @hol.Term::mk_abs(y, y)

// |- (\x. x) = (\y. y)
let th = @hol.Equal::alpha(t1, t2)
assert_eq(th.hyps.length(), 0)
assert_true(@hol.Term::is_eq(th.concl))
}


#8. Putting It All Together

Let's work a complete example that exercises multiple packages. We will:

  1. Build some types and terms
  2. Define a constant
  3. Prove a small theorem using kernel rules
  4. Apply derived rules to transform it

///|
test "worked example: combining packages" {
@hol.Type::reset_table()
@hol.Term::reset_table()
let bool_ty = @types.bool_ty()
let a = @types.mk_var("'a")

// --- Types ---
// We work with bool and 'a -> bool (predicates)
let pred_ty = @types.mk_fun(a, bool_ty)
assert_eq(@hol.Type::pprint(pred_ty), "'a --> bool")

// --- Terms ---
// Build the identity on booleans: \p. p
let p = @terms.mk_var("p", bool_ty)
let id_bool = @hol.Term::mk_abs(p, p)

// --- Boolean syntax ---
// Build: p ==> p
let imp_pp = @hol.BoolSyntax::mk_imp(p, p)
assert_true(@hol.BoolSyntax::is_imp(imp_pp))

// --- Kernel ---
// Prove |- id_bool = id_bool (reflexivity)
let refl_id = @hol.Kernel::refl(id_bool)
assert_eq(refl_id.hyps.length(), 0)

// Prove p |- p (assumption)
let assume_p = @hol.Kernel::assume_(p)
assert_eq(assume_p.hyps.length(), 1)

// Prove |- (\p. p) p = p (beta reduction)
let beta_th = @hol.Kernel::betaConv(id_bool, p)
let (_, beta_rhs) = @hol.Term::dest_eq(beta_th.concl)
assert_true(beta_rhs == p)

// --- Derived rules ---
// Symmetry: |- p = (\p. p) p
let sym_beta = @hol.Equal::sym(beta_th)
let (sym_l, sym_r) = @hol.Term::dest_eq(sym_beta.concl)
assert_true(sym_l == p)
assert_true(sym_r is App(_, _))
}

#Summary

This tutorial has covered the complete HOL construction from first principles:

PackageWhat it providesKey insight
TypesTyVar, TyApp, mk_fun, bool_tySimply-typed lambda calculus with a registry
TermsFVar, BVar, Const, App, AbsLocally nameless: alpha-equivalence for free
SubstitutionType subst, term subst, matchingHow polymorphism and rewriting work
Bool syntaxAll connectives from = aloneDefinitional minimalism
Kernel10 primitive rules, abstract ThmLCF architecture = soundness guarantee
Derived rulessym, trans, apTerm, etc.Sound by construction

The LCF architecture means you can trust the system as long as you trust the kernel (~250 lines). Everything else -- derived rules, tactics, decision procedures -- is just convenience built on that foundation.

#Further Reading

  • John Harrison's HOL Light -- the original system this project is based on
  • John Harrison's HOL Light Tutorial for a comprehensive guide to the OCaml implementation
  • Robin Milner's Logic for Computable Functions for the LCF architecture

#
BoolSyntax

Namespace for Boolean connective and quantifier operations in HOL.

#
Equal

using @bobzhang/hol/logic { type Equal }

Namespace for derived equality rules built from kernel primitives.

#
Kernel

Namespace for the HOL Light LCF kernel inference rules.

#
Subst

An immutable substitution mapping keys of type A to values of type B, represented as an association list with most-recent-first lookup.

#
Term

using @bobzhang/hol/terms { type Term }

HOL term in locally nameless representation: free variables are named, bound variables use de Bruijn indices, and constants carry their (possibly polymorphic) type.

#
TermMatchResult

The result of pattern matching a term: a pair of a term substitution and a type substitution that, when applied to the pattern, yield the observation.

#
TermSubst

A substitution mapping terms to terms, used to replace free variables in a term.

#
Thm

using @bobzhang/hol/logic { type Thm }

A proven theorem, consisting of a set of hypotheses and a conclusion (hyps |- concl).

#
Type

using @bobzhang/hol/types { type Type }

HOL types: a simply-typed lambda calculus type language with type variables (Var) and type constructor applications (App).

#
TypeMatchState

The state carried during type matching: a pair of the current substitution and the list of identity-bound type variables.

#
TypeSubst

A substitution mapping type variables to types.

#
bool_ty

Return the built-in Boolean type (TyApp("bool", [])).

#
const_

Construct a constant term without checking the global signature. Intended for the logic layer, which builds connective and type-operator heads before they are formally registered. Prefer mk_const for user-facing code.

#
ind_ty

Return the built-in individual type (App("ind", [])), used for the axiom of infinity.

#
mk_app

Construct a function application term.

#
mk_fun

Construct the function type t1 -> t2 (i.e., App("fun", [t1, t2])).

#
mk_var

Construct a free (named) variable with the given name and type.

Source Files

Powered by MoonBit

Site sourceReport issuePackagesBuild queueSkillsStatistics

© 2026 mooncakes.io