chiark / gitweb /
src/method-aggregate.lisp: Allow useful behaviour if no primary methods.
[sod] / src / method-aggregate.lisp
1 ;;; -*-lisp-*-
2 ;;;
3 ;;; Aggregating method combinations
4 ;;;
5 ;;; (c) 2015 Straylight/Edgeware
6 ;;;
7
8 ;;;----- Licensing notice ---------------------------------------------------
9 ;;;
10 ;;; This file is part of the Sensible 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 ;;; Classes and protocol.
30
31 (export '(aggregating-message
32           sod-message-combination sod-message-kernel-function))
33 (defclass aggregating-message (simple-message)
34   ((combination :initarg :combination :type keyword
35                 :reader sod-message-combination)
36    (plist :type list :accessor sod-message-plist)
37    (kernel-function :type function :reader sod-message-kernel-function))
38   (:documentation
39    "Message class for aggregating method combinations.
40
41    An aggregating method combination invokes the primary methods in order,
42    most-specific first, collecting their return values, and combining them
43    together in some way to produce a result for the effective method as a
44    whole.
45
46    Mostly, this is done by initializing an accumulator to some appropriate
47    value, updating it with the result of each primary method in turn, and
48    finally returning some appropriate output function of it.  The order is
49    determined by the `:most-specific' property, which may have the value
50    `:first' or `:last'.
51
52    The `progn' method combination is implemented as a slightly weird special
53    case of an aggregating method combination with a trivial state.  More
54    typical combinations are `:sum', `:product', `:min', `:max', `:and', and
55    `:or'.  Finally, there's a `custom' combination which uses user-supplied
56    code fragments to stitch everything together."))
57
58 (export 'aggregating-message-properties)
59 (defgeneric aggregating-message-properties (message combination)
60   (:documentation
61    "Return a description of the properties needed by the method COMBINATION.
62
63    The description should be a plist of alternating property name and type
64    keywords.  The named properties will be looked up in the pset supplied at
65    initialization time, and supplied to `compute-aggregating-message-kernel'
66    as keyword arguments.  Defaults can be supplied in method BVLs.
67
68    The default is not to capture any property values.
69
70    The reason for this is as not to retain the pset beyond message object
71    initialization.")
72   (:method (message combination) nil))
73
74 (export 'compute-aggregating-message-kernel)
75 (defgeneric compute-aggregating-message-kernel
76     (message combination codegen target methods arg-names &key)
77   (:documentation
78    "Determine how to aggregate the direct methods for an aggregating message.
79
80    The return value is a function taking arguments (CODEGEN TARGET ARG-NAMES
81    METHODS): it should emit, to CODEGEN, an appropriate effective-method
82    kernel which invokes the listed direct METHODS, in the appropriate order,
83    collects and aggregates their values, and delivers to TARGET the final
84    result of the method kernel.
85
86    The easy way to implement this method is to use the macro
87    `define-aggregating-method-combination'."))
88
89 (export 'check-aggregating-message-type)
90 (defgeneric check-aggregating-message-type (message combination type)
91   (:documentation
92    "Check that TYPE is an acceptable function TYPE for the COMBINATION.
93
94    For example, `progn' messages must return `void', while `and' and `or'
95    messages must return `int'.")
96   (:method (message combination type)
97     t))
98
99 (defgeneric aggregating-message-method-return-type (message combination)
100   (:documentation
101    "Return the primary method return type for this MESSAGE and COMBINATION.")
102   (:method ((message aggregating-message) (combination t))
103     (c-type-subtype (sod-message-type message))))
104
105 (export 'aggregating-effective-method)
106 (defclass aggregating-effective-method (simple-effective-method) ()
107   (:documentation "Effective method counterpart to `aggregating-message'."))
108
109 (defgeneric aggregating-message-always-live-p (message combination)
110   (:documentation
111    "Return whether the method combination can work without primary methods.
112
113    Return non-nil if the corresponding effective method should be considered
114    live even if it doesn't have any methods.")
115   (:method ((message aggregating-message) (combination t)) nil))
116
117 (defmethod effective-method-live-p ((method aggregating-effective-method))
118   (or (let* ((message (effective-method-message method))
119              (comb (sod-message-combination message)))
120         (aggregating-message-always-live-p message comb))
121       (call-next-method)))
122
123 ;;;--------------------------------------------------------------------------
124 ;;; Implementation.
125
126 (defmethod check-message-type ((message aggregating-message) type)
127   (with-slots (combination) message
128     (check-aggregating-message-type message combination type)))
129
130 (defmethod sod-message-effective-method-class ((message aggregating-message))
131   'aggregating-effective-method)
132
133 (defmethod simple-method-body
134     ((method aggregating-effective-method) codegen target)
135   (let ((argument-names (effective-method-basic-argument-names method))
136         (primary-methods (effective-method-primary-methods method)))
137     (funcall (sod-message-kernel-function (effective-method-message method))
138              codegen target argument-names primary-methods)))
139
140 (defmethod shared-initialize :before
141     ((message aggregating-message) slot-names &key pset)
142   (declare (ignore slot-names))
143   (with-slots (combination plist kernel-function) message
144     (let ((most-specific (get-property pset :most-specific :keyword :first))
145           (comb (get-property pset :combination :keyword)))
146
147       ;; Check that we've been given a method combination and make sure it
148       ;; actually exists.
149       (unless comb
150         (error "The `combination' property is required."))
151       (unless (some (lambda (method)
152                       (let* ((specs (method-specializers method))
153                              (message-spec (car specs))
154                              (combination-spec (cadr specs)))
155                         (and (typep message-spec 'class)
156                              (typep message message-spec)
157                              (typep combination-spec 'eql-specializer)
158                              (eq (eql-specializer-object combination-spec)
159                                  comb))))
160                     (generic-function-methods
161                      #'compute-aggregating-message-kernel))
162         (error "Unknown method combination `~(~A~)'." comb))
163       (setf combination comb)
164
165       ;; Make sure the ordering is actually valid.
166       (unless (member most-specific '(:first :last))
167         (error "The `most_specific' property must be `first' or `last'."))
168
169       ;; Set up the function which will compute the kernel.
170       (let ((magic (cons nil nil))
171             (keys nil))
172
173         ;; Collect the property values wanted by the method combination.
174         (do ((want (aggregating-message-properties message comb)
175                    (cddr want)))
176             ((endp want))
177           (let* ((name (car want))
178                  (type (cadr want))
179                  (prop (get-property pset name type magic)))
180             (unless (eq prop magic)
181               (setf keys (list* name prop keys)))))
182         (setf plist keys)
183
184         ;; Set the kernel function for later.
185         (setf kernel-function
186               (lambda (codegen target arg-names methods)
187                 (apply #'compute-aggregating-message-kernel
188                        message comb
189                        codegen target
190                        (ecase most-specific
191                          (:first methods)
192                          (:last (setf methods (reverse methods))))
193                        arg-names
194                        plist)))))))
195
196 (defmethod check-method-type
197     ((method sod-method) (message aggregating-message)
198      (type c-function-type))
199   (let ((wanted (aggregating-message-method-return-type
200                  message (sod-message-combination message)))
201         (msgtype (sod-message-type message)))
202     (check-method-return-type type wanted)
203     (check-method-argument-lists type msgtype)))
204
205 ;;;--------------------------------------------------------------------------
206 ;;; Utilities.
207
208 (export 'define-aggregating-method-combination)
209 (defmacro define-aggregating-method-combination
210     (comb
211      (vars
212       &key (codegen (gensym "CODEGEN-"))
213            (methods (gensym "METHODS-")))
214      &key properties return-type
215           ((:around around-func) '#'funcall)
216           ((:empty empty-func) nil emptyp)
217           ((:first-method first-method-func) nil firstp)
218           ((:methods methods-func) '#'funcall))
219   "Utility macro for definining aggregating method combinations.
220
221    The VARS are a list of variable names to be bound to temporary variable
222    objects of the method's return type.  Additional keyword arguments define
223    variables names to be bound to other possibly interesting values:
224
225      * CODEGEN is the `codegen' object passed at effective-method computation
226        time; and
227
228      * METHODS is the list of primary methods, in the order in which they
229        should be invoked.  Note that this list must be non-empty, since
230        otherwise the method on `compute-effective-method-body' specialized to
231        `simple-effective-method' will suppress the method entirely.
232
233    The PROPERTIES, if specified, are a list of properties to be collected
234    during message-object initialization; items in the list have the form
235
236            (([KEYWORD] NAME) TYPE [DEFAULT] [SUPPLIEDP])
237
238    similar to a `&key' BVL entry, except for the additional TYPE entry.  In
239    particular, a symbolic NAME may be written in place of a singleton list.
240    The KEYWORD names the property as it should be looked up in the pset,
241    while the NAME names a variable to which the property value or default is
242    bound.
243
244    All of these variables, and the VARS, are available in the functions
245    described below.
246
247    If a RETURN-TYPE is given, it's a C-type S-expression: a method is defined
248    on `check-aggregating-message-type' to check the that the message's return
249    type matches RETURN-TYPE.
250
251    If an EMPTY function is given, then (a) it's OK if there are no primary
252    methods, because (b) the EMPTY function is called to set the return
253    value variable in this case.  Note that EMPTY is only called when there
254    are no primary methods.
255
256    The AROUND, FIRST-METHOD, and METHODS are function designators (probably
257    `lambda' forms) providing pieces of the aggregating behaviour.
258
259    The AROUND function is called first, with a single argument BODY, though
260    the variables above are also in scope.  It is expected to emit code to
261    CODEGEN which invokes the METHODS in the appropriate order, and arranges
262    to store the aggregated return value in the first of the VARS.
263
264    It may call BODY as a function in order to assist with this; let ARGS be
265    the list of arguments supplied to it.  The default behaviour is to call
266    BODY with no arguments.  The BODY function first calls FIRST-METHOD,
267    passing it as arguments a function INVOKE and the ARGS which were passed
268    to BODY, and then calls METHODS once for each remaining method, again
269    passing an INVOKE function and the ARGS.  If FIRST-METHOD is not
270    specified, then the METHODS function is used for all of the methods.  If
271    METHODS is not specified, then the behaviour is simply to call INVOKE
272    immediately.  (See the definition of the `:progn' method combination.)
273
274    Calling (funcall INVOKE [TARGET]) emits instructions to CODEGEN to call
275    the appropriate direct method and deliver its return value to TARGET,
276    which defaults to `:void'."
277
278   (with-gensyms (type msg combvar target arg-names args want-type
279                  meth targ func call-methfunc
280                  aroundfunc fmethfunc methfunc bodyfunc)
281     `(progn
282
283        ;; If properties are listed, arrange for them to be collected.
284        ,@(and properties
285               `((defmethod aggregating-message-properties
286                     ((,msg aggregating-message) (,combvar (eql ',comb)))
287                   ',(mapcan (lambda (prop)
288                               (list (let* ((name (car prop))
289                                            (names (if (listp name) name
290                                                       (list name))))
291                                       (if (cddr names) (car names)
292                                           (intern (car names) :keyword)))
293                                     (cadr prop)))
294                             properties))))
295
296        ;; If a particular return type is wanted, check that.
297        ,@(and return-type
298               `((defmethod check-aggregating-message-type
299                     ((,msg aggregating-message)
300                      (,combvar (eql ',comb))
301                      (,type c-function-type))
302                   (let ((,want-type (c-type ,return-type)))
303                     (unless (c-type-equal-p (c-type-subtype ,type)
304                                             ,want-type)
305                       (error "Messages with `~(~A~)' combination ~
306                               must return `~A'."
307                              ,combvar ,want-type)))
308                   (call-next-method))))
309
310        ;; If there is an EMPTY function then the effective method is always
311        ;; live.
312        ,@(and emptyp
313               `((defmethod aggregating-message-always-live-p
314                     ((,msg aggregating-message)
315                      (,combvar (eql ',comb)))
316                   t)))
317
318        ;; Define the main kernel-compuation method.
319        (defmethod compute-aggregating-message-kernel
320            ((,msg aggregating-message) (,combvar (eql ',comb))
321             ,codegen ,target ,methods ,arg-names
322             &key ,@(mapcar (lambda (prop) (cons (car prop) (cddr prop)))
323                            properties))
324          (declare (ignore ,combvar))
325
326          ;; Declare the necessary variables and give names to the functions
327          ;; supplied by the caller.
328          (let* (,@(and vars
329                        `((,type (c-type-subtype (sod-message-type ,msg)))
330                          (,(car vars) (temporary-var ,codegen ,type))))
331                 ,@(mapcar (lambda (var)
332                             (list var `(and ,methods
333                                             (temporary-var ,codegen ,type))))
334                           (cdr vars))
335                 (,aroundfunc ,around-func)
336                 (,methfunc ,methods-func)
337                 (,fmethfunc ,(if firstp first-method-func methfunc)))
338
339            (flet ((,bodyfunc ()
340                     (funcall ,aroundfunc
341                              (lambda (&rest ,args)
342                                (flet ((,call-methfunc (,func ,meth)
343                                         ;; Call FUNC, passing it an INVOKE
344                                         ;; function which will generate a
345                                         ;; call to METH.
346                                         (apply ,func
347                                                (lambda
348                                                    (&optional (,targ :void))
349                                                  (invoke-method ,codegen
350                                                                 ,targ
351                                                                 ,arg-names
352                                                                 ,meth))
353                                                ,args)))
354
355                                  ;; The first method might need special
356                                  ;; handling.
357                                  (,call-methfunc ,fmethfunc (car ,methods))
358
359                                  ;; Call the remaining methods in the right
360                                  ;; order.
361                                  (dolist (,meth (cdr ,methods))
362                                    (,call-methfunc ,methfunc ,meth)))))))
363
364              ;; Arrange to release the temporaries when we're finished with
365              ;; them.
366              (unwind-protect
367                   (progn
368
369                     ;; If there are no direct methods, then just do the
370                     ;; empty-effective-method thing to set the return
371                     ;; variable.  Otherwise, wrap AROUND round the main body.
372                     ,(if emptyp
373                          `(if (null ,methods)
374                               (funcall ,empty-func)
375                               (,bodyfunc))
376                          `(,bodyfunc))
377
378                   ;; Outside the AROUND function now, deliver the final
379                   ;; result to the right place.
380                   (deliver-expr ,codegen ,target ,(car vars)))
381
382                ;; Finally, release the temporary variables.
383                ,@(mapcar (lambda (var)
384                            `(when ,var (setf (var-in-use-p ,var) nil)))
385                          vars)))))
386
387        ',comb)))
388
389 ;;;--------------------------------------------------------------------------
390 ;;; Fixed aggregating method combinations.
391
392 (define-aggregating-method-combination :progn (nil)
393   :return-type void
394   :empty (lambda () nil))
395
396 (define-aggregating-method-combination :sum ((acc val) :codegen codegen)
397   :empty (lambda () (emit-inst codegen (make-set-inst acc 0)))
398   :first-method (lambda (invoke)
399                   (funcall invoke val)
400                   (emit-inst codegen (make-set-inst acc val)))
401   :methods (lambda (invoke)
402              (funcall invoke val)
403              (emit-inst codegen (make-update-inst acc #\+ val))))
404
405 (define-aggregating-method-combination :product ((acc val) :codegen codegen)
406   :empty (lambda () (emit-inst codegen (make-set-inst acc 1)))
407   :first-method (lambda (invoke)
408                   (funcall invoke val)
409                   (emit-inst codegen (make-set-inst acc val)))
410   :methods (lambda (invoke)
411              (funcall invoke val)
412              (emit-inst codegen (make-update-inst acc #\* val))))
413
414 (define-aggregating-method-combination :min ((acc val) :codegen codegen)
415   :first-method (lambda (invoke)
416                   (funcall invoke val)
417                   (emit-inst codegen (make-set-inst acc val)))
418   :methods (lambda (invoke)
419              (funcall invoke val)
420              (emit-inst codegen (make-if-inst (format nil "~A > ~A" acc val)
421                                               (make-set-inst acc val)))))
422
423 (define-aggregating-method-combination :max ((acc val) :codegen codegen)
424   :first-method (lambda (invoke)
425                   (funcall invoke val)
426                   (emit-inst codegen (make-set-inst acc val)))
427   :methods (lambda (invoke)
428              (funcall invoke val)
429              (emit-inst codegen (make-if-inst (format nil "~A < ~A" acc val)
430                                               (make-set-inst acc val)))))
431
432 (define-aggregating-method-combination :and ((ret) :codegen codegen)
433   :empty (lambda () (emit-inst codegen (make-set-inst ret 1)))
434   :around (lambda (body)
435             (codegen-push codegen)
436             (funcall body)
437             (emit-inst codegen
438                        (make-do-while-inst (codegen-pop-block codegen) 0)))
439   :methods (lambda (invoke)
440              (funcall invoke ret)
441              (emit-inst codegen (make-if-inst (format nil "!~A" ret)
442                                               (make-break-inst)))))
443
444 (define-aggregating-method-combination :or ((ret) :codegen codegen)
445   :empty (lambda () (emit-inst codegen (make-set-inst ret 0)))
446   :around (lambda (body)
447             (codegen-push codegen)
448             (funcall body)
449             (emit-inst codegen
450                        (make-do-while-inst (codegen-pop-block codegen) 0)))
451   :methods (lambda (invoke)
452              (funcall invoke ret)
453              (emit-inst codegen (make-if-inst ret (make-break-inst)))))
454
455 ;;;--------------------------------------------------------------------------
456 ;;; A customizable aggregating method combination.
457
458 (defmethod aggregating-message-properties
459     ((message aggregating-message) (combination (eql :custom)))
460   '(:retvar :id
461     :valvar :id
462     :methty :type
463     :empty :fragment
464     :decls :fragment
465     :before :fragment
466     :first :fragment
467     :each :fragment
468     :after :fragment
469     :count :id))
470
471 (defmethod aggregating-message-method-return-type
472     ((message aggregating-message) (combination (eql :custom)))
473   (getf (sod-message-plist message) :methty
474         (c-type-subtype (sod-message-type message))))
475
476 (defmethod aggregating-message-always-live-p
477     ((message aggregating-message) (combination (eql :custom)))
478   (getf (sod-message-plist message) :empty))
479
480 (defmethod compute-aggregating-message-kernel
481     ((message aggregating-message) (combination (eql :custom))
482      codegen target methods arg-names
483      &key (retvar "sod_ret") (valvar "sod_val") (methty nil methtyp)
484           empty decls before each (first each) after count)
485   (let* ((type (c-type-subtype (sod-message-type message)))
486          (methty (if methtyp methty type)))
487     (unless (eq type c-type-void)
488       (ensure-var codegen retvar type))
489     (unless (or (null methods)
490                 (eq methty c-type-void))
491       (ensure-var codegen valvar methty))
492     (when (and methods count)
493       (ensure-var codegen count c-type-size-t (length methods)))
494     (when (and methods decls)
495       (emit-decl codegen decls))
496     (labels ((maybe-emit (fragment)
497                (when fragment (emit-inst codegen fragment)))
498              (invoke (method fragment)
499                (invoke-method codegen
500                               (if (eq methty c-type-void) :void valvar)
501                               arg-names method)
502                (maybe-emit fragment)))
503       (cond ((and empty (null methods))
504              (emit-inst codegen empty))
505             (t
506              (maybe-emit before)
507              (invoke (car methods) first)
508              (dolist (method (cdr methods)) (invoke method each))
509              (maybe-emit after)))
510       (deliver-expr codegen target retvar))))
511
512 ;;;----- That's all, folks --------------------------------------------------