dotemacs

My Emacs configuration
git clone git://git.entf.net/dotemacs
Log | Files | Refs | LICENSE

compat-macs.el (11684B)


      1 ;;; compat-macs.el --- Compatibility Macros -*- lexical-binding: t; no-byte-compile: t; -*-
      2 
      3 ;; Copyright (C) 2021-2023 Free Software Foundation, Inc.
      4 
      5 ;; This program is free software; you can redistribute it and/or modify
      6 ;; it under the terms of the GNU General Public License as published by
      7 ;; the Free Software Foundation, either version 3 of the License, or
      8 ;; (at your option) any later version.
      9 
     10 ;; This program is distributed in the hope that it will be useful,
     11 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
     12 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13 ;; GNU General Public License for more details.
     14 
     15 ;; You should have received a copy of the GNU General Public License
     16 ;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
     17 
     18 ;;; Commentary:
     19 
     20 ;; This file provides *internal* macros, which are used by Compat to
     21 ;; facilitate the definition of compatibility functions, macros and
     22 ;; variables.  The `compat-macs' feature should never be loaded at
     23 ;; runtime in your Emacs and will only be used during byte
     24 ;; compilation.  Every definition provided here should be considered
     25 ;; internal and may change any time between Compat releases.
     26 
     27 ;;; Code:
     28 
     29 ;; We always require subr-x at compile time for the fboundp check
     30 ;; since definitions have been moved around. The cl-lib macros are
     31 ;; needed by compatibility definitions.
     32 (require 'subr-x)
     33 (require 'cl-lib)
     34 
     35 (defvar compat-macs--version nil
     36   "Version of the currently defined compatibility definitions.")
     37 
     38 (defun compat-macs--strict (cond &rest error)
     39   "Assert strict COND, otherwise fail with ERROR."
     40   (when (bound-and-true-p compat-strict)
     41     (apply #'compat-macs--assert cond error)))
     42 
     43 (defun compat-macs--assert (cond &rest error)
     44   "Assert COND, otherwise fail with ERROR."
     45   (unless cond (apply #'error error)))
     46 
     47 (defun compat-macs--docstring (type name docstring)
     48   "Format DOCSTRING for NAME of TYPE.
     49 Prepend compatibility notice to the actual documentation string."
     50   (with-temp-buffer
     51     (insert
     52      (format
     53       "[Compatibility %s for `%s', defined in Emacs %s. \
     54 See (compat) Emacs %s' for more details.]\n\n%s"
     55       type name compat-macs--version compat-macs--version docstring))
     56     (let ((fill-column 80))
     57       (fill-region (point-min) (point-max)))
     58     (buffer-string)))
     59 
     60 (defun compat-macs--check-attributes (attrs preds)
     61   "Check ATTRS given PREDS predicate plist and return rest."
     62   (while (keywordp (car attrs))
     63     (compat-macs--assert (cdr attrs) "Attribute list length is odd")
     64     (compat-macs--assert (let ((p (plist-get preds (car attrs))))
     65                            (and p (or (eq p t) (funcall p (cadr attrs)))))
     66                          "Invalid attribute %s" (car attrs))
     67     (setq attrs (cddr attrs)))
     68   attrs)
     69 
     70 (defun compat-macs--guard (attrs preds fun)
     71   "Guard compatibility definition generation.
     72 The version constraints specified by ATTRS are checked.  PREDS is
     73 a plist of predicates for arguments which are passed to FUN."
     74   (declare (indent 2))
     75   (compat-macs--assert compat-macs--version "No `compat-version' was declared")
     76   (let* ((body (compat-macs--check-attributes
     77                 attrs `(,@preds :feature symbolp)))
     78          (feature (plist-get attrs :feature))
     79          (attrs `(:body ,body ,@attrs))
     80          args)
     81     ;; Require feature at compile time
     82     (when feature
     83       (compat-macs--assert (not (eq feature 'subr-x)) "Invalid feature subr-x")
     84       (require feature))
     85     ;; The current Emacs must be older than the currently declared version.
     86     (when (version< emacs-version compat-macs--version)
     87       (while preds
     88         (push (plist-get attrs (car preds)) args)
     89         (setq preds (cddr preds)))
     90       (setq body (apply fun (nreverse args)))
     91       (if (and feature body)
     92           `(with-eval-after-load ',feature ,@body)
     93         (macroexp-progn body)))))
     94 
     95 (defun compat-macs--defun (type name arglist docstring rest)
     96   "Define function NAME of TYPE with ARGLIST and DOCSTRING.
     97 REST are attributes and the function BODY."
     98   (compat-macs--guard
     99      rest (list :extended (lambda (x) (or (booleanp x) (version-to-list x)))
    100                 :obsolete (lambda (x) (or (booleanp x) (stringp x)))
    101                 :body t)
    102     (lambda (extended obsolete body)
    103       (when (stringp extended)
    104         (compat-macs--assert
    105          (and (version< extended compat-macs--version) (version< "24.4" extended))
    106          "Invalid :extended version %s for %s %s" extended type name)
    107         (setq extended (version<= extended emacs-version)))
    108       (compat-macs--strict (eq extended (fboundp name))
    109                            "Wrong :extended flag for %s %s" type name)
    110       ;; Remove unsupported declares.  It might be possible to set these
    111       ;; properties otherwise.  That should be looked into and implemented
    112       ;; if it is the case.
    113       (when (and (listp (car-safe body)) (eq (caar body) 'declare))
    114         (when (<= emacs-major-version 25)
    115           (delq (assq 'side-effect-free (car body)) (car body))
    116           (delq (assq 'pure (car body)) (car body))))
    117       ;; Use `:extended' name if the function is already defined.
    118       (let* ((defname (if (and extended (fboundp name))
    119                           (intern (format "compat--%s" name))
    120                         name))
    121              (def `(,(if (memq '&key arglist)
    122                          (if (eq type 'macro) 'cl-defmacro 'cl-defun)
    123                        (if (eq type 'macro) 'defmacro 'defun))
    124                     ,defname ,arglist
    125                     ,(compat-macs--docstring type name docstring)
    126                     ,@body)))
    127         `(,@(if (eq defname name)
    128                 ;; An additional fboundp check is performed at runtime to make
    129                 ;; sure that we never redefine an existing definition if Compat
    130                 ;; is loaded on a newer Emacs version.  Declare the function,
    131                 ;; such that the byte compiler does not complain about possibly
    132                 ;; missing functions at runtime. The warnings are generated due
    133                 ;; to the fboundp check.
    134                 `((declare-function ,name nil)
    135                   (unless (fboundp ',name) ,def))
    136               (list def))
    137           ,@(when obsolete
    138               `((make-obsolete
    139                  ',defname ,(if (stringp obsolete) obsolete "No substitute")
    140                  ,compat-macs--version))))))))
    141 
    142 (defmacro compat-guard (cond &rest rest)
    143   "Guard definition with a runtime COND and a version check.
    144 The runtime condition must make sure that no definition is
    145 overriden.  REST is an attribute plist followed by the definition
    146 body.  The attributes specify the conditions under which the
    147 definition is generated.
    148 
    149 - :feature :: Wrap the definition with `with-eval-after-load' for
    150   the given feature."
    151   (declare (debug ([&rest keywordp sexp] def-body))
    152            (indent 1))
    153   (compat-macs--guard rest '(:body t)
    154     (lambda (body)
    155       (compat-macs--assert body "The guarded body is empty")
    156       (if (eq cond t)
    157           body
    158         (compat-macs--strict (eval cond t) "Guard %S failed" cond)
    159         `((when ,cond ,@body))))))
    160 
    161 (defmacro compat-defalias (name def &rest attrs)
    162   "Define compatibility alias NAME as DEF.
    163 ATTRS is a plist of attributes, which specify the conditions
    164 under which the definition is generated.
    165 
    166 - :obsolete :: Mark the alias as obsolete if t.
    167 
    168 - :feature :: See `compat-guard'."
    169   (declare (debug (name symbolp [&rest keywordp sexp])))
    170   (compat-macs--guard attrs '(:obsolete booleanp)
    171     (lambda (obsolete)
    172       (compat-macs--strict (not (fboundp name)) "%s already defined" name)
    173       ;; The fboundp check is performed at runtime to make sure that we never
    174       ;; redefine an existing definition if Compat is loaded on a newer Emacs
    175       ;; version.
    176       `((unless (fboundp ',name)
    177           (defalias ',name ',def
    178             ,(compat-macs--docstring 'function name
    179                                 (get name 'function-documentation)))
    180           ,@(when obsolete
    181               `((make-obsolete ',name ',def ,compat-macs--version))))))))
    182 
    183 (defmacro compat-defun (name arglist docstring &rest rest)
    184   "Define compatibility function NAME with arguments ARGLIST.
    185 The function must be documented in DOCSTRING.  REST is an
    186 attribute plist followed by the function body.  The attributes
    187 specify the conditions under which the definition is generated.
    188 
    189 - :extended :: Mark the function as extended if t.  The function
    190   must be called explicitly via `compat-call'.  This attribute
    191   should be used for functions which extend already existing
    192   functions, e.g., functions which changed their calling
    193   convention or their behavior.  The value can also be a version
    194   string, which specifies the Emacs version when the original
    195   version of the function was introduced.
    196 
    197 - :obsolete :: Mark the function as obsolete if t, can be a
    198   string describing the obsoletion.
    199 
    200 - :feature :: See `compat-guard'."
    201   (declare (debug (&define name (&rest symbolp)
    202                            stringp
    203                            [&rest keywordp sexp]
    204                            def-body))
    205            (doc-string 3) (indent 2))
    206   (compat-macs--defun 'function name arglist docstring rest))
    207 
    208 (defmacro compat-defmacro (name arglist docstring &rest rest)
    209   "Define compatibility macro NAME with arguments ARGLIST.
    210 The macro must be documented in DOCSTRING.  REST is an attribute
    211 plist followed by the macro body.  See `compat-defun' for
    212 details."
    213   (declare (debug compat-defun) (doc-string 3) (indent 2))
    214   (compat-macs--defun 'macro name arglist docstring rest))
    215 
    216 (defmacro compat-defvar (name initval docstring &rest attrs)
    217   "Define compatibility variable NAME with initial value INITVAL.
    218 The variable must be documented in DOCSTRING.  ATTRS is a plist
    219 of attributes, which specify the conditions under which the
    220 definition is generated.
    221 
    222 - :constant :: Mark the variable as constant if t.
    223 
    224 - :local :: Make the variable buffer-local if t.  If the value is
    225   `permanent' make the variable additionally permanently local.
    226 
    227 - :obsolete :: Mark the variable as obsolete if t, can be a
    228   string describing the obsoletion.
    229 
    230 - :feature :: See `compat-guard'."
    231   (declare (debug (name form stringp [&rest keywordp sexp]))
    232            (doc-string 3) (indent 2))
    233   (compat-macs--guard
    234       attrs (list :constant #'booleanp
    235                   :local (lambda (x) (memq x '(nil t permanent)))
    236                   :obsolete (lambda (x) (or (booleanp x) (stringp x))))
    237     (lambda (constant local obsolete)
    238       (compat-macs--strict (not (boundp name)) "%s already defined" name)
    239       (compat-macs--assert (not (and constant local)) "Both :constant and :local")
    240       ;; The boundp check is performed at runtime to make sure that we never
    241       ;; redefine an existing definition if Compat is loaded on a newer Emacs
    242       ;; version.
    243       `((unless (boundp ',name)
    244           (,(if constant 'defconst 'defvar)
    245            ,name ,initval
    246            ,(compat-macs--docstring 'variable name docstring))
    247           ,@(when obsolete
    248               `((make-obsolete-variable
    249                  ',name ,(if (stringp obsolete) obsolete "No substitute")
    250                  ,compat-macs--version))))
    251         ,@(and local `((make-variable-buffer-local ',name)))
    252         ,@(and (eq local 'permanent) `((put ',name 'permanent-local t)))))))
    253 
    254 (defmacro compat-version (version)
    255   "Set the Emacs version that is currently being handled to VERSION."
    256   (setq compat-macs--version version)
    257   nil)
    258 
    259 (defmacro compat-require (feature version)
    260   "Require FEATURE if the Emacs version is less than VERSION."
    261   (when (version< emacs-version version)
    262     (require feature)
    263     `(require ',feature)))
    264 
    265 (provide 'compat-macs)
    266 ;;; compat-macs.el ends here