summaryrefslogtreecommitdiff
path: root/module/language/cps/contification.scm
blob: fe0a3ad3d22d1eb000b7fabfb17742a7905dc31c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
;;; Continuation-passing style (CPS) intermediate language (IL)

;; Copyright (C) 2013 Free Software Foundation, Inc.

;;;; This library is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU Lesser General Public
;;;; License as published by the Free Software Foundation; either
;;;; version 3 of the License, or (at your option) any later version.
;;;;
;;;; This library is distributed in the hope that it will be useful,
;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
;;;; Lesser General Public License for more details.
;;;;
;;;; You should have received a copy of the GNU Lesser General Public
;;;; License along with this library; if not, write to the Free Software
;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

;;; Commentary:
;;;
;;; Contification is a pass that turns $fun instances into $cont
;;; instances if all calls to the $fun return to the same continuation.
;;; This is a more rigorous variant of our old "fixpoint labels
;;; allocation" optimization.
;;;
;;; See Kennedy's "Compiling with Continuations, Continued", and Fluet
;;; and Weeks's "Contification using Dominators".
;;;
;;; Code:

(define-module (language cps contification)
  #:use-module (ice-9 match)
  #:use-module ((srfi srfi-1) #:select (concatenate filter-map))
  #:use-module (srfi srfi-26)
  #:use-module (language cps)
  #:use-module (language cps dfg)
  #:use-module (language cps primitives)
  #:use-module (language bytecode)
  #:export (contify))

(define (compute-contification fun)
  (let* ((dfg (compute-dfg fun))
         (cont-table (dfg-cont-table dfg))
         (scope-table (make-hash-table))
         (call-substs '())
         (cont-substs '())
         (fun-elisions '())
         (cont-splices (make-hash-table)))
    (define (subst-call! sym arities body-ks)
      (set! call-substs (acons sym (map cons arities body-ks) call-substs)))
    (define (subst-return! old-tail new-tail)
      (set! cont-substs (acons old-tail new-tail cont-substs)))
    (define (elide-function! k cont)
      (set! fun-elisions (acons k cont fun-elisions)))
    (define (splice-conts! scope conts)
      (for-each (match-lambda
                 (($ $cont k) (hashq-set! scope-table k scope)))
                conts)
      (hashq-set! cont-splices scope
                  (append conts (hashq-ref cont-splices scope '()))))

    (define (lookup-return-cont k)
      (match (assq-ref cont-substs k)
        (#f k)
        (k (lookup-return-cont k))))

    ;; If K is a continuation that binds one variable, and it has only
    ;; one predecessor, return that variable.
    (define (bound-symbol k)
      (match (lookup-cont k cont-table)
        (($ $kargs (_) (sym))
         (match (lookup-predecessors k dfg)
           ((_)
            ;; K has one predecessor, the one that defined SYM.
            sym)
           (_ #f)))
        (_ #f)))

    (define (contify-fun term-k sym self tail arities bodies)
      (contify-funs term-k
                    (list sym) (list self) (list tail)
                    (list arities) (list bodies)))

    ;; Given a set of mutually recursive functions bound to local
    ;; variables SYMS, with self symbols SELFS, tail continuations
    ;; TAILS, arities ARITIES, and bodies BODIES, all bound in TERM-K,
    ;; contify them if we can prove that they all return to the same
    ;; continuation.  Returns a true value on success, and false
    ;; otherwise.
    (define (contify-funs term-k syms selfs tails arities bodies)
      (define (unused? sym)
        (null? (lookup-uses sym dfg)))

      ;; Are the given args compatible with any of the arities?
      (define (applicable? proc args)
        (let lp ((arities (assq-ref (map cons syms arities) proc)))
          (match arities
            ((($ $arity req () #f () #f) . arities)
             (or (= (length args) (length req))
                 (lp arities)))
            ;; If we reached the end of the arities, fail.  Also fail if
            ;; the next arity in the list has optional, keyword, or rest
            ;; arguments.
            (_ #f))))

      ;; If the use of PROC in continuation USE is a call to PROC that
      ;; is compatible with one of the procedure's arities, return the
      ;; target continuation.  Otherwise return #f.
      (define (call-target use proc)
        (match (find-call (lookup-cont use cont-table))
          (($ $continue k src ($ $call proc* args))
           (and (eq? proc proc*) (not (memq proc args)) (applicable? proc args)
                ;; Converge more quickly by resolving already-contified
                ;; call targets.
                (lookup-return-cont k)))
          (_ #f)))

      ;; If this set of functions is always called with one
      ;; continuation, not counting tail calls between the functions,
      ;; return that continuation.
      (define (find-common-continuation)
        (let visit-syms ((syms syms) (k #f))
          (match syms
            (() k)
            ((sym . syms)
             (let visit-uses ((uses (lookup-uses sym dfg)) (k k))
               (match uses
                 (() (visit-syms syms k))
                 ((use . uses)
                  (and=> (call-target use sym)
                         (lambda (k*)
                           (cond
                            ((memq k* tails) (visit-uses uses k))
                            ((not k) (visit-uses uses k*))
                            ((eq? k k*) (visit-uses uses k))
                            (else #f)))))))))))

      ;; Given that the functions are called with the common
      ;; continuation K, determine the scope at which to contify the
      ;; functions.  If K is in scope in the term, we go ahead and
      ;; contify them there.  Otherwise the scope is inside the letrec
      ;; body, and so choose the scope in which the continuation is
      ;; defined, whose free variables are a superset of the free
      ;; variables of the functions.
      ;;
      ;; There is some slight trickiness here.  Call-target already uses
      ;; the information we compute within this pass.  Previous
      ;; contifications may cause functions to be contified not at their
      ;; point of definition but at their point of non-recursive use.
      ;; That will cause the scope nesting to change.  (It may
      ;; effectively push a function deeper down the tree -- the second
      ;; case above, a call within the letrec body.)  What if we contify
      ;; to the tail of a previously contified function?  We have to
      ;; track what the new scope tree will be when asking whether K
      ;; will be bound in TERM-K's scope, not the scope tree that
      ;; existed when we started the pass.
      ;;
      ;; FIXME: Does this choose the right scope for contified let-bound
      ;; functions?
      (define (find-contification-scope k)
        (define (scope-contains? scope k)
          (let ((k-scope (or (hashq-ref scope-table k)
                             (let ((k-scope (lookup-block-scope k dfg)))
                               (hashq-set! scope-table k k-scope)
                               k-scope))))
            (or (eq? scope k-scope)
                (and k-scope (scope-contains? scope k-scope)))))

        ;; Find the scope of K.
        (define (continuation-scope k)
          (or (hashq-ref scope-table k)
              (let ((scope (lookup-block-scope k dfg)))
                (hashq-set! scope-table k scope)
                scope)))

        (let ((k-scope (continuation-scope k)))
          (if (scope-contains? k-scope term-k)
              term-k
              (match (lookup-cont k-scope cont-table)
                (($ $kentry self tail clauses)
                 ;; K is the tail of some function.  If that function
                 ;; has just one clause, return that clause.  Otherwise
                 ;; bail.
                 (match clauses
                   ((($ $cont _ ($ $kclause arity ($ $cont kargs))))
                    kargs)
                   (_ #f)))
                (_ k-scope)))))

      ;; We are going to contify.  Mark all SYMs for replacement in
      ;; calls, and mark the tail continuations for replacement by K.
      ;; Arrange for the continuations to be spliced into SCOPE.
      (define (enqueue-contification! k scope)
        (for-each (lambda (sym tail arities bodies)
                    (match bodies
                      ((($ $cont body-k) ...)
                       (subst-call! sym arities body-k)))
                    (subst-return! tail k))
                  syms tails arities bodies)
        (splice-conts! scope (concatenate bodies))
        #t)

      ;; "Call me maybe"
      (and (and-map unused? selfs)
           (and=> (find-common-continuation)
                  (lambda (k)
                    (and=> (find-contification-scope k)
                           (cut enqueue-contification! k <>))))))

    (define (visit-fun term)
      (match term
        (($ $fun src meta free body)
         (visit-cont body))))
    (define (visit-cont cont)
      (match cont
        (($ $cont sym ($ $kargs _ _ body))
         (visit-term body sym))
        (($ $cont sym ($ $kentry self tail clauses))
         (for-each visit-cont clauses))
        (($ $cont sym ($ $kclause arity body))
         (visit-cont body))
        (($ $cont)
         #t)))
    (define (visit-term term term-k)
      (match term
        (($ $letk conts body)
         (for-each visit-cont conts)
         (visit-term body term-k))
        (($ $letrec names syms funs body)
         (define (split-components nsf)
           ;; FIXME: Compute strongly-connected components.  Currently
           ;; we just put non-recursive functions in their own
           ;; components, and lump everything else in the remaining
           ;; component.
           (define (recursive? k)
             (or-map (cut variable-free-in? <> k dfg) syms))
           (let lp ((nsf nsf) (rec '()))
             (match nsf
               (()
                (if (null? rec)
                    '()
                    (list rec)))
               (((and elt (n s ($ $fun src meta free ($ $cont kentry))))
                 . nsf)
                (if (recursive? kentry)
                    (lp nsf (cons elt rec))
                    (cons (list elt) (lp nsf rec)))))))
         (define (visit-component component)
           (match component
             (((name sym fun) ...)
              (match fun
                ((($ $fun src meta free
                     ($ $cont fun-k
                        ($ $kentry self
                           ($ $cont tail-k ($ $ktail))
                           (($ $cont _ ($ $kclause arity body))
                            ...))))
                  ...)
                 (if (contify-funs term-k sym self tail-k arity body)
                     (for-each (cut for-each visit-cont <>) body)
                     (for-each visit-fun fun)))))))
         (visit-term body term-k)
         (for-each visit-component
                   (split-components (map list names syms funs))))
        (($ $continue k src exp)
         (match exp
           (($ $fun src meta free
               ($ $cont fun-k
                  ($ $kentry self
                     ($ $cont tail-k ($ $ktail))
                     (($ $cont _ ($ $kclause arity body)) ...))))
            (if (and=> (bound-symbol k)
                       (lambda (sym)
                         (contify-fun term-k sym self tail-k arity body)))
                (begin
                  (elide-function! k (lookup-cont k cont-table))
                  (for-each visit-cont body))
                (visit-fun exp)))
           (_ #t)))))

    (visit-fun fun)
    (values call-substs cont-substs fun-elisions cont-splices)))

(define (apply-contification fun call-substs cont-substs fun-elisions cont-splices)
  (define (contify-call src proc args)
    (and=> (assq-ref call-substs proc)
           (lambda (clauses)
             (let lp ((clauses clauses))
               (match clauses
                 (() (error "invalid contification"))
                 (((($ $arity req () #f () #f) . k) . clauses)
                  (if (= (length req) (length args))
                      (build-cps-term
                        ($continue k src
                          ($values args)))
                      (lp clauses)))
                 ((_ . clauses) (lp clauses)))))))
  (define (continue k src exp)
    (define (lookup-return-cont k)
      (match (assq-ref cont-substs k)
        (#f k)
        (k (lookup-return-cont k))))
    (let ((k* (lookup-return-cont k)))
      ;; We are contifying this return.  It must be a call or a
      ;; primcall to values, return, or return-values.
      (if (eq? k k*)
          (build-cps-term ($continue k src ,exp))
          (rewrite-cps-term exp
            (($ $primcall 'return (val))
             ($continue k* src ($primcall 'values (val))))
            (($ $values vals)
             ($continue k* src ($primcall 'values vals)))
            (_ ($continue k* src ,exp))))))
  (define (splice-continuations term-k term)
    (match (hashq-ref cont-splices term-k)
      (#f term)
      ((cont ...)
       (let lp ((term term))
         (rewrite-cps-term term
           (($ $letrec names syms funs body)
            ($letrec names syms funs ,(lp body)))
           (($ $letk conts* body)
            ($letk ,(append conts* (filter-map visit-cont cont))
              ,body))
           (body
            ($letk ,(filter-map visit-cont cont)
              ,body)))))))
  (define (visit-fun term)
    (rewrite-cps-exp term
      (($ $fun src meta free body)
       ($fun src meta free ,(visit-cont body)))))
  (define (visit-cont cont)
    (rewrite-cps-cont cont
      (($ $cont (? (cut assq <> fun-elisions)))
       ;; This cont gets inlined in place of the $fun.
       ,#f)
      (($ $cont sym ($ $kargs names syms body))
       (sym ($kargs names syms ,(visit-term body sym))))
      (($ $cont sym ($ $kentry self tail clauses))
       (sym ($kentry self ,tail ,(map visit-cont clauses))))
      (($ $cont sym ($ $kclause arity body))
       (sym ($kclause ,arity ,(visit-cont body))))
      (($ $cont)
       ,cont)))
  (define (visit-term term term-k)
    (match term
      (($ $letk conts body)
       ;; Visit the body first, so we rewrite depth-first.
       (let lp ((body (visit-term body term-k)))
         ;; Because we attach contified functions on a particular
         ;; term-k, and one term-k can correspond to an arbitrarily
         ;; nested sequence of $letrec and $letk instances, normalize
         ;; so that all continuations are bound by one $letk --
         ;; guaranteeing that they are in the same scope.
         (rewrite-cps-term body
           (($ $letrec names syms funs body)
            ($letrec names syms funs ,(lp body)))
           (($ $letk conts* body)
            ($letk ,(append conts* (filter-map visit-cont conts))
              ,body))
           (body
            ($letk ,(filter-map visit-cont conts)
              ,body)))))
      (($ $letrec names syms funs body)
       (rewrite-cps-term (filter (match-lambda
                                  ((n s f) (not (assq s call-substs))))
                                 (map list names syms funs))
         (((names syms funs) ...)
          ($letrec names syms (map visit-fun funs)
                   ,(visit-term body term-k)))))
      (($ $continue k src exp)
       (splice-continuations
        term-k
        (match exp
          (($ $fun)
           (cond
            ((assq-ref fun-elisions k)
             => (match-lambda
                 (($ $kargs (_) (_) body)
                  (visit-term body k))))
            (else
             (continue k src (visit-fun exp)))))
          (($ $call proc args)
           (or (contify-call src proc args)
               (continue k src exp)))
          (_ (continue k src exp)))))))
  (visit-fun fun))

(define (contify fun)
  (call-with-values (lambda () (compute-contification fun))
    (lambda (call-substs cont-substs fun-elisions cont-splices)
      (if (null? call-substs)
          fun
          ;; Iterate to fixed point.
          (contify
           (apply-contification fun call-substs cont-substs fun-elisions cont-splices))))))