Quantcast
Channel: Bashタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 2811

シェルスクリプト(Bashスクリプト)でかんたんな自作言語のコンパイラを書いた

$
0
0
Bashがある環境ならどこでも自作言語で書いたプログラムをコンパイルできるようになりました。 できたもの いろいろと雑なのですが、アドベントカレンダーの期日があるのでとりあえず公開します。 サイズはこんな感じ。 $ wc -l {lexer,parser,codegen}.sh lib/*.sh 220 lexer.sh 655 parser.sh 525 codegen.sh 42 lib/common.sh 193 lib/json.sh 284 lib/utils.sh 1919 合計 動かし方の例 echo ' func add(a, b) { return a + b; } func main() { var one = 1; var result; call_set result = add(one, 2); } ' | ./lexer.sh | ./parser.sh | ./codegen.sh # ↓アセンブリが出力される call main exit label add push bp cp sp bp cp [bp:2] reg_a push reg_a cp [bp:3] reg_a push reg_a pop reg_b pop reg_a add_ab cp bp sp pop bp ret label main push bp cp sp bp sub_sp 1 cp 1 reg_a cp reg_a [bp:-1] sub_sp 1 cp 2 reg_a push reg_a cp [bp:-1] reg_a push reg_a _cmt call~~add call add add_sp 2 cp reg_a [bp:-2] cp bp sp pop bp ret # (snip) 移植元 Bashスクリプト版のベースになっているバージョンは tag:62 のあたり <自作言語処理系の説明用テンプレ> 自分がコンパイラ実装に入門するために作った素朴なトイ言語とその処理系です。簡単に概要を書くと下記のような感じ。 小規模: コンパイラ部分は 1,000 行程度 pure Ruby / 標準ライブラリ以外のライブラリ不要 x86風の自作VM向けにコンパイルする ライフゲームのために必要な機能だけ 変数宣言、代入、反復、条件分岐、関数定義 演算子: +, *, ==, != のみ(優先順位は ( ) で明示) 型なし(値は符号付き整数のみ) 作ったときに書いた備忘記事 RubyでオレオレVMとアセンブラとコード生成器を2週間で作ってライフゲームを動かした話 Rubyでかんたんな自作言語のコンパイラを作った 本体には含めていない後付けの機能など 真偽値リテラル / break / if/else / 単項マイナス / Racc などを使って書いたパーサの別実装 Ruby 以外の言語への移植 コンパイラ部分のみ Python, Java, TypeScript など、2021-12-19 の時点では 20言語 セルフホスト版 製作過程を知りたい場合は製作メモを見てください <説明用テンプレおわり> メモ 文字列 下記の記事のおかげでなんとかなりました。ありがとうございます バイト単位で16進数表現との相互変換さえできてしまえば、あとは煮るなり焼くなり思いのまま、です。 データ構造 [ 123, "fdsa", [ 11, "FDSA", [] ] ] のような、リストの要素として整数、文字列、リストを持たせることができる再帰的なデータ構造が必要です。Bashスクリプトの配列は単純な値を入れる分にはいいのですが、ネストしたリストを扱うことができなさそうでした。 (※ いつもならこういうことをやりたくなった段階で別の言語で書くことを検討しますが、今回は Bashスクリプトで書くこと自体が目的です) そこで、ひとまず下記のようにしました。もっと良いやり方がある気がします。 リストの要素にあたるデータの表現はこんな感じにしました。 # 整数 "int:-123" # 文字列 "str:fdsa" # リスト "list:1" 先頭に型を表すタグを付けた1行の文字列 文字列の値(str:... の ... の部分)は改行を含まない(レキサで捨てる)ため、改行についての考慮は不要 list: の後に続く数字はリストID たとえば ["+", 1, -2] というリストは次のように3行の文字列で表せます。1行がリストの要素1つに対応します。 "str:+ int:1 int:-2 " この文字列をグローバルな配列変数に入れて管理する形にしました。要素のインデックスがリストIDに対応します。 たとえば、入れ子のあるリスト ["+", 1, ["*", 2, 3]] は次のようになります。 # GLOBAL[0] "str:* int:2 int:3 " # GLOBAL[1] "str:+ int:1 list:0 " パフォーマンスの問題は別途ありますが、要するに下記のような操作ができるインターフェイスが用意できればOK。 リストの要素数を求める リストからn番目の要素を読み書きする リストの要素の型を判別する リストの要素の値を取りだす 関数からの値の返却 関数 myfunc から標準出力に出力して result="$(myfunc)" のようにコマンド置換で受け取る方法だと不都合な点が2つあります。 (1) 末尾の改行の有無を制御しにくい シェルスクリプトにはコマンド出力を変数に入れると末尾の改行が全部消えてしまう罠がある! - Qiita に書かれている通り (2) 呼び出した関数内でグローバル変数を更新できない コマンド置換だとサブシェルになるので ちょっと悩みましたが、結局下記の方式に落ち着きました。 関数からの戻り値の受け渡し用のグローバル変数 RV1 を用意する $(...) で囲まずに普通に関数を呼び出す 関数側では、返したい値を RV1 に代入する 呼び出し元では、関数呼び出しの直後に RV1 から移し替える 単純な例として、文字列を2つ受け取って連結した文字列を返す関数。 # return value RV1= myfunc() { RV1="${1}${2}" } myfunc "foo" "bar" retval="$RV1" こうですね。単にこのパターンで書けばよいだけなので頭は使わなくてよいです。無心に書いていくとコンパイラができあがります。並行処理とかやってないですし、関数から戻った直後で受け取る約束さえ守っていれば問題は起こりません。 全然スマートじゃなくてなんじゃこりゃって感じのソリューションですけど、この方法にも良いところがあって、RV2, RV3, ... と増やすことで複数の値をいくらでも返すことができます。あと、コマンド置換よりはオーバーヘッドが小さいかもしれません(未確認)。 関数の中でグローバル変数を書き換える実際の例としてはリストの生成処理があります。ヒープにリスト用の領域を確保して、リストを指し示すポインタを返すようなイメージです。 new_list() { new_gid local self_=$RV1 GLOBAL[$self_]="" RV1=$self_ } new_list list1_=$RV1 Ruby で書くとしたらこんな感じ。 def new_list self_ = new_gid() # 新しいインデックスを採番 $GLOBAL[self_] = "" return self_ end list_ = new_list() というわけで、かんたんなコンパイラを書くことにより、Bashスクリプトにおける 文字列処理 データ構造(入れ子のあるリスト) 関数からの値の返却 についてのノウハウが新たに得られました。めでたしめでたし。 この記事を読んだ人は(たぶん)こちらも読んでいます

Viewing all articles
Browse latest Browse all 2811

Trending Articles