A-normal form 再学習 その4 - Scheme VM

ここに書いてある内容は古いです。A正規形まとめをご参照ください。

A-reduction の定義は Evaluation Contexts の概念を参照する。Evaluation Contexts とは "hole" と称され[]で表現される項のことであり、特定の副項の場所に現れる。
hole の場所は、CEK semantics にしたがって次に evalute されるべき副次式(subexpression)を指し示す。
例えば (let (x M1) M2) という式の場合、Evaluation Contexts の定義に (let (x ε) M)があるので、次に簡約されるべき式は M1 でなければならない。

自信なしの訳。


変換例をなぞっていきましょう。

(+ (+ 2 2) (let (x 1) (f x)))

この式は Evaluation Contexts の (F V ... V ε M ... M) に該当し、εは (+ 2 2) になります。
(+ 2 2) が reduction されるときの Evaluation Contexts は (+ ... (let (x 1) (f x))) です。(ここがややこしい)
reduction されるべき (+ 2 2) は (F V1 ... Vn) に該当しますから、A-reductions の(A3)を適用します。

(let (t (+ 2 2))
  (+ t (let (x 1) (f x))))

ここで t は「t は FV(ε) に含まれない」という条件があるので実装時は、他の変数名とはかぶらないものを使うことになります。

さてε[t] が次に reduction される候補です。

        (+ t (let (x 1) (f x)))

の部分。t は変数ですから V (Values)として扱います。
この式は Evaluation Contexts の (F V ... V ε M ... M) に該当し、εは (let (x 1) (f x)) になります。
(let (x 1) (f x)) が reduction されるときの Evaluation Contexts は (+ t ...) です。
reduction されるべき (let ...) は (let (x M) N) に該当しますから(A1)を適用します。

(let (t (+ 2 2)
  (let (x 1)
    (+ t (f x)))))

となります。

次は (f x) です。A(3)を適用します。

(let (t2 (f x))
  (let (t (+ 2 2)
    (let (x 1)
      (+ t t2)))))

できました。

実装

さてこれをコードに落とすことを考えます。Figure 9 に参考実装があるのですが Figure 7 と一致しない部分もあるともっぱらの噂なので慎重に。
基本的に Figure 9 のコード例が分かりやすい気がする。(normalize M k)の形式。
M が normalize するべき式、k が M が normalize された結果を受け取り、式に埋め込むための継続。(ここが本当に理解が難しい)


Figure 9 には

変換後コードサイズが指数関数的に大きくなってしまうことを防ぐために、条件分岐を取り囲む式の評価を2回してしまわないように工夫している。

と書かれていますが。

(a-normalize '(let (a (if 1 2 3)) (if (let (x 3) x) 4 5)))
=>
(if 1 (let (a 2) (let (x 3) (if x 4 5))) (let (a 3) (let (x 3) (if x 4 5))))

のように分岐後のコードが重複するのを防ぐみたいです。
それと if の predicate 部の normalize が Figure 9 では間違っているような気がします。

[('if m1 m2 m3)
  (a-normalize-with-k m1 (lambda (normalized-m1)
=>
[('if m1 m2 m3)
  (a-normalize-name m1 (lambda (normalized-m1)

だと思います。


問題は lambda の扱い。Figure 7 の簡約規則では lambda は Values と定義されていますから、変換の対象ではないとするのが正しそうです。
ところが Figure 9 のコードでは lambda の body を normalize しています。
確かに最適化のための中間コードとして採用するならば、lambda の body を normalize をしないと normalize の意味がないような気がします。
この件は id:sumii さんも

A正規形の元論文からして、Figure 7の簡約規則とFigure 9のコードが食い違っているような…(if文以外にも、λの中を正規化するかどうかも食い違っている気がする)

と指摘されています。(やっと理解が追いつきました)


ところで lambda の normalize はなぜ、lambda の外にしみださないようになっているのでしょうか。今のところ分かりません。

コード

Figure 9 ほぼそのままですが、Gauche で動くコードです。

(load "./lib/test.scm")

;;--------------------------------------------------------------------
;;
;; A-normalization for Core Scheme.
;;
;;   Based on A linear time A-normalization alogrithm,
;;   in The Essence of Compiling with Continuations.
;;
;;
(define (a-normalize m)
  (a-normalize-with-k m (lambda (x) x)))

(define (a-normalize-with-k m k)
  (define (primitive-op? f)
    (eq? f '+))
  (match m
    ;; normalization of lambda is not in Figure 7, but Figure 9.
    [('lambda params body)
     (k `(lambda ,params ,(a-normalize body)))]
    [('let (x m1) m2)
     (a-normalize-with-k m1 (lambda (normalized-m1)
                              `(let (,x ,normalized-m1)
                                 ,(a-normalize-with-k m2 k))))]
;;     [('if m1 m2 m3)
;;      (a-normalize-with-k m1 (lambda (normalized-m1)
;;                               `(if ,normalized-m1
;;                                    ,(a-normalize-with-k m2 k)
;;                                    ,(a-normalize-with-k m3 k))))]
    ;; To prevent possible exponential growth in code size,
    ;; the algorithm avoids duplicating the evaluation context enclosing a conditional expression.
    [('if m1 m2 m3)
     (a-normalize-name m1 (lambda (normalized-m1)
                            (k
                             `(if ,normalized-m1
                                  ,(a-normalize m2)
                                  ,(a-normalize m3)))))]
    [(fn . m*)
     (if (primitive-op? fn)
         (a-normalize-name* m* (lambda (t*) (k `(,fn . ,t*)))))
         (a-normalize-name fn (lambda (t) (a-normalize-name* m* (lambda (t*) (k `(,t . ,t*))))))]
    [else
     (k m)]))

(define (a-normalize-name m k)
  ;;
  ;;  Values
  ;;     constants or variables.
  (define (value? v)
    (not (pair? v)))
  (a-normalize-with-k m
             (lambda (n) (if (value? n)
                             (k n)
                             (let1 t (gensym)
                               `(let (,t, n) ,(k t)))))))

(define (a-normalize-name* m* k)
  (if (null? m*)
      (k '())
      (a-normalize-name (car m*)
                      (lambda (t)
                        (a-normalize-name* (cdr m*)
                                           (lambda  (t*) (k `(,t . ,t*))))))))

;;--------------------------------------------------------------------
;;
;; Tests
;;

(a-normalize '(if (let (x 3) x) 4 5))
(eqt '(let (x 3) (if x 4 5)) (a-normalize '(if (let (x 3) x) 4 5)))

(with-gensym
 (a-normalize '(+ (+ 2 2) (let (x 1) (f x)))))
(eqt '(let (G7 (+ 2 2)) (let (x 1) (let (G8 (f x)) (+ G7 G8)))) )

(with-gensym
 (a-normalize '(+ (let (a 0) (+ a 3)) 2)))
(eqt '(let (a 0) (let (G4 (+ a 3)) (+ G4 2))) (with-gensym (a-normalize '(+ (let (a 0) (+ a 3)) 2))))

(with-gensym
 (a-normalize '(let (a (let (b 3) (+ b 2)))
                 (+ a 2))))
(eqt '(let (b 3) (let (a (+ b 2)) (+ a 2))) (with-gensym (a-normalize '(let (a (let (b 3) (+ b 2))) (+ a 2)))))

(with-gensym
 (a-normalize '(lambda () (+ (+ 1 2) 3))))
(eqt '(lambda () (let (G4 (+ 1 2)) (+ G4 3))) (with-gensym (a-normalize '(lambda () (+ (+ 1 2) 3)))))

(with-gensym
 (a-normalize
  '(if (+ 1 2) 3 3)))
(eqt '(let (G2 (+ 1 2)) (if G2 3 3)) (with-gensym (a-normalize '(if (+ 1 2) 3 3))))

(test-end)