First working state
This commit is contained in:
106
src/brainfuck/__brainfuck.py
Normal file
106
src/brainfuck/__brainfuck.py
Normal file
@@ -0,0 +1,106 @@
|
||||
from sys import stdin, stdout
|
||||
from os import PathLike
|
||||
from enterpreter import Enterpreter, opcode
|
||||
|
||||
|
||||
BrainFuckByteCode = {
|
||||
">": 1,
|
||||
"<": 2,
|
||||
"+": 3,
|
||||
"-": 4,
|
||||
".": 5,
|
||||
",": 6,
|
||||
"[": 7,
|
||||
"]": 8,
|
||||
}
|
||||
|
||||
|
||||
class BrainFuckByteCodeCompiler:
|
||||
def load_file(self, file: PathLike):
|
||||
self.source_file: PathLike = file
|
||||
self.load(bytearray(Path(file).read_bytes()))
|
||||
|
||||
def load(self, data: bytearray):
|
||||
self.source_data: bytearray = data
|
||||
|
||||
def compile(self) -> bytearray:
|
||||
# whitespace_b = whitespace.encode()
|
||||
valid_b = set(ord(s) for s in BrainFuckByteCode.keys())
|
||||
self.source_data = bytearray(b for b in self.source_data if b in valid_b)
|
||||
|
||||
out = bytearray()
|
||||
for byte in self.source_data:
|
||||
out.append(BrainFuckByteCode[chr(byte)])
|
||||
return out
|
||||
|
||||
|
||||
class BrainFuckEnterpreter(Enterpreter):
|
||||
@opcode(BrainFuckByteCode[">"])
|
||||
def a_(self):
|
||||
self._memory_pointer += 1
|
||||
|
||||
@opcode(BrainFuckByteCode["<"])
|
||||
def b_(self):
|
||||
self._memory_pointer -= 1
|
||||
|
||||
@opcode(BrainFuckByteCode["+"])
|
||||
def c_(self):
|
||||
val = self._memory[self._memory_pointer]
|
||||
self._memory[self._memory_pointer] = (val + 1) % 256
|
||||
|
||||
@opcode(BrainFuckByteCode["-"])
|
||||
def d_(self):
|
||||
val = self._memory[self._memory_pointer]
|
||||
self._memory[self._memory_pointer] = (val - 1) & 255
|
||||
|
||||
@opcode(BrainFuckByteCode["."])
|
||||
def e_(self):
|
||||
stdout.write(chr(self._memory[self._memory_pointer]))
|
||||
|
||||
@opcode(BrainFuckByteCode[","])
|
||||
def f_(self):
|
||||
stdout.flush()
|
||||
self._memory[self._memory_pointer] = ord(stdin.read(1))
|
||||
|
||||
@opcode(BrainFuckByteCode["["])
|
||||
def g_(self):
|
||||
# Ignore opcode if mem_pointer == 0
|
||||
if self._memory[self._memory_pointer] > 0:
|
||||
pass
|
||||
elif self._memory[self._memory_pointer] == 0:
|
||||
self._program_counter = self.findNext() - 1
|
||||
|
||||
@opcode(BrainFuckByteCode["]"])
|
||||
def h_(self):
|
||||
self._program_counter = self.findNext(-1)
|
||||
|
||||
def findNext(self, incrementValue: int = 1) -> int:
|
||||
search_pointer = self._program_counter + incrementValue
|
||||
bracket_counter = 1
|
||||
while bracket_counter != 0:
|
||||
opcode = self._memory[search_pointer]
|
||||
if opcode == BrainFuckByteCode["["]:
|
||||
bracket_counter += incrementValue
|
||||
elif opcode == BrainFuckByteCode["]"]:
|
||||
bracket_counter -= incrementValue
|
||||
search_pointer += incrementValue
|
||||
return search_pointer
|
||||
|
||||
|
||||
def bf(program_path: PathLike, bits=8, mem=2**8) -> BrainFuckEnterpreter:
|
||||
bf_c = BrainFuckByteCodeCompiler()
|
||||
bf_c.load_file(program_path)
|
||||
program = bf_c.compile()
|
||||
|
||||
bf_e = BrainFuckEnterpreter(bits=bits, memsize=mem)
|
||||
bf_e.load_program(program)
|
||||
bf_e.run()
|
||||
|
||||
return bf_e
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from pathlib import Path
|
||||
|
||||
bf(Path("examples", "HelloWorld.bf"))
|
||||
bf(Path("examples", "GameOfLife.bf"), bits=16, mem=2**16)
|
||||
0
src/brainfuck/__init__.py
Normal file
0
src/brainfuck/__init__.py
Normal file
0
src/brainfuck/__main__.py
Normal file
0
src/brainfuck/__main__.py
Normal file
72
src/enterpreter/__enterpreter.py
Normal file
72
src/enterpreter/__enterpreter.py
Normal file
@@ -0,0 +1,72 @@
|
||||
from collections.abc import Callable
|
||||
from inspect import getmembers
|
||||
from typing import TypeAlias
|
||||
from traceback import print_exception
|
||||
from pprint import pprint
|
||||
|
||||
from .util import hexdump
|
||||
|
||||
Operation: TypeAlias = str | int
|
||||
Subroutine: TypeAlias = Callable
|
||||
|
||||
OPCODE_HEADER = "__enterpreter__opcode"
|
||||
|
||||
|
||||
def opcode(opcode: Operation):
|
||||
def _helper(func: Callable):
|
||||
setattr(func, OPCODE_HEADER, opcode)
|
||||
return func
|
||||
|
||||
return _helper
|
||||
|
||||
|
||||
class Enterpreter:
|
||||
def __init__(self, bits=8, memsize=256) -> None:
|
||||
self.__bits = bits
|
||||
self.__memory_max_size = 2**self.__bits
|
||||
self.__memory_size = min(memsize, self.__memory_max_size)
|
||||
|
||||
self._memory = bytearray(self.__memory_size)
|
||||
self._memory_pointer: int = 0
|
||||
|
||||
self._program_counter: int = 0
|
||||
|
||||
self.__operations: dict[Operation, Subroutine] = dict()
|
||||
self.__init_opcodes()
|
||||
|
||||
def __init_opcodes(self):
|
||||
for func_name, func in getmembers(
|
||||
self, lambda obj: hasattr(obj, OPCODE_HEADER)
|
||||
):
|
||||
self._register_opcode(getattr(func, OPCODE_HEADER), func)
|
||||
|
||||
def _register_opcode(self, opcode: Operation, callback: Subroutine):
|
||||
self.__operations[opcode] = callback
|
||||
|
||||
def is_running(self) -> bool:
|
||||
return self._memory[self._program_counter] != 0
|
||||
|
||||
def run(self) -> None:
|
||||
try:
|
||||
# Keep running until you reach a null byte
|
||||
while self.is_running():
|
||||
opcode = self._memory[self._program_counter]
|
||||
self.__operations[opcode]()
|
||||
self._program_counter += 1
|
||||
except Exception as e:
|
||||
self.dump()
|
||||
print_exception(e)
|
||||
|
||||
def dump(self):
|
||||
hexdump(
|
||||
data=self._memory,
|
||||
program_counter=self._program_counter,
|
||||
memory_pointer=self._memory_pointer,
|
||||
)
|
||||
pprint(self.__operations)
|
||||
|
||||
def load_program(self, program: bytearray) -> None:
|
||||
self._memory[: len(program)] = program[:]
|
||||
|
||||
# Set new pointer immediately after code
|
||||
self._memory_pointer = len(program) + 1
|
||||
3
src/enterpreter/__init__.py
Normal file
3
src/enterpreter/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .__enterpreter import Enterpreter, opcode
|
||||
|
||||
__all__ = ["Enterpreter", "opcode"]
|
||||
0
src/enterpreter/__main__.py
Normal file
0
src/enterpreter/__main__.py
Normal file
3
src/enterpreter/util/__init__.py
Normal file
3
src/enterpreter/util/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .hexdump import hexdump
|
||||
|
||||
__all__ = ["hexdump"]
|
||||
45
src/enterpreter/util/hexdump.py
Normal file
45
src/enterpreter/util/hexdump.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from colorama import Fore, Style, init
|
||||
|
||||
# Initialize colorama (especially important on Windows)
|
||||
init()
|
||||
|
||||
|
||||
def hexdump(
|
||||
data: bytearray,
|
||||
width: int = 16,
|
||||
program_counter: int = None,
|
||||
memory_pointer: int = None,
|
||||
):
|
||||
"""
|
||||
Print a hex+ASCII memory dump of the given bytearray.
|
||||
Highlights:
|
||||
- Program counter (PC) in bright red
|
||||
- Memory pointer (MP) in bright blue
|
||||
"""
|
||||
for i in range(0, len(data), width):
|
||||
chunk = data[i : i + width]
|
||||
hex_parts = []
|
||||
ascii_parts = []
|
||||
|
||||
for j, byte in enumerate(chunk):
|
||||
addr = i + j
|
||||
|
||||
# Apply color based on PC or MP
|
||||
if addr == program_counter:
|
||||
style = Style.BRIGHT + Fore.RED
|
||||
elif addr == memory_pointer:
|
||||
style = Style.BRIGHT + Fore.BLUE
|
||||
else:
|
||||
style = ""
|
||||
reset = Style.RESET_ALL if style else ""
|
||||
|
||||
# Hex and ASCII views
|
||||
hex_parts.append(f"{style}{byte:02x}{reset}")
|
||||
ascii_parts.append(
|
||||
f"{style}{chr(byte) if 32 <= byte <= 126 else '.'}{reset}"
|
||||
)
|
||||
|
||||
# Format line: address, hex bytes, ASCII chars
|
||||
hex_field = " ".join(hex_parts).ljust(width * 3 - 1)
|
||||
ascii_field = "".join(ascii_parts)
|
||||
print(f"{i:08x}: {hex_field} {ascii_field}")
|
||||
Reference in New Issue
Block a user