├── examples ├── empty.b ├── cat.b ├── regression1.b ├── unbalanced_block.b ├── hello.b ├── extra_block_end.b ├── decrement_1073741824.b └── regression2.b ├── CPPLINT.cfg ├── pylintrc ├── .gitignore ├── Makefile ├── LICENSE ├── bf_interpreter.h ├── bf_runner.h ├── README.md ├── bf_compile_and_go.h ├── bf_jit.h ├── bf_interpreter.cpp ├── bf_jit.cpp ├── benchmark.py ├── bf_main.cpp ├── bf_compile_and_go.cpp └── test_runner.py /examples/empty.b: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/cat.b: -------------------------------------------------------------------------------- 1 | ,[.,] 2 | -------------------------------------------------------------------------------- /examples/regression1.b: -------------------------------------------------------------------------------- 1 | >,< 2 | , 3 | .>. -------------------------------------------------------------------------------- /examples/unbalanced_block.b: -------------------------------------------------------------------------------- 1 | [][][++ 2 | -------------------------------------------------------------------------------- /CPPLINT.cfg: -------------------------------------------------------------------------------- 1 | filter=-build/include,-runtime/int,-readability/function 2 | -------------------------------------------------------------------------------- /examples/hello.b: -------------------------------------------------------------------------------- 1 | ++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------.>+.>. -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [BASIC] 2 | disable=locally-disabled 3 | docstring-min-length=10 4 | function-rgx=[a-z_][a-z0-9_]{2,64}$ 5 | method-rgx=[a-z_][a-z0-9_]{2,64}$ 6 | -------------------------------------------------------------------------------- /examples/extra_block_end.b: -------------------------------------------------------------------------------- 1 | ] 2 | Print "Hello World!" 3 | ++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------.>+.>. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # Compiled Object files 6 | *.slo 7 | *.lo 8 | *.o 9 | *.obj 10 | 11 | # Precompiled Headers 12 | *.gch 13 | *.pch 14 | 15 | # Compiled Dynamic libraries 16 | *.so 17 | *.dylib 18 | *.dll 19 | 20 | 21 | # Compiled Static libraries 22 | *.lai 23 | *.la 24 | *.a 25 | *.lib 26 | 27 | # Executables 28 | bf 29 | *.exe 30 | *.out 31 | *.app 32 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CC=g++ 2 | CPPFLAGS=-std=c++11 -Wall -Wextra -O3 3 | 4 | all: bf 5 | 6 | bf: bf_main.cpp *.cpp *.h 7 | $(CC) $(CPPFLAGS) -m64 bf_main.cpp bf_compile_and_go.cpp bf_interpreter.cpp bf_jit.cpp -o bf 8 | 9 | test: bf 10 | python test_runner.py 11 | 12 | presubmit: test *.cpp *.h 13 | python tools/cpplint.py *.cpp *.h 14 | (which pylint >/dev/null && pylint --reports=n test_runner benchmark) || (which pylint >/dev/null || echo "WARNING: pylint is not installed, skipping Python checks.") 15 | 16 | benchmark: bf 17 | python benchmark.py 18 | 19 | clean: 20 | rm -rf *.o *.pyc bf 21 | -------------------------------------------------------------------------------- /examples/decrement_1073741824.b: -------------------------------------------------------------------------------- 1 | // Requires 32^6=1073741824 decrements in the inner-most loop 2 | ++++++++++++++++++++++++++++++++ 3 | [ 4 | >++++++++++++++++++++++++++++++++ 5 | [ 6 | >++++++++++++++++++++++++++++++++ 7 | [ 8 | >++++++++++++++++++++++++++++++++ 9 | [ 10 | >++++++++++++++++++++++++++++++++ 11 | [ 12 | >++++++++++++++++++++++++++++++++ 13 | [-] 14 | < 15 | - 16 | ] 17 | < 18 | - 19 | ] 20 | < 21 | - 22 | ] 23 | < 24 | - 25 | ] 26 | < 27 | - 28 | ] 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Brian Quinlan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /bf_interpreter.h: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Brian Quinlan 2 | // See "LICENSE" file for details. 3 | // 4 | // Implements a BrainfuckRunner that interprets the Brainfuck source one command 5 | // at a time. 6 | 7 | #ifndef BF_INTERPRETER_H_ 8 | #define BF_INTERPRETER_H_ 9 | 10 | #include 11 | #include 12 | 13 | #include "bf_runner.h" 14 | 15 | using std::map; 16 | using std::string; 17 | 18 | class BrainfuckInterpreter : public BrainfuckRunner { 19 | public: 20 | BrainfuckInterpreter(); 21 | virtual bool init(string::const_iterator start, 22 | string::const_iterator end); 23 | virtual void* run(BrainfuckReader reader, 24 | void* reader_arg, 25 | BrainfuckWriter writer, 26 | void* writer_arg, 27 | void* memory); 28 | 29 | private: 30 | string::const_iterator start_; 31 | string::const_iterator end_; 32 | 33 | // Maps the position of a Brainfuck block start to the token after the end of 34 | // the block e.g. 35 | // ,[..,] 36 | // ^ ^ 37 | // x => y 38 | map loop_start_to_after_end_; 39 | }; 40 | 41 | #endif // BF_INTERPRETER_H_ 42 | -------------------------------------------------------------------------------- /bf_runner.h: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Brian Quinlan 2 | // See "LICENSE" file for details. 3 | // 4 | // An abstract interface for classes that can execute Brainfuck code (see 5 | // http://en.wikipedia.org/wiki/Brainfuck). 6 | 7 | #ifndef BF_RUNNER_H_ 8 | #define BF_RUNNER_H_ 9 | 10 | #include 11 | 12 | using std::string; 13 | 14 | typedef bool (*BrainfuckWriter)(void* writer_arg, char c); 15 | typedef char (*BrainfuckReader)(void* reader_arg); 16 | 17 | class BrainfuckRunner { 18 | public: 19 | // Initialize the runner using the Brainfuck opcodes between the given 20 | // iterators. Returns false if the Brainfuck code is invalid (i.e. there 21 | // is a "[" with a matching "]") or there is another initialization error. 22 | virtual bool init(string::const_iterator start, 23 | string::const_iterator end) = 0; 24 | 25 | // Runs the Brainfuck code given in "init" using the provided memory. 26 | // When "," is evaluated, call reader(reader_arg). 27 | // When "." is evaluated, call writer(writer_arg, ) 28 | // The return value is the location of the data pointer 29 | // (see http://en.wikipedia.org/wiki/Brainfuck#Commands) when the code is 30 | // finished being executed. 31 | virtual void* run(BrainfuckReader reader, 32 | void* reader_arg, 33 | BrainfuckWriter writer, 34 | void* writer_arg, 35 | void* memory) = 0; 36 | }; 37 | 38 | #endif // BF_RUNNER_H_ 39 | -------------------------------------------------------------------------------- /examples/regression2.b: -------------------------------------------------------------------------------- 1 | # BrainfuckCompileAndGo could not cope with > 127 consecutive shifts 2 | 3 | # Setup memory for Hello World 4 | ++++++++++[>+++++++>++++++++++>+++>+<<<<-] 5 | 6 | # Shift 512 places to the right 7 | >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> [] 8 | >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> [] 9 | >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> [] 10 | >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> [] 11 | 12 | >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> [] 13 | >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> [] 14 | >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> [] 15 | >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> [] 16 | 17 | >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> [] 18 | >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> [] 19 | >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> [] 20 | >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> [] 21 | 22 | >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> [] 23 | >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> [] 24 | >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> [] 25 | >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> [] 26 | 27 | # Shift all the way back to the left 28 | <<<<<<<<<<<<<<<< <<<<<<<<<<<<<<<< 29 | <<<<<<<<<<<<<<<< <<<<<<<<<<<<<<<< 30 | <<<<<<<<<<<<<<<< <<<<<<<<<<<<<<<< 31 | <<<<<<<<<<<<<<<< <<<<<<<<<<<<<<<< 32 | 33 | <<<<<<<<<<<<<<<< <<<<<<<<<<<<<<<< 34 | <<<<<<<<<<<<<<<< <<<<<<<<<<<<<<<< 35 | <<<<<<<<<<<<<<<< <<<<<<<<<<<<<<<< 36 | <<<<<<<<<<<<<<<< <<<<<<<<<<<<<<<< 37 | 38 | <<<<<<<<<<<<<<<< <<<<<<<<<<<<<<<< 39 | <<<<<<<<<<<<<<<< <<<<<<<<<<<<<<<< 40 | <<<<<<<<<<<<<<<< <<<<<<<<<<<<<<<< 41 | <<<<<<<<<<<<<<<< <<<<<<<<<<<<<<<< 42 | 43 | <<<<<<<<<<<<<<<< <<<<<<<<<<<<<<<< 44 | <<<<<<<<<<<<<<<< <<<<<<<<<<<<<<<< 45 | <<<<<<<<<<<<<<<< <<<<<<<<<<<<<<<< 46 | <<<<<<<<<<<<<<<< <<<<<<<<<<<<<<<< 47 | 48 | # Prints "Hello World!" assuming that the shift back to offset 0 worked 49 | >++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------.>+.>. 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Brainfuck JIT 2 | ============= 3 | 4 | ## Introduction 5 | 6 | I started this project to help myself gain a practical understanding of interpreting, compilation and Just-In-Time compilation (JIT). I chose Brainfuck (http://en.wikipedia.org/wiki/Brainfuck) as the source language because it is simple. 7 | 8 | The total size of the entire project (including the interpreter, compiler and JIT) is <1,000 lines of C++ code. 9 | 10 | ## Getting Started 11 | 12 | To get the source code, build it and run the "Hello, world!" example: 13 | 14 | ```bash 15 | git clone https://github.com/brianquinlan/brainfuck-jit.git 16 | cd brainfuck-jit 17 | make test 18 | ./bf --mode=cag examples/hello.b # Run ./bf --help for a list of execution options. 19 | ``` 20 | 21 | ## First Steps 22 | 23 | The first step is to acquire a basic understanding of the Brainfuck language. The ability to write programs in it is optional :-) 24 | - http://en.wikipedia.org/wiki/Brainfuck [to learn Brainfuck commands] 25 | - http://fatiherikli.github.io/brainfuck-visualizer/ [runs Brainfuck in the browser] 26 | 27 | Then look at the interpreter implementation: 28 | - https://github.com/brianquinlan/brainfuck-jit/blob/master/bf_interpreter.h 29 | - https://github.com/brianquinlan/brainfuck-jit/blob/master/bf_interpreter.cpp 30 | 31 | Once you understand the interpreter implementation, the JIT implementation should then be understandable: 32 | - https://github.com/brianquinlan/brainfuck-jit/blob/master/bf_jit.h 33 | - https://github.com/brianquinlan/brainfuck-jit/blob/master/bf_jit.cpp 34 | 35 | The compiler implementation requires a bit of assembly knowledge to understandable: 36 | - https://github.com/brianquinlan/brainfuck-jit/blob/master/bf_compile_and_go.h 37 | - https://github.com/brianquinlan/brainfuck-jit/blob/master/bf_compile_and_go.cpp 38 | -------------------------------------------------------------------------------- /bf_compile_and_go.h: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Brian Quinlan 2 | // See "LICENSE" file for details. 3 | // 4 | // Implements a BrainfuckRunner that compiles the complete source into amd64 5 | // machine code. For a description of the team "compile and go", see: 6 | // http://en.wikipedia.org/wiki/Compile_and_go_system 7 | 8 | #ifndef BF_COMPILE_AND_GO_H_ 9 | #define BF_COMPILE_AND_GO_H_ 10 | 11 | #include 12 | #include 13 | 14 | #include "bf_runner.h" 15 | 16 | using std::string; 17 | using std::map; 18 | 19 | class BrainfuckCompileAndGo : public BrainfuckRunner { 20 | public: 21 | BrainfuckCompileAndGo(); 22 | virtual bool init(string::const_iterator start, 23 | string::const_iterator end); 24 | virtual void* run(BrainfuckReader reader, 25 | void* reader_arg, 26 | BrainfuckWriter writer, 27 | void* writer_arg, 28 | void* memory); 29 | 30 | virtual ~BrainfuckCompileAndGo(); 31 | 32 | private: 33 | int executable_size_; 34 | void* executable_; 35 | int exit_offset_; 36 | 37 | void add_jne_to_exit(string* code); 38 | void add_jl_to_exit(string* code); 39 | void add_jmp_to_offset(int offset, string* code); 40 | void add_jmp_to_exit(string* code); 41 | void emit_offset_table(map* offset_to_change, 42 | int8_t* offset, 43 | string *code); 44 | bool generate_sequence_code(string::const_iterator start, 45 | string::const_iterator end, 46 | string* code); 47 | bool generate_loop_code(string::const_iterator start, 48 | string::const_iterator end, 49 | string* code); 50 | void generate_read_code(string* code); 51 | void generate_write_code(string* code); 52 | }; 53 | 54 | #endif // BF_COMPILE_AND_GO_H_ 55 | -------------------------------------------------------------------------------- /bf_jit.h: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Brian Quinlan 2 | // See "LICENSE" file for details. 3 | // 4 | // Implements a BrainfuckRunner that interprets the Brainfuck source one command 5 | // at a time but will use BrainfuckCompileAndGo to execute loops that are 6 | // run frequently. 7 | 8 | #ifndef BF_JIT_H_ 9 | #define BF_JIT_H_ 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | #include "bf_runner.h" 16 | #include "bf_compile_and_go.h" 17 | 18 | using std::map; 19 | using std::string; 20 | using std::shared_ptr; 21 | 22 | class BrainfuckJIT : public BrainfuckRunner { 23 | public: 24 | BrainfuckJIT(); 25 | virtual bool init(string::const_iterator start, 26 | string::const_iterator end); 27 | virtual void* run(BrainfuckReader reader, 28 | void* reader_arg, 29 | BrainfuckWriter writer, 30 | void* writer_arg, 31 | void* memory); 32 | 33 | private: 34 | struct Loop { 35 | Loop() : condition_evaluation_count(0) { } 36 | explicit Loop(string::const_iterator after) : 37 | after_end(after), condition_evaluation_count(0) { } 38 | 39 | // The position of the Brainfuck token after the end of the loop. 40 | string::const_iterator after_end; 41 | // The number of types that the loop condition (e.g. "[") has been 42 | // evaluated. Note that the count will not be updated after the loop has 43 | // been JITed. 44 | uint64_t condition_evaluation_count; 45 | // The compiled code that represents the loop. Will be NULL until the loop 46 | // is JITed. 47 | shared_ptr compiled; 48 | }; 49 | 50 | string::const_iterator start_; 51 | string::const_iterator end_; 52 | 53 | // Maps the position of a Brainfuck block start to Loop e.g. 54 | // ,[..,] 55 | // ^ ^ 56 | // x y => loop_start_to_loop_[x] = Loop(y); 57 | map loop_start_to_loop_; 58 | }; 59 | 60 | #endif // BF_JIT_H_ 61 | -------------------------------------------------------------------------------- /bf_interpreter.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Brian Quinlan 2 | // See "LICENSE" file for details. 3 | 4 | #include 5 | #include 6 | 7 | #include "bf_interpreter.h" 8 | 9 | using std::stack; 10 | 11 | BrainfuckInterpreter::BrainfuckInterpreter() {} 12 | 13 | bool BrainfuckInterpreter::init(string::const_iterator start, 14 | string::const_iterator end) { 15 | start_ = start; 16 | end_ = end; 17 | 18 | // Build the mapping from the position of the start of a block (i.e. "]") to 19 | // the character *after* the end of the block. 20 | stack block_starts; 21 | for (string::const_iterator it = start; it != end; ++it) { 22 | if (*it == '[') { 23 | block_starts.push(it); 24 | } else if (*it == ']') { 25 | if (block_starts.size() != 0) { 26 | const string::const_iterator &loop_start = block_starts.top(); 27 | loop_start_to_after_end_[loop_start] = it+1; 28 | block_starts.pop(); 29 | } 30 | } 31 | } 32 | 33 | if (block_starts.size() != 0) { 34 | fprintf( 35 | stderr, 36 | "Unable to find loop end in block starting with: %s\n", 37 | string(block_starts.top(), end).c_str()); 38 | return false; 39 | } 40 | return true; 41 | } 42 | 43 | void* BrainfuckInterpreter::run(BrainfuckReader reader, 44 | void* reader_arg, 45 | BrainfuckWriter writer, 46 | void* writer_arg, 47 | void* memory) { 48 | uint8_t* byte_memory = reinterpret_cast(memory); 49 | // When processing a "[", push the position of that command onto a stack so 50 | // that we can quickly return to the start of the block when then "]" is 51 | // interpreted. 52 | stack return_stack; 53 | 54 | for (string::const_iterator it = start_; it != end_;) { 55 | switch (*it) { 56 | case '<': 57 | --byte_memory; 58 | ++it; 59 | break; 60 | case '>': 61 | ++byte_memory; 62 | ++it; 63 | break; 64 | case '-': 65 | *byte_memory -= 1; 66 | ++it; 67 | break; 68 | case '+': 69 | *byte_memory += 1; 70 | ++it; 71 | break; 72 | case ',': 73 | *byte_memory = reader(reader_arg); 74 | ++it; 75 | break; 76 | case '.': 77 | writer(writer_arg, *byte_memory); 78 | ++it; 79 | break; 80 | case '[': 81 | if (*byte_memory) { 82 | return_stack.push(it); 83 | ++it; 84 | } else { 85 | it = loop_start_to_after_end_[it]; 86 | } 87 | break; 88 | case ']': 89 | if (return_stack.size() != 0) { 90 | it = return_stack.top(); 91 | return_stack.pop(); 92 | } else { 93 | ++it; 94 | } 95 | break; 96 | default: 97 | ++it; 98 | break; 99 | } 100 | } 101 | return byte_memory; 102 | } 103 | -------------------------------------------------------------------------------- /bf_jit.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Brian Quinlan 2 | // See "LICENSE" file for details. 3 | 4 | #include 5 | #include 6 | 7 | #include "bf_jit.h" 8 | 9 | using std::stack; 10 | 11 | // The total number of times that a loop condition (e.g. "[") must be evaluated 12 | // before the loop is compiled. 13 | const int kLoopCompilationThreshold = 20; 14 | 15 | BrainfuckJIT::BrainfuckJIT() {} 16 | 17 | bool BrainfuckJIT::init(string::const_iterator start, 18 | string::const_iterator end) { 19 | start_ = start; 20 | end_ = end; 21 | 22 | // Build the mapping from the position of the start of a block (i.e. "]") to 23 | // a Loop struct. 24 | stack block_starts; 25 | for (string::const_iterator it = start; it != end; ++it) { 26 | if (*it == '[') { 27 | block_starts.push(it); 28 | } else if (*it == ']') { 29 | if (block_starts.size() != 0) { 30 | const string::const_iterator &loop_start = block_starts.top(); 31 | loop_start_to_loop_[loop_start] = Loop(it+1); 32 | block_starts.pop(); 33 | } 34 | } 35 | } 36 | 37 | if (block_starts.size() != 0) { 38 | fprintf( 39 | stderr, 40 | "Unable to find loop end in block starting with: %s\n", 41 | string(block_starts.top(), end).c_str()); 42 | return false; 43 | } 44 | return true; 45 | } 46 | 47 | void* BrainfuckJIT::run(BrainfuckReader reader, 48 | void* reader_arg, 49 | BrainfuckWriter writer, 50 | void* writer_arg, 51 | void* memory) { 52 | uint8_t* byte_memory = reinterpret_cast(memory); 53 | // When processing a "[", push the position of that command onto a stack so 54 | // that we can quickly return to the start of the block when then "]" is 55 | // interpreted. 56 | stack return_stack; 57 | 58 | for (string::const_iterator it = start_; it != end_;) { 59 | switch (*it) { 60 | case '<': 61 | --byte_memory; 62 | ++it; 63 | break; 64 | case '>': 65 | ++byte_memory; 66 | ++it; 67 | break; 68 | case '-': 69 | *byte_memory -= 1; 70 | ++it; 71 | break; 72 | case '+': 73 | *byte_memory += 1; 74 | ++it; 75 | break; 76 | case ',': 77 | *byte_memory = reader(reader_arg); 78 | ++it; 79 | break; 80 | case '.': 81 | writer(writer_arg, *byte_memory); 82 | ++it; 83 | break; 84 | case '[': 85 | { 86 | Loop &loop = loop_start_to_loop_[it]; 87 | 88 | if (loop.compiled == nullptr && 89 | loop.condition_evaluation_count > kLoopCompilationThreshold) { 90 | shared_ptr compiled( 91 | new BrainfuckCompileAndGo()); 92 | string::const_iterator compilation_end(loop.after_end); 93 | 94 | if (!compiled->init(it, compilation_end)) { 95 | fprintf(stderr, 96 | "Unable to compile code: %s\n", 97 | string(it, compilation_end).c_str()); 98 | } else { 99 | loop.compiled = compiled; 100 | } 101 | } 102 | 103 | if (loop.compiled) { 104 | byte_memory = reinterpret_cast( 105 | loop.compiled->run(reader, 106 | reader_arg, 107 | writer, 108 | writer_arg, 109 | byte_memory)); 110 | it = loop_start_to_loop_[it].after_end; 111 | } else { 112 | ++loop.condition_evaluation_count; 113 | if (*byte_memory) { 114 | return_stack.push(it); 115 | ++it; 116 | } else { 117 | it = loop_start_to_loop_[it].after_end; 118 | } 119 | } 120 | } 121 | break; 122 | case ']': 123 | if (return_stack.size() != 0) { 124 | it = return_stack.top(); 125 | return_stack.pop(); 126 | } else { 127 | ++it; 128 | } 129 | break; 130 | default: 131 | ++it; 132 | break; 133 | } 134 | } 135 | return byte_memory; 136 | } 137 | -------------------------------------------------------------------------------- /benchmark.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2014 Brian Quinlan 4 | # See "LICENSE" file for details. 5 | 6 | # pylint: disable=print-statement 7 | 8 | """Run benchmarks among the various execution modes.""" 9 | 10 | from __future__ import absolute_import 11 | from __future__ import division 12 | 13 | import argparse 14 | import itertools 15 | import tempfile 16 | import test_runner 17 | import timeit 18 | 19 | HEADER = ( 20 | #pylint: disable=bad-continuation 21 | " Interpreter Compiler JIT") 22 | 23 | TRIALS_HEADER_FORMAT = ( 24 | #pylint: disable=bad-continuation 25 | "============================================================================\n" 26 | "Max Loop Nesting: %d\n" 27 | "============================================================================") 28 | 29 | TRIAL_FORMAT = ( 30 | #pylint: disable=bad-continuation 31 | "Trial %2d: %6.2f %6.2f %6.2f") 32 | 33 | TRIAL_SUMMARY_FORMAT = ( 34 | #pylint: disable=bad-continuation 35 | "----------------------------------------------------------------------------\n" 36 | "Total (range): %6.2f %6.2f (%0.2f-%0.2f) %6.2f (%0.2f-%0.2f)\n" 37 | ) 38 | 39 | def time(mode, path, repeat, number): 40 | times = timeit.repeat( 41 | ("returncode, _, stderr = run_brainfuck(['--mode=%s', %r]); " + 42 | "assert returncode == 0, 'returncode = %%d, stderr = %%r' %% (" + 43 | " returncode, stderr)") % (mode, path), 44 | setup="from test_runner import run_brainfuck", 45 | repeat=repeat, 46 | number=number) 47 | return min(times) 48 | 49 | 50 | def time_all(trial_number, path, repeat, number): 51 | """Time all execution modes with the given file.""" 52 | 53 | interpreter_min_time = time('i', path, repeat, number) 54 | compiler_min_time = time('cag', path, repeat, number) 55 | jit_min_time = time('jit', path, repeat, number) 56 | 57 | print TRIAL_FORMAT % ( 58 | trial_number, 59 | interpreter_min_time, 60 | compiler_min_time, 61 | jit_min_time) 62 | return interpreter_min_time, compiler_min_time, jit_min_time 63 | 64 | 65 | def main(): # pylint: disable=missing-docstring 66 | parser = argparse.ArgumentParser( 67 | description='Benchmark the bf executable') 68 | 69 | parser.add_argument( 70 | '--repeat', '-r', 71 | dest='repeat', action='store', type=int, default=5, 72 | help='the number of times to collect before the minimum is taken') 73 | 74 | parser.add_argument( 75 | '--number', '-n', 76 | dest='number', action='store', type=int, default=20, 77 | help=('the number of times to run each command before collecting the ' 78 | 'time')) 79 | 80 | parser.add_argument( 81 | '--trials', '-t', 82 | dest='num_trials', action='store', type=int, default=20, 83 | help=('the of randomly generated code samples to use for each loop ' 84 | 'level')) 85 | 86 | options = parser.parse_args() 87 | 88 | print 89 | print HEADER 90 | for max_nested_loops in itertools.count(): 91 | print TRIALS_HEADER_FORMAT % max_nested_loops 92 | times = [] 93 | for trial_number in range(options.num_trials): 94 | brainfuck_code = test_runner.generate_brainfuck_code( 95 | '<>+-[]\n', 1024 * 1024, max_nested_loops) 96 | # pylint doesn't seem to understand with statements 97 | # pylint: disable=bad-continuation 98 | with tempfile.NamedTemporaryFile( 99 | suffix='.b', delete=False) as brainfuck_source_file: 100 | brainfuck_source_file.write(brainfuck_code) 101 | brainfuck_source_file.close() 102 | times.append( 103 | time_all(trial_number+1, 104 | brainfuck_source_file.name, 105 | options.repeat, 106 | options.number)) 107 | interpreter_mean = sum(i for (i, _, _) in times) 108 | compiler_mean = sum(c for (_, c, _) in times) 109 | jit_mean = sum(j for (_, _, j) in times) 110 | 111 | jit_best_relative = min(j/i for (i, _, j) in times) 112 | jit_worst_relative = max(j/i for (i, _, j) in times) 113 | 114 | compiler_best_relative = min(c/i for (i, c, _) in times) 115 | compiler_worst_relative = max(c/i for (i, c, _) in times) 116 | 117 | print TRIAL_SUMMARY_FORMAT % ( 118 | interpreter_mean, 119 | compiler_mean, compiler_best_relative, compiler_worst_relative, 120 | jit_mean, jit_best_relative, jit_worst_relative) 121 | 122 | 123 | if __name__ == '__main__': 124 | main() 125 | 126 | -------------------------------------------------------------------------------- /bf_main.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Brian Quinlan 2 | // See "LICENSE" file for details. 3 | // 4 | // Executes Brainfuck code stored in a file. Flags control the execution mode. 5 | // See http://en.wikipedia.org/wiki/Brainfuck. 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | #include "bf_runner.h" 17 | #include "bf_compile_and_go.h" 18 | #include "bf_interpreter.h" 19 | #include "bf_jit.h" 20 | 21 | using std::string; 22 | using std::unique_ptr; 23 | using std::vector; 24 | 25 | const size_t kBrainfuckMemorySize = 1024 * 1024; 26 | 27 | const char USAGE[] = "Usage: %s [options] \n" 28 | "Execute the Brainfuck code in the given file e.g.\n" 29 | "%s examples/hello.b\n" 30 | "\n" 31 | "Options:\n" 32 | "--mode=cag : Run using a compiler\n" 33 | "--mode=i : Run using an interpreter\n" 34 | "--mode=jit : Run using a Just-In-Time compiler\n"; 35 | 36 | // Passed to BrainfuckRunner->run(...) to provide output functionality for 37 | // the "." command. 38 | static bool bf_write(void*, char c) { 39 | return putchar(c) != EOF; 40 | } 41 | 42 | // Passed to BrainfuckRunner->run(...) to provide input functionality for 43 | // the "," command. 44 | static char bf_read(void*) { 45 | int c = getchar(); 46 | if (c == EOF) { 47 | return 0; 48 | } else { 49 | return c; 50 | } 51 | } 52 | 53 | int run_brainfuck_program(BrainfuckRunner* runner, 54 | const string& source_file_path) { 55 | FILE *bf_source_file = fopen(source_file_path.c_str(), "rb"); 56 | if (bf_source_file == NULL) { 57 | fprintf(stderr, "Could not open file \"%s\": %s\n", 58 | source_file_path.c_str(), strerror(errno)); 59 | return 1; 60 | } 61 | 62 | if (fseek(bf_source_file, 0, SEEK_END)) { 63 | fprintf(stderr, "Could not seek in \"%s\": %s\n", 64 | source_file_path.c_str(), strerror(ferror(bf_source_file))); 65 | return 1; 66 | } 67 | 68 | long source_size = ftell(bf_source_file); 69 | if (source_size == -1) { 70 | fprintf(stderr, "Could not tell in \"%s\": %s\n", 71 | source_file_path.c_str(), strerror(errno)); 72 | return 1; 73 | } 74 | rewind(bf_source_file); 75 | 76 | char* source_buffer = reinterpret_cast(malloc(source_size)); 77 | size_t amount_read = fread(source_buffer, 1, source_size, bf_source_file); 78 | fclose(bf_source_file); 79 | if (amount_read != static_cast(source_size)) { 80 | fprintf(stderr, "Error reading file \"%s\": %s\n", 81 | source_file_path.c_str(), strerror(errno)); 82 | return 1; 83 | } 84 | 85 | void* memory = calloc(kBrainfuckMemorySize, 1); 86 | if (memory == NULL) { 87 | fprintf(stderr, 88 | "Unable to allocate memory %ld bytes for Brainfuck memory\n", 89 | static_cast(kBrainfuckMemorySize)); 90 | } 91 | 92 | const string source(source_buffer, source_size); 93 | 94 | if (!runner->init(source.begin(), source.end())) { 95 | return 1; 96 | } 97 | 98 | runner->run(bf_read, NULL, bf_write, NULL, memory); 99 | return 0; 100 | } 101 | 102 | int main(int argc, char *argv[]) { 103 | unique_ptr bf(new BrainfuckInterpreter()); 104 | string bf_file; 105 | 106 | for (int i = 1; i < argc; ++i) { 107 | if (argv[i] == string("-h") || 108 | argv[i] == string("-help") || 109 | argv[i] == string("--help") || 110 | argv[i] == string("-?")) { 111 | printf(USAGE, argv[0], argv[0]); 112 | return 0; 113 | } 114 | } 115 | 116 | vector files; 117 | for (int i = 1; i < argc; ++i) { 118 | string arg(argv[i]); 119 | if (arg.find("--") == 0) { 120 | if (arg.find("--mode=") == 0) { 121 | if (arg == "--mode=cag") { 122 | unique_ptr compile_and_go( 123 | new BrainfuckCompileAndGo()); 124 | bf = std::move(compile_and_go); 125 | } else if (arg == "--mode=i") { 126 | unique_ptr interpreter(new BrainfuckInterpreter()); 127 | bf = std::move(interpreter); 128 | } else if (arg == "--mode=jit") { 129 | unique_ptr jit(new BrainfuckJIT()); 130 | bf = std::move(jit); 131 | } else { 132 | fprintf(stderr, "Unexpected mode: %s\n", arg.c_str()); 133 | return 1; 134 | } 135 | } else { 136 | fprintf(stderr, "Unexpected argument: %s\n", arg.c_str()); 137 | return 1; 138 | } 139 | } else { 140 | files.push_back(arg); 141 | } 142 | } 143 | 144 | if (files.size() != 1) { 145 | fputs("You need to specify exactly one Brainfuck file\n", stderr); 146 | printf(USAGE, argv[0], argv[0]); 147 | return 1; 148 | } 149 | 150 | return run_brainfuck_program(bf.get(), files[0]); 151 | } 152 | -------------------------------------------------------------------------------- /bf_compile_and_go.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Brian Quinlan 2 | // See "LICENSE" file for details. 3 | // 4 | // Compiles Brainfuck commands into amd64 machine code and executes it. The 5 | // assembly documentation uses Intel syntax (see 6 | // http://en.wikipedia.org/wiki/X86_assembly_language#Syntax) and was assembled 7 | // using https://defuse.ca/online-x86-assembler.htm. 8 | // 9 | // References: 10 | // http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-manual-325462.pdf 11 | // http://ref.x86asm.net/ 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | #include 22 | 23 | #include "bf_compile_and_go.h" 24 | 25 | typedef void*(*BrainfuckFunction)(BrainfuckWriter writer, 26 | void* write_arg, 27 | BrainfuckReader reader, 28 | void* read_arg, 29 | void* memory); 30 | 31 | // This is the main entry point for the implementation of "BrainfuckFunction". 32 | // It expects it's arguments to be passed as specified in: 33 | // http://www.x86-64.org/documentation/abi.pdf 34 | // - 3.2 Function Calling Sequence 35 | // - 3.2.3 Parameter Passing 36 | const char START[] = 37 | // Some registers must be saved by the called function (the callee) and 38 | // restored on exit if they are changed. Using these registers is 39 | // convenient because it allows us to call the provided "write" and "read" 40 | // functions without worrying about our registers being changed. 41 | // See: 42 | // http://www.x86-64.org/documentation/abi.pdf "Figure 3.4: Register Usage" 43 | "\x41\x54" // push r12 # r12 will store the "write" arg 44 | "\x41\x55" // push r13 # r13 will store the "write_arg" arg 45 | "\x41\x56" // push r14 # r14 will store the "read" arg 46 | "\x55" // push rbp # rbp will store the "read_arg" arg 47 | "\x53" // push rbx # rbx will store the "memory" arg 48 | 49 | // Store the passed arguments into a callee-saved register. 50 | "\x49\x89\xfc" // mov r12,rdi # write function => r12 51 | "\x49\x89\xf5" // mov r13,rsi # write arg 1 => r13 52 | "\x49\x89\xd6" // mov r14,rdx # read function => r14 53 | "\x48\x89\xcd" // mov rbp,rcx # read arg 1 => rbp 54 | "\x4c\x89\xc3"; // mov rbx,r8 # BF memory => rbx 55 | 56 | const char EXIT[] = 57 | "\x48\x89\xd8" // mov rbx,rax # Store return value 58 | "\x5b" // pop rbx 59 | "\x5d" // pop rbp 60 | "\x41\x5e" // pop r14 61 | "\x41\x5d" // pop r13 62 | "\x41\x5c" // pop r12 63 | "\xc3"; // retq 64 | 65 | // , [part1] rax = read(rdp); if (rax == 0) goto exit; ... 66 | const char READ[] = 67 | "\x48\x89\xef" // mov rdi,rbp, 68 | "\x41\xff\xd6" // callq *%r14 69 | "\x48\x83\xf8\x00"; // cmp rax,0 70 | // // jl exit 71 | 72 | // , [part2] ... *rbx = rax; 73 | const char READ_STORE[] = 74 | "\x88\x03"; // mov [rbx],al 75 | 76 | // . rax = write(r13, rbx); if (rax != 1) goto exit; 77 | const char WRITE[] = 78 | "\x4c\x89\xef" // mov rdi,r13 79 | "\x48\x0f\xb6\x33" // movzbq rsi,[%rbx] 80 | "\x41\xff\xd4" // callq *%r12 81 | "\x48\x83\xf8\x01"; // cmp rax,1 82 | // // jne exit 83 | 84 | char LOOP_CMP[] = 85 | "\x80\x3b\x00"; // cmpb rbx,0 86 | 87 | 88 | static bool find_loop_end(string::const_iterator loop_start, 89 | string::const_iterator string_end, 90 | string::const_iterator* loop_end) { 91 | int level = 1; 92 | for (string::const_iterator it = loop_start+1; it != string_end; ++it) { 93 | if (*it == '[') { 94 | level += 1; 95 | } else if (*it == ']') { 96 | level -= 1; 97 | if (level == 0) { 98 | *loop_end = it; 99 | return true; 100 | } 101 | } 102 | } 103 | return false; 104 | } 105 | 106 | void BrainfuckCompileAndGo::add_jne_to_exit(string* code) { 107 | *code += "\x0f\x85"; // jne ... 108 | uint32_t relative_address = exit_offset_ - (code->size() + 4); 109 | *code += string(reinterpret_cast(&relative_address), 4); // ... exit 110 | } 111 | 112 | void BrainfuckCompileAndGo::add_jl_to_exit(string* code) { 113 | *code += "\x0f\x8c"; // jl ... 114 | uint32_t relative_address = exit_offset_ - (code->size() + 4); 115 | *code += string(reinterpret_cast(&relative_address), 4); // ... exit 116 | } 117 | 118 | void BrainfuckCompileAndGo::add_jmp_to_offset(int offset, string* code) { 119 | *code += "\xe9"; // jmp ... 120 | uint32_t relative_address = offset - (code->size() + 4); 121 | *code += string(reinterpret_cast(&relative_address), 4); // ... exit 122 | } 123 | 124 | void BrainfuckCompileAndGo::add_jmp_to_exit(string* code) { 125 | add_jmp_to_offset(exit_offset_, code); 126 | } 127 | 128 | bool BrainfuckCompileAndGo::generate_loop_code(string::const_iterator start, 129 | string::const_iterator end, 130 | string* code) { 131 | // Converts a Brainfuck command sequence like this: 132 | // [] 133 | // Into this: 134 | // loop_start: 135 | // cmpb [rbx],0 136 | // je loop_end 137 | // 138 | // jmp loop_start 139 | // loop_end: 140 | // 141 | 142 | int loop_start = code->size(); 143 | *code += string(LOOP_CMP, sizeof(LOOP_CMP) - 1); 144 | 145 | int jump_start = code->size(); 146 | *code += string("\xde\xad\xbe\xef\xde\xad"); // Reserve 6 bytes for je. 147 | 148 | if (!generate_sequence_code(start+1, end, code)) { 149 | return false; 150 | } 151 | 152 | add_jmp_to_offset(loop_start, code); // Jump back to the start of the loop. 153 | 154 | string jump_to_end = "\x0f\x84"; // je ... 155 | uint32_t relative_end_of_loop = code->size() - 156 | (jump_start + jump_to_end.size() + 4); 157 | jump_to_end += string( 158 | reinterpret_cast(&relative_end_of_loop), 4); // ... loop_end 159 | 160 | code->replace(jump_start, jump_to_end.size(), jump_to_end); 161 | return true; 162 | } 163 | 164 | void BrainfuckCompileAndGo::generate_read_code(string* code) { 165 | *code += string(READ, sizeof(READ) - 1); 166 | add_jl_to_exit(code); 167 | *code += string(READ_STORE, sizeof(READ_STORE) - 1); 168 | } 169 | 170 | void BrainfuckCompileAndGo::generate_write_code(string* code) { 171 | *code += string(WRITE, sizeof(WRITE) - 1); 172 | add_jne_to_exit(code); 173 | } 174 | 175 | // Converts a table of updates to make to Brainfuck memory (using offsets 176 | // relative to the current datapointer location) into instructions. Resets 177 | // the datapointer offset and offset map. 178 | // 179 | // For example these Brainfuck commands: 180 | // "<<<++>>>--->++><>>+>>>" 181 | // 182 | // Would trigger this call: 183 | // emit_offset_table( 184 | // &{{-3, 0x02}, {0, 0xfd}, {1, 0x02}, {2, 0x01}}, &5, code) 185 | // 186 | // Which would add these instructions to code: 187 | // addb [rbx-3],0x02 # Update each memory location with a single instruction. 188 | // addb [rbx],0xfd 189 | // addb [rbx+1],0x02 190 | // addb [rbx+2],0x01 191 | // add rbx,2 # Move the data pointer to it's final offset. 192 | void BrainfuckCompileAndGo::emit_offset_table( 193 | map* offset_to_change, int8_t* offset, string *code) { 194 | for (auto it = offset_to_change->begin(); 195 | it != offset_to_change->end(); 196 | ++it) { 197 | int8_t change_offset = it->first; 198 | uint8_t change_value = it->second; 199 | 200 | if (change_value == 0) { 201 | continue; 202 | } 203 | 204 | *code += "\x80\x43"; // addb [rbx+XX],YY 205 | *code += string(reinterpret_cast(&change_offset), 1); // XX 206 | *code += string(reinterpret_cast(&change_value), 1); // YY 207 | } 208 | 209 | if (*offset != 0) { 210 | *code += "\x48\x83\xc3"; // add rbx ... 211 | *code += string(reinterpret_cast(offset), 1); // ... offset 212 | *offset = 0; 213 | } 214 | offset_to_change->clear(); 215 | } 216 | 217 | bool BrainfuckCompileAndGo::generate_sequence_code(string::const_iterator start, 218 | string::const_iterator end, 219 | string* code) { 220 | int8_t offset = 0; 221 | // Maps offset relative to the datapointer into the amount to change it. 222 | map offset_to_change; 223 | 224 | for (string::const_iterator it = start; it != end; ++it) { 225 | switch (*it) { 226 | case '<': 227 | --offset; 228 | if (offset == 127 || offset == -128) { 229 | emit_offset_table(&offset_to_change, &offset, code); 230 | } 231 | break; 232 | case '>': 233 | ++offset; 234 | if (offset == 127 || offset == -128) { 235 | emit_offset_table(&offset_to_change, &offset, code); 236 | } 237 | break; 238 | case '-': 239 | offset_to_change[offset] -= 1; 240 | break; 241 | case '+': 242 | offset_to_change[offset] += 1; 243 | break; 244 | case ',': 245 | emit_offset_table(&offset_to_change, &offset, code); 246 | generate_read_code(code); 247 | break; 248 | case '.': 249 | emit_offset_table(&offset_to_change, &offset, code); 250 | generate_write_code(code); 251 | break; 252 | case '[': 253 | emit_offset_table(&offset_to_change, &offset, code); 254 | string::const_iterator loop_end; 255 | if (!find_loop_end(it, end, &loop_end)) { 256 | fprintf( 257 | stderr, 258 | "Unable to find loop end in block starting with: %s\n", 259 | string(it, end).c_str()); 260 | return false; 261 | } 262 | if (!generate_loop_code(it, loop_end, code)) { 263 | return false; 264 | } 265 | it = loop_end; 266 | break; 267 | } 268 | } 269 | // The offset table must be emitted because this function is called 270 | // recursively to handle loops. 271 | emit_offset_table(&offset_to_change, &offset, code); 272 | return true; 273 | } 274 | 275 | 276 | BrainfuckCompileAndGo::BrainfuckCompileAndGo() : executable_(NULL) {} 277 | 278 | bool BrainfuckCompileAndGo::init(string::const_iterator start, 279 | string::const_iterator end) { 280 | string code(START, sizeof(START) - 1); 281 | code += "\xeb"; // relative jump; 282 | code += sizeof(EXIT) - 1; 283 | exit_offset_ = code.size(); 284 | code += string(EXIT, sizeof(EXIT) - 1); 285 | 286 | if (!generate_sequence_code(start, end, &code)) { 287 | return false; 288 | } 289 | add_jmp_to_exit(&code); 290 | 291 | executable_size_ = (code.size() / 292 | sysconf(_SC_PAGESIZE) + 1) * sysconf(_SC_PAGESIZE); 293 | 294 | executable_ = mmap( 295 | NULL, 296 | executable_size_, 297 | PROT_READ | PROT_WRITE, 298 | MAP_PRIVATE | MAP_ANON, -1, 0); 299 | if (executable_ == MAP_FAILED) { 300 | fprintf(stderr, "Error making memory executable: %s\n", strerror(errno)); 301 | return false; 302 | } 303 | 304 | memmove(executable_, code.data(), code.size()); 305 | if (mprotect(executable_, executable_size_, PROT_EXEC | PROT_READ) != 0) { 306 | fprintf(stderr, "mprotect failed: %s\n", strerror(errno)); 307 | return false; 308 | } 309 | 310 | return true; 311 | } 312 | 313 | void* BrainfuckCompileAndGo::run(BrainfuckReader reader, 314 | void* reader_arg, 315 | BrainfuckWriter writer, 316 | void* writer_arg, 317 | void* memory) { 318 | return ((BrainfuckFunction)executable_)( 319 | writer, writer_arg, reader, reader_arg, memory); 320 | } 321 | 322 | BrainfuckCompileAndGo::~BrainfuckCompileAndGo() { 323 | if (executable_) { 324 | if (munmap(executable_, executable_size_) != 0) { 325 | fprintf(stderr, "munmap failed: %s\n", strerror(errno)); 326 | } 327 | } 328 | } 329 | 330 | 331 | -------------------------------------------------------------------------------- /test_runner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2014 Brian Quinlan 4 | # See "LICENSE" file for details. 5 | 6 | """Tests for the Brainfuck executable.""" 7 | 8 | from __future__ import absolute_import 9 | 10 | import functools 11 | import os 12 | import os.path 13 | import random 14 | import subprocess 15 | import tempfile 16 | import time 17 | import unittest 18 | 19 | EXECUTABLE_PATH = os.path.join(os.curdir, 'bf') 20 | 21 | 22 | def _check_datapointer_in_range(commands, restore_offset): 23 | """Verify that a sequence of brainfuck commands has an offset >= 0. 24 | 25 | Args: 26 | commands: The sequence of brainfuck commands e.g. "<>>>+-". 27 | restore_offset: A bool indicating whether the memory offset after 28 | the given sequence of commands must be exactly zero. 29 | 30 | Raises: 31 | AssertionError: if the memory offset of the given commands is negative 32 | (or non-zero if restore_offset is True). 33 | """ 34 | offset = 0 35 | for command in commands: 36 | if command == '<': 37 | offset -= 1 38 | elif command == '>': 39 | offset += 1 40 | assert offset >= 0 41 | if restore_offset: 42 | assert offset == 0 43 | 44 | 45 | def _repeat_for_seconds(seconds): 46 | """Repeat the decorated function for the given number of seconds.""" 47 | def repeat(func): 48 | @functools.wraps(func) 49 | def repeater(*args, **kwds): 50 | end_time = time.time() + seconds 51 | while time.time() < end_time: 52 | func(*args, **kwds) 53 | repeater.count += 1 54 | repeater.count = 0 55 | return repeater 56 | return repeat 57 | 58 | 59 | def run_brainfuck(args, stdin=''): 60 | """Run the Brainfuck executable. 61 | 62 | Args: 63 | args: A sequence containing the command line arguments to use when 64 | invoking the executable e.g. ['--mode=i', 'example.b']. 65 | stdin: The str that should be used for stdin. 66 | Returns: 67 | A 3-tuple of: 68 | o the process return code as an int 69 | o the string written to stdout by the process 70 | o the string written to stderr by the process 71 | """ 72 | run = subprocess.Popen([EXECUTABLE_PATH] + args, 73 | stdin=subprocess.PIPE, 74 | stdout=subprocess.PIPE, 75 | stderr=subprocess.PIPE) 76 | stdoutdata, stderrdata = run.communicate(stdin) 77 | return run.returncode, stdoutdata, stderrdata 78 | 79 | 80 | def generate_brainfuck_code_without_loops( 81 | command_set, 82 | num_commands, 83 | restore_offset=False): 84 | """Generate Brainfuck code (without loops) using the given set of commands. 85 | 86 | Args: 87 | command_set: A sequence containing the commands to use e.g. '<>+-,.'. 88 | May not contain '[' or ']'. 89 | num_commands: The length of the returned string. 90 | restore_offset: If True then there should be no net movement of the 91 | data pointer when the code is finished exited. 92 | 93 | Returns: 94 | A sequence of Brainfuck commands from the given command_set e.g. 95 | '+>-<,,.>><<'. 96 | """ 97 | assert '[' not in command_set 98 | assert ']' not in command_set 99 | 100 | possible_commands = list(command_set) 101 | possible_commands_no_left = list(set(command_set) - set(['<'])) 102 | 103 | commands = [] 104 | offset = 0 105 | while len(commands) < num_commands: 106 | if restore_offset: 107 | offset_fix = offset 108 | if offset_fix and offset_fix >= len(commands) - num_commands + 1: 109 | if offset > 0: 110 | commands.extend('<' * offset_fix) 111 | else: 112 | commands.extend('>' * offset_fix) 113 | offset = 0 114 | continue 115 | 116 | if offset: 117 | command = random.choice(possible_commands) 118 | else: 119 | command = random.choice(possible_commands_no_left) 120 | 121 | if (restore_offset and 122 | num_commands - len(commands) == 1 and 123 | command in '<>'): 124 | continue 125 | 126 | if command == '<': 127 | offset -= 1 128 | elif command == '>': 129 | offset += 1 130 | commands.append(command) 131 | 132 | _check_datapointer_in_range(commands, restore_offset) 133 | assert len(commands) == num_commands 134 | assert set(commands) <= set(command_set) 135 | return ''.join(commands) 136 | 137 | 138 | _LOOP_TEMPLATE = '-[>%s<%s]' 139 | _EMPTY_LOOP_TEMPLATE_LEN = len(_LOOP_TEMPLATE % ('', '')) 140 | 141 | 142 | def generate_brainfuck_code(command_set, num_commands, max_loop_depth): 143 | """Generate Brainfuck code using the given set of commands. 144 | 145 | Args: 146 | command_set: A sequence containing the commands to use e.g. '<>+-,.[]'. 147 | num_commands: The length of the returned string. 148 | max_loop_depth: The maximim level of nesting for loops e.g. 2. 149 | 150 | Returns: 151 | A sequence of Brainfuck commands from the given command_set e.g. 152 | '+--[>,.-[>>>>.<<<<-]<-]><'. 153 | """ 154 | commands_without_loops = ''.join(set(command_set) - set(['[', ']'])) 155 | if '[' not in command_set or ']' not in command_set: 156 | max_loop_depth = 0 157 | 158 | code_blocks = [] 159 | remaining_commands = num_commands 160 | while remaining_commands: 161 | if (max_loop_depth and 162 | # Need one extra space to decrement the loop counter. 163 | remaining_commands >= _EMPTY_LOOP_TEMPLATE_LEN + 1 and 164 | random.choice(['loop', 'code']) == 'loop'): 165 | decrement_space = remaining_commands - _EMPTY_LOOP_TEMPLATE_LEN 166 | loop_decrement = '-' * random.randrange(1, 167 | min(decrement_space+1, 256), 168 | 2) 169 | max_loop_body_size = (remaining_commands - 170 | _EMPTY_LOOP_TEMPLATE_LEN 171 | - len(loop_decrement)) 172 | code_block = _LOOP_TEMPLATE % ( 173 | generate_brainfuck_code( 174 | command_set, 175 | random.randrange(max_loop_body_size + 1), 176 | max_loop_depth - 1), 177 | loop_decrement) 178 | else: 179 | code_block = generate_brainfuck_code_without_loops( 180 | commands_without_loops, 181 | random.randrange(remaining_commands+1), 182 | restore_offset=True) 183 | 184 | remaining_commands -= len(code_block) 185 | code_blocks.append(code_block) 186 | 187 | commands = ''.join(code_blocks) 188 | assert len(commands) == num_commands, ( 189 | 'len(commands) [%d] == num_commands [%d]' % ( 190 | len(commands), num_commands)) 191 | assert set(commands) <= set(command_set) 192 | return commands 193 | 194 | 195 | class TestExecutable(unittest.TestCase): 196 | """Tests the portions of the executable not related to BF interpretation.""" 197 | 198 | def test_no_arguments(self): 199 | returncode, stdout, stderr = run_brainfuck(args=[]) 200 | self.assertEqual(returncode, 1) 201 | self.assertIn('Usage', stdout) 202 | self.assertIn('You need to specify exactly one Brainfuck file', stderr) 203 | 204 | def test_help(self): 205 | returncode, stdout, stderr = run_brainfuck(args=['-h']) 206 | self.assertEqual(returncode, 0) 207 | self.assertIn('Usage', stdout) 208 | self.assertEqual(stderr, '') 209 | 210 | def test_without_mode(self): 211 | test_hello_world = os.path.join(os.curdir, 'examples', 'hello.b') 212 | 213 | returncode, stdout, stderr = run_brainfuck(args=[test_hello_world]) 214 | self.assertEqual(returncode, 0) 215 | self.assertEqual(stdout, 'Hello World!\n') 216 | self.assertEqual(stderr, '') 217 | 218 | def test_with_mode(self): 219 | test_hello_world = os.path.join(os.curdir, 'examples', 'hello.b') 220 | 221 | returncode, stdout, stderr = run_brainfuck( 222 | args=['--mode=jit', test_hello_world]) 223 | self.assertEqual(returncode, 0) 224 | self.assertEqual(stdout, 'Hello World!\n') 225 | self.assertEqual(stderr, '') 226 | 227 | def test_with_bad_mode(self): 228 | test_hello_world = os.path.join(os.curdir, 'examples', 'hello.b') 229 | 230 | returncode, stdout, stderr = run_brainfuck( 231 | args=['--mode=badmode', test_hello_world]) 232 | self.assertEqual(returncode, 1) 233 | self.assertEqual(stdout, '') 234 | self.assertIn('Unexpected mode: --mode=badmode', stderr) 235 | 236 | def test_with_bad_flag(self): 237 | test_hello_world = os.path.join(os.curdir, 'examples', 'hello.b') 238 | 239 | returncode, stdout, stderr = run_brainfuck( 240 | args=['--flag=unknown', test_hello_world]) 241 | self.assertEqual(returncode, 1) 242 | self.assertEqual(stdout, '') 243 | self.assertIn('Unexpected argument: --flag=unknown', stderr) 244 | 245 | def test_with_mode_no_file(self): 246 | returncode, stdout, stderr = run_brainfuck(args=['--mode=jit']) 247 | self.assertEqual(returncode, 1) 248 | self.assertIn('Usage', stdout) 249 | self.assertIn('You need to specify exactly one Brainfuck file', stderr) 250 | 251 | 252 | class BrainfuckRunnerTestMixin(object): 253 | """A abstract class for testing a brainfuck execution mode. 254 | 255 | Subclasses must define a "MODE" class variable corresponding to their mode 256 | flag e.g. "jit". 257 | """ 258 | 259 | MODE = None 260 | 261 | @classmethod 262 | def run_brainfuck(cls, brainfuck_example, stdin=None): 263 | test_brainfuck_path = os.path.join( 264 | os.curdir, 'examples', brainfuck_example) 265 | 266 | return run_brainfuck( 267 | ['--mode=%s' % cls.MODE, test_brainfuck_path], stdin) 268 | 269 | def test_hello_world(self): 270 | returncode, stdout, stderr = self.run_brainfuck('hello.b') 271 | 272 | self.assertEqual(returncode, 0) 273 | self.assertEqual(stdout, 'Hello World!\n') 274 | self.assertEqual(stderr, '') 275 | 276 | def test_cat(self): 277 | returncode, stdout, stderr = self.run_brainfuck( 278 | 'cat.b', 279 | stdin='This should be echoed!') 280 | 281 | self.assertEqual(returncode, 0) 282 | self.assertEqual(stdout, 'This should be echoed!') 283 | self.assertEqual(stderr, '') 284 | 285 | def test_unbalanced_block(self): 286 | returncode, stdout, stderr = self.run_brainfuck('unbalanced_block.b') 287 | 288 | self.assertEqual(returncode, 1) 289 | self.assertEqual(stdout, '') 290 | self.assertIn('Unable to find loop end in block starting with: [++', 291 | stderr) 292 | 293 | def test_extra_block_end(self): 294 | returncode, stdout, stderr = self.run_brainfuck('extra_block_end.b') 295 | 296 | self.assertEqual(returncode, 0) 297 | self.assertEqual(stdout, 'Hello World!\n') 298 | self.assertEqual(stderr, '') 299 | 300 | def test_empty(self): 301 | returncode, stdout, stderr = self.run_brainfuck('empty.b') 302 | 303 | self.assertEqual(returncode, 0) 304 | self.assertEqual(stdout, '') 305 | self.assertEqual(stderr, '') 306 | 307 | def test_regression1(self): 308 | returncode, stdout, stderr = self.run_brainfuck( 309 | 'regression1.b', stdin='mM') 310 | 311 | self.assertEqual(returncode, 0) 312 | self.assertEqual(stdout, 'Mm') 313 | self.assertEqual(stderr, '') 314 | 315 | def test_regression2(self): 316 | returncode, stdout, stderr = self.run_brainfuck('regression2.b') 317 | 318 | self.assertEqual(returncode, 0) 319 | self.assertEqual(stdout, 'Hello World!\n') 320 | self.assertEqual(stderr, '') 321 | 322 | 323 | # pylint: disable=too-few-public-methods 324 | class TestCompileAndGo(unittest.TestCase, BrainfuckRunnerTestMixin): 325 | MODE = 'cag' 326 | 327 | 328 | # pylint: disable=too-few-public-methods 329 | class TestInterpreter(unittest.TestCase, BrainfuckRunnerTestMixin): 330 | MODE = 'i' 331 | 332 | 333 | # pylint: disable=too-few-public-methods 334 | class TestJIT(unittest.TestCase, BrainfuckRunnerTestMixin): 335 | MODE = 'jit' 336 | 337 | 338 | class ConsistentOutputTest(unittest.TestCase): 339 | """Check that the various BrainfuckRunners produce consistent output.""" 340 | 341 | @staticmethod 342 | def _generate_input(length): 343 | return ''.join([chr(random.randrange(256)) for _ in range(length)]) 344 | 345 | def _check_consistency_with_code(self, brainfuck_code, brainfuck_input): 346 | with tempfile.NamedTemporaryFile( 347 | suffix='.b', delete=False) as brainfuck_source_file: 348 | brainfuck_source_file.write(brainfuck_code) 349 | brainfuck_source_file.close() 350 | 351 | stdouts = [] 352 | for klass in [TestCompileAndGo, TestInterpreter, TestJIT]: 353 | returncode, stdout, stderr = klass.run_brainfuck( 354 | brainfuck_source_file.name, stdin=brainfuck_input) 355 | self.assertEqual(returncode, 0) 356 | self.assertEqual(stderr, '') 357 | stdouts.append((klass, stdout)) 358 | 359 | reference_klass, reference_stdout = stdouts[0] 360 | self.longMessage = True # pylint: disable=invalid-name 361 | for klass, stdout in stdouts[1:]: 362 | self.assertSequenceEqual( 363 | reference_stdout, stdout, 364 | 'output for %s does not match output for %s for file %s' % ( 365 | reference_klass.__name__, 366 | klass.__name__, 367 | brainfuck_source_file.name)) 368 | os.unlink(brainfuck_source_file.name) 369 | 370 | @_repeat_for_seconds(2) 371 | def test_consistency_with_random_no_loop_input(self): 372 | brainfuck_code = generate_brainfuck_code_without_loops( 373 | '+-<>,.', 80) 374 | # Can't require more input than the length of the code (without loops). 375 | brainfuck_input = self._generate_input(len(brainfuck_code)) 376 | self._check_consistency_with_code(brainfuck_code, brainfuck_input) 377 | 378 | @_repeat_for_seconds(2) 379 | def test_consistency_with_random_loop_input(self): 380 | brainfuck_code = generate_brainfuck_code('<>+-[].', 80, 2) 381 | self._check_consistency_with_code(brainfuck_code, '') 382 | 383 | if __name__ == '__main__': 384 | unittest.main() 385 | --------------------------------------------------------------------------------