8
\$\begingroup\$

Previous iteration.

You know, I think this is the fastest I've ever pushed out an update to anything. This is Version 2 of my Brainf**k to Ruby converter, and the generated code looks... Well, like Brainf**k, converted directly to Ruby, with no attempt at making it more readable.

I'm looking for any tips on making things more idiomatic, both in the generator and generated code. The nest if/cases really bug me, but I'm not quite sure how to get rid of them, especially since just two characters are blindly replaced. I'd also like advice on making it run faster.

bf_to_ruby.rb

input_file, output_file = ARGV
code = IO.read(input_file).tr('^+-<>.,[]', '')

open(output_file, File::CREAT | File::WRONLY) do |output|
  output.puts <<-END.gsub(/^[ \t]*\||\s*#@.*$/, '')
    |#!/usr/bin/env ruby
    |class Mem < Hash #@ `Hash` because it's more memory-efficient and allows negative values.
    |  def initialize; super(0); end
    |  def []=(i, val); super(i, val & 255); end
    |end
    |data = Mem.new
    |pointer = 0
  END
  indent_level = 0
  code.scan(/(\++)|(\-+)|(<+)|(>+)|([.,\[\]])/)
      .map do |string|
        if string[0]
          next "#{'  ' * indent_level}data[pointer] += #{string[0].length}"
        elsif string[1]
          next "#{'  ' * indent_level}data[pointer] -= #{string[1].length}"
        elsif string[2]
          next "#{'  ' * indent_level}pointer -= #{string[2].length}"
        elsif string[3]
          next "#{'  ' * indent_level}pointer += #{string[3].length}"
        elsif string[4]
          case string[4]
            when '['
              ret = "#{'  ' * indent_level}until data[pointer] == 0"
              indent_level += 1
              next ret #Split it so that it's clear that indent is increased *after* the line
            when ']'
              indent_level -= 1
              next "#{'  ' * indent_level}end"
            when ','
              next "#{'  ' * indent_level}data[pointer] = $stdin.readbyte"
            when '.'
              next "#{'  ' * indent_level}putc data[pointer]"
          end
        end
      end.each { |line| output.puts(line) }
end

Demo

Input:

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

Output:

#!/usr/bin/env ruby
class Mem < Hash
  def initialize; super(0); end
  def []=(i, val); super(i, val & 255); end
end
data = Mem.new
pointer = 0
data[pointer] += 8
until data[pointer] == 0
  pointer += 1
  data[pointer] += 4
  until data[pointer] == 0
    pointer += 1
    data[pointer] += 2
    pointer += 1
    data[pointer] += 3
    pointer += 1
    data[pointer] += 3
    pointer += 1
    data[pointer] += 1
    pointer -= 4
    data[pointer] -= 1
  end
  pointer += 1
  data[pointer] += 1
  pointer += 1
  data[pointer] += 1
  pointer += 1
  data[pointer] -= 1
  pointer += 2
  data[pointer] += 1
  until data[pointer] == 0
    pointer -= 1
  end
  pointer -= 1
  data[pointer] -= 1
end
pointer += 2
putc data[pointer]
pointer += 1
data[pointer] -= 3
putc data[pointer]
data[pointer] += 7
putc data[pointer]
putc data[pointer]
data[pointer] += 3
putc data[pointer]
pointer += 2
putc data[pointer]
pointer -= 1
data[pointer] -= 1
putc data[pointer]
pointer -= 1
putc data[pointer]
data[pointer] += 3
putc data[pointer]
data[pointer] -= 6
putc data[pointer]
data[pointer] -= 8
putc data[pointer]
pointer += 2
data[pointer] += 1
putc data[pointer]
pointer += 1
data[pointer] += 2
putc data[pointer]

You'll notice that the generated code is nearly the same as the last version's generated code, but with a lot of duplicate lines merged and indenting.

\$\endgroup\$
8
\$\begingroup\$

You're correct that nesting a case in an elsif ladder is a bit clunky here, but the two things that jump out at me as making the code difficult to comprehend are: 1) we have to know everything that's going on with the Regexp passed to scan() in order to figure out the intent of the if clauses, and 2) the variable string is not a String, but an Array.

You can remove the capture groups from your Regexp to just get an Array of Strings back from scan() (instead of an Array of Arrays of Strings), and figure out what exactly was matched by looking at the strings themselves - which means you don't need a nested case anymore, and makes it a little more obvious what scan() is doing without having to actually parse its argument.

Like this:

# example.rb
code = '--+++.,,'
code.scan(/\++|\-+|[.,]/).map { |str|
  case str[0]
  when '+'
    "Plus signs: #{str.length}"
  when '-'
    "Minus signs: #{str.length}"
  when '.'
    'Dot!'
  when ','
    'Comma!'
  end
}.each {|ll| puts ll}

Produces:

$ ruby example.rb 
Minus signs: 2
Plus signs: 3
Dot!
Comma!
Comma!
\$\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.