Skip to content

Qi Compiler Sync Oct 6 2022

Siddhartha Kasivajhula edited this page Dec 15, 2022 · 5 revisions

Alternative Compiler Strategies to Support Bindings

Qi Compiler Sync Oct 6 2022

Adjacent meetings: Previous | Up | Next

Summary

We discussed two different ways in which bindings could be implemented in the compiler -- a "lifting lets" (ANF) approach and a "continuation passing" (CPS) approach. We also briefly discussed a possible approach to "deforestation" (a compiler optimization) that recently came up.

Background

Last time, we talked about possible binding and scoping rules in Qi, and settled on a design. Briefly:

  • bindings are scoped to the outermost containing threading form (i.e. ~> or ~>>)
  • in the case of bindings appearing in a tee junction, earlier tines bind later tines

These rules can be ensured at the expander level by notating them in bindingspec, and they also need to be implemented at the compiler level.

Implementation Strategies in the Compiler

"Lifting Lets" or A-Normal Form

When an as binding form is encountered within a threading form, wrap the (outermost) containing ~> with a let form binding those identifiers to placeholder values (e.g. undefined). Then, transform each occurrence of (as ...) to a use of set! to mutate the declared value.

Specifically, this approach entails the transformation:

  1. Escape to Racket to wrap outermost ~> with let and re-enter a Qi context:
  (~> flo ... (... (as name) ...))
  ...
   ↓
  ...
  (esc (let ([name (void)])
         (☯ original-flow)))
  1. as → set!
  (as name)
  ...
   ↓
  ...
  (~> (esc (λ (x) (set! name x))) ⏚)
  1. Overall transformation:
  (~> flo ... (... (as name) ...))
  ...
   ↓
  ...
  (esc (let ([name (void)])
         (☯ (~> flo ... (... (~> (esc (λ (x) (set! name x))) ⏚) ...)))))

This approach involves an extra initial pass to lift the lets, but once that's done, each component can be separately compiled.

"Continuation Passing"

This approach involves nesting to the right instead of separately compiling the components, ensuring sequential scope by wrapping subsequent components in layers of lambdas and call-with-values.

For instance,

(~> (~> f1 (as x)) (~> (lambda (y) (+ x y))))

... compiles to:

(lambda vs1
  (call-with-values
   (apply f1 vs1)
   (lambda vs2
     (let-values ([(x) (apply values vs2)])
       (apply (lambda (y) (+ x y))
              vs2)))))

More illustrative code examples are in the Appendix below.

Which Approach Should We Use?

We agreed that it would be good to continue with the ANF/lifting lets approach since it is perhaps simpler. We can revisit this in the future if any other considerations come to light (for instance, is the output of one or the other easier for the Racket compiler to reason about and therefore optimize? Naively, CPS seems to be more explicit about the binding structure).

Deforestation

We discussed a possible approach to deforestation that recently came up. We agreed that it would perhaps be best to treat map and filter as primitives as far as optimization rules are concerned since it may not be possible to compose folds in the general case.

Next Steps

Appendix - Code Illustrating the Difference between the ANF and CPS Approaches

(From Michael, also including some examples of compilation steps and optimizations)

#lang racket/base

(match x
  [(cons (cons a b) (cons c (? (lambda (v) (eq? v b)) d))
         e])

;; compiles to

(let ([v x])
  (if (pair? v)
      (let ([v1 (car v)]
            [v2 (cdr v)])
        (match v1
          [(cons a b)
           (match v2
             [(cons c d)
              e])]))))

(~> (~> f1 (as x)) (~> (lambda (y) (+ x y))))

(let ([x undefined])
  (~> (~> f1 (as x)) (~> (lambda (y) (+ x y)))))

[(~> f1 f2)
 #`(compose
    #,(compile-flow f2)
    #,(compile-flow f1))]

;; compiles to

(let ([x undefined])
  (compose
   (~> (lambda (y) (+ x y)))
   (~> (~> f1 (as x)))))

(let ([x undefined])
  (compose
   (lambda (y) (+ x y))
   (compose
    (as x)
    f1)))

(let ([x undefined])
  (compose
   (lambda (y) (+ x y))
   (compose
    (lambda (v) (set! x v) v)
    f1)))


(~> (~> f1 (as x)) (~> (lambda (y) (+ x y))))

;; compiles to...

(lambda vs1
  (call-with-values
   (apply f1 vs1)
   (lambda vs2
     (let-values ([(x) (apply values vs2)])
       (apply (lambda (y) (+ x y))
              vs2)))))

[(~> f1:id f2)
 #'(lambda vs1
     (call-with-values
      (apply f1 vs)
      f2))]

(~> (~> (~> f1 f2) f3) f4)

(~> f1
    (~> f2
        (~> f3
            f4)))


(define (compile-flow f later)
  (syntax-parse f
    #:datum-literals (~>)
    [(~> f1 f2)
     (compile-flow #'f1
                   #'(lambda vs
                       (apply later vs)))]
    [f:id
     #'(lambda vs
         (call-with-values
          (lambda () (apply f vs))
          later))]    [(as x)
     #'(lambda (x)
         later)]))

(~> (~> f1 x)
    f2)
               
               
 #'(lambda vs1
     (call-with-values
      (apply f1 vs)
      f2))]
       

(~>
 (~> f1^      ; must return one value
     (as x))  ; whole thread returns one value
 (~> (lambda (y) (+ x y)) ; expects one value
     ))                   ; thread returns one value

;; drop extra ~>
(~>
 (~> f1^      ; must return one value
     (as x))  ; whole thread returns one value
 (lambda (y) (+ x y)) ; expects one value
 )

;; fixed arity to pass; implement as just an application (or a let)

(lambda (vs)
  (let-values ([(v1) (let-values ([(v2) (apply f1 vs)])
                       ;; problem: can't bind x to be visible later because not ANF
                       (as x))])
    ((lambda (y) (+ x y))
     x)))

;; lift out lets via ANF, expand (as x) to insert let binding

(lambda vs
  (let-values ([(v2) (apply f1 vs)])
    (let-values ([(x) v2])
      (let-values ([(v1) v2])
        ((lambda (y) (+ x y))
         x)))))

; Drop no-op let-values, inline application where argument is just a reference

(lambda vs1
  (let ([x^ (apply f1 vs)])
    (+ x x^)))


#lang racket/base


(require syntax/parse)

(define (compile-flow f later-stx)
  (define/syntax-parse later later-stx)
  (syntax-parse f
    #:datum-literals (~> as)
    [(~> f1 f2)
     (compile-flow #'f1
                   (compile-flow #'f2 #'later))]
    [f:id
     #'(lambda vs
         (call-with-values
          (lambda () (apply f vs))
          later))]
    [(as x)
     #'(lambda (x)
         (later x))]))

(compile-flow #'(~> (~> f1 (as x))
                    f2)
              #'values)

(-< f1 f2)

#;(lambda vs
    (call-with-values
     (lambda () (apply f1 vs))
     (lambda (x)
       ((lambda vs
          (call-with-values
           (lambda ()
             (apply f2 vs))
           values))
        x))))

Attendees

Camilo, Michael, Sid

Clone this wiki locally