8.14.900
Predicate Proust with Equality, Typed Racket
Here’s an updated version of Predicate Proust with Equality; it’s written in Typed Racket, to help you understand it, it does a TINY additional amount of error checking in the parser by prohibiting the use of e.g. λ as a variable name, and it refactors ‘reduce‘ to accept a list of bound variable names rather than a context.
I also changed the ‘reduce‘ rule for unbound variables to signal an error; I’m not confident this is correct.
Here’s a download link for this code: ./proust-pred-eq.rkt
#lang typed/racket |
; file contains Unicode characters - download rather than cut/paste from browser |
(provide def check-one reduce-one clear-defs parse-expr reduce type-check type-synth) |
;; proust-pred-eq: for-all, equality. |
;; Prabhakar Ragde (September 2021) |
;; with contributions from Astra Kolomatskaia |
;; Grammar: |
;; expr = (λ x => expr) ; lambda abstraction |
;; | (expr expr) ; function application |
;; | x ; variable |
;; | (expr : expr) ; expression annotated with type |
;; | (∀ (x : expr) -> expr) ; dependent function type |
;; | (expr -> expr) ; special case equiv to (∀ (_ : expr) -> expr) |
;; | Type ; the type of types |
;; | (expr = expr) ; equality types |
;; | (eq-refl x) ; equality introduction |
;; | (eq-elim expr expr expr expr expr) ; equality elimination |
;; Structures for Expr (abstract syntax tree representing terms). |
(define-type Term (U Symbol Lam App Ann Arrow Type Teq Eq-refl Eq-elim)) |
(struct Lam ([var : Symbol] [body : Term]) #:transparent) |
(struct App ([rator : Term] [rand : Term]) #:transparent) |
(struct Ann ([expr : Term] [type : Term]) #:transparent) |
(struct Arrow ([var : Symbol] [domain : Term] [range : Term]) #:transparent) |
(struct Type () #:transparent) |
(struct Teq ([left : Term] [right : Term]) #:transparent) |
(struct Eq-refl ([val : Term]) #:transparent) |
(struct Eq-elim ([x : Term] [P : Term] [px : Term] [y : Term] [peq : Term]) #:transparent) |
;; A Context is a (Listof (List Symbol Expr)) |
;; association list mapping variables to expressions. |
(define-type Context (Listof (List Symbol Term))) |
(define (ctx-bind-name [pr : (List Symbol Term)]) : Symbol (first pr)) |
;; Parsing |
;; parse-expr : sexp -> Expr |
(define (parse-expr [s : Sexp]) : Term |
(match s |
[`(λ ,(? legal-id? x) => ,e) (Lam x (parse-expr e))] |
[`(λ ,(? legal-id? x) ,(? legal-id? xs) ... => ,e) |
;; cast must succeed by nature of match: |
(Lam x (parse-expr `(λ ,@(cast xs (Listof Sexp)) => ,e)))] |
[`(∀ (,(? legal-id? x) : ,t) -> ,e) (Arrow x (parse-expr t) (parse-expr e))] |
[`(∀ (,(? legal-id? x) : ,t) ,(? list? a) ... -> ,e) (Arrow x (parse-expr t) (parse-expr `(∀ ,@(cast a (Listof Sexp)) -> ,e)))] |
[`(,t1 -> ,t2) (Arrow '_ (parse-expr t1) (parse-expr t2))] |
[`(,t1 -> ,t2 -> ,r ...) (Arrow '_ (parse-expr t1) (parse-expr `(,t2 -> ,@r)))] |
[`(,e : ,t) (Ann (parse-expr e) (parse-expr t))] |
['Type (Type)] |
[`(eq-refl ,a) (Eq-refl (parse-expr a))] |
[`(eq-elim ,e1 ,e2 ,e3 ,e4 ,e5) |
(Eq-elim (parse-expr e1) (parse-expr e2) (parse-expr e3) (parse-expr e4) (parse-expr e5))] |
[`(,e1 = ,e2) (Teq (parse-expr e1) (parse-expr e2))] |
[`(,e1 ,e2) (App (parse-expr e1) (parse-expr e2))] |
[`(,e1 ,e2 ,e3 ,r ...) (parse-expr `((,e1 ,e2) ,e3 ,@r))] |
['_ (error 'parse "cannot use underscore\n")] |
[(? symbol? x) |
(cond [(member x keywords) |
(error 'parse-expr "expected legal identifier, got: ~e" x)] |
[else x])] |
[else (error 'parse "bad syntax: ~a\n" s)])) |
(: legal-id? (Any -> Boolean : #:+ Symbol)) |
(define (legal-id? x) |
(and (symbol? x) (not (member x keywords)))) |
(define keywords : (Listof Symbol) '(∀ λ -> : = Type)) |
;; Unparsing, that is, pretty-printing. |
;; pretty-print-expr : Expr -> String |
(define (pretty-print-expr [e : Term]) : String |
(match e |
[(Lam x b) (format "(λ ~a => ~a)" x (pretty-print-expr b))] |
[(App e1 e2) (format "(~a ~a)" (pretty-print-expr e1) (pretty-print-expr e2))] |
[(? symbol? x) (symbol->string x)] |
[(Ann e t) (format "(~a : ~a)" (pretty-print-expr e) (pretty-print-expr t))] |
[(Arrow '_ t1 t2) (format "(~a -> ~a)" (pretty-print-expr t1) (pretty-print-expr t2))] |
[(Arrow x t1 t2) (format "(∀ (~a : ~a) -> ~a)" x (pretty-print-expr t1) (pretty-print-expr t2))] |
[(Type) "Type"] |
[(Eq-refl a) (format "(eq-refl ~a)" (pretty-print-expr a))] |
[(Eq-elim e1 e2 e3 e4 e5) |
(format "(eq-elim ~a ~a ~a ~a ~a)" (pretty-print-expr e1) (pretty-print-expr e2) |
(pretty-print-expr e3) (pretty-print-expr e4) (pretty-print-expr e5))] |
[(Teq e1 e2) (format "(~a = ~a)" (pretty-print-expr e1) (pretty-print-expr e2))])) |
;; pretty-print-context : Context -> String |
(define (pretty-print-context [ctx : Context]) : String |
(cond |
[(empty? ctx) ""] |
[else (string-append (format "\n~a : ~a" (first (first ctx)) (pretty-print-expr (second (first ctx)))) |
(pretty-print-context (rest ctx)))])) |
;; Substitution |
;; subst : Var Expr Expr -> Expr |
(define (subst [oldx : Symbol] [newx : Term] [expr : Term]) : Term |
(match expr |
[(? symbol? x) (if (equal? x oldx) newx x)] |
[(Arrow '_ t w) (Arrow '_ (subst oldx newx t) (subst oldx newx w))] |
[(Arrow x t w) |
(cond |
[(equal? x oldx) expr] |
[(side-cond? x (list newx) false) |
(define repx (refresh x (list newx w))) |
(Arrow repx (subst oldx newx t) (subst oldx newx (subst x repx w)))] |
[else (Arrow x (subst oldx newx t) (subst oldx newx w))])] |
[(Lam '_ b) (Lam '_ (subst oldx newx b))] |
[(Lam x b) |
(cond |
[(equal? x oldx) expr] |
[(side-cond? x (list newx) false) |
(define repx (refresh x (list newx b))) |
(Lam repx (subst oldx newx (subst x repx b)))] |
[else (Lam x (subst oldx newx b))])] |
[(App f a) (App (subst oldx newx f) (subst oldx newx a))] |
[(Ann e t) (Ann (subst oldx newx e) (subst oldx newx t))] |
[(Type) (Type)] |
[(Eq-refl a) (Eq-refl (subst oldx newx a))] |
[(Eq-elim e1 e2 e3 e4 e5) |
(Eq-elim (subst oldx newx e1) (subst oldx newx e2) (subst oldx newx e3) |
(subst oldx newx e4) (subst oldx newx e5))] |
[(Teq a b) (Teq (subst oldx newx a) (subst oldx newx b))])) |
;; Helpers for substitution |
;; refresh : Var (Listof Expr) -> Var |
(define (refresh [x : Symbol] [lst : (Listof Term)]) : Symbol |
(if (side-cond? x lst true) (refresh (freshen x) lst) x)) |
(define (freshen [x : Symbol]) : Symbol |
(string->symbol (string-append (symbol->string x) "_"))) |
;; checks if variable x occurs free in lst (list of expressions) |
;; and, when check-binders? is #t, if x has a binding occurrence in lst |
(define (side-cond? [x : Symbol] [lst : (Listof Term)] [check-binders? : Boolean]) : Boolean |
(ormap (lambda ([expr : Term]) (sc-helper x expr check-binders?)) lst)) |
(define (sc-helper [x : Symbol] [expr : Term] [check-binders? : Boolean]) : Boolean |
(match expr |
[(? symbol? y) (equal? x y)] |
[(Arrow '_ tt tw) (side-cond? x (list tt tw) check-binders?)] |
[(Arrow y tt tw) |
(cond |
[check-binders? (or (equal? x y) (side-cond? x (list tt tw) check-binders?))] |
[else (if (equal? x y) false (side-cond? x (list tt tw) check-binders?))])] |
[(Lam '_ b) (sc-helper x b check-binders?)] |
[(Lam y b) |
(cond |
[check-binders? (or (equal? x y) (sc-helper x b check-binders?))] |
[else (if (equal? x y) false (sc-helper x b check-binders?))])] |
[(App f a) (side-cond? x (list f a) check-binders?)] |
[(Ann e t) (side-cond? x (list e t) check-binders?)] |
[(Type) false] |
[(Eq-refl a) (sc-helper x a check-binders?)] |
[(Eq-elim e1 e2 e3 e4 e5) |
(side-cond? x (list e1 e2 e3 e4 e5) check-binders?)] |
[(Teq a b) (side-cond? x (list a b) check-binders?)])) |
;; Code for finding fresh variable name not already used in a context |
;; (similar to refresh, above) to be used later in type checking/synthesis |
(define (used-in-ctx? [ctx : Context] [x : Symbol]) : Boolean |
(or (and (assoc x deftypes) #t) (ormap (λ ([p : (List Symbol Term)]) (side-cond? x (rest p) false)) ctx))) |
(define (refresh-with-ctx [ctx : Context] [x : Symbol] [lst : (Listof Term)]) : Symbol |
(if (or (side-cond? x lst true) (used-in-ctx? ctx x)) (refresh-with-ctx ctx (freshen x) lst) x)) |
;; Alpha equivalence (produces #t/#f) |
(define (alpha-equiv? [e1 : Term] [e2 : Term]) (ae-helper e1 e2 empty)) |
;; vmap is association list mapping variables in e1 to variables in e2 |
(define (ae-helper [e1 : Term] [e2 : Term] [vmap : (Listof (List Symbol Symbol))]) : Boolean |
(match (list e1 e2) |
[(list (? symbol? x1) (? symbol? x2)) |
(define xm1 (assoc x1 vmap)) |
(equal? (if xm1 (second xm1) x1) x2)] |
[(list (Lam x1 b1) (Lam x2 b2)) (ae-helper b1 b2 (cons (list x1 x2) vmap))] |
[(list (App f1 a1) (App f2 a2)) (and (ae-helper f1 f2 vmap) (ae-helper a1 a2 vmap))] |
[(list (Ann e1 t1) (Ann e2 t2)) (and (ae-helper e1 e2 vmap) (ae-helper t1 t2 vmap))] |
[(list (Arrow x1 t1 w1) (Arrow x2 t2 w2)) |
(and (ae-helper t1 t2 (cons (list x1 x2) vmap)) (ae-helper w1 w2 (cons (list x1 x2) vmap)))] |
[(list (Type) (Type)) true] |
[(list (Eq-refl x1) (Eq-refl x2)) (ae-helper x1 x2 vmap)] |
[(list (Eq-elim x1 P1 px1 y1 peq1) (Eq-elim x2 P2 px2 y2 peq2)) |
(and (ae-helper x1 x2 vmap) (ae-helper P1 P2 vmap) (ae-helper px1 px2 vmap) |
(ae-helper y1 y2 vmap) (ae-helper peq1 peq2 vmap))] |
[(list (Teq a1 b1) (Teq a2 b2)) |
(and (ae-helper a1 a2 vmap) (ae-helper b1 b2 vmap))] |
[else false])) |
;; Reduction |
;; reduce: bound-vars Expr -> Expr |
;; Reduces an expression by recursively looking up definitions |
;; and doing substitution for applications |
(define (reduce [ctx : (Listof Symbol)] [expr : Term]) : Term |
(match expr |
[(? symbol? x) |
(cond |
[(member x ctx) x] |
[(assoc x defs) => (lambda (p) (reduce ctx (second p)))] |
;; this is an error, right? |
[else (error 'reduce "unbound variable: ~e" x)])] |
[(Arrow '_ a b) |
(Arrow '_ (reduce ctx a) (reduce ctx b))] |
[(Arrow x a b) |
(define ra (reduce ctx a)) |
(define rb (reduce (cons x ctx) b)) |
(Arrow x ra rb)] |
[(Lam '_ b) (Lam '_ (reduce ctx b))] |
[(Lam x b) (Lam x (reduce (cons x ctx) b))] |
[(App f a) |
(define fr (reduce ctx f)) |
(define fa (reduce ctx a)) |
(match fr |
[(Lam x b) (reduce ctx (subst x fa b))] |
[else (App fr fa)])] |
[(Ann e t) (reduce ctx e)] |
[(Type) (Type)] |
[(Eq-refl x) (Eq-refl (reduce ctx x))] |
[(Eq-elim x P px y peq) |
(define peqr (reduce ctx peq)) |
(match peqr |
[(Eq-refl _) (reduce ctx px)] |
[else (Eq-elim (reduce ctx x) (reduce ctx P) (reduce ctx px) (reduce ctx y) peqr)])] |
[(Teq a b) (Teq (reduce ctx a) (reduce ctx b))])) |
;; Definitional equality |
(define (equiv? [ctx : Context] [e1 : Term] [e2 : Term]) (alpha-equiv? (reduce (map ctx-bind-name ctx) e1) (reduce (map ctx-bind-name ctx) e2))) |
;; This is the heart of the verifier. |
;; type-check and type-synth are mutually-recursive functions that |
;; check an expression has a type in a context, and |
;; synthesize the type of an expression in a context, respectively. |
;; They implement bidirectional type checking. |
;; type-check : Context Expr Expr -> boolean |
;; Produces true if expr has type t in context ctx (or error if not). |
(define (type-check [ctx : Context] [expr : Term] [type : Term]) : True |
(match expr |
[(Lam x b) |
(type-check ctx type (Type)) |
(define tyr (reduce (map ctx-bind-name ctx) type)) |
(match tyr |
[(Arrow x1 tt tw) |
(match (list x x1) |
[(list '_ '_) (type-check ctx b tw)] |
[(list '_ x1) |
(cond |
[(nor (side-cond? x1 (list b) false) (used-in-ctx? ctx x1)) |
(type-check (cons (list x1 tt) ctx) b tw)] |
[else |
(define newx (refresh-with-ctx ctx x1 (list b tyr))) |
(type-check (cons (list newx tt) ctx) b (subst x1 newx tw))])] |
[(list x '_) |
(cond |
[(nor (side-cond? x (list tyr) false) (used-in-ctx? ctx x)) |
(type-check (cons (list x tt) ctx) b tw)] |
[else |
(define newx (refresh-with-ctx ctx x (list b tyr))) |
(type-check (cons (list newx tt) ctx) (subst x newx b) tw)])] |
[(list x x1) |
(cond |
[(and (equal? x x1) (not (used-in-ctx? ctx x1))) (type-check (cons (list x tt) ctx) b tw)] |
[(nor (side-cond? x (list tyr) true) (used-in-ctx? ctx x)) |
(type-check (cons (list x tt) ctx) b (subst x1 x tw))] |
[else |
(define newx (refresh-with-ctx ctx x (list expr tyr))) |
(type-check (cons (list newx tt) ctx) (subst x newx b) (subst x1 newx tw))])])] |
[else (cannot-check ctx expr type)])] |
[else (if (equiv? ctx (type-synth ctx expr) type) true (cannot-check ctx expr type))])) |
;; cannot-check: Context Expr Type -> void |
;; Prints generic error message for type-check |
;; Error messages can be improved by replacing uses of this with something more specific |
(define (cannot-check [ctx : Context] [e : Term] [t : Term]) |
(error 'type-check "cannot typecheck ~a as ~a in context:\n~a" |
(pretty-print-expr e) (pretty-print-expr t) (pretty-print-context ctx))) |
;; type-synth : Context Expr -> Type |
;; Produces type of expr in context ctx (or error if can't) |
(define (type-synth [ctx : Context] [expr : Term]) : Term |
(match expr |
[(? symbol? x) |
(cond |
[(assoc x ctx) => second] |
[(assoc x deftypes) => second] |
[else (cannot-synth ctx expr)])] |
[(Lam x b) (cannot-synth ctx expr)] |
[(App f a) |
(define t1 (reduce (map ctx-bind-name ctx) (type-synth ctx f))) |
(match t1 |
[(Arrow '_ tt tw) #:when (type-check ctx a tt) tw] |
[(Arrow x tt tw) #:when (type-check ctx a tt) (subst x a tw)] |
[else (cannot-synth ctx expr)])] |
[(Ann e t) (type-check ctx t (Type)) (type-check ctx e t) t] |
[(Arrow '_ tt tw) |
(type-check ctx tt (Type)) |
(type-check ctx tw (Type)) |
(Type)] |
[(Arrow x tt tw) |
(type-check ctx tt (Type)) |
(type-check (cons `(,x ,tt) ctx) tw (Type)) |
(Type)] |
[(Type) (Type)] |
[(Teq e1 e2) |
(define t1 (type-synth ctx e1)) |
(type-check ctx e2 t1) |
(Type)] |
[(Eq-refl x) (type-synth ctx x) (Teq x x)] |
[(Eq-elim x P px y peq) |
(define A (type-synth ctx x)) |
(type-check ctx P (Arrow '_ A (Type))) |
(define Pann (Ann P (Arrow '_ A (Type)))) |
(type-check ctx px (App Pann x)) |
(type-check ctx y A) |
(type-check ctx peq (Teq x y)) |
(App Pann y)] |
[other |
(cannot-synth ctx expr)])) |
;; cannot-synth: Context Expr -> void |
;; Prints generic error message for type-synth |
;; Again, can improve things by being more specific at use sites |
(define (cannot-synth [ctx : Context] [expr : Term]) : Nothing |
(error 'type-synth "cannot infer type of ~a in context:\n~a" |
(pretty-print-expr expr) (pretty-print-context ctx))) |
;; Definitions |
;; Global variables |
(define defs : Context empty) |
(define deftypes : Context empty) |
;; def : symbol Expr -> void |
;; Side effect is to mutate global defs, deftypes to include new def if it checks out |
(define (def [name : Symbol] [expr : Sexp]) |
(when (assoc name defs) (error 'def "~a already defined" name)) |
(define e (parse-expr expr)) |
(define et (type-synth empty e)) |
(match e |
[(Ann ex tp) (set! defs (cons (list name ex) defs)) |
(set! deftypes (cons (list name tp) deftypes))] |
[else (set! defs (cons (list name e) defs)) |
(set! deftypes (cons (list name et) deftypes))])) |
(define (clear-defs) (set! defs empty) (set! deftypes empty)) |
(define (check-one [expr : Sexp]) |
(printf "~a\n" (pretty-print-expr (type-synth empty (parse-expr expr))))) |
(define (reduce-one [expr : Sexp]) |
(printf "~a\n" (pretty-print-expr (reduce empty (parse-expr expr))))) |
(require typed/rackunit) |
(check-true (type-check `((A ,(Type))) 'A (Type))) |
(check-equal? (parse-expr '(∀ (A : Type) -> (A -> A))) |
(Arrow 'A (Type) (Arrow '_ 'A 'A))) |
(check-equal? (parse-expr '(λ a => a)) |
(Lam 'a 'a)) |
;; should succeed: |
(check-one '((λ a => (λ b => b)) : (∀ (A : Type) -> (A -> A)))) |
(check-one '((λ a b c a2b b2c a => (b2c (a2b a))) : (∀ (a : Type) -> (∀ (b : Type) -> (∀ (c : Type) -> ((a -> b) -> (b -> c) -> (a -> c))))))) |