freenode/#sicl - IRC Chatlog
Search
8:17:25
beach
So if we use CHECK-TYPE, for reasons of internationalization, we must omit the STRING argument.
8:18:26
beach
If additional information is required, we would have to include it in the type instead.
8:22:40
heisig
Yes, more specific type specifiers are much better than any kind of string, even without internationalization.
8:26:56
beach
I am also tempted to try to include source information in the error. Then, if we are in an IDE, the PLACE argument to check-type would be highlighted, rather than the name appearing in an error message.
8:27:51
beach
There are lots of situations like this where I want to rethink what traditional Common Lisp implementations do, and adapt the behavior to better tools like an IDE or a real debugger.
8:28:44
beach
Something similar to what traditional Common Lisp implementations do would also have to be included, in case someone has the bad idea to run it from a terminal.
8:29:16
beach
But I want to think about the consequences to the compiler for both these situations to be possible.
8:32:09
beach
The highlighting idea is advantageous for two reasons. First, usually, the message would have symbols in upper case, whereas the code would typically be lower case. Second, there may be several places (typically variables) with the same name in different scopes, By highlighting the right one, the message becomes easier to understand.
8:36:23
beach
Another thing I want to do is to "hide" the use of Acclimation. Currently, condition reporters are necessarily separate from the definition of the condition, but if we use external libraries, that is not always going to be the case. So I want to figure out a way to make the condition reporter in the definition be the English-language one, while still allowing for the *LANGUAGE* variable to be changed.
8:38:06
no-defun-allowed
Sometimes I use the string argument to CHECK-TYPE for "intentional" types, but a suitable alias for the type would be better still.
8:38:23
beach
Another reason I want to "hide" it is that if SICL contains modules with condition definitions in it, and those modules are independently useful, I don't want to impose Acclimation on other users. By re-including the English-language condition reporter in the condition definition, it will be easier for others to use the code.
8:39:35
beach
no-defun-allowed: Right. Since there are several ways of expressing a particular type, we can use specific variants to generate specific messages.
10:24:18
pjb
beach: well, your reticences are consistent with my advice of using check-type on functions close to the user, but not inner functions. No implementation use check-type for CAR: https://termbin.com/fu7mo
12:25:04
beach
Using CHECK-TYPE on CAR would be problematic, because I need the test for CONSP up front so that it will almost always succeed, and so that can be eliminated by type inference.
12:26:54
beach
I wonder whether there might be a better expansion of the CHECK-TYPE macro that would take things like that into account.
12:28:44
no-defun-allowed
So the expansion would look like (unless (consp x) (setf x (handle-check-type-failure x 'cons)))?
12:29:11
beach
So CAR would look like this: (defun car (x) (check-type ...) (if (consp x) (primop:cons-car x) x))
12:37:01
beach
So the combination of check-type and (if (consp x) ...) must result in a single test.
12:37:43
pjb
Now, check-type could expand to something like (loop :named check (with-restart ((store-value …)) (if (or (consp k) (null k)) (return-from check) (invoke-debugger (make-condition 'type-error …))))); but the point is that the post condition of check-type is that (typep x '(or cons null)) and that check-type contains already a test for (or (consp x) (null x)) so data flow analysis could restructure the code to have
12:37:44
pjb
only one test and jump from the test in check-type to the alternative of the car function.
12:38:29
pjb
Note that eg. ccl expands check-type as (do* ((#:g7903 x x)) ((typep #:g7903 'integer)) (setf x (ccl::%check-type #:g7903 'integer 'x nil)))
12:40:32
pjb
beach: well the tests on the type exist in the body already, so to avoid the bloat, we only need to factor out the handling of the error and restart.
12:41:48
pjb
So, something like: (defmacro check-type (place type) `(unless (typep ,place ',type) (setf ,place (%check-type ,place ',type))))
12:42:21
pjb
or with a THE: (defmacro check-type (place type) `(unless (typep ,place ',type) (setf ,place (the ,type (%check-type ,place ',type)))))
12:42:24
beach
Yes, something like that. And then the subsequent test will be eliminated through path replication.
12:44:18
pjb
Now I assume you wouldn't call this car, but directly primop:cons-car if it is known at compilation-time that we have a cons cell.
12:44:42
pjb
(lambda (k) (declare (type cons k)) (car k)) would compile as (lambda (k) (primop:cons-car k)) right?
12:47:33
beach
OK, so (defun car (x) (check-type x (or consp null)) (if (consp x) (primop:car x) x)) will turn into:
12:47:34
beach
(defun car (x) (unless (typep x '(or cons null) (%check-type....))) (if (consp x) (primop:car x) x))
12:50:33
beach
TYPEP will turn into (or (consp x) (null x)) and it will be compiled in a context where a Boolean is required.
12:52:34
beach
Path replication can turn into something that can't be expressed in source code. That's the point.
12:55:07
beach
It is possible that the test in the body must be re-done after a call to %check-type, but that's fine. It's a very slow path anyway, and it will be eliminated when the argument is known to be CONS or NULL.
12:56:15
pjb
beach: Using THE on the result let us avoid half the test. Here we need to test for consp to avoid a possible NIL. But in general, we should have more specific types.
12:56:46
beach
It will be more specific in that it is either CONS or NULL, but we don't know which one.
12:59:06
beach
So the result will be something like (if (consp x) (primop:car x) (if (null x) x (progn (do (setf x (%check-type ...))) (if (consp x) (primop:car x) x))))
13:17:06
beach
(if (consp x) (primop:car x) (if (null x) x (progn (setf x (%check-type ...)) (if (consp x) (primop:car x) x))))
13:24:42
pjb
yes, there's the loop inside %check-type, but I thought after %check-type we can jump back (without a loop since this time we know we have the right type) just to dispatch.
13:24:44
beach
Am I reading the Common Lisp HyperSpec right that the subforms of the place are evaluated a second time if the type test fails?
13:25:24
pjb
(tagbody :switch (if (consp x) (return (primop:car x)) (if (null x) (return x) (progn (setf x (%check-type ...)) (go :switch)))))
13:27:46
pjb
But the check-type look doesn't change the place. And %check-type is a function that takes the old value and return the new value. Hence the surrounding (setf x …).
13:28:19
pjb
It's x in the expansion of check-type in car, but in the expansion in other cases, it would just be the temp variables obtained from get-setf-expansion.
13:43:37
pjb
as you can see, works even with place expression containg (incf i): https://termbin.com/931i
13:44:58
pjb
I assume the let and setq in the expansion for a simple parameter variable s not a problem: https://termbin.com/nr4k
13:48:02
pjb
Basically, it computes all the subexpressions into temporary variables, so it can read and write the place with simple getter and setter operations.
13:49:02
beach
I am saying, I don't see how you can avoid a loop in the expansion of CHECK-TYPE when the place is something other than a simple variable. Because the setter of place can have side effects and those side effects must be executed for each iteration.
13:50:26
beach
So when the place is (f (g x)), you need to call the setf function in each iteration.
13:51:07
pjb
For one thing, you may not be able to store a value of the wrong type in a place (say a slot in a vector). (but granted this is a feable argument since the bad value already comes from the place).
13:51:31
pjb
But I don't think it's specified that check-type must store several times vs. just one with the right value at the end.
13:51:31
beach
Also, the Common Lisp HyperSpec clearly says that the value must be stored in the place.
13:52:23
Colleen
Clhs: macro check-type http://www.lispworks.com/documentation/HyperSpec/Body/m_check_.htm
13:52:59
beach
"If the store-value restart is invoked, check-type stores the new value that is the argument to the restart invocation (or that is prompted for interactively by the debugger) in place and starts over."
13:54:02
beach
So you need to expand to a loop when the place is something complex, and to a function containing the loop otherwise.
13:54:19
pjb
beach: the setter from get-setf-expansion doesn't remove the side effects this setter may have. ie. if you have an accessor with side effects, this will occur. What won't occur, is the side effects from the subexpressions used to find this slot. And this is what is specified in clhs check-type:
13:54:38
pjb
"The first time place is evaluated, it is evaluated by normal evaluation rules. It is later evaluated as a place if the type check fails and the store-value restart is used; see Section 5.1.1.1 (Evaluation of Subforms to Places)."
13:55:15
beach
The problem is that the setter for the place can have side effects so that is must be called in each iteration.
13:56:12
pjb
"check-type stores the new value that is the argument to the restart invocation (or that is prompted for interactively by the debugger) in place and starts over".
13:57:18
beach
It's not a big deal. It can be dealt with. With two different expansions based on the setf expansion of the place.
13:57:28
pjb
The problem I see, is that if we consider a standard-object typed slot, there will be a type check in the writer of the slot, with a type-error signaled.
14:00:40
pjb
There are two types involved. Let's consider: (defclass c () ((s :initarg :s :type (unsigned-byte 8) :accessor s))) (let ((o (make-instance 'c :s 200))) (check-type (s o) (integer 100)) o)
14:01:23
pjb
I mean let's consider: (defclass c () ((s :initarg :s :type (unsigned-byte 8) :accessor s))) (let ((o (make-instance 'c :s 200))) (check-type (s o) (integer 0 100)) o)
14:02:26
pjb
So the slot is of type (integer 0 255) has a value of 200. valid for the class. But not for the check-type. But now we enter the value "hello" as new-value for the store-value restart. So check-type will store "hello" in the slot, and the accessor s will have to signal a type error.
14:04:07
pjb
So I find the specification to store each new value in the place to be problematic. A better specification would allow to store only the last value of the right type.
14:04:08
beach
Interesting stuff. It would be equally interesting to see how existing implementations behave in cases like this.
14:14:34
pjb
there are other macros were they're careless about places… for example (assert (equal (aref v (decf i)) (aref v (decf i))) ((aref v (incf i)) (aref v (incf i)))) https://termbin.com/memy
14:14:59
beach
Actually, this is even more complicated. (typep x '(or cons null)) can't be expanded by a compiler macro to (or (consp x) (null x)) unless x is just a simple variable. If it is a symbol macro that expands to a complex place, then we would introduce multiple evaluations of subforms.
14:16:06
beach
It could be an ELS2022 paper, because it would require checking all the implementations.
14:35:10
pjb
So yes, we need the loop outside the test and handling of the restart case for check-type :-(
14:37:10
pjb
But in the case of CAR and similar functions, wouldn't this loop be free? since we'd have a go back to :switch to reuse the tests: (tagbody :switch (if (consp x) (return (primop:car x)) (if (null x) (return x) (progn (setf x (%check-type ...)) (go :switch)))))
14:38:05
pjb
Otherwise, if check-type is problematic, we can have an internal check-type with different constraints. For example, we can specify to store only once.
14:39:31
beach
Sure, I wanted to check whether there was a way to use normal CHECK-TYPE with CAR/CDR without introducing any performance penalties.
14:52:21
beach
I shouldn't be so pessimistic ("it may not be worth the effort"). There is definitely some value in creating a good CHECK-TYPE macro that takes into account the specific case when the place is a simple variable.
14:53:06
beach
And there is definitely some value in creating a good compiler macro for TYPEP that does the same. The two are independent, so that's a good thing.
14:55:44
beach
For the compiler macro, we need Bike's SUBTYPEP I would think. And we need to recognize the types that are easy to test in sicl, namely SINGLE-FLOAT, CHARACTER, CONS, FIXNUM, and STANDARD-OBJECT. The last one being (NOT (OR <the others>)).
15:13:26
beach
I suppose, as pjb pointed out, that the compiler macro for TYPEP can use a temporary variable, introduced by LET. But then we also need (simple) value numbering to conclude the type of the original place.
15:15:49
beach
So then I guess, my decision should be based on whether I will accept that the first native executable of SICL will be unbearably slow, because we used CHECK-TYPE in CAR/CDR but didn't provide the infrastructure to make it reasonably fast, or whether to special-case CAR/CDR the way we now do, and remove the special case once we have the rest of the infrastructure.
16:02:53
beach
WAIT A SECOND. I should use probably ETYPECASE for CAR/CDR. Or possibly CTYPECASE. But CTYPECASE may just turn into CHECK-TYPE or something similar, of course.
16:05:47
pjb
you'd use check-type again, for the user interface it provides. I'm still not sure it's good or useful to use it from low-level functions. (because they don't hav access to the places where the user data is stored, only to internal parameters). But if you want to benefit from this user interface, you can indeed do it only in the exceptional case. eg.: (tagbody :agan (typecase (cons (primop:cons-car x)) (null nil) (t (check-ty
16:08:23
beach
But this discussion started by my wanting CHECK-TYPE because it is standard, but not wanting it because it signals a correctable error. So etypecase may be preferable.
16:12:51
pjb
Nope, I'm wrong: (defmacro check-type (place type &optional message) `(ctypecase ,place (,type nil))) wouldn't work, because check-type issues the optional message which ctypecase doesn't do.
16:13:41
pjb
for all type checking when a type error must be signaled, etypecase seems to be the right operator.
16:13:57
beach
And this entire discussion depends on how type inference is implemented. I mean, if I define CAR to use the functions CONSP and NULL, then there is no issue. But with CHECK-TYPE, ETYPECASE, and CTYPECASE, then I need to have a plan for turning TYPEP into something that can be checked at compile time.
16:14:39
beach
And this plan depends on why Bike declared failure of the type inference in Cleavir v1.
16:16:29
beach
dukester: But I bet you don't know CLOS, and I bet you don't like object-oriented programming because of the particular style traditional languages define it.