読者です 読者をやめる 読者になる 読者になる

レガシーコード生産ガイド

私に教えられることなら

CommonLispコード読みメモ 1

  • 他人が書いた(大きい)コードを読めるようになりたい
  • CommonLispに詳しくなりたい

ということで、まずはメンテナ不足らしい?CLISPのコードを読むことにしました。

勉強のために他人のコードを読むこと、が殆ど無かったので、コードを読む事自体の練習です。 CommonLisp処理系自体のコードみたいに巨大なものに目を通す練習をすれば、それ以降他のコードは楽に読めそうな気もします。

特に具体的な目標や目的は無いので、目を通してわかりそうなところ・気になったことを 調べて、メモしていきます。

tests/tests.lisp

メインエントリっぽいrun-all-testsを眺めていきます。

キーワード引数を使って大域変数を束縛するの方法で、*eval-method*をローカルに?書き換えています。

*eval-method*

  • :evalの場合は、そのままフォームをevalしたものをテスト
  • :compileの場合は、フォームを無名関数に入れ、それをコンパイルしたものを評価してテスト
  • :bothの場合は、両方の式の値を調べ、equalpで等しくない場合はエラー

という動作のようです。まだその方法は想像つきませんが、フォームをそのままevalした場合とコンパイルした場合とで、 動作が変わるコードがあるみたいです。

run-all-testsでもrun-all-tests-parallelでも、全テストと独立してtest-weakptrというテストが実行されています。 ウィークポインタはGC関連で見た記憶があるので、おそらくGCのテストでしょう。

テストの中身はtests/weakptr.tstにあります。GCについて全然知らないのですが、目を通すだけ通してみます。

tests/weakptr.tst

まずweakptr-testマクロから

(defmacro weakptr-test (&body body)
  `(progn (make-list 100) ,@body (make-array 200)
          (list (eq co (weak-pointer-value wp))
                (multiple-value-list (weak-pointer-value wp))
                (multiple-value-bind (v p) (weak-pointer-value wpp)
                  (list #+(or ALLEGRO LISPWORKS) (if (arrayp v) 'WEAK-POINTER (type-of v))
                        #-(or ALLEGRO LISPWORKS) (type-of v)
                        p)))))

(weakptr-test body...)のbodyの前後でリストの生成と配列の生成を行い、その後にテスト結果のリストを返しています。

テスト対象のconsセルを入れるcoと、(make-weak-pointer foo)で作られるwp、 さらにそのwpへのウィークポインタ?であるwppを前提としているようです。

結果のリストは

  1. coと(weak-pointer-value wp)がeqで等しいか
  2. (weak-pointer-value wp)が返す多値
  3. (weak-pointer-value wpp)が返す多値の2番目

ということで、weak-pointer-valueが何を返すのかわかれば何をしているのかわかりそうです。

SBCL(describe 'weak-pointer-value)で説明を見てみます。 (clispはどうもビルド失敗したのか、何かのダウンロードを始めてSegmentation Faultで落ちました)

Documentation:
    If WEAK-POINTER is valid, return the value of WEAK-POINTER and T.
    If the referent of WEAK-POINTER has been garbage collected,
    returns the values NIL and NIL.

説明を踏まえて再度見直すと、結果のリストは多分次のようになります。

  1. coをwpが指しているか
  2. wpが指しているものと、それがまだgcされていない(t)か既にgcされた(nil)か
  3. wppがまだgcされていないかどうか

意外とわかりやすいです。(合ってれば、ですが)

それぞれのテストの中身を少し見てみます。

(weakptr-test (setq co (cons 1 2) wp (make-weak-pointer co)
                    wpp (make-weak-pointer wp)))
(T ((1 . 2) T) (WEAK-POINTER T))

3番目のwpがまだあるかどうかですが、'(weak-pointer t)と比べています。

clispで実際に(1 . 2)へのウィークポインタへのウィークポインタを作ったところ、 #<WEAK-POINTER (1 . 2)>という表記になりました。テストの最後で処理系毎に表記を 作っているのでそこかと思いますが、今はよくわからないので飛ばします。

2番目と3番目では、(gc)ガベージコレクションを実行させています。

;; 2
(weakptr-test (gc))
(T ((1 . 2) T) (WEAK-POINTER T))

;; 3
(weakptr-test (setq co nil) (gc))
(T (NIL NIL) (WEAK-POINTER T))

2番目ではcoに何もしてないのでgcが実行されても何も変わりません。

3番目ではcoをnilにセットしてgcしています。この時点でcoを指しているwpの値から、 coがgcで回収されたことがわかります。一方、wp自体はwppが指しているのでまだあります。 また結果の1番目から、setqで変数の値を変更しても、ウィークポインタはそれを指したままなのがわかります。

;; 4
(weakptr-test (setq co (cons 1 2) wp (make-weak-pointer 1)))
(NIL (1 T) (WEAK-POINTER T))

4番目では、coに再度'(1 . 2)をsetqしていますが、wpは別のものを指したので1番目の結果はnilになっています。

;; 5
(weakptr-test (setf (weak-pointer-value wp) co) (gc))
(T ((1 . 2) T) (NULL NIL))

5番目が謎です。wpが再度coを指すようにしてgcを実行してるだけに見えますが、wppからwpへの参照が消えています。 (setf (weak-pointer-value wp) co)の動作に鍵がありそうです。

clispで試してみます。(sbclだとsetfでエラーになりました)

> (defvar co (cons 1 2))
CO

> (defvar wp (make-weak-pointer co))
WP

> (defvar wpp (make-weak-pointer wp))
WPP

> (setf (weak-pointer-value wp) co)
(1 . 2)

> (weak-pointer-value wpp)
#<WEAK-POINTER (1 . 2)> ;
T

ただ書き換えるだけだとwppは保たれています。一度coをgcさせて、再度wpから指してみます。

> (setq co nil)
NIL

> (gc)
3486800 ;
871700 ;
164640 ;
1 ;
124784 ;
7746

> (setq wp (make-weak-pointer 1))
#<WEAK-POINTER 1>

> (setq co (cons 1 2))
(1 . 2)

> (setf (weak-pointer-value wp) co)
(1 . 2)

> (weak-pointer-value wpp)
#<WEAK-POINTER (1 . 2)> ;
T

wppは保たれたままでした。テストの時とどう違うんでしょうか。謎です。

とりあえずweak-pointer周りの関数を知れたのでヨシとして、別のところに移ります。

src/compiler.lisp

clispはネイティブコードではなく、VM用のバイトコードコンパイルするみたいです。 VMの実装と動作に興味があるので、ちょっと覗いてみます。

まずコメントを見ると、バイトコードの仕様がChapter 37. The CLISP bytecode specificationにあるようです。

現時点でわかりそうなところだけ概要をメモしてみます。

  • VM
    • STACKSP、二つのスタックを持つアーキテクチャ(スタックマシン?)
    • STACKCLISPオブジェクトとフレーム、SPは他のデータとポインタのスタック
  • コンパイルされた関数
    • 2つのデータを持つ
      1. バイトコードに必要な全てのCLISPオブジェクトへの参照、を含んだ関数自体
      2. バイトコード全体も含むバイトの配列(?)
    • 関数は3つの変更不可能な情報を持つ
      1. name 名前
      2. codevec バイトコードの配列(前項の2?)
      3. consts 他のCLISPオブジェクトへの参照
    • 総称関数のディスパッチが追加された場合(?)、codevecとconstsは破壊的に変更される
    • constsに含まれる参照は更に次の5種類に分けられる
      1. venv-const* レキシカル環境。#(next value_1 ... value_n)という形のベクタ
      2. block-const* carに名前、cdrにBLOCKフレーム(?)をもつレキシカル環境でのブロック(?)
      3. tagbody-const* carにTAGBODYのtagのベクタ、cdrにTAGBODYフレーム(?)をもつレキシカル環境でのTAGBODY(?)
      4. keyword-const* &key用のキーワードが並んだ何か(何だろう?)
      5. other-const* その他
    • venv-const*block-const*tagbody-const*のいずれかがあれば、実行時にclosureが生成される
    • closureが生成される関数の場合、コンパイル時はプロトタイプのみを生成し、venv-const*block-const*tagbody-const*NILがセットされる。実行時にプロトタイプがコピーされ、本来のそれらと置き換えられる(?)

とりあえずまとめてみたところ、BLOCKとTAGBODYについてよく知らないと実感しました。次回はそれらが何なのかと、 (?)なところをわかる範囲で調べてみようと思います。

感想

GCについての理解が少しだけ深まった気がします。ものすごく難しいものだというイメージがあって躊躇していましたが、当たり前だけど何から何まで全て難しい、というわけでは無さそうですね。あんまり怖がらずに目を通すだけ通すのは大切かもしれません。楽しいですし。

それから、VMについて調べたところが、製作中のForth風言語処理系で悩んでいる「コンパイル済みな参照を並べたコードでどうクロージャを作るか」の解決策の一つな予感がするのでワクワクしています。

私事ですが、睡眠不足が続いて調べてる間の集中力低下が気になるので、調べるためにも生活サイクルを整えたいところです。

広告を非表示にする