3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Delphi で Brainf*ck を書いてみる

Last updated at Posted at 2020-08-22

はじめに

Delphi で Brainf*ck を書いてみる事にしました。

Brainf*ck は以前にも書いた事があるのですが、GUI 版やっつけ CUI 版 (GUI 版の流用) だったので、改めてコンソールアプリケーションとして書いてみます。コマンドラインパラメータとして渡されたファイルをソースコードとして処理します。

Delphi 10.4 Sydney で書いていますが、10.3 Rio 以降なら大丈夫だと思います。

See also:

コード

Brainf*ck の基本的な仕様は Wikipedia に準じる事にします。

Char Meaning
> increment the data pointer (to point to the next cell to the right).
< decrement the data pointer (to point to the next cell to the left).
+ increment (increase by one) the byte at the data pointer.
- decrement (decrease by one) the byte at the data pointer.
. output the byte at the data pointer.
, accept one byte of input, storing its value in the byte at the data pointer.
[ if the byte at the data pointer is zero, then instead of moving the instruction pointer forward to the next command, jump it forward to the command after the matching ] command.
] if the byte at the data pointer is nonzero, then instead of moving the instruction pointer forward to the next command, jump it back to the command after the matching [ command.

] の仕様にあるデータポインタが非ゼロだったらというのはいつから追加されたんでしょうね?古い仕様だと次のようになっていて、以前書いたコードも古い仕様に則っていました。

Char Meaning
> increment the pointer.
< decrement the pointer.
+ increment the byte at the pointer.
- decrement the byte at the pointer.
. output the byte at the pointer.
, take byte input and store it in the byte at the pointer.
[ jump forward to the statement after the corresponding ] if the byte at the pointer is zero.
] jump back to the corresponding [.

See also:

■ とりあえず書いてみた版

とりあえず書いてみました。

bf.dpr
program bf;
{$APPTYPE CONSOLE}

uses
  System.SysUtils, System.IOUtils;

begin
  if not TFile.Exists(ParamStr(1)) then Exit;
  var src := TFile.ReadAllBytes(ParamStr(1));
  var buf: TBytes;
  SetLength(buf, 30000);
  var ip: PByte := @src[0];
  var dp: PByte := @buf[0];
  repeat
    case Chr(ip^) of
      '>': Inc(dp);
      '<': Dec(dp);
      '+': Inc(dp^);
      '-': Dec(dp^);
      '.': Write(Chr(dp^));
      ',': Read(PAnsiChar(dp)^);
      '[': if dp^ = 0 then
           begin
             var n := 1;
             repeat
               Inc(ip);
               if      Chr(ip^) = '[' then Inc(n)
               else if Chr(ip^) = ']' then Dec(n);
             until (Chr(ip^) = ']') and (n = 0);
           end;
      ']': if dp^ <> 0 then
           begin
             var n := 1;
             repeat
               Dec(ip);
               if      Chr(ip^) = ']' then Inc(n)
               else if Chr(ip^) = '[' then Dec(n);
             until (Chr(ip^) = '[') and (n = 0);
           end;
    end;
    Inc(ip);
  until (ip^ = 0);
end.

Brainf*ck のソースコード (hello.bf) を用意します。

hello.bf
++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++.

次のようにして Brainf*ck インタプリタを実行します。

> bf hello.bf
Hello World!

オーバーフロー&アンダーフローをチェックしていない、とてもシンプルな Brainf*ck インタプリタです。プラットフォーム固有の特殊なルーチンやクラスは使っていないので、macOS や Linux 向けにビルドしても動作すると思います。

■ ポインタ未使用版

Delphi (Pascal) にはポインタ型があり、Brainf*ck はポインタを使った方が簡潔に書けるのですが、ポインタを使わない書き方もできます。次の例ではポインタの代わりに添字付き変数 (Indexed Variable) と添字 (Index) 用変数を使っています。

bf.dpr
program bf;
{$APPTYPE CONSOLE}

uses
  System.SysUtils, System.IOUtils;

begin
  if not TFile.Exists(ParamStr(1)) then Exit;
  var src := TFile.ReadAllBytes(ParamStr(1));
  var buf: TBytes;
  SetLength(buf, 30000);
  var ip := 0;
  var dp := 0;
  repeat
    case Chr(src[ip]) of
      '>': Inc(dp);
      '<': Dec(dp);
      '+': Inc(buf[dp]);
      '-': Dec(buf[dp]);
      '.': Write(Chr(buf[dp]));
      ',': Read(AnsiChar(buf[dp]));
      '[': if buf[dp] = 0 then
           begin
             var n := 1;
             repeat
               Inc(ip);
               if      Chr(src[ip]) = '[' then Inc(n)
               else if Chr(src[ip]) = ']' then Dec(n);
             until (Chr(src[ip]) = ']') and (n = 0);
           end;
      ']': if buf[dp] <> 0 then
           begin
             var n := 1;
             repeat
               Dec(ip);
               if      Chr(src[ip]) = ']' then Inc(n)
               else if Chr(src[ip]) = '[' then Dec(n);
             until (Chr(src[ip]) = '[') and (n = 0);
           end;
    end;
    Inc(ip);
  until (src[ip] = 0);
end.

■ 型変換未使用版

Delphi (Pascal) は型にウルサイので Chr() による型変換を行う必要があり、少々コードが読みにくくなっていました。Chr() は型キャストではなく型変換関数です。先述のコードで型キャストするには Char() または AnsiChar() とする必要がありました。

型変換を使わなくてすむようにするには、絶対アドレス (Absolute Addresses) を使います。これは C 言語で言う共用体、Pascal で言う 可変レコードに類似した機能で、他の変数と同じアドレスを持つ新しい変数を作る機能です。

変数宣言時にキーワード absolute と対象となる変数を指定する事で絶対アドレスを使用できるようになります。

bf.dpr
program bf;
{$APPTYPE CONSOLE}

uses
  System.SysUtils, System.IOUtils;

var
  ip, dp: PByte;
  ci: PAnsiChar absolute ip;
  cd: PAnsiChar absolute dp;
begin
  if not TFile.Exists(ParamStr(1)) then Exit;
  var src := TFile.ReadAllBytes(ParamStr(1));
  var buf: TBytes;
  SetLength(buf, 30000);
  ip := @src[0];
  dp := @buf[0];
  repeat
    case ci^ of
      '>': Inc(dp);
      '<': Dec(dp);
      '+': Inc(dp^);
      '-': Dec(dp^);
      '.': Write(cd^);
      ',': Read(cd^);
      '[': if dp^ = 0 then
           begin
             var n := 1;
             repeat
               Inc(ip);
               if      ci^ = '[' then Inc(n)
               else if ci^ = ']' then Dec(n);
             until (ci^ = ']') and (n = 0);
           end;
      ']': if dp^ <> 0 then
           begin
             var n := 1;
             repeat
               Dec(ip);
               if      ci^ = ']' then Inc(n)
               else if ci^ = '[' then Dec(n);
             until (ci^ = '[') and (n = 0);
           end;
    end;
    Inc(ip);
  until (ip^ = 0);
end.
変数 意味 役割
ip PByte 型のポインタ ソースコードの位置を指すインストラクションポインタ
ci ip と同じアドレスにある
PAnsiChar 型のポインタ
ソースコードの位置を指すインストラクションポインタ
dp PByte 型のポインタ データの位置を示すデータポインタ
cd dp と同じアドレスにある
PAnsiChar 型のポインタ
データの位置を示すデータポインタ

こうすれば、ip^dp^ は Byte としてアクセスでき、ci^cd^ は AnsiChar としてアクセスできます。型変換や型キャストがないため、コードはスッキリしますが、絶対アドレスでのインライン変数宣言ができないため、var ブロックでの変数宣言を余儀なくされるのと、absolute の意味が解っていないと混乱しかねないのが欠点です。

See also:

■ バイト配列未使用版

Wikipedia にある

少なくとも30000個の要素を持つバイトの配列

に引きずられて動的バイト配列 (TBytes) を使うから処理が面倒になるのであって、8bit Char な AnsiChar の配列 (TArray<AnsiChar> = array of AnsiChar) を使えば簡潔に書けるのではないかと考えたのが次のコードです。

bf.dpr
program bf;
{$APPTYPE CONSOLE}

uses
  System.SysUtils, System.IOUtils;

begin
  if not TFile.Exists(ParamStr(1)) then Exit;
  var src, buf: TArray<AnsiChar>;
  TBytes(src) := TFile.ReadAllBytes(ParamStr(1));
  SetLength(buf, 30000);
  var ip: PAnsiChar := @src[0];
  var dp: PAnsiChar := @buf[0];
  repeat
    case ip^ of
      '>': Inc(dp);
      '<': Dec(dp);
      '+': Inc(dp^);
      '-': Dec(dp^);
      '.': Write(dp^);
      ',': Read(dp^);
      '[': if dp^ = #$00 then
           begin
             var n := 1;
             repeat
               Inc(ip);
               if      ip^ = '[' then Inc(n)
               else if ip^ = ']' then Dec(n);
             until (ip^ = ']') and (n = 0);
           end;
      ']': if dp^ <> #$00 then
           begin
             var n := 1;
             repeat
               Dec(ip);
               if      ip^ = ']' then Inc(n)
               else if ip^ = '[' then Dec(n);
             until (ip^ = '[') and (n = 0);
           end;
    end;
    Inc(ip);
  until (ip^ = #$00);
end.

See also:

■ 文字リテラル未使用版

文字リテラルをなくしてしまえば...

bf.dpr
program bf;
{$APPTYPE CONSOLE}

uses
  System.SysUtils, System.IOUtils;

begin
  if not TFile.Exists(ParamStr(1)) then Exit;
  var src := TFile.ReadAllBytes(ParamStr(1));
  var buf: TBytes;
  SetLength(buf, 30000);
  var ip: PByte := @src[0];
  var dp: PByte := @buf[0];
  repeat
    case ip^ of
{ > } $3E: Inc(dp);
{ < } $3C: Dec(dp);
{ + } $2B: Inc(dp^);
{ - } $2D: Dec(dp^);
{ . } $2E: Write(Chr(dp^));
{ , } $2C: Read(PAnsiChar(dp)^);
{ [ } $5B: if dp^ = 0 then
           begin
             var n := 1;
             repeat
               Inc(ip);
               if      ip^ = $5B { [ } then Inc(n)
               else if ip^ = $5D { ] } then Dec(n);
             until (ip^ = $5D { ] }) and (n = 0);
           end;
{ ] } $5D: if dp^ <> 0 then
           begin
             var n := 1;
             repeat
               Dec(ip);
               if      ip^ = $5D { ] } then Inc(n)
               else if ip^ = $5B { [ } then Dec(n);
             until (ip^ = $5B { [ }) and (n = 0);
           end;
    end;
    Inc(ip);
  until (ip^ = 0);
end.

コメントがないとどうしても解りづらくなりますね。

■ Ord() 使用版

順序値を返す変換関数 Ord() を使ってみると...

bf.dpr
program bf;
{$APPTYPE CONSOLE}

uses
  System.SysUtils, System.IOUtils;

begin
  if not TFile.Exists(ParamStr(1)) then Exit;
  var src := TFile.ReadAllBytes(ParamStr(1));
  var buf: TBytes;
  SetLength(buf, 30000);
  var ip: PByte := @src[0];
  var dp: PByte := @buf[0];
  repeat
    case ip^ of
      Ord('>'>): Inc(dp);
      Ord('<'>): Dec(dp);
      Ord('+'>): Inc(dp^);
      Ord('-'>): Dec(dp^);
      Ord('.'>): Write(Chr(dp^));
      Ord(','>): Read(PAnsiChar(dp)^);
      Ord('['>): if dp^ = 0 then
                begin
                  var n := 1;
                  repeat
                    Inc(ip);
                    if      ip^ = Ord('[') then Inc(n)
                    else if ip^ = Ord(']') then Dec(n);
                  until (ip^ = Ord(']')) and (n = 0);
                end;
      Ord(']'): if dp^ <> 0 then
                begin
                  var n := 1;
                  repeat
                    Dec(ip);
                    if      ip^ = Ord(']') then Inc(n)
                    else if ip^ = Ord('[') then Dec(n);
                  until (ip^ = Ord('[')) and (n = 0);
                end;
    end;
    Inc(ip);
  until (ip^ = 0);
end.

左辺と右辺のどちらに変換関数を持ってくるかの違いしかないですね。

See also:

■ String (UnicodeString) 使用版

8bit 配列に固執しなければバッファとして文字列 (String) が使えるのでとてもスッキリ書けます。

bf.dpr
program bf;
{$APPTYPE CONSOLE}

uses
  System.SysUtils, System.IOUtils;

begin
  if not TFile.Exists(ParamStr(1)) then Exit;
  var src := TFile.ReadAllText(ParamStr(1));
  var buf := StringOfChar(#$00, 30000);
  var ip := PChar(src);
  var dp := PChar(buf);
  repeat
    case ip^ of
      '>': Inc(dp);
      '<': Dec(dp);
      '+': Inc(dp^);
      '-': Dec(dp^);
      '.': Write(dp^);
      ',': Read(dp^);
      '[': if dp^ = #$00 then
           begin
             var n := 1;
             repeat
               Inc(ip);
               if      ip^ = '[' then Inc(n)
               else if ip^ = ']' then Dec(n);
             until (ip^ = ']') and (n = 0);
           end;
      ']': if dp^ <> #$00 then
           begin
             var n := 1;
             repeat
               Dec(ip);
               if      ip^ = ']' then Inc(n)
               else if ip^ = '[' then Dec(n);
             until (ip^ = '[') and (n = 0);
           end;
    end;
    Inc(ip);
  until (ip^ = #$00);
end.

String 型の要素は Unicode 版 Delphi だと Char (WideChar = 16bit Char) です。動的配列と異なり、String 型に対して SetLength() を使うと領域がゼロクリアされないため、StringOfChar() を使って連続した 30,000 個のヌル文字を用意しています。

また、ソースコード中の次の部分は、

  var ip := PChar(src);
  var dp := PChar(buf);

他のコードに倣ってこのように書く事もできます。

  var ip: PChar := @src[1];
  var dp: PChar := @buf[1];

添え字が 1 になっているのは Delphi の String が [1] からデータが格納される 1 ベース文字列だからです 1

See also:

おわりに

使っている手続き/関数の殆どは組み込みルーチンで、標準 Pascal にもあるようなルーチンしか使っていないので、コードを読むのはそんなに難しくないと思います。

See also:

  1. 10.3 Rio 以前のモバイルコンパイラは 0 ベース文字列でしたが、10.4 Sydney において、デスクトップコンパイラと同じ 1 ベース文字列に改められました。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?