Subject: Re: bug#54399: 27.2; Problems with (let ((custom-variable ...)) (autoload-function ...))

From: Ignacio Casso <[email-masked]>
Date: Tue, 12 Apr 2022 11:13:27 +0200

> I think custom.el already uses `set-default-toplevel-value' where
> appropriate by default. So my request is (1) to correct the docstrings
> in custom.el to reflect so, so that users know to use it instead of
> `set-default', and (2) Add some warnings somewhere, although I'm not
> sure where.
> 
> I personally can not think of a single case in which someone would want
> to use `set-default' instead of `set-default-toplevel-value'. If I
> understand them correctly, they both do the same outside a let binding,
> and I don't see why someone would want the `set-default' behavior inside
> the let binding. In fact, I guess most people assume that `set-default'
> behaves like `set-default-toplevel-value' (I did at least).
> 
> So I would at least talk about this in the docstrings of `set-default',
> and also `default-value' and `default-boundp' which suffer the same
> problem. In fact, now that I see it, the docstring of the later is just
> wrong. The others just don't mention let bindings and only talk about
> buffer-local bindings, but that one explicitly says that the function
> can be used to know if a variable has a non-void value outside of a
> let-binding, and with dynamic binding that doesn't work (see snippet
> below).
> 
>   (setq lexical-binding nil)
>   (let ((another-fresh-var 1))
>     (default-boundp 'another-fresh-var)) ;; I expect nil, it returns t

Hello,

I was revisiting this bug report and writing a patch to correct and
update some docstrings, both in custom.el and for `default-value',
`set-default' and `default-boundp'. But for the last three, I'm no
longer sure if the errors are in the implementation or the docstrings,
since I have found more strange cases while experimenting. In a few
words, those functions behave differently inside let bindings depending
on whether the current buffer has or not a local value for the variable,
which I find a little bit inconsistent. If it has, they behave as they
"toplevel" counterparts (`default-toplevel-value',
`set-default-toplevel-value'). If they don't, they behave as I explained
in previous emails. I describe those cases below, with code snippets and
comments. Note that the behavior also depends on whether lexical binding
is enabled or not. I use dynamic binding in these examples.

;;;; `default-value'

;; default defined, buffer-local undefined
(defvar var1 "default")
(let ((var1 "inside let")) (default-value 'var1)) ;; returns "inside let"

;; default defined, buffer-local defined
(defvar var2 "default")
(setq-local var2 "buffer-local")
(let ((var2 "inside let")) (default-value 'var2)) ;; returns "default"

;; default undefined, buffer-local undefined
(let ((var3 "inside let")) (default-value 'var3)) ;; returns "inside let"

;; default undefined, buffer-local defined
(setq-local var4 "buffer-local")
(let ((var4 "inside let")) (default-value 'var4)) ;; void-variable error

;;;; `default-boundp'

;; default defined, buffer-local undefined
(defvar var5 "default")
(let ((var5 "inside let")) (default-boundp 'var5)) ;; returns t

;; default defined, buffer-local defined
(defvar var6 "default")
(setq-local var6 "buffer-local")
(let ((var6 "inside let")) (default-boundp 'var6)) ;; returns t

;; default undefined, buffer-local undefined
(let ((var7 "inside let")) (default-boundp 'var7)) ;; returns t

;; default undefined, buffer-local defined
(setq-local var8 "buffer-local")
(let ((var8 "inside let")) (default-boundp 'var8)) ;; returns nil

;;;; `set-default'

;; default defined, buffer-local undefined
(defvar var9 "default")
(let ((var9 "inside let"))
  (set-default 'var9 "new-default")
  var9)                                          ;; returns "new-default"
var9                                             ;; returns "default"
(default-value 'var9)                            ;; returns "default"

;; default defined, buffer-local defined
(defvar var10 "default")
(setq-local var10 "buffer-local")
(let ((var10 "inside let"))
  (set-default 'var10 "new-default")
  var10)                                         ;; returns "inside let"
var10                                            ;; returns "buffer-local"
(default-value 'var10)                           ;; returns "new-default"

;; default undefined, buffer-local undefined
(let ((var11 "inside let"))
  (set-default 'var11 "new-default")
  var11)                                         ;; returns "new-default"
var11                                            ;; void-variable error
(default-value 'var11)                           ;; void-variable error

;; default undefined, buffer-local defined
(setq-local var12 "buffer-local")
(let ((var12 "inside let"))
  (set-default 'var12 "new-default")
  var12)                                         ;; returns "inside let"
var12                                            ;; returns "buffer-local"
(default-value 'var12)                           ;; returns "new-default"

Should I go ahead and just update the docstrings so that they reflect
this behavior, and then close this bug report? Or do you think that the
code should be changed too? For the later I don't think I could help,
since that code is too low level for me.