Assignment Patterns Rules Strategies Traversals
Pt
Introducing the Stratego Compiler and Interpreter
In this assignment you will use Stratego to implement a number of
small Java program transformations. Stratego can be executed in an
interpreter or by compiling it first and then running the executable
program.
Stratego Programs
A Stratego programs has a main strategy, which can optionally be
specified on the command-line and defaults to
main. Usually, this
main strategy is an
io-wrap of some strategy. The
io-wrap
strategy takes care of command-line arguments, input and output.
The skeleton of the Stratego program for this assignment is:
module pt2
imports
Java-15
libstrategolib
strategies
main =
io-wrap(exc-4)
strategies
exc-4 =
id
Note that this program imports Java-15. This module is part of
java-front and for this exercise it is easier to copy it to your
working directory:
$ cp ~/.nix-profile/share/java-front/Java-15.rtree .
This Stratego Signature for Java describes the AST representation of Java in
Stratego, similar to an RTG. You can find the complete signature at
~/.nix-profile/share/java-front/Java-15.str.
Stratego Compiler
Stratego programs can be compiled using the Stratego Compiler:
$ strc -i pt2.str
The resulting program will be
much faster than the interpreter, but
the compilation itself takes some time. However, for larger examples
this additional compilation time is well spent.
If the Stratego program imports libstrategolib (which is the case for
pt2.str, then you need to link with libstratego-lib:
$ strc -i pt2.str -la stratego-lib
After compilation, the Stratego program can be applied in a pipeline:
$ echo "x == true" | parse-java -s Expr | ./pt2 | pp-java
x
Stratego Interpreter
A Stratego program can also be interpreted. This is usually a bit
faster then compilation followed by a single application, but soon
gets slower if you want to apply the program multiple times.
$ echo "x == true" | parse-java -s Expr | stri pt2.str | pp-java
x
Of course, you'll get a different result, since this is one of the assignments.
Stratego Shell
For interactively learning the Stratego language, you can use the
Stratego Shell. You can start the Stratego Shell using the shell
command
stratego-shell. In the Stratego Shell, use the following
command to learn more about the commands of the Stratego Shell.
stratego> :help
As an example of a command, you can close the Stratego Shell with
:quit,
:exit or
:q.
The Stratego Shell can be included in a pipe. In this assignment we
will use the Java language, so you can use
parse-java to parse input
program and then pipe the result into the Stratego Shell. Optionally,
you can specify a sequence of commands on the command line, separated
by
;;.
$ echo "class Foo { void Foo() {} }" | parse-java -s TypeDec | \
stratego-shell --cmd 'import pt2;; exc-14' | pp-java
class Foo
{
Foo()
{ }
}
Note that you can invoke any strategy in this way (in this case
exc-14) and that the strategy is not wrapped in an
io-wrap. Also, note
that this is again the solution of one of the exercise. In your skeleton, this
strategy is not yet defined and if you define it as id, the output will be
the same as the input.
If you don't provide a
--cmd argument, then the interactive Stratego
Shell will be started and the current term will be the current term of
the pipe. For example:
$ echo "if(x != y);" | parse-java -s BlockStm | stratego-shell
stratego> :show
If(NotEq(ExprName(Id("x")),ExprName(Id("y"))),Empty)
stratego>
Stratego Modules
Stratego Modules are stored in files with the
.str extension. You
can import Stratego modules into the Stratego Shell with the command
import.
stratego> import SmallExercises
Standard library modules can also be included in this way:
stratego> import integers
But, it is more common to import the headers of the separately
compiled Stratego Library:
stratego> import libstrategolib
You can use tab expansion to see what strategies are defined.
Introspection and Debugging
In the Stratego Shell, you can use some commands for debugging and
inspecting the current environment.
-
:show prints the current term
-
:binding and :bindings commands can be used to reflect over the current state of your Stratego program.
-
:trace on will from now print a very detailed description of the strategies that are evaluated by the Stratego Shell. You can turn it off using :trace off.
- The strategy
#bar introduces a break point bar. If evaluation reaches this break point, then a break shell will be opened. You can inspect the state of the program in this break shell and continue evaluation using :continue.
Using the Stratego Library
In some parts of this assignment, you are allowed to use the Stratego
Library. The library contains many useful strategies for io, list
processing, traversals, and so on. API and source documentation of the
Stratego Library is available online (see the navigation bar).
Pattern Matching and Strategies
Create a new Stratego module
pt2.str based on the previously given
template and define the following strategies. Please follow the
suggested naming suggestions (
exc-x). Prefix
all your own
strategies, including auxiliary ones, with
exc.
Most of the exercises are examples of bug pattern that many bug
detection tools will support. It's interesting to see how easy these
bug detections are to implement in a language that supports pattern
matching.
In this part of the assignment, you are not allowed to use
congruences, generic traversals, or library strategies, unless
explicitly mentioned.
Find Control-flow Statements (exc-1)
Implement a strategy that succeeds if the
current term is a Java
control-flow statement, such as the if statement, for and while
loop. Completeness is not important, but support at least 3
constructs.
Parse the inputs using
parse-java -s BlockStm
Example:
echo "while(true) doFoo();" | parse-java -s BlockStm | stratego-shell --cmd 'import pt2;; exc-1'
The strategy should fail for non control-flow statements.
Find Empty If Statements (exc-2)
Implement a strategy that succeeds if it is applied to an
if
statement that has no statements (i.e. the empty statement) in its then or else branch (for
example
if(x);). Usually, this indicates a bug.
Find Equality Check of True (exc-3)
Implement a strategy that succeeds if the current term is an equality
operator where one of the operands is the literal
true. Equality
comparison to
true is usually considered poor style.
Parse the inputs using
parse-java -s Expr
Remove Equality Check of True (exc-4)
Implement rewrite rules that remove the equality check to true. So,
the comparison of an expression to true should be rewritten to the
expression itself. Apply the rewrite rules in the strategy
exc-4,
and make it succeed in all cases.
Again, parse the inputs using
parse-java -s Expr
Find Control-flow Statements without Braces (exc-5)
Implement a strategy that succeeds if the current term is Java
control-flow statement, where one of the statements of control-flow is
not a block. For example, it should succeed for the Java statement
if(x) return y;, but should fail for the Java statement
if(x) { return y; }.
Again, completeness is not important, but at least support
a few different control-flow statements.
Note that you can give all rules the same name, but you can also use
different names and combine them yourself with the choice operator.
Parse the inputs using
parse-java -s BlockStm
Introduce Braces for Control-flow Statements (exc-6)
Implement a set of conditional rewrite rules that introduce blocks in
the branches control-flow statements, but only if the blocks are
missing. Cover a few different control-flow statements. Again, define
the rules separately and try to apply them in exc-6.
Parse the inputs using
parse-java -s BlockStm
Find Self Assignment (exc-7)
Implement a strategy that succeeds if the current term is an
expression that is a self-assignment, i.e. the left-hand side is
identical to the right-hand side. Self-assignment is a common bug in
constructors and setter methods.
Parse the inputs using
parse-java -s Expr
Invert .equals Constant (exc-8)
For invocations of the
equals method that involve a string literal,
it is often a good idea to have the string literal on the left-hand
side and not as the argument of the invocation. This will more
gracefully handle the case where the other expression is
null.
Define a conditional rewrite rule that rewrites such comparisons, but
only if just one of the expressions is a literal. Example:
bar.equals("foo") should be rewritten to
"foo".equals(bar).
It is ok to assume that the
MethodName(AmbName(Id("?")) is a
variable, since we cannot apply disambiguation yet: for
disambiguation, you need to have a full compilation unit, and for
transforming a compilation unit we need traversals.
Try to apply the rewrite rule in the strategy
exc-8
Parse the inputs using
parse-java -s Expr
Invert If Not (exc-9)
Implement a rewrite rule that swaps the branches of an if statement if
the conditional is an inequality check. Of course, you also need to
change the inequality conditional to an equality. Again, invoke the
rewrite rule from
exc-9.
Example:
if(x != y)
doSomething();
else
doSomethingElse();
should be rewritten to:
if(x == y)
doSomethingElse();
else
doSomething();
Define a similar rule for the not operator:
if(!x)
doSomething();
else
doSomethingElse();
should be rewritten to:
if(x)
doSomethingElse();
else
doSomething();
Get Last Statement of Method (exc-10)
Define a strategy that returns the last statement of a method. For
example, if applied to
void foo() { bar(); return x; } the strategy
should return
return x;.
The strategy must be defined using a single pattern match, so you are
not allowed to use composition operators (sequential, or
whatever). But, you are allowed to define an auxiliary strategy for
getting the last element of the list of statements.
Parse the input, which is a method declaration, using the start symbol
ClassBodyDec.
Apply First (exc-11)
Define a strategy that applies a strategy parameter
s to the first
term of a list for which
s succeeds. The strategy should fail if the
argument cannot be applied to any of the elements. For example,
exc-apply-first(\ 1 -> 4 \) applied to
[3, 2, 1, 2, 3, 1] should
return
[3, 2, 4, 2, 3, 1].
Apply the strategy to a small example in
exc-11.
Empty Catch Clause (exc-12)
Define a strategy that if applied to a Java statement succeeds if the
statement is a try-catch block where one or more of the catch clauses
is empty. Of course, defining an empty catch clause is bad style. Most
bug detection tools support these checks.
Note that you can use the
exc-apply-first strategy for this. Check
that you solution can handle empty catches in a
try-catch-finally.
Introduce Print Stack Trace (exc-13)
Implement a strategy that inserts prints of the stack trace of an
exception in these empty catch clauses. This is done by invoking the
printStackTrace method of the exception. Write a separate rule for
introducing the print statement in a catch clause. Invoke this rule
from your strategy.
It's allowed to use the standard
map strategy in this exercise.
Resolve Method Constructor Confusion (exc-14)
Another common mistake in Java code is to define a return type in
front of a constructor, which means that it becomes a method. These
methods can be recognized by their name, which is the same as the
class in which they are defined.
Use the
exc-apply-first strategy to implement a strategy that
rewrites the first wrong method declaration of a class to a
constructor declaration. Of course, you define a rewrite rule for the
actual rewrite to the method. This rule has to be parameterized with
the name of the class. Example:
public class Foo {
int x;
void Foo() {
x = 0;
}
void bar() {
}
}
should be rewritten to:
public class Foo {
int x;
Foo() {
x = 0;
}
void bar() {
}
}
It is important that the strategy fails if there is no wrong method
declaration. Next, use the standard
repeat strategy to rewrite
all
wrong method declarations of a type declaration.
Parse the inputs with
parse-java -s TypeDec.
Semantics of Choice and Guarded Choice (exc-15)
Find an example that shows the semantic difference between
s1; s2 <+ s3
(left choice) and
s1 < s2 + s3 (conditional choice). You don't
have to use Java in this example: just use integers, lists, or
whatever you want.
Traversals
Congruences
In the following two exercises you are not allowed to use generic
traversals. All traversing code must be implemented using congruences.
However, we advice you to reuse the basic elements of a traversal. So,
you can define traversal primitives
all-Java,
some-Java, and
one-Java (if necessary) specific for Java constructs and define the
traversal strategies by composing them.
Recursive Rewriting (exc-16)
In the first part of this assignment you have defined a number of
rewrite rules that apply rewritings to the current term. In this
exercise, you apply these rewriting recursively all over the tree. For
this, define a congruence-based traversal that traverses a compilation
unit by descending into the statements used in the class. Support a
reasonable set of class body declarations and control-flow statements,
but don't spend too much time on this. The point is to illustrate a
congruence-based traversal and how the transformations are applied
during the traversal, not to deliver a product.
Note that because you have developed the rewrite rules first, you have
been forced to cleanly separate the traversal strategy from the
rewrite rules. This is good style. If you have to refactor the
previously defined rules (for example because you have defined the
rewriting entirely in a single strategy), then just do this and
remember why the rule was not reusable in this strategy.
Apply all your rewritings:
- Remove Equality Check of True
- Introduce Braces for Control-flow Statements
- Invert .equals Constant
- Invert If Not
- Introduce Print Stack Trace
- Resolve Method Constructor Confusion
Note that some rewrite rules apply to the same language
construct. Solve this in you rewriting strategy: don't hack this into
the reusable traversal primitives that you have defined. You don't
want to force all strategies to repeat the application of the rewrite
rules.
An example, illustrating some (not all!) of these problems:
public class Foo
{
void f()
{
if(x != true)
doFoo();
else
{
try
{
doBar();
}
catch(Exception exc)
{
}
}
}
void Foo(String x)
{
if(x.equals("Blaat"))
this.x = x;
}
}
$ parse-java -i Foo.java | ./pt2 | pp-java
public class Foo
{
void f()
{
if(x)
{
try
{
doBar();
}
catch(Exception exc)
{
exc.printStackTrace();
}
}
else
{
doFoo();
}
}
Foo (String x)
{
if("Blaat".equals(x))
{
this.x = x;
}
}
}
Replace Symbolic Constant (exc-17)
Sloppy user-interface code based on the Java
BorderLayout sometimes
uses string literals to define the position of a component. Of course,
the constants of the BorderLayout should be used for this. For
example,
p.add(component, "North") should be written as
p.add(component, BorderLayout.NORTH).
First, define a rewrite rule that if applied to a type-checked
expression, rewrites these sloppy invocations of the
add method of
javax.swing.JPanel to use these BorderLayout constants. Use the
DeclaringClass annotation of the expression to figure out if you are
dealing with the right
add method.
Next, apply this rewrite rule recursively to a Java program by reusing
the congruence-based traversal strategy of the previous exercise.
For this example you have to apply the dryad-front type checker
instead of just parse-java. Add the following code to your module to
make the DeclaringClass constructor available:
signature
constructors
DeclaringClass : Type -> Attribute
strategies
declaring-class-attr =
?_{a*}
; <fetch-elem(?DeclaringClass(<id>))> a*
Example:
import java.awt.BorderLayout;
import javax.swing.*;
public class Gui
{
public static JPanel gui()
{
JPanel p = new JPanel(new BorderLayout(10, 10));
p.add(new JLabel("Enter something:"), "North");
p.add(new JScrollPane(new JTextArea(20, 20)), "Center");
return p;
}
public static void main(String[] ps)
{
JFrame frame = new JFrame();
frame.getContentPane().add(gui());
frame.pack();
frame.setVisible(true);
}
}
$ dryad-front --tc on -i Gui.java | ./pt2 | pp-java
import java.awt.BorderLayout;
import javax.swing.*;
public class Gui
{
public static javax.swing.JPanel gui()
{
javax.swing.JPanel p = new javax.swing.JPanel(new java.awt.BorderLayout(10, 10));
p.add(new javax.swing.JLabel("Enter something:"), BorderLayout.NORTH);
p.add(new javax.swing.JScrollPane(new javax.swing.JTextArea(20, 20)), BorderLayout.CENTER);
return p;
}
public static void main(java.lang.String[] ps)
{
javax.swing.JFrame frame = new javax.swing.JFrame();
frame.getContentPane().add(gui());
frame.pack();
frame.setVisible(true);
}
}
Generic Traversals (exc-18 and exc-19)
Redefine the two previous exercises using generic traversals. Try to
find out if you need a single-pass, multi-pass, exhaustive, topdown,
or bottomup rewriting strategy. If you have figured this out, then
don't fix your previous congruence-based traversal if you find a
problematic case that you could not handle earlier.
Try the effect of some different traversals, even if you see the
solution immediately.
Mixing Generic and Data Type Specific Traversals
The traversal you've implemented in the previous exercises are not
that common in real program transformation problems. Instead of doing
the complete traversal specific for a certain data type or completely
generic, the traversal is often a mixture of a generic traversal and
some data type specific cases.
Find Return in Finally Clause (exc-20)
A return statement in a
finally clause should be avoid, because this
return statement can completely change the original result of the
method: a return or a thrown exception in the
try part. This is not
the purpose of a finally clause, therefore compilers often warn about
this:
class Fred
{
boolean error;
int g()
{
if(error)
throw new IllegalStateException();
else
return 4;
}
int f()
{
try
{
return g();
}
finally
{
return 5;
}
}
}
$ javac Fred.java -Xlint
Fred.java:22: warning: [finally] finally clause cannot complete normally
}
^
1 warning
For this exercise, write an transformation that annotates these
questionable return statements with the string "Error". This is not
entirely trivial. Thanks to local and anonymous class declarations,
not every return statement in a finally clause is incorrect. So, you
need to handle these constructs in a special way.
It is useful to think about this transformation as format
checking. For this problem, a Java source file should be written in
two different languages: in a finally clause, the allowed language is
a subset of the complete Java language: no return statements are
allowed. If we encounter a local or anonymous class declaration, then
we switch back to the complete language.
A useful pattern for the implementation of these problems is to have
two strategies, one for the complete language and one for the
subset. In both strategies, specific cases are handled and maybe the
context is switched to the other strategy. By default, they just
traverse.
You need about 10 lines of code for this, so if your strategy is much
longer, then you're doing something wrong
Example:
class Fred
{
int f()
{
try
{
return g();
}
finally
{
class Bla
{
void test()
{
return 6;
}
}
return 5;
}
}
}
$ parse-java -i Fred.java | ./pt2 | pp-aterm
...
[ Try(
Block(
[Return(Some(Invoke(Method(MethodName(Id("g"))), [])))]
)
, []
, Block(
[ ClassDecStm(
ClassDec(
ClassDecHead([], Id("Bla"), None, None, None)
, ClassBody(
[ MethodDec(
MethodDecHead([], None, Void, Id("test"), [], None)
, Block([Return(Some(Lit(Deci("6"))))])
)
]
)
)
)
, Return(Some(Lit(Deci("5")))){"Error"}
]
)
)
...
Submission
Pack your Stratego modules in a
.tar.gz or
.zip file and send your
solution by email to
martin@cs.uu.nl. The same day, you will get a
short notification that I've received your solution. If you don't get
this reply, then please contact me.
Questions
After the lab session you can still send questions to
pt@cs.uu.nl or
join
#stratego at
irc.freenode.com.