Table of Contents
Interpreting programs does not seem a typical application of program transformation. However, in partial evaluation a program is evaluated as far as possible. Also in simpler transformations, evaluating a part of a program is a common operation. Furthermore, in order to execute programs in our TIL language, it is useful have an interpreter.
This chapter shows how to construct an interpreter for typical elements of an imperative language, such as expressions, variable access constructs, and control-flow statements. The specification uses advanced features of Stratego such as traversal strategies, pattern match operations, match-project, and dynamic rewrite rules. So don't be surprised if you don't get it all on a first reading.
The interpreter developed in this chapter requires the parsing and simplification infrastructure from the previous chapters. That is, the interpreter operates on the abstract syntax tree of a program after simplification.
Expressions can be evaluated using a simple bottom-up traversal
of the AST. The bottomup
traversal strategy does
just that; it applies its argument strategy first to the leaves
of the tree, and then to each level above. The try
strategy makes its argument strategy always succeeding. Thus, the
eval-exp
strategy performs a bottomup traversal of
the AST of an expression, applying the Eval
rules
from module sim/til-eval. The
eval-exp
strategy is parameterized with a strategy
for evaluating variables; their values depend on prior
assignments in the program.
Figure 6.1. file: til/run/til-eval-exp.str
module til-eval-exp imports til-eval strategies eval-exp(eval-var) = bottomup(try( eval-var <+ EvalAdd <+ EvalMul <+ EvalSub <+ EvalDiv <+ EvalMod <+ EvalLeq <+ EvalGeq <+ EvalLt <+ EvalGt <+ EvalEqu <+ EvalNeq <+ EvalOr <+ EvalAnd <+ EvalS2I <+ EvalI2S ))
The essence of an imperative language is the manipulation of
values in the store, held by variables. In TIL variables are
introduced by the Declaration
construct. The values
of variables is set, or changed by the Assign
assignment statement, and the Read
input
statement. The Write
statement prints the value of
an expression. The interpreter uses the dynamic rewrite rule
EvalVar to store the mappings from variables to their values.
When encountering a variable declaration, the current scope
is labeled with the name of that variable, and its mapping is
undefined, reflecting the fact that the variable doesn't have a
value after being declared. Note that the scope of a variable in
TIL is the rest of the current block.
When encountering an assignment statement the
EvalVar
rule for the variable being assigned is
updated. Thus, after an assignment to a variable x
,
EvalVar
rewrites that variable to its value.
Similarly, the Read
input statement reads the next
line from the stdin
stream, decides whether it
represents an integer or a string, and defines the
EvalVar
rule for the variable.
Finally, the eval-exp
strategy is now defined in
terms of the parameterized eval-exp
strategy from
module run/til-eval-exp
using the EvalVar
rule as strategy for evaluating
variables. In addition, the VarUndefined
strategy is
provided to catch variables that are used before being assigned
to.
Figure 6.2. file: til/run/til-eval-var.str
module til-eval-var imports til-eval-exp strategies eval-declaration = (?Declaration(x) <+ ?DeclarationTyped(x, t)) ; rules( EvalVar+x :- Var(x) ) eval-assign = Assign(?x, eval-exp => val) ; rules(EvalVar.x : Var(x) -> val) EvalRead : FunCall("read", []) -> String(x) where stdin-stream; read-text-line => x eval-write = ?ProcCall("write", [<eval-exp>]) ; ?String(<try(un-double-quote); try(unescape)>) ; <fprint>(<stdout-stream>, [<id>]) VarUndefined = ?Var(<id>) ; fatal-err(|<concat-strings>["variable ", <id>, " used before being defined"]) eval-exp = eval-exp(EvalVar <+ VarUndefined <+ EvalRead)
What remains is the interpretation of control-flow statements. A
block statements simply entails the execution of each statement
in the block in order. Any variables declared within the block
are local, and shadow variables with the same name in outer
blocks. For this purpose a dynamic rule scope {| EvalVar :
... |}
is used to restrict the scope of
EvalVar
rules to the block. The statements in a
block are evaluated by map
ping the
eval-stat
strategy over the list of statements.
For the execution of the if-then-else
construct,
first the condition is evaluated. The EvalIf
rule
then evaluates the construct, reducing it to one of its
branches. The resulting block statement is then evaluated by
eval-stat
.
The while
statement is evaluated by transforming the
loop to an if-then-else
statement, with a
Goto
at the end of the body. The dynamic rule
EvalGoto
maps the goto statement for the new label
to the if-then-else
statement.
Figure 6.3. file: til/run/til-eval-stats.str
module til-eval-stats imports til-eval-var signature constructors Goto : String -> Stat strategies eval-block = ?Block(<eval-stats>) eval-stats = {| EvalVar : map(eval-stat) |} eval-stat = //debug(!"eval-stat: "); ( eval-assign <+ eval-write <+ eval-declaration <+ eval-block <+ eval-if <+ eval-while <+ EvalGoto //) eval-if = IfElse(eval-exp, id, id) ; EvalIf ; eval-stat eval-while = ?While(e, st*) ; where(new => label) ; where(<conc>(st*, [Goto(label)]) => st2*) ; rules( EvalGoto : Goto(label) -> <eval-stat>IfElse(e, st2*, []) ) ; <eval-stat> Goto(label)
To complete the interpreter we define an interpretation strategy
for the Program
constructor, and a main strategy
that takes care of I/O.
The program is compiled in the usual way, taking into account the
include paths for the ../sig
and ../sim
directories that contain the TIL signature and evaluation rules,
respectively.
Figure 6.4. file: til/run/til-run.str
module til-run imports libstrategolib til-eval-stats strategies io-til-run = io-wrap(eval-program) eval-program = ?Program(<eval-stats>) ; <exit> 0
Figure 6.5. file: til/run/maak
#! /bin/sh -e # compile til-simplify strc -i til-run.str -I ../sig -I ../sim -la stratego-lib -m io-til-run
Now it is time to try out the interpreter at our test1.til program that computes the factorial of its input. Note that the interpreter operates on a parsed and simplified version of the program; not directly on the text file.
Figure 6.6. file: til/xmpl/run-test1
# run test1.til after parsing and simplification echo "10" | ../run/til-run -i test1.sim.atil > test1.run