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

From: Stefan Monnier <[email-masked]>
Date: Sun, 13 Jul 2025 11:19:08 -0400

>>> A file-local variable, which extends the (1) list only for that file?
>> I'm thinking we should replace the hardcoded list with a symbol
>> property.  When defining macros like `define-minor-mode` we'd set this
>> `autoload-macroexpand` property to a non-nil value, and `autoload.el`
>> would simply check the property instead of looking inside the hardcoded
>> list with `memq`.
>
> Oh, this is much better!  It relieves the _user_ of the macro the burden
> of remembering to "auto-load it correctly."  For macros which are
> defined in every session, you're done.  No special autoload-expand magic
> comment needed. It also makes it possible to entirely eliminate
> hard-coded list #1.  You'd just need to insert, e.g.:
>
>   (put 'defun 'autoload-macroexpand t)
>
> where the special macros are _defined_.  This would be a a bit more
> self-documenting too.

Yes, tho we'd probably define a `declare` thingy so you wouldn't write
the `put` but you'd write

    (defmacro define-minor-mode ...
      (declare (...
                (autoload macroexpand)
                ...))
      ...)

> We'd leave the "function-like operator" hard-coded list, but it could
> probably be trimmed down with the new `autoload-macroexpand' feature.
> For example, `transient-define-prefix' boils down to a defalias, which
> is already supported.

Ah, yes, I had not noticed that `transient-define-prefix` had been added
there, and now that I see it, it does make it seem like my impression
that it was unimportant may have been wrong.
Usually macroexpanding those things is enough (the macro can be defined
appropriately).  We could still refine the above with something like:

    (defmacro transient-define-prefix ...
      (declare (...
                (autoload function)
                ...))
      ...)

tho this probably needs more thought/design because we need more info
(where's the name of the defined thing, whether it's a macro or
a function, what's the list of arguments, ...).

>> This would work only for predefined or autoloaded macros (where the
>> `autoload-macroexpand` setting would be also pre-defined/loaded), so
>> it wouldn't work conveniently for a locally-defined and locally-used
>> macro, since you'd then need to somehow set the property and define or
>> autoload the macro before `autoload.el` gets to the macro calls.
>
> We could be aggressive about it.  If the car of some form encountered
> during file scanning (including expansion) is a macro, we check its
> `autoload-macroexpand' property and act accordingly.  But if it not
> already a known special-form, function, or macro, we load the file to
> see if that defines it.

Oh, yeah, neat.

> Importantly, in all of these cases, they are actually using adorned
> magic comments, like:
>
>   ;;;###autoload (autoload 'cape-tex "cape-char" nil t)
>   (cape-char--define tex "TeX" ?\\ ?^ ?_)
>
> so not relevant to our proposal (except that they could be improved by
> it.)

Yes, these are the ones that "document" a need for the new feature.

        Stefan

diff --git a/lisp/emacs-lisp/byte-run.el b/lisp/emacs-lisp/byte-run.el
index 6412c8cde22..654b67ada1b 100644
--- a/lisp/emacs-lisp/byte-run.el
+++ b/lisp/emacs-lisp/byte-run.el
@@ -286,6 +286,11 @@
 	    (list 'put (list 'quote name)
 		  ''edebug-form-spec (list 'quote spec)))))
 
+(defalias 'byte-run--set-autoload-macro
+  #'(lambda (name _args spec)
+     (list 'put (list 'quote name)
+	   ''autoload-macro (list 'quote spec))))
+
 (defalias 'byte-run--set-no-font-lock-keyword
   #'(lambda (name _args val)
       (list 'function-put (list 'quote name)
@@ -365,8 +370,10 @@ macro-declarations-alist
   (cons
    (list 'debug #'byte-run--set-debug)
    (cons
-    (list 'no-font-lock-keyword #'byte-run--set-no-font-lock-keyword)
-    defun-declarations-alist))
+    (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
diff --git a/lisp/emacs-lisp/loaddefs-gen.el b/lisp/emacs-lisp/loaddefs-gen.el
index 8a131bf885f..b8dfe095589 100644
--- a/lisp/emacs-lisp/loaddefs-gen.el
+++ b/lisp/emacs-lisp/loaddefs-gen.el
@@ -198,19 +198,20 @@
           (when exps (cons 'progn exps)))))
 
      ;; For complex cases, try again on the macro-expansion.
-     ((and (memq car '( define-globalized-minor-mode defun defmacro
+     ((and (or (eq 'expand (get car 'autoload-macro))
+               (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))
+                            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)))
+	   (not (eq car (car expand))))
       ;; Recurse on the expansion.
       (loaddefs-generate--make-autoload expand file 'expansion))