src/evaluation.lisp @ eee835835b39

Add LICENSE
author Steve Losh <steve@stevelosh.com>
date Tue, 14 Mar 2017 13:33:00 +0000
parents 919ebd924aac
children (none)
(in-package :nrepl)

(defvar *last-trace* nil)

(define-condition evaluation-error (error)
  ((text :initarg :text :reader text)
   (orig :initarg :orig :reader orig)
   (data :initarg :data :reader data :initform ())))

(defclass evaluator ()
  ((standard-input :initarg :in :reader in)
   (standard-output :initarg :out :reader out)
   (standard-error :initarg :err :reader err)))


(defun read-data (stream)
  (flex:octets-to-string
    (flex:get-output-stream-sequence stream)
    :external-format :utf-8))

(defun shuttle-stream (stream stream-name message)
  "Read data from `stream` and shuttle it back to the client.

  Chunks of data will be read from `stream` until it's finished and closed.

  For each hunk of data read, a response will be sent back to the client using
  the transport defined in `message`, looking something like:

    {\"status\" \"ok\"
     \"stdout\" \"...data...\"}

  `stream-name` should be the name of the stream being shuttled, like
  \"stderr\", and will be used as the key in the response.

  "
  (loop
    :for data = (read-data stream)
    :until (and (not (open-stream-p stream))
                (equal data ""))
    :do (progn
          (when (not (string= data ""))
            (respond message (make-map "status" '("ok")
                                       stream-name data)))
          (sleep 0.1))))


(defun get-forms (code)
  "Get all lisp forms from `code`.

  If `code` is a string, the forms will be read out of it, and an
  `evaluation-error` signaled if the input is mangled.

  If `code` is anything else it will just be returned as-is.

  "
  (if (stringp code)
    (handler-case
        (read-all-from-string code)
      (error (e)
             (error 'evaluation-error
                    :text "Malformed input!"
                    :orig e)))
    code))


(defun parse-frame (frame)
  (let ((call-form (list* (dissect:call frame) (dissect:args frame)))
        (location (list (dissect:file frame) (dissect:line frame))))
    (list call-form location)))

(defun parse-stack (stack)
  (mapcar #'parse-frame (nthcdr 1 (reverse stack))))

(defun string-trace-whole (trace)
  (with-output-to-string (s)
    (loop :for (call-form (file line)) :in trace
          :do (format s "~S~%" call-form))))

(defun string-trace-components (trace)
  (loop :for (call-form (file line)) :in trace
        :collect (list (format nil "~S" call-form)
                       (if file
                         (format nil "~A" file)
                         "")
                       (if line
                         (format nil "~D" line)
                         ""))))


(defun nrepl-evaluate-form (form)
  (declare (optimize (debug 3)))
  (prin1-to-string
    (handler-bind
      ((error
         (lambda (err)
           ; if we hit an error, get the stack trace before reraising.  if we
           ; wait til later to print it, it'll be too late.
           (error 'evaluation-error
                  :text "Error during evaluation!"
                  :orig err
                  :data (let* ((raw (dissect:stack))
                               (clean (parse-stack raw)))
                          (setf *last-trace* raw)
                          (list
                            "form" (prin1-to-string form)
                            "stack-trace" (string-trace-components clean)
                            "stack-trace-string" (string-trace-whole clean)))))))
      (dissect:with-truncated-stack ()
        (eval form)))))


(defun evaluate-forms (message forms &optional in-package)
  "Evaluate each form in `forms` and shuttle back the responses.

  `forms` can be a string, in which case the forms will be read out of it, or
  a ready-to-go list of actual forms.

  `in-package` can be a package designator, or `nil` to just use `*package*`.

  "
  (let* ((captured-out (flex:make-in-memory-output-stream))
         (captured-err (flex:make-in-memory-output-stream))
         (*standard-output*
           (flex:make-flexi-stream captured-out :external-format :utf-8))
         (*error-output*
           (flex:make-flexi-stream captured-err :external-format :utf-8)))
    (flet ((eval-form (form)
             (let ((result (nrepl-evaluate-form form)))
               (respond message
                        (make-map "form" (prin1-to-string form)
                                  "value" result))))
           (error-respond (e)
             (respond message
                      (apply #'make-map
                             "status" '("error")
                             "error" (text e)
                             "original" (format nil "~S" (orig e))
                             (data e))))
           (make-shuttle-thread (stream desc)
             (bt:make-thread
               (lambda () (shuttle-stream stream desc message))
               :name (format nil "NREPL ~A writer" desc))))
      (unwind-protect
          (progn
            (make-shuttle-thread captured-out "stdout")
            (make-shuttle-thread captured-err "stderr")
            (handler-case
                (progn
                  (let ((*package* (parse-in-package in-package)))
                    (mapc #'eval-form (get-forms forms)))
                  (respond message (make-map "status" '("done"))))
              (evaluation-error (e) (error-respond e))))
        (close captured-out)
        (close captured-err)))))