亀岡的プログラマ日記

京都のベッドタウン、亀岡よりだらだらとお送りいたします。

TDDでだらだら作るBrainFuckコンパイラ その1

というわけで、TDDの個人的練習として、BrainFuckの実行環境を作っていきましょう。とはいえ、いきなりコードを書くわけじゃあないです。最初にざっくりとしたインタフェースを固めておかないとどうしようも無いです。これはTDDの基本ですね!

ざっくり仕様を固める

まず、かなりざっくりと仕様を固めておきましょう。BrainFuckの言語仕様そのものは変わらないのですが、まずはそれをどういった形態で提供するか。それだけで単体のexeなのか、dllなのか、exeならコンソールなら、GUIを持つのか・・・

コンパイラっぽさならコンソールアプリが常道なんでしょうが、テストのしやすさとポータビリティから、コンパイラそのものはdllとして実装しましょう。というより、1クラスにまとめるイメージです。

まず、初期化はコンストラクタで行うことにしましょう。入力文字列があるイメージです。それから現在のポインタ値と、ポインタアドレスは見れたほうが良いでしょう。コンパイラだし、テストしやすいし。
それから入出力がプログラムが呼ぶ形で行われます。ここはプログラムからのリクエストを受ける、というふうに捉えてイベントで実装することにしましょう。
後はロードされたプログラムに実行ですが、コンストラクタですぐ走らせちゃうとイベント登録の暇がなくなっちゃうので(コンストラクタで渡してもいいのですが)、Runメソッドでも作ってコールすることにしましょう。
というわけでザクッとした外部仕様として、

  1. クラスに文字列を渡して初期化してやると、それをbrainfuckの入力として実行する
  2. 現在のポインタ値とポインタアドレスを取得できるプロパティを持つ
  3. 入出力はクラスからのイベント発火とする。
  4. ロードしたプログラムはRunメソッドで実行する。

では、実装。

とりあえず、これで進めてみましょう。細かい仕様が変わったらその時考え直す方向で。上記のインタフェースに従うように空っぽのクラスを作ります。本当はテストクラスから書き始めるんでしょうが、まぁそこまで厳密にやるつもりも無いので。

    public class BrainFuckCompiler
	{
		public byte CurrentValue 
		{
			get { throw new NotImplementedException(); }
		}

		public uint ProgramPointer 
		{
			get { throw new NotImplementedException(); }
		}
		
		public BrainFuckCompiler(string inputString)
		{
			
		}
		
		public event Action<byte> OutputEvent;
		public event Func<byte> InputEvent;
         }

とりあえず、コンパイルは通ります。警告はでますけどね。

では、ここにテストコードを追加していきましょう。まずは簡単な仕様としてポインタ値のインクリメント/デクリメントを確認しましょうか。えーと、まずはポインタをインクリメントするテストを書いてみます。

SharpDevelopでは、テストプロジェクトはないものの、テスト用のクラスのテンプレートは標準で搭載されています。
f:id:posaunehm:20120213224709p:image
左下に注目。

なので、テスト用のプロジェクトだけは作って、そこからテスト用クラスを追加しておきましょう。

では、まずポインタ値のインクリメントテストを。こんな感じですね。個人的に単体テストは日本語で積極的に書く派です。

		[Test]
		public void ポインタインクリメントテスト()
		{
			var compiler = new BrainFuckCompiler.BrainFuckCompiler(">");
			compiler.Run();
			Assert.AreEqual(compiler.ProgramPointer, 1);
		}

さて、個人的にAssert.XXXメソッドはなんか好きじゃないです。なんかまどろっこしいですよね。そんなわけで、シンプルに書けるようにしましょう。テストコードの可読性を上げシンプルにするにはChainning Assertionを激しくオススメします。これは一度入れてしまうと外せなくレベル。というわけでChainnnig Assertionをテストコードに追加して、次行ってみましょう。

		[Test]
		public void ポインタインクリメントテスト()
		{
			var compiler = new BrainFuckCompiler.BrainFuckCompiler(">");
			compiler.Run();
			compiler.ProgramPointer.Is(1);
		}

うんうん、これは気持ちいい。
さて、ここでSharpDevelopの素敵なところですが、標準でNUnitのテストランナーがくっついています。こんな感じで!
f:id:posaunehm:20120213230407p:image

手軽でいいですね、SharpDevelop。
さて、じゃあこのRedを消していきましょう。まずはコンパイラで受け取った文字列を適当なフィールド変数に組み込んでおかないといけません。それから文字を1つずつ見ていって、ポインタインクリメントの”>”だったらポインタをインクリメントしておきましょう。こんな実装でいいはず。

		public BrainFuckCompiler(string inputString)
		{
			_input = inputString;
		}
		public void Run()
		{
			foreach(var token in _input)
			{
				switch (token)
				{
					case '>':
						_programPointer++;
						break;
					default:
						break;
				}
			}
		}

うまくいかなきゃ、考えなおしましょう。

では、この調子で逆方向の”<”も実装できますね。
テストコードが、

		[Test]
		public void ポインタデクリメントテスト()
		{
			var compiler = new BrainFuckCompiler.BrainFuckCompiler("<");
			compiler.Run();
			compiler.ProgramPointer.Is(-1);
		}

実装は、Runメソッドが

		public void Run()
		{
			foreach(var token in _input)
			{
				switch (token)
				{
					case '>':
						_programPointer++;
						break;
					case '<':
						_programPointer--;
						break;
					default:
						break;
				}
			}
		}

よしよし、グリーンです。んじゃ次は、ってちょっとまてい。ポインタがマイナス???
おかしいですね、これは。これは仕様の見落としです。もしここで気づかなくても、次にポインタ値を読み出すときのテストコードで判明していたでしょう。テストコードで実際に動かしてみると色々と気づくもんです、これは本当。

さてさて、そいじゃ外部設計を考えなおしましょう。ポインタが負になってしまった場合・・・なので、まぁOutOfRange系のExceptionですかね。ではまず、テストコードを書きます。NUnitではこう書けるんですよね!

		[Test]
		public void ポインタデクリメント例外テスト()
		{
			var compiler = new BrainFuckCompiler.BrainFuckCompiler("<");
			Assert.Throws<IndexOutOfRangeException>(
				() => compiler.Run());
		}

また、元々テストしたかったデクリメントはこう書きなおしておきます。インクリメントはちゃんとテストしてますしね!

		[Test]
		public void ポインタデクリメントテスト()
		{
			var compiler = new BrainFuckCompiler.BrainFuckCompiler(">><");
			compiler.Run();
			compiler.ProgramPointer.Is(1);
		}

そんなこんなで、ポインタインクリメント/デクリメントはとりあえず以下の実装ですかね。

		public void Run()
		{
			foreach(var token in _input)
			{
				switch (token)
				{
					case '>':
						_programPointer++;
						break;
					case '<':
						if(_programPointer <= 0)
						{
							throw new IndexOutOfRangeException();
						}
						_programPointer--;
						break;
					default:
						break;
				}
			}
		}

とりあえず今日はこの辺。まったりいきましょうまったり。