Skip to content

Instantly share code, notes, and snippets.

@marek22k
Created January 12, 2022 22:57
Show Gist options
  • Select an option

  • Save marek22k/837ca65a267b8b8b7c83a9682de3387d to your computer and use it in GitHub Desktop.

Select an option

Save marek22k/837ca65a267b8b8b7c83a9682de3387d to your computer and use it in GitHub Desktop.
Ruby script that compiles Brainfuck code into C code.
def print_help
puts <<HELP
ruby ./bf_compile.rb [OPTIONS] [NUM_CELLS] file_to_compile.bf
Options:
-comments Don't translate Brainfuck comments into C
-stdlib Do not use stdlib.h to determine the successful exit status (EXIT_SUCCESSFUL)
-static Deactivates the analysis in order to generate a static output for input-independent code.
-analyzer Deactivates the analyzer
+tabs Used tabs instead of spaces for the indentations
+cell See below
NUM_CELLS: Specify the number of cells. The +cell option must be activated for this. (default 255)
HELP
exit!
end
if ["help", "-h", "--help"].any? { |s| ARGV.include? s }
print_help
end
require "erb"
# Template to compile a static program
STATIC_PROG = <<CFILE
#include <stdio.h><% if stdlib %>
#include <stdlib.h><% end %>
int main()
{
<%= ws %>unsigned char arr[] = <%= c_array %>;
<%= ws %>printf("%s", arr);
<%= ws %>return <%= stdlib ?"EXIT_SUCCESS":"0" %>;
}
CFILE
# Arguments, which were partially passed via the command line
flags = [:interpret_comments, :stdlib, :static, :analyzer]
flags.delete :interpret_comments if ARGV.include? "-comments"
flags.delete :stdlib if ARGV.include? "-stdlib"
flags.delete :static if ARGV.include? "-static"
flags.delete :analyzer if ARGV.include? "-analyzer"
flags.push :tabs if ARGV.include? "+tabs"
flags.push :cells if ARGV.include? "+cells"
ws = flags.include?(:tabs) ? "\t" : " "
puts "Whitespace: #{ws.inspect}"
puts "Reading file..."
filename = ARGV[-1]
if filename == nil
puts "No file name was given"
print_help
elsif ! File.readable?(filename)
puts "Cannot read file #{filename}"
exit! false
end
puts "Loading compiler..."
class Compiler
BF_CHARS = "<>+-,.[]".split ""
IGNORE_CMT_CHARS = "\n\t\r".split ""
C_FILE_BEGIN = <<CFILE
#include <stdio.h><% if @stdlib %>
#include <stdlib.h><% end %>
int main()
{
<%= @whitespace %>unsigned char arr[<%= @num_cells %>];
<%= @whitespace %>for (<%= @stdlib ? "size_t" : "unsigned int" %> i = 0; i < <%= @num_cells %>; i++)
<%= @whitespace %>{
<%= @whitespace * 2 %>arr[i] = 0;
<%= @whitespace %>}
<%= @whitespace %>unsigned char * ptr = arr;
CFILE
C_FILE_END = <<CFILE
<%= @whitespace %>return <%= @stdlib?"EXIT_SUCCESS":"0" %>;
}
CFILE
INC_POINTER = <<CFILE
<%= @whitespace * (spaces + 1) %>ptr++;
CFILE
DEC_POINTER = <<CFILE
<%= @whitespace * (spaces + 1) %>ptr--;
CFILE
INC_VALUE = <<CFILE
<%= @whitespace * (spaces + 1) %>(*ptr)++;
CFILE
DEC_VALUE = <<CFILE
<%= @whitespace * (spaces + 1) %>(*ptr)--;
CFILE
INPUT_CHAR = <<CFILE
<%= @whitespace * (spaces + 1) %>*ptr = getchar();
CFILE
OUTPUT_CHAR = <<CFILE
<%= @whitespace * (spaces + 1) %>putchar(*ptr);
CFILE
LOOP_BEGIN = <<CFILE
<%= @whitespace * (spaces + 1) %>while(*ptr)
<%= @whitespace * (spaces + 1) %>{
CFILE
LOOP_END = <<CFILE
<%= @whitespace * (spaces) %>}
CFILE
COMMENT_BEGIN = <<CFILE
<%= @whitespace * (nested + 1) %>/*
CFILE
COMMENT_BEGIN.chomp!
COMMENT_END = <<CFILE
*/
CFILE
attr_reader :ccode
def initialize code, num_cells, ws, ic = true, stdlib = true, output = false
@bf = code
@num_cells = num_cells
@ccode = ""
@whitespace = ws
@ic = ic
@stdlib = stdlib
@output = output
end
def clear
@ccode = ""
end
def compile
@ccode = ERB.new(C_FILE_BEGIN).result binding
nested = 0
cmt = false
print " " if @output
@bf.each_char { |c|
if BF_CHARS.any? { |s| s == c }
if cmt
@ccode += ERB.new(COMMENT_END).result binding
cmt = ! cmt
end
print "\b#{c}" if @output
res = compile_bf_char c, nested
@ccode += res[0]
nested += res[1]
elsif @ic && ! IGNORE_CMT_CHARS.any? { |s| s == c}
if ! cmt
@ccode += ERB.new(COMMENT_BEGIN).result binding
cmt = ! cmt
end
@ccode += c
end
}
@ccode += ERB.new(COMMENT_END).result binding if cmt
@ccode += ERB.new(C_FILE_END).result binding
end
def compile_bf_char c, spaces
code = nil
nested = 0
case c
when "<"
code = ERB.new(DEC_POINTER).result binding
when ">"
code = ERB.new(INC_POINTER).result binding
when "+"
code = ERB.new(INC_VALUE).result binding
when "-"
code = ERB.new(DEC_VALUE).result binding
when "["
code = ERB.new(LOOP_BEGIN).result binding
nested = 1
when "]"
code = ERB.new(LOOP_END).result binding
nested = -1
when ","
code = ERB.new(INPUT_CHAR).result binding
when "."
code = ERB.new(OUTPUT_CHAR).result binding
end
return [code, nested]
end
end
puts "Loading analyzer..."
class Analyzer
BF_CHARS = "<>+-,.[]".split ""
attr_reader :rounds, :max, :output
def initialize code, process_output = false
@bf = code
@code_pointer = 0
@cells = Array.new
@pointer = 0
@rounds = 0
@max = 1
@output = []
@process_output = process_output
end
def clear
@code_pointer = 0
@cells = Array.new
@pointer = 0
@rounds = 0
@max = 1
@output = []
end
# The Analyzer works similar to a BF interpreter.
# However, it does not accept any input. It determines the maximum number of
# cells, the number of steps and the output.
def analyze
loops = []
while @code_pointer < @bf.length
c = @bf[@code_pointer]
print "\b#{c}" if @process_output && BF_CHARS.any? { |s| s == c }
case c
when "<"
@pointer -= 1
when ">"
@pointer += 1
when "+"
@cells[@pointer] = @cells[@pointer].to_i + 1
when "-"
@cells[@pointer] -= 1 if @cells[@pointer].to_i > 0
when "."
@output << @cells[@pointer].to_i
when "["
loops << @code_pointer
@code_pointer = find_closing_bracket - 1
when "]"
if @cells[@pointer].to_i == 0
loops.pop
else
@code_pointer = loops[-1]
end
end
@code_pointer += 1
@rounds += 1
@max = @pointer if @pointer > max
end
@max += 1
end
protected
def find_closing_bracket
counter = 0
search = @code_pointer + 1
while search < @bf.length
case @bf[search]
when "["
counter += 1
when "]"
if counter > 0
counter -= 1
else
break
end
end
search += 1
end
return search
end
end
bf = File.read filename
num_cells = 255
code = nil
input_indepented_code = ! bf.include?(",")
# If the user allows static compilation, the code does not request any input,
# the use of the analyzer is allowed and no number of cells is specified, then...
if input_indepented_code && flags.include?(:static) && flags.include?(:analyzer) && ! flags.include?(:cells)
puts "Generating static program... (Use -static option to disable)"
ana = Analyzer.new bf
puts "Analying code... (Use -analyzer option to disable)"
ana.analyze
if ana.output.empty?
puts "Program does not contain any output. The compilation is aborted."
exit! false
else
c_array = "\{#{ana.output.join(", ")}, 0\}"
stdlib = flags.include? :stdlib
code = ERB.new(STATIC_PROG).result binding
end
else
# If the code does not request any input, allows the use of the analyzer and
# no number of cells is specified, then...
if input_indepented_code && ! flags.include?(:cells) && flags.include?(:analyzer)
ana = Analyzer.new bf
puts "Analying code... (Use -analyzer option to disable)"
ana.analyze
num_cells = ana.max
puts "Number of cells determined by the analyzer: #{ana.max}"
puts "Number of steps to calculate the output: #{ana.rounds}"
end
if flags.include? :cells
if ARGV[-2]
num_cells = ARGV[-2].to_i
else
puts "The number of cells could not be read. The compilation is canceled."
exit! false
end
end
puts "Number of cells: #{num_cells}"
puts "Compiling..."
com = Compiler.new bf, num_cells, ws, flags.include?(:interpret_comments), flags.include?(:stdlib), true
print "Compile "
com.compile
print "\n"
code = com.ccode
end
puts "Compiled successfully!"
puts "Writing file..."
File.write "#{filename}.c", code
puts "The full compilation process completed successfully."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment