On this page:
5.1 Conditional execution
5.2 Meaning of Con programs
5.3 An Example of Con compilation
5.4 A Compiler for Con
5.5 Correctness and random testing
7.4

5 Con: branching with conditionals

When you come to a fork in the road, take it.

    5.1 Conditional execution

    5.2 Meaning of Con programs

    5.3 An Example of Con compilation

    5.4 A Compiler for Con

    5.5 Correctness and random testing

5.1 Conditional execution

Let’s now consider add a notion of conditionals to our target language.

We’ll call it Con.

We will use the following syntax: (if (zero? e0) e1 e2).

Together this leads to the following grammar for Con:

image

Which can be modeled with the following data type definition:

con/ast.rkt

  #lang racket
  ;; type Expr =
  ;; | Integer
  ;; | `(add1 ,Expr)
  ;; | `(sub1 ,Expr)
  ;; | `(if (zero? ,Expr) ,Expr ,Expr)
   

We will also need a predicate for well-formed Con expressions, but let’s return to this after considering the semantics and interpreter.

5.2 Meaning of Con programs

The meaning of Con programs depends on the form of the expression and the new form is an if-expression.

Let’s consider some examples:

The semantics is inductively defined as before. There are two new rules added for handling if-expressions: one for when the test expression means 0 and one for when it doesn’t.

image    image    image

image    image

The interpreter has an added case for if-expressions, which recursively evaluates the test expression and branches based on its value.

con/interp.rkt

  #lang racket
  (provide (all-defined-out))
   
  ;; Expr -> Integer
  (define (interp e)
    (match e
      [(? integer? i) i]
      [`(add1 ,e0)
       (+ (interp e0) 1)]
      [`(sub1 ,e0)
       (- (interp e0) 1)]
      [`(if (zero? ,e0) ,e1 ,e2)
       (if (zero? (interp e0))
           (interp e1)
           (interp e2))]))
   

We can confirm the interpreter computes the right result for the examples given earlier:

Examples

> (interp '(if (zero? 0) (add1 2) 4))

3

> (interp '(if (zero? 1) (add1 2) 4))

4

> (interp '(if (zero? (if (zero? (sub1 1)) 1 0)) (add1 2) 4))

4

> (interp '(if (zero? (add1 0)) (add1 2) (if (zero? (sub1 1)) 1 0)))

1

The argument for the correctness of the interpreter follows the same structure as for Blackmail, but with an added case for if-expressions.

5.3 An Example of Con compilation

Suppose we want to compile '(if (zero? 8) 2 3)...

We already know how to compile the 8, 2, and 3 part.

What needs to happen?

We can determine whether 8 evaluates to 0 using a comparison instruction: '(cmp rax 0). To do the conditional execution, we will need to jump to different parts of the code to either execute the code for 2 or 3. There are several ways we could accomplish this, but we take the following approach: immediately after the comparison, do a conditional jump to the code for the else branch when non-zero. Should the jump not occur, the next instructions will carry out the evaluation of the then branch, then (unconditionally) jump over the else branch code.

To accomplish this, we will need two new labels: one for the else branch code and one for the end of the else branch code. The gensym function can be used to generate symbols that have not appeared before.

In total, the code for this example would look like:

'((mov rax 8)
  (cmp rax 0)
  (jne if-else-begin)
  (mov rax 2)
  (jmp if-else-end)
  if-else-begin
  (mov rax 3)
  if-else-end)
5.4 A Compiler for Con

Notice that the '(mov rax 8), '(mov rax 3) and '(mov rax 2) parts are just the instructions generated by compiling 8, 2 and 3. Generalizing from this, we arrive at the following code for the compiler:

(let ((c0 (compile-e e0))
      (c1 (compile-e e1))
      (c2 (compile-e e2))
      (l0 (gensym "if"))
      (l1 (gensym "if")))
  `(,@c0
    (cmp rax 0)
    (jne ,l0)
    ,@c1
    (jmp ,l1)
    ,l0
    ,@c2
    ,l1))

This will require extending our representation of x86 instructions; in particular, we add 'jmp, 'jne, and 'cmp instructions:

con/asm/ast.rkt

  #lang racket
   
  ;; type Asm = [Listof Instruction]
   
  ;; type Instruction =
  ;; | `ret
  ;; | `(mov ,Arg ,Arg)
  ;; | `(add ,Arg ,Arg)
  ;; | `(sub ,Arg ,Arg)
  ;; | `(cmp ,Arg ,Arg)
  ;; | `(jmp ,Label)
  ;; | `(je  ,Label)
  ;; | `(jne ,Label)
  ;; | Label
   
  ;; type Label = Symbol
   
  ;; type Arg =
  ;; | Reg
  ;; | Integer
   
  ;; type Reg =
  ;; | `rax
   

We omit the printer code, which is mundane. See asm/printer.rkt for details.

The complete compiler code is:

con/compile.rkt

  #lang racket
  (provide (all-defined-out))
   
  ;; Expr -> Asm
  (define (compile e)
    `(entry
      ,@(compile-e e)
      ret))
   
  ;; Expr -> Asm
  (define (compile-e e)
    (match e
      [(? integer? i) `((mov rax ,i))]
      [`(add1 ,e0)
       (let ((c0 (compile-e e0)))
         `(,@c0
           (add rax 1)))]    
      [`(sub1 ,e0)
       (let ((c0 (compile-e e0)))
         `(,@c0
           (sub rax 1)))]
      [`(if (zero? ,e0) ,e1 ,e2)
       (let ((c0 (compile-e e0))
             (c1 (compile-e e1))
             (c2 (compile-e e2))
             (l0 (gensym "if"))
             (l1 (gensym "if")))
         `(,@c0
           (cmp rax 0)
           (jne ,l0)
           ,@c1
           (jmp ,l1)
           ,l0
           ,@c2
           ,l1))]))
   

Let’s take a look at a few examples:

Examples

> (asm-display (compile '(if (zero? 8) 2 3)))

global entry

section .text

entry:

mov rax, 8

cmp rax, 0

jne if1541

mov rax, 2

jmp if1542

if1541:

mov rax, 3

if1542:

ret

> (asm-display (compile '(if (zero? 0) 1 2)))

global entry

section .text

entry:

mov rax, 0

cmp rax, 0

jne if1543

mov rax, 1

jmp if1544

if1543:

mov rax, 2

if1544:

ret

> (asm-display (compile '(if (zero? 0) (if (zero? 0) 8 9) 2)))

global entry

section .text

entry:

mov rax, 0

cmp rax, 0

jne if1547

mov rax, 0

cmp rax, 0

jne if1545

mov rax, 8

jmp if1546

if1545:

mov rax, 9

if1546:

jmp if1548

if1547:

mov rax, 2

if1548:

ret

> (asm-display (compile '(if (zero? (if (zero? 2) 1 0)) 4 5)))

global entry

section .text

entry:

mov rax, 2

cmp rax, 0

jne if1549

mov rax, 1

jmp if1550

if1549:

mov rax, 0

if1550:

cmp rax, 0

jne if1551

mov rax, 4

jmp if1552

if1551:

mov rax, 5

if1552:

ret

And confirm they are running as expected:

Examples

> (asm-interp (compile '(if (zero? 8) 2 3)))

3

> (asm-interp (compile '(if (zero? 0) 1 2)))

1

> (asm-interp (compile '(if (zero? 0) (if (zero? 0) 8 9) 2)))

8

> (asm-interp (compile '(if (zero? (if (zero? 2) 1 0)) 4 5)))

4

5.5 Correctness and random testing

The statement of correctness follows the same outline as before:

Compiler Correctness: For all expressions e and integers i, if (e,i) in image, then (asm-interp (compile e)) equals i.

Again, we formulate correctness as a property that can be tested:

Examples

> (define (check-compiler e)
    (check-equal? (asm-interp (compile e))
                  (interp e)
                  e))

Generating random Con programs is essentially the same as Blackmail programs, and are provided in a random.rkt module.

Examples

> (require "random.rkt")
> (random-expr)

'(sub1 (add1 1))

> (random-expr)

'(add1 (if (zero? (add1 (sub1 6))) (if (zero? (sub1 1)) (sub1 3) -1) (sub1 (add1 -3))))

> (random-expr)

'(add1 (sub1 (if (zero? (sub1 -3)) (if (zero? -8) -1 3) (add1 1))))

> (random-expr)

'(add1 (sub1 4))

> (for ([i (in-range 10)])
    (check-compiler (random-expr)))