;;; swank-macrostep.lisp -- fancy macro-expansion via macrostep.el ;; ;; Authors: Luís Oliveira ;; Jon Oddie ;; ;; License: Public Domain (defpackage swank-macrostep (:use cl swank) (:import-from swank #:*macroexpand-printer-bindings* #:with-buffer-syntax #:with-bindings #:to-string #:macroexpand-all #:compiler-macroexpand-1 #:defslimefun #:collect-macro-forms) (:export #:macrostep-expand-1 #:macro-form-p)) (in-package #:swank-macrostep) (defslimefun macrostep-expand-1 (string compiler-macros? context) (with-buffer-syntax () (let ((form (read-from-string string))) (multiple-value-bind (expansion error-message) (expand-form-once form compiler-macros? context) (if error-message `(:error ,error-message) (multiple-value-bind (macros compiler-macros) (collect-macro-forms-in-context expansion context) (let* ((all-macros (append macros compiler-macros)) (pretty-expansion (pprint-to-string expansion)) (positions (collect-form-positions expansion pretty-expansion all-macros)) (subform-info (loop for form in all-macros for (start end) in positions when (and start end) collect (let ((op-name (to-string (first form))) (op-type (if (member form macros) :macro :compiler-macro))) (list op-name op-type start))))) `(:ok ,pretty-expansion ,subform-info)))))))) (defun expand-form-once (form compiler-macros? context) (multiple-value-bind (expansion expanded?) (macroexpand-1-in-context form context) (if expanded? (values expansion nil) (if (not compiler-macros?) (values nil "Not a macro form") (multiple-value-bind (expansion expanded?) (compiler-macroexpand-1 form) (if expanded? (values expansion nil) (values nil "Not a macro or compiler-macro form"))))))) (defslimefun macro-form-p (string compiler-macros? context) (with-buffer-syntax () (let ((form (handler-case (read-from-string string) (error (condition) (unless (debug-on-swank-error) (return-from macro-form-p `(:error ,(format nil "Read error: ~A" condition)))))))) `(:ok ,(macro-form-type form compiler-macros? context))))) (defun macro-form-type (form compiler-macros? context) (cond ((or (not (consp form)) (not (symbolp (car form)))) nil) ((multiple-value-bind (expansion expanded?) (macroexpand-1-in-context form context) (declare (ignore expansion)) expanded?) :macro) ((and compiler-macros? (multiple-value-bind (expansion expanded?) (compiler-macroexpand-1 form) (declare (ignore expansion)) expanded?)) :compiler-macro) (t nil))) ;;;; Hacks to support macro-expansion within local context (defparameter *macrostep-tag* (gensym)) (defparameter *macrostep-placeholder* '*macrostep-placeholder*) (define-condition expansion-in-context-failed (simple-error) ()) (defmacro throw-expansion (form &environment env) (throw *macrostep-tag* (macroexpand-1 form env))) (defmacro throw-collected-macro-forms (form &environment env) (throw *macrostep-tag* (collect-macro-forms form env))) (defun macroexpand-1-in-context (form context) (handler-case (macroexpand-and-catch `(throw-expansion ,form) context) (error () (macroexpand-1 form)))) (defun collect-macro-forms-in-context (form context) (handler-case (macroexpand-and-catch `(throw-collected-macro-forms ,form) context) (error () (collect-macro-forms form)))) (defun macroexpand-and-catch (form context) (catch *macrostep-tag* (macroexpand-all (enclose-form-in-context form context)) (error 'expansion-in-context-failed))) (defun enclose-form-in-context (form context) (with-buffer-syntax () (destructuring-bind (prefix suffix) context (let* ((placeholder-form (read-from-string (concatenate 'string prefix (prin1-to-string *macrostep-placeholder*) suffix))) (substituted-form (subst form *macrostep-placeholder* placeholder-form))) (if (not (equal placeholder-form substituted-form)) substituted-form (error 'expansion-in-context-failed)))))) ;;;; Tracking Pretty Printer (defun marker-char-p (char) (<= #xe000 (char-code char) #xe8ff)) (defun make-marker-char (id) ;; using the private-use characters U+E000..U+F8FF as markers, so ;; that's our upper limit for how many we can use. (assert (<= 0 id #x8ff)) (code-char (+ #xe000 id))) (defun marker-char-id (char) (assert (marker-char-p char)) (- (char-code char) #xe000)) (defparameter +whitespace+ (mapcar #'code-char '(9 13 10 32))) (defun whitespacep (char) (member char +whitespace+)) (defun pprint-to-string (object &optional pprint-dispatch) (let ((*print-pprint-dispatch* (or pprint-dispatch *print-pprint-dispatch*))) (with-bindings *macroexpand-printer-bindings* (to-string object)))) #-clisp (defun collect-form-positions (expansion printed-expansion forms) (loop for (start end) in (collect-marker-positions (pprint-to-string expansion (make-tracking-pprint-dispatch forms)) (length forms)) collect (when (and start end) (list (find-non-whitespace-position printed-expansion start) (find-non-whitespace-position printed-expansion end))))) ;; The pprint-dispatch table constructed by ;; MAKE-TRACKING-PPRINT-DISPATCH causes an infinite loop and stack ;; overflow under CLISP version 2.49. Make the COLLECT-FORM-POSITIONS ;; entry point a no-op in thi case, so that basic macro-expansion will ;; still work (without detection of inner macro forms) #+clisp (defun collect-form-positions (expansion printed-expansion forms) nil) (defun make-tracking-pprint-dispatch (forms) (let ((original-table *print-pprint-dispatch*) (table (copy-pprint-dispatch))) (flet ((maybe-write-marker (position stream) (when position (write-char (make-marker-char position) stream)))) (set-pprint-dispatch 'cons (lambda (stream cons) (let ((pos (position cons forms))) (maybe-write-marker pos stream) ;; delegate printing to the original table. (funcall (pprint-dispatch cons original-table) stream cons) (maybe-write-marker pos stream))) most-positive-fixnum table)) table)) (defun collect-marker-positions (string position-count) (let ((positions (make-array position-count :initial-element nil))) (loop with p = 0 for char across string unless (whitespacep char) do (if (marker-char-p char) (push p (aref positions (marker-char-id char))) (incf p))) (map 'list #'reverse positions))) (defun find-non-whitespace-position (string position) (loop with non-whitespace-position = -1 for i from 0 and char across string unless (whitespacep char) do (incf non-whitespace-position) until (eql non-whitespace-position position) finally (return i))) (provide :swank-macrostep)