Daily work in progress.
[sod] / src / pset-proto.lisp
CommitLineData
dea4d055
MW
1;;; -*-lisp-*-
2;;;
3;;; Protocol for property sets
4;;;
5;;; (c) 2009 Straylight/Edgeware
6;;;
7
8;;;----- Licensing notice ---------------------------------------------------
9;;;
10;;; This file is part of the Sensble Object Design, an object system for C.
11;;;
12;;; SOD is free software; you can redistribute it and/or modify
13;;; it under the terms of the GNU General Public License as published by
14;;; the Free Software Foundation; either version 2 of the License, or
15;;; (at your option) any later version.
16;;;
17;;; SOD is distributed in the hope that it will be useful,
18;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
19;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20;;; GNU General Public License for more details.
21;;;
22;;; You should have received a copy of the GNU General Public License
23;;; along with SOD; if not, write to the Free Software Foundation,
24;;; Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
25
26(cl:in-package #:sod)
27
28;;;--------------------------------------------------------------------------
29;;; Property representation.
30
31(export 'property-key)
32(defun property-key (name)
33 "Convert NAME into a keyword.
34
35 If NAME isn't a symbol already, then flip its case (using
36 `frob-identifier'), and intern into the `keyword' package."
37 (etypecase name
38 (symbol name)
39 (string (intern (frob-identifier name) :keyword))))
40
41(export 'property-type)
42(defgeneric property-type (value)
43 (:documentation "Guess a sensible property type to use for VALUE.")
44 (:method ((value symbol)) :symbol)
239fa5bd 45 (:method ((value integer)) :int)
dea4d055
MW
46 (:method ((value string)) :string)
47 (:method ((value character)) :char)
48 (:method (value) :other))
49
50(export '(property propertyp make-property
51 p-name p-value p-type p-key p-seenp))
52(defstruct (property
53 (:predicate propertyp)
54 (:conc-name p-)
55 (:constructor make-property
56 (name value
57 &key (type (property-type value))
58 ((:location %loc))
59 seenp
60 &aux (key (property-key name))
61 (location (file-location %loc)))))
62 "A simple structure for holding a property in a property set.
63
64 The main useful feature is the ability to tick off properties which have
65 been used, so that we can complain about unrecognized properties.
66
67 An explicit type tag is necessary because we need to be able to talk
68 distinctly about identifiers, strings and symbols, and we've only got two
69 obvious Lisp types to play with. Sad, but true."
70
71 (name nil :type (or string symbol))
72 (value nil :type t)
73 (type nil :type symbol)
74 (location (file-location nil) :type file-location)
75 (key nil :type symbol)
76 (seenp nil :type boolean))
77
78(defun string-to-symbol
79 (string &key (package *package*) (swap-case t) (swap-hyphen t))
80 "Convert STRING to a symbol in PACKAGE.
81
82 Parse off a `PACKAGE:' prefix from STRING if necessary, to identify the
83 package; PACKAGE is used if there isn't a prefix. A doubled colon allows
84 access to internal symbols, and will intern if necessary. Note that
85 escape characters are /not/ processed; don't put colons in package names
86 if you want to use them from SOD property sets.
87
88 The portions of the string are modified by `frob-identifier'; the
89 arguments SWAP-CASE and SWAP-HYPHEN are passed to `frob-identifier' to
90 control this process."
91
92 (let* ((length (length string))
93 (colon (position #\: string)))
94 (multiple-value-bind (start internalp)
95 (cond ((not colon) (values 0 t))
96 ((and (< (1+ colon) length)
97 (char= (char string (1+ colon)) #\:))
98 (values (+ colon 2) t))
99 (t
100 (values (1+ colon) nil)))
101 (when colon
102 (let* ((package-name (if (zerop colon) "KEYWORD"
103 (frob-identifier (subseq string 0 colon)
104 :swap-case swap-case
105 :swap-hyphen swap-hyphen)))
106 (found (find-package package-name)))
107 (unless found
108 (error "Unknown package `~A'" package-name))
109 (setf package found)))
110 (let ((name (frob-identifier (subseq string start)
111 :swap-case swap-case
112 :swap-hyphen swap-hyphen)))
113 (multiple-value-bind (symbol status)
114 (funcall (if internalp #'intern #'find-symbol) name package)
115 (cond ((or internalp (eq status :external))
116 symbol)
117 ((not status)
118 (error "Symbol `~A' not found in package `~A'"
119 name (package-name package)))
120 (t
121 (error "Symbol `~A' not external in package `~A'"
122 name (package-name package)))))))))
123
124(export 'coerce-property-value)
125(defgeneric coerce-property-value (value type wanted)
126 (:documentation
127 "Convert VALUE, a property of type TYPE, to be of type WANTED.
128
129 It's sensible to add additional methods to this function, but there are
130 all the ones we need.")
131
132 ;; If TYPE matches WANTED, we'll assume that VALUE already has the right
133 ;; form. Otherwise, if nothing else matched, then I guess we'll have to
134 ;; say it didn't work.
135 (:method (value type wanted)
136 (if (eql type wanted) value
137 (error "Incorrect type: expected ~A but found ~A" wanted type)))
138
139 ;; If the caller asks for type T then give him the raw thing.
140 (:method (value type (wanted (eql t)))
141 value))
142
143;;;--------------------------------------------------------------------------
144;;; Property set representation.
145
146(export '(pset psetp))
147(defstruct (pset (:predicate psetp)
148 (:constructor %make-pset)
149 (:conc-name %pset-))
150 "A property set.
151
152 Wrapped up in a structure so that we can define a print function."
153 (hash (make-hash-table) :type hash-table))
154
155(export '(make-pset pset-get pset-store pset-map))
156(declaim (inline make-pset pset-get pset-store pset-map))
157
158(defun make-pset ()
159 "Constructor for property sets."
160 (%make-pset))
161
162(defun pset-get (pset key)
163 "Look KEY up in PSET and return what we find.
164
165 If there's no property by that name, return NIL."
166 (values (gethash key (%pset-hash pset))))
167
168(defun pset-store (pset prop)
169 "Store property PROP in PSET.
170
171 Overwrite or replace any previous property with the same name. Mutates
172 the property set."
173 (setf (gethash (p-key prop) (%pset-hash pset)) prop))
174
175(defun pset-map (func pset)
176 "Call FUNC for each property in PSET."
177 (maphash (lambda (key value) (declare (ignore key)) (funcall func value))
178 (%pset-hash pset)))
179
180(export 'with-pset-iterator)
181(defmacro with-pset-iterator ((name pset) &body body)
182 "Evaluate BODY with NAME bound to a macro returning properties from PSET.
183
184 Evaluating (NAME) returns a property object or nil if all properties have
185 been read."
186 (with-gensyms (next win key value)
187 `(with-hash-table-iterator (,next (%pset-hash ,pset))
188 (macrolet ((,name ()
189 (multiple-value-bind (,win ,key ,value) (,next)
190 (declare (ignore ,key))
191 (and ,win ,value))))
192 ,@body))))
193
194;;;--------------------------------------------------------------------------
195;;; `Cooked' property set operations.
196
197(export 'store-property)
198(defun store-property
199 (pset name value &key (type (property-type value)) location)
200 "Store a property in PSET."
201 (pset-store pset
202 (make-property name value :type type :location location)))
203
204(export 'get-property)
205(defun get-property (pset name type &optional default)
206 "Fetch a property from a property set.
207
208 If a property NAME is not found in PSET, or if a property is found, but
209 its type doesn't match TYPE, then return DEFAULT and nil; otherwise return
210 the value and its file location. In the latter case, mark the property as
211 having been used.
212
3109662a
MW
213 The value returned depends on the TYPE argument provided. If you pass
214 `nil' then you get back the entire `property' object. If you pass `t',
215 then you get whatever was left in the property set, uninterpreted.
216 Otherwise the value is coerced to the right kind of thing (where possible)
217 and returned.
dea4d055
MW
218
219 If PSET is nil, then return DEFAULT."
220
221 (let ((prop (and pset (pset-get pset (property-key name)))))
222 (with-default-error-location ((and prop (p-location prop)))
223 (cond ((not prop)
224 (values default nil))
225 ((not type)
226 (setf (p-seenp prop) t)
227 (values prop (p-location prop)))
228 (t
229 (setf (p-seenp prop) t)
230 (values (coerce-property-value (p-value prop)
231 (p-type prop)
232 type)
233 (p-location prop)))))))
234
235(export 'add-property)
236(defun add-property
237 (pset name value &key (type (property-type value)) location)
238 "Add a property to PSET.
239
240 If a property with the same NAME already exists, report an error."
241
242 (with-default-error-location (location)
243 (let ((existing (get-property pset name nil)))
244 (when existing
245 (error "Property ~S already defined~@[ at ~A~]"
246 name (p-location existing)))
247 (store-property pset name value :type type :location location))))
248
249(export 'make-property-set)
250(defun make-property-set (&rest plist)
251 "Make a new property set, with given properties.
252
253 This isn't the way to make properties when parsing, but it works well for
254 programmatic generation. The arguments should form a property list
255 (alternating keywords and values is good).
256
257 An attempt is made to guess property types from the Lisp types of the
258 values. This isn't always successful but it's not too bad. The
3109662a
MW
259 alternative is manufacturing a `property-value' object by hand and
260 stuffing into the set."
dea4d055
MW
261
262 (property-set plist))
263
264(export 'property-set)
265(defgeneric property-set (thing)
266 (:documentation
267 "Convert THING into a property set.")
268 (:method ((pset pset)) pset)
269 (:method ((list list))
270 "Convert a list into a property set. This works for alists and plists."
271 (multiple-value-bind (next name value)
272 (if (and list (consp (car list)))
273 (values #'cdr #'caar #'cdar)
274 (values #'cddr #'car #'cadr))
275 (do ((pset (make-pset))
276 (list list (funcall next list)))
277 ((endp list) pset)
278 (add-property pset (funcall name list) (funcall value list))))))
279
280(export 'check--unused-properties)
281(defun check-unused-properties (pset)
282 "Issue errors about unused properties in PSET."
283 (when pset
284 (pset-map (lambda (prop)
285 (unless (p-seenp prop)
286 (cerror*-with-location (p-location prop)
287 "Unknown property `~A'"
288 (p-name prop))
289 (setf (p-seenp prop) t)))
290 pset)))
291
292;;;--------------------------------------------------------------------------
293;;; Utility macros.
294
295(defmacro default-slot-from-property
296 ((instance slot slot-names)
297 (pset property type
298 &optional (pvar (gensym "PROP-"))
299 &rest convert-forms)
300 &body default-forms)
301 "Initialize a slot from a property.
302
303 We initialize SLOT in INSTANCE. In full: if PSET contains a property
304 called NAME, then convert it to TYPE, bind the value to PVAR and evaluate
305 CONVERT-FORMS -- these default to just using the property value. If
306 there's no property, and the slot is named in SLOT-NAMES and currently
307 unbound, then evaluate DEFAULT-FORMS and use their value to compute the
308 slot value."
309
310 (once-only (instance slot slot-names pset property type)
311 (with-gensyms (floc)
312 `(multiple-value-bind (,pvar ,floc)
313 (get-property ,pset ,property ,type)
314 (if ,floc
315 (setf (slot-value ,instance ,slot)
316 (with-default-error-location (,floc)
317 ,@(or convert-forms `(,pvar))))
318 (default-slot (,instance ,slot ,slot-names)
319 ,@default-forms))))))
320
321;;;----- That's all, folks --------------------------------------------------