317 lines
13 KiB
EmacsLisp
317 lines
13 KiB
EmacsLisp
;;; compat-macs.el --- Compatibility Macros -*- lexical-binding: t; no-byte-compile: t; -*-
|
|
|
|
;; Copyright (C) 2021, 2022 Free Software Foundation, Inc.
|
|
|
|
;; Author: Philip Kaludercic <philipk@posteo.net>
|
|
;; Keywords: lisp
|
|
|
|
;; 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, macros and
|
|
;; advice.
|
|
|
|
;;; Code:
|
|
|
|
(defmacro compat--ignore (&rest _)
|
|
"Ignore all arguments."
|
|
nil)
|
|
|
|
(defvar compat--inhibit-prefixed nil
|
|
"Non-nil means that prefixed definitions are not loaded.
|
|
A prefixed function is something like `compat-assoc', that is
|
|
only made visible when the respective compatibility version file
|
|
is loaded (in this case `compat-26').")
|
|
|
|
(defmacro compat--inhibit-prefixed (&rest body)
|
|
"Ignore BODY unless `compat--inhibit-prefixed' is true."
|
|
`(unless (bound-and-true-p compat--inhibit-prefixed)
|
|
,@body))
|
|
|
|
(defvar compat-current-version nil
|
|
"Default version to use when no explicit version was given.")
|
|
|
|
(defmacro compat-declare-version (version)
|
|
"Set the Emacs version that is currently being handled to VERSION."
|
|
;; FIXME: Avoid setting the version for any definition that might
|
|
;; follow, but try to restrict it to the current file/buffer.
|
|
(setq compat-current-version version)
|
|
nil)
|
|
|
|
(defvar compat--generate-function #'compat--generate-default
|
|
"Function used to generate compatibility code.
|
|
The function must take six arguments: NAME, DEF-FN, INSTALL-FN,
|
|
CHECK-FN, ATTR and TYPE. The resulting body is constructed by
|
|
invoking the functions DEF-FN (passed the \"realname\" and the
|
|
version number, returning the compatibility definition), the
|
|
INSTALL-FN (passed the \"realname\" and returning the
|
|
installation code), CHECK-FN (passed the \"realname\" and
|
|
returning a check to see if the compatibility definition should
|
|
be installed). ATTR is a plist used to modify the generated
|
|
code. The following attributes are handled, all others are
|
|
ignored:
|
|
|
|
- :min-version :: Prevent the compatibility definition from begin
|
|
installed in versions older than indicated (string).
|
|
|
|
- :max-version :: Prevent the compatibility definition from begin
|
|
installed in versions newer than indicated (string).
|
|
|
|
- :feature :: The library the code is supposed to be loaded
|
|
with (via `eval-after-load').
|
|
|
|
- :cond :: Only install the compatibility code, iff the value
|
|
evaluates to non-nil.
|
|
|
|
For prefixed functions, this can be interpreted as a test to
|
|
`defalias' an existing definition or not.
|
|
|
|
- :no-highlight :: Do not highlight this definition as
|
|
compatibility function.
|
|
|
|
- :version :: Manual specification of the version the compatee
|
|
code was defined in (string).
|
|
|
|
- :realname :: Manual specification of a \"realname\" to use for
|
|
the compatibility definition (symbol).
|
|
|
|
- :notes :: Additional notes that a developer using this
|
|
compatibility function should keep in mind.
|
|
|
|
- :prefix :: Add a `compat-' prefix to the name, and define the
|
|
compatibility code unconditionally.
|
|
|
|
TYPE is used to set the symbol property `compat-type' for NAME.")
|
|
|
|
(defun compat--generate-default (name def-fn install-fn check-fn attr type)
|
|
"Generate a leaner compatibility definition.
|
|
See `compat-generate-function' for details on the arguments NAME,
|
|
DEF-FN, INSTALL-FN, CHECK-FN, ATTR and TYPE."
|
|
(let* ((min-version (plist-get attr :min-version))
|
|
(max-version (plist-get attr :max-version))
|
|
(feature (plist-get attr :feature))
|
|
(cond (plist-get attr :cond))
|
|
(version (or (plist-get attr :version)
|
|
compat-current-version))
|
|
(realname (or (plist-get attr :realname)
|
|
(intern (format "compat--%S" name))))
|
|
(check (cond
|
|
((or (and min-version
|
|
(version< emacs-version min-version))
|
|
(and max-version
|
|
(version< max-version emacs-version)))
|
|
'(compat--ignore))
|
|
((plist-get attr :prefix)
|
|
'(compat--inhibit-prefixed))
|
|
((and version (version<= version emacs-version) (not cond))
|
|
'(compat--ignore))
|
|
(`(when (and ,(if cond cond t)
|
|
,(funcall check-fn)))))))
|
|
(cond
|
|
((and (plist-get attr :prefix) (memq type '(func macro))
|
|
(string-match "\\`compat-\\(.+\\)\\'" (symbol-name name))
|
|
(let* ((actual-name (intern (match-string 1 (symbol-name name))))
|
|
(body (funcall install-fn actual-name version)))
|
|
(when (and (version<= version emacs-version)
|
|
(fboundp actual-name))
|
|
`(,@check
|
|
,(if feature
|
|
;; See https://nullprogram.com/blog/2018/02/22/:
|
|
`(eval-after-load ,feature `(funcall ',(lambda () ,body)))
|
|
body))))))
|
|
((plist-get attr :realname)
|
|
`(progn
|
|
,(funcall def-fn realname version)
|
|
(,@check
|
|
,(let ((body (funcall install-fn realname version)))
|
|
(if feature
|
|
;; See https://nullprogram.com/blog/2018/02/22/:
|
|
`(eval-after-load ,feature `(funcall ',(lambda () ,body)))
|
|
body)))))
|
|
((let* ((body (if (eq type 'advice)
|
|
`(,@check
|
|
,(funcall def-fn realname version)
|
|
,(funcall install-fn realname version))
|
|
`(,@check ,(funcall def-fn name version)))))
|
|
(if feature
|
|
;; See https://nullprogram.com/blog/2018/02/22/:
|
|
`(eval-after-load ,feature `(funcall ',(lambda () ,body)))
|
|
body))))))
|
|
|
|
(defun compat-generate-common (name def-fn install-fn check-fn attr type)
|
|
"Common code for generating compatibility definitions.
|
|
See `compat-generate-function' for details on the arguments NAME,
|
|
DEF-FN, INSTALL-FN, CHECK-FN, ATTR and TYPE."
|
|
(when (and (plist-get attr :cond) (plist-get attr :prefix))
|
|
(error "A prefixed function %s cannot have a condition" name))
|
|
(funcall compat--generate-function
|
|
name def-fn install-fn check-fn attr type))
|
|
|
|
(defun compat-common-fdefine (type name arglist docstring rest)
|
|
"Generate compatibility code for a function NAME.
|
|
TYPE is one of `func', for functions and `macro' for macros, and
|
|
`advice' ARGLIST is passed on directly to the definition, and
|
|
DOCSTRING is prepended with a compatibility note. REST contains
|
|
the remaining definition, that may begin with a property list of
|
|
attributes (see `compat-generate-common')."
|
|
(let ((oldname name) (body rest))
|
|
(while (keywordp (car body))
|
|
(setq body (cddr body)))
|
|
;; 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 (version<= emacs-version "25")
|
|
(delq (assq 'side-effect-free (car body)) (car body))
|
|
(delq (assq 'pure (car body)) (car body))))
|
|
;; Check if we want an explicitly prefixed function
|
|
(when (plist-get rest :prefix)
|
|
(setq name (intern (format "compat-%s" name))))
|
|
(compat-generate-common
|
|
name
|
|
(lambda (realname version)
|
|
`(,(cond
|
|
((memq type '(func advice)) 'defun)
|
|
((eq type 'macro) 'defmacro)
|
|
((error "Unknown type")))
|
|
,realname ,arglist
|
|
;; Prepend compatibility notice to the actual
|
|
;; documentation string.
|
|
,(let ((type (cond
|
|
((eq type 'func) "function")
|
|
((eq type 'macro) "macro")
|
|
((eq type 'advice) "advice")
|
|
((error "Unknown type")))))
|
|
(if version
|
|
(format
|
|
"[Compatibility %s for `%S', defined in Emacs %s]\n\n%s"
|
|
type oldname version docstring)
|
|
(format
|
|
"[Compatibility %s for `%S']\n\n%s"
|
|
type oldname docstring)))
|
|
;; Advice may use the implicit variable `oldfun', but
|
|
;; to avoid triggering the byte compiler, we make
|
|
;; sure the argument is used at least once.
|
|
,@(if (eq type 'advice)
|
|
(cons '(ignore oldfun) body)
|
|
body)))
|
|
(lambda (realname _version)
|
|
(cond
|
|
((memq type '(func macro))
|
|
;; Functions and macros are installed by
|
|
;; aliasing the name of the compatible
|
|
;; function to the name of the compatibility
|
|
;; function.
|
|
`(defalias ',name #',realname))
|
|
((eq type 'advice)
|
|
`(advice-add ',name :around #',realname))))
|
|
(lambda ()
|
|
(cond
|
|
((memq type '(func macro))
|
|
`(not (fboundp ',name)))
|
|
((eq type 'advice) t)))
|
|
rest type)))
|
|
|
|
(defmacro compat-defun (name arglist docstring &rest rest)
|
|
"Define NAME with arguments ARGLIST as a compatibility function.
|
|
The function must be documented in DOCSTRING. REST may begin
|
|
with a plist, that is interpreted by the macro but not passed on
|
|
to the actual function. See `compat-generate-common' for a
|
|
listing of attributes.
|
|
|
|
The definition will only be installed, if the version this
|
|
function was defined in, as indicated by the `:version'
|
|
attribute, is greater than the current Emacs version."
|
|
(declare (debug (&define name (&rest symbolp)
|
|
stringp
|
|
[&rest keywordp sexp]
|
|
def-body))
|
|
(doc-string 3) (indent 2))
|
|
(compat-common-fdefine 'func name arglist docstring rest))
|
|
|
|
(defmacro compat-defmacro (name arglist docstring &rest rest)
|
|
"Define NAME with arguments ARGLIST as a compatibility macro.
|
|
The macro must be documented in DOCSTRING. REST may begin
|
|
with a plist, that is interpreted by this macro but not passed on
|
|
to the actual macro. See `compat-generate-common' for a
|
|
listing of attributes.
|
|
|
|
The definition will only be installed, if the version this
|
|
function was defined in, as indicated by the `:version'
|
|
attribute, is greater than the current Emacs version."
|
|
(declare (debug compat-defun) (doc-string 3) (indent 2))
|
|
(compat-common-fdefine 'macro name arglist docstring rest))
|
|
|
|
(defmacro compat-advise (name arglist docstring &rest rest)
|
|
"Define NAME with arguments ARGLIST as a compatibility advice.
|
|
The advice function must be documented in DOCSTRING. REST may
|
|
begin with a plist, that is interpreted by this macro but not
|
|
passed on to the actual advice function. See
|
|
`compat-generate-common' for a listing of attributes. The advice
|
|
wraps the old definition, that is accessible via using the symbol
|
|
`oldfun'.
|
|
|
|
The advice will only be installed, if the version this function
|
|
was defined in, as indicated by the `:version' attribute, is
|
|
greater than the current Emacs version."
|
|
(declare (debug compat-defun) (doc-string 3) (indent 2))
|
|
(compat-common-fdefine 'advice name (cons 'oldfun arglist) docstring rest))
|
|
|
|
(defmacro compat-defvar (name initval docstring &rest attr)
|
|
"Declare compatibility variable NAME with initial value INITVAL.
|
|
The obligatory documentation string DOCSTRING must be given.
|
|
|
|
The remaining arguments ATTR form a plist, modifying the
|
|
behaviour of this macro. See `compat-generate-common' for a
|
|
listing of attributes. Furthermore, `compat-defvar' also handles
|
|
the attribute `:local' that either makes the variable permanent
|
|
local with a value of `permanent' or just buffer local with any
|
|
non-nil value."
|
|
(declare (debug (name form stringp [&rest keywordp sexp]))
|
|
(doc-string 3) (indent 2))
|
|
;; Check if we want an explicitly prefixed function
|
|
(let ((oldname name))
|
|
(when (plist-get attr :prefix)
|
|
(setq name (intern (format "compat-%s" name))))
|
|
(compat-generate-common
|
|
name
|
|
(lambda (realname version)
|
|
(let ((localp (plist-get attr :local)))
|
|
`(progn
|
|
(,(if (plist-get attr :constant) 'defconst 'defvar)
|
|
,realname ,initval
|
|
;; Prepend compatibility notice to the actual
|
|
;; documentation string.
|
|
,(if version
|
|
(format
|
|
"[Compatibility variable for `%S', defined in Emacs %s]\n\n%s"
|
|
oldname version docstring)
|
|
(format
|
|
"[Compatibility variable for `%S']\n\n%s"
|
|
oldname docstring)))
|
|
;; Make variable as local if necessary
|
|
,(cond
|
|
((eq localp 'permanent)
|
|
`(put ',realname 'permanent-local t))
|
|
(localp
|
|
`(make-variable-buffer-local ',realname))))))
|
|
(lambda (realname _version)
|
|
`(defvaralias ',name ',realname))
|
|
(lambda ()
|
|
`(not (boundp ',name)))
|
|
attr 'variable)))
|
|
|
|
(provide 'compat-macs)
|
|
;;; compat-macs.el ends here
|