3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

RubyによるBrainf**k処理系を1行で!

Posted at

概要

Rubyで一行で書くという縛りで、Brainf**kの処理系を書いてみました!
n番煎じの極みのような気がしないでもないですが、まあゆるしてちょ。
もっと改善できる余地があるかもしれないですが、もう見たくないです。

※ 作業環境のRubyのバージョンは以下のとおりです。
Ruby入ってなかったので、とりあえずその時の最新版入れました。

$ ruby -v
ruby 3.1.3p185 (2022-11-24 revision 1a6b16756e) [x86_64-linux]

結果

1行版

brainfook.rb
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コードを用意します。

helloworld.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

brainfook_step_compressed.gif

コードを直接渡して実行/パイプ(標準入力)も地味に対応

これで他のコマンドの実行結果を受け取って、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のコードのほうが簡単に見えるようになりました。あたまおかしくなったんかな。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?