Delphi で Brainf*ck を書いてみる

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

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

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

**See also:**

 - [Brainfuck (Wikipedia)](https://ja.wikipedia.org/wiki/Brainfuck)
 - [Delphi (Embarcadero)](https://www.embarcadero.com/jp/products/delphi)
 - [Delphi Community Edition (Embarcadero)](https://www.embarcadero.com/jp/products/delphi/starter)
 - [Delphi (Wikipedia)](https://ja.wikipedia.org/wiki/Delphi)

# コード

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:**

 - [Brainfuck (Wikipedia: en)](https://en.wikipedia.org/wiki/Brainfuck)
 - [Brainfuck の 履歴 (Wikipedia: en)](https://en.wikipedia.org/w/index.php?title=Brainfuck&oldid=239085)


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

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

```pascal: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`) を用意します。

```text:hello.bf
++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++.
```

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

```
> bf hello.bf
Hello World!
```

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


## ■ ポインタ未使用版

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

```pascal: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** と対象となる変数を指定する事で絶対アドレスを使用できるようになります。

```pascal: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 と同じアドレスにある<br>PAnsiChar 型のポインタ | ソースコードの位置を指すインストラクションポインタ |
| dp | PByte 型のポインタ |データの位置を示すデータポインタ|
| cd | dp と同じアドレスにある<br>PAnsiChar 型のポインタ |データの位置を示すデータポインタ|

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

**See also:**

 - [System.Chr (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/System.Chr)
 - [絶対アドレス (DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/%E5%A4%89%E6%95%B0%EF%BC%88Delphi%EF%BC%89#.E7.B5.B6.E5.AF.BE.E3.82.A2.E3.83.89.E3.83.AC.E3.82.B9)
 - [7.2. 可変レコード (レコードの可変部分) - <7> レコード型 (標準 Pascal 範囲内での Delphi 入門) (Qiita)](https://qiita.com/ht_deko/items/1d2c85768febc95009bf#72-%E5%8F%AF%E5%A4%89%E3%83%AC%E3%82%B3%E3%83%BC%E3%83%89-%E3%83%AC%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AE%E5%8F%AF%E5%A4%89%E9%83%A8%E5%88%86)

## ■ バイト配列未使用版

Wikipedia にある 

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

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

```pascal: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:**

 - [System.AnsiChar (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/System.AnsiChar)
 - [System.SysUtils.TBytes (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/System.SysUtils.TBytes)


## ■ 文字リテラル未使用版

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

```pascal: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()` を使ってみると...

```pascal: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:**

 - [System.Ord (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/System.Ord)


## ■ String (UnicodeString) 使用版

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

```pascal: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 個のヌル文字を用意しています。


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

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

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

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

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


**See also:**

 - [System.Char (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/System.Char)
 - [System.SetLength (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/System.SetLength)
 - [System.StringOfChar (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/System.StringOfChar)
 - [内部データ形式（Delphi）(DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/%E5%86%85%E9%83%A8%E3%83%87%E3%83%BC%E3%82%BF%E5%BD%A2%E5%BC%8F%EF%BC%88Delphi%EF%BC%89)
 - [各種文字列の実際 (ht-deko.com)](https://ht-deko.com/tech018.html)
 - [Delphi 文字列と NULL 終端文字列の混在 (DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/%E6%96%87%E5%AD%97%E5%88%97%E5%9E%8B%EF%BC%88Delphi%EF%BC%89#Delphi_.E6.96.87.E5.AD.97.E5.88.97.E3.81.A8_NULL_.E7.B5.82.E7.AB.AF.E6.96.87.E5.AD.97.E5.88.97.E3.81.AE.E6.B7.B7.E5.9C.A8)

# おわりに

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

**See also:**

 - [標準 Pascal 範囲内での Delphi 入門 (Qiita)](https://qiita.com/ht_deko/items/e2796788c65c2cda1de5)


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