Implementation of a Brainfuck interpreter in Python.

Execute Brain****/Python is an implementation of Brainf***. Other implementations of Brainf***.
Execute Brain****/Python is part of RCBF. You may find other members of RCBF at Category:RCBF.

Usage

prompt > python bfi.py -h
usage: bfi.py [-h] [-e EOF] [-w WRAP] [-B BOUND] infile

positional arguments:
  infile

options:
  -h, --help            show this help message and exit
  -e, --eof EOF         Configure the EOF type; EOF is unchanged (default):zero:negative_one
  -w, --wrap WRAP       Configure cell wrapping size. WRAP is an int
  -B, --bound BOUND     Limits tape length to BOUND (BOUND is an int). Default: unbounded

Code

Works with: Python version 3.10
from __future__ import annotations

import sys
from collections import defaultdict

def mod(a, b):
    r = abs(a % b)
    if a < 0: r = -r
    return r

class Brainfuck:
    """A Brainf*** interpreter."""

    def __init__(self, source: str, eof:str, wrap:str, bound:str):
        self.source = source
        self.eof = eof
        self.wrap = int(wrap)
        self.bound = int(bound)
        self.jump_map = self._jump_map(source)

    @staticmethod
    def compile(source: str) -> Brainfuck:
        """Preprocess Brainf*** source code."""
        return Brainfuck(source)

    @staticmethod
    def execute(source: str, eof: str, wrap: str, bound: str) -> None:
        """Compile and run Brainf*** source code."""
        Brainfuck(source, eof, wrap, bound).run()

    def run(self) -> None:
        """Run the interpreter."""
        tape: defaultdict[int, int] = defaultdict(int)
        cell = 0
        instruction_pointer = 0

        while instruction_pointer < len(self.source):
            op = self.source[instruction_pointer]

            if op == ">":
                cell += 1
            elif op == "<":
                cell -= 1
            elif op == "+":
                tape[cell] += 1
            elif op == "-":
                tape[cell] -= 1
            elif op == ",":
                i = sys.stdin.read(1)
                if self.eof == 'unchanged':
                    if i != '':
                        tape[cell] = ord(i)
                elif self.eof == 'zero':
                    if i == '':
                        tape[cell] = 0
                    else:
                        tape[cell] = ord(i)
                elif self.eof == 'negative_one':
                    if i == '':
                        tape[cell] = -1
                    else:
                        tape[cell] = ord(i)
            elif op == ".":
                sys.stdout.write(chr(tape[cell]))
            elif op == '#':
                print(cell, end=': [')
                for i in range(cell-9,cell+1):
                    print(tape[i],end=' ')
                print(']')
            elif (op == "[" and not tape[cell]) or (op == "]" and tape[cell]):
                instruction_pointer = self.jump_map[instruction_pointer]

            if self.wrap != 0:
                if tape[cell] < 0:
                    tape[cell] += self.wrap
                tape[cell] = mod(tape[cell], self.wrap)
            
            instruction_pointer += 1
            if self.bound != 0:
                if cell < 0:
                    cell += self.bound
                cell = mod(cell, self.bound)
                

    def _jump_map(self, source: str) -> dict[int, int]:
        """Precalculate loop start and end indexes."""
        indexes: dict[int, int] = {}
        stack: list[int] = []

        for i, op in enumerate(source):
            if op == "[":
                stack.append(i)
            if op == "]":
                if not stack:
                    source = source[:i]
                    break
                index = stack.pop()
                indexes[i], indexes[index] = index, i

        if stack:
            raise SyntaxError("unclosed loops at {}".format(stack))

        return indexes


if __name__ == "__main__":
    import argparse

    # Pass `-` on the command line to read from the standard input stream.
    parser = argparse.ArgumentParser()
    parser.add_argument("infile", type=argparse.FileType("r"))
    parser.add_argument("-e", "--eof", default="unchanged",
                        help="Configure the EOF type; EOF is unchanged "
                        "(default):zero:negative_one")
    parser.add_argument("-w", "--wrap", default="0",
                        help="Configure cell wrapping size. WRAP is an int")
    parser.add_argument("-B", "--bound", default="0",
                        help="Limits tape length to BOUND (BOUND is an int). "
                        "Default: unbounded")
    args = parser.parse_args()
    eoftype = args.eof
    Brainfuck.execute(args.infile.read(), eoftype, args.wrap,
                      args.bound)

Sample run (NYYRIKKI's interpreter):

prompt > python bfi.py -w 256 nbfi.b    
++++++++++[>+>+++>++++>+++++++>++++++++>+++++++++>++++++++++>+++++++++++>++++++++++++<<<<<<<<<-]>>>>++.>>>+.+++++++..>+.<<<<<<++.>>>>>>>-.<.+++.<.--------.<<<<<+.<+++.---.:
Hello world!

A program which I used to test tape pointer wrapping:

[ 0 0 0 0 0 64
          ^     ]

++[->++<]>[->++<]>[->++<]>[->++<]>[->++<]> #


<<<<<< #.

<<<<<< .
Output:
prompt > python bfi.py -B 6 test_bound.b
5: [0 0 0 0 0 0 0 0 0 64 ]
5: [0 0 0 0 0 0 0 0 0 64 ]
@@

Testing right bound:

[ 0 0 0 0 0 64
            ^  ]

++[->++<]>[->++<]>[->++<]>[->++<]>[->++<]> #
>>>>>> #.
>>>>>> .
Output:

Same as above.


Wrapping test:

-- #+++ #

< - >#
Output:
prompt> python bfi.py -w 256 test.b   
0: [0 0 0 0 0 0 0 0 0 254 ]
0: [0 0 0 0 0 0 0 0 0 1 ]
0: [0 0 0 0 0 0 0 0 255 1 ]