難読プログラミング言語をサクサク書くためのマクロ機能付超簡易言語

Brainf*ck等の難読プログラミング言語でプログラムを作成するときは、ちょっとしたトランスレータを用意すると格段に書きやすくなります。

1. シンプルなトランスレータ

たとえば、一番シンプルなのはこんな感じ。
step1.awk

#!/usr/bin/nawk -f
$1 == "inc"     { printf("+") }
$1 == "dec"     { printf("-") }
$1 == "input"   { printf(",") }
$1 == "print"   { printf(".") }
$1 == "shift"   { printf(">") }
$1 == "unshift" { printf("<") }
$1 == "while"   { printf("[") }
$1 == "wend"    { printf("]") }

こうすると、以下のようなソースからBrainf*ckプログラムが生成できます。
echo.bfs

# echo program
inc
while
    shift
    input
    print
    unshift
wend

実行結果

$ nawk -f step1.awk echo.bfs
+[>,.<]

2. 連続命令の省略表記

もう少し手を加えて、同じ命令が続く場合に「inc 20」のような省略表記ができるように機能を追加してみます。
step2.awk

#!/usr/bin/nawk -f
$1 == "inc"     { nprint($2, "+") }
$1 == "dec"     { nprint($2, "-") }
$1 == "input"   { nprint($2, ",") }
$1 == "print"   { nprint($2, ".") }
$1 == "shift"   { nprint($2, ">") }
$1 == "unshift" { nprint($2, "<") }
$1 == "while"   { printf("[") }
$1 == "wend"    { printf("]") }

function nprint(n, s) {
    if (n + 0 < 1) n = 1
    for (i = 0; i < n; i++) {
        printf("%s", s)
    }
}

画面へ「foo」と表示するプログラムが、こんな感じで書けるようになりました。
foo.bfs

# print "foo" program
inc 102
print
shift
inc 111
print
print

実行結果

$ nawk -f step2.awk foo.bfs
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
++++++++++++++++++++++++++++++++++++.>++++++++++++++++++++++++++++
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+++++++++++++++++..

3. マクロ機能の実装

この手の言語は、分岐や繰り返し構造を作るために定型的な命令の組み合わせパターンを使うことがよくあります。というわけで最終版では、それらをまとめてマクロ定義できるように機能を追加してみます。またマクロ定義は別ファイルにできると使い勝手が良いのでinclude命令も追加します。

bfs2bf.awk

#!/usr/bin/nawk -f

{ eval($0) }

function eval(line,      token, s, pushedfile) {
    split(line, token)
    if (token[1] == "inc")      nprint(token[2], "+")
    if (token[1] == "dec")      nprint(token[2], "-")
    if (token[1] == "input")    nprint(token[2], ",")
    if (token[1] == "print")    nprint(token[2], ".")
    if (token[1] == "shift")    nprint(token[2], ">")
    if (token[1] == "unshift")  nprint(token[2], "<")
    if (token[1] == "while")    printf("[")
    if (token[1] == "wend")     printf("]")

    # マクロ定義
    if (token[1] == "def") {
        s = ""
        while (getline2() > 0) {
            if (gLineBuffer ~ /end_def/) break
            s = s "\n" gLineBuffer
        }
        gHash[token[2]] = s
    }

    # マクロ展開
    if (token[1] in gHash) {
        s = gHash[token[1]]
        gsub(/\$1/, token[2], s)
        gsub(/\$2/, token[3], s)
        gsub(/\$3/, token[4], s)
        evalLines(s)
    }

    # includeファイル処理
    if (token[1] == "include") {
        pushedfile = gCurrentFile
        gCurrentFile = token[2]
        gsub(/"/, "", gCurrentFile)
        while (getline2() > 0) {
            eval(gLineBuffer)
        }
        close(gCurrentFile)
        gCurrentFile = pushedfile
    }
}

function getline2() {
    if (gCurrentFile == "") {
        return getline gLineBuffer
    } else {
        return getline gLineBuffer < gCurrentFile
    }
}

function evalLines(lines,     line, n, i) {
    n = split(lines, line, "\n")
    for (i = 1; i <= n; i++) {
        eval(line[i])
    }
}

function nprint(n, s) {
    if (n + 0 < 1) n = 1
    for (i = 0; i < n; i++) {
        printf("%s", s)
    }
}

def命令で定義されたマクロ定義はgHash[]に格納されます。逆に現在処理中のトークンがgHashのキーに見つかればマクロを展開します。マクロ定義中で$1,$2,$3と仮引数で書かれていた箇所があれば実引数で置き換えます。
 
includeの方は、インクルードファイルを開いて一行ずつ処理しますが、このときeval()関数が再帰的に呼ばれるため、現在処理中のファイル名をよけておく必要があります。
 
これを使うと、入力文字が数字である場合に"number"と表示するプログラムはこんな感じで書けます。

isnumber.bfs

include "lib.bfs"

# "number"と出力するマクロ定義
def print_number
    multi 10 11
    print
    inc 7
    print
    dec 8
    print
    dec 11
    print
    inc 3
    print
    inc 13
    print
end_def

# main program

# '0'と比較
multi 6 8
dec
shift
input
unshift
compare

# '9'と比較
shift
dup
if_not_zero
    shift 2
    inc 11
    unshift 2
end_if
shift
compare

# "number"出力
shift
if_not_zero
    shift
    print_number
    unshift
end_if


ここまでくると、ほとんどオレオレ言語ですね。インクルードされるライブラリはこんな感じになります。
lib.bfs

# 乗算
# *p => arg1 * arg2
def multi
    inc $1
    while
        shift
        inc $2
        unshift
        dec
    wend
    shift
end_def

# ゼロクリア
# *p => 0
def set_zero
    while
        dec
    wend
end_def

# 分岐
def if_not_zero
    while
        set_zero
end_def

def end_if
    wend
end_def

# *pを*(p+1)に複製
# ただし*(p+2)を作業用に使用する
def dup
    while
        shift
        inc
        shift
        inc
        unshift 2
        dec
    wend
    shift 2
    while
        unshift 2
        inc
        shift 2
        dec
    wend
    unshift 2
end_def

# 比較
# *p >= *(p+1)の場合 *p => 0, *(p+1) => 0
# *p <  *(p+1)の場合 *p => 0, *(p+1) => *(p+1) - *p
def compare
    while
        shift
        dup
        shift
        if_not_zero
            unshift
            dec
            shift
        end_if
        unshift 2
        dec
    wend
end_def

実行結果

$ nawk -f bfs2bf.awk isnumber.bfs
++++++[>++++++++<-]>->,<[>[>+>+<<-]>>[<<+>>-]<<>[[-]<->]<<-]>[>+>+
<<-]>>[<<+>>-]<<[[-]>>+++++++++++<<]>[>[>+>+<<-]>>[<<+>>-]<<>[[-]<
->]<<-]>[[-]>++++++++++[>+++++++++++<-]>.+++++++.--------.--------
---.+++.+++++++++++++.<]

Enjoy!