概要
Rubyで一行で書くという縛りで、Brainf**kの処理系を書いてみました!
n番煎じの極みのような気がしないでもないですが、まあゆるしてちょ。
もっと改善できる余地があるかもしれないですが、もう見たくないです。
※ 作業環境のRubyのバージョンは以下のとおりです。
Ruby入ってなかったので、とりあえずその時の最新版入れました。
$ ruby -v
ruby 3.1.3p185 (2022-11-24 revision 1a6b16756e) [x86_64-linux]
結果
1行版
require('io/console');require('optparse');->o,s:((o['s'].nil?)?((!o['f'].nil?)?(File.read(o['f'])):gets(nil)):o['s']),step:o['step']{->t:'><+-.,[]'.split('').zip([*1..8]).to_h,b:(s.split(/\R/).join.split('').each_with_index.reduce([[0,0,[0]]]){|a,(c,i)|(!t[c].nil?)?((!step)?(((t[c]<5)?((a.last[0]==t[c]?a.push([a.last[0],a.last[1]+=1,a.pop[2]<<i]):a.push([t[c],1,[i]]))):a.push([t[c],0,[i]]))):a.push([t[c],1,[i]])):a}),m:(b.each_with_index.reduce([[],{}]){|a,(e,i)|(e[0]==7||e[0]==8)?(e[0]==7?a[0].push(i)&&a:(a[1][i]=a[0].last)&&(a[1][a[0].pop]=i+1)&&a):a}[1]),k:0,n:[],g:->{(n[k].nil?)?n[k]=0:n[k]},u:->m{n[k]=m&0xFF},c:-1,nc:0,uk:->q{(k=q)&&g.call},sib:'',st:->{($stdin.isatty)?($stdin.raw{|o|->l{l==3?exit(0):l}.call(o.readbyte)}):(!sib.nil?)?((sib.empty?)?(((sib=gets)&&sib.slice!(0)&.ord)||0):sib.slice!(0).ord):0},sb:[],stp:0{->f{(f[b[c][0]].call(b[c][1])&&step&&->{print("\033[2J\033[0;0H#{'+-'*10}\nPTR: #{k}, PC: #{c}, STP: #{stp}\n\nCELL(#{n.size}): [#{k}]=>(#{g.call},#{g.call.chr.dump})\n[#{n.map.with_index{|v,i|(i==k)?"\e[42m#{v}\e[0m":v}.join('|')}]\n#{'+-'*10}\n\n:::PROGRAM:::\n#{[s.split(/\R/).each_with_index.reduce([-1,0,[]]){|a,(v,i)|a[2].push("#{"\e[32m% 3d\e[0m"%i}: #{v.split('').map.with_index{|h,j|((a[0]+=1)==b[c][2].first)?((a[1]=([i-5,[i+5,10].max]))&&"\e[41m#{h}\e[0m"):h}.join}")&&a}].map{|v|v[2].slice([[v[1][0],v[2].size-11].min,0].max..[v[1][1],v[2].size].min)}.flatten.join("\n")}\n#{':'*12}\n\n:::OUT:::\n#{sb.join.lines.last(16).join}\n")||st.call}.call)while(((c=nc)&&(nc+=1))&&0<=c&&c<=b.size-1&&(stp+=1))}.call([->_{1},->q{uk.call(k+q)},->q{uk.call(k-q)},->q{u.call(g.call+q)},->q{u.call(g.call-q)},->_{print(g.call.chr)||sb<<g.call.chr},->_{u.call(st.call)},->_{(g.call==0&&!m[c].nil?)?nc=m[c]:c},->_{(g.call!=0&&!m[c].nil?)?nc=m[c]:c}])}.call}.call(ARGV.getopts('f:','s:','step'))
1825文字です!
全体が見やすいように改行入れた版
(構文上おかしくなさそうな位置で適当に改行を入れていますが、確認してないので(←しろよ)、たぶん動かないです)
require('io/console');require('optparse');->o,s:((o['s'].nil?)?((!o['f'].nil?)?
(File.read(o['f'])):gets(nil)):o['s']),step:o['step']{->t:'><+-.,[]'.split('').zip([*1..8]).to_h,b:
(s.split(/\R/).join.split('').each_with_index.reduce([[0,0,[0]]]){|a,(c,i)|(!t[c].nil?)?((!step)?
(((t[c]<5)?((a.last[0]==t[c]?a.push([a.last[0],a.last[1]+=1,a.pop[2]<<i]):a.push([t[c],1,[i]]))):a.push([t[c],0,[i]]))):a.push([t[c],1,[i]])):a}),
m:(b.each_with_index.reduce([[],{}]){|a,(e,i)|(e[0]==7||e[0]==8)?(e[0]==7?a[0].push(i)&&a:(a[1][i]=a[0].last)&&(a[1][a[0].pop]=i+1)&&a):a}[1]),
k:0,n:[],g:->{(n[k].nil?)?n[k]=0:n[k]},u:->m{n[k]=m&0xFF},c:-1,nc:0,uk:->q{(k=q)&&g.call},sib:'',
st:->{($stdin.isatty)?($stdin.raw{|o|->l{l==3?exit(0):l}.call(o.readbyte)}):(!sib.nil?)?((sib.empty?)?(((sib=gets)&&sib.slice!(0)&.ord)||0):sib.slice!(0).ord):0},sb:[],
stp:0{->f{(f[b[c][0]].call(b[c][1])&&step&&->{print("\033[2J\033[0;0H#{'+-'*10}\nPTR: #{k}, PC: #{c}, STP: #{stp}\n
\nCELL(#{n.size}): [#{k}]=>(#{g.call},#{g.call.chr.dump})\n[#{n.map.with_index{|v,i|(i==k)?"\e[42m#{v}\e[0m":v}.join('|')}]\n
#{'+-'*10}\n\n:::PROGRAM:::\n#{[s.split(/\R/).each_with_index.reduce([-1,0,[]]){|a,(v,i)|a[2].push("#{"\e[32m% 3d\e[0m"%i}:
#{v.split('').map.with_index{|h,j|((a[0]+=1)==b[c][2].first)?((a[1]=([i-5,[i+5,10].max]))&&"\e[41m#{h}\e[0m"):h}.join}")&&a}]
.map{|v|v[2].slice([[v[1][0],v[2].size-11].min,0].max..[v[1][1],v[2].size].min)}.flatten.join("\n")}\n#{':'*12}\n\n:::OUT:::\n#{sb.join.lines.last(16).join}\n")||st.call}.call
)while(((c=nc)&&(nc+=1))&&0<=c&&c<=b.size-1&&(stp+=1))}
.call([->_{1},->q{uk.call(k+q)},->q{uk.call(k-q)},->q{u.call(g.call+q)},
->q{u.call(g.call-q)},->_{print(g.call.chr)||sb<<g.call.chr},->_{u.call(st.call)},
->_{(g.call==0&&!m[c].nil?)?nc=m[c]:c},->_{(g.call!=0&&!m[c].nil?)?nc=m[c]:c}])}.call}.call(ARGV.getopts('f:','s:','step'))
使い方
ファイルから実行
"Hello, world!\n"
と出力するだけの簡単なBFコードを用意します。
++++++++++++[-
>++++++
>++++++++
>+++++++++
>+++++++++
>+++
>++
>+++++++++
>+++++++++
>++++++++
>++
<<<<<<<<<<]
>.
>+++++.
>..
>+++.
>++++++++.
>++++++++.
>+++++++++++.
<<<.
>>>>++++++.
<<<<<.
>>>>>>++++.
>+++++++++.
>++++++++++.
そのまま実行
$ ruby brainfook.rb -f helloworld.bf
Hello, world!
or
$ cat helloworld.rb | ruby brainfook.rb
Hello, world!
なにげについてるステップ実行
キー入力のたびに1ステップ進めます(Ctrl + Cで途中終了)。
$ ruby brainfook.rb --step -f helloworld.bf
コードを直接渡して実行/パイプ(標準入力)も地味に対応
これで他のコマンドの実行結果を受け取って、Brainf**kで処理するという宇宙一素晴しいこともできます!
$ echo "Gdkkn Rsdud" | ruby brainfook.rb -s "+[>,+.->+<[>-<[-]]>[<<->>-]<<]"
Hello!Steve
縛り詳細
せっかくワンライナーなので、もっと苦しむ楽しむためになんとなく以下の縛りで書きました。
ワンライナーチャレンジされる場合はご参考にどうぞ!
- セミコロン禁止!
-
a=1;b=2;c=a+b;print(c);
みたいな改行をただ置き替えたみたいな、軟弱な書き方ができるのでワンライナーではいらんのや! - ただし、requireは例外。ゆるして。
-
- if式,case式禁止! (三項演算子はOK)
-
case(c)when('+')then(p+=1)when('-')then(p-=1)end
みたいな軟弱な書き方ができるので、ワンライナーではいらんのや!
-
- 実装中もエンターキーを押さない!
- ワンライナーでは(ry
- 変数名などのメモ禁止!
- ワ(ry
解説
Brainf**kの言語仕様などは数多のサイトで解説されているので、とりあえずwikipedia貼っておきます。
require("io/console")
require("optparse")
# ↓ オプションの処理部
->o, s: ((o["s"].nil?) ? ((!o["f"].nil?) ? (File.read(o["f"])) : gets(nil)) : o["s"]), step: o["step"] {
# ↓ コード実行前処理部
->t: "><+-.,[]".split("").zip([*1..8]).to_h,
# ↓ BFのソースコードを最適化(連続する+-><をまとめるだけ。ステップ実行時は最適化なし),不明な文字は無視して実行用の配列に変換
b: (s.split(/\R/).join.split("").each_with_index.reduce([[0, 0, [0]]]) { |a, (c, i)| (!t[c].nil?) ? ((!step) ? (((t[c] < 5) ? ((a.last[0] == t[c] ? a.push([a.last[0], a.last[1] += 1, a.pop[2] << i]) : a.push([t[c], 1, [i]]))) : a.push([t[c], 0, [i]]))) : a.push([t[c], 1, [i]])) : a }),
# ↓ 対応する '[' と ']' の位置をあらかじめ計算してHashで持っておく
m: (b.each_with_index.reduce([[], {}]) { |a, (e, i)| (e[0] == 7 || e[0] == 8) ? (e[0] == 7 ? a[0].push(i) && a : (a[1][i] = a[0].last) && (a[1][a[0].pop] = i + 1) && a) : a }[1]),
# ↓ ポインタ
k: 0,
# ↓ Cell用の配列
n: [],
# ↓ ポインタの示すCellの値を取得
g: -> { (n[k].nil?) ? n[k] = 0 : n[k] },
# ↓ ポインタの示すCellの値を更新(mを0xFFで積してるので最大255、マイナス時は256から巻きもどる)
u: ->m { n[k] = m & 0xFF },
# ↓ プログラムカウンタ
c: -1,
# ↓ 次のプログラムカウンタ値
nc: 0,
# ↓ ポインタの値をqで更新
uk: ->q { (k = q) && g.call },
# ↓ stdin buffer (pipe時用)
sib: "",
# ↓ stdinから一文字得る
st: -> { ($stdin.isatty) ? ($stdin.raw { |o| ->l { l == 3 ? exit(0) : l }.call(o.readbyte) }) : (!sib.nil?) ? ((sib.empty?) ? (((sib = gets) && sib.slice!(0)&.ord) || 0) : sib.slice!(0).ord) : 0 },
# ↓ step実行時用の出力を保持するbuffer
sb: [],
# ↓ ステップ数カウンタ
stp: 0 {
# ↓ コード実行部
->f {
# ↓ コード実行
(f[b[c][0]].call(b[c][1]) &&
# ↓ ステップ実行モードのときのみ処理
step && -> {
# ↓ ステップ実行の表示
print("\033[2J\033[0;0H\
#{"+-" * 10}\n\
PTR: #{k}, PC: #{c}, STP: #{stp}\n\n\
CELL(#{n.size}): [#{k}]=>(#{g.call},#{g.call.chr.dump})\n[#{n.map.with_index { |v, i| (i == k) ? "\e[42m#{v}\e[0m" : v }.join("|")}]\n\
#{"+-" * 10}\n\n\
:::PROGRAM:::\n\
#{[s.split(/\R/).each_with_index.reduce([-1, 0, []]) { |a, (v, i)| a[2].push("#{"\e[32m% 3d\e[0m" % i}: #{v.split("").map.with_index { |h, j| ((a[0] += 1) == b[c][2].first) ? ((a[1] = ([i - 5, [i + 5, 10].max])) && "\e[41m#{h}\e[0m") : h }.join}") && a }].map { |v| v[2].slice([[v[1][0], v[2].size - 11].min, 0].max..[v[1][1], v[2].size].min) }.flatten.join("\n")}\n\
#{":" * 12}\n\n\
:::OUT:::\n\
#{sb.join.lines.last(16).join}\n") || st.call
}.call) while (
# ↓ プログラムカウンタの処理/終了判定/ステップ数加算
((c = nc) && (nc += 1)) && 0 <= c && c <= b.size - 1 && (stp += 1)
)
}.call([
->_ { 1 }, # ← nop
->q { uk.call(k + q) }, # ← > ポインタを+q
->q { uk.call(k - q) }, # ← < ポインタを-q
->q { u.call(g.call + q) }, # ← + ポインタの差すCellの値を+q
->q { u.call(g.call - q) }, # ← - ポインタの差すCellの値を-q
->_ { print(g.call.chr) || sb << g.call.chr }, # ← . ポインタの差すCellの値を画面に表示(ASCII)
->_ { u.call(st.call) }, # ← , 標準入力から1文字得て、ポインタの差すCellへ入れる
->_ { (g.call == 0 && !m[c].nil?) ? nc = m[c] : c }, # ← [ ポインタの差す値が0だったら対応する']'の後ろへプログラムカウンタを更新
->_ { (g.call != 0 && !m[c].nil?) ? nc = m[c] : c }, # ← ] ポインタの差す値が0でないなら対応する'['へプログラムカウンタを更新
])
}.call
}.call(ARGV.getopts("f:", "s:", "step"))
オプションの処理
一番外側のラムダの引数部では、デフォルト引数を悪用してオプションを処理しています。
Brainf**kのソースコードの文字列をsに、ステップ実行フラグをstepに入れて中の処理で使用しています。
オプションのパースには、Ruby標準ライブラリのこちらを使ってます→https://docs.ruby-lang.org/ja/latest/library/optparse.html
# ↓ オプションの処理部
->o, s: ((o["s"].nil?) ? ((!o["f"].nil?) ? (File.read(o["f"])) : gets(nil)) : o["s"]), step: o["step"] {
# ...処理...
}.call(ARGV.getopts("f:", "s:", "step"))
実行前処理
前処理としてBFのコードを最適化したり、位置を事前計算したり、BF実行時に使うステートを用意しています。
# ↓ コード実行前処理部
->t: "><+-.,[]".split("").zip([*1..8]).to_h,
# ↓ BFのソースコードを最適化(連続する+-><をまとめるだけ。ステップ実行時は最適化なし),不明な文字は無視して実行用の配列に変換
b: (s.split(/\R/).join.split("").each_with_index.reduce([[0, 0, [0]]]) { |a, (c, i)| (!t[c].nil?) ? ((!step) ? (((t[c] < 5) ? ((a.last[0] == t[c] ? a.push([a.last[0], a.last[1] += 1, a.pop[2] << i]) : a.push([t[c], 1, [i]]))) : a.push([t[c], 0, [i]]))) : a.push([t[c], 1, [i]])) : a }),
# ↓ 対応する '[' と ']' の位置をあらかじめ計算してHashで持っておく
m: (b.each_with_index.reduce([[], {}]) { |a, (e, i)| (e[0] == 7 || e[0] == 8) ? (e[0] == 7 ? a[0].push(i) && a : (a[1][i] = a[0].last) && (a[1][a[0].pop] = i + 1) && a) : a }[1]),
# ...中略...
stp: 0 {
# ...処理...
}.call
コードの最適化
->t: "><+-.,[]".split("").zip([*1..8]).to_h,
# ↓ BFのソースコードを最適化(連続する+-><をまとめるだけ。ステップ実行時は最適化なし),不明な文字は無視して実行用の配列に変換
b: (s.split(/\R/).join.split("").each_with_index.reduce([[0, 0, [0]]]) { |a, (c, i)| (!t[c].nil?) ? ((!step) ? (((t[c] < 5) ? ((a.last[0] == t[c] ? a.push([a.last[0], a.last[1] += 1, a.pop[2] << i]) : a.push([t[c], 1, [i]]))) : a.push([t[c], 0, [i]]))) : a.push([t[c], 1, [i]])) : a }),
t
は、{">"=>1, "<"=>2, "+"=>3, "-"=>4, "."=>5, ","=>6, "["=>7, "]"=>8}
のハッシュを生成する処理です。後に出てきますが、各文字ごとの処理をラムダ関数の配列で持ってるので、その対応するインデックスに変換する用のハッシュです。
b
は、コメントに書いてあるとおり、最適化などをもろもろしています。s
に入ってる文字列を配列に変えてreduceでこねくりまわして、[tから得られた番号,個数,[コード上の位置,...]]
の配列を得ます。いわゆるバイトコード的なやつで、これをもとにBFの各命令を実行しています。
(コード上の位置はステップ実行時にのみ使っていて、ステップ実行しないなら不要ですが、変えるのが億劫だったのでそのままです)
t = "><+-.,[]".split("").zip([*1..8]).to_h
s = '++--[]>>><<'
step = false
b = (s.split(/\R/).join.split("").each_with_index.reduce([[0, 0, [0]]]) { |a, (c, i)| (!t[c].nil?) ? ((!step) ? (((t[c] < 5) ? ((a.last[0] == t[c] ? a.push([a.last[0], a.last[1] += 1, a.pop[2] << i]) : a.push([t[c], 1, [i]]))) : a.push([t[c], 0, [i]]))) : a.push([t[c], 1, [i]])) : a })
pp b
# 実行後↓
[[0, 0, [0]],
[3, 2, [0, 1]],
[4, 2, [2, 3]],
[7, 0, [4]],
[8, 0, [5]],
[1, 3, [6, 7, 8]],
[2, 2, [9, 10]]]
# step = true に変えて 実行後↓
[[0, 0, [0]],
[3, 1, [0]],
[3, 1, [1]],
[4, 1, [2]],
[4, 1, [3]],
[7, 1, [4]],
[8, 1, [5]],
[1, 1, [6]],
[1, 1, [7]],
[1, 1, [8]],
[2, 1, [9]],
[2, 1, [10]]]
コード実行
f
には、.callで渡している、BFの各命令に対応する、ラムダ関数が配列で入っています。
その配列に、前処理で作ったb
の配列の、プログラムカウンタc
の位置に入ってる命令インデックスで、f
の配列にアクセスして、そこに入ってるラムダ関数に個数を渡して命令を実行、をひたすらwhileでプログラムカウンタを進めつつ、ぶんまわしてるだけです。
# ↓ コード実行部
->f {
# ↓ コード実行
(f[b[c][0]].call(b[c][1]) &&
# ↓ ステップ実行モードのときのみ処理
step && -> {
# ...ステップ実行時の処理 略...
}.call) while (
# ↓ プログラムカウンタの処理/終了判定/ステップ数加算
((c = nc) && (nc += 1)) && 0 <= c && c <= b.size - 1 && (stp += 1)
)
}.call([
->_ { 1 }, # ← nop
->q { uk.call(k + q) }, # ← > ポインタを+q
->q { uk.call(k - q) }, # ← < ポインタを-q
->q { u.call(g.call + q) }, # ← + ポインタの差すCellの値を+q
->q { u.call(g.call - q) }, # ← - ポインタの差すCellの値を-q
->_ { print(g.call.chr) || sb << g.call.chr }, # ← . ポインタの差すCellの値を画面に表示(ASCII)
->_ { u.call(st.call) }, # ← , 標準入力から1文字得て、ポインタの差すCellへ入れる
->_ { (g.call == 0 && !m[c].nil?) ? nc = m[c] : c }, # ← [ ポインタの差す値が0だったら対応する']'の後ろへプログラムカウンタを更新
->_ { (g.call != 0 && !m[c].nil?) ? nc = m[c] : c }, # ← ] ポインタの差す値が0でないなら対応する'['へプログラムカウンタを更新
])
感想
Brainf**kのコードのほうが簡単に見えるようになりました。あたまおかしくなったんかな。