Strengthening in Logical Frameworks (Twelf)

Many logic systems admit a weakening principle: a derived conclusion continues to hold when more assumptions are added. Strengthening is the opposite: proving that the established conclusion holds given fewer assumptions. Strengthening certainly is not generally valid and has to be proven case-by-case. It seems strengthening is often needed when proving meta-theoretical properties such as totality of non-surjective endomorphisms of higher-order terms.

The article on Twelf Wiki <http://twelf.plparty.org/wiki/Strengthening> shows an example of the need for strengthening and describes one solution, which relies on defining term equality. In this article we demonstrate simpler examples requiring strengthening, and two surprisingly trivial solutions. Quite literally, one solution deals with the problem head-on, whereas the second one avoids the problem in a round-about way. The examples are inspired by staging calculi. At the end we describe the real development, which gives perhaps the first formal proof of type preservation of primitive reductions in an expressive staged calculus with escapes, run and cross-staged persistence. The type-preservation proof used both of our solutions and required two strengthening lemmas. Throughout we use Twelf – properly: we use the higher-order abstract syntax (HOAS) for higher-order terms, without ever resorting to concrete syntax for “variables”. The complete code is available at <http://okmij.org/ftp/Computation/staging/> .

The simplest example requiring strengthening

Perhaps the simplest example to illustrate the need of strengthening is based on lambda-calculus with terms marked by either 1 or 0. The challenge is to implement a function that converts terms marked by 1 into those marked by 0 – and show its totality.

Our calculus is elementary: we define two marks, 0 and 1

mark: type. 0: mark. 1: mark.

and marked terms, built of applications and abstractions:

exp: mark -> type. %name exp E. a: exp M -> exp M -> exp M. l: (exp M -> exp M) -> exp M.

Here is a sample term, marked with 1 :

t1 : exp 1 = a (l [x] (l [y] (a x y))) (l [x] x).

(in Haskell notation, it is (\x -> \y -> x y) (\x -> x) ). The definition of exp makes it clear that all subterms of a marked term have the same mark.

Our goal is to write a transformation taking a term marked by 1 and producing a term marked by 0 :

cp1: exp 1 -> exp 0 -> type. %mode cp1 +E1 -E2.

It is almost like copying terms and hence straightforward:

cp1-a: cp1 (a E1 E2) (a E1' E2') <- cp1 E2 E2' <- cp1 E1 E1'. cp1-l: cp1 (l E1) (l E1') <- ({x}{x'} cp1 x x' -> cp1 (E1 x) (E1' x')). %block cp1-l-bl : block {x:exp 1} {x':exp 0} {D:cp1 x x'}. %worlds (cp1-l-bl) (cp1 _ _). %total {E1} (cp1 E1 _). %% Does not pass!

The clause cp1-a inductively handles applications. To transform an abstraction, we assume a term x marked with 1, a term x' marked with 0, and assume that x' is the result of transforming x , see cp1 x x' . Under those assumptions, we transform the body of the abstraction, cp1 (E1 x) (E1' x') . The notation (E1' x') means that E1' is a higher-order term applied to x' . That is, E1' is of the type exp 0 -> exp 0 . The block declaration cp1-l-bl lists our set of assumptions; the regular worlds declaration asserts that cp1 is performed given no assumptions, or one set assumption cp1-l-bl , or two sets of such assumptions (with the different x and x' across the sets), etc.

Our transformation procedure is obviously total: we can take any 1-marked term, and, in finite time, always deliver the corresponding 0-marked term. We wish to state this formally, hence the %total assertion. Twelf verifies it first by checking that the transformation is terminating. It then checks that all pattern-matching is exhaustive: we have not missed a case. Alas, the totality check fails and Twelf reports an error:

Totality: Output of subgoal not covered Output coverage error --- missing cases: {E1:exp 1 -> exp 1} {E2:exp 1 -> exp 0 -> exp 0} |- {x:exp 1} {x':exp 0} cp1 x x' -> cp1 (E1 x) (E2 x x').

In the clause cp1-l , we have introduced three assumptions: x , x' and cp1 x x' . By writing (E1' x') we asked the Twelf system to “factor out” the dependence on x' . Implicitly we stated that the result of transforming (E1 x) depends only on x' but not on x or cp1 x x' (more formally, the independence is seen from the fact E1' is used outside of the scope of x introduced via the universal quantification {x} in cp1-l ). Twelf can see that the assumption cp1 x x' cannot possibly enter the set of terms because the definition of the type family exp (with constructors a and l ) shows that no term can contain anything of the type cp1 . But terms may occur in terms, and so the result of transforming the body of the abstraction generally depends on both x and x' . We have not handled that general case, and so our transformation is not total.

Twelf’s complaint is quite insightful: (E1 x) is the body of the abstraction that includes some term x (“the variable”). We transform the body replacing all those occurrences of x with x' – and so the result should include only x' but no x . Thus Twelf demands the proof that we indeed replaced all those x with x' .

Upon consideration, we can see that the result of transforming (E1 x) cannot really depend on, or contain, x . After all, x is an expression marked by 1 and the result of transforming (E1 x) is an expression marked by 0. Any sub-expression of a marked expression is marked with the same mark. Alas, Twelf cannot see this proof by itself and we have to write it down explicitly. The reason Twelf cannot see that proof by itself is because Twelf tracks the dependencies (so-called subordination) using only the names of the type families (e.g., exp ) but not their parameters (e.g., exp 0 or exp 1 ).

Astonishingly, the most trivial way of writing this independence proof works. We state that whenever a term marked by 0 appears to depend on a term marked by 1, it actually doesn’t. Or, constructively, we assert the following proposition

strength : (exp 1 -> exp 0) -> exp 0 -> type. %mode strength +H -E.

For any implication exp 1 -> exp 0 , we can always constructively produce exp 0 . Here is the proof, the detailed procedure of producing that exp 0 term:

-: strength ([x] E) E. -: strength ([x] (a (E1 x) (E2 x))) (a E1' E2') <- strength E1 E1' <- strength E2 E2'. -: strength ([x] (l [x'] (E x' x))) (l E') <- {x':exp 0} strength (E x') (E' x').

The first clause handles the case when E patently does not contain anything of the type exp 1 . The rest of the clauses do straightforward induction on terms; the last clause requires hypothetical reasoning to examine the body of the abstraction, and for that we introduce the assumption x' of the type exp 0 . Perhaps it is because of that hypothetical reasoning that Twelf cannot see the strengthening property on its own. We assert that so defined procedure is total, i.e., it is a constructive proof of the strength proposition:

%block strength-bl : block {x':exp 0}. %worlds (strength-bl | cp2-l-bl) (strength _ _). %covers strength +H -E. %total {E} (strength E _).

The label cp2-l-bl describes the set of assumptions of the improved cp2 procedure below (in the actual code, it is defined in a forward declaration). Twelf has verified the totality of strength, and so we can use it to write the improved transformation procedure, cp2 :

cp2: exp 1 -> exp 0 -> type. %mode cp2 +E1 -E2. cp2-a: cp2 (a E1 E2) (a E1' E2') <- cp2 E2 E2' <- cp2 E1 E1'. cp2-l: cp2 (l E1) E1'' <- ({x}{x'} cp2 x x' -> cp2 (E1 x) (E1' x x')) <- strength ([x] (l (E1' x))) E1''. %block cp2-l-bl : block {x:exp 1} {x':exp 0} {d:cp2 x x'}. %worlds (cp2-l-bl) (cp2 _ _). %total {E1} (cp2 E1 _).

The only difference from cp1 is in the clause cp2-l : by writing (E1' x x') we state that the result of transforming the body of the abstraction may depend not only on x' but also on x . We build the transformed abstraction under that additional assumption: ([x:exp 1] (l (E1' x))) . Using strengthening, we then prove that (l (E1' x)) does not actually depend on x . Now the totality assertion succeeds: cp2 is the total transformation of 1-marked terms.

The complete code is available at <http://okmij.org/ftp/Computation/staging/strengthen.elf>

A more realistic example: type preservation

We have encountered the problem of strengthening in staged calculi, e.g., when proving type preservation. We outline the problem and the solution; please see the complete code <http://okmij.org/ftp/Computation/staging/translation-problem.elf> for the full details.

We introduce a simple staged language, where each expression is annotated with the numeric stage label A . The language has three forms of expressions: natural literals, addition, and fix (i.e., recursive functions).

exp : nat -> type. %name exp E. n : nat -> exp A. %prefix 110 n. + : exp A -> exp A -> exp A. %infix left 154 +. fix : (exp A -> exp A) -> exp A.

We introduce a trivial type system with only one type, int , and the equally simple type-checking relation of E T :

of : exp A -> tp -> type. %name of Q. %mode of +E *T.

Please see the code for details. As in the previous section we write the procedure to downgrade the level of an expression

downgrade : exp (1 N) -> exp N -> type. %mode downgrade +E1 -E2.

We are not that interested in proving downgrade is total. Rather, we wish to prove a meta-theorem that downgrade is type preserving: if the original expression was typed, the downgraded expression shall be typed too. We state that property as a constructive proposition:

dn-pres : downgrade E1 E2 -> of E1 T -> of E2 T -> type. %mode dn-pres +PD +PTI -PTO.

That is, given any downgradeable expression E1 and its type derivation of E1 T (the evidence that E1 is typed), we produce the typing derivation for the corresponding downgraded expression E2 . The most interesting case is the following:

- : dn-pres (dn-f PEH) (of-f PTH) PTH'' <- ({e:exp (1 A)} {pt:of e int} {e':exp A} {dn:downgrade e e'} {pt':of e' int} dn-pres dn pt pt' -> dn-pres (PEH e e' dn) (PTH e pt) (PTH' e pt e' pt')) <- strength-of ([e] [pt] (of-f (PTH' e pt))) PTH''.

Here, dn-f PEH of the type downgrade (fix E1) (fix E2) is the derivation of downgrading (fix E1) into (fix E2) . We assume a term e of the type exp (1 A) , and further assume it is typed (the assumption pt:of e int ). Likewise we assume a typed term e' that is the result of downgrading e . Finally, we assume that the downgrading from e to e' was type preserving. Under those assumptions, we obtain the type derivation of the body of the downgraded fix, by inductively invoking dn-pres. The resulting type derivation (PTH' e pt e' pt') generally depends not only on the assumption e' and its typeability pt' – but also on e and pt , which are in scope and could in principle occur in type derivations. We must thus prove that we can remove the latter dependency. Again, the formal statement of that proposition is surprisingly simple:

strength-of : ({e:exp (1 N)} of e T -> of (E:exp N) T) -> of (E:exp N) T -> type. %mode strength-of +H -P.

We assert that the typing derivation of E T for an expression E at the level N cannot depend on any N+1 -level expression e and the typing derivation of the latter. Our proof is again constructive: from the corresponding implication we produce its conclusion without assuming the hypotheses. The complete code has all details.

Other examples

The need for strengthening does occur (although not frequently) in other circumstances. Here are the example from Twelf distribution. One is “Representation of Mini-ML expressions in Mini-ML*” by David Swasey, file exercises/opt-eval/opt-rep1.elf . It defines a transformation from one higher-order language to another. Proving meta-theoretical properties of that translation (totality, type preservation, etc) requires strengthening.

The second, indirect solution

The second solution to strengthening is, too, trivial, albeit round-about. We use the same problem as in Section 1: in lambda-calculus whose terms are marked with 0 or 1, implement a function that converts terms marked by 1 into those marked by 0 – and show its totality.

Rather than transforming exp 1 to exp 0 directly, we introduce terms expi M isomorphic to exp M , turn exp 1 to expi 1 and then transform expi 1 to exp 0 . The advantage is that terms exp M cannot appear in expi M and vice versa, so exp M cannot depend on assumptions of the type {x:expi M} and vice versa. We avoid strengthening altogether.

Here are a few details: we define intermediary, also marked, terms

expi: mark -> type. %name expi _Ei. a2: expi M -> expi M -> expi M. l2: (expi M -> expi M) -> expi M.

that are clearly isomorphic to the terms exp M of the original calculus. We can state the isomorphism formally; however, we only need one direction of it: given any term exp M , we can always obtain the corresponding expi M :

exp-expi: exp M -> expi M -> type. %mode exp-expi +E1 -E2.

We show only the more complex case of transforming abstractions:

-: exp-expi (l E1) (l2 E1') <- {e:exp M} {e2:expi M} exp-expi e e2 -> exp-expi (E1 e) (E1' e2). %block bl-exp-expi : some {M:mark} block {e:exp M} {e2:expi M} {t:exp-expi e e2}. %worlds (bl-exp-expi) (exp-expi _ _). %total {E} (exp-expi E _).

Now Twelf verifies the totality assertion without any complaints that E1' could depend (that is, include) the assumption e , which is also present in the LF context. Twelf now sees that E1' cannot include e . Compared to Section 1, converting an expression of the type exp M gives us the term of the type expi M . The latter terms can only be constructed by l2 and a2 ; neither constructor can take any argument of the type exp M or contain exp M elsewhere. Strengthening is no longer needed.

The label-changing transformation now takes expi 1 rather than exp 1 as the source a term:

cpaux: expi 1 -> exp 0 -> type. %mode cpaux +E1 -E2.

Here is the interesting case

cpa-l: cpaux (l2 E1) (l E1') <- ({x:expi 1}{x':exp 0} cpaux x x' -> cpaux (E1 x) (E1' x')). %block bl-cpaux : block {x:expi 1} {x':exp 0} {t:cpaux x x'}. %worlds (bl-cpaux) (cpaux _ _). %total {E1} (cpaux E1 _).

Again, Twelf accepts the totality assertion without complaint because clearly no term of the type expi M may appear in the term exp M – in fact, expi was not even defined when exp was introduced. Thus (E1' x') cannot depend on x .

What remains is a mere composition of the two above transformations

cpi: exp 1 -> exp 0 -> type. %mode cpi +E1 -E2. -: cpi E1 E2 <- exp-expi E1 E1' <- cpaux E1' E2. %worlds () (cpi _ _). %total {} (cpi _ _).

The desired transformation now includes two steps: transforming the source expression into the isomorphic form, and changing the mark of the latter. Albeit indirect, the solution avoids strengthening altogether.

A real example: type-preservation in an expressive staged calculus

The file <http://okmij.org/ftp/Computation/staging/lambda-am1.elf> is the formalization of the two-level call-by-value calculus lambda-a1v based on lambda-a of Taha and Nielsen’s “Environment Classifiers” (POPL03). The calculus supports manipulation and splicing in of the open code, running of the closed code, and cross-staged persistence. The lambda-a1v calculus has been introduced in the paper Closing the Stage: From Staged Code to Typed Closures written with Yukiyoshi Kameyama and Chung-chieh Shan (PEPM08).

The file includes (maybe the first) formal proof that any non-value term can be decomposed into a pre-value and the context. This is quite challenging given that the calculus supports the evaluation under lambda and so the context can cross the arbitrary number of dependent binders: variable binders include classifiers that must be bound, too, at that point.

In our staged calculus, all terms are marked by their stage, present and future. This is quite like the examples in the earlier Sections. There is also an operation to demote a term, which takes a future-stage-marked term and changes its mark so the term can be evaluated at the present stage. Demotion is how a future-stage computation can be run. Demotion rebuilds a term as it traverses it, changing the marks along the way. The only special case is handling a cross-staged persistent (CSP) value. The latter is a present-stage expression that was “lifted” to the future stage, i.e., was encapsulated unevaluated. The demotion operation merely removes the encapsulation, splicing CSP into the demoted code.

The type-preservation proof is at the end of the development. The most complex part is proving that demotion is type preserving, in particular, that demoting an abstraction (l [x:exp (1q AL)] Body) from the future stage (marked by (1q AL) ) to the present, marked as qt , stage yielding (l [x':exp qt] Body') is type preserving. As in the simpler examples earlier, the proof proceeds by introducing assumptions {x:exp (1q AL)} , {x':exp qt} and that x' is the result of demoting x . The challenge is to prove that Body' does not actually depend on x . That is no longer as simple as before: a future-stage expression exp (1q AL) could actually occur in the demoted expression – because of CSP. For example, consider the following future-stage term

(l [x:exp (1q AL)] (csp (AL > x)))

where AL > x is future-stage- x marked by classifier AL (in the PEPM08 paper, we write (l [x^AL] %(<x>^AL)) . An attempt to run such future-stage computations does not type in MetaOCaml; accordingly, the lambda-a1v calculus rejects such expressions as mis-typed, due to a special typing rule of CSP. Our preservation proof must take into account that the demoted expression is typed and so possible CSPs are restricted in form. Only then strengthening can be proven.

In our proof, we had to use both approaches to proving strengthening described in this article. We had to prove a direct strengthening lemma

strength-tp-dem : csp-ok1 E -> ({x:exp (1q AL)} of x TV -> of E T) -> of E T -> type. %mode strength-tp-dem +CS +H -P.