Clozure / ccl

Clozure Common Lisp
http://ccl.clozure.com
Apache License 2.0
855 stars 103 forks source link

typep, gensym and compile #352

Closed digikar99 closed 3 years ago

digikar99 commented 3 years ago

I'm on (lisp-implementation-version) ;=> "Version 1.12 (v1.12) LinuxX8664".


POSSIBLE ISSUE 1

(defparameter gs1 (gensym))
(defun #.gs1 (a) (declare (ignore a)) t)
(fdefinition gs1) ;=> undefined function if previous form was C-c C-c; works fine if it was C-x C-e with SLIME

POSSIBLE ISSUE 2

The previous issue can be avoided by using compile (thanks to phoe for the reminder!). So,

(defparameter gs1 (gensym))
(compile gs1 `(lambda (a) (declare (ignore a)) t))
(fdefinition gs1) ; works regardless of whether the previous form was C-c C-c or C-x C-e
(deftype foo () `(satisfies ,gs1))
(defun bar11 (a) (typep a 'foo)) 
(bar11 5) ;=> This works and returns T if the previous form was C-x C-e; otherwise errors with the below backtrace.

Backtrace:

Undefined function #:G9911 called with arguments (5) .
   [Condition of type CCL::UNDEFINED-FUNCTION-CALL]

Restarts:
 0: [CONTINUE] Retry applying #:G9911 to (5).
 1: [USE-VALUE] Apply specified function to (5) this time.
 2: [STORE-VALUE] Specify a function to use as the definition of #:G9911.
 3: [RETRY] Retry SLIME interactive evaluation request.
 4: [*ABORT] Return to SLIME's top level.
 5: [ABORT-BREAK] Reset this thread
 --more--

Backtrace:
  0: (BAR11 #<Unknown Arguments>)
  1: (CCL::CHEAP-EVAL (BAR11 5))
  2: ((:INTERNAL SWANK:INTERACTIVE-EVAL))

Other similar functions, and those that do not involve gensyms work fine regardless of C-c C-c or C-x C-e:

(defun bar12 (a) (typep a (trivial-types:type-expand 'foo)))
(defun bar13 (a) (typep a `(satisfies ,gs1)))

(defun gs2 (a) (declare (ignore a)) t)
(deftype foo2 () `(satisfies gs2))
(defun bar2 (a) (typep a 'foo2))

(bar12 5) ;=> T
(bar13 5) ;=> T
(bar2 5) ;=> T
svspire commented 3 years ago

Using an uninterned symbol as a function name is very unusual. But assuming you really need to do this try wrapping the first line in an eval-when and try again:

(eval-when (:compile-toplevel :load-toplevel :execute)
  (defparameter gs1 (gensym)))

There is no real difference in CCL between evaluation and compilation (everything in CCL is compiled) other than the binding environments in place at the time. That's why eval-when is probably the solution here.

If you are new to Common Lisp I'd gently suggest that using an uninterned symbol as a function name is a bad idea, and most likely an anonymous function is what you want instead.

phoe commented 3 years ago

The issue here is that you can't have anonymous satisfies functions and a gensymed function is the closest you can do to emulate such behavior.

svspire commented 3 years ago

Yes indeed you're right. I had forgotten about that.

digikar99 commented 3 years ago

If you mean something like this:

(eval-when (:compile-toplevel :load-toplevel :execute)
  (defparameter gs1 (gensym)))
(defun #.gs1 (a) (declare (ignore a)) t)
(deftype foo () `(satisfies #.gs1))
(defun bar11 (a) (typep a 'foo))

then, the results are the same - if I C-c C-c each form one-by-one then I get the same error; if I C-x C-e each form one-by-one, I do get the expected T.

Hmm, a bit more pinpointing, again, each form, one-by-one (I guess without that there can be a separate read/compile error due to #., but that's a separate issue as far as I understand):

(defparameter gs1 (gensym))
(defun #.gs1 (a) (declare (ignore a)) t)
(fdefinition gs1) ;=> undefined function if previous form was C-c C-c; works fine if it was C-x C-e

an uninterned symbol as a function name is a bad idea, and most likely an anonymous function is what you want instead.

I'm looking to emulate (container element-type) using satisfies - and gensyms. Not sure if there's a better way.

phoe commented 3 years ago

I emulate the same thing using named predicates interned in a separate scratch package in phoe-toolbox.

(defpackage #:phoe-toolbox/list-of (:use))

(deftype list-of (type)
  "A type specifier that matches a list whose all elements are of the given
type."
  (check-type type symbol) ; Kludge - maybe we'll fix that later.
  (let* ((package-name (package-name (symbol-package type)))
         (symbol-name (symbol-name type))
         (predicate-name (format nil "LIST-OF ~S ~S" package-name symbol-name))
         (package (find-package '#:phoe-toolbox/list-of))
         (predicate (intern predicate-name package)))
    (setf (fdefinition predicate)
          (lambda (list) (every (rcurry #'typep type) list)))
    `(satisfies ,predicate)))

Because this is somewhat dirty, I'm also curious if you come up with a portable solution that uses gensyms.

svspire commented 3 years ago

I'd probably address this problem with #'assert.

(defun foo (a)
  (assert (and (typep a 'array)
               (eql 'single-float (array-element-type a))))
  (print "Success!"))

(setq x (make-array '(3 5) :element-type 'single-float))
(foo x) --> Success!
(setf y (make-array 10))
(foo y) --> error

Of course the disadvantage of #'assert is that it's purely a runtime check, so the compiler won't be able to reason about it in advance. But if you're checking types in a container when that container is a list (as opposed to an array), no Lisp compiler will be able to reason about it anyway, i.e. it's always going to involve a runtime check.

digikar99 commented 3 years ago

Because this is somewhat dirty, I'm also curious if you come up with a portable solution that uses gensyms.

I don't get how that hampers portability; may be this is a bit more portable, with trivial-types:type-expand and caching things in a hash-table; not sure:

(deftype array (&optional (element-type '* elt-supplied-p))
  (if elt-supplied-p
      (let ((element-type (type-expand (upgraded-array-element-type element-type))))
        (unless (gethash element-type *element-type->checker-fn*)
          (let ((fn-sym (make-symbol (concatenate 'string
                                                  "ARRAY-"
                                                  (let ((*package* (find-package :cl)))
                                                    (write-to-string element-type))))))
            (compile fn-sym
                     `(lambda (array)
                        (and (arrayp array)
                             (type= ',element-type (array-element-type array)))))
            (setf (gethash element-type *element-type->checker-fn*) fn-sym)))
        `(satisfies ,(gethash element-type *element-type->checker-fn*)))
      'dense-array))

About caching in the hash-table, seeing your example I recalled that SBCL only seems to expand the types once. Even CCL does it just once; however, just checked, ECL does it as many times as needed; not sure what CLHS says about this.

The array here is in a different package and is intended as a replacement; but that's another separate issue.


Of course the disadvantage of #'assert is that it's purely a runtime check ...

Indeed! However, I also intend to provide some compiler-macros that's take advantage of the user-provided declarations in optimizing user-provided code - or signalling to the user why their code cannot be optimized. On SBCL, I'll need to check to what extent sb-c:deftransform can do it; is there an equivalent of sb-c:deftransform on CCL?


Also, this seems to be getting off-topic; may be we should continue at some other place, eg. https://www.reddit.com/r/lisp/comments/k6sjbm/monthly_whats_on_your_mind_discussion_thread/?

phoe commented 3 years ago

The main issue is that gensyms are not externalizable while maintaining identity. If you store a gensym into a FASL and then load the FASL back, then the resulting gensym is going to be similar but not identical to the original, meaning that all eq checks - including funcalls of the previous gensym - will fail.

You could try avoiding this issue by using make-load-form for the symbol instead, or a hash table at runtime, but AFAIK this is not a CCL-specific issue because the original code is not guaranteed to work portably.