Assignment Compilation By Transformation

Pt

Introduction

In this assignment, you will implement a series of small compilation by transformation phases. In this exercise, you will learn more about various aspects of compilation, program transformation, and Stratego: composing transformation tools, using traversals, and implementing rewritings as separate rewrite rules.

Tiger

We will assume the full Tiger language in this assignment. Tiger examples are available in the directory ~/.nix-profile/share/tiger-xmpl/. You can cat these examples to the standard output using the tiger-xmpl tool. For example,

$ tiger-xmpl -x fac

Remember that you can run Tiger programs using run-tiger. For example,

$ tiger-xmpl -x fac | run-tiger

Setup

Download the tar.gz template for this assignment and unpack it (tar zxvf template-sa6.tar.gz). The tarball contains:

  • Makefile for compiling the compiler and applying checks
  • Template Stratego module compile.str

Use make to compile the compiler. This create the executable compile. This is a complete Tiger to Tiger compiler, so you can apply it immediately to a Tiger program. For example,

$ tiger-xmpl -x fac | ./compile

The compile program takes several optional command-line arguments (all having default values). See --help for more information.

The Stratego module compile.str already contains an entry-point. The main strategy handled command-line options and defines an XTC composition. The real transformation is implemented in the strategy compile. This strategy composes the phases you have to implement. The actual phases are all implemented as identity phases, so you will not

make check runs some checks on the standard examples. You should extend the test suite yourself by adding Tiger programs to the checks directory. Specifying the expected outcome is not necessary: this is determined by running the program before and after compilation and comparing the output. It is important to output something in your own tests, since there is noting to compare without some output.

Phases

Desugar For Loop

In this phase, desugar a Tiger for loop by replacing it with a while loop. Note that the lower and upperbound of the loop should be evaluated only ones. Implement this in the for-with-while strategy. Use a separate rewrite rule for the actual rewriting and apply this rule in a strategy.

In this rewrite rule, you can use the new or newname strategy from the module string (consult the API documentation). These strategies create unique names that you can use for introducing new variables. The newname strategy takes a string prefix, which can make the resulting program more readable. new just produces a unique string.

Example

for x := 1 to 10 do
  print("Nog een keer!\n")

let var x := 1
    var a_0 := 10
 in while x <= a_0 do
      (print("Nog een keer!\n");
       x := x + 1)
end

Make Return Value Explicit

Returns are implicit in Tiger: the return value of a function is that value of the body, which is an expression. In this compilation phase, you must introduce a variable return in every function. Assign the result of the function to this variable.

Example

let function twice(x : int) : int = x + x
 in printint(twice(10))
end

let function twice(x : int) : int =
      let var return
       in return := x + x
        ; return
      end
 in printint(twice(10))
end

Mark Procedure Calls

In this phase, you have to mark all the procedure calls with a Proc constructor. This means that all Call 's at the statement level should be wrapped in a Proc constructor. Statement levels are:

  • body of the While
  • init elements of a Let
  • init elements of a Seq

Some expressions are at the statement level depending on the context in which they occur.

  • all expressions of a Seq that is itself at the statement level
  • all expression of a Let that is itself at the statement level
  • the body of a function declaration, if it is a procedure
  • the branches of an If, if the If itself is at the statement level.

This might sound difficult to implement, but in fact there is nice idiom for implementing such transformations. For each level, you can use a separate strategy.

At the statement level, you switch to the expression level when necessary and stay at the statement level if there is a statement construct at the statement level. Call 's at the statement level are wrapped in a Proc.

At the expression level, you do not wrap Call 's and switch to the statement level for the at a Seq or Let construct. For subexpressions, you just traverse.

You can use this template:

  mark-procedure-calls =
    let stm-level =
          ...

        exp-level =
          ...

     in stm-level
    end

Example

let function twice(x : int) : int = x + x
    var x : int

 in twice(1)
  ; x := twice(2)
  ; while 1 do twice(3)
  ; if 1 then twice(4) else twice(5)
end

...
  [ Proc(Call(Var("twice"), [Int("1")]))
  , Assign(
      Var("x")
    , Call(Var("twice"), [Int("2")])
    )
  , While(
      Int("1")
    , Proc(Call(Var("twice"), [Int("3")]))
    )
  , If(
      Int("1")
    , Proc(Call(Var("twice"), [Int("4")]))
    , Proc(Call(Var("twice"), [Int("5")]))
    )
  ]
...

Simplify Expressions

In this phase, you have to simplify expressions to a from where all arguments of operators are atomic. Atomic expressions are:

  is-atomic = 
    Var(id) + String(id) + Int(id) + NilExp()

You can implement these simplifications by lifting the more complex (not atomic) operators. The following util make-vardecs can help to make this implementation easier by separating the introduction of new declarations in a separate strategy. Use this strategy to lift complex expressions out of Call, BinOp, RelOp, Seq, and so on.

Tip: don't create rewrite rules for these expressions, but for these expressions at the statement level. For example, create a rewrite rule for an BinOp in an Assign instead of rewrite rule for a BinOp.

  /**
   * @type List(Exp) -> (List(Dec), List(Exp))
   */
  make-vardecs =
    fetch(not(is-atomic))
    ; map(MkVarDec)
    ; unzip
    ; (concat, id)

  MkVarDec :
    e -> ([VarDec(x, NoTp, e)], Var(x))
    where <not(is-atomic)> e
   ; new => x

  MkVarDec :
    e -> ([], e)
    where <is-atomic> e

Example

let var x : int
    var y : int

 in x := 1 + 2 * 3
  ; y := f(4 * 5, 6)
end

let var x : int
    var y : int
 in let var a
     in a := 2 * 3;
        x := 1 + a
    end;
    let var c
     in c := 4 * 5;
        y := f(c, 6)
    end
end

Remove Structered Control-flow

In this phase, replace the structured control-flow construct while and if with goto and label. You can use newname or new to generate unique identifiers for labels.

Example

let var x : int := 0

 in while x < 10 do
      x := x + 1

  ; printint(x)
end

let var x : int := 0
 in let var d
     in label a;
        d := not(x < 10);
        if d goto c;
        x := x + 1;
        goto a;
        label c
    end;
    printint(x)
end

Collect Declarations at Top-level

In this phase, you have to collect all variable declarations at the top-level. For local variables, this is directly in the body of a function declaration. For global variables, this is a let declaration at the top-level of the Tiger program.

Implement this transformation using the collect strategy that we learned about in the previous lecture. Try to reuse the same strategy for collecting local and global variables.

Example

let function f() =
      let var y : int
       in y := 1
        ; if 1 then
            let var x : int
             in x := 2
            end
       end

    var z : int
 in z := 3
  ; printint(z)
end

let var z : int
 in let function f() =
          let var y : int
              var x : int
           in y := 1;
              if 1 then
                x := 2
          end
     in z := 3;
        printint(z)
    end
end

Flatten Sequences

In this phase, you have to remove all nested sequences in order to create a simple list of statements. For this phase, you can use the rewrite rules available in the module Tiger-Sugar-Rules.str. This module is already installed and imported, so you don't have to copy the rules.

You can implement the flattening in two stages. First, you split all the existing Let and Seq constructs into single declarations and expressions (reuse rules from Tiger-Sugar-Rules). Second, you can combine a set of Let and Seq flattening rules and apply these in an innermost fashion.

Example

let var x : int
    var y : int
 in ( x := 1 * 2
    ; y := 3
    ; ( x := 4
      ; y := 5
      )
    )
end

let var x : int
    var y : int
 in x := 1 * 2;
    y := 3;
    x := 4;
    y := 5
end

Removed Procedure Call Marks

The Proc markers are now no longer necessary. You have to remove them to pretty-print the Tiger program, so drop them in a simple traversal.