Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Jones N.D.Partial evaluation and automatic program generation.1999

.pdf
Скачиваний:
10
Добавлен:
23.08.2013
Размер:
1.75 Mб
Скачать

Types for interpreters, compilers, and partial evaluators 341

 

 

 

 

 

 

 

;

 

 

 

 

( ! ) (

! )

 

[[-]

[[-]]

 

 

 

?

 

 

 

 

;

 

 

 

( ! ) ( ! )

 

 

 

 

 

- ( ! )

Syntactic composition

 

 

 

 

[[-]]

 

 

 

 

 

?

 

Semantic composition

- ( ! )

 

 

 

 

Figure 16.2: Symbolic composition.

16.2.2Symbolic function specialization = partial evaluation

Specializing (also called restricting) a two-argument function f(x; y) : ! to x = a gives the function fjx=a(y) = f(a; y). Function specialization thus has polymorphic type

fs : ( ! ) ! ( ! )

Partial evaluation is the symbolic operation corresponding to function specialization. Using peval = [[mix]] to denote the partial evaluation function, its correctness is expressed by commutativity of the diagram in Figure 16.3. Partial evaluation has polymorphic type

peval : ( ! ) ! ( ! )

Rede nition of partial evaluation

The description of peval can be both simpli ed and generalized by writing the functions involved in curried form4. This gives peval a new polymorphic type:

peval : !( ! )! ! !

which is an instance of a more general polymorphic type:

peval : ! ! !

Maintaining our emphasis on observable values, we will require to be a rst-order type (i.e. base or a type t L).

4The well-known `curry' isomorphism on functions is ( !( ! )) ' ( ! ).

342 Larger Perspectives

( ! )

 

peval

-

 

( ! )

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

[[-]]

Identity

 

 

 

[[-]]

 

 

 

 

 

 

 

 

 

 

 

?

 

fs

 

 

?

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

)

( ! )

 

 

- ( !

 

 

 

 

 

 

 

 

 

 

 

Figure 16.3: Function specialization and partial evaluation.

Remark. The second input to peval is a value of type , and not representation of a value. In practice, especially if peval is programmed in a strongly typed language, one may need to work with representations rather than directly with values. We ignore this aspect partly because of notational complexity, and partly because the use of representations leads quickly to problems of size explosion when dealing with representations of representations of . . . . This problem has been addressed by Launchbury [169], and appears in Chapter 11.

The mix equation revisited. mix 2 D is a partial evaluator if for all p, a 2 D,

[[p]] a [[[[mix] p a]]

Thus for any n +1 rst-order values d, d1,. . . ,dn, we have [[p]] a d1. . . dn = d if and only if [[[[mix]] p a]] d1. . . dn = d.

16.2.3Compiler and interpreter types

Similarly, the de nitions of interpreter and compiler of Section 3.1.1 may be elegantly restated, a little more generally than before:

S

 

L

= fint j [[s]]S [[int]]Lsg

and

Types for interpreters, compilers, and partial evaluators 343

S

-

T

 

 

 

= f comp j

[[s]]S [[[[comp]]Ls]]T g

 

L

 

 

 

 

 

 

 

 

 

 

 

 

Can an interpreter be typed?

Suppose we have an interpreter up for language L, and written in the same language | a universal program or self-interpreter. By de nition up must satisfy [[p]] [[up]] p for any L-program p. Consequently as p ranges over all L-programs, [[up]] p can take on any program-expressible type. A di cult question arises: is it possible to de ne the type of up non-trivially5?

A traditional response to this problem has been to write an interpreter in an untyped language, e.g. Scheme. This has the disadvantage that it is hard to verify that the interpreter correctly implements the type system of its input language (if any). The reason is that there are two classes of possible errors: those caused by errors in the program being interpreted, and those caused by a badly written interpreter. Without a type system it is di cult to distinguish the one class of interpret-time errors from the other.

Well-typed language processors

Given a source S-program denoting a value of some type t, an S-interpreter should return a value whose type is t. From the same source program, a compiler should yield a target language program whose T-denotation is identical to its source program's S-denotation. This agrees with daily experience|a compiler is a meaningpreserving program transformation, insensitive to the type of its input program (provided only that it is well-typed). Analogous requirements apply to partial evaluators.

A well-typed interpreter is required to have many types: one for every possible input program type. Thus to satisfy these de nitions we must dispense with type unicity, and allow the type of the interpreting program not to be uniquely determined by its syntax.

Compilers must satisfy an analogous demand. One example: x.x has type tL!t L for all types t. It is thus a trivial but well-typed compiling function from L to L. (Henceforth we omit the subscript L.)

A well-typed partial evaluator can be applied to any program p accepting at least one rst-order input, together with a value a for p's rst input. Suppose p has type ! where is rst-order and a 2 [[ ]]. Then [[mix]] p a is a program pa whose result type is , the type of [[p]]a. Thus pa has type .

5This question does not arise at all in classical computability theory since there is only one data type, the natural numbers, and all programs denote functions on them. On the other hand, computer scientists are unwilling to code all data as numbers and so demand programs with varying input, output and data types.

344 Larger Perspectives

1.

Interpreter int 2

 

S

is well-typed if it has type6 8 :

 

S ! .

 

L

 

 

2.

Compiler comp

 

 

S

-

T

 

is well-typed if it has type

2

 

 

 

 

 

 

 

 

 

L

 

 

 

 

 

 

 

8 :

 

 

 

T.

 

 

 

 

 

 

 

 

 

S !

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

3.A partial evaluator mix is well-typed if it has type

8 : 8 : ! ! ! , where ranges over rst-order types.

Remark. The de nition of a well-typed interpreter assumes that all observable S-types are also L-types. Thus it does not take into account the possibility of encoding S-values.

16.2.4Self-application and types

De nitions involving self-application often (and rightly) cause concern as to their well-typedness. We show here that natural types for mix-generated compilers and target programs (and even cogen as well) can be deduced from the few type rules of Figure 16.1. Let source: S be an S-program denoting a value of type .

First Futamura projection

We wish to nd the type of target = [[mix]] int source. The following inference concludes that the target program has type = L, i.e. that it is an L program of the same type as the source program. The inference uses only the rules of Figure 16.1 and instantiation of polymorphic variables.

mix : ! ! !

[[mix]] : ! ! !

 

 

 

 

 

 

 

 

int :

 

S!

 

 

 

[[mix]] :

 

S! !

 

S!

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

source :

 

S

 

 

[[mix]] int :

 

S!

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

[[mix]] int source :

 

 

 

 

 

Second Futamura projection

The previous inference showed that [[mix]] int has the type of a compiling function, though it is not a compiler program. We now wish to nd the type of compiler =

6As a consequence, [[int]]L has type 8 : S ! , that is to say type t S ! t for every type t.

Types for interpreters, compilers, and partial evaluators 345

[mix]] mix int. It turns out to be notationally simpler to begin with a program p of more general type ! than that of int.

mix : ! ! !

[[mix]] : ! ! !

 

mix : ! ! !

 

[[mix]] : ! ! ! ! ! ! !

 

 

 

 

 

p : !

[[mix]] mix : ! ! !

 

 

 

[[mix]] mix p : !

 

Some interesting substitution instances. Compilers can be generated by the second Futamura projection: compiler = [[mix]] mix int. The type of int is S! ,

an instance of the type assigned to p above. By the same substitution we have compiler : S! . Moreover, was chosen arbitrarily, so [[compiler]] : 8 : S!

as desired.

The type of a compiler generator. Even cogen can be given a type, namely

! ! ! by exactly the same technique; but the tree is rather complex. One

substitution instance of cogen's type is the conversion of an interpreter's type into that of a compiler.

The type of [[cogen]] is thus ! ! ! , which looks like the type of the identity

function(!) but with some underlining. It is substantially di erent, however, in that it describes program generation. Speci cally

1.[[cogen]] transforms a two-input program p into another program p-gen, such that for any a2 D

2.p0 = [[p-gen]]a is a program which

3.for any d1 ,. . . ,dn 2 D computes

[[p0]]d1 . . . dn [[p]]a d1 . . .dn

One could even describe the function [[cogen]] as an intensional version of currying, one that works on program texts instead of on functions. To follow this, the type of [[cogen]] has as a substitution instance

[[cogen]] : !( ! )! ! !

346 Larger Perspectives

In most higher-order languages it requires only a trivial modi cation of a program text with type ( ! ) to obtain a variant with type ( !( ! )), and with the same (or better) computational complexity. So a variant cogen0 could be easily constructed that would rst carry out this modi cation on its program input, and then run cogen on the result. The function computed by cogen0 would be of type:

[[cogen0]] : ! ! ! !

which is just the type of the curry transformation, plus some underlining.

16.3Some research problems

Exercise 16.1 Informally verify type correctness of a simple interpreter with integer

and boolean data

2

Exercise 16.2 Figure 16.1 contains no introduction rules for deducing that any programs at all have types of form t L. Problem: for a xed programming language,nd type inference rules appropriate for showing that given programs have given

types.

2

Exercise 16.3 For a familiar language, e.g. the -calculus, formulate a set of type inference rules that is su ciently general to verify type correctness of a range of

compilers, interpreters and partial evaluators.

2

Exercise 16.4 Find a suitable model theory for these types (domains, ideals, etc.). The type semantics given earlier uses ordinary sets, but for computational purposes it is desirable that the domains be !-algebraic, and that the values manipulated

by programs should only range over computable elements.

2

Chapter 17

Program Transformation

Program transformation has been studied for many years since John McCarthy's groundbreaking paper [180]. Burstall and Darlington devised a widely cited approach [43], and they and many others have developed computer-aided transformation systems. Their method can achieve asymptotic (superlinear) speedups, in contrast to the constant speedups we have seen in partial evaluation. A recurring problem with such systems is incomplete automation, resulting in the need for a user to read (and evaluate the e ciency of!) incompletely transformed programs.

Partial evaluation can be seen as an automated instance of Burstall and Darlington's `fold/unfold' methodology. We describe their framework, and express a simple and automatic online partial evaluator using their methodology. We also describe Wadler's `treeless transformer'. This is an application of Turchin's `supercompilation', and is another automated instance of fold/unfold transformations.

17.1A language with pattern matching

For simplicity and elegance of presentation we use a program form based on pattern matching, assuming all data are structured. Figure 17.1 shows the syntax, and Figure 17.2 the semantics of the language. Here a program is a collection of rewrite rules of form L -> R used to transform tree-structured values.

Additional syntactic restrictions: that function names, constructor names, and variable names are pairwise disjoint; that every variable in the right side of rule L -> R must also appear in its left side L; and left linearity: no variable appears twice in the same left side.

347

348 Program Transformation

Pgm :

Program

::=

Defn1 . . . Defnn

 

Defn :

Functionrule

::=

L -> R

 

L :

Leftside

::=

f P1. . . Pn

 

P :

Pattern

::=

x j c P1. . . Pn

 

t,s,R :

Term

::=

x

 

 

 

j

f t1. . . tn

Function call

 

 

j

c t1. . . tn

Construct term

f,g,... :

Fcnname

 

 

 

x,y,... :

Variable

 

 

 

c,... :

Constructor

 

 

 

 

 

 

 

 

Figure 17.1: Syntax of pattern-matching language.

17.1.1Semantics

Informally the semantics may be stated as follows: suppose one is given a program p and a ground term t, i.e. a term containing constructors and function names but no variables. First pick any function call f t1...tn in t, together with a function rule f P1...Pn-> R in p whose patterns match t1...tn. The call f t1...tn in t is then replaced by the right side R of the chosen rule, after substituting the appropriate terms for f's pattern variables. This may be done as long as any function calls remain in the term.

To make this idea concrete, we introduce a few concepts. A substitution is a mapping from variables to ground terms. Term t denotes the result of replacing every variable X in term t by X. (This is very simple since terms have no local scope-de ning operators such as .) Term t is called an instance of t. Alternatively one may say that t0 matches t if it is an instance of t.

A context is a term t[] with exactly one occurrence of [] (a `hole') at a place where another term could legally occur, for instance (f (g [] Nil) Nil). As a special case, [] by itself is also a context. Further, we let t[t0] denote the result of replacing the occurrence of [] in t[] by t0. So writing u = t[t0] points out a

particular occurrence of t0 in u, and t[t00] is the result of replacing that t0 in u by

t00.

Figure 17.2 de nes assertion t ) t0, meaning that t can be rewritten to t0 as described informally above.

Determinacy. Term t00 in t = t0[t00] is called a redex if it can be rewritten by the `function call' semantic rule 3. Further, t is said to be in normal form if it contains no redex. Reduction t ) . . . can be non-deterministic in two respects:

1.term t may contain several redexes; or

2.a redex may be reducible by more than one program rewrite rule.

A language with pattern matching 349

As for (1), the con uence property of the lambda calculus and many functional languages ensures that no two terminating computation sequences yield di erent results. Nonetheless, some deterministic strategy must still be taken in a computer implementation. A common approach is to select an `attention point' by restricting applicability of Rule 1 to certain contexts, i.e. to rede ne the notion of context to select at most one decomposition of a term t into form t0[t00] where t00 is a redex.

As for (2), many implementations apply program rules in the sequence they are written until a match at the attention point is found. Another approach is only to accept a program as well-formed if the left sides of no two rules L->R and L0->R0 have a common instance L = 0L0.

We adopt the latter, to avoid tricky discussions concerning the order in which rules can be applied when only partial data is available.

1.

Context

t0 ) t00

 

 

t[t0] ) t[t00]

 

 

 

 

2.

Transitivity

t ) t0, t0 ) t00

 

t ) t00

 

 

 

 

3.

Function call

L -> R 2 Pgm

 

 

 

L ) R

 

 

 

 

 

 

Figure 17.2: Rewriting semantics.

Call-by-value semantics. This can be de ned by restricting the context rule to

(Value context)

t0

) t00

t[t0]in

) t[t00]in

Here t[t0]in denotes a leftmost innermost context, meaning that t0 is a call containing no subcall, and that there is no complete call to its left. Thus calls within constructor subterms are evaluated, and function arguments are fully evaluated before substitutions are performed. Clearly any term containing a redex (i.e. not in normal form) also contains a redex in a value context.

Lazy evaluation. This is a variant of call-by-name much used in functional languages. Its characteristic is that neither user-de ned functions nor data constructors evaluate their arguments unless needed, e.g. to perform a match or to print the program's nal output. It is de ned by restricting the context rule as follows.

(Lazy context)

t0 ) t00

t[t0]lazy ) t[t00]lazy

where t[t0]lazy denotes a term with subterm t0 which is not contained in any constructor, and that there is no call beginning to its left. Note that a term may contain one or more redexes and still have no redexes in a lazy context (as in the next example).

350 Program Transformation

Consequently, evaluation is outside-in as much as possible. Constructor components of c E1 . . .En are not evaluated at all, and function arguments are not evaluated before substitutions are performed1. Further reductions will occur only if there is a demand (e.g. by the printer) for more output. Following is an example. The calls in the last line will not be not further evaluated unless externally demanded.

append [append (Cons A Nil) (Cons B Nil)]lazy (Cons C Nil) ) [append (Cons A (append Nil (Cons B Nil))]lazy (Cons C Nil) ) Cons A (append (append Nil (Cons B Nil)) (Cons C Nil))

The following example illustrates that lazy semantics assigns meaningful results to programs which would loop using call-by-value. Here, ones is intuitively an in nite list of 1's, and take u v returns the rst n elements of v, where n is the length of list u.

f n -> take n ones ones -> Cons 1 ones

take Nil ys -> Nil

take (Cons x xs) (Cons y ys) -> Cons y (take xs ys)

In the following example, lazy evaluation stops after the rst line. For illustration we assume that the printer demands the entire output, repeatedly forcing evaluation of constructor argments:

f (Cons A (Cons B Nil)) ) Cons 1 (take (Cons B Nil) ones) ) Cons 1 (Cons 1 (take Nil ones))

)Cons 1 (Cons 1 Nil).

17.2Fold/unfold transformations

Transformations as used by Burstall and Darlington [43] are shown in Figure 17.3, which shows how one adds new rules to a given set Pgm. Transformation `de ne' allows a new function rule to be introduced, or it may extend an existing de nition and so cause the program to be de ned on more inputs than before. `Instantiate' allows an existing rule to be specialized. `Unfold', like the `function call' semantic rule, replaces a call of a function de nition by its right side, after performing the needed substitutions. `Fold' does the converse.

Restrictions. If used too freely, the transformations of Figure 17.3 could lead to inconsistent or badly formed programs. The `De ne' restriction ensures that newly de ned function values cannot con ict with old ones. Instantiation will always lead

1In practice implementations use `call-by-need' to avoid repeated subcomputations. This does not change program meanings, just their e ciency.