Haskell implementation of Thierry Coquand and Gerard Huet's Calculus of Constructions. In essence it is the apex of Berendregt's lambda cube, capturing the expressiveness of STLC, System F, Omega, and LF. This calculus is presented as a pure type system and is hence incredibly simple in representation with universal quantification doubling up as the Pi type, abstraction, 2 sorts (* and box), and application.
This strongly normalising calculus This allows us to model higher-order predicate logic in a language, and hence do proof in programs. More importantly it can serve as a constructive foundation of mathematics and hence make the link between proof and program explicit.
You need Haskell, this compiles with GHC 8.2.2 at least (Stack resolver: lts-11.0).
You can use cabal to build and run this, see this README, alternatively you can use vanilla ghc to build:
To compile and run do:
ghc -O2 -o c Main
then run ./c
Alternatively to use the GHCi Interpreter do:
ghci Main
then type main
In either case you get something like the following:
Welcome to the C REPL
Type some terms or press Enter to leave.
>
Note: When run in GHCi, you don't have the luxury of escaped characters, backspace, delete etc... Compile it using GHC if you need this.
Where you can then have some fun, try these examples:
Pi x:*.\y:x.x
identity function on proper types.Pi n:*.(n->n)->n->n
church encoding of Nats.(\x:*.x) (Pi x:*.(x -> x) -> x -> x)
identity function for nats\X:*.\x:X.x
polymorphic identity function\x:*.\f:x->x.\z:x.z
Church-encoding of Peano style zero\n:(Pi n:*.(n->n)->n->n).\A:*.\f:A->A.\z:A.f (n A f z)
succ for church encoded natsλn:Π A:*.(A->A)->A->A.λm:Π A:*.(A->A)->A->A.n (Π A:*.(A->A)->A->A) (λn:Π n:*.(n->n)->n->n.λA:*.λf:A->A.λz:A.f (n A f z)) m
plus for church encoded natsPi A:*.Pi B:*.A -> B
encoding of A implies B from propositional logicPi A:*.Pi B:*.Pi C:*.(A -> B -> C) -> C
encoding of tuples A and BPi A:*.Pi B:*.Pi C:*.(A -> B) -> (B -> C) -> C
encoding of disjoint unions\A:*.\x:A.\y:A.Pi p:A->*.(p x) -> p y
equality for terms of a type.\A:*.\x:A.Pi p:A->*.(p x) -> p x
the reflexivity of equality.- for examples of dependent typing, see the assume examples below.
Note: Π
is the dependent product type and Πx:A.B
is the same as A -> B
when x
does not occur in B
. Π
is alternatively typed as Pi
, respectively. *
is the sort of proper types, it has type □
but □
isn't part of the syntax as it is the maximal sort.
The parser is smart enough to recognise λ, Π , and *; so you can copy and paste from the output:
> Pi x:*.\y:x.x
= Π x:*.λy:x.x
> Π x:*.λy:x.x
= Π x:*.λy:x.x
>
denotes the REPL waiting for input, =
means no reductions occurred (it's the same term), ~>
denotes one reduction, and ~>*
denotes 0 or more reductions (although in practice this is 1 or more due to =
).
There is also a reduction tracer, which should print each reduction step. prefix any string with '
in order to see the reductions:
> '(\n:(Pi n:*.(n->n)->n->n).\A:*.\f:A->A.\z:A.f (n A f z)) (\x:*.\f:x->x.\z:x.z)
~> λA:*.λf:A->A.λz:A.f ((λx:*.λf:x->x.λz:x.z) A f z)
~> λA:*.λf:A->A.λz:A.f ((λf:A->A.λz:A.z) f z)
~> λA:*.λf:A->A.λz:A.f ((λz:A.z) z)
~> λA:*.λf:A->A.λz:A.f z
Note: this is succ 0
in peano arithmetic.
There is also a typing mechanism, which should display the type or fail as usual.
> t\X:*.\x:X.x x
Cannot Type Term: λX:*.λx:X.x x
> t\X:*.\x:X.x
Π X:*.X->X
Note: if you provide a non-normalizing term, the type checker will fail and reduction will not occur.
You can save terms for the life of the program with a let
expression. Any time a saved variable appears in a term, it will be substituted for the saved term:
> let nat = Pi N:*.(N->N)->N->N
Saved term: nat = Π N:*.(N->N)->N->N
> let 0 = \X:*.\f:X->X.\z:X.z
Saved term: 0 = λX:*.λf:X->X.λz:X.z
> let s = \n:nat.\A:*.\f:A->A.\z:A.f (n A f z)
Saved term: s = λn:Π N:*.(N->N)->N->N.λA:*.λf:A->A.λz:A.f (n A f z)
> s 0
~>* λA:*.λf:A->A.λz:A.f z
> let plus = λn:nat.λm:nat.n nat (λn:nat.λA:*.λf:A->A.λz:A.f (n A f z)) m
Saved term: plus = λn:Π N:*.(N->N)->N->N.λm:Π N:*.(N->N)->N->N.n (Π N:*.(N->N)->N->N) (λn:Π N:*.(N->N)->N->N.λA:*.λf:A->A.λz:A.f (n A f z)) m
> plus (s 0) (s 0)
~>* λA:*.λf:A->A.λz:A.f (f z)
try to prove that plus n 0 = n
. You'll need to invent an nat induction term, and make use of equality and refl defined above.
Note: Consequently let
and =
are keywords, and so you cannot name variables with these. Additionally Pi
and assume
are keywords.
We have the ability to embed types, constructors, or type families into the typing context with assume statements:
> assume Bool : *
Saved typing: Bool:*
> assume true : Bool
Saved typing: true:Bool
> assume Vec : Pi X:*. Pi n:nat.*
Saved typing: Vec:*->(Π N:*.(N->N)->N->N)->*
> Vec Bool 0
= Vec Bool (λX:*.λf:X->X.λz:X.z)
> tVec Bool 0
*
> assume nil : Vec Bool 0
Saved typing: nil:Vec Bool (λX:*.λf:X->X.λz:X.z)
> assume cons : Pi n:nat.Bool -> (Vec Bool n) -> (Vec Bool (s n))
Saved typing: cons:Π n:Π N:*.(N->N)->N->N.Bool->(Vec Bool n)->Vec Bool ((λn:Π N:*.(N->N)->N->N.λA:*.λf:A->A.λz:A.f (n A f z)) n)
> assume cons : Pi n:nat.Bool -> (Vec Bool n) -> (Vec Bool (s n))
Saved typing: cons:Π n:Π N:*.(N->N)->N->N.Bool->(Vec Bool n)->Vec Bool ((λn:Π N:*.(N->N)->N->N.λA:*.λf:A->A.λz:A.f (n A f z)) n)
> cons 0 true nil
= cons (λX:*.λf:X->X.λz:X.z) true nil
> tcons 0 true nil
Vec Bool (λA:*.λf:A->A.λz:A.f z)
Note: assume
is not part of the core calculus and if you use it in the wrong way you may run into theoretical problems. It is possible to design a fully polymorphic vector type using just let
and plumbing, but assumes give us syntactic freedom that makes this language fun to use. Parsing and typing is also done here so you must ensure your terms are well-typed.
this allows us to encode new types and their constructors. Consequently assume
is also a keyword.
We base the language on the BNF for the typed calculus:
However we adopt standard bracketing conventions to eliminate ambiguity in the parser. Concretely, the parser implements the non-ambiguous grammar for terms as follows:
Some notes about the syntax:
- The above syntax only covers the core calculus, and not the repl extensions (such as let/assume bindings above). The extensions are simply added on in the repl.
- Term variables are strings, as this is isomorphic to a whiteboard treatment and hence the most familiar. Unlike previous languages in the zoo the constraint on variables is relaxed so that you may make alpha-numeric terms.
- In line with pure type systems, the type/term abstraction is collapsed so that we have a single syntactic category of terms. Terms are variables (
a,b,c,...
), abstractions (\x:t1.t2
), Pi abstractions (Pi n:t1.t2
), applicationst1 t2
, and sorts (*
). - Typing is thus a transformation from terms to terms, where variables parametrise terms through abstraction, and instantiate others through application.
- To prevent cyclic and otherwise theoretical complications there is a sort
*
of well-typed terms, box for well-formed kinds. This allows us to establish a hierarchy of terms. - Arrows
A -> B
termsPi x:A.B
where x doesn't occur inB
. This is syntactic shorthand and is only present in the pretty printer. - Box is not part of the syntax as it is only used in typing.
- To quit use
Ctrl+C
or whatever your machine uses to interrupt computations.
The semantics implements beta-reduction on terms and structural equality as the Eq
instance of CTerm
. The term semantics are the same as STLC with the addition of dependent abstraction of types over terms, and kinds. We reformulate the semantics as typing judgements:
Firstly we have the typical rule for variables (that they must be in scope, and typed bys the Abs rule or in the context):
Next we have a rule that states proper types are well-formed kinds:
The abstraction rule then states that we parametrize over terms of proper type as is standard in LF, FOmega etc...
The Pi rule then states that we must parametrize over terms of proper type/kind (from the set sj
below). The pure type system approach specifies what values of sx
and sy
we may pick, and the calculus of constructions is maximally expressive (allowing all combinations of both sorts). In less expressive languages such as STLC sx
is *
and sy
is *
only.
where:
Finally application:
- The use of Pure Type Systems means the typing context contains mappings from variables to terms only. The phrase X : T thus means term X is has type T (which is also a term). We do not implement Agda style type hierarchies here.
- There is the additional constraint that any term abstractions must take a term of proper type. This prevents nonsensical or partially-applied types terms such as
Vec nat
. - The base calculus on its own does not have any base types (such as
Nat
,Bool
orUnit
) however you can use church encodings and terms as types to get this directly, or otherwise introduce symbolic representations using theassume
statement provided by the parser. - Type-level application is identical to term-level application. In applying a term
t
of typeT
to a termf
of typePi x:T.T2
we substitutet
intoT2
whereverx
is bound. - Arrows/Pi types have the constraint that both the domain and co-domain must be proper types. This prevents non-nonsensical types such as
Pair Nat _ -> Nat
. - Equality is of academic interest in the calculus of constructions and so we have deliberately not included it. Instead we only implement standard alpha equivalence of terms and encourage the user to implement equality as a structure in C, and a proof.
- This implementation follows a small-step operational semantics and Berendregt's variable convention (see
substitution
in C.hs). - Reductions include the one-step reduction (see
reduce1
in C.hs), the many-step reduction (seereduce
in C.hs). Additionally there is a one-step type-level reduction (seereduce1T
in C.hs).
- C.hs contains the Haskell implementation of the calculus, including substitution, reduction, and other useful things.
- Parser.hs contains the monadic parser combinators needed to parse input strings into typed-term ASTs for the calculus.
- Repl.hs contains a simple read-eval-print loop which hooks into main, and into the parser.
- Main.hs is needed for GHC to compile without any flags, it also invokes the repl.
For contributions, see the project to-do list or submit a PR with something you think it needs.