MathJaxで私が書くぐらいの内容をSATySFi化するツール
概要
MathJaxで私が書くぐらいの内容をSATySFi化するツールが欲しいのだが、MathJaxって簡単に構文木取れたりするんだろうか
— hsjoihs (@hsjoihs) 2019年9月21日
desmos.comなどのサイトから数式をコピペしてくるとそのままMathJaxで使えて便利なのだが、それをSATySFi化しようとすると括弧とかをいちいち\paren{}
とかに手動で置き換えていくのがあまりにも面倒である。これはかなり単純作業なので、ツールが欲しくなった。もちろん意味論を見て\paren{}
を\app{}{}
にするとかは手動じゃないとできないが。
とりあえずSATySFi化するツールなのでSATySFifyとかいう名前にしてみる。ググってみても衝突しないっぽいのでOK。
一番面倒なのが数式の変換なので、まずはそれを作るところから始めることにした。
とりあえずRustでフルスクラッチで作っている。行列とかも一応変換できるようになった(ただし \begin{matrix} ... \end{matrix} のみ)し、ギリギリ使えそう。https://t.co/dSMEC5svh5 pic.twitter.com/d2kjLy1m6D
— hsjoihs (@hsjoihs) 2019年10月13日
開発期間
2019年9月末~10月頭に9日間*1ぐらいやって、その後放置してしまっている。多分SATySFiを今後使っていくにつれて私ももっと機能が欲しくなってくるはずで、そうなれば機能がもうちょい増えるはず。きっと。
あっプルリク大歓迎です。今さっきMITライセンス足しました
現状での機能
まあMakefileに直書きしてあるテストを見るのがわかりやすいのだが、
()
を\paren{}
に変換\left( \right)
だったり\left. \right|_{a=x, b=p}
だったりにもちゃんと対応- 行列もちゃんと
matrix
,bmatrix
,pmatrix
,vmatrix
,Vmatrix
に対応。行列の実装は、 nekketsuuuさんのsatysfi-matrixをRust側に埋め込むという手抜き実装である - あとは
\hbar
*2とかに追加で対応している。そのため、Rust側のソースコードに登録しておくことで、ライブラリにないやつを出力ソース中に埋め込める仕組みを実現してある
あれっそんなに機能ないな*3……まあMathJaxで私が書くぐらいの簡単な内容でSATySFiと互換のない部分ってのは主にここぐらいだからか。
今後足したい機能
\dot
とか\vec
が欲しい- 数式単体じゃなくて、数式の入ったMathJax文書をそのまま変換できるようにしたい。http://draft.hyuki.netで書いた下書きをそのままツールに投げ込んでSATySFiとして使えれば理想的
- MathJax側は無駄な
{}
を許容するが、SATySFiは許容しない。これを処理するのは結構たいへんで、例えば現状の実装だと「\frac
は引数を2個取る」とかは一切見ていないので厄介 - alignされたやつの処理。行列では対応できているので、それをちょろっといじればできると信じたい
- 不正な入力を落とす方のテストも書かねば
- ブラウザで動くREPLほしい
\dots
問題。真面目に対応するのが面倒だったので、現状では`\dots` detected; converting it into `\ldots` (you might want to fix this)
というwarningを出すという運用にしている
"For most situations. the undifferentiated \dots can be used, and amsmath will output the most suitable form based on the immediate context;" じゃないんだよ、その判定をどうやって行ってるのかを書いてほしいんだよな
— hsjoihs (@hsjoihs) 2019年10月3日
MathJaxはかつてはすべて\ldotsにしてたらしいが、v2.0で直したらしい
— hsjoihs (@hsjoihs) 2019年10月3日
これは普通に直後の物体を見てるんじゃなかったっけ。a+\dots+b は直後が2項演算子だから \cdots になるけど a+\dots は直後の物体がないから下つきになる
— だめぽラボ@技術書典7 し03D (@mod_poppo) 2019年10月3日
SATySFi<s>ライトユーザー</s>分かってないマンの所感
おことわり
筆者はThe SATySFi bookをまだ読んでいないライトユーザー、いわゆる分かってないマンです。この記事書き終わったタイミングで読んで買います。この記事を読んであなたのSATySFi力が上がることはおそらくありません。
そうそう
この記事はSATySFi Advent Calendar 2018の23日目の記事です。
内容
「The SATySFi bookをまだ読んでいない素人がSATySFiを使っている様子」を(主にツイートから)振り返ってまとめた記事。(Togetterでよかったのでは?)
何に使っているか
- 数学絡みの授業で毎週出る課題
- 手元の計算用紙の清書(たまにしかやっていない)
使っていてハマった点
分数累乗バグ
@bd_gfngfn +math(${\paren{2x}^{\frac{1}{3}}}); が1枚目の画像のようにバグるのですが、仕様でしょうか?(公開初期に入れたまま更新していないので、後のバージョンで直っている既知の不具合でしたらすみません)(対照: +math(${\paren{2x}^{3}});、2枚目)#SATySFi pic.twitter.com/RbxAKXaI3s
— hsjoihs (@hsjoihs) 2018年4月7日
現在の実装でも再現しました,バグなので直します(報告有難うございます)
— (g∘f)(n) (@bd_gfngfn) 2018年4月7日
SATySFiの数式組版機能にめっちゃしょうもないバグが潜んでいたので{U+F970}した(hsjoihs さんに報告してもらった分数を括弧に対して上附にしたときにカーンされすぎるバグの原因もこれだった)
— (g∘f)(n) (@bd_gfngfn) 2018年5月1日
SATySFiの発展に貢献できた。わーい。
改ページ周り
.@bd_gfngfn #SATySFi
— hsjoihs (@hsjoihs) 2018年2月11日
改変して遊んでいたらこうなった(ページ番号が文章より上に来てしまう)のですが、仕様でしょうか?(必要でしたらソースコードの非共有gistを書いたURLをDMでお送りします) pic.twitter.com/Gvz5XFxZdN
報告有難うございます,一応認識はしているバグです.どうも改ページ不可能な鉛直方向の空白が長く続くと稀にこのような結果を生じてしまうようです
— (g∘f)(n) (@bd_gfngfn) 2018年2月11日
文章を書いていると稀にこれに抵触し、そのたびに空の+pn{}
を挿入する必要が発生している。逆にいえば空の+pn{}
を挿入するだけで解決してしまうため、そんなに困っていない。
SATySFiなんもわからんので、挿入した図がページからあふれるのを防ぐために36行ぐらい+pn{}を書いている(SATySFibookを入手できていないので仕方がない)
— hsjoihs (@hsjoihs) 2018年10月9日
\int^{c}_{\infty}
とは書けない書けなかった
あー、\int^{c}_{\infty}とは書けなくて\int_{\infty}^{c}が強制されるのか #SATySFi
— hsjoihs (@hsjoihs) 2018年2月11日
version 0.0.3で試してみたらちゃんと両方動いた。ということはこれバグだったのか。
長い数式がページから溢れたときの対策方法
JOIerがJOIをしているあいだ、私は #SATySFi をしている(さて、長い数式を途中で改行したいときってどうするんだろう)
— hsjoihs (@hsjoihs) 2018年2月11日
わからないので2つの+mathに分割した
— hsjoihs (@hsjoihs) 2018年2月11日
間違いなくThe SATySFi bookに載っているはずなので調べる。
絶対値とかってどう書くの
\left. foobar \right|_{a}^{b}はどう書くのが正解なのだろう
— hsjoihs (@hsjoihs) 2018年2月11日
定義する能力はありますが,伸縮する括弧の定義の説明は正直なところ大変なので少々お待ちください……(すみません)
— (g∘f)(n) (@bd_gfngfn) 2018年2月11日
とりあえず
— hsjoihs (@hsjoihs) 2018年2月11日
let-math \boundary m a b =
${#m \|_{#a}^{#b}}
と定義しておいて、後でまともな定義に置き換えられるようにしておいた #SATySFi https://t.co/pwMO4JmbYt
これもまたThe SATySFi bookに載っているはずなので調べる。
対症療法
人(ここでは私を指す)は愚かなので、組版を都合通りにするためだけにアドホックな個数のaを背景色と同じ色でSATySFiに描画させている
— hsjoihs (@hsjoihs) 2018年11月6日
local.satyhに
let-inline ctx \hide inner = let ctx-hide = ctx |> set-text-color (CMYK(0., 0., 0., 0.)) in read-inline ctx-hide inner
とかいうものが生まれることになった。
フォントの差
#SATySFi でナブラの上にベクトル記号を出す方法が分からない(バグなのか私がSATySFiを理解していないだけなのかも分からない)
— hsjoihs (@hsjoihs) 2018年11月1日
自己解決。フォントの問題っぽい。https://t.co/AD6LigKsxM
— hsjoihs (@hsjoihs) 2018年11月1日
「とてもフォントの問題っぽいよなー、でも万が一バグだったら見過ごすの申し訳ないしちゃんと書いてまとめるか」→書く→GitHubにpush→出るやん→はいフォントですありがとうございました
— hsjoihs (@hsjoihs) 2018年11月1日
これはスクショを貼らないと分かりづらいので補足する。私のパソコン(Mac)上で件のpdfを表示したときには次のようになる。
一方、GitHubにpushしてからpdfのGitHubでの表示をSafariで見ると
となって、右にズレる代わりに表示はされる。
わからん。つらい。
最初期に入れたのでアプデが面倒だった
SATySFiのパッケージファイルだけを更新しようと思ったら無事失敗したので、ちゃんと再インストールを試みている
— hsjoihs (@hsjoihs) 2018年10月12日
最初期にSATySFiインストールチャレンジをしたタイプの人なので環境周りがグチャッとしてる
— hsjoihs (@hsjoihs) 2018年10月12日
特にhashでハマった。
SATySFiってどのパスからフォント読みに行ってるんだろう(足してるのに動かない)
— hsjoihs (@hsjoihs) 2018年10月12日
~/.satysfi/dist/fontsにJunicode.ttf足してるのになぁ
— hsjoihs (@hsjoihs) 2018年10月12日
hashを上書きしたら直った。なるほど。
— hsjoihs (@hsjoihs) 2018年10月12日
感想としては、
SATySFi再インストールチャレンジ、ハマりポイントがなかなか多くて大変だった(旧環境のバックアップを別名で取っておくことを強くおすすめします)
— hsjoihs (@hsjoihs) 2018年10月19日
というのがある(バックアップを取っていなかったため、途中から「これ途中で諦めるとSATySFiそのものが使えなくなるので、アプデを成功させるしかない」という状況になり、つらかった)。
satisfiedの綴りを間違えそうになる
「ゆーてsatisfyを #SATySFi と打ち間違えることなんてないやろ」と思っていたところ、無事satisfiedをsatysfiedとtypoし終了
— hsjoihs (@hsjoihs) 2018年4月16日
「yの数保存則」として脳内で定式化されてしまったので、satisfyと末尾にyがあれば間違えないがsatisfiedになると途端にミスる
— hsjoihs (@hsjoihs) 2018年4月16日
「satysfi【動詞】(三単現: satysfies, 現在分詞: satysfiing, 過去・過去分詞: satysfied):組版ソフトSATySFiを用いて組版すること。」が辞書に載るようになるぐらいSATySFiを盛り上げていくことで解決される問題である。*1
使っていて思ったこと
フォーマッターほしい
「Rustで何か書きたいけど書くものがない」と思っていたが、ふとSATySFiのフォーマッターが欲しいかもしれないと思い始めたんよな
— hsjoihs (@hsjoihs) 2018年11月1日
フォーマッターほしい。作りたい。
Haskellやっててよかった
まだThe SATySFi bookが紙でしか公開されていなかったタイミング(しかも当時日本にいなかったため紙のにもアクセスできず)で、表の罫線をいじる必要が発生したことがあった。
一方こちらはSATySFiで書いてるのでSATySFibookを入手したいアカウントである(居住地がね)
— hsjoihs (@hsjoihs) 2018年10月11日
具体的には、群論の授業で「九九表*2を書け」という課題が出た。
Haskellをやっていたおかげで、demo.pdfしかヒントのない状況で以下のようなコードを書くことに成功した。
\tabular(fun cellf multif empty -> [ [cellf {Table} ; multif 1 1 {${0}}; multif 1 1 {${3}}; multif 1 1 {${6}}; multif 1 1 {${9}};]; [cellf {${0}}; cellf {${0}}; cellf {${3}}; cellf {${6}}; cellf {${9}};]; [cellf {${3}}; cellf {${3}}; cellf {${6}}; cellf {${9}}; cellf {${0}};]; [cellf {${6}}; cellf {${6}}; cellf {${9}}; cellf {${0}}; cellf {${3}};]; [cellf {${9}}; cellf {${9}}; cellf {${0}}; cellf {${3}}; cellf {${6}};]; ])(fun xs ys -> ( match (ys, List.reverse ys) with | (y0 :: y2 :: _, ylast :: _) -> ( match (xs, List.reverse xs) with | (x0 :: x1 :: x3 :: _, xlast :: _) -> let grlstY2 = [y0; ylast] |> List.map (fun y -> stroke 1pt Color.black (Gr.line (x0, y) (xlast, y))) in let grlstX2 = [x0; xlast] |> List.map (fun x -> stroke 1pt Color.black (Gr.line (x, y0) (x, ylast))) in (stroke 0.5pt Color.black (Gr.line (x1, y0) (x1, ylast)) :: stroke 0.5pt Color.black (Gr.line (x0, y2) (xlast, y2)) :: List.append grlstX2 grlstY2 ) | _ -> [] ) | _ -> [] ));
当時は日本国外でThe SATySFi bookを入手する手段が乏しかったため読んでいなかったのは仕方がないが、この記事を書いている段階ではオンラインでも閲覧ができ、そもそも私が日本国内にいる。そんな状況で読んでいないというのはただの怠慢なので読んでいきたい。本当はネット公開された時点で読もうと思ったのだけれどpixivアカウントを作るのが面倒で放置していた
ブロックコメントってあるんだろうか
あるのか分からないので全部の行頭に%
をつけている。「コメントモード」とやらは%
のみで発生する、みたいな記述を見たことがある気がするし、これが正解なのでは。
ちなみに私は「SATySFiに複数行コメントがほしい」と主張しているわけではなく、
/* */スタイルのコメントがネストできることの欠点って、パーサの処理が少し増える以外に何かあるのだろうか。
— hikalium (@hikalium) 2018年4月19日
ネストできない環境下では コメントの中身に関係なく*/で終わるってのはそれなりに明確だと思うのですが、プログラムをコメントで必ず無効化出来るとすると結構大変で中途半端にやるなら出来なくていいやって考える言語設計者もそれなりにいるだろうなと思います。
— Miura Hideki (@miura1729) 2018年4月19日
などを考えると「複数行コメントなどない」というのも普通によい設計だと考える。(ここまで書いておいて「ありますよ〜」って言われたらどうしよう)
エラーメッセージのわかりやすさって正義ですよね
SATySFi、普通に学校の課題とか出すのにも便利で使っている(ライブラリの少なさによるデメリットよりも、エラーメッセージの分かりやすさのメリットのほうが私にとっては勝るため)
— hsjoihs (@hsjoihs) 2018年4月2日
LaTeX、エラーメッセージがあまりにアなのも相まって最近はSATySFiを使っている https://t.co/tgDWAoxYjT
— hsjoihs (@hsjoihs) 2018年5月23日
利点
— hsjoihs (@hsjoihs) 2018年10月18日
・エラーメッセージが分かりやすい
・エラーメッセージが分かりやすい
欠点
・Essential Copying and Pasting From Stack Overflowが効かない
・SATySFiを理解していないので、欲しいライブラリが無いときは「書いた人いませんかー」とTwitterで呼びかける羽目になる
という(至極当たり前)
The SATySFi book、読まなきゃなんだよなぁ
さて課題の中で行列を書きたいのだがSATySFiなんもわからん
— hsjoihs (@hsjoihs) 2018年10月2日
解決しました?(聞きたい
— れいにー☔️2日目東ツ-32b (@wraikny) 2018年10月7日
してないです(複素数平面で逃げて提出しました)
— hsjoihs (@hsjoihs) 2018年10月7日
遅れてしまいましたが、SATySFi で行列を書くことができたのでライブラリとして公開してみました。ご自由にコピペしてお使いください :) https://t.co/mbVPoPA3ie
— ねっけつ (@nekketsuuu) 2018年10月12日
ありがとうございます!
— hsjoihs (@hsjoihs) 2018年10月12日
The SATySFi bookを読まないとライブラリが足りないときにライブラリを書けないので、The SATySFi bookを読まねばならない。それはそう。
追記
記事を公開した後、
ライトユーザー?(何を言っているんだという気持ち)
— ぷりにゃん (@puripuri2100) 2018年12月23日
hsjoihsさんはヘビーユーザーな気がする
— ぷりにゃん (@puripuri2100) 2018年12月23日
言われてみれば、SATySFiへの理解が足りていないだけで、書いている量はそれなりにあるのかもしれない https://t.co/DgiK9v2Qsn
— hsjoihs (@hsjoihs) 2018年12月23日
自分みたいに「SATySFiで遊んでいる時間は長いけれども書いた文章量は少ない」という人もある程度いますしね
— ぷりにゃん (@puripuri2100) 2018年12月23日
ということになったので、「ライトユーザー」を「分かってないマン」に変更。
C(のサブセット)コンパイラを書く上でハマった点:配列編
初めに
「配列編」と銘打っていますが、続編が投稿される保証はありません。
そうそう
この記事は言語実装 Advent Calendar 2018とC言語 Advent Calendar 2018の17日目の記事です。
想定している読者層
(どういう読者層を想定しているんだろう、書いていて自分でもよく分からなくなった)(Cコンパイラ書いていて「配列の配列(いわゆる二次元配列)がなんかバグるなぁ」となった人のための記事かもなぁ)(というか、多分バグらせていた当時の自分への手紙)
本題に入ろう
Cコンパイラを書く上で微妙にハマった、配列へのポインタの話、それに付随して構造体を実装する際の話について軽く書きます。
まずは結論から
配列の識別子には値がない、というのがその原因かもしれません。ということでちょっと図を書いてみました(そういうことじゃない気もする)。 pic.twitter.com/2Y9PQlLaeT
— hikalium (@hikalium) 2018年8月10日
そうそうそう、配列だけアドレスが指す先をメモリから読むって処理を飛ばさないといけないのよね(ちょうど散歩しながら考えてた)
— Shinya Kato (@0x19f) 2018年8月10日
正直上2ツイートで私が今回言おうとしている話はだいたい尽くされているんですが、一応書いていきます。
ポインタ+整数
C言語では、「ポインタが難しい」と良く言われますが、 実際に初心者がCを学習する過程を見ると、以下のことだけは すぐに理解しているようです。
「ポインタっつーのは、要するにアドレスのことなんだな」
ここまでは簡単、誰でもすぐに理解します。
ポインタに1足したら、2byteとか4byteとか進む、ということを習った時から ? が点灯する。「ポインタってアドレスなんだろ? そんなもん1進むに決まってるんじゃないのか?」
一般にT *
型のポインタp
に1を足すと、アドレスはsizeof(T)
だけ増える*1ので、C言語においてポインタを扱う際には、アドレスだけではなくポインタの型が何であるかという情報も不可欠です。まあそもそも型が分かっていないと間接参照((ポインタに*
を適用すること))もできませんが。
ちなみに、話が逸れますが、アドレスが同一で型が同一でもポインタとして同一であるとは限りません。
ポインターは簡単だ。それはただの整数じゃないか。いいえ、そんなことはありません。みたいな話がありますね https://t.co/mkqidkaYTb
— Masaki Hara (@qnighy) 2018年9月26日
これを知らないと、
どこも間違っていないのに何故か間違った答えのでるバグの原因が、コンパイラのundefined behavior検出によるものでした。
— ゆかたゆ (@yukata_yu) 2018年9月6日
int d[4][8]に対して
for(int n=0; n<4*8; ++n) d[0][n] = f(n);
でub吐かないで欲しいのです…。
ポインタに一旦入れたら治りました。
そりゃ完璧にUB踏んでるしなぁ。
— yoh2 (@yoh2_sdj) 2018年9月6日
それにしてもこのパターンでもおかしくなることがあるのか。今度ネタにしよう。
規格を読んだことがないのですが、
— ゆかたゆ (@yukata_yu) 2018年9月6日
このケースでd[0][8a+b]とd[a][b]が同じものを指す保証はされないのでしょうか?
メモリ配置が連続してるのは保証されてますが、 d[0]の型が int [8] である以上、d[0]を通してのアクセスはd[0]〜d[7] (アドレスを得るだけなら&d[8]も可) に限るんです。
— yoh2 (@yoh2_sdj) 2018年9月6日
みたいなことになったりするようです。私も上のやりとり見て初めてこの話を知りました(そして前述のqnighyさんのツイートで腑に落ちた)
配列とポインタ、&
と*
C言語には、「配列」と「配列の先頭要素へのポインタ」と「配列全体へのポインタ」があります。例えば、int arr[8]; int *p1 = arr;
とあるとき、arr
は配列であり、p1
は「配列の先頭要素へのポインタ」であり、&arr
は「配列全体へのポインタ」です。&p1
は「ポインタへのポインタ」ですね。int *p1 = arr;
と書けるのは、式の中では基本的に「配列」が「配列の先頭要素へのポインタ」へと勝手に読み替えられるからですね。
Cコンパイラを書いた時点で私がちゃんと分かっていなかったのは、「ポインタ&arr
のアドレス値『は』ポインタp1
のアドレス値と等しく、これを間接参照する際には
アドレスが指す先をメモリから読むって処理を飛ばさないといけない
」という話です。
やっぱりみんなこれ疑問に思いますよね
今学期(2018年秋学期)取っていた授業で、配列arr
がメモリ上のどこにあるのかを調べる際、講師がgdbでp &arr
としていました。その結果、授業のQ&Aフォーラムで次のような質問がなされることとなりました:
文字列
s
に対して、gdbでp s
したときとp &s
したときでアドレスが違うんですが、これはなぜでしょう?
これに対して私が返した解説をそのまま日本語訳すればいい説が出てきたので、ほぼそのまま和訳して載せていこうと思います。
「講義だとp &arr
で文字列のアドレスが得られたのになぜ今回はそうならないのか、ということですよね?これは配列とポインタが別物であることに由来します。Cでは、 sizeof
か&
のオペランドになっている場合を除き、配列はその先頭要素へのポインタに暗黙に変換されます。s
がchar
へのポインタである場合、s
というのはとあるアドレスを表すビットパターンであり、そのビットパターンはまたメモリのどこかに格納されています。前者がs
で後者が&s
なので、値が違うわけです。」
「s
がchar
の配列である場合は話が少し変わってきます。s
が配列で、アドレス0x7fffffffe900から始まるとしましょう。s
は配列なので、先頭要素へのポインタへと変換されます。先頭要素は0x7fffffffe900という場所にあるので、s
の値は0x7fffffffe900です。」
「では、s
がchar
の配列であるとき、&s
はどうなるでしょう?実はこれは配列全体へのポインタという意味になります。講義のビデオをよく見ると、gdbで&s
の型がchar (*)[6]
と表示されていることが分かります。これは、&s
が(ポインタへのポインタではなく)配列へのポインタであるという意味です。さて、ということで&s
は配列が格納されているアドレスですが、これも0x7fffffffe900です。ゆえにs
と&s
は同じアドレスとなる(けれども型は違う)のです。」
「では、そもそもなぜこの『配列全体へのポインタ』とかいうよく分からないものが存在するのでしょう?これは主に多次元配列というものを許容するためにあります。*2。」
「Cでは、int
の配列の配列を作ることができます。具体例として、3つの『5個のintからなる配列』からなる配列を考えましょう。これはint arr[3][5];
として宣言できます。arr
はなんかの配列なので、大体の文脈でなんかへのポインタに変換されます。変換されてできるものは何でしょう?『5個のintからなる配列』へのポインタです。(Cの構文は奇怪なので、これをint (*p)[5]
と書きます。)」
「3つの『5個のintからなる配列』からなる配列int arr[3][5];
では、intを格納する15個の『箱』全てが隣り合って存在することが保証されています。この条件の元で多次元配列を正しくサポートするという要請こそが、s
が真の配列であるときにs
と&s
のアドレス値が一致しなければならない理由です。int arr[3][5];
に対して、arr[2][4]
、つまり*(*(arr+2)+4)
が最後の箱を表すようになってほしいわけです。」
(原文ではここに*(*(arr+2)+4)
を追いかける文章が入るが、K&Rとか「C言語ポインタ完全制覇」とかに載っている図の方が分かりやすいので割愛)
「ということで、コンパイラが『配列全体へのポインタ』を間接参照するときには、実は型が変わっているだけであり、間接参照に対応するアセンブリが吐かれたりしません。このことによって、多次元配列が動いてくれるので、一貫性のため、全ての『配列全体へのポインタ』はこの性質を満たさないといけないのです。」
補足:構造体
struct A s; のとき、s.a というのは *( (char *)&s + offsetof(struct A, a) ) のことだけれど、arrが配列型なら (char *)&s + offsetof(struct A, arr) で得られるポインタは配列へのポインタなので、C言語上での間接参照はno-opにしてやらないといけない
— hsjoihs (@hsjoihs) 2018年10月25日
注: (char *)&s + offsetof(struct A, arr)
を間接参照する前に当然*(s.a)
の型にキャストして戻さないといけないが、上ツイートではその話が抜けている
最後に
今日あと4分しかないのでとても雑
hsjoihs_c_compilerの現状
この記事はセキュリティキャンプ 修了生進捗 #seccamp OB/OG Advent Calendar 2018の15日目の記事です。
概要
セキュリティ・キャンプ2018で作ってきていた自作Cコンパイラが9月4日にセルフホストを達成した後、どうなっているのかを軽く述べる。
セルフホスト達成時からあった機能
C89の演算子のうちキャスト・ sizeof 値
以外全部
キャストはvoid *
などを用いることによって代用でき、sizeof 値
はsizeof(型)
で代用できるため。あとポインタからポインタを引く場合、sizeof (*ポインタ)
が1, 2, 4, 8以外だとダメ。
goto以外の制御構文全て
正直gotoぐらいすぐ足せると思うし足したい
型: void・char・int・ポインタ・配列・構造体・enumをサポート
shortとかlong longとかunsignedとかがない。charはunsigned charだけど。
文字列リテラル(連結・エスケープシーケンスなども)
流石にこれがないとね
関数から構造体を値として返す
ただし手抜き実装なので、「小さい構造体でアラインメントが4未満」だと動かない。
その後足した機能
文字リテラル
セルフホスト時はギリギリなかった。
構造体を値としてもっと使えるように
(構造体, スカラー)
だったり(構造体 = 構造体).a
だったりフラグ ? 構造体 : 構造体
だったり。
Ubuntu対応
いつのまにかUbuntuで動かなくなっていた。gccだと-no-pieというオプションが必要になったりする、とかの対応をし、再び両方で動くように。
プリプロセッサ
とりあえず#include
(相対パスが正しく動かない)・#define
(ただし置き換え後は単一のトークンであることが必要)・#
・#ifdef
・#ifndef
・#endif
を追加したことにより、2018年10月7日にプリプロセッサ込でセルフホスト。
関数ポインタ
これで(**********printf)("Hello, World!\n");
ができる
構造体のメンバの配列の要素にアクセスすると落ちるバグの修正
「配列へのポインタ」、やはりおそろしい
ポインタからポインタを引いた時、任意のサイズで割れるように。
そもそもコード書く量を減らすための手抜きだったので。
前述の構造体アラインメント問題を修正
そもそもコード書く量を減らすための手抜きだったので。
行コメント
なんか足し忘れてた。
__func__
意外と便利。
可変長引数
作る方も呼ぶ方も対応。
中間表現のdump
--ir=
でダンプを吐けるようにした。今後IRが複数発生したら名前どうしよう。現状では、「英語の語頭文字の確率分布に従って乱数で名前を決め、あとでバクロニムする」を考えている。
現状ない機能
とはいえ、いろいろ足りない機能もまだまだある。
typedef
typedefがない。va_list
とかFILE
とかで困る。
7個以上の引数を取る関数
引数をスタックに積み残す必要があるが、その位置が正しくない。
構造体を値で関数に渡す
スタックだったりレジスタだったりする。面倒。
union
structのオフセットを全て0にすればだいたい実現できるが、自分で使ったこともないしまだ実装していない
enumの初期化子
定数式周りをきちんとやっていない
int array[] = {0, 1, 2, 3};
つらそう
複数のトークンに展開される#define
再帰を禁止したり確保するメモリが増えたりとかがめんどい。Rust使いたい。
空文
これこそ一瞬で足せるのでは。
short, long long, unsigned
暗黙の型変換とかを考えるのがとても険しい。ちなみにmalloc
とかのsize_t
はsigned int
になってしまっているが、x86-64において64bitレジスタの下32bitに書き込むと上位32bitはゼロクリアされてくれるため、レジスタで渡している限りではuint64_t
へのキャストが勝手に発生してくれる。
真面目なconst
現状const
というキーワードを読み飛ばしているだけ。
最適化
現状、x64をスタックマシンとして使っていて、そこにコンパイルする形になっている。
#アンケート
— hsjoihs (@hsjoihs) 2018年12月1日
今後の #hsjoihs_c_compiler:
ということで最適化をしていくことになっているので、やらないとなぁ。
セキュリティ・キャンプ2018参加記
セキュリティ・キャンプ2018で作ってきていた自作Cコンパイラが、9月4日にセルフホストを達成したので参加記を書いていく。
セルフホスト、達成です!(プリプロセッサはまだない) #hsjoihs_c_compilerhttps://t.co/HlCUT5Hqlg
— hsjoihs@数情物化語(@hsjoihs@mstdn.jp) (@hsjoihs) September 3, 2018
(執筆はプリプロセッサを実装してからにしようとも思っていたが、アンケートの結果「先に記事を書くべし」とのことだったので)
先にやるべきは #hsjoihs_c_compiler
— hsjoihs@数情物化語(@hsjoihs@mstdn.jp) (@hsjoihs) September 8, 2018
応募とか
sksat_tty氏とかが言及していたことをきっかけに興味を持ち始め、↓に直接後押しされる感じで「じゃあ応募してみるかなぁ」と思い始めた。なお、この段階では「思った」だけであった。
セキュキャンなりなんらかの審査があるイベント、「自分なんかだとダメだろうなあ」とか勝手に思って申込みすらしないのが一番もったいなくて、とりあえず申し込んでみるのとても大事です、これホント https://t.co/eCE2HDnNs6
— ロケットはえっち (@sksat_tty) 2018年3月21日
なんかCコンパイラゼミとかいうのが行われるらしい。Cなら趣味で仕様とか読んでるし(書いたことはあんまりないけど)楽しめそう。
というわけで僕ですら3日だけなら無謀だと思っている企画なんですが、事前にしっかりサポートしていけば大丈夫であろうという目論見のもとセキュキャンの講師をすることにしました。というわけでみっちりやりますよ。 https://t.co/WlxWZw4HF8
— Rui Ueyama (@rui314) 2018年4月24日
月日は流れ5月後半。締切も近づきTLがセキュキャンで盛り上がり始める。なお私は書き始めてすらいない。
セキュキャン、楽しそうだがまだ課題を始めていないのである(やれ)
— hsjoihs@数情物化語(@hsjoihs@mstdn.jp) (@hsjoihs) 2018年5月19日
とりあえず応募用紙を7296文字書いて出した。*1メールの履歴を見る限り、応募を提出したのは5月27日だそうだ。
そして締切がやってくる。
世界はつらく厳しいものですが、セキュキャン応募チャレンジを達成された皆様は本当にお疲れさまでした。回答を書き上げる過程だけでも、学んだことは多かったと思います。
— hikalium (@hikalium) 2018年5月28日
これから我々は真剣に選考しますので、皆様まずはゆっくりと休んでください...。#seccamp
受かった
6月14日に合否発表があった。受かっていた。感謝の気持ちにあふれるなどした。
私は(当時バタバタしていたこともあり)合格したことをツイートしそこねており(他人の合否ツイはRTしていたというのに)、数日後に「あれ、セキュキャン来るんですよね?」というDMが講師のhikalium先生から来てしまったので慌ててツイートをした。
セキュリティキャンプ全国大会2018の「Cコンパイラを自作してみよう!」ゼミに参加します。よろしくお願いします。#seccamp#seccamp2018
— hsjoihs@数情物化語(@hsjoihs@mstdn.jp) (@hsjoihs) 2018年6月18日
不安感を与えてしまって申し訳ないなどと思った。
これでセキュキャン2018 Cコンパイラコースの受講者が全員Twitter上で参加表明をしてくれたので、とても安心した。
— hikalium (@hikalium) 2018年6月18日
やっていき
6月下旬には事前学習slackが立ち上がり、
#seccamp2018 の事前学習slackに入った
— hsjoihs@数情物化語(@hsjoihs@mstdn.jp) (@hsjoihs) 2018年6月22日
7月8日から開発が始まった。なお、開発日記は↓に書いている。
セキュキャン前日
なんか天気が荒れており、雷が鳴っていたので念の為パソコンをコンセントから外しておいたりしていたところ、slackの方で「前泊だけどずぶ濡れになった」という情報が流れてきた。大変そう。
とか思ってのんきにコードを書いていたところ*2、
はりぼて自作Cコンパイラaqccでセルフコンパイルできました。楽しい。https://t.co/bOlDDuAv50 #seccamp
— 艮 鮟鱇 (@ushitora_anqou) 2018年8月13日
とかいう話がtwitterとslackに流れてきていた。つよい。ちなみに当時の私はポインタ・配列・char・文字列リテラルとかが実装されている程度の進捗であった。
あと、
セキュキャン期間中、C言語でお困りの方がいらっしゃいましたら、YトラックCコンパイラゼミにおいでください。きっと6人の賢者たちが仕様書をもちだして答えてくれます...(講師はのんびりそれを眺めるお仕事です)。
— hikalium (@hikalium) 2018年8月12日
とかいうのが流れてきたので「これ、手元の本を色々持ってっといた方がよいのでは」という気持ちになるなどした。
セキュキャン当日 (Day 1)
荷物の準備とかを慌てて始め、無事到着。
初日は集中開発コースの人々も全員共通の講義を受けるので、Cコンパイラには一切手を付けなかった。(同じテーブルにいる人に対してコンパイラのdemoとかをしたりはした。)共通講義では様々な有益情報が得られ、ありがたかった*3。
Day 2
集中開発である。とりあえずはコードを書いていく。
具体的な開発内容は前述の日記に書くとして、大まかな変更点は以下の3つ。
- 過去に発見していたものの見なかったことにしていた「関数呼び出し式の中で関数呼び出しが行われるとバグることがある」を修正した。このバグの原因究明で私が悩んでいる間、隣のほうでは「レジスタに整数演算するとセグフォする」とかいうすごい現象が起きていて、「つらそう(つらそう)」などと思った。
- 構文解析と意味解析を分離した。時間は掛かったが、わりとただの作業だったのでそこまで大変ではなかった。
- で、旧い「構文+意味解析機」と新しい「意味解析機」で生成される構文木にわずかに差がある(ので、新しい構文木でテストを走らせるととあるテストケースで無限ループになる)問題が検出された。要するに新しいほうがバグってる。経験上、デバッグは睡眠時間を削って行うと効率が下がるので、そこそこ原因が絞り込めた段階で寝ることにした。
Day 3
朝起きてデバッグ。後置++と後置--に関して意味解析器でノード付け忘れていただけだった。なるほど、それでforの第三式が吹っ飛ぶので無限ループになっていたと。
とりあえず構造体が欲しい。オフセットの計算が少々厄介という話を聞いた*4ので、これを機にそのあたりについてちゃんと勉強しようと考えた。とりあえず軽くググって出てきた「データ型のアラインメントとは何か,なぜ必要なのか?」に言及したところ、「そのサイト分かりやすかったからオススメ」と言われたので読んだ。
理解したので組んでいく。「このパターンは使わんから組まなくていいか」みたいなアドホックをやりまくっていたら泥沼にはまり、作り直すはめになった。型のスコープをちゃんと実装するのが面倒ということも鑑み、最終的に取った戦略は次の通り。
- ちゃんとtype-specifierとして
struct A {int a; int b;}
とstruct A
の両方を許す - 変数宣言/定義およびそれに準ずるものが登場した時、その宣言の中に構造体の言及があるかどうか確認。
{int a; int b;}
つきで構造体型が言及されていればメンバを登録。ただしローカルの場合は現状では未対応。{int a; int b;}
なしでの言及はローカルでも問題なく素通り。
この「登録」でstructのsizeを追加していくことで、structの配列とかをローカルやグローバルに作れるようになった。
Day 4
構造体だけあってもメンバアクセスができなければほぼ意味がない。ということで実装した。
実質今日が最終日なので、とりあえずあと一つぐらい機能を足して「ここまでできました」をやりたい。何を足そう。
そういえばvoidが一切無いな。で、voidを足せばvector.cがセルフホストできそうだ*5。足すか。
ということで「空の引数リストとしてのvoid」「void*としてのvoid」「何も返さない関数としてのvoid」を実装。スライド(↓*6)も作って発表した。
そのあとは、本を頂いたりした。私はそこそこ前に買おうかと思って結局買わないでいた 「https://www.amazon.co.jp/独自CPU開発で学ぶコンピュータのしくみ-伊藤-剛浩/dp/4798045365」などを頂いた。
Day 5
最終日。とりあえず、増えた荷物*7を処理するのが大変だった。
$ zip 荷物.zip 荷物
— callsnote (@callsnote) 2018年8月17日
をしている(7zしたい)(そもそもの容量が大きい)
様々な発表が行われた。uint256_t氏の「自作JSエンジンの、自作ブラウザ上での発表」のインパクトがとても強かった。
uint256_t氏やばすぎる(自作ブラウザで自作JavaScriptのプレゼンをするという粋な演出)。
— hikalium (@hikalium) 2018年8月18日
0日目セルフホストの人がプレゼンも2分スピーチも完璧にやっていて「すごいなぁ」となった。
一方私は発表の合間を縫ってswitch-caseを実装することを試みた。結果、caseもdefaultもないswitch *8が実装できた。
「さてセキュキャン終わったし帰るか」とか思っていたら、講師のRuiさんの「Cコンパイラ班で焼肉食べませんか、よかったらuint256_tくんもどうぞ」という提案により焼肉となった。
Rui さんたちと焼肉食べます
— uint256_t (@uint256_t) 2018年8月18日
Ruiさんごちそうさまでした。ありがとうございました。(DMに書き忘れたのでここに書いておく)
その後
なんやかんや機能を足していってセルフホストできた。やったね。
…というので締めるのも微妙なので、セキュキャン以降セルフホストまでの話をかいつまんで書いて締めとする。
フランケンコンパイル
順当に機能を足していき、8月26日には音ゲー勢とのオフ会で(人々が音ゲーをしている中、私はゲーセンの椅子に座ってコードを書いていたので)フランケンコンパイル*9に成功した。
はすじょいさんと椅子で待機してる
— 南夏 (@t_blazerona) 2018年8月26日
コンパイラのフランケンコンパイル(自身を部分的にコンパイル)が成功した。 #hsjoihs_c_compilerhttps://t.co/Tz6PIDHj4j
— hsjoihs@数情物化語(@hsjoihs@mstdn.jp) (@hsjoihs) 2018年8月26日
ソースコードに手動で手を加えることで自身のソースコードと同じ機能のアセンブリを吐くのはセキュキャン期間中にできていたけど、手を加えずにできるようになったのは比較的うれしいものがある
— hsjoihs@数情物化語(@hsjoihs@mstdn.jp) (@hsjoihs) 2018年8月26日
なお、余談だが、フランケンコンパイルに浮かれていたらあえなくバッテリー切れを起こした。
隣でブチッを見て驚いた
— 南夏 (@t_blazerona) 2018年8月26日
フランケンコンパイルのテストケース増やしてテスト回してる最中に落ちた(最近は残り30%で落ちるのでつらい) https://t.co/UbLSGpTkfu
— hsjoihs@数情物化語(@hsjoihs@mstdn.jp) (@hsjoihs) 2018年8月26日
一回復活しかけたけどゲージ真ん中で再度ブチッ
— 南夏 (@t_blazerona) 2018年8月26日
悲しいなぁ
最近パソコンのバッテリーが不安定でつらい思いをしている。
diff
先人の轍を踏まぬよう、今の段階から2世代目(clangでコンパイルした初代自作コンパイラで、自作コンパイラの一部をコンパイルしてできたバイナリ)と3世代目(2代目の自作コンパイラで、自作コンパイラの一部をコンパイルしてできたバイナリ)のdiffを取るようにしておいた。これが実はあとでかなり役立った。
先人が通ってきた「2代目と3代目が違う」というヤバを早期から検出すべく、もうテストに2代目と3代目のdiffを入れるようにした
— hsjoihs@数情物化語(@hsjoihs@mstdn.jp) (@hsjoihs) 2018年8月27日
構造体を値で返す
セキュキャンCコンパイラ班の他の人のコンパイラと私のコンパイラの大きな違いとして、私の実装では構造体を値で扱っているコードがかなり多いというのがある。ということで8月28日の段階で構造体の一括代入を実装した。
さて、「構造体を値で扱っているコードがかなり多い」ということは、構造体を値で返す関数も実装する必要がある。ところで、(私がソースコード中で使っている構造体については)x86-64 System V ABIでは2種*10に分類され、挙動が異なる。
System V ABI だと、この2ファイルをコンパイル&リンクすると正常に動いてくれるのか… pic.twitter.com/zNk8o9EV4t
— hsjoihs@数情物化語(@hsjoihs@mstdn.jp) (@hsjoihs) 2018年8月28日
ちなみに、struct F {int a; int b;}; とかのときは System V ABI ではこの互換性がありません(普通に8バイトの値を返す) https://t.co/GOf2DFD0ke
— hsjoihs@数情物化語(@hsjoihs@mstdn.jp) (@hsjoihs) 2018年8月28日
とりあえず、INTEGERという分類に属する構造体を返せるようにして、
小さめの構造体を値で返せるようにした。 #hsjoihs_c_compilerhttps://t.co/imMCjoZJfd
— hsjoihs@数情物化語(@hsjoihs@mstdn.jp) (@hsjoihs) 2018年8月29日
関数に構造体を渡すことに関しては実装しないことにして*11、
「INTEGERに属する構造体を返す関数」を呼ぶ処理を書いたら興味深いタイプのバグが出てきて、
さて「二世代目の実行中に非自明に落ちる」が出てきて、盛り上がってまいりました #hsjoihs_c_compiler
— hsjoihs@数情物化語(@hsjoihs@mstdn.jp) (@hsjoihs) 2018年9月2日
原因判明。構造体を返す関数を呼ぶ時、引数をレジスタに入れていない!!! #hsjoihs_c_compiler
— hsjoihs@数情物化語(@hsjoihs@mstdn.jp) (@hsjoihs) 2018年9月3日
MEMORYに属する構造体を返す関数の呼び出しを実装し、
MEMORYなクラスの構造体を返す関数を呼び出せるようになった。 #hsjoihs_c_compilerhttps://t.co/EkxO4GPvYy
— hsjoihs@数情物化語(@hsjoihs@mstdn.jp) (@hsjoihs) 2018年9月3日
MEMORYに属する構造体を返す関数の定義を実装し、
MEMORY_CLASSの構造体を返す関数を定義するのにも成功。さああとはセルフコンパイル(してバグが出たらデバッグ)するだけだ #hsjoihs_c_compilerhttps://t.co/lAE0SvJfez
— hsjoihs@数情物化語(@hsjoihs@mstdn.jp) (@hsjoihs) 2018年9月3日
冒頭のとおりになるという話である。
セルフホスト、達成です!(プリプロセッサはまだない) #hsjoihs_c_compilerhttps://t.co/HlCUT5Hqlg
— hsjoihs@数情物化語(@hsjoihs@mstdn.jp) (@hsjoihs) September 3, 2018
*1:応募用紙はまた別の機会に全文公開する
*2:荷物の準備、とかではないことに注目
*3:講義の内容にどこまで言及していいのかが分からないので、何も言及しないでおく
*4:「int, char, char, intって12バイトなんですよ。『intが4バイトだから全部4バイトに合わせて、intが4バイト、charが4バイト、charが4バイト、intが4バイト』みたいに勘違いすると間違える」みたいな話がされていた
*5:constとか無いからそれは手で取り除かないと動かないけど
*6:ただしセキュキャン時とのdiffあり
*7:様々なものを頂けたのはありがたいことであって、どちらかというと私の用意したスーツケースが妙に小さかったことが主問題であった
*8:「それはswitchではない」と言われた。それはそう
*9:コンパイラの一部を自分のコンパイラで、残りをclangでコンパイルすること
*10:詳しいことは https://www.uclibc.org/docs/psABI-x86_64.pdfに書いてある
*11:まず、MEMORYに属する構造体はスタックを使わなければならない。現状引数をスタックで渡す処理は実装していないので後回しにしたい。また、INTEGERに属する構造体はレジスタを使って渡すのだが、ソースコード上の引数と消費するレジスタ数が異なってくるとか面倒なので、こっちも後回しにしたい。