6

I am trying to include support for smartparens in a major-mode package. I want that if a user is using smartparens, then he will get additional pair definitions, and if he isn't, he will not notice anything different.

For that, I am using instructions of the form:

(declare-function sp-with-modes "ext:smartparens.el" t t)
(declare-function sp-local-pair "ext:smartparens.el" t t)

(eval-after-load 'smartparens
  '(sp-with-modes '(test-mode)
     (sp-local-pair "<" ">"
                    :actions '(insert wrap autoskip))))

This works fine if the file is not byte-compiled, or if it is byte-compiled by an emacs with smartparens loaded, or with emacs 24.3. However, with 24.5 (and probably with 24.4, though I cannot easily test it at the moment), and if it is compiled in a vanilla emacs (for example with cask build), it gives an error, stating that the argument (insert wrap autoskip) is not one of the keywords admissible by sp-local-pair (:actions...).

If I remove the call to sp-with-modes, and I replace the definition with

(sp-local-pair 'test-mode "<" ">"
               :actions '(insert wrap autoskip))

then it works fine. The macro sp-with-modes is supposed to loop over its argument (a list), inserting each element as first argument to everything in the body, I suspect that it confuses the byte-compiler at some point.

Is there a way to include this kind of "maybe evaluation" in a byte-compiled file, without confusing the byte-compiler? For example, is it possible to tell the compiler "do not compile this form"?

Or what would be the idiomatic way of achieving this kind of third-party support in a package?

Please note that the question involves smartparens because that's where I noticed the problem, but I suspect that it may happen with a lot of other packages.


The rest of the question is a MWE. I assumes that your regular emacs configuration has smartparens installed.

File ~/.emacs.d/tests/test.el

(declare-function sp-with-modes "ext:smartparens.el" t t)
(declare-function sp-local-pair "ext:smartparens.el" t t)

(eval-after-load 'smartparens
  '(sp-with-modes '(test-mode)
     (sp-local-pair "<" ">"
                    :actions '(insert wrap autoskip))))

(define-derived-mode test-mode
  fundamental-mode "Test" "Test mode")

(provide 'test)

File ~/.emacs.d/tests/init.el:

(require 'package)
(setq package-enable-at-startup nil)
(add-to-list 'package-archives '("melpa" . "http://melpa.org/packages/"))

(package-initialize)

(add-to-list 'load-path "~/.emacs.d/tests/")
(require 'test)

(require 'smartparens)
(smartparens-global-mode t)

Run emacs -Q and evaluate

(byte-compile-file "~/.emacs.d/tests/test.el")

then run emacs -Q --load "~/.emacs.d/tests/init.el".

  • This sounds like a bug to me. Or at least as a behavior that wasn't intended / foreseen by the people who wrote byte compilation. I'd file a report / feature request. – wvxvw Jul 11 '15 at 10:19
  • I don't know much about byte compilation but I am aware of this snippet suggested by @lunaryorn in expand-region package when adding a function save-mark-and-excursion that was introduced in emacs 25.0: Does a similar approach of using eval-when-compile and fboundp work here? – Kaushal Modi Jul 11 '15 at 12:03
  • 1
    eval-after-load has a compiler-macro which expands quoted forms into a lambda-form, which is then compiled (This could be viewed as a bug.). Since the compiler does not know, that sp-with-modes is a macro, it treats it as function, I believe. When the code gets evaluated, it'll be to late for any macro exansion and sp-local-pair is eval'd as the first argument to the other function... – politza Jul 11 '15 at 14:36
3

Is there a way to include this kind of "maybe evaluation" in a byte-compiled file, without confusing the byte-compiler? For example, is it possible to tell the compiler "do not compile this form"?

Yes, you can quote it and then eval it: (eval '(sp-whatever ...)). This will not be seen by the compiler and will only be expanded at runtime.

Or what would be the idiomatic way of achieving this kind of third-party support in a package?

Emacs has no dedicated support for optional dependencies ATM. If this would be useful to you, please file a bug. This sort of thing only gets implemented after people ask for it.

  • The eval solution works fine, and sounds simple enough for most cases. I doubt it will have any significant runtime cost for my case at least, not worth spending developer time. Does this behavior of eval risk being changed/fixed in the future? – T. Verron Jul 11 '15 at 14:55
4

To add to what @Malabarba said, I think the problem here is that smartparens should provide an API that does not rely on a macro.

The "normal" way this works with usual packages is that the major mode can just do something like:

(when (boundp 'smartparens-thingies)
  (push mythingies smartparens-thingies))

or even

(setq smartparens-extra-thingies mythingies)

And I think it's likely [beware: I know next to nothing about smartparens] that rather then using eval-after-load and sp-with-modes you can just call sp-local-pair from the major mode function:

(define-derived-mode test-mode parent-mode "doc"
  ...
  (when (fboundp 'sp-local-pair)
    (sp-local-pair "<" ">" :actions '(insert wrap autoskip)))
  ...)

which will solve your problem since it doesn't use a macro that's only available when smartparens is installed.

  • sp-with-modes is a helper for settings which are common to several modes. The api itself does not "rely" on a macro, only on the function sp-local-pair: I could well use (sp-local-pair 'test-mode...) for all the pairs, but I do want to define several pairs, and apply them to 3 different modes. But indeed, if it would use a variable smartparens-extra-thingies, 3rd-party packages could be made truly independent of the presence of smartparens. – T. Verron Jul 11 '15 at 14:52
  • So does my "likely" solution work? If it does, you can easily share it between various major modes by moving that code to a separate function you call from those 3 major modes. – Stefan Jul 11 '15 at 14:58
  • There are a few problems with that solution, sadly: for example, if smartparens is loaded after the mode, the pairs will not be seen. And "local" here means "mode-local", not "buffer-local", and loading several files will populate the list of pairs with duplicated pairs. I am not sure if smartparens tries to detect duplicates, but if it doesn't, it could slow-down each call of self-insert-command (but I am not sure of the magnitude of that possible slowdown either). Many conditionals... – T. Verron Jul 11 '15 at 15:01
  • But I think you have a point though: sp-with-modes does not really need to be a macro, it is essentially just a "map" combined with a partial application of a function. What you suggest when you say that I could move that code to a separate function is essentially to write a function which given a mode name defines pairs for that mode name... If @politza's explanation of the problem is accurate, this function should behave more nicely with the eval-after-load compiler macro, shouldn't it? – T. Verron Jul 11 '15 at 15:09

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.