#lang racket/base

;; Poor man's stack-trace-on-exceptions/profiler.
;; See manual for information.

(require "stacktrace.rkt"
         "errortrace-key.rkt"
         "private/utils.rkt"
         racket/contract/base
         racket/unit
         (for-template racket/base "errortrace-key.rkt")
         (for-syntax racket/base))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Test coverage run-time support
(define test-coverage-enabled (make-parameter #f))

(define test-coverage-state '())
(define (initialize-test-coverage) (set! test-coverage-state '()))

(define (initialize-test-coverage-point expr)
  (when (and (syntax-position expr)
             (syntax-span expr))
    (set! test-coverage-state (cons (list (syntax-source expr)
                                          (syntax-position expr)
                                          (syntax-span expr))
                                    test-coverage-state))))

;; get-coverage : -> (values (listof (list src number number)) (listof (list src number number)))
;; the first result is a (minimized) set of ranges for all of the code that could be executed
;; the second result is the set of ranges that were actually executed.
(define (get-coverage) 
  (let* ([hash (test-coverage-info)]
         [all (hash-ref hash 'base '())]
         [covered '()])
    (hash-for-each hash (lambda (x y) (unless (eq? x 'base) (set! covered (cons x covered)))))
    (values all covered)))

(define code-insp (variable-reference->module-declaration-inspector
                   (#%variable-reference)))
(define (disarm stx)
  (syntax-disarm stx code-insp))

(define (transform-all-modules stx proc [in-mod-id (namespace-module-identifier)])
  (syntax-case stx ()
    [(mod name init-import mb)
     (syntax-case (disarm #'mb) (#%plain-module-begin)
       [(#%plain-module-begin body ...)
        (let ()
          (define ((handle-top-form phase) expr)
            (define disarmed-expr (disarm expr))
            (syntax-case* disarmed-expr (begin-for-syntax module module*)
                          (lambda (a b)
                            (free-identifier=? a b phase 0))
              [(begin-for-syntax body ...)
               (syntax-rearm
                #`(#,(car (syntax-e disarmed-expr))
                    #,@(map (handle-top-form (add1 phase)) 
                            (syntax->list #'(body ...))))
                expr)]
              [(module . _)
               (syntax-rearm
                (transform-all-modules disarmed-expr proc #f)
                expr)]
              [(module* name init-import . _)
               (let ([shift (if (syntax-e #'init-import)
                                0
                                phase)])
                 (syntax-rearm
                  (syntax-shift-phase-level
                   (transform-all-modules (syntax-shift-phase-level disarmed-expr (- shift)) proc #f)
                   shift)
                  expr))]
              [else expr]))
          (define mod-id (or in-mod-id #'mod))
          (proc
           (copy-props
            stx
            #`(#,mod-id name init-import
                        #,(syntax-rearm
                           #`(#%plain-module-begin
                              . #,(map (handle-top-form 0) (syntax->list #'(body ...))))
                           #'mb)))
           mod-id))])]))
    
(define (add-test-coverage-init-code stx)
  (transform-all-modules
   stx
   (lambda (stx mod-id)
     (syntax-case stx ()
       [(mod name init-import mb)
        (syntax-case (disarm #'mb) (#%plain-module-begin)
          [(#%plain-module-begin b1 body ...)
           (copy-props
            stx
            #`(#,mod-id name init-import
               #,(syntax-rearm
                  #`(#%plain-module-begin
                     b1 ;; the requires that were introduced earlier
                     (#%plain-app init-test-coverage '#,(remove-duplicates test-coverage-state))
                     body ...)
                  #'mb)))])]))))

(define (annotate-covered-file filename-path [display-string #f])
  (annotate-file filename-path
                 (map (λ (c) (cons (car c) (if (cdr c) 1 0))) (get-coverage))
                 display-string))


;; The next procedure is called by `annotate' and `annotate-top' to wrap
;; expressions with test suite coverage information.  Returning the
;; first argument means no tests coverage information is collected.

;; test-coverage-point : syntax syntax integer -> (values syntax info)
;; sets a test coverage point for a single expression
(define (test-coverage-point body expr phase)
  (if (and (test-coverage-enabled) (zero? phase))
      (syntax-case expr ()
        [(mod name . reste)
         (and (identifier? #'mod)
              (free-identifier=? #'mod 
                                 (namespace-module-identifier)
                                 (namespace-base-phase)))
         ;; don't annotate module expressions
         body]
        [_
         (cond
           [(and (syntax-source expr)
                 (number? (syntax-position expr))
                 (number? (syntax-position expr)))
            (initialize-test-coverage-point expr)
            (with-syntax ([src (datum->syntax #f (syntax-source expr) (quote-syntax here))]
                          [start-pos (syntax-position expr)]
                          [end-pos (+ (syntax-position expr) (syntax-span expr))]
                          [body body])
              #'(begin (#%plain-app test-covered '(src start-pos end-pos)) body))]
           [else
            body])])
      body))

;; remove-duplicates : (listof X) -> (listof X)
(define (remove-duplicates l)
  (let ([ht (make-hash)])
    (for-each (lambda (x) (hash-set! ht x #t)) l)
    (sort (hash-map ht (lambda (x y) x))
          (lambda (x y)
            (cond
              [(= (list-ref x 1) (list-ref y 1))
               (< (list-ref x 2) (list-ref y 2))]
              [else
               (< (list-ref x 1) (list-ref y 1))])))))

(define (copy-props orig new)
  (datum->syntax orig (syntax-e new) orig orig))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Profiling run-time support

(define profile-thread-cell (make-thread-cell #f))
(define profile-key (gensym))

(define thread->profile-table (make-weak-hasheq))

(define profiling-enabled (make-parameter #f))
(define profiling-record-enabled (make-parameter #t))
(define profile-paths-enabled (make-parameter #f))

(define (clear-profile-results)
  (when (thread-cell-ref profile-thread-cell)
    (hash-for-each
     (thread-cell-ref profile-thread-cell)
     (lambda (k v)
       (set-box! (vector-ref v 0) #f)
       (vector-set! v 1 0)
       (vector-set! v 2 0)
       (vector-set! v 4 null)))))

(define (initialize-profile-point key name expr)
  (unless (thread-cell-ref profile-thread-cell)
    (let ([new-table (make-hasheq)])
      (hash-set! thread->profile-table (current-thread) new-table)
      (thread-cell-set! profile-thread-cell new-table)))
  (hash-set! (thread-cell-ref profile-thread-cell)
             key
             (vector (box #f) 0 0 (and name (syntax-e name)) expr null)))

(define (register-profile-start key)
  (and (profiling-record-enabled)
       (thread-cell-ref profile-thread-cell)
       (let ([v (hash-ref (thread-cell-ref profile-thread-cell) key #f)])
         (when v
           (let ([b (vector-ref v 0)])
             (vector-set! v 1 (add1 (vector-ref v 1)))
             (when (profile-paths-enabled)
               (let ([cms
                      (continuation-mark-set->list
                       (current-continuation-marks)
                       profile-key)])
                 (unless (hash? (vector-ref v 5))
                   (vector-set! v 5 (make-hash)))
                 (hash-set! (vector-ref v 5) cms 
                            (add1 (hash-ref (vector-ref v 5) cms (lambda () 0))))))
             (if (unbox b)
                 #f
                 (begin
                   (set-box! b #t)
                   (current-process-milliseconds))))))))

(define (register-profile-done key start)
  (when start
    (when (thread-cell-ref profile-thread-cell)
      (let ([v (hash-ref (thread-cell-ref profile-thread-cell) key #f)])
        (when v
          (let ([b (vector-ref v 0)])
            (set-box! b #f)
            (vector-set! v 2
                         (+ (- (current-process-milliseconds) start)
                            (vector-ref v 2)))))))))

(define (get-profile-results [t (current-thread)])
  (cond
    [(hash-ref thread->profile-table t #f)
     =>
     (λ (profile-info)
       (hash-map profile-info
                 (lambda (key val)
                   (let ([count (vector-ref val 1)]
                         [time (vector-ref val 2)]
                         [name (vector-ref val 3)]
                         [expr (vector-ref val 4)]
                         [cmss (vector-ref val 5)])
                     (list count time name expr
                           (if (hash? cmss)
                               (hash-map cmss (lambda (ks v)
                                                (cons v 
                                                      (map (lambda (k)
                                                             (let ([v (cdr (hash-ref profile-info k))])
                                                               (list (vector-ref v 2)
                                                                     (vector-ref v 3))))
                                                           ks))))
                               null))))))]
    [else '()]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Stacktrace instrumenter

(define base-phase
  (variable-reference->module-base-phase (#%variable-reference)))

;; with-mark : stx stx -> stx
(define (with-mark mark expr phase)
  (let ([loc (make-st-mark mark phase)])
    (if loc
        (with-syntax ([expr expr]
                      [loc loc]
                      [et-key (syntax-shift-phase-level #'errortrace-key (- phase base-phase))]
                      [wcm (syntax-shift-phase-level #'with-continuation-mark (- phase base-phase))])
          (execute-point
           mark
           (syntax
            (wcm et-key
                 loc
                 expr))))
        expr)))

(define (should-annotate? s phase)
  (and (syntax-source s)
       (syntax-property s 'errortrace:annotate)))

;; Add the 'errortrace:annotate property every in an original syntax
;; object, so that we can recognize pieces from the original program
;; after expansion:
(define (add-annotate-property s)
  (cond
   [(syntax? s)
    (define new-s (syntax-rearm 
                   (let ([s (disarm s)])
                     (datum->syntax s
                                    (add-annotate-property (syntax-e s))
                                    s
                                    s))
                   s))
    ;; We could check for "original" syntax here, because partial expansion
    ;; of top-level forms happens before the compile handler gets
    ;; control. As a compromise for programs that may use errortrace
    ;; with `eval` and S-expressions or non-`syntax-original?` arrangements, we add
    ;; the property everywhere:
    (if #t ; (syntax-original? s)
        (syntax-property new-s
                         'errortrace:annotate #t
                         ;; preserve the property in bytecode:
                         #t)
        new-s)]
   [(pair? s)
    (cons (add-annotate-property (car s))
          (add-annotate-property (cdr s)))]
   [(vector? s)
    (for/vector #:length (vector-length s) ([e (in-vector s)])
                (add-annotate-property e))]
   [(box? s) (box (add-annotate-property (unbox s)))]
   [(prefab-struct-key s)
    => (lambda (k)
         (apply make-prefab-struct
                k
                (add-annotate-property (cdr (vector->list (struct->vector s))))))]
   [(and (hash? s) (immutable? s))
    (cond
     [(hash-eq? s)
      (for/hasheq ([(k v) (in-hash s)])
        (values k (add-annotate-property v)))]
     [(hash-eqv? s)
      (for/hasheqv ([(k v) (in-hash s)])
        (values k (add-annotate-property v)))]
     [else
      (for/hash ([(k v) (in-hash s)])
        (values k (add-annotate-property v)))])]
   [else s]))

(define-values/invoke-unit/infer stacktrace/filter@)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Execute counts

(define execute-info (make-hasheq))

(define execute-counts-enabled (make-parameter #f))

(define (register-executed-once key)
  (let ([i (hash-ref execute-info key)])
    (set-mcdr! i (add1 (mcdr i)))))

(define (execute-point mark expr)
  (if (execute-counts-enabled)
      (let ([key (gensym)])
        (hash-set! execute-info key (mcons mark 0))
        (with-syntax ([key (datum->syntax #f key (quote-syntax here))]
                      [expr expr]
                      [register-executed-once register-executed-once]);<- 3D!
          #'(begin (register-executed-once 'key)
                   expr)))
      expr))

(define (get-execute-counts)
  (hash-map execute-info
            (lambda (k v) (cons (mcar v) (mcdr v)))))

(define (annotate-executed-file filename-path [display-string "^.,"])
  (annotate-file filename-path (get-execute-counts) display-string))

;; shared functionality for annotate-executed-file and annotate-covered-file
(define (annotate-file name counts display-string)
  (let ([name (path->complete-path name (current-directory))])
    (let* (;; Filter relevant syntaxes
           [here (filter (lambda (s)
                           (and (equal? name (syntax-source (car s)))
                                (syntax-position (car s))))
                         counts)]
           ;; Sort them: earlier first, wider if in same position
           [sorted (sort here
                         (lambda (a b)
                           (let ([ap (syntax-position (car a))]
                                 [bp (syntax-position (car b))])
                             (or (< ap bp)
                                 (and (= ap bp)
                                      (> (syntax-span (car a))
                                         (syntax-span (car b))))))))]
           ;; Merge entries with the same position+span
           [sorted (if (null? sorted)
                       sorted ; guarantee one element for the next case
                       (let loop ([xs (reverse sorted)] [r '()])
                         (cond [(null? (cdr xs)) (append xs r)]
                               [(and (= (syntax-position (caar xs))
                                        (syntax-position (caadr xs)))
                                     (= (syntax-span (caar xs))
                                        (syntax-span (caadr xs))))
                                ;; doesn't matter which syntax object is kept,
                                ;; we only care about its position+span
                                (loop (cons (cons (caar xs)
                                                  (max (cdar xs) (cdadr xs)))
                                            (cddr xs))
                                      r)]
                               [else (loop (cdr xs) (cons (car xs) r))])))]
           [pic (make-string (file-size name) #\space)]
           [display-string
            (case display-string
              [(#t) "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"]
              [(#f) "#."]
              [else display-string])]
           [many-char (string-ref display-string
                                  (sub1 (string-length display-string)))])
      ;; Fill out picture
      (for-each (lambda (s)
                  (let ([pos (sub1 (syntax-position (car s)))]
                        [span (syntax-span (car s))]
                        [key (let ([k (cdr s)])
                               (if (< k (string-length display-string))
                                   (string-ref display-string k)
                                   many-char))])
                    (let loop ([p pos])
                      (unless (= p (+ pos span))
                        (string-set! pic p key)
                        (loop (add1 p))))))
                sorted)
      ;; Write annotated file
      (with-input-from-file name
        (lambda ()
          (let loop ()
            (let ([pos (file-position (current-input-port))]
                  [line (read-line (current-input-port) 'any)])
              (unless (eof-object? line)
                (printf "~a\n" line)
                (let ([w (string-length line)])
                  ;; Blank leading spaces in pic (copy them: works for tabs)
                  (let loop ([i 0])
                    (when (and (< i w)
                               (char-whitespace? (string-ref line i)))
                      (string-set! pic (+ pos i) (string-ref line i))
                      (loop (add1 i))))
                  (printf "~a\n" (substring pic pos (+ pos w))))
                (loop)))))))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Eval handler, exception handler

(define instrumenting-enabled
  (make-parameter #t))
(define error-context-display-depth
  (make-parameter 10000 (lambda (x) (and (integer? x) x))))

;; port exn -> void
;; effect: prints out the context surrounding the exception
(define (print-error-trace p x)
  (let loop ([n (error-context-display-depth)]
             [l (map st-mark-source
                     (continuation-mark-set->list (exn-continuation-marks x)
                                                  errortrace-key))])
    (cond
      [(or (zero? n) (null? l)) (void)]
      [(pair? l)
       (let* ([stx (car l)]
              [source (syntax-source stx)]
              [file (cond
                      [(string? source) source]
                      [(path? source)
                       (path->string source)]
                      [(not source)
                       #f]
                      [else
                       (format "~a" source)])]
              [line (syntax-line stx)]
              [col (syntax-column stx)]
              [pos (syntax-position stx)])
         (fprintf p "\n   ~a~a: ~.s"
                  (or file "[unknown source]")
                  (cond [line (format ":~a:~a" line col)]
                        [pos (format "::~a" pos)]
                        [else ""])
                  (syntax->datum stx))
         (loop (- n 1) (cdr l)))])))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Profile printer

(define (output-profile-results paths? sort-time?)
  (profiling-enabled #f)
  (error-print-width 50)
  (printf "Sorting profile data...\n")
  (let* ([sel (if sort-time? cadr car)]
         [counts (sort (filter (lambda (c) (positive? (car c)))
                               (get-profile-results))
                       < #:key sel)]
         [total 0])
    (for-each
     (lambda (c)
       (set! total (+ total (sel c)))
       (printf "=========================================================\n")
       (printf "time = ~a : no. = ~a : ~.s in ~s\n"
               (cadr c) (car c) (caddr c) (cadddr c))
       ;; print call paths
       (when paths?
         (for-each
          (lambda (cms)
            (unless (null? (cdr cms))
              (printf "  ~e VIA ~e" (car cms) (caadr cms))
              (for-each
               (lambda (cm)
                 (printf " <- ~e" (car cm)))
               (cddr cms))
              (printf "\n")))
          (sort (cadddr (cdr c)) (lambda (a b) (> (car a) (car b)))))))
     counts)
    (printf "Total sample ~a: ~a\n" (if sort-time? "time" "counts") total)))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(define errortrace-annotate
  (lambda (top-e)
    (define (normal e)
      (define expanded-e (expand-syntax (add-annotate-property e)))
      (parameterize ([original-stx e]
                     [expanded-stx expanded-e])
        (annotate-top expanded-e (namespace-base-phase))))
    (syntax-case top-e ()
      [(mod name . reste)
       (and (identifier? #'mod)
            (free-identifier=? #'mod 
                               (namespace-module-identifier)
                               (namespace-base-phase)))
       (if (eq? (syntax-e #'name) 'errortrace-key)
           top-e
           (let ([expanded-e (normal top-e)])
             (cond
              [(has-cross-phase-declare?
                (syntax-case expanded-e ()
                  [(mod name init-import mb) #'mb]))
               expanded-e]
              [else
               (initialize-test-coverage)
               (add-test-coverage-init-code
                (transform-all-modules 
                 expanded-e
                 (lambda (top-e mod-id)
                   (syntax-case top-e ()
                     [(mod name init-import mb)
                      (syntax-case (disarm #'mb) (#%plain-module-begin)
                        [(#%plain-module-begin body ...)
                         (let ([meta-depth ((count-meta-levels 0) #'(begin body ...))])
                           (copy-props
                            top-e
                            #`(#,mod-id name init-import
                                        #,(syntax-rearm
                                           #`(#%plain-module-begin
                                              #,(generate-key-imports meta-depth)
                                              body ...)
                                           #'mb))))])]))))])))]
      [_else
       (let ([e (normal top-e)])
         (let ([meta-depth ((count-meta-levels 0) e)])
           ;; We need to force the `require`s now, so that `e` can be compiled.
           ;; It doesn't work to reply on `begin` unrolling for a top-level `eval`,
           ;; because we're in a compile handler and already committed to a single form.
           (for ([i (in-range meta-depth)])
             (namespace-require `(for-meta ,(add1 i) errortrace/errortrace-key)))
           #`(begin
               #,(generate-key-imports meta-depth)
               #,e)))])))

(define (has-cross-phase-declare? e)
  (for/or ([a (in-list (or (syntax->list e) '()))])
    (syntax-case* a (#%declare begin) (lambda (a b)
                                        (free-identifier=? a b 0 (namespace-base-phase)))
      [(#%declare kw ...)
       (for/or ([kw (in-list (syntax->list #'(kw ...)))])
         (eq? (syntax-e kw) '#:cross-phase-persistent))]
      [(begin . e)
       (has-cross-phase-declare? #'e)]
      [_ #f])))

(define-namespace-anchor orig-namespace)
                      
(define (make-errortrace-compile-handler)
  (let ([orig (current-compile)]
        [reg (namespace-module-registry (current-namespace))]
        [phase (namespace-base-phase (current-namespace))])
    (namespace-attach-module (namespace-anchor->namespace orig-namespace) 'racket/base)
    (namespace-attach-module (namespace-anchor->namespace orig-namespace) 'errortrace/errortrace-key)
    (lambda (e immediate-eval?)
      (orig
       (if (and (instrumenting-enabled)
                (eq? reg
                     (namespace-module-registry (current-namespace)))
                (equal? phase
                        (namespace-base-phase (current-namespace)))
                (not (compiled-expression? (if (syntax? e)
                                               (syntax-e e)
                                               e))))
           (let ([e2 (errortrace-annotate
                      (if (syntax? e)
                          e
                          (namespace-syntax-introduce
                           (datum->syntax #f e))))])
             e2)
           e)
       immediate-eval?))))

(define errortrace-compile-handler (make-errortrace-compile-handler))

(define errortrace-error-display-handler
  (let ([orig (error-display-handler)])
    (lambda (msg exn)
      (if (exn? exn)
          (let ([p (open-output-string)])
            (display (exn-message exn) p)
            (display "\n  errortrace...:" p)
            (print-error-trace p exn)
            (orig (get-output-string p) exn))
          (orig msg exn)))))

(provide/contract
 [annotate-covered-file (->* (path-string?) ((or/c string? #t #f)) void?)]
 [annotate-executed-file (->* (path-string?) ((or/c string? #t #f)) void?)])
(provide make-errortrace-compile-handler
         errortrace-compile-handler
         errortrace-error-display-handler
         errortrace-annotate
         
         print-error-trace
         error-context-display-depth
         
         instrumenting-enabled
         
         profiling-enabled
         profiling-record-enabled
         profile-paths-enabled
         get-profile-results
         output-profile-results
         clear-profile-results
         
         execute-counts-enabled
         get-execute-counts
         
         ;; need to rename here to avoid having to rename when the unit is invoked.
         (rename-out [test-coverage-enabled coverage-counts-enabled])
         get-coverage
         test-coverage-info
         
         annotate-top)
