2017年7月ごろから衝動的にコンパイラを自作し始めました。1年経っても完成には程遠く、完全に自己満足の世界なのですが、誰かの為になることも書けるかも(?)知れないので日記・覚書をここにつけてみます。
メインiPhone更新なのでレイアウト変だったらすみません。
Githubリポジトリ: Palanソースコード(C++)
リファレンス: Palanリファレンスv0.1
ヒストリ
1年目: プログラミング言語Palan開発日記
日記
上の方が最新です。時系列で読みたい人は下から読んでね
実装中
C関数プロトタイプ
7月1日 3年目突入
ついに3年目突入したので、日記を新しくします。ある程度貯まってから公開したいと思います。
公開しました。→3年目日記
6月30日 机上
gdbでも関係無さそうなところで死ぬので、デバッガでの特定は諦めて、発生条件から関連しそうなコードを机上デバッグ。以下の箇所をなんとか特定。
i = 0;
for (auto p: f->parameters) {
arg_dps[i]->data_type = p->var_type->data_type;
++i;
}
範囲for使っとけば大抵大丈夫だろうと思ってたら油断したコード。可変長引数対応で前提が変わってメモリ破壊。
vector 範囲チェックありで書いた方が良さげな箇所だな。でも理論的に意味のない範囲チェックはしたくないプログラマのさが。とりあえずAssertionを入れ込む。
6月29日 フリーズ
原因不明のフリーズ。どこかのメモリ壊して、関係ないとかで変になってるっぽい動き。これあかんやつや。。
6月27日 可変長引数
Cの関数をちゃんと呼べるようにするため、今までいい加減だったC関数の宣言を厳密にできるように着手。でもテストでprintf使いまくりなので可変長引数の宣言サポートという変化球からやるはめに。ところで可変長引数ってVariable Argumentと英語で言うらしい。引数はそりゃ変数だろうよっと誤訳しかねない。
6月24日 v0.2 リリース
v0.2のタグをつけて、2回目のリリース完了!
全てスキマ時間でやってるのもあるが、思ったよりの2倍の時間がかかるなー。ホントは1年ぐらいでここまで行きたかった。まぁ、一歩一歩ゆっくりやってきますよー。
次のv0.3はC言語ライブラリを出来るだけちゃんと呼べることにフォーカスしたい。構造体がちゃんとうまくできるかどうか、うまく文法を自然にできるかが不安だが。
6月23日 英語
Google翻訳の力も借りて、githubに0.2の英語版言語リファレンスも作成した。コードで使う記号はバッククォート使えばいい感じにできた。前回はそこまで気を使う余裕はなかったな。
Qiita版も余裕があれば直そう。後は技術メモの更新かな。
6月16日 v0.2リファレンス公開
V0.2としてのコードfixはできてないけどQiitaに一旦リファレンスを公開した。githubの英語wikiも、更新しようと思ってるけどモバイルでやる事が多く、その場合は日本語版をqiitaに公開しておいた方がいろいろ楽なので。
6月15日 ループの中断
ループの中断、いわゆるbreak文とcontinue文を追加した。特に難易度は高くなかったが、中断する際はブロック内で確保していたメモリの解放を考慮する必要があった。
将来的には二重ループなどラベルをつけたループへのジャンプを実装したいが、v0.2ではこれでいいかな。
0.2に向けてドキュメントのメンテを続けよう。
6月10日 やり残し
0.2に向けてリファレンス手直し中。そこで、ループの中断の実装ができていないことに気づいた。さすがにこれがないと0.2の目標の簡単な数値計算ができるとは言えない気がする。ループ中断を何とか入れ込もう。
5月29日 配列アクセス最適化2
Moveするローカル変数が、すでにレジスタに入ってて変更なさそうな時は、Moveを省略する処理を入れ込んだ。前回のアセンブリは下記のようになる。
movq $100, -8(%rbp) # 100 -> i
movq $8, %rdi # 8 -> arg
xorq %rax, %rax
call malloc
movq %rax, -16(%rbp) # return -> a
movq -8(%rbp), %rax # i -> %accm
incq %rax # %accm + 1
movq -16(%rbp), %rbx # a -> base
movq -8(%rbp), %r11
movl %r11d, (%rbx) # i -> a[]
movl %eax, 4(%rbx) # %accm -> a[]
movq $.LC0, %rdi # ".." -> arg
movslq (%rbx), %rsi # a[] -> arg
movslq 4(%rbx), %rdx # a[] -> arg
xorq %rax, %rax
call printf
何度も出てきていた「movq -16(%rbp), %rbx」が1箇所になった。あまり深く考えてない気がするのでどこかに穴がありそうだが(ダメじゃん)、問題があったら取り除くのは簡単なので一旦これで行こう。
先日Generatorをすぐにアセンブリ出力せずに一旦貯めて分析/更新しやすくしていたので、かなり楽に実装できた。
5月24日 配列アクセス最適化
現在、アドレッシングモードで配列のアクセスをしている。そのために毎回配列の先頭アドレスをレジスタに入れているが、同じ値になることが多く無駄なのでそれを無くしたい。
例えば、下記のようなコードを書くと、
ccall int32 printf();
i = 100;
int32[] a = [i,i+1];
printf("%d %d", a[0],a[1]);
下記のようなアセンブリになる。
movq $100, -8(%rbp) # 100 -> i
movq $8, %rdi # 8 -> arg
xorq %rax, %rax
call malloc
movq %rax, -16(%rbp) # return -> a
movq -8(%rbp), %rax # i -> %accm
incq %rax # %accm + 1
movq -16(%rbp), %rbx # a -> base
movq -8(%rbp), %r11
movl %r11d, (%rbx) # i -> a[]
movq -16(%rbp), %rbx # a -> base
movl %eax, 4(%rbx) # %accm -> a[]
movq $.LC0, %rdi # ".." -> arg
movq -16(%rbp), %rbx # a -> base
movslq (%rbx), %rsi # a[] -> arg
movq -16(%rbp), %rbx # a -> base
movslq 4(%rbx), %rdx # a[] -> arg
xorq %rax, %rax
call printf
上記アセンブリでは何度も配列aのアドレスをRBXレジスタに書き込んでいるが(「movq -16(%rbp), %rbx」の箇所)、RBXが書き換えられない限りは書き込む必要がないはず。簡易VMみたいなものを作ってを単に前と同じ値だったらmovを出力しないで最適化できそうな気がする。ただ、マルチスレッドや、aのアドレスがどこかに持たれていてそれ経由で更新されることがないことなど気を付けることがあるはずなので、そのあたりをちゃんと考慮するのが難しそう。
5月23日 一段落
配列の配列の初期化等も配列表現でできるようになって、配列表現の実装が一段落するところまできた。半年はかかったか。。
少し作っては設計やり直しばかりで、嫌になったが、もともとどう設計したらいいのかさえ分からない状態だったから、個人的には大きな進歩と思う。
次は配列アクセス時のアセンブリ出力の最適化しようかな。
そこまでできたら数値計算ぐらいはできると思うので、ver0.2としてfixして仕上げたいな。
5月9日 設計ポリシー
いろいろ考えてみたが、結局、以前の設計ポリシーはちゃんと考えた結果で、ここを変えると破綻するとの結論に。なので以前の設計ポリシーに合わせて、今の配列表現の設計の方を変えることに。再設計はいびつになっている骨格を整体で直してるような感覚。で、実装してみるも、テスト通らず四苦八苦。
でも、ようやく動き始めた。配列の配列も目論見通りサポートできそうだ。
4月22日 俯瞰
配列の配列の配列表現(もう何のことだか)を進めてみたが、やはり以前の設計ポリシーとのズレがあるのが明確になってきた。
部分最適はできそうなんだけど、一旦全体を俯瞰して以前の設計ポリシーのままで行くか、変えるかを考えたい。具体的には文(Expression)のデータの受け渡しは基本値渡しの仕組みしかなくて、深いコピー等は代入文のオブジェクト内でやりくりしてたが、そこが配列表現の実装と今はマッチしていない。値渡し以外の仕組みを作った方がいいのか迷い中。
4月15日 配列表現リファクタリング終わり
ようやく配列表現のリファクタが終わって重複コードを大分潰すことが出来た。その代わり一部設計が多少いびつになっている気がする。
ただし、これは直さず一旦配列の配列を配列表現でできるようにする事を進めることにしたい。多次元配列はできてるけど、配列の配列ができないと、結局、配列表現の実装がコンプリートしないし、また設計が変わるかもしれないので。
にしても配列表現ってこんな面倒だっけか?他の人はサクッと実装してる気がするが。なかなか進捗が進まず辛抱の時期が続いてる。
4月9日 1-2-5の法則
新しい事を始めると1年目は天狗になり、2年目で自信をなくし、5年目でようやく身に付いたかなと思える自分の経験則。
2年目はその世界の広さが分かってきて自分の実力の無さを実感する年なのだ。それは成長と考え挫折しないようにしないと次には行けない。
4月7日 迷路
なかなか配列表現リファクタが終わらない。生成されるアセンブリを妥協すればシンプルにいくとは思うんだけど、今の基礎部分で妥協するともう修正できない気がするしとか思いながら、リファクタ迷路で迷い中。仕事でこんなのやってたら怒られるけど、趣味だから時間だけはいくらでもかけられる。
3月24日 地道
少しづつ広げる方法で、なんとか配列表現は配列リテラルと同じレベルまで実装できたのでマージ。予想してたがデータ構造は違うけど80%ぐらい2つのアルゴリズムは同じになった。そのせいか、車輪の再発明感でなかなかモチベが上がらず苦労した。まだまだリファクタリングや、上手くいかないパターンがあるので、出来たと言えるところまで行くのには時間かかりそう。でも、ここは時間かけてでも納得いくまでやる所のような気がしているので、地道に進めよう。
3月13日 地味
特に書くことないけど、地味に配列表現の実装進めてます。
3月7日 長いコメントより1行のAssert
自分で設計してても、少し時間が経つと細かい部分はどうしても忘れてしまう。こうだったよなと思ってクラスを使ったら、Assertに引っかかりまくる。
Assertを見て細かい設計を理解しなおすの繰り返し。結局、正しい使い方は全部Assertじっちゃんが教えてくれた。
3月4日 初心
定数の場合の配列表現([]で囲むと配列になる)が、できたので要素が定数でなかった場合(要素が変数やその数式)の配列表現に着手している。配列定数ができているのでこれは余裕かと思いきや、初めてみると全く別物で、かなり考えることが多すぎる。配列定数の場合は、ReadOnlyメモリに用意してそこを配列みたいに扱うことで事足りたけど、今回はそうはいかず、正直どこから手をつけていいかわからない。これまでの設計にどう合わせていいのか、ちょっと書き始めてはこれ違う、の繰り返し。
すでにある程度できた大きな実装に対し、いきなり合わせこもうすると全然頭が追いつかない。初心にかえって、ちょっとでもいいから動くものを作ってそれを広げて、大きくなったら共通化する作戦に切り替えてみている。
2月27日 first issue
Coverallsにつながらなかった件は、coverallsがtravisCIのブラックリストに間違えて設定されたらしく、1週間前頃に解決済となっていた。でも設定戻しても、どうしてもソースが見えるようにならず、coverallsのgithubにissue登録したところ、半日で直してくれた。github初issueとなった。
2月24日 coveralls
テストカバレッジの確認にcoverallsを使っているのだが、何も設定変えてないのに、先日から急に上手く連係出来ないようになった。いろいろ試してなんとか数値データまでは上がるようになったが、ソースは見えないまま。githubと上手く接続できてないっぽいが、原因も対処方も結局不明。。
2月20日 手術完了
ふー、なんとか悪いデータ構造の差し替えが完了した。直したのは悪いデータ構造1個だけだが影響するソースは結局40ファイルにもなった。あちこちで小さなvectorの生成やコピー処理も無くなったので、パフォーマンスやメモリ効率も良くなったはず。
カバレッジが高いテストが通るのをキープしながら修正するのは多少面倒でもかなりの安心感があった。でも、カバレッジの高さよりも、これだけの修正をしてもテストは一切修正無しなのもポイントと思う。いくらカバレッジが高くても細かすぎるテストで構成してしまっていたら、テスト修正が足を引っ張っただろうから。
2月16日 手術中
今のところテストが全て通るのをキープしながら、順調には進んでいる。根が浅いところは大体対応したが、やはり深く混み合ってるところは分離に時間がかかっている。でも、まぁ、目論見通り、変にプログラムを壊したりせずに安全に切り替えはできそうだ。
2月8日 手術プラン
相変わらず3歩進んで2歩下がるプロジェクト。今回は全体に広がった悪いデータ構造を差し替える手術となる。プランを説明しよう。
まずそのデータ構造を使っているメンバー名を変更する。PlnVariable型ではvar_typeとしているのをvar_type2などとする。元を変えれば、使っているところはコンパイルエラーになるので簡単に安全に変更はできる。
次に、新データ構造でvar_typeのメンバーを作る。機能的には旧データ構造と新データ構造は同じにする。
その後、旧データ構造を渡しているところには新データ構造も渡して、すべてのデータのパスを新旧二重にする。パスを増やすだけで中身はそのまま旧を使う。テストはすべて通るはず。
新データ構造は主に配列の型が大きな影響を受ける。まず、配列の生成部分で新データ構造の中身を生成し正しく返すようにする。
後は、テストを壊さないようにしながら旧から新を使うように処理を更新していく。新に切り替えることが成功できた処理は旧のデータパスも削除する。これを使っている箇所全体に繰り返す。
最後に、すべての切り替えができたら、旧メンバvar_type2などを削除する。漏れがあればテストで引っかかるはず。
とまぁ、全体に広がった悪設計もこんな感じで安全に除去できるとは思うが、言うは易く行うは難し。また何かにハマって時間かかりそうだ。
2月6日 がん
今はまだ配列リテラルのリファクタリング中。リファクタリングといってもほぼ作り直し&差し替え作業。新と旧のハイブリッド構成で切り替えたり戻したりしながら、新構成での既存テストは全部通った。後は旧構成の削除で切り替えが完了するはず。
まぁ、ここはいいんだけど型情報のデータの作りが悪いことにだんだん気づいてきた。配列とその要素の関係をコンポジットにせずに配列で持ってるが、これはヤバイ。型情報が拡張できないしクラスのような型も行き詰まりそう。思い起こせば、これ配列実装時のとりあえず設計で配列動いたらすぐに見直すつもりだった気が。。
もう、プログラム全体に癌のように広がってるのでこの除去手術は大変になりそう。でもこのまま機能追加して、さらに広がると致命的なので今直すしかない。
1月29日 また
これでいいのかわかんないが方針決めてぼちぼちリファクタリング中。なかなか先は長そうだ。
またreturn書き忘れでしばらくハマってた。←成長なし
1月24日 どこに何を
型の整合の設計がイマイチなのを直そうとしてるが、どう直せば良くなるのかサッパリわからない。オーバーロードサポートするために引数の型の互換性を見たり、型がない場合は型推論したり配列リテラルは代入先の型に合わせたり、それが複数の代入に絡んだらして、なかなかゴチャッとしてきてる。
今はとりあえずコード眺めては現状把握してみてるだけ。これまで感覚で設計してばかりだったから詰まると弱いなー。
眺めてて何となく思うのは、結局の所、どこに何を置くかというのが、設計の基本なのかな。
データや機能を置くクラスやモジュールが間違っているからイマイチと感じるんだと思う。
データや機能をいろいろな場所に仮置きして、シミュレーションしてみてみると、良い設計が出てくるかもしれないと思ってる。
1月22日 アセンブリ生成改良
アセンブリ生成の構造の修正が完了した。難易度は高くないが、量が多くてなかなか面倒だった。ついでにおまけ程度だが、関数を一度も呼ばない場合はスタックの確保処理を省く最適化も入れ込んだ。
次は型の整合をとるロジックを改良したい。先日書いたように寝かしてたら、いいアイデア浮かぶと思ったがそんなことはなかった。アイデア0なので、しばらくはソース眺めるだけになりそう。
1月17日 タイミング
アセンブリ生成モジュールの構造を変更中。今はストリームにアセンブリの文字列をすぐに出力していたが、1関数分は論理的なデータで貯めてからまとめて出力するようにする。
これにより、アセンブリレベルでの最適化が可能になる。例えば、すでに同じ値がレジスタに設定済みの時はmovを出力しないようにしたり、関数を一度も呼ばない場合はスタックの確保処理を省いたり、四捨五入を一命令にしたりなど出来るようになるはず。(←2019/1/22 四捨五入は不可でした)
しかしインターフェースは変えないが、1000行を超える最大のモジュールのほぼ全体の修正になる。浮動小数点数実装前にやるべきだったと後悔中。
でも、あのときは浮動小数点数のアセンブリがわからなかったから一通りやってからだと思ったんだよね。
このような将来を見た構造変更はタイミングが難しい。今回は何とかなりそうだが、タイミングを誤って機能追加をやり過ぎると気づいた時には手が付けられないほど大きくなってしまう。早すぎる最適化は悪とも聞くが、何事もバランス。とくに構造に関わる部分は、変更のタイミングを逃すと致命的になると思う。
1月12日 デフォルト引数
中途半端になっていたデフォルト引数をちゃんと実装した。配列定数と合わせて以下のような記述が可能になった。
const C = 10;
const ARR = [1,2,3];
arr3 = ARR; // int64[3]の変数宣言
arr2x3 = [ARR,ARR]; // int64[2,3]の変数宣言
arrayfunc(); // 全引数省略
arrayfunc(,[3,4],); // 一部省略
// デフォルト引数ありの関数宣言
func arrayfunc(int64[3] arr=ARR, int64[2] arr2=[1,2], int16 c=C)
{
...
}
できるにはできたんだが、デフォルト引数実装する中で、配列リテラルや型の整合をとるロジックの設計のイマイチさをひしひしと感じた。引数に配列リテラル書くと整数型はint64に固定されるという制限もあったり。設計を見直してどこかで大幅なリファクタリングをしないと将来詰まりそう。
とはいえモチベが湧かないので、手をつけずに一旦寝かせておきたい。寝かせているうちに設計アイディアが固まることもよくあるので。前からやりたかったアセンブリ生成時に最適化できるような構造に変える方を先にやってしまおう。
1月9日 配列定数
配列リテラルを定数にできるようになった。参考に他の言語はどうしてるのか気になって探してみたが、配列リテラルを普通の定数として扱える言語が全然見つからない。PHP7.0でできるとあったが標準サポートとは違う感じ。変数にして代入できないようにな実装の言語もあるが、実際は更新できたりなど中途半端。結構、配列が定数にできないんですが等の質問も多いのに何故なんだ。。
変わり者とは自覚してるが、また、唯一無二の構文を追加してしまったかも、、配列定数は結構便利と思うんだけどなあ。
1月4日 カバレッジ (ほぼ) 100%!
やっとカバレッジが99.5%を超えたので、ようやく鮮やかな緑の100%バッジが拝めるようになったヨ。記念魚拓。
99.5%とはいってもすべてのコードを通っているわけではなくてノイズとなるAssert(通ったらバグ)や、flex/bisonのコードはうまくとれなさそうだったので今は除外して計算している。とは言え、達成感はあるなぁ。
学びとしては、
- コードが通ってないところ周辺は8割何かしらバグが眠っている。97%すぎたころから顕著。コーディングスキルの問題かもしれんが。テストが通ってない(どうやったら通るのかわからない)コードは設計ミスだったり、いつ使うかわからない機能のコードを書いていたりが多かった。
- できるだけ上位処理からデータパターンを増やす自動テストでカバレッジを取ったほうがいい。世の常識としては、クラスごとにUTを書いてカバレッジをとるように言われている気がするが、個人的には賛成しかねる。それをやるとリファクタリングのたびにUTを書き直しが必要だったり、不要コードが検出できなくなったりするのでデメリットのほうが多い。複雑なアルゴリズムのテストをしなければならない以外は、細かいテストはコードが完成したら捨てたほうがいい。上位から流したほうがテストコードも少なくメンテナンスもしやすい。
- コード書いたらできるだけ早くその部分を通るテストを書く。後からだと、テストが通る条件がわからなくなる。
- 高いカバレッジを目指せば目指すほど工数が増えるという通説は、少なくともこのプロジェクトに限っては当てはまっていない。通っていないコードを通るテストを書くと、かなりの確率で容易に未検出バグを見つけてくれる。これらのバグをカバレッジ測らずに見つけようとすると相当な労力。低いカバレッジのプロジェクトでは、コード変更の影響範囲の確認に工数がかかったり、テストを書いてもバグを検出できなかったりしていることに、気づいていないだけでかなり多いのではないか。
- カバレッジ100%は目指すべき。なんか「カバレッジ100% = 必ずしも品質が高いとは限らない」記事をよく見るんだが、現場の開発者の頭の中では100%を目指さなくていい理由に変換されてしまっていることが多い気がする。品質が高くならないのはそもそもちゃんとしたテストを書いてないからであって、100%を目指さない理由にはならない。ちゃんとしたテストで高カバレッジにできれば、自ずと品質は高くなる。またコスト対効果が低いというのも、プロジェクト次第であるが、100%を目指していれば、なぜこの箇所はカバレッジの対象外という理由も明確になる。理由が明確であれば、ちゃんとその部分を除外した上で100%を目指せばよい。
偉そうに書いたけど、上記は自戒。
1月2日 型推論
簡易的な型推論の実装ができた。型推論と言っても単に変数宣言時に初期値の型を適用するだけ。代入と初期化は演算子が異なるので'='で初期化された場合は変数宣言と識別できる。
int32 i = 10; // 通常の宣言
j = 10; // 型推論ありの宣言
ただ2つ同時の宣言の場合は、bisonのLALRでは1先読みしかできないので何もなしだとコンフリクトになってしまう。flexの方で頑張って先読みすればできないことはないが、まぁそこまで頑張らなくてもいい気がしたのでvarをつければいいことにした。注意としては、直後の変数にしか効かないので、別の型の変数を宣言する場合は、そのたびにvarが必要。
var i, j = 10, 20; // 複数宣言の場合はvar必要。jはiと同じint64 型。
var x, var f = 10, 1.5; // xはint64, fはflo64
12月27日 メモ
配列サイズの推論?は大体できた。実装してみると配列サイズするぐらいなら型全体やる方が簡単というのに気づいたので、そちらもやってしまおう。
実装途中で気づいた、いろいろなことを忘れそうなので↓にメモ。
- 変数初期化時の型チェックが今ざる
- 文字列リテラルの実装を配列リテラルに合わせる
- デフォルト引数処理が中途半端に入ってる
- mainのjson例外をなくすためASTチェック強化
- 配列リテラル定数化
- 配列間の互換性どうする
- jsonライブラリ最新にしてみる
12月26日 たまたま
ローカル環境ではテストが全部通るのに、TravisCI環境ではセグメンテーション違反になる現象に悩まされた。CIでしか起こらないのでデバッグもできず、調査も面倒だった。最初はTestフレームワークとCI環境のgccのバージョン不整合を疑った(CIはTestフレームワークが常に最新になる)が、突き詰めるとただのバク。returnすべきところで変数に代入して安心してreturnしてなかった。
たまたま直前にraxレジスタ(return値に使われる)に正しい値が入っているので動いてた。CI環境ではカバレッジonにしてるのでraxを上書きしてくれて正しくクラッシュしたんだろう。
以前は、このたまたま起こるバクの仕組みは分からなかったが、コンパイラ作ってたら理解できるようになってしまったヨ。
にしても、このような、たまたま動くバクが1番厄介。CIしておいてよかった。。その前にワーニングレベル上げとくか静的解析しろって話だが。
12月21日 固定添字の最適化
配列のインデックスにリテラルが設定されていたときのアセンブリ出力を改善した。いままでは添え字は全部レジスタに入れていた。↓みたいな感じでアセンブリが出力されていた。
movq -32(%rbp), %rbx # arr1 -> base
movq $ 2, %rdi # 2 -> index
movq (%rbx,%rdi,8), %r11 # r11にarr[2]の値を設定
この出力が↓で出るようになった。
movq -32(%rbp), %rbx # arr -> base
movq 16(%rbx), %r11 # r11にarr[2]の値を設定
12月19日 配列リテラル
静的な配列リテラルの実装ができた。静的なリテラルでなくて動的な配列表現は考えることが多そうちょっと今は手が出なかった。
実装してみたら今度は配列の宣言のサイズ省略がないと片手落ち感があるなと思い始めた。さらに配列のサイズ取得が定数化できると便利なはず。ただ配列サイズ取得の定数化は今のやり方ではできなさそうで、また考える必要がある。
一旦別の気になっているところ(配列のアセンブリ出力)を直してから、やってみよう。
12月16日 3次元配列表現
先日書いた2次元配列の記法を3次元まで適用すると、下記の表記になると思ってた。
int32[2,3,4] a = [[1,2,3,4][5,6,7,8][9,10,11]]
[[1,2,3,4][5,6,7,8][9,10,11]]; // 3次元?
2次元の実装ができて、上の表記で書いてみるとなぜかうまくいかない。調べてみるとどうやら2x3x4ではなくて、2x1x3x4にパースされてた。そういわれれば、そうなるかなー。
パーサの方で工夫して上の表現だと2x3x4になるようになったけど、まだ穴があるかもしれない。でも、普通の,で区切る記法も使えてそちらは正常なので、まぁ何かあっても致命的にはならない。
int32[2,3,4] a = [[[1,2,3,4],[5,6,7,8],[9,10,11]],
[[1,2,3,4],[5,6,7,8],[9,10,11]]]; // じつはこの記法も使える
12月12日 メモリ配置バク
配列リテラルは、あらかじめリードオンリーのところに値列を書き込んでおいて、そこからまとめてコピーする実装で進めている。これはclangに近い。gccは1要素づつ即値で代入しているようで、コンパイラの癖が出るところかもしれない。
何とか単純な整数リテラルは実装できたけど、試したら思った値がとれなかった。実は配列のインデックス計算が今までしくじってて、メモリ配置が思ってたのと違ってた。普通に動くんだけど、3x2の配列のつもりが2x3の実装になってたという、ちょっと恥ずかしいバクが原因。
にしても、配列リテラルは代入元の型に合わせないといけなかったり、要素が定数でない場合に拡張した時のことや、逆に配列自体を定数にできるようにしたいとか、なかなか考えないといけないことが多くて大変そう。
12月11日 配列リテラル
配列リテラルというか配列表現を実装中。腑に落ちる設計がなかなか思いつかず、どうアセンブラ出力まで繋げていいのか、まだよく分からない。
文法の方はアイデアがあって以下のようにした。いわゆるよくある言語の文法からまた外れてしまった感があるが、行列のように見えるのが個人的にはしっくりくる。
int32[3] a = [1,2,3]; // 1次元
int32[2,3] b = [1,2,3][4,5,6]; // 2次元
int32[2,2]
c =[1,2]
[3,4]; // 改行で行列のようになる
ずいぶん前、秋田脳研で開発されたmacで動くmcというMatlabのフリー版のような、使い勝手のいい行列計算の言語があって、その文法をちょっと真似ている。
12月3日 セミコロン
ブロック内の最後の文のみセミコロンを省略できるようにした。if分の実行部をブロック必須にしたので、実行する文が1行しかない場合にセミコロン書くのがなんとなく煩わしく感じていたので。Pascalも同じような感じだったと思う。
最近の言語のように行末のセミコロン不要までする勇気はない。何となく文の終わりを誤解釈した場合のダメージが大きそうというか、人が間違いに気づくのが大変そうな気がするんだよね。
11月30日 浮動小数点サポート
浮動小数点の比較の実装が完了。これで一通り浮動小数点はサポートでき、大きな山場を超えた。
次は配列リテラルに行きたいがその前に気になるところを潰しておきたい。とりあえず、メモリ確保したあと全く解放してなかったので、メモリを適時フリーするコードを入れてみている。効果確認のため計測するとメモリを1番使ってたのはjsonのオブジェクトのようだった。自分が書いたコード部分で頑張ってメモリ節約しても最大メモリ使用量はあまり減らずちょっと残念。
でも後で必要になってからでは省メモリ化は面倒だし、UTカバレッジもテスト追加なしで、かなり上昇したから自己満足しとくか。
11月27日 100%の壁
浮動小数点の比較ができてきたので、テスト見直し中。現在カバレッジは98%まで上がっている。githubに貼り付けているCoverallsバッジの色は一応グリーンなんだけど、ややくすんでいる。100%(99.5%以上)にしないと綺麗な緑のバッジにはならない鬼仕様のようです。
ちゃんとテストしたコードを追加してれば母数が増えてその内上がると思っていたけど、最近どうも上がりが悪い。計算してみたら、現在テストで1行通ってない箇所を潰すのと、100%カバレッジのコードを50行追加するので同じ上昇率。既存コードの未テストこのままだと全体規模4倍になるまで100%緑バッジは拝めない。。
このぐらいのカバレッジになってくると、既存コードのテストしっかりした方がカバレッジ上げるのには寄与しやすいということか。
でも、テスト通してないとこって、なかなか通る条件がわかりにくい、過去のコードならなおさら理解が難しい箇所なんだよね。
11月25日 Alignment
浮動小数点の比較実装に手こずり中。途中comisdを使うべきところでcomissを書いてしまってしばらくハマってた。あとは、整数と小数リテラルの比較では作業用レジスタ節約のため、リテラルの値はメモリから読むことにしたのでその実装追加も行った。ただ、Alignmentのルールがよくわからない。SIMDのときはメモリは16バイト境界に置く必要があるという記憶があったが、gccのコード見ると8byte境界に置いてあったので、とりあえず8byte境界にした。
11月18日 Float比較
浮動小数点の比較に着手したがいまいちアセンブラの使い方がピンと来ない。gccではucomisdを等号系の比較に使い、comisdを不等号系の比較に使っているようだ。違いはNaNの時の例外にあるとのことだが勉強不足でよくわからない。比較後のフラグでのジャンプもjgeとかではなくてjaeのほうを使わないとうまくいかないようだ。不明点が多いのでしばらくのんびりペースになりそう。
あと浮動小数点の定数は即値で作業用レジスタにコピーしているが(gccはメモリ。9月29日参照)、デメリットも感じてきた。使える作業用レジスタが定数を使うときに占有されてしまうのが痛いかも。いまのところは大丈夫だけど、将来問題に当たりそうな気がしてきた。あとは、整数にした定数が32bit/64bitをまたぐかまたがないかで処理を分けないといけないのも、面倒になってきた。
11月13日 Float四則演算
スキマ時間にやってるので、仕事でコーティングに疲れたり、調べ物すると進捗劇落ちしてします。ともかく浮動小数点の四則演算は大体できた気がする。試しにここの最小二乗法をポーティングしてみた。
ccall int32 printf();
const N = 5;
flo64[N] x, y;
1.1, 2.3, 2.8, 4.2, 5.1 -> x[0], x[1], x[2], x[3], x[4];
0.7, 1.9, 3.1, 4.2, 5.6 -> y[0], y[1], y[2], y[3], y[4];
printf("%.3f, %.3f\n", lsm(x, y));
func lsm(flo64[N] x, y) -> flo64 a0, a1
{
int32 i=0;
flo64 a00, a01, a02, a11, a12 = 0,0,0,0,0;
while i<N {
a00 + 1.0 -> a00;
a01 + x[i] -> a01;
a02 + y[i] -> a02;
a11 + x[i]*x[i] -> a11;
a12 + x[i]*y[i] -> a12;
i+1 -> i;
}
(a02*a11-a01*a12) / (a00*a11-a01*a01) -> a0;
(a00*a12-a01*a02) / (a00*a11-a01*a01) -> a1;
}
小数点の型名は迷ったけど、intっぽく短くかつサイズを明確にしたかったので、こんなんなりました。
やっぱり配列リテラルがないと数値計算にはイマイチ使いづらい。小数が一通りできたら配列リテラル実装してみよう。
10月28日 DBL_MINのその先
テストでどうしても通らない小数点リテラルのロード処理があったので、テストもできるようにと指数表現リテラルも追加してみた。そして、テストを追加したら、C++標準ライブラリの文字列→小数変換でDBL_MIN(2.225074e-308)を超えたらしくアンダーフローしてしまった。でもアンダーフローになるはずのバイナリ値でもprintfではさらに下(4.940656e-324)を表示できていて、意味がわからなかった。
ここは完全に不勉強だったんだけど、浮動小数点で最小限界を超えた場合に、指数部を0にして、残りのビットは固定少数点になるという仕組みがあるとのこと。浮動小数点の表現ができているうちは正規化数、固定小数点になった表現を非正規化数と呼ぶみたい。また、0や無限大は特別扱い。今回のテストは非正規化数の領域の値にならないと通らないが、C++の文字列→小数変換は非正規部分はアンダーフロー扱いみたいだった。
ライブラリがサポートしてないので、非正規化部分のリテラルで表現とテストは一旦諦め、余裕がある時に頑張ってみよう。
10月25日 -0.0
浮動小数点の符号反転を実装しようとしたが、それらしいアセンブリが見つからなかった。そこでgccの動きを調べてみると、最上位の符号ビットを直接反転させてるみたいだった。0x8000000000000000をレジスタにロードしてそれとのxorをとるという、割と重い処理。整数ならアセンブリで一発なのに。
ふと、0の場合はどうなるんだろうと頭によぎった。0なのに最上位ビットが立ってしまう。で、C言語で試してみた。
#include <stdio.h>
int main()
{
double a = 0.0;
a = -a;
printf("%.1f\n", a);
if (a) printf("not zero\n");
if (a==0.0) printf("zero\n");
if (a<0.0) printf("minus\n");
return 0;
}
結果
-0.0
zero
なんと表示は -0.0になってしまった。ただし、条件分岐では0.0と同じ動きになる。うーん、なんか気持ち悪い。
10月23日 アグリーコード
小数点減算をほぼ実装できた。でも、ただでさえ醜いコードだった加算減算のモデル生成部分がさらに悲惨なコードになった。コンパイル時計算を入れ込んでるので条件分岐がひどい。まぁ読めない事はないんだけど。前回も綺麗にしようとして直したんだが醜いままだった。何とかリファクタしてビューティフルコードにできないかなー。
10月18日 切り捨て
加算あたりを実装中。やっはり他の型との変換部分のコードが膨らんでしまう。
それで整数への変換をテストしていて気づいた。小数から整数に変換した際、四捨五入になってる。。小数から整数は当然切り捨てのみと思ってたので、これは少し驚いた。
cvtsd2siだと四捨五入の変換になる。(←2019/1/22 嘘でした。正確には最近接丸め(偶数丸め)でした。)切り捨て(truncate)の場合はcvttsd2siを使えばいいみたい。
10/19追記)直前に0.5を加算のアセンブリ検出した際は四捨五入の方に置き換える事で最適化できる。(←2019/1/22 できません)。覚えておこう。(←2019/1/22 覚えておいたが意味なかったです)
10月14日 floatアセンブリ
ベクタ計算を考えず浮動小数点の扱いに限れば、x86-64の浮動小数点のアセンブリの書き方はそんなに難しくないみたい。簡単にいうと、同じ型の場合はmov*系を使い、違う型の場合はcvt*2*を使えばよい。*の部分は倍精度の場合sd、単精度の場合ss、64bit整数の場合siになる。汎用レジスタの内容がすでに小数点型の場合で、xmm系のベクタレジスタに転送する場合は汎用レジスタ系のmovであるmovqが使える。
本当はベクタ(SIMD)を使ってガンガン計算やるでー、っとしたいところだが、今は全然ムリ。
あと、定数のための固定値は少数点のバイナリを整数値にして出力する必要がある。キャストではバイナリが変わってしまうので、裏技なのか正攻法かはわからないが、union { int64t i; double d; } u; みたいな共用体を使うと簡単に変換できる。例えば3.14のバイナリ整数値が欲しい場合は、u.d = 3.14;で代入した後に、u.iで簡単に取り出せる。
10月11日 バランス
なんとか、小数-整数間の変換の組み合わせは網羅できた気がする。まだ力技っぽいのでこれからリファクタリングして綺麗にしないといけない。
ただ、コードを最小化しようとして共通化しすぎ、後から見るとどう処理しているのかわかりにくくしてしまう癖があるようだ。
シンプルな構造にして、わかりやすさと無駄のないコードのバランスをとらないといけないと自分に言い聞かせてみる。
10月6日 組み合わせ
浮動小数点の方の代入を実装中。浮動小数点関連アセンブリの書き方のコツはつかめてきた気がする。汎用レジスタ-ベクタレジスタ間の移動に普通にmovqを使えばいいと気づくのに時間がかかった以外は、大きな問題には特にハマってはいない。しかし、整数型との変換のところが組み合わせ爆発気味でなかなか終わりがみえない。
たった型を二つ増やしただけなんだが、整数8種(1,2,4,8/sign,unsign)、即値からと、メモリ、レジスタ、移動向き、など、いろいろ組み合わせがあってそれ毎に実装が必要。アセンブリって、意外と命令名や法則がバラバラなので共通化するのは簡単ではない。とりあえず力技で実装した後に、法則を見つけてリファクタリングする方針。
10月2日 BOOST_ASSERT
浮動小数点の型を追加するにあたって、コードを追加しなければならない所があちらこちらに存在する。変更が漏れた場合は謎の動きをしたりしてデバッグ地獄に落ちるパターンの恐れがあるが、今のところ、あまりそんな苦労はしてない。
なぜかというと、変更が必要な個所で、大体BOOST_ASSERTで引っ掛かって止まってくれるから。私のコーディングの癖というかポリシーなんだけど、想定外の値が来た場合はBOOST_ASSERTで落ちるようにコーディングしている。
そうすると、例えば型のIDを追加して、修正して動かすと大抵BOOST_ASSERTで引っ掛かって落ちる。さらに7月初めにviの設定をしたので、その該当BOOST_ASSERTコードの箇所が自動的に開くようになってる。大方そこが修正箇所だから、修正→コンパイル&実行→アサートで止まった箇所を修正 の繰り返しをしていれば、もれなく修正ができて動くようになる。
最近は特にJavaなど、Assertionが軽視されている傾向があるような気がするが、Assertionはホントに助かる。何でこの文化がなかなか根付かないのか。(私の周りだけ?)
9月28日 float関連 ABI
とりあえず、小数リテラルで変数初期化してprintf表示をやってみているが、はまったり悩んだり中。
- @ruiuさんの日記に書かれてたスタックを16バイトでアラインメントできてなくて、printf がSEGる現象に当たる。ちゃんと日記の内容は覚えてて、事前にアラインメントしてるつもりだったが、もれなく8バイトずれてました。でも、すぐ原因がわかったので、日記様々。
- 小数点定数のメモリのロードの仕方で迷う。整数と同じで、即値を汎用レジスタに入れてメモリに書き込めばいいと思うんだけど、gccもclangも即値ではなくてReadOnly領域から一旦xmm0にコピーする実装みたい。何故だか理由がわからない。32bitとの互換性なのかな? とりあえず即値で実装してみる。
- printfみたいな可変長引数の場合には使用するvector レジスタの数をraxレジスタにセットすることにABIがなっているが、全く意図がわからない。レジスタの数に関係なくprintfはxmm0-xmm7からデータを取ってるみたい。raxに3を渡して、xmm2まで使って残りはスタック渡しできるとか、そういうABIかと思ったがそういう動きでもないみたいだし、0を渡すとxmmNからはとらなくなるけど、どこから値を取ろうとしてるのか不明。。とりあえず何か必要なことがあるかもしれないんだろうということで、このABIの深追いは止めよう。
- ASTをjsonにしているが、jsonでの保持の仕方に迷う。ライブラリはdoubleをサポートしてるが処理の過程でtext->double->text->double変換になるので、途中、誤差で情報がロストしていないか不安になる。今はnlohmann/jsonライブラリを信じてdoubleでいくことにする。
9月24日 着手
ようやく入れ替え可能な代入の実装が完了した。カバレッジも確保。
今まで、カバレッジはCoverallsから確認してた。ただ、Coverallsはどこができてないかを調べるのにはよかったけど、テストケース追加して通ったか確認するのには向かない。Pushしてから表示されるまで5分以上かかるのはさすがに効率が悪すぎた。今回は、ローカルでピンポイントのカバレッジを観れるよう、スクリプトを書いた。
gcovを使うと、開発してるフォルダが荒れるのでそのお掃除ふくめて。これでテストケース追加から確認が1秒でできるようになった。
よーし、これで念願の浮動小数点サポートに着手できる。でも、アセンブリの勉強から始めないと、、実は、浮動小数点のアセンブリの書き方は全然知らないんです
9月21日 所有権入れ替え
所有権の入れ替え実装できたかも。
int32[10] a, b;
a, b ->>b, >>a; // aとbの所有権swap
Palanの所有権移動は単なるアドレスの代入ではない。所有権を渡した変数は0クリアされるし、渡される変数は既にあるものはfreeする。
Pythonみたいにただのリファレンスなら基本型と同じで楽なんだが。
なんでうまくいくか、じつは未だに理解できてないが、ちょこっとの修正で動いた。後はテストで問題ないか確認する。
しかし、ちゃんとカバレッジを満たしたテストを書くのは難しい。面倒くさいだけなら我慢してテストを増やせばいいだけだが、自分が書いたコードなのにコードを通る条件がわからない事が結構ある。コード見ると必要な処理と思うんだけど、通る条件がわからない。たまによく調べると絶対通らないコードだったりする。
後から通らないコード検証は辛すぎるので、やっぱり新規コード追加時にちゃんと全て通すのが大切なんだろうな。わかっちゃいるけど実践は大変。
9月19日 入れ替え代入
入れ替え可能な代入が基本型と配列のディープコピーが実装できた。最初は上手いこと条件探せば最小限の手間でいけると思ったけど、ディープコピー時は無理で結局静的解析みたいなことしてる。
i, j -> j, i; // iとjのswap
手を抜いて事前にコピー前の値を全部保存しとけばいいんだろうけど、それはプライドが許さないみたい。
これ他の言語あんまりやってない文法と思ってたらPythonできたのね。流石にディープコピーはないだろうが、なんか残念。
後は所有権の移動もうまく入れ替えしたいんだが、これも複雑で頭が回らない。でも、地道に実現するぞー。
9月13日 思い込み
Swap可能な代入のテスト書いてたら、whileの後の条件式をカッコで囲むと文法エラーになるバグ発見。
字句解析Flexの字句定義の優先順位は上から書いたものが最優先されるという残念な思い込みをしていた。
ifのようなキーワードは上に書いとけば大丈夫だと思い込んでたけど、よく考えたら1番長く一致したものが最優先だよね。長さが同じ時にのみ上が優先。そうしないとiflなどifから始まる変数が使えないよね。今回は(が後ろに続くので関数名と判断されていた。。
カッコぐらい当然動くと思ってテストもなかった。思い込み厳禁ですね。
9月11日 ブランク
しばらくドキュメントばかり書いてたせいか頭が働かず代入の仕様変更のコーディングがなかなか進まない。
もともと一回実装あきらめたところなので難易度が高いというのもあるんだろう。だけど、以前のリファクタリングでだいぶ難易度は落ちてるはずなのだが。しばらく、ぼちぼち進めてリハビリするか。
9月6日 Google様
Google様の翻訳力に98%依存して英語リファレンスも作成、githubのwikiに掲載。変な英語もあるかもしれないが、まぁ意味は通じると思う。本当に使える言語ができたとして、その頃にはGoogle様の翻訳力もさらにUpしてるはず(←自分でやる気なし)。ようやくドキュメント地獄も抜け、v0.1の作業は全て終了。
次のv0.2のメインは浮動小数点のサポートと決めてるが、その前に代入の仕様と動作を変えたい。。
9月4日 日本語リファレンス完成
ようやくリファレンス完成してQiitaに公開。
https://qiita.com/tosyama/items/15f9f32b1fcf35d36178
ただ、リファレンスに載せた簡単なコードを念のため動かしてみたら、コンパイラがバグって思った出力がでないという情けない状況に。大したバグではないんだけど、これはあかん。気を取り直してv0.1.1をだしてから、v0.2に向けて頑張ろう。
ちょっと、その前にgithubにもWikiに英語でリファレンスを載せたいが、自力で書く気力が出ない。ここはもう、google様の翻訳に任せてしまおう。
8月30日 v0.1.0
ちょっと気になるところ修正して、v0.1.0のタグづけして、祝初コードフリーズ
残るはリファレンス
8月28日 進捗
リファレンスの進捗は日本語でようやく7割ぐらい。まだ最小限の仕様のはずなのに結構書くことがある。コーディング1年、ドキュメント2週間と考えれば、文書にかけた時間はかなり少ない方だ、と自分に言い聞かせてみる。
8月25日 終わらない
リファレンス作成が全然終わらない。どう書けばいいかも迷うし。やはり少しずつ作っておくべきだったかなぁ。でも、挫折しそうで仕様も決まってなかった、すぐ仕様変わるのにリファレンス作る気になるわけなかったので必然の結果といえば結果かなぁ。
8月20日 ドキュメント
v0.1として入れる機能の実装はほぼ終わったのでヘルプ実装とかリファレンスを作り始めた。リファレンスは流石に英語だと辛いので、まずはQiitaに日本語でまとめる。
はぁ、ヘルプ、ドキュメントは面倒だー。面倒だけど人に伝えるものを用意するのは大事。でもやっぱり面倒だよー。
8月18日 クイックソート2
定数の実装が終わった。これで2月頃に書いたクイックソートを書き直すと以下のようになる。
ccall int32 printf();
ccall exit();
const N=10;
// start
int16[N] data;
int32 i=0;
// init data.
while i<N {
i*13 % (N-1) -> data[i];
i+1 -> i;
}
// sort
printf("before:");
show(data);
printf("after:");
data ->> quicksort(0, N-1)
->> data
-> show();
// flush stdout.
exit(0);
func show(int16[N] data)
{
int32 i=0;
while i<N {
printf(" %d",data[i]);
i+1 -> i;
}
printf("\n");
}
func quicksort(int16[N] >>data, int32 left, right)
-> int16[N] data
{
int32 i, mid;
int32 temp;
if left >= right {
return;
}
left -> mid;
left+1 -> i;
while i<=right {
if data[i] < data[left] {
mid+1 -> mid;
data[mid] -> temp;
data[i] -> data[mid];
temp -> data[i];
}
i+1 -> i;
}
data[left] -> temp;
data[mid] -> data[left];
temp -> data[mid];
data ->> quicksort(left, mid-1)
->> quicksort(mid+1, right)
->> data;
}
定数が出来たことで大分スクリプト的に使えそうな雰囲気が出てきた(あくまで雰囲気だけです)。気になるのは、for文がなく手動インクリメントしてることと、exitを明確に書かないとprintfから出力されないことと、swapのハードコーディングぐらいかな。
8月14日 近道
思ったより少ない修正でスコープ対応ができた。定数もほぼ実装できた。が、副作用として関数の宣言の引数な配列サイズにも定数を使えるようにするために関数定義もスコープで管理するようにしなければならなくなった。まぁ、もともと出来たらいいなとは思っていた構文だったので、なし崩し的に実装中。ある意味近道してるかも。
今まで関数定義がトップレベルでしかできなかったのが、ブロックの中や関数の中でも関数が定義できるようになる。例えば関数が大きくなりすぎた際に分割した関数なども関数の中に書けるようになる。スコープで管理されるのでprivate等の識別子なしで明確に隠蔽され結構便利と思う。
ただスコープが別で同じ名前の関数の場合の取り扱いなど、まだ考えることが残っており完成まではもう少しかかりそう。
8月12日 スコープ
定数の実装を進め始めた。結局、仕様はGoに近い感じなのかな。そこで問題発生。
定数の有効範囲にスコープを持たせたかったが、トップレベルと関数定義間のスコープの関係が、ASTにした段階で失われてしまっていた。直すにはASTの定義からの見直しになる。
とりあえずの対応もできるけど、後になるほど修正困難になるのでやるなら今やらないとなー。
この修正をどうしても回り道と感じてしまうのはマインドの問題。ここは近道と考えるようにしたい。
8月10日 定数
定数の実装を開始した。ver0.1で追加する言語機能はこれで一旦最後にしたい。後でもよさそうだがv0.1でも最低限の実用性も欲しいという想いもあり、サンプル等を書いててまず最初に気になったのが定数だったから。
定数といっても検討を始めてみると、なかなか奥が深い。単純なのはCの#defineマクロで単純に置換するものから、JavaやC++のconstのようにスコープと実体と型を持った変数だが変更不可のもの、Pythonに至っては定数はない。配列宣言時の添字にも使えるようにコンパイル時の計算も必須。
Palanでどうするか悩んだが、基本リテラルとそれ同士の計算結果に限ることにした。スコープは持たせる。定数の用途でメモリに実体を持たせて便利なケースがあまり思いつかないので。値はリテラルなので型の指定も不要にして気軽に使えるようにしたい。個人的には、ちょっと定数を使いたいだけなのに、public static final 型名まで書かないといけないJavaにはうんざり。
8月8日 配列宣言見直し
配列の宣言、正確に言うと配列の配列の宣言の見直しを行った。何ヶ月か前もこのトピックで迷ってたが、今のD言語形式をやめて、結局Java/C#形式にした。自分でテスト書いてて混乱したので、いくら理論的に文法が綺麗でも人間が使いにくい構文はダメだよね、と思い直した。
具体的には下記のように、宣言時と利用時で後置子の順番が逆になってた。
int32[3][3,2] a;
3 -> a[2,0][1];
逆ではあるが、これは”int32の配列の2次元配列”と、素直にデータ構造を宣言で表現できてた。でも使うときにやっぱり混乱してしまう。
そこで下記のようによく見る形式に直した。
int32[3,2][3] a;
3 -> a[2,0][1];
これだと、”変数に[,][]の後置子を使用するとint32になるよ”、みたいな意味合いになるのかな。
将来的にポインタを扱えるようにしたとして、その場合の識別子は、Cと違って後置にしてしまうかも。
8月6日 チェーンコールマージ
チェーンコールの実装が完了し、masterにマージした。これで、例えば、今テストに使ってる8クイーンの下記のコード
byte[8,8] board;
initBoard(board>>)->>board;
queen(board>>, -1, -1)->>board, count;
が、
byte[8,8] board;
board->>initBoard()
->> queen(-1, -1)
->> board, count;
とより直感的に書けるようになった。
言うなれば、ベルトコンベアに基となる材料を載せて、ベルトコンベアが進んでいく中で加工機械と設定や部品を加えて、最後に求めた加工物が出てくるようなイメージで書ける。
8月3日 弊害
テストカバレッジが計測されるようになって、テストしてる所としてない所が見えるようになった。そこを埋めてカバレッジを上げるのも、なかなかやった感があり、これはいいこと尽くしだなと思ってた。
で、先日コード眺めてて、継承の親クラスのデストラクタvirtualしてなかったぜー、と思って追加してコミットしたら、今はあえてdeleteしてないこともあって、暗黙に生成されたデストラクタが実行されず、子クラスのカバレッジが軒並み落ちた。当然のようにこの修正は止めとこっと思って、コードをいそいそと元にもどしたのだった。
しばらくして、ハッとした。コードカバレッジを下げないことを優先してしまい、やるべきことをやめてしまった自分に気づいた。無意識にやってしまったことに、洗脳に似た恐ろしさを感じた。数値に支配されないようにせんとね。
8月1日 チェーンコール
チェーンコールの実装に入った。現時点でほぼ思ったように動いてる。チェーンコールとは、簡単に言うと、関数の引数を代入の構文(->)で指定する仕組み。これにより、データと処理を左から右に流れるように表現できる(かも)。この文法を思いついたので、代入を=でない構文にすることを決心できた。
例えば今まで
add(a,3)->b;
と書いていたコードが、
a->add(3)->b;
a,3->add()->b;
のようにも書ける。もちろん繋げることもできるので
// add(add(a,3),9)->b;
a->add(3)->add(9)->b;
のようにも書ける。今のところは引数の順番に依存するなど制限が多い文法にはなるが、このままでも十分使えると考えている。
また、
a->add(3)->a;
は、再代入を書くのも面倒なので、将来的に
a.add(3);
と書けるようにしたい。あれ?これは、よく見る書き方に。まだ関数のことしか考えてないのに、何でもオブジェクトのように扱えて自由に拡張できる文法ができてしまったかも!?
7月30日 解決
一時退避用のスタックが解放されない問題が思いのほか早く解決した。原因は関数コールの後、戻り値を使わない場合に、レジスタを解放済みのステータスに変える処理が漏れてた。なので次にレジスタを使うときに、裏で退避用のスタックが確保されて、そのままになってた。コードにより微妙に動きが変わるので、再利用されない複雑なバグと思ってたが、割と単純な原因でよかった。
戻り値がmallocで確保したアドレスの場合はちゃんとやってたのに、普通のプリミティブの処理を漏らしていたとは。printf呼ぶたびこっそりこのリークが発生していた。
今後、同様な事が起こらないように、文の終了毎に一時的に使ったスタックとレジスタが解放のステータスかどうかチェックする処理も入れ込んだ。
7月29日 スタックリーク
未実装だった代入の連結が何とかできたが、一つ厄介そうなバグを見つける。全然原因がわからん。
問題は一時退避用に使用したスタックの領域が使い終わっても再利用されないというもの。
movq $.LC8, %r11
movq %r11, -200(%rbp) # ".." -> (savex) for save
movq -160(%rbp), %rbx # ee -> base
movq $0, %rdi # $ -> index
movzbq (%rbx,%rdi,1), %rsi # ee[] -> arg
movq -168(%rbp), %rbx # ff -> base
movq $1, %rdi # $ -> index
movzbq (%rbx,%rdi,1), %rdx # ff[] -> arg
movq -200(%rbp), %rdi # ".."(savex) -> arg
xorq %rax, %rax
call printf
上でいうと-200(%rbp)が一時退避用の領域で、この後再利用したいんだけど、その後のコードで一度も利用されず、代わりに新しく-208(%rbp)が使われるような動きになってる。
これは直さなくてもコンパイルしたプログラムは動くは動くんだけど、将来的にレジスタ割り付けの最適化しようとした場合の障害になりそうなんで今のうちに直したい。おかしい場所はわかるのに再現用のコードを書くと再現しないしなかなか原因がつかめない。これは厄介。
7月26日 カバレッジ
テストカバレッジを見える化したら、それが気になってちょこちょこ直してしまう。現在は95%まで上昇。でもテストが通っていない箇所を埋めるためにテスト追加したら未実装見つけたり、ロジック的に全く通らない想定外のところを見つけたりなかなか有用である。
が、そのせいで今やりたい実装が亀の歩みになってしまう副作用発生中。
以下、tips。coverallsから直接バッジをgithubのreadmeに貼り付けると、キャッシュコントロールの設定がされてないらしく、githubに画像がキャッシュされて、いつまでも古いカバレッジが表示されてしまう。どうやら何年も前からある問題らしいが、原因が理解できないのかcoverallsは直す気がない。
回避策としてはshields.io経由でバッジを取得すればいいみたい。
7月22日 CI
ちょっと設定等に苦労したが、CI(継続的インテグレーション)を導入してみた。make時に毎回全テスト走ってるので必要ないかなとは思ったが、テストカバレッジが気になったので。Travis CIとCoveralls使ってGitHub のREADMEにmasterブランチのビルド結果とカバレッジが表示できるようになった。
現在のテストカバレッジは92%でした。まずまずかな。
7月20日 github-flow
ちょっとまだ仮設工事感はあるが、なんとか設計不具合を直してmasterにマージした。
で、githubでdevelopからマージを何回もしてると、マージの際のコメントが既にマージ済みの最初のコメントから自動的に足されてて、うざいなぁと思ってた。これ、なんとかならんのかと思いつつ苦節一年、ようやくgithub-flowの存在に気付きしましたよ。なんかすみません。
githubは名前の雰囲気から、何も考えずに、当然git-flowに従ってやらないといけないだろうと思ってたが(つまり開発中のものはdevelopブランチに貯めて安定版をmasterマージ)、実はgithubにはgithub-flowという別のルールがあったのね。基本はmasterから専用のブランチを作成して動作確認したら直接masterにマージして、ブランチは削除。git-flowでやろうとしてたからgithubの機能と合わず、変なことになってた。
これからはgithub-flowで進めよう。
7月18日 詰む
代入の連結に向けて未実装だった代入の式化(値を返せるようにする)を着々と進めていたが、見逃してた既存の設計不具合にぶつかった。代入時のディープコピーの仕組みと複数の戻り値を処理する造りと設計がマッチせず実質詰んだ。1つだけ戻りがある場合は動くと思うけど、考慮もテストも漏れてたなぁ。
ディープコピーは場合によって関数コールの構文モデル使ってるけど、既存のモデルを流用しようとしてるのが間違いなのか。
チェーンコールでも既存の関数コールモデルで似たような問題が出そうなのでそこから設計見直しかなぁ。んー、なかなかすんなりと前に行かせてくれないぜ。
7月13日 発覚
チェーンコール(仮)の考えがまとまってきてパーサの目処がついたので進め始めたら、代入の連結の未実装が発覚して進められない。他の言語なら=で繋げるアレ。
1-> a[i] -> b;
もー、実現の材料揃ってると偉そうに書いたばかりのにぃー。ダメダメですがな。しかもこれ上のように配列の要素とか絡むと結構面倒そう。
7月11日 コンパイルエラー強化
ようやくコンパイルエラー強化の実装が終わった。終わったと言ってもエラーのファイル位置やハンドリングのベースと、AST実装時に消えてたエラーの復活+αと自動テストのベースが完成したレベルで、エラー処理としてはまだ不十分だとは思う。
まぁエラー処理は突き詰めると際限がないので一旦ここで区切り。今後は機能を追加しつつコンパイルエラーも同時に追加していくことになる。
次はver0.1の目玉、チェーンコール(仮)を進めたい。代入演算子を->にした時に思いついた文法だが、まだ細いところまで詰め切れておらず、パーサかける気がしない。。でもパーサかければ実現の材料は揃ってるはず。考えがまとまったらまた説明を書きたい。
7月9日 道具
アサーションのライブラリとしてboost/assertを使っている。チェックに引っかかった時にファイル名と行番号が出るからという理由から。ただvimからMakeしてテストが走って引っかかったときにはファイル名を上手く検出できず、新規ファイルが開いてしまい面倒だった。結局gdbを使ってcoreから辿ったりもう一回別実行したりと無駄なことやってた。
vimの設定変えれば出来るとはわかってるんだけどズルズル1年間放置してやってなかった。
今回、コンパイルエラー強化にあたりこれはいかんと思いようやく.vimrcに設定した。
let &errorformat = '%.%#: %f:%l: %m,' . &errorformat
これだけ。たった一行だけどわかりにくくて2日ぐらいハマったぞ。
おまけ。vimですばやくファイルを開くのにctrlpを使っているんだけど、間違ってバイナリファイル開くと面倒なんで拡張子がないファイルは除きたいがMakefileは除きたくない設定もしている。これもどこにも例がなく設定に苦労したのでマイナー用途と思うけどもし誰かの役に立ったらと思い設定を晒す。
let g:ctrlp_custom_ignore = {'file': '\m^\(\(Makefile\)\@![^.]\)*$'}
いい職人ほど自分で使いやすいように自分で道具を作ると聞く。テキストエディタはプログラマにとって職人の道具みたいなもの。これからはちゃんと使いやすいようにカスタマイズするぞー。
7月3日 例外処理
コンパイルエラーの処理はC++の例外を使って実装を進めている。エラーを伝搬しなくても上の処理まで簡単に戻るのですごく楽。サーバーとかアプリケーションだと、メモリリークに気をつけないと致命的になるので使いづらいのだが、コマンドラインツールにおいては基本エラーならそこで終了なので、そのあたりはサボっても大事にならない。
ただ、必要になってから直すのは大変なので、例外飛ばす前にローカルでアロケートしたインスタンスぐらいは解放してる。
気になるのは、あちこちでやってるので関数が例外出すのかどうかがよくわからなくなる。Javaは嫌いだが、検査例外はちょっと欲しくなった。調べてみたら検査例外というか例外仕様は昔はあったが問題があり、C++では削除されたらしい。他の言語でも軒並みリジェクトな悲しい運命。。
とまぁ今回お世話になっている例外だが、Palanでは例外は採用せず別の方法を考えたい。いつ出来るかわからんが、、。なぜかというと、gccが出力するtry-catchのアセンブリは私の心折るのに十分でした
コールスタックを巻き出し(アンワインド)つつ必要な後掃除やるとか変態かと思った(褒め言葉)。あの界隈のスーパープログラマはホントすごすぎる。
7月1日 1年目振り返り
衝動的に作り始めたコンパイラだが、挫折せずに1年続けることができた。ただ、始めた頃にこのペースなら1年で出来るはずと思ったところには全く届がなかった。浮動小数点演算ぐらいは到達したかったが、まだ手もつけてられてない。
とはいえ、アセンブリもほぼわからない状態から隙間時間の開発でここまで出来たのはまぁいい方なんじゃないかとは思う。出来た文法は以下の通り。
- 整数の演算(加減算、乗算、剰余)
- 8/16/32/64符号有無整数型サポート。
- while文/if-else文
- 論理演算(条件演算)/比較
- 関数定義/呼び出し (引数/ 複数戻り値)
- 関数オーバーロード
- 1次元配列/多次元配列/配列の配列 (自動Malloc/free)
- 所有権移動/ディープコピー
- 簡易C言語関数呼び出し(文字列リテラルサポート)
プログラム自体も適度にリファクタリングかけながらやってるので出力コードの最適化など発展性も維持してるつもり。
2年目としては、まずは既存の荒いエラー処理と文法や簡易ドキュメントを一旦仕上げて、ver0.1としてまとめてから、今度こそ浮動小数点数演算の追加に挑みたい。
日記の方は他の人から見て興味を引くものにできてるかは正直わからんです。アクセスがそもそもない潜水艦的な日記になってるので、いいねもコメントも少ないし、、
もし、おもしろいと思ってくれたらモチベにつながるので、いいねもらえたら嬉しい。でも返事は苦手なのでコメントは少ない方がありがたい
覚書
- 実現出来るかわからないけどNULLを使わせないというコンセプトも持ってるが、配列のからコピーせずに要素を取り出す等の用途向けにswapを演算子<->等を組み込むのは有用かもしれない。
- 数値計算などパフォーマンス上げる為にfreeをしないモードを入れてもいいかもしれない。
- 名前付きブロック。宣言した時点でメモリ確保してしまうので、子のブロックで親のブロックの変数を宣言できる方がいい。ループ内の振る舞いは要検討。
- 関数呼び出し時のデーブコピーは呼び出し元でやってるが、リードオンリーの際リファレンスを使うどうかを呼び出し元で選べるから呼び出し先でやってもいいかも、と、一瞬思ったが、引数確定後に元の値が変わる恐れがあるのでやっぱり今まで通りでいい。
- Bool(and/or)は改良の余地ありかも。比較の種類を返すのではなくて上位if文からジャンプラベルもらった方がいいかも。
- 継承は便利なもことも多いいいが、子の型が確定しているときに子のデータにダイレクトにアクセスしたい時が面倒。共用体のように型変換せずとも気軽にアクセスできる仕組みがあればいいと思う。
- 引数にコピーを指示している関数にmoveで渡せてもいい。用途はsetterとか。