Derivation of a Pattern-Matching Compiler

Geoff Barrett and Philip Wadler Oxford University Computing Laboratory, Programming Research Group

1986

Introduction

This paper presents the derivation of an efficient compiler for pattern-matching in a functional language. The algorithm is described in [A] and [W], and is used in the LML compiler [J]. The derivation is based on that given in [Bar].

The algorithm relates to functional languages in two ways: a functional language is used as the medium of the derivation, and the algorithm is useful for compiling functional languages. The features of functional languages that make them well suited to program transformation, such as referential transparency, are widely known [BD]. Less widely known is the fact that it is now possible to write efficient compilers for such languages [P], and indeed the LML compiler is itself written in LML.

The derivation relies heavily on the use of higher-order functions, such as fold and map, that encapsulate common patterns of computation, and on the laws satisfied by these functions. The higher-order functions and their associated laws are well-known and have applications in a wide variety of contexts: for instance, see [Bu], [Bi], and [BW]. This paper provides an extended example of how these laws can be used to derive a practical and significant result. The derivation also relies heavily on familiar mathematical properties, such as associativity, distributivity, and commutativity.

These results are encouraging, because they suggest that much of the hard work of many derivations can be factored into a few general theorems, and that many of the concepts and manipulations necessary for such derivations will already be familiar from traditional algebra.

The emergence of general laws and an algebraic framework indicate how the field of program derivation has matured. In early work on the transformation of functional programs many similar derivations were repeated to derive similar results [BD, F]. Experience with such derivations has led to the emergence of more general theorems, and many derivations can be structured and shortened by the use of such theorems.

The value of program derivations most often quoted is that they increase the reliability of the resulting program. Another equally important value is that they lead to deeper understanding. Knowledge of the derivation given here was very useful in writing the explanation of the algorithm given in [W]. In particular, the structure of the explanation there follows closely the structure of the derivation here.

1 Notation

The notation used for structures and functions is essentially that of SQUIGOL (see [Bi]). The list constructor is the infix (:) and signatures are introduced by (::). All infix operators have the same precedence and associate to the right. Four standard functions are used throughout, namely ∗ for map, = for folding functions which are associative and have an identity, \underrightarrow{=} for folding functions which are not associative or have no identity and = which constructs the list of corresponding pairs from two lists. Their definitions follow:

f ∗ [ ]  = [ ]
f ∗ (a : x) = f a : f ∗ x
f \underrightarrow{=} a [ ]  = a
f \underrightarrow{=} a (b : x) = f b (f \underrightarrow{=} a x)
(f=) = f \underrightarrow{=} e where e is the unit of f
[ ] = [ ]  = [ ]
(a : x) = (b : y) = (a; b) : x = y

Two particular functions are used throughout: ++ appends two lists; flatten produces a list from a list of lists:

x ++ y = (:) \underrightarrow{=} y x

flatten = ((++) =)

1.1 Specification of the function group

The derivation will require a function group which groups the elements of a list into segments on which a function, f , is constant. These segments are tagged with the value of f on the segment. For instance,

group f [4; 0; 3; 5; 6] = [(false; [4]) ; (true; [0; 3]) ; (false; [5; 6])]

where f x = x ≤ 3

We can specify the situation where y is a grouping of x under f as follows:

groups f x y ⟺ flatten(snd ∗ y) = x ∧ (∧) = [f a = c | (c; z) ← y; a ← z]

The group function is uniquely specified by taking the shortest possible grouping of x:

groups f x (group f x) ∧ ∀ y | y groups f x y · #(group f x) ≤ #y

It can easily be shown that snd∗(group f x) contains no empty lists. (Otherwise, removing elements of the form (a; [ ]) retains the grouping property but is shorter.)

A possible implementation of group is as follows:

group f x  = group′ \underrightarrow{=} [ ] (graph f ∗ x)

where graph f a = (f a; a)

group′ [ ] fbb  = [fbb]
group′ ((fa; x) : g) (fb; b) = \{ \begin{matrix}(fa; b : x) : g; & fa = fb \\ (fb; [b]) : (fa; x) : g; & fa ≠ fb \end{matrix}

2 An Abstract Language for Pattern Matching

In this section, we introduce a language in which the relevant features of pattern matching may be expressed. The language is designed to obey a number of elegant, algebraic laws by means of which it is given a semantics. The abstract syntax of this language is given in terms of operators on the various types.

2.1 Syntax

Three types are primitive to the discussion. The type of variable names var. The type of constructors con; with each constructor there is associated a natural number, called its arity :: con ⟶ nat, which gives the maximum number of terms to which the constructor may be applied. For example, the arity of the empty list is 0 and the arity of the list constructor is 2. The third type is that of expressions which we shall introduce presently.

¿From the var and con types, we construct the type of patterns. A pattern is either a variable or a constructor with a list of sub-patterns whose length is the arity of the constructor:

pat ::= v var | c con [pat]

where c c ps is well-formed just when arity c = #ps.

We now come to the type of expressions, exp. We require the type to have the operation of variable renaming:

sub :: (var; var) ⟶ exp ⟶ exp

where ren (u; v) e denotes the renaming of u for v in e; further, we assume a case expression so that we have a function

case of :: var ⟶ [(pat; exp)] ⟶ exp

and the following pattern-matching operators:

e :: exp
f :: exp
⇒ :: (var; pat) ⟶ exp ⟶ exp
:: exp ⟶ exp ⟶ exp

where e is the value returned when a complete set of equations has failed to match; f is the exception returned when a single equation does not apply.

The phrase (v; p) ⇒ e matches the binding of v against p. If the match succeeds then the appropriate subterms of the binding are bound to the variables of p and e is evaluated in the new environment; otherwise, the whole phrase is equivalent to f. It is well-formed only when the variables of p are not bound in e and v is not free in e. The bound variables of the phrase are the bound variables of e and the variables appearing in p; the free variables are v along with the free variables of e which do not appear in p.

The phrase e_{1} e_{2} denotes the successive attempts to evaluate the matches e_{1} and e_{2}. If the first succeeds, then the second is discarded; otherwise, the second match is tried. The phrase is always well-formed; its free variables are the free variables of e_{1} and the free variables of e_{2}; likewise the bound variables.

2.2 Semantics

The pattern-matching language is given a semantics by means of a set of algebraic equivalences. The intended equivalence is the largest in which the constants e and f are not equivalent, the operators and (v; p) ⇒ and congruences and the following laws are true.

We first give the semantics of (v; p) ⇒ . This involves describing the way in which variables are bound and expressions are matched against patterns. Matching against a variable pattern always succeeds. The effect is to rename the variable in the final expression:

(u; v v) ⇒ e ≡ ren (u; v) e  (1)

Under suitable disjointness conditions on bound variables, (u; v v) ⇒ and (u; p) ⇒ commute:

(u; v v) ⇒ (u′; p) ⇒ e ≡ (u′; p) ⇒ (u; v v) ⇒ e  (2)

whenever v ≠ u′ and u is not a variable of p. Note that by the well-formedness conditions, v may not be a variable of p and since u′ is free in the expression (u; v v) ⇒ (u′; p) ⇒ e, u ≠ u′. For the sake of completeness we consider the case in which v = u′. In this case, the binding of u is bound to v and so we need only match u against p for u′ = v may not be free in e by the well-formedness rules:

(u; v v) ⇒ (v; p) ⇒ e ≡ (u; p) ⇒ e

Note that these rules are not independent.

When matching a constructor against a pattern, the matching may be performed one constructor at a time so that the sub-terms of the binding of u which matches the outermost constructor of the pattern may be bound to new variables which are then matched against the sub-patterns:

(v; c c ps) ⇒ e ≡ (v; c c (v ∗us)) ⇒ (⇒) \underrightarrow{=} e (us = ps)  (3)

where us is a list of distinct variables which do not appear in any of the ps and are not free in e; and #us = arity c.

All we need to know of the operator is that it is associative and has identity f:

e_{1} (e_{2} e_{3}) ≡ (e_{1} e_{2}) e_{3}  (4)
f e ≡ e
e f ≡ e

It is not in general true that is commutative. Consider, for instance, that u is bound to ⊥ in the match

((u; c c ps) ⇒ e_{1}) ((u; v v) ⇒ e_{2})

This phrase must be equivalent to ⊥. However, the phrase

((u; v v) ⇒ e_{2}) ((u; c c ps) ⇒ e_{1})

is equivalent to ren (u; v) e, which may not be ⊥. Attempting to match against patterns with distinct constructors does commute since everything except ⊥ fails to match at least one of them:

((v; c c_{1} ps_{1}) ⇒ e_{1} (v; c c_{2} ps_{2}) ⇒ e_{2}) ≡ ((v; c c_{2} ps_{2}) ⇒ e_{2} (v; c c_{1} ps_{1}) ⇒ e_{1}) {c ≠ c′}

(5)

The last law is a distributivity law:

(v; p) ⇒ (e_{1} e_{2}) ≡ ((v; p) ⇒ e_{1}) ((v; p) ⇒ e_{2})  (6)

which holds because if the match (v; p) ⇒ fails then both sides are equivalent to f; if the match succeeds, evaluating e_{1} e_{2} in an environment which binds the variables of p is the same as evaluating e_{1} and e_{2} in that environment and choosing the first to succeed.

2.3 Equations and Case Expressions

Concretely, a named list of equations looks like:

mappairs f [ ] ys  = [ ]
mappairs f (x : xs) [ ]  = [ ]
mappairs f (x : xs) (y : ys) = f x y : mappairs f xs ys

An equation is nothing more than a list of patterns followed by an expression. For example, the first equation of the function mappairs is

([v f; c NIL [ ] ; v ys] ; NIL)

so that in general we define equn = ([pat] ; exp)

Furthermore, if mappairs is applied to the terms u_{1}, u_{2} and u_{3}, and the first equation applies, then the result is given by

(u_{1}; v f ) ⇒ (u_{2}; c NIL [ ]) ⇒ (u_{3}; v ys) ⇒ NIL

and, in general, given a list of variables, us, the meaning of an equation is given by:

clause :: [var] ⟶ equn ⟶ match clause us (ps; e) = (⇒) \underrightarrow{=} e (us = ps)

A list of equations is intended to be tried in order from top to bottom so that if the first fails, the second is tried and so on. However, if no equation matches, the result is an error. We restrict ourselves to non-empty lists of equations whose pattern lists are all of the same length so that

qs :: equns = [equn]

is well-formed only when it is non-empty and

all [m = n | n ← lens] where m : lens = [#ps | (ps; e) ← qs]

If mappairs is applied to terms u_{1}, u_{2} and u_{3}, the desired result is given by:

(u_{1}; v f ) ⇒ (u_{2}; c NIL [ ]) ⇒ (u_{3}; v ys) ⇒ NIL
(u_{1}; v f ) ⇒ (u_{2}; c CONS [v x; v xs]) ⇒ (u_{3}; c NIL [ ]) ⇒ NIL
(u_{1}; v f ) ⇒ (u_{2}; c CONS [v x; v xs]) ⇒ (u_{3}; c CONS [v y; v ys]) ⇒

(f x y : mappairs f xs ys)

e

(7)

so that e is returned if no equation matches. In general, the meaning of a set of equations is given by:

mapEqun :: [var] ⟶ [equn] ⟶ match mapEqun us qs = ( ) = (clause us ) ∗ qs e

where us are distinct, do not appear in the pattern of qs and #us = #ps for (ps; e):qs′ = qs.

We will wish to compile this expression into case expressions. The meaning of a case expression is given in terms of ⇒ and as follows:

mapCase :: exp ⟶ exp

mapCase (case v of pes) = ( ) = (f ∗ pes) where f (p; e) = (v; p) ⇒ e

Note that if no pattern is matched, the whole expression is equivalent to f.

3 The Problem

Consider an evaluation of expression (7), when u_{1} is bound to (+), u_{2} is bound to [1; 2] and u_{3} is bound to [0]. The evaluation proceeds thus:

1. (+) matches f ; (1 : [2]) does not match [ ];

2. (+) matches f ; (1 : [2]) matches (x : xs); (0 : [ ]) does not match [ ];

3. (+) matches f ; (1 : [2]) matches (x : xs); (0 : [ ]) matches (y : ys).

By using the laws (4), (6), (2), and (1), and the semantics of case expressions, the reader is invited to verify that the expression for mappairs u_{1} u_{2} u_{3}, (7), may be transformed to:

case u_{2} of c NIL [ ] ⇒ [ ]

c CONS [v x; v xs] ⇒ case u_{3} of

c NIL [ ] ⇒ [ ]

c CONS [v y; v ys] ⇒ (u_{1} x y : mappairs u_{1} xs ys) e

An evaluation of this yields:

1. 1 : [2] does not match c NIL [ ].

2. 1 : [2] matches c CONS [v x; v xs],

(a) 0 : [ ] does not match c NIL [ ].

(b) 0 : [ ] does match c CONS [v y; v ys].

In the first evaluation there are three successful matches followed by failure; in the second there are none.

This is the required output from the compiler. It has a number of features which serve as the specification of the problem. The most important and most obvious is that the output of the compiler means the same as its input. Secondly, all the patterns which are to be matched have only variables as sub-patterns. Furthermore, each case expression has a different constructor in each of its patterns.

The meaning-preserving part of the specification is formulated as:

compile :: [var] ⟶ [equn] ⟶ exp compile us qs e ≡ mapEqun qs us

Since this is a consequence of:

compile us qs ≡ ( ) = (clause us) ∗ qs

we may take the latter as the specification of the compilation function.

4 The Derivation

The algorithm is derived in a number of cases. The first case depends upon whether are any patterns in the equations.

4.1 The degenerate Case

If there are no patterns in the list of equations, then it must be the case that the list of variables is empty:

compile [ ] qs

Thus, the definition is:

compile [ ] qs = ( ) = snd ∗ qs

4.2 The non-degenerate case

When the list of patterns in each equation is non-empty, we start the compilation by grouping the equations into segments whose first patterns are variables or contain constructors. The two sorts of group require different compilation strategies; the correct strategy for each group is selected by the function varconsCompile.

The first function to be defined inspects the first pattern to see whether it is a variable or a constructor:

varcon :: equn ⟶ (VAR | CON)

varcon (v v : ps; m) = VAR
varcon (c c ps : ps′; m) = CON

The result of grouping a list of equations according to this function will be a list of pairs, the first of which is VAR or CON. The function varconsCompile will select the correct strategy for each group but the required meaning can be given regardless of the compilation strategy so that the specification of varconsCompile is:

varconsCompile :: [var] ⟶ ((VAR | CON) ; [equn]) ⟶ exp varconsCompile us (vc; qs) ≡ ( ) = clause us ∗ qs

Let gqs = group varcon qs so that

qs

then we have

compile (u : us) qs

and this last line suffices for the definition of compile, so that:

compile (u : us) qs = ( ) = varconsCompile (u : us) ∗ gqs where gqs = group varcon qs

In order to define varconsCompile, we invent two new functions, varCompile and consCompile which embody the different compilation strategies. Each applies to a list of equations, but they have preconditions that the first pattern of each of the equations must be a variable or contain a constructor, respectively, so that the specifications are:

varCompile; consCompile :: [var] ⟶ [equn] ⟶ exp

varCompile us qs ≡ ( ) = clause us ∗ qs
consCompile us qs ≡ ( ) = clause us ∗ qs

and we can see that the following definition of varconsCompile meets its specification:

varconsCompile us (VAR; qs) = varCompile us qs
varconsCompile us (CON; qs) = consCompile us qs

4.2.1 When all first patterns are variables

We begin to find a definition for varCompile by studying an application of clause to an equation whose first pattern is a variable:

clause (u : us) (v v : ps; e)

Defining

equnSubst u (v v : ps; e) = (ps; ren (u; v) e)

we have

clause (u : us) (v v : ps; e)

Since the precondition of varCompile is that all first patterns must be variables, this reformulation of clause may be substituted in the specification:

varCompile (u : us) qs

Hence we can make the definition:

varCompile (u : us) qs = compile us (equnSubst u ∗ qs)

4.2.2 When all first patterns have constructors

We now come to the compilation for a list of equations whose first pattern has a constructor. The aim is to use the commutativity rule, (5), in order to sort the equations so that equations whose first patterns contain the same constructor are adjacent. We first define a subsidiary order:

(u; c c ps) ⇒ m ≺′ (u; c c′ ps′) ⇒ m′ ⟺ c ¡ c′

which is useful in the derivation, and a similar order on equn which will be used in the program:

(c c_{1} ps_{1} : ps′_{1}; m_{1}) ≺ (c c_{2} ps_{2} : ps′_{2}; m_{2}) ⟺ c_{1} ¡ c_{2}

consCompile us qs

Let oqs = stablesort (≺) qs. We next wish to group the equations according to the of the first pattern, so we define:

conName :: equn ⟶ con conName (c c ps′; e) = c

and let gqs = group conName oqs so that:

oqs

Lastly, each group of equations is to form one branch of a case expression. The function which compiles the group is called onConsCompile and satisfies the specification:

oneConsCompile :: [var] ⟶ (con; [equn])

f u (oneConsCompile us (c; qs)) ≡ ( ) = clause (u : us) qs where f u (p; e) = (u; p) ⇒ e

so we have:

consCompile (u : us) qs

Hence we make the definition:

consCompile us qs = case u of oneConsCompile us ∗ group conName oqs where oqs = stablesort (≺) qs

4.2.3 When all first patterns have the same constructor

As in the case when all first patterns were variables, we begin by considering the application of clause to a single equation. The following lemma is useful in the derivation: Lemma 1

clause (u : us) (p : ps; e) = (u; p) ⇒ clause us (ps; e)

Proof

clause (u : us) (p : ps; e)

The derivation proceeds:

clause (u : us) (c c ps′ : ps; e)

Defining

deconstruct (c c ps′ : ps; e) = (ps′ ++ ps; e)

and letting p′ = c c (v ∗us′), we have: clause (u : us) (c c ps′ : ps; e)

Let (p; e) = oneConsCompile us (c; qs), then (u; p) ⇒ e

Hence we make the definition oneConsCompile us (c; qs) = (c c (v ∗us′); compile (us′ ++ us) (deconstruct ∗ qs))

4.3 The complete algorithm and a note on termination

Taking together all the definitions derived so far gives the final program:

compile [ ] qs  = ( ) = snd ∗ qs

compile (u : us) qs = ( ) = varconsCompile (u : us) ∗ gqs

where gqs = group varcon qs

varcon (v v : ps; m) = VAR
varcon (c c ps : ps′; m) = CON
varconsCompile us (VAR; qs) = varCompile us qs
varconsCompile us (CON; qs) = consCompile us qs

equnSubst u (v v : ps; e) = (ps; ren (u; v) e) varCompile (u : us) qs = compile us (equnSubst u ∗ qs) (c c_{1} ps_{1} : ps′_{1}; m_{1}) ≺ (c c_{2} ps_{2} : ps′_{2}; m_{2}) = c_{1} ¡ c_{2} conName (c c ps′; e) = c consCompile us qs = case u of oneConsCompile us ∗ group conName oqs where oqs = stablesort (≺) qs deconstruct (c c ps′ : ps; e) = (ps′ ++ ps; e) oneConsCompile us (c; qs) = (c c (v ∗us′) ; compile (us′ ++ us) (deconstruct ∗ qs))

where us′ is a list of new variables.

Defining the variant to be the sum of the arities of the constructors plus the number of constructors appearing in qs plus the length of us, the variant is reduced each time compile is applied. Thus, the algorithm terminates.

5 Conclusions

Although the basic laws of and ⇒ and those pertaining to = and ∗ are intuitively simple, the complete algorithm is not immediately comprehensible. The proof of correctness is an aid not only to why the compiler works but also to how it works. By abstracting away from the basic, unfamiliar operators in exp and relying on their more familiar properties, irrelevant details can be ignored and the proof becomes much simpler. In [Bar], this strategy is not employed and the derivation is at once far less comprehensible and four times longer!

The use of = and ∗ to augment binary operators is widely applicable and generally produces well-modularised proofs. (See [Bi], for instance.) The derivation presented here contains no induction on lists. All of this manipulation can be factored into general theorems, and used again in other derivations.

The particular properties of associativity, commutativity and distributivity appear in other transformations (see [RH] where corresponds to IF or ALT and ⇒ to SEQ, and [Bac] in which the transformation of non-deterministic to deterministic state-machines relies on identical properties for | and ·). Programs are made more efficient by eliminating redundant calculations. One common method of achieving this is by accumulation of a result; in this transformation and in [Bac], however, the increase in efficiency relies on a distributivity property. The object of both the latter transformations is a decision procedure which is optimised by eliminating backtracking. In [RH] too, a normal form for finite programs is produced by, essentially, transformation into a decision procedure. Accumulation is an effective strategy when the predominant form of a program is sequential calculation; distribution applies when the predominant form is a conditional construct. This suggests that a ‘distribution’ strategy is a valuable member of the transformer’s tool kit.

References

[A] Augustsson, L. Compiling Pattern Matching. IFIP international conference on Functional Programming and Computer Architecture. Nancy, September 1985.

[Bac] Backhouse, R. The Syntax of Programming Languages. Prentice Hall, 1979.

[Bar] Barrett, G. Compiling Pattern Matching. M.Sc. Dissertation, Oxford University, 1985.

[Bi] Bird, R.S. An introduction to the theory of lists. International Summer School on Logic of Programming and Calculi of Discrete Design, Marktoberdorf, Germany, August 1986.

[BW] Bird, R.S., and Wadler, P.L., An Introduction to Functional Programming, Prentice-Hall, 1988.

[Bu] Burge, W.H, Recursive Programming Techniques. Addison-Wesley, 1975.

[BD] Burstall, R.M. and Darlington, J. A transformation system for developing recursive programs. Journal of the ACM, 24(1), January 1977.

[F] Feather, M.S. A system for assisting program transformation. ACM Transactions on Programming Languages and Systems, 4(1):1–20, January 1982.

[J] Johnsson, T. Efficient compilation of lazy evaluation. ACM SIGPLAN Symposium on Compiler Construction, SIGPLAN Notices, 19(6), June 1984.

[P] Peyton-Jones, S.L., The Implementation of Functional Programming Languages, Prentice-Hall, 1987.

[RH] Roscoe, A.W. and Hoare, C.A.R. The Laws of occam Programming. TCS 60 (1988).

[W] Wadler, P.L., Compiling pattern matching, in [P].

A Laws about Lists

The derivation depends on the following results about functions of lists. Most of these theorems are well-known and have straightforward proofs. For further discussion of similar laws, see [Bi] or [BW].

Theorem 1

(f ∗) · (g∗) = ((f · g) ∗)

Theorem 2

#x = #y ⟹ (x = y) ++ (u = v) = (x ++ u) = (y ++ v)

Theorem 3

((⊕) =) · flatten = ((⊕) =) · (((⊕) =) ∗)

Theorem 4

(f ∗) · flatten = flatten · (((f ∗)) ∗)

Theorem 5

(⊕) \underrightarrow{=} ((⊕) \underrightarrow{=} a x) y = (⊕) \underrightarrow{=} a (y ++ x)

Theorem 6 (∀ a b · f (a ⊕ b) = f a ⊗ f b) ⟹ f ((⊕) = (a : x)) = (⊗) = (f ∗ (a : x))

Theorem 7 If ≺ is a linear pre-order then (a ≺ b ⇒ a ⊕ b = b ⊕ a) ⟹ ((⊕) =) = ((⊕) =) · stablesort (≺)

Theorem 8 If f is strictly monotonic then sorting and mapping f on a list commute: (f a ≺′ f b ⇔ a ≺ b) ⟹ stablesort (≺′) · (f ∗) = (f ∗) · stablesort (≺)

Theorem 9 (∀ a b · f (b ⊕ a) = b ⊕ f a) ⟹ f · (⊕) \underrightarrow{=} a = (⊕) \underrightarrow{=} (f a)