Brainfuckのインタープリタを作ろう(1)

過去のブログのアーカイブ
この記事は前身のブログのアーカイブを引き継いだものです. 画像が正しく表示できないなど,コンテンツの表示に問題がある恐れがあります.

今回はとてもシンプルな言語のBrainfuck(以下「BF」)のインタープリタを作っていきたいと思います。BFとは8個しか命令のないとてもシンプルなプログラミング言語。それを動かすためのプログラム作りもとてもシンプルに実装できます。
なおインタープリタとは言語を動作させるためのプログラムです。C言語とかはコンパイラーで機械語にしてから実行しますが、インタープリタは機械語にせず実行するためのプログラムです。

必要なスキル

まず制作する前に必要なスキルは以下の通りです。

  • C#でコンソールアプリケーションの作り方を知っている
  • 条件分岐がわかる
  • BFの仕組みを一通り知っている

これだけで大丈夫です。それでは手順を追って進めていきたいと思います。
BFの仕様についてはこちらで紹介しています。

Brainf*ckの仕様と動作

2016.10.17

原型作り

スクリーンショット 2018-05-31 11.25.58
まずBFのプログラムが使用するメモリ(memory)が最低限必要です。このメモリはたくさんあればあるほどいいのですがここでは256個用意することにします。(+ -命令)
そして、今メモリのどこを指しているのかを記録するためのポインター(pointer)が必要です。(< >命令)
あとは実装の部分として文字の入出力(, . 命令)、そしてループ処理ですね( [ ] 命令)

早速実装してみよう

下準備

string source = Console.ReadLine();
int[] memory = new int[256];
int pointer = 0;

まずBrainfuckのコードを実行時に入力してもらうことにします。プログラムのコードは一般的にソース(source)と言われています。
そしてBF用のメモリ(memory)としてint型の256個の配列、そして今どこのメモリを指しているのかを記録するポインター(pointer)を作ります。

005.7. 配列を使おう

2016.07.26

ソースの読み込み&条件分岐

BFでは命令を1文字ずつ読み込んでいくため、ソースから1文字ずう読み込んでいく必要があります。
string型は文字列です。つまり言ってしまえばこれも文字の配列なのです。なので配列と同じような使い方で文字を取り出すことができます。

for (int i = 0; i < source.Length; i++)
{
    char c = source[i];
    // ...
}

文字型はcharです。なので取り出した文字はchar型の変数にいれてあげましょう。
そして条件分岐についてですが、条件分岐の基本は2つ。ifとswitchを使う方法です。if文は複雑な分岐に向いていて、switchは同じ対象物の比較でかつたくさん条件分岐がある場合に便利です。
今回は対象物がすべてcで、複雑なことはしないのでswitchを使いたいと思います。

switch (c)
{
    case '+':
          break;
     case '-':
          break;
     case '>':
          break;
     case '<':
          break;
     case '.':
          break;
     case ',':
          break;
     case '[':
          break;
     case ']':
          break;
}

このような感じで実装していきます。

メモリ操作を一通り実装する

では早速実装していきましょう。
+と-の命令はメモリの中身をインクリメント(1増やす)・デクリメント(1減らす)します。

// +命令
memory[pointer]++;
// -命令
memory[pointer]--;

このようになりますね。

ポインター操作

ポインターの操作はポインターの場所をインクリメント、デクリメントすればいいだけです。

// >命令
pointer++;
// <命令
pointer--;

文字の入出力

では次に文字の入出力です。ここで予め知っておいてほしいことは、「文字」というのはコンピュータ内では数字として扱われています。
つまり、文字から数字の変換、数字から文字の変換は普通のキャスト変換で実装できます。

008. 型を変換してみよう

2016.07.20
それでは実際に実装していきます。

// .命令
char output = (char)memory[pointer];
Console.Write(output);
// ,命令
char input = Console.ReadKey(true).KeyChar;
memory[pointer] = input;

まずは順番を追っていきたいと思います。
2行目(char)memory[pointer];では出力するメモリの値を文字に変換しています。
そしてその文字を3行目Console.Writeで出力しています。
普段はConsole.WriteLineを使う機会の方が多いと思いますが、WriteLineの方では毎回改行されてしまうので、ここではWriteの方を使っています。
次に文字の入力である,命令の実装ですが、ここはちょっと厄介になっています。
文字の入力といえばConsole.Readだと思いますが、今回は入力された文字を画面上に出したくなかったので、Console.ReadKey命令の方を使用しています。パラメータにtrueを入れると入力された文字を画面に出さないようにできます。なんとなくても分からないならここはスルーしていただいたほうが懸命だと思います。
その次に入力された文字(input)をメモリに格納する部分ですが、ここでは型の変換を行わなくてもメモリの中に入れることができます。理由は、char型よりint型のほうが扱える範囲が大きいからです。
この変換の詳細は008. 型を変換してみようを見てください。

まずは一旦これで実行してみよう

これでループ以外の命令ができました。なので試しにループを使わずにできるプログラムを動かして見ましょう。

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++.+.+.

このコードを動かして見てください。abcと出たら成功です。
スクリーンショット 2018-05-31 12.24.49
もしできなかった場合、ここまでのコードを置いておきますので自分のコードと比較してみてください。

static void Main(string[] args)
{
    string source = Console.ReadLine();
    int[] memory = new int[256];
    int pointer = 0;
    for (int i = 0; i < source.Length; i++)
    {
        char c = source[i];
        switch (c)
        {
            case '+':
                memory[pointer]++;
                break;
            case '-':
                memory[pointer]--;
                break;
            case '>':
                pointer++;
                break;
            case '<':
                pointer--;
                break;
            case '.':
                char output = (char)memory[pointer];
                Console.Write(output);
                break;
            case ',':
                char input = Console.ReadKey(true).KeyChar;
                memory[pointer] = input;
                break;
        }
    }
}

その2へ続く

Brainfuckのインタープリタを作ろう(2)

2018.05.31