From 2aa912dcc7d3732ba04eb96ab1cd186565a48f98 Mon Sep 17 00:00:00 2001 From: Matus Goljer Date: Sat, 18 Mar 2023 21:24:12 +0100 Subject: [PATCH] feat(analysis): add expansion and analysis of macros Fixes #205 --- elsa-analyser.el | 19 ++++ elsa-error.el | 63 +++++++----- elsa-form.el | 16 +++ elsa-lsp-core.el | 16 ++- elsa-lsp.el | 2 + elsa-reader.el | 261 +++++++++++++++++++++++++++++++++++++++-------- elsa-startup.el | 39 +++++++ elsa-state.el | 13 ++- elsa.el | 71 +++++++------ 9 files changed, 398 insertions(+), 102 deletions(-) create mode 100644 elsa-startup.el diff --git a/elsa-analyser.el b/elsa-analyser.el index c6cdfa8..9ee1503 100644 --- a/elsa-analyser.el +++ b/elsa-analyser.el @@ -656,6 +656,13 @@ The registered object can be a `defun', `defmacro', or :return (elsa-type-mixed)) :arglist (elsa-form-to-lisp args)))))) +(defun elsa--analyse:elsa--form (form scope state) + "Analyse special marker for macroexpanded forms." + (let ((real-form (elsa-nth 2 form))) + (elsa--analyse-form real-form scope state) + (oset form type (oref real-form type)) + (oset form narrow-types (oref real-form narrow-types)))) + (defun elsa--analyse:defmacro (form _scope state) "just skip for now, it's too complicated." (let ((name (elsa-get-name (elsa-cadr form))) @@ -1142,6 +1149,18 @@ SCOPE and STATE are the scope and state objects." (pcase name ((guard (functionp analyse-fn-name)) (funcall analyse-fn-name form scope state)) + ((guard (and (oref form expanded-form) + (oref form was-expanded))) + (let ((exp-form (oref form expanded-form))) + (elsa--analyse-form exp-form scope state) + (let ((form-alist nil)) + (elsa-form-visit form + (lambda (fm) + (when-let ((expanded (oref fm expanded-form))) + (oset fm type (oref expanded type)) + (oset fm narrow-types (oref expanded narrow-types)))))) + (oset form type (oref exp-form type)) + (oset form narrow-types (oref exp-form narrow-types)))) (`\` (elsa--analyse-backquote form scope state)) (`\, (elsa--analyse-unquote form scope state)) (`\,@ (elsa--analyse-splice form scope state)) diff --git a/elsa-error.el b/elsa-error.el index db10a24..fea7d7b 100644 --- a/elsa-error.el +++ b/elsa-error.el @@ -108,37 +108,46 @@ In general, we recognize three states: error, warning, notice (cl-defmethod elsa-message-to-lsp-severity ((_this elsa-notice)) lsp/diagnostic-severity-information) +(defun elsa--message-resolve (expression) + (or (when-let ((of (elsa-form-find-parent expression + (lambda (x) + (oref x original-form))))) + (oref of original-form)) + expression)) + (cl-defmethod elsa-message-format ((this elsa-message)) "Format an `elsa-message'." - (with-ansi - (bright-green "%s" (oref this line)) - ":" - (green "%s" (or (oref this column) "?")) - ":" - (elsa-message-type-ansi this) - ":" - (format "%s" (replace-regexp-in-string "%" "%%" (oref this message))))) + (let ((expr (elsa--message-resolve (oref this expression)))) + (with-ansi + (bright-green "%s" (oref expr line)) + ":" + (green "%s" (or (oref expr column) "?")) + ":" + (elsa-message-type-ansi this) + ":" + (format "%s" (replace-regexp-in-string "%" "%%" (oref this message)))))) (cl-defmethod elsa-message-to-lsp ((this elsa-message)) - (lsp-make-diagnostic - :code (oref this code) - :range (lsp-make-range - :start (lsp-make-position - :line (1- (oref this line)) - :character (oref this column)) - :end (lsp-make-position - :line (1- (oref this line)) - :character (let ((expr (oref this expression))) - (if (and (elsa-form-sequence-p expr) - (elsa-car expr)) - (oref (elsa-car expr) end-column) - (+ (oref this column) - (if (or (elsa-form-symbol-p expr) - (elsa-get-name expr)) - (length (symbol-name (elsa-get-name expr))) - 1)))))) - :severity (elsa-message-to-lsp-severity this) - :message (oref this message))) + (let ((expression (elsa--message-resolve (oref this expression)))) + (lsp-make-diagnostic + :code (oref this code) + :range (lsp-make-range + :start (lsp-make-position + :line (1- (oref expression line)) + :character (oref expression column)) + :end (lsp-make-position + :line (1- (oref expression line)) + :character (let ((expr expression)) + (if (and (elsa-form-sequence-p expr) + (elsa-car expr)) + (oref (elsa-car expr) end-column) + (+ (oref expression column) + (if (or (elsa-form-symbol-p expr) + (elsa-get-name expr)) + (length (symbol-name (elsa-get-name expr))) + 1)))))) + :severity (elsa-message-to-lsp-severity this) + :message (oref this message)))) (defun elsa--make-message (constructor expression format args) (let (code) diff --git a/elsa-form.el b/elsa-form.el index 5f738fc..c97d747 100644 --- a/elsa-form.el +++ b/elsa-form.el @@ -2,6 +2,7 @@ (require 'trinary) +(require 'elsa-methods) (require 'elsa-types-simple) (defclass elsa-form nil @@ -23,6 +24,21 @@ :type (or elsa-form null) :initarg :previous :documentation "Previous form in a sequence.") + (was-expanded + :type boolean + :initform nil + :documentation "Was this form a macro call form which was expanded. + +This is only set on the macro call form which was expanded, not on the +child forms.") + (expanded-form + :type (or elsa-form null) + :initform nil + :documentation "The form corresponding to this form in macroexpanded subtree.") + (original-form + :type (or elsa-form null) + :initform nil + :documentation "The form from which this form was macroexpanded.") (annotation :type list :initarg :annotation :initform nil)) :abstract t) diff --git a/elsa-lsp-core.el b/elsa-lsp-core.el index 5f80083..8f44d85 100644 --- a/elsa-lsp-core.el +++ b/elsa-lsp-core.el @@ -244,11 +244,17 @@ be re-analysed during textDocument/didOpen handler."))) (defun elsa-lsp--analyze-textDocument/hover (form _state _method params) (-let* (((&HoverParams :position (&Position :line :character)) - params)) - (when (and (= (oref form line) (1+ line)) - (<= (oref form column) character) - (or (< (1+ line) (oref form end-line)) - (<= character (oref form end-column)))) + params) + (orig-form (oref form original-form))) + (when (or (and orig-form + (= (oref orig-form line) (1+ line)) + (<= (oref orig-form column) character) + (or (< (1+ line) (oref orig-form end-line)) + (<= character (oref orig-form end-column)))) + (and (= (oref form line) (1+ line)) + (<= (oref form column) character) + (or (< (1+ line) (oref form end-line)) + (<= character (oref form end-column))))) (throw 'lsp-response (lsp-make-hover :contents (lsp-make-markup-content diff --git a/elsa-lsp.el b/elsa-lsp.el index c03eaf7..50adc67 100644 --- a/elsa-lsp.el +++ b/elsa-lsp.el @@ -7,6 +7,8 @@ (defun elsa-lsp-stdin-loop () "Reads from standard input in a loop and process incoming requests." (elsa-load-config) + (require 'elsa-startup) + (-> (lgr-get-logger "elsa") (lgr-reset-appenders) (lgr-add-appender diff --git a/elsa-reader.el b/elsa-reader.el index 320f262..b9523f0 100644 --- a/elsa-reader.el +++ b/elsa-reader.el @@ -6,7 +6,10 @@ (require 'backquote) (require 'seq) (require 'map) +(require 'macroexp) +(require 'edebug) +(require 'lgr) (require 'trinary) (require 'elsa-form) @@ -317,7 +320,9 @@ prefix and skipped by the sexp scanner.") (map-elt (elsa-form-sequence seq) key default)) (cl-defmethod elsa-form-print ((this elsa-form-list)) - (format "(%s)" (mapconcat 'elsa-form-print (oref this sequence) " "))) + (if (eq (elsa-get-name this) 'elsa--form) + (elsa-form-print (elsa-nth 2 this)) + (format "(%s)" (mapconcat 'elsa-form-print (oref this sequence) " ")))) (cl-defmethod elsa-form-print ((this list)) (format "(%s)" (mapconcat 'elsa-form-print this " "))) @@ -452,6 +457,153 @@ prefix and skipped by the sexp scanner.") (cl-defmethod elsa-cdr ((this elsa-form-improper-list)) (cdr (oref this conses))) +(defun elsa--form (_offset form) + "This is a fake form used in macroexpand to track original position. + +We use edebug to instrument forms in macroexpansion. Instead of +`edebug-before' and `edebug-after', we replace them with just +`elsa--form' with first argument the *end* position of original +form and second argument being the original form. Using this +information we can restore the original position and pair the +original and expanded form." + form) + +(put 'elsa--form 'gv-expander + (lambda (do index place) + (gv-letplace (getter setter) place + (funcall do `(elsa--form ,index ,getter) + (lambda (store) + `(elsa--form ,index ,(funcall setter store))))))) + +;; For some reason these variables need to be redefined... +(defvar edebug-top-window-data nil) +(defvar edebug-form-begin-marker nil) +(defvar edebug-offset-index 0) +(defvar edebug-offset-list nil) + +(defun elsa--replace-edebug (tree &optional offsets) + "Replace edebug instrumentation forms with `elsa--form'." + (-tree-map-nodes + (lambda (x) (and (listp x) + (or + (and (eq (car x) 'edebug-after) + (numberp (nth 2 x))) + (and (eq (car x) 'edebug-enter) + (listp (nth 1 x)) + (eq (car (nth 1 x)) 'quote) + (symbolp (cadr (nth 1 x))) + (string-match-p + "\\`edebug-anon" + (symbol-name (cadr (nth 1 x))))) + (elsa--improper-list-p x)))) + (lambda (x) + (cond + ((elsa--improper-list-p x) x) + ((and (eq (car x) 'edebug-after) offsets) + `(elsa--form ,(aref offsets (nth 2 x)) ,(elsa--replace-edebug (nth 3 x) offsets))) + ((eq (car x) 'edebug-enter) + (let ((offsets (nth 2 (get (cadr (nth 1 x)) 'edebug)))) + `(progn + ,@(elsa--replace-edebug + (nthcdr 2 (cadr (nth 3 x))) + (vconcat (mapcar (lambda (x) (+ x edebug-form-begin-marker)) offsets)))))))) + tree)) + +(defun elsa--instrument-form (form) + "Instrument FORM with macro-expansion markers. + +This process uses edebug but replaces edebug annotations with +`elsa--form'." + (let* ((edebug-all-forms t) + (spec (edebug-get-spec (car form))) + (edebug-form-begin-marker (point-marker))) + (when spec + (let ((instrumented (edebug-read-and-maybe-wrap-form))) + (elsa--replace-edebug instrumented))))) + +;; This is brutal, but we have to do it! Here we simply make sure to +;; annotate all the forms, even constants, because we have to be able +;; to track them back to the "unexpanded" forms. +(defun edebug-form (cursor) + ;; Return the instrumented form for the following form. + ;; Add the point offsets to the edebug-offset-list for the form. + (let* ((form (edebug-top-element-required cursor "Expected form")) + (offset (edebug-top-offset cursor))) + (prog1 + (cond + ((consp form) + ;; The first offset for a list form is for the list form itself. + (let* ((head (car form)) + (spec (and (symbolp head) (edebug-get-spec head))) + (new-cursor (edebug-new-cursor form offset))) + ;; Find out if this is a defining form from first symbol. + ;; An indirect spec would not work here, yet. + (if (and (consp spec) (eq '&define (car spec))) + (edebug-defining-form + new-cursor + (car offset);; before the form + (edebug-after-offset cursor) + (cons (symbol-name head) (cdr spec))) + ;; Wrap a regular form. + (edebug-make-before-and-after-form + (edebug-inc-offset (car offset)) + (edebug-list-form new-cursor) + ;; After processing the list form, the new-cursor is left + ;; with the offset after the form. + (edebug-inc-offset (edebug-cursor-offsets new-cursor)))))) + ((vectorp form) + ;; ELSA: vectors are "self-evaluating", but we still might + ;; want to wrap them, because they can be say argument to a + ;; different function and we want to attach a message. + `(edebug-after + 0 + ,(edebug-inc-offset (cdr (last (car (edebug-cursor-offsets cursor))))) + ,form)) + (t (edebug-make-after-form form (edebug-inc-offset (cdr offset))))) + (edebug-move-cursor cursor)))) + +(defun elsa--macroexpand-form (form elsa-orig-form) + "Macroexpand buffer FORM corresponding to elsa reader form ELSA-ORIG-FORM." + (oset + elsa-orig-form + expanded-form + (save-excursion + (goto-char (oref elsa-orig-form start)) + (condition-case err + (when-let* ((instrumented-form (elsa--instrument-form form)) + (macroexpanded-form (macroexpand-all instrumented-form)) + (exp-state (elsa-state))) + (oset exp-state no-expand t) + (with-temp-buffer + (emacs-lisp-mode) + (insert (format "%S" macroexpanded-form)) + (goto-char (point-min)) + (elsa-read-form exp-state))) + (error + (lgr-error (lgr-get-logger "elsa.reader.macroexpand") + (error-message-string err)) + nil)))) + + (when (oref elsa-orig-form expanded-form) + (oset elsa-orig-form was-expanded t) + + (let ((form-alist nil)) + (elsa-form-visit (oref elsa-orig-form expanded-form) + ;; if name is elsa--form add to alist indexed by cadr + (lambda (form) + (when (and (elsa-form-function-call-p form 'elsa--form) + (slot-boundp (elsa-cadr form) 'value)) + (push (cons (oref (elsa-cadr form) value) + form) + form-alist)))) + (elsa-form-visit elsa-orig-form + (lambda (fm) + (when-let ((expanded (cdr (assq (oref fm end) form-alist)))) + (oset fm expanded-form expanded) + (oset expanded original-form fm))))) + + (oset (oref elsa-orig-form expanded-form) original-form elsa-orig-form))) + ;; (elsa--read-cons :: (function ((list mixed) mixed) mixed)) (defsubst elsa--read-cons (form state) (elsa--skip-whitespace-forward) @@ -476,31 +628,50 @@ prefix and skipped by the sexp scanner.") (while (>= (cl-decf depth) 0) (up-list)) (apply 'cl-list* (nreverse items))) :end (progn (up-list) (point))) - (elsa-form-list - :start (prog1 (point) (down-list)) - :sequence - (let ((depth 0) - (items)) - (while form - (cond - ((elsa--quote-p (car form)) - (let ((quoted-form (elsa--read-form form state))) - (setq items - (-concat (reverse (oref quoted-form sequence)) - items))) - (setq form nil)) - (t - (push (elsa--read-form (car form) state) items) - (!cdr form))) - (elsa--skip-whitespace-forward) - (when (and form (looking-at-p "\\.[^[:alnum:].]")) - (if (elsa--quote-p (car form)) - (forward-sexp) ;; skip the dot - (cl-incf depth) - (down-list)))) - (while (>= (cl-decf depth) 0) (up-list)) - (nreverse items)) - :end (progn (up-list) (point))))) + ;; In case this form is a macro and has no "elsa analysis" + ;; attached, we need to expand it and attach the expanded form to + ;; this form for analysis. + (let* ((orig-form (copy-sequence form)) + (maybe-name (car-safe form)) + (analyse-fn-name (and (listp form) + (symbolp (car form)) + (intern (concat "elsa--analyse:" (symbol-name (car form)))))) + (should-expand-form (and analyse-fn-name + (not (oref state no-expand)) + (not (functionp analyse-fn-name)) + (not (null (macrop maybe-name))))) + (list-form (elsa-form-list + :start (prog1 (point) (down-list)) + :sequence + (let ((depth 0) + (items)) + (when should-expand-form + (oset state no-expand t)) + (while form + (cond + ((elsa--quote-p (car form)) + (let ((quoted-form (elsa--read-form form state))) + (setq items + (-concat (reverse (oref quoted-form sequence)) + items))) + (setq form nil)) + (t + (push (elsa--read-form (car form) state) items) + (!cdr form))) + (elsa--skip-whitespace-forward) + (when (and form (looking-at-p "\\.[^[:alnum:].]")) + (if (elsa--quote-p (car form)) + (forward-sexp) ;; skip the dot + (cl-incf depth) + (down-list)))) + (while (>= (cl-decf depth) 0) (up-list)) + (when should-expand-form + (oset state no-expand nil)) + (nreverse items)) + :end (progn (up-list) (point))))) + (when should-expand-form + (elsa--macroexpand-form orig-form list-form)) + list-form))) (defclass elsa-form-function (elsa-form) ((function :type function :initarg :function))) @@ -523,6 +694,7 @@ prefix and skipped by the sexp scanner.") (elsa--skip-whitespace-forward) (let* ((expanded-form nil) (start (point)) + (should-expand-form (not (oref state no-expand))) (seq (cons (elsa--set-line-and-column (elsa-form-symbol @@ -540,20 +712,29 @@ prefix and skipped by the sexp scanner.") (forward-char (length (symbol-name (car form))))))) :name (car form) :end (point))) - (-map (lambda (f) (elsa--read-form f state)) - ;; make sure to make list here, because we might - ;; be reading a "quoted quote" where the structure - ;; can be for example an alist with key - ;; `function' or `quote' and value anything, - ;; including just a symbol. - (-list (cdr form)))))) - (elsa-form-list - :quote-type (car form) - :start start - :sequence seq - :end (progn - (when expanded-form (up-list)) - (point))))) + (progn + (when should-expand-form + (oset state no-expand t)) + (prog1 (-map (lambda (f) (elsa--read-form f state)) + ;; make sure to make list here, because we might + ;; be reading a "quoted quote" where the structure + ;; can be for example an alist with key + ;; `function' or `quote' and value anything, + ;; including just a symbol. + (-list (cdr form))) + (when should-expand-form + (oset state no-expand nil)))))) + (list-form (elsa-form-list + :quote-type (car form) + :start start + :sequence seq + :end (progn + (when expanded-form (up-list)) + (point))))) + (when (and should-expand-form + (eq (oref list-form quote-type) '\`)) + (elsa--macroexpand-form form list-form)) + list-form)) (defsubst elsa--process-annotation (reader-form comment-form state) "Process annotation over a form. diff --git a/elsa-startup.el b/elsa-startup.el new file mode 100644 index 0000000..bea5c59 --- /dev/null +++ b/elsa-startup.el @@ -0,0 +1,39 @@ +;;; elsa-startup.el --- Startup setup -*- lexical-binding: t -*- + +;; Copyright (C) 2023 Matúš Goljer + +;; Author: Matúš Goljer +;; Maintainer: Matúš Goljer +;; Version: 0.0.1 +;; Created: 18th March 2023 +;; Package-requires: ((dash "2.17.0")) +;; Keywords: + +;; This program is free software; you can redistribute it and/or +;; modify it under the terms of the GNU General Public License +;; as published by the Free Software Foundation; either version 3 +;; of the License, or (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; This file is required from both CLI and LSP Elsa instances. + +;;; Code: + +(require 'edebug) + +;; Support deeper instrumented code. +(setq edebug-max-depth 1000) +;; Do not print "Edebug: " spam +(setq edebug-new-definition-function #'ignore) + +(provide 'elsa-startup) +;;; elsa-startup.el ends here diff --git a/elsa-state.el b/elsa-state.el index 9b1dbd3..a695d75 100644 --- a/elsa-state.el +++ b/elsa-state.el @@ -3,6 +3,7 @@ (require 'trinary) +(require 'elsa-form) (require 'elsa-types) (require 'elsa-scope) (require 'elsa-error) @@ -362,7 +363,17 @@ a single file, form or set of forms.") (dependencies :initform nil :type list :documentation "List of all recursively processed dependencies") (ignored-lines :initform nil) - (reachable :initform (list (trinary-true))) + (reachable + :initform (list (trinary-true)) + :documentation "Is this form reachable during execution? + +This field is used during the analysis step.") + (no-expand + :type boolean + :initform nil + :documentation "Whether we are in an expanded macro. + +We should not try to instrument already instrumented forms.") ;; TODO: I don't remember or understand what this is. I'm going to ;; commit it since I can't see it making any difference. But it ;; probably serves some purpose. diff --git a/elsa.el b/elsa.el index e6e0384..a81ea4f 100644 --- a/elsa.el +++ b/elsa.el @@ -118,7 +118,8 @@ tokens." (with-ansi (green "[%s]" (elsa-global-state-get-counter global-state)) (format " Processing file %s ..." file)))) - (let ((state (elsa-state :global-state global-state)) + (let ((lgr (lgr-get-logger "elsa.process.file")) + (state (elsa-state :global-state global-state)) (current-time (current-time)) (form)) (elsa-state-clear-file-state global-state file) @@ -162,6 +163,9 @@ tokens." ;; When not running as language server, just crash on errors. (condition-case nil (while (setq form (elsa-read-form state)) + (lgr-trace lgr + "Processing form %s" + (replace-regexp-in-string "%" "%%%%" (elsa-tostring form))) (elsa-analyse-form state form)) (end-of-file t)))) (unless no-log @@ -296,27 +300,29 @@ GLOBAL-STATE is the initial configuration." (push frame frames)) (when (eq (elt frame 1) 'elsa--worker-debugger) (setq in-program-stack t))) - (mapconcat - ;; Frame is EVALD FUNC ARGS FLAGS. Flags is - ;; useless so we drop it. - (lambda (frame) - (->> - (if (car frame) - (format " %S%s" - (cadr frame) - (if (nth 2 frame) - (cl-prin1-to-string (nth 2 frame)) - "()")) - (format " (%S %s)" - (cadr frame) - (mapconcat - (lambda (x) (format "%S" x)) - (nth 2 frame) - " "))) - (replace-regexp-in-string "\n" "\\\\n") - (replace-regexp-in-string "%" "%%"))) - (nreverse frames) - "\n"))))) + (concat + (format " %s\n" args) + (mapconcat + ;; Frame is EVALD FUNC ARGS FLAGS. Flags is + ;; useless so we drop it. + (lambda (frame) + (->> + (if (car frame) + (format " %S%s" + (cadr frame) + (if (nth 2 frame) + (cl-prin1-to-string (nth 2 frame)) + "()")) + (format " (%S %s)" + (cadr frame) + (mapconcat + (lambda (x) (format "%S" x)) + (nth 2 frame) + " "))) + (replace-regexp-in-string "\n" "\\\\n") + (replace-regexp-in-string "%" "%%%%"))) + (nreverse frames) + "\n")))))) (defun elsa--worker-function-factory (worker-id project-directory) "Return function running in the Elsa analysis worker." @@ -328,7 +334,7 @@ GLOBAL-STATE is the initial configuration." (lgr-reset-appenders) (lgr-add-appender (-> (elsa-worker-appender) - (lgr-set-layout (lgr-layout-format :format "[%K] %m")))) + (lgr-set-layout (lgr-layout-format :format "[%g] %K %m")))) (lgr-set-threshold lgr-level-warn)) (let ((result nil)) (let ((lgr (lgr-get-logger "elsa.analyse.worker")) @@ -396,7 +402,10 @@ GLOBAL-STATE is the initial configuration." (ack (plist-get result :ack))) (cond ((equal ack "error") - (lgr-fatal lgr (plist-get result :error)) + (lgr-fatal lgr + (with-ansi + (red "Worker %d errored with:\n" worker-id) + (yellow (plist-get result :error)))) (kill-emacs 1)) ((equal op "echo") (lgr-debug lgr @@ -540,6 +549,7 @@ used by the LSP server to not reload already processed files." (elsa-get-elapsed start-time))) (cl-incf (oref global-state processed-file-index)) (elsa-save-cache state global-state) + (elsa-state-update-global state global-state) state)))) (defun elsa-analyse-file (file global-state &optional already-loaded) @@ -657,6 +667,9 @@ Currently, these flags are supported: were reported. This flag exists because flycheck complains when process exits with non-zero status." (elsa-load-config) + + (require 'elsa-startup) + (let ((errors 0) (warnings 0) (notices 0) @@ -695,11 +708,11 @@ Currently, these flags are supported: (blue "%.3f seconds" duration))) (lgr-trace (lgr-get-logger "elsa.perf") - "memory report %s" - (with-current-buffer (get-buffer-create "*Memory Report*") - (require 'memory-report) - (memory-report) - (buffer-string)))) + "memory report %s" + (with-current-buffer (get-buffer-create "*Memory Report*") + (require 'memory-report) + (memory-report) + (buffer-string)))) (when (and elsa-cli-with-exit (< 0 errors)) (kill-emacs 1))))