シェルスクリプト Advent Calendar 2021
https://qiita.com/advent-calendar/2021/shellscript
の20日目の記事です。
Bashがある環境ならどこでも自作言語で書いたプログラムをコンパイルできるようになりました。
https://github.com/sonota88/vm2gol-v2-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)
https://github.com/sonota88/vm2gol-v2
Bashスクリプト版のベースになっているバージョンは tag:62 のあたり
<自作言語処理系の説明用テンプレ>
自分がコンパイラ実装に入門するために作った素朴なトイ言語とその処理系です。簡単に概要を書くと下記のような感じ。
+
, *
, ==
, !=
のみ(優先順位は (
)
で明示)<説明用テンプレおわり>
下記の記事のおかげでなんとかなりました。ありがとうございます 🙏
シェルスクリプトはバイナリを扱えない。さてどうしよう…… - Qiita
バイト単位で16進数表現との相互変換さえできてしまえば、あとは煮るなり焼くなり思いのまま、です。
[
123, "fdsa",
[
11, "FDSA",
[]
]
]
のような、リストの要素として整数、文字列、リストを持たせることができる再帰的なデータ構造が必要です。Bashスクリプトの配列は単純な値を入れる分にはいいのですが、ネストしたリストを扱うことができなさそうでした。
(※ いつもならこういうことをやりたくなった段階で別の言語で書くことを検討しますが、今回は Bashスクリプトで書くこと自体が目的です)
そこで、ひとまず下記のようにしました。もっと良いやり方がある気がします。
リストの要素にあたるデータの表現はこんな感じ。
# 整数
"int:-123"
# 文字列
"str:fdsa"
# リスト
"list: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。
関数 myfunc
から標準出力に出力して result="$(myfunc)"
のようにコマンド置換で受け取る方法だと不都合な点が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スクリプトにおける
についてのノウハウが新たに得られました。めでたしめでたし。
Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。
また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!
こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?
コメント