figure a
figure b

1 Introduction

Type-directed program synthesis is a technique for synthesising programs from user-provided type specifications. The technique has a long history intertwined with proof search, thanks to the Curry-Howard correspondence [22, 37]. We present a program synthesis approach that leverages the information of graded type systems that track and enforce program properties related to data flow. Our approach follows the concept of program synthesis as a form of proof search in logic: given a type \( A \) we want to find a program term \( t \) which inhabits \( A \). We express this in terms of a synthesis judgement akin to typing or proof rules:

$$\begin{aligned} \varGamma \vdash A \Rightarrow t \end{aligned}$$

meaning that the term \( t \) can be synthesised for the goal type \( A \) under a context of assumptions \(\varGamma \). A calculus of synthesis rules for inductively defines the above synthesis judgement for each type former of a language. For example, we may define a synthesis rule for standard product types in the following way:

$$\begin{aligned} \frac{ \varGamma \vdash A \Rightarrow t_{{\textrm{1}}} \quad \;\;\;\, \varGamma \vdash B \Rightarrow t_{{\textrm{2}}} }{ \varGamma \vdash A \times B \Rightarrow ( t_{{\textrm{1}}} , t_{{\textrm{2}}} )}\times _{\textsc {Intro}} \end{aligned}$$

Reading ‘clockwise’ from the bottom-left: to synthesise a value of type \(A \times B\), we synthesise a value of type A and then a value of type B and combine them into a pair in the conclusion. The ‘ingredients’ for synthesising the subterms \( t_{{\textrm{1}}} \) and \( t_{{\textrm{2}}} \) come from the free-variable assumptions \(\varGamma \) and any constructors of A and B.

Depending on the context, there may be many possible combinations of assumptions to synthesise a pair. Consider the following type and partial program with a hole (marked ?) specifying a position to perform program synthesis:

$$\begin{aligned} f : A \rightarrow A \rightarrow A \rightarrow A \times A \qquad \quad f\ x\ y\ z =\ ? \end{aligned}$$

The function has three parameters all of type A which can be used to synthesise an expression of the goal type \(A \times A\). Expressing this synthesis problem as an instantiation of the above \(\times _{\textsc {Intro}}\) rule yields:

$$\begin{aligned} \frac{ x : A , y : A , z : A \vdash A \Rightarrow t_{{\textrm{1}}} \quad \;\;\;\, x : A , y : A , z : A \vdash A \Rightarrow t_{{\textrm{2}}} }{ x : A , y : A , z : A \vdash A \times A \Rightarrow ( t_{{\textrm{1}}} , t_{{\textrm{2}}} ) }\times _{\textsc {Intro}} \end{aligned}$$

Even in this simple setting, the number of possibilities starts to become unwieldy: there are \(3^2\) possible candidate programs based on combinations of x, y and z. We thus wish to constrain the number of choices required by the synthesis algorithm. Many systems achieve this by allowing the user to specify additional information about the desired program behaviour. For example, recent work extends type-directed synthesis to refinement types [50], cost specifications [35], differential privacy [52], ownership information [16], example-guided synthesis [2, 15] and examples integrated with types [17, 47]. The general idea is that the proof search / program synthesis procedure can be pruned and refined given more information, whether richer types, additional examples, or behavioural specifications.

We instead leverage the information contained in graded type systems which constrain how data can be used by a program and thus reduce the number of possible synthesis choices. Our hypothesis is that grade-and-type-directed synthesis reduces the number of paths that need to be explored and the number of input-output examples that are needed, thus potentially speeding up synthesis.

Graded type systems trace their roots to linear logic. In linear logic, data is treated as though it were a finite resource which must be consumed exactly once, disallowing arbitrary copying and discarding [20]. Non-linearity is captured by the ! modal operator (the exponential modality). This gives a binary view—a value may either be used exactly once or in a completely unconstrained way. Bounded Linear Logic (BLL) refines this view, replacing ! with a family of indexed modal operators where the index provides an upper bound on usage [21], e.g., \(!_{\le 4}A\) represents a value A which may be used up to 4 times. Various works have generalised BLL, resulting in graded type systems in which these indices are drawn from an arbitrary pre-ordered semiring [1, 5, 12, 14, 19, 39, 49]. This allows numerous program properties to be tracked and enforced statically. Such systems are being deployed in language implementations, forming the basis of Haskell’s linear types extension [8], Idris 2 [11], and the language Granule [45].

Returning to our example in a graded setting, the function’s parameters now have grades that we choose, for the sake of example, to be particular natural numbers describing the exact number of times the parameters must be used:

$$\begin{aligned} f : A^{2} \rightarrow A^{0} \rightarrow A^{0} \rightarrow A \times A \qquad \quad f\ x\ y\ z =\ ? \end{aligned}$$

The first A is annotated with a grade 2 meaning it must be used twice. The types of y and z are graded with 0, enforcing zero usage, i.e., they cannot be used in the body of f. The result is that there is only one (normal form) inhabitant for this type: \(( x , x )\). For synthesis, the other assumptions will not even be considered, allowing pruning of branches which use resources in a way which violates the grades. Natural number grades in this example explain how many times a value can be used, but we may instead represent different program properties such as sensitivity, strictness, or security levels for tracking non-interference, all of which are well-known instances of graded type systems [1, 18, 45]. These examples are all graded presentations of coeffects, tracking how a programs uses its context, in contrast with graded types for effects [32, 46] which are not considered here.

In prior work, we built on proof search for linear logic [25], developing a program synthesis technique for a linear type theory with graded modalities \(\Box _r A\) (where r is drawn from a semiring) and non-recursive types [27], which we refer to as lgm  i.e., linear-graded-modal. We adapt some of these ideas to a setting which does not have a linear basis, but rather a type system where grades are pervasive (such as the core of Haskell’s linear types extension [8]) alongside recursive algebraic data types and input-output example specifications.

We make the following contributions:

  • We define a synthesis calculus for a core graded type system, adapting the context management scheme of lgm to a fully graded setting (rather than the linear setting) and also addressing recursion, recursive types, and user-defined ADTs, none of which were considered in previous work. Synthesised is proved sound, i.e., synthesised programs are typed by the goal type.

  • We implemented both the core type system as an extension of Granule and implemented the synthesis calculus algorithmically.Footnote 1 We elide full details of the implementation but explain its connection to the formal development.

  • We extend the Granule language to include input-output examples as specifications with first-class syntax (that is type checked), which complements the synthesis algorithm and helps guide synthesis. This also aids our evaluation.

  • We evaluate our tool on a benchmark suite of recursive functional programs leveraging standard data types like lists, streams, and trees. We compare against non-graded synthesis provided by Myth [47].

  • Leveraging our calculus and implementation, we provide a prototype tool for synthesising Haskell programs that use GHC 9’s linear types extension.

Roadmap Section 2 gives a brief overview of proof search in resourceful settings, recalling the ‘resource management problem’. Section 3 then defines a core calculus as the target of our synthesis approach. This type system closely resembles various other graded systems [1, 5, 39, 49] including the core of Linear Haskell [8]. We implemented this system as a language extension of Granule [45].

Section 4 presents a calculus of synthesis rules for our language, showing how grades enforce resource usage potentially leading to pruning of the search space of candidate programs. We also discuss some details of the implementation of our tool. We observe the close connection between synthesis in a graded setting and automated theorem proving for linear logic, allowing us to exploit existing optimisation techniques, such as the idea of a focused proof [4].

Section 5 evaluates our implementation on a set of 46 benchmarks, including several non-trivial programs which use algebraic data types and recursion.

Section 6 demonstrates the practicality and versatility of our approach by retargeting our algorithm to synthesise programs in Haskell from type signatures that use GHC’s linear types extension (which is a graded type system [8]).

2 Overview of Resourceful Program Synthesis

Section 1 discussed synthesising pairs and how graded types could control the number of times assumptions are used in a synthesised term. In a linear or graded setting, synthesis must handle the resource management problem [13, 24]: how do we give a resourceful accounting to the context during synthesis, respecting its constraints? We overview the main ideas for addressing this problem.

Section 1 considered (Cartesian) product types \(\times \), but we now switch to the multiplicative product of linear types, which has the typing rule [20]:

$$\begin{aligned} \frac{\varGamma _1 \vdash t_1 : A \qquad \varGamma _2 \vdash t_2 : B}{\varGamma _1, \varGamma _2 \vdash (t_1, t_2) : A \otimes B}\otimes \end{aligned}$$

Each subterm is typed by different contexts, which are combined by disjoint union: a pair cannot be formed if variables are shared between \(\varGamma _{{\textrm{1}}}\) and \(\varGamma _{{\textrm{2}}}\), preventing the structural behaviour of contraction where variables appear in multiple subterms. Naïvely converting this typing rule into a synthesis rule yields:

$$\begin{aligned} \frac{ \varGamma _1 \vdash A \Rightarrow t_{{\textrm{1}}} \qquad \varGamma _2 \vdash B \Rightarrow t_{{\textrm{2}}} }{ \varGamma _1, \varGamma _2 \vdash A \otimes B \Rightarrow ( t_{{\textrm{1}}} , t_{{\textrm{2}}} )}\otimes _{\textsc {Intro}} \end{aligned}$$

As a declarative specification, this synthesis rule is sufficient. However, this rule embeds a considerable amount of non-determinism when considered from an algorithmic perspective. Reading ‘clockwise’ starting from the bottom-left, given a context \(\varGamma \) and a goal \(A \otimes B\), we have to split \(\varGamma \) into disjoint subparts \(\varGamma _{{\textrm{1}}}\) and \(\varGamma _{{\textrm{2}}}\) such that \(\varGamma = \varGamma _{{\textrm{1}}} , \varGamma _{{\textrm{2}}}\) in order to pass \(\varGamma _{{\textrm{1}}}\) and \(\varGamma _{{\textrm{2}}}\) to the subgoals for A and B. For a context of size n there are \(2^n\) possible such partitions! This quickly becomes intractable. Instead, Hodas and Miller developed a technique for linear logic programming [25], refined by Cervesato et al. [13], where proof search has an input context of available resources and an output context of the remaining resources, which we write as judgments \( \varGamma \vdash A \Rightarrow ^{-} t \mid \varGamma '\) for input context \(\varGamma \) and output context \(\varGamma '\). Synthesis for multiplicative products then becomes:

$$\begin{aligned} \frac{\varGamma _1 \vdash A \Rightarrow ^- t_{{\textrm{1}}} \ |\ \varGamma _{2} \qquad \varGamma _{2} \vdash B \Rightarrow ^- t_{{\textrm{2}}} \ | \ \varGamma _{3} }{ \varGamma _1 \vdash A \, \otimes \, B \Rightarrow ^- ( t_{{\textrm{1}}} , t_{{\textrm{2}}} ) \ | \ \varGamma _{3}}\otimes _{\textsc {Intro}}^{-} \end{aligned}$$

The resources remaining after synthesising the term \( t_{{\textrm{1}}} \) for A are \(\varGamma _{{\textrm{2}}}\), which are then passed as the resources for synthesising the term of goal type B. There is an ordering implicit here in ‘threading through’ the contexts between the premises. For example, starting with a context \( x : A , y : B \), this rule can be instantiated:

figure c

This avoids the problem of splitting the input context, facilitating efficient proof search for linear types. lgm adapted this idea to linear types augmented with graded modalities [27]. We call the above approach subtractive resource management due to its similarity to left-over type-checking for linear types [3, 54]. In a graded modal setting however this approach is costly [27].

Graded type systems, as considered here, have typing contexts in which free variables are assigned a type and a grade: an element of a semiring. For example, the semiring of natural numbers describes how many times an assumption can be used, in contrast to linear assumptions which must be used exactly once, e.g., the context \( x :_{{ 2 } } A , y :_{{ 0 } } B \) says that x must be used twice but y cannot be used. The literature contains many example semirings for tracking other properties as graded types, e.g., security labels [1, 18], intervals of usage [45], and hardware schedules [19]. In a graded setting, the subtractive approach is problematic though as there is not necessarily a notion of subtraction for grades.

Consider the above example but for a context with grades r and s on the variables. Using a variable to synthesise a subterm no longer results in that variable being ‘left out’ of the output context. Instead a new grade is given in the output context relating to the input with a constraint capturing the usage:

figure d

In the first premise, x has grade r in the input context and x is synthesised for the goal, thus the output context has some grade \(r'\) where \(r' + 1 = r\), denoting a use of x by the 1 element of the semiring. The second premise is similar.

For the natural numbers, if \(r = s = 1\) then the above constraints are satisfied by \(r' = s' = 0\). In general, subtractive synthesis for graded types requires solving many such existential equations over semirings, which introduces a new source of non-determinism as there can be more than one solution. lgm implemented this approach, leveraging SMT solving in the context of the Granule language, but show that a dual additive approach has better performance. In the additive approach, output contexts describe what was used instead of what is left. To synthesise a term with multiple subterms (e.g. pairs), the output contexts of each premise are added using the semiring addition applied pointwise on contexts to produce the conclusion output. For pairs this looks like:

$$\begin{aligned} \frac{\varGamma \vdash A \Rightarrow ^+ t_{{\textrm{1}}} \ |\ \varDelta _{1} \qquad \varGamma \vdash B \Rightarrow ^+ t_{{\textrm{2}}} \ |\ \varDelta _{2} }{ \varGamma \vdash A \, \otimes \, B \Rightarrow ^+ ( t_{{\textrm{1}}} , t_{{\textrm{2}}} ) \ |\ \varDelta _{1} + \varDelta _{2}}\otimes _{\textsc {Intro}}^{+} \end{aligned}$$

The whole of \(\varGamma \) is used to synthesise both premises. For example, for goal \( A \, \otimes \, A \):

figure e

Synthesis rules for binders check whether the output context describes use that is within the grades given by \(\varGamma \), i.e., that synthesised terms are well-resourced.

Both subtractive and additive approaches avoid having to split the incoming context \(\varGamma \) prior to synthesising subterms. In lgm, we evaluated both resource management strategies in a synthesis tool for a subset of Granule’s ‘linear base’ system, finding that in most cases, the additive strategy was more efficient for use in program synthesis with grades as it involves solving less complex predicates; the subtractive approach typically incurs higher overhead due to the existentially-derived notion of subtraction seen above. We therefore take the additive approach to resource management.

lgm developed our approach for the linear \(\lambda \)-calculus with products, coproducts, and semiring-graded modalities. Here, we instead consider a graded calculus without a linear base but where all assumptions are graded and function types therefore incorporate a grade. Furthermore, our approach permits synthesis for user-defined recursive ADTs to address more real-world problems.

3 Core Calculus

We define a core language with graded types, drawing from the coeffect calculus of Petricek et al. [49], Quantitative Type Theory (QTT) [5, 39] and other graded dependent type theories [42] (omitting dependent types from our language), the calculus of Abel and Bernardy [1], and the core of the linear types extension to Haskell [8]. This calculus shares much in common with languages based on linear types, such as the graded monadic-comonadic calculus of [18], generalisations of Bounded Linear Logic [12, 19], and Granule [45] in its original ‘linear base’ form.

Our target calculus extends the \(\lambda \)-calculus with grades and a graded necessity modality as well as recursive algebraic data types. Parameterising the calculus is a pre-ordered semiring \((\mathcal {R}, {*}, {1}, {+}, {0}, \sqsubseteq )\) where pre-ordering requires that \(+\) and \(*\) are monotonic wrt. \(\sqsubseteq \). Throughout rs range over \(\mathcal {R}\). The syntax of types is:

figure f

The function space \( A ^{ r } \rightarrow B \) annotates the input type with a grade \( r \in \mathcal {R}\). The graded necessity modality \(\Box _{ r } A \) is similarly annotated/indexed with a grade \( r \). Type constructors K include the multiplicative linear products and units, additive coproducts, and is extended by names of user-defined ADTs in the implementation. Constructors are applied to zero or more type parameters written \(\overline{A}\). Recursive types \(\mu X . A\) are equi-recursive with type recursion variables X. Data constructors and other top-level definitions are typed by type schemes \(\tau \) (rank-1 polymorphic types), which bind a set of kind-annotated universally quantified type variables \(\overline{\alpha : \kappa }\) à la ML [40]. Subsequently, types may contain type variables \(\alpha \). Kinds \(\kappa \) are standard, given in the appendix [28].

The term syntax comprises the \(\lambda \)-calculus, a promotion construct [t] which introduces a graded modality, data constructors (\(C \, t_{{\textrm{1}}} \, ... \, t_{ n } \)), and elimination by \({\textbf {case}}\) expressions with patterns \( p \), where \([ p ]\) eliminates graded modalities:

figure g

Example 1

In the type system (below), the k-combinator is typed as on the left:

$$\begin{aligned} \begin{array}{ll} k : A^{1} \rightarrow B ^{0} \rightarrow A &{} \\ k = \lambda x. \lambda y . x &{} \end{array} \quad \begin{array}{ll} k' : (A \times \Box _0 B)^r \rightarrow \Box _r A &{} \\ k' = \lambda p . {\textbf {case}}\ p\ {\textbf {of}}\ (x, y) \mapsto {\textbf {case}}\ y\ {\textbf {of}}\ [y'] \mapsto [x] \end{array} \end{aligned}$$

On the right, an uncurried version uses graded modalities. The argument pair uses a graded modality to capture that the B part is not used. This graded modal argument is eliminated by the second \({\textbf {case}}\) with pattern \([y']\) binding \(y'\) with grade 0, indicating it is unused. The return result is of graded modal type with some grade r which is introduced by promotion [x]. Promotion propagates its grade to its dependencies, i.e., the parameter p must also have grade r.

A useful semiring is of security levels [1, 18], e.g., \(\mathcal {R} = \{\textsf{Private}, \textsf{Public}\}\) where \(\textsf{Private} \sqsubseteq \textsf{Public}\), \(+ = \wedge \) with \(0 = \textsf{Private}\), and \(*= \vee \) with \(1 = \textsf{Public}\). In the above example, the second argument to k would thus be \(\textsf{Private}\). If the return result of \(k'\) is for public consumption, i.e., \(r = \textsf{Public}\), then the argument must also be public, with the private component B not usable in the result.

Figure 1 defines the typing judgments of the form \(\varSigma ; \varGamma \vdash t : A \) assigning a type \( A \) to a term \( t \) under type variables \(\varSigma \). For such judgments we say that t is both well typed and well resourced to highlight the role of grading in accounting for resource use via the grades. Contexts \(\varGamma \) are given by:

figure h

That is, a context may be empty \(\emptyset \) or extended with a graded assumption \( x :_{{ r } } A \). Graded assumptions must be used in a way which adheres to the grade \( r \). Structural exchange is permitted, allowing a context to be arbitrarily reordered. A global context D parameterises the system, containing top-level definitions and data constructors annotated with type schemes. A context of kind annotated type variables \(\varSigma \) is used for kinding and when instantiating a type scheme from D. Appendix A gives the (standard) kinding relation [28].

Fig. 1.
figure 1

Typing rules

Variables are typed (rule Var) in a context where the variable x has grade 1 denoting its single use here. All other variable assumptions are given the grade of the 0 semiring element (providing weakening), using scalar multiplication:

Definition 1 (Scalar multiplication)

[Scalar multiplication] For a context \(\varGamma \) then \({ r {\,\cdot \,} } \varGamma \) scales each assumption by grade \( r \), where \({ r {\,\cdot \,} } \emptyset = \emptyset \) and \({ r {\,\cdot \,} } ( \varGamma , x :_{{ s } } A ) = ( { r {\,\cdot \,} } \varGamma ) , x :_{{ r {\,\cdot \,} s } } A \).

Top-level definitions (Def) must be present in the global definition context D, with the type scheme \(\forall \overline{\alpha : \kappa } . A' \). The type \( A \) results from instantiating all of the universal variables to types via the judgment \(\varSigma \vdash A = \text {inst}(\forall \overline{\alpha : \kappa } . A' )\) in a standard way as in Algorithm W [40]. Relatedly, the TopLevel rule types top-level definitions with polymorphic type schemes (corresponding to the generalisation rule [40]). Reading bottom up, universally quantified type variables are added to the type variable context to form the type A of the definition term t.

Abstraction (Abs) binds a variable \( x \) which is used in the body \( t \) according to grade \( r \) and thus this grade is captured onto the function arrow in the conclusion. Relatedly, application (App) scales the context \(\varGamma _{{\textrm{2}}}\) of the argument term \( t_{{\textrm{2}}} \) by the grade of the function arrow \( r \) since \( t_{{\textrm{2}}} \) is used according to \( r \) in \( t_{{\textrm{1}}} \, t_{{\textrm{2}}} \). To this scaled context is ‘added’ the context \(\varGamma _{{\textrm{1}}}\) of the function term, via \(+\) defined:

Definition 2 (Context addition)

[Context addition] For contexts \(\varGamma _{{\textrm{1}}}, \varGamma _{{\textrm{2}}}\), then \(\varGamma _{{\textrm{1}}} + \varGamma _{{\textrm{2}}}\) computes the pointwise addition using semiring addition (providing contraction), where:

$$\begin{aligned} \varGamma + \emptyset = \varGamma \qquad ( \varGamma _{{\textrm{1}}} , x :_{{ r } } A ) + ( \varGamma _{{\textrm{2}}} , x :_{{ s } } A ) & = ( \varGamma _{{\textrm{1}}} + \varGamma _{{\textrm{2}}} ) , x :_{{ r + s } } A \\ ( \varGamma _{{\textrm{1}}} , x :_{{ r } } A ) + \varGamma _{{\textrm{2}}} & = ( \varGamma _{{\textrm{1}}} + \varGamma _{{\textrm{2}}} ) , x :_{{ r } } A \qquad \textit{if}\ x \not \in \textsf{dom}(\varGamma _{{\textrm{2}}}) \end{aligned}$$

For example, \(( x :_{{ 1 } } A , y :_{{ 0 } } B ) + x :_{{ 1 } } A = x :_{{ ( 1 + 1 ) } } A , y :_{{ 0 } } B \). The operation is commutative and undefined if the type of a variable differs in two contexts.

Introduction of graded modalities is achieved via promotion (Pr rule) where grade \( r \) is propagated to the assumptions in \(\varGamma \) through the scaling of \(\varGamma \) by \( r \). Approximation (Approx) allows a grade \( r \) to be converted to grade \( s \) provided that \( s \) approximates \( r \) as defined by the pre-order relation \(\sqsubseteq \). This relation is occasionally lifted pointwise to contexts: we write \(\varGamma \ {\sqsubseteq } \ \varGamma '\) to mean that \(\varGamma '\) over-approximates \(\varGamma \), i.e., for all \(( x :_{{ r } } A ) \in \varGamma \) then \(( x :_{{ r' } } A ) \in \varGamma '\) and \( r \, {\sqsubseteq } \, r' \).

Recursion is typed via the \(\mu _1\) rule and its inverse \(\mu _2\), in a standard way.

Introduction of data types (Con) via a constructor \( C \) of a data type \(K \, \overline{ A }\) (with zero or more type parameters) incurs an instantiation of its polymorphic type scheme from D. Each argument has a grade \(q_i\). Constructors are closed, thus have only zero-use grades in the context by scaling with 0. Elimination of data types (Case) is via pattern matching. Patterns \( p \) are typed by the judgement \( r \vdash \, p : A \, \rhd \, \varDelta \) (Figure 2) stating that pattern \( p \) has type \( A \) and produces a context of typed binders \(\varDelta \). The grade \( r \) to the left of the turnstile represents grade information arising from usage in the context generated by this pattern.

Fig. 2.
figure 2

Pattern typing rules

Variable patterns (PVar) produce a singleton context with \( x :_{{ r } } A \) of the grade \( r \). Pattern matches on data constructors (PCon rule) may have zero or more sub-patterns (\( p_{{\textrm{1}}} ... p_{ n } \)), each of which is typed under the grade \( q_{ i } \cdot r \) (where \( q_{ i } \) is the grade of corresponding argument type for the constructor, as defined in D). Additionally, we have the constraint \(| K \, \overline{ A } | > 1 \Rightarrow 1 \sqsubseteq r \) which witnesses the fact that if there is more than one data constructor for the data type (written \(|K \, \overline{ A }| > 1\)), then \( r \) must approximate 1 because pattern matching on a data constructor incurs some usage since it reveals information about that constructor.Footnote 2 By contrast, pattern matching on a type with only one constructor cannot convey any information by itself and so no usage requirement is imposed. Finally, elimination of a graded modality (often called unboxing) takes place via the PBox rule, with syntax \([ p ]\). Like PCon, this rule propagates the grade information of the box pattern’s type \( s \) to the enclosed sub-pattern \( p \), yielding a context with the grades \( r {\,\cdot \,} s \). One may observe that PBox (and by extension Pr) could be considered as special cases of PCon (and Con respectively), if we were to treat promotion as a data constructor with the type \( A ^{ r } \rightarrow \Box _{ r } A \). We however chose to keep modal introduction and elimination distinct from constructors.

Example 2

Discussed early, the natural numbers semiring with discrete ordering \(({\mathbb {N}}, {*}, {1}, {+}, {0}, {\equiv })\) counts exactly how many times variables are used. We denote this semiring as \(\mathbb {N}_\equiv \). This semiring is less useful in the presence of control-flow, e.g., for multiple branches in a \({\textbf {case}}\) using variables differently. A semiring of natural number intervals [45] is more helpful here. An interval is a pair of natural numbers \(\mathbb {N} \times \mathbb {N}\) written \( r ... s \) for lower bound \( r \in \mathbb {N}\) and upper bound by \( s \in \mathbb {N} \). Addition is defined pointwise with zero \(0 = 0 ... 0\) and multiplication defined as in interval arithmetic with \(1 = 1 ... 1\) and ordering \( r ... s \sqsubseteq r' ... s' = r' \le r \wedge s \le s' \). This semiring allows us to write a function which performs an elimination on a coproduct (assuming \(\textsf{inl} : A^1 \rightarrow A \oplus B\), and \(\textsf{inr} : B^1 \rightarrow A \oplus B\) in D):

$$\begin{aligned} \begin{array}{ll} \oplus _{elim} : (A^{1} \rightarrow C)^{0...1} \rightarrow (B^{1} \rightarrow C)^{0...1} \rightarrow (A \oplus B)^{1} \rightarrow C &{} \\ \oplus _{elim} = \lambda f. \lambda g . \lambda x. {\textbf {case }} x {\textbf { of }} \textsf {inl}\ y \mapsto f\ y;\ \textsf {inr}\ z \mapsto g\ z&{} \end{array} \end{aligned}$$

Example 3

The ! modality of linear logic can be (almost) recovered via the {0,1,\(\omega \)} semiring where \(0 \sqsubseteq \omega \) and \(1 \sqsubseteq \omega \). Addition is \(r + s = r\) if \(s = 0\), \(r + s = s\) if \(r = 0\), otherwise \(\omega \). Multiplication is \(r \cdot 0 = 0 \cdot r = 0\), \(r \cdot \omega = \omega \cdot r = \omega \) (where \(r \ne 0\)), and \(r \cdot 1 = 1 \cdot r = r\). This semiring expresses linear and non-linear usage, where 1 indicates linear use, 0 requires the value be discarded, and \(\omega \) acts as linear logic’s ! permitting arbitrary use. This is similar to Haskell’s multiplicity annotations, although they have no equivalent of a 0 grade, with only and grades [8]. Some additional restrictions are required on pattern typing to get exactly the behaviour of ! with respect to products [26], not considered here.

Lastly we note that the calculus enjoys admissibility of substitution [1] which is critical in type preservation proofs, and is needed for soundness of synthesis:

Lemma 1 (Admissibility of substitution)

[Admissibility of substitution] Let \(\varDelta \vdash t' : A \), then: If \(\varGamma , x :_{{ r } } A , \varGamma ' \vdash t : B \) then \(\varGamma + ( { r {\,\cdot \,} } \varDelta ) + \varGamma ' \vdash [ t' / x ] t : B \)

4 Synthesis Calculus

Having defined the target language, we define our synthesis calculus, which uses the additive approach to resource management (see Section 2), with judgments:

$$\begin{aligned} \varSigma ; \varGamma \vdash A \Rightarrow t \mid \varDelta \end{aligned}$$

That is, given an input context \(\varGamma \), for goal type \( A \) we can synthesise the term \( t \) with output context \(\varDelta \) describing how variables were used in \( t \). As in typing, top-level definitions and data constructors in scope are provided by a set D parameterising the system. \(\varSigma \) is a context of type variables, which we elide when it is simply passed inductively to the premise(s). The context \(\varDelta \) need not use the variables in \(\varGamma \) with the same grades. Instead, the relationship between synthesis and typing is given by the central soundness result, which we state up-front: that synthesised terms are typed by their goal type under their output context:

Theorem 1 (Soundness)

[Soundness] For all pre-ordered semirings \(\mathcal {R}\):

  1. 1.

    For all contexts \(\varGamma \) and \(\varDelta \), types \( A \), terms \( t \):

    $$\begin{aligned} \varSigma ; \varGamma \vdash A \Rightarrow t \mid \varDelta \quad \implies \quad \varSigma ; \varDelta \vdash t : A \end{aligned}$$
  2. 2.

    At the top-level, for all type schemes \(\forall \overline{\alpha : \kappa } . A \) and terms \( t \) then:

    $$\begin{aligned} \emptyset ; \emptyset \vdash \forall \overline{\alpha : \kappa } . A \Rightarrow t \mid \emptyset \quad \implies \quad \emptyset ; \emptyset \vdash t : \forall \overline{\alpha : \kappa } . A \end{aligned}$$

Appendix D of the additional material provides the soundness proof [28], which in part resembles a translation from sequent calculus to natural deduction, but also with the management of grades between synthesis and type checking.

The first part of soundness on its own does not guarantee that a synthesised program t is well resourced, i.e., the grades in \(\varDelta \) may not be approximated by grades in \(\varGamma \). For example, for semiring \(\mathbb {N}_\equiv \) a valid judgement is:

$$\begin{aligned} x :_{{ 2 } } A \vdash A \Rightarrow x \mid x :_{{ 1 } } A \end{aligned}$$

i.e., for goal A, if x has type A in the context then we synthesis x as the resulting program, regardless of the grades. Such a synthesis judgement may be part of a larger derivation in which the grades eventually match due to a further subderivation, e.g., using x again and thus total usage for x is eventually 2 as prescribed by the input context. However, at the level of an individual judgement we do not guarantee that the synthesised term is well-resourced with respect to the input context. A reasonable pruning condition to assess whether any synthesis judgement is potentially well-resourced is \(\exists \varDelta ' . ( \varDelta + \varDelta ' ) \ {\sqsubseteq } \ \varGamma \), i.e., there is some additional usage \(\varDelta '\) (that might come from further on in the synthesis process) that ‘fills the gap’ in resource use to produce \(\varDelta + \varDelta '\) which is overapproximated by \(\varGamma \). In this example, \(\varDelta ' = x :_{{ 1 } } A \) would satisfy this constraint, explaining that there is some further possible single usage which will satisfy the incoming grade. However, our previous work on graded linear types showed that excessive pruning at every step becomes too costly in a general setting [27]. Instead, we apply such pruning more judiciously, only requiring that variable use is well-resourced at the point of synthesising binders. Therefore synthesised closed terms are always well-resourced (second part of the soundness theorem).

We next present the synthesis calculus in stages. Each type former of the core calculus (with the exception of type variables) has two corresponding synthesis rules: a right rule for introduction (labelled \(\textsc {R}\)) and a left rule for elimination (labelled \(\textsc {L}\)). We frequently apply the algorithmic reading of the judgments, where meta variables to the left of \(\Rightarrow \) are inputs (i.e., context \(\varGamma \) and goal type \( A \)) and terms to the right of \(\Rightarrow \) are outputs (i.e., the synthesised term \( t \) and the usage context \(\varDelta \)). Whilst we largely present the approach here in abstract terms, via the synthesis judgments, we highlight some choices made in our implementation (e.g., heuristics applied in the algorithmic version of the rules).

4.1 Core Synthesis Rules

Top-level We begin with synthesis from a type scheme goal (which is technically a separate judgment form), providing the entry-point to synthesis:

$$\begin{aligned} \frac{\overline{\alpha : \kappa } ; \emptyset \vdash A \Rightarrow t \mid \emptyset }{\emptyset ; \emptyset \vdash \forall \overline{\alpha : \kappa } . A \Rightarrow t \mid \emptyset }{\textsc {TopLevel}} \end{aligned}$$

The universally-quantified type variables \(\overline{\alpha : \kappa }\) are thus added to the type variable context of the premise (note, type variables are only equal to themselves).

Variables For any goal type A, if there is a variable in the context matching this type then it can be synthesised for the goal, given by a terminal rule:

$$\begin{aligned} \frac{ \varSigma \vdash A : \textsf {Ty} }{ \varSigma ; \varGamma , x :_{{ r } } A \vdash A \Rightarrow x \mid { 0 {\,\cdot \,} } \varGamma , x :_{{ 1 } } A }{\textsc {Var}} \end{aligned}$$

Said another way, to synthesise the use of a variable \( x \), we require that \( x \) be present in the input context \(\varGamma \). The output context here then explains that only variable x is used: it consists of the entirety of the input context \(\varGamma \) scaled by grade 0 (using definition 1), extended with \( x :_{{ 1 } } A \), i.e. a single usage of \( x \) as denoted by the 1 element of the semiring. Maintaining this zeroed \(\varGamma \) in the output context simplifies subsequent rules by avoiding excessive context membership checks.

The \(\textsc {Var}\) rule permits synthesis of terms which may not be well-resourced, e.g., if \(r = 0\), the rule still synthesises a use of x. As discussed at this section’s start, this may be locally ill-resourced, but is acceptable at the global level as we check that an assumption has been used correctly when it is bound. This reduces the number of intermediate theorems that need solving (previously shown to be expensive [27], especially since the variable rule is applied very frequently), but increases the number of paths that are ill-resourced so must be pruned later.

The use of a top-level polymorphic function is synthesised if it can be instantiated to match the goal type:

$$\begin{aligned} \frac{ (x : \forall \overline{\alpha : \kappa } . A' ) \in D \quad \;\; \varSigma \vdash A = \text {inst}(\forall \overline{\alpha : \kappa } . A' ) }{ \varSigma ; \varGamma \vdash A \Rightarrow x \mid { 0 {\,\cdot \,} } \varGamma }{\textsc {Def}} \end{aligned}$$

For example, assuming \(\textit{flip} : \forall c : \text {Type}, d : \text {Type} . (c \otimes d)^1 \rightarrow (d \otimes c) \in D\) then \(\textit{flip}\) is synthesised for a goal type of \((K_1 \otimes K_2)^1 \rightarrow (K_2 \otimes K_1)\) for some type constants \(K_1\) and \(K_2\), via the instantiation \(\emptyset \vdash (K_1 \otimes K_2)^1 \rightarrow (K_2 \otimes K_1) = \text {inst}(\forall c : \text {Type}, d : \text {Type} . (c \otimes d)^1 \rightarrow (d \otimes c))\).

Recursion is provided by populating D with the name and type of the definition currently being synthesised for (see Section 4.2 for implementation details).

Functions Synthesis from function types is handled by the \(\rightarrow _{\textsc {R}}\)rule:

$$\begin{aligned} \frac{\varGamma , x :_{{q}}{A}\vdash {B} \Rightarrow {t} \mid \varDelta , x :_{{r}} {A} \quad \;\;{r} \, {\sqsubseteq } \, {q} }{\varGamma \vdash {A} ^{ {q} } \rightarrow {B} \Rightarrow \lambda x . {t} \mid \varDelta }{\rightarrow _{\textsc {R}}} \end{aligned}$$

Reading bottom up, to synthesise a term of type \( A ^{ q } \rightarrow B \) in context \(\varGamma \) we first extend the context with a fresh variable assumption \( x :_{{ q } } A \) and synthesise a term of type \( B \) that will ultimately become the body of the function. The type \( A ^{ q } \rightarrow B \) conveys that \( A \) must be used according to \( q \) in our term for \( B \). The fresh variable \( x \) is passed to the premise of the rule using the grade of the binder: \( q \). The \( x \) must then be used to synthesise a term \( t \) with \( q \) usage. In the premise, after synthesising \( t \) we obtain an output context \(\varDelta , x :_{{ r } } A \). As mentioned, the \(\textsc {Var}\) rule ensures that \( x \) is present in this context, even if it was not used in the synthesis of \( t \) (e.g., \( r = 0\)). The rule ensures the usage of bound term (r) in \( t \) does not violate the input grade q via the requirement that \( r \sqsubseteq q \) i.e. that \( r \) is approximated by \( q \). If met, \(\varDelta \) becomes the output context of the rule’s conclusion.

Function application is synthesised from functions in the context (a left rule):

$$\begin{aligned} \frac{\begin{array}{l} \varGamma , x :_{{ r_{{\textrm{1}}} } } A ^{ q } \rightarrow B , y :_{{ r_{{\textrm{1}}} } } B \vdash C \Rightarrow t_{{\textrm{1}}} \mid \varDelta _{{\textrm{1}}} , x :_{{ s_{{\textrm{1}}} } } A ^{ q } \rightarrow B , y :_{{ s_{{\textrm{2}}} } } B \\ \varGamma , x :_{{ r_{{\textrm{1}}} } } A ^{ q } \rightarrow B \vdash A \Rightarrow t_{{\textrm{2}}} \mid \varDelta _{{\textrm{2}}} , x :_{{ s_{{\textrm{3}}} } } A ^{ q } \rightarrow B \end{array}}{\varGamma , x :_{{ r_{{\textrm{1}}} } } A ^{ q } \rightarrow B \vdash C \Rightarrow [ ( x \, t_{{\textrm{2}}} ) / y ] t_{{\textrm{1}}} \mid ( \varDelta _{{\textrm{1}}} + { s_{{\textrm{2}}} {\,\cdot \,} } { q {\,\cdot \,} } \varDelta _{{\textrm{2}}} ) , x :_{{ s_{{\textrm{2}}} + s_{{\textrm{1}}} + ( s_{{\textrm{2}}} {\,\cdot \,} q {\,\cdot \,} s_{{\textrm{3}}} ) } } A ^{ q } \rightarrow B }{\rightarrow _{\textsc {L}}} \end{aligned}$$

Reading bottom up, the input context contains an assumption of function type \( x :_{{ r_{{\textrm{1}}} } } A ^{ q } \rightarrow B \). An application of \( x \) can be synthesised if an argument \(t_2\) can be synthesised for the input type \( A \) (second premise). The goal type \( C \) is synthesised (first premise), under the assumption of a result of type \( B \) bound to \( y \). In the conclusion, a term is synthesised which substitutes in \( t_{{\textrm{1}}} \) the result placeholder variable \( y \) for the application \( x \, t_{{\textrm{2}}} \).

We explain the concluding output context in two stages. Firstly, the output context \(\varDelta _{{\textrm{1}}}\) of the first premise is added to a scaled \(\varDelta _{{\textrm{2}}}\). Since \(\varDelta _{{\textrm{2}}}\) are the resources used by the synthesised argument \( t_{{\textrm{2}}} \), this context is scaled by \( q \) as \( t_{{\textrm{2}}} \) is used according to \( q \) by \( x \) as per its type. This context is further scaled by \( s_{{\textrm{2}}} \) which is the usage of the entire application \( x \, t_{{\textrm{2}}} \) inside \( t_{{\textrm{1}}} \) as given by the output grade for y in the first premise. Secondly, the output context calculates the use of \( x \) used in the application itself and potentially also by both premises (which differs from lgm’s treatment of synthesis in a linear setting). Apart from application, \( x \) may be used also to synthesise the argument \( t_{{\textrm{2}}} \), calculated as grade \( s_{{\textrm{3}}} \) in the second premise. Thus, the application accrues \( q {\,\cdot \,} s_{{\textrm{3}}} \) use. Furthermore as the result \( y \) is used according to \( s_{{\textrm{2}}} \), we must further scale by \( s_{{\textrm{2}}} \), obtaining \( s_{{\textrm{2}}} {\,\cdot \,} q {\,\cdot \,} s_{{\textrm{3}}} \). To this we must also add the additional usage of \( x \) in the first premise \( s_{{\textrm{1}}} \) as well as the use of \( x \) in actually performing application, which is 1 scaled by \( s_{{\textrm{2}}} \) to account for the usage of its result, thus obtaining the output grade for x. Following the soundness proof for this rule (Appendix D) can be instructive.

The declarative rule above does not imply an ordering of whether \(t_1\) or \(t_2\) is synthesised first. As a heuristic, the implementation first attempts to synthesise \( t_{{\textrm{1}}} \) assuming \( y :_{{ r_{{\textrm{1}}} } } B \) according to the first premise to avoid possibly unnecessary work if no term can be synthesised anyway for \( C \).

Example 4

Let \(T = ( A \, \otimes \, A ) ^{ { 0 }..{ 1 } } \rightarrow A \) type an assumption \(\textsf{fst}\) in a use of \(\rightarrow _{\textsc {L}}\):

$$\begin{aligned}\begin{gathered} \dfrac{ \begin{array}{l} z :_{{ s } } A , \textsf{fst} :_{{ r } } T , y :_{{ r } } A \vdash A \, \otimes \, A \Rightarrow ( y , y ) \mid z :_{{ 0 } } A , \textsf{fst} :_{{ 0 } } T , y :_{{ 2 } } A \\ z :_{{ s } } A , \textsf{fst} :_{{ r } } T \qquad \quad \vdash A \Rightarrow ( z , z ) \mid z :_{{ 2 } } A , \textsf{fst} :_{{ 0 } } T \end{array} }{ z :_{{ s } } A , \textsf{fst} :_{{ r } } T \vdash A \, \otimes \, A \Rightarrow ( \textsf{fst} \, ( z , z ) , \textsf{fst} \, ( z , z ) ) \mid z :_{{ 0 + 2 {\,\cdot \,} ( { 0 }..{ 1 } ) {\,\cdot \,} 2 } } A , \textsf{fst} :_{{ 2 + 0 + ( 2 {\,\cdot \,} ( { 0 }..{ 1 } ) {\,\cdot \,} 0 ) } } T} \end{gathered}\end{aligned}$$

In this instantiation of the (\(\rightarrow _{\textsc {L}}\)) rule, \( q = { 0 }..{ 1 }\) and \( s_{{\textrm{1}}} = s_{{\textrm{3}}} = 0\), i.e., the function \(\textsf{fst}\) is not used in the subterms, and \( s_{{\textrm{2}}} = 2\), i.e., the result y of \(\textsf{fst}\) is used twice. In the conclusion then, z then has output grade \(0 + 2 {\,\cdot \,} ( { 0 }..{ 1 } ) {\,\cdot \,} 2 = { 0 }..{ 4 }\), i.e., it is used up to four times and \(\textsf{fst}\) has grade \({ 2 }..{ 2 }\), i.e., it is used twice.

Graded Modalities Graded modalities are introduced through the \(\Box _{\textsc {R}}\)rule, synthesising a promotion [t] for some graded modal type \(\Box _r A\):

$$\begin{aligned} \frac{\varGamma \vdash A \Rightarrow t \mid \varDelta }{\varGamma \vdash \Box _{ r } A \Rightarrow [ t ] \mid { r {\,\cdot \,} } \varDelta }{\Box _{\textsc {R}}} \end{aligned}$$

The premise synthesises term t from \( A \) with output context \(\varDelta \). In the conclusion, \(\varDelta \) is scaled by the grade \( r \) of the goal type since \([ t ]\) must use \( t \) as \( r \) requires.

Grade elimination (unboxing) takes place via pattern matching in case:

$$\begin{aligned} \frac{\begin{array}{l} \varGamma , y :_{{ r {\,\cdot \,} q } } A , x :_{{ r } } \Box _{ q } A \vdash B \Rightarrow t \mid \varDelta , y :_{{ s_{{\textrm{1}}} } } A , x :_{{ s_{{\textrm{2}}} } } \Box _{ q } A \\ \exists s_{{\textrm{3}}} .\, s_{{\textrm{1}}} \sqsubseteq s_{{\textrm{3}}} {\,\cdot \,} q \sqsubseteq r {\,\cdot \,} q \end{array}}{\varGamma , x :_{{ r } } \Box _{ q } A \vdash B \Rightarrow {\textbf {case}} \ x \ {\textbf {of}} \ [ y ] \rightarrow t \mid \varDelta , x :_{{ s_{{\textrm{3}}} + s_{{\textrm{2}}} } } \Box _{ q } A }{\Box _{\textsc {L}}} \end{aligned}$$

To eliminate an assumption \( x \) of graded modal type \(\Box _{ q } A \), we bind a fresh assumption the premise: \( y :_{{ r {\,\cdot \,} q } } A \). This assumption is graded with \( r {\,\cdot \,} q \): the grade from the assumption’s type multiplied by the grade of the assumption itself. As with previous elimination rules, \( x \) is rebound in the rule’s premise. A term \( t \) is then synthesised resulting in the output context \(\varDelta , y :_{{ s_{{\textrm{1}}} } } A , x :_{{ s_{{\textrm{2}}} } } \Box _{ q } A \), where \( s_{{\textrm{1}}} \) and \( s_{{\textrm{2}}} \) describe how \( y \) and \( x \) were used in \( t \). The second premise ensures that the usage of \( y \) is well-resourced. The grade \( s_{{\textrm{3}}} \) represents how much the usage of \( y \) inside \( t \) contributes to the overall usage of \( x \). The constraint \( s_{{\textrm{1}}} \sqsubseteq s_{{\textrm{3}}} {\,\cdot \,} q \) conveys the fact that \( q \) uses of \( y \) constitutes a single use of \( x \), with the constraint \( s_{{\textrm{3}}} {\,\cdot \,} q \sqsubseteq r {\,\cdot \,} q \) ensuring that the overall usage does not exceed the binding grade. For the output context of the conclusion, we simply remove the bound \( y \) from \(\varDelta \) and add \( x \), with the grade \( s_{{\textrm{2}}} + s_{{\textrm{3}}} \) representing the total usage of \( x \) in \( t \).

Data Types The synthesis of introduction forms for data types is by the \(\textsc {C}_{\textsc {R}}\)rule:

$$\begin{aligned} \frac{\begin{array}{l} ( C : \forall \overline{\alpha : \kappa }. {B'_{{\textrm{1}}}} ^ q_{{\textrm{1}}} \rightarrow ... \rightarrow {B'_{ n }} ^ q_{ n } \rightarrow K \, \overline{ {A'} } ) \in D \\ \varSigma \vdash B_{{\textrm{1}}} ^{q_1} \rightarrow \! ... \! \rightarrow B_{ n } ^{q_n} \rightarrow K \, \overline{ A } = \text {inst}(\forall \overline{\alpha : \kappa } . {B'_{{\textrm{1}}}} ^{q_1} \rightarrow \! ... \! \rightarrow {B'_{ n }} ^{q_n} \rightarrow K \, \overline{ {A'} } ) \\ \varSigma ; \varGamma \vdash B_{ i } \Rightarrow t_{ i } \mid \varDelta _{ i } \end{array}}{ \varSigma ; \varGamma \vdash K \, \overline{ A } \Rightarrow C \, t_{{\textrm{1}}} \, ... \, t_{ n } \mid { 0 {\,\cdot \,} } \varGamma + ( { q_{{\textrm{1}}} {\,\cdot \,} } \varDelta _{{\textrm{1}}} ) + \, ... \, + ( { q_{ n } {\,\cdot \,} } \varDelta _{ n } ) }{\textsc {C}_{\textsc {R}}} \end{aligned}$$

where D is the set of data constructors in global scope, e.g., coming from ADT definitions, including here products, unit, and coproducts with \((,) : A^1 \rightarrow B^1 \rightarrow A \otimes B\), \(\textsf{unit} : \textsf{Unit}\), \(\textsf{inl} : A^1 \rightarrow A \oplus B\), and \(\textsf{inr} : B^1 \rightarrow A \oplus B\).

For a goal type \(K \, \overline{ A }\) where K is a data type with zero or more type arguments (denoted by the vector \(\overline{ A }\)), then a constructor term \(C \, t_{{\textrm{1}}} \, .. \, t_{ n } \) for \(K \, \overline{ A }\) is synthesised. The type scheme of the constructor in D is first instantiated (similar to Def rule), yielding a type \( B_{{\textrm{1}}} ^{q_1} \rightarrow \! ... \! \rightarrow B_{ n } ^{q_n} \rightarrow K \, \overline{ A } \). A sub-term is then synthesised for each of the constructor’s arguments \( t_{ i } \) in the third premise (which is repeated for each instantiated argument type \( B_{ i } \)), yielding output contexts \(\varDelta _{ i }\). The output context for the rule’s conclusion is obtained by performing a context addition across all the output contexts generated from the premises, where each context \(\varDelta _{ i }\) is scaled by the corresponding grade \( q_{ i } \) from the data constructor in D capturing the fact that each argument \( t_{ i } \) is used according to \( q_{ i } \).

Data type elimination synthesises case expressions, pattern matching on each data constructor of the goal data type \(K \, \overline{ A }\), with various constraints on grades. In the rule, we use the least-upper bound (lub) operator \(\sqcup \) on grades, which is defined wrt. \(\sqsubseteq \) and may not always be defined:

$$\begin{aligned} \frac{\begin{array}{l} ( C_{ i } : \forall \overline{\alpha : \kappa }. {B'_{{\textrm{1}}}} ^{q_1} \rightarrow ... \rightarrow {B'_{ n }} ^{q_n} \rightarrow K \, \overline{ {A'} } ) \in D \quad \;\; \varSigma \vdash K \, \overline{ A } : \textsf {Ty} \\ \varSigma \vdash B_{{\textrm{1}}} ^{q_1} \rightarrow \! ... \! \rightarrow B_{ n } ^{q_n} \rightarrow K \, \overline{ A } = \text {inst}(\forall \overline{\alpha : \kappa } . {B'_{{\textrm{1}}}} ^{q_1} \rightarrow \! ... \! \rightarrow {B'_{ n }} ^{q_n} \rightarrow K \, \overline{ {A'} } ) \\ \varSigma ; \varGamma , x :_{{ r } } K \, \overline{ A } , { y ^ i _ 1 }:_{{ r {\,\cdot \,} q ^ i _ 1 } } B_{{\textrm{1}}} , ... , { y ^ i _ n }:_{{ r {\,\cdot \,} q ^ i _ 1 } } B_{ n } \vdash B \Rightarrow t_{ i } \mid \varDelta _{ i } , x :_{{ r_{ i } } } K \, \overline{ A } , { y ^ i _ 1 }:_{{ s ^ i _ 1 } } B_{{\textrm{1}}} , ... , { y ^ i _ n }:_{{ s ^ i _ n } } B_{ n } \\ \exists {s'} ^ i _ j .\, s ^ i _ j \sqsubseteq {s'} ^ i _ j {\,\cdot \,} q ^ i _ j \sqsubseteq r {\,\cdot \,} q ^ i _ j \quad \;\; s _ i = {s'} ^ i _ 1 \sqcup ... \sqcup {s'} ^ i _ n \quad \;\; | K \, \overline{ A } | > 1 \Rightarrow 1 \sqsubseteq s _ 1 \sqcup ... \sqcup s _ m \end{array}}{ \varSigma ; \varGamma , x :_{{ r } } K \, \overline{ A } \vdash B \Rightarrow {\textbf {case}} \ x \ {\textbf {of}} \ \overline{ C_{ i } \ y ^ i _ 1 ... y ^ i _ n \mapsto t_{ i } } \mid ( \varDelta _{{\textrm{1}}} \sqcup ... \sqcup \varDelta _{ m } ) , x :_{{ \bigsqcup r _i + \bigsqcup s _i } } K \, \overline{ A } }{\textsc {C}_{\textsc {L}}} \end{aligned}$$

where \(1 \le i \le m\) indexes data constructors of which there are m (i.e., \(m = |K \, \overline{ A }|\)) and \(1 \le j \le n\) indexes arguments of the \(i^{th}\) data constructor, thus n depends on i. The rule considers data constructors where \(n > 0\) for brevity.

The relevant data constructors \( C_{ i } \) are retrieved from the global scope D in the first premise. Each polymorphic type scheme is instantiated to a monomorphic type. The monomorphised type for each i is a function from constructor arguments \( B_{{\textrm{1}}} \ldots B_{ n } \) to the applied type constructor \(K \, \overline{ A }\). For each \(C_{i}\), we synthesise a term \( t_{ i } \) from this result type \(K \, \overline{ A }\), binding the data constructor’s argument types as fresh assumptions to be used in the synthesis of \( t_{ i } \). The grades of each argument are scaled by \( r \). This follows the pattern typing rule for constructors; a pattern match under some grade \( r \) must bind assumptions that have the capability to be used according to \( r \). The assumption being eliminated \( x :_{{ r } } K \, \overline{ A }\) is also included in the premise’s context (as in \(\rightarrow _{\textsc {L}}\)) as we may perform additional eliminations on the current assumption subsequently.

The output context for each branch can be broken down into three parts:

  1. 1.

    \(\varDelta _{ i }\) contains any assumptions from \(\varGamma \) were used to construct \( t_{ i } \);

  2. 2.

    \( x :_{{ r_{ i } } } K \, A \) describes how the assumption \( x \) was used;

  3. 3.

    \({ y ^ i _ 1 }:_{{ s ^ i _ 1 } } B_{{\textrm{1}}} , ... , { y ^ i _ n }:_{{ s ^ i _ n } } B_{ n } \) describes how each assumption \( y ^ i_j\) bound in the pattern match was used in \( t_{ i } \) according to grade \(s^i_j\).

For the concluding output context, we take the least-upper bound of the shared output contexts \(\varDelta _{ i }\) of the branches. This is extended with the grade for x which requires some calculation. For each bound assumption, we generate a fresh grade variable \({s'}^{i}_{j}\) which represents how that variable was used in \( t_{ i } \) after factoring out the multiplication by \( q ^ i _ j\). This is done via the constraint in the third premise that \( \exists {s'}^{i}_{j}.\,\textit{s}^{i}_{j}\sqsubseteq {s'}^{i}_{j}{\,\cdot \,}{} \textit{q}^{i}_{j}\sqsubseteq \textit{r}{\,\cdot \,}{} \textit{q}^{i}_{j}\). The lub of \({s'}^{i}_{j}\) for all j is then taken to form a grade variable \( s _ i\) which represents the total usage of \( x \) for branch i arising from the use of assumptions bound via the pattern match (i.e., not usage that arises from reusing \( x \) explicitly inside \( t_{ i } \)). The final grade for x is then the lub of each \( r_{ i } \) (the usages of \( x \) directly in each branch) plus the lub of each \( s_{ i } \) (the usages of the assumptions that were bound from matching on a constructor of \( x \)).

Example 5

(case synthesis) Consider two possible synthesis results:

$$\begin{aligned} & x :_{{ r } } A \, \oplus \, \textsf{Unit} , y :_{{ s } } A , z :_{{ r {\,\cdot \,} q_{{\textrm{1}}} } } A \vdash A \Rightarrow z \mid x :_{{ 0 } } A \, \oplus \, \textsf{Unit} , y :_{{ 0 } } A , z :_{{ 1 } } A \end{aligned}$$
(1)
$$\begin{aligned} & x :_{{ r } } A \, \oplus \, \textsf{Unit} , y :_{{ s } } A \qquad \quad \vdash A \Rightarrow y \mid x :_{{ 0 } } A \, \oplus \, \textsf{Unit} , y :_{{ 1 } } A \end{aligned}$$
(2)

We will plug these into the rule for generating case as follows, where \(\varSigma \) has been elided and instead of using the above concrete grades we have used the abstract form of the rule (the two will be linked by equations after):

figure k

To unify (1) and (2) with the \(\textsc {C}_{\textsc {L}}\) rule format \( s_{{\textrm{1}}} = 1\) and \( q_{{\textrm{1}}} = 1\) (from the type of \(\textsf {inl}\)). Applying these equalities to the existential constraint we have

$$\begin{aligned} & \exists s'_{{\textrm{1}}} .\, 1 \sqsubseteq ( s'_{{\textrm{1}}} {\,\cdot \,} 1 ) \sqsubseteq ( r {\,\cdot \,} 1 ) \quad \implies \quad \exists s'_{{\textrm{1}}} .\, 1 \sqsubseteq s'_{{\textrm{1}}} \sqsubseteq r \end{aligned}$$

With the natural-number intervals semiring this is satisfied by \( s'_{{\textrm{1}}} = { 1 }..{ 1 } = s' \) and thus in the output context \( x \) has grade 1..1 and \( y \) has grade 0..1.

Recursive Types Though \(\mu \) types are equi-recursive, we define explicit synthesis rules to facilitate the implementation (Section 4.2) where depth information needs to be tracked when employing the following \(\mu _\textsc {L}\) and \(\mu _\textsc {R}\) rules:

$$\begin{aligned} \begin{array}{cc} \frac{\varGamma \vdash A [ \mu X . A / X] \Rightarrow t \mid \ \varDelta }{\varGamma \vdash \mu X . A \Rightarrow t \mid \ \varDelta }\mu _\textsc {R} &\quad \frac{ \varGamma , x :_r A [\mu X . A / X ] \vdash B \Rightarrow t \mid \ \varDelta }{\varGamma , x :_r \mu X . A \vdash B \Rightarrow t \mid \ \varDelta }\mu _\textsc {L} \end{array} \end{aligned}$$

To synthesise a recursive data structure of type \(\mu X . A \), we must be able to synthesise \( A \) with \(\mu X . A \) substituted for the recursion variable X in \( A \). For example, if we wish to synthesise a list typed (where ) then when synthesising a constructor in the \(\mu _\textsc {R}\) rule, we must re-apply the \(\mu _\textsc {R}\) rule to synthesise the recursive argument. Elimination of a value \(\mu X . A \) in the context is via the \(\mu _\textsc {L}\), which expands the recursive type in the synthesis context.

4.2 Algorithmic Implementation

The calculus presented above serves as a starting point for our implemented synthesis algorithm in Granule. However, the rules are highly non-deterministic with regards their order in which they may be applied. For example, after applying a (\(\rightarrow _{\textsc {R}}\ \))-rule, we may choose to apply any of the elimination rules before applying an introduction rule for the goal type. This leads to us exploring a large number of redundant search branches which can be avoided through the application of a technique known as focusing [4]. Focusing is a tool from linear logic proof theory based on the idea that some rules are invertible, i.e., whenever the conclusion of the rule is derivable, then so are the premises. In other words, the order in which we apply invertible rules doesn’t matter. By fixing a particular ordering on the application of invertible rules, we eliminate much of the non-determinism that arises from trying branches which differ only in the order in which invertible rules are applied. The full focusing versions of the rules from our calculus, and their proof of soundness, can be found in Appendix E [28]. This forms the basis of our implementation with the high-level algorithm given in appendix Figure 5 as a (non-deterministic) finite state machine, which shows the ordering given to the rules under the focussing approach, starting with trying to synthesise function types before switching to eliminations rules, and so on. In standard terminology, our algorithm is ‘top-down’ (see, e.g., [17, 23, 47, 53]), or goal-directed, in which we start with a type goal and an input context and progress by gradually building the syntax tree from the empty term following the focussing-ordered rules of our calculus. This contrasts with ‘bottom-up’ approaches [2, 41, 44] which maintain complete programs which can be executed (tested) and combined.

Where transitions are non-deterministic in the algorithm, multiple branches are then explored in synthesis. Our implementation relies on the use of backtracking proof search, leveraging a monadic interface that provides both choice (e.g., between multiple possible synthesis options based on the goal type) and failure (e.g., when a constraint fails to hold) [33]. For every rule that generates a constraint on grades, due to binding (\(\Box _{\textsc {L}}\), \(\rightarrow _{\textsc {R}}\), \(\textsc {C}_{\textsc {L}}\)), we compile the constraints to the SMT-lib format [7] which are then discharged by the Z3 SMT solver [43]. If the constraint is invalid then we trigger the failure of this synthesis pathway, triggering backtracking via the “logic” monad [33]. A synthesised program can also be rejected by user (or due to a failing an example, see below) and synthesis then produces an alternate result (what we call a retry) via backtracking.

Recursive data structures present a challenge in the implementation. For example, for the list data type, how do we prevent synthesis from applying the \(\mu _\textsc {L}\) rule, followed by the \(\textsc {C}_\textsc {L}\) rule on the constructor ad infinitum? We resolve this issue using an iterative deepening approach similar to that used by Myth [48]. Programs are synthesised with elimination (and introduction) forms of constructors restricted up to a given depth. If no program is synthesised within these bounds, then the depth limits are incremented. The current depth and the depth limit are part of the state of the synthesiser. Combined with focusing this provides the basis an efficient implementation of the synthesis calculus. Furthermore, to ensure that a synthesised programs terminates, we only permit synthesis of recursive function calls which are structurally recursive, i.e., those which apply the recursive definition to a subterm of the function’s inputs [48].

Lastly, after synthesis, a post-synthesis refactoring step runs to simplify terms and produce a more idiomatic style. For example for the k combinator type signature we synthesis the term: . Our refactoring procedure collects the outermost abstractions of a synthesised term and transforms them into equation-level patterns with the innermost abstraction body forming the equation body: . Repeated case expressions are also refactored into nested pattern matches, which are part of Granule. For example, nested matching on pairs is simplified to a single \({\textbf {case}}\) with nested pattern matching: is refactored to .

Input-output Examples Further to the implementation described above, we also allow user-defined input-output examples which are checked as part of synthesis. Our approach is deliberately naïve: we evaluate a fully synthesised candidate program against the inputs and check that the results match the corresponding outputs. Unlike sophisticated example-driven synthesis tools, the examples only influence the search procedure by backtracking on a complete program that doesn’t satisfy the examples. This lets us consider the effectiveness of search based primarily around the use of grades (see Section 5). Integrating examples more tightly with the type-and-grade directed approach is further work.

Our implementation augments Granule with first-class syntax for specifying input-output examples, both as a feature for aiding synthesis but also for aiding documentation that is type checked (and therefore more likely to stay consistent with a code base as it evolves). Synthesis specifications are written in Granule directly above a program hole (written using ) using the keyword. The input-output examples are then listed per-line. For example, one of benchmark programs (Section 5) for the length of a list is specified as:

figure w

Any synthesised definition must then behave according to this example.

In a block, a user can also specify the names of functions in scope which are to be taken as the available definitions (set D in the formal specification). For example, line 4 above specifies that length can be used here (i.e., recursively).

5 Evaluation

In evaluating our approach and tool, we made the following hypotheses:

H1.:

(Expressivity; less consultation) The use of grades in synthesis results in a synthesised program that is more likely to have the behaviour desired by the user; the user needs to request fewer alternate synthesised results (retries) and thus is consulted less in order to arrive at the desired program.

H2.:

(Expressivity; fewer examples) Grade-and-type directed synthesis requires fewer input-output examples to arrive at the desired program compare with a purely type-driven approach.

H3.:

(Performance; more pruning) The ability to prune resource-violating candidate programs from the search tree leads to a synthesised program being found more quickly when synthesised from a graded type compared with the same type but without grades (purely type-driven approach).

5.1 Methodology

To evaluate our approach, we collected a suite of benchmarks comprising graded type signatures for common transformations on structures such as lists, streams, booleans, option (‘maybe’) types, unary natural numbers, and binary trees. A representative sample of benchmarks from the Myth synthesis tool [47] are included alongside a variety of other programs one might write in a graded setting. Benchmarks are categorised based on the main data type, with an additional miscellaneous category. Appendix C lists type schemes for all benchmarks [28]. To compare, in various ways, our grade-and-type-directed synthesis to traditional type-directed synthesis, each benchmark signature is also “de-graded” by replacing all grades in the type with which is the only element of the singleton semiring in Granule. When synthesising in this semiring, we can forgo discharging grade constraints in the SMT solver entirely. Thus, synthesis for Cartesian grades degenerates to type-directed synthesis following our rules.

To assess hypothesis 1 (grade-and-type directed leads to less consultation / more likely to synthesise the intended program) we perform grade-and-type directed synthesis on each benchmark problem and type-directed synthesis on the corresponding de-graded version. For the de-graded versions, we record the number of retries N needed to arrive at a well-resourced answer by type checking the output programs against the original graded type signature, retrying if the program is not well-typed (essentially, not well-resourced). This checks whether a program is ‘as intended’ without requiring input from a user. In each case, we also compared whether the resulting programs from synthesis via graded-and-type directed vs. type-directed with retries (on non-we were equivalent.

To assess hypothesis 2 (graded-and-type directed requires fewer examples than type-directed), we run the de-graded (Cartesian) synthesis with the smallest set of examples which leads to the model program being synthesised (without any retries). To compare across approaches to the state-of-the-art type-directed approach, we also run a separate set of experiments comparing the minimal number of examples required to synthesise in Granule (with grades) vs. Myth.

To assess hypothesis 3 (grade-and-type-directed faster than type-directed) we compare performance in the graded setting to the de-graded Cartesian setting. Comparing our tool for speed against another type-directed (but not graded-directed) synthesis tool such as Myth is likely to be largely uninformative due to differences in implementation (engineering artefacts) obscuring meaningful comparison. Thus, we instead compare timings for the graded and de-graded approach within Granule. This normalises implementation artefacts as the two approaches vary only in the use of SMT solving to prune ill-resourced programs (in the graded approach). We also record the number of search paths taken (over all retries) to assess the level of pruning in the graded vs de-graded case.

We ran our synthesis tool on each benchmark for both the graded type and the de-graded Cartesian case, computing the mean after 10 trials for timing data. Benchmarking was carried out using version 4.12.1 of Z3 [43] on an M1 MacBook Air with 16 GB of RAM. A timeout limit of 10 seconds was set for synthesis.

5.2 Results and Analysis

Table 1 records the results comparing grade-and-type synthesis vs. the Cartesian (de-graded) type-directed synthesis. The left column gives the benchmark name, number of top-level definitions in scope that can be used as components (size of the synthesis context) labelled \(\textsc {Ctxt}\), and the minimum number of examples needed (#/Exs) to synthesise the Graded and Cartesian programs. In the Cartesian setting, where grade information is not available, if we forgo type-checking a candidate program against the original graded type then additional input-output examples are required to provide a strong enough specification such that the correct program is synthesised (see H3). The number of additional examples is given in parentheses for those benchmarks which required these additional examples to synthesise a program in the Cartesian setting.

Each subsequent results column records: whether a program was synthesised successfully or not (due to timeout or no solution found), the mean synthesis time (\(\mu {}T\)) or if timeout occurred, and the number branching paths (Paths) explored in the synthesis search space.

The first results column (Graded) contains the results for graded synthesis. The second results column (Cartesian + Graded type-check) contains the results for synthesising in the Cartesian (de-graded) setting, using the same examples set as the Graded column, and recording the number of retries (consultations of the type-checker at the end) N needed to reach a well-resourced program. In all cases, the resulting program in the Cartesian case was equivalent to that generated by the graded synthesis, none of which needed any retries (i.e., implicitly \(N = 0\) for graded synthesis, i.e., no retries are needed). H1 is confirmed by the fact that N is greater than 0 in 29 out of 46 benchmarks (60%), i.e., the Cartesian case does not synthesis the correct program first time and needs multiple retries to reach a well-resource program, with a mean of 19.60 retries and a median of 4 retries.

For each row, we highlight the column which synthesised a result the fastest in . In 17 of the 46 benchmarks (37%) the graded approach out-performed non-graded synthesis. This contradicts hypothesis 3 somewhat: whilst type-directed synthesis often requires multiple retries (versus no retries for graded) it still outperforms graded synthesis. This is due to the cost of SMT solving which must compile a first-order theorem on grades into the SMT-lib file format, start Z3, and then run the solver. Considerable amounts of system overhead are incurred in this procedure. A more efficient implementation calling Z3 directly (via a dynamic library call) may give more favourable results here. However, H3 is still somewhat supported: the cases in which the graded does outperform the Cartesian are those which involve considerable complexity in their use of grades, such as , , and for lists, and for both lists and trees. In each case, the Cartesian column is significantly slower, even timing out for ; this shows the power of the graded approach. Furthermore, we highlight the column with the smallest number of synthesis paths explored in , observing that the number of paths in the graded case is always the same or less than that those in the Cartesian+graded type check case (apart from Tree stutter). The paths explored are the sometimes the same between Graded and Cartesian synthesis because we use backtracking search even in the Cartesian case where, if an output program fails to type check against the graded type, the search backtracks rather than starting from the beginning. This leads to an equal number of paths in the graded case when solving occurred only at a top-level abstraction. However, paths explored are fewer in the graded case when solving occurs at other binders, e.g., in case and unboxing.

Table 1. Results. \(\mu {T}\) in ms to 2 d.p. with standard sample error in brackets

Confirming H2, the de-graded setting without graded type checking requires more examples to synthesise the same program as the graded in 20 out of 46 (43%) cases. In these cases, an average of 1.25 additional examples are required. To further interrogate H2, we compare the number of examples required by Granule (using grades) against the Myth synthesis tool (based on pruning by examples) [47], and the more advanced assertion-based Smyth [36]. We consider the subset of our benchmarks drawn from Myth. Table 2 shows the minimum number of input-output examples needed to synthesise the correct program in Granule, Myth, and Smyth. For all cases, Granule required the same or fewer examples than Myth to synthesis the desired program, requiring fewer examples in 16 out of 21 cases. The disparity in the number of examples required is quite significant in some cases: with 13 examples required by Myth to synthesise concat but only 1 example for Granule. Overall, Smyth needed the same or fewer examples than Myth. Granule needed the same or fewer examples than Smyth in 18 out of 21 cases, but in the other 3 cases (and, impl, or) Smyth required 1 fewer example. Overall, the lower number of examples needed in our approach shows the pruning power of grades in synthesis, confirming H2.

Table 2. Number of examples needed for synthesis, Granule vs. Myth vs. Smyth

We briefly examine one of the more complex benchmarks which uses almost all of our synthesis rules in one program. The case (List class) is specified:

figure ak

Its input is a list of elements graded by , i.e., must be used twice. The argument list itself must be used at least once but possibly infinitely, suggesting that some recursion will be necessary. This is further emphasised by the , which states we can use itself inside the function. Without grades, synthesis times out. Graded synthesise produces the following in 1325ms (\(\sim \)1.3 seconds):

figure ao

6 Synthesis of Linear Haskell Programs

As part of a growing trend of resourceful types being added to more mainstream languages, Haskell has introduced support for linear types as of GHC 9, using an underlying graded type system which can be enabled as a language extension [8] (called ). This system is closely related to the calculus here but limited to one semiring. This however presents an opportunity to leverage our tool to synthesise (linear) Haskell programs. Like Granule, grades in Haskell can be expressed as “multiplicities” on function types: . The multiplicity r can be either 1 or \(\omega \) (or polymorphic), with 1 denoting linear usage (also written as ) and \(\omega \) () for unrestricted use. Similarly, Granule can model linear types using the 0-1-\(\omega \) semiring (Example 1) [26]. Synthesising Linear Haskell programs then simply becomes a task of parsing a Haskell type into a Granule equivalent, synthesising a term from it, and compiling the synthesised term back to Haskell (which has similar syntax to Granule anyway).

Our implementation includes a prototype synthesis tool using this approach. A synthesis problem takes the form of a Linear Haskell program with a hole, e.g.

figure at

We invoke the synthesis tool with which produces:

figure av

Users may make use of lists, tuples, and data types from Haskell’s prelude, as well as user-defined ADTs. Further integration of the tool, as well as support for additional Haskell features such as GADTs is left as future work.

7 Discussion

Comparison with prior work Previously, lgm targeted the linear \(\lambda \)-calculus with graded modalities [27]. In this paper, we instead considered a fully-graded (‘graded base’) calculus with no linearity: all assumptions are graded and subsequently there is a graded function arrow (not present in the ‘linear base’ style). This graded calculus matches practical implementations of graded types seen in Idris 2 and Haskell. Furthermore, a key contribution beyond lgm is the handling of recursion, general user-defined (recursive) ADTs, and polymorphism. Due to the pervasive grading, the majority of the synthesis rules are considerably different to lgm. For example, lgm’s synthesis of functions is linear, and thus need not handle the complexity of grading (cf. \(\rightarrow _{\textsc {L}}\) on p. 13):

$$\begin{aligned} \dfrac{\varGamma , x_2 : B \vdash C \Rightarrow ^+ t_1 ; \varDelta _1 , x_2 : B \qquad \varGamma \vdash A \Rightarrow ^+ t_2 ; \varDelta _2}{\varGamma , x_1 : A \multimap B \vdash C \Rightarrow ^+ [(x_1 t_2) / x_2] t_1; (\varDelta _1 + \varDelta _2), x_1 : A \multimap B} {L\multimap ^+}\text {[27]} \end{aligned}$$

As above, in the linear setting of lgm, many of the constraints and grades handled in this paper are essentially specialised away as equal to 1, with only linear products and coproducts considered. Since grading is potentially more permissive than linearity, elimination rules in our synthesis calculus must also make available an eliminated variable for re-use in every premise, which was not needed in lgm. Furthermore, the power of this paper’s case rule means there are simple, non-recursive terms we can synthesise which lgm cannot. In particular, synthesis of programs which perform “deep” pattern matching over a graded data structure are not possible in lgm. For example, lgm’s approach cannot synthesise a term for \(\Box _{0..1} (\alpha , (\alpha , \beta )) \multimap \beta \) as it cannot propagate information from one case to another to inherit the grade 0..1 on the pair’s components. However, here we can synthesise (in just a few steps, plus refactoring):

figure ay

Thus, not only does our approach consider a different mode of grading, as well as extending to arbitrary recursive ADTs and recursive functions, it is also more expressive in the interaction between data types and grades.

lgm introduced additive and subtractive resource management schemes (summarised and re-contextualised in Section 2). Comparative evaluation of lgm showed that constraints from the subtractive approach are typically larger, more complex, and discharged more frequently than in additive synthesis. We concluded that subtractive only ever outperformed additive on purely linear types. Coupled with the fact that the subtractive approach has limitations in the presence of polymorphic grades, we thus adopted the additive scheme, especially in light of us considering more complex programs. Our evaluation of lgm did not given any evidence justifying use of grades for synthesis compared to just using types. Here, we showed that grading significantly reduces the number of paths explored and examples needed when compared with purely type-directed approaches, including in comparison with Myth [47].

Other Related Work Beyond Myth, other recent work has extended type-and-example-directed synthesis approaches. Smyth constrains the search space further by augmenting types with assertions (called ‘sketches’) to guide synthesis [36]. This techniques involves employing more evaluation during synthesis to generated intermediate input-output examples to prune the search space. They evaluate on a subset of Myth benchmarks (somewhat similar to our own method here). Whilst we compared our approach (with graded + types + examples considered at the end) to Myth (with types + examples integrated) to show that grading reduces the number of examples, comparing with the assertion-based approach in Smyth is further work. Another recent work, Burst, also leverages the Myth benchmark, but using a ‘bottom-up’ technique [41] (in contrast to our top-down approach, Section 4.2). The bottom-up approach synthesises a sequence of complete programs which can be refined and tested under an ‘angelic semantics’. Whether a bottom-up grade-directed approach could lead to performance improvements is an open question.

Whilst we considered resourceful programming via graded types, other notions of resourceful typing exist, including ‘ownership’ (e.g., Rust [31]) and related ‘uniqueness’ (e.g., Clean [51]). Recently, Fiala et al. synthesised Rust programs from a custom program logic Synthetic Ownership Logic that integrates a typed approach to Rust ownership with functional specifications, allowing synthesis to follow a deductive approach [16]. There is some philosophical overlap in the resourceful ideas in their approach and ours. Drawing a closer correspondence between Rust-style ownership and grading, to perhaps leverage our resourceful approach to synthesis, is future work. Notably, Marshall and Orchard show that uniqueness types can be implemented as an extension of a linear type theory with a non-linearity modality and uniqueness modality [38]. Further work could adapt our approach to this setting to provide synthesis for uniqueness types as a precursor to the full ownership and borrowing system of Rust.

The dependently-typed language Idris provides automated proof search as part of its implementation [10]. In Idris 2, the core type theory is based on a graded type theory [5, 39] with 0-1-\(\omega \) semiring (Example 1) and with proof synthesis extended to utilise these grades [11]. This approach has some relation to ours, but in a limited single-semiring setting and restricted in how grades can be leveraged. Our approach is readily applicable to Idris, which is future work.

Conclusion Our work is grounded in the philosophy of type-driven development where the user thinks about the expected behaviour or constraints of a program first, writing the type as a specification. Synthesis is not necessarily about having complicated programs generated but is often about generating straightforward programs to save effort. This is the gain provided by type-directed synthesis in existing languages such as Agda [9] and Idris [10]. Our technique augments this, such that boilerplate code and simple algorithms can be automatically generated, freeing the developer to focus on other parts of a program.

A next step is to incorporate GADTs (Generalised ADTs), i.e., indexed types, into synthesis. Granule provides support for user-defined GADTs, and the interaction between grades and type indices is a key contributor to its expressive power [45]. For example, consider a function that replicates a value a number of times to create a list, typed . Given a standard indexed type of natural numbers and sized-indexed vectors , a more precise specification can be given as for which the search space could be more effectively pruned by including type indices in synthesis.

We intend to pursue further improvements to our tool to reduce the overhead of SMT solving, integrate examples into the search algorithm itself in the style of Myth [47] and Leon [34], as well as considering possible semiring-dependent optimisations that may be applicable. Another further work is prove completeness of our synthesis calculus which we believe this holds.

With the rise in Large Language Models showing their power at program synthesis [6, 30] the deductive approach still has value, providing correct-by-construction synthesis from specification rather than predicting programs which may violate fine-grained type constraints, e.g., from grades. Future work, and a general challenge for the deductive synthesis community, is to combine the two approaches with the logical engine of the deductive approach guiding prediction.