Jones N.D.Partial evaluation and automatic program generation.1999
.pdf342 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.
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 |
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).