Poole The Jekyll Butler

Flattening Trees

Introduction

I’ve been reading the final chapter of SICP which covers register machines (this is like assembly/machine code), in particular they reimplement the explicit control evaluator as a register machine and then use those ideas to create a lisp compiler. In order to create my own compiler I’m planning on following alongn with SICP but using x86 rather than the register machine simulator from the book. On the other hand x86 programming is scary and I’m not very good at it!

One of the important parts of running lisp in a register machine is “lisp structured memory”, the ability to cons, car and cdr. So for practice I’ve written a trivial compiler that takes a tree and builds a sequence of instructions that build it on the stack - then implement the following recursive function in x86:

(define (print-tree tree)
  (if (number? tree)
      (display tree)
      (begin (display "(")
             (print-tree (car tree))
             (display " . ")
             (print-tree (cdr tree))
             (display ")"))))

Here’s an example of running this:

#;> (print-tree '((#x66 . #x77) . (#x8989 . #x4141)))
((102 . 119) . (35209 . 16705))

To flatten a tree of cons cells onto the stack we will need a representation that uses pointers. Number are represented as a single tagged qword (64 bits), and conses are a two qwords (the car and cdr pointers), the car is tagged. Tagging is needed to tell what type a piece of data on the stack is. We reserve the four lowest bits of a qword to use for the tag. 0000 for a cons and 0001 for a number.

To tag a number in assembly you need to bitshift it left 4 bits then or it with 0b0001, for example if our number was 1945

mov rax,1945
shl rax,4
or  rax,0b001

to test the tag you bitmask it using and, and to get the data out once you looked at the tag you can use shr.

Here’s an example of allocating the following structure on the stack: (#x66 . #x77)

address         value
0x7fffffffe618: 0x0007fffffffe6300
0x7fffffffe620: 0x00007fffffffe628
0x7fffffffe628: 0x0000000000000771
0x7fffffffe630: 0x0000000000000661

the two numbers (tagged with a 1 nybble) are at the bottom of the stack because they were allocated first (note that the stack grows by subtracting the stack pointer so the address are lower the higher up on the stack you are). At the top there is a 0 tagged pointer, and then an untagged pointer - these two qwords represent a cons cell.

Here is the source for the little compiler that takes a tree and allocates it onto the stack:

(define base-pointer
  ;; this counter is used to keep track of where we
  ;; are, relative to the base-pointer (that points
  ;; to the bottom of the stack frame)
  (let ((rsp 0))
    (lambda ()
      (set! rsp (+ rsp 8))
      rsp)))

(define (push-number n)
  ;; to allocate a number just tag it and push it onto the stack
  ;; return its location relative to the base pointer
  (emit `(mov rax ,n))
  (emit `(shl rax 4))
  (emit `(or rax 0b0001)) ; number tag is 0b0001
  (emit `(push rax))
  (base-pointer))

(define (push-cons kar kdr)
  ;; to allocate a cons cell, gives the addresses of its car and cdr
  ;; we push the cdr, then push the tagged cdr - then increment the
  ;; stack pointer and return that value
  (emit `(mov rax rbp))
  (emit `(sub rax ,kdr))
  (emit `(push rax))
  (emit `(mov rax rbp))
  (emit `(sub rax ,kar))
  (emit `(shl rax 4))
  ;(emit `(or rax 0b000)) ; cons tag is 0, no need to or anything
  (emit `(push rax))
  (base-pointer)
  (base-pointer))

(define (compile-tree tree)
  (if (number? tree)
      (push-number tree)
      (begin
        (let* ((kar (compile-tree (car tree)))
               (kdr (compile-tree (cdr tree))))
          (push-cons kar kdr)))))

Here’s the code it generates for the following invocation

(let ((root (compile-tree '(#x66 . #x77))))
  (emit `(mov rax rbp))
  (emit `(sub rax ,root)))

produces

    mov rax,102
    shl rax,4
    or rax,0b0001
    push rax
    mov rax,119
    shl rax,4
    or rax,0b0001
    push rax
    mov rax,rbp
    sub rax,16
    push rax
    mov rax,rbp
    sub rax,8
    shl rax,4
    push rax
    mov rax,rbp
    sub rax,32

and that creates a stack just like we saw before. And finally here is the print-tree procedure which recursively walks over the tree printing it out:

print_tree:
        mov     rbx,[rax]
        mov     rcx,rbx
        and     rbx,0xF
        test    rbx,rbx

        jnz     .number

        ;; cons

        mov     rbx,rcx
        mov     rcx,[rax+8]
        push    rcx
        push    rbx
        
        mov     rax,1
        mov     rdi,1
        mov     rsi,open_str
        mov     rdx,1
        syscall

        pop     rax
        shr     rax,4
        call    print_tree

        mov     rax,1
        mov     rdi,1
        mov     rsi,dot_str
        mov     rdx,3
        syscall

        pop     rax
        call    print_tree

        mov     rax,1
        mov     rdi,1
        mov     rsi,close_str
        mov     rdx,1
        syscall

        ret

.number:
        mov     rax,rcx
        shr     rax,4
        call    print_number

        ret

To do all this it was really useful to be able to debug my code in GDB, in particular the following commands:

break tree.asm:61 # to break at line 61
info registers    # to see what the register values are
ni                # next instruction
layout asm        # to see the assembly code in gdb
x/64xg $rbp-64    # to e'x'amine the top few 'g'iant qwords on the stack