HOL icon indicating copy to clipboard operation
HOL copied to clipboard

Extend Definition template for nested recursion

Open konrad-slind opened this issue 8 months ago • 3 comments

Problem

The current

Definition <name> <attributes>:
  <eqns>
Termination
  <tactic>
End

template doesn't support nested recursive definitions properly. It does support some "syntactically" nested definitions where the termination argument doesn't have to develop facts about the nested calls, but that's only part of the problem and we'll ignore them here.

Currently a definition of a nested recursion has 5 parts

  • Initial definition
  • Instantiation of auxiliaries
  • Proof of "semantic" lemma(s)
  • Termination proof
  • Storing results

We'd like to collapse these as much as possible into the nice Definition ... End presentation. In the following we go through an example and then discuss some ideas.

Example of how things work now.

Initial definition. Consider the following list reversal algorithm.

val rev_defn = Hol_defn
  "rev"
  ‘rev [] = [] /\
   rev (x::xs) =
      case rev xs
       of [] => [x]
       | y::ys => y :: rev (x::rev ys)’

Hol_defn defines rev, extracts termination conditions, and builds the recursion equations and induction theorem for rev. The latter two facts are constrained by synthesized termination conditions, which are parametric in the as-yet-not-specified termination relation R. This information is packaged up into rev_defn.

Instantiation of auxiliaries.

We now choose a termination relation for R, instantiate the constrained recursion equations and induction theorem, and simplify. This has been packaged up into a call to Defn.instantiate_aux:

val [rev_ind, rev_nil, rev_cons] =
  Defn.instantiate_aux rev_defn
        “measure LENGTH”
        (SIMP_RULE arith_ss [] o SRULE[]);

Overload revFn[local] = “rev_aux (measure LENGTH)”

We use overloading to make the formulas a bit more readable.

Proof of "semantic" lemma(s)

A key part of most proofs of termination for nested functions is the use of so-called "semantic lemmas" in the termination proof. Such a lemma is a fact about the behaviour of the defined function. Somewhat mind-bendingly, we are proving a fact about the function before finishing its termination proof, which is ordinarily the point after which the equations and induction can be validly used. However, the attached applicability conditions on the (instantiated, simplified) auxiliary definitions means this is OK. In this case, the semantic lemma asserts that invocation of revFn doesn't change the length of its input.

Theorem revFn_length_eq[local]:
  ∀L. LENGTH (revFn L) = LENGTH L
Proof
  recInduct rev_ind >> rw[]
  >- rw[rev_nil]
  >- (mp_tac rev_cons >> CASE_TAC >> gvs[])
QED

Termination proof

The termination proof is set up interactively with Defn.tgoal rev_defn and usually initiated with a call to WF_REL_TAC <reln>. Once the interactive proof is finished, it has to be packaged up with Defn.tprove:

val (def, ind) = Defn.tprove
(rev_defn,
  WF_REL_TAC ‘measure LENGTH’ >>
  rw[GSYM measure_length] >>
  rule_assum_tac $ Q.AP_TERM ‘LENGTH’ >>
  gvs [revFn_length_eq])

Storing results

Since Defn.tprove doesn't store its results in the current segment, we finish with the following simple calls:

Theorem rev_def = def;
Theorem rev_ind = ind;

Discussion.

The introductory definition part and the termination part have to be separated so that the semantic lemmas can be proved. Something like

Definition rev_def [nested]:
  rev [] = [] /\
  rev (x::xs) =
    case rev xs
     of [] => [x]
      | y::ys => y :: rev (x::rev ys)
End

could result in rev_defn being created and bound in the SML top-level. Then, for now, the user would invoke the instantiation of auxiliaries, declare the Overload and prove the semantic lemmas.

[It's possible that the instantiation of auxiliaries could be done by the system instead of the user, but that could be addressed in a later revision.]

This would also set up a goalstack with Defn.tgoal rev_defn and tell the user about it. The user would interactively prove termination. Then something like

Termination [rev]
  WF_REL_TAC ‘measure LENGTH’ >>
  rw[GSYM measure_length] >>
  rule_assum_tac $ Q.AP_TERM ‘LENGTH’ >>
  gvs [revFn_length_eq]
End

would run the Defn.tprove(rev,defn, <tactic>) and bind the results to rev_def and rev_ind.

konrad-slind avatar Apr 12 '25 20:04 konrad-slind

Actually, the "detached Termination block" idea could be useful for other definitions too. For example, if a function needed to define a bespoke "weighting" function (aka polynomial interpretation) to be used as the measure, there could be intermediate proofs that need to be done before the termination goal can be proved. In that case, doing a

Definition <name>:
  <equations>
End

 .... definition of weighting function, proofs about it if necessary ...

Termination <name> :
....
End

would provide a neat way to "bracket" the entirely local weighting stuff, even though it could be moved to come before the initial definition.

konrad-slind avatar Apr 14 '25 06:04 konrad-slind

Yes! Perhaps the magic attribute for this situation should be deferred rather than nested.

mn200 avatar Apr 14 '25 07:04 mn200

We could also contemplate nice front-end syntax for new_specification and new_type_definition. Both of these have witness theorems that have to be proved in order for the new specification or type definition to happen. So

Definition <name>[spec(s1,...,sn)]:
  P[v1,...,vn]
Witness
  <proof>
End

supplies a formula P with n free variables and the witness proof will be to show the existence of suitable t1,...,tn making P[t1,...,tn] true. The resulting definition gets made once the proof is successful. NB: when the Definition ...Witness ... End form is repeated (say when the enclosing file gets replayed to the SML top-level), the introduced constants need to be hidden. Hence the inclusion of the names s1,...,sn in the attribute. Maybe something nicer can be found.

Also

Definition <name>[type]:
  P  (* closed *)
Witness
  <proof>
End

can encapsulate the introduction of a new type operator, going up to and including an invocation of new_type_bijections.

For both forms, a first step is to add a new category for it to the DefnBase and to define the analogues of tgoal and tprove for it.

konrad-slind avatar Apr 16 '25 22:04 konrad-slind