Created
January 12, 2022 22:57
-
-
Save marek22k/837ca65a267b8b8b7c83a9682de3387d to your computer and use it in GitHub Desktop.
Ruby script that compiles Brainfuck code into C code.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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