1 Introduction

Program synthesis is a powerful technique for automatically generating programs from high-level specifications, such as input-output examples. Due to its myriad use cases across a wide range of application domains (e.g., spreadsheet automation [1,2,3], data science [4,5,6], cryptography [7, 8], improving programming productivity [9,10,11]), program synthesis has received widespread attention from the research community in recent years.

Because program synthesis is, in essence, a very difficult search problem, many recent solutions prune the search space by utilizing program abstractions [4, 12,13,14,15,16]. For example, state-of-the-art synthesis tools, such as Blaze  [14], Morpheus  [4] and Scythe [16], symbolically execute (partial) programs over some abstract domain and reject those programs whose abstract behavior is inconsistent with the given specification. Because many programs share the same behavior in terms of their abstract semantics, the use of abstractions allows these synthesis tools to significantly reduce the search space.

While the abstraction-guided synthesis paradigm has proven to be quite powerful, a down-side of such techniques is that they require a domain expert to manually come up with a suitable abstract domain and write abstract transformers for each DSL construct. For instance, the Blaze synthesis framework [14] expects a domain expert to manually specify a universe of predicate templates, together with sound abstract transformers for every DSL construct. Unfortunately, this process is not only time-consuming but also requires significant insight about the application domain as well as the internal workings of the synthesizer.

Fig. 1.
figure 1

Schematic overview of our approach.

In this paper, we propose a novel technique for automatically learning domain-specific abstractions that are useful for instantiating an example-guided synthesis framework in a new domain. Given a DSL and a training set of synthesis problems (i.e., input-output examples), our method learns a useful abstract domain in the form of predicate templates and infers sound abstract transformers for each DSL construct. In addition to eliminating the significant manual effort required from a domain expert, the abstractions learned by our method often outperform manually-crafted ones in terms of their benefit to synthesizer performance.

The workflow of our approach, henceforth called AtlasFootnote 1, is shown schematically in Fig. 1. Since Atlas is meant to be used as an off-line training step for a general-purpose programming-by-example (PBE) system, it takes as input a DSL as well as a set of synthesis problems \({\varvec{\mathcal {E}}}\) that can be used for training purposes. Given these inputs, our method enters a refinement loop where an Abstraction Learner component discovers a sequence of increasingly precise abstract domains \(\mathcal {A}_1, \cdot \cdot , \mathcal {A}_n\), and their corresponding abstract transformers \(\mathcal {T}_1, \cdot \cdot , \mathcal {T}_n\), in order to help the Abstraction-Guided Synthesizer (AGS) solve all training problems. While the AGS can reject many incorrect solutions using an abstract domain \(\mathcal {A}_i\), it might still return some incorrect solutions due to the insufficiency of \(\mathcal {A}_i\). Thus, whenever the AGS returns an incorrect solution to any training problem, the Abstraction Learner discovers a more precise abstract domain and automatically synthesizes the corresponding abstract transformers. Upon termination of the algorithm, the final abstract domain \(\mathcal {A}_n\) and transformers \(\mathcal {T}_n\) are sufficient for the AGS to correctly solve all training problems. Furthermore, because our method learns general abstractions in the form of predicate templates, the learned abstractions are expected to be useful for solving many other synthesis problems beyond those in the training set.

From a technical perspective, the Abstraction Learner uses two key ideas, namely tree interpolation and data-driven constraint solving, for learning useful abstract domains and transformers respectively. Specifically, given an incorrect program \(\mathcal {P}\) that cannot be refuted by the AGS using the current abstract domain \(\mathcal {A}_i\), the Abstraction Learner generates a tree interpolant \(\mathcal {I}_i\) that serves as a proof of \(\mathcal {P}\)’s incorrectness and constructs a new abstract domain \(\mathcal {A}_{i+1}\) by extracting templates from the predicates used in \(\mathcal {I}_i\). The Abstraction Learner also synthesizes the corresponding abstract transformers for \(\mathcal {A}_{i+1}\) by setting up a second-order constraint solving problem where the goal is to find the unknown relationship between symbolic constants used in the predicate templates. Our method solves this problem in a data-driven way by sampling input-output examples for DSL operators and ultimately reduces the transformer learning problem to solving a system of linear equations.

We have implemented these ideas in a tool called Atlas and evaluate it in the context of the Blaze program synthesis framework [14]. Our evaluation shows that the proposed technique eliminates the manual effort involved in designing useful abstractions. More surprisingly, our evaluation also shows that the abstractions generated by Atlas outperform manually-crafted ones in terms of the performance of the Blaze synthesizer in two different application domains.

To summarize, this paper makes the following key contributions:

  • We describe a method for learning abstractions (domains/transformers) that are useful for instantiating program synthesis frameworks in new domains.

  • We show how tree interpolation can be used for learning abstract domains (i.e., predicate templates) from a few training problems.

  • We describe a method for automatically synthesizing transformers for a given abstract domain under certain assumptions. Our method is guaranteed to find the unique best transformer if one exists.

  • We implement our method in a tool called Atlas and experimentally evaluate it in the context of the Blaze synthesis framework. Our results demonstrate that the abstractions discovered by Atlas outperform manually-written ones used for evaluating Blaze in two application domains.

2 Illustrative Example

Suppose that we wish to use the Blaze meta-synthesizer to automate the class of string transformations considered by FlashFill [1] and BlinkFill [17]. In the original version of the Blaze framework [14], a domain expert needs to come up with a universe of suitable predicate templates as well as abstract transformers for each DSL construct. We will now illustrate how Atlas automates this process, given a suitable DSL and its semantics (e.g., the one used in [17]).

In order to use Atlas, one needs to provide a set of synthesis problems \({{\mathcal {E}}}\) (i.e., input-output examples) that will be used in the training process. Specifically, let us consider the three synthesis problems given below:

figure a

In order to construct the abstract domain \(\mathcal {A}\) and transformers \(\mathcal {T}\), Atlas starts with the trivial abstract domain \(\mathcal {A}_0 = \{ \top \}\) and transformers \(\mathcal {T}_0\), defined as \( {\![\![{F(\top , \cdot \cdot , \top )}\!]\!]^\sharp } =\top \) for each DSL construct F. Using this abstraction, Atlas invokes Blaze to find a program \(\mathcal {P}_0\) that satisfies specification \(\mathcal {E}_1\) under the current abstraction \((\mathcal {A}_0, \mathcal {T}_0)\). However, since the program \(\mathcal {P}_0\) returned by Blaze is incorrect with respect to the concrete semantics, Atlas tries to find a more precise abstraction that allows Blaze to succeed.

Towards this goal, Atlas enters a refinement loop that culminates in the discovery of the abstract domain , where \(\alpha \) denotes a variable and c is an integer constant. In other words, \(\mathcal {A}_1\) tracks equality and inequality constraints on the length of strings. After learning these predicate templates, Atlas also synthesizes the corresponding abstract transformers \(\mathcal {T}_1\). In particular, for each DSL construct, Atlas learns one abstract transformer for each combination of predicate templates used in \(\mathcal {A}_1\). For instance, for the Concat operator which returns the concatenation y of two strings \(x_1, x_2\), Atlas synthesizes the following abstract transformers, where \(\star \) denotes any predicate:

$$\begin{aligned} \mathcal {T}_1 = \left\{ \ \ \begin{array}{rcl} {\![\![{\texttt {Concat} ( \top , \star ) }\!]\!]^\sharp } &{} =&{} \top \\ {\![\![{\texttt {Concat} ( \star , \top ) }\!]\!]^\sharp } &{} =&{} \top \\ {\![\![{\texttt {Concat} \big ( len (x_1) \ne \texttt {c}_1, len (x_2) \ne \texttt {c}_2 \big ) }\!]\!]^\sharp } &{} =&{} \top \\ {\![\![{\texttt {Concat} \big ( len (x_1) = \texttt {c}_1, len (x_2) = \texttt {c}_2 \big ) }\!]\!]^\sharp } &{} =&{} \big ( len (y) = \texttt {c}_1 + \texttt {c}_2 \big ) \\ {\![\![{\texttt {Concat} \big ( len (x_1) = \texttt {c}_1, len (x_2) \ne \texttt {c}_2 \big ) }\!]\!]^\sharp } &{} =&{} \big ( len (y) \ne \texttt {c}_1 + \texttt {c}_2 \big ) \\ {\![\![{\texttt {Concat} \big ( len (x_1) \ne \texttt {c}_1, len (x_2) = \texttt {c}_2 \big ) }\!]\!]^\sharp } &{} =&{} \big ( len (y) \ne \texttt {c}_1 + \texttt {c}_2 \big ) \end{array} \ \ \right\} . \end{aligned}$$

Since the AGS can successfully solve \(\mathcal {E}_1\) using \((\mathcal {A}_1, \mathcal {T}_1)\), Atlas now moves on to the next training problem.

For synthesis problem \(\mathcal {E}_2\), the current abstraction \((\mathcal {A}_1, \mathcal {T}_1)\) is not sufficient for Blaze to discover the correct program. After processing \(\mathcal {E}_2\), Atlas refines the abstract domain to the following set of predicate templates:

Observe that Atlas has discovered two additional predicate templates that track positions of characters in the string. Atlas also learns the corresponding abstract transformers \(\mathcal {T}_2\) for \(\mathcal {A}_2\).

Moving on to the final training problem \(\mathcal {E}_3\), Blaze can already successfully solve it using \((\mathcal {A}_2, \mathcal {T}_2)\); thus, Atlas terminates with this abstraction.

Fig. 2.
figure 2

Overall learning algorithm. \(\textsf {Constructs}\) gives the DSL constructs in \(\mathcal {L}\).

3 Overall Abstraction Learning Algorithm

Our top-level algorithm for learning abstractions, called LearnAbstractions, is shown in Fig. 2. The algorithm takes two inputs, namely a domain-specific language \(\mathcal {L}\) (both syntax and semantics) as well as a set of training problems \({\varvec{\mathcal {E}}}\), where each problem is specified as a set of input-output examples \(\mathcal {E}_i\). The output of our algorithm is a pair \((\mathcal {A}, \mathcal {T})\), where \(\mathcal {A}\) is an abstract domain represented by a set of predicate templates and \(\mathcal {T}\) is the corresponding abstract transformers.

At a high-level, the LearnAbstractions procedure starts with the most imprecise abstraction (just consisting of \(\top \)) and incrementally improves the precision of the abstract domain \(\mathcal {A}\) whenever the AGS fails to synthesize the correct program using \(\mathcal {A}\). Specifically, the outer loop (lines 4–10) considers each training instance \(\mathcal {E}_i\) and performs a fixed-point computation (lines 5–10) that terminates when the current abstract domain \(\mathcal {A}\) is good enough to solve problem \(\mathcal {E}_i\). Thus, upon termination, the learned abstract domain \(\mathcal {A}\) is sufficiently precise for the AGS to solve all training problems \({\varvec{\mathcal {E}}}\).

Specifically, in order to find an abstraction that is sufficient for solving \(\mathcal {E}_i\), our algorithm invokes the AGS with the current abstract domain \(\mathcal {A}\) and corresponding transformers \(\mathcal {T}\) (line 6). We assume that Synthesize returns a program \(\mathcal {P}\) that is consistent with \(\mathcal {E}_i\) under abstraction (\(\mathcal {A}\), \(\mathcal {T}\)). That is, symbolically executing \(\mathcal {P}\) (according to \(\mathcal {T}\)) on inputs \(\mathcal {E}_i^in \) yields abstract values \({\varvec{\varphi }}\) that are consistent with the outputs \(\mathcal {E}_i^out \) (i.e., \(\forall j. \ \mathcal {E}_{ij}^out \in \gamma (\varphi _j)\)). However, while \(\mathcal {P}\) is guaranteed to be consistent with \(\mathcal {E}_i\) under the abstract semantics, it may not satisfy \(\mathcal {E}_i\) under the concrete semantics. We refer to such a program \(\mathcal {P}\) as spurious.

Thus, whenever the call to IsCorrect fails at line 8, we invoke the LearnAbstractDomain procedure (line 9) to learn additional predicate templates that are later added to \(\mathcal {A}\). Since the refinement of \(\mathcal {A}\) necessitates the synthesis of new transformers, we then call LearnTransformers (line 10) to learn a new \(\mathcal {T}\). The new abstraction is guaranteed to rule out the spurious program \(\mathcal {P}\) as long as there is a unique best transformer of each DSL construct for domain \(\mathcal {A}\).

4 Learning Abstract Domain Using Tree Interpolation

In this section, we present the LearnAbstractDomain procedure: Given a spurious program \(\mathcal {P}\) and a synthesis problem \(\mathcal {E}\) that \(\mathcal {P}\) does not solve, our goal is to find new predicate templates \(\mathcal {A}^\prime \) to add to the abstract domain \(\mathcal {A}\) such that the Abstraction-Guided Synthesizer no longer returns \(\mathcal {P}\) as a valid solution to the synthesis problem \(\mathcal {E}\). Our key insight is that we can mine for such useful predicate templates by constructing a tree interpolation problem. In what follows, we first review tree interpolants (based on [18]) and then explain how we use this concept to find useful predicate templates.

Definition 1

(Tree interpolation problem). A tree interpolation problem \(T = (V, r, P, L)\) is a directed labeled tree, where V is a finite set of nodes, \(r \in V\) is the root, \(P: (V \backslash \{ r \}) \mapsto V\) is a function that maps children nodes to their parents, and \(L : V \mapsto \mathbb {F}\) is a labeling function that maps nodes to formulas from a set \(\mathbb {F}\) of first-order formulas such that \(\bigwedge _{v \in V} L(v)\) is unsatisfiable.

In other words, a tree interpolation problem is defined by a tree T where each node is labeled with a formula and the conjunction of these formulas is unsatisfiable. In what follows, we write \(Desc (v)\) to denote the set of all descendants of node v, including v itself, and we write \(NonDesc (v)\) to denote all nodes other than those in \(Desc (v)\) (i.e., \(V\backslash Desc (v)\)). Also, given a set of nodes \(V'\), we write \(L(V')\) to denote the set of all formulas labeling nodes in \(V'\).

Given a tree interpolation problem T, a tree interpolant \(\mathcal {I}\) is an annotation from every node in V to a formula such that the label of the root node is false and the label of an internal node v is entailed by the conjunction of annotations of its children nodes. More formally, a tree interpolant is defined as follows:

Definition 2

(Tree interpolant).  Given a tree interpolation problem \(T = (V, r, P, L)\), a tree interpolant for T is a function \(\mathcal {I}: V \mapsto \mathbb {F}\) that satisfies the following conditions:

  1. 1.

    \(\mathcal {I}(r) = false \);

  2. 2.

    For each \(v \in V\): \(\Big ( \big ( \bigwedge _{P(c_i) = v} \mathcal {I}(c_i) \big ) \wedge L(v) \Big ) \Rightarrow \mathcal {I}(v)\);

  3. 3.

    For each \(v \in V\): \( Vars \big ( \mathcal {I}(v) \big ) \subseteq Vars \big ( L( { \small { Desc }(v) }) \big ) \bigcap Vars \big ( L( { \small NonDesc (v) } ) \big )\).

Fig. 3.
figure 3

A tree interpolation problem and a tree interpolant (underlined).

Intuitively, the first condition ensures that \(\mathcal {I}\) establishes the unsatisfiability of formulas in T, and the second condition states that \(\mathcal {I}\) is a valid annotation. As standard in Craig interpolation [19, 20], the third condition stipulates a “shared vocabulary” condition by ensuring that the annotation at each node v refers to the common variables between the descendants and non-descendants of v.

Example 1

Consider the tree interpolation problem \(T = (V, r, P, L)\) in Fig. 3, where L(v) is shown to the right of each node v. A tree interpolant \(\mathcal {I}\) for this problem maps each node to the corresponding underlined formula. For instance, we have \(\mathcal {I}(v_1) = (len (v_1) \ne 7)\). It is easy to confirm that \(\mathcal {I}\) is a valid interpolant according to Definition 2.

To see how tree interpolation is useful for learning predicates, suppose that the spurious program \(\mathcal {P}\) is represented as an abstract syntax tree (AST), where each non-leaf node is labeled with the axiomatic semantics of the corresponding DSL construct. Now, since \(\mathcal {P}\) does not satisfy the given input-output example \(({e_{in }}, e_{out })\), we are able to use this information to construct a labeled tree where the conjunction of labels is unsatisfiable. Our key idea is to mine useful predicate templates from the formulas used in the resulting tree interpolant.

Fig. 4.
figure 4

Algorithm for learning abstract domain using tree interpolation.

With this intuition in mind, let us consider the LearnAbstractDomain procedure shown in  Fig. 4: The algorithm uses a procedure called ConstructTree to generate a tree interpolation problem T for each input-output example \(({e_{in }}, e_{out })\)Footnote 2 that program \(\mathcal {P}\) does not satisfy (line 5). Specifically, letting \(\varPi \) denote the AST representation of \(\mathcal {P}\), we construct \(T=(V, r, P, L)\) as follows:

  • V consists of all AST nodes in \(\varPi \) as well as a “dummy” node d.

  • The root r of T is the dummy node d.

  • P is a function that maps children AST nodes to their parents and maps the root AST node to the dummy node d.

  • L maps each node \(v \in V\) to a formula as follows:

Essentially, the ConstructTree procedure labels any leaf node representing the program input with the input example \({e_{in }}\) and the root node with the output example \(e_{out }\). All other internal nodes are labeled with the axiomatic semantics of the corresponding DSL operator (modulo renaming).Footnote 3 Observe that the formula \(\bigwedge _{v\in V} L(v)\) is guaranteed to be unsatisfiable since \(\mathcal {P}\) does not satisfy the I/O example \(({e_{in }}, e_{out })\); thus, we can obtain a tree interpolant for T.

Example 2

Consider program \(\mathcal {P}: \texttt {Concat}(x, ``\textsf {18}")\) which concatenates constant string “18” to input x. Figure 3 shows the result of invoking ConstructTree for \(\mathcal {P}\) and input-output example \((``{} \texttt {CAV}", ``{} \texttt {CAV2018}")\). As mentioned in Example 1, the tree interpolant \(\mathcal {I}\) for this problem is indicated with the underlined formulas.

Since the tree interpolant \(\mathcal {I}\) effectively establishes the incorrectness of program \(\mathcal {P}\), the predicates used in \(\mathcal {I}\) serve as useful abstract values that the synthesizer (AGS) should consider during the synthesis task. Towards this goal, the LearnAbstractDomain algorithm iterates over each predicate used in \(\mathcal {I}\) (lines 7–8 in Fig. 4) and converts it to a suitable template by replacing the constants and variables used in \(\mathcal {I}(v)\) with symbolic names (or “holes”). Because the original predicates used in \(\mathcal {I}\) may be too specific for the current input-output example, extracting templates from the interpolant allows our method to learn reusable abstract domains.

Example 3

Given the tree interpolant \(\mathcal {I}\) from Example 1, LearnAbstractDomain extracts two predicate templates, namely, and .

5 Synthesis of Abstract Transformers

In this section, we turn our attention to the LearnTransformers procedure for synthesizing abstract transformers \(\mathcal {T}\) for a given abstract domain \(\mathcal {A}\). Following presentation in prior work [14], we consider abstract transformers that are described using equations of the following form:

$$\begin{aligned} {\![\![{F \big ( \chi _1(x_1, {\varvec{c}}_1), \cdot \cdot , \chi _n(x_n, {\varvec{c}}_n) \big )}\!]\!]^\sharp } = \bigwedge _{1 \le j \le m} {\chi '_{j} \big ( y, {\varvec{f}}_j({\varvec{c}}) \big )} \end{aligned}$$
(1)

Here, F is a DSL construct, \(\chi _i, \chi '_{j}\) are predicate templatesFootnote 4, \(x_i\) is the i’th input of F, y is F’s output, \({\varvec{c}}_\mathbf 1 , \cdot \cdot , {\varvec{c}}_{\varvec{n}}\) are vectors of symbolic constants, and \({\varvec{f}}_j\) denotes a vector of affine functions over \({\varvec{c}}= {\varvec{c}}_\mathbf 1 , \cdot \cdot , {\varvec{c}}_{\varvec{n}}\). Intuitively, given concrete predicates describing the inputs to F, the transformer returns concrete predicates describing the output. Given such a transformer \(\tau \), let \(\mathsf {Outputs}(\tau )\) be the set of pairs \((\chi _j', {\varvec{f}}_j)\) in Eq. 1.

Fig. 5.
figure 5

Algorithm for synthesizing abstract transformers. \(\phi _{F}\) at line 6 denotes the axiomatic semantics of DSL construct F. Formula \(\varLambda \) at line 8 refers to Eq. 5.

We define the soundness of a transformer \(\tau \) for DSL operator F with respect to F’s axiomatic semantics \(\phi _F\). In particular, we say that the abstract transformer from Eq. 1 is sound if the following implication is valid:

$$\begin{aligned} \Big ( \phi _{F}({\varvec{x}}, {y}) \wedge \bigwedge _{1 \le i \le n} \chi _i(x_i, {\varvec{c}}_i) \Big ) \Rightarrow \bigwedge _{1 \le j \le m} {\chi '_{j} \big ( y, {\varvec{f}}_j({\varvec{c}}) \big )} \end{aligned}$$
(2)

That is, the transformer for F is sound if the (symbolic) output predicate is indeed implied by the (symbolic) input predicates according to F’s semantics.

Our key observation is that the problem of learning sound transformers can be reduced to solving the following second-order constraint solving problem:

$$\begin{aligned} \exists {\varvec{f}}. \ \forall {\varvec{V}}. {\Big (} \big ( \phi _{F}({\varvec{x}}, {y}) \wedge \bigwedge _{1 \le i \le n} \chi _i(x_i, {\varvec{c}}_i) \big ) \Rightarrow \bigwedge _{1 \le j \le m} {\chi '_{j} \big ( y, {\varvec{f}}_j({\varvec{c}}) \big ) {\Big )} } \end{aligned}$$
(3)

where \({\varvec{f}} = {\varvec{f}}_1, \cdot \cdot , {\varvec{f}}_m\) and \({\varvec{V}}\) includes all variables and functions from Eq. 2 other than \({\varvec{f}}\). In other words, the goal of this constraint solving problem is to find interpretations of the unknown functions \({\varvec{f}}\) that make Eq. 2 valid. Our key insight is to solve this problem in a data-driven way by exploiting the fact that each unknown function \(f_{j,k}\) is affine.

Towards this goal, we first express each affine function \(f_{j,k}({\varvec{c}})\) as follows:

$$ f_{j,k}({\varvec{c}}) = p_{j,k,1} \cdot c_{1} + \cdot \cdot + p_{j,k,|{\varvec{c}}|} \cdot c_{|{\varvec{c}}|} + p_{j,k, |{\varvec{c}}| + 1} $$

where each \(p_{j,k,l}\) corresponds to an unknown integer constant that we would like to learn. Now, arranging the coefficients of functions \(f_{j,1}, \cdot \cdot , f_{j,|{\varvec{f}}_j|}\) in \({\varvec{f}}_j\) into a \(|{\varvec{f}}_j| \times (|{\varvec{c}}| + 1)\) matrix \(P_j\), we can represent \({\varvec{f}}_j({\varvec{c}})\) in the following way:

$$\begin{aligned} \small {\varvec{f}}_j({\varvec{c}})^{\intercal } = \underbrace{ \left[ \begin{array}{cccc} f_{j,1}({\varvec{c}}) \\ \cdot \cdot \\ f_{j,|{\varvec{f}}_j|}({\varvec{c}}) \\ \end{array} \right] }_{{{\varvec{c}}^{\prime }_{j}}^{\intercal }} = \underbrace{ \left[ \begin{array}{cccc} p_{j,1,1} &{} \cdot \cdot &{} \ \ p_{j,1, |{\varvec{c}}| + 1} \\ \cdot \cdot &{} &{} \cdot \cdot \\ p_{j,|{\varvec{f}}_j|,1} &{} \cdot \cdot &{} \ \ p_{j,|{\varvec{f}}_j|, |{\varvec{c}}| + 1} \\ \end{array} \right] }_{P_j} \underbrace{ \left[ \begin{array}{c} c_{1} \\ \cdot \cdot \\ c_{|{\varvec{c}}|} \\ 1 \\ \end{array} \right] }_{{\varvec{c}}^{\dagger }} \end{aligned}$$
(4)

where \({\varvec{c}}^\dagger \) is \({\varvec{c}}^{\intercal }\) appended with the constant 1.

Given this representation, it is easy to see that the problem of synthesizing the unknown functions \({\varvec{f}}_1, \cdot \cdot , {\varvec{f}}_m\) from  Eq. 2 boils down to finding the unknown matrices \(P_1, \cdot \cdot , P_m\) such that each \(P_j\) makes the following implication valid:

$$\begin{aligned} \varLambda \ \equiv \ \Big ( \Big ( ({{\varvec{c}}^{\prime }_{j}}^{\intercal } = P_j {\varvec{c}}^\dagger ) \wedge \phi _{F}({\varvec{x}}, {y}) \wedge \bigwedge _{1 \le i \le n} \chi _i(x_i, {\varvec{c}}_i) \Big ) \Rightarrow \chi '_j(y, {{\varvec{c}}^{\prime }_{j}}) \Big ) \end{aligned}$$
(5)

Our key idea is to infer these unknown matrices \(P_1, \cdot \cdot , P_m\) in a data-driven way by generating input-output examples of the form \([i_1, \cdot \cdot , i_{|{\varvec{c}}|}] \mapsto [o_1, \cdot \cdot , o_{|{\varvec{f}}_j|}]\) for each \({\varvec{f}}_j\). In other words, \({\varvec{i}}\) and \({\varvec{o}}\) correspond to instantiations of \({\varvec{c}}\) and \({\varvec{f}}_j({\varvec{c}})\) respectively. Given sufficiently many such examples for every \({\varvec{f}}_j\), we can then reduce the problem of learning each unknown matrix \(P_j\) to the problem of solving a system of linear equations.

Based on this intuition, the LearnTransformers procedure from Fig. 5 describes our algorithm for learning abstract transformers \(\mathcal {T}\) for a given abstract domain \(\mathcal {A}\). At a high-level, our algorithm synthesizes one abstract transformer for each DSL construct F and n argument predicate templates \(\chi _1, \cdot \cdot , \chi _n\). In particular, given F and \(\chi _1, \cdot \cdot , \chi _n\), the algorithm constructs the “return value” of the transformer as:

$$ \varphi = \bigwedge _{1 \le j \le m} \chi '_j(y, {\varvec{f}}_j({\varvec{c}})) $$

where \({\varvec{f}}_j\) is the inferred affine function for each predicate template \(\chi '_j\).

The key part of our LearnTransformers procedure is the inner loop (lines 5–8) for inferring each of these \({\varvec{f}}_j\)’s. Specifically, given an output predicate template \(\chi '_j\), our algorithm first generates a set of input-output examples E of the form \([p_1, \cdot \cdot , p_n] \mapsto p_0\) such that \( {\![\![{F(p_1 , \cdot \cdot , p_n)}\!]\!]^\sharp } = p_0 \) is a sound (albeit overly specific) transformer. Essentially, each \(p_i\) is a concrete instantiation of a predicate template, so the examples E generated at line 6 of the algorithm can be viewed as sound input-output examples for the general symbolic transformer given in  Eq. 1. (We will describe the GenerateExamples procedure in Sect. 5.1).

Once we generate these examples E, the next step of the algorithm is to learn the unknown coefficients of matrix \(P_j\) from Eq. 5 by solving a system of linear equations (line 7). Specifically, observe that we can use each input-output example \([p_1, \cdot \cdot , p_n] \mapsto p_0\) in E to construct one row of Eq. 4. In particular, we can directly extract \({\varvec{c}} = {\varvec{c}}_1, \cdot \cdot , {\varvec{c}}_n\) from \(p_1, \cdot \cdot , p_n\) and the corresponding value of \({\varvec{f}}_j({\varvec{c}})\) from \(p_0\). Since we have one instantiation of Eq. 4 for each of the input-output examples in E, the problem of inferring matrix \(P_j\) now reduces to solving a system of linear equations of the form \(A P_j^T = B\) where A is a \(|E| \times (|{\varvec{c}}| + 1)\) (input) matrix and B is a \(|E| \times |{\varvec{f}}_j|\) (output) matrix. Thus, a solution to the equation \(A P_j^T = B\) generated from E corresponds to a candidate solution for matrix \(P_j\), which in turn uniquely defines \({\varvec{f}}_j\).

Observe that the call to Solve at line 7 may return null if no affine function exists. Furthermore, any non-null \({\varvec{f}}_j\) returned by Solve is just a candidate solution and may not satisfy Eq. 5. For example, this situation can arise if we do not have sufficiently many examples in E and end up discovering an affine function that is “over-fitted” to the examples. Thus, the validity check at line 8 of the algorithm ensures that the learned transformers are actually sound.

5.1 Example Generation

In our discussion so far, we assumed an oracle that is capable of generating valid input-output examples for a given transformer. We now explain our GenerateExamples procedure from Fig. 6 that essentially implements this oracle. In a nutshell, the goal of GenerateExamples is to synthesize input-output examples of the form \([p_1, \cdot \cdot , p_n] \mapsto p_0\) such that \({\![\![{F(p_1, \cdot \cdot , p_n)}\!]\!]^\sharp } = p_0\) is sound where each \(p_i\) is a concrete predicate (rather than symbolic).

Going into more detail, GenerateExamples takes as input the semantics \(\phi _F\) of DSL construct F for which we want to learn a transformer for as well as the input predicate templates \(\chi _1, \cdot \cdot , \chi _n\) and output predicate template \(\chi _0\) that are supposed to be used in the transformer. For any example \([p_1, \cdot \cdot , p_n] \mapsto p_0\) synthesized by GenerateExamples, each concrete predicate \(p_i\) is an instantiation of the predicate template \(\chi _i\) where the symbolic constants used in \(\chi _i\) are substituted with concrete values.

Fig. 6.
figure 6

Example generation for learning abstract transformers.

Conceptually, the GenerateExamples algorithm proceeds as follows: First, it generates concrete input-output examples \([s_1, \cdot \cdot , s_n] \mapsto s_0\) by evaluating F on randomly-generated inputs \(s_1, \cdot \cdot , s_n\) (lines 4–5). Now, for each concrete I/O example \([s_1, \cdot \cdot , s_n] \mapsto s_0\), we generate a set of abstract I/O examples of the form \([p_1, \cdot \cdot , p_n] \mapsto p_0\) (line 6). Specifically, we assume that the return value \((A_0, \cdot \cdot , A_n)\) of Abstract at line 6 satisfies the following properties for every \(p_i \in A_i\):

  • \(p_i\) is an instantiation of template \(\chi _i\).

  • \(p_i\) is a sound over-approximation of \(s_i\) (i.e., \(s_i \in \gamma (p_i)\)).

  • For any other \(p_i'\) satisfying the above two conditions, \(p_i'\) is not logically stronger than \(p_i\).

In other words, we assume that Abstract returns a set of “best” sound abstractions of \((s_0, \cdot \cdot , s_n)\) under predicate templates \((\chi _0, \cdot \cdot , \chi _n)\).

Next, given abstractions \((A_0, \cdot \cdot , A_n)\) for \((s_0, \cdot \cdot , s_n)\), we consider each candidate abstract example of the form \([p_1, \cdot \cdot , p_n] \mapsto p_0\) where \(p_i \in A_i\). Even though each \(p_i\) is a sound abstraction of \(s_i\), the example \([p_1, \cdot \cdot , p_n] \mapsto p_0\) may not be valid according to the semantics of operator F. Thus, the validity check at line 8 ensures that each example added to E is in fact valid.

Example 4

Given abstract domain , suppose we want to learn an abstract transformer \(\tau \) for the Concat operator of the following form:

$$\begin{aligned} {\![\![{\texttt {Concat} \big ( len (x_1) = \textsf {c}_1, len (x_2) = \textsf {c}_2 \big )}\!]\!]^\sharp } = \big ( len (y) = f([\textsf {c}_1, \textsf {c}_2]) \big ) \end{aligned}$$

We learn the affine function f used in the transformer by first generating a set E of I/O examples for f (line 6 in LearnTransformers). In particular, GenerateExamples generates concrete input values for Concat at random and obtains the corresponding output values by executing Concat on the input values. For instance, it may generate \(s_1 = ``abc"\) and \(s_2 = ``de"\) as inputs, and obtain \(s_0 = ``abcde"\) as output. Then, it abstracts these values under the given templates. In this case, we have an abstract example with \(p_1 = \big (len (x_1) = {{3}} \big )\), \(p_2 = \big ( len (x_2) = {{2}} \big )\) and \(p_0 = \big ( len (y) = {{5}} \big )\). Since \([p_1, p_2] \mapsto p_0\) is a valid example, it is added in E (line 8 in GenerateExamples). At this point, E is not yet full rank, so the algorithm keeps generating more examples. Suppose it generates two more valid examples \(\big ( len (x_1) = {{1}}, len (x_2) = {{4}} \big ) \mapsto \big ( len (y) = {{5}} \big )\) and \(\big ( len (x_1) = 6, len (x_2) = 4 \big ) \mapsto \big ( len (y) = 10 \big )\). Now E is full rank, so LearnTransformers computes f by solving the following system of linear equations:

Here, P corresponds to the function \(f([ \texttt {c}_1, \texttt {c}_2]) = \texttt {c}_1 + \texttt {c}_2\), and this function defines the sound transformer:  \({\![\![{\texttt {Concat} \big ( len ({x_1}) = \texttt {c}_1, len ({x_2}) = \texttt {c}_2 \big )}\!]\!]^\sharp } = \big ( len ({y}) = \texttt {c}_1 + \texttt {c}_2 \big ) \) which is added to \(\mathcal {T}\) at line 9 in LearnTransformers.

6 Soundness and Completeness

In this section we present theorems stating some of the soundness, completeness, and termination guarantees of our approach. All proofs can be found in the extended version of this paper [21].

Theorem 1

(Soundness of LearnTransformers). Let \(\mathcal {T}\) be the set of transformers returned by LearnTransformers. Then, every \(\tau \in \mathcal {T}\) is sound according to Eq. 2.

The remaining theorems are predicated on the assumptions that for each DSL construct F and input predicate templates \(\chi _1, \cdot \cdot , \chi _n\) (i) there exists a unique best abstract transformer and (ii) the strongest transformer expressible in Eq. 2 is logically equivalent to the unique best transformer. Thus, before stating these theorems, we first state what we mean by a unique best abstract transformer.

Definition 3

(Unique best function). Consider a family of transformers of the shape \( {\![\![{F \big ( \chi _1(x_1, {\varvec{c}}_1), \cdot \cdot , \chi _n(x_n, {\varvec{c}}_n) \big )}\!]\!]^\sharp } = \chi '(y, \star ) \). We say that \({\varvec{f}}\) is the unique best function for \((F, \chi _1, \cdot \cdot , \chi _n, \chi ')\) if (a) replacing \(\star \) with \({\varvec{f}}\) yields a sound transformer, and (b) replacing \(\star \) with any other \({\varvec{f'}}\) yields a transformer that is either unsound or strictly worse (i.e., \(\chi '(y, {\varvec{f}}) \Rightarrow \chi '(y, {\varvec{f}}')\) and \(\chi '(y, {\varvec{f}}') \not \Rightarrow \chi '(y, {\varvec{f}})\)).

We now define unique best transformer in terms of unique best function:

Definition 4

(Unique best transformer). Let F be a DSL construct and let \((\chi _1, \cdot \cdot , \chi _n) \in \mathcal {A}^n\) be the input templates for F. We say that the abstract transformer \(\tau \) is a unique best transformer for \(F, \chi _1, \cdot \cdot , \chi _n\) if (a) \(\tau \) is sound, and (b) for any predicate template \(\chi \in \mathcal {A}\), we have \((\chi , {\varvec{f}}) \in \mathsf {Outputs}(\tau )\) if and only if \({\varvec{f}}\) is a unique best function for \((F, \chi _1, \cdot \cdot , \chi _n, \chi )\) for some affine \({\varvec{f}}\).

Definition 5

(Complete sampling oracle). Let F be a construct, \(\mathcal {A}\) an abstract domain, and \(R_F\) a probability distribution over \(\textsc {Domain}(F)\) with finite support S. Futher, for any input predicate templates \(\chi _1, \cdot \cdot , \chi _n\) and output predicate template \(\chi _0\) in \(\mathcal {A}\) admitting a unique best function \({\varvec{f}}\), let \(C(\chi _0,\cdot \cdot ,\chi _n)\) be the set of tuples \((c_0,\cdot \cdot ,c_n)\) such that \((\chi _0(y,c_0),\chi _1(x_1,c_1),\cdot \cdot ,\chi _n(x_n,c_n)) \in A_0 \times \cdot \cdot \times A_n\) and \(c_0 = {\varvec{f}}(c_1,\cdot \cdot ,c_n)\), where \(A_0 \times \cdot \cdot \times A_n = \textsc {Abstract}(s_0,\chi _0,\cdot \cdot ,s_n,\chi _n)\) and \((s_1,\cdot \cdot ,s_n) \in S\) and \(s_0 = \![\![{F(s_1, \cdot \cdot , s_n)}\!]\!]\). The distribution \(R_F\) is a complete sampling oracle if \(C(\chi _0,\cdot \cdot ,\chi _n)\) has full rank for all \(\chi _0,\cdot \cdot ,\chi _n\).

The following theorem states that LearnTransformers is guaranteed to synthesize the best transformer if a unique one exists:

Theorem 2

(Completeness of LearnTransformers). Given an abstract domain \(\mathcal {A}\) and a complete sampling oracle \(R_F\) for \(\mathcal {A}\), LearnTransformers terminates. Further, let \(\mathcal {T}\) be the set of transformers returned and let \(\tau \) be the unique best transformer for DSL construct F and input predicate templates \(\chi _1, \cdot \cdot , \chi _n \in \mathcal {A}^n\). Then we have \(\tau \in \mathcal {T}\).

Using this completeness (modulo unique best transformer) result, we can now state the termination guarantees of our LearnAbstractions algorithm:

Theorem 3

(Termination of LearnAbstractions). Given a complete sampling oracle \(R_F\) for every abstract domain and the unique best transformer assumption, if there exists a solution for every problem \(\mathcal {E}_i \in {\varvec{\mathcal {E}}}\), then LearnAbstractions terminates.

7 Implementation and Evaluation

We have implemented the proposed method as a new tool called Atlas, which is written in Java. Atlas takes as input a set of training problems, an Abstraction-Guided Synthesizer (AGS), and a DSL and returns an abstract domain (in the form of predicate templates) and the corresponding transformers. Internally, Atlas uses the Z3 theorem prover [22] to compute tree interpolants and the JLinAlg linear algebra library [23] to solve linear equations.

To assess the usefulness of Atlas, we conduct an experimental evaluation in which our goal is to answer the following two questions:

  1. 1.

    How does Atlas perform during training? That is, how many training problems does it require and how long does training take?

  2. 2.

    How useful are the abstractions learned by Atlas in the context of synthesis?

7.1 Abstraction Learning

To answer our first question, we use Atlas to automatically learn abstractions for two application domains: (i) string manipulations and (ii) matrix transformations. We provide Atlas with the DSLs used in [14] and employ Blaze as the underlying Abstraction-Guided Synthesizer. Axiomatic semantics for each DSL construct were given in the theory of equality with uninterpreted functions.

Training Set Information. For the string domain, our training set consists of exactly the four problems used as motivating examples in the BlinkFill paper [17]. Specifically, each training problem consists of 4–6 examples that demonstrate the desired string transformation. For the matrix domain, our training set consists of four (randomly selected) synthesis problems taken from online forums. Since almost all online posts contain a single input-output example, each training problem includes one example illustrating the desired matrix transformation.

Main Results. Our main results are summarized in Fig. 7. The main take-away message is that Atlas can learn abstractions quite efficiently and does not require a large training set. For example, Atlas learns 5 predicate templates and 30 abstract transformers for the string domain in a total of 10.2 s. Interestingly, Atlas does not need all the training problems to infer these four predicates and converges to the final abstraction after just processing the first training instance. Furthermore, for the first training instance, it takes Atlas 4 iterations in the learning loop (lines 5–10 from Fig. 2) before it converges to the final abstraction. Since this abstraction is sufficient, it takes just one iteration for each following training problem to synthesize a correct program.

Fig. 7.
figure 7

Training results. \(|\mathcal {A}|, |\mathcal {T}|\), Iters denote the number of predicate templates, abstract transformers, and iterations taken per training instance (lines 5–10 from Fig. 2), respectively. \(T_{\textsf {AGS}}, T_{\mathcal {A}}, T_{\mathcal {T}}\) denote the times for invoking the synthesizer (AGS), learning the abstract domain, and learning the abstract transformers, respectively. \(T_{total }\) shows the total training time in seconds.

Fig. 8.
figure 8

Improvement of Blaze \(^\star \) over Blaze \(^\dagger \) on string and matrix benchmarks.

Looking at the right side of Fig. 7, we also observe similar results for the matrix domain. In particular, Atlas learns 10 predicate templates and 59 abstract transformers in a total of 22.5 s. Furthermore, Atlas converges to the final abstract domain after processing the first three problemsFootnote 5 and the number of iterations for each training instance is also quite small (ranging from 1 to 3).

7.2 Evaluating the Usefulness of Learned Abstractions

To answer our second question, we integrated the abstractions synthesized by Atlas into the Blaze meta-synthesizer. In the remainder of this section, we refer to all instantiations of Blaze using the Atlas-generated abstractions as Blaze \(^\star \). To assess how useful the automatically generated abstractions are, we compare Blaze \(^\star \) against Blaze \(^\dagger \), which refers to the manually-constructed instantiations of Blaze described in [14].

Benchmark Information. For the string domain, our benchmark suite consists of (1) all 108 string transformation benchmarks that were used to evaluate Blaze \(^\dagger \) and (2) 40 additional challenging problems that are collected from online forums which involve manipulating file paths, URLs, etc. The number of examples for each benchmark ranges from 1 to 400, with a median of 7 examples. For the matrix domain, our benchmark set includes (1) all 39 matrix transformation benchmarks in the Blaze \(^\dagger \) benchmark suite and (2) 20 additional challenging problems collected from online forums. We emphasize that the set of benchmarks used for evaluating Blaze \(^\star \) are completely disjoint from the set of synthesis problems used for training Atlas.

Experimental Setup. We evaluate Blaze \(^\star \) and Blaze \(^\dagger \) using the same DSLs from the Blaze paper [14]. For each benchmark, we provide the same set of input-output examples to Blaze \(^\star \) and Blaze \(^\dagger \), and use a time limit of 20 min per synthesis task.

Main Results. Our main evaluation results are summarized in Fig. 8. The key observation is that Blaze \(^\star \) consistently improves upon Blaze \(^\dagger \) for both string and matrix transformations. In particular, Blaze \(^\star \) not only solves more benchmarks than Blaze \(^\dagger \) for both domains, but also achieves about an order of magnitude speed-up on average for the common benchmarks that both tools can solve. Specifically, for the string domain, Blaze \(^\star \) solves 133 (out of 148) benchmarks within an average of 2.8 s and achieves an average 8.3\(\times \) speed-up over Blaze \(^\dagger \). For the matrix domain, we also observe a very similar result where Blaze \(^\star \) leads to an overall speed-up of 9.2\(\times \) on average.

In summary, this experiment confirms that the abstractions discovered by Atlas are indeed useful and that they outperform manually-crafted abstractions despite eliminating human effort.

8 Related Work

To our knowledge, this paper is the first one to automatically learn abstract domains and transformers that are useful for program synthesis. We also believe it is the first to apply interpolation to program synthesis, although interpolation has been used to synthesize other artifacts such as circuits [24] and strategies for infinite games [25]. In what follows, we briefly survey existing work related to program synthesis, abstraction learning, and abstract transformer computations.

Program Synthesis. Our work is intended to complement example-guided program synthesis techniques that utilize program abstractions to prune the search space [4, 14,15,16]. For example, Simpl [15] uses abstract interpretation to speed up search-based synthesis and applies this technique to the generation of imperative programs for introductory programming assignments. Similarly, Scythe [16] and Morpheus  [4] perform enumeration over program sketches and use abstractions to reject sketches that do not have any valid completion. Somewhat different from these techniques, Blaze constructs a finite tree automaton that accepts all programs whose behavior is consistent with the specification according to the DSL’s abstract semantics. We believe that the method described in this paper can be useful to all such abstraction-guided synthesizers.

Abstraction Refinement. In verification, as opposed to synthesis, there have been many works that use Craig interpolants to refine abstractions [20, 26, 27]. Typically, these techniques generalize the interpolants to abstract domains by extracting a vocabulary of predicates, but they do not generalize by adding parameters to form templates. In our case, this is essential because interpolants derived from fixed input values are too specific to be directly useful. Moreover, we reuse the resulting abstractions for subsequent synthesis problems. In verification, this would be analogous to re-using an abstraction from one property or program to the next. It is conceivable that template-based generalization could be applied in verification to facilitate such reuse.

Abstract Transformers. Many verification techniques use logical abstract domains [28,29,30,31,32]. Some of these, following Yorsh, et al. [33] use sampling with a decision procedure to evaluate the abstract transformer [34]. Interpolation has also been used to compile efficient symbolic abstract transformers [35]. However, these techniques are restricted to finite domains or domains of finite height to allow convergence. Here, we use infinite parameterized domains to obtain better generalization; hence, the abstract transformer computation is more challenging. Nonetheless, the approach might also be applicable in verification.

9 Limitations

While this paper takes a first step towards automatically inferring useful abstractions for synthesis, our proposed method has the following limitations:

Shapes of Transformers. Following prior work [14], our algorithm assumes that abstract transformers have the shape given in Eq. 1. We additionally assume that constants \({\varvec{c}}\) used in predicate templates are numeric values and that functions in Eq. 1 are affine. This assumption holds in several domains considered in prior work [4, 14] and allows us to develop an efficient learning algorithm that reduces the problem to solving a system of linear equations.

DSL Semantics. Our method requires the DSL designer to provide the DSL’s logical semantics. We believe that giving logical semantics is much easier than coming up with useful abstractions, as it does not require insights about the internal workings of the synthesizer. Furthermore, our technique could, in principle, also work without logical specifications although the learned abstract domain may not be as effective (see Footnote 3 in Sect. 4) and the synthesized transformers would not be provably sound.

UBT Assumption. Our completeness and termination theorems are predicated on the unique best transformer (UBT) assumption. While this assumption holds in our evaluation, it may not hold in general. However, as mentioned in Sect. 6, we can always guarantee termination by including the concrete predicates used in the interpolant \(\mathcal {I}\) in addition to the symbolic templates extracted from \(\mathcal {I}\).

10 Conclusion

We proposed a new technique for automatically instantiating abstraction-guided synthesis frameworks in new domains. Given a DSL and a few training problems, our method automatically discovers a useful abstract domain and the corresponding transformers for each DSL construct. From a technical perspective, our method uses tree interpolation to extract reusable templates from failed synthesis attempts and automatically synthesizes unique best transformers if they exist. We have incorporated the proposed approach into the Blaze meta-synthesizer and show that the abstractions discovered by Atlas are very useful.

While we have applied the proposed technique to program synthesis, we believe that some of the ideas introduced here are more broadly applicable. For instance, the idea of extracting reusable predicate templates from interpolants and synthesizing transformers in a data-driven way could also be useful in the context of program verification.