Skip to content

Instantly share code, notes, and snippets.

@jordonbiondo
Last active February 27, 2024 16:21
Show Gist options
  • Save jordonbiondo/592618ee665b18205676949f0b3766fa to your computer and use it in GitHub Desktop.
Save jordonbiondo/592618ee665b18205676949f0b3766fa to your computer and use it in GitHub Desktop.
Emacs string interpolation macro example
;; Example usage
;; (let ((a 5)
;; (b 6))
;; (message "%s" ($fmt "If you add ${a} and ${b}, you get ${(+ a b)}!")))
;;
;; Outputs: If you add 5 and 6 you get 11!
;;
;;
;; Expanding the macro:
;; (macroexpand-all
;; '(let ((a 5)
;; (b 6))
;; (message "%s" ($fmt "If you add ${a} and ${b}, you get ${(+ a b)}!"))))
;;
;; Creates this code:
;; (let ((a 5)
;; (b 6))
;; (message "%s" (format "If you add %s and %s, you get %s!" a b (+ a b))))
;;
;;
;; The hardest part of this problem is not the code generation, it is the parsing
;; This code uses `read` to attempt to find the valid elisp inside the brackets
;; It `read's multiple times from the start of ${ to the nearest }, if it fails it tries to find the next }
;; This is to ensure nested $fmt calls, and elisp that contains $ or { or } unrelated to the $fmt call doesn't
;; break the macro.
;; Using a properly written parser grammar would clean up the code, almost all the code below is about handling
;; parsing, not the actual code generation.
;; There is probably a lib out there that does this already and does it better, but if not, could be the start
;; of a nice utility.
(defun $fmt-expand (string)
(let ((format-strings '())
(format-arguments '())
(position 2))
(with-temp-buffer
(insert " " string)
(goto-char 0)
(while (ignore-errors (re-search-forward "[^\\]\\${"))
(let ((text (buffer-substring-no-properties position (+ 1 (match-beginning 0)))))
(unless (string-empty-p text)
(push (buffer-substring-no-properties position (+ 1 (match-beginning 0))) format-strings)))
(let ((start (point))
(expr nil))
(while (not
(setq
expr
(progn
(or (ignore-errors (search-forward "}"))
(user-error
"invalid interpolation expression, found in %S"
(buffer-substring-no-properties (- start 2) (point-max))))
(backward-char 1)
(ignore-errors
(read (concat "'(" (buffer-substring-no-properties start (point)) ")"))))))
(forward-char 1))
(push "%s" format-strings)
(push (macroexpand-all (caadr expr)) format-arguments)
(setq position (+ 1 (point)))))
;(unless (= (+ 1 position (point-max)))
(push (buffer-substring-no-properties position (point-max)) format-strings))
;)
`(format ,(apply 'concat (reverse format-strings)) ,@(reverse format-arguments))))
(defmacro $fmt (string)
(unless (stringp string)
(user-error "$fmt requires an explicit string, but a %s was given" (type-of string)))
($fmt-expand string))
(progn
(cl-assert (equal
"Hello World"
(let ((w "World")) ($fmt "Hello ${w}"))))
(cl-assert (equal
"Hello World!"
(let ((w "World")) ($fmt "Hello ${(concat w \"!\")}"))))
(cl-assert (equal
"Hello to the World"
(let ((w "World")) ($fmt "Hello ${($fmt \"to the ${w}\")}")))))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment