#lang plai/gc2/collector
;; metadata size
(define METADATA-SIZE 2)

;; where the start of the current "to space" is stored in the heap.
(define LOC:OFFSET 0)

;; where the allocation pointer is stored in the heap
(define LOC:PTR 1)

;; Start of the current "to space"
(define (off) (heap-ref LOC:OFFSET))

;; Offset into the current "to space" where free space starts.
(define (ptr) (heap-ref LOC:PTR))

;; How big are the semi-spaces?
(define (space-size)
  (quotient (- (heap-size) METADATA-SIZE) 2))

;; All functions named gc:*, along with init-allocator, must be
;; implemented by any plai/gc2 collector. For their functionality and
;; interface see [1].

(define (init-allocator)
  (heap-set! LOC:OFFSET METADATA-SIZE)
  (heap-set! LOC:PTR 0))

(module+ test
  (test (with-heap (make-vector 1000)  (+ METADATA-SIZE (space-size) (space-size))) 1000)
  (test (with-heap (make-vector 999)  (+ METADATA-SIZE (space-size) (space-size))) 998))


;; Define some syntax rules to make it easier to write tests

;; Test if the last two expressions are equal.
;; Takes a vector for a heap

(define-syntax (test/heap stx)
  (syntax-case stx ()
    [(test/heap heap oper ...  expected)
     (syntax-protect
      #`(with-heap heap
          (init-allocator)
          #,(syntax/loc stx
              (test (begin oper ...) expected))))]))

;; Test if one of the expressions before the last throws an exception
;; matching the last expression (a string).
;; Takes a vector for a heap
(define-syntax (test/heap/exn stx)
  (syntax-case stx ()
    [(test/heap heap oper ...  expected)
     (syntax-protect
      #`(with-heap heap
          (init-allocator)
          #,(syntax/loc stx
              (test/exn (begin oper ...) expected))))]))

(define (swap-spaces!)
  ;; do nothing
  (void))

(module+ test

  ;; Initially, allocations are in the left half.
  (test/heap
      (make-vector (+ 4 METADATA-SIZE) '?)
    (gc:alloc-flat #f)
    (current-heap)
    #(2 2 flat #f ? ?))

  ;; After calling swap-spaces!, allocations are in the right half
  (test/heap
      (make-vector (+ 4 METADATA-SIZE) '?)
    (swap-spaces!)
    (gc:alloc-flat #f)
    (current-heap)
    #(4 2 ? ? flat #f))

  ;; Swapping twice is back to allocating in left
  (test/heap
      (make-vector (+ 4 METADATA-SIZE) '?)
    (swap-spaces!)
    (swap-spaces!)
    (gc:alloc-flat #f)
    (current-heap)
    #(2 2 flat #f ? ?))
  )

;; malloc : size -> address
(define (malloc n)
  (when (> (+ (ptr) n) (space-size))
    (gc!))
  (when (> (+ (ptr) n) (space-size))
    (error 'malloc "out of memory!"))
  (heap-set! LOC:PTR (+ (ptr) n))
  (+ (heap-ref LOC:OFFSET) (- (ptr) n)))

(define (gc!)
  ;; do nothing
  (void))

;; gc:alloc-flat : flat-value -> address
(define (gc:alloc-flat value)
  (define addr (malloc 2))
  (heap-set! addr 'flat)
  (heap-set! (+ addr 1) value)
  addr)

;; gc:flat? : address -> boolean
(define (gc:flat? address)
  (equal? (heap-ref address) 'flat))

;; gc:deref : address -> flat-value
(define (gc:deref address)
  (unless (gc:flat? address)
    (error 'gc:deref "not a flat: ~a" address))
  (heap-ref (+ address 1)))

;; gc:cons : root root -> address
(define (gc:cons root1 root2)
  (define addr (malloc 3 root1 root2))
  (heap-set! addr 'cons)
  (heap-set! (+ addr 1) (read-root root1))
  (heap-set! (+ addr 2) (read-root root2))
  addr)

;; gc:cons? : address -> boolean
(define (gc:cons? address)
  (equal? (heap-ref address) 'cons))

;; gc:first : address -> address
(define (gc:first address)
  (unless (gc:cons? address)
    (error 'gc:first "not a pair: ~a" address))
  (heap-ref (+ address 1)))

;; gc:rest : address -> address
(define (gc:rest address)
  (unless (gc:cons? address)
    (error 'gc:rest "not a pair: ~a" address))
  (heap-ref (+ address 2)))

;; gc:set-first! : address address -> void
(define (gc:set-first! address new-value-address)
  (unless (gc:cons? address)
    (error 'gc:set-first! "not a pair: ~a" address))
  (heap-set! (+ address 1) new-value-address))

;; gc:set-rest! : address address -> void
(define (gc:set-rest! address new-value-address)
  (unless (gc:cons? address)
    (error 'gc:set-rest! "not a pair: ~a" address))
  (heap-set! (+ address 2) new-value-address))

;; gc:closure : opaque-value (listof root) -> address
(define (gc:closure code-ptr free-vars)
  (define n-vars (length free-vars))
  (define addr (malloc (+ 3 n-vars)))
  (heap-set! addr 'clos)
  (heap-set! (+ addr 1) code-ptr)
  (heap-set! (+ addr 2) n-vars)
  (for ([i  (in-range n-vars)]
        [fv (in-list free-vars)])
    (heap-set! (+ addr 3 i) (read-root fv)))
  addr)

;; gc:closure? : address -> boolean
(define (gc:closure? address)
  (equal? (heap-ref address) 'clos))

;; gc:closure-code-ptr : address -> opaque-value
(define (gc:closure-code-ptr address)
  (unless (gc:closure? address)
    (error 'gc:closure-code-ptr "not a closure: ~a" address))
  (heap-ref (+ address 1)))

;; gc:closure-env-ref : address integer -> address
(define (gc:closure-env-ref address i)
  (unless (gc:closure? address)
    (error 'gc:closure-env-ref "not a closure: ~a" address))
  (heap-ref (+ address 3 i)))

(module+ test
  ;; OOM
  (test/heap/exn (make-vector METADATA-SIZE)
    (gc:alloc-flat #f)
    "out of memory")

  ;; OOM due to using only half of the heap
  (test/heap/exn
      (make-vector (+ 2 METADATA-SIZE))
    (gc:alloc-flat #f)
    "out of memory")

  ;; dereferencing cons as flat
  (test/heap/exn (make-vector 1000)
    (let ([cons-addr
           (gc:cons
            (simple-root (gc:alloc-flat #f))
            (simple-root (gc:alloc-flat #t)))])
      (gc:deref cons-addr))
    "not a flat")

  ;; dereferencing flat as cons
  (test/heap/exn (make-vector 1000)
    (let ([flat-addr (gc:alloc-flat #f)])
      (gc:first flat-addr))
    "not a pair")

  ;; dereferencing flat as cons
  (test/heap/exn (make-vector 1000)
    (let ([flat-addr (gc:alloc-flat #f)])
      (gc:rest flat-addr))
    "not a pair")

  ;; setting flat as cons
  (test/heap/exn (make-vector 1000)
    (let ([flat-addr (gc:alloc-flat #f)])
      (gc:set-first! flat-addr #t))
    "not a pair")

  ;; setting flat as cons
  (test/heap/exn (make-vector 1000)
    (let ([flat-addr (gc:alloc-flat #f)])
      (gc:set-rest! flat-addr #t))
    "not a pair")

  ;; getting code ptr from non closure
  (test/heap/exn (make-vector 1000)
    (let ([flat-addr (gc:alloc-flat #f)])
      (gc:closure-code-ptr flat-addr))
    "not a closure")

  ;; getting code ptr from non closure
  (test/heap/exn (make-vector 1000)
    (let ([flat-addr (gc:alloc-flat #f)])
      (gc:closure-env-ref flat-addr 1))
    "not a closure")

  ;; Successful dereference: flat
  (test/heap (make-vector 1000)
    (gc:deref (gc:alloc-flat #t))
    #t)

  ;; successful dereference: cons
  (test/heap (make-vector 1000)
    (gc:deref
     (gc:rest
      (gc:cons
       (simple-root (gc:alloc-flat 'first))
       (simple-root (gc:alloc-flat 'rest)))))
    'rest)

  (test/heap (make-vector 1000)
    (gc:deref
     (gc:first
      (gc:cons
       (simple-root (gc:alloc-flat 'first))
       (simple-root (gc:alloc-flat 'rest)))))
    'first)

  ;; successful alloc / deref closure
  (test/heap (make-vector 1000)
    (gc:closure-code-ptr
     (gc:closure 'dummy '()))
    'dummy)

  (test/heap (make-vector 1000)
    (gc:deref
     (gc:closure-env-ref
      (gc:closure
       'dummy
       (list (simple-root (gc:alloc-flat #f))))
      0))
    #f)

  ;; setting cons parts
  (test/heap (make-vector 1000)
    (let ([cons-loc
           (gc:cons
            (simple-root (gc:alloc-flat 'first))
            (simple-root (gc:alloc-flat 'rest)))])
      (gc:set-first! cons-loc (gc:alloc-flat 'mutated))
      (gc:deref (gc:first cons-loc)))
    'mutated)

  (test/heap (make-vector 1000)
    (let ([cons-loc
           (gc:cons
            (simple-root (gc:alloc-flat 'first))
            (simple-root (gc:alloc-flat 'rest)))])
      (gc:set-rest! cons-loc (gc:alloc-flat 'mutated))
      (gc:deref (gc:rest cons-loc)))
    'mutated)
  )

(module+ test
  ;; heap state after initial allocation
  (test/heap
      (make-vector 12 '?)
    (gc:alloc-flat #f)
    (current-heap)
    #(2 2 flat #f ? ?  ? ? ? ? ? ?))

  (test/heap
      (make-vector 18  '?)
    (gc:cons
     (simple-root (gc:alloc-flat #f))
     (simple-root (gc:alloc-flat #t)))
    (current-heap)
    #(2 7 flat #f flat #t cons 2 4 ? ? ? ? ? ? ? ? ?))

  (test/heap
      (make-vector 18  '?)
    (gc:closure
     'dummy
     (list (simple-root (gc:alloc-flat #f))))
    (current-heap)
    #(2 6 flat #f clos dummy 1 2 ? ? ? ? ? ? ? ? ? ?))
  )

(module+ test
  ;; heap state and roots after gc

  (test/heap
      (make-vector 12 '?)
    (define f1 (gc:alloc-flat #f))
    (with-roots (f1)
      (gc!)
      (cons (current-heap) (map read-root (get-root-set))))
    (cons
     #(7 2 forwarded 7 ? ? ? flat #f ? ? ?)
     '(7)))

  (test/heap
      (make-vector 18  '?)
    (define c1
      (gc:cons
       (simple-root (gc:alloc-flat #f))
       (simple-root (gc:alloc-flat #t))))
    (with-roots (c1)
      (gc!)
      (cons (current-heap) (map read-root (get-root-set))))
    (cons
     #(10 7 forwarded 13 forwarded 15 forwarded 10 4 ? cons 13 15 flat #f flat #t ?)
     '(10)))

  (test/heap
      (make-vector 18  '?)
    (define cl1
      (gc:closure 'dummy (list (simple-root (gc:alloc-flat #f)))))
    (with-roots (cl1)
      (gc!)
      (cons (current-heap) (map read-root (get-root-set))))
    (cons
     #(10 6 forwarded 14 forwarded 10 1 2 ? ? clos dummy 1 14 flat #f ? ?)
     '(10)))

  ;; Test for coverage of forwarded tags.
  (test/heap
      (make-vector 26 '?)
    (define c1
      (gc:cons
       (simple-root (gc:alloc-flat 2))
       (simple-root (gc:alloc-flat empty))))
    (define c2
      (gc:cons
       (simple-root (gc:alloc-flat 1))
       (simple-root c1)))
    ;; force both cons cells to be moved before starting update pass
    (with-roots (c1 c2)
      (gc!)
      (current-heap))
    #(14 12
         forwarded 20 forwarded 22 forwarded 14 4 forwarded 24 forwarded 17 6
         cons 20 22 cons 24 14 flat 2 flat () flat 1)
    )
  )

(module+ test
  (test/heap
      (make-vector 12 '?)
    (define f1 (gc:alloc-flat #f))
    (gc! (simple-root f1))
    (current-heap)
    #(7 2 forwarded 7 ? ? ? flat #f ? ? ?)))
