###### tags: `授業` `情報工学実験II` # 情報工学実験B: LLVMを用いた簡単なコンパイラの作成 ## 実験の目的 極めて小規模な(とはいえ十分に汎用的な)C言語サブセットのコンパイラをLLVMコンパイラ基盤とC言語を用いて実現することで - [x] コンパイラが原始言語を対象(目的)言語に翻訳する仕組みを具体的に理解する - [x] 広く利用されているLLVMコンパイラ基盤の中間言語の初歩を理解する - [x] C言語についてよりよく理解し使いこなせるようになる ## 全体の構成 本実験で作成するのはC言語の小さなサブセット(以下,**Pico-C**)のコンパイラである。 中間言語として**LLVMコンパイラ基盤**の中間言語(**LLVM-IR**)を用いる。この実験の主要な対象はLLVM-IRを出力するまでの工程(**フロントエンド**(の一部))である。LLVM-IRを実際のハードウェアの目的言語に翻訳する工程(**バックエンド**)はLLVMに対応したC言語コンパイラである**clang**(クラン)をはじめとする既存のLLVMのツールチェーンを使用する。 ![実験IIの流れ](https://hackmd.io/_uploads/r1LjJblCC.svg) 実験1では実際に様々なツールをシェル上でパイプして実行しながらこのコンパイルの流れ作業を理解する。コンパイラフロントエンド`picoc`のソースコードの本体はファイル`picoc.c`で,実験2(LLVM-IR)が済むと理解できるようになる。実験3ではこのファイルにコードを追加・変更しフロントエンドの機能を強化する。 **字句解析器**(scan.c)や**ハッシュ表**(hashmap.c)のソースコードは別のファイルに分離してあり、今回の実験ではこの内容を理解・変更する必要は一切ない。 ソースコードはすべてC99(ISO9899:1999, JIS X3010:2003) / C11(ISO9899:2011)に準拠したC言語の基本的な機能と標準ライブラリの関数だけを用い,理解し易さを重視して平易に記述されている。 ## 実験環境の準備(事前に必ず準備・確認しておくこと) 実験にはWindow11のインストールされた各自のラップトップ(ノート)PCを用いる。**事前に以下の環境を準備・確認**しておくこと。 :::danger 本実験資料は入学時に大学で指定された仕様を満たすPC環境、つまり`amd64`/`x86_64`アーキテクチャ(Intel/AMDの64bit CPU)のWindows 11 PCを前提にしている。Windows 10は2025年10月14日にサポートが終了するので本実験でもサポートしない。 自力でサポート外の`aarch64`/`arm64`アーキテクチャの環境を用いる人のための非公式の手引きは[こちら](https://hackmd.io/@okuisatoshi/HkvoExoiA) ::: OS : [WSL2](https://learn.microsoft.com/ja-jp/training/modules/wsl-introduction/?source=recommendations)(Windows Sybsystem for Linux version 2)上のUbuntu Linuxの**24.04 LTS**を用いる。他の授業で24.04を既にインストール済みの人はそのまま使えばよい。現在用いているバージョンが古い人は,[公式解説](https://learn.microsoft.com/ja-jp/windows/wsl/install)を参考に24.04をインストールすること(既に使用しているディストリビューションと共存可能)。基本的には管理者権限で起動したPowerShellで以下のコマンドを実行し,ユーザ名とパスワードを設定するだけでよいはず。 ``` wsl --install -d Ubuntu-24.04 ``` :::info どうしてもローカルのPCにUbuntu 24.04 LTS以降の環境を用意できない人は、クラウドにUbuntuサーバーを用意してSSHでリモート接続する方法もある。例:[ConoHa VPS](https://conoha.jp/vps/) ::: ターミナル(端末) : 実験の作業は専らターミナル(正確にはターミナル・エミュレータ)からコマンドを投入して(つまりCLI操作で)おこなう。Windows 11に標準でインストールされている**Windowsターミナル**を推奨する。もしなければStoreからインストールできる。あるいは,PowerShellで[winget](https://learn.microsoft.com/ja-jp/windows/package-manager/winget/)コマンドを用いるとより簡単にインストールできる。 ``` winget update -e --id Microsoft.WindowsTerminal ``` ::: danger Linuxでシェル(bash)のコマンド操作ができない人は,実験開始までに1年次春学期の「情報処理演習」の[教科書](https://honto.jp/ebook/pd-contents_0626682446.html)の下記の部分を復習し,よく理解しておくこと。 - 4〜6章(ファイルとディレクトリ(フォルダ)の基本操作に関すること) - 8章(ページャ(less)に関すること) - 18、19章(リダイレクトとパイプに関すること) 参考までに,2024年度の情報処理演習(午後クラス)の資料は[こちら](https://hackmd.io/@okuisatoshi/Sy8dsBbpC)。 ::: 実験で用いる各種ツール : 本実験で用いるCコンパイラ、LLVMツールチェーン等はすべてUbuntuの`apt`コマンドを用いて以下のようにパッケージでインストールできる。 ``` sudo apt update && sudo apt install -y \ curl git gcc make cgdb tree nkf bat \ clang clangd llvm lld libclang-rt-dev-wasm32 wasi-libc \ g++-aarch64-linux-gnu qemu-user \ graphviz wl-clipboard libsixel-bin ``` テキストエディタ : コードの編集作業に用いるテキストエディタを準備しておくこと。C言語の授業等で普段使いのテキストエディタ(`vim`, `emacs`, `nano`, `micro`など)がある人はUbuntuにインストールしてそれを使い続ければよい。エディタを使い慣れない人(というのが情報工学科の3年次にいるのか疑問だが)には最近マイクロソフトが公開した[Edit](https://forest.watch.impress.co.jp/docs/news/2018261.html)がある。機能は最小限で誰でも直感的に使える。PowerShellで ``` winget install Microsoft.Edit ``` とすれば簡単にインストールできる(近い将来Windows11に標準でインストールされる予定)。WSLのUbuntuから起動するにはコマンド ``` edit.exe ``` を実行する。 Editより高機能なエディタを求める人には、マイクロソフトのVisual Studio Code (以下,VSCode)がある([公式解説](https://learn.microsoft.com/ja-jp/windows/wsl/tutorials/wsl-vscode))。VSCodeはStoreからインストールできる。あるいは,PowerShellで[winget](https://learn.microsoft.com/ja-jp/windows/package-manager/winget/)コマンドを用いるとより簡単に導入できる。 ``` winget install -e --id Microsoft.VisualStudioCode ``` 上記のVSCodeはWindows版だが,WSLと連携する機能があるのでUbuntuからも簡単に起動できる。ただしそのためには拡張機能のインストールが必要。WSLが既に有効になっているとVSCodeを起動したときに導入を促す通知が表示されるのでそれに従えばよい。あるいは,PowerShellで以下のコマンドを実行するとより簡単確実に導入できる。 ``` code --install-extension ms-vscode-remote.remote-wsl ``` もしよくわからないときは上記の公式解説を[参照](https://learn.microsoft.com/ja-jp/windows/wsl/tutorials/wsl-vscode#install-vs-code-and-the-wsl-extension)。 VSCodeと拡張機能が正しくインストールされていれば,WSLのUbuntuからコマンド ``` code . ``` でVSCodeが起動する。 **[追記]** 2025-10-16にv0.208.4で[Windowsに正式対応](https://zed.dev/blog/zed-for-windows-is-here)したばかりのZedも動作が非常に高速かつ高機能でおすすめできる。Linux版をWSLのUbuntuにインストールするのではなく,[Windows版をダウンロード](https://zed.dev/releases/stable)してインストールすること(WSLからの起動にも標準で対応している)。 WSLのUbuntuから起動するにはコマンド ``` zed ファイル名 ``` を実行する。初回起動時にVimモードを選べば,普段Vimを使っている人にも使いやすい。また,clangdがインストールされていればそれを自動的に使うので,C言語のソースコードの編集が便利になる。 ### 作業を効率化するための事前設定 Ubuntuで`~/.profile`に以下の設定を追記しておくと実験の作業が格段に効率化できる。詳しくはこの資料の最後の付録2,3を参照。 ``` alias bat='batcat --theme Nord' function open() { explorer.exe $(wslpath -w "$1") } alias pbcopy='(nkf -Lw | wl-copy)' alias pbpaste='(wl-paste | nkf -Lu)' alias pbrun='echo "$(pbpaste)"; read -p "y/n?: " c; [[ "$c" == y ]] && . <(pbpaste)' # 以下はWSL 2.5.7以前のバグへの対応: if [ ! -S "$XDG_RUNTIME_DIR/wayland-0" ]; then ln -s /mnt/wslg/runtime-dir/wayland-0* "$XDG_RUNTIME_DIR" fi ``` 設定は ``` . ~/.profile ``` を実行して即時有効にするか,あるいは次回WSL2のUbuntu起動時から有効になる。 ## 実験で用いるコード一式のダウンロード `git`コマンドを用いてダウンロードする。 ``` git clone https://github.com/okuisatoshi/jikkenB.git ``` カレントディレクトリに`jikkenB`というディレクトリがつくられる。このディレクトリ名は自由に変更してよい。実験の作業はすべてこのディレクトリに移動しておこなう。 ``` cd jikkenB ``` ## 実験1. コンパイルの流れ シェルのパイプを用いてソースコードが実行ファイルにコンパイルされる各段階を確認する。また,中間言語の意義,クロスコンパイルと最適化の効果について理解する。 ### コンパイルの過程 Pico-Cコンパイラ`picoc`の構築 ``` make ``` フィボナッチ数列を求めるPico-Cソースコード(C言語のサブセット) ``` cat example/fib.pc ``` 中間言語(LLVM-IR)への翻訳 ``` cat example/fib.pc | ./picoc ``` さらにLLVMのバックエンド`llc`を用いて`x86_64`のアセンブリ言語に翻訳 ``` cat example/fib.pc \ | ./picoc \ | llc ``` :::info `llc --version`で表示される様々なターゲットに対応している。 例: ``` llc -march aarch64 # 64bit ARMのアセンブリを出力 llc -march wasm32 # 32bitのwebアセンブリを出力 ``` ::: さらに`x86_64`のアセンブラ(`x86_64-linux-gnu-as`あるいは短く`as`)を用いて`x86_64`のオブジェクトファイル`fib.o`を生成 ``` cat example/fib.pc \ | ./picoc \ | llc \ | as -o fib.o ``` さらにリンカー(`x86_64-linux-gnu-ld`あるいは短く`ld`)を用いてランタイムルーチンやライブラリを結合して最終的に`x86_64`の実行可能ファイル(バイナリ)`fib`を生成。手動でリンカーを呼び出すのは面倒なので`clang`コンパイラを経由して間接的に`ld`を呼び出す。 ``` clang -o fib fib.o -static ``` :::info `-static`はライブラリを静的にリンクして単一バイナリーを生成するリンカーオプション(エミュレータで実行する時など単一バイナリーのほうが扱いが易しいため)。 オプション`-v`を付けると`clang`が内部でリンカーを呼び出す様子が確認できる。 ::: `x86_64`マシン上でネイティブ実行 ``` ./fib ``` ### 最適化 最適化器(オプティマイザ)`opt`を用いて中間言語を**最適化**する段階を挿入する。`example/fib.pc`は計算が速すぎて効果が分かりずらいので,代わりに2,147,483,647回ループする`example/loop.pc`で試す。 ``` cat example/loop.pc \ | ./picoc \ | llc \ | as -o loop.o \ ; clang -o loop loop.o -static ``` これに`opt -S -O2`をパイプに挿入するとLLVM-IRのコードが最適化され実行ファイルの実行速度が著しく高速化される。 ``` cat example/loop.pc \ | ./picoc \ | opt -S -O2 \ | llc \ | as -o loop.o \ ; clang -o loop loop.o -static ``` :::info `-O2`は中程度の最適化を有効にするオプション。`-S`はビットコードではなく人間に読めるテキスト形式でLLVM-IRを出力する指定。逆に最適化を無効化するには`-O0`を指定すればよい。 ::: 実際, ``` cat example/loop.pc \ | ./picoc \ | opt -S -O2 ``` の出力をみるとループが消え,2147483647を出力するだけのコードに最適化されていることが確認できる。 ### LLVMバックエンドとしてのclang / lli `clang`はLLVMコンパイラ基盤を用いて作られたコンパイラなので,実はLLVM-IRのコードも受け付ける。Pico-Cのコードを実行可能ファイルにコンパイルするには,単に`picoc`と`clang`をパイプして以下のようにするだけでよい。 ``` cat example/fib.pc \ | ./picoc \ | clang -Wno-override-module -x ir - -O2 -o fib -static ``` 以下の課題1の作業ではこの方法を用いるとよい。 :::info `-`は入力を標準入力ストリームから受け付けることを、`-x ir`はそれがLLVM-IRのコードであることを示している。`-Wno-override-module`はターゲット組の上書き警告の出力抑制(警告表示が気にならなければ付けなくてもOK)。最適化オプション`-O2`もここで与えられる。 ::: あるいはバックエンドに中間言語LLVM-IRの**インタプリタ**`lli`を用いて直ちに解釈実行することもできる(実行可能ファイルは生成されない)。実験2ではこの方法が便利である。 ``` cat example/fib.pc | ./picoc | lli ``` ### クロスコンパイル ここまでは`x86_64`アーキテクチャのマシン上で`x86_64`向けのバイナリを生成してきた(いわゆる**ネイティブコンパイル**)が,ターゲットを指定して他のアーキテクチャのマシン向けのバイナリを生成することもできる(**クロスコンパイル**)。 64bit ARM([`aarch64`](https://stackoverflow.com/questions/31851611/differences-between-arm64-and-aarch64))用にクロスコンパイル ``` cat example/fib.pc \ | ./picoc \ | clang -Wno-override-module \ -x ir - -O2 -o fib.aarch64 -static \ -target aarch64-linux-gnu ``` ::: info `-target aarch64-linux-gnu`で64bit ARMアーキテクチャのLinux OSマシン用にコンパイルするよう指定している。 ::: `aarch64`用の実行可能ファイルであることを確認 ```bash file fib.aarch64 # (出力) # fib.aarch64: ELF 64-bit LSB executable, ARM aarch64, version 1 (GNU/Linux), statically linked, (以下略) ``` エミュレータ`qemu-aarch64`を用いて`x86_64`マシン上で`aarch64`用の実行可能ファイルをエミュレーション実行 ``` qemu-aarch64 fib.aarch64 ``` ::: warning Linuxには実行可能ファイルのフォーマットを認識して`qemu-aarch64`等を呼び出すbinfmt_miscという仕組みがあるので,設定次第で単に以下でも実行できるが,混同・誤解を避けるために上記のように明示的に`qemu-aarch64`コマンドを用いることをすすめる。 ``` ./fib.aarch64 ``` ::: ソフトウェアでエミュレートするので,当然ながらネイティブ実行よりも実行が遅い。`fib.aarch64`はライブラリを静的にリンクした単一バイナリなので,このファイルを`aarch64`マシン(例:Raspberry Pi 3以降や,ARM CPUのMacやWindows PC)上のLinuxに移してネイティブ実行すればもっと高速になる(該当のマシンを持っている人はどのくらい速くなるか試してみるとよい)。 ターゲットを`wasm32-wasi`にしてWebAssemblyを出力することもできる. ``` cat example/fib.pc \ | ./picoc \ | clang -Wno-override-module \ -x ir - -O2 -o dist/fib.wasm -static \ -target wasm32-wasi ``` ``` file dist/fib.wasm # (出力) # fib.wasm: WebAssembly (wasm) binary module version 0x1 (MVP) ``` WebAssemblyの実行環境であるwasmerをダウンロードし ``` make wasmer ``` これを用いて実行する。 ``` ~/.wasmer/bin/wasmer dist/fib.wasm ``` 次にwebブラウザでの実行を確認する。 ``` (cd dist; python3 server.py) ``` でwebサーバを起動し,webブラウザで`localhost:8000/`にアクセスすると`fib.wasm`の実行結果が表示される。 :::success ### 課題1(ネイティブ実行とエミュレーション実行の実行時間比較) コード`example/bench.pc`を`picoc`でLLVM-IRにコンパイルし,さらに 1. `clang`で`x86_64`用にコンパイルした実行可能ファイルを`x86_64`マシン上で実行した場合 2. `clang`で`aarch64`用にクロスコンパイルした実行可能ファイルを`qemu-aarch64`を用いて実行した場合 3. `clang`で`wasm32`用にクロスコンパイルした実行可能ファイルを`wasmer`を用いて実行した場合 で,実行可能ファイルの実行に要する時間を計測し比較せよ。それぞれ最適化を有効にして(`-O2`)生成した実行可能ファイルと無効にして(`-O0`)生成したものの両方についておこなえ。 レポートには**実験環境(ハードウェア/ソフトウェアとも)と実際におこなった作業・手順を客観的,正確,かつなるべく簡潔に記述**し、読者が実験を再試可能なようにせよ。また、結果は比較しやすいように表とグラフにまとめ,比較の結果について考察を付せ。 ### 実験上の注意点 実行時間の計測には`time`(`/usr/bin/time`)コマンドを用いるのが簡単である。**実時間**(経過時間、あるいは壁時間とも呼ばれる)と**ユーザCPU時間**を計測せよ.以下は一例(詳しくは`man 1 time`でマニュアルを参照): ``` \time -f "real=%e[s] user=%U[s] (%P)" 計測したいコマンド ``` 先頭のバックスラッシュ(`\`)はbashの内部コマンド版の`time`コマンドと区別するため。bashの内部コマンドの`time`でも機能は少ないが同様な計測が可能。 >考察のヒント:実時間がユーザCPU時間より長くなることはあるか?逆に実時間のほうが短くなることはあるか?その場合、何故か? 実行毎に計測値は異なるはずである。**10回試行した中の**[**最良(最小)の値**を採れ](https://stackoverflow.com/questions/43939345/should-we-measure-the-average-or-the-minimum-execution-time-of-a-routine)。 計測で得られた生データのすべてをレポートに記載する必要はないが,生データはすべて記録し、必要に応じて後で確認できるように整理した上で保存、提出せよ。 また,ターミナルでの作業は正確に記録しておくことが客観的で正確なレポートを作成するのに重要である。実験がうまくいかないときに作業を客観的に振り返り原因を探ったり,他の人や教員・TAのアドバイスを求めるとき等にも有用である。`asciinema`を用いるとターミナルでの作業の正確な記録を簡単に残すことができる。`asciinema`の記録ファイルは提出を求める。 ::: :::info ### `asciinema`の簡単な使い方 **asciinema 3.0のダウンロード** ``` make asciinema ``` **収録** ``` ./asciinema rec -i 1 -c 'bash -l' a.cast ``` カレントディレクトリにファイル`a.cast`が作られ作業が収録される。作業の無い経過時間(アイドリング)は1秒以内に切り詰められる(`-i 1`)。収録を終了するには`ctrl-d`。 既存の`a.cast`に追記で収録するには`--append`をつける。 ``` ./asciinema rec -i 1 -c 'bash -l' --append a.cast ``` 複数のファイルに分けて収録し、後述の`ascinema cat`で後から連結してもよい。 **再生** ``` ./asciinema play -s 5 a.cast ``` で記録`a.cast`が(5倍速で)再生される。スペースキーで再生を一時停止・再開。再生中止は`ctrl-c`。 URLを指定してサーバにアップロードされている記録(後述)も再生できる。 ``` ./asciinema play https://asciinema.org/a/xxxyyyzzz ``` **連結** ``` ./asciinema cat part1.cast part2.cast part3.cast > one.cast ``` で`part1.cast`,`part2.cast`,`part3.cast`をこの順に連結してひとつのファイル`one.cast`にする。 **asciinema.orgへのアップロード(公開)** ``` ./asciinema upload a.cast ``` で`a.cast`をasciinema.orgへアップロードするとURLを知っている人なら誰でも再生できるようになる(1週間で消去される)。後でアクセスするには表示されるURL(例:`https://asciinema.org/a/xxxyyyzzz`)が必要。 [asciinemaの公式マニュアル](https://docs.asciinema.org/manual/cli/quick-start/) ::: ## 実験2. LLVM中間言語 現代の多くのコンパイラで用いられる[LLVM中間言語(LLVM-IR)](https://llvm.org/docs/LangRef.html)の基本を理解し,簡単なC言語のコードを手作業でLLVM-IRに翻訳できるようになる。 ### 初めてのLLVM-IRコード ```llvm define i32 @main() { ret i32 123 } ``` これはC言語の ```cpp int main() { return 123; } ``` に相当する。**ret命令**は関数から呼び出し元に復帰する命令である。`i32`はret命令の引数123の型が32ビットの符号付き整数であることを示している。`@main`の前の`i32`も同様で、@main関数の返り値の型を表している。 :::info LLVM-IRではグローバルな識別子には`@`をつけ、ローカルな識別子には`%`をつける。 ::: #### LLVM-IRのコードの実行 LLVM-IRのコードを実行確認するときは実験1で学んだインタープリタ`lli`を用いるのが簡便である。ファイル`a.ll`にLLVM-IRのコードが保存されているとすると ``` lli a.ll ``` のようにして解釈実行できる。実験1でみたように`lli`は標準入力を受け付けるので,以下のようにすればいちいちファイルに保存しなくて済む。 ``` lli # LLVM-IRのコードをコピペしてctrl-d ``` :::info この資料の事前準備をしておいた人は以下のようにすればコピペする手間すらいらない。 ``` pbpaste | lli ``` ::: OSに`123`が戻されたことは以下のように確認できる。 ``` echo $? # 123と表示 ``` ### 式の計算 LLVM-IRでは**式は書けないので分解して計算したい順序通り並べる**必要がある(これがLLVM-IRと高級言語との一番の大きな違いである)。よって、例えばC言語の ```cpp int main() { return 1+2*3; } ``` に相当するLLVM-IRコードは ```llvm define i32 @main() { ret i32 1+2*3 ; 間違い! } ``` ではなく ```llvm define i32 @main() { %t.1 = mul i32 2, 3 %t.2 = add i32 1, %t.1 ret i32 %t.2 } ``` のようになる。 [ブラウザで実行してみる](https://godbolt.org/z/81dfrxono) :::info コンパイラの授業で出てきた**4つ組(3アドレスコード)** と同じだと思えばよい。 ::: #### 仮想レジスタ(一時的な値の保持) `%t.1`, `%t.2`は計算の途中の値に付けられた一時的な名前(**仮想レジスタ**を表すローカル識別子)である。変数とは異なり**値の変更はできない**。上のコードを ```llvm ; このコードは誤り define i32 @main() { %t = mul i32 2, 3 %t = add i32 1, %t ret i32 %t } ``` のようにするのは間違いである(`%t`の値を再設定して変更しているから)。必ず他と重複しない新しい名前を用いる必要がある。`%`で始まっていればどんな名前でもよいが、以下では原則`%t.`に接尾辞として数字を付加して新しい名前をつくりだすことにする。 #### 算術演算命令 `add` (+), `mul` (*)以外にも`sub` (-), `sdiv` (/), `srem` (%)などの命令が用意されている。 ### printf()の呼び出し LLVM-IRはC言語の関数を呼び出すことが可能である。C言語の標準ライブラリにある`printf()`関数を呼び出して標準出力ストリームに整数`1234567`を出力するコードは以下のようになる。 ```llvm declare i32 @printf(ptr, ...) @fmt = constant [4 x i8] c"%d\0A\00" define i32 @main() { %t.1 = call i32 (ptr, ...) @printf(ptr @fmt, i32 1234567) ret i32 0 } ``` [ブラウザで実行してみる](https://godbolt.org/z/qaGcMKTbb) これはC言語のコード ```cpp extern int printf (const char *, ...); const char fmt[4] = "%d\n"; int main() { printf(fmt, 1234567); return 0; } ``` にほぼ相当する。 LLVM-IRのコードの1行目は外部の関数`printf()`のプロトタイプ(引数や返り値の型等)を宣言している。`printf()`は可変引数なので第2引数以降は`...`になっている。2行目ではフォーマット文字列"%d\n"を用意している。6行目の**call命令**は関数を呼び出す命令である。 ### 変数(ローカル変数) ローカル変数を(スタックに)割り当てるには **`alloca`命令**を用いる。 ```llvm %t.1 = alloca i32 ``` は32ビット整数型のローカル変数をスタックに割り当てており、C言語の変数宣言 ```cpp int x; ``` に相当する。`%t.1`は変数`x`へのポインタ(変数`x`が割り当てられている最初のアドレス)を表す。割り当てた変数への読み書きはこのポインタ(アドレス)を経由して行なわれる。 LLVM-IRコードの可読性を高めるために、以降では **`alloca`の値の一時名には宣言したい変数の名前の最初に%を補ったものを使う**約束にする(また,後のコンパイラ作成も簡単になる)。 ```llvm %x = alloca i32 ``` 変数に値を格納するには **`store`命令**を用いる。以下の ```llvm %x = alloca i32 store i32 123, ptr %x ``` はC言語の ```cpp int x; x = 123; ``` に相当する。 逆に、変数に格納されている値を読み出すには **`load`命令**を用いる。上記の変数`x`の値を読み出すには ```llvm %t.1 = load i32, ptr %x ``` とする。%t.1が読み出した値を表している。 alloca, load, storeを用いたまとまった例:C言語の ```cpp int main() { int x = 2; int y = x * 2; x = x + 1; return x + y; } ``` に相当するLLVM-IRのコードは以下のようになる。 ```llvm define i32 @main() { ; int x = 2; %x = alloca i32 store i32 2, ptr %x ; int y = x * 2; %y = alloca i32 %t.1 = load i32, ptr %x %t.2 = mul i32 %t.1, 2 store i32 %t.2, ptr %y ; x = x + 1; %t.3 = load i32, ptr %x %t.4 = add i32 %t.3, 1 store i32 %t.4, ptr %x ; return x + y; %t.5 = load i32, ptr %x %t.6 = load i32, ptr %y %t.7 = add i32 %t.5, %t.6 ret i32 %t.7 } ``` [ブラウザで実行してみる](https://godbolt.org/z/8493a5TTj) ### scanf()の呼び出し 以下のコードでは,`scanf()`を呼び出して標準入力ストリームから入力した整数値を変数`x`に格納している。 ```llvm declare i32 @scanf(ptr, ...) @fmt = constant [3 x i8] c"%d\00" define i32 @main() { %x = alloca i32; %t.1 = call i32 (ptr, ...) @scanf(ptr @fmt, ptr %x) %t.2 = load i32, ptr %x; ret i32 %t.2 } ``` これはC言語のコード ```cpp extern int scanf (const char *, ...); const char fmt[3] = "%d"; int main() { int x; scanf(fmt, &x); return x; } ``` にほぼ相当する。 `scanf()`も標準入力からデータを読み取るので,LLVM-IRのコードをコピペして`ctrl-d`したあとで`123`を入力する。 ``` lli; echo $? # コードをコピペしてctrl-d # 123を入力 # 123が出力 ``` もちろん一旦ファイル(例:`a.ll`)に保存しておいて引数として`lli`に与えてもよい。 ``` echo 123 | lli a.ll ; echo $? ``` ### 制御構造 LLVM-IRと高級言語のもうひとつの大きな違いは、前者には後者のような構造化された制御構造が用意されていないことである。代わりにC言語のgoto文に相当する **`br`命令**(分岐命令)を用いて任意の制御構造を実現する。 #### 例:while文を含むコードの翻訳 C言語のコード ```cpp #include <stdio.h> int main() { int n = 10; while (n) { printf("%d\n", n); n = n - 1; } return 0; } ``` をフローチャートで表せば ```flow begin=>start: 開始 end=>end: 終了 L0=>operation: int n=10; L1=>condition: L1: nが0でない L2=>operation: L2: printf("%d\n", n); n = n - 1; L3=>operation: L3: return 0; begin->L0 L0->L1 L1(yes@はい)->L2 L1(no@いいえ)->L3 L2(left)->L1 L3->end ``` これはフローチャートの矢印に相当する**goto文**と矢印の指すボックスに付けられた**ラベル**(`L1`〜`L3`)を用いて以下のように表せる。 ```cpp #include <stdio.h> int main() { int n = 10; goto L1; L1: // while冒頭 if (n) goto L2; else goto L3; L2: // while本体 printf("%d\n", n); n = n - 1; goto L1; L3: // while直後 return 0; } ``` 元のコードにあった`while`文はなくなっている。また`if`文は`goto`文と組み合わされた ```cpp if (式) goto ラベル; else goto ラベル; ``` という限定された形でのみ出現する。これを以下では**if-goto文**と呼ぶ。 goto文 ``` goto <ラベル>; ``` は ```llvm br label %<ラベル> ``` に翻訳される。一方、if-goto文 ``` if (式) goto <ラベル1> else goto <ラベル2> ``` は`式`の値を`a` (`a`は仮想レジスタや整数)とすると ```llvm %t.1 = icmp ne a, 0 br i1 %t.1, label %<ラベル1>, label %<ラベル2> ``` のように翻訳される。ここで**icmp命令**は`a != 0`なら(1ビット整数の)1を,そうでなければ0を返す。次の行の`br`命令はこの`%t.1`の値をみて,1(真)なら`%<ラベル1>`へ0(偽)なら`%<ラベル2>`へと条件分岐する。 `icmp`命令には`ne` (!=)の他にも`eq` (==),`slt` (<), `sgt` (>), `sle` (<=), `sge` (>=)などが指定できる。よって上記は`eq`を用いて ```llvm %t.1 = icmp eq a, 0 br i1 %t.1, label %<ラベル2>, label %<ラベル1> ``` でもよい(ラベルの順序が交換されているのに注意)。 このようにして最終的にコード全体は以下のように翻訳される。 ```llvm declare i32 @printf(ptr, ...) @fmt = constant [4 x i8] c"%d\0A\00" define i32 @main() { ; 開始(ラベル省略) %n = alloca i32 store i32 10, ptr %n br label %L.1 L.1: ; while冒頭 %t.1 = load i32, ptr %n %t.2 = icmp eq i32 %t.1, 0 br i1 %t.2, label %L.3, label %L.2 L.2: ; while本体 %t.3 = call i32 (ptr, ...) @printf(ptr @fmt, i32 %t.1) %t.4 = sub i32 %t.1, 1 store i32 %t.4, ptr %n br label %L.1 L.3: ; while直後 ret i32 0 } ``` [ブラウザで実行してみる](https://godbolt.org/z/1jPqo1Gnj) ### 制御フローグラフ ```graphviz digraph "CFG for 'main' function" { graph [bb="0,0,571.5,341"]; node [label="\N"]; node [fontsize=13]; Node0x5e58596594a0 [color="#3d50c3ff", fillcolor="#d6524470", fontname=Courier, height=1.0694, label="{0:\l| %n = alloca i32\l store i32 10, ptr %n\l br label %L.1\l}", pos="264.5,302.5", rects="169.5,317.5,359.5,340.5 169.5,264.5,359.5,317.5", shape=record, width=2.6389]; Node0x5e585965a5e0 [color="#b70d28ff", fillcolor="#b70d2870", fontname=Courier, height=1.3889, label="{L.1:\l| %t.1 = load i32, ptr %n\l %t.2 = icmp eq i32 %t.1, 0\l br i1 %t.2, label %L.3, label %L.2\l|{<s0>T|<s1>F}}", pos="264.5,178", rects="112,204.5,417,227.5 112,151.5,417,204.5 112,128.5,264,151.5 264,128.5,417,151.5", shape=record, width=4.2361]; Node0x5e58596594a0 -> Node0x5e585965a5e0 [pos="e,264.5,227.73 264.5,264.41 264.5,256.02 264.5,246.91 264.5,237.87", tooltip="0 -> L.1\nProbability 100.00%"]; Node0x5e585965ac20 [color="#3d50c3ff", fillcolor="#d6524470", fontname=Courier, height=0.65278, label="{L.3:\l| ret i32 0\l}", pos="45.5,46", rects="0,46,91,69 0,23,91,46", shape=record, width=1.2639]; Node0x5e585965a5e0:s0 -> Node0x5e585965ac20 [pos="e,50.551,69.04 110.5,140 79.933,140 62.54,105.83 53.581,78.914", tooltip="L.1 -> L.3\nProbability 3.12%"]; Node0x5e585965ac90 [color="#b70d28ff", fillcolor="#b70d2870", fontname=Courier, height=1.2778, label="{L.2:\l| %t.3 = call i32 (ptr, ...) @printf(ptr @fmt, i32 %t.1)\l %t.4 = sub i32 %t.1, 1\l store i32 %t.4, ptr %n\l br label %\ L.1\l}", pos="340.5,46", rects="109.5,68.5,571.5,91.5 109.5,0.5,571.5,68.5", shape=record, width=6.4167]; Node0x5e585965a5e0:s1 -> Node0x5e585965ac90 [pos="e,334.55,91.563 340.5,128 338.38,119.51 336.76,110.52 335.63,101.74", tooltip="L.1 -> L.2\nProbability 96.88%"]; Node0x5e585965ac90 -> Node0x5e585965a5e0 [pos="e,340.61,127.58 346.44,91.7 345.72,100.08 344.51,108.93 342.82,117.57", tooltip="L.2 -> L.1\nProbability 100.00%"]; } ``` 上記のような**制御フローグラフ**(CFG)を用いるとLLVM-IRのコードの流れが分かりやすく可視化できる。CFGはLLVMの`opt`コマンドとGraphvizの`dot`コマンドを用いれば簡単に描ける。例えばファイル`example/prime.pc`の`main`関数の内容をCFGで描き画像ファイル`prime.pdf`として出力するには以下のようにする。 ``` cat example/prime.pc \ | ./picoc \ | opt -passes=dot-cfg >&- \ ; dot -T pdf .main.dot >prime.pdf \ ; rm -f .main.dot ``` シェルスクリプトを用意したので例えば ``` cat example/prime.pc \ | ./picoc \ | ./misc/cfg.sh pdf >prime.pdf ``` のようにするだけでよい。 ```graphviz digraph "CFG for 'main' function" { graph [bb="0,0,1142.5,1184"]; node [label="\N"]; node [fontsize=13]; Node0xb553524315e0 [color="#3d50c3ff", fillcolor="#f6a38570", fontname=Courier, height=2.5278, label="{0:\l| %n = alloca i32\l %c = alloca i32\l %i = alloca i32\l %j = alloca i32\l %k = alloca i32\l store i32 3, ptr %n\l store \ i32 0, ptr %c\l %t.3 = load i32, ptr %n\l store i32 %t.3, ptr %i\l br label %L.1\l}", pos="573.5,1093", rects="466,1160.5,681,1183.5 466,1002.5,681,1160.5", shape=record, width=2.9861]; Node0xb55352432f40 [color="#3d50c3ff", fillcolor="#ec7f6370", fontname=Courier, height=1.8056, label="{L.1:\l| %t.4 = load i32, ptr %i\l %t.6 = icmp sgt i32 %t.4, 0\l %t.7 = zext i1 %t.6 to i32\l %t.8 = icmp eq i32 %t.7, 0\l \ br i1 %t.8, label %L.3, label %L.2\l|{<s0>T|<s1>F}}", pos="573.5,901", rects="421,942.5,726,965.5 421,859.5,726,942.5 421,836.5,573,859.5 573,836.5,726,859.5", shape=record, width=4.2361]; Node0xb553524315e0 -> Node0xb55352432f40 [pos="e,573.5,965.59 573.5,1002.3 573.5,993.38 573.5,984.41 573.5,975.69", tooltip="0 -> L.1\nProbability 100.00%"]; Node0xb55352433190 [color="#3d50c3ff", fillcolor="#f6a38570", fontname=Courier, height=1.4861, label="{L.3:\l| %t.38 = load i32, ptr %n\l %t.39 = call i32 (ptr, ...) @printf(ptr @fmt_printf, i32 %t.38)\l %t.40 = load i32, ptr %\ c\l %t.41 = call i32 (ptr, ...) @printf(ptr @fmt_printf, i32 %t.40)\l ret i32 0\l}", pos="272.5,746.5", rects="0,776.5,545,799.5 0,693.5,545,776.5", shape=record, width=7.5694]; Node0xb55352432f40:s0 -> Node0xb55352433190 [pos="e,318.23,799.52 419.5,848 384.48,848 351.67,828.32 325.97,806.38", tooltip="L.1 -> L.3\nProbability 3.12%"]; Node0xb55352433200 [color="#3d50c3ff", fillcolor="#ec7f6370", fontname=Courier, height=1.0694, label="{L.2:\l| %t.9 = load i32, ptr %n\l store i32 %t.9, ptr %j\l br label %L.4\l}", pos="731.5,746.5", rects="628.5,761.5,834.5,784.5 628.5,708.5,834.5,761.5", shape=record, width=2.8611]; Node0xb55352432f40:s1 -> Node0xb55352433200 [pos="e,743.41,784.64 727.5,848 750.8,848 750.9,820.69 745.65,794.54", tooltip="L.1 -> L.2\nProbability 96.88%"]; Node0xb55352433660 [color="#3d50c3ff", fillcolor="#d6524470", fontname=Courier, height=1.8056, label="{L.4:\l| %t.10 = load i32, ptr %j\l %t.12 = icmp sgt i32 %t.10, 0\l %t.13 = zext i1 %t.12 to i32\l %t.14 = icmp eq i32 %t.13, \ 0\l br i1 %t.14, label %L.6, label %L.5\l|{<s0>T|<s1>F}}", pos="758.5,592", rects="601.5,633.5,915.5,656.5 601.5,550.5,915.5,633.5 601.5,527.5,758.5,550.5 758.5,527.5,915.5,550.5", shape=record, width=4.3611]; Node0xb55352433200 -> Node0xb55352433660 [pos="e,747.23,656.67 738.1,708.19 740.34,695.58 742.91,681.03 745.47,666.61", tooltip="L.2 -> L.4\nProbability 100.00%"]; Node0xb553524338b0 [color="#3d50c3ff", fillcolor="#ec7f6370", fontname=Courier, height=1.2778, label="{L.6:\l| %t.35 = load i32, ptr %i\l %t.37 = sub i32 %t.35, 1\l store i32 %t.37, ptr %i\l br label %L.1\l}", pos="595.5,445", rects="484,467.5,707,490.5 484,399.5,707,467.5", shape=record, width=3.0972]; Node0xb55352433660:s0 -> Node0xb553524338b0 [pos="e,590.35,490.59 600.5,539 591.36,539 589.32,520.88 589.89,500.63", tooltip="L.4 -> L.6\nProbability 3.12%"]; Node0xb55352433920 [color="#3d50c3ff", fillcolor="#d6524470", fontname=Courier, height=1.0694, label="{L.5:\l| %t.15 = load i32, ptr %n\l store i32 %t.15, ptr %k\l br label %L.7\l}", pos="906.5,445", rects="799,460,1014,483 799,407,1014,460", shape=record, width=2.9861]; Node0xb55352433660:s1 -> Node0xb55352433920 [pos="e,923.69,483.02 916.5,539 937.27,539 934.79,515.79 927.11,492.56", tooltip="L.4 -> L.5\nProbability 96.88%"]; Node0xb553524338b0 -> Node0xb55352432f40 [pos="e,576.58,836.4 593.33,490.69 589.54,568.89 581.68,731.23 577.07,826.28", tooltip="L.6 -> L.1\nProbability 100.00%"]; Node0xb55352433be0 [color="#b70d28ff", fillcolor="#b70d2870", fontname=Courier, height=1.8056, label="{L.7:\l| %t.16 = load i32, ptr %k\l %t.18 = icmp sgt i32 %t.16, 0\l %t.19 = zext i1 %t.18 to i32\l %t.20 = icmp eq i32 %t.19, \ 0\l br i1 %t.20, label %L.9, label %L.8\l|{<s0>T|<s1>F}}", pos="931.5,298", rects="774.5,339.5,1088.5,362.5 774.5,256.5,1088.5,339.5 774.5,233.5,931.5,256.5 931.5,233.5,1088.5,256.5", shape=record, width=4.3611]; Node0xb55352433920 -> Node0xb55352433be0 [pos="e,920.53,362.65 912.94,406.66 914.75,396.17 916.78,384.39 918.82,372.56", tooltip="L.5 -> L.7\nProbability 100.00%"]; Node0xb55352434270 [color="#3d50c3ff", fillcolor="#d6524470", fontname=Courier, height=1.2778, label="{L.9:\l| %t.32 = load i32, ptr %j\l %t.34 = sub i32 %t.32, 1\l store i32 %t.34, ptr %j\l br label %L.4\l}", pos="747.5,98.5", rects="636,121,859,144 636,53,859,121", shape=record, width=3.0972]; Node0xb55352433be0:s0 -> Node0xb55352434270 [pos="e,751.28,144.34 773.5,245 763.93,245 756.68,195.58 752.3,154.34", tooltip="L.7 -> L.9\nProbability 3.12%"]; Node0xb553524342e0 [color="#b70d28ff", fillcolor="#b70d2870", fontname=Courier, height=2.7361, label="{L.8:\l| %t.21 = load i32, ptr %n\l %t.23 = srem i32 %t.21, 32768\l %t.25 = add i32 %t.23, 1\l store i32 %t.25, ptr %n\l %t.26 = \ load i32, ptr %c\l %t.28 = add i32 %t.26, 1\l store i32 %t.28, ptr %c\l %t.29 = load i32, ptr %k\l %t.31 = sub i32 %t.29, 1\l \ store i32 %t.31, ptr %k\l br label %L.7\l}", pos="1010.5,98.5", rects="878.5,173.5,1142.5,196.5 878.5,0.5,1142.5,173.5", shape=record, width=3.6667]; Node0xb55352433be0:s1 -> Node0xb553524342e0 [pos="e,1006.2,196.84 1010.5,233 1009.2,224.5 1008.1,215.69 1007.2,206.84", tooltip="L.7 -> L.8\nProbability 96.88%"]; Node0xb55352434270 -> Node0xb55352433660 [pos="e,757.07,527.25 748.5,144.2 750.4,228.96 754.53,413.79 756.85,517.2", tooltip="L.9 -> L.4\nProbability 100.00%"]; Node0xb553524342e0 -> Node0xb55352433be0 [pos="e,1010.6,232.65 1014.8,196.55 1014,205.28 1013.1,214.07 1012,222.67", tooltip="L.8 -> L.7\nProbability 100.00%"]; } ``` 画像の形式は`pdf`の他に`png`、`gif`, `jpg`,`svg`など様々な画像形式が指定できる。 :::info この資料の付録3の設定をしておけば ``` open prime.pdf ``` のようにしてWSLのUbuntuから簡単にWindowsの既定アプリを開いて画像を確認できる。また,Windows Terminalのバージョン1.22以降では`img2sixel`コマンドを用いてターミナル上に`png`,`gif`,`jpg`等の画像を描画できる。 ``` img2sixel -w 800 prime.png ``` ::: ### 基本ブロック フローチャートから忠実に実現した(すなわち矢印をgoto文で置き換えた)C言語のコードは - ラベルで始まり - goto文/if-goto文/return文で終わる ようなコードの集まりになるはずである。よって,これから逐語訳で変換したLLVM-IRのコードも,CFGが示すように - ラベルで始まり - br命令/ret命令で終わる ようなコード(**基本ブロック**)の集まりになる。 各命令の構文に間違いがないはずなのに自分の書いたLLVM-IRのコードが構文エラーになるようなときは,goto文/if-goto文を用いたC言語のコードに戻って忠実にフローチャートを実現しているか確認し,正しく基本ブロックになっているかチェックするとよい。 ### 真偽値から整数への変換 `icmp`命令の返す真偽値(0,1)は1ビットの整数である。真偽値`a`(`a`は仮想レジスタや0,1)を32ビット整数にキャスト(型変換)するには**ゼロ拡張命令**`zext`をつかって ```llvm %t.1 = zext i1 a to i32 ``` のようにする。`%t.1`が結果として得られるi32型の値としての0,1を表している。 `picoc.c`では`icmp`命令で計算した不等号`<`,`>`,`<=`,`>=`の値を32ビット整数に統一するのにこれを用いている。 :::warning **符号拡張命令**`sext`を使うと真(1)は-1に変換されてしまうので注意。2の補数では1ビット整数の1は-1を意味するからである。 ::: :::success ### 課題2(手作業によるコンパイル) 以下のC言語のコード`div.c`をLLVM-IRのコードに手作業で翻訳せよ。LLVM-IRのコードはコンパイル・実行して**網羅的な**テストを実施し`div.c`の正しい翻訳になっていることを確認せよ。 翻訳は以下の手順にしたがい段階をおって作業し,この各段階を含めレポートにまとめよ。 1. `div.c`をフローチャートを用いて表せ。矢印の指すブロックにはラベル(`L1`, `L2`...等)を記せ。 2. フローチャートを制御構造の代わりにgoto文/if-goto文を用いたC言語のコードで表せ。この段階で一度テストを実施するのもよい方法である。 3. さらにLLVM-IRのコードに逐次置き換えよ。CFGも与えよ。 4. 最後に網羅的なテストを実施し正しい翻訳になっていることを確認せよ。 ### 実験上の注意点 (1) この課題は次回の実験3(コンパイラ作成・拡張)のための準備である。やみくもに翻訳するのではなく,機械(人間コンパイラ)になったつもりで系統的に(順序よく)作業するのが重要である。つまり最初にフローチャートを正しくつくれば後は頭をひねらずに機械的な逐語訳で進めるのが理想である。フローチャートとgoto文のコードの対応が不明瞭であったり,goto文のコードとLLVM-IRのコードの対応が不明瞭な答案は評価が低くなるおそれがある。 (2) テストは単に適当な例でやみくもに多くのケースを試せばよいというものではない。あらゆる場合をできるだけ広範にカバーするように各テストケースの選択は明確な意図をもってなされるべきである。少なくとも典型的な例と境界的な例(極端な例)を含むべきである。実施した各テストケースがどのような意図で選ばれたのかをレポートでは明確にせよ。 `div.c` ```cpp #include <stdio.h> int main() { int n; scanf("%d", &n); int i = 1; while (i * i <= n) { if (n % i == 0) { printf("%d\n", i); printf("%d\n", n / i); } i = i + 1; } return 0; } ``` >C言語の文法ではラベルの直後には必ず文が必要(空や宣言は不可)。どうしても空にしたいときや宣言で始めたいときは空の式文(`;`だけからなる文)を使うかブロック{ ... }にすればよい。もっとも、この課題ではそのような必要はないはずだが。 ::: ## 実験3. コンパイラの機能拡張 コンパイラフロントエンドのコード(`picoc.c`)を理解し,新たにif文をサポートする機能を実現する。さらに余力のある人は配列をサポートする機能も実現する。 ### Pico-Cの文法 Pico-Cは標準C言語の(ほぼ)サブセットである。扱うデータ型は`int`型だけで,他の整数型や浮動小数型はない。また,配列,構造体,ポインタ等の派生型もない。よって文字列もない。ローカル変数が宣言できるが,初期値は代入で設定する。代入は式文としてしか出現できない。制御構造はwhile文だけで,if文等はない。演算子も9個しかない(後述)。関数は`main()`しかなく他の関数を呼び出す機能もない。このため,`scanf()`/`printf()`関数を呼び出す代わりに,標準入力ストリームから10進整数を読み込む`scan`文と標準出力ストリームに10進整数を書き出す`print`文が追加されている。以上の文法をBNFと構文図式でまとめると以下の通りである。 ```bnf Program ::= 'int' 'id' '(' ')' Block Block ::= '{' ( Decl | Statement )* '}' Decl ::= 'int' 'id' ';' Statement ::= Assign_st | Scan_st | Print_st | While_st | Block Assign_st ::= 'id' '=' Expr ';' Scan_st ::= 'scan' 'id' ';' Print_st ::= 'print' Expr ';' While_st ::= 'while' '(' Expr ')' Statement Expr ::= Additive_1 ( ( '<' | '>' | '<=' | '>=') Additive_2 )* Additive ::= Term_1 ( ( '+' | '-' ) Term_2 )* Term ::= Factor_1 ( ( '*' | '/' | '%') Factor_2 )* Factor ::= 'id' | 'num' | '(' Expr ')' ``` Program: ![Program](https://hackmd.io/_uploads/SkPy7gq_eg.png) Block: ![Block](https://hackmd.io/_uploads/rJvg7e9uex.png) Decl: ![Decl](https://hackmd.io/_uploads/SJPhtwWlbg.png) Statement: ![Statement](https://hackmd.io/_uploads/SyCZXe9_eg.png) Assign_st: ![Assign_st](https://hackmd.io/_uploads/rJKMmg9dee.png) Scan_st: ![Scan_st](https://hackmd.io/_uploads/HJAtHlcdxx.png) Print_st: ![Print_st](https://hackmd.io/_uploads/ryGEQlcugl.png) While_st: ![While_st](https://hackmd.io/_uploads/BJP97xqdgg.png) Expr: ![Expr](https://hackmd.io/_uploads/H1oLQgqOxx.png) Additive: ![Additive](https://hackmd.io/_uploads/Hy-w7xc_xg.png) Term: ![Term](https://hackmd.io/_uploads/ry2PQeq_lx.png) Factor: ![Factor](https://hackmd.io/_uploads/HJNdQg9dgl.png) :::info 構文図式はすべてこのサイトを用いて生成した。 [Railroad Diagram Generator](https://www.bottlecaps.de/rr/ui) ::: 標準C言語と同じく整数0が偽値falseを表し、0以外の整数(典型的には1)が真値trueを表す。式を構成する演算子は`+`, `-`, `*`, `/`, `%`,`<`, `>`, `<=`, `>=`の9個ですべて左結合の二項演算子である。このうち`<`, `>`, `<=`, `>=`は成り立てば1をそうでなければ0を返す。演算子の結合順位は標準C言語と同じである。 工夫次第でより複雑な条件式を表せる。以下`c`,`d`を値0か1を取る式とすると,条件`c`の否定「`c`でない」(`!c`)は`c < 1`と表せる.「`c`かつ`d`」は`c * d`と表せる.よって,「`c`または`d`」はド=モルガンの法則を用いて`((c < 1) * (d < 1)) < 1`と表せる。あるいはより簡単に`c + d > 0`でもよい。 (不)等号`==`, `!=`は用意されていないが,`a == b`は`(a <= b) * (b <= a)`で表せる。よって`a != b`は`((a <= b) * (b <= a)) < 1`で表せる。あるいは `(a < b) + (b < a) > 0`でもよい。 変数の宣言では初期化はできないので ``` int n = 0; ``` は ``` int n; n = 0; ``` とする必要がある。 代入は式文としての出現に制限しているので,例えば入れ子になった代入式 ``` x = y = 0; ``` は記述できない。代わりに2文に分けて ``` y = 0; x = y; ``` のように書く必要がある。データ型はint型だけなので代入式の左辺は変数名を表す識別子しか出現しない。 while文は標準Cのそれと同じである。条件式の値が0でない間,反復する。反復される文が1つしかないときは,それを囲むブラケット`{ ... }`は不要である。 if文は用意されていないが,高々1回だけ反復するwhile文で模倣できる。具体的には ``` if (C) S; ``` は ``` int c; c = C; while (c) { S; c = 0; } ``` で実現できる。 ``` if (C) S1 else S2; ``` も ``` int c1; int c2; c1 = C; c2 = c1 < 1; while (c1) { S1; c1 = 0; } while (c2) { S2; c2 = 0; } ``` のようにすればよい。短絡論理演算(`&&`, `||`)はif文で `c = a && b` ``` if (a) c = b; else c = 0; ``` `c = a || b` ``` if (a) c = 1; else c = b; ``` のように実現できるので上記のようにwhile文になおせる。 ディレクトリ`example`にはPico-Cで書かれたコードのサンプルがいくつかある。 ``` fib.pc フィボナッチ数列を求める gcd.pc ユークリッドの互助法で最大公約数を求める prime.pc 素数列を求める binary.pc バイナリー法でべき剰余を高速に求める bench.pc 10億回程度反復するベンチマーク ``` `prime.pc`と`binary.pc`あたりをみるとPico-Cのだいたいの感じがつかめる。 ### コンパイラのソースコード(picoc.c) :::danger **ソースコードpicoc.c** :::spoiler  (▶︎をクリックして展開) ```cpp= /* * Pico-Cコンパイラ */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include "scan.h" #include "hashmap.h" int token; // 最後に読んだトークン char name[MAX_TK_LEN]; // ...の名前 int vsuffix = 1; // 値の一時名の接尾番号 int lsuffix = 1; // ラベルの接尾番号 struct hashmap *hmap; // 変数名と行番号の対を保存するハッシュ表 #define MAX_TMP_LEN (MAX_TK_LEN + 1) // 値の一時名の最大長 // デバッグモードでは標準エラー出力に構文解析木が出力される //#define DEBUG int findent = 0; #ifdef DEBUG #define ENTER() { findent += 3; fprintf(stderr, "%*c|- %s\n", findent, ' ', __func__); } #define LEAVE() findent -= 3 #define FEED() fprintf(stderr, "%*c \'\e[7m%s\e[0m\' \e[2m (行%d:%d)\e[0m\n", findent, ' ', name, row, col) #else #define ENTER() #define LEAVE() #define FEED() #endif void statement(void); void expr(char *ret); void expect(int expected) { if (token == expected) { FEED(); token = get_next_token(name); return; } fprintf(stderr, "期待されない字句(行%d:%d): %s\n", row, col, name); exit(1); } void lval(char *x) { ENTER(); if (hget(hmap, name) < 0) { fprintf(stderr, "不明な変数(行%d:%d): %s\n", row, col, name); exit(1); } sprintf(x, "%%%s", name); expect(TK_ID); LEAVE(); } void num(char *ret) { ENTER(); strcpy(ret, name); expect(TK_NUM); LEAVE(); } void factor(char *ret) { ENTER(); char x[MAX_TMP_LEN]; int v = vsuffix++; switch (token) { case TK_ID: lval(x); // 代入の左辺以外に出現する左辺値は参照を外す printf(" %%t.%d = load i32, ptr %s\n", v, x); sprintf(ret, "%%t.%d", v); break; case TK_NUM: num(ret); break; case '(': expect('('); expr(ret); expect(')'); break; default: fprintf(stderr, "期待されない字句(行%d:%d): %s\n", row, col, name); exit(1); } LEAVE(); } void term(char *left) { ENTER(); factor(left); while (token == '*' || token == '/' || token == '%') { int op = token; expect(op); char right[MAX_TMP_LEN]; factor(right); int v = vsuffix++; const char *f = op == '*' ? "mul" : op == '/' ? "sdiv" : "srem"; printf(" %%t.%d = %s i32 %s, %s\n", v, f, left, right); sprintf(left, "%%t.%d", v); } LEAVE(); } void additive(char *left) { ENTER(); term(left); while (token == '+' || token == '-') { int op = token; expect(op); char right[MAX_TMP_LEN]; term(right); int v = vsuffix++; const char *f = op == '+' ? "add" : "sub"; printf(" %%t.%d = %s i32 %s, %s\n", v, f, left, right); sprintf(left, "%%t.%d", v); } LEAVE(); } void expr(char *left) { ENTER(); additive(left); while (token == '<' || token == '>' || token == TK_LEQ || token == TK_GEQ) { int op = token; expect(op); char right[MAX_TMP_LEN]; additive(right); int u = vsuffix++, v = vsuffix++; const char *f = op == '<' ? "slt" : op == '>' ? "sgt" : op == TK_LEQ ? "sle" : "sge"; printf(" %%t.%d = icmp %s i32 %s, %s\n", u, f, left, right); printf(" %%t.%d = zext i1 %%t.%d to i32\n", v, u); sprintf(left, "%%t.%d", v); } LEAVE(); } void print_st(void) { ENTER(); expect(TK_PRINT); char e[MAX_TMP_LEN]; expr(e); printf(" %%t.%d = call i32 (ptr, ...) @printf(ptr @fmt_printf, i32 %s)\n", vsuffix++, e); expect(';'); LEAVE(); } void scan_st(void) { ENTER(); expect(TK_SCAN); char x[MAX_TMP_LEN]; lval(x); printf(" %%t.%d = call i32 (ptr, ...) @scanf(ptr @fmt_scanf, ptr %s)\n", vsuffix++, x); expect(';'); LEAVE(); } void assign_st(void) { ENTER(); char x[MAX_TMP_LEN]; lval(x); expect('='); char e[MAX_TMP_LEN]; expr(e); printf(" store i32 %s, ptr %s\n", e, x); expect(';'); LEAVE(); } void while_st(void) { ENTER(); int while_entry = lsuffix++; int while_body = lsuffix++; int while_end = lsuffix++; printf(" br label %%L.%d\n", while_entry); // whileループ冒頭のブロック printf("L.%d:\n", while_entry); expect(TK_WHILE); expect('('); char e[MAX_TMP_LEN]; expr(e); expect(')'); int c = vsuffix++; printf(" %%t.%d = icmp eq i32 %s, 0\n", c, e); printf(" br i1 %%t.%d, label %%L.%d, label %%L.%d\n", c, while_end, while_body); // whileループ本体のブロック printf("L.%d:\n", while_body); statement(); printf(" br label %%L.%d\n", while_entry); // whileループの直後のブロック printf("L.%d:\n", while_end); LEAVE(); } void decl(void) { ENTER(); expect(TK_INT); int r = hget(hmap, name); if (r >= 0) { fprintf(stderr, "宣言の重複(行%d:%d): %s (行%dで既出)\n", row, col, name, r); exit(1); } char *x = malloc(MAX_TK_LEN); // hdestroy()内でfree()される strcpy(x, name); hinsert(hmap, x, row); // xの宣言の行番号を記録 expect(TK_ID); printf(" %%%s = alloca i32\n", x); expect(';'); LEAVE(); } void block(void) { ENTER(); expect('{'); while (token != '}') { if (token == TK_INT) decl(); else statement(); } expect('}'); LEAVE(); } void statement(void) { ENTER(); switch (token) { case TK_PRINT: print_st(); break; case TK_SCAN: scan_st(); break; case TK_ID: assign_st(); break; case TK_WHILE: while_st(); break; case '{': block(); break; default: fprintf(stderr, "期待されない字句(行%d:%d): %s\n", row, col, name); exit(1); } LEAVE(); } void program(void) { ENTER(); printf("@fmt_printf = constant [4 x i8] c\"%%d\\0A\\00\"\n"); printf("@fmt_scanf = constant [3 x i8] c\"%%d\\00\"\n"); printf("declare i32 @printf(ptr, ...)\n"); printf("declare i32 @scanf(ptr, ...)\n"); printf("@__main_void = alias i32 (), ptr @main\n\n"); // target:wasm32-wasiで必要 expect(TK_INT); if (strcmp(name, "main") != 0) { fprintf(stderr, "main関数が必要(行%d:%d): %s\n", row, col, name); exit(1); } expect(TK_ID); expect('('); expect(')'); printf("define i32 @main() {\n"); block(); printf(" ret i32 0\n"); printf("}\n"); LEAVE(); } int main() { hmap = hnew(); token = get_next_token(name); program(); hdestroy(hmap); // キーもここでfreeしている } ``` ::: #### `picoc.c`の概要 ファイル`picoc.c`はおよそ300行のC言語のコードで、5つのグローバル変数と16個の関数定義が含まれている。 #### グローバル変数と字句解析器の呼び出し 各グローバル変数の役割は以下の通りである。 ``` token 次に処理する字句の識別番号(scan.h参照) name その名前 vsuffix 次に用いる仮想レジスタ名の添字番号 lsuffix 次に用いるラベル名の添字番号 hmap 変数名を管理するハッシュ表 ``` グローバル変数`token`は次に処理すべき字句の識別番号を常に保持している。またその字句の名前(文字列)は`name`というグローバル変数に保持されている。字句解析器(scan.c)を呼び出してこれらを更新するには`get_next_token()`という関数を呼び出す。 ```cpp token = get_next_token(name); ``` `vsuffix`や`lsuffix`は重複のない仮想レジスタ名やラベル名を生成するための添え字として使用される。使用する毎にカウントアップし,重複を避ける。`hmap`は宣言された変数の名前をコンパイル作業の間、いつでも参照できるよう保存しておくためのハッシュ表で,宣言のし忘れや重複宣言を検出するのに用いられる。 #### 再帰下降型解析 16個の関数のうち、**12個は上述の12個の構文図式と1対1に対応**しており、各図式にしたがって字句を読み進めながらLLVM-IRの命令列を(printf()関数を用いて)出力する。構文の解析が終了した時点でコードの生成も同時に完了している。このように構文解析とコード生成を同時におこなうコンパイラは**1パス**方式と呼ばれる。抽象構文木を生成しないので仕組みが簡単であり、単純な言語のコンパイラでしばしば用いられる。また、構文図式にそって各構文に対応する関数を(再帰)呼び出ししながら解析を進める方法は**再帰下降型構文解析**と呼ばれる。 残りの4つの関数は`main()`と下請け関数である。 ``` main() 処理の開始 expect() 字句をひとつ読み進めたりエラーを検出する下請け関数 num() 数を処理する下請け関数 lval() 左辺値(変数)を処理する下請け関数 ``` #### 式 式の構文図式に対応する4つの関数 ``` factor() 因子(これ以上分割できない基本的な式)の処理 term()   項(1つ以上の因子を乗除余算でつないだ式)の処理 additive() 加法式(1つ以上の項を加減算でつないだ式)の処理 expr()   式(1つ以上の加法式を不等号でつないだ式)の処理 ``` に`num()`, `lval()`を加えた6つの関数は、呼び出されるときに文字列を格納するための**バッファ**(`char`型の配列)を実引数として渡される(正確にはその先頭要素へのポインタを渡される)。復帰(リターン)の際にはこのバッファに式の計算結果を表す`"%t.1"`のような仮想レジスタ名かあるいは十進整数を表す`"123"`のような文字列を書き戻す。 :::info このように結果を格納するためのバッファを呼び出し側で用意しバッファへのポインタを引数として渡すのはC言語の常套句である(例:`scanf()`)。 ::: #### 変数の宣言と参照 変数の宣言(図式`Decl`)は関数`decl()`で処理される。まず,`hget(hmap, name)`を呼び出して変数が既に登録されていないかチェックし,登録済みの場合は重複宣言のエラーにしている。登録されていない場合は`hinsert(hmap, x, row)`を呼び出してハッシュ表に登録し, ```cpp printf(" %%%s = alloca i32\n", x); ``` で`alloca`命令を出力する。 変数名を表す文字列は`decl()`関数から抜けた後も随時参照されるのでヒープに動的に割り付ける必要がある。 ```cpp char *x = malloc(MAX_TK_LEN); // hdestroy()内でfree()される strcpy(x, name); ``` 割り付けられた記憶域は,最後に`hdestroy()`を呼んでハッシュ表を解放する際にあわせて解放される(ので自分でfreeしないこと)。 左辺値(変数)の参照に関する処理は下請けの関数`lval(char *x)`として共通化されており,関数`factor()`や`assign_st()`から呼び出される。`lval()`は次に出現する字句をハッシュ表から検索し宣言済みの変数名であることを確認する(なければエラーにする)。確認した変数名の最初に`'%'`を補った文字列を、引数として与えられたバッファに書き込む(このような文字列の加工には`sprintf()`が便利)。 約束により変数名の前に`'%'`を付けるとその変数を指すポインタ(アドレス)を意味するので、これを`store`/`load`命令に渡せば変数を読み書きできる。代入文(図式`Assign_st`)を処理する`assign_st()`ではこれを用いて`store`命令を出力している。 ```cpp printf(" store i32 %s, ptr %s\n", e, x); ``` 因子(図式`Factor`)を処理する`factor()`関数ではこれを用いて`load`命令を出力している。 ```cpp printf(" %%t.%d = load i32, ptr %s\n", v, x); ``` `printf()`の書式文字列の中で`'%'`そのものを表すには`%`を重ね書きして`%%`のようにすることに注意。 #### while文 while文(図式`While_st`)に対応する関数`while_st()`では実験2(LLVM-IR)のwhile文の実現方法にしたがって、3つのブロックのためのラベルを用意する。 ```cpp int while_entry = lsuffix++; int while_body = lsuffix++; int while_end = lsuffix++; ``` 冒頭ブロック(while_entry)では,まず条件式の値`e`を計算するコードを出力する。この値は`i32`(32ビットの整数)である。次にこれが`0`かどうかによって`while_end`か`while_body`に条件分岐するコード ```cpp int c = vsuffix++; printf(" %%t%d = icmp eq i32 %s, 0\n", c, e); printf(" br i1 %%t%d, label %%L%d, label %%L%d\n", c, while_end, while_body); ``` を出力している。 while_bodyの末尾では反復のために ```cpp printf(" br label %%L%d\n", while_entry); ``` で冒頭のブロックに無条件に戻る命令を出力している。 :::success ### 課題3(if文のサポート) 標準C言語と同じ(※)if文をサポートするよう`picoc.c`を拡張せよ。 (※)標準C言語のif文と同じとは - 条件式の値が0かどうかで場合分け - `else`以下は省略可能 - 文がひとつしかないときは文を囲むブラケット`{`...`}`も省略可能 - 省略時に生じるあいまいさ([**dangling else**](https://en.wikipedia.org/wiki/Dangling_else))は「elseが直近のifと対応する」ように解消 の要件を満足することを意味する。 以下の手順にしたがい作業しレポートにまとめよ。 1. if文をサポートするようPico-Cの構文図式に追加・変更を加える。 2. 構文図式にしたがい`picoc.c`に系統的にコードを追加する(下記参照)。 3. 網羅的に十分なテストをおこなう(下記参照)。 4. その上で`example/kadai3`ディレクトリにある`binary_kadai3.pc`と`prime_kadai3.pc`が正しくコンパイル・実行できるか最終確認する。 ### 実験上の注意点 (1) やみくもに試行錯誤でコードを追加変更するのではなく,ステップ1でつくった構文図式に添って系統的にコーディングすること。そうすれば40行程度の簡単な追記作業で済む。 (2) テストは網羅的におこなうこと。ブラケットの有無,elseの有無,if文が入れ子になった例も忘れずに。特に,宙ぶらりんのelseが正しく解決されていることも確認せよ。 (3) レポートではソースコード全体を無駄に漫然と貼り付けるのではなく,ステップ2でコードを追加・変更した箇所(つまりオリジナルからの差分)が明確になるようにせよ。以下のコマンドの結果を利用するのが最も簡単である。 ``` git diff --color=always picoc.c ``` このコマンド出力では追記行が`+`で,削除行が`-`で明示される。さらに追加行は緑で,削除行は赤で色分けされる。もしも差分が思った通りに認識されないときは他の差分アルゴリズム(例:`histogram`)も試すとよい。 ``` git diff --diff-algorithm=histogram --color=always picoc.c ``` (4)課題文に「`picoc.c`に...コードを追加する」と明記されているにもかかわらず`picoc.c`以外のファイルをつくってそれに追加しようとしてコンパイルに苦労している人が例年何人かいる。課題文通り`picoc.c`に追加すれば,コンパイルは ``` make ``` とするだけで済む。 参考:キーワード`if`と`else`の識別番号は`scan.h`の中で以下のようにマクロ定義されている。 ```cpp #define TK_IF 134 // if #define TK_ELSE 135 // else ``` ::: :::success ### 課題4(配列のサポート)[提出は任意,加点対象] >注意:これは課題1〜3に取り組んだ人だけが提出できる余力と意欲のある人向けのオプション課題である.課題1〜3にやり残しがあると採点されない。また,再提出レポートでの追加提出はできない。 課題3に加え、int型の要素の(1次元)配列をサポートするように`picoc.c`を拡張せよ。`example/array`にあるサンプルコードが正しくコンパイル・実行できるか確認せよ。 配列をサポートしたPicoc-Cの構文は以下のようになる(変更のある箇所のみ記載)。 ``` Decl ::= 'int' id ( '[' 'num' ']' )? ';' Assign_st ::= 'id' ( '[' Expr ']' )? '=' Expr ';' Factor ::= 'id' ( '[' Expr ']' )? | 'num' | '(' Expr ')' ``` Decl: ![Decl](https://hackmd.io/_uploads/HyK-crodex.png) Assign_st: ![Assign_st](https://hackmd.io/_uploads/BkHfqBjdll.png) Factor: ![Factor](https://hackmd.io/_uploads/Byaf9riOex.png) Pico-Cはポインタをサポートしないので,式の中に配列名`a`が出現するときは必ずブラケットを伴って`a[...]`という形で出現する。 ### 実験上のヒント 配列の宣言も`alloca`命令でおこなえる。例えばint型の要素3つの配列`a`を宣言するには ```llvm %a = alloca [3 x i32] ``` とすればよい。`%a`は確保された配列を指すポインタである。 >(参考)LLVMはバージョン15で[**opaque pointer**](https://llvm.org/docs/OpaquePointers.html) (ポインタの指す先のオブジェクトの型情報を持たないポインタ,C言語でいえば`void *`のようなもの)がデフォルトになったので,「配列全体の先頭を指すポインタ」と「配列の先頭要素を指すポインタ」は区別されない。アドレスとしては両者は同一だからである。 `alloca`命令が返す配列の先頭を指すポインタから各要素を指すポインタを得るにLLVM-IRの**getelementptr (GEP)命令**を用いる。GEP命令は配列や構造体等を指すポインタ(アドレス)からその要素やメンバを指すポインタ(アドレス)を算出するための汎用的な命令である。 [The Often Misunderstood GEP Instruction](https://llvm.org/docs/GetElementPtr.html) [LLVM's getelementptr, by example](https://blog.yossarian.net/2020/09/19/LLVMs-getelementptr-by-example) 例えば`&a[2]`(= `a + 2`)を得るには`getelementptr`命令を用いて ```llvm %t.1 = getelementptr i32, ptr %a, i32 2 ``` のようにする。最初の`i32`がポインタの指す先のオブジェクト(要素)の型を,最後の`2`が要素2つ分ずらすことを指定している部分である。GEP命令の返すポインタ(アドレス)を`load`命令や`store`命令に渡せば,通常の変数と同じように配列の要素の読み書きができる。 例えば ```cpp int main() { int a[3]; a[0] = 12; a[1] = 34; a[2] = a[0] + a[1]; return a[2]; } ``` は以下のように実現できる。 ```llvm define i32 @main() { %a = alloca [3 x i32] %t.1 = getelementptr i32, ptr %a, i32 0 ; &a[0] %t.2 = getelementptr i32, ptr %a, i32 1 ; &a[1] %t.3 = getelementptr i32, ptr %a, i32 2 ; &a[2] store i32 12, ptr %t.1 ; a[0] = 12 store i32 34, ptr %t.2 ; a[1] = 34 %t.4 = load i32, ptr %t.1 %t.5 = load i32, ptr %t.2 %t.6 = add i32 %t.4, %t.5 store i32 %t.6, ptr %t.3 ; a[2] = a[0] + a[1] ret i32 %t.6 } ``` >`%a`と`%t.1`は同じアドレスなのでGEP命令 ```llvm %t.1 = getelementptr i32, ptr %a, i32 0 ; &a[0] ``` は冗長だが,`&a[0]`を`&a[1]`や`&a[2]`と統一的に扱うために敢えて入れた。 図式`Assign_st`,`Factor`の左辺値(変数)を参照する部分は下請けの関数`lval()`に共通化されているので,関数`assign_st()`と`factor()`に手を加えるより`lval()`に手を加えるほうが作業が簡単に済む。`lval()`と`decl()`への追加・変更は合わせてもわずか20行程度である。 ::: ## 付録1:デバッグのヒント ### デバッグ出力を有効にする 変数DEBUGをセットしてpicocをコンパイルしなおすと標準エラー出力ストリームに構文解析木を出力するようになる。 ``` make clean; CFLAGS=-DDEBUG make ``` >もとに戻すには >``` >make clean; make >``` `CFLAGS`で`DEBUG`を定義する代わりに`petitc.c`の中で ```cpp // #define DEBUG ``` のコメントを外して有効化してもよい。 これでコンパイルの過程を(標準エラー出力ストリームに)ツリー表示するようになるのでデバッグに役立つ。以下は`fib.pc`を処理したときの出力の冒頭部分。どのように関数が呼ばれてどの関数でどの字句を処理したのかが分かるのでデバッグに役立つ。 実行例: ``` cat example/fib.pc | ./picoc 2>&1 1>- | bat |- program 'int' (行1:1) 'main' (行1:5) '(' (行1:9) ')' (行1:10) |- block '{' (行2:1) |- statement |- print_st 'print' (行3:5) |- expr |- additive |- term |- factor |- num '1' (行3:11) ';' (行3:12) |- decl 'int' (行4:5) 'f0' (行4:9) ';' (行4:11) |- statement |- assign_st |- lval 'f0' (行4:13) '=' (行4:16) (以下省略) ``` ### 想定外のことが起こったときに実行を強制終了させる `assert(式)`マクロを挿入すると式が成り立たないときに実行を強制終了し、その行番号を表示できる。 例: ``` assert(token == '+'); ``` は、`token`の値が`'+'`でなかった場合に強制終了し、行番号を表示する。 `assert()`マクロを用いるには ``` #include <assert.h> ``` を加える。コード完成後、`assert.h`を読み込むより前で ``` #define NDEBUG ``` しておけば、`assert()`を無効化できる(無害なvoid式で置換される)ので、手作業で`assert()`式をわざわざ取り除く必要はない。 ### デバッガgdb/cgdbを活用する 別ページを用意した。 [コンパイラ実験に便利なgdb (cgdb)の使い方](https://hackmd.io/@okuisatoshi/cgdb) ## 付録2 WSL2のファイルをエクスプローラで開く WSLのファイルやディレクトリも`explorer.exe`で開けるが,パスの仕様がWindowsとLinuxでは異なるのでやや不便である.`~/.profile`に下記の設定 ``` function open() { explorer.exe $(wslpath -w "$1") } ``` を追記しておくと ``` open example/kadai3/prime_kadai3.pc ``` のように深い階層にあるファイルやディレクトリもLinuxのパス指定の方法で簡単に開けるようになる. ## 付録3 コマンドとクリップボードを連携する `~/.profile`に以下の設定 ``` alias pbcopy='(nkf -Lw | wl-copy)' alias pbpaste='(wl-paste | nkf -Lu)' alias pbrun='echo "$(pbpaste)"; read -p "y/n?: " c; [[ "$c" == y ]] && . <(pbpaste)' # 以下はWSL 2.5.7以前のバグへの対応: if [ ! -S "$XDG_RUNTIME_DIR/wayland-0" ]; then ln -s /mnt/wslg/runtime-dir/wayland-0* "$XDG_RUNTIME_DIR" fi ``` を追記しておくとコマンドの出力をクリップボードにコピーしてWindowsアプリで用いたり,逆にクリップボードの内容をコマンドにパイプして利用したりできるようになる。 >(別の方法) 上記の方法が上手く動作しない環境では、[win32yank.exe](https://github.com/equalsraf/win32yank/releases/tag/v0.1.1)というWindowsアプリ(リンク先の[win32yank-x64.zip](https://github.com/equalsraf/win32yank/releases/download/v0.1.1/win32yank-x64.zip)をダウンロードして解凍すると得られる)をCドライブ直下`C:\`に置き,`~/.profile`に以下の設定を書いておくのでもよい。 > ``` alias pbcopy="/mnt/c/win32yank.exe -i --crlf" alias pbpaste="/mnt/c/win32yank.exe -o --lf" alias pbrun='echo "$(pbpaste)"; read -p "y/n?: " c; [[ "$c" == y ]] && . <(pbpaste)' ``` ### `pbcopy`/`pbpaste`/`pbrun`の使用例 例:(Webブラウザ上で)クリップボードにコピーしたLLVM中間言語のコードを`hello.ll`というファイルに保存。 ``` pbpaste > hello.ll ``` 例:(Webブラウザ上で)クリップボードにコピーしたLLVM中間言語のコードをインタプリタ`lli`で実行。 ``` pbpaste | lli ``` 例:(Webブラウザ上で)クリップボードにコピーしたC言語のコードを`clang`でコンパイルして実行し,実行結果をクリップボードにコピー(してWord/LaTeXなどで利用)。 ``` pbpaste | clang -x c -; ./a.out | pbcopy ``` 例:(Webブラウザ上で)クリップボードにコピーしたPico-Cのコードを`picoc`と`clang`でコンパイル。 ``` pbpaste \ | ./picoc \ | clang -Wno-override-module -x ir - -O2 ``` 例:`example/fib.pc`を`picoc`でコンパイルし,その出力したLLVM中間言語をクリップボードにコピー(してワードやLaTeXで利用)。 ``` cat example/fib.pc | ./picoc | pbcopy ``` 例:(Webブラウザ等から)クリップボードにコピーしたコマンド行をターミナルで実行 ``` pbrun ``` 例:最後に実行したコマンド行をクリップボードにコピー ``` echo "!!" | pbcopy ``` 例:最後に実行した`clang`で始まるコマンド行をクリップボードにコピー ``` echo "!clang" | pbcopy ``` :::warning クリップボードの内容のペースト/実行はセキュリティ上の潜在的なリスクが伴うので, `pbrun`ではいったん内容を表示し`y/n`で確認を求めるようにしている。 :::