This commit is contained in:
2026-04-19 04:17:45 +00:00
commit a154e2b98c
14 changed files with 617 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
.lake/
build/
lakefile.olean
lake-manifest.json
+7
View File
@@ -0,0 +1,7 @@
import BidirTT.Syntax
import BidirTT.Value
import BidirTT.Pretty
import BidirTT.Eval
import BidirTT.Context
import BidirTT.Check
import BidirTT.Examples
+105
View File
@@ -0,0 +1,105 @@
import BidirTT.Context
import BidirTT.Eval
import BidirTT.Pretty
namespace BidirTT
abbrev TCM := Except String
private def showTy (l : Lvl) (v : Val) : String :=
match quote l v with
| Except.ok tm => prettyTm tm
| Except.error err => s!"<ill-scoped value: {err}>"
mutual
partial def check (cxt : Cxt) : Raw Val TCM Tm
| .lam x t, .pi a c => do
let bodyTy cApp c (.var cxt.lvl)
let t' check (cxt.bind x a) t bodyTy
pure (.lam t')
| .pair t u, .sig a c => do
let t' check cxt t a
let vt eval cxt.env t'
let bodyTy cApp c vt
let u' check cxt u bodyTy
pure (.pair t' u')
| .letE x a t u, ty => do
let (a', _) inferUniverse cxt a
let va := eval cxt.env a'
let va va
let t' check cxt t va
let vt eval cxt.env t'
let u' check (cxt.define x vt va) u ty
pure (.letE a' t' u')
| r, ty => do
let (t', ty') infer cxt r
if ( conv cxt.lvl ty' ty) then
pure t'
else
throw s!"type mismatch: expected {showTy cxt.lvl ty}, got {showTy cxt.lvl ty'}"
partial def infer (cxt : Cxt) : Raw TCM (Tm × Val)
| .var x =>
match cxt.lookup x with
| some (i, a) => pure (.var i, a)
| none => throw s!"unknown variable {x}"
| .univ i => pure (.univ i, .univ (i + 1))
| .app t u => do
let (t', tty) infer cxt t
match tty with
| .pi a c => do
let u' check cxt u a
let vu eval cxt.env u'
let bodyTy cApp c vu
pure (.app t' u', bodyTy)
| _ => throw s!"expected Pi type in application, got {showTy cxt.lvl tty}"
| .fst t => do
let (t', tty) infer cxt t
match tty with
| .sig a _ => pure (.fst t', a)
| _ => throw s!"expected Sigma type in .1, got {showTy cxt.lvl tty}"
| .snd t => do
let (t', tty) infer cxt t
match tty with
| .sig _ c => do
let vt eval cxt.env t'
let fstv vFst vt
let bodyTy cApp c fstv
pure (.snd t', bodyTy)
| _ => throw s!"expected Sigma type in .2, got {showTy cxt.lvl tty}"
| .pi x a b => do
let (a', i) inferUniverse cxt a
let va eval cxt.env a'
let (b', j) inferUniverse (cxt.bind x va) b
pure (.pi a' b', .univ (Nat.max i j))
| .sig x a b => do
let (a', i) inferUniverse cxt a
let va eval cxt.env a'
let (b', j) inferUniverse (cxt.bind x va) b
pure (.sig a' b', .univ (Nat.max i j))
| .ann t a => do
let (a', _) inferUniverse cxt a
let va eval cxt.env a'
let t' check cxt t va
pure (t', va)
| .letE x a t u => do
let (a', _) inferUniverse cxt a
let va eval cxt.env a'
let t' check cxt t va
let vt eval cxt.env t'
let (u', uty) infer (cxt.define x vt va) u
pure (.letE a' t' u', uty)
| .lam _ _ => throw "cannot infer type of lambda, use an annotation"
| .pair _ _ => throw "cannot infer type of pair, use an annotation"
partial def inferUniverse (cxt : Cxt) (r : Raw) : TCM (Tm × Nat) := do
let (t, ty) infer cxt r
match ty with
| .univ level => pure (t, level)
| _ => throw s!"expected a universe, got {showTy cxt.lvl ty}"
end
def checkTop (r : Raw) : TCM (Tm × Val) :=
infer Cxt.empty r
end BidirTT
+30
View File
@@ -0,0 +1,30 @@
import BidirTT.Value
namespace BidirTT
structure Cxt where
env : Env
types : List (Name × Val)
lvl : Lvl
deriving Inhabited
def Cxt.empty : Cxt := [], [], 0
def Cxt.bind (cxt : Cxt) (x : Name) (a : Val) : Cxt :=
{ env := .var cxt.lvl :: cxt.env
, types := (x, a) :: cxt.types
, lvl := cxt.lvl + 1 }
def Cxt.define (cxt : Cxt) (x : Name) (v a : Val) : Cxt :=
{ env := v :: cxt.env
, types := (x, a) :: cxt.types
, lvl := cxt.lvl + 1 }
private def lookupGo : Name List (Name × Val) Nat Option (Nat × Val)
| _, [], _ => none
| x, (y, a) :: rest, i => if x == y then some (i, a) else lookupGo x rest (i+1)
def Cxt.lookup (cxt : Cxt) (x : Name) : Option (Nat × Val) :=
lookupGo x cxt.types 0
end BidirTT
+150
View File
@@ -0,0 +1,150 @@
import BidirTT.Value
namespace BidirTT
abbrev EvalM := Except String
mutual
partial def eval : Env Tm EvalM Val
| env, .var i =>
match env[i]? with
| some v => pure v
| none =>
throw s!"bad de Bruijn index {i} in environment of size {env.length}"
| env, .lam t => pure (.lam (.mk env t))
| env, .app t u => do
let vt eval env t
let vu eval env u
vApp vt vu
| env, .pi a b => do
let va eval env a
pure (.pi va (.mk env b))
| env, .sig a b => do
let va eval env a
pure (.sig va (.mk env b))
| env, .pair t u => do
let vt eval env t
let vu eval env u
pure (.pair vt vu)
| env, .fst t => do
let vt eval env t
vFst vt
| env, .snd t => do
let vt eval env t
vSnd vt
| _, .univ i => pure (.univ i)
| env, .letE _ t u => do
let vt eval env t
eval (vt :: env) u
partial def vApp : Val Val EvalM Val
| .lam c, u => cApp c u
| t, u => pure (.app t u)
partial def vFst : Val EvalM Val
| .pair a _ => pure a
| t => pure (.fst t)
partial def vSnd : Val EvalM Val
| .pair _ b => pure b
| t => pure (.snd t)
partial def cApp : Closure Val EvalM Val
| .mk env body, v => eval (v :: env) body
end
partial def quote : Lvl Val EvalM Tm
| l, .var x =>
if x < l then
pure (.var (l - x - 1))
else
throw s!"bad level {x} while quoting at level {l}"
| l, .app t u => do
let qt quote l t
let qu quote l u
pure (.app qt qu)
| l, .fst t => do
let qt quote l t
pure (.fst qt)
| l, .snd t => do
let qt quote l t
pure (.snd qt)
| l, .lam c => do
let body cApp c (.var l)
let qb quote (l + 1) body
pure (.lam qb)
| l, .pi a c => do
let qa quote l a
let body cApp c (.var l)
let qb quote (l + 1) body
pure (.pi qa qb)
| l, .sig a c => do
let qa quote l a
let body cApp c (.var l)
let qb quote (l + 1) body
pure (.sig qa qb)
| l, .pair a b => do
let qa quote l a
let qb quote l b
pure (.pair qa qb)
| _, .univ i => pure (.univ i)
private def andThen (lhs : EvalM Bool) (rhs : Unit EvalM Bool) : EvalM Bool := do
if ( lhs) then
rhs ()
else
pure false
partial def conv : Lvl Val Val EvalM Bool
| _, .univ i, .univ j => pure (i == j)
| l, .pi a c, .pi a' c' =>
andThen (conv l a a') fun _ => do
let b cApp c (.var l)
let b' cApp c' (.var l)
conv (l + 1) b b'
| l, .sig a c, .sig a' c' =>
andThen (conv l a a') fun _ => do
let b cApp c (.var l)
let b' cApp c' (.var l)
conv (l + 1) b b'
| l, .lam c, .lam c' =>
do
let body cApp c (.var l)
let body' cApp c' (.var l)
conv (l + 1) body body'
| l, .lam c, t =>
do
let body cApp c (.var l)
let apped vApp t (.var l)
conv (l + 1) body apped
| l, t, .lam c =>
do
let apped vApp t (.var l)
let body cApp c (.var l)
conv (l + 1) apped body
| l, .pair a b, .pair a' b' =>
andThen (conv l a a') fun _ => conv l b b'
| l, .pair a b, p =>
andThen
(do
let fstp vFst p
conv l a fstp)
fun _ => do
let sndp vSnd p
conv l b sndp
| l, p, .pair a b =>
andThen
(do
let fstp vFst p
conv l fstp a)
fun _ => do
let sndp vSnd p
conv l sndp b
| _, .var x, .var y => pure (x == y)
| l, .app t u, .app t' u' =>
andThen (conv l t t') fun _ => conv l u u'
| l, .fst t, .fst t' => conv l t t'
| l, .snd t, .snd t' => conv l t t'
| _, _, _ => pure false
end BidirTT
+72
View File
@@ -0,0 +1,72 @@
import BidirTT.Syntax
namespace BidirTT.Examples
open BidirTT
def idTy : Raw :=
.pi "A" (.univ 0) (.pi "_" (.var "A") (.var "A"))
def idTm : Raw :=
.lam "A" (.lam "x" (.var "x"))
def idAnn : Raw := .ann idTm idTy
def constTy : Raw :=
.pi "A" (.univ 0) (.pi "B" (.univ 0)
(.pi "_" (.var "A") (.pi "_" (.var "B") (.var "A"))))
def constTm : Raw :=
.lam "A" (.lam "B" (.lam "x" (.lam "_" (.var "x"))))
def constAnn : Raw := .ann constTm constTy
def swapTy : Raw :=
.pi "A" (.univ 0) (.pi "B" (.univ 0)
(.pi "_" (.sig "_" (.var "A") (.var "B"))
(.sig "_" (.var "B") (.var "A"))))
def swapTm : Raw :=
.lam "A" (.lam "B" (.lam "p"
(.pair (.snd (.var "p")) (.fst (.var "p")))))
def swapAnn : Raw := .ann swapTm swapTy
def depPairTy : Raw :=
.sig "A" (.univ 2) (.var "A")
def depPairTm : Raw :=
.pair
(.univ 1)
(.pi "_" (.univ 0) (.univ 0))
def depPairAnn : Raw := .ann depPairTm depPairTy
def fstDepPair : Raw := .fst depPairAnn
def sndDepPair : Raw := .snd depPairAnn
def omegaTy : Raw :=
.pi "A" (.univ 0) (.var "A")
def omegaTm : Raw :=
.lam "x" (.app (.var "x") (.var "x"))
def omegaAnn : Raw := .ann omegaTm omegaTy
def unknownVar : Raw := .var "nope"
def pairMismatch : Raw :=
.ann (.pair (.univ 1) (.univ 1))
(.sig "A" (.univ 2) (.var "A"))
def badFst : Raw := .fst (.univ 0)
def letUniverse : Raw :=
.ann
(.letE "A" (.univ 1) (.pi "_" (.univ 0) (.univ 0)) (.var "A"))
(.univ 1)
def univ0 : Raw := .univ 0
end BidirTT.Examples
+19
View File
@@ -0,0 +1,19 @@
import BidirTT.Syntax
namespace BidirTT
mutual
partial def prettyTm : Tm String
| .var i => s!"#{i}"
| .lam t => s!"(fun => {prettyTm t})"
| .app t u => s!"({prettyTm t} {prettyTm u})"
| .pi a b => s!"(Pi {prettyTm a} -> {prettyTm b})"
| .sig a b => s!"(Sigma {prettyTm a} * {prettyTm b})"
| .pair t u => s!"({prettyTm t}, {prettyTm u})"
| .fst t => s!"({prettyTm t}.1)"
| .snd t => s!"({prettyTm t}.2)"
| .univ i => s!"U{i}"
| .letE a t u => s!"(let : {prettyTm a} := {prettyTm t}; {prettyTm u})"
end
end BidirTT
+32
View File
@@ -0,0 +1,32 @@
namespace BidirTT
abbrev Name := String
inductive Raw where
| var : Name Raw
| lam : Name Raw Raw
| app : Raw Raw Raw
| pi : Name Raw Raw Raw
| sig : Name Raw Raw Raw
| pair : Raw Raw Raw
| fst : Raw Raw
| snd : Raw Raw
| univ : Nat Raw
| letE : Name Raw Raw Raw Raw
| ann : Raw Raw Raw
deriving Repr, Inhabited, BEq, DecidableEq
inductive Tm where
| var : Nat Tm
| lam : Tm Tm
| app : Tm Tm Tm
| pi : Tm Tm Tm
| sig : Tm Tm Tm
| pair : Tm Tm Tm
| fst : Tm Tm
| snd : Tm Tm
| univ : Nat Tm
| letE : Tm Tm Tm Tm
deriving Repr, Inhabited, BEq, DecidableEq
end BidirTT
+27
View File
@@ -0,0 +1,27 @@
import BidirTT.Syntax
namespace BidirTT
mutual
inductive Val where
| var : Nat Val
| app : Val Val Val
| fst : Val Val
| snd : Val Val
| lam : Closure Val
| pi : Val Closure Val
| sig : Val Closure Val
| pair : Val Val Val
| univ : Nat Val
inductive Closure where
| mk : List Val Tm Closure
end
abbrev Env := List Val
abbrev Lvl := Nat
instance : Inhabited Val := .univ 0
instance : Inhabited Closure := .mk [] (.univ 0)
end BidirTT
+30
View File
@@ -0,0 +1,30 @@
import BidirTT
open BidirTT
def runOne (label : String) (r : Raw) : IO Unit := do
match checkTop r with
| .ok (t, ty) =>
IO.println s!"[ok] {label}"
IO.println s!" term : {BidirTT.prettyTm t}"
match quote 0 ty with
| Except.ok qt =>
IO.println s!" type : {BidirTT.prettyTm qt}"
| Except.error err =>
IO.println s!" type : <error: {err}>"
| .error e =>
IO.println s!"[err] {label}: {e}"
def main : IO Unit := do
runOne "U0" Examples.univ0
runOne "id" Examples.idAnn
runOne "const" Examples.constAnn
runOne "swap" Examples.swapAnn
runOne "depPair" Examples.depPairAnn
runOne "depPair.1" Examples.fstDepPair
runOne "depPair.2" Examples.sndDepPair
runOne "let universe" Examples.letUniverse
runOne "omega (bad)" Examples.omegaAnn
runOne "unknown var" Examples.unknownVar
runOne "pair mismatch" Examples.pairMismatch
runOne "bad fst" Examples.badFst
+17
View File
@@ -0,0 +1,17 @@
# iris
Small (toy) dependently typed core w. a bidirectional typechecker. It takes a named raw syntax, checks and elaborates it into a de Bruijn core and evaluates terms into semantic values, quotes them back for diagnostics and uses conversion checking to compare normal forms. The current kernel has explicit universe levels `U0`, `U1`, `U2`, dependent function and pair types, projections, annotations, and `let`
One of the checked examples is:
```lean
def depPairTy : Raw :=
.sig "A" (.univ 2) (.var "A")
def depPairTm : Raw :=
.pair
(.univ 1)
(.pi "_" (.univ 0) (.univ 0))
```
which elaborates to a pair whose first component is the type `U1` and whose second component inhabits it
+106
View File
@@ -0,0 +1,106 @@
import BidirTT
open BidirTT
inductive Expectation where
| okTy : Tm Expectation
| errContains : String Expectation
private def renderType (ty : Val) : Except String Tm :=
BidirTT.quote 0 ty
private def containsText (haystack needle : String) : Bool :=
needle.isEmpty || (haystack.splitOn needle).length > 1
structure TestCase where
name : String
input : Raw
expect : Expectation
def cases : List TestCase := [
"U0 is typed by U1", Examples.univ0,
.okTy (.univ 1),
"id typechecks", Examples.idAnn,
.okTy (.pi (.univ 0) (.pi (.var 0) (.var 1))),
"const typechecks", Examples.constAnn,
.okTy (.pi (.univ 0) (.pi (.univ 0) (.pi (.var 1) (.pi (.var 1) (.var 3))))),
"swap typechecks", Examples.swapAnn,
.okTy (.pi (.univ 0) (.pi (.univ 0) (.pi (.sig (.var 1) (.var 1)) (.sig (.var 1) (.var 3))))),
"dependent pair typechecks", Examples.depPairAnn,
.okTy (.sig (.univ 2) (.var 0)),
"fst infers the first projection", Examples.fstDepPair,
.okTy (.univ 2),
"snd infers the dependent second projection", Examples.sndDepPair,
.okTy (.univ 1),
"let infers through definitions", Examples.letUniverse,
.okTy (.univ 1),
"self application rejected", Examples.omegaAnn,
.errContains "expected Pi type in application",
"unknown variable rejected", Examples.unknownVar,
.errContains "unknown variable nope",
"pair mismatch rejected at the Sigma body", Examples.pairMismatch,
.errContains "type mismatch: expected U1, got U2",
"bad fst rejected", Examples.badFst,
.errContains "expected Sigma type in .1, got U1"
]
def runCase (tc : TestCase) : IO Bool := do
match tc.expect, checkTop tc.input with
| .okTy expectedTy, .ok (_, ty) =>
match renderType ty with
| Except.ok actualTy =>
if actualTy == expectedTy then
IO.println s!"PASS {tc.name}"
pure true
else
IO.println s!"FAIL {tc.name} (expected type {BidirTT.prettyTm expectedTy}, got {BidirTT.prettyTm actualTy})"
pure false
| Except.error err =>
IO.println s!"FAIL {tc.name} (could not quote type: {err})"
pure false
| .okTy expectedTy, .error err =>
IO.println s!"FAIL {tc.name} (expected type {BidirTT.prettyTm expectedTy}, got error {err})"
pure false
| .errContains needle, .error err =>
if containsText err needle then
IO.println s!"PASS {tc.name}"
pure true
else
IO.println s!"FAIL {tc.name} (expected error containing {needle}, got {err})"
pure false
| .errContains needle, .ok (_, ty) =>
match renderType ty with
| Except.ok actualTy =>
IO.println s!"FAIL {tc.name} (expected error containing {needle}, got type {BidirTT.prettyTm actualTy})"
pure false
| Except.error err =>
IO.println s!"FAIL {tc.name} (expected error containing {needle}, got quote failure {err})"
pure false
def runInternalSafetyChecks : IO Bool := do
let malformedEvalOk :=
match eval [] (.var 0) with
| Except.error err => containsText err "bad de Bruijn index 0"
| Except.ok _ => false
let malformedQuoteOk :=
match quote 0 (.var 0) with
| Except.error err => containsText err "bad level 0"
| Except.ok _ => false
if malformedEvalOk && malformedQuoteOk then
IO.println "PASS malformed core terms are rejected safely"
pure true
else
IO.println "FAIL malformed core terms are rejected safely"
pure false
def main : IO UInt32 := do
let results cases.mapM runCase
let safetyOk runInternalSafetyChecks
let allResults := results ++ [safetyOk]
let failed := allResults.countP (· == false)
if failed == 0 then
IO.println s!"\n{allResults.length} passed, 0 failed"
pure 0
else
IO.println s!"\n{allResults.length - failed} passed, {failed} failed"
pure 1
+17
View File
@@ -0,0 +1,17 @@
import Lake
open Lake DSL
package bidirtt where
leanOptions := #[
`pp.unicode.fun, true,
`autoImplicit, false
]
@[default_target]
lean_lib BidirTT where
lean_exe bidirtt where
root := `Main
lean_exe tests where
root := `Tests
+1
View File
@@ -0,0 +1 @@
leanprover/lean4:v4.15.0