|
#! /usr/bin/env python3 |
|
# Simple Brainfuck REPL interpreter. Written by davidhcefx, 2024.5.25 |
|
from typing import List |
|
import io |
|
import readline |
|
from select import select |
|
import sys |
|
|
|
DEBUG_STEP = False |
|
MAX_INSTRUCTIONS = 2 ** 16 |
|
last_stdin_char = '\n' |
|
|
|
|
|
def interpret(code: str, stdin: io.TextIOWrapper, stdout: io.TextIOWrapper) -> List[int]: |
|
""" |
|
@brief Interpret code and I/O to stdin and stdout. |
|
@return The resulting data array (tape) after execution |
|
@throw SyntaxError when encountered invalid instructions or unmatching brackets |
|
@throw RuntimeError when number of instructions exceeded MAX_INSTRUCTIONS |
|
""" |
|
global last_stdin_char |
|
ins_cnt = 0 # number of executed instructions |
|
cod_idx = 0 # curent code index |
|
dat_idx = 2 # current read/write index |
|
dat_len = 5 # length of data array |
|
data = [0] * dat_len |
|
left_brackets = [] # stack of cod_idx of unmatched left brackets |
|
|
|
while cod_idx < len(code) and ins_cnt < MAX_INSTRUCTIONS: |
|
c = code[cod_idx] |
|
ins_cnt += 1 |
|
|
|
if DEBUG_STEP: |
|
print('#{}'.format(ins_cnt)) |
|
datastr = list(map(str, data)) |
|
datastr[dat_idx] = '-> ' + datastr[dat_idx] |
|
print('Tape: [{}]'.format(', '.join(datastr))) |
|
print('Code: {}'.format(code)) |
|
print(' {}^'.format(' ' * cod_idx)) |
|
|
|
if c in '><': |
|
dat_idx += 1 if c == '>' else -1 |
|
# grows data array twice as large |
|
if dat_idx < 0: |
|
data = ([0] * dat_len) + data |
|
dat_idx += dat_len |
|
dat_len += dat_len |
|
elif dat_idx >= dat_len: |
|
data = data + ([0] * dat_len) |
|
dat_len += dat_len |
|
elif c in '+-': |
|
data[dat_idx] += 1 if c == '+' else -1 |
|
elif c == '.': |
|
stdout.write(chr(data[dat_idx])) |
|
elif c == ',': |
|
last_stdin_char = stdin.read(1) |
|
data[dat_idx] = ord(last_stdin_char) |
|
elif c == '[': |
|
if data[dat_idx]: |
|
left_brackets.append(cod_idx) |
|
else: |
|
# goto end of matching right bracket (NOTE: this is suboptimal) |
|
old_cod_idx = cod_idx |
|
bk = 1 |
|
cod_idx += 1 |
|
while cod_idx < len(code) and bk > 0: |
|
if code[cod_idx] == '[': |
|
bk += 1 |
|
elif code[cod_idx] == ']': |
|
bk -= 1 |
|
cod_idx += 1 |
|
|
|
if bk > 0: |
|
raise SyntaxError('Unmatched opening bracket at index {}'.format(old_cod_idx)) |
|
continue |
|
elif c == ']': |
|
if len(left_brackets) == 0: |
|
raise SyntaxError('Unmatched closing bracket at index {}'.format(cod_idx)) |
|
|
|
if data[dat_idx]: |
|
# goto matching left bracket |
|
cod_idx = left_brackets.pop() |
|
continue |
|
else: |
|
left_brackets.pop() |
|
else: |
|
raise SyntaxError('Bad instruction "{}". Brainf**k only accepts "><+-.,[]"'.format(c)) |
|
|
|
cod_idx += 1 |
|
|
|
if ins_cnt >= MAX_INSTRUCTIONS: |
|
raise RuntimeError('Exceeding maximum number of instructions! ({})'.format(MAX_INSTRUCTIONS)) |
|
|
|
return data |
|
|
|
|
|
if __name__ == '__main__': |
|
for arg in sys.argv[1:]: |
|
if arg == '--debug-step': |
|
DEBUG_STEP = True |
|
|
|
print('Brainfuck REPL intepreter in Python!') |
|
while True: |
|
try: |
|
line = input('> ').strip() |
|
if line == '': # no input |
|
continue |
|
except EOFError: |
|
break |
|
try: |
|
tape = interpret(line, sys.stdin, sys.stdout) |
|
print('\nTape dump: {}'.format(tape)) |
|
# consume trailing newlines from input if any |
|
if last_stdin_char != '\n': |
|
sys.stdin.readline() |
|
last_stdin_char = '\n' |
|
except (SyntaxError, RuntimeError) as e: |
|
print('{}: {}'.format(type(e).__name__, e)) |
|
|
|
print('Bye!') |
|
|
|
# Sample program: |
|
# ,+. input a output b |
|
# ,[>+<--]>. input b output 1 |