|
| 1 | +from __future__ import absolute_import |
| 2 | +from __future__ import division |
| 3 | +from __future__ import print_function |
| 4 | + |
| 5 | +"""BrainF**k interpreter. |
| 6 | +
|
| 7 | +Language info: https://en.wikipedia.org/wiki/Brainfuck |
| 8 | +
|
| 9 | +Based on public implementation: |
| 10 | +https://github.com/pocmo/Python-Brainfuck/blob/master/brainfuck.py |
| 11 | +""" |
| 12 | + |
| 13 | +from collections import namedtuple |
| 14 | +import time |
| 15 | + |
| 16 | + |
| 17 | +EvalResult = namedtuple( |
| 18 | + 'EvalResult', ['output', 'success', 'failure_reason', 'steps', 'time', |
| 19 | + 'memory', 'program_trace']) |
| 20 | + |
| 21 | + |
| 22 | +ExecutionSnapshot = namedtuple( |
| 23 | + 'ExecutionSnapshot', |
| 24 | + ['codeptr', 'codechar', 'memptr', 'memval', 'memory', 'next_input', |
| 25 | + 'output_buffer']) |
| 26 | + |
| 27 | + |
| 28 | +class Status(object): |
| 29 | + SUCCESS = 'success' |
| 30 | + TIMEOUT = 'timeout' |
| 31 | + STEP_LIMIT = 'step-limit' |
| 32 | + SYNTAX_ERROR = 'syntax-error' |
| 33 | + |
| 34 | + |
| 35 | +CHARS = INT_TO_CHAR = ['>', '<', '+', '-', '[', ']', '.', ','] |
| 36 | +CHAR_TO_INT = dict([(c, i) for i, c in enumerate(INT_TO_CHAR)]) |
| 37 | + |
| 38 | + |
| 39 | +class LookAheadIterator(object): |
| 40 | + """Same API as Python iterator, with additional peek method.""" |
| 41 | + |
| 42 | + def __init__(self, iterable): |
| 43 | + self._it = iter(iterable) |
| 44 | + self._current_element = None |
| 45 | + self._done = False |
| 46 | + self._preload_next() |
| 47 | + |
| 48 | + def _preload_next(self): |
| 49 | + try: |
| 50 | + self._current_element = self._it.next() |
| 51 | + except StopIteration: |
| 52 | + self._done = True |
| 53 | + |
| 54 | + def next(self): |
| 55 | + if self._done: |
| 56 | + raise StopIteration |
| 57 | + element = self._current_element |
| 58 | + self._preload_next() |
| 59 | + return element |
| 60 | + |
| 61 | + def peek(self, default_value=None): |
| 62 | + if self._done: |
| 63 | + if default_value is None: |
| 64 | + raise StopIteration |
| 65 | + return default_value |
| 66 | + return self._current_element |
| 67 | + |
| 68 | + |
| 69 | +def buildbracemap(code): |
| 70 | + """Build jump map. |
| 71 | +
|
| 72 | + Args: |
| 73 | + code: List or string or BF chars. |
| 74 | +
|
| 75 | + Returns: |
| 76 | + bracemap: dict mapping open and close brace positions in the code to their |
| 77 | + destination jumps. Specifically, positions of matching open/close braces |
| 78 | + if they exist. |
| 79 | + correct_syntax: True if all braces match. False if there are unmatched |
| 80 | + braces in the code. Even if there are unmatched braces, a bracemap will |
| 81 | + be built, and unmatched braces will map to themselves. |
| 82 | + """ |
| 83 | + bracestack, bracemap = [], {} |
| 84 | + |
| 85 | + correct_syntax = True |
| 86 | + for position, command in enumerate(code): |
| 87 | + if command == '[': |
| 88 | + bracestack.append(position) |
| 89 | + if command == ']': |
| 90 | + if not bracestack: # Unmatched closing brace. |
| 91 | + bracemap[position] = position # Don't jump to any position. |
| 92 | + correct_syntax = False |
| 93 | + continue |
| 94 | + start = bracestack.pop() |
| 95 | + bracemap[start] = position |
| 96 | + bracemap[position] = start |
| 97 | + if bracestack: # Unmatched opening braces. |
| 98 | + for pos in bracestack: |
| 99 | + bracemap[pos] = pos # Don't jump to any position. |
| 100 | + correct_syntax = False |
| 101 | + return bracemap, correct_syntax |
| 102 | + |
| 103 | + |
| 104 | +def evaluate(code, input_buffer=None, init_memory=None, base=256, timeout=1.0, |
| 105 | + max_steps=None, require_correct_syntax=True, output_memory=False, |
| 106 | + debug=False): |
| 107 | + """Execute BF code. |
| 108 | +
|
| 109 | + Args: |
| 110 | + code: String or list of BF characters. Any character not in CHARS will be |
| 111 | + ignored. |
| 112 | + input_buffer: A list of ints which will be used as the program's input |
| 113 | + stream. Each read op "," will read an int from this list. 0's will be |
| 114 | + read once the end of the list is reached, or if no input buffer is |
| 115 | + given. |
| 116 | + init_memory: A list of ints. Memory for first k positions will be |
| 117 | + initialized to this list (where k = len(init_memory)). Memory positions |
| 118 | + are initialized to 0 by default. |
| 119 | + base: Integer base for the memory. When a memory value is incremented to |
| 120 | + `base` it will overflow to 0. When a memory value is decremented to -1 |
| 121 | + it will underflow to `base` - 1. |
| 122 | + timeout: Time limit for program execution in seconds. Set to None to |
| 123 | + disable. |
| 124 | + max_steps: Execution step limit. An execution step is the execution of one |
| 125 | + operation (code character), even if that op has been executed before. |
| 126 | + Execution exits when this many steps are reached. Set to None to |
| 127 | + disable. Disabled by default. |
| 128 | + require_correct_syntax: If True, unmatched braces will cause `evaluate` to |
| 129 | + return without executing the code. The failure reason will be |
| 130 | + `Status.SYNTAX_ERROR`. If False, unmatched braces are ignored |
| 131 | + and execution will continue. |
| 132 | + output_memory: If True, the state of the memory at the end of execution is |
| 133 | + returned. |
| 134 | + debug: If True, then a full program trace will be returned. |
| 135 | +
|
| 136 | + Returns: |
| 137 | + EvalResult namedtuple containing |
| 138 | + output: List of ints which were written out by the program with the "." |
| 139 | + operation. |
| 140 | + success: Boolean. Whether execution completed successfully. |
| 141 | + failure_reason: One of the attributes of `Status`. Gives extra info |
| 142 | + about why execution was not successful. |
| 143 | + steps: Number of execution steps the program ran for. |
| 144 | + time: Amount of time in seconds the program ran for. |
| 145 | + memory: If `output_memory` is True, a list of memory cells up to the last |
| 146 | + one written to. otherwise, None. |
| 147 | + """ |
| 148 | + input_iter = ( |
| 149 | + LookAheadIterator(input_buffer) if input_buffer is not None |
| 150 | + else LookAheadIterator([])) |
| 151 | + |
| 152 | + # Null memory value. This is the value of an empty memory. Also the value |
| 153 | + # returned by the read operation when the input buffer is empty, or the |
| 154 | + # end of the buffer is reached. |
| 155 | + null_value = 0 |
| 156 | + |
| 157 | + code = list(code) |
| 158 | + bracemap, correct_syntax = buildbracemap(code) # will modify code list |
| 159 | + if require_correct_syntax and not correct_syntax: |
| 160 | + return EvalResult([], False, Status.SYNTAX_ERROR, 0, 0.0, |
| 161 | + [] if output_memory else None, [] if debug else None) |
| 162 | + |
| 163 | + output_buffer = [] |
| 164 | + |
| 165 | + codeptr, cellptr = 0, 0 |
| 166 | + |
| 167 | + cells = list(init_memory) if init_memory else [0] |
| 168 | + |
| 169 | + program_trace = [] if debug else None |
| 170 | + success = True |
| 171 | + reason = Status.SUCCESS |
| 172 | + start_time = time.time() |
| 173 | + steps = 0 |
| 174 | + while codeptr < len(code): |
| 175 | + command = code[codeptr] |
| 176 | + |
| 177 | + if debug: |
| 178 | + # Add step to program trace. |
| 179 | + program_trace.append(ExecutionSnapshot( |
| 180 | + codeptr=codeptr, codechar=command, memptr=cellptr, |
| 181 | + memval=cells[cellptr], memory=list(cells), |
| 182 | + next_input=input_iter.peek(null_value), |
| 183 | + output_buffer=list(output_buffer))) |
| 184 | + |
| 185 | + if command == '>': |
| 186 | + cellptr += 1 |
| 187 | + if cellptr == len(cells): cells.append(null_value) |
| 188 | + |
| 189 | + if command == '<': |
| 190 | + cellptr = 0 if cellptr <= 0 else cellptr - 1 |
| 191 | + |
| 192 | + if command == '+': |
| 193 | + cells[cellptr] = cells[cellptr] + 1 if cells[cellptr] < (base - 1) else 0 |
| 194 | + |
| 195 | + if command == '-': |
| 196 | + cells[cellptr] = cells[cellptr] - 1 if cells[cellptr] > 0 else (base - 1) |
| 197 | + |
| 198 | + if command == '[' and cells[cellptr] == 0: codeptr = bracemap[codeptr] |
| 199 | + if command == ']' and cells[cellptr] != 0: codeptr = bracemap[codeptr] |
| 200 | + |
| 201 | + if command == '.': output_buffer.append(cells[cellptr]) |
| 202 | + if command == ',': cells[cellptr] = next(input_iter, null_value) |
| 203 | + |
| 204 | + codeptr += 1 |
| 205 | + steps += 1 |
| 206 | + |
| 207 | + if timeout is not None and time.time() - start_time > timeout: |
| 208 | + success = False |
| 209 | + reason = Status.TIMEOUT |
| 210 | + break |
| 211 | + if max_steps is not None and steps >= max_steps: |
| 212 | + success = False |
| 213 | + reason = Status.STEP_LIMIT |
| 214 | + break |
| 215 | + |
| 216 | + if debug: |
| 217 | + # Add step to program trace. |
| 218 | + command = code[codeptr] if codeptr < len(code) else '' |
| 219 | + program_trace.append(ExecutionSnapshot( |
| 220 | + codeptr=codeptr, codechar=command, memptr=cellptr, |
| 221 | + memval=cells[cellptr], memory=list(cells), |
| 222 | + next_input=input_iter.peek(null_value), |
| 223 | + output_buffer=list(output_buffer))) |
| 224 | + |
| 225 | + return EvalResult( |
| 226 | + output=output_buffer, |
| 227 | + success=success, |
| 228 | + failure_reason=reason, |
| 229 | + steps=steps, |
| 230 | + time=time.time() - start_time, |
| 231 | + memory=cells if output_memory else None, |
| 232 | + program_trace=program_trace) |
| 233 | + |
| 234 | + |
0 commit comments