Assignment Partial Evaluation
Pt04
Assignment
The goal of the lab assignment for the second half of this course is
to write a partial evaluator for Tiger. You will have to hand in your
implementation at the end. For your convenience the development is
broken down in small steps.
We start with Tiger-FOF, the restriction of Tiger to a first-order
functional language, i.e., without the imperative constructs
(assignments, loops) and without arrays and records. Once you have a
partial evaluator for that language it can be extended in various
directions.
The Tiger-FOF specializer can be further decomposed into binding-time
analysis and specialization proper (and a bunch of standard
components). These tasks can also be approached separately by using an
intermediate format in which binding-time analysis has been made
explicit using annotations.
So the first task is to write a specializer for Tiger (FOF) programs
with binding-time annotations.
Components of the Partial Evaluator
The complete partial evaluator for Tiger-FOF consists of the following
components:
- parse-tiger : parser
- Tiger-Desugar : desugaring
- FOF-Format : a format checker that checks the language restrictions
- Tigerfof-BTA : binding-time analysis
- Tigerfof-PE : partial evaluation
- ... : optional transformations
- Tiger-Ensugar : undo some desugarings to produce readable code
- pp-tiger : pretty-print tiger program
Most of these components are available in the standard Tiger
distribution. The composition is made with XTC, the transformation
tool composition framework.
Template Package
Since the composition of a complete transformation framework is too
complex for command-line build-management, we have constructed a
template package for your development, containing the composition,
template modules for specializer components, and some tests.
You can get the package at
The package should be configured as follows:
tar zxf partial-eval-0.3.tar.gz
cd partial-eval-0.3
./configure --prefix=/tmp/$HOME --with-xt=/usr --with-tiger=/projects/stratego/tiger
make
make install
(Assuming that you work on the Linux machines in the lab. Of course
you can use a different installation directory.)
Version Management
Create a subversion repository pt03-name1-name2 at
http://svn.cs.uu.nl
for your project and import the source of the template package in
it. Commit regularly and make tags of working versions. Make the
repository readable for visser and mbravenboer so that we can access
it.
Using the Partial Evaluator
The package implements the composition described above via the
tigerfof-specialize tool. Eventually, you should be able to
completely specialize programs as follows:
tigerfof-specialize -i power.tig -o power9.tig --arg2 9
The tool can also be used to only specialization or only binding-time
analysis, by turning off the appropriate tools. Thus, to only specialize
turn off binding-time analysis:
tigerfof-specialize -i power-bta.tig -o power9.tig --arg2 9 --bta off
This assumes that
power-bta.tig is a program with binding-time
annotations.
To test binding-time analysis you can turn off specialization:
tigerfof-specialize -i power.tig -o power9-bta.tig --arg2 9 --pe off
It is even possible to turn off pretty-printing and ensugaring in order
to inspect the raw ATerms produced by your transformation. For
instance to see the result specialization:
tigerfof-specialize -i power-bta.tig -o power9.tig --arg2 9 --bta off --pp off -ensugar off
Command-line Arguments in Tiger
Tiger programs can read command-line arguments with the built-in
function
argv(i), which returns the value (a string) of the i-th
argument. For example, the
power.tig program below reads two
arguments, converts them to integers (using
string2int) and computes
the power of the first argument to the second argument. To run the
program with the interpreter arguments should be passed to it as
--argi val. For example, to compute the 8th power of 2, run
power.tig as:
run-tiger -i power.tig --arg1 2 --arg2 8
Now the point of partial evaluation is that a program can be
specialized to one or more of its arguments. Thus, the partial
evaluator should also interpret the command-line arguments and
incorporate them in the specialized program. For example, the power
program can be specialized to its second argument, and the specialized
program can then be run with different values for its first argument
and
no value for its second argument:
tigerfof-specialize -i power.tig -o power9.tig --arg2 9
run-tiger -i power9.tig --arg1 2
run-tiger -i power9.tig --arg1 4
run-tiger -i power9.tig --arg1 6
The power Program
The following program is the basic example that your partial evaluator
should work on. It reads two strings from the command-line, converts
them to integeres, and then computes the power using the
power
function, which calls several auxiliary functions.
---------------------------------
let
function mod(x : int, y : int) : int =
x - ((x / y) * y)
function even(x : int) : int =
mod(x, 2) = 0
function square(x : int) : int =
x * x
function power(x : int, n : int) : int =
if n = 0 then 1
else if even(n) then square(power(x, n / 2))
else x * power(x, n - 1)
function main() =
printint(power(string2int(argv(1)), string2int(argv(2))))
in main() end
---------------------------------
Note the convention of having a call to the function
main() as the
body of the (outermost) let.
Binding-Time Annotations
Partial evaluation is split into two phases: binding-time analysis and
specialization. The process of binding-time analysis produces a
program with
binding-time annotations. Code between
<...>
indicates
dynamic code, which should be evaluated at run-time, and
code in
~... is
static code, which should be evaluated during
specialization. (This notation is copied from the MetaML language.)
Here is the power program with binding-time annotations which indicate
that the second argument is provided at specialization-time and the
first at run-time. The rest of the program is annotated consistently:
---------------------------------
let
function mod(x : int, y : int) : int =
x - ((x / y) * y)
function even(x : int) : int =
mod(x, 2) = 0
function square(x : <int>) : int =
<x * x>
function power(x : <int>, n : int) : <int> =
if n = 0 then <1>
else if even(n) then square(power(<x>, n / 2))
else <x * ~power(<x>, n - 1)>
function main() =
<printint(power(string2int(argv(1)), ~string2int(argv(2))))>
in main() end
---------------------------------
Note that the types of dynamic function arguments are annotated to
indicate their binding-times.
A program with binding-time annotations can be `run', resulting in a
specialized `residual' program. For example, when specializing the
program above as:
tigerfof-specialize -i power-bta.tig -o power9.tig --arg2 9 --bta off
the result is the residual program:
---------------------------------
let function k_0(e_0 : int) =
let var q_1 : int := e_0 * 1
var r_1 : int := q_1 * q_1
var s_1 : int := r_1 * r_1
in e_0 * (s_1 * s_1)
end
in printint(k_0(string2int(argv(1))))
end
---------------------------------
Note that a new function
k_0 has been generated that computes
e_0
to the power of 9.
Specialization
The first part of the assignment is to write the specializer component
Tigerfof-PE.str. The test script
xmpl/fof/power-test applies the
tool in various configurations to the power example program. As usual
you should develop your own additional (smaller) test programs to test
specific situations. By all means add these to your repository.
Step 1: Incorporate Static Arguments
The start of partial evaluation is the incorporation of the static
arguments of a program. This can be done by evaluating the
argv(i)
expressions in the program. Command-line arguments are processed using
the 'command-line-options' strategy in the template module
Tigerfof-PE.str. An argument can be obtained by looking it up in
the configuration table:
<conc-strings; get-config> ("--arg", <int-to-string>i) => s
But you can reuse the implementation in module
Eval-Primitive.str
from the tiger installation. It defines a rule
EvalPrim for
evaluating expressions
argv(i). See
Write a strategy for tiger-pe that replaces all
argv(i) expressions
by their value on the command-line, if provided of course.
Step 2: Evaluate Static Code, Residualize Dynamic Code
The next step is to evaluate those expressions that should be
evaluated statically (the code in
~...) and produce a residual
expression in which static expressions have been replaced with their
value. You can use the evaluation rules in the following modules:
It is useful to write
two strategies
reduce-static and
reduce-dynamic_ which reduce static and dynamic code, respectively.
(See also Figure 5.7 on page 115 in the partial evaluation book.)
Step 3: Unfold Static Function Calls
Next extend the
reduce-static strategy such that static function
calls are unfolded (inlined). Use the approach to inlining described
in Chapter 11 (Scoped Dynamic Rewrite Rules). That is, generate an
unfolding rule when encountering a function definition and use it at
its (static) call sites. Create
let var declarations to bind the
actual parameters to the formal parameters of the function. That is,
if the function definition is
function f(x1 : t1, x2 : t2) : t3 = e
replace a function call
f(e1,e2) by
let var x1 : t1 := e1
var x2 : t2 := e2
in e
end
Step 4: Propagate Static Let Bindings
Variables that are bound to a constant expression can be substituted in the
body of the let. That is, a variable declaration
let var x := ~e1 in e2
can be reduced to
e2 in which
x is replaced by
e1.
Note that you can use a dynamic rule rewriting
x to
e1
for this substitution.
Step 5: Specialize Dynamic Function Calls
Dynamic function calls should not be unfolded, but rather specialized. That is,
a new function definition should be created that specializes the function to
the static arguments used in the call. Thus, a call
<f(e1,~e2)>
should be replaced with a call to a new function
g with the dynamic argument
e1:
<g(e1)>
Given the definition of function
f:
function f(x1 : <tid1>, x2 : tid2) : <tid3> = e
a new function definition for
g should be generated, which binds
the static argument
e2 to the corresponding parameter
x2:
function g(x : tid1) : tid3 =
let var x2 : tid2 := e2 in e end
The body of this specialized function should of course be reduced in order
to propagate the static parameter.
You can collect all specializations for a function using a dynamic
rule. Make sure to memoize a generated specialization such that the
next time a call to the same function with the same static parameters
is encountered, a call to the same new function is produced.
Binding-Time Analysis
Binding-time analyis works similarly to specialization. Instead of
reducing an expression to a value, it is reduced to an expression
with binding-time annotations.
Plain Binding-Time Analysis
Start with a simple binding-time analysis that attaches binding-time
annotations to expressions according to the following rules:
- A constant is static
- A function call / operator / record access is static if all its arguments are static
- A record creation is static if all its field initializations are static
- A conditional is static if its condition is static
In all other cases the expression is dynamic.
The binding-time of a variable is determined by the function
parameter or variable declaration that it corresponds to and
should be propagated from the declaration to the use site
(using a dynamic rule of course).
Binding-Time Analysis by Transformation
Binding-time analysis can be implemented by a program transformation
that propagates binding-time annotations. For example, an
annotation of the arguments of a function, is transformed to an
annotation of the function call as follows:
f(<e1>,~e2) ---> <f(e1,~e2)>
Here the call becomes dynamic, since one of its arguments is. This
propagation can be expressed using simple rewrite rules.
Specializing Functions to Binding-Time
To achieve
poly-variant binding-time analysis, each combination
of binding-times for the arguments of a function should be used
to generate a new function declaration, in which the body is annotated
according to the binding-times of the parameters. The function should
be given a new name to avoid name clashes. A good convention is to
use the letters
d and
s to indicate the static and dynamic parameters.
Thus,
power(x,5) is annotated as
<power_ds(x,~5)>
Note that for each function call an corresponding function should be
generated. Of course, only one function should be generated for
each combination of binding times, and all other calls with the
same binding times should be replaced with a call to that function.
Dynamic rules come in handy to memoize the specialization of
a function call and to collect all specializations of a function
declaration.
The following is the
power program with binding-time annotations
assuming that the second argument is static.
---------------------------------
let
function mod_s(x : int, y : int) : int =
x - ((x / y) * y)
function even_s(x : int) : int =
mod(x, 2) = 0
function square_d(x : <int>) : <int> =
<x * x>
function power_ds(x : <int>, n : int) : <int> =
if n = 0 then <1>
else if even_s(n) then <square_d(power_ds(x, ~(n / 2)))>
else <x * power_ds(x, ~(n - 1))>
function main_s() =
<printint(power_ds(string2int(argv(1)), ~string2int(argv(2))))>
in main_s() end
---------------------------------
Maximize Call Unfolding (Transition Compression)
In order to reduce the number of specialized functions, it is
attractive to inline as many function calls as possible. This
can be achieved by adapting the binding-time analysis to make
all calls that are dynamic, but do not occur under a dynamic
conditional 'static'. This indicates to the specializer that
it can be inlined. Since dynamic functions expect (some of)
their parameters to be dynamic, these should be annotated as
f(<~g(<e1>,e2)>, e3)
to indicate that the call to
g should be unfolded, but that
the result is
code that should be passed to function
f.
This can be achieved by a transformation on the plain binding-time
annoation described above. For the
power program this results
in:
---------------------------------
let
function mod_s(x : int, y : int) : int =
x - ((x / y) * y)
function even_s(x : int) : int =
mod(x, 2) = 0
function square_d(x : <int>) : <int> =
<x * x>
function power_ds(x : <int>, n : int) : <int> =
if n = 0 then <1>
else if even_s(n) then <square_d(<~power_ds(<x>, (n / 2))>)>
else <x * <~power_ds(<x>, (n - 1))>>
function main() =
<printint(<~power_ds(<string2int(argv(1))>, string2int(argv(2)))>)>
in main() end
---------------------------------
Strategies
There are many variations on the basic strategies for binding-time analysis and specialization.
Unfold All Static Calls
When applying the transition compression transformation described above it is safest to
leave calls under dynamic conditionals untouched, i.e., dynamic. Sometimes, a dynamic
conditional has internal static code; in a variation calls in such static parts can be made
static.
Unfold All Calls
Alternatively
all function calls can be unfolded during specialization. This only works
with the following option.
Memoize Unfolding
Often it is not possible to determine during binding-time analysis whether a specific
call can be safely unfolded or not. An alternative strategy is to unfold all/more calls,
but to fall back to specialization if it turns out that there where more calls to
the same function with the same static parameters; in which case code duplication is
avoided by specialization.
Substitute Values
The values and which are substituted for variables can also be varied. Code with records
yield a nicer program (not necessarily more efficient) when record accesses of the form
lv.x are propagated to their usess.
Additional Transformations
Finally, the offline partial evaluator described here uses only limited program analysis
to make decisions about unfolding versus specialization, since only limited information
is available dusing binding-time analysis. Therefore, specialization can be complemeneted
with additional transformations. Some example transformation components that could be
applied are
- Tiger-Inline: inlines function declarations which are 'simple' or which are called only once
- Tiger-ElimDead: eliminates function and variable declarations that are not used in the program
Testing the Partial Evaluator
A new template package is available with a number of test programs and scripts to apply the
partial evaluator to the programs. The new package also contains an extension of the driver
program with switches to turn on or off the strategies discussed above. The new programs
are in subdirectory fofrec, which you can add to your own codebase. The examples are in
directory
xmpl/fof.