この記事は EEICアドベントカレンダー2018 13日目の記事です。
自己紹介
こんにちは.EEIC2016のtellusiumと申します.
昨日は意味のわからない記事をアップしてしまい申し訳ありませんでした.
まさかの解読者が出たことに驚き(難易度という意味でなく)です…おそらく身内の方は「またか…」という反応をすると思ったので…
今回は輝夜月をモチーフにしたbrainfxckの派生言語,Luna言語(Luaじゃないよ)を作ってみました.
以下ではbrainfxck,輝夜月について,使う言葉の選定,処理系の製作について述べます.
brainfxckについて
brainfxckは非常にコンパクトながらチューリング完全である難解なプログラミング言語です.
命令は次の8つしかありません.
-
>
データポインタを1進める -
<
データポインタを1戻す -
+
データポインタの示す値を1増やす -
-
データポインタの示す値を1減らす -
.
データポインタの示す値を出力する -
,
入力された値をデータポインタの示すバイトに代入する -
[
データポインタの示す値が0ならば,対応する次の]
までジャンプする -
]
データポインタの示す値が0でなければ,対応する前の[
までジャンプする
この各命令の記号はなんでも良いため,ここに好きな言葉を割り当てることによって独自の派生言語を作成することができます.
探すと,いくつか製作事例が見つかります.
これらをみて輝夜月の派生言語作ってみたいなぁと思いつつ,調べると意外なことに誰もやっていないっぽいので作ってみたというのがこの記事になります.
輝夜月について
動画をみてください.
ついこの前の12/9に初の動画投稿から1周年を迎えました.
おめでとうございます🍤
こんなのもどうぞ(ZeppVRで行われたライブの感想です)
使う言葉を決める
正直ここが唯一のオリジナリティですね.
YouTubeの字幕やTwitterのツイートから取ってきた単語をカウントして多い順に…とか考えてはいたのですが,輝夜月らしい単語はそういった場所に出てきにくいだろうなぁとか考えた結果,ちょうどLINEスタンプが目に入ったので,ここから使えば間違いないだろうと考えそのように決めました.
(独断と偏見で)決まったのは次の通り.
-
「おはよー!こんちわー!こんばんわー!おやすみー!おきてええええええええ!!!」
- 言わずと知れた輝夜月と言えば,という挨拶(?).10月に行われたZepp VRライブでも参加者全員による唱和が行われた.
-
「みてみてこれエビ!」「それは竹」
- https://www.youtube.com/watch?v=4i1p0cMebMY&t=0s 内の企画にて用意されたスクショして使う画面において使われている言葉.「エビ」はこの後登場するゆるキャラ,ジャスティン・エビーバーの元になっている(はず).「竹」は輝夜月が竹から生まれたお姫様であることに由来していると思われる.
-
「ヘケッ☆」
- 初期に付けられた愛称「首絞めハ○太郎」由来.初出はこのツイート…?(違ってたらすみません)
LINEスタンプ以外からも「めずらし!喋れるゴリラだ!!」「うわぁぁぁぁぁぁぁぁぁぁぁぁ」など他にも候補があったのですが,長さや著名度などを考えると上記の8つの言葉に決めました.
以上より,Luna言語の命令は次の8つとなります.
一応処理のイメージと近い単語を選んでいるつもりです.
-
>
こんちわぁぁ -
<
こんばんはぁ -
+
おはよぉぉぉ -
-
ヘケッ☆ -
.
みてみてこれエビ! -
,
それは竹 -
[
おきてぇぇぇ -
]
おやすみぃぃぃ
brainfxck処理系と変換器
ようやく技術っぽい話に入りました.
処理系と呼ぶには小規模かもしれませんが,Luna言語で書かれたプログラムを入力してそれを実行するものを作ります.インタープリタという感じですね.
基本的にインタープリタは字句解析,構文解析…という流れで処理が行われますが,brainfxckでは順に実行していけばいいため構文解析ほどの解析は必要ありません.簡単ですね.
言語はPythonを使いました.以下に実装の簡単な流れを説明します.
字句解析
プログラムの文字列を解析して,構文の構成要素であるトークンに分解していくのが字句解析です.
まずトークンはEnum
を使い次のように表現しました.
class Inst(Enum):
INCP = 1
DECP = 2
INCV = 3
DECV = 4
OUTV = 5
INPV = 6
JMPIFZ = 7
RETIFNZ = 8
次に変換表となる辞書型のオブジェクトを用意します.
luna_dictionary = {
'こんちわぁぁ': Inst.INCP,
'こんばんはぁ': Inst.DECP,
'おはよぉぉぉ': Inst.INCV,
'ヘケッ☆': Inst.DECV,
'みてみてこれエビ!': Inst.OUTV,
'それは竹www': Inst.INPV,
'おきてぇぇぇ': Inst.JMPIFZ,
'おやすみぃぃぃ': Inst.RETIFNZ
}
以上を用いてLuna言語の文字列をトークンの配列に変換します.
一つの単語が別の単語の一要素となっている,などの現象は発生しないので,非常に簡単な解析ですみます.
def convert_luna_to_brainfxck(string):
p = 0
keys = luna_dictionary.keys()
key_lengths = list(set([len(key) for key in keys]))
instructions = []
while p < len(string):
try:
# 命令と一致する単語があるか
new_instruction = reduce(
lambda arr, length: arr + [string[p: p+length]] if string[p: p+length] in keys else arr, key_lengths, [])[0]
except:
# ない場合は1文字ずらす
p += 1
continue
instructions.append(luna_dictionary[new_instruction])
p += len(new_instruction)
return instructions
これでトークンの配列が得られたので,次に実行を行います.
実行に必要なのは次の3つになります.
- 命令(トークン配列)
- 先ほど作ったもの
- データポインタ
- データメモリの番地を表すポインタ
- アドレス
- 今どの命令を実行しているのかを表すポインタ
- (なくてもできそう)
- データメモリ
- データポインタや命令により操作する値の配列
ポインタとデータメモリはクラスとして次のように作成しました(概要のみ)
# アドレス,データポインタ兼用
class Pointer:
def __init__(self):
self.__p = 0
def inc(self):
self.__p += 1
def dec(self):
...
# メモリとその操作メソッド(ほぼ命令そのもの)を集めたクラス
class Interpreter:
def __init__(self):
self.memory = [0 for _ in range(100)]
def inc(self, p):
self.memory[p()] += 1
def dec(self, p):
self.memory[p()] -= 1
...
命令と処理の対応関係は辞書型で対応する処理を格納する形で表現しました.
どの処理も,インタプリタ,ポインタ,命令群,アドレスを共通の引数として持っています.
execute_dict = {
Inst.INCP: lambda intp, p, insts, a: p.inc(),
Inst.DECP: lambda intp, p, insts, a: p.dec(),
Inst.INCV: lambda intp, p, insts, a: intp.inc(p),
Inst.DECV: lambda intp, p, insts, a: intp.dec(p),
Inst.OUTV: lambda intp, p, insts, a: print(chr(intp.out(p)), end=''),
Inst.INPV: lambda intp, p, insts, a: intp.inp(p, ord(input())), # 入力文字の文字コードを得る(1文字の入力のみ可能,本来はinputでなく1文字のみ受け付けるメソッドを用意すべき)
Inst.JMPIFZ: lambda intp, p, insts, a: intp.jmpifz(p, insts, a),
Inst.RETIFNZ: lambda intp, p, insts, a: intp.retifnz(p, insts, a)
}
あとは実行するだけですね.
def execute(instructions):
addr = Pointer() # 現在実行している命令を示すアドレスポインタ,ほぼ同じデータ構造なので流用
p = Pointer() # 現在参照しているメモリを表すデータポインタ
interpreter = Interpreter()
while addr() < len(instructions):
try:
execution = execute_dict[instructions[addr()]]
execution(interpreter, p, instructions, addr)
addr.inc()
except:
print("Could not execute", sys.exc_info())
break
print()
return
ちなみに昨日あげた記事を実行すると
This is the article of the 13th day of the EEIC Advent Calendar.
The details will be available tomorrow.
という結果が得られます.
まとめ
輝夜月をイメージしたbrainfxck派生言語であるLuna言語を作ってみました.
コードそのものはエラー処理がかなりガバガバなので追加しなくちゃですね…
今回作ったコードはGitHubにアップしてあります.
python main.py luna.txt
のようにして実行することができます.
加えてbrainfxckからLuna言語に変換するスクリプトも作ってあるので,文字列を出力するbrainfxckを書いてくれるサービスなど使うと色々遊べます(昨日の記事もそのようにして作成しました)
おまけ
VTuberブームが来てから1年経ちましたね.
アドベントカレンダーの最初の方をみてると研究が辛かった時期を思い出します.
ちょうどVTuberブームが爆発している12月後半だったので(まさに1年前)配信される動画が気持ちを和らげてくれました(けれど研究に対する気持ちには勝てなかった…
画像が全くなかったので最後に自分のデスクの写真で締めたいと思います.
最後まで読んでいただきありがとうございました.