(require 'subr-x)
(require 'cl-lib)
(defvar compat-macs--version nil
"Version of the currently defined compatibility definitions.")
(defun compat-macs--strict (cond &rest error)
"Assert strict COND, otherwise fail with ERROR."
(when (bound-and-true-p compat-strict)
(apply #'compat-macs--assert cond error)))
(defun compat-macs--assert (cond &rest error)
"Assert COND, otherwise fail with ERROR."
(unless cond (apply #'error error)))
(defun compat-macs--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. \
See (compat) Emacs %s' for more details.]\n\n%s"
type name compat-macs--version compat-macs--version docstring))
(let ((fill-column 80))
(fill-region (point-min) (point-max)))
(buffer-string)))
(defun compat-macs--check-attributes (attrs preds)
"Check ATTRS given PREDS predicate plist and return rest."
(while (keywordp (car attrs))
(compat-macs--assert (cdr attrs) "Attribute list length is odd")
(compat-macs--assert (let ((p (plist-get preds (car attrs))))
(and p (or (eq p t) (funcall p (cadr attrs)))))
"Invalid attribute %s" (car attrs))
(setq attrs (cddr attrs)))
attrs)
(defun compat-macs--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))
(compat-macs--assert compat-macs--version "No `compat-version' was declared")
(let* ((body (compat-macs--check-attributes
attrs `(,@preds :feature symbolp)))
(feature (plist-get attrs :feature))
(attrs `(:body ,body ,@attrs))
args)
(when feature
(compat-macs--assert (not (eq feature 'subr-x)) "Invalid feature subr-x")
(require feature))
(when (version< emacs-version compat-macs--version)
(while preds
(push (plist-get attrs (car preds)) args)
(setq preds (cddr preds)))
(setq body (apply fun (nreverse args)))
(if (and feature body)
`(with-eval-after-load ',feature ,@body)
(macroexp-progn body)))))
(defun compat-macs--defun (type name arglist docstring rest)
"Define function NAME of TYPE with ARGLIST and DOCSTRING.
REST are attributes and the function BODY."
(compat-macs--guard
rest (list :extended (lambda (x) (or (booleanp x) (version-to-list x)))
:obsolete (lambda (x) (or (booleanp x) (stringp x)))
:body t)
(lambda (extended obsolete body)
(when (stringp extended)
(compat-macs--assert
(and (version< extended compat-macs--version) (version< "24.4" extended))
"Invalid :extended version %s for %s %s" extended type name)
(setq extended (version<= extended emacs-version)))
(compat-macs--strict (eq extended (fboundp name))
"Wrong :extended flag for %s %s" type name)
(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))))
(let* ((defname (if (and extended (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-macs--docstring type name docstring)
,@body)))
`(,@(if (eq defname name)
`((declare-function ,name nil)
(unless (fboundp ',name) ,def))
(list def))
,@(when obsolete
`((make-obsolete
',defname ,(if (stringp obsolete) obsolete "No substitute")
,compat-macs--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."
(declare (debug ([&rest keywordp sexp] def-body))
(indent 1))
(compat-macs--guard rest '(:body t)
(lambda (body)
(compat-macs--assert body "The guarded body is empty")
(if (eq cond t)
body
(compat-macs--strict (eval cond t) "Guard %S failed" cond)
`((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 :: See `compat-guard'."
(declare (debug (name symbolp [&rest keywordp sexp])))
(compat-macs--guard attrs '(:obsolete booleanp)
(lambda (obsolete)
(compat-macs--strict (not (fboundp name)) "%s already defined" name)
`((unless (fboundp ',name)
(defalias ',name ',def
,(compat-macs--docstring 'function name
(get name 'function-documentation)))
,@(when obsolete
`((make-obsolete ',name ',def ,compat-macs--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.
- :extended :: Mark the function as extended if t. The function
must be called explicitly via `compat-call'. This attribute
should be used for functions which extend already existing
functions, e.g., functions which changed their calling
convention or their behavior. The value can also be a version
string, which specifies the Emacs version when the original
version of the function was introduced.
- :obsolete :: Mark the function as obsolete if t, can be a
string describing the obsoletion.
- :feature :: See `compat-guard'."
(declare (debug (&define name (&rest symbolp)
stringp
[&rest keywordp sexp]
def-body))
(doc-string 3) (indent 2))
(compat-macs--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-macs--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 :: See `compat-guard'."
(declare (debug (name form stringp [&rest keywordp sexp]))
(doc-string 3) (indent 2))
(compat-macs--guard
attrs (list :constant #'booleanp
:local (lambda (x) (memq x '(nil t permanent)))
:obsolete (lambda (x) (or (booleanp x) (stringp x))))
(lambda (constant local obsolete)
(compat-macs--strict (not (boundp name)) "%s already defined" name)
(compat-macs--assert (not (and constant local)) "Both :constant and :local")
`((unless (boundp ',name)
(,(if constant 'defconst 'defvar)
,name ,initval
,(compat-macs--docstring 'variable name docstring))
,@(when obsolete
`((make-obsolete-variable
',name ,(if (stringp obsolete) obsolete "No substitute")
,compat-macs--version))))
,@(and local `((make-variable-buffer-local ',name)))
,@(and (eq local 'permanent) `((put ',name 'permanent-local t)))))))
(defmacro compat-version (version)
"Set the Emacs version that is currently being handled to VERSION."
(setq compat-macs--version version)
nil)
(defmacro compat-require (feature version)
"Require FEATURE if the Emacs version is less than VERSION."
(when (version< emacs-version version)
(require feature)
`(require ',feature)))
(provide 'compat-macs)