SilverlightでBrainf*ckとかMisaとか実装してみた

Brainf*ckの処理系書かせると結構いい勉強になるかなと思って。
この土日で書いてみました。
Brainfuckというのは、8種類の語句のみで成り立つ言語です。
詳しくはWikipediaの当該項目参照ということで。難読だけど、単純ゆえに組みがいがあるものです。
ちなみに本文ではそれの亜種の「プログラミング言語Misa」にしたバージョンで説明してます。
といっても、ちょこっとだけ処理を変えただけだけどね。

ちょっと長くなるんでー。

最初の宣言はこんなかんじです。
といっても、デフォルトに「System.Collections.Generic」足しただけだけど。

using System;
using System.Collections.Generic;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

処理系の本体は、別にクラスを作りました。(bfmain.cs)

namespace brainfuk
{

    // 本体
    public class bfmain
    {
        // デリゲート
        public delegate void doRunningCB(string s);
        public delegate void doOutputCB(string s);

        // クラス内変数
        private int[]      store   ;  // 配列(30,000)
        private int        stoptr  ;  // スタックポインタ
        private List<int>  stJpPtr ;  // ジャンプテーブル(開始点)
        private List<int>  edJpPtr ;  // ジャンプテーブル(終了点)
        private List<int>  runCode ;  // 実行コード
        private int        pgmCtr  ;  // プログラムカウンタ(現在実行位置)
        private Stack<int> jpStack ;  // ジャンプテーブル生成用スタック

いきなりデリゲートの宣言をぶちかましてますが、これは「実際の出力(doOutputCB)」「デバッグ用出力(doRunningCB)」をTextBoxにさせるためにやっております(サンプルではdoRunningCBは省略してます)
こういう使い方が適当かどうかは(私には)ちょっとわかりませんが、経験的にこれはこう使うべきかも、と判断したので。
間違ってたらツッコミ御願いします><

あと、ここで「はっ!?」と思った人は流石かなと思います。
ListとStackで、ね。
Listは可変長配列。追加したければpushすればOK、みたいな。
Stackは文字通りスタック。LIFO。

ちなみに、Sliverlightでは(よくC#の可変長配列で使われる)ArrayListは使えません。
ここは気をつけてね!
※ソリューションエクスプローラで参照設定→systemを選んで、「パス」のところを比べるとわかると思います。C#とSliverlightとで違ってきます。

ま、List使えば全然no problemだったりします。
要素追加は、こう。

runCode.Add(1);

読み出すときは、

a = runCode[2];

Stackですが、文字通りpushとpopです。
積むとき、

jpStack.Push(stjp);

取り出すとき、

edjp = jpStack.Pop();

今回の実装は、最初に構文解析をして、命令セット配列に入れてから、その次に実際に実行、というかたちをとっています。
その初期化部分はこうです。

        // 実行
        public void run(string srccstr, doOutputCB outcb, doRunningCB runcb)
        {
            string w_srcstr = srccstr ;
            string debug_out = "" ;
            char[] w_pickstr ;
            int srclen = srccstr.Length;
            int srcptr = 0 ;
            int fwpt = 0;
            int currunpt = 0;
            int pcnt = 0;

        // 定数
            string[] Cmd_FwdPtr = { ">","→","~","〜","ー"};  // 01
            string[] Cmd_RevPtr = { "<","←","☆","★"}     ;  // 02
            string[] Cmd_AddPtr = { "+","あ","ぁ","お","ぉ"};  // 03
            string[] Cmd_DecPtr = { "-","っ","ッ"}          ;  // 04
            string[] Cmd_PutChr = { ".","!","!"}           ;  // 05
            string[] Cmd_SetChr = { ",","?","?"}           ;  // 06 nn (setする文字)
            string[] Cmd_BgnLop = { "[","「","『"}          ;  // 07 nn (ジャンプテーブル)
            string[] Cmd_EndLop = { "]","」","』"}          ;  // 08 nn (ジャンプテーブル)

いっぱいあるので、配列に入れて比較する手法です。
ちなみに「〜」が2つあるように見えますが、これはUnicodeの「波ダッシュ」問題対策です。2つとも違う文字コードです。
革命の日々! それは典型的な波ダッシュ問題ではあるまいかあたりが参考になると思います。

            while (srcptr <= srclen)
            {
                pcnt = runCode.Count;
                // 比較してコードリストへ入れる
                if ((fwpt = compareComs(Cmd_FwdPtr, w_srcstr)) > 0)
                {
                    runCode.Add(1);
                }
                if (fwpt==0&&(fwpt = compareComs(Cmd_RevPtr, w_srcstr)) > 0)
                {
                    runCode.Add(2);
                }
                if (fwpt==0&&(fwpt = compareComs(Cmd_AddPtr, w_srcstr)) > 0)
                {
                    runCode.Add(3);
                }
                if (fwpt==0&&(fwpt = compareComs(Cmd_DecPtr, w_srcstr)) > 0)
                {
                    runCode.Add(4);
                }
                if (fwpt==0&&(fwpt = compareComs(Cmd_PutChr, w_srcstr)) > 0)
                {
                    runCode.Add(5);
                }
                if (fwpt==0&&(fwpt = compareComs(Cmd_SetChr, w_srcstr)) > 0)
                {
                    runCode.Add(6);
                    w_pickstr = w_srcstr.Substring(fwpt, 1).ToCharArray() ;
                    fwpt++;
                    runCode.Add((int)w_pickstr[0]);
                }
                if (fwpt==0&&(fwpt = compareComs(Cmd_BgnLop, w_srcstr)) > 0)
                {
                    runCode.Add(7);
                    int stjp;
                    stJpPtr.Add(runCode.Count+1);
                    runCode.Add( ( stjp = stJpPtr.Count) );
                    jpStack.Push(stjp);
                }
                if (fwpt==0&&(fwpt = compareComs(Cmd_EndLop, w_srcstr)) > 0)
                {
                    runCode.Add(8);
                    int edjp;
                    edJpPtr.Add(runCode.Count+1);
                    edjp = jpStack.Pop();
                    runCode.Add(edjp);
                }
                // 次の文字へ進める
                if (w_srcstr.Length == 0) break;
                if (fwpt > 0)
                {
                    w_srcstr = w_srcstr.Substring(fwpt);
                    srcptr += fwpt;
                }
                else
                {
                    w_srcstr = w_srcstr.Substring(1);
                    srcptr++;
                }
            }
            runCode.Add(255); // end code

括弧対応の処理で、前述のStackを使用しております。
ループの開始点と終了点をListに入れています。
そして、ループ命令のところで、Listの要素番号を入れるようにしてます。

ちなみに、比較で使用してる関数(クラスメンバー) compareComs は以下のように定義しています。

        private int compareComs(string[] chkSrc, string chkTarget)
        {
            int stlen;
            string picked;
            foreach (string chk in chkSrc)
            {
                stlen = chk.Length;
                if (stlen > chkTarget.Length) continue;
                picked = chkTarget.Substring(0, stlen);
                if (chk == picked) return stlen;
            }
            return 0;
        }

1文字じゃなくて任意長の文字に対応してます。
そういうわけで、定義さえ変えればジョジョ言語も可能です :D

実行部分はこうなってます。

            int curcode = 0;
            int nextPgmCtr=0;
            while (curcode != 255 || pgmCtr > runCode.Count)
            {
                curcode = runCode[pgmCtr] ;
                switch (curcode)
                {
                    case 1:
                        doFwdPtr();
                        pgmCtr++;
                        break;
                    case 2:
                        doRevPtr();
                        pgmCtr++;
                        break;
                    case 3:
                        doAddPtr();
                        pgmCtr++;
                        break;
                    case 4:
                        doDecPtr();
                        pgmCtr++;
                        break;
                    case 5:
                        doPutChr(outcb);
                        pgmCtr++;
                        break;
                    case 6:
                        doSetChr(runCode[++pgmCtr]);
                        pgmCtr++;
                        break;
                    case 7:
                        nextPgmCtr = doBgnLop(runCode[++pgmCtr]);
                        if (nextPgmCtr > 0) pgmCtr = nextPgmCtr;
                        else pgmCtr++;
                        break;
                    case 8:
                        nextPgmCtr = doEndLop(runCode[++pgmCtr]);
                        if (nextPgmCtr > 0) pgmCtr = nextPgmCtr;
                        else pgmCtr++;
                        break;
                    default:
                        pgmCtr++;
                        break;
                }

単にswitch〜caseでわけてるだけです。ちょっとつまんないかな。
ちなみにループ処理関数で0が返ってきたら判定は「偽(false)」扱いです。数値が返ってきたらそれは「真(true)」扱いで、内容は次のプログラムカウンタが入ります。
それぞれの処理関数はこう書いてます。

        // 各コマンドごとの処理
        // FwdPTR : ポインタを1つ進める
        private void doFwdPtr()
        {
            if ((++stoptr) >= 30000) stoptr = 29999 ;
        }

        // RevPTR : ポインタを1つ戻す
        private void doRevPtr()
        {
            if ((--stoptr) < 0) stoptr = 0;
        }

        // AddPTR : ポインタの示すスタックの値を1たす
        private void doAddPtr()
        {
            ++store[stoptr];
        }

        // DecPTR : ポインタの示すスタックの値を1へらす
        private void doDecPtr()
        {
            --store[stoptr];
        }

        // PutCHR : ポインタの示すスタックの値(文字)を出す
        private void doPutChr(doOutputCB outcb)
        {
            char outchara;
            outchara = (char)(store[stoptr]);
            string outtext;
            outtext = "" + outchara;
            outcb(outtext);
        }

        // SetCHR : ポインタへ次の文字をセットする
        private void doSetChr(int setchr)
        {
            store[stoptr] = setchr;
        }

        // BgnLop : ループの開始
        private int doBgnLop(int loopent)
        {
            int endpc = 0 ;
            if (store[stoptr] == 0)  endpc = edJpPtr[loopent-1];
            return endpc;
        }

        // EndLop : ループの終了
        private int doEndLop(int loopent)
        {
            int bgnpc = 0;
            if (store[stoptr] != 0) bgnpc = stJpPtr[loopent-1];
            return bgnpc;
        }

そして、これを呼び出してる本体(MainPage.xaml.cs)ではこうなってます。

        private void runexec(object sender, RoutedEventArgs e)
        {

            textBoxOut.Text= "";
            textBoxDbg.Text = "";

            bfmain bf = new bfmain () ;
            bfmain.doOutputCB sout_p = new bfmain.doOutputCB( showOutput );
            bfmain.doRunningCB sout_d = new bfmain.doRunningCB( showDebug );
            bf.run( textBoxSrc.Text , sout_p , sout_d );

        }

        // for delegate
        public void showOutput(string outdata)
        {
            string tout = textBoxOut.Text;
            textBoxOut.Text = tout + outdata;
            this.UpdateLayout();
        }
        public void showDebug(string outdata)
        {
            string tout = textBoxDbg.Text;
            textBoxDbg.Text = tout + outdata;
            this.UpdateLayout();
        }

そして最後に、デリゲートについて。
デリゲート(delegate)ってのは、辞書でひっぱればわかるように「委任」「代理」という言葉に相当します。
つまり、(指定した)実行処理をまかせちゃう、ということ。
このへん、実はC#の1つの重要ポイントだと思います。
処理系として独立させて、表示だけこうさせる、って使い方ですが。

ただ。
C#の哲学的にこれであってるのか・・・ちょっと心配。
違ってたらごめんなさい!

あ。
今回作ったサンプルをupしました。
http://ktkr.sakuratan.com/dev2/misa/
暴走対策なんてやってないので、自己責任で!!!
奇特な人向けに・・・プロジェクトファイルのダウンロードはこちらより。
ちなみにVS2010のβ2で作りました。RC入れ直さないとね・・・。

最後に。

今回作るにあたって参考にさせていただきました。
C# によるプログラミング入門

あと、個人的に衝撃受けたのはこれ。
とある画像の自動生成<ジェネレータ>

Comments are closed.