Subject: bug#77725: 31.0.50; Add support for types accepted by `cl-typep' to cl-generic?

From: Stefan Monnier <[email-masked]>
Date: Sun, 27 Apr 2025 01:54:55 -0400

> Exactly, I fully agree, the question is "how to keep track of the
> children"?

You could approximate it with `after-load-functions`.  Or, with the
proposed patch, you could use an advice on `cl--type-deftype`.  Once you
do that, you won't need to break circularity in `cl-types-of`.

> I wonder if it would be possible to do something when the type is
> (re)defined? For example, pass the type once defined to some kind of
> "after-defined-hook" that could adjust the environment accordingly.

Maybe more generally we could/should provide "fast" (i.e. cached)
versions of `cl--class-allparents` and `cl--class-children`.
[ And remove those `--`.  ]

>>> This makes me think that errors that might occur when calling
>>> `cl-typep' should be handled in `cl-types-of' to prevent an
>>> incorrect definition from impacting method dispatching on types
>>> correctly defined.
>>> WDYT?
>> Maybe.  I'd tend to think that `cl-typep` should never error, so if
>> it ever does (which would indicate a bug in some `cl-deftype`), we
>> wouldn't want to hide that error.
> Unfortunately, currently `cl-typep' can error, but currently the
> impact is limited to the broken definition.  Here is an example of
> broken definition:
>
> (cl-deftype bad-type ()
>   '(satisfies unknown-function))

But this is bug in the definition of the type.

> But, when any type defined by 'cl-deftype' can also be a method type
> argument, any broken type can also break dispatching of method for
> other types.  So, such errors need to be caught and reported by
> `cl-types-of'.  For now, I updated `cl-types-of' accordingly.

You could also take the position that we shouldn't catch those errors
and instead put pressure to fix those broken `cl-deftype` definitions.

> I am afraid, I still don't see how I can implement this "common
> approach" to break circularity.

I think it would look something like:

    (defvar cl--type-pending nil)

    (defun cl--typep (o type)
      ;; Like `cl-typep` but with recursion-check.
      (let ((entry (cons o type)))
        ;; Note: `member` is not quite right.
        (if (member entry cl--type-pending)
            ;; We're called recursively while in the process of testing
            ;; if `o` is of type `type`, so presume it's true for now,
            ;; rather than inf-loop.
            t
          (let ((cl--type-pending (cons  cl--type-pending)))
            (cl-typep o type)))))

    (defun cl-types-of (o)
      (let ((types ()))
        (dolist (type ...alltypes...)
          (when (cl--typep o type)
            (push type types))))))
        ...types...))

> When I consider that a type "match" before to check, it will still
> match after a cycle is detected, if I understood correctly your
> explanation.  So, a circular type will always match for any objet?

If the type is "trivially circular", yes.  Otherwise, not necessarily.

E.g.

    (cl-deftype is-cons (t1 t2)
      `(and cons (satisfies
                  ,(lambda (x)
                     (and (cl--typep (car x) t1) (cl--typep (car x) t2))))))

    (cl-deftype list-of-int ()
      `(is-cons integer (or null list-of-int)))

and then

    (let ((x (list 1 2 3 4 5)))
      (setcdr (last x) x)
      (cl--typep x 'list-of-int))

would terminate and return t but

    (let ((x (list "1" "2" "3" "4" "5")))
      (setcdr (last x) x)
      (cl--typep x 'list-of-int))

would return nil.

> Please find attached a new simpler and cleaner version of
> cl-types-test.el.  All tests pass for me :-)

Looks good, thanks.

> Also attached my last version of cl-types.el.  For now, I prefer to
> work on separate files.  Based on your previous proposal, it should
> not be difficult to merge the code in existing libraries when/if we
> are satisfied of the implementation ;-)

It would be good to check that the split I proposed works correctly, tho.
The main issues are likely to be around whether things are autoloaded
when they to.

        Stefan