5 ntac: The New Tactic System
As Coq has shown, tactics have proven useful for doing complex proofs. In Cur, tactics are not built-in or provided by the language. However, any user can use meta-programming to add tactics to Cur. In fact, a user did. Cur originally shipped with a proof-of-concept tactic system, but the system did not scale well. So Jay designed and prototyped a new one over a weekend in 200 lines of code. Now it’s the default system.
5.1 The ntac system
(require cur/ntac/base) | package: cur-lib |
A tactic is used at the top-level of a proof script. A tactic is a Racket function that satisfies the contact (r:-> nttz? nttz?) (where r:-> is the Racket function contract normally written ->; we use r:-> since Cur redefined ->). Tactics easily compose, may navigate the proof tree, resolve multiple holes, and be called recursively.
A tactical is used to manipulate the focus of proof tree, such as to resolve a single hole. A tactical satisfies the contract (r:-> dict? ntt?) We will conflate tacticals with functions that produce tacticals from addition arguments. I.e. we also call functions that satisfy (r:-> syntax? ... (r:-> dict? ntt?)) tacticals. Tacticals receive additional arguments as Cur syntax; for technical reasons these need to be processed by ntac-syntax before being used in a tactical. Tacticals are usually used by the fill tactic, which simply applies a tactical to the current focus of the proof tree zipper. We usually provide macros for the surface syntax of tacticals so that users need not deal with syntax objects directly, or explicitly use the fill tactic. These macros follow the naming convention by-_tactical.
5.1.1 Usage
syntax
(ntac type tactic ...)
> ((ntac (forall (x : Nat) Nat) by-intro by-assumption) z) (z)
> ((ntac (forall (x : Nat) Nat) interactive) z) (z)
syntax
(define-theorem name ty ps ...)
> (define-theorem nat-id (forall (x : Nat) Nat) by-intro by-assumption)
5.1.2 Base Tactics
These tactics are used in the tactic system API, while other other tactics are defined outside the base system.
5.1.3 Proof Trees
TODO: The internal details of the tactic system should be de-emphasized, and the tactics that programmers actually want to use should be more prominent. These next few subsections should probably be moved to the end of the documentation.
The ntac proof tree datatype ntt represents a Cur term with holes. Specifically, the proof tree contains nodes for a hole, an exact term, a combination of subterms, and a context manipulation.
struct
(struct ntt-hole ntt (contains-hole? goal) #:transparent) contains-hole? : boolean? goal : syntax?
procedure
goal : syntax?
struct
(struct ntt-exact ntt (contains-hole? goal term) #:transparent) contains-hole? : boolean? goal : syntax? term : syntax?
struct
(struct ntt-context ntt ( contains-hole? goal env-transformer subtree) #:transparent) contains-hole? : boolean? goal : syntax? env-transformer : (r:-> dict? dict?) subtree : ntt?
struct
(struct ntt-apply ntt (contains-hole? goal subtrees f) #:transparent) contains-hole? : boolean? goal : syntax? subtrees : (listof ntt?) f : (r:-> syntax? ... syntax?)
struct
(struct ntt-done ntt (contains-hole? goal subtree) #:transparent) contains-hole? : boolean? goal : syntax? subtree : ntt?
5.1.4 Proof Tree Zipper
To navigate the proof tree, we define the ntac tree zipper.
procedure
(nttz-done? nttz) → boolean?
nttz : nttz?
5.1.5 Tactic System API
procedure
goal : syntax?
procedure
ptz : nttz? pstep : syntax?
5.1.6 theorem-info
struct
(struct theorem-info identifier-info (name orig))
name : identifier? orig : syntax?
5.2 Standard Tactics and Tacticals
(require cur/ntac/standard) | package: cur-lib |
struct
(struct exn:fail:ntac exn:fail ())
struct
procedure
(raise-ntac-goal-exception msgf arg ...) → any
msgf : string? arg : string?
syntax
(ntac-match goal [pattern branch] ...)
value
nop : tactic?
value
display-focus : tactic?
> ((ntac (forall (x : Nat) Nat) display-focus by-intro display-focus by-assumption) z)
--------------------------------
(Π (x : Nat) Nat)
x : Nat
--------------------------------
Nat
(z)
procedure
(try t) → tactic?
t : tactic?
> ((ntac (forall (x : Nat) Nat) by-assumption) z) could not find matching assumption for goal #<syntax:/home/r
oot/user/.local/share/racket/8.16.0.1/pkgs/cur-lib/cur/curne
l/coc.rkt:61:39 (#%app Π-- (#%plain-app Nat) (lambda (x)
(#%plain-app Nat)))>
> ((ntac (forall (x : Nat) Nat) (try by-assumption)) z) eval:6:0: qed: Proof incomplete.
in: ((try by-assumption))
procedure
(fill t) → tactic?
t : tactical?
procedure
(intro [name]) → tactical?
name : identifier? = #f
> ((ntac (forall (x : Nat) Nat) (fill (intro)) by-assumption) z) (z)
> ((ntac (forall (x : Nat) Nat) (fill (intro (ntac-syntax #'x))) by-assumption) z) ntac-syntax: undefined;
cannot reference an identifier before its definition
in module: 'anonymous-module
syntax
(by-intros id ...)
value
assumption : tactical?
syntax
value
obvious : tactical?
value
by-obvious : tactic?
> ((ntac (forall (x : Nat) Nat) by-obvious) z) (z)
syntax
(by-assert name thm)
> ((ntac (∀ [x : Nat] [y : Nat] (-> (== Nat x y) (== Nat y x))) (by-intros x y x=y) ; define local thm named y=x (by-assert y=x (== Nat y x)) ; prove y=x display-focus (by-rewriteR x=y) display-focus reflexivity ; prove original goal using y=x display-focus (by-assumption y=x)) 1 1 (refl Nat 1)) y: unbound identifier in module
procedure
name : identifier? thm : syntax?
value
simpl : tactic?
> (define-theorem plus_1_l (∀ [n : Nat] (== Nat (plus 1 n) (s n))) by-intro display-focus simpl display-focus reflexivity)
n : Nat
--------------------------------
(== Nat (s n) (s n))
n : Nat
--------------------------------
(== Nat (s n) (s n))
syntax
(by-destruct name)
(by-destruct name #:as param-namess)
> (define-theorem plus-1-neq-0 (∀ [n : Nat] (== Bool (nat-equal? (plus 1 n) 0) false)) (by-intro n) display-focus (by-destruct n #:as [() (n-1)]) ; zero case display-focus simpl display-focus reflexivity ; succ case display-focus simpl display-focus reflexivity)
n : Nat
--------------------------------
(== Bool false false)
--------------------------------
(== Bool false false)
--------------------------------
(== Bool false false)
n-1 : Nat
--------------------------------
(== Bool false false)
n-1 : Nat
--------------------------------
(== Bool false false)
> (plus-1-neq-0 0) (refl (Bool) (false))
> (plus-1-neq-0 1) (refl (Bool) (false))
procedure
(destruct name [param-namess]) → tactical?
name : identifier? param-namess : syntax? = #f
The resulting proof term uses match.
param-namess should be a list of list of identifiers, which are used as the binders in each clause. If not specified, the original binder names from the data declaration are used. Does not include induction hypotheses for recursive arguments.
syntax
(by-induction name #:as param-namess)
> (define-theorem plus-n-0 (∀ [n : Nat] (== Nat n (plus n z))) (by-intro n) simpl (by-induction n #:as [() (n-1 IH)]) ; zero case display-focus reflexivity ; succ case display-focus simpl display-focus (by-rewriteL IH) display-focus reflexivity)
--------------------------------
(== Nat z z)
n-1 : Nat
IH : (== Nat n-1 (plus n-1 z))
--------------------------------
(== Nat (s n-1) (s (plus n-1 z)))
n-1 : Nat
IH : (== Nat n-1 (plus n-1 z))
--------------------------------
(== Nat (s n-1) (s (plus n-1 z)))
n-1 : Nat
IH : (== Nat n-1 (plus n-1 z))
--------------------------------
(== Nat (s n-1) (s n-1))
procedure
(induction name param-namess) → tactical?
name : identifier? param-namess : syntax?
Unlike destruct, induction binds an induction hypothesis for recursive arguments in each case.
The resulting proof term uses new-elim.
param-namess should be a list of list of identifiers, which are used as the binders in each clause. If not specified, the original binder names from the data declaration are used.
5.2.1 Interactive Tactic
In Cur, interactivity is just a user-defined tactic.
value
interactive : tactic?
> ((ntac (forall (x : Nat) Nat) interactive) z) (z)
5.3 Auto-related Tactics
(require cur/ntac/auto) | package: cur-lib |
value
auto : tactic?
We first try by-obvious for trivial goals. If this fails, we then introduce each variable to context and exhaustively attempt pairings of preset tactics and identifiers.
The tactic returns when either the goal has been completed or the maximum number of tactic applications specified by current-max-auto-depth has been reached.
value
value
Uses a guard to ensure that values are ordered and unique. The ordering of the hints determines the order of tactic application.
value
display-hints : tactic?
syntax
(define-theorem #:hints (hint ...) name ty ps ...)
Optionally allows for specification of a hints database scoped to the theorem.
> (define-theorem pred-example-1 (∀ [p : Bool] (== (and p p) p)) (by-intros p) (by-destruct p) reflexivity reflexivity) p: unbound identifier in module
> (define-theorem pred-example-2 #:hints (pred-example-1) (∀ [p : Bool] [q : Bool] (-> (== p q) (== p true) (== (and p q) true))) display-hints auto) (pred-example-1)
syntax
(hints hint ...)
> (define-theorem pred-example-1 (∀ [p : Bool] (== (and p p) p)) (by-intros p) (by-destruct p) reflexivity reflexivity) p: unbound identifier in module
> (define-theorem pred-example-2 #:hints (pred-example-1) (∀ [p : Bool] [q : Bool] (-> (== p q) (== p true) (== (and p q) true))) display-hints (hints (pred-example-2) display-hints) display-hints auto)
(pred-example-1)
(pred-example-2 pred-example-1)
(pred-example-1)
struct
(struct tactic-preset (proc id no-name-arg?) #:transparent) proc : tactic? id : syntax? no-name-arg? : boolean?
value
The ordering of the presets determines execution order, so it is recommended to arrange base cases first (e.g. by-assumption).
5.4 Rewrite-related Tactics
(require cur/ntac/rewrite) | package: cur-lib |
Ntac includes two libraries of rewrite tactics: one (the above) for "standard" Paulin-Mohring equality (ie, ==), and another (cur/ntac/ML-rewrite) for Martin-Lof equality (i.e., ML-=).
Each library provides versions of the following bindings.
value
reflexivity : tactic?
syntax
(by-rewrite name . es)
Rewrites from right-to-left (equivalent to Coq’s ->), i.e., for a theorem (== A x y), x is replaced with y.
Short hand for (fill (rewrite #'name #:es #'es)).
Equivalent to by-rewriteR.
> (define-theorem identity-fn-applied-twice (∀ [f : (-> Bool Bool)] (-> (∀ [x : Bool] (== Bool (f x) x)) (∀ [b : Bool] (== Bool (f (f b)) b)))) (by-intros f H b) display-focus (by-rewrite H b) display-focus (by-rewrite H b) display-focus reflexivity)
f : (→ Bool Bool)
H : (Π (x : Bool) (== Bool (f x) x))
b : Bool
--------------------------------
(== Bool (f (f b)) b)
b: unbound identifier in module
syntax
(by-rewriteR name . es)
Rewrites from right-to-left (equivalent to Coq’s ->), i.e., for a theorem (== A x y), x is replaced with y.
Short hand for (fill (rewrite #'name #:es #'es)).
Equivalent to by-rewrite.
syntax
(by-rewriteL name . es)
Short hand for (fill (rewrite #'name #:es #'es #:left? #t)).
Rewrites from left-to-right (equivalent to Coq’s <-), i.e., for a theorem (== A x y), y is replaced with x.
procedure
(rewrite name [ #:left? left? #:es es #:thm-name thm-name #:thm thm]) → tactical? name : identifier? left? : boolean? = #f es : syntax? = #'() thm-name : identifier? = #f thm : syntax? = #f
By default, rewrites from right-to-left (equivalent to Coq’s ->), i.e., for a theorem (== A x y), x is replaced with y. If left? is #t, rewrites from left-to-right instead (Coq’s <-).
syntax
(by-replace ty from to)
Replaces instances of from in the goal, which has type ty, with to. Adds ty as a subgoal.
> (define-theorem plus-n-Sm (∀ [n : Nat] [m : Nat] (== Nat (s (plus n m)) (plus n (s m)))) (by-intro n) (by-intro m) simpl (by-induction n #:as [() (n-1 IH)]) simpl reflexivity simpl (by-rewrite IH) reflexivity)
> (define-theorem plus_comm (∀ [n : Nat] [m : Nat] (== Nat (plus n m) (plus m n))) (by-intro n) (by-intro m) simpl (by-induction n #:as [() (n-1 IH)]) simpl display-focus (by-rewriteL plus-n-0 m) display-focus reflexivity simpl (by-rewriteL plus-n-Sm m n-1) (by-rewrite IH) reflexivity)
m : Nat
--------------------------------
(== Nat m (plus m z))
m : Nat
--------------------------------
(== Nat m m)
> (define-theorem plus-assoc (∀ [n : Nat] [m : Nat] [p : Nat] (== Nat (plus n (plus m p)) (plus (plus n m) p))) (by-intros n m p) simpl (by-induction n #:as [() (n-1 IH)]) ; zero case reflexivity ; succ case simpl (by-rewrite IH) reflexivity) n: unbound identifier in module
> (define-theorem plus-swap (∀ [n : Nat] [m : Nat] [p : Nat] (== Nat (plus n (plus m p)) (plus m (plus n p)))) (by-intros n m p) (by-rewrite plus-assoc n m p) display-focus (by-replace Nat (plus n m) (plus m n)) display-focus (by-rewriteL plus-assoc m n p) reflexivity ; proof of by-replace theorem display-focus (by-rewrite plus_comm m n) display-focus reflexivity) plus-assoc: unbound identifier in module