Brainfuck風のきわどい言語「NKOLANG」を作ってみた

巷で話題になったNKODICEというチンチロリン風ゲームにインスピレーションを受けて、Brainfuck風のプログラミング言語（インタープリタ）名付けて「NKOLANG」を作りました。

https://github.com/ys-j/NKOLANG

# 概要
Brainfuckという言語は`>`，`<`，`+`，`-`，`[`，`]`，`.`，`,`の8種類の文字（命令）のみから構成されるチューリング完全なプログラミング言語です。1つの長い配列についてポインタを操作するだけです。

NKOLANGではこれら8文字に対して、NKODICE本家様から`う`，`お`，`こ`，`ち`，`ま`，`ん`の6文字、および独自に`ぽ`，`！`の2文字を割り当て、Brainfuck風の言語としました。対応は下表の通りです。

| Brainfuck | NKOLANG | 説明 |
| --------- | ------- | ---- |
| > | こ | ポインタをインクリメントする |
| < | う | ポインタをデクリメントする |
| + | ち | ポインタの指す値をインクリメントする |
| - | ん | ポインタの指す値をデクリメントする |
| [ | お | ポインタの指す値が0ならば対応する「ま」の直後へ移動する |
| ] | ま | 対応する「お」へ移動する |
| . | ぽ | ポインタの指す値を文字として出力する |
| , | ！ | 入力した文字をポインタの指す値へ格納する |

普通に書く分にはおそらく不適切な単語は出現しません。例えば`ちん`という文字列はBrainfuckでは`+-`という命令で、値をインクリメントしてデクリメントするという無意味な命令になりますので、普通に書く分では出てきません。動揺に`まん`という文字列もBrainfuckでは`]-`（値が0の状態でループを抜けてさらに値をデクリメントする）という無意味な命令ですので、同様です。

おそらくあまり上品でない命令では💩こがたまに出る程度だと思われます。（他の言語ではいくらでも下品なコメントを挿入できるため、それができないという意味では他の言語よりもお上品に仕上がっています。）

# 例
```brainfuck:hello.bf
+[-->-[>>+>-----<<]<--<---]>-.>>>+.>>..+++[.>]<<<<.+++.------.<<-.>>>>+.
```

```nkolang:hello.nko
こちちちちちちちちちおうちちちちちちちちこんまうぽこちちちちちちちおうちちちちこんまうちぽちちちちちちちぽぽちちちぽここ
こちちちちちちちちおうちちちちこんまうぽこここちちちちちちちちちちおうちちちちちちちちちこんまうんんんぽううううぽちちち
ぽんんんんんんぽんんんんんんんんぽここちぽ
```

```
> Hello, world!
```

※日本語の出力ももちろん可能ですが、コードポイントは実際に動作する環境に依存します。（Windowsであれば通常Shift JIS（CP932）です。）

# インタープリタの解説

概念はBrainfuckと同じなので省略します。

ちなみに「NKOLANG」ではファイルサイズと実装のしやすさの観点からUTF-16を採用しています。

| 採用 | 種類 | アルファベット | ひらがな | 良いところ |
| :-: | :-: | :-: | :-: | :-- |
| | UTF-8 | 1バイト | 3バイト | 一般的ではある<br>ASCII互換 |
| ○ | UTF-16 | 2バイト | 2バイト | 固定長なのでポインタ操作が楽<br>ひらがな中心であればサイズ小 |

※「NKOLANG」の製作で初めてC++に触ったので書き方が変なところもあると思います。修正した方がいい部分があればコメントをくださると幸いです。


```cpp:main.cpp
#include <iostream>
#include <fstream>
#include <cstring>
#include <sstream>
#include <stack>
using namespace std;

// インタープリタだけならプレーンなenumで可
// 2バイトなのでshortでOK
enum class NkoCommand : unsigned short {
	Right = u'こ',
	Left = u'う',
	Increment = u'ち',
	Decrement = u'ん',
	LoopBegin = u'お',
	LoopEnd = u'ま',
	Output = u'ぽ',
	Input = u'！',
};
constexpr unsigned char BYTE_LEN = 2;

/*
  本体
*/
int run(string file_path, unsigned int memory_size) {

	unsigned char memory[memory_size];  // 1マス1バイト(char)がわかりやすいと思います
	unsigned int ptr = 0;
	unsigned int code_ptr = 0;
	unsigned int code_len = 0;
	memset(memory, 0, sizeof(memory));

	stack<int> loops;

	ifstream file(file_path);
	if (!file) {
		cerr << "Error: " << file_path << " cannot be opened." << endl;
		return -1;
	}

	stringstream buffer;
	buffer << file.rdbuf();
	string code(buffer.str());
	code_len = code.size();

	while (code_ptr < code_len) {
		// リトルエンディアン前提で2バイトずつ読みます
		unsigned char upper_char = code[code_ptr + 1];
		unsigned char lower_char = code[code_ptr];

		// 反switch勢もいますがパフォーマンスは優秀です
		switch (upper_char << 8 | lower_char) {
			// プレーンなenumならstatic_cast不要です
			case static_cast<unsigned short>(NkoCommand::Increment):
				memory[ptr]++;
				break;
			case static_cast<unsigned short>(NkoCommand::Decrement):
				memory[ptr]--;
				break;
			case static_cast<unsigned short>(NkoCommand::Right):
				// 反3項演算子勢もいますがネストしなければわかりやすくて自分は好きです
				// ポインタ位置がメモリサイズを超えたら0へジャンプ
				ptr = (ptr >= memory_size - 1) ? 0 : ptr + 1;
				break;
			case static_cast<unsigned short>(NkoCommand::Left):
				// ポインタ位置が0より左に行こうとしたら1番右へジャンプ
				ptr = (ptr <= 0) ? memory_size - 1 : ptr - 1;
				break;
			case static_cast<unsigned short>(NkoCommand::LoopBegin):
				loops.push(code_ptr);  // ループ開始位置をスタックへ保存
				// 値が0以外のときはそのまま次へ
				// 0のとき
				if (memory[ptr] == 0) {
					int depth = 1;  // ループの深さ
					while (depth > 0) {
						code_ptr += BYTE_LEN;
						// ループ中にファイルの末尾まで来たらエラー
						if (code_ptr >= code_len){
							cerr << "Error: Lack loop-end." << endl;
							return -1;
						}
						// 次の文字
						unsigned char _upper_char = code[code_ptr + 1];
						unsigned char _lower_char = code[code_ptr];
						switch (_upper_char << 8 | _lower_char) {
							case static_cast<unsigned short>(NkoCommand::LoopBegin):
								depth++;  // ループの深さ+1
								break;
							case static_cast<unsigned short>(NkoCommand::LoopEnd):
								depth--;  // ループの深さ-1
								break;
						}
					}
					loops.pop();  // スタックから除去
				}
				break;
			case static_cast<unsigned short>(NkoCommand::LoopEnd):
				if (loops.empty()) {
					cerr << "Error: Lack loop-begin." << endl;
					return -1;
				}
				// ポインタ位置をスタックから取得
				code_ptr = loops.top() - BYTE_LEN;
				loops.pop();
				break;
			case static_cast<unsigned short>(NkoCommand::Output):
				putchar(memory[ptr]);  // 出力
				break;
			case static_cast<unsigned short>(NkoCommand::Input):
				memory[ptr] = getchar();  // 入力
				break;
		}
		// 次の文字へ
		code_ptr += BYTE_LEN;
	}

	return 0;
}


int main(int argc, char* argv[]) {
	// 省略
}

```

このほか、公開中のコードにはBrainfuckとの相互変換機能も書いています。

# 参考

https://store.steampowered.com/app/1510950/NKODICE/

https://rightcode.co.jp/blog/information-technology/brainfuck-learn-cpp-basics

https://sites.google.com/site/nssvdabb/brainfuck