AST
SK Polymorphic Typing
I interpret the SK combinators as polymorphic functions of the form:
$$ K\ (\alpha : \text{Type}\ m)\ (\beta : \text{Type}\ n) : \alpha \rightarrow \beta \rightarrow \alpha \\ S\ (\alpha : \text{Type}\ m)\ (\beta : \text{Type}\ n)\ (\gamma : \text{Type}\ o) : (\alpha \rightarrow \beta \rightarrow \gamma) \rightarrow (\alpha \rightarrow \beta) \rightarrow \alpha \rightarrow \gamma $$
where \(K\ \mathbb{N}\ \mathbb{R}\) produces a function of type \(\mathbb{N} \rightarrow \mathbb{R} \rightarrow \mathbb{N}\).
Borrowing from the calculus of constructions, the types of \(K\) and \(S\) are explicitly given by the \(\forall\) expression:
$$ K : (\forall \alpha : \text{Type}\ m, \beta : \text{Type}\ n, x : \alpha\ y : \beta . \alpha) \\ S : (\forall \alpha : \text{Type}\ m, \beta : \text{Type}\ n, \gamma : \text{Type}\ o, \\ x : (\forall x : \alpha, y : \beta, z : \gamma . \gamma), y : (\forall x : \alpha . \beta), z : \alpha . \gamma) $$
Typing judgements on function application are derived from substitution on \(\forall\). For example, the type of \(K\ \mathbb{N}\) is derived from substitution of \(\mathbb{N}\) into the type of \(K\). That is, \(\alpha\) is replaced with \(\mathbb{N}\), and the type of the expression is said to be equal to the body of the \(\forall\) in which the value was substituted.
The \(K\) and \(S\) combinators can be encoded in AST form in Lean using De Bruijn indices like such:
import Mathlib.Tactic
A BindId n
refers to the value of the variable binder in the nth-up \(\forall\) expression.
structure BindId where
toNat : ℕ
deriving BEq, Repr, DecidableEq
namespace BindId
def succ (id : BindId) : BindId := ⟨id.toNat.succ⟩
instance : LT BindId where
lt (slf : BindId) (other : BindId) : Prop :=
slf.toNat < other.toNat
instance : LE BindId where
le (slf : BindId) (other : BindId) : Prop :=
slf.toNat ≤ other.toNat
instance : DecidableRel (@LT.lt BindId _) :=
fun a b => inferInstanceAs (Decidable (a.toNat < b.toNat))
instance : Preorder BindId where
le_refl (slf : BindId) : slf ≤ slf := by
unfold LE.le
unfold instLE
simp
le_trans (a b c : BindId) : a ≤ b → b ≤ c → a ≤ c := by
intro a b
unfold LE.le at *
unfold instLE at *
simp_all
linarith
lt_iff_le_not_le (a b : BindId) : a < b ↔ a ≤ b ∧ ¬b ≤ a := by
unfold LE.le
unfold LT.lt
unfold instLT
unfold instLE
simp
intro a
unfold toNat at *
linarith
end BindId
For example, the expression \(\forall x : \alpha.x\) can be rewritten using De Bruijn indices to \(\forall \alpha.1\).
An expression is one of: \(K\), \(S\), \(\text{Prop}\), \(\text{Ty}\ n\), \((\forall e_{1}.e_{2})\), \(e_{1}\ e_{2}\), or a variable \(n\).
For convenience and legibility, I create a separate definition of each expression:
inductive K where
| mk : K
deriving DecidableEq, BEq, Repr
inductive S where
| mk : S
deriving DecidableEq, BEq, Repr
inductive Prp where
| mk : Prp
deriving DecidableEq, BEq, Repr
Stratified type universes.
inductive Ty where
| mk : ℕ → Ty
deriving DecidableEq, BEq, Repr
mutual
inductive Call where
| mk : SkExpr → SkExpr → Call
deriving DecidableEq, BEq, Repr
Var uses a 1-indexed De Bruijn position.
inductive Var where
| mk : BindId → Var
deriving DecidableEq, BEq, Repr
\(\forall\) takes a type of its binder \(x : t\), and has a body containing \(e\).
inductive Fall where
| mk : SkExpr → SkExpr → Fall
inductive SkExpr where
| k : K → SkExpr
| s : S → SkExpr
| prp : Prp → SkExpr
| ty : Ty → SkExpr
| fall : Fall → SkExpr
| call : Call → SkExpr
| var : Var → SkExpr
deriving DecidableEq, BEq, Repr
end
Some convenience accessor methods:
namespace Ty
def n : Ty → ℕ
| .mk n => n
end Ty
namespace Call
def lhs : Call → SkExpr
| .mk lhs _ => lhs
def rhs : Call → SkExpr
| .mk _ rhs => rhs
end Call
namespace SkExpr
Whenever a value is substituted in to a \(\lambda\) expression, all free variables must be incremented by 1 in order to prevent shadowing from the new surrounding expression.
def with_indices_plus (in_expr : SkExpr) (shift_by : BindId) (at_depth : ℕ) : SkExpr :=
match in_expr with
| .fall (.mk bind_ty body) =>
.fall (.mk (bind_ty.with_indices_plus shift_by at_depth.succ) (body.with_indices_plus shift_by at_depth.succ))
| .var (.mk n) =>
if n.toNat > at_depth then
var (.mk ⟨n.toNat + shift_by.toNat⟩)
else
var (.mk n)
| .call (.mk lhs rhs) =>
call (.mk (lhs.with_indices_plus shift_by at_depth) (rhs.with_indices_plus shift_by at_depth))
| x => x
end SkExpr
namespace Fall
def bind_ty : Fall → SkExpr
| .mk bind_ty _ => bind_ty
def body : Fall → SkExpr
| .mk _ body => body
def substitute (in_fall : Fall) (with_expr : SkExpr) : Fall :=
let rec substitute_e (in_expr : SkExpr) (n : BindId) (with_expr : SkExpr) : SkExpr :=
match in_expr with
| .fall (.mk bind_ty body) =>
.fall (.mk (substitute_e bind_ty n.succ with_expr) (substitute_e body n.succ with_expr))
| .var (.mk n') => if n = n' then with_expr else .var (.mk n')
| .call (.mk lhs rhs) => .call (.mk (substitute_e lhs n with_expr) (substitute_e rhs n with_expr))
| x => x
match in_fall with
| .mk bind_ty body =>
.mk (substitute_e bind_ty ⟨1⟩ (with_expr.with_indices_plus ⟨1⟩ 0)) (substitute_e body ⟨1⟩ (with_expr.with_indices_plus ⟨1⟩ 0))
end Fall
I give a few test cases of index shifting. See the preservation chapter for more complicated, rigorous lemmas.
example : ((Fall.mk (.ty (.mk 0)) (.var (.mk ⟨1⟩)))).substitute (.var (.mk ⟨2⟩)) = (Fall.mk (.ty (.mk 0)) (.var (.mk ⟨3⟩))) := by
simp [Fall.substitute]
simp [Fall.substitute.substitute_e]
simp [SkExpr.with_indices_plus]
example : (Fall.mk (.ty (.mk 0)) (.var (.mk ⟨1⟩))).substitute (.fall (.mk (.ty (.mk 0)) (.var (.mk ⟨4⟩)))) = (Fall.mk (.ty (.mk 0)) ((.fall (.mk (.ty (.mk 0)) (.var (.mk ⟨5⟩)))))) := by
simp [Fall.substitute]
simp [Fall.substitute.substitute_e]
simp [SkExpr.with_indices_plus]
One-step evaluation is only defined for \(K\ \alpha\ \beta\ x\ y\) and \(S\ \alpha\ \beta\ \gamma\ x\ y\ z\).
namespace Call
def eval_once : Call → SkExpr
| (.mk (.call (.mk (.call (.mk (.call (.mk (.k .mk) _)) _)) x)) _) => x
| (.mk (.call (.mk (.call (.mk (.call (.mk (.call (.mk (.call (.mk (.s .mk) _)) _)) _)) x)) y)) z) => .call (.mk (.call (.mk x z)) (.call (.mk y z)))
| (.mk x y) => (.call (.mk x y))
end Call
Made by Dowland Aiello.