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