;;; -*- Mode:LISP; Package:LISP-INTERNALS; Readtable:CL; Base:10 -*- ;;; ;;; ;;; DEFSTRUCT.LISP ;;; ;;; ;;; The DEFSTRUCT macro expands into a PROGN form which contains a whole bunch ;;; of forms (herein called "tasks") that do the dirty work of the DEFSTRUCT. ;;; The macro itself calls several parsers to parse the DEFSTRUCT body, creates ;;; a procedure to incrementally build up a list of tasks, calls MAKE-DEFSTRUCT- ;;; TASKS to make the tasks, and constructs the PROGN form. Most of the gory ;;; work is done by MAKE-DEFSTRUCT-TASKS and the functions it calls. ;;; Problems: ;;; Print functions are still somewhat broken. DEFTYPE in expansion raises an ;;; error if something different has been DEFTYPEd with the same name. This ;;; might be the right thing, though. (defmacro defstruct (name-and-options &body documentation-and-slot-descriptions) (multiple-value-bind (name options doc-string slot-records) (parse-defstruct name-and-options documentation-and-slot-descriptions) `(PROGN ; (SETF (DOCUMENTATION ',name :STRUCTURE) ,doc-string) ,@(make-defstruct-tasks name options slot-records) ',name))) ;;; DEFSTRUCT-DESCRIPTOR ;;; ;;; A defstruct-descriptor holds information about a defstruct. ;;; This information is needed for :INCLUDE options in subsequent defstructs and ;;; other things. (defmacro defstruct-descriptor-name (descriptor) `(svref ,descriptor 0)) (defmacro defstruct-descriptor-constructor (descriptor) `(svref ,descriptor 1)) (defmacro defstruct-descriptor-includes (descriptor) `(svref ,descriptor 2)) (defmacro defstruct-descriptor-type (descriptor) "The defstruct's type, which may be LIST, VECTOR, (VECTOR foo), or NIL." `(svref ,descriptor 3)) (defmacro defstruct-descriptor-slots (descriptor) `(svref ,descriptor 4)) (defun make-defstruct-descriptor (name constructor includes type slots) (let ((desc (array:make-vector 5))) (setf (defstruct-descriptor-name desc) name) (setf (defstruct-descriptor-constructor desc) constructor) (setf (defstruct-descriptor-includes desc) includes) (setf (defstruct-descriptor-type desc) type) (setf (defstruct-descriptor-slots desc) slots) desc)) (defmacro get-defstruct-descriptor (defstruct-name) `(nc:get-declaration ,defstruct-name 'defstruct-descriptor)) ;(defun constructor-function-name (name) ; "Return the constructor function for structures with type NAME. If no such ;structure type exists, return :NO-SUCH-TYPE. If no constructor function exists for ;that structure type, return :NO-SUCH-CONSTRUCTOR." ; (let ((desc (get-defstruct-descriptor name))) ; (defstruct-descriptor-constructor name))) ;;; Slots, Slot-Descriptions, and Slot-Records ;;; ;;; SLOT-DESCRIPTIONS make up the body of DEFSTRUCT forms. In Common Lisp, ;;; the syntax of a SLOT-DESCRIPTION is ;;; slot-name | (slot-name [default-init . slot-plist]) ;;; If no default-init is provided, then the contents of the slot are undefined. ;;; ;;; SLOT-RECORDS are the elements used to represent a structure. If a SLOT-RECORD ;;; represents an actual slot, its syntax is ;;; (slot-name default-init . slot-plist), ;;; which is also valid syntax for an equivalent SLOT-DESCRIPTION. If a SLOT-RECORD ;;; represents a place to contain the name of a structure, its syntax is ;;; structure-name, ;;; a symbol. If a SLOT-RECORD represents an empty slot being reserved by a ;;; :INITIAL-OFFSET, its syntax is ;;; NIL. ;;; ;;; The function PARSE-SLOT-DESCRIPTION translates a SLOT-DESCRIPTION into the ;;; first kind of SLOT-RECORD. If a default-init is not given, UNDEFINED is used. ;;; If a slot-plist is not given, NIL is used. ;;; ;;; SLOT-RECORDS of the first kind may be neatly constructed and dissected with ;;; the MAKE-SLOT-RECORD constructor and the SLOT-RECORD-NAME, SLOT-RECORD-DEFAULT, ;;; and SLOT-RECORD-PLIST selectors. ;;; ;;; The predicate ACTUAL-SLOT-RECORD-P returns T if a slot-record is of the first kind, ;;; and NIL otherwise. The function EXTRACT-ACTUAL-SLOT-RECORDS takes a list of ;;; slot-records and returns a list of all the actual slot-records in that list. ;;; The function FIND-SLOT finds an actual slot-record in a list of slot-records with ;;; a given name. (defun make-slot-record (slot-name default-value plist) (list* slot-name default-value plist)) (defun slot-record-name (slot-record) (car slot-record)) (defun slot-record-default (slot-record) (cadr slot-record)) (defun slot-record-plist (slot-record) (cddr slot-record)) (defun actual-slot-record-p (slot-record) (and (consp slot-record) (not (eq 'structure-included-name-list (car slot-record))))) (defun parse-slot (slot-description) (etypecase slot-description (symbol (make-slot-record slot-description ''UNDEFINED NIL)) (cons (let ((slot-name (car slot-description)) (default (if (< (length slot-description) 2) ''UNDEFINED (second slot-description))) (plist (cddr slot-description))) (if (symbolp slot-name) (make-slot-record slot-name default plist) (error "Slot name ~S must be a symbol." slot-name)))))) (defun find-slot (name slot-records) "Return the first actual slot-record in the list SLOT-RECORDS with name NAME, or NIL if there is none." (cond ((null slot-records) NIL) ((and (actual-slot-record-p (first slot-records)) (eq name (slot-record-name (first slot-records)))) (first slot-records)) (t (find-slot name (rest slot-records))))) (defun extract-actual-slot-records (slot-records) (cond ((null slot-records) NIL) ((actual-slot-record-p (car slot-records)) (cons (car slot-records) (extract-actual-slot-records (cdr slot-records)))) (t (extract-actual-slot-records (cdr slot-records))))) ;;; Structure objects (the default type for representing defstructs) ;;; ;;; Structure objects are defined in the cold load (defun copy-structure-object (structure-object) (SI::MAKE-ARRAY-INTO-NAMED-STRUCTURE (copy-seq structure-object))) (defun set-structure-object-print-function (name print-function) (declare (ignore name print-function)) NIL) ;;; Parse Defstruct (defun parse-defstruct (name-and-options documentation-and-slot-descriptions) (let (name options doc-string slots) (etypecase name-and-options (symbol (setq name name-and-options) (setq options NIL)) (cons (setq name (first name-and-options)) (setq options (rest name-and-options)))) (if (stringp (car documentation-and-slot-descriptions)) (progn (setq doc-string (car documentation-and-slot-descriptions)) (setq slots (cdr documentation-and-slot-descriptions))) (progn (setq doc-string NIL) (setq slots documentation-and-slot-descriptions))) (values name options doc-string (mapcar #'parse-slot slots)))) ;;;; Task Generation (defun make-defstruct-tasks (name options slots) (let ((tasks '())) (flet ((push-task (task) (setq tasks (cons task tasks)))) (let ( (conc-name (concatenate 'lisp:string (symbol-name name) "-")) (constructor (intern (concatenate 'lisp:string "MAKE-" (symbol-name name)) lisp:*package*)) (copier (intern (concatenate 'lisp:string "COPY-" (symbol-name name)) lisp:*package*)) (predicate (intern (concatenate 'lisp:string (symbol-name name) "-P") lisp:*package*)) (BOA-constructors NIL) (inclusions NIL) (print-function NIL) ;default? (type NIL) (namedp :IMPLIED) (predicatep :IMPLIED) (initial-offset-p NIL) (initial-offset 0) (net-offset NIL) ) (dolist (option options) (let (option-type option-args first-arg) (etypecase option (symbol (setq option-type option) (setq option-args NIL) (setq first-arg NIL)) (cons (setq option-type (first option)) (setq option-args (rest option)) (setq first-arg (car option-args)))) (case option-type (:conc-name (if first-arg (setq conc-name (string first-arg)) (setq conc-name ""))) (:constructor (case (length option-args) (1 (if (symbolp first-arg) (setq constructor first-arg) (error "~S is not a valid name for a constructor function." first-arg))) (2 (push option-args BOA-constructors)) (otherwise (error ":CONSTRUCTOR must have 1 or 2 arguments.")))) (:copier (when option-args (if first-arg (setq copier first-arg) (setq copier NIL)))) (:predicate (when option-args (if first-arg (progn (setq predicate first-arg) (setq predicatep T)) (progn (setq predicate NIL) (setq predicatep NIL))))) (:include (if (or inclusions (> (length option-args) 1)) (error "Broken :INCLUDE argument in DEFSTRUCT.") (setq inclusions option-args))) (:print-function (setq print-function first-arg)) (:type (if (or (eq first-arg 'VECTOR) (eq first-arg 'LIST) (and (listp first-arg) (= (length first-arg) 2) (eq (car first-arg) 'VECTOR))) (setq type first-arg) (error "Don't recognize :TYPE ~S." first-arg))) (:named (setq namedp T)) (:initial-offset (unless (and (numberp first-arg) (>= first-arg 0)) (error "Initial-offset can't be ~S." first-arg)) (setq initial-offset first-arg) (setq initial-offset-p T)) ))) ;; Decide, once and for all, if the structure is named or not. (when (and (eq namedp :IMPLIED) type) (setq namedp NIL)) ;; Augment the slots with :INCLUDEd slots, :INITIAL-OFFSET nils, and the name, where appropriate. (let ((addenda (append (when (eq namedp :implied) (list (do ((name-list name (if (consp name-list) (lisp:nconc name-list include) (list name-list include))) (include (car inclusions) (defstruct-descriptor-includes (get-defstruct-descriptor include)))) ((null include) (if (consp name-list) (list 'structure-included-name-list name-list) name-list))))) (when inclusions (include (car inclusions) (cdr inclusions) type)) (make-list initial-offset) (when (eq namedp t) (list name)) ))) (setq net-offset (length addenda)) (setq slots (append addenda slots))) ;; Make sure all the rules are being followed (when (and predicatep type (not namedp)) (cond ((eq predicatep T) (error "Predicate cannot be defined for unnamed structure type ~S." type)) ((eq predicatep :IMPLIED) (setq predicatep NIL)))) (when (and initial-offset-p (not type)) (error ":TYPE must be specified for :INITIAL-OFFSET.")) ;; Make the constructors (when (and constructor (not BOA-constructors)) (push-task (make-defun-constructor constructor NIL nil type slots))) (dolist (BOA-constructor BOA-constructors) (let ((constructor-name (first BOA-constructor)) (lambda-list (second BOA-constructor))) (push-task (make-defun-constructor constructor-name lambda-list t type slots)))) ;; Make the copier (when copier (push-task (make-defun-copier copier name type))) ;; Make the predicate. Note that if (:PREDICATE NIL) is explicitly requested, but ;; there is no type, then a predicate will be generated anyway. This is because ;; the predicate is needed by the following DEFTYPE. (when (or predicatep (not type)) (push-task (make-defun-predicate predicate name type net-offset))) ;; Define the type (unless type (push-task `(DEFTYPE ,name () `(SATISFIES ,',predicate)))) ;; Define the print function (if type (when print-function (error ":PRINT-FUNCTION may not be specified for type ~S." type)) (when print-function (push-task `(SET-STRUCTURE-OBJECT-PRINT-FUNCTION ',name ',print-function)))) ;; Make the slot accessors and SETF methods (let ((index 0)) (dolist (slot slots) (when (actual-slot-record-p slot) (push-task (make-define-accessor conc-name slot name type index)) (unless (cadr (member :READ-ONLY (slot-record-plist slot))) (push-task (make-defsetf-accessor conc-name slot name type index)))) (incf index))) ;; Remember for posterity's sake (push-task `(NC:DEF-DECLARATION ,name DEFSTRUCT-DESCRIPTOR ',(make-defstruct-descriptor name constructor (car inclusions) type slots))))) tasks)) ;;; Task Generators ;;; ;;; These functions generate the myriad DEFUN, DEFSUBST, and DEFSETF tasks that appear ;;; in the expansion of DEFSTRUCT. (defun make-defun-constructor (constructor-name lambda-list BOA-p type slots) (flet ((value-for-slot (slot) (cond ((null slot) NIL) ((symbolp slot) (list 'QUOTE slot)) ((and (consp slot) (eq (first slot) 'structure-included-name-list)) (list 'quote (second slot))) ((consp slot) (slot-record-name slot))))) (let ((lambda-list (make-constructor-lambda-list (extract-actual-slot-records slots) lambda-list BOA-p)) (slot-names (mapcar #'value-for-slot slots))) (let* ((rest-arg (cadr (member '&REST lambda-list))) ; (make-it-safe (if rest-arg ; `((SETQ ,rest-arg (COPY-LIST ,rest-arg))) ; NIL)) ) (case (internal-type-representation type) (:DEFAULT `(DEFUN ,constructor-name ,lambda-list ; ,@make-it-safe (MAKE-STRUCTURE-OBJECT ,@slot-names))) (:VECTOR `(DEFUN ,constructor-name ,lambda-list ; ,@make-it-safe (VECTOR ,@slot-names))) (:LIST `(DEFUN ,constructor-name ,lambda-list ; ,@make-it-safe (LIST ,@slot-names)))))))) (defun make-defun-copier (copier-name defstruct-name type) (case (internal-type-representation type) (:DEFAULT `(DEFUN ,copier-name (,defstruct-name) (COPY-STRUCTURE-OBJECT ,defstruct-name))) (:VECTOR `(DEFUN ,copier-name (,defstruct-name) (COPY-SEQ ,defstruct-name))) (:LIST `(DEFUN ,copier-name (,defstruct-name) (COPY-LIST ,defstruct-name))))) (defun make-defun-predicate (predicate-name defstruct-name type net-offset) (case (internal-type-representation type) (:DEFAULT `(DEFSUBST ,predicate-name (THING) (TYPEP-STRUCTURE THING ',defstruct-name))) (:VECTOR `(DEFUN ,predicate-name (THING) (AND (VECTORP THING) (>= (LENGTH THING) ,net-offset) (EQ (AREF THING ,(1- net-offset)) ',defstruct-name)))) (:LIST `(DEFUN ,predicate-name (THING) (AND (LISTP THING) (>= (LENGTH THING) ,net-offset) (EQ (NTH ,(1- net-offset) THING) ',defstruct-name)))))) ; we can't require length because of subtypes. (defun make-define-accessor (conc-name slot defstruct-name type index) (let ((accessor-name (concatenate-and-intern conc-name (symbol-name (slot-record-name slot))))) (case (internal-type-representation type) (:DEFAULT `(DEFSUBST ,accessor-name (,defstruct-name) (STRUCTURE-REF ',defstruct-name ,index ,defstruct-name))) (:VECTOR `(DEFSUBST ,accessor-name (,defstruct-name) (AREF ,defstruct-name ,index))) (:LIST `(DEFSUBST ,accessor-name (,defstruct-name) (NTH ,index ,defstruct-name)))))) (defun make-defsetf-accessor (conc-name slot defstruct-name type index) (let ((accessor-name (concatenate-and-intern conc-name (symbol-name (slot-record-name slot)))) (value-temp (gensym))) (case (internal-type-representation type) (:DEFAULT `(DEFSETF ,accessor-name (,defstruct-name) (,value-temp) `(STRUCTURE-SET ',',defstruct-name ,,index ,,defstruct-name ,,value-temp))) (:VECTOR `(DEFSETF ,accessor-name (,defstruct-name) (,value-temp) `(SETF (AREF ,,defstruct-name ,,index) ,,value-temp))) (:LIST `(DEFSETF ,accessor-name (,defstruct-name) (,value-temp) `(SETF (NTH ,,index ,,defstruct-name) ,,value-temp)))))) (defun concatenate-and-intern (string1 string2) "Concatenate STRING1 and STRING2, and intern the resulting string in the current package." (intern (concatenate 'lisp:string string1 string2) lisp:*package*)) (defun internal-type-representation (type-spec) (cond ((eq type-spec NIL) :DEFAULT) ((or (eq type-spec 'VECTOR) (and (listp type-spec) (eq (car type-spec) 'VECTOR))) :VECTOR) ((eq type-spec 'LIST) :LIST) (t (error "Internal: unrecognized type ~S" type-spec)))) ;;; Incredible Hair ("Cheveux Incroyable") ;;; ;;; The following atrocity takes care of lambda lists for :CONSTRUCTOR functions. ;;; SLOTS is a list only of actual-slot-records. (defun make-constructor-lambda-list (slots supplied-lambda-list BOA-p) (if BOA-p ;; Parse the lambda list into its parameters (multiple-value-bind (required optional rest aux) (USER::PARSE-LAMBDA-LIST supplied-lambda-list '(:REQUIRED :OPTIONAL :REST :AUX)) (let ((lambda-list-variables (append required (mapcar #'USER::PARSE-OPTIONAL-PARAMETER optional) (if rest (list rest)) (mapcar #'USER::PARSE-AUX-PARAMETER aux))) (redefaulted-optionals NIL) (redefaulted-auxs NIL) (unmentioned-slots NIL)) ;; Add &OPTIONAL supplied-p variables to the list of things we know are bound. (Ugh!) (dolist (opt-parameter optional) (multiple-value-bind (ignore ignore svar) (USER::PARSE-OPTIONAL-PARAMETER opt-parameter) (if svar (push svar lambda-list-variables)))) ;; Require each parameter to name a slot. (mapcar #'(lambda (llvar) (unless (find-slot llvar slots) (error "~S does not name a slot." llvar))) lambda-list-variables) ;; Use the defstruct's defaults (rather than NIL) for the &OPTIONAL and &AUX parameters. ;; Arbitrarily forbid supplied-p variables. (dolist (parameter optional) (multiple-value-bind (variable initform svar) (USER::PARSE-OPTIONAL-PARAMETER parameter) (if (and (listp parameter) (> (length parameter) 1)) (push (list* variable initform (when svar (list svar))) redefaulted-optionals) (push (list* variable (slot-record-default (find-slot variable slots)) (when svar (list svar))) redefaulted-optionals)))) (setq redefaulted-optionals (reverse redefaulted-optionals)) (dolist (parameter aux) (multiple-value-bind (variable value) (USER::PARSE-AUX-PARAMETER parameter) (if (and (listp parameter) (> (length parameter) 1)) (push (list variable value) redefaulted-auxs) (push (list variable (slot-record-default (find-slot variable slots))) redefaulted-auxs)))) (setq redefaulted-auxs (reverse redefaulted-auxs)) ;; Add an &AUX parameter for each unmentioned slot. (mapcar #'(lambda (slot) (unless (member (slot-record-name slot) lambda-list-variables) (push `(,(slot-record-name slot) ,(slot-record-default slot)) unmentioned-slots))) slots) (setq unmentioned-slots (reverse unmentioned-slots)) ;; Reassemble the lambda list. `(,@required ,@(when redefaulted-optionals (cons '&OPTIONAL redefaulted-optionals)) ,@(when rest (list '&REST rest)) &AUX ,@redefaulted-auxs ,@unmentioned-slots))) ;; If no lambda list was supplied, construct one with &KEY parameters. `(&KEY ,@(mapcar #'(lambda (slot) `(,(slot-record-name slot) ,(slot-record-default slot))) slots)))) ;;; Inheritance ;;; ;;; Returns a list of slot-records to include (defun include (parent new-slot-descriptions new-type) (let ((parent-desc (get-defstruct-descriptor parent))) (unless parent-desc (error "Attempt to include structure ~S, which hasn't been defined." parent)) (let ((parent-type (defstruct-descriptor-type parent-desc)) (included-slots (defstruct-descriptor-slots parent-desc))) (unless (equal parent-type new-type) (error "Included structure ~S has :TYPE ~S, which does not match type ~S." parent parent-type new-type)) (dolist (foo new-slot-descriptions) (let* ((new-slot (parse-slot foo)) (slot-name (slot-record-name new-slot)) (new-plist (slot-record-plist new-slot)) (old-slot (find-slot slot-name included-slots)) (old-plist (slot-record-plist old-slot))) (unless old-slot (error "Slot ~S is not defined in the included structure ~S" slot-name parent)) (when (and (getf old-plist :read-only) (not (getf new-plist :read-only))) (error "Slot ~S is :READ-ONLY in ~S; it must also be :READ-ONLY when included." slot-name parent)) (let ((old-type (getf old-plist :type)) (new-type (getf new-plist :type))) (when (and new-type old-type (not (subtypep new-type old-type))) (error ":TYPE of slot ~S must be a subtype of included type ~S." slot-name old-type))) ;; maybe an error if old-type & ~new-type (setq included-slots (substitute-if new-slot #'(lambda (u) (and (actual-slot-record-p u) (eq (slot-record-name u) slot-name))) included-slots)))) (if (and (symbolp (car included-slots)) (not (null (car included-slots)))) (cdr included-slots) included-slots))))