6
\$\begingroup\$

I've seen a lot of Brainf**k interpreters in various languages. I decided that actually interpreting Brainf**k is too hard, so instead of that, I wrote a 'compiler' in Ruby that directly transcribes BF to Ruby code. This is Version 1.0, and I'm looking for any tips on how to make it more efficient.

There are a few things that really bug me:

  1. replacements feels like it's messed up somehow, but I can't put my finger on how.
  2. $stdin.readbyte feels entirely too long, but I can't find an alternative that's shorter.
  3. Should I be using $stdin at all?
input_file = $ARGV[0]
output_file = $ARGV[1]
start = <<-END.gsub(/[ \t]*\|/, '')
  |# Automatically generated by bf_to_ruby.rb
  |# Source available at GitHub
  |data = Hash.new(0)
  |pointer = 0
END

replacements = [
    { replacing: '+', replacement: 'data[pointer] += 1'              },
    { replacing: '-', replacement: 'data[pointer] -= 1'              },
    { replacing: '>', replacement: 'pointer += 1'                    },
    { replacing: '<', replacement: 'pointer -= 1'                    },
    { replacing: '.', replacement: 'putc data[pointer]'              },
    { replacing: ',', replacement: 'data[pointer] = $stdin.readbyte' },
    { replacing: '[', replacement: 'until data[pointer] == 0'        },
    { replacing: ']', replacement: 'end'                             },
]

output = open(output_file, File::CREAT | File::WRONLY)
output.puts(start)

open(input_file, File::RDONLY) do |input|
  input.each_char do |char|
    replacements.each do |data|
      if data[:replacing] == char
        output.puts(data[:replacement])
      end
    end
  end
end

Demo

Input:

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

Output:

# Automatically generated by bf_to_ruby.rb
# Source available at GitHub
data = Hash.new(0)
pointer = 0
data[pointer] += 1
data[pointer] += 1
data[pointer] += 1
data[pointer] += 1
data[pointer] += 1
data[pointer] += 1
data[pointer] += 1
data[pointer] += 1
until data[pointer] == 0
pointer += 1
data[pointer] += 1
data[pointer] += 1
data[pointer] += 1
data[pointer] += 1
until data[pointer] == 0
pointer += 1
data[pointer] += 1
data[pointer] += 1
pointer += 1
data[pointer] += 1
data[pointer] += 1
data[pointer] += 1
pointer += 1
data[pointer] += 1
data[pointer] += 1
data[pointer] += 1
pointer += 1
data[pointer] += 1
pointer -= 1
pointer -= 1
pointer -= 1
pointer -= 1
data[pointer] -= 1
end
pointer += 1
data[pointer] += 1
pointer += 1
data[pointer] += 1
pointer += 1
data[pointer] -= 1
pointer += 1
pointer += 1
data[pointer] += 1
until data[pointer] == 0
pointer -= 1
end
pointer -= 1
data[pointer] -= 1
end
pointer += 1
pointer += 1
$stdout.putc data[pointer]
pointer += 1
data[pointer] -= 1
data[pointer] -= 1
data[pointer] -= 1
$stdout.putc data[pointer]
data[pointer] += 1
data[pointer] += 1
data[pointer] += 1
data[pointer] += 1
data[pointer] += 1
data[pointer] += 1
data[pointer] += 1
$stdout.putc data[pointer]
$stdout.putc data[pointer]
data[pointer] += 1
data[pointer] += 1
data[pointer] += 1
$stdout.putc data[pointer]
pointer += 1
pointer += 1
$stdout.putc data[pointer]
pointer -= 1
data[pointer] -= 1
$stdout.putc data[pointer]
pointer -= 1
$stdout.putc data[pointer]
data[pointer] += 1
data[pointer] += 1
data[pointer] += 1
$stdout.putc data[pointer]
data[pointer] -= 1
data[pointer] -= 1
data[pointer] -= 1
data[pointer] -= 1
data[pointer] -= 1
data[pointer] -= 1
$stdout.putc data[pointer]
data[pointer] -= 1
data[pointer] -= 1
data[pointer] -= 1
data[pointer] -= 1
data[pointer] -= 1
data[pointer] -= 1
data[pointer] -= 1
data[pointer] -= 1
$stdout.putc data[pointer]
pointer += 1
pointer += 1
data[pointer] += 1
$stdout.putc data[pointer]
pointer += 1
data[pointer] += 1
data[pointer] += 1
$stdout.putc data[pointer]

Both, when run, output:

Hello World!
\$\endgroup\$
  • 1
    \$\begingroup\$ I'm always amazed by the ways people find spend their time. Of course, I once wrote a language. I made the comment character $$ because I knew I would never make any money with the thing. \$\endgroup\$ – psoft Jun 24 '15 at 4:01
6
\$\begingroup\$

You should open the input file before the output file, so that if the input file is unreadable, no file is created at all. File opening should be done using blocks for automatic closure and cleanup.

This is too complicated:

replacements.each do |data|
  if data[:replacing] == char
    output.puts(data[:replacement])
  end
end

replacements would be better as a Hash:

replacements = {
    '+' => 'data[pointer] += 1',
    '-' => 'data[pointer] -= 1',
    '>' => 'pointer += 1',
    '<' => 'pointer -= 1',
    '.' => 'putc data[pointer]',
    ',' => 'data[pointer] = $stdin.readbyte',
    '[' => 'until data[pointer] == 0',
    ']' => 'end',
}

open(input_file, File::RDONLY) do |input|
  open(output_file, File::CREAT | File::WRONLY) do |output|
    output.puts(start)
    output.puts(input.each_char.map { |c| replacements[c] }.compact.join("\n"))
  end
end

One big puts should be faster than many indivual puts.

\$\endgroup\$
  • \$\begingroup\$ "One big puts should be faster than many indivual puts." Yes, but that requires a lot more memory, so I intentionally didn't do that. In retrospect, I probably should have commented, but it doesn't matter much. \$\endgroup\$ – Nic Hartley Jun 23 '15 at 23:08
  • \$\begingroup\$ I doubt you'll come close to hitting your memory limit. \$\endgroup\$ – 200_success Jun 23 '15 at 23:11
  • \$\begingroup\$ Fair enough. It still bugs me, though. \$\endgroup\$ – Nic Hartley Jun 24 '15 at 1:01
  • \$\begingroup\$ Could I ask you two merge your answers into one? I'd like to accept both, since both helped greatly and v2 is about to come out, but unfortunately I can't. \$\endgroup\$ – Nic Hartley Jun 24 '15 at 4:35
  • \$\begingroup\$ This answer addresses the Ruby code; the other discusses the generated Brainfuck code. They even present different approaches to performing the translation. Since the answers are independent, I'd prefer to keep them separate. \$\endgroup\$ – 200_success Jun 24 '15 at 5:25
6
\$\begingroup\$

In Brainfuck, there are often long strings of -, +, <, and > commands. Your generated code could be made more efficient by coalescing these commands.

In a common dialect of Brainfuck, data values wrap modulo 256.

Using Hash.new(0) to represent the memory is an interesting trick to present the illusion of infinite memory preinitialized to 0. I would personally prefer to use an array, which should be more compact and doesn't require hashing.

It would be a good idea to include a shebang line in the output.

START = <<-END.gsub(/[ \t]*\|/, '')
  |#!/usr/bin/env ruby
  |# Automatically generated by bf_to_ruby.rb
  |# Source available at GitHub
  |class Mem < Array
  |  def [](i); self.fetch(i, 0); end
  |  def []=(i, val); super(i, val < 0 ? val += 256 : val); end
  |end
  |data = Mem.new
  |pointer = 0
END

def compile(bf)
  regex = /([-+][^<>.,\[\]]*) | # Run of - or +
           ([<>][^-+.,\[\]]*) | # Run of < or >
           ([,.\[\]])         | # Other significant characters
           ./mx
  bf.gsub(regex) do |match|
    if $1
      "data[pointer] += #{(match.count('+') - match.count('-')) % 256}\n"
    elsif $2
      "pointer += #{match.count('>') - match.count('<')}\n"
    elsif match == '.'
      "putc data[pointer]\n"
    elsif match == ','
      "data[pointer] = $stdin.readbyte\n"
    elsif match == '['
      "until data[pointer] == 0\n"
    elsif match == ']'
      "end\n"
    end
  end
end

input_file, output_file = ARGV[0..1]
bf = IO.read(input_file)
open(output_file, File::CREAT | File::WRONLY) do |output|
  output.puts(START)
  output.puts(compile(bf))
end
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.