In this section, we formally describe our inverse computation. As briefly explained in Sect. 2, first, we convert an MTT program into a non-accumulative context-generating program (a program that generates contexts instead of trees) without multiple data traversals, such as ev
c in Sect. 2.3. Then, we perform inverse computation with memoization. More precisely, we construct a tree automaton [10] that represents the inverse computation result, whose run implicitly corresponds to (a context-aware version of) the existing inverse computation process with memoization [1, 4].
Our inverse computation consists of three steps:
-
1.
We convert a parameter-linear MTT into a non-accumulative context-generating program.
-
2.
We apply tupling [8, 25] to eliminate multiple data traversals.
-
3.
We construct a tree automaton that represents the inverse computation result.
The first two steps are to obtain a non-accumulative context-generating program without multiple data traversals. The third step represents memoized inverse computation. The rest of this section explains each step in detail.
Conversion to context-generating program
The first and most important step is to convert an MTT program into a non-accumulative context-generating program. This transformation is also useful for removing certain multiple data traversals, as shown in the example of ev in Sect. 2. Moreover, this makes it easy to apply tupling [8, 25] to programs. Note that viewing MTT programs as non-accumulative context-generating transformations is not a new idea (see Sect. 3.1 of [12] for example). The semantics of the context-generating programs shown later is nothing but using Lemma 3.4 of [12] to evaluate MTT programs.
First, we will give a formal definition of contexts.
Definition 2
An (m-hole) context
K is a tree in
where •1,…,•
m
are nullary symbols such that \({\bullet }_{1},\ldots,{\bullet }_{m} \not\in \varSigma \).
An m-hole context K is linear if each •
i
(1≤i≤m) occurs exactly once in K. We write K[t
1,…,t
m
] for the tree obtained by replacing •
i
with t
i
for each 1≤i≤m. For example, K=Cons(•1,•2) is a 2-hole context and K[Z,Nil] is the tree Cons(Z,Nil). For 1-hole contexts, •1 is sometimes written as •.
We showed that ev is indeed a non-accumulative context-generating transformation in Sect. 2. In general, any MTT program can be regarded as a non-accumulative context-generating transformation in the sense that, since output variables cannot be pattern-matched, the values bound to the output variables appear as-is in the computation result. Formally, we can state the following fact (Engelfriet and Vogler [15]; Lemma 3.19).
Fact 1
\({\mathopen {[\![}f\mathclose {]\!]}}(s,\overline {t}) = t\)
if and only if there is
K
such that
\({\mathopen {[\![}f\mathclose {]\!]}}(s,\overline {{\bullet }}) = K\)
and
\(t = K[\overline {t}]\).
Accordingly, we can convert an MTT program into a non-accumulative context-generating program, as shown below.
As a result of the above, in a converted program, the arguments of every function are variables, and the return value of a function cannot be traversed again. This rules out any accumulative computation.
The algorithm above is very similar to that used for deaccumulation [19, 31]. Unlike deaccumulation, we treat contexts as first-class objects, which enables us to adopt special treatment for contexts in our inverse computation method.
Example 4
(reverse)
The reverse program is converted into the following program.
The converted program has no accumulative computation.
Example 5
(eval)
The eval program in Sect. 1 is converted into the following program.
Note that the two occurrences of the function call evalAcc(x,…) on the right-hand side of the rule evalAcc(Dbl(x))=… are unified into the single call k=evalAcc
c(x). Recall that Algorithm 1 generates a new variable k
f,x
for a pair of a function f and its input x, but not for its occurrence. Applying the same function to the same input results in the same context in a context-generating program, even though different accumulating arguments are passed in the original program. As a side effect, certain multiple data traversals, i.e., traversals of the same input by the same function, are eliminated through this conversion.
Example 6
(mirror)
The mirror program in Sect. 3 is converted into the following program.
We have omitted the definitions of rev
c and nat
c because they are the same as in Example 4. Some multiple data traversals still remain as k
1=app
c(x),k
2=rev
c(x). However, thanks to the conversion, this sort of multiple data traversal is easy to eliminate by tupling [8, 25] (see the next subsection).
For formal discussion, we define the syntax and the semantics of the non-accumulative context-generating programs in Fig. 3. Since contexts are bound to context variables k, the semantics uses second-order substitutions [12] that are mappings from variables to contexts. The application eΘ of a second-order substitution Θ to a term e is inductively defined by: σ(e
1,…,e
n
)Θ=σ(e
1
Θ,…,e
n
Θ) and k[e
1,…,e
n
]Θ=K[e
1
Θ,…,e
n
Θ] where K=Θ(k). Similarly to MTT, we write [[f]] for the semantics of f.
Now, we can show that the conversion is sound; it does not change the semantics of the functions.
Lemma 1
For any tree
s, \({\mathopen {[\![}f\mathclose {]\!]}}(s,\overline {{\bullet }}) = {\mathopen {[\![}f_{\mathrm {c}}\mathclose {]\!]}}(s)\).
Together with Fact 1, we have \({\mathopen {[\![}f\mathclose {]\!]}}(s,\overline {t}) = K[\overline {t}]\) with K=[[f
c]](s) for every tree s and \(\overline {t}\).
Tupling
Tupling is a well-known semantic-preserving program transformation that can remove some of the multiple data traversals [8, 25].
Roughly speaking, tupling transforms a rule
$$h(x) = \dots k_1 \dots k_2 \dots ~\mathbf {where}~k_1 = f(x), k_2 = g(x) $$
into
$$h(x) = \dots k_1 \dots k_2 \dots ~\mathbf {where}~(k_1,k_2) = {\langle f,g \rangle }(x)\mbox{.} $$
Here, 〈f,g〉 is a function name introduced by tupling, and it is expected to satisfy 〈f,g〉(x)=(f(x),g(x)). Tupling tries to find a recursive definition of 〈f,g〉(x) recursively. For example, the following program for mirror is obtained by tupling.
We shall not explain the tupling in detail because it has been well-studied in the literature of functional programming [8, 25]. Moreover, we shall omit the formal definition of the syntax and the semantics of tupled programs because they are straightforward.
Note that we tuple only the functions that need to be tupled, i.e., the functions that traverse the same input, for the sake of simplicity of our inverse computation method that we will discuss later. For example, app
c and rev
c are tupled because they traverse the same input, whereas nat
c and app
c are not tupled. Thus, the tupling step does not change the reverse
c and eval
c programs. In the tupled program obtained in this way, for any call of a tupled function (k
1,…,k
n
)=〈f
1,…,f
n
〉(x), each variable k
i
(1≤i≤n) occurs at least once in the corresponding expression.
Thanks to the conversion described in the previous section, tupling can eliminate all the multiple data traversals from the converted programs. After tupling, a rule has the form of either
$$f(x) = \overline {e}~\mathbf {where}~\overline {k} = g(x) $$
or
$$f(\sigma(x_1,\dots,x_n)) = \overline {e}~\mathbf {where}~\overline {k_1} = g_1(x_1),\dots,\overline {k_n} = g_n(x_n)\mbox{.} $$
Here, f, g, g
1,…,g
n
are tupled functions. In other words, the tupled programs are always input linear; that is, every input variable occurring on the left-hand side also occurs exactly once on the corresponding right-hand side of each rule.
Tupling may cause size blow-up of a program: a tupled program is at worst 2F-times as big as the original program; F here is the number of functions in the original program. Recall that we tuple only the functions that traverse the same input, not all the functions in a program. Note that only one of 〈rev
c,app
c〉 and 〈app
c,rev
c〉 can appear in a tupled program. Thus, the tupled functions 〈f
1,…,f
n
〉 are as numerous as the sets of the original functions
.
Tree automata construction as memoized inverse computation
We perform inverse computation with memoization after all the preprocessing steps have been completed. However, as mentioned in Sect. 2, unlike the existing inverse computation methods [1, 2, 4], we use a tree automaton [10] to express the memoized-inverse-computation result for the following reasons.
-
A tree automaton is more suitable for a theoretical treatment than a side-effectful memoization table.
-
The set
may be infinite (e.g., eval).
-
We can extract a tree (in DAG representation) from an automaton in time linear to the size of the automaton [10].
-
In some applications such as test-case generation, it is more useful to enumerate the set of the corresponding inputs instead of returning one of the corresponding inputs.
Thus, the use of memoization is implicit in our inverse computation, and we shall not mention narrowing ⇝ and check \(\stackrel {?}{=}\) in this formal development. Note that tree automata are used in the inverse computation because they are convenient rather than necessary; even without them, we can use (a memoized and context-aware version of) the existing inverse computation methods [1, 2, 4].
First of all, we review the definition of tree automata. A tree automaton [10] \(\mathcal{A}\) is a triple (Σ,Q,R), where Σ is a ranked alphabet, Q is a finite set of states, and R is a finite set of transition rules each having the form of either q←q′ or q←σ(q
1,…,q
n
) where σ∈Q
(n). We write \({\mathopen {[\![}q\mathclose {]\!]}}_{\mathcal{A}}\) for the trees accepted by state q in \(\mathcal{A}\), i.e.,
where we take ← as rewriting.
We shall roughly explain the construction of a tree automaton as inverse computation by using the example of ev
c given in Sect. 2. We construct an automaton whose states have the form \(q_{f^{-1}(K)}\) that represents the evaluation of f
−1(K), or the inverse computation result of f for K. Consider inverse computation of ev
c for S
2(Z). The idea behind the construction is to track the evaluation of ev
−1(S
2(Z)). Since the right-hand side of ev
c is k[Z], where k=evA
c(x), the evaluation \(\mathit {ev} _{\mathrm {c}}^{-1}( \mathsf {S} ^{2}( \mathsf {Z} ))\) invokes the evaluation of \(\mathit {evA} _{\mathrm {c}}^{-1}(k)\) for k such that k[Z]=S
2[Z]. In this case, we have only such a k=S
2(•). Thus, we generate a transition rule,
$$q_{ \mathit {ev} _{\mathrm {c}}^{-1}( \mathsf {S} ^2( \mathsf {Z} ))} \leftarrow q_{ \mathit {evA} _{\mathrm {c}}^{-1}( \mathsf {S} ^2({\bullet }))}. $$
Next, let us focus on how \(\mathit {evA} _{\mathrm {c}}^{-1}( \mathsf {S} ^{2}({\bullet }))\) is evaluated. There are three rules of evA
c. The first one has the right-hand side S(•), the second one has the right-hand side k
1[k
2[•]] where k
1=evA
c(x
1) and k
2=evA
c(x
2), and the third one has the right-hand side k[k[•]] where k=evA
c(x). Then, we shall consider the (second-order) matching between the context S
2(•), the argument of \(\mathit {evA} _{\mathrm {c}}^{-1}\), and the right-hand sides. The right-hand side of the first rule does not match the context. For the second rule, there are possibly three (second-order) substitutions obtained from matching S
2(•) with k
1[k
2[•]]: k
1=•,k
2=S
2(•); k
1=S(•),k
2=S(•); and k
1=S
2(•),k
2=•. Recall that k
1 and k
2 are defined by k
1=evA
c(x
1) and k
2=evA
c(x
2), and x
1 and x
2 come from the pattern Add(x
1,x
2). Thus, we generate the following rules.
Similarly, for the third rule, since there is only one substitution k=S(•) obtained from matching S
2(•) with k[k[•]], we generate the following rule.
$$q_{ \mathit {evA} _{\mathrm {c}}^{-1}( \mathsf {S} ^2({\bullet }))} \leftarrow \mathsf {Dbl} (q_{ \mathit {evA} _{\mathrm {c}}^{-1}( \mathsf {S} ({\bullet }))}) $$
Now that we have obtained the transition rules corresponding to the call \(\mathit {evA} _{\mathrm {c}}^{-1}( \mathsf {S} ^{2}({\bullet }))\), we focus on \(\mathit {evA} _{\mathrm {c}}^{-1}( \mathsf {S} ({\bullet }))\). A similar discussion to the one above enables us to generate the following rules.
After that, we move to the rules of \(\mathit {evA} _{\mathrm {c}}^{-1}({\bullet })\) and generate the following rules.
Thus, the inverse computation of ev
c for S
2(Z) is complete. Let \(\mathcal{A}_{ \mathrm {I} }\) be the automaton constructed by gathering the generated rules. We can see that
. Note that the state \(q_{ \mathit {evA} _{\mathrm {c}}^{-1}({\bullet })}\) accepts no trees.
This automaton construction is formalized as follows.
The problem of finding Θ satisfying \(\overline {e}\varTheta = \overline {K}\) for given \(\overline {e}\) and \(\overline {K}\) is called second-order (pattern) matching, and there have been proposed some algorithms to the problem [11, 26, 27]. In the actual construction of the automaton, we do not generate any state that cannot reach \(q_{f^{-1}(t)}\), where f is the function to be inverted and t is the original output. The examples that will be discussed below use this optimization. Note that a tree is a 0-hole context. The nondeleting property is used in the above algorithm for simplicity. If a program is not nondeleting, some input variable x may not have the corresponding function call g(x) in a rule of the tupled program. Then, we have to adopt special treatment for such a x in the construction of R in the algorithm.
Example 7
(reverse
c)
The following automaton \(\mathcal{A}_{ \mathrm {I} }\) is obtained from reverse
c and t=Cons(S(Z),Cons(Z,Nil)).
We have
, which means that there is only one input s=Cons(Z,Cons(S(Z),Nil)) satisfying reverse(s)=reverse
c(s)=t.
Example 8
(eval
c)
The following automaton \(\mathcal{A}_{ \mathrm {I} }\), where q
i
stands for state \(q_{ \mathit {evalA} _{\mathrm {c}}^{-1}( \mathsf {S} ^{i}({\bullet }_{1}))}\), is obtained from eval and S
2(Z).
Intuitively, q
i
represents the set of the arithmetic expressions that evaluate to S
i(Z).
Example 9
(mirror
c)
The following automaton \(\mathcal{A}_{ \mathrm {I} }\) is obtained from mirror
c and Cons(Z,Cons(Z,Nil)).
We have
. Note that some states occurring on the right-hand side do not occur on the left-hand side. An automaton with such states commonly appear when we try to construct an automaton for a function f and a tree t that is not in the range of f. For example, the following automaton \(\mathcal{A}_{ \mathrm {I} }\) is obtained from mirror
c and Cons(Z,Nil).
We have \({\mathopen {[\![} q_{ \mathit {mirror} _{\mathrm {c}}^{-1}( \mathsf {Cons} ( \mathsf {Z} , \mathsf {Nil} ))} \mathclose {]\!]}}_{\mathcal{A}_{ \mathrm {I} }} = \emptyset\).
Our inverse computation is correct in the following sense.
Theorem 1
(Soundness and completeness)
For an input-linear tupled program, \(s \in {\mathopen {[\![}q_{ {\langle \overline {f} \rangle }^{-1}(\overline {K})}\mathclose {]\!]}}_{\mathcal{A}_{ \mathrm {I} }}\)
if and only if
\({\mathopen {[\![} {\langle \overline {f} \rangle } \mathclose {]\!]}}(s) = (\overline {K})\).
Proof
Straightforward by induction. □
Complexity analysis of our inverse computation
We show that the inverse computation runs in time polynomial to the size of the original output and the size of the program, but in time exponential to the number of functions and the maximum arity of the functions and constructors. We state as such in the following theorem.
Theorem 2
Given a parameter-linear MTT program that defines a function
f
and a tree
t, we can construct an automaton representing the set
in time O(2F
m(2F
n
MF)N+1
n
NMF) where
F
is the number of the functions in the program, n
is the size of
t, N
is the maximum arity of constructors in
Σ, m
is the size of the program, and
M
is the maximum arity of functions.
Proof
First, let us examine the cost of our preprocessing. The conversion into context-generating transformation does not increase the program size and can be done in time linear to the program size. In contrast, the tupling may increase the program size to 2F
m. Thus, the total worst-case time complexity for preprocessing is O(2F
m).
Next, let us examine the cost of the inverse computation. The constructed automaton has at most 2F
n
MF states because every state is in the form 〈g
1,…,g
l
〉−1(K
1,…,K
l
), the number of 〈g
1,…,g
l
〉 is smaller than 2F, the number of K
i
is smaller than n
M, and l is no more than F. Note that the number of k-hole subcontexts in t is at most n
k+1 and the contexts occurring in our inverse computation have at most (M−1) kinds of holes. Since the number of the states in an automaton is bounded by P=2F
n
MF and the transition rules are obtained from the rules of the tupled programs that are smaller than 2F
m, the number of the transition rules is bounded by 2F
mP
N+1. Each rule construction takes O(n
NMF) time because, for the second-order matching to find Θ such that \(\overline {e} \varTheta = \overline {K}\), the size of the solution space is bounded by O(n
NMF); note that \(\overline {e}\) contains at most NF context variables that have at most (M−1) kind of holes. Thus, an upper bound of the worst-case cost of the inverse computation is O(2F
m(2F
n
MF)N+1
n
NMF).
Therefore, the total worst-case time complexity of our method is bounded by O(2F
m(2F
n
MF)N+1
n
NMF). □
Note that, if we start from input-linear tupled context-generating programs, the cost is O(m(Fn
Md)N+1
n
Mc), where c is the maximum number of context variables in the rules, and d is the maximum number of components of the tuples in the program. Also note that the above approximation is quite rough. For example, our method ideally runs in time linear to the size of the original output for reverse and mirror for eval, assuming some sophisticated second-order pattern matching algorithm under some sophisticated context representation depending on programs, which will be discussed in Sect. 5.5.
Each step of our inverse computation itself shown in Sects. 4.1, 4.2 and 4.3 does not use the parameter-linearity of an MTT. We only use the parameter-linearity to guarantee that our inverse computation is performed in polynomial time. For parameter-linear MTTs, we only have to consider linear contexts; the number of linear subcontexts of a tree t of size n is a polynomial of n, which leads our polynomial-time results. Our inverse computation indeed terminates for MTTs without restrictions in exponential time because the number of possibly-non-linear m-hole subcontexts of a tree t is at most |t|(m+1)|t|.