aboutsummaryrefslogtreecommitdiff
path: root/compat-macs.el
blob: ca5c800214b25e859a0c2d193d93e0ee1ee82793 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
;;; compat-macs.el --- Compatibility Macros -*- lexical-binding: t; no-byte-compile: t; -*-

;; Copyright (C) 2021-2023 Free Software Foundation, Inc.

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; These macros are used to define compatibility functions and macros.

;;; Code:

;; We always require subr-x at compile time for the fboundp check
;; since definitions have been moved around. The cl-lib macros are
;; needed by compatibility definitions.
(require 'subr-x)
(require 'cl-lib)

(defvar compat--version nil
  "Version of the currently defined compatibility definitions.")

(defmacro compat-declare-version (version)
  "Set the Emacs version that is currently being handled to VERSION."
  (setq compat--version version)
  (let ((before (1- (car (version-to-list version)))))
    (when (and (< 24 before) (< emacs-major-version before))
      `(require ',(intern (format "compat-%d" before))))))

(defun compat--format-docstring (type name docstring)
  "Format DOCSTRING for NAME of TYPE.
Prepend compatibility notice to the actual documentation string."
  (with-temp-buffer
    (insert
     (format
      "[Compatibility %s for `%S', defined in Emacs %s.  \
If this is not documented on yourself system, you can check \
`(compat) Emacs %s' for more details.]\n\n%s"
      type name
      compat--version compat--version
      docstring))
    (let ((fill-column 80))
      (fill-region (point-min) (point-max)))
    (buffer-string)))

(defun compat--check-attributes (attrs preds)
  "Check ATTRS given PREDS predicate plist and return rest."
  (while (keywordp (car attrs))
    (unless (cdr attrs)
      (error "Odd number of element in attribute list"))
    (let ((pred (plist-get preds (car attrs))))
      (unless (and pred (or (eq pred t) (funcall pred (cadr attrs))))
        (error "Invalid attribute %s" (car attrs))))
    (setq attrs (cddr attrs)))
  attrs)

(defun compat--guard (attrs preds fun)
  "Guard compatibility definition generation.
The version constraints specified by ATTRS are checked.  PREDS is
a plist of predicates for arguments which are passed to FUN."
  (declare (indent 2))
  (let* ((body (compat--check-attributes
                attrs `(,@preds :when t :feature symbolp)))
         (feature (plist-get attrs :feature))
         (attrs `(:body ,body ,@attrs))
         (when (plist-get attrs :when))
         args)
    ;; Require feature at compile time
    (when feature
      (when (eq feature 'subr-x)
        (error "Feature subr-x must not be specified"))
      (require feature))
    (when (if when
              ;; If a condition is specified, no version check is performed.
              (eval when t)
            ;; The current Emacs must be older than the current declared Compat
            ;; version, see `compat-declare-version'.
            (version< emacs-version compat--version))
      (while preds
        (push (plist-get attrs (car preds)) args)
        (setq preds (cddr preds)))
      (setq body (apply fun (nreverse args)))
      (when body
        (if feature
            `(with-eval-after-load ',feature ,@body)
          (macroexp-progn body))))))

(defun compat--guard-defun (type name arglist docstring rest)
  "Define function NAME of TYPE with ARGLIST and DOCSTRING.
REST are attributes and the function BODY."
  (compat--guard rest `(:explicit booleanp
                        :obsolete ,(lambda (x) (or (booleanp x) (stringp x)))
                        :body t)
    (lambda (explicit obsolete body)
      ;; Remove unsupported declares.  It might be possible to set these
      ;; properties otherwise.  That should be looked into and implemented
      ;; if it is the case.
      (when (and (listp (car-safe body)) (eq (caar body) 'declare))
        (when (<= emacs-major-version 25)
          (delq (assq 'side-effect-free (car body)) (car body))
          (delq (assq 'pure (car body)) (car body))))
      ;; Use `:explicit' name if the function is already defined.
      (let* ((defname (if (and explicit (fboundp name))
                          (intern (format "compat--%s" name))
                        name))
             (def `(,(if (memq '&key arglist)
                         (if (eq type 'macro) 'cl-defmacro 'cl-defun)
                       (if (eq type 'macro) 'defmacro 'defun))
                    ,defname ,arglist
                    ,(compat--format-docstring type name docstring)
                    ,@body)))
        `(,@(if (eq defname name)
                ;; An additional fboundp check is performed at runtime to make
                ;; sure that we never redefine an existing definition if Compat
                ;; is loaded on a newer Emacs version.  Declare the function,
                ;; such that the byte compiler does not complain about possibly
                ;; missing functions at runtime. The warnings are generated due
                ;; to the fboundp check.
                `((declare-function ,name nil)
                  (unless (fboundp ',name) ,def))
              (list def))
          ,@(when obsolete
              `((make-obsolete
                 ',name ,(if (stringp obsolete) obsolete "No substitute")
                 ,compat--version))))))))

(defmacro compat-guard (cond &rest rest)
  "Guard definition with a runtime COND and a version check.
The runtime condition must make sure that no definition is
overriden.  REST is an attribute plist followed by the definition
body.  The attributes specify the conditions under which the
definition is generated.

- :feature :: Wrap the definition with `with-eval-after-load' for
  the given feature.

- :when :: Install the definition if :when evaluates to non-nil.
  The usual version check is not performed.  If you still want to
  check against the current Emacs version, the comparison must be
  part of the :when expression."
  (declare (debug ([&rest keywordp sexp] def-body))
           (indent 1))
  (compat--guard rest '(:body t)
    (lambda (body)
      (if (eq cond t)
          body
        `((when ,cond ,@body))))))

(defmacro compat-defalias (name def &rest attrs)
  "Define compatibility alias NAME as DEF.
ATTRS is a plist of attributes, which specify the conditions
under which the definition is generated.

- :obsolete :: Mark the alias as obsolete if t.

- :feature and :when :: See `compat-guard'."
  (declare (debug (name symbolp [&rest keywordp sexp])))
  (compat--guard attrs '(:obsolete booleanp)
    (lambda (obsolete)
      ;; The fboundp check is performed at runtime to make sure that we never
      ;; redefine an existing definition if Compat is loaded on a newer Emacs
      ;; version.
      `((unless (fboundp ',name)
          (defalias ',name ',def
            ,(compat--format-docstring 'function name
                                       (get name 'function-documentation)))
          ,@(when obsolete
              `((make-obsolete ',name ',def ,compat--version))))))))

(defmacro compat-defun (name arglist docstring &rest rest)
  "Define compatibility function NAME with arguments ARGLIST.
The function must be documented in DOCSTRING.  REST is an
attribute plist followed by the function body.  The attributes
specify the conditions under which the definition is generated.

- :explicit :: Make the definition available for explicit
  invocation via `compat-call'.  :explicit should be used for
  functions which extend already existing functions, e.g.,
  functions which changed their calling convention or their
  behavior.

- :obsolete :: Mark the function as obsolete if t, can be a
  string describing the obsoletion.

- :feature and :when :: See `compat-guard'."
  (declare (debug (&define name (&rest symbolp)
                           stringp
                           [&rest keywordp sexp]
                           def-body))
           (doc-string 3) (indent 2))
  (compat--guard-defun 'function name arglist docstring rest))

(defmacro compat-defmacro (name arglist docstring &rest rest)
  "Define compatibility macro NAME with arguments ARGLIST.
The macro must be documented in DOCSTRING.  REST is an attribute
plist followed by the macro body.  See `compat-defun' for
details."
  (declare (debug compat-defun) (doc-string 3) (indent 2))
  (compat--guard-defun 'macro name arglist docstring rest))

(defmacro compat-defvar (name initval docstring &rest attrs)
  "Define compatibility variable NAME with initial value INITVAL.
The variable must be documented in DOCSTRING.  ATTRS is a plist
of attributes, which specify the conditions under which the
definition is generated.

- :constant :: Mark the variable as constant if t.

- :local :: Make the variable buffer-local if t.  If the value is
  `permanent' make the variable additionally permanently local.

- :obsolete :: Mark the variable as obsolete if t, can be a
  string describing the obsoletion.

- :feature and :when :: See `compat-guard'."
  (declare (debug (name form stringp [&rest keywordp sexp]))
           (doc-string 3) (indent 2))
  (compat--guard attrs `(:constant booleanp
                         :local ,(lambda (x) (memq x '(nil t permanent)))
                         :obsolete ,(lambda (x) (or (booleanp x) (stringp x))))
    (lambda (constant local obsolete)
      (when (and constant local)
        (error ":constant and :local cannot be specified together"))
      ;; The boundp check is performed at runtime to make sure that we never
      ;; redefine an existing definition if Compat is loaded on a newer Emacs
      ;; version.
      `((unless (boundp ',name)
          (,(if constant 'defconst 'defvar)
           ,name ,initval
           ,(compat--format-docstring 'variable name docstring))
          ,@(when obsolete
              `((make-obsolete-variable
                 ',name ,(if (stringp obsolete) obsolete "No substitute")
                 ,compat--version))))
        ,@(and local `((make-variable-buffer-local ',name)))
        ,@(and (eq local 'permanent) `((put ',name 'permanent-local t)))))))

(provide 'compat-macs)
;;; compat-macs.el ends here