🤷‍♂️

Cと3だけで書けるプログラミング言語作ってみた

2023/01/03に公開

はじめに

この記事は2022年のアドベントカレンダーで私が書いたものを加筆修正したものです。

まずは実行してみよう

さて、Cと3だけで書けるプログラミング言語といってもおそらく何のことか分からないと思うので、とりあえずコードを見てみましょう。
Hello, World!のプログラムは次のようになります。

C3CC3CC3CC3CC3CC3CC3CC3CC3CC3C33CCCCC3CC3CC3CC3CC3CC3CC3CCCCC3CC3CC3CC3CC3CC3CC3CC3CC3CC3CCCCC3CC3CC3CC3CCCCC3CC3CC3CCCCC3CC3CC3CC3CC3CC3CC3CC3CC3CCCCC3CCC3CC3CC3CC3CC3CC3C33333CCCC3CC3C3CCCCCC3C3CCC3CC3CC3CC3CC3CC3CC3C3CC3CCC3CC3CC3C3CCCCCC3CC3CC3CC3C3CCCCCC3CC3C3CCCCCC33C33C333CCCC3CC3CC33CCC3CC3CC3C3CCC33C33C33C33C33C333CCC33C33C33C33C33C33C33C333CCCCCC33C33C33C33C33C33C33C33C33C33C333CCCCCCCCCCC3CC

このコードを自作のインタプリタで実行するとHello, World!が出力されます。
C言語で同じ処理を書くとこのようになります。

#include <stdio.h>

void main() {
    printf("Hello, World!");
}

また、出力の他に入力や演算にも対応しており、例えば次のコードは0を入力すると1を、1を入力すると0を出力するプログラムになります。

3C3CCCC3CCCCC3CC3CC3CC3CC3CC3CC3CC3C33CCC3CC3C33C33C33C33C33C33CCCCCCC33333CC3CC333CCCCCCCCCCC3CC3CC3CC3CC3CC3CC3CC3C33CCCCC3CC3CC3CC3CC3CC3CCC3C33333CCC3CCCC3CC3CC3C33CC3C33333CCC33CCCCCCCCCCCCCC3CC3CC3CC3CC3CC3CC3C33CCCCC3CC3CC3CC3CC3CC3CC3CCC3C33333CCC3CCCC3CC3CC3CC3CC3C33333

こちらもC言語で同じ処理を書くとこのようになります。

#include <stdio.h>

void main() {
    char c;
    scanf("%c", &c);
    c -= 48;
    if (c) printf("0");
    else printf("1");
}

これを見ていただければ、if文を用いた反転が実装できることが分かると思います。
コードがとんでもなく長くなってしまうので割愛しますが、論理演算や比較を行うことも可能です。では、次の章で言語の仕組みを解説していきます。

仕組みとか

少し話が変わりますが、Brainfuckという言語はご存じでしょうか。
この言語は>,<,+,-,.,,,[,]の8つの文字のみで書くことができる言語で、チューリング完全(メモリなどのリソースが無限であれば理論上どのようなコードでも実装できる)であることが知られています。
Brainfuckについて説明すると長くなるため、それぞれの文字がどのような命令を表すかだけを解説するとこのようになります。

Brainfuckの文字 命令の内容
> ポインタをインクリメントする。
< ポインタをデクリメントする。
+ ポインタが指す値をインクリメントする。
- ポインタが指す値をデクリメントする。
. ポインタが指す値を出力に書き出す。
, 入力から1バイト読み込んで、ポインタが指す先に代入する。
[ ポインタが指す値が0なら、対応する]の直後にジャンプする。
] ポインタが指す値が0でないなら、対応する[の直後にジャンプする。

ポインタの話だらけで何を言っているか分からないという人もいると思いますが、この8つの命令が実装されていればどんなプログラムでも書けるということだけ分かっていただければ大丈夫です。
また、この言語はその仕様上、インタプリタの実装が他の言語と比較して極めて容易です。
ところで、8って2の3乗ですよね?
ということは、Brainfuckの1つの命令を2種類の文字を用いて構成される長さ3の文字列に置き換えると、簡単にCと3だけで書くことができるプログラミング言語が実装できるというわけです。
実際にインタプリタを実装する前にBrainfuckの8種類の文字の置き換えを考えます。今回は下の表のように順番に置き換えてみました。

Brainfuckの文字 置き換えた文字列
> CCC
< CC3
+ C3C
- C33
. 3CC
, 3C3
[ 33C
] 333

あとはなぜか自作のBrainfuckインタプリタがパソコンの中に眠っていたので、少し弄ってこの文字列を受け取るようにしてあげればCと3で書けるプログラミング言語の完成です。
実際に使用しているインタプリタはこちらになります。ファイルからソースコードを文字列として取得し、前から順番に読み込んで命令を実行している比較的シンプルなものです。メモリとポインタについては、配列を用いて疑似的に実装しています。
インタプリタの実装には、実行速度と実装コストを考えてC++を採用しました。

#include <bits/stdc++.h>
using namespace std;

int main() {

    // Initialization
    int mem_size = 100000;
    unsigned int ptr = 0;
    vector<unsigned char> mem(mem_size, 0);
    stack<int> loops;

    // Load Source File
    ifstream file(".source.c3");
    stringstream buffer;
    buffer << file.rdbuf();
    string code = buffer.str();
    int len = code.size()/3;

    // interpreter Body
    for (int prg=0; prg<len; prg++) {

        // Get Order
        string ord = code.substr(prg*3, 3);

        // Modify Pointer
        if (ord == "CCC") ptr = (ptr >= mem_size-1) ? 0 : ptr+1;
        if (ord == "CC3") ptr = (ptr <= 0) ? mem_size-1 : ptr-1;

        // Modify Memory
        if (ord == "C3C") mem[ptr]++;
        if (ord == "C33") mem[ptr]--;

        // Input and Output
        if (ord == "3CC") putchar(mem[ptr]);
        if (ord == "3C3") mem[ptr] = getchar();

        // Loop Start
        if (ord == "33C") {
            if (mem[ptr] != 0) loops.push(prg);
            else {
                int depth = 1;
                while (depth>0) {
                    prg++;
                    if (code.substr(prg*3, 3) == "33C") depth++;
                    if (code.substr(prg*3, 3) == "333") depth--;
                }
            }
        }

        // Loop End
        if (ord == "333") {
            prg = loops.top()-1;
            loops.pop();
        }

    }

}

さて、実際に実行するコードの書き方についてですが、仕様としてはBrainfuckと全く同じなのでBrainfuckのHello, World!のプログラム

++++++++++[>+++++++>++++++++++>++++>+++>+++++++++>+<<<<<<-]>++.>+.+++++++..+++.>++++.>++.>---.<<<.+++.------.--------.>-----------.>>>.

を上の表の通りにそれぞれ置き換えてあげるだけで、

C3CC3CC3CC3CC3CC3CC3CC3CC3CC3C33CCCCC3CC3CC3CC3CC3CC3CC3CCCCC3CC3CC3CC3CC3CC3CC3CC3CC3CC3CCCCC3CC3CC3CC3CCCCC3CC3CC3CCCCC3CC3CC3CC3CC3CC3CC3CC3CC3CCCCC3CCC3CC3CC3CC3CC3CC3C33333CCCC3CC3C3CCCCCC3C3CCC3CC3CC3CC3CC3CC3CC3C3CC3CCC3CC3CC3C3CCCCCC3CC3CC3CC3C3CCCCCC3CC3C3CCCCCC33C33C333CCCC3CC3CC33CCC3CC3CC3C3CCC33C33C33C33C33C333CCC33C33C33C33C33C33C33C333CCCCCC33C33C33C33C33C33C33C33C33C33C333CCCCCCCCCCC3CC

となり、冒頭のHello, World!プログラムが得られるというわけです。

おわりに

この記事で使用したインタプリタやコード例については、GitHubに上げているので、自由にオモチャにしていただいて構いません。実行したいコードを.source.c3に書き、.interpreter.cppを実行すれば動作します。ただし、構文エラーや未定義動作についての例外処理は実装していませんので、その点は悪しからず。
ところで、便宜上コードの拡張子を.c3にしていたのですが、どうやら調べてみるとすでにC3言語というものがあるようです。ちょっと残念ですね。
それでは、良い感じのオチが出来たところで締めようと思います。ありがとうございました。

Discussion