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".

T. Verron
  • 4,233
  • 1
  • 22
  • 55
  • 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](https://github.com/magnars/expand-region.el/commit/69819ac1417b8fad6561f8072d76d0fa2fcebfc0) 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 `quote`d 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

2 Answers2

4

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.

Malabarba
  • 22,878
  • 6
  • 78
  • 163
  • 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.

Stefan
  • 26,154
  • 3
  • 46
  • 84
  • `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