From 7486e5c3680b41cb58cb6eb1fa5aa3620d4a50bd Mon Sep 17 00:00:00 2001 From: JD Smith <93749+jdtsmith@users.noreply.github.com> Date: Thu, 24 Jul 2025 15:42:10 -0400 Subject: [PATCH] Implement new autoload macro expansion declare form Currently, a hard-coded set of macros is automatically expanded during generation of autoloads. To allow user macros to request such expansion, this implements a new declare form `autoload-macro' (Bug#78995), with supported value `expand'. For example, macros which wrap `define-minor-mode', can declare `(autoload-macro expand)' to request that ;;;###autoload-adorned calls to the macro are expanded during generation, such that an autoload for the resulting function is created. * lisp/emacs-lisp/byte-run.el (byte-run--set-autoload-macro): Handle autoload-macro declare forms. (macro-declarations-alist) Add handler for 'autoload-macro declare forms. (defmacro, defun): * lisp/emacs-lisp/cl-generic.el (cl-defgeneric, cl-defun) (cl-iter-defun, cl-defmacro, cl-defstruct): * lisp/emacs-lisp/easy-mmode.el (define-minor-mode, define-globalized-minor-mode, iter-defun): * lisp/emacs-lisp/inline.el (define-inline): * lisp/emacs-lisp/pcase.el (pcase-defmacro): Declare (autoload-macro expand) to request expansion of the macro during autoload generation. * lisp/emacs-lisp/loaddefs-gen.el (loaddefs-generate--make-autoload): Handle the `autoload-macro=expand' property for macros. Load the ;;;###autoload-containing file if an unknown symbol is encountered in the car of the following form, to give packages a chance to define their macros and request expansion. Factor list of special function-defining macros out as a constant variable: `loaddefs--defining-macros'. * doc/lispref/functions.texi (Declare Form): * doc/lispref/loading.texi (Autoload): Document `autoload-macro'. --- doc/lispref/functions.texi | 8 ++++ doc/lispref/loading.texi | 65 +++++++++++++++++++++---- lisp/emacs-lisp/byte-run.el | 21 ++++++-- lisp/emacs-lisp/cl-generic.el | 4 +- lisp/emacs-lisp/cl-macs.el | 15 ++++-- lisp/emacs-lisp/easy-mmode.el | 12 +++-- lisp/emacs-lisp/generator.el | 3 +- lisp/emacs-lisp/inline.el | 3 +- lisp/emacs-lisp/loaddefs-gen.el | 85 +++++++++++++++++++++------------ lisp/emacs-lisp/pcase.el | 4 +- 10 files changed, 164 insertions(+), 56 deletions(-) diff --git a/doc/lispref/functions.texi b/doc/lispref/functions.texi index ea6e07e05fb..1b2a21e160c 100644 --- a/doc/lispref/functions.texi +++ b/doc/lispref/functions.texi @@ -2742,6 +2742,14 @@ Indent calls to this function or macro according to @var{indent-spec}. This is typically used for macros, though it works for functions too. @xref{Indenting Macros}. +@cindex @code{autoload-macro} (@code{declare} spec) +@item (autoload-macro @var{value}) +This is used when defining a macro. If @var{value} is @samp{expand}, +any calls to the macro which follow an autoload comment will first be +expanded during generation of the autoloads. This declaration is used +as an alternative to hard-coding lists of macros to expand in +@code{loaddefs-generate--make-autoload}. @xref{Autoload}. + @item (interactive-only @var{value}) Set the function's @code{interactive-only} property to @var{value}. @xref{The interactive-only property}. diff --git a/doc/lispref/loading.texi b/doc/lispref/loading.texi index 7fa64b72999..836d980ff0d 100644 --- a/doc/lispref/loading.texi +++ b/doc/lispref/loading.texi @@ -665,10 +665,12 @@ Building Emacs loads @file{loaddefs.el} and thus calls @code{autoload}. The same magic comment can copy any kind of form into @file{loaddefs.el}. The form following the magic comment is copied -verbatim, @emph{except} if it is one of the forms which the autoload -facility handles specially (e.g., by conversion into an -@code{autoload} call). The forms which are not copied verbatim are -the following: +verbatim, unless it is a form which the autoload facility handles +specially, by conversion directly into an @code{autoload} call, or by +recursively expanding the macro. Any macro can request expansion of +autoloads which call it during generation; @xref{autoload-macro expand}. + +The following forms are handled specially: @table @asis @item Definitions for function or function-like objects: @@ -684,8 +686,12 @@ and @code{define-overloadable-function} (see the commentary in @item Other definition types: @code{defcustom}, @code{defgroup}, @code{deftheme}, @code{defclass} -(@pxref{Top,EIEIO,,eieio,EIEIO}), and @code{define-skeleton} -(@pxref{Top,Autotyping,,autotype,Autotyping}). +(@pxref{Top,EIEIO,,eieio,EIEIO}), @code{define-skeleton} +(@pxref{Top,Autotyping,,autotype,Autotyping}), +@code{transient-define-prefix}, @code{transient-define-suffix}, +@code{transient-define-infix}, @code{transient-define-argument}, and +@code{transient-define-group} (@pxref{TOP,Transient,,transient,Transient +User and Developer Manual}). @end table You can also use a magic comment to execute a form at build time @@ -727,11 +733,50 @@ keep the line length down. @samp{(fn)} in the usage part of the documentation string is replaced with the function's name when the various help functions (@pxref{Help Functions}) display it. +@anchor{autoload-macro expand} If you write a function definition with an unusual macro that is not -one of the known and recognized function definition methods, use of an -ordinary magic autoload comment would copy the whole definition into -@code{loaddefs.el}. That is not desirable. You can put the desired -@code{autoload} call into @code{loaddefs.el} instead by writing this: +one of the known and recognized function definition methods, using an +ordinary magic autoload comment with a call to the macro would require +autoloading the macro definition itself to work. Doing so copies the +whole macro definition into the autoload file. If that is not desired, +you can use the special declare form @code{(autoload-macro expand)} in +your macro definition (@pxref{Declare Form}), instead of autoloading it. +This indicates to the autoload system that calls to your macro following +@samp{;;;###autoload} should be expanded, similar to how the special +functions listed above are handled. For example, a macro which wraps +@code{define-minor-mode} can request expansion, so that proper +@code{autoload} calls for the function it defines are generated. + +@cindex @code{:autoload-end} +The keyword symbol @code{:autoload-end} can be used in the expansion of +a macro to prevent including unwanted forms in the autoload output. Its +presence causes any further elements within the form where it appears to +be silently skipped. For example, if during autoload generation, a +macro's expansion includes: + +@example +(progn + (put my-mode 'visible-prop t) + :autoload-end + (put my-mode 'hidden-prop nil)) +@end example + +@noindent +the final form (@w{@code{(put my-mode 'hidden-prop nil)}}) will not be +copied into the autoload file. + +Note that, if a symbol in the @code{car} of an autoloaded form is found +to be undefined during autoload generation, the file in which the +associated @samp{;;;###autoload} appears will itself be loaded, to give +the macro a chance to be defined. Packages which use special loading +mechanisms, including loading their own @file{@var{package}-loaddefs.el} +file, should therefore gracefully handle load errors, since these files +may not yet exist during autoload generation. This can be done, e.g., +by setting the @var{no-error} argument of @code{require} non-@code{nil} +(@pxref{Named Features})). + +Alternatively, instead of expansion, you can put the desired +@code{autoload} call into @file{loaddefs.el} by writing this: @example ;;;###autoload (autoload 'foo "myfile") diff --git a/lisp/emacs-lisp/byte-run.el b/lisp/emacs-lisp/byte-run.el index 6412c8cde22..49fd6084693 100644 --- a/lisp/emacs-lisp/byte-run.el +++ b/lisp/emacs-lisp/byte-run.el @@ -286,6 +286,12 @@ This is used by `declare'.") (list 'put (list 'quote name) ''edebug-form-spec (list 'quote spec))))) +(defalias 'byte-run--set-autoload-macro + #'(lambda (name _args spec) + (list 'function-put (list 'quote name) + ''autoload-macro (list 'quote spec))) + "Handle autoload-macro declarations") + (defalias 'byte-run--set-no-font-lock-keyword #'(lambda (name _args val) (list 'function-put (list 'quote name) @@ -365,8 +371,13 @@ This is used by `declare'.") (cons (list 'debug #'byte-run--set-debug) (cons - (list 'no-font-lock-keyword #'byte-run--set-no-font-lock-keyword) - defun-declarations-alist)) + ;; macros can declare (autoload-macro expand) to request expansion + ;; during autoload generation of forms calling them. See + ;; `loaddefs-generate--make-autoload'. + (list 'autoload-macro #'byte-run--set-autoload-macro) + (cons + (list 'no-font-lock-keyword #'byte-run--set-no-font-lock-keyword) + defun-declarations-alist))) "List associating properties of macros to their macro expansion. Each element of the list takes the form (PROP FUN) where FUN is a function. For each (PROP . VALUES) in a macro's declaration, the FUN corresponding @@ -412,6 +423,8 @@ The return value is undefined. (if declarations (cons 'prog1 (cons def (car declarations))) def)))))) +;; Expand to defalias and related forms on autoload gen +(function-put 'defmacro 'autoload-macro 'expand) ; Since we cannot `declare' it ;; Now that we defined defmacro we can use it! (defmacro defun (name arglist &rest body) @@ -424,7 +437,9 @@ INTERACTIVE is an optional `interactive' specification. The return value is undefined. \(fn NAME ARGLIST [DOCSTRING] [DECL] [INTERACTIVE] BODY...)" - (declare (doc-string 3) (indent 2)) + (declare (doc-string 3) (indent 2) + ;; Expand to defalias on autoload gen + (autoload-macro expand)) (or name (error "Cannot define '%s' as a function" name)) (if (null (and (listp arglist) diff --git a/lisp/emacs-lisp/cl-generic.el b/lisp/emacs-lisp/cl-generic.el index 4c6c6a0007c..d02d0d6c2a4 100644 --- a/lisp/emacs-lisp/cl-generic.el +++ b/lisp/emacs-lisp/cl-generic.el @@ -258,7 +258,9 @@ DEFAULT-BODY, if present, is used as the body of a default method. cl--generic-edebug-make-name in:method] lambda-doc def-body)]] - def-body))) + def-body)) + ;; Expand to defun and related forms on autoload definition + (autoload-macro expand)) (let* ((doc (if (stringp (car-safe options-and-methods)) (pop options-and-methods))) (declarations nil) diff --git a/lisp/emacs-lisp/cl-macs.el b/lisp/emacs-lisp/cl-macs.el index a076012cd30..08a6cbf209d 100644 --- a/lisp/emacs-lisp/cl-macs.el +++ b/lisp/emacs-lisp/cl-macs.el @@ -396,7 +396,9 @@ more details. [&optional ("interactive" interactive)] def-body)) (doc-string 3) - (indent 2)) + (indent 2) + ;; expand to function definition on autoload gen + (autoload-macro expand)) `(defun ,name ,@(cl--transform-lambda (cons args body) name))) ;;;###autoload @@ -414,7 +416,9 @@ and BODY is implicitly surrounded by (cl-block NAME ...). [&optional ("interactive" interactive)] def-body)) (doc-string 3) - (indent 2)) + (indent 2) + ;; expand (eventually) to function definition on autoload gen + (autoload-macro expand)) (require 'generator) `(iter-defun ,name ,@(cl--transform-lambda (cons args body) name))) @@ -473,7 +477,8 @@ more details. (declare (debug (&define name cl-macro-list cl-declarations-or-string def-body)) (doc-string 3) - (indent 2)) + (indent 2) + (autoload-macro expand)) ; expand to defmacro on autoload gen `(defmacro ,name ,@(cl--transform-lambda (cons args body) name))) (def-edebug-elem-spec 'cl-lambda-expr @@ -3087,7 +3092,9 @@ To see the documentation for a defined struct type, use sexp])] [&optional stringp] ;; All the above is for the following def-form. - &rest &or symbolp (symbolp &optional def-form &rest sexp)))) + &rest &or symbolp (symbolp &optional def-form &rest sexp))) + ;; expand to function definitions and related forms on autoload gen + (autoload-macro expand)) (let* ((name (if (consp struct) (car struct) struct)) (warning nil) (opts (cdr-safe struct)) diff --git a/lisp/emacs-lisp/easy-mmode.el b/lisp/emacs-lisp/easy-mmode.el index b5831c314e1..ca921308877 100644 --- a/lisp/emacs-lisp/easy-mmode.el +++ b/lisp/emacs-lisp/easy-mmode.el @@ -222,10 +222,12 @@ INIT-VALUE LIGHTER KEYMAP. (indent defun) (debug (&define name string-or-null-p [&optional [¬ keywordp] sexp - &optional [¬ keywordp] sexp - &optional [¬ keywordp] sexp] + &optional [¬ keywordp] sexp + &optional [¬ keywordp] sexp] [&rest [keywordp sexp]] - def-body))) + def-body)) + ;; expand to the command definition on autoload gen + (autoload-macro expand)) (let* ((last-message (make-symbol "last-message")) (mode-name (symbol-name mode)) @@ -488,7 +490,9 @@ after running the major mode's hook. However, MODE is not turned on if the hook has explicitly disabled it. \(fn GLOBAL-MODE MODE TURN-ON [KEY VALUE]... BODY...)" - (declare (doc-string 2) (indent defun)) + (declare (doc-string 2) (indent defun) + ;; expand to the minor-mode definition on autoload gen + (autoload-macro expand)) (let* ((global-mode-name (symbol-name global-mode)) (mode-name (symbol-name mode)) (pretty-name (easy-mmode-pretty-mode-name mode)) diff --git a/lisp/emacs-lisp/generator.el b/lisp/emacs-lisp/generator.el index 344e11e245e..c01e559fd5c 100644 --- a/lisp/emacs-lisp/generator.el +++ b/lisp/emacs-lisp/generator.el @@ -675,7 +675,8 @@ encapsulates the state of a computation that produces a sequence of values. Callers can retrieve each value using `iter-next'." (declare (indent defun) (debug (&define name lambda-list lambda-doc &rest sexp)) - (doc-string 3)) + (doc-string 3) + (autoload-macro expand)) ; expand to the defun on autoload gen (cl-assert lexical-binding) (let* ((parsed-body (macroexp-parse-body body)) (declarations (car parsed-body)) diff --git a/lisp/emacs-lisp/inline.el b/lisp/emacs-lisp/inline.el index c015e2b57d0..027c24c465b 100644 --- a/lisp/emacs-lisp/inline.el +++ b/lisp/emacs-lisp/inline.el @@ -135,7 +135,8 @@ After VARS is handled, BODY is evaluated in the new environment." This is like `defmacro', but has several advantages. See Info node `(elisp)Defining Functions' for more details." ;; FIXME: How can this work with CL arglists? - (declare (indent defun) (debug defun) (doc-string 3)) + (declare (indent defun) (debug defun) (doc-string 3) + (autoload-macro expand)) ; expand to the defun on autoload gen (let ((doc (if (stringp (car-safe body)) (list (pop body)))) (declares (if (eq (car-safe (car-safe body)) 'declare) (pop body))) (cm-name (intern (format "%s--inliner" name))) diff --git a/lisp/emacs-lisp/loaddefs-gen.el b/lisp/emacs-lisp/loaddefs-gen.el index a8500759cf8..fc05e8b9ec1 100644 --- a/lisp/emacs-lisp/loaddefs-gen.el +++ b/lisp/emacs-lisp/loaddefs-gen.el @@ -143,12 +143,37 @@ scanning for autoloads and will be in the `load-path'." 3) form)) +;; The following macros are known to define functions, and are treated +;; specially when encountered during autoload generation, translating +;; calls to them directly into appropriate (autoload function ...) +;; forms. +;; +;; An alternative to appearing on this list is for a macro to declare +;; (autoload-macro expand), so calls to it get expanded into more basic +;; forms during generation. Macros may be removed from this list once +;; they request such expansion and produce suitable output (e.g. by +;; employing :autoload-end to omit unneeded forms). +(defconst loaddefs--defining-macros + '( define-skeleton define-derived-mode define-compilation-mode + define-generic-mode define-globalized-minor-mode define-minor-mode + cl-defun defun* cl-defmacro defmacro* define-overloadable-function + transient-define-prefix transient-define-suffix transient-define-infix + transient-define-argument transient-define-group + ;; Obsolete; keep until the alias is removed. + easy-mmode-define-global-mode + easy-mmode-define-minor-mode + define-global-minor-mode)) + +(defvar loaddefs--load-error-files nil) (defun loaddefs-generate--make-autoload (form file &optional expansion) "Turn FORM into an autoload or defvar for source file FILE. Returns nil if FORM is not a special autoload form (i.e. a function definition or macro definition or a defcustom). If EXPANSION is non-nil, we're processing the macro expansion of an -expression, in which case we want to handle forms differently." +expression, in which case we want to handle forms differently. + +Note that macros can request expansion by including `(autoload-macro +expand)' among their `declare' forms." (let ((car (car-safe form)) expand) (cond ((and expansion (eq car 'defalias)) @@ -192,42 +217,40 @@ expression, in which case we want to handle forms differently." (setq form (copy-sequence form)) (setcdr (memq :autoload-end form) nil)) (let ((exps (delq nil (mapcar (lambda (form) - (loaddefs-generate--make-autoload - form file expansion)) + (unless (eq form :autoload-end) + (loaddefs-generate--make-autoload + form file expansion))) (cdr form))))) (when exps (cons 'progn exps))))) - ;; For complex cases, try again on the macro-expansion. - ((and (memq car '( define-globalized-minor-mode defun defmacro - define-minor-mode define-inline - cl-defun cl-defmacro cl-defgeneric - cl-defstruct pcase-defmacro iter-defun cl-iter-defun - ;; Obsolete; keep until the alias is removed. - easy-mmode-define-global-mode - easy-mmode-define-minor-mode - define-global-minor-mode)) - (macrop car) - (setq expand (let ((load-true-file-name file) - (load-file-name file)) - (macroexpand form))) - (memq (car expand) '(progn prog1 defalias))) + ;; For macros which request it, try again on their expansion. + ((progn + ;; If the car is an unknown symbol, we load the file first to + ;; give packages a chance to define their macros. + (unless (or (not (symbolp car)) (fboundp car) + ;; Special cases handled below + (memq car loaddefs--defining-macros) + (memq car '(defclass defcustom deftheme defgroup nil)) + (assoc file load-history) + (member file loaddefs--load-error-files)) + (let ((load-path (cons (file-name-directory file) load-path))) + (message "loaddefs-gen: loading file %s (for %s)" file car) + (condition-case e (load file) + (error + (push file loaddefs--load-error-files) ; do not attempt again + (warn "loaddefs-gen: load error\n\t%s" e))))) + (and (macrop car) + (eq 'expand (function-get car 'autoload-macro)) + (setq expand (let ((load-true-file-name file) + (load-file-name file)) + (macroexpand form))) + (not (eq car (car expand))))) ;; Recurse on the expansion. (loaddefs-generate--make-autoload expand file 'expansion)) - ;; For special function-like operators, use the `autoload' function. - ((memq car '( define-skeleton define-derived-mode - define-compilation-mode define-generic-mode - define-globalized-minor-mode - define-minor-mode - cl-defun defun* cl-defmacro defmacro* - define-overloadable-function - transient-define-prefix transient-define-suffix - transient-define-infix transient-define-argument - transient-define-group - ;; Obsolete; keep until the alias is removed. - easy-mmode-define-global-mode - easy-mmode-define-minor-mode - define-global-minor-mode)) + ;; For known special macros which define functions, use `autoload' + ;; directly. + ((memq car loaddefs--defining-macros) (let* ((macrop (memq car '(defmacro cl-defmacro defmacro*))) (name (nth 1 form)) (args (pcase car diff --git a/lisp/emacs-lisp/pcase.el b/lisp/emacs-lisp/pcase.el index c68b8961ee3..cd6b5e42771 100644 --- a/lisp/emacs-lisp/pcase.el +++ b/lisp/emacs-lisp/pcase.el @@ -544,7 +544,9 @@ to this macro. By convention, DOC should use \"EXPVAL\" to stand for the result of evaluating EXP (first arg to `pcase'). \n(fn NAME ARGS [DOC] &rest BODY...)" - (declare (indent 2) (debug defun) (doc-string 3)) + (declare (indent 2) (debug defun) (doc-string 3) + ;; Expand to defun and related forms on autoload gen + (autoload-macro expand)) ;; Add the function via `fsym', so that an autoload cookie placed ;; on a pcase-defmacro will cause the macro to be loaded on demand. (let ((fsym (intern (format "%s--pcase-macroexpander" name)))