The following file causes a problem in ABCL (1.1.0-dev-svn-14041) when compiled (!) and loaded.
(in-package cl-user)
(defstruct a (s1 nil))
(defstruct (b (:include a) (:conc-name foo-)) (s2 nil))
(defstruct (c (:include a) (:conc-name foo-)) (s3 nil))
(defun test () (let ((x (make-b :s1 1 :s2 2))) (foo-s1 x)))
CL-USER(4): (test) #<THREAD "interpreter" {2EF7D41F}>: Debugger invoked on condition of type SIMPLE-TYPE-ERROR The value #<B {564434F7}> is not of type C. Restarts: 0: TOP-LEVEL Return to top level. [1] CL-USER(6): (lisp-implementation-version) "1.1.0-dev-svn-14041" [1] CL-USER(7):
The problem is that the defstruct declaration for c "overwrites" the accessor foo-s1 generated by defstruct b. If foo-s1 is called for a b instance, the type assertions introduced by define-reader (and define-writer, see the ABCL implementation for defstruct) cause the error described above.
The code above runs fine in ACL, Lispworks, SBCL, and other Lisps. I had to remove the generation of type assertions in define-reader and define-accessor (see below) to make the code work in ABCL. Note, however, that there is no type checking pursued with the code below. Thus, a better solution might be developed.
Regards, Ralf Moeler http://www.sts.tu-harburg.de/~r.f.moeller/
(defun define-reader (slot) (let ((accessor-name (dsd-reader slot)) (index (dsd-index slot)) (type (dsd-type slot))) (cond ((eq *dd-type* 'list) `((declaim (ftype (function * ,type) ,accessor-name)) (setf (symbol-function ',accessor-name) (make-list-reader ,index)))) ((or (eq *dd-type* 'vector) (and (consp *dd-type*) (eq (car *dd-type*) 'vector))) `((declaim (ftype (function * ,type) ,accessor-name)) (setf (symbol-function ',accessor-name) (make-vector-reader ,index)) (define-source-transform ,accessor-name (instance) `(aref ,instance ,,index)))) (t `((declaim (ftype (function * ,type) ,accessor-name)) (setf (symbol-function ',accessor-name) (make-structure-reader ,index ',*dd-name*)) (define-source-transform ,accessor-name (instance) ,(if (eq type 't) ``(structure-ref ,instance ,,index) ``(the ,',type (structure-ref ,instance ,,index)))))))))
(defun define-writer (slot) (let ((accessor-name (dsd-reader slot)) (index (dsd-index slot))) (cond ((eq *dd-type* 'list) `((setf (get ',accessor-name 'setf-function) (make-list-writer ,index)))) ((or (eq *dd-type* 'vector) (and (consp *dd-type*) (eq (car *dd-type*) 'vector))) `((setf (get ',accessor-name 'setf-function) (make-vector-writer ,index)) (define-source-transform (setf ,accessor-name) (value instance) `(aset ,instance ,,index ,value)))) (t `((setf (get ',accessor-name 'setf-function) (make-structure-writer ,index ',*dd-name*)) (define-source-transform (setf ,accessor-name) (value instance) `(structure-set ,instance ,,index ,value)))))))
Hi Ralf,
On Fri, Aug 3, 2012 at 3:01 PM, Ralf Moeller moeller@tu-harburg.de wrote:
The following file causes a problem in ABCL (1.1.0-dev-svn-14041) when compiled (!) and loaded.
Thanks for the report. I've been thinking about it and I think the best solution is even easier to code: when an included DEFSTRUCT already defines the same accessor, we should simply not define it again: the result will be that the accessor checks for structs of type 'A and all of its subtypes will be allowed.
I'll log a ticket to that extent and try to come up with the right change to defstruct.lisp. However, if you can submit a patch to that extent, that'd be most appreciated!
Thanks again,
Erik.
On Aug 7, 2012, at 1:51 PM, Erik Huelsmann ehuels@gmail.com wrote:
Hi Ralf,
On Fri, Aug 3, 2012 at 3:01 PM, Ralf Moeller moeller@tu-harburg.de wrote:
The following file causes a problem in ABCL (1.1.0-dev-svn-14041) when compiled (!) and loaded.
Thanks for the report. I've been thinking about it and I think the best solution is even easier to code: when an included DEFSTRUCT already defines the same accessor, we should simply not define it again: the result will be that the accessor checks for structs of type 'A and all of its subtypes will be allowed.
I thought about this, but the problem with my example (repeated here for convenience)
(defstruct a (s1 nil))
(defstruct (b (:include a) (:conc-name foo-)) (s2 nil))
(defstruct (c (:include a) (:conc-name foo-)) (s3 nil))
is that, if we focus on defstruct "c", it is not the included defstruct "a" which defines foo-s1 but it is defstruct "b", which is not related to "c" at all. Thus, in a naive view, the code that generates "c" has to have a look at all substructs of those which are (directly or indirectly) included into "c" (possibly going up and down in the hierarchy several times). I can hardly imagine that this idea will lead to a stable implementation. So, the code for "b" must somehow store in the management info for defstruct "a" that there is a successor function foo-s1 for a's slot s1 (and the accessor is called foo-s1). This slot accessor is not to be generated again for "c", of course, as you said. The strange thing this is that, in principle, foo-s1 is not applicable to instances of "a", but to instances of "b" and "c" only. Since new sub-structures of "a" (with conc-name foo-) can be defined at any time, the type for the applicable structures used in the implementation of foo-s1 cannot be statically encoded but must be dynamically determined, or the code for foo-s1 could be dynamically adapted for every new sub-defstruct of "a" with conc-name foo-. I tend to believe this is not easy to implement.
Best, Ralf
I'll log a ticket to that extent and try to come up with the right change to defstruct.lisp. However, if you can submit a patch to that extent, that'd be most appreciated!
Thanks again,
Erik.
Thanks for the report. I've been thinking about it and I think the best
solution is even easier to code: when an included DEFSTRUCT already defines the same accessor, we should simply not define it again: the result will be that the accessor checks for structs of type 'A and all of its subtypes will be allowed.
I thought about this, but the problem with my example (repeated here for convenience)
(defstruct a (s1 nil))
(defstruct (b (:include a) (:conc-name foo-)) (s2 nil))
(defstruct (c (:include a) (:conc-name foo-)) (s3 nil))
is that, if we focus on defstruct "c", it is not the included defstruct "a" which defines foo-s1 but it is defstruct "b", which is not related to "c" at all. Thus, in a naive view, the code that generates "c" has to have a look at all substructs of those which are (directly or indirectly) included into "c" (possibly going up and down in the hierarchy several times). I can hardly imagine that this idea will lead to a stable implementation. So, the code for "b" must somehow store in the management info for defstruct "a" that there is a successor function foo-s1 for a's slot s1 (and the accessor is called foo-s1).
Ah. I thought the problem was in the foo-s1 accessor being generated multiple times. Sorry I missed the important bit where the definition of A wasn't also defining the :nconc-name FOO-.
This slot accessor is not to be generated again for "c", of course, as you said. The strange thing this is that, in principle, foo-s1 is not applicable to instances of "a", but to instances of "b" and "c" only. Since new sub-structures of "a" (with conc-name foo-) can be defined at any time, the type for the applicable structures used in the implementation of foo-s1 cannot be statically encoded but must be dynamically determined, or the code for foo-s1 could be dynamically adapted for every new sub-defstruct of "a" with conc-name foo-. I tend to believe this is not easy to implement.
Actually, it's really easy to implement. But the runtime cost of doing so is huge, unfortunately: we can simply use DEFMETHOD to define the accessors. That will automatically notice the accessor is already defined and add a method for each additionally defined type.
But, the performance of a method is by no means as efficient as the current method.
You're talking about adding types to be checked for dynamically to the current accessors. That's indeed far from trivial: the defstruct generates inline expansions. These expansions would have to be "dynamically changeable".
As far as your change to remove the type checks goes: The type checks were added by me a few years ago exactly because the 'type-check-less' versions didn't catch errors in my code. I mean to say: the original ones were without type checks.
I see multiple options:
1. Remove the type checks in the inline expansions and use DEFMETHOD for the accessor functions, where we need to use the methods only with sufficiently high SAFETY declarations 2. Use your patch to remove the type checks completely 3. Come up with a way to add dynamic type checks to the inline expansions and function definitions (ie not methods) - possibly executed conditionally with safety settings. 4. Do nothing (not saying it's a good option, but it's always an option)
Do you see any others? I'm not sure about the performance impact of dynamic checks, but I do think that if we decide to apply a caching strategy, using the slow-path of updating the cache in case of a type-mismatch, the impact may not be much worse than it is today. After all, the biggest change would be to generate code which allows to check more than one type instead of the current single type.
Bye,
Erik.
Am 07.08.2012 um 14:58 schrieb Erik Huelsmann ehuels@gmail.com:
Thanks for the report. I've been thinking about it and I think the best solution is even easier to code: when an included DEFSTRUCT already defines the same accessor, we should simply not define it again: the result will be that the accessor checks for structs of type 'A and all of its subtypes will be allowed.
I thought about this, but the problem with my example (repeated here for convenience)
(defstruct a (s1 nil))
(defstruct (b (:include a) (:conc-name foo-)) (s2 nil))
(defstruct (c (:include a) (:conc-name foo-)) (s3 nil))
is that, if we focus on defstruct "c", it is not the included defstruct "a" which defines foo-s1 but it is defstruct "b", which is not related to "c" at all. Thus, in a naive view, the code that generates "c" has to have a look at all substructs of those which are (directly or indirectly) included into "c" (possibly going up and down in the hierarchy several times). I can hardly imagine that this idea will lead to a stable implementation. So, the code for "b" must somehow store in the management info for defstruct "a" that there is a successor function foo-s1 for a's slot s1 (and the accessor is called foo-s1).
Ah. I thought the problem was in the foo-s1 accessor being generated multiple times. Sorry I missed the important bit where the definition of A wasn't also defining the :nconc-name FOO-.
This slot accessor is not to be generated again for "c", of course, as you said. The strange thing this is that, in principle, foo-s1 is not applicable to instances of "a", but to instances of "b" and "c" only. Since new sub-structures of "a" (with conc-name foo-) can be defined at any time, the type for the applicable structures used in the implementation of foo-s1 cannot be statically encoded but must be dynamically determined, or the code for foo-s1 could be dynamically adapted for every new sub-defstruct of "a" with conc-name foo-. I tend to believe this is not easy to implement.
Actually, it's really easy to implement.
I am afraid, using defmethod for the non-optimized case is not that easy because not all defstructs are necessarily compiled with the same optimization settings (e.g., they might be located in different files). In addition, one has to remove methods if a defstruct is recompiled with other slots or with different optimization settings..... :-(
But the runtime cost of doing so is huge, unfortunately: we can simply use DEFMETHOD to define the accessors. That will automatically notice the accessor is already defined and add a method for each additionally defined type.
But, the performance of a method is by no means as efficient as the current method.
You're talking about adding types to be checked for dynamically to the current accessors. That's indeed far from trivial: the defstruct generates inline expansions. These expansions would have to be "dynamically changeable".
As far as your change to remove the type checks goes: The type checks were added by me a few years ago exactly because the 'type-check-less' versions didn't catch errors in my code. I mean to say: the original ones were without type checks.
I see multiple options:
- Remove the type checks in the inline expansions and use DEFMETHOD for the accessor functions, where we need to use the methods only with sufficiently high SAFETY declarations
- Use your patch to remove the type checks completely
- Come up with a way to add dynamic type checks to the inline expansions and function definitions (ie not methods) - possibly executed conditionally with safety settings.
- Do nothing (not saying it's a good option, but it's always an option)
Do you see any others? I'm not sure about the performance impact of dynamic checks, but I do think that if we decide to apply a caching strategy, using the slow-path of updating the cache in case of a type-mismatch, the impact may not be much worse than it is today. After all, the biggest change would be to generate code which allows to check more than one type instead of the current single type.
Bye,
Erik.
Hi Ralf,
Sorry for the late response. I've been really busy improving some other parts of ABCL which consumed all my time.
This slot accessor is not to be generated again for "c", of course, as you
said. The strange thing this is that, in principle, foo-s1 is not applicable to instances of "a", but to instances of "b" and "c" only. Since new sub-structures of "a" (with conc-name foo-) can be defined at any time, the type for the applicable structures used in the implementation of foo-s1 cannot be statically encoded but must be dynamically determined, or the code for foo-s1 could be dynamically adapted for every new sub-defstruct of "a" with conc-name foo-. I tend to believe this is not easy to implement.
Actually, it's really easy to implement.
I am afraid, using defmethod for the non-optimized case is not that easy because not all defstructs are necessarily compiled with the same optimization settings (e.g., they might be located in different files). In addition, one has to remove methods if a defstruct is recompiled with other slots or with different optimization settings..... :-(
The part about the accessors being defined in different files with different optimization strategies: that's correct. However, it's not the context of the definer, but the context of the user which causes the optimized or non-optimized code path to be used.
With respect to recompilation/redefinition of a defstruct: the spec says you can't do that, or at least, you can't portably depend on that ("The consequences of redefining a *defstruct*http://www.lispworks.com/documentation/HyperSpec/Body/m_defstr.htm#defstruct structure are undefined."). ABCL allows it, under certain strict conditions, but it's not required to and doesn't support any "interesting" cases (ie changes in the number of slots). But the case of method removal is easy: when a defmethod is evaluated, it will cause the original method with the same specializers to be overwritten. That, combined with the explanation that all accessors are always generated (but only sometimes used), should solve it.
But the runtime cost of doing so is huge, unfortunately: we can simply use DEFMETHOD to define the accessors. That will automatically notice the accessor is already defined and add a method for each additionally defined type.
[ snip ]
I see multiple options:
- Remove the type checks in the inline expansions and use DEFMETHOD for the accessor functions, where we need to use the
methods only with sufficiently high SAFETY declarations 2. Use your patch to remove the type checks completely 3. Come up with a way to add dynamic type checks to the inline expansions and function definitions (ie not methods) - possibly executed conditionally with safety settings. 4. Do nothing (not saying it's a good option, but it's always an option)
Do you see any others?
Actually, I see one other solution, now that I had some time to think
about it: we could trace back to the original type and be lenient and allow all descendants of that type access to the slot using the generated slot. That would allow both structures B and C to be read. According to the spec that's too lenient though:
The difference between the *reader*http://www.lispworks.com/documentation/HyperSpec/Body/26_glo_r.htm#reader functions person-name and astro-name is that person-name can be correctly applied to any person, including anastronaut, while astro-name can be correctly applied only to an astronaut. An implementation might check for incorrect use of *reader*http://www.lispworks.com/documentation/HyperSpec/Body/26_glo_r.htm#reader functions.
where "name" is a slot in the 'person' structure which is included in the 'astronaut' structure. Just to see what other implementations do: I ran this code on clisp (2.47) and it generates the same error. Apparently, the spec doesn't define this case and you can't depend on exact behaviour; the only thing I *can* find is a section on the relation between including and included structures, not between syblings:
Whether or not the :conc-name option is explicitly supplied, the following rule governs name conflicts of generated *reader*http://www.lispworks.com/documentation/HyperSpec/Body/26_glo_r.htm#reader (or *accessor*http://www.lispworks.com/documentation/HyperSpec/Body/26_glo_a.htm#accessor) names: For any *structure*http://www.lispworks.com/documentation/HyperSpec/Body/26_glo_s.htm#structure *type*http://www.lispworks.com/documentation/HyperSpec/Body/26_glo_t.htm#type S1 having a *reader*http://www.lispworks.com/documentation/HyperSpec/Body/26_glo_r.htm#reader function named R for a slot named X1 that is inherited by another *structure*http://www.lispworks.com/documentation/HyperSpec/Body/26_glo_s.htm#structure *type*http://www.lispworks.com/documentation/HyperSpec/Body/26_glo_t.htm#type S2 that would have a *reader*http://www.lispworks.com/documentation/HyperSpec/Body/26_glo_r.htm#reader function with the same name R for a slot named X2, no definition for R is generated by the definition of S2; instead, the definition of R is inherited from the definition of S1. (In such a case, if X1 and X2 are different slots, the * implementation*http://www.lispworks.com/documentation/HyperSpec/Body/26_glo_i.htm#implementation might signal a style warning.)
Which suggests that if you used FOO- for the nconc of the A structure as well, that all should be defined and well. Testing that on clisp and ABCL shows that it indeed is.
I'm not sure about the performance impact of dynamic checks, but I do think that if we decide to apply a caching strategy, using the slow-path of updating the cache in case of a type-mismatch, the impact may not be much worse than it is today. After all, the biggest change would be to generate code which allows to check more than one type instead of the current single type.
Given the performance impact of more complicated checks and the fact that
structures are supposed to be geared for high performance (with classes being their flexible counter parts) *and* with the support (for my point) that I find in the spec and "other implementations" (i.e. clisp), I'd rather keep things the way they are at this time, although I'm open to input of the other developers of course.
Is it a solution for you to add the FOO- conc-name option to the A class as well?
Bye,
Erik.
armedbear-devel@common-lisp.net