ModanShogi quine


この記事は Quine Advent Calender 2014 の 103 日目になりました。
ref: http://gihyo.jp/dev/column/01/prog/2010/aprilfool2010-01
ref: http://route477.net/d/?date=20100402#p01
で紹介されている yhara さん作の言語で quine を書きました。
以下のように走らせます。生成も実行も Ruby 1.9 でないと動きません。Strict モード対応は、後進のためにとっときます。

$ ruby19 quine.shogimodan.gen.rb > quine.shogimodan
$ ruby19 bin/shogimodan quine.shogimodan > quine2.shogimodan
$ diff -q quine.shogimodan quine2.shogimodan

ただし、参照実装がバグっていたので以下のパッチをあてています。*1

diff --git a/lib/shogimodan/vm.rb b/lib/shogimodan/vm.rb
index 448efdc..56ea567 100644
--- a/lib/shogimodan/vm.rb
+++ b/lib/shogimodan/vm.rb
@@ -74,7 +74,6 @@ class ShogiModan
       {}.tap{|labels|
         code.each_with_index{|(op, arg1, arg2), i|
           if op == :label
-            check_register_idx(arg1)
             labels[arg1] = i
           end
         }



実はぼくもチェスの棋譜をプログラムにするというネタを 1 年くらい温めていたのですが、なかなか満足いく形にまとまらなくて、先を越されてしまいました。
ModanShogi については、レジスタの数をマスの数に合わせるというネタは面白いと思いましたが、以下の点が不満です。

  • Strict が必須でない (Strict 必須でとても書けそうにないのに意外に書ける!という言語設計がかっこいいと思う)
  • チューリング完全じゃない (任意の brainfuck コードから ModanShogi に変換できないよね?)
  • 命令と駒の割り当てに面白さを感じない、スタックの存在に根拠がない (将棋と言うネタに絡めて欲しかった)

正直、ネタ負けしてるなーという印象です。はい、負け惜しみです。まあ、

100%満足行かなくても公開すればいろいろ良いフィードバックがある

Route 477 - プログラミング言語ModanShogiを公開しました

ということなんでしょうね。それでも、esoteric language に妥協は似合わない、とぼくは思う。

追記
チューリング完全にできそうだとkinaba さんが教えてくれました。ただし、レジスタ有理数か何かで無限の精度を保持してくれることを保証してくれるなら。


戯言はともかく、quine.shogimodan.gen.rb のソース。なんとなく、解説をいっぱい書いてみました。ぼくが普段どんな風に quine を書いているかわかるかと思います (普段はこんなコメント書かないけど) 。

# coding: UTF-8

# 変数の使い方
# $1: 1 固定
# $2: 2 固定
# $3: モード (3: データ出力、2: コード出力、1: 終了)
# $4: ▲と△の切り替えフラグ
# $5: ループカウンタ
# $6: リターン先ラベル保持
# $7, $8, $9: 雑多

# 指定した文字のコード番号を計算して出力するコードを生成する
# $7 と $8 を使い、$8 に結果を入れて返す
# accum が真なら $8 を初期化しないで計算する
def output(ch, accum = false)
  c = ""
  c = "▲8八金 (* $8 -= $8 *)" unless accum
  c << "▲7一と (* $7 = $1 *)"
  ch.unpack("U").first.to_s(2)[1..-1].reverse.each_char do |n|
    c << "▲8七歩 (* $8 += $7 *)" if n == "1"
    c << "▲7二銀 (* $7 *= $2 *)"
  end
  c << "▲8七歩 (* $8 += $7 *)" if ch != "\0"
  c << "▲8一玉 (* putc $8 *)"
  c
end

# ▲と△を交互に出力するコード断片
output_player = <<END
# $4 を反転させる (4 -> 0 -> 4 -> 0 -> ...)
▲8一と ▲8二金          # $8 = -1
▲4二金 ▲4八銀 ▲4二歩 # $4 = ($4 - $2) * $8 + $2

# $8 に加算分を入れる (1 -> 0 -> 1 -> 0 -> ...)
▲8四と ▲8二桂 ▲8二桂 # $8 = $4 / 4

# ▲か△を出力する
#{ output("", true) } # putc ▲
END

code = <<END.gsub(/#.*|\(\*(?:.|\n)*?\*\)|[+\-|>^]/, "").split.join(" ")
##### データ部分 (後でコード部分から生成する) #####

    # ラベル 6 からスタート
    #*6

    # 文字のコード番号を 1 ビットずつ 16 ビット push する (little endian)
    #▲1一龍 ▲2一龍 ▲1一龍 ▲1一龍 # push $1; push $2; ...
    #▲1一龍 ▲1一龍 ▲2一龍 ▲1一龍
    #▲1一龍 ▲1一龍 ▲1一龍 ▲1一龍
    #▲2一龍 ▲2一龍 ▲1一龍 ▲1一龍

    # 文字を処理するサブルーチンを呼び出す (ラベル 2 へジャンプ)
    # (リターン先ラベルは $6 に入っている)
    #▲1二飛 # jump_if $1 $2

    # 以上で 1 文字の処理が終了、以降全文字分繰り返し

    # ...


##### コード部分 #####

    # 全データを処理した
    #*?

    # モードを進めてデータ部分の先頭に戻る (ラベル 6 へジャンプ)
    ▲3一金                   # $3 -= 1
    ▲6二と ▲6二歩 ▲6二歩 # $6 = 6
    ▲6六飛                   # jump_if $6 $6


    ### 文字を処理するサブルーチン

# リターン
^
|       *2
|       # push されたビット列データをデコードする (16 ビットを pop して整数に戻す)
|       # $9 に戻した整数を記録する
|       # $8 はビット演算用 (1<<16)
|       # $5 はループカウンタ (16)
|       ▲9九金                                     # $9 = 0
|       ▲8二と ▲8八銀 ▲8八銀 ▲8八銀 ▲8八銀 # $8 = 1<<16
|       ▲5二と ▲5五銀 ▲5五銀                   # $5 = 16
|
|       # デコードのループ
|   +-> *4
|   |   # $8 を右シフトしながら、pop した値が 2 だったら $9 に $8 を足す
|   |   ▲8二桂                   # $8 /= 2
|   |   ▲7一馬 ▲7一金 ▲7八銀 # $7 = $8 * (pop - 1)
|   |   ▲9七歩                   # $9 += $7
|   |
|   |   # ループカウンタをデクリメントし、ループの先頭にジャンプする
|   |   ▲5一金          # $5 -= 1
|   |   ▲7二と ▲7二歩 # $7 = 4
|   +-- ▲5七飛          # jump_if $5 $7
|   |
|   +-> # この時点で $9 にはデコード結果が保持されている
|
|       # 現在のモードに基づいてディスパッチする
| +---- ▲8三と ▲8二金 ▲8三飛 # $8 = $3 - $2; jump_if $8 $3
| |
| |
| +---> ### 現在のモードは 2 (コード部分出力)
| |
| |     # デコードした文字をそのまま出力する
| |     ▲9九玉 # putc $9
| |
| |     # リターン先ラベルを 1 つ進めてジャンプする (リターンする)
+-+---- ▲6一歩 ▲6六飛 # $6 += 1; jump_if $6 $6
| |     # assert false
| |
| |
| +---> *3
| |     ### 現在のモードは 3 (データ部分出力)
| |
| |     # ラベルの文字列を出力する (ex. "*6 ")
| |     #{ output("*") } ▲6一王 #{ output(" ") } 
| |
| |     # エンコードする (little endian で 1 ビットずつ push するコードを出力する)
| |     # $5 はループカウンタ (16)
| |     ▲5二と ▲5五銀 ▲5五銀 # $5 = 16
| |
| | +-> *5
| | |   # push するコードを出力する (ex. "▲1一龍 ")
| | |   #{ output_player }
| | |
| | |   # $8 に $9 の最下位ビットを代入し、$9 を右シフトする
| | |   ▲8九と ▲8二香 ▲9八金 ▲9二桂 # $8 = $9; $8 %= $2; $9 -= $8; $9 /= 2
| | |
| | |   #{ output("", true) } # putc $8
| | |   #{ output("") + output("") + output(" ") }
| | |
| | |   # ループカウンタをデクリメントし、ループの先頭にジャンプする
| | |   ▲5一金                   # $5 -= 1
| | |   ▲7二と ▲7二歩 ▲7一歩 # $7 = 4
| | +-- ▲5七飛                   # jump_if $5 $7
| | |
| | +-> # サブルーチンを呼び出すコードを出力する ("1二飛 ")
| |     #{ output_player }
| |     #{ output("") + output("") + output("") + output(" ") } # putc "1二飛 "
| |
| |     # リターン先ラベルを 1 つ進めてジャンプする (リターンする)
+-+---- ▲6一歩 ▲6六飛 # $6 += 1; jump_if $6 $6
  |     # assert false
  |
  |
  +---> *1
        ### 現在のモードは 1 (終了)

        # 改行を出力して終了する
        #{ output("\n") }
END


# コード部分の先頭にラベルを付ける (ラベル番号はコード長に依存する)
code = "*#{ (code.size + 6 + 6) } " + code

# コード部分の▲△を交互にする (飾り)
flag = (code.size % 2) == 0
code.gsub!(/[▲△]/) { (flag = !flag) ? "" : "" }

# コード部分からデータ部分を生成する
data, label = "", 6
code.unpack("U*").each do |ch|
  data << "*#{ label } "
  label += 1
  ("%016b" % ch).reverse.chars.each do |n|
    data << "#{ n == "0" ? "" : "" }一龍 "
  end
  data << "▲1二飛 "
end

# コード部分の先頭にデータ部分をつける
code = data + code
flag = true

# 全体の▲△を交互にする (飾り)
code.gsub!(/[▲△]/) { (flag = !flag) ? "" : "" }

puts code

*1:10 以上のラベルが使えないようになっていた。ジャンプ先のラベル名をレジスタの値として指定できる仕様から見てバグだと判断しました。10 以上のラベルが禁止されているとすると、難しいですね。