Subject: Re: bug#78995: [PATCH] ;;;autoload-expand for special macros

From: "J.D. Smith" <[email-masked]>
Date: Sat, 12 Jul 2025 13:26:04 -0400

Stefan Monnier <[email-masked]> writes:

>> This makes it challenging to wrap things like `define-minor-mode' into a new
>> macro, and autoload the resulting forms.  This small patch adds a new
>> variant of the autoload cookie:
>>
>> ;;;autoload-expand
>> (my-autoloadable-macro-to-expand ...)
>
> I suspect this will often fail to work because autoloads are often
> generated by a fresh new session where `my-autoloadable-macro-to-expand`
> will not yet be defined.

Good point.  That chicken-and-egg problem was definitely in the back of
my mind, and is why I didn't attempt to create an expandable list to
replace the list of hard-coded special macros.

> I've been wanting to add the feature you propose, but this "macro is not
> defined" has gotten in the way until now.  I think we need to add
> something like a file-local `autoload-requires-for-macros`.

How about this solution?  If the car of the following form is not a
macro (and not a function), attempt to load the file itself (temporarily
expanding `load-path' to include the file's containing directory).  If
that fails to define the macro, warn the user, and simply proceed
without expansion.

diff --git a/lisp/emacs-lisp/loaddefs-gen.el b/lisp/emacs-lisp/loaddefs-gen.el
index 0f136df1fe2..f7919a7a377 100644
--- a/lisp/emacs-lisp/loaddefs-gen.el
+++ b/lisp/emacs-lisp/loaddefs-gen.el
@@ -147,7 +147,10 @@ loaddefs-generate--make-autoload
 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.  If
+EXPANSION is the symbol `force' and the following form is a macro call,
+it will be expanded prior to generating autoload forms, similar to what
+is done automatically for special macros like `define-minor-mode'."
   (let ((car (car-safe form)) expand)
     (cond
      ((and expansion (eq car 'defalias))
@@ -196,12 +199,13 @@ loaddefs-generate--make-autoload
                                       (cdr form)))))
           (when exps (cons 'progn exps)))))
 
-     ;; For complex cases, try again on the macro-expansion.
-     ((and (memq car '(easy-mmode-define-global-mode define-global-minor-mode
-                       define-globalized-minor-mode defun defmacro
-		       easy-mmode-define-minor-mode define-minor-mode
-                       define-inline cl-defun cl-defmacro cl-defgeneric
-                       cl-defstruct pcase-defmacro iter-defun cl-iter-defun))
+     ;; For complex cases and forced expansions, try again on the macro-expansion.
+     ((and (or (eq expansion 'force)
+	       (memq car '(easy-mmode-define-global-mode define-global-minor-mode
+			    define-globalized-minor-mode defun defmacro
+			    easy-mmode-define-minor-mode define-minor-mode
+			    define-inline cl-defun cl-defmacro cl-defgeneric
+			    cl-defstruct pcase-defmacro iter-defun cl-iter-defun)))
            (macrop car)
 	   (setq expand (let ((load-true-file-name file)
                               (load-file-name file))
@@ -381,6 +385,10 @@ loaddefs-generate--parse-file
 setting of `generated-autoload-file' in FILE, and
 by ;;;###foo-autoload statements.
 
+The special statement ;;;###[foo-]autoload-expand causes the following
+form to be macro-expanded prior to generating autoloads; see
+`loaddefs-generate--make-autoload'.
+
 If PACKAGE-DATA is `only', return only the package data.  If t,
 include the package data with the rest of the data.  Otherwise,
 don't include."
@@ -465,16 +473,38 @@ loaddefs-generate--parse-file
                                 (expand-file-name
                                  (concat aname "-loaddefs.el")
                                  (file-name-directory file))
-                              (or local-outfile main-outfile))))
+                              (or local-outfile main-outfile)))
+		   (expansion (and (looking-at "-expand[ \t]*") 'force)))
+	      (when expansion
+		(goto-char (match-end 0)))
               (if (eolp)
                   ;; We have a form following.
                   (let* ((form (prog1
                                    (read (current-buffer))
                                  (unless (bolp)
                                    (forward-line 1))))
-                         (autoload (or (loaddefs-generate--make-autoload
-                                        form load-name)
-                                       form)))
+                         (car (car form))
+                         autoload)
+                    (when expansion
+                      ;; We encountered ;;;###autoload-expand, so the
+                      ;; following form should be a macro.  Check that
+                      ;; it is defined.  If it is undefined (and not a
+                      ;; function), load the file to attempt to define
+                      ;; it.  If it remains undefined, warn and simply
+                      ;; ignore the expand request.
+                      (unless (or (macrop car) (functionp car))
+                        (let ((load-path (cons (file-name-directory file) load-path)))
+                         (message "loaddefs-gen: loading file %s" file)
+                         (condition-case e (load file)
+                           (error
+                            (warn "loaddefs-gen: load error\n\t%s" e)))))
+                      (unless (macrop car)
+                        (warn "loaddefs-gen: macro undefined, skipping expansion\n\t%s"
+                              car)
+                        (setq expansion nil)))
+                    (setq autoload (or (loaddefs-generate--make-autoload
+                                        form load-name expansion)
+                                       form))
                     ;; We get back either an autoload form, or a tree
                     ;; structure of `(progn ...)' things, so unravel that.
                     (let ((forms (if (eq (car autoload) 'progn)