On this page:
8.1 Assignment Summary
8.2 Language Diagram
8.3 Register Allocation
8.4 Conflict Analysis
8.5 Undead Analysis
8.6 Optimizing assign-homes
7.5.0.17

8 Compiler 3: Register Allocation (Original)

8.1 Assignment Summary

The goal of this assignment is to introduce (1) "optimizing" compilation (2) register allocation, perhaps the most important "optimization". In this assignment, you will replace the assign-homes pass with assign-homes-opt, a version that tries hard to put variables into registers instead of in the frame.

This assignment is due Friday January 31, 2020 at 11:59pm.

Assignment Checklist

You should find a new repository in your https://github.students.cs.ubc.ca account named a3_<team ids> with a code skeleton and a support library. You should complete the assignment in that git repository and push it to the GitHub Students instance.

You should first copy your solution to Compiler 2: Values-lang to x64 into the starter code provided. You can start from the code base of any of your team members.

If we patch the code skeleton or library after the release, the most recent version will be available here at a3-skeleton.rkt and a3-compiler-lib.rkt, and the functional graph library a3-graph-lib.rkt. The name of the skeleton is a3-skeleton.rkt to avoid accidentally overwriting your files, but your file in the Git repository should be named a3.rkt.

8.2 Language Diagram

8.3 Register Allocation

To assign abstract locations (variables) to physical locations efficiently, we need to know when any two variables are in conflict, i.e., cannot be assigned to the same physical location because they might both be used at the same time.

We start by designing a language, Conflict-lang, in which we assume we already know all the variables that are in use and which other variables they are in conflict with. In Conflict-lang, the program is annotated with: (1) a list of local variables declarations, like in Loc-locals-lang, and (2) a conflict graph, represented as an association list from a variable to the set of variables with which it conflicts. The language is essentially the same as Loc-locals-lang except for the addition of the conflict graph to the info field.

  p ::= (begin info s ...)
     
  info ::= 
((locals (aloc ...))
 (conflicts ((aloc (aloc ...)) ...))
 any ...)
     
  s ::= (set! aloc int64)
  | (set! aloc aloc)
  | (set! aloc (binop aloc aloc))
  | (halt aloc)
     
  binop ::= *
  | +

The (conflicts ((aloc (aloc ...)) ...)) form in the info field represents a conflict graph. Interpreted as an undirected graph, the variables represent nodes and there is an edge from each initial variable to each of the variables in the associated set. If there is an edge between any two nodes, then they are deemed to be in conflict. Interpreted as a dictionary, this form maps each variable to a set of its conflicts.

As in the previous assignment, the info field also contains a declaration of the (locals (aloc ...)) abstract locations that may be used in the program, and (as usual) possibly other non-required but useful information.

The register assignment algorithm takes a Conflict-lang program and produces a Loc-assigned-lang program, annotating the program with an assignment of variables to physical locations in the info field.

The core algorithm has a straight-forward recursive description, recurring over the list of locals and producing an assignment, i.e., an association list mapping variables to physical locations.
  • If the list of variables is empty, return the empty assignment.

  • Choose a low-degree variable from the input list of variables, if one exists. Otherwise, pick an arbtrary variable (such as the first one in the list).

    A low-degree variable is one with fewer than k conflicts. We pick k to be the number of registers available for use during register allocation.

  • Recur with the chosen variable removed from the input list and the conflict graph. The recursive call should return an assignment for all the remaining variables.

  • Attempt to select a register for the chosen variable. You cannot select registers to which conflicting variables were assigned by the recursive call. This attempt succeeds if a low-degree variable was chosen, and might fail otherwise (but it depends on which registers got allocated in the recursive call).
    • If you succeed in assigning a register, then add the assignment for the chosen variable to the result of the recursive call.

    • Otherwise, we cannot assign the choosen variable to a register. Instead, we spill it, i.e., we allocate a fresh frame location using a displacement mode operand.

This algorithm is an adaptation of the optimistic register allocation described in "Improvements to graph coloring register allocation" (ACM TOPLAS 6:3, 1994) by Preston Briggs, et al.

Exercise 1: Design and implement the parameter? current-assignable-registers, which stores the list of k registers available to the register allocator.

The default should be all registers except the following. You may not use the register rax, since it is used to patch instructions, or rbp, since it contains the frame pointer.

Exercise 2: Design and implement the function assign-registers, which performs graph-coloring register allocation. The source language is Conflict-lang and the target language is Loc-assigned-lang. The pass should attempt to fit each of the variables declared in (locals (aloc ...)) into a free register, and if one cannot be found, should allocate the variable a fresh frame location.

We provide a functional graph library, a3-graph-lib.rkt, for working with the conflict graph.

This algorithm is somewhat tedious to write purely functionally. You may use a single mutable variable in your implementation. Use box, unbox, and set-box! for an idomatic way of using mutable variables in Racket.

It will be difficult to keep enough variables live at one time to test spilling. You should use parameterize and current-assignable-registers to test spilling.

Below we give some examples. However, your register allocator may produce different assignments for the same program and still be correct. This is because the register allocation algorithm makes arbitrary choices in some places. Do not overfit to these examples.

Examples:
> (assign-registers
   '(begin ((locals (x.1))
            (conflicts ((x.1 ()))))
      (set! x.1 42)
      (halt x.1)))

'(begin ((assignment ((x.1 r15)))) (set! x.1 42) (halt x.1))

> (parameterize ([current-assignable-registers '(r9)])
    (assign-registers
     '(begin ((locals (x.1))
              (conflicts ((x.1 ()))))
             (set! x.1 42)
             (halt x.1))))

'(begin ((assignment ((x.1 r9)))) (set! x.1 42) (halt x.1))

> (parameterize ([current-assignable-registers '()])
    (assign-registers
     '(begin ((locals (x.1))
              (conflicts ((x.1 ()))))
        (set! x.1 42)
        (halt x.1))))

'(begin ((assignment ((x.1 (rbp - 8))))) (set! x.1 42) (halt x.1))

> (assign-registers
   '(begin ((locals (a.1 b.2 c.3 d.4 e.5))
            (conflicts ((a.1 (b.2 c.3 d.4 e.5))
                        (b.2 (a.1 c.3 e.5))
                        (c.3 (a.1 b.2 e.5))
                        (d.4 (a.1 e.5))
                        (e.5 (a.1 b.2 c.3 d.4)))))
     (set! a.1 1)
     (set! c.3 2)
     (set! b.2 a.1)
     (set! b.2 (+ a.1 c.3))
     (set! d.4 (* b.2 c.3))
     (halt d.4)))

'(begin

   ((assignment ((a.1 r12) (b.2 r13) (c.3 r14) (d.4 r14) (e.5 r15))))

   (set! a.1 1)

   (set! c.3 2)

   (set! b.2 a.1)

   (set! b.2 (+ a.1 c.3))

   (set! d.4 (* b.2 c.3))

   (halt d.4))

> (parameterize ([current-assignable-registers '(rbx rcx r9 r10)])
    (assign-registers
     '(begin ((locals (a.1 b.2 c.3 d.4 e.5))
              (conflicts ((a.1 (b.2 c.3 d.4 e.5))
                          (b.2 (a.1 c.3 e.5))
                          (c.3 (a.1 b.2 e.5))
                          (d.4 (a.1 e.5))
                          (e.5 (a.1 b.2 c.3 d.4)))))
        (set! a.1 1)
        (set! c.3 2)
        (set! b.2 a.1)
        (set! b.2 (+ a.1 c.3))
        (set! d.4 (* b.2 c.3))
        (halt d.4))))

'(begin

   ((assignment ((b.2 rbx) (a.1 rcx) (c.3 r9) (d.4 r9) (e.5 r10))))

   (set! a.1 1)

   (set! c.3 2)

   (set! b.2 a.1)

   (set! b.2 (+ a.1 c.3))

   (set! d.4 (* b.2 c.3))

   (halt d.4))

> (assign-registers
   '(begin ((locals (v.1 w.2 x.3 y.4 z.5 t.6 p.1))
            (conflicts
             ((x.3 (z.5 p.1 y.4 v.1 w.2))
              (w.2 (z.5 p.1 y.4 v.1 x.3))
              (v.1 (w.2 x.3))
              (y.4 (t.6 z.5 p.1 w.2 x.3))
              (p.1 (t.6 z.5 y.4 w.2 x.3))
              (z.5 (t.6 p.1 y.4 w.2 x.3))
              (t.6 (z.5 p.1 y.4)))))
       (set! v.1 1)
       (set! w.2 46)
       (set! x.3 v.1)
       (set! p.1 7)
       (set! x.3 (+ x.3 p.1))
       (set! y.4 x.3)
       (set! p.1 4)
       (set! y.4 (+ y.4 p.1))
       (set! z.5 x.3)
       (set! z.5 (+ z.5 w.2))
       (set! t.6 y.4)
       (set! p.1 -1)
       (set! t.6 (* t.6 p.1))
       (set! z.5 (+ z.5 t.6))
       (halt z.5)))

'(begin

   ((assignment

     ((v.1 r15) (w.2 r11) (x.3 r14) (y.4 r12) (z.5 r13) (t.6 r14) (p.1 r15))))

   (set! v.1 1)

   (set! w.2 46)

   (set! x.3 v.1)

   (set! p.1 7)

   (set! x.3 (+ x.3 p.1))

   (set! y.4 x.3)

   (set! p.1 4)

   (set! y.4 (+ y.4 p.1))

   (set! z.5 x.3)

   (set! z.5 (+ z.5 w.2))

   (set! t.6 y.4)

   (set! p.1 -1)

   (set! t.6 (* t.6 p.1))

   (set! z.5 (+ z.5 t.6))

   (halt z.5))

When debugging your register allocator, you might try comparing your allocator to the graph coloring algorithm provided by Racket Generic Graph Library. This provides an implementation of graph coloring, although it does not perform spilling so it will not work in general. You cannot use this library in the implementation of your compiler; it may only be used for testing. Unfortunately, it uses a different representation of graphs, so you will need to write conversion procedures.

You may also choose to preserve additional info fields in the output of assign-registers. We define Loc-assigned-lang v2 below, which assumes that you preserve the conflicts and locals info fields. Note that Loc-assigned-lang v2 also meets the specification for Loc-assigned-lang, so every Loc-assigned-lang v2 program is a Loc-assigned-lang program.
  p ::= (begin info s ... (halt aloc))
     
  info ::= 
((assignment ((aloc rloc) ...))
 (locals (aloc ...))
 (conflicts ((aloc (aloc ...)) ...))
 any ...)
     
  s ::= ; Same as Loc-assigned-lang

If you do, you can use check-assignment in your compiler pipeline to detect bugs in the register allocator. check-assignment expects a Loc-assigned-lang v2 programs and returns a Loc-assigned-lang program, or an error if a bad assignment is detected.

Examples:
> (check-assignment
  '(begin ((locals (x.1 y.1))
           (conflicts ((x.1 (y.1)) (y.1 (x.1))))
           (assignment ((x.1 r9) (y.1 r9))))
          (set! x.1 42)
          (set! y.1 42)
          (set! x.1 (+ x.1 y.1))
          (halt x.1)))

check-assignment: Produced bad assignment:

  y.1 and x.1 both assigned to r9

> (check-assignment
   '(begin ((locals (x.1 y.1))
            (conflicts ((x.1 (y.1)) (y.1 (x.1))))
            (assignment ((x.1 r9))))
           (set! x.1 42)
           (set! y.1 42)
           (set! x.1 (+ x.1 y.1))
           (halt x.1)))

check-assignment: Some locals not assigned homes:

  homeless locals: y.1

8.4 Conflict Analysis

We have just assumed that we know which variables are in conflict, so now we need to discharge that assumption and build conflict graphs.

To figure out when two variables are in conflict, we need to know which variables might be in-use on entry to an instruction. We therefore make an assumption that the input program is decorated with this information, called the undead-in sets. The undead-in sets are a list of sets of variables (undead ((aloc ...) ...)), with each set associated with an instruction in the program. Each undead set represents, on entry to the associated instruction, which variables are undeadvariable that might not technically be alive, but aren’t conclusively proven to be dead. The first element of the list represents the set of variables that are undead when the first instruction begins executing, the second element represents the set of undead variables when the second instruction begins executing, and so on. Any variable that is referenced during an instruction is in conflict with every variable in the undead-in set associated with the instruction.

We design Undead-loc-lang to contain the undead-in sets for each instruction in a program. The language is essentially the same as Conflict-lang except for the info field.

  p ::= (begin info s ...)
     
  info ::= 
((locals (aloc ...))
 (undead ((aloc ...) ...))
 any ...)
     
  s ::= (set! aloc int64)
  | (set! aloc aloc)
  | (set! aloc (binop aloc aloc))
  | (halt aloc)
     
  binop ::= *
  | +

Exercise 3: Design and implement the function conflict-analysis, which decorates a program with its conflict graph. The source language is Undead-loc-lang and the target language is Conflict-lang program.

For working with sets, you may want to use Sets.

Examples:
> (conflict-analysis
   '(begin ((locals (x.1))
            (undead ((x.1) ())))
      (set! x.1 42)
      (halt x.1)))

'(begin ((locals (x.1)) (conflicts ((x.1 ())))) (set! x.1 42) (halt x.1))

> (conflict-analysis
   '(begin ((locals (a.1 b.2 c.3 d.4 e.5))
            (undead ((a.1) (c.3 a.1) (c.3 a.1) (b.2 c.3) (d.4) ())))
           (set! a.1 1)
      (set! c.3 2)
      (set! b.2 a.1)
      (set! b.2 (+ a.1 c.3))
      (set! d.4 (* b.2 c.3))
      (halt d.4)))

'(begin

   ((locals (a.1 b.2 c.3 d.4 e.5))

    (conflicts

     ((e.5 ()) (d.4 ()) (c.3 (a.1 b.2)) (b.2 (a.1 c.3)) (a.1 (c.3 b.2)))))

   (set! a.1 1)

   (set! c.3 2)

   (set! b.2 a.1)

   (set! b.2 (+ a.1 c.3))

   (set! d.4 (* b.2 c.3))

   (halt d.4))

> (conflict-analysis
   '(begin ((locals (v.1 w.2 x.3 y.4 z.5 t.6 p.1))
            (undead
             ((v.1)
              (v.1 w.2)
              (w.2 x.3)
              (p.1 w.2 x.3)
              (w.2 x.3)
              (y.4 w.2 x.3)
              (p.1 y.4 w.2 x.3)
              (y.4 w.2 x.3)
              (z.5 y.4 w.2)
              (z.5 y.4)
              (t.6 z.5)
              (t.6 z.5 p.1)
              (t.6 z.5)
              (z.5)
              ())))
           (set! v.1 1)
           (set! w.2 46)
           (set! x.3 v.1)
           (set! p.1 7)
           (set! x.3 (+ x.3 p.1))
           (set! y.4 x.3)
           (set! p.1 4)
           (set! y.4 (+ y.4 p.1))
           (set! z.5 x.3)
           (set! z.5 (+ z.5 w.2))
           (set! t.6 y.4)
           (set! p.1 -1)
           (set! t.6 (* t.6 p.1))
           (set! z.5 (+ z.5 t.6))
           (halt z.5)))

'(begin

   ((locals (v.1 w.2 x.3 y.4 z.5 t.6 p.1))

    (conflicts

     ((p.1 (t.6 z.5 y.4 w.2 x.3))

      (t.6 (z.5 p.1))

      (z.5 (t.6 p.1 y.4 w.2))

      (y.4 (z.5 p.1 w.2 x.3))

      (x.3 (p.1 y.4 w.2))

      (w.2 (z.5 p.1 y.4 v.1 x.3))

      (v.1 (w.2)))))

   (set! v.1 1)

   (set! w.2 46)

   (set! x.3 v.1)

   (set! p.1 7)

   (set! x.3 (+ x.3 p.1))

   (set! y.4 x.3)

   (set! p.1 4)

   (set! y.4 (+ y.4 p.1))

   (set! z.5 x.3)

   (set! z.5 (+ z.5 w.2))

   (set! t.6 y.4)

   (set! p.1 -1)

   (set! t.6 (* t.6 p.1))

   (set! z.5 (+ z.5 t.6))

   (halt z.5))

8.5 Undead Analysis

Again we have made an assumption so again we must discharge it. We must analysis a program to determine which variables are undead. The source language is the same Loc-locals-lang from last week.

Any (assignment to a) variable that definitely will be used is considered live, and (assignment to a) variable that definitely won’t be used is considered dead. Due to Rice’s theorem, a huge buzz kill, we know it’s generally impossible to tell whether a variable is dead or alive. This means that when writing an analysis, we must assume partial knowledge. We ignore liveness entirely. Instead, we assume that any variable that gets used, or might get used, might not be dead, that is, we assume it is undead, and consider a variable dead only when we have conclusive proof—like witnessing an instruction driving a new value through its heart.

Most compilers and textbooks call our undead-in sets "live-in sets" or "live-before sets". This suggests that variables are definitely alive, and that the analysis is computing sets of variables that are definitely alive, neither of which is true. Undead is not exactly the same as not-definitely-dead, except in horror movies and video games, but it’s more suggestive of reality for compilers.

To determine whether a variable is in the undead-in set, we analyze the program by looping over the instruction sequence backwards, starting with the last statement. The main part of the analysis takes an instruction and its undead-out set, the set of variables undead after that instruction executes. To start the process, we need a default undead-out set for the last instruction; the default undead-out set for Loc-locals-lang is empty. We analyze each instruction with its undead-out set and compute an undead-in set for the current instruction, which is the same as the undead-out set of the preceding instruction in the instruction sequence. That is, the undead-in set for an instruction s_i is the undead-out set for the instruction s_{i-1} in the program. We use the output, the undead-in set, for the info field, and use it as the input for analyzing the previous instruction in the sequence in the next iteration of the loop.

The analysis on a particular instruction knows which variables are undead after that instruction, and computes which are undead before executing the instruction. If a variable is used in the instruction, it ought to be live (but we don’t actually know) and is added to the undead-in setlive is a subset of undead. If a variable is overwritten in the instruction, it is dead at that point and removed from the undead-in set.

Loc-locals-lang is a simple enough language that we can tell whether a variable is dead or alive. Later, when we add new instructions, we will modify the undead analysis and find variables that aren’t necessarily live or dead, and must be assumed to be undead.

Exercise 4: Design and implement the function undead-analysis, which decorates a program with its list of undead-in sets; only the info field of the program is modified. The source language is Loc-locals-lang and the target language is Undead-loc-lang.

Examples:
> (undead-analysis
   '(begin ((locals (x.1)))
           (set! x.1 42)
           (halt x.1)))

'(begin ((locals (x.1)) (undead (() (x.1)))) (set! x.1 42) (halt x.1))

> (undead-analysis
   '(begin ((locals (a.1 b.2 c.3 d.4 e.5)))
      (set! a.1 1)
      (set! c.3 2)
      (set! b.2 a.1)
      (set! b.2 (+ a.1 c.3))
      (set! d.4 (* b.2 c.3))
      (halt d.4)))

'(begin

   ((locals (a.1 b.2 c.3 d.4 e.5))

    (undead (() (a.1) (a.1 c.3) (a.1 c.3) (c.3 b.2) (d.4))))

   (set! a.1 1)

   (set! c.3 2)

   (set! b.2 a.1)

   (set! b.2 (+ a.1 c.3))

   (set! d.4 (* b.2 c.3))

   (halt d.4))

> (undead-analysis
   '(begin ((locals (v.1 w.2 x.3 y.4 z.5 t.6 p.1)))
       (set! v.1 1)
       (set! w.2 46)
       (set! x.3 v.1)
       (set! p.1 7)
       (set! x.3 (+ x.3 p.1))
       (set! y.4 x.3)
       (set! p.1 4)
       (set! y.4 (+ y.4 p.1))
       (set! z.5 x.3)
       (set! z.5 (+ z.5 w.2))
       (set! t.6 y.4)
       (set! p.1 -1)
       (set! t.6 (* t.6 p.1))
       (set! z.5 (+ z.5 t.6))
       (halt z.5)))

'(begin

   ((locals (v.1 w.2 x.3 y.4 z.5 t.6 p.1))

    (undead

     (()

      (v.1)

      (v.1 w.2)

      (x.3 w.2)

      (p.1 x.3 w.2)

      (x.3 w.2)

      (y.4 x.3 w.2)

      (p.1 y.4 x.3 w.2)

      (x.3 w.2 y.4)

      (w.2 z.5 y.4)

      (y.4 z.5)

      (t.6 z.5)

      (p.1 t.6 z.5)

      (t.6 z.5)

      (z.5))))

   (set! v.1 1)

   (set! w.2 46)

   (set! x.3 v.1)

   (set! p.1 7)

   (set! x.3 (+ x.3 p.1))

   (set! y.4 x.3)

   (set! p.1 4)

   (set! y.4 (+ y.4 p.1))

   (set! z.5 x.3)

   (set! z.5 (+ z.5 w.2))

   (set! t.6 y.4)

   (set! p.1 -1)

   (set! t.6 (* t.6 p.1))

   (set! z.5 (+ z.5 t.6))

   (halt z.5))

An important corner case to consider is what happens when unused variables appear in a program.

Examples:
> (undead-analysis
   '(begin ((locals (x.1 y.1)))
      (set! y.1 42)
      (set! x.1 5)
      (halt x.1)))

'(begin

   ((locals (x.1 y.1)) (undead (() () (x.1))))

   (set! y.1 42)

   (set! x.1 5)

   (halt x.1))

> (undead-analysis
   '(begin ((locals (x.1 y.1)))
      (set! x.1 5)
      (set! y.1 42)
      (halt x.1)))

'(begin

   ((locals (x.1 y.1)) (undead (() (x.1) (x.1))))

   (set! x.1 5)

   (set! y.1 42)

   (halt x.1))

You can assume that unused variable do not appear. In a realistic compiler, they should be removed be an optimization.

Challenge 1: Design and implement the function bury-dead, which removes assignments to unused abstract locations. The source language is Loc-locals-lang and the target language is Loc-locals-lang. Optimizations often have the same source and target language.

8.6 Optimizing assign-homes

We can now assign registers for all Loc-locals-lang programs from last week. By assigning variables to registers, patch-instructions will be able to produce much faster code because it will not need to move values to and from memory on every single instruction.

Exercise 5: Define assign-homes-opt, which can be used as a drop-in replacement for assign-homes. The interface is the same as assign-home: the source language is Loc-locals-lang and the target language is Loc-assigned-lang.

Run your test suite and ensure you get the same outputs whether you use assign-homes or assign-homes-opt.

Examples:
> (define p
    '(begin ((locals (v.1 w.2 x.3 y.4 z.5 t.6 p.1)))
       (set! v.1 1)
       (set! w.2 46)
       (set! x.3 v.1)
       (set! p.1 7)
       (set! x.3 (+ x.3 p.1))
       (set! y.4 x.3)
       (set! p.1 4)
       (set! y.4 (+ y.4 p.1))
       (set! z.5 x.3)
       (set! z.5 (+ z.5 w.2))
       (set! t.6 y.4)
       (set! p.1 -1)
       (set! t.6 (* t.6 p.1))
       (set! z.5 (+ z.5 t.6))
       (halt z.5)))
> (assign-homes-opt p)

'(begin

   ((assignment

     ((v.1 r15) (w.2 r11) (x.3 r14) (y.4 r12) (z.5 r13) (t.6 r14) (p.1 r15))))

   (set! v.1 1)

   (set! w.2 46)

   (set! x.3 v.1)

   (set! p.1 7)

   (set! x.3 (+ x.3 p.1))

   (set! y.4 x.3)

   (set! p.1 4)

   (set! y.4 (+ y.4 p.1))

   (set! z.5 x.3)

   (set! z.5 (+ z.5 w.2))

   (set! t.6 y.4)

   (set! p.1 -1)

   (set! t.6 (* t.6 p.1))

   (set! z.5 (+ z.5 t.6))

   (halt z.5))

> (assign-homes p)

'(begin

   ((locals (v.1 w.2 x.3 y.4 z.5 t.6 p.1))

    (assignment

     ((v.1 (rbp - 8))

      (w.2 (rbp - 16))

      (x.3 (rbp - 24))

      (y.4 (rbp - 32))

      (z.5 (rbp - 40))

      (t.6 (rbp - 48))

      (p.1 (rbp - 56)))))

   (set! v.1 1)

   (set! w.2 46)

   (set! x.3 v.1)

   (set! p.1 7)

   (set! x.3 (+ x.3 p.1))

   (set! y.4 x.3)

   (set! p.1 4)

   (set! y.4 (+ y.4 p.1))

   (set! z.5 x.3)

   (set! z.5 (+ z.5 w.2))

   (set! t.6 y.4)

   (set! p.1 -1)

   (set! t.6 (* t.6 p.1))

   (set! z.5 (+ z.5 t.6))

   (halt z.5))

> (define (loc-assigned-execute f)
    (lambda (p)
      (parameterize ([current-pass-list
                      (list
                       f
                       replace-locations
                       patch-instructions
                       check-paren-x64
                       generate-x64
                       wrap-x64-boilerplate
                       wrap-x64-run-time)])
        (execute p))))
> (define loc-lang-execute-opt (loc-assigned-execute assign-homes-opt))
> (define loc-lang-execute (loc-assigned-execute assign-homes))
> (loc-lang-execute-opt p)

42

> (loc-lang-execute p)

42

Exercise 6: Create two versions of your compiler by defining two functions, compiler-a2 and compiler-a3. The source language should be Values-lang and the target should be x64-linux, represented as a string. compiler-a2 should use assign-homes, while compiler-a3 should use assign-home-opt instead. You should use parameterize and current-pass-list.

Write a paragraph comparing the two compilers. Consider the following questions:
  • What are benefits and the disadvantages of each?

  • Is one "optimal", and if so, in what sense?

  • Try running the same programs compiled with each compiler. Are there any surprises?