Skip to content

Instantly share code, notes, and snippets.

@Bike
Last active June 11, 2022 01:29
Show Gist options
  • Save Bike/168c37f74ddd67eebda1719e33759c13 to your computer and use it in GitHub Desktop.
Save Bike/168c37f74ddd67eebda1719e33759c13 to your computer and use it in GitHub Desktop.
DEFSTRUCT stuff

Upgraded slot type

Implementations can store values in the slots of structure objects (and possibly standard objects) in a packed/unboxed format for space and sometimes speed efficiency, similarly to how they can be stored in arrays and complexes. But unlike those cases, there is no defined interface for the programmer to get information about this storage.

Add a function: upgraded-slot-type. This works analogously to upgraded-array-element-type or upgraded-complex-part-type, in that it takes a type specifier and environment and returns an upgraded type specifier, where the input is a recognizable (fixing the UAET spec bug) subtype of the output. There are no other requirements, and in particular implementations are permitted to always return T. upgraded-slot-type has no semantic effects and only serves as an optimization hint for the programmer.

upgraded-slot-type is essentially only relevant for structure objects. Vector and list structures have "slot types" of the vector element type and T, respectively.

Structure redefinition

The standard explicitly leaves the consequences of redefining a structure undefined. This is one reason that defstruct is inconvenient for development: implementations are allowed to do essentially anything in the event of defstruct definition, even technically when it's the same form being evaluated (though all implementations I know of do something reasonable in this case). But I think it's possible to make redefinition semantics clearer and safer, while retaining defstruct's efficiency.

X3J13 decided against more specific redefinition semantics in issue DEFSTRUCT-REDEFINITION, on the theory that it was only really relevant for interactive programming. This is probably true, but interactive programming is a lot of programming and I feel that leaving it totally undefined is pretty unfortunate. At the very least, merely changing initforms ought to be fine.

Redefining a structure, i.e. evaluating a defstruct form where the name has previously been used to define a structure, works as follows.

Any functions or setf expansions defined by the new definition that were also defined by the old definition are redefined if necessary. For example, given (defstruct foo bar) followed by (defstruct foo bar baz), make-foo is redefined as if by defun.

It is implementation-dependent whether any functions or setf expansions defined by the previous definition will still be available if the new structure definition does not define them. For example, given (defstruct foo bar) then (defstruct foo (bar 0 :read-only t)) it is implementation-dependent whether there is a setf expansion for foo-bar. Portable code must not rely on these definitions persisting.

If the new definition is compatible with the old definition, the structure class (if there is one) is perhaps reinitialized but not replaced, and existing instances continue to be instances of the same structure class. If the new definition is not compatible with the old, any structure class is replaced in the global environment rather than reinitialized (as by (setf find-class)), but the old class and any methods specialized on it, etc., are retained. Implementations are encouraged to issue a warning upon incompatible structure redefinition.

In case of compatible redefinition, any new slots in an old object are uninitialized even if the new definition has an initform, so reading these slots of an old object before they are written has undefined consequences. Reading the value of any slots corresponding to an :initial-offset is undefined behavior. The value of any slots corresponding to a :named option is the name, and the consequences are undefined if the new slot definition's :type would not permit this. [NOTE: The X3J13 rationale for uninitialized element access being undefined behavior rather than simply returning an indetermine value is to allow implementations to issue warnings or errors, and I think that applies here. See issue UNINITIALIZED-ELEMENTS.]

Two structure definitions are compatible if:

  • They have the same :type option, i.e. they are both structure objects, both lists, or both vectors of the same element type.
  • They both have no :include or both have the same :include.
  • They either both have :named or neither does.
  • They either both have the same :initial-offset or neither has an :initial-offset.
  • They have the same number of slots, in the same order, with the same names, and the same upgraded types.

Implementations may define additional situations in which structure definitions are compatible. Compatibility is defined essentially so that old instances can be used as new instances without any reinitialization process.

Note that for the purpose of determining the upgraded type of a slot that is :included in another structure, types declared for the slot in an overriding slot description are irrelevant. That is, the type is upgraded from its original type declaration, not from that declared in a subtype of the structure, even in that subtype of the structure.

Examples

(defstruct foo)
(defparameter *old-foo-class* (find-class 'foo))
(defparameter *old-foo-constructor* #'make-foo)
(defmethod testfun ((x foo)) :old)
(defstruct foo (bar nil))
(defmethod testfun ((x foo)) :new)

;; Assuming the structure definitions are incompatible in this implementation:
(eq *old-foo-class* (find-class 'foo)) ; => NIL
(typep (funcall *old-foo-constructor*) 'foo) ; => NIL
(eq *old-foo-class* (class-of (funcall *old-foo-constructor*))) ; => true
(testfun (funcall *old-foo-constructor*)) ; => :OLD
(typep (make-foo) 'foo) ; => true
(testfun (make-foo)) ; => :NEW
(foo-bar (funcall *old-foo-constructor*)) ; => undefined behavior, as the type restriction on foo-bar was violated

;; If they are compatible:
(eq *old-foo-class* (find-class 'foo)) ; => T
(typep (funcall *old-foo-constructor*) 'foo) ; => T
(testfun (funcall *old-foo-constructor*)) ; => :NEW
(foo-bar (funcall *old-foo-constructor*)) ; => undefined behavior, as the slot is uninitialized (despite the initform)
(defstruct foo (bar 0 :type fixnum))
(defstruct (foo2 (:include foo)) (baz 'baz :type symbol))

(defstruct (foo (:constructor)) (bar 13 :read-only t :type fixnum)) ; compatible
(defstruct (foo2 (:include foo (bar 15 :type (unsigned-byte 8))) (baz 'foo2:type symbol)) ; compatible, regardless of how ub8 upgrades

Structs and make-instance

Structures can be made to work with make-instance and related functions in some respects in what is I think a reasonable way.

make-instance is permitted on structure classes. The initargs and initforms are as they are for a non-BOA structure constructor. Structure constructors are not required to use make-instance, i.e. methods on make-instance may not be invoked by a structure constructor.

allocate-instance is permitted on structure classes. A structure constructed in this way is not initialized, so reading any of its slots before they are written has undefined consequences. Structure constructors and make-instance are not required to use allocate-instance, i.e. methods on it may not be invoked by a structure constructor.

Methods on shared-initialize, initialize-instance etc. specialized on structure classes may be defined by the programmer, but are not guaranteed to be run at any time unless directly invoked, even in the case of using make-instance.

Unboxable structs

The obstacle to storing structures unboxed in arrays is the semantics of eql. It is generally understood, though not explicitly stated by the standard that I can tell, that arrays preserve EQLity. That is, (let ((a (make-array ...))) (setf (aref a 0) some-object) (eql some-object (aref a 0))) must be true, regardless of the specialization of the array. Storing objects in arrays unboxed essentially means aref must box a new object at times, and this new object will not be eq to the original object. This is okay for numbers or characters, since eql is defined for them anyway. But structures are only eql if they are eq, so boxing a structure in aref would mean the array did not preserve EQLity. Therefore we need an extension.

New unboxable structure classes can be defined through defstruct. A new :unboxable option is provided. It takes no parameters, like :named. An error is signaled if :unboxable and :type are both supplied.

Implementations are permitted to not support unboxable structure objects in some or all circumstances. If an implementation cannot make an unboxable structure object from a defstruct definition in which that was requested, it is encouraged to issue a note.

If :unboxable is provided to defstruct, all slots of the defined class, including inherited slots, are read only, as if :read-only t was supplied in their definitions. An error is signaled if :read-only nil is specified in the definition of a slot in an :unboxable structure.

Unboxability is not inherited: structure classes that are not unboxable may :include unboxable structure classes and vice versa. [NOTE: This presents a problem with respect to UAET's requirement to move "up" in the type lattice.]

Implementations are encouraged but not required to store unboxable structure objects unboxed in specialized arrays and to make unboxable structure classes their own upgraded-array-element-type and upgraded-slot-type.

If an unboxable struct class is the upgraded element type of an array, the consequences are undefined if an attempt is made to store an instance of a subclass of that class in that array. [Also presents some UAET weirdness. Also, maybe "should signal an error" instead of just UB?]

Unboxable structure objects behave differently when passed to eql than other structure objects. Two unboxable structure objects are eql if and only if they have the same class and all of their fields are eql to each other. equal is similarly defined recursively.

eq on unboxable structure objects is defined as it is for numbers and characters. That is, the implementation is permitted to "copy" unboxable structure objects at any time, and there is no guarantee that eq on unboxable structure objects is ever true. eql should be used instead.

Examples

(defstruct bpoint (x 0.0 :type single-float) (y 0.0 :type single-float) (z 0.0 :type single-float))
(defstruct (point :unboxable) (x 0.0 :type single-float) (y 0.0 :type single-float) (z 0.0 :type single-float))

(let* ((bpoint (make-bpoint :x 1.0 :y 39.4 :z -7.6))
       (array (make-array 23 :element-type 'bpoint :initial-element bpoint)))
  (print (array-element-type array)) ; probably T
  (print (eq (aref array 0) bpoint)) ; => T
  (print (eql (aref array 0) bpoint)) ; => T
  (+ (bpoint-x (aref array 0)) (bpoint-y (aref array 0)) (bpoint-z (aref array 0))))

(let* ((point (make-point :x 1.0 :y 39.4 :z -7.6))
       (array (make-array 23 :element-type 'point :initial-element point)))
  (print (array-element-type array)) ; can be POINT or #<STRUCTURE-CLASS POINT> or T or some other superclass
  (print (eq (aref array 0) point)) ; can be NIL
  (print (eql (aref array 0) point)) ; => T
  ;; In an implementation that has unboxable POINT but doesn't optimize very well, this could incur three boxings,
  ;; but a better compiler could avoid boxing entirely (except maybe the resulting float).
  (+ (point-x (aref array 0)) (point-y (aref array 0)) (point-z (aref array 0))))

(eql (make-bpoint) (make-bpoint)) ; => NIL
(eql (make-point) (make-point)) ; => T
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment