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

From: "J.D. Smith" <[email-masked]>
Date: Sun, 13 Jul 2025 09:47:48 -0400

Stefan Monnier <[email-masked]> writes:

>> 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.

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.

> 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.  If it _remains_ undefined, we just include the
whole form in the autoload file, just as we do now (generating a
warning).  But if loading the file _did_ define it as a macro, we check
its `autoload-macroexpand' property, and proceed as before.

I was curious how many existing ;;;###autoloads this would affect, since
I almost always see it used with `defun' or `define-minor-mode'.  I
looked through the entire emacs-30 lisp codebase[1] and could only find
`define-erc-module' as the car of a top-level autoload form which wasn't
already a known function, macro, or special form.  I repeated this
exercise among my 171 installed packages, and found only:

  Unknown autoload car: cape-char--define
  Unknown autoload car: define-erc-module

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.)

> I assume there's a concrete use-case which motivated your patch.
> Do you mind sharing it?

I have a large package in development which has a flexible extension
system.  I'd like to enable "3rd-party" extensions with a convenience
`define-extension' macro.  In addition to defining a minor mode, this
macro will generate various `(put some-extension-mode ...)' statements,
which themselves need to be auto-loaded, as they are used to determine
when to load the extension (including, possibly, at package load time).

[1] Here's the distribution of pure ;;;###autoload forms in the emacs-30
    codebase:

     defun: 3226
     defcustom: 181
     define-minor-mode: 173
     define-derived-mode: 157
     defmacro: 130
     defalias: 101
     put: 88
     defvar: 74
     define-obsolete-function-alias: 27
     defvar-local: 20
     define-overloadable-function: 19
     cl-defgeneric: 19
     defconst: 15
     define-globalized-minor-mode: 15
     progn: 12
     defclass: 8
     add-to-list: 8
     cl-defun: 8
     defsubst: 7
     cl-defmacro: 5
     define-mail-user-agent: 4
     or: 3
     pcase-defmacro: 3
     define-global-minor-mode: 2
     make-variable-buffer-local: 2
     eval-after-load: 2
     dolist: 2
     define-key: 2
     unless: 1
     autoload: 1
     def-edebug-elem-spec: 1
     defvar-keymap: 1
     define-inline: 1
     when: 1
     define-compilation-mode: 1
     define-skeleton: 1
     let: 1
     fset: 1
     setq: 1

   Created with:

   rg -IA 1 -t elisp '^;;;###autoload *$' | perl -ne '$AL{$1}++ if /^\(([^ ]+) /; END { foreach $k (reverse sort { $AL{$a} <=> $AL{$b} } keys %AL) {print "$k: $AL{$k}\n"}}'