// Learn the basics of Lambdapi in 15 minutes (this is a one-line comment).
/* Install support for Lambdapi files in Emacs or VSCode to better
visualize this file and the generated subgoals in proofs
(this is a multi-lines comment). */
/* In Lambdapi, you can declare type and object symbols. Symbol names
can contain unicode characters (utf8). */
/* Convention: identifiers starting with an uppercase letter denote
types, while identifiers starting with a lowercase letter denote objects. */
symbol ℕ : TYPE; // is a type declaration
// Commands are separated by semi-colons.
symbol zero : ℕ; // is an object declaration
symbol succ : ℕ → ℕ; /* means that "succ" takes an argument of type ℕ
and returns something of type ℕ. */
/* We can use the usual decimal notation for natural numbers by mapping
Lambdapi builtin symbols to user-defined symbols: */
builtin "0" ≔ zero;
builtin "+1" ≔ succ;
// We can make definitions as follows:
symbol ten ≔ 10;
// We can ask Lambdapi the type of a term:
type ten;
// We can check that a term has a given type:
assert ⊢ ten : ℕ;
// or that a term does not have a given type:
assertnot ⊢ succ : ℕ;
symbol + : ℕ → ℕ → ℕ;
notation + infix right 10;
/* means that + is declared as infix.
"right" means that "x + y + z" is the same as "x + (y + z)".
10 is the priority level of +. It is useful to parse expressions
with various infix operators (see below). */
symbol × : ℕ → ℕ → ℕ;
notation × infix right 20;
/* The priority level of × is higher than the one of +.
So "x + y × z" is parsed as "x + (y × z)". We can check it as follows: */
assert x y z ⊢ x + y × z ≡ x + (y × z);
assertnot x y z ⊢ x + y × z ≡ (x + y) × z;
/* We can now tell Lambdapi to identify any term of the form "t + 0" with "t"
by simply declaring the following rewrite rule: */
rule $x + zero ↪ $x; // rule variables must be prefixed by "$"
// We can also ask Lambdapi to evaluate a term using the declared rules:
compute zero + zero;
// and check the result:
assert ⊢ zero + zero ≡ zero;
/* ≡ is the equational theory generated by the user-defined rules
and β-reduction + η-reduction if the flag "eta_equality" is on. */
// The definition of + can be completed by adding a new rule later:
rule $x + succ $y ↪ succ ($x + $y);
// Several rules can also be given as once:
rule zero × $y ↪ zero
with succ $x × $y ↪ $y + $x × $y;
/* We now would like to prove some theorem about +. To this end, since
Lambdapi does not come with a pre-defined logic, we first need to
define what is a proposition and what is a proof.
You usually just need to install a package defining some logics and
require it in your development. For instance, using Opam, you can do:
opam install lambdapi-logics
Then, in your development, you can use one of the logics defined in
this package (see https://github.com/Deducteam/lambdapi-logics for
more details) as follows:
require Logic.TFF.Main;
This tells Lambdapi to load in the environment the symbols, rules,
etc. declared in the package Logic.TFF, which defines multi-sorted
first-order logic. A symbol f of Logic.TFF can then be refered to by
Logic.TFF.f. To avoid writing Logic.TFF every time, you can do:
open Logic.TFF.Main;
The two operations can also be done at the same time by simply writing;
require open Logic.TFF;
But we are going to define our own logic hereafter to show that it is
simple. */
// We first declare a type for propositions:
symbol Prop : TYPE;
// and symbols for building propositions:
symbol = : ℕ → ℕ → Prop;
notation = infix 1;
/* We then use the "proposition-as-type" technique to reduce
proof-checking to type-checking. To this end, we define a type for the
proofs of a proposition: */
injective symbol Prf : Prop → TYPE;
/* Note that this is a type-level function, that is, a function that
takes as argument a term of type Prop and return a term of type TYPE.
Moreover, we declare Prf as injective. This means that Prf
should satisfy the following property:
if Prf(p) ≡ Prf(q) then p ≡ q.
The verification of this property is left to the user.
This information is used by Lambdapi to simplify a unification constraint
of the form Prf(p) ≡ Prf(q) into p ≡ q (see below). */
/* We then say that a proof of a proposition "p" is any term of type
"Prf(p)". We therefore need to declare axioms saying which
propositions are true. For instance, a usual axiom for equality is
that it is a reflexive relation: */
symbol =-refl x : Prf(x = x);
/* For all x, "=-refl x" is a proof of the proposition "x = x".
Note that Lambdapi can infer the type of "x" automatically:
the user does not need to write "symbol =-refl (x : ℕ) : Prf(x = x)". */
/* Another usual axiom is that function application preserves equality: */
symbol =-succ x y : Prf(x = y) → Prf(succ x = succ y);
/* We also need to declare that we can prove any proposition "p" on natural
numbers by induction: */
symbol ind_ℕ (p : ℕ → Prop)
(case-zero : Prf(p zero))
(case-succ : Π x : ℕ, Prf(p x) → Prf(p (succ x)))
/* While "A → B" is the Lambdapi type of functions from "A" to "B",
"Π x : A, B(x)" is the Lambdapi type of so-called "dependent" functions
since x may occur in B(x). This is the type of functions mapping
any element a of type A to some element of type B(a).
It boils down to "A → B" if x does not occur in B.
Hence, case-succ is a function that takes as arguments a natural
number x and a proof that "p x" is true, and returns a proof that
"p (succ x)" is true. */
(n : ℕ)
: Prf(p n);
/* We will later see that this induction principle can in fact be
automatically generated by Lambdapi by using the "inductive" command
when declaring ℕ, zero and succ. */
/* We are now ready to prove that, for any natural number "x",
"zero + x" is equal to "x", that is, to show that there exists a term, that
we will call "zero_is_neutral_for_+", of type "Π x : ℕ, Prf (zero + x = x)".
To this end, Lambdapi provides an interactive mode (launched by the keyword
"begin") to enable users to define this term step by step using tactics. */
opaque /* We declare the symbol as opaque as we do not want Lambdapi
to unfold it later. */
symbol zero_is_neutral_for_+ x : Prf (zero + x = x) ≔
begin
/* Here, in Emacs or VSCode, the system prints the following goal to prove:
?zero_is_neutral_for_+: Π x: ℕ, Prf ((zero + x) = x) */
/* To proceed by induction on x, we simply need to say that
?zero_is_neutral_for_+ should be of the form "ind_ℕ _ _ _"
where "_" stands for a term to be built.
This can be done by using the "refine" tactic: */
/* However, if we simply write "refine ind_ℕ _ _ _",
Lambdapi will complain with the following error message:
"Missing subproofs (0 subproofs for 2 subgoals)."
This is because we gave no subproof for the case-zero and case-succ arguments.
Indeed, in Lambdapi, proofs must be well structured, that is, a tactic
must be followed by as many subproofs enclosed between curly brackets
as the number of subgoals generated by the tactic. So, here, we need to write
"refine nat _ _ _ {} {}" and then write the missing subproofs. */
/* Remark: if we hadn't declared Prf as injective, we would have gotten
4 subgoals. the first generated subgoal would not have been a typing goal
but the following unification goal:
"Prf (?4 n) ≡ Prf ((zero + n) = n)"
where "?4" stands for the unknown predicate "p" that we try to prove,
which would have been the second goal, the third goal being the case for
zero, and the fourth goal being the case for succ.
This is one of the interesting features of Lambdapi to have unification
goals. However, currently, Lambdapi has only one tactic for unification
goals, namely "solve", which is applied automatically and thus didn't work
here. However, we can see that, to simplify this unification goal to
"?4 n ≡ (zero + n) = n"
we need Prf to be injective. */
refine ind_ℕ _ _ _
{ /* Here comes the proof of the first generated subgoal:
"Prf ((zero + zero) = zero)".
Note that Lambdapi infered the predicate to prove automatically.
To prove the first subgoal, we can first ask Lambdapi to simplify it
by applying user-defined rewriting rules with the tactic "simplify": */
simplify;
/* We then get the following goal:
"Prf (zero = zero)"
which can be solved by using =-refl: */
refine =-refl zero;
}
{ /* Here comes the proof of the second generated subgoal:
"Π x: ℕ, Prf ((zero + x) = x) → Prf ((zero + succ x) = succ x)"
We start by assuming a given x : ℕ such that "zero + x = x"
using the tactic "assume": */
assume x hyp_on_x;
/* Again, we can simplify the goal: */
simplify;
/* We can then conclude by using the axiom =-succ and the assumption
hyp_on_x: */
refine =-succ (zero + x) x hyp_on_x;
/* Alternatively, we could use the "rewrite" tactic of Lambdapi,
to replace "zero + x" by "x", but this requires to set up a number
of "builtins" (see below). */
};
end;
/* Remark: now that we proved that "zero + x" is equal to "x", we can turn
this equality into a rewrite rule to reason modulo this rule automatically. */
rule zero + $y ↪ $y;
/* We are going to see below how this first proof can be simplified
by using more advanced features of Lambdapi. */
/* First, the induction principle on natural numbers can be automatically
generated by Lambdapi by using the command "inductive",
as long as the builtins "Prop" and "P" are set: */
builtin "Prop" ≔ Prop;
builtin "P" ≔ Prf;
inductive Nat : TYPE ≔ z : Nat | s : Nat → Nat;
/* To see what has been generated, you can write: */
print Nat;
/* prints:
constant symbol Nat: TYPE
constructors:
z: Nat
s: Nat → Nat
induction principle:
ind_Nat: Π p0: (Nat → Prop), P (p0 z) → (Π x0: Nat, P (p0 x0) → P (p0 (s x0))) → Π x0: Nat, P (p0 x0)
*/
print ind_Nat;
/* prints:
symbol ind_Nat
: Π p0: (Nat → Prop), P (p0 z) → (Π x0: Nat, P (p0 x0) → P (p0 (s x0))) → Π x0: Nat, P (p0 x0)
rules:
ind_Nat $v0_p0 $v1_z $v2_s z ↪ $v1_z
ind_Nat $v0_p0 $v1_z $v2_s (s $v3_x0) ↪ $v2_s $v3_x0 (ind_Nat $v0_p0 $v1_z $v2_s $v3_x0)
*/
print ten; // to see the definition of ten
print +; // to see the type, notation and rules of +
/* It is also possible now to replace the tactic "refine ind_Nat _ _ _"
by a call to the tactic "induction" (see below). */
/* Similarly, we can define a type for lists of natural numbers: */
inductive List_Nat : TYPE ≔
| nil_Nat : List_Nat
| cons_Nat : Nat → List_Nat → List_Nat;
/* But what if we want to have lists of booleans, or lists of lists of
natural numbers, etc.? Lambdapi does not allow to quantify over
types. On the other hand, it is possible to interpret objects as types
using type-level rewriting rules, in the same way the function Prf
maps terms of type Prop to types. We can define polymorphic objects by
defining a type for type codes, and quantify over type codes instead
of types: */
symbol Set : TYPE; // type for type codes
injective symbol τ : Set → TYPE; // function interpreting type codes as types
// For instance, we can define a code for Nat:
symbol nat : Set;
rule τ nat ↪ Nat;
// We can now define polymorphic lists:
(a:Set) inductive 𝕃:TYPE ≔
| □ /* \Box */ : 𝕃 a
| ⸬ /* :: */ : τ a → 𝕃 a → 𝕃 a;
/* We are now going to see how to use tactics related to equality like
"reflexivity", "symmetry" and "rewrite". To this end, we need to use a
polymorphic equality and define a few more builtins that are necessary
for Lambdapi to generate the corresponding proofs. */
constant // means that we cannot add rules later
symbol ≃ [a] /* arguments between square brackets are implicit
and must not be written later, unless they are enclosed between square brackets
or if the symbol is prefixed by "@". */
: τ a → τ a → Prop;
notation ≃ infix 1;
constant symbol ≃-refl [a] (x:τ a) : Prf(x ≃ x);
constant symbol ≃-ind [a] [x y:τ a] : Prf(x ≃ y) → Π p, Prf(p y) → Prf(p x);
// Leibniz principle.
builtin "T" ≔ τ;
builtin "eq" ≔ ≃;
builtin "refl" ≔ ≃-refl;
builtin "eqind" ≔ ≃-ind;
/* We now reprove our theorem on the inductive type Nat instead of ℕ,
using the tactics "induction", "reflexivity" and "rewrite".
To this end, we first need to define addition on Nat: */
symbol ⊕ : Nat → Nat → Nat;
notation ⊕ infix right 10;
rule $x ⊕ z ↪ $x
with $x ⊕ s $y ↪ s ($x ⊕ $y);
opaque symbol zero_is_neutral_for_⊕ x : Prf(z ⊕ x ≃ x) ≔
begin
induction
{ simplify; reflexivity; }
{ assume x hyp_on_x; simplify; rewrite hyp_on_x; reflexivity; }
end;
/* Note finally that a development can be split into several
files. For instance, imagine that your development is made of file1.lp
and file2.lp, and that file2.lp uses symbols defined in
file1.lp. Then, you should create a file lambdapi.pkg with the
following two lines:
package_name = my_package
root_path = my_package
where my_package is the name of your package. Then, at the beginning
of file2.lp, you should add:
require my_package.file1;
*/