1

I'm writing a macro which calls a helper function. I need to return a list like this:

`(s -- (cons 'commentary s))

I have tried many combinations, but I can't seem to get what I need without an extra comma:

(let ((rule-name 'commentary))
  (car `(`(s -- (cons ',,rule-name s)))))
;; => `(s -- (cons ',commentary s))

(let ((rule-name 'commentary))
  (car `(`(s -- (cons ,',rule-name s)))))
;; => `(s -- (cons ,'commentary s))

I don't understand why the second comma is not being consumed.

The only possibly related discussion I have found is here, but I'm not sure it's related, because it's about splicing, which I'm not doing here.

What I'm actually trying to do is rewrite PEGs for peg-parse in peg.el to automatically capture rules that use the substring action. To do this, I need to add this to the end of each rule that contains substring:

`(s -- (cons 'rule-name s))

For example, I'm trying to write a PEG to parse Emacs packages. Here's the call to my macro with the PEG:

(peg-auto-capture-substring
 (package package-header-line headers commentary-heading commentary code-heading rest)
 (package-header-line  heading-1 package-name triple-hyphen package-desc (opt file-local-variables) newline)
 (headers (substring (+ (not commentary-heading) (any))))
 (commentary-heading heading-1 "Commentary:" newline)
 (commentary (substring (+ (not code-heading) (any))))
 (code-heading heading-1 "Code:" newline)
 (rest (substring (+ (any))))

 (heading-1 (bol) ";;;" space)
 (package-name (substring (+ (not space) (any))))
 (triple-hyphen space "---" space)
 (package-desc (substring (+ (not file-local-variables "\n") (any))))
 (file-local-variables "-*-" space (substring (+ (not "-*-") (any))) "-*-")
 (space (+ (not "\n") (or " " "\t")))
 (newline (opt space) (+ "\n")))

Here's the macro:

(cl-defmacro peg-auto-capture-substring (&rest body)
  `(peg-parse ,@(apply #'peg-auto-capture-substring-1 body)))

And here's the helper function:

(defun peg-auto-capture-substring-1 (&rest grammar)
  ;; This should work but it leaves an extra comma!
  (cl-labels ((has-substring
               (list)
               (cl-loop for elem in list
                        if (and (atom elem)
                                (eq elem 'substring))
                        return t
                        else if (and (listp elem)
                                     (has-substring elem))
                        return t)))
    (cl-loop for rule in grammar
             for rule-name = (car rule)
             for action = `(`(s -- (cons ,',rule-name s)))
             when (has-substring rule)
             do (setq rule (append rule action))
             collect rule)))

And here's the expansion of my macro:

(peg-parse
 (package package-header-line headers commentary-heading commentary code-heading rest)
 (package-header-line heading-1 package-name triple-hyphen package-desc (opt file-local-variables) newline)
 (headers (substring (+ (not commentary-heading) (any)))
          `(s -- (cons ,'headers s)))
 (commentary-heading heading-1 "Commentary:" newline)
 (commentary (substring (+ (not code-heading) (any)))
             `(s -- (cons ,'commentary s)))
 (code-heading heading-1 "Code:" newline)
 (rest (substring (+ (any)))
       `(s -- (cons ,'rest s)))
 (heading-1 (bol) ";;;" space)
 (package-name (substring (+ (not space) (any)))
               `(s -- (cons ,'package-name s)))
 (triple-hyphen space "---" space)
 (package-desc (substring (+ (not file-local-variables "\n") (any)))
               `(s -- (cons ,'package-desc s)))
 (file-local-variables "-*-" space (substring (+ (not "-*-") (any))) "-*-"
                       `(s -- (cons ,'file-local-variables s)))
 (space (+ (not "\n") (or " " " ")))
 (newline (opt space) (+ "\n")))

As you can see, the only problem is the unconsumed comma in each action expression.

Any help would be appreciated!

Workaround found

I discovered that there is an undocumented, alternative syntax for "stack actions" in peg.el, which allows me to do this instead of a backquoted list:

(stack-action (s -- (cons 'commentary s)))

I suppose it's like peg.el's equivalent of (backquote). So I'm able to use this helper function for my macro:

(defun peg-auto-capture-substring-1 (&rest grammar)
  `,@(cl-labels ((has-substring (list) (cl-loop for elem in list
                                                if (and (atom elem)
                                                        (eq elem 'substring))
                                                return t
                                                else if (and (listp elem)
                                                             (has-substring elem))
                                                return t)))
       (cl-loop for rule in grammar
                for rule-name = (car rule)
                for action = (list (list 'stack-action `(s -- (cons ',rule-name s))))
                when (has-substring rule)
                do (setq rule (append rule action))
                collect rule)))

Which expands to:

(peg-parse
 (package package-header-line headers commentary-heading commentary code-heading rest)
 (package-header-line heading-1 package-name triple-hyphen package-desc (opt file-local-variables) newline)
 (headers (substring (+ (not commentary-heading) (any)))
          (stack-action (s -- (cons 'headers s))))
 (commentary-heading heading-1 "Commentary:" newline)
 (commentary (substring (+ (not code-heading) (any)))
             (stack-action (s -- (cons 'commentary s))))
 (code-heading heading-1 "Code:" newline)
 (rest (substring (+ (any)))
       (stack-action (s -- (cons 'rest s))))
 (heading-1 (bol) ";;;" space)
 (package-name (substring (+ (not space) (any)))
               (stack-action (s -- (cons 'package-name s))))
 (triple-hyphen space "---" space)
 (package-desc (substring (+ (not file-local-variables "\n") (any)))
               (stack-action (s -- (cons 'package-desc s))))
 (file-local-variables "-*-" space (substring (+ (not "-*-") (any))) "-*-"
                       (stack-action (s -- (cons 'file-local-variables s))))
 (space (+ (not "\n") (or " " " ")))
 (newline (opt space) (+ "\n")))

And this works, so now it parses an Emacs package file into, e.g.:

((package-name . "peg.el")
 (package-desc . "Parsing Expression Grammars in Emacs Lisp  ")
 (file-local-variables . "lexical-binding: t; ")
 ...)

I'd still like to learn more about how backquotes work. I using escaped backquotes as well, like:

(let ((rule-name 'commentary))
  (list '\` `(s -- (cons ',rule-name s))))

Which almost worked, except that expanded to an escaped backquote rather than an actual backquote.

I'd also like to know why the double-commas were not completely consumed by the double-backquote, because that seemed to be the real problem I was facing. Is that a bug in Emacs's backquoting, or do I just not understand how it's working internally?

Thanks for your help.

  • 1
    Suggestion: Try using just list and cons to create the code you are trying to create using backquotes and commas. That will give you a hint of what you need in terms of backquote syntax. IOW, to figure out what's wrong with your syntactic sugar, try coding without it and then working toward using it, bit by bit. (BTW, if what you want to end up with is the list (cons 'foo s) then it is perfectly OK to get there via `(cons ,'foo s).) – Drew Jan 5 '18 at 2:27
  • 1
    @blujay: (let ((rule-name 'commentary)) (list 'backquote `(s -- (cons ',rule-name s)))) produces the result you've stated that you want (without nesting backquotes), but I'm confused as to why you want the outer backquote at all, if you are not evaluating anything in that form? – phils Jan 5 '18 at 5:21
  • @Drew I tried that at one point, but I'll give it another try and report back. Thanks. @phils The peg-parse macro requires action forms to be backquoted. See github.com/ellerh/peg.el/blob/master/peg.el#L69 – blujay Jan 5 '18 at 6:51
  • @phils Thanks for your suggestion, but it produces the symbol backquote instead of an actual (?) backquote, which PEG doesn't accept--it gives the error (error "Reference to undefined PEG rule: backquote"). – blujay Jan 5 '18 at 6:58
  • @blujay, I see. I know nothing about PEG, but is it any happier with this? (let ((rule-name 'commentary)) (list '\` `(s -- (cons ',rule-name s)))) (edit: Ah, I see from your edit that you've already tried that in the interim.) – phils Jan 5 '18 at 9:57
2

You can use a few different approaches, but for your specific case maybe my favorite would be:

(let ((rule-name 'commentary))
  `(,'\` (s -- (cons ',rule-name s))))
  • Thanks, but that also results in an escaped backslash in the macro expansion, e.g. (headers (substring (+ (not commentary-heading) (any))) \`(s -- (cons 'headers s))). – blujay Jan 16 '18 at 14:41
  • Hmm... maybe there's some odd interaction with peg.el, but I can't figure out how to get the output you show (even reading the C code of the Emacs printer, I can't see how you could ever get \`( in the output (my reading makes me think there's either no backslash or a space before the open paren). – Stefan Jan 16 '18 at 15:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.