Subject: bug#76132: Clojure-style auto-gensyms for macros

From: Tassilo Horn <[email-masked]>
Date: Mon, 10 Feb 2025 08:36:54 +0100

Stefan Monnier <[email-masked]> writes:

Hi Stefan,

>> Why?
>
> I'd ask the author of the code.

;-)

>> I would expect that it's one set of uninterned symbols per expansion.
>
> Try:
>
>     (defmacro my-foo (exp)
>       (with-uninterned-symbols
>        `(let ((x$ 6))
>           (+ x$ ,exp))))
>
> and then check (equal (macroexpand '(my-foo r)) (macroexpand '(my-foo r)))

That's shocking! :-|

> [ Admittedly, this all depends on when `with-uninterned-symbols` is
>   macro-expanded, but in most cases it will be macro-expanded once and
>   for all when the macro is defined.  ]

Well, my naive assumption about macro expansion is that it's done lazily
from inside-out.  In that case, I would assume the macro works as I
intended.  Also nesting should be no problem, because when the outer
with-uninterned-symbols is expanded, any inner with-uninterned-symbols
has already been expanded so its own xs $ have been replaced with #:xs.

I guess you are talking about eager macro expansion, right?  I have no
clue how that works and the docs don't tell either.  Does it mean the
macro is expanded once its defmacro form is reached and the resulting
code is just inserted at its usages?  But if that's the case, how are
the macro arguments injected in the code at the right places?

And furthermore, why does

(defmacro th/my-foo (exp)
  (let ((x (make-symbol "x")))
    `(let ((,x 6))
       (+ ,x ,exp))))

not exhibit the exactly same problem?  I guess it's because "the
machine" knows that every ,foo needs to be once-per-expansion, right?
Or maybe this dependency disables eager macro expansion for this macro
altoghether?

Is there a way to fix my macro?  Maybe to define it so that it would
result in something like

  (let-alist (collect-$-vars exp)
    ;; Now build a replacement for exp which uses ,.x accesses in place
    ;; of x$, e.g., this:
    `(let ((,.x 6))
       (+ ,.x ,exp)))

such that there's a "dependency" of the expansion to an expression
that's evaluated at expansion time which in turn depends on exp?

>> Oh, and obviously you made me curious about those weird corner cases.
>> Please elaborate! ;-)
>
> I wish I could.  I'm starting to wonder if I dreamed it.
> The best I could come up with is:
>
>     (defmacro my-countref-let (var form &rest body)
>       (with-uninterned-symbols
>        `(let ((x$ ,form)
>               (count$ 0))
>           (cl-symbol-macrolet ((,var (progn (cl-incf count$) x$)))
>             (unwind-protect
>                 (progn ,@body)
>               (message "%S ref'd %d times" ',var count$))))))
>
> where (my-countref-let v1 67 (my-countref-let v2 89 (+ v1 v2))) will
> say that `v2` was referenced twice and `v1` zero times (at least if
> that macro is byte-compiled).

Jesus Christ! ;-)

Bye,
Tassilo