8.18
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))))))) |