Last active
February 27, 2024 16:21
-
-
Save jordonbiondo/592618ee665b18205676949f0b3766fa to your computer and use it in GitHub Desktop.
Emacs string interpolation macro example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;; 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