branch: externals/compat commit cfe0394b982fc61250c87a574ace59d6690bf738 Author: Daniel Mendler <m...@daniel-mendler.de> Commit: Daniel Mendler <m...@daniel-mendler.de>
Rework the macros in compat-macs - New macro compat--guarded-definition, which handles the generic feature and version checks (:feature, :min-version, :max-version). - compat--function-definition: Use compat--guarded-definition. - compat-defun, compat-defmacro: Use compat--function-definition. - compat-defvar: Use compat--guarded-definition. - compat-defalias: Use compat--guarded-definition. - compat--format-docstring: New helper function to format the compatibility docstring. Used by compat-defvar and compat-defun. - compat--condition-satisfied: New helper function which performs the version constraint checks. Used by compat--guarded-definition. - compat--check-attributes: New helper function which checks the attribute plists for validity. Used by compat--guarded-definition. --- NEWS.org | 3 + compat-macs.el | 403 +++++++++++++++++++++++++-------------------------------- 2 files changed, 182 insertions(+), 224 deletions(-) diff --git a/NEWS.org b/NEWS.org index b49e01d646..66e0bd5fab 100644 --- a/NEWS.org +++ b/NEWS.org @@ -2,6 +2,9 @@ * Development of "Compat" Version 29.1.1.0 +- The macros in ~compat-macs.el~ have been rewritten and greatly simplified. This + change makes it possible to further refine the criteria under which + compatibility aliases, functions, macros and variables are installed. - Remove deprecated, prefixed compatibility functions. - Remove deprecated features ~compat-help~, ~compat-font-lock~ and ~compat-24~. diff --git a/compat-macs.el b/compat-macs.el index f62659e082..4c9eda4f74 100644 --- a/compat-macs.el +++ b/compat-macs.el @@ -1,4 +1,4 @@ -;;; compat-macs.el --- Compatibility Macros -*- lexical-binding: t; no-byte-compile: t; -*- +;;; compat-macs.el --- Compatibility Macros -*- lexical-binding: t; no-byte-compile: t; -*- ;; Copyright (C) 2021-2023 Free Software Foundation, Inc. @@ -33,241 +33,196 @@ (setq compat--current-version version) nil) -(defun compat--with-feature (feature &rest body) - "Protect BODY with `with-eval-after-load' if FEATURE is non-nil." - (declare (indent 1)) - (if feature - `(with-eval-after-load ',feature ,@body) - (macroexp-progn body))) - -(defun compat--generate (name def-fn install-fn check-fn attr) - "Function used to generate compatibility code. -The function must take six arguments: NAME, DEF-FN, INSTALL-FN, -CHECK-FN and ATTR. 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 :: Do not install the compatibility definition - if Emacs version older than indicated. - -- :max-version :: Do not install the compatibility definition - if Emacs version newer or equal than indicated. - -- :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. - -- :realname :: Manual specification of a \"realname\" to use for - the compatibility definition (symbol). - -- :explicit :: Add a `compat-' prefix to the name, and define the - compatibility code unconditionally." - (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)) - (check)) - (unless compat--current-version - (error "No compat version declared")) - (when (and (plist-get attr :realname) - (string= name (plist-get attr :realname))) - (error "%S: Name is equal to realname" name)) - ;; subr-x is available at compile time. - (when (eq feature 'subr-x) - (error "Feature subr-x is forbidden")) - (when feature - (unless (require feature nil t) - (setq feature nil))) - (setq check - (cond - ((or (and min-version - (version< emacs-version min-version)) - (and max-version - (version<= max-version emacs-version))) - nil) - ((plist-get attr :explicit) - t) - ((and (version<= compat--current-version emacs-version) (not cond)) - nil) - ((and (if cond (eval cond t) t) - (funcall check-fn))))) - (cond - ((and (plist-get attr :explicit) - (let ((actual-name (intern (substring (symbol-name name) - (length "compat-"))))) - ;; NOTE: For prefixed/explicit functions check the Emacs version, - ;; since the fboundp check cannot be used! We want to redefine - ;; existing functions. - (when (and (version<= compat--current-version emacs-version) - (fboundp actual-name) - check) - (compat--with-feature feature - (funcall install-fn actual-name)))))) - ((let ((realname (plist-get attr :realname))) - (when realname - `(progn - ,(funcall def-fn realname) - ,(when check - (compat--with-feature feature - (funcall install-fn realname))))))) - (check - (compat--with-feature feature - (funcall def-fn name)))))) - -(defun compat--define-function (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')." - (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)))) - ;; Ensure that :realname is not the same as compat--<name>, - ;; since this is the compat-call/compat-function naming convention. - (when (and (plist-get rest :realname) - (string= (plist-get rest :realname) (format "compat--%s" name))) - (error "%s: :realname must not be the same as compat--<name>" name)) - ;; Check if we want an explicitly prefixed function - (when (plist-get rest :explicit) - (setq name (intern (format "compat-%s" name)))) - (compat--generate - name - (lambda (realname) - `(progn - (,(cond - ((eq type 'function) 'defun) - ((eq type 'macro) 'defmacro) - ((error "Unknown type"))) - ,(if (plist-get rest :explicit) - (intern (format "compat--%s" oldname)) - realname) - ,arglist - ;; Prepend compatibility notice to the actual - ;; documentation string. - ,(with-temp-buffer - (insert - (format - "[Compatibility %s for `%S', defined in Emacs %s. \ +(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" - type oldname compat--current-version compat--current-version - docstring)) - (let ((fill-column 80)) - (fill-region (point-min) (point-max))) - (buffer-string)) - ,@body) - ,@(and (plist-get rest :explicit) - (not (string= realname name)) - `((defalias ',realname #',(intern (format "compat--%s" oldname))))))) - (lambda (realname) - `(progn - ;; Functions and macros are installed by aliasing the name of the - ;; compatible function to the name of the compatibility function. - ,@(when (and (plist-get rest :realname) - (not (string= (plist-get rest :realname) name)) - (not (string= (plist-get rest :realname) realname))) - `((defalias ',(plist-get rest :realname) #',realname))) - ,@(unless (and (plist-get rest :explicit) (string= realname oldname)) - `((defalias ',name #',realname))))) - (lambda () - `(not (fboundp ',name))) - rest))) +`(compat) Emacs %s' for more details.]\n\n%s" + type name + compat--current-version compat--current-version + docstring)) + (let ((fill-column 80)) + (fill-region (point-min) (point-max))) + (buffer-string))) + +(defun compat--check-attributes (attrs allowed) + "Check ATTRS for ALLOWED keys and return rest." + (while (keywordp (car attrs)) + (unless (memq (car attrs) allowed) + (error "Invalid attribute %s" (car attrs))) + (unless (cdr attrs) + (error "Odd number of element in attribute list")) + (setq attrs (cddr attrs))) + attrs) + +(defun compat--condition-satisfied (attrs) + "Check that version constraints specified by ATTRS are satisfied." + (let ((min-version (plist-get attrs :min-version)) + (max-version (plist-get attrs :max-version)) + (cond (plist-get attrs :cond)) + (realname (plist-get attrs :realname))) + (and + ;; Min/max version bounds must be satisfied. + (or (not min-version) (version<= min-version emacs-version)) + (or (not max-version) (version< emacs-version max-version)) + ;; If a condition is specified, it must be satisfied. + (or (not cond) (eval cond t)) + ;; :realname specified or version constraint satisfied. + (or realname (version< emacs-version compat--current-version))))) + +(defun compat--guarded-definition (attrs args fun) + "Guard compatibility definition generation. +The version constraints specified by ATTRS are checked. +ARGS is a list of keywords which are looked up and passed to FUN." + (declare (indent 2)) + (let* ((body (compat--check-attributes + attrs `(,@args :min-version :max-version :cond :feature))) + (feature (plist-get attrs :feature)) + (attrs `(:body ,body ,@attrs))) + ;; Require feature at compile time + (when feature + (when (eq feature 'subr-x) + (error "Feature subr-x must not be specified")) + ;; If the feature does not exist, treat it as nil. The function will then + ;; be defined on the toplevel and not in a `with-eval-after-load' block. + (setq feature (require feature nil t))) + (when (compat--condition-satisfied attrs) + (setq body (apply fun (mapcar (lambda (x) (plist-get attrs x)) args))) + (when body + (if feature + `(with-eval-after-load ',feature ,@body) + (macroexp-progn body)))))) + +(defun compat--function-definition (type name arglist docstring rest) + "Define function NAME of TYPE with ARGLIST and DOCSTRING. +REST are attributes and the function BODY." + (compat--guarded-definition rest '(:explicit :realname :body) + (lambda (explicit realname 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 (version<= emacs-version "25") + (delq (assq 'side-effect-free (car body)) (car body)) + (delq (assq 'pure (car body)) (car body)))) + ;; Ensure that :realname is not the same as compat--<name>, + ;; since this is the compat-call/compat-function naming convention. + (when (and realname + (or (string= realname explicit) + (not (string-prefix-p + "compat--" (symbol-name realname))))) + (error "%s: Invalid :realname name" realname)) + (let ((def-name ;; Name of the definition. May be nil -> no definition. + (if (not (fboundp name)) ;; If not bound, `name' should be bound. + name + ;; Use `:explicit' name if the function is already defined, + ;; and if version constraint is satisfied. + (and explicit + (version< emacs-version compat--current-version) + (intern (format "compat--%s" name)))))) + `(,@(when def-name + `((,(if (eq type 'macro) 'defmacro 'defun) + ,def-name ,arglist + ,(compat--format-docstring type name docstring) + ,@body))) + ,@(when realname + `((defalias ',realname #',(or def-name name))))))))) + +(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. + +- :min-version :: Only install the definition if the Emacs + version is greater or equal than the given version. + +- :max-version :: Only install the definition if the Emacs + version is smaller than the given version. + +- :feature :: Wrap the definition with `with-eval-after-load'. + +- :cond :: Only install the definition if :cond evaluates to + non-nil." + (compat--guarded-definition attrs () + (lambda () + (unless (fboundp name) + `((defalias ',name ',def)))))) (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' for a -listing of attributes." + "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 compatiblity function is +defined. + +- :realname :: Additionally install the definition under the + given name. + +- :explicit :: Make the definition available such that it can be + called explicitly via `compat-call'. + +- :min-version :: Install the definition if the Emacs version is + greater or equal than the given version. + +- :max-version :: Install the definition if the Emacs version is + smaller than the given version. + +- :feature :: Wrap the definition with `with-eval-after-load'. + +- :cond :: Install the definition if :cond evaluates to non-nil." (declare (debug (&define name (&rest symbolp) stringp [&rest keywordp sexp] def-body)) (doc-string 3) (indent 2)) - (compat--define-function 'function name arglist docstring rest)) + (compat--function-definition 'function 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' for a -listing of attributes." - (declare (debug compat-defun) (doc-string 3) (indent 2)) ;; <UNTESTED> - (compat--define-function 'macro name arglist docstring rest)) - -(defmacro compat-defalias (name def) - "Declare compatibility alias NAME with DEF." - (compat--generate - name - (lambda (realname) - `(defalias ',realname ',def)) - (lambda (realname) - `(defalias ',name ',realname)) - (lambda () - `(not (fboundp ',name))) - nil)) - -(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' 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." + "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--function-definition '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 :: Define a constant if non-nil. + +- :local :: Make the variable permanently local if the value is + `permanent'. For other non-nil values make the variable + buffer-local. + +- :min-version :: Install the definition if the Emacs version is + greater or equal than the given version. + +- :max-version :: Install the definition if the Emacs version is + smaller than the given version. + +- :feature :: Wrap the definition with `with-eval-after-load'. + +- :cond :: Install the definition if :cond evaluates to non-nil." (declare (debug (name form stringp [&rest keywordp sexp])) (doc-string 3) (indent 2)) - (when (or (plist-get attr :explicit) (plist-get attr :realname)) - (error ":explicit cannot be specified for compatibility variables")) - (compat--generate - name - (lambda (realname) - (let ((localp (plist-get attr :local))) - `(progn - (,(if (plist-get attr :constant) 'defconst 'defvar) - ,realname ,initval - ;; Prepend compatibility notice to the actual - ;; documentation string. - ,(with-temp-buffer - (insert - (format - "[Compatibility variable for `%S', defined in Emacs %s]\n\n%s" - name compat--current-version docstring)) - (let ((fill-column 80)) - (fill-region (point-min) (point-max))) - (buffer-string))) - ;; Make variable as local if necessary - ,(cond - ((eq localp 'permanent) - `(put ',realname 'permanent-local t)) - (localp - `(make-variable-buffer-local ',realname)))))) - (lambda (realname) - `(defvaralias ',name ',realname)) - (lambda () - `(not (boundp ',name))) - attr)) + (compat--guarded-definition attrs '(:local :constant) + (lambda (local constant) + (unless (boundp name) + `((,(if constant 'defconst 'defvar) + ,name ,initval + ,(compat--format-docstring 'variable name docstring)) + ,@(cond + ((eq local 'permanent) + `((put ',name 'permanent-local t))) + (local + `((make-variable-buffer-local ',name))))))))) (provide 'compat-macs) ;;; compat-macs.el ends here