formatterで高速化
【追記】内容が間違っていたので何回か書き直しました。
Common Lispの、formatter
マクロの使い方を紹介します。
このマクロは、format
の制御文字を受け取って関数を返却するものです。
とりあえず例をあげます。
下記のformat
文を考えます。
(format t "Hello~%")
実行すると次のような出力をします。
Hello
それに対してformatter
は、ただ関数を返却するだけです。
とりあえず実行してみます。
(formatter "Hello~%") -> #<FUNCTION>
関数が返却されました。
これを実行してみるとどうなるでしょう。
(funcall (formatter "Hello~%") *standard-output*) Hello
ちなみにfuncall
はnil
を返却します。
これは一体なにが楽しいのでしょうか?
formatter
はformat
関数を高速化するためのものです。
大きく、下記の2つのアプローチから高速化を実現します。
まず「構文解析を事前に行う」とはどういう事でしょうか。
構文解析とは、例えば"Hello~%"
という文字を見て、
①Hello
の出力と②terpri
の出力に分けることです。
format
の制御文字は文字列によりゴチャゴチャと記載されているため、
一文字ずつ調べて行き、~
文字が現れたら
さあどうしようかと考える処理が必要になります。
制御文字を一つずつ読んで解析するのはそれなりに時間がかかる作業です。
この解析を行うのがformatter
マクロの仕事になります。
もう一つの「コンパイルにより高速化を行う」というのはどういう事でしょうか。
それはformatter
を用いることでformat
の制御文字そのものを
機械語の変換してしまおうというものです。
npt
はインタープリタなのでその恩恵は得られませんが、
compile
を実装したLisp処理系だと、
format
制御文字をLisp式にさえ変換できれば
コンパイルで機械語に変換できるので、
かなり高速に実行できるようになります。
formatter
による高速化の作業は、実際にコードが実行する時点より
遥かに前で終わらせておくことができます。
例えば次のような文を考えます。
(dotimes (i 5) (format t "Hello: ~A~%" i))
難しいことは何もない文ですね。
実行結果を下記に示します。
Hello: 0 Hello: 1 Hello: 2 Hello: 3 Hello: 4
この文の問題点は、"Hello: ~A~%"
という文字を
5回も構文解析している点にあります。
「先頭にHello:
があってprinc
の次にterpri
がある」という分析は、
何も律義に5回実行する必要なんてありません。
それに今回は5回で済んでいますが
何万回も出力するということは珍しくないと思います。
そういうときにformatter
マクロの出番です。
(setq *format* (formatter "Hello: ~A~%")) (dotimes (i 5) (funcall *format* *standard-output* i))
いいですね。
構文解析が1回で終わっています。
【変更】
さらに次のように書き直すことができます。
(dotimes (i 5) (format t #.(formatter "Hello: ~A~%") i))
formatter
の結果を#.
によりread
時に実行するように変更しています。
すると全体では下記のような文になります。
(dotimes (i 5) (format t #<FUNCTION> i))
なぜformat
の引数にformatter
を指定しているのか疑問に思うかもしれません。
実はformat
関数は、制御文字の引数に関数そのものを受け取ることができます。
ちょうどformatter
の返却値を受け取るように考慮されているのです。
実際、やっていることは次の命令と変わりません。
(dotimes (i 5) (funcall #<FUNCTION> *standard-output* i))
以上でformat
関数をより高速に実行する方法を覚えました。
では実際にやってみましょうということで、 速度を測定してみたりしても 全然変わらないじゃないかと思う人もいるかもしれません。
実際、全然変わっていない可能性が高いです。
なぜなら、Lisp処理系がeval
時に勝手にformatter
を実行しているからです。
つまり、
(format t "Hello~%")
と記載していたにも関わらず、 処理系はお節介にも
(format t (formatter "Hello~%"))
と変換して実行しているのです。
本当に?
調べる方法があります。
次の例文を考えましょう。
(format t "ERROR ~! ERROR")
見てわかるように、~!
なんて命令は無いので不正な文です。
実行してみましょう。
(format t "ERROR ~! ERROR") →エラー、~!なんて知らない
そりゃそうです。
しかしlambda
で囲んだらどうなるでしょうか。
(lambda () (format t "ERROR ~! ERROR"))
普通に考えるならば、実行結果は何も問題が無いはずです。
では実際はどうなるでしょうか?
各処理系で実行した結果を示します。
sbcl
WARNING
,~!
がおかしいFUNCTION
返却
clisp
FUNCTION
返却
ccl
WARNING
,~!
がおかしいFUNCTION
返却
clisp
は素直にlambda
で囲んだ分を返却しています。
しかしsbcl
とccl
はWARNING
が出力されているため、
format
の制御文字を構文解析しようと試みたのが分かります。
この事前処理を行おうとしたのはeval
の最適化です。
もしformat
文の2番目の引数が文字列だったら、
先行してformatter
を実行しておこうした結果です。
これはありがたい。
何も考えなくてもいいんですから。
しかし最適化が見てくれているとしても、
例えば実験データなんかを大量に出力するということが分かり切っている場合は、
format
文の制御文字の代わりにformatter
を使っておくのはいい考えだと思います。
ちなみにsbcl
だと、optimize
のspeed
を0
にすることで
この機能を解除することができました。
(locally (declare (optimize (speed 0))) (lambda () (format t "ERROR ~! ERROR"))) -> WARNINGは出ない -> FUNCTION返却
書き直しについて
最初は、#.
をつけずに下記のように実行することを考えていました。
(dotimes (i 5) (format t (formatter "Hello: ~A~%") i))
しかしどの処理系を見てもformatter
はlambda
式を返却するため、
これだと構文解析は1回で終わるのですが、実行するたびに関数が生成されてしまいます。
つまり上記の例では、lambda
により5個の無名関数が生成されてしまいます。
formatter
が無いときに比べれば早くなるので悪くはないのですが、
メモリを無駄に使うのも良くはありません。
確実に#.
を付けた方がいいでしょう。
しかし#.
による仕組みは、見た目は綺麗ですが結構変なことをしていますので、
setq
やlet
なんかでformatter
の関数を
変数に束縛するのが一番素直で確実なのかもしれません。
つまりはこんな感じ。
(setq *format* (formatter "Hello: ~A~%")) (dotimes (i 5) (format t *format* i))
clispをFreeBSDにインストールする2
以前の投稿clispをFreeBSDにインストールする - nptclのブログ では、
portsにてclispのインストールができませんでしたが復活していました。
とても嬉しいです。
それで早速portsでインストールしてみたのですが、
やっぱりclangでコンパイルは無理のようで、
gccを入れてからclispを構築しているようです。
gccの分だけ依存関係が多くなっているのではないでしょうか。
どんな感じなのかやってみました。
実験した環境はamd64機器です。
# freebsd-version 12.1-RELEASE
何もインストールされていない状況から、 まずはpkgのインストール。
# portsnap fetch extract # cd /usr/ports/ports-mgmt/pkg # make install ★pkgが同バージョンだとエラーが出るけど気にしない # make clean
現時点でのインストール状況を確認。
# pkg info dialog4ports-0.1.6 Console Interface to configure ports pkg-1.12.0 Package manager
clispのインストール
# cd /usr/ports/lang/clisp # make config-recursive ★考えたくないので全部適当にOK # make
途中で失敗
===> llvm80-8.0.1_3 needs Python 3.6 at least, but 2.7 was specified.
Pythonの問題じゃなさそうなので、llvm80を直接インストールする
# cd /usr/ports/devel/llvm80 # make # make install # make clean
さらに途中で失敗
===> meson-0.52.0 needs Python 3.5 at least, but 2.7 was specified.
mesonを直接インストール
# cd /usr/ports/devel/meson # make # make install # make clean
再びclispのインストール
# cd /usr/ports/lang/clisp # make # make install # make clean
完了!
ちなみにうちのPCは遅すぎるので、
だらだら作業して、寝てる間もコンパイルしてて丸一日かかりました。
起動してみます。
# clisp i i i i i i i ooooo o ooooooo ooooo ooooo I I I I I I I 8 8 8 8 8 o 8 8 I \ `+' / I 8 8 8 8 8 8 \ `-+-' / 8 8 8 ooooo 8oooo `-__|__-' 8 8 8 8 8 | 8 o 8 8 o 8 8 ------+------ ooooo 8oooooo ooo8ooo ooooo 8 Welcome to GNU CLISP 2.49.93+ (2018-02-18) <http://clisp.org/> Copyright (c) Bruno Haible, Michael Stoll 1992-1993 Copyright (c) Bruno Haible, Marcus Daniels 1994-1997 Copyright (c) Bruno Haible, Pierpaolo Bernardi, Sam Steingold 1998 Copyright (c) Bruno Haible, Sam Steingold 1999-2000 Copyright (c) Sam Steingold, Bruno Haible 2001-2018 Type :h and hit Enter for context help. [1]> ^D Bye. #
では一体どれくらいのパッケージが入ったのか。
# pkg info autoconf-2.69_3 Automatically configure source code on many Un*x platforms autoconf-wrapper-20131203 Wrapper script for GNU autoconf ... ... ... # pkg info | wc 163 1105 11779 #
163個!
pkg関連の2個とclispの1個以外に、依存関係で160個追加されたことになります。
けっこう入ったなあ。
pprint-newlineの使い方
Common Lispで、いまいちよくわからない「条件付き改行」の使い方を見て行きます。
条件付き改行とは、Pretty Printingの機能の一つであり、
名前の通り条件よって改行するかどうかが決まるものです。
この機能はpprint-logical-block
マクロの中で、
pprint-newline
関数を使うことで実行できます。
簡単に例を挙げるとこんな感じ。
(setq *print-pretty* t) (setq *print-miser-width* nil) (setq *print-right-margin* 80) (pprint-logical-block (nil nil) (princ "AAA") (pprint-newline :linear))
条件付き改行は4種類ありますが
通常の改行であるterpri
も特別扱いされますので、
下記の5種類の改行について説明してきます。
:linear
:miser
:fill
:mandatory
terpri
さっそく説明に行きたいのですが、 その前にPretty Printingの基本である 下記の用語の説明をしていきます。
- 論理ブロック
- セクション
- マイザ書式
論理ブロック
論理ブロックとは、pprint-logical-block
マクロで囲んだところです。
これは簡単ですね。
例えばこんな感じ。
(pprint-logical-block (nil nil) (princ "AAA") (princ "BBB"))
セクション
セクションとは、条件付き改行で区切った範囲を表します。
セクションの範囲は条件付き改行に付随するものであり、
このセクションで表される範囲によって
実際に改行が出力されるかどうかが決定されます。
まずは範囲がどのように決定されるかを見て行きましょう。
次の例を考えます。
(setq *print-pretty* t) (setq *print-miser-width* nil) (setq *print-right-margin* 80) (pprint-logical-block (nil nil) (princ "AAA") (pprint-newline :linear) (princ "BBB") (pprint-newline :linear) (princ "CCC") (pprint-newline :linear) (princ "DDD") (pprint-newline :linear))
出力結果は次のようになります。
AAABBBCCCDDD
出力例では、条件付き改行の条件が当てはまらなかったため、 一つも改行が出力されませんでした。
ではどの場所で条件付き改行が実施されたかを見たいので、 次のように数値で表してみます。
1 2 3 4 AAABBBCCCDDD
セクションは、自分の改行から見て、前の改行と後ろの改行に挟まれた範囲になります。
数値で範囲を表してみます。
1 2 3 4 AAABBBCCCDDD 111111 222222 333333 444
セクションの決定には色々と複雑な条件があるのですが、 本投稿では全部無視して上記の定義で説明します。
マイザ書式
「マイザ、miser」とは日本語で「けち」を意味します。
例えば、横に長い出力の例を考えましょう。
出力が長くなり、画面右端に近くなった場合は、
なんとか狭いスペースで出力したいと考えるかもしれません。
そこで可能な限り横幅をケチって
縦長に表示したい、というのがマイザ書式です。
ではどういう時にマイザ書式になるのでしょうか。
それは論理ブロックの開始位置が、画面の右端から
*print-miser-width*
変数までの間に入っているときです。
画面の横幅は*print-right-margin*
であり、
マイザ書式の条件が*print-miser-width*
です。
例えば、
*print-right-margin*
が80
*print-miser-width*
が70
のとき、マイザ書式の基準点は80-70=10
になります。
論理ブロックの開始位置が10
以上であったならばマイザ書式になります。
次の文を考えます。
(setq *print-pretty* t) (setq *print-miser-width* 70) (setq *print-right-margin* 80) (fresh-line) (princ "AAAAA") (pprint-logical-block (nil nil) ...)
この場合、論理ブロックは左から5文字目で始まっているため、
マイザ書式の基準には当てはまりません。
しかし、10を超えればいいので、
(setq *print-pretty* t) (setq *print-miser-width* 70) (setq *print-right-margin* 80) (fresh-line) (princ "AAAAABBBBBCCCCC") (pprint-logical-block (nil nil) ...)
これだと論理ブロックが15文字から開始になるので、マイザ書式になります。
ではマイザ書式になると何が変わるのでしょうか?
一つは、インデントが強制的に0になり、設定不可能となります。
もう一つは、条件付き改行で改行しやすくなります。
pprint-newline
関数
条件付き改行はpprint-newline
関数によって出力します。
使い方は次の4通り。
(pprint-newline :linear) (pprint-newline :miser) (pprint-newline :fill) (pprint-newline :mandatory)
省略可能な第二引数により、ストリームを指定できます。
(pprint-newline :linear stream)
この関数は論理ブロック内でしか機能しません。
もし普通の場所で呼ばれた場合は何もしません。
引数の簡単な意味を説明します。
:linear
- 一行に入らなかったら全てを改行する。
:miser
- マイザ書式時に、一行に入らなかったら全てを改行する。
:fill
- 一行に入らなかったら改行するが、可能なかぎり改行しない。
:mandatory
- 一行に入らなかったことにして改行する。
こう書けば簡単に見えるのですが、条件というのがまあ複雑で説明に苦労します。
この4種類の改行と通常のterpri
改行を合わせて
5通りの挙動について説明していきます。
条件付き改行:mandatory
上から順番に説明して行けたらいいのですが、
まずは下の方にある:mandatory
から。
この改行は、次の二通りの意味を持ちます。
- 必ず改行を出力する。
- セクションが一行に収まらなかったことにする。
:mandatory
は改行が必ず出力されるので、
改行を挿入したいときに使うのかと考えるかもしれませんが、
そんなことよりも二つ目の意味が強烈です。
「セクションが一行に収まらなかった」という条件は、
他の条件付き改行である:linear
, :miser
, :fill
の判定で使うものです。
:mandatory
は同じ論理ブロックに配置されている
他の条件付き改行に全部対して、
「セクションが一行に収まらなかった」という報告を出します。
言い換えるなら、他の条件付き改行に対して一斉に改行指示を出す、
ということになります。
ただし、他の条件付き改行を絶対に改行させるという訳ではなく、
さらに固有の条件を満たしたときだけに出力されます。
例えば:linear
は:mandatory
により絶対に改行しますが、
:miser
と:fill
は他の条件を満たさないと改行しません。
通常の改行terpri
通常の改行であるterpri
は、:mandatory
と似ています。
terpri
があった場合は、無条件に改行を出力しますし、
同じ論理ブロックの全ての条件付き改行に改行指示を出します。
では:mandatory
との違いは何でしょうか?
terpri
関数はインデントを出力しません。
もし条件付き改行が改行を出力した場合は、
次の行の開始からper-line-prefix
とインデントの空白が出力されます。
それに対してterpri
関数の改行の場合は、per-line-prefix
しか出力されません。
条件付き改行:linear
条件付き改行の基本となるのが:linear
です。
:linear
は、次の条件の時に改行が挿入されます。
- セクションが一行に収まらなかったとき
- 同じ論理ブロックに
:mandatory
があるとき
もし条件に当てはまった場合は、その改行が一つだけ挿入されるわけではなく、
同じ論理ブロックに含まれる:linear
の条件付き改行が一斉に全部改行されます。
つまり同じ論理ブロック中にある:linear
は、
全てが改行されるか、あるいは全て何もしないかのどちらかになります。
次の例を考えます。
(setq *print-pretty* t) (setq *print-miser-width* nil) (setq *print-right-margin* 80) (pprint-logical-block (nil nil) (princ "AAAA") (pprint-newline :linear) (princ "BBBB") (pprint-newline :linear) (princ "CCCC") (pprint-newline :linear))
この例では全体の出力が80文字以内に余裕で収まるため、 全ての条件付き改行は無視されて次のような出力となります。
AAAABBBBCCCC
では次の場合はどうでしょうか。
(setq *print-pretty* t) (setq *print-miser-width* nil) (setq *print-right-margin* 11) (pprint-logical-block (nil nil) (princ "AAAA") (pprint-newline :linear) (princ "BBBB") (pprint-newline :linear) (princ "CCCC") (pprint-newline :linear))
出力幅は11文字に設定しています。
1番目の条件付き改行のセクションはAAAABBBB
で8文字です。
2番目の条件付き改行のセクションはBBBBCCCC
の8文字であり、
開始位置4文字目からだと4+8=12
で11文字に収まらないため、
次のように出力されると考えるかもしれません。
AAAABBBB CCCC
しかしこれは間違い。
:linear
は今回の判定では無関係なものも全て一斉に改行されるため、
実際には次のように出力されます。
AAAA BBBB CCCC
改行の条件にはさらに:madatory
があり、セクションが一行に十分収まる場合でも、
同じ論理ブロック内に:madatory
がある場合は全部改行されます。
(setq *print-pretty* t) (setq *print-miser-width* nil) (setq *print-right-margin* 80) (pprint-logical-block (nil nil) (princ "AAAA") (pprint-newline :linear) (princ "BBBB") (pprint-newline :linear) (princ "CCCC") (pprint-newline :mandatory))
実行例は下記の通り。
AAAA BBBB CCCC
条件付き改行:miser
この条件付き改行は:linear
と同じなのですが、
マイザ書式中でないと改行を出力しません。
これから例文も含めて色々と書いていくわけですが、
結局のところ:miser
は:linear
とたった一つを除いて
全て同じだという事を説明しています。
たった一つの違いとは「マイザ書式中じゃなきゃ改行を出力しない」です。
:linear
と同じように動作するので、
:linear
が一斉に改行されたらマイザ書式中であれば:miser
も改行します。
逆に:miser
が一行に収まらなかった場合は、
:linear
は一斉に改行しますし、マイザ書式中であれば:miser
も改行します。
さらに言うと、:mandatory
があるだけで
:linear
とマイザ書式中の:miser
は全部改行します。
分かりづらいのですが、マイザ書式中でなくても、
一行に収まるか収まらないかの判定は:miser
にて行われます。
マイザ書式ではない場合に行われないのは、改行の出力だけです。
下記に例を示します。
(setq *print-pretty* t) (setq *print-miser-width* 30) (setq *print-right-margin* 20) (pprint-logical-block (nil nil) (princ "AAAA") (pprint-newline :miser) (princ "BBBB") (pprint-newline :miser) (princ "CCCC") (pprint-newline :miser) (princ "DDDD") (pprint-newline :linear) (princ "EEEE") (pprint-newline :linear) (princ "FFFF") (pprint-newline :linear))
実行結果は下記の通り。
AAAA BBBB CCCC DDDD EEEE FFFF
例では:linear
周辺で一行に収まらないため、
少なくとも:linear
は全部改行されることは分かると思いますが、
マイザ書式中のため:miser
も同時に全て改行されています。
もしマイザ書式ではなかった場合はどうなるでしょうか。
次に例を示します。
(setq *print-pretty* t) (setq *print-miser-width* nil) (setq *print-right-margin* 20) (pprint-logical-block (nil nil) (princ "AAAA") (pprint-newline :miser) (princ "BBBB") (pprint-newline :miser) (princ "CCCC") (pprint-newline :miser) (princ "DDDD") (pprint-newline :linear) (princ "EEEE") (pprint-newline :linear) (princ "FFFF") (pprint-newline :linear))
実行結果は次の通り。
AAAABBBBCCCCDDDD EEEE FFFF
:linear
だけが全て改行され、:miser
は改行が出力されませんでした。
条件付き改行:fill
この条件付き改行は、可能な限り横に列挙することを目的とします。
もし一行に収まらなかった場合、:linear
では一斉に改行されましたが、
:fill
ではセクションが超過したもののみ改行を行い、他の:fill
は改行されません。
もし:fill
のセクションが一行に収まらない場合は、
:linear
と:miser
の場合と同様に、
同じ論理ブロックに配置している:linear
と:miser
も
改行条件を満たすことになります。
つまり、:fill
が一行に収まらない時は:linear
は全て改行し、
マイザ書式中であれば:miser
も全て改行します。
一方、:linear
などでセクションの横幅が超過した場合は、
他の条件付き改行も一斉に改行するのですが、
:fill
に関しては無関係であり影響を受けません。
次の例を考えます。
(setq *print-pretty* t) (setq *print-miser-width* nil) (setq *print-right-margin* 11) (pprint-logical-block (nil nil) (princ "AAAA") (pprint-newline :fill) (princ "BBBB") (pprint-newline :fill) (princ "CCCC") (pprint-newline :fill))
実行結果は次の通り。
AAAABBBB CCCC
この例文は、:linear
の例を全て:fill
に変えたものです。
:linear
の時は全てが改行されましたが、
:fill
の場合はなるべく一行に収めようとして、
はみ出る場合にのみ改行をしているのがわかります。
:fill
にはさらに言っておかなければいけないことが色々とあります。
:mandatory
だとどうなるでしょうか?
:mandatory
は、セクションが一行に収まらなかったことにする意味があると書きました。
しかし:fill
の場合には当てはまらず、
もし:mandatory
が存在しても:fill
の挙動は変わりません。
実行例を見て行きます。
(setq *print-pretty* t) (setq *print-miser-width* nil) (setq *print-right-margin* 11) (pprint-logical-block (nil nil) (princ "AAAA") (pprint-newline :fill) (princ "BBBB") (pprint-newline :fill) (princ "CCCC") (pprint-newline :mandatory))
実行結果は次の通り。
AAAABBBB CCCC
:mandatory
があっても:fill
の挙動が変化していないのがわかります。
ただし例外があるのです。
:fill
は、マイザ書式中だと:linear
と全く同じ意味になります。
よって:fill
のセクションが一行に収まらなかったら
他の条件付き改行も巻き込んで一斉に改行するし、
:linear
と:miser
と:mandatory
が改行した場合は
つられてマイザ書式中の:fill
も改行します。
次の例を示します。
(setq *print-pretty* t) (setq *print-miser-width* 100) (setq *print-right-margin* 11) (pprint-logical-block (nil nil) (princ "AAAA") (pprint-newline :fill) (princ "BBBB") (pprint-newline :fill) (princ "CCCC") (pprint-newline :mandatory))
実行結果は次の通り。
AAAA BBBB CCCC
マイザ書式なので:fill
が:miser
と同じ動きになっているのがわかります。
さらに次の例を示します。
(setq *print-pretty* t) (setq *print-miser-width* nil) (setq *print-right-margin* 15) (pprint-logical-block (nil nil) (princ "AAAA") (pprint-newline :linear) (princ "BBBB") (pprint-newline :fill) (princ "CCCC") (pprint-newline :fill) (princ "DDDD") (pprint-newline :fill) (princ "EEEE") (pprint-newline :fill))
実行結果は次の通り。
AAAA BBBBCCCCDDDD EEEE
この例では、:fill
のセクションが一行に収まらなかったため、
:linear
にも影響が出て改行されています。
:fill
は一行に収めようとする挙動自体は変わらないのですが、
論理ブロック全体に改行の条件が加わるため:linear
にも影響が出るのです。
改行の連鎖
次の出力はどうなるかわかるでしょうか。
(setq *print-pretty* t) (setq *print-miser-width* nil) (setq *print-right-margin* 100) (pprint-logical-block (nil nil) (princ "AAA") (pprint-newline :linear) (princ "BBB") (pprint-newline :linear) (pprint-logical-block (nil nil) (princ "CCC") (pprint-newline :linear) (princ "DDD") (pprint-newline :linear) (pprint-logical-block (nil nil) (princ "EEE") (pprint-newline :linear) (princ "FFF") (pprint-newline :linear) (princ "GGG") (pprint-newline :mandatory))))
論理ブロックが3つネストしており、
一番最後に:mandatory
で強制改行をしています。
最後のブロックだけが強制的に改行されるので、
次のようになると思うかもしれません。
AAABBBCCCDDDEEE FFF GGG
しかしそうではなく、全部改行されます。
実行結果は下記の通り。
AAA BBB CCC DDD EEE FFF GGG
なぜこうなるのでしょうか?
論理ブロックの:linear
の考え方を思い出しましょう。
もし:linear
のセクションが一行に収まらなかったら、
同じ論理ブロックの全ての:linear
が一斉に改行されました。
そのあとに:miser
も:fill
も改行されることがあると言いました。
これは論理ブロック内にある出力関数が、
一つでも改行した場合にはすべてを一斉に改行させる
という機能によるものです。
条件付き改行が一つでも改行したら他の条件付き改行も同時に改行しますが、
改行の出力は必ずしも「条件付き改行」じゃなくても構いません。
terpri
であったり、format
でもいいわけです。
もっと言うと、並列に配置してあるpprint-logical-block
でもいいわけです。
では:linear
と同じ論理ブロックに配置されている
別の論理ブロックが改行した場合はどうなるでしょうか?
別の論理ブロックの改行が契機となって:linear
が一斉に改行します。
じゃあ別の論理ブロックに配置されている、
さらにネストされた別の論理ブロックが改行したら?
答えは例文で示した通り、ネストされた論理ブロックで出力された改行が
元の論理ブロックへと次々と伝搬していきます。
結果、条件付き改行は全てが改行されたわけです。
では、今回の例のように連鎖のきっかけとなる改行は何でしょうか?
答えはterpri
か:mandatory
のどちらかだけとなります。
不思議に思うかもしれませんが、
例えば、ネストされた:linear
では改行の連鎖のきっかけにはなりません。
例を示します。
(setq *print-pretty* t) (setq *print-miser-width* nil) (setq *print-right-margin* 20) (pprint-logical-block (nil nil) (princ "AAA") (pprint-newline :linear) (princ "BBB") (pprint-newline :linear) (pprint-logical-block (nil nil) (princ "CCC") (pprint-newline :linear) (princ "DDD") (pprint-newline :linear) (pprint-logical-block (nil nil) (princ "EEE") (pprint-newline :linear) (princ "FFF") (pprint-newline :linear) (princ "GGG") (pprint-newline :linear))))
横幅20文字に対して
AAABBBCCCDDDEEEFFFGGG
の横幅21文字を出力しようとしています。
入りきれないのでどこかで改行するわけですが、
どこで改行するかわかるでしょうか。
出力結果は下記の通り。
AAA BBB CCCDDDEEEFFFGGG
見てわかる通り、ネストされていない 一番最初の論理ブロックで改行してしまうため、 連鎖して改行するということがありえないのです。
繰り返しになりますが、以上の結果から覚えておかなければいけないことは、
terpri
と:mandatory
はネストされた論理ブロックを巻き込んで
一斉に改行させる、ということです。
規約を見ただけでは、この動作を作り込むのは困難だと思います。
format
による条件付き改行
最後に条件付き改行のformat
の話をします。
format
の書式では、条件付き改行~_
が用意されています。
こんな感じ。
:linear ~_ :miser ~@_ :fill ~:_ :mandatory ~:@_
さらに言うと、pprint-logical-block
も用意されています。
~< ... ~:>
こちらは引数を1つ取ります。
pprint-logical-block
の~<
は非常に多機能で
色んなことができるのですが説明しません。
次の例文を考えます。
(setq *print-pretty* t) (setq *print-miser-width* nil) (setq *print-right-margin* 20) (pprint-logical-block (nil nil) (princ "AAAA") (pprint-newline :miser) (princ "BBBB") (pprint-newline :miser) (princ "CCCC") (pprint-newline :miser) (princ "DDDD") (pprint-newline :linear) (princ "EEEE") (pprint-newline :linear) (princ "FFFF") (pprint-newline :linear))
format
文で書き換えると次の通り。
(setq *print-pretty* t) (setq *print-miser-width* nil) (setq *print-right-margin* 20) (format t "~<AAAA~@_BBBB~@_CCCC~@_DDDD~_EEEE~_FFFF~_~:>" '(nil nil))
出力結果は下記の通り。
AAAABBBBCCCCDDDD EEEE FFFF
format
文は相変わらず何書いてあるんだが全然わからん。
このアルゴリズムについて
Pretty Printingは、次の論文がもとになっているとのこと。
22.2.3 Notes about the Pretty Printer's Background http://www.lispworks.com/documentation/HyperSpec/Body/22_bc.htm XP. A Common Lisp Pretty Printing System https://dspace.mit.edu/handle/1721.1/6503
見てはいませんが、cltl2にそのまま書き写されたような感じでした。
次のような文があったのを何となく覚えています。
27.1. Introduction https://www.cs.cmu.edu/Groups/AI/html/cltl/clm/node254.html Implementation note: ...XP uses a highly efficient linear-time algorithm.
highly efficient?
確かにそうかも。
作ってみてわかったのですが、確かに線形時間で終わりそうなものです。
もっとバックトラックとかふんだんに使って試行錯誤する
訳の分からないものかと思ってたんですが、
この通り作って行けば、素直に出力できると思います。
初めは何書いてるのかさっぱりわからなくて、
一体どうすりゃいいんだと何日も悩んでたんですけど、
たぶん何とかなりそうです。
ただ、条件付き改行の挙動をちゃんと理解するのはとても難しかったです。
めちゃ苦労しました。
だから本投稿にて文章で残します。
amd64ユーザーランド(とりあえず実行)
本投稿はnpt
に最小限のamd64コンパイラを実装できるか検討したときのメモです。
まあ結局npt
でamd64コンパイラは実装しないんですけど。
ユーザーランドとは、つまり一般ユーザーが作るプログラムのことです。
OSを作るわけじゃないので、カーネルモードは対象外になります。
条件は次の通り。
- CPUはamd64形式
- 64bitモードのみ
- ユーザーランドのみ
まず覚えなければいけないのは、機械語を実行して確認する方法です。
自分にとってはC言語から実行する方法が簡単だったので、その方法を示します。
方法はOS依存なので、下記の環境を対象に考えます。
機械語とは、結局は単なるバイナリデータの集合です。
C言語で言うならば、unsigned char
の配列でも作って、
それを強制的に関数ポインタとみなして実行すればいいことになります。
例えばこんな感じ。
unsigned char data[] = { /* exit(9)を呼び出す例文です */ 0xB8, 0x01, 0x00, 0x00, 0x00, /* mov eax <- 1 */ 0xBF, 0x09, 0x00, 0x00, 0x00, /* mov edi <- 9 */ 0xCD, 0x80 /* int 0x80 */ }; int main() { void (*call)(void); call = (void (*)(void))data; call(); /* ★たぶんうまく行かない */ return 0; }
注釈に記載した通り、たぶんうまく行きません。
FreeBSDで実行した結果が下記の通り。
$ cc main.c $ ./a.out Segmentation fault (core dumped)
大昔のOSだったらうまく行ったことでしょう。
しかしセキュリティが大切だともてはやされた結果、
メモリに対して実行可能な許可が無いとOSレベルでエラーになってしまいます。
実行許可があるメモリを用意する方法は大きく2つあり、
すでに確保されたメモリに実行許可を与える方法と新しく確保する方法です。
どちらの方法でも、mmap
関数か、VirtualAlloc
関数を使います。
今回は新規にメモリを確保することにします。
malloc
/free
のかわりに、mmap
/munmap
を使う方法を示します。
ではC言語にて下記の命令を実行することを考えます。
ptr = malloc(size);
これを実行可能なメモリ領域の確保に変えると次のようになります。
ptr = mmap(0, size, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_ANON|MAP_SHARED, -1, 0);
ptr = VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
ptr
が指しているメモリ空間は、関数と同様に実行可能な領域となります。
それでは、簡単な例として下記のコードを自力で機械語に翻訳してみます。
int testcode(int a, int b) { return a + b; }
関数testcode
は、単純に二つの引数を足すだけです。
面倒なことにOSによって機械語の翻訳結果が異なります。
unsigned char code[] = { 0x89, 0xF8, /* mov eax <- edi */ 0x01, 0xF0, /* add eax <- esi */ 0xC3 /* ret */ };
unsigned char code[] = { 0x89, 0xC8, /* mov eax <- ecx */ 0x01, 0xD0, /* add eax <- edx */ 0xC3 /* ret */ };
どうして内容が異なるのでしょうか?
それは「呼出規約」というものが異なるからです。
呼出規約とは、関数の呼び出しと返却をどのようにして行うかという、
関数間のインターフェイスのことです。
例えば、第一引数はスタックに積むのか、あるいはレジスタに格納するのか、
格納するならどういう順番で何を使うかなどを規定したものです。
呼出規約は、CPU、OS、言語の種類ごとに異なります。
例えばCPUのSPARCとIntelでは違っていますし、同じIntelでもi386とx86-64(amd64)で違います。
FreeBSDとLinuxは同じ場合も違う場合もあります。
FreeBSDとWindowsはどうも絶対に異なっているようです。
つまりコンパイラを作るためには、次の2つを勉強する必要があるという事です。
一気に説明することはできないので次の投稿になるとは思うのですが、 とりあえずは例で挙げた機械語のコードを実行するためのソースを示します。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/mman.h> unsigned char code[] = { 0x89, 0xF8, /* mov eax <- edi */ 0x01, 0xF0, /* add eax <- esi */ 0xC3 /* ret */ }; void execute(void *ptr) { int (*proc)(int, int); int result; memcpy(ptr, code, 5); proc = (int (*)(int, int))ptr; result = proc(30, 10); printf("%d\n", result); } int main() { void *ptr; size_t size; size = 1024; ptr = mmap(0, size, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_ANON|MAP_SHARED, -1, 0); if (ptr == MAP_FAILED) { perror("mmap"); return 1; } execute(ptr); if (munmap(ptr, size)) { perror("munmap"); return 1; } return 0; }
#include <windows.h> #include <stdio.h> #include <stdlib.h> #include <string.h> unsigned char code[] = { 0x89, 0xC8, /* mov eax <- ecx */ 0x01, 0xD0, /* add eax <- edx */ 0xC3 /* ret */ }; void execute(void *ptr) { int (*proc)(int, int); int result; memcpy(ptr, code, 5); proc = (int (*)(int, int))ptr; result = proc(30, 10); printf("%d\n", result); } int main() { void *ptr; size_t size; size = 1024; ptr = VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE); if (ptr == NULL) { fprintf(stderr, "VirutalAlloc error\n"); return 1; } execute(ptr); if (! VirtualFree(ptr, 0, MEM_RELEASE)) { fprintf(stderr, "VirutalFree error\n"); return 1; } return 0; }
実行結果は、どちらも40
が出力されます。
これで思い通りのバイトコードを実行できるようになりました。
クラスをsubtypepしたときの問題点
クラスはsubtypep
で継承関係を調査できます。
例えば次の通り。
(subtypep 'standard-class 'class) -> t; t
つまり、standard-class
はclass
を継承しているということです。
次の例はどうでしょうか。
(defclass aaa () ()) (defclass bbb (aaa) ()) (subtypep 'bbb 'aaa) -> t; t (subtypep 'aaa 'bbb) -> nil; t
何ら問題なく理解できると思います。
問題は否定したときです。
以前subtypep実装の基本 - nptclのブログ では、
右側が否定の場合は、比較する2つの型が完全に排他かどうかをチェックする必要があると書きました。
例として挙げた、
(defclass aaa () ()) (defclass bbb (aaa) ())
は、継承関係にあるため完全に排他とは言えません。
つまり、右を否定した場合は必ずnil; t
が返却されるはずです。
では実際にはどうなるでしょうか。
(subtypep 'aaa '(not bbb)) -> nil; t ;; sbcl, ccl -> nil; nil ;; clisp (subtypep 'bbb '(not aaa)) -> nil; t ;; sbcl, ccl -> nil; nil ;; clisp
さて、これはどういうことなのでしょうか。
sbcl
とccl
は予想通りでしたが、clisp
は第二返却値がnil
のため、
つまりは分からないと言って判定を放棄したことになります。
排他的な場合はどうなるでしょうか。
テスト用に次のクラスを用意します。
(defclass ccc () ()) (defclass ddd () ())
両者は一見すると排他的に見えます。
subtypep
の結果を次に記載します。
(subtypep 'ccc '(not ddd)) -> nil; nil ;; sbcl, clisp -> t; t ;; ccl
大きく分かれました。
これから上記の結果について私の見解を書いて行くわけですが、
実の所これらの結果はかなりわかりづらいですし、
ちゃんとした説明はできないかもしれません。
まずは次の結果から。
(subtypep 'ccc '(not ddd)) -> t; t ;; ccl
クラスccc
とクラスddd
は継承関係ではないため、
互いに排他として見るならば、返却値t
は一見して正しいように見えます。
しかし実はクラスccc
もクラスddd
もどちらも同じt
というクラスを含んでいるため、
互いに排他ではないのです。
(class-precedence-list (find-class 'ccc)) -> (#<STANDARD-CLASS CCC> #<STANDARD-CLASS STANDARD-OBJECT> #<BUILT-IN-CLASS T>) (class-precedence-list (find-class 'ddd)) -> (#<STANDARD-CLASS DDD> #<STANDARD-CLASS STANDARD-OBJECT> #<BUILT-IN-CLASS T>)
完全に排他ではないということならば、subtypep
の右がnot
の場合は
t
ではなくnil
が正解ということになります。
しかしsbcl
とclisp
では、nil; t
ではなくnil; nil
になっています。
どういうことなのでしょうか?
この返却は規約に矛盾が生じているため、
判定不可能とするのが正しいという妥協によるものだと思います。
もしnil; t
を許容してしまうならば、
built-in-class
はどうなんだと突っ込まれてしまいます。
例えば下記の式、
(subtypep 'integer '(not cons)) -> t; t
は、integer
とcons
は互いに排他のため、当然t
が返却されます。
しかしどちらの型もCLOSオブジェクトの写像を持っており、
両方ともt
クラスを含んでいます。
(class-precedence-list (find-class 'integer)) -> (#<BUILT-IN-CLASS INTEGER> #<BUILT-IN-CLASS RATIONAL> #<BUILT-IN-CLASS REAL> #<BUILT-IN-CLASS NUMBER> #<BUILT-IN-CLASS T>) (class-precedence-list (find-class 'cons)) -> (#<BUILT-IN-CLASS CONS> #<BUILT-IN-CLASS LIST> #<BUILT-IN-CLASS SEQUENCE> #<BUILT-IN-CLASS T>)
型として判定した場合はt
であり、クラスとして判定した場合はnil
になります。
もうどうしようもないですよね。
矛盾が生じた以上は未定義です。
sbcl
とclisp
はnil; nil
として、申し訳ないけどわからないと返却し、
ccl
はclass
の場合だけでもt
クラスを無視しようという結果になります。
さて、では次の場合はどうなるでしょうか。
(subtypep 'bbb '(not aaa)) -> nil; t ;; sbcl, ccl -> nil; nil ;; clisp
sbcl
とccl
はnil; t
であっています。
しかしclisp
はnil; nil
としています。
私の個人的な意見としてはsbcl
とccl
が正しいとは思います。
しかしすでにclass
の判定に妥協が生じている以上は、
nil; nil
としてしまってもいいのではないでしょうか。
以上です。
結論を書くならば、
subtypep
でclass
をチェックするときには、not
を使うべきではない。
となります。
FreeBSD 12にOpenSMTPDを入れたかった
昔はこうやって入れていました。
# portsnap fetch update # cd /usr/ports/mail/opensmtpd # make ===> opensmtpd-5.9.2p1_6,1 is marked as broken: Incompatible with Openssl 1.1.x yet. *** Error code 1 Stop. make: stopped in /usr/ports/mail/opensmtpd #
何か月たってもエラー。
あきらめようか。
【追記】FreeBSD 12.1-RELEASEでやったらうまく行きました。
やったー!!
# freebsd-version 12.1-RELEASE # portsnap fetch update # cd /usr/ports/mail/opensmtpd # make install # rehash # smtpd -h version: OpenSMTPD 6.6.0-portable usage: smtpd [-dFhnv] [-D macro=value] [-f file] [-P system] [-T trace] #
ソースから手動で入れる
ここでの手順はパッケージ管理されないので色々問題ありそう。
ずっとOpenSMTPDを使ってきたけど、結局sendmailに戻すかも。
それではOpenSMTPDをソースから入れる手順を示します。
方針は下記の通り。
- 必要なライブラリも全部ソースから入れる
/opt/smtpd
にインストール- 起動停止スクリプトは
/usr/local/etc
上に配置
管理ユーザーの作成
# pw useradd -n _smtpd -u 257 -c OpenSMTPD -w no -m -d /opt/smtpd -s sh # pw useradd -n _smtpq -u 258 -c OpenSMTPD -w no -m -d /var/empty -s /usr/sbin/nologin
切り替え
# su - _smtpd $
ディレクトリ作成
# mkdir src $ mkdir distfiles
ダウンロード
$ cd distfiles $ fetch --no-verify-peer https://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-2.9.2.tar.gz $ fetch --no-verify-peer https://github.com/libevent/libevent/releases/download/release-2.1.10-stable/libevent-2.1.10-stable.tar.gz $ fetch --no-verify-peer https://www.opensmtpd.org/archives/libasr-1.0.2.tar.gz $ fetch --no-verify-peer https://www.opensmtpd.org/archives/opensmtpd-6.4.1p2.tar.gz
LibreSSL構築
$ cd $ cd src $ tar zxf ../distfiles/libressl-2.9.2.tar.gz $ cd libressl-2.9.2/ $ ./configure --prefix=$HOME $ make $ make install $ make clean
環境変数LD_LIBRARY_PATH
を設定する。
$ cd $ vi .profile
下記の3行を追記
# OpenSMPTD LD_LIBRARY_PATH="${HOME}/lib:${LD_LIBRARY_PATH}" export LD_LIBRARY_PATH
一度ログアウトする
$ exit
切り替え
# su - _smtpd
環境変数の確認
$ env | grep LD_LIBRARY_PATH LD_LIBRARY_PATH=/opt/smtpd/lib:
libevent構築
$ cd $ cd src $ tar zxf ../distfiles/libevent-2.1.10-stable.tar.gz $ cd libevent-2.1.10-stable/ $ ./configure --prefix=$HOME $ make $ make install $ make clean
libasr構築
$ cd $ cd src $ tar zxf ../distfiles/libasr-1.0.2.tar.gz $ cd libasr-1.0.2/ $ ./configure --prefix=$HOME $ make $ make install $ make clean
OpenSMPTD構築
$ cd $ cd src $ tar zxf ../distfiles/opensmtpd-6.4.1p2.tar.gz $ cd opensmtpd-6.4.1p2/ $ ./configure --prefix=$HOME \ --with-libasr=$HOME \ --with-libevent=$HOME \ --with-libssl=$HOME $ make $ make install $ make clean
環境設定
root
にて作業を行う
ディレクトリの移動
# cd /usr/local/etc/rc.d/
もし存在しない場合は作る
# mkdir -p /usr/local/etc/rc.d/ # mkdir -p /usr/local/etc/mail/ # cd /usr/local/etc/rc.d/
サービス用スクリプトを作成する。
# vi optsmtpd
内容は下記の通り。
#!/bin/sh # PROVIDE: optsmtpd mail # REQUIRE: LOGIN # KEYWORD: shutdown . /etc/rc.subr basepath="/opt/smtpd" LD_LIBRARY_PATH="${basepath}/lib:${LD_LIBRARY_PATH}" export LD_LIBRARY_PATH name="optsmtpd" rcvar=optsmtpd_enable start_precmd="optsmtpd_precmd" restart_precmd="optsmtpd_checkconfig" configtest_cmd="optsmtpd_checkconfig" extra_commands="configtest" load_rc_config $name : ${optsmtpd_enable:="NO"} : ${optsmtpd_config:="/usr/local/etc/mail/smtpd.conf"} : ${optsmtpd_procname:="${basepath}/sbin/smtpd"} : ${optsmtpd_flags:=""} command=${optsmtpd_procname} command_args="-f ${optsmtpd_config} ${command_args}" required_files="${optsmtpd_config}" procname=${optsmtpd_procname} pidfile="/var/run/smtpd.pid" optsmtpd_checkconfig() { echo "Performing sanity check on optsmtpd configuration:" eval ${command} ${command_args} ${optsmtpd_flags} -n } optsmtpd_precmd() { optsmtpd_checkconfig } run_rc_command "$1"
権限の設定を行う
# chmod 755 optsmtpd # service -l | grep optsmtpd optsmtpd
自動起動の設定を行う。
# cd # vi /etc/rc.conf
下記の行を追加する。
sendmail_enable="NO" sendmail_submit_enable="NO" sendmail_outbound_enable="NO" sendmail_msp_queue_enable="NO" optsmtpd_enable="YES"
sendmailの強制停止
# service sendmail forcestop Stopping sendmail. Waiting for PIDS: 1983. Stopping sendmail_msp_queue. Waiting for PIDS: 1986.
設定ファイルの配置
# vi /usr/local/etc/mail/smtpd.conf
内容は下記の通り。
table aliases file:/etc/mail/aliases listen on 127.0.0.1 action "local" mbox alias <aliases> match for local action "local"
プロセスを立ち上げる
# service optsmtpd start Performing sanity check on optsmtpd configuration: configuration OK Starting optsmtpd.
起動確認
# ps -ax | grep smtpd 70778 - SsJ 0:00.02 /opt/smtpd/sbin/smtpd -f /usr/local/etc/mail/smtpd.conf 70779 - IJ 0:00.02 smtpd: klondike (smtpd) 70780 - IJ 0:00.02 smtpd: control (smtpd) 70781 - IJ 0:00.03 smtpd: lookup (smtpd) 70782 - IJ 0:00.03 smtpd: pony express (smtpd) 70783 - IJ 0:00.03 smtpd: queue (smtpd) 70784 - IJ 0:00.02 smtpd: scheduler (smtpd) 70790 0 S+J 0:00.00 grep smtpd
メール送信テスト
# echo hello | mail root (少し待つ) # mail Mail version 8.1 6/6/93. Type ? for help. "/var/mail/root": 1 message 1 new >N 1 root@empty.lan Tue Jul 23 05:08 18/575 & 1 Message 1: From root@empty.lan Tue Jul 23 05:08:39 2019 Delivered-To: root@empty.lan Date: Tue, 23 Jul 2019 05:08:39 GMT From: Charlie Root <root@empty.lan> To: root@empty.lan hello & q Saved 1 message in mbox #
停止確認
# cd # service optsmtpd stop Stopping optsmtpd. Waiting for PIDS: 70778. # ps -ax | grep smtpd 70810 0 S+J 0:00.00 grep smtpd #
最後に
どこまでうまく動くかは不明。
あと、6.4.0からsmtpd.confの書き方が変わったんですって。
Nptで把握している問題点
いま現在問題と思っていることを列挙します。
書いておかないと本人が忘れるので。
これは、問題と確定しているわけではなく、
問題があるかどうか調査することから必要があるものです。
標準入出力のリダイレクトが非常に遅いのはなぜか調べる。 例えばこんな感じ
$ npt --script file.lisp > zzz
。 標準I/Oはキャッシュオフにしているせいだと思うがどうすればいいか考える。defstruct
で:type vector
にしたacessor
関数が、LISPTYPE_VECTOR
のみ対応しているが、LISPTYPE_ARRAY
に対応させる必要がある。defstruct
で:type
がlist
,vector
のとき、slot-makunbound
をすると、 オブジェクトの内容がunbound
になってしまうのでそれをやめる。defstruct
で:type vector
がspecialized arrayなら生成するオブジェクトも specializedにするべきなのでは?deftype
で標準のtype
を設定したらエラーになるか調べる。 例えば(deftype integer ...)
でエラーになるべきでは?deftype
で指定したsymbolがreadonlyの場合はエラーになるか調べる。 例えば(deftype :hello ...)
はエラーになるべきでは?その場合
car
とかいう名前のdeftype
はどうなる?Common Lispパッケージのsymbolをreadonlyにする必要がある。 もうなっているかも?
type-function
の第三引数のテストケースを追加。 型(function 第1 第2 第3)
の第三引数は内部用で表に出てこない。typep
でsatisfies
の実行割り込みreturn 1
に対応させる。 つまりsatisfies
の関数内でreturn-from
やthrow
が発生したときを考慮する。subtypep
でsatisfies
の実行割り込みreturn 1
に対応させる。 つまりsatisfies
の関数内でreturn-from
やthrow
が発生したときを考慮する。 でもこれは無いかも。fmte
,fmtw
を実行割り込みreturn 1
に対応させる。 つまり関数内でreturn-from
やthrow
が発生したときを考慮する。RefLispDecl
,GetLispDecl
を使用しているコードでNOTが考慮されているか調べる。 これなんだったか忘れたので調べなおす。strarray
のcharactertype
はおかしいかもしれない。strarray-base-p
関数を使うこと。 これなんだったか忘れたので調べなおす。subtypep
の引数にclos
を指定すると、not
で正しく出力されない。 これは規格自体に問題がある可能性があるため、他の処理系と合わせる。 現在、TODO
と書かれているはず。:allow-other-keys
が対応していない。
全体的に見直す必要がある、とても作業量が多くなるかも。 引数が(&rest args &key)
の場合、&allow-other-keys
が自動的に付与されるべきなのか?&key &allow-other-keys
のとき、key-value
のペアのチェックが行われていない? 一般の関数でも:allow-other-keys t
が使用できる(★&ではなく:)。 現時点だと:allow-other-keys
がt
ならたぶんeval
時点でwarning
が起こるはず。fmakunbound
で型も削除function
オブジェクトのname
にあるcallname
がsetf
になっていない。defun
で型を登録するdefun
の型をもとにeval
情報に型を登録function-lambda-expression
のためにlambda-expression
をfunction
に登録する。fasl
再作成fixnum-local
オブジェクトをcache
できるか考えるcode_macro_function
にname
を渡して処理する。 戻り値とdocumentation
がどうなるかを調査する。macro-lambda
のallow-other-keys
判定は必要だが、ordinary-lambda
には不要。 ただし、:allow-other-keys
の実装によっていろいろ変わるかも。macro
の型チェックをどうするか調査。 現状、他の処理系では型チェックしているが、npt
では一切していない。common-arrays
でtype
チェックしているにもかかわらず、 手動で型のチェックをしている所が結構あるので削除する。(integer 0 *)
をSIZE_MAX
にすることでgetindex_integer
を簡略化できる。型
index
やSIZE_MAX
はfixnum
にしてしまった方が早くてよいのでは?adjust-array
の試験を追加する。普段は
GetArrayXX
ではなくgetarray
を使うように変更、type-array
など。function
型チェックの都合上type-or
,and
,not
のasterisk
を許容したが、typep
コマンドでちゃんとエラーになることを確認するstart-end
のend
は標準でUnbound
ではなくNil
が正解。sxhash
はもう少しちゃんとできないか考える。open
は8bit以外のbinary
入出力をサポート、(unsigned-byte 16)
など。:direction :io
でread
->write
が切り替わるときにunread
を破棄する:direction :io
をもう少し何とかする。ANSI-C
モードのopen
をもう少し何とかする。全ての
stream
にてclose
された状態を考慮していないので見直す。Uniocdeのencodeをもう少し何とかする。 例えばBOMの扱いなど、Lispの
strjis
モジュール開発でわかったことを反映する。浮動小数点の出力がおかしい
most-positive-short-float
。 これなんだったか覚えてないので調べる。closの情報取得関数にデバッグの型チェックを入れる
コードに
TODO
と埋め込まれている部分が結構あるので片づける。*LOAD-TRUENAME*
で絶対パス名を入れる必要があるか考える(snmsts様、ご協力ありがとうございました)loop
マクロでtermination-testあたりがtype-spec
を無視しているので見直す。top-levelで
setf
によりlogical-pathname
を設定したら認識しなかったが、 たぶん間違いなので調査すること。
結構ありますね。
そしてもっとあると思います。
もし何かバグやら問題を見つけましたら、githubのissuesでもいいですけど、
ここにコメントで書きなぐってもらっても構いません。
ただし開発時間があまりとれなくなったため、対応に時間がかかるとは思います。