はじめに
Delphi で Brainf*ck を書いてみる事にしました。
Brainf*ck は以前にも書いた事があるのですが、GUI 版
と やっつけ CUI 版 (GUI 版の流用)
だったので、改めてコンソールアプリケーションとして書いてみます。コマンドラインパラメータとして渡されたファイルをソースコードとして処理します。
Delphi 10.4 Sydney で書いていますが、10.3 Rio 以降なら大丈夫だと思います。
See also:
- Brainfuck (Wikipedia)
- Delphi (Embarcadero)
- Delphi Community Edition (Embarcadero)
- Delphi (Wikipedia)
コード
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:
■ とりあえず書いてみた版
とりあえず書いてみました。
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
) を用意します。
++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++.
次のようにして Brainf*ck インタプリタを実行します。
> bf hello.bf
Hello World!
オーバーフロー&アンダーフローをチェックしていない、とてもシンプルな Brainf*ck インタプリタです。プラットフォーム固有の特殊なルーチンやクラスは使っていないので、macOS や Linux 向けにビルドしても動作すると思います。
■ ポインタ未使用版
Delphi (Pascal) にはポインタ型があり、Brainf*ck はポインタを使った方が簡潔に書けるのですが、ポインタを使わない書き方もできます。次の例ではポインタの代わりに添字付き変数 (Indexed Variable) と添字 (Index) 用変数を使っています。
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 と対象となる変数を指定する事で絶対アドレスを使用できるようになります。
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:
- System.Chr (DocWiki)
- 絶対アドレス (DocWiki)
- 7.2. 可変レコード (レコードの可変部分) - <7> レコード型 (標準 Pascal 範囲内での Delphi 入門) (Qiita)
■ バイト配列未使用版
Wikipedia にある
少なくとも30000個の要素を持つバイトの配列
に引きずられて動的バイト配列 (TBytes) を使うから処理が面倒になるのであって、8bit Char な AnsiChar の配列 (TArray<AnsiChar> = array of AnsiChar) を使えば簡潔に書けるのではないかと考えたのが次のコードです。
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:
■ 文字リテラル未使用版
文字リテラルをなくしてしまえば...
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()
を使ってみると...
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) が使えるのでとてもスッキリ書けます。
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:
- System.Char (DocWiki)
- System.SetLength (DocWiki)
- System.StringOfChar (DocWiki)
- 内部データ形式(Delphi)(DocWiki)
- 各種文字列の実際 (ht-deko.com)
- Delphi 文字列と NULL 終端文字列の混在 (DocWiki)
おわりに
使っている手続き/関数の殆どは組み込みルーチンで、標準 Pascal にもあるようなルーチンしか使っていないので、コードを読むのはそんなに難しくないと思います。
See also:
-
10.3 Rio 以前のモバイルコンパイラは 0 ベース文字列でしたが、10.4 Sydney において、デスクトップコンパイラと同じ 1 ベース文字列に改められました。 ↩