Virtual method calls can contribute substantial overhead to the execution of a program in a highly object-oriented language, such as Cool. For this assignment, you will implement two different kinds of static virtual function resolution by determining what are the possible classes of the receiving object. At a dispatch, we see if the limited set of classes mean that only one possible method is the result. At the same time, we will check to see if the receiver could be void. If the receiver will never be void and the target of the dispatch can be statically resolved, a dispatch node will be converted into a static dispatch. In a real compiler, we would want to make use the cases when only one of the two conditions can be verified, but for here, it is easiest to require both (since the code generation of static dispatch assumes that the receiver cannot be void).
Similarly, with the information about what classes an expression can evaluate to and whether void is possible, one can prune a case expression to remove branches that will not execute.
Class hierarchy analysis uses the static type of the receiver to do static virtual function resolution. If none of the classes under the static type override the method, it can be statically resolved. In particular, if the static class of the receiver has no subclasses, it can be statically resolved.
However, as mentioned above, we also require that the receiver be guaranteed non-void in order to convert it into a static dispatch. For this task, we will use a simple local analysis:
self is never void.
(Not required for undergraduates.)
This analysis determines the set of types (classes) each expression
could evaluate to using an iterative whole-program analysis.
The
sets described below will need to be saved and then the
inequations below are iterated until there are no changes in these
sets.
The
sets will be computed on-the-fly using (recursive) virtual
methods.
Let
be the method named
selected for an object of dynamic
type
. Let
be the defining occurrence of
self in method
. Let
be the class/void for the default value of class
. We have
for
and
``void'' for all other classes.
Let
be the least set for each defining occurrence
(we
assume all defining occurrences are unique), and
be the least set for each expression
in the program such
that
and
satisfy the following conditions:
To get the analysis started, we note that the constructor for class
Main will be called:
For each attribute
in each class
:
,
Let
be
(
:
,...,
:
):
is
end; in
,
, ...
,
NB: internally, a super calls will be represented by a
static_dispatch node. The method
is known from the
binding, you don't need to do the lookup
.
Make sure that you use the stored value of
rather than computing it on the fly, which can lead to infinite recursion.
For each
with
,
let
be
(
:
,...,
:
):
is
end; in
,
, ...
,
NB: for dispatch nodes, the method binding is
not useful. You will need to re-implement method lookup
.
And as with static_dispatch, you should use the stored value of
.
Let
be
(
:
,...,
:
) is
end; in
,
, ...
,
NB This case actually doesn't occur. It is folded into
dispatch. Your analysis should always determine that these
calls can be statically resolved.
For each
with
,
if exists
, then
and
.
(Don't traverse branches unless they are selected!)
,
,
For
For
If literal is a string constants,
.
Similarly with boolean, integer and void literals.
This term may come one of the following sources:
| Class | Method |
|
|---|---|---|
| Object | Object |
|
| Object | abort |
|
| Object | type_name |
|
| Object | copy |
|
| IO | out_string |
|
| IO | out_int |
|
| IO | in_string |
|
| IO | in_int |
|
| Integer | equals |
|
| Boolean | equals |
|
| String | equals |
|
| String | concat |
|
| String | substr |
|
| Array | Array |
|
| Array | resize |
|
| Array | get_item |
|
| Array | set_item |
|
The occurrence of
in the rule for
get_item refers the formal parameter of this name in
set_item.
You will be given a skeleton optimizer that accepts analysis options. You should implement static class resolution and should use it to perform the following changes:
null node.
(Not a no_expr node because that would mean it was native, and
would get link errors.)resolve_level global
variable to select the kind of resolution desired. Do nothing is the
level is 0. If it has value ``1'' (or any non-zero value for
undergraduate students) then do CHA resolution. If it has anything
higher for graduate students, do 0-CFA.
Optimization at the AST level requires changing the AST for expressions. This can be done by having a recursive function for optimizing expression ASTs that returns a new AST (or perhaps the same AST) and callers update their children using these routines.
The 0-CFA analysis requires that a set be stored for every
set:
We associate with every formal parameter, every attribute, every let
bound variable, every `when' bound variable and every method body a
set of classes, initially empty.
The
sets can be computed by recursive member functions.
Each set may also contain an indication that the
variable could be void. Then we make repeated passes over the program
in which we compute the set for every expression and use this to
update the
sets involved. Once a fixed point is reached, we can use
the information for a receiver to determine whether it is statically
dispatchable. The analysis must be implemented to carry along possible
void-ness as well.
You need to write a test case (as one big file test.cl, for
easy of testing)
that checks for bugs in your implementations. Optimizations are very
easy to do wrong, so testing is important.
Evaluate the results of optimization (or the non-optimized ``control'' case) in two ways:
-keepstats in
the CS754 version of spim.)/afs/cs.uwm.edu/users/classes/cs754/src/homework6/benchmark
(For simplicity, each benchmark is a standalone Cool program.)Please create a readable table summarizing your results in README.
To get started, type
make -f /afs/cs.uwm.edu/users/classes/cs754/src/homework6/MakefileThis will get a number of files. The files you will edit are:
init_class_table yourself.