HomeMAL' // MANUFACTORUM
datacrypt
Piton / src / compiler.rkt
src/compiler.rkt14.6 KB
#lang racket/base

(require racket/list
         racket/match
         (prefix-in ast- "ast.rkt")
         (prefix-in std- "compiled-builtin.rkt")
         "mips.rkt"
         "call-simplifier.rkt"
         "mips-helper.rkt")
(provide compile)
(require racket/pretty)

;; <return> mips instructions
;; arguments are passed on stack rather than through $a0-$a3
;; $v0 is used for return values
;; $t9 registrer is reserved for tests and quick one-shot operations (mem-to-mem moves, etc)
;; $t8 registrer is reserved for outer scope variable fetching
;; $s registers are never used
(define (compile ast)
  ; prepare built-in funcs
  (define-values (builtin-funcs-instrs builtin-symtable) (assemble-builtin-funcs))
  ; prepare main function
  (define main-symtable (build-symtable ast 1))
  (let ([main-symtable (hash-set main-symtable "main" (cons 1 "main"))])
    ; local and built-in scopes (no global, but main is sort of global)
    (define symtables (list main-symtable builtin-symtable))
    ; compile function definitions in main
    (define func-instrs (compile-funcdefs ast symtables))
    (define frame-size (symtable-frame-size main-symtable))
    (define instrs
      (append builtin-funcs-instrs
              func-instrs
              (list (label "main"))
              (compile-stmts ast symtables frame-size)
              (list (jr $ra))))

    ; separe data and text instructions
    (define-values (data-instrs text-instrs) (partition data-instr? instrs))
    (append (list (data-section))
            data-instrs
            (list (text-section)
                  (globl "main"))
            text-instrs)))

;; <return> instructions, symbols table
(define (assemble-builtin-funcs)
  (for/fold ([instrs (list)]
             [symtable (hash)])
            ([name (hash-keys std-lib)]
             [func (hash-values std-lib)])
    ; generate random id to avoid name clashes
    (define mips-id (string-append "func_" (symbol->string (gensym))))
    (values (append instrs
                    (list (comment (string-append "func " name))
                          (label mips-id))
                    (func))
            ; level 0, outmost level (built-in scope)
            (hash-set symtable name (cons 0 mips-id)))))

;; <return> size of frame occupied by symbols of symtable (number of local vars)
(define (symtable-frame-size symtable)
  (+ 1 (count (lambda (sym) (number? (cdr sym))) (hash-values symtable))))

;; <return> instructions, location (label or mem address)
(define (resolve-id id symtables [level-offset 0])
  (when (empty? symtables) ; for debug
    (raise-user-error (format "Symbol not found '~a'" id)))
  (let ([symtable (first symtables)])
    (if (hash-has-key? symtable id)
        (match (cdr (hash-ref symtable id))
          ; label: always accessible
          [mips-id
           #:when (string? mips-id)
           (label mips-id)]
          ; mem address relative to sp: use access link
          [stack-offset
           #:when (number? stack-offset)
           (if (equal? 0 level-offset)
               (mem stack-offset $sp)
               (outer-mem level-offset stack-offset))])
        (resolve-id id (rest symtables) (+ 1 level-offset)))))

;; <return> symbol level (outmost = 0)
;; based on scope in which it was declared/defined
(define (level-for-symbol symtables id)
  (when (empty? symtables) ; for debug
    (raise-user-error (format "Symbol not found '~a'" id)))
  (let ([symtable (first symtables)])
    (if (hash-has-key? symtable id)
        (car (hash-ref symtable id))
        (level-for-symbol (rest symtables) id))))

;; Add ids of local variable declarations and function definitions to <symtable>
;; <return> symbol table
;; a symbol table hash table with ids as key and (level . mips-id-or-offset) as values
;; <level>: depth of scope in which symbol is declared/defined
;; mips-id-or-offset: auto generated label for functions and offset to $sp for variables
(define (build-symtable stmts level [symtable (hash)])
  (for/fold ([symtable symtable])
            ([stmt stmts])
    (match stmt
      ; variable declaration or function parameter
      [(or (ast-decl id _ _) (ast-param id _))
       ; multiple declarations may occur (if/else), first one prevails
       ; (important for stack order)
       (if (hash-has-key? symtable id)
           symtable
           (let ([stack-offset (* -4 (symtable-frame-size symtable))])
             ; location is mem relative to stack
             (hash-set symtable id (cons level stack-offset))))]
      ; function definition
      [(ast-funcdef id return-type params body)
       ; multiple declarations may occur (if/else), first one prevails
       (if (hash-has-key? symtable id)
           symtable
           ; generate random id to avoid name clashes
           (let ([mips-id (string-append "func_" (symbol->string (gensym)))])
             (hash-set symtable id (cons level mips-id))))]
      ; declarations in test if block and in if/else bodies
      [(ast-if test body else-body)
       (let ([symtable (match test
                         [(ast-block body) (build-symtable body level symtable)]
                         [_ symtable])])
         (if else-body
             (let ([symtable (build-symtable body level symtable)])
               (build-symtable else-body level symtable))
             (build-symtable body level symtable)))]
      ; declarations in while test if block and in and body
      [(ast-while test body)
       (let ([symtable (match test
                         [(ast-block body) (build-symtable body level symtable)]
                         [_ symtable])])
         (build-symtable body level symtable))]
      ; declarations in block body
      [(ast-block body)
       (build-symtable body level symtable)]
      ; anything else: nothing to do
      [_ symtable])))

;; Compile local function definitions in <stmts>
;; <return> instructions
(define (compile-funcdefs stmts symtables)
  (for/fold ([instrs (list)])
            ([stmt stmts])
    (match stmt
      ; function definition
      [(ast-funcdef id return-type params body)
       (define func-instrs (compile-funcdef id return-type params body symtables))
       (append instrs func-instrs)]
      ; anything else: nothing to do
      [_ instrs])))

;; Function definition
;; <return> instructions
(define (compile-funcdef id return-type params body symtables)
  ; construct inner scope symtable
  (define level (length symtables))
  (define inner-symtable (build-symtable params level))
  (let ([inner-symtable (build-symtable body level inner-symtable)])
    (define body-symtables (cons inner-symtable symtables))
    ; compile nested function declarations first
    (define body-funcs-instrs (compile-funcdefs body body-symtables))
    ; retrieve from symbol table the auto generated label
    (define mips-id (cdr (hash-ref (first symtables) id)))
    ; 1 because activation link is already on stack
    (define frame-size (+ 1 (symtable-frame-size inner-symtable)))
    (append body-funcs-instrs
            (list (comment (string-append "func " id))
                  (label mips-id))
            (compile-stmts body body-symtables frame-size)
            (list (jr $ra)))))

;; Group of statements
;; <return> instructions
(define (compile-stmts stmts symtables frame-size [in-block? #f])
  (for/fold ([instrs (list)])
            ([stmt stmts])
    (append instrs
            (compile-stmt stmt symtables frame-size in-block?))))

;; Statement
;; <return> instructions
(define (compile-stmt stmt symtables frame-size [in-block? #f])
  (match stmt
    ; variable declaration
    [(ast-decl id type value)
     ; declaration part already handled by <build-symtables>
     ; assignation part identical to ast-assign
     (compile-assign id value symtables frame-size)]
    ; assignation
    [(ast-assign target value)
     (compile-assign target value symtables frame-size)]
    ; return
    [(ast-return value)
     (compile-return value symtables frame-size in-block?)]
    ; if
    [(ast-if test body else-body)
     (compile-if test body else-body symtables frame-size)]
    ; while
    [(ast-while test body)
     (compile-while test body symtables frame-size)]
    ; function definition: already compiled by <compile-funcdefs>
    [(ast-funcdef name return-type params body) empty]
    ; anything else is an expression, result is ignored
    [_ (compile-expr stmt #f symtables frame-size)]))

;; Assignation
;; <return> instructions
(define (compile-assign target value symtables frame-size)
  (define target-loc (resolve-id target symtables))
  (compile-expr value target-loc symtables frame-size))

;; Return
;; <return> instructions
(define (compile-return value symtables frame-size in-block?)
  ; test block: put result in $t9 and no call to jr
  (if in-block?
      (compile-expr value $t9 symtables frame-size)
      (append (compile-expr value $v0 symtables frame-size)
              (list (jr $ra)))))

;; If
;; <return> instructions
(define (compile-if test body else-body symtables frame-size)
  ; prepare labels
  (define if-id (symbol->string (gensym)))
  (define if-label (label (string-append "if_" if-id)))
  (define else-label (label (string-append "else_" if-id)))
  (define endif-label (label (string-append "endif_" if-id)))

  ; compile test first
  (define test-instrs
    ; $t9 register used for tests
    (match test
        [(ast-block body) (compile-block body $t9 symtables frame-size)]
        [_ (compile-expr test $t9 symtables frame-size)]))

  (append (list if-label) ; here for clarity only
          test-instrs
          (list (beqz $t9 else-label))
          (compile-stmts body symtables frame-size)
          (list (b endif-label)
                else-label)
          (if else-body
              (compile-stmts else-body symtables frame-size)
              (list (nop)))
          (list endif-label)))

;; While
;; <return> instructions
(define (compile-while test body symtables frame-size)
  ; prepare labels
  (define while-id (symbol->string (gensym)))
  (define while-label (label (string-append "while_" while-id)))
  (define endwhile-label (label (string-append "endwhile_" while-id)))

  ; compile test first
  (define test-instrs
    ; $t9 register used for tests
    (match test
        [(ast-block body) (compile-block body $t9 symtables frame-size)]
        [_ (compile-expr test $t9 symtables frame-size)]))

  (append (list while-label)
          test-instrs
          (list (beqz $t9 endwhile-label))
          (compile-stmts body symtables frame-size)
          (list (b while-label)
                endwhile-label)))

;; Expression
;; <return> instructions
(define (compile-expr expr dest-loc symtables frame-size)
  (match expr
    [(ast-num value)        (compile-num value dest-loc)]
    [(ast-const value)      (compile-const value dest-loc)]
    [(ast-str value)        (compile-str value dest-loc)]
    [(ast-name id)          (compile-name id dest-loc symtables frame-size)]
    [(ast-call callee args) (compile-call callee args dest-loc symtables frame-size)]))

;; Num
;; <return> instructions
(define (compile-num value dest-loc)
  (move-val dest-loc value))

;; Const
;; <return> instructions
(define (compile-const value dest-loc)
  (match value
    ['true  (move-val dest-loc 1)]
    ['false (move-val dest-loc 0)]
    ['none  (move-val dest-loc 0)]))

;; String
; <return> instructions
(define (compile-str value dest-loc)
  (define str-id (symbol->string (gensym)))
  (define str-label (label (string-append "str_" str-id)))
  (append (list (asciiz str-label value))
          (move-val dest-loc str-label)))

;; Name
;; <return> instructions
(define (compile-name id dest-loc symtables frame-size)
  (move-val dest-loc (resolve-id id symtables)))

;; Block (used only in test conditions)
;; <return> instructions
(define (compile-block body dest-loc symtables frame-size)
  (compile-stmts body symtables frame-size #t))

;; Call
;; <return> instructions
(define (compile-call callee args dest-loc symtables frame-size)
  (if (hash-has-key? std-lib-inlined callee)
      (compile-inlined-call callee args dest-loc symtables frame-size)
      (compile-real-call callee args dest-loc symtables frame-size)))

;; Inlined call
;; copy instructions from builtin function instead of jumping
;; <return> instructions
(define (compile-inlined-call callee args dest-loc symtables frame-size)
  ;; ok to put arguments in $a registers because we know there are no nested
  ;; calls so they will not be overwritten
  ;; TODO unnecessary ? directly pass arg locs?
  (define a-regs (list $a0 $a1 $a2 $a3))
  (define args-instrs
    (for/fold ([args-instrs (list)])
              ([arg args]
               [i (length args)])
      (define dest-loc (list-ref a-regs i))
      (define expr-instrs (compile-expr arg dest-loc symtables frame-size))
      (append args-instrs expr-instrs)))

  ; insert mips code
  (define func (hash-ref std-lib-inlined callee))
  (define func-instrs (func dest-loc a-regs))
  (append args-instrs
          func-instrs))

;; Real call
;; <return> instructions, value ($v0)
(define (compile-real-call callee args dest-loc symtables frame-size)
  (define caller-level (- (length symtables) 1))
  (define callee-level (level-for-symbol symtables callee))
  (define callee-loc (resolve-id callee symtables))
  ; jump to label or address in register
  (define jump-instrs
    (match callee-loc
      [(? label?)     (list (jal callee-loc))]
      [(? reg?)       (list (jalr callee-loc))]
      [(? outer-mem?) (append (move-val $t9 callee-loc)
                              (list (jalr $t9)))]))
  ;(pretty-print (compile-args args symtables frame-size))
  ; push $ra on stack before activation link and arguments
  ; thanks to call-simplifier, arguments cannot trigger calls
  ; so it is safe to push $ra before arguments instructions are exectued
  (append (compile-args args symtables frame-size) ; + 2 because of $ra and activlnk
          (list (sw $ra (mem (* frame-size -4) $sp)))
          ; push activation link on stack after $ra, before arguments
          (push-next-activlnk caller-level callee-level
                              (mem (* (+ frame-size 1) -4) $sp))
          ; execute argument expressions and push them on stack
          (list (addi $sp $sp (* (+ frame-size 1) -4)))
          jump-instrs
          (list (addi $sp $sp (* (+ frame-size 1) 4))
                (lw $ra (mem (* frame-size -4) $sp)))
          (move-val dest-loc $v0)))

;; Call arguments
;; <return> instructions, number of args
;; arguments are pushed to stack
(define (compile-args args symtables frame-size)
  (for/fold ([instrs (list)])
            ([arg args]
             [i (length args)])
    ; + 2 because arguments are added after $ra and activation link
    (define stack-offset (* (+ frame-size i 2) -4))
    (define dest-loc (mem stack-offset $sp))
    (define expr-instrs (compile-expr arg dest-loc symtables (+ frame-size i)))
    (append instrs expr-instrs)))